This is page 1 of 5. Use http://codebase.md/cyanheads/pubmed-mcp-server?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .clinerules
│ └── clinerules.md
├── .dockerignore
├── .github
│ ├── FUNDING.yml
│ └── workflows
│ └── publish.yml
├── .gitignore
├── .ncurc.json
├── CHANGELOG.md
├── Dockerfile
├── docs
│ ├── project-spec.md
│ ├── publishing-mcp-server-registry.md
│ └── tree.md
├── eslint.config.js
├── examples
│ ├── generate_pubmed_chart
│ │ ├── bar_chart.png
│ │ ├── doughnut_chart.png
│ │ ├── line_chart.png
│ │ ├── pie_chart.png
│ │ ├── polar_chart.png
│ │ ├── radar_chart.png
│ │ └── scatter_plot.png
│ ├── pubmed_article_connections_1.md
│ ├── pubmed_article_connections_2.md
│ ├── pubmed_fetch_contents_example.md
│ ├── pubmed_research_agent_example.md
│ └── pubmed_search_articles_example.md
├── LICENSE
├── mcp.json
├── package-lock.json
├── package.json
├── README.md
├── repomix.config.json
├── scripts
│ ├── clean.ts
│ ├── fetch-openapi-spec.ts
│ ├── make-executable.ts
│ ├── tree.ts
│ └── validate-mcp-publish-schema.ts
├── server.json
├── smithery.yaml
├── src
│ ├── config
│ │ └── index.ts
│ ├── index.ts
│ ├── mcp-server
│ │ ├── server.ts
│ │ ├── tools
│ │ │ ├── pubmedArticleConnections
│ │ │ │ ├── index.ts
│ │ │ │ ├── logic
│ │ │ │ │ ├── citationFormatter.ts
│ │ │ │ │ ├── elinkHandler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── types.ts
│ │ │ │ └── registration.ts
│ │ │ ├── pubmedFetchContents
│ │ │ │ ├── index.ts
│ │ │ │ ├── logic.ts
│ │ │ │ └── registration.ts
│ │ │ ├── pubmedGenerateChart
│ │ │ │ ├── index.ts
│ │ │ │ ├── logic.ts
│ │ │ │ └── registration.ts
│ │ │ ├── pubmedResearchAgent
│ │ │ │ ├── index.ts
│ │ │ │ ├── logic
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── inputSchema.ts
│ │ │ │ │ ├── outputTypes.ts
│ │ │ │ │ └── planOrchestrator.ts
│ │ │ │ ├── logic.ts
│ │ │ │ └── registration.ts
│ │ │ └── pubmedSearchArticles
│ │ │ ├── index.ts
│ │ │ ├── logic.ts
│ │ │ └── registration.ts
│ │ └── transports
│ │ ├── auth
│ │ │ ├── authFactory.ts
│ │ │ ├── authMiddleware.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ ├── authContext.ts
│ │ │ │ ├── authTypes.ts
│ │ │ │ └── authUtils.ts
│ │ │ └── strategies
│ │ │ ├── authStrategy.ts
│ │ │ ├── jwtStrategy.ts
│ │ │ └── oauthStrategy.ts
│ │ ├── core
│ │ │ ├── baseTransportManager.ts
│ │ │ ├── headerUtils.ts
│ │ │ ├── honoNodeBridge.ts
│ │ │ ├── statefulTransportManager.ts
│ │ │ ├── statelessTransportManager.ts
│ │ │ └── transportTypes.ts
│ │ ├── http
│ │ │ ├── httpErrorHandler.ts
│ │ │ ├── httpTransport.ts
│ │ │ ├── httpTypes.ts
│ │ │ ├── index.ts
│ │ │ └── mcpTransportMiddleware.ts
│ │ └── stdio
│ │ ├── index.ts
│ │ └── stdioTransport.ts
│ ├── services
│ │ └── NCBI
│ │ ├── core
│ │ │ ├── ncbiConstants.ts
│ │ │ ├── ncbiCoreApiClient.ts
│ │ │ ├── ncbiRequestQueueManager.ts
│ │ │ ├── ncbiResponseHandler.ts
│ │ │ └── ncbiService.ts
│ │ └── parsing
│ │ ├── eSummaryResultParser.ts
│ │ ├── index.ts
│ │ ├── pubmedArticleStructureParser.ts
│ │ └── xmlGenericHelpers.ts
│ ├── types-global
│ │ ├── declarations.d.ts
│ │ ├── errors.ts
│ │ └── pubmedXml.ts
│ └── utils
│ ├── index.ts
│ ├── internal
│ │ ├── errorHandler.ts
│ │ ├── index.ts
│ │ ├── logger.ts
│ │ ├── performance.ts
│ │ └── requestContext.ts
│ ├── metrics
│ │ ├── index.ts
│ │ └── tokenCounter.ts
│ ├── network
│ │ ├── fetchWithTimeout.ts
│ │ └── index.ts
│ ├── parsing
│ │ ├── dateParser.ts
│ │ ├── index.ts
│ │ └── jsonParser.ts
│ ├── scheduling
│ │ ├── index.ts
│ │ └── scheduler.ts
│ ├── security
│ │ ├── idGenerator.ts
│ │ ├── index.ts
│ │ ├── rateLimiter.ts
│ │ └── sanitization.ts
│ └── telemetry
│ ├── instrumentation.ts
│ └── semconv.ts
├── tsconfig.json
├── tsconfig.typedoc.json
├── tsdoc.json
└── typedoc.json
```
# Files
--------------------------------------------------------------------------------
/.ncurc.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "reject": ["chrono-node", "dotenv", "zod", "@hono/node-server", "hono"]
3 | }
4 |
```
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
```
1 | node_modules
2 | npm-debug.log
3 | .npm
4 | .nyc_output
5 | coverage
6 | .git
7 | .gitignore
8 | README.md
9 | .env
10 | .env.local
11 | .env.development.local
12 | .env.test.local
13 | .env.production.local
14 | dist
15 | *.log
16 | .DS_Store
17 | Thumbs.db
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # =============================================================================
2 | # OPERATING SYSTEM FILES
3 | # =============================================================================
4 | .DS_Store
5 | .DS_Store?
6 | ._*
7 | .Spotlight-V100
8 | .Trashes
9 | ehthumbs.db
10 | Thumbs.db
11 |
12 | # =============================================================================
13 | # IDE AND EDITOR FILES
14 | # =============================================================================
15 | .idea/
16 | *.swp
17 | *.swo
18 | *~
19 | *.sublime-workspace
20 | *.sublime-project
21 | .history/
22 |
23 | # =============================================================================
24 | # NODE.JS & PACKAGE MANAGERS
25 | # =============================================================================
26 | node_modules/
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 | .pnpm-debug.log*
31 | .npm
32 | .pnp.js
33 | .pnp.cjs
34 | .pnp.mjs
35 | .pnp.json
36 | .pnp.ts
37 |
38 | # =============================================================================
39 | # TYPESCRIPT & JAVASCRIPT
40 | # =============================================================================
41 | *.tsbuildinfo
42 | .tscache/
43 | *.js.map
44 | *.mjs.map
45 | *.cjs.map
46 | *.d.ts.map
47 | *.d.ts
48 | !*.d.ts.template
49 | *.tgz
50 | .eslintcache
51 | .rollup.cache
52 |
53 | # =============================================================================
54 | # PYTHON
55 | # =============================================================================
56 | __pycache__/
57 | *.py[cod]
58 | *$py.class
59 | *.so
60 | .Python
61 | develop-eggs/
62 | eggs/
63 | .eggs/
64 | parts/
65 | sdist/
66 | var/
67 | wheels/
68 | *.egg-info/
69 | .installed.cfg
70 | *.egg
71 | .pytest_cache/
72 | .coverage
73 | htmlcov/
74 | .tox/
75 | .venv
76 | venv/
77 | ENV/
78 |
79 | # =============================================================================
80 | # JAVA
81 | # =============================================================================
82 | *.class
83 | *.jar
84 | *.war
85 | *.nar
86 | *.ear
87 | hs_err_pid*
88 | target/
89 | .gradle/
90 |
91 | # =============================================================================
92 | # RUBY
93 | # =============================================================================
94 | *.gem
95 | *.rbc
96 | /.config
97 | /coverage/
98 | /InstalledFiles
99 | /pkg/
100 | /spec/reports/
101 | /test/tmp/
102 | /test/version_tmp/
103 | /tmp/
104 | .byebug_history
105 |
106 | # =============================================================================
107 | # BUILD & DISTRIBUTION
108 | # =============================================================================
109 | build/
110 | dist/
111 | out/
112 |
113 | # =============================================================================
114 | # COMPILED FILES
115 | # =============================================================================
116 | *.com
117 | *.dll
118 | *.exe
119 | *.o
120 |
121 | # =============================================================================
122 | # PACKAGE & ARCHIVE FILES
123 | # =============================================================================
124 | *.7z
125 | *.dmg
126 | *.gz
127 | *.iso
128 | *.rar
129 | *.tar
130 | *.tar.gz
131 | *.zip
132 |
133 | # =============================================================================
134 | # LOGS & DATABASES
135 | # =============================================================================
136 | *.log
137 | *.sql
138 | *.sqlite
139 | *.sqlite3
140 | logs/
141 |
142 | # =============================================================================
143 | # TESTING & COVERAGE
144 | # =============================================================================
145 | coverage/
146 | .nyc_output/
147 |
148 | # =============================================================================
149 | # CACHE & TEMPORARY FILES
150 | # =============================================================================
151 | .cache/
152 | .parcel-cache/
153 | *.bak
154 |
155 | # =============================================================================
156 | # ENVIRONMENT & CONFIGURATION
157 | # =============================================================================
158 | .env
159 | .env.local
160 | .env.development.local
161 | .env.test.local
162 | .env.production.local
163 | .sample-env
164 | !sample.template.*
165 | mcp-servers.json
166 | mcp-config.json
167 | .wrangler
168 | worker-configuration.d.ts
169 |
170 | # =============================================================================
171 | # DEMO & EXAMPLE DIRECTORIES
172 | # =============================================================================
173 |
174 | # =============================================================================
175 | # GENERATED DOCUMENTATION
176 | # =============================================================================
177 | docs/api/
178 |
179 | # =============================================================================
180 | # APPLICATION SPECIFIC
181 | # =============================================================================
182 | .storage/
183 | repomix-output*
184 | duckdata/
185 | .claude
186 | data/
187 | docs/devdocs.md
188 |
189 | # =============================================================================
190 | # MCP REGISTRY
191 | # =============================================================================
192 | .mcpregistry_github_token
193 | .mcpregistry_registry_token
194 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | <div align="center">
2 |
3 | # pubmed-mcp-server
4 |
5 | **Empower your AI agents and research tools with seamless PubMed integration!**
6 |
7 | [](https://www.typescriptlang.org/)
8 | [](https://modelcontextprotocol.io/)
9 | [](./CHANGELOG.md)
10 | [](https://opensource.org/licenses/Apache-2.0)
11 | [](https://github.com/cyanheads/pubmed-mcp-server/issues)
12 | [](https://github.com/cyanheads/pubmed-mcp-server)
13 |
14 | </div>
15 |
16 | A production-grade Model Context Protocol (MCP) server that empowers AI agents and research tools with comprehensive access to PubMed. Enables advanced, automated workflows for searching, retrieving, analyzing, and visualizing biomedical and scientific literature via NCBI E-utilities.
17 |
18 | Built on the [`cyanheads/mcp-ts-template`](https://github.com/cyanheads/mcp-ts-template), this server follows a modular architecture with robust error handling, logging, and security features.
19 |
20 | ## 🚀 Core Capabilities: PubMed Tools 🛠️
21 |
22 | This server equips your AI with specialized tools to interact with PubMed:
23 |
24 | | Tool Name | Description | Example |
25 | | :------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- |
26 | | [`pubmed_search_articles`](./src/mcp-server/tools/pubmedSearchArticles/) | Searches PubMed for articles based on your query. | [View Example](./examples/pubmed_search_articles_example.md) |
27 | | [`pubmed_fetch_contents`](./src/mcp-server/tools/pubmedFetchContents/) | Retrieves detailed information for PubMed articles. | [View Example](./examples/pubmed_fetch_contents_example.md) |
28 | | [`pubmed_article_connections`](./src/mcp-server/tools/pubmedArticleConnections/) | Finds related articles (cited by, similar, references) or formats citations for a PMID. | [Ex. 1](./examples/pubmed_article_connections_1.md), [Ex. 2](./examples/pubmed_article_connections_2.md) |
29 | | [`pubmed_research_agent`](./src/mcp-server/tools/pubmedResearchAgent/) | Generates a standardized JSON research plan outline from component details. | [View Example](./examples/pubmed_research_agent_example.md) |
30 | | [`pubmed_generate_chart`](./src/mcp-server/tools/pubmedGenerateChart/) | Generates a chart image (PNG) from given input data. | [View Examples](./examples/generate_pubmed_chart/) |
31 |
32 | ---
33 |
34 | ## Table of Contents
35 |
36 | | [Overview](#overview) | [Features](#features) | [Installation](#installation) |
37 | | :------------------------------ | :--------------------------------------------- | :---------------------------- |
38 | | [Configuration](#configuration) | [Project Structure](#project-structure) |
39 | | [Tools](#tools) | [Development & Testing](#development--testing) | [License](#license) |
40 |
41 | ## Overview
42 |
43 | The PubMed MCP Server acts as a bridge, allowing applications (MCP Clients) that understand the Model Context Protocol (MCP) – like advanced AI assistants (LLMs), IDE extensions, or custom research tools – to interact directly and efficiently with PubMed's vast biomedical literature database.
44 |
45 | Instead of complex API integration or manual searches, your tools can leverage this server to:
46 |
47 | - **Automate research workflows**: Search literature, fetch full article metadata, track citations, and generate research plans programmatically.
48 | - **Gain research insights**: Access detailed publication data, author information, journal details, MeSH terms, and citation networks without leaving the host application.
49 | - **Integrate PubMed into AI-driven research**: Enable LLMs to conduct literature reviews, analyze research trends, and support evidence-based decision making.
50 | - **Visualize research data**: Generate charts and visualizations from publication metadata and search results.
51 |
52 | Built on the robust `mcp-ts-template`, this server provides a standardized, secure, and efficient way to expose PubMed functionality via the MCP standard. It achieves this by integrating with NCBI's E-utilities API, ensuring compliance with rate limits and providing comprehensive error handling.
53 |
54 | > **Developer Note**: This repository includes a [.clinerules](.clinerules) file that serves as a developer cheat sheet for your LLM coding agent with quick reference for the codebase patterns, file locations, and code snippets.
55 |
56 | ## Features
57 |
58 | ### Core Utilities
59 |
60 | Leverages the robust utilities provided by the `mcp-ts-template`:
61 |
62 | - **Logging**: Structured, configurable logging (file rotation, stdout JSON, MCP notifications) with sensitive data redaction.
63 | - **Error Handling**: Centralized error processing, standardized error types (`McpError`), and automatic logging.
64 | - **Configuration**: Environment variable loading (`dotenv`) with comprehensive validation using Zod.
65 | - **Input Validation/Sanitization**: Uses `zod` for schema validation and custom sanitization logic.
66 | - **Request Context**: Tracking and correlation of operations via unique request IDs using `AsyncLocalStorage`.
67 | - **Type Safety**: Strong typing enforced by TypeScript and Zod schemas.
68 | - **HTTP Transport**: High-performance HTTP server using **Hono**, featuring session management and authentication support.
69 | - **Authentication**: Robust authentication layer supporting JWT and OAuth 2.1, with fine-grained scope enforcement.
70 | - **Deployment**: Multi-stage `Dockerfile` for creating small, secure production images with native dependency support.
71 |
72 | ### PubMed Integration
73 |
74 | - **NCBI E-utilities Integration**: Comprehensive access to ESearch, EFetch, ELink, and ESummary APIs with automatic XML parsing.
75 | - **Advanced Search Capabilities**: Complex query construction with date ranges, publication types, author filters, and MeSH term support.
76 | - **Full Article Metadata**: Retrieve complete publication data including abstracts, authors, affiliations, journal information, DOIs, and citation data.
77 | - **Citation Network Analysis**: Find related articles, citing articles, and reference lists through ELink integration.
78 | - **Research Planning**: Generate structured research plans with automated literature search strategies.
79 | - **Data Visualization**: Create PNG charts from publication metadata (bar, line, scatter, pie, bubble, radar, polarArea).
80 | - **Multiple Output Formats**: Support for JSON, MEDLINE text, full XML, and formatted citations (RIS, BibTeX, APA, MLA).
81 | - **Batch Processing**: Efficient handling of multiple PMIDs with pagination support.
82 |
83 | ## Installation
84 |
85 | ### Prerequisites
86 |
87 | - [Node.js (>=20.0.0)](https://nodejs.org/)
88 | - [npm](https://www.npmjs.com/) (comes with Node.js)
89 | - **NCBI API Key** (recommended for higher rate limits) - [Get one here](https://ncbiinsights.ncbi.nlm.nih.gov/2017/11/02/new-api-keys-for-the-e-utilities/)
90 |
91 | ### MCP Client Settings
92 |
93 | Add the following to your MCP client's configuration file (e.g., `cline_mcp_settings.json`).
94 | This configuration uses `npx` to run the server, which will automatically install the package if not already present.
95 | All environment variables are optional, but recommended for production use. NCBI API key is recommended to avoid rate limiting issues.
96 |
97 | ```json
98 | {
99 | "mcpServers": {
100 | "pubmed-mcp-server": {
101 | "command": "npx",
102 | "args": ["@cyanheads/pubmed-mcp-server"],
103 | "env": {
104 | "MCP_LOG_LEVEL": "debug",
105 | "MCP_TRANSPORT_TYPE": "http",
106 | "MCP_HTTP_PORT": "3017",
107 | "NCBI_API_KEY": "YOUR_NCBI_API_KEY_HERE"
108 | }
109 | }
110 | }
111 | }
112 | ```
113 |
114 | ### If running manually (not via MCP client for development or testing)
115 |
116 | #### Install via npm
117 |
118 | ```bash
119 | npm install @cyanheads/pubmed-mcp-server
120 | ```
121 |
122 | #### Alternatively Install from Source
123 |
124 | 1. Clone the repository:
125 | ```bash
126 | git clone https://github.com/cyanheads/pubmed-mcp-server.git
127 | cd pubmed-mcp-server
128 | ```
129 | 2. Install dependencies:
130 | ```bash
131 | npm install
132 | ```
133 | 3. Build the project:
134 | ```bash
135 | npm run build
136 | ```
137 |
138 | ## Configuration
139 |
140 | ### Environment Variables
141 |
142 | Configure the server using environment variables. For local development, these can be set in a `.env` file at the project root or directly in your environment. Otherwise, you can set them in your MCP client configuration as shown above.
143 |
144 | | Variable | Description | Default |
145 | | :-------------------- | :--------------------------------------------------------------------------------------- | :------------ |
146 | | `MCP_TRANSPORT_TYPE` | Transport mechanism: `stdio` or `http`. | `stdio` |
147 | | `MCP_HTTP_PORT` | Port for the HTTP server (if `MCP_TRANSPORT_TYPE=http`). | `3017` |
148 | | `MCP_HTTP_HOST` | Host address for the HTTP server (if `MCP_TRANSPORT_TYPE=http`). | `127.0.0.1` |
149 | | `MCP_ALLOWED_ORIGINS` | Comma-separated list of allowed origins for CORS (if `MCP_TRANSPORT_TYPE=http`). | (none) |
150 | | `MCP_LOG_LEVEL` | Logging level (`debug`, `info`, `notice`, `warning`, `error`, `crit`, `alert`, `emerg`). | `debug` |
151 | | `MCP_AUTH_MODE` | Authentication mode for HTTP: `jwt` or `oauth`. | `jwt` |
152 | | `MCP_AUTH_SECRET_KEY` | **Required for `jwt` auth.** Minimum 32-character secret key for JWT authentication. | (none) |
153 | | `NCBI_API_KEY` | **Recommended.** Your NCBI API Key for higher rate limits and reliable access. | (none) |
154 | | `LOGS_DIR` | Directory for log file storage. | `logs/` |
155 | | `NODE_ENV` | Runtime environment (`development`, `production`). | `development` |
156 |
157 | ## Project Structure
158 |
159 | The codebase follows a modular structure within the `src/` directory:
160 |
161 | ```
162 | src/
163 | ├── index.ts # Entry point: Initializes and starts the server
164 | ├── config/ # Configuration loading (env vars, package info)
165 | │ └── index.ts
166 | ├── mcp-server/ # Core MCP server logic and capability registration
167 | │ ├── server.ts # Server setup, capability registration
168 | │ ├── transports/ # Transport handling (stdio, http)
169 | │ └── tools/ # MCP Tool implementations (subdirs per tool)
170 | ├── services/ # External service integrations
171 | │ └── NCBI/ # NCBI E-utilities API client and parsing
172 | ├── types-global/ # Shared TypeScript type definitions
173 | └── utils/ # Common utility functions (logger, error handler, etc.)
174 | ```
175 |
176 | For a detailed file tree, run `npm run tree` or see [docs/tree.md](docs/tree.md).
177 |
178 | ## Tools
179 |
180 | The PubMed MCP Server provides a comprehensive suite of tools for biomedical literature research, callable via the Model Context Protocol.
181 |
182 | | Tool Name | Description | Key Arguments |
183 | | :--------------------------- | :--------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------- |
184 | | `pubmed_search_articles` | Searches PubMed for articles using queries, filters, and date ranges. | `queryTerm`, `maxResults?`, `sortBy?`, `dateRange?`, `filterByPublicationTypes?`, `fetchBriefSummaries?` |
185 | | `pubmed_fetch_contents` | Fetches detailed article information using PMIDs or search history. | `pmids?`, `queryKey?`, `webEnv?`, `detailLevel?`, `includeMeshTerms?`, `includeGrantInfo?` |
186 | | `pubmed_article_connections` | Finds related articles, citations, and references for a given PMID. | `sourcePmid`, `relationshipType?`, `maxRelatedResults?`, `citationStyles?` |
187 | | `pubmed_research_agent` | Generates structured research plans with literature search strategies. | `project_title_suggestion`, `primary_research_goal`, `research_keywords`, `organism_focus?`, `p1_*`, etc. |
188 | | `pubmed_generate_chart` | Creates customizable PNG charts from structured publication data. | `chartType`, `dataValues`, `xField`, `yField`, `title?`, `seriesField?`, `sizeField?` |
189 |
190 | _Note: All tools support comprehensive error handling and return structured JSON responses._
191 |
192 | ## Examples
193 |
194 | Comprehensive usage examples for each tool are available in the [`examples/`](examples/) directory.
195 |
196 | - **`pubmed_search_articles`**: [View Example](./examples/pubmed_search_articles_example.md)
197 | - **`pubmed_fetch_contents`**: [View Example](./examples/pubmed_fetch_contents_example.md)
198 | - **`pubmed_article_connections`**: [Ex. 1](./examples/pubmed_article_connections_1.md), [Ex. 2](./examples/pubmed_article_connections_2.md)
199 | - **`pubmed_research_agent`**: [View Example](./examples/pubmed_research_agent_example.md)
200 | - **`pubmed_generate_chart`**: [View Examples](./examples/generate_pubmed_chart/)
201 |
202 | ## Development & Testing
203 |
204 | ### Development Scripts
205 |
206 | ```bash
207 | # Build the project (compile TS to JS in dist/ and make executable)
208 | npm run build
209 |
210 | # Clean build artifacts
211 | npm run clean
212 |
213 | # Clean build artifacts and then rebuild the project
214 | npm run rebuild
215 |
216 | # Lint the codebase
217 | npm run lint
218 |
219 | # Format code with Prettier
220 | npm run format
221 |
222 | # Generate a file tree representation for documentation
223 | npm run tree
224 | ```
225 |
226 | ### Running the Server
227 |
228 | ```bash
229 | # Start the server using stdio (default)
230 | npm start
231 | # Or explicitly:
232 | npm run start:stdio
233 |
234 | # Start the server using HTTP transport
235 | npm run start:http
236 |
237 | # Test the server locally using the MCP inspector tool (stdio transport)
238 | npm run inspector
239 |
240 | # Test the server locally using the MCP inspector tool (http transport)
241 | npm run inspector:http
242 | ```
243 |
244 | ## License
245 |
246 | This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
247 |
248 | ---
249 |
250 | <div align="center">
251 | Built with the <a href="https://modelcontextprotocol.io/">Model Context Protocol</a>
252 | </div>
253 |
```
--------------------------------------------------------------------------------
/src/types-global/declarations.d.ts:
--------------------------------------------------------------------------------
```typescript
1 | declare module "citation-js";
2 |
```
--------------------------------------------------------------------------------
/tsconfig.typedoc.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "rootDir": "."
5 | },
6 | "include": ["src/**/*", "scripts/**/*.ts"]
7 | // The 'exclude' is also inherited.
8 | }
9 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/stdio/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for the Stdio transport module.
3 | * @module src/mcp-server/transports/stdio/index
4 | */
5 |
6 | export { startStdioTransport } from "./stdioTransport.js";
7 |
```
--------------------------------------------------------------------------------
/src/utils/scheduling/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for the scheduling module.
3 | * Exports the singleton schedulerService for application-wide use.
4 | * @module src/utils/scheduling
5 | */
6 |
7 | export * from "./scheduler.js";
8 |
```
--------------------------------------------------------------------------------
/src/utils/network/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for network utilities.
3 | * @module src/utils/network/index
4 | */
5 |
6 | export * from "./fetchWithTimeout.js";
7 | export type { FetchWithTimeoutOptions } from "./fetchWithTimeout.js"; // Explicitly re-exporting type
8 |
```
--------------------------------------------------------------------------------
/src/utils/metrics/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for metrics-related utility modules.
3 | * This file re-exports utilities for collecting and processing metrics,
4 | * such as token counting.
5 | * @module src/utils/metrics
6 | */
7 |
8 | export * from "./tokenCounter.js";
9 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedFetchContents/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for the pubmed_fetch_contents tool.
3 | * Exports the tool's registration function.
4 | * @module src/mcp-server/tools/pubmedFetchContents/index
5 | */
6 |
7 | export { registerPubMedFetchContentsTool } from "./registration.js";
8 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedResearchAgent/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for the pubmed_research_agent tool.
3 | * Exports the tool's registration function.
4 | * @module src/mcp-server/tools/pubmedResearchAgent/index
5 | */
6 |
7 | export { registerPubMedResearchAgentTool } from "./registration.js";
8 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedSearchArticles/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for the pubmedSearchArticles tool.
3 | * Exports the tool's registration function.
4 | * @module src/mcp-server/tools/pubmedSearchArticles/index
5 | */
6 |
7 | export { registerPubMedSearchArticlesTool } from "./registration.js";
8 |
```
--------------------------------------------------------------------------------
/src/utils/parsing/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for parsing utility modules.
3 | * This file re-exports utilities related to parsing various data formats,
4 | * such as JSON and dates.
5 | * @module src/utils/parsing
6 | */
7 |
8 | export * from "./dateParser.js";
9 | export * from "./jsonParser.js";
10 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedArticleConnections/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for the pubmedArticleConnections tool.
3 | * Exports the registration function for this tool.
4 | * @module src/mcp-server/tools/pubmedArticleConnections/index
5 | */
6 |
7 | export { registerPubMedArticleConnectionsTool } from "./registration.js";
8 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedGenerateChart/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel export for the 'pubmed_generate_chart' tool.
3 | * This file re-exports the registration function for the tool,
4 | * making it easier to import and register with the MCP server.
5 | * @module src/mcp-server/tools/pubmedGenerateChart/index
6 | */
7 | export * from "./registration.js";
8 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/http/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for the HTTP transport module.
3 | * @module src/mcp-server/transports/http/index
4 | */
5 |
6 | export { createHttpApp, startHttpTransport } from "./httpTransport.js";
7 | export { httpErrorHandler } from "./httpErrorHandler.js";
8 | export type { HonoNodeBindings } from "./httpTypes.js";
9 |
```
--------------------------------------------------------------------------------
/src/utils/security/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for security-related utility modules.
3 | * This file re-exports utilities for input sanitization, rate limiting,
4 | * and ID generation.
5 | * @module src/utils/security
6 | */
7 |
8 | export * from "./idGenerator.js";
9 | export * from "./rateLimiter.js";
10 | export * from "./sanitization.js";
11 |
```
--------------------------------------------------------------------------------
/src/services/NCBI/parsing/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for NCBI XML parsing helper utilities.
3 | * Re-exports functions from more specific parser modules.
4 | * @module src/services/NCBI/parsing/index
5 | */
6 |
7 | export * from "./xmlGenericHelpers.js";
8 | export * from "./pubmedArticleStructureParser.js";
9 | export * from "./eSummaryResultParser.js";
10 |
```
--------------------------------------------------------------------------------
/typedoc.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://typedoc.org/schema.json",
3 | "entryPoints": ["src", "scripts"],
4 | "entryPointStrategy": "expand",
5 | "out": "docs/api",
6 | "readme": "README.md",
7 | "name": "mcp-ts-template API Documentation",
8 | "includeVersion": true,
9 | "excludePrivate": true,
10 | "excludeProtected": true,
11 | "excludeInternal": true,
12 | "theme": "default"
13 | }
14 |
```
--------------------------------------------------------------------------------
/src/utils/internal/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for internal utility modules.
3 | * This file re-exports core internal utilities related to error handling,
4 | * logging, and request context management.
5 | * @module src/utils/internal
6 | */
7 |
8 | export * from "./errorHandler.js";
9 | export * from "./logger.js";
10 | export * from "./performance.js";
11 | export * from "./requestContext.js";
12 |
```
--------------------------------------------------------------------------------
/mcp.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "mcpServers": {
3 | "pubmed-mcp-server": {
4 | "command": "npx",
5 | "args": ["@cyanheads/pubmed-mcp-server"],
6 | "env": {
7 | "MCP_LOG_LEVEL": "debug",
8 | "MCP_TRANSPORT_TYPE": "http",
9 | "MCP_HTTP_PORT": "3017",
10 | "NCBI_API_KEY": "YOUR_NCBI_API_KEY_HERE",
11 | "MCP_HTTP_HOST": "0.0.0.0",
12 | "MCP_SESSION_MODE": "stateless"
13 | }
14 | }
15 | }
16 | }
17 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedResearchAgent/logic/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel export file for the pubmed_research_agent tool's core logic.
3 | * @module pubmedResearchAgent/logic/index
4 | */
5 |
6 | export * from "./inputSchema.js";
7 | export * from "./outputTypes.js";
8 | export * from "./planOrchestrator.js";
9 | // Individual section prompt generators are not typically exported directly from here,
10 | // as they are used internally by the planOrchestrator.
11 |
```
--------------------------------------------------------------------------------
/repomix.config.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "output": {
3 | "filePath": "repomix-output.xml",
4 | "style": "xml",
5 | "removeComments": false,
6 | "removeEmptyLines": false,
7 | "topFilesLength": 5,
8 | "showLineNumbers": false,
9 | "copyToClipboard": false
10 | },
11 | "include": [],
12 | "ignore": {
13 | "useGitignore": true,
14 | "useDefaultPatterns": true,
15 | "customPatterns": [".clinerules"]
16 | },
17 | "security": {
18 | "enableSecurityCheck": true
19 | }
20 | }
21 |
```
--------------------------------------------------------------------------------
/src/utils/telemetry/semconv.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Defines local OpenTelemetry semantic convention constants to ensure
3 | * stability and avoid dependency conflicts with different versions of
4 | * `@opentelemetry/semantic-conventions`.
5 | * @module src/utils/telemetry/semconv
6 | */
7 |
8 | /**
9 | * The method or function name, or equivalent (usually rightmost part of the code unit's name).
10 | */
11 | export const ATTR_CODE_FUNCTION = "code.function";
12 |
13 | /**
14 | * The "namespace" within which `code.function` is defined.
15 | * Usually the qualified class or module name, etc.
16 | */
17 | export const ATTR_CODE_NAMESPACE = "code.namespace";
18 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/http/httpTypes.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Defines custom types for the Hono HTTP transport layer.
3 | * @module src/mcp-server/transports/http/httpTypes
4 | */
5 |
6 | import type { IncomingMessage, ServerResponse } from "http";
7 |
8 | /**
9 | * Extends Hono's Bindings to include the raw Node.js request and response objects.
10 | * This is necessary for integrating with libraries like the MCP SDK that
11 | * need to write directly to the response stream.
12 | *
13 | * As per `@hono/node-server`, the response object is available on `c.env.outgoing`.
14 | */
15 | export type HonoNodeBindings = {
16 | incoming: IncomingMessage;
17 | outgoing: ServerResponse;
18 | };
19 |
```
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Publish Package to npm
2 | on:
3 | push:
4 | tags:
5 | - "v*"
6 |
7 | jobs:
8 | build-and-publish:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | contents: read
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - name: Setup Node.js
16 | uses: actions/setup-node@v4
17 | with:
18 | node-version: "20.x"
19 | registry-url: "https://registry.npmjs.org"
20 | cache: "npm"
21 |
22 | - name: Install dependencies
23 | run: npm ci
24 |
25 | - name: Build
26 | run: npm run build
27 |
28 | - name: Publish to npm
29 | run: npm publish
30 | env:
31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
32 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/auth/lib/authTypes.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Shared types for authentication middleware.
3 | * @module src/mcp-server/transports/auth/core/auth.types
4 | */
5 |
6 | import type { AuthInfo as SdkAuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
7 |
8 | /**
9 | * Defines the structure for authentication information derived from a token.
10 | * It extends the base SDK type to include common optional claims.
11 | */
12 | export type AuthInfo = SdkAuthInfo & {
13 | subject?: string;
14 | };
15 |
16 | // The declaration for `http.IncomingMessage` is no longer needed here,
17 | // as the new architecture avoids direct mutation where possible and handles
18 | // the attachment within the Hono context.
19 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/auth/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for the auth module.
3 | * Exports core utilities and middleware strategies for easier imports.
4 | * @module src/mcp-server/transports/auth/index
5 | */
6 |
7 | export { authContext } from "./lib/authContext.js";
8 | export { withRequiredScopes } from "./lib/authUtils.js";
9 | export type { AuthInfo } from "./lib/authTypes.js";
10 |
11 | export { createAuthStrategy } from "./authFactory.js";
12 | export { createAuthMiddleware } from "./authMiddleware.js";
13 | export type { AuthStrategy } from "./strategies/authStrategy.js";
14 | export { JwtStrategy } from "./strategies/jwtStrategy.js";
15 | export { OauthStrategy } from "./strategies/oauthStrategy.js";
16 |
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
1 | import pluginJs from "@eslint/js";
2 | import globals from "globals";
3 | import tseslint from "typescript-eslint";
4 |
5 | const combinedGlobals = { ...globals.browser, ...globals.node };
6 | const trimmedGlobals = Object.fromEntries(
7 | Object.entries(combinedGlobals).map(([key, value]) => [key.trim(), value]),
8 | );
9 |
10 | export default [
11 | {
12 | ignores: ["coverage/", "tests/", "dist/", "build/", "node_modules/"],
13 | },
14 | { languageOptions: { globals: trimmedGlobals } },
15 | pluginJs.configs.recommended,
16 | ...tseslint.configs.recommended,
17 | {
18 | rules: {
19 | "@typescript-eslint/no-unused-vars": [
20 | "error",
21 | {
22 | argsIgnorePattern: "^_",
23 | varsIgnorePattern: "^_",
24 | caughtErrorsIgnorePattern: "^_",
25 | },
26 | ],
27 | },
28 | },
29 | ];
30 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/auth/strategies/authStrategy.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Defines the interface for all authentication strategies.
3 | * This interface establishes a contract for verifying authentication tokens,
4 | * ensuring that any authentication method (JWT, OAuth, etc.) can be used
5 | * interchangeably by the core authentication middleware.
6 | * @module src/mcp-server/transports/auth/strategies/AuthStrategy
7 | */
8 | import type { AuthInfo } from "../lib/authTypes.js";
9 |
10 | export interface AuthStrategy {
11 | /**
12 | * Verifies an authentication token.
13 | * @param token The raw token string extracted from the request.
14 | * @returns A promise that resolves with the AuthInfo on successful verification.
15 | * @throws {McpError} if the token is invalid, expired, or fails verification for any reason.
16 | */
17 | verify(token: string): Promise<AuthInfo>;
18 | }
19 |
```
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
```yaml
1 | # These are supported funding model platforms
2 |
3 | github: cyanheads
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: cyanheads
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
```
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Barrel file for the utils module.
3 | * This file re-exports all utilities from their categorized subdirectories,
4 | * providing a single entry point for accessing utility functions.
5 | * @module src/utils
6 | */
7 |
8 | // Re-export all utilities from their categorized subdirectories
9 | export * from "./internal/index.js";
10 | export * from "./metrics/index.js";
11 | export * from "./parsing/index.js";
12 | export * from "./security/index.js";
13 | export * from "./network/index.js";
14 | export * from "./scheduling/index.js";
15 |
16 | // It's good practice to have index.ts files in each subdirectory
17 | // that export the contents of that directory.
18 | // Assuming those will be created or already exist.
19 | // If not, this might need adjustment to export specific files, e.g.:
20 | // export * from './internal/errorHandler.js';
21 | // export * from './internal/logger.js';
22 | // ... etc.
23 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedArticleConnections/logic/types.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Shared type definitions for the pubmedArticleConnections tool logic.
3 | * @module src/mcp-server/tools/pubmedArticleConnections/logic/types
4 | */
5 |
6 | import type { PubMedArticleConnectionsInput } from "./index.js";
7 |
8 | // Helper type for enriched related articles
9 | export interface RelatedArticle {
10 | pmid: string;
11 | title?: string;
12 | authors?: string; // e.g., "Smith J, Doe A"
13 | score?: number; // From ELink, if available
14 | linkUrl: string;
15 | }
16 |
17 | export interface CitationOutput {
18 | ris?: string;
19 | bibtex?: string;
20 | apa_string?: string;
21 | mla_string?: string;
22 | }
23 |
24 | export interface ToolOutputData {
25 | sourcePmid: string;
26 | relationshipType: PubMedArticleConnectionsInput["relationshipType"];
27 | relatedArticles: RelatedArticle[];
28 | citations: CitationOutput;
29 | retrievedCount: number;
30 | eUtilityUrl?: string; // ELink or EFetch URL
31 | message?: string; // For errors or additional info
32 | }
33 |
```
--------------------------------------------------------------------------------
/tsdoc.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
3 | "tagDefinitions": [
4 | {
5 | "tagName": "@fileoverview",
6 | "syntaxKind": "modifier"
7 | },
8 | {
9 | "tagName": "@module",
10 | "syntaxKind": "modifier"
11 | },
12 | {
13 | "tagName": "@type",
14 | "syntaxKind": "modifier"
15 | },
16 | {
17 | "tagName": "@typedef",
18 | "syntaxKind": "block"
19 | },
20 | {
21 | "tagName": "@function",
22 | "syntaxKind": "block"
23 | },
24 | {
25 | "tagName": "@template",
26 | "syntaxKind": "modifier"
27 | },
28 | {
29 | "tagName": "@property",
30 | "syntaxKind": "block"
31 | },
32 | {
33 | "tagName": "@class",
34 | "syntaxKind": "block"
35 | },
36 | {
37 | "tagName": "@static",
38 | "syntaxKind": "modifier"
39 | },
40 | {
41 | "tagName": "@private",
42 | "syntaxKind": "modifier"
43 | },
44 | {
45 | "tagName": "@constant",
46 | "syntaxKind": "block"
47 | }
48 | ]
49 | }
50 |
```
--------------------------------------------------------------------------------
/src/services/NCBI/core/ncbiConstants.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Constants and shared type definitions for NCBI E-utility interactions.
3 | * @module src/services/NCBI/core/ncbiConstants
4 | */
5 |
6 | export const NCBI_EUTILS_BASE_URL =
7 | "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
8 |
9 | /**
10 | * Interface for common NCBI E-utility request parameters.
11 | * Specific E-utilities will have additional parameters.
12 | */
13 | export interface NcbiRequestParams {
14 | db?: string; // Target database (e.g., "pubmed", "pmc"). Optional for EInfo to list all databases.
15 | [key: string]: string | number | undefined; // Allows for other E-utility specific parameters
16 | }
17 |
18 | /**
19 | * Interface for options controlling how NCBI requests are made and responses are handled.
20 | */
21 | export interface NcbiRequestOptions {
22 | retmode?: "xml" | "json" | "text"; // Desired response format
23 | rettype?: string; // Specific type of data to return (e.g., "abstract", "medline")
24 | usePost?: boolean; // Hint to use HTTP POST for large payloads (e.g., many IDs)
25 | returnRawXml?: boolean; // If true and retmode is 'xml', returns the raw XML string instead of parsed object (after error checking)
26 | }
27 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/auth/lib/authContext.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Defines the AsyncLocalStorage context for authentication information.
3 | * This module provides a mechanism to store and retrieve authentication details
4 | * (like scopes and client ID) across asynchronous operations, making it available
5 | * from the middleware layer down to the tool and resource handlers without
6 | * drilling props.
7 | *
8 | * @module src/mcp-server/transports/auth/core/authContext
9 | */
10 |
11 | import { AsyncLocalStorage } from "async_hooks";
12 | import type { AuthInfo } from "./authTypes.js";
13 |
14 | /**
15 | * Defines the structure of the store used within the AsyncLocalStorage.
16 | * It holds the authentication information for the current request context.
17 | */
18 | interface AuthStore {
19 | authInfo: AuthInfo;
20 | }
21 |
22 | /**
23 | * An instance of AsyncLocalStorage to hold the authentication context (`AuthStore`).
24 | * This allows `authInfo` to be accessible throughout the async call chain of a request
25 | * after being set in the authentication middleware.
26 | *
27 | * @example
28 | * // In middleware:
29 | * await authContext.run({ authInfo }, next);
30 | *
31 | * // In a deeper handler:
32 | * const store = authContext.getStore();
33 | * const scopes = store?.authInfo.scopes;
34 | */
35 | export const authContext = new AsyncLocalStorage<AuthStore>();
36 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/core/baseTransportManager.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Abstract base class for transport managers.
3 | * @module src/mcp-server/transports/core/baseTransportManager
4 | */
5 |
6 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7 | import type { IncomingHttpHeaders } from "http";
8 | import {
9 | logger,
10 | RequestContext,
11 | requestContextService,
12 | } from "../../../utils/index.js";
13 | import { TransportManager, TransportResponse } from "./transportTypes.js";
14 |
15 | /**
16 | * Abstract base class for transport managers, providing common functionality.
17 | */
18 | export abstract class BaseTransportManager implements TransportManager {
19 | protected readonly createServerInstanceFn: () => Promise<McpServer>;
20 |
21 | constructor(createServerInstanceFn: () => Promise<McpServer>) {
22 | const context = requestContextService.createRequestContext({
23 | operation: "BaseTransportManager.constructor",
24 | managerType: this.constructor.name,
25 | });
26 | logger.debug("Initializing transport manager.", context);
27 | this.createServerInstanceFn = createServerInstanceFn;
28 | }
29 |
30 | abstract handleRequest(
31 | headers: IncomingHttpHeaders,
32 | body: unknown,
33 | context: RequestContext,
34 | sessionId?: string,
35 | ): Promise<TransportResponse>;
36 |
37 | abstract shutdown(): Promise<void>;
38 | }
39 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedResearchAgent/logic.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Core logic invocation for the pubmed_research_agent tool.
3 | * This tool generates a structured research plan outline with instructive placeholders,
4 | * designed to be completed by a calling LLM (the MCP Client).
5 | * @module pubmedResearchAgent/logic
6 | */
7 |
8 | import {
9 | logger,
10 | RequestContext,
11 | requestContextService,
12 | sanitizeInputForLogging,
13 | } from "../../../utils/index.js";
14 | import {
15 | generateFullResearchPlanOutline,
16 | PubMedResearchAgentInput,
17 | PubMedResearchPlanGeneratedOutput,
18 | } from "./logic/index.js";
19 |
20 | export async function pubmedResearchAgentLogic(
21 | input: PubMedResearchAgentInput,
22 | parentRequestContext: RequestContext,
23 | ): Promise<PubMedResearchPlanGeneratedOutput> {
24 | const operationContext = requestContextService.createRequestContext({
25 | parentRequestId: parentRequestContext.requestId,
26 | operation: "pubmedResearchAgentLogicExecution",
27 | input: sanitizeInputForLogging(input),
28 | });
29 |
30 | logger.info(
31 | `Executing 'pubmed_research_agent' to generate research plan outline. Keywords: ${input.research_keywords.join(
32 | ", ",
33 | )}`,
34 | operationContext,
35 | );
36 |
37 | const researchPlanOutline = generateFullResearchPlanOutline(
38 | input,
39 | operationContext,
40 | );
41 |
42 | logger.notice("Successfully generated research plan outline.", {
43 | ...operationContext,
44 | projectTitle: input.project_title_suggestion,
45 | });
46 |
47 | return researchPlanOutline;
48 | }
49 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | // Target modern JavaScript
4 | "target": "ES2022",
5 |
6 | // Use modern Node.js module system
7 | "module": "NodeNext",
8 | "moduleResolution": "NodeNext",
9 |
10 | // Enable all strict type checking
11 | "strict": true,
12 | "noUncheckedIndexedAccess": true,
13 |
14 | // Module interop for CommonJS compatibility
15 | "esModuleInterop": true,
16 | "allowSyntheticDefaultImports": true,
17 |
18 | // Output configuration
19 | "outDir": "./dist",
20 | "rootDir": "./src",
21 | "declaration": true,
22 | "declarationMap": true,
23 | "sourceMap": true,
24 |
25 | // Import helpers to reduce bundle size
26 | "importHelpers": true,
27 |
28 | // Skip type checking of declaration files
29 | "skipLibCheck": true,
30 |
31 | // Ensure consistent file naming
32 | "forceConsistentCasingInFileNames": true,
33 |
34 | // Enable experimental decorators if needed
35 | "experimentalDecorators": true,
36 | "emitDecoratorMetadata": true,
37 |
38 | // Node.js specific
39 | "lib": ["ES2022"],
40 | "types": ["node"],
41 |
42 | // Error on unused locals and parameters
43 | "noUnusedLocals": true,
44 | "noUnusedParameters": true,
45 |
46 | // Ensure void returns are handled
47 | "noImplicitReturns": true,
48 | "noFallthroughCasesInSwitch": true,
49 |
50 | // Modern resolution features
51 | "resolveJsonModule": true,
52 | "allowJs": false
53 | },
54 | "include": ["src/**/*"],
55 | "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"],
56 | "ts-node": {
57 | "esm": true,
58 | "experimentalSpecifierResolution": "node"
59 | }
60 | }
61 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/core/headerUtils.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Provides a utility for converting HTTP headers between Node.js
3 | * and Web Standards formats, ensuring compliance and correctness.
4 | * @module src/mcp-server/transports/core/headerUtils
5 | */
6 |
7 | import type { OutgoingHttpHeaders } from "http";
8 |
9 | /**
10 | * Converts Node.js-style OutgoingHttpHeaders to a Web-standard Headers object.
11 | *
12 | * This function is critical for interoperability between Node.js's `http` module
13 | * and Web APIs like Fetch and Hono. It correctly handles multi-value headers
14 | * (e.g., `Set-Cookie`), which Node.js represents as an array of strings, by
15 | * using the `Headers.append()` method. Standard single-value headers are set
16 | * using `Headers.set()`.
17 | *
18 | * @param nodeHeaders - The Node.js-style headers object to convert.
19 | * @returns A Web-standard Headers object.
20 | */
21 | export function convertNodeHeadersToWebHeaders(
22 | nodeHeaders: OutgoingHttpHeaders,
23 | ): Headers {
24 | const webHeaders = new Headers();
25 | for (const [key, value] of Object.entries(nodeHeaders)) {
26 | // Skip undefined headers, which are valid in Node.js but not in Web Headers.
27 | if (value === undefined) {
28 | continue;
29 | }
30 |
31 | if (Array.isArray(value)) {
32 | // For arrays, append each value to support multi-value headers.
33 | for (const v of value) {
34 | webHeaders.append(key, String(v));
35 | }
36 | } else {
37 | // For single values, set the header, overwriting any existing value.
38 | webHeaders.set(key, String(value));
39 | }
40 | }
41 | return webHeaders;
42 | }
43 |
```
--------------------------------------------------------------------------------
/examples/pubmed_article_connections_1.md:
--------------------------------------------------------------------------------
```markdown
1 | Tool Call Arguments:
2 |
3 | ```json
4 | {
5 | "sourcePmid": "39704040",
6 | "relationshipType": "pubmed_similar_articles",
7 | "maxRelatedResults": 3
8 | }
9 | ```
10 |
11 | Tool Response:
12 |
13 | ```json
14 | {
15 | "sourcePmid": "39704040",
16 | "relationshipType": "pubmed_similar_articles",
17 | "relatedArticles": [
18 | {
19 | "pmid": "38728204",
20 | "title": "Ciita Regulates Local and Systemic Immune Responses in a Combined rAAV-α-synuclein and Preformed Fibril-Induced Rat Model for Parkinson's Disease.",
21 | "authors": "Fredlund F, Jimenez-Ferrer I, Grabert K, et al.",
22 | "score": 34156797,
23 | "linkUrl": "https://pubmed.ncbi.nlm.nih.gov/38728204/"
24 | },
25 | {
26 | "pmid": "27147665",
27 | "title": "Inhibition of the JAK/STAT Pathway Protects Against α-Synuclein-Induced Neuroinflammation and Dopaminergic Neurodegeneration.",
28 | "authors": "Qin H, Buckley JA, Li X, et al.",
29 | "score": 33315411,
30 | "linkUrl": "https://pubmed.ncbi.nlm.nih.gov/27147665/"
31 | },
32 | {
33 | "pmid": "39652643",
34 | "title": "Transmission of peripheral blood α-synuclein fibrils exacerbates synucleinopathy and neurodegeneration in Parkinson's disease by endothelial Lag3 endocytosis.",
35 | "authors": "Duan Q, Zhang Q, Jiang S, et al.",
36 | "score": 33247981,
37 | "linkUrl": "https://pubmed.ncbi.nlm.nih.gov/39652643/"
38 | }
39 | ],
40 | "citations": {},
41 | "retrievedCount": 3,
42 | "eUtilityUrl": "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi?dbfrom=pubmed&db=pubmed&id=39704040&retmode=xml&cmd=neighbor_score"
43 | }
44 | ```
45 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/auth/authFactory.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Factory for creating an authentication strategy based on configuration.
3 | * This module centralizes the logic for selecting and instantiating the correct
4 | * authentication strategy, promoting loose coupling and easy extensibility.
5 | * @module src/mcp-server/transports/auth/authFactory
6 | */
7 | import { config } from "../../../config/index.js";
8 | import { logger, requestContextService } from "../../../utils/index.js";
9 | import { AuthStrategy } from "./strategies/authStrategy.js";
10 | import { JwtStrategy } from "./strategies/jwtStrategy.js";
11 | import { OauthStrategy } from "./strategies/oauthStrategy.js";
12 |
13 | /**
14 | * Creates and returns an authentication strategy instance based on the
15 | * application's configuration (`config.mcpAuthMode`).
16 | *
17 | * @returns An instance of a class that implements the `AuthStrategy` interface,
18 | * or `null` if authentication is disabled (`none`).
19 | * @throws {Error} If the auth mode is unknown or misconfigured.
20 | */
21 | export function createAuthStrategy(): AuthStrategy | null {
22 | const context = requestContextService.createRequestContext({
23 | operation: "createAuthStrategy",
24 | authMode: config.mcpAuthMode,
25 | });
26 | logger.info("Creating authentication strategy...", context);
27 |
28 | switch (config.mcpAuthMode) {
29 | case "jwt":
30 | logger.debug("Instantiating JWT authentication strategy.", context);
31 | return new JwtStrategy();
32 | case "oauth":
33 | logger.debug("Instantiating OAuth authentication strategy.", context);
34 | return new OauthStrategy();
35 | case "none":
36 | logger.info("Authentication is disabled ('none' mode).", context);
37 | return null; // No authentication
38 | default:
39 | // This ensures that if a new auth mode is added to the config type
40 | // but not to this factory, we get a compile-time or runtime error.
41 | logger.error(
42 | `Unknown authentication mode: ${config.mcpAuthMode}`,
43 | context,
44 | );
45 | throw new Error(`Unknown authentication mode: ${config.mcpAuthMode}`);
46 | }
47 | }
48 |
```
--------------------------------------------------------------------------------
/examples/pubmed_article_connections_2.md:
--------------------------------------------------------------------------------
```markdown
1 | Tool Call Arguments:
2 |
3 | ```json
4 | {
5 | "sourcePmid": "39704040",
6 | "relationshipType": "citation_formats",
7 | "citationStyles": ["ris", "bibtex", "apa_string", "mla_string"]
8 | }
9 | ```
10 |
11 | Tool Response:
12 |
13 | ```json
14 | {
15 | "sourcePmid": "39704040",
16 | "relationshipType": "citation_formats",
17 | "relatedArticles": [],
18 | "citations": {
19 | "ris": "TY - JOUR\nAU - Bellini, Gabriele\nAU - D'Antongiovanni, Vanessa\nAU - Palermo, Giovanni\nAU - Antonioli, Luca\nAU - Fornai, Matteo\nAU - Ceravolo, Roberto\nAU - Bernardini, Nunzia\nAU - Derkinderen, Pascal\nAU - Pellegrini, Carolina\nTI - α-Synuclein in Parkinson's Disease: From Bench to Bedside.\nJO - Medicinal research reviews\nVL - 45\nIS - 3\nSP - 909\nEP - 946\nPY - 2025\nDO - 10.1002/med.22091\nUR - https://pubmed.ncbi.nlm.nih.gov/39704040\nER - \n",
20 | "bibtex": "@article{Bellini2025,\n author = {Bellini, Gabriele and D'Antongiovanni, Vanessa and Palermo, Giovanni and Antonioli, Luca and Fornai, Matteo and Ceravolo, Roberto and Bernardini, Nunzia and Derkinderen, Pascal and Pellegrini, Carolina},\n title = {α-Synuclein in Parkinson's Disease: From Bench to Bedside.},\n journal = {Medicinal research reviews},\n year = {2025},\n volume = {45},\n number = {3},\n pages = {909--946},\n month = {may},\n doi = {10.1002/med.22091},\n pmid = {39704040}\n}\n",
21 | "apa_string": "Bellini, G., D'Antongiovanni, V., Palermo, G., Antonioli, L., Fornai, M., Ceravolo, R., Bernardini, N., Derkinderen, P., Pellegrini & C.. (2025). α-Synuclein in Parkinson's Disease: From Bench to Bedside.. <em>Medicinal research reviews</em>, <em>45</em>(3), 909-946. https://doi.org/10.1002/med.22091",
22 | "mla_string": "Bellini, Gabriele, et al. \"α-Synuclein in Parkinson's Disease: From Bench to Bedside..\" <em>Medicinal research reviews</em>, vol. 45, no. 3, May. 2025, pp. 909–946. PubMed Central, doi:10.1002/med.22091."
23 | },
24 | "retrievedCount": 1,
25 | "eUtilityUrl": "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=39704040&retmode=xml"
26 | }
27 | ```
28 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # ---- Build Stage ----
2 | # Use a modern, secure Node.js Alpine image.
3 | # Alpine is lightweight, which reduces the attack surface.
4 | FROM node:23-alpine AS build
5 |
6 | # Set the working directory inside the container.
7 | WORKDIR /usr/src/app
8 |
9 | # Install build-time dependencies for native modules, especially node-canvas.
10 | # This includes python, make, and g++ for compilation, and dev libraries for canvas.
11 | RUN apk add --no-cache \
12 | python3 \
13 | make \
14 | g++ \
15 | cairo-dev \
16 | jpeg-dev \
17 | pango-dev \
18 | giflib-dev
19 |
20 | # Copy package definitions to leverage Docker layer caching.
21 | COPY package.json package-lock.json* ./
22 |
23 | # Install all npm dependencies. `npm ci` is used for reproducible builds.
24 | RUN npm ci
25 |
26 | # Copy the rest of the application source code.
27 | COPY . .
28 |
29 | # Compile TypeScript to JavaScript.
30 | RUN npm run build
31 |
32 | # ---- Production Stage ----
33 | # Start from a fresh, minimal Node.js Alpine image for the final image.
34 | FROM node:23-alpine AS production
35 |
36 | WORKDIR /usr/src/app
37 |
38 | # Set the environment to production for optimized performance.
39 | ENV NODE_ENV=production
40 |
41 | # Install only the runtime dependencies for node-canvas.
42 | # This keeps the final image smaller than including the -dev packages.
43 | RUN apk add --no-cache \
44 | cairo \
45 | jpeg \
46 | pango \
47 | giflib
48 |
49 | # Create a non-root user and group for enhanced security.
50 | RUN addgroup -S appgroup && adduser -S appuser -G appgroup
51 |
52 | # Create and set permissions for the log directory.
53 | RUN mkdir -p /var/log/pubmed-mcp-server && chown -R appuser:appgroup /var/log/pubmed-mcp-server
54 |
55 | # Copy build artifacts from the build stage.
56 | # This includes the compiled code and production node_modules.
57 | COPY --from=build /usr/src/app/dist ./dist
58 | COPY --from=build /usr/src/app/node_modules ./node_modules
59 | COPY --from=build /usr/src/app/package.json ./
60 |
61 | # Switch to the non-root user.
62 | USER appuser
63 |
64 | # Expose the port the server will listen on.
65 | # The PORT variable is typically provided by the deployment environment (e.g., Smithery).
66 | ENV MCP_HTTP_PORT=${PORT:-3017}
67 | EXPOSE ${MCP_HTTP_PORT}
68 |
69 | # Set runtime environment variables.
70 | ENV MCP_HTTP_HOST=0.0.0.0
71 | ENV MCP_TRANSPORT_TYPE=http
72 | ENV MCP_SESSION_MODE=stateless
73 | ENV MCP_LOG_LEVEL=info
74 | ENV LOGS_DIR=/var/log/pubmed-mcp-server
75 | ENV MCP_AUTH_MODE=none
76 | ENV MCP_FORCE_CONSOLE_LOGGING=true
77 |
78 | # The command to start the server.
79 | CMD ["node", "dist/index.js"]
80 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/auth/lib/authUtils.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Provides utility functions for authorization, specifically for
3 | * checking token scopes against required permissions for a given operation.
4 | * @module src/mcp-server/transports/auth/core/authUtils
5 | */
6 |
7 | import { BaseErrorCode, McpError } from "../../../../types-global/errors.js";
8 | import { logger, requestContextService } from "../../../../utils/index.js";
9 | import { authContext } from "./authContext.js";
10 |
11 | /**
12 | * Checks if the current authentication context contains all the specified scopes.
13 | * This function is designed to be called within tool or resource handlers to
14 | * enforce scope-based access control. It retrieves the authentication information
15 | * from `authContext` (AsyncLocalStorage).
16 | *
17 | * @param requiredScopes - An array of scope strings that are mandatory for the operation.
18 | * @throws {McpError} Throws an error with `BaseErrorCode.INTERNAL_ERROR` if the
19 | * authentication context is missing, which indicates a server configuration issue.
20 | * @throws {McpError} Throws an error with `BaseErrorCode.FORBIDDEN` if one or
21 | * more required scopes are not present in the validated token.
22 | */
23 | export function withRequiredScopes(requiredScopes: string[]): void {
24 | const operationName = "withRequiredScopesCheck";
25 | const initialContext = requestContextService.createRequestContext({
26 | operation: operationName,
27 | requiredScopes,
28 | });
29 |
30 | logger.debug("Performing scope authorization check.", initialContext);
31 |
32 | const store = authContext.getStore();
33 |
34 | if (!store || !store.authInfo) {
35 | logger.crit(
36 | "Authentication context is missing in withRequiredScopes. This is a server configuration error.",
37 | initialContext,
38 | );
39 | // This is a server-side logic error; the auth middleware should always populate this.
40 | throw new McpError(
41 | BaseErrorCode.INTERNAL_ERROR,
42 | "Authentication context is missing. This indicates a server configuration error.",
43 | {
44 | ...initialContext,
45 | error: "AuthStore not found in AsyncLocalStorage.",
46 | },
47 | );
48 | }
49 |
50 | const { scopes: grantedScopes, clientId, subject } = store.authInfo;
51 | const grantedScopeSet = new Set(grantedScopes);
52 |
53 | const missingScopes = requiredScopes.filter(
54 | (scope) => !grantedScopeSet.has(scope),
55 | );
56 |
57 | const finalContext = {
58 | ...initialContext,
59 | grantedScopes,
60 | clientId,
61 | subject,
62 | };
63 |
64 | if (missingScopes.length > 0) {
65 | const errorContext = { ...finalContext, missingScopes };
66 | logger.warning(
67 | "Authorization failed: Missing required scopes.",
68 | errorContext,
69 | );
70 | throw new McpError(
71 | BaseErrorCode.FORBIDDEN,
72 | `Insufficient permissions. Missing required scopes: ${missingScopes.join(", ")}`,
73 | errorContext,
74 | );
75 | }
76 |
77 | logger.debug("Scope authorization successful.", finalContext);
78 | }
79 |
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration for a custom Docker container deployment.
2 |
3 | # Defines how to start the server once the container is built.
4 | startCommand:
5 | # Specifies that the server communicates over HTTP.
6 | type: "http"
7 | # Defines the configuration variables the server expects.
8 | # These are passed as environment variables or query parameters.
9 | configSchema:
10 | type: "object"
11 | properties:
12 | NCBI_API_KEY:
13 | type: "string"
14 | description: "API key for the NCBI service."
15 | NCBI_TOOL_IDENTIFIER:
16 | type: "string"
17 | description: "Tool identifier for the NCBI service."
18 | NCBI_ADMIN_EMAIL:
19 | type: "string"
20 | description: "Admin email for the NCBI service."
21 | MCP_LOG_LEVEL:
22 | type: "string"
23 | default: "info"
24 | description: "Minimum logging level."
25 | MCP_TRANSPORT_TYPE:
26 | type: "string"
27 | enum: ["stdio", "http"]
28 | default: "http"
29 | description: "MCP communication transport ('stdio' or 'http')."
30 | MCP_HTTP_PORT:
31 | type: "integer"
32 | default: 3017
33 | description: "HTTP server port (if MCP_TRANSPORT_TYPE is 'http')."
34 | MCP_HTTP_HOST:
35 | type: "string"
36 | default: "0.0.0.0"
37 | description: "HTTP server host to bind to."
38 | MCP_SESSION_MODE:
39 | type: "string"
40 | enum: ["stateless", "stateful", "auto"]
41 | default: "stateless"
42 | description: "Server session management mode."
43 | MCP_AUTH_MODE:
44 | type: "string"
45 | enum: ["none", "jwt", "oauth"]
46 | default: "none"
47 | description: "Server authentication mode."
48 | MCP_FORCE_CONSOLE_LOGGING:
49 | type: "boolean"
50 | default: false
51 | description: "Force console logging, even in non-TTY environments like Docker."
52 | # Specifies the build configuration for the Smithery Docker container.
53 | commandFunction: |-
54 | (config) => ({
55 | command: 'node',
56 | args: ['build/index.js'],
57 | env: {
58 | NCBI_API_KEY: config.NCBI_API_KEY,
59 | NCBI_TOOL_IDENTIFIER: config.NCBI_TOOL_IDENTIFIER,
60 | NCBI_ADMIN_EMAIL: config.NCBI_ADMIN_EMAIL,
61 | MCP_LOG_LEVEL: config.MCP_LOG_LEVEL,
62 | MCP_TRANSPORT_TYPE: config.MCP_TRANSPORT_TYPE,
63 | MCP_HTTP_PORT: config.MCP_HTTP_PORT,
64 | MCP_HTTP_HOST: config.MCP_HTTP_HOST,
65 | MCP_SESSION_MODE: config.MCP_SESSION_MODE,
66 | MCP_AUTH_MODE: config.MCP_AUTH_MODE,
67 | MCP_FORCE_CONSOLE_LOGGING: config.MCP_FORCE_CONSOLE_LOGGING
68 | }
69 | })
70 | # Provides an example configuration for users.
71 | exampleConfig:
72 | NCBI_API_KEY: "your_ncbi_api_key"
73 | NCBI_TOOL_IDENTIFIER: "@cyanheads/pubmed-mcp-server"
74 | NCBI_ADMIN_EMAIL: "[email protected]"
75 | MCP_LOG_LEVEL: "debug"
76 | MCP_TRANSPORT_TYPE: "http"
77 | MCP_HTTP_PORT: 3017
78 | MCP_HTTP_HOST: "0.0.0.0"
79 | MCP_SESSION_MODE: "stateless"
80 | MCP_AUTH_MODE: "none"
81 | MCP_FORCE_CONSOLE_LOGGING: true
82 |
```
--------------------------------------------------------------------------------
/src/services/NCBI/parsing/xmlGenericHelpers.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Generic helper functions for parsing XML data, particularly
3 | * structures from fast-xml-parser.
4 | * @module src/services/NCBI/parsing/xmlGenericHelpers
5 | */
6 |
7 | /**
8 | * Ensures that the input is an array. If it's not an array, it wraps it in one.
9 | * Handles undefined or null by returning an empty array.
10 | * @param item - The item to ensure is an array.
11 | * @returns An array containing the item, or an empty array if item is null/undefined.
12 | * @template T - The type of the items in the array.
13 | */
14 | export function ensureArray<T>(item: T | T[] | undefined | null): T[] {
15 | if (item === undefined || item === null) {
16 | return [];
17 | }
18 | return Array.isArray(item) ? item : [item];
19 | }
20 |
21 | /**
22 | * Safely extracts text content from an XML element, which might be a string or an object with a "#text" property.
23 | * Handles cases where #text might be a number or boolean by converting to string.
24 | * @param element - The XML element (string, object with #text, or undefined).
25 | * @param defaultValue - The value to return if text cannot be extracted. Defaults to an empty string.
26 | * @returns The text content or the default value.
27 | */
28 | export function getText(element: unknown, defaultValue = ""): string {
29 | if (element === undefined || element === null) {
30 | return defaultValue;
31 | }
32 | if (typeof element === "string") {
33 | return element;
34 | }
35 | if (typeof element === "number" || typeof element === "boolean") {
36 | return String(element); // Handle direct number/boolean elements
37 | }
38 | if (typeof element === "object") {
39 | const obj = element as Record<string, unknown>;
40 | if (obj["#text"] !== undefined) {
41 | const val = obj["#text"];
42 | if (typeof val === "string") return val;
43 | if (typeof val === "number" || typeof val === "boolean")
44 | return String(val);
45 | }
46 | }
47 | return defaultValue;
48 | }
49 |
50 | /**
51 | * Safely extracts an attribute value from an XML element.
52 | * Assumes attributes are prefixed with "@_" by fast-xml-parser.
53 | * @param element - The XML element object.
54 | * @param attributeName - The name of the attribute (e.g., "_UI", "_MajorTopicYN", without the "@_" prefix).
55 | * @param defaultValue - The value to return if the attribute is not found. Defaults to an empty string.
56 | * @returns The attribute value or the default value.
57 | */
58 | export function getAttribute(
59 | element: unknown,
60 | attributeName: string, // e.g., "UI", "MajorTopicYN"
61 | defaultValue = "",
62 | ): string {
63 | const fullAttributeName = `@_${attributeName}`; // As per fast-xml-parser config
64 | if (element && typeof element === "object") {
65 | const obj = element as Record<string, unknown>;
66 | const val = obj[fullAttributeName];
67 | if (typeof val === "string") return val;
68 | if (typeof val === "boolean") return String(val); // Convert boolean attributes to string
69 | if (typeof val === "number") return String(val); // Convert number attributes to string
70 | }
71 | return defaultValue;
72 | }
73 |
```
--------------------------------------------------------------------------------
/scripts/clean.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * @fileoverview Utility script to clean build artifacts and temporary directories.
5 | * @module scripts/clean
6 | * By default, it removes the 'dist' and 'logs' directories.
7 | * Custom directories can be specified as command-line arguments.
8 | * Works on all platforms using Node.js path normalization.
9 | *
10 | * @example
11 | * // Add to package.json:
12 | * // "scripts": {
13 | * // "clean": "ts-node --esm scripts/clean.ts",
14 | * // "rebuild": "npm run clean && npm run build"
15 | * // }
16 | *
17 | * // Run with default directories:
18 | * // npm run clean
19 | *
20 | * // Run with custom directories:
21 | * // ts-node --esm scripts/clean.ts temp coverage
22 | */
23 |
24 | import { rm, access } from "fs/promises";
25 | import { join } from "path";
26 |
27 | /**
28 | * Represents the result of a clean operation for a single directory.
29 | * @property dir - The name of the directory targeted for cleaning.
30 | * @property status - Indicates if the cleaning was successful or skipped.
31 | * @property reason - If skipped, the reason why.
32 | */
33 | interface CleanResult {
34 | dir: string;
35 | status: "success" | "skipped";
36 | reason?: string;
37 | }
38 |
39 | /**
40 | * Asynchronously checks if a directory exists at the given path.
41 | * @param dirPath - The absolute or relative path to the directory.
42 | * @returns A promise that resolves to `true` if the directory exists, `false` otherwise.
43 | */
44 | async function directoryExists(dirPath: string): Promise<boolean> {
45 | try {
46 | await access(dirPath);
47 | return true;
48 | } catch {
49 | return false;
50 | }
51 | }
52 |
53 | /**
54 | * Main function to perform the cleaning operation.
55 | * It reads command line arguments for target directories or uses defaults ('dist', 'logs').
56 | * Reports the status of each cleaning attempt.
57 | */
58 | const clean = async (): Promise<void> => {
59 | try {
60 | let dirsToClean: string[] = ["dist", "logs"];
61 | const args = process.argv.slice(2);
62 |
63 | if (args.length > 0) {
64 | dirsToClean = args;
65 | }
66 |
67 | console.log(`Attempting to clean directories: ${dirsToClean.join(", ")}`);
68 |
69 | const results = await Promise.allSettled(
70 | dirsToClean.map(async (dir): Promise<CleanResult> => {
71 | const dirPath = join(process.cwd(), dir);
72 |
73 | const exists = await directoryExists(dirPath);
74 |
75 | if (!exists) {
76 | return { dir, status: "skipped", reason: "does not exist" };
77 | }
78 |
79 | await rm(dirPath, { recursive: true, force: true });
80 | return { dir, status: "success" };
81 | }),
82 | );
83 |
84 | results.forEach((result) => {
85 | if (result.status === "fulfilled") {
86 | const { dir, status, reason } = result.value;
87 | if (status === "success") {
88 | console.log(`Successfully cleaned directory: ${dir}`);
89 | } else {
90 | console.log(`Skipped cleaning directory ${dir}: ${reason}.`);
91 | }
92 | } else {
93 | // The error here is the actual error object from the rejected promise
94 | console.error(
95 | `Error cleaning a directory (details below):\n`,
96 | result.reason,
97 | );
98 | }
99 | });
100 | } catch (error) {
101 | console.error(
102 | "An unexpected error occurred during the clean script execution:",
103 | error instanceof Error ? error.message : error,
104 | );
105 | process.exit(1);
106 | }
107 | };
108 |
109 | clean();
110 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/auth/authMiddleware.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Defines a unified Hono middleware for authentication.
3 | * This middleware is strategy-agnostic. It extracts a Bearer token,
4 | * delegates verification to the provided authentication strategy, and
5 | * populates the async-local storage context with the resulting auth info.
6 | * @module src/mcp-server/transports/auth/authMiddleware
7 | */
8 | import type { HttpBindings } from "@hono/node-server";
9 | import type { Context, Next } from "hono";
10 | import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
11 | import {
12 | ErrorHandler,
13 | logger,
14 | requestContextService,
15 | } from "../../../utils/index.js";
16 | import { authContext } from "./lib/authContext.js";
17 | import type { AuthStrategy } from "./strategies/authStrategy.js";
18 |
19 | /**
20 | * Creates a Hono middleware function that enforces authentication using a given strategy.
21 | *
22 | * @param strategy - An instance of a class that implements the `AuthStrategy` interface.
23 | * @returns A Hono middleware function.
24 | */
25 | export function createAuthMiddleware(strategy: AuthStrategy) {
26 | return async function authMiddleware(
27 | c: Context<{ Bindings: HttpBindings }>,
28 | next: Next,
29 | ) {
30 | const context = requestContextService.createRequestContext({
31 | operation: "authMiddleware",
32 | method: c.req.method,
33 | path: c.req.path,
34 | });
35 |
36 | logger.debug("Initiating authentication check.", context);
37 |
38 | const authHeader = c.req.header("Authorization");
39 | if (!authHeader || !authHeader.startsWith("Bearer ")) {
40 | logger.warning("Authorization header missing or invalid.", context);
41 | throw new McpError(
42 | BaseErrorCode.UNAUTHORIZED,
43 | "Missing or invalid Authorization header. Bearer scheme required.",
44 | context,
45 | );
46 | }
47 |
48 | const token = authHeader.substring(7);
49 | if (!token) {
50 | logger.warning(
51 | "Bearer token is missing from Authorization header.",
52 | context,
53 | );
54 | throw new McpError(
55 | BaseErrorCode.UNAUTHORIZED,
56 | "Authentication token is missing.",
57 | context,
58 | );
59 | }
60 |
61 | logger.debug(
62 | "Extracted Bearer token, proceeding to verification.",
63 | context,
64 | );
65 |
66 | try {
67 | const authInfo = await strategy.verify(token);
68 |
69 | const authLogContext = {
70 | ...context,
71 | clientId: authInfo.clientId,
72 | subject: authInfo.subject,
73 | scopes: authInfo.scopes,
74 | };
75 | logger.info(
76 | "Authentication successful. Auth context populated.",
77 | authLogContext,
78 | );
79 |
80 | // Run the next middleware in the chain within the populated auth context.
81 | await authContext.run({ authInfo }, next);
82 | } catch (error) {
83 | // The strategy is expected to throw an McpError.
84 | // We re-throw it here to be caught by the global httpErrorHandler.
85 | logger.warning("Authentication verification failed.", {
86 | ...context,
87 | error: error instanceof Error ? error.message : String(error),
88 | });
89 |
90 | // Ensure consistent error handling
91 | throw ErrorHandler.handleError(error, {
92 | operation: "authMiddlewareVerification",
93 | context,
94 | rethrow: true, // Rethrow to be caught by Hono's global error handler
95 | errorCode: BaseErrorCode.UNAUTHORIZED, // Default to unauthorized if not more specific
96 | });
97 | }
98 | };
99 | }
100 |
```
--------------------------------------------------------------------------------
/examples/pubmed_search_articles_example.md:
--------------------------------------------------------------------------------
```markdown
1 | Tool Call Arguments:
2 |
3 | ```json
4 | {
5 | "queryTerm": "neuroinflammation AND (Alzheimer's OR Parkinson's) AND microglia",
6 | "maxResults": 15,
7 | "sortBy": "pub_date",
8 | "dateRange": {
9 | "minDate": "2023/01/01",
10 | "maxDate": "2024/12/31",
11 | "dateType": "pdat"
12 | },
13 | "filterByPublicationTypes": ["Review", "Journal Article"],
14 | "fetchBriefSummaries": 5
15 | }
16 | ```
17 |
18 | Tool Response:
19 |
20 | ```json
21 | {
22 | "searchParameters": {
23 | "queryTerm": "neuroinflammation AND (Alzheimer's OR Parkinson's) AND microglia",
24 | "maxResults": 15,
25 | "sortBy": "pub_date",
26 | "dateRange": {
27 | "minDate": "2023/01/01",
28 | "maxDate": "2024/12/31",
29 | "dateType": "pdat"
30 | },
31 | "filterByPublicationTypes": ["Review", "Journal Article"],
32 | "fetchBriefSummaries": 5
33 | },
34 | "effectiveESearchTerm": "neuroinflammation AND (Alzheimer's OR Parkinson's) AND microglia AND (2023/01/01[pdat] : 2024/12/31[pdat]) AND (\"Review\"[Publication Type] OR \"Journal Article\"[Publication Type])",
35 | "totalFound": 1290,
36 | "retrievedPmidCount": 15,
37 | "pmids": [
38 | 39715098, 39359093, 39704040, 39653749, 39648189, 39075895, 40256246,
39 | 39761611, 39726135, 39719687, 39718073, 39514171, 39433702, 39400857,
40 | 39029776
41 | ],
42 | "briefSummaries": [
43 | {
44 | "pmid": "39715098",
45 | "title": "The compound (E)-2-(3,4-dihydroxystyryl)-3-hydroxy-4H-pyran-4-one alleviates neuroinflammation and cognitive impairment in a mouse model of Alzheimer's disease.",
46 | "authors": "Liu X, Wu W, Li X, et al.",
47 | "source": "Neural Regen Res",
48 | "doi": "",
49 | "pubDate": "2025-11-01",
50 | "epubDate": "2024-07-10"
51 | },
52 | {
53 | "pmid": "39359093",
54 | "title": "The cGAS-STING-interferon regulatory factor 7 pathway regulates neuroinflammation in Parkinson's disease.",
55 | "authors": "Zhou S, Li T, Zhang W, et al.",
56 | "source": "Neural Regen Res",
57 | "doi": "",
58 | "pubDate": "2025-08-01",
59 | "epubDate": "2024-06-03"
60 | },
61 | {
62 | "pmid": "39704040",
63 | "title": "α-Synuclein in Parkinson's Disease: From Bench to Bedside.",
64 | "authors": "Bellini G, D'Antongiovanni V, Palermo G, et al.",
65 | "source": "Med Res Rev",
66 | "doi": "",
67 | "pubDate": "2026-05-20",
68 | "epubDate": "2024-12-20"
69 | },
70 | {
71 | "pmid": "39653749",
72 | "title": "Neuroinflammation in Alzheimer disease.",
73 | "authors": "Heneka MT, van der Flier WM, Jessen F, et al.",
74 | "source": "Nat Rev Immunol",
75 | "doi": "",
76 | "pubDate": "2026-05-20",
77 | "epubDate": "2024-12-09"
78 | },
79 | {
80 | "pmid": "39648189",
81 | "title": "Unveiling the Involvement of Herpes Simplex Virus-1 in Alzheimer's Disease: Possible Mechanisms and Therapeutic Implications.",
82 | "authors": "Chauhan P, Begum MY, Narapureddy BR, et al.",
83 | "source": "Mol Neurobiol",
84 | "doi": "",
85 | "pubDate": "2026-05-20",
86 | "epubDate": "2024-12-09"
87 | }
88 | ],
89 | "eSearchUrl": "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=neuroinflammation+AND+%28Alzheimer%27s+OR+Parkinson%27s%29+AND+microglia+AND+%282023%2F01%2F01%5Bpdat%5D+%3A+2024%2F12%2F31%5Bpdat%5D%29+AND+%28%22Review%22%5BPublication+Type%5D+OR+%22Journal+Article%22%5BPublication+Type%5D%29&retmax=15&sort=pub_date&usehistory=y",
90 | "eSummaryUrl": "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&version=2.0&retmode=xml&WebEnv=MCID_6832175795dfc79c7001d173&query_key=1&retmax=5"
91 | }
92 | ```
93 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/http/httpErrorHandler.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Centralized error handler for the Hono HTTP transport.
3 | * This middleware intercepts errors that occur during request processing,
4 | * standardizes them using the application's ErrorHandler utility, and
5 | * formats them into a consistent JSON-RPC error response.
6 | * @module src/mcp-server/transports/httpErrorHandler
7 | */
8 |
9 | import { Context } from "hono";
10 | import { StatusCode } from "hono/utils/http-status";
11 | import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
12 | import {
13 | ErrorHandler,
14 | logger,
15 | requestContextService,
16 | } from "../../../utils/index.js";
17 | import { HonoNodeBindings } from "./httpTypes.js";
18 |
19 | /**
20 | * A centralized error handling middleware for Hono.
21 | * This function is registered with `app.onError()` and will catch any errors
22 | * thrown from preceding middleware or route handlers.
23 | *
24 | * @param err - The error that was thrown.
25 | * @param c - The Hono context object for the request.
26 | * @returns A Response object containing the formatted JSON-RPC error.
27 | */
28 | export const httpErrorHandler = async (
29 | err: Error,
30 | c: Context<{ Bindings: HonoNodeBindings }>,
31 | ): Promise<Response> => {
32 | const context = requestContextService.createRequestContext({
33 | operation: "httpErrorHandler",
34 | path: c.req.path,
35 | method: c.req.method,
36 | });
37 | logger.debug("HTTP error handler invoked.", context);
38 |
39 | const handledError = ErrorHandler.handleError(err, {
40 | operation: "httpTransport",
41 | context,
42 | });
43 |
44 | let status: StatusCode = 500;
45 | if (handledError instanceof McpError) {
46 | switch (handledError.code) {
47 | case BaseErrorCode.NOT_FOUND:
48 | status = 404;
49 | break;
50 | case BaseErrorCode.UNAUTHORIZED:
51 | status = 401;
52 | break;
53 | case BaseErrorCode.FORBIDDEN:
54 | status = 403;
55 | break;
56 | case BaseErrorCode.VALIDATION_ERROR:
57 | case BaseErrorCode.INVALID_INPUT:
58 | status = 400;
59 | break;
60 | case BaseErrorCode.CONFLICT:
61 | status = 409;
62 | break;
63 | case BaseErrorCode.RATE_LIMITED:
64 | status = 429;
65 | break;
66 | default:
67 | status = 500;
68 | }
69 | }
70 | logger.debug(`Mapping error to HTTP status ${status}.`, {
71 | ...context,
72 | status,
73 | errorCode: (handledError as McpError).code,
74 | });
75 |
76 | // Attempt to get the request ID from the body, but don't fail if it's not there or unreadable.
77 | let requestId: string | number | null = null;
78 | // Only attempt to read the body if it hasn't been consumed already.
79 | if (c.req.raw.bodyUsed === false) {
80 | try {
81 | const body = await c.req.json();
82 | requestId = body?.id || null;
83 | logger.debug("Extracted JSON-RPC request ID from body.", {
84 | ...context,
85 | jsonRpcId: requestId,
86 | });
87 | } catch {
88 | logger.warning(
89 | "Could not parse request body to extract JSON-RPC ID.",
90 | context,
91 | );
92 | // Ignore parsing errors, requestId will remain null
93 | }
94 | } else {
95 | logger.debug(
96 | "Request body already consumed, cannot extract JSON-RPC ID.",
97 | context,
98 | );
99 | }
100 |
101 | const errorCode =
102 | handledError instanceof McpError ? handledError.code : -32603;
103 |
104 | c.status(status);
105 | const errorResponse = {
106 | jsonrpc: "2.0",
107 | error: {
108 | code: errorCode,
109 | message: handledError.message,
110 | },
111 | id: requestId,
112 | };
113 | logger.info(`Sending formatted error response for request.`, {
114 | ...context,
115 | status,
116 | errorCode,
117 | jsonRpcId: requestId,
118 | });
119 | return c.json(errorResponse);
120 | };
121 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/stdio/stdioTransport.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Handles the setup and connection for the Stdio MCP transport.
3 | * Implements the MCP Specification 2025-03-26 for stdio transport.
4 | * This transport communicates directly over standard input (stdin) and
5 | * standard output (stdout), typically used when the MCP server is launched
6 | * as a child process by a host application.
7 | *
8 | * Specification Reference:
9 | * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#stdio
10 | *
11 | * --- Authentication Note ---
12 | * As per the MCP Authorization Specification (2025-03-26, Section 1.2),
13 | * STDIO transports SHOULD NOT implement HTTP-based authentication flows.
14 | * Authorization is typically handled implicitly by the host application
15 | * controlling the server process. This implementation follows that guideline.
16 | *
17 | * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
18 | * @module src/mcp-server/transports/stdioTransport
19 | */
20 |
21 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23 | import { ErrorHandler, logger, RequestContext } from "../../../utils/index.js";
24 |
25 | /**
26 | * Connects a given `McpServer` instance to the Stdio transport.
27 | * This function initializes the SDK's `StdioServerTransport`, which manages
28 | * communication over `process.stdin` and `process.stdout` according to the
29 | * MCP stdio transport specification.
30 | *
31 | * MCP Spec Points Covered by SDK's `StdioServerTransport`:
32 | * - Reads JSON-RPC messages (requests, notifications, responses, batches) from stdin.
33 | * - Writes JSON-RPC messages to stdout.
34 | * - Handles newline delimiters and ensures no embedded newlines in output messages.
35 | * - Ensures only valid MCP messages are written to stdout.
36 | *
37 | * Logging via the `logger` utility MAY result in output to stderr, which is
38 | * permitted by the spec for logging purposes.
39 | *
40 | * @param server - The `McpServer` instance.
41 | * @param parentContext - The logging and tracing context from the calling function.
42 | * @returns A promise that resolves when the Stdio transport is successfully connected.
43 | * @throws {Error} If the connection fails during setup.
44 | */
45 | export async function startStdioTransport(
46 | server: McpServer,
47 | parentContext: RequestContext,
48 | ): Promise<void> {
49 | const operationContext = {
50 | ...parentContext,
51 | operation: "connectStdioTransport",
52 | transportType: "Stdio",
53 | };
54 | logger.info("Attempting to connect stdio transport...", operationContext);
55 |
56 | try {
57 | logger.debug("Creating StdioServerTransport instance...", operationContext);
58 | const transport = new StdioServerTransport();
59 |
60 | logger.debug(
61 | "Connecting McpServer instance to StdioServerTransport...",
62 | operationContext,
63 | );
64 | await server.connect(transport);
65 |
66 | logger.info(
67 | "MCP Server connected and listening via stdio transport.",
68 | operationContext,
69 | );
70 | if (process.stdout.isTTY) {
71 | console.log(
72 | `\n🚀 MCP Server running in STDIO mode.\n (MCP Spec: 2025-03-26 Stdio Transport)\n`,
73 | );
74 | }
75 | } catch (err) {
76 | // Let the ErrorHandler log the error with all context, then rethrow.
77 | throw ErrorHandler.handleError(err, {
78 | operation: "connectStdioTransport",
79 | context: operationContext,
80 | critical: true,
81 | rethrow: true,
82 | });
83 | }
84 | }
85 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedGenerateChart/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Registers the 'pubmed_generate_chart' tool with the MCP server.
3 | * This tool now accepts parameterized input for generating charts.
4 | * @module src/mcp-server/tools/pubmedGenerateChart/registration
5 | */
6 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7 | import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
8 | import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
9 | import {
10 | ErrorHandler,
11 | logger,
12 | RequestContext,
13 | requestContextService,
14 | } from "../../../utils/index.js";
15 | import {
16 | PubMedGenerateChartInput,
17 | PubMedGenerateChartInputSchema,
18 | pubmedGenerateChartLogic,
19 | } from "./logic.js";
20 |
21 | export async function registerPubMedGenerateChartTool(
22 | server: McpServer,
23 | ): Promise<void> {
24 | const operation = "registerPubMedGenerateChartTool";
25 | const toolName = "pubmed_generate_chart";
26 | const toolDescription =
27 | "Generates a customizable chart (PNG) from structured data. Supports various plot types and requires data values and field mappings for axes. Returns a Base64-encoded PNG image.";
28 | const context = requestContextService.createRequestContext({ operation });
29 |
30 | await ErrorHandler.tryCatch(
31 | async () => {
32 | server.tool(
33 | toolName,
34 | toolDescription,
35 | PubMedGenerateChartInputSchema.shape,
36 | async (
37 | input: PubMedGenerateChartInput,
38 | mcpProvidedContext: unknown,
39 | ): Promise<CallToolResult> => {
40 | const richContext: RequestContext =
41 | requestContextService.createRequestContext({
42 | parentRequestId: context.requestId,
43 | operation: "pubmedGenerateChartToolHandler",
44 | mcpToolContext: mcpProvidedContext,
45 | input,
46 | });
47 |
48 | try {
49 | const result = await pubmedGenerateChartLogic(input, richContext);
50 | return {
51 | content: [
52 | {
53 | type: "image",
54 | data: result.base64Data,
55 | mimeType: "image/png",
56 | },
57 | ],
58 | isError: false,
59 | };
60 | } catch (error) {
61 | const handledError = ErrorHandler.handleError(error, {
62 | operation: "pubmedGenerateChartToolHandler",
63 | context: richContext,
64 | input,
65 | rethrow: false,
66 | });
67 |
68 | const mcpError =
69 | handledError instanceof McpError
70 | ? handledError
71 | : new McpError(
72 | BaseErrorCode.INTERNAL_ERROR,
73 | "An unexpected error occurred while generating the chart.",
74 | {
75 | originalErrorName: handledError.name,
76 | originalErrorMessage: handledError.message,
77 | },
78 | );
79 |
80 | return {
81 | content: [
82 | {
83 | type: "text",
84 | text: JSON.stringify({
85 | error: {
86 | code: mcpError.code,
87 | message: mcpError.message,
88 | details: mcpError.details,
89 | },
90 | }),
91 | },
92 | ],
93 | isError: true,
94 | };
95 | }
96 | },
97 | );
98 | logger.notice(`Tool '${toolName}' registered.`, context);
99 | },
100 | {
101 | operation,
102 | context,
103 | errorCode: BaseErrorCode.INITIALIZATION_FAILED,
104 | critical: true,
105 | },
106 | );
107 | }
108 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedSearchArticles/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Registration for the pubmed_search_articles MCP tool.
3 | * @module src/mcp-server/tools/pubmedSearchArticles/registration
4 | */
5 |
6 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7 | import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
8 | import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
9 | import {
10 | ErrorHandler,
11 | logger,
12 | RequestContext,
13 | requestContextService,
14 | } from "../../../utils/index.js";
15 | import {
16 | PubMedSearchArticlesInput,
17 | PubMedSearchArticlesInputSchema,
18 | pubmedSearchArticlesLogic,
19 | } from "./logic.js";
20 |
21 | /**
22 | * Registers the pubmed_search_articles tool with the MCP server.
23 | * @param server - The McpServer instance.
24 | */
25 | export async function registerPubMedSearchArticlesTool(
26 | server: McpServer,
27 | ): Promise<void> {
28 | const operation = "registerPubMedSearchArticlesTool";
29 | const toolName = "pubmed_search_articles";
30 | const toolDescription =
31 | "Searches PubMed for articles using a query term and optional filters (max results, sort, date range, publication types). Uses NCBI ESearch to find PMIDs and ESummary (optional) for brief summaries. Returns a JSON object with search parameters, ESearch term, result counts, PMIDs, optional summaries, and E-utility URLs.";
32 | const context = requestContextService.createRequestContext({ operation });
33 |
34 | await ErrorHandler.tryCatch(
35 | async () => {
36 | server.tool(
37 | toolName,
38 | toolDescription,
39 | PubMedSearchArticlesInputSchema.shape,
40 | async (
41 | input: PubMedSearchArticlesInput,
42 | mcpProvidedContext: unknown,
43 | ): Promise<CallToolResult> => {
44 | const richContext: RequestContext =
45 | requestContextService.createRequestContext({
46 | parentRequestId: context.requestId,
47 | operation: "pubmedSearchArticlesToolHandler",
48 | mcpToolContext: mcpProvidedContext,
49 | input,
50 | });
51 |
52 | try {
53 | const result = await pubmedSearchArticlesLogic(input, richContext);
54 | return {
55 | content: [
56 | { type: "text", text: JSON.stringify(result, null, 2) },
57 | ],
58 | isError: false,
59 | };
60 | } catch (error) {
61 | const handledError = ErrorHandler.handleError(error, {
62 | operation: "pubmedSearchArticlesToolHandler",
63 | context: richContext,
64 | input,
65 | rethrow: false,
66 | });
67 |
68 | const mcpError =
69 | handledError instanceof McpError
70 | ? handledError
71 | : new McpError(
72 | BaseErrorCode.INTERNAL_ERROR,
73 | "An unexpected error occurred while searching PubMed articles.",
74 | {
75 | originalErrorName: handledError.name,
76 | originalErrorMessage: handledError.message,
77 | },
78 | );
79 |
80 | return {
81 | content: [
82 | {
83 | type: "text",
84 | text: JSON.stringify({
85 | error: {
86 | code: mcpError.code,
87 | message: mcpError.message,
88 | details: mcpError.details,
89 | },
90 | }),
91 | },
92 | ],
93 | isError: true,
94 | };
95 | }
96 | },
97 | );
98 | logger.notice(`Tool '${toolName}' registered.`, context);
99 | },
100 | {
101 | operation,
102 | context,
103 | errorCode: BaseErrorCode.INITIALIZATION_FAILED,
104 | critical: true,
105 | },
106 | );
107 | }
108 |
```
--------------------------------------------------------------------------------
/server.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
3 | "name": "io.github.cyanheads/pubmed-mcp-server",
4 | "description": "Comprehensive PubMed MCP Server to search, retrieve, and analyze biomedical literature from NCBI.",
5 | "status": "active",
6 | "repository": {
7 | "url": "https://github.com/cyanheads/pubmed-mcp-server",
8 | "source": "github"
9 | },
10 | "website_url": "https://github.com/cyanheads/pubmed-mcp-server#readme",
11 | "version": "1.4.4",
12 | "packages": [
13 | {
14 | "registry_type": "npm",
15 | "registry_base_url": "https://registry.npmjs.org",
16 | "identifier": "@cyanheads/pubmed-mcp-server",
17 | "version": "1.4.4",
18 | "runtime_hint": "node",
19 | "package_arguments": [
20 | {
21 | "type": "positional",
22 | "value": "dist/index.js"
23 | }
24 | ],
25 | "environment_variables": [
26 | {
27 | "name": "MCP_TRANSPORT_TYPE",
28 | "description": "Specifies the transport mechanism for the server.",
29 | "format": "string",
30 | "is_required": true,
31 | "default": "stdio"
32 | },
33 | {
34 | "name": "MCP_LOG_LEVEL",
35 | "description": "Sets the minimum log level for output (e.g., 'debug', 'info', 'warn').",
36 | "format": "string",
37 | "is_required": false,
38 | "default": "info"
39 | },
40 | {
41 | "name": "NCBI_API_KEY",
42 | "description": "Your NCBI API key for higher rate limits.",
43 | "format": "string",
44 | "is_required": false
45 | }
46 | ],
47 | "transport": {
48 | "type": "stdio"
49 | }
50 | },
51 | {
52 | "registry_type": "npm",
53 | "registry_base_url": "https://registry.npmjs.org",
54 | "identifier": "@cyanheads/pubmed-mcp-server",
55 | "version": "1.4.4",
56 | "runtime_hint": "node",
57 | "package_arguments": [
58 | {
59 | "type": "positional",
60 | "value": "dist/index.js"
61 | }
62 | ],
63 | "environment_variables": [
64 | {
65 | "name": "MCP_TRANSPORT_TYPE",
66 | "description": "Specifies the transport mechanism for the server.",
67 | "format": "string",
68 | "is_required": true,
69 | "default": "http"
70 | },
71 | {
72 | "name": "MCP_HTTP_HOST",
73 | "description": "The host for the HTTP server.",
74 | "format": "string",
75 | "is_required": false,
76 | "default": "localhost"
77 | },
78 | {
79 | "name": "MCP_HTTP_PORT",
80 | "description": "The port for the HTTP server.",
81 | "format": "string",
82 | "is_required": false,
83 | "default": "3017"
84 | },
85 | {
86 | "name": "MCP_HTTP_ENDPOINT_PATH",
87 | "description": "The endpoint path for MCP requests.",
88 | "format": "string",
89 | "is_required": false,
90 | "default": "/mcp"
91 | },
92 | {
93 | "name": "MCP_AUTH_MODE",
94 | "description": "Authentication mode: 'none', 'jwt', or 'oauth'.",
95 | "format": "string",
96 | "is_required": false,
97 | "default": "none"
98 | },
99 | {
100 | "name": "MCP_LOG_LEVEL",
101 | "description": "Sets the minimum log level (e.g., 'debug', 'info', 'warn').",
102 | "format": "string",
103 | "is_required": false,
104 | "default": "info"
105 | },
106 | {
107 | "name": "NCBI_API_KEY",
108 | "description": "Your NCBI API key for higher rate limits.",
109 | "format": "string",
110 | "is_required": false
111 | }
112 | ],
113 | "transport": {
114 | "type": "streamable-http",
115 | "url": "http://localhost:3017/mcp"
116 | }
117 | }
118 | ],
119 | "mcpName": "io.github.cyanheads/pubmed-mcp-server"
120 | }
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedFetchContents/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Registration for the pubmed_fetch_contents MCP tool.
3 | * @module src/mcp-server/tools/pubmedFetchContents/registration
4 | */
5 |
6 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7 | import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
8 | import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
9 | import {
10 | ErrorHandler,
11 | logger,
12 | RequestContext,
13 | requestContextService,
14 | } from "../../../utils/index.js";
15 | import {
16 | PubMedFetchContentsInput,
17 | PubMedFetchContentsInputSchema,
18 | pubMedFetchContentsLogic,
19 | } from "./logic.js";
20 |
21 | /**
22 | * Registers the pubmed_fetch_contents tool with the MCP server.
23 | * @param server - The McpServer instance.
24 | */
25 | export async function registerPubMedFetchContentsTool(
26 | server: McpServer,
27 | ): Promise<void> {
28 | const operation = "registerPubMedFetchContentsTool";
29 | const toolName = "pubmed_fetch_contents";
30 | const toolDescription =
31 | "Fetches detailed information from PubMed using NCBI EFetch. Can be used with a direct list of PMIDs or with queryKey/webEnv from an ESearch history entry. Supports pagination (retstart, retmax) when using history. Available 'detailLevel' options: 'abstract_plus' (parsed details), 'full_xml' (raw PubMedArticle XML), 'medline_text' (MEDLINE format), or 'citation_data' (minimal citation data). Returns a JSON object containing results, any PMIDs not found (if applicable), and EFetch details.";
32 |
33 | const context = requestContextService.createRequestContext({ operation });
34 |
35 | await ErrorHandler.tryCatch(
36 | async () => {
37 | server.tool(
38 | toolName,
39 | toolDescription,
40 | PubMedFetchContentsInputSchema._def.schema.shape,
41 | async (
42 | input: PubMedFetchContentsInput,
43 | toolContext: unknown,
44 | ): Promise<CallToolResult> => {
45 | const richContext: RequestContext =
46 | requestContextService.createRequestContext({
47 | parentRequestId: context.requestId,
48 | operation: "pubMedFetchContentsToolHandler",
49 | mcpToolContext: toolContext,
50 | input,
51 | });
52 |
53 | try {
54 | const result = await pubMedFetchContentsLogic(input, richContext);
55 | return {
56 | content: [{ type: "text", text: result.content }],
57 | isError: false,
58 | };
59 | } catch (error) {
60 | const handledError = ErrorHandler.handleError(error, {
61 | operation: "pubMedFetchContentsToolHandler",
62 | context: richContext,
63 | input,
64 | rethrow: false,
65 | });
66 |
67 | const mcpError =
68 | handledError instanceof McpError
69 | ? handledError
70 | : new McpError(
71 | BaseErrorCode.INTERNAL_ERROR,
72 | "An unexpected error occurred while fetching PubMed content.",
73 | {
74 | originalErrorName: handledError.name,
75 | originalErrorMessage: handledError.message,
76 | },
77 | );
78 |
79 | return {
80 | content: [
81 | {
82 | type: "text",
83 | text: JSON.stringify({
84 | error: {
85 | code: mcpError.code,
86 | message: mcpError.message,
87 | details: mcpError.details,
88 | },
89 | }),
90 | },
91 | ],
92 | isError: true,
93 | };
94 | }
95 | },
96 | );
97 |
98 | logger.notice(`Tool '${toolName}' registered.`, context);
99 | },
100 | {
101 | operation,
102 | context,
103 | errorCode: BaseErrorCode.INITIALIZATION_FAILED,
104 | critical: true,
105 | },
106 | );
107 | }
108 |
```
--------------------------------------------------------------------------------
/src/utils/internal/performance.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Provides a utility for performance monitoring of tool execution.
3 | * This module introduces a higher-order function to wrap tool logic, measure its
4 | * execution time, and log a structured metrics event.
5 | * @module src/utils/internal/performance
6 | */
7 |
8 | import { SpanStatusCode, trace } from "@opentelemetry/api";
9 | import {
10 | ATTR_CODE_FUNCTION,
11 | ATTR_CODE_NAMESPACE,
12 | } from "../telemetry/semconv.js";
13 | import { config } from "../../config/index.js";
14 | import { McpError } from "../../types-global/errors.js";
15 | import { logger } from "./logger.js";
16 | import { RequestContext } from "./requestContext.js";
17 |
18 | /**
19 | * Calculates the size of a payload in bytes.
20 | * @param payload - The payload to measure.
21 | * @returns The size in bytes.
22 | * @private
23 | */
24 | function getPayloadSize(payload: unknown): number {
25 | if (!payload) return 0;
26 | try {
27 | const stringified = JSON.stringify(payload);
28 | return Buffer.byteLength(stringified, "utf8");
29 | } catch {
30 | return 0; // Could not stringify
31 | }
32 | }
33 |
34 | /**
35 | * A higher-order function that wraps a tool's core logic to measure its performance
36 | * and log a structured metrics event upon completion.
37 | *
38 | * @template T The expected return type of the tool's logic function.
39 | * @param toolLogicFn - The asynchronous tool logic function to be executed and measured.
40 | * @param context - The request context for the operation, used for logging and tracing.
41 | * @param inputPayload - The input payload to the tool for size calculation.
42 | * @returns A promise that resolves with the result of the tool logic function.
43 | * @throws Re-throws any error caught from the tool logic function after logging the failure.
44 | */
45 | export async function measureToolExecution<T>(
46 | toolLogicFn: () => Promise<T>,
47 | context: RequestContext & { toolName: string },
48 | inputPayload: unknown,
49 | ): Promise<T> {
50 | const tracer = trace.getTracer(
51 | config.openTelemetry.serviceName,
52 | config.openTelemetry.serviceVersion,
53 | );
54 | const { toolName } = context;
55 |
56 | return tracer.startActiveSpan(`tool_execution:${toolName}`, async (span) => {
57 | span.setAttributes({
58 | [ATTR_CODE_FUNCTION]: toolName,
59 | [ATTR_CODE_NAMESPACE]: "mcp-tools",
60 | "mcp.tool.input_bytes": getPayloadSize(inputPayload),
61 | });
62 |
63 | const startTime = process.hrtime.bigint();
64 | let isSuccess = false;
65 | let errorCode: string | undefined;
66 | let outputPayload: T | undefined;
67 |
68 | try {
69 | const result = await toolLogicFn();
70 | isSuccess = true;
71 | outputPayload = result;
72 | span.setStatus({ code: SpanStatusCode.OK });
73 | span.setAttribute("mcp.tool.output_bytes", getPayloadSize(outputPayload));
74 | return result;
75 | } catch (error) {
76 | if (error instanceof McpError) {
77 | errorCode = error.code;
78 | } else if (error instanceof Error) {
79 | errorCode = "UNHANDLED_ERROR";
80 | } else {
81 | errorCode = "UNKNOWN_ERROR";
82 | }
83 |
84 | if (error instanceof Error) {
85 | span.recordException(error);
86 | }
87 | span.setStatus({
88 | code: SpanStatusCode.ERROR,
89 | message: error instanceof Error ? error.message : String(error),
90 | });
91 |
92 | throw error;
93 | } finally {
94 | const endTime = process.hrtime.bigint();
95 | const durationMs = Number(endTime - startTime) / 1_000_000;
96 |
97 | span.setAttributes({
98 | "mcp.tool.duration_ms": parseFloat(durationMs.toFixed(2)),
99 | "mcp.tool.success": isSuccess,
100 | });
101 | if (errorCode) {
102 | span.setAttribute("mcp.tool.error_code", errorCode);
103 | }
104 |
105 | span.end();
106 |
107 | logger.info("Tool execution finished.", {
108 | ...context,
109 | metrics: {
110 | durationMs: parseFloat(durationMs.toFixed(2)),
111 | isSuccess,
112 | errorCode,
113 | inputBytes: getPayloadSize(inputPayload),
114 | outputBytes: getPayloadSize(outputPayload),
115 | },
116 | });
117 | }
118 | });
119 | }
120 |
```
--------------------------------------------------------------------------------
/src/utils/network/fetchWithTimeout.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Provides a utility function to make fetch requests with a specified timeout.
3 | * @module src/utils/network/fetchWithTimeout
4 | */
5 |
6 | import { logger } from "../internal/logger.js"; // Adjusted import path
7 | import type { RequestContext } from "../internal/requestContext.js"; // Adjusted import path
8 | import { McpError, BaseErrorCode } from "../../types-global/errors.js";
9 |
10 | /**
11 | * Options for the fetchWithTimeout utility.
12 | * Extends standard RequestInit but omits 'signal' as it's handled internally.
13 | */
14 | export type FetchWithTimeoutOptions = Omit<RequestInit, "signal">;
15 |
16 | /**
17 | * Fetches a resource with a specified timeout.
18 | *
19 | * @param url - The URL to fetch.
20 | * @param timeoutMs - The timeout duration in milliseconds.
21 | * @param context - The request context for logging.
22 | * @param options - Optional fetch options (RequestInit), excluding 'signal'.
23 | * @returns A promise that resolves to the Response object.
24 | * @throws {McpError} If the request times out or another fetch-related error occurs.
25 | */
26 | export async function fetchWithTimeout(
27 | url: string | URL,
28 | timeoutMs: number,
29 | context: RequestContext,
30 | options?: FetchWithTimeoutOptions,
31 | ): Promise<Response> {
32 | const controller = new AbortController();
33 | const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
34 |
35 | const urlString = url.toString();
36 | const operationDescription = `fetch ${options?.method || "GET"} ${urlString}`;
37 |
38 | logger.debug(
39 | `Attempting ${operationDescription} with ${timeoutMs}ms timeout.`,
40 | context,
41 | );
42 |
43 | try {
44 | const response = await fetch(url, {
45 | ...options,
46 | signal: controller.signal,
47 | });
48 | clearTimeout(timeoutId);
49 |
50 | if (!response.ok) {
51 | const errorBody = await response
52 | .text()
53 | .catch(() => "Could not read response body");
54 | logger.error(
55 | `Fetch failed for ${urlString} with status ${response.status}.`,
56 | {
57 | ...context,
58 | statusCode: response.status,
59 | statusText: response.statusText,
60 | responseBody: errorBody,
61 | errorSource: "FetchHttpError",
62 | },
63 | );
64 | throw new McpError(
65 | BaseErrorCode.SERVICE_UNAVAILABLE,
66 | `Fetch failed for ${urlString}. Status: ${response.status}`,
67 | {
68 | ...context,
69 | statusCode: response.status,
70 | statusText: response.statusText,
71 | responseBody: errorBody,
72 | },
73 | );
74 | }
75 |
76 | logger.debug(
77 | `Successfully fetched ${urlString}. Status: ${response.status}`,
78 | context,
79 | );
80 | return response;
81 | } catch (error) {
82 | clearTimeout(timeoutId);
83 | if (error instanceof Error && error.name === "AbortError") {
84 | logger.error(`${operationDescription} timed out after ${timeoutMs}ms.`, {
85 | ...context,
86 | errorSource: "FetchTimeout",
87 | });
88 | throw new McpError(
89 | BaseErrorCode.TIMEOUT,
90 | `${operationDescription} timed out.`,
91 | { ...context, errorSource: "FetchTimeout" },
92 | );
93 | }
94 |
95 | // Log and re-throw other errors as McpError
96 | const errorMessage = error instanceof Error ? error.message : String(error);
97 | logger.error(
98 | `Network error during ${operationDescription}: ${errorMessage}`,
99 | {
100 | ...context,
101 | originalErrorName: error instanceof Error ? error.name : "UnknownError",
102 | errorSource: "FetchNetworkError",
103 | },
104 | );
105 |
106 | if (error instanceof McpError) {
107 | // If it's already an McpError, re-throw it
108 | throw error;
109 | }
110 |
111 | throw new McpError(
112 | BaseErrorCode.SERVICE_UNAVAILABLE, // Generic error for network/service issues
113 | `Network error during ${operationDescription}: ${errorMessage}`,
114 | {
115 | ...context,
116 | originalErrorName: error instanceof Error ? error.name : "UnknownError",
117 | errorSource: "FetchNetworkErrorWrapper",
118 | },
119 | );
120 | }
121 | }
122 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedArticleConnections/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Registers the 'pubmed_article_connections' tool with the MCP server.
3 | * This tool finds articles related to a source PMID or retrieves citation formats.
4 | * @module src/mcp-server/tools/pubmedArticleConnections/registration
5 | */
6 |
7 | import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8 | import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
9 | import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
10 | import {
11 | ErrorHandler,
12 | logger,
13 | RequestContext,
14 | requestContextService,
15 | } from "../../../utils/index.js";
16 | import {
17 | PubMedArticleConnectionsInput,
18 | PubMedArticleConnectionsInputSchema,
19 | handlePubMedArticleConnections,
20 | } from "./logic/index.js";
21 |
22 | /**
23 | * Registers the 'pubmed_article_connections' tool with the given MCP server instance.
24 | * @param {McpServer} server - The MCP server instance.
25 | */
26 | export async function registerPubMedArticleConnectionsTool(
27 | server: McpServer,
28 | ): Promise<void> {
29 | const operation = "registerPubMedArticleConnectionsTool";
30 | const toolName = "pubmed_article_connections";
31 | const toolDescription =
32 | "Finds articles related to a source PubMed ID (PMID) or retrieves formatted citations for it. Supports finding similar articles, articles that cite the source, articles referenced by the source (via NCBI ELink), or fetching data to generate citations in various styles (RIS, BibTeX, APA, MLA via NCBI EFetch and server-side formatting). Returns a JSON object detailing the connections or formatted citations.";
33 | const context = requestContextService.createRequestContext({ operation });
34 |
35 | await ErrorHandler.tryCatch(
36 | async () => {
37 | server.tool(
38 | toolName,
39 | toolDescription,
40 | PubMedArticleConnectionsInputSchema.shape,
41 | async (
42 | input: PubMedArticleConnectionsInput,
43 | mcpProvidedContext: unknown,
44 | ): Promise<CallToolResult> => {
45 | const richContext: RequestContext =
46 | requestContextService.createRequestContext({
47 | parentRequestId: context.requestId,
48 | operation: "pubMedArticleConnectionsToolHandler",
49 | mcpToolContext: mcpProvidedContext,
50 | input,
51 | });
52 |
53 | try {
54 | const result = await handlePubMedArticleConnections(
55 | input,
56 | richContext,
57 | );
58 | return {
59 | content: [
60 | { type: "text", text: JSON.stringify(result, null, 2) },
61 | ],
62 | isError: false,
63 | };
64 | } catch (error) {
65 | const handledError = ErrorHandler.handleError(error, {
66 | operation: "pubMedArticleConnectionsToolHandler",
67 | context: richContext,
68 | input,
69 | rethrow: false,
70 | });
71 |
72 | const mcpError =
73 | handledError instanceof McpError
74 | ? handledError
75 | : new McpError(
76 | BaseErrorCode.INTERNAL_ERROR,
77 | "An unexpected error occurred while getting PubMed article connections.",
78 | {
79 | originalErrorName: handledError.name,
80 | originalErrorMessage: handledError.message,
81 | },
82 | );
83 |
84 | return {
85 | content: [
86 | {
87 | type: "text",
88 | text: JSON.stringify({
89 | error: {
90 | code: mcpError.code,
91 | message: mcpError.message,
92 | details: mcpError.details,
93 | },
94 | }),
95 | },
96 | ],
97 | isError: true,
98 | };
99 | }
100 | },
101 | );
102 | logger.notice(`Tool '${toolName}' registered.`, context);
103 | },
104 | {
105 | operation,
106 | context,
107 | errorCode: BaseErrorCode.INITIALIZATION_FAILED,
108 | critical: true,
109 | },
110 | );
111 | }
112 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedResearchAgent/registration.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Registration for the pubmed_research_agent tool.
3 | * @module pubmedResearchAgent/registration
4 | */
5 |
6 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7 | import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
8 | import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
9 | import {
10 | ErrorHandler,
11 | logger,
12 | RequestContext,
13 | requestContextService,
14 | } from "../../../utils/index.js";
15 | import { pubmedResearchAgentLogic } from "./logic.js";
16 | import {
17 | PubMedResearchAgentInput,
18 | PubMedResearchAgentInputSchema,
19 | } from "./logic/index.js";
20 |
21 | /**
22 | * Registers the pubmed_research_agent tool with the MCP server.
23 | * @param server - The McpServer instance.
24 | */
25 | export async function registerPubMedResearchAgentTool(
26 | server: McpServer,
27 | ): Promise<void> {
28 | const operation = "registerPubMedResearchAgentTool";
29 | const toolName = "pubmed_research_agent";
30 | const toolDescription =
31 | "Generates a standardized JSON research plan outline from component details you provide. It accepts granular inputs for all research phases (conception, data collection, analysis, dissemination, cross-cutting concerns). If `include_detailed_prompts_for_agent` is true, the output plan will embed instructive prompts and detailed guidance notes to aid the research agent. The tool's primary function is to organize and structure your rough ideas into a formal, machine-readable plan. This plan is intended for further processing; as the research agent, you should then utilize your full suite of tools (e.g., file manipulation, `get_pubmed_article_connections` for literature/data search via PMID) to execute the outlined research, tailored to the user's request.";
32 | const context = requestContextService.createRequestContext({ operation });
33 |
34 | await ErrorHandler.tryCatch(
35 | async () => {
36 | server.tool(
37 | toolName,
38 | toolDescription,
39 | PubMedResearchAgentInputSchema.shape,
40 | async (
41 | input: PubMedResearchAgentInput,
42 | mcpProvidedContext: unknown,
43 | ): Promise<CallToolResult> => {
44 | const richContext: RequestContext =
45 | requestContextService.createRequestContext({
46 | parentRequestId: context.requestId,
47 | operation: "pubmedResearchAgentToolHandler",
48 | mcpToolContext: mcpProvidedContext,
49 | input,
50 | });
51 |
52 | try {
53 | const result = await pubmedResearchAgentLogic(input, richContext);
54 | return {
55 | content: [
56 | { type: "text", text: JSON.stringify(result, null, 2) },
57 | ],
58 | isError: false,
59 | };
60 | } catch (error) {
61 | const handledError = ErrorHandler.handleError(error, {
62 | operation: "pubmedResearchAgentToolHandler",
63 | context: richContext,
64 | input,
65 | rethrow: false,
66 | });
67 |
68 | const mcpError =
69 | handledError instanceof McpError
70 | ? handledError
71 | : new McpError(
72 | BaseErrorCode.INTERNAL_ERROR,
73 | "An unexpected error occurred while generating the research plan.",
74 | {
75 | originalErrorName: handledError.name,
76 | originalErrorMessage: handledError.message,
77 | },
78 | );
79 |
80 | return {
81 | content: [
82 | {
83 | type: "text",
84 | text: JSON.stringify({
85 | error: {
86 | code: mcpError.code,
87 | message: mcpError.message,
88 | details: mcpError.details,
89 | },
90 | }),
91 | },
92 | ],
93 | isError: true,
94 | };
95 | }
96 | },
97 | );
98 | logger.notice(`Tool '${toolName}' registered.`, context);
99 | },
100 | {
101 | operation,
102 | context,
103 | errorCode: BaseErrorCode.INITIALIZATION_FAILED,
104 | critical: true,
105 | },
106 | );
107 | }
108 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Defines TypeScript types for the structured research plan outline
3 | * generated by the pubmed_research_agent tool. The tool primarily structures
4 | * client-provided inputs.
5 | * @module pubmedResearchAgent/logic/outputTypes
6 | */
7 |
8 | // All string fields are optional as they depend on client input.
9 | // If include_detailed_prompts_for_agent is true, these strings might be prefixed
10 | // with a generic instruction by the planOrchestrator.
11 |
12 | export interface Phase1Step1_1_Content {
13 | primary_research_question?: string;
14 | knowledge_gap_statement?: string;
15 | primary_hypothesis?: string;
16 | pubmed_search_strategy?: string;
17 | guidance_notes?: string | string[];
18 | }
19 | export interface Phase1Step1_2_Content {
20 | literature_review_scope?: string;
21 | key_databases_and_search_approach?: string;
22 | guidance_notes?: string | string[];
23 | }
24 | export interface Phase1Step1_3_Content {
25 | experimental_paradigm?: string;
26 | data_acquisition_plan_existing_data?: string;
27 | data_acquisition_plan_new_data?: string;
28 | blast_utilization_plan?: string;
29 | controls_and_rigor_measures?: string;
30 | methodological_challenges_and_mitigation?: string;
31 | guidance_notes?: string | string[];
32 | }
33 | export interface Phase1Output {
34 | title: "Phase 1: Conception and Planning";
35 | step_1_1_research_question_and_hypothesis: Phase1Step1_1_Content;
36 | step_1_2_literature_review_strategy: Phase1Step1_2_Content;
37 | step_1_3_experimental_design_and_data_acquisition: Phase1Step1_3_Content;
38 | }
39 |
40 | export interface Phase2Step2_1_Content {
41 | data_collection_methods_wet_lab?: string;
42 | data_collection_methods_dry_lab?: string;
43 | guidance_notes?: string | string[];
44 | }
45 | export interface Phase2Step2_2_Content {
46 | data_preprocessing_and_qc_plan?: string;
47 | guidance_notes?: string | string[];
48 | }
49 | export interface Phase2Output {
50 | title: "Phase 2: Data Collection and Processing";
51 | step_2_1_data_collection_retrieval: Phase2Step2_1_Content;
52 | step_2_2_data_preprocessing_and_qc: Phase2Step2_2_Content;
53 | }
54 |
55 | export interface Phase3Step3_1_Content {
56 | data_analysis_strategy?: string;
57 | bioinformatics_pipeline_summary?: string;
58 | guidance_notes?: string | string[];
59 | }
60 | export interface Phase3Step3_2_Content {
61 | results_interpretation_framework?: string;
62 | comparison_with_literature_plan?: string;
63 | guidance_notes?: string | string[];
64 | }
65 | export interface Phase3Output {
66 | title: "Phase 3: Analysis and Interpretation";
67 | step_3_1_data_analysis_plan: Phase3Step3_1_Content;
68 | step_3_2_results_interpretation: Phase3Step3_2_Content;
69 | }
70 |
71 | export interface Phase4Step4_1_Content {
72 | dissemination_manuscript_plan?: string;
73 | dissemination_data_deposition_plan?: string;
74 | guidance_notes?: string | string[];
75 | }
76 | export interface Phase4Step4_2_Content {
77 | peer_review_and_publication_approach?: string;
78 | guidance_notes?: string | string[];
79 | }
80 | export interface Phase4Step4_3_Content {
81 | future_research_directions?: string;
82 | guidance_notes?: string | string[];
83 | }
84 | export interface Phase4Output {
85 | title: "Phase 4: Dissemination and Iteration";
86 | step_4_1_dissemination_strategy: Phase4Step4_1_Content;
87 | step_4_2_peer_review_and_publication: Phase4Step4_2_Content;
88 | step_4_3_further_research_and_iteration: Phase4Step4_3_Content;
89 | }
90 |
91 | export interface CrossCuttingContent {
92 | record_keeping_and_data_management?: string;
93 | collaboration_strategy?: string;
94 | ethical_considerations?: string;
95 | guidance_notes?: string | string[];
96 | }
97 | export interface CrossCuttingOutput {
98 | title: "Cross-Cutting Considerations";
99 | content: CrossCuttingContent;
100 | }
101 |
102 | export interface PubMedResearchPlanGeneratedOutput {
103 | plan_title: string;
104 | overall_instructions_for_research_agent?: string;
105 | input_summary: {
106 | keywords_received: string[];
107 | primary_goal_stated_or_inferred: string;
108 | organism_focus?: string; // Ensured this is present
109 | included_detailed_prompts_for_agent: boolean; // Renamed from included_challenges_consideration for clarity
110 | };
111 | phase_1_conception_and_planning: Phase1Output;
112 | phase_2_data_collection_and_processing: Phase2Output;
113 | phase_3_analysis_and_interpretation: Phase3Output;
114 | phase_4_dissemination_and_iteration: Phase4Output;
115 | cross_cutting_considerations: CrossCuttingOutput;
116 | }
117 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/http/mcpTransportMiddleware.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Hono middleware for handling MCP transport logic.
3 | * This middleware encapsulates the logic for processing MCP requests,
4 | * delegating to the appropriate transport manager, and preparing the
5 | * response for Hono to send.
6 | * @module src/mcp-server/transports/http/mcpTransportMiddleware
7 | */
8 |
9 | import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
10 | import { MiddlewareHandler } from "hono";
11 | import { createMiddleware } from "hono/factory";
12 | import { IncomingHttpHeaders } from "http";
13 | import { config } from "../../../config/index.js";
14 | import { RequestContext, requestContextService } from "../../../utils/index.js";
15 | import { ServerInstanceInfo } from "../../server.js";
16 | import { StatefulTransportManager } from "../core/statefulTransportManager.js";
17 | import { StatelessTransportManager } from "../core/statelessTransportManager.js";
18 | import { TransportManager, TransportResponse } from "../core/transportTypes.js";
19 | import { HonoNodeBindings } from "./httpTypes.js";
20 |
21 | /**
22 | * Converts a Fetch API Headers object to Node.js IncomingHttpHeaders.
23 | * @param headers - The Headers object to convert.
24 | * @returns An object compatible with IncomingHttpHeaders.
25 | */
26 | function toIncomingHttpHeaders(headers: Headers): IncomingHttpHeaders {
27 | const result: IncomingHttpHeaders = {};
28 | headers.forEach((value, key) => {
29 | result[key] = value;
30 | });
31 | return result;
32 | }
33 |
34 | /**
35 | * Handles a stateless request by creating an ephemeral transport manager.
36 | * @param createServerInstanceFn - Function to create an McpServer instance.
37 | * @param headers - The request headers.
38 | * @param body - The request body.
39 | * @param context - The request context.
40 | * @returns A promise resolving with the transport response.
41 | */
42 | async function handleStatelessRequest(
43 | createServerInstanceFn: () => Promise<ServerInstanceInfo>,
44 | headers: Headers,
45 | body: unknown,
46 | context: RequestContext,
47 | ): Promise<TransportResponse> {
48 | const getMcpServer = async () => (await createServerInstanceFn()).server;
49 | const statelessManager = new StatelessTransportManager(getMcpServer);
50 | return statelessManager.handleRequest(
51 | toIncomingHttpHeaders(headers),
52 | body,
53 | context,
54 | );
55 | }
56 |
57 | /**
58 | * Creates a Hono middleware for handling MCP POST requests.
59 | * @param transportManager - The main transport manager (usually stateful).
60 | * @param createServerInstanceFn - Function to create an McpServer instance.
61 | * @returns A Hono middleware function.
62 | */
63 |
64 | type McpMiddlewareEnv = {
65 | Variables: {
66 | mcpResponse: TransportResponse;
67 | };
68 | };
69 |
70 | export const mcpTransportMiddleware = (
71 | transportManager: TransportManager,
72 | createServerInstanceFn: () => Promise<ServerInstanceInfo>,
73 | ): MiddlewareHandler<McpMiddlewareEnv & { Bindings: HonoNodeBindings }> => {
74 | return createMiddleware<McpMiddlewareEnv & { Bindings: HonoNodeBindings }>(
75 | async (c, next) => {
76 | const sessionId = c.req.header("mcp-session-id");
77 | const context = requestContextService.createRequestContext({
78 | operation: "mcpTransportMiddleware",
79 | sessionId,
80 | });
81 |
82 | const body = await c.req.json();
83 | let response: TransportResponse;
84 |
85 | if (isInitializeRequest(body)) {
86 | if (config.mcpSessionMode === "stateless") {
87 | response = await handleStatelessRequest(
88 | createServerInstanceFn,
89 | c.req.raw.headers,
90 | body,
91 | context,
92 | );
93 | } else {
94 | response = await (
95 | transportManager as StatefulTransportManager
96 | ).initializeAndHandle(
97 | toIncomingHttpHeaders(c.req.raw.headers),
98 | body,
99 | context,
100 | );
101 | }
102 | } else {
103 | if (sessionId) {
104 | response = await transportManager.handleRequest(
105 | toIncomingHttpHeaders(c.req.raw.headers),
106 | body,
107 | context,
108 | sessionId,
109 | );
110 | } else {
111 | response = await handleStatelessRequest(
112 | createServerInstanceFn,
113 | c.req.raw.headers,
114 | body,
115 | context,
116 | );
117 | }
118 | }
119 |
120 | c.set("mcpResponse", response);
121 | await next();
122 | },
123 | );
124 | };
125 |
```
--------------------------------------------------------------------------------
/src/services/NCBI/core/ncbiRequestQueueManager.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Manages a queue for NCBI E-utility requests to ensure compliance with rate limits.
3 | * @module src/services/NCBI/core/ncbiRequestQueueManager
4 | */
5 |
6 | import { config } from "../../../config/index.js";
7 | import {
8 | logger,
9 | RequestContext,
10 | requestContextService,
11 | sanitizeInputForLogging,
12 | } from "../../../utils/index.js";
13 | import { NcbiRequestParams } from "./ncbiConstants.js";
14 |
15 | /**
16 | * Interface for a queued NCBI request.
17 | */
18 | export interface QueuedRequest<T = unknown> {
19 | resolve: (value: T | PromiseLike<T>) => void;
20 | reject: (reason?: unknown) => void;
21 | task: () => Promise<T>; // The actual function that makes the API call
22 | context: RequestContext;
23 | endpoint: string; // For logging purposes
24 | params: NcbiRequestParams; // For logging purposes
25 | }
26 |
27 | export class NcbiRequestQueueManager {
28 | private requestQueue: QueuedRequest[] = [];
29 | private isProcessingQueue = false;
30 | private lastRequestTime = 0;
31 |
32 | constructor() {
33 | // The constructor is kept for future initializations, if any.
34 | }
35 |
36 | /**
37 | * Processes the request queue, ensuring delays between requests to respect NCBI rate limits.
38 | */
39 | private async processQueue(): Promise<void> {
40 | if (this.isProcessingQueue || this.requestQueue.length === 0) {
41 | return;
42 | }
43 | this.isProcessingQueue = true;
44 |
45 | const requestItem = this.requestQueue.shift();
46 | if (!requestItem) {
47 | this.isProcessingQueue = false;
48 | return;
49 | }
50 |
51 | const { resolve, reject, task, context, endpoint, params } = requestItem;
52 |
53 | try {
54 | const now = Date.now();
55 | const timeSinceLastRequest = now - this.lastRequestTime;
56 | const delayNeeded = config.ncbiRequestDelayMs - timeSinceLastRequest;
57 |
58 | if (delayNeeded > 0) {
59 | logger.debug(
60 | `Delaying NCBI request by ${delayNeeded}ms to respect rate limit.`,
61 | requestContextService.createRequestContext({
62 | ...context,
63 | operation: "NCBI_RateLimitDelay",
64 | delayNeeded,
65 | endpoint,
66 | }),
67 | );
68 | await new Promise((r) => setTimeout(r, delayNeeded));
69 | }
70 |
71 | this.lastRequestTime = Date.now();
72 | logger.info(
73 | `Executing NCBI request via queue: ${endpoint}`,
74 | requestContextService.createRequestContext({
75 | ...context,
76 | operation: "NCBI_ExecuteFromQueue",
77 | endpoint,
78 | params: sanitizeInputForLogging(params),
79 | }),
80 | );
81 | const result = await task();
82 | resolve(result);
83 | } catch (error: unknown) {
84 | const err = error as Error;
85 | logger.error(
86 | "Error processing NCBI request from queue",
87 | err,
88 | requestContextService.createRequestContext({
89 | ...context,
90 | operation: "NCBI_QueueError",
91 | endpoint,
92 | params: sanitizeInputForLogging(params),
93 | errorMessage: err?.message,
94 | }),
95 | );
96 | reject(err);
97 | } finally {
98 | this.isProcessingQueue = false;
99 | if (this.requestQueue.length > 0) {
100 | // Ensure processQueue is called without awaiting it here to prevent deep stacks
101 | Promise.resolve().then(() => this.processQueue());
102 | }
103 | }
104 | }
105 |
106 | /**
107 | * Enqueues a task (an NCBI API call) to be processed.
108 | * @param task A function that returns a Promise resolving to the API call result.
109 | * @param context The request context for logging and correlation.
110 | * @param endpoint The NCBI endpoint being called (e.g., "esearch", "efetch").
111 | * @param params The parameters for the NCBI request.
112 | * @returns A Promise that resolves or rejects with the result of the task.
113 | */
114 | public enqueueRequest<T>(
115 | task: () => Promise<T>,
116 | context: RequestContext,
117 | endpoint: string,
118 | params: NcbiRequestParams,
119 | ): Promise<T> {
120 | return new Promise<T>((resolve, reject) => {
121 | this.requestQueue.push({
122 | resolve: (value: unknown) => resolve(value as T),
123 | reject: (reason?: unknown) => reject(reason),
124 | task,
125 | context,
126 | endpoint,
127 | params,
128 | });
129 | if (!this.isProcessingQueue) {
130 | // Ensure processQueue is called without awaiting it here
131 | Promise.resolve().then(() => this.processQueue());
132 | }
133 | });
134 | }
135 | }
136 |
```
--------------------------------------------------------------------------------
/src/services/NCBI/core/ncbiService.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Service for interacting with NCBI E-utilities.
3 | * This module centralizes all communication with NCBI's E-utility APIs,
4 | * handling request construction, API key management, rate limiting,
5 | * retries, and parsing of XML/JSON responses. It aims to provide a robust
6 | * and compliant interface for other parts of the pubmed-mcp-server to
7 | * access PubMed data.
8 | * @module src/services/NCBI/core/ncbiService
9 | */
10 |
11 | import {
12 | ESearchResult,
13 | EFetchArticleSet,
14 | ESearchResponseContainer,
15 | } from "../../../types-global/pubmedXml.js";
16 | import {
17 | logger,
18 | RequestContext,
19 | requestContextService,
20 | } from "../../../utils/index.js";
21 | import { NcbiRequestParams, NcbiRequestOptions } from "./ncbiConstants.js";
22 | import { NcbiCoreApiClient } from "./ncbiCoreApiClient.js";
23 | import { NcbiRequestQueueManager } from "./ncbiRequestQueueManager.js";
24 | import { NcbiResponseHandler } from "./ncbiResponseHandler.js";
25 |
26 | export class NcbiService {
27 | private queueManager: NcbiRequestQueueManager;
28 | private apiClient: NcbiCoreApiClient;
29 | private responseHandler: NcbiResponseHandler;
30 |
31 | constructor() {
32 | this.queueManager = new NcbiRequestQueueManager();
33 | this.apiClient = new NcbiCoreApiClient();
34 | this.responseHandler = new NcbiResponseHandler();
35 | }
36 |
37 | private async performNcbiRequest<T>(
38 | endpoint: string,
39 | params: NcbiRequestParams,
40 | context: RequestContext,
41 | options: NcbiRequestOptions = {},
42 | ): Promise<T> {
43 | const task = async () => {
44 | const rawResponse = await this.apiClient.makeRequest(
45 | endpoint,
46 | params,
47 | context,
48 | options,
49 | );
50 | return this.responseHandler.parseAndHandleResponse<T>(
51 | rawResponse,
52 | endpoint,
53 | context,
54 | options,
55 | );
56 | };
57 |
58 | return this.queueManager.enqueueRequest<T>(task, context, endpoint, params);
59 | }
60 |
61 | public async eSearch(
62 | params: NcbiRequestParams,
63 | context: RequestContext,
64 | ): Promise<ESearchResult> {
65 | const response = await this.performNcbiRequest<ESearchResponseContainer>(
66 | "esearch",
67 | params,
68 | context,
69 | {
70 | retmode: "xml",
71 | },
72 | );
73 |
74 | const esResult = response.eSearchResult;
75 | return {
76 | count: parseInt(esResult.Count, 10) || 0,
77 | retmax: parseInt(esResult.RetMax, 10) || 0,
78 | retstart: parseInt(esResult.RetStart, 10) || 0,
79 | queryKey: esResult.QueryKey,
80 | webEnv: esResult.WebEnv,
81 | idList: esResult.IdList?.Id || [],
82 | queryTranslation: esResult.QueryTranslation,
83 | errorList: esResult.ErrorList,
84 | warningList: esResult.WarningList,
85 | };
86 | }
87 |
88 | public async eSummary(
89 | params: NcbiRequestParams,
90 | context: RequestContext,
91 | ): Promise<unknown> {
92 | // Determine retmode based on params, default to xml
93 | const retmode =
94 | params.version === "2.0" && params.retmode === "json" ? "json" : "xml";
95 | return this.performNcbiRequest("esummary", params, context, { retmode });
96 | }
97 |
98 | public async eFetch(
99 | params: NcbiRequestParams,
100 | context: RequestContext,
101 | options: NcbiRequestOptions = { retmode: "xml" }, // Default retmode for eFetch
102 | ): Promise<EFetchArticleSet> {
103 | // Determine if POST should be used based on number of IDs
104 | const usePost =
105 | typeof params.id === "string" && params.id.split(",").length > 200;
106 | const fetchOptions = { ...options, usePost };
107 |
108 | return this.performNcbiRequest<EFetchArticleSet>(
109 | "efetch",
110 | params,
111 | context,
112 | fetchOptions,
113 | );
114 | }
115 |
116 | public async eLink(
117 | params: NcbiRequestParams,
118 | context: RequestContext,
119 | ): Promise<unknown> {
120 | return this.performNcbiRequest("elink", params, context, {
121 | retmode: "xml",
122 | });
123 | }
124 |
125 | public async eInfo(
126 | params: NcbiRequestParams,
127 | context: RequestContext,
128 | ): Promise<unknown> {
129 | return this.performNcbiRequest("einfo", params, context, {
130 | retmode: "xml",
131 | });
132 | }
133 | }
134 |
135 | let ncbiServiceInstance: NcbiService;
136 |
137 | export function getNcbiService(): NcbiService {
138 | if (!ncbiServiceInstance) {
139 | ncbiServiceInstance = new NcbiService();
140 | logger.debug(
141 | "NcbiService lazily initialized.",
142 | requestContextService.createRequestContext({
143 | service: "NcbiService",
144 | operation: "getNcbiServiceInstance",
145 | }),
146 | );
147 | }
148 | return ncbiServiceInstance;
149 | }
150 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@cyanheads/pubmed-mcp-server",
3 | "version": "1.4.4",
4 | "description": "Production-ready PubMed Model Context Protocol (MCP) server that empowers AI agents and research tools with comprehensive access to PubMed's article database. Enables advanced, automated LLM workflows for searching, retrieving, analyzing, and visualizing biomedical and scientific literature via NCBI E-utilities.",
5 | "mcpName": "io.github.cyanheads/pubmed-mcp-server",
6 | "main": "dist/index.js",
7 | "files": [
8 | "dist"
9 | ],
10 | "bin": {
11 | "pubmed-mcp-server": "dist/index.js"
12 | },
13 | "exports": "./dist/index.js",
14 | "type": "module",
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/cyanheads/pubmed-mcp-server.git"
18 | },
19 | "bugs": {
20 | "url": "https://github.com/cyanheads/pubmed-mcp-server/issues"
21 | },
22 | "homepage": "https://github.com/cyanheads/pubmed-mcp-server#readme",
23 | "scripts": {
24 | "build": "tsc && node --loader ts-node/esm scripts/make-executable.ts dist/index.js",
25 | "start": "node dist/index.js",
26 | "start:stdio": "MCP_LOG_LEVEL=debug MCP_TRANSPORT_TYPE=stdio node dist/index.js",
27 | "start:http": "MCP_LOG_LEVEL=debug MCP_TRANSPORT_TYPE=http node dist/index.js",
28 | "rebuild": "ts-node --esm scripts/clean.ts && npm run build",
29 | "docs:generate": "typedoc --tsconfig ./tsconfig.typedoc.json",
30 | "lint": "eslint .",
31 | "lint:fix": "eslint . --fix",
32 | "tree": "ts-node --esm scripts/tree.ts",
33 | "fetch-spec": "ts-node --esm scripts/fetch-openapi-spec.ts",
34 | "format": "prettier --write \"**/*.{ts,js,json,md,html,css}\"",
35 | "inspector": "mcp-inspector --config mcp.json --server pubmed-mcp-server",
36 | "test": "vitest run",
37 | "test:watch": "vitest",
38 | "test:coverage": "vitest run --coverage"
39 | },
40 | "dependencies": {
41 | "@hono/node-server": "^1.17.1",
42 | "@modelcontextprotocol/sdk": "^1.18.0",
43 | "@opentelemetry/api": "^1.9.0",
44 | "@opentelemetry/auto-instrumentations-node": "^0.64.1",
45 | "@opentelemetry/exporter-metrics-otlp-http": "^0.205.0",
46 | "@opentelemetry/exporter-trace-otlp-http": "^0.205.0",
47 | "@opentelemetry/instrumentation-winston": "^0.50.0",
48 | "@opentelemetry/resources": "^2.1.0",
49 | "@opentelemetry/sdk-metrics": "^2.1.0",
50 | "@opentelemetry/sdk-node": "^0.205.0",
51 | "@opentelemetry/sdk-trace-node": "^2.1.0",
52 | "@opentelemetry/semantic-conventions": "^1.37.0",
53 | "@types/node": "^24.4.0",
54 | "@types/sanitize-html": "^2.16.0",
55 | "@types/validator": "13.15.3",
56 | "axios": "^1.12.2",
57 | "chart.js": "^4.5.0",
58 | "chartjs-node-canvas": "^5.0.0",
59 | "chrono-node": "^2.8.3",
60 | "citation-js": "^0.7.20",
61 | "dotenv": "^16.6.1",
62 | "fast-xml-parser": "^5.2.5",
63 | "hono": "^4.8.4",
64 | "jose": "^6.1.0",
65 | "js-yaml": "^4.1.0",
66 | "node-cron": "^4.2.1",
67 | "openai": "^5.20.2",
68 | "partial-json": "^0.1.7",
69 | "patch-package": "^8.0.0",
70 | "sanitize-html": "^2.17.0",
71 | "tiktoken": "^1.0.22",
72 | "ts-node": "^10.9.2",
73 | "typescript": "^5.9.2",
74 | "typescript-eslint": "^8.43.0",
75 | "validator": "13.15.15",
76 | "winston": "^3.17.0",
77 | "winston-transport": "^4.9.0",
78 | "zod": "^3.25.74"
79 | },
80 | "keywords": [
81 | "mcp",
82 | "model-context-protocol",
83 | "ai-agent",
84 | "llm",
85 | "llm-integration",
86 | "pubmed",
87 | "pubmed-api",
88 | "ncbi",
89 | "e-utilities",
90 | "biomedical-research",
91 | "scientific-literature",
92 | "computational-biology",
93 | "typescript",
94 | "ai-tools",
95 | "bioinformatics",
96 | "health-tech",
97 | "research-automation",
98 | "literature-search",
99 | "citation-analysis",
100 | "data-visualization",
101 | "api-server",
102 | "typescript",
103 | "nodejs",
104 | "ai-tools",
105 | "research-agent",
106 | "generative-ai"
107 | ],
108 | "author": "Casey Hand @cyanheads",
109 | "email": "[email protected]",
110 | "license": "Apache-2.0",
111 | "funding": [
112 | {
113 | "type": "github",
114 | "url": "https://github.com/sponsors/cyanheads"
115 | },
116 | {
117 | "type": "buy_me_a_coffee",
118 | "url": "https://www.buymeacoffee.com/cyanheads"
119 | }
120 | ],
121 | "engines": {
122 | "node": ">=20.0.0"
123 | },
124 | "devDependencies": {
125 | "@types/js-yaml": "^4.0.9",
126 | "@types/node-cron": "^3.0.11",
127 | "ajv": "^8.17.1",
128 | "ajv-formats": "^3.0.1",
129 | "eslint": "^9.35.0",
130 | "prettier": "^3.6.2",
131 | "typedoc": "^0.28.13",
132 | "vite-tsconfig-paths": "^5.1.4",
133 | "vitest": "^3.2.4"
134 | },
135 | "overrides": {
136 | "patch-package": {
137 | "tmp": "0.2.5"
138 | }
139 | },
140 | "publishConfig": {
141 | "access": "public"
142 | }
143 | }
144 |
```
--------------------------------------------------------------------------------
/scripts/make-executable.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * @fileoverview Utility script to make files executable (chmod +x) on Unix-like systems.
5 | * @module scripts/make-executable
6 | * On Windows, this script does nothing but exits successfully.
7 | * Useful for CLI applications where built output needs executable permissions.
8 | * Default target (if no args): dist/index.js.
9 | * Ensures output paths are within the project directory for security.
10 | *
11 | * @example
12 | * // Add to package.json build script:
13 | * // "build": "tsc && ts-node --esm scripts/make-executable.ts dist/index.js"
14 | *
15 | * @example
16 | * // Run directly with custom files:
17 | * // ts-node --esm scripts/make-executable.ts path/to/script1 path/to/script2
18 | */
19 |
20 | import fs from "fs/promises";
21 | import os from "os";
22 | import path from "path";
23 |
24 | const isUnix = os.platform() !== "win32";
25 | const projectRoot = process.cwd();
26 | const EXECUTABLE_MODE = 0o755; // rwxr-xr-x
27 |
28 | /**
29 | * Represents the result of an attempt to make a file executable.
30 | * @property file - The relative path of the file targeted.
31 | * @property status - The outcome of the operation ('success', 'error', or 'skipped').
32 | * @property reason - If status is 'error' or 'skipped', an explanation.
33 | */
34 | interface ExecutableResult {
35 | file: string;
36 | status: "success" | "error" | "skipped";
37 | reason?: string;
38 | }
39 |
40 | /**
41 | * Main function to make specified files executable.
42 | * Skips operation on Windows. Processes command-line arguments for target files
43 | * or defaults to 'dist/index.js'. Reports status for each file.
44 | */
45 | const makeExecutable = async (): Promise<void> => {
46 | try {
47 | const targetFiles: string[] =
48 | process.argv.slice(2).length > 0
49 | ? process.argv.slice(2)
50 | : ["dist/index.js"];
51 |
52 | if (!isUnix) {
53 | console.log(
54 | "Skipping chmod operation: Script is running on Windows (not applicable).",
55 | );
56 | return;
57 | }
58 |
59 | console.log(
60 | `Attempting to make files executable: ${targetFiles.join(", ")}`,
61 | );
62 |
63 | const results = await Promise.allSettled(
64 | targetFiles.map(async (targetFile): Promise<ExecutableResult> => {
65 | const normalizedPath = path.resolve(projectRoot, targetFile);
66 |
67 | if (
68 | !normalizedPath.startsWith(projectRoot + path.sep) &&
69 | normalizedPath !== projectRoot
70 | ) {
71 | return {
72 | file: targetFile,
73 | status: "error",
74 | reason: `Path resolves outside project boundary: ${normalizedPath}`,
75 | };
76 | }
77 |
78 | try {
79 | await fs.access(normalizedPath); // Check if file exists
80 | await fs.chmod(normalizedPath, EXECUTABLE_MODE);
81 | return { file: targetFile, status: "success" };
82 | } catch (error) {
83 | const err = error as NodeJS.ErrnoException;
84 | if (err.code === "ENOENT") {
85 | return {
86 | file: targetFile,
87 | status: "error",
88 | reason: "File not found",
89 | };
90 | }
91 | console.error(
92 | `Error setting executable permission for ${targetFile}: ${err.message}`,
93 | );
94 | return { file: targetFile, status: "error", reason: err.message };
95 | }
96 | }),
97 | );
98 |
99 | let hasErrors = false;
100 | results.forEach((result) => {
101 | if (result.status === "fulfilled") {
102 | const { file, status, reason } = result.value;
103 | if (status === "success") {
104 | console.log(`Successfully made executable: ${file}`);
105 | } else if (status === "error") {
106 | console.error(`Error for ${file}: ${reason}`);
107 | hasErrors = true;
108 | } else if (status === "skipped") {
109 | // This status is not currently generated by the mapAsync logic but kept for future flexibility
110 | console.warn(`Skipped ${file}: ${reason}`);
111 | }
112 | } else {
113 | console.error(
114 | `Unexpected failure for one of the files: ${result.reason}`,
115 | );
116 | hasErrors = true;
117 | }
118 | });
119 |
120 | if (hasErrors) {
121 | console.error(
122 | "One or more files could not be made executable. Please check the errors above.",
123 | );
124 | // process.exit(1); // Uncomment to exit with error if any file fails
125 | } else {
126 | console.log("All targeted files processed successfully.");
127 | }
128 | } catch (error) {
129 | console.error(
130 | "A fatal error occurred during the make-executable script:",
131 | error instanceof Error ? error.message : error,
132 | );
133 | process.exit(1);
134 | }
135 | };
136 |
137 | makeExecutable();
138 |
```
--------------------------------------------------------------------------------
/src/mcp-server/tools/pubmedArticleConnections/logic/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Main logic handler for the 'pubmed_article_connections' MCP tool.
3 | * Orchestrates calls to ELink or citation formatting handlers.
4 | * @module src/mcp-server/tools/pubmedArticleConnections/logic/index
5 | */
6 |
7 | import { z } from "zod";
8 | import { BaseErrorCode, McpError } from "../../../../types-global/errors.js";
9 | import {
10 | logger,
11 | RequestContext,
12 | requestContextService,
13 | sanitizeInputForLogging,
14 | } from "../../../../utils/index.js";
15 | import { handleCitationFormats } from "./citationFormatter.js";
16 | import { handleELinkRelationships } from "./elinkHandler.js";
17 | import type { ToolOutputData } from "./types.js";
18 |
19 | /**
20 | * Zod schema for the input parameters of the 'pubmed_article_connections' tool.
21 | */
22 | export const PubMedArticleConnectionsInputSchema = z.object({
23 | sourcePmid: z
24 | .string()
25 | .regex(/^\d+$/)
26 | .describe(
27 | "The PubMed Unique Identifier (PMID) of the source article for which to find connections or format citations. This PMID must be a valid number string.",
28 | ),
29 | relationshipType: z
30 | .enum([
31 | "pubmed_similar_articles",
32 | "pubmed_citedin",
33 | "pubmed_references",
34 | "citation_formats",
35 | ])
36 | .default("pubmed_similar_articles")
37 | .describe(
38 | "Specifies the type of connection or action: 'pubmed_similar_articles' (finds similar articles), 'pubmed_citedin' (finds citing articles), 'pubmed_references' (finds referenced articles), or 'citation_formats' (retrieves formatted citations).",
39 | ),
40 | maxRelatedResults: z
41 | .number()
42 | .int()
43 | .positive()
44 | .max(50, "Maximum 50 related results can be requested.")
45 | .optional()
46 | .default(5)
47 | .describe(
48 | "Maximum number of related articles to retrieve for relationship-based searches. Default is 5, max is 50.",
49 | ),
50 | citationStyles: z
51 | .array(z.enum(["ris", "bibtex", "apa_string", "mla_string"]))
52 | .optional()
53 | .default(["ris"])
54 | .describe(
55 | "An array of citation styles to format the source article into when 'relationshipType' is 'citation_formats'. Supported styles: 'ris', 'bibtex', 'apa_string', 'mla_string'. Default is ['ris'].",
56 | ),
57 | });
58 |
59 | /**
60 | * Type alias for the validated input of the 'pubmed_article_connections' tool.
61 | */
62 | export type PubMedArticleConnectionsInput = z.infer<
63 | typeof PubMedArticleConnectionsInputSchema
64 | >;
65 |
66 | /**
67 | * Main handler for the 'pubmed_article_connections' tool.
68 | * @param {PubMedArticleConnectionsInput} input - Validated input parameters.
69 | * @param {RequestContext} context - The request context for this tool invocation.
70 | * @returns {Promise<ToolOutputData>} The result of the tool call.
71 | */
72 | export async function handlePubMedArticleConnections(
73 | input: PubMedArticleConnectionsInput,
74 | context: RequestContext,
75 | ): Promise<ToolOutputData> {
76 | const toolLogicContext = requestContextService.createRequestContext({
77 | parentRequestId: context.requestId,
78 | operation: "handlePubMedArticleConnections",
79 | toolName: "pubmed_article_connections",
80 | input: sanitizeInputForLogging(input),
81 | });
82 |
83 | logger.info("Executing pubmed_article_connections tool", toolLogicContext);
84 |
85 | const outputData: ToolOutputData = {
86 | sourcePmid: input.sourcePmid,
87 | relationshipType: input.relationshipType,
88 | relatedArticles: [],
89 | citations: {},
90 | retrievedCount: 0,
91 | eUtilityUrl: undefined,
92 | message: undefined,
93 | };
94 |
95 | switch (input.relationshipType) {
96 | case "pubmed_similar_articles":
97 | case "pubmed_citedin":
98 | case "pubmed_references":
99 | await handleELinkRelationships(input, outputData, toolLogicContext);
100 | break;
101 | case "citation_formats":
102 | await handleCitationFormats(input, outputData, toolLogicContext);
103 | break;
104 | default:
105 | throw new McpError(
106 | BaseErrorCode.VALIDATION_ERROR,
107 | `Unsupported relationshipType: ${input.relationshipType}`,
108 | { ...toolLogicContext, receivedType: input.relationshipType },
109 | );
110 | }
111 |
112 | if (
113 | outputData.retrievedCount === 0 &&
114 | !outputData.message &&
115 | (input.relationshipType !== "citation_formats" ||
116 | Object.keys(outputData.citations).length === 0)
117 | ) {
118 | outputData.message = "No results found for the given parameters.";
119 | }
120 |
121 | logger.notice("Successfully executed pubmed_article_connections tool.", {
122 | ...toolLogicContext,
123 | relationshipType: input.relationshipType,
124 | retrievedCount: outputData.retrievedCount,
125 | citationsGenerated: Object.keys(outputData.citations).length,
126 | });
127 |
128 | return outputData;
129 | }
130 |
```
--------------------------------------------------------------------------------
/src/utils/internal/requestContext.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Utilities for creating and managing request contexts.
3 | * A request context is an object carrying a unique ID, timestamp, and other
4 | * relevant data for logging, tracing, and processing. It also defines
5 | * configuration and operational context structures.
6 | * @module src/utils/internal/requestContext
7 | */
8 |
9 | import { trace } from "@opentelemetry/api";
10 | import { generateUUID } from "../index.js";
11 | import { logger } from "./logger.js";
12 |
13 | /**
14 | * Defines the core structure for context information associated with a request or operation.
15 | * This is fundamental for logging, tracing, and passing operational data.
16 | */
17 | export interface RequestContext {
18 | /**
19 | * Unique ID for the context instance.
20 | * Used for log correlation and request tracing.
21 | */
22 | requestId: string;
23 |
24 | /**
25 | * ISO 8601 timestamp indicating when the context was created.
26 | */
27 | timestamp: string;
28 |
29 | /**
30 | * Allows arbitrary key-value pairs for specific context needs.
31 | * Using `unknown` promotes type-safe access.
32 | * Consumers must type-check/assert when accessing extended properties.
33 | */
34 | [key: string]: unknown;
35 | }
36 |
37 | /**
38 | * Configuration for the {@link requestContextService}.
39 | * Allows for future extensibility of service-wide settings.
40 | */
41 | export interface ContextConfig {
42 | /** Custom configuration properties. Allows for arbitrary key-value pairs. */
43 | [key: string]: unknown;
44 | }
45 |
46 | /**
47 | * Represents a broader context for a specific operation or task.
48 | * It can optionally include a base {@link RequestContext} and other custom properties
49 | * relevant to the operation.
50 | */
51 | export interface OperationContext {
52 | /** Optional base request context data, adhering to the `RequestContext` structure. */
53 | requestContext?: RequestContext;
54 |
55 | /** Allows for additional, custom properties specific to the operation. */
56 | [key: string]: unknown;
57 | }
58 |
59 | /**
60 | * Singleton-like service object for managing request context operations.
61 | * @private
62 | */
63 | const requestContextServiceInstance = {
64 | /**
65 | * Internal configuration store for the service.
66 | */
67 | config: {} as ContextConfig,
68 |
69 | /**
70 | * Configures the request context service with new settings.
71 | * Merges the provided partial configuration with existing settings.
72 | *
73 | * @param config - A partial `ContextConfig` object containing settings to update or add.
74 | * @returns A shallow copy of the newly updated configuration.
75 | */
76 | configure(config: Partial<ContextConfig>): ContextConfig {
77 | this.config = {
78 | ...this.config,
79 | ...config,
80 | };
81 | const logContext = this.createRequestContext({
82 | operation: "RequestContextService.configure",
83 | newConfigState: { ...this.config },
84 | });
85 | logger.debug("RequestContextService configuration updated", logContext);
86 | return { ...this.config };
87 | },
88 |
89 | /**
90 | * Retrieves a shallow copy of the current service configuration.
91 | * This prevents direct mutation of the internal configuration state.
92 | *
93 | * @returns A shallow copy of the current `ContextConfig`.
94 | */
95 | getConfig(): ContextConfig {
96 | return { ...this.config };
97 | },
98 |
99 | /**
100 | * Creates a new {@link RequestContext} instance.
101 | * Each context is assigned a unique `requestId` (UUID) and a current `timestamp` (ISO 8601).
102 | * Additional custom properties can be merged into the context.
103 | *
104 | * @param additionalContext - An optional record of key-value pairs to be
105 | * included in the created request context.
106 | * @returns A new `RequestContext` object.
107 | */
108 | createRequestContext(
109 | additionalContext: Record<string, unknown> = {},
110 | ): RequestContext {
111 | const requestId = generateUUID();
112 | const timestamp = new Date().toISOString();
113 |
114 | const context: RequestContext = {
115 | requestId,
116 | timestamp,
117 | ...additionalContext,
118 | };
119 |
120 | // --- OpenTelemetry Integration ---
121 | // Automatically inject active trace and span IDs into the context for correlation.
122 | const activeSpan = trace.getActiveSpan();
123 | if (activeSpan) {
124 | const spanContext = activeSpan.spanContext();
125 | context.traceId = spanContext.traceId;
126 | context.spanId = spanContext.spanId;
127 | }
128 | // --- End OpenTelemetry Integration ---
129 |
130 | return context;
131 | },
132 | };
133 |
134 | /**
135 | * Primary export for request context functionalities.
136 | * This service provides methods to create and manage {@link RequestContext} instances,
137 | * which are essential for logging, tracing, and correlating operations.
138 | */
139 | export const requestContextService = requestContextServiceInstance;
140 |
```
--------------------------------------------------------------------------------
/src/mcp-server/transports/core/transportTypes.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Defines the core types and interfaces for the transport layer abstraction.
3 | * This module establishes the data contracts and abstract interfaces that decouple
4 | * the MCP server's core logic from specific transport implementations like HTTP or stdio.
5 | * @module src/mcp-server/transports/core/transportTypes
6 | */
7 |
8 | import type { IncomingHttpHeaders } from "http";
9 | import { RequestContext } from "../../../utils/index.js";
10 |
11 | /**
12 | * Defines the set of valid HTTP status codes that the transport layer can return.
13 | * This ensures type safety and consistency in response handling.
14 | */
15 | export type HttpStatusCode =
16 | | 200 // OK
17 | | 201 // Created
18 | | 400 // Bad Request
19 | | 401 // Unauthorized
20 | | 403 // Forbidden
21 | | 404 // Not Found
22 | | 409 // Conflict
23 | | 429 // Too Many Requests
24 | | 500 // Internal Server Error
25 | | 502 // Bad Gateway
26 | | 503; // Service Unavailable
27 |
28 | /**
29 | * A base interface for all transport responses, containing common properties.
30 | */
31 | interface BaseTransportResponse {
32 | sessionId?: string;
33 | headers: Headers;
34 | statusCode: HttpStatusCode;
35 | }
36 |
37 | /**
38 | * Represents a transport response where the entire body is buffered in memory.
39 | * Suitable for small, non-streamed responses.
40 | */
41 | export interface BufferedTransportResponse extends BaseTransportResponse {
42 | type: "buffered";
43 | body: unknown;
44 | }
45 |
46 | /**
47 | * Represents a transport response that streams its body.
48 | * Essential for handling large or chunked responses efficiently without high memory usage.
49 | */
50 | export interface StreamingTransportResponse extends BaseTransportResponse {
51 | type: "stream";
52 | stream: ReadableStream<Uint8Array>;
53 | }
54 |
55 | /**
56 | * A discriminated union representing the possible types of a transport response.
57 | * Using a discriminated union on the `type` property allows for type-safe handling
58 | * of different response formats (buffered vs. streamed).
59 | */
60 | export type TransportResponse =
61 | | BufferedTransportResponse
62 | | StreamingTransportResponse;
63 |
64 | /**
65 | * Represents the state of an active, persistent transport session.
66 | */
67 | export interface TransportSession {
68 | id: string;
69 | createdAt: Date;
70 | lastAccessedAt: Date;
71 | /**
72 | * A counter for requests currently being processed for this session.
73 | * This is a critical mechanism to prevent race conditions where a session
74 | * might be garbage-collected while a long-running request is still in flight.
75 | * It is incremented when a request begins and decremented when it finishes.
76 | */
77 | activeRequests: number;
78 | }
79 |
80 | /**
81 | * Defines the abstract interface for a transport manager.
82 | * This contract ensures that any transport manager, regardless of its statefulness,
83 | * provides a consistent way to handle requests and manage its lifecycle.
84 | */
85 | export interface TransportManager {
86 | /**
87 | * Handles an incoming request.
88 | * @param headers The incoming request headers.
89 | * @param body The parsed body of the request.
90 | * @param context The request context for logging, tracing, and metadata.
91 | * @param sessionId An optional session identifier for stateful operations.
92 | * @returns A promise that resolves to a TransportResponse object.
93 | */
94 | handleRequest(
95 | headers: IncomingHttpHeaders,
96 | body: unknown,
97 | context: RequestContext,
98 | sessionId?: string,
99 | ): Promise<TransportResponse>;
100 |
101 | /**
102 | * Gracefully shuts down the transport manager, cleaning up any resources.
103 | */
104 | shutdown(): Promise<void>;
105 | }
106 |
107 | /**
108 | * Extends the base TransportManager with operations specific to stateful sessions.
109 | */
110 | export interface StatefulTransportManager extends TransportManager {
111 | /**
112 | * Initializes a new stateful session and handles the first request.
113 | * @param headers The incoming request headers.
114 | * @param body The parsed body of the request.
115 | * @param context The request context.
116 | * @returns A promise resolving to a TransportResponse, which will include a session ID.
117 | */
118 | initializeAndHandle(
119 | headers: IncomingHttpHeaders,
120 | body: unknown,
121 | context: RequestContext,
122 | ): Promise<TransportResponse>;
123 |
124 | /**
125 | * Handles a request to explicitly delete a session.
126 | * @param sessionId The ID of the session to delete.
127 | * @param context The request context.
128 | * @returns A promise resolving to a TransportResponse confirming closure.
129 | */
130 | handleDeleteRequest(
131 | sessionId: string,
132 | context: RequestContext,
133 | ): Promise<TransportResponse>;
134 |
135 | /**
136 | * Retrieves information about a specific session.
137 | * @param sessionId The ID of the session to retrieve.
138 | * @returns A TransportSession object if the session exists, otherwise undefined.
139 | */
140 | getSession(sessionId: string): TransportSession | undefined;
141 | }
142 |
```
--------------------------------------------------------------------------------
/src/utils/parsing/dateParser.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Provides utility functions for parsing natural language date strings
3 | * into Date objects or detailed parsing results using the `chrono-node` library.
4 | * @module src/utils/parsing/dateParser
5 | */
6 | import * as chrono from "chrono-node";
7 | import { BaseErrorCode } from "../../types-global/errors.js";
8 | import { ErrorHandler, logger, RequestContext } from "../index.js";
9 |
10 | /**
11 | * Parses a natural language date string into a JavaScript Date object.
12 | * Uses `chrono.parseDate` for lenient parsing of various date formats.
13 | *
14 | * @param text - The natural language date string to parse.
15 | * @param context - The request context for logging and error tracking.
16 | * @param refDate - Optional reference date for parsing relative dates. Defaults to current date/time.
17 | * @returns A promise resolving with a Date object or `null` if parsing fails.
18 | * @throws {McpError} If an unexpected error occurs during parsing.
19 | * @private
20 | */
21 | export async function parseDateString(
22 | text: string,
23 | context: RequestContext,
24 | refDate?: Date,
25 | ): Promise<Date | null> {
26 | const operation = "parseDateString";
27 | const logContext = { ...context, operation, inputText: text, refDate };
28 | logger.debug(`Attempting to parse date string: "${text}"`, logContext);
29 |
30 | return await ErrorHandler.tryCatch(
31 | async () => {
32 | const parsedDate = chrono.parseDate(text, refDate, { forwardDate: true });
33 | if (parsedDate) {
34 | logger.debug(
35 | `Successfully parsed "${text}" to ${parsedDate.toISOString()}`,
36 | logContext,
37 | );
38 | return parsedDate;
39 | } else {
40 | logger.warning(`Failed to parse date string: "${text}"`, logContext);
41 | return null;
42 | }
43 | },
44 | {
45 | operation,
46 | context: logContext,
47 | input: { text, refDate },
48 | errorCode: BaseErrorCode.PARSING_ERROR,
49 | },
50 | );
51 | }
52 |
53 | /**
54 | * Parses a natural language date string and returns detailed parsing results.
55 | * Provides more information than just the Date object, including matched text and components.
56 | *
57 | * @param text - The natural language date string to parse.
58 | * @param context - The request context for logging and error tracking.
59 | * @param refDate - Optional reference date for parsing relative dates. Defaults to current date/time.
60 | * @returns A promise resolving with an array of `chrono.ParsedResult` objects. Empty if no dates found.
61 | * @throws {McpError} If an unexpected error occurs during parsing.
62 | * @private
63 | */
64 | export async function parseDateStringDetailed(
65 | text: string,
66 | context: RequestContext,
67 | refDate?: Date,
68 | ): Promise<chrono.ParsedResult[]> {
69 | const operation = "parseDateStringDetailed";
70 | const logContext = { ...context, operation, inputText: text, refDate };
71 | logger.debug(
72 | `Attempting detailed parse of date string: "${text}"`,
73 | logContext,
74 | );
75 |
76 | return await ErrorHandler.tryCatch(
77 | async () => {
78 | const results = chrono.parse(text, refDate, { forwardDate: true });
79 | logger.debug(
80 | `Detailed parse of "${text}" resulted in ${results.length} result(s)`,
81 | logContext,
82 | );
83 | return results;
84 | },
85 | {
86 | operation,
87 | context: logContext,
88 | input: { text, refDate },
89 | errorCode: BaseErrorCode.PARSING_ERROR,
90 | },
91 | );
92 | }
93 |
94 | /**
95 | * An object providing date parsing functionalities.
96 | *
97 | * @example
98 | * ```typescript
99 | * import { dateParser, requestContextService } from './utils'; // Assuming utils/index.js exports these
100 | * const context = requestContextService.createRequestContext({ operation: 'TestDateParsing' });
101 | *
102 | * async function testParsing() {
103 | * const dateObj = await dateParser.parseDate("next Friday at 3pm", context);
104 | * if (dateObj) {
105 | * console.log("Parsed Date:", dateObj.toISOString());
106 | * }
107 | *
108 | * const detailedResults = await dateParser.parse("Meeting on 2024-12-25 and another one tomorrow", context);
109 | * detailedResults.forEach(result => {
110 | * console.log("Detailed Result:", result.text, result.start.date());
111 | * });
112 | * }
113 | * testParsing();
114 | * ```
115 | */
116 | export const dateParser = {
117 | /**
118 | * Parses a natural language date string and returns detailed parsing results
119 | * from `chrono-node`.
120 | * @param text - The natural language date string to parse.
121 | * @param context - The request context for logging and error tracking.
122 | * @param refDate - Optional reference date for parsing relative dates.
123 | * @returns A promise resolving with an array of `chrono.ParsedResult` objects.
124 | */
125 | parse: parseDateStringDetailed,
126 | /**
127 | * Parses a natural language date string into a single JavaScript Date object.
128 | * @param text - The natural language date string to parse.
129 | * @param context - The request context for logging and error tracking.
130 | * @param refDate - Optional reference date for parsing relative dates.
131 | * @returns A promise resolving with a Date object or `null`.
132 | */
133 | parseDate: parseDateString,
134 | };
135 |
```