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:
parent
dcc8d2bbec
commit
e4345978be
4 changed files with 9556 additions and 1999 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -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 */
|
||||
|
||||
|
|
|
|||
3942
schema/draft/spec.schema.json
Normal file
3942
schema/draft/spec.schema.json
Normal file
File diff suppressed because it is too large
Load diff
3205
schema/draft/spec.types.ts
Normal file
3205
schema/draft/spec.types.ts
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue