UNPKG

@uh-joan/cortellis-mcp-server

Version:

MCP server for Cortellis drug database search and ontology exploration

1,127 lines 97.8 kB
#!/usr/bin/env node /** * Cortellis MCP Server * * This server provides a bridge between the Model Context Protocol (MCP) and the Cortellis API. * It supports both MCP server mode (with stdio or SSE transport) and HTTP server mode for flexible integration. * * Environment Variables: * - CORTELLIS_USERNAME: Required. Username for Cortellis API authentication * - CORTELLIS_PASSWORD: Required. Password for Cortellis 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') */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js"; import fetch from 'node-fetch'; import express from "express"; import 'dotenv/config'; import { createHash } from 'crypto'; class Logger { constructor(options) { this.levelPriority = { 'error': 0, 'warn': 1, 'info': 2, 'debug': 3 }; this.level = options.level; this.prefix = options.prefix || ''; } shouldLog(messageLevel) { return this.levelPriority[messageLevel] <= this.levelPriority[this.level]; } formatLogMessage(level, message, meta) { const timestamp = new Date().toISOString(); const metaStr = meta ? ` ${JSON.stringify(meta)}` : ''; // Write to stderr to avoid mixing with JSON output return `${timestamp} [${level.toUpperCase()}] ${this.prefix}${message}${metaStr}\n`; } debug(message, meta) { if (this.shouldLog('debug')) { process.stderr.write(this.formatLogMessage('debug', message, meta)); } } info(message, meta) { if (this.shouldLog('info')) { process.stderr.write(this.formatLogMessage('info', message, meta)); } } warn(message, meta) { if (this.shouldLog('warn')) { process.stderr.write(this.formatLogMessage('warn', message, meta)); } } error(message, meta) { if (this.shouldLog('error')) { process.stderr.write(this.formatLogMessage('error', message, meta)); } } getLevel() { return this.level; } setLevel(level) { this.level = level; } } // Create logger instance const logger = new Logger({ level: process.env.LOG_LEVEL || 'info' }); // API configuration and environment variables const USERNAME = process.env.CORTELLIS_USERNAME || ''; const PASSWORD = process.env.CORTELLIS_PASSWORD || ''; 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'; // Validate required environment variables if (!USERNAME || !PASSWORD) { process.stderr.write(`${new Date().toISOString()} [ERROR] Missing required environment variables: ${JSON.stringify({ username: !USERNAME ? "missing" : "present", password: !PASSWORD ? "missing" : "present" })}\n`); process.exit(1); } // Validate transport configuration if (TRANSPORT !== 'stdio') { logger.warn("SSE transport is temporarily disabled. Defaulting to stdio transport.", { requested_transport: TRANSPORT }); } // Tool definitions const SEARCH_DRUGS_TOOL = { name: "ci_search_drugs", description: "Search for drugs in the Cortellis database. If the amount of drugs returned do not match with the totalResults, ALWAYS use the offset parameter to get the next page(s) of results.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Raw search query (if you want to use the full Cortellis query syntax directly)" }, company: { type: "string", description: "Company ID for the developing company (e.g., 18614)" }, indication: { type: "string", description: "Indication ID (numeric ID only, e.g., 238 for Obesity). Use ci_explore_ontology to find the correct ID." }, action: { type: "string", description: "Target specific action (e.g. glucagon)" }, phase: { type: "string", description: "Overall Highest development status of drug. For both non-historic and historic queries, only a single phase value is supported; do not use OR/AND. Agents should run separate queries for each phase.", enum: [ "S", // Suspended "DR", // Discovery/Preclinical "CU", // Clinical unknown "C1", // Phase 1 "C2", // Phase 2 "C3", // Phase 3 "PR", // Pre-registration "R", // Registered "L", // Launched "OL", // Outlicensed "NDR", // No Development Reported "DX", // Discontinued "W" // Withdrawn ], enumDescriptions: { "S": "Suspended - Development temporarily halted", "DR": "Discovery/Preclinical - Early stage research", "CU": "Clinical unknown - Clinical stage not specified", "C1": "Phase 1 - Initial human safety trials", "C2": "Phase 2 - Small scale efficacy trials", "C3": "Phase 3 - Large scale efficacy trials", "PR": "Pre-registration - Submitted for approval", "R": "Registered - Approved but not yet launched", "L": "Launched - Available in market", "OL": "Outlicensed - Rights transferred to another company", "NDR": "No Development Reported - No recent updates", "DX": "Discontinued - Development stopped", "W": "Withdrawn - Removed from market" }, examples: [ "L", "C1", "C2", "C3" ], format: "Only a single phase value is supported. Do not use OR/AND. Run separate queries for each phase." }, phase_terminated: { type: "string", description: "Last phase before No Dev Reported or Discontinued statuses" }, technology: { type: "string", description: "Technologies used in drug development (e.g. small molecule, biologic)" }, drug_name: { type: "string", description: "Name of the drug (e.g. semaglutide)" }, country: { type: "string", description: "Country ID (e.g., US)" }, offset: { type: "number", description: "Starting position in the results (default: 0)" }, hits: { type: "number", description: "Number of results per page (default: 100)" }, company_size: { type: "string", description: "The size of a company based on market capitalization in billions USD", format: "'<X' for less than $XB, 'X' for greater than $XB", examples: ["<2", "2"], notes: "Values are in billions USD" }, developmentStatusDate: { type: "string", description: "Date of change in status (only possible within LINKED queries). Use RANGE(>=YYYY-MM-DD;<=YYYY-MM-DD) for ranges.", examples: ["RANGE(>=2023-01-01;<=2023-12-31)"] }, historic: { type: "boolean", description: "Set to true to search using the historic development status fields. This is required for questions about the status of a drug at a specific point in the past (e.g., 'What drugs were in phase 3 in 2019?'). If you want to know the status as it was at a particular date or within a date range, always set historic: true and use the developmentStatusDate parameter.", examples: [true, false] }, } }, examples: [ { description: "Search for Launched drugs in the US", usage: `{ "phase": "L", "country": "US" }` }, { description: "Search for Launched drugs in the US with a page size of 10", usage: `{ "phase": "L", "country": "US", "hits": 10 }` }, { description: "Search for drugs with a status change in 2023", usage: `{ "developmentStatusDate": "RANGE(>=2023-01-01;<=2023-12-31)" }` }, { description: "Search for historic launched drugs in the US for Obesity", usage: `{ "phase": "L", "country": "US", "indication": "238", "historic": true }` }, { description: "Search for drugs in Phase 3 OR Pre-registration", usage: `{ "phase": "C3 OR PR" }` }, { description: "Find drugs that were in phase 3 for Parkinson's disease in 2019 (historic snapshot)", usage: `{ "indication": "255", "phase": "C3", "developmentStatusDate": "RANGE(>=2019-01-01;<=2019-12-31)", "historic": true }` } ] }; const EXPLORE_ONTOLOGY_TOOL = { name: "ci_explore_ontology", description: "Search for any particular taxonomy term or its associated synonyms within the taxonomy tree.", inputSchema: { type: "object", properties: { term: { type: "string", description: "Search term or synonym to look up in the taxonomy tree.", examples: ["GLP-1", "obesity", "diabetes", "semaglutide"] }, category: { type: "string", description: "Taxonomy category. Supported values: action, company, indication, technology, target, condition, drug name.", enum: [ "action", "company", "indication", "technology", "target", "condition", "drug name" ], examples: ["action", "indication", "drug name"] } }, required: ["term", "category"] }, examples: [ { description: "Search for GLP-1 related actions", usage: `{ "category": "action", "term": "GLP-1" }` }, { description: "Search for semaglutide as a drug name", usage: `{ "category": "drug name", "term": "semaglutide" }` } ] }; const GET_DRUG_TOOL = { name: "ci_get_drug", description: "Return drug information from Cortellis API. Can return the full drug record, SWOT analysis, or financial data based on the category parameter.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Numeric Drug Identifier (e.g. '101964' for tirzepatide, not the drug name)", examples: ["101964"] }, category: { type: "string", description: "Type of drug information to retrieve", enum: ["report", "swot", "financial"], default: "report", enumDescriptions: { "report": "Full drug record with all available fields", "swot": "SWOT analysis for the drug", "financial": "Financial data and forecasts" } } }, required: ["id"] }, examples: [ { description: "Get full drug report", usage: `{ "id": "101964" }` }, { description: "Get SWOT analysis", usage: `{ "id": "101964", "category": "swot" }` }, { description: "Get financial data", usage: `{ "id": "101964", "category": "financial" }` } ] }; const GET_COMPANY_TOOL = { name: "ci_get_company", description: "Return the entire company record with all available fields for a given identifier from Cortellis API", inputSchema: { type: "object", properties: { id: { type: "string", description: "Numeric Company Identifier (not the company name)", examples: ["12345"] } }, required: ["id"] } }; const SEARCH_COMPANIES_TOOL = { name: "ci_search_companies", description: "Search for companies in the Cortellis database. If the amount of companies returned do not match with the totalResults, ALWAYS use the offset parameter to get the next page(s) of results.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Raw search query (if you want to use the full Cortellis query syntax directly)" }, company_name: { type: "string", description: "Company name to search for (e.g. pfizer)" }, hq_country: { type: "string", description: "Company headquarters country (e.g. US)" }, deals_count: { type: "string", description: "Count for all distinct deals where the company is a principal or partner. Format: '<20' for less than 20 deals, '20' for greater than 20 deals (default behavior)" }, indications: { type: "string", description: "Top 10 indication terms from drugs and patents where company is main assignee (e.g. asthma)" }, actions: { type: "string", description: "Top 10 target-based action terms from drugs and patents where company is main assignee (e.g. cyclooxygenase)" }, technologies: { type: "string", description: "Top 10 technologies terms from drugs and patents where company is main assignee (e.g. Antibiotic)" }, company_size: { type: "string", description: "The size of a company based on the market capitalization in billions USD. Format: '<2' for less than $2B, '2' for greater than $2B (default behavior)" }, status: { type: "string", description: "Highest status of the associated drug linked to the company (e.g. launched)" }, offset: { type: "number", description: "Starting position in the results (default: 0)" }, hits: { type: "number", description: "Number of results per page (default: 100)" } } } }; const SEARCH_DEALS_TOOL = { name: "ci_search_deals", description: "Search for deals in the Cortellis database. Supports all deal search parameters, including drug, company, indication, phase, value, and date filters.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Raw search query (if you want to use the full Cortellis query syntax directly)" }, dealDrugNamesAll: { type: "string", description: "Main name of drug including synonyms associated with the deal" }, indications: { type: "string", description: "Indications associated with the deal" }, dealDrugCompanyPartnerIndications: { type: "string", description: "The indication and the partner company linked to a drug associated with the deal" }, dealPhaseHighestStart: { type: "string", description: "Highest dev. status of the drug at the deal start" }, dealPhaseHighestNow: { type: "string", description: "Current highest dev. status of the drug" }, dealStatus: { type: "string", description: "Status of the deal" }, dealSummary: { type: "string", description: "Summary of the deal" }, dealTitleSummary: { type: "string", description: "Title or summary of the deal" }, technologies: { type: "string", description: "Technology linked to the drug" }, dealTitle: { type: "string", description: "Title of the deal" }, dealType: { type: "string", description: "Type of deal" }, actionsPrimary: { type: "string", description: "Primary mechanism of action associated with the deal" }, dealDrugActionsPrimary: { type: "string", description: "The primary mechanism of action of a drug associated with the deal" }, dealCompanyPrincipal: { type: "string", description: "Principal company (Seller/Licensor)" }, dealCompanyPartner: { type: "string", description: "Partner company (Buyer/Licensee)" }, dealCompanyPrincipalHq: { type: "string", description: "Location of the HQ of the principal company" }, dealTerritoriesIncluded: { type: "string", description: "The deal applies in the included countries" }, dealTerritoriesExcluded: { type: "string", description: "The deal doesn't apply in the excluded countries" }, dealDateStart: { type: "string", description: "Start date of the deal. To specify a range, use: RANGE(>YYYY-MM-DD;<YYYY-MM-DD). For example, to get deals in the last month: RANGE(>2025-04-08;<2025-05-08)", examples: ["2025-04-08", "RANGE(>2025-04-08;<2025-05-08)"] }, dealDateEnd: { type: "string", description: "End date of the deal" }, dealDateEventMostRecent: { type: "string", description: "Date of the latest timeline event" }, dealValuePaidToPartnerMaxNumber: { type: "string", description: "Maximal paid payment amount to partner company in M USD considering the accuracy range" }, dealTotalProjectedCurrentAmount: { type: "string", description: "Total current projection of the agreement in US dollars million" }, dealValuePaidToPartnerMinNumber: { type: "string", description: "Minimal paid payment amount to partner company in M USD considering the accuracy range" }, dealTotalPaidAmount: { type: "string", description: "Total payment value of the agreement realized in US dollars million" }, dealValuePaidToPrincipalMaxDisclosureStatus: { type: "string", description: "Whether the paid payment of the principal company is either 'Payment Unspecified', 'Unknown', or 'Known'" }, dealValuePaidToPrincipalMaxNumber: { type: "string", description: "Maximal paid amount to principal company in M USD considering the accuracy range" }, dealValuePaidToPrincipalMinNumber: { type: "string", description: "Minimal paid amount to principal company in M USD considering the accuracy range" }, dealValueProjectedToPartnerMaxNumber: { type: "string", description: "Maximal projected current amount to partner company in M USD considering the accuracy range" }, dealValueProjectedToPartnerMinNumber: { type: "string", description: "Minimal projected current amount to partner company in M USD considering the accuracy range" }, dealValueProjectedToPrincipalMaxDisclosureStatus: { type: "string", description: "Whether the projected current payment of the principal company is either 'Payment Unspecified', 'Unknown', or 'Known'" }, dealValueProjectedToPrincipalMaxNumber: { type: "string", description: "Maximal projected current amount to principal company in M USD considering the accuracy range" }, dealValueProjectedToPrincipalMinNumber: { type: "string", description: "Minimal projected current amount to principal company in M USD considering the accuracy range" }, sortBy: { type: "string", description: "Sort order for results. Use '+field' for ascending or '-field' for descending. Supported fields: dealDateStart, dealDateEnd, dealDateEventMostRecent, dealTotalPaidSortBy, dealTotalProjectedCurrentSortBy, dealValuePaidToPrincipalMaxSortBy, dealValueProjectedToPrincipalMaxSortBy. Example: '+dealDateStart' for oldest first, '-dealDateStart' for newest first.", examples: [ "+dealDateStart", "-dealDateStart", "+dealDateEnd", "-dealDateEnd", "+dealDateEventMostRecent", "-dealDateEventMostRecent", "+dealTotalPaidSortBy", "-dealTotalPaidSortBy", "+dealTotalProjectedCurrentSortBy", "-dealTotalProjectedCurrentSortBy", "+dealValuePaidToPrincipalMaxSortBy", "-dealValuePaidToPrincipalMaxSortBy", "+dealValueProjectedToPrincipalMaxSortBy", "-dealValueProjectedToPrincipalMaxSortBy" ] }, offset: { type: "number", description: "Starting position in the results (default: 0)" } } }, examples: [ { description: "Search for completed deals involving melanoma", usage: `{ "dealStatus": "Completed", "indications": "Melanoma" }` }, { description: "Find deals in the last month targeting GLP-1", usage: `{ "dealDrugActionsPrimary": "Glucagon-like peptide 1 receptor agonist", "dealDateStart": "RANGE(>2025-04-08;<2025-05-08)" }` }, { description: "Get the last 10 deals for Novo Nordisk, sorted by most recent end date", usage: `{ "dealCompanyPrincipal": "Novo Nordisk", "sortBy": "-dealDateEnd", "offset": 0 }` }, { description: "Get the last 10 deals for Novo Nordisk, sorted by most recent event date", usage: `{ "dealCompanyPrincipal": "Novo Nordisk", "sortBy": "-dealDateEventMostRecent", "offset": 0 }` }, { description: "Get the top 10 deals for Novo Nordisk by total paid amount", usage: `{ "dealCompanyPrincipal": "Novo Nordisk", "sortBy": "-dealTotalPaidSortBy", "offset": 0 }` }, { description: "Get the top 10 deals for Novo Nordisk by total projected current amount", usage: `{ "dealCompanyPrincipal": "Novo Nordisk", "sortBy": "-dealTotalProjectedCurrentSortBy", "offset": 0 }` }, { description: "Get the top 10 deals for Novo Nordisk by value paid to principal (max)", usage: `{ "dealCompanyPrincipal": "Novo Nordisk", "sortBy": "-dealValuePaidToPrincipalMaxSortBy", "offset": 0 }` }, { description: "Get the top 10 deals for Novo Nordisk by value projected to principal (max)", usage: `{ "dealCompanyPrincipal": "Novo Nordisk", "sortBy": "-dealValueProjectedToPrincipalMaxSortBy", "offset": 0 }` } ] }; const SEARCH_CLINICAL_TRIALS_TOOL = { name: "ci_search_trials", description: "Search for clinical trials in the Cortellis database with support for both structured parameters and direct query strings.", inputSchema: { type: "object", properties: { query: { type: "string", description: "Raw search query for direct searching" }, offset: { type: "number", description: "Number of records to skip (default: 0)" }, hits: { type: "number", description: "Number of results per page (default: 20, max: 300)" }, sortBy: { type: "string", description: "Sort field", enum: [ "trialPhase", "trialRecruitmentStatus", "trialPatientCountEnrollment", "trialDateStart", "trialDateEnd", "trialDateChangeLast" ] }, trialTitleOfficial: { type: "string", description: "The official documented title of the clinical trial, as registered with the source registry" }, trialIdentifiers: { type: "string", description: "Identifiers assigned to the trial, such as UTN, NCT number, NIH grant number, etc. Examples: NCT00003140, CTI_umbrella, CTI_basket" }, indications: { type: "string", description: "Medical condition being treated" }, technology: { type: "string", description: "Technology involved" }, category: { type: "string", description: "Trial category" }, trialPhase: { type: "string", description: "Phase of clinical trial. Supports both code format (e.g., 'C12' for Phase 1/2) and descriptive format (e.g., 'Phase 1/Phase 2 Clinical')", examples: ["C12", "Phase 1/Phase 2 Clinical"] }, trialRecruitmentStatus: { type: "string", description: "Trial recruitment status" }, trialCompaniesSponsor: { type: "string", description: "Organisation sponsoring the trial (supports both name and ID format, e.g., 'Novartis' or '23137')" }, trialCompaniesCollaborator: { type: "string", description: "Organisations collaborating on the trial (supports both name and ID format, e.g., 'Genentech' or '19453')" }, trialFunderType: { type: "string", description: "The type of organisation conducting the trial. Supports both code format (e.g., 'GOV') and descriptive format (e.g., 'Government')", examples: ["GOV", "Government"] }, trialPatientCountEnrollment: { type: "string", description: "Planned or actual number of patients involved in trial. Use exact number or RANGE for comparison.", examples: ["5187", "RANGE(>1000)"] }, trialEndpointsAchieved: { type: "string", description: "Trial outcomes achieved (Y/N)", examples: ["Yes", "No"] }, trialOutcomesAvailable: { type: "string", description: "Trial outcomes available (yes or no)", examples: ["Yes", "No"] }, trialDateStart: { type: "string", description: "Date the trial is scheduled to start (format: YYYY-MM-DD)", examples: ["1998-08-31"] }, trialDateEnd: { type: "string", description: "Date the trial ended or is expected to end (format: YYYY-MM-DD)", examples: ["2003-10-31"] }, trialDateChangeLast: { type: "string", description: "Update date (format: YYYY-MM-DD)", examples: ["2016-05-02"] }, trialDateAdded: { type: "string", description: "Date the trial is added in our database (format: YYYY-MM-DD). Supports RANGE for date comparison.", examples: ["2015-09-16", "RANGE(>2015-09-16)"] }, trialDateEnrollmentEnd: { type: "string", description: "The date when patient enrollment is completed for a trial (format: YYYY-MM-DD)", examples: ["2008-07-31"] }, trialDateEnrollmentEndByMonth: { type: "string", description: "The date, by month, when patient enrollment is completed for a trial (format: YYYYMM)", examples: ["201506"] }, trialAimsAndScope: { type: "string", description: "Brief summary of trial objectives. Free text search.", examples: ["randomized"] }, trialProtocolDescription: { type: "string", description: "Detailed description of trial protocols. Free text search.", examples: ["objective"] }, trialRegimens: { type: "string", description: "Summary of treatment regimens. Free text search.", examples: ["placebo"] }, trialOutcomes: { type: "string", description: "Summary of trial outcomes. Free text search.", examples: ["results"] }, trialAdverseEvents: { type: "string", description: "Summary of adverse events observed. Free text search.", examples: ["side effects"] }, trialSuspensionReason: { type: "string", description: "Reason for suspension of trial. Free text search.", examples: ["random"] }, trialCriteriaInclusion: { type: "string", description: "Criteria used to select patients for trial. Free text search.", examples: ["disease"] }, trialCriteriaExclusion: { type: "string", description: "Criteria used to exclude patients from trial. Free text search.", examples: ["pain"] }, trialBiomarkerNames: { type: "string", description: "Lists the names and types of any biomarkers relevant to the study. Supports both code format and descriptive format.", examples: ["Neutrophils", "1925"] }, trialGeneVariantSegmentId: { type: "string", description: "Search trial results by gene variant segment Id", examples: ["1192344657"] }, trialContacts: { type: "string", description: "Details of contacts to which queries about the trial can be addressed", examples: ["Paul Goss"] }, trialId: { type: "string", description: "Trial identifier", examples: ["28005"] }, trialCountries: { type: "string", description: "Countries where trial conducted. Supports both code and name format.", examples: ["US"] }, trialRegions: { type: "string", description: "Regions where trial conducted. Supports both code format (e.g., 'USCA') and descriptive format (e.g., 'US & Canada')", examples: ["US & Canada", "USCA"] }, trialSources: { type: "string", description: "Source of trial data. Supports both code format (e.g., 'CTGOV') and descriptive format (e.g., 'ClinicalTrials.gov')", examples: ["ClinicalTrials.gov", "CTGOV"] }, trialDateStartByMonth: { type: "string", description: "Month trial expected to start (format: YYYYMM)", examples: ["201509"] }, trialDateEndByMonth: { type: "string", description: "Month trial expected to finish (format: YYYYMM)", examples: ["201612"] }, trialPrimaryCompletionDateByMonth: { type: "string", description: "The date when data for the primary outcome measure is collected (format: YYYYMM)", examples: ["201712"] }, trialIndicationsPioneer: { type: "string", description: "Conditions where at least one of the interventions is being evaluated in the particular condition for the first time. Supports both code and descriptive format.", examples: ["Metastasis", "1069"] }, trialDrugPhaseHighest: { type: "string", description: "Highest development status of any intervention that is linked to a trial. Supports both code format (e.g., 'L') and descriptive format (e.g., 'Launched')", examples: ["Launched", "L"] }, trialActionsPrimaryInterventionsPrimary: { type: "string", description: "Target based Actions of Trial Intervention. Supports both code format and descriptive format.", examples: ["mTOR inhibitor", "10240"] }, trialIndicationsAdverse: { type: "string", description: "Side effect; any untoward medical occurrence in a patient or trial subject. Supports both code format and descriptive format.", examples: ["Fatigue", "829"] }, trialInterventions: { type: "string", description: "Combined index of trialInterventions alone and in combinations. Supports both code format and descriptive format.", examples: ["docetaxel", "2953"] }, trialContactNames: { type: "string", description: "Contact name", examples: ["Costas Constantinou"] }, trialCities: { type: "string", description: "City name / ID. Supports both code format and descriptive format.", examples: ["Alcoy", "10000028"] }, trialCountrySubdivisions: { type: "string", description: "State/province/country name/id. Supports both code format and descriptive format.", examples: ["Barcelona", "10000010"] }, trialSiteNamesDisplay: { type: "string", description: "Standardised site names, including anonymous sites", examples: ["Japan"] }, trialIsCommerciallySignificant: { type: "string", description: "Indicate whether or not the sponsor or collaborator of the trial is the 'Active' company (or one of its subsidiaries) which is currently developing one or more drug interventions of that trial", examples: ["YES"] }, trialBiomarkerRoles: { type: "string", description: "How a biomarker is being used in a clinical trial, such as a disease marker, therapeutic effect marker, or toxic effect marker", examples: ["disease marker"] }, trialBiomarkerTypes: { type: "string", description: "Biomarker classification, such as genomic, proteomic, biochemical, physiological, cellular, anthropomorphic, structural (imaging)", examples: ["proteomic"] }, trialBiomarkerNamesAll: { type: "string", description: "All biomarkers", examples: ["heart rate"] } } } }; const GET_TRIAL_TOOL = { name: "ci_get_trial", description: "Retrieve trial information. Can return the full trial record or trial sites based on the category parameter.", inputSchema: { type: "object", properties: { id: { type: "string", description: "Trial identifier", examples: ["9997", "77227"] }, category: { type: "string", description: "Type of trial information to retrieve", enum: ["report", "sites"], default: "report", enumDescriptions: { "report": "Full trial record with all available fields", "sites": "Trial sites information" } }, hits: { type: "number", description: "Number of results per page (for sites category only)", default: 20 }, offset: { type: "number", description: "Number of records to skip (for sites category only)", default: 0 } }, required: ["id"] }, examples: [ { description: "Get full trial record", usage: `{ "id": "9997", "category": "report" }` }, { description: "Get trial sites", usage: `{ "id": "77227", "category": "sites", "hits": 20, "offset": 0 }` } ] }; /** * Parameter validation functions */ function validateExploreOntologyParams(params) { if (!params || typeof params !== 'object') { throw new McpError(-32603, 'Invalid parameters: must be an object'); } const p = params; // Validate required parameters if (typeof p.term !== 'string' || !p.term.trim()) { throw new McpError(-32603, 'term parameter is required and must be a non-empty string'); } if (typeof p.category !== 'string' || !p.category.trim()) { throw new McpError(-32603, 'category parameter is required and must be a non-empty string'); } // Validate optional string parameters const optionalStringParams = ['action', 'indication', 'company', 'drug_name', 'target', 'technology']; for (const param of optionalStringParams) { if (param in p && (typeof p[param] !== 'string' || !p[param].trim())) { throw new McpError(-32603, `${param} parameter must be a non-empty string when provided`); } } // Validate offset if present if ('offset' in p) { const offset = p.offset; if (typeof offset !== 'number' || !Number.isInteger(offset) || offset < 0) { throw new McpError(-32603, 'offset parameter must be a non-negative integer when provided'); } } } function validateSearchDrugsParams(params) { if (!params || typeof params !== 'object') { throw new McpError(-32603, 'Invalid parameters: must be an object'); } const p = params; // Validate optional string parameters const stringParams = [ 'query', 'company', 'indication', 'action', 'phase', 'phase_terminated', 'technology', 'drug_name', 'country', 'developmentStatusDate' ]; for (const param of stringParams) { if (param in p && typeof p[param] !== 'string') { throw new McpError(-32603, `${param} parameter must be a string when provided`); } } // Validate historic flag if present if ('historic' in p && typeof p.historic !== 'boolean') { throw new McpError(-32603, 'historic parameter must be a boolean when provided'); } // Validate offset if present if ('offset' in p && (typeof p.offset !== 'number' || p.offset < 0)) { throw new McpError(-32603, 'offset parameter must be a non-negative number when provided'); } } function validateSearchCompaniesParams(params) { if (!params || typeof params !== 'object') { throw new McpError(-32603, 'Invalid parameters: must be an object'); } const p = params; // Validate optional string parameters const stringParams = [ 'query', 'company_name', 'hq_country', 'deals_count', 'indications', 'actions', 'technologies', 'company_size', 'status' ]; for (const param of stringParams) { if (param in p && typeof p[param] !== 'string') { throw new McpError(-32603, `${param} parameter must be a string when provided`); } } // Validate offset if present if ('offset' in p && (typeof p.offset !== 'number' || p.offset < 0)) { throw new McpError(-32603, 'offset parameter must be a non-negative number when provided'); } } /** * Enhanced response sanitization utility */ function sanitizeResponse(response) { // If response is already a string, try to parse it if (typeof response === 'string') { try { const parsed = JSON.parse(response); if (!parsed || typeof parsed !== 'object') { throw new McpError(-32603, 'Invalid response format: expected JSON object'); } return parsed; } catch (error) { if (error instanceof McpError) throw error; throw new McpError(-32603, 'Invalid JSON response received'); } } // If response is already an object/array, validate it if (!response || typeof response !== 'object') { throw new McpError(-32603, 'Invalid response format: expected JSON object'); } // Deep clone and sanitize the response to ensure it's a valid JSON value try { return JSON.parse(JSON.stringify(response)); } catch (error) { throw new McpError(-32603, 'Response contains non-JSON-serializable values'); } } /** * Performs digest authentication for Cortellis API requests * Implements a two-step authentication process: * 1. Initial request to get nonce * 2. Authenticated request with digest credentials * * @param url - The API endpoint URL * @param method - HTTP method (default: 'GET') * @returns Promise resolving to the API response * @throws McpError if authentication or request fails */ async function digestAuth(url, method = 'GET') { try { // process.stderr.write(`${new Date().toISOString()} [INFO] Starting request to: ${url}\n`); // process.stderr.write(`${new Date().toISOString()} [INFO] Using method: ${method}\n`); // First request to get the nonce const response = await fetch(url, { method, headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'Cortellis API Client' } }); // Get WWW-Authenticate header const authHeader = response.headers.get('www-authenticate'); if (!authHeader) { throw new McpError(-32603, 'No WWW-Authenticate header received'); } // Parse WWW-Authenticate header const realm = authHeader.match(/realm="([^"]+)"/)?.[1]; const nonce = authHeader.match(/nonce="([^"]+)"/)?.[1]; const qop = authHeader.match(/qop="([^"]+)"/)?.[1]; if (!realm || !nonce) { throw new McpError(-32603, 'Invalid WWW-Authenticate header - missing realm or nonce'); } // Generate cnonce and nc only if qop is specified let digestResponse; if (qop) { const cnonce = Math.random().toString(36).substring(2); const nc = '00000001'; // Calculate hashes const ha1 = createHash('md5') .update(`${USERNAME}:${realm}:${PASSWORD}`) .digest('hex'); const ha2 = createHash('md5') .update(`${method}:${url}`) .digest('hex'); const response_value = createHash('md5') .update(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`) .digest('hex'); // Construct Authorization header with qop digestResponse = `Digest username="${USERNAME}", realm="${realm}", nonce="${nonce}", uri="${url}", qop=${qop}, nc=${nc}, cnonce="${cnonce}", response="${response_value}", algorithm="MD5"`; } else { // If no qop, use RFC 2069 algorithm const ha1 = createHash('md5') .update(`${USERNAME}:${realm}:${PASSWORD}`) .digest('hex'); const ha2 = createHash('md5') .update(`${method}:${url}`) .digest('hex'); const response_value = createHash('md5') .update(`${ha1}:${nonce}:${ha2}`) .digest('hex'); // Construct Authorization header without qop digestResponse = `Digest username="${USERNAME}", realm="${realm}", nonce="${nonce}", uri="${url}", response="${response_value}", algorithm="MD5"`; } // Make authenticated request const authenticatedResponse = await fetch(url, { method, headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'Cortellis API Client', 'Authorization': digestResponse } }); if (!authenticatedResponse.ok) { const errorText = await authenticatedResponse.text(); throw new McpError(-32603, `Request failed with status ${authenticatedResponse.status}: ${errorText}`); } const text = await authenticatedResponse.text(); try { const jsonResponse = JSON.parse(text); if (!jsonResponse) { throw new McpError(-32603, 'Empty response from API'); } return jsonResponse; } catch (parseError) { throw new McpError(-32603, `Invalid JSON response: ${text.substring(0, 100)}...`); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError(-32603, `API request failed: ${error instanceof Error ? error.message : String(error)}`); } } /** * Searches for drugs in the Cortellis database * Constructs queries based on provided parameters and handles both * LINKED and non-LINKED parameters appropriately * * @param params - Search parameters for filtering drugs * @returns Promise resolving to the search results * @throws McpError if the search fails */ async function searchDrugs(params) { const baseUrl = "https://api.cortellis.com/api-ws/ws/rs/drugs-v2/drug/search"; let query = params.query; if (!query) { const linkedParts = []; const otherParts = []; const isHistoric = params.historic === true; // Handle development status related parameters with LINKED clause if (params.company) linkedParts.push(`${isHistoric ? 'developmentStatusHistoricCompanyId' : 'developmentStatusCompanyId'}:${params.company}`); if (params.indication) linkedParts.push(`${isHistoric ? 'developmentStatusHistoricIndicationId' : 'developmentStatusIndicationId'}:${params.indication}`); if (params.country) linkedParts.push(`${isHistoric ? 'developmentStatusHistoricCountryId' : 'developmentStatusCountryId'}:${params.country}`); if (params.phase) linkedParts.push(`${isHistoric ? 'developmentStatusHistoricPhaseId' : 'developmentStatusPhaseId'}:${params.phase}`); if (params.developmentStatusDate) linkedParts.push(`${isHistoric ? 'developmentStatusHistoricDate' : 'developmentStatusDate'}:${params.developmentStatusDate}`); // Handle other parameters if (params.technology) otherParts.push(`technologies:${params.technology}`); if (params.phase_terminated) { // Handle OR and AND conditions in phase_terminated const phases = params.phase_terminated.split(/\s+(?:OR|AND)\s+/).map(p => p.trim()); if (phases.length > 1) { // Check if original string contains OR or AND const operator = params.phase_terminated.match(/\s+(OR|AND)\s+/)?.[1] || 'OR'; // Handle both formats for each phase const formattedPhases = phases.map(p => { // If it's in the short format (DX, etc) if (/^[A-Z0-9]+$/.test(p)) { return `phaseTerminated::${p}`; } // If it's in the descriptive format ("phase 2 Clinical", etc) return `phaseTerminated:"${p}"`; }); otherParts.push(`(${formattedPhases.join(` ${operator} `)})`); } else { // Single phase - handle both formats const phase = phases[0]; if (/^[A-Z0-9]+$/.test(phase)) { otherParts.push(`phaseTerminated::${phase}`); } else { otherParts.push(`phaseTerminated:"${phase}"`); } } } if (params.action) otherParts.push(`actionsPrimary:${params.action}`); if (params.drug_name) otherParts.push(`drugNamesAll:${