@ythalorossy/openfda 1.1.0 → 1.2.0
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 +5 -3
- package/dist/index.js +588 -303
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
import { McpServer as
|
|
6
|
-
import { StdioServerTransport as
|
|
7
|
-
import
|
|
8
|
-
const
|
|
2
|
+
var B = Object.defineProperty;
|
|
3
|
+
var J = (e, t, n) => t in e ? B(e, t, { enumerable: !0, configurable: !0, writable: !0, value: n }) : e[t] = n;
|
|
4
|
+
var A = (e, t, n) => J(e, typeof t != "symbol" ? t + "" : t, n);
|
|
5
|
+
import { McpServer as z } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { StdioServerTransport as H } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import u from "zod";
|
|
8
|
+
const Q = `OPENFDA_API_KEY is not set. This server reads the key from its process environment; a .env file is not loaded. Add it to the "env" block of your MCP client configuration. Get a free key at https://open.fda.gov/apis/authentication/
|
|
9
9
|
|
|
10
|
-
To run without a key on the unauthenticated tier (40 requests/minute, 1,000/day per IP, and no rate-limit headers), set OPENFDA_ALLOW_KEYLESS=1.`,
|
|
11
|
-
function
|
|
10
|
+
To run without a key on the unauthenticated tier (40 requests/minute, 1,000/day per IP, and no rate-limit headers), set OPENFDA_ALLOW_KEYLESS=1.`, Z = "openfda: OPENFDA_ALLOW_KEYLESS=1 — using the unauthenticated tier (40 requests/minute, no rate-limit visibility). Set OPENFDA_API_KEY for 240 requests/minute.", ee = "openfda: OPENFDA_API_KEY is not set; every tool call will return a configuration error. See https://open.fda.gov/apis/authentication/";
|
|
11
|
+
function O(e = process.env) {
|
|
12
12
|
var n;
|
|
13
13
|
const t = (n = e.OPENFDA_API_KEY) == null ? void 0 : n.trim();
|
|
14
|
-
return t ? { ok: !0, apiKey: t } : e.OPENFDA_ALLOW_KEYLESS === "1" ? { ok: !0, apiKey: null } : { ok: !1, message:
|
|
14
|
+
return t ? { ok: !0, apiKey: t } : e.OPENFDA_ALLOW_KEYLESS === "1" ? { ok: !0, apiKey: null } : { ok: !1, message: Q };
|
|
15
15
|
}
|
|
16
|
-
function
|
|
17
|
-
const t =
|
|
18
|
-
t.ok ? t.apiKey === null && console.error(
|
|
16
|
+
function te(e = process.env) {
|
|
17
|
+
const t = O(e);
|
|
18
|
+
t.ok ? t.apiKey === null && console.error(Z) : console.error(ee);
|
|
19
19
|
}
|
|
20
|
-
class
|
|
20
|
+
class ne {
|
|
21
21
|
constructor(t) {
|
|
22
|
-
|
|
22
|
+
A(this, "registerTool", (t) => this.server.registerTool(
|
|
23
23
|
t.name,
|
|
24
24
|
{
|
|
25
25
|
title: t.name,
|
|
@@ -27,9 +27,9 @@ class J {
|
|
|
27
27
|
inputSchema: t.inputSchema
|
|
28
28
|
},
|
|
29
29
|
async (n) => {
|
|
30
|
-
const
|
|
31
|
-
return
|
|
32
|
-
content: [{ type: "text", text:
|
|
30
|
+
const r = O();
|
|
31
|
+
return r.ok ? t.handler(n) : {
|
|
32
|
+
content: [{ type: "text", text: r.message }],
|
|
33
33
|
isError: !0
|
|
34
34
|
};
|
|
35
35
|
}
|
|
@@ -37,14 +37,14 @@ class J {
|
|
|
37
37
|
this.server = t;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
const m = (e) => Array.isArray(e) ? e : [],
|
|
40
|
+
const m = (e) => Array.isArray(e) ? e : [], M = (e) => {
|
|
41
41
|
const t = m(e.warnings);
|
|
42
42
|
return t.length > 0 ? t : m(e.warnings_and_cautions);
|
|
43
43
|
};
|
|
44
|
-
function
|
|
44
|
+
function re(e) {
|
|
45
45
|
return {
|
|
46
46
|
boxed_warning: m(e.boxed_warning),
|
|
47
|
-
warnings:
|
|
47
|
+
warnings: M(e),
|
|
48
48
|
warnings_and_cautions: m(e.warnings_and_cautions),
|
|
49
49
|
contraindications: m(e.contraindications),
|
|
50
50
|
drug_interactions: m(e.drug_interactions),
|
|
@@ -57,11 +57,11 @@ function W(e) {
|
|
|
57
57
|
pregnancy_or_breast_feeding: m(e.pregnancy_or_breast_feeding)
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
-
function
|
|
60
|
+
function ae(e) {
|
|
61
61
|
return {
|
|
62
62
|
indications_and_usage: m(e.indications_and_usage),
|
|
63
63
|
boxed_warning: m(e.boxed_warning),
|
|
64
|
-
warnings:
|
|
64
|
+
warnings: M(e),
|
|
65
65
|
warnings_and_cautions: m(e.warnings_and_cautions),
|
|
66
66
|
do_not_use: m(e.do_not_use),
|
|
67
67
|
ask_doctor: m(e.ask_doctor),
|
|
@@ -70,10 +70,10 @@ function V(e) {
|
|
|
70
70
|
pregnancy_or_breast_feeding: m(e.pregnancy_or_breast_feeding)
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
|
-
class
|
|
73
|
+
class y {
|
|
74
74
|
constructor() {
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
A(this, "urlBase", "https://api.fda.gov");
|
|
76
|
+
A(this, "params", /* @__PURE__ */ new Map());
|
|
77
77
|
}
|
|
78
78
|
dataset(t) {
|
|
79
79
|
return this.params.set("dataset", t), this;
|
|
@@ -87,87 +87,104 @@ class b {
|
|
|
87
87
|
limit(t = 1) {
|
|
88
88
|
return this.params.set("limit", t), this;
|
|
89
89
|
}
|
|
90
|
+
/** Aggregate by a field instead of returning records. */
|
|
91
|
+
count(t) {
|
|
92
|
+
return this.params.set("count", t), this;
|
|
93
|
+
}
|
|
94
|
+
/** Offset into the result set. openFDA rejects values above 25000. */
|
|
95
|
+
skip(t) {
|
|
96
|
+
return this.params.set("skip", t), this;
|
|
97
|
+
}
|
|
98
|
+
/** e.g. 'receivedate:desc'. */
|
|
99
|
+
sort(t) {
|
|
100
|
+
return this.params.set("sort", t), this;
|
|
101
|
+
}
|
|
90
102
|
build() {
|
|
91
|
-
const t = this.params.get("dataset"), n = this.params.get("context"),
|
|
92
|
-
if (!t || !n || !
|
|
103
|
+
const t = this.params.get("dataset"), n = this.params.get("context"), r = this.params.get("search"), i = this.params.get("limit") ?? 1;
|
|
104
|
+
if (!t || !n || !r)
|
|
93
105
|
throw new Error("Missing required parameters: context or search");
|
|
94
|
-
const o =
|
|
95
|
-
|
|
106
|
+
const o = O(), s = new URLSearchParams();
|
|
107
|
+
o.ok && o.apiKey && s.set("api_key", o.apiKey), s.set("search", String(r)), s.set("limit", String(i));
|
|
108
|
+
for (const d of ["count", "skip", "sort"]) {
|
|
109
|
+
const c = this.params.get(d);
|
|
110
|
+
c !== void 0 && s.set(d, String(c));
|
|
111
|
+
}
|
|
112
|
+
return `${this.urlBase}/${t}/${n}.json?${s}`;
|
|
96
113
|
}
|
|
97
114
|
}
|
|
98
|
-
const
|
|
115
|
+
const U = (e) => e.replace(/([?&]api_key=)[^&\s]*/gi, "$1<REDACTED>"), se = {
|
|
99
116
|
maxRetries: 3,
|
|
100
117
|
retryDelay: 1e3,
|
|
101
118
|
// 1 second
|
|
102
119
|
timeout: 3e4
|
|
103
120
|
// 30 seconds
|
|
104
121
|
};
|
|
105
|
-
function
|
|
122
|
+
function I(e) {
|
|
106
123
|
return !!(e.name === "TypeError" && e.message.includes("fetch") || e.name === "AbortError" || e.status >= 500 && e.status <= 599 || e.status === 429);
|
|
107
124
|
}
|
|
108
|
-
function
|
|
125
|
+
function P(e) {
|
|
109
126
|
return new Promise((t) => setTimeout(t, e));
|
|
110
127
|
}
|
|
111
|
-
async function
|
|
112
|
-
const { maxRetries: n, retryDelay:
|
|
128
|
+
async function v(e, t = {}) {
|
|
129
|
+
const { maxRetries: n, retryDelay: r, timeout: i } = { ...se, ...t }, o = {
|
|
113
130
|
"User-Agent": "@ythalorossy/openfda",
|
|
114
131
|
Accept: "application/json"
|
|
115
132
|
};
|
|
116
133
|
let s = null;
|
|
117
|
-
for (let
|
|
134
|
+
for (let d = 0; d <= n; d++)
|
|
118
135
|
try {
|
|
119
|
-
const c = new AbortController(),
|
|
136
|
+
const c = new AbortController(), l = setTimeout(() => c.abort(), i), a = await fetch(e, {
|
|
120
137
|
headers: o,
|
|
121
138
|
signal: c.signal
|
|
122
139
|
});
|
|
123
|
-
if (clearTimeout(
|
|
124
|
-
const
|
|
140
|
+
if (clearTimeout(l), !a.ok) {
|
|
141
|
+
const f = await a.text().catch(() => "Unable to read error response"), _ = {
|
|
125
142
|
type: "http",
|
|
126
|
-
message: `HTTP ${
|
|
127
|
-
status:
|
|
128
|
-
details:
|
|
143
|
+
message: `HTTP ${a.status}: ${a.statusText}`,
|
|
144
|
+
status: a.status,
|
|
145
|
+
details: f
|
|
129
146
|
};
|
|
130
|
-
switch (
|
|
147
|
+
switch (a.status) {
|
|
131
148
|
case 400:
|
|
132
|
-
|
|
149
|
+
_.message = "Bad Request: Invalid search query or parameters";
|
|
133
150
|
break;
|
|
134
151
|
case 401:
|
|
135
|
-
|
|
152
|
+
_.message = "Unauthorized: Invalid or missing API key";
|
|
136
153
|
break;
|
|
137
154
|
case 403:
|
|
138
|
-
|
|
155
|
+
_.message = "Forbidden: API key may be invalid or quota exceeded";
|
|
139
156
|
break;
|
|
140
157
|
case 404:
|
|
141
|
-
|
|
158
|
+
_.message = "Not Found: No results found for the specified query";
|
|
142
159
|
break;
|
|
143
160
|
case 429:
|
|
144
|
-
|
|
161
|
+
_.message = "Rate Limited: Too many requests. Retrying...";
|
|
145
162
|
break;
|
|
146
163
|
case 500:
|
|
147
|
-
|
|
164
|
+
_.message = "Server Error: OpenFDA service is experiencing issues";
|
|
148
165
|
break;
|
|
149
166
|
default:
|
|
150
|
-
|
|
167
|
+
_.message = `HTTP Error ${a.status}: ${a.statusText}`;
|
|
151
168
|
}
|
|
152
|
-
if (s =
|
|
169
|
+
if (s = _, a.status >= 400 && a.status < 500 && a.status !== 429)
|
|
153
170
|
break;
|
|
154
|
-
if (
|
|
155
|
-
const h =
|
|
156
|
-
await
|
|
171
|
+
if (d < n && I({ status: a.status })) {
|
|
172
|
+
const h = r * Math.pow(2, d);
|
|
173
|
+
await P(h);
|
|
157
174
|
continue;
|
|
158
175
|
}
|
|
159
176
|
break;
|
|
160
177
|
}
|
|
161
178
|
let g;
|
|
162
179
|
try {
|
|
163
|
-
g = await
|
|
164
|
-
} catch (
|
|
180
|
+
g = await a.json();
|
|
181
|
+
} catch (f) {
|
|
165
182
|
s = {
|
|
166
183
|
type: "parsing",
|
|
167
|
-
message:
|
|
168
|
-
`Failed to parse JSON response: ${
|
|
184
|
+
message: U(
|
|
185
|
+
`Failed to parse JSON response: ${f instanceof Error ? f.message : "Unknown parsing error"}`
|
|
169
186
|
),
|
|
170
|
-
details:
|
|
187
|
+
details: f
|
|
171
188
|
};
|
|
172
189
|
break;
|
|
173
190
|
}
|
|
@@ -180,65 +197,99 @@ async function k(e, t = {}) {
|
|
|
180
197
|
}
|
|
181
198
|
return { data: g, error: null };
|
|
182
199
|
} catch (c) {
|
|
183
|
-
let
|
|
184
|
-
if (c.name === "AbortError" ?
|
|
200
|
+
let l;
|
|
201
|
+
if (c.name === "AbortError" ? l = {
|
|
185
202
|
type: "timeout",
|
|
186
203
|
message: `Request timeout after ${i}ms`,
|
|
187
204
|
details: c
|
|
188
|
-
} : c instanceof TypeError && c.message.includes("fetch") ?
|
|
205
|
+
} : c instanceof TypeError && c.message.includes("fetch") ? l = {
|
|
189
206
|
type: "network",
|
|
190
207
|
message: "Network error: Unable to connect to OpenFDA API",
|
|
191
208
|
details: c.message
|
|
192
|
-
} :
|
|
209
|
+
} : l = {
|
|
193
210
|
type: "unknown",
|
|
194
|
-
message:
|
|
211
|
+
message: U(
|
|
195
212
|
`Unexpected error: ${c.message || "Unknown error occurred"}`
|
|
196
213
|
),
|
|
197
214
|
details: c
|
|
198
|
-
}, s =
|
|
199
|
-
const
|
|
200
|
-
await
|
|
215
|
+
}, s = l, d < n && I(c)) {
|
|
216
|
+
const a = r * Math.pow(2, d);
|
|
217
|
+
await P(a);
|
|
201
218
|
continue;
|
|
202
219
|
}
|
|
203
220
|
break;
|
|
204
221
|
}
|
|
205
222
|
return { data: null, error: s };
|
|
206
223
|
}
|
|
207
|
-
const
|
|
224
|
+
const j = [
|
|
208
225
|
"openfda.brand_name",
|
|
209
226
|
"openfda.generic_name",
|
|
210
227
|
"openfda.substance_name",
|
|
211
228
|
"spl_product_data_elements"
|
|
212
229
|
];
|
|
213
|
-
async function
|
|
230
|
+
async function X(e, t = 1) {
|
|
214
231
|
let n;
|
|
215
|
-
for (const
|
|
216
|
-
const i = new
|
|
232
|
+
for (const r of j) {
|
|
233
|
+
const i = new y().dataset("drug").context("label").search(`${r}:"${e}"`).limit(t).build(), { data: o, error: s } = await v(i);
|
|
217
234
|
if (s) {
|
|
218
235
|
n = s;
|
|
219
236
|
continue;
|
|
220
237
|
}
|
|
221
238
|
if (o != null && o.results && o.results.length > 0)
|
|
222
|
-
return { found: !0, matched_via:
|
|
239
|
+
return { found: !0, matched_via: r, data: o };
|
|
223
240
|
}
|
|
224
241
|
return { found: !1, error: n };
|
|
225
242
|
}
|
|
226
|
-
const
|
|
243
|
+
const L = (e) => `No label found for "${e}".
|
|
227
244
|
|
|
228
|
-
Searched, in order: ${
|
|
245
|
+
Searched, in order: ${j.join(", ")}.
|
|
229
246
|
|
|
230
247
|
Suggestions:
|
|
231
248
|
- Check the spelling.
|
|
232
249
|
- Try the generic name instead of the brand (e.g. "amiodarone" rather than "Cordarone").
|
|
233
250
|
- Try the originator brand rather than a repackager's name.
|
|
234
|
-
- Some discontinued brands have no current FDA label
|
|
251
|
+
- Some discontinued brands have no current FDA label.`;
|
|
252
|
+
function oe(e) {
|
|
253
|
+
const t = (n) => Array.isArray(n) && typeof n[0] == "string" && n[0] ? n[0] : void 0;
|
|
254
|
+
return t(e == null ? void 0 : e.generic_name) ?? t(e == null ? void 0 : e.substance_name) ?? "Unknown";
|
|
255
|
+
}
|
|
256
|
+
function x(e, t, n) {
|
|
257
|
+
return typeof t != "number" ? `Showing ${e} ${n} (total unknown)` : e >= t ? `Showing all ${t} ${n}` : `Showing ${e} of ${t} ${n}`;
|
|
258
|
+
}
|
|
259
|
+
function k(e, t, n) {
|
|
260
|
+
return {
|
|
261
|
+
total: typeof t == "number" ? t : null,
|
|
262
|
+
returned: e.length,
|
|
263
|
+
limit: n,
|
|
264
|
+
results: e
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const ie = {
|
|
235
268
|
name: "get-drug-by-name",
|
|
236
|
-
description: "
|
|
237
|
-
|
|
238
|
-
|
|
269
|
+
description: "Look up a drug by brand, generic or substance name. Returns brand_name, generic_name, manufacturer_name, product_ndc, substance_name, indications_and_usage, and the safety narrative: boxed_warning, warnings, warnings_and_cautions, do_not_use, ask_doctor, ask_doctor_or_pharmacist, stop_use and pregnancy_or_breast_feeding. Every field is always present, empty when the label has none. Reports matched_via to say which field matched, and a total for how many labels matched.",
|
|
270
|
+
returnsFields: [
|
|
271
|
+
"brand_name",
|
|
272
|
+
"generic_name",
|
|
273
|
+
"manufacturer_name",
|
|
274
|
+
"product_ndc",
|
|
275
|
+
"substance_name",
|
|
276
|
+
"boxed_warning",
|
|
277
|
+
"warnings",
|
|
278
|
+
"warnings_and_cautions",
|
|
279
|
+
"do_not_use",
|
|
280
|
+
"ask_doctor",
|
|
281
|
+
"ask_doctor_or_pharmacist",
|
|
282
|
+
"stop_use",
|
|
283
|
+
"pregnancy_or_breast_feeding",
|
|
284
|
+
"indications_and_usage",
|
|
285
|
+
"matched_via"
|
|
286
|
+
],
|
|
287
|
+
inputSchema: u.object({
|
|
288
|
+
drugName: u.string().describe("Drug name")
|
|
239
289
|
}),
|
|
240
290
|
async handler({ drugName: e }) {
|
|
241
|
-
|
|
291
|
+
var i, o, s, d;
|
|
292
|
+
const t = await X(e, 1);
|
|
242
293
|
if (!t.found)
|
|
243
294
|
return t.error ? {
|
|
244
295
|
content: [
|
|
@@ -249,9 +300,9 @@ Suggestions:
|
|
|
249
300
|
],
|
|
250
301
|
isError: !0
|
|
251
302
|
} : {
|
|
252
|
-
content: [{ type: "text", text:
|
|
303
|
+
content: [{ type: "text", text: L(e) }]
|
|
253
304
|
};
|
|
254
|
-
const n = t.data.results[0],
|
|
305
|
+
const n = t.data.results[0], r = {
|
|
255
306
|
brand_name: n == null ? void 0 : n.openfda.brand_name,
|
|
256
307
|
generic_name: n == null ? void 0 : n.openfda.generic_name,
|
|
257
308
|
manufacturer_name: n == null ? void 0 : n.openfda.manufacturer_name,
|
|
@@ -260,44 +311,37 @@ Suggestions:
|
|
|
260
311
|
route: n == null ? void 0 : n.openfda.route,
|
|
261
312
|
substance_name: n == null ? void 0 : n.openfda.substance_name,
|
|
262
313
|
matched_via: t.matched_via,
|
|
263
|
-
...
|
|
314
|
+
...ae(n)
|
|
264
315
|
};
|
|
265
316
|
return {
|
|
266
317
|
content: [
|
|
267
318
|
{
|
|
268
319
|
type: "text",
|
|
269
|
-
text:
|
|
320
|
+
text: `${x(1, (o = (i = t.data.meta) == null ? void 0 : i.results) == null ? void 0 : o.total, `labels matching "${e}"`)}
|
|
270
321
|
|
|
271
|
-
${JSON.stringify(
|
|
322
|
+
${JSON.stringify(k([r], (d = (s = t.data.meta) == null ? void 0 : s.results) == null ? void 0 : d.total, 1), null, 2)}`
|
|
272
323
|
}
|
|
273
324
|
]
|
|
274
325
|
};
|
|
275
326
|
}
|
|
276
|
-
}
|
|
277
|
-
function N(e, t, n) {
|
|
278
|
-
return typeof t != "number" ? `Showing ${e} ${n} (total unknown)` : e >= t ? `Showing all ${t} ${n}` : `Showing ${e} of ${t} ${n}`;
|
|
279
|
-
}
|
|
280
|
-
function S(e, t, n) {
|
|
281
|
-
return {
|
|
282
|
-
total: typeof t == "number" ? t : null,
|
|
283
|
-
returned: e.length,
|
|
284
|
-
limit: n,
|
|
285
|
-
results: e
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
const Q = {
|
|
327
|
+
}, ce = {
|
|
289
328
|
name: "get-drug-by-generic-name",
|
|
290
|
-
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.",
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
329
|
+
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 as results, up to limit, along with total matched and returned counts.",
|
|
330
|
+
// Raw upstream records wrapped in an envelope: declare only the envelope
|
|
331
|
+
// keys this tool guarantees (total, returned, limit, results), never
|
|
332
|
+
// inner record fields, because those vary per record. This tool does not
|
|
333
|
+
// emit matched_via — it searches a single fixed field.
|
|
334
|
+
returnsFields: ["total", "returned", "limit", "results"],
|
|
335
|
+
inputSchema: u.object({
|
|
336
|
+
genericName: u.string().describe("Generic drug name (active ingredient)"),
|
|
337
|
+
limit: u.number().optional().default(5).describe("Maximum number of results to return")
|
|
294
338
|
}),
|
|
295
339
|
async handler({
|
|
296
340
|
genericName: e,
|
|
297
341
|
limit: t
|
|
298
342
|
}) {
|
|
299
|
-
var s,
|
|
300
|
-
const n = new
|
|
343
|
+
var s, d, c, l;
|
|
344
|
+
const n = new y().dataset("drug").context("label").search(`openfda.generic_name:"${e}"`).limit(t).build(), { data: r, error: i } = await v(n);
|
|
301
345
|
if (i)
|
|
302
346
|
return {
|
|
303
347
|
content: [
|
|
@@ -308,7 +352,7 @@ const Q = {
|
|
|
308
352
|
],
|
|
309
353
|
isError: !0
|
|
310
354
|
};
|
|
311
|
-
if (!(
|
|
355
|
+
if (!(r != null && r.results) || r.results.length === 0)
|
|
312
356
|
return {
|
|
313
357
|
content: [
|
|
314
358
|
{
|
|
@@ -317,28 +361,28 @@ const Q = {
|
|
|
317
361
|
}
|
|
318
362
|
]
|
|
319
363
|
};
|
|
320
|
-
const o =
|
|
321
|
-
var g,
|
|
364
|
+
const o = r.results.map((a) => {
|
|
365
|
+
var g, f, _, h;
|
|
322
366
|
return {
|
|
323
|
-
brand_name: ((g =
|
|
324
|
-
generic_name: ((
|
|
325
|
-
manufacturer_name: ((
|
|
326
|
-
product_type: ((h =
|
|
327
|
-
route: (
|
|
367
|
+
brand_name: ((g = a == null ? void 0 : a.openfda.brand_name) == null ? void 0 : g[0]) || "Unknown",
|
|
368
|
+
generic_name: ((f = a == null ? void 0 : a.openfda.generic_name) == null ? void 0 : f[0]) || "Unknown",
|
|
369
|
+
manufacturer_name: ((_ = a == null ? void 0 : a.openfda.manufacturer_name) == null ? void 0 : _[0]) || "Unknown",
|
|
370
|
+
product_type: ((h = a == null ? void 0 : a.openfda.product_type) == null ? void 0 : h[0]) || "Unknown",
|
|
371
|
+
route: (a == null ? void 0 : a.openfda.route) || []
|
|
328
372
|
};
|
|
329
373
|
});
|
|
330
374
|
return {
|
|
331
375
|
content: [
|
|
332
376
|
{
|
|
333
377
|
type: "text",
|
|
334
|
-
text: `${
|
|
378
|
+
text: `${x(o.length, (d = (s = r.meta) == null ? void 0 : s.results) == null ? void 0 : d.total, `labels with generic name "${e}"`)}
|
|
335
379
|
|
|
336
|
-
${JSON.stringify(
|
|
380
|
+
${JSON.stringify(k(o, (l = (c = r.meta) == null ? void 0 : c.results) == null ? void 0 : l.total, t ?? 5), null, 2)}`
|
|
337
381
|
}
|
|
338
382
|
]
|
|
339
383
|
};
|
|
340
384
|
}
|
|
341
|
-
},
|
|
385
|
+
}, de = {
|
|
342
386
|
1: "Recovered/resolved",
|
|
343
387
|
2: "Recovering/resolving",
|
|
344
388
|
3: "Not recovered/not resolved",
|
|
@@ -346,49 +390,93 @@ ${JSON.stringify(S(o, (f = (c = a.meta) == null ? void 0 : c.results) == null ?
|
|
|
346
390
|
5: "Fatal",
|
|
347
391
|
6: "Unknown"
|
|
348
392
|
};
|
|
349
|
-
function
|
|
393
|
+
function ue(e) {
|
|
350
394
|
if (e == null || e === "") return "Not reported";
|
|
351
395
|
const t = String(e);
|
|
352
|
-
return
|
|
396
|
+
return de[t] ?? `Unrecognized outcome code "${t}"`;
|
|
397
|
+
}
|
|
398
|
+
const K = [
|
|
399
|
+
"patient.drug.openfda.generic_name",
|
|
400
|
+
"patient.drug.openfda.substance_name",
|
|
401
|
+
"patient.drug.medicinalproduct"
|
|
402
|
+
], G = `union(${K.join(", ")})`;
|
|
403
|
+
function q(e) {
|
|
404
|
+
return K.map((t) => `${t}:"${e}"`).join(
|
|
405
|
+
" OR "
|
|
406
|
+
);
|
|
353
407
|
}
|
|
354
|
-
function
|
|
408
|
+
function le(e) {
|
|
355
409
|
const t = /* @__PURE__ */ new Set(), n = [];
|
|
356
|
-
for (const
|
|
357
|
-
const i =
|
|
410
|
+
for (const r of e) {
|
|
411
|
+
const i = r == null ? void 0 : r.reactionmeddrapt;
|
|
358
412
|
if (!i) continue;
|
|
359
|
-
const o = `${i}\0${String(
|
|
360
|
-
t.has(o) || (t.add(o), n.push({ reaction: i, outcome:
|
|
413
|
+
const o = `${i}\0${String(r == null ? void 0 : r.reactionoutcome)}`;
|
|
414
|
+
t.has(o) || (t.add(o), n.push({ reaction: i, outcome: r == null ? void 0 : r.reactionoutcome }));
|
|
361
415
|
}
|
|
362
416
|
return n.slice(0, 3);
|
|
363
417
|
}
|
|
364
|
-
const
|
|
418
|
+
const F = 25e3, pe = {
|
|
365
419
|
name: "get-drug-adverse-events",
|
|
366
|
-
description: "Get adverse event reports for a drug. This provides safety information about reported side effects and reactions. Use brand name or generic name.",
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
420
|
+
description: "Get adverse event reports for a drug. This provides safety information about reported side effects and reactions. Use brand name or generic name. Without the sort parameter, results are a deterministic earliest-report_id slice, so a small sample is not representative. Returns results, up to limit, reporting matched_via, the total matched, and returned, the number actually sent back. When skip is set, the response also reports the offset (not declared here, since it is only present when skip is supplied).",
|
|
421
|
+
// Raw upstream records wrapped in an envelope: declare only the envelope
|
|
422
|
+
// keys this tool ALWAYS guarantees (matched_via, total, returned, limit,
|
|
423
|
+
// results), never inner record fields, because those vary per record.
|
|
424
|
+
// `skip` is deliberately NOT declared: it is only present in the payload
|
|
425
|
+
// when the caller supplies `skip`, and a conditional field must not be
|
|
426
|
+
// declared here (an earlier round established that declaring a
|
|
427
|
+
// sometimes-absent field makes this guard assert something false).
|
|
428
|
+
returnsFields: [
|
|
429
|
+
"matched_via",
|
|
430
|
+
"total",
|
|
431
|
+
"returned",
|
|
432
|
+
"limit",
|
|
433
|
+
"results"
|
|
434
|
+
],
|
|
435
|
+
inputSchema: u.object({
|
|
436
|
+
drugName: u.string().describe("Drug name (brand or generic)"),
|
|
437
|
+
limit: u.number().optional().default(10).describe("Maximum number of events to return"),
|
|
438
|
+
seriousness: u.enum(["serious", "non-serious", "all"]).optional().default("all").describe("Filter by event seriousness"),
|
|
439
|
+
skip: u.number().int().min(0).max(F).optional().describe(
|
|
440
|
+
`Offset into the result set, for paging past the limit. Maximum ${F}.`
|
|
441
|
+
),
|
|
442
|
+
sort: u.enum(["receivedate:desc", "receivedate:asc"]).optional().describe(
|
|
443
|
+
"Order results by report receive date. Without it, results are a deterministic earliest-report_id slice, so a small sample is not representative."
|
|
444
|
+
)
|
|
371
445
|
}),
|
|
372
446
|
async handler({
|
|
373
447
|
drugName: e,
|
|
374
448
|
limit: t,
|
|
375
|
-
seriousness: n
|
|
449
|
+
seriousness: n,
|
|
450
|
+
skip: r,
|
|
451
|
+
sort: i
|
|
376
452
|
}) {
|
|
377
|
-
var
|
|
378
|
-
|
|
379
|
-
n !== "all" && (a += ` AND serious:${n === "serious" ? "1" : "2"}`);
|
|
380
|
-
const i = new b().dataset("drug").context("event").search(a).limit(t).build(), { data: o, error: s } = await k(i);
|
|
381
|
-
if (s)
|
|
453
|
+
var h, E;
|
|
454
|
+
if (r !== void 0 && r > F)
|
|
382
455
|
return {
|
|
383
456
|
content: [
|
|
384
457
|
{
|
|
385
458
|
type: "text",
|
|
386
|
-
text: `
|
|
459
|
+
text: `skip must be ${F} or less (openFDA's ceiling); received ${r}. To reach records beyond that, narrow the search or use sort to bring the records you want into range.`
|
|
387
460
|
}
|
|
388
461
|
],
|
|
389
462
|
isError: !0
|
|
390
463
|
};
|
|
391
|
-
|
|
464
|
+
let o = q(e);
|
|
465
|
+
n !== "all" && (o = `(${o}) AND serious:${n === "serious" ? "1" : "2"}`);
|
|
466
|
+
const s = new y().dataset("drug").context("event").search(o).limit(t);
|
|
467
|
+
r !== void 0 && s.skip(r), i !== void 0 && s.sort(i);
|
|
468
|
+
const d = s.build(), { data: c, error: l } = await v(d);
|
|
469
|
+
if (l)
|
|
470
|
+
return {
|
|
471
|
+
content: [
|
|
472
|
+
{
|
|
473
|
+
type: "text",
|
|
474
|
+
text: `Failed to retrieve adverse events for "${e}": ${l.message}`
|
|
475
|
+
}
|
|
476
|
+
],
|
|
477
|
+
isError: !0
|
|
478
|
+
};
|
|
479
|
+
if (!(c != null && c.results) || c.results.length === 0)
|
|
392
480
|
return {
|
|
393
481
|
content: [
|
|
394
482
|
{
|
|
@@ -397,45 +485,63 @@ const ne = {
|
|
|
397
485
|
}
|
|
398
486
|
]
|
|
399
487
|
};
|
|
400
|
-
const
|
|
401
|
-
var
|
|
402
|
-
const
|
|
488
|
+
const a = c.results.map((p) => {
|
|
489
|
+
var N, D, S, $;
|
|
490
|
+
const w = le(((N = p.patient) == null ? void 0 : N.reaction) ?? []);
|
|
403
491
|
return {
|
|
404
|
-
report_id:
|
|
405
|
-
serious:
|
|
406
|
-
patient_age: ((
|
|
407
|
-
patient_sex: ((
|
|
492
|
+
report_id: p.safetyreportid,
|
|
493
|
+
serious: p.serious === "1" ? "Yes" : "No",
|
|
494
|
+
patient_age: ((D = p.patient) == null ? void 0 : D.patientonsetage) || "Unknown",
|
|
495
|
+
patient_sex: ((S = p.patient) == null ? void 0 : S.patientsex) === "1" ? "Male" : (($ = p.patient) == null ? void 0 : $.patientsex) === "2" ? "Female" : "Unknown",
|
|
408
496
|
// Derived from the same deduped pairs so the two arrays stay the
|
|
409
497
|
// same length and positionally aligned (see dedupeReactionPairs).
|
|
410
|
-
reactions:
|
|
411
|
-
outcomes:
|
|
412
|
-
report_date:
|
|
498
|
+
reactions: w.map((T) => T.reaction),
|
|
499
|
+
outcomes: w.map((T) => ue(T.outcome)),
|
|
500
|
+
report_date: p.receiptdate || "Unknown"
|
|
413
501
|
};
|
|
414
|
-
})
|
|
502
|
+
}), g = (E = (h = c.meta) == null ? void 0 : h.results) == null ? void 0 : E.total, f = r !== void 0 ? `${x(a.length, g, `adverse event reports for "${e}"`)}, starting at offset ${r}` : x(
|
|
503
|
+
a.length,
|
|
504
|
+
g,
|
|
505
|
+
`adverse event reports for "${e}"`
|
|
506
|
+
), _ = {
|
|
507
|
+
matched_via: G,
|
|
508
|
+
...r !== void 0 ? { skip: r } : {},
|
|
509
|
+
...k(a, g, t ?? 10)
|
|
510
|
+
};
|
|
415
511
|
return {
|
|
416
512
|
content: [
|
|
417
513
|
{
|
|
418
514
|
type: "text",
|
|
419
|
-
text: `${
|
|
515
|
+
text: `${f}
|
|
420
516
|
|
|
421
|
-
${JSON.stringify(
|
|
517
|
+
${JSON.stringify(_, null, 2)}`
|
|
422
518
|
}
|
|
423
519
|
]
|
|
424
520
|
};
|
|
425
521
|
}
|
|
426
|
-
},
|
|
522
|
+
}, me = {
|
|
427
523
|
name: "get-drugs-by-manufacturer",
|
|
428
|
-
description: "Get all drugs manufactured by a specific company. Useful for finding alternatives or checking manufacturer portfolios.",
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
524
|
+
description: "Get all drugs manufactured by a specific company. Useful for finding alternatives or checking manufacturer portfolios. Returns results, up to limit, reporting matched_via, the total matched, and returned, the number actually sent back.",
|
|
525
|
+
// Raw upstream records wrapped in an envelope: declare only the envelope
|
|
526
|
+
// keys this tool guarantees (matched_via, total, returned, limit,
|
|
527
|
+
// results), never inner record fields, because those vary per record.
|
|
528
|
+
returnsFields: [
|
|
529
|
+
"matched_via",
|
|
530
|
+
"total",
|
|
531
|
+
"returned",
|
|
532
|
+
"limit",
|
|
533
|
+
"results"
|
|
534
|
+
],
|
|
535
|
+
inputSchema: u.object({
|
|
536
|
+
manufacturerName: u.string().describe("Manufacturer/company name"),
|
|
537
|
+
limit: u.number().optional().default(20).describe("Maximum number of drugs to return")
|
|
432
538
|
}),
|
|
433
539
|
async handler({
|
|
434
540
|
manufacturerName: e,
|
|
435
541
|
limit: t
|
|
436
542
|
}) {
|
|
437
|
-
var s,
|
|
438
|
-
const n = new
|
|
543
|
+
var s, d, c, l;
|
|
544
|
+
const n = new y().dataset("drug").context("label").search(`openfda.manufacturer_name:"${e}"`).limit(t).build(), { data: r, error: i } = await v(n);
|
|
439
545
|
if (i)
|
|
440
546
|
return {
|
|
441
547
|
content: [
|
|
@@ -446,7 +552,7 @@ ${JSON.stringify(S(p, (g = (r = o.meta) == null ? void 0 : r.results) == null ?
|
|
|
446
552
|
],
|
|
447
553
|
isError: !0
|
|
448
554
|
};
|
|
449
|
-
if (!(
|
|
555
|
+
if (!(r != null && r.results) || r.results.length === 0)
|
|
450
556
|
return {
|
|
451
557
|
content: [
|
|
452
558
|
{
|
|
@@ -455,36 +561,53 @@ ${JSON.stringify(S(p, (g = (r = o.meta) == null ? void 0 : r.results) == null ?
|
|
|
455
561
|
}
|
|
456
562
|
]
|
|
457
563
|
};
|
|
458
|
-
const o =
|
|
459
|
-
var g,
|
|
564
|
+
const o = r.results.map((a) => {
|
|
565
|
+
var g, f, _, h;
|
|
460
566
|
return {
|
|
461
|
-
brand_name: ((g =
|
|
462
|
-
generic_name: ((
|
|
463
|
-
product_type: ((
|
|
464
|
-
route: (
|
|
465
|
-
ndc: ((h =
|
|
567
|
+
brand_name: ((g = a == null ? void 0 : a.openfda.brand_name) == null ? void 0 : g[0]) || "Unknown",
|
|
568
|
+
generic_name: ((f = a == null ? void 0 : a.openfda.generic_name) == null ? void 0 : f[0]) || "Unknown",
|
|
569
|
+
product_type: ((_ = a == null ? void 0 : a.openfda.product_type) == null ? void 0 : _[0]) || "Unknown",
|
|
570
|
+
route: (a == null ? void 0 : a.openfda.route) || [],
|
|
571
|
+
ndc: ((h = a == null ? void 0 : a.openfda.product_ndc) == null ? void 0 : h[0]) || "Unknown"
|
|
466
572
|
};
|
|
467
573
|
});
|
|
468
574
|
return {
|
|
469
575
|
content: [
|
|
470
576
|
{
|
|
471
577
|
type: "text",
|
|
472
|
-
text: `${
|
|
578
|
+
text: `${x(o.length, (d = (s = r.meta) == null ? void 0 : s.results) == null ? void 0 : d.total, `labels from manufacturer "${e}"`)}
|
|
473
579
|
|
|
474
|
-
${JSON.stringify(
|
|
580
|
+
${JSON.stringify({ matched_via: "openfda.manufacturer_name", ...k(o, (l = (c = r.meta) == null ? void 0 : c.results) == null ? void 0 : l.total, t ?? 20) }, null, 2)}`
|
|
475
581
|
}
|
|
476
582
|
]
|
|
477
583
|
};
|
|
478
584
|
}
|
|
479
|
-
},
|
|
585
|
+
}, fe = {
|
|
480
586
|
name: "get-drug-safety-info",
|
|
481
|
-
description: "Get
|
|
482
|
-
|
|
483
|
-
|
|
587
|
+
description: "Get safety information for a drug. Returns drug_name, generic_name, boxed_warning, warnings, warnings_and_cautions, contraindications, drug_interactions, precautions, adverse_reactions, overdosage, do_not_use, ask_doctor, stop_use and pregnancy_or_breast_feeding. Every field is always present, empty when the label has none, so an empty boxed_warning means the drug has none rather than that it was not checked. Accepts a brand, generic or substance name and reports matched_via.",
|
|
588
|
+
returnsFields: [
|
|
589
|
+
"drug_name",
|
|
590
|
+
"generic_name",
|
|
591
|
+
"matched_via",
|
|
592
|
+
"boxed_warning",
|
|
593
|
+
"warnings",
|
|
594
|
+
"warnings_and_cautions",
|
|
595
|
+
"contraindications",
|
|
596
|
+
"drug_interactions",
|
|
597
|
+
"precautions",
|
|
598
|
+
"adverse_reactions",
|
|
599
|
+
"overdosage",
|
|
600
|
+
"do_not_use",
|
|
601
|
+
"ask_doctor",
|
|
602
|
+
"stop_use",
|
|
603
|
+
"pregnancy_or_breast_feeding"
|
|
604
|
+
],
|
|
605
|
+
inputSchema: u.object({
|
|
606
|
+
drugName: u.string().describe("Drug brand name")
|
|
484
607
|
}),
|
|
485
608
|
async handler({ drugName: e }) {
|
|
486
|
-
var i
|
|
487
|
-
const t = await
|
|
609
|
+
var i;
|
|
610
|
+
const t = await X(e, 1);
|
|
488
611
|
if (!t.found)
|
|
489
612
|
return t.error ? {
|
|
490
613
|
content: [
|
|
@@ -495,13 +618,15 @@ ${JSON.stringify(S(o, (f = (c = a.meta) == null ? void 0 : c.results) == null ?
|
|
|
495
618
|
],
|
|
496
619
|
isError: !0
|
|
497
620
|
} : {
|
|
498
|
-
content: [{ type: "text", text:
|
|
621
|
+
content: [{ type: "text", text: L(e) }]
|
|
499
622
|
};
|
|
500
|
-
const n = t.data.results[0],
|
|
623
|
+
const n = t.data.results[0], r = {
|
|
501
624
|
drug_name: ((i = n == null ? void 0 : n.openfda.brand_name) == null ? void 0 : i[0]) || e,
|
|
502
|
-
generic_name: (
|
|
625
|
+
generic_name: oe(
|
|
626
|
+
(n == null ? void 0 : n.openfda) ?? {}
|
|
627
|
+
),
|
|
503
628
|
matched_via: t.matched_via,
|
|
504
|
-
...
|
|
629
|
+
...re(n)
|
|
505
630
|
};
|
|
506
631
|
return {
|
|
507
632
|
content: [
|
|
@@ -509,76 +634,94 @@ ${JSON.stringify(S(o, (f = (c = a.meta) == null ? void 0 : c.results) == null ?
|
|
|
509
634
|
type: "text",
|
|
510
635
|
text: `Safety information for "${e}":
|
|
511
636
|
|
|
512
|
-
${JSON.stringify(
|
|
637
|
+
${JSON.stringify(r, null, 2)}`
|
|
513
638
|
}
|
|
514
639
|
]
|
|
515
640
|
};
|
|
516
641
|
}
|
|
517
|
-
},
|
|
518
|
-
function
|
|
642
|
+
}, _e = /^(?:\d{4}-\d{4}|\d{5}-\d{3,4})$/, ge = /^\d{1,2}$/;
|
|
643
|
+
function V(e) {
|
|
519
644
|
const t = e.trim().toUpperCase(), n = { productNDC: t, packageNDC: null, isValid: !1 };
|
|
520
|
-
let
|
|
645
|
+
let r, i = null;
|
|
521
646
|
if (t.includes("-")) {
|
|
522
647
|
const o = t.split("-");
|
|
523
648
|
if (o.length === 2)
|
|
524
|
-
|
|
649
|
+
r = t;
|
|
525
650
|
else if (o.length === 3) {
|
|
526
651
|
const s = o[2];
|
|
527
|
-
if (!s || !
|
|
528
|
-
|
|
652
|
+
if (!s || !ge.test(s)) return n;
|
|
653
|
+
r = `${o[0]}-${o[1]}`, i = t;
|
|
529
654
|
} else
|
|
530
655
|
return n;
|
|
531
656
|
} else if (/^\d+$/.test(t))
|
|
532
657
|
if (t.length === 11)
|
|
533
|
-
|
|
658
|
+
r = `${t.slice(0, 5)}-${t.slice(5, 9)}`, i = `${r}-${t.slice(9, 11)}`;
|
|
534
659
|
else if (t.length === 9)
|
|
535
|
-
|
|
660
|
+
r = `${t.slice(0, 5)}-${t.slice(5, 9)}`;
|
|
536
661
|
else
|
|
537
662
|
return n;
|
|
538
663
|
else
|
|
539
664
|
return n;
|
|
540
|
-
return
|
|
665
|
+
return _e.test(r) ? { productNDC: r, packageNDC: i, isValid: !0 } : n;
|
|
541
666
|
}
|
|
542
|
-
const
|
|
667
|
+
const he = [
|
|
668
|
+
"✅ Accepted formats:",
|
|
669
|
+
"• 4-4 product NDC: 0456-4020",
|
|
670
|
+
"• 5-3 product NDC: 58151-155",
|
|
671
|
+
"• 5-4 product NDC: 12345-1234",
|
|
672
|
+
"• Package NDC: 12345-1234-01 or 58151-155-01",
|
|
673
|
+
"• Undashed 9 digits: 123451234 (read as 5-4)",
|
|
674
|
+
"• Undashed 11 digits: 12345123401 (read as 5-4-2)",
|
|
675
|
+
"",
|
|
676
|
+
"Undashed 8- and 10-digit input is rejected because the split is ambiguous: 8 digits could be 5-3 or 4-4, and 10 could be 4-4-2, 5-3-2 or 5-4-1. Guessing could return a different drug, so add the dashes instead."
|
|
677
|
+
].join(`
|
|
678
|
+
`), W = (e, t) => `Invalid ${t} format: "${e}"
|
|
679
|
+
|
|
680
|
+
${he}`, be = {
|
|
543
681
|
name: "get-drug-by-ndc",
|
|
544
|
-
description: "Get drug information by National Drug Code (NDC). Accepts
|
|
545
|
-
|
|
546
|
-
|
|
682
|
+
description: "Get drug information by National Drug Code (NDC). Accepts dashed formats: 4-4 (0456-4020), 5-3 (58151-155), 5-4 (12345-1234), and package NDC. Also accepts undashed 9-digit and 11-digit input. Undashed 8- and 10-digit input is rejected as ambiguous. Returns results, up to limit, reporting matched_via, the total matched, and returned, the number actually sent back.",
|
|
683
|
+
// Raw upstream records wrapped in an envelope: declare only the envelope
|
|
684
|
+
// keys this tool guarantees (matched_via, total, returned, limit,
|
|
685
|
+
// results), never inner record fields, because those vary per record.
|
|
686
|
+
returnsFields: [
|
|
687
|
+
"matched_via",
|
|
688
|
+
"total",
|
|
689
|
+
"returned",
|
|
690
|
+
"limit",
|
|
691
|
+
"results"
|
|
692
|
+
],
|
|
693
|
+
inputSchema: u.object({
|
|
694
|
+
ndcCode: u.string().describe(
|
|
547
695
|
"National Drug Code (NDC) - accepts formats: XXXXX-XXXX, XXXXX-XXXX-XX, or without dashes"
|
|
548
696
|
)
|
|
549
697
|
}),
|
|
550
698
|
async handler({ ndcCode: e }) {
|
|
551
|
-
var
|
|
552
|
-
const { productNDC: t, packageNDC: n, isValid:
|
|
553
|
-
if (!
|
|
699
|
+
var f, _, h, E;
|
|
700
|
+
const { productNDC: t, packageNDC: n, isValid: r } = V(e);
|
|
701
|
+
if (!r)
|
|
554
702
|
return {
|
|
555
703
|
content: [
|
|
556
704
|
{
|
|
557
705
|
type: "text",
|
|
558
|
-
text:
|
|
559
|
-
|
|
560
|
-
✅ Accepted formats:
|
|
561
|
-
• Product NDC: 12345-1234
|
|
562
|
-
• Package NDC: 12345-1234-01
|
|
563
|
-
• Without dashes: 123451234 or 12345123401`
|
|
706
|
+
text: W(e, "NDC")
|
|
564
707
|
}
|
|
565
708
|
],
|
|
566
709
|
isError: !0
|
|
567
710
|
};
|
|
568
711
|
let i = `openfda.product_ndc:"${t}"`;
|
|
569
712
|
n && (i += ` OR openfda.package_ndc:"${n}"`);
|
|
570
|
-
const o = new
|
|
571
|
-
if (
|
|
713
|
+
const o = n ? "openfda.product_ndc OR openfda.package_ndc" : "openfda.product_ndc", s = new y().dataset("drug").context("label").search(i).limit(10).build(), { data: d, error: c } = await v(s);
|
|
714
|
+
if (c)
|
|
572
715
|
return {
|
|
573
716
|
content: [
|
|
574
717
|
{
|
|
575
718
|
type: "text",
|
|
576
|
-
text: `Failed to retrieve drug data for NDC "${e}": ${
|
|
719
|
+
text: `Failed to retrieve drug data for NDC "${e}": ${c.message}`
|
|
577
720
|
}
|
|
578
721
|
],
|
|
579
722
|
isError: !0
|
|
580
723
|
};
|
|
581
|
-
if (!(
|
|
724
|
+
if (!(d != null && d.results) || d.results.length === 0)
|
|
582
725
|
return {
|
|
583
726
|
content: [
|
|
584
727
|
{
|
|
@@ -592,75 +735,82 @@ const ie = {
|
|
|
592
735
|
}
|
|
593
736
|
]
|
|
594
737
|
};
|
|
595
|
-
const
|
|
596
|
-
var
|
|
597
|
-
const
|
|
598
|
-
(
|
|
738
|
+
const l = d.results.map((p) => {
|
|
739
|
+
var D, S;
|
|
740
|
+
const w = ((D = p.openfda.product_ndc) == null ? void 0 : D.filter(($) => $ === t)) || [], N = ((S = p.openfda.package_ndc) == null ? void 0 : S.filter(
|
|
741
|
+
($) => n ? $ === n : $.startsWith(t)
|
|
599
742
|
)) || [];
|
|
600
743
|
return {
|
|
601
|
-
brand_name:
|
|
602
|
-
generic_name:
|
|
603
|
-
manufacturer_name:
|
|
604
|
-
product_type:
|
|
605
|
-
route:
|
|
606
|
-
substance_name:
|
|
607
|
-
matching_product_ndc:
|
|
608
|
-
matching_package_ndc:
|
|
609
|
-
all_product_ndc:
|
|
610
|
-
all_package_ndc:
|
|
611
|
-
dosage_and_administration:
|
|
612
|
-
package_label_principal_display_panel:
|
|
613
|
-
active_ingredient:
|
|
614
|
-
purpose:
|
|
744
|
+
brand_name: p.openfda.brand_name || [],
|
|
745
|
+
generic_name: p.openfda.generic_name || [],
|
|
746
|
+
manufacturer_name: p.openfda.manufacturer_name || [],
|
|
747
|
+
product_type: p.openfda.product_type || [],
|
|
748
|
+
route: p.openfda.route || [],
|
|
749
|
+
substance_name: p.openfda.substance_name || [],
|
|
750
|
+
matching_product_ndc: w,
|
|
751
|
+
matching_package_ndc: N,
|
|
752
|
+
all_product_ndc: p.openfda.product_ndc || [],
|
|
753
|
+
all_package_ndc: p.openfda.package_ndc || [],
|
|
754
|
+
dosage_and_administration: p.dosage_and_administration || [],
|
|
755
|
+
package_label_principal_display_panel: p.package_label_principal_display_panel || [],
|
|
756
|
+
active_ingredient: p.active_ingredient || [],
|
|
757
|
+
purpose: p.purpose || []
|
|
615
758
|
};
|
|
616
|
-
}),
|
|
617
|
-
(
|
|
759
|
+
}), a = l.reduce(
|
|
760
|
+
(p, w) => p + w.matching_package_ndc.length,
|
|
618
761
|
0
|
|
619
|
-
),
|
|
762
|
+
), g = n ? `Searched for specific package NDC: ${n}` : `Searched for product NDC: ${t} (all packages)`;
|
|
620
763
|
return {
|
|
621
764
|
content: [
|
|
622
765
|
{
|
|
623
766
|
type: "text",
|
|
624
|
-
text: `${
|
|
767
|
+
text: `${x(l.length, (_ = (f = d.meta) == null ? void 0 : f.results) == null ? void 0 : _.total, `labels for NDC "${e}"`)} with ${a} package(s)
|
|
625
768
|
|
|
626
|
-
${
|
|
769
|
+
${g}
|
|
627
770
|
|
|
628
|
-
${JSON.stringify(
|
|
771
|
+
${JSON.stringify({ matched_via: o, ...k(l, (E = (h = d.meta) == null ? void 0 : h.results) == null ? void 0 : E.total, 10) }, null, 2)}`
|
|
629
772
|
}
|
|
630
773
|
]
|
|
631
774
|
};
|
|
632
775
|
}
|
|
633
|
-
},
|
|
776
|
+
}, ye = {
|
|
634
777
|
name: "get-drug-by-product-ndc",
|
|
635
|
-
description: "Get drug information by product NDC. Accepts the dashed forms 4-4 (0456-4020), 5-3 (58151-155) and 5-4 (12345-1234), plus undashed 9-digit (5-4) and 11-digit (5-4-2) input. Undashed 8- and 10-digit input is rejected as ambiguous — dash it. This ignores package variations and finds all packages for a product.",
|
|
636
|
-
|
|
637
|
-
|
|
778
|
+
description: "Get drug information by product NDC. Accepts the dashed forms 4-4 (0456-4020), 5-3 (58151-155) and 5-4 (12345-1234), plus undashed 9-digit (5-4) and 11-digit (5-4-2) input. Undashed 8- and 10-digit input is rejected as ambiguous — dash it. This ignores package variations and finds all packages for a product. Returns product_ndc, available_packages, brand_name, generic_name, manufacturer_name, product_type, route, substance_name, active_ingredient, purpose and dosage_and_administration; every field is always present, empty when the label has none.",
|
|
779
|
+
// Fixed payload of named fields (one record, not an envelope over many):
|
|
780
|
+
// every key here is always set on the success path, defaulting to an
|
|
781
|
+
// empty array when the upstream label omits it.
|
|
782
|
+
returnsFields: [
|
|
783
|
+
"product_ndc",
|
|
784
|
+
"available_packages",
|
|
785
|
+
"brand_name",
|
|
786
|
+
"generic_name",
|
|
787
|
+
"manufacturer_name",
|
|
788
|
+
"product_type",
|
|
789
|
+
"route",
|
|
790
|
+
"substance_name",
|
|
791
|
+
"active_ingredient",
|
|
792
|
+
"purpose",
|
|
793
|
+
"dosage_and_administration"
|
|
794
|
+
],
|
|
795
|
+
inputSchema: u.object({
|
|
796
|
+
productNDC: u.string().describe(
|
|
638
797
|
"Product NDC: dashed 4-4 (0456-4020), 5-3 (58151-155) or 5-4 (12345-1234); undashed 9-digit (123451234) or 11-digit (12345123401) also work"
|
|
639
798
|
)
|
|
640
799
|
}),
|
|
641
800
|
async handler({ productNDC: e }) {
|
|
642
|
-
var
|
|
643
|
-
const { productNDC: t, isValid: n } =
|
|
801
|
+
var l;
|
|
802
|
+
const { productNDC: t, isValid: n } = V(e);
|
|
644
803
|
if (!n)
|
|
645
804
|
return {
|
|
646
805
|
content: [
|
|
647
806
|
{
|
|
648
807
|
type: "text",
|
|
649
|
-
text:
|
|
650
|
-
|
|
651
|
-
✅ Accepted formats:
|
|
652
|
-
• 4-4 product NDC: 0456-4020
|
|
653
|
-
• 5-3 product NDC: 58151-155
|
|
654
|
-
• 5-4 product NDC: 12345-1234
|
|
655
|
-
• Undashed 9 digits: 123451234 (read as 5-4)
|
|
656
|
-
• Undashed 11 digits: 12345123401 (read as 5-4-2)
|
|
657
|
-
|
|
658
|
-
Undashed 8- and 10-digit input is rejected because the split is ambiguous: 8 digits could be 5-3 or 4-4, and 10 could be 4-4-2, 5-3-2 or 5-4-1. Guessing could return a different drug, so add the dashes instead.`
|
|
808
|
+
text: W(e, "product NDC")
|
|
659
809
|
}
|
|
660
810
|
],
|
|
661
811
|
isError: !0
|
|
662
812
|
};
|
|
663
|
-
const
|
|
813
|
+
const r = new y().dataset("drug").context("label").search(`openfda.product_ndc:"${t}"`).limit(1).build(), { data: i, error: o } = await v(r);
|
|
664
814
|
if (o)
|
|
665
815
|
return {
|
|
666
816
|
content: [
|
|
@@ -681,11 +831,11 @@ Undashed 8- and 10-digit input is rejected because the split is ambiguous: 8 dig
|
|
|
681
831
|
],
|
|
682
832
|
structuredContent: null
|
|
683
833
|
};
|
|
684
|
-
const s = i.results[0],
|
|
685
|
-
(
|
|
834
|
+
const s = i.results[0], d = ((l = s.openfda.package_ndc) == null ? void 0 : l.filter(
|
|
835
|
+
(a) => a.startsWith(t)
|
|
686
836
|
)) || [], c = {
|
|
687
837
|
product_ndc: t,
|
|
688
|
-
available_packages:
|
|
838
|
+
available_packages: d,
|
|
689
839
|
brand_name: s.openfda.brand_name || [],
|
|
690
840
|
generic_name: s.openfda.generic_name || [],
|
|
691
841
|
manufacturer_name: s.openfda.manufacturer_name || [],
|
|
@@ -700,81 +850,215 @@ Undashed 8- and 10-digit input is rejected because the split is ambiguous: 8 dig
|
|
|
700
850
|
content: [
|
|
701
851
|
{
|
|
702
852
|
type: "text",
|
|
703
|
-
text: `✅ Product NDC "${t}" found with ${
|
|
853
|
+
text: `✅ Product NDC "${t}" found with ${d.length} package variation(s):
|
|
704
854
|
|
|
705
855
|
${JSON.stringify(c, null, 2)}`
|
|
706
856
|
}
|
|
707
857
|
]
|
|
708
858
|
};
|
|
709
859
|
}
|
|
710
|
-
},
|
|
860
|
+
}, R = {
|
|
861
|
+
application: {
|
|
862
|
+
prefix: "",
|
|
863
|
+
fields: {
|
|
864
|
+
application_number: {},
|
|
865
|
+
// Case-sensitive, stored uppercase: "UPJOHN" -> 13 results,
|
|
866
|
+
// "Upjohn"/"upjohn" -> NOT_FOUND. Normalised so a caller typing
|
|
867
|
+
// "Pfizer" does not get a silent miss that looks like absent data.
|
|
868
|
+
sponsor_name: { uppercase: !0 }
|
|
869
|
+
}
|
|
870
|
+
},
|
|
871
|
+
openfda: {
|
|
872
|
+
prefix: "openfda",
|
|
873
|
+
fields: {
|
|
874
|
+
application_number: {},
|
|
875
|
+
brand_name: {},
|
|
876
|
+
generic_name: {},
|
|
877
|
+
manufacturer_name: {},
|
|
878
|
+
route: {},
|
|
879
|
+
substance_name: {},
|
|
880
|
+
product_ndc: {}
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
products: {
|
|
884
|
+
prefix: "products",
|
|
885
|
+
fields: {
|
|
886
|
+
dosage_form: {},
|
|
887
|
+
marketing_status: {},
|
|
888
|
+
product_number: {},
|
|
889
|
+
reference_drug: {},
|
|
890
|
+
route: {},
|
|
891
|
+
te_code: {}
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
submissions: {
|
|
895
|
+
prefix: "submissions",
|
|
896
|
+
fields: {
|
|
897
|
+
review_priority: {},
|
|
898
|
+
submission_class_code: {},
|
|
899
|
+
submission_number: {},
|
|
900
|
+
submission_status: {},
|
|
901
|
+
submission_status_date: {},
|
|
902
|
+
submission_type: {}
|
|
903
|
+
}
|
|
904
|
+
},
|
|
905
|
+
application_docs: {
|
|
906
|
+
prefix: "submissions.application_docs",
|
|
907
|
+
fields: { id: {}, url: {}, date: {}, type: {} }
|
|
908
|
+
}
|
|
909
|
+
}, C = Object.keys(R);
|
|
910
|
+
function ve(e, t) {
|
|
911
|
+
const n = R[e];
|
|
912
|
+
if (!n)
|
|
913
|
+
return {
|
|
914
|
+
ok: !1,
|
|
915
|
+
message: `Unknown section "${e}". Valid sections: ${C.join(", ")}.`
|
|
916
|
+
};
|
|
917
|
+
const r = n.fields[t];
|
|
918
|
+
return r ? {
|
|
919
|
+
ok: !0,
|
|
920
|
+
path: n.prefix ? `${n.prefix}.${t}` : t,
|
|
921
|
+
uppercase: r.uppercase === !0
|
|
922
|
+
} : {
|
|
923
|
+
ok: !1,
|
|
924
|
+
message: `Unknown field "${t}" for section "${e}". Valid fields: ${Object.keys(n.fields).join(", ")}.`
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
const $e = (e) => `${e}: ${Object.keys(R[e].fields).join(", ")}`, xe = {
|
|
711
928
|
name: "get-drugsfda",
|
|
712
|
-
description: "
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
929
|
+
description: "Search Drugs@FDA application data by section and field. Returns application, sponsor, product and submission records as results, up to limit. Reports matched_via (the resolved field path), the total number of records matched, and returned, the number actually sent back.",
|
|
930
|
+
// Raw upstream records wrapped in an envelope: declare only the envelope
|
|
931
|
+
// keys this tool guarantees (matched_via, total, returned, limit,
|
|
932
|
+
// results), never inner record fields, because those vary per record.
|
|
933
|
+
returnsFields: [
|
|
934
|
+
"matched_via",
|
|
935
|
+
"total",
|
|
936
|
+
"returned",
|
|
937
|
+
"limit",
|
|
938
|
+
"results"
|
|
939
|
+
],
|
|
940
|
+
inputSchema: u.object({
|
|
941
|
+
sectionName: u.enum([...C]).describe(`Section to search. One of: ${C.join(", ")}`),
|
|
942
|
+
fieldName: u.string().describe(
|
|
943
|
+
`Field within the section. ${C.map($e).join(". ")}`
|
|
716
944
|
),
|
|
717
|
-
|
|
718
|
-
"
|
|
945
|
+
searchValue: u.string().describe(
|
|
946
|
+
"Value to search for. Sponsor names are stored uppercase and are normalised automatically."
|
|
719
947
|
),
|
|
720
|
-
|
|
948
|
+
limit: u.number().int().min(1).max(100).optional().default(5).describe("Maximum number of records to return")
|
|
721
949
|
}),
|
|
722
950
|
async handler({
|
|
723
951
|
sectionName: e,
|
|
724
952
|
fieldName: t,
|
|
725
|
-
searchValue: n
|
|
953
|
+
searchValue: n,
|
|
954
|
+
limit: r
|
|
726
955
|
}) {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
switch (o.type) {
|
|
731
|
-
case "http":
|
|
732
|
-
o.status === 404 ? s += `
|
|
733
|
-
|
|
734
|
-
Suggestions:
|
|
735
|
-
- Verify the field name is correct for the section
|
|
736
|
-
- Check the search value spelling` : (o.status === 401 || o.status === 403) && (s += `
|
|
737
|
-
|
|
738
|
-
Please check the API key configuration.`);
|
|
739
|
-
break;
|
|
740
|
-
case "network":
|
|
741
|
-
s += `
|
|
742
|
-
|
|
743
|
-
Please check your internet connection and try again.`;
|
|
744
|
-
break;
|
|
745
|
-
case "timeout":
|
|
746
|
-
s += `
|
|
747
|
-
|
|
748
|
-
The request took too long. Please try again.`;
|
|
749
|
-
break;
|
|
750
|
-
}
|
|
956
|
+
var g, f;
|
|
957
|
+
const i = r ?? 5, o = ve(e, t);
|
|
958
|
+
if (!o.ok)
|
|
751
959
|
return {
|
|
752
|
-
content: [{ type: "text", text:
|
|
960
|
+
content: [{ type: "text", text: o.message }],
|
|
753
961
|
isError: !0
|
|
754
962
|
};
|
|
755
|
-
}
|
|
756
|
-
|
|
963
|
+
const s = o.uppercase ? n.toUpperCase() : n, d = new y().dataset("drug").context("drugsfda").search(`${o.path}:"${s}"`).limit(i).build(), { data: c, error: l } = await v(d);
|
|
964
|
+
if (l)
|
|
965
|
+
return {
|
|
966
|
+
content: [
|
|
967
|
+
{
|
|
968
|
+
type: "text",
|
|
969
|
+
text: `Failed to retrieve Drugs@FDA data for "${s}" in ${o.path}: ${l.message}`
|
|
970
|
+
}
|
|
971
|
+
],
|
|
972
|
+
isError: !0
|
|
973
|
+
};
|
|
974
|
+
if (!(c != null && c.results) || c.results.length === 0)
|
|
975
|
+
return {
|
|
976
|
+
content: [
|
|
977
|
+
{
|
|
978
|
+
type: "text",
|
|
979
|
+
text: `No Drugs@FDA records found for "${s}" in ${o.path}.`
|
|
980
|
+
}
|
|
981
|
+
]
|
|
982
|
+
};
|
|
983
|
+
const a = (f = (g = c.meta) == null ? void 0 : g.results) == null ? void 0 : f.total;
|
|
984
|
+
return {
|
|
757
985
|
content: [
|
|
758
986
|
{
|
|
759
987
|
type: "text",
|
|
760
|
-
text:
|
|
988
|
+
text: `${x(c.results.length, a, `Drugs@FDA records matching ${o.path}`)}
|
|
989
|
+
|
|
990
|
+
${JSON.stringify({ matched_via: o.path, ...k(c.results, a, i) }, null, 2)}`
|
|
761
991
|
}
|
|
762
992
|
]
|
|
763
|
-
}
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
}, we = [
|
|
996
|
+
"patient.reaction.reactionmeddrapt.exact",
|
|
997
|
+
"patient.reaction.reactionoutcome",
|
|
998
|
+
"serious",
|
|
999
|
+
"patient.patientsex",
|
|
1000
|
+
"occurcountry.exact",
|
|
1001
|
+
"patient.drug.openfda.generic_name.exact"
|
|
1002
|
+
], ke = {
|
|
1003
|
+
name: "get-drug-adverse-event-counts",
|
|
1004
|
+
description: "Rank adverse-event values for a drug by frequency — for example the most commonly reported reactions. Returns aggregated {term, count} pairs as results, not individual reports, along with matched_via, counted_by (the field that was aggregated) and returned (how many ranked terms came back). Note that openFDA omits a result total on aggregated responses, so this tool reports no total; use get-drug-adverse-events for individual reports and their total.",
|
|
1005
|
+
// Raw upstream records wrapped in an envelope: declare only the envelope
|
|
1006
|
+
// keys this tool guarantees (matched_via, counted_by, returned, results),
|
|
1007
|
+
// never inner record fields, because those vary per record. There is no
|
|
1008
|
+
// `total` or `limit` here — openFDA omits a total on aggregated
|
|
1009
|
+
// responses, so this tool deliberately does not declare one.
|
|
1010
|
+
returnsFields: ["matched_via", "counted_by", "returned", "results"],
|
|
1011
|
+
inputSchema: u.object({
|
|
1012
|
+
drugName: u.string().describe("Drug name (brand, generic or substance)"),
|
|
1013
|
+
field: u.enum(we).optional().default("patient.reaction.reactionmeddrapt.exact").describe("Field to aggregate by"),
|
|
1014
|
+
limit: u.number().int().min(1).max(100).optional().default(10).describe("Maximum number of ranked terms to return")
|
|
1015
|
+
}),
|
|
1016
|
+
async handler({
|
|
1017
|
+
drugName: e,
|
|
1018
|
+
field: t,
|
|
1019
|
+
limit: n
|
|
1020
|
+
}) {
|
|
1021
|
+
const r = t ?? "patient.reaction.reactionmeddrapt.exact", i = n ?? 10, o = new y().dataset("drug").context("event").search(q(e)).count(r).limit(i).build(), { data: s, error: d } = await v(o);
|
|
1022
|
+
if (d)
|
|
1023
|
+
return {
|
|
1024
|
+
content: [
|
|
1025
|
+
{
|
|
1026
|
+
type: "text",
|
|
1027
|
+
text: `Failed to retrieve adverse event counts for "${e}": ${d.message}`
|
|
1028
|
+
}
|
|
1029
|
+
],
|
|
1030
|
+
isError: !0
|
|
1031
|
+
};
|
|
1032
|
+
if (!(s != null && s.results) || s.results.length === 0)
|
|
1033
|
+
return {
|
|
1034
|
+
content: [
|
|
1035
|
+
{
|
|
1036
|
+
type: "text",
|
|
1037
|
+
text: `No adverse event counts found for "${e}".`
|
|
1038
|
+
}
|
|
1039
|
+
]
|
|
1040
|
+
};
|
|
1041
|
+
const c = {
|
|
1042
|
+
matched_via: G,
|
|
1043
|
+
counted_by: r,
|
|
1044
|
+
returned: s.results.length,
|
|
1045
|
+
results: s.results
|
|
1046
|
+
};
|
|
1047
|
+
return {
|
|
764
1048
|
content: [
|
|
765
1049
|
{
|
|
766
1050
|
type: "text",
|
|
767
|
-
text: `
|
|
1051
|
+
text: `Top ${s.results.length} values of ${r} for "${e}" (aggregated responses carry no result total)
|
|
768
1052
|
|
|
769
|
-
${JSON.stringify(
|
|
1053
|
+
${JSON.stringify(c, null, 2)}`
|
|
770
1054
|
}
|
|
771
1055
|
]
|
|
772
1056
|
};
|
|
773
1057
|
}
|
|
774
|
-
},
|
|
1058
|
+
}, Ee = "1.2.0", Y = new z(
|
|
775
1059
|
{
|
|
776
1060
|
name: "openfda",
|
|
777
|
-
version:
|
|
1061
|
+
version: Ee,
|
|
778
1062
|
description: "OpenFDA Model Context Protocol"
|
|
779
1063
|
},
|
|
780
1064
|
{
|
|
@@ -783,20 +1067,21 @@ ${JSON.stringify(i.results[0], null, 2)}`
|
|
|
783
1067
|
tools: {}
|
|
784
1068
|
}
|
|
785
1069
|
}
|
|
786
|
-
),
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1070
|
+
), b = new ne(Y);
|
|
1071
|
+
b.registerTool(ie);
|
|
1072
|
+
b.registerTool(ce);
|
|
1073
|
+
b.registerTool(pe);
|
|
1074
|
+
b.registerTool(me);
|
|
1075
|
+
b.registerTool(fe);
|
|
1076
|
+
b.registerTool(be);
|
|
1077
|
+
b.registerTool(ye);
|
|
1078
|
+
b.registerTool(xe);
|
|
1079
|
+
b.registerTool(ke);
|
|
1080
|
+
async function De() {
|
|
1081
|
+
te();
|
|
1082
|
+
const e = new H();
|
|
1083
|
+
await Y.connect(e), console.error("OpenFDA MCP Server running on stdio");
|
|
799
1084
|
}
|
|
800
|
-
|
|
1085
|
+
De().catch((e) => {
|
|
801
1086
|
console.error("Fatal error in main():", e), process.exit(1);
|
|
802
1087
|
});
|