Skip to main content
Glama
ezhou89

Medical Research MCP Suite

by ezhou89
drug-knowledge-graph.ts21.6 kB
// src/utils/drug-knowledge-graph.ts import logger from './logger.js'; export interface DrugNode { id: string; name: string; type: 'drug' | 'compound' | 'ingredient'; aliases: string[]; properties: { molecularWeight?: number; formula?: string; mechanism?: string; therapeuticClass?: string; approvalStatus?: string; approvalDate?: string; indication?: string[]; contraindications?: string[]; interactions?: string[]; }; metadata: { sources: string[]; lastUpdated: string; confidence: number; }; } export interface RelationshipEdge { id: string; source: string; target: string; type: RelationshipType; strength: number; // 0-1 scale properties: Record<string, any>; metadata: { sources: string[]; lastUpdated: string; confidence: number; }; } export type RelationshipType = | 'contains' // Drug contains ingredient | 'similar_to' // Similar mechanism/structure | 'interacts_with' // Drug-drug interaction | 'treats' // Drug treats condition | 'causes' // Drug causes side effect | 'metabolized_by' // Metabolic pathway | 'contraindicated_with' // Contraindication | 'alternative_to' // Alternative treatment | 'precursor_to' // Chemical precursor | 'derived_from' // Chemical derivation | 'competes_with' // Market competition | 'combined_with'; // Combination therapy export interface DrugPath { nodes: DrugNode[]; edges: RelationshipEdge[]; totalDistance: number; pathStrength: number; pathType: string; } export interface DrugCluster { id: string; name: string; drugs: string[]; centerDrug?: string; clusterType: 'therapeutic' | 'mechanism' | 'structure' | 'indication'; coherence: number; // How tightly related the drugs are size: number; } export interface KnowledgeGraphQuery { startNode?: string; endNode?: string; relationshipTypes?: RelationshipType[]; maxDepth?: number; minStrength?: number; includeProperties?: string[]; excludeTypes?: string[]; } export interface GraphAnalytics { totalNodes: number; totalEdges: number; averageDegree: number; density: number; stronglyConnectedComponents: number; clusters: DrugCluster[]; hubNodes: string[]; // Most connected nodes bridgeNodes: string[]; // Nodes that connect different clusters } export class DrugKnowledgeGraph { private nodes: Map<string, DrugNode>; private edges: Map<string, RelationshipEdge>; private adjacencyList: Map<string, Set<string>>; private reverseAdjacencyList: Map<string, Set<string>>; private drugAliases: Map<string, string>; // alias -> canonical name private lastUpdated: string; constructor() { this.nodes = new Map(); this.edges = new Map(); this.adjacencyList = new Map(); this.reverseAdjacencyList = new Map(); this.drugAliases = new Map(); this.lastUpdated = new Date().toISOString(); this.initializeBasicDrugKnowledge(); } /** * Add a drug node to the knowledge graph */ public addDrugNode(drug: Omit<DrugNode, 'id' | 'metadata'>): DrugNode { const id = this.generateNodeId(drug.name); const node: DrugNode = { ...drug, id, metadata: { sources: ['manual'], lastUpdated: new Date().toISOString(), confidence: 0.8 } }; this.nodes.set(id, node); this.adjacencyList.set(id, new Set()); this.reverseAdjacencyList.set(id, new Set()); // Index aliases for (const alias of drug.aliases) { this.drugAliases.set(alias.toLowerCase(), id); } this.drugAliases.set(drug.name.toLowerCase(), id); logger.info('Added drug node to knowledge graph', { drugId: id, drugName: drug.name, aliasCount: drug.aliases.length }); return node; } /** * Add a relationship between two drugs */ public addRelationship( sourceDrug: string, targetDrug: string, type: RelationshipType, strength: number = 0.5, properties: Record<string, any> = {} ): RelationshipEdge { const sourceId = this.resolveDrugId(sourceDrug); const targetId = this.resolveDrugId(targetDrug); if (!sourceId || !targetId) { throw new Error(`Cannot find one or both drugs: ${sourceDrug}, ${targetDrug}`); } const edgeId = `${sourceId}_${type}_${targetId}`; const edge: RelationshipEdge = { id: edgeId, source: sourceId, target: targetId, type, strength: Math.max(0, Math.min(1, strength)), properties, metadata: { sources: ['manual'], lastUpdated: new Date().toISOString(), confidence: 0.8 } }; this.edges.set(edgeId, edge); this.adjacencyList.get(sourceId)!.add(targetId); this.reverseAdjacencyList.get(targetId)!.add(sourceId); logger.info('Added relationship to knowledge graph', { source: sourceDrug, target: targetDrug, type, strength }); return edge; } /** * Find drugs related to a given drug */ public findRelatedDrugs( drugName: string, options: { maxResults?: number; relationshipTypes?: RelationshipType[]; minStrength?: number; maxDepth?: number; } = {} ): Array<{ drug: DrugNode; relationship: RelationshipEdge; distance: number }> { const { maxResults = 20, relationshipTypes = [], minStrength = 0.1, maxDepth = 2 } = options; const drugId = this.resolveDrugId(drugName); if (!drugId) { logger.warn('Drug not found in knowledge graph', { drugName }); return []; } const results: Array<{ drug: DrugNode; relationship: RelationshipEdge; distance: number }> = []; const visited = new Set<string>(); const queue: Array<{ nodeId: string; distance: number; path: RelationshipEdge[] }> = [{ nodeId: drugId, distance: 0, path: [] }]; while (queue.length > 0 && results.length < maxResults) { const { nodeId, distance, path } = queue.shift()!; if (visited.has(nodeId) || distance > maxDepth) { continue; } visited.add(nodeId); // Get all outgoing edges const adjacentNodes = this.adjacencyList.get(nodeId) || new Set(); for (const adjacentNodeId of adjacentNodes) { const edgeId = this.findEdgeId(nodeId, adjacentNodeId); if (!edgeId) continue; const edge = this.edges.get(edgeId)!; // Filter by relationship type if (relationshipTypes.length > 0 && !relationshipTypes.includes(edge.type)) { continue; } // Filter by strength if (edge.strength < minStrength) { continue; } // Skip self-references if (adjacentNodeId === drugId) { continue; } const adjacentNode = this.nodes.get(adjacentNodeId); if (!adjacentNode) continue; results.push({ drug: adjacentNode, relationship: edge, distance: distance + 1 }); // Add to queue for further exploration if (distance + 1 < maxDepth) { queue.push({ nodeId: adjacentNodeId, distance: distance + 1, path: [...path, edge] }); } } } // Sort by relevance (combination of distance and strength) results.sort((a, b) => { const scoreA = a.relationship.strength / (a.distance + 1); const scoreB = b.relationship.strength / (b.distance + 1); return scoreB - scoreA; }); logger.info('Found related drugs', { drugName, relatedCount: results.length, maxDepth, relationshipTypes }); return results.slice(0, maxResults); } /** * Find the shortest path between two drugs */ public findShortestPath( sourceDrug: string, targetDrug: string, options: { maxDepth?: number; relationshipTypes?: RelationshipType[]; minStrength?: number; } = {} ): DrugPath | null { const sourceId = this.resolveDrugId(sourceDrug); const targetId = this.resolveDrugId(targetDrug); if (!sourceId || !targetId) { return null; } const { maxDepth = 5, relationshipTypes = [], minStrength = 0.1 } = options; // BFS to find shortest path const queue: Array<{ nodeId: string; path: string[]; edges: RelationshipEdge[]; totalStrength: number; }> = [{ nodeId: sourceId, path: [sourceId], edges: [], totalStrength: 1.0 }]; const visited = new Set<string>(); while (queue.length > 0) { const { nodeId, path, edges, totalStrength } = queue.shift()!; if (nodeId === targetId) { // Found path const nodes = path.map(id => this.nodes.get(id)!); const pathStrength = totalStrength / path.length; return { nodes, edges, totalDistance: path.length - 1, pathStrength, pathType: this.classifyPath(edges) }; } if (visited.has(nodeId) || path.length > maxDepth) { continue; } visited.add(nodeId); // Explore adjacent nodes const adjacentNodes = this.adjacencyList.get(nodeId) || new Set(); for (const adjacentNodeId of adjacentNodes) { if (path.includes(adjacentNodeId)) continue; // Avoid cycles const edgeId = this.findEdgeId(nodeId, adjacentNodeId); if (!edgeId) continue; const edge = this.edges.get(edgeId)!; // Apply filters if (relationshipTypes.length > 0 && !relationshipTypes.includes(edge.type)) { continue; } if (edge.strength < minStrength) { continue; } queue.push({ nodeId: adjacentNodeId, path: [...path, adjacentNodeId], edges: [...edges, edge], totalStrength: totalStrength * edge.strength }); } } return null; // No path found } /** * Find drug clusters based on relationships */ public findDrugClusters( options: { minClusterSize?: number; maxClusters?: number; clusterType?: 'therapeutic' | 'mechanism' | 'structure' | 'indication'; minCoherence?: number; } = {} ): DrugCluster[] { const { minClusterSize = 3, maxClusters = 10, clusterType = 'therapeutic', minCoherence = 0.5 } = options; const clusters: DrugCluster[] = []; const visited = new Set<string>(); // Use connected components approach for (const [nodeId, node] of this.nodes) { if (visited.has(nodeId)) continue; const cluster = this.buildCluster(nodeId, visited, clusterType, minCoherence); if (cluster.drugs.length >= minClusterSize) { clusters.push(cluster); } if (clusters.length >= maxClusters) { break; } } // Sort clusters by size and coherence clusters.sort((a, b) => { const scoreA = a.size * a.coherence; const scoreB = b.size * b.coherence; return scoreB - scoreA; }); logger.info('Found drug clusters', { clusterCount: clusters.length, clusterType, averageSize: clusters.reduce((sum, c) => sum + c.size, 0) / clusters.length }); return clusters; } /** * Get analytics about the knowledge graph */ public getAnalytics(): GraphAnalytics { const totalNodes = this.nodes.size; const totalEdges = this.edges.size; // Calculate average degree let totalDegree = 0; for (const adjacentSet of this.adjacencyList.values()) { totalDegree += adjacentSet.size; } const averageDegree = totalNodes > 0 ? totalDegree / totalNodes : 0; // Calculate density const maxPossibleEdges = totalNodes * (totalNodes - 1); const density = maxPossibleEdges > 0 ? totalEdges / maxPossibleEdges : 0; // Find hub nodes (most connected) const hubNodes = Array.from(this.adjacencyList.entries()) .sort(([, a], [, b]) => b.size - a.size) .slice(0, 5) .map(([nodeId]) => nodeId); // Find bridge nodes (simplified approach) const bridgeNodes = this.findBridgeNodes(); // Get clusters const clusters = this.findDrugClusters(); return { totalNodes, totalEdges, averageDegree: Math.round(averageDegree * 100) / 100, density: Math.round(density * 10000) / 10000, stronglyConnectedComponents: this.countConnectedComponents(), clusters, hubNodes, bridgeNodes }; } /** * Query the knowledge graph with complex criteria */ public query(query: KnowledgeGraphQuery): { nodes: DrugNode[]; edges: RelationshipEdge[]; paths: DrugPath[]; } { const { startNode, endNode, relationshipTypes = [], maxDepth = 3, minStrength = 0.1, includeProperties = [], excludeTypes = [] } = query; let resultNodes: DrugNode[] = []; let resultEdges: RelationshipEdge[] = []; let paths: DrugPath[] = []; if (startNode && endNode) { // Find paths between specific nodes const path = this.findShortestPath(startNode, endNode, { maxDepth, relationshipTypes, minStrength }); if (path) { paths.push(path); resultNodes.push(...path.nodes); resultEdges.push(...path.edges); } } else if (startNode) { // Find related nodes from start node const related = this.findRelatedDrugs(startNode, { relationshipTypes, minStrength, maxDepth }); resultNodes.push(this.nodes.get(this.resolveDrugId(startNode)!)!); for (const rel of related) { resultNodes.push(rel.drug); resultEdges.push(rel.relationship); } } else { // General query - return filtered nodes and edges for (const [, node] of this.nodes) { if (excludeTypes.includes(node.type)) continue; resultNodes.push(node); } for (const [, edge] of this.edges) { if (relationshipTypes.length > 0 && !relationshipTypes.includes(edge.type)) continue; if (edge.strength < minStrength) continue; resultEdges.push(edge); } } // Remove duplicates const uniqueNodes = Array.from(new Map(resultNodes.map(n => [n.id, n])).values()); const uniqueEdges = Array.from(new Map(resultEdges.map(e => [e.id, e])).values()); logger.info('Knowledge graph query executed', { query, resultNodes: uniqueNodes.length, resultEdges: uniqueEdges.length, paths: paths.length }); return { nodes: uniqueNodes, edges: uniqueEdges, paths }; } // Private helper methods private generateNodeId(name: string): string { return name.toLowerCase().replace(/[^a-z0-9]/g, '_'); } private resolveDrugId(drugName: string): string | null { const canonical = this.drugAliases.get(drugName.toLowerCase()); return canonical || null; } private findEdgeId(sourceId: string, targetId: string): string | null { // Try to find any edge between these nodes for (const [edgeId, edge] of this.edges) { if (edge.source === sourceId && edge.target === targetId) { return edgeId; } } return null; } private classifyPath(edges: RelationshipEdge[]): string { if (edges.length === 0) return 'direct'; const types = edges.map(e => e.type); const uniqueTypes = [...new Set(types)]; if (uniqueTypes.length === 1) { return uniqueTypes[0]; } else if (uniqueTypes.every(t => ['treats', 'alternative_to'].includes(t))) { return 'therapeutic'; } else if (uniqueTypes.every(t => ['similar_to', 'derived_from'].includes(t))) { return 'structural'; } else { return 'mixed'; } } private buildCluster( startNodeId: string, visited: Set<string>, clusterType: string, minCoherence: number ): DrugCluster { const clusterNodes = new Set<string>(); const queue = [startNodeId]; while (queue.length > 0) { const nodeId = queue.shift()!; if (visited.has(nodeId)) continue; visited.add(nodeId); clusterNodes.add(nodeId); // Add connected nodes based on cluster type const adjacentNodes = this.adjacencyList.get(nodeId) || new Set(); for (const adjacentNodeId of adjacentNodes) { if (visited.has(adjacentNodeId)) continue; const edgeId = this.findEdgeId(nodeId, adjacentNodeId); if (!edgeId) continue; const edge = this.edges.get(edgeId)!; // Include based on cluster type if (this.shouldIncludeInCluster(edge, clusterType)) { queue.push(adjacentNodeId); } } } const coherence = this.calculateClusterCoherence(Array.from(clusterNodes)); return { id: `cluster_${startNodeId}`, name: `${clusterType} cluster`, drugs: Array.from(clusterNodes), centerDrug: startNodeId, clusterType: clusterType as any, coherence, size: clusterNodes.size }; } private shouldIncludeInCluster(edge: RelationshipEdge, clusterType: string): boolean { switch (clusterType) { case 'therapeutic': return ['treats', 'alternative_to', 'combined_with'].includes(edge.type); case 'mechanism': return ['similar_to', 'metabolized_by'].includes(edge.type); case 'structure': return ['similar_to', 'derived_from', 'precursor_to'].includes(edge.type); case 'indication': return ['treats', 'alternative_to'].includes(edge.type); default: return edge.strength > 0.5; } } private calculateClusterCoherence(nodeIds: string[]): number { if (nodeIds.length < 2) return 1.0; let totalEdges = 0; let existingEdges = 0; // Calculate how many edges exist vs how many could exist for (let i = 0; i < nodeIds.length; i++) { for (let j = i + 1; j < nodeIds.length; j++) { totalEdges++; if (this.findEdgeId(nodeIds[i], nodeIds[j]) || this.findEdgeId(nodeIds[j], nodeIds[i])) { existingEdges++; } } } return totalEdges > 0 ? existingEdges / totalEdges : 0; } private findBridgeNodes(): string[] { // Simplified bridge node detection const bridgeNodes: string[] = []; for (const [nodeId] of this.nodes) { const adjacent = this.adjacencyList.get(nodeId) || new Set(); if (adjacent.size > 1) { // Check if removing this node would disconnect the graph // This is a simplified check - in practice would need more sophisticated algorithm const connectivityScore = this.calculateConnectivityScore(nodeId); if (connectivityScore > 0.7) { bridgeNodes.push(nodeId); } } } return bridgeNodes.slice(0, 5); // Return top 5 } private calculateConnectivityScore(nodeId: string): number { const adjacent = this.adjacencyList.get(nodeId) || new Set(); const degree = adjacent.size; const totalNodes = this.nodes.size; // Simple heuristic: nodes with many connections that span different clusters return Math.min(degree / Math.sqrt(totalNodes), 1); } private countConnectedComponents(): number { const visited = new Set<string>(); let components = 0; for (const [nodeId] of this.nodes) { if (!visited.has(nodeId)) { this.dfsVisit(nodeId, visited); components++; } } return components; } private dfsVisit(nodeId: string, visited: Set<string>): void { visited.add(nodeId); const adjacent = this.adjacencyList.get(nodeId) || new Set(); for (const adjacentNodeId of adjacent) { if (!visited.has(adjacentNodeId)) { this.dfsVisit(adjacentNodeId, visited); } } } /** * Initialize basic drug knowledge */ private initializeBasicDrugKnowledge(): void { // Add some common drugs with relationships const aspirin = this.addDrugNode({ name: 'Aspirin', type: 'drug', aliases: ['acetylsalicylic acid', 'ASA'], properties: { mechanism: 'COX inhibitor', therapeuticClass: 'NSAID', approvalStatus: 'approved', indication: ['pain', 'inflammation', 'fever', 'cardiovascular protection'] } }); const ibuprofen = this.addDrugNode({ name: 'Ibuprofen', type: 'drug', aliases: ['Advil', 'Motrin'], properties: { mechanism: 'COX inhibitor', therapeuticClass: 'NSAID', approvalStatus: 'approved', indication: ['pain', 'inflammation', 'fever'] } }); const metformin = this.addDrugNode({ name: 'Metformin', type: 'drug', aliases: ['Glucophage'], properties: { mechanism: 'biguanide', therapeuticClass: 'antidiabetic', approvalStatus: 'approved', indication: ['type 2 diabetes'] } }); // Add relationships this.addRelationship('Aspirin', 'Ibuprofen', 'similar_to', 0.8, { reason: 'both are NSAIDs with similar mechanisms' }); this.addRelationship('Aspirin', 'Ibuprofen', 'alternative_to', 0.7, { reason: 'can be used as alternatives for pain relief' }); logger.info('Initialized basic drug knowledge graph', { nodes: this.nodes.size, edges: this.edges.size }); } } // Export singleton instance export const drugKnowledgeGraph = new DrugKnowledgeGraph();

Latest Blog Posts

MCP directory API

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

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

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