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
jsonrpcmember that is missing or other than"2.0". -
A
paramsthat is not an object. -
A
tools/callwhoseargumentsis 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 answerslogging/setLevelwith "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();
}