@mcpmake/core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyzer/auth-detector.d.ts +13 -0
- package/dist/analyzer/auth-detector.d.ts.map +1 -0
- package/dist/analyzer/auth-detector.js +143 -0
- package/dist/analyzer/auth-detector.js.map +1 -0
- package/dist/analyzer/dom-parser.d.ts +11 -0
- package/dist/analyzer/dom-parser.d.ts.map +1 -0
- package/dist/analyzer/dom-parser.js +260 -0
- package/dist/analyzer/dom-parser.js.map +1 -0
- package/dist/analyzer/goal-crawler.d.ts +26 -0
- package/dist/analyzer/goal-crawler.d.ts.map +1 -0
- package/dist/analyzer/goal-crawler.js +178 -0
- package/dist/analyzer/goal-crawler.js.map +1 -0
- package/dist/analyzer/hybrid-detector.d.ts +29 -0
- package/dist/analyzer/hybrid-detector.d.ts.map +1 -0
- package/dist/analyzer/hybrid-detector.js +97 -0
- package/dist/analyzer/hybrid-detector.js.map +1 -0
- package/dist/analyzer/index.d.ts +13 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +9 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/analyzer/screenshot-capture.d.ts +30 -0
- package/dist/analyzer/screenshot-capture.d.ts.map +1 -0
- package/dist/analyzer/screenshot-capture.js +43 -0
- package/dist/analyzer/screenshot-capture.js.map +1 -0
- package/dist/analyzer/selector-builder.d.ts +20 -0
- package/dist/analyzer/selector-builder.d.ts.map +1 -0
- package/dist/analyzer/selector-builder.js +200 -0
- package/dist/analyzer/selector-builder.js.map +1 -0
- package/dist/analyzer/semantic-analyzer.d.ts +14 -0
- package/dist/analyzer/semantic-analyzer.d.ts.map +1 -0
- package/dist/analyzer/semantic-analyzer.js +146 -0
- package/dist/analyzer/semantic-analyzer.js.map +1 -0
- package/dist/analyzer/site-crawler.d.ts +39 -0
- package/dist/analyzer/site-crawler.d.ts.map +1 -0
- package/dist/analyzer/site-crawler.js +236 -0
- package/dist/analyzer/site-crawler.js.map +1 -0
- package/dist/config/configurable-command.d.ts +14 -0
- package/dist/config/configurable-command.d.ts.map +1 -0
- package/dist/config/configurable-command.js +71 -0
- package/dist/config/configurable-command.js.map +1 -0
- package/dist/config/mcpmake-config.d.ts +69 -0
- package/dist/config/mcpmake-config.d.ts.map +1 -0
- package/dist/config/mcpmake-config.js +209 -0
- package/dist/config/mcpmake-config.js.map +1 -0
- package/dist/emitter/code-writer.d.ts +9 -0
- package/dist/emitter/code-writer.d.ts.map +1 -0
- package/dist/emitter/code-writer.js +26 -0
- package/dist/emitter/code-writer.js.map +1 -0
- package/dist/emitter/index.d.ts +33 -0
- package/dist/emitter/index.d.ts.map +1 -0
- package/dist/emitter/index.js +297 -0
- package/dist/emitter/index.js.map +1 -0
- package/dist/emitter/mcpb-bundler.d.ts +32 -0
- package/dist/emitter/mcpb-bundler.d.ts.map +1 -0
- package/dist/emitter/mcpb-bundler.js +173 -0
- package/dist/emitter/mcpb-bundler.js.map +1 -0
- package/dist/emitter/project-scaffolder.d.ts +5 -0
- package/dist/emitter/project-scaffolder.d.ts.map +1 -0
- package/dist/emitter/project-scaffolder.js +101 -0
- package/dist/emitter/project-scaffolder.js.map +1 -0
- package/dist/emitter/python-template-loader.d.ts +5 -0
- package/dist/emitter/python-template-loader.d.ts.map +1 -0
- package/dist/emitter/python-template-loader.js +31 -0
- package/dist/emitter/python-template-loader.js.map +1 -0
- package/dist/emitter/python-templates/dockerfile.hbs +14 -0
- package/dist/emitter/python-templates/env.example.hbs +12 -0
- package/dist/emitter/python-templates/requirements.txt.hbs +4 -0
- package/dist/emitter/python-templates/server.py.hbs +93 -0
- package/dist/emitter/site-scaffolder.d.ts +14 -0
- package/dist/emitter/site-scaffolder.d.ts.map +1 -0
- package/dist/emitter/site-scaffolder.js +71 -0
- package/dist/emitter/site-scaffolder.js.map +1 -0
- package/dist/emitter/site-template-loader.d.ts +6 -0
- package/dist/emitter/site-template-loader.d.ts.map +1 -0
- package/dist/emitter/site-template-loader.js +48 -0
- package/dist/emitter/site-template-loader.js.map +1 -0
- package/dist/emitter/site-templates/browser-manager.ts.hbs +233 -0
- package/dist/emitter/site-templates/config.ts.hbs +28 -0
- package/dist/emitter/site-templates/dockerfile.hbs +31 -0
- package/dist/emitter/site-templates/env.example.hbs +19 -0
- package/dist/emitter/site-templates/package.json.hbs +26 -0
- package/dist/emitter/site-templates/server-main-http.ts.hbs +108 -0
- package/dist/emitter/site-templates/server-main.ts.hbs +23 -0
- package/dist/emitter/site-templates/tool-handler-action.ts.hbs +86 -0
- package/dist/emitter/site-templates/tool-handler-form.ts.hbs +116 -0
- package/dist/emitter/site-templates/tool-handler-lifecycle.ts.hbs +146 -0
- package/dist/emitter/site-templates/tool-index.ts.hbs +11 -0
- package/dist/emitter/template-loader.d.ts +2 -0
- package/dist/emitter/template-loader.d.ts.map +1 -0
- package/dist/emitter/template-loader.js +28 -0
- package/dist/emitter/template-loader.js.map +1 -0
- package/dist/emitter/templates/auth-provider.ts.hbs +57 -0
- package/dist/emitter/templates/config.ts.hbs +110 -0
- package/dist/emitter/templates/discovery.ts.hbs +320 -0
- package/dist/emitter/templates/dockerfile.hbs +34 -0
- package/dist/emitter/templates/env.example.hbs +52 -0
- package/dist/emitter/templates/gitignore.hbs +5 -0
- package/dist/emitter/templates/http-executor.ts.hbs +159 -0
- package/dist/emitter/templates/oauth.ts.hbs +188 -0
- package/dist/emitter/templates/package.json.hbs +25 -0
- package/dist/emitter/templates/prompts.ts.hbs +22 -0
- package/dist/emitter/templates/readme.md.hbs +156 -0
- package/dist/emitter/templates/resources.ts.hbs +63 -0
- package/dist/emitter/templates/response-filter.ts.hbs +128 -0
- package/dist/emitter/templates/server-main-http.ts.hbs +442 -0
- package/dist/emitter/templates/server-main.ts.hbs +40 -0
- package/dist/emitter/templates/task-handlers.ts.hbs +189 -0
- package/dist/emitter/templates/task-manager.ts.hbs +139 -0
- package/dist/emitter/templates/task-sse.ts.hbs +105 -0
- package/dist/emitter/templates/tool-handler.ts.hbs +142 -0
- package/dist/emitter/templates/tool-index.ts.hbs +42 -0
- package/dist/emitter/templates/tool-test.ts.hbs +57 -0
- package/dist/emitter/templates/trace.ts.hbs +79 -0
- package/dist/emitter/templates/tsconfig.json.hbs +16 -0
- package/dist/emitter/templates/types.ts.hbs +5 -0
- package/dist/emitter/worker-template-loader.d.ts +6 -0
- package/dist/emitter/worker-template-loader.d.ts.map +1 -0
- package/dist/emitter/worker-template-loader.js +34 -0
- package/dist/emitter/worker-template-loader.js.map +1 -0
- package/dist/emitter/worker-templates/config.ts.hbs +93 -0
- package/dist/emitter/worker-templates/dev-vars.example.hbs +27 -0
- package/dist/emitter/worker-templates/gitignore.hbs +6 -0
- package/dist/emitter/worker-templates/package.json.hbs +24 -0
- package/dist/emitter/worker-templates/readme.md.hbs +73 -0
- package/dist/emitter/worker-templates/server.test.ts.hbs +20 -0
- package/dist/emitter/worker-templates/tool-handler.ts.hbs +103 -0
- package/dist/emitter/worker-templates/tool-index.ts.hbs +28 -0
- package/dist/emitter/worker-templates/tsconfig.json.hbs +17 -0
- package/dist/emitter/worker-templates/worker.ts.hbs +286 -0
- package/dist/emitter/worker-templates/wrangler.toml.hbs +19 -0
- package/dist/generator/spec-generator.d.ts +7 -0
- package/dist/generator/spec-generator.d.ts.map +1 -0
- package/dist/generator/spec-generator.js +51 -0
- package/dist/generator/spec-generator.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/har-filter.d.ts +9 -0
- package/dist/parser/har-filter.d.ts.map +1 -0
- package/dist/parser/har-filter.js +72 -0
- package/dist/parser/har-filter.js.map +1 -0
- package/dist/parser/har-loader.d.ts +3 -0
- package/dist/parser/har-loader.d.ts.map +1 -0
- package/dist/parser/har-loader.js +15 -0
- package/dist/parser/har-loader.js.map +1 -0
- package/dist/parser/har-normalizer.d.ts +21 -0
- package/dist/parser/har-normalizer.d.ts.map +1 -0
- package/dist/parser/har-normalizer.js +79 -0
- package/dist/parser/har-normalizer.js.map +1 -0
- package/dist/parser/index.d.ts +11 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +7 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/openapi-loader.d.ts +7 -0
- package/dist/parser/openapi-loader.d.ts.map +1 -0
- package/dist/parser/openapi-loader.js +309 -0
- package/dist/parser/openapi-loader.js.map +1 -0
- package/dist/parser/operation-extractor.d.ts +14 -0
- package/dist/parser/operation-extractor.d.ts.map +1 -0
- package/dist/parser/operation-extractor.js +156 -0
- package/dist/parser/operation-extractor.js.map +1 -0
- package/dist/parser/overlay-loader.d.ts +11 -0
- package/dist/parser/overlay-loader.d.ts.map +1 -0
- package/dist/parser/overlay-loader.js +185 -0
- package/dist/parser/overlay-loader.js.map +1 -0
- package/dist/parser/postman-loader.d.ts +10 -0
- package/dist/parser/postman-loader.d.ts.map +1 -0
- package/dist/parser/postman-loader.js +107 -0
- package/dist/parser/postman-loader.js.map +1 -0
- package/dist/parser/schema-converter.d.ts +13 -0
- package/dist/parser/schema-converter.d.ts.map +1 -0
- package/dist/parser/schema-converter.js +118 -0
- package/dist/parser/schema-converter.js.map +1 -0
- package/dist/plugins/adapter.d.ts +41 -0
- package/dist/plugins/adapter.d.ts.map +1 -0
- package/dist/plugins/adapter.js +16 -0
- package/dist/plugins/adapter.js.map +1 -0
- package/dist/plugins/loader.d.ts +26 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +59 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/pricing.d.ts +56 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +134 -0
- package/dist/pricing.js.map +1 -0
- package/dist/providers/index.d.ts +16 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +57 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/recorder/browser-recorder.d.ts +23 -0
- package/dist/recorder/browser-recorder.d.ts.map +1 -0
- package/dist/recorder/browser-recorder.js +206 -0
- package/dist/recorder/browser-recorder.js.map +1 -0
- package/dist/rescan/diff-engine.d.ts +6 -0
- package/dist/rescan/diff-engine.d.ts.map +1 -0
- package/dist/rescan/diff-engine.js +313 -0
- package/dist/rescan/diff-engine.js.map +1 -0
- package/dist/rescan/index.d.ts +4 -0
- package/dist/rescan/index.d.ts.map +1 -0
- package/dist/rescan/index.js +3 -0
- package/dist/rescan/index.js.map +1 -0
- package/dist/rescan/rescan-runner.d.ts +43 -0
- package/dist/rescan/rescan-runner.d.ts.map +1 -0
- package/dist/rescan/rescan-runner.js +70 -0
- package/dist/rescan/rescan-runner.js.map +1 -0
- package/dist/rescan/rescan-scheduler.d.ts +42 -0
- package/dist/rescan/rescan-scheduler.d.ts.map +1 -0
- package/dist/rescan/rescan-scheduler.js +180 -0
- package/dist/rescan/rescan-scheduler.js.map +1 -0
- package/dist/site-transformer/browser-tools.d.ts +11 -0
- package/dist/site-transformer/browser-tools.d.ts.map +1 -0
- package/dist/site-transformer/browser-tools.js +60 -0
- package/dist/site-transformer/browser-tools.js.map +1 -0
- package/dist/site-transformer/index.d.ts +3 -0
- package/dist/site-transformer/index.d.ts.map +1 -0
- package/dist/site-transformer/index.js +3 -0
- package/dist/site-transformer/index.js.map +1 -0
- package/dist/site-transformer/selector-healer.d.ts +9 -0
- package/dist/site-transformer/selector-healer.d.ts.map +1 -0
- package/dist/site-transformer/selector-healer.js +107 -0
- package/dist/site-transformer/selector-healer.js.map +1 -0
- package/dist/site-transformer/tool-generator.d.ts +14 -0
- package/dist/site-transformer/tool-generator.d.ts.map +1 -0
- package/dist/site-transformer/tool-generator.js +246 -0
- package/dist/site-transformer/tool-generator.js.map +1 -0
- package/dist/transformer/auth-detector.d.ts +29 -0
- package/dist/transformer/auth-detector.d.ts.map +1 -0
- package/dist/transformer/auth-detector.js +104 -0
- package/dist/transformer/auth-detector.js.map +1 -0
- package/dist/transformer/catalog-builder.d.ts +19 -0
- package/dist/transformer/catalog-builder.d.ts.map +1 -0
- package/dist/transformer/catalog-builder.js +57 -0
- package/dist/transformer/catalog-builder.js.map +1 -0
- package/dist/transformer/client-compat.d.ts +7 -0
- package/dist/transformer/client-compat.d.ts.map +1 -0
- package/dist/transformer/client-compat.js +45 -0
- package/dist/transformer/client-compat.js.map +1 -0
- package/dist/transformer/har-clusterer.d.ts +10 -0
- package/dist/transformer/har-clusterer.d.ts.map +1 -0
- package/dist/transformer/har-clusterer.js +28 -0
- package/dist/transformer/har-clusterer.js.map +1 -0
- package/dist/transformer/har-dedup.d.ts +11 -0
- package/dist/transformer/har-dedup.d.ts.map +1 -0
- package/dist/transformer/har-dedup.js +82 -0
- package/dist/transformer/har-dedup.js.map +1 -0
- package/dist/transformer/har-schema-inferrer.d.ts +16 -0
- package/dist/transformer/har-schema-inferrer.d.ts.map +1 -0
- package/dist/transformer/har-schema-inferrer.js +91 -0
- package/dist/transformer/har-schema-inferrer.js.map +1 -0
- package/dist/transformer/har-to-operations.d.ts +14 -0
- package/dist/transformer/har-to-operations.d.ts.map +1 -0
- package/dist/transformer/har-to-operations.js +193 -0
- package/dist/transformer/har-to-operations.js.map +1 -0
- package/dist/transformer/index.d.ts +9 -0
- package/dist/transformer/index.d.ts.map +1 -0
- package/dist/transformer/index.js +7 -0
- package/dist/transformer/index.js.map +1 -0
- package/dist/transformer/llm-namer.d.ts +7 -0
- package/dist/transformer/llm-namer.d.ts.map +1 -0
- package/dist/transformer/llm-namer.js +60 -0
- package/dist/transformer/llm-namer.js.map +1 -0
- package/dist/transformer/naming.d.ts +5 -0
- package/dist/transformer/naming.d.ts.map +1 -0
- package/dist/transformer/naming.js +31 -0
- package/dist/transformer/naming.js.map +1 -0
- package/dist/transformer/operation-filter.d.ts +14 -0
- package/dist/transformer/operation-filter.d.ts.map +1 -0
- package/dist/transformer/operation-filter.js +53 -0
- package/dist/transformer/operation-filter.js.map +1 -0
- package/dist/transformer/resource-builder.d.ts +13 -0
- package/dist/transformer/resource-builder.d.ts.map +1 -0
- package/dist/transformer/resource-builder.js +87 -0
- package/dist/transformer/resource-builder.js.map +1 -0
- package/dist/transformer/schema-merger.d.ts +15 -0
- package/dist/transformer/schema-merger.d.ts.map +1 -0
- package/dist/transformer/schema-merger.js +66 -0
- package/dist/transformer/schema-merger.js.map +1 -0
- package/dist/transformer/stainless-config.d.ts +75 -0
- package/dist/transformer/stainless-config.d.ts.map +1 -0
- package/dist/transformer/stainless-config.js +59 -0
- package/dist/transformer/stainless-config.js.map +1 -0
- package/dist/transformer/stainless-translator.d.ts +61 -0
- package/dist/transformer/stainless-translator.d.ts.map +1 -0
- package/dist/transformer/stainless-translator.js +375 -0
- package/dist/transformer/stainless-translator.js.map +1 -0
- package/dist/transformer/tool-builder.d.ts +4 -0
- package/dist/transformer/tool-builder.d.ts.map +1 -0
- package/dist/transformer/tool-builder.js +119 -0
- package/dist/transformer/tool-builder.js.map +1 -0
- package/dist/types/index.d.ts +140 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/site.d.ts +285 -0
- package/dist/types/site.d.ts.map +1 -0
- package/dist/types/site.js +9 -0
- package/dist/types/site.js.map +1 -0
- package/dist/utils/fail.d.ts +49 -0
- package/dist/utils/fail.d.ts.map +1 -0
- package/dist/utils/fail.js +223 -0
- package/dist/utils/fail.js.map +1 -0
- package/dist/utils/fs.d.ts +6 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +29 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/interactive.d.ts +7 -0
- package/dist/utils/interactive.d.ts.map +1 -0
- package/dist/utils/interactive.js +31 -0
- package/dist/utils/interactive.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +3 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/sanitize.d.ts +29 -0
- package/dist/utils/sanitize.d.ts.map +1 -0
- package/dist/utils/sanitize.js +45 -0
- package/dist/utils/sanitize.js.map +1 -0
- package/dist/utils/watcher.d.ts +12 -0
- package/dist/utils/watcher.d.ts.map +1 -0
- package/dist/utils/watcher.js +37 -0
- package/dist/utils/watcher.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { registerAllTools } from './tools/index.js';
|
|
5
|
+
{{#if hasResources}}
|
|
6
|
+
import { registerAllResources } from './resources.js';
|
|
7
|
+
{{/if}}
|
|
8
|
+
{{#if hasPrompts}}
|
|
9
|
+
import { registerAllPrompts } from './prompts.js';
|
|
10
|
+
{{/if}}
|
|
11
|
+
import { loadConfig } from './config.js';
|
|
12
|
+
{{#if hasAsyncTools}}
|
|
13
|
+
import { handleTaskRoutes, handleTaskRpc } from './task-handlers.js';
|
|
14
|
+
import { handleTaskSse } from './task-sse.js';
|
|
15
|
+
{{/if}}
|
|
16
|
+
{{#if hasOAuth}}
|
|
17
|
+
import { getAuthServerMetadata } from './oauth.js';
|
|
18
|
+
{{/if}}
|
|
19
|
+
import http from 'node:http';
|
|
20
|
+
import crypto from 'node:crypto';
|
|
21
|
+
import { deriveContext, runWithTrace } from './trace.js';
|
|
22
|
+
|
|
23
|
+
/* ── Structured JSON logger ─────────────────────────────────────────── */
|
|
24
|
+
|
|
25
|
+
function log(level: 'info' | 'error', msg: string, extra?: Record<string, unknown>): void {
|
|
26
|
+
const entry = { ts: new Date().toISOString(), level, msg, ...extra };
|
|
27
|
+
process.stderr.write(JSON.stringify(entry) + '\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* ── MCP 2026-07-28 ─────────────────────────────────────────────────────
|
|
31
|
+
* This server is being migrated toward the 2026-07-28 spec revision (RC).
|
|
32
|
+
* Implemented here (additive, works against the current SDK):
|
|
33
|
+
* - stateless transport by default (set MCP_STATEFUL=true to keep sessions)
|
|
34
|
+
* - server/discover preflight (SEP-2575)
|
|
35
|
+
* - Mcp-Method / Mcp-Name routing headers logged (SEP-2243)
|
|
36
|
+
* - Tasks extension methods tasks/get|update|cancel (tasks/list removed)
|
|
37
|
+
* SDK-gated (lands when @modelcontextprotocol/sdk ships RC support, then the
|
|
38
|
+
* package.json pin is bumped): removal of the initialize/initialized handshake
|
|
39
|
+
* and moving protocolVersion/clientInfo/capabilities into per-request _meta.
|
|
40
|
+
* The 2026-07-28 RC is NOT final — wire shapes marked VERIFY may change. */
|
|
41
|
+
const PROTOCOL_REVISION = '2026-07-28'; // VERIFY against the final spec
|
|
42
|
+
|
|
43
|
+
/* ── Bearer authentication ──────────────────────────────────────────────
|
|
44
|
+
* Constant-time check that the request presents the expected bearer token via
|
|
45
|
+
* the `Authorization: Bearer <token>` header. Returns true when no token is
|
|
46
|
+
* configured (auth disabled). The token is never accepted from the query
|
|
47
|
+
* string — URLs leak through logs, history, and referrers. */
|
|
48
|
+
function isAuthorized(req: http.IncomingMessage, expected: string | undefined): boolean {
|
|
49
|
+
if (!expected) return true; // auth disabled when MCP_AUTH_TOKEN is unset
|
|
50
|
+
|
|
51
|
+
const header = req.headers.authorization;
|
|
52
|
+
if (typeof header !== 'string' || !header.startsWith('Bearer ')) return false;
|
|
53
|
+
const presented = header.slice(7).trim();
|
|
54
|
+
if (!presented) return false;
|
|
55
|
+
|
|
56
|
+
const a = Buffer.from(presented);
|
|
57
|
+
const b = Buffer.from(expected);
|
|
58
|
+
if (a.length !== b.length) return false;
|
|
59
|
+
return crypto.timingSafeEqual(a, b);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ── Bounded request-body reader ─────────────────────────────────────────
|
|
63
|
+
* Buffers the body so we can peek JSON-RPC for server/discover + tasks/* and
|
|
64
|
+
* then hand the parsed value to the SDK transport's `parsedBody` argument
|
|
65
|
+
* (so the drained stream is never re-read). Resolves null past the size cap. */
|
|
66
|
+
const MAX_BODY = 4 * 1024 * 1024; // 4 MB
|
|
67
|
+
function readBody(req: http.IncomingMessage): Promise<Buffer | null> {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const chunks: Buffer[] = [];
|
|
70
|
+
let total = 0;
|
|
71
|
+
let settled = false;
|
|
72
|
+
const done = (v: Buffer | null) => {
|
|
73
|
+
if (!settled) {
|
|
74
|
+
settled = true;
|
|
75
|
+
resolve(v);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
req.on('data', (c: Buffer) => {
|
|
79
|
+
total += c.length;
|
|
80
|
+
if (total > MAX_BODY) {
|
|
81
|
+
done(null);
|
|
82
|
+
req.destroy();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
chunks.push(c);
|
|
86
|
+
});
|
|
87
|
+
req.on('end', () => done(Buffer.concat(chunks)));
|
|
88
|
+
req.on('error', () => done(null));
|
|
89
|
+
req.on('aborted', () => done(null));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sendJson(res: http.ServerResponse, status: number, body: unknown): void {
|
|
94
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
95
|
+
res.end(JSON.stringify(body));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Narrow a possibly-repeated request header to a single string value. */
|
|
99
|
+
function headerValue(v: string | string[] | undefined): string | undefined {
|
|
100
|
+
return Array.isArray(v) ? v[0] : v;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function rpcError(res: http.ServerResponse, id: unknown, code: number, message: string): void {
|
|
104
|
+
sendJson(res, 200, { jsonrpc: '2.0', id: id ?? null, error: { code, message } });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* ── server/discover (SEP-2575) — VERIFY result shape against final spec ── */
|
|
108
|
+
function buildDiscoverResult(id: unknown): Record<string, unknown> {
|
|
109
|
+
return {
|
|
110
|
+
jsonrpc: '2.0',
|
|
111
|
+
id: id ?? null,
|
|
112
|
+
result: {
|
|
113
|
+
server: { name: '{{serverName}}', version: '{{serverVersion}}' },
|
|
114
|
+
protocolRevision: PROTOCOL_REVISION,
|
|
115
|
+
capabilities: {
|
|
116
|
+
tools: { count: {{tools.length}} },
|
|
117
|
+
{{#if hasResources}}
|
|
118
|
+
resources: {},
|
|
119
|
+
{{/if}}
|
|
120
|
+
{{#if hasPrompts}}
|
|
121
|
+
prompts: {},
|
|
122
|
+
{{/if}}
|
|
123
|
+
},
|
|
124
|
+
extensions: [
|
|
125
|
+
{{#if hasAsyncTools}}
|
|
126
|
+
'tasks',
|
|
127
|
+
{{/if}}
|
|
128
|
+
{{#if hasOAuth}}
|
|
129
|
+
'oauth',
|
|
130
|
+
{{/if}}
|
|
131
|
+
],
|
|
132
|
+
cacheTtlSeconds: 300,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ── MCP server factory ──────────────────────────────────────────────────
|
|
138
|
+
* Builds a fully-registered McpServer. In stateless mode a fresh instance is
|
|
139
|
+
* created per request (a single McpServer connected to multiple transports
|
|
140
|
+
* concurrently races on its internal transport reference). */
|
|
141
|
+
function createMcpServer(config: ReturnType<typeof loadConfig>): McpServer {
|
|
142
|
+
const server = new McpServer({
|
|
143
|
+
name: '{{serverName}}',
|
|
144
|
+
version: '{{serverVersion}}',
|
|
145
|
+
});
|
|
146
|
+
registerAllTools(server, config);
|
|
147
|
+
{{#if hasResources}}
|
|
148
|
+
registerAllResources(server, config);
|
|
149
|
+
{{/if}}
|
|
150
|
+
{{#if hasPrompts}}
|
|
151
|
+
registerAllPrompts(server);
|
|
152
|
+
{{/if}}
|
|
153
|
+
return server;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ── Bootstrap ───────────────────────────────────────────────────────── */
|
|
157
|
+
|
|
158
|
+
const config = loadConfig();
|
|
159
|
+
const transportMode = process.env.TRANSPORT ?? 'stdio';
|
|
160
|
+
|
|
161
|
+
if (transportMode === 'http') {
|
|
162
|
+
const port = parseInt(process.env.PORT ?? '3000', 10);
|
|
163
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
164
|
+
log('error', 'Invalid PORT value, must be 1-65535', { PORT: process.env.PORT });
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
const allowedOrigin = process.env.ALLOWED_ORIGIN;
|
|
168
|
+
const authToken = process.env.MCP_AUTH_TOKEN;
|
|
169
|
+
/* Stateless by default (2026-07-28). MCP_STATEFUL=true keeps Mcp-Session-Id
|
|
170
|
+
* sessions during the migration / for clients that need resumable streams. */
|
|
171
|
+
const stateful = process.env.MCP_STATEFUL === 'true';
|
|
172
|
+
let isReady = false;
|
|
173
|
+
|
|
174
|
+
/* Stateful mode keeps one McpServer + transport PER SESSION, keyed by the
|
|
175
|
+
* Mcp-Session-Id the SDK assigns on `initialize`, so concurrent clients get
|
|
176
|
+
* independent, isolated sessions (a single shared transport cannot multiplex
|
|
177
|
+
* them). Stateless mode builds a fresh server + transport per request. */
|
|
178
|
+
const sessions = new Map<string, { server: McpServer; transport: StreamableHTTPServerTransport }>();
|
|
179
|
+
|
|
180
|
+
function isInitializeRequest(body: unknown): boolean {
|
|
181
|
+
return (
|
|
182
|
+
!!body &&
|
|
183
|
+
typeof body === 'object' &&
|
|
184
|
+
!Array.isArray(body) &&
|
|
185
|
+
(body as { method?: unknown }).method === 'initialize'
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function dispatchToTransport(
|
|
190
|
+
req: http.IncomingMessage,
|
|
191
|
+
res: http.ServerResponse,
|
|
192
|
+
parsedBody?: unknown,
|
|
193
|
+
): Promise<void> {
|
|
194
|
+
/* Bind W3C Trace Context for this request (continuing an inbound trace or
|
|
195
|
+
* starting one) so upstream API calls made inside the tool handlers carry
|
|
196
|
+
* the traceparent and the distributed trace spans the API hop. */
|
|
197
|
+
const trace = deriveContext(
|
|
198
|
+
headerValue(req.headers['traceparent']),
|
|
199
|
+
headerValue(req.headers['tracestate']),
|
|
200
|
+
);
|
|
201
|
+
await runWithTrace(trace, async () => {
|
|
202
|
+
if (stateful) {
|
|
203
|
+
const sessionId = headerValue(req.headers['mcp-session-id']);
|
|
204
|
+
const existing = sessionId ? sessions.get(sessionId) : undefined;
|
|
205
|
+
if (existing) {
|
|
206
|
+
await existing.transport.handleRequest(req, res, parsedBody);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (!sessionId && isInitializeRequest(parsedBody)) {
|
|
210
|
+
// New session: create a dedicated server + transport and register it
|
|
211
|
+
// under the session id the SDK generates during initialize.
|
|
212
|
+
const server = createMcpServer(config);
|
|
213
|
+
const transport = new StreamableHTTPServerTransport({
|
|
214
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
215
|
+
onsessioninitialized: (sid: string) => {
|
|
216
|
+
sessions.set(sid, { server, transport });
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
transport.onclose = () => {
|
|
220
|
+
const sid = transport.sessionId;
|
|
221
|
+
if (sid) sessions.delete(sid);
|
|
222
|
+
void server.close();
|
|
223
|
+
};
|
|
224
|
+
await server.connect(transport);
|
|
225
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Stateful, but no live session and not an initialize → the client must
|
|
229
|
+
// initialize (to obtain a session id) before sending other requests.
|
|
230
|
+
rpcError(res, null, -32600, 'Missing or unknown Mcp-Session-Id — send initialize first');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const reqServer = createMcpServer(config);
|
|
234
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
235
|
+
res.on('close', () => {
|
|
236
|
+
void transport.close();
|
|
237
|
+
void reqServer.close();
|
|
238
|
+
});
|
|
239
|
+
await reqServer.connect(transport);
|
|
240
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
245
|
+
const url = new URL(req.url ?? '/', `http://localhost:${port}`);
|
|
246
|
+
const method = req.method ?? 'GET';
|
|
247
|
+
|
|
248
|
+
/* ── Authentication ─────────────────────────────────────────────
|
|
249
|
+
* Every endpoint except /health, /ready{{#if hasOAuth}}, and the public
|
|
250
|
+
* OAuth metadata{{/if}} requires a valid bearer token when MCP_AUTH_TOKEN
|
|
251
|
+
* is set. This is the in-container line of defense; the hosting backend
|
|
252
|
+
* also validates before proxying. */
|
|
253
|
+
const isPublicPath =
|
|
254
|
+
url.pathname === '/health' ||
|
|
255
|
+
url.pathname === '/ready'{{#if hasOAuth}} ||
|
|
256
|
+
url.pathname === '/.well-known/oauth-authorization-server'{{/if}};
|
|
257
|
+
if (!isPublicPath && !isAuthorized(req, authToken)) {
|
|
258
|
+
log('error', 'Unauthorized request rejected', { path: url.pathname });
|
|
259
|
+
res.writeHead(401, {
|
|
260
|
+
'Content-Type': 'application/json',
|
|
261
|
+
'WWW-Authenticate': 'Bearer',
|
|
262
|
+
});
|
|
263
|
+
res.end(JSON.stringify({ error: 'Unauthorized: missing or invalid bearer token' }));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* ── Routing-header observability (SEP-2243, additive) ─────────── */
|
|
268
|
+
const mcpMethod = req.headers['mcp-method'];
|
|
269
|
+
const mcpName = req.headers['mcp-name'];
|
|
270
|
+
if (mcpMethod || mcpName) {
|
|
271
|
+
log('info', 'mcp-meta-headers', { mcpMethod, mcpName, path: url.pathname });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
{{#if hasOAuth}}
|
|
275
|
+
/* ── OAuth Authorization Server Metadata (RFC 8414) ─────────────
|
|
276
|
+
* Public discovery endpoint (the 2026-07-28 auth changes clarify
|
|
277
|
+
* .well-known discovery; DCR is retained, not replaced). */
|
|
278
|
+
if (url.pathname === '/.well-known/oauth-authorization-server') {
|
|
279
|
+
if (method !== 'GET') {
|
|
280
|
+
res.writeHead(405, { Allow: 'GET' });
|
|
281
|
+
res.end();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const authorizationUrl = config.oauth2AuthorizationUrl;
|
|
285
|
+
const tokenUrl = config.oauth2TokenUrl;
|
|
286
|
+
if (!authorizationUrl || !tokenUrl) {
|
|
287
|
+
sendJson(res, 503, { error: 'OAuth is not configured' });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const proto = req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http';
|
|
291
|
+
const issuer = allowedOrigin ?? `${proto}://${req.headers.host ?? 'localhost'}`;
|
|
292
|
+
sendJson(
|
|
293
|
+
res,
|
|
294
|
+
200,
|
|
295
|
+
getAuthServerMetadata(
|
|
296
|
+
{
|
|
297
|
+
clientId: config.oauth2ClientId ?? '',
|
|
298
|
+
clientSecret: config.oauth2ClientSecret,
|
|
299
|
+
authorizationUrl,
|
|
300
|
+
tokenUrl,
|
|
301
|
+
scopes: [],
|
|
302
|
+
redirectUri: config.oauth2RedirectUri ?? '',
|
|
303
|
+
},
|
|
304
|
+
issuer,
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
{{/if}}
|
|
311
|
+
{{#if hasAsyncTools}}
|
|
312
|
+
/* ── Task management REST routes (retained during the grace period) ── */
|
|
313
|
+
if (handleTaskSse(req, res, url)) return;
|
|
314
|
+
if (handleTaskRoutes(req, res, url)) return;
|
|
315
|
+
|
|
316
|
+
{{/if}}
|
|
317
|
+
if (url.pathname === '/mcp') {
|
|
318
|
+
/* Origin validation (DNS-rebinding protection per MCP spec) */
|
|
319
|
+
const origin = req.headers.origin;
|
|
320
|
+
if (allowedOrigin) {
|
|
321
|
+
if (origin && origin !== allowedOrigin) {
|
|
322
|
+
log('error', 'Origin rejected', { origin, allowedOrigin });
|
|
323
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
324
|
+
res.end(JSON.stringify({ error: 'Forbidden: invalid origin' }));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
} else if (origin) {
|
|
328
|
+
/* No allowlist configured but Origin header present — reject cross-origin requests */
|
|
329
|
+
log('error', 'Cross-origin request rejected (ALLOWED_ORIGIN not set)', { origin });
|
|
330
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
331
|
+
res.end(JSON.stringify({ error: 'Forbidden: ALLOWED_ORIGIN not configured' }));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* POST carries a JSON-RPC body: peek for server/discover + tasks/* and
|
|
336
|
+
* forward the parsed body to the transport. GET/DELETE (SSE stream /
|
|
337
|
+
* session teardown) pass straight through. */
|
|
338
|
+
if (method === 'POST') {
|
|
339
|
+
const raw = await readBody(req);
|
|
340
|
+
if (raw === null) {
|
|
341
|
+
rpcError(res, null, -32600, 'Request body too large');
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (raw.length === 0) {
|
|
345
|
+
rpcError(res, null, -32600, 'Empty request body');
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
let parsed: unknown;
|
|
349
|
+
try {
|
|
350
|
+
parsed = JSON.parse(raw.toString('utf-8'));
|
|
351
|
+
} catch {
|
|
352
|
+
rpcError(res, null, -32700, 'Parse error');
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* Intercept single-object JSON-RPC extension methods the SDK doesn't
|
|
357
|
+
* dispatch yet. Batches are forwarded untouched. */
|
|
358
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
359
|
+
const rpcMethod = (parsed as { method?: unknown }).method;
|
|
360
|
+
const rpcId = (parsed as { id?: unknown }).id;
|
|
361
|
+
if (rpcMethod === 'server/discover') {
|
|
362
|
+
sendJson(res, 200, buildDiscoverResult(rpcId));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
{{#if hasAsyncTools}}
|
|
366
|
+
if (typeof rpcMethod === 'string' && rpcMethod.startsWith('tasks/')) {
|
|
367
|
+
const handled = handleTaskRpc(rpcMethod, (parsed as { params?: unknown }).params, rpcId);
|
|
368
|
+
if (handled) {
|
|
369
|
+
sendJson(res, 200, handled);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
{{/if}}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
await dispatchToTransport(req, res, parsed);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
await dispatchToTransport(req, res);
|
|
381
|
+
} else if (url.pathname === '/health') {
|
|
382
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
383
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
384
|
+
} else if (url.pathname === '/ready') {
|
|
385
|
+
if (isReady) {
|
|
386
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
387
|
+
res.end(JSON.stringify({ status: 'ready' }));
|
|
388
|
+
} else {
|
|
389
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
390
|
+
res.end(JSON.stringify({ status: 'not ready' }));
|
|
391
|
+
}
|
|
392
|
+
} else {
|
|
393
|
+
res.writeHead(404);
|
|
394
|
+
res.end('Not found');
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Sessions are created lazily on `initialize` (stateful) or per request
|
|
399
|
+
// (stateless), so readiness gates on the listener being up in both modes.
|
|
400
|
+
httpServer.listen(port, () => {
|
|
401
|
+
isReady = true;
|
|
402
|
+
log('info', 'MCP server running', {
|
|
403
|
+
name: '{{serverName}}',
|
|
404
|
+
port,
|
|
405
|
+
endpoint: '/mcp',
|
|
406
|
+
mode: stateful ? 'stateful' : 'stateless',
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
/* ── Graceful shutdown ─────────────────────────────────────────────── */
|
|
411
|
+
|
|
412
|
+
process.on('SIGTERM', () => {
|
|
413
|
+
log('info', 'SIGTERM received, shutting down');
|
|
414
|
+
isReady = false;
|
|
415
|
+
|
|
416
|
+
httpServer.close(async () => {
|
|
417
|
+
try {
|
|
418
|
+
// Close every live session's server + transport.
|
|
419
|
+
for (const { server, transport } of sessions.values()) {
|
|
420
|
+
await transport.close().catch(() => {});
|
|
421
|
+
await server.close();
|
|
422
|
+
}
|
|
423
|
+
sessions.clear();
|
|
424
|
+
} catch (err) {
|
|
425
|
+
log('error', 'Error closing MCP server', { err: String(err) });
|
|
426
|
+
}
|
|
427
|
+
log('info', 'Server closed');
|
|
428
|
+
process.exit(0);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
/* Force exit after 5 seconds if in-flight requests don't complete */
|
|
432
|
+
setTimeout(() => {
|
|
433
|
+
log('error', 'Shutdown timed out after 5 s, forcing exit');
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}, 5000).unref();
|
|
436
|
+
});
|
|
437
|
+
} else {
|
|
438
|
+
const server = createMcpServer(config);
|
|
439
|
+
const transport = new StdioServerTransport();
|
|
440
|
+
await server.connect(transport);
|
|
441
|
+
log('info', 'MCP server running on stdio', { name: '{{serverName}}' });
|
|
442
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
{{#if hasStaticTools}}
|
|
4
|
+
import { registerAllTools } from './tools/index.js';
|
|
5
|
+
{{/if}}
|
|
6
|
+
{{#if hasDynamicDiscovery}}
|
|
7
|
+
import { registerDiscoveryTools } from './discovery.js';
|
|
8
|
+
{{/if}}
|
|
9
|
+
{{#if hasResources}}
|
|
10
|
+
import { registerAllResources } from './resources.js';
|
|
11
|
+
{{/if}}
|
|
12
|
+
{{#if hasPrompts}}
|
|
13
|
+
import { registerAllPrompts } from './prompts.js';
|
|
14
|
+
{{/if}}
|
|
15
|
+
import { loadConfig } from './config.js';
|
|
16
|
+
|
|
17
|
+
const config = loadConfig();
|
|
18
|
+
|
|
19
|
+
const server = new McpServer({
|
|
20
|
+
name: '{{serverName}}',
|
|
21
|
+
version: '{{serverVersion}}',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
{{#if hasStaticTools}}
|
|
25
|
+
registerAllTools(server, config);
|
|
26
|
+
{{/if}}
|
|
27
|
+
{{#if hasDynamicDiscovery}}
|
|
28
|
+
registerDiscoveryTools(server, config);
|
|
29
|
+
{{/if}}
|
|
30
|
+
{{#if hasResources}}
|
|
31
|
+
registerAllResources(server, config);
|
|
32
|
+
{{/if}}
|
|
33
|
+
{{#if hasPrompts}}
|
|
34
|
+
registerAllPrompts(server);
|
|
35
|
+
{{/if}}
|
|
36
|
+
|
|
37
|
+
const transport = new StdioServerTransport();
|
|
38
|
+
await server.connect(transport);
|
|
39
|
+
|
|
40
|
+
console.error('{{serverName}} MCP server running on stdio');
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP route handlers for task management.
|
|
3
|
+
* Provides REST endpoints for querying, waiting on, and cancelling async tasks.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
7
|
+
import {
|
|
8
|
+
getTask,
|
|
9
|
+
updateTask,
|
|
10
|
+
cancelTask,
|
|
11
|
+
listTasks,
|
|
12
|
+
getResult,
|
|
13
|
+
type Task,
|
|
14
|
+
type TaskStatus,
|
|
15
|
+
} from './task-manager.js';
|
|
16
|
+
|
|
17
|
+
const VALID_STATUSES = new Set<string>([
|
|
18
|
+
'working', 'input_required', 'completed', 'failed', 'cancelled',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/* ── MCP Tasks extension (2026-07-28) — JSON-RPC over /mcp ────────────────
|
|
22
|
+
* The 2026-07-28 RC moves Tasks to an extension driven by tasks/get,
|
|
23
|
+
* tasks/update, and tasks/cancel; tasks/list is removed (it can't be scoped
|
|
24
|
+
* without sessions). The current SDK doesn't dispatch these, so server-main
|
|
25
|
+
* peeks the JSON-RPC body and calls handleTaskRpc. Wire shapes are marked
|
|
26
|
+
* VERIFY — the RC is not final. The REST routes above are retained during the
|
|
27
|
+
* 12-month grace period. */
|
|
28
|
+
|
|
29
|
+
/** Serialize a task for the wire (VERIFY field names against the final spec). */
|
|
30
|
+
function taskToWire(task: Task): Record<string, unknown> {
|
|
31
|
+
return {
|
|
32
|
+
taskId: task.id,
|
|
33
|
+
status: task.status,
|
|
34
|
+
result: task.result,
|
|
35
|
+
error: task.error,
|
|
36
|
+
createdAt: task.createdAt,
|
|
37
|
+
updatedAt: task.updatedAt,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rpcResult(id: unknown, result: unknown): Record<string, unknown> {
|
|
42
|
+
return { jsonrpc: '2.0', id: id ?? null, result };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rpcError(id: unknown, code: number, message: string): Record<string, unknown> {
|
|
46
|
+
return { jsonrpc: '2.0', id: id ?? null, error: { code, message } };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Handle a `tasks/*` JSON-RPC method. Returns a JSON-RPC response object, or
|
|
51
|
+
* `null` if the method isn't a recognized task method (caller forwards it on).
|
|
52
|
+
*/
|
|
53
|
+
export function handleTaskRpc(
|
|
54
|
+
method: string,
|
|
55
|
+
params: unknown,
|
|
56
|
+
id: unknown,
|
|
57
|
+
): Record<string, unknown> | null {
|
|
58
|
+
const p = (params ?? {}) as { taskId?: unknown; status?: unknown; result?: unknown; error?: unknown };
|
|
59
|
+
const taskId = typeof p.taskId === 'string' ? p.taskId : '';
|
|
60
|
+
|
|
61
|
+
switch (method) {
|
|
62
|
+
case 'tasks/get': {
|
|
63
|
+
const task = getTask(taskId);
|
|
64
|
+
return task ? rpcResult(id, taskToWire(task)) : rpcError(id, -32001, `Task ${taskId} not found`);
|
|
65
|
+
}
|
|
66
|
+
case 'tasks/update': {
|
|
67
|
+
const status =
|
|
68
|
+
typeof p.status === 'string' && VALID_STATUSES.has(p.status)
|
|
69
|
+
? (p.status as TaskStatus)
|
|
70
|
+
: undefined;
|
|
71
|
+
const task = updateTask(taskId, {
|
|
72
|
+
status,
|
|
73
|
+
result: p.result,
|
|
74
|
+
error: typeof p.error === 'string' ? p.error : undefined,
|
|
75
|
+
});
|
|
76
|
+
return task ? rpcResult(id, taskToWire(task)) : rpcError(id, -32001, `Task ${taskId} not found`);
|
|
77
|
+
}
|
|
78
|
+
case 'tasks/cancel': {
|
|
79
|
+
const task = cancelTask(taskId);
|
|
80
|
+
return task ? rpcResult(id, taskToWire(task)) : rpcError(id, -32001, `Task ${taskId} not found`);
|
|
81
|
+
}
|
|
82
|
+
case 'tasks/list':
|
|
83
|
+
// Removed in the 2026-07-28 Tasks extension (can't be scoped without sessions).
|
|
84
|
+
return rpcError(id, -32601, 'tasks/list is not supported (removed in the Tasks extension)');
|
|
85
|
+
default:
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Route handler for task endpoints. Returns true if the request was handled.
|
|
92
|
+
*/
|
|
93
|
+
export function handleTaskRoutes(
|
|
94
|
+
req: IncomingMessage,
|
|
95
|
+
res: ServerResponse,
|
|
96
|
+
url: URL,
|
|
97
|
+
): boolean {
|
|
98
|
+
const method = req.method ?? 'GET';
|
|
99
|
+
const path = url.pathname;
|
|
100
|
+
|
|
101
|
+
// GET /tasks — list tasks
|
|
102
|
+
if (path === '/tasks' && method === 'GET') {
|
|
103
|
+
const statusParam = url.searchParams.get('status') ?? undefined;
|
|
104
|
+
const limitParam = url.searchParams.get('limit');
|
|
105
|
+
const limit = limitParam ? parseInt(limitParam, 10) : 50;
|
|
106
|
+
|
|
107
|
+
if (statusParam && !VALID_STATUSES.has(statusParam)) {
|
|
108
|
+
sendJson(res, 400, { error: `Invalid status filter: ${statusParam}` });
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const tasks = listTasks(statusParam as TaskStatus | undefined, limit);
|
|
113
|
+
sendJson(res, 200, { tasks });
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Match /tasks/:taskId routes
|
|
118
|
+
const taskIdMatch = path.match(/^\/tasks\/([0-9a-f-]{36})(\/.*)?$/);
|
|
119
|
+
if (!taskIdMatch) return false;
|
|
120
|
+
|
|
121
|
+
const taskId = taskIdMatch[1];
|
|
122
|
+
const subPath = taskIdMatch[2] ?? '';
|
|
123
|
+
|
|
124
|
+
// GET /tasks/:taskId — get task status
|
|
125
|
+
if (subPath === '' && method === 'GET') {
|
|
126
|
+
const task = getTask(taskId);
|
|
127
|
+
if (!task) {
|
|
128
|
+
sendJson(res, 404, { error: `Task ${taskId} not found` });
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
sendJson(res, 200, {
|
|
132
|
+
taskId: task.id,
|
|
133
|
+
status: task.status,
|
|
134
|
+
result: task.result,
|
|
135
|
+
error: task.error,
|
|
136
|
+
createdAt: task.createdAt,
|
|
137
|
+
updatedAt: task.updatedAt,
|
|
138
|
+
});
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// GET /tasks/:taskId/result — wait for task result
|
|
143
|
+
if (subPath === '/result' && method === 'GET') {
|
|
144
|
+
const timeoutParam = url.searchParams.get('timeout');
|
|
145
|
+
const timeoutMs = timeoutParam ? parseInt(timeoutParam, 10) : 30000;
|
|
146
|
+
|
|
147
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0 || timeoutMs > 300000) {
|
|
148
|
+
sendJson(res, 400, { error: 'timeout must be between 0 and 300000 ms' });
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
getResult(taskId, timeoutMs)
|
|
153
|
+
.then((task) => {
|
|
154
|
+
sendJson(res, 200, {
|
|
155
|
+
taskId: task.id,
|
|
156
|
+
status: task.status,
|
|
157
|
+
result: task.result,
|
|
158
|
+
error: task.error,
|
|
159
|
+
});
|
|
160
|
+
})
|
|
161
|
+
.catch((err: Error) => {
|
|
162
|
+
if (err.message === 'timeout') {
|
|
163
|
+
sendJson(res, 408, { error: 'Timed out waiting for task result' });
|
|
164
|
+
} else {
|
|
165
|
+
sendJson(res, 404, { error: err.message });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// POST /tasks/:taskId/cancel — cancel a task
|
|
173
|
+
if (subPath === '/cancel' && method === 'POST') {
|
|
174
|
+
const task = cancelTask(taskId);
|
|
175
|
+
if (!task) {
|
|
176
|
+
sendJson(res, 404, { error: `Task ${taskId} not found` });
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
sendJson(res, 200, { taskId: task.id, status: task.status });
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function sendJson(res: ServerResponse, status: number, body: unknown): void {
|
|
187
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
188
|
+
res.end(JSON.stringify(body));
|
|
189
|
+
}
|