MCP Server
The SupDesk MCP (Model Context Protocol) server gives AI assistants and other LLM-powered tools direct access to a project’s help center, feedback board, changelog, message threads, beta programs, and waitlist.
Endpoint
https://mcp.supdesk.app/mcpAuthentication
Pass an API key as a Bearer token:
Authorization: Bearer sd_live_...Generate keys in the SupDesk console under Workspace Settings → API Keys.
The key determines the project, so no tool takes a project_id argument — there’s nothing
for a model to guess or get wrong.
What you get
Which tools appear depends on the project. A feature switched off in Project Settings has its
tools omitted from tools/list entirely, exactly as the REST API 404s those routes.
| Group | Tools | Requires |
|---|---|---|
| Help center | 10 | Help center enabled |
| Feedback board | 5 | Feedback board enabled |
| Changelog | 5 | Changelog enabled |
| Messages | 5 | Private messages enabled |
| Beta testing | 8 | Beta enabled |
| Waitlist | 4 | Waitlist enabled |
| Overview | 1 | Always available |
Reads work on every plan. Writes require a paid plan — a create, update, or delete on a
read-only key returns forbidden, while reads on the same key work fine.
Help center
list_articles— articles, newest edit first. Includes drafts and archived articles; filter withstatusfor what customers can see.get_article— one article by id, including its Markdown body.search_articles— full-text search across published articles, ranked, with a snippet.create_article— creates a draft. Publishing is a separate call, so nothing reaches customers by accident.update_article— patch any field; setstatustopublishedorarchived.delete_article— permanent; archive instead to retire an article you want to keep.list_article_categories,create_article_category,update_article_category,delete_article_category— deleting a category leaves its articles in place, uncategorised.
Article bodies are Markdown, not HTML. The portal renders them through a viewer that never evaluates HTML, so HTML arrives at readers as literal text.
create_article fails with limit_reached once the plan’s article allowance is spent — Free 5,
Pro 50, Team unlimited.
Feedback board
list_feedback— posts, newest first. Covers all three types (bug,feature,feedback) unless you narrow it withtype.get_feedback,create_feedback,update_feedback—update_feedbackis how you move a post alongopen → planned → in_progress → done.delete_feedback— removes the post, its votes, and its comments.
Changelog, messages, beta, and waitlist
Full parity with the REST API, over the same queries:
- Changelog —
list_changelog,get_changelog_entry,create_changelog_entry,update_changelog_entry,delete_changelog_entry. Entries default to draft, because publishing is what emails your subscribers. - Messages —
list_threads,get_thread(with the full message history),create_thread,reply_to_thread,update_thread. - Beta testing —
list_beta_programs,get_beta_program,create_beta_program,update_beta_program,delete_beta_program,list_beta_testers,add_beta_tester,remove_beta_tester.add_beta_testeris idempotent on email and returns the accept token; this server sends no email, so delivering the invite is yours to do. - Waitlist —
list_waitlist,add_waitlist_signup(idempotent on email),update_waitlist_signup,remove_waitlist_signup.
Overview
project_stats returns counts across the project — feedback posts by status, open threads,
published and draft articles, and the helpful / not-helpful votes those articles have collected —
so an agent can answer “how much is outstanding?” without walking four paginated lists.
Structured output
Every tool declares an outputSchema and returns both structuredContent (typed, validated) and
a content text block holding the same data as JSON. Parse whichever your client supports.
const result = await mcpClient.callTool("get_article", { id: "8f7a..." });
result.structuredContent.article.title; // typed
JSON.parse(result.content[0].text); // same payload, fallbackAnnotations
Every tool carries annotations so a client knows what it’s invoking before it invokes it:
| Annotation | Set on |
|---|---|
readOnlyHint: true | list_*, get_*, search_*, project_stats |
idempotentHint: true | reads and update_* |
destructiveHint: true | delete_*, remove_* |
Pagination
List tools take an opaque cursor and return a next_cursor — pass it back for the next page,
and stop when it’s null.
let cursor;
do {
const page = await mcpClient.callTool("list_articles", { status: "published", cursor });
handle(page.structuredContent.data);
cursor = page.structuredContent.next_cursor;
} while (cursor);A cursor is a token, not arithmetic — treat it as opaque. A malformed one returns
invalid_request rather than silently restarting from page one, which would make a paginated
walk loop forever.
Note that this is a convention in each tool’s own argument schema. MCP’s protocol-level cursor applies to listings of tools and resources, not to what a tool returns; the resource listing below uses the real thing.
Resources
For attaching project content as context, rather than calling a tool and pasting the result:
| URI | Contents |
|---|---|
supdesk://articles/{slug} | A published help article, as Markdown. |
supdesk://board | The project’s open feedback posts, as JSON. |
The article listing is genuinely protocol-paginated, and covers published articles only — a resource is content a client may surface verbatim, and a draft is unpublished for a reason.
Prompts
Two starting points, wired to the project’s real data:
draft_article_from_thread— turns a resolved support conversation into a help-center article, so the next customer with that question finds the answer instead of opening a thread. Needs the help center and private messages enabled.triage_feedback— assesses one post and recommends a status, with the reasoning. It checkssearch_articlesfirst, so an existing answer is surfaced instead of new work being proposed.
Both use MCP’s completion capability for their arguments: as you type a category, the server answers with the project’s actual category slugs rather than making you guess.
Logging
Multi-step writes send progress notifications over MCP logging — create_article checks the plan
allowance, probes for a free slug, then inserts, and says so at each step. A client without
logging support is unaffected; the write proceeds either way.
Examples
Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"sup-desk": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://mcp.supdesk.app/mcp",
"--header",
"Authorization: Bearer sd_live_..."
]
}
}
}Answer a question, or write the answer
// Check whether the help center already covers it.
const hits = await mcpClient.callTool("search_articles", {
query: "reset password",
limit: 5
});
if (hits.structuredContent.data.length === 0) {
// Nothing yet — draft one. It's created as a draft.
const created = await mcpClient.callTool("create_article", {
title: "How do I reset my password?",
body: "Open **Settings**, then choose *Reset password*.",
category_id: "3c2b..."
});
// Publishing is deliberate and separate.
await mcpClient.callTool("update_article", {
id: created.structuredContent.article.id,
status: "published"
});
}Triage the board
const open = await mcpClient.callTool("list_feedback", {
status: "open",
limit: 10
});
await mcpClient.callTool("update_feedback", {
id: open.structuredContent.data[0].id,
status: "planned"
});Everything outstanding, in one call
const stats = await mcpClient.callTool("project_stats", {});
// { posts_by_status: { open: 12, planned: 3, ... }, open_threads: 4,
// articles_published: 18, articles_draft: 2,
// article_helpful: 240, article_not_helpful: 11 }Rate limits
The MCP server shares the REST API’s limits: 120 requests per 60 seconds per project. See Rate Limits & Usage.
Errors
A failing tool call returns isError: true with the same error vocabulary the REST API uses:
{
"error": {
"code": "limit_reached",
"message": "Your plan allows 5 help-center articles."
}
}| Code | Meaning |
|---|---|
unauthorized | Missing, malformed, revoked, or unknown API key. |
forbidden | Valid key, but the plan has no API write access. |
invalid_request | Bad arguments — including a malformed cursor. |
not_found | No such id in this project. |
limit_reached | A plan allowance is spent (for example the article cap). |
rate_limited | Too many requests in the current window. |
internal_error | Something failed on our side. Safe to retry. |
The full list is documented under Errors.