create-ailk 0.18.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/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 +7 -2
- package/dist/topology.js +60 -3
- 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 +9 -4
- 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__/audit-single-page.test.ts +15 -3
- package/templates/apps/mcp/__tests__/boot.test.ts +78 -1
- package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
- package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
- 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/boot.ts +5 -23
- package/templates/apps/mcp/src/http-transport.ts +93 -73
- package/templates/apps/mcp/src/index.ts +2 -0
- package/templates/apps/mcp/src/request-context.ts +52 -0
- package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
- package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
- 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/[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 +24 -45
- 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/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/topology.generated.ts +24 -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
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
* Abuse rule orchestrator for MCP write tools.
|
|
3
3
|
*
|
|
4
4
|
* Runs all in-process heuristics for the anonymous write tools:
|
|
5
|
-
* capture_lead, subscribe_newsletter, request_callback,
|
|
6
|
-
*
|
|
5
|
+
* capture_lead, subscribe_newsletter, request_callback, start_purchase
|
|
6
|
+
* (#5971), and — since #8071 — create_booking and submit_flow_step.
|
|
7
7
|
*
|
|
8
8
|
* Rules applied (in order):
|
|
9
9
|
* 1. User-Agent deny list (applies to all write tools)
|
|
10
10
|
* 2. Honeypot field check (applies to all write tools)
|
|
11
|
-
* 3. Email format validation (capture_lead, subscribe_newsletter
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* 3. Email format validation (capture_lead, subscribe_newsletter, and
|
|
12
|
+
* create_booking's attendee.email)
|
|
13
|
+
* 4. Per-IP daily cap (capture_lead, start_purchase, create_booking and
|
|
14
|
+
* submit_flow_step, each on its OWN tracker — sharing one would let one
|
|
15
|
+
* tool's calls consume another's budget, a silent cross-tool denial of
|
|
16
|
+
* service)
|
|
15
17
|
*
|
|
16
18
|
* The paid layer (lead-routing.ts) extends with real abuse-protection
|
|
17
19
|
* services. OSS gets basic heuristics that stop obvious bots.
|
|
@@ -24,11 +26,40 @@ import { isValidEmail } from "./email-validator.js";
|
|
|
24
26
|
import { isHoneypotTripped } from "./honeypot.js";
|
|
25
27
|
import { buildDenyList, isUserAgentBlocked } from "./user-agent-deny.js";
|
|
26
28
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Tools that require email format validation on their input, each mapped to
|
|
31
|
+
* where the email sits in it: top level for the lead tools, `attendee.email`
|
|
32
|
+
* for create_booking (#8071 D3). submit_flow_step has no fixed email field —
|
|
33
|
+
* its `values` is a free record the hub's flow engine validates.
|
|
34
|
+
*/
|
|
35
|
+
const EMAIL_VALIDATED_TOOLS = new Map<string, readonly string[]>([
|
|
36
|
+
["capture_lead", ["email"]],
|
|
37
|
+
["subscribe_newsletter", ["email"]],
|
|
38
|
+
["create_booking", ["attendee", "email"]],
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The value at `path` in `input`, or undefined when any step is not a plain
|
|
43
|
+
* object — a `null`, string or array `attendee` reads as a missing email, so a
|
|
44
|
+
* malformed body can never throw out of the guard (#8071 SEC-13).
|
|
45
|
+
*/
|
|
46
|
+
function valueAt(input: unknown, path: readonly string[]): unknown {
|
|
47
|
+
let value = input;
|
|
48
|
+
for (const key of path) {
|
|
49
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
value = (value as Record<string, unknown>)[key];
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
29
56
|
|
|
30
57
|
/** The per-tool daily-cap budgets, keyed by MCP tool name. */
|
|
31
|
-
type DailyCapKey =
|
|
58
|
+
type DailyCapKey =
|
|
59
|
+
| "captureLead"
|
|
60
|
+
| "startPurchase"
|
|
61
|
+
| "createBooking"
|
|
62
|
+
| "submitFlowStep";
|
|
32
63
|
|
|
33
64
|
/**
|
|
34
65
|
* The typed reason codes an {@link AbuseResult} refusal can carry.
|
|
@@ -56,8 +87,9 @@ export type AbuseReasonCode =
|
|
|
56
87
|
/**
|
|
57
88
|
* Tools that apply a per-IP daily cap, each mapped to the tracker that budgets
|
|
58
89
|
* it and the refusal reason it answers with. One tracker per entry — never
|
|
59
|
-
* shared across tools, since a shared budget would let
|
|
60
|
-
* consume the lead
|
|
90
|
+
* shared across tools, since a shared budget would let one tool's calls
|
|
91
|
+
* consume another's allowance (purchase attempts the lead budget, flow steps
|
|
92
|
+
* the lead budget they could otherwise mint Leads past).
|
|
61
93
|
*
|
|
62
94
|
* The reason strings here are prose for the caller and nothing more: every
|
|
63
95
|
* entry refuses with {@link AbuseReasonCode.DAILY_CAP_REACHED}, and that code
|
|
@@ -81,6 +113,20 @@ const DAILY_CAPPED_TOOLS = new Map<
|
|
|
81
113
|
reason: "daily cap reached for start_purchase from this IP",
|
|
82
114
|
},
|
|
83
115
|
],
|
|
116
|
+
[
|
|
117
|
+
"create_booking",
|
|
118
|
+
{
|
|
119
|
+
tracker: "createBooking",
|
|
120
|
+
reason: "daily cap reached for create_booking from this IP",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
[
|
|
124
|
+
"submit_flow_step",
|
|
125
|
+
{
|
|
126
|
+
tracker: "submitFlowStep",
|
|
127
|
+
reason: "daily cap reached for submit_flow_step from this IP",
|
|
128
|
+
},
|
|
129
|
+
],
|
|
84
130
|
]);
|
|
85
131
|
|
|
86
132
|
export interface AbuseConfig {
|
|
@@ -93,6 +139,18 @@ export interface AbuseConfig {
|
|
|
93
139
|
* keep compiling; omitted falls back to the default.
|
|
94
140
|
*/
|
|
95
141
|
startPurchaseDailyCapPerIp?: number;
|
|
142
|
+
/**
|
|
143
|
+
* Per-IP daily cap for create_booking (#8071). Default 20, as for
|
|
144
|
+
* start_purchase: each call creates a real calendar event and emails the
|
|
145
|
+
* attendee an invite. Optional; omitted falls back to the default.
|
|
146
|
+
*/
|
|
147
|
+
createBookingDailyCapPerIp?: number;
|
|
148
|
+
/**
|
|
149
|
+
* Per-IP daily cap for submit_flow_step (#8071). Default 50, equal to
|
|
150
|
+
* capture_lead's and no higher: a one-step flow creates a Lead in one call.
|
|
151
|
+
* Optional; omitted falls back to the default.
|
|
152
|
+
*/
|
|
153
|
+
submitFlowStepDailyCapPerIp?: number;
|
|
96
154
|
/** Comma-separated extra UA substrings to block. */
|
|
97
155
|
blockedUserAgents: string;
|
|
98
156
|
}
|
|
@@ -103,6 +161,12 @@ export const DEFAULT_START_PURCHASE_DAILY_CAP = 20;
|
|
|
103
161
|
/** Default per-IP daily cap for capture_lead when none is configured. */
|
|
104
162
|
export const DEFAULT_CAPTURE_LEAD_DAILY_CAP = 50;
|
|
105
163
|
|
|
164
|
+
/** Default per-IP daily cap for create_booking when none is configured. */
|
|
165
|
+
export const DEFAULT_CREATE_BOOKING_DAILY_CAP = 20;
|
|
166
|
+
|
|
167
|
+
/** Default per-IP daily cap for submit_flow_step when none is configured. */
|
|
168
|
+
export const DEFAULT_SUBMIT_FLOW_STEP_DAILY_CAP = 50;
|
|
169
|
+
|
|
106
170
|
/**
|
|
107
171
|
* Reads a positive-integer env var, falling back to `fallback` for anything
|
|
108
172
|
* that is not one.
|
|
@@ -162,6 +226,18 @@ export class AbuseGuard {
|
|
|
162
226
|
DEFAULT_START_PURCHASE_DAILY_CAP,
|
|
163
227
|
),
|
|
164
228
|
),
|
|
229
|
+
createBooking: new DailyCapTracker(
|
|
230
|
+
usableCap(
|
|
231
|
+
config.createBookingDailyCapPerIp,
|
|
232
|
+
DEFAULT_CREATE_BOOKING_DAILY_CAP,
|
|
233
|
+
),
|
|
234
|
+
),
|
|
235
|
+
submitFlowStep: new DailyCapTracker(
|
|
236
|
+
usableCap(
|
|
237
|
+
config.submitFlowStepDailyCapPerIp,
|
|
238
|
+
DEFAULT_SUBMIT_FLOW_STEP_DAILY_CAP,
|
|
239
|
+
),
|
|
240
|
+
),
|
|
165
241
|
};
|
|
166
242
|
}
|
|
167
243
|
|
|
@@ -198,10 +274,12 @@ export class AbuseGuard {
|
|
|
198
274
|
};
|
|
199
275
|
}
|
|
200
276
|
|
|
201
|
-
// Rule 3: Email format validation
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
277
|
+
// Rule 3: Email format validation, at the tool's own email path. A
|
|
278
|
+
// missing or non-string email passes — the handler's Zod schema is the
|
|
279
|
+
// authority on required fields.
|
|
280
|
+
const emailPath = EMAIL_VALIDATED_TOOLS.get(toolName);
|
|
281
|
+
if (emailPath !== undefined) {
|
|
282
|
+
const email = valueAt(input, emailPath);
|
|
205
283
|
if (typeof email === "string" && !isValidEmail(email)) {
|
|
206
284
|
return {
|
|
207
285
|
allowed: false,
|
|
@@ -227,12 +305,16 @@ export class AbuseGuard {
|
|
|
227
305
|
}
|
|
228
306
|
|
|
229
307
|
/**
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
308
|
+
* The abuse configuration, read from the environment. The one reader of the
|
|
309
|
+
* MCP_ABUSE_* variables: both entries reach it through
|
|
310
|
+
* `httpTransportConfigFromEnv()` (`src/boot.ts`), and
|
|
311
|
+
* {@link createAbuseGuardFromEnv} wraps it, so a new cap is added here once.
|
|
312
|
+
*
|
|
313
|
+
* Every cap goes through {@link readPositiveIntEnv}: a blank, zero or
|
|
314
|
+
* non-numeric value yields its default, never an unlimited cap.
|
|
233
315
|
*/
|
|
234
|
-
export function
|
|
235
|
-
return
|
|
316
|
+
export function abuseConfigFromEnv(): Required<AbuseConfig> {
|
|
317
|
+
return {
|
|
236
318
|
captureLeadDailyCapPerIp: readPositiveIntEnv(
|
|
237
319
|
"MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP",
|
|
238
320
|
DEFAULT_CAPTURE_LEAD_DAILY_CAP,
|
|
@@ -241,6 +323,20 @@ export function createAbuseGuardFromEnv(): AbuseGuard {
|
|
|
241
323
|
"MCP_ABUSE_START_PURCHASE_DAILY_CAP",
|
|
242
324
|
DEFAULT_START_PURCHASE_DAILY_CAP,
|
|
243
325
|
),
|
|
326
|
+
createBookingDailyCapPerIp: readPositiveIntEnv(
|
|
327
|
+
"MCP_ABUSE_CREATE_BOOKING_DAILY_CAP",
|
|
328
|
+
DEFAULT_CREATE_BOOKING_DAILY_CAP,
|
|
329
|
+
),
|
|
330
|
+
submitFlowStepDailyCapPerIp: readPositiveIntEnv(
|
|
331
|
+
"MCP_ABUSE_SUBMIT_FLOW_STEP_DAILY_CAP",
|
|
332
|
+
DEFAULT_SUBMIT_FLOW_STEP_DAILY_CAP,
|
|
333
|
+
),
|
|
334
|
+
// A literal read, so create-ailk's declared-env scan finds it (#7874).
|
|
244
335
|
blockedUserAgents: process.env["MCP_ABUSE_BLOCKED_USER_AGENTS"] ?? "",
|
|
245
|
-
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** An AbuseGuard built from {@link abuseConfigFromEnv}. */
|
|
340
|
+
export function createAbuseGuardFromEnv(): AbuseGuard {
|
|
341
|
+
return new AbuseGuard(abuseConfigFromEnv());
|
|
246
342
|
}
|
|
@@ -11,11 +11,7 @@
|
|
|
11
11
|
* a type only, so the stdio entry still never loads the HTTP stack.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
DEFAULT_CAPTURE_LEAD_DAILY_CAP,
|
|
16
|
-
DEFAULT_START_PURCHASE_DAILY_CAP,
|
|
17
|
-
readPositiveIntEnv,
|
|
18
|
-
} from "./abuse/index.js";
|
|
14
|
+
import { abuseConfigFromEnv, readPositiveIntEnv } from "./abuse/index.js";
|
|
19
15
|
import type { HttpTransportConfig } from "./http-transport.js";
|
|
20
16
|
import {
|
|
21
17
|
initMcpCommerceClaim,
|
|
@@ -158,28 +154,14 @@ export function httpTransportConfigFromEnv(): HttpTransportConfig & {
|
|
|
158
154
|
const writeAllowedOrigins = writeOriginsRaw
|
|
159
155
|
? writeOriginsRaw.split(",").map((o) => o.trim())
|
|
160
156
|
: [];
|
|
161
|
-
// Both caps go through readPositiveIntEnv rather than a bare parseInt: a
|
|
162
|
-
// BLANK env var would otherwise yield NaN and silently disable the cap
|
|
163
|
-
// (#5971 security review round 2).
|
|
164
|
-
const captureLeadDailyCapPerIp = readPositiveIntEnv(
|
|
165
|
-
"MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP",
|
|
166
|
-
DEFAULT_CAPTURE_LEAD_DAILY_CAP,
|
|
167
|
-
);
|
|
168
|
-
const startPurchaseDailyCapPerIp = readPositiveIntEnv(
|
|
169
|
-
"MCP_ABUSE_START_PURCHASE_DAILY_CAP",
|
|
170
|
-
DEFAULT_START_PURCHASE_DAILY_CAP,
|
|
171
|
-
);
|
|
172
|
-
const blockedUserAgents = process.env["MCP_ABUSE_BLOCKED_USER_AGENTS"] ?? "";
|
|
173
|
-
|
|
174
157
|
return {
|
|
175
158
|
port,
|
|
176
159
|
path,
|
|
177
160
|
rateLimits: { readPerMin, writePerMin },
|
|
178
161
|
cors: { writeAllowedOrigins },
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
},
|
|
162
|
+
// The MCP_ABUSE_* variables have one reader, shared with
|
|
163
|
+
// createAbuseGuardFromEnv (#8071); every cap falls back to its default
|
|
164
|
+
// on a blank or non-numeric value, never to unlimited (#5971).
|
|
165
|
+
abuse: abuseConfigFromEnv(),
|
|
184
166
|
};
|
|
185
167
|
}
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
AbuseReasonCode,
|
|
52
52
|
type AbuseConfig,
|
|
53
53
|
} from "./abuse/index.js";
|
|
54
|
+
import { trustedCallerAddress } from "./request-context.js";
|
|
54
55
|
import { createMcpServer } from "./server.js";
|
|
55
56
|
import { createWorkspaceMcpServer } from "./workspace-server.js";
|
|
56
57
|
|
|
@@ -85,6 +86,13 @@ export const ABUSE_REFUSAL_STATUS: Record<AbuseReasonCode, 403 | 422> = {
|
|
|
85
86
|
* worth money to abuse, so it takes the write branch — the origin allow-list,
|
|
86
87
|
* the write CORS posture, and `abuseGuard.validate()` — rather than the read
|
|
87
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.
|
|
88
96
|
*/
|
|
89
97
|
// Any name added here MUST be enforced through the batch-aware path in
|
|
90
98
|
// writeToolCalls() below — a request body can be a JSON-RPC ARRAY, and a
|
|
@@ -96,6 +104,8 @@ export const WRITE_TOOLS = new Set([
|
|
|
96
104
|
"subscribe_newsletter",
|
|
97
105
|
"request_callback",
|
|
98
106
|
"start_purchase",
|
|
107
|
+
"create_booking",
|
|
108
|
+
"submit_flow_step",
|
|
99
109
|
]);
|
|
100
110
|
|
|
101
111
|
export interface HttpTransportCorsConfig {
|
|
@@ -382,83 +392,93 @@ export async function registerHttpTransport(
|
|
|
382
392
|
}
|
|
383
393
|
}
|
|
384
394
|
},
|
|
385
|
-
handler: async (request, reply) =>
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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;
|
|
414
433
|
}
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (sessionId && transports.has(sessionId)) {
|
|
419
|
-
// Existing session — reuse transport
|
|
420
|
-
transport = transports.get(sessionId)!;
|
|
421
|
-
} else if (!sessionId && isInitializeRequest(body)) {
|
|
422
|
-
// New initialization — create a stateful transport + fresh server
|
|
423
|
-
transport = new StreamableHTTPServerTransport({
|
|
424
|
-
sessionIdGenerator: () => randomUUID(),
|
|
425
|
-
onsessioninitialized: (sid) => {
|
|
426
|
-
transports.set(sid, transport);
|
|
427
|
-
},
|
|
428
|
-
});
|
|
429
434
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
+
}
|
|
449
469
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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;
|
|
458
480
|
}
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
},
|
|
481
|
+
}),
|
|
462
482
|
});
|
|
463
483
|
|
|
464
484
|
// DELETE — session termination
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* MCP_CORS_WRITE_ALLOWED_ORIGINS= (empty = no cross-origin writes)
|
|
16
16
|
* MCP_ABUSE_CAPTURE_LEAD_DAILY_CAP=50
|
|
17
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
|
|
18
20
|
* MCP_ABUSE_BLOCKED_USER_AGENTS= (comma-separated substrings)
|
|
19
21
|
*
|
|
20
22
|
* Content adapter (all optional; `CONTENT_ADAPTER` default: mdx):
|
|
@@ -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
|
+
}
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
type SubmitLeadOutput,
|
|
18
18
|
} from "@working-theory/validation";
|
|
19
19
|
|
|
20
|
+
import { visitorForwardHeadersForThisCall } from "../request-context.js";
|
|
21
|
+
|
|
20
22
|
export { submitLeadInputSchema, submitLeadOutputSchema };
|
|
21
23
|
export type { SubmitLeadInput, SubmitLeadOutput };
|
|
22
24
|
|
|
@@ -47,7 +49,13 @@ export async function ailkCaptureLead(
|
|
|
47
49
|
},
|
|
48
50
|
};
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
// #8073 — forward the address the transport trusted (never an identity
|
|
53
|
+
// argument on this tool's own schema) so the hub counts the write in that
|
|
54
|
+
// visitor's own rate-limit bucket rather than this MCP host's.
|
|
55
|
+
const headers = visitorForwardHeadersForThisCall();
|
|
56
|
+
const res = headers
|
|
57
|
+
? await apiClient.leads.submit(body, { headers })
|
|
58
|
+
: await apiClient.leads.submit(body);
|
|
51
59
|
if (!res.ok) {
|
|
52
60
|
return { ok: false, error: { kind: "api_error", status: res.status } };
|
|
53
61
|
}
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
type RequestCallbackOutput,
|
|
19
19
|
} from "@working-theory/validation";
|
|
20
20
|
|
|
21
|
+
import { visitorForwardHeadersForThisCall } from "../request-context.js";
|
|
22
|
+
|
|
21
23
|
export { requestCallbackInputSchema, requestCallbackOutputSchema };
|
|
22
24
|
export type { RequestCallbackInput, RequestCallbackOutput };
|
|
23
25
|
|
|
@@ -42,7 +44,13 @@ export async function ailkRequestCallback(
|
|
|
42
44
|
payload: {},
|
|
43
45
|
};
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
// #8073 — forward the address the transport trusted (never an identity
|
|
48
|
+
// argument on this tool's own schema) so the hub counts the write in that
|
|
49
|
+
// visitor's own rate-limit bucket rather than this MCP host's.
|
|
50
|
+
const headers = visitorForwardHeadersForThisCall();
|
|
51
|
+
const res = headers
|
|
52
|
+
? await apiClient.leads.callback(body, { headers })
|
|
53
|
+
: await apiClient.leads.callback(body);
|
|
46
54
|
if (!res.ok) {
|
|
47
55
|
return { ok: false, error: { kind: "api_error", status: res.status } };
|
|
48
56
|
}
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
type SubscribeNewsletterOutput,
|
|
19
19
|
} from "@working-theory/validation";
|
|
20
20
|
|
|
21
|
+
import { visitorForwardHeadersForThisCall } from "../request-context.js";
|
|
22
|
+
|
|
21
23
|
export { subscribeNewsletterInputSchema, subscribeNewsletterOutputSchema };
|
|
22
24
|
export type { SubscribeNewsletterInput, SubscribeNewsletterOutput };
|
|
23
25
|
|
|
@@ -40,7 +42,13 @@ export async function ailkSubscribeNewsletter(
|
|
|
40
42
|
const siteId = process.env.AILK_SITE_ID ?? "default";
|
|
41
43
|
const body = { siteId, email, source: "mcp" };
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
// #8073 — forward the address the transport trusted (never an identity
|
|
46
|
+
// argument on this tool's own schema) so the hub counts the write in that
|
|
47
|
+
// visitor's own rate-limit bucket rather than this MCP host's.
|
|
48
|
+
const headers = visitorForwardHeadersForThisCall();
|
|
49
|
+
const res = headers
|
|
50
|
+
? await apiClient.leads.subscribe(body, { headers })
|
|
51
|
+
: await apiClient.leads.subscribe(body);
|
|
44
52
|
if (!res.ok) {
|
|
45
53
|
return { ok: false, error: { kind: "api_error", status: res.status } };
|
|
46
54
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
Apache License
|
|
2
3
|
Version 2.0, January 2004
|
|
3
4
|
http://www.apache.org/licenses/
|
|
@@ -137,8 +138,8 @@
|
|
|
137
138
|
|
|
138
139
|
6. Trademarks. This License does not grant permission to use the trade
|
|
139
140
|
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
-
except as required for
|
|
141
|
-
reproducing the content of the NOTICE file.
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
143
|
|
|
143
144
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
145
|
agreed to in writing, Licensor provides the Work (and each
|
|
@@ -162,7 +163,7 @@
|
|
|
162
163
|
other commercial damages or losses), even if such Contributor
|
|
163
164
|
has been advised of the possibility of such damages.
|
|
164
165
|
|
|
165
|
-
9. Accepting Warranty or
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
167
|
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
168
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
169
|
or other liability obligations and/or rights consistent with this
|
|
@@ -171,7 +172,7 @@
|
|
|
171
172
|
of any other Contributor, and only if You agree to indemnify,
|
|
172
173
|
defend, and hold each Contributor harmless for any liability
|
|
173
174
|
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
-
of your accepting any such warranty or
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
175
176
|
|
|
176
177
|
END OF TERMS AND CONDITIONS
|
|
177
178
|
|
|
@@ -186,7 +187,7 @@
|
|
|
186
187
|
same "printed page" as the copyright notice for easier
|
|
187
188
|
identification within third-party archives.
|
|
188
189
|
|
|
189
|
-
Copyright
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
191
|
|
|
191
192
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
193
|
you may not use this file except in compliance with the License.
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* See `docs/security/agent-manifest.md` for the full consumer guidance.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { SITE_NAME } from "../../../lib/site-brand";
|
|
31
|
+
import { SITE_DESCRIPTION, SITE_NAME } from "../../../lib/site-brand";
|
|
32
32
|
import { resolveBaseUrl } from "../../../src/lib/routes";
|
|
33
33
|
|
|
34
34
|
/** Soft cap to detect runaway `description_for_model` growth. */
|
|
@@ -66,8 +66,8 @@ export function GET(): Response {
|
|
|
66
66
|
schema_version: "v1",
|
|
67
67
|
name_for_human: SITE_NAME,
|
|
68
68
|
name_for_model: "ai_launch_kit",
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// #8085 — the site's own description, as /.well-known/mcp.json (#8057).
|
|
70
|
+
description_for_human: SITE_DESCRIPTION,
|
|
71
71
|
description_for_model: MANIFEST_DESCRIPTION_FOR_MODEL,
|
|
72
72
|
auth: { type: "none" },
|
|
73
73
|
api: {
|