@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.
Files changed (95) hide show
  1. package/README.md +2 -2
  2. package/dist/config/index.d.ts +13 -52
  3. package/dist/config/index.js +51 -222
  4. package/dist/mcp-server/server.d.ts +0 -5
  5. package/dist/mcp-server/server.js +18 -34
  6. package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +8 -8
  7. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +6 -5
  8. package/dist/mcp-server/tools/generatePubMedChart/logic.js +19 -19
  9. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +65 -240
  10. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +3 -3
  11. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +10 -11
  12. package/dist/mcp-server/transports/auth/authFactory.d.ts +10 -0
  13. package/dist/mcp-server/transports/auth/authFactory.js +41 -0
  14. package/dist/mcp-server/transports/auth/authMiddleware.d.ts +19 -0
  15. package/dist/mcp-server/transports/auth/authMiddleware.js +57 -0
  16. package/dist/mcp-server/transports/auth/index.d.ts +8 -5
  17. package/dist/mcp-server/transports/auth/index.js +6 -4
  18. package/dist/mcp-server/transports/auth/{core → lib}/authTypes.d.ts +0 -5
  19. package/dist/mcp-server/transports/auth/lib/authTypes.js +8 -0
  20. package/dist/mcp-server/transports/auth/{core → lib}/authUtils.js +21 -14
  21. package/dist/mcp-server/transports/auth/strategies/authStrategy.d.ts +17 -0
  22. package/dist/mcp-server/transports/auth/strategies/authStrategy.js +1 -0
  23. package/dist/mcp-server/transports/auth/strategies/jwtStrategy.d.ts +7 -0
  24. package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +112 -0
  25. package/dist/mcp-server/transports/auth/strategies/oauthStrategy.d.ts +7 -0
  26. package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +101 -0
  27. package/dist/mcp-server/transports/core/baseTransportManager.d.ts +17 -0
  28. package/dist/mcp-server/transports/core/baseTransportManager.js +18 -0
  29. package/dist/mcp-server/transports/core/honoNodeBridge.d.ts +23 -0
  30. package/dist/mcp-server/transports/core/honoNodeBridge.js +51 -0
  31. package/dist/mcp-server/transports/core/statefulTransportManager.d.ts +31 -0
  32. package/dist/mcp-server/transports/core/statefulTransportManager.js +233 -0
  33. package/dist/mcp-server/transports/core/statelessTransportManager.d.ts +20 -0
  34. package/dist/mcp-server/transports/core/statelessTransportManager.js +92 -0
  35. package/dist/mcp-server/transports/core/transportTypes.d.ts +68 -0
  36. package/dist/mcp-server/transports/core/transportTypes.js +5 -0
  37. package/dist/mcp-server/transports/{httpErrorHandler.d.ts → http/httpErrorHandler.d.ts} +4 -9
  38. package/dist/mcp-server/transports/{httpErrorHandler.js → http/httpErrorHandler.js} +33 -8
  39. package/dist/mcp-server/transports/http/httpTransport.d.ts +22 -0
  40. package/dist/mcp-server/transports/http/httpTransport.js +251 -0
  41. package/dist/mcp-server/transports/http/httpTypes.d.ts +16 -0
  42. package/dist/mcp-server/transports/http/httpTypes.js +5 -0
  43. package/dist/mcp-server/transports/http/index.d.ts +7 -0
  44. package/dist/mcp-server/transports/http/index.js +6 -0
  45. package/dist/mcp-server/transports/http/mcpTransportMiddleware.d.ts +25 -0
  46. package/dist/mcp-server/transports/http/mcpTransportMiddleware.js +63 -0
  47. package/dist/mcp-server/transports/stdio/index.d.ts +5 -0
  48. package/dist/mcp-server/transports/stdio/index.js +5 -0
  49. package/dist/mcp-server/transports/{stdioTransport.d.ts → stdio/stdioTransport.d.ts} +2 -2
  50. package/dist/mcp-server/transports/{stdioTransport.js → stdio/stdioTransport.js} +10 -5
  51. package/dist/services/NCBI/{ncbiConstants.d.ts → core/ncbiConstants.d.ts} +1 -1
  52. package/dist/services/NCBI/{ncbiConstants.js → core/ncbiConstants.js} +1 -1
  53. package/dist/services/NCBI/{ncbiCoreApiClient.d.ts → core/ncbiCoreApiClient.d.ts} +3 -3
  54. package/dist/services/NCBI/core/ncbiCoreApiClient.js +117 -0
  55. package/dist/services/NCBI/{ncbiRequestQueueManager.d.ts → core/ncbiRequestQueueManager.d.ts} +2 -2
  56. package/dist/services/NCBI/{ncbiRequestQueueManager.js → core/ncbiRequestQueueManager.js} +3 -3
  57. package/dist/services/NCBI/{ncbiResponseHandler.d.ts → core/ncbiResponseHandler.d.ts} +2 -2
  58. package/dist/services/NCBI/{ncbiResponseHandler.js → core/ncbiResponseHandler.js} +3 -3
  59. package/dist/services/NCBI/{ncbiService.d.ts → core/ncbiService.d.ts} +5 -4
  60. package/dist/services/NCBI/{ncbiService.js → core/ncbiService.js} +15 -3
  61. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.d.ts +1 -1
  62. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.js +1 -1
  63. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.d.ts +1 -1
  64. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.js +1 -1
  65. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.d.ts +1 -1
  66. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.js +5 -1
  67. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.d.ts +1 -1
  68. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.js +1 -1
  69. package/dist/types-global/errors.d.ts +2 -0
  70. package/dist/types-global/errors.js +2 -0
  71. package/dist/types-global/pubmedXml.d.ts +15 -0
  72. package/dist/utils/internal/errorHandler.js +1 -1
  73. package/dist/utils/internal/logger.d.ts +13 -1
  74. package/dist/utils/internal/logger.js +43 -9
  75. package/dist/utils/network/fetchWithTimeout.d.ts +21 -0
  76. package/dist/utils/network/fetchWithTimeout.js +59 -0
  77. package/dist/utils/network/index.d.ts +6 -0
  78. package/dist/utils/network/index.js +5 -0
  79. package/dist/utils/scheduling/index.d.ts +6 -0
  80. package/dist/utils/scheduling/index.js +6 -0
  81. package/dist/utils/scheduling/scheduler.d.ts +72 -0
  82. package/dist/utils/scheduling/scheduler.js +150 -0
  83. package/dist/utils/security/sanitization.js +35 -18
  84. package/package.json +17 -13
  85. package/dist/mcp-server/transports/auth/core/authTypes.js +0 -5
  86. package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.d.ts +0 -27
  87. package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +0 -149
  88. package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.d.ts +0 -20
  89. package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +0 -124
  90. package/dist/mcp-server/transports/httpTransport.d.ts +0 -21
  91. package/dist/mcp-server/transports/httpTransport.js +0 -208
  92. package/dist/services/NCBI/ncbiCoreApiClient.js +0 -113
  93. /package/dist/mcp-server/transports/auth/{core → lib}/authContext.d.ts +0 -0
  94. /package/dist/mcp-server/transports/auth/{core → lib}/authContext.js +0 -0
  95. /package/dist/mcp-server/transports/auth/{core → lib}/authUtils.d.ts +0 -0
@@ -1,21 +0,0 @@
1
- /**
2
- * @fileoverview Configures and starts the Streamable HTTP MCP transport using Hono.
3
- * This module integrates the `@modelcontextprotocol/sdk`'s `StreamableHTTPServerTransport`
4
- * into a Hono web server. Its responsibilities include:
5
- * - Creating a Hono server instance.
6
- * - Applying and configuring middleware for CORS, rate limiting, and authentication (JWT/OAuth).
7
- * - Defining the routes (`/mcp` endpoint for POST, GET, DELETE) to handle the MCP lifecycle.
8
- * - Orchestrating session management by mapping session IDs to SDK transport instances.
9
- * - Implementing port-binding logic with automatic retry on conflicts.
10
- *
11
- * The underlying implementation of the MCP Streamable HTTP specification, including
12
- * Server-Sent Events (SSE) for streaming, is handled by the SDK's transport class.
13
- *
14
- * Specification Reference:
15
- * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
16
- * @module src/mcp-server/transports/httpTransport
17
- */
18
- import { ServerType } from "@hono/node-server";
19
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
20
- import { RequestContext } from "../../utils/index.js";
21
- export declare function startHttpTransport(createServerInstanceFn: () => Promise<McpServer>, parentContext: RequestContext): Promise<ServerType>;
@@ -1,208 +0,0 @@
1
- /**
2
- * @fileoverview Configures and starts the Streamable HTTP MCP transport using Hono.
3
- * This module integrates the `@modelcontextprotocol/sdk`'s `StreamableHTTPServerTransport`
4
- * into a Hono web server. Its responsibilities include:
5
- * - Creating a Hono server instance.
6
- * - Applying and configuring middleware for CORS, rate limiting, and authentication (JWT/OAuth).
7
- * - Defining the routes (`/mcp` endpoint for POST, GET, DELETE) to handle the MCP lifecycle.
8
- * - Orchestrating session management by mapping session IDs to SDK transport instances.
9
- * - Implementing port-binding logic with automatic retry on conflicts.
10
- *
11
- * The underlying implementation of the MCP Streamable HTTP specification, including
12
- * Server-Sent Events (SSE) for streaming, is handled by the SDK's transport class.
13
- *
14
- * Specification Reference:
15
- * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
16
- * @module src/mcp-server/transports/httpTransport
17
- */
18
- import { serve } from "@hono/node-server";
19
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
20
- import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
21
- import { Hono } from "hono";
22
- import { cors } from "hono/cors";
23
- import http from "http";
24
- import { randomUUID } from "node:crypto";
25
- import { config } from "../../config/index.js";
26
- import { BaseErrorCode, McpError } from "../../types-global/errors.js";
27
- import { logger, rateLimiter, requestContextService, } from "../../utils/index.js";
28
- import { jwtAuthMiddleware, oauthMiddleware, } from "./auth/index.js";
29
- import { httpErrorHandler } from "./httpErrorHandler.js";
30
- const HTTP_PORT = config.mcpHttpPort;
31
- const HTTP_HOST = config.mcpHttpHost;
32
- const MCP_ENDPOINT_PATH = "/mcp";
33
- const MAX_PORT_RETRIES = 15;
34
- // The transports map will store active sessions, keyed by session ID.
35
- // NOTE: This is an in-memory session store, which is a known limitation for scalability.
36
- // It will not work in a multi-process (clustered) or serverless environment.
37
- // For a scalable deployment, this would need to be replaced with a distributed
38
- // store like Redis or Memcached.
39
- const transports = {};
40
- async function isPortInUse(port, host, parentContext) {
41
- const checkContext = requestContextService.createRequestContext({
42
- ...parentContext,
43
- operation: "isPortInUse",
44
- port,
45
- host,
46
- });
47
- return new Promise((resolve) => {
48
- const tempServer = http.createServer();
49
- tempServer
50
- .once("error", (err) => {
51
- resolve(err.code === "EADDRINUSE");
52
- })
53
- .once("listening", () => {
54
- tempServer.close(() => resolve(false));
55
- })
56
- .listen(port, host);
57
- });
58
- }
59
- function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentContext) {
60
- const startContext = requestContextService.createRequestContext({
61
- ...parentContext,
62
- operation: "startHttpServerWithRetry",
63
- });
64
- return new Promise(async (resolve, reject) => {
65
- for (let i = 0; i <= maxRetries; i++) {
66
- const currentPort = initialPort + i;
67
- const attemptContext = {
68
- ...startContext,
69
- port: currentPort,
70
- attempt: i + 1,
71
- };
72
- if (await isPortInUse(currentPort, host, attemptContext)) {
73
- logger.warning(`Port ${currentPort} is in use, retrying...`, attemptContext);
74
- continue;
75
- }
76
- try {
77
- const serverInstance = serve({ fetch: app.fetch, port: currentPort, hostname: host }, (info) => {
78
- const serverAddress = `http://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
79
- logger.info(`HTTP transport listening at ${serverAddress}`, {
80
- ...attemptContext,
81
- address: serverAddress,
82
- });
83
- if (process.stdout.isTTY) {
84
- console.log(`\n🚀 MCP Server running at: ${serverAddress}\n`);
85
- }
86
- });
87
- resolve(serverInstance);
88
- return;
89
- }
90
- catch (err) {
91
- if (err.code !== "EADDRINUSE") {
92
- reject(err);
93
- return;
94
- }
95
- }
96
- }
97
- reject(new Error("Failed to bind to any port after multiple retries."));
98
- });
99
- }
100
- export async function startHttpTransport(createServerInstanceFn, parentContext) {
101
- const app = new Hono();
102
- const transportContext = requestContextService.createRequestContext({
103
- ...parentContext,
104
- component: "HttpTransportSetup",
105
- });
106
- app.use("*", cors({
107
- origin: config.mcpAllowedOrigins || [],
108
- allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
109
- allowHeaders: [
110
- "Content-Type",
111
- "Mcp-Session-Id",
112
- "Last-Event-ID",
113
- "Authorization",
114
- ],
115
- credentials: true,
116
- }));
117
- app.use("*", async (c, next) => {
118
- c.res.headers.set("X-Content-Type-Options", "nosniff");
119
- await next();
120
- });
121
- app.use(MCP_ENDPOINT_PATH, async (c, next) => {
122
- // NOTE (Security): The 'x-forwarded-for' header is used for rate limiting.
123
- // This is only secure if the server is run behind a trusted proxy that
124
- // correctly sets or validates this header.
125
- const clientIp = c.req.header("x-forwarded-for")?.split(",")[0].trim() || "unknown_ip";
126
- const context = requestContextService.createRequestContext({
127
- operation: "httpRateLimitCheck",
128
- ipAddress: clientIp,
129
- });
130
- // Let the centralized error handler catch rate limit errors
131
- rateLimiter.check(clientIp, context);
132
- await next();
133
- });
134
- if (config.mcpAuthMode === "oauth") {
135
- app.use(MCP_ENDPOINT_PATH, oauthMiddleware);
136
- }
137
- else {
138
- app.use(MCP_ENDPOINT_PATH, jwtAuthMiddleware);
139
- }
140
- // Centralized Error Handling
141
- app.onError(httpErrorHandler);
142
- app.post(MCP_ENDPOINT_PATH, async (c) => {
143
- const postContext = requestContextService.createRequestContext({
144
- ...transportContext,
145
- operation: "handlePost",
146
- });
147
- const body = await c.req.json();
148
- const sessionId = c.req.header("mcp-session-id");
149
- let transport = sessionId
150
- ? transports[sessionId]
151
- : undefined;
152
- if (isInitializeRequest(body)) {
153
- // If a transport already exists for a session, it's a re-initialization.
154
- if (transport) {
155
- logger.warning("Re-initializing existing session.", {
156
- ...postContext,
157
- sessionId,
158
- });
159
- await transport.close(); // This will trigger the onclose handler.
160
- }
161
- // Create a new transport for a new session.
162
- const newTransport = new StreamableHTTPServerTransport({
163
- sessionIdGenerator: () => randomUUID(),
164
- onsessioninitialized: (newId) => {
165
- transports[newId] = newTransport;
166
- logger.info(`HTTP Session created: ${newId}`, {
167
- ...postContext,
168
- newSessionId: newId,
169
- });
170
- },
171
- });
172
- // Set up cleanup logic for when the transport is closed.
173
- newTransport.onclose = () => {
174
- const closedSessionId = newTransport.sessionId;
175
- if (closedSessionId && transports[closedSessionId]) {
176
- delete transports[closedSessionId];
177
- logger.info(`HTTP Session closed: ${closedSessionId}`, {
178
- ...postContext,
179
- closedSessionId,
180
- });
181
- }
182
- };
183
- // Connect the new transport to a new server instance.
184
- const server = await createServerInstanceFn();
185
- await server.connect(newTransport);
186
- transport = newTransport;
187
- }
188
- else if (!transport) {
189
- // If it's not an initialization request and no transport was found, it's an error.
190
- throw new McpError(BaseErrorCode.NOT_FOUND, "Invalid or expired session ID.");
191
- }
192
- // Pass the request to the transport to handle.
193
- return await transport.handleRequest(c.env.incoming, c.env.outgoing, body);
194
- });
195
- // A reusable handler for GET and DELETE requests which operate on existing sessions.
196
- const handleSessionRequest = async (c) => {
197
- const sessionId = c.req.header("mcp-session-id");
198
- const transport = sessionId ? transports[sessionId] : undefined;
199
- if (!transport) {
200
- throw new McpError(BaseErrorCode.NOT_FOUND, "Session not found or expired.");
201
- }
202
- // Let the transport handle the streaming (GET) or termination (DELETE) request.
203
- return await transport.handleRequest(c.env.incoming, c.env.outgoing);
204
- };
205
- app.get(MCP_ENDPOINT_PATH, handleSessionRequest);
206
- app.delete(MCP_ENDPOINT_PATH, handleSessionRequest);
207
- return startHttpServerWithRetry(app, HTTP_PORT, HTTP_HOST, MAX_PORT_RETRIES, transportContext);
208
- }
@@ -1,113 +0,0 @@
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 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 = {}, retries = 0) {
29
- const rawParams = {
30
- tool: config.ncbiToolIdentifier,
31
- email: config.ncbiAdminEmail,
32
- api_key: config.ncbiApiKey,
33
- ...params,
34
- };
35
- // Filter out undefined/null values and convert others to string for URLSearchParams/request body
36
- const finalParams = {};
37
- for (const key in rawParams) {
38
- if (Object.prototype.hasOwnProperty.call(rawParams, key)) {
39
- const value = rawParams[key];
40
- if (value !== undefined && value !== null) {
41
- finalParams[key] = String(value);
42
- }
43
- }
44
- }
45
- const requestConfig = {
46
- method: options.usePost ? "POST" : "GET",
47
- url: `${NCBI_EUTILS_BASE_URL}/${endpoint}.fcgi`,
48
- };
49
- if (options.usePost) {
50
- requestConfig.data = new URLSearchParams(finalParams).toString();
51
- requestConfig.headers = {
52
- "Content-Type": "application/x-www-form-urlencoded",
53
- };
54
- }
55
- else {
56
- requestConfig.params = finalParams;
57
- }
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: retries + 1,
66
- }));
67
- const response = await this.axiosInstance(requestConfig);
68
- return response;
69
- }
70
- catch (error) {
71
- if (retries < config.ncbiMaxRetries) {
72
- const retryDelay = Math.pow(2, retries) * 200; // Increased base delay for retries
73
- logger.warning(`NCBI request to ${endpoint} failed. Retrying (${retries + 1}/${config.ncbiMaxRetries}) in ${retryDelay}ms...`, requestContextService.createRequestContext({
74
- ...context,
75
- operation: "NCBI_HttpRequestRetry",
76
- endpoint,
77
- error: error.message,
78
- retryCount: retries + 1,
79
- maxRetries: config.ncbiMaxRetries,
80
- delay: retryDelay,
81
- }));
82
- await new Promise((r) => setTimeout(r, retryDelay));
83
- return this.makeRequest(endpoint, params, context, options, retries + 1);
84
- }
85
- if (axios.isAxiosError(error)) {
86
- logger.error(`Axios error during NCBI request to ${endpoint} after ${retries} retries`, error, requestContextService.createRequestContext({
87
- ...context,
88
- operation: "NCBI_AxiosError",
89
- endpoint,
90
- status: error.response?.status,
91
- responseData: sanitizeInputForLogging(error.response?.data),
92
- }));
93
- throw new McpError(BaseErrorCode.NCBI_SERVICE_UNAVAILABLE, `NCBI request failed: ${error.message}`, {
94
- endpoint,
95
- status: error.response?.status,
96
- details: error.response?.data
97
- ? String(error.response.data).substring(0, 500)
98
- : undefined,
99
- });
100
- }
101
- // If it's already an McpError, rethrow it (could be from a previous stage if this function is used more broadly)
102
- if (error instanceof McpError)
103
- throw error;
104
- logger.error(`Unexpected error during NCBI request to ${endpoint} after ${retries} 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
- }