@cyanheads/pubmed-mcp-server 1.2.3 → 1.3.0
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 +2 -2
- package/dist/config/index.d.ts +13 -52
- package/dist/config/index.js +51 -222
- package/dist/mcp-server/server.d.ts +0 -5
- package/dist/mcp-server/server.js +18 -34
- package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +8 -8
- package/dist/mcp-server/tools/fetchPubMedContent/logic.js +6 -5
- package/dist/mcp-server/tools/generatePubMedChart/logic.js +19 -19
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +65 -240
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +3 -3
- package/dist/mcp-server/tools/searchPubMedArticles/logic.js +10 -11
- package/dist/mcp-server/transports/auth/authFactory.d.ts +10 -0
- package/dist/mcp-server/transports/auth/authFactory.js +41 -0
- package/dist/mcp-server/transports/auth/authMiddleware.d.ts +19 -0
- package/dist/mcp-server/transports/auth/authMiddleware.js +57 -0
- package/dist/mcp-server/transports/auth/index.d.ts +8 -5
- package/dist/mcp-server/transports/auth/index.js +6 -4
- package/dist/mcp-server/transports/auth/{core → lib}/authTypes.d.ts +0 -5
- package/dist/mcp-server/transports/auth/lib/authTypes.js +8 -0
- package/dist/mcp-server/transports/auth/{core → lib}/authUtils.js +21 -14
- package/dist/mcp-server/transports/auth/strategies/authStrategy.d.ts +17 -0
- package/dist/mcp-server/transports/auth/strategies/authStrategy.js +1 -0
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.d.ts +7 -0
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +112 -0
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.d.ts +7 -0
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +101 -0
- package/dist/mcp-server/transports/core/baseTransportManager.d.ts +17 -0
- package/dist/mcp-server/transports/core/baseTransportManager.js +18 -0
- package/dist/mcp-server/transports/core/honoNodeBridge.d.ts +23 -0
- package/dist/mcp-server/transports/core/honoNodeBridge.js +51 -0
- package/dist/mcp-server/transports/core/statefulTransportManager.d.ts +31 -0
- package/dist/mcp-server/transports/core/statefulTransportManager.js +233 -0
- package/dist/mcp-server/transports/core/statelessTransportManager.d.ts +20 -0
- package/dist/mcp-server/transports/core/statelessTransportManager.js +92 -0
- package/dist/mcp-server/transports/core/transportTypes.d.ts +68 -0
- package/dist/mcp-server/transports/core/transportTypes.js +5 -0
- package/dist/mcp-server/transports/{httpErrorHandler.d.ts → http/httpErrorHandler.d.ts} +4 -9
- package/dist/mcp-server/transports/{httpErrorHandler.js → http/httpErrorHandler.js} +33 -8
- package/dist/mcp-server/transports/http/httpTransport.d.ts +22 -0
- package/dist/mcp-server/transports/http/httpTransport.js +251 -0
- package/dist/mcp-server/transports/http/httpTypes.d.ts +16 -0
- package/dist/mcp-server/transports/http/httpTypes.js +5 -0
- package/dist/mcp-server/transports/http/index.d.ts +7 -0
- package/dist/mcp-server/transports/http/index.js +6 -0
- package/dist/mcp-server/transports/http/mcpTransportMiddleware.d.ts +25 -0
- package/dist/mcp-server/transports/http/mcpTransportMiddleware.js +63 -0
- package/dist/mcp-server/transports/stdio/index.d.ts +5 -0
- package/dist/mcp-server/transports/stdio/index.js +5 -0
- package/dist/mcp-server/transports/{stdioTransport.d.ts → stdio/stdioTransport.d.ts} +2 -2
- package/dist/mcp-server/transports/{stdioTransport.js → stdio/stdioTransport.js} +10 -5
- package/dist/services/NCBI/{ncbiConstants.d.ts → core/ncbiConstants.d.ts} +1 -1
- package/dist/services/NCBI/{ncbiConstants.js → core/ncbiConstants.js} +1 -1
- package/dist/services/NCBI/{ncbiCoreApiClient.d.ts → core/ncbiCoreApiClient.d.ts} +3 -3
- package/dist/services/NCBI/core/ncbiCoreApiClient.js +117 -0
- package/dist/services/NCBI/{ncbiRequestQueueManager.d.ts → core/ncbiRequestQueueManager.d.ts} +2 -2
- package/dist/services/NCBI/{ncbiRequestQueueManager.js → core/ncbiRequestQueueManager.js} +3 -3
- package/dist/services/NCBI/{ncbiResponseHandler.d.ts → core/ncbiResponseHandler.d.ts} +2 -2
- package/dist/services/NCBI/{ncbiResponseHandler.js → core/ncbiResponseHandler.js} +3 -3
- package/dist/services/NCBI/{ncbiService.d.ts → core/ncbiService.d.ts} +5 -4
- package/dist/services/NCBI/{ncbiService.js → core/ncbiService.js} +15 -3
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.js +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.js +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.js +5 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.js +1 -1
- package/dist/types-global/errors.d.ts +2 -0
- package/dist/types-global/errors.js +2 -0
- package/dist/types-global/pubmedXml.d.ts +15 -0
- package/dist/utils/internal/errorHandler.js +1 -1
- package/dist/utils/internal/logger.d.ts +13 -1
- package/dist/utils/internal/logger.js +43 -9
- package/dist/utils/network/fetchWithTimeout.d.ts +21 -0
- package/dist/utils/network/fetchWithTimeout.js +59 -0
- package/dist/utils/network/index.d.ts +6 -0
- package/dist/utils/network/index.js +5 -0
- package/dist/utils/scheduling/index.d.ts +6 -0
- package/dist/utils/scheduling/index.js +6 -0
- package/dist/utils/scheduling/scheduler.d.ts +72 -0
- package/dist/utils/scheduling/scheduler.js +150 -0
- package/dist/utils/security/sanitization.js +35 -18
- package/package.json +17 -13
- package/dist/mcp-server/transports/auth/core/authTypes.js +0 -5
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.d.ts +0 -27
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +0 -149
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.d.ts +0 -20
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +0 -124
- package/dist/mcp-server/transports/httpTransport.d.ts +0 -21
- package/dist/mcp-server/transports/httpTransport.js +0 -208
- package/dist/services/NCBI/ncbiCoreApiClient.js +0 -113
- /package/dist/mcp-server/transports/auth/{core → lib}/authContext.d.ts +0 -0
- /package/dist/mcp-server/transports/auth/{core → lib}/authContext.js +0 -0
- /package/dist/mcp-server/transports/auth/{core → lib}/authUtils.d.ts +0 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Configures and starts the HTTP MCP transport using Hono.
|
|
3
|
+
* This file has been refactored to correctly integrate Hono's streaming
|
|
4
|
+
* capabilities with the Model Context Protocol SDK's transport layer.
|
|
5
|
+
* @module src/mcp-server/transports/http/httpTransport
|
|
6
|
+
*/
|
|
7
|
+
import { serve } from "@hono/node-server";
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import { cors } from "hono/cors";
|
|
10
|
+
import { stream } from "hono/streaming";
|
|
11
|
+
import http from "http";
|
|
12
|
+
import { config } from "../../../config/index.js";
|
|
13
|
+
import { logger, rateLimiter, requestContextService, } from "../../../utils/index.js";
|
|
14
|
+
import { createAuthMiddleware, createAuthStrategy } from "../auth/index.js";
|
|
15
|
+
import { StatefulTransportManager } from "../core/statefulTransportManager.js";
|
|
16
|
+
import { StatelessTransportManager } from "../core/statelessTransportManager.js";
|
|
17
|
+
import { httpErrorHandler } from "./httpErrorHandler.js";
|
|
18
|
+
import { mcpTransportMiddleware } from "./mcpTransportMiddleware.js";
|
|
19
|
+
const HTTP_PORT = config.mcpHttpPort;
|
|
20
|
+
const HTTP_HOST = config.mcpHttpHost;
|
|
21
|
+
const MCP_ENDPOINT_PATH = config.mcpHttpEndpointPath;
|
|
22
|
+
/**
|
|
23
|
+
* Converts a Fetch API Headers object to Node.js IncomingHttpHeaders.
|
|
24
|
+
* Hono uses Fetch API Headers, but the underlying transport managers expect
|
|
25
|
+
* Node's native IncomingHttpHeaders.
|
|
26
|
+
* @param headers - The Headers object to convert.
|
|
27
|
+
* @returns An object compatible with IncomingHttpHeaders.
|
|
28
|
+
*/
|
|
29
|
+
async function isPortInUse(port, host, parentContext) {
|
|
30
|
+
const context = { ...parentContext, operation: "isPortInUse", port, host };
|
|
31
|
+
logger.debug(`Checking if port ${port} is in use...`, context);
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const tempServer = http.createServer();
|
|
34
|
+
tempServer
|
|
35
|
+
.once("error", (err) => {
|
|
36
|
+
const inUse = err.code === "EADDRINUSE";
|
|
37
|
+
logger.debug(`Port check resulted in error: ${err.code}. Port in use: ${inUse}`, context);
|
|
38
|
+
resolve(inUse);
|
|
39
|
+
})
|
|
40
|
+
.once("listening", () => {
|
|
41
|
+
logger.debug(`Successfully bound to port ${port} temporarily. Port is not in use.`, context);
|
|
42
|
+
tempServer.close(() => resolve(false));
|
|
43
|
+
})
|
|
44
|
+
.listen(port, host);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentContext) {
|
|
48
|
+
const startContext = {
|
|
49
|
+
...parentContext,
|
|
50
|
+
operation: "startHttpServerWithRetry",
|
|
51
|
+
};
|
|
52
|
+
logger.info(`Attempting to start HTTP server on port ${initialPort} with ${maxRetries} retries.`, startContext);
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const tryBind = (port, attempt) => {
|
|
55
|
+
const attemptContext = { ...startContext, port, attempt };
|
|
56
|
+
if (attempt > maxRetries + 1) {
|
|
57
|
+
const error = new Error(`Failed to bind to any port after ${maxRetries} retries.`);
|
|
58
|
+
logger.fatal(error.message, attemptContext);
|
|
59
|
+
return reject(error);
|
|
60
|
+
}
|
|
61
|
+
isPortInUse(port, host, attemptContext)
|
|
62
|
+
.then((inUse) => {
|
|
63
|
+
if (inUse) {
|
|
64
|
+
logger.warning(`Port ${port} is in use, retrying on port ${port + 1}...`, attemptContext);
|
|
65
|
+
setTimeout(() => tryBind(port + 1, attempt + 1), config.mcpHttpPortRetryDelayMs);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const serverInstance = serve({ fetch: app.fetch, port, hostname: host }, (info) => {
|
|
70
|
+
const serverAddress = `http://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
|
|
71
|
+
logger.info(`HTTP transport listening at ${serverAddress}`, {
|
|
72
|
+
...attemptContext,
|
|
73
|
+
address: serverAddress,
|
|
74
|
+
sessionMode: config.mcpSessionMode,
|
|
75
|
+
});
|
|
76
|
+
if (process.stdout.isTTY) {
|
|
77
|
+
console.log(`\n🚀 MCP Server running at: ${serverAddress}`);
|
|
78
|
+
console.log(` Session Mode: ${config.mcpSessionMode}\n`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
resolve(serverInstance);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
if (err &&
|
|
85
|
+
typeof err === "object" &&
|
|
86
|
+
"code" in err &&
|
|
87
|
+
err.code !== "EADDRINUSE") {
|
|
88
|
+
const errorToLog = err instanceof Error ? err : new Error(String(err));
|
|
89
|
+
logger.error("An unexpected error occurred while starting the server.", errorToLog, attemptContext);
|
|
90
|
+
return reject(err);
|
|
91
|
+
}
|
|
92
|
+
logger.warning(`Encountered EADDRINUSE race condition on port ${port}, retrying...`, attemptContext);
|
|
93
|
+
setTimeout(() => tryBind(port + 1, attempt + 1), config.mcpHttpPortRetryDelayMs);
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
.catch((err) => {
|
|
97
|
+
logger.fatal("Failed to check if port is in use.", err, attemptContext);
|
|
98
|
+
reject(err);
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
tryBind(initialPort, 1);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function createTransportManager(createServerInstanceFn, sessionMode, context) {
|
|
105
|
+
const opContext = {
|
|
106
|
+
...context,
|
|
107
|
+
operation: "createTransportManager",
|
|
108
|
+
sessionMode,
|
|
109
|
+
};
|
|
110
|
+
logger.info(`Creating transport manager for session mode: ${sessionMode}`, opContext);
|
|
111
|
+
switch (sessionMode) {
|
|
112
|
+
case "stateless":
|
|
113
|
+
return new StatelessTransportManager(createServerInstanceFn);
|
|
114
|
+
case "stateful":
|
|
115
|
+
return new StatefulTransportManager(createServerInstanceFn);
|
|
116
|
+
case "auto":
|
|
117
|
+
default:
|
|
118
|
+
logger.info("Defaulting to 'auto' mode (stateful with stateless fallback).", opContext);
|
|
119
|
+
return new StatefulTransportManager(createServerInstanceFn);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export function createHttpApp(transportManager, createServerInstanceFn, parentContext) {
|
|
123
|
+
const app = new Hono();
|
|
124
|
+
const transportContext = {
|
|
125
|
+
...parentContext,
|
|
126
|
+
component: "HttpTransportSetup",
|
|
127
|
+
};
|
|
128
|
+
logger.info("Creating Hono HTTP application.", transportContext);
|
|
129
|
+
app.use("*", cors({
|
|
130
|
+
origin: config.mcpAllowedOrigins || [],
|
|
131
|
+
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
132
|
+
allowHeaders: [
|
|
133
|
+
"Content-Type",
|
|
134
|
+
"Mcp-Session-Id",
|
|
135
|
+
"Last-Event-ID",
|
|
136
|
+
"Authorization",
|
|
137
|
+
],
|
|
138
|
+
credentials: true,
|
|
139
|
+
}));
|
|
140
|
+
app.use("*", async (c, next) => {
|
|
141
|
+
c.env.outgoing.setHeader("X-Content-Type-Options", "nosniff");
|
|
142
|
+
await next();
|
|
143
|
+
});
|
|
144
|
+
app.use(MCP_ENDPOINT_PATH, async (c, next) => {
|
|
145
|
+
const clientIp = c.req.header("x-forwarded-for")?.split(",")[0].trim() || "unknown_ip";
|
|
146
|
+
const context = requestContextService.createRequestContext({
|
|
147
|
+
operation: "httpRateLimitCheck",
|
|
148
|
+
ipAddress: clientIp,
|
|
149
|
+
});
|
|
150
|
+
try {
|
|
151
|
+
rateLimiter.check(clientIp, context);
|
|
152
|
+
logger.debug("Rate limit check passed.", context);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
logger.warning("Rate limit check failed.", {
|
|
156
|
+
...context,
|
|
157
|
+
error: error instanceof Error ? error.message : String(error),
|
|
158
|
+
});
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
await next();
|
|
162
|
+
});
|
|
163
|
+
const authStrategy = createAuthStrategy();
|
|
164
|
+
if (authStrategy) {
|
|
165
|
+
logger.info("Authentication strategy found, enabling auth middleware.", transportContext);
|
|
166
|
+
app.use(MCP_ENDPOINT_PATH, createAuthMiddleware(authStrategy));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
logger.info("No authentication strategy found, auth middleware disabled.", transportContext);
|
|
170
|
+
}
|
|
171
|
+
app.onError(httpErrorHandler);
|
|
172
|
+
app.get("/healthz", (c) => {
|
|
173
|
+
return c.json({
|
|
174
|
+
status: "ok",
|
|
175
|
+
timestamp: new Date().toISOString(),
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
app.get(MCP_ENDPOINT_PATH, (c) => {
|
|
179
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
180
|
+
if (sessionId) {
|
|
181
|
+
return c.text("GET requests to existing sessions are not supported.", 405);
|
|
182
|
+
}
|
|
183
|
+
return c.json({
|
|
184
|
+
status: "ok",
|
|
185
|
+
mode: "stateless",
|
|
186
|
+
message: "Server is running. Provide a Mcp-Session-Id header to stream from a session.",
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
app.post(MCP_ENDPOINT_PATH, mcpTransportMiddleware(transportManager, createServerInstanceFn), (c) => {
|
|
190
|
+
const response = c.get("mcpResponse");
|
|
191
|
+
if (response.sessionId) {
|
|
192
|
+
c.header("Mcp-Session-Id", response.sessionId);
|
|
193
|
+
}
|
|
194
|
+
response.headers.forEach((value, key) => {
|
|
195
|
+
c.header(key, value);
|
|
196
|
+
});
|
|
197
|
+
c.status(response.statusCode);
|
|
198
|
+
if (response.stream) {
|
|
199
|
+
return stream(c, async (s) => {
|
|
200
|
+
if (response.stream) {
|
|
201
|
+
await s.pipe(response.stream);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
207
|
+
return c.json(response.body);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
app.delete(MCP_ENDPOINT_PATH, async (c) => {
|
|
211
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
212
|
+
const context = requestContextService.createRequestContext({
|
|
213
|
+
...transportContext,
|
|
214
|
+
operation: "handleDeleteRequest",
|
|
215
|
+
sessionId,
|
|
216
|
+
});
|
|
217
|
+
if (sessionId) {
|
|
218
|
+
if (transportManager instanceof StatefulTransportManager) {
|
|
219
|
+
const response = await transportManager.handleDeleteRequest(sessionId, context);
|
|
220
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
221
|
+
return c.json(response.body, response.statusCode);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
return c.json({
|
|
225
|
+
error: "Method Not Allowed",
|
|
226
|
+
message: "DELETE operations are not supported in this mode.",
|
|
227
|
+
}, 405);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
return c.json({
|
|
232
|
+
status: "stateless_mode",
|
|
233
|
+
message: "No sessions to delete in stateless mode",
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
logger.info("Hono application setup complete.", transportContext);
|
|
238
|
+
return app;
|
|
239
|
+
}
|
|
240
|
+
export async function startHttpTransport(createServerInstanceFn, parentContext) {
|
|
241
|
+
const transportContext = {
|
|
242
|
+
...parentContext,
|
|
243
|
+
component: "HttpTransportStart",
|
|
244
|
+
};
|
|
245
|
+
logger.info("Starting HTTP transport.", transportContext);
|
|
246
|
+
const transportManager = createTransportManager(createServerInstanceFn, config.mcpSessionMode, transportContext);
|
|
247
|
+
const app = createHttpApp(transportManager, createServerInstanceFn, transportContext);
|
|
248
|
+
const server = await startHttpServerWithRetry(app, HTTP_PORT, HTTP_HOST, config.mcpHttpMaxPortRetries, transportContext);
|
|
249
|
+
logger.info("HTTP transport started successfully.", transportContext);
|
|
250
|
+
return { app, server, transportManager };
|
|
251
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines custom types for the Hono HTTP transport layer.
|
|
3
|
+
* @module src/mcp-server/transports/http/httpTypes
|
|
4
|
+
*/
|
|
5
|
+
import type { IncomingMessage, ServerResponse } from "http";
|
|
6
|
+
/**
|
|
7
|
+
* Extends Hono's Bindings to include the raw Node.js request and response objects.
|
|
8
|
+
* This is necessary for integrating with libraries like the MCP SDK that
|
|
9
|
+
* need to write directly to the response stream.
|
|
10
|
+
*
|
|
11
|
+
* As per `@hono/node-server`, the response object is available on `c.env.outgoing`.
|
|
12
|
+
*/
|
|
13
|
+
export type HonoNodeBindings = {
|
|
14
|
+
incoming: IncomingMessage;
|
|
15
|
+
outgoing: ServerResponse;
|
|
16
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Barrel file for the HTTP transport module.
|
|
3
|
+
* @module src/mcp-server/transports/http/index
|
|
4
|
+
*/
|
|
5
|
+
export { createHttpApp, startHttpTransport } from "./httpTransport.js";
|
|
6
|
+
export { httpErrorHandler } from "./httpErrorHandler.js";
|
|
7
|
+
export type { HonoNodeBindings } from "./httpTypes.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { TransportManager, TransportResponse } from "../core/transportTypes.js";
|
|
10
|
+
import { HonoNodeBindings } from "./httpTypes.js";
|
|
11
|
+
/**
|
|
12
|
+
* Creates a Hono middleware for handling MCP POST requests.
|
|
13
|
+
* @param transportManager - The main transport manager (usually stateful).
|
|
14
|
+
* @param createServerInstanceFn - Function to create an McpServer instance.
|
|
15
|
+
* @returns A Hono middleware function.
|
|
16
|
+
*/
|
|
17
|
+
type McpMiddlewareEnv = {
|
|
18
|
+
Variables: {
|
|
19
|
+
mcpResponse: TransportResponse;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export declare const mcpTransportMiddleware: (transportManager: TransportManager, createServerInstanceFn: () => Promise<McpServer>) => import("hono").MiddlewareHandler<McpMiddlewareEnv & {
|
|
23
|
+
Bindings: HonoNodeBindings;
|
|
24
|
+
}, string, {}>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { createMiddleware } from "hono/factory";
|
|
10
|
+
import { config } from "../../../config/index.js";
|
|
11
|
+
import { requestContextService } from "../../../utils/index.js";
|
|
12
|
+
import { StatelessTransportManager } from "../core/statelessTransportManager.js";
|
|
13
|
+
/**
|
|
14
|
+
* Converts a Fetch API Headers object to Node.js IncomingHttpHeaders.
|
|
15
|
+
* @param headers - The Headers object to convert.
|
|
16
|
+
* @returns An object compatible with IncomingHttpHeaders.
|
|
17
|
+
*/
|
|
18
|
+
function toIncomingHttpHeaders(headers) {
|
|
19
|
+
const result = {};
|
|
20
|
+
headers.forEach((value, key) => {
|
|
21
|
+
result[key] = value;
|
|
22
|
+
});
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Handles a stateless request by creating an ephemeral transport manager.
|
|
27
|
+
* @param createServerInstanceFn - Function to create an McpServer instance.
|
|
28
|
+
* @param headers - The request headers.
|
|
29
|
+
* @param body - The request body.
|
|
30
|
+
* @param context - The request context.
|
|
31
|
+
* @returns A promise resolving with the transport response.
|
|
32
|
+
*/
|
|
33
|
+
async function handleStatelessRequest(createServerInstanceFn, headers, body, context) {
|
|
34
|
+
const statelessManager = new StatelessTransportManager(createServerInstanceFn);
|
|
35
|
+
return statelessManager.handleRequest(toIncomingHttpHeaders(headers), body, context);
|
|
36
|
+
}
|
|
37
|
+
export const mcpTransportMiddleware = (transportManager, createServerInstanceFn) => createMiddleware(async (c, next) => {
|
|
38
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
39
|
+
const context = requestContextService.createRequestContext({
|
|
40
|
+
operation: "mcpTransportMiddleware",
|
|
41
|
+
sessionId,
|
|
42
|
+
});
|
|
43
|
+
const body = await c.req.json();
|
|
44
|
+
let response;
|
|
45
|
+
if (isInitializeRequest(body)) {
|
|
46
|
+
if (config.mcpSessionMode === "stateless") {
|
|
47
|
+
response = await handleStatelessRequest(createServerInstanceFn, c.req.raw.headers, body, context);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
response = await transportManager.initializeAndHandle(toIncomingHttpHeaders(c.req.raw.headers), body, context);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
if (sessionId) {
|
|
55
|
+
response = await transportManager.handleRequest(toIncomingHttpHeaders(c.req.raw.headers), body, context, sessionId);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
response = await handleStatelessRequest(createServerInstanceFn, c.req.raw.headers, body, context);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
c.set("mcpResponse", response);
|
|
62
|
+
await next();
|
|
63
|
+
});
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* @module src/mcp-server/transports/stdioTransport
|
|
19
19
|
*/
|
|
20
20
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21
|
-
import { RequestContext } from "
|
|
21
|
+
import { RequestContext } from "../../../utils/index.js";
|
|
22
22
|
/**
|
|
23
23
|
* Connects a given `McpServer` instance to the Stdio transport.
|
|
24
24
|
* This function initializes the SDK's `StdioServerTransport`, which manages
|
|
@@ -39,4 +39,4 @@ import { RequestContext } from "../../utils/index.js";
|
|
|
39
39
|
* @returns A promise that resolves when the Stdio transport is successfully connected.
|
|
40
40
|
* @throws {Error} If the connection fails during setup.
|
|
41
41
|
*/
|
|
42
|
-
export declare function
|
|
42
|
+
export declare function startStdioTransport(server: McpServer, parentContext: RequestContext): Promise<void>;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* @module src/mcp-server/transports/stdioTransport
|
|
19
19
|
*/
|
|
20
20
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
|
-
import { ErrorHandler, logger } from "
|
|
21
|
+
import { ErrorHandler, logger } from "../../../utils/index.js";
|
|
22
22
|
/**
|
|
23
23
|
* Connects a given `McpServer` instance to the Stdio transport.
|
|
24
24
|
* This function initializes the SDK's `StdioServerTransport`, which manages
|
|
@@ -39,13 +39,13 @@ import { ErrorHandler, logger } from "../../utils/index.js";
|
|
|
39
39
|
* @returns A promise that resolves when the Stdio transport is successfully connected.
|
|
40
40
|
* @throws {Error} If the connection fails during setup.
|
|
41
41
|
*/
|
|
42
|
-
export async function
|
|
42
|
+
export async function startStdioTransport(server, parentContext) {
|
|
43
43
|
const operationContext = {
|
|
44
44
|
...parentContext,
|
|
45
45
|
operation: "connectStdioTransport",
|
|
46
46
|
transportType: "Stdio",
|
|
47
47
|
};
|
|
48
|
-
logger.
|
|
48
|
+
logger.info("Attempting to connect stdio transport...", operationContext);
|
|
49
49
|
try {
|
|
50
50
|
logger.debug("Creating StdioServerTransport instance...", operationContext);
|
|
51
51
|
const transport = new StdioServerTransport();
|
|
@@ -57,7 +57,12 @@ export async function connectStdioTransport(server, parentContext) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
catch (err) {
|
|
60
|
-
ErrorHandler
|
|
61
|
-
throw err
|
|
60
|
+
// Let the ErrorHandler log the error with all context, then rethrow.
|
|
61
|
+
throw ErrorHandler.handleError(err, {
|
|
62
|
+
operation: "connectStdioTransport",
|
|
63
|
+
context: operationContext,
|
|
64
|
+
critical: true,
|
|
65
|
+
rethrow: true,
|
|
66
|
+
});
|
|
62
67
|
}
|
|
63
68
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Constants and shared type definitions for NCBI E-utility interactions.
|
|
3
|
-
* @module src/services/NCBI/ncbiConstants
|
|
3
|
+
* @module src/services/NCBI/core/ncbiConstants
|
|
4
4
|
*/
|
|
5
5
|
export declare const NCBI_EUTILS_BASE_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
|
|
6
6
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Constants and shared type definitions for NCBI E-utility interactions.
|
|
3
|
-
* @module src/services/NCBI/ncbiConstants
|
|
3
|
+
* @module src/services/NCBI/core/ncbiConstants
|
|
4
4
|
*/
|
|
5
5
|
export const NCBI_EUTILS_BASE_URL = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils";
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Core client for making HTTP requests to NCBI E-utilities.
|
|
3
3
|
* Handles request construction, API key injection, retries, and basic error handling.
|
|
4
|
-
* @module src/services/NCBI/ncbiCoreApiClient
|
|
4
|
+
* @module src/services/NCBI/core/ncbiCoreApiClient
|
|
5
5
|
*/
|
|
6
6
|
import { AxiosResponse } from "axios";
|
|
7
|
-
import { RequestContext } from "
|
|
7
|
+
import { RequestContext } from "../../../utils/index.js";
|
|
8
8
|
import { NcbiRequestParams, NcbiRequestOptions } from "./ncbiConstants.js";
|
|
9
9
|
export declare class NcbiCoreApiClient {
|
|
10
10
|
private axiosInstance;
|
|
@@ -20,5 +20,5 @@ export declare class NcbiCoreApiClient {
|
|
|
20
20
|
* @returns A Promise resolving to the raw AxiosResponse.
|
|
21
21
|
* @throws {McpError} If the request fails after all retries or an unexpected error occurs.
|
|
22
22
|
*/
|
|
23
|
-
makeRequest(endpoint: string, params: NcbiRequestParams, context: RequestContext, options?: NcbiRequestOptions
|
|
23
|
+
makeRequest(endpoint: string, params: NcbiRequestParams, context: RequestContext, options?: NcbiRequestOptions): Promise<AxiosResponse>;
|
|
24
24
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
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/core/ncbiCoreApiClient
|
|
5
|
+
*/
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
import { config } from "../../../config/index.js";
|
|
8
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
9
|
+
import { logger, requestContextService, sanitizeInputForLogging, } from "../../../utils/index.js";
|
|
10
|
+
import { NCBI_EUTILS_BASE_URL, } from "./ncbiConstants.js";
|
|
11
|
+
export class NcbiCoreApiClient {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.axiosInstance = axios.create({
|
|
14
|
+
timeout: 30000, // 30 seconds timeout for NCBI requests
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Makes an HTTP request to the specified NCBI E-utility endpoint.
|
|
19
|
+
* Handles parameter assembly, API key injection, GET/POST selection, and retries.
|
|
20
|
+
* @param endpoint The E-utility endpoint (e.g., "esearch", "efetch").
|
|
21
|
+
* @param params The parameters for the E-utility.
|
|
22
|
+
* @param context The request context for logging.
|
|
23
|
+
* @param options Options for the request, like retmode and whether to use POST.
|
|
24
|
+
* @param retries The current retry attempt number.
|
|
25
|
+
* @returns A Promise resolving to the raw AxiosResponse.
|
|
26
|
+
* @throws {McpError} If the request fails after all retries or an unexpected error occurs.
|
|
27
|
+
*/
|
|
28
|
+
async makeRequest(endpoint, params, context, options = {}) {
|
|
29
|
+
const rawParams = {
|
|
30
|
+
tool: config.ncbiToolIdentifier,
|
|
31
|
+
email: config.ncbiAdminEmail,
|
|
32
|
+
api_key: config.ncbiApiKey,
|
|
33
|
+
...params,
|
|
34
|
+
};
|
|
35
|
+
const finalParams = {};
|
|
36
|
+
for (const key in rawParams) {
|
|
37
|
+
if (Object.prototype.hasOwnProperty.call(rawParams, key)) {
|
|
38
|
+
const value = rawParams[key];
|
|
39
|
+
if (value !== undefined && value !== null) {
|
|
40
|
+
finalParams[key] = String(value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const requestConfig = {
|
|
45
|
+
method: options.usePost ? "POST" : "GET",
|
|
46
|
+
url: `${NCBI_EUTILS_BASE_URL}/${endpoint}.fcgi`,
|
|
47
|
+
};
|
|
48
|
+
if (options.usePost) {
|
|
49
|
+
requestConfig.data = new URLSearchParams(finalParams).toString();
|
|
50
|
+
requestConfig.headers = {
|
|
51
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
requestConfig.params = finalParams;
|
|
56
|
+
}
|
|
57
|
+
for (let attempt = 0; attempt <= config.ncbiMaxRetries; attempt++) {
|
|
58
|
+
try {
|
|
59
|
+
logger.debug(`Making NCBI HTTP request: ${requestConfig.method} ${requestConfig.url}`, requestContextService.createRequestContext({
|
|
60
|
+
...context,
|
|
61
|
+
operation: "NCBI_HttpRequest",
|
|
62
|
+
endpoint,
|
|
63
|
+
method: requestConfig.method,
|
|
64
|
+
requestParams: sanitizeInputForLogging(finalParams),
|
|
65
|
+
attempt: attempt + 1,
|
|
66
|
+
}));
|
|
67
|
+
const response = await this.axiosInstance(requestConfig);
|
|
68
|
+
return response;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (attempt < config.ncbiMaxRetries) {
|
|
72
|
+
const retryDelay = Math.pow(2, attempt) * 200;
|
|
73
|
+
logger.warning(`NCBI request to ${endpoint} failed. Retrying (${attempt + 1}/${config.ncbiMaxRetries}) in ${retryDelay}ms...`, requestContextService.createRequestContext({
|
|
74
|
+
...context,
|
|
75
|
+
operation: "NCBI_HttpRequestRetry",
|
|
76
|
+
endpoint,
|
|
77
|
+
error: error.message,
|
|
78
|
+
retryCount: attempt + 1,
|
|
79
|
+
maxRetries: config.ncbiMaxRetries,
|
|
80
|
+
delay: retryDelay,
|
|
81
|
+
}));
|
|
82
|
+
await new Promise((r) => setTimeout(r, retryDelay));
|
|
83
|
+
continue; // Continue to the next iteration of the loop
|
|
84
|
+
}
|
|
85
|
+
// If all retries are exhausted, handle the final error
|
|
86
|
+
if (axios.isAxiosError(error)) {
|
|
87
|
+
logger.error(`Axios error during NCBI request to ${endpoint} after ${attempt} retries`, error, requestContextService.createRequestContext({
|
|
88
|
+
...context,
|
|
89
|
+
operation: "NCBI_AxiosError",
|
|
90
|
+
endpoint,
|
|
91
|
+
status: error.response?.status,
|
|
92
|
+
responseData: sanitizeInputForLogging(error.response?.data),
|
|
93
|
+
}));
|
|
94
|
+
throw new McpError(BaseErrorCode.NCBI_SERVICE_UNAVAILABLE, `NCBI request failed: ${error.message}`, {
|
|
95
|
+
endpoint,
|
|
96
|
+
status: error.response?.status,
|
|
97
|
+
details: error.response?.data
|
|
98
|
+
? String(error.response.data).substring(0, 500)
|
|
99
|
+
: undefined,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (error instanceof McpError)
|
|
103
|
+
throw error;
|
|
104
|
+
logger.error(`Unexpected error during NCBI request to ${endpoint} after ${attempt} retries`, error, requestContextService.createRequestContext({
|
|
105
|
+
...context,
|
|
106
|
+
operation: "NCBI_UnexpectedError",
|
|
107
|
+
endpoint,
|
|
108
|
+
errorMessage: error.message,
|
|
109
|
+
}));
|
|
110
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Unexpected error communicating with NCBI: ${error.message}`, { endpoint });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// This line should theoretically be unreachable, but it satisfies TypeScript's need
|
|
114
|
+
// for a return path if the loop completes without returning or throwing.
|
|
115
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Request failed after all retries.", { endpoint });
|
|
116
|
+
}
|
|
117
|
+
}
|
package/dist/services/NCBI/{ncbiRequestQueueManager.d.ts → core/ncbiRequestQueueManager.d.ts}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Manages a queue for NCBI E-utility requests to ensure compliance with rate limits.
|
|
3
|
-
* @module src/services/NCBI/ncbiRequestQueueManager
|
|
3
|
+
* @module src/services/NCBI/core/ncbiRequestQueueManager
|
|
4
4
|
*/
|
|
5
|
-
import { RequestContext } from "
|
|
5
|
+
import { RequestContext } from "../../../utils/index.js";
|
|
6
6
|
import { NcbiRequestParams } from "./ncbiConstants.js";
|
|
7
7
|
/**
|
|
8
8
|
* Interface for a queued NCBI request.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Manages a queue for NCBI E-utility requests to ensure compliance with rate limits.
|
|
3
|
-
* @module src/services/NCBI/ncbiRequestQueueManager
|
|
3
|
+
* @module src/services/NCBI/core/ncbiRequestQueueManager
|
|
4
4
|
*/
|
|
5
|
-
import { config } from "
|
|
6
|
-
import { logger, requestContextService, sanitizeInputForLogging, } from "
|
|
5
|
+
import { config } from "../../../config/index.js";
|
|
6
|
+
import { logger, requestContextService, sanitizeInputForLogging, } from "../../../utils/index.js";
|
|
7
7
|
export class NcbiRequestQueueManager {
|
|
8
8
|
constructor() {
|
|
9
9
|
this.requestQueue = [];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Handles parsing of NCBI E-utility responses and NCBI-specific error extraction.
|
|
3
|
-
* @module src/services/NCBI/ncbiResponseHandler
|
|
3
|
+
* @module src/services/NCBI/core/ncbiResponseHandler
|
|
4
4
|
*/
|
|
5
5
|
import { AxiosResponse } from "axios";
|
|
6
|
-
import { RequestContext } from "
|
|
6
|
+
import { RequestContext } from "../../../utils/index.js";
|
|
7
7
|
import { NcbiRequestOptions } from "./ncbiConstants.js";
|
|
8
8
|
export declare class NcbiResponseHandler {
|
|
9
9
|
private xmlParser;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Handles parsing of NCBI E-utility responses and NCBI-specific error extraction.
|
|
3
|
-
* @module src/services/NCBI/ncbiResponseHandler
|
|
3
|
+
* @module src/services/NCBI/core/ncbiResponseHandler
|
|
4
4
|
*/
|
|
5
5
|
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
6
|
-
import { BaseErrorCode, McpError } from "
|
|
7
|
-
import { logger, requestContextService, sanitizeInputForLogging, } from "
|
|
6
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
7
|
+
import { logger, requestContextService, sanitizeInputForLogging, } from "../../../utils/index.js";
|
|
8
8
|
export class NcbiResponseHandler {
|
|
9
9
|
constructor() {
|
|
10
10
|
this.xmlParser = new XMLParser({
|