Skip to content

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

  1. Sign in to your Canviq dashboard.
  2. Go to Settings > Agents (/admin/settings/agents).
  3. Create an agent, or open an existing one.
  4. On the agent's own page, click Generate key. Choose the environment:
  5. Live (pk_live_...): Accesses production data
  6. Test (pk_test_...): Accesses sandbox data only
  7. Assign a policy to the agent (see Permission scopes) if it doesn't have one already.
  8. 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:

Authorization: Bearer pk_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345

For MCP clients that use the api-key configuration field, set:

{
  "mcpServers": {
    "canviq": {
      "url": "https://canviq.app/api/mcp",
      "apiKey": "pk_live_..."
    }
  }
}

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."