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.
Files changed (193) hide show
  1. package/LICENSE +6 -5
  2. package/NOTICE +2 -0
  3. package/dist/add-module.js +4 -1
  4. package/dist/category-profile.d.ts +10 -0
  5. package/dist/category-profile.js +21 -0
  6. package/dist/derive-architecture.d.ts +5 -1
  7. package/dist/derive-architecture.js +8 -3
  8. package/dist/lib/env-catalog.js +5 -9
  9. package/dist/lib/license-key-files.d.ts +6 -3
  10. package/dist/lib/license-key-files.js +8 -2
  11. package/dist/lib/scaffold-render-sweep.d.ts +9 -0
  12. package/dist/lib/scaffold-render-sweep.js +24 -3
  13. package/dist/license-set.d.ts +1 -1
  14. package/dist/license-set.js +13 -8
  15. package/dist/module-architecture.js +119 -0
  16. package/dist/programmatic.js +3 -0
  17. package/dist/spoke-checkout.d.ts +11 -3
  18. package/dist/spoke-checkout.js +62 -12
  19. package/dist/starter-grid.d.ts +1 -1
  20. package/dist/surfaces.js +140 -1
  21. package/dist/template-manifest.d.ts +4 -3
  22. package/dist/topology.d.ts +15 -2
  23. package/dist/topology.js +83 -4
  24. package/package.json +5 -4
  25. package/templates/.env.example +3 -3
  26. package/templates/.github/workflows/ci.yml +5 -0
  27. package/templates/apps/api/CLAUDE.md +25 -7
  28. package/templates/apps/api/LICENSE +6 -5
  29. package/templates/apps/api/NOTICE +2 -0
  30. package/templates/apps/api/jest.config.cjs +4 -29
  31. package/templates/apps/api/package.json +1 -0
  32. package/templates/apps/api/src/lib/__mocks__/prisma.ts +9 -0
  33. package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +38 -0
  34. package/templates/apps/api/src/lib/deliverable-asset-store.ts +5 -4
  35. package/templates/apps/api/src/lib/stripe.ts +81 -0
  36. package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +266 -39
  37. package/templates/apps/api/src/middleware/auth.ts +206 -46
  38. package/templates/apps/api/src/routes/leads/index.ts +4 -0
  39. package/templates/apps/api/src/server.ts +107 -0
  40. package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +58 -0
  41. package/templates/apps/api/src/services/lead-routing.ts +30 -2
  42. package/templates/apps/api/vercel.json +2 -2
  43. package/templates/apps/mcp/.env.example +10 -0
  44. package/templates/apps/mcp/CLAUDE.md +58 -7
  45. package/templates/apps/mcp/LICENSE +6 -5
  46. package/templates/apps/mcp/NOTICE +2 -0
  47. package/templates/apps/mcp/__tests__/abuse/guard.test.ts +198 -0
  48. package/templates/apps/mcp/__tests__/app-entry.test.ts +325 -0
  49. package/templates/apps/mcp/__tests__/audit-single-page.test.ts +15 -3
  50. package/templates/apps/mcp/__tests__/boot.test.ts +280 -0
  51. package/templates/apps/mcp/__tests__/http-transport.test.ts +291 -2
  52. package/templates/apps/mcp/__tests__/index.test.ts +86 -47
  53. package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +5 -6
  54. package/templates/apps/mcp/__tests__/lib/content-root.test.ts +93 -0
  55. package/templates/apps/mcp/__tests__/request_callback.test.ts +69 -7
  56. package/templates/apps/mcp/__tests__/server.test.ts +63 -6
  57. package/templates/apps/mcp/__tests__/submit_lead.test.ts +69 -8
  58. package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +69 -7
  59. package/templates/apps/mcp/jest.config.cjs +3 -44
  60. package/templates/apps/mcp/src/abuse/index.ts +117 -21
  61. package/templates/apps/mcp/src/app.ts +32 -0
  62. package/templates/apps/mcp/src/boot.ts +167 -0
  63. package/templates/apps/mcp/src/http-transport.ts +132 -88
  64. package/templates/apps/mcp/src/index.ts +19 -126
  65. package/templates/apps/mcp/src/lib/audit-single-page.ts +7 -6
  66. package/templates/apps/mcp/src/lib/content-adapter.ts +34 -5
  67. package/templates/apps/mcp/src/lib/topology.generated.ts +3 -0
  68. package/templates/apps/mcp/src/request-context.ts +52 -0
  69. package/templates/apps/mcp/src/server.ts +10 -2
  70. package/templates/apps/mcp/src/serverless-app.ts +88 -0
  71. package/templates/apps/mcp/src/tools/capture_lead.ts +9 -1
  72. package/templates/apps/mcp/src/tools/get_business_hours.ts +6 -5
  73. package/templates/apps/mcp/src/tools/get_faq.ts +6 -5
  74. package/templates/apps/mcp/src/tools/get_locations.ts +6 -5
  75. package/templates/apps/mcp/src/tools/get_offerings.ts +6 -5
  76. package/templates/apps/mcp/src/tools/get_pricing.ts +6 -5
  77. package/templates/apps/mcp/src/tools/get_site_info.ts +6 -5
  78. package/templates/apps/mcp/src/tools/list_pages.ts +8 -7
  79. package/templates/apps/mcp/src/tools/read_content.ts +7 -7
  80. package/templates/apps/mcp/src/tools/request_callback.ts +9 -1
  81. package/templates/apps/mcp/src/tools/search_content.ts +6 -5
  82. package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +9 -1
  83. package/templates/apps/mcp/tsconfig.json +2 -0
  84. package/templates/apps/web/LICENSE +6 -5
  85. package/templates/apps/web/NOTICE +2 -0
  86. package/templates/apps/web/app/.well-known/ai-plugin.json/route.ts +3 -3
  87. package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +16 -4
  88. package/templates/apps/web/app/.well-known/mcp.json/route.ts +3 -0
  89. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +288 -0
  90. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/general.test.tsx +568 -0
  91. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/hub-mode.test.tsx +272 -42
  92. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/item.test.tsx +11 -13
  93. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/layout.test.tsx +115 -16
  94. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-menu.test.tsx +305 -0
  95. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/new-popups.test.tsx +82 -0
  96. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/notifications.test.tsx +126 -0
  97. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/page.test.tsx +163 -72
  98. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/popups.test.tsx +451 -0
  99. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/security.test.tsx +139 -0
  100. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/settings.test.tsx +20 -390
  101. package/templates/apps/web/app/[locale]/(authed)/account/__tests__/support.test.tsx +16 -11
  102. package/templates/apps/web/app/[locale]/(authed)/account/account-data.ts +1 -1
  103. package/templates/apps/web/app/[locale]/(authed)/account/account-frame.tsx +114 -50
  104. package/templates/apps/web/app/[locale]/(authed)/account/actions.ts +25 -32
  105. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/page.tsx +253 -0
  106. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/review-form.tsx +145 -0
  107. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/[pieceId]/versions/[version]/file/route.ts +100 -0
  108. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/file-route.test.ts +179 -0
  109. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/page.test.tsx +514 -0
  110. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/__tests__/review.test.tsx +288 -0
  111. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-actions.ts +123 -0
  112. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/artifact-data.ts +82 -0
  113. package/templates/apps/web/app/[locale]/(authed)/account/artifacts/page.tsx +177 -0
  114. package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +172 -0
  115. package/templates/apps/web/app/[locale]/(authed)/account/billing-source.ts +147 -0
  116. package/templates/apps/web/app/[locale]/(authed)/account/components/request-quote.tsx +229 -0
  117. package/templates/apps/web/app/[locale]/(authed)/account/components/schedule-service.tsx +276 -0
  118. package/templates/apps/web/app/[locale]/(authed)/account/components/send-message.tsx +128 -0
  119. package/templates/apps/web/app/[locale]/(authed)/account/general/billing-anchor.tsx +27 -0
  120. package/templates/apps/web/app/[locale]/(authed)/account/general/deletion-request.tsx +132 -0
  121. package/templates/apps/web/app/[locale]/(authed)/account/general/page.tsx +131 -0
  122. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-form.tsx +104 -0
  123. package/templates/apps/web/app/[locale]/(authed)/account/general/profile-source.ts +73 -0
  124. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/__tests__/route.test.ts +18 -8
  125. package/templates/apps/web/app/[locale]/(authed)/account/inbox/message/[id]/file/route.ts +4 -6
  126. package/templates/apps/web/app/[locale]/(authed)/account/layout.tsx +4 -1
  127. package/templates/apps/web/app/[locale]/(authed)/account/new-actions.ts +219 -0
  128. package/templates/apps/web/app/[locale]/(authed)/account/new-menu.tsx +340 -0
  129. package/templates/apps/web/app/[locale]/(authed)/account/new-popups.tsx +107 -0
  130. package/templates/apps/web/app/[locale]/(authed)/account/notifications/page.tsx +57 -0
  131. package/templates/apps/web/app/[locale]/(authed)/account/page.tsx +85 -26
  132. package/templates/apps/web/app/[locale]/(authed)/account/profile-actions.ts +116 -0
  133. package/templates/apps/web/app/[locale]/(authed)/account/security/page.tsx +80 -0
  134. package/templates/apps/web/app/[locale]/(authed)/account/settings/page.tsx +13 -354
  135. package/templates/apps/web/app/[locale]/(authed)/account/settings-frame.tsx +108 -0
  136. package/templates/apps/web/app/[locale]/(authed)/account/sign-in-method.ts +37 -0
  137. package/templates/apps/web/app/[locale]/(authed)/account/support-actions.ts +9 -10
  138. package/templates/apps/web/app/[locale]/layout.tsx +23 -4
  139. package/templates/apps/web/app/api/auth/hub/__tests__/continuation.test.ts +143 -0
  140. package/templates/apps/web/app/api/auth/hub/authorize/__tests__/route.test.ts +62 -10
  141. package/templates/apps/web/app/api/auth/hub/authorize/route.ts +22 -7
  142. package/templates/apps/web/app/api/auth/hub/continuation.ts +73 -0
  143. package/templates/apps/web/app/api/auth/hub/continue/[ref]/__tests__/route.test.ts +188 -0
  144. package/templates/apps/web/app/api/auth/hub/continue/[ref]/route.ts +53 -0
  145. package/templates/apps/web/app/robots.txt/route.ts +10 -4
  146. package/templates/apps/web/auth.ts +5 -1
  147. package/templates/apps/web/lib/__tests__/account-config.test.ts +258 -46
  148. package/templates/apps/web/lib/__tests__/site-brand.test.ts +42 -0
  149. package/templates/apps/web/lib/account-config.ts +188 -52
  150. package/templates/apps/web/lib/hub-session/__tests__/hub-session.test.ts +143 -0
  151. package/templates/apps/web/lib/hub-session/index.ts +1 -0
  152. package/templates/apps/web/lib/hub-session/reseal.ts +90 -0
  153. package/templates/apps/web/lib/hub-session/seal.ts +13 -9
  154. package/templates/apps/web/lib/site-brand.tsx +22 -6
  155. package/templates/apps/web/lib/topology.generated.ts +27 -0
  156. package/templates/apps/web/messages/en.json +156 -35
  157. package/templates/database/CHANGELOG.md +21 -0
  158. package/templates/database/LICENSE +6 -5
  159. package/templates/database/NOTICE +2 -0
  160. package/templates/database/README.md +1 -1
  161. package/templates/database/__tests__/member-area.db.test.ts +15 -3
  162. package/templates/database/__tests__/member-contract-migration.test.ts +85 -0
  163. package/templates/database/__tests__/member-contract.db.test.ts +413 -0
  164. package/templates/database/__tests__/seed.test.ts +54 -2
  165. package/templates/database/inbox/schema.prisma +90 -15
  166. package/templates/database/migrations/20261012120000_member_contract/migration.sql +192 -0
  167. package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +20 -0
  168. package/templates/database/package.json +3 -2
  169. package/templates/database/seed.ts +34 -2
  170. package/versions.json +36 -36
  171. package/templates/apps/api/src/__tests__/content-write-production-boot.test.ts +0 -104
  172. package/templates/apps/api/src/__tests__/cors.test.ts +0 -276
  173. package/templates/apps/api/src/__tests__/module-exclusion.test.ts +0 -279
  174. package/templates/apps/api/src/__tests__/openapi-keyless-hub.test.ts +0 -122
  175. package/templates/apps/api/src/__tests__/review-surface-registration.test.ts +0 -167
  176. package/templates/apps/api/src/__tests__/root-modules.test.ts +0 -175
  177. package/templates/apps/api/src/__tests__/schedule-forwarded-rate-limit.test.ts +0 -117
  178. package/templates/apps/api/src/__tests__/server.test.ts +0 -591
  179. package/templates/apps/api/src/__tests__/workspace-module-registration.test.ts +0 -175
  180. package/templates/apps/api/src/openapi/__tests__/surface-drift.test.ts +0 -293
  181. package/templates/apps/api/src/routes/leads/__tests__/index.test.ts +0 -1620
  182. package/templates/apps/api/src/routes/project-listings/__tests__/directory.test.ts +0 -528
  183. package/templates/apps/api/src/routes/workspaces/__tests__/inbox-wire-schemas.test.ts +0 -198
  184. package/templates/apps/api/src/services/__tests__/aeo-score.test.ts +0 -652
  185. package/templates/apps/api/src/services/__tests__/lead-promotion.test.ts +0 -408
  186. package/templates/apps/api/src/services/__tests__/leads-inbox.test.ts +0 -476
  187. package/templates/apps/mcp/__tests__/audit.test.ts +0 -346
  188. package/templates/apps/mcp/__tests__/parity.test.ts +0 -325
  189. package/templates/apps/mcp/__tests__/workspace-surface.test.ts +0 -364
  190. package/templates/apps/web/app/[locale]/(authed)/account/settings/color-mode.tsx +0 -68
  191. package/templates/apps/web/app/[locale]/(authed)/account/settings/deletion-request.tsx +0 -55
  192. package/templates/apps/web/app/[locale]/(authed)/account/settings/name-form.tsx +0 -118
  193. package/templates/database/__tests__/build.test.ts +0 -131
@@ -27,6 +27,7 @@ import {
27
27
  mcpHeldFeatures,
28
28
  } from "./lib/commerce-claim.js";
29
29
  import { notFoundError } from "./lib/errors.js";
30
+ import { UNSERVED_MCP_TOOLS } from "./lib/topology.generated.js";
30
31
  import { ailkAudit } from "./tools/audit.js";
31
32
  import { ailkCaptureLead } from "./tools/capture_lead.js";
32
33
  import { ailkCreateProject } from "./tools/create_project.js";
@@ -169,8 +170,12 @@ export interface RegisteredToolDescriptor {
169
170
 
170
171
  /**
171
172
  * Whether a catalog registration entry should register on this build/boot —
172
- * three filters:
173
+ * four filters:
173
174
  *
175
+ * 0. the topology filter (#8070 D7): a name in the generated seam's
176
+ * `UNSERVED_MCP_TOOLS` is never served — `create_project` on a spoke,
177
+ * which carries no create-ailk CLI for it to spawn. `[]` on a hub. The
178
+ * web manifest drops the same list, so the two surfaces agree;
174
179
  * 1. the OSS-disabled filter (static per-name set; empty since #5971);
175
180
  * 2. the module-presence filter (S1, #5088): a tool tagged with `module`
176
181
  * (e.g. `create_booking` carrying `module: "scheduling"`) registers only
@@ -200,6 +205,7 @@ function shouldRegister(reg: {
200
205
  module?: string;
201
206
  addon?: "commerce";
202
207
  }): boolean {
208
+ if (UNSERVED_MCP_TOOLS.includes(reg.name)) return false;
203
209
  if (OSS_DISABLED_TOOLS.has(reg.name)) return false;
204
210
  if (reg.module && !isModuleEnabled(reg.module)) return false;
205
211
  if (reg.addon === "commerce" && mcpCommerceClaim() !== "claimed")
@@ -240,7 +246,8 @@ function catalogOptionsForThisBoot(): McpCatalogOptions {
240
246
  * boot serves whose licence `feature` the deployment does not hold, calls
241
247
  * `warn` once, naming the tool and the feature, and returns their names.
242
248
  *
243
- * Called ONCE per process from `index.ts`, after the claim resolves — never
249
+ * Called ONCE per process from `bootMcp()` (`boot.ts`, which both `index.ts`
250
+ * and the Vercel entry `app.ts` await), after the claim resolves — never
244
251
  * from {@link createMcpServer}, which runs once per HTTP session. In refuse
245
252
  * mode it finds nothing: the upstream catalog filter has already dropped
246
253
  * every such tool, so it needs no mode check of its own. Defaults to
@@ -283,6 +290,7 @@ export function listRegisteredTools(): RegisteredToolDescriptor[] {
283
290
 
284
291
  /**
285
292
  * Creates a configured McpServer with all AILK tools registered, less any
293
+ * tool this topology does not serve (`UNSERVED_MCP_TOOLS`, #8070 D7), less any
286
294
  * tool name in {@link OSS_DISABLED_TOOLS}, less any tool whose `module` is
287
295
  * not enabled on this build (S1, #5088), and less any `addon`-tagged tool
288
296
  * whose claim this deployment does not hold (#5971) — see
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The testable part of the Vercel entry (#8070 D2, D5).
3
+ *
4
+ * `src/app.ts` is the thin file Vercel's Node-backend detection finds: it
5
+ * awaits the boot at module level and default-exports the
6
+ * {@link serverlessHandler} for the app. A module-level
7
+ * `await` does not compile under the package's ts-jest CommonJS preset, so
8
+ * everything app.ts does beyond that lives here, where a test can build the
9
+ * same app (`__tests__/app-entry.test.ts`).
10
+ */
11
+
12
+ import type { IncomingMessage, ServerResponse } from "node:http";
13
+
14
+ import type { FastifyInstance } from "fastify";
15
+
16
+ import { httpTransportConfigFromEnv, readStatelessEnv } from "./boot.js";
17
+ import { registerHttpTransport } from "./http-transport.js";
18
+
19
+ /**
20
+ * Trust exactly one proxy hop — the immediate peer — and nothing behind it
21
+ * (D5). Fastify then takes `request.ip` from the LAST `X-Forwarded-For`
22
+ * entry, the address Vercel's edge saw, and a value a caller writes earlier
23
+ * in the header cannot move it.
24
+ *
25
+ * A function, not `trustProxy: 1`: Fastify 5.12 treats a numeric hop count as
26
+ * trust-nothing ("hop-count-only trust cannot validate the immediate peer"),
27
+ * so `1` would silently key every caller on the edge's address. A hop-only
28
+ * function is safe here for the reason Fastify's warning names: a Vercel
29
+ * function cannot be reached except through Vercel's edge, so its immediate
30
+ * peer is always the edge. It would NOT be safe on a host a client can reach
31
+ * directly — which is why the long-running entry (`buildHttpTransportApp`)
32
+ * sets no `trustProxy` at all.
33
+ *
34
+ * Trusting hop 0 also makes `request.host` and `request.protocol` follow
35
+ * `X-Forwarded-Host` / `X-Forwarded-Proto`. Nothing in apps/mcp reads either
36
+ * today; anything that starts to must remember they come from the edge.
37
+ */
38
+ export function trustOneHop(_address: string, hop: number): boolean {
39
+ return hop === 0;
40
+ }
41
+
42
+ /**
43
+ * The Fastify options the Vercel entry builds its app with: no logger, and
44
+ * {@link trustOneHop} — so the per-IP rate limit and the daily caps key on
45
+ * the real caller. That holds for the topology the sites deploy: the MCP host
46
+ * name points straight at the Vercel project. A second proxy in front
47
+ * (Cloudflare in proxied mode, a rewrite from another Vercel project) would
48
+ * make every caller that proxy's address.
49
+ */
50
+ export const SERVERLESS_FASTIFY_OPTIONS = {
51
+ logger: false,
52
+ trustProxy: trustOneHop,
53
+ } as const;
54
+
55
+ /**
56
+ * Register the MCP HTTP transport on the Vercel entry's app, from the
57
+ * environment. Stateless unless `MCP_HTTP_STATELESS` says otherwise (D1): a
58
+ * stateful transport keeps its sessions in one instance's memory, which a
59
+ * multi-instance host cannot honour. The port is read and ignored — Vercel
60
+ * calls the exported {@link serverlessHandler} and nothing listens.
61
+ */
62
+ export async function configureServerlessApp(
63
+ app: FastifyInstance,
64
+ ): Promise<void> {
65
+ const { port: _port, ...config } = httpTransportConfigFromEnv();
66
+ await registerHttpTransport(app, {
67
+ ...config,
68
+ stateless: readStatelessEnv(true),
69
+ });
70
+ }
71
+
72
+ /**
73
+ * The `(req, res)` function the Vercel entry default-exports (#8070 D2,
74
+ * amendment 2). Vercel's Node runtime accepts a request function, an object of
75
+ * web handlers, or an `http.Server` the module starts listening — a Fastify
76
+ * instance is none of these, and exporting one answered every request with a
77
+ * 500. `app.server` is the `http.Server` Fastify builds without listening;
78
+ * emitting `request` on it is Fastify's own request path, so routing,
79
+ * `trustProxy` and every hook behave as on a listening server. The app must
80
+ * already be `ready()`.
81
+ */
82
+ export function serverlessHandler(
83
+ app: FastifyInstance,
84
+ ): (req: IncomingMessage, res: ServerResponse) => void {
85
+ return (req, res) => {
86
+ app.server.emit("request", req, res);
87
+ };
88
+ }
@@ -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
- const res = await apiClient.leads.submit(body);
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
  }
@@ -15,8 +15,6 @@
15
15
  * Session/scope: none — read-only public content.
16
16
  */
17
17
 
18
- import { join } from "node:path";
19
-
20
18
  import {
21
19
  getBusinessHoursInputSchema,
22
20
  getBusinessHoursOutputSchema,
@@ -24,7 +22,10 @@ import {
24
22
  type GetBusinessHoursOutput,
25
23
  } from "@working-theory/validation";
26
24
 
27
- import { getMcpContentAdapter } from "../lib/content-adapter.js";
25
+ import {
26
+ getMcpContentAdapter,
27
+ mcpContentRoot,
28
+ } from "../lib/content-adapter.js";
28
29
  import { resolveLocale } from "../lib/resolve-locale.js";
29
30
 
30
31
  export { getBusinessHoursInputSchema, getBusinessHoursOutputSchema };
@@ -36,7 +37,7 @@ const DEFAULT_SLUG = "home";
36
37
  * get_business_hours handler.
37
38
  *
38
39
  * Accepts an optional `contentRoot` override for testing. When omitted,
39
- * resolves the content root relative to cwd (supports ESM runtime).
40
+ * resolves it through `mcpContentRoot()` (#8070 D3).
40
41
  */
41
42
  export async function ailkGetBusinessHours(
42
43
  input: GetBusinessHoursInput,
@@ -44,7 +45,7 @@ export async function ailkGetBusinessHours(
44
45
  ): Promise<GetBusinessHoursOutput> {
45
46
  getBusinessHoursInputSchema.parse(input);
46
47
 
47
- const root = contentRoot ?? join(process.cwd(), "content");
48
+ const root = contentRoot ?? mcpContentRoot();
48
49
  const locale = resolveLocale(input.locale);
49
50
  const adapter = getMcpContentAdapter({ contentRoot: root, locale });
50
51
 
@@ -15,8 +15,6 @@
15
15
  * Session/scope: none — read-only public content.
16
16
  */
17
17
 
18
- import { join } from "node:path";
19
-
20
18
  import {
21
19
  getFaqInputSchema,
22
20
  getFaqOutputSchema,
@@ -24,7 +22,10 @@ import {
24
22
  type GetFaqOutput,
25
23
  } from "@working-theory/validation";
26
24
 
27
- import { getMcpContentAdapter } from "../lib/content-adapter.js";
25
+ import {
26
+ getMcpContentAdapter,
27
+ mcpContentRoot,
28
+ } from "../lib/content-adapter.js";
28
29
  import { resolveLocale } from "../lib/resolve-locale.js";
29
30
 
30
31
  export { getFaqInputSchema, getFaqOutputSchema };
@@ -36,7 +37,7 @@ const DEFAULT_SLUG = "faq";
36
37
  * get_faq handler.
37
38
  *
38
39
  * Accepts an optional `contentRoot` override for testing. When omitted,
39
- * resolves the content root relative to cwd (supports ESM runtime).
40
+ * resolves it through `mcpContentRoot()` (#8070 D3).
40
41
  */
41
42
  export async function ailkGetFaq(
42
43
  input: GetFaqInput,
@@ -44,7 +45,7 @@ export async function ailkGetFaq(
44
45
  ): Promise<GetFaqOutput> {
45
46
  getFaqInputSchema.parse(input);
46
47
 
47
- const root = contentRoot ?? join(process.cwd(), "content");
48
+ const root = contentRoot ?? mcpContentRoot();
48
49
  const locale = resolveLocale(input.locale);
49
50
  const adapter = getMcpContentAdapter({ contentRoot: root, locale });
50
51
 
@@ -15,8 +15,6 @@
15
15
  * Session/scope: none — read-only public content.
16
16
  */
17
17
 
18
- import { join } from "node:path";
19
-
20
18
  import { SCHEMA_ORG_TYPES } from "@working-theory/content-adapters";
21
19
  import {
22
20
  getLocationsInputSchema,
@@ -25,7 +23,10 @@ import {
25
23
  type GetLocationsOutput,
26
24
  } from "@working-theory/validation";
27
25
 
28
- import { getMcpContentAdapter } from "../lib/content-adapter.js";
26
+ import {
27
+ getMcpContentAdapter,
28
+ mcpContentRoot,
29
+ } from "../lib/content-adapter.js";
29
30
  import { resolveLocale } from "../lib/resolve-locale.js";
30
31
 
31
32
  export { getLocationsInputSchema, getLocationsOutputSchema };
@@ -35,7 +36,7 @@ export type { GetLocationsInput, GetLocationsOutput };
35
36
  * get_locations handler.
36
37
  *
37
38
  * Accepts an optional `contentRoot` override for testing. When omitted,
38
- * resolves the content root relative to cwd (supports ESM runtime).
39
+ * resolves it through `mcpContentRoot()` (#8070 D3).
39
40
  */
40
41
  export async function ailkGetLocations(
41
42
  input: GetLocationsInput,
@@ -43,7 +44,7 @@ export async function ailkGetLocations(
43
44
  ): Promise<GetLocationsOutput> {
44
45
  getLocationsInputSchema.parse(input);
45
46
 
46
- const root = contentRoot ?? join(process.cwd(), "content");
47
+ const root = contentRoot ?? mcpContentRoot();
47
48
  const locale = resolveLocale(input.locale);
48
49
  const adapter = getMcpContentAdapter({ contentRoot: root, locale });
49
50
 
@@ -21,8 +21,6 @@
21
21
  * Session/scope: none — read-only public content.
22
22
  */
23
23
 
24
- import { join } from "node:path";
25
-
26
24
  import { type SchemaOrgType } from "@working-theory/content-adapters";
27
25
  import {
28
26
  getOfferingsInputSchema,
@@ -31,7 +29,10 @@ import {
31
29
  type GetOfferingsOutput,
32
30
  } from "@working-theory/validation";
33
31
 
34
- import { getMcpContentAdapter } from "../lib/content-adapter.js";
32
+ import {
33
+ getMcpContentAdapter,
34
+ mcpContentRoot,
35
+ } from "../lib/content-adapter.js";
35
36
  import { resolveLocale } from "../lib/resolve-locale.js";
36
37
 
37
38
  export { getOfferingsInputSchema, getOfferingsOutputSchema };
@@ -41,7 +42,7 @@ export type { GetOfferingsInput, GetOfferingsOutput };
41
42
  * get_offerings handler.
42
43
  *
43
44
  * Accepts an optional `contentRoot` override for testing. When omitted,
44
- * resolves the content root relative to cwd (supports ESM runtime).
45
+ * resolves it through `mcpContentRoot()` (#8070 D3).
45
46
  */
46
47
  export async function ailkGetOfferings(
47
48
  input: GetOfferingsInput,
@@ -49,7 +50,7 @@ export async function ailkGetOfferings(
49
50
  ): Promise<GetOfferingsOutput> {
50
51
  getOfferingsInputSchema.parse(input);
51
52
 
52
- const root = contentRoot ?? join(process.cwd(), "content");
53
+ const root = contentRoot ?? mcpContentRoot();
53
54
  const locale = resolveLocale(input.locale);
54
55
  const adapter = getMcpContentAdapter({ contentRoot: root, locale });
55
56
 
@@ -21,8 +21,6 @@
21
21
  * Session/scope: none — read-only public content.
22
22
  */
23
23
 
24
- import { join } from "node:path";
25
-
26
24
  import {
27
25
  getPricingInputSchema,
28
26
  getPricingOutputSchema,
@@ -30,7 +28,10 @@ import {
30
28
  type GetPricingOutput,
31
29
  } from "@working-theory/validation";
32
30
 
33
- import { getMcpContentAdapter } from "../lib/content-adapter.js";
31
+ import {
32
+ getMcpContentAdapter,
33
+ mcpContentRoot,
34
+ } from "../lib/content-adapter.js";
34
35
  import { resolveLocale } from "../lib/resolve-locale.js";
35
36
 
36
37
  export { getPricingInputSchema, getPricingOutputSchema };
@@ -42,7 +43,7 @@ const DEFAULT_SLUG = "pricing";
42
43
  * get_pricing handler.
43
44
  *
44
45
  * Accepts an optional `contentRoot` override for testing. When omitted,
45
- * resolves the content root relative to cwd (supports ESM runtime).
46
+ * resolves it through `mcpContentRoot()` (#8070 D3).
46
47
  */
47
48
  export async function ailkGetPricing(
48
49
  input: GetPricingInput,
@@ -50,7 +51,7 @@ export async function ailkGetPricing(
50
51
  ): Promise<GetPricingOutput> {
51
52
  getPricingInputSchema.parse(input);
52
53
 
53
- const root = contentRoot ?? join(process.cwd(), "content");
54
+ const root = contentRoot ?? mcpContentRoot();
54
55
  const locale = resolveLocale(input.locale);
55
56
  const adapter = getMcpContentAdapter({ contentRoot: root, locale });
56
57
 
@@ -15,8 +15,6 @@
15
15
  * Session/scope: none — read-only public content.
16
16
  */
17
17
 
18
- import { join } from "node:path";
19
-
20
18
  import {
21
19
  getSiteInfoInputSchema,
22
20
  getSiteInfoOutputSchema,
@@ -24,7 +22,10 @@ import {
24
22
  type GetSiteInfoOutput,
25
23
  } from "@working-theory/validation";
26
24
 
27
- import { getMcpContentAdapter } from "../lib/content-adapter.js";
25
+ import {
26
+ getMcpContentAdapter,
27
+ mcpContentRoot,
28
+ } from "../lib/content-adapter.js";
28
29
  import { resolveLocale } from "../lib/resolve-locale.js";
29
30
 
30
31
  export { getSiteInfoInputSchema, getSiteInfoOutputSchema };
@@ -36,7 +37,7 @@ const DEFAULT_SLUG = "home";
36
37
  * get_site_info handler.
37
38
  *
38
39
  * Accepts an optional `contentRoot` override for testing. When omitted,
39
- * resolves the content root relative to cwd (supports ESM runtime).
40
+ * resolves it through `mcpContentRoot()` (#8070 D3).
40
41
  */
41
42
  export async function ailkGetSiteInfo(
42
43
  input: GetSiteInfoInput,
@@ -44,7 +45,7 @@ export async function ailkGetSiteInfo(
44
45
  ): Promise<GetSiteInfoOutput> {
45
46
  getSiteInfoInputSchema.parse(input);
46
47
 
47
- const root = contentRoot ?? join(process.cwd(), "content");
48
+ const root = contentRoot ?? mcpContentRoot();
48
49
  const locale = resolveLocale(input.locale);
49
50
  const adapter = getMcpContentAdapter({ contentRoot: root, locale });
50
51
 
@@ -15,8 +15,6 @@
15
15
  * Session/scope: none — read-only public content.
16
16
  */
17
17
 
18
- import { join } from "node:path";
19
-
20
18
  import {
21
19
  SCHEMA_ORG_TYPES,
22
20
  type SchemaOrgType,
@@ -28,7 +26,10 @@ import {
28
26
  type ListPagesOutput,
29
27
  } from "@working-theory/validation";
30
28
 
31
- import { getMcpContentAdapter } from "../lib/content-adapter.js";
29
+ import {
30
+ getMcpContentAdapter,
31
+ mcpContentRoot,
32
+ } from "../lib/content-adapter.js";
32
33
  import { resolveLocale } from "../lib/resolve-locale.js";
33
34
 
34
35
  export { listPagesInputSchema, listPagesOutputSchema };
@@ -38,8 +39,8 @@ export type { ListPagesInput, ListPagesOutput };
38
39
  * list_pages handler.
39
40
  *
40
41
  * Accepts an optional `contentRoot` override for testing (avoids coupling
41
- * unit tests to the deployed filesystem layout). When omitted, resolves the
42
- * content root relative to this file at call time (supports ESM runtime).
42
+ * unit tests to the deployed filesystem layout). When omitted, resolves it
43
+ * through `mcpContentRoot()` (#8070 D3).
43
44
  */
44
45
  export async function ailkListPages(
45
46
  input: ListPagesInput,
@@ -47,8 +48,8 @@ export async function ailkListPages(
47
48
  ): Promise<ListPagesOutput> {
48
49
  listPagesInputSchema.parse(input);
49
50
 
50
- // Default: resolve content/ relative to cwd (works in ESM and jest/commonjs).
51
- const root = contentRoot ?? join(process.cwd(), "content");
51
+ // Default: the one content-root resolver (#8070 D3).
52
+ const root = contentRoot ?? mcpContentRoot();
52
53
  const locale = resolveLocale(input.locale);
53
54
  const adapter = getMcpContentAdapter({ contentRoot: root, locale });
54
55
 
@@ -20,8 +20,6 @@
20
20
  * Session/scope: none — read-only public content.
21
21
  */
22
22
 
23
- import { join } from "node:path";
24
-
25
23
  import {
26
24
  readContentInputSchema,
27
25
  readContentOutputSchema,
@@ -29,7 +27,10 @@ import {
29
27
  type ReadContentOutput,
30
28
  } from "@working-theory/validation";
31
29
 
32
- import { getMcpMdxContentAdapter } from "../lib/content-adapter.js";
30
+ import {
31
+ getMcpMdxContentAdapter,
32
+ mcpContentRoot,
33
+ } from "../lib/content-adapter.js";
33
34
  import { resolveLocale } from "../lib/resolve-locale.js";
34
35
 
35
36
  export { readContentInputSchema, readContentOutputSchema };
@@ -39,8 +40,7 @@ export type { ReadContentInput, ReadContentOutput };
39
40
  * read_content handler.
40
41
  *
41
42
  * Accepts an optional `contentRoot` override for testing. When omitted,
42
- * resolves the content root relative to this file at call time (supports
43
- * ESM runtime).
43
+ * resolves it through `mcpContentRoot()` (#8070 D3).
44
44
  */
45
45
  export async function ailkReadContent(
46
46
  input: ReadContentInput,
@@ -48,8 +48,8 @@ export async function ailkReadContent(
48
48
  ): Promise<ReadContentOutput> {
49
49
  readContentInputSchema.parse(input);
50
50
 
51
- // Default: resolve content/ relative to cwd (works in ESM and jest/commonjs).
52
- const root = contentRoot ?? join(process.cwd(), "content");
51
+ // Default: the one content-root resolver (#8070 D3).
52
+ const root = contentRoot ?? mcpContentRoot();
53
53
  const locale = resolveLocale(input.locale);
54
54
 
55
55
  const adapter = getMcpMdxContentAdapter({ contentRoot: root, locale });
@@ -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
- const res = await apiClient.leads.callback(body);
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
  }
@@ -20,8 +20,6 @@
20
20
  * Session/scope: none — read-only public content.
21
21
  */
22
22
 
23
- import { join } from "node:path";
24
-
25
23
  import {
26
24
  searchContentInputSchema,
27
25
  searchContentOutputSchema,
@@ -29,7 +27,10 @@ import {
29
27
  type SearchContentOutput,
30
28
  } from "@working-theory/validation";
31
29
 
32
- import { getMcpContentAdapter } from "../lib/content-adapter.js";
30
+ import {
31
+ getMcpContentAdapter,
32
+ mcpContentRoot,
33
+ } from "../lib/content-adapter.js";
33
34
  import { resolveLocale } from "../lib/resolve-locale.js";
34
35
 
35
36
  export { searchContentInputSchema, searchContentOutputSchema };
@@ -41,7 +42,7 @@ const DEFAULT_LIMIT = 10;
41
42
  * search_content handler.
42
43
  *
43
44
  * Accepts an optional `contentRoot` override for testing. When omitted,
44
- * resolves the content root relative to cwd (supports ESM runtime).
45
+ * resolves it through `mcpContentRoot()` (#8070 D3).
45
46
  */
46
47
  export async function ailkSearchContent(
47
48
  input: SearchContentInput,
@@ -49,7 +50,7 @@ export async function ailkSearchContent(
49
50
  ): Promise<SearchContentOutput> {
50
51
  searchContentInputSchema.parse(input);
51
52
 
52
- const root = contentRoot ?? join(process.cwd(), "content");
53
+ const root = contentRoot ?? mcpContentRoot();
53
54
  const locale = resolveLocale(input.locale);
54
55
  const adapter = getMcpContentAdapter({ contentRoot: root, locale });
55
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
- const res = await apiClient.leads.subscribe(body);
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
  }
@@ -4,6 +4,8 @@
4
4
  "outDir": "./dist",
5
5
  "rootDir": "./src",
6
6
  "composite": true,
7
+ "module": "ESNext",
8
+ "moduleResolution": "bundler",
7
9
  "types": ["node"]
8
10
  },
9
11
  "include": ["src"],
@@ -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 describing the origin of the Work and
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 Support. While redistributing
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 support.
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 2026 Tailwind AI
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.
@@ -0,0 +1,2 @@
1
+ AI Launch Kit
2
+ Copyright 2026 SalesSmyth LLC
@@ -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
- description_for_human:
70
- "AX-first OSS template — ship software with API + MCP as canonical agent surfaces.",
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: {