From 831885d2d17ef5de0473bf70c793d3c6e1c5ec75 Mon Sep 17 00:00:00 2001 From: adidev001 Date: Thu, 19 Mar 2026 14:50:07 +0530 Subject: [PATCH 1/5] fixed mcp insepct package name --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dc0f456..8e081e2 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 @modelcontextprotocol/inspector node dist/index.js", "demo": "node web/mcp-bridge.js", "demo:simple": "node web/server.js" }, From 635f6949f3662884e6267a362015eba5db7de18c Mon Sep 17 00:00:00 2001 From: adidev001 Date: Thu, 19 Mar 2026 16:09:26 +0530 Subject: [PATCH 2/5] Add initial automated smoke tests --- .gitignore | 1 + CHANGELOG.md | 124 ++++++++++++++++++++++++ package-lock.json | 14 ++- package.json | 2 + src/index.ts | 14 +-- src/server.ts | 15 +++ src/tools/analysis.ts | 14 +-- src/tools/search.ts | 14 +-- test/helpers/mcp-test-utils.ts | 47 +++++++++ test/resources-smoke.test.ts | 83 ++++++++++++++++ test/run-tests.ts | 3 + test/server-registration.test.ts | 31 ++++++ test/tools-smoke.test.ts | 157 +++++++++++++++++++++++++++++++ tsconfig.test.json | 12 +++ 14 files changed, 506 insertions(+), 25 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 src/server.ts create mode 100644 test/helpers/mcp-test-utils.ts create mode 100644 test/resources-smoke.test.ts create mode 100644 test/run-tests.ts create mode 100644 test/server-registration.test.ts create mode 100644 test/tools-smoke.test.ts create mode 100644 tsconfig.test.json diff --git a/.gitignore b/.gitignore index 55ea50a..01e8b02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ dist/ +dist-test/ # Environment .env diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0b5d03b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,124 @@ +# Changelog + +## 2026-03-19 + +### Added + +#### Initial automated smoke test suite + +Files: +- `test/server-registration.test.ts` +- `test/tools-smoke.test.ts` +- `test/resources-smoke.test.ts` +- `test/helpers/mcp-test-utils.ts` +- `test/run-tests.ts` + +Why this was added: +- The project did not have automated coverage for MCP surface registration or basic handler behavior. +- These tests provide a fast offline check that the server still exposes expected tools, resources, and templates. +- The tests stub the content and analysis client boundaries, so contributors can run them without depending on live Reactome services. + +Purpose it serves: +- Catches accidental breakage in MCP registration. +- Verifies representative tool formatting and handler wiring. +- Verifies representative static resource and resource-template behavior. +- Gives contributors a small, readable starting point for expanding coverage. + +#### Test-specific TypeScript build config + +File: +- `tsconfig.test.json` + +Why this was added: +- The existing TypeScript config only compiled `src/`. +- Tests need to compile cleanly alongside source files without changing the production build output. + +Purpose it serves: +- Builds `src/` and `test/` into `dist-test/`. +- Keeps test artifacts separate from production artifacts in `dist/`. + +#### Server factory for testability + +File: +- `src/server.ts` + +Why this was added: +- Server construction and registration were previously trapped in the runtime entrypoint. +- Tests needed a clean way to create the MCP server without connecting stdio transport. + +Purpose it serves: +- Centralizes MCP server construction. +- Lets tests import and inspect a fully registered server. +- Preserves the runtime behavior while making initialization testable. + +### Changed + +#### Runtime entrypoint now uses shared server factory + +File: +- `src/index.ts` + +What changed: +- Replaced inline server creation with `createServer()`. + +Why this was changed: +- To avoid duplicating server construction logic between runtime and tests. + +Purpose it serves: +- Keeps production startup behavior unchanged. +- Ensures runtime and tests use the same registration path. + +#### Package scripts now support test compilation and execution + +File: +- `package.json` + +What changed: +- Added `build:test`. +- Added `test`. + +Why this was changed: +- Contributors need a single command to build and run the new automated suite. + +Purpose it serves: +- `npm test` now runs the smoke suite. +- Keeps the workflow simple and contributor-friendly. + +#### Ignore test build artifacts + +File: +- `.gitignore` + +What changed: +- Added `dist-test/`. + +Why this was changed: +- The new test build produces compiled output separate from `dist/`. + +Purpose it serves: +- Prevents generated test artifacts from being committed accidentally. + +#### Input validation tightened for clearer failure behavior + +Files: +- `src/tools/search.ts` +- `src/tools/analysis.ts` + +What changed: +- Search query inputs now require a non-empty trimmed string in representative search tools. +- Analysis token inputs now require a non-empty trimmed string in token-based analysis tools. + +Why this was changed: +- Empty strings were previously accepted by schema validation, which made invalid-input behavior less clear. +- The test suite needed deterministic validation coverage for common bad inputs. + +Purpose it serves: +- Produces clearer failures for missing/blank search queries. +- Produces clearer failures for missing/blank analysis tokens. +- Improves behavior without changing the core tool output shape. + +### Notes + +- The automated suite uses `node:test` and `node:assert/strict` to stay lightweight and compatible with the repository's Node support target. +- Tests are intentionally small smoke tests, not full endpoint-by-endpoint coverage. +- External Reactome network calls are avoided in automated tests by stubbing the client-layer methods used by handlers. 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 8e081e2..17f6eee 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,10 @@ }, "scripts": { "build": "tsc", + "build:test": "tsc -p tsconfig.test.json", "start": "node dist/index.js", "dev": "tsc --watch", + "test": "npm run build:test && node dist-test/test/run-tests.js", "inspect": "npx @modelcontextprotocol/inspector node dist/index.js", "demo": "node web/mcp-bridge.js", "demo:simple": "node web/server.js" diff --git a/src/index.ts b/src/index.ts index 1b1a2a6..4e9b38a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,11 @@ #!/usr/bin/env node -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { registerAllTools } from "./tools/index.js"; -import { registerAllResources } from "./resources/index.js"; - -const server = new McpServer({ - name: "reactome", - version: "1.0.0", -}); - -// Register all tools and resources -registerAllTools(server); -registerAllResources(server); +import { createServer } from "./server.js"; // Start the server async function main() { + const server = createServer(); const transport = new StdioServerTransport(); await server.connect(transport); console.error("Reactome MCP server running on stdio"); diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..d000f63 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,15 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { registerAllTools } from "./tools/index.js"; +import { registerAllResources } from "./resources/index.js"; + +export function createServer() { + const server = new McpServer({ + name: "reactome", + version: "1.0.0", + }); + + registerAllTools(server); + registerAllResources(server); + + return server; +} diff --git a/src/tools/analysis.ts b/src/tools/analysis.ts index 52a81b4..0ac104f 100644 --- a/src/tools/analysis.ts +++ b/src/tools/analysis.ts @@ -11,6 +11,8 @@ import type { Bin, } from "../types/index.js"; +const analysisTokenSchema = z.string().trim().min(1).describe("Analysis token from a previous analysis"); + function formatPathwaySummary(pathway: PathwaySummary): string { return [ `- **${pathway.name}** (${pathway.stId})`, @@ -104,7 +106,7 @@ export function registerAnalysisTools(server: McpServer) { "reactome_get_analysis_result", "Retrieve a previously computed analysis result using its token. Allows filtering and pagination.", { - token: z.string().describe("Analysis token from a previous analysis"), + token: analysisTokenSchema, species: z.string().optional().describe("Filter by species"), sort_by: z.enum(["NAME", "TOTAL_ENTITIES", "FOUND_ENTITIES", "ENTITIES_PVALUE", "ENTITIES_FDR", "ENTITIES_RATIO"]).optional().default("ENTITIES_PVALUE").describe("Sort field"), order: z.enum(["ASC", "DESC"]).optional().default("ASC").describe("Sort order"), @@ -133,7 +135,7 @@ export function registerAnalysisTools(server: McpServer) { "reactome_analysis_found_entities", "Get the identifiers that were found in a specific pathway from an analysis result.", { - token: z.string().describe("Analysis token"), + token: analysisTokenSchema, pathway: z.string().describe("Pathway stable ID (e.g., R-HSA-109582)"), resource: z.string().optional().default("TOTAL").describe("Resource filter (TOTAL, UNIPROT, ENSEMBL, etc.)"), }, @@ -169,7 +171,7 @@ export function registerAnalysisTools(server: McpServer) { "reactome_analysis_not_found", "Get the list of identifiers that could not be mapped in an analysis.", { - token: z.string().describe("Analysis token"), + token: analysisTokenSchema, page: z.number().optional().default(1).describe("Page number"), page_size: z.number().optional().default(100).describe("Results per page"), }, @@ -197,7 +199,7 @@ export function registerAnalysisTools(server: McpServer) { "reactome_analysis_resources", "Get a summary of the molecule types (resources) found in an analysis.", { - token: z.string().describe("Analysis token"), + token: analysisTokenSchema, }, async ({ token }) => { const result = await analysisClient.get(`/token/${token}/resources`); @@ -244,7 +246,7 @@ export function registerAnalysisTools(server: McpServer) { "reactome_analysis_pathway_sizes", "Get the distribution of pathway sizes (binned) from an analysis result.", { - token: z.string().describe("Analysis token"), + token: analysisTokenSchema, bin_size: z.number().optional().default(100).describe("Bin size for grouping pathway sizes"), species: z.string().optional().describe("Filter by species"), resource: z.string().optional().default("TOTAL").describe("Resource filter"), @@ -275,7 +277,7 @@ export function registerAnalysisTools(server: McpServer) { "reactome_filter_analysis_pathways", "Filter an analysis result to only include specific pathways.", { - token: z.string().describe("Analysis token"), + token: analysisTokenSchema, pathways: z.array(z.string()).describe("List of pathway stable IDs to include"), resource: z.string().optional().default("TOTAL").describe("Resource filter"), p_value: z.number().optional().describe("p-value threshold"), diff --git a/src/tools/search.ts b/src/tools/search.ts index 9e88059..81c3c3a 100644 --- a/src/tools/search.ts +++ b/src/tools/search.ts @@ -18,6 +18,8 @@ interface PathwaySearchResult { species: string; } +const searchQuerySchema = z.string().trim().min(1); + function stripHtml(text: string): string { return text.replace(/<[^>]*>/g, ''); } @@ -64,7 +66,7 @@ export function registerSearchTools(server: McpServer) { "reactome_search", "Search the Reactome knowledgebase for pathways, reactions, proteins, genes, compounds, and other entities.", { - query: z.string().describe("Search term (gene name, protein, pathway name, disease, etc.)"), + query: searchQuerySchema.describe("Search term (gene name, protein, pathway name, disease, etc.)"), species: z.string().optional().describe("Filter by species (e.g., 'Homo sapiens', 'Mus musculus')"), types: z.array(z.string()).optional().describe("Filter by type (Pathway, Reaction, Protein, Gene, Complex, etc.)"), compartments: z.array(z.string()).optional().describe("Filter by cellular compartment"), @@ -115,7 +117,7 @@ export function registerSearchTools(server: McpServer) { "reactome_search_paginated", "Search Reactome with pagination support for browsing through large result sets.", { - query: z.string().describe("Search term"), + query: searchQuerySchema.describe("Search term"), page: z.number().optional().default(1).describe("Page number (1-based)"), rows_per_page: z.number().optional().default(20).describe("Results per page"), species: z.string().optional().describe("Filter by species"), @@ -158,7 +160,7 @@ export function registerSearchTools(server: McpServer) { "reactome_search_suggest", "Get auto-complete suggestions for a search query.", { - query: z.string().describe("Partial search term"), + query: searchQuerySchema.describe("Partial search term"), }, async ({ query }) => { const result = await contentClient.get("/search/suggest", { query }); @@ -184,7 +186,7 @@ export function registerSearchTools(server: McpServer) { "reactome_search_spellcheck", "Get spell-check suggestions for a search query.", { - query: z.string().describe("Search term to check"), + query: searchQuerySchema.describe("Search term to check"), }, async ({ query }) => { const result = await contentClient.get("/search/spellcheck", { query }); @@ -212,7 +214,7 @@ export function registerSearchTools(server: McpServer) { "reactome_search_facets", "Get available facets (filters) for search results, either globally or for a specific query.", { - query: z.string().optional().describe("Search term (optional, returns global facets if omitted)"), + query: searchQuerySchema.optional().describe("Search term (optional, returns global facets if omitted)"), }, async ({ query }) => { interface FacetResult { @@ -309,7 +311,7 @@ export function registerSearchTools(server: McpServer) { "Search for entities within a specific pathway diagram.", { diagram: z.string().describe("Pathway stable ID for the diagram"), - query: z.string().describe("Search term"), + query: searchQuerySchema.describe("Search term"), include_interactors: z.boolean().optional().default(false).describe("Include interactors"), }, async ({ diagram, query, include_interactors }) => { diff --git a/test/helpers/mcp-test-utils.ts b/test/helpers/mcp-test-utils.ts new file mode 100644 index 0000000..9c3c218 --- /dev/null +++ b/test/helpers/mcp-test-utils.ts @@ -0,0 +1,47 @@ +import type { + McpServer, + RegisteredResource, + RegisteredResourceTemplate, + RegisteredTool, +} from "@modelcontextprotocol/sdk/server/mcp.js"; + +type ServerInternals = { + _registeredResources: Record; + _registeredResourceTemplates: Record; + _registeredTools: Record; + validateToolInput(tool: RegisteredTool, args: unknown, toolName: string): Promise; + executeToolHandler(tool: RegisteredTool, args: unknown, extra: Record): Promise; +}; + +export function getRegisteredTools(server: McpServer) { + return (server as unknown as ServerInternals)._registeredTools; +} + +export function getRegisteredResources(server: McpServer) { + return (server as unknown as ServerInternals)._registeredResources; +} + +export function getRegisteredResourceTemplates(server: McpServer) { + return (server as unknown as ServerInternals)._registeredResourceTemplates; +} + +export async function invokeTool(server: McpServer, name: string, args: unknown) { + const internals = server as unknown as ServerInternals; + const tool = internals._registeredTools[name]; + + if (!tool) { + throw new Error(`Tool ${name} is not registered`); + } + + const parsedArgs = await internals.validateToolInput(tool, args, name); + return internals.executeToolHandler(tool, parsedArgs, {}); +} + +export function stubMethod(target: T, key: K, replacement: T[K]) { + const original = target[key]; + (target as T)[key] = replacement; + + return () => { + (target as T)[key] = original; + }; +} diff --git a/test/resources-smoke.test.ts b/test/resources-smoke.test.ts new file mode 100644 index 0000000..3b27863 --- /dev/null +++ b/test/resources-smoke.test.ts @@ -0,0 +1,83 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { contentClient } from "../src/clients/content.js"; +import { createServer } from "../src/server.js"; +import { + getRegisteredResources, + getRegisteredResourceTemplates, + stubMethod, +} from "./helpers/mcp-test-utils.js"; + +const emptyRequestContext = {} as Parameters< + ReturnType[string]["readCallback"] +>[1]; + +test("reactome://database/info static resource returns JSON content", async () => { + const server = createServer(); + const resources = getRegisteredResources(server); + const restoreGetText = stubMethod( + contentClient, + "getText", + (async (path: string) => { + if (path === "/data/database/name") { + return "Reactome"; + } + + if (path === "/data/database/version") { + return "92"; + } + + throw new Error(`Unexpected path ${path}`); + }) as typeof contentClient.getText + ); + + try { + const result = await resources["reactome://database/info"].readCallback( + new URL("reactome://database/info"), + emptyRequestContext + ); + const content = result.contents[0] as { text: string; mimeType?: string }; + const payload = JSON.parse(content.text); + + assert.equal(content.mimeType, "application/json"); + assert.deepEqual(payload, { name: "Reactome", version: 92 }); + } finally { + restoreGetText(); + } +}); + +test("reactome://pathway/{id} template returns pathway JSON for the requested identifier", async () => { + const server = createServer(); + const templates = getRegisteredResourceTemplates(server); + const restoreGet = stubMethod( + contentClient, + "get", + (async (path: string) => { + assert.equal(path, "/data/query/enhanced/R-HSA-141409"); + + return { + dbId: 141409, + stId: "R-HSA-141409", + displayName: "Hemostasis", + schemaClass: "Pathway", + }; + }) as typeof contentClient.get + ); + + try { + const result = await templates.pathway.readCallback( + new URL("reactome://pathway/R-HSA-141409"), + { id: "R-HSA-141409" }, + emptyRequestContext + ); + const content = result.contents[0] as { uri: string; text: string; mimeType?: string }; + const payload = JSON.parse(content.text); + + assert.equal(content.uri, "reactome://pathway/R-HSA-141409"); + assert.equal(content.mimeType, "application/json"); + assert.equal(payload.stId, "R-HSA-141409"); + assert.equal(payload.displayName, "Hemostasis"); + } finally { + restoreGet(); + } +}); diff --git a/test/run-tests.ts b/test/run-tests.ts new file mode 100644 index 0000000..3f64cbb --- /dev/null +++ b/test/run-tests.ts @@ -0,0 +1,3 @@ +import "./server-registration.test.js"; +import "./tools-smoke.test.js"; +import "./resources-smoke.test.js"; diff --git a/test/server-registration.test.ts b/test/server-registration.test.ts new file mode 100644 index 0000000..0f18502 --- /dev/null +++ b/test/server-registration.test.ts @@ -0,0 +1,31 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createServer } from "../src/server.js"; +import { + getRegisteredResources, + getRegisteredResourceTemplates, + getRegisteredTools, +} from "./helpers/mcp-test-utils.js"; + +test("createServer registers representative tools, resources, and templates", () => { + const server = createServer(); + + const tools = getRegisteredTools(server); + const resources = getRegisteredResources(server); + const templates = getRegisteredResourceTemplates(server); + + assert.ok(Object.keys(tools).length > 0); + assert.ok(Object.keys(resources).length > 0); + assert.ok(Object.keys(templates).length > 0); + + assert.ok(tools.reactome_search); + assert.ok(tools.reactome_get_pathway); + assert.ok(tools.reactome_get_analysis_result); + + assert.ok(resources["reactome://species"]); + assert.ok(resources["reactome://database/info"]); + + assert.ok(templates.pathway); + assert.ok(templates.analysis); + assert.equal(templates.pathway.resourceTemplate.uriTemplate.toString(), "reactome://pathway/{id}"); +}); diff --git a/test/tools-smoke.test.ts b/test/tools-smoke.test.ts new file mode 100644 index 0000000..4a34760 --- /dev/null +++ b/test/tools-smoke.test.ts @@ -0,0 +1,157 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { analysisClient } from "../src/clients/analysis.js"; +import { contentClient } from "../src/clients/content.js"; +import { createServer } from "../src/server.js"; +import { invokeTool, stubMethod } from "./helpers/mcp-test-utils.js"; + +test("reactome_search returns formatted search results from the content client", async () => { + const server = createServer(); + const restore = stubMethod( + contentClient, + "get", + (async (path: string, params?: Record) => { + assert.equal(path, "/search/query"); + assert.equal(params?.query, "BRCA1"); + assert.equal(params?.rows, 25); + assert.equal(params?.cluster, true); + + return { + results: [ + { + entriesCount: 1, + entries: [ + { + dbId: 123, + stId: "R-HSA-123", + exactType: "Pathway", + name: "BRCA1 DNA repair", + species: ["Homo sapiens"], + summation: "

DNA repair pathway summary.

", + }, + ], + }, + ], + }; + }) as typeof contentClient.get + ); + + try { + const result = await invokeTool(server, "reactome_search", { query: "BRCA1" }); + const text = (result as { content: Array<{ text: string }> }).content[0].text; + + assert.match(text, /## Search Results for "BRCA1"/); + assert.match(text, /\*\*BRCA1 DNA repair\*\* \(R-HSA-123\)/); + assert.match(text, /DNA repair pathway summary\./); + assert.doesNotMatch(text, /|

/); + } finally { + restore(); + } +}); + +test("reactome_get_pathway returns formatted pathway details", async () => { + const server = createServer(); + const restore = stubMethod( + contentClient, + "get", + (async (path: string) => { + assert.equal(path, "/data/query/enhanced/R-HSA-199420"); + + return { + dbId: 199420, + stId: "R-HSA-199420", + displayName: "Apoptosis", + schemaClass: "Pathway", + speciesName: "Homo sapiens", + hasDiagram: true, + isInDisease: false, + summation: [{ text: "Programmed cell death pathway." }], + }; + }) as typeof contentClient.get + ); + + try { + const result = await invokeTool(server, "reactome_get_pathway", { id: "R-HSA-199420" }); + const text = (result as { content: Array<{ text: string }> }).content[0].text; + + assert.match(text, /## Apoptosis/); + assert.match(text, /\*\*Stable ID:\*\* R-HSA-199420/); + assert.match(text, /\*\*Has diagram:\*\* Yes/); + assert.match(text, /Programmed cell death pathway\./); + } finally { + restore(); + } +}); + +test("reactome_get_analysis_result returns formatted analysis output from the analysis client", async () => { + const server = createServer(); + const restore = stubMethod( + analysisClient, + "get", + (async (path: string, params?: Record) => { + assert.equal(path, "/token/mock-token"); + assert.equal(params?.sortBy, "ENTITIES_PVALUE"); + assert.equal(params?.order, "ASC"); + assert.equal(params?.page, 1); + assert.equal(params?.pageSize, 25); + + return { + token: "mock-token", + summary: { + type: "OVERREPRESENTATION", + species: "Homo sapiens", + speciesName: "Homo sapiens", + }, + pathwaysFound: 1, + identifiersNotFound: 0, + pathways: [ + { + stId: "R-HSA-109581", + name: "Apoptosis", + entities: { + found: 3, + total: 100, + ratio: 0.03, + pValue: 0.00012, + fdr: 0.001, + }, + reactions: { + found: 2, + total: 30, + }, + }, + ], + }; + }) as typeof analysisClient.get + ); + + try { + const result = await invokeTool(server, "reactome_get_analysis_result", { token: "mock-token" }); + const text = (result as { content: Array<{ text: string }> }).content[0].text; + + assert.match(text, /## Analysis Result/); + assert.match(text, /\*\*Token:\*\* mock-token/); + assert.match(text, /\*\*Apoptosis\*\* \(R-HSA-109581\)/); + assert.match(text, /p-value: 1\.20e-4, FDR: 1\.00e-3/); + } finally { + restore(); + } +}); + +test("reactome_search rejects missing required query input", async () => { + const server = createServer(); + + await assert.rejects( + () => invokeTool(server, "reactome_search", {}), + /Invalid arguments for tool reactome_search/ + ); +}); + +test("reactome_get_analysis_result rejects blank analysis tokens", async () => { + const server = createServer(); + + await assert.rejects( + () => invokeTool(server, "reactome_get_analysis_result", { token: " " }), + /Invalid arguments for tool reactome_get_analysis_result/ + ); +}); diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..3cb5d55 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "./dist-test", + "declaration": false, + "declarationMap": false, + "sourceMap": false + }, + "include": ["src/**/*", "test/**/*"], + "exclude": ["node_modules", "dist", "dist-test"] +} From 5af181b2c01689822ef9a79a97a6b8d16171413b Mon Sep 17 00:00:00 2001 From: adidev001 Date: Thu, 19 Mar 2026 16:14:02 +0530 Subject: [PATCH 3/5] Add initial automated smoke tests --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 01e8b02..b294ae6 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ coverage/ # Temporary files *.tmp *.bak + +CHANGELOG.md +CHANGELOG.md From 3af3cfbaf7d46e13093c4a373e12f98158aaa44d Mon Sep 17 00:00:00 2001 From: Devansh Rai Date: Thu, 19 Mar 2026 16:20:24 +0530 Subject: [PATCH 4/5] Delete CHANGELOG.md --- CHANGELOG.md | 124 --------------------------------------------------- 1 file changed, 124 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 0b5d03b..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,124 +0,0 @@ -# Changelog - -## 2026-03-19 - -### Added - -#### Initial automated smoke test suite - -Files: -- `test/server-registration.test.ts` -- `test/tools-smoke.test.ts` -- `test/resources-smoke.test.ts` -- `test/helpers/mcp-test-utils.ts` -- `test/run-tests.ts` - -Why this was added: -- The project did not have automated coverage for MCP surface registration or basic handler behavior. -- These tests provide a fast offline check that the server still exposes expected tools, resources, and templates. -- The tests stub the content and analysis client boundaries, so contributors can run them without depending on live Reactome services. - -Purpose it serves: -- Catches accidental breakage in MCP registration. -- Verifies representative tool formatting and handler wiring. -- Verifies representative static resource and resource-template behavior. -- Gives contributors a small, readable starting point for expanding coverage. - -#### Test-specific TypeScript build config - -File: -- `tsconfig.test.json` - -Why this was added: -- The existing TypeScript config only compiled `src/`. -- Tests need to compile cleanly alongside source files without changing the production build output. - -Purpose it serves: -- Builds `src/` and `test/` into `dist-test/`. -- Keeps test artifacts separate from production artifacts in `dist/`. - -#### Server factory for testability - -File: -- `src/server.ts` - -Why this was added: -- Server construction and registration were previously trapped in the runtime entrypoint. -- Tests needed a clean way to create the MCP server without connecting stdio transport. - -Purpose it serves: -- Centralizes MCP server construction. -- Lets tests import and inspect a fully registered server. -- Preserves the runtime behavior while making initialization testable. - -### Changed - -#### Runtime entrypoint now uses shared server factory - -File: -- `src/index.ts` - -What changed: -- Replaced inline server creation with `createServer()`. - -Why this was changed: -- To avoid duplicating server construction logic between runtime and tests. - -Purpose it serves: -- Keeps production startup behavior unchanged. -- Ensures runtime and tests use the same registration path. - -#### Package scripts now support test compilation and execution - -File: -- `package.json` - -What changed: -- Added `build:test`. -- Added `test`. - -Why this was changed: -- Contributors need a single command to build and run the new automated suite. - -Purpose it serves: -- `npm test` now runs the smoke suite. -- Keeps the workflow simple and contributor-friendly. - -#### Ignore test build artifacts - -File: -- `.gitignore` - -What changed: -- Added `dist-test/`. - -Why this was changed: -- The new test build produces compiled output separate from `dist/`. - -Purpose it serves: -- Prevents generated test artifacts from being committed accidentally. - -#### Input validation tightened for clearer failure behavior - -Files: -- `src/tools/search.ts` -- `src/tools/analysis.ts` - -What changed: -- Search query inputs now require a non-empty trimmed string in representative search tools. -- Analysis token inputs now require a non-empty trimmed string in token-based analysis tools. - -Why this was changed: -- Empty strings were previously accepted by schema validation, which made invalid-input behavior less clear. -- The test suite needed deterministic validation coverage for common bad inputs. - -Purpose it serves: -- Produces clearer failures for missing/blank search queries. -- Produces clearer failures for missing/blank analysis tokens. -- Improves behavior without changing the core tool output shape. - -### Notes - -- The automated suite uses `node:test` and `node:assert/strict` to stay lightweight and compatible with the repository's Node support target. -- Tests are intentionally small smoke tests, not full endpoint-by-endpoint coverage. -- External Reactome network calls are avoided in automated tests by stubbing the client-layer methods used by handlers. From 337f2b6321efa2c4d0205b7e0ab6c5c9e15fb5ea Mon Sep 17 00:00:00 2001 From: Devansh Rai Date: Thu, 19 Mar 2026 16:20:41 +0530 Subject: [PATCH 5/5] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index b294ae6..6867d74 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,3 @@ coverage/ *.bak CHANGELOG.md -CHANGELOG.md