mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
Merge pull request #1179 from morluto/fix/session-client-metadata-2025
fix(server): report 2025-era client identity and features in binary_session
This commit is contained in:
@@ -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<ServerContext["mcpReq"], "envelope">;
|
||||
}) => {
|
||||
/** 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<ServerContext["mcpReq"], "envelope"> },
|
||||
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 } : {}),
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -54,3 +54,91 @@ describe("per-request client capability reporting", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("initialize-scoped client metadata", () => {
|
||||
const statusFor = async (
|
||||
capabilities: Record<string, unknown>,
|
||||
meta?: Record<string, unknown>,
|
||||
) => {
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -367,7 +367,8 @@ const assertLiveIdentity = async (client: Client): Promise<void> => {
|
||||
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" },
|
||||
},
|
||||
},
|
||||
@@ -421,7 +422,7 @@ const assertSessionIdentity = async (client: Client): Promise<void> => {
|
||||
}),
|
||||
]),
|
||||
client_features: {
|
||||
elicitation_form: false,
|
||||
elicitation_form: true,
|
||||
elicitation_url: false,
|
||||
roots: false,
|
||||
sampling: false,
|
||||
|
||||
Reference in New Issue
Block a user