@ythalorossy/openfda 1.0.6 → 1.0.7

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.
@@ -0,0 +1,174 @@
1
+ const DEFAULT_CONFIG = {
2
+ maxRetries: 3,
3
+ retryDelay: 1000, // 1 second
4
+ timeout: 30000, // 30 seconds
5
+ };
6
+ // Helper function to determine if error is retryable
7
+ function isRetryableError(error) {
8
+ // Network errors, timeouts, and 5xx server errors are retryable
9
+ if (error.name === "TypeError" && error.message.includes("fetch"))
10
+ return true;
11
+ if (error.name === "AbortError")
12
+ return true;
13
+ if (error.status >= 500 && error.status <= 599)
14
+ return true;
15
+ if (error.status === 429)
16
+ return true; // Rate limit
17
+ return false;
18
+ }
19
+ // Sleep utility for retry delays
20
+ function sleep(ms) {
21
+ return new Promise((resolve) => setTimeout(resolve, ms));
22
+ }
23
+ // Enhanced OpenFDA request function
24
+ async function makeOpenFDARequest(url, config = {}) {
25
+ const { maxRetries, retryDelay, timeout } = { ...DEFAULT_CONFIG, ...config };
26
+ const headers = {
27
+ "User-Agent": "@ythalorossy/openfda",
28
+ Accept: "application/json",
29
+ };
30
+ let lastError = null;
31
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
32
+ try {
33
+ // Create abort controller for timeout handling
34
+ const controller = new AbortController();
35
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
36
+ console.log(`Making OpenFDA request (attempt ${attempt + 1}/${maxRetries + 1}): ${url}`);
37
+ const response = await fetch(url, {
38
+ headers,
39
+ signal: controller.signal,
40
+ });
41
+ clearTimeout(timeoutId);
42
+ // Handle HTTP errors with OpenFDA-specific context
43
+ if (!response.ok) {
44
+ const errorText = await response
45
+ .text()
46
+ .catch(() => "Unable to read error response");
47
+ const httpError = {
48
+ type: "http",
49
+ message: `HTTP ${response.status}: ${response.statusText}`,
50
+ status: response.status,
51
+ details: errorText,
52
+ };
53
+ console.error(`OpenFDA HTTP Error (${response.status}):`, {
54
+ url,
55
+ status: response.status,
56
+ statusText: response.statusText,
57
+ errorText: errorText.substring(0, 200), // Truncate long error messages
58
+ });
59
+ // OpenFDA-specific status code handling
60
+ switch (response.status) {
61
+ case 400:
62
+ httpError.message = `Bad Request: Invalid search query or parameters`;
63
+ break;
64
+ case 401:
65
+ httpError.message = `Unauthorized: Invalid or missing API key`;
66
+ break;
67
+ case 403:
68
+ httpError.message = `Forbidden: API key may be invalid or quota exceeded`;
69
+ break;
70
+ case 404:
71
+ httpError.message = `Not Found: No results found for the specified query`;
72
+ break;
73
+ case 429:
74
+ httpError.message = `Rate Limited: Too many requests. Retrying...`;
75
+ break;
76
+ case 500:
77
+ httpError.message = `Server Error: OpenFDA service is experiencing issues`;
78
+ break;
79
+ default:
80
+ httpError.message = `HTTP Error ${response.status}: ${response.statusText}`;
81
+ }
82
+ lastError = httpError;
83
+ // Don't retry client errors (4xx) except rate limiting
84
+ if (response.status >= 400 &&
85
+ response.status < 500 &&
86
+ response.status !== 429) {
87
+ break;
88
+ }
89
+ // Retry server errors and rate limits
90
+ if (attempt < maxRetries &&
91
+ isRetryableError({ status: response.status })) {
92
+ const delay = retryDelay * Math.pow(2, attempt); // Exponential backoff
93
+ console.log(`Retrying in ${delay}ms...`);
94
+ await sleep(delay);
95
+ continue;
96
+ }
97
+ break;
98
+ }
99
+ // Parse JSON response
100
+ let parsedData;
101
+ try {
102
+ parsedData = await response.json();
103
+ }
104
+ catch (parseError) {
105
+ const parsingError = {
106
+ type: "parsing",
107
+ message: `Failed to parse JSON response: ${parseError instanceof Error
108
+ ? parseError.message
109
+ : "Unknown parsing error"}`,
110
+ details: parseError,
111
+ };
112
+ console.error("OpenFDA JSON Parsing Error:", {
113
+ url,
114
+ parseError: parseError instanceof Error ? parseError.message : parseError,
115
+ });
116
+ lastError = parsingError;
117
+ break; // Don't retry parsing errors
118
+ }
119
+ // Check for empty response
120
+ if (!parsedData) {
121
+ const emptyError = {
122
+ type: "empty_response",
123
+ message: "Received empty response from OpenFDA API",
124
+ };
125
+ lastError = emptyError;
126
+ break;
127
+ }
128
+ console.log(`OpenFDA request successful on attempt ${attempt + 1}`);
129
+ return { data: parsedData, error: null };
130
+ }
131
+ catch (error) {
132
+ // Handle network errors, timeouts, and other fetch errors
133
+ let networkError;
134
+ if (error.name === "AbortError") {
135
+ networkError = {
136
+ type: "timeout",
137
+ message: `Request timeout after ${timeout}ms`,
138
+ details: error,
139
+ };
140
+ }
141
+ else if (error instanceof TypeError &&
142
+ error.message.includes("fetch")) {
143
+ networkError = {
144
+ type: "network",
145
+ message: `Network error: Unable to connect to OpenFDA API`,
146
+ details: error.message,
147
+ };
148
+ }
149
+ else {
150
+ networkError = {
151
+ type: "unknown",
152
+ message: `Unexpected error: ${error.message || "Unknown error occurred"}`,
153
+ details: error,
154
+ };
155
+ }
156
+ console.error(`OpenFDA Request Error (attempt ${attempt + 1}):`, {
157
+ url,
158
+ error: error.message,
159
+ type: error.name,
160
+ });
161
+ lastError = networkError;
162
+ // Retry network errors and timeouts
163
+ if (attempt < maxRetries && isRetryableError(error)) {
164
+ const delay = retryDelay * Math.pow(2, attempt); // Exponential backoff
165
+ console.log(`Network error, retrying in ${delay}ms...`);
166
+ await sleep(delay);
167
+ continue;
168
+ }
169
+ break;
170
+ }
171
+ }
172
+ return { data: null, error: lastError };
173
+ }
174
+ export { makeOpenFDARequest };
package/bin/index.js CHANGED
@@ -3,7 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import z from "zod";
5
5
  import { OpenFDABuilder } from "./OpenFDABuilder.js";
6
- const USER_AGENT = "openfda-mcp/1.0";
6
+ import { makeOpenFDARequest } from "./OpenFDAClient.js";
7
7
  const server = new McpServer({
8
8
  name: "openfda",
9
9
  version: "1.0.0",
@@ -13,29 +13,53 @@ const server = new McpServer({
13
13
  tools: {},
14
14
  },
15
15
  });
16
- // Helper function for making OpenFDA API requests
17
- async function makeOpenFDARequest(url) {
18
- const headers = {
19
- "User-Agent": USER_AGENT,
20
- Accept: "application/json",
21
- };
22
- try {
23
- const response = await fetch(url, { headers });
24
- if (!response.ok) {
25
- throw new Error(`HTTP error! status: ${response.status}`);
16
+ // Helper function to normalize and validate NDC format
17
+ function normalizeNDC(ndc) {
18
+ // Remove spaces and convert to uppercase
19
+ const cleanNDC = ndc.trim().toUpperCase();
20
+ // Check for different NDC formats:
21
+ // 1. XXXXX-XXXX (product NDC)
22
+ // 2. XXXXX-XXXX-XX (package NDC)
23
+ // 3. XXXXXXXXXXX (11-digit without dashes)
24
+ // 4. XXXXXXXXX (9-digit without dashes - product only)
25
+ let productNDC;
26
+ let packageNDC = null;
27
+ if (cleanNDC.includes('-')) {
28
+ const parts = cleanNDC.split('-');
29
+ if (parts.length === 2) {
30
+ // Format: XXXXX-XXXX (product NDC)
31
+ productNDC = cleanNDC;
32
+ packageNDC = null;
33
+ }
34
+ else if (parts.length === 3) {
35
+ // Format: XXXXX-XXXX-XX (package NDC)
36
+ productNDC = `${parts[0]}-${parts[1]}`;
37
+ packageNDC = cleanNDC;
38
+ }
39
+ else {
40
+ return { productNDC: cleanNDC, packageNDC: null, isValid: false };
26
41
  }
27
- return (await response.json());
28
42
  }
29
- catch (error) {
30
- console.error("Error making NWS request:", error);
31
- return null;
43
+ else {
44
+ // No dashes - need to format based on length
45
+ if (cleanNDC.length === 11) {
46
+ // 11-digit package NDC: XXXXXYYYYZZ -> XXXXX-YYYY-ZZ
47
+ productNDC = `${cleanNDC.substring(0, 5)}-${cleanNDC.substring(5, 9)}`;
48
+ packageNDC = `${cleanNDC.substring(0, 5)}-${cleanNDC.substring(5, 9)}-${cleanNDC.substring(9, 11)}`;
49
+ }
50
+ else if (cleanNDC.length === 9) {
51
+ // 9-digit product NDC: XXXXXXXXX -> XXXXX-XXXX
52
+ productNDC = `${cleanNDC.substring(0, 5)}-${cleanNDC.substring(5, 9)}`;
53
+ packageNDC = null;
54
+ }
55
+ else {
56
+ return { productNDC: cleanNDC, packageNDC: null, isValid: false };
57
+ }
32
58
  }
59
+ // Basic validation
60
+ const isValid = /^\d{5}-\d{4}$/.test(productNDC);
61
+ return { productNDC, packageNDC, isValid };
33
62
  }
34
- // Tool: get-drug-by-name
35
- // Description: Retrieves drug information from the OpenFDA API by brand name.
36
- // Input: drugName (string) - The brand name of the drug to search for.
37
- // Output: Returns key drug information fields such as brand name, generic name, manufacturer, NDC, product type, route, substance name, indications and usage, warnings, and other safety information.
38
- // Usage: Use this tool to look up detailed drug label data for a given brand name.
39
63
  server.tool("get-drug-by-name", "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.", {
40
64
  drugName: z.string().describe("Drug name"),
41
65
  }, async ({ drugName }) => {
@@ -44,18 +68,42 @@ server.tool("get-drug-by-name", "Get drug by name. Use this tool to get the drug
44
68
  .search(`openfda.brand_name:"${drugName}"`)
45
69
  .limit(1)
46
70
  .build();
47
- const drugData = await makeOpenFDARequest(url);
48
- if (!drugData) {
71
+ const { data: drugData, error } = await makeOpenFDARequest(url);
72
+ if (error) {
73
+ let errorMessage = `Failed to retrieve drug data for "${drugName}": ${error.message}`;
74
+ // Provide helpful suggestions based on error type
75
+ switch (error.type) {
76
+ case 'http':
77
+ if (error.status === 404) {
78
+ errorMessage += `\n\nSuggestions:\n- Verify the exact brand name spelling\n- Try searching for the generic name instead\n- Check if the drug is FDA-approved`;
79
+ }
80
+ else if (error.status === 401 || error.status === 403) {
81
+ errorMessage += `\n\nPlease check the API key configuration.`;
82
+ }
83
+ break;
84
+ case 'network':
85
+ errorMessage += `\n\nPlease check your internet connection and try again.`;
86
+ break;
87
+ case 'timeout':
88
+ errorMessage += `\n\nThe request took too long. Please try again.`;
89
+ break;
90
+ }
49
91
  return {
50
- content: [
51
- {
92
+ content: [{
52
93
  type: "text",
53
- text: `Failed to retrieve drug data for ${drugName}`,
54
- },
55
- ],
94
+ text: errorMessage,
95
+ }],
56
96
  };
57
97
  }
58
- const drug = drugData.results.at(0);
98
+ if (!drugData || !drugData.results || drugData.results.length === 0) {
99
+ return {
100
+ content: [{
101
+ type: "text",
102
+ text: `No drug information found for "${drugName}". Please verify the brand name spelling or try searching for the generic name.`,
103
+ }],
104
+ };
105
+ }
106
+ const drug = drugData.results[0];
59
107
  const drugInfo = {
60
108
  brand_name: drug?.openfda.brand_name,
61
109
  generic_name: drug?.openfda.generic_name,
@@ -73,18 +121,322 @@ server.tool("get-drug-by-name", "Get drug by name. Use this tool to get the drug
73
121
  pregnancy_or_breast_feeding: drug?.pregnancy_or_breast_feeding,
74
122
  };
75
123
  return {
76
- content: [
77
- {
124
+ content: [{
125
+ type: "text",
126
+ text: `Drug information retrieved successfully:\n\n${JSON.stringify(drugInfo, null, 2)}`,
127
+ }],
128
+ };
129
+ });
130
+ server.tool("get-drug-by-generic-name", "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.", {
131
+ genericName: z.string().describe("Generic drug name (active ingredient)"),
132
+ limit: z.number().optional().default(5).describe("Maximum number of results to return")
133
+ }, async ({ genericName, limit }) => {
134
+ const url = new OpenFDABuilder()
135
+ .context("label")
136
+ .search(`openfda.generic_name:"${genericName}"`)
137
+ .limit(limit)
138
+ .build();
139
+ const { data: drugData, error } = await makeOpenFDARequest(url);
140
+ if (error) {
141
+ return {
142
+ content: [{
143
+ type: "text",
144
+ text: `Failed to retrieve drug data for generic name "${genericName}": ${error.message}`,
145
+ }],
146
+ };
147
+ }
148
+ if (!drugData || !drugData.results || drugData.results.length === 0) {
149
+ return {
150
+ content: [{
151
+ type: "text",
152
+ text: `No drug information found for generic name "${genericName}".`,
153
+ }],
154
+ };
155
+ }
156
+ const drugs = drugData.results.map(drug => ({
157
+ brand_name: drug?.openfda.brand_name?.[0] || 'Unknown',
158
+ generic_name: drug?.openfda.generic_name?.[0] || 'Unknown',
159
+ manufacturer_name: drug?.openfda.manufacturer_name?.[0] || 'Unknown',
160
+ product_type: drug?.openfda.product_type?.[0] || 'Unknown',
161
+ route: drug?.openfda.route || [],
162
+ }));
163
+ return {
164
+ content: [{
165
+ type: "text",
166
+ text: `Found ${drugs.length} drug(s) with generic name "${genericName}":\n\n${JSON.stringify(drugs, null, 2)}`,
167
+ }],
168
+ };
169
+ });
170
+ server.tool("get-drug-adverse-events", "Get adverse event reports for a drug. This provides safety information about reported side effects and reactions. Use brand name or generic name.", {
171
+ drugName: z.string().describe("Drug name (brand or generic)"),
172
+ limit: z.number().optional().default(10).describe("Maximum number of events to return"),
173
+ seriousness: z.enum(["serious", "non-serious", "all"]).optional().default("all").describe("Filter by event seriousness")
174
+ }, async ({ drugName, limit, seriousness }) => {
175
+ let searchQuery = `patient.drug.medicinalproduct:"${drugName}"`;
176
+ if (seriousness !== "all") {
177
+ const serious = seriousness === "serious" ? "1" : "2";
178
+ searchQuery += `+AND+serious:${serious}`;
179
+ }
180
+ const url = new OpenFDABuilder()
181
+ .context("event")
182
+ .search(searchQuery)
183
+ .limit(limit)
184
+ .build();
185
+ const { data: eventData, error } = await makeOpenFDARequest(url);
186
+ if (error) {
187
+ return {
188
+ content: [{
189
+ type: "text",
190
+ text: `Failed to retrieve adverse events for "${drugName}": ${error.message}`,
191
+ }],
192
+ };
193
+ }
194
+ if (!eventData || !eventData.results || eventData.results.length === 0) {
195
+ return {
196
+ content: [{
197
+ type: "text",
198
+ text: `No adverse events found for "${drugName}".`,
199
+ }],
200
+ };
201
+ }
202
+ const events = eventData.results.map((event) => ({
203
+ report_id: event.safetyreportid,
204
+ serious: event.serious === "1" ? "Yes" : "No",
205
+ patient_age: event.patient?.patientonsetage || "Unknown",
206
+ patient_sex: event.patient?.patientsex === "1" ? "Male" : event.patient?.patientsex === "2" ? "Female" : "Unknown",
207
+ reactions: event.patient?.reaction?.map((r) => r.reactionmeddrapt).slice(0, 3) || [],
208
+ outcomes: event.patient?.reaction?.map((r) => r.reactionoutcome).slice(0, 3) || [],
209
+ report_date: event.receiptdate || "Unknown"
210
+ }));
211
+ return {
212
+ content: [{
213
+ type: "text",
214
+ text: `Found ${events.length} adverse event report(s) for "${drugName}":\n\n${JSON.stringify(events, null, 2)}`,
215
+ }],
216
+ };
217
+ });
218
+ server.tool("get-drugs-by-manufacturer", "Get all drugs manufactured by a specific company. Useful for finding alternatives or checking manufacturer portfolios.", {
219
+ manufacturerName: z.string().describe("Manufacturer/company name"),
220
+ limit: z.number().optional().default(20).describe("Maximum number of drugs to return")
221
+ }, async ({ manufacturerName, limit }) => {
222
+ const url = new OpenFDABuilder()
223
+ .context("label")
224
+ .search(`openfda.manufacturer_name:"${manufacturerName}"`)
225
+ .limit(limit)
226
+ .build();
227
+ const { data: drugData, error } = await makeOpenFDARequest(url);
228
+ if (error) {
229
+ return {
230
+ content: [{
231
+ type: "text",
232
+ text: `Failed to retrieve drugs for manufacturer "${manufacturerName}": ${error.message}`,
233
+ }],
234
+ };
235
+ }
236
+ if (!drugData || !drugData.results || drugData.results.length === 0) {
237
+ return {
238
+ content: [{
239
+ type: "text",
240
+ text: `No drugs found for manufacturer "${manufacturerName}".`,
241
+ }],
242
+ };
243
+ }
244
+ const drugs = drugData.results.map(drug => ({
245
+ brand_name: drug?.openfda.brand_name?.[0] || 'Unknown',
246
+ generic_name: drug?.openfda.generic_name?.[0] || 'Unknown',
247
+ product_type: drug?.openfda.product_type?.[0] || 'Unknown',
248
+ route: drug?.openfda.route || [],
249
+ ndc: drug?.openfda.product_ndc?.[0] || 'Unknown'
250
+ }));
251
+ return {
252
+ content: [{
253
+ type: "text",
254
+ text: `Found ${drugs.length} drug(s) from manufacturer "${manufacturerName}":\n\n${JSON.stringify(drugs, null, 2)}`,
255
+ }],
256
+ };
257
+ });
258
+ server.tool("get-drug-safety-info", "Get comprehensive safety information for a drug including warnings, contraindications, drug interactions, and precautions. Use brand name.", {
259
+ drugName: z.string().describe("Drug brand name")
260
+ }, async ({ drugName }) => {
261
+ const url = new OpenFDABuilder()
262
+ .context("label")
263
+ .search(`openfda.brand_name:"${drugName}"`)
264
+ .limit(1)
265
+ .build();
266
+ const { data: drugData, error } = await makeOpenFDARequest(url);
267
+ if (error) {
268
+ return {
269
+ content: [{
270
+ type: "text",
271
+ text: `Failed to retrieve safety information for "${drugName}": ${error.message}`,
272
+ }],
273
+ };
274
+ }
275
+ if (!drugData || !drugData.results || drugData.results.length === 0) {
276
+ return {
277
+ content: [{
278
+ type: "text",
279
+ text: `No safety information found for "${drugName}".`,
280
+ }],
281
+ };
282
+ }
283
+ const drug = drugData.results[0];
284
+ const safetyInfo = {
285
+ drug_name: drug?.openfda.brand_name?.[0] || drugName,
286
+ generic_name: drug?.openfda.generic_name?.[0] || 'Unknown',
287
+ warnings: drug?.warnings || [],
288
+ contraindications: drug?.contraindications || [],
289
+ drug_interactions: drug?.drug_interactions || [],
290
+ precautions: drug?.precautions || [],
291
+ adverse_reactions: drug?.adverse_reactions || [],
292
+ overdosage: drug?.overdosage || [],
293
+ do_not_use: drug?.do_not_use || [],
294
+ ask_doctor: drug?.ask_doctor || [],
295
+ stop_use: drug?.stop_use || [],
296
+ pregnancy_or_breast_feeding: drug?.pregnancy_or_breast_feeding || []
297
+ };
298
+ return {
299
+ content: [{
300
+ type: "text",
301
+ text: `Safety information for "${drugName}":\n\n${JSON.stringify(safetyInfo, null, 2)}`,
302
+ }],
303
+ };
304
+ });
305
+ server.tool("get-drug-by-ndc", "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.", {
306
+ ndcCode: z.string().describe("National Drug Code (NDC) - accepts formats: XXXXX-XXXX, XXXXX-XXXX-XX, or without dashes")
307
+ }, async ({ ndcCode }) => {
308
+ const { productNDC, packageNDC, isValid } = normalizeNDC(ndcCode);
309
+ if (!isValid) {
310
+ return {
311
+ content: [{
312
+ type: "text",
313
+ text: `Invalid NDC format: "${ndcCode}"\n\nāœ… Accepted formats:\n• Product NDC: 12345-1234\n• Package NDC: 12345-1234-01\n• Without dashes: 123451234 or 12345123401`,
314
+ }],
315
+ };
316
+ }
317
+ console.log(`Searching for NDC: input="${ndcCode}", productNDC="${productNDC}", packageNDC="${packageNDC}"`);
318
+ // Try searching by product NDC first (most flexible)
319
+ let searchQuery = `openfda.product_ndc:"${productNDC}"`;
320
+ // If a specific package was requested, also search package NDC
321
+ if (packageNDC) {
322
+ searchQuery += `+OR+openfda.package_ndc:"${packageNDC}"`;
323
+ }
324
+ const url = new OpenFDABuilder()
325
+ .context("label")
326
+ .search(searchQuery)
327
+ .limit(10) // Get multiple results since product NDC might have multiple packages
328
+ .build();
329
+ const { data: drugData, error } = await makeOpenFDARequest(url);
330
+ if (error) {
331
+ return {
332
+ content: [{
333
+ type: "text",
334
+ text: `Failed to retrieve drug data for NDC "${ndcCode}": ${error.message}`,
335
+ }],
336
+ };
337
+ }
338
+ if (!drugData || !drugData.results || drugData.results.length === 0) {
339
+ return {
340
+ content: [{
341
+ type: "text",
342
+ text: `No drug found with NDC "${ndcCode}" (product: ${productNDC}).\n\nšŸ’” Tips:\n• Verify the NDC format\n• Try without the package suffix (e.g., use 12345-1234 instead of 12345-1234-01)\n• Check if this is an FDA-approved product`,
343
+ }],
344
+ };
345
+ }
346
+ // Process results and group by product
347
+ const results = drugData.results.map(drug => {
348
+ const matchingProductNDCs = drug.openfda.product_ndc?.filter(ndc => ndc === productNDC) || [];
349
+ const matchingPackageNDCs = drug.openfda.package_ndc?.filter(ndc => packageNDC ? ndc === packageNDC : ndc.startsWith(productNDC)) || [];
350
+ return {
351
+ // Basic drug information
352
+ brand_name: drug.openfda.brand_name || [],
353
+ generic_name: drug.openfda.generic_name || [],
354
+ manufacturer_name: drug.openfda.manufacturer_name || [],
355
+ product_type: drug.openfda.product_type || [],
356
+ route: drug.openfda.route || [],
357
+ substance_name: drug.openfda.substance_name || [],
358
+ // NDC information
359
+ matching_product_ndc: matchingProductNDCs,
360
+ matching_package_ndc: matchingPackageNDCs,
361
+ all_product_ndc: drug.openfda.product_ndc || [],
362
+ all_package_ndc: drug.openfda.package_ndc || [],
363
+ // Additional product details
364
+ dosage_and_administration: drug.dosage_and_administration || [],
365
+ package_label_principal_display_panel: drug.package_label_principal_display_panel || [],
366
+ active_ingredient: drug.active_ingredient || [],
367
+ purpose: drug.purpose || []
368
+ };
369
+ });
370
+ // Summary information
371
+ const totalPackages = results.reduce((sum, result) => sum + result.matching_package_ndc.length, 0);
372
+ const searchSummary = packageNDC
373
+ ? `Searched for specific package NDC: ${packageNDC}`
374
+ : `Searched for product NDC: ${productNDC} (all packages)`;
375
+ return {
376
+ content: [{
377
+ type: "text",
378
+ text: `āœ… Found ${results.length} drug(s) with ${totalPackages} package(s) for NDC "${ndcCode}"\n\n${searchSummary}\n\n${JSON.stringify(results, null, 2)}`,
379
+ }],
380
+ };
381
+ });
382
+ // Alternative: Simple product-only NDC search tool
383
+ server.tool("get-drug-by-product-ndc", "Get drug information by product NDC only (XXXXX-XXXX format). This ignores package variations and finds all packages for a product.", {
384
+ productNDC: z.string().describe("Product NDC in format XXXXX-XXXX")
385
+ }, async ({ productNDC }) => {
386
+ // Validate product NDC format
387
+ if (!/^\d{5}-\d{4}$/.test(productNDC.trim())) {
388
+ return {
389
+ content: [{
390
+ type: "text",
391
+ text: `Invalid product NDC format: "${productNDC}"\n\nāœ… Required format: XXXXX-XXXX (e.g., 12345-1234)`,
392
+ }],
393
+ };
394
+ }
395
+ const url = new OpenFDABuilder()
396
+ .context("label")
397
+ .search(`openfda.product_ndc:"${productNDC.trim()}"`)
398
+ .limit(1)
399
+ .build();
400
+ const { data: drugData, error } = await makeOpenFDARequest(url);
401
+ if (error) {
402
+ return {
403
+ content: [{
404
+ type: "text",
405
+ text: `Failed to retrieve drug data for product NDC "${productNDC}": ${error.message}`,
406
+ }],
407
+ };
408
+ }
409
+ if (!drugData || !drugData.results || drugData.results.length === 0) {
410
+ return {
411
+ content: [{
412
+ type: "text",
413
+ text: `No drug found with product NDC "${productNDC}".`,
414
+ }],
415
+ };
416
+ }
417
+ const drug = drugData.results[0];
418
+ // Get all packages for this product
419
+ const allPackagesForProduct = drug.openfda.package_ndc?.filter(ndc => ndc.startsWith(productNDC.trim())) || [];
420
+ const drugInfo = {
421
+ product_ndc: productNDC,
422
+ available_packages: allPackagesForProduct,
423
+ brand_name: drug.openfda.brand_name || [],
424
+ generic_name: drug.openfda.generic_name || [],
425
+ manufacturer_name: drug.openfda.manufacturer_name || [],
426
+ product_type: drug.openfda.product_type || [],
427
+ route: drug.openfda.route || [],
428
+ substance_name: drug.openfda.substance_name || [],
429
+ active_ingredient: drug.active_ingredient || [],
430
+ purpose: drug.purpose || [],
431
+ dosage_and_administration: drug.dosage_and_administration || []
432
+ };
433
+ return {
434
+ content: [{
78
435
  type: "text",
79
- text: `drug: ${JSON.stringify(drugInfo)}`,
80
- },
81
- ],
436
+ text: `āœ… Product NDC "${productNDC}" found with ${allPackagesForProduct.length} package variation(s):\n\n${JSON.stringify(drugInfo, null, 2)}`,
437
+ }],
82
438
  };
83
439
  });
84
- // The above code registers a tool named "get-drug-by-name" with the MCP server.
85
- // This tool allows users to retrieve detailed drug label information from the OpenFDA API
86
- // by providing a brand name. It constructs the appropriate API request, fetches the data,
87
- // and returns key drug information fields in a structured text response.
88
440
  async function main() {
89
441
  const transport = new StdioServerTransport();
90
442
  await server.connect(transport);
package/build/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
4
  import z from "zod";
@@ -262,7 +263,6 @@ server.tool("get-drug-safety-info", "Get comprehensive safety information for a
262
263
  .search(`openfda.brand_name:"${drugName}"`)
263
264
  .limit(1)
264
265
  .build();
265
- console.log(`Calling: ${url}`);
266
266
  const { data: drugData, error } = await makeOpenFDARequest(url);
267
267
  if (error) {
268
268
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ythalorossy/openfda",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "OpenFDA Model Context Protocol",
5
5
  "repository": {
6
6
  "type": "git",