What happens
McpAsyncServer adds the logging capability to every server it builds, overriding whatever the
caller passed to .capabilities(...). There is no flag and no branch, so a server cannot decline to
advertise it.
Both constructors — the McpServerTransportProvider one and the McpStreamableServerTransportProvider
one — run:
this.serverCapabilities = features.serverCapabilities().mutate().logging().build();
Reproduction
Build a server that asks for tools only:
McpServer.sync(transport)
.serverInfo("example", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(theTools)
.build();
initialize answers:
"capabilities": { "logging": {}, "tools": { "listChanged": true } }
Evidence
Verified in the bytecode of mcp-core-2.0.0.jar rather than from source, in case the mutation was
conditional at runtime. It is not — javap -c io/modelcontextprotocol/server/McpAsyncServer.class
shows the same three calls at identical offsets in both constructors:
101: invokevirtual // McpServerFeatures$Async.serverCapabilities:()...ServerCapabilities;
104: invokevirtual // ServerCapabilities.mutate:()...ServerCapabilities$Builder;
107: invokevirtual // ServerCapabilities$Builder.logging:()...ServerCapabilities$Builder;
113: putfield // serverCapabilities
Why this is worth changing
MCP's in-protocol logging utility is deprecated as of protocol revision 2026-07-28
(SEP-2577); new
implementations SHOULD NOT adopt it. The current behaviour means every server built with this SDK
advertises the deprecated capability, whether or not it sends notifications/message — which is
close to the opposite of what the deprecation is trying to achieve, and it makes the advertisement
useless to clients as a signal, since it is true of everyone.
It also leaves implementors with only two honest options, neither of them "follow the spec's advice":
- implement a deprecated feature they did not want, purely so the advertisement is not empty; or
- advertise a capability that does nothing, and hope no client acts on it.
Worth noting the gap is narrower than it first appears, which is why this is a small fix rather than
a design change: the SDK does implement logging/setLevel and filters on
McpServerSession.minLoggingLevel (default INFO) inside loggingNotification. So a server that
never calls loggingNotification is offering an always-empty stream rather than a broken method.
The problem is only that it cannot say so.
Suggested fix
Honour what the caller passed:
this.serverCapabilities = features.serverCapabilities();
If the unconditional .logging() is load-bearing for existing users, an opt-out on the builder would
also do — anything that lets a server say "I do not implement this". Happy to open a PR if you have a
preference between the two.
Related but not the same ask: #872 (constructors package-private, so behaviour cannot be customised
by subclassing).
Version
io.modelcontextprotocol.sdk:mcp:2.0.0, Java 21.
What happens
McpAsyncServeradds theloggingcapability to every server it builds, overriding whatever thecaller passed to
.capabilities(...). There is no flag and no branch, so a server cannot decline toadvertise it.
Both constructors — the
McpServerTransportProviderone and theMcpStreamableServerTransportProviderone — run:
Reproduction
Build a server that asks for tools only:
initializeanswers:Evidence
Verified in the bytecode of
mcp-core-2.0.0.jarrather than from source, in case the mutation wasconditional at runtime. It is not —
javap -c io/modelcontextprotocol/server/McpAsyncServer.classshows the same three calls at identical offsets in both constructors:
Why this is worth changing
MCP's in-protocol logging utility is deprecated as of protocol revision
2026-07-28(SEP-2577); new
implementations SHOULD NOT adopt it. The current behaviour means every server built with this SDK
advertises the deprecated capability, whether or not it sends
notifications/message— which isclose to the opposite of what the deprecation is trying to achieve, and it makes the advertisement
useless to clients as a signal, since it is true of everyone.
It also leaves implementors with only two honest options, neither of them "follow the spec's advice":
Worth noting the gap is narrower than it first appears, which is why this is a small fix rather than
a design change: the SDK does implement
logging/setLeveland filters onMcpServerSession.minLoggingLevel(defaultINFO) insideloggingNotification. So a server thatnever calls
loggingNotificationis offering an always-empty stream rather than a broken method.The problem is only that it cannot say so.
Suggested fix
Honour what the caller passed:
If the unconditional
.logging()is load-bearing for existing users, an opt-out on the builder wouldalso do — anything that lets a server say "I do not implement this". Happy to open a PR if you have a
preference between the two.
Related but not the same ask: #872 (constructors package-private, so behaviour cannot be customised
by subclassing).
Version
io.modelcontextprotocol.sdk:mcp:2.0.0, Java 21.