@cyanheads/pubmed-mcp-server 1.1.4 → 1.2.2

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 (47) hide show
  1. package/README.md +10 -12
  2. package/dist/mcp-server/server.d.ts +0 -7
  3. package/dist/mcp-server/server.js +17 -53
  4. package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +6 -2
  5. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +94 -202
  6. package/dist/mcp-server/tools/fetchPubMedContent/registration.js +43 -7
  7. package/dist/mcp-server/tools/generatePubMedChart/logic.d.ts +9 -28
  8. package/dist/mcp-server/tools/generatePubMedChart/logic.js +133 -192
  9. package/dist/mcp-server/tools/generatePubMedChart/registration.js +57 -13
  10. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.d.ts +1 -1
  11. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.d.ts +1 -1
  12. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.d.ts +27 -4
  13. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +50 -45
  14. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.d.ts +1 -1
  15. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +0 -24
  16. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +51 -45
  17. package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +2 -4
  18. package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +7 -41
  19. package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +49 -8
  20. package/dist/mcp-server/tools/searchPubMedArticles/logic.d.ts +12 -10
  21. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +68 -126
  22. package/dist/mcp-server/tools/searchPubMedArticles/registration.js +46 -6
  23. package/dist/mcp-server/transports/{authentication → auth/core}/authContext.d.ts +2 -2
  24. package/dist/mcp-server/transports/{authentication → auth/core}/authContext.js +1 -1
  25. package/dist/mcp-server/transports/{authentication/types.d.ts → auth/core/authTypes.d.ts} +1 -1
  26. package/dist/mcp-server/transports/{authentication/types.js → auth/core/authTypes.js} +1 -1
  27. package/dist/mcp-server/transports/{authentication → auth/core}/authUtils.d.ts +1 -1
  28. package/dist/mcp-server/transports/{authentication → auth/core}/authUtils.js +3 -3
  29. package/dist/mcp-server/transports/auth/index.d.ts +10 -0
  30. package/dist/mcp-server/transports/auth/index.js +9 -0
  31. package/dist/mcp-server/transports/{authentication/authMiddleware.d.ts → auth/strategies/jwt/jwtMiddleware.d.ts} +4 -12
  32. package/dist/mcp-server/transports/{authentication/authMiddleware.js → auth/strategies/jwt/jwtMiddleware.js} +36 -43
  33. package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.d.ts +2 -6
  34. package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.js +33 -18
  35. package/dist/mcp-server/transports/httpErrorHandler.d.ts +26 -0
  36. package/dist/mcp-server/transports/httpErrorHandler.js +73 -0
  37. package/dist/mcp-server/transports/httpTransport.d.ts +11 -14
  38. package/dist/mcp-server/transports/httpTransport.js +91 -389
  39. package/package.json +13 -21
  40. package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +0 -79
  41. package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +0 -82
  42. package/dist/mcp-server/resources/echoResource/index.d.ts +0 -13
  43. package/dist/mcp-server/resources/echoResource/index.js +0 -13
  44. package/dist/mcp-server/resources/echoResource/registration.d.ts +0 -30
  45. package/dist/mcp-server/resources/echoResource/registration.js +0 -168
  46. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +0 -6
  47. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.js +0 -6
@@ -61,14 +61,6 @@ export const SearchPubMedArticlesInputSchema = z.object({
61
61
  .default(0)
62
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
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
64
  export async function searchPubMedArticlesLogic(input, parentRequestContext) {
73
65
  const ncbiService = getNcbiService();
74
66
  const toolLogicContext = requestContextService.createRequestContext({
@@ -107,129 +99,79 @@ export async function searchPubMedArticlesLogic(input, parentRequestContext) {
107
99
  sort: input.sortBy,
108
100
  usehistory: currentFetchBriefSummaries > 0 ? "y" : "n",
109
101
  };
110
- let eSearchUrl = "";
111
- let eSummaryUrl = "";
112
- try {
113
- const eSearchResponse = await ncbiService.eSearch(eSearchParams, toolLogicContext);
114
- const eSearchBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi";
115
- const eSearchQueryStringParams = {};
116
- for (const key in eSearchParams) {
117
- if (eSearchParams[key] !== undefined) {
118
- eSearchQueryStringParams[key] = String(eSearchParams[key]);
119
- }
102
+ const eSearchResponse = await ncbiService.eSearch(eSearchParams, toolLogicContext);
103
+ const eSearchBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi";
104
+ const eSearchQueryStringParams = {};
105
+ for (const key in eSearchParams) {
106
+ if (eSearchParams[key] !== undefined) {
107
+ eSearchQueryStringParams[key] = String(eSearchParams[key]);
120
108
  }
121
- const eSearchQueryString = new URLSearchParams(eSearchQueryStringParams).toString();
122
- eSearchUrl = `${eSearchBase}?${eSearchQueryString}`;
123
- if (!eSearchResponse || !eSearchResponse.eSearchResult) {
124
- throw new McpError(BaseErrorCode.NCBI_PARSING_ERROR, "Invalid or empty ESearch response from NCBI.", {
125
- responsePreview: sanitizeInputForLogging(JSON.stringify(eSearchResponse).substring(0, 200)),
126
- requestId: toolLogicContext.requestId,
127
- });
109
+ }
110
+ const eSearchQueryString = new URLSearchParams(eSearchQueryStringParams).toString();
111
+ const eSearchUrl = `${eSearchBase}?${eSearchQueryString}`;
112
+ if (!eSearchResponse || !eSearchResponse.eSearchResult) {
113
+ throw new McpError(BaseErrorCode.NCBI_PARSING_ERROR, "Invalid or empty ESearch response from NCBI.", {
114
+ ...toolLogicContext,
115
+ responsePreview: sanitizeInputForLogging(JSON.stringify(eSearchResponse).substring(0, 200)),
116
+ });
117
+ }
118
+ const esResult = eSearchResponse.eSearchResult;
119
+ const pmids = esResult.IdList?.Id || [];
120
+ const totalFound = parseInt(esResult.Count || "0", 10);
121
+ const retrievedPmidCount = pmids.length;
122
+ let briefSummaries = [];
123
+ let eSummaryUrl;
124
+ if (currentFetchBriefSummaries > 0 && pmids.length > 0) {
125
+ const eSummaryParams = {
126
+ db: "pubmed",
127
+ version: "2.0",
128
+ retmode: "xml",
129
+ };
130
+ if (esResult.WebEnv && esResult.QueryKey) {
131
+ eSummaryParams.WebEnv = esResult.WebEnv;
132
+ eSummaryParams.query_key = esResult.QueryKey;
133
+ eSummaryParams.retmax = currentFetchBriefSummaries;
128
134
  }
129
- const esResult = eSearchResponse.eSearchResult;
130
- const pmids = esResult.IdList?.Id || [];
131
- const totalFound = parseInt(esResult.Count || "0", 10);
132
- const retrievedPmidCount = pmids.length;
133
- let briefSummaries = [];
134
- if (currentFetchBriefSummaries > 0 && pmids.length > 0) {
135
- const eSummaryParams = {
136
- db: "pubmed",
137
- version: "2.0",
138
- retmode: "xml",
139
- };
140
- if (esResult.WebEnv && esResult.QueryKey) {
141
- eSummaryParams.WebEnv = esResult.WebEnv;
142
- eSummaryParams.query_key = esResult.QueryKey;
143
- eSummaryParams.retmax = currentFetchBriefSummaries;
144
- }
145
- else {
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
- }
135
+ else {
136
+ const pmidsForSummary = pmids
137
+ .slice(0, currentFetchBriefSummaries)
138
+ .join(",");
139
+ eSummaryParams.id = pmidsForSummary;
140
+ }
141
+ const eSummaryBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi";
142
+ const eSummaryQueryStringParams = {};
143
+ for (const key in eSummaryParams) {
144
+ if (eSummaryParams[key] !== undefined) {
145
+ eSummaryQueryStringParams[key] = String(eSummaryParams[key]);
157
146
  }
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):", {
147
+ }
148
+ const eSummaryQueryString = new URLSearchParams(eSummaryQueryStringParams).toString();
149
+ eSummaryUrl = `${eSummaryBase}?${eSummaryQueryString}`;
150
+ const eSummaryResponseXml = await ncbiService.eSummary(eSummaryParams, toolLogicContext);
151
+ if (eSummaryResponseXml && eSummaryResponseXml.eSummaryResult) {
152
+ briefSummaries = await extractBriefSummaries(eSummaryResponseXml.eSummaryResult, toolLogicContext);
153
+ }
154
+ else if (eSummaryResponseXml && eSummaryResponseXml.ERROR) {
155
+ logger.warning("ESummary returned a top-level error", {
162
156
  ...toolLogicContext,
163
- eSummaryResponse: sanitizeInputForLogging(eSummaryResponseXml),
157
+ errorDetails: eSummaryResponseXml.ERROR,
164
158
  });
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
159
  }
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,
188
- briefSummaries,
189
- eSearchUrl,
190
- eSummaryUrl: currentFetchBriefSummaries > 0 && pmids.length > 0
191
- ? eSummaryUrl
192
- : undefined,
193
- };
194
- logger.notice("Successfully executed searchPubMedArticles tool.", {
195
- ...toolLogicContext,
196
- totalFound,
197
- retrievedPmidCount,
198
- summariesFetched: briefSummaries.length,
199
- });
200
- return {
201
- content: [{ type: "text", text: JSON.stringify(resultPayload) }],
202
- isError: false,
203
- };
204
- }
205
- catch (error) {
206
- logger.error("Error in searchPubMedArticlesLogic", error, toolLogicContext);
207
- const mcpError = error instanceof McpError
208
- ? error
209
- : new McpError(BaseErrorCode.INTERNAL_ERROR, "Failed to search PubMed articles due to an unexpected error.", {
210
- originalErrorName: error.name,
211
- originalErrorMessage: error.message,
212
- requestId: toolLogicContext.requestId,
213
- });
214
- return {
215
- content: [
216
- {
217
- type: "text",
218
- text: JSON.stringify({
219
- error: {
220
- code: mcpError.code,
221
- message: mcpError.message,
222
- details: mcpError.details,
223
- },
224
- searchParameters: sanitizeInputForLogging(input),
225
- eSearchUrl,
226
- eSummaryUrl: (input.fetchBriefSummaries ?? 0) > 0 && eSummaryUrl
227
- ? eSummaryUrl
228
- : undefined,
229
- }),
230
- },
231
- ],
232
- isError: true,
233
- };
234
160
  }
161
+ logger.notice("Successfully executed searchPubMedArticles tool.", {
162
+ ...toolLogicContext,
163
+ totalFound,
164
+ retrievedPmidCount,
165
+ summariesFetched: briefSummaries.length,
166
+ });
167
+ return {
168
+ searchParameters: input,
169
+ effectiveESearchTerm: effectiveQuery,
170
+ totalFound,
171
+ retrievedPmidCount,
172
+ pmids,
173
+ briefSummaries,
174
+ eSearchUrl,
175
+ eSummaryUrl,
176
+ };
235
177
  }
@@ -2,7 +2,7 @@
2
2
  * @fileoverview Registration for the searchPubMedArticles MCP tool.
3
3
  * @module src/mcp-server/tools/searchPubMedArticles/registration
4
4
  */
5
- import { BaseErrorCode } from "../../../types-global/errors.js";
5
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
6
6
  import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
7
7
  import { SearchPubMedArticlesInputSchema, searchPubMedArticlesLogic, } from "./logic.js";
8
8
  /**
@@ -11,17 +11,57 @@ import { SearchPubMedArticlesInputSchema, searchPubMedArticlesLogic, } from "./l
11
11
  */
12
12
  export async function registerSearchPubMedArticlesTool(server) {
13
13
  const operation = "registerSearchPubMedArticlesTool";
14
+ const toolName = "search_pubmed_articles";
15
+ const toolDescription = "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.";
14
16
  const context = requestContextService.createRequestContext({ operation });
15
- await ErrorHandler.tryCatch(() => {
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, async (input, toolContext) => {
17
+ await ErrorHandler.tryCatch(async () => {
18
+ server.tool(toolName, toolDescription, SearchPubMedArticlesInputSchema.shape, async (input, mcpProvidedContext) => {
17
19
  const richContext = requestContextService.createRequestContext({
18
20
  parentRequestId: context.requestId,
19
21
  operation: "searchPubMedArticlesToolHandler",
20
- mcpToolContext: toolContext,
22
+ mcpToolContext: mcpProvidedContext,
23
+ input,
21
24
  });
22
- return searchPubMedArticlesLogic(input, richContext);
25
+ try {
26
+ const result = await searchPubMedArticlesLogic(input, richContext);
27
+ return {
28
+ content: [
29
+ { type: "text", text: JSON.stringify(result, null, 2) },
30
+ ],
31
+ isError: false,
32
+ };
33
+ }
34
+ catch (error) {
35
+ const handledError = ErrorHandler.handleError(error, {
36
+ operation: "searchPubMedArticlesToolHandler",
37
+ context: richContext,
38
+ input,
39
+ rethrow: false,
40
+ });
41
+ const mcpError = handledError instanceof McpError
42
+ ? handledError
43
+ : new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred while searching PubMed articles.", {
44
+ originalErrorName: handledError.name,
45
+ originalErrorMessage: handledError.message,
46
+ });
47
+ return {
48
+ content: [
49
+ {
50
+ type: "text",
51
+ text: JSON.stringify({
52
+ error: {
53
+ code: mcpError.code,
54
+ message: mcpError.message,
55
+ details: mcpError.details,
56
+ },
57
+ }),
58
+ },
59
+ ],
60
+ isError: true,
61
+ };
62
+ }
23
63
  });
24
- logger.notice("Tool 'search_pubmed_articles' registered.", context);
64
+ logger.notice(`Tool '${toolName}' registered.`, context);
25
65
  }, {
26
66
  operation,
27
67
  context,
@@ -5,10 +5,10 @@
5
5
  * from the middleware layer down to the tool and resource handlers without
6
6
  * drilling props.
7
7
  *
8
- * @module src/mcp-server/transports/authentication/authContext
8
+ * @module src/mcp-server/transports/auth/core/authContext
9
9
  */
10
10
  import { AsyncLocalStorage } from "async_hooks";
11
- import type { AuthInfo } from "./types.js";
11
+ import type { AuthInfo } from "./authTypes.js";
12
12
  /**
13
13
  * Defines the structure of the store used within the AsyncLocalStorage.
14
14
  * It holds the authentication information for the current request context.
@@ -5,7 +5,7 @@
5
5
  * from the middleware layer down to the tool and resource handlers without
6
6
  * drilling props.
7
7
  *
8
- * @module src/mcp-server/transports/authentication/authContext
8
+ * @module src/mcp-server/transports/auth/core/authContext
9
9
  */
10
10
  import { AsyncLocalStorage } from "async_hooks";
11
11
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Shared types for authentication middleware.
3
- * @module src/mcp-server/transports/authentication/types
3
+ * @module src/mcp-server/transports/auth/core/auth.types
4
4
  */
5
5
  import type { AuthInfo as SdkAuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
6
6
  /**
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * @fileoverview Shared types for authentication middleware.
3
- * @module src/mcp-server/transports/authentication/types
3
+ * @module src/mcp-server/transports/auth/core/auth.types
4
4
  */
5
5
  export {};
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @fileoverview Provides utility functions for authorization, specifically for
3
3
  * checking token scopes against required permissions for a given operation.
4
- * @module src/mcp-server/transports/authentication/authUtils
4
+ * @module src/mcp-server/transports/auth/core/authUtils
5
5
  */
6
6
  /**
7
7
  * Checks if the current authentication context contains all the specified scopes.
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * @fileoverview Provides utility functions for authorization, specifically for
3
3
  * checking token scopes against required permissions for a given operation.
4
- * @module src/mcp-server/transports/authentication/authUtils
4
+ * @module src/mcp-server/transports/auth/core/authUtils
5
5
  */
6
- import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
7
- import { logger, requestContextService } from "../../../utils/index.js";
6
+ import { BaseErrorCode, McpError } from "../../../../types-global/errors.js";
7
+ import { logger, requestContextService } from "../../../../utils/index.js";
8
8
  import { authContext } from "./authContext.js";
9
9
  /**
10
10
  * Checks if the current authentication context contains all the specified scopes.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @fileoverview Barrel file for the auth module.
3
+ * Exports core utilities and middleware strategies for easier imports.
4
+ * @module src/mcp-server/transports/auth/index
5
+ */
6
+ export { authContext } from "./core/authContext.js";
7
+ export { withRequiredScopes } from "./core/authUtils.js";
8
+ export type { AuthInfo } from "./core/authTypes.js";
9
+ export { mcpAuthMiddleware as jwtAuthMiddleware } from "./strategies/jwt/jwtMiddleware.js";
10
+ export { oauthMiddleware } from "./strategies/oauth/oauthMiddleware.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @fileoverview Barrel file for the auth module.
3
+ * Exports core utilities and middleware strategies for easier imports.
4
+ * @module src/mcp-server/transports/auth/index
5
+ */
6
+ export { authContext } from "./core/authContext.js";
7
+ export { withRequiredScopes } from "./core/authUtils.js";
8
+ export { mcpAuthMiddleware as jwtAuthMiddleware } from "./strategies/jwt/jwtMiddleware.js";
9
+ export { oauthMiddleware } from "./strategies/oauth/oauthMiddleware.js";
@@ -10,26 +10,18 @@
10
10
  * is attached to `c.env.incoming.auth`. This direct attachment to the raw Node.js
11
11
  * request object is for compatibility with the underlying SDK transport, which is
12
12
  * not Hono-context-aware.
13
- * If the token is missing, invalid, or expired, it returns an HTTP 401 Unauthorized response.
13
+ * If the token is missing, invalid, or expired, it throws an `McpError`, which is
14
+ * then handled by the centralized `httpErrorHandler`.
14
15
  *
15
16
  * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
16
- * @module src/mcp-server/transports/authentication/authMiddleware
17
+ * @module src/mcp-server/transports/auth/strategies/jwt/jwtMiddleware
17
18
  */
18
19
  import { HttpBindings } from "@hono/node-server";
19
20
  import { Context, Next } from "hono";
20
- /**
21
- * Validates the presence of the MCP_AUTH_SECRET_KEY at startup.
22
- * This should be called once when the application is initializing.
23
- */
24
- export declare function initializeAuthMiddleware(): void;
25
21
  /**
26
22
  * Hono middleware for verifying JWT Bearer token authentication.
27
23
  * It attaches authentication info to `c.env.incoming.auth` for SDK compatibility with the node server.
28
24
  */
29
25
  export declare function mcpAuthMiddleware(c: Context<{
30
26
  Bindings: HttpBindings;
31
- }>, next: Next): Promise<void | (Response & import("hono").TypedResponse<{
32
- error: string;
33
- }, 500, "json">) | (Response & import("hono").TypedResponse<{
34
- error: string;
35
- }, 401, "json">)>;
27
+ }>, next: Next): Promise<void>;
@@ -10,32 +10,25 @@
10
10
  * is attached to `c.env.incoming.auth`. This direct attachment to the raw Node.js
11
11
  * request object is for compatibility with the underlying SDK transport, which is
12
12
  * not Hono-context-aware.
13
- * If the token is missing, invalid, or expired, it returns an HTTP 401 Unauthorized response.
13
+ * If the token is missing, invalid, or expired, it throws an `McpError`, which is
14
+ * then handled by the centralized `httpErrorHandler`.
14
15
  *
15
16
  * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
16
- * @module src/mcp-server/transports/authentication/authMiddleware
17
+ * @module src/mcp-server/transports/auth/strategies/jwt/jwtMiddleware
17
18
  */
18
- import jwt from "jsonwebtoken";
19
- import { config, environment } from "../../../config/index.js";
20
- import { logger, requestContextService } from "../../../utils/index.js";
21
- import { authContext } from "./authContext.js";
22
- /**
23
- * Validates the presence of the MCP_AUTH_SECRET_KEY at startup.
24
- * This should be called once when the application is initializing.
25
- */
26
- export function initializeAuthMiddleware() {
27
- const context = requestContextService.createRequestContext({
28
- operation: "initializeAuthMiddleware",
29
- });
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") {
30
26
  if (environment === "production" && !config.mcpAuthSecretKey) {
31
- logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production environment. Authentication cannot proceed securely.", context);
27
+ logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production environment for JWT auth. Authentication cannot proceed securely.");
32
28
  throw new Error("MCP_AUTH_SECRET_KEY must be set in production environment for JWT authentication.");
33
29
  }
34
30
  else if (!config.mcpAuthSecretKey) {
35
- logger.warning("MCP_AUTH_SECRET_KEY is not set. Authentication middleware will bypass checks (DEVELOPMENT ONLY). This is insecure for production.", context);
36
- }
37
- else {
38
- logger.debug("Auth middleware secret key check passed.", context);
31
+ logger.warning("MCP_AUTH_SECRET_KEY is not set. JWT auth middleware will bypass checks (DEVELOPMENT ONLY). This is insecure for production.");
39
32
  }
40
33
  }
41
34
  /**
@@ -50,6 +43,10 @@ export async function mcpAuthMiddleware(c, next) {
50
43
  });
51
44
  logger.debug("Running MCP Authentication Middleware (Bearer Token Validation)...", context);
52
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
+ }
53
50
  // Development Mode Bypass
54
51
  if (!config.mcpAuthSecretKey) {
55
52
  if (environment !== "production") {
@@ -68,28 +65,23 @@ export async function mcpAuthMiddleware(c, next) {
68
65
  }
69
66
  else {
70
67
  logger.error("FATAL: MCP_AUTH_SECRET_KEY is missing in production. Cannot bypass auth.", context);
71
- return c.json({ error: "Server configuration error: Authentication key missing." }, 500);
68
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Server configuration error: Authentication key missing.");
72
69
  }
73
70
  }
71
+ const secretKey = new TextEncoder().encode(config.mcpAuthSecretKey);
74
72
  const authHeader = c.req.header("Authorization");
75
73
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
76
74
  logger.warning("Authentication failed: Missing or malformed Authorization header (Bearer scheme required).", context);
77
- return c.json({
78
- error: "Unauthorized: Missing or invalid authentication token format.",
79
- }, 401);
75
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Missing or invalid authentication token format.");
80
76
  }
81
77
  const tokenParts = authHeader.split(" ");
82
78
  if (tokenParts.length !== 2 || tokenParts[0] !== "Bearer" || !tokenParts[1]) {
83
79
  logger.warning("Authentication failed: Malformed Bearer token.", context);
84
- return c.json({ error: "Unauthorized: Malformed authentication token." }, 401);
80
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Malformed authentication token.");
85
81
  }
86
82
  const rawToken = tokenParts[1];
87
83
  try {
88
- const decoded = jwt.verify(rawToken, config.mcpAuthSecretKey);
89
- if (typeof decoded === "string") {
90
- logger.warning("Authentication failed: JWT decoded to a string, expected an object payload.", context);
91
- return c.json({ error: "Unauthorized: Invalid token payload format." }, 401);
92
- }
84
+ const { payload: decoded } = await jwtVerify(rawToken, secretKey);
93
85
  const clientIdFromToken = typeof decoded.cid === "string"
94
86
  ? decoded.cid
95
87
  : typeof decoded.client_id === "string"
@@ -97,7 +89,7 @@ export async function mcpAuthMiddleware(c, next) {
97
89
  : undefined;
98
90
  if (!clientIdFromToken) {
99
91
  logger.warning("Authentication failed: JWT 'cid' or 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
100
- return c.json({ error: "Unauthorized: Invalid token, missing client identifier." }, 401);
92
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Invalid token, missing client identifier.");
101
93
  }
102
94
  let scopesFromToken = [];
103
95
  if (Array.isArray(decoded.scp) &&
@@ -113,7 +105,7 @@ export async function mcpAuthMiddleware(c, next) {
113
105
  }
114
106
  if (scopesFromToken.length === 0) {
115
107
  logger.warning("Authentication failed: Token resulted in an empty scope array, and scopes are required.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
116
- return c.json({ error: "Unauthorized: Token must contain valid, non-empty scopes." }, 401);
108
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain valid, non-empty scopes.");
117
109
  }
118
110
  reqWithAuth.auth = {
119
111
  token: rawToken,
@@ -131,26 +123,27 @@ export async function mcpAuthMiddleware(c, next) {
131
123
  await authContext.run({ authInfo }, next);
132
124
  }
133
125
  catch (error) {
134
- let errorMessage = "Invalid token";
135
- if (error instanceof jwt.TokenExpiredError) {
136
- errorMessage = "Token expired";
126
+ let errorMessage = "Invalid token.";
127
+ let errorCode = BaseErrorCode.UNAUTHORIZED;
128
+ if (error instanceof Error && error.name === "JWTExpired") {
129
+ errorMessage = "Token expired.";
137
130
  logger.warning("Authentication failed: Token expired.", {
138
131
  ...context,
139
- expiredAt: error.expiredAt,
132
+ errorName: error.name,
140
133
  });
141
134
  }
142
- else if (error instanceof jwt.JsonWebTokenError) {
143
- errorMessage = `Invalid token: ${error.message}`;
144
- logger.warning(`Authentication failed: ${errorMessage}`, { ...context });
145
- }
146
135
  else if (error instanceof Error) {
147
- errorMessage = `Verification error: ${error.message}`;
148
- logger.error("Authentication failed: Unexpected error during token verification.", { ...context, error: error.message });
136
+ errorMessage = `Invalid token: ${error.message}`;
137
+ logger.warning(`Authentication failed: ${errorMessage}`, {
138
+ ...context,
139
+ errorName: error.name,
140
+ });
149
141
  }
150
142
  else {
151
- errorMessage = "Unknown verification error";
143
+ errorMessage = "Unknown verification error.";
144
+ errorCode = BaseErrorCode.INTERNAL_ERROR;
152
145
  logger.error("Authentication failed: Unexpected non-error exception during token verification.", { ...context, error });
153
146
  }
154
- return c.json({ error: `Unauthorized: ${errorMessage}.` }, 401);
147
+ throw new McpError(errorCode, errorMessage);
155
148
  }
156
149
  }
@@ -5,7 +5,7 @@
5
5
  * On success, it populates an AuthInfo object and stores it in an AsyncLocalStorage
6
6
  * context for use in downstream handlers.
7
7
  *
8
- * @module src/mcp-server/transports/authentication/oauthMiddleware
8
+ * @module src/mcp-server/transports/auth/strategies/oauth/oauthMiddleware
9
9
  */
10
10
  import { HttpBindings } from "@hono/node-server";
11
11
  import { Context, Next } from "hono";
@@ -17,8 +17,4 @@ import { Context, Next } from "hono";
17
17
  */
18
18
  export declare function oauthMiddleware(c: Context<{
19
19
  Bindings: HttpBindings;
20
- }>, next: Next): Promise<(Response & import("hono").TypedResponse<{
21
- error: string;
22
- }, 500, "json">) | (Response & import("hono").TypedResponse<{
23
- error: string;
24
- }, 401, "json">) | undefined>;
20
+ }>, next: Next): Promise<void>;