efaimo 0.1.0 → 0.1.2

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 (45) hide show
  1. package/CHANGELOG.md +160 -4
  2. package/README.md +89 -31
  3. package/SKILL.md +13 -1
  4. package/dist/check/check.js +14 -2
  5. package/dist/cli.js +95 -9
  6. package/dist/clients/introspect.js +10 -2
  7. package/dist/clients/rawprobe.d.ts +21 -5
  8. package/dist/clients/rawprobe.js +101 -43
  9. package/dist/clients/stateless.js +17 -14
  10. package/dist/core/engine.js +20 -2
  11. package/dist/core/grade.js +7 -1
  12. package/dist/core/types.d.ts +22 -3
  13. package/dist/reporters/markdown.js +16 -4
  14. package/dist/reporters/pretty.js +38 -25
  15. package/dist/rules/injection.d.ts +1 -2
  16. package/dist/rules/injection.js +7 -11
  17. package/dist/rules/mcp/index.d.ts +14 -0
  18. package/dist/rules/mcp/index.js +89 -25
  19. package/dist/rules/skill/index.js +5 -3
  20. package/dist/skills/parse.js +39 -3
  21. package/dist/targets/clientConfigs.d.ts +7 -0
  22. package/dist/targets/clientConfigs.js +4 -1
  23. package/dist/targets/repoScan.js +0 -0
  24. package/dist/testing/anthropicRunner.js +2 -0
  25. package/dist/testing/harness.d.ts +22 -0
  26. package/dist/testing/harness.js +98 -13
  27. package/dist/testing/openaiRunner.js +15 -1
  28. package/dist/testing/stats.d.ts +30 -0
  29. package/dist/testing/stats.js +89 -0
  30. package/dist/util/dotenv.d.ts +9 -1
  31. package/dist/util/dotenv.js +11 -1
  32. package/dist/util/safeText.d.ts +8 -0
  33. package/dist/util/safeText.js +48 -0
  34. package/dist/version.d.ts +1 -1
  35. package/dist/version.js +1 -1
  36. package/dist/weigh/weigh.js +3 -1
  37. package/dist/weigh/window.d.ts +26 -0
  38. package/dist/weigh/window.js +44 -0
  39. package/docs/METHODOLOGY.md +70 -0
  40. package/docs/RULES.md +31 -25
  41. package/examples/contoso-crm-import/SKILL.md +21 -0
  42. package/examples/csv-cleanup/SKILL.md +17 -0
  43. package/examples/scenario.crm.yaml +22 -0
  44. package/examples/scenario.example.yaml +17 -0
  45. package/package.json +4 -2
package/CHANGELOG.md CHANGED
@@ -4,9 +4,163 @@ 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.2] - 2026-08-03
8
8
 
9
- ## [0.1.0]
9
+ The 0.1.2 code backlog: edge-case fixes surfaced by the five-agent review, none
10
+ of which changes a published number or the documented command surface, plus the
11
+ security and CI hardening already staged.
12
+
13
+ ### Fixed
14
+
15
+ - `check --mcp <directory>` with no source files and no MCP SDK dependency now
16
+ fails instead of returning a clean A(100): an empty scan is a failure, never a
17
+ pass.
18
+ - S101: a skill with no frontmatter reports its missing `name` and `description`
19
+ as errors, so a total absence no longer grades higher than a broken one.
20
+ - E105: a server that exits on a bare stateless `tools/list` is flagged. The
21
+ crash outcome was unhandled, so a crashing server looked more migrated than
22
+ one that answers with a not-initialized error.
23
+ - E124: the "N of M parameters lack descriptions" summary is no longer dropped
24
+ by the display cap on schemas with five or more oversized enums.
25
+ - S106: the escape check is lexical (the resolved path), not a `..` substring,
26
+ so a file merely named `notes..md` is not flagged as escaping.
27
+ - A Poetry `mcp = "^2.0"` constraint in `pyproject.toml` is read as the 2.x
28
+ line, not legacy; the version regex had captured the `=` assignment.
29
+ - `weigh --diff` with a skill baseline fails with a clear message instead of a
30
+ raw TypeError; a server baseline is required.
31
+ - `--max-tokens` and `--allow-increase` reject non-positive / negative values
32
+ instead of turning the gate into an always-fail with a nonsense message.
33
+ - `efaimo test --live` against an OpenAI o-series reasoning model no longer
34
+ sends an unsupported `temperature` (a 400) and gives it token headroom.
35
+ - The live test harness retries a transient 429 or 5xx with backoff instead of
36
+ discarding every already-paid trial on the first blip.
37
+ - The test report prints a clean note instead of "95% interval NaN to NaN" when
38
+ an arm produces no scoreable trial.
39
+ - A dead stdio target fails in about one `--timeout`, not two: the legacy
40
+ handshake and the stateless fallback now share the budget.
41
+ - Internal: dropped a dead exported constant and the unused per-pattern
42
+ `severity` on the injection heuristics (every hit is info + ungraded by
43
+ design).
44
+
45
+ ### Security
46
+
47
+ - The lockfile was re-resolved so the dev/CI tree carries
48
+ `@hono/node-server` 2.0.12 instead of the 1.19.x line that
49
+ GHSA-frvp-7c67-39w9 covers (moderate, path traversal in `serve-static`, in
50
+ the half of the MCP SDK this CLI never executes). A fresh `npx efaimo`
51
+ install already resolved the patched line; this aligns the committed tree
52
+ with what users actually get, and `pnpm audit --prod` is clean again.
53
+
54
+ ### Changed
55
+
56
+ - `@modelcontextprotocol/sdk` floor moved within-range from ^1.29.0 to ^1.30.0
57
+ (what a fresh install resolves anyway). The full suite (136 tests) is green.
58
+ - CI: the reference-server smoke is pinned to
59
+ `@modelcontextprotocol/server-everything@2026.7.4`. Per-commit CI should be
60
+ deterministic; news about the reference server's latest belongs to a
61
+ scheduled run, not to a push.
62
+ - CI: `spec-drift.yml` and the example workflow now pin actions by commit SHA
63
+ like the other workflows, and the example declares least-privilege
64
+ `permissions`.
65
+
66
+ ## [0.1.1] - 2026-08-02
67
+
68
+ Everything below was on `main` and unpublished. That gap was itself the
69
+ problem: `efaimo@0.1.0` still tells every user "the spec finalizes
70
+ 2026-07-28" (a future tense about a date that has passed), the README
71
+ documents `--strict-readiness` and `--window`, and neither flag exists in
72
+ 0.1.0. Cutting this release is what makes the documentation true.
73
+
74
+ The MCP specification published on 2026-07-28. The readiness rules here had
75
+ been written against the Release Candidate locked 2026-05-21, and the two are
76
+ not the same document: `DiscoverResult.serverInfo` was deleted,
77
+ `DiscoverResult` became cacheable, and three error codes were renumbered. The
78
+ entries below that name the published spec are the repairs, plus the tooling
79
+ that notices the next revision by itself.
80
+
81
+ ### Security
82
+
83
+ - `--client` now prints the actual command of every server it is about to
84
+ spawn, and warns by name when a config came from the CURRENT DIRECTORY
85
+ rather than your user config. It reads `.mcp.json`, `.cursor/mcp.json` and
86
+ `.vscode/mcp.json` from the working directory and executes what it finds, so
87
+ running the audit inside a repository you cloned was arbitrary code
88
+ execution, announced only by a friendly config key.
89
+ - Terminal control characters are stripped from anything the audited target
90
+ supplied, at the render boundary of the pretty and markdown reporters. A
91
+ hostile server could put ESC[1A ESC[2K in a tool name and rewrite the grade
92
+ line printed above its own finding, so the reader saw "grade A (100), 0
93
+ errors" while the contradiction was erased. `--no-color` did not help; that
94
+ only suppresses colors efaimo adds.
95
+ - `.env` loading is restricted to `ANTHROPIC_API_KEY` and `OPENAI_API_KEY`.
96
+ It previously set ANY key from a working-directory `.env`, so a cloned
97
+ repository could set `NODE_TLS_REJECT_UNAUTHORIZED=0` and silently disable
98
+ certificate verification for later HTTPS calls, including ones carrying your
99
+ key.
100
+ - Skill references that resolve outside the skill directory are no longer read
101
+ by `weigh`. S106 already warned about them, but only after the read.
102
+ - `SKILL.md` is read through a capped read (2MB) on the same guarded path that
103
+ caps referenced files at 512KB, instead of an uncapped read.
104
+ - The published composite Action no longer interpolates its inputs into a bash
105
+ `run:` body, which was script injection in the consumer's runner.
106
+
107
+ ### Added
108
+
109
+ - `check --strict-readiness`: exit non-zero when the 2026-07-28 migration diff
110
+ is not clean. Readiness still never touches the grade or the badge, and the
111
+ default exit code is unchanged. It exists because a team that had finished
112
+ migrating had no way to stay migrated: `--strict` covers quality only, so
113
+ nothing in CI could catch a regression back onto the legacy handshake.
114
+ - `--window <tokens>` on `weigh` and `check`: report the context-window share
115
+ against a window you name instead of the 1,000,000-token default. Absolute
116
+ token counts do not change; only the denominator does.
117
+ - `npm run spec:drift`, plus a daily `spec-drift` workflow: fails when an MCP
118
+ revision newer than the one efaimo targets publishes, or when the targeted
119
+ revision changes under its own tag. The 2026-07-28 publication went unnoticed
120
+ in this repo for four days because nothing watched the spec itself.
121
+
122
+ ### Changed
123
+
124
+ - `check --conformance` pins `@modelcontextprotocol/conformance@0.2.0-alpha.10`
125
+ rather than the floating `alpha` tag, which had moved off `alpha.9` on
126
+ 2026-07-27. A dist-tag is not a pin, and the old one meant reporting "the
127
+ official conformance suite" for a build nobody here had run.
128
+ - Readiness output no longer claims the spec "finalizes 2026-07-28". It has
129
+ published; the diff still prints ungraded.
130
+ - Rule detail lines name the revision by its date instead of calling it "the
131
+ RC", which it no longer is.
132
+
133
+ ### Fixed
134
+
135
+ - `efaimo test` reports a two-sided Fisher exact p and a 95% interval, and
136
+ `helps`/`hurts` require p < 0.05. The old rule was a +-15 point threshold at
137
+ a default of 5 trials per arm, where 5/5 against 4/5 is +20 points and
138
+ p = 1.0000. Judge now runs at temperature 0; an unparseable judge verdict is
139
+ excluded rather than scored FAIL; default trials 5 -> 20.
140
+ - E123 fires. The matcher was `(delete|...)` and `_` is a word character,
141
+ so `delete_file`, `deleteFile` and `drop_table` never matched: the rule was
142
+ dead for every conventionally named tool.
143
+ - A rule that throws is reported as an ungraded E000 instead of being silently
144
+ swallowed, which used to RAISE the grade.
145
+ - `weigh`'s heaviest-tools column derives its width from the rows printed, so
146
+ a 30-character tool name no longer shifts the number column.
147
+ - The median in `scripts/skills-index.mjs` is the true median.
148
+ - `check --mcp` could hang forever on a stdio server. The readiness probes
149
+ cleaned up their spawned servers only on the happy path, so a probe that threw
150
+ left the child's stdin open; the child never saw EOF, never exited, and its
151
+ stdio handles kept the CLI's event loop alive. The report printed and then the
152
+ process simply never returned. Cleanup now runs in a `finally`, matching the
153
+ stateless introspection path next door, which already did.
154
+ - `check --mcp` and `weigh` lost server identity on servers that had finished
155
+ migrating. The published spec deleted `DiscoverResult.serverInfo` and moved
156
+ identity into `_meta["io.modelcontextprotocol/serverInfo"]`, and efaimo read
157
+ only the Release Candidate's field. Both shapes are read now, newest first.
158
+ - E118 measured cache fields on `tools/list` only, while its own detail line
159
+ already listed `server/discover`. The published spec made `DiscoverResult`
160
+ extend `CacheableResult`, so that surface is required now, is measured now,
161
+ and the message names whichever surface failed.
162
+
163
+ ## [0.1.0] - 2026-07-18
10
164
 
11
165
  First release.
12
166
 
@@ -19,7 +173,7 @@ First release.
19
173
  broken or auth-gated server is skipped with a reason, not fatal.
20
174
  `--out`/`--diff`/`--max-tokens`/`--allow-increase` for CI budget gates,
21
175
  `--badge` for a shields endpoint.
22
- - `efaimo check --mcp`: a quality grade (E121-E130: descriptions, schemas,
176
+ - `efaimo check --mcp`: a quality grade (E121-E128, E130: descriptions, schemas,
23
177
  annotations, cost) plus a separate, ungraded 2026-07-28 migration diff
24
178
  (E101-E118: what the stateless spec breaks and how to fix it, each item naming
25
179
  its SEP). Speaks both the legacy handshake and bare stateless requests, so
@@ -43,5 +197,7 @@ First release.
43
197
  Documented rule set (`docs/RULES.md`), token methodology (`docs/METHODOLOGY.md`),
44
198
  and integration guide (`docs/INTEGRATIONS.md`).
45
199
 
46
- [Unreleased]: https://github.com/efaimo-ai/efaimo/compare/v0.1.0...HEAD
200
+ [unreleased]: https://github.com/efaimo-ai/efaimo/compare/v0.1.2...HEAD
201
+ [0.1.2]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.1.2
202
+ [0.1.1]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.1.1
47
203
  [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
@@ -53,8 +53,8 @@ hygiene. Run it on a folder and every skill gets its own grade, not one aggregat
53
53
  **We graded 36 public Agent Skills**: every skill in `anthropics/skills`,
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
- `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
56
+ `claude-api` scores a C (73) with an over-limit description and ~18k-token
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.2"
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. Until 2026-08-02 this warning lived only in
30
+ `SECURITY.md`, which was 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) {
@@ -46,6 +50,14 @@ export async function checkMcpTarget(target, opts = {}) {
46
50
  }
47
51
  export function checkMcpRepoOnly(repoPath, label) {
48
52
  const repo = scanRepo(repoPath);
53
+ // An empty result is a failure, never a pass. With no source files read and no
54
+ // dependency manifest naming an MCP SDK, there was nothing to examine here, and
55
+ // a clean A(100) would be a grade for a directory we never looked inside. Fail
56
+ // loudly, the way checkSkillSet does for a missing SKILL.md.
57
+ if (repo.filesScanned === 0 && !repo.sdk) {
58
+ throw new Error(`no MCP server source found under "${repoPath}": no code files and no SDK dependency. ` +
59
+ `Point --mcp at the server's run command, or at a directory that contains its source.`);
60
+ }
49
61
  const emptyIntro = {
50
62
  targetLabel: label,
51
63
  transport: "stdio",
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}`));
@@ -66,12 +82,18 @@ function seconds(value, label = "--timeout") {
66
82
  fail(`${label} must be a positive number of seconds, got "${value}"`);
67
83
  return n;
68
84
  }
69
- function parseNumberOpt(value, label) {
85
+ function parseNumberOpt(value, label, bound) {
70
86
  if (value === undefined)
71
87
  return undefined;
72
88
  const n = Number(value);
73
89
  if (!Number.isFinite(n))
74
90
  fail(`${label} must be a number, got "${value}"`);
91
+ // A budget or a percentage below its floor is not a stricter gate, it is a
92
+ // nonsensical one: `--max-tokens -5` turned "over budget" into always-true
93
+ // with a negative ceiling in the message. Reject it at the boundary.
94
+ if (bound && (bound.exclusive ? n <= bound.min : n < bound.min)) {
95
+ fail(`${label} must be ${bound.exclusive ? "greater than" : "at least"} ${bound.min}, got "${value}"`);
96
+ }
75
97
  return n;
76
98
  }
77
99
  function anthropicKeyFor(opts) {
@@ -100,12 +122,14 @@ program
100
122
  .option("--allow-increase <pct>", "with --diff: exit 1 if the increase exceeds pct percent")
101
123
  .option("--badge [file]", "write an SVG badge + shields endpoint JSON")
102
124
  .option("--anthropic [model]", "also measure exact Claude tokens via the count_tokens API (needs ANTHROPIC_API_KEY)")
125
+ .option("--window <tokens>", "context window the share is reported against", String(DEFAULT_CONTEXT_WINDOW))
103
126
  .option("--no-color", "disable colors")
104
127
  .action(async (targetArg, opts) => {
105
128
  colorSetup(opts);
129
+ windowSetup(opts);
106
130
  const timeoutMs = seconds(opts.timeout) * 1000;
107
- const maxTokens = parseNumberOpt(opts.maxTokens, "--max-tokens");
108
- const allowIncreasePct = parseNumberOpt(opts.allowIncrease, "--allow-increase");
131
+ const maxTokens = parseNumberOpt(opts.maxTokens, "--max-tokens", { min: 0, exclusive: true });
132
+ const allowIncreasePct = parseNumberOpt(opts.allowIncrease, "--allow-increase", { min: 0 });
109
133
  const anthropic = anthropicKeyFor(opts);
110
134
  const targets = [];
111
135
  if (opts.client) {
@@ -114,6 +138,29 @@ program
114
138
  fail(`no MCP servers found for client "${opts.client}" (checked: ${[...conf.sources, ...conf.missing].join(", ")})`);
115
139
  }
116
140
  console.error(pc.dim(`config: ${conf.sources.join(", ")}`));
141
+ // Say what is about to be EXECUTED, not just which file it came from.
142
+ //
143
+ // --client reads .mcp.json, .cursor/mcp.json and .vscode/mcp.json from
144
+ // the CURRENT DIRECTORY as well as the user config, and then spawns
145
+ // every command in them. Cloning an untrusted repo and running the audit
146
+ // tool inside it was therefore arbitrary code execution, and the only
147
+ // thing printed was the friendly config key, never the command. Cursor
148
+ // and VS Code gate project-scoped MCP config behind workspace trust;
149
+ // this printed nothing at all.
150
+ //
151
+ // Listing them is the minimum. A stdio entry names a real command that
152
+ // is about to run on this machine.
153
+ for (const e of conf.entries) {
154
+ const t = e.target;
155
+ const shown = t.kind === "stdio"
156
+ ? [t.command, ...t.args].join(" ") + (t.env && Object.keys(t.env).length ? ` (+${Object.keys(t.env).length} env)` : "")
157
+ : t.url;
158
+ console.error(pc.dim(` ${e.name}: ${shown}`));
159
+ }
160
+ if (conf.fromCwd.length) {
161
+ console.error(pc.yellow(` ! ${conf.fromCwd.length} of these came from the current directory (${conf.fromCwd.join(", ")}), not your user config.\n` +
162
+ ` Auditing a stdio server means executing it. Only continue in a repository you trust.`));
163
+ }
117
164
  targets.push(...conf.entries.map((e) => e.target));
118
165
  }
119
166
  if (targetArg) {
@@ -186,7 +233,7 @@ program
186
233
  .filter((r) => r.kind === "mcp")
187
234
  .reduce((s, r) => s + r.totals.claudeStyle, 0);
188
235
  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)`));
236
+ console.log(pc.bold(`\ncombined MCP context cost (Claude-style, o200k est.): ${mcpTotals.toLocaleString("en-US")} tokens (${formatWindowShare(mcpTotals)})`));
190
237
  }
191
238
  }
192
239
  }
@@ -198,8 +245,14 @@ program
198
245
  fail("--diff works on a single MCP server result");
199
246
  const baselineRaw = JSON.parse(fs.readFileSync(opts.diff, "utf8"));
200
247
  const baseline = ("data" in baselineRaw && baselineRaw.data ? baselineRaw.data : baselineRaw);
201
- if (!baseline?.totals)
202
- fail(`"${opts.diff}" is not a weigh baseline (write one with --out)`);
248
+ // A skill weigh baseline also carries a `totals` object, so a bare
249
+ // truthiness check passed it through and diffServerWeigh then died on the
250
+ // missing tool array with a raw TypeError. A server baseline is the one
251
+ // whose totals hold the three serializations; require the Claude-style
252
+ // total specifically so a skill baseline is rejected with a clear message.
253
+ if (!baseline?.totals || typeof baseline.totals.claudeStyle !== "number") {
254
+ fail(`"${opts.diff}" is not an MCP server weigh baseline; write one with: efaimo weigh <server> --out ${opts.diff}`);
255
+ }
203
256
  const d = diffServerWeigh(baseline, single);
204
257
  console.log("");
205
258
  console.log(opts.md ? renderDiffMarkdown(d) : renderDiffPretty(d, { maxTokens, allowIncreasePct }));
@@ -226,6 +279,7 @@ program
226
279
  .option("--repo <path>", "additionally scan this source repo for deprecated API usage")
227
280
  .option("--no-probe", "skip live readiness probes (bare request, server/discover, ordering)")
228
281
  .option("--strict", "exit 1 on warnings too")
282
+ .option("--strict-readiness", "exit 1 if the 2026-07-28 migration diff is not clean (never changes the grade)")
229
283
  .option("--conformance", "after the audit, run the official MCP conformance suite (http targets)")
230
284
  .option("--header <header>", 'HTTP header "Key: Value" (repeatable)', collectPairs(":"))
231
285
  .option("--env <pair>", "KEY=VALUE for stdio servers (repeatable)", collectPairs("="))
@@ -234,9 +288,11 @@ program
234
288
  .option("--md", "print Markdown")
235
289
  .option("--badge [file]", "write a grade badge SVG + shields endpoint JSON")
236
290
  .option("--anthropic [model]", "use exact Claude token counts where relevant")
291
+ .option("--window <tokens>", "context window the share is reported against", String(DEFAULT_CONTEXT_WINDOW))
237
292
  .option("--no-color", "disable colors")
238
293
  .action(async (targetArg, opts) => {
239
294
  colorSetup(opts);
295
+ windowSetup(opts);
240
296
  if (!targetArg)
241
297
  fail("nothing to check: pass a target (see --help)");
242
298
  if (opts.mcp && opts.skill)
@@ -305,10 +361,31 @@ program
305
361
  console.error(pc.yellow("note: the official conformance suite drives http targets (--url); skipping for this target"));
306
362
  }
307
363
  else {
308
- console.error(pc.dim("\nrunning official MCP conformance suite (npx @modelcontextprotocol/conformance)...\n"));
364
+ // The `latest` line of the conformance suite (0.1.16) predates the
365
+ // 2026-07-28 revision, so it silently tests the old protocol. Those
366
+ // scenarios ship on the `alpha` line only; --spec-version scopes them
367
+ // to the revision efaimo is about.
368
+ //
369
+ // Pinned to an exact version, because a dist-tag is not a pin. `alpha`
370
+ // moved from 0.2.0-alpha.9 to 0.2.0-alpha.10 on 2026-07-27, and we went
371
+ // on reporting "the official conformance suite" for a build nobody here
372
+ // had run: the same "we do not know what we ran" bug this block was
373
+ // written to fix, through the other door. Bumping it means re-reading
374
+ // `server --help` first; the two flags below were confirmed against
375
+ // 0.2.0-alpha.10.
376
+ const args = [
377
+ "-y",
378
+ "@modelcontextprotocol/conformance@0.2.0-alpha.10",
379
+ "server",
380
+ "--url",
381
+ target.url,
382
+ "--spec-version",
383
+ SPEC_VERSION,
384
+ ];
385
+ console.error(pc.dim(`\nrunning official MCP conformance suite:\n npx ${args.slice(1).join(" ")}\n`));
309
386
  // cross-spawn resolves npx(.cmd) without a shell, so target.url is passed
310
387
  // as a literal argument (never interpreted by cmd.exe).
311
- const r = crossSpawn.sync("npx", ["-y", "@modelcontextprotocol/conformance", "server", "--url", target.url], {
388
+ const r = crossSpawn.sync("npx", args, {
312
389
  stdio: "inherit",
313
390
  });
314
391
  if (r.status !== 0)
@@ -318,6 +395,15 @@ program
318
395
  if (report.counts.error > 0 || (opts.strict && report.counts.warn > 0)) {
319
396
  process.exitCode = 1;
320
397
  }
398
+ // Readiness stays out of the grade, the badge and the default exit code
399
+ // (ADR-014, reaffirmed in ADR-027). But a team that HAS migrated had no way
400
+ // to stay migrated: --strict covers quality only, so nothing in CI could
401
+ // catch a regression back onto the legacy handshake. This is the opt-in for
402
+ // exactly that, and it moves the exit code only. Nobody's grade changes,
403
+ // no published number moves, and the operator decides rather than us.
404
+ if (opts.strictReadiness && (report.readiness?.findings.length ?? 0) > 0) {
405
+ process.exitCode = 1;
406
+ }
321
407
  });
322
408
  program
323
409
  .command("test")