@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,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime configuration, sourced from the Workers `env` binding (not the Node
|
|
3
|
+
* process environment). Same `AppConfig` shape the shared HTTP executor + auth
|
|
4
|
+
* modules consume, so those are reused verbatim from the Node templates.
|
|
5
|
+
*/
|
|
6
|
+
export interface AppConfig {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
{{#each authSchemes}}
|
|
9
|
+
{{#if (eq type "apiKey")}}
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
{{/if}}
|
|
12
|
+
{{#if (eq type "http-bearer")}}
|
|
13
|
+
bearerToken?: string;
|
|
14
|
+
{{/if}}
|
|
15
|
+
{{#if (eq type "http-basic")}}
|
|
16
|
+
basicUsername?: string;
|
|
17
|
+
basicPassword?: string;
|
|
18
|
+
{{/if}}
|
|
19
|
+
{{/each}}
|
|
20
|
+
maxRetries: number;
|
|
21
|
+
requestIntervalMs: number;
|
|
22
|
+
/** Extra headers added to every upstream request (non-breaking; empty by default). */
|
|
23
|
+
defaultHeaders: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type EnvLike = Record<string, string | undefined>;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the upstream base URL with optional named environments.
|
|
30
|
+
* `MCP_ENVIRONMENTS` is a JSON object mapping name → base URL; `API_ENVIRONMENT`
|
|
31
|
+
* selects one. Falls back to `BASE_URL` when unset or unknown.
|
|
32
|
+
*/
|
|
33
|
+
function resolveBaseUrl(env: EnvLike): string {
|
|
34
|
+
const raw = env.MCP_ENVIRONMENTS;
|
|
35
|
+
const selected = env.API_ENVIRONMENT;
|
|
36
|
+
if (raw && selected) {
|
|
37
|
+
try {
|
|
38
|
+
const envs = JSON.parse(raw) as Record<string, unknown>;
|
|
39
|
+
const url = envs[selected];
|
|
40
|
+
if (typeof url === 'string' && url) {
|
|
41
|
+
return url.replace(/\/$/, '');
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Malformed MCP_ENVIRONMENTS — fall through to BASE_URL.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return requireEnv(env, 'BASE_URL').replace(/\/$/, '');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Parse `MCP_DEFAULT_HEADERS` (JSON object of header → value) into a map. */
|
|
51
|
+
function loadDefaultHeaders(env: EnvLike): Record<string, string> {
|
|
52
|
+
const raw = env.MCP_DEFAULT_HEADERS;
|
|
53
|
+
if (!raw) return {};
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
56
|
+
const headers: Record<string, string> = {};
|
|
57
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
58
|
+
if (typeof value === 'string') headers[key] = value;
|
|
59
|
+
}
|
|
60
|
+
return headers;
|
|
61
|
+
} catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function loadConfig(env: EnvLike): AppConfig {
|
|
67
|
+
return {
|
|
68
|
+
baseUrl: resolveBaseUrl(env),
|
|
69
|
+
{{#each authSchemes}}
|
|
70
|
+
{{#if (eq type "apiKey")}}
|
|
71
|
+
apiKey: env.{{envVarName}},
|
|
72
|
+
{{/if}}
|
|
73
|
+
{{#if (eq type "http-bearer")}}
|
|
74
|
+
bearerToken: env.{{envVarName}},
|
|
75
|
+
{{/if}}
|
|
76
|
+
{{#if (eq type "http-basic")}}
|
|
77
|
+
basicUsername: env.BASIC_USERNAME,
|
|
78
|
+
basicPassword: env.BASIC_PASSWORD,
|
|
79
|
+
{{/if}}
|
|
80
|
+
{{/each}}
|
|
81
|
+
maxRetries: parseInt(env.MAX_RETRIES ?? '3', 10),
|
|
82
|
+
requestIntervalMs: parseInt(env.REQUEST_INTERVAL_MS ?? '100', 10),
|
|
83
|
+
defaultHeaders: loadDefaultHeaders(env),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function requireEnv(env: EnvLike, name: string): string {
|
|
88
|
+
const value = env[name];
|
|
89
|
+
if (!value) {
|
|
90
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
91
|
+
}
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Local dev secrets for `wrangler dev`. Copy to `.dev.vars` (gitignored) and fill in.
|
|
2
|
+
# In production these are set with `wrangler secret put <NAME>` instead.
|
|
3
|
+
|
|
4
|
+
# Bearer token clients must send as `Authorization: Bearer <token>`.
|
|
5
|
+
# Leave empty to disable auth (local/dev only — never deploy without it).
|
|
6
|
+
MCP_AUTH_TOKEN=
|
|
7
|
+
{{#each authEnvVars}}
|
|
8
|
+
# {{description}}
|
|
9
|
+
{{name}}=
|
|
10
|
+
{{/each}}
|
|
11
|
+
|
|
12
|
+
# Runtime tool filtering (optional, comma-separated tool names).
|
|
13
|
+
# MCP_TOOLS=
|
|
14
|
+
# MCP_EXCLUDE_TOOLS=
|
|
15
|
+
|
|
16
|
+
# Named environments (optional). JSON map of name → base URL; API_ENVIRONMENT
|
|
17
|
+
# selects one. Falls back to BASE_URL when unset.
|
|
18
|
+
{{#if environmentsJson}}
|
|
19
|
+
MCP_ENVIRONMENTS={{environmentsJson}}
|
|
20
|
+
API_ENVIRONMENT={{defaultEnvironment}}
|
|
21
|
+
{{else}}
|
|
22
|
+
# MCP_ENVIRONMENTS={"production":"{{baseUrl}}","sandbox":"https://sandbox.example.com"}
|
|
23
|
+
# API_ENVIRONMENT=production
|
|
24
|
+
{{/if}}
|
|
25
|
+
|
|
26
|
+
# Custom default headers added to every upstream request (optional, JSON object).
|
|
27
|
+
# MCP_DEFAULT_HEADERS={"X-Tenant-Id":"acme"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{serverName}}",
|
|
3
|
+
"version": "{{serverVersion}}",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"deploy": "wrangler deploy",
|
|
9
|
+
"start": "wrangler dev",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"test": "vitest run"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"zod": "^3.24.0",
|
|
15
|
+
"zod-to-json-schema": "^3.24.1"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@cloudflare/workers-types": "^4.20241127.0",
|
|
19
|
+
"@types/node": "^22.0.0",
|
|
20
|
+
"typescript": "^5.8.0",
|
|
21
|
+
"vitest": "^3.1.0",
|
|
22
|
+
"wrangler": "^3.90.0"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# {{serverName}}
|
|
2
|
+
|
|
3
|
+
An MCP server for **{{baseUrl}}**, generated by [mcpmake](https://mcpmake.dev) for
|
|
4
|
+
**Cloudflare Workers** (`--target cloudflare`).
|
|
5
|
+
|
|
6
|
+
It runs as a stateless [Fetch handler](https://developers.cloudflare.com/workers/runtime-apis/handlers/fetch/):
|
|
7
|
+
every `POST /mcp` carries one self-contained JSON-RPC message and gets one JSON
|
|
8
|
+
response — which maps cleanly onto the **2026-07-28** stateless transport (still a
|
|
9
|
+
release candidate), so no session state is kept and any request can hit any edge
|
|
10
|
+
location. Cloudflare's free plan covers 100,000 requests/day.
|
|
11
|
+
|
|
12
|
+
The `initialize` handshake negotiates to the current **stable** revision
|
|
13
|
+
(`2025-11-25`) or to the client's requested version when it is one this server
|
|
14
|
+
implements; it does not blindly echo arbitrary versions. This is a **tools-only**
|
|
15
|
+
server (no resources, prompts, Tasks, or OAuth) — use `--target node` if you need
|
|
16
|
+
those.
|
|
17
|
+
|
|
18
|
+
## Tools
|
|
19
|
+
|
|
20
|
+
This server exposes **{{tools.length}}** tool(s). Run `tools/list` to see them.
|
|
21
|
+
|
|
22
|
+
Every tool also accepts two optional per-call arguments: `jq_filter` (a jq-style
|
|
23
|
+
selector like `.data` or `.items[]` applied to the response to trim it) and
|
|
24
|
+
`idempotency_key` (sent as the `Idempotency-Key` header on mutating requests).
|
|
25
|
+
|
|
26
|
+
## Runtime configuration (optional)
|
|
27
|
+
|
|
28
|
+
Set these as Workers vars/secrets (none are required):
|
|
29
|
+
|
|
30
|
+
- `MCP_TOOLS` / `MCP_EXCLUDE_TOOLS` — comma-separated allow/deny list of tool
|
|
31
|
+
names to expose without redeploying.
|
|
32
|
+
- `MCP_ENVIRONMENTS` (JSON map of name → base URL) + `API_ENVIRONMENT` — select
|
|
33
|
+
an upstream environment by name; falls back to `BASE_URL`.
|
|
34
|
+
- `MCP_DEFAULT_HEADERS` — JSON object of headers added to every upstream request.
|
|
35
|
+
|
|
36
|
+
## Develop
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install
|
|
40
|
+
cp .dev.vars.example .dev.vars # fill in MCP_AUTH_TOKEN + API credentials
|
|
41
|
+
npm run dev # wrangler dev — local Workers runtime
|
|
42
|
+
npm run typecheck
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Deploy
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# One-time: store each secret in Cloudflare (never commit them).
|
|
49
|
+
npx wrangler secret put MCP_AUTH_TOKEN # bearer token clients must present
|
|
50
|
+
{{#each authEnvVars}}
|
|
51
|
+
npx wrangler secret put {{name}}
|
|
52
|
+
{{/each}}
|
|
53
|
+
|
|
54
|
+
npx wrangler deploy
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`BASE_URL` and HTTP tuning live in `wrangler.toml` under `[vars]`; secrets are
|
|
58
|
+
set with `wrangler secret put` and injected via the `env` binding at runtime.
|
|
59
|
+
|
|
60
|
+
## Connect a client
|
|
61
|
+
|
|
62
|
+
Point an MCP client at `https://<your-worker>.workers.dev/mcp` with the header
|
|
63
|
+
`Authorization: Bearer <MCP_AUTH_TOKEN>`. Health checks: `GET /health`, `GET /ready`.
|
|
64
|
+
|
|
65
|
+
## Notes & limits (Workers target)
|
|
66
|
+
|
|
67
|
+
- **Auth:** every path except `/health` and `/ready` requires the bearer token
|
|
68
|
+
(constant-time check) when `MCP_AUTH_TOKEN` is set.
|
|
69
|
+
- **Stateless only:** there is no SSE stream or `Mcp-Session-Id` session; `GET`/
|
|
70
|
+
`DELETE /mcp` are not used. Long-running tools resolve within the request (no
|
|
71
|
+
Tasks extension / background polling).
|
|
72
|
+
- Need MCP resources, prompts, the Tasks extension, or OAuth2 outbound auth?
|
|
73
|
+
Regenerate with `--target node` (the default) for the full feature set.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { tools } from '../src/tools/index.js';
|
|
3
|
+
|
|
4
|
+
describe('{{serverName}} worker tools', () => {
|
|
5
|
+
it('registers every tool with a usable definition', () => {
|
|
6
|
+
expect(tools.length).toBe({{tools.length}});
|
|
7
|
+
for (const { definition, handler } of tools) {
|
|
8
|
+
expect(definition.name).toMatch(/^[a-zA-Z0-9_-]+$/);
|
|
9
|
+
expect(typeof definition.description).toBe('string');
|
|
10
|
+
expect(typeof handler).toBe('function');
|
|
11
|
+
// The input schema must accept a safeParse call (used for tools/call validation).
|
|
12
|
+
expect(typeof definition.inputSchema.safeParse).toBe('function');
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('has unique tool names', () => {
|
|
17
|
+
const names = tools.map((t) => t.definition.name);
|
|
18
|
+
expect(new Set(names).size).toBe(names.length);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { executeRequest } from '../http.js';
|
|
3
|
+
import { applyJqFilter } from '../response-filter.js';
|
|
4
|
+
import type { AppConfig } from '../config.js';
|
|
5
|
+
|
|
6
|
+
// Per-call control arguments shared by every generated tool. Optional and
|
|
7
|
+
// stripped before the upstream request, so omitting them is a no-op.
|
|
8
|
+
const controlSchema = {
|
|
9
|
+
jq_filter: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe(
|
|
13
|
+
'Optional jq-style filter applied to the JSON response before it is returned ' +
|
|
14
|
+
'(e.g. ".data", ".items[0].id", ".items[]"). Trims large responses.',
|
|
15
|
+
),
|
|
16
|
+
idempotency_key: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe(
|
|
20
|
+
'Optional Idempotency-Key header for safe retries of mutating requests. ' +
|
|
21
|
+
'Ignored for read-only operations.',
|
|
22
|
+
),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const inputSchema = ({{{inputSchemaCode}}}).extend(controlSchema);
|
|
26
|
+
|
|
27
|
+
export const definition = {
|
|
28
|
+
name: '{{name}}',
|
|
29
|
+
title: '{{title}}',
|
|
30
|
+
description: `{{{description}}}`,
|
|
31
|
+
inputSchema,
|
|
32
|
+
{{#if annotations}}
|
|
33
|
+
annotations: {
|
|
34
|
+
{{#if annotations.readOnlyHint}}
|
|
35
|
+
readOnlyHint: true,
|
|
36
|
+
{{/if}}
|
|
37
|
+
{{#if annotations.destructiveHint}}
|
|
38
|
+
destructiveHint: true,
|
|
39
|
+
{{/if}}
|
|
40
|
+
{{#if annotations.idempotentHint}}
|
|
41
|
+
idempotentHint: true,
|
|
42
|
+
{{/if}}
|
|
43
|
+
} as Record<string, boolean>,
|
|
44
|
+
{{/if}}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface ToolResult {
|
|
48
|
+
content: { type: 'text'; text: string }[];
|
|
49
|
+
isError?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Execute this tool. On the Workers (stateless) target a tool always runs to
|
|
54
|
+
* completion within the request — there is no Tasks extension / background
|
|
55
|
+
* polling, so async operations resolve synchronously here.
|
|
56
|
+
*/
|
|
57
|
+
export async function handler(
|
|
58
|
+
input: Record<string, unknown>,
|
|
59
|
+
config: AppConfig,
|
|
60
|
+
): Promise<ToolResult> {
|
|
61
|
+
try {
|
|
62
|
+
const result = await runRequest(input, config);
|
|
63
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function runRequest(rawInput: Record<string, unknown>, config: AppConfig): Promise<unknown> {
|
|
71
|
+
// Pull off the per-call control args so they are never forwarded upstream.
|
|
72
|
+
const { jq_filter: jqFilter, idempotency_key: idempotencyKey, ...input } = rawInput;
|
|
73
|
+
const url = buildUrl(input, config.baseUrl);
|
|
74
|
+
|
|
75
|
+
const response = await executeRequest({
|
|
76
|
+
method: '{{method}}',
|
|
77
|
+
url,
|
|
78
|
+
{{#if hasRequestBody}}
|
|
79
|
+
body: input.body,
|
|
80
|
+
contentType: '{{requestBodyContentType}}',
|
|
81
|
+
{{/if}}
|
|
82
|
+
headers: {},
|
|
83
|
+
config,
|
|
84
|
+
idempotencyKey: typeof idempotencyKey === 'string' ? idempotencyKey : undefined,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
let result: unknown = response.data;
|
|
88
|
+
{{#if jqFilter}}
|
|
89
|
+
// Build-time x-mcp-jq-filter (applied first when no per-call filter is given).
|
|
90
|
+
if (typeof jqFilter !== 'string') {
|
|
91
|
+
result = applyJqFilter(result, '{{{jqFilter}}}');
|
|
92
|
+
}
|
|
93
|
+
{{/if}}
|
|
94
|
+
// Per-call jq_filter takes precedence (absent → unchanged).
|
|
95
|
+
if (typeof jqFilter === 'string' && jqFilter) {
|
|
96
|
+
result = applyJqFilter(result, jqFilter);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildUrl(params: Record<string, unknown>, baseUrl: string): string {
|
|
102
|
+
{{{buildUrlBody}}}
|
|
103
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { AppConfig } from '../config.js';
|
|
3
|
+
{{#each tools}}
|
|
4
|
+
import {
|
|
5
|
+
definition as def{{capitalize functionName}},
|
|
6
|
+
handler as handler{{capitalize functionName}},
|
|
7
|
+
} from './{{fileName}}.js';
|
|
8
|
+
{{/each}}
|
|
9
|
+
|
|
10
|
+
export interface ToolEntry {
|
|
11
|
+
definition: {
|
|
12
|
+
name: string;
|
|
13
|
+
title: string;
|
|
14
|
+
description: string;
|
|
15
|
+
inputSchema: z.ZodTypeAny;
|
|
16
|
+
annotations?: Record<string, boolean>;
|
|
17
|
+
};
|
|
18
|
+
handler: (
|
|
19
|
+
input: Record<string, unknown>,
|
|
20
|
+
config: AppConfig,
|
|
21
|
+
) => Promise<{ content: { type: 'text'; text: string }[]; isError?: boolean }>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const tools: ToolEntry[] = [
|
|
25
|
+
{{#each tools}}
|
|
26
|
+
{ definition: def{{capitalize functionName}}, handler: handler{{capitalize functionName}} },
|
|
27
|
+
{{/each}}
|
|
28
|
+
];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"types": ["@cloudflare/workers-types", "node"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"noEmit": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts", "test/**/*.ts"],
|
|
16
|
+
"exclude": ["node_modules"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Workers entry — stateless MCP server (Fetch handler).
|
|
3
|
+
*
|
|
4
|
+
* The MCP 2026-07-28 transport is stateless (no Mcp-Session-Id, no persistent
|
|
5
|
+
* connection): each `POST /mcp` carries one self-contained JSON-RPC message (or
|
|
6
|
+
* a batch) and receives one JSON response. That maps cleanly onto the Workers
|
|
7
|
+
* request/response model, so we dispatch JSON-RPC by hand here instead of using
|
|
8
|
+
* the SDK's StreamableHTTPServerTransport, which is bound to node:http req/res
|
|
9
|
+
* objects and cannot run on Workers.
|
|
10
|
+
*
|
|
11
|
+
* Secrets (API credentials + the MCP bearer token) arrive via the `env` binding,
|
|
12
|
+
* not process.env — set them with `wrangler secret put <NAME>`.
|
|
13
|
+
*/
|
|
14
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
15
|
+
import { loadConfig, type AppConfig } from './config.js';
|
|
16
|
+
import { tools, type ToolEntry } from './tools/index.js';
|
|
17
|
+
import { deriveContext, runWithTrace } from './trace.js';
|
|
18
|
+
|
|
19
|
+
export interface Env {
|
|
20
|
+
[key: string]: string | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// initialize handshake. We answer with our preferred stable revision, or the
|
|
24
|
+
// client's requested version when it is one we actually implement — we never
|
|
25
|
+
// blindly echo an arbitrary requested string (that would claim compatibility
|
|
26
|
+
// this tools-only Worker has not implemented). The basic initialize/tools flow
|
|
27
|
+
// is unchanged across these revisions, so all are genuinely supported.
|
|
28
|
+
const PROTOCOL_VERSION = '2025-11-25'; // preferred (current stable)
|
|
29
|
+
const SUPPORTED_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26'];
|
|
30
|
+
const PROTOCOL_REVISION = '2026-07-28'; // server/discover preflight only (SEP-2575); NOT the negotiated initialize version
|
|
31
|
+
const SERVER_INFO = { name: '{{serverName}}', version: '{{serverVersion}}' } as const;
|
|
32
|
+
const MAX_BODY = 4 * 1024 * 1024; // 4 MB
|
|
33
|
+
|
|
34
|
+
const toolByName = new Map<string, ToolEntry>(tools.map((t) => [t.definition.name, t]));
|
|
35
|
+
|
|
36
|
+
/* ── Runtime tool filtering (additive) ───────────────────────────────────
|
|
37
|
+
* Two optional env vars narrow which tools are exposed without rebuilding:
|
|
38
|
+
* MCP_TOOLS comma-separated allowlist (only these names)
|
|
39
|
+
* MCP_EXCLUDE_TOOLS comma-separated denylist (applied after the allowlist)
|
|
40
|
+
* When neither is set every tool is exposed exactly as before. */
|
|
41
|
+
function parseList(raw: string | undefined): Set<string> | undefined {
|
|
42
|
+
if (!raw) return undefined;
|
|
43
|
+
const names = raw
|
|
44
|
+
.split(',')
|
|
45
|
+
.map((s) => s.trim())
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
return names.length > 0 ? new Set(names) : undefined;
|
|
48
|
+
}
|
|
49
|
+
function toolEnabled(name: string, env: Env): boolean {
|
|
50
|
+
const allow = parseList(env.MCP_TOOLS);
|
|
51
|
+
const deny = parseList(env.MCP_EXCLUDE_TOOLS);
|
|
52
|
+
if (allow && !allow.has(name)) return false;
|
|
53
|
+
if (deny && deny.has(name)) return false;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
function enabledTools(env: Env): ToolEntry[] {
|
|
57
|
+
return tools.filter((t) => toolEnabled(t.definition.name, env));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ── Constant-time bearer check ──────────────────────────────────────────
|
|
61
|
+
* HMACs both tokens to fixed-length digests with a random per-isolate key,
|
|
62
|
+
* then compares the bytes — constant time and independent of token length, so
|
|
63
|
+
* neither equality nor length leaks through timing. The key is created lazily
|
|
64
|
+
* on first use: Workers forbids generating random values in global scope, so it
|
|
65
|
+
* must be minted inside a request handler. */
|
|
66
|
+
let cmpKeyPromise: Promise<CryptoKey> | null = null;
|
|
67
|
+
function compareKey(): Promise<CryptoKey> {
|
|
68
|
+
if (!cmpKeyPromise) {
|
|
69
|
+
cmpKeyPromise = crypto.subtle.importKey(
|
|
70
|
+
'raw',
|
|
71
|
+
crypto.getRandomValues(new Uint8Array(32)),
|
|
72
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
73
|
+
false,
|
|
74
|
+
['sign'],
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return cmpKeyPromise;
|
|
78
|
+
}
|
|
79
|
+
async function safeEqual(a: string, b: string): Promise<boolean> {
|
|
80
|
+
const key = await compareKey();
|
|
81
|
+
const enc = new TextEncoder();
|
|
82
|
+
const da = new Uint8Array(await crypto.subtle.sign('HMAC', key, enc.encode(a)));
|
|
83
|
+
const db = new Uint8Array(await crypto.subtle.sign('HMAC', key, enc.encode(b)));
|
|
84
|
+
let diff = 0;
|
|
85
|
+
for (let i = 0; i < da.length; i++) diff |= da[i] ^ db[i];
|
|
86
|
+
return diff === 0;
|
|
87
|
+
}
|
|
88
|
+
async function isAuthorized(request: Request, expected: string | undefined): Promise<boolean> {
|
|
89
|
+
if (!expected) return true; // auth disabled when MCP_AUTH_TOKEN is unset (local/dev only)
|
|
90
|
+
const header = request.headers.get('authorization');
|
|
91
|
+
if (!header || !header.startsWith('Bearer ')) return false;
|
|
92
|
+
const presented = header.slice(7).trim();
|
|
93
|
+
if (!presented) return false;
|
|
94
|
+
return safeEqual(presented, expected);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ── HTTP / CORS helpers ─────────────────────────────────────────────── */
|
|
98
|
+
function corsHeaders(origin?: string | null): Record<string, string> {
|
|
99
|
+
return {
|
|
100
|
+
'access-control-allow-origin': origin || '*',
|
|
101
|
+
'access-control-allow-methods': 'POST, GET, OPTIONS',
|
|
102
|
+
'access-control-allow-headers':
|
|
103
|
+
'authorization, content-type, mcp-method, mcp-name, mcp-protocol-version, traceparent, tracestate',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function json(body: unknown, status = 200, extra?: Record<string, string>): Response {
|
|
107
|
+
return new Response(JSON.stringify(body), {
|
|
108
|
+
status,
|
|
109
|
+
headers: { 'content-type': 'application/json', ...corsHeaders(), ...extra },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function ok(id: unknown, result: unknown): object {
|
|
113
|
+
return { jsonrpc: '2.0', id: id ?? null, result };
|
|
114
|
+
}
|
|
115
|
+
function err(id: unknown, code: number, message: string): object {
|
|
116
|
+
return { jsonrpc: '2.0', id: id ?? null, error: { code, message } };
|
|
117
|
+
}
|
|
118
|
+
function rpcErrorResponse(id: unknown, code: number, message: string): Response {
|
|
119
|
+
return json(err(id, code, message));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* ── server/discover (SEP-2575) — VERIFY result shape against the final spec ── */
|
|
123
|
+
function discoverResult(env: Env): Record<string, unknown> {
|
|
124
|
+
return {
|
|
125
|
+
server: SERVER_INFO,
|
|
126
|
+
protocolRevision: PROTOCOL_REVISION,
|
|
127
|
+
capabilities: { tools: { count: enabledTools(env).length } },
|
|
128
|
+
extensions: [],
|
|
129
|
+
cacheTtlSeconds: 300,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A tool's `tools/list` entry: its input schema rendered as JSON Schema. */
|
|
134
|
+
function toListEntry(t: ToolEntry): Record<string, unknown> {
|
|
135
|
+
const schema = zodToJsonSchema(t.definition.inputSchema, {
|
|
136
|
+
$refStrategy: 'none',
|
|
137
|
+
}) as Record<string, unknown>;
|
|
138
|
+
delete schema.$schema;
|
|
139
|
+
return {
|
|
140
|
+
name: t.definition.name,
|
|
141
|
+
title: t.definition.title,
|
|
142
|
+
description: t.definition.description,
|
|
143
|
+
inputSchema: schema,
|
|
144
|
+
...(t.definition.annotations ? { annotations: t.definition.annotations } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function handleToolCall(
|
|
149
|
+
id: unknown,
|
|
150
|
+
params: { name?: unknown; arguments?: unknown } | undefined,
|
|
151
|
+
config: AppConfig,
|
|
152
|
+
env: Env,
|
|
153
|
+
): Promise<object> {
|
|
154
|
+
const name = typeof params?.name === 'string' ? params.name : '';
|
|
155
|
+
const entry = toolByName.get(name);
|
|
156
|
+
if (!entry || !toolEnabled(name, env)) return err(id, -32602, `Unknown tool: ${name}`);
|
|
157
|
+
const parsed = entry.definition.inputSchema.safeParse(params?.arguments ?? {});
|
|
158
|
+
if (!parsed.success) {
|
|
159
|
+
return err(id, -32602, `Invalid arguments for ${name}: ${parsed.error.message}`);
|
|
160
|
+
}
|
|
161
|
+
const result = await entry.handler(parsed.data as Record<string, unknown>, config);
|
|
162
|
+
return ok(id, result);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Dispatch one JSON-RPC message. Returns null for notifications (no reply). */
|
|
166
|
+
async function handleRpc(
|
|
167
|
+
msg: { id?: unknown; method?: unknown; params?: unknown } | null,
|
|
168
|
+
config: AppConfig,
|
|
169
|
+
env: Env,
|
|
170
|
+
): Promise<object | null> {
|
|
171
|
+
const id = msg?.id;
|
|
172
|
+
const method = msg?.method;
|
|
173
|
+
switch (method) {
|
|
174
|
+
case 'initialize': {
|
|
175
|
+
// Negotiate, don't echo: honor the client's requested version only when
|
|
176
|
+
// we implement it; otherwise answer with our preferred version so the
|
|
177
|
+
// client can decide whether to proceed.
|
|
178
|
+
const requested = (msg?.params as { protocolVersion?: unknown })?.protocolVersion;
|
|
179
|
+
const negotiated =
|
|
180
|
+
typeof requested === 'string' && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
|
|
181
|
+
? requested
|
|
182
|
+
: PROTOCOL_VERSION;
|
|
183
|
+
return ok(id, {
|
|
184
|
+
protocolVersion: negotiated,
|
|
185
|
+
capabilities: { tools: { listChanged: false } },
|
|
186
|
+
serverInfo: SERVER_INFO,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
case 'ping':
|
|
190
|
+
return ok(id, {});
|
|
191
|
+
case 'tools/list':
|
|
192
|
+
return ok(id, { tools: enabledTools(env).map(toListEntry) });
|
|
193
|
+
case 'tools/call':
|
|
194
|
+
return handleToolCall(
|
|
195
|
+
id,
|
|
196
|
+
msg?.params as { name?: unknown; arguments?: unknown },
|
|
197
|
+
config,
|
|
198
|
+
env,
|
|
199
|
+
);
|
|
200
|
+
case 'server/discover':
|
|
201
|
+
return ok(id, discoverResult(env));
|
|
202
|
+
default:
|
|
203
|
+
// Notifications (e.g. notifications/initialized) carry no id and get no reply.
|
|
204
|
+
if (id === undefined || id === null) return null;
|
|
205
|
+
return err(id, -32601, `Method not found: ${String(method)}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export default {
|
|
210
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
211
|
+
const url = new URL(request.url);
|
|
212
|
+
const origin = request.headers.get('origin');
|
|
213
|
+
|
|
214
|
+
if (request.method === 'OPTIONS') {
|
|
215
|
+
return new Response(null, { status: 204, headers: corsHeaders(origin) });
|
|
216
|
+
}
|
|
217
|
+
if (url.pathname === '/health') return json({ status: 'ok' });
|
|
218
|
+
if (url.pathname === '/ready') return json({ status: 'ready' });
|
|
219
|
+
|
|
220
|
+
// Bearer authentication (every path except /health and /ready).
|
|
221
|
+
if (!(await isAuthorized(request, env.MCP_AUTH_TOKEN))) {
|
|
222
|
+
return json({ error: 'Unauthorized: missing or invalid bearer token' }, 401, {
|
|
223
|
+
'www-authenticate': 'Bearer',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Routing-header observability (SEP-2243, additive — never rejects).
|
|
228
|
+
const mcpMethod = request.headers.get('mcp-method');
|
|
229
|
+
const mcpName = request.headers.get('mcp-name');
|
|
230
|
+
if (mcpMethod || mcpName) {
|
|
231
|
+
console.log(JSON.stringify({ level: 'info', msg: 'mcp-meta-headers', mcpMethod, mcpName }));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (url.pathname !== '/mcp') return json({ error: 'Not found' }, 404);
|
|
235
|
+
if (request.method !== 'POST') {
|
|
236
|
+
// Stateless: no SSE stream to open (GET) or session to delete (DELETE).
|
|
237
|
+
return rpcErrorResponse(null, -32601, 'Only POST is supported on this stateless server');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Origin guard (DNS-rebinding protection). Enforced only when configured —
|
|
241
|
+
// a Workers endpoint is public and gated by the bearer token, not by origin.
|
|
242
|
+
const allowedOrigin = env.ALLOWED_ORIGIN;
|
|
243
|
+
if (allowedOrigin && origin && origin !== allowedOrigin) {
|
|
244
|
+
return json({ error: 'Forbidden: invalid origin' }, 403);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const contentLength = Number(request.headers.get('content-length') ?? '0');
|
|
248
|
+
if (contentLength > MAX_BODY) return rpcErrorResponse(null, -32600, 'Request body too large');
|
|
249
|
+
const raw = await request.text();
|
|
250
|
+
if (raw.length > MAX_BODY) return rpcErrorResponse(null, -32600, 'Request body too large');
|
|
251
|
+
if (!raw) return rpcErrorResponse(null, -32600, 'Empty request body');
|
|
252
|
+
|
|
253
|
+
let parsed: unknown;
|
|
254
|
+
try {
|
|
255
|
+
parsed = JSON.parse(raw);
|
|
256
|
+
} catch {
|
|
257
|
+
return rpcErrorResponse(null, -32700, 'Parse error');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const config = loadConfig(env);
|
|
261
|
+
// Bind a W3C Trace Context for this request so upstream API calls made inside
|
|
262
|
+
// the tool handlers carry the traceparent and the trace spans the API hop.
|
|
263
|
+
const trace = deriveContext(
|
|
264
|
+
request.headers.get('traceparent') ?? undefined,
|
|
265
|
+
request.headers.get('tracestate') ?? undefined,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
return runWithTrace(trace, async () => {
|
|
269
|
+
if (Array.isArray(parsed)) {
|
|
270
|
+
const settled = await Promise.all(
|
|
271
|
+
parsed.map((m) => handleRpc(m as { id?: unknown; method?: unknown }, config, env)),
|
|
272
|
+
);
|
|
273
|
+
const responses = settled.filter((r): r is object => r !== null);
|
|
274
|
+
if (responses.length === 0) {
|
|
275
|
+
return new Response(null, { status: 202, headers: corsHeaders(origin) });
|
|
276
|
+
}
|
|
277
|
+
return json(responses);
|
|
278
|
+
}
|
|
279
|
+
const response = await handleRpc(parsed as { id?: unknown; method?: unknown }, config, env);
|
|
280
|
+
if (response === null) {
|
|
281
|
+
return new Response(null, { status: 202, headers: corsHeaders(origin) });
|
|
282
|
+
}
|
|
283
|
+
return json(response);
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
};
|