@cyanheads/pubmed-mcp-server 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +174 -0
  3. package/dist/config/index.d.ts +91 -0
  4. package/dist/config/index.js +324 -0
  5. package/dist/index.d.ts +24 -0
  6. package/dist/index.js +190 -0
  7. package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +79 -0
  8. package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +82 -0
  9. package/dist/mcp-server/resources/echoResource/index.d.ts +13 -0
  10. package/dist/mcp-server/resources/echoResource/index.js +13 -0
  11. package/dist/mcp-server/resources/echoResource/registration.d.ts +30 -0
  12. package/dist/mcp-server/resources/echoResource/registration.js +168 -0
  13. package/dist/mcp-server/server.d.ts +28 -0
  14. package/dist/mcp-server/server.js +159 -0
  15. package/dist/mcp-server/tools/fetchPubMedContent/index.d.ts +6 -0
  16. package/dist/mcp-server/tools/fetchPubMedContent/index.js +6 -0
  17. package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +63 -0
  18. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +518 -0
  19. package/dist/mcp-server/tools/fetchPubMedContent/registration.d.ts +10 -0
  20. package/dist/mcp-server/tools/fetchPubMedContent/registration.js +37 -0
  21. package/dist/mcp-server/tools/getPubMedArticleConnections/index.d.ts +6 -0
  22. package/dist/mcp-server/tools/getPubMedArticleConnections/index.js +6 -0
  23. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.d.ts +9 -0
  24. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +288 -0
  25. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.d.ts +9 -0
  26. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +208 -0
  27. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.d.ts +15 -0
  28. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +82 -0
  29. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.d.ts +27 -0
  30. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.js +5 -0
  31. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +6 -0
  32. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.js +6 -0
  33. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +36 -0
  34. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +76 -0
  35. package/dist/mcp-server/tools/pubmedResearchAgent/index.d.ts +6 -0
  36. package/dist/mcp-server/tools/pubmedResearchAgent/index.js +6 -0
  37. package/dist/mcp-server/tools/pubmedResearchAgent/logic/index.d.ts +7 -0
  38. package/dist/mcp-server/tools/pubmedResearchAgent/logic/index.js +9 -0
  39. package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.d.ts +111 -0
  40. package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.js +149 -0
  41. package/dist/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.d.ts +106 -0
  42. package/dist/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.js +7 -0
  43. package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.d.ts +10 -0
  44. package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.js +251 -0
  45. package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +12 -0
  46. package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +55 -0
  47. package/dist/mcp-server/tools/pubmedResearchAgent/registration.d.ts +10 -0
  48. package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +36 -0
  49. package/dist/mcp-server/tools/searchPubMedArticles/index.d.ts +6 -0
  50. package/dist/mcp-server/tools/searchPubMedArticles/index.js +6 -0
  51. package/dist/mcp-server/tools/searchPubMedArticles/logic.d.ts +61 -0
  52. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +229 -0
  53. package/dist/mcp-server/tools/searchPubMedArticles/registration.d.ts +10 -0
  54. package/dist/mcp-server/tools/searchPubMedArticles/registration.js +38 -0
  55. package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +29 -0
  56. package/dist/mcp-server/transports/authentication/authMiddleware.js +174 -0
  57. package/dist/mcp-server/transports/httpTransport.d.ts +23 -0
  58. package/dist/mcp-server/transports/httpTransport.js +463 -0
  59. package/dist/mcp-server/transports/stdioTransport.d.ts +42 -0
  60. package/dist/mcp-server/transports/stdioTransport.js +63 -0
  61. package/dist/services/NCBI/ncbiConstants.d.ts +22 -0
  62. package/dist/services/NCBI/ncbiConstants.js +5 -0
  63. package/dist/services/NCBI/ncbiCoreApiClient.d.ts +24 -0
  64. package/dist/services/NCBI/ncbiCoreApiClient.js +118 -0
  65. package/dist/services/NCBI/ncbiRequestQueueManager.d.ts +36 -0
  66. package/dist/services/NCBI/ncbiRequestQueueManager.js +96 -0
  67. package/dist/services/NCBI/ncbiResponseHandler.d.ts +22 -0
  68. package/dist/services/NCBI/ncbiResponseHandler.js +192 -0
  69. package/dist/services/NCBI/ncbiService.d.ts +24 -0
  70. package/dist/services/NCBI/ncbiService.js +57 -0
  71. package/dist/services/index.d.ts +7 -0
  72. package/dist/services/index.js +7 -0
  73. package/dist/services/llm-providers/index.d.ts +7 -0
  74. package/dist/services/llm-providers/index.js +7 -0
  75. package/dist/services/llm-providers/llmFactory.d.ts +69 -0
  76. package/dist/services/llm-providers/llmFactory.js +132 -0
  77. package/dist/services/llm-providers/openRouter/index.d.ts +6 -0
  78. package/dist/services/llm-providers/openRouter/index.js +7 -0
  79. package/dist/services/llm-providers/openRouter/openRouterProvider.d.ts +99 -0
  80. package/dist/services/llm-providers/openRouter/openRouterProvider.js +329 -0
  81. package/dist/types-global/errors.d.ts +119 -0
  82. package/dist/types-global/errors.js +119 -0
  83. package/dist/types-global/pubmedXml.d.ts +350 -0
  84. package/dist/types-global/pubmedXml.js +7 -0
  85. package/dist/utils/index.d.ts +10 -0
  86. package/dist/utils/index.js +18 -0
  87. package/dist/utils/internal/errorHandler.d.ts +176 -0
  88. package/dist/utils/internal/errorHandler.js +335 -0
  89. package/dist/utils/internal/index.d.ts +9 -0
  90. package/dist/utils/internal/index.js +9 -0
  91. package/dist/utils/internal/logger.d.ts +141 -0
  92. package/dist/utils/internal/logger.js +426 -0
  93. package/dist/utils/internal/requestContext.d.ts +83 -0
  94. package/dist/utils/internal/requestContext.js +72 -0
  95. package/dist/utils/metrics/index.d.ts +7 -0
  96. package/dist/utils/metrics/index.js +7 -0
  97. package/dist/utils/metrics/tokenCounter.d.ts +35 -0
  98. package/dist/utils/metrics/tokenCounter.js +110 -0
  99. package/dist/utils/parsing/dateParser.d.ts +73 -0
  100. package/dist/utils/parsing/dateParser.js +107 -0
  101. package/dist/utils/parsing/index.d.ts +8 -0
  102. package/dist/utils/parsing/index.js +8 -0
  103. package/dist/utils/parsing/jsonParser.d.ts +82 -0
  104. package/dist/utils/parsing/jsonParser.js +126 -0
  105. package/dist/utils/parsing/ncbi-parsing/eSummaryResultParser.d.ts +32 -0
  106. package/dist/utils/parsing/ncbi-parsing/eSummaryResultParser.js +295 -0
  107. package/dist/utils/parsing/ncbi-parsing/index.d.ts +8 -0
  108. package/dist/utils/parsing/ncbi-parsing/index.js +8 -0
  109. package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.d.ts +70 -0
  110. package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.js +229 -0
  111. package/dist/utils/parsing/ncbi-parsing/xmlGenericHelpers.d.ts +31 -0
  112. package/dist/utils/parsing/ncbi-parsing/xmlGenericHelpers.js +76 -0
  113. package/dist/utils/security/idGenerator.d.ts +134 -0
  114. package/dist/utils/security/idGenerator.js +198 -0
  115. package/dist/utils/security/index.d.ts +9 -0
  116. package/dist/utils/security/index.js +9 -0
  117. package/dist/utils/security/rateLimiter.d.ts +108 -0
  118. package/dist/utils/security/rateLimiter.js +179 -0
  119. package/dist/utils/security/sanitization.d.ts +173 -0
  120. package/dist/utils/security/sanitization.js +439 -0
  121. package/package.json +97 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * @fileoverview Logic for the searchPubMedArticles MCP tool.
3
+ * Handles constructing ESearch and ESummary queries, interacting with
4
+ * the NcbiService, and formatting the results.
5
+ * @module src/mcp-server/tools/searchPubMedArticles/logic
6
+ */
7
+ import { z } from "zod";
8
+ import { ncbiService } from "../../../services/NCBI/ncbiService.js";
9
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
10
+ import { logger, requestContextService, sanitizeInputForLogging, } from "../../../utils/index.js";
11
+ import { extractBriefSummaries } from "../../../utils/parsing/ncbi-parsing/index.js";
12
+ import { sanitization } from "../../../utils/security/sanitization.js";
13
+ export const SearchPubMedArticlesInputSchema = z.object({
14
+ queryTerm: z
15
+ .string()
16
+ .min(3, "Query term must be at least 3 characters")
17
+ .describe("The primary keyword or phrase to search for in PubMed. Must be at least 3 characters long."),
18
+ maxResults: z
19
+ .number()
20
+ .int()
21
+ .positive()
22
+ .max(1000, "Max results per query. ESearch's retmax is used.")
23
+ .optional()
24
+ .default(20)
25
+ .describe("Maximum number of articles to retrieve. Corresponds to ESearch's 'retmax' parameter. Default is 20, max is 1000."),
26
+ sortBy: z
27
+ .enum(["relevance", "pub_date", "author", "journal_name"])
28
+ .optional()
29
+ .default("relevance")
30
+ .describe("Sorting criteria for results. Options: 'relevance' (default), 'pub_date', 'author', 'journal_name'. Note: Other sorting (e.g., last_author, title) may require client-side implementation or be future server enhancements."),
31
+ dateRange: z
32
+ .object({
33
+ minDate: z
34
+ .string()
35
+ .regex(/^\d{4}(\/\d{2}(\/\d{2})?)?$/, "Date must be YYYY, YYYY/MM, or YYYY/MM/DD")
36
+ .optional()
37
+ .describe("The start date for the search range (YYYY, YYYY/MM, or YYYY/MM/DD)."),
38
+ maxDate: z
39
+ .string()
40
+ .regex(/^\d{4}(\/\d{2}(\/\d{2})?)?$/, "Date must be YYYY, YYYY/MM, or YYYY/MM/DD")
41
+ .optional()
42
+ .describe("The end date for the search range (YYYY, YYYY/MM, or YYYY/MM/DD)."),
43
+ dateType: z
44
+ .enum(["pdat", "mdat", "edat"])
45
+ .optional()
46
+ .default("pdat")
47
+ .describe("The type of date to filter by: 'pdat' (Publication Date), 'mdat' (Modification Date), 'edat' (Entrez Date). Default is 'pdat'."),
48
+ })
49
+ .optional()
50
+ .describe("Defines an optional date range for the search, including min/max dates and the type of date field to use."),
51
+ filterByPublicationTypes: z
52
+ .array(z.string())
53
+ .optional()
54
+ .describe('An array of publication types to filter by (e.g., ["Review", "Clinical Trial"]). The server maps these to the appropriate Entrez query syntax (e.g., "Review"[Publication Type]).'),
55
+ fetchBriefSummaries: z
56
+ .number()
57
+ .int()
58
+ .min(0)
59
+ .max(50)
60
+ .optional()
61
+ .default(0)
62
+ .describe("Number of top PMIDs for which to fetch brief summaries using ESummary v2.0. Set to 0 to disable. Maximum is 50 for this tool. Default is 0."),
63
+ });
64
+ /**
65
+ * Logic for the searchPubMedArticles tool.
66
+ * Constructs and executes ESearch and optionally ESummary queries via NcbiService,
67
+ * then formats the results into a CallToolResult.
68
+ * @param input - Validated input arguments for the tool.
69
+ * @param parentRequestContext - The parent request context for logging and correlation.
70
+ * @returns A promise resolving to a CallToolResult.
71
+ */
72
+ export async function searchPubMedArticlesLogic(input, parentRequestContext) {
73
+ const toolLogicContext = requestContextService.createRequestContext({
74
+ parentRequestId: parentRequestContext.requestId,
75
+ operation: "searchPubMedArticlesLogic",
76
+ input: sanitizeInputForLogging(input),
77
+ });
78
+ logger.info("Executing searchPubMedArticles tool", toolLogicContext);
79
+ let effectiveQuery = sanitization.sanitizeString(input.queryTerm, {
80
+ context: "text",
81
+ });
82
+ if (input.dateRange) {
83
+ const { minDate, maxDate, dateType } = input.dateRange;
84
+ if (minDate && maxDate) {
85
+ effectiveQuery += ` AND (${minDate}[${dateType}] : ${maxDate}[${dateType}])`;
86
+ }
87
+ else if (minDate) {
88
+ effectiveQuery += ` AND ${minDate}[${dateType}]`;
89
+ }
90
+ else if (maxDate) {
91
+ effectiveQuery += ` AND ${maxDate}[${dateType}]`;
92
+ }
93
+ }
94
+ if (input.filterByPublicationTypes &&
95
+ input.filterByPublicationTypes.length > 0) {
96
+ const ptQuery = input.filterByPublicationTypes
97
+ .map((pt) => `"${sanitization.sanitizeString(pt, { context: "text" })}"[Publication Type]`)
98
+ .join(" OR ");
99
+ effectiveQuery += ` AND (${ptQuery})`;
100
+ }
101
+ const currentFetchBriefSummaries = input.fetchBriefSummaries ?? 0;
102
+ const eSearchParams = {
103
+ db: "pubmed",
104
+ term: effectiveQuery,
105
+ retmax: input.maxResults,
106
+ sort: input.sortBy,
107
+ usehistory: currentFetchBriefSummaries > 0 ? "y" : "n",
108
+ };
109
+ let eSearchUrl = "";
110
+ let eSummaryUrl = "";
111
+ try {
112
+ const eSearchResponse = await ncbiService.eSearch(eSearchParams, toolLogicContext);
113
+ const eSearchBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi";
114
+ const eSearchQueryStringParams = {};
115
+ for (const key in eSearchParams) {
116
+ if (eSearchParams[key] !== undefined) {
117
+ eSearchQueryStringParams[key] = String(eSearchParams[key]);
118
+ }
119
+ }
120
+ const eSearchQueryString = new URLSearchParams(eSearchQueryStringParams).toString();
121
+ eSearchUrl = `${eSearchBase}?${eSearchQueryString}`;
122
+ if (!eSearchResponse || !eSearchResponse.eSearchResult) {
123
+ throw new McpError(BaseErrorCode.NCBI_PARSING_ERROR, "Invalid or empty ESearch response from NCBI.", {
124
+ responsePreview: sanitizeInputForLogging(JSON.stringify(eSearchResponse).substring(0, 200)),
125
+ requestId: toolLogicContext.requestId,
126
+ });
127
+ }
128
+ const esResult = eSearchResponse.eSearchResult;
129
+ const pmids = esResult.IdList?.Id || [];
130
+ const totalFound = parseInt(esResult.Count || "0", 10);
131
+ const retrievedPmidCount = pmids.length;
132
+ let briefSummaries = [];
133
+ if (currentFetchBriefSummaries > 0 && pmids.length > 0) {
134
+ const eSummaryParams = {
135
+ db: "pubmed",
136
+ version: "2.0",
137
+ retmode: "xml",
138
+ };
139
+ if (esResult.WebEnv && esResult.QueryKey) {
140
+ eSummaryParams.WebEnv = esResult.WebEnv;
141
+ eSummaryParams.query_key = esResult.QueryKey;
142
+ eSummaryParams.retmax = currentFetchBriefSummaries; // Use history with retmax
143
+ }
144
+ else {
145
+ // Fallback to using explicit IDs if history is not available (should not happen if usehistory='y' was successful)
146
+ const pmidsForSummary = pmids
147
+ .slice(0, currentFetchBriefSummaries)
148
+ .join(",");
149
+ eSummaryParams.id = pmidsForSummary;
150
+ }
151
+ const eSummaryBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi";
152
+ const eSummaryQueryStringParams = {};
153
+ for (const key in eSummaryParams) {
154
+ if (eSummaryParams[key] !== undefined) {
155
+ eSummaryQueryStringParams[key] = String(eSummaryParams[key]);
156
+ }
157
+ }
158
+ const eSummaryQueryString = new URLSearchParams(eSummaryQueryStringParams).toString();
159
+ eSummaryUrl = `${eSummaryBase}?${eSummaryQueryString}`;
160
+ const eSummaryResponseXml = await ncbiService.eSummary(eSummaryParams, toolLogicContext);
161
+ logger.debug("Raw ESummary Response (XML parsed by ncbiService):", {
162
+ ...toolLogicContext,
163
+ eSummaryResponse: sanitizeInputForLogging(eSummaryResponseXml),
164
+ });
165
+ if (eSummaryResponseXml && eSummaryResponseXml.eSummaryResult) {
166
+ briefSummaries = await extractBriefSummaries(eSummaryResponseXml.eSummaryResult, toolLogicContext);
167
+ }
168
+ else if (eSummaryResponseXml && eSummaryResponseXml.ERROR) {
169
+ logger.warning("ESummary returned a top-level error", {
170
+ ...toolLogicContext,
171
+ errorDetails: eSummaryResponseXml.ERROR,
172
+ });
173
+ }
174
+ }
175
+ const resultPayload = {
176
+ searchParameters: {
177
+ queryTerm: input.queryTerm,
178
+ maxResults: input.maxResults,
179
+ sortBy: input.sortBy,
180
+ dateRange: input.dateRange,
181
+ filterByPublicationTypes: input.filterByPublicationTypes,
182
+ fetchBriefSummaries: currentFetchBriefSummaries,
183
+ },
184
+ effectiveESearchTerm: effectiveQuery,
185
+ totalFound,
186
+ retrievedPmidCount,
187
+ pmids, // These are the PMIDs from ESearch, limited by input.maxResults
188
+ briefSummaries, // These should now be limited by currentFetchBriefSummaries
189
+ eSearchUrl,
190
+ eSummaryUrl: currentFetchBriefSummaries > 0 && pmids.length > 0
191
+ ? eSummaryUrl
192
+ : undefined,
193
+ };
194
+ return {
195
+ content: [{ type: "text", text: JSON.stringify(resultPayload) }],
196
+ isError: false,
197
+ };
198
+ }
199
+ catch (error) {
200
+ logger.error("Error in searchPubMedArticlesLogic", error, toolLogicContext);
201
+ const mcpError = error instanceof McpError
202
+ ? error
203
+ : new McpError(BaseErrorCode.INTERNAL_ERROR, "Failed to search PubMed articles due to an unexpected error.", {
204
+ originalErrorName: error.name,
205
+ originalErrorMessage: error.message,
206
+ requestId: toolLogicContext.requestId,
207
+ });
208
+ return {
209
+ content: [
210
+ {
211
+ type: "text",
212
+ text: JSON.stringify({
213
+ error: {
214
+ code: mcpError.code,
215
+ message: mcpError.message,
216
+ details: mcpError.details,
217
+ },
218
+ searchParameters: sanitizeInputForLogging(input),
219
+ eSearchUrl,
220
+ eSummaryUrl: (input.fetchBriefSummaries ?? 0) > 0 && eSummaryUrl
221
+ ? eSummaryUrl
222
+ : undefined,
223
+ }),
224
+ },
225
+ ],
226
+ isError: true,
227
+ };
228
+ }
229
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @fileoverview Registration for the searchPubMedArticles MCP tool.
3
+ * @module src/mcp-server/tools/searchPubMedArticles/registration
4
+ */
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ /**
7
+ * Registers the searchPubMedArticles tool with the MCP server.
8
+ * @param server - The McpServer instance.
9
+ */
10
+ export declare function registerSearchPubMedArticlesTool(server: McpServer): void;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @fileoverview Registration for the searchPubMedArticles MCP tool.
3
+ * @module src/mcp-server/tools/searchPubMedArticles/registration
4
+ */
5
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Import McpError
6
+ import { ErrorHandler, requestContextService } from "../../../utils/index.js";
7
+ import { SearchPubMedArticlesInputSchema, searchPubMedArticlesLogic, } from "./logic.js";
8
+ /**
9
+ * Registers the searchPubMedArticles tool with the MCP server.
10
+ * @param server - The McpServer instance.
11
+ */
12
+ export function registerSearchPubMedArticlesTool(server) {
13
+ const operation = "registerSearchPubMedArticlesTool";
14
+ const context = requestContextService.createRequestContext({ operation });
15
+ try {
16
+ server.tool("search_pubmed_articles", "Searches PubMed for articles using a query term and optional filters (max results, sort, date range, publication types). Uses NCBI ESearch to find PMIDs and ESummary (optional) for brief summaries. Returns a JSON object with search parameters, ESearch term, result counts, PMIDs, optional summaries (PMID, title, authors, source, dates), and E-utility URLs.", SearchPubMedArticlesInputSchema.shape, // Pass the .shape for ZodRawShape
17
+ async (input, toolContext) => {
18
+ // Explicitly type input
19
+ const richContext = requestContextService.createRequestContext({
20
+ parentRequestId: context.requestId,
21
+ operation: "searchPubMedArticlesToolHandler",
22
+ mcpToolContext: toolContext, // Include MCP-provided context if any
23
+ });
24
+ return searchPubMedArticlesLogic(input, richContext);
25
+ });
26
+ }
27
+ catch (error) {
28
+ ErrorHandler.handleError(new McpError(// Create an McpError for consistent handling
29
+ BaseErrorCode.INITIALIZATION_FAILED, "Failed to register searchPubMedArticles tool", {
30
+ originalError: error instanceof Error ? error.message : String(error),
31
+ }), {
32
+ operation,
33
+ context,
34
+ errorCode: BaseErrorCode.INITIALIZATION_FAILED, // errorCode is still useful for categorization
35
+ critical: true,
36
+ });
37
+ }
38
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT).
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
+ * (expected to contain `token`, `clientId`, and `scopes`) is attached to `req.auth`.
11
+ * If the token is missing, invalid, or expired, it sends an HTTP 401 Unauthorized response.
12
+ *
13
+ * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
14
+ * @module src/mcp-server/transports/authentication/authMiddleware
15
+ */
16
+ import { NextFunction, Request, Response } from "express";
17
+ import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
18
+ declare global {
19
+ namespace Express {
20
+ interface Request {
21
+ /** Authentication information derived from the JWT, conforming to MCP SDK's AuthInfo. */
22
+ auth?: AuthInfo;
23
+ }
24
+ }
25
+ }
26
+ /**
27
+ * Express middleware for verifying JWT Bearer token authentication.
28
+ */
29
+ export declare function mcpAuthMiddleware(req: Request, res: Response, next: NextFunction): void;
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT).
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
+ * (expected to contain `token`, `clientId`, and `scopes`) is attached to `req.auth`.
11
+ * If the token is missing, invalid, or expired, it sends an HTTP 401 Unauthorized response.
12
+ *
13
+ * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
14
+ * @module src/mcp-server/transports/authentication/authMiddleware
15
+ */
16
+ import jwt from "jsonwebtoken";
17
+ import { config, environment } from "../../../config/index.js";
18
+ import { logger, requestContextService } from "../../../utils/index.js";
19
+ // Startup Validation: Validate secret key presence on module load.
20
+ if (environment === "production" && !config.mcpAuthSecretKey) {
21
+ logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production environment. Authentication cannot proceed securely.");
22
+ throw new Error("MCP_AUTH_SECRET_KEY must be set in production environment for JWT authentication.");
23
+ }
24
+ else if (!config.mcpAuthSecretKey) {
25
+ logger.warning("MCP_AUTH_SECRET_KEY is not set. Authentication middleware will bypass checks (DEVELOPMENT ONLY). This is insecure for production.");
26
+ }
27
+ /**
28
+ * Express middleware for verifying JWT Bearer token authentication.
29
+ */
30
+ export function mcpAuthMiddleware(req, res, next) {
31
+ const context = requestContextService.createRequestContext({
32
+ operation: "mcpAuthMiddleware",
33
+ method: req.method,
34
+ path: req.path,
35
+ });
36
+ logger.debug("Running MCP Authentication Middleware (Bearer Token Validation)...", context);
37
+ // Development Mode Bypass
38
+ if (!config.mcpAuthSecretKey) {
39
+ if (environment !== "production") {
40
+ logger.warning("Bypassing JWT authentication: MCP_AUTH_SECRET_KEY is not set (DEVELOPMENT ONLY).", context);
41
+ // Populate req.auth strictly according to SDK's AuthInfo
42
+ req.auth = {
43
+ token: "dev-mode-placeholder-token",
44
+ clientId: "dev-client-id",
45
+ scopes: ["dev-scope"],
46
+ };
47
+ // Log dev mode details separately, not attaching to req.auth if not part of AuthInfo
48
+ logger.debug("Dev mode auth object created.", {
49
+ ...context,
50
+ authDetails: req.auth,
51
+ });
52
+ return next();
53
+ }
54
+ else {
55
+ logger.error("FATAL: MCP_AUTH_SECRET_KEY is missing in production. Cannot bypass auth.", context);
56
+ res.status(500).json({
57
+ error: "Server configuration error: Authentication key missing.",
58
+ });
59
+ return;
60
+ }
61
+ }
62
+ const authHeader = req.headers.authorization;
63
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
64
+ logger.warning("Authentication failed: Missing or malformed Authorization header (Bearer scheme required).", context);
65
+ res.status(401).json({
66
+ error: "Unauthorized: Missing or invalid authentication token format.",
67
+ });
68
+ return;
69
+ }
70
+ const tokenParts = authHeader.split(" ");
71
+ if (tokenParts.length !== 2 || tokenParts[0] !== "Bearer" || !tokenParts[1]) {
72
+ logger.warning("Authentication failed: Malformed Bearer token.", context);
73
+ res
74
+ .status(401)
75
+ .json({ error: "Unauthorized: Malformed authentication token." });
76
+ return;
77
+ }
78
+ const rawToken = tokenParts[1];
79
+ try {
80
+ const decoded = jwt.verify(rawToken, config.mcpAuthSecretKey);
81
+ if (typeof decoded === "string") {
82
+ logger.warning("Authentication failed: JWT decoded to a string, expected an object payload.", context);
83
+ res
84
+ .status(401)
85
+ .json({ error: "Unauthorized: Invalid token payload format." });
86
+ return;
87
+ }
88
+ // Extract and validate fields for SDK's AuthInfo
89
+ const clientIdFromToken = typeof decoded.cid === "string"
90
+ ? decoded.cid
91
+ : typeof decoded.client_id === "string"
92
+ ? decoded.client_id
93
+ : undefined;
94
+ if (!clientIdFromToken) {
95
+ logger.warning("Authentication failed: JWT 'cid' or 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
96
+ res.status(401).json({
97
+ error: "Unauthorized: Invalid token, missing client identifier.",
98
+ });
99
+ return;
100
+ }
101
+ let scopesFromToken;
102
+ if (Array.isArray(decoded.scp) &&
103
+ decoded.scp.every((s) => typeof s === "string")) {
104
+ scopesFromToken = decoded.scp;
105
+ }
106
+ else if (typeof decoded.scope === "string" &&
107
+ decoded.scope.trim() !== "") {
108
+ scopesFromToken = decoded.scope.split(" ").filter((s) => s);
109
+ if (scopesFromToken.length === 0 && decoded.scope.trim() !== "") {
110
+ // handles case " " -> [""]
111
+ scopesFromToken = [decoded.scope.trim()];
112
+ }
113
+ else if (scopesFromToken.length === 0 && decoded.scope.trim() === "") {
114
+ // If scope is an empty string, treat as no scopes rather than erroring, or use a default.
115
+ // Depending on strictness, could also error here. For now, allow empty array if scope was empty string.
116
+ logger.debug("JWT 'scope' claim was an empty string, resulting in empty scopes array.", context);
117
+ }
118
+ }
119
+ else {
120
+ // If scopes are strictly mandatory and not found or invalid format
121
+ logger.warning("Authentication failed: JWT 'scp' or 'scope' claim is missing, not an array of strings, or not a valid space-separated string. Assigning default empty array of scopes.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
122
+ // Default to empty array if scopes are not found or are in an invalid format.
123
+ // IMPORTANT: Downstream authorization logic MUST be aware of this default.
124
+ // If specific scopes are mandatory for certain operations, that logic needs to check
125
+ // for the presence and validity of required scopes in this `scopesFromToken` array.
126
+ // An empty array here means no specific scopes were granted by this token,
127
+ // which might restrict access depending on the authorization rules.
128
+ scopesFromToken = [];
129
+ // If truly mandatory and must be non-empty for *all* authenticated requests,
130
+ // an alternative would be to reject the token here:
131
+ // res.status(401).json({ error: "Unauthorized: Invalid token, missing or invalid scopes." });
132
+ // return;
133
+ }
134
+ // Construct req.auth with only the properties defined in SDK's AuthInfo
135
+ // All other claims from 'decoded' are not part of req.auth for type safety.
136
+ req.auth = {
137
+ token: rawToken,
138
+ clientId: clientIdFromToken,
139
+ scopes: scopesFromToken,
140
+ };
141
+ // Log separately if other JWT claims like 'sub' (sessionId) are needed for app logic
142
+ const subClaimForLogging = typeof decoded.sub === "string" ? decoded.sub : undefined;
143
+ logger.debug("JWT verified successfully. AuthInfo attached to request.", {
144
+ ...context,
145
+ mcpSessionIdContext: subClaimForLogging,
146
+ clientId: req.auth.clientId,
147
+ scopes: req.auth.scopes,
148
+ });
149
+ next();
150
+ }
151
+ catch (error) {
152
+ let errorMessage = "Invalid token";
153
+ if (error instanceof jwt.TokenExpiredError) {
154
+ errorMessage = "Token expired";
155
+ logger.warning("Authentication failed: Token expired.", {
156
+ ...context,
157
+ expiredAt: error.expiredAt,
158
+ });
159
+ }
160
+ else if (error instanceof jwt.JsonWebTokenError) {
161
+ errorMessage = `Invalid token: ${error.message}`;
162
+ logger.warning(`Authentication failed: ${errorMessage}`, { ...context });
163
+ }
164
+ else if (error instanceof Error) {
165
+ errorMessage = `Verification error: ${error.message}`;
166
+ logger.error("Authentication failed: Unexpected error during token verification.", { ...context, error: error.message });
167
+ }
168
+ else {
169
+ errorMessage = "Unknown verification error";
170
+ logger.error("Authentication failed: Unexpected non-error exception during token verification.", { ...context, error });
171
+ }
172
+ res.status(401).json({ error: `Unauthorized: ${errorMessage}.` });
173
+ }
174
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @fileoverview Handles the setup and management of the Streamable HTTP MCP transport.
3
+ * Implements the MCP Specification 2025-03-26 for Streamable HTTP.
4
+ * This includes creating an Express server, configuring middleware (CORS, Authentication),
5
+ * defining request routing for the single MCP endpoint (POST/GET/DELETE),
6
+ * managing server-side sessions, handling Server-Sent Events (SSE) for streaming,
7
+ * and binding to a network port with retry logic for port conflicts.
8
+ *
9
+ * Specification Reference:
10
+ * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
11
+ * @module src/mcp-server/transports/httpTransport
12
+ */
13
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ import { RequestContext } from "../../utils/index.js";
15
+ /**
16
+ * Sets up and starts the Streamable HTTP transport layer for the MCP server.
17
+ *
18
+ * @param createServerInstanceFn - An asynchronous factory function that returns a new `McpServer` instance.
19
+ * @param parentContext - Logging context from the main server startup process.
20
+ * @returns A promise that resolves when the HTTP server is successfully listening.
21
+ * @throws {Error} If the server fails to start after all port retries.
22
+ */
23
+ export declare function startHttpTransport(createServerInstanceFn: () => Promise<McpServer>, parentContext: RequestContext): Promise<void>;