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