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 <noreply@anthropic.com>
81 lines
3.1 KiB
TypeScript
81 lines
3.1 KiB
TypeScript
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";
|
|
|
|
/** 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(
|
|
value(CLIENT_INFO_META_KEY, () => connection.getClientVersion()),
|
|
);
|
|
const protocolVersion = value(PROTOCOL_VERSION_META_KEY, () =>
|
|
connection.getNegotiatedProtocolVersion(),
|
|
);
|
|
return {
|
|
...(client === undefined ? {} : { client }),
|
|
clientFeatures: capabilityFeatures(
|
|
value(CLIENT_CAPABILITIES_META_KEY, () =>
|
|
connection.getClientCapabilities(),
|
|
),
|
|
),
|
|
...(typeof protocolVersion === "string" ? { protocolVersion } : {}),
|
|
};
|
|
};
|
|
|
|
/** Read an SDK-defined metadata key from the SDK's currently opaque envelope type. */
|
|
export const mcpEnvelopeValue = (
|
|
envelope: ServerContext["mcpReq"]["envelope"],
|
|
key: string,
|
|
): unknown => (envelope === undefined ? undefined : Reflect.get(envelope, key));
|
|
|
|
const implementation = (value: unknown): ConnectedClientIdentity | undefined =>
|
|
isRecord(value) &&
|
|
typeof value.name === "string" &&
|
|
typeof value.version === "string"
|
|
? { name: value.name, version: value.version }
|
|
: undefined;
|
|
|
|
const capabilityFeatures = (value: unknown): ClientFeatureAvailability => {
|
|
const capabilities = isRecord(value) ? value : {};
|
|
const elicitation = isRecord(capabilities.elicitation)
|
|
? capabilities.elicitation
|
|
: undefined;
|
|
return {
|
|
// A declared capability without modes retains the protocol's form-only
|
|
// compatibility rule; a missing capability still grants no support.
|
|
elicitation_form:
|
|
elicitation !== undefined &&
|
|
(elicitation.form !== undefined || elicitation.url === undefined),
|
|
elicitation_url: elicitation?.url !== undefined,
|
|
roots: capabilities.roots !== undefined,
|
|
sampling: capabilities.sampling !== undefined,
|
|
};
|
|
};
|
|
|
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
typeof value === "object" && value !== null && !Array.isArray(value);
|