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