acuvo-code 0.2.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 (154) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/ENTERPRISE.md +927 -0
  3. package/LICENSE +120 -0
  4. package/README.md +1245 -0
  5. package/ROADMAP.md +556 -0
  6. package/bin/acuvo-mcp.mjs +208 -0
  7. package/bin/acuvo.mjs +3108 -0
  8. package/lib/acceptance-consent.mjs +168 -0
  9. package/lib/acceptance.mjs +859 -0
  10. package/lib/account.mjs +226 -0
  11. package/lib/acuvo-dir.mjs +72 -0
  12. package/lib/acuvo-models.mjs +141 -0
  13. package/lib/apply-patch.mjs +570 -0
  14. package/lib/ask-user.mjs +173 -0
  15. package/lib/audit.mjs +530 -0
  16. package/lib/auto-lease.mjs +174 -0
  17. package/lib/background.mjs +842 -0
  18. package/lib/best-of.mjs +334 -0
  19. package/lib/board.mjs +232 -0
  20. package/lib/breaker.mjs +93 -0
  21. package/lib/budget.mjs +1375 -0
  22. package/lib/builtin-skills.mjs +135 -0
  23. package/lib/cache-floor.mjs +204 -0
  24. package/lib/chain.mjs +303 -0
  25. package/lib/changed-paths.mjs +84 -0
  26. package/lib/chat.mjs +434 -0
  27. package/lib/checkpoint.mjs +637 -0
  28. package/lib/child-lifetime.mjs +71 -0
  29. package/lib/cli-args.mjs +1255 -0
  30. package/lib/code-review.mjs +1382 -0
  31. package/lib/colour.mjs +82 -0
  32. package/lib/command.mjs +2847 -0
  33. package/lib/compact.mjs +1151 -0
  34. package/lib/completion.mjs +515 -0
  35. package/lib/creative-engines.mjs +779 -0
  36. package/lib/db-inspect.mjs +1624 -0
  37. package/lib/delete.mjs +111 -0
  38. package/lib/design-loop.mjs +570 -0
  39. package/lib/diff-preview.mjs +1044 -0
  40. package/lib/doctor.mjs +2139 -0
  41. package/lib/dropped.mjs +216 -0
  42. package/lib/edit-diagnostics.mjs +277 -0
  43. package/lib/edit.mjs +460 -0
  44. package/lib/env-file.mjs +250 -0
  45. package/lib/escalate.mjs +702 -0
  46. package/lib/evaluate.mjs +284 -0
  47. package/lib/fetch-text.mjs +952 -0
  48. package/lib/fleet-budget.mjs +256 -0
  49. package/lib/gh.mjs +1536 -0
  50. package/lib/git.mjs +1341 -0
  51. package/lib/github.mjs +261 -0
  52. package/lib/h2.mjs +194 -0
  53. package/lib/handoff.mjs +417 -0
  54. package/lib/hooks.mjs +626 -0
  55. package/lib/http-probe.mjs +907 -0
  56. package/lib/image-director.mjs +322 -0
  57. package/lib/image-edit.mjs +522 -0
  58. package/lib/imagegen.mjs +998 -0
  59. package/lib/interrupt.mjs +234 -0
  60. package/lib/learned.mjs +353 -0
  61. package/lib/lease-watch.mjs +115 -0
  62. package/lib/lease.mjs +868 -0
  63. package/lib/localize.mjs +834 -0
  64. package/lib/log-tail.mjs +1052 -0
  65. package/lib/login.mjs +157 -0
  66. package/lib/lsp.mjs +1613 -0
  67. package/lib/mcp-consent.mjs +377 -0
  68. package/lib/mcp-defaults.mjs +780 -0
  69. package/lib/mcp-server.mjs +1343 -0
  70. package/lib/mcp.mjs +1263 -0
  71. package/lib/media.mjs +1283 -0
  72. package/lib/memory-workspace.mjs +179 -0
  73. package/lib/model-json.mjs +132 -0
  74. package/lib/model-tier.mjs +171 -0
  75. package/lib/model.mjs +1445 -0
  76. package/lib/parallel.mjs +144 -0
  77. package/lib/perchance.mjs +210 -0
  78. package/lib/plan-coherence.mjs +1461 -0
  79. package/lib/plan-ledger.mjs +981 -0
  80. package/lib/plan.mjs +461 -0
  81. package/lib/policy.mjs +783 -0
  82. package/lib/prefix-order.mjs +38 -0
  83. package/lib/project-memory.mjs +127 -0
  84. package/lib/prompt.mjs +109 -0
  85. package/lib/python.mjs +862 -0
  86. package/lib/rcfile.mjs +853 -0
  87. package/lib/read-window.mjs +743 -0
  88. package/lib/refute-tools.mjs +34 -0
  89. package/lib/refute.mjs +806 -0
  90. package/lib/repl-driver.mjs +264 -0
  91. package/lib/repl.mjs +324 -0
  92. package/lib/replay.mjs +1218 -0
  93. package/lib/repo-map.mjs +1101 -0
  94. package/lib/report.mjs +419 -0
  95. package/lib/search-rank.mjs +99 -0
  96. package/lib/search.mjs +659 -0
  97. package/lib/secret-paths.mjs +54 -0
  98. package/lib/session.mjs +1017 -0
  99. package/lib/skills.mjs +703 -0
  100. package/lib/slash.mjs +356 -0
  101. package/lib/spawn-argv.mjs +1151 -0
  102. package/lib/spend.mjs +250 -0
  103. package/lib/steer.mjs +280 -0
  104. package/lib/stream.mjs +253 -0
  105. package/lib/stuck.mjs +712 -0
  106. package/lib/subagent.mjs +749 -0
  107. package/lib/terminal-graphics.mjs +171 -0
  108. package/lib/tool-prefix.mjs +226 -0
  109. package/lib/tool-shortlist.mjs +162 -0
  110. package/lib/tools.mjs +2333 -0
  111. package/lib/tsserver.mjs +423 -0
  112. package/lib/turn.mjs +5672 -0
  113. package/lib/untrusted-block.mjs +271 -0
  114. package/lib/verify-claim.mjs +299 -0
  115. package/lib/vision.mjs +330 -0
  116. package/lib/voice-task.mjs +561 -0
  117. package/lib/warm-provider.mjs +255 -0
  118. package/lib/websearch.mjs +401 -0
  119. package/lib/workspace.mjs +928 -0
  120. package/lib/write-approval.mjs +235 -0
  121. package/lib/write-many.mjs +162 -0
  122. package/package.json +62 -0
  123. package/scripts/bundle.mjs +768 -0
  124. package/scripts/cache-floor.mjs +176 -0
  125. package/scripts/machine.mjs +226 -0
  126. package/scripts/test.mjs +139 -0
  127. package/skills/accessibility.md +87 -0
  128. package/skills/acuvo-design-system.md +123 -0
  129. package/skills/animation.md +84 -0
  130. package/skills/api-design.md +82 -0
  131. package/skills/auth-and-sessions.md +78 -0
  132. package/skills/build-with-a-framework.md +101 -0
  133. package/skills/colour-and-contrast.md +112 -0
  134. package/skills/creative-engines.md +81 -0
  135. package/skills/css-layout.md +85 -0
  136. package/skills/data-and-charts.md +77 -0
  137. package/skills/debugging.md +76 -0
  138. package/skills/designing-by-looking.md +84 -0
  139. package/skills/error-handling.md +78 -0
  140. package/skills/forms-and-validation.md +93 -0
  141. package/skills/nextjs-app-router.md +75 -0
  142. package/skills/page-composition.md +103 -0
  143. package/skills/performance.md +77 -0
  144. package/skills/plan-before-building.md +52 -0
  145. package/skills/planning-and-delegating.md +72 -0
  146. package/skills/refactoring.md +70 -0
  147. package/skills/security-basics.md +76 -0
  148. package/skills/state-management.md +73 -0
  149. package/skills/supabase-multitenant.md +72 -0
  150. package/skills/typescript-strict.md +90 -0
  151. package/skills/typography.md +135 -0
  152. package/skills/verify-your-own-work.md +62 -0
  153. package/skills/web-app-quality.md +62 -0
  154. package/skills/working-in-the-background.md +64 -0
@@ -0,0 +1,780 @@
1
+ /**
2
+ * ── ⭐⭐ MCP DEFAULTS — THE CURATED SET, AND WHY IT IS SO SMALL ──────────────
3
+ *
4
+ * `mcp.mjs` proved this CLI is a working MCP client: read a config, spawn the
5
+ * servers, namespace their tools, call them, shut them down. What it does NOT
6
+ * do is tell a new user which servers are worth having. Neither does anyone
7
+ * else — Claude Code, Cursor, Cline and Codex all speak MCP, and all four hand
8
+ * you an empty config file and wish you luck.
9
+ *
10
+ * ⭐ THE OPPORTUNITY IS THE DEFAULT SET, NOT THE PROTOCOL. MCP access is an
11
+ * open standard and is not our edge. "Acuvo arrives already able to do X" is a
12
+ * claim none of the others make, and it is integration work, not invention.
13
+ *
14
+ * ── ⚠️⚠️ AND THE HARD PART IS THE HONESTY, WHICH COST THIS FILE ITS SIZE ────
15
+ *
16
+ * The obvious version of this module is forty servers copied off a README. I
17
+ * measured what that would actually do on this machine, through the real
18
+ * `connectServer`, and the numbers killed it:
19
+ *
20
+ * · `npx -y @modelcontextprotocol/server-filesystem` — REFUSED. `mcp.mjs`
21
+ * deliberately injects `--no` and strips `-y`, so npx may only run a
22
+ * package that is ALREADY INSTALLED. npx spent 12s asking the registry and
23
+ * then said "npx canceled due to missing packages and no YES option".
24
+ * · `npx firecrawl-mcp` — the package IS installed globally here, and it
25
+ * still failed: "Either FIRECRAWL_API_KEY or FIRECRAWL_API_URL must be
26
+ * provided".
27
+ * · `node <packageRoot>/bin/acuvo-mcp.mjs` — connected in **156ms** with no
28
+ * download and no credentials, and offered **zero tools**, because its two
29
+ * tools are gated on RENDER_AUDIT_URL / MODAL_PRESS_URL.
30
+ *
31
+ * ⚠️⚠️ **A DARK ENTRY COSTS 20 SECONDS, NOT NOTHING.** Both failures above were
32
+ * reported by `connectServer` at **20,052ms** and **20,083ms** — the full
33
+ * `HANDSHAKE_TIMEOUT_MS`. The underlying process had already died in under a
34
+ * second; the client waits out the whole budget regardless. So a "harmless"
35
+ * default that happens to be unconfigured is a **20-second stall before the
36
+ * user's first prompt**, and four of them is a minute and a half.
37
+ *
38
+ * ⭐ THAT is why this module exists and why it is PURE. The point is not to
39
+ * publish a list. The point is to decide, WITHOUT SPAWNING ANYTHING, which
40
+ * entries provably cannot work here, so they are never spawned and never
41
+ * charged for. `assessCatalogue` is a precheck, and every 'dark' it returns is
42
+ * 20 seconds the session does not spend.
43
+ *
44
+ * ── THE RULES, ENFORCED BY TESTS RATHER THAN BY INTENTION ───────────────────
45
+ * 1. Nothing that needs a DOWNLOAD is enabled by default. It cannot work —
46
+ * `--no` forbids the install — so enabling it buys a guaranteed 20s stall.
47
+ * 2. Nothing that needs CREDENTIALS is enabled by default, for the same
48
+ * arithmetic: no key, no handshake, 20s gone.
49
+ * 3. Nothing UNVERIFIED is enabled by default. An entry nobody ran is a
50
+ * promise, and this repo has spent the day deleting promises.
51
+ * 4. Every entry that needs a download must carry the exact install command,
52
+ * because "install it yourself" without the line to paste is not help.
53
+ *
54
+ * ⚠️ WHAT I PERSONALLY RAN, so nobody has to guess which claims are load-bearing:
55
+ * · `acuvo` — VERIFIED, end to end, through `readMcpConfig` +
56
+ * `connectServer`. Connected, listed tools, closed clean.
57
+ * · `browser` — VERIFIED, end to end, INCLUDING A REAL TOOL CALL. See its
58
+ * entry: connected in 1,935ms, 29 tools, navigated a page
59
+ * and read the accessibility tree back. RE-VERIFIED
60
+ * independently 2026-08-14: 2,894ms, still 29 tools, and the
61
+ * same navigate + snapshot pair answered ok.
62
+ * · `playwright` — VERIFIED 2026-08-14, having been INERT for one day. See
63
+ * its entry: 24 tools and a real navigation.
64
+ * · `docs` — VERIFIED 2026-08-14, end to end, INCLUDING TWO REAL CALLS
65
+ * THAT RETURNED REAL DOCUMENTATION, with no API key.
66
+ * · `filesystem` — VERIFIED FAILING. I ran it and watched npx refuse.
67
+ * · `firecrawl` — VERIFIED FAILING. I ran it and read its own complaint.
68
+ * Everything else in `CATALOGUE` is marked `verified: false` and is INERT: it
69
+ * can never be enabled, never be rendered active, and exists only so the
70
+ * availability report can name the install command. I did not run those, and
71
+ * the entry says so rather than implying otherwise by sitting in a list.
72
+ *
73
+ * ── ⚠️⚠️ 2026-08-14: A CURATED SET THAT NAMED A STRANGER'S CANARY PACKAGE ───
74
+ *
75
+ * The entries below were curated by hand and NOT ONE of the npm names had ever
76
+ * been checked against the registry. Checked on 2026-08-14 with `npm view`, and
77
+ * the result is the argument for doing it:
78
+ *
79
+ * · `mcp-server-git` — **REMOVED.** npm `mcp-server-git@0.0.2` describes
80
+ * itself as *"Security research canary — not for production use. Part of an
81
+ * authorized bug bounty research project"*, repository
82
+ * `github.com/theinfosecguy/npx-canary`. It is a dependency-confusion probe,
83
+ * not the git MCP server (the real one is a PYTHON package run with
84
+ * `uvx mcp-server-git`, which this npx-only client cannot start anyway). Our
85
+ * catalogue was handing users `npm i -g mcp-server-git` — a curated set that
86
+ * tells you to globally install a stranger's canary is worse than no set.
87
+ * ⭐ And it bought nothing: this CLI already ships native `git_status`,
88
+ * `git_diff`, `git_log` and `git_commit`.
89
+ * · `@modelcontextprotocol/server-github` and `…/server-postgres` — both carry
90
+ * an npm `deprecated` field: *"Package no longer supported."* Kept, because
91
+ * they still resolve and still work, but their notes now say so. Silently
92
+ * recommending abandonware is the same class of stale claim this file is
93
+ * otherwise strict about.
94
+ * · `@modelcontextprotocol/server-filesystem` (2026.7.10) and `firecrawl-mcp`
95
+ * (3.24.0) — current, not deprecated. Unchanged.
96
+ *
97
+ * ⭐ THE RULE THAT FOLLOWS FROM IT: an entry's package name must be checked
98
+ * against the registry before it is written down, and the package spec must
99
+ * never carry a dist-tag (`@latest`) — `packageOf` feeds the `installed` lookup,
100
+ * and `"@playwright/mcp@latest"` can never match a package called
101
+ * `@playwright/mcp`, so a tagged spec reports an installed server as dark
102
+ * forever.
103
+ *
104
+ * ⚠️⚠️ AND THAT RULE SAID OF ITSELF "AND IT IS NOW A TEST" WHILE NO SUCH TEST
105
+ * EXISTED. Checked on 2026-08-14 — `grep -n "latest\|dist-tag"
106
+ * test/mcp-defaults.test.mjs` returned nothing, so the sentence asserting the
107
+ * rule was enforced was the only thing enforcing it. It is a test NOW (see
108
+ * `test/mcp-catalogue-claims.test.mjs`), which is a smaller and truer claim
109
+ * than the one it replaces. ⭐ A comment that says "there is a test for this" is
110
+ * itself a factual claim about the repo, and this file is otherwise strict
111
+ * about exactly that — the honesty rules have to apply to the honesty rules.
112
+ *
113
+ * ── ⚠️⚠️ 2026-08-14, LATER THE SAME DAY: THE INSTALL BLOCK IS GONE ──────────
114
+ *
115
+ * This header stated, at length and in bold, that **no new npm package could be
116
+ * installed on this machine** — a network appliance answering HTTP 503 with an
117
+ * HTML "File Transfer Blocked" page for every `.tgz` under `registry.npmjs.org`,
118
+ * `is-odd` included. That was true when it was measured and it is FALSE NOW.
119
+ * Re-measured today, same machine: `npm i is-odd` → *"added 2 packages in 1s"*,
120
+ * `npm i -g @upstash/context7-mcp` → *"added 88 packages in 1m"*,
121
+ * `npm i -g @playwright/mcp` → *"added 3 packages in 36s"*.
122
+ *
123
+ * ⭐ AND THE STALE CLAIM WAS COSTING REAL CAPABILITY, which is why it is worth
124
+ * this much space. `playwright` was filed as INERT *solely* because of it, and
125
+ * the note said so. One re-measurement promoted it to verified and added a
126
+ * second verified entry — the blocker was a sentence, not a fact. This repo's
127
+ * standing rule is that a warning which has gone stale is as expensive as the
128
+ * wrong instruction it replaced, and going stale in the PESSIMISTIC direction
129
+ * is the sneakier half: nothing breaks, so nobody re-checks, and the catalogue
130
+ * just quietly stays smaller than the machine can support.
131
+ *
132
+ * ⚠️ SO: RE-MEASURE BEFORE INHERITING ANY "CANNOT" IN THIS FILE. The install
133
+ * channel here has now changed twice in one day; treat every environment claim
134
+ * below as dated, not permanent.
135
+ *
136
+ * ⚠️ THE HONEST LIMIT OF THIS FILE: it decides what CANNOT work. It cannot
137
+ * promise that a `live` entry WILL work — a key can be revoked and a package
138
+ * can be broken. `live` here means "nothing we can check from memory rules it
139
+ * out", which is a smaller claim than it looks and is deliberately worded that
140
+ * way everywhere it surfaces.
141
+ */
142
+
143
+ import { fileURLToPath } from 'node:url';
144
+ import { dirname, resolve } from 'node:path';
145
+
146
+ import { MAX_SERVERS, HANDSHAKE_TIMEOUT_MS, MCP_CONFIG_FILES } from './mcp.mjs';
147
+
148
+ /**
149
+ * ⭐ IMPORTED, NOT RETYPED. `readMcpConfig` silently `break`s past the 9th
150
+ * server, so a renderer with its own idea of the cap would emit a config whose
151
+ * tail is dropped without a word. The cap has to be the same number by
152
+ * construction, not by comment.
153
+ */
154
+ export { MAX_SERVERS, HANDSHAKE_TIMEOUT_MS, MCP_CONFIG_FILES };
155
+
156
+ /** Where `renderStarterConfig`'s output is meant to be written. */
157
+ export const STARTER_CONFIG_FILE = MCP_CONFIG_FILES[0];
158
+
159
+ /**
160
+ * The token standing in for this package's install directory inside `args`.
161
+ *
162
+ * ⚠️ A LITERAL ABSOLUTE PATH CANNOT LIVE IN THE CATALOGUE. The catalogue is a
163
+ * constant; the path is different on every machine and is not knowable until
164
+ * someone asks for a rendered config. Substituting at render time keeps the
165
+ * data pure and keeps the rendered file correct.
166
+ */
167
+ export const PACKAGE_ROOT_TOKEN = '{ACUVO_PACKAGE_ROOT}';
168
+
169
+ /** This package's root, for the default substitution. Computed, no I/O. */
170
+ export const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
171
+
172
+ /**
173
+ * ⚠️ Measured, and it is the whole argument for the precheck: this is what a
174
+ * dark entry costs at session start. Not a guess — `connectServer` returned at
175
+ * 20,052ms and 20,083ms for the two failures described in the header.
176
+ */
177
+ export const DARK_ENTRY_COST_MS = HANDSHAKE_TIMEOUT_MS;
178
+
179
+ /**
180
+ * ── THE CATALOGUE ───────────────────────────────────────────────────────────
181
+ *
182
+ * Fields, and why each one is here rather than being obvious from the command:
183
+ *
184
+ * name becomes `mcp__<name>__<tool>`, so it must satisfy the server
185
+ * name rule in `readMcpConfig` or the whole config is rejected.
186
+ * purpose one honest line. Not marketing — what you get.
187
+ * command/args exactly what `mcp.mjs` will spawn. No shell, no expansion.
188
+ * needsDownload true when the package is not already on the machine. Under
189
+ * this client that means it CANNOT self-install.
190
+ * install the line to paste. Required whenever needsDownload is true.
191
+ * credentials [{ env, required, why }]. `required: false` means the server
192
+ * starts without it but offers fewer tools.
193
+ * verified did I personally run it, through the real client?
194
+ * note what running it actually did, or why it is unverified.
195
+ * enabledByDefault only ever true when verified && !needsDownload && no
196
+ * required credentials. Tests enforce this; see the header.
197
+ */
198
+ export const CATALOGUE = Object.freeze([
199
+ Object.freeze({
200
+ name: 'acuvo',
201
+ purpose: 'Render HTML in a real browser and get the screenshot plus measured layout and contrast defects back; turn HTML into a PDF, PNG or PPTX.',
202
+ command: 'node',
203
+ args: Object.freeze([`${PACKAGE_ROOT_TOKEN}/bin/acuvo-mcp.mjs`]),
204
+ /**
205
+ * ⭐ THE ONLY ENTRY THAT NEEDS NO DOWNLOAD, because we ship it. `bin/
206
+ * acuvo-mcp.mjs` is in this package's `files` list, so it is on disk the
207
+ * moment acuvo-code is.
208
+ *
209
+ * ⚠️ AND IT IS SPAWNED AS `node <abs path>`, NOT AS `acuvo-mcp`. I tried
210
+ * the bare bin name and got ENOENT: the shim is `acuvo-mcp.cmd`, and
211
+ * `resolveExecutable` only finds it when PATH is separated the Windows way
212
+ * — under Git Bash PATH is `:`-separated and the lookup misses entirely.
213
+ * `node` resolves as `node.exe` everywhere, and an absolute script path
214
+ * needs no lookup at all.
215
+ */
216
+ needsDownload: false,
217
+ install: null,
218
+ credentials: Object.freeze([
219
+ Object.freeze({ env: 'RENDER_AUDIT_URL', required: false, why: 'without it the `see_page` tool is not offered' }),
220
+ Object.freeze({ env: 'MODAL_PRESS_URL', required: false, why: 'without it the `make_document` tool is not offered' }),
221
+ Object.freeze({ env: 'MODAL_VIDEO_SECRET', required: false, why: 'only if those services require a shared secret' }),
222
+ ]),
223
+ verified: true,
224
+ note: 'Ran it: connected in 156ms with no download and no credentials, and offered zero tools — its two tools are gated on the URLs above. It stays up and answers with an empty list rather than dying, so it costs a spawn and never a 20s timeout.',
225
+ enabledByDefault: true,
226
+ }),
227
+
228
+ /**
229
+ * ── ⭐⭐ THE ONE CAPABILITY THIS CLI COULD NOT REACH AT ALL ────────────────
230
+ *
231
+ * `see_page` RENDERS a page and measures it. Nothing in the 49-tool registry
232
+ * can CLICK a button, FILL a form, or drive a flow — which is most of what
233
+ * "test the thing you just built" actually means. That is not a gap MCP
234
+ * merely papers over; it is the single largest capability this client does
235
+ * not have and cannot cheaply build.
236
+ *
237
+ * ⭐ WHY chrome-devtools-mcp AND NOT PLAYWRIGHT, having weighed both:
238
+ * 1. It drives the Chrome ALREADY ON THE MACHINE. Playwright's MCP server
239
+ * additionally needs `npx playwright install chromium` — a ~150MB
240
+ * download on top of the package, on a client that cannot download.
241
+ * 2. `npm view chrome-devtools-mcp` → **zero runtime dependencies** (it is
242
+ * rollup-bundled), Apache-2.0, 14MB installed, and no postinstall step
243
+ * that fetches a browser. Nothing about it can surprise a security
244
+ * reviewer, which for a server we RECOMMEND is the whole point.
245
+ * 3. It is Google's own, versioned 1.7.0 and current.
246
+ *
247
+ * ⚠️ IT IS STILL NOT A DEFAULT, and the rule is not being bent for it: it
248
+ * needs a download, so under `--no` it cannot start, so enabling it would buy
249
+ * a guaranteed 20s stall. Rule 1 applies to the capability we most want.
250
+ *
251
+ * ⚠️ AND IT NEEDS A REAL CHROME. That is a machine fact no environment
252
+ * variable expresses, so it cannot be in `credentials` and the precheck
253
+ * cannot see it — `assessEntry` will say "live" on a machine with no browser.
254
+ * Stated here rather than implied away.
255
+ */
256
+ Object.freeze({
257
+ name: 'browser',
258
+ purpose: 'Drive a real Chrome: click, fill forms, type, navigate, read the accessibility tree, screenshot, and read console messages, network requests and performance traces.',
259
+ command: 'npx',
260
+ args: Object.freeze(['-y', 'chrome-devtools-mcp']),
261
+ needsDownload: true,
262
+ install: 'npm i -g chrome-devtools-mcp',
263
+ credentials: Object.freeze([]),
264
+ verified: true,
265
+ note: 'RAN IT, end to end, through the real connectServer — and then CALLED IT, which no other entry here has earned. Connected in 1,935ms and listed 29 tools (click, fill, fill_form, type_text, navigate_page, take_snapshot, take_screenshot, evaluate_script, upload_file, list_network_requests, performance_start_trace, lighthouse_audit …); `navigate_page` to a data: URL answered ok in 789ms and `take_snapshot` in 13ms, returning the accessibility tree with the button named. 29 is under MAX_TOOLS_PER_SERVER (40), so nothing is truncated. Add `--headless` and `--isolated` to args for CI. ⚠️ It drives the Chrome already installed on the machine and does NOT download one; with no Chrome present the connection still succeeds and the first tool call is what fails. Installed here from the npm cache — see the header on why nothing else could be.',
266
+ enabledByDefault: false,
267
+ }),
268
+
269
+ /**
270
+ * ── ⭐⭐ THE SECOND CAPABILITY THIS CLI STRUCTURALLY CANNOT HAVE ───────────
271
+ *
272
+ * A coding agent's most common wrong answer is not a logic error — it is
273
+ * CONFIDENTLY CURRENT-SOUNDING API ADVICE FROM A STALE TRAINING SET. Nothing
274
+ * in the 49-tool registry fixes that: `web_search` returns result pages and
275
+ * `fetch_url` returns one document, so "how do I write a route handler in
276
+ * this framework's current major" costs several paid rounds of reading HTML
277
+ * and still lands wherever the model's priors were.
278
+ *
279
+ * ⭐ WHY THIS EARNS A SLOT WHEN `web_search` AND `fetch_url` ALREADY EXIST:
280
+ * it returns VERSIONED, SOURCE-CITED SNIPPETS from the library's own repo
281
+ * rather than prose about them. The measured call below came back with the
282
+ * `route.js` signature and a GitHub source path per snippet. That is the
283
+ * difference between evidence and a search result, and it is the same
284
+ * argument `see_page` makes against a screenshot: hand the model the answer,
285
+ * not the material to derive it from.
286
+ *
287
+ * ⭐⭐ AND IT NEEDS NO CREDENTIAL, which is rare enough to be the deciding
288
+ * factor. Rule 2 exists because a keyed server is a guaranteed 20s stall for
289
+ * anyone who has not signed up; this one is one `npm i -g` away from working
290
+ * for every user, with no account. Of everything weighed for this expansion it
291
+ * is the only candidate that clears both the "a working developer reaches for
292
+ * it" bar and the no-signup bar.
293
+ *
294
+ * ⚠️ IT IS STILL NOT A DEFAULT. Rule 1 is not bent for it either: it needs a
295
+ * download, so under `--no` it cannot start, so enabling it would buy the
296
+ * guaranteed 20s stall. Every argument above is an argument for CURATING it,
297
+ * not for spawning it uninvited.
298
+ *
299
+ * ⚠️ AND IT IS A THIRD-PARTY NETWORK SERVICE. The query text — which will
300
+ * often be the user's actual problem statement — leaves the machine to
301
+ * Upstash's API. That is an egress path an enterprise reviewer must be told
302
+ * about, exactly like `generate_image`'s, and it is why this sits behind an
303
+ * explicit opt-in rather than in `mcpServers`.
304
+ *
305
+ * ⚠️ ITS TWO REQUIRED ARGUMENTS CONTRADICT EACH OTHER IN THE ERROR MESSAGE,
306
+ * measured, and it cost two calls to work out: passing only `libraryName`
307
+ * complains *"query: expected string, received undefined"*, and passing only
308
+ * `query` complains *"libraryName: expected string, received undefined"*.
309
+ * Both are required. Recorded because the model will hit this too, and the
310
+ * server's own error names the field it was NOT given.
311
+ */
312
+ Object.freeze({
313
+ name: 'docs',
314
+ purpose: 'Look up current, version-specific documentation and code examples for a library, returned as source-cited snippets rather than as search results.',
315
+ command: 'npx',
316
+ args: Object.freeze(['-y', '@upstash/context7-mcp']),
317
+ needsDownload: true,
318
+ install: 'npm i -g @upstash/context7-mcp',
319
+ /**
320
+ * ⚠️ DELIBERATELY EMPTY, and that is a claim I checked rather than assumed.
321
+ * Context7 sells an API key for higher rate limits; the server starts and
322
+ * ANSWERS without one — proven by the calls in `note`, made with a scrubbed
323
+ * environment containing no Context7 variable of any kind. Listing an
324
+ * optional credential here would have darkened nothing but would have
325
+ * implied a signup that is not required.
326
+ */
327
+ credentials: Object.freeze([]),
328
+ verified: true,
329
+ note: 'RAN IT, end to end, through the real connectServer — and CALLED IT TWICE, with no API key. Connected in 3,338–8,348ms across four runs and listed 2 tools (resolve-library-id, query-docs). `resolve-library-id` for "next.js" answered ok in 2,250ms with real registry data (/vercel/next.js, 6071 snippets, a version list); `query-docs` on /vercel/next.js for "how do I define a route handler" answered ok in 2,536ms with the actual current `export async function GET(request: Request) {}` signature and a GitHub source URL per snippet. ⚠️ Two tools is FAR under MAX_TOOLS_PER_SERVER (40), so it is a cheap entry in prefix bytes as well as in dollars. ⚠️ It is a network service: your query text leaves the machine to Upstash. ⚠️ Both `libraryName` and `query` are required by resolve-library-id even though each error message names only the other one.',
330
+ enabledByDefault: false,
331
+ }),
332
+
333
+ Object.freeze({
334
+ name: 'filesystem',
335
+ purpose: 'Read and write files under directories you name — the reference MCP server, and the usual first one people add.',
336
+ command: 'npx',
337
+ args: Object.freeze(['-y', '@modelcontextprotocol/server-filesystem', '.']),
338
+ needsDownload: true,
339
+ install: 'npm i -g @modelcontextprotocol/server-filesystem',
340
+ credentials: Object.freeze([]),
341
+ verified: true,
342
+ note: 'Ran it: REFUSED. `mcp.mjs` injects `--no` and strips `-y`, so npx may only run an already-installed package. npx spent 12s on the registry then said "npx canceled due to missing packages and no YES option", and connectServer still reported it at 20,083ms. Install it globally first and this entry works.',
343
+ enabledByDefault: false,
344
+ }),
345
+
346
+ Object.freeze({
347
+ name: 'firecrawl',
348
+ purpose: 'Fetch and crawl web pages as clean markdown, including JavaScript-rendered ones.',
349
+ command: 'npx',
350
+ args: Object.freeze(['-y', 'firecrawl-mcp']),
351
+ needsDownload: true,
352
+ install: 'npm i -g firecrawl-mcp',
353
+ credentials: Object.freeze([
354
+ Object.freeze({ env: 'FIRECRAWL_API_KEY', required: true, why: 'the server refuses to start without it' }),
355
+ ]),
356
+ verified: true,
357
+ note: 'Ran it with the package already installed globally: it still failed, with its own message — "Either FIRECRAWL_API_KEY or FIRECRAWL_API_URL must be provided" — and connectServer reported it at 20,052ms. This is the entry that proves the credential rule is about latency, not tidiness.',
358
+ enabledByDefault: false,
359
+ }),
360
+
361
+ /**
362
+ * ⚠️ THIS SLOT USED TO BE `git`, POINTING AT npm `mcp-server-git` — which is a
363
+ * security-research canary, not a server. See the header. It is gone, and the
364
+ * capability was never missing: `git_status`, `git_diff`, `git_log` and
365
+ * `git_commit` are native tools in this CLI.
366
+ *
367
+ * ⚠️ THE CANONICAL PLAYWRIGHT INSTALL LINE IS REFUSED BY THIS CLIENT BY
368
+ * DESIGN, and that is the single most valuable thing this entry carries.
369
+ * Every Playwright MCP README says:
370
+ * {"command":"npx","args":["-y","@playwright/mcp@latest"]}
371
+ * `mcp.mjs:261` filters `-y`/`--yes` out and injects `--no`, so that becomes
372
+ * `npx --no @playwright/mcp@latest` — which cannot install anything and dies,
373
+ * costing the full 20s handshake with no explanation. A user who pastes the
374
+ * documented line gets a silent 20-second stall and a dark server, and has no
375
+ * way to know why. The args below are the form that CAN work: no `-y` to be
376
+ * stripped, and NO `@latest`, because `packageOf` feeds the installed-package
377
+ * lookup and a tagged spec never matches an installed package name.
378
+ *
379
+ * ── ⭐ PROMOTED FROM INERT TO VERIFIED, 2026-08-14 ──────────────────────────
380
+ *
381
+ * It sat unverified for exactly one day, and the reason recorded in its own
382
+ * note was an environment claim — "no npm package can be installed on this
383
+ * machine" — that stopped being true. See the header. Re-measured rather than
384
+ * re-argued.
385
+ */
386
+ Object.freeze({
387
+ name: 'playwright',
388
+ purpose: 'Drive a Playwright-managed browser — click, fill, navigate and assert against a live page, across Chromium, Firefox and WebKit.',
389
+ command: 'npx',
390
+ args: Object.freeze(['-y', '@playwright/mcp']),
391
+ needsDownload: true,
392
+ // ⚠️ TWO commands, because the package alone is not always enough:
393
+ // Playwright installs its browsers separately, and the second line is
394
+ // ~150MB. It is `&&`-joined rather than split because the second half is
395
+ // the one people skip, and skipping it fails at the first CALL rather than
396
+ // at connect — see the note.
397
+ install: 'npm i -g @playwright/mcp && npx playwright install chromium',
398
+ credentials: Object.freeze([]),
399
+ verified: true,
400
+ note: 'RAN IT, end to end, through the real connectServer, and CALLED IT. Connected in 2,600–15,118ms and listed 24 tools (browser_click, browser_fill_form, browser_type, browser_navigate, browser_snapshot, browser_take_screenshot, browser_evaluate, browser_file_upload, browser_select_option, browser_tabs, browser_network_requests …); `browser_navigate` to a data: URL answered ok in 1,027ms and returned the Playwright code it ran. 24 is under MAX_TOOLS_PER_SERVER (40), so nothing is truncated. ⚠️ THE FIRST CONNECT TOOK 15,118ms — 75% of the 20s handshake budget — while a warm one took 2,600ms; a slower machine can therefore fail the handshake on first use and look permanently broken when it is merely cold. ⚠️ IT WRITES INTO YOUR WORKSPACE: the navigate call created `.playwright-mcp/page-<timestamp>.yml` in the current directory, unasked. Pass `--output-dir` to send that somewhere else, and expect to gitignore it otherwise — no other entry in this catalogue writes to the repo. ⚠️ It found a browser here without `npx playwright install chromium` having been run in this session, so that step is conditional on what the machine already has, not universal. Prefer the `browser` entry above unless you need Firefox or WebKit — it needs no browser download and does not litter the workspace.',
401
+ enabledByDefault: false,
402
+ }),
403
+
404
+ /**
405
+ * ── ⚠️ BELOW HERE: UNVERIFIED, AND THEREFORE INERT ────────────────────────
406
+ * I did not run these. They are real, widely-used servers and the commands
407
+ * are the documented ones, but "documented" is not "measured" and this file
408
+ * refuses to blur the two. They can never be enabled and are never rendered
409
+ * active; they exist so the availability report can hand over an install
410
+ * command instead of a shrug. Promote one by RUNNING it and rewriting `note`
411
+ * with what happened.
412
+ */
413
+
414
+ Object.freeze({
415
+ name: 'github',
416
+ purpose: 'Issues and pull requests — open, read and comment on your issue tracker from inside a run.',
417
+ command: 'npx',
418
+ args: Object.freeze(['-y', '@modelcontextprotocol/server-github']),
419
+ needsDownload: true,
420
+ install: 'npm i -g @modelcontextprotocol/server-github',
421
+ credentials: Object.freeze([
422
+ Object.freeze({ env: 'GITHUB_PERSONAL_ACCESS_TOKEN', required: true, why: 'every call is authenticated; the server will not start without it' }),
423
+ ]),
424
+ verified: false,
425
+ note: 'NOT RUN by me. Listed for the install command only. Needs both a download and a token, so it is dark twice over. ⚠️ AND IT IS DEPRECATED: `npm view` on 2026-08-14 reports version 2025.4.8 carrying `deprecated: "Package no longer supported."` It still resolves and still installs, but it is not maintained, and GitHub\'s current server is a Go binary / hosted HTTP service that this stdio-only, npx-only client cannot start.',
426
+ enabledByDefault: false,
427
+ }),
428
+
429
+ Object.freeze({
430
+ name: 'postgres',
431
+ purpose: 'Run read-only queries against a Postgres database and inspect its schema.',
432
+ command: 'npx',
433
+ args: Object.freeze(['-y', '@modelcontextprotocol/server-postgres']),
434
+ needsDownload: true,
435
+ install: 'npm i -g @modelcontextprotocol/server-postgres',
436
+ credentials: Object.freeze([
437
+ Object.freeze({ env: 'POSTGRES_CONNECTION_STRING', required: true, why: 'there is nothing to connect to without it' }),
438
+ ]),
439
+ verified: false,
440
+ note: 'NOT RUN by me. Listed for the install command only. Needs both a download and a connection string. ⚠️ AND IT IS DEPRECATED: `npm view` on 2026-08-14 reports version 0.6.2 carrying `deprecated: "Package no longer supported."` It is the last published build of the reference server and still installs; treat it as frozen, not as maintained.',
441
+ enabledByDefault: false,
442
+ }),
443
+
444
+ /**
445
+ * ── ⭐ WHAT WAS WEIGHED AND REJECTED, 2026-08-14 ────────────────────────────
446
+ *
447
+ * The thesis at the top of this file is that CURATION is the edge, not access.
448
+ * That is only true if the rejections are real, so they are written down here
449
+ * with the reason — otherwise "curated" degrades into "whatever got added".
450
+ *
451
+ * · **SQLite** (`mcp-server-sqlite-npx`, 0.8.0, ISC) — REJECTED, and it was
452
+ * the strongest miss. "A database the audience actually uses" is a fair
453
+ * brief and SQLite is the honest answer to it. But `npm view … dependencies`
454
+ * shows it pulls **`sqlite3` ^5.1.7**, a NATIVE module: installing it means
455
+ * a prebuilt-binary download or a node-gyp compile, on a package we are
456
+ * RECOMMENDING to strangers. Every other entry here is plain JavaScript
457
+ * that a reviewer can read. `npm view` also returned no `repository.url`,
458
+ * so provenance is weaker than the two Anthropic-published entries above.
459
+ * ⭐ The deciding argument: a curated set is a set of things we are willing
460
+ * to be blamed for. A native compile that fails on a user's machine is a
461
+ * support burden bought for a capability they can already reach by
462
+ * declaring the server themselves.
463
+ * · **`@modelcontextprotocol/server-memory` / `…-sequential-thinking`
464
+ * (2026.7.4)** — REJECTED as DUPLICATES of shipped native tools, which is
465
+ * the failure mode `mcp-server-git` already demonstrated. This CLI has
466
+ * `remember`/`forget` (`learned.mjs`) and `plan_*` (`plan-ledger.mjs`).
467
+ * Adding an MCP server that shadows a native tool spends 20s of handshake
468
+ * budget and prefix bytes to offer the model a second, worse door to a
469
+ * verb it already has — and gives it two places to store one fact.
470
+ * · **Notion (2.5.1) / Supabase (0.10.0)** — REJECTED for this pass, not on
471
+ * quality: both require an account and a token, so both are dark on every
472
+ * machine until a signup happens (rule 2). They are the right SECOND wave,
473
+ * once someone actually asks. A catalogue whose entries are mostly dark
474
+ * for mostly everyone is the forty-servers-off-a-README failure wearing
475
+ * better names.
476
+ */
477
+ ]);
478
+
479
+ /** Look one up by name. Returns null rather than throwing — callers branch anyway. */
480
+ export function catalogueEntry(name) {
481
+ return CATALOGUE.find((e) => e.name === name) ?? null;
482
+ }
483
+
484
+ /**
485
+ * Substitute `PACKAGE_ROOT_TOKEN` in an entry's args.
486
+ *
487
+ * ⚠️ FORWARD SLASHES ARE LEFT ALONE ON PURPOSE. Windows accepts them in a path
488
+ * passed to `node`, and rewriting them to backslashes would put an escape
489
+ * character into a JSON file that a human is expected to read and edit.
490
+ */
491
+ export function resolveArgs(entry, { packageRoot = PACKAGE_ROOT } = {}) {
492
+ const root = String(packageRoot).split(String.fromCharCode(92)).join('/');
493
+ return (entry?.args ?? []).map((a) => a.split(PACKAGE_ROOT_TOKEN).join(root));
494
+ }
495
+
496
+ /** The `mcp.mjs` server spec for an entry — what `connectServer` wants. */
497
+ export function toServerSpec(entry, { packageRoot = PACKAGE_ROOT } = {}) {
498
+ return {
499
+ name: entry.name,
500
+ command: entry.command,
501
+ args: resolveArgs(entry, { packageRoot }),
502
+ env: {},
503
+ };
504
+ }
505
+
506
+ /** The credentials an entry cannot start without. */
507
+ export function requiredCredentials(entry) {
508
+ return (entry?.credentials ?? []).filter((c) => c.required);
509
+ }
510
+
511
+ function present(env, name) {
512
+ const v = env?.[name];
513
+ return typeof v === 'string' && v.trim() !== '';
514
+ }
515
+
516
+ /**
517
+ * ── ⭐ THE PRECHECK — DECIDE WITHOUT SPAWNING ───────────────────────────────
518
+ *
519
+ * Returns doctor's shape (`state` / `detail` / `fix`) so a doctor lane can drop
520
+ * these straight into its check list without a translation layer.
521
+ *
522
+ * `installed` is INJECTED rather than probed. Whether a package is on disk is
523
+ * an I/O question, and this module stays pure so it is testable with no
524
+ * network and no filesystem. A caller that knows (the doctor, which is already
525
+ * allowed to look) passes a Set of package names; a caller that does not gets
526
+ * the honest answer that a download-needing entry cannot be assumed present.
527
+ *
528
+ * ⚠️ `state: 'live'` IS THE WEAKER CLAIM IT LOOKS LIKE. It means "nothing
529
+ * checkable rules this out", not "this will work". A revoked key looks exactly
530
+ * like a good one from here, and the wording of `detail` never pretends
531
+ * otherwise.
532
+ */
533
+ export function assessEntry(entry, { env = process.env, installed = null, packageRoot = PACKAGE_ROOT } = {}) {
534
+ const base = {
535
+ id: `mcp.${entry.name}`,
536
+ label: entry.name,
537
+ entry: entry.name,
538
+ purpose: entry.purpose,
539
+ verified: entry.verified,
540
+ enabledByDefault: entry.enabledByDefault,
541
+ };
542
+
543
+ // ⚠️ DOWNLOAD FIRST: it is the reason that cannot be worked around by setting
544
+ // a variable, so reporting a missing key on a package that is not even here
545
+ // would send the user to fix the second problem first.
546
+ if (entry.needsDownload) {
547
+ const known = installed instanceof Set ? installed : null;
548
+ const pkg = packageOf(entry);
549
+ if (!known || !known.has(pkg)) {
550
+ return {
551
+ ...base,
552
+ state: 'dark',
553
+ detail: known
554
+ ? `${pkg} is not installed — this client passes npx \`--no\`, so it cannot download it`
555
+ : `needs ${pkg}, and whether it is installed was not checked — this client passes npx \`--no\`, so it cannot download it`,
556
+ fix: `${entry.install} — then re-run. Leaving it unconfigured costs a ${Math.round(DARK_ENTRY_COST_MS / 1000)}s timeout every session it is enabled.`,
557
+ costMs: DARK_ENTRY_COST_MS,
558
+ };
559
+ }
560
+ }
561
+
562
+ const missing = requiredCredentials(entry).filter((c) => !present(env, c.env));
563
+ if (missing.length > 0) {
564
+ const names = missing.map((c) => c.env).join(', ');
565
+ return {
566
+ ...base,
567
+ state: 'dark',
568
+ detail: `${names} ${missing.length === 1 ? 'is' : 'are'} not set — ${missing[0].why}`,
569
+ fix: `set ${names}. Until then this server cannot start, and enabling it costs a ${Math.round(DARK_ENTRY_COST_MS / 1000)}s timeout every session.`,
570
+ costMs: DARK_ENTRY_COST_MS,
571
+ };
572
+ }
573
+
574
+ const optionalMissing = (entry.credentials ?? []).filter((c) => !c.required && !present(env, c.env));
575
+ const detail = optionalMissing.length > 0
576
+ ? `can start; ${optionalMissing.map((c) => c.env).join(', ')} not set, so it will offer fewer tools`
577
+ : 'can start, and nothing checkable rules it out';
578
+
579
+ return {
580
+ ...base,
581
+ state: 'live',
582
+ detail,
583
+ /**
584
+ * ⚠️ EACH REASON STAYS ATTACHED TO ITS VARIABLE. Joining the names and then
585
+ * joining the reasons produced "without it …; without it …", where "it"
586
+ * pointed at nothing — three variables and three dangling pronouns.
587
+ */
588
+ fix: optionalMissing.length > 0
589
+ ? `set ${optionalMissing.map((c) => `${c.env} (${c.why})`).join('; ')}`
590
+ : null,
591
+ costMs: 0,
592
+ command: `${entry.command} ${resolveArgs(entry, { packageRoot }).join(' ')}`.trim(),
593
+ };
594
+ }
595
+
596
+ /**
597
+ * The npm package an entry runs, for install checks. Null when we ship it.
598
+ *
599
+ * ── ⚠️ THE DIST-TAG IS STRIPPED, AND THAT IS NOT COSMETIC ───────────────────
600
+ *
601
+ * This value is the KEY looked up in the `installed` Set, and an installed
602
+ * package is recorded under its NAME. Every MCP README in the world writes
603
+ * `@playwright/mcp@latest` or `chrome-devtools-mcp@latest`, so the moment
604
+ * somebody copies one in, `assessEntry` looks up `"@playwright/mcp@latest"`,
605
+ * never finds it, and reports a perfectly working server as dark FOREVER —
606
+ * a check that cannot pass, which is the same shape as a check that cannot
607
+ * fail and just as useless.
608
+ *
609
+ * ⚠️ THE LAST `@`, NOT THE FIRST. A scoped name begins with one:
610
+ * `@playwright/mcp` is the package, `@playwright/mcp@latest` is the package plus
611
+ * a tag. Splitting on the first `@` would turn every scoped package into an
612
+ * empty string.
613
+ *
614
+ * A catalogue entry must not carry a tag in the first place — there is a test —
615
+ * but the stripping stays, because the next person to add an entry will paste
616
+ * the README line and the failure it causes is silent.
617
+ */
618
+ export function packageOf(entry) {
619
+ if (!entry?.needsDownload) return null;
620
+ // The first arg that is not a flag is the package npx would run.
621
+ const spec = (entry.args ?? []).find((a) => !a.startsWith('-')) ?? null;
622
+ if (!spec) return null;
623
+ const at = spec.lastIndexOf('@');
624
+ return at > 0 ? spec.slice(0, at) : spec;
625
+ }
626
+
627
+ /** Assess every entry. Same order as the catalogue, so output is stable. */
628
+ export function assessCatalogue({ env = process.env, installed = null, packageRoot = PACKAGE_ROOT } = {}) {
629
+ return CATALOGUE.map((e) => assessEntry(e, { env, installed, packageRoot }));
630
+ }
631
+
632
+ /**
633
+ * The entries that should actually be spawned here: enabled by default AND
634
+ * assessed live.
635
+ *
636
+ * ⚠️ BOTH CONDITIONS, NOT EITHER. `enabledByDefault` is a property of the
637
+ * catalogue; `live` is a property of this machine right now. An entry can be a
638
+ * fine default and still be dark today, and spawning it anyway is the 20s
639
+ * stall this whole module exists to avoid.
640
+ */
641
+ export function defaultServerSpecs({ env = process.env, installed = null, packageRoot = PACKAGE_ROOT } = {}) {
642
+ const assessed = new Map(assessCatalogue({ env, installed, packageRoot }).map((a) => [a.entry, a]));
643
+ return CATALOGUE
644
+ .filter((e) => e.enabledByDefault && assessed.get(e.name)?.state === 'live')
645
+ .slice(0, MAX_SERVERS)
646
+ .map((e) => toServerSpec(e, { packageRoot }));
647
+ }
648
+
649
+ /**
650
+ * ── THE STARTER CONFIG ──────────────────────────────────────────────────────
651
+ *
652
+ * ⚠️ `mcp.json` IS STRICT JSON, SO THE EXPLANATIONS CANNOT BE COMMENTS. They go
653
+ * in `_disabled`, a key `readMcpConfig` never reads (it takes `mcpServers` ??
654
+ * `servers` and nothing else). The user gets the whole catalogue in the file
655
+ * they are already editing, with the reason each one is off and the command
656
+ * that turns it on — and the client still only ever spawns what is in
657
+ * `mcpServers`.
658
+ *
659
+ * ⚠️ CAPPED AT `MAX_SERVERS`, because `readMcpConfig` drops the overflow with a
660
+ * bare `break` — no error, no warning. Rendering a 10-entry config would be
661
+ * rendering two entries that silently never run.
662
+ */
663
+ export function renderStarterConfig({ env = process.env, installed = null, packageRoot = PACKAGE_ROOT } = {}) {
664
+ const assessed = new Map(assessCatalogue({ env, installed, packageRoot }).map((a) => [a.entry, a]));
665
+
666
+ const mcpServers = {};
667
+ const _disabled = {};
668
+
669
+ for (const entry of CATALOGUE) {
670
+ const a = assessed.get(entry.name);
671
+ const active = entry.enabledByDefault && a?.state === 'live' && Object.keys(mcpServers).length < MAX_SERVERS;
672
+ if (active) {
673
+ mcpServers[entry.name] = {
674
+ command: entry.command,
675
+ args: resolveArgs(entry, { packageRoot }),
676
+ };
677
+ continue;
678
+ }
679
+ _disabled[entry.name] = {
680
+ what: entry.purpose,
681
+ why_off: a?.detail ?? 'not enabled by default',
682
+ to_enable: a?.fix ?? null,
683
+ verified_by_us: entry.verified,
684
+ note: entry.note,
685
+ command: entry.command,
686
+ args: resolveArgs(entry, { packageRoot }),
687
+ };
688
+ }
689
+
690
+ return {
691
+ // ⭐ A header the user reads before the servers, in a key the client ignores.
692
+ _readme: [
693
+ `Written by acuvo-code. Only "mcpServers" is read; "_disabled" and "_hosted_example" are documentation.`,
694
+ `Move an entry from _disabled into mcpServers to turn it on — its "to_enable" says what it needs first.`,
695
+ `A server that cannot start costs a ${Math.round(DARK_ENTRY_COST_MS / 1000)}s timeout at session start, which is why they are off.`,
696
+ `At most ${MAX_SERVERS} servers are read; anything past that is silently ignored.`,
697
+ `Hosted servers work too — see "_hosted_example"; they need no install at all.`,
698
+ ].join(' '),
699
+ mcpServers,
700
+ /**
701
+ * ── ⭐ THE HOSTED SHAPE, IN THE FILE THE USER IS ALREADY EDITING ─────────
702
+ *
703
+ * Every entry in `CATALOGUE` is a program we spawn, because that was the
704
+ * only transport `mcp.mjs` had until 2026-08-15. It now speaks Streamable
705
+ * HTTP and SSE as well, and hosted servers are the half of the ecosystem
706
+ * that needs no install, no npx and no `--no` argument — the rules that
707
+ * darken most of the catalogue simply do not apply to them.
708
+ *
709
+ * ⚠️ AN EXAMPLE, NOT A RECOMMENDATION, and it sits OUTSIDE `mcpServers` for
710
+ * that reason. `readMcpConfig` reads `mcpServers` (or `servers`) and nothing
711
+ * else, so this is inert text — the same guarantee `_disabled` relies on.
712
+ * Nothing here has been run by us, and this file's whole discipline is that
713
+ * an unverified entry must not be able to become an active one.
714
+ *
715
+ * ⚠️ THE `${…}` IS THE POINT. A remote server gets no inherited environment,
716
+ * so a credential has to be named in the config and is expanded from your
717
+ * shell at connect time — never written into this file. If the variable is
718
+ * unset, acuvo refuses to connect rather than sending the literal text.
719
+ */
720
+ _hosted_example: {
721
+ _what: 'A server reached over the network. Not read by the client — copy an entry into "mcpServers" to use it.',
722
+ _rules: [
723
+ 'https:// is required unless the host is loopback, so a token never crosses in cleartext.',
724
+ 'Credentials come from your environment via ${VAR} in "headers"; the value is never stored here.',
725
+ 'You are asked to approve the HOST before the first connection, and again if the url or the headers change.',
726
+ ].join(' '),
727
+ example_http: { type: 'http', url: 'https://mcp.example.com/mcp', headers: { Authorization: 'Bearer ${EXAMPLE_TOKEN}' } },
728
+ example_sse: { type: 'sse', url: 'http://127.0.0.1:3845/sse' },
729
+ },
730
+ _disabled,
731
+ };
732
+ }
733
+
734
+ /** The starter config as the text to write to `.acuvo/mcp.json`. */
735
+ export function renderStarterConfigJson(opts = {}) {
736
+ return `${JSON.stringify(renderStarterConfig(opts), null, 2)}\n`;
737
+ }
738
+
739
+ /**
740
+ * A human-readable availability report — what is usable here and what is dark,
741
+ * with the reason and the fix on the same line as the name.
742
+ */
743
+ export function formatAvailability(report) {
744
+ const rows = Array.isArray(report) ? report : assessCatalogue(report ?? {});
745
+ const live = rows.filter((r) => r.state === 'live');
746
+ const lines = [
747
+ `MCP defaults — ${live.length} of ${rows.length} usable here`,
748
+ '',
749
+ ];
750
+ for (const r of rows) {
751
+ const mark = r.state === 'live' ? 'live' : 'dark';
752
+ const flag = r.verified ? '' : ' (unverified — never enabled)';
753
+ lines.push(` ${mark.padEnd(4)} ${r.label.padEnd(12)} ${r.detail}${flag}`);
754
+ if (r.fix) lines.push(` ${' '.repeat(12)} → ${r.fix}`);
755
+ }
756
+ const stalled = rows.filter((r) => r.state === 'dark' && r.enabledByDefault);
757
+ if (stalled.length > 0) {
758
+ lines.push('', ` ⚠️ ${stalled.length} default(s) would stall this session by ${Math.round((stalled.length * DARK_ENTRY_COST_MS) / 1000)}s if spawned — they are skipped.`);
759
+ }
760
+ return lines.join('\n');
761
+ }
762
+
763
+ /**
764
+ * Doctor-shaped checks. Kept separate from `assessCatalogue` so the doctor's
765
+ * list is not flooded: one line per entry is right for `--mcp`, but the health
766
+ * report wants the summary plus only the entries a user can act on.
767
+ */
768
+ export function doctorChecks({ env = process.env, installed = null, packageRoot = PACKAGE_ROOT } = {}) {
769
+ const rows = assessCatalogue({ env, installed, packageRoot });
770
+ const live = rows.filter((r) => r.state === 'live');
771
+ const summary = {
772
+ id: 'mcp.defaults',
773
+ label: 'MCP defaults',
774
+ state: live.length > 0 ? 'live' : 'dark',
775
+ verified: true,
776
+ detail: `${live.length} of ${rows.length} catalogue entries usable here${live.length ? ` (${live.map((r) => r.label).join(', ')})` : ''}`,
777
+ fix: live.length > 0 ? null : `run with --mcp-init to write ${STARTER_CONFIG_FILE}, then install or configure one`,
778
+ };
779
+ return [summary, ...rows.filter((r) => r.enabledByDefault)];
780
+ }