fda-mcp-server 0.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,717 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * FDA MCP Server
4
+ *
5
+ * MCP server exposing FDA Warning Letters, Form 483 Inspections,
6
+ * and Enforcement/Recall data as queryable tools.
7
+ *
8
+ * Data sources:
9
+ * - FDA.gov Warning Letters (Drupal/Solr DataTables)
10
+ * - FDA.gov FOIA 483 Inspection Observations
11
+ * - openFDA Enforcement (Recalls) API
12
+ */
13
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+ import { z } from "zod";
16
+ import { searchWarningLetters, searchInspections, searchEnforcement, } from "./fda-client.js";
17
+ import { isEnrichedAvailable, validateApiKey, testConnection, csaRiskAssessment, citationAnalysis, supplierQualification, } from "./db.js";
18
+ const server = new McpServer({
19
+ name: "fda-mcp-server",
20
+ version: "1.0.0",
21
+ });
22
+ // ─── Tool: search_warning_letters ────────────────────────────────────────────
23
+ server.tool("search_warning_letters", "Search FDA warning letters by company name, keyword, issuing office, or date range. Returns letter metadata including subject, issuing office, dates, and excerpts.", {
24
+ search: z
25
+ .string()
26
+ .optional()
27
+ .describe("Search term (company name, keyword, CFR citation, etc.)"),
28
+ issuing_office: z
29
+ .string()
30
+ .optional()
31
+ .describe("Filter by issuing office (e.g., 'CDRH', 'CDER', 'CBER', 'ORA')"),
32
+ date_from: z
33
+ .string()
34
+ .optional()
35
+ .describe("Start date for issue date filter (YYYY-MM-DD)"),
36
+ date_to: z
37
+ .string()
38
+ .optional()
39
+ .describe("End date for issue date filter (YYYY-MM-DD)"),
40
+ limit: z
41
+ .number()
42
+ .min(1)
43
+ .max(100)
44
+ .optional()
45
+ .describe("Max results to return (default 25, max 100)"),
46
+ offset: z
47
+ .number()
48
+ .min(0)
49
+ .optional()
50
+ .describe("Pagination offset (default 0)"),
51
+ }, async (params) => {
52
+ try {
53
+ const result = await searchWarningLetters(params);
54
+ if (result.letters.length === 0) {
55
+ return {
56
+ content: [
57
+ {
58
+ type: "text",
59
+ text: `No warning letters found matching your criteria.`,
60
+ },
61
+ ],
62
+ };
63
+ }
64
+ const summary = [
65
+ `## FDA Warning Letters`,
66
+ `**Total results:** ${result.total}`,
67
+ `**Showing:** ${result.letters.length} (offset ${params.offset ?? 0})`,
68
+ ``,
69
+ ];
70
+ for (const letter of result.letters) {
71
+ summary.push(`### ${letter.company_name}`);
72
+ summary.push(`- **Subject:** ${letter.subject}`);
73
+ summary.push(`- **Issue Date:** ${letter.issue_date}`);
74
+ summary.push(`- **Issuing Office:** ${letter.issuing_office}`);
75
+ if (letter.url)
76
+ summary.push(`- **URL:** ${letter.url}`);
77
+ if (letter.excerpt) {
78
+ const excerpt = letter.excerpt.length > 300
79
+ ? letter.excerpt.substring(0, 300) + "..."
80
+ : letter.excerpt;
81
+ summary.push(`- **Excerpt:** ${excerpt}`);
82
+ }
83
+ summary.push(``);
84
+ }
85
+ return {
86
+ content: [{ type: "text", text: summary.join("\n") }],
87
+ };
88
+ }
89
+ catch (error) {
90
+ return {
91
+ content: [
92
+ {
93
+ type: "text",
94
+ text: `Error searching warning letters: ${error instanceof Error ? error.message : String(error)}`,
95
+ },
96
+ ],
97
+ isError: true,
98
+ };
99
+ }
100
+ });
101
+ // ─── Tool: search_483_inspections ────────────────────────────────────────────
102
+ server.tool("search_483_inspections", "Search FDA Form 483 inspection observations. Returns firm name, FEI number, location, inspection date, classification, center, and posted citations.", {
103
+ firm_name: z
104
+ .string()
105
+ .optional()
106
+ .describe("Search by firm/company name"),
107
+ state: z
108
+ .string()
109
+ .optional()
110
+ .describe("Filter by US state (e.g., 'PA', 'CA', 'NJ')"),
111
+ center: z
112
+ .string()
113
+ .optional()
114
+ .describe("Filter by FDA center (e.g., 'CDRH', 'CDER', 'CBER')"),
115
+ project_area: z
116
+ .string()
117
+ .optional()
118
+ .describe("Filter by project area (e.g., 'Drugs', 'Devices', 'Biologics')"),
119
+ date_from: z
120
+ .string()
121
+ .optional()
122
+ .describe("Start date for inspection end date filter (YYYY-MM-DD)"),
123
+ date_to: z
124
+ .string()
125
+ .optional()
126
+ .describe("End date for inspection end date filter (YYYY-MM-DD)"),
127
+ limit: z
128
+ .number()
129
+ .min(1)
130
+ .max(100)
131
+ .optional()
132
+ .describe("Max results to return (default 25, max 100)"),
133
+ offset: z
134
+ .number()
135
+ .min(0)
136
+ .optional()
137
+ .describe("Pagination offset (default 0)"),
138
+ }, async (params) => {
139
+ try {
140
+ const result = await searchInspections(params);
141
+ if (result.inspections.length === 0) {
142
+ return {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: `No 483 inspection records found matching your criteria.`,
147
+ },
148
+ ],
149
+ };
150
+ }
151
+ const summary = [
152
+ `## FDA Form 483 Inspections`,
153
+ `**Total results:** ${result.total}`,
154
+ `**Showing:** ${result.inspections.length} (offset ${params.offset ?? 0})`,
155
+ ``,
156
+ ];
157
+ for (const insp of result.inspections) {
158
+ summary.push(`### ${insp.firm_name}`);
159
+ summary.push(`- **FEI:** ${insp.fei_number}`);
160
+ summary.push(`- **Location:** ${[insp.city, insp.state, insp.country].filter(Boolean).join(", ")}`);
161
+ summary.push(`- **Inspection End:** ${insp.inspection_end_date}`);
162
+ summary.push(`- **Classification:** ${insp.classification}`);
163
+ summary.push(`- **Center:** ${insp.center}`);
164
+ summary.push(`- **Project Area:** ${insp.project_area}`);
165
+ if (insp.posted_citations)
166
+ summary.push(`- **Posted Citations:** ${insp.posted_citations}`);
167
+ if (insp.url)
168
+ summary.push(`- **URL:** ${insp.url}`);
169
+ summary.push(``);
170
+ }
171
+ return {
172
+ content: [{ type: "text", text: summary.join("\n") }],
173
+ };
174
+ }
175
+ catch (error) {
176
+ return {
177
+ content: [
178
+ {
179
+ type: "text",
180
+ text: `Error searching 483 inspections: ${error instanceof Error ? error.message : String(error)}`,
181
+ },
182
+ ],
183
+ isError: true,
184
+ };
185
+ }
186
+ });
187
+ // ─── Tool: search_recalls ────────────────────────────────────────────────────
188
+ server.tool("search_recalls", "Search FDA enforcement actions (recalls) via the openFDA API. Filter by product type, classification, firm, state, or date range.", {
189
+ search: z
190
+ .string()
191
+ .optional()
192
+ .describe("Free-text search across all recall fields"),
193
+ product_type: z
194
+ .enum(["drug", "device", "food"])
195
+ .optional()
196
+ .describe("Product type (default: drug)"),
197
+ classification: z
198
+ .enum(["Class I", "Class II", "Class III"])
199
+ .optional()
200
+ .describe("Recall classification (I = most serious)"),
201
+ recalling_firm: z
202
+ .string()
203
+ .optional()
204
+ .describe("Filter by recalling firm name"),
205
+ state: z
206
+ .string()
207
+ .optional()
208
+ .describe("Filter by US state (two-letter code)"),
209
+ date_from: z
210
+ .string()
211
+ .optional()
212
+ .describe("Start date for report date filter (YYYY-MM-DD)"),
213
+ date_to: z
214
+ .string()
215
+ .optional()
216
+ .describe("End date for report date filter (YYYY-MM-DD)"),
217
+ limit: z
218
+ .number()
219
+ .min(1)
220
+ .max(100)
221
+ .optional()
222
+ .describe("Max results to return (default 25, max 100)"),
223
+ skip: z
224
+ .number()
225
+ .min(0)
226
+ .optional()
227
+ .describe("Pagination skip (default 0)"),
228
+ }, async (params) => {
229
+ try {
230
+ const result = await searchEnforcement(params);
231
+ if (result.records.length === 0) {
232
+ return {
233
+ content: [
234
+ {
235
+ type: "text",
236
+ text: `No recall/enforcement records found matching your criteria.`,
237
+ },
238
+ ],
239
+ };
240
+ }
241
+ const summary = [
242
+ `## FDA Enforcement Actions (Recalls)`,
243
+ `**Total results:** ${result.total}`,
244
+ `**Showing:** ${result.records.length} (skip ${params.skip ?? 0})`,
245
+ ``,
246
+ ];
247
+ for (const rec of result.records) {
248
+ summary.push(`### ${rec.recalling_firm} — ${rec.recall_number}`);
249
+ summary.push(`- **Classification:** ${rec.classification}`);
250
+ summary.push(`- **Status:** ${rec.status}`);
251
+ summary.push(`- **Product Type:** ${rec.product_type}`);
252
+ summary.push(`- **Location:** ${[rec.city, rec.state, rec.country].filter(Boolean).join(", ")}`);
253
+ summary.push(`- **Initiation Date:** ${rec.recall_initiation_date}`);
254
+ summary.push(`- **Reason:** ${rec.reason_for_recall}`);
255
+ if (rec.product_description) {
256
+ const desc = rec.product_description.length > 200
257
+ ? rec.product_description.substring(0, 200) + "..."
258
+ : rec.product_description;
259
+ summary.push(`- **Product:** ${desc}`);
260
+ }
261
+ summary.push(`- **Distribution:** ${rec.distribution_pattern}`);
262
+ summary.push(``);
263
+ }
264
+ return {
265
+ content: [{ type: "text", text: summary.join("\n") }],
266
+ };
267
+ }
268
+ catch (error) {
269
+ return {
270
+ content: [
271
+ {
272
+ type: "text",
273
+ text: `Error searching recalls: ${error instanceof Error ? error.message : String(error)}`,
274
+ },
275
+ ],
276
+ isError: true,
277
+ };
278
+ }
279
+ });
280
+ // ─── Tool: company_enforcement_profile ───────────────────────────────────────
281
+ server.tool("company_enforcement_profile", "Build a comprehensive enforcement profile for a company. Searches warning letters, 483 inspections, and recalls in parallel and returns a consolidated view. Use this to assess a company's FDA compliance posture.", {
282
+ company_name: z
283
+ .string()
284
+ .describe("Company or firm name to profile"),
285
+ product_type: z
286
+ .enum(["drug", "device", "food"])
287
+ .optional()
288
+ .describe("Product type for recall search (default: drug)"),
289
+ }, async (params) => {
290
+ try {
291
+ const [warnings, inspections, recalls] = await Promise.all([
292
+ searchWarningLetters({ search: params.company_name, limit: 10 }).catch(() => ({ total: 0, letters: [] })),
293
+ searchInspections({ firm_name: params.company_name, limit: 10 }).catch(() => ({ total: 0, inspections: [] })),
294
+ searchEnforcement({
295
+ recalling_firm: params.company_name,
296
+ product_type: params.product_type,
297
+ limit: 10,
298
+ }).catch(() => ({ total: 0, records: [] })),
299
+ ]);
300
+ const summary = [
301
+ `## FDA Enforcement Profile: ${params.company_name}`,
302
+ ``,
303
+ `### Summary`,
304
+ `- **Warning Letters:** ${warnings.total} found`,
305
+ `- **483 Inspections:** ${inspections.total} found`,
306
+ `- **Recalls:** ${recalls.total} found`,
307
+ ``,
308
+ ];
309
+ if (warnings.letters.length > 0) {
310
+ summary.push(`### Warning Letters (showing ${warnings.letters.length})`);
311
+ for (const l of warnings.letters) {
312
+ summary.push(`- **${l.issue_date}** | ${l.issuing_office} | ${l.subject}`);
313
+ }
314
+ summary.push(``);
315
+ }
316
+ if (inspections.inspections.length > 0) {
317
+ summary.push(`### 483 Inspections (showing ${inspections.inspections.length})`);
318
+ for (const i of inspections.inspections) {
319
+ summary.push(`- **${i.inspection_end_date}** | ${i.center} | ${i.classification} | ${i.project_area}`);
320
+ }
321
+ summary.push(``);
322
+ }
323
+ if (recalls.records.length > 0) {
324
+ summary.push(`### Recalls (showing ${recalls.records.length})`);
325
+ for (const r of recalls.records) {
326
+ summary.push(`- **${r.recall_initiation_date}** | ${r.classification} | ${r.status} | ${r.reason_for_recall.substring(0, 120)}...`);
327
+ }
328
+ summary.push(``);
329
+ }
330
+ return {
331
+ content: [{ type: "text", text: summary.join("\n") }],
332
+ };
333
+ }
334
+ catch (error) {
335
+ return {
336
+ content: [
337
+ {
338
+ type: "text",
339
+ text: `Error building enforcement profile: ${error instanceof Error ? error.message : String(error)}`,
340
+ },
341
+ ],
342
+ isError: true,
343
+ };
344
+ }
345
+ });
346
+ // ─── Enriched Tools (require DATABASE_URL → fda-csa-data PostgreSQL) ─────────
347
+ // Tool: csa_risk_assessment
348
+ server.tool("csa_risk_assessment", "Assess a company's Computer Software Assurance (CSA) enforcement risk using enriched FDA dataset. Returns risk score, CSA-specific citation counts, failure modes, and escalation analysis. Requires DATABASE_URL environment variable pointing to the fda-csa-data PostgreSQL database.", {
349
+ company_name: z
350
+ .string()
351
+ .describe("Company or firm name to assess"),
352
+ }, async (params) => {
353
+ if (!isEnrichedAvailable()) {
354
+ return {
355
+ content: [
356
+ {
357
+ type: "text",
358
+ text: `CSA risk assessment requires the enriched dataset. Set DATABASE_URL to your fda-csa-data PostgreSQL instance.\n\nSee: https://github.com/jasencarroll/fda-csa-data for setup instructions.\n\nFalling back: use the \`company_enforcement_profile\` tool for live FDA data without CSA analysis.`,
359
+ },
360
+ ],
361
+ };
362
+ }
363
+ const auth = validateApiKey();
364
+ if (!auth.valid) {
365
+ return {
366
+ content: [{ type: "text", text: auth.message }],
367
+ };
368
+ }
369
+ try {
370
+ const connected = await testConnection();
371
+ if (!connected) {
372
+ return {
373
+ content: [
374
+ {
375
+ type: "text",
376
+ text: `Could not connect to the fda-csa-data database. Check your DATABASE_URL.`,
377
+ },
378
+ ],
379
+ isError: true,
380
+ };
381
+ }
382
+ const result = await csaRiskAssessment(params.company_name);
383
+ if (!result) {
384
+ return {
385
+ content: [
386
+ {
387
+ type: "text",
388
+ text: `No data found for "${params.company_name}" in the enriched dataset.`,
389
+ },
390
+ ],
391
+ };
392
+ }
393
+ const summary = [
394
+ `## CSA Risk Assessment: ${result.company_name}`,
395
+ ``,
396
+ `### Risk Score: ${result.risk_score} — ${result.risk_level}`,
397
+ ``,
398
+ `| Metric | Total | CSA-Specific |`,
399
+ `|--------|------:|-------------:|`,
400
+ `| Warning Letters | ${result.warning_letter_count} | ${result.csa_warning_letter_count} |`,
401
+ `| Citations | ${result.total_citations} | ${result.csa_citations} |`,
402
+ `| 483 Inspections | ${result.inspection_count} | ${result.csa_inspection_count} |`,
403
+ ``,
404
+ `**Escalation Multiplier:** ${result.escalation_multiplier}x (CSA findings on 483s are ${result.escalation_multiplier}x more likely to become warning letters)`,
405
+ ``,
406
+ ];
407
+ if (result.top_cfr_citations.length > 0) {
408
+ summary.push(`### Top CFR Citations`);
409
+ for (const c of result.top_cfr_citations) {
410
+ summary.push(`- **${c.cfr_full}** (${c.category}) — ${c.count} citations`);
411
+ }
412
+ summary.push(``);
413
+ }
414
+ if (result.failure_modes.length > 0) {
415
+ summary.push(`### Identified Failure Modes`);
416
+ for (const fm of result.failure_modes) {
417
+ summary.push(`- ${fm}`);
418
+ }
419
+ summary.push(``);
420
+ }
421
+ return {
422
+ content: [{ type: "text", text: summary.join("\n") }],
423
+ };
424
+ }
425
+ catch (error) {
426
+ return {
427
+ content: [
428
+ {
429
+ type: "text",
430
+ text: `Error running CSA risk assessment: ${error instanceof Error ? error.message : String(error)}`,
431
+ },
432
+ ],
433
+ isError: true,
434
+ };
435
+ }
436
+ });
437
+ // Tool: citation_analysis
438
+ server.tool("citation_analysis", "Analyze FDA citation patterns across warning letters. Break down by CFR part, section, category, and type. Identify CSA-relevant citation percentages. Requires DATABASE_URL environment variable.", {
439
+ cfr_part: z
440
+ .string()
441
+ .optional()
442
+ .describe("Filter by CFR part (e.g., '820', '211', '11')"),
443
+ category: z
444
+ .string()
445
+ .optional()
446
+ .describe("Filter by normalized category (e.g., 'software_validation', 'data_integrity', 'design_controls')"),
447
+ center: z
448
+ .string()
449
+ .optional()
450
+ .describe("Filter by FDA center (e.g., 'CDRH', 'CDER')"),
451
+ fiscal_year: z
452
+ .number()
453
+ .optional()
454
+ .describe("Filter by fiscal year (e.g., 2024, 2025)"),
455
+ }, async (params) => {
456
+ if (!isEnrichedAvailable()) {
457
+ return {
458
+ content: [
459
+ {
460
+ type: "text",
461
+ text: `Citation analysis requires the enriched dataset. Set DATABASE_URL to your fda-csa-data PostgreSQL instance.\n\nSee: https://github.com/jasencarroll/fda-csa-data`,
462
+ },
463
+ ],
464
+ };
465
+ }
466
+ const auth = validateApiKey();
467
+ if (!auth.valid) {
468
+ return {
469
+ content: [{ type: "text", text: auth.message }],
470
+ };
471
+ }
472
+ try {
473
+ const connected = await testConnection();
474
+ if (!connected) {
475
+ return {
476
+ content: [
477
+ {
478
+ type: "text",
479
+ text: `Could not connect to the fda-csa-data database. Check your DATABASE_URL.`,
480
+ },
481
+ ],
482
+ isError: true,
483
+ };
484
+ }
485
+ const result = await citationAnalysis(params);
486
+ if (!result) {
487
+ return {
488
+ content: [
489
+ {
490
+ type: "text",
491
+ text: `No citation data found for the given filters.`,
492
+ },
493
+ ],
494
+ };
495
+ }
496
+ const filters = [
497
+ params.cfr_part && `CFR Part ${params.cfr_part}`,
498
+ params.category && `Category: ${params.category}`,
499
+ params.center && `Center: ${params.center}`,
500
+ params.fiscal_year && `FY${params.fiscal_year}`,
501
+ ]
502
+ .filter(Boolean)
503
+ .join(" | ");
504
+ const summary = [
505
+ `## FDA Citation Analysis`,
506
+ filters ? `**Filters:** ${filters}` : `**Scope:** All citations`,
507
+ `**Total Citations:** ${result.total_citations}`,
508
+ `**CSA-Relevant:** ${result.csa_percentage}%`,
509
+ ``,
510
+ ];
511
+ if (result.by_cfr_part.length > 0) {
512
+ summary.push(`### By CFR Part`);
513
+ for (const p of result.by_cfr_part) {
514
+ summary.push(`- **Part ${p.part}** — ${p.count} citations`);
515
+ }
516
+ summary.push(``);
517
+ }
518
+ if (result.by_category.length > 0) {
519
+ summary.push(`### By Category`);
520
+ for (const c of result.by_category) {
521
+ const csa = c.csa_relevant ? " ⚠ CSA" : "";
522
+ summary.push(`- **${c.category}** — ${c.count}${csa}`);
523
+ }
524
+ summary.push(``);
525
+ }
526
+ if (result.top_sections.length > 0) {
527
+ summary.push(`### Top CFR Sections`);
528
+ for (const s of result.top_sections) {
529
+ summary.push(`- **${s.cfr_full}** (${s.description}) — ${s.count}`);
530
+ }
531
+ summary.push(``);
532
+ }
533
+ if (result.by_type.length > 0) {
534
+ summary.push(`### By Citation Type`);
535
+ for (const t of result.by_type) {
536
+ summary.push(`- **${t.type}** — ${t.count}`);
537
+ }
538
+ summary.push(``);
539
+ }
540
+ return {
541
+ content: [{ type: "text", text: summary.join("\n") }],
542
+ };
543
+ }
544
+ catch (error) {
545
+ return {
546
+ content: [
547
+ {
548
+ type: "text",
549
+ text: `Error running citation analysis: ${error instanceof Error ? error.message : String(error)}`,
550
+ },
551
+ ],
552
+ isError: true,
553
+ };
554
+ }
555
+ });
556
+ // Tool: supplier_qualification_report
557
+ server.tool("supplier_qualification_report", "Generate a comprehensive supplier qualification report for FDA-regulated supplier assessments. Consolidates warning letters, 483 inspections, citations, failure modes, and CSA risk into a single audit-ready brief. Requires DATABASE_URL environment variable.", {
558
+ company_name: z
559
+ .string()
560
+ .describe("Supplier/company name to qualify"),
561
+ }, async (params) => {
562
+ if (!isEnrichedAvailable()) {
563
+ return {
564
+ content: [
565
+ {
566
+ type: "text",
567
+ text: `Supplier qualification report requires the enriched dataset. Set DATABASE_URL to your fda-csa-data PostgreSQL instance.\n\nSee: https://github.com/jasencarroll/fda-csa-data\n\nFor a lighter-weight alternative, use the \`company_enforcement_profile\` tool.`,
568
+ },
569
+ ],
570
+ };
571
+ }
572
+ const auth = validateApiKey();
573
+ if (!auth.valid) {
574
+ return {
575
+ content: [{ type: "text", text: auth.message }],
576
+ };
577
+ }
578
+ try {
579
+ const connected = await testConnection();
580
+ if (!connected) {
581
+ return {
582
+ content: [
583
+ {
584
+ type: "text",
585
+ text: `Could not connect to the fda-csa-data database. Check your DATABASE_URL.`,
586
+ },
587
+ ],
588
+ isError: true,
589
+ };
590
+ }
591
+ const result = await supplierQualification(params.company_name);
592
+ if (!result) {
593
+ return {
594
+ content: [
595
+ {
596
+ type: "text",
597
+ text: `No data found for "${params.company_name}" in the enriched dataset.`,
598
+ },
599
+ ],
600
+ };
601
+ }
602
+ const summary = [
603
+ `## Supplier Qualification Report: ${result.company_name}`,
604
+ `*Generated ${new Date().toISOString().split("T")[0]}*`,
605
+ ``,
606
+ ];
607
+ // Risk summary
608
+ if (result.csa_risk) {
609
+ summary.push(`### CSA Risk Assessment`);
610
+ summary.push(`- **Risk Score:** ${result.csa_risk.risk_score} — **${result.csa_risk.risk_level}**`);
611
+ summary.push(`- **CSA Warning Letters:** ${result.csa_risk.csa_warning_letter_count} of ${result.csa_risk.warning_letter_count}`);
612
+ summary.push(`- **CSA Inspections:** ${result.csa_risk.csa_inspection_count} of ${result.csa_risk.inspection_count}`);
613
+ summary.push(`- **CSA Citations:** ${result.csa_risk.csa_citations} of ${result.csa_risk.total_citations}`);
614
+ summary.push(``);
615
+ }
616
+ // FEI numbers
617
+ if (result.fei_numbers.length > 0) {
618
+ summary.push(`### Facility Establishment Identifiers (FEI)`);
619
+ for (const fei of result.fei_numbers) {
620
+ summary.push(`- ${fei}`);
621
+ }
622
+ summary.push(``);
623
+ }
624
+ // Warning letters
625
+ if (result.warning_letters.length > 0) {
626
+ summary.push(`### Warning Letters (${result.warning_letters.length})`);
627
+ summary.push(`| Date | Center | Regime | CSA |`);
628
+ summary.push(`|------|--------|--------|-----|`);
629
+ for (const wl of result.warning_letters) {
630
+ summary.push(`| ${wl.issue_date} | ${wl.center} | ${wl.regime} | ${wl.csa_relevant ? "YES" : "no"} |`);
631
+ }
632
+ summary.push(``);
633
+ }
634
+ // Inspections
635
+ if (result.inspections.length > 0) {
636
+ summary.push(`### Form 483 Inspections (${result.inspections.length})`);
637
+ summary.push(`| Date | Center | Area | CSA |`);
638
+ summary.push(`|------|--------|------|-----|`);
639
+ for (const insp of result.inspections) {
640
+ summary.push(`| ${insp.inspection_end_date} | ${insp.center} | ${insp.product_area} | ${insp.csa_relevant ? "YES" : "no"} |`);
641
+ }
642
+ summary.push(``);
643
+ }
644
+ // Citations
645
+ if (result.citation_summary.length > 0) {
646
+ summary.push(`### Citation Summary`);
647
+ for (const c of result.citation_summary) {
648
+ summary.push(`- **${c.cfr_full}** (${c.category}) — ${c.count}x`);
649
+ }
650
+ summary.push(``);
651
+ }
652
+ // Failure modes
653
+ if (result.failure_modes.length > 0) {
654
+ summary.push(`### Identified Failure Modes`);
655
+ for (const fm of result.failure_modes) {
656
+ summary.push(`- **${fm.mode}** (${fm.confidence} confidence) — ${fm.count}x`);
657
+ }
658
+ summary.push(``);
659
+ }
660
+ // Recommendation
661
+ if (result.csa_risk) {
662
+ summary.push(`### Qualification Recommendation`);
663
+ switch (result.csa_risk.risk_level) {
664
+ case "CRITICAL":
665
+ summary.push(`**DO NOT QUALIFY without on-site audit.** Critical CSA enforcement history. Requires detailed remediation evidence review, CAPA verification, and management commitment assessment before supplier approval.`);
666
+ break;
667
+ case "HIGH":
668
+ summary.push(`**Conditional qualification.** Significant CSA enforcement history warrants enhanced supplier monitoring. Require CAPA documentation for all cited findings. Schedule qualification audit within 90 days.`);
669
+ break;
670
+ case "MODERATE":
671
+ summary.push(`**Qualify with enhanced monitoring.** Enforcement history present but manageable. Include CSA-specific questions in supplier questionnaire. Annual re-evaluation recommended.`);
672
+ break;
673
+ case "LOW":
674
+ summary.push(`**Standard qualification pathway.** No significant CSA enforcement concerns. Proceed with standard supplier qualification process.`);
675
+ break;
676
+ }
677
+ summary.push(``);
678
+ }
679
+ return {
680
+ content: [{ type: "text", text: summary.join("\n") }],
681
+ };
682
+ }
683
+ catch (error) {
684
+ return {
685
+ content: [
686
+ {
687
+ type: "text",
688
+ text: `Error generating supplier qualification report: ${error instanceof Error ? error.message : String(error)}`,
689
+ },
690
+ ],
691
+ isError: true,
692
+ };
693
+ }
694
+ });
695
+ // ─── Start Server ────────────────────────────────────────────────────────────
696
+ async function main() {
697
+ const transport = new StdioServerTransport();
698
+ await server.connect(transport);
699
+ const enriched = isEnrichedAvailable();
700
+ console.error(`FDA MCP Server running on stdio`);
701
+ console.error(` Tools: search_warning_letters, search_483_inspections, search_recalls, company_enforcement_profile`);
702
+ if (enriched) {
703
+ const connected = await testConnection();
704
+ console.error(` Enriched tools: ${connected ? "ACTIVE" : "DATABASE_URL set but connection failed"}`);
705
+ if (connected) {
706
+ console.error(` + csa_risk_assessment, citation_analysis, supplier_qualification_report`);
707
+ }
708
+ }
709
+ else {
710
+ console.error(` Enriched tools: INACTIVE (set DATABASE_URL to enable)`);
711
+ }
712
+ }
713
+ main().catch((error) => {
714
+ console.error("Fatal error:", error);
715
+ process.exit(1);
716
+ });
717
+ //# sourceMappingURL=index.js.map