@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,110 @@
|
|
|
1
|
+
export interface AppConfig {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
{{#each authSchemes}}
|
|
4
|
+
{{#if (eq type "apiKey")}}
|
|
5
|
+
apiKey?: string;
|
|
6
|
+
{{/if}}
|
|
7
|
+
{{#if (eq type "http-bearer")}}
|
|
8
|
+
bearerToken?: string;
|
|
9
|
+
{{/if}}
|
|
10
|
+
{{#if (eq type "http-basic")}}
|
|
11
|
+
basicUsername?: string;
|
|
12
|
+
basicPassword?: string;
|
|
13
|
+
{{/if}}
|
|
14
|
+
{{#if (eq type "oauth2")}}
|
|
15
|
+
oauth2Token?: string;
|
|
16
|
+
oauth2ClientId?: string;
|
|
17
|
+
oauth2ClientSecret?: string;
|
|
18
|
+
oauth2AuthorizationUrl?: string;
|
|
19
|
+
oauth2TokenUrl?: string;
|
|
20
|
+
oauth2RedirectUri?: string;
|
|
21
|
+
{{/if}}
|
|
22
|
+
{{/each}}
|
|
23
|
+
maxRetries: number;
|
|
24
|
+
requestIntervalMs: number;
|
|
25
|
+
/** Extra headers added to every upstream request (non-breaking; empty by default). */
|
|
26
|
+
defaultHeaders: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the upstream base URL with optional named environments.
|
|
31
|
+
*
|
|
32
|
+
* `MCP_ENVIRONMENTS` is a JSON object mapping an environment name to a base URL,
|
|
33
|
+
* e.g. `{"production":"https://api.example.com","sandbox":"https://sandbox.example.com"}`.
|
|
34
|
+
* `API_ENVIRONMENT` selects one of those names. When either is unset (or the
|
|
35
|
+
* name is unknown) the resolver falls back to `BASE_URL`, so existing
|
|
36
|
+
* deployments are unaffected.
|
|
37
|
+
*/
|
|
38
|
+
function resolveBaseUrl(): string {
|
|
39
|
+
const raw = process.env.MCP_ENVIRONMENTS;
|
|
40
|
+
const selected = process.env.API_ENVIRONMENT;
|
|
41
|
+
if (raw && selected) {
|
|
42
|
+
try {
|
|
43
|
+
const envs = JSON.parse(raw) as Record<string, unknown>;
|
|
44
|
+
const url = envs[selected];
|
|
45
|
+
if (typeof url === 'string' && url) {
|
|
46
|
+
return url.replace(/\/$/, '');
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// Malformed MCP_ENVIRONMENTS — fall through to BASE_URL.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return requireEnv('BASE_URL').replace(/\/$/, '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse `MCP_DEFAULT_HEADERS` (a JSON object of header name → value) into a map
|
|
57
|
+
* applied to every upstream request. Invalid JSON or non-string values are
|
|
58
|
+
* ignored so a bad value never breaks the server.
|
|
59
|
+
*/
|
|
60
|
+
function loadDefaultHeaders(): Record<string, string> {
|
|
61
|
+
const raw = process.env.MCP_DEFAULT_HEADERS;
|
|
62
|
+
if (!raw) return {};
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
|
65
|
+
const headers: Record<string, string> = {};
|
|
66
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
67
|
+
if (typeof value === 'string') headers[key] = value;
|
|
68
|
+
}
|
|
69
|
+
return headers;
|
|
70
|
+
} catch {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function loadConfig(): AppConfig {
|
|
76
|
+
return {
|
|
77
|
+
baseUrl: resolveBaseUrl(),
|
|
78
|
+
{{#each authSchemes}}
|
|
79
|
+
{{#if (eq type "apiKey")}}
|
|
80
|
+
apiKey: process.env.{{envVarName}},
|
|
81
|
+
{{/if}}
|
|
82
|
+
{{#if (eq type "http-bearer")}}
|
|
83
|
+
bearerToken: process.env.{{envVarName}},
|
|
84
|
+
{{/if}}
|
|
85
|
+
{{#if (eq type "http-basic")}}
|
|
86
|
+
basicUsername: process.env.BASIC_USERNAME,
|
|
87
|
+
basicPassword: process.env.BASIC_PASSWORD,
|
|
88
|
+
{{/if}}
|
|
89
|
+
{{#if (eq type "oauth2")}}
|
|
90
|
+
oauth2Token: process.env.OAUTH2_TOKEN,
|
|
91
|
+
oauth2ClientId: process.env.OAUTH2_CLIENT_ID,
|
|
92
|
+
oauth2ClientSecret: process.env.OAUTH2_CLIENT_SECRET,
|
|
93
|
+
oauth2AuthorizationUrl: process.env.OAUTH2_AUTHORIZATION_URL,
|
|
94
|
+
oauth2TokenUrl: process.env.OAUTH2_TOKEN_URL,
|
|
95
|
+
oauth2RedirectUri: process.env.OAUTH2_REDIRECT_URI,
|
|
96
|
+
{{/if}}
|
|
97
|
+
{{/each}}
|
|
98
|
+
maxRetries: parseInt(process.env.MAX_RETRIES ?? '3', 10),
|
|
99
|
+
requestIntervalMs: parseInt(process.env.REQUEST_INTERVAL_MS ?? '100', 10),
|
|
100
|
+
defaultHeaders: loadDefaultHeaders(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function requireEnv(name: string): string {
|
|
105
|
+
const value = process.env[name];
|
|
106
|
+
if (!value) {
|
|
107
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic tool discovery — 4 meta-tools that let LLMs discover and call tools
|
|
3
|
+
* without all tool schemas loaded into context upfront.
|
|
4
|
+
*
|
|
5
|
+
* Reduces initial context from ~25-40K tokens (for large APIs) to ~1,300 tokens.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
import { executeRequest } from './http.js';
|
|
11
|
+
import { applyJqFilter } from './response-filter.js';
|
|
12
|
+
import type { AppConfig } from './config.js';
|
|
13
|
+
import catalogData from './tool-catalog.json' with { type: 'json' };
|
|
14
|
+
|
|
15
|
+
interface CatalogEntry {
|
|
16
|
+
name: string;
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
method: string;
|
|
20
|
+
path: string;
|
|
21
|
+
inputSchema: Record<string, unknown>;
|
|
22
|
+
pathParams: string[];
|
|
23
|
+
queryParams: string[];
|
|
24
|
+
hasRequestBody: boolean;
|
|
25
|
+
requestBodyContentType: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const catalog: CatalogEntry[] = catalogData as CatalogEntry[];
|
|
29
|
+
|
|
30
|
+
// Simple TF-IDF search index built at startup
|
|
31
|
+
const searchIndex = buildSearchIndex(catalog);
|
|
32
|
+
|
|
33
|
+
export function registerDiscoveryTools(server: McpServer, config: AppConfig): void {
|
|
34
|
+
// 1. list_tools — list available tools with optional query filter
|
|
35
|
+
server.registerTool(
|
|
36
|
+
'list_tools',
|
|
37
|
+
{
|
|
38
|
+
title: 'List Available Tools',
|
|
39
|
+
description: `List all ${catalog.length} available API tools. Use "query" to filter by name or description.`,
|
|
40
|
+
inputSchema: z.object({
|
|
41
|
+
query: z.string().optional().describe('Filter tools by name or description'),
|
|
42
|
+
offset: z.number().int().optional().describe('Pagination offset (default: 0)'),
|
|
43
|
+
limit: z.number().int().optional().describe('Max results (default: 20)'),
|
|
44
|
+
}),
|
|
45
|
+
annotations: { readOnlyHint: true },
|
|
46
|
+
},
|
|
47
|
+
async (input) => {
|
|
48
|
+
const query = (input as Record<string, unknown>).query as string | undefined;
|
|
49
|
+
const offset = ((input as Record<string, unknown>).offset as number) ?? 0;
|
|
50
|
+
const limit = ((input as Record<string, unknown>).limit as number) ?? 20;
|
|
51
|
+
|
|
52
|
+
let results = catalog;
|
|
53
|
+
if (query) {
|
|
54
|
+
results = searchTools(query);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const page = results.slice(offset, offset + limit);
|
|
58
|
+
const hasMore = offset + limit < results.length;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: 'text' as const,
|
|
64
|
+
text: JSON.stringify(
|
|
65
|
+
{
|
|
66
|
+
tools: page.map((t) => ({ name: t.name, description: t.description })),
|
|
67
|
+
total: results.length,
|
|
68
|
+
hasMore,
|
|
69
|
+
nextOffset: hasMore ? offset + limit : undefined,
|
|
70
|
+
},
|
|
71
|
+
null,
|
|
72
|
+
2,
|
|
73
|
+
),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// 2. get_tool_schema — get the full input schema for a specific tool
|
|
81
|
+
server.registerTool(
|
|
82
|
+
'get_tool_schema',
|
|
83
|
+
{
|
|
84
|
+
title: 'Get Tool Schema',
|
|
85
|
+
description: 'Get the full input schema and details for a specific tool by name.',
|
|
86
|
+
inputSchema: z.object({
|
|
87
|
+
name: z.string().describe('Tool name (from list_tools)'),
|
|
88
|
+
}),
|
|
89
|
+
annotations: { readOnlyHint: true },
|
|
90
|
+
},
|
|
91
|
+
async (input) => {
|
|
92
|
+
const name = (input as Record<string, unknown>).name as string;
|
|
93
|
+
const tool = catalog.find((t) => t.name === name);
|
|
94
|
+
|
|
95
|
+
if (!tool) {
|
|
96
|
+
return {
|
|
97
|
+
content: [{ type: 'text' as const, text: `Tool "${name}" not found.` }],
|
|
98
|
+
isError: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
content: [
|
|
104
|
+
{
|
|
105
|
+
type: 'text' as const,
|
|
106
|
+
text: JSON.stringify(
|
|
107
|
+
{
|
|
108
|
+
name: tool.name,
|
|
109
|
+
title: tool.title,
|
|
110
|
+
description: tool.description,
|
|
111
|
+
method: tool.method,
|
|
112
|
+
path: tool.path,
|
|
113
|
+
inputSchema: tool.inputSchema,
|
|
114
|
+
},
|
|
115
|
+
null,
|
|
116
|
+
2,
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// 3. execute_tool — execute a tool by name with arguments
|
|
125
|
+
server.registerTool(
|
|
126
|
+
'execute_tool',
|
|
127
|
+
{
|
|
128
|
+
title: 'Execute Tool',
|
|
129
|
+
description: 'Execute any API tool by name. Get the schema first with get_tool_schema.',
|
|
130
|
+
inputSchema: z.object({
|
|
131
|
+
name: z.string().describe('Tool name'),
|
|
132
|
+
args: z.record(z.unknown()).optional().describe('Tool arguments (match the input schema)'),
|
|
133
|
+
jq_filter: z
|
|
134
|
+
.string()
|
|
135
|
+
.optional()
|
|
136
|
+
.describe('Optional jq-style filter applied to the response (e.g. ".data", ".items[]").'),
|
|
137
|
+
idempotency_key: z
|
|
138
|
+
.string()
|
|
139
|
+
.optional()
|
|
140
|
+
.describe('Optional Idempotency-Key header for safe retries of mutating requests.'),
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
async (input) => {
|
|
144
|
+
const name = (input as Record<string, unknown>).name as string;
|
|
145
|
+
const args = ((input as Record<string, unknown>).args as Record<string, unknown>) ?? {};
|
|
146
|
+
const jqFilter = (input as Record<string, unknown>).jq_filter as string | undefined;
|
|
147
|
+
const idempotencyKey = (input as Record<string, unknown>).idempotency_key as
|
|
148
|
+
| string
|
|
149
|
+
| undefined;
|
|
150
|
+
const tool = catalog.find((t) => t.name === name);
|
|
151
|
+
|
|
152
|
+
if (!tool) {
|
|
153
|
+
return {
|
|
154
|
+
content: [{ type: 'text' as const, text: `Tool "${name}" not found.` }],
|
|
155
|
+
isError: true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
// Build URL from path template
|
|
161
|
+
let url = config.baseUrl + tool.path;
|
|
162
|
+
for (const param of tool.pathParams) {
|
|
163
|
+
if (args[param] !== undefined) {
|
|
164
|
+
url = url.replace(`{${param}}`, encodeURIComponent(String(args[param])));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Add query params
|
|
169
|
+
const queryParams = new URLSearchParams();
|
|
170
|
+
for (const param of tool.queryParams) {
|
|
171
|
+
if (args[param] !== undefined) {
|
|
172
|
+
queryParams.set(param, String(args[param]));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const qs = queryParams.toString();
|
|
176
|
+
if (qs) url += `?${qs}`;
|
|
177
|
+
|
|
178
|
+
const response = await executeRequest({
|
|
179
|
+
method: tool.method,
|
|
180
|
+
url,
|
|
181
|
+
...(tool.hasRequestBody && args.body
|
|
182
|
+
? { body: args.body, contentType: tool.requestBodyContentType }
|
|
183
|
+
: {}),
|
|
184
|
+
headers: {},
|
|
185
|
+
config,
|
|
186
|
+
idempotencyKey,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const result =
|
|
190
|
+
typeof jqFilter === 'string' && jqFilter
|
|
191
|
+
? applyJqFilter(response.data, jqFilter)
|
|
192
|
+
: response.data;
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: 'text' as const,
|
|
198
|
+
text: JSON.stringify(result, null, 2),
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
} catch (error) {
|
|
203
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
204
|
+
return {
|
|
205
|
+
content: [{ type: 'text' as const, text: `Error: ${message}` }],
|
|
206
|
+
isError: true,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// 4. search_tools — semantic search over tool descriptions
|
|
213
|
+
server.registerTool(
|
|
214
|
+
'search_tools',
|
|
215
|
+
{
|
|
216
|
+
title: 'Search Tools',
|
|
217
|
+
description: 'Search for tools by description. Uses fuzzy matching.',
|
|
218
|
+
inputSchema: z.object({
|
|
219
|
+
description: z.string().describe('What you want to do (natural language)'),
|
|
220
|
+
limit: z.number().int().optional().describe('Max results (default: 5)'),
|
|
221
|
+
}),
|
|
222
|
+
annotations: { readOnlyHint: true },
|
|
223
|
+
},
|
|
224
|
+
async (input) => {
|
|
225
|
+
const description = (input as Record<string, unknown>).description as string;
|
|
226
|
+
const limit = ((input as Record<string, unknown>).limit as number) ?? 5;
|
|
227
|
+
|
|
228
|
+
const results = searchTools(description).slice(0, limit);
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
content: [
|
|
232
|
+
{
|
|
233
|
+
type: 'text' as const,
|
|
234
|
+
text: JSON.stringify(
|
|
235
|
+
results.map((t) => ({ name: t.name, description: t.description, method: t.method, path: t.path })),
|
|
236
|
+
null,
|
|
237
|
+
2,
|
|
238
|
+
),
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// TF-IDF Search
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
interface SearchEntry {
|
|
251
|
+
tool: CatalogEntry;
|
|
252
|
+
terms: Map<string, number>; // term -> TF-IDF weight
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function buildSearchIndex(tools: CatalogEntry[]): SearchEntry[] {
|
|
256
|
+
const docFreq = new Map<string, number>();
|
|
257
|
+
const docs: Array<{ tool: CatalogEntry; terms: string[] }> = [];
|
|
258
|
+
|
|
259
|
+
for (const tool of tools) {
|
|
260
|
+
const text = `${tool.name} ${tool.title} ${tool.description} ${tool.method} ${tool.path}`;
|
|
261
|
+
const terms = tokenize(text);
|
|
262
|
+
docs.push({ tool, terms });
|
|
263
|
+
|
|
264
|
+
const unique = new Set(terms);
|
|
265
|
+
for (const term of unique) {
|
|
266
|
+
docFreq.set(term, (docFreq.get(term) ?? 0) + 1);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const N = tools.length;
|
|
271
|
+
return docs.map(({ tool, terms }) => {
|
|
272
|
+
const termFreq = new Map<string, number>();
|
|
273
|
+
for (const t of terms) {
|
|
274
|
+
termFreq.set(t, (termFreq.get(t) ?? 0) + 1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const weighted = new Map<string, number>();
|
|
278
|
+
for (const [term, tf] of termFreq) {
|
|
279
|
+
const df = docFreq.get(term) ?? 1;
|
|
280
|
+
const idf = Math.log(N / df);
|
|
281
|
+
weighted.set(term, tf * idf);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { tool, terms: weighted };
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function searchTools(query: string): CatalogEntry[] {
|
|
289
|
+
const queryTerms = tokenize(query);
|
|
290
|
+
if (queryTerms.length === 0) return catalog;
|
|
291
|
+
|
|
292
|
+
const scores: Array<{ tool: CatalogEntry; score: number }> = [];
|
|
293
|
+
|
|
294
|
+
for (const entry of searchIndex) {
|
|
295
|
+
let score = 0;
|
|
296
|
+
for (const qt of queryTerms) {
|
|
297
|
+
// Exact match
|
|
298
|
+
score += entry.terms.get(qt) ?? 0;
|
|
299
|
+
// Prefix match (fuzzy)
|
|
300
|
+
for (const [term, weight] of entry.terms) {
|
|
301
|
+
if (term.startsWith(qt) || qt.startsWith(term)) {
|
|
302
|
+
score += weight * 0.5;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (score > 0) {
|
|
307
|
+
scores.push({ tool: entry.tool, score });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return scores.sort((a, b) => b.score - a.score).map((s) => s.tool);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function tokenize(text: string): string[] {
|
|
315
|
+
return text
|
|
316
|
+
.toLowerCase()
|
|
317
|
+
.replace(/[^a-z0-9]/g, ' ')
|
|
318
|
+
.split(/\s+/)
|
|
319
|
+
.filter((t) => t.length > 1);
|
|
320
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
FROM node:20-alpine AS builder
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
COPY package.json package-lock.json ./
|
|
6
|
+
RUN npm ci
|
|
7
|
+
|
|
8
|
+
COPY tsconfig.json ./
|
|
9
|
+
COPY src/ src/
|
|
10
|
+
RUN npm run build
|
|
11
|
+
|
|
12
|
+
FROM node:20-alpine
|
|
13
|
+
|
|
14
|
+
LABEL org.opencontainers.image.title="{{serverName}}"
|
|
15
|
+
|
|
16
|
+
ENV NODE_ENV=production
|
|
17
|
+
ENV TRANSPORT=http
|
|
18
|
+
ENV PORT=3000
|
|
19
|
+
|
|
20
|
+
WORKDIR /app
|
|
21
|
+
|
|
22
|
+
COPY package.json package-lock.json ./
|
|
23
|
+
RUN npm ci --omit=dev
|
|
24
|
+
|
|
25
|
+
COPY --from=builder /app/dist dist/
|
|
26
|
+
|
|
27
|
+
USER node
|
|
28
|
+
|
|
29
|
+
EXPOSE 3000
|
|
30
|
+
|
|
31
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
32
|
+
CMD node -e "fetch('http://localhost:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
|
33
|
+
|
|
34
|
+
CMD ["node", "dist/index.js"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# {{serverName}} configuration
|
|
2
|
+
|
|
3
|
+
# Base URL for the API
|
|
4
|
+
BASE_URL={{baseUrl}}
|
|
5
|
+
|
|
6
|
+
# Named environments (optional). MCP_ENVIRONMENTS is a JSON object mapping a
|
|
7
|
+
# name to a base URL; API_ENVIRONMENT selects one. Falls back to BASE_URL when
|
|
8
|
+
# unset or the name is unknown.
|
|
9
|
+
{{#if environmentsJson}}
|
|
10
|
+
MCP_ENVIRONMENTS={{environmentsJson}}
|
|
11
|
+
API_ENVIRONMENT={{defaultEnvironment}}
|
|
12
|
+
{{else}}
|
|
13
|
+
# MCP_ENVIRONMENTS={"production":"{{baseUrl}}","sandbox":"https://sandbox.example.com"}
|
|
14
|
+
# API_ENVIRONMENT=production
|
|
15
|
+
{{/if}}
|
|
16
|
+
|
|
17
|
+
{{#each authEnvVars}}
|
|
18
|
+
# {{description}}
|
|
19
|
+
{{name}}=
|
|
20
|
+
{{/each}}
|
|
21
|
+
|
|
22
|
+
# HTTP tuning
|
|
23
|
+
MAX_RETRIES=3
|
|
24
|
+
REQUEST_INTERVAL_MS=100
|
|
25
|
+
|
|
26
|
+
# Runtime tool filtering (optional). Comma-separated tool names.
|
|
27
|
+
# MCP_TOOLS — allowlist: only these tools are registered.
|
|
28
|
+
# MCP_EXCLUDE_TOOLS — denylist: these tools are never registered (applied after the allowlist).
|
|
29
|
+
# MCP_TOOLS=
|
|
30
|
+
# MCP_EXCLUDE_TOOLS=
|
|
31
|
+
|
|
32
|
+
# Custom default headers added to every upstream request (optional, JSON object).
|
|
33
|
+
# MCP_DEFAULT_HEADERS={"X-Tenant-Id":"acme","X-Trace":"1"}
|
|
34
|
+
|
|
35
|
+
# Auto-generate an Idempotency-Key header for mutating (POST/PUT/PATCH/DELETE)
|
|
36
|
+
# requests when no per-call idempotency_key is supplied (optional).
|
|
37
|
+
# MCP_IDEMPOTENCY_AUTO=true
|
|
38
|
+
{{#if (eq transport "http")}}
|
|
39
|
+
|
|
40
|
+
# Transport: "stdio" or "http"
|
|
41
|
+
TRANSPORT=http
|
|
42
|
+
PORT=3000
|
|
43
|
+
# Allowed origin for CORS/DNS-rebinding protection (leave empty to allow all in dev)
|
|
44
|
+
ALLOWED_ORIGIN=
|
|
45
|
+
# Bearer token required on every /mcp request. When set, clients must send
|
|
46
|
+
# `Authorization: Bearer <token>`. Leave empty to disable auth (local/dev only).
|
|
47
|
+
MCP_AUTH_TOKEN=
|
|
48
|
+
# Session mode (MCP 2026-07-28): leave empty for stateless (default, recommended
|
|
49
|
+
# — any request can hit any instance). Set to "true" for stateful per-session
|
|
50
|
+
# (Mcp-Session-Id) mode if a client needs resumable streams.
|
|
51
|
+
MCP_STATEFUL=
|
|
52
|
+
{{/if}}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { getAuthHeaders } from './auth.js';
|
|
2
|
+
import { traceHeaders } from './trace.js';
|
|
3
|
+
import type { AppConfig } from './config.js';
|
|
4
|
+
|
|
5
|
+
export interface RequestOptions {
|
|
6
|
+
method: string;
|
|
7
|
+
url: string;
|
|
8
|
+
body?: unknown;
|
|
9
|
+
contentType?: string;
|
|
10
|
+
headers: Record<string, string>;
|
|
11
|
+
config: AppConfig;
|
|
12
|
+
/**
|
|
13
|
+
* Optional Idempotency-Key for mutating (POST/PUT/PATCH/DELETE) requests. When
|
|
14
|
+
* absent and `MCP_IDEMPOTENCY_AUTO=true`, a UUID is generated automatically.
|
|
15
|
+
*/
|
|
16
|
+
idempotencyKey?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ApiResponse {
|
|
20
|
+
status: number;
|
|
21
|
+
data: unknown;
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class ApiError extends Error {
|
|
26
|
+
constructor(
|
|
27
|
+
message: string,
|
|
28
|
+
public readonly status: number,
|
|
29
|
+
public readonly data: unknown,
|
|
30
|
+
) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'ApiError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const RETRYABLE_STATUS_CODES = [429, 500, 502, 503, 504];
|
|
37
|
+
|
|
38
|
+
/** Methods that mutate server state and therefore benefit from an idempotency key. */
|
|
39
|
+
const MUTATING_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
40
|
+
|
|
41
|
+
let lastRequestTime = 0;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the Idempotency-Key for a mutating request. Precedence:
|
|
45
|
+
* 1. an explicit per-call key passed in `options.idempotencyKey`
|
|
46
|
+
* 2. when `MCP_IDEMPOTENCY_AUTO=true`, a generated UUID (so retries of the
|
|
47
|
+
* same upstream call carry a stable key for the duration of the attempt loop)
|
|
48
|
+
* Returns undefined for non-mutating methods or when neither is configured, so
|
|
49
|
+
* the default behavior is unchanged.
|
|
50
|
+
*/
|
|
51
|
+
function resolveIdempotencyKey(method: string, explicit: string | undefined): string | undefined {
|
|
52
|
+
if (!MUTATING_METHODS.has(method)) return undefined;
|
|
53
|
+
if (explicit) return explicit;
|
|
54
|
+
// `process` is absent on Workers (nodejs_compat leaves it undefined here), so
|
|
55
|
+
// guard it; auto-keying is then simply off on that target unless a per-call
|
|
56
|
+
// key is supplied. The Web Crypto `crypto` global exists on both runtimes.
|
|
57
|
+
const auto =
|
|
58
|
+
typeof process !== 'undefined' && process.env?.MCP_IDEMPOTENCY_AUTO === 'true';
|
|
59
|
+
if (auto) {
|
|
60
|
+
return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
61
|
+
? crypto.randomUUID()
|
|
62
|
+
: `idem-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function executeRequest(options: RequestOptions): Promise<ApiResponse> {
|
|
68
|
+
const { config } = options;
|
|
69
|
+
|
|
70
|
+
// Rate limiting
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const elapsed = now - lastRequestTime;
|
|
73
|
+
if (elapsed < config.requestIntervalMs) {
|
|
74
|
+
await sleep(config.requestIntervalMs - elapsed);
|
|
75
|
+
}
|
|
76
|
+
lastRequestTime = Date.now();
|
|
77
|
+
|
|
78
|
+
const authHeaders = await getAuthHeaders(config);
|
|
79
|
+
// Header precedence (lowest → highest): auth, runtime default headers
|
|
80
|
+
// (MCP_DEFAULT_HEADERS), W3C Trace Context, then per-call headers.
|
|
81
|
+
// Propagate W3C Trace Context to the upstream API (no-op outside an HTTP
|
|
82
|
+
// request, e.g. stdio transport, where there is no active trace).
|
|
83
|
+
const method = options.method.toUpperCase();
|
|
84
|
+
const headers: Record<string, string> = {
|
|
85
|
+
...authHeaders,
|
|
86
|
+
...config.defaultHeaders,
|
|
87
|
+
...traceHeaders(),
|
|
88
|
+
...options.headers,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (options.body && options.contentType) {
|
|
92
|
+
headers['Content-Type'] = options.contentType;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Idempotency-Key for mutating requests (explicit per-call, or auto when enabled).
|
|
96
|
+
const idempotencyKey = resolveIdempotencyKey(method, options.idempotencyKey);
|
|
97
|
+
if (idempotencyKey && !headers['Idempotency-Key']) {
|
|
98
|
+
headers['Idempotency-Key'] = idempotencyKey;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let lastError: Error | undefined;
|
|
102
|
+
|
|
103
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
104
|
+
try {
|
|
105
|
+
const response = await fetch(options.url, {
|
|
106
|
+
method,
|
|
107
|
+
headers,
|
|
108
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (RETRYABLE_STATUS_CODES.includes(response.status) && attempt < config.maxRetries) {
|
|
112
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
113
|
+
const delay = retryAfter
|
|
114
|
+
? parseInt(retryAfter, 10) * 1000
|
|
115
|
+
: 1000 * Math.pow(2, attempt);
|
|
116
|
+
await sleep(delay);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const data = await parseResponseBody(response);
|
|
121
|
+
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
throw new ApiError(
|
|
124
|
+
`${response.status} ${response.statusText}`,
|
|
125
|
+
response.status,
|
|
126
|
+
data,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
status: response.status,
|
|
132
|
+
data,
|
|
133
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
134
|
+
};
|
|
135
|
+
} catch (error) {
|
|
136
|
+
lastError = error as Error;
|
|
137
|
+
if (error instanceof ApiError && !RETRYABLE_STATUS_CODES.includes(error.status)) {
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
if (attempt < config.maxRetries) {
|
|
141
|
+
await sleep(1000 * Math.pow(2, attempt));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
throw lastError ?? new Error('Request failed after retries');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function parseResponseBody(response: Response): Promise<unknown> {
|
|
150
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
151
|
+
if (contentType.includes('application/json')) {
|
|
152
|
+
return response.json();
|
|
153
|
+
}
|
|
154
|
+
return response.text();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function sleep(ms: number): Promise<void> {
|
|
158
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
159
|
+
}
|