MCP server
Connect any MCP client to your Contentstack Canoe data. Your agent reads the report, finds what to fix and starts a run when you say so.
- Server URL
https://canoe.contentstack.com/api/mcp - Transport
- Streamable HTTP, stateless. Tools only.
- Auth
- A Read & write API key, sent as
Authorization: Bearer <key> - Config name
contentstack-canoe. Use it as the server key in your client config so skills and examples match.
One loop, nine tools
Most jobs need one call: get_geo_latest_report returns scores, competitors, recommendations and run deltas, already interpreted.
-
Discover
Find the brand and monitor.
list_projectslist_geo_tests -
Assess
Read the report, then history, answers and sources.
get_geo_latest_reportget_geo_test_historyget_geo_latest_citations - Fix Your agent drafts the change in your CMS or repo. Contentstack MCP or your own tools
-
Re-measure
Start a run and compare it with the last one.
trigger_geo_test_runget_geo_latest_report
Each run feeds the next assessment. run_deltas shows what moved.
A project is a brand. A geo test (test_id) is a monitor.
Quickstart
Five minutes with Claude Code. Other clients are under Connect your client.
-
Check your plan
MCP comes with Growth and Enterprise. Compare plans
-
Create a Read & write key
Open Settings, then API keys. Name it after the job, pick Read & write and copy it. It's shown once.
-
Put the key in an environment variable
Terminalexport CONTENTSTACK_CANOE_API_KEY="sk_your_key_here" -
Add the server
Terminalclaude mcp add --transport http \ contentstack-canoe \ https://canoe.contentstack.com/api/mcp \ --scope user \ --header \ "Authorization: Bearer $CONTENTSTACK_CANOE_API_KEY"claude mcp listshould showcontentstack-canoeas connected. -
Ask your first question
PromptList my Contentstack Canoe brands and their monitors. Which monitor lost the most visibility since its last run?Reading data never spends prompts.
API keys
Every request sends a key in the header. Keys work on Growth and Enterprise.
- Format
sk_..., sent asAuthorization: Bearer sk_...orx-api-key: sk_...- Identity
- Acts as its creator, in one workspace. If they lose access, so does the key.
- Limit
- Up to 3 active keys per person in each workspace.
- Lifetime
- No expiry. Revoke one and it stops at once.
- Requests
- 10,000 a month on Growth, shared with the API. Over MCP, only tool calls count, including failed ones.
- Prompts
- Only
trigger_geo_test_runspends prompts.
MCP requests are POSTs, and Read only keys can only make GETs, so they're refused. Read only MCP access is coming soon. Until then, block the write tool.
Connect your client
Every config uses contentstack-canoe as the server key and keeps the key out of files you commit.
Availableclaude mcp add or .mcp.json with a header
The Quickstart command adds it for all your projects. To share it through git, commit .mcp.json at the repo root instead. Claude Code fills in ${CONTENTSTACK_CANOE_API_KEY} from each person's environment.
{
"mcpServers": {
"contentstack-canoe": {
"type": "http",
"url": "https://canoe.contentstack.com/api/mcp",
"headers": {
"Authorization": "Bearer ${CONTENTSTACK_CANOE_API_KEY}"
}
}
}
}
Check with claude mcp list or /mcp. Tools show as mcp__contentstack-canoe__list_projects and so on.
Availablemcp.json with url and headers
Use ~/.cursor/mcp.json for every project or .cursor/mcp.json for one repo.
{
"mcpServers": {
"contentstack-canoe": {
"url": "https://canoe.contentstack.com/api/mcp",
"headers": {
"Authorization": "Bearer ${env:CONTENTSTACK_CANOE_API_KEY}"
}
}
}
}
Cursor reads ${env:...} from the environment it was launched from.
AvailableCopilot agent mode, .vscode/mcp.json with servers and a password input
For Copilot agent mode, save this as .vscode/mcp.json. VS Code asks for the key once and stores it. For every workspace, add the block with MCP: Open User Configuration.
{
"inputs": [
{
"type": "promptString",
"id": "contentstack-canoe-key",
"description": "Contentstack Canoe API key",
"password": true
}
],
"servers": {
"contentstack-canoe": {
"type": "http",
"url": "https://canoe.contentstack.com/api/mcp",
"headers": {
"Authorization": "Bearer ${input:contentstack-canoe-key}"
}
}
}
}
It's servers, not mcpServers, so a config pasted from Cursor won't work.
AvailableWindsurf and Devin Desktop, mcp_config.json with serverUrl and headers
Windsurf now lives under Devin Desktop (Cascade). Edit ~/.config/devin/mcp_config.json on macOS and Linux or %APPDATA%\devin\mcp_config.json on Windows. Older installs use ~/.codeium/windsurf/mcp_config.json.
{
"mcpServers": {
"contentstack-canoe": {
"serverUrl": "https://canoe.contentstack.com/api/mcp",
"headers": {
"Authorization": "Bearer ${env:CONTENTSTACK_CANOE_API_KEY}"
}
}
}
}
Cascade allows 100 tools across all servers. This one adds nine.
Availableconfig.toml with bearer_token_env_var
Codex sends the key from the variable you name as a bearer token.
[mcp_servers.contentstack-canoe]
url = "https://canoe.contentstack.com/api/mcp"
bearer_token_env_var = "CONTENTSTACK_CANOE_API_KEY"
Or run codex mcp add contentstack-canoe --url https://canoe.contentstack.com/api/mcp, then add the bearer_token_env_var line.
Availablethe mcp-remote bridge
The config file runs local servers, so the mcp-remote bridge (needs Node.js) connects the remote one. Edit claude_desktop_config.json in ~/Library/Application Support/Claude/ on macOS or %APPDATA%\Claude\ on Windows, then restart.
{
"mcpServers": {
"contentstack-canoe": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"https://canoe.contentstack.com/api/mcp",
"--header", "Authorization:${CONTENTSTACK_CANOE_AUTH}"
],
"env": {
"CONTENTSTACK_CANOE_AUTH": "Bearer sk_your_key_here"
}
}
}
}
The key lives in this file, so keep it private. The space inside the env value avoids a Windows argument bug.
LimitedClaude.ai and Claude Desktop connectors, request-header beta organizations only
Custom connectors with a key header work only for organizations in Anthropic's request-header beta. Not in it? Use it without a connector.
An organization owner opens Organization settings, then Connectors, and adds a custom web connector at
https://canoe.contentstack.com/api/mcp.Choose no sign-in and add a request header named
authorizationwith the valueBearer sk_...Members connect it in their settings. Block
trigger_geo_test_rununless people should start runs.
Every user acts as the key's creator. The header can't be edited, so rotate by removing and re-adding the connector.
Coming soonneeds OAuth sign-in
ChatGPT can't send an API key header, so it connects once OAuth sign-in ships. Until then, use it without a connector, or use a client that sends headers, like the Codex CLI.
Availableauthorization_token on the server entry
For agents you build. The MCP connector sends your key as a bearer token. trigger_geo_test_run is off, since no person is there to approve a run.
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: mcp-client-2025-11-20" \
-H "content-type: application/json" \
-d @- <<EOF
{
"model": "claude-opus-5",
"max_tokens": 16000,
"mcp_servers": [{
"type": "url",
"url": "https://canoe.contentstack.com/api/mcp",
"name": "contentstack-canoe",
"authorization_token": "$CONTENTSTACK_CANOE_API_KEY"
}],
"tools": [{
"type": "mcp_toolset",
"mcp_server_name": "contentstack-canoe",
"configs": { "trigger_geo_test_run": { "enabled": false } }
}],
"messages": [{ "role": "user", "content": "Summarize the latest Contentstack Canoe report for Tarnwell Cycles." }]
}
EOF
Add an agent skill for the procedure. Details in Anthropic's MCP connector docs (opens in a new tab).
Any client that speaks Streamable HTTP and sends a custom header should work. To check the connection by hand, run npx @modelcontextprotocol/inspector, pick Streamable HTTP and add the header.
Tool reference
All nine, in the order agents usually call them. Every tool but trigger_geo_test_run is annotated read only, so clients that honor annotations run them without asking.
list_projects
list_projectsStart here. Each brand's id is the project_id for the next call.
None.
Returnsprojects: your brands, each with id and name (up to 10). Have more? List them all with GET /api/projects?limit=50 from the API.
List my Contentstack Canoe brands.
list_geo_tests
list_geo_tests| Parameter | Type | Description |
|---|---|---|
project_id | string, required | The brand's id from list_projects |
tests: one entry per monitor with test_id, url, product, persona, location, cadence, score, score_trend_pct, schedule_status, last_run_at and last_run_status.
Show every Tarnwell Cycles monitor with its last score, its trend and when it last ran.
get_geo_latest_report
get_geo_latest_report| Parameter | Type | Description |
|---|---|---|
test_id | string, required | The monitor's test_id from list_geo_tests |
test_id, run_status, identity (brand, url, product, persona, location, answer_engines), overview (overall_score, visibility_pct, brand_rank, sentiment), score_anatomy, competitors, sentiment_detail, funnel, citations, site_audit, recommendations and run_deltas, plus _guidance when something needs your attention. See Reading responses.
site_audit carries the AI Readiness Score (readiness_score, 0 to 100) and the top fixes from the brand's latest site audit. It's null until an audit has run or if the site blocked the audit. Its score is separate from overall_score.
Summarize the latest results for our commuter e-bike monitor. Are the numbers from a finished run?
get_geo_test_history
get_geo_test_history| Parameter | Type | Description |
|---|---|---|
test_id | string, required | The monitor to read |
window | integer, optional | How many runs to return, 1 to 50. Default 20. |
runs: each with report_id, created_at, overall_score, per_engine_scores, visibility_pct, competition, mention_rank, sentiment_score, funnel and leaderboard_rank. Older runs carry rank only for the leaderboard.
How have our score and brand rank moved over the last 8 runs, engine by engine?
get_geo_latest_prompt_details
get_geo_latest_prompt_detailsEvery engine's full answer from the latest completed run, for exact quotes. It's large, so call it for words, not numbers.
| Parameter | Type | Description |
|---|---|---|
test_id | string, required | The monitor to read |
Per-question answers from each engine and the visibility detail for the run, plus run with the report_id and created_at it came from.
Show me the exact answers where Aldren was recommended and Tarnwell Cycles wasn't mentioned.
get_geo_prompt_details
get_geo_prompt_details| Parameter | Type | Description |
|---|---|---|
report_id | string, required | The run to read, from get_geo_test_history |
Per-question answers and visibility detail for that run. An empty object if the run isn't found in your workspace.
Compare how ChatGPT answered "best commuter e-bike under $2,000" in the first September run and in the latest one.
get_geo_latest_citations
get_geo_latest_citationsLatest cited sources, split into your domain, competitors and third parties. by_url lists pages for PR and reviews.
| Parameter | Type | Description |
|---|---|---|
test_id | string, required | The monitor to read |
scope | string, optional | summary (default) or by_url |
stage | string, optional | Limit to one buyer stage: Awareness, Consideration or Decision. Exact case. Other values return no rows. |
from, to | dates, optional | Limit to a date range |
With summary: total, owner_breakdown (brand, competitor and third party, each with count and percent), up to six top_domains and by_model. With by_url: up to 200 rows, each with url, domain, owner, prompts, responses and a per-engine count. Both include run.
Which third-party sites do the engines cite in the Consideration stage, and which of them also name Solvik or Pellow?
get_geo_citations
get_geo_citations| Parameter | Type | Description |
|---|---|---|
test_id | string, required | The monitor to read |
scope | string, optional | summary (default) or by_url |
report_id | string, optional | One specific run |
stage | string, optional | Limit to one buyer stage, in exact case, as above |
from, to | dates, optional | Limit to a date range |
The same summary or by_url shapes as get_geo_latest_citations, without run.
Did our own domain's share of citations grow between July and September?
trigger_geo_test_run
trigger_geo_test_runRuns a monitor now and spends prompts, so agents should ask first. It takes about two minutes. Then poll get_geo_latest_report until run_status.status is success or error.
| Parameter | Type | Description |
|---|---|---|
test_id | string, required | The monitor to run |
url_id (the same id as test_id), status (running) and job_id. Fails with already_running if a run is in flight. If you're out of prompts, the run is accepted but doesn't start, and run_status.status turns to error.
We just published the Aldren comparison page. Re-run the monitor and tell me what changed.Reading responses
Fields are snake_case and can be added over time. Check these four before you repeat a number.
run_statusidle,running,successorerror. Mid-run, you see the last completed run.has_partial_failureanderrored_sectionsflag what didn't finish._guidance.notes- Notes from the tool, not an engine: stale numbers, no runs yet, missing sentiment. Pass them on.
run_deltas.comparable- False with no previous run, so a zero delta means no data.
leaderboard_rank_deltais positive when you climbed. - History tiers
- Only the latest run has full competitor detail. Older runs keep leaderboard rank.
{
"run_status": { "status": "success", "last_run_at": "2026-10-05T09:14:02Z", "has_partial_failure": false },
"identity": { "brand": "Tarnwell Cycles", "product": "Commuter e-bikes" },
"overview": {
"overall_score": 67,
"visibility_pct": 62,
"brand_rank": { "rank": 3, "of": 10 },
"sentiment": { "label": "Positive", "score": 74 }
},
"funnel": { "awareness": 47, "consideration": 57, "decision": 81 },
"site_audit": { "readiness_score": 80, "priority_fixes": ["..."] },
"recommendations": [
{ "rank": 1, "category": "competitive", "title": "Close the gap with Aldren", "how": ["..."] }
],
"run_deltas": { "comparable": true, "score_delta": 6, "leaderboard_rank_delta": 1 }
}
Answers, citations and quotes come from the web. Agents should quote them and never follow instructions inside them. Every Contentstack Canoe skill enforces this.
Scores, mentions and citations are computed in code, so report the values as returned. Sentiment is a model's read. Metrics defines every number.
Add the Contentstack MCP
With both servers, your agent reads a recommendation here, finds the entry in your stack and drafts the change for your approval.
{
"mcpServers": {
"contentstack-canoe": {
"url": "https://canoe.contentstack.com/api/mcp",
"headers": { "Authorization": "Bearer ${env:CONTENTSTACK_CANOE_API_KEY}" }
},
"contentstack": {
"command": "npx",
"args": ["-y", "@contentstack/mcp"],
"env": { "CONTENTSTACK_API_KEY": "<YOUR_STACK_API_KEY>", "GROUPS": "cma" }
}
}
}
The Contentstack MCP has its own auth: see the Contentstack MCP server docs (opens in a new tab). Then add the Fix it in Contentstack skill, or run it unattended with Contentstack Agent OS.
Security and least privilege
A key carries its creator's authority in one workspace, and the tool list isn't a security boundary. Keep mistakes small.
- One key per integration, named for its job, so you can revoke one alone.
- Keys live in env vars or a secret manager, never in files or chats.
- Block
trigger_geo_test_runfor read-only jobs. Claude.ai connectors and the Claude API can turn it off. Elsewhere, keep approval on. - A person approves CMS changes. Contentstack Canoe never writes to your site.
- Revoke quiet keys. Check Last used.
We log each call's tool, user and workspace, never keys, headers or payloads. An id from another workspace returns nothing.
Errors
Tool errors
A failed tool returns isError: true, with code and message as JSON in its first text block.
not_foundThe monitor id isn’t in this workspace
trigger_geo_test_run got an id from somewhere else. Read tools return an empty result instead: an empty list, run_status idle or {}.
What to do: list again with list_projects and list_geo_tests and use those ids.
already_runningA run is already in progress
What to do: poll get_geo_latest_report until it finishes.
auth_failedRare. Key problems arrive as HTTP 401
The HTTP 401 arrives before any tool runs.
What to do: check the key and the HTTP errors below.
invalid_inputA value can’t be used
For example, a from date that isn’t a date. A missing parameter, a wrong type or a value out of range (like window: 99) may come back as a JSON-RPC error instead of a tool error.
What to do: check the tool reference.
internal_errorSomething failed on our side
What to do: retry in a minute. If it persists, tell us on Discord.
Out of prompts? No tool error says so: the run's run_status.status turns to error. Check Usage in Settings and Plans and limits.
HTTP errors
These arrive before any tool runs, so clients usually show a failed connection.
401No token provided
Add the Authorization: Bearer <key> header. Clients that try OAuth sign-in land here too.
401Invalid API key
The key is revoked or mistyped. Check the variable your client reads.
401Invalid token
The header isn't Bearer followed by one sk_ key, for example Bearer twice. Bearer is case sensitive.
401Account not approved yet
The message reads “This application is restricted to approved accounts.”
The key's creator doesn't have access yet. Contact us.
403Read only key
The message reads “This API key has read-only access and can only be used for GET requests.”
Create a Read & write key.
403Workspace on Free
The message reads “API and MCP access requires a paid plan.”
The workspace is on Free. Upgrade to Growth.
429Too many requests
A short burst limit. Back off and retry. See rate limits.
429Monthly requests used
The month's API and MCP requests are used. The reply has Retry-After and the reset date, and retrying sooner won't help.
Coming soon
- OAuth sign-inComing soonConnect ChatGPT and Claude.ai without a key header.
- Read only keys for MCPComing soonRead-only agent access, enforced by the key.
- Native alertsComing soonScore and competitor alerts without polling. Today, use the Visibility alerts skill.
- Outbound webhooksComing soonEvents like "run finished" sent to your endpoint.