@ythalorossy/openfda 1.0.12 → 1.0.13
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/LICENSE +20 -20
- package/README.md +84 -87
- package/bin/OpenFDABuilder.js +49 -49
- package/bin/OpenFDAClient.js +178 -178
- package/bin/index.js +616 -420
- package/bin/types.js +5 -5
- package/dist/index.js +648 -0
- package/package.json +21 -9
- package/build/OpenFDABuilder.js +0 -49
- package/build/OpenFDAClient.js +0 -178
- package/build/index.js +0 -452
- package/build/types.js +0 -5
package/bin/index.js
CHANGED
|
@@ -1,452 +1,648 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
1
|
+
var D = Object.defineProperty;
|
|
2
|
+
var v = (t, n, r) => n in t ? D(t, n, { enumerable: !0, configurable: !0, writable: !0, value: r }) : t[n] = r;
|
|
3
|
+
var k = (t, n, r) => v(t, typeof n != "symbol" ? n + "" : n, r);
|
|
4
|
+
import { McpServer as N } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport as T } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import u from "zod";
|
|
7
|
+
class h {
|
|
8
|
+
constructor() {
|
|
9
|
+
k(this, "url", "https://api.fda.gov/drug/");
|
|
10
|
+
k(this, "params", /* @__PURE__ */ new Map());
|
|
11
|
+
}
|
|
12
|
+
context(n) {
|
|
13
|
+
return this.params.set("context", n), this;
|
|
14
|
+
}
|
|
15
|
+
search(n) {
|
|
16
|
+
return this.params.set("search", n), this;
|
|
17
|
+
}
|
|
18
|
+
limit(n = 1) {
|
|
19
|
+
return this.params.set("limit", n), this;
|
|
20
|
+
}
|
|
21
|
+
build() {
|
|
22
|
+
const n = this.params.get("context"), r = this.params.get("search");
|
|
23
|
+
let s = this.params.get("limit");
|
|
24
|
+
const e = process.env.OPENFDA_API_KEY;
|
|
25
|
+
if (!n || !r)
|
|
26
|
+
throw new Error("Missing required parameters: context or search");
|
|
27
|
+
return s === void 0 && (s = 1), `${this.url}${n}.json?api_key=${e}&search=${r}&limit=${s}`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
class C {
|
|
31
|
+
constructor(n) {
|
|
32
|
+
this.server = n;
|
|
33
|
+
}
|
|
34
|
+
registerTool(n) {
|
|
35
|
+
this.server.tool(
|
|
36
|
+
n.name,
|
|
37
|
+
n.description,
|
|
38
|
+
n.schema.shape,
|
|
39
|
+
n.handler
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const F = {
|
|
44
|
+
maxRetries: 3,
|
|
45
|
+
retryDelay: 1e3,
|
|
46
|
+
// 1 second
|
|
47
|
+
timeout: 3e4
|
|
48
|
+
// 30 seconds
|
|
49
|
+
};
|
|
50
|
+
function x(t) {
|
|
51
|
+
return !!(t.name === "TypeError" && t.message.includes("fetch") || t.name === "AbortError" || t.status >= 500 && t.status <= 599 || t.status === 429);
|
|
52
|
+
}
|
|
53
|
+
function w(t) {
|
|
54
|
+
return new Promise((n) => setTimeout(n, t));
|
|
55
|
+
}
|
|
56
|
+
async function y(t, n = {}) {
|
|
57
|
+
const { maxRetries: r, retryDelay: s, timeout: e } = { ...F, ...n }, p = {
|
|
58
|
+
"User-Agent": "@ythalorossy/openfda",
|
|
59
|
+
Accept: "application/json"
|
|
60
|
+
};
|
|
61
|
+
let a = null;
|
|
62
|
+
for (let c = 0; c <= r; c++)
|
|
63
|
+
try {
|
|
64
|
+
const o = new AbortController(), d = setTimeout(() => o.abort(), e);
|
|
65
|
+
console.log(
|
|
66
|
+
`Making OpenFDA request (attempt ${c + 1}/${r + 1}): ${t}`
|
|
67
|
+
);
|
|
68
|
+
const i = await fetch(t, {
|
|
69
|
+
headers: p,
|
|
70
|
+
signal: o.signal
|
|
71
|
+
});
|
|
72
|
+
if (clearTimeout(d), !i.ok) {
|
|
73
|
+
const m = await i.text().catch(() => "Unable to read error response"), f = {
|
|
74
|
+
type: "http",
|
|
75
|
+
message: `HTTP ${i.status}: ${i.statusText}`,
|
|
76
|
+
status: i.status,
|
|
77
|
+
details: m
|
|
78
|
+
};
|
|
79
|
+
switch (console.error(`OpenFDA HTTP Error (${i.status}):`, {
|
|
80
|
+
url: t,
|
|
81
|
+
status: i.status,
|
|
82
|
+
statusText: i.statusText,
|
|
83
|
+
errorText: m.substring(0, 200)
|
|
84
|
+
// Truncate long error messages
|
|
85
|
+
}), i.status) {
|
|
86
|
+
case 400:
|
|
87
|
+
f.message = "Bad Request: Invalid search query or parameters";
|
|
88
|
+
break;
|
|
89
|
+
case 401:
|
|
90
|
+
f.message = "Unauthorized: Invalid or missing API key";
|
|
91
|
+
break;
|
|
92
|
+
case 403:
|
|
93
|
+
f.message = "Forbidden: API key may be invalid or quota exceeded";
|
|
94
|
+
break;
|
|
95
|
+
case 404:
|
|
96
|
+
f.message = "Not Found: No results found for the specified query";
|
|
97
|
+
break;
|
|
98
|
+
case 429:
|
|
99
|
+
f.message = "Rate Limited: Too many requests. Retrying...";
|
|
100
|
+
break;
|
|
101
|
+
case 500:
|
|
102
|
+
f.message = "Server Error: OpenFDA service is experiencing issues";
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
f.message = `HTTP Error ${i.status}: ${i.statusText}`;
|
|
58
106
|
}
|
|
59
|
-
|
|
60
|
-
|
|
107
|
+
if (a = f, i.status >= 400 && i.status < 500 && i.status !== 429)
|
|
108
|
+
break;
|
|
109
|
+
if (c < r && x({ status: i.status })) {
|
|
110
|
+
const g = s * Math.pow(2, c);
|
|
111
|
+
console.log(`Retrying in ${g}ms...`), await w(g);
|
|
112
|
+
continue;
|
|
61
113
|
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
let l;
|
|
117
|
+
try {
|
|
118
|
+
l = await i.json();
|
|
119
|
+
} catch (m) {
|
|
120
|
+
const f = {
|
|
121
|
+
type: "parsing",
|
|
122
|
+
message: `Failed to parse JSON response: ${m instanceof Error ? m.message : "Unknown parsing error"}`,
|
|
123
|
+
details: m
|
|
124
|
+
};
|
|
125
|
+
console.error("OpenFDA JSON Parsing Error:", {
|
|
126
|
+
url: t,
|
|
127
|
+
parseError: m instanceof Error ? m.message : m
|
|
128
|
+
}), a = f;
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
if (!l) {
|
|
132
|
+
a = {
|
|
133
|
+
type: "empty_response",
|
|
134
|
+
message: "Received empty response from OpenFDA API"
|
|
135
|
+
};
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
return console.log(`OpenFDA request successful on attempt ${c + 1}`), { data: l, error: null };
|
|
139
|
+
} catch (o) {
|
|
140
|
+
let d;
|
|
141
|
+
if (o.name === "AbortError" ? d = {
|
|
142
|
+
type: "timeout",
|
|
143
|
+
message: `Request timeout after ${e}ms`,
|
|
144
|
+
details: o
|
|
145
|
+
} : o instanceof TypeError && o.message.includes("fetch") ? d = {
|
|
146
|
+
type: "network",
|
|
147
|
+
message: "Network error: Unable to connect to OpenFDA API",
|
|
148
|
+
details: o.message
|
|
149
|
+
} : d = {
|
|
150
|
+
type: "unknown",
|
|
151
|
+
message: `Unexpected error: ${o.message || "Unknown error occurred"}`,
|
|
152
|
+
details: o
|
|
153
|
+
}, console.error(`OpenFDA Request Error (attempt ${c + 1}):`, {
|
|
154
|
+
url: t,
|
|
155
|
+
error: o.message,
|
|
156
|
+
type: o.name
|
|
157
|
+
}), a = d, c < r && x(o)) {
|
|
158
|
+
const i = s * Math.pow(2, c);
|
|
159
|
+
console.log(`Network error, retrying in ${i}ms...`), await w(i);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
62
163
|
}
|
|
63
|
-
|
|
64
|
-
const isValid = /^\d{5}-\d{4}$/.test(productNDC);
|
|
65
|
-
return { productNDC, packageNDC, isValid };
|
|
164
|
+
return { data: null, error: a };
|
|
66
165
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
let errorMessage = `Failed to retrieve drug data for "${drugName}": ${error.message}`;
|
|
78
|
-
// Provide helpful suggestions based on error type
|
|
79
|
-
switch (error.type) {
|
|
80
|
-
case 'http':
|
|
81
|
-
if (error.status === 404) {
|
|
82
|
-
errorMessage += `\n\nSuggestions:\n- Verify the exact brand name spelling\n- Try searching for the generic name instead\n- Check if the drug is FDA-approved`;
|
|
83
|
-
}
|
|
84
|
-
else if (error.status === 401 || error.status === 403) {
|
|
85
|
-
errorMessage += `\n\nPlease check the API key configuration.`;
|
|
86
|
-
}
|
|
87
|
-
break;
|
|
88
|
-
case 'network':
|
|
89
|
-
errorMessage += `\n\nPlease check your internet connection and try again.`;
|
|
90
|
-
break;
|
|
91
|
-
case 'timeout':
|
|
92
|
-
errorMessage += `\n\nThe request took too long. Please try again.`;
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
content: [{
|
|
97
|
-
type: "text",
|
|
98
|
-
text: errorMessage,
|
|
99
|
-
}],
|
|
100
|
-
};
|
|
166
|
+
const X = new N(
|
|
167
|
+
{
|
|
168
|
+
name: "openfda",
|
|
169
|
+
version: "1.0.0",
|
|
170
|
+
description: "OpenFDA Model Context Protocol"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
capabilities: {
|
|
174
|
+
resources: {},
|
|
175
|
+
tools: {}
|
|
101
176
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
177
|
+
}
|
|
178
|
+
), b = new C(X);
|
|
179
|
+
function A(t) {
|
|
180
|
+
const n = t.trim().toUpperCase();
|
|
181
|
+
let r, s = null;
|
|
182
|
+
if (n.includes("-")) {
|
|
183
|
+
const p = n.split("-");
|
|
184
|
+
if (p.length === 2)
|
|
185
|
+
r = n, s = null;
|
|
186
|
+
else if (p.length === 3)
|
|
187
|
+
r = `${p[0]}-${p[1]}`, s = n;
|
|
188
|
+
else
|
|
189
|
+
return { productNDC: n, packageNDC: null, isValid: !1 };
|
|
190
|
+
} else if (n.length === 11)
|
|
191
|
+
r = `${n.substring(0, 5)}-${n.substring(5, 9)}`, s = `${n.substring(0, 5)}-${n.substring(5, 9)}-${n.substring(9, 11)}`;
|
|
192
|
+
else if (n.length === 9)
|
|
193
|
+
r = `${n.substring(0, 5)}-${n.substring(5, 9)}`, s = null;
|
|
194
|
+
else
|
|
195
|
+
return { productNDC: n, packageNDC: null, isValid: !1 };
|
|
196
|
+
const e = /^\d{5}-\d{4}$/.test(r);
|
|
197
|
+
return { productNDC: r, packageNDC: s, isValid: e };
|
|
198
|
+
}
|
|
199
|
+
b.registerTool({
|
|
200
|
+
name: "get-drug-by-name",
|
|
201
|
+
description: "Get drug by name. Use this tool to get the drug information by name. The drug name should be the brand name. It returns the brand name, generic name, manufacturer name, product NDC, product type, route, substance name, indications and usage, warnings, do not use, ask doctor, ask doctor or pharmacist, stop use, pregnancy or breast feeding.",
|
|
202
|
+
schema: u.object({
|
|
203
|
+
drugName: u.string().describe("Drug name")
|
|
204
|
+
}),
|
|
205
|
+
handler: async ({ drugName: t }) => {
|
|
206
|
+
const n = new h().context("label").search(`openfda.brand_name:"${t}"`).limit(1).build(), { data: r, error: s } = await y(n);
|
|
207
|
+
if (s) {
|
|
208
|
+
let a = `Failed to retrieve drug data for "${t}": ${s.message}`;
|
|
209
|
+
switch (s.type) {
|
|
210
|
+
case "http":
|
|
211
|
+
s.status === 404 ? a += `
|
|
212
|
+
|
|
213
|
+
Suggestions:
|
|
214
|
+
- Verify the exact brand name spelling
|
|
215
|
+
- Try searching for the generic name instead
|
|
216
|
+
- Check if the drug is FDA-approved` : (s.status === 401 || s.status === 403) && (a += `
|
|
217
|
+
|
|
218
|
+
Please check the API key configuration.`);
|
|
219
|
+
break;
|
|
220
|
+
case "network":
|
|
221
|
+
a += `
|
|
222
|
+
|
|
223
|
+
Please check your internet connection and try again.`;
|
|
224
|
+
break;
|
|
225
|
+
case "timeout":
|
|
226
|
+
a += `
|
|
227
|
+
|
|
228
|
+
The request took too long. Please try again.`;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
content: [
|
|
233
|
+
{
|
|
234
|
+
type: "text",
|
|
235
|
+
text: a
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
};
|
|
109
239
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
240
|
+
if (!r || !r.results || r.results.length === 0)
|
|
241
|
+
return {
|
|
242
|
+
content: [
|
|
243
|
+
{
|
|
244
|
+
type: "text",
|
|
245
|
+
text: `No drug information found for "${t}". Please verify the brand name spelling or try searching for the generic name.`
|
|
246
|
+
}
|
|
247
|
+
]
|
|
248
|
+
};
|
|
249
|
+
const e = r.results[0], p = {
|
|
250
|
+
brand_name: e == null ? void 0 : e.openfda.brand_name,
|
|
251
|
+
generic_name: e == null ? void 0 : e.openfda.generic_name,
|
|
252
|
+
manufacturer_name: e == null ? void 0 : e.openfda.manufacturer_name,
|
|
253
|
+
product_ndc: e == null ? void 0 : e.openfda.product_ndc,
|
|
254
|
+
product_type: e == null ? void 0 : e.openfda.product_type,
|
|
255
|
+
route: e == null ? void 0 : e.openfda.route,
|
|
256
|
+
substance_name: e == null ? void 0 : e.openfda.substance_name,
|
|
257
|
+
indications_and_usage: e == null ? void 0 : e.indications_and_usage,
|
|
258
|
+
warnings: e == null ? void 0 : e.warnings,
|
|
259
|
+
do_not_use: e == null ? void 0 : e.do_not_use,
|
|
260
|
+
ask_doctor: e == null ? void 0 : e.ask_doctor,
|
|
261
|
+
ask_doctor_or_pharmacist: e == null ? void 0 : e.ask_doctor_or_pharmacist,
|
|
262
|
+
stop_use: e == null ? void 0 : e.stop_use,
|
|
263
|
+
pregnancy_or_breast_feeding: e == null ? void 0 : e.pregnancy_or_breast_feeding
|
|
126
264
|
};
|
|
127
265
|
return {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
266
|
+
content: [
|
|
267
|
+
{
|
|
268
|
+
type: "text",
|
|
269
|
+
text: `Drug information retrieved successfully:
|
|
270
|
+
|
|
271
|
+
${JSON.stringify(p, null, 2)}`
|
|
272
|
+
}
|
|
273
|
+
]
|
|
132
274
|
};
|
|
275
|
+
}
|
|
133
276
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (!
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
277
|
+
b.registerTool({
|
|
278
|
+
name: "get-drug-by-generic-name",
|
|
279
|
+
description: "Get drug information by generic (active ingredient) name. Useful when you know the generic name but not the brand name. Returns all brand versions of the generic drug.",
|
|
280
|
+
schema: u.object({
|
|
281
|
+
genericName: u.string().describe("Generic drug name (active ingredient)"),
|
|
282
|
+
limit: u.number().optional().default(5).describe("Maximum number of results to return")
|
|
283
|
+
}),
|
|
284
|
+
handler: async ({ genericName: t, limit: n }) => {
|
|
285
|
+
const r = new h().context("label").search(`openfda.generic_name:"${t}"`).limit(n).build(), { data: s, error: e } = await y(r);
|
|
286
|
+
if (e)
|
|
287
|
+
return {
|
|
288
|
+
content: [
|
|
289
|
+
{
|
|
290
|
+
type: "text",
|
|
291
|
+
text: `Failed to retrieve drug data for generic name "${t}": ${e.message}`
|
|
292
|
+
}
|
|
293
|
+
]
|
|
294
|
+
};
|
|
295
|
+
if (!s || !s.results || s.results.length === 0)
|
|
296
|
+
return {
|
|
297
|
+
content: [
|
|
298
|
+
{
|
|
299
|
+
type: "text",
|
|
300
|
+
text: `No drug information found for generic name "${t}".`
|
|
301
|
+
}
|
|
302
|
+
]
|
|
303
|
+
};
|
|
304
|
+
const p = s.results.map((a) => {
|
|
305
|
+
var c, o, d, i;
|
|
306
|
+
return {
|
|
307
|
+
brand_name: ((c = a == null ? void 0 : a.openfda.brand_name) == null ? void 0 : c[0]) || "Unknown",
|
|
308
|
+
generic_name: ((o = a == null ? void 0 : a.openfda.generic_name) == null ? void 0 : o[0]) || "Unknown",
|
|
309
|
+
manufacturer_name: ((d = a == null ? void 0 : a.openfda.manufacturer_name) == null ? void 0 : d[0]) || "Unknown",
|
|
310
|
+
product_type: ((i = a == null ? void 0 : a.openfda.product_type) == null ? void 0 : i[0]) || "Unknown",
|
|
311
|
+
route: (a == null ? void 0 : a.openfda.route) || []
|
|
312
|
+
};
|
|
313
|
+
});
|
|
167
314
|
return {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
315
|
+
content: [
|
|
316
|
+
{
|
|
317
|
+
type: "text",
|
|
318
|
+
text: `Found ${p.length} drug(s) with generic name "${t}":
|
|
319
|
+
|
|
320
|
+
${JSON.stringify(p, null, 2)}`
|
|
321
|
+
}
|
|
322
|
+
]
|
|
172
323
|
};
|
|
324
|
+
}
|
|
173
325
|
});
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
report_id:
|
|
208
|
-
serious:
|
|
209
|
-
patient_age:
|
|
210
|
-
patient_sex:
|
|
211
|
-
reactions:
|
|
212
|
-
outcomes:
|
|
213
|
-
report_date:
|
|
214
|
-
|
|
326
|
+
b.registerTool({
|
|
327
|
+
name: "get-drug-adverse-events",
|
|
328
|
+
description: "Get adverse event reports for a drug. This provides safety information about reported side effects and reactions. Use brand name or generic name.",
|
|
329
|
+
schema: u.object({
|
|
330
|
+
drugName: u.string().describe("Drug name (brand or generic)"),
|
|
331
|
+
limit: u.number().optional().default(10).describe("Maximum number of events to return"),
|
|
332
|
+
seriousness: u.enum(["serious", "non-serious", "all"]).optional().default("all").describe("Filter by event seriousness")
|
|
333
|
+
}),
|
|
334
|
+
handler: async ({ drugName: t, limit: n, seriousness: r }) => {
|
|
335
|
+
let s = `patient.drug.medicinalproduct:"${t}"`;
|
|
336
|
+
r !== "all" && (s += `+AND+serious:${r === "serious" ? "1" : "2"}`);
|
|
337
|
+
const e = new h().context("event").search(s).limit(n).build(), { data: p, error: a } = await y(e);
|
|
338
|
+
if (a)
|
|
339
|
+
return {
|
|
340
|
+
content: [
|
|
341
|
+
{
|
|
342
|
+
type: "text",
|
|
343
|
+
text: `Failed to retrieve adverse events for "${t}": ${a.message}`
|
|
344
|
+
}
|
|
345
|
+
]
|
|
346
|
+
};
|
|
347
|
+
if (!p || !p.results || p.results.length === 0)
|
|
348
|
+
return {
|
|
349
|
+
content: [
|
|
350
|
+
{
|
|
351
|
+
type: "text",
|
|
352
|
+
text: `No adverse events found for "${t}".`
|
|
353
|
+
}
|
|
354
|
+
]
|
|
355
|
+
};
|
|
356
|
+
const c = p.results.map((o) => {
|
|
357
|
+
var d, i, l, m, f, g, $;
|
|
358
|
+
return {
|
|
359
|
+
report_id: o.safetyreportid,
|
|
360
|
+
serious: o.serious === "1" ? "Yes" : "No",
|
|
361
|
+
patient_age: ((d = o.patient) == null ? void 0 : d.patientonsetage) || "Unknown",
|
|
362
|
+
patient_sex: ((i = o.patient) == null ? void 0 : i.patientsex) === "1" ? "Male" : ((l = o.patient) == null ? void 0 : l.patientsex) === "2" ? "Female" : "Unknown",
|
|
363
|
+
reactions: ((f = (m = o.patient) == null ? void 0 : m.reaction) == null ? void 0 : f.map((_) => _.reactionmeddrapt).slice(0, 3)) || [],
|
|
364
|
+
outcomes: (($ = (g = o.patient) == null ? void 0 : g.reaction) == null ? void 0 : $.map((_) => _.reactionoutcome).slice(0, 3)) || [],
|
|
365
|
+
report_date: o.receiptdate || "Unknown"
|
|
366
|
+
};
|
|
367
|
+
});
|
|
215
368
|
return {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
369
|
+
content: [
|
|
370
|
+
{
|
|
371
|
+
type: "text",
|
|
372
|
+
text: `Found ${c.length} adverse event report(s) for "${t}":
|
|
373
|
+
|
|
374
|
+
${JSON.stringify(c, null, 2)}`
|
|
375
|
+
}
|
|
376
|
+
]
|
|
220
377
|
};
|
|
378
|
+
}
|
|
221
379
|
});
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
if (!
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
380
|
+
b.registerTool({
|
|
381
|
+
name: "get-drugs-by-manufacturer",
|
|
382
|
+
description: "Get all drugs manufactured by a specific company. Useful for finding alternatives or checking manufacturer portfolios.",
|
|
383
|
+
schema: u.object({
|
|
384
|
+
manufacturerName: u.string().describe("Manufacturer/company name"),
|
|
385
|
+
limit: u.number().optional().default(20).describe("Maximum number of drugs to return")
|
|
386
|
+
}),
|
|
387
|
+
handler: async ({ manufacturerName: t, limit: n }) => {
|
|
388
|
+
const r = new h().context("label").search(`openfda.manufacturer_name:"${t}"`).limit(n).build(), { data: s, error: e } = await y(r);
|
|
389
|
+
if (e)
|
|
390
|
+
return {
|
|
391
|
+
content: [
|
|
392
|
+
{
|
|
393
|
+
type: "text",
|
|
394
|
+
text: `Failed to retrieve drugs for manufacturer "${t}": ${e.message}`
|
|
395
|
+
}
|
|
396
|
+
]
|
|
397
|
+
};
|
|
398
|
+
if (!s || !s.results || s.results.length === 0)
|
|
399
|
+
return {
|
|
400
|
+
content: [
|
|
401
|
+
{
|
|
402
|
+
type: "text",
|
|
403
|
+
text: `No drugs found for manufacturer "${t}".`
|
|
404
|
+
}
|
|
405
|
+
]
|
|
406
|
+
};
|
|
407
|
+
const p = s.results.map((a) => {
|
|
408
|
+
var c, o, d, i;
|
|
409
|
+
return {
|
|
410
|
+
brand_name: ((c = a == null ? void 0 : a.openfda.brand_name) == null ? void 0 : c[0]) || "Unknown",
|
|
411
|
+
generic_name: ((o = a == null ? void 0 : a.openfda.generic_name) == null ? void 0 : o[0]) || "Unknown",
|
|
412
|
+
product_type: ((d = a == null ? void 0 : a.openfda.product_type) == null ? void 0 : d[0]) || "Unknown",
|
|
413
|
+
route: (a == null ? void 0 : a.openfda.route) || [],
|
|
414
|
+
ndc: ((i = a == null ? void 0 : a.openfda.product_ndc) == null ? void 0 : i[0]) || "Unknown"
|
|
415
|
+
};
|
|
416
|
+
});
|
|
255
417
|
return {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
418
|
+
content: [
|
|
419
|
+
{
|
|
420
|
+
type: "text",
|
|
421
|
+
text: `Found ${p.length} drug(s) from manufacturer "${t}":
|
|
422
|
+
|
|
423
|
+
${JSON.stringify(p, null, 2)}`
|
|
424
|
+
}
|
|
425
|
+
]
|
|
260
426
|
};
|
|
427
|
+
}
|
|
261
428
|
});
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const { data:
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
429
|
+
b.registerTool({
|
|
430
|
+
name: "get-drug-safety-info",
|
|
431
|
+
description: "Get comprehensive safety information for a drug including warnings, contraindications, drug interactions, and precautions. Use brand name.",
|
|
432
|
+
schema: u.object({
|
|
433
|
+
drugName: u.string().describe("Drug brand name")
|
|
434
|
+
}),
|
|
435
|
+
handler: async ({ drugName: t }) => {
|
|
436
|
+
var a, c;
|
|
437
|
+
const n = new h().context("label").search(`openfda.brand_name:"${t}"`).limit(1).build(), { data: r, error: s } = await y(n);
|
|
438
|
+
if (s)
|
|
439
|
+
return {
|
|
440
|
+
content: [
|
|
441
|
+
{
|
|
442
|
+
type: "text",
|
|
443
|
+
text: `Failed to retrieve safety information for "${t}": ${s.message}`
|
|
444
|
+
}
|
|
445
|
+
]
|
|
446
|
+
};
|
|
447
|
+
if (!r || !r.results || r.results.length === 0)
|
|
448
|
+
return {
|
|
449
|
+
content: [
|
|
450
|
+
{
|
|
451
|
+
type: "text",
|
|
452
|
+
text: `No safety information found for "${t}".`
|
|
453
|
+
}
|
|
454
|
+
]
|
|
455
|
+
};
|
|
456
|
+
const e = r.results[0], p = {
|
|
457
|
+
drug_name: ((a = e == null ? void 0 : e.openfda.brand_name) == null ? void 0 : a[0]) || t,
|
|
458
|
+
generic_name: ((c = e == null ? void 0 : e.openfda.generic_name) == null ? void 0 : c[0]) || "Unknown",
|
|
459
|
+
warnings: (e == null ? void 0 : e.warnings) || [],
|
|
460
|
+
contraindications: (e == null ? void 0 : e.contraindications) || [],
|
|
461
|
+
drug_interactions: (e == null ? void 0 : e.drug_interactions) || [],
|
|
462
|
+
precautions: (e == null ? void 0 : e.precautions) || [],
|
|
463
|
+
adverse_reactions: (e == null ? void 0 : e.adverse_reactions) || [],
|
|
464
|
+
overdosage: (e == null ? void 0 : e.overdosage) || [],
|
|
465
|
+
do_not_use: (e == null ? void 0 : e.do_not_use) || [],
|
|
466
|
+
ask_doctor: (e == null ? void 0 : e.ask_doctor) || [],
|
|
467
|
+
stop_use: (e == null ? void 0 : e.stop_use) || [],
|
|
468
|
+
pregnancy_or_breast_feeding: (e == null ? void 0 : e.pregnancy_or_breast_feeding) || []
|
|
301
469
|
};
|
|
302
470
|
return {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
471
|
+
content: [
|
|
472
|
+
{
|
|
473
|
+
type: "text",
|
|
474
|
+
text: `Safety information for "${t}":
|
|
475
|
+
|
|
476
|
+
${JSON.stringify(p, null, 2)}`
|
|
477
|
+
}
|
|
478
|
+
]
|
|
307
479
|
};
|
|
480
|
+
}
|
|
308
481
|
});
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
:
|
|
482
|
+
b.registerTool({
|
|
483
|
+
name: "get-drug-by-ndc",
|
|
484
|
+
description: "Get drug information by National Drug Code (NDC). Accepts both product NDC (XXXXX-XXXX) and package NDC (XXXXX-XXXX-XX) formats. Also accepts NDC codes without dashes.",
|
|
485
|
+
schema: u.object({
|
|
486
|
+
ndcCode: u.string().describe(
|
|
487
|
+
"National Drug Code (NDC) - accepts formats: XXXXX-XXXX, XXXXX-XXXX-XX, or without dashes"
|
|
488
|
+
)
|
|
489
|
+
}),
|
|
490
|
+
handler: async ({ ndcCode: t }) => {
|
|
491
|
+
const { productNDC: n, packageNDC: r, isValid: s } = A(t);
|
|
492
|
+
if (!s)
|
|
493
|
+
return {
|
|
494
|
+
content: [
|
|
495
|
+
{
|
|
496
|
+
type: "text",
|
|
497
|
+
text: `Invalid NDC format: "${t}"
|
|
498
|
+
|
|
499
|
+
✅ Accepted formats:
|
|
500
|
+
• Product NDC: 12345-1234
|
|
501
|
+
• Package NDC: 12345-1234-01
|
|
502
|
+
• Without dashes: 123451234 or 12345123401`
|
|
503
|
+
}
|
|
504
|
+
]
|
|
505
|
+
};
|
|
506
|
+
console.log(
|
|
507
|
+
`Searching for NDC: input="${t}", productNDC="${n}", packageNDC="${r}"`
|
|
508
|
+
);
|
|
509
|
+
let e = `openfda.product_ndc:"${n}"`;
|
|
510
|
+
r && (e += `+OR+openfda.package_ndc:"${r}"`);
|
|
511
|
+
const p = new h().context("label").search(e).limit(10).build(), { data: a, error: c } = await y(p);
|
|
512
|
+
if (c)
|
|
513
|
+
return {
|
|
514
|
+
content: [
|
|
515
|
+
{
|
|
516
|
+
type: "text",
|
|
517
|
+
text: `Failed to retrieve drug data for NDC "${t}": ${c.message}`
|
|
518
|
+
}
|
|
519
|
+
]
|
|
520
|
+
};
|
|
521
|
+
if (!a || !a.results || a.results.length === 0)
|
|
522
|
+
return {
|
|
523
|
+
content: [
|
|
524
|
+
{
|
|
525
|
+
type: "text",
|
|
526
|
+
text: `No drug found with NDC "${t}" (product: ${n}).
|
|
527
|
+
|
|
528
|
+
💡 Tips:
|
|
529
|
+
• Verify the NDC format
|
|
530
|
+
• Try without the package suffix (e.g., use 12345-1234 instead of 12345-1234-01)
|
|
531
|
+
• Check if this is an FDA-approved product`
|
|
532
|
+
}
|
|
533
|
+
]
|
|
534
|
+
};
|
|
535
|
+
const o = a.results.map((l) => {
|
|
536
|
+
var g, $;
|
|
537
|
+
const m = ((g = l.openfda.product_ndc) == null ? void 0 : g.filter((_) => _ === n)) || [], f = (($ = l.openfda.package_ndc) == null ? void 0 : $.filter(
|
|
538
|
+
(_) => r ? _ === r : _.startsWith(n)
|
|
539
|
+
)) || [];
|
|
540
|
+
return {
|
|
541
|
+
// Basic drug information
|
|
542
|
+
brand_name: l.openfda.brand_name || [],
|
|
543
|
+
generic_name: l.openfda.generic_name || [],
|
|
544
|
+
manufacturer_name: l.openfda.manufacturer_name || [],
|
|
545
|
+
product_type: l.openfda.product_type || [],
|
|
546
|
+
route: l.openfda.route || [],
|
|
547
|
+
substance_name: l.openfda.substance_name || [],
|
|
548
|
+
// NDC information
|
|
549
|
+
matching_product_ndc: m,
|
|
550
|
+
matching_package_ndc: f,
|
|
551
|
+
all_product_ndc: l.openfda.product_ndc || [],
|
|
552
|
+
all_package_ndc: l.openfda.package_ndc || [],
|
|
553
|
+
// Additional product details
|
|
554
|
+
dosage_and_administration: l.dosage_and_administration || [],
|
|
555
|
+
package_label_principal_display_panel: l.package_label_principal_display_panel || [],
|
|
556
|
+
active_ingredient: l.active_ingredient || [],
|
|
557
|
+
purpose: l.purpose || []
|
|
558
|
+
};
|
|
559
|
+
}), d = o.reduce(
|
|
560
|
+
(l, m) => l + m.matching_package_ndc.length,
|
|
561
|
+
0
|
|
562
|
+
), i = r ? `Searched for specific package NDC: ${r}` : `Searched for product NDC: ${n} (all packages)`;
|
|
379
563
|
return {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
564
|
+
content: [
|
|
565
|
+
{
|
|
566
|
+
type: "text",
|
|
567
|
+
text: `✅ Found ${o.length} drug(s) with ${d} package(s) for NDC "${t}"
|
|
568
|
+
|
|
569
|
+
${i}
|
|
570
|
+
|
|
571
|
+
${JSON.stringify(o, null, 2)}`
|
|
572
|
+
}
|
|
573
|
+
]
|
|
384
574
|
};
|
|
575
|
+
}
|
|
385
576
|
});
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
577
|
+
b.registerTool({
|
|
578
|
+
name: "get-drug-by-product-ndc",
|
|
579
|
+
description: "Get drug information by product NDC only (XXXXX-XXXX format). This ignores package variations and finds all packages for a product.",
|
|
580
|
+
schema: u.object({
|
|
581
|
+
productNDC: u.string().describe("Product NDC in format XXXXX-XXXX")
|
|
582
|
+
}),
|
|
583
|
+
handler: async ({ productNDC: t }) => {
|
|
584
|
+
var c;
|
|
585
|
+
if (!/^\d{5}-\d{4}$/.test(t.trim()))
|
|
586
|
+
return {
|
|
587
|
+
content: [
|
|
588
|
+
{
|
|
589
|
+
type: "text",
|
|
590
|
+
text: `Invalid product NDC format: "${t}"
|
|
591
|
+
|
|
592
|
+
✅ Required format: XXXXX-XXXX (e.g., 12345-1234)`
|
|
593
|
+
}
|
|
594
|
+
]
|
|
595
|
+
};
|
|
596
|
+
const n = new h().context("label").search(`openfda.product_ndc:"${t.trim()}"`).limit(1).build(), { data: r, error: s } = await y(n);
|
|
597
|
+
if (s)
|
|
598
|
+
return {
|
|
599
|
+
content: [
|
|
600
|
+
{
|
|
601
|
+
type: "text",
|
|
602
|
+
text: `Failed to retrieve drug data for product NDC "${t}": ${s.message}`
|
|
603
|
+
}
|
|
604
|
+
]
|
|
605
|
+
};
|
|
606
|
+
if (!r || !r.results || r.results.length === 0)
|
|
607
|
+
return {
|
|
608
|
+
content: [
|
|
609
|
+
{
|
|
610
|
+
type: "text",
|
|
611
|
+
text: `No drug found with product NDC "${t}".`
|
|
612
|
+
}
|
|
613
|
+
]
|
|
614
|
+
};
|
|
615
|
+
const e = r.results[0], p = ((c = e.openfda.package_ndc) == null ? void 0 : c.filter(
|
|
616
|
+
(o) => o.startsWith(t.trim())
|
|
617
|
+
)) || [], a = {
|
|
618
|
+
product_ndc: t,
|
|
619
|
+
available_packages: p,
|
|
620
|
+
brand_name: e.openfda.brand_name || [],
|
|
621
|
+
generic_name: e.openfda.generic_name || [],
|
|
622
|
+
manufacturer_name: e.openfda.manufacturer_name || [],
|
|
623
|
+
product_type: e.openfda.product_type || [],
|
|
624
|
+
route: e.openfda.route || [],
|
|
625
|
+
substance_name: e.openfda.substance_name || [],
|
|
626
|
+
active_ingredient: e.active_ingredient || [],
|
|
627
|
+
purpose: e.purpose || [],
|
|
628
|
+
dosage_and_administration: e.dosage_and_administration || []
|
|
436
629
|
};
|
|
437
630
|
return {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
631
|
+
content: [
|
|
632
|
+
{
|
|
633
|
+
type: "text",
|
|
634
|
+
text: `✅ Product NDC "${t}" found with ${p.length} package variation(s):
|
|
635
|
+
|
|
636
|
+
${JSON.stringify(a, null, 2)}`
|
|
637
|
+
}
|
|
638
|
+
]
|
|
442
639
|
};
|
|
640
|
+
}
|
|
443
641
|
});
|
|
444
|
-
async function
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
console.error("OpenFDA MCP Server running on stdio");
|
|
642
|
+
async function P() {
|
|
643
|
+
const t = new T();
|
|
644
|
+
await X.connect(t), console.error("OpenFDA MCP Server running on stdio");
|
|
448
645
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
process.exit(1);
|
|
646
|
+
P().catch((t) => {
|
|
647
|
+
console.error("Fatal error in main():", t), process.exit(1);
|
|
452
648
|
});
|