@cyanheads/git-mcp-server 2.1.2 → 2.1.4
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 +92 -72
- package/dist/config/index.js +10 -2
- package/dist/mcp-server/server.js +33 -32
- package/dist/mcp-server/transports/auth/core/authContext.js +24 -0
- package/dist/mcp-server/transports/auth/core/authTypes.js +5 -0
- package/dist/mcp-server/transports/auth/core/authUtils.js +45 -0
- package/dist/mcp-server/transports/auth/index.js +9 -0
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +149 -0
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +127 -0
- package/dist/mcp-server/transports/httpErrorHandler.js +73 -0
- package/dist/mcp-server/transports/httpTransport.js +149 -495
- package/dist/mcp-server/transports/stdioTransport.js +18 -48
- package/package.json +10 -8
- package/dist/mcp-server/transports/authentication/authMiddleware.js +0 -167
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Centralized error handler for the Hono HTTP transport.
|
|
3
|
+
* This middleware intercepts errors that occur during request processing,
|
|
4
|
+
* standardizes them using the application's ErrorHandler utility, and
|
|
5
|
+
* formats them into a consistent JSON-RPC error response.
|
|
6
|
+
* @module src/mcp-server/transports/httpErrorHandler
|
|
7
|
+
*/
|
|
8
|
+
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
|
|
9
|
+
import { ErrorHandler, requestContextService } from "../../utils/index.js";
|
|
10
|
+
/**
|
|
11
|
+
* A centralized error handling middleware for Hono.
|
|
12
|
+
* This function is registered with `app.onError()` and will catch any errors
|
|
13
|
+
* thrown from preceding middleware or route handlers.
|
|
14
|
+
*
|
|
15
|
+
* @param err - The error that was thrown.
|
|
16
|
+
* @param c - The Hono context object for the request.
|
|
17
|
+
* @returns A Response object containing the formatted JSON-RPC error.
|
|
18
|
+
*/
|
|
19
|
+
export const httpErrorHandler = async (err, c) => {
|
|
20
|
+
const context = requestContextService.createRequestContext({
|
|
21
|
+
operation: "httpErrorHandler",
|
|
22
|
+
path: c.req.path,
|
|
23
|
+
method: c.req.method,
|
|
24
|
+
});
|
|
25
|
+
const handledError = ErrorHandler.handleError(err, {
|
|
26
|
+
operation: "httpTransport",
|
|
27
|
+
context,
|
|
28
|
+
});
|
|
29
|
+
let status = 500;
|
|
30
|
+
if (handledError instanceof McpError) {
|
|
31
|
+
switch (handledError.code) {
|
|
32
|
+
case BaseErrorCode.NOT_FOUND:
|
|
33
|
+
status = 404;
|
|
34
|
+
break;
|
|
35
|
+
case BaseErrorCode.UNAUTHORIZED:
|
|
36
|
+
status = 401;
|
|
37
|
+
break;
|
|
38
|
+
case BaseErrorCode.FORBIDDEN:
|
|
39
|
+
status = 403;
|
|
40
|
+
break;
|
|
41
|
+
case BaseErrorCode.VALIDATION_ERROR:
|
|
42
|
+
status = 400;
|
|
43
|
+
break;
|
|
44
|
+
case BaseErrorCode.CONFLICT:
|
|
45
|
+
status = 409;
|
|
46
|
+
break;
|
|
47
|
+
case BaseErrorCode.RATE_LIMITED:
|
|
48
|
+
status = 429;
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
status = 500;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Attempt to get the request ID from the body, but don't fail if it's not there or unreadable.
|
|
55
|
+
let requestId = null;
|
|
56
|
+
try {
|
|
57
|
+
const body = await c.req.json();
|
|
58
|
+
requestId = body?.id || null;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Ignore parsing errors, requestId will remain null
|
|
62
|
+
}
|
|
63
|
+
const errorCode = handledError instanceof McpError ? handledError.code : -32603;
|
|
64
|
+
c.status(status);
|
|
65
|
+
return c.json({
|
|
66
|
+
jsonrpc: "2.0",
|
|
67
|
+
error: {
|
|
68
|
+
code: errorCode,
|
|
69
|
+
message: handledError.message,
|
|
70
|
+
},
|
|
71
|
+
id: requestId,
|
|
72
|
+
});
|
|
73
|
+
};
|