Skip to main content
Glama
ezhou89

Medical Research MCP Suite

by ezhou89
fda.ts15.9 kB
// src/apis/fda.ts import axios, { AxiosInstance } from 'axios'; import { ResponseSizeMonitor } from '../utils/responseSizeMonitor.js'; import { SearchRefinementService } from '../services/searchRefinementService.js'; export interface FDADrugSearchParams { drugName?: string; activeIngredient?: string; approvalStatus?: string; limit?: number; } export interface FDAAdverseEventParams { drugName: string; dateRange?: { from?: string; to?: string; }; limit?: number; } export interface FDADrug { applicationNumber: string; sponsorName: string; brandName?: string; genericName?: string; activeIngredients: Array<{ name: string; strength: string; }>; approvalDate: string; approvalStatus: string; indication: string; dosageForm: string; route: string; } export interface FDAAdverseEvent { reportId: string; drugName: string; eventDate: string; eventDescription: string; seriousness: string; outcome: string; patientAge?: number; patientSex?: string; reporterType: string; } export interface FDADrugSearchResponse { drugs: FDADrug[]; totalCount: number; } export interface FDAAdverseEventResponse { events: FDAAdverseEvent[]; totalCount: number; summary: { total: number; serious: number; hospitalizations: number; deaths: number; topEvents: Array<{ event: string; count: number; }>; }; } export class FDAClient { private axios: AxiosInstance; private readonly baseURL = 'https://api.fda.gov'; private cache = new Map<string, { data: any; timestamp: number }>(); private readonly cacheTimeout = 3600000; // 1 hour private readonly apiKey?: string; private sizeMonitor: ResponseSizeMonitor; private refinementService: SearchRefinementService; private enableSizeMonitoring: boolean = true; constructor() { this.apiKey = process.env.FDA_API_KEY; this.sizeMonitor = ResponseSizeMonitor.getInstance(); this.refinementService = SearchRefinementService.getInstance(); this.axios = axios.create({ baseURL: this.baseURL, timeout: 30000, headers: { 'User-Agent': 'Medical-Research-MCP-Suite/1.0.0 (https://github.com/medical-research-mcp; contact@example.com)', 'Accept': 'application/json', 'Content-Type': 'application/json', }, }); // Add response interceptor for error handling this.axios.interceptors.response.use( (response) => response, (error) => { const status = error.response?.status || 'Unknown'; const statusText = error.response?.statusText || ''; const errorData = error.response?.data?.error; const message = errorData?.message || errorData?.code || error.message || 'Unknown error'; throw new Error(`FDA API Error: ${status} ${statusText} - ${message}`); } ); } private getCacheKey(params: any): string { return `fda_${JSON.stringify(params)}`; } private isValidCache(timestamp: number): boolean { return Date.now() - timestamp < this.cacheTimeout; } async searchDrugs(params: FDADrugSearchParams): Promise<FDADrugSearchResponse> { const cacheKey = this.getCacheKey({ ...params, endpoint: 'drugs' }); // Check cache first const cached = this.cache.get(cacheKey); if (cached && this.isValidCache(cached.timestamp)) { return cached.data; } let searchQuery = '*'; // Declare outside try block try { // Build search query const searchTerms: string[] = []; if (params.drugName) { // Search both brand and generic names searchTerms.push(`(openfda.brand_name:"${params.drugName}"+OR+openfda.generic_name:"${params.drugName}"+OR+products.brand_name:"${params.drugName}"+OR+products.generic_name:"${params.drugName}")`); } if (params.activeIngredient) { // Try multiple field patterns for active ingredient search searchTerms.push(`products.active_ingredients.name:"${params.activeIngredient}"`); } if (params.approvalStatus) { searchTerms.push(`approval_status:"${params.approvalStatus}"`); } searchQuery = searchTerms.length > 0 ? searchTerms.join('+AND+') : '*'; // FDA search query constructed const searchParams = new URLSearchParams(); searchParams.append('search', searchQuery); searchParams.append('limit', (params.limit || 20).toString()); if (this.apiKey) { searchParams.append('api_key', this.apiKey); } // FDA API request prepared const response = await this.axios.get('/drug/drugsfda.json', { params: searchParams, }); const results = response.data.results || []; const drugs = results.map((drug: any) => this.transformDrug(drug)); const result = { drugs, totalCount: response.data.meta?.results?.total || drugs.length, }; // Check response size if monitoring is enabled if (this.enableSizeMonitoring) { const sizeCheck = this.sizeMonitor.checkSizeLimit(result, 'fda-drugs-search'); if (!sizeCheck.withinLimit && sizeCheck.exceededInfo) { // FDA search result size exceeds limit // Note: Unlike clinical trials, we'll allow oversized FDA results for now // but log a warning for future refinement implementation } } // Cache the result this.cache.set(cacheKey, { data: result, timestamp: Date.now(), }); return result; } catch (error: any) { // FDA search error occurred - check logs for details // If no results found, return empty array instead of error if (error.response?.status === 404 || (error.response?.data?.error?.code === 'NOT_FOUND') || error.message.includes('NOT_FOUND')) { // FDA API returned no results return { drugs: [], totalCount: 0, }; } // Try alternative search strategy for any search that fails if (params.activeIngredient || params.drugName) { // Attempting alternative search strategies return await this.searchDrugsAlternative(params); } throw new Error(`Failed to search FDA drugs: ${error.response?.status} ${error.response?.statusText || error.message}`); } } private async searchDrugsAlternative(params: FDADrugSearchParams): Promise<FDADrugSearchResponse> { // Attempting alternative FDA search strategies let alternativeStrategies: string[] = []; // Add strategies for active ingredient search if (params.activeIngredient) { alternativeStrategies.push( `products.active_ingredients.name.exact:"${params.activeIngredient}"`, `active_ingredients.name:"${params.activeIngredient}"`, `products.active_ingredient:"${params.activeIngredient}"`, `active_ingredient:"${params.activeIngredient}"`, // Try partial matching `products.active_ingredients.name:${params.activeIngredient}*`, `active_ingredients.name:${params.activeIngredient}*`, ); } // Add strategies for drug name search if (params.drugName) { alternativeStrategies.push( `products.brand_name.exact:"${params.drugName}"`, `products.generic_name.exact:"${params.drugName}"`, `openfda.brand_name.exact:"${params.drugName}"`, `openfda.generic_name.exact:"${params.drugName}"`, `brand_name:"${params.drugName}"`, `generic_name:"${params.drugName}"`, // Try partial matching `products.brand_name:${params.drugName}*`, `products.generic_name:${params.drugName}*`, `openfda.brand_name:${params.drugName}*`, `openfda.generic_name:${params.drugName}*`, ); } for (const strategy of alternativeStrategies) { try { // Trying FDA search strategy const searchParams = new URLSearchParams(); searchParams.append('search', strategy); searchParams.append('limit', (params.limit || 20).toString()); if (this.apiKey) { searchParams.append('api_key', this.apiKey); } const response = await this.axios.get('/drug/drugsfda.json', { params: searchParams, }); if (response.data.results && response.data.results.length > 0) { // FDA alternative search successful const results = response.data.results || []; const drugs = results.map((drug: any) => this.transformDrug(drug)); return { drugs, totalCount: response.data.meta?.results?.total || drugs.length, }; } } catch (error: any) { // FDA strategy failed continue; // Try next strategy } } // If all strategies fail, try a broader search const searchTerm = params.activeIngredient || params.drugName; if (searchTerm) { // Trying broad text search try { const searchParams = new URLSearchParams(); searchParams.append('search', `"${searchTerm}"`); // Simple text search searchParams.append('limit', (params.limit || 20).toString()); if (this.apiKey) { searchParams.append('api_key', this.apiKey); } const response = await this.axios.get('/drug/drugsfda.json', { params: searchParams, }); if (response.data.results && response.data.results.length > 0) { // FDA broad text search successful const results = response.data.results || []; const drugs = results.map((drug: any) => this.transformDrug(drug)); return { drugs, totalCount: response.data.meta?.results?.total || drugs.length, }; } } catch (error: any) { // FDA broad search failed } } // All FDA search strategies exhausted return { drugs: [], totalCount: 0, }; } async getAdverseEvents(params: FDAAdverseEventParams): Promise<FDAAdverseEventResponse> { const cacheKey = this.getCacheKey({ ...params, endpoint: 'adverse_events' }); // Check cache first const cached = this.cache.get(cacheKey); if (cached && this.isValidCache(cached.timestamp)) { return cached.data; } try { // Build search query for adverse events let searchQuery = `patient.drug.medicinalproduct:"${params.drugName}"`; // Add date range if specified if (params.dateRange) { if (params.dateRange.from && params.dateRange.to) { const fromDate = params.dateRange.from.replace(/-/g, ''); const toDate = params.dateRange.to.replace(/-/g, ''); searchQuery += `+AND+receivedate:[${fromDate}+TO+${toDate}]`; } } const searchParams = new URLSearchParams(); searchParams.append('search', searchQuery); searchParams.append('limit', (params.limit || 100).toString()); if (this.apiKey) { searchParams.append('api_key', this.apiKey); } const response = await this.axios.get('/drug/event.json', { params: searchParams, }); const results = response.data.results || []; const events = results.map((event: any) => this.transformAdverseEvent(event)); // Generate summary statistics const summary = this.generateAdverseEventSummary(events); const result = { events, totalCount: response.data.meta?.results?.total || events.length, summary, }; // Check response size if monitoring is enabled if (this.enableSizeMonitoring) { const sizeCheck = this.sizeMonitor.checkSizeLimit(result, 'fda-adverse-events'); if (!sizeCheck.withinLimit && sizeCheck.exceededInfo) { // FDA adverse events result size exceeds limit } } // Cache the result this.cache.set(cacheKey, { data: result, timestamp: Date.now(), }); return result; } catch (error: any) { // If no results found, return empty array instead of error if (error.response?.status === 404) { return { events: [], totalCount: 0, summary: { total: 0, serious: 0, hospitalizations: 0, deaths: 0, topEvents: [], }, }; } throw new Error(`Failed to get adverse events: ${error}`); } } private transformDrug(drug: any): FDADrug { const products = drug.products || []; const product = products[0] || {}; return { applicationNumber: drug.application_number || 'Unknown', sponsorName: drug.sponsor_name || 'Unknown', brandName: product.brand_name, genericName: product.generic_name, activeIngredients: (product.active_ingredients || []).map((ingredient: any) => ({ name: ingredient.name || 'Unknown', strength: ingredient.strength || 'Unknown', })), approvalDate: product.marketing_status?.[0]?.marketing_start_date || 'Unknown', approvalStatus: product.marketing_status?.[0]?.marketing_category || 'Unknown', indication: product.te_code || 'Not specified', dosageForm: product.dosage_form || 'Unknown', route: product.route || 'Unknown', }; } private transformAdverseEvent(event: any): FDAAdverseEvent { const patient = event.patient || {}; const drugs = patient.drug || []; const reactions = patient.reaction || []; // Get the primary drug name const drugName = drugs[0]?.medicinalproduct || 'Unknown'; // Get the primary reaction const primaryReaction = reactions[0]?.reactionmeddrapt || 'Unknown'; return { reportId: event.safetyreportid || 'Unknown', drugName, eventDate: event.receivedate || 'Unknown', eventDescription: primaryReaction, seriousness: event.serious === '1' ? 'Serious' : 'Non-serious', outcome: reactions[0]?.reactionoutcome || 'Unknown', patientAge: patient.patientonsetage ? parseInt(patient.patientonsetage) : undefined, patientSex: patient.patientsex === '1' ? 'Male' : patient.patientsex === '2' ? 'Female' : 'Unknown', reporterType: event.primarysourcecountry || 'Unknown', }; } private generateAdverseEventSummary(events: FDAAdverseEvent[]): any { const total = events.length; const serious = events.filter(e => e.seriousness === 'Serious').length; // Count hospitalizations and deaths (simplified) const hospitalizations = events.filter(e => e.outcome.toLowerCase().includes('hospital') || e.outcome === '2' ).length; const deaths = events.filter(e => e.outcome.toLowerCase().includes('death') || e.outcome === '5' ).length; // Generate top events const eventCounts = new Map<string, number>(); events.forEach(event => { const description = event.eventDescription; eventCounts.set(description, (eventCounts.get(description) || 0) + 1); }); const topEvents = Array.from(eventCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([event, count]) => ({ event, count })); return { total, serious, hospitalizations, deaths, topEvents, }; } /** * Enable or disable size monitoring */ setSizeMonitoring(enabled: boolean): void { this.enableSizeMonitoring = enabled; } /** * Get size monitoring configuration */ getSizeMonitoringConfig() { return this.sizeMonitor.getConfig(); } /** * Update size monitoring configuration */ updateSizeMonitoringConfig(config: any): void { this.sizeMonitor.updateConfig(config); } clearCache(): void { this.cache.clear(); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ezhou89/medical-research-mcp-suite'

If you have feedback or need assistance with the MCP directory API, please join our Discord server