SDK v2 migration (behavior-neutral, versionNegotiation: 'legacy') (#1624)
Migrate all three clients + core + test-servers from
@modelcontextprotocol/sdk@1.29 to SDK v2 (2.0.0-beta.4), keeping behavior
identical against legacy (2025-11-25) servers. Foundation for the SDK V2 +
New Spec stack.
Packages
- Split @modelcontextprotocol/sdk -> @modelcontextprotocol/client (Client,
transports, OAuth, error taxonomy, protocol types) + core (Zod schemas);
stdio -> client/stdio, Ajv -> client/validators/ajv. Test-servers ->
@modelcontextprotocol/server (+ server-legacy/sse). Deterministic import
rewrite (schemas -> core, everything else -> client); the codemod misroutes
client code to /server so it was not used for path routing.
- new Client(..., { versionNegotiation: { mode: 'legacy' } }) — nothing
2026-era goes on the wire.
Runtime (core/mcp/inspectorClient.ts)
- Error taxonomy: McpError -> ProtocolError, ErrorCode -> ProtocolErrorCode;
RequestTimeout -> SdkError/SdkErrorCode.
- Handlers take method strings; task methods (tasks/*, notifications/tasks/*)
use the 3-arg custom form (excluded from v2's spec-method set).
- Legacy tasks kept working (per project requirement): requestor tasks re-
implemented over raw client.request + explicit deprecated schemas;
experimental.tasks.callToolStream replaced by a local pollTaskToolCall
async generator (with _progressHandlers revival so task-execution progress
still reaches the caller). Receiver (server-initiated) tasks: the v2 Client
validates a sampling/elicit handler result and rejects the legacy { task }
response with -32602 — installReceiverTaskResponseBypass swaps the wrapped
entry in the private _requestHandlers map for the task-augmented branch so
{ task } reaches the wire (fixes server-initiated sampling-via-task).
- Pagination: list verbs use raw client.request({method}, List*ResultSchema)
for single-page behavior (v2's client.listX() auto-aggregate all pages).
Web / auth / apps
- Rename local JsonSchemaType -> InspectorFormSchema + add toFormSchema()
structural narrowing at the SDK-schema -> form boundary.
- ext-apps still peers on SDK v1: one compat boundary with documented casts
(remove when ext-apps#702 lands a v2 peer).
- OAuth provider saveClientInformation reconciled with v2's
OAuthClientInformationContext; InvalidClientMetadataError re-homed as a
local class (server-legacy/auth is not browser-safe).
- vitest.shared.mts dedupes @modelcontextprotocol/client+core to one copy so
vi.mock intercepts the SUT's import.
Accepted v2 behavior changes (per #1624 / spec §9.2): unknown-tool call
rejects with -32602; cancellation is stream-abort (no guaranteed
notifications/cancelled frame); output-schema strictness restored via the
Inspector's own Ajv check on the default path.
npm run ci green: validate (web 3087 / cli 212 / tui 274 / launcher 5),
coverage (>=90 per-file, all clients), smoke (launcher/cli/web), storybook
(410).
Closes #1624
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8d33e76f97
commit
19a6df7ad3
1 changed files with 2545 additions and 2123 deletions
|
|
@ -13,7 +13,7 @@ export type JsonSchemaConst = {
|
|||
description?: string;
|
||||
};
|
||||
|
||||
export type JsonSchemaType = {
|
||||
export type InspectorFormSchema = {
|
||||
type?:
|
||||
| "string"
|
||||
| "number"
|
||||
|
|
@ -35,8 +35,8 @@ export type JsonSchemaType = {
|
|||
description?: string;
|
||||
required?: string[];
|
||||
default?: JsonValue;
|
||||
properties?: Record<string, JsonSchemaType>;
|
||||
items?: JsonSchemaType;
|
||||
properties?: Record<string, InspectorFormSchema>;
|
||||
items?: InspectorFormSchema;
|
||||
// Array validation constraints
|
||||
minItems?: number;
|
||||
maxItems?: number;
|
||||
|
|
@ -51,13 +51,35 @@ export type JsonSchemaType = {
|
|||
// Non-standard legacy support: titles for enum values
|
||||
enumNames?: string[];
|
||||
const?: JsonValue;
|
||||
oneOf?: (JsonSchemaType | JsonSchemaConst)[];
|
||||
anyOf?: (JsonSchemaType | JsonSchemaConst)[];
|
||||
oneOf?: (InspectorFormSchema | JsonSchemaConst)[];
|
||||
anyOf?: (InspectorFormSchema | JsonSchemaConst)[];
|
||||
$ref?: string;
|
||||
};
|
||||
|
||||
export type JsonObject = { [key: string]: JsonValue };
|
||||
|
||||
/**
|
||||
* Narrow an MCP protocol schema (SDK `JsonSchemaType` — e.g. `Tool["inputSchema"]`
|
||||
* / `outputSchema`, an elicitation `requestedSchema`) to the {@link
|
||||
* InspectorFormSchema} subset the {@link SchemaForm} renderer understands.
|
||||
*
|
||||
* Under SDK v2 the protocol schema type (from `json-schema-typed`, exported as
|
||||
* `JsonSchemaType` from `@modelcontextprotocol/client`) is structurally distinct
|
||||
* from Inspector's form schema — same JSON on the wire, incompatible TS types.
|
||||
* Rather than cast at every call site, callers pass the SDK schema through here.
|
||||
* Returns `null` when there is no renderable object shape (missing schema, or a
|
||||
* non-object schema the form can't build fields from); callers handle `null`.
|
||||
*/
|
||||
export function toFormSchema(schema: unknown): InspectorFormSchema | null {
|
||||
if (schema == null || typeof schema !== "object" || Array.isArray(schema)) {
|
||||
return null;
|
||||
}
|
||||
// Structural narrow: the SDK schema's fields are a superset of what the form
|
||||
// reads (`type`, `properties`, `required`, `items`, …); the values the form
|
||||
// never dereferences don't affect rendering.
|
||||
return schema as InspectorFormSchema;
|
||||
}
|
||||
|
||||
export type DataType =
|
||||
| "string"
|
||||
| "number"
|
||||
|
|
@ -90,7 +112,7 @@ export function getDataType(value: JsonValue): DataType {
|
|||
* omits fields that have no default.
|
||||
*/
|
||||
export function collectSchemaDefaults(
|
||||
schema: JsonSchemaType,
|
||||
schema: InspectorFormSchema,
|
||||
): Record<string, unknown> {
|
||||
const properties = schema.properties ?? {};
|
||||
const result: Record<string, unknown> = {};
|
||||
|
|
@ -113,7 +135,7 @@ export function collectSchemaDefaults(
|
|||
* action until required fields are supplied.
|
||||
*/
|
||||
export function hasMissingRequiredFields(
|
||||
schema: JsonSchemaType,
|
||||
schema: InspectorFormSchema,
|
||||
values: Record<string, unknown>,
|
||||
): boolean {
|
||||
const required = schema.required ?? [];
|
||||
|
|
|
|||
Loading…
Reference in a new issue