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