@cyanheads/cpsc-recalls-mcp-server 0.2.3 → 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 +24 -9
- package/CLAUDE.md +24 -9
- package/Dockerfile +13 -6
- package/README.md +42 -111
- 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/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 -5
- package/dist/mcp-server/tools/response-budget.js.map +1 -1
- package/package.json +7 -6
- package/server.json +3 -3
- package/skills/cpsc-recalls-guide/SKILL.md +48 -0
package/AGENTS.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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.
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.4.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
8
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.
|
|
@@ -50,7 +50,9 @@ The three registered tools live in `src/mcp-server/tools/definitions/`:
|
|
|
50
50
|
- `cpsc-get-recall.tool.ts` expands a recall number into the full record and distinguishes quoted CPSC narrative from server guidance.
|
|
51
51
|
- `cpsc-get-recent.tool.ts` browses a date window with the same pagination and response-budget contract as search.
|
|
52
52
|
|
|
53
|
-
All three declare typed error contracts and use `getCpscRecallService()`. The service normalizes CPSC text once before returning records. There are no
|
|
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`).
|
|
54
|
+
|
|
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`.
|
|
54
56
|
|
|
55
57
|
### Server identity and instructions
|
|
56
58
|
|
|
@@ -174,7 +176,7 @@ src/
|
|
|
174
176
|
|
|
175
177
|
## Naming
|
|
176
178
|
|
|
177
|
-
**CPSC prose formatting:** retain `src/mcp-server/tools/markdown-escape.ts` for plain CPSC text: it
|
|
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.
|
|
178
180
|
|
|
179
181
|
| What | Convention | Example |
|
|
180
182
|
|:-----|:-----------|:--------|
|
|
@@ -187,9 +189,18 @@ src/
|
|
|
187
189
|
|
|
188
190
|
## Skills
|
|
189
191
|
|
|
190
|
-
|
|
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.
|
|
191
202
|
|
|
192
|
-
**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.
|
|
193
204
|
|
|
194
205
|
Available skills:
|
|
195
206
|
|
|
@@ -203,6 +214,7 @@ Available skills:
|
|
|
203
214
|
| `add-prompt` | Scaffold a new prompt definition |
|
|
204
215
|
| `add-service` | Scaffold a new service integration |
|
|
205
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 |
|
|
206
218
|
| `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
|
|
207
219
|
| `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
|
|
208
220
|
| `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
|
|
@@ -248,7 +260,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
248
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` |
|
|
249
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` |
|
|
250
262
|
| `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
|
|
251
|
-
| `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) |
|
|
252
264
|
| `bun run list-skills` | Print the skill registry |
|
|
253
265
|
| `bun run tree` | Generate directory structure doc |
|
|
254
266
|
| `bun run format` | Auto-fix formatting (safe fixes only) |
|
|
@@ -266,7 +278,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
266
278
|
|
|
267
279
|
## Bundling
|
|
268
280
|
|
|
269
|
-
`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.
|
|
270
282
|
|
|
271
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": ""`.
|
|
272
284
|
|
|
@@ -333,7 +345,10 @@ import { getMyService } from '@/services/my-domain/my-service.js';
|
|
|
333
345
|
- [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
|
|
334
346
|
- [ ] Registered in `createApp()` arrays (directly or via barrel exports)
|
|
335
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"`
|
|
336
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
|
|
337
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
|
|
338
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
|
|
339
354
|
- [ ] `bun run devcheck` passes
|
package/CLAUDE.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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.
|
|
7
|
+
**MCP SDK:** `@modelcontextprotocol/server` ^2.4.0 (protocol revisions 2026-07-28 and 2025-*)
|
|
8
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.
|
|
@@ -50,7 +50,9 @@ The three registered tools live in `src/mcp-server/tools/definitions/`:
|
|
|
50
50
|
- `cpsc-get-recall.tool.ts` expands a recall number into the full record and distinguishes quoted CPSC narrative from server guidance.
|
|
51
51
|
- `cpsc-get-recent.tool.ts` browses a date window with the same pagination and response-budget contract as search.
|
|
52
52
|
|
|
53
|
-
All three declare typed error contracts and use `getCpscRecallService()`. The service normalizes CPSC text once before returning records. There are no
|
|
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`).
|
|
54
|
+
|
|
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`.
|
|
54
56
|
|
|
55
57
|
### Server identity and instructions
|
|
56
58
|
|
|
@@ -174,7 +176,7 @@ src/
|
|
|
174
176
|
|
|
175
177
|
## Naming
|
|
176
178
|
|
|
177
|
-
**CPSC prose formatting:** retain `src/mcp-server/tools/markdown-escape.ts` for plain CPSC text: it
|
|
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.
|
|
178
180
|
|
|
179
181
|
| What | Convention | Example |
|
|
180
182
|
|:-----|:-----------|:--------|
|
|
@@ -187,9 +189,18 @@ src/
|
|
|
187
189
|
|
|
188
190
|
## Skills
|
|
189
191
|
|
|
190
|
-
|
|
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.
|
|
191
202
|
|
|
192
|
-
**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.
|
|
193
204
|
|
|
194
205
|
Available skills:
|
|
195
206
|
|
|
@@ -203,6 +214,7 @@ Available skills:
|
|
|
203
214
|
| `add-prompt` | Scaffold a new prompt definition |
|
|
204
215
|
| `add-service` | Scaffold a new service integration |
|
|
205
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 |
|
|
206
218
|
| `field-test` | Exercise tools/resources/prompts with real inputs, verify behavior, report issues |
|
|
207
219
|
| `tool-defs-analysis` | Read-only audit of MCP definition language across the surface — voice, leaks, defaults, recovery hints, output descriptions |
|
|
208
220
|
| `security-pass` | Audit server for MCP-flavored security gaps: output injection, scope blast radius, input sinks, tenant isolation |
|
|
@@ -248,7 +260,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
248
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` |
|
|
249
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` |
|
|
250
262
|
| `bun run lint:mcp` | Run the MCP definition linter standalone (rule catalog: `api-linter` skill) |
|
|
251
|
-
| `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) |
|
|
252
264
|
| `bun run list-skills` | Print the skill registry |
|
|
253
265
|
| `bun run tree` | Generate directory structure doc |
|
|
254
266
|
| `bun run format` | Auto-fix formatting (safe fixes only) |
|
|
@@ -266,7 +278,7 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
266
278
|
|
|
267
279
|
## Bundling
|
|
268
280
|
|
|
269
|
-
`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.
|
|
270
282
|
|
|
271
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": ""`.
|
|
272
284
|
|
|
@@ -333,7 +345,10 @@ import { getMyService } from '@/services/my-domain/my-service.js';
|
|
|
333
345
|
- [ ] If wrapping external API: tests include at least one sparse payload case with omitted upstream fields
|
|
334
346
|
- [ ] Registered in `createApp()` arrays (directly or via barrel exports)
|
|
335
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"`
|
|
336
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
|
|
337
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
|
|
338
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
|
|
339
354
|
- [ ] `bun run devcheck` passes
|
package/Dockerfile
CHANGED
|
@@ -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,8 +45,8 @@ 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.
|
|
@@ -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
|
|
|
@@ -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.
|
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<h1>@cyanheads/cpsc-recalls-mcp-server</h1>
|
|
3
3
|
<p><b>Search and retrieve US consumer product recalls from the CPSC (Consumer Product Safety Commission) via MCP. STDIO or Streamable HTTP.</b>
|
|
4
|
-
<div>3 Tools</div>
|
|
4
|
+
<div>3 Tools • 1 Skill</div>
|
|
5
5
|
</p>
|
|
6
6
|
</div>
|
|
7
7
|
|
|
8
8
|
<div align="center">
|
|
9
9
|
|
|
10
|
-
[](./CHANGELOG.md) [](./LICENSE) [](https://github.com/users/cyanheads/packages/container/package/cpsc-recalls-mcp-server) [](https://modelcontextprotocol.io/) [](https://www.npmjs.com/package/@cyanheads/cpsc-recalls-mcp-server) [](https://www.typescriptlang.org/) [](https://bun.sh/)
|
|
11
11
|
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -39,6 +39,12 @@ Consumer product recalls from the CPSC [saferproducts.gov](https://www.saferprod
|
|
|
39
39
|
| `cpsc_get_recall` | Full detail for a single recall by recall number — hazards, remedy, products, injuries, images, and the official CPSC page |
|
|
40
40
|
| `cpsc_get_recent` | Fetch the most recent recalls ordered newest-first, scoped to a configurable date window |
|
|
41
41
|
|
|
42
|
+
### Skills
|
|
43
|
+
|
|
44
|
+
| Skill | Description |
|
|
45
|
+
|:---|:---|
|
|
46
|
+
| `cpsc-recalls-guide` | Find and confirm US consumer product recalls with the `cpsc_*` tools — search formulation, recall-number quirks, and when an empty result proves nothing |
|
|
47
|
+
|
|
42
48
|
CPSC jurisdiction is consumer products only — food/drugs (FDA), motor vehicles/tires (NHTSA), boats (USCG), pesticides (EPA), and firearms (ATF) are not in this database; every response carries a jurisdiction note.
|
|
43
49
|
|
|
44
50
|
## Capability reference
|
|
@@ -64,6 +70,13 @@ CPSC jurisdiction is consumer products only — food/drugs (FDA), motor vehicles
|
|
|
64
70
|
- `days` look-back window (1–365, default 30) anchored to today; `limit` (1–100, default 20) and `offset` page through `total_found`, since narrowing `days` drops the oldest recalls rather than paging. Older recalls are reachable through `cpsc_search_recalls` date bounds
|
|
65
71
|
- Returns a lightweight record per recall — number, date, title, hazards, remedy types, product names, `cpsc_url`, `data_quality_notes` — for `cpsc_get_recall` to expand; `has_more` is the paging signal, and an empty window, an offset past the end, or a budget-cut page carries a notice. Failures: `upstream_rejected` (not retryable), `upstream_error` (retryable)
|
|
66
72
|
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### `cpsc-recalls-guide` <sub>skill</sub>
|
|
76
|
+
|
|
77
|
+
- Helps an agent check whether a specific product, model, serial, or UPC was recalled, sweep a brand's or retailer's recall history, monitor new or republished recalls for a hazard, and decide whether a product is CPSC's at all
|
|
78
|
+
- Served over MCP as the resource `skill://cpsc-recalls-guide/SKILL.md` under the template `skill://{+path}`; also ships in `skills/` for plugin installs and the `.mcpb` bundle
|
|
79
|
+
|
|
67
80
|
## Features
|
|
68
81
|
|
|
69
82
|
Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports, pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.
|
|
@@ -85,133 +98,32 @@ Agent-friendly output:
|
|
|
85
98
|
|
|
86
99
|
## Getting started
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
A public instance is available at `https://cpsc-recalls.caseyjhand.com/mcp` — no installation required. It serves Streamable HTTP; each client takes its own configuration shape.
|
|
91
|
-
|
|
92
|
-
For Claude Code:
|
|
101
|
+
Connect to the hosted instance (Streamable HTTP, stateless), no install:
|
|
93
102
|
|
|
94
103
|
```sh
|
|
95
104
|
claude mcp add --transport http cpsc-recalls-mcp-server https://cpsc-recalls.caseyjhand.com/mcp
|
|
96
105
|
```
|
|
97
106
|
|
|
98
|
-
|
|
107
|
+
Or run it locally:
|
|
99
108
|
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
"mcpServers": {
|
|
103
|
-
"cpsc-recalls-mcp-server": {
|
|
104
|
-
"type": "streamableHttp",
|
|
105
|
-
"url": "https://cpsc-recalls.caseyjhand.com/mcp"
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
For Cursor (`mcp.json`):
|
|
112
|
-
|
|
113
|
-
```json
|
|
114
|
-
{
|
|
115
|
-
"mcpServers": {
|
|
116
|
-
"cpsc-recalls-mcp-server": {
|
|
117
|
-
"url": "https://cpsc-recalls.caseyjhand.com/mcp"
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
Claude Desktop adds remote servers under Settings → Connectors.
|
|
124
|
-
|
|
125
|
-
### Self-Hosted / Local
|
|
126
|
-
|
|
127
|
-
Add the following to your MCP client configuration file.
|
|
128
|
-
|
|
129
|
-
```json
|
|
130
|
-
{
|
|
131
|
-
"mcpServers": {
|
|
132
|
-
"cpsc-recalls-mcp-server": {
|
|
133
|
-
"type": "stdio",
|
|
134
|
-
"command": "bunx",
|
|
135
|
-
"args": ["@cyanheads/cpsc-recalls-mcp-server@latest"],
|
|
136
|
-
"env": {
|
|
137
|
-
"MCP_TRANSPORT_TYPE": "stdio",
|
|
138
|
-
"MCP_LOG_LEVEL": "info"
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
109
|
+
```sh
|
|
110
|
+
claude mcp add cpsc-recalls-mcp-server -- npx -y @cyanheads/cpsc-recalls-mcp-server
|
|
143
111
|
```
|
|
144
112
|
|
|
145
|
-
|
|
113
|
+
For other clients, add this to the MCP config file:
|
|
146
114
|
|
|
147
115
|
```json
|
|
148
116
|
{
|
|
149
117
|
"mcpServers": {
|
|
150
118
|
"cpsc-recalls-mcp-server": {
|
|
151
|
-
"type": "stdio",
|
|
152
119
|
"command": "npx",
|
|
153
|
-
"args": ["-y", "@cyanheads/cpsc-recalls-mcp-server
|
|
154
|
-
"env": {
|
|
155
|
-
"MCP_TRANSPORT_TYPE": "stdio",
|
|
156
|
-
"MCP_LOG_LEVEL": "info"
|
|
157
|
-
}
|
|
120
|
+
"args": ["-y", "@cyanheads/cpsc-recalls-mcp-server"]
|
|
158
121
|
}
|
|
159
122
|
}
|
|
160
123
|
}
|
|
161
124
|
```
|
|
162
125
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
```json
|
|
166
|
-
{
|
|
167
|
-
"mcpServers": {
|
|
168
|
-
"cpsc-recalls-mcp-server": {
|
|
169
|
-
"type": "stdio",
|
|
170
|
-
"command": "docker",
|
|
171
|
-
"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/cpsc-recalls-mcp-server:latest"]
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
For Streamable HTTP, set the transport and start the server:
|
|
178
|
-
|
|
179
|
-
```sh
|
|
180
|
-
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
|
|
181
|
-
# Server listens at http://localhost:3010/mcp
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### Prerequisites
|
|
185
|
-
|
|
186
|
-
- [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
|
|
187
|
-
- No API key required — the CPSC public recalls API is freely accessible.
|
|
188
|
-
|
|
189
|
-
### Installation
|
|
190
|
-
|
|
191
|
-
1. **Clone the repository:**
|
|
192
|
-
|
|
193
|
-
```sh
|
|
194
|
-
git clone https://github.com/cyanheads/cpsc-recalls-mcp-server.git
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
2. **Navigate into the directory:**
|
|
198
|
-
|
|
199
|
-
```sh
|
|
200
|
-
cd cpsc-recalls-mcp-server
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
3. **Install dependencies:**
|
|
204
|
-
|
|
205
|
-
```sh
|
|
206
|
-
bun install
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
4. **Configure environment:**
|
|
210
|
-
|
|
211
|
-
```sh
|
|
212
|
-
cp .env.example .env
|
|
213
|
-
# edit .env if needed — no required API keys
|
|
214
|
-
```
|
|
126
|
+
No API key is required — the CPSC public recalls API is freely accessible. For Docker or a source build, see [Running the server](#running-the-server).
|
|
215
127
|
|
|
216
128
|
## Configuration
|
|
217
129
|
|
|
@@ -238,6 +150,16 @@ No server-specific API keys are required. See [`.env.example`](./.env.example) f
|
|
|
238
150
|
|
|
239
151
|
### Local development
|
|
240
152
|
|
|
153
|
+
Requires [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+). No API key is needed.
|
|
154
|
+
|
|
155
|
+
- **Clone and install:**
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
git clone https://github.com/cyanheads/cpsc-recalls-mcp-server.git
|
|
159
|
+
cd cpsc-recalls-mcp-server && bun install
|
|
160
|
+
cp .env.example .env # optional — no required API keys
|
|
161
|
+
```
|
|
162
|
+
|
|
241
163
|
- **Build and run the production version:**
|
|
242
164
|
|
|
243
165
|
```sh
|
|
@@ -250,6 +172,12 @@ No server-specific API keys are required. See [`.env.example`](./.env.example) f
|
|
|
250
172
|
bun run start:http
|
|
251
173
|
```
|
|
252
174
|
|
|
175
|
+
For Streamable HTTP, set the transport and port; the server listens at `http://localhost:3010/mcp`:
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
|
|
179
|
+
```
|
|
180
|
+
|
|
253
181
|
- **Run checks and tests:**
|
|
254
182
|
|
|
255
183
|
```sh
|
|
@@ -264,13 +192,16 @@ docker build -t cpsc-recalls-mcp-server .
|
|
|
264
192
|
docker run --rm -p 3010:3010 cpsc-recalls-mcp-server
|
|
265
193
|
```
|
|
266
194
|
|
|
195
|
+
To run the published image over stdio from an MCP client, use `"command": "docker"` with `"args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/cpsc-recalls-mcp-server:latest"]`.
|
|
196
|
+
|
|
267
197
|
The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `/var/log/cpsc-recalls-mcp-server`. OpenTelemetry peer dependencies are installed by default — build with `--build-arg OTEL_ENABLED=false` to omit them.
|
|
268
198
|
|
|
269
199
|
## Project structure
|
|
270
200
|
|
|
271
201
|
| Directory | Purpose |
|
|
272
202
|
|:---|:---|
|
|
273
|
-
| `src/index.ts` | `createApp()` entry point — registers tools and inits the CPSC service |
|
|
203
|
+
| `src/index.ts` | `createApp()` entry point — registers tools, serves the published skill from `skills/`, and inits the CPSC service |
|
|
204
|
+
| `skills/` | `cpsc-recalls-guide/SKILL.md`, the published agent skill |
|
|
274
205
|
| `src/mcp-server/tools` | Tool definitions (`definitions/*.tool.ts`) — search, get-recall, get-recent — plus `response-budget.ts`, the page size budget the two list tools share, and `markdown-escape.ts`, the escaping applied to CPSC text in `content[]` |
|
|
275
206
|
| `src/services/cpsc-recall` | CPSC recall service — API client, types, and `normalize-text.ts`, which converts CPSC's HTML markup and character codes to plain text |
|
|
276
207
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
summary: "Publishes the cpsc-recalls-guide Agent Skill as an MCP resource; mcp-ts-core moves to 0.13.18, which renders the search query echo as a code span and stops the HTTP transport from retaining memory on every request."
|
|
3
|
+
breaking: false
|
|
4
|
+
security: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 0.3.0 — 2026-10-28
|
|
8
|
+
|
|
9
|
+
## Added
|
|
10
|
+
|
|
11
|
+
- **`cpsc-recalls-guide` skill** served over MCP as the resource `skill://cpsc-recalls-guide/SKILL.md` (template `skill://{+path}`) via `createApp({ skills: 'skills' })`, and shipped in the npm package, the Docker image, and the `.mcpb`. It covers search-formulation gates, recall-number and identifier quirks, jurisdiction edges, and recall-checking workflows.
|
|
12
|
+
- **Agent Plugins** root `plugin.json` and `mcp.json` (mcp-ts-core 0.13.18 template).
|
|
13
|
+
|
|
14
|
+
## Changed
|
|
15
|
+
|
|
16
|
+
- **`cpsc_search_recalls` `content[]`**: the `Query:` echo line renders as a code span instead of Markdown-escaped text, e.g. `` Query: `product_name="stroller" AND date_start=2025-01-01 AND date_end=2025-12-31` `` (mcp-ts-core 0.13.18). `structuredContent` is unchanged.
|
|
17
|
+
- **64,000-byte page budget** reserves room for the code-span echo and per-line notice quoting, so a cut page stays within budget for any echoed value.
|
|
18
|
+
- **README Getting started** is reorganized: hosted `claude mcp add`, local npx, one JSON config, with install and prerequisites under Running the server.
|
|
19
|
+
- **`.mcpb` bundle** leaves out repo-local files (root-anchored `.mcpbignore` from the 0.13.18 template) and the framework's build-only files (0.13.18 cleaner). It keeps the MCP SDK's skills runtime modules, which the 0.13.18 cleaner would otherwise strip (cyanheads/mcp-ts-core#794).
|
|
20
|
+
|
|
21
|
+
## Fixed
|
|
22
|
+
|
|
23
|
+
- **HTTP transport** no longer retains about 71 KiB per request, so sustained traffic no longer grows the heap. 0.2.3 shipped on mcp-ts-core 0.13.16, which leaked it through the shutdown signal; fixed in mcp-ts-core 0.13.17 (cyanheads/mcp-ts-core#786).
|
|
24
|
+
|
|
25
|
+
## Dependencies
|
|
26
|
+
|
|
27
|
+
- `@cyanheads/mcp-ts-core` ^0.13.16 → ^0.13.18.
|
|
28
|
+
- `@modelcontextprotocol/server` 2.3.1 → 2.4.0 (through the framework, 0.13.17).
|
|
29
|
+
- `@biomejs/biome` ^2.6.2 → ^2.6.3.
|
|
30
|
+
- `ignore` ^7.0.13 → ^7.0.14.
|
|
31
|
+
- `tsc-alias` ^1.9.8 → ^1.9.9.
|
|
32
|
+
- `minimumReleaseAge` 3 days → 1 day.
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ await createApp({
|
|
|
20
20
|
tools: [cpscSearchRecalls, cpscGetRecall, cpscGetRecent],
|
|
21
21
|
resources: [],
|
|
22
22
|
prompts: [],
|
|
23
|
+
skills: 'skills',
|
|
23
24
|
instructions: 'CPSC consumer product recalls from saferproducts.gov. Search with cpsc_search_recalls, which needs at least one filter (a product, brand, hazard, or other text filter, or a date bound); browse the latest recalls with cpsc_get_recent, and pass a recall number from either to cpsc_get_recall for the full record. CPSC jurisdiction: consumer products only — food/drugs (FDA), motor vehicles/tires (NHTSA), boats (USCG), pesticides (EPA), and firearms (ATF) are covered by other agencies.',
|
|
24
25
|
setup(core) {
|
|
25
26
|
void core;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,wDAAwD,CAAC;AACvF,OAAO,EAAE,aAAa,EAAE,MAAM,wDAAwD,CAAC;AACvF,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAC;AAEtF,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,yBAAyB;IAC/B,KAAK,EAAE,yBAAyB;IAChC;;;;OAIG;IACH,WAAW,EAAE,WAAW;IACxB,KAAK,EAAE,CAAC,iBAAiB,EAAE,aAAa,EAAE,aAAa,CAAC;IACxD,SAAS,EAAE,EAAE;IACb,OAAO,EAAE,EAAE;IACX,YAAY,EACV,seAAse;IACxe,KAAK,CAAC,IAAI;QACR,KAAK,IAAI,CAAC;QACV,qBAAqB,EAAE,CAAC;IAC1B,CAAC;CACF,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,wDAAwD,CAAC;AACvF,OAAO,EAAE,aAAa,EAAE,MAAM,wDAAwD,CAAC;AACvF,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAC;AAEtF,MAAM,SAAS,CAAC;IACd,IAAI,EAAE,yBAAyB;IAC/B,KAAK,EAAE,yBAAyB;IAChC;;;;OAIG;IACH,WAAW,EAAE,WAAW;IACxB,KAAK,EAAE,CAAC,iBAAiB,EAAE,aAAa,EAAE,aAAa,CAAC;IACxD,SAAS,EAAE,EAAE;IACb,OAAO,EAAE,EAAE;IACX,MAAM,EAAE,QAAQ;IAChB,YAAY,EACV,seAAse;IACxe,KAAK,CAAC,IAAI;QACR,KAAK,IAAI,CAAC;QACV,qBAAqB,EAAE,CAAC;IAC1B,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -24,9 +24,13 @@ export declare function countWithinBudget<T>(records: readonly T[], render: (rec
|
|
|
24
24
|
* (records emptied, enrichment merged in) and the rendered non-record text plus the
|
|
25
25
|
* enrichment trailer the framework appends to `content[]`.
|
|
26
26
|
*
|
|
27
|
-
* The trailer is a leading blank line, then
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* The trailer is its own text block: a leading blank line, then one field per enrichment
|
|
28
|
+
* value. A `notice` renders every line behind `> `; an `echo` renders `Query: ` and a
|
|
29
|
+
* generic field `**key:** ` before the value as a code literal — a code span, sized past
|
|
30
|
+
* the value's longest backtick run, or a fenced block on lines of its own when the value
|
|
31
|
+
* holds a line break, and nothing when it is empty. Fields after a quote are set off by a
|
|
32
|
+
* blank line, the rest by a line break. Each field is reserved its largest rendering, so
|
|
33
|
+
* the reservation holds whichever helper set it and whatever the value holds.
|
|
30
34
|
*/
|
|
31
35
|
export declare function pageOverheadBytes(skeleton: object, text: string, enrichment: Readonly<Record<string, string>>): number;
|
|
32
36
|
/** The notice for a page the budget ended before `requested` records. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response-budget.d.ts","sourceRoot":"","sources":["../../../src/mcp-server/tools/response-budget.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"response-budget.d.ts","sourceRoot":"","sources":["../../../src/mcp-server/tools/response-budget.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAS,CAAC;AAO5C;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,OAAO,EAAE,SAAS,CAAC,EAAE,EACrB,MAAM,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,EAC7B,aAAa,EAAE,MAAM,GACpB,MAAM,CASR;AAqCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC3C,MAAM,CASR;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/F"}
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* `structuredContent` and to the `content[]` text alike.
|
|
5
5
|
* @module mcp-server/tools/response-budget
|
|
6
6
|
*/
|
|
7
|
-
import { escapeMarkdownInline } from '@cyanheads/mcp-ts-core/utils';
|
|
8
7
|
/**
|
|
9
8
|
* UTF-8 byte cap on each response surface: the serialized `structuredContent`, and all
|
|
10
9
|
* `content[]` text including the enrichment trailer.
|
|
@@ -34,17 +33,55 @@ export function countWithinBudget(records, render, overheadBytes) {
|
|
|
34
33
|
}
|
|
35
34
|
return count;
|
|
36
35
|
}
|
|
36
|
+
/** Length of the longest backtick run in `value`; 0 when it holds none. */
|
|
37
|
+
function longestBacktickRun(value) {
|
|
38
|
+
let longest = 0;
|
|
39
|
+
for (const [run] of value.matchAll(/`+/g))
|
|
40
|
+
longest = Math.max(longest, run.length);
|
|
41
|
+
return longest;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Upper bound on the bytes the framework renders for `value` as a code literal: nothing
|
|
45
|
+
* for the empty string; for one line, a code span delimited by a backtick run one longer
|
|
46
|
+
* than the value's longest, plus a padding space on each side; for a value holding a line
|
|
47
|
+
* break, a fenced block on lines of its own — a line break before and after, and a fence
|
|
48
|
+
* at least three backticks long (one longer than the value's longest run) on each side of
|
|
49
|
+
* the value's own line.
|
|
50
|
+
*/
|
|
51
|
+
function codeLiteralBytes(value) {
|
|
52
|
+
if (value === '')
|
|
53
|
+
return 0;
|
|
54
|
+
const run = longestBacktickRun(value);
|
|
55
|
+
return /[\r\n]/.test(value)
|
|
56
|
+
? utf8Bytes(value) + 2 * Math.max(3, run + 1) + 4
|
|
57
|
+
: utf8Bytes(value) + 2 * (run + 1) + 2;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Upper bound on the bytes the framework's enrichment trailer renders for one field,
|
|
61
|
+
* separator included: the larger of its renderings as a `notice` (every line behind `> `)
|
|
62
|
+
* and as an `echo` (`Query: `) or generic (`**key:** `) line carrying the value as a code
|
|
63
|
+
* literal, plus the two-byte line break or blank line set before it.
|
|
64
|
+
*/
|
|
65
|
+
function trailerFieldBytes(key, value) {
|
|
66
|
+
const lines = 1 + (value.match(/\r\n|\r|\n/g)?.length ?? 0);
|
|
67
|
+
const label = Math.max(utf8Bytes('Query: '), utf8Bytes(`**${key}:** `));
|
|
68
|
+
return 2 + Math.max(utf8Bytes(value) + 2 * lines, label + codeLiteralBytes(value));
|
|
69
|
+
}
|
|
37
70
|
/**
|
|
38
71
|
* Bytes a page spends outside its records: the larger of the `structuredContent` skeleton
|
|
39
72
|
* (records emptied, enrichment merged in) and the rendered non-record text plus the
|
|
40
73
|
* enrichment trailer the framework appends to `content[]`.
|
|
41
74
|
*
|
|
42
|
-
* The trailer is a leading blank line, then
|
|
43
|
-
*
|
|
44
|
-
*
|
|
75
|
+
* The trailer is its own text block: a leading blank line, then one field per enrichment
|
|
76
|
+
* value. A `notice` renders every line behind `> `; an `echo` renders `Query: ` and a
|
|
77
|
+
* generic field `**key:** ` before the value as a code literal — a code span, sized past
|
|
78
|
+
* the value's longest backtick run, or a fenced block on lines of its own when the value
|
|
79
|
+
* holds a line break, and nothing when it is empty. Fields after a quote are set off by a
|
|
80
|
+
* blank line, the rest by a line break. Each field is reserved its largest rendering, so
|
|
81
|
+
* the reservation holds whichever helper set it and whatever the value holds.
|
|
45
82
|
*/
|
|
46
83
|
export function pageOverheadBytes(skeleton, text, enrichment) {
|
|
47
|
-
const trailer = Object.
|
|
84
|
+
const trailer = Object.entries(enrichment).reduce((sum, [key, value]) => sum + trailerFieldBytes(key, value), 0);
|
|
48
85
|
return Math.max(utf8Bytes(JSON.stringify({ ...skeleton, ...enrichment })), utf8Bytes(text) + trailer);
|
|
49
86
|
}
|
|
50
87
|
/** The notice for a page the budget ended before `requested` records. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response-budget.js","sourceRoot":"","sources":["../../../src/mcp-server/tools/response-budget.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH
|
|
1
|
+
{"version":3,"file":"response-budget.js","sourceRoot":"","sources":["../../../src/mcp-server/tools/response-budget.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAE5C,mCAAmC;AACnC,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAqB,EACrB,MAA6B,EAC7B,aAAqB;IAErB,IAAI,IAAI,GAAG,aAAa,CAAC;IACzB,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACnF,IAAI,KAAK,GAAG,CAAC,IAAI,IAAI,GAAG,qBAAqB;YAAE,MAAM;QACrD,KAAK,EAAE,CAAC;IACV,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,2EAA2E;AAC3E,SAAS,kBAAkB,CAAC,KAAa;IACvC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnF,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,gBAAgB,CAAC,KAAa;IACrC,IAAI,KAAK,KAAK,EAAE;QAAE,OAAO,CAAC,CAAC;IAC3B,MAAM,GAAG,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACtC,OAAO,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC;QACzB,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;QACjD,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,GAAW,EAAE,KAAa;IACnD,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC,CAAC,CAAC;IAC5D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC;IACxE,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,EAAE,KAAK,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC;AACrF,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAAgB,EAChB,IAAY,EACZ,UAA4C;IAE5C,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,MAAM,CAC/C,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,iBAAiB,CAAC,GAAG,EAAE,KAAK,CAAC,EAC1D,CAAC,CACF,CAAC;IACF,OAAO,IAAI,CAAC,GAAG,CACb,SAAS,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,QAAQ,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC,EACzD,SAAS,CAAC,IAAI,CAAC,GAAG,OAAO,CAC1B,CAAC;AACJ,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,SAAiB,EAAE,UAAkB;IACrF,OAAO,YAAY,QAAQ,WAAW,SAAS,6CAA6C,qBAAqB,CAAC,cAAc,CAAC,OAAO,CAAC,oDAAoD,UAAU,gBAAgB,CAAC;AAC1N,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/cpsc-recalls-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"mcpName": "io.github.cyanheads/cpsc-recalls-mcp-server",
|
|
5
5
|
"description": "Search and retrieve US consumer product recalls from the CPSC (Consumer Product Safety Commission) via MCP. STDIO or Streamable HTTP.",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"CLAUDE.md",
|
|
19
19
|
"AGENTS.md",
|
|
20
20
|
"Dockerfile",
|
|
21
|
-
"server.json"
|
|
21
|
+
"server.json",
|
|
22
|
+
"skills/"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
24
25
|
"build": "bun run scripts/build.ts",
|
|
@@ -87,19 +88,19 @@
|
|
|
87
88
|
"access": "public"
|
|
88
89
|
},
|
|
89
90
|
"dependencies": {
|
|
90
|
-
"@cyanheads/mcp-ts-core": "^0.13.
|
|
91
|
+
"@cyanheads/mcp-ts-core": "^0.13.18",
|
|
91
92
|
"pino-pretty": "^13.2.0",
|
|
92
93
|
"zod": "^4.6.7"
|
|
93
94
|
},
|
|
94
95
|
"devDependencies": {
|
|
95
|
-
"@biomejs/biome": "^2.6.
|
|
96
|
+
"@biomejs/biome": "^2.6.3",
|
|
96
97
|
"@socketsecurity/bun-security-scanner": "^1.1.3",
|
|
97
98
|
"@types/markdown-it": "^14.2.0",
|
|
98
99
|
"@types/node": "^26.6.5",
|
|
99
100
|
"depcheck": "^1.4.7",
|
|
100
|
-
"ignore": "^7.0.
|
|
101
|
+
"ignore": "^7.0.14",
|
|
101
102
|
"markdown-it": "^15.0.3",
|
|
102
|
-
"tsc-alias": "^1.9.
|
|
103
|
+
"tsc-alias": "^1.9.9",
|
|
103
104
|
"typescript": "^7.0.2",
|
|
104
105
|
"vitest": "^5.0.5"
|
|
105
106
|
}
|
package/server.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"url": "https://github.com/cyanheads/cpsc-recalls-mcp-server",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.3.0",
|
|
10
10
|
"remotes": [
|
|
11
11
|
{
|
|
12
12
|
"type": "streamable-http",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
20
20
|
"identifier": "@cyanheads/cpsc-recalls-mcp-server",
|
|
21
21
|
"runtimeHint": "npx",
|
|
22
|
-
"version": "0.
|
|
22
|
+
"version": "0.3.0",
|
|
23
23
|
"environmentVariables": [
|
|
24
24
|
{
|
|
25
25
|
"name": "MCP_LOG_LEVEL",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
39
39
|
"identifier": "@cyanheads/cpsc-recalls-mcp-server",
|
|
40
40
|
"runtimeHint": "npx",
|
|
41
|
-
"version": "0.
|
|
41
|
+
"version": "0.3.0",
|
|
42
42
|
"environmentVariables": [
|
|
43
43
|
{
|
|
44
44
|
"name": "MCP_TRANSPORT_TYPE",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cpsc-recalls-guide
|
|
3
|
+
description: Find and confirm US consumer product recalls with the cpsc_* tools. Use when checking whether a specific product, model, serial, or UPC was recalled, sweeping a brand's or retailer's recall history, monitoring new or republished recalls for a hazard, or deciding whether a product is CPSC's at all. Covers search formulation, recall-number quirks, and when an empty result proves nothing.
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
metadata:
|
|
6
|
+
author: cyanheads
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Scope
|
|
11
|
+
|
|
12
|
+
CPSC recall records from saferproducts.gov, 1973 to the present, keyless. The server reads recall records only, so an empty result means no recall matched, never that a product is safe.
|
|
13
|
+
|
|
14
|
+
The jurisdiction note on every successful response names the agencies that own food, drugs, on-road vehicles, boats, pesticides, and firearms. CPSC still holds recalls for products next to those lines: drugs and dietary supplements recalled for failing child-resistant packaging rules, all-terrain vehicles and snowmobiles, e-bikes and bicycle tires, gun safes and locks, and car-seat adapters and accessories (some car seat recalls were announced jointly with NHTSA). Search here before routing such a product away. For a car seat or a tire, check the vehicle-safety agency's recalls as well.
|
|
15
|
+
|
|
16
|
+
## Identifiers
|
|
17
|
+
|
|
18
|
+
- **Recall number.** Comes from cpsc_search_recalls or cpsc_get_recent. Pass five digits with no separator; a hyphenated form such as `25-043` fails validation. The first two digits are almost always the federal fiscal year the recall was issued in, which starts October 1 (`27xxx` numbers begin in October 2026, `03064` was issued in late 2002). Use that to bound a date search when a number is all you have.
|
|
19
|
+
- **Lettered numbers.** Some 1998–2001 releases are split into lettered records (`01160a`, `01160b`, `01160c`) that are unrelated recalls issued the same day. The bare base number is not a record, and cpsc_get_recall on it returns one of the lettered siblings instead of failing. Check that the returned recall number equals the one you asked for; to see every sibling, search with date_start and date_end both set to that day.
|
|
20
|
+
- **Model numbers, serials, UPCs.** No filter matches them as fields. They live in the description text that cpsc_get_recall returns; see the unit-level gate below.
|
|
21
|
+
|
|
22
|
+
## Gates
|
|
23
|
+
|
|
24
|
+
1. **One empty search is not "not recalled".** Before telling a user no recall exists, run title_search with brand plus product, then a company-role filter (manufacturer, importer, or retailer) with product_name, and merge the two result sets by recall number. They diverge: for Graco strollers, title_search found a recall where Graco is listed only as importer, and the role search found a recall whose title names activity trays while a product name says stroller. Report "no CPSC recall found" and the searches you ran, never "safe".
|
|
25
|
+
|
|
26
|
+
2. **Matching is substring, with no stemming.** A filter word matches inside longer words and misses other inflections. `fire` returns firearm safes, fireworks, and fireplace products; `burn` matches "burner"; `battery` misses hazard text that only says "batteries". Search the shortest distinctive stem (`batter`, `ingest`, `strangul`) and read each hit's hazard text before counting it toward a hazard class.
|
|
27
|
+
|
|
28
|
+
3. **hazard_search never reads the title, and the hazard field can be wrong.** One toy recall titled for battery ingestion carries "None reported." as its hazard text, and its data_quality_notes are empty, because those notes flag only fields CPSC left blank. For a hazard sweep, run the same stem through title_search too and merge by recall number.
|
|
29
|
+
|
|
30
|
+
4. **Pair hazard_search with something narrower.** A hazard_search with no other criterion downloads the whole recall history before filtering, which can take many seconds when CPSC's cache is cold. Add a date bound or a product, title, or company filter when the question allows.
|
|
31
|
+
|
|
32
|
+
5. **Unit-level checks happen by reading.** Descriptions state models, serials, and date codes as ranges or masks (`1HFVE05**K4000003` to `1HFVE05**K4003902`, `111522DXXXXXX`, serial runs like `3591767-3591769`), so a specific serial rarely matches as text. Search a model family or a distinctive digit run with description_search, open the candidates with cpsc_get_recall, and compare the user's unit against the stated ranges yourself.
|
|
33
|
+
|
|
34
|
+
6. **A UPC is findable only as the description prints it.** Most UPC-bearing descriptions repeat the code, but formatting varies: `082294319754` found nothing where the description prints `0-82294-319754`, while `319754` found it. Search a run of six or more trailing digits, then confirm the full code in the record.
|
|
35
|
+
|
|
36
|
+
7. **units_recalled is narrative, not a count.** It is approximate ("About 61,000"), often adds Canadian or Mexican sales in parentheses, and on an expanded or re-announced recall it can cite or already include units from the earlier recall ("About 569,000 (In August 2018, 105,000 of the 569,000 blending containers and blade bases were previously recalled)"). Read the parenthetical before summing across recalls, and present totals as approximate.
|
|
37
|
+
|
|
38
|
+
8. **Issue date and last-published date are separate axes.** cpsc_get_recent windows on issue date. A record CPSC republishes keeps its original issue date, so a recent window or an issue-date search misses it and updated_start and updated_end find it; 1998 recalls republished in October 2026 appear that way. Search rows don't carry the last-published date. cpsc_get_recall shows it as last_updated, but nothing says what changed.
|
|
39
|
+
|
|
40
|
+
## Workflows
|
|
41
|
+
|
|
42
|
+
**Is this product recalled?** Run gate 1's searches, narrow the candidates by title and issue date against when the user bought the product, open each with cpsc_get_recall, and compare model, serial, UPC, and the retailers' sale-date and price narrative against the user's unit (gates 5 and 6). Answer with the remedy text, the consumer contact, and cpsc_url so the user can verify.
|
|
43
|
+
|
|
44
|
+
**A company's or retailer's history.** The retailer filter matches CPSC's free-text sale narrative ("Amazon.com from March 2025 through August 2026 for about $13 to $14"), so it also reaches online marketplaces. For a brand, combine title_search with the manufacturer and importer filters (gate 1). Expansions and re-announcements are separate recalls with their own numbers, titled "Expands Recall" or "Reannounces"; title_search on brand plus product returns the original next to them.
|
|
45
|
+
|
|
46
|
+
**Tracing one recall's history.** Start from a title_search on brand plus product, not a model number. A model named in the original recall's description can appear in the expansion only inside units_recalled: `2551W` returns the original BISSELL wet-dry vacuum recall from January 2023 through description_search, while title_search "BISSELL Wet Dry" returns both it and the April 2023 expansion.
|
|
47
|
+
|
|
48
|
+
**Monitoring.** For a calendar window ("this month"), call cpsc_search_recalls with date_start and date_end; a date bound alone satisfies its criterion requirement, and it accepts a hazard or company filter. cpsc_get_recent covers only a rolling window ending today and takes no filters. Key stored results by recall number, and add an updated_start pass to catch records CPSC republishes after the fact.
|