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
-
Origin check: a request carrying an
Originheader outside the deployment’s allowed origins (loopback origins are always allowed) is refused with HTTP 403 and a JSON-RPC error body, on every method; a request with noOriginheader — the normal case for non-browser clients — is unaffected. -
Supported methods depend on which path the request is on. Legacy:
initialize,tools/list,tools/call,ping. Modern (2026-07-28):server/discover,tools/list,tools/call—pingis a legacy-only mechanism there, andserver/discoveris not served on the legacy path.initializealways selects the legacy path, even when decorated with modern signals. Anything outside a path’s set answers-32601. -
Every modern-path result carries
resultType: "complete". Legacy results carry noresultType. -
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.
The initialize result also carries instructions — server-level guidance covering anonymous access and the CLI naming convention, built from the same constants warmhub_capabilities returns. It is a standard optional InitializeResult field. Client support varies: Claude Code honors it, while Claude.ai connectors currently ignore it, so treat it as a bonus and never as the only place a fact is stated.
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 anything other than a known older revision (2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25). Those stay legacy-tolerated because conformant older clients echo their own negotiated version on every request; any other value is a version declaration and gets the modern verdict, including an unrecognized one. |
_meta["io.modelcontextprotocol/protocolVersion"] | the key is present at all, whatever its value or type — with one exception: the exact string 2024-11-05 is the revision the legacy path serves, so a request declaring it is served as legacy rather than judged against the modern contract. (A 2026-07-28 header alongside it still wins classification, and the two then disagree — see step 2 below.) Other older revisions in _meta are not served and get -32022. |
A request carrying neither signal is served exactly as before.
Once a request is on the modern path, it is validated in this fixed order. The order is the contract: each step is free to reject before a later step could judge the request against rules a future revision may have changed.
- Version is a string. If
_meta["io.modelcontextprotocol/protocolVersion"]is present it must be a string. - Version agreement. If the header and
_metaboth carry a version string, they must be equal. A request that contradicts itself gets no verdict about either value. - Version verdict. The declared version — the
_metavalue when present, otherwise the header — must be2026-07-28. - Required headers. See Required headers. Skipped for notifications (requests with no
id). _metaenvelope._metaexists, is an object, and carries a string_meta["io.modelcontextprotocol/protocolVersion"]and an object_meta["io.modelcontextprotocol/clientCapabilities"].
Because the verdict runs ahead of steps 4 and 5, a request that names an unrecognized version fails with -32022 whatever else is wrong with it — a missing clientCapabilities or a contradicting Mcp-Method never changes that answer.
Required headers
Section titled “Required headers”Every modern-era request that expects a response must carry:
| Header | Rule |
|---|---|
MCP-Protocol-Version | present |
Mcp-Method | present and exactly equal to the request’s method |
Mcp-Name | present on tools/call, resources/read, and prompts/get, and exactly equal to params.name (params.uri for resources/read). On any other method there is no target to name, so the header is ignored rather than rejected. |
Header names are case-insensitive; values are compared case-sensitively. A value outside the RFC 9110 field-value range is rejected. A Mcp-Name may be sent in the =?base64?<base64 of the UTF-8 name>?= sentinel form — the markers are lowercase and case-sensitive — and is decoded before the comparison.
These rules bind only clients that declare 2026-07-28. A legacy-path request needs no protocol headers at all.
WarmHub does not serve resources/read or prompts/get — both answer -32601 with HTTP 404. They still appear in the table because header validation runs before dispatch: a request for either that omits or mismatches Mcp-Name gets -32020 with HTTP 400 first, and never reaches the 404.
Status and code by outcome
Section titled “Status and code by outcome”| Outcome | JSON-RPC code | HTTP |
|---|---|---|
Header and _meta versions both present as strings and disagree | -32020 | 400 |
Version value is a string but not 2026-07-28 (error data carries supported and requested) | -32022 | 400 |
| A required header is missing, mismatched, or malformed | -32020 | 400 |
Malformed _meta (missing, not an object, missing or non-string version, missing or non-object clientCapabilities) | -32602 | 400 |
Method not served on the modern path, ping and unknown methods included | -32601 | 404 |
Unknown tool name on tools/call | -32602 | 200 |
| Tool execution error | -32000 (in an isError result) | 200 |
Notification (no id) accepted | — (no body) | 202 |
Legacy-path responses are unaffected: every legacy JSON-RPC error still rides out on HTTP 200, and a legacy notification also answers 202. A non-string _meta version value classifies the request as modern and fails at step 1 — 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. Both _meta members — io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities — are required. Sending the MCP-Protocol-Version: 2026-07-28 header without these _meta fields fails the envelope step and returns -32602 with HTTP 400:
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": { "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientCapabilities": {} } }}That request must also carry the headers MCP-Protocol-Version: 2026-07-28 and Mcp-Method: tools/list — see Required headers. The _meta members are required whether or not the header is present, and vice versa. A successful response will include result._meta["io.modelcontextprotocol/serverInfo"] with server identity.
server/discover
Section titled “server/discover”server/discover is the modern path’s handshake-free orientation call. It takes no arguments beyond the _meta members every modern request carries, needs no Bearer token, and answers with:
| Field | Meaning |
|---|---|
supportedVersions | Every protocol revision this server serves, newest first: ["2026-07-28", "2024-11-05"]. This is the union across both paths — initialize still counter-offers only 2024-11-05. |
capabilities | The same capability object initialize returns ({ "tools": { "listChanged": false } }). |
instructions | The same server-level guidance the initialize result carries. |
ttlMs / cacheScope | Cache hints matching tools/list: 1 hour by default, "public" — the result names no org, repo, or caller. |
It is served on the modern path only. A legacy-path server/discover answers
-32601, and so does a modern-path ping. initialize always selects the
legacy path, even when decorated with modern signals.
Endpoints
Section titled “Endpoints”| Method | Path | Description |
|---|---|---|
POST | /mcp | Global MCP endpoint |
GET | /mcp | Returns 405 Method Not Allowed with RFC 9728 discovery challenge. A disallowed Origin is refused with 403 first — see Authentication |
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_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, editing org or repo descriptions, archiving, unarchiving,
changing repo visibility, or deleting a repo; use the CLI or SDK for those operations. Meta
tools — warmhub_doctor and 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, server/discover, tools/list, and the OAuth discovery
endpoints answer pre-auth on the path that serves them — initialize on the
legacy path, server/discover on the modern one, tools/list on both. 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://mcp.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://mcp.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.
The Origin check runs ahead of the method check, so this is the response for a request with no Origin header — the normal case for a non-browser client — or one whose Origin is allowlisted or loopback. A GET carrying a disallowed Origin is refused with HTTP 403 and a JSON-RPC -32600 error instead, and that response carries neither Allow nor WWW-Authenticate:
HTTP/1.1 403 ForbiddenContent-Type: application/json
{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":"Origin not allowed"}}The resource field in the protected-resource metadata document and the resource_metadata URL in every WWW-Authenticate challenge are host-specific: they reflect the MCP hostname the client actually reached, when that host is owned by the deployment. A client reaching mcp.example.com sees resource_metadata pointing at https://mcp.example.com/.well-known/oauth-protected-resource; a client reaching api.example.com sees the equivalent URL for that host. MCP/OAuth clients that follow the challenge to the protected-resource document will therefore always receive metadata scoped to the host they contacted, not a single canonical API origin.
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 type field names the transport; some clients infer it from the URL scheme, but stating it is always safe. On claude mcp add the equivalent flag is spelled --transport, and the entry it writes uses type.
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": { "type": "http", "url": "https://mcp.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 MCP URL. For self-hosted deployments, replace mcp.warmhub.ai with your deployment URL.
Updating an older configuration
Section titled “Updating an older configuration”Point new configurations at mcp.warmhub.ai. An existing one on api.warmhub.ai/mcp needs no change — the API hostname serves /mcp as a compatibility path, and both hostnames reach the same server — so switch it the next time you edit the file.
If your agent calls tools by remembered name, check them against the tables below. There are no aliases, so an unrecognized name answers Unknown MCP tool immediately. Calling warmhub_capabilities at the start of a session avoids the problem entirely: it returns the current catalog.
| Use this | Instead of |
|---|---|
warmhub_subscription_log | warmhub_action_livefeed |
warmhub_subscription_attempts | warmhub_action_attempts |
warmhub_subscription_runs | warmhub_action_runs |
warmhub_notifications | warmhub_action_notifications |
warmhub_subscription_* with scope: "org" | warmhub_org_subscription_* (7 tools) |
warmhub_thing_get, which takes wrefs at every cardinality | warmhub_thing_get_many |
warmhub_commit_submit | warmhub_commit_apply |
warmhub_repo_content_get / warmhub_repo_content_set, with kind | warmhub_repo_readme_get / _set, warmhub_repo_agents_get / _set, warmhub_repo_llms_txt_get |
Some tools have no replacement on the MCP surface. Reach for these instead:
| Instead of | Do this |
|---|---|
warmhub_wref_resolve | Call warmhub_thing_get with includeRetracted: true — a superset of the same identity fields |
warmhub_repo_content_generate, warmhub_repo_readme_generate, warmhub_repo_agents_generate | Draft with wh repo content prompt, then persist with warmhub_repo_content_set |
warmhub_org_set_display_name, warmhub_repo_set_display_name | wh org rename or wh repo rename, or the web app |
warmhub_org_set_description, warmhub_repo_set_description | wh org update or wh repo update, or the web app |
warmhub_use and warmhub_channel have no equivalent. Both were local client conveniences rather than repo operations, so there is nothing to call in their place.
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 modern-path result, not just tool calls; legacy-path results omit it.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 call still returns structuredContent for programmatic access. WarmHub keeps reviewed output schemas internally for contract tests and schema-clarity audits covering successful structured results (including auth) and structured tool-error payloads (including error and auth), but does not repeat those schemas in tools/list. Each listed tool includes its input schema 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,title,description,tier,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.- Every listed tool also carries a top-level
title— a short display name for tool pickers and permission prompts. Tool names followwarmhub_<noun>_<verb>↔wh <noun> <verb>;warmhub_capabilitiesreturns the two aliases, the eight exceptions, and the two tools with no CLI command undernaming. See the CLI name convention.
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": "..." } } }The tier itself is not in the descriptions. Read it from warmhub_capabilities, where every tool summary carries tier: 'anonymous' | 'builder'; the anonymous-access guidance is stated once there as a usage pattern. Calling a builder-tier tool without a token returns the 401 challenge, which is the signal to authenticate. A miss on a private or missing target from an anonymous caller is reported in auth.hint as possibly 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. - POST with a valid bearer the gate never got to verify → treated as anonymous, and therefore able to draw a transport-level 429 even though the token itself is good. Verifying an unrecognized bearer is admitted through its own per-IP budget, separate from the anonymous request budget; when that budget is exhausted the token is not checked at all and the request falls back to the anonymous path. A token verified recently enough to still be cached skips this budget entirely, so this affects bursts of first-time tokens from one IP. Back off and retry rather than re-issuing the token. The 429 body is identical to the anonymous case described below.
- 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 transport error (code
-32029) whose retry metadata lives aterror.data.warmhub:{ code, message, status, hint, retryAfter, details }. A request whose bearer was never verified reaches this same path — see the verification-budget bullet above. Thedetailsobject has the shape{ reason: "rate_limit_reset", retryAfterSeconds, resetAt }. ReadretryAfter(seconds) to back off; readdetails.resetAtfor the precise reset timestamp. This transport-level 429 shape — with retry metadata aterror.data.warmhub.retryAfter— differs from the tool-resultRATE_LIMITEDerror; see the Tool-error code reference below for the tool-result fields.
Once a bearer is cryptographically verified, MCP traffic bypasses the anonymous rate limit entirely — the pre-authentication gate is skipped rather than passed through, so a verified token never draws a transport-level 429. Per-user and per-org tier limits hit during a call surface instead as the RATE_LIMITED tool-result error described below. The one way a token-bearing request still sees a transport-level 429 is the verification-budget fallback above, where the token was never verified and the request was handled as anonymous.
Parsing transport-level 429 responses. Both origins — an anonymous request, and a bearer the gate never got to verify — produce the same response shape: an HTTP 429 with a JSON-RPC error body where retry metadata lives at error.data.warmhub. Do not look for backendCode or isError on a 429 — those fields belong to tool-result errors and are absent here. Branch first on the HTTP status code: if it is 429, read error.data.warmhub.retryAfter and error.data.warmhub.details.resetAt; if it is 200 with isError: true in the body, parse the tool-result error fields instead.
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 during a tool call. 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 (errorCodeplusdetailsunderstructuredContent.error.data) differs from the transport-level 429 shape (JSON-RPC error code-32029, witherror.data.warmhubcarryingcode,message,status,hint,retryAfter, anddetails) — on a transport-level 429 no tool ran, so there is no tool result to read. Parse them separately: check forisError: truein a tool result to identify aRATE_LIMITEDtool error; check for an HTTP 429 response with a JSON-RPC error body to identify a transport-level rate limit. -
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.