@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.
Files changed (28) hide show
  1. package/AGENTS.md +29 -104
  2. package/CLAUDE.md +29 -104
  3. package/Dockerfile +17 -10
  4. package/README.md +46 -93
  5. package/changelog/0.2.x/0.2.3.md +29 -0
  6. package/changelog/0.3.x/0.3.0.md +32 -0
  7. package/dist/index.js +1 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/mcp-server/tools/definitions/cpsc-get-recall.tool.d.ts.map +1 -1
  10. package/dist/mcp-server/tools/definitions/cpsc-get-recall.tool.js +5 -4
  11. package/dist/mcp-server/tools/definitions/cpsc-get-recall.tool.js.map +1 -1
  12. package/dist/mcp-server/tools/definitions/cpsc-get-recent.tool.d.ts.map +1 -1
  13. package/dist/mcp-server/tools/definitions/cpsc-get-recent.tool.js +3 -2
  14. package/dist/mcp-server/tools/definitions/cpsc-get-recent.tool.js.map +1 -1
  15. package/dist/mcp-server/tools/definitions/cpsc-search-recalls.tool.d.ts.map +1 -1
  16. package/dist/mcp-server/tools/definitions/cpsc-search-recalls.tool.js +4 -3
  17. package/dist/mcp-server/tools/definitions/cpsc-search-recalls.tool.js.map +1 -1
  18. package/dist/mcp-server/tools/markdown-escape.d.ts +0 -14
  19. package/dist/mcp-server/tools/markdown-escape.d.ts.map +1 -1
  20. package/dist/mcp-server/tools/markdown-escape.js +0 -28
  21. package/dist/mcp-server/tools/markdown-escape.js.map +1 -1
  22. package/dist/mcp-server/tools/response-budget.d.ts +7 -3
  23. package/dist/mcp-server/tools/response-budget.d.ts.map +1 -1
  24. package/dist/mcp-server/tools/response-budget.js +42 -4
  25. package/dist/mcp-server/tools/response-budget.js.map +1 -1
  26. package/package.json +13 -10
  27. package/server.json +3 -3
  28. package/skills/cpsc-recalls-guide/SKILL.md +48 -0
@@ -12,18 +12,4 @@
12
12
  * Applied where upstream text is interpolated into `content[]`, never to `structuredContent`.
13
13
  */
14
14
  export declare function escapeMarkdown(text: string): string;
15
- /**
16
- * `url` as a Markdown link destination. A bare destination ends at the first space and
17
- * cannot hold `<`, `>`, or an unbalanced parenthesis, so those are percent-encoded. A
18
- * destination also reads backslash escapes and character references, so a backslash and an
19
- * `&` opening a reference are backslash-escaped, and the link points at `url` as stored.
20
- */
21
- export declare function linkDestination(url: string): string;
22
- /**
23
- * `url` shown as a bare address. A web address has its delimiters percent-encoded, and a
24
- * trailing `.`, `_`, or `~` — which GFM leaves out of an autolink — is percent-encoded too,
25
- * so the autolink covers the whole address; encoding those unreserved characters names the
26
- * same address. A value that is not a web address is escaped as text.
27
- */
28
- export declare function bareAddress(url: string): string;
29
15
  //# sourceMappingURL=markdown-escape.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-escape.d.ts","sourceRoot":"","sources":["../../../src/mcp-server/tools/markdown-escape.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA+CH;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAwBnD;AAYD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAM/C"}
1
+ {"version":3,"file":"markdown-escape.d.ts","sourceRoot":"","sources":["../../../src/mcp-server/tools/markdown-escape.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA+CH;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAwBnD"}
@@ -62,32 +62,4 @@ export function escapeMarkdown(text) {
62
62
  ? `${lead}${indent}${digits}\\${delimiter}`
63
63
  : `${lead}${indent}\\${marker}`);
64
64
  }
65
- /**
66
- * `url` with whitespace, `<`, `>`, and parentheses percent-encoded as UTF-8 — the
67
- * characters that end or unbalance an address in Markdown. The address they name is the same.
68
- */
69
- function encodeDelimiters(url) {
70
- return url.replace(/[\s<>()]/gu, (char) => char === '(' ? '%28' : char === ')' ? '%29' : encodeURIComponent(char));
71
- }
72
- /**
73
- * `url` as a Markdown link destination. A bare destination ends at the first space and
74
- * cannot hold `<`, `>`, or an unbalanced parenthesis, so those are percent-encoded. A
75
- * destination also reads backslash escapes and character references, so a backslash and an
76
- * `&` opening a reference are backslash-escaped, and the link points at `url` as stored.
77
- */
78
- export function linkDestination(url) {
79
- return encodeDelimiters(url).replace(/\\|&(?=#?[A-Za-z\d]{1,32};)/g, '\\$&');
80
- }
81
- /**
82
- * `url` shown as a bare address. A web address has its delimiters percent-encoded, and a
83
- * trailing `.`, `_`, or `~` — which GFM leaves out of an autolink — is percent-encoded too,
84
- * so the autolink covers the whole address; encoding those unreserved characters names the
85
- * same address. A value that is not a web address is escaped as text.
86
- */
87
- export function bareAddress(url) {
88
- if (!/^https?:\/\//.test(url))
89
- return escapeMarkdown(url);
90
- const destination = encodeDelimiters(url).replace(/[._~]+$/, (run) => run.replace(/./g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`));
91
- return escapeMarkdown(destination);
92
- }
93
65
  //# sourceMappingURL=markdown-escape.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-escape.js","sourceRoot":"","sources":["../../../src/mcp-server/tools/markdown-escape.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"markdown-escape.js","sourceRoot":"","sources":["../../../src/mcp-server/tools/markdown-escape.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;GAMG;AACH,MAAM,OAAO,GACX,6HAA6H,CAAC;AAEhI,mEAAmE;AACnE,MAAM,OAAO,GAAG,gEAAgE,CAAC;AAEjF;;;;GAIG;AACH,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,CAAC;AAEzE;;;;;;GAMG;AACH,MAAM,UAAU,GAAG,wDAAwD,CAAC;AAE5E;;;;GAIG;AACH,SAAS,SAAS,CAAC,OAAe,EAAE,MAA0B;IAC5D,IAAI,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACpF,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,0BAA0B,EAAE,EAAE,CAAC,CAAC;IACzF,OAAO,CAAC,IAAI;SACT,KAAK,CAAC,GAAG,CAAC;SACV,KAAK,CAAC,CAAC,CAAC,CAAC;SACT,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI;SACR,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC;SACvB,OAAO,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,OAA2B,EAAE,MAAc,EAAE,KAAa,EAAE,EAAE,CAC5F,OAAO,KAAK,SAAS;QACnB,CAAC,CAAC,KAAK,IAAI,EAAE;QACb,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACrC,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,CACvC;SACA,OAAO,CACN,UAAU,EACV,CACE,KAAK,EACL,IAAY,EACZ,MAAc,EACd,MAAe,EACf,MAAe,EACf,SAAkB,EAClB,EAAE,CACF,MAAM,KAAK,SAAS;QAClB,CAAC,CAAC,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,KAAK,SAAS,EAAE;QAC3C,CAAC,CAAC,GAAG,IAAI,GAAG,MAAM,KAAK,MAAM,EAAE,CACpC,CAAC;AACN,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 each field behind a label of at most seven
28
- * bytes (`Query: `, `> `), separated from the next by at most a blank line — so each field
29
- * is reserved nine bytes beyond its value.
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;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;AAED;;;;;;;;GAQG;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,CAMR;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/F"}
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"}
@@ -33,17 +33,55 @@ export function countWithinBudget(records, render, overheadBytes) {
33
33
  }
34
34
  return count;
35
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
+ }
36
70
  /**
37
71
  * Bytes a page spends outside its records: the larger of the `structuredContent` skeleton
38
72
  * (records emptied, enrichment merged in) and the rendered non-record text plus the
39
73
  * enrichment trailer the framework appends to `content[]`.
40
74
  *
41
- * The trailer is a leading blank line, then each field behind a label of at most seven
42
- * bytes (`Query: `, `> `), separated from the next by at most a blank line — so each field
43
- * is reserved nine bytes beyond its value.
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.
44
82
  */
45
83
  export function pageOverheadBytes(skeleton, text, enrichment) {
46
- const trailer = Object.values(enrichment).reduce((sum, value) => sum + utf8Bytes(value) + 9, 2);
84
+ const trailer = Object.entries(enrichment).reduce((sum, [key, value]) => sum + trailerFieldBytes(key, value), 0);
47
85
  return Math.max(utf8Bytes(JSON.stringify({ ...skeleton, ...enrichment })), utf8Bytes(text) + trailer);
48
86
  }
49
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;;;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;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAAgB,EAChB,IAAY,EACZ,UAA4C;IAE5C,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAChG,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"}
1
+ {"version":3,"file":"response-budget.js","sourceRoot":"","sources":["../../../src/mcp-server/tools/response-budget.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/cpsc-recalls-mcp-server",
3
- "version": "0.2.2",
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",
@@ -78,7 +79,7 @@
78
79
  }
79
80
  ],
80
81
  "license": "Apache-2.0",
81
- "packageManager": "bun@1.4.2",
82
+ "packageManager": "bun@1.4.4",
82
83
  "engines": {
83
84
  "bun": ">=1.4.0",
84
85
  "node": ">=24.0.0"
@@ -87,18 +88,20 @@
87
88
  "access": "public"
88
89
  },
89
90
  "dependencies": {
90
- "@cyanheads/mcp-ts-core": "^0.13.14",
91
+ "@cyanheads/mcp-ts-core": "^0.13.18",
91
92
  "pino-pretty": "^13.2.0",
92
- "zod": "^4.6.5"
93
+ "zod": "^4.6.7"
93
94
  },
94
95
  "devDependencies": {
95
- "@biomejs/biome": "^2.5.15",
96
+ "@biomejs/biome": "^2.6.3",
96
97
  "@socketsecurity/bun-security-scanner": "^1.1.3",
97
- "@types/node": "^26.6.4",
98
+ "@types/markdown-it": "^14.2.0",
99
+ "@types/node": "^26.6.5",
98
100
  "depcheck": "^1.4.7",
99
- "ignore": "^7.0.12",
100
- "tsc-alias": "^1.9.7",
101
+ "ignore": "^7.0.14",
102
+ "markdown-it": "^15.0.3",
103
+ "tsc-alias": "^1.9.9",
101
104
  "typescript": "^7.0.2",
102
- "vitest": "^5.0.3"
105
+ "vitest": "^5.0.5"
103
106
  }
104
107
  }
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.2.2",
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.2.2",
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.2.2",
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.