@cyanheads/pubmed-mcp-server 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/config/index.d.ts +13 -52
- package/dist/config/index.js +51 -222
- package/dist/mcp-server/server.d.ts +0 -5
- package/dist/mcp-server/server.js +18 -34
- package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +8 -8
- package/dist/mcp-server/tools/fetchPubMedContent/logic.js +6 -5
- package/dist/mcp-server/tools/generatePubMedChart/logic.js +19 -19
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +65 -240
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +3 -3
- package/dist/mcp-server/tools/searchPubMedArticles/logic.js +10 -11
- package/dist/mcp-server/transports/auth/authFactory.d.ts +10 -0
- package/dist/mcp-server/transports/auth/authFactory.js +41 -0
- package/dist/mcp-server/transports/auth/authMiddleware.d.ts +19 -0
- package/dist/mcp-server/transports/auth/authMiddleware.js +57 -0
- package/dist/mcp-server/transports/auth/index.d.ts +8 -5
- package/dist/mcp-server/transports/auth/index.js +6 -4
- package/dist/mcp-server/transports/auth/{core → lib}/authTypes.d.ts +0 -5
- package/dist/mcp-server/transports/auth/lib/authTypes.js +8 -0
- package/dist/mcp-server/transports/auth/{core → lib}/authUtils.js +21 -14
- package/dist/mcp-server/transports/auth/strategies/authStrategy.d.ts +17 -0
- package/dist/mcp-server/transports/auth/strategies/authStrategy.js +1 -0
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.d.ts +7 -0
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +112 -0
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.d.ts +7 -0
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +101 -0
- package/dist/mcp-server/transports/core/baseTransportManager.d.ts +17 -0
- package/dist/mcp-server/transports/core/baseTransportManager.js +18 -0
- package/dist/mcp-server/transports/core/honoNodeBridge.d.ts +23 -0
- package/dist/mcp-server/transports/core/honoNodeBridge.js +51 -0
- package/dist/mcp-server/transports/core/statefulTransportManager.d.ts +31 -0
- package/dist/mcp-server/transports/core/statefulTransportManager.js +233 -0
- package/dist/mcp-server/transports/core/statelessTransportManager.d.ts +20 -0
- package/dist/mcp-server/transports/core/statelessTransportManager.js +92 -0
- package/dist/mcp-server/transports/core/transportTypes.d.ts +68 -0
- package/dist/mcp-server/transports/core/transportTypes.js +5 -0
- package/dist/mcp-server/transports/{httpErrorHandler.d.ts → http/httpErrorHandler.d.ts} +4 -9
- package/dist/mcp-server/transports/{httpErrorHandler.js → http/httpErrorHandler.js} +33 -8
- package/dist/mcp-server/transports/http/httpTransport.d.ts +22 -0
- package/dist/mcp-server/transports/http/httpTransport.js +251 -0
- package/dist/mcp-server/transports/http/httpTypes.d.ts +16 -0
- package/dist/mcp-server/transports/http/httpTypes.js +5 -0
- package/dist/mcp-server/transports/http/index.d.ts +7 -0
- package/dist/mcp-server/transports/http/index.js +6 -0
- package/dist/mcp-server/transports/http/mcpTransportMiddleware.d.ts +25 -0
- package/dist/mcp-server/transports/http/mcpTransportMiddleware.js +63 -0
- package/dist/mcp-server/transports/stdio/index.d.ts +5 -0
- package/dist/mcp-server/transports/stdio/index.js +5 -0
- package/dist/mcp-server/transports/{stdioTransport.d.ts → stdio/stdioTransport.d.ts} +2 -2
- package/dist/mcp-server/transports/{stdioTransport.js → stdio/stdioTransport.js} +10 -5
- package/dist/services/NCBI/{ncbiConstants.d.ts → core/ncbiConstants.d.ts} +1 -1
- package/dist/services/NCBI/{ncbiConstants.js → core/ncbiConstants.js} +1 -1
- package/dist/services/NCBI/{ncbiCoreApiClient.d.ts → core/ncbiCoreApiClient.d.ts} +3 -3
- package/dist/services/NCBI/core/ncbiCoreApiClient.js +117 -0
- package/dist/services/NCBI/{ncbiRequestQueueManager.d.ts → core/ncbiRequestQueueManager.d.ts} +2 -2
- package/dist/services/NCBI/{ncbiRequestQueueManager.js → core/ncbiRequestQueueManager.js} +3 -3
- package/dist/services/NCBI/{ncbiResponseHandler.d.ts → core/ncbiResponseHandler.d.ts} +2 -2
- package/dist/services/NCBI/{ncbiResponseHandler.js → core/ncbiResponseHandler.js} +3 -3
- package/dist/services/NCBI/{ncbiService.d.ts → core/ncbiService.d.ts} +5 -4
- package/dist/services/NCBI/{ncbiService.js → core/ncbiService.js} +15 -3
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.js +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.js +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.js +5 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.js +1 -1
- package/dist/types-global/errors.d.ts +2 -0
- package/dist/types-global/errors.js +2 -0
- package/dist/types-global/pubmedXml.d.ts +15 -0
- package/dist/utils/internal/errorHandler.js +1 -1
- package/dist/utils/internal/logger.d.ts +13 -1
- package/dist/utils/internal/logger.js +43 -9
- package/dist/utils/network/fetchWithTimeout.d.ts +21 -0
- package/dist/utils/network/fetchWithTimeout.js +59 -0
- package/dist/utils/network/index.d.ts +6 -0
- package/dist/utils/network/index.js +5 -0
- package/dist/utils/scheduling/index.d.ts +6 -0
- package/dist/utils/scheduling/index.js +6 -0
- package/dist/utils/scheduling/scheduler.d.ts +72 -0
- package/dist/utils/scheduling/scheduler.js +150 -0
- package/dist/utils/security/sanitization.js +35 -18
- package/package.json +17 -13
- package/dist/mcp-server/transports/auth/core/authTypes.js +0 -5
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.d.ts +0 -27
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +0 -149
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.d.ts +0 -20
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +0 -124
- package/dist/mcp-server/transports/httpTransport.d.ts +0 -21
- package/dist/mcp-server/transports/httpTransport.js +0 -208
- package/dist/services/NCBI/ncbiCoreApiClient.js +0 -113
- /package/dist/mcp-server/transports/auth/{core → lib}/authContext.d.ts +0 -0
- /package/dist/mcp-server/transports/auth/{core → lib}/authContext.js +0 -0
- /package/dist/mcp-server/transports/auth/{core → lib}/authUtils.d.ts +0 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
2
|
+
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
|
|
3
|
+
import { authContext } from "./lib/authContext.js";
|
|
4
|
+
/**
|
|
5
|
+
* Creates a Hono middleware function that enforces authentication using a given strategy.
|
|
6
|
+
*
|
|
7
|
+
* @param strategy - An instance of a class that implements the `AuthStrategy` interface.
|
|
8
|
+
* @returns A Hono middleware function.
|
|
9
|
+
*/
|
|
10
|
+
export function createAuthMiddleware(strategy) {
|
|
11
|
+
return async function authMiddleware(c, next) {
|
|
12
|
+
const context = requestContextService.createRequestContext({
|
|
13
|
+
operation: "authMiddleware",
|
|
14
|
+
method: c.req.method,
|
|
15
|
+
path: c.req.path,
|
|
16
|
+
});
|
|
17
|
+
logger.debug("Initiating authentication check.", context);
|
|
18
|
+
const authHeader = c.req.header("Authorization");
|
|
19
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
20
|
+
logger.warning("Authorization header missing or invalid.", context);
|
|
21
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Missing or invalid Authorization header. Bearer scheme required.", context);
|
|
22
|
+
}
|
|
23
|
+
const token = authHeader.substring(7);
|
|
24
|
+
if (!token) {
|
|
25
|
+
logger.warning("Bearer token is missing from Authorization header.", context);
|
|
26
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Authentication token is missing.", context);
|
|
27
|
+
}
|
|
28
|
+
logger.debug("Extracted Bearer token, proceeding to verification.", context);
|
|
29
|
+
try {
|
|
30
|
+
const authInfo = await strategy.verify(token);
|
|
31
|
+
const authLogContext = {
|
|
32
|
+
...context,
|
|
33
|
+
clientId: authInfo.clientId,
|
|
34
|
+
subject: authInfo.subject,
|
|
35
|
+
scopes: authInfo.scopes,
|
|
36
|
+
};
|
|
37
|
+
logger.info("Authentication successful. Auth context populated.", authLogContext);
|
|
38
|
+
// Run the next middleware in the chain within the populated auth context.
|
|
39
|
+
await authContext.run({ authInfo }, next);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// The strategy is expected to throw an McpError.
|
|
43
|
+
// We re-throw it here to be caught by the global httpErrorHandler.
|
|
44
|
+
logger.warning("Authentication verification failed.", {
|
|
45
|
+
...context,
|
|
46
|
+
error: error instanceof Error ? error.message : String(error),
|
|
47
|
+
});
|
|
48
|
+
// Ensure consistent error handling
|
|
49
|
+
throw ErrorHandler.handleError(error, {
|
|
50
|
+
operation: "authMiddlewareVerification",
|
|
51
|
+
context,
|
|
52
|
+
rethrow: true, // Rethrow to be caught by Hono's global error handler
|
|
53
|
+
errorCode: BaseErrorCode.UNAUTHORIZED, // Default to unauthorized if not more specific
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
* Exports core utilities and middleware strategies for easier imports.
|
|
4
4
|
* @module src/mcp-server/transports/auth/index
|
|
5
5
|
*/
|
|
6
|
-
export { authContext } from "./
|
|
7
|
-
export { withRequiredScopes } from "./
|
|
8
|
-
export type { AuthInfo } from "./
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
6
|
+
export { authContext } from "./lib/authContext.js";
|
|
7
|
+
export { withRequiredScopes } from "./lib/authUtils.js";
|
|
8
|
+
export type { AuthInfo } from "./lib/authTypes.js";
|
|
9
|
+
export { createAuthStrategy } from "./authFactory.js";
|
|
10
|
+
export { createAuthMiddleware } from "./authMiddleware.js";
|
|
11
|
+
export { AuthStrategy } from "./strategies/authStrategy.js";
|
|
12
|
+
export { JwtStrategy } from "./strategies/jwtStrategy.js";
|
|
13
|
+
export { OauthStrategy } from "./strategies/oauthStrategy.js";
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* Exports core utilities and middleware strategies for easier imports.
|
|
4
4
|
* @module src/mcp-server/transports/auth/index
|
|
5
5
|
*/
|
|
6
|
-
export { authContext } from "./
|
|
7
|
-
export { withRequiredScopes } from "./
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
6
|
+
export { authContext } from "./lib/authContext.js";
|
|
7
|
+
export { withRequiredScopes } from "./lib/authUtils.js";
|
|
8
|
+
export { createAuthStrategy } from "./authFactory.js";
|
|
9
|
+
export { createAuthMiddleware } from "./authMiddleware.js";
|
|
10
|
+
export { JwtStrategy } from "./strategies/jwtStrategy.js";
|
|
11
|
+
export { OauthStrategy } from "./strategies/oauthStrategy.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Shared types for authentication middleware.
|
|
3
|
+
* @module src/mcp-server/transports/auth/core/auth.types
|
|
4
|
+
*/
|
|
5
|
+
export {};
|
|
6
|
+
// The declaration for `http.IncomingMessage` is no longer needed here,
|
|
7
|
+
// as the new architecture avoids direct mutation where possible and handles
|
|
8
|
+
// the attachment within the Hono context.
|
|
@@ -19,27 +19,34 @@ import { authContext } from "./authContext.js";
|
|
|
19
19
|
* more required scopes are not present in the validated token.
|
|
20
20
|
*/
|
|
21
21
|
export function withRequiredScopes(requiredScopes) {
|
|
22
|
+
const operationName = "withRequiredScopesCheck";
|
|
23
|
+
const initialContext = requestContextService.createRequestContext({
|
|
24
|
+
operation: operationName,
|
|
25
|
+
requiredScopes,
|
|
26
|
+
});
|
|
27
|
+
logger.debug("Performing scope authorization check.", initialContext);
|
|
22
28
|
const store = authContext.getStore();
|
|
23
29
|
if (!store || !store.authInfo) {
|
|
30
|
+
logger.crit("Authentication context is missing in withRequiredScopes. This is a server configuration error.", initialContext);
|
|
24
31
|
// 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.",
|
|
26
|
-
|
|
32
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Authentication context is missing. This indicates a server configuration error.", {
|
|
33
|
+
...initialContext,
|
|
27
34
|
error: "AuthStore not found in AsyncLocalStorage.",
|
|
28
|
-
})
|
|
35
|
+
});
|
|
29
36
|
}
|
|
30
|
-
const { scopes: grantedScopes, clientId } = store.authInfo;
|
|
37
|
+
const { scopes: grantedScopes, clientId, subject } = store.authInfo;
|
|
31
38
|
const grantedScopeSet = new Set(grantedScopes);
|
|
32
39
|
const missingScopes = requiredScopes.filter((scope) => !grantedScopeSet.has(scope));
|
|
40
|
+
const finalContext = {
|
|
41
|
+
...initialContext,
|
|
42
|
+
grantedScopes,
|
|
43
|
+
clientId,
|
|
44
|
+
subject,
|
|
45
|
+
};
|
|
33
46
|
if (missingScopes.length > 0) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
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 });
|
|
47
|
+
const errorContext = { ...finalContext, missingScopes };
|
|
48
|
+
logger.warning("Authorization failed: Missing required scopes.", errorContext);
|
|
49
|
+
throw new McpError(BaseErrorCode.FORBIDDEN, `Insufficient permissions. Missing required scopes: ${missingScopes.join(", ")}`, errorContext);
|
|
44
50
|
}
|
|
51
|
+
logger.debug("Scope authorization successful.", finalContext);
|
|
45
52
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines the interface for all authentication strategies.
|
|
3
|
+
* This interface establishes a contract for verifying authentication tokens,
|
|
4
|
+
* ensuring that any authentication method (JWT, OAuth, etc.) can be used
|
|
5
|
+
* interchangeably by the core authentication middleware.
|
|
6
|
+
* @module src/mcp-server/transports/auth/strategies/AuthStrategy
|
|
7
|
+
*/
|
|
8
|
+
import type { AuthInfo } from "../lib/authTypes.js";
|
|
9
|
+
export interface AuthStrategy {
|
|
10
|
+
/**
|
|
11
|
+
* Verifies an authentication token.
|
|
12
|
+
* @param token The raw token string extracted from the request.
|
|
13
|
+
* @returns A promise that resolves with the AuthInfo on successful verification.
|
|
14
|
+
* @throws {McpError} if the token is invalid, expired, or fails verification for any reason.
|
|
15
|
+
*/
|
|
16
|
+
verify(token: string): Promise<AuthInfo>;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Implements the JWT authentication strategy.
|
|
3
|
+
* This module provides a concrete implementation of the AuthStrategy for validating
|
|
4
|
+
* JSON Web Tokens (JWTs). It encapsulates all logic related to JWT verification,
|
|
5
|
+
* including secret key management and payload validation.
|
|
6
|
+
* @module src/mcp-server/transports/auth/strategies/JwtStrategy
|
|
7
|
+
*/
|
|
8
|
+
import { jwtVerify } from "jose";
|
|
9
|
+
import { config, environment } from "../../../../config/index.js";
|
|
10
|
+
import { BaseErrorCode, McpError } from "../../../../types-global/errors.js";
|
|
11
|
+
import { ErrorHandler, logger, requestContextService, } from "../../../../utils/index.js";
|
|
12
|
+
export class JwtStrategy {
|
|
13
|
+
constructor() {
|
|
14
|
+
const context = requestContextService.createRequestContext({
|
|
15
|
+
operation: "JwtStrategy.constructor",
|
|
16
|
+
});
|
|
17
|
+
logger.debug("Initializing JwtStrategy...", context);
|
|
18
|
+
if (config.mcpAuthMode === "jwt") {
|
|
19
|
+
if (environment === "production" && !config.mcpAuthSecretKey) {
|
|
20
|
+
logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production for JWT auth.", context);
|
|
21
|
+
throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, "MCP_AUTH_SECRET_KEY must be set for JWT auth in production.", context);
|
|
22
|
+
}
|
|
23
|
+
else if (!config.mcpAuthSecretKey) {
|
|
24
|
+
logger.warning("MCP_AUTH_SECRET_KEY is not set. JWT auth will be bypassed (DEV ONLY).", context);
|
|
25
|
+
this.secretKey = null;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
logger.info("JWT secret key loaded successfully.", context);
|
|
29
|
+
this.secretKey = new TextEncoder().encode(config.mcpAuthSecretKey);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
this.secretKey = null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async verify(token) {
|
|
37
|
+
const context = requestContextService.createRequestContext({
|
|
38
|
+
operation: "JwtStrategy.verify",
|
|
39
|
+
});
|
|
40
|
+
logger.debug("Attempting to verify JWT.", context);
|
|
41
|
+
// Handle development mode bypass
|
|
42
|
+
if (!this.secretKey) {
|
|
43
|
+
if (environment !== "production") {
|
|
44
|
+
logger.warning("Bypassing JWT verification: No secret key (DEV ONLY).", context);
|
|
45
|
+
return {
|
|
46
|
+
token: "dev-mode-placeholder-token",
|
|
47
|
+
clientId: config.devMcpClientId || "dev-client-id",
|
|
48
|
+
scopes: config.devMcpScopes || ["dev-scope"],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// This path is defensive. The constructor should prevent this state in production.
|
|
52
|
+
logger.crit("Auth secret key is missing in production.", context);
|
|
53
|
+
throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, "Auth secret key is missing in production. This indicates a server configuration error.", context);
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const { payload: decoded } = await jwtVerify(token, this.secretKey);
|
|
57
|
+
logger.debug("JWT signature verified successfully.", {
|
|
58
|
+
...context,
|
|
59
|
+
claims: decoded,
|
|
60
|
+
});
|
|
61
|
+
const clientId = typeof decoded.cid === "string"
|
|
62
|
+
? decoded.cid
|
|
63
|
+
: typeof decoded.client_id === "string"
|
|
64
|
+
? decoded.client_id
|
|
65
|
+
: undefined;
|
|
66
|
+
if (!clientId) {
|
|
67
|
+
logger.warning("Invalid token: missing 'cid' or 'client_id' claim.", context);
|
|
68
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Invalid token: missing 'cid' or 'client_id' claim.", context);
|
|
69
|
+
}
|
|
70
|
+
let scopes = [];
|
|
71
|
+
if (Array.isArray(decoded.scp) &&
|
|
72
|
+
decoded.scp.every((s) => typeof s === "string")) {
|
|
73
|
+
scopes = decoded.scp;
|
|
74
|
+
}
|
|
75
|
+
else if (typeof decoded.scope === "string" && decoded.scope.trim()) {
|
|
76
|
+
scopes = decoded.scope.split(" ").filter(Boolean);
|
|
77
|
+
}
|
|
78
|
+
if (scopes.length === 0) {
|
|
79
|
+
logger.warning("Invalid token: missing or empty 'scp' or 'scope' claim.", context);
|
|
80
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain valid, non-empty scopes.", context);
|
|
81
|
+
}
|
|
82
|
+
const authInfo = {
|
|
83
|
+
token,
|
|
84
|
+
clientId,
|
|
85
|
+
scopes,
|
|
86
|
+
subject: decoded.sub,
|
|
87
|
+
};
|
|
88
|
+
logger.info("JWT verification successful.", {
|
|
89
|
+
...context,
|
|
90
|
+
clientId,
|
|
91
|
+
scopes,
|
|
92
|
+
});
|
|
93
|
+
return authInfo;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
const message = error instanceof Error && error.name === "JWTExpired"
|
|
97
|
+
? "Token has expired."
|
|
98
|
+
: "Token verification failed.";
|
|
99
|
+
logger.warning(`JWT verification failed: ${message}`, {
|
|
100
|
+
...context,
|
|
101
|
+
errorName: error instanceof Error ? error.name : "Unknown",
|
|
102
|
+
});
|
|
103
|
+
throw ErrorHandler.handleError(error, {
|
|
104
|
+
operation: "JwtStrategy.verify",
|
|
105
|
+
context,
|
|
106
|
+
rethrow: true,
|
|
107
|
+
errorCode: BaseErrorCode.UNAUTHORIZED,
|
|
108
|
+
errorMapper: () => new McpError(BaseErrorCode.UNAUTHORIZED, message, context),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Implements the OAuth 2.1 authentication strategy.
|
|
3
|
+
* This module provides a concrete implementation of the AuthStrategy for validating
|
|
4
|
+
* JWTs against a remote JSON Web Key Set (JWKS), as is common in OAuth 2.1 flows.
|
|
5
|
+
* @module src/mcp-server/transports/auth/strategies/OauthStrategy
|
|
6
|
+
*/
|
|
7
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
8
|
+
import { config } from "../../../../config/index.js";
|
|
9
|
+
import { BaseErrorCode, McpError } from "../../../../types-global/errors.js";
|
|
10
|
+
import { ErrorHandler, logger, requestContextService, } from "../../../../utils/index.js";
|
|
11
|
+
export class OauthStrategy {
|
|
12
|
+
constructor() {
|
|
13
|
+
const context = requestContextService.createRequestContext({
|
|
14
|
+
operation: "OauthStrategy.constructor",
|
|
15
|
+
});
|
|
16
|
+
logger.debug("Initializing OauthStrategy...", context);
|
|
17
|
+
if (config.mcpAuthMode !== "oauth") {
|
|
18
|
+
// This check is for internal consistency, so a standard Error is acceptable here.
|
|
19
|
+
throw new Error("OauthStrategy instantiated for non-oauth auth mode.");
|
|
20
|
+
}
|
|
21
|
+
if (!config.oauthIssuerUrl || !config.oauthAudience) {
|
|
22
|
+
logger.fatal("CRITICAL: OAUTH_ISSUER_URL and OAUTH_AUDIENCE must be set for OAuth mode.", context);
|
|
23
|
+
// This is a user-facing configuration error, so McpError is appropriate.
|
|
24
|
+
throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, "OAUTH_ISSUER_URL and OAUTH_AUDIENCE must be set for OAuth mode.", context);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const jwksUrl = new URL(config.oauthJwksUri ||
|
|
28
|
+
`${config.oauthIssuerUrl.replace(/\/$/, "")}/.well-known/jwks.json`);
|
|
29
|
+
this.jwks = createRemoteJWKSet(jwksUrl, {
|
|
30
|
+
cooldownDuration: 300000, // 5 minutes
|
|
31
|
+
timeoutDuration: 5000, // 5 seconds
|
|
32
|
+
});
|
|
33
|
+
logger.info(`JWKS client initialized for URL: ${jwksUrl.href}`, context);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
logger.fatal("Failed to initialize JWKS client.", {
|
|
37
|
+
...context,
|
|
38
|
+
error: error instanceof Error ? error.message : String(error),
|
|
39
|
+
});
|
|
40
|
+
// This is a critical startup failure, so a specific McpError is warranted.
|
|
41
|
+
throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, "Could not initialize JWKS client for OAuth strategy.", {
|
|
42
|
+
...context,
|
|
43
|
+
originalError: error instanceof Error ? error.message : "Unknown",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async verify(token) {
|
|
48
|
+
const context = requestContextService.createRequestContext({
|
|
49
|
+
operation: "OauthStrategy.verify",
|
|
50
|
+
});
|
|
51
|
+
logger.debug("Attempting to verify OAuth token via JWKS.", context);
|
|
52
|
+
try {
|
|
53
|
+
const { payload } = await jwtVerify(token, this.jwks, {
|
|
54
|
+
issuer: config.oauthIssuerUrl,
|
|
55
|
+
audience: config.oauthAudience,
|
|
56
|
+
});
|
|
57
|
+
logger.debug("OAuth token signature verified successfully.", {
|
|
58
|
+
...context,
|
|
59
|
+
claims: payload,
|
|
60
|
+
});
|
|
61
|
+
const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : [];
|
|
62
|
+
if (scopes.length === 0) {
|
|
63
|
+
logger.warning("Invalid token: missing or empty 'scope' claim.", context);
|
|
64
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain valid, non-empty scopes.", context);
|
|
65
|
+
}
|
|
66
|
+
const clientId = typeof payload.client_id === "string" ? payload.client_id : undefined;
|
|
67
|
+
if (!clientId) {
|
|
68
|
+
logger.warning("Invalid token: missing 'client_id' claim.", context);
|
|
69
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain a 'client_id' claim.", context);
|
|
70
|
+
}
|
|
71
|
+
const authInfo = {
|
|
72
|
+
token,
|
|
73
|
+
clientId,
|
|
74
|
+
scopes,
|
|
75
|
+
subject: typeof payload.sub === "string" ? payload.sub : undefined,
|
|
76
|
+
};
|
|
77
|
+
logger.info("OAuth token verification successful.", {
|
|
78
|
+
...context,
|
|
79
|
+
clientId,
|
|
80
|
+
scopes,
|
|
81
|
+
});
|
|
82
|
+
return authInfo;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const message = error instanceof Error && error.name === "JWTExpired"
|
|
86
|
+
? "Token has expired."
|
|
87
|
+
: "OAuth token verification failed.";
|
|
88
|
+
logger.warning(`OAuth token verification failed: ${message}`, {
|
|
89
|
+
...context,
|
|
90
|
+
errorName: error instanceof Error ? error.name : "Unknown",
|
|
91
|
+
});
|
|
92
|
+
throw ErrorHandler.handleError(error, {
|
|
93
|
+
operation: "OauthStrategy.verify",
|
|
94
|
+
context,
|
|
95
|
+
rethrow: true,
|
|
96
|
+
errorCode: BaseErrorCode.UNAUTHORIZED,
|
|
97
|
+
errorMapper: () => new McpError(BaseErrorCode.UNAUTHORIZED, message, context),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Abstract base class for transport managers.
|
|
3
|
+
* @module src/mcp-server/transports/core/baseTransportManager
|
|
4
|
+
*/
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import type { IncomingHttpHeaders } from "http";
|
|
7
|
+
import { RequestContext } from "../../../utils/index.js";
|
|
8
|
+
import { TransportManager, TransportResponse } from "./transportTypes.js";
|
|
9
|
+
/**
|
|
10
|
+
* Abstract base class for transport managers, providing common functionality.
|
|
11
|
+
*/
|
|
12
|
+
export declare abstract class BaseTransportManager implements TransportManager {
|
|
13
|
+
protected readonly createServerInstanceFn: () => Promise<McpServer>;
|
|
14
|
+
constructor(createServerInstanceFn: () => Promise<McpServer>);
|
|
15
|
+
abstract handleRequest(headers: IncomingHttpHeaders, body: unknown, context: RequestContext, sessionId?: string): Promise<TransportResponse>;
|
|
16
|
+
abstract shutdown(): Promise<void>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Abstract base class for transport managers.
|
|
3
|
+
* @module src/mcp-server/transports/core/baseTransportManager
|
|
4
|
+
*/
|
|
5
|
+
import { logger, requestContextService, } from "../../../utils/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* Abstract base class for transport managers, providing common functionality.
|
|
8
|
+
*/
|
|
9
|
+
export class BaseTransportManager {
|
|
10
|
+
constructor(createServerInstanceFn) {
|
|
11
|
+
const context = requestContextService.createRequestContext({
|
|
12
|
+
operation: "BaseTransportManager.constructor",
|
|
13
|
+
managerType: this.constructor.name,
|
|
14
|
+
});
|
|
15
|
+
logger.debug("Initializing transport manager.", context);
|
|
16
|
+
this.createServerInstanceFn = createServerInstanceFn;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Provides a bridge between the MCP SDK's Node.js-style
|
|
3
|
+
* streamable HTTP transport and Hono's Web Standards-based streaming response.
|
|
4
|
+
* @module src/mcp-server/transports/core/honoNodeBridge
|
|
5
|
+
*/
|
|
6
|
+
import { PassThrough } from "stream";
|
|
7
|
+
/**
|
|
8
|
+
* A mock ServerResponse that pipes writes to a PassThrough stream.
|
|
9
|
+
* This is the bridge between Model Context Protocol's SDK's Node.js-style response handling
|
|
10
|
+
* and Hono's stream-based body. It captures status and headers.
|
|
11
|
+
*/
|
|
12
|
+
export declare class HonoStreamResponse extends PassThrough {
|
|
13
|
+
statusCode: number;
|
|
14
|
+
headers: Record<string, string | number | string[]>;
|
|
15
|
+
constructor();
|
|
16
|
+
writeHead(statusCode: number, headers?: Record<string, string | number | string[]>): this;
|
|
17
|
+
setHeader(name: string, value: string | number | string[]): this;
|
|
18
|
+
getHeader(name: string): string | number | string[] | undefined;
|
|
19
|
+
getHeaders(): Record<string, string | number | string[]>;
|
|
20
|
+
removeHeader(name: string): void;
|
|
21
|
+
write(chunk: unknown, encodingOrCallback?: BufferEncoding | ((error: Error | null | undefined) => void), callback?: (error: Error | null | undefined) => void): boolean;
|
|
22
|
+
end(chunk?: unknown, encodingOrCallback?: BufferEncoding | (() => void), callback?: () => void): this;
|
|
23
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Provides a bridge between the MCP SDK's Node.js-style
|
|
3
|
+
* streamable HTTP transport and Hono's Web Standards-based streaming response.
|
|
4
|
+
* @module src/mcp-server/transports/core/honoNodeBridge
|
|
5
|
+
*/
|
|
6
|
+
import { PassThrough } from "stream";
|
|
7
|
+
/**
|
|
8
|
+
* A mock ServerResponse that pipes writes to a PassThrough stream.
|
|
9
|
+
* This is the bridge between Model Context Protocol's SDK's Node.js-style response handling
|
|
10
|
+
* and Hono's stream-based body. It captures status and headers.
|
|
11
|
+
*/
|
|
12
|
+
export class HonoStreamResponse extends PassThrough {
|
|
13
|
+
constructor() {
|
|
14
|
+
super();
|
|
15
|
+
this.statusCode = 200;
|
|
16
|
+
this.headers = {};
|
|
17
|
+
}
|
|
18
|
+
writeHead(statusCode, headers) {
|
|
19
|
+
this.statusCode = statusCode;
|
|
20
|
+
if (headers) {
|
|
21
|
+
this.headers = { ...this.headers, ...headers };
|
|
22
|
+
}
|
|
23
|
+
return this;
|
|
24
|
+
}
|
|
25
|
+
setHeader(name, value) {
|
|
26
|
+
this.headers[name.toLowerCase()] = value;
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
getHeader(name) {
|
|
30
|
+
return this.headers[name.toLowerCase()];
|
|
31
|
+
}
|
|
32
|
+
getHeaders() {
|
|
33
|
+
return this.headers;
|
|
34
|
+
}
|
|
35
|
+
removeHeader(name) {
|
|
36
|
+
delete this.headers[name.toLowerCase()];
|
|
37
|
+
}
|
|
38
|
+
write(chunk, encodingOrCallback, callback) {
|
|
39
|
+
const encoding = typeof encodingOrCallback === "string" ? encodingOrCallback : undefined;
|
|
40
|
+
const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
return super.write(chunk, encoding, cb);
|
|
43
|
+
}
|
|
44
|
+
end(chunk, encodingOrCallback, callback) {
|
|
45
|
+
const encoding = typeof encodingOrCallback === "string" ? encodingOrCallback : undefined;
|
|
46
|
+
const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
+
super.end(chunk, encoding, cb);
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Stateful Transport Manager implementation for MCP SDK.
|
|
3
|
+
* This manager handles multiple, persistent sessions, creating a dedicated
|
|
4
|
+
* McpServer and StreamableHTTPServerTransport instance for each one.
|
|
5
|
+
* This version is adapted for Hono by bridging the SDK's Node.js-style
|
|
6
|
+
* request handling with Hono's stream-based response model.
|
|
7
|
+
* @module src/mcp-server/transports/core/statefulTransportManager
|
|
8
|
+
*/
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import type { IncomingHttpHeaders } from "http";
|
|
11
|
+
import { RequestContext } from "../../../utils/index.js";
|
|
12
|
+
import { BaseTransportManager } from "./baseTransportManager.js";
|
|
13
|
+
import { StatefulTransportManager as IStatefulTransportManager, TransportResponse, TransportSession } from "./transportTypes.js";
|
|
14
|
+
/**
|
|
15
|
+
* Stateful Transport Manager that handles MCP SDK integration and session management
|
|
16
|
+
* for a Hono-based HTTP server.
|
|
17
|
+
*/
|
|
18
|
+
export declare class StatefulTransportManager extends BaseTransportManager implements IStatefulTransportManager {
|
|
19
|
+
private readonly transports;
|
|
20
|
+
private readonly servers;
|
|
21
|
+
private readonly sessions;
|
|
22
|
+
private readonly garbageCollector;
|
|
23
|
+
constructor(createServerInstanceFn: () => Promise<McpServer>);
|
|
24
|
+
initializeAndHandle(headers: IncomingHttpHeaders, body: unknown, context: RequestContext): Promise<TransportResponse>;
|
|
25
|
+
handleRequest(headers: IncomingHttpHeaders, body: unknown, context: RequestContext, sessionId?: string): Promise<TransportResponse>;
|
|
26
|
+
handleDeleteRequest(sessionId: string, context: RequestContext): Promise<TransportResponse>;
|
|
27
|
+
getSession(sessionId: string): TransportSession | undefined;
|
|
28
|
+
shutdown(): Promise<void>;
|
|
29
|
+
private closeSession;
|
|
30
|
+
private cleanupStaleSessions;
|
|
31
|
+
}
|