@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.
Files changed (3) hide show
  1. package/README.md +16 -5
  2. package/dist/index.js +669 -311
  3. package/package.json +4 -2
package/dist/index.js CHANGED
@@ -1,25 +1,25 @@
1
1
  #!/usr/bin/env node
2
- var M = Object.defineProperty;
3
- var G = (e, t, n) => t in e ? M(e, t, { enumerable: !0, configurable: !0, writable: !0, value: n }) : e[t] = n;
4
- var D = (e, t, n) => G(e, typeof t != "symbol" ? t + "" : t, n);
5
- import { McpServer as K } from "@modelcontextprotocol/sdk/server/mcp.js";
6
- import { StdioServerTransport as L } from "@modelcontextprotocol/sdk/server/stdio.js";
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 j = `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/
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.`, q = "openfda: OPENFDA_ALLOW_KEYLESS=1 — using the unauthenticated tier (40 requests/minute, no rate-limit visibility). Set OPENFDA_API_KEY for 240 requests/minute.", Y = "openfda: OPENFDA_API_KEY is not set; every tool call will return a configuration error. See https://open.fda.gov/apis/authentication/";
11
- function E(e = process.env) {
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: j };
14
+ return t ? { ok: !0, apiKey: t } : e.OPENFDA_ALLOW_KEYLESS === "1" ? { ok: !0, apiKey: null } : { ok: !1, message: ne };
15
15
  }
16
- function B(e = process.env) {
17
- const t = E(e);
18
- t.ok ? t.apiKey === null && console.error(q) : console.error(Y);
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 J {
20
+ class oe {
21
21
  constructor(t) {
22
- D(this, "registerTool", (t) => this.server.registerTool(
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 = E();
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 m = (e) => Array.isArray(e) ? e : [], P = (e) => {
41
- const t = m(e.warnings);
42
- return t.length > 0 ? t : m(e.warnings_and_cautions);
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 W(e) {
44
+ function ie(e) {
45
45
  return {
46
- boxed_warning: m(e.boxed_warning),
47
- warnings: P(e),
48
- warnings_and_cautions: m(e.warnings_and_cautions),
49
- contraindications: m(e.contraindications),
50
- drug_interactions: m(e.drug_interactions),
51
- precautions: m(e.precautions),
52
- adverse_reactions: m(e.adverse_reactions),
53
- overdosage: m(e.overdosage),
54
- do_not_use: m(e.do_not_use),
55
- ask_doctor: m(e.ask_doctor),
56
- stop_use: m(e.stop_use),
57
- pregnancy_or_breast_feeding: m(e.pregnancy_or_breast_feeding)
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 V(e) {
61
+ function ce(e) {
61
62
  return {
62
- indications_and_usage: m(e.indications_and_usage),
63
- boxed_warning: m(e.boxed_warning),
64
- warnings: P(e),
65
- warnings_and_cautions: m(e.warnings_and_cautions),
66
- do_not_use: m(e.do_not_use),
67
- ask_doctor: m(e.ask_doctor),
68
- ask_doctor_or_pharmacist: m(e.ask_doctor_or_pharmacist),
69
- stop_use: m(e.stop_use),
70
- pregnancy_or_breast_feeding: m(e.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 b {
74
+ class x {
74
75
  constructor() {
75
- D(this, "urlBase", "https://api.fda.gov");
76
- D(this, "params", /* @__PURE__ */ new Map());
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 o = E(), s = new URLSearchParams();
95
- return o.ok && o.apiKey && s.set("api_key", o.apiKey), s.set("search", String(a)), s.set("limit", String(i)), `${this.urlBase}/${t}/${n}.json?${s}`;
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 C = (e) => e.replace(/([?&]api_key=)[^&\s]*/gi, "$1<REDACTED>"), z = {
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 T(e) {
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 F(e) {
126
+ function P(e) {
109
127
  return new Promise((t) => setTimeout(t, e));
110
128
  }
111
- async function k(e, t = {}) {
112
- const { maxRetries: n, retryDelay: a, timeout: i } = { ...z, ...t }, o = {
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 p = 0; p <= n; p++)
135
+ for (let o = 0; o <= n; o++)
118
136
  try {
119
- const c = new AbortController(), f = setTimeout(() => c.abort(), i), r = await fetch(e, {
120
- headers: o,
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(f), !r.ok) {
124
- const u = await r.text().catch(() => "Unable to read error response"), l = {
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: u
146
+ details: _
129
147
  };
130
148
  switch (r.status) {
131
149
  case 400:
132
- l.message = "Bad Request: Invalid search query or parameters";
150
+ m.message = "Bad Request: Invalid search query or parameters";
133
151
  break;
134
152
  case 401:
135
- l.message = "Unauthorized: Invalid or missing API key";
153
+ m.message = "Unauthorized: Invalid or missing API key";
136
154
  break;
137
155
  case 403:
138
- l.message = "Forbidden: API key may be invalid or quota exceeded";
156
+ m.message = "Forbidden: API key may be invalid or quota exceeded";
139
157
  break;
140
158
  case 404:
141
- l.message = "Not Found: No results found for the specified query";
159
+ m.message = "Not Found: No results found for the specified query";
142
160
  break;
143
161
  case 429:
144
- l.message = "Rate Limited: Too many requests. Retrying...";
162
+ m.message = "Rate Limited: Too many requests. Retrying...";
145
163
  break;
146
164
  case 500:
147
- l.message = "Server Error: OpenFDA service is experiencing issues";
165
+ m.message = "Server Error: OpenFDA service is experiencing issues";
148
166
  break;
149
167
  default:
150
- l.message = `HTTP Error ${r.status}: ${r.statusText}`;
168
+ m.message = `HTTP Error ${r.status}: ${r.statusText}`;
151
169
  }
152
- if (s = l, r.status >= 400 && r.status < 500 && r.status !== 429)
170
+ if (s = m, r.status >= 400 && r.status < 500 && r.status !== 429)
153
171
  break;
154
- if (p < n && T({ status: r.status })) {
155
- const h = a * Math.pow(2, p);
156
- await F(h);
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 g;
179
+ let h;
162
180
  try {
163
- g = await r.json();
164
- } catch (u) {
181
+ h = await r.json();
182
+ } catch (_) {
165
183
  s = {
166
184
  type: "parsing",
167
- message: C(
168
- `Failed to parse JSON response: ${u instanceof Error ? u.message : "Unknown parsing error"}`
185
+ message: I(
186
+ `Failed to parse JSON response: ${_ instanceof Error ? _.message : "Unknown parsing error"}`
169
187
  ),
170
- details: u
188
+ details: _
171
189
  };
172
190
  break;
173
191
  }
174
- if (!g) {
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: g, error: null };
199
+ return { data: h, error: null };
182
200
  } catch (c) {
183
- let f;
184
- if (c.name === "AbortError" ? f = {
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") ? f = {
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
- } : f = {
210
+ } : l = {
193
211
  type: "unknown",
194
- message: C(
212
+ message: I(
195
213
  `Unexpected error: ${c.message || "Unknown error occurred"}`
196
214
  ),
197
215
  details: c
198
- }, s = f, p < n && T(c)) {
199
- const r = a * Math.pow(2, p);
200
- await F(r);
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 X = [
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 O(e, t = 1) {
214
- let n;
215
- for (const a of X) {
216
- const i = new b().dataset("drug").context("label").search(`${a}:"${e}"`).limit(t).build(), { data: o, error: s } = await k(i);
217
- if (s) {
218
- n = s;
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: a, data: o };
242
+ return { found: !0, matched_via: i, data: o };
223
243
  }
224
- return { found: !1, error: n };
244
+ return { found: !1, error: a };
225
245
  }
226
- const U = (e) => `No label found for "${e}".
246
+ const K = (e) => `No label found for "${e}".
227
247
 
228
- Searched, in order: ${X.join(", ")}.
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.`, H = {
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: "Get drug information by brand name, generic name, or active substance. Returns the brand name, generic name, manufacturer name, product NDC, product type, route, substance name, indications and usage, warnings, do not use, ask doctor, ask doctor or pharmacist, stop use, pregnancy or breast feeding. The response reports which field matched via matched_via.",
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({ drugName: e }) {
241
- const t = await O(e, 1);
242
- if (!t.found)
243
- return t.error ? {
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}": ${t.error.message}`
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: U(e) }]
314
+ content: [{ type: "text", text: K(e) }]
253
315
  };
254
- const n = t.data.results[0], a = {
255
- brand_name: n == null ? void 0 : n.openfda.brand_name,
256
- generic_name: n == null ? void 0 : n.openfda.generic_name,
257
- manufacturer_name: n == null ? void 0 : n.openfda.manufacturer_name,
258
- product_ndc: n == null ? void 0 : n.openfda.product_ndc,
259
- product_type: n == null ? void 0 : n.openfda.product_type,
260
- route: n == null ? void 0 : n.openfda.route,
261
- substance_name: n == null ? void 0 : n.openfda.substance_name,
262
- matched_via: t.matched_via,
263
- ...V(n)
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: `Drug information retrieved successfully:
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(a, null, 2)}`
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, p, c, f;
300
- const n = new b().dataset("drug").context("label").search(`openfda.generic_name:"${e}"`).limit(t).build(), { data: a, error: i } = await k(n);
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 o = a.results.map((r) => {
321
- var g, u, l, h;
382
+ const u = a.results.map((r) => {
383
+ var h, _, m;
322
384
  return {
323
- brand_name: ((g = r == null ? void 0 : r.openfda.brand_name) == null ? void 0 : g[0]) || "Unknown",
324
- generic_name: ((u = r == null ? void 0 : r.openfda.generic_name) == null ? void 0 : u[0]) || "Unknown",
325
- manufacturer_name: ((l = r == null ? void 0 : r.openfda.manufacturer_name) == null ? void 0 : l[0]) || "Unknown",
326
- product_type: ((h = r == null ? void 0 : r.openfda.product_type) == null ? void 0 : h[0]) || "Unknown",
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: `${N(o.length, (p = (s = a.meta) == null ? void 0 : s.results) == null ? void 0 : p.total, `labels with generic name "${e}"`)}
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(o, (f = (c = a.meta) == null ? void 0 : c.results) == null ? void 0 : f.total, t ?? 5), null, 2)}`
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
- }, Z = {
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 ee(e) {
413
+ function pe(e) {
350
414
  if (e == null || e === "") return "Not reported";
351
415
  const t = String(e);
352
- return Z[t] ?? `Unrecognized outcome code "${t}"`;
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 te(e) {
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 o = `${i}\0${String(a == null ? void 0 : a.reactionoutcome)}`;
360
- t.has(o) || (t.add(o), n.push({ reaction: i, outcome: a == null ? void 0 : a.reactionoutcome }));
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 ne = {
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 c, f, r, g;
378
- let a = `patient.drug.medicinalproduct:"${e}"`;
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: `Failed to retrieve adverse events for "${e}": ${s.message}`
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
- if (!(o != null && o.results) || o.results.length === 0)
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 p = o.results.map((u) => {
401
- var h, _, $, v;
402
- const l = te(((h = u.patient) == null ? void 0 : h.reaction) ?? []);
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: u.safetyreportid,
405
- serious: u.serious === "1" ? "Yes" : "No",
406
- patient_age: ((_ = u.patient) == null ? void 0 : _.patientonsetage) || "Unknown",
407
- patient_sex: (($ = u.patient) == null ? void 0 : $.patientsex) === "1" ? "Male" : ((v = u.patient) == null ? void 0 : v.patientsex) === "2" ? "Female" : "Unknown",
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: l.map((w) => w.reaction),
411
- outcomes: l.map((w) => ee(w.outcome)),
412
- report_date: u.receiptdate || "Unknown"
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: `${N(p.length, (f = (c = o.meta) == null ? void 0 : c.results) == null ? void 0 : f.total, `adverse event reports for "${e}"`)}
555
+ text: `${_}
420
556
 
421
- ${JSON.stringify(S(p, (g = (r = o.meta) == null ? void 0 : r.results) == null ? void 0 : g.total, t ?? 10), null, 2)}`
557
+ ${JSON.stringify(m, null, 2)}`
422
558
  }
423
559
  ]
424
560
  };
425
561
  }
426
- }, ae = {
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, p, c, f;
438
- const n = new b().dataset("drug").context("label").search(`openfda.manufacturer_name:"${e}"`).limit(t).build(), { data: a, error: i } = await k(n);
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 o = a.results.map((r) => {
459
- var g, u, l, h;
604
+ const u = a.results.map((r) => {
605
+ var h, _, m;
460
606
  return {
461
- brand_name: ((g = r == null ? void 0 : r.openfda.brand_name) == null ? void 0 : g[0]) || "Unknown",
462
- generic_name: ((u = r == null ? void 0 : r.openfda.generic_name) == null ? void 0 : u[0]) || "Unknown",
463
- product_type: ((l = r == null ? void 0 : r.openfda.product_type) == null ? void 0 : l[0]) || "Unknown",
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: ((h = r == null ? void 0 : r.openfda.product_ndc) == null ? void 0 : h[0]) || "Unknown"
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: `${N(o.length, (p = (s = a.meta) == null ? void 0 : s.results) == null ? void 0 : p.total, `labels from manufacturer "${e}"`)}
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(o, (f = (c = a.meta) == null ? void 0 : c.results) == null ? void 0 : f.total, t ?? 20), null, 2)}`
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
- }, re = {
627
+ }, be = {
480
628
  name: "get-drug-safety-info",
481
- description: "Get comprehensive safety information for a drug including the boxed warning, warnings and cautions, contraindications, drug interactions, and precautions. Accepts a brand name, generic name, or active substance; the response reports which field matched via matched_via.",
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, o;
487
- const t = await O(e, 1);
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: U(e) }]
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: ((o = n == null ? void 0 : n.openfda.generic_name) == null ? void 0 : o[0]) || "Unknown",
668
+ generic_name: O(
669
+ (n == null ? void 0 : n.openfda) ?? {}
670
+ ),
503
671
  matched_via: t.matched_via,
504
- ...W(n)
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
- }, se = /^(?:\d{4}-\d{4}|\d{5}-\d{3,4})$/, oe = /^\d{1,2}$/;
518
- function I(e) {
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 o = t.split("-");
523
- if (o.length === 2)
690
+ const u = t.split("-");
691
+ if (u.length === 2)
524
692
  a = t;
525
- else if (o.length === 3) {
526
- const s = o[2];
527
- if (!s || !oe.test(s)) return n;
528
- a = `${o[0]}-${o[1]}`, i = t;
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 se.test(a) ? { productNDC: a, packageNDC: i, isValid: !0 } : n;
708
+ return ye.test(a) ? { productNDC: a, packageNDC: i, isValid: !0 } : n;
541
709
  }
542
- const ie = {
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 both product NDC (XXXXX-XXXX) and package NDC (XXXXX-XXXX-XX) formats. Also accepts NDC codes without dashes.",
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 g, u, l, h;
552
- const { productNDC: t, packageNDC: n, isValid: a } = I(e);
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: `Invalid NDC format: "${e}"
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 o = new b().dataset("drug").context("label").search(i).limit(10).build(), { data: s, error: p } = await k(o);
571
- if (p)
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}": ${p.message}`
762
+ text: `Failed to retrieve drug data for NDC "${e}": ${c.message}`
577
763
  }
578
764
  ],
579
765
  isError: !0
580
766
  };
581
- if (!(s != null && s.results) || s.results.length === 0)
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 c = s.results.map((_) => {
596
- var w, A;
597
- const $ = ((w = _.openfda.product_ndc) == null ? void 0 : w.filter((x) => x === t)) || [], v = ((A = _.openfda.package_ndc) == null ? void 0 : A.filter(
598
- (x) => n ? x === n : x.startsWith(t)
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: _.openfda.brand_name || [],
602
- generic_name: _.openfda.generic_name || [],
603
- manufacturer_name: _.openfda.manufacturer_name || [],
604
- product_type: _.openfda.product_type || [],
605
- route: _.openfda.route || [],
606
- substance_name: _.openfda.substance_name || [],
607
- matching_product_ndc: $,
608
- matching_package_ndc: v,
609
- all_product_ndc: _.openfda.product_ndc || [],
610
- all_package_ndc: _.openfda.package_ndc || [],
611
- dosage_and_administration: _.dosage_and_administration || [],
612
- package_label_principal_display_panel: _.package_label_principal_display_panel || [],
613
- active_ingredient: _.active_ingredient || [],
614
- purpose: _.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
- }), f = c.reduce(
617
- (_, $) => _ + $.matching_package_ndc.length,
802
+ }), r = l.reduce(
803
+ (p, k) => p + k.matching_package_ndc.length,
618
804
  0
619
- ), r = n ? `Searched for specific package NDC: ${n}` : `Searched for product NDC: ${t} (all packages)`;
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: `${N(c.length, (u = (g = s.meta) == null ? void 0 : g.results) == null ? void 0 : u.total, `labels for NDC "${e}"`)} with ${f} package(s)
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
- ${r}
812
+ ${h}
627
813
 
628
- ${JSON.stringify(S(c, (h = (l = s.meta) == null ? void 0 : l.results) == null ? void 0 : h.total, 10), null, 2)}`
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
- }, ce = {
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 f;
643
- const { productNDC: t, isValid: n } = I(e);
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: `Invalid product NDC format: "${e}"
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 b().dataset("drug").context("label").search(`openfda.product_ndc:"${t}"`).limit(1).build(), { data: i, error: o } = await k(a);
664
- if (o)
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}": ${o.message}`
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], p = ((f = s.openfda.package_ndc) == null ? void 0 : f.filter(
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: p,
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 ${p.length} package variation(s):
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
- }, ue = {
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: "Get drugsfda data by section and field. Search OpenFDA drugsfda endpoint by specifying a section (application, openfda, products, submissions, application_docs), a field name within that section, and a search value.",
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.string().describe(
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
- "Field name within the selected section. application: application_number. openfda: application_number, brand_name, generic_name, manufacturer_name, nui, package_ndc, pharm_class_cs, pharm_class_epc, pharm_class_pe, pharm_class_moa, product_ndc, route, rxcui, spl_id, spl_set_id, substance_name, unii. products: active_ingredients.name, active_ingredients.strength, dosage_form, marketing_status, product_number, reference_drug, reference_standard, route, te_code. submissions: application_docs, review_priority, submission_class_code, submission_class_code_description, submission_number, submission_property_type.code, submission_public_notes, submission_status, submission_status_date, submission_type. application_docs: applications_doc_id, applications_doc_date, application_docs_title, applications_doc_type, applications_doc_url"
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
- searchValue: d.string().describe("Value to search for in the specified field")
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
- const a = new b().dataset("drug").context("drugsfda").search(`${e}.${t}:"${n}"`).limit(1).build(), { data: i, error: o } = await k(a);
728
- if (o) {
729
- let s = `Failed to retrieve drugsfda data for "${n}" in ${e}.${t}: ${o.message}`;
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: s }],
1025
+ content: [{ type: "text", text: o.message }],
753
1026
  isError: !0
754
1027
  };
755
- }
756
- return !(i != null && i.results) || i.results.length === 0 ? {
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: `No drugsfda data found for "${n}" in ${e}.${t}. Please verify the search parameters.`
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: `drugsfda data retrieved successfully:
1124
+ text: `Top ${s.results.length} values of ${a} for "${e}" (aggregated responses carry no result total)
768
1125
 
769
- ${JSON.stringify(i.results[0], null, 2)}`
1126
+ ${JSON.stringify(c, null, 2)}`
770
1127
  }
771
1128
  ]
772
1129
  };
773
1130
  }
774
- }, de = "1.1.0", R = new K(
1131
+ }, Te = "1.3.0", H = new ee(
775
1132
  {
776
1133
  name: "openfda",
777
- version: de,
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
- ), y = new J(R);
787
- y.registerTool(H);
788
- y.registerTool(Q);
789
- y.registerTool(ne);
790
- y.registerTool(ae);
791
- y.registerTool(re);
792
- y.registerTool(ie);
793
- y.registerTool(ce);
794
- y.registerTool(ue);
795
- async function pe() {
796
- B();
797
- const e = new L();
798
- await R.connect(e), console.error("OpenFDA MCP Server running on stdio");
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
- pe().catch((e) => {
1158
+ Ce().catch((e) => {
801
1159
  console.error("Fatal error in main():", e), process.exit(1);
802
1160
  });