Skip to main content

Standards

GATool serves two revisions of the Model Context Protocol (MCP): 2025-11-25 and 2025-06-18. It targets the transport rules of 2025-11-25. The compliance filter, the gaToolMcpComplianceFilter bean on the MCP endpoint, applies the transport rules that the MCP Java SDK and Spring AI leave open. It checks the Origin header and the protocol version, and caps the request body. It also refuses a body it cannot read the way the server would. The refused bodies are:

  • A body declared in a charset other than UTF-8.

  • A media type other than JSON, or one that carries a wildcard.

  • A body that fails to parse, or one that is not a JSON object.

  • An id other than a string or an integer.

  • A method that is not a string.

  • A jsonrpc member that is missing or other than "2.0".

  • A params that is not an object.

  • A tools/call whose arguments is not an object.

  • A JSON-RPC batch.

The Origin check runs before the body is read. So does the answer to an HTTP method the transport does not route. Those methods are PUT on both transports and DELETE on the stateless one. The answer is 405, with an Allow header and a JSON-RPC error. The compliance filter passes a request without MCP-Protocol-Version through, and the SDK serves it at a revision this release supports. The specification says the server should assume 2025-03-26. That revision requires JSON-RPC batches, which this release cannot read, so assuming it would refuse every such call. An explicit 2025-03-26 header is refused with 400, unless gatool.mcp.security.unsafe.allow-superseded-mcp-revisions adds that revision back. A tools/call that names a tool outside printable ASCII is refused as well. The refusal travels inside a 200, so that the client keeps its session. Such a name could otherwise reach a challenge header. The scope check and the server therefore always read the same message.

The current MCP revision is 2026-07-28. It removes the initialize handshake and sessions, and it requires server/discover. This release does not implement it. In that revision's terms, GATool is a legacy server. The revision's compatibility matrix gives the outcome for each client. A client that supports both revisions falls back to initialize and works. GATool answers an initialize that asks for 2026-07-28 with 2025-11-25. It answers any other request carrying that revision with 400 and a JSON-RPC error. A client that supports 2026-07-28 alone cannot connect.

When an application declares its own McpStatelessServerTransport or McpStreamableServerTransportProvider bean, the server is built on that transport. GATool's own transport backs off. Such a transport answers the handshake from its own revision list. The compliance filter still refuses a superseded revision on every request.

Two deviations are inherited from Spring AI 2.0.1 and MCP Java SDK 2.0.0. Every SSE event id on a POST response stream carries the session id. The specification asks for an id that is unique across the session. On the stateful transport, a request for a method the server does not serve gets its -32601 as an SSE event. An example is resources/list on a server without the resources capability. The stream then stays open. The specification says the server should end it. A client therefore waits until its own timeout. Both are SDK behaviour. A test pins the second until the SDK closes the stream. Two more are inherited the same way. A Last-Event-ID header opens a stream without a replay, so resumability is unsupported. The SDK does not send the priming SSE event the specification recommends. GATool normalises the Accept header ahead of the transports. It drops q-values. It reads */* or a missing header as the two media types MCP names. Only a client whose header leaves both types out is refused.

GATool leaves the pagination rule to the SDK. Every tools/list answer holds the whole list and leaves nextCursor out. MCP's rule says a server should refuse a cursor with -32602. MCP Java SDK 2.0.0 implements pagination in its schema and in its client, and leaves the server half out. The compliance filter is the only place where GATool could add such a rule. A rule in the filter would apply over HTTP alone, while GATool serves stdio as well. A client that sends a cursor reads every tool either way.

Every build runs the official MCP conformance suite, version 0.2.0-alpha.11, against a fixture application. It runs with --requirements 2025-11-25, once in stateless mode and once in stateful mode. This release claims the tool contract and the transport rules. These scenarios pass in both modes:

server-initialize, ping, tools-list, tools-call-simple-text, tools-call-image, tools-call-audio, tools-call-embedded-resource, tools-call-mixed-content, tools-call-error and dns-rebinding-protection.

The stateful run also passes logging-set-level, tools-call-with-logging, tools-call-with-progress, server-sse-multiple-streams and the suite's own server-session-lifecycle. Those pass there because a session lets the server reach the client.

The suite counts a baselined failure as a failure. The file conformance-baseline.yml therefore lists all twenty scenarios that the stateless run leaves out, each with its reason. The file conformance-baseline-stateful.yml lists the twelve of the stateful run the same way. They fall into three groups:

  • Resources, prompts and completions. GATool turns GraphQL operation files into tools. An application declares its own resources and prompts when it wants them. The fixture therefore declares tools alone.

  • Sampling, elicitation and multiple streams. The first two ask the server to call the client. The third wants several streams open in one session. A stateless server answers a request and closes. Spring AI 2.0.1 leaves a tool that takes a request context unregistered there. The stateful run passes all of them.

  • Progress, logging and logging/setLevel, in stateless mode. Spring AI 2.0.1 skips a tool method that takes a request context on a stateless server. The stateless server answers logging/setLevel with "Missing handler for request type". All three pass in stateful mode.

A stateful server keeps a session until its client deletes it or the application stops. The server also evicts a session after gatool.mcp.sessions.idle-timeout of silence. It keeps at most gatool.mcp.sessions.max-count at once. It answers 503 to a client that initializes past the cap. Spring AI's own auto-configuration leaves both settings at the transport's defaults, which are 100,000 sessions without eviction. GATool therefore builds the transport bean itself. Startup warns while a stateful server runs without authentication, because any caller can then fill the cap.

That cap counts the whole server, so one caller can fill it and leave every other caller with 503. Authentication bounds who may do it and makes the denial attributable. One authenticated caller can still fill the cap. Set gatool.mcp.sessions.max-count-per-caller to bound what one caller takes. A caller past their own cap reads 429 with Retry-After and the wait, the way the rate limiter answers. The server cap keeps answering 503, because that cap is about the server. The per-caller cap defaults to 100, a tenth of the server cap. Zero or below turns it off. A deployment whose workers share one service principal sets it to the number of sessions those workers hold. A secured stateful server may leave the per-caller cap off, or set it at or above gatool.mcp.sessions.max-count. Either setting leaves one caller free to fill the whole server cap. Startup then warns and names both properties. One MCP client normally holds one session. A caller that reconnects may briefly hold two. A small number therefore suits a deployment where each caller runs one client.

Taking the endpoint's security over​

Excluding GAToolMcpSecurityAutoConfiguration removes the one place that applies McpServerSecurityConfigurer. The per-tool scope check goes with it. Spring Boot's own resource server chain then serves the endpoint under a rule that asks for a valid token alone. Any token for the audience then calls every tool, the scoped ones included. Startup stops and says so. The same stop applies when Spring Security's filter is absent because Boot's web security auto-configuration was excluded too. The endpoint would then answer every caller.

An application that wants to own the endpoint's security declares a chain of its own and applies the configurer in it. GATool supports that set-up, and the application starts normally:

@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
SecurityFilterChain mcpChain(HttpSecurity http) throws Exception {
return http.securityMatcher("/mcp", "/.well-known/oauth-protected-resource/mcp")
.with(McpServerSecurityConfigurer.mcpServer(), withDefaults())
.build();
}