efaimo 0.4.0 → 0.5.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 CHANGED
@@ -4,6 +4,53 @@ 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
+ ## [0.5.0] - 2026-09-04
8
+
9
+ ### Changed
10
+
11
+ - **`efaimo mcp` is built on the 2.x SDK line and now satisfies the
12
+ specification this tool audits other servers against.** The 1.x line speaks
13
+ 2025-11-25; it cannot answer `server/discover` and cannot carry the SEP-2549
14
+ cache fields, so the one MCP server this project ships failed two MUST-level
15
+ items of the 2026-07-28 revision, and `check --mcp` said so about it on every
16
+ run. The skill this project publishes about that migration opens with "upgrade
17
+ the SDK first". This is that.
18
+
19
+ Measured against the built server, before and after, with the same command:
20
+
21
+ ```
22
+ before 2026-07-28 readiness 3 items to migrate E106, E118, E107
23
+ after 2026-07-28 readiness clean, nothing to migrate
24
+ ```
25
+
26
+ Read off the wire rather than from our own tool as well: a modern opening now
27
+ answers `server/discover` with `supportedVersions`, `capabilities`,
28
+ `resultType: "complete"`, `ttlMs` and `cacheScope`, and `tools/list` carries
29
+ the same three fields. A 2025-era client that opens with `initialize` is still
30
+ served, unchanged, because `serveStdio` pins the era per connection rather
31
+ than making the server pick one.
32
+
33
+ - **The client half moved with it**, from `@modelcontextprotocol/sdk` to
34
+ `@modelcontextprotocol/client`. Doing only the server half would have left
35
+ both generations installed, and E101 stops firing the moment any 2.x package
36
+ is present, so the half-migration would have silenced the rule that was
37
+ telling the truth.
38
+
39
+ ### Fixed
40
+
41
+ - **Two moderate advisories, by subtraction.** `qs` arrived through
42
+ `body-parser` through `express` through the 1.x SDK, which pulls a whole HTTP
43
+ and OAuth stack that a stdio server never speaks. `pnpm audit --prod` now
44
+ reports no known vulnerabilities, and production dependencies went from 30 to
45
+ 13.
46
+
47
+ ### Notes
48
+
49
+ - No rule changed, so no grade moved. `rulesVersion` is untouched and the
50
+ published skills index does not need regenerating.
51
+ - `check --skill`, `weigh`, `find` and `test` are byte-identical in behaviour;
52
+ the only thing that moved is what `efaimo mcp` puts on the wire.
53
+
7
54
  ## [0.4.0] - 2026-09-03
8
55
 
9
56
  ### Fixed
@@ -408,6 +455,7 @@ First release.
408
455
  Documented rule set (`docs/RULES.md`), token methodology (`docs/METHODOLOGY.md`),
409
456
  and integration guide (`docs/INTEGRATIONS.md`).
410
457
 
458
+ [0.5.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.5.0
411
459
  [0.4.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.4.0
412
460
  [0.3.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.3.0
413
461
  [0.2.0]: https://github.com/efaimo-ai/efaimo/releases/tag/v0.2.0
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, find out whether a search can single out a server's tools under deferred tool loading, or add an efaimo badge or CI gate.
4
4
  license: Apache-2.0
5
5
  metadata:
6
- version: "0.4.0"
6
+ version: "0.5.0"
7
7
  homepage: "https://efaimo.ai"
8
8
  ---
9
9
 
@@ -1,8 +1,8 @@
1
1
  import crossSpawn from "cross-spawn";
2
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
2
+ import { Client } from "@modelcontextprotocol/client";
3
+ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
4
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
5
+ import { SSEClientTransport } from "@modelcontextprotocol/client";
6
6
  import { introspectStateless } from "./stateless.js";
7
7
  import { minimalChildEnv } from "../util/childEnv.js";
8
8
  import { truncate, withTimeout } from "../util/misc.js";
@@ -1,3 +1,3 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- export declare function buildMcpServer(): Server;
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
+ export declare function buildMcpServer(): McpServer;
3
3
  export declare function runMcpServer(): Promise<void>;
@@ -1,88 +1,94 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
3
+ import { z } from "zod";
4
4
  import { checkSkillSet } from "../check/check.js";
5
5
  import { weighSkills } from "../weigh/weigh.js";
6
6
  import { findSkills } from "../skills/parse.js";
7
7
  import { renderCheckMarkdown, renderSkillSetMarkdown, renderWeighMarkdown } from "../reporters/markdown.js";
8
8
  import { VERSION } from "../version.js";
9
+ /**
10
+ * `efaimo mcp`: a small, read-only MCP server that exposes the skill checks to an
11
+ * agent, so it can lint or weigh a skill mid-session before committing it to
12
+ * context. Deliberately narrow: the tools only read files. They spawn no process
13
+ * and open no socket (unlike `check --mcp`, which connects to a live server), and
14
+ * `test` is not exposed at all because it spends tokens. That keeps the surface
15
+ * safe for an agent to call unattended.
16
+ *
17
+ * Built on the 2.x SDK line since 0.5.0. The 1.x line speaks 2025-11-25 and
18
+ * cannot answer `server/discover` or carry the SEP-2549 cache fields, so this
19
+ * server failed two MUST-level items of the specification the rest of this tool
20
+ * audits other servers against, and the skill this project publishes about that
21
+ * migration opens with "upgrade the SDK first". It was the only server we ship
22
+ * and the only one we had not migrated.
23
+ *
24
+ * `serveStdio` owns the era decision rather than this file: the opening exchange
25
+ * selects it, one instance is pinned for the connection, and a 2025-era client
26
+ * that opens with `initialize` is still served. Registering the tools once, on a
27
+ * factory that serves both eras, is the whole reason to use it instead of
28
+ * wiring a transport by hand.
29
+ */
9
30
  // Both tools only read local files, so the same hints apply: safe to call without
10
31
  // a confirmation, safe to repeat, and not reaching out to any external service.
11
32
  const READ_ONLY = { readOnlyHint: true, idempotentHint: true, openWorldHint: false };
12
- function requireString(args, key) {
13
- const v = args[key];
14
- if (typeof v !== "string" || v.trim() === "") {
15
- throw new Error(`"${key}" is required and must be a non-empty string`);
16
- }
17
- return v;
18
- }
19
- const pathSchema = (what) => ({
20
- type: "object",
21
- properties: { path: { type: "string", description: what } },
22
- required: ["path"],
23
- additionalProperties: false,
33
+ // One shared shape. `.min(1)` is the schema saying what `requireString` used to
34
+ // say at runtime: an empty path is not a path. Under the 2.x registration the
35
+ // SDK validates against this before the handler runs, so the check moved from
36
+ // our code into the contract, which is where a client can also see it.
37
+ const pathInput = (what) => z.object({ path: z.string().min(1).describe(what) });
38
+ const CHECK_DESCRIPTION = "Lint an Agent Skill, or a folder of skills, for spec compliance, trigger quality, " +
39
+ "context-window cost, reference integrity, and injection hygiene. Returns a grade from " +
40
+ "A to F and the findings. Read-only: reads files only.";
41
+ const WEIGH_DESCRIPTION = "Measure the context-window token cost of an Agent Skill: the metadata loaded into every " +
42
+ "session and the body loaded when the skill triggers. Returns token counts per skill. " +
43
+ "Read-only: reads files only.";
44
+ const text = (s) => ({ content: [{ type: "text", text: s }] });
45
+ const failure = (e) => ({
46
+ content: [{ type: "text", text: `error: ${e instanceof Error ? e.message : String(e)}` }],
47
+ isError: true,
24
48
  });
25
- const TOOLS = [
26
- {
27
- name: "efaimo_check_skill",
28
- description: "Lint an Agent Skill, or a folder of skills, for spec compliance, trigger quality, " +
29
- "context-window cost, reference integrity, and injection hygiene. Returns a grade from " +
30
- "A to F and the findings. Read-only: reads files only.",
31
- inputSchema: pathSchema("Path to a SKILL.md file or a directory that contains skills."),
49
+ export function buildMcpServer() {
50
+ const server = new McpServer({ name: "efaimo", version: VERSION }, { capabilities: { tools: {} } });
51
+ server.registerTool("efaimo_check_skill", {
52
+ description: CHECK_DESCRIPTION,
53
+ inputSchema: pathInput("Path to a SKILL.md file or a directory that contains skills."),
32
54
  annotations: READ_ONLY,
33
- async run(args) {
34
- const p = requireString(args, "path");
35
- const res = await checkSkillSet(p, p);
55
+ }, async ({ path }) => {
56
+ try {
57
+ const res = await checkSkillSet(path, path);
36
58
  if (res.perSkill.length === 1 && res.setFindings.length === 0) {
37
- return renderCheckMarkdown(res.perSkill[0].report);
59
+ return text(renderCheckMarkdown(res.perSkill[0].report));
38
60
  }
39
- return renderSkillSetMarkdown(res);
40
- },
41
- },
42
- {
43
- name: "efaimo_weigh_skill",
44
- description: "Measure the context-window token cost of an Agent Skill: the metadata loaded into every " +
45
- "session and the body loaded when the skill triggers. Returns token counts per skill. " +
46
- "Read-only: reads files only.",
47
- inputSchema: pathSchema("Path to a SKILL.md file or a directory that contains skills."),
48
- annotations: READ_ONLY,
49
- async run(args) {
50
- const p = requireString(args, "path");
51
- const set = findSkills(p);
52
- if (!set.skills.length)
53
- throw new Error(`no SKILL.md found under "${p}"`);
54
- return renderWeighMarkdown(await weighSkills(set));
55
- },
56
- },
57
- ];
58
- export function buildMcpServer() {
59
- const server = new Server({ name: "efaimo", version: VERSION }, { capabilities: { tools: {} } });
60
- server.setRequestHandler(ListToolsRequestSchema, () => ({
61
- tools: TOOLS.map((t) => ({
62
- name: t.name,
63
- description: t.description,
64
- inputSchema: t.inputSchema,
65
- annotations: t.annotations,
66
- })),
67
- }));
68
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
69
- const tool = TOOLS.find((t) => t.name === req.params.name);
70
- if (!tool) {
71
- return { content: [{ type: "text", text: `unknown tool: ${req.params.name}` }], isError: true };
61
+ return text(renderSkillSetMarkdown(res));
72
62
  }
63
+ catch (e) {
64
+ return failure(e);
65
+ }
66
+ });
67
+ server.registerTool("efaimo_weigh_skill", {
68
+ description: WEIGH_DESCRIPTION,
69
+ inputSchema: pathInput("Path to a SKILL.md file or a directory that contains skills."),
70
+ annotations: READ_ONLY,
71
+ }, async ({ path }) => {
73
72
  try {
74
- const text = await tool.run(req.params.arguments ?? {});
75
- return { content: [{ type: "text", text }] };
73
+ const set = findSkills(path);
74
+ if (!set.skills.length)
75
+ throw new Error(`no SKILL.md found under "${path}"`);
76
+ return text(renderWeighMarkdown(await weighSkills(set)));
76
77
  }
77
78
  catch (e) {
78
- const message = e instanceof Error ? e.message : String(e);
79
- return { content: [{ type: "text", text: `error: ${message}` }], isError: true };
79
+ return failure(e);
80
80
  }
81
81
  });
82
82
  return server;
83
83
  }
84
84
  export async function runMcpServer() {
85
- const server = buildMcpServer();
86
- await server.connect(new StdioServerTransport());
85
+ // A factory, not an instance: `serveStdio` pins one per connection after the
86
+ // opening exchange tells it which era it is talking to.
87
+ serveStdio(() => buildMcpServer(), {
88
+ onerror: (e) => console.error(`efaimo mcp: ${e.message}`),
89
+ });
87
90
  console.error(`efaimo mcp v${VERSION}: read-only skill tools ready on stdio`);
91
+ // stdin holds the process open; there is nothing to await. Returning a promise
92
+ // that never settles would only make the shutdown path harder to read.
93
+ await new Promise(() => { });
88
94
  }
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Kept in sync with package.json by test/meta.test.ts. */
2
- export declare const VERSION = "0.4.0";
2
+ export declare const VERSION = "0.5.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Kept in sync with package.json by test/meta.test.ts. */
2
- export const VERSION = "0.4.0";
2
+ export const VERSION = "0.5.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "efaimo",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Audit what your agent loads: quality and context cost for MCP servers and Agent Skills",
5
5
  "keywords": [
6
6
  "mcp",
@@ -65,12 +65,14 @@
65
65
  "prepublishOnly": "npm run typecheck && npm run build && npm run test"
66
66
  },
67
67
  "dependencies": {
68
- "@modelcontextprotocol/sdk": "^1.30.0",
68
+ "@modelcontextprotocol/client": "2.0.0",
69
+ "@modelcontextprotocol/server": "2.0.0",
69
70
  "commander": "^15.0.0",
70
71
  "cross-spawn": "^7.0.6",
71
72
  "gpt-tokenizer": "^3.4.0",
72
73
  "picocolors": "^1.1.1",
73
- "yaml": "^2.9.0"
74
+ "yaml": "^2.9.0",
75
+ "zod": "^4.5.4"
74
76
  },
75
77
  "devDependencies": {
76
78
  "@types/cross-spawn": "^6.0.6",