From b8312fa8d184ba9f422d1f5592fb41e49811544a Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Fri, 9 Oct 2026 03:20:43 +0000 Subject: [PATCH] fix(server): report 2025-era client identity and features in binary_session binary_session read the client's identity, protocol revision, and capabilities only from the 2026-era per-request envelope. A 2025-era connection sends none, so every such client was reported as `client: null` with no negotiated protocol and no features, although it declared them in `initialize`. When a request carries no envelope, read the values the SDK keeps from the connection's initialize handshake. An envelope still takes precedence. Fixes #1167 Co-Authored-By: Claude Opus 5.5 --- src/server/mcpClientMetadata.ts | 34 +++++-- src/server/registerSessionStatusTool.ts | 6 +- tests/boundary/mcp/clientCapabilities.test.ts | 88 +++++++++++++++++++ tests/boundary/mcp/serverIdentity.test.ts | 5 +- 4 files changed, 122 insertions(+), 11 deletions(-) diff --git a/src/server/mcpClientMetadata.ts b/src/server/mcpClientMetadata.ts index 94690eae98..e14cfdb0c9 100644 --- a/src/server/mcpClientMetadata.ts +++ b/src/server/mcpClientMetadata.ts @@ -2,25 +2,45 @@ import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY, PROTOCOL_VERSION_META_KEY, + type McpServer, type ServerContext, } from "@modelcontextprotocol/server"; import type { ClientFeatureAvailability } from "../contracts/toolOutputSchemaPrimitives.js"; import type { ConnectedClientIdentity } from "../serverIdentity.js"; -/** Client metadata bound to the current SDK request rather than server-global state. */ -export const mcpClientMetadata = (context: { - readonly mcpReq: Pick; -}) => { +/** Initialize-scoped client metadata the SDK keeps for a 2025-era connection. */ +export type McpConnectionClientMetadata = Pick< + McpServer["server"], + "getClientCapabilities" | "getClientVersion" | "getNegotiatedProtocolVersion" +>; + +/** + * Client metadata for the current request. A 2026-era request carries it in + * its per-request envelope; a 2025-era connection sends no envelope, so its + * `initialize` handshake supplies the values. + */ +export const mcpClientMetadata = ( + context: { readonly mcpReq: Pick }, + connection: McpConnectionClientMetadata, +) => { const envelope = context.mcpReq.envelope; + // The SDK deprecates these accessors only in favor of the envelope, which + // 2025-era requests do not carry; they still return initialize values. + const value = (key: string, initialized: () => unknown): unknown => + envelope === undefined ? initialized() : mcpEnvelopeValue(envelope, key); const client = implementation( - mcpEnvelopeValue(envelope, CLIENT_INFO_META_KEY), + value(CLIENT_INFO_META_KEY, () => connection.getClientVersion()), + ); + const protocolVersion = value(PROTOCOL_VERSION_META_KEY, () => + connection.getNegotiatedProtocolVersion(), ); - const protocolVersion = mcpEnvelopeValue(envelope, PROTOCOL_VERSION_META_KEY); return { ...(client === undefined ? {} : { client }), clientFeatures: capabilityFeatures( - mcpEnvelopeValue(envelope, CLIENT_CAPABILITIES_META_KEY), + value(CLIENT_CAPABILITIES_META_KEY, () => + connection.getClientCapabilities(), + ), ), ...(typeof protocolVersion === "string" ? { protocolVersion } : {}), }; diff --git a/src/server/registerSessionStatusTool.ts b/src/server/registerSessionStatusTool.ts index e184367514..c220267306 100644 --- a/src/server/registerSessionStatusTool.ts +++ b/src/server/registerSessionStatusTool.ts @@ -44,8 +44,10 @@ export const registerSessionStatusTool = ( contract.name, toolRegistrationOptions(contract), async (input, context) => { - const { client, clientFeatures, protocolVersion } = - mcpClientMetadata(context); + const { client, clientFeatures, protocolVersion } = mcpClientMetadata( + context, + server.server, + ); let androidAvailability: ProviderAvailability; try { androidAvailability = await androidAnalysisAvailability( diff --git a/tests/boundary/mcp/clientCapabilities.test.ts b/tests/boundary/mcp/clientCapabilities.test.ts index f39d87e0fb..323f79980d 100644 --- a/tests/boundary/mcp/clientCapabilities.test.ts +++ b/tests/boundary/mcp/clientCapabilities.test.ts @@ -54,3 +54,91 @@ describe("per-request client capability reporting", () => { }, ); }); + +describe("initialize-scoped client metadata", () => { + const statusFor = async ( + capabilities: Record, + meta?: Record, + ) => { + const session = createTestBinarySession(createCacheProvider([])); + const server = createServer(session, session); + const client = new Client( + { name: "initialize-test", version: "2" }, + { capabilities }, + ); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + return ( + await client.callTool({ + name: "binary_session", + arguments: {}, + ...(meta === undefined ? {} : { _meta: meta }), + }) + ).structuredContent; + } finally { + await Promise.allSettled([ + client.close(), + server.close(), + session.close(), + ]); + } + }; + const allFeatures = { + elicitation: { form: {}, url: {} }, + roots: {}, + sampling: {}, + }; + + it("reports a 2025-era client from its initialize handshake", async () => { + expect(await statusFor(allFeatures)).toMatchObject({ + result: { + server_identity: { + client: { name: "initialize-test", version: "2" }, + negotiated_protocol_version: expect.any(String), + }, + client_features: { + elicitation_form: true, + elicitation_url: true, + roots: true, + sampling: true, + }, + }, + }); + expect(await statusFor({})).toMatchObject({ + result: { + client_features: { + elicitation_form: false, + elicitation_url: false, + roots: false, + sampling: false, + }, + }, + }); + }); + + it("prefers a per-request envelope over initialize values", async () => { + expect( + await statusFor(allFeatures, { + [PROTOCOL_VERSION_META_KEY]: "2026-07-28", + [CLIENT_INFO_META_KEY]: { name: "envelope-test", version: "3" }, + [CLIENT_CAPABILITIES_META_KEY]: {}, + }), + ).toMatchObject({ + result: { + server_identity: { + client: { name: "envelope-test", version: "3" }, + negotiated_protocol_version: "2026-07-28", + }, + client_features: { + elicitation_form: false, + elicitation_url: false, + roots: false, + sampling: false, + }, + }, + }); + }); +}); diff --git a/tests/boundary/mcp/serverIdentity.test.ts b/tests/boundary/mcp/serverIdentity.test.ts index 2305a346e2..8f51487fed 100644 --- a/tests/boundary/mcp/serverIdentity.test.ts +++ b/tests/boundary/mcp/serverIdentity.test.ts @@ -262,7 +262,8 @@ const assertLiveIdentity = async (client: Client): Promise => { server: SDK_IDENTITY.server, client_test: PACKAGE_METADATA.clientSdkVersion, }, - client: null, + client: { name: "identity-test", version: "9" }, + negotiated_protocol_version: expect.any(String), alignment: { state: "unknown" }, }, }, @@ -316,7 +317,7 @@ const assertSessionIdentity = async (client: Client): Promise => { }), ]), client_features: { - elicitation_form: false, + elicitation_form: true, elicitation_url: false, roots: false, sampling: false,