MCP conformance
The signer speaks the Model Context Protocol with a handler written by hand (src/serve/mcp.rs), not an SDK (Design decisions says why). This page is the record of what it implements, checked against the specification’s pages on 2026-11-11. The Rust MCP SDK (rmcp) is a test-only dependency that checks the handler’s messages (Checked by the SDK).
The specification to read is the one at modelcontextprotocol.io, which is built from the modelcontextprotocol/specification repository. Other sites reproduce it; when they differ, that one is right. Each revision has its own tree, such as https://modelcontextprotocol.io/specification/2026-07-28/, and its TypeScript schema is the source of truth for message shapes.
Revision
The signer speaks 2026-07-28 and nothing earlier. That revision is stateless: there is no initialize handshake and no session, and every request carries its protocol version and the client’s capabilities in params._meta.
A client of an earlier revision cannot use the signer. Its initialize is answered with error -32022 (unsupported protocol version) naming 2026-07-28, on HTTP with status 400, so the client can say why. The specification gives such a client no way to move forward on its own. Claude Code speaks 2026-07-28 by default from version 2.1.274, by its own documentation.
Scope
The service is an MCP server with two features: tools and resources. It offers no prompts, completions, subscriptions, tasks or extensions, never asks the client for input (every result is complete, never input_required), and sends no notifications. Its lists are short and fixed, so they are never paginated and never change.
| Method | Answer |
|---|---|
server/discover | supportedVersions (2026-07-28), capabilities (tools, resources), and instructions |
tools/list | check, sign, key_show and halt, each with an inputSchema, always in that order |
tools/call | One text content block and isError (Serving over HTTP and MCP) |
resources/list, resources/read | Four static texts (Resources and the skill) |
| any notification | Accepted and ignored |
initialize | Error -32022, as above |
anything else, ping included | Error -32601, method not found, on HTTP with status 404 |
Every result carries resultType: "complete" and names the server in _meta["io.modelcontextprotocol/serverInfo"]. The discovery result, both lists and a resource read also carry the caching hints the revision requires: ttlMs of 300000 and cacheScope private, since every answer is behind a bearer token.
A request
| Rule | The service |
|---|---|
A request’s id is a string or an integer, never null | Anything else is error -32600, status 400 |
params._meta carries io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities; a request without them is error -32602, status 400 | Yes. The capabilities must be an object; the service needs none, so it reads no further |
A version the server does not implement is error -32022 with data.supported and data.requested, status 400 | Yes |
| A server must not rely on earlier requests for context | The handler keeps no state between requests |
The HTTP transport
POST /mcp on sa-forge-signer-serve follows the Streamable HTTP transport, in its plain form: one JSON object per reply, no event stream.
| Rule | The service |
|---|---|
A request is answered with Content-Type: application/json or an event stream | Always application/json |
An accepted notification gets 202 Accepted with no body | Yes |
Every POST carries MCP-Protocol-Version, matching the body’s _meta; a missing or different one is error -32020 (header mismatch), status 400 | Yes |
Every request carries Mcp-Method, matching the body’s method | Yes: missing or different is -32020, status 400 |
tools/call and resources/read carry Mcp-Name, matching params.name or params.uri | Yes: missing or different is -32020, status 400. A value sent as =?base64?...?= is decoded before it is compared |
A method the server does not implement is 404 Not Found with error -32601 | Yes |
A server of this revision alone answers GET and DELETE with 405 Method Not Allowed | Yes |
The Origin header must be validated, and an invalid one answered with 403 Forbidden | Yes: Origin and Host must name an entry in allowed_hosts |
Mcp-Session-Id and Last-Event-ID are ignored | They are never read |
The checks run in this order: the body is JSON and one message, initialize, the _meta fields, the headers against the body, then the version. Authentication is outside the specification’s own scheme: the service takes a bearer token from its config, not OAuth. A request without a known token is 401 before the body is read, with a plain JSON body, not a JSON-RPC error.
Tool arguments are not mirrored into Mcp-Param-* headers: no tool marks a parameter with x-mcp-header.
Tools and resources
| Rule | The service |
|---|---|
A tool that ran and failed reports it in its result with isError: true; a protocol problem is a JSON-RPC error | A report whose outcome is not ok or confirmed, a refused key and a failed halt are results with isError: true |
An unknown tool is error -32602 | Yes |
| A server with tools or resources declares the capability | Both declared, as empty objects: no listChanged, no subscribe |
A resource that does not exist is error -32602; -32002 must not be sent | -32602 with the uri in data |
A listed resource has uri and name | uri, name, description and mimeType |
The stdio transport
sa-forge-signer-mcp is the stdio side. It is not a server of its own: it relays each message to the service above, so the methods and the rules are the service’s.
| Rule | The binary |
|---|---|
| Messages are JSON-RPC, one per line, with no newline inside a message | Each reply is written as one line, whatever the service’s formatting |
| Standard output carries only protocol messages | Yes: anything else goes to standard error |
| A notification gets no reply | None is written, also when the service cannot be reached |
On stdio the request metadata is only in _meta; there are no headers | The binary builds the three HTTP headers from each message’s body before forwarding it |
A client probing with server/discover gets a discovery result or a recognised error | The service’s answer is passed through, as is its -32022 for an initialize |
When the service cannot be reached, or answers outside JSON-RPC (a wrong token is HTTP 401), the binary writes a JSON-RPC error with code -32603 for that request’s id. A line that is not JSON gets -32700 with no id.
Checked by the SDK
src/serve/mcp_sdk_tests.rs reads the handler’s messages back with the types of rmcp, the Rust MCP SDK, pinned to one version as a dev-dependency. The binaries do not link it. The tests check that:
- the requests the tests send parse as the SDK’s client requests, so they model a real client;
- the results of
server/discover,tools/list,tools/call,resources/listandresources/readparse as the SDK’s types for them, withresultType, the caching hints and the server’s name where the SDK reads them; - each error parses as the SDK’s error with the code the SDK defines for it, and the unsupported-version error equals the one the SDK itself builds.
Not checked
- The SDK’s types are looser than the specification in places: fields the
2026-07-28schema requires, such asresultTypeon a list, are optional in the SDK because it also models earlier revisions. The tests assert those fields themselves. - No MCP client has been run against a live service. The SDK’s client is not used: it would bring an HTTP stack into the tests for little beyond what the type checks give. The tests in
src/serve/andsrc/mcp_cli.rssend requests built by hand. - The specification’s TypeScript schema was read only in part; the pages for the base protocol, versioning, discovery, both transports, tools, resources, pagination and the changelog were read in full.
Keeping this page true
A change to src/serve/mcp.rs, src/mcp_wire.rs or src/mcp_cli.rs that adds a method, a header, a status or an error code updates this page and src/serve/mcp_sdk_tests.rs in the same change. When rmcp is bumped, a test that stops passing is a difference between the handler and the SDK’s reading of the specification: find out which is right before changing either. Before supporting a new revision, read its changelog page first: it lists what was removed as well as what was added.