create-ailk 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -5
- package/dist/cli.js +17 -2
- package/dist/content-payloads.d.ts +37 -0
- package/dist/content-payloads.js +64 -0
- package/dist/lib/fetch-module.d.ts +3 -3
- package/dist/lib/fetch-module.js +11 -5
- package/dist/lib/fetch-template.d.ts +1 -1
- package/dist/lib/fetch-template.js +7 -4
- package/dist/lib/module-license-gate.d.ts +29 -23
- package/dist/lib/module-license-gate.js +98 -0
- package/dist/module-architecture.d.ts +9 -0
- package/dist/module-architecture.js +46 -14
- package/dist/parse-args.d.ts +2 -0
- package/dist/parse-args.js +7 -0
- package/dist/programmatic.js +12 -4
- package/dist/starter-grid.d.ts +4 -67
- package/dist/starter-grid.js +6 -84
- package/dist/surfaces.js +2 -3
- package/dist/template-manifest.d.ts +1 -1
- package/dist/template-manifest.js +5 -5
- package/package.json +6 -4
- package/release-scope.json +25 -0
- package/site-templates/designs/minimalism/sample/README.md +2 -2
- package/site-templates/designs/minimalism/sample/content/en/HomePage/home.mdx +1 -1
- package/site-templates/marketing-site-free/TEMPLATE-MANIFEST.md +4 -3
- package/templates/.claude/agents/web.md +10 -8
- package/templates/.claude/rules/architecture.md +10 -7
- package/templates/.claude/settings.json +12 -0
- package/templates/.env.example +24 -3
- package/templates/CLAUDE.md +38 -22
- package/templates/README.md +12 -12
- package/templates/apps/api/jest.config.cjs +1 -0
- package/templates/apps/api/package.json +1 -0
- package/templates/apps/mcp/CLAUDE.md +23 -10
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +160 -17
- package/templates/apps/mcp/__tests__/index.test.ts +118 -1
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +425 -5
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +67 -3
- package/templates/apps/mcp/__tests__/server.test.ts +162 -1
- package/templates/apps/mcp/src/index.ts +35 -3
- package/templates/apps/mcp/src/lib/commerce-claim.ts +193 -24
- package/templates/apps/mcp/src/server.ts +77 -5
- package/templates/apps/mcp/src/tools/purchase.ts +1 -1
- package/templates/apps/web/app/.well-known/mcp.json/build-manifest.ts +15 -0
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +10 -3
- package/templates/apps/web/src/lib/commerce-claim.ts +15 -6
- package/templates/content/_site.mdx +23 -4
- package/templates/database/CHANGELOG.md +7 -0
- package/templates/database/package.json +1 -1
- package/templates/package.json +2 -0
- package/versions.json +34 -33
- package/dist/modules/commerce/0.6.0.tar.gz +0 -0
- package/dist/modules/commerce/0.6.1.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.0.tar.gz +0 -0
- package/dist/modules/docs-blog/0.6.1.tar.gz +0 -0
- package/dist/modules/marketing-site/0.6.1.tar.gz +0 -0
- package/dist/modules/minimalism-site/0.6.1.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.0.tar.gz +0 -0
- package/dist/modules/multi-tenant/0.6.1.tar.gz +0 -0
- package/dist/modules/saas-product/0.6.1.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.0.tar.gz +0 -0
- package/dist/modules/scheduling/0.6.1.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.0.tar.gz +0 -0
- package/dist/modules/signal-collectors/0.6.1.tar.gz +0 -0
- package/dist/modules/simple-service/0.6.1.tar.gz +0 -0
- package/dist/modules/workspace/0.6.0.tar.gz +0 -0
- package/dist/modules/workspace/0.6.1.tar.gz +0 -0
- package/site-templates/aliases/minimalism-site.TEMPLATE-MANIFEST.json +0 -538
- package/templates/.claude/skills/README.md +0 -37
- package/templates/.claude/skills/add-auth/SKILL.md +0 -261
- package/templates/.claude/skills/add-auth/references/env-vars.md +0 -98
- package/templates/.claude/skills/add-auth/references/oauth-app-setup.md +0 -172
- package/templates/.claude/skills/add-page/SKILL.md +0 -260
- package/templates/.claude/skills/add-page/references/page-types.md +0 -124
- package/templates/.claude/skills/add-page/scripts/site-pages.mjs +0 -87
- package/templates/.claude/skills/add-page/templates/comparison-page.mdx.tmpl +0 -99
- package/templates/.claude/skills/add-page/templates/content.mdx.tmpl +0 -18
- package/templates/.claude/skills/add-page/templates/flow-landing-page.mdx.tmpl +0 -43
- package/templates/.claude/skills/add-page/templates/report-page.mdx.tmpl +0 -52
- package/templates/.claude/skills/add-page/templates/reports-list.mdx.tmpl +0 -38
- package/templates/.claude/skills/check-site/SKILL.md +0 -235
- package/templates/.claude/skills/check-site/references/common-errors.md +0 -113
- package/templates/.claude/skills/check-site/references/findings-glossary.md +0 -140
- package/templates/.claude/skills/check-site/references/severity-levels.md +0 -114
- package/templates/.claude/skills/fill-page/SKILL.md +0 -199
- package/templates/.claude/skills/fill-page/profiles/plumbing.yaml +0 -80
- package/templates/.claude/skills/fill-page/references/section-sources.md +0 -41
- package/templates/.claude/skills/setup-database/SKILL.md +0 -156
- package/templates/.claude/skills/setup-database/scripts/verify-inbox.ts +0 -154
- package/templates/.claude/skills/setup-deploy/SKILL.md +0 -275
- package/templates/.claude/skills/setup-deploy/references/env-vars-template.md +0 -192
- package/templates/.claude/skills/setup-deploy/references/runbook-template.md +0 -204
- package/templates/.claude/skills/setup-deploy/references/vercel-json-template.md +0 -98
- package/templates/.claude/skills/update-page/SKILL.md +0 -117
- package/templates/.claude/skills/update-page/scripts/update-page.ts +0 -640
- package/templates/.claude/skills/update-page/templates/redirects.mjs.tmpl +0 -31
- package/templates/.claude/skills/upgrade-plan/SKILL.md +0 -121
- package/templates/.claude/skills/upgrade-plan/references/options.md +0 -24
- package/templates/.claude/skills/upgrade-plan/scripts/upgrade-plan.ts +0 -527
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-31.552Z-fallback-1789916251590-037e9005.json +0 -8
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.840Z-fallback-1789916253842-b52659e9.json +0 -12
- package/templates/apps/api/.ailk/inbox/2026-09-20T14-57-33.844Z-fallback-1789916253845-6b266f8c.json +0 -12
- package/templates/apps/mcp/__tests__/lib/require-paid-tier.test.ts +0 -75
- package/templates/apps/mcp/src/lib/require-paid-tier.ts +0 -44
package/templates/CLAUDE.md
CHANGED
|
@@ -21,7 +21,7 @@ apps/api/ Fastify HTTP API — lead capture, checkout, webhooks
|
|
|
21
21
|
apps/mcp/ MCP server — agent-callable tools
|
|
22
22
|
database/ Prisma schema + migrations
|
|
23
23
|
content/ Site settings (_site.mdx) and MDX pages (en/<TypeName>/)
|
|
24
|
-
.claude/ Rules,
|
|
24
|
+
.claude/ Rules, the agent definition and settings for Claude Code
|
|
25
25
|
tools/ This repo's own checks (architecture, CLAUDE.md drift)
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -67,7 +67,7 @@ your module set; `pnpm architecture` enforces it, and a hook in
|
|
|
67
67
|
| Diff hygiene when editing existing files | `.claude/rules/preserve-existing-style.md` |
|
|
68
68
|
| Code style, testing, security | `CONVENTIONS.md` |
|
|
69
69
|
| Environment variables and what they unlock | `.env.example` |
|
|
70
|
-
| Deploy configuration | `apps/api/vercel.json`; the
|
|
70
|
+
| Deploy configuration | `apps/api/vercel.json`; the `/ailk:setup-deploy` skill |
|
|
71
71
|
| Verification before push | `pnpm preflight` (see § Checks) |
|
|
72
72
|
|
|
73
73
|
<!-- /ailk:template id="resource-map" -->
|
|
@@ -76,18 +76,34 @@ your module set; `pnpm architecture` enforces it, and a hook in
|
|
|
76
76
|
|
|
77
77
|
## Skills
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
The AI Launch Kit skills come from the `ailk` Claude Code plugin, not from
|
|
80
|
+
this repo. `.claude/settings.json` names its marketplace and enables it, so
|
|
81
|
+
Claude Code loads it once you trust this folder. To install it by hand, run
|
|
82
|
+
these two lines in a Claude Code session:
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
84
|
+
```
|
|
85
|
+
/plugin marketplace add working-theory-labs/claude-plugins
|
|
86
|
+
/plugin install ailk@working-theory
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Skill | Use when |
|
|
90
|
+
| ---------------------- | ----------------------------------------------------------------- |
|
|
91
|
+
| `/ailk:add-page` | You need a new page, or extra JSON-LD on an existing one |
|
|
92
|
+
| `/ailk:update-page` | A page should live at a different URL, or needs a new title |
|
|
93
|
+
| `/ailk:fill-page` | A page still carries `{placeholder}` copy |
|
|
94
|
+
| `/ailk:check-site` | You want the site's citation-readiness score and findings |
|
|
95
|
+
| `/ailk:setup-database` | Leads should land in a database inbox at `/leads` |
|
|
96
|
+
| `/ailk:setup-deploy` | You are ready to deploy to Vercel |
|
|
97
|
+
| `/ailk:add-auth` | Visitors need to sign in (email, Google, GitHub) |
|
|
98
|
+
| `/ailk:upgrade-plan` | You are weighing Free against a paid level or the commerce add-on |
|
|
99
|
+
|
|
100
|
+
A repo scaffolded by an earlier create-ailk also carries its own older
|
|
101
|
+
copies of these skills. Once the plugin is installed, delete those copies;
|
|
102
|
+
keep scaffold-commerce, which the commerce module delivers.
|
|
103
|
+
|
|
104
|
+
For design work, the third-party impeccable plugin is optional:
|
|
105
|
+
`/plugin marketplace add pbakaus/impeccable`, then
|
|
106
|
+
`/plugin install impeccable@impeccable`.
|
|
91
107
|
|
|
92
108
|
<!-- /ailk:template id="skills" -->
|
|
93
109
|
|
|
@@ -95,15 +111,15 @@ In `.claude/skills/`; Claude Code finds them at session start.
|
|
|
95
111
|
|
|
96
112
|
## Checks
|
|
97
113
|
|
|
98
|
-
| Command
|
|
99
|
-
|
|
|
100
|
-
| `pnpm preflight`
|
|
101
|
-
| `pnpm architecture`
|
|
102
|
-
| `pnpm claude-md:check`
|
|
103
|
-
| `pnpm type-check`
|
|
104
|
-
| `pnpm lint`
|
|
105
|
-
| `pnpm test`
|
|
106
|
-
|
|
|
114
|
+
| Command | Catches |
|
|
115
|
+
| ------------------------ | -------------------------------------------------------- |
|
|
116
|
+
| `pnpm preflight` | The next four and `build`, not `test`. Run before a push |
|
|
117
|
+
| `pnpm architecture` | An import that crosses a boundary in `architecture.yaml` |
|
|
118
|
+
| `pnpm claude-md:check` | A hand-edit inside a template-owned section of this file |
|
|
119
|
+
| `pnpm type-check` | Type errors in every workspace |
|
|
120
|
+
| `pnpm lint` | Lint errors in every workspace |
|
|
121
|
+
| `pnpm test` | Failing unit tests |
|
|
122
|
+
| `/ailk:check-site` skill | Missing or invalid JSON-LD, weak citation-readiness |
|
|
107
123
|
|
|
108
124
|
<!-- /ailk:template id="checks" -->
|
|
109
125
|
|
package/templates/README.md
CHANGED
|
@@ -73,7 +73,7 @@ A page marked `--shape organization-product` is in your site when you scaffolded
|
|
|
73
73
|
|
|
74
74
|
To customize the home page, edit [`content/en/HomePage/home.mdx`](./content/en/HomePage/home.mdx). Save and reload the dev server. No build step.
|
|
75
75
|
|
|
76
|
-
To add a new page, ask Claude Code to run the
|
|
76
|
+
To add a new page, ask Claude Code to run the `/ailk:add-page` skill — it writes the page's content file and nav link in one confirm-before-write pass, and adds a missing route with `create-ailk add-route` first. See [Skills](#skills).
|
|
77
77
|
|
|
78
78
|
> `content/_site.mdx` stays at the top level (not under `content/en/`) — site identity is locale-blind.
|
|
79
79
|
|
|
@@ -200,20 +200,20 @@ Change the ranges by hand. Below 1.0 a caret range (`^0.26.0`) never crosses a m
|
|
|
200
200
|
|
|
201
201
|
## Skills
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
The AI Launch Kit skills come from the `ailk` Claude Code plugin; `.claude/settings.json` names it, and CLAUDE.md § Skills has the two install lines.
|
|
204
204
|
|
|
205
205
|
<!-- generated:begin skills -->
|
|
206
206
|
|
|
207
|
-
| Skill
|
|
208
|
-
|
|
|
209
|
-
|
|
|
210
|
-
|
|
|
211
|
-
|
|
|
212
|
-
|
|
|
213
|
-
|
|
|
214
|
-
|
|
|
215
|
-
|
|
|
216
|
-
|
|
|
207
|
+
| Skill | What it does |
|
|
208
|
+
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
209
|
+
| `/ailk:add-auth` | Activate the site's sign-in providers (email-and-password, Google OAuth, GitHub OAuth) and write the env vars for the ones you pick. |
|
|
210
|
+
| `/ailk:add-page` | Add a new page — its content file, its structured data and its nav link — in one confirm-before-write pass. |
|
|
211
|
+
| `/ailk:check-site` | Run `ailk-audit` on the site and explain the score and findings in plain language. It never edits files. |
|
|
212
|
+
| `/ailk:fill-page` | Fill one existing page's section content from a category profile: labelled sample content, or your own first draft from a short intake. |
|
|
213
|
+
| `/ailk:setup-database` | Take the site from no database to a working lead inbox, and prove it with one test message that must appear in the inbox. |
|
|
214
|
+
| `/ailk:setup-deploy` | Emit deploy configuration — `vercel.json` and `RUNBOOK.md`, plus an env checklist read from the site's own `.env.example`. |
|
|
215
|
+
| `/ailk:update-page` | Rename a page — its URL, its title, or both — in one plan-then-confirm pass, with a permanent redirect from the old URL. |
|
|
216
|
+
| `/ailk:upgrade-plan` | Explain the AI Launch Kit options side by side, recommend one option for this site, and link the public pricing page for prices. |
|
|
217
217
|
|
|
218
218
|
<!-- generated:end -->
|
|
219
219
|
|
|
@@ -64,6 +64,7 @@ module.exports = {
|
|
|
64
64
|
'^@working-theory/schema-pack-registry$': '<rootDir>/../../packages/schema-pack-registry/src/index.ts',
|
|
65
65
|
'^@working-theory/schema-validator$': '<rootDir>/../../packages/schema-validator/src/index.ts',
|
|
66
66
|
'^@working-theory/stripe-client$': '<rootDir>/../../packages/stripe-client/src/index.ts',
|
|
67
|
+
'^@working-theory/metadata$': '<rootDir>/../../packages/metadata/src/index.ts',
|
|
67
68
|
'^@working-theory/validation$': '<rootDir>/../../packages/validation/src/index.ts',
|
|
68
69
|
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
69
70
|
},
|
|
@@ -25,8 +25,8 @@ Every tool maps 1:1 to an `apps/api` endpoint. When you change a tool, change th
|
|
|
25
25
|
**Read (9):** `list_pages`, `read_content`, `search_content`, `get_site_info`, `get_business_hours`, `get_locations`, `get_offerings`, `get_pricing`, `get_faq`
|
|
26
26
|
**Conversion-write (3):** `capture_lead`, `subscribe_newsletter`, `request_callback`
|
|
27
27
|
**Maintenance (2):** `audit`, `create_project`
|
|
28
|
-
**Flow (2):** `get_flow`, `submit_flow_step` — module-gated (`module: "signal-collectors"`, D-48, #5366)
|
|
29
|
-
**Booking (3):** `list_availability`, `get_event_meta`, `create_booking` — module-gated (`module: "scheduling"`)
|
|
28
|
+
**Flow (2):** `get_flow`, `submit_flow_step` — module-gated (`module: "signal-collectors"`, D-48, #5366) and licence-feature-tagged (`feature: "flows-deliverables"`, #7136): identity-blind like every tool in this list, registered only when the signal-collectors module is present, and gated on the deployment's `flows-deliverables` licence feature under `MCP_FEATURE_GATE_MODE`, the same shape Booking below has
|
|
29
|
+
**Booking (3):** `list_availability`, `get_event_meta`, `create_booking` — module-gated (`module: "scheduling"`) and licence-feature-tagged (`feature: "scheduling"`, #7136), gated on the deployment's `scheduling` licence feature under `MCP_FEATURE_GATE_MODE`
|
|
30
30
|
|
|
31
31
|
### Why the parity test counts 20 and a keyless server registers 19
|
|
32
32
|
|
|
@@ -41,11 +41,21 @@ Every tool maps 1:1 to an `apps/api` endpoint. When you change a tool, change th
|
|
|
41
41
|
`addon: "commerce"`, so it is the one entry whose presence depends on the
|
|
42
42
|
licence — 19 without the claim, 20 with it.
|
|
43
43
|
- **19 free anonymous tools**: since #5853 retired the three content-write
|
|
44
|
-
tools,
|
|
45
|
-
|
|
44
|
+
tools, every registered tool remains anonymous — call-time gating checks a
|
|
45
|
+
licence claim, never an identity. The five carrying `tier: "pro"`
|
|
46
46
|
(`get_flow`, `submit_flow_step`, `list_availability`, `get_event_meta`,
|
|
47
|
-
`create_booking`) are
|
|
48
|
-
|
|
47
|
+
`create_booking`) are gated at REGISTRATION **and at call time** (#7315):
|
|
48
|
+
each needs its module, and each catalog row names the licence `feature:`
|
|
49
|
+
that entitles it. `start_purchase` is gated at call time by both this
|
|
50
|
+
server-wide gate and its own Gate 2 in `tools/purchase.ts`.
|
|
51
|
+
`MCP_FEATURE_GATE_MODE` (`packages/metadata`) decides what an unheld
|
|
52
|
+
feature does. In `"warn"` mode (this release) the five register on module
|
|
53
|
+
presence alone, and `index.ts` logs one boot warning (stderr) per tool
|
|
54
|
+
served without its feature. In `"refuse"` mode (the next release) they
|
|
55
|
+
also need the deployment's licence to hold the feature, and
|
|
56
|
+
`/.well-known/mcp.json` drops them in step. The claim is resolved at boot
|
|
57
|
+
and re-read every `MCP_CLAIM_REFRESH_MS` through `checkLicense()`
|
|
58
|
+
(`lib/commerce-claim.ts`) and fails closed.
|
|
49
59
|
`index.ts` is the transport entry point and exports no tools — registration
|
|
50
60
|
is entirely via `createMcpServer`.
|
|
51
61
|
|
|
@@ -69,7 +79,8 @@ Every tool maps 1:1 to an `apps/api` endpoint. When you change a tool, change th
|
|
|
69
79
|
`commerce` licence claim, per its `addon: "commerce"` catalog marker. A
|
|
70
80
|
keyless site advertises it in neither the discovery manifest nor the running
|
|
71
81
|
server's tool list, and calling it by name there returns the module-disabled
|
|
72
|
-
`not_found` body. The claim is resolved
|
|
82
|
+
`not_found` body. The claim is resolved at boot and re-read every
|
|
83
|
+
`MCP_CLAIM_REFRESH_MS`
|
|
73
84
|
(`src/lib/commerce-claim.ts`, awaited in `src/index.ts`) and read
|
|
74
85
|
synchronously by both the registration filter and the handler's own
|
|
75
86
|
re-check. Calls are rate-limited per IP by the AbuseGuard
|
|
@@ -111,14 +122,16 @@ Example (lead routing):
|
|
|
111
122
|
## What NOT to do
|
|
112
123
|
|
|
113
124
|
- Don't add identity arguments to OSS tools.
|
|
114
|
-
- Don't
|
|
125
|
+
- Don't add a paid tool without a `feature:` marker on its catalog row — registration and the manifest gate on the deployment's claim, never on a caller-supplied key or argument.
|
|
115
126
|
- Don't import `@working-theory/*` from `apps/mcp`.
|
|
116
127
|
- Don't add a tool without the matching `apps/api` endpoint.
|
|
117
128
|
- Don't put business logic in tool handlers — they're adapters.
|
|
118
129
|
|
|
119
|
-
Last updated: 2026-09-
|
|
130
|
+
Last updated: 2026-09-29 — #7136: the five `tier: "pro"` tools name their licence `feature:` (`scheduling`, `flows-deliverables`) and gate on the deployment's own claim under `MCP_FEATURE_GATE_MODE` — `"warn"` this release (served, one boot warning each), `"refuse"` the next. The unused call-time paid-tier guard is deleted.
|
|
120
131
|
|
|
121
|
-
Prior: 2026-
|
|
132
|
+
Prior: 2026-09-17 — #5971: `start_purchase` re-enabled behind the `commerce` licence claim. `OSS_DISABLED_TOOLS` is now empty; the registered count is 19 on a keyless deployment and 20 on a commerce-claimed one; the tool's input field is `productCode` (was `priceId`), matching its HTTP twin; a per-IP daily cap (`MCP_ABUSE_START_PURCHASE_DAILY_CAP`) covers it.
|
|
133
|
+
|
|
134
|
+
Prior: 2026-07-09 — corrected the tool-count explanation (18 catalog → 17 registered → 14 free anonymous) and the Pro-tool gating (registered + gated by the call-time paid-tier guard, not `OSS_DISABLED_TOOLS`-disabled).
|
|
122
135
|
|
|
123
136
|
Last updated: 2026-09-17 — doc-sync: tool counts "23 catalog → 22 registered → 19 free anonymous" → "20 in TOOLS → 19 registered → 19 free anonymous", and the Pro-tools section rewritten to record that `create_page`/`update_page`/`delete_page` were RETIRED by #5853 rather than disabled (D7) — derived from `packages/metadata/src/mcp-tool-catalog.ts` and verified against `apps/mcp/src/server.ts`.
|
|
124
137
|
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* `start_purchase` visible, so comparing a claimed server against an
|
|
14
14
|
* unclaimed catalog (or vice versa) would compare unlike surfaces and
|
|
15
15
|
* report drift that isn't there — or, worse, miss drift that is.
|
|
16
|
+
* - …and under every licence-feature state × gate mode (#7136): 2 commerce
|
|
17
|
+
* states × {warn, refuse} × 4 held-feature sets = 16. In refuse mode a
|
|
18
|
+
* feature's tools are in both surfaces iff the feature is held; in warn
|
|
19
|
+
* mode all five are in both.
|
|
16
20
|
*
|
|
17
21
|
* What this does NOT assert:
|
|
18
22
|
* - inputSchema equality — the server registers Zod schemas; the catalog
|
|
@@ -21,14 +25,35 @@
|
|
|
21
25
|
*/
|
|
22
26
|
|
|
23
27
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
24
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
getMcpToolCatalog,
|
|
30
|
+
type McpCatalogOptions,
|
|
31
|
+
type McpToolCatalogEntry,
|
|
32
|
+
} from "@working-theory/metadata";
|
|
25
33
|
|
|
26
34
|
import {
|
|
35
|
+
MCP_CLAIM_REFRESH_MS,
|
|
27
36
|
__setMcpCommerceClaimForTest,
|
|
37
|
+
__setMcpFeatureGateForTest,
|
|
38
|
+
__setMcpHeldFeaturesForTest,
|
|
39
|
+
initMcpCommerceClaim,
|
|
28
40
|
resetMcpCommerceClaim,
|
|
41
|
+
startMcpClaimRefresh,
|
|
29
42
|
type McpCommerceClaim,
|
|
30
43
|
} from "../src/lib/commerce-claim.js";
|
|
31
|
-
import { createMcpServer } from "../src/server.js";
|
|
44
|
+
import { createMcpServer, listRegisteredTools } from "../src/server.js";
|
|
45
|
+
|
|
46
|
+
/** The injected licence reader — same seam `commerce-claim.test.ts` types. */
|
|
47
|
+
type CheckLicenseSeam = NonNullable<
|
|
48
|
+
NonNullable<Parameters<typeof initMcpCommerceClaim>[0]>["checkLicense"]
|
|
49
|
+
>;
|
|
50
|
+
|
|
51
|
+
type LicenseShape = Awaited<ReturnType<CheckLicenseSeam>>;
|
|
52
|
+
|
|
53
|
+
/** A licence reader that resolves to the given (partial) result. */
|
|
54
|
+
function resolving(result: Partial<LicenseShape>): CheckLicenseSeam {
|
|
55
|
+
return () => Promise.resolve(result as LicenseShape);
|
|
56
|
+
}
|
|
32
57
|
|
|
33
58
|
interface RegisteredTool {
|
|
34
59
|
description?: string;
|
|
@@ -47,29 +72,70 @@ const PAID_MODULES = "scheduling,content-write,signal-collectors";
|
|
|
47
72
|
* @modelcontextprotocol/sdk ^1.29.0 — if the SDK renames the field, change it
|
|
48
73
|
* here once rather than at each call site.
|
|
49
74
|
*/
|
|
50
|
-
function registeredToolsOf(
|
|
51
|
-
server: McpServer,
|
|
52
|
-
): Record<string, RegisteredTool> {
|
|
75
|
+
function registeredToolsOf(server: McpServer): Record<string, RegisteredTool> {
|
|
53
76
|
return (
|
|
54
77
|
// eslint-disable-next-line no-restricted-syntax -- #506/#507: the SDK exposes no public accessor; the double assertion is confined to this one helper
|
|
55
|
-
server as unknown as { _registeredTools: Record<string, RegisteredTool> }
|
|
56
|
-
|
|
78
|
+
(server as unknown as { _registeredTools: Record<string, RegisteredTool> })
|
|
79
|
+
._registeredTools
|
|
80
|
+
);
|
|
57
81
|
}
|
|
58
82
|
|
|
83
|
+
type GateMode = NonNullable<McpCatalogOptions["featureGate"]>;
|
|
84
|
+
|
|
85
|
+
type McpFeature = NonNullable<McpToolCatalogEntry["feature"]>;
|
|
86
|
+
|
|
87
|
+
/** The five licence-feature tools, by the feature that entitles them. */
|
|
88
|
+
const FEATURE_TOOLS: Record<McpFeature, readonly string[]> = {
|
|
89
|
+
scheduling: ["list_availability", "get_event_meta", "create_booking"],
|
|
90
|
+
"flows-deliverables": ["get_flow", "submit_flow_step"],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const HELD_SETS: readonly (readonly McpFeature[])[] = [
|
|
94
|
+
[],
|
|
95
|
+
["scheduling"],
|
|
96
|
+
["flows-deliverables"],
|
|
97
|
+
["scheduling", "flows-deliverables"],
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
/** Every claim state: commerce × gate mode × held features (16). */
|
|
101
|
+
const CLAIM_STATES = (["unclaimed", "claimed"] as const).flatMap((claim) =>
|
|
102
|
+
(["warn", "refuse"] as const).flatMap((mode) =>
|
|
103
|
+
HELD_SETS.map(
|
|
104
|
+
(held) =>
|
|
105
|
+
[claim, mode, held] as [
|
|
106
|
+
McpCommerceClaim,
|
|
107
|
+
GateMode,
|
|
108
|
+
readonly McpFeature[],
|
|
109
|
+
],
|
|
110
|
+
),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
|
|
59
114
|
/** The catalog options a given claim state implies — the same mapping
|
|
60
115
|
* `server.ts` applies internally, so both surfaces are read like for like. */
|
|
61
|
-
function catalogOptionsFor(
|
|
62
|
-
|
|
116
|
+
function catalogOptionsFor(
|
|
117
|
+
claim: McpCommerceClaim,
|
|
118
|
+
mode: GateMode,
|
|
119
|
+
held: readonly McpFeature[],
|
|
120
|
+
): McpCatalogOptions {
|
|
121
|
+
return {
|
|
122
|
+
addons: { commerce: claim === "claimed" },
|
|
123
|
+
features: held,
|
|
124
|
+
featureGate: mode,
|
|
125
|
+
};
|
|
63
126
|
}
|
|
64
127
|
|
|
65
|
-
describe.each
|
|
66
|
-
"MCP catalog ↔ server drift — commerce
|
|
67
|
-
(claim) => {
|
|
128
|
+
describe.each(CLAIM_STATES)(
|
|
129
|
+
"MCP catalog ↔ server drift — commerce %s, mode %s, held %j",
|
|
130
|
+
(claim, mode, held) => {
|
|
68
131
|
const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
|
|
132
|
+
const options = catalogOptionsFor(claim, mode, held);
|
|
69
133
|
|
|
70
134
|
beforeEach(() => {
|
|
71
135
|
process.env.AILK_ENABLED_MODULES = PAID_MODULES;
|
|
72
136
|
__setMcpCommerceClaimForTest(claim);
|
|
137
|
+
__setMcpFeatureGateForTest(mode);
|
|
138
|
+
__setMcpHeldFeaturesForTest(held);
|
|
73
139
|
});
|
|
74
140
|
|
|
75
141
|
afterEach(() => {
|
|
@@ -87,7 +153,7 @@ describe.each<McpCommerceClaim>(["unclaimed", "claimed"])(
|
|
|
87
153
|
it("registered tool names equal catalog tool names", () => {
|
|
88
154
|
const registered = registeredToolsOf(createMcpServer());
|
|
89
155
|
const registeredNames = Object.keys(registered).sort();
|
|
90
|
-
const catalogNames = getMcpToolCatalog(
|
|
156
|
+
const catalogNames = getMcpToolCatalog(options)
|
|
91
157
|
.map((t) => t.name)
|
|
92
158
|
.sort();
|
|
93
159
|
expect(registeredNames).toEqual(catalogNames);
|
|
@@ -95,7 +161,7 @@ describe.each<McpCommerceClaim>(["unclaimed", "claimed"])(
|
|
|
95
161
|
|
|
96
162
|
it("registered tool descriptions equal catalog descriptions", () => {
|
|
97
163
|
const registered = registeredToolsOf(createMcpServer());
|
|
98
|
-
for (const entry of getMcpToolCatalog(
|
|
164
|
+
for (const entry of getMcpToolCatalog(options)) {
|
|
99
165
|
expect(registered[entry.name]).toBeDefined();
|
|
100
166
|
expect(registered[entry.name]!.description).toBe(entry.description);
|
|
101
167
|
}
|
|
@@ -107,10 +173,87 @@ describe.each<McpCommerceClaim>(["unclaimed", "claimed"])(
|
|
|
107
173
|
|
|
108
174
|
expect("start_purchase" in registered).toBe(expected);
|
|
109
175
|
expect(
|
|
110
|
-
getMcpToolCatalog(
|
|
111
|
-
(t) => t.name === "start_purchase",
|
|
112
|
-
),
|
|
176
|
+
getMcpToolCatalog(options).some((t) => t.name === "start_purchase"),
|
|
113
177
|
).toBe(expected);
|
|
114
178
|
});
|
|
179
|
+
|
|
180
|
+
it("each feature's tools appear in both surfaces exactly when the gate lets them", () => {
|
|
181
|
+
const registered = registeredToolsOf(createMcpServer());
|
|
182
|
+
const catalogNames = getMcpToolCatalog(options).map((t) => t.name);
|
|
183
|
+
|
|
184
|
+
for (const [feature, tools] of Object.entries(FEATURE_TOOLS)) {
|
|
185
|
+
const expected =
|
|
186
|
+
mode === "warn" || held.includes(feature as McpFeature);
|
|
187
|
+
for (const name of tools) {
|
|
188
|
+
expect(name in registered).toBe(expected);
|
|
189
|
+
expect(catalogNames.includes(name)).toBe(expected);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
115
193
|
},
|
|
116
194
|
);
|
|
195
|
+
|
|
196
|
+
describe("MCP catalog ↔ server agreement after a bounded re-read (#7156 AC-1)", () => {
|
|
197
|
+
const priorEnabledModules = process.env.AILK_ENABLED_MODULES;
|
|
198
|
+
|
|
199
|
+
beforeEach(() => {
|
|
200
|
+
process.env.AILK_ENABLED_MODULES = PAID_MODULES;
|
|
201
|
+
jest.useFakeTimers();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
afterEach(() => {
|
|
205
|
+
resetMcpCommerceClaim();
|
|
206
|
+
jest.useRealTimers();
|
|
207
|
+
if (priorEnabledModules === undefined) {
|
|
208
|
+
delete process.env.AILK_ENABLED_MODULES;
|
|
209
|
+
} else {
|
|
210
|
+
process.env.AILK_ENABLED_MODULES = priorEnabledModules;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("a new server and listRegisteredTools() match the catalog once the re-read resolves", async () => {
|
|
215
|
+
__setMcpFeatureGateForTest("refuse");
|
|
216
|
+
await initMcpCommerceClaim({
|
|
217
|
+
checkLicense: resolving({
|
|
218
|
+
tier: "pro",
|
|
219
|
+
features: ["scheduling", "flows-deliverables", "commerce"],
|
|
220
|
+
}),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const bootOptions: McpCatalogOptions = {
|
|
224
|
+
addons: { commerce: true },
|
|
225
|
+
features: ["scheduling", "flows-deliverables"],
|
|
226
|
+
featureGate: "refuse",
|
|
227
|
+
};
|
|
228
|
+
const bootNames = Object.keys(registeredToolsOf(createMcpServer())).sort();
|
|
229
|
+
expect(bootNames).toEqual(
|
|
230
|
+
getMcpToolCatalog(bootOptions)
|
|
231
|
+
.map((t) => t.name)
|
|
232
|
+
.sort(),
|
|
233
|
+
);
|
|
234
|
+
expect(bootNames).toHaveLength(20);
|
|
235
|
+
|
|
236
|
+
startMcpClaimRefresh({ checkLicense: resolving({ features: [] }) });
|
|
237
|
+
await jest.advanceTimersByTimeAsync(MCP_CLAIM_REFRESH_MS);
|
|
238
|
+
|
|
239
|
+
const refreshedOptions: McpCatalogOptions = {
|
|
240
|
+
addons: { commerce: false },
|
|
241
|
+
features: [],
|
|
242
|
+
featureGate: "refuse",
|
|
243
|
+
};
|
|
244
|
+
const catalogNames = getMcpToolCatalog(refreshedOptions)
|
|
245
|
+
.map((t) => t.name)
|
|
246
|
+
.sort();
|
|
247
|
+
expect(catalogNames).toHaveLength(14);
|
|
248
|
+
|
|
249
|
+
const serverNames = Object.keys(
|
|
250
|
+
registeredToolsOf(createMcpServer()),
|
|
251
|
+
).sort();
|
|
252
|
+
expect(serverNames).toEqual(catalogNames);
|
|
253
|
+
|
|
254
|
+
const listedNames = listRegisteredTools()
|
|
255
|
+
.map((t) => t.name)
|
|
256
|
+
.sort();
|
|
257
|
+
expect(listedNames).toEqual(catalogNames);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structural ordering guard for apps/mcp's bootstrap (#5876 spec D4, AC-3
|
|
2
|
+
* Structural ordering guard for apps/mcp's bootstrap (#5876 spec D4, AC-3;
|
|
3
|
+
* #7156 AC-1, AC-2).
|
|
3
4
|
*
|
|
4
5
|
* `apps/mcp/src/index.ts` has real side effects at import time (starts a
|
|
5
6
|
* transport, opens a stdio/HTTP listener) — it cannot safely be imported by
|
|
@@ -8,6 +9,9 @@
|
|
|
8
9
|
*/
|
|
9
10
|
import { readFileSync } from "node:fs";
|
|
10
11
|
import { resolve } from "node:path";
|
|
12
|
+
import vm from "node:vm";
|
|
13
|
+
|
|
14
|
+
import * as ts from "typescript";
|
|
11
15
|
|
|
12
16
|
describe("apps/mcp/src/index.ts — bootstrap ordering", () => {
|
|
13
17
|
const src = readFileSync(resolve(__dirname, "../src/index.ts"), "utf-8");
|
|
@@ -37,3 +41,116 @@ describe("apps/mcp/src/index.ts — bootstrap ordering", () => {
|
|
|
37
41
|
expect(initIdx).toBeLessThan(catchIdx);
|
|
38
42
|
});
|
|
39
43
|
});
|
|
44
|
+
|
|
45
|
+
describe("apps/mcp/src/index.ts — the claim refresh loop starts in production (#7156 AC-1)", () => {
|
|
46
|
+
const src = readFileSync(resolve(__dirname, "../src/index.ts"), "utf-8");
|
|
47
|
+
|
|
48
|
+
it("starts after the boot read and before the transport branch", () => {
|
|
49
|
+
const initIdx = src.indexOf("await initMcpCommerceClaim()");
|
|
50
|
+
const callMatches = [...src.matchAll(/^startMcpClaimRefresh\(\);$/gm)];
|
|
51
|
+
const transportBranchIdx = src.indexOf('if (transport === "http")');
|
|
52
|
+
|
|
53
|
+
expect(initIdx).toBeGreaterThan(-1);
|
|
54
|
+
expect(callMatches).toHaveLength(1);
|
|
55
|
+
expect(transportBranchIdx).toBeGreaterThan(-1);
|
|
56
|
+
|
|
57
|
+
const callIdx = callMatches[0]!.index!;
|
|
58
|
+
expect(initIdx).toBeLessThan(callIdx);
|
|
59
|
+
expect(callIdx).toBeLessThan(transportBranchIdx);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("apps/mcp/src/index.ts — the boot warning cannot stop the server (#7156 AC-2, D2)", () => {
|
|
64
|
+
const src = readFileSync(resolve(__dirname, "../src/index.ts"), "utf-8");
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The exact behavioural slice AC-2 specifies: from the last `try {` before
|
|
68
|
+
* the `reportUnlicensedProTools();` call statement, up to `const transport
|
|
69
|
+
* =`. Transpiled and run in a fresh vm context so a real throw from the
|
|
70
|
+
* stubbed function proves the guard, not a mock of the guard itself.
|
|
71
|
+
*/
|
|
72
|
+
function guardedSlice(): string {
|
|
73
|
+
const callMatches = [
|
|
74
|
+
...src.matchAll(/^\s*reportUnlicensedProTools\(\);$/gm),
|
|
75
|
+
];
|
|
76
|
+
expect(callMatches).toHaveLength(1);
|
|
77
|
+
const callIdx = callMatches[0]!.index!;
|
|
78
|
+
const tryIdx = src.lastIndexOf("try {", callIdx);
|
|
79
|
+
const transportIdx = src.indexOf("const transport =");
|
|
80
|
+
|
|
81
|
+
expect(tryIdx).toBeGreaterThan(-1);
|
|
82
|
+
expect(transportIdx).toBeGreaterThan(callIdx);
|
|
83
|
+
|
|
84
|
+
const slice = src.slice(tryIdx, transportIdx);
|
|
85
|
+
// The extracted try block must be the one that immediately wraps the
|
|
86
|
+
// call, not an earlier try{}/await pair (code review, #7156): the two
|
|
87
|
+
// other try blocks in this file each await a boot call, so a regressed
|
|
88
|
+
// boundary would carry "await" into the slice. Asserting that here
|
|
89
|
+
// turns a boundary regression into a clear failure instead of an opaque
|
|
90
|
+
// vm ReferenceError from the un-injected awaited function.
|
|
91
|
+
expect(slice).not.toContain("await");
|
|
92
|
+
|
|
93
|
+
return slice;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function runGuardedSlice(
|
|
97
|
+
reportUnlicensedProTools: () => unknown,
|
|
98
|
+
errorSpy: jest.Mock,
|
|
99
|
+
): void {
|
|
100
|
+
const slice = guardedSlice();
|
|
101
|
+
const { outputText } = ts.transpileModule(slice, {
|
|
102
|
+
compilerOptions: {
|
|
103
|
+
module: ts.ModuleKind.CommonJS,
|
|
104
|
+
target: ts.ScriptTarget.ES2020,
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
vm.runInNewContext(outputText, {
|
|
108
|
+
reportUnlicensedProTools,
|
|
109
|
+
console: { error: errorSpy },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
it("a throw is logged to stderr and does not escape", () => {
|
|
114
|
+
const errorSpy = jest.fn();
|
|
115
|
+
|
|
116
|
+
expect(() =>
|
|
117
|
+
runGuardedSlice(() => {
|
|
118
|
+
throw new Error("boom");
|
|
119
|
+
}, errorSpy),
|
|
120
|
+
).not.toThrow();
|
|
121
|
+
|
|
122
|
+
expect(errorSpy).toHaveBeenCalledTimes(1);
|
|
123
|
+
const call = errorSpy.mock.calls[0]!;
|
|
124
|
+
expect(String(call[0])).toContain("reportUnlicensedProTools");
|
|
125
|
+
const lastArg = call[call.length - 1];
|
|
126
|
+
expect(lastArg).toBeInstanceOf(Error);
|
|
127
|
+
expect((lastArg as Error).message).toBe("boom");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("a successful call is not logged", () => {
|
|
131
|
+
const errorSpy = jest.fn();
|
|
132
|
+
const report = jest.fn(() => []);
|
|
133
|
+
|
|
134
|
+
runGuardedSlice(report, errorSpy);
|
|
135
|
+
|
|
136
|
+
expect(report).toHaveBeenCalledTimes(1);
|
|
137
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("the call statement occurs exactly once, before the transport branch", () => {
|
|
141
|
+
const callMatches = [
|
|
142
|
+
...src.matchAll(/^\s*reportUnlicensedProTools\(\);$/gm),
|
|
143
|
+
];
|
|
144
|
+
const transportBranchIdx = src.indexOf('if (transport === "http")');
|
|
145
|
+
|
|
146
|
+
expect(callMatches).toHaveLength(1);
|
|
147
|
+
expect(callMatches[0]!.index!).toBeLessThan(transportBranchIdx);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("logs to stderr only — the guarded slice never writes stdout", () => {
|
|
151
|
+
const slice = guardedSlice();
|
|
152
|
+
|
|
153
|
+
expect(slice).toContain("console.error");
|
|
154
|
+
expect(slice).not.toContain("console.log");
|
|
155
|
+
});
|
|
156
|
+
});
|