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:
cliffhall 2026-07-15 10:44:24 -04:00
commit 19a6df7ad3

View file

@ -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 ?? [];