create-ailk 0.17.0 → 0.19.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/LICENSE +6 -5
- package/NOTICE +2 -0
- package/dist/add-module.js +4 -1
- package/dist/category-profile.d.ts +10 -0
- package/dist/category-profile.js +21 -0
- package/dist/derive-architecture.d.ts +5 -1
- package/dist/derive-architecture.js +8 -3
- package/dist/lib/env-catalog.js +5 -9
- package/dist/lib/license-key-files.d.ts +6 -3
- package/dist/lib/license-key-files.js +8 -2
- package/dist/lib/scaffold-render-sweep.d.ts +9 -0
- package/dist/lib/scaffold-render-sweep.js +24 -3
- package/dist/license-set.d.ts +1 -1
- package/dist/license-set.js +13 -8
- package/dist/module-architecture.js +119 -0
- package/dist/programmatic.js +3 -0
- package/dist/spoke-checkout.d.ts +11 -3
- package/dist/spoke-checkout.js +62 -12
- package/dist/starter-grid.d.ts +1 -1
- package/dist/surfaces.js +140 -1
- package/dist/template-manifest.d.ts +4 -3
- package/dist/topology.d.ts +15 -2
- package/dist/topology.js +83 -4
- package/package.json +5 -4
- package/templates/.env.example +3 -3
- package/templates/.github/workflows/ci.yml +5 -0
- package/templates/apps/api/CLAUDE.md +25 -7
- package/templates/apps/api/LICENSE +6 -5
- package/templates/apps/api/NOTICE +2 -0
- package/templates/apps/api/jest.config.cjs +4 -29
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +9 -0
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
- package/templates/apps/api/src/lib/stripe.ts +81 -0
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +266 -39
- package/templates/apps/api/src/middleware/auth.ts +206 -46
- package/templates/apps/api/src/routes/leads/index.ts +4 -0
- package/templates/apps/api/src/server.ts +107 -0
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +58 -0
- package/templates/apps/api/src/services/lead-routing.ts +30 -2
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/.env.example +10 -0
- package/templates/apps/mcp/CLAUDE.md +58 -7
- package/templates/apps/mcp/LICENSE +6 -5
- package/templates/apps/mcp/NOTICE +2 -0
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
- package/templates/apps/mcp/__tests__/app-entry.test.ts +325 -0
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
- package/templates/apps/mcp/__tests__/boot.test.ts +280 -0
- package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
- package/templates/apps/mcp/__tests__/index.test.ts +86 -47
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +5 -6
- package/templates/apps/mcp/__tests__/lib/content-root.test.ts +93 -0
- package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
- package/templates/apps/mcp/__tests__/server.test.ts +63 -6
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
- package/templates/apps/mcp/jest.config.cjs +3 -44
- package/templates/apps/mcp/src/abuse/index.ts +117 -21
- package/templates/apps/mcp/src/app.ts +32 -0
- package/templates/apps/mcp/src/boot.ts +167 -0
- package/templates/apps/mcp/src/http-transport.ts +132 -88
- package/templates/apps/mcp/src/index.ts +19 -126
- package/templates/apps/mcp/src/lib/audit-single-page.ts +7 -6
- package/templates/apps/mcp/src/lib/content-adapter.ts +34 -5
- package/templates/apps/mcp/src/lib/topology.generated.ts +3 -0
- package/templates/apps/mcp/src/request-context.ts +52 -0
- package/templates/apps/mcp/src/server.ts +10 -2
- package/templates/apps/mcp/src/serverless-app.ts +88 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
- package/templates/apps/mcp/src/tools/get_business_hours.ts +6 -5
- package/templates/apps/mcp/src/tools/get_faq.ts +6 -5
- package/templates/apps/mcp/src/tools/get_locations.ts +6 -5
- package/templates/apps/mcp/src/tools/get_offerings.ts +6 -5
- package/templates/apps/mcp/src/tools/get_pricing.ts +6 -5
- package/templates/apps/mcp/src/tools/get_site_info.ts +6 -5
- package/templates/apps/mcp/src/tools/list_pages.ts +8 -7
- package/templates/apps/mcp/src/tools/read_content.ts +7 -7
- package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
- package/templates/apps/mcp/src/tools/search_content.ts +6 -5
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
- package/templates/apps/mcp/tsconfig.json +2 -0
- package/templates/apps/web/LICENSE +6 -5
- package/templates/apps/web/NOTICE +2 -0
- package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +16 -4
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +3 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +568 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +272 -42
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +115 -16
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +126 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +139 -0
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +20 -390
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
- package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
- package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +114 -50
- package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +25 -32
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
- package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +177 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
- package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +131 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
- package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
- package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
- package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +4 -1
- package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
- package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
- package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
- package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
- package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
- package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +80 -0
- package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
- package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +108 -0
- package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
- package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
- package/templates/apps/web/app/[locale]/layout.tsx +23 -4
- package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
- package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
- package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
- package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
- package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
- package/templates/apps/web/app/robots.txt/route.ts +10 -4
- package/templates/apps/web/auth.ts +5 -1
- package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
- package/templates/apps/web/lib/__tests__/site-brand.test.ts +42 -0
- package/templates/apps/web/lib/account-config.ts +188 -52
- package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
- package/templates/apps/web/lib/hub-session/index.ts +1 -0
- package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
- package/templates/apps/web/lib/hub-session/seal.ts +13 -9
- package/templates/apps/web/lib/site-brand.tsx +22 -6
- package/templates/apps/web/lib/topology.generated.ts +27 -0
- package/templates/apps/web/messages/en.json +156 -35
- package/templates/database/CHANGELOG.md +21 -0
- package/templates/database/LICENSE +6 -5
- package/templates/database/NOTICE +2 -0
- package/templates/database/README.md +1 -1
- package/templates/database/__tests__/member-area.db.test.ts +15 -3
- package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
- package/templates/database/__tests__/member-contract.db.test.ts +413 -0
- package/templates/database/__tests__/seed.test.ts +54 -2
- package/templates/database/inbox/schema.prisma +90 -15
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
- package/templates/database/package.json +3 -2
- package/templates/database/seed.ts +34 -2
- package/versions.json +36 -36
- package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
- package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
- package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
- package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
- package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
- package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
- package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
- package/templates/apps/api/src/__tests__/server.test.ts +0 -591
- package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
- package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
- package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
- package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
- package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
- package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
- package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
- package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
- package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
- package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
- package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
- package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
- package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
- package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
- package/templates/database/__tests__/build.test.ts +0 -131
|
@@ -7,15 +7,22 @@
|
|
|
7
7
|
* - abuse heuristics — UA deny list, honeypot, email validation, daily cap
|
|
8
8
|
*
|
|
9
9
|
* Decision references:
|
|
10
|
-
* Decision 1 — same binary, env-var transport selection (apps/mcp/src/index.ts
|
|
10
|
+
* Decision 1 — same binary, env-var transport selection (apps/mcp/src/index.ts;
|
|
11
|
+
* the Vercel entry, apps/mcp/src/app.ts, serves HTTP only — #8070)
|
|
11
12
|
* Decision 2 — configurable path, default /mcp, standalone root-mounted
|
|
12
13
|
* Decision 3 — @fastify/rate-limit per-IP token bucket
|
|
13
14
|
* Decision 4 — in-process abuse heuristics for write tools
|
|
14
15
|
* Decision 5 — @fastify/cors open reads, allowlist writes
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* Two modes, chosen by `config.stateless` (#8070 D1; the entries read it from
|
|
18
|
+
* `MCP_HTTP_STATELESS` through `readStatelessEnv`, src/boot.ts):
|
|
19
|
+
* - stateful (the long-running entry's default): an `initialize` creates a
|
|
20
|
+
* StreamableHTTPServerTransport + McpServer pair kept in an in-memory map
|
|
21
|
+
* keyed by its Mcp-Session-Id. Sessions live in ONE process's memory and
|
|
22
|
+
* are not persisted across restarts, so this mode needs a single instance.
|
|
23
|
+
* - stateless (the Vercel entry's default): every POST gets a fresh
|
|
24
|
+
* transport + server and no session ID; GET and DELETE answer 400. Any
|
|
25
|
+
* instance can answer any request.
|
|
19
26
|
*
|
|
20
27
|
* The anonymous `config.path` never reads `Authorization`. Beside it,
|
|
21
28
|
* `${config.path}/workspace` serves the signed-in workspace tools (#3591 D15):
|
|
@@ -44,6 +51,7 @@ import {
|
|
|
44
51
|
AbuseReasonCode,
|
|
45
52
|
type AbuseConfig,
|
|
46
53
|
} from "./abuse/index.js";
|
|
54
|
+
import { trustedCallerAddress } from "./request-context.js";
|
|
47
55
|
import { createMcpServer } from "./server.js";
|
|
48
56
|
import { createWorkspaceMcpServer } from "./workspace-server.js";
|
|
49
57
|
|
|
@@ -78,6 +86,13 @@ export const ABUSE_REFUSAL_STATUS: Record<AbuseReasonCode, 403 | 422> = {
|
|
|
78
86
|
* worth money to abuse, so it takes the write branch — the origin allow-list,
|
|
79
87
|
* the write CORS posture, and `abuseGuard.validate()` — rather than the read
|
|
80
88
|
* branch's UA check alone.
|
|
89
|
+
*
|
|
90
|
+
* `create_booking` and `submit_flow_step` joined in #8071: each forwards
|
|
91
|
+
* caller-chosen data to the hub — a real booking on the seller's calendar
|
|
92
|
+
* that emails the attendee an invite, and a flow step whose completing call
|
|
93
|
+
* can create a Lead — so they take the same branch, each on its own daily
|
|
94
|
+
* cap. `__tests__/write-tool-guard.test.ts` fails when a catalog tool that
|
|
95
|
+
* sends a non-GET request to the hub is missing from this set.
|
|
81
96
|
*/
|
|
82
97
|
// Any name added here MUST be enforced through the batch-aware path in
|
|
83
98
|
// writeToolCalls() below — a request body can be a JSON-RPC ARRAY, and a
|
|
@@ -89,6 +104,8 @@ export const WRITE_TOOLS = new Set([
|
|
|
89
104
|
"subscribe_newsletter",
|
|
90
105
|
"request_callback",
|
|
91
106
|
"start_purchase",
|
|
107
|
+
"create_booking",
|
|
108
|
+
"submit_flow_step",
|
|
92
109
|
]);
|
|
93
110
|
|
|
94
111
|
export interface HttpTransportCorsConfig {
|
|
@@ -118,9 +135,10 @@ export interface HttpTransportConfig {
|
|
|
118
135
|
abuse: AbuseConfig;
|
|
119
136
|
/**
|
|
120
137
|
* When true, use stateless transport mode: no session IDs, every request
|
|
121
|
-
* creates a fresh McpServer + transport.
|
|
122
|
-
*
|
|
123
|
-
* Default: false (stateful).
|
|
138
|
+
* creates a fresh McpServer + transport. Needed on a host that runs several
|
|
139
|
+
* instances (the Vercel entry), and used by tests.
|
|
140
|
+
* Default: false (stateful). The entries set it from `MCP_HTTP_STATELESS`
|
|
141
|
+
* (`readStatelessEnv`, src/boot.ts — #8070 D1).
|
|
124
142
|
*/
|
|
125
143
|
stateless?: boolean;
|
|
126
144
|
}
|
|
@@ -274,24 +292,25 @@ function registerWorkspaceRoute(
|
|
|
274
292
|
}
|
|
275
293
|
|
|
276
294
|
/**
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
295
|
+
* Register the MCP HTTP transport at `config.path` on an existing Fastify
|
|
296
|
+
* instance: CORS, the per-IP rate limit, the abuse guard, the anonymous MCP
|
|
297
|
+
* route and the signed-in workspace route (#8070 D2). The caller owns the
|
|
298
|
+
* instance — and so its options, such as the Vercel entry's `trustProxy`
|
|
299
|
+
* (src/serverless-app.ts).
|
|
280
300
|
*
|
|
301
|
+
* @param app - The Fastify instance to register on (not yet listening)
|
|
281
302
|
* @param config - HTTP transport configuration
|
|
282
|
-
* @returns Configured Fastify instance (ready to listen or inject)
|
|
283
303
|
*/
|
|
284
|
-
export async function
|
|
304
|
+
export async function registerHttpTransport(
|
|
305
|
+
app: FastifyInstance,
|
|
285
306
|
config: HttpTransportConfig,
|
|
286
|
-
): Promise<
|
|
307
|
+
): Promise<void> {
|
|
287
308
|
const mcpPath = config.path ?? "/mcp";
|
|
288
309
|
const abuseGuard = new AbuseGuard(config.abuse);
|
|
289
310
|
|
|
290
311
|
// Active transports keyed by session ID (stateful mode only).
|
|
291
312
|
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
292
313
|
|
|
293
|
-
const app = Fastify({ logger: false });
|
|
294
|
-
|
|
295
314
|
// ── CORS ──────────────────────────────────────────────────────────────────
|
|
296
315
|
// Read tools: open (origin: true — reflect the caller's Origin).
|
|
297
316
|
// Write tools: enforced per-request in the preHandler below.
|
|
@@ -373,83 +392,93 @@ export async function buildHttpTransportApp(
|
|
|
373
392
|
}
|
|
374
393
|
}
|
|
375
394
|
},
|
|
376
|
-
handler: async (request, reply) =>
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
395
|
+
handler: async (request, reply) =>
|
|
396
|
+
// The lead-write tools read this address (#8073) through
|
|
397
|
+
// `visitorForwardHeadersForThisCall()` while the SDK dispatches their
|
|
398
|
+
// call, somewhere inside `transport.handleRequest()` below — the whole
|
|
399
|
+
// handler body runs inside the store so every path reaches it.
|
|
400
|
+
trustedCallerAddress.run(request.ip, async () => {
|
|
401
|
+
const body = request.body as unknown;
|
|
402
|
+
const sessionId = request.headers["mcp-session-id"] as
|
|
403
|
+
| string
|
|
404
|
+
| undefined;
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
let transport: StreamableHTTPServerTransport;
|
|
408
|
+
|
|
409
|
+
if (config.stateless) {
|
|
410
|
+
// Stateless mode: each request gets a fresh transport + server.
|
|
411
|
+
// enableJsonResponse: true ensures plain JSON responses (no SSE).
|
|
412
|
+
// sessionIdGenerator is intentionally omitted (stateless = no sessions).
|
|
413
|
+
transport = new StreamableHTTPServerTransport({
|
|
414
|
+
enableJsonResponse: true,
|
|
415
|
+
});
|
|
416
|
+
const server = createMcpServer();
|
|
417
|
+
// Cast needed: SDK's StreamableHTTPServerTransport.onclose is typed
|
|
418
|
+
// as `(() => void) | undefined`, which conflicts with Transport.onclose
|
|
419
|
+
// under exactOptionalPropertyTypes: true. The runtime behaviour is
|
|
420
|
+
// identical; the cast is safe.
|
|
421
|
+
await server.connect(transport as unknown as Transport);
|
|
422
|
+
try {
|
|
423
|
+
await transport.handleRequest(request.raw, reply.raw, body);
|
|
424
|
+
} finally {
|
|
425
|
+
// Close the per-request transport + server to release internal
|
|
426
|
+
// handles. Without this, the MCP SDK's transport leaves open
|
|
427
|
+
// sockets that surface as `socket.destroySoon is not a function`
|
|
428
|
+
// during Jest teardown on Node 20.x (@hono/node-server compat).
|
|
429
|
+
await transport.close().catch(() => undefined);
|
|
430
|
+
await server.close().catch(() => undefined);
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
405
433
|
}
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (sessionId && transports.has(sessionId)) {
|
|
410
|
-
// Existing session — reuse transport
|
|
411
|
-
transport = transports.get(sessionId)!;
|
|
412
|
-
} else if (!sessionId && isInitializeRequest(body)) {
|
|
413
|
-
// New initialization — create a stateful transport + fresh server
|
|
414
|
-
transport = new StreamableHTTPServerTransport({
|
|
415
|
-
sessionIdGenerator: () => randomUUID(),
|
|
416
|
-
onsessioninitialized: (sid) => {
|
|
417
|
-
transports.set(sid, transport);
|
|
418
|
-
},
|
|
419
|
-
});
|
|
420
434
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
435
|
+
if (sessionId && transports.has(sessionId)) {
|
|
436
|
+
// Existing session — reuse transport
|
|
437
|
+
transport = transports.get(sessionId)!;
|
|
438
|
+
} else if (!sessionId && isInitializeRequest(body)) {
|
|
439
|
+
// New initialization — create a stateful transport + fresh server
|
|
440
|
+
transport = new StreamableHTTPServerTransport({
|
|
441
|
+
sessionIdGenerator: () => randomUUID(),
|
|
442
|
+
onsessioninitialized: (sid) => {
|
|
443
|
+
transports.set(sid, transport);
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
transport.onclose = () => {
|
|
448
|
+
const sid = transport.sessionId;
|
|
449
|
+
if (sid) transports.delete(sid);
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
const server = createMcpServer();
|
|
453
|
+
// Cast needed: SDK's StreamableHTTPServerTransport.onclose is typed
|
|
454
|
+
// as `(() => void) | undefined`, which conflicts with Transport.onclose
|
|
455
|
+
// under exactOptionalPropertyTypes: true. The runtime behaviour is
|
|
456
|
+
// identical; the cast is safe.
|
|
457
|
+
await server.connect(transport as unknown as Transport);
|
|
458
|
+
} else {
|
|
459
|
+
reply.code(400).send({
|
|
460
|
+
jsonrpc: "2.0",
|
|
461
|
+
error: {
|
|
462
|
+
code: -32000,
|
|
463
|
+
message: "Bad Request: no valid session",
|
|
464
|
+
},
|
|
465
|
+
id: null,
|
|
466
|
+
});
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
440
469
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
470
|
+
await transport.handleRequest(request.raw, reply.raw, body);
|
|
471
|
+
} catch (err) {
|
|
472
|
+
if (!reply.sent) {
|
|
473
|
+
reply.code(500).send({
|
|
474
|
+
jsonrpc: "2.0",
|
|
475
|
+
error: { code: -32603, message: "Internal server error" },
|
|
476
|
+
id: null,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
throw err;
|
|
449
480
|
}
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
},
|
|
481
|
+
}),
|
|
453
482
|
});
|
|
454
483
|
|
|
455
484
|
// DELETE — session termination
|
|
@@ -465,7 +494,22 @@ export async function buildHttpTransportApp(
|
|
|
465
494
|
|
|
466
495
|
// ── Signed-in workspace route (#3591 D15) ─────────────────────────────────
|
|
467
496
|
registerWorkspaceRoute(app, `${mcpPath}/workspace`, abuseGuard);
|
|
497
|
+
}
|
|
468
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Build and return a configured Fastify instance that exposes the MCP HTTP
|
|
501
|
+
* transport at `config.path`. The instance is not yet listening — callers
|
|
502
|
+
* call `.listen()` or `.inject()` (tests). Sets no `trustProxy`, so
|
|
503
|
+
* `request.ip` is the socket's address.
|
|
504
|
+
*
|
|
505
|
+
* @param config - HTTP transport configuration
|
|
506
|
+
* @returns Configured Fastify instance (ready to listen or inject)
|
|
507
|
+
*/
|
|
508
|
+
export async function buildHttpTransportApp(
|
|
509
|
+
config: HttpTransportConfig,
|
|
510
|
+
): Promise<FastifyInstance> {
|
|
511
|
+
const app = Fastify({ logger: false });
|
|
512
|
+
await registerHttpTransport(app, config);
|
|
469
513
|
return app;
|
|
470
514
|
}
|
|
471
515
|
|
|
@@ -9,155 +9,48 @@
|
|
|
9
9
|
* HTTP transport configuration (all optional, shown with defaults):
|
|
10
10
|
* MCP_HTTP_PORT=8080
|
|
11
11
|
* MCP_HTTP_PATH=/mcp
|
|
12
|
+
* MCP_HTTP_STATELESS=false (true = no sessions; any instance answers)
|
|
12
13
|
* MCP_RATE_LIMIT_READ_PER_MIN=60
|
|
13
14
|
* MCP_RATE_LIMIT_WRITE_PER_MIN=10
|
|
14
15
|
* MCP_CORS_WRITE_ALLOWED_ORIGINS= (empty = no cross-origin writes)
|
|
15
16
|
* MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP=50
|
|
16
17
|
* MCP_ABUSE_START_PURCHASE_DAILY_CAP=20
|
|
18
|
+
* MCP_ABUSE_CREATE_BOOKING_DAILY_CAP=20
|
|
19
|
+
* MCP_ABUSE_SUBMIT_FLOW_STEP_DAILY_CAP=50
|
|
17
20
|
* MCP_ABUSE_BLOCKED_USER_AGENTS= (comma-separated substrings)
|
|
18
21
|
*
|
|
19
22
|
* Content adapter (all optional; `CONTENT_ADAPTER` default: mdx):
|
|
20
23
|
* CONTENT_ADAPTER=mdx|projection|database
|
|
21
24
|
* DATABASE_URL= (required only when CONTENT_ADAPTER=database)
|
|
25
|
+
*
|
|
26
|
+
* The boot sequence and the HTTP env reading live in `boot.ts`, shared with
|
|
27
|
+
* the Vercel entry `app.ts` (#8070 D2).
|
|
22
28
|
*/
|
|
23
29
|
|
|
24
30
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
initPublicWorkspace,
|
|
30
|
-
registerLicenseKeyStore,
|
|
31
|
-
} from "./lib/topology.generated.js";
|
|
32
|
-
|
|
33
|
-
// Resolves the MCP server's own workspace ONCE, before either transport
|
|
34
|
-
// branch below — a no-op unless CONTENT_ADAPTER=database (#5876 spec D4/D6).
|
|
35
|
-
// This is the one `await` that covers both arms: `getMcpContentAdapter()`
|
|
36
|
-
// (content-adapter.ts) stays synchronous and reads the memoised result.
|
|
37
|
-
//
|
|
38
|
-
// A REJECTION here (a transient DB error — unreachable at boot, a migration
|
|
39
|
-
// in progress, a bad DATABASE_URL) is caught rather than left to crash the
|
|
40
|
-
// process: every OTHER tool (lead capture, scheduling, …) has nothing to do
|
|
41
|
-
// with the content adapter and must keep serving. The module's un-resolved
|
|
42
|
-
// `{ kind: "not-required" }` state is left in place on a caught failure —
|
|
43
|
-
// the SAME fail-closed shape an ambiguous workspace count already produces
|
|
44
|
-
// (D5) — so content tools serve their empty/not-found shape rather than the
|
|
45
|
-
// whole server going dark over one adapter's connectivity.
|
|
46
|
-
try {
|
|
47
|
-
await initPublicWorkspace();
|
|
48
|
-
} catch (err) {
|
|
49
|
-
console.error(
|
|
50
|
-
"[mcp] initPublicWorkspace() failed — content tools will serve their " +
|
|
51
|
-
"fail-closed empty shape under CONTENT_ADAPTER=database until this " +
|
|
52
|
-
"resolves; every other tool is unaffected:",
|
|
53
|
-
err,
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Registers the renewed licence key's durable home (#6677 DD-6) BEFORE the
|
|
58
|
-
// commerce claim below memoises the licence: with a database, the key a
|
|
59
|
-
// sibling app renewed lives in one SiteSetting row, and the claim must read
|
|
60
|
-
// it. Never throws; without DATABASE_URL the file store stays in place.
|
|
61
|
-
await registerLicenseKeyStore();
|
|
62
|
-
|
|
63
|
-
// Resolves the deployment's `commerce` add-on claim at boot, before either
|
|
64
|
-
// transport branch — the sibling of the workspace resolution above and the
|
|
65
|
-
// same #5876 idiom (#5971 spec D2). Without this await the memoised claim is
|
|
66
|
-
// never resolved and `start_purchase` is invisible on a KEYED site too. The
|
|
67
|
-
// same single licence read now also resolves the licence features the
|
|
68
|
-
// deployment holds, which gate the five `feature`-tagged tools (#7136).
|
|
69
|
-
// The re-read loop below runs every `MCP_CLAIM_REFRESH_MS` thereafter
|
|
70
|
-
// (#7156), so the server's origin agrees with the manifest route's origin
|
|
71
|
-
// response within that bound; the route answers
|
|
72
|
-
// `cache-control: public, max-age=86400`, so a downstream cache may serve an
|
|
73
|
-
// older manifest for up to 24 hours.
|
|
74
|
-
//
|
|
75
|
-
// A REJECTION is caught for the same reason: a licence read that fails must
|
|
76
|
-
// not take the other 19 tools down. `initMcpCommerceClaim()` already resolves
|
|
77
|
-
// to the fail-closed "unclaimed" internally, so this catch is belt-and-braces
|
|
78
|
-
// for anything thrown outside its own try.
|
|
79
|
-
try {
|
|
80
|
-
await initMcpCommerceClaim();
|
|
81
|
-
} catch (err) {
|
|
82
|
-
console.error(
|
|
83
|
-
"[mcp] initMcpCommerceClaim() failed — start_purchase stays unregistered " +
|
|
84
|
-
"(fail-closed) until this resolves; every other tool is unaffected:",
|
|
85
|
-
err,
|
|
86
|
-
);
|
|
87
|
-
}
|
|
31
|
+
bootMcp,
|
|
32
|
+
httpTransportConfigFromEnv,
|
|
33
|
+
readStatelessEnv,
|
|
34
|
+
} from "./boot.js";
|
|
88
35
|
|
|
89
|
-
//
|
|
90
|
-
// the
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// The one-release warning cycle (#7136): once per process, whichever
|
|
94
|
-
// transport follows, one stderr line per tool served without its licence
|
|
95
|
-
// feature. After the claim resolves, or it would warn for every tool.
|
|
96
|
-
//
|
|
97
|
-
// A throw here must not stop the server (#7156 spec D2, L1) — only the call
|
|
98
|
-
// is guarded, not the import: if `./server.js` fails to load, both transport
|
|
99
|
-
// branches load it again and the server could not start anyway.
|
|
100
|
-
const { reportUnlicensedProTools } = await import("./server.js");
|
|
101
|
-
try {
|
|
102
|
-
reportUnlicensedProTools();
|
|
103
|
-
} catch (err) {
|
|
104
|
-
console.error(
|
|
105
|
-
"[mcp] reportUnlicensedProTools() failed — the licence boot warning was " +
|
|
106
|
-
"not written; every tool is served as registered:",
|
|
107
|
-
err,
|
|
108
|
-
);
|
|
109
|
-
}
|
|
36
|
+
// The workspace, the licence-key store, the commerce claim, its re-read loop
|
|
37
|
+
// and the licence boot warning — before either transport branch below.
|
|
38
|
+
await bootMcp();
|
|
110
39
|
|
|
111
40
|
const transport = process.env["MCP_TRANSPORT"] ?? "stdio";
|
|
112
41
|
|
|
113
42
|
if (transport === "http") {
|
|
114
43
|
const { startHttpTransport } = await import("./http-transport.js");
|
|
115
44
|
|
|
116
|
-
const
|
|
117
|
-
const path = process.env["MCP_HTTP_PATH"] ?? "/mcp";
|
|
118
|
-
const readPerMin = parseInt(
|
|
119
|
-
process.env["MCP_RATE_LIMIT_READ_PER_MIN"] ?? "60",
|
|
120
|
-
10,
|
|
121
|
-
);
|
|
122
|
-
const writePerMin = parseInt(
|
|
123
|
-
process.env["MCP_RATE_LIMIT_WRITE_PER_MIN"] ?? "10",
|
|
124
|
-
10,
|
|
125
|
-
);
|
|
126
|
-
const writeOriginsRaw = process.env["MCP_CORS_WRITE_ALLOWED_ORIGINS"] ?? "";
|
|
127
|
-
const writeAllowedOrigins = writeOriginsRaw
|
|
128
|
-
? writeOriginsRaw.split(",").map((o) => o.trim())
|
|
129
|
-
: [];
|
|
130
|
-
// Both caps go through readPositiveIntEnv rather than a bare parseInt: a
|
|
131
|
-
// BLANK env var would otherwise yield NaN and silently disable the cap
|
|
132
|
-
// (#5971 security review round 2).
|
|
133
|
-
const {
|
|
134
|
-
readPositiveIntEnv,
|
|
135
|
-
DEFAULT_CAPTURE_LEAD_DAILY_CAP,
|
|
136
|
-
DEFAULT_START_PURCHASE_DAILY_CAP,
|
|
137
|
-
} = await import("./abuse/index.js");
|
|
138
|
-
const captureLeadDailyCapPerIp = readPositiveIntEnv(
|
|
139
|
-
"MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP",
|
|
140
|
-
DEFAULT_CAPTURE_LEAD_DAILY_CAP,
|
|
141
|
-
);
|
|
142
|
-
const startPurchaseDailyCapPerIp = readPositiveIntEnv(
|
|
143
|
-
"MCP_ABUSE_START_PURCHASE_DAILY_CAP",
|
|
144
|
-
DEFAULT_START_PURCHASE_DAILY_CAP,
|
|
145
|
-
);
|
|
146
|
-
const blockedUserAgents = process.env["MCP_ABUSE_BLOCKED_USER_AGENTS"] ?? "";
|
|
147
|
-
|
|
45
|
+
const config = httpTransportConfigFromEnv();
|
|
148
46
|
await startHttpTransport({
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
rateLimits: { readPerMin, writePerMin },
|
|
152
|
-
cors: { writeAllowedOrigins },
|
|
153
|
-
abuse: {
|
|
154
|
-
captureLeadDailyCapPerIp,
|
|
155
|
-
startPurchaseDailyCapPerIp,
|
|
156
|
-
blockedUserAgents,
|
|
157
|
-
},
|
|
47
|
+
...config,
|
|
48
|
+
stateless: readStatelessEnv(false),
|
|
158
49
|
});
|
|
159
50
|
|
|
160
|
-
console.log(
|
|
51
|
+
console.log(
|
|
52
|
+
`MCP HTTP transport listening on port ${config.port} at ${config.path}`,
|
|
53
|
+
);
|
|
161
54
|
} else {
|
|
162
55
|
// Default: stdio transport
|
|
163
56
|
const { StdioServerTransport } =
|
|
@@ -20,8 +20,6 @@
|
|
|
20
20
|
* Session/scope: none — read-only filesystem access via content adapter.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { join } from "node:path";
|
|
24
|
-
|
|
25
23
|
import {
|
|
26
24
|
READINESS_SCAN_TYPES,
|
|
27
25
|
chassisSectionIds,
|
|
@@ -44,7 +42,10 @@ import {
|
|
|
44
42
|
pageTypeRegistry,
|
|
45
43
|
} from "@working-theory/schema";
|
|
46
44
|
|
|
47
|
-
import {
|
|
45
|
+
import {
|
|
46
|
+
getMcpContentAdapter,
|
|
47
|
+
mcpContentRoot,
|
|
48
|
+
} from "./content-adapter.js";
|
|
48
49
|
import { resolveLocale } from "./resolve-locale.js";
|
|
49
50
|
import { readStripeTaxSignal } from "./stripe-tax-signal.js";
|
|
50
51
|
|
|
@@ -221,10 +222,10 @@ async function scanSitePages(
|
|
|
221
222
|
* Builds an `AuditInput` for a single page.
|
|
222
223
|
*
|
|
223
224
|
* Accepts an optional `contentRoot` override for testing. When omitted,
|
|
224
|
-
* resolves
|
|
225
|
+
* resolves it through `mcpContentRoot()` (#8070 D3).
|
|
225
226
|
*
|
|
226
227
|
* `projectRoot` is the matching seam for the project.yaml the Stripe-Tax signal
|
|
227
|
-
* is read from (#5949 D9); it
|
|
228
|
+
* is read from (#5949 D9); it defaults to the process cwd.
|
|
228
229
|
*/
|
|
229
230
|
export async function buildSinglePageAuditInput(
|
|
230
231
|
input: SinglePageInput,
|
|
@@ -244,7 +245,7 @@ export async function buildSinglePageAuditInput(
|
|
|
244
245
|
throw new Error("Either slug or url is required.");
|
|
245
246
|
}
|
|
246
247
|
|
|
247
|
-
const root = contentRoot ??
|
|
248
|
+
const root = contentRoot ?? mcpContentRoot();
|
|
248
249
|
const locale = resolveLocale(input.locale);
|
|
249
250
|
const adapter = getMcpContentAdapter({ contentRoot: root, locale });
|
|
250
251
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* `projection` paths below are byte-identical to before this issue — no
|
|
16
16
|
* `workspaceId`, no `await import()`, no workspace read (D6, NFR-1/NFR-2). On
|
|
17
17
|
* `database`, this seam reads `mcpPublicWorkspace()` — the resolution
|
|
18
|
-
* `apps/mcp/src/
|
|
18
|
+
* `bootMcp()` (`apps/mcp/src/boot.ts`) already awaited once at bootstrap
|
|
19
19
|
* (`public-workspace.ts`, D4) — and forwards `workspaceId` + `prisma` to the
|
|
20
20
|
* resolver on a resolved deployment; every other arm (unresolved, or a tool
|
|
21
21
|
* invoked before bootstrap resolved) returns the fail-closed
|
|
@@ -27,10 +27,12 @@
|
|
|
27
27
|
*
|
|
28
28
|
* The `contentRoot` argument is the test-override seam every tool already
|
|
29
29
|
* exposes as its optional second parameter — tests inject a fixture root
|
|
30
|
-
* through it, and that keeps working unchanged.
|
|
30
|
+
* through it, and that keeps working unchanged. Absent, every tool and
|
|
31
|
+
* `audit-single-page` take {@link mcpContentRoot} (#8070 D3).
|
|
31
32
|
*/
|
|
32
33
|
|
|
33
|
-
import {
|
|
34
|
+
import { existsSync } from "node:fs";
|
|
35
|
+
import { join, resolve } from "node:path";
|
|
34
36
|
|
|
35
37
|
import {
|
|
36
38
|
resolveContentAdapter,
|
|
@@ -46,12 +48,39 @@ import {
|
|
|
46
48
|
import { publicWorkspace } from "./topology.generated.js";
|
|
47
49
|
|
|
48
50
|
export interface McpContentAdapterOptions {
|
|
49
|
-
/** Filesystem content root. Defaults to
|
|
51
|
+
/** Filesystem content root. Defaults to {@link mcpContentRoot}. */
|
|
50
52
|
contentRoot?: string;
|
|
51
53
|
/** Locale already resolved against the project allowlist (`resolveLocale()`). */
|
|
52
54
|
locale?: string;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The MCP server's content root — the ONE place apps/mcp derives it (#8070 D3).
|
|
59
|
+
*
|
|
60
|
+
* In order: `AILK_CONTENT_ROOT` when set and non-empty (absolute, or resolved
|
|
61
|
+
* against the working directory); else `<cwd>/content` when it exists (the
|
|
62
|
+
* server started from the repo root); else `<cwd>/../../content` when it
|
|
63
|
+
* exists (the server started from `apps/mcp` — a Vercel project whose root
|
|
64
|
+
* directory is `apps/mcp`); else `<cwd>/content`, the prior default, so a
|
|
65
|
+
* missing root keeps serving the same empty result it always did.
|
|
66
|
+
*
|
|
67
|
+
* Read on every call, never cached: it is two `existsSync` probes, and a
|
|
68
|
+
* cached value would pin the first working directory a test or a host used.
|
|
69
|
+
*/
|
|
70
|
+
export function mcpContentRoot(): string {
|
|
71
|
+
const cwd = process.cwd();
|
|
72
|
+
const override = process.env["AILK_CONTENT_ROOT"]?.trim();
|
|
73
|
+
if (override) return resolve(cwd, override);
|
|
74
|
+
|
|
75
|
+
const local = join(cwd, "content");
|
|
76
|
+
if (existsSync(local)) return local;
|
|
77
|
+
|
|
78
|
+
const repo = join(cwd, "..", "..", "content");
|
|
79
|
+
if (existsSync(repo)) return repo;
|
|
80
|
+
|
|
81
|
+
return local;
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
/**
|
|
56
85
|
* Returns the `ContentAdapter` the environment selects, rooted at the caller's
|
|
57
86
|
* content root. The resolver memoises per distinct option set, so this is safe
|
|
@@ -60,7 +89,7 @@ export interface McpContentAdapterOptions {
|
|
|
60
89
|
export function getMcpContentAdapter(
|
|
61
90
|
options: McpContentAdapterOptions = {},
|
|
62
91
|
): ContentAdapter {
|
|
63
|
-
const contentRoot = options.contentRoot ??
|
|
92
|
+
const contentRoot = options.contentRoot ?? mcpContentRoot();
|
|
64
93
|
const defaultLocale = options.locale;
|
|
65
94
|
|
|
66
95
|
if (resolvedAdapterKind() !== "database") {
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// every scaffold rewrites it. A hub resolves its workspace through the
|
|
5
5
|
// database; a spoke (`--topology spoke`) has none, so its content tools
|
|
6
6
|
// read the spoke's own MDX and its writes go to the hub at AILK_API_URL.
|
|
7
|
+
// UNSERVED_MCP_TOOLS names the tools this server does not register on this
|
|
8
|
+
// topology (#8070).
|
|
7
9
|
import {
|
|
8
10
|
initMcpPublicWorkspace,
|
|
9
11
|
mcpPublicWorkspace,
|
|
@@ -14,6 +16,7 @@ export { registerLicenseKeyStore } from "./license-key-store.js";
|
|
|
14
16
|
export type { McpWorkspaceResolution };
|
|
15
17
|
|
|
16
18
|
export const TOPOLOGY: "hub" | "spoke" = "hub";
|
|
19
|
+
export const UNSERVED_MCP_TOOLS: readonly string[] = [];
|
|
17
20
|
|
|
18
21
|
export async function initPublicWorkspace(): Promise<void> {
|
|
19
22
|
await initMcpPublicWorkspace();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-request context for @working-theory/mcp, shared by the HTTP transport
|
|
3
|
+
* and the MCP lead-write tools (#8073).
|
|
4
|
+
*
|
|
5
|
+
* Deliberately dependency-free beyond `@working-theory/api-client` (already
|
|
6
|
+
* loaded by every tool): `http-transport.ts` imports Fastify and the full
|
|
7
|
+
* HTTP stack, and `src/index.ts`'s stdio branch dynamic-imports `server.js`
|
|
8
|
+
* specifically to avoid loading that stack for a local stdio caller. A tool
|
|
9
|
+
* importing `trustedCallerAddress` straight from `http-transport.ts` would
|
|
10
|
+
* pull Fastify into every stdio boot and, worse, close an import cycle
|
|
11
|
+
* (`http-transport.ts` → `server.ts` → `tools/*.ts` → `http-transport.ts`).
|
|
12
|
+
* This module sits below both: `http-transport.ts` populates the store,
|
|
13
|
+
* the tools read it, neither imports the other.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
|
+
|
|
18
|
+
import { visitorForwardHeaders } from "@working-theory/api-client";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The caller address the transport trusted for the POST request currently
|
|
22
|
+
* being handled — `request.ip`, Fastify's own `trustProxy`-resolved value
|
|
23
|
+
* (the Vercel entry trusts exactly one hop, #8070 D5), never the raw,
|
|
24
|
+
* caller-forgeable leftmost `X-Forwarded-For` entry. Populated by
|
|
25
|
+
* `http-transport.ts` for the duration of one `transport.handleRequest()`
|
|
26
|
+
* call and read by the three MCP lead-write tools (#8073) so they can
|
|
27
|
+
* forward it to the hub without the tool gaining an identity argument.
|
|
28
|
+
* `undefined` outside an HTTP request — stdio has no client address to
|
|
29
|
+
* forward.
|
|
30
|
+
*/
|
|
31
|
+
export const trustedCallerAddress = new AsyncLocalStorage<string>();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The two visitor-forwarding headers (`x-ailk-visitor-ip`,
|
|
35
|
+
* `x-ailk-visitor-forward-key`) for the request this call is running inside,
|
|
36
|
+
* or `undefined` when there is no trusted address for this call (not an HTTP
|
|
37
|
+
* request), no `AILK_VISITOR_FORWARD_KEY`, or `visitorForwardHeaders` itself
|
|
38
|
+
* declines (an unsafe transport, an unsafe key value). Read by the lead-write
|
|
39
|
+
* tools right before they call the hub (#8073).
|
|
40
|
+
*/
|
|
41
|
+
export function visitorForwardHeadersForThisCall():
|
|
42
|
+
| Record<string, string>
|
|
43
|
+
| undefined {
|
|
44
|
+
const address = trustedCallerAddress.getStore();
|
|
45
|
+
if (!address) return undefined;
|
|
46
|
+
return visitorForwardHeaders(
|
|
47
|
+
{
|
|
48
|
+
get: (name: string) => (name === "x-forwarded-for" ? address : null),
|
|
49
|
+
},
|
|
50
|
+
process.env.AILK_VISITOR_FORWARD_KEY,
|
|
51
|
+
);
|
|
52
|
+
}
|