@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
JavaScript
#!/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