efaimo 0.1.0 → 0.1.1

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 (41) hide show
  1. package/CHANGELOG.md +97 -2
  2. package/README.md +88 -30
  3. package/SKILL.md +13 -1
  4. package/dist/check/check.js +6 -2
  5. package/dist/cli.js +78 -4
  6. package/dist/clients/rawprobe.d.ts +21 -5
  7. package/dist/clients/rawprobe.js +101 -43
  8. package/dist/clients/stateless.js +17 -14
  9. package/dist/core/engine.js +20 -2
  10. package/dist/core/grade.js +7 -1
  11. package/dist/core/types.d.ts +22 -3
  12. package/dist/reporters/markdown.js +16 -4
  13. package/dist/reporters/pretty.js +36 -25
  14. package/dist/rules/injection.js +7 -0
  15. package/dist/rules/mcp/index.d.ts +14 -0
  16. package/dist/rules/mcp/index.js +68 -24
  17. package/dist/skills/parse.js +39 -3
  18. package/dist/targets/clientConfigs.d.ts +7 -0
  19. package/dist/targets/clientConfigs.js +4 -1
  20. package/dist/testing/anthropicRunner.js +2 -0
  21. package/dist/testing/harness.d.ts +21 -0
  22. package/dist/testing/harness.js +62 -11
  23. package/dist/testing/openaiRunner.js +8 -1
  24. package/dist/testing/stats.d.ts +32 -0
  25. package/dist/testing/stats.js +91 -0
  26. package/dist/util/dotenv.d.ts +9 -1
  27. package/dist/util/dotenv.js +11 -1
  28. package/dist/util/safeText.d.ts +8 -0
  29. package/dist/util/safeText.js +48 -0
  30. package/dist/version.d.ts +1 -1
  31. package/dist/version.js +1 -1
  32. package/dist/weigh/weigh.js +3 -1
  33. package/dist/weigh/window.d.ts +26 -0
  34. package/dist/weigh/window.js +44 -0
  35. package/docs/METHODOLOGY.md +70 -0
  36. package/docs/RULES.md +29 -23
  37. package/examples/contoso-crm-import/SKILL.md +21 -0
  38. package/examples/csv-cleanup/SKILL.md +17 -0
  39. package/examples/scenario.crm.yaml +22 -0
  40. package/examples/scenario.example.yaml +17 -0
  41. package/package.json +3 -1
package/CHANGELOG.md CHANGED
@@ -4,7 +4,102 @@ All notable changes to efaimo are documented here. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project uses
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
- ## [Unreleased]
7
+ ## [0.1.1] - 2026-08-02
8
+
9
+ Everything below was on `main` and unpublished. That gap was itself the
10
+ problem: `efaimo@0.1.0` still tells every user "the spec finalizes
11
+ 2026-07-28" (a future tense about a date that has passed), the README
12
+ documents `--strict-readiness` and `--window`, and neither flag exists in
13
+ 0.1.0. Cutting this release is what makes the documentation true.
14
+
15
+ The MCP specification published on 2026-07-28. The readiness rules here had
16
+ been written against the Release Candidate locked 2026-05-21, and the two are
17
+ not the same document: `DiscoverResult.serverInfo` was deleted,
18
+ `DiscoverResult` became cacheable, and three error codes were renumbered. The
19
+ entries below that name the published spec are the repairs, plus the tooling
20
+ that notices the next revision by itself.
21
+
22
+ ### Security
23
+
24
+ - `--client` now prints the actual command of every server it is about to
25
+ spawn, and warns by name when a config came from the CURRENT DIRECTORY
26
+ rather than your user config. It reads `.mcp.json`, `.cursor/mcp.json` and
27
+ `.vscode/mcp.json` from the working directory and executes what it finds, so
28
+ running the audit inside a repository you cloned was arbitrary code
29
+ execution, announced only by a friendly config key.
30
+ - Terminal control characters are stripped from anything the audited target
31
+ supplied, at the render boundary of the pretty and markdown reporters. A
32
+ hostile server could put ESC[1A ESC[2K in a tool name and rewrite the grade
33
+ line printed above its own finding, so the reader saw "grade A (100), 0
34
+ errors" while the contradiction was erased. `--no-color` did not help; that
35
+ only suppresses colours efaimo adds.
36
+ - `.env` loading is restricted to `ANTHROPIC_API_KEY` and `OPENAI_API_KEY`.
37
+ It previously set ANY key from a working-directory `.env`, so a cloned
38
+ repository could set `NODE_TLS_REJECT_UNAUTHORIZED=0` and silently disable
39
+ certificate verification for later HTTPS calls, including ones carrying your
40
+ key.
41
+ - Skill references that resolve outside the skill directory are no longer read
42
+ by `weigh`. S106 already warned about them, but only after the read.
43
+ - `SKILL.md` is read through the same 512KB-class cap as referenced files
44
+ (2MB), instead of an uncapped read.
45
+ - The published composite Action no longer interpolates its inputs into a bash
46
+ `run:` body, which was script injection in the consumer's runner.
47
+
48
+ ### Added
49
+
50
+ - `check --strict-readiness`: exit non-zero when the 2026-07-28 migration diff
51
+ is not clean. Readiness still never touches the grade or the badge, and the
52
+ default exit code is unchanged. It exists because a team that had finished
53
+ migrating had no way to stay migrated: `--strict` covers quality only, so
54
+ nothing in CI could catch a regression back onto the legacy handshake.
55
+ - `--window <tokens>` on `weigh` and `check`: report the context-window share
56
+ against a window you name instead of the 1,000,000-token default. Absolute
57
+ token counts do not change; only the denominator does.
58
+ - `npm run spec:drift`, plus a daily `spec-drift` workflow: fails when an MCP
59
+ revision newer than the one efaimo targets publishes, or when the targeted
60
+ revision changes under its own tag. The 2026-07-28 publication went unnoticed
61
+ in this repo for four days because nothing watched the spec itself.
62
+
63
+ ### Changed
64
+
65
+ - `check --conformance` pins `@modelcontextprotocol/conformance@0.2.0-alpha.10`
66
+ rather than the floating `alpha` tag, which had moved off `alpha.9` on
67
+ 2026-07-27. A dist-tag is not a pin, and the old one meant reporting "the
68
+ official conformance suite" for a build nobody here had run.
69
+ - Readiness output no longer claims the spec "finalizes 2026-07-28". It has
70
+ published; the diff still prints ungraded.
71
+ - Rule detail lines name the revision by its date instead of calling it "the
72
+ RC", which it no longer is.
73
+
74
+ ### Fixed
75
+
76
+ - `efaimo test` reports a two-sided Fisher exact p and a 95% interval, and
77
+ `helps`/`hurts` require p < 0.05. The old rule was a +-15 point threshold at
78
+ a default of 5 trials per arm, where 5/5 against 4/5 is +20 points and
79
+ p = 1.0000. Judge now runs at temperature 0; an unparseable judge verdict is
80
+ excluded rather than scored FAIL; default trials 5 -> 20.
81
+ - E123 fires. The matcher was `(delete|...)` and `_` is a word character,
82
+ so `delete_file`, `deleteFile` and `drop_table` never matched: the rule was
83
+ dead for every conventionally named tool.
84
+ - A rule that throws is reported as an ungraded E000 instead of being silently
85
+ swallowed, which used to RAISE the grade.
86
+ - `weigh`'s heaviest-tools column derives its width from the rows printed, so
87
+ a 30-character tool name no longer shifts the number column.
88
+ - The median in `scripts/skills-index.mjs` is the true median.
89
+ - `check --mcp` could hang forever on a stdio server. The readiness probes
90
+ cleaned up their spawned servers only on the happy path, so a probe that threw
91
+ left the child's stdin open; the child never saw EOF, never exited, and its
92
+ stdio handles kept the CLI's event loop alive. The report printed and then the
93
+ process simply never returned. Cleanup now runs in a `finally`, matching the
94
+ stateless introspection path next door, which already did.
95
+ - `check --mcp` and `weigh` lost server identity on servers that had finished
96
+ migrating. The published spec deleted `DiscoverResult.serverInfo` and moved
97
+ identity into `_meta["io.modelcontextprotocol/serverInfo"]`, and efaimo read
98
+ only the Release Candidate's field. Both shapes are read now, newest first.
99
+ - E118 measured cache fields on `tools/list` only, while its own detail line
100
+ already listed `server/discover`. The published spec made `DiscoverResult`
101
+ extend `CacheableResult`, so that surface is required now, is measured now,
102
+ and the message names whichever surface failed.
8
103
 
9
104
  ## [0.1.0]
10
105
 
@@ -43,5 +138,5 @@ First release.
43
138
  Documented rule set (`docs/RULES.md`), token methodology (`docs/METHODOLOGY.md`),
44
139
  and integration guide (`docs/INTEGRATIONS.md`).
45
140
 
46
- [Unreleased]: https://github.com/efaimo-ai/efaimo/compare/v0.1.0...HEAD
141
+ [0.1.1]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.1.1
47
142
  [0.1.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.1.0
package/README.md CHANGED
@@ -21,8 +21,8 @@ fires at all. Good tools exist for single slices of that problem; efaimo's job i
21
21
  the whole audit in one command, for both halves of what an agent loads. Lint a
22
22
  skill, weigh a server's tool definitions, get a migration diff for the 2026-07-28
23
23
  stateless MCP spec, and measure whether a skill actually improves task completion
24
- ([SkillsBench](https://www.skillsbench.ai) found 16 of 84 curated skills making
25
- agents worse; linting alone cannot catch that).
24
+ ([SkillsBench](https://www.skillsbench.ai) found curated skills made agents
25
+ worse on 16 of 84 tasks; linting alone cannot catch that).
26
26
 
27
27
  ## Agent Skills
28
28
 
@@ -35,11 +35,11 @@ Point it at one skill or a whole folder. It validates each skill against the
35
35
 
36
36
  ```text
37
37
  check skill claude-api
38
- grade C (71) 1 error 2 warnings 4 info
38
+ grade C (73) 1 error 2 warnings 4 info
39
39
 
40
40
  x S101 description is 1068 chars (spec max 1024)
41
41
  ! S104 instructions are ~18.4k tokens (spec recommends staying under 5k)
42
- fix: move detail into references/ files loaded on demand
42
+ fix: move detail into references/ files loaded on demand (progressive disclosure)
43
43
  ! S104 SKILL.md is 570 lines (spec recommends under 500)
44
44
  i S104 metadata level is ~294 tokens; the spec targets ~100 and this loads at
45
45
  startup for every installed skill
@@ -54,7 +54,7 @@ hygiene. Run it on a folder and every skill gets its own grade, not one aggregat
54
54
  `anthropics/claude-cookbooks`, and `obra/superpowers`, at pinned commits. 97%
55
55
  score an A, but even this curated set has real issues: Anthropic's own
56
56
  `claude-api` scores a C with an over-limit description and ~18k-token
57
- instructions, and the median skill's instructions run ~1,700 tokens loaded on
57
+ instructions, and the median skill's instructions run ~1,632 tokens loaded on
58
58
  every trigger. Full report, the corpus manifest, and the two commands that
59
59
  reproduce it: [the Skills Quality Index](./research/skills-index/REPORT.md).
60
60
 
@@ -103,9 +103,19 @@ test contoso-crm-import helps on an unknowable format
103
103
  </details>
104
104
 
105
105
  The first is pure context overhead, the second earns its tokens, and only the
106
- trial data tells them apart. Experimental and probabilistic: raise the trial
107
- count for confidence, and treat small deltas as noise. It is opt-in because a
108
- live run spends tokens on your key. See
106
+ trial data tells them apart.
107
+
108
+ The verdict is a significance test, not a threshold on the gap. Every run
109
+ reports a two-sided Fisher exact `p` and a 95% interval on the delta, and
110
+ `helps` / `hurts` require `p < 0.05`; anything else is `no measurable effect`
111
+ no matter how large the gap looks. That matters more than it sounds: at the
112
+ old default of 5 trials per arm, 5/5 against 4/5 is +20 points and a `p` of
113
+ exactly 1.0000, and the previous rule called it `helps`. The `+100` run above
114
+ is `p = 0.0002`; a `+25` run at 8 trials per arm is `p = 0.47` and now says so.
115
+ The default is 20 trials per arm, because below about that a partial result
116
+ cannot reach significance at all.
117
+
118
+ It is opt-in because a live run spends tokens on your key. See
109
119
  [examples/scenario.example.yaml](./examples/scenario.example.yaml) and
110
120
  [examples/scenario.crm.yaml](./examples/scenario.crm.yaml).
111
121
 
@@ -128,18 +138,21 @@ stateless protocol, so a 2026-07-28 server audits fine) and reports two things,
128
138
  separately. First, a **quality grade** for what models actually experience:
129
139
  descriptions, schemas, annotations, tool count, token cost. Second, an ungraded
130
140
  **migration diff** for the 2026-07-28 stateless spec (which removes
131
- `initialize`, sessions, Sampling, Roots, and Logging, and requires
141
+ `initialize` and sessions, deprecates Sampling, Roots, and Logging, and requires
132
142
  `server/discover`, `resultType`, and cache fields): exactly what will break and
133
143
  how to fix it, each rule naming the SEP it came from. Readiness never drags the
134
- grade; not having migrated to a spec that is not final until 2026-07-28 is a
135
- to-do list, not a defect. Full list: [docs/RULES.md](./docs/RULES.md).
144
+ grade, because the two answer different questions: the grade is what a model
145
+ experiences from this tool surface, readiness is whether a 2026-07-28 client can
146
+ talk to this server. Combining them scored the reference server a C for a tool
147
+ surface that is an A. Pass `--strict-readiness` when you want CI to fail on
148
+ the diff. Full list: [docs/RULES.md](./docs/RULES.md).
136
149
 
137
150
  <p align="center">
138
151
  <img src="assets/check.png" alt="efaimo check example output: quality grade A (95) plus a four-item 2026-07-28 migration diff for the reference server" width="800">
139
152
  </p>
140
153
 
141
- *(That is the official reference server: solid quality, four things to migrate
142
- before the 28th.)*
154
+ *(That is the official reference server: solid quality, four things left to
155
+ migrate.)*
143
156
 
144
157
  <details>
145
158
  <summary>The same output as copyable text</summary>
@@ -151,8 +164,7 @@ grade A (95) quality: 0 errors 1 warning 0 info
151
164
  ! E122 tool "echo": description misses 3/4 quality axes (length 40..600;
152
165
  says when to use it; mentions the result)
153
166
 
154
- 2026-07-28 readiness 4 items to migrate (a migration diff, not graded: the
155
- spec finalizes 2026-07-28)
167
+ 2026-07-28 readiness 4 items to migrate (a migration diff, not graded)
156
168
  ! E104 server declares the logging capability; MCP Logging is deprecated in
157
169
  2026-07-28 (SEP-2577) and logging/setLevel is removed
158
170
  ! E106 server/discover is not implemented (-32601 Method not found)
@@ -192,19 +204,56 @@ your context window.
192
204
  Focused tools already own single slices of this space, and some are very good.
193
205
  efaimo is the one tool that covers the whole audit surface. As of mid-2026:
194
206
 
195
- | | efaimo | [skill-validator](https://github.com/agent-ecosystem/skill-validator) | [upskill](https://github.com/huggingface/upskill) | [mcp-spec-check](https://github.com/Roee-Tsur/mcp-spec-check) | [conformance](https://github.com/modelcontextprotocol/conformance) (official) |
196
- |---|---|---|---|---|---|
197
- | Agent Skills linting | yes | yes | no | no | no |
198
- | Skill token cost | yes (3-level split) | yes | no | no | no |
199
- | Skill outcome testing | with/without A/B trials (experimental) | static LLM scoring | yes (generate + eval, code agents) | no | no |
200
- | MCP tool-definition cost | yes (3 serializations, `--anthropic` exact) | no | no | no | no |
201
- | MCP quality rules | yes | no | no | no | no |
202
- | 2026-07-28 readiness | migration diff: what breaks and how to fix | no | no | yes/no verdict | official test suite |
203
- | CI budget gate + badge | yes | no | no | no | no |
207
+ | | efaimo | [skill-validator](https://github.com/agent-ecosystem/skill-validator) | [skillgrade](https://github.com/mgechev/skillgrade) | [upskill](https://github.com/huggingface/upskill) | [mcp-spec-check](https://github.com/Roee-Tsur/mcp-spec-check) | [conformance](https://github.com/modelcontextprotocol/conformance) (official) |
208
+ |---|---|---|---|---|---|---|
209
+ | Agent Skills linting | yes | yes | no | no | no | no |
210
+ | Skill token cost | yes (3-level split) | yes | no | no | no | no |
211
+ | Skill outcome testing | with/without A/B trials (experimental) | static LLM scoring | **yes, and more thoroughly**: Docker-isolated runs, script + LLM-rubric graders, weighted scores, CI pass-rate gates | yes (generate + eval, code agents) | no | no |
212
+ | MCP tool-definition cost | yes (3 serializations, `--anthropic` exact) | no | no | no | no | no |
213
+ | MCP quality rules | yes | no | no | no | no | no |
214
+ | 2026-07-28 readiness | migration diff: what breaks and how to fix | no | no | no | yes/no verdict | official test suite |
215
+ | CI budget gate + badge | yes (token budget) | no | pass-rate threshold | no | no | no |
216
+
217
+ Read that table honestly: on skill outcome testing, `skillgrade` is the more
218
+ serious tool and we would use it over our own `efaimo test` for anything
219
+ depending on the result. Ours is one experimental subcommand in an audit CLI;
220
+ theirs is a testing framework. efaimo's claim is the row count, not any single
221
+ row.
222
+
223
+ [SkillsBench](https://github.com/benchflow-ai/skillsbench), whose paper this
224
+ README cites, is not in the table because it is a benchmark rather than a tool
225
+ you point at your own work: a fixed suite of tasks with packaged environments
226
+ and verifiers, extensible by contributing tasks upstream. Useful for the
227
+ question "do skills help in general", which is why we cite it, and not for
228
+ "does mine help", which is what `efaimo test` and `skillgrade` answer.
204
229
 
205
230
  efaimo runs the official conformance suite for you (`check --conformance` on
206
231
  http targets) and complements security scanners such as Snyk agent-scan and the
207
- skills installer (`npx skills`) rather than replacing them.
232
+ skills installer (`npx skills`) rather than replacing them. If your server is
233
+ built on the TypeScript SDK, efaimo tells you what breaks and why, and
234
+ [`@modelcontextprotocol/codemod`](https://www.npmjs.com/package/@modelcontextprotocol/codemod)
235
+ rewrites the SDK v1 to v2 call sites for you. Diff, codemod, re-check.
236
+
237
+ ### "Doesn't tool search already solve this?"
238
+
239
+ Partly, and that is the point. Anthropic ships a
240
+ [tool search tool](https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool):
241
+ mark tools `defer_loading: true` and the model discovers schemas on demand
242
+ instead of carrying all of them, with discovered schemas appended so the prompt
243
+ cache survives. Where it applies, it is a better fix than anything a linter can
244
+ tell you, and efaimo's own rules point at it (E127 and E128 suggest deferred
245
+ loading as a remedy).
246
+
247
+ It does not make measurement unnecessary, for three reasons. It is opt-in per
248
+ tool, so somebody has to decide which tools are worth deferring, and that
249
+ decision needs the per-tool numbers. It is a client-side feature, so it says
250
+ nothing about what your server publishes to hosts that do not implement it.
251
+ And a mitigation you cannot measure is a mitigation you are trusting on faith:
252
+ `weigh` reports the definitions your server actually ships, which is the input
253
+ every mitigation operates on.
254
+
255
+ If deferred loading covers your whole setup, you may not need this tool. Run
256
+ `weigh` once and find out; that answer is worth more than our opinion.
208
257
 
209
258
  ## Install
210
259
 
@@ -222,6 +271,9 @@ Or add it to a project with `npm i -D efaimo`, or the GitHub Action:
222
271
  command: check --skill ./skills --strict
223
272
  ```
224
273
 
274
+ The Action is listed on the
275
+ [GitHub Marketplace](https://github.com/marketplace/actions/efaimo).
276
+
225
277
  Gate a pull request on context-window growth, too:
226
278
 
227
279
  ```bash
@@ -243,11 +295,16 @@ More recipes (pre-commit, GitLab, editor audit, programmatic use):
243
295
  |---|---|
244
296
  | **S101 to S106** | skills: frontmatter and trigger quality, trigger collisions, context budget, reference integrity, injection hygiene |
245
297
  | **E101 to E118** | MCP 2026-07-28 readiness: deprecated primitives, statelessness, `server/discover`, `resultType`, cache fields, transport |
246
- | **E121 to E130** | MCP quality: description quality, annotations, schema hygiene, tool-count and token-cost budgets |
298
+ | **E121 to E128, E130** | MCP quality: description quality, annotations, schema hygiene, tool-count and token-cost budgets |
299
+
300
+ `E000` is not a finding about your server: it means one of efaimo's own rules
301
+ threw and was skipped, so the report is incomplete. It is never scored.
247
302
 
248
303
  Quality (E12x-E13x) and skill (S) findings set the letter grade; readiness
249
- findings (E101-E118) are reported as an ungraded migration diff until the spec
250
- ratifies. Every finding carries a stable id you can suppress or link. See
304
+ findings (E101-E118) are reported as an ungraded migration diff, which
305
+ `--strict-readiness` can turn into a non-zero exit
306
+ without touching the grade.
307
+ Every finding carries a stable id you can suppress or link. See
251
308
  [docs/RULES.md](./docs/RULES.md).
252
309
 
253
310
  ## Roadmap
@@ -255,8 +312,9 @@ ratifies. Every finding carries a stable id you can suppress or link. See
255
312
  - Harden `efaimo test`: a separately chosen judge model, confidence intervals on
256
313
  the delta, multi-turn tool-use trials, and judge calibration, so the
257
314
  experimental harness earns unqualified trust.
258
- - Track the 2026-07-28 spec to ratification, then start grading readiness (today
259
- it is an ungraded migration diff on purpose).
315
+ - Readiness rules for the two 2026-07-28 changes that have none yet: the
316
+ `Mcp-Method` / `Mcp-Name` request headers (SEP-2243), and Tasks moving out of
317
+ core into the `io.modelcontextprotocol/tasks` extension (SEP-2663).
260
318
  - A public, continuously updated Agent Skills Quality Index over a broad corpus.
261
319
 
262
320
  ## Stability
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: efaimo
3
3
  description: Audit MCP servers and Agent Skills for quality and context-window cost. Use when the user wants to check an MCP server's token cost, lint a skill, verify 2026-07-28 MCP spec readiness, or add an efaimo badge or CI gate.
4
4
  license: Apache-2.0
5
5
  metadata:
6
- version: "0.1.0"
6
+ version: "0.1.1"
7
7
  homepage: "https://efaimo.ai"
8
8
  ---
9
9
 
@@ -18,6 +18,18 @@ skill well-formed", or "add a CI gate / badge for context cost".
18
18
 
19
19
  Run via `npx efaimo` (no install needed).
20
20
 
21
+ **Before running any of these against a stdio server: auditing one means
22
+ executing it.** `weigh` and `check --mcp` start the command you pass as a child
23
+ process and speak MCP to it, because that is the only way to see the tool
24
+ definitions it would put in a context window. So only point them at commands
25
+ you would be willing to run yourself. A remote target (`https://...`) is not
26
+ executed and is the safe option for something you do not trust. `--client`
27
+ reads the MCP config files in the current directory as well as your user
28
+ config, so running it inside a repository you did not write means starting the
29
+ servers that repository configured. This warning lives in `SECURITY.md` and did
30
+ not live here, which is the wrong way round: the policy is where humans look,
31
+ and this file is where agents take orders.
32
+
21
33
  - **Weigh context cost of an MCP server**
22
34
  `npx efaimo weigh "npx -y <server-package>"` for a stdio server, or
23
35
  `npx efaimo weigh https://host/mcp` for a remote one. Add `--json` for machine
@@ -10,8 +10,12 @@ import { findSkills } from "../skills/parse.js";
10
10
  import { weighServer, weighSkills } from "../weigh/weigh.js";
11
11
  import { VERSION } from "../version.js";
12
12
  function buildReport(surface, target, findings, notes) {
13
- // MCP readiness findings (E101-E118) are a migration diff for a spec that is
14
- // not final until 2026-07-28; they are reported separately and never graded.
13
+ // MCP readiness findings (E101-E118) are a migration diff for the 2026-07-28
14
+ // spec: a migration not yet made is a to-do list, not a defect in what
15
+ // shipped. Reported separately and never graded (ADR-014, reaffirmed in
16
+ // ADR-027). That decision is CLOSED: the spec published on 2026-07-28 and
17
+ // readiness still does not count toward a grade. This comment described it
18
+ // as open for a day after ADR-027, in the file that implements the split.
15
19
  const readinessAll = [];
16
20
  const gradedAll = [];
17
21
  for (const f of findings) {
package/dist/cli.js CHANGED
@@ -7,9 +7,11 @@ import { VERSION } from "./version.js";
7
7
  import { resolveTarget } from "./targets/resolve.js";
8
8
  import { loadClientServers, SUPPORTED_CLIENTS } from "./targets/clientConfigs.js";
9
9
  import { introspectServer } from "./clients/introspect.js";
10
+ import { SPEC_VERSION } from "./clients/rawprobe.js";
10
11
  import { weighServer, weighSkills } from "./weigh/weigh.js";
11
12
  import { findSkills } from "./skills/parse.js";
12
13
  import { diffServerWeigh } from "./weigh/diff.js";
14
+ import { DEFAULT_CONTEXT_WINDOW, formatWindowShare, setContextWindow } from "./weigh/window.js";
13
15
  import { checkMcpRepoOnly, checkMcpTarget, checkSkillSet } from "./check/check.js";
14
16
  import { renderCheckPretty, renderDiffPretty, renderScenarioPlan, renderServerWeighPretty, renderSkillSetPretty, renderSkillWeighPretty, renderTestReportPretty, setColor, } from "./reporters/pretty.js";
15
17
  import { parseScenario, runScenario } from "./testing/harness.js";
@@ -45,7 +47,21 @@ function collectPairs(sep) {
45
47
  };
46
48
  }
47
49
  function colorSetup(opts) {
48
- setColor(opts.color !== false && process.env.NO_COLOR === undefined);
50
+ // Escape codes are noise once stdout is a file or another tool's stdin, and
51
+ // `efaimo weigh > report.txt` is a normal thing to do. So colour needs both
52
+ // the user's consent and a terminal to write to; FORCE_COLOR is the escape
53
+ // hatch for CI that renders ANSI in its log viewer.
54
+ const forced = process.env.FORCE_COLOR !== undefined && process.env.FORCE_COLOR !== "0";
55
+ const wanted = opts.color !== false && process.env.NO_COLOR === undefined;
56
+ setColor(wanted && (forced || process.stdout.isTTY === true));
57
+ }
58
+ function windowSetup(opts) {
59
+ if (opts.window === undefined)
60
+ return;
61
+ const n = Number(opts.window);
62
+ if (!Number.isFinite(n) || n <= 0)
63
+ fail(`--window must be a positive number of tokens, got "${opts.window}"`);
64
+ setContextWindow(n);
49
65
  }
50
66
  function fail(message) {
51
67
  console.error(pc.red(`error: ${message}`));
@@ -100,9 +116,11 @@ program
100
116
  .option("--allow-increase <pct>", "with --diff: exit 1 if the increase exceeds pct percent")
101
117
  .option("--badge [file]", "write an SVG badge + shields endpoint JSON")
102
118
  .option("--anthropic [model]", "also measure exact Claude tokens via the count_tokens API (needs ANTHROPIC_API_KEY)")
119
+ .option("--window <tokens>", "context window the share is reported against", String(DEFAULT_CONTEXT_WINDOW))
103
120
  .option("--no-color", "disable colors")
104
121
  .action(async (targetArg, opts) => {
105
122
  colorSetup(opts);
123
+ windowSetup(opts);
106
124
  const timeoutMs = seconds(opts.timeout) * 1000;
107
125
  const maxTokens = parseNumberOpt(opts.maxTokens, "--max-tokens");
108
126
  const allowIncreasePct = parseNumberOpt(opts.allowIncrease, "--allow-increase");
@@ -114,6 +132,29 @@ program
114
132
  fail(`no MCP servers found for client "${opts.client}" (checked: ${[...conf.sources, ...conf.missing].join(", ")})`);
115
133
  }
116
134
  console.error(pc.dim(`config: ${conf.sources.join(", ")}`));
135
+ // Say what is about to be EXECUTED, not just which file it came from.
136
+ //
137
+ // --client reads .mcp.json, .cursor/mcp.json and .vscode/mcp.json from
138
+ // the CURRENT DIRECTORY as well as the user config, and then spawns
139
+ // every command in them. Cloning an untrusted repo and running the audit
140
+ // tool inside it was therefore arbitrary code execution, and the only
141
+ // thing printed was the friendly config key, never the command. Cursor
142
+ // and VS Code gate project-scoped MCP config behind workspace trust;
143
+ // this printed nothing at all.
144
+ //
145
+ // Listing them is the minimum. A stdio entry names a real command that
146
+ // is about to run on this machine.
147
+ for (const e of conf.entries) {
148
+ const t = e.target;
149
+ const shown = t.kind === "stdio"
150
+ ? [t.command, ...t.args].join(" ") + (t.env && Object.keys(t.env).length ? ` (+${Object.keys(t.env).length} env)` : "")
151
+ : t.url;
152
+ console.error(pc.dim(` ${e.name}: ${shown}`));
153
+ }
154
+ if (conf.fromCwd.length) {
155
+ console.error(pc.yellow(` ! ${conf.fromCwd.length} of these came from the current directory (${conf.fromCwd.join(", ")}), not your user config.\n` +
156
+ ` Auditing a stdio server means executing it. Only continue in a repository you trust.`));
157
+ }
117
158
  targets.push(...conf.entries.map((e) => e.target));
118
159
  }
119
160
  if (targetArg) {
@@ -186,7 +227,7 @@ program
186
227
  .filter((r) => r.kind === "mcp")
187
228
  .reduce((s, r) => s + r.totals.claudeStyle, 0);
188
229
  if (mcpTotals > 0) {
189
- console.log(pc.bold(`\ncombined MCP context cost (Claude-style, o200k est.): ${mcpTotals.toLocaleString("en-US")} tokens (~${((mcpTotals / 200000) * 100).toFixed(1)}% of a 200k window)`));
230
+ console.log(pc.bold(`\ncombined MCP context cost (Claude-style, o200k est.): ${mcpTotals.toLocaleString("en-US")} tokens (${formatWindowShare(mcpTotals)})`));
190
231
  }
191
232
  }
192
233
  }
@@ -226,6 +267,7 @@ program
226
267
  .option("--repo <path>", "additionally scan this source repo for deprecated API usage")
227
268
  .option("--no-probe", "skip live readiness probes (bare request, server/discover, ordering)")
228
269
  .option("--strict", "exit 1 on warnings too")
270
+ .option("--strict-readiness", "exit 1 if the 2026-07-28 migration diff is not clean (never changes the grade)")
229
271
  .option("--conformance", "after the audit, run the official MCP conformance suite (http targets)")
230
272
  .option("--header <header>", 'HTTP header "Key: Value" (repeatable)', collectPairs(":"))
231
273
  .option("--env <pair>", "KEY=VALUE for stdio servers (repeatable)", collectPairs("="))
@@ -234,9 +276,11 @@ program
234
276
  .option("--md", "print Markdown")
235
277
  .option("--badge [file]", "write a grade badge SVG + shields endpoint JSON")
236
278
  .option("--anthropic [model]", "use exact Claude token counts where relevant")
279
+ .option("--window <tokens>", "context window the share is reported against", String(DEFAULT_CONTEXT_WINDOW))
237
280
  .option("--no-color", "disable colors")
238
281
  .action(async (targetArg, opts) => {
239
282
  colorSetup(opts);
283
+ windowSetup(opts);
240
284
  if (!targetArg)
241
285
  fail("nothing to check: pass a target (see --help)");
242
286
  if (opts.mcp && opts.skill)
@@ -305,10 +349,31 @@ program
305
349
  console.error(pc.yellow("note: the official conformance suite drives http targets (--url); skipping for this target"));
306
350
  }
307
351
  else {
308
- console.error(pc.dim("\nrunning official MCP conformance suite (npx @modelcontextprotocol/conformance)...\n"));
352
+ // The `latest` line of the conformance suite (0.1.16) predates the
353
+ // 2026-07-28 revision, so it silently tests the old protocol. Those
354
+ // scenarios ship on the `alpha` line only; --spec-version scopes them
355
+ // to the revision efaimo is about.
356
+ //
357
+ // Pinned to an exact version, because a dist-tag is not a pin. `alpha`
358
+ // moved from 0.2.0-alpha.9 to 0.2.0-alpha.10 on 2026-07-27, and we went
359
+ // on reporting "the official conformance suite" for a build nobody here
360
+ // had run: the same "we do not know what we ran" bug this block was
361
+ // written to fix, through the other door. Bumping it means re-reading
362
+ // `server --help` first; the two flags below were confirmed against
363
+ // 0.2.0-alpha.10.
364
+ const args = [
365
+ "-y",
366
+ "@modelcontextprotocol/conformance@0.2.0-alpha.10",
367
+ "server",
368
+ "--url",
369
+ target.url,
370
+ "--spec-version",
371
+ SPEC_VERSION,
372
+ ];
373
+ console.error(pc.dim(`\nrunning official MCP conformance suite:\n npx ${args.slice(1).join(" ")}\n`));
309
374
  // cross-spawn resolves npx(.cmd) without a shell, so target.url is passed
310
375
  // as a literal argument (never interpreted by cmd.exe).
311
- const r = crossSpawn.sync("npx", ["-y", "@modelcontextprotocol/conformance", "server", "--url", target.url], {
376
+ const r = crossSpawn.sync("npx", args, {
312
377
  stdio: "inherit",
313
378
  });
314
379
  if (r.status !== 0)
@@ -318,6 +383,15 @@ program
318
383
  if (report.counts.error > 0 || (opts.strict && report.counts.warn > 0)) {
319
384
  process.exitCode = 1;
320
385
  }
386
+ // Readiness stays out of the grade, the badge and the default exit code
387
+ // (ADR-014, reaffirmed in ADR-027). But a team that HAS migrated had no way
388
+ // to stay migrated: --strict covers quality only, so nothing in CI could
389
+ // catch a regression back onto the legacy handshake. This is the opt-in for
390
+ // exactly that, and it moves the exit code only. Nobody's grade changes,
391
+ // no published number moves, and the operator decides rather than us.
392
+ if (opts.strictReadiness && (report.readiness?.findings.length ?? 0) > 0) {
393
+ process.exitCode = 1;
394
+ }
321
395
  });
322
396
  program
323
397
  .command("test")
@@ -6,7 +6,14 @@ import type { ResolvedTarget } from "../targets/resolve.js";
6
6
  * stateless requests, server/discover) need raw transport access.
7
7
  * Every probe fails soft into a { skipped } marker.
8
8
  */
9
- export declare const RC_VERSION = "2026-07-28";
9
+ /**
10
+ * The MCP revision efaimo audits against. It was called RC_VERSION until
11
+ * 2026-08-01, by which point the revision had been published for four days and
12
+ * the name had been lying for all of them. scripts/spec-drift.mjs reads this
13
+ * declaration by name, so renaming it again means updating that regex, which
14
+ * fails loudly rather than silently watching nothing.
15
+ */
16
+ export declare const SPEC_VERSION = "2026-07-28";
10
17
  interface RpcReply {
11
18
  result?: Record<string, unknown>;
12
19
  error?: {
@@ -16,7 +23,7 @@ interface RpcReply {
16
23
  timeout?: boolean;
17
24
  procExit?: number | null;
18
25
  }
19
- export declare function rcMeta(): Record<string, unknown>;
26
+ export declare function specMeta(): Record<string, unknown>;
20
27
  /**
21
28
  * Only an explicit "not initialized" / "session required" error proves the
22
29
  * server requires the removed initialize handshake. A plain timeout is NOT proof
@@ -24,11 +31,20 @@ export declare function rcMeta(): Record<string, unknown>;
24
31
  * inconclusive; a crash is its own signal. This keeps E105 from false-flagging a
25
32
  * healthy but slow server, the worst verdict a readiness checker can emit.
26
33
  *
27
- * Detection is by message, not by error code: the codes SDKs use here are
34
+ * Detection is by message, not by error code, and the spec that published on
35
+ * 2026-07-28 is the argument for why. The codes SDKs use here are
28
36
  * implementation-defined (TS StreamableHTTP sends -32000 for both "Server not
29
37
  * initialized" and "Mcp-Session-Id header is required", -32001 for "Session not
30
- * found"), -32002 means Resource-not-found in the 2025 specs, and the RC
31
- * reserves -32022 for UnsupportedProtocolVersion, so no code is diagnostic.
38
+ * found"), and the MCP-defined ones MOVED between the locked RC and the
39
+ * published spec: MissingRequiredClientCapability went -32003 to -32021 and
40
+ * UnsupportedProtocolVersion went -32004 to -32022, and HeaderMismatch moved
41
+ * the same way, -32001 to -32020. This comment said HeaderMismatch was "added
42
+ * alongside them", which was wrong: the RC already defined it at -32001 in the
43
+ * transport prose, and what the published revision added was its entry in the
44
+ * schema. The distinction matters to anyone still returning -32001.
45
+ * -32002 (resource not found) is retired outright,
46
+ * replaced by -32602 and promised never to be reused. Matching on numbers
47
+ * would have rotted here on 2026-07-28; matching on messages did not.
32
48
  */
33
49
  export declare function looksLikeInitGate(o: ProbeOutcome): boolean;
34
50
  export declare function runProbes(target: Extract<ResolvedTarget, {