From 4ef719bc7b0a0fa9d697cfa834628ad8f256e060 Mon Sep 17 00:00:00 2001 From: JahnaviSingh2005 Date: Thu, 19 Mar 2026 14:51:02 +0530 Subject: [PATCH 1/2] test-bug --- package-lock.json | 14 +++++++++++++- package.json | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9d63d8e..a7bc55a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "reactome-mcp", "version": "1.0.0", - "license": "MIT", + "license": "Apache-2.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", "zod": "^3.25.0" @@ -389,6 +389,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -585,6 +586,16 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.12.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz", + "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -1142,6 +1153,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index dc0f456..ba27e01 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build": "tsc", "start": "node dist/index.js", "dev": "tsc --watch", - "inspect": "npx @anthropic-ai/mcp-inspector node dist/index.js", + "inspect": "npx -y @modelcontextprotocol/inspector node dist/index.js", "demo": "node web/mcp-bridge.js", "demo:simple": "node web/server.js" }, From b87cb88645f382f6640e31af6d6df4438270efe6 Mon Sep 17 00:00:00 2001 From: JahnaviSingh2005 Date: Thu, 19 Mar 2026 16:10:08 +0530 Subject: [PATCH 2/2] feat: add reactome_ai_explain tool without LLM dependency --- src/tools/ai-explain.ts | 233 ++++++++++++++++++++++++++++++++++++++++ src/tools/index.ts | 2 + 2 files changed, 235 insertions(+) create mode 100644 src/tools/ai-explain.ts diff --git a/src/tools/ai-explain.ts b/src/tools/ai-explain.ts new file mode 100644 index 0000000..0eb21e4 --- /dev/null +++ b/src/tools/ai-explain.ts @@ -0,0 +1,233 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { contentClient } from "../clients/content.js"; +import type { SearchResult, Event, Pathway } from "../types/index.js"; + +/** + * Strip HTML tags from search result text. + */ +function stripHtml(text: string): string { + return text.replace(/<[^>]*>/g, ""); +} + +/** + * Search Reactome, fetch details for the top result, and build + * a human-readable explanation entirely from the Reactome data. + * No external LLM API required. + */ +async function buildExplanation( + query: string, + detailLevel: "brief" | "standard" | "detailed" +): Promise { + // Step 1: Search Reactome + const searchResult = await contentClient.get("/search/query", { + query, + rows: 10, + cluster: true, + }); + + const entries = searchResult.results.flatMap((group) => group.entries); + + if (entries.length === 0) { + return [ + `## No Results Found`, + "", + `Reactome has no entries matching **"${query}"**.`, + "", + "**Tips:**", + "- Try a gene symbol like `TP53` instead of a full name", + "- Try a pathway name like `apoptosis` or `cell cycle`", + "- Check spelling with the `reactome_search_spellcheck` tool", + ].join("\n"); + } + + const lines: string[] = []; + const topEntry = entries[0]; + + // Step 2: Fetch detailed info for the top result + let detailed: Event | null = null; + if (topEntry.stId) { + try { + detailed = await contentClient.get( + `/data/query/enhanced/${encodeURIComponent(topEntry.stId)}` + ); + } catch { + // If detailed fetch fails, continue with search data only + } + } + + // Step 3: Build the explanation header + const name = detailed?.displayName || stripHtml(topEntry.name); + lines.push(`## ${name}`); + lines.push(""); + + // Basic metadata + if (detailed) { + lines.push(`| Property | Value |`); + lines.push(`|----------|-------|`); + lines.push(`| **Reactome ID** | ${detailed.stId} |`); + lines.push(`| **Type** | ${detailed.schemaClass} |`); + if (detailed.speciesName) { + lines.push(`| **Species** | ${detailed.speciesName} |`); + } + if (detailed.isInDisease) { + lines.push(`| **Disease pathway** | Yes |`); + } + if (detailed.hasDiagram) { + lines.push(`| **Has diagram** | Yes |`); + } + lines.push(""); + } + + // Step 4: Add the summary/description + if (detailed?.summation && detailed.summation.length > 0) { + const fullSummary = detailed.summation[0].text; + // Strip HTML from summation text + const cleanSummary = stripHtml(fullSummary); + + if (detailLevel === "brief") { + // Just first 2 sentences + const sentences = cleanSummary.match(/[^.!?]+[.!?]+/g) || [cleanSummary]; + lines.push("### Summary"); + lines.push(sentences.slice(0, 2).join(" ").trim()); + } else { + lines.push("### Description"); + lines.push(cleanSummary); + } + lines.push(""); + } + + // Step 5: For standard/detailed — fetch sub-pathways and participants + if (detailLevel !== "brief" && detailed?.stId) { + // Try to get contained events (sub-pathways and reactions) + if (detailed.schemaClass === "Pathway" || detailed.schemaClass === "TopLevelPathway") { + try { + const containedEvents = await contentClient.get( + `/data/pathway/${encodeURIComponent(detailed.stId)}/containedEvents` + ); + + const subPathways = containedEvents.filter( + (e) => e.schemaClass === "Pathway" + ); + const reactions = containedEvents.filter( + (e) => e.schemaClass === "Reaction" || e.schemaClass === "BlackBoxEvent" + ); + + if (subPathways.length > 0) { + lines.push("### Sub-pathways"); + const limit = detailLevel === "detailed" ? 15 : 5; + subPathways.slice(0, limit).forEach((p) => { + lines.push(`- **${p.displayName}** (\`${p.stId}\`)`); + }); + if (subPathways.length > limit) { + lines.push(`- *...and ${subPathways.length - limit} more*`); + } + lines.push(""); + } + + if (reactions.length > 0) { + lines.push(`### Reactions`); + lines.push(`This pathway involves **${reactions.length}** reactions.`); + if (detailLevel === "detailed") { + reactions.slice(0, 10).forEach((r) => { + lines.push(`- ${r.displayName} (\`${r.stId}\`)`); + }); + if (reactions.length > 10) { + lines.push(`- *...and ${reactions.length - 10} more*`); + } + } + lines.push(""); + } + } catch { + // Not a pathway or fetch failed — skip + } + } + } + + // Step 6: Literature references + if ( + detailed?.literatureReference && + detailed.literatureReference.length > 0 + ) { + const limit = detailLevel === "brief" ? 2 : detailLevel === "standard" ? 3 : 5; + lines.push("### Literature References"); + detailed.literatureReference.slice(0, limit).forEach((ref) => { + if (ref.pubMedIdentifier) { + lines.push( + `- [${ref.displayName}](https://pubmed.ncbi.nlm.nih.gov/${ref.pubMedIdentifier})` + ); + } else { + lines.push(`- ${ref.displayName}`); + } + }); + if (detailed.literatureReference.length > limit) { + lines.push( + `- *...and ${detailed.literatureReference.length - limit} more references*` + ); + } + lines.push(""); + } + + // Step 7: Related results from search + if (detailLevel === "detailed" && entries.length > 1) { + lines.push("### Related Entries in Reactome"); + entries.slice(1, 6).forEach((entry) => { + const entryName = stripHtml(entry.name); + lines.push(`- **${entryName}** (\`${entry.stId}\`) — ${entry.exactType}`); + }); + lines.push(""); + } + + // Step 8: Helpful next steps + lines.push("### Explore Further"); + if (detailed?.stId) { + lines.push(`- Use \`reactome_get_pathway\` with ID \`${detailed.stId}\` for full details`); + if (detailed.hasDiagram) { + lines.push(`- Use \`reactome_export_diagram\` with ID \`${detailed.stId}\` to get the pathway diagram`); + } + lines.push(`- Use \`reactome_participants\` with ID \`${detailed.stId}\` to see molecular participants`); + } + lines.push( + `- Use \`reactome_search\` with query \`${query}\` to see all matching results` + ); + + return lines.join("\n"); +} + +export function registerAiExplainTools(server: McpServer) { + server.tool( + "reactome_ai_explain", + "Get a clear, human-readable explanation of a biological concept, pathway, or entity using data from the Reactome knowledgebase. No API key required.", + { + query: z + .string() + .describe( + 'The biological concept or entity to explain (e.g., "TP53", "apoptosis", "cell cycle", "BRCA1")' + ), + detail_level: z + .enum(["brief", "standard", "detailed"]) + .optional() + .default("standard") + .describe( + "How detailed the explanation should be: brief (summary only), standard (with sub-pathways), detailed (comprehensive with reactions and references)" + ), + }, + async ({ query, detail_level }) => { + try { + const explanation = await buildExplanation(query, detail_level); + return { + content: [{ type: "text", text: explanation }], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `## Error\n\nFailed to fetch data from Reactome: ${error instanceof Error ? error.message : String(error)}`, + }, + ], + }; + } + } + ); +} diff --git a/src/tools/index.ts b/src/tools/index.ts index af3a1c3..9b2e1fe 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -9,6 +9,7 @@ import { registerSearchTools } from "./search.js"; import { registerEntityTools } from "./entity.js"; import { registerExportTools } from "./export.js"; import { registerInteractorTools } from "./interactors.js"; +import { registerAiExplainTools } from "./ai-explain.js"; export function registerAllTools(server: McpServer) { // Register tools from all modules @@ -18,6 +19,7 @@ export function registerAllTools(server: McpServer) { registerEntityTools(server); registerExportTools(server); registerInteractorTools(server); + registerAiExplainTools(server); // Register utility tools directly here registerUtilityTools(server);