@ythalorossy/openfda 1.0.19 → 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 (4) hide show
  1. package/LICENSE +20 -20
  2. package/README.md +103 -94
  3. package/dist/index.js +740 -362
  4. package/package.json +6 -4
package/dist/index.js CHANGED
@@ -1,28 +1,79 @@
1
1
  #!/usr/bin/env node
2
- var v = Object.defineProperty;
3
- var D = (n, t, r) => t in n ? v(n, t, { enumerable: !0, configurable: !0, writable: !0, value: r }) : n[t] = r;
4
- var k = (n, t, r) => D(n, typeof t != "symbol" ? t + "" : t, r);
5
- import { McpServer as N } from "@modelcontextprotocol/sdk/server/mcp.js";
6
- import { StdioServerTransport as C } from "@modelcontextprotocol/sdk/server/stdio.js";
7
- import d from "zod";
8
- class T {
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
+
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
+ var n;
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: Q };
15
+ }
16
+ function te(e = process.env) {
17
+ const t = O(e);
18
+ t.ok ? t.apiKey === null && console.error(Z) : console.error(ee);
19
+ }
20
+ class ne {
9
21
  constructor(t) {
10
- k(this, "registerTool", (t) => this.server.registerTool(
22
+ A(this, "registerTool", (t) => this.server.registerTool(
11
23
  t.name,
12
24
  {
13
25
  title: t.name,
14
26
  description: t.description,
15
27
  inputSchema: t.inputSchema
16
28
  },
17
- t.handler
29
+ async (n) => {
30
+ const r = O();
31
+ return r.ok ? t.handler(n) : {
32
+ content: [{ type: "text", text: r.message }],
33
+ isError: !0
34
+ };
35
+ }
18
36
  ));
19
37
  this.server = t;
20
38
  }
21
39
  }
22
- class g {
40
+ const m = (e) => Array.isArray(e) ? e : [], M = (e) => {
41
+ const t = m(e.warnings);
42
+ return t.length > 0 ? t : m(e.warnings_and_cautions);
43
+ };
44
+ function re(e) {
45
+ return {
46
+ boxed_warning: m(e.boxed_warning),
47
+ warnings: M(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)
58
+ };
59
+ }
60
+ function ae(e) {
61
+ return {
62
+ indications_and_usage: m(e.indications_and_usage),
63
+ boxed_warning: m(e.boxed_warning),
64
+ warnings: M(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)
71
+ };
72
+ }
73
+ class y {
23
74
  constructor() {
24
- k(this, "urlBase", "https://api.fda.gov");
25
- k(this, "params", /* @__PURE__ */ new Map());
75
+ A(this, "urlBase", "https://api.fda.gov");
76
+ A(this, "params", /* @__PURE__ */ new Map());
26
77
  }
27
78
  dataset(t) {
28
79
  return this.params.set("dataset", t), this;
@@ -36,229 +87,287 @@ class g {
36
87
  limit(t = 1) {
37
88
  return this.params.set("limit", t), this;
38
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
+ }
39
102
  build() {
40
- const t = this.params.get("dataset"), r = this.params.get("context"), s = this.params.get("search"), e = this.params.get("limit") ?? 1, o = process.env.OPENFDA_API_KEY;
41
- if (!t || !r || !s)
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)
42
105
  throw new Error("Missing required parameters: context or search");
43
- return `${this.urlBase}/${t}/${r}.json?api_key=${o}&search=${s}&limit=${e}`;
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}`;
44
113
  }
45
114
  }
46
- const F = {
115
+ const U = (e) => e.replace(/([?&]api_key=)[^&\s]*/gi, "$1<REDACTED>"), se = {
47
116
  maxRetries: 3,
48
117
  retryDelay: 1e3,
49
118
  // 1 second
50
119
  timeout: 3e4
51
120
  // 30 seconds
52
121
  };
53
- function x(n) {
54
- return !!(n.name === "TypeError" && n.message.includes("fetch") || n.name === "AbortError" || n.status >= 500 && n.status <= 599 || n.status === 429);
122
+ function I(e) {
123
+ return !!(e.name === "TypeError" && e.message.includes("fetch") || e.name === "AbortError" || e.status >= 500 && e.status <= 599 || e.status === 429);
55
124
  }
56
- function w(n) {
57
- return new Promise((t) => setTimeout(t, n));
125
+ function P(e) {
126
+ return new Promise((t) => setTimeout(t, e));
58
127
  }
59
- async function h(n, t = {}) {
60
- const { maxRetries: r, retryDelay: s, timeout: e } = { ...F, ...t }, o = {
128
+ async function v(e, t = {}) {
129
+ const { maxRetries: n, retryDelay: r, timeout: i } = { ...se, ...t }, o = {
61
130
  "User-Agent": "@ythalorossy/openfda",
62
131
  Accept: "application/json"
63
132
  };
64
- let a = null;
65
- for (let p = 0; p <= r; p++)
133
+ let s = null;
134
+ for (let d = 0; d <= n; d++)
66
135
  try {
67
- const i = new AbortController(), l = setTimeout(() => i.abort(), e), c = await fetch(n, {
136
+ const c = new AbortController(), l = setTimeout(() => c.abort(), i), a = await fetch(e, {
68
137
  headers: o,
69
- signal: i.signal
138
+ signal: c.signal
70
139
  });
71
- if (clearTimeout(l), !c.ok) {
72
- const f = await c.text().catch(() => "Unable to read error response"), m = {
140
+ if (clearTimeout(l), !a.ok) {
141
+ const f = await a.text().catch(() => "Unable to read error response"), _ = {
73
142
  type: "http",
74
- message: `HTTP ${c.status}: ${c.statusText}`,
75
- status: c.status,
143
+ message: `HTTP ${a.status}: ${a.statusText}`,
144
+ status: a.status,
76
145
  details: f
77
146
  };
78
- switch (c.status) {
147
+ switch (a.status) {
79
148
  case 400:
80
- m.message = "Bad Request: Invalid search query or parameters";
149
+ _.message = "Bad Request: Invalid search query or parameters";
81
150
  break;
82
151
  case 401:
83
- m.message = "Unauthorized: Invalid or missing API key";
152
+ _.message = "Unauthorized: Invalid or missing API key";
84
153
  break;
85
154
  case 403:
86
- m.message = "Forbidden: API key may be invalid or quota exceeded";
155
+ _.message = "Forbidden: API key may be invalid or quota exceeded";
87
156
  break;
88
157
  case 404:
89
- m.message = "Not Found: No results found for the specified query";
158
+ _.message = "Not Found: No results found for the specified query";
90
159
  break;
91
160
  case 429:
92
- m.message = "Rate Limited: Too many requests. Retrying...";
161
+ _.message = "Rate Limited: Too many requests. Retrying...";
93
162
  break;
94
163
  case 500:
95
- m.message = "Server Error: OpenFDA service is experiencing issues";
164
+ _.message = "Server Error: OpenFDA service is experiencing issues";
96
165
  break;
97
166
  default:
98
- m.message = `HTTP Error ${c.status}: ${c.statusText}`;
167
+ _.message = `HTTP Error ${a.status}: ${a.statusText}`;
99
168
  }
100
- if (a = m, c.status >= 400 && c.status < 500 && c.status !== 429)
169
+ if (s = _, a.status >= 400 && a.status < 500 && a.status !== 429)
101
170
  break;
102
- if (p < r && x({ status: c.status })) {
103
- const b = s * Math.pow(2, p);
104
- await w(b);
171
+ if (d < n && I({ status: a.status })) {
172
+ const h = r * Math.pow(2, d);
173
+ await P(h);
105
174
  continue;
106
175
  }
107
176
  break;
108
177
  }
109
- let u;
178
+ let g;
110
179
  try {
111
- u = await c.json();
180
+ g = await a.json();
112
181
  } catch (f) {
113
- a = {
182
+ s = {
114
183
  type: "parsing",
115
- message: `Failed to parse JSON response: ${f instanceof Error ? f.message : "Unknown parsing error"}`,
184
+ message: U(
185
+ `Failed to parse JSON response: ${f instanceof Error ? f.message : "Unknown parsing error"}`
186
+ ),
116
187
  details: f
117
188
  };
118
189
  break;
119
190
  }
120
- if (!u) {
121
- a = {
191
+ if (!g) {
192
+ s = {
122
193
  type: "empty_response",
123
194
  message: "Received empty response from OpenFDA API"
124
195
  };
125
196
  break;
126
197
  }
127
- return { data: u, error: null };
128
- } catch (i) {
198
+ return { data: g, error: null };
199
+ } catch (c) {
129
200
  let l;
130
- if (i.name === "AbortError" ? l = {
201
+ if (c.name === "AbortError" ? l = {
131
202
  type: "timeout",
132
- message: `Request timeout after ${e}ms`,
133
- details: i
134
- } : i instanceof TypeError && i.message.includes("fetch") ? l = {
203
+ message: `Request timeout after ${i}ms`,
204
+ details: c
205
+ } : c instanceof TypeError && c.message.includes("fetch") ? l = {
135
206
  type: "network",
136
207
  message: "Network error: Unable to connect to OpenFDA API",
137
- details: i.message
208
+ details: c.message
138
209
  } : l = {
139
210
  type: "unknown",
140
- message: `Unexpected error: ${i.message || "Unknown error occurred"}`,
141
- details: i
142
- }, a = l, p < r && x(i)) {
143
- const c = s * Math.pow(2, p);
144
- await w(c);
211
+ message: U(
212
+ `Unexpected error: ${c.message || "Unknown error occurred"}`
213
+ ),
214
+ details: c
215
+ }, s = l, d < n && I(c)) {
216
+ const a = r * Math.pow(2, d);
217
+ await P(a);
145
218
  continue;
146
219
  }
147
220
  break;
148
221
  }
149
- return { data: null, error: a };
222
+ return { data: null, error: s };
150
223
  }
151
- const S = {
152
- name: "get-drug-by-name",
153
- description: "Get drug by name. Use this tool to get the drug information by name. The drug name should be the brand name. It returns the brand name, generic name, manufacturer name, product NDC, product type, route, substance name, indications and usage, warnings, do not use, ask doctor, ask doctor or pharmacist, stop use, pregnancy or breast feeding.",
154
- inputSchema: d.object({
155
- drugName: d.string().describe("Drug name")
156
- }),
157
- async handler({ drugName: n }) {
158
- const t = new g().dataset("drug").context("label").search(`openfda.brand_name:"${n}"`).limit(1).build(), { data: r, error: s } = await h(t);
224
+ const j = [
225
+ "openfda.brand_name",
226
+ "openfda.generic_name",
227
+ "openfda.substance_name",
228
+ "spl_product_data_elements"
229
+ ];
230
+ async function X(e, t = 1) {
231
+ let n;
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);
159
234
  if (s) {
160
- let a = `Failed to retrieve drug data for "${n}": ${s.message}`;
161
- switch (s.type) {
162
- case "http":
163
- s.status === 404 ? a += `${t}
164
-
165
- Suggestions:
166
- - Verify the exact brand name spelling
167
- - Try searching for the generic name instead
168
- - Check if the drug is FDA-approved` : (s.status === 401 || s.status === 403) && (a += `
235
+ n = s;
236
+ continue;
237
+ }
238
+ if (o != null && o.results && o.results.length > 0)
239
+ return { found: !0, matched_via: r, data: o };
240
+ }
241
+ return { found: !1, error: n };
242
+ }
243
+ const L = (e) => `No label found for "${e}".
169
244
 
170
- Please check the API key configuration.`);
171
- break;
172
- case "network":
173
- a += `
245
+ Searched, in order: ${j.join(", ")}.
174
246
 
175
- Please check your internet connection and try again.`;
176
- break;
177
- case "timeout":
178
- a += `
179
-
180
- The request took too long. Please try again.`;
181
- break;
182
- }
183
- return {
184
- content: [{ type: "text", text: a }],
185
- isError: !0
186
- };
187
- }
188
- if (!(r != null && r.results) || r.results.length === 0)
189
- return {
247
+ Suggestions:
248
+ - Check the spelling.
249
+ - Try the generic name instead of the brand (e.g. "amiodarone" rather than "Cordarone").
250
+ - Try the originator brand rather than a repackager's name.
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 = {
268
+ name: "get-drug-by-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")
289
+ }),
290
+ async handler({ drugName: e }) {
291
+ var i, o, s, d;
292
+ const t = await X(e, 1);
293
+ if (!t.found)
294
+ return t.error ? {
190
295
  content: [
191
296
  {
192
297
  type: "text",
193
- text: `No drug information found for "${n}". Please verify the brand name spelling or try searching for the generic name.`
298
+ text: `Failed to retrieve drug data for "${e}": ${t.error.message}`
194
299
  }
195
- ]
300
+ ],
301
+ isError: !0
302
+ } : {
303
+ content: [{ type: "text", text: L(e) }]
196
304
  };
197
- const e = r.results[0], o = {
198
- brand_name: e == null ? void 0 : e.openfda.brand_name,
199
- generic_name: e == null ? void 0 : e.openfda.generic_name,
200
- manufacturer_name: e == null ? void 0 : e.openfda.manufacturer_name,
201
- product_ndc: e == null ? void 0 : e.openfda.product_ndc,
202
- product_type: e == null ? void 0 : e.openfda.product_type,
203
- route: e == null ? void 0 : e.openfda.route,
204
- substance_name: e == null ? void 0 : e.openfda.substance_name,
205
- indications_and_usage: e == null ? void 0 : e.indications_and_usage,
206
- warnings: e == null ? void 0 : e.warnings,
207
- do_not_use: e == null ? void 0 : e.do_not_use,
208
- ask_doctor: e == null ? void 0 : e.ask_doctor,
209
- ask_doctor_or_pharmacist: e == null ? void 0 : e.ask_doctor_or_pharmacist,
210
- stop_use: e == null ? void 0 : e.stop_use,
211
- pregnancy_or_breast_feeding: e == null ? void 0 : e.pregnancy_or_breast_feeding
305
+ const n = t.data.results[0], r = {
306
+ brand_name: n == null ? void 0 : n.openfda.brand_name,
307
+ generic_name: n == null ? void 0 : n.openfda.generic_name,
308
+ manufacturer_name: n == null ? void 0 : n.openfda.manufacturer_name,
309
+ product_ndc: n == null ? void 0 : n.openfda.product_ndc,
310
+ product_type: n == null ? void 0 : n.openfda.product_type,
311
+ route: n == null ? void 0 : n.openfda.route,
312
+ substance_name: n == null ? void 0 : n.openfda.substance_name,
313
+ matched_via: t.matched_via,
314
+ ...ae(n)
212
315
  };
213
316
  return {
214
317
  content: [
215
318
  {
216
319
  type: "text",
217
- 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}"`)}
218
321
 
219
- ${JSON.stringify(o, 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)}`
220
323
  }
221
324
  ]
222
325
  };
223
326
  }
224
- }, E = {
327
+ }, ce = {
225
328
  name: "get-drug-by-generic-name",
226
- 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.",
227
- inputSchema: d.object({
228
- genericName: d.string().describe("Generic drug name (active ingredient)"),
229
- 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")
230
338
  }),
231
339
  async handler({
232
- genericName: n,
340
+ genericName: e,
233
341
  limit: t
234
342
  }) {
235
- const r = new g().dataset("drug").context("label").search(`openfda.generic_name:"${n}"`).limit(t).build(), { data: s, error: e } = await h(r);
236
- if (e)
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);
345
+ if (i)
237
346
  return {
238
347
  content: [
239
348
  {
240
349
  type: "text",
241
- text: `Failed to retrieve drug data for generic name "${n}": ${e.message}`
350
+ text: `Failed to retrieve drug data for generic name "${e}": ${i.message}`
242
351
  }
243
352
  ],
244
353
  isError: !0
245
354
  };
246
- if (!(s != null && s.results) || s.results.length === 0)
355
+ if (!(r != null && r.results) || r.results.length === 0)
247
356
  return {
248
357
  content: [
249
358
  {
250
359
  type: "text",
251
- text: `No drug information found for generic name "${n}".`
360
+ text: `No drug information found for generic name "${e}".`
252
361
  }
253
362
  ]
254
363
  };
255
- const o = s.results.map((a) => {
256
- var p, i, l, c;
364
+ const o = r.results.map((a) => {
365
+ var g, f, _, h;
257
366
  return {
258
- brand_name: ((p = a == null ? void 0 : a.openfda.brand_name) == null ? void 0 : p[0]) || "Unknown",
259
- generic_name: ((i = a == null ? void 0 : a.openfda.generic_name) == null ? void 0 : i[0]) || "Unknown",
260
- manufacturer_name: ((l = a == null ? void 0 : a.openfda.manufacturer_name) == null ? void 0 : l[0]) || "Unknown",
261
- product_type: ((c = a == null ? void 0 : a.openfda.product_type) == null ? void 0 : c[0]) || "Unknown",
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",
262
371
  route: (a == null ? void 0 : a.openfda.route) || []
263
372
  };
264
373
  });
@@ -266,242 +375,358 @@ ${JSON.stringify(o, null, 2)}`
266
375
  content: [
267
376
  {
268
377
  type: "text",
269
- text: `Found ${o.length} drug(s) with generic name "${n}":
378
+ text: `${x(o.length, (d = (s = r.meta) == null ? void 0 : s.results) == null ? void 0 : d.total, `labels with generic name "${e}"`)}
270
379
 
271
- ${JSON.stringify(o, 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)}`
272
381
  }
273
382
  ]
274
383
  };
275
384
  }
276
- }, P = {
385
+ }, de = {
386
+ 1: "Recovered/resolved",
387
+ 2: "Recovering/resolving",
388
+ 3: "Not recovered/not resolved",
389
+ 4: "Recovered/resolved with sequelae",
390
+ 5: "Fatal",
391
+ 6: "Unknown"
392
+ };
393
+ function ue(e) {
394
+ if (e == null || e === "") return "Not reported";
395
+ const t = String(e);
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
+ );
407
+ }
408
+ function le(e) {
409
+ const t = /* @__PURE__ */ new Set(), n = [];
410
+ for (const r of e) {
411
+ const i = r == null ? void 0 : r.reactionmeddrapt;
412
+ if (!i) continue;
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 }));
415
+ }
416
+ return n.slice(0, 3);
417
+ }
418
+ const F = 25e3, pe = {
277
419
  name: "get-drug-adverse-events",
278
- description: "Get adverse event reports for a drug. This provides safety information about reported side effects and reactions. Use brand name or generic name.",
279
- inputSchema: d.object({
280
- drugName: d.string().describe("Drug name (brand or generic)"),
281
- limit: d.number().optional().default(10).describe("Maximum number of events to return"),
282
- 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
+ )
283
445
  }),
284
446
  async handler({
285
- drugName: n,
447
+ drugName: e,
286
448
  limit: t,
287
- seriousness: r
449
+ seriousness: n,
450
+ skip: r,
451
+ sort: i
288
452
  }) {
289
- let s = `patient.drug.medicinalproduct:"${n}"`;
290
- r !== "all" && (s += `+AND+serious:${r === "serious" ? "1" : "2"}`);
291
- const e = new g().dataset("drug").context("event").search(s).limit(t).build(), { data: o, error: a } = await h(e);
292
- if (a)
453
+ var h, E;
454
+ if (r !== void 0 && r > F)
293
455
  return {
294
456
  content: [
295
457
  {
296
458
  type: "text",
297
- text: `Failed to retrieve adverse events for "${n}": ${a.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.`
298
460
  }
299
461
  ],
300
462
  isError: !0
301
463
  };
302
- 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)
303
470
  return {
304
471
  content: [
305
472
  {
306
473
  type: "text",
307
- text: `No adverse events found for "${n}".`
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)
480
+ return {
481
+ content: [
482
+ {
483
+ type: "text",
484
+ text: `No adverse events found for "${e}".`
308
485
  }
309
486
  ]
310
487
  };
311
- const p = o.results.map((i) => {
312
- var l, c, u, f, m, b, $;
488
+ const a = c.results.map((p) => {
489
+ var N, D, S, $;
490
+ const w = le(((N = p.patient) == null ? void 0 : N.reaction) ?? []);
313
491
  return {
314
- report_id: i.safetyreportid,
315
- serious: i.serious === "1" ? "Yes" : "No",
316
- patient_age: ((l = i.patient) == null ? void 0 : l.patientonsetage) || "Unknown",
317
- patient_sex: ((c = i.patient) == null ? void 0 : c.patientsex) === "1" ? "Male" : ((u = i.patient) == null ? void 0 : u.patientsex) === "2" ? "Female" : "Unknown",
318
- reactions: ((m = (f = i.patient) == null ? void 0 : f.reaction) == null ? void 0 : m.map((_) => _.reactionmeddrapt).slice(0, 3)) || [],
319
- outcomes: (($ = (b = i.patient) == null ? void 0 : b.reaction) == null ? void 0 : $.map((_) => _.reactionoutcome).slice(0, 3)) || [],
320
- report_date: i.receiptdate || "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",
496
+ // Derived from the same deduped pairs so the two arrays stay the
497
+ // same length and positionally aligned (see dedupeReactionPairs).
498
+ reactions: w.map((T) => T.reaction),
499
+ outcomes: w.map((T) => ue(T.outcome)),
500
+ report_date: p.receiptdate || "Unknown"
321
501
  };
322
- });
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
+ };
323
511
  return {
324
512
  content: [
325
513
  {
326
514
  type: "text",
327
- text: `Found ${p.length} adverse event report(s) for "${n}":
515
+ text: `${f}
328
516
 
329
- ${JSON.stringify(p, null, 2)}`
517
+ ${JSON.stringify(_, null, 2)}`
330
518
  }
331
519
  ]
332
520
  };
333
521
  }
334
- }, A = {
522
+ }, me = {
335
523
  name: "get-drugs-by-manufacturer",
336
- description: "Get all drugs manufactured by a specific company. Useful for finding alternatives or checking manufacturer portfolios.",
337
- inputSchema: d.object({
338
- manufacturerName: d.string().describe("Manufacturer/company name"),
339
- 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")
340
538
  }),
341
539
  async handler({
342
- manufacturerName: n,
540
+ manufacturerName: e,
343
541
  limit: t
344
542
  }) {
345
- const r = new g().dataset("drug").context("label").search(`openfda.manufacturer_name:"${n}"`).limit(t).build(), { data: s, error: e } = await h(r);
346
- if (e)
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);
545
+ if (i)
347
546
  return {
348
547
  content: [
349
548
  {
350
549
  type: "text",
351
- text: `${r}
352
- Failed to retrieve drugs for manufacturer "${n}": ${e.message}`
550
+ text: `Failed to retrieve drugs for manufacturer "${e}": ${i.message}`
353
551
  }
354
552
  ],
355
553
  isError: !0
356
554
  };
357
- if (!(s != null && s.results) || s.results.length === 0)
555
+ if (!(r != null && r.results) || r.results.length === 0)
358
556
  return {
359
557
  content: [
360
558
  {
361
559
  type: "text",
362
- text: `No drugs found for manufacturer "${n}".`
560
+ text: `No drugs found for manufacturer "${e}".`
363
561
  }
364
562
  ]
365
563
  };
366
- const o = s.results.map((a) => {
367
- var p, i, l, c;
564
+ const o = r.results.map((a) => {
565
+ var g, f, _, h;
368
566
  return {
369
- brand_name: ((p = a == null ? void 0 : a.openfda.brand_name) == null ? void 0 : p[0]) || "Unknown",
370
- generic_name: ((i = a == null ? void 0 : a.openfda.generic_name) == null ? void 0 : i[0]) || "Unknown",
371
- product_type: ((l = a == null ? void 0 : a.openfda.product_type) == null ? void 0 : l[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",
372
570
  route: (a == null ? void 0 : a.openfda.route) || [],
373
- ndc: ((c = a == null ? void 0 : a.openfda.product_ndc) == null ? void 0 : c[0]) || "Unknown"
571
+ ndc: ((h = a == null ? void 0 : a.openfda.product_ndc) == null ? void 0 : h[0]) || "Unknown"
374
572
  };
375
573
  });
376
574
  return {
377
575
  content: [
378
576
  {
379
577
  type: "text",
380
- text: `Found ${o.length} drug(s) from manufacturer "${n}":
578
+ text: `${x(o.length, (d = (s = r.meta) == null ? void 0 : s.results) == null ? void 0 : d.total, `labels from manufacturer "${e}"`)}
381
579
 
382
- ${JSON.stringify(o, 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)}`
383
581
  }
384
582
  ]
385
583
  };
386
584
  }
387
- }, U = {
585
+ }, fe = {
388
586
  name: "get-drug-safety-info",
389
- description: "Get comprehensive safety information for a drug including warnings, contraindications, drug interactions, and precautions. Use brand name.",
390
- inputSchema: d.object({
391
- 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")
392
607
  }),
393
- async handler({ drugName: n }) {
394
- var a, p;
395
- const t = new g().dataset("drug").context("label").search(`openfda.brand_name:"${n}"`).limit(1).build(), { data: r, error: s } = await h(t);
396
- if (s)
397
- return {
608
+ async handler({ drugName: e }) {
609
+ var i;
610
+ const t = await X(e, 1);
611
+ if (!t.found)
612
+ return t.error ? {
398
613
  content: [
399
614
  {
400
615
  type: "text",
401
- text: `Failed to retrieve safety information for "${n}": ${s.message}`
616
+ text: `Failed to retrieve safety information for "${e}": ${t.error.message}`
402
617
  }
403
618
  ],
404
619
  isError: !0
620
+ } : {
621
+ content: [{ type: "text", text: L(e) }]
405
622
  };
406
- if (!(r != null && r.results) || r.results.length === 0)
407
- return {
408
- content: [
409
- {
410
- type: "text",
411
- text: `No safety information found for "${n}".`
412
- }
413
- ]
414
- };
415
- const e = r.results[0], o = {
416
- drug_name: ((a = e == null ? void 0 : e.openfda.brand_name) == null ? void 0 : a[0]) || n,
417
- generic_name: ((p = e == null ? void 0 : e.openfda.generic_name) == null ? void 0 : p[0]) || "Unknown",
418
- warnings: (e == null ? void 0 : e.warnings) || [],
419
- contraindications: (e == null ? void 0 : e.contraindications) || [],
420
- drug_interactions: (e == null ? void 0 : e.drug_interactions) || [],
421
- precautions: (e == null ? void 0 : e.precautions) || [],
422
- adverse_reactions: (e == null ? void 0 : e.adverse_reactions) || [],
423
- overdosage: (e == null ? void 0 : e.overdosage) || [],
424
- do_not_use: (e == null ? void 0 : e.do_not_use) || [],
425
- ask_doctor: (e == null ? void 0 : e.ask_doctor) || [],
426
- stop_use: (e == null ? void 0 : e.stop_use) || [],
427
- pregnancy_or_breast_feeding: (e == null ? void 0 : e.pregnancy_or_breast_feeding) || []
623
+ const n = t.data.results[0], r = {
624
+ drug_name: ((i = n == null ? void 0 : n.openfda.brand_name) == null ? void 0 : i[0]) || e,
625
+ generic_name: oe(
626
+ (n == null ? void 0 : n.openfda) ?? {}
627
+ ),
628
+ matched_via: t.matched_via,
629
+ ...re(n)
428
630
  };
429
631
  return {
430
632
  content: [
431
633
  {
432
634
  type: "text",
433
- text: `Safety information for "${n}":
635
+ text: `Safety information for "${e}":
434
636
 
435
- ${JSON.stringify(o, null, 2)}`
637
+ ${JSON.stringify(r, null, 2)}`
436
638
  }
437
639
  ]
438
640
  };
439
641
  }
440
- };
441
- function O(n) {
442
- const t = n.trim().toUpperCase();
443
- let r, s = null;
642
+ }, _e = /^(?:\d{4}-\d{4}|\d{5}-\d{3,4})$/, ge = /^\d{1,2}$/;
643
+ function V(e) {
644
+ const t = e.trim().toUpperCase(), n = { productNDC: t, packageNDC: null, isValid: !1 };
645
+ let r, i = null;
444
646
  if (t.includes("-")) {
445
647
  const o = t.split("-");
446
648
  if (o.length === 2)
447
- r = t, s = null;
448
- else if (o.length === 3)
449
- r = `${o[0]}-${o[1]}`, s = t;
649
+ r = t;
650
+ else if (o.length === 3) {
651
+ const s = o[2];
652
+ if (!s || !ge.test(s)) return n;
653
+ r = `${o[0]}-${o[1]}`, i = t;
654
+ } else
655
+ return n;
656
+ } else if (/^\d+$/.test(t))
657
+ if (t.length === 11)
658
+ r = `${t.slice(0, 5)}-${t.slice(5, 9)}`, i = `${r}-${t.slice(9, 11)}`;
659
+ else if (t.length === 9)
660
+ r = `${t.slice(0, 5)}-${t.slice(5, 9)}`;
450
661
  else
451
- return { productNDC: t, packageNDC: null, isValid: !1 };
452
- } else if (t.length === 11)
453
- r = `${t.substring(0, 5)}-${t.substring(5, 9)}`, s = `${t.substring(0, 5)}-${t.substring(5, 9)}-${t.substring(9, 11)}`;
454
- else if (t.length === 9)
455
- r = `${t.substring(0, 5)}-${t.substring(5, 9)}`, s = null;
662
+ return n;
456
663
  else
457
- return { productNDC: t, packageNDC: null, isValid: !1 };
458
- const e = /^\d{5}-\d{4}$/.test(r);
459
- return { productNDC: r, packageNDC: s, isValid: e };
664
+ return n;
665
+ return _e.test(r) ? { productNDC: r, packageNDC: i, isValid: !0 } : n;
460
666
  }
461
- const I = {
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 = {
462
681
  name: "get-drug-by-ndc",
463
- 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.",
464
- inputSchema: d.object({
465
- 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(
466
695
  "National Drug Code (NDC) - accepts formats: XXXXX-XXXX, XXXXX-XXXX-XX, or without dashes"
467
696
  )
468
697
  }),
469
- async handler({ ndcCode: n }) {
470
- const { productNDC: t, packageNDC: r, isValid: s } = O(n);
471
- if (!s)
698
+ async handler({ ndcCode: e }) {
699
+ var f, _, h, E;
700
+ const { productNDC: t, packageNDC: n, isValid: r } = V(e);
701
+ if (!r)
472
702
  return {
473
703
  content: [
474
704
  {
475
705
  type: "text",
476
- text: `Invalid NDC format: "${n}"
477
-
478
- ✅ Accepted formats:
479
- • Product NDC: 12345-1234
480
- • Package NDC: 12345-1234-01
481
- • Without dashes: 123451234 or 12345123401`
706
+ text: W(e, "NDC")
482
707
  }
483
708
  ],
484
709
  isError: !0
485
710
  };
486
- let e = `openfda.product_ndc:"${t}"`;
487
- r && (e += `+OR+openfda.package_ndc:"${r}"`);
488
- const o = new g().dataset("drug").context("label").search(e).limit(10).build(), { data: a, error: p } = await h(o);
489
- if (p)
711
+ let i = `openfda.product_ndc:"${t}"`;
712
+ n && (i += ` OR openfda.package_ndc:"${n}"`);
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)
490
715
  return {
491
716
  content: [
492
717
  {
493
718
  type: "text",
494
- text: `Failed to retrieve drug data for NDC "${n}": ${p.message}`
719
+ text: `Failed to retrieve drug data for NDC "${e}": ${c.message}`
495
720
  }
496
721
  ],
497
722
  isError: !0
498
723
  };
499
- if (!(a != null && a.results) || a.results.length === 0)
724
+ if (!(d != null && d.results) || d.results.length === 0)
500
725
  return {
501
726
  content: [
502
727
  {
503
728
  type: "text",
504
- text: `No drug found with NDC "${n}" (product: ${t}).
729
+ text: `No drug found with NDC "${e}" (product: ${t}).
505
730
 
506
731
  💡 Tips:
507
732
  • Verify the NDC format
@@ -510,179 +735,330 @@ const I = {
510
735
  }
511
736
  ]
512
737
  };
513
- const i = a.results.map((u) => {
514
- var b, $;
515
- const f = ((b = u.openfda.product_ndc) == null ? void 0 : b.filter((_) => _ === t)) || [], m = (($ = u.openfda.package_ndc) == null ? void 0 : $.filter(
516
- (_) => r ? _ === r : _.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)
517
742
  )) || [];
518
743
  return {
519
- brand_name: u.openfda.brand_name || [],
520
- generic_name: u.openfda.generic_name || [],
521
- manufacturer_name: u.openfda.manufacturer_name || [],
522
- product_type: u.openfda.product_type || [],
523
- route: u.openfda.route || [],
524
- substance_name: u.openfda.substance_name || [],
525
- matching_product_ndc: f,
526
- matching_package_ndc: m,
527
- all_product_ndc: u.openfda.product_ndc || [],
528
- all_package_ndc: u.openfda.package_ndc || [],
529
- dosage_and_administration: u.dosage_and_administration || [],
530
- package_label_principal_display_panel: u.package_label_principal_display_panel || [],
531
- active_ingredient: u.active_ingredient || [],
532
- purpose: u.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 || []
533
758
  };
534
- }), l = i.reduce(
535
- (u, f) => u + f.matching_package_ndc.length,
759
+ }), a = l.reduce(
760
+ (p, w) => p + w.matching_package_ndc.length,
536
761
  0
537
- ), c = r ? `Searched for specific package NDC: ${r}` : `Searched for product NDC: ${t} (all packages)`;
762
+ ), g = n ? `Searched for specific package NDC: ${n}` : `Searched for product NDC: ${t} (all packages)`;
538
763
  return {
539
764
  content: [
540
765
  {
541
766
  type: "text",
542
- text: `✅ Found ${i.length} drug(s) with ${l} package(s) for NDC "${n}"
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)
543
768
 
544
- ${c}
769
+ ${g}
545
770
 
546
- ${JSON.stringify(i, 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)}`
547
772
  }
548
773
  ]
549
774
  };
550
775
  }
551
- }, M = {
776
+ }, ye = {
552
777
  name: "get-drug-by-product-ndc",
553
- description: "Get drug information by product NDC only (XXXXX-XXXX format). This ignores package variations and finds all packages for a product.",
554
- inputSchema: d.object({
555
- productNDC: d.string().describe("Product NDC in format XXXXX-XXXX")
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(
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"
798
+ )
556
799
  }),
557
- async handler({ productNDC: n }) {
558
- var p;
559
- if (!/^\d{5}-\d{4}$/.test(n.trim()))
800
+ async handler({ productNDC: e }) {
801
+ var l;
802
+ const { productNDC: t, isValid: n } = V(e);
803
+ if (!n)
560
804
  return {
561
805
  content: [
562
806
  {
563
807
  type: "text",
564
- text: `Invalid product NDC format: "${n}"
565
-
566
- ✅ Required format: XXXXX-XXXX (e.g., 12345-1234)`
808
+ text: W(e, "product NDC")
567
809
  }
568
810
  ],
569
811
  isError: !0
570
812
  };
571
- const t = new g().dataset("drug").context("label").search(`openfda.product_ndc:"${n.trim()}"`).limit(1).build(), { data: r, error: s } = await h(t);
572
- if (s)
813
+ const r = new y().dataset("drug").context("label").search(`openfda.product_ndc:"${t}"`).limit(1).build(), { data: i, error: o } = await v(r);
814
+ if (o)
573
815
  return {
574
816
  content: [
575
817
  {
576
818
  type: "text",
577
- text: `${t}Failed to retrieve drug data for product NDC "${n}": ${s.message}`
819
+ text: `Failed to retrieve drug data for product NDC "${e}": ${o.message}`
578
820
  }
579
821
  ],
580
822
  isError: !0
581
823
  };
582
- if (!(r != null && r.results) || r.results.length === 0)
824
+ if (!(i != null && i.results) || i.results.length === 0)
583
825
  return {
584
826
  content: [
585
827
  {
586
828
  type: "text",
587
- text: `No drug found with product NDC "${n}".`
829
+ text: `No drug found with product NDC "${e}".`
588
830
  }
589
831
  ],
590
832
  structuredContent: null
591
833
  };
592
- const e = r.results[0], o = ((p = e.openfda.package_ndc) == null ? void 0 : p.filter(
593
- (i) => i.startsWith(n.trim())
594
- )) || [], a = {
595
- product_ndc: n,
596
- available_packages: o,
597
- brand_name: e.openfda.brand_name || [],
598
- generic_name: e.openfda.generic_name || [],
599
- manufacturer_name: e.openfda.manufacturer_name || [],
600
- product_type: e.openfda.product_type || [],
601
- route: e.openfda.route || [],
602
- substance_name: e.openfda.substance_name || [],
603
- active_ingredient: e.active_ingredient || [],
604
- purpose: e.purpose || [],
605
- dosage_and_administration: e.dosage_and_administration || []
834
+ const s = i.results[0], d = ((l = s.openfda.package_ndc) == null ? void 0 : l.filter(
835
+ (a) => a.startsWith(t)
836
+ )) || [], c = {
837
+ product_ndc: t,
838
+ available_packages: d,
839
+ brand_name: s.openfda.brand_name || [],
840
+ generic_name: s.openfda.generic_name || [],
841
+ manufacturer_name: s.openfda.manufacturer_name || [],
842
+ product_type: s.openfda.product_type || [],
843
+ route: s.openfda.route || [],
844
+ substance_name: s.openfda.substance_name || [],
845
+ active_ingredient: s.active_ingredient || [],
846
+ purpose: s.purpose || [],
847
+ dosage_and_administration: s.dosage_and_administration || []
606
848
  };
607
849
  return {
608
850
  content: [
609
851
  {
610
852
  type: "text",
611
- text: `✅ Product NDC "${n}" found with ${o.length} package variation(s):
853
+ text: `✅ Product NDC "${t}" found with ${d.length} package variation(s):
612
854
 
613
- ${JSON.stringify(a, null, 2)}`
855
+ ${JSON.stringify(c, null, 2)}`
614
856
  }
615
857
  ]
616
858
  };
617
859
  }
618
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 = {
619
928
  name: "get-drugsfda",
620
- 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.",
621
- inputSchema: d.object({
622
- sectionName: d.string().describe(
623
- "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(". ")}`
624
944
  ),
625
- fieldName: d.string().describe(
626
- "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."
627
947
  ),
628
- 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")
629
949
  }),
630
950
  async handler({
631
- sectionName: n,
951
+ sectionName: e,
632
952
  fieldName: t,
633
- searchValue: r
953
+ searchValue: n,
954
+ limit: r
634
955
  }) {
635
- const s = new g().dataset("drug").context("drugsfda").search(`${n}.${t}:"${r}"`).limit(1).build(), { data: e, error: o } = await h(s);
636
- if (o) {
637
- let a = `${s} Failed to retrieve drugsfda data for "${r}" in ${n}.${t}: ${o.message}`;
638
- switch (o.type) {
639
- case "http":
640
- o.status === 404 ? a += `
641
-
642
- Suggestions:
643
- - Verify the field name is correct for the section
644
- - Check the search value spelling` : (o.status === 401 || o.status === 403) && (a += `
645
-
646
- Please check the API key configuration.`);
647
- break;
648
- case "network":
649
- a += `
650
-
651
- Please check your internet connection and try again.`;
652
- break;
653
- case "timeout":
654
- a += `
655
-
656
- The request took too long. Please try again.`;
657
- break;
658
- }
956
+ var g, f;
957
+ const i = r ?? 5, o = ve(e, t);
958
+ if (!o.ok)
659
959
  return {
660
- content: [{ type: "text", text: a }],
960
+ content: [{ type: "text", text: o.message }],
661
961
  isError: !0
662
962
  };
663
- }
664
- return !(e != null && e.results) || e.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 {
665
985
  content: [
666
986
  {
667
987
  type: "text",
668
- text: `No drugsfda data found for "${r}" in ${n}.${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)}`
669
991
  }
670
992
  ]
671
- } : {
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 {
672
1048
  content: [
673
1049
  {
674
1050
  type: "text",
675
- text: `drugsfda data retrieved successfully:
1051
+ text: `Top ${s.results.length} values of ${r} for "${e}" (aggregated responses carry no result total)
676
1052
 
677
- ${JSON.stringify(e.results[0], null, 2)}`
1053
+ ${JSON.stringify(c, null, 2)}`
678
1054
  }
679
1055
  ]
680
1056
  };
681
1057
  }
682
- }, X = new N(
1058
+ }, Ee = "1.2.0", Y = new z(
683
1059
  {
684
1060
  name: "openfda",
685
- version: "1.0.0",
1061
+ version: Ee,
686
1062
  description: "OpenFDA Model Context Protocol"
687
1063
  },
688
1064
  {
@@ -691,19 +1067,21 @@ ${JSON.stringify(e.results[0], null, 2)}`
691
1067
  tools: {}
692
1068
  }
693
1069
  }
694
- ), y = new T(X);
695
- y.registerTool(S);
696
- y.registerTool(E);
697
- y.registerTool(P);
698
- y.registerTool(A);
699
- y.registerTool(U);
700
- y.registerTool(I);
701
- y.registerTool(M);
702
- y.registerTool(R);
703
- async function j() {
704
- const n = new C();
705
- await X.connect(n), 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");
706
1084
  }
707
- j().catch((n) => {
708
- console.error("Fatal error in main():", n), process.exit(1);
1085
+ De().catch((e) => {
1086
+ console.error("Fatal error in main():", e), process.exit(1);
709
1087
  });