@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
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @fileoverview Provides a singleton service for scheduling and managing cron jobs.
3
+ * This service wraps the 'node-cron' library to offer a unified interface for
4
+ * defining, starting, stopping, and listing recurring tasks within the application.
5
+ * @module src/utils/scheduling/scheduler
6
+ */
7
+ import cron, { createTask } from "node-cron";
8
+ import { logger } from "../internal/index.js";
9
+ import { requestContextService } from "../internal/requestContext.js";
10
+ /**
11
+ * A singleton service for scheduling and managing cron jobs.
12
+ */
13
+ export class SchedulerService {
14
+ /** @private */
15
+ constructor() {
16
+ this.jobs = new Map();
17
+ logger.info("SchedulerService initialized.", {
18
+ requestId: "scheduler-init",
19
+ timestamp: new Date().toISOString(),
20
+ });
21
+ }
22
+ /**
23
+ * Gets the singleton instance of the SchedulerService.
24
+ * @returns The singleton SchedulerService instance.
25
+ */
26
+ static getInstance() {
27
+ if (!SchedulerService.instance) {
28
+ SchedulerService.instance = new SchedulerService();
29
+ }
30
+ return SchedulerService.instance;
31
+ }
32
+ /**
33
+ * Schedules a new job.
34
+ *
35
+ * @param id - A unique identifier for the job.
36
+ * @param schedule - The cron pattern for the schedule (e.g., '* * * * *').
37
+ * @param taskFunction - The function to execute on schedule. It receives a RequestContext.
38
+ * @param description - A description of the job.
39
+ * @returns The newly created Job object.
40
+ */
41
+ schedule(id, schedule, taskFunction, description) {
42
+ if (this.jobs.has(id)) {
43
+ throw new Error(`Job with ID '${id}' already exists.`);
44
+ }
45
+ if (!cron.validate(schedule)) {
46
+ throw new Error(`Invalid cron schedule: ${schedule}`);
47
+ }
48
+ const task = createTask(schedule, async () => {
49
+ const job = this.jobs.get(id);
50
+ if (job && job.isRunning) {
51
+ logger.warning(`Job '${id}' is already running. Skipping this execution.`, {
52
+ requestId: `job-skip-${id}`,
53
+ timestamp: new Date().toISOString(),
54
+ });
55
+ return;
56
+ }
57
+ if (job) {
58
+ job.isRunning = true;
59
+ }
60
+ const context = requestContextService.createRequestContext({
61
+ jobId: id,
62
+ schedule,
63
+ });
64
+ logger.info(`Starting job '${id}'...`, context);
65
+ try {
66
+ await Promise.resolve(taskFunction(context));
67
+ logger.info(`Job '${id}' completed successfully.`, context);
68
+ }
69
+ catch (error) {
70
+ logger.error(`Job '${id}' failed.`, error, context);
71
+ }
72
+ finally {
73
+ if (job) {
74
+ job.isRunning = false;
75
+ }
76
+ }
77
+ });
78
+ const newJob = {
79
+ id,
80
+ schedule,
81
+ description,
82
+ task,
83
+ isRunning: false,
84
+ };
85
+ this.jobs.set(id, newJob);
86
+ logger.info(`Job '${id}' scheduled: ${description}`, {
87
+ requestId: `job-schedule-${id}`,
88
+ timestamp: new Date().toISOString(),
89
+ });
90
+ return newJob;
91
+ }
92
+ /**
93
+ * Starts a scheduled job.
94
+ * @param id - The ID of the job to start.
95
+ */
96
+ start(id) {
97
+ const job = this.jobs.get(id);
98
+ if (!job) {
99
+ throw new Error(`Job with ID '${id}' not found.`);
100
+ }
101
+ job.task.start();
102
+ logger.info(`Job '${id}' started.`, {
103
+ requestId: `job-start-${id}`,
104
+ timestamp: new Date().toISOString(),
105
+ });
106
+ }
107
+ /**
108
+ * Stops a scheduled job.
109
+ * @param id - The ID of the job to stop.
110
+ */
111
+ stop(id) {
112
+ const job = this.jobs.get(id);
113
+ if (!job) {
114
+ throw new Error(`Job with ID '${id}' not found.`);
115
+ }
116
+ job.task.stop();
117
+ logger.info(`Job '${id}' stopped.`, {
118
+ requestId: `job-stop-${id}`,
119
+ timestamp: new Date().toISOString(),
120
+ });
121
+ }
122
+ /**
123
+ * Removes a job from the scheduler. The job is stopped before being removed.
124
+ * @param id - The ID of the job to remove.
125
+ */
126
+ remove(id) {
127
+ const job = this.jobs.get(id);
128
+ if (!job) {
129
+ throw new Error(`Job with ID '${id}' not found.`);
130
+ }
131
+ job.task.stop();
132
+ this.jobs.delete(id);
133
+ logger.info(`Job '${id}' removed.`, {
134
+ requestId: `job-remove-${id}`,
135
+ timestamp: new Date().toISOString(),
136
+ });
137
+ }
138
+ /**
139
+ * Gets a list of all scheduled jobs.
140
+ * @returns An array of all Job objects.
141
+ */
142
+ listJobs() {
143
+ return Array.from(this.jobs.values());
144
+ }
145
+ }
146
+ /**
147
+ * The singleton instance of the SchedulerService.
148
+ * Use this instance for all job scheduling operations.
149
+ */
150
+ export const schedulerService = SchedulerService.getInstance();
@@ -121,14 +121,25 @@ export class Sanitization {
121
121
  sanitizeHtml(input, config) {
122
122
  if (!input)
123
123
  return "";
124
- const effectiveConfig = { ...this.defaultHtmlSanitizeConfig, ...config };
124
+ const effectiveConfig = {
125
+ allowedTags: config?.allowedTags ?? this.defaultHtmlSanitizeConfig.allowedTags,
126
+ allowedAttributes: config?.allowedAttributes ??
127
+ this.defaultHtmlSanitizeConfig.allowedAttributes,
128
+ transformTags: config?.transformTags, // Can be undefined
129
+ preserveComments: config?.preserveComments ??
130
+ this.defaultHtmlSanitizeConfig.preserveComments,
131
+ };
125
132
  const options = {
126
133
  allowedTags: effectiveConfig.allowedTags,
127
134
  allowedAttributes: effectiveConfig.allowedAttributes,
128
135
  transformTags: effectiveConfig.transformTags,
129
136
  };
130
137
  if (effectiveConfig.preserveComments) {
131
- options.allowedTags = [...(options.allowedTags || []), "!--"];
138
+ // Ensure allowedTags is an array before spreading
139
+ const baseTags = Array.isArray(options.allowedTags)
140
+ ? options.allowedTags
141
+ : [];
142
+ options.allowedTags = [...baseTags, "!--"];
132
143
  }
133
144
  return sanitizeHtml(input, options);
134
145
  }
@@ -144,14 +155,18 @@ export class Sanitization {
144
155
  sanitizeString(input, options = {}) {
145
156
  if (!input)
146
157
  return "";
147
- switch (options.context) {
148
- case "html":
149
- return this.sanitizeHtml(input, {
150
- allowedTags: options.allowedTags,
151
- allowedAttributes: options.allowedAttributes
152
- ? this.convertAttributesFormat(options.allowedAttributes)
153
- : undefined,
154
- });
158
+ const context = options.context ?? "text";
159
+ switch (context) {
160
+ case "html": {
161
+ const config = {};
162
+ if (options.allowedTags) {
163
+ config.allowedTags = options.allowedTags;
164
+ }
165
+ if (options.allowedAttributes) {
166
+ config.allowedAttributes = this.convertAttributesFormat(options.allowedAttributes);
167
+ }
168
+ return this.sanitizeHtml(input, config);
169
+ }
155
170
  case "attribute":
156
171
  return sanitizeHtml(input, { allowedTags: [], allowedAttributes: {} });
157
172
  case "url":
@@ -233,7 +248,6 @@ export class Sanitization {
233
248
  rootDir: options.rootDir ? path.resolve(options.rootDir) : undefined,
234
249
  };
235
250
  let wasAbsoluteInitially = false;
236
- let convertedToRelative = false;
237
251
  try {
238
252
  if (!input || typeof input !== "string")
239
253
  throw new Error("Invalid path input: must be a non-empty string.");
@@ -262,8 +276,7 @@ export class Sanitization {
262
276
  else {
263
277
  if (path.isAbsolute(normalized)) {
264
278
  if (!effectiveOptions.allowAbsolute) {
265
- finalSanitizedPath = normalized.replace(/^(?:[A-Za-z]:)?[/\\]+/, "");
266
- convertedToRelative = true;
279
+ throw new Error("Absolute paths are disallowed by current options.");
267
280
  }
268
281
  else {
269
282
  finalSanitizedPath = normalized;
@@ -355,7 +368,7 @@ export class Sanitization {
355
368
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Invalid number value (NaN or Infinity).", { input });
356
369
  }
357
370
  let clamped = false;
358
- let originalValueForLog = value;
371
+ const originalValueForLog = value;
359
372
  if (min !== undefined && value < min) {
360
373
  value = min;
361
374
  clamped = true;
@@ -399,8 +412,8 @@ export class Sanitization {
399
412
  try {
400
413
  if (!input || typeof input !== "object")
401
414
  return input;
402
- const clonedInput = typeof structuredClone === "function"
403
- ? structuredClone(input)
415
+ const clonedInput = typeof globalThis.structuredClone === "function"
416
+ ? globalThis.structuredClone(input)
404
417
  : JSON.parse(JSON.stringify(input));
405
418
  this.redactSensitiveFields(clonedInput);
406
419
  return clonedInput;
@@ -428,8 +441,12 @@ export class Sanitization {
428
441
  for (const key in obj) {
429
442
  if (Object.prototype.hasOwnProperty.call(obj, key)) {
430
443
  const value = obj[key];
431
- const lowerKey = key.toLowerCase();
432
- const isSensitive = this.sensitiveFields.some((field) => lowerKey.includes(field));
444
+ // Split camelCase and snake_case/kebab-case keys into words
445
+ const keyWords = key
446
+ .replace(/([A-Z])/g, " $1") // Add space before uppercase letters
447
+ .toLowerCase()
448
+ .split(/[\s_-]+/); // Split by space, underscore, or hyphen
449
+ const isSensitive = keyWords.some((word) => this.sensitiveFields.includes(word));
433
450
  if (isSensitive) {
434
451
  obj[key] = "[REDACTED]";
435
452
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/pubmed-mcp-server",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "A Model Context Protocol (MCP) server enabling AI agents to intelligently search, retrieve, and analyze biomedical literature from PubMed via NCBI E-utilities. Built on the mcp-ts-template for robust, production-ready performance.",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -32,31 +32,33 @@
32
32
  "inspector": "mcp-inspector --config mcp.json --server pubmed-mcp-server"
33
33
  },
34
34
  "dependencies": {
35
- "@hono/node-server": "^1.14.4",
36
- "@modelcontextprotocol/sdk": "^1.13.0",
35
+ "@hono/node-server": "^1.15.0",
36
+ "@modelcontextprotocol/sdk": "^1.17.0",
37
37
  "@types/jsonwebtoken": "^9.0.10",
38
- "@types/node": "^24.0.3",
38
+ "@types/node": "^24.1.0",
39
39
  "@types/sanitize-html": "^2.16.0",
40
40
  "@types/validator": "13.15.2",
41
- "axios": "^1.10.0",
41
+ "axios": "^1.11.0",
42
+ "chart.js": "^4.5.0",
43
+ "chartjs-node-canvas": "^5.0.0",
42
44
  "chrono-node": "^2.8.3",
45
+ "citation-js": "^0.7.20",
43
46
  "dotenv": "^16.5.0",
44
47
  "fast-xml-parser": "^5.2.5",
45
- "hono": "^4.8.2",
46
- "jose": "^6.0.11",
48
+ "hono": "^4.8.4",
49
+ "jose": "^6.0.12",
47
50
  "jsonwebtoken": "^9.0.2",
48
- "openai": "^5.6.0",
51
+ "node-cron": "^4.2.1",
52
+ "openai": "^5.10.2",
49
53
  "partial-json": "^0.1.7",
50
54
  "sanitize-html": "^2.17.0",
51
55
  "tiktoken": "^1.0.21",
52
56
  "ts-node": "^10.9.2",
53
57
  "typescript": "^5.8.3",
54
58
  "validator": "13.15.15",
55
- "chart.js": "^4.5.0",
56
- "chartjs-node-canvas": "^5.0.0",
57
59
  "winston": "^3.17.0",
58
60
  "winston-transport": "^4.9.0",
59
- "zod": "^3.25.67"
61
+ "zod": "^3.25.74"
60
62
  },
61
63
  "keywords": [
62
64
  "mcp",
@@ -97,8 +99,10 @@
97
99
  },
98
100
  "devDependencies": {
99
101
  "@types/js-yaml": "^4.0.9",
102
+ "@types/node-cron": "^3.0.11",
100
103
  "js-yaml": "^4.1.0",
101
- "prettier": "^3.5.3",
102
- "typedoc": "^0.28.5"
104
+ "patch-package": "^8.0.0",
105
+ "prettier": "^3.6.2",
106
+ "typedoc": "^0.28.8"
103
107
  }
104
108
  }
@@ -1,5 +0,0 @@
1
- /**
2
- * @fileoverview Shared types for authentication middleware.
3
- * @module src/mcp-server/transports/auth/core/auth.types
4
- */
5
- export {};
@@ -1,27 +0,0 @@
1
- /**
2
- * @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT) for Hono.
3
- *
4
- * This middleware validates JSON Web Tokens (JWT) passed via the 'Authorization' header
5
- * using the 'Bearer' scheme (e.g., "Authorization: Bearer <your_token>").
6
- * It verifies the token's signature and expiration using the secret key defined
7
- * in the configuration (`config.mcpAuthSecretKey`).
8
- *
9
- * If the token is valid, an object conforming to the MCP SDK's `AuthInfo` type
10
- * is attached to `c.env.incoming.auth`. This direct attachment to the raw Node.js
11
- * request object is for compatibility with the underlying SDK transport, which is
12
- * not Hono-context-aware.
13
- * If the token is missing, invalid, or expired, it throws an `McpError`, which is
14
- * then handled by the centralized `httpErrorHandler`.
15
- *
16
- * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
17
- * @module src/mcp-server/transports/auth/strategies/jwt/jwtMiddleware
18
- */
19
- import { HttpBindings } from "@hono/node-server";
20
- import { Context, Next } from "hono";
21
- /**
22
- * Hono middleware for verifying JWT Bearer token authentication.
23
- * It attaches authentication info to `c.env.incoming.auth` for SDK compatibility with the node server.
24
- */
25
- export declare function mcpAuthMiddleware(c: Context<{
26
- Bindings: HttpBindings;
27
- }>, next: Next): Promise<void>;
@@ -1,149 +0,0 @@
1
- /**
2
- * @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT) for Hono.
3
- *
4
- * This middleware validates JSON Web Tokens (JWT) passed via the 'Authorization' header
5
- * using the 'Bearer' scheme (e.g., "Authorization: Bearer <your_token>").
6
- * It verifies the token's signature and expiration using the secret key defined
7
- * in the configuration (`config.mcpAuthSecretKey`).
8
- *
9
- * If the token is valid, an object conforming to the MCP SDK's `AuthInfo` type
10
- * is attached to `c.env.incoming.auth`. This direct attachment to the raw Node.js
11
- * request object is for compatibility with the underlying SDK transport, which is
12
- * not Hono-context-aware.
13
- * If the token is missing, invalid, or expired, it throws an `McpError`, which is
14
- * then handled by the centralized `httpErrorHandler`.
15
- *
16
- * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
17
- * @module src/mcp-server/transports/auth/strategies/jwt/jwtMiddleware
18
- */
19
- import { jwtVerify } from "jose";
20
- import { config, environment } from "../../../../../config/index.js";
21
- import { logger, requestContextService } from "../../../../../utils/index.js";
22
- import { BaseErrorCode, McpError } from "../../../../../types-global/errors.js";
23
- import { authContext } from "../../core/authContext.js";
24
- // Startup Validation: Validate secret key presence on module load.
25
- if (config.mcpAuthMode === "jwt") {
26
- if (environment === "production" && !config.mcpAuthSecretKey) {
27
- logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production environment for JWT auth. Authentication cannot proceed securely.");
28
- throw new Error("MCP_AUTH_SECRET_KEY must be set in production environment for JWT authentication.");
29
- }
30
- else if (!config.mcpAuthSecretKey) {
31
- logger.warning("MCP_AUTH_SECRET_KEY is not set. JWT auth middleware will bypass checks (DEVELOPMENT ONLY). This is insecure for production.");
32
- }
33
- }
34
- /**
35
- * Hono middleware for verifying JWT Bearer token authentication.
36
- * It attaches authentication info to `c.env.incoming.auth` for SDK compatibility with the node server.
37
- */
38
- export async function mcpAuthMiddleware(c, next) {
39
- const context = requestContextService.createRequestContext({
40
- operation: "mcpAuthMiddleware",
41
- method: c.req.method,
42
- path: c.req.path,
43
- });
44
- logger.debug("Running MCP Authentication Middleware (Bearer Token Validation)...", context);
45
- const reqWithAuth = c.env.incoming;
46
- // If JWT auth is not enabled, skip the middleware.
47
- if (config.mcpAuthMode !== "jwt") {
48
- return await next();
49
- }
50
- // Development Mode Bypass
51
- if (!config.mcpAuthSecretKey) {
52
- if (environment !== "production") {
53
- logger.warning("Bypassing JWT authentication: MCP_AUTH_SECRET_KEY is not set (DEVELOPMENT ONLY).", context);
54
- reqWithAuth.auth = {
55
- token: "dev-mode-placeholder-token",
56
- clientId: "dev-client-id",
57
- scopes: ["dev-scope"],
58
- };
59
- const authInfo = reqWithAuth.auth;
60
- logger.debug("Dev mode auth object created.", {
61
- ...context,
62
- authDetails: authInfo,
63
- });
64
- return await authContext.run({ authInfo }, next);
65
- }
66
- else {
67
- logger.error("FATAL: MCP_AUTH_SECRET_KEY is missing in production. Cannot bypass auth.", context);
68
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Server configuration error: Authentication key missing.");
69
- }
70
- }
71
- const secretKey = new TextEncoder().encode(config.mcpAuthSecretKey);
72
- const authHeader = c.req.header("Authorization");
73
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
74
- logger.warning("Authentication failed: Missing or malformed Authorization header (Bearer scheme required).", context);
75
- throw new McpError(BaseErrorCode.UNAUTHORIZED, "Missing or invalid authentication token format.");
76
- }
77
- const tokenParts = authHeader.split(" ");
78
- if (tokenParts.length !== 2 || tokenParts[0] !== "Bearer" || !tokenParts[1]) {
79
- logger.warning("Authentication failed: Malformed Bearer token.", context);
80
- throw new McpError(BaseErrorCode.UNAUTHORIZED, "Malformed authentication token.");
81
- }
82
- const rawToken = tokenParts[1];
83
- try {
84
- const { payload: decoded } = await jwtVerify(rawToken, secretKey);
85
- const clientIdFromToken = typeof decoded.cid === "string"
86
- ? decoded.cid
87
- : typeof decoded.client_id === "string"
88
- ? decoded.client_id
89
- : undefined;
90
- if (!clientIdFromToken) {
91
- logger.warning("Authentication failed: JWT 'cid' or 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
92
- throw new McpError(BaseErrorCode.UNAUTHORIZED, "Invalid token, missing client identifier.");
93
- }
94
- let scopesFromToken = [];
95
- if (Array.isArray(decoded.scp) &&
96
- decoded.scp.every((s) => typeof s === "string")) {
97
- scopesFromToken = decoded.scp;
98
- }
99
- else if (typeof decoded.scope === "string" &&
100
- decoded.scope.trim() !== "") {
101
- scopesFromToken = decoded.scope.split(" ").filter((s) => s);
102
- if (scopesFromToken.length === 0 && decoded.scope.trim() !== "") {
103
- scopesFromToken = [decoded.scope.trim()];
104
- }
105
- }
106
- if (scopesFromToken.length === 0) {
107
- logger.warning("Authentication failed: Token resulted in an empty scope array, and scopes are required.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
108
- throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain valid, non-empty scopes.");
109
- }
110
- reqWithAuth.auth = {
111
- token: rawToken,
112
- clientId: clientIdFromToken,
113
- scopes: scopesFromToken,
114
- };
115
- const subClaimForLogging = typeof decoded.sub === "string" ? decoded.sub : undefined;
116
- const authInfo = reqWithAuth.auth;
117
- logger.debug("JWT verified successfully. AuthInfo attached to request.", {
118
- ...context,
119
- mcpSessionIdContext: subClaimForLogging,
120
- clientId: authInfo.clientId,
121
- scopes: authInfo.scopes,
122
- });
123
- await authContext.run({ authInfo }, next);
124
- }
125
- catch (error) {
126
- let errorMessage = "Invalid token.";
127
- let errorCode = BaseErrorCode.UNAUTHORIZED;
128
- if (error instanceof Error && error.name === "JWTExpired") {
129
- errorMessage = "Token expired.";
130
- logger.warning("Authentication failed: Token expired.", {
131
- ...context,
132
- errorName: error.name,
133
- });
134
- }
135
- else if (error instanceof Error) {
136
- errorMessage = `Invalid token: ${error.message}`;
137
- logger.warning(`Authentication failed: ${errorMessage}`, {
138
- ...context,
139
- errorName: error.name,
140
- });
141
- }
142
- else {
143
- errorMessage = "Unknown verification error.";
144
- errorCode = BaseErrorCode.INTERNAL_ERROR;
145
- logger.error("Authentication failed: Unexpected non-error exception during token verification.", { ...context, error });
146
- }
147
- throw new McpError(errorCode, errorMessage);
148
- }
149
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * @fileoverview Hono middleware for OAuth 2.1 Bearer Token validation.
3
- * This middleware extracts a JWT from the Authorization header, validates it against
4
- * a remote JWKS (JSON Web Key Set), and checks its issuer and audience claims.
5
- * On success, it populates an AuthInfo object and stores it in an AsyncLocalStorage
6
- * context for use in downstream handlers.
7
- *
8
- * @module src/mcp-server/transports/auth/strategies/oauth/oauthMiddleware
9
- */
10
- import { HttpBindings } from "@hono/node-server";
11
- import { Context, Next } from "hono";
12
- /**
13
- * Hono middleware for verifying OAuth 2.1 JWT Bearer tokens.
14
- * It validates the token and uses AsyncLocalStorage to pass auth info.
15
- * @param c - The Hono context object.
16
- * @param next - The function to call to proceed to the next middleware.
17
- */
18
- export declare function oauthMiddleware(c: Context<{
19
- Bindings: HttpBindings;
20
- }>, next: Next): Promise<void>;
@@ -1,124 +0,0 @@
1
- /**
2
- * @fileoverview Hono middleware for OAuth 2.1 Bearer Token validation.
3
- * This middleware extracts a JWT from the Authorization header, validates it against
4
- * a remote JWKS (JSON Web Key Set), and checks its issuer and audience claims.
5
- * On success, it populates an AuthInfo object and stores it in an AsyncLocalStorage
6
- * context for use in downstream handlers.
7
- *
8
- * @module src/mcp-server/transports/auth/strategies/oauth/oauthMiddleware
9
- */
10
- import { createRemoteJWKSet, jwtVerify } from "jose";
11
- import { config } from "../../../../../config/index.js";
12
- import { BaseErrorCode, McpError } from "../../../../../types-global/errors.js";
13
- import { logger, requestContextService } from "../../../../../utils/index.js";
14
- import { ErrorHandler } from "../../../../../utils/internal/errorHandler.js";
15
- import { authContext } from "../../core/authContext.js";
16
- // --- Startup Validation ---
17
- // Ensures that necessary OAuth configuration is present when the mode is 'oauth'.
18
- if (config.mcpAuthMode === "oauth") {
19
- if (!config.oauthIssuerUrl) {
20
- throw new Error("OAUTH_ISSUER_URL must be set when MCP_AUTH_MODE is 'oauth'");
21
- }
22
- if (!config.oauthAudience) {
23
- throw new Error("OAUTH_AUDIENCE must be set when MCP_AUTH_MODE is 'oauth'");
24
- }
25
- logger.info("OAuth 2.1 mode enabled. Verifying tokens against issuer.", requestContextService.createRequestContext({
26
- issuer: config.oauthIssuerUrl,
27
- audience: config.oauthAudience,
28
- }));
29
- }
30
- // --- JWKS Client Initialization ---
31
- // The remote JWK set is fetched and cached to avoid network calls on every request.
32
- let jwks;
33
- if (config.mcpAuthMode === "oauth" && config.oauthIssuerUrl) {
34
- try {
35
- const jwksUrl = new URL(config.oauthJwksUri ||
36
- `${config.oauthIssuerUrl.replace(/\/$/, "")}/.well-known/jwks.json`);
37
- jwks = createRemoteJWKSet(jwksUrl, {
38
- cooldownDuration: 300000, // 5 minutes
39
- timeoutDuration: 5000, // 5 seconds
40
- });
41
- logger.info(`JWKS client initialized for URL: ${jwksUrl.href}`, requestContextService.createRequestContext({
42
- operation: "oauthMiddlewareSetup",
43
- }));
44
- }
45
- catch (error) {
46
- logger.fatal("Failed to initialize JWKS client.", error, requestContextService.createRequestContext({
47
- operation: "oauthMiddlewareSetup",
48
- }));
49
- // Prevent server from starting if JWKS setup fails in oauth mode
50
- process.exit(1);
51
- }
52
- }
53
- /**
54
- * Hono middleware for verifying OAuth 2.1 JWT Bearer tokens.
55
- * It validates the token and uses AsyncLocalStorage to pass auth info.
56
- * @param c - The Hono context object.
57
- * @param next - The function to call to proceed to the next middleware.
58
- */
59
- export async function oauthMiddleware(c, next) {
60
- // If OAuth is not the configured auth mode, skip this middleware.
61
- if (config.mcpAuthMode !== "oauth") {
62
- return await next();
63
- }
64
- const context = requestContextService.createRequestContext({
65
- operation: "oauthMiddleware",
66
- httpMethod: c.req.method,
67
- httpPath: c.req.path,
68
- });
69
- if (!jwks) {
70
- // This should not happen if startup validation is correct, but it's a safeguard.
71
- // This should not happen if startup validation is correct, but it's a safeguard.
72
- throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, "OAuth middleware is active, but JWKS client is not initialized.", context);
73
- }
74
- const authHeader = c.req.header("Authorization");
75
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
76
- throw new McpError(BaseErrorCode.UNAUTHORIZED, "Missing or invalid token format.");
77
- }
78
- const token = authHeader.substring(7);
79
- try {
80
- const { payload } = await jwtVerify(token, jwks, {
81
- issuer: config.oauthIssuerUrl,
82
- audience: config.oauthAudience,
83
- });
84
- // The 'scope' claim is typically a space-delimited string in OAuth 2.1.
85
- const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : [];
86
- if (scopes.length === 0) {
87
- logger.warning("Authentication failed: Token contains no scopes, but scopes are required.", { ...context, jwtPayloadKeys: Object.keys(payload) });
88
- throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain valid, non-empty scopes.");
89
- }
90
- const clientId = typeof payload.client_id === "string" ? payload.client_id : undefined;
91
- if (!clientId) {
92
- logger.warning("Authentication failed: OAuth token 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(payload) });
93
- throw new McpError(BaseErrorCode.UNAUTHORIZED, "Invalid token, missing client identifier.");
94
- }
95
- const authInfo = {
96
- token,
97
- clientId,
98
- scopes,
99
- subject: typeof payload.sub === "string" ? payload.sub : undefined,
100
- };
101
- // Attach to the raw request for potential legacy compatibility and
102
- // store in AsyncLocalStorage for modern, safe access in handlers.
103
- c.env.incoming.auth = authInfo;
104
- await authContext.run({ authInfo }, next);
105
- }
106
- catch (error) {
107
- if (error instanceof Error && error.name === "JWTExpired") {
108
- logger.warning("Authentication failed: OAuth token expired.", context);
109
- throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token expired.");
110
- }
111
- const handledError = ErrorHandler.handleError(error, {
112
- operation: "oauthMiddleware",
113
- context,
114
- rethrow: false, // We will throw a new McpError below
115
- });
116
- // Ensure we always throw an McpError for consistency
117
- if (handledError instanceof McpError) {
118
- throw handledError;
119
- }
120
- else {
121
- throw new McpError(BaseErrorCode.UNAUTHORIZED, `Unauthorized: ${handledError.message || "Invalid token"}`, { originalError: handledError.name });
122
- }
123
- }
124
- }