MCP Protocol Reference
MCP Protocol Reference
Reference material for Hazaar's MCP implementation: what is on the wire, what is validated, and what is deliberately not implemented. For writing tools, see Defining Tools.
Hazaar implements protocol revision 2026-07-28 over the Streamable HTTP transport, and also accepts the two prior revisions still widely negotiated by real clients, 2025-11-25 and 2025-06-18. All three share the same stateless initialize/tools/list/tools/call subset; see Not implemented for what those earlier revisions additionally allow that Hazaar does not implement.
Endpoint
A single URL accepting POST. Each JSON-RPC message is one HTTP request.
| Method | Behaviour |
|---|---|
POST | Handles a JSON-RPC request or notification. |
GET | 405 Method Not Allowed. |
DELETE | 405 Method Not Allowed. |
GET opened a standalone SSE stream and DELETE terminated a session in revisions up to 2025-11-25. Hazaar implements neither mechanism for any revision it accepts, so both return 405 regardless of the negotiated protocol version.
Supported methods
initialize (legacy handshake)
Not part of 2026-07-28, which carries protocol version and capabilities per-request instead. It exists purely for interoperability: real clients still open every connection with initialize / notifications/initialized even when they otherwise speak the modern, per-request model, so Hazaar answers it statelessly — no session is created, and nothing depends on the notifications/initialized that follows.
{
"protocolVersion": "2025-06-18",
"capabilities": { "tools": {} },
"serverInfo": { "name": "acme-crm", "version": "1.0.0" },
"instructions": "Tools for querying the Acme customer database.",
"resultType": "complete",
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "acme-crm", "version": "1.0.0" }
}
}protocolVersion echoes the client's requested version only when it is one of Protocol::SUPPORTED_VERSIONS; otherwise the response names 2026-07-28 instead, per the specification's negotiation rule, so a compliant client renegotiates rather than proceeding on a version every subsequent request would then fail with -32022.
server/discover
Mandatory in the specification. Returns the server's supported protocol versions, capabilities and identity in a single request, letting a client select a version without probing.
{
"resultType": "complete",
"supportedVersions": ["2026-07-28", "2025-11-25", "2025-06-18"],
"capabilities": { "tools": {} },
"instructions": "Tools for querying the Acme customer database.",
"ttlMs": 300000,
"cacheScope": "public",
"_meta": {
"io.modelcontextprotocol/serverInfo": { "name": "acme-crm", "version": "1.0.0" }
}
}instructions is emitted only when the $instructions property is set. Legacy clients call initialize instead of this — see above.
tools/list
Returns every registered tool definition. Tools are returned in registration order, which is stable across requests — clients cache the list, and a stable order improves prompt cache hit rates.
{
"resultType": "complete",
"tools": [ { "name": "...", "description": "...", "inputSchema": { } } ],
"ttlMs": 300000,
"cacheScope": "public"
}ttlMs is a freshness hint letting clients cache the list instead of polling. Adjust it with the $listCacheTTL property (milliseconds).
tools/call
Executes a tool. Requires the Mcp-Name header to match params.name.
{
"resultType": "complete",
"content": [ { "type": "text", "text": "..." } ],
"structuredContent": { },
"isError": false
}Any other method returns HTTP 404 with a JSON-RPC -32601. The 404 is deliberate: it is what lets a client distinguish a modern endpoint that lacks the method from a legacy server that does not host the endpoint at all.
Request headers
MCP-Protocol-Version, Mcp-Method, and Mcp-Name are all optional — plenty of real clients (including current claude.ai and Claude Desktop builds) carry this information only in the JSON-RPC body, never as headers, and Hazaar does not reject them for it. When a client (or an intermediary in front of it) does send one, its value is validated against the body:
| Header | Checked on | Must match, when present |
|---|---|---|
MCP-Protocol-Version | Any request | params._meta["io.modelcontextprotocol/protocolVersion"] |
Mcp-Method | Any request | method |
Mcp-Name | tools/call | params.name |
Content-Type | Any request | Must be application/json |
Accept | Any request | Should list application/json and text/event-stream |
If MCP-Protocol-Version is absent from both the header and the body's _meta, Hazaar assumes 2026-07-28 rather than rejecting the request.
Header names are compared case-insensitively; values are case-sensitive.
Values that cannot be expressed in plain ASCII are wrapped in a base64 sentinel, which Hazaar decodes before comparing against the body:
Mcp-Name: =?base64?c2VhcmNoQ3VzdG9tZXJz?=Why the headers are validated when present
The transport mirrors selected body fields into headers so that load balancers, gateways and observability tooling can route and inspect requests without parsing the body. If the two were allowed to disagree, a gateway could route or rate-limit on one value while the server executed another — a request-smuggling vector. So while sending the headers is optional, a header and body that disagree are always rejected with 400 and -32020.
Request metadata
A 2026-07-28 request carries its context in params._meta rather than a handshake. Legacy (2025-06-18/2025-11-25) requests other than initialize typically have no _meta at all — they identify their version, if at all, only via the MCP-Protocol-Version header:
| Key | Purpose |
|---|---|
io.modelcontextprotocol/protocolVersion | The revision this request uses. Must match the header, when the header is present. |
io.modelcontextprotocol/clientInfo | Client name and version, for display and logging. |
io.modelcontextprotocol/clientCapabilities | What the client supports. |
Every result carries io.modelcontextprotocol/serverInfo in its _meta, from the $serverName and $serverVersion properties. It is self-reported and unverified — for display and debugging only, never for security decisions.
Error codes
| Code | Name | HTTP | Cause |
|---|---|---|---|
-32700 | Parse error | 400 | Request body is not well-formed JSON. |
-32600 | Invalid request | 400 | Not a valid JSON-RPC 2.0 message. |
-32600 | Invalid origin | 403 | Origin header not permitted. |
-32601 | Method not found | 404 | RPC method not implemented by this server. |
-32602 | Invalid params | 200 | Unknown tool named in tools/call. |
-32603 | Internal error | 500 | Unhandled server-side failure. |
-32020 | Header mismatch | 400 | A present header disagrees with the body. |
-32022 | Unsupported protocol version | 400 | Requested revision not implemented. Carries data.supported. |
An unsupported version response lists what the server does speak, so the client can retry:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version: 2024-11-05",
"data": { "supported": ["2026-07-28", "2025-11-25", "2025-06-18"] }
}
}Tool execution failures are not protocol errors — they come back as a normal result with isError: true. See Error handling.
Notifications
A JSON-RPC message with no id is a notification. It is acknowledged with 202 Accepted and an empty body, without regard to headers — the MCP-Protocol-Version/Mcp-Method validation above only ever runs for requests. 2026-07-28's core protocol defines no client-to-server notifications over Streamable HTTP, but legacy clients send exactly one, notifications/initialized, as part of the initialize handshake — it is accepted the same way as any other notification.
Origin validation
Requests carrying an Origin header are validated, which is what prevents a malicious web page from driving a locally-bound MCP server via DNS rebinding.
By default only an origin whose host matches the request host is accepted. Browser-based clients served from another host need their origin listed explicitly:
class Assistant extends \Hazaar\Controller\MCP
{
protected array $allowedOrigins = [
'https://app.example.com',
];
}Requests without an Origin header — the normal case for a server-side or CLI agent — are not affected.
Server properties
| Property | Default | Purpose |
|---|---|---|
$serverName | hazaar-mcp | Server name reported in serverInfo. |
$serverVersion | 1.0.0 | Server version reported in serverInfo. |
$instructions | null | Guidance for the model on using the server as a whole. |
$allowedOrigins | [] | Permitted origins. Empty means same-host only. |
$listCacheTTL | 300000 | Cache hint in milliseconds for list results. |
Not implemented
Each of the following is optional in the specification, and omitting it leaves the server compliant.
| Feature | Notes |
|---|---|
| stdio transport | HTTP only. Local agents needing a subprocess server are not supported. |
| Resources and prompts | The capabilities are not declared, so clients will not request them. |
| SSE response streams | tools/call always answers with application/json. Streaming exists to carry progress notifications during long calls, which a tools-only server has none of. |
subscriptions/listen | Consequently listChanged is not declared — advertising it would oblige the server to support the listen stream. |
| Multi Round-Trip Requests | Tools cannot ask the client for sampling, elicitation or roots mid-call. Take what you need as arguments instead. |
| Pagination | tools/list returns every tool in one response and never sets nextCursor. |
x-mcp-header | Mirroring tool parameters into HTTP headers is optional for servers; Hazaar emits no such annotations. |
| Legacy sessions, GET SSE, DELETE teardown | 2025-06-18/2025-11-25 are accepted for the stateless initialize/tools/list/tools/call subset (see above), but their optional session machinery is not: no Mcp-Session-Id is ever minted, and GET/DELETE still return 405 regardless of the negotiated version. |
| Full JSON Schema validation | Arguments are checked against their PHP types and enum cases, not against arbitrary schema keywords. Validate anything security-relevant inside the tool. |