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.
- package/CHANGELOG.md +328 -0
- package/ENTERPRISE.md +927 -0
- package/LICENSE +120 -0
- package/README.md +1245 -0
- package/ROADMAP.md +556 -0
- package/bin/acuvo-mcp.mjs +208 -0
- package/bin/acuvo.mjs +3108 -0
- package/lib/acceptance-consent.mjs +168 -0
- package/lib/acceptance.mjs +859 -0
- package/lib/account.mjs +226 -0
- package/lib/acuvo-dir.mjs +72 -0
- package/lib/acuvo-models.mjs +141 -0
- package/lib/apply-patch.mjs +570 -0
- package/lib/ask-user.mjs +173 -0
- package/lib/audit.mjs +530 -0
- package/lib/auto-lease.mjs +174 -0
- package/lib/background.mjs +842 -0
- package/lib/best-of.mjs +334 -0
- package/lib/board.mjs +232 -0
- package/lib/breaker.mjs +93 -0
- package/lib/budget.mjs +1375 -0
- package/lib/builtin-skills.mjs +135 -0
- package/lib/cache-floor.mjs +204 -0
- package/lib/chain.mjs +303 -0
- package/lib/changed-paths.mjs +84 -0
- package/lib/chat.mjs +434 -0
- package/lib/checkpoint.mjs +637 -0
- package/lib/child-lifetime.mjs +71 -0
- package/lib/cli-args.mjs +1255 -0
- package/lib/code-review.mjs +1382 -0
- package/lib/colour.mjs +82 -0
- package/lib/command.mjs +2847 -0
- package/lib/compact.mjs +1151 -0
- package/lib/completion.mjs +515 -0
- package/lib/creative-engines.mjs +779 -0
- package/lib/db-inspect.mjs +1624 -0
- package/lib/delete.mjs +111 -0
- package/lib/design-loop.mjs +570 -0
- package/lib/diff-preview.mjs +1044 -0
- package/lib/doctor.mjs +2139 -0
- package/lib/dropped.mjs +216 -0
- package/lib/edit-diagnostics.mjs +277 -0
- package/lib/edit.mjs +460 -0
- package/lib/env-file.mjs +250 -0
- package/lib/escalate.mjs +702 -0
- package/lib/evaluate.mjs +284 -0
- package/lib/fetch-text.mjs +952 -0
- package/lib/fleet-budget.mjs +256 -0
- package/lib/gh.mjs +1536 -0
- package/lib/git.mjs +1341 -0
- package/lib/github.mjs +261 -0
- package/lib/h2.mjs +194 -0
- package/lib/handoff.mjs +417 -0
- package/lib/hooks.mjs +626 -0
- package/lib/http-probe.mjs +907 -0
- package/lib/image-director.mjs +322 -0
- package/lib/image-edit.mjs +522 -0
- package/lib/imagegen.mjs +998 -0
- package/lib/interrupt.mjs +234 -0
- package/lib/learned.mjs +353 -0
- package/lib/lease-watch.mjs +115 -0
- package/lib/lease.mjs +868 -0
- package/lib/localize.mjs +834 -0
- package/lib/log-tail.mjs +1052 -0
- package/lib/login.mjs +157 -0
- package/lib/lsp.mjs +1613 -0
- package/lib/mcp-consent.mjs +377 -0
- package/lib/mcp-defaults.mjs +780 -0
- package/lib/mcp-server.mjs +1343 -0
- package/lib/mcp.mjs +1263 -0
- package/lib/media.mjs +1283 -0
- package/lib/memory-workspace.mjs +179 -0
- package/lib/model-json.mjs +132 -0
- package/lib/model-tier.mjs +171 -0
- package/lib/model.mjs +1445 -0
- package/lib/parallel.mjs +144 -0
- package/lib/perchance.mjs +210 -0
- package/lib/plan-coherence.mjs +1461 -0
- package/lib/plan-ledger.mjs +981 -0
- package/lib/plan.mjs +461 -0
- package/lib/policy.mjs +783 -0
- package/lib/prefix-order.mjs +38 -0
- package/lib/project-memory.mjs +127 -0
- package/lib/prompt.mjs +109 -0
- package/lib/python.mjs +862 -0
- package/lib/rcfile.mjs +853 -0
- package/lib/read-window.mjs +743 -0
- package/lib/refute-tools.mjs +34 -0
- package/lib/refute.mjs +806 -0
- package/lib/repl-driver.mjs +264 -0
- package/lib/repl.mjs +324 -0
- package/lib/replay.mjs +1218 -0
- package/lib/repo-map.mjs +1101 -0
- package/lib/report.mjs +419 -0
- package/lib/search-rank.mjs +99 -0
- package/lib/search.mjs +659 -0
- package/lib/secret-paths.mjs +54 -0
- package/lib/session.mjs +1017 -0
- package/lib/skills.mjs +703 -0
- package/lib/slash.mjs +356 -0
- package/lib/spawn-argv.mjs +1151 -0
- package/lib/spend.mjs +250 -0
- package/lib/steer.mjs +280 -0
- package/lib/stream.mjs +253 -0
- package/lib/stuck.mjs +712 -0
- package/lib/subagent.mjs +749 -0
- package/lib/terminal-graphics.mjs +171 -0
- package/lib/tool-prefix.mjs +226 -0
- package/lib/tool-shortlist.mjs +162 -0
- package/lib/tools.mjs +2333 -0
- package/lib/tsserver.mjs +423 -0
- package/lib/turn.mjs +5672 -0
- package/lib/untrusted-block.mjs +271 -0
- package/lib/verify-claim.mjs +299 -0
- package/lib/vision.mjs +330 -0
- package/lib/voice-task.mjs +561 -0
- package/lib/warm-provider.mjs +255 -0
- package/lib/websearch.mjs +401 -0
- package/lib/workspace.mjs +928 -0
- package/lib/write-approval.mjs +235 -0
- package/lib/write-many.mjs +162 -0
- package/package.json +62 -0
- package/scripts/bundle.mjs +768 -0
- package/scripts/cache-floor.mjs +176 -0
- package/scripts/machine.mjs +226 -0
- package/scripts/test.mjs +139 -0
- package/skills/accessibility.md +87 -0
- package/skills/acuvo-design-system.md +123 -0
- package/skills/animation.md +84 -0
- package/skills/api-design.md +82 -0
- package/skills/auth-and-sessions.md +78 -0
- package/skills/build-with-a-framework.md +101 -0
- package/skills/colour-and-contrast.md +112 -0
- package/skills/creative-engines.md +81 -0
- package/skills/css-layout.md +85 -0
- package/skills/data-and-charts.md +77 -0
- package/skills/debugging.md +76 -0
- package/skills/designing-by-looking.md +84 -0
- package/skills/error-handling.md +78 -0
- package/skills/forms-and-validation.md +93 -0
- package/skills/nextjs-app-router.md +75 -0
- package/skills/page-composition.md +103 -0
- package/skills/performance.md +77 -0
- package/skills/plan-before-building.md +52 -0
- package/skills/planning-and-delegating.md +72 -0
- package/skills/refactoring.md +70 -0
- package/skills/security-basics.md +76 -0
- package/skills/state-management.md +73 -0
- package/skills/supabase-multitenant.md +72 -0
- package/skills/typescript-strict.md +90 -0
- package/skills/typography.md +135 -0
- package/skills/verify-your-own-work.md +62 -0
- package/skills/web-app-quality.md +62 -0
- 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
|
+
}
|