mcpmake 0.1.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/README.md +691 -0
- package/bin/mcpmake.mjs +2 -0
- package/dist/analyzer/auth-detector.d.ts +12 -0
- package/dist/analyzer/auth-detector.js +142 -0
- package/dist/analyzer/dom-parser.d.ts +10 -0
- package/dist/analyzer/dom-parser.js +259 -0
- package/dist/analyzer/goal-crawler.d.ts +25 -0
- package/dist/analyzer/goal-crawler.js +177 -0
- package/dist/analyzer/hybrid-detector.d.ts +28 -0
- package/dist/analyzer/hybrid-detector.js +96 -0
- package/dist/analyzer/index.d.ts +12 -0
- package/dist/analyzer/index.js +8 -0
- package/dist/analyzer/screenshot-capture.d.ts +29 -0
- package/dist/analyzer/screenshot-capture.js +42 -0
- package/dist/analyzer/selector-builder.d.ts +19 -0
- package/dist/analyzer/selector-builder.js +199 -0
- package/dist/analyzer/semantic-analyzer.d.ts +13 -0
- package/dist/analyzer/semantic-analyzer.js +145 -0
- package/dist/analyzer/site-crawler.d.ts +38 -0
- package/dist/analyzer/site-crawler.js +235 -0
- package/dist/cloud/billing/billing-engine.d.ts +44 -0
- package/dist/cloud/billing/billing-engine.js +81 -0
- package/dist/cloud/billing/credit-store.d.ts +64 -0
- package/dist/cloud/billing/credit-store.js +168 -0
- package/dist/cloud/billing/index.d.ts +4 -0
- package/dist/cloud/billing/index.js +2 -0
- package/dist/cloud/billing/usage-store.d.ts +42 -0
- package/dist/cloud/billing/usage-store.js +85 -0
- package/dist/cloud/billing/usage-tracker.d.ts +38 -0
- package/dist/cloud/billing/usage-tracker.js +95 -0
- package/dist/cloud/build-pipeline.d.ts +39 -0
- package/dist/cloud/build-pipeline.js +310 -0
- package/dist/cloud/build-queue.d.ts +30 -0
- package/dist/cloud/build-queue.js +70 -0
- package/dist/cloud/caddy-manager.d.ts +18 -0
- package/dist/cloud/caddy-manager.js +97 -0
- package/dist/cloud/container-backend.d.ts +62 -0
- package/dist/cloud/container-backend.js +59 -0
- package/dist/cloud/container-manager.d.ts +64 -0
- package/dist/cloud/container-manager.js +301 -0
- package/dist/cloud/crypto.d.ts +27 -0
- package/dist/cloud/crypto.js +63 -0
- package/dist/cloud/db/index.d.ts +27 -0
- package/dist/cloud/db/index.js +53 -0
- package/dist/cloud/db/migrations.d.ts +12 -0
- package/dist/cloud/db/migrations.js +329 -0
- package/dist/cloud/db/pg-store.d.ts +45 -0
- package/dist/cloud/db/pg-store.js +336 -0
- package/dist/cloud/failure-tracker.d.ts +51 -0
- package/dist/cloud/failure-tracker.js +102 -0
- package/dist/cloud/idle-monitor.d.ts +30 -0
- package/dist/cloud/idle-monitor.js +70 -0
- package/dist/cloud/mailer.d.ts +21 -0
- package/dist/cloud/mailer.js +193 -0
- package/dist/cloud/mcp-proxy.d.ts +58 -0
- package/dist/cloud/mcp-proxy.js +203 -0
- package/dist/cloud/metric-samples.d.ts +43 -0
- package/dist/cloud/metric-samples.js +85 -0
- package/dist/cloud/metrics.d.ts +26 -0
- package/dist/cloud/metrics.js +59 -0
- package/dist/cloud/multipart.d.ts +26 -0
- package/dist/cloud/multipart.js +132 -0
- package/dist/cloud/observability.d.ts +27 -0
- package/dist/cloud/observability.js +98 -0
- package/dist/cloud/rate-limiter.d.ts +31 -0
- package/dist/cloud/rate-limiter.js +58 -0
- package/dist/cloud/request-security.d.ts +5 -0
- package/dist/cloud/request-security.js +74 -0
- package/dist/cloud/resource-monitor.d.ts +69 -0
- package/dist/cloud/resource-monitor.js +130 -0
- package/dist/cloud/secret-store.d.ts +38 -0
- package/dist/cloud/secret-store.js +103 -0
- package/dist/cloud/security.d.ts +26 -0
- package/dist/cloud/security.js +142 -0
- package/dist/cloud/server.d.ts +21 -0
- package/dist/cloud/server.js +1079 -0
- package/dist/cloud/shared-state.d.ts +72 -0
- package/dist/cloud/shared-state.js +159 -0
- package/dist/cloud/ssrf.d.ts +43 -0
- package/dist/cloud/ssrf.js +150 -0
- package/dist/cloud/store.d.ts +41 -0
- package/dist/cloud/store.js +75 -0
- package/dist/cloud/stripe.d.ts +78 -0
- package/dist/cloud/stripe.js +317 -0
- package/dist/cloud/telemetry-store.d.ts +53 -0
- package/dist/cloud/telemetry-store.js +108 -0
- package/dist/cloud/web/auth.d.ts +225 -0
- package/dist/cloud/web/auth.js +555 -0
- package/dist/cloud/web/charts.d.ts +70 -0
- package/dist/cloud/web/charts.js +178 -0
- package/dist/cloud/web/csrf.d.ts +14 -0
- package/dist/cloud/web/csrf.js +22 -0
- package/dist/cloud/web/docs.d.ts +40 -0
- package/dist/cloud/web/docs.js +174 -0
- package/dist/cloud/web/router.d.ts +25 -0
- package/dist/cloud/web/router.js +1921 -0
- package/dist/cloud/web/static/alpine.min.js +5 -0
- package/dist/cloud/web/static/favicon.svg +4 -0
- package/dist/cloud/web/static/htmx-sse.js +290 -0
- package/dist/cloud/web/static/htmx.min.js +1 -0
- package/dist/cloud/web/static/style.css +2683 -0
- package/dist/cloud/web/static-server.d.ts +13 -0
- package/dist/cloud/web/static-server.js +73 -0
- package/dist/cloud/web/template-engine.d.ts +27 -0
- package/dist/cloud/web/template-engine.js +146 -0
- package/dist/cloud/web/templates/layouts/admin.hbs +57 -0
- package/dist/cloud/web/templates/layouts/auth.hbs +138 -0
- package/dist/cloud/web/templates/layouts/base.hbs +16 -0
- package/dist/cloud/web/templates/layouts/dashboard.hbs +39 -0
- package/dist/cloud/web/templates/layouts/landing.hbs +82 -0
- package/dist/cloud/web/templates/pages/admin/overview.hbs +123 -0
- package/dist/cloud/web/templates/pages/admin/servers.hbs +129 -0
- package/dist/cloud/web/templates/pages/admin/telemetry.hbs +39 -0
- package/dist/cloud/web/templates/pages/admin/user-edit.hbs +91 -0
- package/dist/cloud/web/templates/pages/admin/users.hbs +179 -0
- package/dist/cloud/web/templates/pages/auth/forgot-password.hbs +25 -0
- package/dist/cloud/web/templates/pages/auth/login.hbs +33 -0
- package/dist/cloud/web/templates/pages/auth/register.hbs +32 -0
- package/dist/cloud/web/templates/pages/auth/reset-password.hbs +34 -0
- package/dist/cloud/web/templates/pages/dashboard/billing.hbs +140 -0
- package/dist/cloud/web/templates/pages/dashboard/create.hbs +173 -0
- package/dist/cloud/web/templates/pages/dashboard/index.hbs +8 -0
- package/dist/cloud/web/templates/pages/dashboard/server-detail.hbs +280 -0
- package/dist/cloud/web/templates/pages/dashboard/server-logs.hbs +35 -0
- package/dist/cloud/web/templates/pages/dashboard/server-metrics.hbs +63 -0
- package/dist/cloud/web/templates/pages/dashboard/servers-partial.hbs +21 -0
- package/dist/cloud/web/templates/pages/dashboard/servers.hbs +44 -0
- package/dist/cloud/web/templates/pages/docs/show.hbs +16 -0
- package/dist/cloud/web/templates/pages/errors/404.hbs +9 -0
- package/dist/cloud/web/templates/pages/errors/500.hbs +8 -0
- package/dist/cloud/web/templates/pages/landing/index.hbs +223 -0
- package/dist/cloud/web/templates/pages/legal/privacy.hbs +71 -0
- package/dist/cloud/web/templates/pages/legal/terms.hbs +73 -0
- package/dist/cloud/web/templates/partials/admin-stats.hbs +52 -0
- package/dist/cloud/web/templates/partials/flash-message.hbs +6 -0
- package/dist/cloud/web/templates/partials/pricing-table.hbs +103 -0
- package/dist/cloud/web/templates/partials/server-card.hbs +19 -0
- package/dist/cloud/web/templates/partials/status-badge.hbs +1 -0
- package/dist/commands/bundle.d.ts +18 -0
- package/dist/commands/bundle.js +82 -0
- package/dist/commands/ci.d.ts +25 -0
- package/dist/commands/ci.js +149 -0
- package/dist/commands/deploy.d.ts +24 -0
- package/dist/commands/deploy.js +145 -0
- package/dist/commands/diff.d.ts +18 -0
- package/dist/commands/diff.js +185 -0
- package/dist/commands/from/describe.d.ts +65 -0
- package/dist/commands/from/describe.js +173 -0
- package/dist/commands/from/har.d.ts +81 -0
- package/dist/commands/from/har.js +255 -0
- package/dist/commands/from/openapi.d.ts +105 -0
- package/dist/commands/from/openapi.js +302 -0
- package/dist/commands/from/postman.d.ts +51 -0
- package/dist/commands/from/postman.js +146 -0
- package/dist/commands/from/target-support.d.ts +11 -0
- package/dist/commands/from/target-support.js +33 -0
- package/dist/commands/from/url.d.ts +75 -0
- package/dist/commands/from/url.js +244 -0
- package/dist/commands/from/website.d.ts +75 -0
- package/dist/commands/from/website.js +284 -0
- package/dist/commands/lint.d.ts +24 -0
- package/dist/commands/lint.js +184 -0
- package/dist/commands/merge.d.ts +18 -0
- package/dist/commands/merge.js +161 -0
- package/dist/commands/publish.d.ts +27 -0
- package/dist/commands/publish.js +334 -0
- package/dist/commands/rescan.d.ts +40 -0
- package/dist/commands/rescan.js +255 -0
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.js +87 -0
- package/dist/commands/verify.d.ts +14 -0
- package/dist/commands/verify.js +71 -0
- package/dist/config/configurable-command.d.ts +13 -0
- package/dist/config/configurable-command.js +70 -0
- package/dist/config/mcpmake-config.d.ts +68 -0
- package/dist/config/mcpmake-config.js +207 -0
- package/dist/docs/cli.md +400 -0
- package/dist/docs/mcp-2026-07-28-migration.md +78 -0
- package/dist/docs/migrate-from-stainless.md +94 -0
- package/dist/docs/quickstart.md +166 -0
- package/dist/docs/show-hn.md +26 -0
- package/dist/docs/website-servers.md +169 -0
- package/dist/emitter/code-writer.d.ts +8 -0
- package/dist/emitter/code-writer.js +25 -0
- package/dist/emitter/index.d.ts +32 -0
- package/dist/emitter/index.js +280 -0
- package/dist/emitter/mcpb-bundler.d.ts +31 -0
- package/dist/emitter/mcpb-bundler.js +172 -0
- package/dist/emitter/project-scaffolder.d.ts +4 -0
- package/dist/emitter/project-scaffolder.js +89 -0
- package/dist/emitter/python-template-loader.d.ts +4 -0
- package/dist/emitter/python-template-loader.js +30 -0
- package/dist/emitter/python-templates/dockerfile.hbs +14 -0
- package/dist/emitter/python-templates/env.example.hbs +6 -0
- package/dist/emitter/python-templates/requirements.txt.hbs +4 -0
- package/dist/emitter/python-templates/server.py.hbs +77 -0
- package/dist/emitter/site-scaffolder.d.ts +13 -0
- package/dist/emitter/site-scaffolder.js +70 -0
- package/dist/emitter/site-template-loader.d.ts +5 -0
- package/dist/emitter/site-template-loader.js +47 -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 +1 -0
- package/dist/emitter/template-loader.js +27 -0
- package/dist/emitter/templates/auth-provider.ts.hbs +57 -0
- package/dist/emitter/templates/config.ts.hbs +63 -0
- package/dist/emitter/templates/discovery.ts.hbs +301 -0
- package/dist/emitter/templates/dockerfile.hbs +34 -0
- package/dist/emitter/templates/env.example.hbs +28 -0
- package/dist/emitter/templates/gitignore.hbs +5 -0
- package/dist/emitter/templates/http-executor.ts.hbs +117 -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 +123 -0
- package/dist/emitter/templates/resources.ts.hbs +63 -0
- package/dist/emitter/templates/server-main-http.ts.hbs +407 -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 +124 -0
- package/dist/emitter/templates/tool-index.ts.hbs +11 -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 +5 -0
- package/dist/emitter/worker-template-loader.js +33 -0
- package/dist/emitter/worker-templates/config.ts.hbs +54 -0
- package/dist/emitter/worker-templates/dev-vars.example.hbs +10 -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 +53 -0
- package/dist/emitter/worker-templates/server.test.ts.hbs +20 -0
- package/dist/emitter/worker-templates/tool-handler.ts.hbs +85 -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 +242 -0
- package/dist/emitter/worker-templates/wrangler.toml.hbs +19 -0
- package/dist/generator/spec-generator.d.ts +6 -0
- package/dist/generator/spec-generator.js +50 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +64 -0
- package/dist/parser/har-filter.d.ts +8 -0
- package/dist/parser/har-filter.js +71 -0
- package/dist/parser/har-loader.d.ts +2 -0
- package/dist/parser/har-loader.js +14 -0
- package/dist/parser/har-normalizer.d.ts +20 -0
- package/dist/parser/har-normalizer.js +78 -0
- package/dist/parser/index.d.ts +10 -0
- package/dist/parser/index.js +6 -0
- package/dist/parser/openapi-loader.d.ts +6 -0
- package/dist/parser/openapi-loader.js +308 -0
- package/dist/parser/operation-extractor.d.ts +13 -0
- package/dist/parser/operation-extractor.js +155 -0
- package/dist/parser/overlay-loader.d.ts +10 -0
- package/dist/parser/overlay-loader.js +184 -0
- package/dist/parser/postman-loader.d.ts +9 -0
- package/dist/parser/postman-loader.js +106 -0
- package/dist/parser/schema-converter.d.ts +12 -0
- package/dist/parser/schema-converter.js +117 -0
- package/dist/plugins/adapter.d.ts +40 -0
- package/dist/plugins/adapter.js +15 -0
- package/dist/plugins/loader.d.ts +25 -0
- package/dist/plugins/loader.js +58 -0
- package/dist/pricing.d.ts +55 -0
- package/dist/pricing.js +133 -0
- package/dist/providers/index.d.ts +15 -0
- package/dist/providers/index.js +56 -0
- package/dist/recorder/browser-recorder.d.ts +22 -0
- package/dist/recorder/browser-recorder.js +205 -0
- package/dist/registry/official-registry.d.ts +90 -0
- package/dist/registry/official-registry.js +129 -0
- package/dist/rescan/diff-engine.d.ts +5 -0
- package/dist/rescan/diff-engine.js +312 -0
- package/dist/rescan/index.d.ts +3 -0
- package/dist/rescan/index.js +2 -0
- package/dist/rescan/rescan-runner.d.ts +42 -0
- package/dist/rescan/rescan-runner.js +69 -0
- package/dist/rescan/rescan-scheduler.d.ts +41 -0
- package/dist/rescan/rescan-scheduler.js +179 -0
- package/dist/site-transformer/browser-tools.d.ts +10 -0
- package/dist/site-transformer/browser-tools.js +59 -0
- package/dist/site-transformer/index.d.ts +2 -0
- package/dist/site-transformer/index.js +2 -0
- package/dist/site-transformer/selector-healer.d.ts +8 -0
- package/dist/site-transformer/selector-healer.js +106 -0
- package/dist/site-transformer/tool-generator.d.ts +13 -0
- package/dist/site-transformer/tool-generator.js +245 -0
- package/dist/transformer/auth-detector.d.ts +13 -0
- package/dist/transformer/auth-detector.js +90 -0
- package/dist/transformer/catalog-builder.d.ts +18 -0
- package/dist/transformer/catalog-builder.js +56 -0
- package/dist/transformer/client-compat.d.ts +6 -0
- package/dist/transformer/client-compat.js +44 -0
- package/dist/transformer/har-clusterer.d.ts +9 -0
- package/dist/transformer/har-clusterer.js +27 -0
- package/dist/transformer/har-dedup.d.ts +10 -0
- package/dist/transformer/har-dedup.js +81 -0
- package/dist/transformer/har-schema-inferrer.d.ts +15 -0
- package/dist/transformer/har-schema-inferrer.js +90 -0
- package/dist/transformer/har-to-operations.d.ts +13 -0
- package/dist/transformer/har-to-operations.js +192 -0
- package/dist/transformer/index.d.ts +8 -0
- package/dist/transformer/index.js +6 -0
- package/dist/transformer/llm-namer.d.ts +6 -0
- package/dist/transformer/llm-namer.js +59 -0
- package/dist/transformer/naming.d.ts +4 -0
- package/dist/transformer/naming.js +30 -0
- package/dist/transformer/operation-filter.d.ts +13 -0
- package/dist/transformer/operation-filter.js +52 -0
- package/dist/transformer/resource-builder.d.ts +12 -0
- package/dist/transformer/resource-builder.js +80 -0
- package/dist/transformer/schema-merger.d.ts +14 -0
- package/dist/transformer/schema-merger.js +65 -0
- package/dist/transformer/tool-builder.d.ts +3 -0
- package/dist/transformer/tool-builder.js +114 -0
- package/dist/types/index.d.ts +131 -0
- package/dist/types/index.js +1 -0
- package/dist/types/site.d.ts +284 -0
- package/dist/types/site.js +8 -0
- package/dist/utils/fail.d.ts +48 -0
- package/dist/utils/fail.js +204 -0
- package/dist/utils/fs.d.ts +5 -0
- package/dist/utils/fs.js +28 -0
- package/dist/utils/interactive.d.ts +6 -0
- package/dist/utils/interactive.js +30 -0
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.js +2 -0
- package/dist/utils/sanitize.d.ts +28 -0
- package/dist/utils/sanitize.js +44 -0
- package/dist/utils/watcher.d.ts +11 -0
- package/dist/utils/watcher.js +36 -0
- package/package.json +65 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Task state machine for async/long-running operations.
|
|
3
|
+
*
|
|
4
|
+
* Tasks are created when a tool returns 202 Accepted. The task manager
|
|
5
|
+
* tracks state transitions and notifies via SSE when status changes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from 'node:crypto';
|
|
9
|
+
import { EventEmitter } from 'node:events';
|
|
10
|
+
|
|
11
|
+
export type TaskStatus = 'working' | 'input_required' | 'completed' | 'failed' | 'cancelled';
|
|
12
|
+
|
|
13
|
+
export interface Task {
|
|
14
|
+
id: string;
|
|
15
|
+
toolName: string;
|
|
16
|
+
status: TaskStatus;
|
|
17
|
+
result?: unknown;
|
|
18
|
+
error?: string;
|
|
19
|
+
createdAt: string;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TaskStatusChangeEvent {
|
|
24
|
+
task: Task;
|
|
25
|
+
previousStatus: TaskStatus;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const tasks = new Map<string, Task>();
|
|
29
|
+
const MAX_TASKS = 1000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Event emitter for task status changes.
|
|
33
|
+
* Emits 'statusChange' events with { task, previousStatus }.
|
|
34
|
+
*/
|
|
35
|
+
export const taskEvents = new EventEmitter();
|
|
36
|
+
|
|
37
|
+
export function createTask(toolName: string): Task {
|
|
38
|
+
// Evict oldest if at capacity
|
|
39
|
+
if (tasks.size >= MAX_TASKS) {
|
|
40
|
+
const oldest = tasks.keys().next().value;
|
|
41
|
+
if (oldest) tasks.delete(oldest);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const task: Task = {
|
|
45
|
+
id: randomUUID(),
|
|
46
|
+
toolName,
|
|
47
|
+
status: 'working',
|
|
48
|
+
createdAt: new Date().toISOString(),
|
|
49
|
+
updatedAt: new Date().toISOString(),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
tasks.set(task.id, task);
|
|
53
|
+
return task;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getTask(taskId: string): Task | undefined {
|
|
57
|
+
return tasks.get(taskId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function updateTask(
|
|
61
|
+
taskId: string,
|
|
62
|
+
update: { status?: TaskStatus; result?: unknown; error?: string },
|
|
63
|
+
): Task | undefined {
|
|
64
|
+
const task = tasks.get(taskId);
|
|
65
|
+
if (!task) return undefined;
|
|
66
|
+
|
|
67
|
+
const previousStatus = task.status;
|
|
68
|
+
if (update.status) task.status = update.status;
|
|
69
|
+
if (update.result !== undefined) task.result = update.result;
|
|
70
|
+
if (update.error !== undefined) task.error = update.error;
|
|
71
|
+
task.updatedAt = new Date().toISOString();
|
|
72
|
+
|
|
73
|
+
if (task.status !== previousStatus) {
|
|
74
|
+
taskEvents.emit('statusChange', { task, previousStatus } as TaskStatusChangeEvent);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return task;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function cancelTask(taskId: string): Task | undefined {
|
|
81
|
+
const task = tasks.get(taskId);
|
|
82
|
+
if (!task) return undefined;
|
|
83
|
+
if (task.status === 'completed' || task.status === 'failed') return task;
|
|
84
|
+
|
|
85
|
+
const previousStatus = task.status;
|
|
86
|
+
task.status = 'cancelled';
|
|
87
|
+
task.updatedAt = new Date().toISOString();
|
|
88
|
+
|
|
89
|
+
taskEvents.emit('statusChange', { task, previousStatus } as TaskStatusChangeEvent);
|
|
90
|
+
|
|
91
|
+
return task;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function listTasks(status?: TaskStatus, limit?: number): Task[] {
|
|
95
|
+
const all = [...tasks.values()];
|
|
96
|
+
const filtered = status ? all.filter((t) => t.status === status) : all;
|
|
97
|
+
return limit !== undefined && limit > 0 ? filtered.slice(0, limit) : filtered;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Returns a promise that resolves with the task when it reaches a terminal
|
|
102
|
+
* state (completed, failed, cancelled), or rejects if the timeout elapses.
|
|
103
|
+
*/
|
|
104
|
+
export function getResult(taskId: string, timeoutMs: number = 30000): Promise<Task> {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const task = tasks.get(taskId);
|
|
107
|
+
if (!task) {
|
|
108
|
+
reject(new Error(`Task ${taskId} not found`));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Already in a terminal state
|
|
113
|
+
if (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled') {
|
|
114
|
+
resolve(task);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const cleanup = (): void => {
|
|
119
|
+
taskEvents.removeListener('statusChange', onChange);
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const timer = setTimeout(() => {
|
|
124
|
+
cleanup();
|
|
125
|
+
reject(new Error('timeout'));
|
|
126
|
+
}, timeoutMs);
|
|
127
|
+
|
|
128
|
+
const onChange = (event: TaskStatusChangeEvent): void => {
|
|
129
|
+
if (event.task.id !== taskId) return;
|
|
130
|
+
const s = event.task.status;
|
|
131
|
+
if (s === 'completed' || s === 'failed' || s === 'cancelled') {
|
|
132
|
+
cleanup();
|
|
133
|
+
resolve(event.task);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
taskEvents.on('statusChange', onChange);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-Sent Events endpoint for real-time task status notifications.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
6
|
+
import { taskEvents, type TaskStatusChangeEvent } from './task-manager.js';
|
|
7
|
+
|
|
8
|
+
interface SseConnection {
|
|
9
|
+
res: ServerResponse;
|
|
10
|
+
taskIdFilter: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const connections = new Set<SseConnection>();
|
|
14
|
+
|
|
15
|
+
const HEARTBEAT_INTERVAL_MS = 30000;
|
|
16
|
+
let heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
17
|
+
|
|
18
|
+
function ensureHeartbeat(): void {
|
|
19
|
+
if (heartbeatTimer) return;
|
|
20
|
+
heartbeatTimer = setInterval(() => {
|
|
21
|
+
for (const conn of connections) {
|
|
22
|
+
if (!conn.res.writableEnded) {
|
|
23
|
+
conn.res.write(':ping\n\n');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
27
|
+
heartbeatTimer.unref();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stopHeartbeatIfEmpty(): void {
|
|
31
|
+
if (connections.size === 0 && heartbeatTimer) {
|
|
32
|
+
clearInterval(heartbeatTimer);
|
|
33
|
+
heartbeatTimer = null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function broadcast(eventName: string, data: unknown, taskId: string): void {
|
|
38
|
+
const payload = `event: ${eventName}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
39
|
+
for (const conn of connections) {
|
|
40
|
+
if (conn.res.writableEnded) continue;
|
|
41
|
+
if (conn.taskIdFilter && conn.taskIdFilter !== taskId) continue;
|
|
42
|
+
conn.res.write(payload);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Subscribe to task status changes and broadcast SSE events */
|
|
47
|
+
taskEvents.on('statusChange', (event: TaskStatusChangeEvent) => {
|
|
48
|
+
const { task } = event;
|
|
49
|
+
|
|
50
|
+
broadcast('task.status', {
|
|
51
|
+
taskId: task.id,
|
|
52
|
+
status: task.status,
|
|
53
|
+
updatedAt: task.updatedAt,
|
|
54
|
+
}, task.id);
|
|
55
|
+
|
|
56
|
+
if (task.status === 'completed') {
|
|
57
|
+
broadcast('task.completed', {
|
|
58
|
+
taskId: task.id,
|
|
59
|
+
status: 'completed',
|
|
60
|
+
result: task.result,
|
|
61
|
+
}, task.id);
|
|
62
|
+
} else if (task.status === 'failed') {
|
|
63
|
+
broadcast('task.failed', {
|
|
64
|
+
taskId: task.id,
|
|
65
|
+
status: 'failed',
|
|
66
|
+
error: task.error,
|
|
67
|
+
}, task.id);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Handle the SSE endpoint: GET /tasks/events
|
|
73
|
+
* Returns true if the request was handled.
|
|
74
|
+
*/
|
|
75
|
+
export function handleTaskSse(
|
|
76
|
+
req: IncomingMessage,
|
|
77
|
+
res: ServerResponse,
|
|
78
|
+
url: URL,
|
|
79
|
+
): boolean {
|
|
80
|
+
if (url.pathname !== '/tasks/events' || (req.method ?? 'GET') !== 'GET') {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const taskIdFilter = url.searchParams.get('taskId') ?? null;
|
|
85
|
+
|
|
86
|
+
res.writeHead(200, {
|
|
87
|
+
'Content-Type': 'text/event-stream',
|
|
88
|
+
'Cache-Control': 'no-cache',
|
|
89
|
+
Connection: 'keep-alive',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Initial comment to confirm connection
|
|
93
|
+
res.write(':connected\n\n');
|
|
94
|
+
|
|
95
|
+
const conn: SseConnection = { res, taskIdFilter };
|
|
96
|
+
connections.add(conn);
|
|
97
|
+
ensureHeartbeat();
|
|
98
|
+
|
|
99
|
+
req.on('close', () => {
|
|
100
|
+
connections.delete(conn);
|
|
101
|
+
stopHeartbeatIfEmpty();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { executeRequest } from '../http.js';
|
|
4
|
+
import type { AppConfig } from '../config.js';
|
|
5
|
+
{{#if isAsync}}
|
|
6
|
+
import { createTask, updateTask } from '../task-manager.js';
|
|
7
|
+
{{/if}}
|
|
8
|
+
|
|
9
|
+
const inputSchema = {{{inputSchemaCode}}};
|
|
10
|
+
{{#if outputSchemaCode}}
|
|
11
|
+
const outputSchema = {{{outputSchemaCode}}};
|
|
12
|
+
{{/if}}
|
|
13
|
+
|
|
14
|
+
/** Execute the HTTP request for this tool and return the (optionally filtered) data. */
|
|
15
|
+
async function runRequest(input: Record<string, unknown>, config: AppConfig): Promise<unknown> {
|
|
16
|
+
const url = buildUrl(input, config.baseUrl);
|
|
17
|
+
|
|
18
|
+
const response = await executeRequest({
|
|
19
|
+
method: '{{method}}',
|
|
20
|
+
url,
|
|
21
|
+
{{#if hasRequestBody}}
|
|
22
|
+
body: input.body,
|
|
23
|
+
contentType: '{{requestBodyContentType}}',
|
|
24
|
+
{{/if}}
|
|
25
|
+
headers: {},
|
|
26
|
+
config,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
{{#if jqFilter}}
|
|
30
|
+
// Apply x-mcp-jq-filter to trim response before returning to agent
|
|
31
|
+
let result: unknown = response.data;
|
|
32
|
+
try {
|
|
33
|
+
const parts = '{{{jqFilter}}}'.split('.');
|
|
34
|
+
for (const part of parts) {
|
|
35
|
+
if (part && result != null && typeof result === 'object') {
|
|
36
|
+
result = (result as Record<string, unknown>)[part];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Filter failed — return full response
|
|
41
|
+
result = response.data;
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
{{else}}
|
|
45
|
+
return response.data;
|
|
46
|
+
{{/if}}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function register(server: McpServer, config: AppConfig): void {
|
|
50
|
+
server.registerTool(
|
|
51
|
+
'{{name}}',
|
|
52
|
+
{
|
|
53
|
+
title: '{{title}}',
|
|
54
|
+
description: `{{{description}}}`,
|
|
55
|
+
inputSchema,
|
|
56
|
+
{{#if outputSchemaCode}}
|
|
57
|
+
outputSchema,
|
|
58
|
+
{{/if}}
|
|
59
|
+
{{#if annotations}}
|
|
60
|
+
annotations: {
|
|
61
|
+
{{#if annotations.readOnlyHint}}
|
|
62
|
+
readOnlyHint: true,
|
|
63
|
+
{{/if}}
|
|
64
|
+
{{#if annotations.destructiveHint}}
|
|
65
|
+
destructiveHint: true,
|
|
66
|
+
{{/if}}
|
|
67
|
+
{{#if annotations.idempotentHint}}
|
|
68
|
+
idempotentHint: true,
|
|
69
|
+
{{/if}}
|
|
70
|
+
},
|
|
71
|
+
{{/if}}
|
|
72
|
+
},
|
|
73
|
+
async (input) => {
|
|
74
|
+
{{#if isAsync}}
|
|
75
|
+
// Tasks extension (2026-07-28): a long-running tool answers immediately
|
|
76
|
+
// with a task handle and runs the work in the background; the client
|
|
77
|
+
// drives it with tasks/get / tasks/update / tasks/cancel. VERIFY the
|
|
78
|
+
// returned handle envelope against the final spec.
|
|
79
|
+
const task = createTask('{{name}}');
|
|
80
|
+
void (async () => {
|
|
81
|
+
try {
|
|
82
|
+
const result = await runRequest(input as Record<string, unknown>, config);
|
|
83
|
+
updateTask(task.id, { status: 'completed', result });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
updateTask(task.id, {
|
|
86
|
+
status: 'failed',
|
|
87
|
+
error: error instanceof Error ? error.message : String(error),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
const handle = { taskId: task.id, status: task.status };
|
|
92
|
+
return {
|
|
93
|
+
content: [{ type: 'text' as const, text: JSON.stringify({ task: handle }, null, 2) }],
|
|
94
|
+
structuredContent: { task: handle },
|
|
95
|
+
};
|
|
96
|
+
{{else}}
|
|
97
|
+
try {
|
|
98
|
+
const result = await runRequest(input as Record<string, unknown>, config);
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: 'text' as const,
|
|
103
|
+
text: JSON.stringify(result, null, 2),
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: 'text' as const, text: `Error: ${message}` }],
|
|
111
|
+
isError: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
{{/if}}
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildUrl(
|
|
120
|
+
params: Record<string, unknown>,
|
|
121
|
+
baseUrl: string,
|
|
122
|
+
): string {
|
|
123
|
+
{{{buildUrlBody}}}
|
|
124
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { AppConfig } from '../config.js';
|
|
3
|
+
{{#each tools}}
|
|
4
|
+
import { register as register{{capitalize functionName}} } from './{{fileName}}.js';
|
|
5
|
+
{{/each}}
|
|
6
|
+
|
|
7
|
+
export function registerAllTools(server: McpServer, config: AppConfig): void {
|
|
8
|
+
{{#each tools}}
|
|
9
|
+
register{{capitalize functionName}}(server, config);
|
|
10
|
+
{{/each}}
|
|
11
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import type { AppConfig } from '../../src/config.js';
|
|
5
|
+
import { register } from '../../src/tools/{{fileName}}.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Capture the server.registerTool() call without a real MCP server or any
|
|
9
|
+
* network access. The tool's HTTP handler is never invoked here.
|
|
10
|
+
*/
|
|
11
|
+
function capture(): { name: string; def: Record<string, unknown>; handler: unknown }[] {
|
|
12
|
+
const calls: { name: string; def: Record<string, unknown>; handler: unknown }[] = [];
|
|
13
|
+
const server = {
|
|
14
|
+
registerTool(name: string, def: Record<string, unknown>, handler: unknown) {
|
|
15
|
+
calls.push({ name, def, handler });
|
|
16
|
+
},
|
|
17
|
+
} as unknown as McpServer;
|
|
18
|
+
const config = {
|
|
19
|
+
baseUrl: 'https://example.test',
|
|
20
|
+
maxRetries: 0,
|
|
21
|
+
requestIntervalMs: 0,
|
|
22
|
+
} as unknown as AppConfig;
|
|
23
|
+
register(server, config);
|
|
24
|
+
return calls;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('{{name}}', () => {
|
|
28
|
+
// Tool: {{name}} — {{method}} {{pathTemplate}}
|
|
29
|
+
|
|
30
|
+
it('registers exactly one tool under its name', () => {
|
|
31
|
+
const calls = capture();
|
|
32
|
+
expect(calls).toHaveLength(1);
|
|
33
|
+
expect(calls[0].name).toBe('{{name}}');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('exposes a title and description', () => {
|
|
37
|
+
const { def } = capture()[0];
|
|
38
|
+
expect(def.title).toBeTruthy();
|
|
39
|
+
expect(typeof def.description).toBe('string');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('registers a callable handler', () => {
|
|
43
|
+
expect(typeof capture()[0].handler).toBe('function');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('has a well-formed input schema', () => {
|
|
47
|
+
const raw = capture()[0].def.inputSchema as unknown;
|
|
48
|
+
// inputSchema is a Zod raw shape (record of validators); it must build into
|
|
49
|
+
// a usable object schema and run without throwing.
|
|
50
|
+
const schema =
|
|
51
|
+
raw && typeof (raw as { safeParse?: unknown }).safeParse === 'function'
|
|
52
|
+
? (raw as z.ZodTypeAny)
|
|
53
|
+
: z.object(raw as z.ZodRawShape);
|
|
54
|
+
const result = schema.safeParse({});
|
|
55
|
+
expect(typeof result.success).toBe('boolean');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W3C Trace Context propagation (https://www.w3.org/TR/trace-context/).
|
|
3
|
+
*
|
|
4
|
+
* The HTTP server reads an inbound `traceparent` (joining a distributed trace)
|
|
5
|
+
* or starts a fresh one, binds it to the request via AsyncLocalStorage, and the
|
|
6
|
+
* HTTP executor stamps it onto every upstream API call so the trace spans the
|
|
7
|
+
* API hop. Stdio servers never establish a context, so upstream calls simply
|
|
8
|
+
* carry no trace headers (the store is empty — `traceHeaders()` returns `{}`).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
12
|
+
import { randomBytes } from 'node:crypto';
|
|
13
|
+
|
|
14
|
+
export interface TraceContext {
|
|
15
|
+
/** 32-hex trace id, shared across the whole distributed trace. */
|
|
16
|
+
traceId: string;
|
|
17
|
+
/** 16-hex span id for this server's work (the parent of upstream calls). */
|
|
18
|
+
spanId: string;
|
|
19
|
+
/** 2-hex trace-flags (`01` = sampled). */
|
|
20
|
+
flags: string;
|
|
21
|
+
/** Opaque vendor `tracestate`, propagated verbatim when present. */
|
|
22
|
+
traceState?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const storage = new AsyncLocalStorage<TraceContext>();
|
|
26
|
+
|
|
27
|
+
const TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
|
|
28
|
+
const INVALID_TRACE_ID = '0'.repeat(32);
|
|
29
|
+
|
|
30
|
+
/** Parse a `traceparent` header; returns its trace id + flags, or null if invalid. */
|
|
31
|
+
export function parseTraceparent(
|
|
32
|
+
header: string | undefined,
|
|
33
|
+
): { traceId: string; flags: string } | null {
|
|
34
|
+
if (!header) return null;
|
|
35
|
+
const match = TRACEPARENT_RE.exec(header.trim().toLowerCase());
|
|
36
|
+
if (!match) return null;
|
|
37
|
+
const traceId = match[1];
|
|
38
|
+
const flags = match[3];
|
|
39
|
+
if (traceId === INVALID_TRACE_ID) return null;
|
|
40
|
+
return { traceId, flags };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build the context for a request: continue a valid inbound trace, otherwise
|
|
45
|
+
* start a new one. A fresh span id is always minted for this server's work.
|
|
46
|
+
*/
|
|
47
|
+
export function deriveContext(traceparent?: string, tracestate?: string): TraceContext {
|
|
48
|
+
const parent = parseTraceparent(traceparent);
|
|
49
|
+
return {
|
|
50
|
+
traceId: parent?.traceId ?? randomBytes(16).toString('hex'),
|
|
51
|
+
spanId: randomBytes(8).toString('hex'),
|
|
52
|
+
flags: parent?.flags ?? '01',
|
|
53
|
+
traceState: tracestate?.trim() || undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Format a context as a `traceparent` header value. */
|
|
58
|
+
export function formatTraceparent(ctx: TraceContext): string {
|
|
59
|
+
return `00-${ctx.traceId}-${ctx.spanId}-${ctx.flags}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Run `fn` with `ctx` bound as the active trace context for its async subtree. */
|
|
63
|
+
export function runWithTrace<T>(ctx: TraceContext, fn: () => T): T {
|
|
64
|
+
return storage.run(ctx, fn);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The active trace context, or undefined outside a traced request. */
|
|
68
|
+
export function currentTrace(): TraceContext | undefined {
|
|
69
|
+
return storage.getStore();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Trace headers to stamp on an upstream request, or `{}` when no trace is active. */
|
|
73
|
+
export function traceHeaders(): Record<string, string> {
|
|
74
|
+
const ctx = storage.getStore();
|
|
75
|
+
if (!ctx) return {};
|
|
76
|
+
const headers: Record<string, string> = { traceparent: formatTraceparent(ctx) };
|
|
77
|
+
if (ctx.traceState) headers.tracestate = ctx.traceState;
|
|
78
|
+
return headers;
|
|
79
|
+
}
|