@cyanheads/mcp-ts-core 0.5.0 → 0.5.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"toolDefinition.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/utils/toolDefinition.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAErD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc,CAC7B,MAAM,SAAS,SAAS,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC,WAAW,CAAC,EAC9D,OAAO,SAAS,SAAS,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC,WAAW,CAAC;IAE/D,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,qCAAqC;IACrC,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,8BAA8B;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,YAAY,EAAE,CAAC;IACtD;;;OAGG;IACH,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAChG,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,MAAM,EAAE,OAAO,CAAC;IAChB,qEAAqE;IACrE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,gEAAgE;AAChE,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC;AAM/F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,IAAI,CAAC,MAAM,SAAS,SAAS,CAAC,WAAW,CAAC,EAAE,OAAO,SAAS,SAAS,CAAC,WAAW,CAAC,EAChG,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,GACrD,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,CAEjC"}
1
+ {"version":3,"file":"toolDefinition.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/utils/toolDefinition.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAErD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc,CAC7B,MAAM,SAAS,SAAS,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC,WAAW,CAAC,EAC9D,OAAO,SAAS,SAAS,CAAC,WAAW,CAAC,GAAG,SAAS,CAAC,WAAW,CAAC;IAE/D,0DAA0D;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,qCAAqC;IACrC,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,8BAA8B;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,YAAY,EAAE,CAAC;IACtD;;;OAGG;IACH,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAChG,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,MAAM,EAAE,OAAO,CAAC;IAChB,qEAAqE;IACrE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,gEAAgE;AAChE,MAAM,MAAM,iBAAiB,GAAG,cAAc,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC;AAM/F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,IAAI,CAAC,MAAM,SAAS,SAAS,CAAC,WAAW,CAAC,EAAE,OAAO,SAAS,SAAS,CAAC,WAAW,CAAC,EAChG,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,GACrD,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC,CAEjC"}
@@ -28,8 +28,8 @@
28
28
  * ctx.log.info('Processing', { query: input.query });
29
29
  * return { items: await search(input.query) };
30
30
  * },
31
- * // format() populates content[] — the only field most LLM clients read.
32
- * // Render all data the model needs; structuredContent is not forwarded.
31
+ * // format() populates content[] — the markdown twin of structuredContent.
32
+ * // Different clients read different surfaces; both must be content-complete.
33
33
  * format: (result) => [{
34
34
  * type: 'text',
35
35
  * text: result.items.map(i => `**${i.id}**: ${i.name} (${i.status})`).join('\n'),
@@ -1 +1 @@
1
- {"version":3,"file":"toolDefinition.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/utils/toolDefinition.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA2FH,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,IAAI,CAClB,IAAY,EACZ,OAAsD;IAEtD,OAAO,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC;AAC9B,CAAC"}
1
+ {"version":3,"file":"toolDefinition.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/utils/toolDefinition.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA4FH,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,IAAI,CAClB,IAAY,EACZ,OAAsD;IAEtD,OAAO,EAAE,IAAI,EAAE,GAAG,OAAO,EAAE,CAAC;AAC9B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/mcp-ts-core",
3
- "version": "0.5.0",
3
+ "version": "0.5.3",
4
4
  "mcpName": "io.github.cyanheads/mcp-ts-core",
5
5
  "description": "Agent-native TypeScript framework for building MCP servers. Build tools, not infrastructure. Declarative definitions with auth, multi-backend storage, OpenTelemetry, and first-class support for Node.js and Cloudflare Workers.",
6
6
  "main": "dist/core/index.js",
@@ -8,6 +8,7 @@
8
8
  "files": [
9
9
  "dist/",
10
10
  "scripts/build.ts",
11
+ "scripts/check-docs-sync.ts",
11
12
  "scripts/clean.ts",
12
13
  "scripts/devcheck.ts",
13
14
  "scripts/lint-mcp.ts",
@@ -250,7 +251,7 @@
250
251
  },
251
252
  "dependencies": {
252
253
  "@hono/mcp": "^0.2.5",
253
- "@hono/node-server": "^1.19.14",
254
+ "@hono/node-server": "^2.0.0",
254
255
  "@modelcontextprotocol/ext-apps": "^1.6.0",
255
256
  "@modelcontextprotocol/sdk": "^1.29.0",
256
257
  "@opentelemetry/api": "^1.9.1",
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview Verifies that CLAUDE.md and AGENTS.md stay in sync. The init CLI
4
+ * ships both files byte-identical and each agent tool reads the file named for
5
+ * it — silent drift after edits leaves one agent on a stale protocol.
6
+ *
7
+ * Behavior:
8
+ * • Both exist, identical → pass
9
+ * • Both exist, drift → fail, print first divergent lines + fix hint
10
+ * • Only one exists → pass (report which file is present)
11
+ * • Neither exists → skip (not an mcp-ts-core project)
12
+ *
13
+ * Runs as a devcheck step and standalone: `bun run scripts/check-docs-sync.ts`.
14
+ *
15
+ * @module scripts/check-docs-sync
16
+ */
17
+
18
+ import { existsSync, readFileSync } from 'node:fs';
19
+ import { resolve } from 'node:path';
20
+ import process from 'node:process';
21
+
22
+ const CLAUDE_PATH = resolve('CLAUDE.md');
23
+ const AGENTS_PATH = resolve('AGENTS.md');
24
+ const MAX_DIFF_LINES = 20;
25
+
26
+ /**
27
+ * Line-by-line drift summary. Not a true unified diff — tools that move lines
28
+ * will show every shifted line as divergent, which is fine for the enforcement
29
+ * use case (the fix is always "reconcile both files" regardless).
30
+ */
31
+ function summarizeDrift(a: string, b: string): string {
32
+ const aLines = a.split('\n');
33
+ const bLines = b.split('\n');
34
+ const max = Math.max(aLines.length, bLines.length);
35
+ const lines: string[] = [];
36
+ let drifts = 0;
37
+
38
+ for (let i = 0; i < max; i++) {
39
+ if (aLines[i] !== bLines[i]) {
40
+ drifts++;
41
+ if (drifts <= MAX_DIFF_LINES) {
42
+ const lineNo = String(i + 1).padStart(4);
43
+ if (aLines[i] !== undefined) lines.push(`${lineNo} - CLAUDE.md: ${aLines[i]}`);
44
+ if (bLines[i] !== undefined) lines.push(`${lineNo} + AGENTS.md: ${bLines[i]}`);
45
+ }
46
+ }
47
+ }
48
+
49
+ if (drifts > MAX_DIFF_LINES) {
50
+ lines.push(` ... and ${drifts - MAX_DIFF_LINES} more diverging line(s)`);
51
+ }
52
+ return lines.join('\n');
53
+ }
54
+
55
+ const hasClaude = existsSync(CLAUDE_PATH);
56
+ const hasAgents = existsSync(AGENTS_PATH);
57
+
58
+ if (!hasClaude && !hasAgents) {
59
+ console.log('Skipped: neither CLAUDE.md nor AGENTS.md exists.');
60
+ process.exit(0);
61
+ }
62
+
63
+ if (hasClaude !== hasAgents) {
64
+ const present = hasClaude ? 'CLAUDE.md' : 'AGENTS.md';
65
+ const absent = hasClaude ? 'AGENTS.md' : 'CLAUDE.md';
66
+ console.log(`${present} found. No ${absent} found — nothing to sync.`);
67
+ process.exit(0);
68
+ }
69
+
70
+ const claude = readFileSync(CLAUDE_PATH, 'utf-8');
71
+ const agents = readFileSync(AGENTS_PATH, 'utf-8');
72
+
73
+ if (claude === agents) {
74
+ console.log('CLAUDE.md and AGENTS.md are in sync.');
75
+ process.exit(0);
76
+ }
77
+
78
+ console.error('CLAUDE.md and AGENTS.md have drifted:');
79
+ console.error('');
80
+ console.error(summarizeDrift(claude, agents));
81
+ console.error('');
82
+ console.error(
83
+ 'Fix: edit both files together, or `cp CLAUDE.md AGENTS.md` (or reverse) if one is canonical.',
84
+ );
85
+ process.exit(1);
@@ -413,6 +413,14 @@ const ALL_CHECKS: Check[] = [
413
413
  tip: (c) =>
414
414
  `Fix definition errors reported above. See ${c.bold('validateDefinitions()')} docs for rule details.`,
415
415
  },
416
+ {
417
+ name: 'Docs Sync',
418
+ flag: '--no-docs-sync',
419
+ canFix: false,
420
+ getCommand: () => ['bun', 'run', 'scripts/check-docs-sync.ts'],
421
+ tip: (c) =>
422
+ `Edit both files together, or run ${c.bold('cp CLAUDE.md AGENTS.md')} (or reverse) to resync.`,
423
+ },
416
424
  {
417
425
  name: 'Biome',
418
426
  flag: '--no-lint',
@@ -4,7 +4,7 @@ description: >
4
4
  Scaffold a new MCP tool definition. Use when the user asks to add a tool, create a new tool, or implement a new capability for the server.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.4"
7
+ version: "1.6"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -57,9 +57,10 @@ export const {{TOOL_EXPORT}} = tool('{{tool_name}}', {
57
57
  return { /* output */ };
58
58
  },
59
59
 
60
- // format() populates MCP content[] — the only field most LLM clients forward
61
- // to the model. structuredContent (from output) is for programmatic use only.
62
- // Render ALL data the LLM needs to reason about the result.
60
+ // format() populates MCP content[] — the markdown twin of structuredContent.
61
+ // Different clients read different surfaces (Claude Code structuredContent,
62
+ // Claude Desktop content[]), so both must carry the same data.
63
+ // Enforced at lint time: every field in `output` must appear in the rendered text.
63
64
  format: (result) => {
64
65
  const lines: string[] = [];
65
66
  // Render each item with all relevant fields — not just a count or title.
@@ -312,7 +313,7 @@ Large payloads burn the agent's context window. Default to curated summaries; of
312
313
  - [ ] JSDoc `@fileoverview` and `@module` header present
313
314
  - [ ] Optional nested objects guarded for empty inner values from form-based clients (check `?.field` truthiness, not just object presence)
314
315
  - [ ] `handler(input, ctx)` is pure — throws on failure, no try/catch
315
- - [ ] `format()` renders all data the LLM needs (not just a count or title) `content[]` is the only field most clients forward to the model
316
+ - [ ] `format()` renders every field in the output schema — enforced at lint time via sentinel injection, startup fails with `format-parity` errors otherwise. Different clients forward different surfaces (Claude Code `structuredContent`, Claude Desktop `content[]`); both must carry the same data. Primary fix: render the missing field in `format()` (use `z.discriminatedUnion` for list/detail variants). Escape hatch: if the output schema was over-typed for a genuinely dynamic upstream API, relax it (`z.object({}).passthrough()`) rather than maintaining aspirational typing
316
317
  - [ ] If wrapping external API: output schema and `format()` preserve uncertainty from sparse upstream payloads instead of inventing concrete values
317
318
  - [ ] `auth` scopes declared if the tool needs authorization
318
319
  - [ ] `task: true` added if the tool is long-running
@@ -4,7 +4,7 @@ description: >
4
4
  Reference for core and server configuration in `@cyanheads/mcp-ts-core`. Covers env var tables with defaults, priority order, server-specific Zod schema pattern, and Workers lazy-parsing requirement.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.1"
7
+ version: "1.2"
8
8
  audience: external
9
9
  type: reference
10
10
  ---
@@ -4,7 +4,7 @@ description: >
4
4
  Design the tool surface, resources, and service layer for a new MCP server. Use when starting a new server, planning a major feature expansion, or when the user describes a domain/API they want to expose via MCP. Produces a design doc at docs/design.md that drives implementation.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "2.3"
7
+ version: "2.4"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -201,7 +201,7 @@ output: z.object({
201
201
  ```
202
202
 
203
203
  - **Truncate large output with counts.** When a list exceeds a reasonable display size, show the top N and append "...and X more". Don't silently drop results.
204
- - **`format()` is the model-facing output — make it content-complete.** MCP `content[]` (populated by `format()`) is the only field most LLM clients forward to the model. `structuredContent` (from `output`) is for programmatic/machine use and is not reliably shown to the LLM. A thin `format()` that returns only a count or title leaves the model blind to the actual data. Render all fields the LLM needs to reason about or act on the result. Use structured markdown (headers, bold labels, lists) for readability.
204
+ - **`format()` is the markdown twin of `structuredContent` — make both content-complete.** Different MCP clients forward different surfaces to the model: some (e.g., Claude Code) read `structuredContent` from `output`, others (e.g., Claude Desktop) read `content[]` from `format()`. Both must carry the same data so every client sees the same picture — `format()` just dresses it up with markdown. A thin `format()` that returns only a count or title leaves `content[]`-only clients blind to data that `structuredContent` clients can see. Render all fields the LLM needs, with structured markdown (headers, bold labels, lists) for readability.
205
205
 
206
206
  #### Batch input design
207
207
 
@@ -428,7 +428,7 @@ Execute the plan using the scaffolding skills:
428
428
  - [ ] Parameter `.describe()` text explains what the value is, what it affects, and tradeoffs
429
429
  - [ ] Input schemas use constrained types (enums, literals, regex) over free strings
430
430
  - [ ] Output schemas designed for LLM's next action — chaining IDs, post-write state, filtering communicated
431
- - [ ] `format()` renders all data the LLM needs — `content[]` is the only field most clients forward to the model (not just a count or title)
431
+ - [ ] `format()` renders all data the LLM needs — different clients forward different surfaces (Claude Code → `structuredContent`, Claude Desktop → `content[]`); both must carry the same data, not just a count or title
432
432
  - [ ] Error messages guide recovery — name what went wrong and what to do next
433
433
  - [ ] Annotations set correctly (`readOnlyHint`, `destructiveHint`, etc.)
434
434
  - [ ] Tool surface is self-sufficient — a tool-only agent can accomplish everything the server is for
@@ -4,7 +4,7 @@ description: >
4
4
  Exercise tools, resources, and prompts with real-world inputs to verify behavior end-to-end. Use after adding or modifying definitions, or when the user asks to test, try out, or verify their MCP surface. Calls each definition with realistic and adversarial inputs and produces a report of issues, pain points, and recommendations.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.1"
7
+ version: "1.2"
8
8
  audience: external
9
9
  type: debug
10
10
  ---
@@ -36,7 +36,7 @@ For every tool, resource, and prompt, run through these categories:
36
36
  | Category | What to test |
37
37
  |:---------|:-------------|
38
38
  | **Happy path** | Realistic input that should succeed. Verify output shape matches the output schema. Verify format function produces sensible content blocks. |
39
- | **`structuredContent` parity** | Compare the raw handler return, the data captured in `structuredContent`, and what `format()` renders into `content[]`. Flag fields or records that exist in the handler result or `structuredContent` but are absent from `content[]` unless the omission is explicit and acceptable for the tool's purpose. Most LLM clients only see `content[]`, so silent formatter drops are real bugs. |
39
+ | **`structuredContent` parity** | The `format-parity` lint rule already asserts every terminal field in the output schema appears in `format()`'s rendered text (via sentinel injection at startup). Field testing layers real-data checks on top: are values rendered accurately (not just their labels)? Do conditional-render branches in `format()` still render every field when specific values are present? Does the content look right to a human reading the LLM's view? |
40
40
  | **Variations** | Different valid input combinations — optional fields omitted, optional fields included, different enum values, min/max boundaries. |
41
41
  | **Field selection / projection** | For tools with `fields`, `include`, `expand`, `view`, or similar parameters, call the tool with non-default selections. Verify the handler returns the requested fields and `format()` renders each requested field rather than a hardcoded summary subset. |
42
42
  | **Edge cases** | Empty strings, zero values, very long inputs, special characters, Unicode. |
@@ -4,7 +4,7 @@ description: >
4
4
  Finalize documentation and project metadata for a ship-ready MCP server. Use after implementation is complete, tests pass, and devcheck is clean. Safe to run at any stage — each step checks current state and only acts on what still needs work.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.3"
7
+ version: "1.4"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -48,7 +48,7 @@ Capture: tool count, resource count, prompt count, service count, required env v
48
48
 
49
49
  Read `references/readme.md` for structure and conventions. If `README.md` doesn't exist, create it from scratch. If it exists, diff the current content against the audit — update tool/resource/prompt tables, env var lists, and descriptions to match the actual surface area. Don't rewrite sections that are already accurate.
50
50
 
51
- The header tagline (`<p><b>...</b></p>`) must match the `package.json` `description`.
51
+ The bold header tagline (the `<b>` text inside the first `<p>`) must match the `package.json` `description`. The surface count is a nested `<div>` inside the same `<p>`, separated by `•`.
52
52
 
53
53
  ### 3. Agent Protocol (CLAUDE.md / AGENTS.md)
54
54