@cyanheads/git-mcp-server 2.2.2 → 2.2.3
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 +63 -124
- package/dist/config/index.js +248 -53
- package/dist/mcp-server/server.js +6 -6
- package/dist/mcp-server/tools/gitCherryPick/logic.js +39 -13
- package/dist/mcp-server/tools/gitCommit/logic.js +1 -1
- package/dist/mcp-server/tools/gitMerge/logic.js +38 -13
- package/dist/mcp-server/tools/gitTag/logic.js +38 -19
- package/dist/mcp-server/transports/auth/authFactory.js +41 -0
- package/dist/mcp-server/transports/auth/authMiddleware.js +57 -0
- package/dist/mcp-server/transports/auth/index.js +6 -4
- 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.js +1 -0
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +113 -0
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +102 -0
- package/dist/mcp-server/transports/core/baseTransportManager.js +19 -0
- package/dist/mcp-server/transports/core/honoNodeBridge.js +51 -0
- package/dist/mcp-server/transports/core/statefulTransportManager.js +234 -0
- package/dist/mcp-server/transports/core/statelessTransportManager.js +92 -0
- package/dist/mcp-server/transports/core/transportTypes.js +5 -0
- package/dist/mcp-server/transports/{httpErrorHandler.js → http/httpErrorHandler.js} +33 -8
- package/dist/mcp-server/transports/http/httpTransport.js +254 -0
- package/dist/mcp-server/transports/http/httpTypes.js +5 -0
- package/dist/mcp-server/transports/http/index.js +6 -0
- package/dist/mcp-server/transports/http/mcpTransportMiddleware.js +63 -0
- package/dist/mcp-server/transports/stdio/index.js +5 -0
- package/dist/mcp-server/transports/{stdioTransport.js → stdio/stdioTransport.js} +10 -5
- package/dist/types-global/errors.js +75 -19
- package/dist/utils/internal/errorHandler.js +11 -13
- package/package.json +18 -7
- package/dist/mcp-server/transports/auth/core/authTypes.js +0 -5
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +0 -149
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +0 -127
- package/dist/mcp-server/transports/httpTransport.js +0 -207
- /package/dist/mcp-server/transports/auth/{core → lib}/authContext.js +0 -0
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT) for Hono.
|
|
3
|
-
*
|
|
4
|
-
* This middleware validates JSON Web Tokens (JWT) passed via the 'Authorization' header
|
|
5
|
-
* using the 'Bearer' scheme (e.g., "Authorization: Bearer <your_token>").
|
|
6
|
-
* It verifies the token's signature and expiration using the secret key defined
|
|
7
|
-
* in the configuration (`config.mcpAuthSecretKey`).
|
|
8
|
-
*
|
|
9
|
-
* If the token is valid, an object conforming to the MCP SDK's `AuthInfo` type
|
|
10
|
-
* is attached to `c.env.incoming.auth`. This direct attachment to the raw Node.js
|
|
11
|
-
* request object is for compatibility with the underlying SDK transport, which is
|
|
12
|
-
* not Hono-context-aware.
|
|
13
|
-
* If the token is missing, invalid, or expired, it throws an `McpError`, which is
|
|
14
|
-
* then handled by the centralized `httpErrorHandler`.
|
|
15
|
-
*
|
|
16
|
-
* @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
|
|
17
|
-
* @module src/mcp-server/transports/auth/strategies/jwt/jwtMiddleware
|
|
18
|
-
*/
|
|
19
|
-
import { jwtVerify } from "jose";
|
|
20
|
-
import { config, environment } from "../../../../../config/index.js";
|
|
21
|
-
import { logger, requestContextService } from "../../../../../utils/index.js";
|
|
22
|
-
import { BaseErrorCode, McpError } from "../../../../../types-global/errors.js";
|
|
23
|
-
import { authContext } from "../../core/authContext.js";
|
|
24
|
-
// Startup Validation: Validate secret key presence on module load.
|
|
25
|
-
if (config.mcpAuthMode === "jwt") {
|
|
26
|
-
if (environment === "production" && !config.mcpAuthSecretKey) {
|
|
27
|
-
logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production environment for JWT auth. Authentication cannot proceed securely.");
|
|
28
|
-
throw new Error("MCP_AUTH_SECRET_KEY must be set in production environment for JWT authentication.");
|
|
29
|
-
}
|
|
30
|
-
else if (!config.mcpAuthSecretKey) {
|
|
31
|
-
logger.warning("MCP_AUTH_SECRET_KEY is not set. JWT auth middleware will bypass checks (DEVELOPMENT ONLY). This is insecure for production.");
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Hono middleware for verifying JWT Bearer token authentication.
|
|
36
|
-
* It attaches authentication info to `c.env.incoming.auth` for SDK compatibility with the node server.
|
|
37
|
-
*/
|
|
38
|
-
export async function mcpAuthMiddleware(c, next) {
|
|
39
|
-
const context = requestContextService.createRequestContext({
|
|
40
|
-
operation: "mcpAuthMiddleware",
|
|
41
|
-
method: c.req.method,
|
|
42
|
-
path: c.req.path,
|
|
43
|
-
});
|
|
44
|
-
logger.debug("Running MCP Authentication Middleware (Bearer Token Validation)...", context);
|
|
45
|
-
const reqWithAuth = c.env.incoming;
|
|
46
|
-
// If JWT auth is not enabled, skip the middleware.
|
|
47
|
-
if (config.mcpAuthMode !== "jwt") {
|
|
48
|
-
return await next();
|
|
49
|
-
}
|
|
50
|
-
// Development Mode Bypass
|
|
51
|
-
if (!config.mcpAuthSecretKey) {
|
|
52
|
-
if (environment !== "production") {
|
|
53
|
-
logger.warning("Bypassing JWT authentication: MCP_AUTH_SECRET_KEY is not set (DEVELOPMENT ONLY).", context);
|
|
54
|
-
reqWithAuth.auth = {
|
|
55
|
-
token: "dev-mode-placeholder-token",
|
|
56
|
-
clientId: "dev-client-id",
|
|
57
|
-
scopes: ["dev-scope"],
|
|
58
|
-
};
|
|
59
|
-
const authInfo = reqWithAuth.auth;
|
|
60
|
-
logger.debug("Dev mode auth object created.", {
|
|
61
|
-
...context,
|
|
62
|
-
authDetails: authInfo,
|
|
63
|
-
});
|
|
64
|
-
return await authContext.run({ authInfo }, next);
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
logger.error("FATAL: MCP_AUTH_SECRET_KEY is missing in production. Cannot bypass auth.", context);
|
|
68
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Server configuration error: Authentication key missing.");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
const secretKey = new TextEncoder().encode(config.mcpAuthSecretKey);
|
|
72
|
-
const authHeader = c.req.header("Authorization");
|
|
73
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
74
|
-
logger.warning("Authentication failed: Missing or malformed Authorization header (Bearer scheme required).", context);
|
|
75
|
-
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Missing or invalid authentication token format.");
|
|
76
|
-
}
|
|
77
|
-
const tokenParts = authHeader.split(" ");
|
|
78
|
-
if (tokenParts.length !== 2 || tokenParts[0] !== "Bearer" || !tokenParts[1]) {
|
|
79
|
-
logger.warning("Authentication failed: Malformed Bearer token.", context);
|
|
80
|
-
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Malformed authentication token.");
|
|
81
|
-
}
|
|
82
|
-
const rawToken = tokenParts[1];
|
|
83
|
-
try {
|
|
84
|
-
const { payload: decoded } = await jwtVerify(rawToken, secretKey);
|
|
85
|
-
const clientIdFromToken = typeof decoded.cid === "string"
|
|
86
|
-
? decoded.cid
|
|
87
|
-
: typeof decoded.client_id === "string"
|
|
88
|
-
? decoded.client_id
|
|
89
|
-
: undefined;
|
|
90
|
-
if (!clientIdFromToken) {
|
|
91
|
-
logger.warning("Authentication failed: JWT 'cid' or 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
|
|
92
|
-
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Invalid token, missing client identifier.");
|
|
93
|
-
}
|
|
94
|
-
let scopesFromToken = [];
|
|
95
|
-
if (Array.isArray(decoded.scp) &&
|
|
96
|
-
decoded.scp.every((s) => typeof s === "string")) {
|
|
97
|
-
scopesFromToken = decoded.scp;
|
|
98
|
-
}
|
|
99
|
-
else if (typeof decoded.scope === "string" &&
|
|
100
|
-
decoded.scope.trim() !== "") {
|
|
101
|
-
scopesFromToken = decoded.scope.split(" ").filter((s) => s);
|
|
102
|
-
if (scopesFromToken.length === 0 && decoded.scope.trim() !== "") {
|
|
103
|
-
scopesFromToken = [decoded.scope.trim()];
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (scopesFromToken.length === 0) {
|
|
107
|
-
logger.warning("Authentication failed: Token resulted in an empty scope array, and scopes are required.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
|
|
108
|
-
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain valid, non-empty scopes.");
|
|
109
|
-
}
|
|
110
|
-
reqWithAuth.auth = {
|
|
111
|
-
token: rawToken,
|
|
112
|
-
clientId: clientIdFromToken,
|
|
113
|
-
scopes: scopesFromToken,
|
|
114
|
-
};
|
|
115
|
-
const subClaimForLogging = typeof decoded.sub === "string" ? decoded.sub : undefined;
|
|
116
|
-
const authInfo = reqWithAuth.auth;
|
|
117
|
-
logger.debug("JWT verified successfully. AuthInfo attached to request.", {
|
|
118
|
-
...context,
|
|
119
|
-
mcpSessionIdContext: subClaimForLogging,
|
|
120
|
-
clientId: authInfo.clientId,
|
|
121
|
-
scopes: authInfo.scopes,
|
|
122
|
-
});
|
|
123
|
-
await authContext.run({ authInfo }, next);
|
|
124
|
-
}
|
|
125
|
-
catch (error) {
|
|
126
|
-
let errorMessage = "Invalid token.";
|
|
127
|
-
let errorCode = BaseErrorCode.UNAUTHORIZED;
|
|
128
|
-
if (error instanceof Error && error.name === "JWTExpired") {
|
|
129
|
-
errorMessage = "Token expired.";
|
|
130
|
-
logger.warning("Authentication failed: Token expired.", {
|
|
131
|
-
...context,
|
|
132
|
-
errorName: error.name,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
else if (error instanceof Error) {
|
|
136
|
-
errorMessage = `Invalid token: ${error.message}`;
|
|
137
|
-
logger.warning(`Authentication failed: ${errorMessage}`, {
|
|
138
|
-
...context,
|
|
139
|
-
errorName: error.name,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
errorMessage = "Unknown verification error.";
|
|
144
|
-
errorCode = BaseErrorCode.INTERNAL_ERROR;
|
|
145
|
-
logger.error("Authentication failed: Unexpected non-error exception during token verification.", { ...context, error });
|
|
146
|
-
}
|
|
147
|
-
throw new McpError(errorCode, errorMessage);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Hono middleware for OAuth 2.1 Bearer Token validation.
|
|
3
|
-
* This middleware extracts a JWT from the Authorization header, validates it against
|
|
4
|
-
* a remote JWKS (JSON Web Key Set), and checks its issuer and audience claims.
|
|
5
|
-
* On success, it populates an AuthInfo object and stores it in an AsyncLocalStorage
|
|
6
|
-
* context for use in downstream handlers.
|
|
7
|
-
*
|
|
8
|
-
* @module src/mcp-server/transports/auth/strategies/oauth/oauthMiddleware
|
|
9
|
-
*/
|
|
10
|
-
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
11
|
-
import { config } from "../../../../../config/index.js";
|
|
12
|
-
import { BaseErrorCode, McpError } from "../../../../../types-global/errors.js";
|
|
13
|
-
import { ErrorHandler } from "../../../../../utils/internal/errorHandler.js";
|
|
14
|
-
import { logger, requestContextService } from "../../../../../utils/index.js";
|
|
15
|
-
import { authContext } from "../../core/authContext.js";
|
|
16
|
-
// --- Startup Validation ---
|
|
17
|
-
// Ensures that necessary OAuth configuration is present when the mode is 'oauth'.
|
|
18
|
-
if (config.mcpAuthMode === "oauth") {
|
|
19
|
-
if (!config.oauthIssuerUrl) {
|
|
20
|
-
throw new Error("OAUTH_ISSUER_URL must be set when MCP_AUTH_MODE is 'oauth'");
|
|
21
|
-
}
|
|
22
|
-
if (!config.oauthAudience) {
|
|
23
|
-
throw new Error("OAUTH_AUDIENCE must be set when MCP_AUTH_MODE is 'oauth'");
|
|
24
|
-
}
|
|
25
|
-
logger.info("OAuth 2.1 mode enabled. Verifying tokens against issuer.", requestContextService.createRequestContext({
|
|
26
|
-
issuer: config.oauthIssuerUrl,
|
|
27
|
-
audience: config.oauthAudience,
|
|
28
|
-
}));
|
|
29
|
-
}
|
|
30
|
-
// --- JWKS Client Initialization ---
|
|
31
|
-
// The remote JWK set is fetched and cached to avoid network calls on every request.
|
|
32
|
-
let jwks;
|
|
33
|
-
if (config.mcpAuthMode === "oauth" && config.oauthIssuerUrl) {
|
|
34
|
-
try {
|
|
35
|
-
const jwksUrl = new URL(config.oauthJwksUri ||
|
|
36
|
-
`${config.oauthIssuerUrl.replace(/\/$/, "")}/.well-known/jwks.json`);
|
|
37
|
-
jwks = createRemoteJWKSet(jwksUrl, {
|
|
38
|
-
cooldownDuration: 300000, // 5 minutes
|
|
39
|
-
timeoutDuration: 5000, // 5 seconds
|
|
40
|
-
});
|
|
41
|
-
logger.info(`JWKS client initialized for URL: ${jwksUrl.href}`, requestContextService.createRequestContext({
|
|
42
|
-
operation: "oauthMiddlewareSetup",
|
|
43
|
-
}));
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
logger.fatal("Failed to initialize JWKS client.", {
|
|
47
|
-
error: error,
|
|
48
|
-
context: requestContextService.createRequestContext({
|
|
49
|
-
operation: "oauthMiddlewareSetup",
|
|
50
|
-
}),
|
|
51
|
-
});
|
|
52
|
-
// Prevent server from starting if JWKS setup fails in oauth mode
|
|
53
|
-
process.exit(1);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Hono middleware for verifying OAuth 2.1 JWT Bearer tokens.
|
|
58
|
-
* It validates the token and uses AsyncLocalStorage to pass auth info.
|
|
59
|
-
* @param c - The Hono context object.
|
|
60
|
-
* @param next - The function to call to proceed to the next middleware.
|
|
61
|
-
*/
|
|
62
|
-
export async function oauthMiddleware(c, next) {
|
|
63
|
-
// If OAuth is not the configured auth mode, skip this middleware.
|
|
64
|
-
if (config.mcpAuthMode !== "oauth") {
|
|
65
|
-
return await next();
|
|
66
|
-
}
|
|
67
|
-
const context = requestContextService.createRequestContext({
|
|
68
|
-
operation: "oauthMiddleware",
|
|
69
|
-
httpMethod: c.req.method,
|
|
70
|
-
httpPath: c.req.path,
|
|
71
|
-
});
|
|
72
|
-
if (!jwks) {
|
|
73
|
-
// This should not happen if startup validation is correct, but it's a safeguard.
|
|
74
|
-
// This should not happen if startup validation is correct, but it's a safeguard.
|
|
75
|
-
throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, "OAuth middleware is active, but JWKS client is not initialized.", context);
|
|
76
|
-
}
|
|
77
|
-
const authHeader = c.req.header("Authorization");
|
|
78
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
79
|
-
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Missing or invalid token format.");
|
|
80
|
-
}
|
|
81
|
-
const token = authHeader.substring(7);
|
|
82
|
-
try {
|
|
83
|
-
const { payload } = await jwtVerify(token, jwks, {
|
|
84
|
-
issuer: config.oauthIssuerUrl,
|
|
85
|
-
audience: config.oauthAudience,
|
|
86
|
-
});
|
|
87
|
-
// The 'scope' claim is typically a space-delimited string in OAuth 2.1.
|
|
88
|
-
const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : [];
|
|
89
|
-
if (scopes.length === 0) {
|
|
90
|
-
logger.warning("Authentication failed: Token contains no scopes, but scopes are required.", { ...context, jwtPayloadKeys: Object.keys(payload) });
|
|
91
|
-
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain valid, non-empty scopes.");
|
|
92
|
-
}
|
|
93
|
-
const clientId = typeof payload.client_id === "string" ? payload.client_id : undefined;
|
|
94
|
-
if (!clientId) {
|
|
95
|
-
logger.warning("Authentication failed: OAuth token 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(payload) });
|
|
96
|
-
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Invalid token, missing client identifier.");
|
|
97
|
-
}
|
|
98
|
-
const authInfo = {
|
|
99
|
-
token,
|
|
100
|
-
clientId,
|
|
101
|
-
scopes,
|
|
102
|
-
subject: typeof payload.sub === "string" ? payload.sub : undefined,
|
|
103
|
-
};
|
|
104
|
-
// Attach to the raw request for potential legacy compatibility and
|
|
105
|
-
// store in AsyncLocalStorage for modern, safe access in handlers.
|
|
106
|
-
c.env.incoming.auth = authInfo;
|
|
107
|
-
await authContext.run({ authInfo }, next);
|
|
108
|
-
}
|
|
109
|
-
catch (error) {
|
|
110
|
-
if (error instanceof Error && error.name === "JWTExpired") {
|
|
111
|
-
logger.warning("Authentication failed: OAuth token expired.", context);
|
|
112
|
-
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token expired.");
|
|
113
|
-
}
|
|
114
|
-
const handledError = ErrorHandler.handleError(error, {
|
|
115
|
-
operation: "oauthMiddleware",
|
|
116
|
-
context,
|
|
117
|
-
rethrow: false, // We will throw a new McpError below
|
|
118
|
-
});
|
|
119
|
-
// Ensure we always throw an McpError for consistency
|
|
120
|
-
if (handledError instanceof McpError) {
|
|
121
|
-
throw handledError;
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
throw new McpError(BaseErrorCode.UNAUTHORIZED, `Unauthorized: ${handledError.message || "Invalid token"}`, { originalError: handledError.name });
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Configures and starts the Streamable HTTP MCP transport using Hono.
|
|
3
|
-
* This module integrates the `@modelcontextprotocol/sdk`'s `StreamableHTTPServerTransport`
|
|
4
|
-
* into a Hono web server. Its responsibilities include:
|
|
5
|
-
* - Creating a Hono server instance.
|
|
6
|
-
* - Applying and configuring middleware for CORS, rate limiting, and authentication (JWT/OAuth).
|
|
7
|
-
* - Defining the routes (`/mcp` endpoint for POST, GET, DELETE) to handle the MCP lifecycle.
|
|
8
|
-
* - Orchestrating session management by mapping session IDs to SDK transport instances.
|
|
9
|
-
* - Implementing port-binding logic with automatic retry on conflicts.
|
|
10
|
-
*
|
|
11
|
-
* The underlying implementation of the MCP Streamable HTTP specification, including
|
|
12
|
-
* Server-Sent Events (SSE) for streaming, is handled by the SDK's transport class.
|
|
13
|
-
*
|
|
14
|
-
* Specification Reference:
|
|
15
|
-
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
|
|
16
|
-
* @module src/mcp-server/transports/httpTransport
|
|
17
|
-
*/
|
|
18
|
-
import { serve } from "@hono/node-server";
|
|
19
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
20
|
-
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
21
|
-
import { Hono } from "hono";
|
|
22
|
-
import { cors } from "hono/cors";
|
|
23
|
-
import http from "http";
|
|
24
|
-
import { randomUUID } from "node:crypto";
|
|
25
|
-
import { config } from "../../config/index.js";
|
|
26
|
-
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
|
|
27
|
-
import { logger, rateLimiter, requestContextService, } from "../../utils/index.js";
|
|
28
|
-
import { jwtAuthMiddleware, oauthMiddleware, } from "./auth/index.js";
|
|
29
|
-
import { httpErrorHandler } from "./httpErrorHandler.js";
|
|
30
|
-
const HTTP_PORT = config.mcpHttpPort;
|
|
31
|
-
const HTTP_HOST = config.mcpHttpHost;
|
|
32
|
-
const MCP_ENDPOINT_PATH = "/mcp";
|
|
33
|
-
const MAX_PORT_RETRIES = 15;
|
|
34
|
-
// The transports map will store active sessions, keyed by session ID.
|
|
35
|
-
// NOTE: This is an in-memory session store, which is a known limitation for scalability.
|
|
36
|
-
// It will not work in a multi-process (clustered) or serverless environment.
|
|
37
|
-
// For a scalable deployment, this would need to be replaced with a distributed
|
|
38
|
-
// store like Redis or Memcached.
|
|
39
|
-
const transports = {};
|
|
40
|
-
async function isPortInUse(port, host, parentContext) {
|
|
41
|
-
requestContextService.createRequestContext({
|
|
42
|
-
...parentContext,
|
|
43
|
-
operation: "isPortInUse",
|
|
44
|
-
port,
|
|
45
|
-
host,
|
|
46
|
-
});
|
|
47
|
-
return new Promise((resolve) => {
|
|
48
|
-
const tempServer = http.createServer();
|
|
49
|
-
tempServer
|
|
50
|
-
.once("error", (err) => {
|
|
51
|
-
resolve(err.code === "EADDRINUSE");
|
|
52
|
-
})
|
|
53
|
-
.once("listening", () => {
|
|
54
|
-
tempServer.close(() => resolve(false));
|
|
55
|
-
})
|
|
56
|
-
.listen(port, host);
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentContext) {
|
|
60
|
-
const startContext = requestContextService.createRequestContext({
|
|
61
|
-
...parentContext,
|
|
62
|
-
operation: "startHttpServerWithRetry",
|
|
63
|
-
});
|
|
64
|
-
return new Promise(async (resolve, reject) => {
|
|
65
|
-
for (let i = 0; i <= maxRetries; i++) {
|
|
66
|
-
const currentPort = initialPort + i;
|
|
67
|
-
const attemptContext = {
|
|
68
|
-
...startContext,
|
|
69
|
-
port: currentPort,
|
|
70
|
-
attempt: i + 1,
|
|
71
|
-
};
|
|
72
|
-
if (await isPortInUse(currentPort, host, attemptContext)) {
|
|
73
|
-
logger.warning(`Port ${currentPort} is in use, retrying...`, attemptContext);
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
const serverInstance = serve({ fetch: app.fetch, port: currentPort, hostname: host }, (info) => {
|
|
78
|
-
const serverAddress = `http://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
|
|
79
|
-
logger.info(`HTTP transport listening at ${serverAddress}`, {
|
|
80
|
-
...attemptContext,
|
|
81
|
-
address: serverAddress,
|
|
82
|
-
});
|
|
83
|
-
if (process.stdout.isTTY) {
|
|
84
|
-
console.log(`\n🚀 MCP Server running at: ${serverAddress}\n`);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
resolve(serverInstance);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
catch (err) {
|
|
91
|
-
if (err.code !== "EADDRINUSE") {
|
|
92
|
-
reject(err);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
reject(new Error("Failed to bind to any port after multiple retries."));
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
export async function startHttpTransport(server, parentContext) {
|
|
101
|
-
const app = new Hono();
|
|
102
|
-
const transportContext = requestContextService.createRequestContext({
|
|
103
|
-
...parentContext,
|
|
104
|
-
component: "HttpTransportSetup",
|
|
105
|
-
});
|
|
106
|
-
app.use("*", cors({
|
|
107
|
-
origin: config.mcpAllowedOrigins || [],
|
|
108
|
-
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
109
|
-
allowHeaders: [
|
|
110
|
-
"Content-Type",
|
|
111
|
-
"Mcp-Session-Id",
|
|
112
|
-
"Last-Event-ID",
|
|
113
|
-
"Authorization",
|
|
114
|
-
],
|
|
115
|
-
credentials: true,
|
|
116
|
-
}));
|
|
117
|
-
app.use("*", async (c, next) => {
|
|
118
|
-
c.res.headers.set("X-Content-Type-Options", "nosniff");
|
|
119
|
-
await next();
|
|
120
|
-
});
|
|
121
|
-
app.use(MCP_ENDPOINT_PATH, async (c, next) => {
|
|
122
|
-
// NOTE (Security): The 'x-forwarded-for' header is used for rate limiting.
|
|
123
|
-
// This is only secure if the server is run behind a trusted proxy that
|
|
124
|
-
// correctly sets or validates this header.
|
|
125
|
-
const clientIp = c.req.header("x-forwarded-for")?.split(",")[0].trim() || "unknown_ip";
|
|
126
|
-
const context = requestContextService.createRequestContext({
|
|
127
|
-
operation: "httpRateLimitCheck",
|
|
128
|
-
ipAddress: clientIp,
|
|
129
|
-
});
|
|
130
|
-
// Let the centralized error handler catch rate limit errors
|
|
131
|
-
rateLimiter.check(clientIp, context);
|
|
132
|
-
await next();
|
|
133
|
-
});
|
|
134
|
-
if (config.mcpAuthMode === "oauth") {
|
|
135
|
-
app.use(MCP_ENDPOINT_PATH, oauthMiddleware);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
app.use(MCP_ENDPOINT_PATH, jwtAuthMiddleware);
|
|
139
|
-
}
|
|
140
|
-
// Centralized Error Handling
|
|
141
|
-
app.onError(httpErrorHandler);
|
|
142
|
-
app.post(MCP_ENDPOINT_PATH, async (c) => {
|
|
143
|
-
const postContext = requestContextService.createRequestContext({
|
|
144
|
-
...transportContext,
|
|
145
|
-
operation: "handlePost",
|
|
146
|
-
});
|
|
147
|
-
const body = await c.req.json();
|
|
148
|
-
const sessionId = c.req.header("mcp-session-id");
|
|
149
|
-
let transport = sessionId
|
|
150
|
-
? transports[sessionId]
|
|
151
|
-
: undefined;
|
|
152
|
-
if (isInitializeRequest(body)) {
|
|
153
|
-
// If a transport already exists for a session, it's a re-initialization.
|
|
154
|
-
if (transport) {
|
|
155
|
-
logger.warning("Re-initializing existing session.", {
|
|
156
|
-
...postContext,
|
|
157
|
-
sessionId,
|
|
158
|
-
});
|
|
159
|
-
await transport.close(); // This will trigger the onclose handler.
|
|
160
|
-
}
|
|
161
|
-
// Create a new transport for a new session.
|
|
162
|
-
const newTransport = new StreamableHTTPServerTransport({
|
|
163
|
-
sessionIdGenerator: () => randomUUID(),
|
|
164
|
-
onsessioninitialized: (newId) => {
|
|
165
|
-
transports[newId] = newTransport;
|
|
166
|
-
logger.info(`HTTP Session created: ${newId}`, {
|
|
167
|
-
...postContext,
|
|
168
|
-
newSessionId: newId,
|
|
169
|
-
});
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
// Set up cleanup logic for when the transport is closed.
|
|
173
|
-
newTransport.onclose = () => {
|
|
174
|
-
const closedSessionId = newTransport.sessionId;
|
|
175
|
-
if (closedSessionId && transports[closedSessionId]) {
|
|
176
|
-
delete transports[closedSessionId];
|
|
177
|
-
logger.info(`HTTP Session closed: ${closedSessionId}`, {
|
|
178
|
-
...postContext,
|
|
179
|
-
closedSessionId,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
// Connect the new transport to the existing server instance.
|
|
184
|
-
await server.connect(newTransport);
|
|
185
|
-
transport = newTransport;
|
|
186
|
-
}
|
|
187
|
-
else if (!transport) {
|
|
188
|
-
// If it's not an initialization request and no transport was found, it's an error.
|
|
189
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, "Invalid or expired session ID.");
|
|
190
|
-
}
|
|
191
|
-
// Pass the request to the transport to handle.
|
|
192
|
-
return await transport.handleRequest(c.env.incoming, c.env.outgoing, body);
|
|
193
|
-
});
|
|
194
|
-
// A reusable handler for GET and DELETE requests which operate on existing sessions.
|
|
195
|
-
const handleSessionRequest = async (c) => {
|
|
196
|
-
const sessionId = c.req.header("mcp-session-id");
|
|
197
|
-
const transport = sessionId ? transports[sessionId] : undefined;
|
|
198
|
-
if (!transport) {
|
|
199
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, "Session not found or expired.");
|
|
200
|
-
}
|
|
201
|
-
// Let the transport handle the streaming (GET) or termination (DELETE) request.
|
|
202
|
-
return await transport.handleRequest(c.env.incoming, c.env.outgoing);
|
|
203
|
-
};
|
|
204
|
-
app.get(MCP_ENDPOINT_PATH, handleSessionRequest);
|
|
205
|
-
app.delete(MCP_ENDPOINT_PATH, handleSessionRequest);
|
|
206
|
-
return startHttpServerWithRetry(app, HTTP_PORT, HTTP_HOST, MAX_PORT_RETRIES, transportContext);
|
|
207
|
-
}
|
|
File without changes
|