@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Handles the setup and connection for the Stdio MCP transport.
|
|
2
|
+
* @fileoverview Handles the setup and connection for the Stdio MCP transport.
|
|
3
3
|
* Implements the MCP Specification 2025-03-26 for stdio transport.
|
|
4
4
|
* This transport communicates directly over standard input (stdin) and
|
|
5
5
|
* standard output (stdout), typically used when the MCP server is launched
|
|
@@ -15,79 +15,49 @@
|
|
|
15
15
|
* controlling the server process. This implementation follows that guideline.
|
|
16
16
|
*
|
|
17
17
|
* @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
|
|
18
|
+
* @module src/mcp-server/transports/stdioTransport
|
|
18
19
|
*/
|
|
19
20
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
-
// Import core utilities: ErrorHandler for centralized error management and logger for logging.
|
|
21
21
|
import { ErrorHandler, logger } from "../../utils/index.js";
|
|
22
|
-
// --- Stdio Session State ---
|
|
23
|
-
// Since stdio typically involves a single, persistent connection managed by a parent,
|
|
24
|
-
// we manage a single working directory state for the entire process.
|
|
25
|
-
let currentWorkingDirectory = undefined; // Initialize as undefined
|
|
26
22
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return currentWorkingDirectory;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Sets the working directory for the stdio session.
|
|
35
|
-
* @param {string} dir - The new working directory path.
|
|
36
|
-
*/
|
|
37
|
-
export function setStdioWorkingDirectory(dir) {
|
|
38
|
-
currentWorkingDirectory = dir;
|
|
39
|
-
logger.info(`Stdio working directory set to: ${dir}`, {
|
|
40
|
-
operation: "setStdioWorkingDirectory",
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Connects a given McpServer instance to the Stdio transport. (Asynchronous)
|
|
45
|
-
* Initializes the SDK's StdioServerTransport, which handles reading newline-delimited
|
|
46
|
-
* JSON-RPC messages from process.stdin and writing corresponding messages to process.stdout,
|
|
47
|
-
* adhering to the MCP stdio transport specification.
|
|
23
|
+
* Connects a given `McpServer` instance to the Stdio transport.
|
|
24
|
+
* This function initializes the SDK's `StdioServerTransport`, which manages
|
|
25
|
+
* communication over `process.stdin` and `process.stdout` according to the
|
|
26
|
+
* MCP stdio transport specification.
|
|
48
27
|
*
|
|
49
|
-
* MCP Spec Points Covered by SDK's StdioServerTransport
|
|
28
|
+
* MCP Spec Points Covered by SDK's `StdioServerTransport`:
|
|
50
29
|
* - Reads JSON-RPC messages (requests, notifications, responses, batches) from stdin.
|
|
51
30
|
* - Writes JSON-RPC messages to stdout.
|
|
52
31
|
* - Handles newline delimiters and ensures no embedded newlines in output messages.
|
|
53
32
|
* - Ensures only valid MCP messages are written to stdout.
|
|
54
33
|
*
|
|
55
|
-
*
|
|
34
|
+
* Logging via the `logger` utility MAY result in output to stderr, which is
|
|
56
35
|
* permitted by the spec for logging purposes.
|
|
57
36
|
*
|
|
58
|
-
* @param
|
|
59
|
-
* @param
|
|
60
|
-
* @returns
|
|
61
|
-
* @throws {Error}
|
|
37
|
+
* @param server - The `McpServer` instance.
|
|
38
|
+
* @param parentContext - The logging and tracing context from the calling function.
|
|
39
|
+
* @returns A promise that resolves when the Stdio transport is successfully connected.
|
|
40
|
+
* @throws {Error} If the connection fails during setup.
|
|
62
41
|
*/
|
|
63
|
-
export async function connectStdioTransport(server,
|
|
64
|
-
// Add a specific operation name to the context for better log filtering.
|
|
42
|
+
export async function connectStdioTransport(server, parentContext) {
|
|
65
43
|
const operationContext = {
|
|
66
|
-
...
|
|
44
|
+
...parentContext,
|
|
67
45
|
operation: "connectStdioTransport",
|
|
68
46
|
transportType: "Stdio",
|
|
69
47
|
};
|
|
70
48
|
logger.debug("Attempting to connect stdio transport...", operationContext);
|
|
71
49
|
try {
|
|
72
50
|
logger.debug("Creating StdioServerTransport instance...", operationContext);
|
|
73
|
-
// Instantiate the transport provided by the SDK for standard I/O communication.
|
|
74
|
-
// This class encapsulates the logic for reading from stdin and writing to stdout
|
|
75
|
-
// according to the MCP stdio spec.
|
|
76
51
|
const transport = new StdioServerTransport();
|
|
77
52
|
logger.debug("Connecting McpServer instance to StdioServerTransport...", operationContext);
|
|
78
|
-
// Establish the link between the server's core logic and the transport layer.
|
|
79
|
-
// This internally starts the necessary listeners on process.stdin.
|
|
80
53
|
await server.connect(transport);
|
|
81
|
-
// Log successful connection. The server is now ready to process messages via stdio.
|
|
82
54
|
logger.info("MCP Server connected and listening via stdio transport.", operationContext);
|
|
83
|
-
|
|
84
|
-
|
|
55
|
+
if (process.stdout.isTTY) {
|
|
56
|
+
console.log(`\nš MCP Server running in STDIO mode.\n (MCP Spec: 2025-03-26 Stdio Transport)\n`);
|
|
57
|
+
}
|
|
85
58
|
}
|
|
86
59
|
catch (err) {
|
|
87
|
-
// Catch and handle any critical errors during the transport connection setup.
|
|
88
|
-
// Mark as critical because the server cannot function without a connected transport.
|
|
89
60
|
ErrorHandler.handleError(err, { ...operationContext, critical: true });
|
|
90
|
-
//
|
|
91
|
-
throw err;
|
|
61
|
+
throw err; // Re-throw after handling to allow caller to react if necessary
|
|
92
62
|
}
|
|
93
63
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/git-mcp-server",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
4
4
|
"description": "An MCP (Model Context Protocol) server enabling LLMs and AI agents to interact with Git repositories. Provides tools for comprehensive Git operations including clone, commit, branch, diff, log, status, push, pull, merge, rebase, worktree, tag management, and more, via the MCP standard. STDIO & HTTP.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -35,21 +35,23 @@
|
|
|
35
35
|
"clean": "ts-node --esm scripts/clean.ts"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@
|
|
39
|
-
"@modelcontextprotocol/
|
|
40
|
-
"@
|
|
41
|
-
"@types/
|
|
38
|
+
"@hono/node-server": "^1.14.4",
|
|
39
|
+
"@modelcontextprotocol/inspector": "^0.14.3",
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.13.0",
|
|
41
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
42
|
+
"@types/node": "^24.0.3",
|
|
42
43
|
"@types/sanitize-html": "^2.16.0",
|
|
43
|
-
"@types/validator": "^13.15.
|
|
44
|
+
"@types/validator": "^13.15.2",
|
|
44
45
|
"chalk": "^5.4.1",
|
|
45
46
|
"chrono-node": "2.8.0",
|
|
46
47
|
"cli-table3": "^0.6.5",
|
|
47
48
|
"dotenv": "^16.5.0",
|
|
48
49
|
"express": "^5.1.0",
|
|
50
|
+
"hono": "^4.8.2",
|
|
49
51
|
"ignore": "^7.0.5",
|
|
50
52
|
"jose": "^6.0.11",
|
|
51
53
|
"jsonwebtoken": "^9.0.2",
|
|
52
|
-
"openai": "^5.
|
|
54
|
+
"openai": "^5.6.0",
|
|
53
55
|
"partial-json": "^0.1.7",
|
|
54
56
|
"sanitize-html": "^2.17.0",
|
|
55
57
|
"tiktoken": "^1.0.21",
|
|
@@ -59,7 +61,7 @@
|
|
|
59
61
|
"winston": "^3.17.0",
|
|
60
62
|
"winston-daily-rotate-file": "^5.0.0",
|
|
61
63
|
"yargs": "^18.0.0",
|
|
62
|
-
"zod": "^3.25.
|
|
64
|
+
"zod": "^3.25.67"
|
|
63
65
|
},
|
|
64
66
|
"keywords": [
|
|
65
67
|
"typescript",
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT).
|
|
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
|
-
* (expected to contain `token`, `clientId`, and `scopes`) is attached to `req.auth`.
|
|
11
|
-
* If the token is missing, invalid, or expired, it sends an HTTP 401 Unauthorized response.
|
|
12
|
-
*
|
|
13
|
-
* @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
|
|
14
|
-
* @module src/mcp-server/transports/authentication/authMiddleware
|
|
15
|
-
*/
|
|
16
|
-
import jwt from "jsonwebtoken";
|
|
17
|
-
import { config, environment } from "../../../config/index.js";
|
|
18
|
-
import { logger, requestContextService } from "../../../utils/index.js";
|
|
19
|
-
// Startup Validation: Validate secret key presence on module load.
|
|
20
|
-
if (environment === "production" && !config.security.mcpAuthSecretKey) {
|
|
21
|
-
logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production environment. Authentication cannot proceed securely.");
|
|
22
|
-
throw new Error("MCP_AUTH_SECRET_KEY must be set in production environment for JWT authentication.");
|
|
23
|
-
}
|
|
24
|
-
else if (!config.security.mcpAuthSecretKey) {
|
|
25
|
-
logger.warning("MCP_AUTH_SECRET_KEY is not set. Authentication middleware will bypass checks (DEVELOPMENT ONLY). This is insecure for production.");
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Express middleware for verifying JWT Bearer token authentication.
|
|
29
|
-
*/
|
|
30
|
-
export function mcpAuthMiddleware(req, res, next) {
|
|
31
|
-
const context = requestContextService.createRequestContext({
|
|
32
|
-
operation: "mcpAuthMiddleware",
|
|
33
|
-
method: req.method,
|
|
34
|
-
path: req.path,
|
|
35
|
-
});
|
|
36
|
-
logger.debug("Running MCP Authentication Middleware (Bearer Token Validation)...", context);
|
|
37
|
-
// Development Mode Bypass
|
|
38
|
-
if (!config.security.mcpAuthSecretKey) {
|
|
39
|
-
if (environment !== "production") {
|
|
40
|
-
logger.warning("Bypassing JWT authentication: MCP_AUTH_SECRET_KEY is not set (DEVELOPMENT ONLY).", context);
|
|
41
|
-
// Populate req.auth strictly according to SDK's AuthInfo
|
|
42
|
-
req.auth = {
|
|
43
|
-
token: "dev-mode-placeholder-token",
|
|
44
|
-
clientId: "dev-client-id",
|
|
45
|
-
scopes: ["dev-scope"],
|
|
46
|
-
};
|
|
47
|
-
// Log dev mode details separately, not attaching to req.auth if not part of AuthInfo
|
|
48
|
-
logger.debug("Dev mode auth object created.", {
|
|
49
|
-
...context,
|
|
50
|
-
authDetails: req.auth,
|
|
51
|
-
});
|
|
52
|
-
return next();
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
logger.error("FATAL: MCP_AUTH_SECRET_KEY is missing in production. Cannot bypass auth.", context);
|
|
56
|
-
res.status(500).json({
|
|
57
|
-
error: "Server configuration error: Authentication key missing.",
|
|
58
|
-
});
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
const authHeader = req.headers.authorization;
|
|
63
|
-
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
64
|
-
logger.warning("Authentication failed: Missing or malformed Authorization header (Bearer scheme required).", context);
|
|
65
|
-
res.status(401).json({
|
|
66
|
-
error: "Unauthorized: Missing or invalid authentication token format.",
|
|
67
|
-
});
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const tokenParts = authHeader.split(" ");
|
|
71
|
-
if (tokenParts.length !== 2 || tokenParts[0] !== "Bearer" || !tokenParts[1]) {
|
|
72
|
-
logger.warning("Authentication failed: Malformed Bearer token.", context);
|
|
73
|
-
res
|
|
74
|
-
.status(401)
|
|
75
|
-
.json({ error: "Unauthorized: Malformed authentication token." });
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const rawToken = tokenParts[1];
|
|
79
|
-
try {
|
|
80
|
-
const decoded = jwt.verify(rawToken, config.security.mcpAuthSecretKey);
|
|
81
|
-
if (typeof decoded === "string") {
|
|
82
|
-
logger.warning("Authentication failed: JWT decoded to a string, expected an object payload.", context);
|
|
83
|
-
res
|
|
84
|
-
.status(401)
|
|
85
|
-
.json({ error: "Unauthorized: Invalid token payload format." });
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
// Extract and validate fields for SDK's AuthInfo
|
|
89
|
-
const clientIdFromToken = typeof decoded.cid === "string"
|
|
90
|
-
? decoded.cid
|
|
91
|
-
: typeof decoded.client_id === "string"
|
|
92
|
-
? decoded.client_id
|
|
93
|
-
: undefined;
|
|
94
|
-
if (!clientIdFromToken) {
|
|
95
|
-
logger.warning("Authentication failed: JWT 'cid' or 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
|
|
96
|
-
res.status(401).json({
|
|
97
|
-
error: "Unauthorized: Invalid token, missing client identifier.",
|
|
98
|
-
});
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
let scopesFromToken;
|
|
102
|
-
if (Array.isArray(decoded.scp) &&
|
|
103
|
-
decoded.scp.every((s) => typeof s === "string")) {
|
|
104
|
-
scopesFromToken = decoded.scp;
|
|
105
|
-
}
|
|
106
|
-
else if (typeof decoded.scope === "string" &&
|
|
107
|
-
decoded.scope.trim() !== "") {
|
|
108
|
-
scopesFromToken = decoded.scope.split(" ").filter((s) => s);
|
|
109
|
-
if (scopesFromToken.length === 0 && decoded.scope.trim() !== "") {
|
|
110
|
-
// handles case " " -> [""]
|
|
111
|
-
scopesFromToken = [decoded.scope.trim()];
|
|
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
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
// If scopes are strictly mandatory and not found or invalid format
|
|
121
|
-
logger.warning("Authentication failed: JWT 'scp' or 'scope' claim is missing, not an array of strings, or not a valid space-separated string. Assigning default empty array.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
|
|
122
|
-
scopesFromToken = []; // Default to empty array if scopes are mandatory but not found/invalid
|
|
123
|
-
// Or, if truly mandatory and must be non-empty:
|
|
124
|
-
// res.status(401).json({ error: "Unauthorized: Invalid token, missing or invalid scopes." });
|
|
125
|
-
// return;
|
|
126
|
-
}
|
|
127
|
-
// Construct req.auth with only the properties defined in SDK's AuthInfo
|
|
128
|
-
// All other claims from 'decoded' are not part of req.auth for type safety.
|
|
129
|
-
req.auth = {
|
|
130
|
-
token: rawToken,
|
|
131
|
-
clientId: clientIdFromToken,
|
|
132
|
-
scopes: scopesFromToken,
|
|
133
|
-
};
|
|
134
|
-
// Log separately if other JWT claims like 'sub' (sessionId) are needed for app logic
|
|
135
|
-
const subClaimForLogging = typeof decoded.sub === "string" ? decoded.sub : undefined;
|
|
136
|
-
logger.debug("JWT verified successfully. AuthInfo attached to request.", {
|
|
137
|
-
...context,
|
|
138
|
-
mcpSessionIdContext: subClaimForLogging,
|
|
139
|
-
clientId: req.auth.clientId,
|
|
140
|
-
scopes: req.auth.scopes,
|
|
141
|
-
});
|
|
142
|
-
next();
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
let errorMessage = "Invalid token";
|
|
146
|
-
if (error instanceof jwt.TokenExpiredError) {
|
|
147
|
-
errorMessage = "Token expired";
|
|
148
|
-
logger.warning("Authentication failed: Token expired.", {
|
|
149
|
-
...context,
|
|
150
|
-
expiredAt: error.expiredAt,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
else if (error instanceof jwt.JsonWebTokenError) {
|
|
154
|
-
errorMessage = `Invalid token: ${error.message}`;
|
|
155
|
-
logger.warning(`Authentication failed: ${errorMessage}`, { ...context });
|
|
156
|
-
}
|
|
157
|
-
else if (error instanceof Error) {
|
|
158
|
-
errorMessage = `Verification error: ${error.message}`;
|
|
159
|
-
logger.error("Authentication failed: Unexpected error during token verification.", { ...context, error: error.message });
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
errorMessage = "Unknown verification error";
|
|
163
|
-
logger.error("Authentication failed: Unexpected non-error exception during token verification.", { ...context, error });
|
|
164
|
-
}
|
|
165
|
-
res.status(401).json({ error: `Unauthorized: ${errorMessage}.` });
|
|
166
|
-
}
|
|
167
|
-
}
|