Files
reverse-engineering-anything/src/server/mcpClientMetadata.ts
T
morluto 5bf66f7500 refactor(domain): one owner for the non-array record predicate (#10588)
* refactor(domain): one owner for the non-array record predicate

Thirty-four source files each declared isRecord as a non-null object that is
not an array, and six more spelled the same predicate as isObject,
isSchemaRecord or isStorageRecord. They now import isRecord from
src/domain/isRecord.ts, whose overloads keep JsonValue narrowing for the
desktop and environment readers. DshCordisPatch's predicate also excludes
Cordis expressions, so it is renamed isPatchRecord.

architecture-guards refuses a new local copy under any name. The seven files
that still hold one are listed and must leave the list when their copy goes.

* fix(guards): count record predicate copies precisely

* test(hopper): retry after confirmed application exit

* fix: reject stale record predicate exceptions
2026-11-15 17:05:54 +08:00

73 lines
2.7 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";
import { isRecord } from "../domain/isRecord.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() : Reflect.get(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 } : {}),
};
};
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,
};
};