Canviq MCP and AI Agent Integration Guide¶
Complete reference for connecting AI agents and automated pipelines to Canviq via the Model Context Protocol (MCP) server.
Overview¶
The Canviq MCP server exposes every capability available in the dashboard as an MCP tool. Agents can create surveys, query PMF scores, list responses, and trigger automation workflows. All operations use the same authentication layer as the REST API.
Server URL: https://canviq.app/api/mcp
Transport: Plain HTTP + JSON. POST /api/mcp/tools calls a tool; GET /api/mcp/health is a health check. No persistent connection or streaming transport is used.
Request body: { "tool": "...", "arguments": { ... } }
Authentication¶
!!! warning "This guide requires an agent-backed key" MCP tool calls only accept API keys created from the Agent Management page (/admin/settings/agents). Keys created through the generic Settings > API Keys wizard (including ones where you pick "MCP" as the key type) and SDK keys are not linked to an agent and are rejected by /api/mcp/tools with 401 Invalid API key, even though the key itself is valid elsewhere. See Key format below for how to tell keys apart.
Creating an API key¶
- Sign in to your Canviq dashboard.
- Go to Settings > Agents (
/admin/settings/agents). - Create an agent, or open an existing one.
- On the agent's own page, click Generate key. Choose the environment:
- Live (
pk_live_...): Accesses production data - Test (
pk_test_...): Accesses sandbox data only - Assign a policy to the agent (see Permission scopes) if it doesn't have one already.
- Copy the key. It is shown exactly once and cannot be retrieved again.
Store the key in a secrets manager (AWS Secrets Manager, Vault, 1Password, etc.). Never commit it to source control.
Using the key¶
Pass the key in the Authorization header on every request:
For MCP clients that use the api-key configuration field, set:
Permission scopes¶
| Scope | What it allows |
|---|---|
surveys:read | List and view surveys, responses, and triggers |
surveys:write | Create, update, publish, pause, and archive surveys, questions, and triggers |
pmf:read | View PMF scores and segment breakdowns |
* | Full access to every tool. High-privilege; grant sparingly. |
Read-only scopes (surveys:read, pmf:read) are available on the Free tier. The write scope (surveys:write) requires the Growth plan.
Key format¶
Keys follow the pattern pk_{env}_{32-char-base64url-secret}. The first 14 characters are used for O(1) database lookup. The full key is verified against an Argon2id hash stored at rest. The plaintext is never persisted.
This exact format is shared by all three key types Canviq issues: REST keys, SDK keys, and agent MCP keys. The prefix never tells you which one you have: a pk_live_ key could be any of the three. The only reliable way to know is where you created it. The generic API Keys wizard produces REST or SDK keys, and Agent Management produces the agent-backed MCP keys this guide requires. See AI Agents > Authentication > Key types for the full breakdown of which key type authenticates against which endpoint.
Rate limits¶
| Tier | Requests/minute |
|---|---|
| Standard | 30 |
| Growth | 300 |
| Scale | 1,000 |
See the Rate Limits page for how a tier is assigned to an agent and the full error contract.
When you exceed the rate limit, the server returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait. Use exponential backoff for retries:
1st retry: wait Retry-After seconds
2nd retry: wait 2× Retry-After seconds
3rd retry: wait 4× Retry-After seconds
Request format¶
Every tool call is a single JSON object with a tool name and an arguments object matching that tool's input schema. arguments is validated before the tool executes.
{
"tool": "survey_create",
"arguments": {
"title": "Feature Satisfaction Survey",
"questions": [
{
"type": "rating",
"text": "How satisfied are you with the new dashboard?",
"scale": { "min": 1, "max": 5 }
},
{
"type": "text",
"text": "What could we improve?",
"optional": true
}
]
}
}
Canviq does not accept free-text prompts at this endpoint. If you connect an MCP-compatible AI client (Claude Desktop, Claude Code, Cursor), that client is what turns a plain-English request into the structured call above before it ever reaches Canviq.
Tool reference¶
Survey management¶
survey_create¶
Creates a new survey.
Required scope: surveys:write
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Survey display name (max 200 characters) |
survey_type | string | No | generic (default) or pmf |
status | string | No | draft (default) or published |
description | string | No | Optional survey description |
settings | object | No | Optional custom survey settings (JSON object) |
questions | array | No | Question definitions (see below); defaults to none |
Question types:
| Type | Description |
|---|---|
text | Open-ended text response |
rating | 1–5 or 1–10 emoji or numeric scale |
multiple_choice | Single or multi-select list of options |
yes_no | Binary yes/no response |
survey_create's embedded questions schema only accepts these four values today (passing text passes JSON schema validation but then fails at the database layer with an enum constraint violation; it is not a working free-text question and will raise an error at runtime - this is the bug #5630 tracks). Adding a question with question_create, or changing its type with question_update, accepts the full 8-value survey_question_type enum instead: multiple_choice, rating, free_text, yes_no, matrix, ranking, image_selection, file_upload. Aligning survey_create's embedded schema with the full enum is tracked in #5630.
Returns: SurveyPayload (the created survey including its generated ID).
survey_update¶
Updates a survey's title, description, or settings. Does not change survey status; use survey_publish, survey_pause, or survey_archive for status transitions. settings is merged into the existing settings object (keys you omit are left unchanged), not replaced wholesale. Archived surveys cannot be updated.
Required scope: surveys:write
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
survey_id | string | Yes | UUID of the survey to update |
title | string | No | New survey title (max 200 characters) |
description | string | No | New survey description |
settings | object | No | Settings to merge into the existing settings object |
Returns: { survey_id, updated } (confirmation only; survey fields are not echoed back).
survey_publish¶
Publishes a survey so it is live and accepting responses. Valid from draft or paused status. Rejects the request if the survey has zero questions.
Required scope: surveys:write
Input: { "survey_id": "string" }
Returns: { survey_id, status, published_at } (the survey ID, its new published status, and the publish timestamp).
survey_pause¶
Pauses a published survey so it stops accepting responses. Valid only from published status. Existing responses are preserved and the survey can be published again later.
Required scope: surveys:write
Input: { "survey_id": "string" }
Returns: { survey_id, status } (the survey ID and its new paused status).
survey_archive¶
Archives a survey. Valid from any non-archived status.
Warning: archiving is irreversible. An archived survey cannot be moved to any other status through the MCP tools or the admin dashboard, and its responses are not deleted.
Required scope: surveys:write
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
survey_id | string | Yes | UUID of the survey to archive |
reason | string | No | Optional reason for archiving, recorded for audit purposes |
Returns: { survey_id, status, reason } (the survey ID, its new archived status, and the reason if one was provided).
survey_list¶
Lists surveys for the organization, or fetches a single survey by ID.
Required scope: surveys:read
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | No | Filter to a single survey by ID. Returns a not-found error if no survey matches. |
status | string | No | Filter by status: draft, published, paused, or archived |
limit | number | No | Max results to return (default 50, max 200) |
Returns: { type: 'platform_data', value: SurveyRow[] } (the matching surveys, each with id, title, status, created_at, and response_count; wrapped as platform data per ADR-0019).
PMF quickstart¶
pmf_create¶
Creates and publishes a complete PMF survey (the four standard PMF questions) for the organization in a single call. This replaces the multi-step sequence of calling survey_create, then question_create four times, then survey_publish, and optionally trigger_create. Returns a structured error (not an exception) if a PMF survey already exists for this organization and environment, so the agent can look up and reuse the existing survey instead of creating a duplicate.
Required scope: surveys:write
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
product_name | string | No | Product name used to personalize question text. Defaults to "your product". |
publish | boolean | No | Whether to publish the survey immediately. Defaults to true. |
trigger_event | string | No | Event name that fires the survey. When provided, also creates an event trigger (type: 'event') alongside the survey. |
trigger_min_count | integer | No | Minimum event count before the trigger fires. Defaults to 1. Out-of-range values are silently clamped rather than rejected: values below 1 are raised to 1. Non-integer values are also silently floored via Math.floor, even when already in range (e.g. 2.7 becomes 2). |
cooldown_days | integer | No | Days before the same user can be surveyed again. Defaults to 90. Out-of-range values are silently clamped rather than rejected: negative values are raised to 0. Non-integer values are also silently floored via Math.floor, even when already in range (e.g. 2.7 becomes 2). |
Returns: { survey_id, question_ids } (also includes trigger_id when trigger_event was provided; returns { error } instead when a PMF survey already exists for this organization and environment).
Question management¶
question_create¶
Adds a new question to an existing survey owned by the organization.
Required scope: surveys:write
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
survey_id | string | Yes | UUID of the survey to add the question to |
text | string | Yes | Question text (max 1,000 characters) |
type | string | Yes | multiple_choice, rating, free_text, yes_no, matrix, ranking, image_selection, or file_upload |
config | object | No | Optional JSONB config (scoring flags, choices list, etc.) |
sort_order | number | No | Explicit position: a non-negative integer up to 2147483647. Omitted values append after the last question. |
Returns: { id, survey_id, type, text, sort_order, created_at } (the created question row).
question_update¶
Updates a question's text, type, and/or config. Does not accept sort_order; use questions_reorder to change a question's position.
Required scope: surveys:write
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
question_id | string | Yes | UUID of the question to update |
text | string | No | Updated question text (max 1,000 characters) |
type | string | No | Updated question type: same 8 values as question_create |
config | object | No | Updated JSONB config (replaces the existing config entirely) |
Returns: { success: true } (confirmation only; a call with no fields to update is a no-op and still returns success).
question_delete¶
Deletes a question from a survey. Rejects the request if the question is the last one remaining on a published or paused survey, so a live survey is never left with zero questions.
Required scope: surveys:write
Input: { "question_id": "string" }
Returns: { success: true } (confirmation that the question was deleted).
questions_reorder¶
Reorders a survey's questions by providing the full list of question IDs in the desired order. The list must include every question on the survey exactly once. Partial lists, duplicate IDs, and IDs belonging to a different survey are all rejected with no changes applied.
Required scope: surveys:write
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
survey_id | string | Yes | UUID of the survey whose questions are being reordered |
question_ids | array | Yes | Question UUIDs in the desired order; must cover every question on the survey exactly once. sort_order values 0..N-1 are assigned positionally. |
Returns: { success: true } (confirmation that the new order was applied).
Response access¶
response_list¶
Lists responses for a survey owned by the organization, most recent first.
Required scope: surveys:read
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
survey_id | string | Yes | UUID of the survey |
limit | number | No | Max results (default 50, max 200) |
offset | number | No | Pagination offset (default 0) |
Returns: { type: 'user_content', value: ResponseRecord[] } (each response's id, status, completed_at, started_at, and its survey_answers, an array of { question_id, value }; wrapped as user-generated content per ADR-0019 since response text is caller-supplied).
Date-range filtering, answer-value filtering, and cursor-based pagination described in earlier drafts of this guide are not implemented. See #93.
Analytics¶
get_pmf_score¶
Returns the PMF score (the percentage of respondents who answered "very disappointed") for a survey. When fewer than 30 enriched responses exist, returns a below-threshold state instead of a score. This 30-response minimum is the same PMF_MIN_RESPONSES constant that pmf_segments_list (below) uses; the two tools read from one shared source of truth, so this note and that one describe the same gate and should be updated together.
Required scope: pmf:read
Input: { "survey_id": "string" }
Returns: PmfScoreResult ({ insufficient_responses: false, score, very_disappointed, somewhat_disappointed, not_disappointed, total, threshold } when 30 or more enriched responses exist; otherwise { insufficient_responses: true, reason: 'insufficient_responses', count, threshold, message }).
pmf_segments_list¶
Returns the per-cohort PMF segment breakdown for a survey: very_disappointed, somewhat_disappointed, and not_disappointed, each with a count and a percentage. Uses the same 30-response PMF_MIN_RESPONSES threshold as get_pmf_score (above); the two tools read from one shared constant, so this note and that one cannot silently diverge from each other.
Required scope: pmf:read
Input: { "survey_id": "string" }
Returns: PmfSegmentsResult ({ type: 'platform_data', value: { survey_id, total_responses, segments: [{ cohort, count, pct }], pmf_score, at_pmf } } when 30 or more enriched responses exist; otherwise the same unwrapped below-threshold shape as get_pmf_score).
Time-series PMF trend and free-text sentiment analysis described in earlier drafts of this guide are not implemented. See #94.
Distribution¶
trigger_create¶
Creates a trigger that determines when a survey is shown to users.
Required scope: surveys:write
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
survey_id | string | Yes | UUID of the survey to attach the trigger to |
type | string | Yes | event, manual, scheduled, or page_view |
config | object | No | Trigger config JSONB, e.g. { event, min_count, window_days, cooldown_days } |
enabled | boolean | No | Whether the trigger is active (default true) |
Returns: TriggerRow (the created trigger, including its generated ID).
trigger_update¶
Updates a trigger's config and/or enabled state. Setting enabled to false deactivates the trigger without deleting it, preserving the config for future re-activation.
Required scope: surveys:write
Input:
| Parameter | Type | Required | Description |
|---|---|---|---|
trigger_id | string | Yes | UUID of the trigger to update |
config | object | No | New trigger config JSONB, replaces the existing config |
enabled | boolean | No | Set to false to deactivate without deleting |
Returns: { success: true } (confirmation of the update; the trigger's new state is not echoed back).
Audience targeting rules and per-user fatigue limits described in earlier drafts of this guide are not implemented as MCP tools (see #92). The two tools below, trigger_list and trigger_delete, manage existing triggers and are unaffected by that gap.
trigger_list¶
Lists all triggers for a survey owned by the organization.
Required scope: surveys:read
Input: { "survey_id": "string" }
Returns: { type: 'platform_data', value: TriggerRow[] } (each trigger's id, survey_id, type, config, enabled, environment, created_at, and updated_at; wrapped as platform data per ADR-0019).
trigger_delete¶
Deletes a trigger owned by the organization.
Required scope: surveys:write
Input: { "trigger_id": "string" }
Returns: { success: true } (confirmation that the trigger was deleted).
Automation¶
Workflow automation triggered by survey response events is not implemented as an MCP tool. See #96.
Error codes¶
Errors use RFC 7807 Problem Details (Content-Type: application/problem+json): a JSON body shaped { type, title, status, detail, request_id }. The HTTP status code is the primary signal; type is a slug under https://canviq.app/errors/. See the MCP Tools API > Error Codes page for the full table of status codes and slugs.
Example: Claude Desktop configuration¶
{
"mcpServers": {
"canviq": {
"url": "https://canviq.app/api/mcp",
"apiKey": "pk_live_your_key_here"
}
}
}
Once connected, you can ask Claude:
"Query my Canviq PMF score and summarize the top themes from the Very Disappointed cohort."
Related¶
- Founder Guide: Dashboard features and PMF methodology
- Respondent Guide: What survey respondents see
- iOS SDK API Reference: iOS SDK public methods