@cyanheads/cpsc-recalls-mcp-server 0.2.2 → 0.3.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/AGENTS.md +29 -104
- package/CLAUDE.md +29 -104
- package/Dockerfile +17 -10
- package/README.md +46 -93
- package/changelog/0.2.x/0.2.3.md +29 -0
- package/changelog/0.3.x/0.3.0.md +32 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/tools/definitions/cpsc-get-recall.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/cpsc-get-recall.tool.js +5 -4
- package/dist/mcp-server/tools/definitions/cpsc-get-recall.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/cpsc-get-recent.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/cpsc-get-recent.tool.js +3 -2
- package/dist/mcp-server/tools/definitions/cpsc-get-recent.tool.js.map +1 -1
- package/dist/mcp-server/tools/definitions/cpsc-search-recalls.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/definitions/cpsc-search-recalls.tool.js +4 -3
- package/dist/mcp-server/tools/definitions/cpsc-search-recalls.tool.js.map +1 -1
- package/dist/mcp-server/tools/markdown-escape.d.ts +0 -14
- package/dist/mcp-server/tools/markdown-escape.d.ts.map +1 -1
- package/dist/mcp-server/tools/markdown-escape.js +0 -28
- package/dist/mcp-server/tools/markdown-escape.js.map +1 -1
- package/dist/mcp-server/tools/response-budget.d.ts +7 -3
- package/dist/mcp-server/tools/response-budget.d.ts.map +1 -1
- package/dist/mcp-server/tools/response-budget.js +42 -4
- package/dist/mcp-server/tools/response-budget.js.map +1 -1
- package/package.json +13 -10
- package/server.json +3 -3
- package/skills/cpsc-recalls-guide/SKILL.md +48 -0
package/AGENTS.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** cpsc-recalls-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.3.0
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
-
**MCP SDK:** `@modelcontextprotocol/server` ^2.
|
|
8
|
-
**Zod:** ^4.6.
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.4.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
8
|
+
**Zod:** ^4.6.7
|
|
9
9
|
|
|
10
10
|
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
11
11
|
|
|
@@ -44,105 +44,15 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
44
44
|
|
|
45
45
|
## Patterns
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
The three registered tools live in `src/mcp-server/tools/definitions/`:
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
export const searchItems = tool('search_items', {
|
|
53
|
-
description: 'Search inventory items by query.',
|
|
54
|
-
annotations: { readOnlyHint: true },
|
|
55
|
-
input: z.object({
|
|
56
|
-
query: z.string().describe('Search terms'),
|
|
57
|
-
limit: z.number().int().min(1).max(100).default(10).describe('Max results (1–100)'),
|
|
58
|
-
}),
|
|
59
|
-
output: z.object({
|
|
60
|
-
items: z.array(z.object({
|
|
61
|
-
id: z.string().describe('Item ID'),
|
|
62
|
-
name: z.string().describe('Item name'),
|
|
63
|
-
})).describe('Matching items'),
|
|
64
|
-
}),
|
|
65
|
-
auth: ['inventory:read'],
|
|
66
|
-
|
|
67
|
-
async handler(input, ctx) {
|
|
68
|
-
const items = await findItems(input.query, input.limit);
|
|
69
|
-
ctx.log.info('Search completed', { query: input.query, count: items.length });
|
|
70
|
-
return { items };
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
// format() populates content[] — the markdown twin of structuredContent.
|
|
74
|
-
// Different clients read different surfaces (Claude Code → structuredContent,
|
|
75
|
-
// Claude Desktop → content[]); both must carry the same data.
|
|
76
|
-
// Enforced at lint time: every field in `output` must appear in the rendered text.
|
|
77
|
-
format: (result) => [{
|
|
78
|
-
type: 'text',
|
|
79
|
-
text: result.items.map(i => `**${i.id}**: ${i.name}`).join('\n'),
|
|
80
|
-
}],
|
|
81
|
-
});
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### Resource
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
import { resource, z } from '@cyanheads/mcp-ts-core';
|
|
88
|
-
import { notFound } from '@cyanheads/mcp-ts-core/errors';
|
|
89
|
-
|
|
90
|
-
export const itemData = resource('inventory://{itemId}', {
|
|
91
|
-
description: 'Fetch an inventory item by ID.',
|
|
92
|
-
params: z.object({ itemId: z.string().describe('Item identifier') }),
|
|
93
|
-
auth: ['inventory:read'],
|
|
94
|
-
async handler(params, ctx) {
|
|
95
|
-
const item = await ctx.state.get(`item/${params.itemId}`);
|
|
96
|
-
if (!item) throw notFound(`Item ${params.itemId} not found`, { itemId: params.itemId });
|
|
97
|
-
return item;
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Prompt
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
import { prompt, z } from '@cyanheads/mcp-ts-core';
|
|
106
|
-
|
|
107
|
-
export const reviewCode = prompt('review_code', {
|
|
108
|
-
description: 'Review code for issues and best practices.',
|
|
109
|
-
args: z.object({
|
|
110
|
-
code: z.string().describe('Code to review'),
|
|
111
|
-
language: z.string().optional().describe('Programming language'),
|
|
112
|
-
}),
|
|
113
|
-
generate: (args) => [
|
|
114
|
-
{ role: 'user', content: { type: 'text', text: `Review this ${args.language ?? ''} code:\n${args.code}` } },
|
|
115
|
-
],
|
|
116
|
-
});
|
|
117
|
-
```
|
|
49
|
+
- `cpsc-search-recalls.tool.ts` applies text/date filters and offset pagination; its record renderer also supplies the byte-budget calculation.
|
|
50
|
+
- `cpsc-get-recall.tool.ts` expands a recall number into the full record and distinguishes quoted CPSC narrative from server guidance.
|
|
51
|
+
- `cpsc-get-recent.tool.ts` browses a date window with the same pagination and response-budget contract as search.
|
|
118
52
|
|
|
119
|
-
|
|
53
|
+
All three declare typed error contracts and use `getCpscRecallService()`. The service normalizes CPSC text once before returning records. There are no prompts or server-specific environment variables, and no resource definitions: the only resource is the published skill `skill://cpsc-recalls-guide/SKILL.md`, which the framework serves from root `skills/` (`createApp({ skills: 'skills' })` in `src/index.ts`).
|
|
120
54
|
|
|
121
|
-
|
|
122
|
-
// src/config/server-config.ts — lazy-parsed, separate from framework config
|
|
123
|
-
import { z } from '@cyanheads/mcp-ts-core';
|
|
124
|
-
import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
|
|
125
|
-
|
|
126
|
-
const ServerConfigSchema = z.object({
|
|
127
|
-
apiKey: z.string().describe('External API key'),
|
|
128
|
-
maxResults: z.coerce.number().default(100),
|
|
129
|
-
verboseLogging: z.stringbool().default(false).describe('Enable verbose logging'),
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
let _config: z.infer<typeof ServerConfigSchema> | undefined;
|
|
133
|
-
export function getServerConfig() {
|
|
134
|
-
_config ??= parseEnvConfig(ServerConfigSchema, {
|
|
135
|
-
apiKey: 'MY_API_KEY',
|
|
136
|
-
maxResults: 'MY_MAX_RESULTS',
|
|
137
|
-
verboseLogging: 'MY_VERBOSE_LOGGING',
|
|
138
|
-
});
|
|
139
|
-
return _config;
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
`parseEnvConfig` maps Zod schema paths → env var names so errors name the variable (`MY_API_KEY`) not the path (`apiKey`). Throws `ConfigurationError`, which the framework prints as a clean startup banner.
|
|
144
|
-
|
|
145
|
-
For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
|
|
55
|
+
`skills/cpsc-recalls-guide/SKILL.md` carries what the tool descriptions don't: search-formulation gates (one empty search is not "not recalled"), recall-number and identifier quirks, and the jurisdiction edges. Edit it with the `add-skill` skill, and re-check its gates against live tool behavior with `field-test`.
|
|
146
56
|
|
|
147
57
|
### Server identity and instructions
|
|
148
58
|
|
|
@@ -266,6 +176,8 @@ src/
|
|
|
266
176
|
|
|
267
177
|
## Naming
|
|
268
178
|
|
|
179
|
+
**CPSC prose formatting:** retain `src/mcp-server/tools/markdown-escape.ts` for plain CPSC text: it leaves an address GFM autolinks unescaped, where the framework's CommonMark inline escaper would put a backslash inside the link. URL fields use the framework's `markdownLink` / `markdownImage`. The enrichment byte reservation in `response-budget.ts` models the framework trailer's rendering — code spans and fenced blocks sized past each value's backtick runs, `> `-quoted notice lines — and needs re-checking whenever the framework changes that trailer.
|
|
180
|
+
|
|
269
181
|
| What | Convention | Example |
|
|
270
182
|
|:-----|:-----------|:--------|
|
|
271
183
|
| Files | kebab-case with suffix | `search-docs.tool.ts` |
|
|
@@ -277,9 +189,18 @@ src/
|
|
|
277
189
|
|
|
278
190
|
## Skills
|
|
279
191
|
|
|
280
|
-
|
|
192
|
+
Two skill trees live at the project root, written for different readers:
|
|
193
|
+
|
|
194
|
+
| Tree | For | Written by |
|
|
195
|
+
|:-----|:----|:-----------|
|
|
196
|
+
| `framework-skills/` | The agent developing this server — framework skills (`metadata.audience: external`) plus any development-time skills of the server's own | `init` and the `maintenance` sync (framework skills); you (your own) |
|
|
197
|
+
| `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill. This server publishes one: `cpsc-recalls-guide` |
|
|
198
|
+
|
|
199
|
+
Development skills are modular instructions. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. Their directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand them to every agent that installs it.
|
|
200
|
+
|
|
201
|
+
A published skill in root `skills/` reaches plugin installs and the `.mcpb` bundle as-is, ships in the npm package and the Docker image, and reaches MCP clients when the server opts in with `createApp({ skills: 'skills' })`. `lint:packaging` validates the tree.
|
|
281
202
|
|
|
282
|
-
**Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
|
|
203
|
+
**Agent skill directory:** Copy `framework-skills/` into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
|
|
283
204
|
|
|
284
205
|
Available skills:
|
|
285
206
|
|
|
@@ -293,6 +214,7 @@ Available skills:
|
|
|
293
214
|
| `add-prompt` | Scaffold a new prompt definition |
|
|
294
215
|
| `add-service` | Scaffold a new service integration |
|
|
295
216
|
| `add-test` | Scaffold test file for a tool, resource, or service |
|
|
217
|
+
| `add-skill` | Author a skill the server publishes in root `skills/` for the agents that use it — when one earns its place, structure, validation, delivery |
|
|
296
218
|
| `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
|
|
297
219
|
| `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
|
|
298
220
|
| `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
|
|
@@ -338,7 +260,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
338
260
|
| `bun run audit:fix` | `bun audit fix` — upgrade vulnerable packages to the lowest safe version within existing ranges (`--dry-run` previews, `--latest` rewrites ranges). First response when `devcheck` flags a transitive advisory; then `bun update <name>`, then `bun dedupe` |
|
|
339
261
|
| `bun run audit:refresh` | Delete `bun.lock` and reinstall. Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe` — re-resolves every ranged dep (the framework pin included) and rewrites the lockfile as `lockfileVersion: 2` |
|
|
340
262
|
| `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
|
|
341
|
-
| `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
|
|
263
|
+
| `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, `.mcpbignore` coverage, plugin manifests, the published `skills/` tree (run by devcheck) |
|
|
342
264
|
| `bun run list-skills` | Print the skill registry |
|
|
343
265
|
| `bun run tree` | Generate directory structure doc |
|
|
344
266
|
| `bun run format` | Auto-fix formatting (safe fixes only) |
|
|
@@ -356,7 +278,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
356
278
|
|
|
357
279
|
## Bundling
|
|
358
280
|
|
|
359
|
-
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips
|
|
281
|
+
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored local files — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line. Root `skills/` ships on purpose. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips four classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/` at a package root, and any `SKILL.md` — a runtime directory deeper in a package, such as the MCP SDK's `dist/ext/skills/`, ships); platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on; node-gyp build output beside a `binding.gyp` (`config.gypi`, Makefiles, `obj.target/`, `.deps/`, object files and debug symbols), which records the packing machine's paths — compiled `*.node` addons stay; and build-only package files — the framework's `changelog/`, `scripts/`, `templates/`, `CLAUDE.md`, `AGENTS.md`, and config bases (`biome.json`, `vitest.config.base.mjs`, `config/tsconfig.base.json`), and `better-sqlite3`'s `deps/` and `src/` C sources. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
|
|
360
282
|
|
|
361
283
|
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.
|
|
362
284
|
|
|
@@ -423,7 +345,10 @@ import { getMyService } from '@/services/my-domain/my-service.js';
|
|
|
423
345
|
- [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
|
|
424
346
|
- [ ] Registered in `createApp()` arrays (directly or via barrel exports)
|
|
425
347
|
- [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
|
|
348
|
+
- [ ] `plugin.json` + `mcp.json` (Agent Plugins 1.0.0 — read by Codex, Cursor, GitHub Copilot, VS Code, Kiro) populated — `plugin.json` `name` = the unscoped repo name (lowercase `a-z`, `0-9`, `-`, `.` only); `version`, `description`, `author`, `homepage`, `repository`, `license`, `keywords` from `package.json`; no other top-level field, `extensions` included. `mcp.json` holds one `"type": "stdio"` entry keyed by the unscoped name, `npx -y <full package name>`, and the same fixed `env` as `.codex-plugin/mcp.json`. Neither file carries a user-supplied variable (no `""`, no `${…}` besides `${PLUGIN_ROOT}`/`${PLUGIN_DATA}`): keys reach Codex through `.codex-plugin/mcp.json` `env_vars`, which works only while `.codex-plugin/plugin.json` keeps `"mcpServers": "./.codex-plugin/mcp.json"`
|
|
426
349
|
- [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
|
|
427
350
|
- [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
|
|
428
351
|
- [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
|
|
352
|
+
- [ ] Any new root dev path has a root-anchored line in `.mcpbignore`; `bun run bundle` and `lint:packaging` pass
|
|
353
|
+
- [ ] If the server publishes a skill in root `skills/`: `lint:packaging` passes, and `field-test` verified every gate and identifier claim in it against live tool behavior
|
|
429
354
|
- [ ] `bun run devcheck` passes
|
package/CLAUDE.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Developer Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** cpsc-recalls-mcp-server
|
|
4
|
-
**Version:** 0.
|
|
5
|
-
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.
|
|
4
|
+
**Version:** 0.3.0
|
|
5
|
+
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.18`
|
|
6
6
|
**Engines:** Bun ≥1.4.0, Node ≥24.0.0
|
|
7
|
-
**MCP SDK:** `@modelcontextprotocol/server` ^2.
|
|
8
|
-
**Zod:** ^4.6.
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.4.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
8
|
+
**Zod:** ^4.6.7
|
|
9
9
|
|
|
10
10
|
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
11
11
|
|
|
@@ -44,105 +44,15 @@ Tailor suggestions to what's actually missing or stale — don't recite the full
|
|
|
44
44
|
|
|
45
45
|
## Patterns
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
The three registered tools live in `src/mcp-server/tools/definitions/`:
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
export const searchItems = tool('search_items', {
|
|
53
|
-
description: 'Search inventory items by query.',
|
|
54
|
-
annotations: { readOnlyHint: true },
|
|
55
|
-
input: z.object({
|
|
56
|
-
query: z.string().describe('Search terms'),
|
|
57
|
-
limit: z.number().int().min(1).max(100).default(10).describe('Max results (1–100)'),
|
|
58
|
-
}),
|
|
59
|
-
output: z.object({
|
|
60
|
-
items: z.array(z.object({
|
|
61
|
-
id: z.string().describe('Item ID'),
|
|
62
|
-
name: z.string().describe('Item name'),
|
|
63
|
-
})).describe('Matching items'),
|
|
64
|
-
}),
|
|
65
|
-
auth: ['inventory:read'],
|
|
66
|
-
|
|
67
|
-
async handler(input, ctx) {
|
|
68
|
-
const items = await findItems(input.query, input.limit);
|
|
69
|
-
ctx.log.info('Search completed', { query: input.query, count: items.length });
|
|
70
|
-
return { items };
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
// format() populates content[] — the markdown twin of structuredContent.
|
|
74
|
-
// Different clients read different surfaces (Claude Code → structuredContent,
|
|
75
|
-
// Claude Desktop → content[]); both must carry the same data.
|
|
76
|
-
// Enforced at lint time: every field in `output` must appear in the rendered text.
|
|
77
|
-
format: (result) => [{
|
|
78
|
-
type: 'text',
|
|
79
|
-
text: result.items.map(i => `**${i.id}**: ${i.name}`).join('\n'),
|
|
80
|
-
}],
|
|
81
|
-
});
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### Resource
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
import { resource, z } from '@cyanheads/mcp-ts-core';
|
|
88
|
-
import { notFound } from '@cyanheads/mcp-ts-core/errors';
|
|
89
|
-
|
|
90
|
-
export const itemData = resource('inventory://{itemId}', {
|
|
91
|
-
description: 'Fetch an inventory item by ID.',
|
|
92
|
-
params: z.object({ itemId: z.string().describe('Item identifier') }),
|
|
93
|
-
auth: ['inventory:read'],
|
|
94
|
-
async handler(params, ctx) {
|
|
95
|
-
const item = await ctx.state.get(`item/${params.itemId}`);
|
|
96
|
-
if (!item) throw notFound(`Item ${params.itemId} not found`, { itemId: params.itemId });
|
|
97
|
-
return item;
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Prompt
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
import { prompt, z } from '@cyanheads/mcp-ts-core';
|
|
106
|
-
|
|
107
|
-
export const reviewCode = prompt('review_code', {
|
|
108
|
-
description: 'Review code for issues and best practices.',
|
|
109
|
-
args: z.object({
|
|
110
|
-
code: z.string().describe('Code to review'),
|
|
111
|
-
language: z.string().optional().describe('Programming language'),
|
|
112
|
-
}),
|
|
113
|
-
generate: (args) => [
|
|
114
|
-
{ role: 'user', content: { type: 'text', text: `Review this ${args.language ?? ''} code:\n${args.code}` } },
|
|
115
|
-
],
|
|
116
|
-
});
|
|
117
|
-
```
|
|
49
|
+
- `cpsc-search-recalls.tool.ts` applies text/date filters and offset pagination; its record renderer also supplies the byte-budget calculation.
|
|
50
|
+
- `cpsc-get-recall.tool.ts` expands a recall number into the full record and distinguishes quoted CPSC narrative from server guidance.
|
|
51
|
+
- `cpsc-get-recent.tool.ts` browses a date window with the same pagination and response-budget contract as search.
|
|
118
52
|
|
|
119
|
-
|
|
53
|
+
All three declare typed error contracts and use `getCpscRecallService()`. The service normalizes CPSC text once before returning records. There are no prompts or server-specific environment variables, and no resource definitions: the only resource is the published skill `skill://cpsc-recalls-guide/SKILL.md`, which the framework serves from root `skills/` (`createApp({ skills: 'skills' })` in `src/index.ts`).
|
|
120
54
|
|
|
121
|
-
|
|
122
|
-
// src/config/server-config.ts — lazy-parsed, separate from framework config
|
|
123
|
-
import { z } from '@cyanheads/mcp-ts-core';
|
|
124
|
-
import { parseEnvConfig } from '@cyanheads/mcp-ts-core/config';
|
|
125
|
-
|
|
126
|
-
const ServerConfigSchema = z.object({
|
|
127
|
-
apiKey: z.string().describe('External API key'),
|
|
128
|
-
maxResults: z.coerce.number().default(100),
|
|
129
|
-
verboseLogging: z.stringbool().default(false).describe('Enable verbose logging'),
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
let _config: z.infer<typeof ServerConfigSchema> | undefined;
|
|
133
|
-
export function getServerConfig() {
|
|
134
|
-
_config ??= parseEnvConfig(ServerConfigSchema, {
|
|
135
|
-
apiKey: 'MY_API_KEY',
|
|
136
|
-
maxResults: 'MY_MAX_RESULTS',
|
|
137
|
-
verboseLogging: 'MY_VERBOSE_LOGGING',
|
|
138
|
-
});
|
|
139
|
-
return _config;
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
`parseEnvConfig` maps Zod schema paths → env var names so errors name the variable (`MY_API_KEY`) not the path (`apiKey`). Throws `ConfigurationError`, which the framework prints as a clean startup banner.
|
|
144
|
-
|
|
145
|
-
For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
|
|
55
|
+
`skills/cpsc-recalls-guide/SKILL.md` carries what the tool descriptions don't: search-formulation gates (one empty search is not "not recalled"), recall-number and identifier quirks, and the jurisdiction edges. Edit it with the `add-skill` skill, and re-check its gates against live tool behavior with `field-test`.
|
|
146
56
|
|
|
147
57
|
### Server identity and instructions
|
|
148
58
|
|
|
@@ -266,6 +176,8 @@ src/
|
|
|
266
176
|
|
|
267
177
|
## Naming
|
|
268
178
|
|
|
179
|
+
**CPSC prose formatting:** retain `src/mcp-server/tools/markdown-escape.ts` for plain CPSC text: it leaves an address GFM autolinks unescaped, where the framework's CommonMark inline escaper would put a backslash inside the link. URL fields use the framework's `markdownLink` / `markdownImage`. The enrichment byte reservation in `response-budget.ts` models the framework trailer's rendering — code spans and fenced blocks sized past each value's backtick runs, `> `-quoted notice lines — and needs re-checking whenever the framework changes that trailer.
|
|
180
|
+
|
|
269
181
|
| What | Convention | Example |
|
|
270
182
|
|:-----|:-----------|:--------|
|
|
271
183
|
| Files | kebab-case with suffix | `search-docs.tool.ts` |
|
|
@@ -277,9 +189,18 @@ src/
|
|
|
277
189
|
|
|
278
190
|
## Skills
|
|
279
191
|
|
|
280
|
-
|
|
192
|
+
Two skill trees live at the project root, written for different readers:
|
|
193
|
+
|
|
194
|
+
| Tree | For | Written by |
|
|
195
|
+
|:-----|:----|:-----------|
|
|
196
|
+
| `framework-skills/` | The agent developing this server — framework skills (`metadata.audience: external`) plus any development-time skills of the server's own | `init` and the `maintenance` sync (framework skills); you (your own) |
|
|
197
|
+
| `skills/` | The agents that install the server and call its tools — tool chains, identifier sources, gates | You, deliberately, with the `add-skill` skill. This server publishes one: `cpsc-recalls-guide` |
|
|
198
|
+
|
|
199
|
+
Development skills are modular instructions. Read them directly when a task matches — e.g., `framework-skills/add-tool/SKILL.md` when adding a tool. `bun run list-skills` prints the full registry. Their directory is deliberately not `skills/`: Claude Code and Codex auto-load a plugin's root `skills/`, so a server that ships `.claude-plugin/` or `.codex-plugin/` would hand them to every agent that installs it.
|
|
200
|
+
|
|
201
|
+
A published skill in root `skills/` reaches plugin installs and the `.mcpb` bundle as-is, ships in the npm package and the Docker image, and reaches MCP clients when the server opts in with `createApp({ skills: 'skills' })`. `lint:packaging` validates the tree.
|
|
281
202
|
|
|
282
|
-
**Agent skill directory:** Copy skills into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
|
|
203
|
+
**Agent skill directory:** Copy `framework-skills/` into the directory your agent discovers (Claude Code: `.claude/skills/`, others: equivalent). Skills then load as context without referencing `framework-skills/` paths. After framework updates, run the `maintenance` skill — Phase B re-syncs the agent directory.
|
|
283
204
|
|
|
284
205
|
Available skills:
|
|
285
206
|
|
|
@@ -293,6 +214,7 @@ Available skills:
|
|
|
293
214
|
| `add-prompt` | Scaffold a new prompt definition |
|
|
294
215
|
| `add-service` | Scaffold a new service integration |
|
|
295
216
|
| `add-test` | Scaffold test file for a tool, resource, or service |
|
|
217
|
+
| `add-skill` | Author a skill the server publishes in root `skills/` for the agents that use it — when one earns its place, structure, validation, delivery |
|
|
296
218
|
| `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
|
|
297
219
|
| `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
|
|
298
220
|
| `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
|
|
@@ -338,7 +260,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
338
260
|
| `bun run audit:fix` | `bun audit fix` — upgrade vulnerable packages to the lowest safe version within existing ranges (`--dry-run` previews, `--latest` rewrites ranges). First response when `devcheck` flags a transitive advisory; then `bun update <name>`, then `bun dedupe` |
|
|
339
261
|
| `bun run audit:refresh` | Delete `bun.lock` and reinstall. Last resort after `audit:fix`, `bun update <name>`, and `bun dedupe` — re-resolves every ranged dep (the framework pin included) and rewrites the lockfile as `lockfileVersion: 2` |
|
|
340
262
|
| `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
|
|
341
|
-
| `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity (run by devcheck) |
|
|
263
|
+
| `bun run lint:packaging` | Packaging surface checks — `server.json`/`manifest.json` env-var parity, `.mcpbignore` coverage, plugin manifests, the published `skills/` tree (run by devcheck) |
|
|
342
264
|
| `bun run list-skills` | Print the skill registry |
|
|
343
265
|
| `bun run tree` | Generate directory structure doc |
|
|
344
266
|
| `bun run format` | Auto-fix formatting (safe fixes only) |
|
|
@@ -356,7 +278,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
356
278
|
|
|
357
279
|
## Bundling
|
|
358
280
|
|
|
359
|
-
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips
|
|
281
|
+
`bun run bundle` produces a `.mcpb` extension bundle for one-click install in Claude Desktop. `mcpb pack` reads `.mcpbignore`, not `.gitignore`, so every root path the bundle must not ship — source, tests, docs, caches, logs, agent docs, plugin manifests, gitignored local files — needs a root-anchored line there (`/docs/`, never `docs/`, which also strips `node_modules/*/docs/`); `lint:packaging` names each missing or unanchored line. Root `skills/` ships on purpose. The pack step is followed by `scripts/clean-mcpb.ts`, which prunes dev dependencies (`mcpb clean`) and strips four classes of `node_modules/**` content that root-anchored `.mcpbignore` patterns cannot reach: dependency-shipped agent docs (`framework-skills/`, `skills/`, `.claude/`, `.agents/` at a package root, and any `SKILL.md` — a runtime directory deeper in a package, such as the MCP SDK's `dist/ext/skills/`, ships); platform-specific native bindings, which would otherwise lock the bundle to the platform it was packed on; node-gyp build output beside a `binding.gyp` (`config.gypi`, Makefiles, `obj.target/`, `.deps/`, object files and debug symbols), which records the packing machine's paths — compiled `*.node` addons stay; and build-only package files — the framework's `changelog/`, `scripts/`, `templates/`, `CLAUDE.md`, `AGENTS.md`, and config bases (`biome.json`, `vitest.config.base.mjs`, `config/tsconfig.base.json`), and `better-sqlite3`'s `deps/` and `src/` C sources. MCPB is stdio-only — HTTP and Cloudflare Workers deployments are unaffected. Consumers who don't need it can delete `manifest.json` and `.mcpbignore`; `lint:packaging` skips cleanly.
|
|
360
282
|
|
|
361
283
|
**Adding an env var requires both files:** `server.json` (registry discovery, `environmentVariables[]`) and `manifest.json` (bundle install UX, `mcp_config.env` + `user_config`). `lint:packaging` (run by `devcheck`) verifies the env var names match, that every `user_config` option is wired into `mcp_config.env` as `"X": "${user_config.X}"` (the host substitutes nothing else — `"${X}"` reaches the server as that literal string), and that an optional string option carries `"default": ""`.
|
|
362
284
|
|
|
@@ -423,7 +345,10 @@ import { getMyService } from '@/services/my-domain/my-service.js';
|
|
|
423
345
|
- [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
|
|
424
346
|
- [ ] Registered in `createApp()` arrays (directly or via barrel exports)
|
|
425
347
|
- [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
|
|
348
|
+
- [ ] `plugin.json` + `mcp.json` (Agent Plugins 1.0.0 — read by Codex, Cursor, GitHub Copilot, VS Code, Kiro) populated — `plugin.json` `name` = the unscoped repo name (lowercase `a-z`, `0-9`, `-`, `.` only); `version`, `description`, `author`, `homepage`, `repository`, `license`, `keywords` from `package.json`; no other top-level field, `extensions` included. `mcp.json` holds one `"type": "stdio"` entry keyed by the unscoped name, `npx -y <full package name>`, and the same fixed `env` as `.codex-plugin/mcp.json`. Neither file carries a user-supplied variable (no `""`, no `${…}` besides `${PLUGIN_ROOT}`/`${PLUGIN_DATA}`): keys reach Codex through `.codex-plugin/mcp.json` `env_vars`, which works only while `.codex-plugin/plugin.json` keeps `"mcpServers": "./.codex-plugin/mcp.json"`
|
|
426
349
|
- [ ] `.codex-plugin/plugin.json` populated — `name`, `version`, `description`, `repository`, `license` from `package.json`; `interface.displayName` = the unscoped repo name (never the npm scope — `lint:packaging` enforces this); `interface.shortDescription` from `package.json` description
|
|
427
350
|
- [ ] `.codex-plugin/mcp.json` updated — server name key is the unscoped repo name; every user-supplied variable (API key, contact email, instance URL) is listed in `env_vars` so Codex forwards it from the user's environment. Never write `"KEY": ""` into `env` — an empty value replaces the user's exported key and is read as unset
|
|
428
351
|
- [ ] `.claude-plugin/plugin.json` populated — `name`, `version`, `description`, `author`, `repository`, `license`, `keywords` from `package.json`; inline `mcpServers` entry keyed by the unscoped repo name. Every user-supplied variable is declared under `userConfig` (`type`, `title`, `description`; `sensitive: true` for keys and tokens; `required: true` or `default: ""`) and referenced from `env` as `"KEY": "${user_config.<option>}"` — mirror the `user_config` block in `manifest.json`. Never write `"KEY": ""` into `env`
|
|
352
|
+
- [ ] Any new root dev path has a root-anchored line in `.mcpbignore`; `bun run bundle` and `lint:packaging` pass
|
|
353
|
+
- [ ] If the server publishes a skill in root `skills/`: `lint:packaging` passes, and `field-test` verified every gate and identifier claim in it against live tool behavior
|
|
429
354
|
- [ ] `bun run devcheck` passes
|
package/Dockerfile
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
# output. A stage that compiles a native addon needs the target-arch toolchain
|
|
15
15
|
# and cannot cross-compile this way — drop the flag there.
|
|
16
16
|
# ==============================================================================
|
|
17
|
-
FROM --platform=$BUILDPLATFORM oven/bun:1.4.
|
|
17
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.4 AS build
|
|
18
18
|
|
|
19
19
|
WORKDIR /usr/src/app
|
|
20
20
|
|
|
@@ -29,6 +29,11 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
|
29
29
|
# Copy the rest of the source code
|
|
30
30
|
COPY . .
|
|
31
31
|
|
|
32
|
+
# The skills a server publishes (`createApp({ skills: 'skills' })`) are read at
|
|
33
|
+
# runtime from the application root. Created when the project has none, so the
|
|
34
|
+
# production stage's copy below holds with or without one.
|
|
35
|
+
RUN mkdir -p skills
|
|
36
|
+
|
|
32
37
|
# Build the application
|
|
33
38
|
RUN bun run build
|
|
34
39
|
|
|
@@ -40,13 +45,13 @@ RUN bun run build
|
|
|
40
45
|
# here can run JavaScript — bunfig.toml's security scanner runs as a Bun
|
|
41
46
|
# program, and so do the OTel and musl-prune scripts — so the stage runs on
|
|
42
47
|
# $BUILDPLATFORM and cross-installs with `--os`/`--cpu`, which pick each
|
|
43
|
-
# platform-specific optional dependency (native bindings
|
|
44
|
-
# `node_modules` leaves this stage.
|
|
48
|
+
# platform-specific optional dependency (native bindings such as DuckDB's) for
|
|
49
|
+
# the target. Only `node_modules` leaves this stage.
|
|
45
50
|
#
|
|
46
51
|
# A clean image rather than `FROM build`: the build stage's node_modules holds
|
|
47
52
|
# devDependencies.
|
|
48
53
|
# ==============================================================================
|
|
49
|
-
FROM --platform=$BUILDPLATFORM oven/bun:1.4.
|
|
54
|
+
FROM --platform=$BUILDPLATFORM oven/bun:1.4.4 AS deps
|
|
50
55
|
|
|
51
56
|
WORKDIR /usr/src/app
|
|
52
57
|
|
|
@@ -94,10 +99,11 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \
|
|
|
94
99
|
fi
|
|
95
100
|
|
|
96
101
|
# `--os`/`--cpu` have no libc counterpart, so a native dependency published in
|
|
97
|
-
# glibc and musl variants installs both. The
|
|
98
|
-
# never loads the musl copy; the script
|
|
99
|
-
# admits only musl. It follows every
|
|
100
|
-
# restores what it removes.
|
|
102
|
+
# glibc and musl variants (DuckDB's bindings, for one) installs both. The
|
|
103
|
+
# runtime image is Debian (glibc) and never loads the musl copy; the script
|
|
104
|
+
# deletes every package whose own `libc` admits only musl. It follows every
|
|
105
|
+
# install, since a later `bun install` restores what it removes. A server on a
|
|
106
|
+
# musl (Alpine) runtime image drops these two lines.
|
|
101
107
|
COPY scripts/prune-musl-packages.ts ./scripts/
|
|
102
108
|
RUN bun scripts/prune-musl-packages.ts
|
|
103
109
|
|
|
@@ -113,7 +119,7 @@ RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
|
|
|
113
119
|
# dependencies and build artifacts. Its only Bun invocations are HEALTHCHECK
|
|
114
120
|
# and CMD, which run on the real target at container start.
|
|
115
121
|
# ==============================================================================
|
|
116
|
-
FROM oven/bun:1.4.
|
|
122
|
+
FROM oven/bun:1.4.4-slim AS production
|
|
117
123
|
|
|
118
124
|
WORKDIR /usr/src/app
|
|
119
125
|
|
|
@@ -123,7 +129,7 @@ ENV NODE_ENV=production
|
|
|
123
129
|
# OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
|
|
124
130
|
ARG APP_VERSION
|
|
125
131
|
LABEL org.opencontainers.image.title="cpsc-recalls-mcp-server"
|
|
126
|
-
LABEL org.opencontainers.image.description="
|
|
132
|
+
LABEL org.opencontainers.image.description="Search and retrieve US consumer product recalls from the CPSC (Consumer Product Safety Commission) via MCP. STDIO or Streamable HTTP."
|
|
127
133
|
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
|
128
134
|
LABEL org.opencontainers.image.version="${APP_VERSION}"
|
|
129
135
|
LABEL org.opencontainers.image.source="https://github.com/cyanheads/cpsc-recalls-mcp-server"
|
|
@@ -135,6 +141,7 @@ COPY --from=deps /usr/src/app/node_modules ./node_modules
|
|
|
135
141
|
|
|
136
142
|
# Copy the compiled application code from the build stage
|
|
137
143
|
COPY --from=build /usr/src/app/dist ./dist
|
|
144
|
+
COPY --from=build /usr/src/app/skills ./skills
|
|
138
145
|
|
|
139
146
|
# The 'oven/bun' image already provides a non-root user named 'bun'.
|
|
140
147
|
# We will use this existing user for enhanced security.
|