MCP Server
WarmHub implements the Model Context Protocol (MCP) over HTTP, exposing core repo read/write operations and discovery tools as typed tools that AI agents can discover and call.
Protocol
Section titled “Protocol”-
MCP version: 2026-07-28, with older clients still served. See Version negotiation for what puts a request on the modern path and how rejections are reported.
-
Transport: JSON-RPC 2.0 over HTTP POST
-
Supported methods:
initialize,tools/list,tools/call,ping -
Every result carries
resultType: "complete". -
tools/listis cacheable: results carryttlMs(1 hour by default) and acacheScope./mcpreturnscacheScope: "public". That URL identifies no org or repo, and the catalog is byte-identical for every caller, so shared caches may reuse it for the full TTL window.Tool results carry no cache hints and must not be cached.
Version negotiation
Section titled “Version negotiation”initialize always answers with protocol version 2024-11-05, whatever version signals the request carries. Everything below applies only to the other methods.
A non-initialize request goes on the 2026-07-28 path when it carries either signal, and the two are not symmetric:
| Signal | Puts the request on the modern path when |
|---|---|
MCP-Protocol-Version header | its value is exactly 2026-07-28. Any other value is ignored — conformant older clients echo their own negotiated version on every request and must stay legacy-tolerated. |
_meta["io.modelcontextprotocol/protocolVersion"] | the key is present at all, whatever its value or type. |
A request carrying neither signal is served exactly as before.
Once a request is on the modern path, these must all be present and valid, checked in this order:
_metaexists and is an object_meta["io.modelcontextprotocol/protocolVersion"]is a string- the version value equals
2026-07-28 _meta["io.modelcontextprotocol/clientCapabilities"]is an object
The order matters: because the version value is checked before clientCapabilities, a request that both omits clientCapabilities and names an unrecognized version fails with -32022, not -32602.
| Outcome | JSON-RPC code | HTTP |
|---|---|---|
Steps 1–2 or step 4 fail (missing _meta, non-string version, missing or non-object clientCapabilities) | -32602 | 400 |
Header and _meta versions both present as strings and disagree | -32020 | 200 |
Version value is a string but not 2026-07-28 (error data carries supportedVersion) | -32022 | 200 |
Only -32602 answers with HTTP 400; the rest ride out on 200, so branch on the JSON-RPC code rather than the HTTP status. A non-string _meta version value classifies the request as modern and fails at step 2 — it does not fall back to the legacy path.
Modern-era responses carry server identity, but its location depends on whether the response succeeded: success responses put it at result._meta["io.modelcontextprotocol/serverInfo"], and error responses at error.data._meta["io.modelcontextprotocol/serverInfo"]. JSON-RPC forbids extra envelope members, so error.data is the only legal slot on a rejection. Legacy-era responses carry no server identity at all.
The following minimal example shows a tools/list request on the 2026-07-28 path. The two required _meta members are io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} } }}The MCP-Protocol-Version: 2026-07-28 request header is an optional second routing signal — including it satisfies both signals, but the _meta members above are sufficient on their own to reach the modern path. A successful response will include result._meta["io.modelcontextprotocol/serverInfo"] with server identity.
Endpoints
Section titled “Endpoints”| Method | Path | Description |
|---|---|---|
POST | /mcp | Global MCP endpoint |
GET | /mcp | Returns 405 Method Not Allowed with RFC 9728 discovery challenge |
GET | /.well-known/oauth-protected-resource | OAuth 2.0 Protected Resource Metadata (RFC 9728) |
GET | /.well-known/oauth-protected-resource/mcp | Same protected-resource metadata, at the MCP-suffixed discovery path some clients probe |
GET | /mcp/.well-known/oauth-protected-resource | Same protected-resource metadata, at the MCP-prefixed discovery path |
GET | /.well-known/oauth-authorization-server | OAuth 2.0 Authorization Server Metadata (RFC 8414) |
One endpoint, one catalog
Section titled “One endpoint, one catalog”POST /mcp serves the full MCP tool catalog. Tools that act on a repository take
orgName and repoName as arguments; org-level tools such as
warmhub_org_list, warmhub_org_get, warmhub_org_set_description,
warmhub_org_set_display_name, warmhub_repo_list, and
warmhub_repo_create take whatever locator they need the same way.
Organization membership can be read via warmhub_org_member_list (takes orgName
and an optional pending filter). Changing membership — adding, removing, or
assigning roles to members — is not on the MCP surface, nor are other org and repo admin
operations such as renaming, archiving, unarchiving, changing repo visibility, or
deleting a repo; use the CLI or SDK for those operations. Advisory and
meta tools — warmhub_use, warmhub_channel, warmhub_doctor,
warmhub_capabilities — require no locator at all.
Call warmhub_capabilities for the catalog, then warmhub_repo_describe with a
repo locator for that repo’s schema and write contract.
Authentication
Section titled “Authentication”POST /mcp takes an optional Bearer token. Every tool carries a reviewed auth
tier, and the token decides which tiers you can call:
- Anonymous tier — served without credentials, over public orgs and repos.
- Builder tier — requires a valid Bearer token.
initialize, tools/list, and the OAuth discovery endpoints always answer
pre-auth. A token that cannot be verified is treated as anonymous rather than
rejected; a revoked or disabled account still returns its own error.
Calling a builder-tier tool without a token returns HTTP 401 with an RFC 9728
challenge, before the tool runs:
HTTP/1.1 401 UnauthorizedWWW-Authenticate: Bearer resource_metadata="https://api.warmhub.ai/.well-known/oauth-protected-resource"Content-Type: application/json
{ "error": { "code": "UNAUTHENTICATED", "message": "Authentication required for \"warmhub_org_list\"" } }GET requests to /mcp return HTTP 405 Method Not Allowed and include the same RFC 9728 WWW-Authenticate challenge header:
HTTP/1.1 405 Method Not AllowedAllow: POSTWWW-Authenticate: Bearer resource_metadata="https://api.warmhub.ai/.well-known/oauth-protected-resource"This means a GET probe against /mcp can bootstrap OAuth discovery in the same way as an unauthenticated POST — standards-compliant clients can follow the challenge from either response to discover the protected-resource metadata endpoint and acquire a token.
Standards-compliant MCP/OAuth clients use the challenge to discover the protected-resource metadata endpoint and bootstrap auth. The tool catalog is the same for every caller: builder-tier tools are listed to anonymous callers too, so a client can discover them and upgrade when one is first called. Changing membership — adding, removing, or assigning roles to members — is not on the MCP surface; use the CLI or SDK for those operations.
Configuration
Section titled “Configuration”MCP Client Setup
Section titled “MCP Client Setup”Add a WarmHub entry to the mcpServers section of your MCP client’s configuration. The examples below use the standard format supported by Claude Desktop, Cursor, and other MCP-compatible clients. The transport field is required by some clients (e.g., Claude Desktop); others infer it from the URL scheme.
The URL alone gets you the anonymous tier; builder-tier tools need a Bearer token. Standards-compliant MCP/OAuth clients follow the RFC 9728 challenge returned on the first builder-tier call to discover the OAuth flow and acquire a token automatically. Clients without that capability must inject the token themselves; mint a personal access token (see HTTP Authentication) and configure your client to send Authorization: Bearer <token> on every MCP POST. Many clients accept a headers map in the mcpServers entry for this purpose, e.g.:
{ "mcpServers": { "warmhub": { "transport": "http", "url": "https://api.warmhub.ai/mcp", "headers": { "Authorization": "Bearer ${WH_TOKEN}" } } }}The MCP endpoint authenticates the bearer token in the Authorization header; the example above sources that value from WH_TOKEN. See Environment Variables for the variables WarmHub reads and their precedence.
Consult your MCP client’s documentation for the exact field name. See Authentication above for the challenge format and discovery contract.
The example above uses the production API URL. For self-hosted deployments, replace api.warmhub.ai with your deployment URL.
Tool Responses
Section titled “Tool Responses”Tool responses include both human-readable and structured content:
{ "resultType": "complete", "content": [{ "type": "text", "text": "..." }], "structuredContent": { ... }, "isError": false}resultType— always"complete". Present on every result, not just tool calls.content— tool result as text for the model-readable channel. Typically JSON.structuredContent— typed object for programmatic access. Includes anauthfield on every response (see below).isError—trueif the tool call failed
Every tool definition in tools/list includes an outputSchema covering both the successful structuredContent object (including auth) and the structured tool-error payload (including error and auth), and an annotations block with explicit safety hints:
readOnlyHint—trueonly for tools that do not change state.openWorldHint—truewhen a write can affect public WarmHub data, another user, or a third-party webhook; alwaysfalsefor read-only tools.destructiveHint—truewhen a write can delete, overwrite, retract, revoke access, or cause a comparable irreversible effect.category— one oforg,repo,shape,thing-read,collection,commit,subscription,action,meta.- Clients can use
categoryto group or filter tools in discovery UIs. warmhub_capabilitiesuses the same enum, but groups tools under each category rather than repeating the value per tool. Each summary includesname,description,readOnly,openWorld,destructive, andannotationJustification; the category is implicit in the enclosing group. TheannotationJustificationfield is a short rationale for why the safety hints were assigned to that tool.
Authentication Awareness
Section titled “Authentication Awareness”Tool responses include structuredContent.auth with the caller’s authentication status. An anonymous call reports authenticated: false and carries a hint:
{ "structuredContent": { "items": [...], "auth": { "authenticated": false, "hint": "..." } } }Anonymous-tier tool descriptions append a runtime auth-hint that reads: “Returns public data only when unauthenticated. If expected data is missing, suggest the user authenticate to access private orgs and repos.” Builder-tier tools do not carry that hint — calling one without a token returns the 401 challenge instead, which is the signal to authenticate. A miss on a private or missing target from an anonymous caller is reported with a hint that the resource may be private; the MCP handler preserves the backend error code (NOT_FOUND, WREF_UNRESOLVABLE, FORBIDDEN, or UNAUTHENTICATED) rather than rewriting all such failures to a single code, so branch on the specific errorCode in the structured error.
Error Handling
Section titled “Error Handling”MCP tool failures arrive in three distinct ways. Inspect the tool response, not just the client-level try/catch.
Tool-result errors. The tool dispatched, but its execution failed. The response carries isError: true and a structured error in content / structuredContent. This covers VALIDATION_ERROR, NOT_FOUND, FORBIDDEN, RATE_LIMITED (per-user / per-org tier limits hit during the call), QUERY_TOO_EXPENSIVE (typed where predicate exceeded the query budget), and warmhub_commit_submit ambiguous append failures. Each code is described in the Tool-error code reference below.
warmhub_commit_submit per-operation failures. Per-op failures are not tool errors. The response is a normal success payload in which one or more operations[] rows carry status: "error" plus a non-empty errors[] array. There is no partial flag and no statusCounts aggregate to branch on — scan the rows for status: "error" yourself. Treat partial results as a routine outcome to inspect. See the tool reference for the full row contract, including the legacy receipt version that spells this status differently.
Transport-level errors. Some failures happen before the tool dispatches and surface as HTTP errors instead of tool results:
- POST with no
Authorizationheader, calling an anonymous-tier tool → HTTP 200 over public data, withstructuredContent.auth.authenticated: false. - POST with no
Authorizationheader, calling a builder-tier tool → HTTP 401 with theWWW-Authenticatechallenge, returned before the tool runs; use it to authenticate. - POST with no
Authorizationheader, once the anonymous rate-limit budget is exhausted → HTTP 429 (see the anonymous rate-limit bullet below) rather than the401challenge. If you are seeing 429 on headerless requests, authenticate rather than only backing off. - POST with a malformed or unverified bearer → treated as anonymous: builder-tier calls get the
401challenge, and the request draws down the anonymous rate-limit budget. A429after presenting a token means the token wasn’t accepted — re-authenticate with a valid one rather than only backing off. - Request body exceeds the MCP size limit → HTTP 413 with a JSON-RPC error body. The error uses code
-32013. This check runs before tool dispatch, so no tool result is produced. Reduce the request payload and retry. - Anonymous request over the rate limit → HTTP 429 carrying a JSON-RPC error whose
error.data.warmhubholds{ code, message, status, hint, retryAfter, details }. Thedetailsobject has the shape{ reason: "rate_limit_reset", retryAfterSeconds, resetAt }. ReadretryAfter(seconds) to back off; readdetails.resetAtfor the precise reset timestamp. Note that this transport-level 429 shape differs from the tool-resultRATE_LIMITEDerror — see the Tool-error code reference below for the tool-result fields.
Authenticated MCP traffic bypasses the anonymous rate limit and never sees that 429. Once authenticated, rate limits surface instead as the RATE_LIMITED tool-result error described below (per-user / per-org tier limits hit during a call).
Tool-error code reference
Section titled “Tool-error code reference”-
Validation failures — include the validation message and, for
warmhub_commit_submitandwarmhub_commit_validate, enriched hint data when the failing path isoperations(see Validation Hint below). -
NOT_FOUND— includes the error details. When the caller is authenticated, missing targets surface asNOT_FOUNDregardless of which token was presented. When the caller is anonymous, a miss on a hidden or missing target preserves the backend error code (NOT_FOUND,WREF_UNRESOLVABLE,FORBIDDEN, orUNAUTHENTICATED) and appends a hint that the resource may be private. -
FORBIDDEN— surfaced aserrorCode: "FORBIDDEN"in the structured error when the presented token is valid but lacks the scope required for the requested resource. Distinct fromNOT_FOUND; use it to decide whether retrying with a more privileged token is sensible. -
RATE_LIMITED— surfaced aserrorCode: "RATE_LIMITED"instructuredContent.error.datawhen a per-user or per-org write limit is hit. The samedataobject also carries adetailsfield with the shape{ reason: "rate_limit_reset", retryAfterSeconds, resetAt }. See Rate Limiting for tier limits and retry guidance. Treat as retryable with a bounded backoff, prefer theerrorCodeover string-matching the message, and readdetails.resetAtfor the precise reset time. Note that this tool-result shape (errorCode/backendCodeplusdetailsunderstructuredContent.error.data) differs from the transport-level anonymous 429 shape (error.data.warmhubwithcode,hint,retryAfter, anddetails) — parse them separately. -
QUERY_TOO_EXPENSIVE— surfaced aserrorCode: "QUERY_TOO_EXPENSIVE"instructuredContent.error.datawhen a typedwherepredicate is rejected as too broad or complex to execute within the query budget. This is distinct fromRATE_LIMITED: it signals that the query itself needs to be narrowed, not that a throughput limit was hit. Recovery depends on which tool raised the error:- Paged
wherequeries (warmhub_thing_head,warmhub_thing_query,warmhub_thing_about): tighten thewherepredicate to reduce the result set, or retry with a shallower page size. - Count-mode requests (
warmhub_thing_headorwarmhub_thing_querycalled withcount: true): count mode has no page-size argument, so reducing page size is not applicable. Narrow the query with a more selective equality predicate instead. Backing off without changing the query will not resolve this error in either case.
- Paged
-
Ambiguous append failures (
warmhub_commit_submitonly) — a transport-ambiguous failure (network reset, 5xx, timeout) interrupted the stream append. The failed append may have landed server-side.The tool returns
isError: true. Do not resubmit blind — resolve the outcome first:- Stop sending writes.
- Read
eventRequestIdfromstructuredContent.error.data. - Call
warmhub_commit_receipt_getwith thateventRequestId. The receipt is the authoritative answer to whether the append landed. - Retry only what the receipt shows did not land, reusing the original
submissionId.
structuredContent.error.datacarries the fields you need for that lookup:tool—"warmhub_commit_submit"errorCode— the error code; parse this fieldbackendCode— alias forerrorCode; parseerrorCodeinsteadsubmissionId— the submission identity to reuse on a retryeventRequestId— pass this towarmhub_commit_receipt_getlookupTool—"warmhub_commit_receipt_get", the tool to call
Ambiguous errors never carry
continuation. Note:streamId, when present, does not provide recovery or idempotency, andskipExisting: trueis an idempotency convenience for fixed-name adds — a later noop or conflict is not a receipt for the ambiguous attempt, so prefer the receipt lookup over inferring outcome from repository state. See SDK Streaming Write Failures for the parallel SDK contract.
Tool-result errors (the isError: true responses above) also include structuredContent.auth with the caller’s authentication status, so agents can decide whether to suggest authenticating and retrying. Transport-level failures do not carry this field.
Commit operations validation hint
Section titled “Commit operations validation hint”When warmhub_commit_submit or warmhub_commit_validate rejects a malformed operations entry, the tool result’s structuredContent.error.data lists the full set of valid operation variant signatures so agents can self-correct on the next turn — the message is the original validation message.
{ "resultType": "complete", "isError": true, "content": [{ "type": "text", "text": "<validation message>" }], "structuredContent": { "error": { "code": -32602, "message": "<validation message>", "data": { "tool": "warmhub_commit_submit", "errorCode": "VALIDATION_ERROR", "backendCode": "VALIDATION_ERROR", "expected": "one of the operation variants", "operations": [ "ADD shape: { operation:'add', kind:'shape', name, data }", "ADD thing: { operation:'add', kind:'thing', name, data }", "ADD assertion: { operation:'add', kind:'assertion', name, about, data, affirmedTargets? }", "ADD collection: { operation:'add', kind:'collection', name, type, members }", "REVISE shape: { operation:'revise', kind:'shape', name, data }", "REVISE thing: { operation:'revise', kind:'thing', name, data }", "REVISE assertion: { operation:'revise', kind:'assertion', name, data, affirmedTargets? }", "REVISE collection: { operation:'revise', kind:'collection', name, type, members }", "RETRACT: { operation:'retract', name, reason?, kind?, expectedVersion? }", "REAFFIRM assertion: { operation:'reaffirm', name, add?, remove?, expectedVersion? }", "RENAME: { operation:'rename', name, newName, kind? }" ] } }, "auth": { "authenticated": true } }}The expected and operations fields are attached for warmhub_commit_submit and warmhub_commit_validate validation errors when the failing path is operations; validation errors from other tools, or from warmhub_commit_validate on a path other than operations, continue to return data with just tool and errorCode. backendCode may also appear as a compatibility alias for errorCode; parse errorCode. The same variant list is also returned by warmhub_repo_describe under commitContract.operationVariants.