@cyanheads/pubmed-mcp-server 1.0.16 → 1.1.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.
- package/dist/config/index.d.ts +12 -22
- package/dist/config/index.js +29 -51
- package/dist/index.js +77 -29
- package/dist/mcp-server/server.d.ts +3 -2
- package/dist/mcp-server/server.js +16 -14
- package/dist/mcp-server/tools/fetchPubMedContent/logic.js +2 -1
- package/dist/mcp-server/tools/generatePubMedChart/logic.js +2 -1
- package/dist/mcp-server/tools/generatePubMedChart/registration.js +2 -2
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +2 -1
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +2 -1
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.js +1 -1
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.js +19 -10
- package/dist/mcp-server/tools/searchPubMedArticles/logic.js +2 -1
- package/dist/mcp-server/transports/authentication/authContext.d.ts +33 -0
- package/dist/mcp-server/transports/authentication/authContext.js +24 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +21 -15
- package/dist/mcp-server/transports/authentication/authMiddleware.js +51 -69
- package/dist/mcp-server/transports/authentication/authUtils.d.ts +18 -0
- package/dist/mcp-server/transports/authentication/authUtils.js +45 -0
- package/dist/mcp-server/transports/authentication/oauthMiddleware.d.ts +24 -0
- package/dist/mcp-server/transports/authentication/oauthMiddleware.js +109 -0
- package/dist/mcp-server/transports/authentication/types.d.ts +17 -0
- package/dist/mcp-server/transports/authentication/types.js +5 -0
- package/dist/mcp-server/transports/httpTransport.d.ts +5 -4
- package/dist/mcp-server/transports/httpTransport.js +177 -143
- package/dist/services/NCBI/ncbiCoreApiClient.js +0 -5
- package/dist/services/NCBI/ncbiRequestQueueManager.js +2 -4
- package/dist/services/NCBI/ncbiResponseHandler.js +0 -3
- package/dist/services/NCBI/ncbiService.d.ts +1 -1
- package/dist/services/NCBI/ncbiService.js +11 -4
- package/dist/utils/internal/logger.js +52 -74
- package/package.json +21 -9
- package/dist/services/index.d.ts +0 -7
- package/dist/services/index.js +0 -7
- package/dist/services/llm-providers/index.d.ts +0 -7
- package/dist/services/llm-providers/index.js +0 -7
- package/dist/services/llm-providers/llmFactory.d.ts +0 -69
- package/dist/services/llm-providers/llmFactory.js +0 -132
- package/dist/services/llm-providers/openRouter/index.d.ts +0 -6
- package/dist/services/llm-providers/openRouter/index.js +0 -7
- package/dist/services/llm-providers/openRouter/openRouterProvider.d.ts +0 -99
- package/dist/services/llm-providers/openRouter/openRouterProvider.js +0 -329
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the AsyncLocalStorage context for authentication information.
|
|
3
|
+
* This module provides a mechanism to store and retrieve authentication details
|
|
4
|
+
* (like scopes and client ID) across asynchronous operations, making it available
|
|
5
|
+
* from the middleware layer down to the tool and resource handlers without
|
|
6
|
+
* drilling props.
|
|
7
|
+
*
|
|
8
|
+
* @module src/mcp-server/transports/authentication/authContext
|
|
9
|
+
*/
|
|
10
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
11
|
+
/**
|
|
12
|
+
* An instance of AsyncLocalStorage to hold the authentication context (`AuthStore`).
|
|
13
|
+
* This allows `authInfo` to be accessible throughout the async call chain of a request
|
|
14
|
+
* after being set in the authentication middleware.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // In middleware:
|
|
18
|
+
* await authContext.run({ authInfo }, next);
|
|
19
|
+
*
|
|
20
|
+
* // In a deeper handler:
|
|
21
|
+
* const store = authContext.getStore();
|
|
22
|
+
* const scopes = store?.authInfo.scopes;
|
|
23
|
+
*/
|
|
24
|
+
export const authContext = new AsyncLocalStorage();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT).
|
|
2
|
+
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT) for Hono.
|
|
3
3
|
*
|
|
4
4
|
* This middleware validates JSON Web Tokens (JWT) passed via the 'Authorization' header
|
|
5
5
|
* using the 'Bearer' scheme (e.g., "Authorization: Bearer <your_token>").
|
|
@@ -7,23 +7,29 @@
|
|
|
7
7
|
* in the configuration (`config.mcpAuthSecretKey`).
|
|
8
8
|
*
|
|
9
9
|
* If the token is valid, an object conforming to the MCP SDK's `AuthInfo` type
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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 returns an HTTP 401 Unauthorized response.
|
|
12
14
|
*
|
|
13
15
|
* @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
|
|
14
16
|
* @module src/mcp-server/transports/authentication/authMiddleware
|
|
15
17
|
*/
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
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
|
-
}
|
|
18
|
+
import { HttpBindings } from "@hono/node-server";
|
|
19
|
+
import { Context, Next } from "hono";
|
|
26
20
|
/**
|
|
27
|
-
*
|
|
21
|
+
* Validates the presence of the MCP_AUTH_SECRET_KEY at startup.
|
|
22
|
+
* This should be called once when the application is initializing.
|
|
28
23
|
*/
|
|
29
|
-
export declare function
|
|
24
|
+
export declare function initializeAuthMiddleware(): void;
|
|
25
|
+
/**
|
|
26
|
+
* Hono middleware for verifying JWT Bearer token authentication.
|
|
27
|
+
* It attaches authentication info to `c.env.incoming.auth` for SDK compatibility with the node server.
|
|
28
|
+
*/
|
|
29
|
+
export declare function mcpAuthMiddleware(c: Context<{
|
|
30
|
+
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">)>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT).
|
|
2
|
+
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT) for Hono.
|
|
3
3
|
*
|
|
4
4
|
* This middleware validates JSON Web Tokens (JWT) passed via the 'Authorization' header
|
|
5
5
|
* using the 'Bearer' scheme (e.g., "Authorization: Bearer <your_token>").
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* in the configuration (`config.mcpAuthSecretKey`).
|
|
8
8
|
*
|
|
9
9
|
* If the token is valid, an object conforming to the MCP SDK's `AuthInfo` type
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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 returns an HTTP 401 Unauthorized response.
|
|
12
14
|
*
|
|
13
15
|
* @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
|
|
14
16
|
* @module src/mcp-server/transports/authentication/authMiddleware
|
|
@@ -16,76 +18,78 @@
|
|
|
16
18
|
import jwt from "jsonwebtoken";
|
|
17
19
|
import { config, environment } from "../../../config/index.js";
|
|
18
20
|
import { logger, requestContextService } from "../../../utils/index.js";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
+
});
|
|
30
|
+
if (environment === "production" && !config.mcpAuthSecretKey) {
|
|
31
|
+
logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production environment. Authentication cannot proceed securely.", context);
|
|
32
|
+
throw new Error("MCP_AUTH_SECRET_KEY must be set in production environment for JWT authentication.");
|
|
33
|
+
}
|
|
34
|
+
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);
|
|
39
|
+
}
|
|
26
40
|
}
|
|
27
41
|
/**
|
|
28
|
-
*
|
|
42
|
+
* Hono middleware for verifying JWT Bearer token authentication.
|
|
43
|
+
* It attaches authentication info to `c.env.incoming.auth` for SDK compatibility with the node server.
|
|
29
44
|
*/
|
|
30
|
-
export function mcpAuthMiddleware(
|
|
45
|
+
export async function mcpAuthMiddleware(c, next) {
|
|
31
46
|
const context = requestContextService.createRequestContext({
|
|
32
47
|
operation: "mcpAuthMiddleware",
|
|
33
|
-
method: req.method,
|
|
34
|
-
path: req.path,
|
|
48
|
+
method: c.req.method,
|
|
49
|
+
path: c.req.path,
|
|
35
50
|
});
|
|
36
51
|
logger.debug("Running MCP Authentication Middleware (Bearer Token Validation)...", context);
|
|
52
|
+
const reqWithAuth = c.env.incoming;
|
|
37
53
|
// Development Mode Bypass
|
|
38
54
|
if (!config.mcpAuthSecretKey) {
|
|
39
55
|
if (environment !== "production") {
|
|
40
56
|
logger.warning("Bypassing JWT authentication: MCP_AUTH_SECRET_KEY is not set (DEVELOPMENT ONLY).", context);
|
|
41
|
-
|
|
42
|
-
req.auth = {
|
|
57
|
+
reqWithAuth.auth = {
|
|
43
58
|
token: "dev-mode-placeholder-token",
|
|
44
59
|
clientId: "dev-client-id",
|
|
45
60
|
scopes: ["dev-scope"],
|
|
46
61
|
};
|
|
47
|
-
|
|
62
|
+
const authInfo = reqWithAuth.auth;
|
|
48
63
|
logger.debug("Dev mode auth object created.", {
|
|
49
64
|
...context,
|
|
50
|
-
authDetails:
|
|
65
|
+
authDetails: authInfo,
|
|
51
66
|
});
|
|
52
|
-
return next
|
|
67
|
+
return await authContext.run({ authInfo }, next);
|
|
53
68
|
}
|
|
54
69
|
else {
|
|
55
70
|
logger.error("FATAL: MCP_AUTH_SECRET_KEY is missing in production. Cannot bypass auth.", context);
|
|
56
|
-
|
|
57
|
-
error: "Server configuration error: Authentication key missing.",
|
|
58
|
-
});
|
|
59
|
-
return;
|
|
71
|
+
return c.json({ error: "Server configuration error: Authentication key missing." }, 500);
|
|
60
72
|
}
|
|
61
73
|
}
|
|
62
|
-
const authHeader = req.
|
|
74
|
+
const authHeader = c.req.header("Authorization");
|
|
63
75
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
64
76
|
logger.warning("Authentication failed: Missing or malformed Authorization header (Bearer scheme required).", context);
|
|
65
|
-
|
|
77
|
+
return c.json({
|
|
66
78
|
error: "Unauthorized: Missing or invalid authentication token format.",
|
|
67
|
-
});
|
|
68
|
-
return;
|
|
79
|
+
}, 401);
|
|
69
80
|
}
|
|
70
81
|
const tokenParts = authHeader.split(" ");
|
|
71
82
|
if (tokenParts.length !== 2 || tokenParts[0] !== "Bearer" || !tokenParts[1]) {
|
|
72
83
|
logger.warning("Authentication failed: Malformed Bearer token.", context);
|
|
73
|
-
|
|
74
|
-
.status(401)
|
|
75
|
-
.json({ error: "Unauthorized: Malformed authentication token." });
|
|
76
|
-
return;
|
|
84
|
+
return c.json({ error: "Unauthorized: Malformed authentication token." }, 401);
|
|
77
85
|
}
|
|
78
86
|
const rawToken = tokenParts[1];
|
|
79
87
|
try {
|
|
80
88
|
const decoded = jwt.verify(rawToken, config.mcpAuthSecretKey);
|
|
81
89
|
if (typeof decoded === "string") {
|
|
82
90
|
logger.warning("Authentication failed: JWT decoded to a string, expected an object payload.", context);
|
|
83
|
-
|
|
84
|
-
.status(401)
|
|
85
|
-
.json({ error: "Unauthorized: Invalid token payload format." });
|
|
86
|
-
return;
|
|
91
|
+
return c.json({ error: "Unauthorized: Invalid token payload format." }, 401);
|
|
87
92
|
}
|
|
88
|
-
// Extract and validate fields for SDK's AuthInfo
|
|
89
93
|
const clientIdFromToken = typeof decoded.cid === "string"
|
|
90
94
|
? decoded.cid
|
|
91
95
|
: typeof decoded.client_id === "string"
|
|
@@ -93,12 +97,9 @@ export function mcpAuthMiddleware(req, res, next) {
|
|
|
93
97
|
: undefined;
|
|
94
98
|
if (!clientIdFromToken) {
|
|
95
99
|
logger.warning("Authentication failed: JWT 'cid' or 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
|
|
96
|
-
|
|
97
|
-
error: "Unauthorized: Invalid token, missing client identifier.",
|
|
98
|
-
});
|
|
99
|
-
return;
|
|
100
|
+
return c.json({ error: "Unauthorized: Invalid token, missing client identifier." }, 401);
|
|
100
101
|
}
|
|
101
|
-
let scopesFromToken;
|
|
102
|
+
let scopesFromToken = [];
|
|
102
103
|
if (Array.isArray(decoded.scp) &&
|
|
103
104
|
decoded.scp.every((s) => typeof s === "string")) {
|
|
104
105
|
scopesFromToken = decoded.scp;
|
|
@@ -107,46 +108,27 @@ export function mcpAuthMiddleware(req, res, next) {
|
|
|
107
108
|
decoded.scope.trim() !== "") {
|
|
108
109
|
scopesFromToken = decoded.scope.split(" ").filter((s) => s);
|
|
109
110
|
if (scopesFromToken.length === 0 && decoded.scope.trim() !== "") {
|
|
110
|
-
// handles case " " -> [""]
|
|
111
111
|
scopesFromToken = [decoded.scope.trim()];
|
|
112
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
113
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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;
|
|
114
|
+
if (scopesFromToken.length === 0) {
|
|
115
|
+
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);
|
|
133
117
|
}
|
|
134
|
-
|
|
135
|
-
// All other claims from 'decoded' are not part of req.auth for type safety.
|
|
136
|
-
req.auth = {
|
|
118
|
+
reqWithAuth.auth = {
|
|
137
119
|
token: rawToken,
|
|
138
120
|
clientId: clientIdFromToken,
|
|
139
121
|
scopes: scopesFromToken,
|
|
140
122
|
};
|
|
141
|
-
// Log separately if other JWT claims like 'sub' (sessionId) are needed for app logic
|
|
142
123
|
const subClaimForLogging = typeof decoded.sub === "string" ? decoded.sub : undefined;
|
|
124
|
+
const authInfo = reqWithAuth.auth;
|
|
143
125
|
logger.debug("JWT verified successfully. AuthInfo attached to request.", {
|
|
144
126
|
...context,
|
|
145
127
|
mcpSessionIdContext: subClaimForLogging,
|
|
146
|
-
clientId:
|
|
147
|
-
scopes:
|
|
128
|
+
clientId: authInfo.clientId,
|
|
129
|
+
scopes: authInfo.scopes,
|
|
148
130
|
});
|
|
149
|
-
next
|
|
131
|
+
await authContext.run({ authInfo }, next);
|
|
150
132
|
}
|
|
151
133
|
catch (error) {
|
|
152
134
|
let errorMessage = "Invalid token";
|
|
@@ -169,6 +151,6 @@ export function mcpAuthMiddleware(req, res, next) {
|
|
|
169
151
|
errorMessage = "Unknown verification error";
|
|
170
152
|
logger.error("Authentication failed: Unexpected non-error exception during token verification.", { ...context, error });
|
|
171
153
|
}
|
|
172
|
-
|
|
154
|
+
return c.json({ error: `Unauthorized: ${errorMessage}.` }, 401);
|
|
173
155
|
}
|
|
174
156
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Provides utility functions for authorization, specifically for
|
|
3
|
+
* checking token scopes against required permissions for a given operation.
|
|
4
|
+
* @module src/mcp-server/transports/authentication/authUtils
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Checks if the current authentication context contains all the specified scopes.
|
|
8
|
+
* This function is designed to be called within tool or resource handlers to
|
|
9
|
+
* enforce scope-based access control. It retrieves the authentication information
|
|
10
|
+
* from `authContext` (AsyncLocalStorage).
|
|
11
|
+
*
|
|
12
|
+
* @param requiredScopes - An array of scope strings that are mandatory for the operation.
|
|
13
|
+
* @throws {McpError} Throws an error with `BaseErrorCode.INTERNAL_ERROR` if the
|
|
14
|
+
* authentication context is missing, which indicates a server configuration issue.
|
|
15
|
+
* @throws {McpError} Throws an error with `BaseErrorCode.FORBIDDEN` if one or
|
|
16
|
+
* more required scopes are not present in the validated token.
|
|
17
|
+
*/
|
|
18
|
+
export declare function withRequiredScopes(requiredScopes: string[]): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Provides utility functions for authorization, specifically for
|
|
3
|
+
* checking token scopes against required permissions for a given operation.
|
|
4
|
+
* @module src/mcp-server/transports/authentication/authUtils
|
|
5
|
+
*/
|
|
6
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
7
|
+
import { logger, requestContextService } from "../../../utils/index.js";
|
|
8
|
+
import { authContext } from "./authContext.js";
|
|
9
|
+
/**
|
|
10
|
+
* Checks if the current authentication context contains all the specified scopes.
|
|
11
|
+
* This function is designed to be called within tool or resource handlers to
|
|
12
|
+
* enforce scope-based access control. It retrieves the authentication information
|
|
13
|
+
* from `authContext` (AsyncLocalStorage).
|
|
14
|
+
*
|
|
15
|
+
* @param requiredScopes - An array of scope strings that are mandatory for the operation.
|
|
16
|
+
* @throws {McpError} Throws an error with `BaseErrorCode.INTERNAL_ERROR` if the
|
|
17
|
+
* authentication context is missing, which indicates a server configuration issue.
|
|
18
|
+
* @throws {McpError} Throws an error with `BaseErrorCode.FORBIDDEN` if one or
|
|
19
|
+
* more required scopes are not present in the validated token.
|
|
20
|
+
*/
|
|
21
|
+
export function withRequiredScopes(requiredScopes) {
|
|
22
|
+
const store = authContext.getStore();
|
|
23
|
+
if (!store || !store.authInfo) {
|
|
24
|
+
// This is a server-side logic error; the auth middleware should always populate this.
|
|
25
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Authentication context is missing. This indicates a server configuration error.", requestContextService.createRequestContext({
|
|
26
|
+
operation: "withRequiredScopesCheck",
|
|
27
|
+
error: "AuthStore not found in AsyncLocalStorage.",
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
const { scopes: grantedScopes, clientId } = store.authInfo;
|
|
31
|
+
const grantedScopeSet = new Set(grantedScopes);
|
|
32
|
+
const missingScopes = requiredScopes.filter((scope) => !grantedScopeSet.has(scope));
|
|
33
|
+
if (missingScopes.length > 0) {
|
|
34
|
+
const context = requestContextService.createRequestContext({
|
|
35
|
+
operation: "withRequiredScopesCheck",
|
|
36
|
+
required: requiredScopes,
|
|
37
|
+
granted: grantedScopes,
|
|
38
|
+
missing: missingScopes,
|
|
39
|
+
clientId: clientId,
|
|
40
|
+
subject: store.authInfo.subject,
|
|
41
|
+
});
|
|
42
|
+
logger.warning("Authorization failed: Missing required scopes.", context);
|
|
43
|
+
throw new McpError(BaseErrorCode.FORBIDDEN, `Insufficient permissions. Missing required scopes: ${missingScopes.join(", ")}`, { requiredScopes, missingScopes });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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/authentication/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<(Response & import("hono").TypedResponse<{
|
|
21
|
+
error: string;
|
|
22
|
+
}, 500, "json">) | (Response & import("hono").TypedResponse<{
|
|
23
|
+
error: string;
|
|
24
|
+
}, 401, "json">) | undefined>;
|
|
@@ -0,0 +1,109 @@
|
|
|
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/authentication/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 { ErrorHandler } from "../../../utils/internal/errorHandler.js";
|
|
14
|
+
import { logger, requestContextService } from "../../../utils/index.js";
|
|
15
|
+
import { authContext } from "./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
|
+
const context = requestContextService.createRequestContext({
|
|
61
|
+
operation: "oauthMiddleware",
|
|
62
|
+
httpMethod: c.req.method,
|
|
63
|
+
httpPath: c.req.path,
|
|
64
|
+
});
|
|
65
|
+
if (!jwks) {
|
|
66
|
+
// This should not happen if startup validation is correct, but it's a safeguard.
|
|
67
|
+
const error = new McpError(BaseErrorCode.CONFIGURATION_ERROR, "OAuth middleware is active, but JWKS client is not initialized.", context);
|
|
68
|
+
ErrorHandler.handleError(error, { operation: "oauthMiddleware", context });
|
|
69
|
+
return c.json({ error: "Server configuration error." }, 500);
|
|
70
|
+
}
|
|
71
|
+
const authHeader = c.req.header("Authorization");
|
|
72
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
73
|
+
return c.json({ error: "Unauthorized: Missing or invalid token format." }, 401);
|
|
74
|
+
}
|
|
75
|
+
const token = authHeader.substring(7);
|
|
76
|
+
try {
|
|
77
|
+
const { payload } = await jwtVerify(token, jwks, {
|
|
78
|
+
issuer: config.oauthIssuerUrl,
|
|
79
|
+
audience: config.oauthAudience,
|
|
80
|
+
});
|
|
81
|
+
// The 'scope' claim is typically a space-delimited string in OAuth 2.1.
|
|
82
|
+
const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : [];
|
|
83
|
+
const clientId = typeof payload.client_id === "string" ? payload.client_id : undefined;
|
|
84
|
+
if (!clientId) {
|
|
85
|
+
logger.warning("Authentication failed: OAuth token 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(payload) });
|
|
86
|
+
return c.json({ error: "Unauthorized: Invalid token, missing client identifier." }, 401);
|
|
87
|
+
}
|
|
88
|
+
const authInfo = {
|
|
89
|
+
token,
|
|
90
|
+
clientId,
|
|
91
|
+
scopes,
|
|
92
|
+
subject: typeof payload.sub === "string" ? payload.sub : undefined,
|
|
93
|
+
};
|
|
94
|
+
// Attach to the raw request for potential legacy compatibility and
|
|
95
|
+
// store in AsyncLocalStorage for modern, safe access in handlers.
|
|
96
|
+
c.env.incoming.auth = authInfo;
|
|
97
|
+
await authContext.run({ authInfo }, next);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
logger.warning("OAuth token validation failed", {
|
|
101
|
+
...context,
|
|
102
|
+
errorName: error.name,
|
|
103
|
+
errorMessage: error.message,
|
|
104
|
+
});
|
|
105
|
+
// The `jose` library provides specific error codes like 'ERR_JWT_EXPIRED' or 'ERR_JWS_INVALID'
|
|
106
|
+
const message = `Unauthorized: ${error.message || "Invalid token"}`;
|
|
107
|
+
return c.json({ error: message }, 401);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared types for authentication middleware.
|
|
3
|
+
* @module src/mcp-server/transports/authentication/types
|
|
4
|
+
*/
|
|
5
|
+
import type { AuthInfo as SdkAuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Defines the structure for authentication information derived from a token.
|
|
8
|
+
* It extends the base SDK type to include common optional claims.
|
|
9
|
+
*/
|
|
10
|
+
export type AuthInfo = SdkAuthInfo & {
|
|
11
|
+
subject?: string;
|
|
12
|
+
};
|
|
13
|
+
declare module "http" {
|
|
14
|
+
interface IncomingMessage {
|
|
15
|
+
auth?: AuthInfo;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Handles the setup and management of the Streamable HTTP MCP transport.
|
|
2
|
+
* @fileoverview Handles the setup and management of the Streamable HTTP MCP transport using Hono.
|
|
3
3
|
* Implements the MCP Specification 2025-03-26 for Streamable HTTP.
|
|
4
|
-
* This includes creating
|
|
4
|
+
* This includes creating a Hono server, configuring middleware (CORS, Authentication),
|
|
5
5
|
* defining request routing for the single MCP endpoint (POST/GET/DELETE),
|
|
6
6
|
* managing server-side sessions, handling Server-Sent Events (SSE) for streaming,
|
|
7
7
|
* and binding to a network port with retry logic for port conflicts.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
|
|
11
11
|
* @module src/mcp-server/transports/httpTransport
|
|
12
12
|
*/
|
|
13
|
+
import { ServerType } from "@hono/node-server";
|
|
13
14
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
15
|
import { RequestContext } from "../../utils/index.js";
|
|
15
16
|
/**
|
|
@@ -17,7 +18,7 @@ import { RequestContext } from "../../utils/index.js";
|
|
|
17
18
|
*
|
|
18
19
|
* @param createServerInstanceFn - An asynchronous factory function that returns a new `McpServer` instance.
|
|
19
20
|
* @param parentContext - Logging context from the main server startup process.
|
|
20
|
-
* @returns A promise that resolves when the HTTP server is successfully listening.
|
|
21
|
+
* @returns A promise that resolves with the Node.js `http.Server` instance when the HTTP server is successfully listening.
|
|
21
22
|
* @throws {Error} If the server fails to start after all port retries.
|
|
22
23
|
*/
|
|
23
|
-
export declare function startHttpTransport(createServerInstanceFn: () => Promise<McpServer>, parentContext: RequestContext): Promise<
|
|
24
|
+
export declare function startHttpTransport(createServerInstanceFn: () => Promise<McpServer>, parentContext: RequestContext): Promise<ServerType>;
|