UNPKG

@uh-joan/offx-mcp-server

Version:

OFF-X (Target Safety) MCP server for drug safety, adverse event, and target risk analytics.

1,296 lines 80.3 kB
#!/usr/bin/env node /** * OFFX MCP Server * * This server provides a bridge between the Model Context Protocol (MCP) and the OFFX (Target Safety) API. * It supports both MCP server mode (with stdio or SSE transport) and HTTP server mode for flexible integration. * * Environment Variables: * - OFFX_API_TOKEN: Required. API token for OFFX API authentication * - USE_HTTP: Optional. Set to 'true' to run as HTTP server (default: false) * - PORT: Optional. Port number for HTTP server (default: 3000) * - LOG_LEVEL: Optional. Logging level (default: 'info') * - TRANSPORT: Optional. MCP transport type ('stdio' or 'sse', default: 'stdio') * - SSE_PATH: Optional. Path for SSE endpoint when using SSE transport (default: '/mcp') * * # OFFX MCP Server - README * * This server exposes the following OFFX tools via MCP: * * ## Tools * * - offx_search_drugs: Search for drugs by name in the OFFX database * - Input: { drug: string } * * - offx_get_drug_alerts: Retrieve alerts for a drug by drug_id (with optional filters) * - Input: { * drug_id: string, * page?: number, * adverse_event_id?: string, * ref_source_type?: string, * alert_type?: string, * alert_phase?: string, * alert_level_evidence?: string, * alert_severity?: string, * alert_causality?: string, * alert_species?: string, * alert_date_from?: string, * alert_date_to?: string, * order_by_date?: string, * order_by_adv?: string * } * * - offx_get_drugs_by_action: Retrieve drugs by target and action ID * - Input: { target_id: string, action_id: string } * * - offx_get_drugs_by_adve: Retrieve drugs by adverse event ID * - Input: { adverse_event_id: string, page?: number } * * - offx_get_drug_score: Get drug score by drug id (and optionally adverse event id) * - Input: { drug_id: string, adverse_event_id?: string } * * - offx_search_adverse_events: Search adverse events by name in the OFFX database * - Input: { adverse_event: string } * * - offx_get_adverse_events: Get adverse events by drug id * - Input: { drug_id: string } * * - offx_get_adverse_events_by_target: Get adverse events by target id * - Input: { target_id: string } * * - offx_get_drug: Get drug masterview by drug id using the OFFX API. Supports optional filters. * - Input: { * drug_id: string, * page: number, * adverse_event_id?: string, * ref_source_type?: string, * alert_type?: string, * alert_phase?: string, * alert_level_evidence?: string, * alert_severity?: string, * alert_causality?: string, * alert_species?: string, * alert_date_from?: string, * alert_date_to?: string * } * * - offx_search_targets: Search targets by name in the OFFX database * - Input: { target: string } * * - offx_get_target: Get target masterview by target_id and action_id using the OFFX API. Supports optional filters. * - Input: { * target_id: string, * action_id: string, * page: number, * adverse_event_id?: string, * ref_source_type?: string, * alert_type?: string, * alert_phase?: string, * alert_level_evidence?: string, * alert_onoff_target?: string, * alert_severity?: string, * alert_causality?: string, * alert_species?: string, * alert_date_from?: string, * alert_date_to?: string * } * * - offx_get_targets: Get primary or secondary targets for a drug by drug_id, or targets by adverse_event_id. You must provide exactly one of: drug_id or adverse_event_id (not both, not neither). If drug_id is provided and type is not specified, it defaults to "primary". * - Input: { drug_id: string, type?: 'primary' | 'secondary', adverse_event_id?: string } * * ## Usage * * - MCP mode: Communicate via stdio (default) or SSE (experimental) * - HTTP mode: POST to /offx_search_drugs and other endpoints * * ## Example * * { * "tool": "offx_get_drug_alerts", * "arguments": { "drug_id": "140448", "alert_type": "serious" } * } * * See the tool schemas in the code for full details and examples. */ import fetchModule from 'node-fetch'; const fetch = (globalThis.fetch || fetchModule.default || fetchModule); import 'dotenv/config'; import http from 'http'; const logger = { levels: { error: 0, warn: 1, info: 2, debug: 3 }, level: (process.env.LOG_LEVEL || 'info'), formatMessage: (level, message, meta) => { const timestamp = new Date().toISOString(); const metaStr = meta ? ` | ${JSON.stringify(meta)}` : ''; return `[${timestamp}] ${level.toUpperCase()}: ${message}${metaStr}`; }, error: (message, meta) => { if (logger.levels[logger.level] >= logger.levels.error) { // Always use stderr to avoid interfering with MCP protocol process.stderr.write(logger.formatMessage('error', message, meta) + '\n'); } }, warn: (message, meta) => { if (logger.levels[logger.level] >= logger.levels.warn) { // Always use stderr to avoid interfering with MCP protocol process.stderr.write(logger.formatMessage('warn', message, meta) + '\n'); } }, info: (message, meta) => { if (logger.levels[logger.level] >= logger.levels.info) { // In MCP mode, use stderr to avoid interfering with protocol // In HTTP mode, can use stdout if (!USE_HTTP) { process.stderr.write(logger.formatMessage('info', message, meta) + '\n'); } else { process.stdout.write(logger.formatMessage('info', message, meta) + '\n'); } } }, debug: (message, meta) => { if (logger.levels[logger.level] >= logger.levels.debug) { // In MCP mode, use stderr to avoid interfering with protocol // In HTTP mode, can use stdout if (!USE_HTTP) { process.stderr.write(logger.formatMessage('debug', message, meta) + '\n'); } else { process.stdout.write(logger.formatMessage('debug', message, meta) + '\n'); } } } }; // API configuration and environment variables const USE_HTTP = process.env.USE_HTTP === 'true'; const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000; const TRANSPORT = process.env.TRANSPORT || 'stdio'; const SSE_PATH = process.env.SSE_PATH || '/mcp'; const OFFX_API_TOKEN = process.env.OFFX_API_TOKEN || ''; // Validate required environment variables if (!OFFX_API_TOKEN) { logger.error('Missing required environment variable: OFFX_API_TOKEN'); process.exit(1); } // Shared error schema const ERROR_SCHEMA = { type: 'object', properties: { error: { type: 'string', description: 'Error message' }, code: { type: 'number', description: 'HTTP status code' } }, required: ['error', 'code'] }; // Tool definition for OFFX search_drugs const SEARCH_DRUGS_TOOL = { name: 'offx_search_drugs', description: 'Search for drugs by name in the OFFX database. Returns drugs matching the full or partial name.', inputSchema: { type: 'object', properties: { drug: { type: 'string', description: 'Drug name to search for' } } }, responseSchema: { type: 'object', properties: { drugs: { type: 'array', items: { type: 'object', properties: { drug_id: { type: 'string' }, drug_main_name: { type: 'string' }, drug_other_names: { type: 'array', items: { type: 'string' } }, drug_phase: { type: 'string' }, drug_molecule_type: { type: 'string' }, drug_modalities: { type: 'array', items: { type: 'string' } }, chembl_id: { type: 'string' } } } } } }, examples: [ { description: 'Search for drugs by name', usage: '{ "drug": "everolimus" }', response: '{ "drugs": [{ "drug_id": "99402", "drug_main_name": "everolimus" }] }' } ] }; // Tool definition for OFFX get_drugs const GET_DRUGS_TOOL = { name: 'offx_get_drugs', description: 'Get drugs using one of these two options:\n1. Search by target and action: Provide both target_id AND action_id together\n2. Search by adverse event: Provide only adverse_event_id\nDo not mix these options.', inputSchema: { type: 'object', properties: { target_id: { type: 'string', description: 'Target identifier. Must be used together with action_id. Cannot be used with adverse_event_id.' }, action_id: { type: 'string', description: 'Action identifier. Must be used together with target_id. Cannot be used with adverse_event_id.' }, adverse_event_id: { type: 'string', description: 'Adverse event identifier. Use this alone. Cannot be combined with target_id or action_id.' }, page: { type: 'number', description: 'Page number for pagination. Defaults to 1.', default: 1 } } }, responseSchema: { type: 'object', properties: { drugs: { type: 'array', items: { type: 'object', properties: { drug_id: { type: 'string' }, drug_main_name: { type: 'string' }, drug_other_names: { type: 'array', items: { type: 'string' } }, drug_phase: { type: 'string' }, drug_molecule_type: { type: 'string' }, drug_modalities: { type: 'array', items: { type: 'string' } }, chembl_id: { type: 'string' } } } } } }, examples: [ { description: 'Search by target and action', usage: '{ "target_id": "123", "action_id": "456" }', response: '{ "drugs": [{ "drug_id": "140448", "drug_main_name": "semaglutide" }] }' }, { description: 'Search by adverse event', usage: '{ "adverse_event_id": "10001551" }', response: '{ "drugs": [{ "drug_id": "140448", "drug_main_name": "semaglutide" }] }' } ] }; // Tool definition for OFFX get_alerts const GET_ALERTS_TOOL = { name: 'offx_get_alerts', description: 'Get alerts using one of these two options:\n1. Drug alerts: Provide only drug_id\n2. Target alerts: Provide target_id (action_id is optional)\nDo not provide both drug_id and target_id.', inputSchema: { type: 'object', properties: { drug_id: { type: 'string', description: 'Drug identifier. Use this alone for drug alerts. Cannot be used with target_id.' }, target_id: { type: 'string', description: 'Target identifier. Use this for target alerts. Cannot be used with drug_id.' }, action_id: { type: 'string', description: 'Action identifier. Optional. Can only be used with target_id.' }, page: { type: 'number', description: 'Page number for pagination. Defaults to 1.', default: 1 }, adverse_event_id: { type: 'string', description: 'Filter by specific adverse event' }, ref_source_type: { type: 'string', description: 'Filter by reference source types. Use comma-separated numbers:\n9=Congress, 10=Website, 11=Company, 27=Health Org, 24=Database, 22=DailyMed, 23=Agency Brief, 25=Patent, etc.\nExample: "9,10,11"' }, alert_type: { type: 'string', description: 'Filter by alert types. Use comma-separated numbers:\n1=Class Alert, 2=Drug Alert\nExample: "1" for Class Alert, "2" for Drug Alert, "1,2" for both' }, alert_phase: { type: 'string', description: 'Filter by phases. Use comma-separated numbers:\n1=Clinical/Postmarketing, 2=Preclinical, 3=Clinical, 4=Postmarketing, 5=Target Discovery, 6-12=Phase I-IV\nExample: "1,2,3"' }, alert_level_evidence: { type: 'string', description: 'Filter by evidence level. Use comma-separated numbers:\n1=Confirmed, 2=Suspected, 3=Refuted\nExample: "1" or "1,2"' }, alert_onoff_target: { type: 'string', description: 'Filter by on/off target. Use comma-separated numbers:\n1=On-Target, 2=Off-Target, 3=Not Specified\nExample: "1" or "1,2"' }, alert_severity: { type: 'string', description: 'Filter by severity. Use "yes" or "no".' }, alert_causality: { type: 'string', description: 'Filter by causality' }, alert_species: { type: 'string', description: 'Filter by species' }, alert_date_from: { type: 'string', description: 'Start date filter in YYYY-MM-DD format. Example: "2023-01-01"' }, alert_date_to: { type: 'string', description: 'End date filter in YYYY-MM-DD format. Example: "2023-12-31"' }, order_by_date: { type: 'string', description: 'Sort by date. Use "asc" for ascending or "desc" for descending.' }, order_by_adv: { type: 'string', description: 'Sort by adverse event. Use "asc" for ascending or "desc" for descending.' } } }, responseSchema: { type: 'object', properties: { alerts: { type: 'array', items: { type: 'object', properties: { drug_id: { type: 'string' }, target_id: { type: 'string' }, action_id: { type: 'string' }, adverse_event_id: { type: 'string' }, ref_source_type: { type: 'string' }, alert_type: { type: 'string' }, alert_phase: { type: 'string' }, alert_level_evidence: { type: 'string' }, alert_severity: { type: 'string' }, alert_causality: { type: 'string' }, alert_species: { type: 'string' }, alert_date: { type: 'string' } } } } } }, examples: [ { description: 'Get drug alerts', usage: '{ "drug_id": "11204", "page": 1 }', response: '{ "alerts": [{ "drug_id": "11204", "alert_type": "2" }] }' }, { description: 'Get target alerts', usage: '{ "target_id": "158", "action_id": "15", "page": 2, "alert_type": "2" }', response: '{ "alerts": [{ "target_id": "158", "action_id": "15", "alert_type": "2" }] }' } ] }; // Tool definition for OFFX get_score const GET_SCORE_TOOL = { name: 'offx_get_score', description: 'Get safety scores using one of these two options:\n1. Drug score: Provide drug_id (adverse_event_id is optional)\n2. Target/class score: Provide target_id and action_id\nDo not mix these options.', inputSchema: { type: 'object', properties: { drug_id: { type: 'string', description: 'Drug identifier. Use this for drug score. Cannot be used with target_id/action_id.' }, adverse_event_id: { type: 'string', description: 'Optional adverse event identifier when using drug_id.' }, target_id: { type: 'string', description: 'Target identifier. Use this with action_id for target/class score. Cannot be used with drug_id.' }, action_id: { type: 'string', description: 'Action identifier. Required when using target_id. Cannot be used with drug_id.' } } }, responseSchema: { type: 'object', properties: { score: { type: 'object', properties: { score_id: { type: 'string' }, score_value: { type: 'number' }, score_type: { type: 'string' } } } } }, examples: [ { description: 'Get drug score', usage: '{ "drug_id": "140448" }', response: '{ "score": { "score_id": "123", "score_value": 0.85, "score_type": "drug" } }' }, { description: 'Get drug score for specific adverse event', usage: '{ "drug_id": "140448", "adverse_event_id": "10001551" }', response: '{ "score": { "score_id": "456", "score_value": 0.75, "score_type": "drug_ae" } }' }, { description: 'Get target/class score', usage: '{ "target_id": "165", "action_id": "4" }', response: '{ "score": { "score_id": "789", "score_value": 0.92, "score_type": "target" } }' } ] }; // Tool definition for OFFX get_drug const GET_DRUG_TOOL = { name: 'offx_get_drug', description: 'Get a comprehensive drug summary (masterview) with optional filters.', inputSchema: { type: 'object', properties: { drug_id: { type: 'string', description: 'Drug identifier. Required.' }, page: { type: 'number', description: 'Page number for pagination. Defaults to 1.', default: 1 }, adverse_event_id: { type: 'string', description: 'Filter by specific adverse event' }, ref_source_type: { type: 'string', description: 'Filter by reference source types. Use comma-separated numbers:\n9=Congress, 10=Website, 11=Company, 27=Health Org, 24=Database, 22=DailyMed, 23=Agency Brief, 25=Patent, etc.\nExample: "9,10,11"' }, alert_type: { type: 'string', description: 'Filter by alert types. Use comma-separated numbers:\n1=Class Alert, 2=Drug Alert\nExample: "1" for Class Alert, "2" for Drug Alert, "1,2" for both' }, alert_phase: { type: 'string', description: 'Filter by phases. Use comma-separated numbers:\n1=Clinical/Postmarketing, 2=Preclinical, 3=Clinical, 4=Postmarketing, 5=Target Discovery, 6-12=Phase I-IV\nExample: "1,2,3"' }, alert_level_evidence: { type: 'string', description: 'Filter by evidence level. Use comma-separated numbers:\n1=Confirmed, 2=Suspected, 3=Refuted\nExample: "1" or "1,2"' }, alert_severity: { type: 'string', description: 'Filter by severity. Use "yes" or "no".' }, alert_causality: { type: 'string', description: 'Filter by causality' }, alert_species: { type: 'string', description: 'Filter by species' }, alert_date_from: { type: 'string', description: 'Start date filter in YYYY-MM-DD format. Example: "2023-01-01"' }, alert_date_to: { type: 'string', description: 'End date filter in YYYY-MM-DD format. Example: "2023-12-31"' } } }, responseSchema: { type: 'object', properties: { drug: { type: 'object', properties: { drug_id: { type: 'string' }, drug_main_name: { type: 'string' }, drug_other_names: { type: 'array', items: { type: 'string' } }, drug_phase: { type: 'string' }, drug_molecule_type: { type: 'string' }, drug_modalities: { type: 'array', items: { type: 'string' } }, chembl_id: { type: 'string' } } } } }, examples: [ { description: 'Get drug summary', usage: '{ "drug_id": "140448", "page": 1 }', response: '{ "drug": { "drug_id": "140448", "drug_main_name": "semaglutide" } }' }, { description: 'Get drug summary with filters', usage: '{ "drug_id": "140448", "page": 1, "alert_type": "2", "alert_phase": "3,4" }', response: '{ "drug": { "drug_id": "140448", "drug_main_name": "semaglutide" } }' } ] }; // Tool definition for OFFX get_target const GET_TARGET_TOOL = { name: 'offx_get_target', description: 'Get a comprehensive target summary (masterview) with optional filters.', inputSchema: { type: 'object', properties: { target_id: { type: 'string', description: 'Target identifier. Required.' }, action_id: { type: 'string', description: 'Action identifier. Required.' }, page: { type: 'number', description: 'Page number for pagination. Defaults to 1.', default: 1 }, adverse_event_id: { type: 'string', description: 'Filter by specific adverse event' }, ref_source_type: { type: 'string', description: 'Filter by reference source types. Use comma-separated numbers:\n9=Congress, 10=Website, 11=Company, 27=Health Org, 24=Database, 22=DailyMed, 23=Agency Brief, 25=Patent, etc.\nExample: "9,10,11"' }, alert_type: { type: 'string', description: 'Filter by alert types. Use comma-separated numbers:\n1=Class Alert, 2=Drug Alert\nExample: "1" for Class Alert, "2" for Drug Alert, "1,2" for both' }, alert_phase: { type: 'string', description: 'Filter by phases. Use comma-separated numbers:\n1=Clinical/Postmarketing, 2=Preclinical, 3=Clinical, 4=Postmarketing, 5=Target Discovery, 6-12=Phase I-IV\nExample: "1,2,3"' }, alert_level_evidence: { type: 'string', description: 'Filter by evidence level. Use comma-separated numbers:\n1=Confirmed, 2=Suspected, 3=Refuted\nExample: "1" or "1,2"' }, alert_onoff_target: { type: 'string', description: 'Filter by on/off target. Use comma-separated numbers:\n1=On-Target, 2=Off-Target, 3=Not Specified\nExample: "1" or "1,2"' }, alert_severity: { type: 'string', description: 'Filter by severity. Use "yes" or "no".' }, alert_causality: { type: 'string', description: 'Filter by causality' }, alert_species: { type: 'string', description: 'Filter by species' }, alert_date_from: { type: 'string', description: 'Start date filter in YYYY-MM-DD format. Example: "2023-01-01"' }, alert_date_to: { type: 'string', description: 'End date filter in YYYY-MM-DD format. Example: "2023-12-31"' } } }, responseSchema: { type: 'object', properties: { target: { type: 'object', properties: { target_id: { type: 'string' }, target_name: { type: 'string' }, action_id: { type: 'string' }, action_type: { type: 'string' } } } } }, examples: [ { description: 'Get target summary', usage: '{ "target_id": "165", "action_id": "4", "page": 1 }', response: '{ "target": { "target_id": "165", "target_name": "GLP-1 receptor", "action_id": "4", "action_type": "agonist" } }' }, { description: 'Get target summary with filters', usage: '{ "target_id": "165", "action_id": "4", "page": 1, "alert_type": "1", "alert_phase": "3,4" }', response: '{ "target": { "target_id": "165", "target_name": "GLP-1 receptor", "action_id": "4", "action_type": "agonist" } }' } ] }; // Tool definition for OFFX get_targets const GET_TARGETS_TOOL = { name: 'offx_get_targets', description: 'Get primary or secondary targets for a drug by drug_id, or targets by adverse_event_id. You must provide exactly one of: drug_id or adverse_event_id (not both, not neither). If drug_id is provided and type is not specified, it defaults to "primary".', inputSchema: { type: 'object', properties: { drug_id: { type: 'string', description: 'Drug identifier. Required when searching drug targets. Must be used with type parameter.' }, type: { type: 'string', description: 'Target type. Required when using drug_id. Values: "primary" or "secondary". Defaults to "primary" if not specified.', enum: ['primary', 'secondary'] }, adverse_event_id: { type: 'string', description: 'Adverse event identifier. Required when searching by adverse event. Cannot be used with drug_id.' } }, oneOf: [ { required: ['drug_id'] }, { required: ['adverse_event_id'] } ] }, responseSchema: { type: 'object', properties: { primary_targets: { type: 'array', items: { type: 'object', properties: { target_id: { type: 'string' }, target_name: { type: 'string' }, action_id: { type: 'string' }, action_type: { type: 'string' } } } }, secondary_targets: { type: 'array', items: { type: 'object', properties: { target_id: { type: 'string' }, target_name: { type: 'string' }, action_id: { type: 'string' }, action_type: { type: 'string' } } } }, targets: { type: 'array', items: { type: 'object', properties: { target_id: { type: 'string' }, target_name: { type: 'string' }, action_id: { type: 'string' }, action_type: { type: 'string' } } } } } }, examples: [ { description: 'Get primary targets for a drug', usage: '{ "drug_id": "11204" }', response: '{ "primary_targets": [{ "target_id": "158", "target_name": "GLP-1 receptor" }] }' }, { description: 'Get secondary targets for a drug', usage: '{ "drug_id": "11204", "type": "secondary" }', response: '{ "secondary_targets": [{ "target_id": "159", "target_name": "GLP-2 receptor" }] }' }, { description: 'Get targets by adverse event', usage: '{ "adverse_event_id": "10001551" }', response: '{ "targets": [{ "target_id": "158", "target_name": "GLP-1 receptor" }] }' } ] }; // Tool definition for OFFX get_adverse_events const GET_ADVERSE_EVENTS_TOOL = { name: 'offx_get_adverse_events', description: 'Get adverse events using one of these two options:\n1. Drug adverse events: Provide drug_id\n2. Target adverse events: Provide target_id\nDo not provide both.', inputSchema: { type: 'object', properties: { drug_id: { type: 'string', description: 'Drug identifier. Use this for drug adverse events. Cannot be used with target_id.' }, target_id: { type: 'string', description: 'Target identifier. Use this for target adverse events. Cannot be used with drug_id.' } } }, responseSchema: { type: 'object', properties: { adverse_events: { type: 'array', items: { type: 'object', properties: { adverse_event_id: { type: 'string' }, adverse_event_name: { type: 'string' }, adverse_event_type: { type: 'string' } } } } } }, examples: [ { description: 'Get drug adverse events', usage: '{ "drug_id": "140448" }', response: '{ "adverse_events": [{ "adverse_event_id": "10001551", "adverse_event_name": "nausea" }] }' }, { description: 'Get target adverse events', usage: '{ "target_id": "165" }', response: '{ "adverse_events": [{ "adverse_event_id": "10001552", "adverse_event_name": "vomiting" }] }' } ] }; // Tool definition for OFFX search_adverse_events const SEARCH_ADVERSE_EVENTS_TOOL = { name: 'offx_search_adverse_events', description: 'Search for adverse events by name in the OFFX database. Returns adverse events matching the full or partial name.', inputSchema: { type: 'object', properties: { adverse_event: { type: 'string', description: 'Adverse event name to search for (minimum 3 characters)' } }, required: ['adverse_event'] }, responseSchema: { type: 'object', properties: { adverse_events: { type: 'array', items: { type: 'object', properties: { adverse_event_id: { type: 'string' }, adverse_event: { type: 'string' }, ref_source_type: { type: 'string' }, alert_type: { type: 'string' }, alert_phase: { type: 'string' } } } } } }, examples: [ { description: 'Search for adverse events by name', usage: '{ "adverse_event": "headache" }', response: '{ "adverse_events": [{ "adverse_event_id": "123", "adverse_event": "headache", "ref_source_type": "clinical_trial", "alert_type": "2", "alert_phase": "3" }] }' } ] }; // Tool definition for OFFX search_targets const SEARCH_TARGETS_TOOL = { name: 'offx_search_targets', description: 'Search for targets by name in the OFFX database. Returns targets matching the full or partial name.', inputSchema: { type: 'object', properties: { target: { type: 'string', description: 'Target name to search for' } } }, responseSchema: { type: 'object', properties: { targets: { type: 'array', items: { type: 'object', properties: { target_id: { type: 'string' }, target_name: { type: 'string' }, target_type: { type: 'string' } } } } } }, examples: [ { description: 'Search for targets by name', usage: '{ "target": "GLP-1" }', response: '{ "targets": [{ "target_id": "165", "target_name": "GLP-1 receptor" }] }' } ] }; async function searchDrugsByName({ drug }) { if (!drug) { throw new Error('drug is required'); } const query = new URLSearchParams({ drug, token: OFFX_API_TOKEN }); const url = `https://api.targetsafety.info/api/drug/search/param?${query.toString()}`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Request failed with status ${response.status}: ${errorText}`); } return await response.json(); } async function getDrugs(args) { const { target_id, action_id, adverse_event_id, page } = args; const hasTarget = !!target_id; const hasAction = !!action_id; const hasAdve = !!adverse_event_id; if ((hasTarget && hasAction && !hasAdve) || (!hasTarget && !hasAction && hasAdve)) { // valid } else { throw new Error('You must provide either both target_id and action_id, or only adverse_event_id'); } const pageNum = page ?? 1; let query; let url; if (hasTarget && hasAction) { query = new URLSearchParams({ target_id: String(target_id), action_id: String(action_id), page: String(pageNum), token: OFFX_API_TOKEN }); url = `https://api.targetsafety.info/api/drug/search/param?${query.toString()}`; } else if (hasAdve) { query = new URLSearchParams({ adverse_event_id: String(adverse_event_id), page: String(pageNum), token: OFFX_API_TOKEN }); url = `https://api.targetsafety.info/api/drug/search/param?${query.toString()}`; } else { throw new Error('You must provide either both target_id and action_id, or only adverse_event_id'); } const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Request failed with status ${response.status}: ${errorText}`); } return await response.json(); } // Validation functions function validateCommaSeparatedNumbers(value, field) { if (value === undefined) return; const str = String(value); if (!/^\d+(,\d+)*$/.test(str)) { throw new Error(`${field} must be a number or comma-separated numbers (e.g., "1" or "1,2,3")`); } } function validateNumber(value, field) { if (value === undefined) return; const num = Number(value); if (isNaN(num)) { throw new Error(`${field} must be a valid number`); } } function validateStringEnum(value, field, allowed) { if (value === undefined || value === null || value === '') return; if (typeof value !== 'string' || !allowed.includes(value)) { throw new Error(`${field} must be one of: ${allowed.join(', ')}`); } } async function getAlerts(params) { // Validate numeric filter fields validateCommaSeparatedNumbers(params.drug_id, 'drug_id'); validateCommaSeparatedNumbers(params.target_id, 'target_id'); validateCommaSeparatedNumbers(params.action_id, 'action_id'); validateNumber(params.page, 'page'); validateCommaSeparatedNumbers(params.adverse_event_id, 'adverse_event_id'); validateCommaSeparatedNumbers(params.ref_source_type, 'ref_source_type'); validateCommaSeparatedNumbers(params.alert_type, 'alert_type'); validateCommaSeparatedNumbers(params.alert_phase, 'alert_phase'); validateCommaSeparatedNumbers(params.alert_level_evidence, 'alert_level_evidence'); validateCommaSeparatedNumbers(params.alert_onoff_target, 'alert_onoff_target'); // Validate string enums validateStringEnum(params.alert_severity, 'alert_severity', ['yes', 'no']); validateStringEnum(params.order_by_date, 'order_by_date', ['asc', 'desc']); validateStringEnum(params.order_by_adv, 'order_by_adv', ['asc', 'desc']); const { drug_id, target_id, page } = params; const pageNum = page ?? 1; if ((drug_id && target_id) || (!drug_id && !target_id)) { throw new Error('You must provide exactly one of: drug_id or target_id'); } if (drug_id) { if (!drug_id) throw new Error('drug_id is required'); } else if (target_id) { if (!target_id) throw new Error('target_id is required'); } const query = new URLSearchParams({ page: String(pageNum), token: OFFX_API_TOKEN }); if (drug_id) query.append('drug_id', String(drug_id)); if (target_id) query.append('target_id', String(target_id)); [ 'action_id', 'adverse_event_id', 'ref_source_type', 'alert_type', 'alert_phase', 'alert_level_evidence', 'alert_onoff_target', 'alert_severity', 'alert_causality', 'alert_species', 'alert_date_from', 'alert_date_to', 'order_by_date', 'order_by_adv' ].forEach(key => { const value = params[key]; if (value !== undefined && value !== null && value !== '') { query.append(key, String(value)); } }); const url = drug_id ? `https://api.targetsafety.info/api/drug/alerts/param?${query.toString()}` : `https://api.targetsafety.info/api/target/alerts/param?${query.toString()}`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Request failed with status ${response.status}: ${errorText}`); } return await response.json(); } async function getDrugScore({ drug_id, adverse_event_id }) { if (!drug_id) { throw new Error('drug_id is required'); } const query = new URLSearchParams({ drug_id: String(drug_id), token: OFFX_API_TOKEN }); if (adverse_event_id) { query.append('adverse_event_id', String(adverse_event_id)); } const url = `https://api.targetsafety.info/api/score/drug/search/param?${query.toString()}`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Request failed with status ${response.status}: ${errorText}`); } return await response.json(); } async function searchAdverseEvents({ adverse_event }) { if (!adverse_event) { throw new Error('adverse_event is required'); } const query = new URLSearchParams({ adverse_event, token: OFFX_API_TOKEN }); const url = `https://api.targetsafety.info/api/adverseevent/search/param?${query.toString()}`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Request failed with status ${response.status}: ${errorText}`); } return await response.json(); } async function getAdverseEvents(args) { const { drug_id, target_id } = args; if ((drug_id && target_id) || (!drug_id && !target_id)) { throw new Error('You must provide exactly one of: drug_id or target_id'); } let query; let url; if (drug_id) { query = new URLSearchParams({ drug_id: String(drug_id), token: OFFX_API_TOKEN }); url = `https://api.targetsafety.info/api/adverseevent/search/param?${query.toString()}`; } else if (target_id) { query = new URLSearchParams({ target_id: String(target_id), token: OFFX_API_TOKEN }); url = `https://api.targetsafety.info/api/adverseevent/search/param?${query.toString()}`; } else { throw new Error('You must provide exactly one of: drug_id or target_id'); } const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Request failed with status ${response.status}: ${errorText}`); } return await response.json(); } async function getTargetScore({ target_id, action_id, adverse_event_id }) { if (!target_id) { throw new Error('target_id is required'); } if (!action_id) { throw new Error('action_id is required'); } const query = new URLSearchParams({ target_id: String(target_id), action_id: String(action_id), token: OFFX_API_TOKEN }); if (adverse_event_id) { query.append('adverse_event_id', String(adverse_event_id)); } const url = `https://api.targetsafety.info/api/score/target/search/param?${query.toString()}`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Request failed with status ${response.status}: ${errorText}`); } return await response.json(); } async function getScore(args) { const { drug_id, adverse_event_id, target_id, action_id } = args; const hasDrug = !!drug_id; const hasTarget = !!target_id; const hasAction = !!action_id; if ((hasDrug && !hasTarget && !hasAction) || (!hasDrug && hasTarget && hasAction)) { // valid } else { throw new Error('You must provide either drug_id (alone), or both target_id and action_id (together), but not neither, not all, and not just one of target_id/action_id'); } if (hasDrug) { return await getDrugScore({ drug_id, adverse_event_id }); } else if (hasTarget && hasAction) { return await getTargetScore({ target_id, action_id, adverse_event_id }); } else { throw new Error('You must provide either drug_id (alone), or both target_id and action_id (together)'); } } async function getDrugMasterview(params) { validateCommaSeparatedNumbers(params.drug_id, 'drug_id'); validateNumber(params.page, 'page'); validateCommaSeparatedNumbers(params.adverse_event_id, 'adverse_event_id'); validateCommaSeparatedNumbers(params.ref_source_type, 'ref_source_type'); validateCommaSeparatedNumbers(params.alert_type, 'alert_type'); validateCommaSeparatedNumbers(params.alert_phase, 'alert_phase'); validateCommaSeparatedNumbers(params.alert_level_evidence, 'alert_level_evidence'); validateStringEnum(params.alert_severity, 'alert_severity', ['yes', 'no']); const { drug_id, page } = params; if (!drug_id) { throw new Error('drug_id is required'); } if (!page) { throw new Error('page is required'); } const query = new URLSearchParams({ drug_id: String(drug_id), page: String(page), token: OFFX_API_TOKEN }); [ 'adverse_event_id', 'ref_source_type', 'alert_type', 'alert_phase', 'alert_level_evidence', 'alert_severity', 'alert_causality', 'alert_species', 'alert_date_from', 'alert_date_to' ].forEach(key => { const value = params[key]; if (value !== undefined && value !== null && value !== '') { query.append(key, String(value)); } }); const url = `https://api.targetsafety.info/api/drug/masterview/param?${query.toString()}`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Request failed with status ${response.status}: ${errorText}`); } return await response.json(); } async function searchTargets({ target }) { if (!target) { throw new Error('target is required'); } const query = new URLSearchParams({ target, token: OFFX_API_TOKEN }); const url = `https://api.targetsafety.info/api/target/search/param?${query.toString()}`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Request failed with status ${response.status}: ${errorText}`); } return await response.json(); } async function getTargetMasterview(params) { validateCommaSeparatedNumbers(params.target_id, 'target_id'); validateCommaSeparatedNumbers(params.action_id, 'action_id'); validateNumber(params.page, 'page'); validateCommaSeparatedNumbers(params.adverse_event_id, 'adverse_event_id'); validateCommaSeparatedNumbers(params.ref_source_type, 'ref_source_type'); validateCommaSeparatedNumbers(params.alert_type, 'alert_type'); validateCommaSeparatedNumbers(params.alert_phase, 'alert_phase'); validateCommaSeparatedNumbers(params.alert_level_evidence, 'alert_level_evidence'); validateCommaSeparatedNumbers(params.alert_onoff_target, 'alert_onoff_target'); validateStringEnum(params.alert_severity, 'alert_severity', ['yes', 'no']); const { target_id, action_id, page } = params; if (!target_id) { throw new Error('target_id is required'); } if (!action_id) { throw new Error('action_id is required'); } const pageNum = page ?? 1; const query = new URLSearchParams({ target_id: String(target_id), action_id: String(action_id), page: String(pageNum), token: OFFX_API_TOKEN }); [ 'adverse_event_id', 'ref_source_type', 'alert_type', 'alert_phase', 'alert_level_evidence', 'alert_onoff_target', 'alert_severity', 'alert_causality', 'alert_species', 'alert_date_from', 'alert_date_to' ].forEach(key => { const value = params[key]; if (value !== undefined && value !== null && value !== '') { query.append(key, String(value)); } }); const url = `https://api.targetsafety.info/api/target/masterview/param?${query.toString()}`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Request failed with status ${response.status}: ${errorText}`); } return await response.json(); } async function getPrimaryTargets({ drug_id }) { if (!drug_id) throw new Error('drug_id is required'); const query = new URLSearchParams({ drug_id: String(drug_id), token: OFFX_API_TOKEN }); const url = `https://api.targetsafety.info/api/target/primary/search/param?${query.toString()}`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Request failed with status ${response.status}: ${errorText}`); } return await response.json(); } async function getSecondaryTargets({ drug_id }) { if (!drug_id) throw new Error('drug_id is required'); const query = new URLSearchParams({ drug_id: String(drug_id), token: OFFX_API_TOKEN }); const url = `https://api.targetsafety.info/api/target/secondary/search/param?${query.toString()}`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Request failed with status ${response.status}: ${errorText}`); } return await