efaimo 0.1.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 +47 -0
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +281 -0
- package/SKILL.md +57 -0
- package/dist/check/check.d.ts +31 -0
- package/dist/check/check.js +77 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +372 -0
- package/dist/clients/introspect.d.ts +8 -0
- package/dist/clients/introspect.js +198 -0
- package/dist/clients/rawprobe.d.ts +69 -0
- package/dist/clients/rawprobe.js +597 -0
- package/dist/clients/stateless.d.ts +7 -0
- package/dist/clients/stateless.js +121 -0
- package/dist/core/engine.d.ts +6 -0
- package/dist/core/engine.js +17 -0
- package/dist/core/grade.d.ts +8 -0
- package/dist/core/grade.js +19 -0
- package/dist/core/types.d.ts +243 -0
- package/dist/core/types.js +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +26 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +88 -0
- package/dist/reporters/badge.d.ts +12 -0
- package/dist/reporters/badge.js +37 -0
- package/dist/reporters/json.d.ts +1 -0
- package/dist/reporters/json.js +10 -0
- package/dist/reporters/markdown.d.ts +7 -0
- package/dist/reporters/markdown.js +121 -0
- package/dist/reporters/pretty.d.ts +15 -0
- package/dist/reporters/pretty.js +221 -0
- package/dist/rules/injection.d.ts +20 -0
- package/dist/rules/injection.js +106 -0
- package/dist/rules/mcp/index.d.ts +4 -0
- package/dist/rules/mcp/index.js +643 -0
- package/dist/rules/skill/index.d.ts +2 -0
- package/dist/rules/skill/index.js +279 -0
- package/dist/skills/parse.d.ts +4 -0
- package/dist/skills/parse.js +113 -0
- package/dist/targets/clientConfigs.d.ts +17 -0
- package/dist/targets/clientConfigs.js +110 -0
- package/dist/targets/repoScan.d.ts +2 -0
- package/dist/targets/repoScan.js +0 -0
- package/dist/targets/resolve.d.ts +28 -0
- package/dist/targets/resolve.js +48 -0
- package/dist/testing/anthropicRunner.d.ts +6 -0
- package/dist/testing/anthropicRunner.js +33 -0
- package/dist/testing/harness.d.ts +45 -0
- package/dist/testing/harness.js +96 -0
- package/dist/testing/openaiRunner.d.ts +12 -0
- package/dist/testing/openaiRunner.js +40 -0
- package/dist/util/childEnv.d.ts +1 -0
- package/dist/util/childEnv.js +43 -0
- package/dist/util/dotenv.d.ts +12 -0
- package/dist/util/dotenv.js +53 -0
- package/dist/util/misc.d.ts +15 -0
- package/dist/util/misc.js +121 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/dist/weigh/anthropicExact.d.ts +11 -0
- package/dist/weigh/anthropicExact.js +46 -0
- package/dist/weigh/diff.d.ts +14 -0
- package/dist/weigh/diff.js +19 -0
- package/dist/weigh/serializers.d.ts +21 -0
- package/dist/weigh/serializers.js +44 -0
- package/dist/weigh/tokens.d.ts +12 -0
- package/dist/weigh/tokens.js +23 -0
- package/dist/weigh/weigh.d.ts +6 -0
- package/dist/weigh/weigh.js +104 -0
- package/docs/INTEGRATIONS.md +119 -0
- package/docs/METHODOLOGY.md +83 -0
- package/docs/RULES.md +76 -0
- package/package.json +77 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { countClaudeToolTokens } from "./anthropicExact.js";
|
|
2
|
+
import { serializeClaudeStyle, serializeOpenAITools, serializeRawJson, serializeSingle } from "./serializers.js";
|
|
3
|
+
import { loadTokenizer } from "./tokens.js";
|
|
4
|
+
import { isBinaryPath, readTextSafe } from "../util/misc.js";
|
|
5
|
+
export async function weighServer(intro, opts = {}) {
|
|
6
|
+
const count = await loadTokenizer();
|
|
7
|
+
const perTool = [];
|
|
8
|
+
for (const tool of intro.tools) {
|
|
9
|
+
const s = serializeSingle(tool);
|
|
10
|
+
perTool.push({
|
|
11
|
+
name: tool.name,
|
|
12
|
+
tokens: {
|
|
13
|
+
rawJson: count(s.rawJson),
|
|
14
|
+
claudeStyle: count(s.claudeStyle),
|
|
15
|
+
openaiTools: count(s.openaiTools),
|
|
16
|
+
},
|
|
17
|
+
descriptionTokens: count(tool.description ?? ""),
|
|
18
|
+
schemaTokens: count(JSON.stringify(tool.inputSchema ?? {})),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
perTool.sort((a, b) => b.tokens.claudeStyle - a.tokens.claudeStyle);
|
|
22
|
+
const totals = {
|
|
23
|
+
rawJson: count(serializeRawJson(intro.tools)),
|
|
24
|
+
claudeStyle: count(serializeClaudeStyle(intro.tools)),
|
|
25
|
+
openaiTools: count(serializeOpenAITools(intro.tools)),
|
|
26
|
+
};
|
|
27
|
+
// Per-tool rows count each tool's bare definition line; the total counts the
|
|
28
|
+
// <functions> block around them. Report the wrapper explicitly so the rows
|
|
29
|
+
// reconcile with the total (clamped: tokenizer merges across boundaries can
|
|
30
|
+
// in principle make the residual negative on other serializations).
|
|
31
|
+
const framingTokens = Math.max(0, totals.claudeStyle - perTool.reduce((s, t) => s + t.tokens.claudeStyle, 0));
|
|
32
|
+
const notes = [
|
|
33
|
+
"token counts are estimates using the o200k_base tokenizer; hosts add fixed framing text on top (see docs/METHODOLOGY.md)",
|
|
34
|
+
];
|
|
35
|
+
let anthropicExactTotal;
|
|
36
|
+
if (opts.anthropicApiKey && intro.tools.length > 0) {
|
|
37
|
+
anthropicExactTotal = await countClaudeToolTokens(intro.tools, {
|
|
38
|
+
apiKey: opts.anthropicApiKey,
|
|
39
|
+
model: opts.anthropicModel,
|
|
40
|
+
});
|
|
41
|
+
if (anthropicExactTotal !== undefined) {
|
|
42
|
+
notes.push("anthropic-exact measured via /v1/messages/count_tokens (tools delta)");
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
notes.push("anthropic-exact measurement failed; showing o200k estimates only");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
kind: "mcp",
|
|
50
|
+
label: intro.targetLabel,
|
|
51
|
+
toolCount: intro.tools.length,
|
|
52
|
+
resourceCount: intro.resources.length,
|
|
53
|
+
promptCount: intro.prompts.length,
|
|
54
|
+
perTool,
|
|
55
|
+
totals,
|
|
56
|
+
framingTokens,
|
|
57
|
+
instructionsTokens: count(intro.instructions ?? ""),
|
|
58
|
+
anthropicExactTotal,
|
|
59
|
+
notes,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export async function weighSkills(set) {
|
|
63
|
+
const count = await loadTokenizer();
|
|
64
|
+
const perSkill = [];
|
|
65
|
+
for (const skill of set.skills) {
|
|
66
|
+
const name = skill.name ?? "(unnamed)";
|
|
67
|
+
const metadataTokens = count(`${name}: ${skill.description ?? ""}`);
|
|
68
|
+
const bodyTokens = count(skill.body);
|
|
69
|
+
let refFileTokens = 0;
|
|
70
|
+
let refFileCount = 0;
|
|
71
|
+
for (const ref of skill.referencedPaths) {
|
|
72
|
+
if (!ref.exists || isBinaryPath(ref.resolved))
|
|
73
|
+
continue;
|
|
74
|
+
const text = readTextSafe(ref.resolved);
|
|
75
|
+
if (text === undefined)
|
|
76
|
+
continue;
|
|
77
|
+
refFileCount++;
|
|
78
|
+
refFileTokens += count(text);
|
|
79
|
+
}
|
|
80
|
+
perSkill.push({
|
|
81
|
+
name,
|
|
82
|
+
dir: skill.dir,
|
|
83
|
+
metadataTokens,
|
|
84
|
+
bodyTokens,
|
|
85
|
+
bodyLines: skill.bodyLines,
|
|
86
|
+
refFileCount,
|
|
87
|
+
refFileTokens,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
kind: "skill",
|
|
92
|
+
label: set.root,
|
|
93
|
+
perSkill,
|
|
94
|
+
totals: {
|
|
95
|
+
metadata: perSkill.reduce((s, x) => s + x.metadataTokens, 0),
|
|
96
|
+
body: perSkill.reduce((s, x) => s + x.bodyTokens, 0),
|
|
97
|
+
refFiles: perSkill.reduce((s, x) => s + x.refFileTokens, 0),
|
|
98
|
+
},
|
|
99
|
+
notes: [
|
|
100
|
+
"metadata loads at session start for every installed skill; body loads on trigger; referenced files load on demand",
|
|
101
|
+
"token counts are o200k_base estimates (see docs/METHODOLOGY.md)",
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Integrating efaimo
|
|
2
|
+
|
|
3
|
+
efaimo is a plain CLI, so it drops into anything that can run a command. This is the
|
|
4
|
+
practical guide, from local dev to CI to calling it from an agent.
|
|
5
|
+
|
|
6
|
+
## Local: pre-commit
|
|
7
|
+
|
|
8
|
+
Stop a broken skill before it lands. `.pre-commit-config.yaml`:
|
|
9
|
+
|
|
10
|
+
```yaml
|
|
11
|
+
repos:
|
|
12
|
+
- repo: local
|
|
13
|
+
hooks:
|
|
14
|
+
- id: efaimo-skills
|
|
15
|
+
name: efaimo check --skill
|
|
16
|
+
entry: npx efaimo check --skill ./skills --strict
|
|
17
|
+
language: system
|
|
18
|
+
pass_filenames: false
|
|
19
|
+
files: ^skills/.*SKILL\.md$
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## CI: GitHub Actions
|
|
23
|
+
|
|
24
|
+
Fail the build on skill regressions or a context-budget blowout.
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
- uses: efaimo-ai/efaimo@v0
|
|
28
|
+
with:
|
|
29
|
+
command: check --skill ./skills --strict
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Ratchet MCP tool-definition cost with the budget gate (commit `base.json` once with
|
|
33
|
+
`npx efaimo weigh "npx -y your-server" --out base.json`):
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
- run: npx efaimo weigh "npx -y your-server" --diff base.json --allow-increase 10
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Post the grade on the PR with `--md` (pipe into any comment action):
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
- run: npx efaimo check --skill ./skills --md >> "$GITHUB_STEP_SUMMARY"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## CI: GitLab
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
efaimo:
|
|
49
|
+
image: node:24
|
|
50
|
+
script:
|
|
51
|
+
- npx efaimo check --skill ./skills --strict
|
|
52
|
+
- npx efaimo weigh "npx -y your-server" --diff base.json --allow-increase 10
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Audit what an editor already loads
|
|
56
|
+
|
|
57
|
+
efaimo reads the MCP config of the tools you already use and weighs everything they
|
|
58
|
+
load, so you can see the context tax before you open a chat:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx efaimo weigh --client claude-code # also: claude-desktop, cursor, vscode
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Use it as an Agent Skill
|
|
65
|
+
|
|
66
|
+
efaimo ships a `SKILL.md`, so an agent that supports Agent Skills can install it and
|
|
67
|
+
run the checks itself:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx skills add efaimo-ai/efaimo
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The skill is a thin trigger; the `npx efaimo` binary does the work. It pairs with
|
|
74
|
+
`skills validate` (metadata) as the deeper tier (quality, cost, collisions, and the
|
|
75
|
+
`test` A/B harness): `npx skills validate ./skill && npx efaimo check --skill ./skill`.
|
|
76
|
+
|
|
77
|
+
## Cursor / VS Code / Windsurf
|
|
78
|
+
|
|
79
|
+
Any editor that runs tasks can run efaimo. Add a task or a keybinding to
|
|
80
|
+
`npx efaimo check --skill ${workspaceFolder}/skills`, and use `weigh --client cursor`
|
|
81
|
+
(or `vscode`) to audit the MCP servers that editor loads.
|
|
82
|
+
|
|
83
|
+
## Calling efaimo from an agent (efaimo mcp)
|
|
84
|
+
|
|
85
|
+
`efaimo mcp` runs efaimo as a small, read-only MCP server over stdio. It exposes two
|
|
86
|
+
tools, `efaimo_check_skill` and `efaimo_weigh_skill`, so an agent can lint or weigh a
|
|
87
|
+
skill mid-session, before it commits it to context. The tools read files only: they
|
|
88
|
+
spawn no process, open no socket, and `test` (which spends tokens) is not exposed, so
|
|
89
|
+
the server is safe to call unattended.
|
|
90
|
+
|
|
91
|
+
Add it to a client config (Claude Desktop, Claude Code, Cursor, ...):
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"mcpServers": {
|
|
96
|
+
"efaimo": { "command": "npx", "args": ["-y", "efaimo", "mcp"] }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Both tools take a `path` to a SKILL.md or a folder of skills and return Markdown.
|
|
102
|
+
|
|
103
|
+
efaimo holds its own server to the same bar. `efaimo check --mcp "npx -y efaimo mcp"`
|
|
104
|
+
grades it a quality A with a three-item 2026-07-28 migration diff: a fully
|
|
105
|
+
stateless server wants the 2.x MCP SDK for `server/discover`, cache fields, and
|
|
106
|
+
`resultType`. That SDK upgrade is the roadmap item; the server is useful today on
|
|
107
|
+
the current SDK.
|
|
108
|
+
|
|
109
|
+
## Programmatic use
|
|
110
|
+
|
|
111
|
+
Every capability is also a typed function:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { weighServer, introspectServer, checkSkillSet } from "efaimo";
|
|
115
|
+
|
|
116
|
+
const intro = await introspectServer({ kind: "stdio", command: "npx", args: ["-y", "my-server"], label: "my-server" });
|
|
117
|
+
const cost = await weighServer(intro);
|
|
118
|
+
console.log(cost.totals.claudeStyle);
|
|
119
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# How efaimo counts tokens
|
|
2
|
+
|
|
3
|
+
efaimo's numbers are meant to be **reproducible and honest**, not authoritative
|
|
4
|
+
down to the token. This document is the whole method so you can check it.
|
|
5
|
+
|
|
6
|
+
## The tokenizer
|
|
7
|
+
|
|
8
|
+
Estimates use **OpenAI's `o200k_base`** tokenizer (via `gpt-tokenizer`), because
|
|
9
|
+
it is public, deterministic, runs offline, and tracks the vocabularies of current
|
|
10
|
+
frontier models closely on English and JSON. Every estimated number in efaimo's
|
|
11
|
+
output is labeled `o200k` / `estimated`.
|
|
12
|
+
|
|
13
|
+
Different hosts use different tokenizers, and no vendor tokenizer but OpenAI's is
|
|
14
|
+
public, so the estimate carries an **unmeasured, consistent bias** against any
|
|
15
|
+
specific host. That makes it reliable for **comparison** (bias cancels in a
|
|
16
|
+
`--diff`, so budget gates are trustworthy) but only **approximate in absolute
|
|
17
|
+
terms**: the absolute thresholds (E127, E128) and the "percent of window" figure
|
|
18
|
+
can be off by more than a few percent, and should be read as order-of-magnitude,
|
|
19
|
+
not billing. When you need Claude-exact numbers, pass `--anthropic` (see below).
|
|
20
|
+
A calibration study across real schemas is future work; until then the default is
|
|
21
|
+
labeled an estimate precisely because its absolute accuracy is unverified.
|
|
22
|
+
|
|
23
|
+
## What we serialize
|
|
24
|
+
|
|
25
|
+
A tool definition's real cost depends on how a host serializes it into the model's
|
|
26
|
+
context. efaimo reports three serializations so you can see the spread:
|
|
27
|
+
|
|
28
|
+
- **raw JSON** - the `tools/list` payload, minified. A neutral lower bound.
|
|
29
|
+
- **Claude-style** - each tool as `{"description","name","parameters"}` wrapped in
|
|
30
|
+
a `<functions>` block, mirroring how Claude-family harnesses present tools. This
|
|
31
|
+
is efaimo's **primary metric** (the one used for badges, diffs, and budgets)
|
|
32
|
+
because it is the closest public approximation of a real system-prompt injection.
|
|
33
|
+
- **OpenAI tools** - the Chat Completions `tools` array shape.
|
|
34
|
+
|
|
35
|
+
Hosts wrap these in a small amount of fixed framing text (headers, instructions on
|
|
36
|
+
how to call tools). That framing is **per-host constant** and independent of your
|
|
37
|
+
server, so efaimo excludes it: it would add the same number to every server and
|
|
38
|
+
wash out of any comparison or diff.
|
|
39
|
+
|
|
40
|
+
### Per-tool numbers vs the total
|
|
41
|
+
|
|
42
|
+
Per-tool numbers count each tool's **bare definition line**; the Claude-style
|
|
43
|
+
total counts the whole `<functions>` block, including the `<function>` tags and
|
|
44
|
+
newlines around every line. That wrapper is reported as its own **block framing**
|
|
45
|
+
line item, so per-tool numbers plus framing equal the Claude-style total. The
|
|
46
|
+
per-tool threshold rule (E127) uses the bare line; the total rule (E128) uses the
|
|
47
|
+
wrapped total. For the other two serializations no wrapper exists, and summing
|
|
48
|
+
per-tool counts may differ from the total by a few tokens, because a tokenizer
|
|
49
|
+
can merge characters across element boundaries when tools are concatenated.
|
|
50
|
+
|
|
51
|
+
## Skills
|
|
52
|
+
|
|
53
|
+
For Agent Skills efaimo reports the three progressive-disclosure levels defined by
|
|
54
|
+
the spec separately, because they load at different times:
|
|
55
|
+
|
|
56
|
+
- **metadata** (`name` + `description`) - loaded at session start for *every*
|
|
57
|
+
installed skill. This is the always-on cost; the spec targets ~100 tokens.
|
|
58
|
+
- **body** - the rest of `SKILL.md`, loaded when the skill triggers.
|
|
59
|
+
- **referenced files** - `references/`, `scripts/`, `assets/` linked from the body,
|
|
60
|
+
loaded only on demand.
|
|
61
|
+
|
|
62
|
+
## Claude-exact mode
|
|
63
|
+
|
|
64
|
+
With `--anthropic` (and `ANTHROPIC_API_KEY` set), efaimo also calls Anthropic's
|
|
65
|
+
`POST /v1/messages/count_tokens`, which accepts a `tools` array, and measures the
|
|
66
|
+
**delta** between a request with your tools and the same request without them. That
|
|
67
|
+
delta is the exact number of tokens your tool definitions add for the chosen Claude
|
|
68
|
+
model. This requires network and a key, so it is opt-in; the o200k estimate is
|
|
69
|
+
always shown alongside.
|
|
70
|
+
|
|
71
|
+
## Determinism
|
|
72
|
+
|
|
73
|
+
Given the same server output, efaimo produces the same numbers every run: the
|
|
74
|
+
tokenizer is deterministic and no sampling is involved. Server responses can change
|
|
75
|
+
between runs (a server may reorder or add tools); efaimo's `check` flags
|
|
76
|
+
nondeterministic `tools/list` ordering (E112) precisely because it affects both
|
|
77
|
+
prompt-cache hits and reproducibility.
|
|
78
|
+
|
|
79
|
+
## What this is not
|
|
80
|
+
|
|
81
|
+
efaimo estimates the **cost of tool/skill definitions sitting in context**. It does
|
|
82
|
+
not measure per-call argument/result tokens, host framing, or model-side reasoning.
|
|
83
|
+
For end-to-end token accounting, instrument your agent run directly.
|
package/docs/RULES.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# efaimo rules
|
|
2
|
+
|
|
3
|
+
Every finding carries a stable rule id so you can suppress, track, or link it.
|
|
4
|
+
Severities: **error** (will bite you), **warn** (should fix), **info** (worth knowing).
|
|
5
|
+
|
|
6
|
+
Grade starts at 100; each error costs 15, each warning 5, each info 1.
|
|
7
|
+
`A >= 90, B >= 80, C >= 70, D >= 60, else F`.
|
|
8
|
+
|
|
9
|
+
MCP checks split into **2026-07-28 readiness** (E101-E118) and **quality**
|
|
10
|
+
(E121-E130); skill checks are S1xx. **Only quality and skill findings are
|
|
11
|
+
graded.** Readiness findings are reported as a separate, ungraded **migration
|
|
12
|
+
diff**: the stateless revision they target is a locked Release Candidate
|
|
13
|
+
(2026-05-21) that does not finalize until 2026-07-28, and not yet migrating to
|
|
14
|
+
an unreleased spec is a to-do, not a defect. Readiness severities still order
|
|
15
|
+
the diff by urgency, but the diff never affects the grade, the badge, or the
|
|
16
|
+
exit code (including `--strict`). Once the spec ratifies, expect readiness to
|
|
17
|
+
start counting in a minor release. Source-pattern rules are heuristics and say
|
|
18
|
+
so in their detail line.
|
|
19
|
+
|
|
20
|
+
## MCP readiness (E101-E118)
|
|
21
|
+
|
|
22
|
+
| id | sev | what it catches | source of truth |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| E101 | warn | depends on a pre-`2026-07-28` SDK line (TS `@modelcontextprotocol/sdk` 1.x, Python `mcp` 1.x) instead of the 2.x stateless line | changelog / SDK releases |
|
|
25
|
+
| E102 | warn | source uses **Sampling** (`sampling/createMessage`), deprecated (SEP-2577) | draft changelog |
|
|
26
|
+
| E103 | warn | source uses **Roots** (`roots/list`), deprecated (SEP-2577) | draft changelog |
|
|
27
|
+
| E104 | warn | server declares the **logging** capability or source uses MCP Logging, deprecated (SEP-2577); `logging/setLevel` is removed | draft changelog |
|
|
28
|
+
| E105 | warn | the server does not answer a bare stateless `tools/list` (timeout, crash, or a not-initialized error), so it requires the removed `initialize` handshake. A server that answers (even with an error) is judged by E106/E107/E118 instead | SEP-2567 / stateless core |
|
|
29
|
+
| E106 | warn | `server/discover` is not implemented (MUST in the RC, SEP-2575) | draft changelog |
|
|
30
|
+
| E107 | info | results omit the required `resultType` field (`"complete"` \| `"input_required"`) | draft changelog |
|
|
31
|
+
| E108 | info | source relies on removed SSE resumability (`Last-Event-ID`) | draft changelog |
|
|
32
|
+
| E109 | info | auth advertises deprecated DCR (RFC 7591) with no detected CIMD support | PR #2858 / CIMD |
|
|
33
|
+
| E110 | warn | source uses legacy elicitation (`elicitation/create`); replaced by MRTR `input_required` results (SEP-2322) | draft changelog |
|
|
34
|
+
| E111 | info | in-process session-state patterns; statelessness expects server-minted handles in tool args | stateless core |
|
|
35
|
+
| E112 | warn | `tools/list` order is nondeterministic across connections (hurts prompt-cache hits) | draft changelog |
|
|
36
|
+
| E113 | info | no Server Card found at `/.well-known/mcp` (discovery metadata; heuristic path) | roadmap / Server Card WG |
|
|
37
|
+
| E114 | info | source uses the removed `ping` utility | draft changelog |
|
|
38
|
+
| E115 | info | source uses `resources/subscribe`, replaced by `subscriptions/listen` | draft changelog |
|
|
39
|
+
| E116 | warn | server prints non-JSON noise on stdout (breaks stdio framing) | transport basics |
|
|
40
|
+
| E117 | warn | only the deprecated HTTP+SSE transport worked; Streamable HTTP failed | draft changelog |
|
|
41
|
+
| E118 | warn | `tools/list` result omits the now-required `ttlMs`/`cacheScope` cache fields (SEP-2549, CacheableResult; also required on prompts/list, resources/list, resources/read, resources/templates/list, server/discover) | draft changelog |
|
|
42
|
+
|
|
43
|
+
## MCP quality (E121-E130)
|
|
44
|
+
|
|
45
|
+
| id | sev | what it catches |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| E121 | error/warn | tool description missing, a placeholder, or under ~20 chars |
|
|
48
|
+
| E122 | warn | description misses 3+ of 4 quality axes (length, when-to-use, params documented, mentions result), mirrors Glama's tool-definition-quality dimensions, computed locally |
|
|
49
|
+
| E123 | warn/info | no tools declare annotations; a destructive-looking tool lacks `destructiveHint` |
|
|
50
|
+
| E124 | warn | many undocumented parameters, or an oversized `enum` inflating every prompt |
|
|
51
|
+
| E125 | warn | tool count high enough (30+, 60+) to degrade routing and inflate context |
|
|
52
|
+
| E126 | warn/info | tool names mix conventions or exceed 64 chars |
|
|
53
|
+
| E127 | warn | a single tool definition costs 800+ tokens (needs `weigh` data) |
|
|
54
|
+
| E128 | warn | total tool-definition tax over ~10k / ~25k tokens, estimated (needs `weigh` data); hard CI failure is left to the `--max-tokens`/`--diff` gate |
|
|
55
|
+
| E130 | info | instruction-injection patterns in a tool description or server instructions (shallow heuristic, never a security verdict; use a dedicated scanner for depth) |
|
|
56
|
+
|
|
57
|
+
## Skills (S101-S106)
|
|
58
|
+
|
|
59
|
+
| id | sev | what it catches | spec |
|
|
60
|
+
|---|---|---|---|
|
|
61
|
+
| S101 | error/warn/info | frontmatter invalid: missing `name`/`description`, `name` not matching the directory, over length limits, `metadata` not a string map, non-standard fields | agentskills.io |
|
|
62
|
+
| S102 | warn/info | description too thin or never says when to use the skill | agentskills.io |
|
|
63
|
+
| S103 | error/warn | duplicate skill names, or two skills with heavily overlapping descriptions (trigger collision) | internal |
|
|
64
|
+
| S104 | warn/info | metadata over ~100 tokens, body over ~5k tokens or 500 lines, or long body with no progressive disclosure | agentskills.io |
|
|
65
|
+
| S105 | info | instruction-injection patterns in the skill body or description (shallow heuristic, never a security verdict) | internal |
|
|
66
|
+
| S106 | error/warn/info | referenced file missing, escaping the skill dir (`..`), or nested more than one level deep | agentskills.io |
|
|
67
|
+
|
|
68
|
+
## Notes on heuristics
|
|
69
|
+
|
|
70
|
+
Rules that scan **source code** (E101-E104, E108, E110, E111, E114, E115) are
|
|
71
|
+
pattern matches over your repo and can produce false positives; each finding says
|
|
72
|
+
so and points at the file:line so you can confirm. Rules that scan **a live
|
|
73
|
+
server** (E105-E107, E112, E116, E117) and the token rules (E127, E128) reflect
|
|
74
|
+
what the server actually did. `efaimo` is a linter, not a security scanner:
|
|
75
|
+
E130/S105 are surface heuristics, for real supply-chain security use a dedicated
|
|
76
|
+
tool such as Snyk agent-scan or the Cisco MCP Scanner.
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "efaimo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Audit what your agent loads: quality and context cost for MCP servers and Agent Skills",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"agent-skills",
|
|
9
|
+
"skills",
|
|
10
|
+
"audit",
|
|
11
|
+
"lint",
|
|
12
|
+
"tokens",
|
|
13
|
+
"context-window",
|
|
14
|
+
"claude",
|
|
15
|
+
"ai-agents",
|
|
16
|
+
"cli"
|
|
17
|
+
],
|
|
18
|
+
"license": "Apache-2.0",
|
|
19
|
+
"author": "efaimo ai <hello@efaimo.ai> (https://efaimo.ai)",
|
|
20
|
+
"homepage": "https://github.com/efaimo-ai/efaimo#readme",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/efaimo-ai/efaimo.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": "https://github.com/efaimo-ai/efaimo/issues",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
},
|
|
31
|
+
"bin": {
|
|
32
|
+
"efaimo": "dist/cli.js"
|
|
33
|
+
},
|
|
34
|
+
"main": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"exports": {
|
|
37
|
+
".": {
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"import": "./dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"provenance": true,
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"SKILL.md",
|
|
50
|
+
"NOTICE",
|
|
51
|
+
"CHANGELOG.md",
|
|
52
|
+
"docs/METHODOLOGY.md",
|
|
53
|
+
"docs/RULES.md",
|
|
54
|
+
"docs/INTEGRATIONS.md"
|
|
55
|
+
],
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsc -p tsconfig.build.json",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"test:watch": "vitest",
|
|
61
|
+
"prepublishOnly": "npm run typecheck && npm run build && npm run test"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
65
|
+
"commander": "^15.0.0",
|
|
66
|
+
"cross-spawn": "^7.0.6",
|
|
67
|
+
"gpt-tokenizer": "^3.4.0",
|
|
68
|
+
"picocolors": "^1.1.1",
|
|
69
|
+
"yaml": "^2.9.0"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@types/cross-spawn": "^6.0.6",
|
|
73
|
+
"@types/node": "^26.1.1",
|
|
74
|
+
"typescript": "^7.0.2",
|
|
75
|
+
"vitest": "^4.1.10"
|
|
76
|
+
}
|
|
77
|
+
}
|