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