diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1a30b49 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + check-schema: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "24" + - run: npm ci + - run: npm run check:schema:ts + - run: npm run check:schema:json diff --git a/.gitignore b/.gitignore index d5e1392..ead45c0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ package-lock.json .jj/ *.tsbuildinfo dist/ +schema/**/generated/ diff --git a/package.json b/package.json index fd8fcab..f9f8b54 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,20 @@ "name": "@modelcontextprotocol/experimental-ext-tasks", "private": true, "version": "0.1.0", + "type": "module", "description": "MCP Tasks Extension - Experimental extension for task management in the Model Context Protocol", "license": "Apache-2.0", "scripts": { - "check:schema": "tsc --noEmit" + "generate:schemas": "tsx scripts/generate-schemas.ts", + "check:schema": "npm run check:schema:ts && npm run check:schema:json", + "check:schema:ts": "tsc --noEmit", + "check:schema:json": "tsx scripts/generate-schemas.ts --check" }, "devDependencies": { "@modelcontextprotocol/sdk": "^1.29.0", - "typescript": "^5.0.0" + "ts-to-zod": "^5.1.0", + "tsx": "^4.21.0", + "typescript": "^5.0.0", + "zod": "^4.4.3" } } diff --git a/schema/2025-11-25/schema.json b/schema/2025-11-25/schema.json new file mode 100644 index 0000000..5f657c2 --- /dev/null +++ b/schema/2025-11-25/schema.json @@ -0,0 +1,1134 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://modelcontextprotocol.io/ext-tasks/schema.json", + "title": "MCP Tasks Extension", + "description": "JSON Schema for MCP Tasks extension protocol messages. Extension Identifier: io.modelcontextprotocol/tasks", + "$defs": { + "CancelTaskRequest": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "jsonrpc": { + "type": "string", + "const": "2.0" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + ] + }, + "method": { + "type": "string", + "const": "tasks/cancel" + }, + "params": { + "type": "object", + "properties": { + "taskId": { + "type": "string" + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + } + }, + "required": [ + "jsonrpc", + "id", + "method", + "params" + ], + "additionalProperties": false + }, + "CancelTaskResult": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "type": "object", + "properties": { + "_meta": { + "type": "object", + "properties": { + "progressToken": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + ] + }, + "io.modelcontextprotocol/related-task": { + "type": "object", + "properties": { + "taskId": { + "type": "string" + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + } + }, + "additionalProperties": {} + } + }, + "additionalProperties": {} + }, + { + "type": "object", + "properties": { + "taskId": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "type": "string", + "const": "working" + }, + { + "type": "string", + "const": "input_required" + }, + { + "type": "string", + "const": "completed" + }, + { + "type": "string", + "const": "failed" + }, + { + "type": "string", + "const": "cancelled" + } + ] + }, + "statusMessage": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "lastUpdatedAt": { + "type": "string" + }, + "ttl": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "pollInterval": { + "type": "number" + } + }, + "required": [ + "taskId", + "status", + "createdAt", + "lastUpdatedAt", + "ttl" + ], + "additionalProperties": false + } + ] + }, + "CreateTaskResult": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "_meta": { + "type": "object", + "properties": { + "progressToken": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + ] + }, + "io.modelcontextprotocol/related-task": { + "type": "object", + "properties": { + "taskId": { + "type": "string" + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + } + }, + "additionalProperties": {} + }, + "task": { + "type": "object", + "properties": { + "taskId": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "type": "string", + "const": "working" + }, + { + "type": "string", + "const": "input_required" + }, + { + "type": "string", + "const": "completed" + }, + { + "type": "string", + "const": "failed" + }, + { + "type": "string", + "const": "cancelled" + } + ] + }, + "statusMessage": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "lastUpdatedAt": { + "type": "string" + }, + "ttl": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "pollInterval": { + "type": "number" + } + }, + "required": [ + "taskId", + "status", + "createdAt", + "lastUpdatedAt", + "ttl" + ], + "additionalProperties": false + } + }, + "required": [ + "task" + ], + "additionalProperties": {} + }, + "GetTaskPayloadRequest": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "jsonrpc": { + "type": "string", + "const": "2.0" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + ] + }, + "method": { + "type": "string", + "const": "tasks/result" + }, + "params": { + "type": "object", + "properties": { + "taskId": { + "type": "string" + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + } + }, + "required": [ + "jsonrpc", + "id", + "method", + "params" + ], + "additionalProperties": false + }, + "GetTaskPayloadResult": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "type": "object", + "properties": { + "_meta": { + "type": "object", + "properties": { + "progressToken": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + ] + }, + "io.modelcontextprotocol/related-task": { + "type": "object", + "properties": { + "taskId": { + "type": "string" + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + } + }, + "additionalProperties": {} + } + }, + "additionalProperties": {} + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + ] + }, + "GetTaskRequest": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "jsonrpc": { + "type": "string", + "const": "2.0" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + ] + }, + "method": { + "type": "string", + "const": "tasks/get" + }, + "params": { + "type": "object", + "properties": { + "taskId": { + "type": "string" + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + } + }, + "required": [ + "jsonrpc", + "id", + "method", + "params" + ], + "additionalProperties": false + }, + "GetTaskResult": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "type": "object", + "properties": { + "_meta": { + "type": "object", + "properties": { + "progressToken": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + ] + }, + "io.modelcontextprotocol/related-task": { + "type": "object", + "properties": { + "taskId": { + "type": "string" + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + } + }, + "additionalProperties": {} + } + }, + "additionalProperties": {} + }, + { + "type": "object", + "properties": { + "taskId": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "type": "string", + "const": "working" + }, + { + "type": "string", + "const": "input_required" + }, + { + "type": "string", + "const": "completed" + }, + { + "type": "string", + "const": "failed" + }, + { + "type": "string", + "const": "cancelled" + } + ] + }, + "statusMessage": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "lastUpdatedAt": { + "type": "string" + }, + "ttl": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "pollInterval": { + "type": "number" + } + }, + "required": [ + "taskId", + "status", + "createdAt", + "lastUpdatedAt", + "ttl" + ], + "additionalProperties": false + } + ] + }, + "ListTasksRequest": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "method": { + "type": "string", + "const": "tasks/list" + }, + "params": { + "type": "object", + "properties": { + "_meta": { + "type": "object", + "properties": { + "progressToken": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + ] + }, + "io.modelcontextprotocol/related-task": { + "type": "object", + "properties": { + "taskId": { + "type": "string" + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + } + }, + "additionalProperties": {} + }, + "cursor": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "required": [ + "method" + ], + "additionalProperties": false + }, + "ListTasksResult": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "_meta": { + "type": "object", + "properties": { + "progressToken": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + ] + }, + "io.modelcontextprotocol/related-task": { + "type": "object", + "properties": { + "taskId": { + "type": "string" + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + } + }, + "additionalProperties": {} + }, + "nextCursor": { + "type": "string" + }, + "tasks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "taskId": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "type": "string", + "const": "working" + }, + { + "type": "string", + "const": "input_required" + }, + { + "type": "string", + "const": "completed" + }, + { + "type": "string", + "const": "failed" + }, + { + "type": "string", + "const": "cancelled" + } + ] + }, + "statusMessage": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "lastUpdatedAt": { + "type": "string" + }, + "ttl": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "pollInterval": { + "type": "number" + } + }, + "required": [ + "taskId", + "status", + "createdAt", + "lastUpdatedAt", + "ttl" + ], + "additionalProperties": false + } + } + }, + "required": [ + "tasks" + ], + "additionalProperties": {} + }, + "RelatedTaskMetadata": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "taskId": { + "type": "string" + } + }, + "required": [ + "taskId" + ], + "additionalProperties": false + }, + "TaskAugmentedRequestParams": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "_meta": { + "type": "object", + "properties": { + "progressToken": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + ] + } + }, + "additionalProperties": {} + }, + "task": { + "type": "object", + "properties": { + "ttl": { + "type": "number" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": {} + }, + "TaskClientCapabilities": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "tasks": { + "type": "object", + "properties": { + "list": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "not": {} + } + }, + "cancel": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "not": {} + } + }, + "requests": { + "type": "object", + "properties": { + "sampling": { + "type": "object", + "properties": { + "createMessage": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "not": {} + } + } + }, + "additionalProperties": false + }, + "elicitation": { + "type": "object", + "properties": { + "create": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "not": {} + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "TaskMetadata": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "ttl": { + "type": "number" + } + }, + "additionalProperties": false + }, + "Task": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "taskId": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "type": "string", + "const": "working" + }, + { + "type": "string", + "const": "input_required" + }, + { + "type": "string", + "const": "completed" + }, + { + "type": "string", + "const": "failed" + }, + { + "type": "string", + "const": "cancelled" + } + ] + }, + "statusMessage": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "lastUpdatedAt": { + "type": "string" + }, + "ttl": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "pollInterval": { + "type": "number" + } + }, + "required": [ + "taskId", + "status", + "createdAt", + "lastUpdatedAt", + "ttl" + ], + "additionalProperties": false + }, + "TaskServerCapabilities": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "tasks": { + "type": "object", + "properties": { + "list": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "not": {} + } + }, + "cancel": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "not": {} + } + }, + "requests": { + "type": "object", + "properties": { + "tools": { + "type": "object", + "properties": { + "call": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "not": {} + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "TaskStatusNotificationParams": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "type": "object", + "properties": { + "_meta": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": {} + }, + { + "type": "object", + "properties": { + "taskId": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "type": "string", + "const": "working" + }, + { + "type": "string", + "const": "input_required" + }, + { + "type": "string", + "const": "completed" + }, + { + "type": "string", + "const": "failed" + }, + { + "type": "string", + "const": "cancelled" + } + ] + }, + "statusMessage": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "lastUpdatedAt": { + "type": "string" + }, + "ttl": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "pollInterval": { + "type": "number" + } + }, + "required": [ + "taskId", + "status", + "createdAt", + "lastUpdatedAt", + "ttl" + ], + "additionalProperties": false + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + ] + }, + "TaskStatusNotification": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "jsonrpc": { + "type": "string", + "const": "2.0" + }, + "method": { + "type": "string", + "const": "notifications/tasks/status" + }, + "params": { + "allOf": [ + { + "type": "object", + "properties": { + "_meta": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "additionalProperties": {} + }, + { + "type": "object", + "properties": { + "taskId": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "type": "string", + "const": "working" + }, + { + "type": "string", + "const": "input_required" + }, + { + "type": "string", + "const": "completed" + }, + { + "type": "string", + "const": "failed" + }, + { + "type": "string", + "const": "cancelled" + } + ] + }, + "statusMessage": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "lastUpdatedAt": { + "type": "string" + }, + "ttl": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "pollInterval": { + "type": "number" + } + }, + "required": [ + "taskId", + "status", + "createdAt", + "lastUpdatedAt", + "ttl" + ], + "additionalProperties": false + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + ] + } + }, + "required": [ + "jsonrpc", + "method", + "params" + ], + "additionalProperties": false + }, + "TaskStatus": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "anyOf": [ + { + "type": "string", + "const": "working" + }, + { + "type": "string", + "const": "input_required" + }, + { + "type": "string", + "const": "completed" + }, + { + "type": "string", + "const": "failed" + }, + { + "type": "string", + "const": "cancelled" + } + ] + }, + "ToolTaskSupport": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "anyOf": [ + { + "type": "string", + "const": "forbidden" + }, + { + "type": "string", + "const": "optional" + }, + { + "type": "string", + "const": "required" + } + ] + } + } +} diff --git a/schema/draft/2025-11-25/schema.ts b/schema/2025-11-25/schema.ts similarity index 95% rename from schema/draft/2025-11-25/schema.ts rename to schema/2025-11-25/schema.ts index efbe04b..05771c8 100644 --- a/schema/draft/2025-11-25/schema.ts +++ b/schema/2025-11-25/schema.ts @@ -1,9 +1,12 @@ /** - * MCP Tasks Extension Schema + * MCP Tasks Extension Schema (spec.types.ts) * Extension Identifier: io.modelcontextprotocol/tasks * - * This file contains only the task-related types from the MCP 2025-11-25 specification. - * Base types are imported from the core MCP SDK. + * This file contains pure TypeScript interface definitions for the MCP Tasks extension. + * These types are the source of truth and are used to generate Zod schemas via `ts-to-zod`. + * + * - Use `@description` JSDoc tags to generate `.describe()` calls on schemas + * - Run `npm run generate:schemas` to regenerate schemas from these types * * @see https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks */ diff --git a/schema/draft/2025-11-25/schema.json b/schema/draft/2025-11-25/schema.json deleted file mode 100644 index 0bef671..0000000 --- a/schema/draft/2025-11-25/schema.json +++ /dev/null @@ -1,374 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "MCP Tasks Extension Schema. Contains only task-related definitions from the MCP 2025-11-25 specification. Core MCP types are referenced via remote $ref pointing to the 2025-11-25 core schema.", - "$comment": "Extension Identifier: io.modelcontextprotocol/tasks. Core schema: https://github.com/modelcontextprotocol/modelcontextprotocol/tree/2025-11-25/schema/2025-11-25", - "$defs": { - "TaskStatus": { - "description": "The status of a task.", - "enum": ["cancelled", "completed", "failed", "input_required", "working"], - "type": "string" - }, - "TaskMetadata": { - "description": "Metadata for augmenting a request with task execution.\nInclude this in the `task` field of the request parameters.", - "properties": { - "ttl": { - "description": "Requested duration in milliseconds to retain task from creation.", - "type": "integer" - } - }, - "type": "object" - }, - "RelatedTaskMetadata": { - "description": "Metadata for associating messages with a task.\nInclude this in the `_meta` field under the key `io.modelcontextprotocol/related-task`.", - "properties": { - "taskId": { - "description": "The task identifier this message is associated with.", - "type": "string" - } - }, - "required": ["taskId"], - "type": "object" - }, - "Task": { - "description": "Data associated with a task.", - "properties": { - "createdAt": { - "description": "ISO 8601 timestamp when the task was created.", - "type": "string" - }, - "lastUpdatedAt": { - "description": "ISO 8601 timestamp when the task was last updated.", - "type": "string" - }, - "pollInterval": { - "description": "Suggested polling interval in milliseconds.", - "type": "integer" - }, - "status": { - "$ref": "#/$defs/TaskStatus", - "description": "Current task state." - }, - "statusMessage": { - "description": "Optional human-readable message describing the current task state.\nThis can provide context for any status, including:\n- Reasons for \"cancelled\" status\n- Summaries for \"completed\" status\n- Diagnostic information for \"failed\" status (e.g., error details, what went wrong)", - "type": "string" - }, - "taskId": { - "description": "The task identifier.", - "type": "string" - }, - "ttl": { - "description": "Actual retention duration from creation in milliseconds, null for unlimited.", - "type": ["integer", "null"] - } - }, - "required": ["createdAt", "lastUpdatedAt", "status", "taskId", "ttl"], - "type": "object" - }, - "CreateTaskResult": { - "description": "A response to a task-augmented request.", - "properties": { - "_meta": { - "additionalProperties": {}, - "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", - "type": "object" - }, - "task": { - "$ref": "#/$defs/Task" - } - }, - "required": ["task"], - "type": "object" - }, - "TaskAugmentedRequestParams": { - "description": "Common params for any task-augmented request.", - "properties": { - "_meta": { - "additionalProperties": {}, - "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", - "properties": { - "progressToken": { - "$ref": "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-11-25/schema.json#/$defs/ProgressToken", - "description": "If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications." - } - }, - "type": "object" - }, - "task": { - "$ref": "#/$defs/TaskMetadata", - "description": "If specified, the caller is requesting task-augmented execution for this request.\nThe request will return a CreateTaskResult immediately, and the actual result can be\nretrieved later via tasks/result.\n\nTask augmentation is subject to capability negotiation - receivers MUST declare support\nfor task augmentation of specific request types in their capabilities." - } - }, - "type": "object" - }, - "GetTaskRequest": { - "description": "A request to retrieve the state of a task.", - "properties": { - "id": { - "$ref": "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-11-25/schema.json#/$defs/RequestId" - }, - "jsonrpc": { - "const": "2.0", - "type": "string" - }, - "method": { - "const": "tasks/get", - "type": "string" - }, - "params": { - "properties": { - "taskId": { - "description": "The task identifier to query.", - "type": "string" - } - }, - "required": ["taskId"], - "type": "object" - } - }, - "required": ["id", "jsonrpc", "method", "params"], - "type": "object" - }, - "GetTaskResult": { - "allOf": [ - { - "$ref": "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-11-25/schema.json#/$defs/Result" - }, - { - "$ref": "#/$defs/Task" - } - ], - "description": "The response to a tasks/get request." - }, - "GetTaskPayloadRequest": { - "description": "A request to retrieve the result of a completed task.", - "properties": { - "id": { - "$ref": "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-11-25/schema.json#/$defs/RequestId" - }, - "jsonrpc": { - "const": "2.0", - "type": "string" - }, - "method": { - "const": "tasks/result", - "type": "string" - }, - "params": { - "properties": { - "taskId": { - "description": "The task identifier to retrieve results for.", - "type": "string" - } - }, - "required": ["taskId"], - "type": "object" - } - }, - "required": ["id", "jsonrpc", "method", "params"], - "type": "object" - }, - "GetTaskPayloadResult": { - "additionalProperties": {}, - "description": "The response to a tasks/result request.\nThe structure matches the result type of the original request.\nFor example, a tools/call task would return the CallToolResult structure.", - "properties": { - "_meta": { - "additionalProperties": {}, - "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", - "type": "object" - } - }, - "type": "object" - }, - "CancelTaskRequest": { - "description": "A request to cancel a task.", - "properties": { - "id": { - "$ref": "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-11-25/schema.json#/$defs/RequestId" - }, - "jsonrpc": { - "const": "2.0", - "type": "string" - }, - "method": { - "const": "tasks/cancel", - "type": "string" - }, - "params": { - "properties": { - "taskId": { - "description": "The task identifier to cancel.", - "type": "string" - } - }, - "required": ["taskId"], - "type": "object" - } - }, - "required": ["id", "jsonrpc", "method", "params"], - "type": "object" - }, - "CancelTaskResult": { - "allOf": [ - { - "$ref": "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-11-25/schema.json#/$defs/Result" - }, - { - "$ref": "#/$defs/Task" - } - ], - "description": "The response to a tasks/cancel request." - }, - "ListTasksRequest": { - "description": "A request to retrieve a list of tasks.", - "properties": { - "id": { - "$ref": "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-11-25/schema.json#/$defs/RequestId" - }, - "jsonrpc": { - "const": "2.0", - "type": "string" - }, - "method": { - "const": "tasks/list", - "type": "string" - }, - "params": { - "$ref": "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-11-25/schema.json#/$defs/PaginatedRequestParams" - } - }, - "required": ["id", "jsonrpc", "method"], - "type": "object" - }, - "ListTasksResult": { - "description": "The response to a tasks/list request.", - "properties": { - "_meta": { - "additionalProperties": {}, - "description": "See [General fields: `_meta`](/specification/2025-11-25/basic/index#meta) for notes on `_meta` usage.", - "type": "object" - }, - "nextCursor": { - "description": "An opaque token representing the pagination position after the last returned result.\nIf present, there may be more results available.", - "type": "string" - }, - "tasks": { - "items": { - "$ref": "#/$defs/Task" - }, - "type": "array" - } - }, - "required": ["tasks"], - "type": "object" - }, - "TaskStatusNotificationParams": { - "allOf": [ - { - "$ref": "https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/2025-11-25/schema.json#/$defs/NotificationParams" - }, - { - "$ref": "#/$defs/Task" - } - ], - "description": "Parameters for a `notifications/tasks/status` notification." - }, - "TaskStatusNotification": { - "description": "An optional notification from the receiver to the requestor, informing them that a task's status has changed. Receivers are not required to send these notifications.", - "properties": { - "jsonrpc": { - "const": "2.0", - "type": "string" - }, - "method": { - "const": "notifications/tasks/status", - "type": "string" - }, - "params": { - "$ref": "#/$defs/TaskStatusNotificationParams" - } - }, - "required": ["jsonrpc", "method", "params"], - "type": "object" - }, - "TaskServerCapabilities": { - "description": "Task-related server capabilities. Include this in the server's capabilities object during initialization.", - "properties": { - "tasks": { - "properties": { - "list": { - "description": "Whether this server supports tasks/list.", - "type": "object" - }, - "cancel": { - "description": "Whether this server supports tasks/cancel.", - "type": "object" - }, - "requests": { - "properties": { - "tools": { - "properties": { - "call": { - "description": "Whether the server supports task-augmented tools/call requests.", - "type": "object" - } - }, - "type": "object" - } - }, - "type": "object" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "TaskClientCapabilities": { - "description": "Task-related client capabilities. Include this in the client's capabilities object during initialization.", - "properties": { - "tasks": { - "properties": { - "list": { - "description": "Whether this client supports tasks/list.", - "type": "object" - }, - "cancel": { - "description": "Whether this client supports tasks/cancel.", - "type": "object" - }, - "requests": { - "properties": { - "sampling": { - "properties": { - "createMessage": { - "description": "Whether the client supports task-augmented sampling/createMessage requests.", - "type": "object" - } - }, - "type": "object" - }, - "elicitation": { - "properties": { - "create": { - "description": "Whether the client supports task-augmented elicitation/create requests.", - "type": "object" - } - }, - "type": "object" - } - }, - "type": "object" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "ToolTaskSupport": { - "description": "Task support declaration for individual tools.\n- \"forbidden\": Tool does not support task-augmented execution (default when absent)\n- \"optional\": Tool may support task-augmented execution\n- \"required\": Tool requires task-augmented execution", - "enum": ["forbidden", "optional", "required"], - "type": "string" - } - } -} diff --git a/scripts/generate-schemas.ts b/scripts/generate-schemas.ts new file mode 100644 index 0000000..3329cbb --- /dev/null +++ b/scripts/generate-schemas.ts @@ -0,0 +1,326 @@ +/** + * Schema Generation Script using ts-to-zod as a library + * + * This script generates Zod schemas from schema.ts and performs necessary + * post-processing for compatibility with this project. + * + * Follows the same pattern as ext-apps/scripts/generate-schemas.ts. + * + * ## Post-Processing Steps + * + * ### 1. Zod Import Path (`"zod"` → `"zod/v4"`) + * ts-to-zod generates `import { z } from "zod"`. We rewrite to `import { z } from "zod/v4"` + * to match MCP SDK's zod/v4 schemas. + * + * ### 2. External Type References (`z.any()` → SDK schemas) + * ts-to-zod cannot resolve types imported from @modelcontextprotocol/sdk. + * We replace z.any() placeholders with actual SDK schema imports. + * + * ### 3. Index Signatures (`z.record().and()` → `.passthrough()`) + * TypeScript index signatures are translated to z.record().and(z.object({...})). + * We replace with z.object({...}).passthrough() for compatibility. + * + * @see https://github.com/fabien0102/ts-to-zod + */ + +import { readFileSync, writeFileSync, existsSync } from "node:fs"; +import { dirname, join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; +import { generate } from "ts-to-zod"; +import { toJSONSchema, type $ZodType } from "zod/v4/core"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const PROJECT_ROOT = join(__dirname, ".."); + +const SCHEMA_DIR = join(PROJECT_ROOT, "schema", "draft", "2025-11-25"); +const SPEC_TYPES_FILE = join(SCHEMA_DIR, "schema.ts"); +const GENERATED_DIR = join(SCHEMA_DIR, "generated"); +const SCHEMA_OUTPUT_FILE = join(GENERATED_DIR, "schema.ts"); +const JSON_SCHEMA_OUTPUT_FILE = join(SCHEMA_DIR, "schema.json"); + +/** + * Mapping from ts-to-zod generated placeholder names to actual SDK schema exports. + * Only includes types that have matching SDK exports. + */ +const EXTERNAL_TYPE_SCHEMAS: string[] = [ + "JSONRPCRequestSchema", + "JSONRPCNotificationSchema", + "PaginatedRequestSchema", + "PaginatedResultSchema", + "ResultSchema", + "RequestIdSchema", + "ProgressTokenSchema", + "CursorSchema", +]; + +/** + * External types that don't have direct SDK schema exports. + * We replace their z.any() placeholders with local Zod definitions. + */ +const LOCAL_SCHEMA_REPLACEMENTS: Record = { + RequestParamsSchema: `z.object({ + _meta: z.object({ progressToken: ProgressTokenSchema.optional() }).passthrough().optional(), +}).passthrough()`, + NotificationParamsSchema: `z.object({ + _meta: z.record(z.string(), z.unknown()).optional(), +}).passthrough()`, +}; + +async function main() { + const isCheck = process.argv.includes("--check"); + + if (isCheck) { + console.log("Checking schemas..."); + } else { + console.log("šŸ”§ Generating Zod schemas from schema.ts...\n"); + } + + const sourceText = readFileSync(SPEC_TYPES_FILE, "utf-8"); + + const result = generate({ + sourceText, + keepComments: true, + skipParseJSDoc: false, + getSchemaName: (typeName: string) => `${typeName}Schema`, + }); + + if (result.errors.length > 0) { + console.error("āŒ Generation errors:"); + for (const error of result.errors) { + console.error(` - ${error}`); + } + process.exit(1); + } + + if (result.hasCircularDependencies) { + console.warn("āš ļø Warning: Circular dependencies detected in types"); + } + + // Generate Zod schemas file + const relativeImportPath = relative(GENERATED_DIR, SCHEMA_DIR).replace( + /\\/g, + "/" + ); + let schemasContent = result.getZodSchemasFile( + `${relativeImportPath}/schema.js` + ); + schemasContent = postProcess(schemasContent); + + if (isCheck) { + // Check mode: compare generated output against existing files + let hasChanges = false; + + if (existsSync(SCHEMA_OUTPUT_FILE)) { + const existing = readFileSync(SCHEMA_OUTPUT_FILE, "utf-8"); + if (existing !== schemasContent) { + console.error( + "āŒ Generated Zod schemas are out of date. Run: npm run generate:schemas" + ); + hasChanges = true; + } else { + console.log(" āœ“ Zod schemas are up to date"); + } + } else { + console.error( + "āŒ Generated Zod schemas file does not exist. Run: npm run generate:schemas" + ); + hasChanges = true; + } + + // Write temp file to generate JSON Schema for comparison + const tempFile = SCHEMA_OUTPUT_FILE + ".check.ts"; + writeFileSync(tempFile, schemasContent, "utf-8"); + try { + const jsonSchemaContent = await generateJsonSchemaContent(tempFile); + if (existsSync(JSON_SCHEMA_OUTPUT_FILE)) { + const existing = readFileSync(JSON_SCHEMA_OUTPUT_FILE, "utf-8"); + if (existing !== jsonSchemaContent) { + console.error( + "āŒ JSON Schema is out of date. Run: npm run generate:schemas" + ); + hasChanges = true; + } else { + console.log(" āœ“ JSON Schema is up to date"); + } + } else { + console.error( + "āŒ JSON Schema file does not exist. Run: npm run generate:schemas" + ); + hasChanges = true; + } + } finally { + // Clean up temp file + const { unlinkSync } = await import("node:fs"); + try { + unlinkSync(tempFile); + } catch {} + } + + if (hasChanges) { + process.exit(1); + } + console.log("\nAll schemas are up to date!"); + return; + } + + // Write Zod schemas + writeFileSync(SCHEMA_OUTPUT_FILE, schemasContent, "utf-8"); + console.log(`āœ… Written: ${SCHEMA_OUTPUT_FILE}`); + + // Generate and write JSON Schema + const jsonSchemaContent = await generateJsonSchemaContent(SCHEMA_OUTPUT_FILE); + writeFileSync(JSON_SCHEMA_OUTPUT_FILE, jsonSchemaContent, "utf-8"); + console.log(`āœ… Written: ${JSON_SCHEMA_OUTPUT_FILE}`); + + console.log("\nšŸŽ‰ Schema generation complete!"); +} + +/** + * Generate JSON Schema content from the Zod schemas file. + */ +async function generateJsonSchemaContent( + zodSchemasFile: string +): Promise { + // Convert to file:// URL for dynamic import (required on Windows) + const fileUrl = new URL(`file:///${zodSchemasFile.replace(/\\/g, "/")}`); + const schemas = await import(fileUrl.href); + + const jsonSchema: { + $schema: string; + $id: string; + title: string; + description: string; + $defs: Record; + } = { + $schema: "https://json-schema.org/draft/2020-12/schema", + $id: "https://modelcontextprotocol.io/ext-tasks/schema.json", + title: "MCP Tasks Extension", + description: + "JSON Schema for MCP Tasks extension protocol messages. Extension Identifier: io.modelcontextprotocol/tasks", + $defs: {}, + }; + + for (const [name, schema] of Object.entries(schemas)) { + if ( + name.endsWith("Schema") && + typeof schema === "object" && + schema !== null + ) { + const typeName = name.replace(/Schema$/, ""); + + // Skip SDK external types and local replacement types — only include task-specific types + if ( + EXTERNAL_TYPE_SCHEMAS.includes(name) || + name in LOCAL_SCHEMA_REPLACEMENTS + ) { + continue; + } + + try { + jsonSchema.$defs[typeName] = toJSONSchema(schema as $ZodType, { + unrepresentable: "any", + }); + } catch (error) { + console.warn(`āš ļø Could not convert ${name} to JSON Schema: ${error}`); + } + } + } + + return JSON.stringify(jsonSchema, null, 2) + "\n"; +} + +/** + * Post-process generated Zod schemas for project compatibility. + */ +function postProcess(content: string): string { + // 1. Rewrite to zod/v4 and add MCP SDK schema imports + const sdkImports = EXTERNAL_TYPE_SCHEMAS.join(",\n "); + + content = content.replace( + 'import { z } from "zod";', + `import { z } from "zod/v4"; +import { + ${sdkImports}, +} from "@modelcontextprotocol/sdk/types.js";` + ); + + // 2. Remove z.any() placeholders for SDK-imported external types + for (const schemaName of EXTERNAL_TYPE_SCHEMAS) { + content = content.replace( + new RegExp( + `(?:export )?const ${schemaName} = z\\.any\\(\\);\\n?`, + "g" + ), + "" + ); + } + + // 3. Replace z.any() placeholders for locally-defined types + for (const [schemaName, replacement] of Object.entries( + LOCAL_SCHEMA_REPLACEMENTS + )) { + content = content.replace( + new RegExp( + `(?:export )?const ${schemaName} = z\\.any\\(\\);`, + "g" + ), + `const ${schemaName} = ${replacement};` + ); + } + + // 4. Replace z.record().and(z.object({...})) with z.object({...}).passthrough() + content = replaceRecordAndWithPassthrough(content); + + // 5. Add header comment + content = content.replace( + "// Generated by ts-to-zod", + `// Generated by ts-to-zod +// Post-processed for Zod v4 compatibility and MCP SDK integration +// Run: npm run generate:schemas` + ); + + return content; +} + +/** + * Replace z.record(z.string(), z.unknown()).and(z.object({...})) with z.object({...}).passthrough() + * Uses brace-counting to handle nested objects correctly. + * passthrough() works in both Zod v3 and v4, allowing extra properties. + */ +function replaceRecordAndWithPassthrough(content: string): string { + const pattern = "z.record(z.string(), z.unknown()).and(z.object({"; + let result = content; + let startIndex = 0; + + while (true) { + const matchStart = result.indexOf(pattern, startIndex); + if (matchStart === -1) break; + + const objectStart = matchStart + pattern.length; + let braceCount = 1; + let i = objectStart; + + while (i < result.length && braceCount > 0) { + if (result[i] === "{") braceCount++; + else if (result[i] === "}") braceCount--; + i++; + } + + if (result.slice(i, i + 2) === "))") { + const objectContent = result.slice(objectStart, i - 1); + const replacement = `z.object({${objectContent}}).passthrough()`; + result = result.slice(0, matchStart) + replacement + result.slice(i + 2); + startIndex = matchStart + replacement.length; + } else { + startIndex = i; + } + } + + return result; +} + +main().catch((error) => { + console.error("āŒ Schema generation failed:", error); + process.exit(1); +}); diff --git a/tsconfig.json b/tsconfig.json index fbf7ab8..e6f480e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,5 +7,6 @@ "moduleResolution": "node", "skipLibCheck": true }, - "include": ["schema/**/*.ts"] + "include": ["schema/**/*.ts"], + "exclude": ["schema/**/generated/**"] }