@cyanheads/cpsc-recalls-mcp-server 0.1.1

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 (34) hide show
  1. package/AGENTS.md +372 -0
  2. package/CLAUDE.md +372 -0
  3. package/Dockerfile +99 -0
  4. package/LICENSE +201 -0
  5. package/README.md +266 -0
  6. package/changelog/0.1.x/0.1.0.md +11 -0
  7. package/changelog/0.1.x/0.1.1.md +26 -0
  8. package/changelog/template.md +127 -0
  9. package/dist/index.d.ts +7 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +25 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/mcp-server/tools/definitions/cpsc-get-recall.tool.d.ts +51 -0
  14. package/dist/mcp-server/tools/definitions/cpsc-get-recall.tool.d.ts.map +1 -0
  15. package/dist/mcp-server/tools/definitions/cpsc-get-recall.tool.js +239 -0
  16. package/dist/mcp-server/tools/definitions/cpsc-get-recall.tool.js.map +1 -0
  17. package/dist/mcp-server/tools/definitions/cpsc-get-recent.tool.d.ts +36 -0
  18. package/dist/mcp-server/tools/definitions/cpsc-get-recent.tool.d.ts.map +1 -0
  19. package/dist/mcp-server/tools/definitions/cpsc-get-recent.tool.js +147 -0
  20. package/dist/mcp-server/tools/definitions/cpsc-get-recent.tool.js.map +1 -0
  21. package/dist/mcp-server/tools/definitions/cpsc-search-recalls.tool.d.ts +54 -0
  22. package/dist/mcp-server/tools/definitions/cpsc-search-recalls.tool.d.ts.map +1 -0
  23. package/dist/mcp-server/tools/definitions/cpsc-search-recalls.tool.js +245 -0
  24. package/dist/mcp-server/tools/definitions/cpsc-search-recalls.tool.js.map +1 -0
  25. package/dist/services/cpsc-recall/cpsc-recall-service.d.ts +29 -0
  26. package/dist/services/cpsc-recall/cpsc-recall-service.d.ts.map +1 -0
  27. package/dist/services/cpsc-recall/cpsc-recall-service.js +97 -0
  28. package/dist/services/cpsc-recall/cpsc-recall-service.js.map +1 -0
  29. package/dist/services/cpsc-recall/types.d.ts +86 -0
  30. package/dist/services/cpsc-recall/types.d.ts.map +1 -0
  31. package/dist/services/cpsc-recall/types.js +6 -0
  32. package/dist/services/cpsc-recall/types.js.map +1 -0
  33. package/package.json +101 -0
  34. package/server.json +99 -0
@@ -0,0 +1,245 @@
1
+ /**
2
+ * @fileoverview Search CPSC consumer product recalls by product name, brand, retailer,
3
+ * hazard keyword, or date range.
4
+ * @module mcp-server/tools/definitions/cpsc-search-recalls
5
+ */
6
+ import { tool, z } from '@cyanheads/mcp-ts-core';
7
+ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
8
+ import { getCpscRecallService } from '../../../services/cpsc-recall/cpsc-recall-service.js';
9
+ /** Static jurisdiction note included in every response. */
10
+ const JURISDICTION = 'CPSC covers consumer products — toys, electronics, furniture, appliances, tools, clothing. ' +
11
+ 'Does NOT cover: food/drugs (FDA), motor vehicles/tires (NHTSA), boats (USCG), pesticides (EPA), firearms (ATF). ' +
12
+ 'For those categories, use the appropriate server.';
13
+ export const cpscSearchRecalls = tool('cpsc_search_recalls', {
14
+ title: 'Search CPSC Recalls',
15
+ description: 'Search consumer product recalls from the CPSC (Consumer Product Safety Commission) database. ' +
16
+ "Covers toys, electronics, furniture, appliances, children's products, tools, and clothing — everything under CPSC jurisdiction. " +
17
+ 'Does NOT cover food/drugs (FDA), motor vehicles/tires (NHTSA), boats (USCG), or pesticides (EPA). ' +
18
+ 'All filter fields are optional substring matches that combine with AND. ' +
19
+ 'For hazard-type filtering ("fire", "choking", "burn"), use description_search — the dedicated Hazard filter is non-functional in the upstream API. ' +
20
+ 'When manufacturer returns no results, try importer or retailer: many recalls list the importer or retailer as the primary responsible org. ' +
21
+ 'Use cpsc_get_recall with a recall_number from results to retrieve the full record including complete description, all images, and incident reports.',
22
+ annotations: { readOnlyHint: true, idempotentHint: true },
23
+ input: z.object({
24
+ product_name: z
25
+ .string()
26
+ .optional()
27
+ .describe('Product name to search for, e.g. "crib", "space heater", "bicycle". Substring match — partial names work.'),
28
+ manufacturer: z
29
+ .string()
30
+ .optional()
31
+ .describe('Manufacturer name, e.g. "Samsung", "LEGO". Substring match against the Manufacturers array. ' +
32
+ 'Note: many recalls list the importer or retailer as the primary org rather than the manufacturer — try importer or retailer if this returns no results.'),
33
+ retailer: z
34
+ .string()
35
+ .optional()
36
+ .describe('Retailer name, e.g. "Walmart", "Target", "Amazon". Substring match against the retailer narrative (which includes store name, dates sold, and price).'),
37
+ importer: z
38
+ .string()
39
+ .optional()
40
+ .describe('Importer company name. Use when searching for recalls by the company that brought the product into the US.'),
41
+ description_search: z
42
+ .string()
43
+ .optional()
44
+ .describe('Keyword search within the recall Description field only (does not search Title or Hazards text). ' +
45
+ 'Use for hazard types like "fire", "choking", "burn", or to find product details not captured in product_name. ' +
46
+ 'Note: hazard keywords often appear in the Description field — this is the correct filter for hazard-type searching since the Hazard filter param is non-functional upstream.'),
47
+ date_start: z
48
+ .union([
49
+ z.literal(''),
50
+ z
51
+ .string()
52
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
53
+ .describe('ISO 8601 date: "YYYY-MM-DD".'),
54
+ ])
55
+ .optional()
56
+ .describe('Include only recalls on or after this date. ISO 8601 format: "YYYY-MM-DD".'),
57
+ date_end: z
58
+ .union([
59
+ z.literal(''),
60
+ z
61
+ .string()
62
+ .regex(/^\d{4}-\d{2}-\d{2}$/)
63
+ .describe('ISO 8601 date: "YYYY-MM-DD".'),
64
+ ])
65
+ .optional()
66
+ .describe('Include only recalls on or before this date. ISO 8601 format: "YYYY-MM-DD".'),
67
+ limit: z
68
+ .number()
69
+ .int()
70
+ .min(1)
71
+ .max(200)
72
+ .default(20)
73
+ .describe('Maximum number of results to return (applied client-side — the API returns all matches). Defaults to 20.'),
74
+ }),
75
+ output: z.object({
76
+ recalls: z
77
+ .array(z
78
+ .object({
79
+ recall_number: z
80
+ .string()
81
+ .describe('Recall identifier (5-digit numeric for 2002+ records, e.g. "25043"; 6-char with letter suffix for 1998–2001 records, e.g. "99003a"). Pass to cpsc_get_recall for full detail.'),
82
+ recall_date: z.string().describe('Date the recall was issued, ISO 8601.'),
83
+ title: z.string().describe('Official recall title.'),
84
+ hazards: z
85
+ .array(z.string().describe('Hazard description — what is dangerous.'))
86
+ .describe('Hazard descriptions — what is dangerous about this product.'),
87
+ remedy_options: z
88
+ .array(z.string().describe('Remedy type.'))
89
+ .describe('Remedy types: Refund, Repair, Replace, Dispose, Label, New Instructions. Multiple may apply.'),
90
+ remedy_summary: z
91
+ .string()
92
+ .describe('Full remedy instructions — what the consumer should do and how to claim.'),
93
+ products: z
94
+ .array(z
95
+ .object({
96
+ name: z.string().describe('Product name.'),
97
+ units_recalled: z
98
+ .string()
99
+ .describe('Estimated number of units recalled, e.g. "About 2,500".'),
100
+ })
101
+ .describe('A product covered by this recall.'))
102
+ .describe('Products covered by this recall. A recall may cover multiple products.'),
103
+ upcs: z
104
+ .array(z.string().describe('UPC code.'))
105
+ .describe('UPC codes for this recall (sparse — ~4% of records have UPCs). ' +
106
+ 'UPCs are stored at the recall level in the API, not per-product; ' +
107
+ 'when a recall covers multiple products, all UPCs apply to the recall as a whole.'),
108
+ manufacturers: z
109
+ .array(z.string().describe('Manufacturer name.'))
110
+ .describe('Manufacturer names. Often empty — importer or retailer may be listed instead.'),
111
+ importers: z
112
+ .array(z.string().describe('Importer name.'))
113
+ .describe('Importer company names.'),
114
+ retailers: z
115
+ .array(z.string().describe('Retailer name and sale details.'))
116
+ .describe('Retailer names and sale details (narrative text including stores, dates, price range).'),
117
+ cpsc_url: z.string().describe('Official CPSC recall page URL for human verification.'),
118
+ images: z
119
+ .array(z
120
+ .object({
121
+ url: z.string().describe('Image URL.'),
122
+ caption: z.string().describe('Image caption.'),
123
+ })
124
+ .describe('An image from the recall notice.'))
125
+ .describe('Product images from the recall notice.'),
126
+ })
127
+ .describe('A CPSC recall record.'))
128
+ .describe('Matching recalls, ordered newest-first.'),
129
+ total_found: z.number().describe('Total matching records before the limit was applied.'),
130
+ truncated: z.boolean().describe('True when total_found exceeds the limit.'),
131
+ cpsc_jurisdiction: z
132
+ .string()
133
+ .describe('CPSC covers consumer products — toys, electronics, furniture, appliances, tools, clothing. ' +
134
+ 'Does NOT cover: food/drugs (FDA), motor vehicles/tires (NHTSA), boats (USCG), pesticides (EPA), firearms (ATF).'),
135
+ }),
136
+ errors: [
137
+ {
138
+ reason: 'no_results',
139
+ code: JsonRpcErrorCode.NotFound,
140
+ when: 'No recalls matched the search filters',
141
+ recovery: 'Broaden the search — try a shorter product name, fewer filters, or remove the date range. Check CPSC jurisdiction: food, vehicle, and drug recalls are not in this database.',
142
+ },
143
+ {
144
+ reason: 'upstream_error',
145
+ code: JsonRpcErrorCode.ServiceUnavailable,
146
+ when: 'The saferproducts.gov API returned an error or timed out',
147
+ recovery: 'The CPSC API is occasionally unavailable. Retry in a few seconds.',
148
+ retryable: true,
149
+ },
150
+ ],
151
+ async handler(input, ctx) {
152
+ ctx.log.info('Searching CPSC recalls', {
153
+ product_name: input.product_name,
154
+ manufacturer: input.manufacturer,
155
+ retailer: input.retailer,
156
+ importer: input.importer,
157
+ description_search: input.description_search,
158
+ date_start: input.date_start,
159
+ date_end: input.date_end,
160
+ limit: input.limit,
161
+ });
162
+ const svc = getCpscRecallService();
163
+ let raw;
164
+ try {
165
+ raw = await svc.search({
166
+ ...(input.product_name && { ProductName: input.product_name }),
167
+ ...(input.manufacturer && { Manufacturer: input.manufacturer }),
168
+ ...(input.retailer && { Retailer: input.retailer }),
169
+ ...(input.importer && { Importer: input.importer }),
170
+ ...(input.description_search && { RecallDescription: input.description_search }),
171
+ ...(input.date_start && { RecallDateStart: input.date_start }),
172
+ ...(input.date_end && { RecallDateEnd: input.date_end }),
173
+ }, ctx);
174
+ }
175
+ catch (err) {
176
+ throw ctx.fail('upstream_error', 'CPSC API request failed.', { ...ctx.recoveryFor('upstream_error') }, { cause: err });
177
+ }
178
+ if (raw.length === 0) {
179
+ throw ctx.fail('no_results', 'No recalls matched the search filters.', {
180
+ ...ctx.recoveryFor('no_results'),
181
+ });
182
+ }
183
+ const total_found = raw.length;
184
+ const slice = raw.slice(0, input.limit);
185
+ const truncated = total_found > input.limit;
186
+ const recalls = slice.map((r) => ({
187
+ recall_number: r.RecallNumber,
188
+ recall_date: r.RecallDate.slice(0, 10),
189
+ title: r.Title,
190
+ hazards: r.Hazards.map((h) => h.Name).filter(Boolean),
191
+ remedy_options: r.RemedyOptions.map((o) => o.Option).filter(Boolean),
192
+ remedy_summary: r.Remedies.map((rem) => rem.Name)
193
+ .filter(Boolean)
194
+ .join(' '),
195
+ products: r.Products.map((p) => ({
196
+ name: p.Name,
197
+ units_recalled: p.NumberOfUnits ?? '',
198
+ })),
199
+ upcs: r.ProductUPCs.map((u) => u.UPC).filter(Boolean),
200
+ manufacturers: r.Manufacturers.map((m) => m.Name).filter(Boolean),
201
+ importers: r.Importers.map((i) => i.Name).filter(Boolean),
202
+ retailers: r.Retailers.map((ret) => ret.Name).filter(Boolean),
203
+ cpsc_url: r.URL,
204
+ images: r.Images.map((img) => ({ url: img.URL, caption: img.Caption })),
205
+ }));
206
+ ctx.log.info('Search complete', { total_found, returned: recalls.length, truncated });
207
+ return { recalls, total_found, truncated, cpsc_jurisdiction: JURISDICTION };
208
+ },
209
+ format(result) {
210
+ const lines = [];
211
+ for (const r of result.recalls) {
212
+ lines.push(`## [${r.recall_number}] — ${r.title} (${r.recall_date})`);
213
+ const hazardText = r.hazards.length > 0 ? r.hazards.join('; ') : 'Not specified';
214
+ lines.push(`**Hazard:** ${hazardText}`);
215
+ const remedyTypes = r.remedy_options.length > 0 ? r.remedy_options.join(', ') : 'Not specified';
216
+ const remedyText = r.remedy_summary || 'See CPSC recall page.';
217
+ lines.push(`**Remedy:** ${remedyTypes} — ${remedyText}`);
218
+ const productNames = r.products
219
+ .map((p) => `${p.name} (${p.units_recalled || 'units not specified'})`)
220
+ .join(', ');
221
+ lines.push(`**Products:** ${productNames || 'Not specified'}`);
222
+ if (r.upcs.length > 0) {
223
+ lines.push(`**UPCs:** ${r.upcs.join(', ')}`);
224
+ }
225
+ const soldBy = r.retailers.length > 0 ? r.retailers.join('; ') : 'Not specified';
226
+ lines.push(`**Sold by:** ${soldBy}`);
227
+ const orgs = [...r.manufacturers, ...r.importers];
228
+ const orgText = orgs.length > 0 ? orgs.join(', ') : 'Not specified';
229
+ lines.push(`**Manufacturer/Importer:** ${orgText}`);
230
+ if (r.images.length > 0) {
231
+ const imgList = r.images.map((img) => `${img.caption}: ${img.url}`).join('; ');
232
+ lines.push(`**Images (${r.images.length}):** ${imgList}`);
233
+ }
234
+ else {
235
+ lines.push(`**Images:** None`);
236
+ }
237
+ lines.push(`[View recall](${r.cpsc_url})`);
238
+ lines.push('---');
239
+ }
240
+ lines.push(`Showing ${result.recalls.length} of ${result.total_found} recalls.${result.truncated ? ' Results truncated — narrow by date or filter to see more.' : ''}`);
241
+ lines.push(`CPSC covers: ${result.cpsc_jurisdiction}`);
242
+ return [{ type: 'text', text: lines.join('\n') }];
243
+ },
244
+ });
245
+ //# sourceMappingURL=cpsc-search-recalls.tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cpsc-search-recalls.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/cpsc-search-recalls.tool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @fileoverview Service for fetching consumer product recall data from the CPSC
3
+ * saferproducts.gov REST API. Keyless, stateless per-request fetches.
4
+ * @module services/cpsc-recall/cpsc-recall-service
5
+ */
6
+ import type { Context } from '@cyanheads/mcp-ts-core';
7
+ import type { CpscSearchParams, RawRecall } from './types.js';
8
+ export declare class CpscRecallService {
9
+ /**
10
+ * Search recalls by filter params. Returns all matching records (the API has no
11
+ * server-side pagination); client-side limiting must be applied by callers.
12
+ */
13
+ search(params: CpscSearchParams, ctx: Context): Promise<RawRecall[]>;
14
+ /**
15
+ * Fetch a single recall by recall number. Returns `null` when no matching record
16
+ * exists (API returns empty array for unknown numbers).
17
+ */
18
+ getByNumber(recallNumber: string, ctx: Context): Promise<RawRecall | null>;
19
+ /**
20
+ * Fetch recalls within a date window. The date range is required — passing no
21
+ * dates returns all 9,828+ records, which is too large to be useful.
22
+ */
23
+ getRecent(dateStart: string, dateEnd: string, ctx: Context): Promise<RawRecall[]>;
24
+ private buildUrl;
25
+ private fetchRecalls;
26
+ }
27
+ export declare function initCpscRecallService(): void;
28
+ export declare function getCpscRecallService(): CpscRecallService;
29
+ //# sourceMappingURL=cpsc-recall-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cpsc-recall-service.d.ts","sourceRoot":"","sources":["../../../src/services/cpsc-recall/cpsc-recall-service.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAGtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAM9D,qBAAa,iBAAiB;IAC5B;;;OAGG;IACH,MAAM,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAapE;;;OAGG;IACG,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAMhF;;;OAGG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAKjF,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,YAAY;CAoCrB;AAMD,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED,wBAAgB,oBAAoB,IAAI,iBAAiB,CAKxD"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @fileoverview Service for fetching consumer product recall data from the CPSC
3
+ * saferproducts.gov REST API. Keyless, stateless per-request fetches.
4
+ * @module services/cpsc-recall/cpsc-recall-service
5
+ */
6
+ import { serviceUnavailable } from '@cyanheads/mcp-ts-core/errors';
7
+ import { fetchWithTimeout, requestContextService, withRetry } from '@cyanheads/mcp-ts-core/utils';
8
+ const BASE_URL = 'https://www.saferproducts.gov/RestWebServices/Recall';
9
+ /** Request timeout: 30 s. The API returns full datasets; allow enough time. */
10
+ const TIMEOUT_MS = 30_000;
11
+ export class CpscRecallService {
12
+ /**
13
+ * Search recalls by filter params. Returns all matching records (the API has no
14
+ * server-side pagination); client-side limiting must be applied by callers.
15
+ */
16
+ search(params, ctx) {
17
+ const url = this.buildUrl({
18
+ ProductName: params.ProductName,
19
+ Manufacturer: params.Manufacturer,
20
+ Retailer: params.Retailer,
21
+ Importer: params.Importer,
22
+ RecallDescription: params.RecallDescription,
23
+ RecallDateStart: params.RecallDateStart,
24
+ RecallDateEnd: params.RecallDateEnd,
25
+ });
26
+ return this.fetchRecalls(url, ctx);
27
+ }
28
+ /**
29
+ * Fetch a single recall by recall number. Returns `null` when no matching record
30
+ * exists (API returns empty array for unknown numbers).
31
+ */
32
+ async getByNumber(recallNumber, ctx) {
33
+ const url = this.buildUrl({ RecallNumber: recallNumber });
34
+ const results = await this.fetchRecalls(url, ctx);
35
+ return results[0] ?? null;
36
+ }
37
+ /**
38
+ * Fetch recalls within a date window. The date range is required — passing no
39
+ * dates returns all 9,828+ records, which is too large to be useful.
40
+ */
41
+ getRecent(dateStart, dateEnd, ctx) {
42
+ const url = this.buildUrl({ RecallDateStart: dateStart, RecallDateEnd: dateEnd });
43
+ return this.fetchRecalls(url, ctx);
44
+ }
45
+ buildUrl(params) {
46
+ const qs = new URLSearchParams({ format: 'json' });
47
+ for (const [key, value] of Object.entries(params)) {
48
+ if (value !== undefined && value !== '')
49
+ qs.set(key, value);
50
+ }
51
+ return `${BASE_URL}?${qs.toString()}`;
52
+ }
53
+ fetchRecalls(url, ctx) {
54
+ const reqCtx = requestContextService.createRequestContext({
55
+ operation: 'CpscRecallService.fetchRecalls',
56
+ requestId: ctx.requestId,
57
+ });
58
+ return withRetry(async () => {
59
+ const response = await fetchWithTimeout(url, TIMEOUT_MS, reqCtx, {
60
+ signal: ctx.signal,
61
+ });
62
+ const text = await response.text();
63
+ // Some upstream error pages return HTTP 200 with HTML.
64
+ if (/^\s*<(!DOCTYPE\s+html|html[\s>])/i.test(text)) {
65
+ throw serviceUnavailable('CPSC API returned HTML instead of JSON — likely a transient service issue.');
66
+ }
67
+ let data;
68
+ try {
69
+ data = JSON.parse(text);
70
+ }
71
+ catch {
72
+ throw serviceUnavailable('CPSC API returned unparseable response.');
73
+ }
74
+ if (!Array.isArray(data)) {
75
+ throw serviceUnavailable('CPSC API response was not a JSON array.');
76
+ }
77
+ return data;
78
+ }, {
79
+ operation: 'CpscRecallService.fetchRecalls',
80
+ context: reqCtx,
81
+ baseDelayMs: 500,
82
+ signal: ctx.signal,
83
+ });
84
+ }
85
+ }
86
+ // --- Init/accessor pattern ---
87
+ let _service;
88
+ export function initCpscRecallService() {
89
+ _service = new CpscRecallService();
90
+ }
91
+ export function getCpscRecallService() {
92
+ if (!_service) {
93
+ throw new Error('CpscRecallService not initialized — call initCpscRecallService() in setup()');
94
+ }
95
+ return _service;
96
+ }
97
+ //# sourceMappingURL=cpsc-recall-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cpsc-recall-service.js","sourceRoot":"","sources":["../../../src/services/cpsc-recall/cpsc-recall-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @fileoverview Raw API response types from the CPSC saferproducts.gov recall endpoint.
3
+ * @module services/cpsc-recall/types
4
+ */
5
+ /** Raw recall record as returned by the CPSC REST API. */
6
+ export interface RawRecall {
7
+ ConsumerContact: string | null;
8
+ Description: string;
9
+ Distributors: Array<{
10
+ Name: string;
11
+ CompanyID: string;
12
+ }>;
13
+ Hazards: Array<{
14
+ Name: string;
15
+ HazardType: string;
16
+ HazardTypeID: string;
17
+ }>;
18
+ Images: Array<{
19
+ URL: string;
20
+ Caption: string;
21
+ }>;
22
+ Importers: Array<{
23
+ Name: string;
24
+ CompanyID: string;
25
+ }>;
26
+ /** Coordinated recalls from other agencies (e.g. Canada Health). */
27
+ Inconjunctions: Array<{
28
+ URL: string;
29
+ }>;
30
+ Injuries: Array<{
31
+ Name: string;
32
+ }>;
33
+ LastPublishDate: string;
34
+ ManufacturerCountries: Array<{
35
+ Country: string;
36
+ }>;
37
+ Manufacturers: Array<{
38
+ Name: string;
39
+ CompanyID: string;
40
+ }>;
41
+ Products: RawProduct[];
42
+ /** Sparse (~4% of records). UPCs are at recall level, not per-product. */
43
+ ProductUPCs: Array<{
44
+ UPC: string;
45
+ }>;
46
+ RecallDate: string;
47
+ RecallID: number;
48
+ RecallNumber: string;
49
+ Remedies: Array<{
50
+ Name: string;
51
+ }>;
52
+ RemedyOptions: Array<{
53
+ Option: string;
54
+ }>;
55
+ Retailers: Array<{
56
+ Name: string;
57
+ CompanyID: string;
58
+ }>;
59
+ /** Always null in practice — omitted from output. */
60
+ SoldAtLabel: null;
61
+ Title: string;
62
+ URL: string;
63
+ }
64
+ export interface RawProduct {
65
+ /** Always empty. */
66
+ CategoryID: string;
67
+ /** Always empty in full dataset. */
68
+ Description: string;
69
+ /** Almost always empty — model info is in Description text instead. */
70
+ Model: string;
71
+ Name: string;
72
+ NumberOfUnits: string;
73
+ Type: string;
74
+ }
75
+ /** Parameters for the CPSC search endpoint. */
76
+ export interface CpscSearchParams {
77
+ Importer?: string;
78
+ Manufacturer?: string;
79
+ ProductName?: string;
80
+ RecallDateEnd?: string;
81
+ RecallDateStart?: string;
82
+ /** Maps to RecallDescription — searches the Description field only. */
83
+ RecallDescription?: string;
84
+ Retailer?: string;
85
+ }
86
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/services/cpsc-recall/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,0DAA0D;AAC1D,MAAM,WAAW,SAAS;IACxB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzD,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3E,MAAM,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChD,SAAS,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtD,oEAAoE;IACpE,cAAc,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACvC,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClC,eAAe,EAAE,MAAM,CAAC;IACxB,qBAAqB,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClD,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1D,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,0EAA0E;IAC1E,WAAW,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClC,aAAa,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzC,SAAS,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtD,qDAAqD;IACrD,WAAW,EAAE,IAAI,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,UAAU;IACzB,oBAAoB;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,+CAA+C;AAC/C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fileoverview Raw API response types from the CPSC saferproducts.gov recall endpoint.
3
+ * @module services/cpsc-recall/types
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/services/cpsc-recall/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "@cyanheads/cpsc-recalls-mcp-server",
3
+ "version": "0.1.1",
4
+ "mcpName": "io.github.cyanheads/cpsc-recalls-mcp-server",
5
+ "description": "MCP server for US consumer product recalls from the Consumer Product Safety Commission — hazards, remedies, and affected products.",
6
+ "type": "module",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "bin": {
10
+ "cpsc-recalls-mcp-server": "dist/index.js"
11
+ },
12
+ "files": [
13
+ "changelog/",
14
+ "dist/",
15
+ "README.md",
16
+ "LICENSE",
17
+ "CLAUDE.md",
18
+ "AGENTS.md",
19
+ "Dockerfile",
20
+ "server.json"
21
+ ],
22
+ "scripts": {
23
+ "build": "bun run scripts/build.ts",
24
+ "rebuild": "bun run scripts/clean.ts && bun run scripts/build.ts",
25
+ "clean": "bun run scripts/clean.ts",
26
+ "devcheck": "bun run scripts/devcheck.ts",
27
+ "audit:refresh": "rm -f bun.lock && bun install && bun audit",
28
+ "tree": "bun run scripts/tree.ts",
29
+ "list-skills": "bun run scripts/list-skills.ts",
30
+ "format": "biome check --write .",
31
+ "format:unsafe": "biome check --write --unsafe .",
32
+ "lint:mcp": "bun run scripts/lint-mcp.ts",
33
+ "lint:packaging": "bun run scripts/lint-packaging.ts",
34
+ "bundle": "bun run build && npx -y @anthropic-ai/mcpb pack . dist/cpsc-recalls-mcp-server.mcpb",
35
+ "changelog:build": "bun run scripts/build-changelog.ts",
36
+ "changelog:check": "bun run scripts/build-changelog.ts --check",
37
+ "release:github": "bun run scripts/release-github.ts",
38
+ "test": "bunx vitest run",
39
+ "publish-mcp": "mcp-publisher login github -token \"$(security find-generic-password -a \"$USER\" -s mcp-publisher-github-pat -w)\" && mcp-publisher publish",
40
+ "start": "bun ./dist/index.js",
41
+ "start:stdio": "MCP_TRANSPORT_TYPE=stdio bun ./dist/index.js",
42
+ "start:http": "MCP_TRANSPORT_TYPE=http bun ./dist/index.js"
43
+ },
44
+ "keywords": [
45
+ "cpsc",
46
+ "consumer-product-safety",
47
+ "recalls",
48
+ "product-safety",
49
+ "hazards",
50
+ "mcp",
51
+ "mcp-server",
52
+ "model-context-protocol",
53
+ "typescript",
54
+ "bun",
55
+ "stdio",
56
+ "streamable-http",
57
+ "ai-agent"
58
+ ],
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "git+https://github.com/cyanheads/cpsc-recalls-mcp-server.git"
62
+ },
63
+ "bugs": {
64
+ "url": "https://github.com/cyanheads/cpsc-recalls-mcp-server/issues"
65
+ },
66
+ "homepage": "https://github.com/cyanheads/cpsc-recalls-mcp-server#readme",
67
+ "author": "cyanheads <casey@caseyjhand.com> (https://github.com/cyanheads)",
68
+ "funding": [
69
+ {
70
+ "type": "github",
71
+ "url": "https://github.com/sponsors/cyanheads"
72
+ },
73
+ {
74
+ "type": "buy_me_a_coffee",
75
+ "url": "https://www.buymeacoffee.com/cyanheads"
76
+ }
77
+ ],
78
+ "license": "Apache-2.0",
79
+ "packageManager": "bun@1.3.11",
80
+ "engines": {
81
+ "bun": ">=1.3.0",
82
+ "node": ">=24.0.0"
83
+ },
84
+ "publishConfig": {
85
+ "access": "public"
86
+ },
87
+ "dependencies": {
88
+ "@cyanheads/mcp-ts-core": "^0.9.19",
89
+ "pino-pretty": "^13.1.3",
90
+ "zod": "^4.4.3"
91
+ },
92
+ "devDependencies": {
93
+ "@biomejs/biome": "^2.4.7",
94
+ "@types/node": "^25.6.0",
95
+ "depcheck": "^1.4.7",
96
+ "ignore": "^7.0.5",
97
+ "tsc-alias": "^1.8.16",
98
+ "typescript": "^5.9.3",
99
+ "vitest": "^4.1.8"
100
+ }
101
+ }