fix: make generated schema.json enforce spec constraints (#18)

The generated JSON Schema artifact previously failed to encode several
normative requirements, forcing non-TypeScript implementations to
maintain their own mirrors of core shapes and discriminators:

- InputRequest/InputResponse collapsed to anyOf:[{},{},{}] because the
  imported request/result types resolved to empty schemas, accepting
  any instance.
- The required resultType discriminators ('task' on CreateTaskResult,
  'complete' on Get/Update/CancelTaskResult) appeared nowhere in the
  artifact despite being normative MUSTs in the extension prose.
- Strip-mode object emission produced additionalProperties:false inside
  allOf intersections, making CreateTaskResult unsatisfiable: every
  spec-conformant instance (with resultType or _meta) was rejected.

The canonical specification is now the sole source of truth for all
core-spec shapes — no SDK package is consulted at any layer.
scripts/fetch-spec-schema.ts vendors spec.schema.json and spec.types.ts
from the same recorded spec commit with provenance headers:

- schema.ts imports every core type (JSON-RPC envelopes, Result,
  NotificationParams, the MRTR InputRequests/InputResponses, and the
  JSON-RPC Error object) from the vendored spec.types.ts, deleting the
  local mirrors that had already drifted from upstream.
- generate-schemas.ts emits $ref for all spec-owned types — including
  the JSON-RPC envelopes intersected by the extension's own types — and
  splices their transitive $ref closure verbatim from the vendored
  canonical schema.json. Core-spec corrections flow in via an explicit,
  reviewable refresh (npm run fetch:spec-schema) instead of silently
  diverging. The @modelcontextprotocol/sdk devDependency is removed.
- Runtime Zod stand-ins for imported types are permissive locals whose
  shapes never reach the artifact; generation fails on any unmapped
  placeholder, and on any definition that cannot be converted.
- JSON Schema emission uses io:'input' so TypeScript's open interface
  semantics are preserved and intersections remain satisfiable.
- resultType discriminators are encoded structurally in schema.ts.
- FailedTask.error now requires the canonical JSON-RPC Error shape;
  ttlMs and pollIntervalMs are integers per the prose.
- createdAt/lastUpdatedAt deliberately remain unconstrained strings:
  the prose requires ISO 8601 generally, and JSON Schema's date-time
  format/pattern machinery can only express RFC 3339, which would
  reject valid basic-form timestamps.

Validated with a Draft 2020-12 jsonschema matrix covering union
rejection/acceptance, discriminator enforcement, JSON-RPC envelope
enforcement, error-shape and integer constraints, byte-fidelity of all
56 spliced canonical definitions, complete $ref resolution, and
check-mode idempotency.
This commit is contained in:
Luca Chang 2026-08-21 13:44:01 -07:00 • committed by GitHub
commit e4345978be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 9556 additions and 1999 deletions

File diff suppressed because it is too large Load diff

View file

@ -12,17 +12,14 @@
*/
import type {
CreateMessageRequest,
CreateMessageResult,
ElicitRequest,
ElicitResult,
Error as JSONRPCErrorObject,
InputRequests,
InputResponses,
JSONRPCNotification,
JSONRPCRequest,
ListRootsRequest,
ListRootsResult,
NotificationParams,
Result,
} from "@modelcontextprotocol/sdk/types.js";
} from "./spec.types.js";
/* Tasks */
@ -79,6 +76,7 @@ export interface Task {
* Time-to-live duration from creation in integer milliseconds, null for unlimited.
* The server may discard the task after the TTL elapses. This value MAY change
* over the lifetime of a task.
* @format int
* @nullable
*/
ttlMs: number | null;
@ -87,6 +85,7 @@ export interface Task {
* Suggested polling interval in integer milliseconds. Clients SHOULD honor
* this value to avoid overwhelming the server. This value MAY change over
* the lifetime of a task.
* @format int
*/
pollIntervalMs?: number;
}
@ -103,51 +102,6 @@ export interface WorkingTask extends Task {
status: "working";
}
/**
* A single input request from the server to the client during task execution.
*
* @category `tasks`
*/
// TODO: Import from SDK once updated for MRTR
export type InputRequest =
| CreateMessageRequest
| ListRootsRequest
| ElicitRequest;
/**
* A single input response from the client to the server during task execution.
*
* @category `tasks`
*/
// TODO: Import from SDK once updated for MRTR
export type InputResponse =
| CreateMessageResult
| ListRootsResult
| ElicitResult;
/**
* Outstanding server-to-client requests that need to be fulfilled during task execution.
* Keys are arbitrary identifiers for matching requests to responses. Each key MUST be
* unique over the lifetime of a single task.
*
* @category `tasks`
*/
// TODO: Import from SDK once updated for MRTR
export interface InputRequests {
[key: string]: InputRequest;
}
/**
* Client responses to outstanding input requests. Each key MUST correspond to a
* currently-outstanding inputRequest key.
*
* @category `tasks`
*/
// TODO: Import from SDK once updated for MRTR
export interface InputResponses {
[key: string]: InputResponse;
}
/**
* A task that is waiting for input from the client.
* Used by tasks/get and notifications/tasks.
@ -193,7 +147,7 @@ export interface FailedTask extends Task {
/**
* The JSON-RPC error that caused the task to fail.
*/
error: { [key: string]: unknown };
error: JSONRPCErrorObject;
}
/**
@ -230,7 +184,13 @@ export type DetailedTask =
*
* @category `tasks`
*/
export type CreateTaskResult = Result & Task;
export type CreateTaskResult = Result &
Task & {
/**
* Discriminator distinguishing a task handle from a standard result.
*/
resultType: "task";
};
/* Task Operations */
@ -239,7 +199,7 @@ export type CreateTaskResult = Result & Task;
*
* @category `tasks/get`
*/
export interface GetTaskRequest extends JSONRPCRequest {
export type GetTaskRequest = JSONRPCRequest & {
method: "tasks/get";
params: {
/**
@ -247,7 +207,7 @@ export interface GetTaskRequest extends JSONRPCRequest {
*/
taskId: string;
};
}
};
/**
* The response to a tasks/get request. Carries the appropriate DetailedTask
@ -256,14 +216,20 @@ export interface GetTaskRequest extends JSONRPCRequest {
*
* @category `tasks/get`
*/
export type GetTaskResult = Result & DetailedTask;
export type GetTaskResult = Result &
DetailedTask & {
/**
* Discriminator marking this as the standard result shape for tasks/get.
*/
resultType: "complete";
};
/**
* A request to provide input responses to a task in the input_required state.
*
* @category `tasks/update`
*/
export interface UpdateTaskRequest extends JSONRPCRequest {
export type UpdateTaskRequest = JSONRPCRequest & {
method: "tasks/update";
params: {
/**
@ -277,7 +243,7 @@ export interface UpdateTaskRequest extends JSONRPCRequest {
*/
inputResponses: InputResponses;
};
}
};
/**
* The response to a tasks/update request. An empty acknowledgement.
@ -285,14 +251,19 @@ export interface UpdateTaskRequest extends JSONRPCRequest {
*
* @category `tasks/update`
*/
export type UpdateTaskResult = Result;
export type UpdateTaskResult = Result & {
/**
* Discriminator marking this as the standard result shape for tasks/update.
*/
resultType: "complete";
};
/**
* A request to cancel a task.
*
* @category `tasks/cancel`
*/
export interface CancelTaskRequest extends JSONRPCRequest {
export type CancelTaskRequest = JSONRPCRequest & {
method: "tasks/cancel";
params: {
/**
@ -300,7 +271,7 @@ export interface CancelTaskRequest extends JSONRPCRequest {
*/
taskId: string;
};
}
};
/**
* The response to a tasks/cancel request. An empty acknowledgement.
@ -309,7 +280,12 @@ export interface CancelTaskRequest extends JSONRPCRequest {
*
* @category `tasks/cancel`
*/
export type CancelTaskResult = Result;
export type CancelTaskResult = Result & {
/**
* Discriminator marking this as the standard result shape for tasks/cancel.
*/
resultType: "complete";
};
/* Task Notifications */
@ -329,10 +305,10 @@ export type TaskStatusNotificationParams = NotificationParams &
*
* @category `notifications/tasks`
*/
export interface TaskStatusNotification extends JSONRPCNotification {
export type TaskStatusNotification = JSONRPCNotification & {
method: "notifications/tasks";
params: TaskStatusNotificationParams;
}
};
/* Subscription Additions */

File diff suppressed because it is too large Load diff

3205
schema/draft/spec.types.ts Normal file

File diff suppressed because it is too large Load diff