@cyanheads/pubmed-mcp-server 1.0.12
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/LICENSE +201 -0
- package/README.md +174 -0
- package/dist/config/index.d.ts +91 -0
- package/dist/config/index.js +324 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +190 -0
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +79 -0
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +82 -0
- package/dist/mcp-server/resources/echoResource/index.d.ts +13 -0
- package/dist/mcp-server/resources/echoResource/index.js +13 -0
- package/dist/mcp-server/resources/echoResource/registration.d.ts +30 -0
- package/dist/mcp-server/resources/echoResource/registration.js +168 -0
- package/dist/mcp-server/server.d.ts +28 -0
- package/dist/mcp-server/server.js +159 -0
- package/dist/mcp-server/tools/fetchPubMedContent/index.d.ts +6 -0
- package/dist/mcp-server/tools/fetchPubMedContent/index.js +6 -0
- package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +63 -0
- package/dist/mcp-server/tools/fetchPubMedContent/logic.js +518 -0
- package/dist/mcp-server/tools/fetchPubMedContent/registration.d.ts +10 -0
- package/dist/mcp-server/tools/fetchPubMedContent/registration.js +37 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/index.d.ts +6 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/index.js +6 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.d.ts +9 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +288 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.d.ts +9 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +208 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.d.ts +15 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +82 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.d.ts +27 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.js +5 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +6 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic.js +6 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +36 -0
- package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +76 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/index.d.ts +6 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/index.js +6 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/index.d.ts +7 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/index.js +9 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.d.ts +111 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.js +149 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.d.ts +106 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.js +7 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.d.ts +10 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.js +251 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +12 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +55 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/registration.d.ts +10 -0
- package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +36 -0
- package/dist/mcp-server/tools/searchPubMedArticles/index.d.ts +6 -0
- package/dist/mcp-server/tools/searchPubMedArticles/index.js +6 -0
- package/dist/mcp-server/tools/searchPubMedArticles/logic.d.ts +61 -0
- package/dist/mcp-server/tools/searchPubMedArticles/logic.js +229 -0
- package/dist/mcp-server/tools/searchPubMedArticles/registration.d.ts +10 -0
- package/dist/mcp-server/tools/searchPubMedArticles/registration.js +38 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +29 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.js +174 -0
- package/dist/mcp-server/transports/httpTransport.d.ts +23 -0
- package/dist/mcp-server/transports/httpTransport.js +463 -0
- package/dist/mcp-server/transports/stdioTransport.d.ts +42 -0
- package/dist/mcp-server/transports/stdioTransport.js +63 -0
- package/dist/services/NCBI/ncbiConstants.d.ts +22 -0
- package/dist/services/NCBI/ncbiConstants.js +5 -0
- package/dist/services/NCBI/ncbiCoreApiClient.d.ts +24 -0
- package/dist/services/NCBI/ncbiCoreApiClient.js +118 -0
- package/dist/services/NCBI/ncbiRequestQueueManager.d.ts +36 -0
- package/dist/services/NCBI/ncbiRequestQueueManager.js +96 -0
- package/dist/services/NCBI/ncbiResponseHandler.d.ts +22 -0
- package/dist/services/NCBI/ncbiResponseHandler.js +192 -0
- package/dist/services/NCBI/ncbiService.d.ts +24 -0
- package/dist/services/NCBI/ncbiService.js +57 -0
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.js +7 -0
- package/dist/services/llm-providers/index.d.ts +7 -0
- package/dist/services/llm-providers/index.js +7 -0
- package/dist/services/llm-providers/llmFactory.d.ts +69 -0
- package/dist/services/llm-providers/llmFactory.js +132 -0
- package/dist/services/llm-providers/openRouter/index.d.ts +6 -0
- package/dist/services/llm-providers/openRouter/index.js +7 -0
- package/dist/services/llm-providers/openRouter/openRouterProvider.d.ts +99 -0
- package/dist/services/llm-providers/openRouter/openRouterProvider.js +329 -0
- package/dist/types-global/errors.d.ts +119 -0
- package/dist/types-global/errors.js +119 -0
- package/dist/types-global/pubmedXml.d.ts +350 -0
- package/dist/types-global/pubmedXml.js +7 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/index.js +18 -0
- package/dist/utils/internal/errorHandler.d.ts +176 -0
- package/dist/utils/internal/errorHandler.js +335 -0
- package/dist/utils/internal/index.d.ts +9 -0
- package/dist/utils/internal/index.js +9 -0
- package/dist/utils/internal/logger.d.ts +141 -0
- package/dist/utils/internal/logger.js +426 -0
- package/dist/utils/internal/requestContext.d.ts +83 -0
- package/dist/utils/internal/requestContext.js +72 -0
- package/dist/utils/metrics/index.d.ts +7 -0
- package/dist/utils/metrics/index.js +7 -0
- package/dist/utils/metrics/tokenCounter.d.ts +35 -0
- package/dist/utils/metrics/tokenCounter.js +110 -0
- package/dist/utils/parsing/dateParser.d.ts +73 -0
- package/dist/utils/parsing/dateParser.js +107 -0
- package/dist/utils/parsing/index.d.ts +8 -0
- package/dist/utils/parsing/index.js +8 -0
- package/dist/utils/parsing/jsonParser.d.ts +82 -0
- package/dist/utils/parsing/jsonParser.js +126 -0
- package/dist/utils/parsing/ncbi-parsing/eSummaryResultParser.d.ts +32 -0
- package/dist/utils/parsing/ncbi-parsing/eSummaryResultParser.js +295 -0
- package/dist/utils/parsing/ncbi-parsing/index.d.ts +8 -0
- package/dist/utils/parsing/ncbi-parsing/index.js +8 -0
- package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.d.ts +70 -0
- package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.js +229 -0
- package/dist/utils/parsing/ncbi-parsing/xmlGenericHelpers.d.ts +31 -0
- package/dist/utils/parsing/ncbi-parsing/xmlGenericHelpers.js +76 -0
- package/dist/utils/security/idGenerator.d.ts +134 -0
- package/dist/utils/security/idGenerator.js +198 -0
- package/dist/utils/security/index.d.ts +9 -0
- package/dist/utils/security/index.js +9 -0
- package/dist/utils/security/rateLimiter.d.ts +108 -0
- package/dist/utils/security/rateLimiter.js +179 -0
- package/dist/utils/security/sanitization.d.ts +173 -0
- package/dist/utils/security/sanitization.js +439 -0
- package/package.json +97 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Handles the setup and management of the Streamable HTTP MCP transport.
|
|
3
|
+
* Implements the MCP Specification 2025-03-26 for Streamable HTTP.
|
|
4
|
+
* This includes creating an Express server, configuring middleware (CORS, Authentication),
|
|
5
|
+
* defining request routing for the single MCP endpoint (POST/GET/DELETE),
|
|
6
|
+
* managing server-side sessions, handling Server-Sent Events (SSE) for streaming,
|
|
7
|
+
* and binding to a network port with retry logic for port conflicts.
|
|
8
|
+
*
|
|
9
|
+
* Specification Reference:
|
|
10
|
+
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
|
|
11
|
+
* @module src/mcp-server/transports/httpTransport
|
|
12
|
+
*/
|
|
13
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
14
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
+
import express from "express";
|
|
16
|
+
import http from "http";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
import { config } from "../../config/index.js";
|
|
19
|
+
import { logger, requestContextService, } from "../../utils/index.js";
|
|
20
|
+
import { mcpAuthMiddleware } from "./authentication/authMiddleware.js";
|
|
21
|
+
/**
|
|
22
|
+
* The port number for the HTTP transport, configured via `MCP_HTTP_PORT` environment variable.
|
|
23
|
+
* Defaults to 3010 if not specified (default is managed by the config module).
|
|
24
|
+
* @constant {number} HTTP_PORT
|
|
25
|
+
* @private
|
|
26
|
+
*/
|
|
27
|
+
const HTTP_PORT = config.mcpHttpPort;
|
|
28
|
+
/**
|
|
29
|
+
* The host address for the HTTP transport, configured via `MCP_HTTP_HOST` environment variable.
|
|
30
|
+
* Defaults to '127.0.0.1' if not specified (default is managed by the config module).
|
|
31
|
+
* MCP Spec Security Note: Recommends binding to localhost for local servers to minimize exposure.
|
|
32
|
+
* @private
|
|
33
|
+
*/
|
|
34
|
+
const HTTP_HOST = config.mcpHttpHost;
|
|
35
|
+
/**
|
|
36
|
+
* The single HTTP endpoint path for all MCP communication, as required by the MCP specification.
|
|
37
|
+
* This endpoint supports POST, GET, DELETE, and OPTIONS methods.
|
|
38
|
+
* @constant {string} MCP_ENDPOINT_PATH
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
41
|
+
const MCP_ENDPOINT_PATH = "/mcp";
|
|
42
|
+
/**
|
|
43
|
+
* Maximum number of attempts to find an available port if the initial `HTTP_PORT` is in use.
|
|
44
|
+
* The server will try ports sequentially: `HTTP_PORT`, `HTTP_PORT + 1`, ..., up to `MAX_PORT_RETRIES`.
|
|
45
|
+
* @constant {number} MAX_PORT_RETRIES
|
|
46
|
+
* @private
|
|
47
|
+
*/
|
|
48
|
+
const MAX_PORT_RETRIES = 15;
|
|
49
|
+
/**
|
|
50
|
+
* Stores active `StreamableHTTPServerTransport` instances from the SDK, keyed by their session ID.
|
|
51
|
+
* This is essential for routing subsequent HTTP requests (GET, DELETE, non-initialize POST)
|
|
52
|
+
* to the correct stateful session transport instance.
|
|
53
|
+
* @type {Record<string, StreamableHTTPServerTransport>}
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
const httpTransports = {};
|
|
57
|
+
/**
|
|
58
|
+
* Checks if an incoming HTTP request's `Origin` header is permissible based on configuration.
|
|
59
|
+
* MCP Spec Security: Servers MUST validate the `Origin` header for cross-origin requests.
|
|
60
|
+
* This function checks the request's origin against the `config.mcpAllowedOrigins` list.
|
|
61
|
+
* If the server is bound to localhost, requests from localhost or with no/null origin are also permitted.
|
|
62
|
+
* Sets appropriate CORS headers (`Access-Control-Allow-Origin`, etc.) if the origin is allowed.
|
|
63
|
+
*
|
|
64
|
+
* @param req - The Express request object.
|
|
65
|
+
* @param res - The Express response object.
|
|
66
|
+
* @returns True if the origin is allowed, false otherwise.
|
|
67
|
+
* @private
|
|
68
|
+
*/
|
|
69
|
+
function isOriginAllowed(req, res) {
|
|
70
|
+
const origin = req.headers.origin;
|
|
71
|
+
const host = req.hostname;
|
|
72
|
+
const isLocalhostBinding = ["127.0.0.1", "::1", "localhost"].includes(host);
|
|
73
|
+
const allowedOrigins = config.mcpAllowedOrigins || [];
|
|
74
|
+
const context = requestContextService.createRequestContext({
|
|
75
|
+
operation: "isOriginAllowed",
|
|
76
|
+
origin,
|
|
77
|
+
host,
|
|
78
|
+
isLocalhostBinding,
|
|
79
|
+
allowedOrigins,
|
|
80
|
+
});
|
|
81
|
+
logger.debug("Checking origin allowance", context);
|
|
82
|
+
const allowed = (origin && allowedOrigins.includes(origin)) ||
|
|
83
|
+
(isLocalhostBinding && (!origin || origin === "null"));
|
|
84
|
+
if (allowed && origin) {
|
|
85
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
86
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
87
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Last-Event-ID, Authorization");
|
|
88
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
89
|
+
}
|
|
90
|
+
else if (!allowed && origin) {
|
|
91
|
+
logger.warning(`Origin denied: ${origin}`, context);
|
|
92
|
+
}
|
|
93
|
+
logger.debug(`Origin check result: ${allowed}`, { ...context, allowed });
|
|
94
|
+
return allowed;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Proactively checks if a specific network port is already in use.
|
|
98
|
+
* @param port - The port number to check.
|
|
99
|
+
* @param host - The host address to check the port on.
|
|
100
|
+
* @param parentContext - Logging context from the caller.
|
|
101
|
+
* @returns A promise that resolves to `true` if the port is in use, or `false` otherwise.
|
|
102
|
+
* @private
|
|
103
|
+
*/
|
|
104
|
+
async function isPortInUse(port, host, parentContext) {
|
|
105
|
+
const checkContext = requestContextService.createRequestContext({
|
|
106
|
+
...parentContext,
|
|
107
|
+
operation: "isPortInUse",
|
|
108
|
+
port,
|
|
109
|
+
host,
|
|
110
|
+
});
|
|
111
|
+
logger.debug(`Proactively checking port usability...`, checkContext);
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
const tempServer = http.createServer();
|
|
114
|
+
tempServer
|
|
115
|
+
.once("error", (err) => {
|
|
116
|
+
if (err.code === "EADDRINUSE") {
|
|
117
|
+
logger.debug(`Proactive check: Port confirmed in use (EADDRINUSE).`, checkContext);
|
|
118
|
+
resolve(true);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
logger.debug(`Proactive check: Non-EADDRINUSE error encountered: ${err.message}`, { ...checkContext, errorCode: err.code });
|
|
122
|
+
resolve(false);
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
.once("listening", () => {
|
|
126
|
+
logger.debug(`Proactive check: Port is available.`, checkContext);
|
|
127
|
+
tempServer.close(() => resolve(false));
|
|
128
|
+
})
|
|
129
|
+
.listen(port, host);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Attempts to start the HTTP server, retrying on incrementing ports if `EADDRINUSE` occurs.
|
|
134
|
+
*
|
|
135
|
+
* @param serverInstance - The Node.js HTTP server instance.
|
|
136
|
+
* @param initialPort - The initial port number to try.
|
|
137
|
+
* @param host - The host address to bind to.
|
|
138
|
+
* @param maxRetries - Maximum number of additional ports to attempt.
|
|
139
|
+
* @param parentContext - Logging context from the caller.
|
|
140
|
+
* @returns A promise that resolves with the port number the server successfully bound to.
|
|
141
|
+
* @throws {Error} If binding fails after all retries or for a non-EADDRINUSE error.
|
|
142
|
+
* @private
|
|
143
|
+
*/
|
|
144
|
+
function startHttpServerWithRetry(serverInstance, initialPort, host, maxRetries, parentContext) {
|
|
145
|
+
const startContext = requestContextService.createRequestContext({
|
|
146
|
+
...parentContext,
|
|
147
|
+
operation: "startHttpServerWithRetry",
|
|
148
|
+
initialPort,
|
|
149
|
+
host,
|
|
150
|
+
maxRetries,
|
|
151
|
+
});
|
|
152
|
+
logger.debug(`Attempting to start HTTP server...`, startContext);
|
|
153
|
+
return new Promise(async (resolve, reject) => {
|
|
154
|
+
let lastError = null;
|
|
155
|
+
for (let i = 0; i <= maxRetries; i++) {
|
|
156
|
+
const currentPort = initialPort + i;
|
|
157
|
+
const attemptContext = requestContextService.createRequestContext({
|
|
158
|
+
...startContext,
|
|
159
|
+
port: currentPort,
|
|
160
|
+
attempt: i + 1,
|
|
161
|
+
maxAttempts: maxRetries + 1,
|
|
162
|
+
});
|
|
163
|
+
logger.debug(`Attempting port ${currentPort} (${attemptContext.attempt}/${attemptContext.maxAttempts})`, attemptContext);
|
|
164
|
+
if (await isPortInUse(currentPort, host, attemptContext)) {
|
|
165
|
+
logger.warning(`Proactive check detected port ${currentPort} is in use, retrying...`, attemptContext);
|
|
166
|
+
lastError = new Error(`EADDRINUSE: Port ${currentPort} detected as in use by proactive check.`);
|
|
167
|
+
await new Promise((res) => setTimeout(res, 100));
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
await new Promise((listenResolve, listenReject) => {
|
|
172
|
+
serverInstance
|
|
173
|
+
.listen(currentPort, host, () => {
|
|
174
|
+
const serverAddress = `http://${host}:${currentPort}${MCP_ENDPOINT_PATH}`;
|
|
175
|
+
logger.info(`HTTP transport successfully listening on host ${host} at ${serverAddress}`, { ...attemptContext, address: serverAddress });
|
|
176
|
+
listenResolve();
|
|
177
|
+
})
|
|
178
|
+
.on("error", (err) => {
|
|
179
|
+
listenReject(err);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
resolve(currentPort);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
lastError = err;
|
|
187
|
+
logger.debug(`Listen error on port ${currentPort}: Code=${err.code}, Message=${err.message}`, { ...attemptContext, errorCode: err.code, errorMessage: err.message });
|
|
188
|
+
if (err.code === "EADDRINUSE") {
|
|
189
|
+
logger.warning(`Port ${currentPort} already in use (EADDRINUSE), retrying...`, attemptContext);
|
|
190
|
+
await new Promise((res) => setTimeout(res, 100));
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
logger.error(`Failed to bind to port ${currentPort} due to non-EADDRINUSE error: ${err.message}`, { ...attemptContext, error: err.message });
|
|
194
|
+
reject(err);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
logger.error(`Failed to bind to any port after ${maxRetries + 1} attempts. Last error: ${lastError?.message}`, { ...startContext, error: lastError?.message });
|
|
200
|
+
reject(lastError ||
|
|
201
|
+
new Error("Failed to bind to any port after multiple retries."));
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Sets up and starts the Streamable HTTP transport layer for the MCP server.
|
|
206
|
+
*
|
|
207
|
+
* @param createServerInstanceFn - An asynchronous factory function that returns a new `McpServer` instance.
|
|
208
|
+
* @param parentContext - Logging context from the main server startup process.
|
|
209
|
+
* @returns A promise that resolves when the HTTP server is successfully listening.
|
|
210
|
+
* @throws {Error} If the server fails to start after all port retries.
|
|
211
|
+
*/
|
|
212
|
+
export async function startHttpTransport(createServerInstanceFn, parentContext) {
|
|
213
|
+
const app = express();
|
|
214
|
+
const transportContext = requestContextService.createRequestContext({
|
|
215
|
+
...parentContext,
|
|
216
|
+
transportType: "HTTP",
|
|
217
|
+
component: "HttpTransportSetup",
|
|
218
|
+
});
|
|
219
|
+
logger.debug("Setting up Express app for HTTP transport...", transportContext);
|
|
220
|
+
app.use(express.json());
|
|
221
|
+
app.options(MCP_ENDPOINT_PATH, (req, res) => {
|
|
222
|
+
const optionsContext = requestContextService.createRequestContext({
|
|
223
|
+
...transportContext,
|
|
224
|
+
operation: "handleOptions",
|
|
225
|
+
origin: req.headers.origin,
|
|
226
|
+
method: req.method,
|
|
227
|
+
path: req.path,
|
|
228
|
+
});
|
|
229
|
+
logger.debug(`Received OPTIONS request for ${MCP_ENDPOINT_PATH}`, optionsContext);
|
|
230
|
+
if (isOriginAllowed(req, res)) {
|
|
231
|
+
logger.debug("OPTIONS request origin allowed, sending 204.", optionsContext);
|
|
232
|
+
res.sendStatus(204);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
logger.debug("OPTIONS request origin denied, sending 403.", optionsContext);
|
|
236
|
+
res.status(403).send("Forbidden: Invalid Origin");
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
app.use((req, res, next) => {
|
|
240
|
+
const securityContext = requestContextService.createRequestContext({
|
|
241
|
+
...transportContext,
|
|
242
|
+
operation: "securityMiddleware",
|
|
243
|
+
path: req.path,
|
|
244
|
+
method: req.method,
|
|
245
|
+
origin: req.headers.origin,
|
|
246
|
+
});
|
|
247
|
+
logger.debug(`Applying security middleware...`, securityContext);
|
|
248
|
+
if (!isOriginAllowed(req, res)) {
|
|
249
|
+
logger.debug("Origin check failed, sending 403.", securityContext);
|
|
250
|
+
res.status(403).send("Forbidden: Invalid Origin");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
254
|
+
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
255
|
+
res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'self'; frame-src 'none'; font-src 'self'; connect-src 'self'");
|
|
256
|
+
logger.debug("Security middleware passed.", securityContext);
|
|
257
|
+
next();
|
|
258
|
+
});
|
|
259
|
+
app.use(mcpAuthMiddleware);
|
|
260
|
+
app.post(MCP_ENDPOINT_PATH, async (req, res) => {
|
|
261
|
+
const basePostContext = requestContextService.createRequestContext({
|
|
262
|
+
...transportContext,
|
|
263
|
+
operation: "handlePost",
|
|
264
|
+
method: "POST",
|
|
265
|
+
path: req.path,
|
|
266
|
+
origin: req.headers.origin,
|
|
267
|
+
});
|
|
268
|
+
logger.debug(`Received POST request on ${MCP_ENDPOINT_PATH}`, {
|
|
269
|
+
...basePostContext,
|
|
270
|
+
headers: req.headers,
|
|
271
|
+
bodyPreview: JSON.stringify(req.body).substring(0, 100),
|
|
272
|
+
});
|
|
273
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
274
|
+
logger.debug(`Extracted session ID: ${sessionId}`, {
|
|
275
|
+
...basePostContext,
|
|
276
|
+
sessionId,
|
|
277
|
+
});
|
|
278
|
+
let transport = sessionId ? httpTransports[sessionId] : undefined;
|
|
279
|
+
logger.debug(`Found existing transport for session ID: ${!!transport}`, {
|
|
280
|
+
...basePostContext,
|
|
281
|
+
sessionId,
|
|
282
|
+
});
|
|
283
|
+
const isInitReq = isInitializeRequest(req.body);
|
|
284
|
+
logger.debug(`Is InitializeRequest: ${isInitReq}`, {
|
|
285
|
+
...basePostContext,
|
|
286
|
+
sessionId,
|
|
287
|
+
});
|
|
288
|
+
const requestId = req.body?.id || null;
|
|
289
|
+
try {
|
|
290
|
+
if (isInitReq) {
|
|
291
|
+
if (transport) {
|
|
292
|
+
logger.warning("Received InitializeRequest on an existing session ID. Closing old session and creating new.", { ...basePostContext, sessionId });
|
|
293
|
+
await transport.close();
|
|
294
|
+
delete httpTransports[sessionId];
|
|
295
|
+
}
|
|
296
|
+
logger.info("Handling Initialize Request: Creating new session...", {
|
|
297
|
+
...basePostContext,
|
|
298
|
+
sessionId,
|
|
299
|
+
});
|
|
300
|
+
transport = new StreamableHTTPServerTransport({
|
|
301
|
+
sessionIdGenerator: () => {
|
|
302
|
+
const newId = randomUUID();
|
|
303
|
+
logger.debug(`Generated new session ID: ${newId}`, basePostContext);
|
|
304
|
+
return newId;
|
|
305
|
+
},
|
|
306
|
+
onsessioninitialized: (newId) => {
|
|
307
|
+
logger.debug(`Session initialized callback triggered for ID: ${newId}`, { ...basePostContext, newSessionId: newId });
|
|
308
|
+
httpTransports[newId] = transport;
|
|
309
|
+
logger.info(`HTTP Session created: ${newId}`, {
|
|
310
|
+
...basePostContext,
|
|
311
|
+
newSessionId: newId,
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
transport.onclose = () => {
|
|
316
|
+
const closedSessionId = transport.sessionId;
|
|
317
|
+
if (closedSessionId) {
|
|
318
|
+
logger.debug(`onclose handler triggered for session ID: ${closedSessionId}`, { ...basePostContext, closedSessionId });
|
|
319
|
+
delete httpTransports[closedSessionId];
|
|
320
|
+
logger.info(`HTTP Session closed: ${closedSessionId}`, {
|
|
321
|
+
...basePostContext,
|
|
322
|
+
closedSessionId,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
logger.debug("onclose handler triggered for transport without session ID (likely init failure).", basePostContext);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
logger.debug("Creating McpServer instance for new session...", basePostContext);
|
|
330
|
+
const server = await createServerInstanceFn();
|
|
331
|
+
logger.debug("Connecting McpServer to new transport...", basePostContext);
|
|
332
|
+
await server.connect(transport);
|
|
333
|
+
logger.debug("McpServer connected to transport.", basePostContext);
|
|
334
|
+
}
|
|
335
|
+
else if (!transport) {
|
|
336
|
+
logger.warning("Invalid or missing session ID for non-initialize POST request.", { ...basePostContext, sessionId });
|
|
337
|
+
res.status(404).json({
|
|
338
|
+
jsonrpc: "2.0",
|
|
339
|
+
error: { code: -32004, message: "Invalid or expired session ID" },
|
|
340
|
+
id: requestId,
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const currentSessionId = transport.sessionId;
|
|
345
|
+
logger.debug(`Processing POST request content for session ${currentSessionId}...`, { ...basePostContext, sessionId: currentSessionId, isInitReq });
|
|
346
|
+
await transport.handleRequest(req, res, req.body);
|
|
347
|
+
logger.debug(`Finished processing POST request content for session ${currentSessionId}.`, { ...basePostContext, sessionId: currentSessionId });
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
const errorSessionId = transport?.sessionId || sessionId;
|
|
351
|
+
logger.error("Error handling POST request", {
|
|
352
|
+
...basePostContext,
|
|
353
|
+
sessionId: errorSessionId,
|
|
354
|
+
isInitReq,
|
|
355
|
+
error: err instanceof Error ? err.message : String(err),
|
|
356
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
357
|
+
});
|
|
358
|
+
if (!res.headersSent) {
|
|
359
|
+
res.status(500).json({
|
|
360
|
+
jsonrpc: "2.0",
|
|
361
|
+
error: {
|
|
362
|
+
code: -32603,
|
|
363
|
+
message: "Internal server error during POST handling",
|
|
364
|
+
},
|
|
365
|
+
id: requestId,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
if (isInitReq && transport && !transport.sessionId) {
|
|
369
|
+
logger.debug("Cleaning up transport after initialization failure.", {
|
|
370
|
+
...basePostContext,
|
|
371
|
+
sessionId: errorSessionId,
|
|
372
|
+
});
|
|
373
|
+
await transport.close().catch((closeErr) => logger.error("Error closing transport after init failure", {
|
|
374
|
+
...basePostContext,
|
|
375
|
+
sessionId: errorSessionId,
|
|
376
|
+
closeError: closeErr,
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
const handleSessionReq = async (req, res) => {
|
|
382
|
+
const method = req.method;
|
|
383
|
+
const baseSessionReqContext = requestContextService.createRequestContext({
|
|
384
|
+
...transportContext,
|
|
385
|
+
operation: `handle${method}`,
|
|
386
|
+
method,
|
|
387
|
+
path: req.path,
|
|
388
|
+
origin: req.headers.origin,
|
|
389
|
+
});
|
|
390
|
+
logger.debug(`Received ${method} request on ${MCP_ENDPOINT_PATH}`, {
|
|
391
|
+
...baseSessionReqContext,
|
|
392
|
+
headers: req.headers,
|
|
393
|
+
});
|
|
394
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
395
|
+
logger.debug(`Extracted session ID: ${sessionId}`, {
|
|
396
|
+
...baseSessionReqContext,
|
|
397
|
+
sessionId,
|
|
398
|
+
});
|
|
399
|
+
const transport = sessionId ? httpTransports[sessionId] : undefined;
|
|
400
|
+
logger.debug(`Found existing transport for session ID: ${!!transport}`, {
|
|
401
|
+
...baseSessionReqContext,
|
|
402
|
+
sessionId,
|
|
403
|
+
});
|
|
404
|
+
if (!transport) {
|
|
405
|
+
logger.warning(`Session not found for ${method} request`, {
|
|
406
|
+
...baseSessionReqContext,
|
|
407
|
+
sessionId,
|
|
408
|
+
});
|
|
409
|
+
res.status(404).json({
|
|
410
|
+
jsonrpc: "2.0",
|
|
411
|
+
error: { code: -32004, message: "Session not found or expired" },
|
|
412
|
+
id: null, // Or a relevant request identifier if available from context
|
|
413
|
+
});
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
logger.debug(`Delegating ${method} request to transport for session ${sessionId}...`, { ...baseSessionReqContext, sessionId });
|
|
418
|
+
await transport.handleRequest(req, res);
|
|
419
|
+
logger.info(`Successfully handled ${method} request for session ${sessionId}`, { ...baseSessionReqContext, sessionId });
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
logger.error(`Error handling ${method} request for session ${sessionId}`, {
|
|
423
|
+
...baseSessionReqContext,
|
|
424
|
+
sessionId,
|
|
425
|
+
error: err instanceof Error ? err.message : String(err),
|
|
426
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
427
|
+
});
|
|
428
|
+
if (!res.headersSent) {
|
|
429
|
+
res.status(500).json({
|
|
430
|
+
jsonrpc: "2.0",
|
|
431
|
+
error: { code: -32603, message: "Internal Server Error" },
|
|
432
|
+
id: null, // Or a relevant request identifier
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
app.get(MCP_ENDPOINT_PATH, handleSessionReq);
|
|
438
|
+
app.delete(MCP_ENDPOINT_PATH, handleSessionReq);
|
|
439
|
+
logger.debug("Creating HTTP server instance...", transportContext);
|
|
440
|
+
const serverInstance = http.createServer(app);
|
|
441
|
+
try {
|
|
442
|
+
logger.debug("Attempting to start HTTP server with retry logic...", transportContext);
|
|
443
|
+
const actualPort = await startHttpServerWithRetry(serverInstance, config.mcpHttpPort, config.mcpHttpHost, MAX_PORT_RETRIES, transportContext);
|
|
444
|
+
let serverAddressLog = `http://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
|
|
445
|
+
let productionNote = "";
|
|
446
|
+
if (config.environment === "production") {
|
|
447
|
+
// The server itself runs HTTP, but it's expected to be behind an HTTPS proxy in production.
|
|
448
|
+
// The log reflects the effective public-facing URL.
|
|
449
|
+
serverAddressLog = `https://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
|
|
450
|
+
productionNote = ` (via HTTPS, ensure reverse proxy is configured)`;
|
|
451
|
+
}
|
|
452
|
+
if (process.stdout.isTTY) {
|
|
453
|
+
console.log(`\n🚀 MCP Server running in HTTP mode at: ${serverAddressLog}${productionNote}\n (MCP Spec: 2025-03-26 Streamable HTTP Transport)\n`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
logger.fatal("HTTP server failed to start after multiple port retries.", {
|
|
458
|
+
...transportContext,
|
|
459
|
+
error: err instanceof Error ? err.message : String(err),
|
|
460
|
+
});
|
|
461
|
+
throw err;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21
|
+
import { RequestContext } from "../../utils/index.js";
|
|
22
|
+
/**
|
|
23
|
+
* Connects a given `McpServer` instance to the Stdio transport.
|
|
24
|
+
* This function initializes the SDK's `StdioServerTransport`, which manages
|
|
25
|
+
* communication over `process.stdin` and `process.stdout` according to the
|
|
26
|
+
* MCP stdio transport specification.
|
|
27
|
+
*
|
|
28
|
+
* MCP Spec Points Covered by SDK's `StdioServerTransport`:
|
|
29
|
+
* - Reads JSON-RPC messages (requests, notifications, responses, batches) from stdin.
|
|
30
|
+
* - Writes JSON-RPC messages to stdout.
|
|
31
|
+
* - Handles newline delimiters and ensures no embedded newlines in output messages.
|
|
32
|
+
* - Ensures only valid MCP messages are written to stdout.
|
|
33
|
+
*
|
|
34
|
+
* Logging via the `logger` utility MAY result in output to stderr, which is
|
|
35
|
+
* permitted by the spec for logging purposes.
|
|
36
|
+
*
|
|
37
|
+
* @param server - The `McpServer` instance.
|
|
38
|
+
* @param parentContext - The logging and tracing context from the calling function.
|
|
39
|
+
* @returns A promise that resolves when the Stdio transport is successfully connected.
|
|
40
|
+
* @throws {Error} If the connection fails during setup.
|
|
41
|
+
*/
|
|
42
|
+
export declare function connectStdioTransport(server: McpServer, parentContext: RequestContext): Promise<void>;
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
|
+
import { ErrorHandler, logger } from "../../utils/index.js";
|
|
22
|
+
/**
|
|
23
|
+
* Connects a given `McpServer` instance to the Stdio transport.
|
|
24
|
+
* This function initializes the SDK's `StdioServerTransport`, which manages
|
|
25
|
+
* communication over `process.stdin` and `process.stdout` according to the
|
|
26
|
+
* MCP stdio transport specification.
|
|
27
|
+
*
|
|
28
|
+
* MCP Spec Points Covered by SDK's `StdioServerTransport`:
|
|
29
|
+
* - Reads JSON-RPC messages (requests, notifications, responses, batches) from stdin.
|
|
30
|
+
* - Writes JSON-RPC messages to stdout.
|
|
31
|
+
* - Handles newline delimiters and ensures no embedded newlines in output messages.
|
|
32
|
+
* - Ensures only valid MCP messages are written to stdout.
|
|
33
|
+
*
|
|
34
|
+
* Logging via the `logger` utility MAY result in output to stderr, which is
|
|
35
|
+
* permitted by the spec for logging purposes.
|
|
36
|
+
*
|
|
37
|
+
* @param server - The `McpServer` instance.
|
|
38
|
+
* @param parentContext - The logging and tracing context from the calling function.
|
|
39
|
+
* @returns A promise that resolves when the Stdio transport is successfully connected.
|
|
40
|
+
* @throws {Error} If the connection fails during setup.
|
|
41
|
+
*/
|
|
42
|
+
export async function connectStdioTransport(server, parentContext) {
|
|
43
|
+
const operationContext = {
|
|
44
|
+
...parentContext,
|
|
45
|
+
operation: "connectStdioTransport",
|
|
46
|
+
transportType: "Stdio",
|
|
47
|
+
};
|
|
48
|
+
logger.debug("Attempting to connect stdio transport...", operationContext);
|
|
49
|
+
try {
|
|
50
|
+
logger.debug("Creating StdioServerTransport instance...", operationContext);
|
|
51
|
+
const transport = new StdioServerTransport();
|
|
52
|
+
logger.debug("Connecting McpServer instance to StdioServerTransport...", operationContext);
|
|
53
|
+
await server.connect(transport);
|
|
54
|
+
logger.info("MCP Server connected and listening via stdio transport.", operationContext);
|
|
55
|
+
if (process.stdout.isTTY) {
|
|
56
|
+
console.log(`\n🚀 MCP Server running in STDIO mode.\n (MCP Spec: 2025-03-26 Stdio Transport)\n`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
ErrorHandler.handleError(err, { ...operationContext, critical: true });
|
|
61
|
+
throw err; // Re-throw after handling to allow caller to react if necessary
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Constants and shared type definitions for NCBI E-utility interactions.
|
|
3
|
+
* @module src/services/NCBI/ncbiConstants
|
|
4
|
+
*/
|
|
5
|
+
export declare const NCBI_EUTILS_BASE_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
|
|
6
|
+
/**
|
|
7
|
+
* Interface for common NCBI E-utility request parameters.
|
|
8
|
+
* Specific E-utilities will have additional parameters.
|
|
9
|
+
*/
|
|
10
|
+
export interface NcbiRequestParams {
|
|
11
|
+
db?: string;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Interface for options controlling how NCBI requests are made and responses are handled.
|
|
16
|
+
*/
|
|
17
|
+
export interface NcbiRequestOptions {
|
|
18
|
+
retmode?: "xml" | "json" | "text";
|
|
19
|
+
rettype?: string;
|
|
20
|
+
usePost?: boolean;
|
|
21
|
+
returnRawXml?: boolean;
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Core client for making HTTP requests to NCBI E-utilities.
|
|
3
|
+
* Handles request construction, API key injection, retries, and basic error handling.
|
|
4
|
+
* @module src/services/NCBI/ncbiCoreApiClient
|
|
5
|
+
*/
|
|
6
|
+
import { AxiosResponse } from "axios";
|
|
7
|
+
import { RequestContext } from "../../utils/index.js";
|
|
8
|
+
import { NcbiRequestParams, NcbiRequestOptions } from "./ncbiConstants.js";
|
|
9
|
+
export declare class NcbiCoreApiClient {
|
|
10
|
+
private axiosInstance;
|
|
11
|
+
constructor();
|
|
12
|
+
/**
|
|
13
|
+
* Makes an HTTP request to the specified NCBI E-utility endpoint.
|
|
14
|
+
* Handles parameter assembly, API key injection, GET/POST selection, and retries.
|
|
15
|
+
* @param endpoint The E-utility endpoint (e.g., "esearch", "efetch").
|
|
16
|
+
* @param params The parameters for the E-utility.
|
|
17
|
+
* @param context The request context for logging.
|
|
18
|
+
* @param options Options for the request, like retmode and whether to use POST.
|
|
19
|
+
* @param retries The current retry attempt number.
|
|
20
|
+
* @returns A Promise resolving to the raw AxiosResponse.
|
|
21
|
+
* @throws {McpError} If the request fails after all retries or an unexpected error occurs.
|
|
22
|
+
*/
|
|
23
|
+
makeRequest(endpoint: string, params: NcbiRequestParams, context: RequestContext, options?: NcbiRequestOptions, retries?: number): Promise<AxiosResponse>;
|
|
24
|
+
}
|