How the MCP Server Works
This page is for developers building their own MCP client or integration, and for anyone who wants to know exactly what happens when an assistant calls a CheckFlow tool. If you only want to connect an existing client, Connecting a Client is enough.
Transport
The server implements the MCP Streamable HTTP transport in its stateless form:
- One endpoint, one method. Every message is a
POSTof a JSON-RPC 2.0 message — or a batch of them — to/mcp, and every answer is JSON in the response body. - No sessions. The server does not issue an
Mcp-Session-Idand remembers nothing between requests. Each request is authenticated and answered on its own. - No event stream. The server never streams Server-Sent Events. A
GETorDELETEon/mcp— what a client sends to open a stream or end a session — is answered405 Method Not Allowedwith anAllow: POSTheader. - The handshake is optional.
initializeandnotifications/initializedare answered properly for clients that need them, buttools/listandtools/callwork without them. - Authentication is per request. Every request must carry a credential: an API key in the
X-API-KEYheader or inAuthorization: Bearer, or an OAuth access token inAuthorization: Bearer— see Signing In with OAuth. The credential decides the workspace and who the calls act as. If a request carries both headers,X-API-KEYis used.
The server identifies itself as checkflow, version 3.0.0, and supports the tools and resources capabilities. It has no prompts.
Signing In with OAuth
A client can authenticate by signing a person in instead of sending an API key. CheckFlow follows the MCP authorization specification: OAuth 2.1 with the authorisation code flow and PKCE. Most clients do all of this for you; this section is for developers writing their own.
Discovery
A request to /mcp with no credential is answered 401 with a challenge that says where to look:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.checkflow.io/.well-known/oauth-protected-resource/mcp", scope="mcp"
The protected resource metadata (RFC 9728) names the authorisation server. It is also served at /.well-known/oauth-protected-resource:
{
"resource": "https://api.checkflow.io/mcp",
"authorization_servers": ["https://app.checkflow.io"],
"scopes_supported": ["mcp"],
"bearer_methods_supported": ["header"],
"resource_name": "CheckFlow",
"resource_documentation": "https://docs.checkflow.io/docs/mcp/overview"
}
Each region's server describes its own address as the resource. Both use the same authorisation server, whose metadata (RFC 8414) is at https://app.checkflow.io/.well-known/oauth-authorization-server:
| Endpoint | Address |
|---|---|
| Authorisation | https://app.checkflow.io/oauth/authorize |
| Token | https://app.checkflow.io/oauth/token (form-encoded) |
| Registration | https://app.checkflow.io/oauth/register (JSON) |
| Revocation | https://app.checkflow.io/oauth/revoke (form-encoded) |
Identifying the Client
A client identifies itself in one of two ways:
- A Client ID Metadata Document. The
client_idis anhttpsURL whose JSON document lists the client's name and redirect URIs. CheckFlow fetches it when the person signs in. The authorisation server metadata says this is supported withclient_id_metadata_document_supported: true. - Dynamic Client Registration (RFC 7591). The client posts its name and redirect URIs to the registration endpoint and receives a
client_id.
Every client is a public client: there is no client secret, and token_endpoint_auth_method is none. The redirect URI must be one CheckFlow sends sign-ins to: Claude's and ChatGPT's callbacks, VS Code's, Cursor's and Microsoft 365 Copilot's, or a loopback address on the person's own computer (http://localhost:<port>/callback or http://127.0.0.1:<port>/callback). A client that needs another redirect URI can connect with an API key.
The Flow
- The client sends the person's browser to the authorisation endpoint with
response_type=code, itsclient_idandredirect_uri, a PKCEcode_challengewithcode_challenge_method=S256, astate, andresourceset to the MCP server's address.plainPKCE is refused. - The person signs in to CheckFlow and allows the connection. See Signing In.
- The browser returns to the redirect URI with
code,stateandiss.iss(RFC 9207) ishttps://app.checkflow.io, and is sent on errors too. A code lasts five minutes and can be used once. - The client exchanges the code at the token endpoint with
grant_type=authorization_code, thecode, the sameredirect_uri, itsclient_idand thecode_verifier.
The token response:
{
"access_token": "cfat_...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "cfrt_...",
"scope": "mcp"
}
Using and Refreshing Tokens
- Send the access token as
Authorization: Bearer <access_token>on every request to/mcp. It lasts one hour. - Refresh it with
grant_type=refresh_token, therefresh_tokenand theclient_id. Every refresh returns a new refresh token, and the old one stops working. A refresh token that is not used for 30 days expires. - A refresh token that has expired, has already been used, or belongs to a connection that has been revoked is refused with
invalid_grant. The client should then sign the person in again. Presenting a used refresh token a second time ends the connection, because it means two parties hold it. - To end a connection, post either token to the revocation endpoint with the
client_id(RFC 7009).
What a Token Can Do
- It acts as the person who signed in, in the workspace they were signed in to, with the same permission checks as an API key that acts as that member. No token acts as the workspace.
- It works on
/mcponly. The REST API refuses it withACCESS_TOKEN_NOT_ACCEPTED, and still takes API keys. - It is subject to Allow AI assistants to connect, and to the member's AI.Assistant permission. While the switch is off, or the member does not hold the permission, calls are refused with
MCP_DISABLEDorAI_ASSISTANT_NOT_PERMITTED, and no new connection can be made. Turning the switch back on, or giving the permission back, lets existing connections carry on without signing in again. - It is served by the workspace's own region. A token for a workspace in Europe is refused with
WRONG_REGIONby the United States server, and the other way round.
A Session by Hand
You can drive the server with any HTTP client. These are the requests a client makes. The examples send an API key; with an OAuth access token, send Authorization: Bearer <access_token> instead of X-API-KEY.
Initialise
POST https://api.checkflow.io/mcp
X-API-KEY: your-api-key-here
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": { "name": "my-integration", "version": "1.0.0" }
}
}
The answer carries the negotiated protocolVersion, the server's capabilities, serverInfo and the server's instructions — a short text that most clients add to the model's context:
This server is CheckFlow, a checklist and process tool. Every call acts on the one workspace the API key belongs to: there is no workspace to choose and none to name in an argument.
Names are not free text. Members, groups, tags and templates are resolved against what the workspace actually contains, so read get_workspace_info before sending any of them rather than inventing a name that looks right.
Before composing a template document for the first time, call get_authoring_guide. The schema tools say what a document may contain; the guide says what to put in one, including the things a schema cannot express.
If a write times out or its answer is lost, retry it with the same idempotencyKey rather than calling it again: the first answer comes back instead of the work happening twice.
A client then sends the notifications/initialized notification. Notifications have no id, so the server answers 202 Accepted with an empty body.
List the Tools
POST https://api.checkflow.io/mcp
X-API-KEY: your-api-key-here
Content-Type: application/json
{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }
Each of the 147 tools comes back with its name, a title for people to read, a description written for a model, an inputSchema (JSON Schema for its arguments) and annotations — see Tool Annotations.
Call a Tool
POST https://api.checkflow.io/mcp
X-API-KEY: your-api-key-here
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "list_tags",
"arguments": {}
}
}
Batches
You can send an array of messages in one request. The answer is an array with one entry per message that had an id, in the same order; notifications get no entry. An array of one is still answered with an array.
Tool Results
A successful tool call returns the same data as the equivalent REST request — same property names, same omitted nulls, same UTC timestamps. It is returned twice in the result: as structuredContent for clients that parse JSON, and as a single text block containing the same JSON for clients that only show text to the model.
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "{\"items\":[{\"name\":\"q3-audit\",\"checklists\":14,\"templates\":1,\"tasks\":3,\"total\":18},{\"name\":\"urgent\",\"checklists\":9,\"templates\":0,\"tasks\":22,\"total\":31}],\"hasMore\":false}"
}
],
"structuredContent": {
"items": [
{ "name": "q3-audit", "checklists": 14, "templates": 1, "tasks": 3, "total": 18 },
{ "name": "urgent", "checklists": 9, "templates": 0, "tasks": 22, "total": 31 }
],
"hasMore": false
}
}
}
When a tool's REST equivalent returns an object, structuredContent is that object. When it returns something else — an array, a string — it is wrapped in { "result": ... }, because structuredContent must be an object.
The tool reference pages link each tool to the REST object it returns.
Errors
The server distinguishes a refusal — the request was understood and the answer is no — from a protocol error — the message itself was wrong. Only protocol errors change the HTTP status.
Refusals Are Tool Results
When a tool refuses — a template that does not exist, a value that fails validation, a permission the acting member lacks — the call still succeeds at the protocol level. The result has isError: true, and carries the same error body the REST API uses:
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"isError": true,
"content": [
{
"type": "text",
"text": "TEMPLATE_NOT_FOUND: Template '0e7ad584-7788-4ab1-95a6-ca0a5b444cbb' was not found.\nFull detail in structuredContent.error. requestId req_8c3f0e1a2b7d4c59a6e4d1f0b9c2a735."
}
],
"structuredContent": {
"error": {
"code": "TEMPLATE_NOT_FOUND",
"message": "Template '0e7ad584-7788-4ab1-95a6-ca0a5b444cbb' was not found.",
"requestId": "req_8c3f0e1a2b7d4c59a6e4d1f0b9c2a735"
}
}
}
}
This is deliberate. MCP clients pass tool results back to the model, but typically report HTTP and protocol errors to the user as a connection failure. Returning refusals as results means the model reads why the call failed and can correct itself — pick the right template, fix the value, or ask you.
The text block starts with the code and message, followed by the field at fault in brackets where there is one. For TEMPLATE_INVALID it also lists up to three violations and says how many more are in structuredContent.error.violations.
Refusals Before the Tool Runs
Two refusals are decided before any tool runs: the key belongs to the other region (WRONG_REGION), and the rate limit is spent (RATE_LIMIT_EXCEEDED). They are also answered inside the protocol, with HTTP status 200:
- a
tools/callis answered with a result withisError: true, as above; - any other request, such as
initializeortools/list, is answered with a JSON-RPC error with code-32000, the message, and the error body indata.
A workspace whose Administrator has turned off Allow AI assistants to connect, on the AI tab, is refused the same way, with MCP_DISABLED. The message says who turned it off and where it is turned back on. A request acting as a Member who does not hold the AI.Assistant permission is refused the same way too, with AI_ASSISTANT_NOT_PERMITTED. See Choose Who Can Connect an AI Assistant. In both cases the REST API is not affected.
Protocol Errors
| Situation | HTTP status | JSON-RPC error |
|---|---|---|
| The body is not JSON, or is empty. | 400 | -32700 Parse error |
| The body is JSON but not a JSON-RPC message, or is an empty batch. | 400 | -32600 Invalid request |
| The method does not exist. | 200 | -32601 Method not found |
| The tool does not exist. | 200 | -32602 Invalid params |
| The server failed while handling the request. | 500 | -32603 Internal error |
Protocol errors carry the request id in error.data.requestId.
Authentication Errors
A missing or invalid credential is refused before the request is read as JSON-RPC, so it is answered as the REST API answers it: HTTP 401 and the v3 error body. On /mcp, the 401 also carries the WWW-Authenticate challenge described in Discovery. A refused bearer credential adds error="invalid_token" to it, which tells a client to refresh its token or sign the person in again.
| Code | When |
|---|---|
MISSING_API_KEY | The request carries no credential. |
INVALID_API_KEY, API_KEY_REVOKED, API_KEY_EXPIRED, API_KEY_ACTOR_UNAVAILABLE | The API key is wrong, revoked or expired, or its member can no longer be acted as. See Authentication Errors. |
INVALID_ACCESS_TOKEN | The access token is not one CheckFlow issued, was issued for another server, or is a refresh token or code sent in its place. |
ACCESS_TOKEN_EXPIRED | The access token is more than an hour old. Refresh it. |
ACCESS_TOKEN_REVOKED | The connection was disconnected by the member, revoked by an Administrator, or ended by the client. Sign in again. |
OAUTH_MEMBER_UNAVAILABLE | The member who signed in has been deactivated, removed or made a Guest. |
Tool Annotations
Every tool declares hints that clients use to decide whether to ask you before running it:
| Annotation | Meaning | Tools |
|---|---|---|
readOnlyHint: true | The tool only reads. | 57 |
destructiveHint: true | The tool deletes something, or has an effect that cannot be taken back: deleting, removing a comment, file, table row or sub-task, replacing a Data Set's records, publishing a version, upgrading checklists, re-generating a schedule's pending runs, replaying a webhook. | 26 |
destructiveHint: false | The tool writes, but only in ways you can undo in the app: creating, completing, setting, updating or clearing something, adding or removing a tag. Every tool that writes declares one or the other. | 64 |
idempotentHint: true | Calling it again with the same arguments has no further effect. For a delete that means the second call changes nothing and answers NOT_FOUND. | Most tools |
Tools that are neither read-only nor destructive — creating a checklist, completing a task, adding a comment — change your workspace in ways you can see and undo in the app. A tool that takes an idempotencyKey does not also declare idempotentHint: sending the key again is the way to retry it, and returns the first call's answer. The tool reference pages list each tool's annotations.
Retries and Idempotency
MCP clients cannot set a header per tool call, so every tool that writes and is not safe to repeat takes an optional idempotencyKey argument instead of the REST API's Idempotency-Key header. Its description tells the model:
Optional. Send the same key again to retry this call safely: the retry gives back the first call's answer instead of doing the work twice. Use a new key whenever you mean a new operation.
It behaves exactly like the REST header and uses the same store:
- The first call with a key does the work and keeps its answer for 24 hours.
- A later call with the same key and the same arguments gets the kept answer back.
- The same key with different arguments is refused with
CONFLICT. Arguments are compared as sent, so adding an optional argument on the retry counts as different. - A key is at most 255 characters.
Rate Limits
MCP requests share the workspace's rate limit budgets with the REST API. One POST to /mcp counts as one request, however many messages it batches.
The protocol's own traffic — initialize, ping, tools/list, resources/list, resources/read and notifications — and the schema, guide and validation tools are charged to the cheap budget, so a client can list tools and an assistant can validate a template repeatedly without eating into the budget for real work. A batch is charged to cheap only if every message in it is cheap.
Every response carries the X-RateLimit-* headers. When the budget is spent, the refusal carries retryAfterSeconds in its error body — the only way the wait reaches the model, which never sees headers.
The Authoring Guide Resource
The server publishes one resource:
| Property | Value |
|---|---|
| URI | checkflow://guides/template-authoring |
| Name | template_authoring_guide |
| Type | text/markdown |
It is a prose guide to composing a CheckFlow template document: the order to work in, what tasks and fields are, how to choose between the field types, how conditions, due dates and dynamic values in text are written, and how to publish. Clients that support resources let you attach it to a conversation. The same text is available from the get_authoring_guide tool and from GET /v3/schema/guide.
Related Pages
- Connecting a Client — setting up a client instead of writing one.
- Authentication — API keys and who they act as.
- Errors — every error code the tools can return.
- Idempotency — the mechanism behind
idempotencyKey. - Template Authoring Tools — the tools that use the guide, schema and validators.