@ythalorossy/openfda 1.1.0 → 1.2.0

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