@cyanheads/pubmed-mcp-server 1.3.2 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -5
- package/dist/config/index.js +47 -14
- package/dist/mcp-server/server.d.ts +2 -2
- package/dist/mcp-server/server.js +21 -15
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
[](https://modelcontextprotocol.io/)
|
|
9
|
-
[](./CHANGELOG.md)
|
|
10
10
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
11
11
|
[](https://github.com/cyanheads/pubmed-mcp-server/issues)
|
|
12
12
|
[](https://github.com/cyanheads/pubmed-mcp-server)
|
|
@@ -84,13 +84,15 @@ Leverages the robust utilities provided by the `mcp-ts-template`:
|
|
|
84
84
|
|
|
85
85
|
### Prerequisites
|
|
86
86
|
|
|
87
|
-
- [Node.js (>=
|
|
87
|
+
- [Node.js (>=20.0.0)](https://nodejs.org/)
|
|
88
88
|
- [npm](https://www.npmjs.com/) (comes with Node.js)
|
|
89
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
90
|
|
|
91
91
|
### MCP Client Settings
|
|
92
92
|
|
|
93
|
-
Add the following to your MCP client's configuration file (e.g., `cline_mcp_settings.json`).
|
|
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.
|
|
94
96
|
|
|
95
97
|
```json
|
|
96
98
|
{
|
|
@@ -99,7 +101,10 @@ Add the following to your MCP client's configuration file (e.g., `cline_mcp_sett
|
|
|
99
101
|
"command": "npx",
|
|
100
102
|
"args": ["@cyanheads/pubmed-mcp-server"],
|
|
101
103
|
"env": {
|
|
102
|
-
"
|
|
104
|
+
"MCP_LOG_LEVEL": "debug",
|
|
105
|
+
"MCP_TRANSPORT_TYPE": "http",
|
|
106
|
+
"MCP_HTTP_PORT": "3017",
|
|
107
|
+
"NCBI_API_KEY": "YOUR_NCBI_API_KEY_HERE"
|
|
103
108
|
}
|
|
104
109
|
}
|
|
105
110
|
}
|
|
@@ -139,7 +144,7 @@ Configure the server using environment variables. For local development, these c
|
|
|
139
144
|
| Variable | Description | Default |
|
|
140
145
|
| :-------------------- | :--------------------------------------------------------------------------------------- | :------------ |
|
|
141
146
|
| `MCP_TRANSPORT_TYPE` | Transport mechanism: `stdio` or `http`. | `stdio` |
|
|
142
|
-
| `MCP_HTTP_PORT` | Port for the HTTP server (if `MCP_TRANSPORT_TYPE=http`). | `
|
|
147
|
+
| `MCP_HTTP_PORT` | Port for the HTTP server (if `MCP_TRANSPORT_TYPE=http`). | `3017` |
|
|
143
148
|
| `MCP_HTTP_HOST` | Host address for the HTTP server (if `MCP_TRANSPORT_TYPE=http`). | `127.0.0.1` |
|
|
144
149
|
| `MCP_ALLOWED_ORIGINS` | Comma-separated list of allowed origins for CORS (if `MCP_TRANSPORT_TYPE=http`). | (none) |
|
|
145
150
|
| `MCP_LOG_LEVEL` | Logging level (`debug`, `info`, `notice`, `warning`, `error`, `crit`, `alert`, `emerg`). | `debug` |
|
package/dist/config/index.js
CHANGED
|
@@ -60,7 +60,7 @@ const EnvSchema = z
|
|
|
60
60
|
// Transport
|
|
61
61
|
MCP_TRANSPORT_TYPE: z.enum(["stdio", "http"]).default("stdio"),
|
|
62
62
|
MCP_SESSION_MODE: z.enum(["stateless", "stateful", "auto"]).default("auto"),
|
|
63
|
-
MCP_HTTP_PORT: z.coerce.number().int().positive().default(
|
|
63
|
+
MCP_HTTP_PORT: z.coerce.number().int().positive().default(3017),
|
|
64
64
|
MCP_HTTP_HOST: z.string().default("127.0.0.1"),
|
|
65
65
|
MCP_HTTP_ENDPOINT_PATH: z.string().default("/mcp"),
|
|
66
66
|
MCP_HTTP_MAX_PORT_RETRIES: z.coerce
|
|
@@ -137,30 +137,63 @@ const ensureDirectory = (dirPath, rootDir, dirName) => {
|
|
|
137
137
|
const resolvedDirPath = path.isAbsolute(dirPath)
|
|
138
138
|
? dirPath
|
|
139
139
|
: path.resolve(rootDir, dirPath);
|
|
140
|
-
if (!resolvedDirPath.startsWith(rootDir)
|
|
140
|
+
if (!resolvedDirPath.startsWith(rootDir + path.sep) &&
|
|
141
|
+
resolvedDirPath !== rootDir) {
|
|
141
142
|
if (process.stdout.isTTY) {
|
|
142
|
-
console.error(`Error: ${dirName} path "${dirPath}" is outside the project boundary "${rootDir}".`);
|
|
143
|
+
console.error(`Error: ${dirName} path "${dirPath}" resolves to "${resolvedDirPath}", which is outside the project boundary "${rootDir}".`);
|
|
143
144
|
}
|
|
144
145
|
return null;
|
|
145
146
|
}
|
|
146
|
-
|
|
147
|
-
|
|
147
|
+
if (!existsSync(resolvedDirPath)) {
|
|
148
|
+
try {
|
|
148
149
|
mkdirSync(resolvedDirPath, { recursive: true });
|
|
150
|
+
if (process.stdout.isTTY) {
|
|
151
|
+
console.log(`Created ${dirName} directory: ${resolvedDirPath}`);
|
|
152
|
+
}
|
|
149
153
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
catch (err) {
|
|
155
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
156
|
+
if (process.stdout.isTTY) {
|
|
157
|
+
console.error(`Error creating ${dirName} directory at ${resolvedDirPath}: ${errorMessage}`);
|
|
154
158
|
}
|
|
159
|
+
return null;
|
|
155
160
|
}
|
|
156
|
-
return resolvedDirPath;
|
|
157
161
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
else {
|
|
163
|
+
try {
|
|
164
|
+
const stats = statSync(resolvedDirPath);
|
|
165
|
+
if (!stats.isDirectory()) {
|
|
166
|
+
if (process.stdout.isTTY) {
|
|
167
|
+
console.error(`Error: ${dirName} path ${resolvedDirPath} exists but is not a directory.`);
|
|
168
|
+
}
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (statError) {
|
|
173
|
+
const errorMessage = statError instanceof Error
|
|
174
|
+
? statError.message
|
|
175
|
+
: "An unknown error occurred";
|
|
176
|
+
if (process.stdout.isTTY) {
|
|
177
|
+
console.error(`Error accessing ${dirName} path ${resolvedDirPath}: ${errorMessage}`);
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
161
181
|
}
|
|
182
|
+
return resolvedDirPath;
|
|
162
183
|
};
|
|
163
|
-
|
|
184
|
+
let validatedLogsPath = ensureDirectory(env.LOGS_DIR, projectRoot, "logs");
|
|
185
|
+
if (!validatedLogsPath) {
|
|
186
|
+
if (process.stdout.isTTY) {
|
|
187
|
+
console.warn(`Warning: Custom logs directory ('${env.LOGS_DIR}') is invalid or outside the project boundary. Falling back to default.`);
|
|
188
|
+
}
|
|
189
|
+
const defaultLogsDir = path.join(projectRoot, "logs");
|
|
190
|
+
validatedLogsPath = ensureDirectory(defaultLogsDir, projectRoot, "logs");
|
|
191
|
+
if (!validatedLogsPath) {
|
|
192
|
+
if (process.stdout.isTTY) {
|
|
193
|
+
console.warn("Warning: Default logs directory could not be created. File logging will be disabled.");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
164
197
|
export const config = {
|
|
165
198
|
pkg,
|
|
166
199
|
mcpServerName: env.MCP_SERVER_NAME || pkg.name,
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module src/mcp-server/server
|
|
11
11
|
*/
|
|
12
|
-
import { ServerType } from "@hono/node-server";
|
|
13
12
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
|
+
import http from "http";
|
|
14
14
|
/**
|
|
15
15
|
* Main application entry point. Initializes and starts the MCP server.
|
|
16
16
|
*/
|
|
17
|
-
export declare function initializeAndStartServer(): Promise<
|
|
17
|
+
export declare function initializeAndStartServer(): Promise<McpServer | http.Server>;
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
13
|
import { config, environment } from "../config/index.js";
|
|
14
14
|
import { ErrorHandler, logger, requestContextService } from "../utils/index.js";
|
|
15
|
-
import { BaseErrorCode } from "../types-global/errors.js";
|
|
16
15
|
import { registerFetchPubMedContentTool } from "./tools/fetchPubMedContent/index.js";
|
|
17
16
|
import { registerGeneratePubMedChartTool } from "./tools/generatePubMedChart/index.js";
|
|
18
17
|
import { registerGetPubMedArticleConnectionsTool } from "./tools/getPubMedArticleConnections/index.js";
|
|
@@ -44,7 +43,7 @@ async function createMcpServerInstance() {
|
|
|
44
43
|
tools: { listChanged: true },
|
|
45
44
|
},
|
|
46
45
|
});
|
|
47
|
-
|
|
46
|
+
try {
|
|
48
47
|
logger.debug("Registering resources and tools...", context);
|
|
49
48
|
// IMPORTANT: Keep tool registrations in alphabetical order.
|
|
50
49
|
await registerFetchPubMedContentTool(server);
|
|
@@ -53,18 +52,21 @@ async function createMcpServerInstance() {
|
|
|
53
52
|
await registerPubMedResearchAgentTool(server);
|
|
54
53
|
await registerSearchPubMedArticlesTool(server);
|
|
55
54
|
logger.info("Resources and tools registered successfully", context);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
logger.error("Failed to register resources/tools", {
|
|
58
|
+
...context,
|
|
59
|
+
error: err instanceof Error ? err.message : String(err),
|
|
60
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
61
|
+
});
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
62
64
|
return server;
|
|
63
65
|
}
|
|
64
66
|
/**
|
|
65
67
|
* Selects, sets up, and starts the appropriate MCP transport layer based on configuration.
|
|
66
68
|
*
|
|
67
|
-
* @returns Resolves with `McpServer` for 'stdio'
|
|
69
|
+
* @returns Resolves with `McpServer` for 'stdio' or `http.Server` for 'http'.
|
|
68
70
|
* @throws {Error} If transport type is unsupported or setup fails.
|
|
69
71
|
* @private
|
|
70
72
|
*/
|
|
@@ -75,16 +77,16 @@ async function startTransport() {
|
|
|
75
77
|
transport: transportType,
|
|
76
78
|
});
|
|
77
79
|
logger.info(`Starting transport: ${transportType}`, context);
|
|
78
|
-
const serverFactory = createMcpServerInstance;
|
|
79
80
|
if (transportType === "http") {
|
|
80
|
-
const { server } = await startHttpTransport(
|
|
81
|
+
const { server } = await startHttpTransport(createMcpServerInstance, context);
|
|
81
82
|
return server;
|
|
82
83
|
}
|
|
83
84
|
if (transportType === "stdio") {
|
|
84
|
-
const server = await
|
|
85
|
+
const server = await createMcpServerInstance();
|
|
85
86
|
await startStdioTransport(server, context);
|
|
86
87
|
return server;
|
|
87
88
|
}
|
|
89
|
+
logger.fatal(`Unsupported transport type configured: ${transportType}`, context);
|
|
88
90
|
throw new Error(`Unsupported transport type: ${transportType}. Must be 'stdio' or 'http'.`);
|
|
89
91
|
}
|
|
90
92
|
/**
|
|
@@ -101,11 +103,15 @@ export async function initializeAndStartServer() {
|
|
|
101
103
|
return result;
|
|
102
104
|
}
|
|
103
105
|
catch (err) {
|
|
106
|
+
logger.fatal("Critical error during MCP server initialization.", {
|
|
107
|
+
...context,
|
|
108
|
+
error: err instanceof Error ? err.message : String(err),
|
|
109
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
110
|
+
});
|
|
104
111
|
ErrorHandler.handleError(err, {
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
...context,
|
|
113
|
+
operation: "initializeAndStartServer_Catch",
|
|
107
114
|
critical: true,
|
|
108
|
-
rethrow: false,
|
|
109
115
|
});
|
|
110
116
|
logger.info("Exiting process due to critical initialization error.", context);
|
|
111
117
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/pubmed-mcp-server",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.4",
|
|
4
4
|
"description": "A Model Context Protocol (MCP) server enabling AI agents to intelligently search, retrieve, and analyze biomedical literature from PubMed via NCBI E-utilities. Built on the mcp-ts-template for robust, production-ready performance.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"chartjs-node-canvas": "^5.0.0",
|
|
44
44
|
"chrono-node": "^2.8.3",
|
|
45
45
|
"citation-js": "^0.7.20",
|
|
46
|
-
"dotenv": "^16.
|
|
46
|
+
"dotenv": "^16.6.1",
|
|
47
47
|
"fast-xml-parser": "^5.2.5",
|
|
48
48
|
"hono": "^4.8.4",
|
|
49
49
|
"jose": "^6.0.12",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"node-cron": "^4.2.1",
|
|
52
52
|
"openai": "^5.11.0",
|
|
53
53
|
"partial-json": "^0.1.7",
|
|
54
|
+
"js-yaml": "^4.1.0",
|
|
54
55
|
"sanitize-html": "^2.17.0",
|
|
55
56
|
"tiktoken": "^1.0.21",
|
|
56
57
|
"ts-node": "^10.9.2",
|
|
@@ -91,18 +92,17 @@
|
|
|
91
92
|
"url": "https://www.buymeacoffee.com/cyanheads"
|
|
92
93
|
}
|
|
93
94
|
],
|
|
94
|
-
"publishConfig": {
|
|
95
|
-
"access": "public"
|
|
96
|
-
},
|
|
97
95
|
"engines": {
|
|
98
|
-
"node": ">=
|
|
96
|
+
"node": ">=20.0.0"
|
|
99
97
|
},
|
|
100
98
|
"devDependencies": {
|
|
101
99
|
"@types/js-yaml": "^4.0.9",
|
|
102
100
|
"@types/node-cron": "^3.0.11",
|
|
103
|
-
"js-yaml": "^4.1.0",
|
|
104
101
|
"patch-package": "^8.0.0",
|
|
105
102
|
"prettier": "^3.6.2",
|
|
106
103
|
"typedoc": "^0.28.8"
|
|
104
|
+
},
|
|
105
|
+
"publishConfig": {
|
|
106
|
+
"access": "public"
|
|
107
107
|
}
|
|
108
108
|
}
|