MCP server
Verbatim AI is also a Model Context Protocol server, so an AI assistant (Claude, Cursor, an agent built on an MCP SDK…) can query your knowledge bases directly.
Verbatim AI is also a Model Context Protocol server, so an AI
assistant (Claude, Cursor, an agent built on an MCP SDK…) can query your knowledge bases directly.
| Endpoint | POST https://<api-host>/mcp |
| Transport | Streamable HTTP, stateless — no MCP session, no Mcp-Session-Id, no server-sent stream |
| Authentication | an access token, as Authorization: Bearer <token> or X-Access-Token: <token> |
| Tools | list_corpora, rag_query, get_document_markdown |
Authentication
Only access tokens open /mcp. On this path the Authorization: Bearer header carries an
access token — an RSA or Firebase JWT sent there is rejected. Everywhere else in the API,
Authorization: Bearer still means a JWT.
A missing, unknown, expired or revoked token is answered 401 Unauthorized (the rest of the API
answers 403).
Each tool requires the scope of the REST call it mirrors. A token without it can connect and list
the tools, but calling the tool returns a tool error (isError: true) and does nothing.
| Tool | Mirrors | Scope |
|---|---|---|
list_corpora | GET /v1/corpus/ | corpus:read |
rag_query | GET /v1/post/q | post:read |
get_document_markdown | GET /v1/doc/{id}/md | doc:read |
Mint a dedicated token, scoped to what the assistant needs:
POST /v1/auth/access-token/
Authorization: Bearer <your RSA JWT>
Content-Type: application/json
{"scope": ["corpus:read", "post:read", "doc:read"], "ttl": 86400}The token expires like any other access token (ttl, capped by the platform); the client has to be
given a new one then. Revoke it with DELETE /v1/auth/access-token/{token}.
Tools
list_corpora
list_corporaList the knowledge bases (corpora) of your organization — deleted ones excluded — so the assistant
can pick the corpusIds it passes to rag_query.
| Argument | Required | Description |
|---|---|---|
pageSize | no | Corpora per page, 1 to 100 (default 25) |
pageIndex | no | Page index, from 0 (default 0) |
{
"pageIndex": 0,
"pageSize": 25,
"items": [
{"id": "…", "name": "Support knowledge base", "description": "…", "metadata": {}}
]
}A full page means there may be more: ask for the next pageIndex. The tool declares an outputSchema describing every field and returns the result both as structuredContent and as JSON text content.
rag_query
rag_queryAsk a question to your organization's knowledge bases. The answer is generated from the documents
of the corpora searched, exactly as GET /v1/post/q does.
Each call is independent: it opens a new thread over the corpora searched and records the
exchange (question + answer) in it, so it shows up in GET /v1/thread/ and GET /v1/post/ with
metadata.source = "mcp". Opening that thread needs no scope of its own — post:read is enough.
| Argument | Required | Description |
|---|---|---|
question | yes | The question, in natural language |
corpusIds | no | UUIDs of the corpora to search. Default: every corpus of the organization (deleted ones excluded) |
lang | no | ISO 639 language code of the answer (default en) |
agentId | no | UUID of the agent to run the query on (default: the platform's default agent) |
The tool declares an outputSchema (every field described, for the assistant to know what to do with it) and returns the result both as structuredContent and as JSON text content:
{
"threadId": "550e8400-e29b-41d4-a716-446655440000",
"corpusIds": ["…", "…"],
"questionId": "…",
"answerId": "…",
"agentId": "…",
"lang": "en",
"answer": "The corpus mainly covers customer support runbooks…",
"sources": [
{"docId": "…", "filename": "runbook.pdf", "pages": [3, 4], "summary": "…"}
]
}A corpus of another organization, an unknown corpus or agent, an organization without any corpus,
or a missing question returns a tool error, and nothing is written — not even the thread.
get_document_markdown
get_document_markdownRead one whole document as Markdown — the text ingestion extracted from the original file, headings, lists and
tables included. Where rag_query answers from a few excerpts, this gives the assistant the full document: typically
one of the sources a rag_query answer cites, by its docId.
| Argument | Required | Description |
|---|---|---|
docId | yes | UUID of the document |
Where GET /v1/doc/{id}/md hands out a presigned URL, the tool returns the content itself — an assistant cannot follow
a URL into storage. The result holds a single embedded resource whose mimeType is text/markdown:
{
"content": [
{
"type": "resource",
"resource": {
"uri": "verbatim://doc/550e8400-e29b-41d4-a716-446655440000/md",
"mimeType": "text/markdown",
"text": "# Annual report 2025\n\nRevenue grew **12%** over the year…"
}
}
],
"isError": false
}The uri identifies the document; it is not fetchable — the server exposes no MCP resources. The whole conversion is
returned, with no truncation: a long document is a long result, which counts against the assistant's context.
A document that has not been converted yet — still PENDING or PROCESSING, or FAILED before conversion — returns a
tool error whose message carries its status: try again once it is READY. A document of another organization, an
unknown one or a missing docId returns a tool error too.
Connecting a client
Claude Code
claude mcp add --transport http verbatim https://<api-host>/mcp \
--header "Authorization: Bearer <access-token>"Any client with a JSON configuration (.mcp.json, Cursor, …)
{
"mcpServers": {
"verbatim": {
"type": "http",
"url": "https://<api-host>/mcp",
"headers": {"Authorization": "Bearer <access-token>"}
}
}
}Raw JSON-RPC, to check a token:
curl -s https://<api-host>/mcp \
-H "Authorization: Bearer <access-token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'Connectors that require OAuth (claude.ai custom connectors, for instance) are not supported yet:
the server takes a token you mint, it does not run an OAuth authorization flow.
Updated about 1 hour ago