@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
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Configures and starts the HTTP MCP transport using Hono.
|
|
3
|
+
* This file has been refactored to correctly integrate Hono's streaming
|
|
4
|
+
* capabilities with the Model Context Protocol SDK's transport layer.
|
|
5
|
+
* @module src/mcp-server/transports/http/httpTransport
|
|
6
|
+
*/
|
|
7
|
+
import { serve } from "@hono/node-server";
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import { cors } from "hono/cors";
|
|
10
|
+
import { stream } from "hono/streaming";
|
|
11
|
+
import http from "http";
|
|
12
|
+
import { config } from "../../../config/index.js";
|
|
13
|
+
import { logger, rateLimiter, requestContextService, } from "../../../utils/index.js";
|
|
14
|
+
import { createAuthMiddleware, createAuthStrategy } from "../auth/index.js";
|
|
15
|
+
import { StatefulTransportManager } from "../core/statefulTransportManager.js";
|
|
16
|
+
import { StatelessTransportManager } from "../core/statelessTransportManager.js";
|
|
17
|
+
import { httpErrorHandler } from "./httpErrorHandler.js";
|
|
18
|
+
import { mcpTransportMiddleware } from "./mcpTransportMiddleware.js";
|
|
19
|
+
const HTTP_PORT = config.mcpHttpPort;
|
|
20
|
+
const HTTP_HOST = config.mcpHttpHost;
|
|
21
|
+
const MCP_ENDPOINT_PATH = config.mcpHttpEndpointPath;
|
|
22
|
+
/**
|
|
23
|
+
* Converts a Fetch API Headers object to Node.js IncomingHttpHeaders.
|
|
24
|
+
* Hono uses Fetch API Headers, but the underlying transport managers expect
|
|
25
|
+
* Node's native IncomingHttpHeaders.
|
|
26
|
+
* @param headers - The Headers object to convert.
|
|
27
|
+
* @returns An object compatible with IncomingHttpHeaders.
|
|
28
|
+
*/
|
|
29
|
+
async function isPortInUse(port, host, parentContext) {
|
|
30
|
+
const context = { ...parentContext, operation: "isPortInUse", port, host };
|
|
31
|
+
logger.debug(`Checking if port ${port} is in use...`, context);
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const tempServer = http.createServer();
|
|
34
|
+
tempServer
|
|
35
|
+
.once("error", (err) => {
|
|
36
|
+
const inUse = err.code === "EADDRINUSE";
|
|
37
|
+
logger.debug(`Port check resulted in error: ${err.code}. Port in use: ${inUse}`, context);
|
|
38
|
+
resolve(inUse);
|
|
39
|
+
})
|
|
40
|
+
.once("listening", () => {
|
|
41
|
+
logger.debug(`Successfully bound to port ${port} temporarily. Port is not in use.`, context);
|
|
42
|
+
tempServer.close(() => resolve(false));
|
|
43
|
+
})
|
|
44
|
+
.listen(port, host);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentContext) {
|
|
48
|
+
const startContext = {
|
|
49
|
+
...parentContext,
|
|
50
|
+
operation: "startHttpServerWithRetry",
|
|
51
|
+
};
|
|
52
|
+
logger.info(`Attempting to start HTTP server on port ${initialPort} with ${maxRetries} retries.`, startContext);
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const tryBind = (port, attempt) => {
|
|
55
|
+
const attemptContext = { ...startContext, port, attempt };
|
|
56
|
+
if (attempt > maxRetries + 1) {
|
|
57
|
+
const error = new Error(`Failed to bind to any port after ${maxRetries} retries.`);
|
|
58
|
+
logger.fatal(error.message, attemptContext);
|
|
59
|
+
return reject(error);
|
|
60
|
+
}
|
|
61
|
+
isPortInUse(port, host, attemptContext)
|
|
62
|
+
.then((inUse) => {
|
|
63
|
+
if (inUse) {
|
|
64
|
+
logger.warning(`Port ${port} is in use, retrying on port ${port + 1}...`, attemptContext);
|
|
65
|
+
setTimeout(() => tryBind(port + 1, attempt + 1), config.mcpHttpPortRetryDelayMs);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const serverInstance = serve({ fetch: app.fetch, port, hostname: host }, (info) => {
|
|
70
|
+
const serverAddress = `http://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
|
|
71
|
+
logger.info(`HTTP transport listening at ${serverAddress}`, {
|
|
72
|
+
...attemptContext,
|
|
73
|
+
address: serverAddress,
|
|
74
|
+
sessionMode: config.mcpSessionMode,
|
|
75
|
+
});
|
|
76
|
+
if (process.stdout.isTTY) {
|
|
77
|
+
console.log(`\n🚀 MCP Server running at: ${serverAddress}`);
|
|
78
|
+
console.log(` Session Mode: ${config.mcpSessionMode}\n`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
resolve(serverInstance);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
if (err &&
|
|
85
|
+
typeof err === "object" &&
|
|
86
|
+
"code" in err &&
|
|
87
|
+
err.code !== "EADDRINUSE") {
|
|
88
|
+
const errorToLog = err instanceof Error ? err : new Error(String(err));
|
|
89
|
+
logger.error("An unexpected error occurred while starting the server.", { ...attemptContext, error: errorToLog });
|
|
90
|
+
return reject(err);
|
|
91
|
+
}
|
|
92
|
+
logger.warning(`Encountered EADDRINUSE race condition on port ${port}, retrying...`, attemptContext);
|
|
93
|
+
setTimeout(() => tryBind(port + 1, attempt + 1), config.mcpHttpPortRetryDelayMs);
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
.catch((err) => {
|
|
97
|
+
logger.fatal("Failed to check if port is in use.", {
|
|
98
|
+
...attemptContext,
|
|
99
|
+
error: err,
|
|
100
|
+
});
|
|
101
|
+
reject(err);
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
tryBind(initialPort, 1);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function createTransportManager(createServerInstanceFn, sessionMode, context) {
|
|
108
|
+
const opContext = {
|
|
109
|
+
...context,
|
|
110
|
+
operation: "createTransportManager",
|
|
111
|
+
sessionMode,
|
|
112
|
+
};
|
|
113
|
+
logger.info(`Creating transport manager for session mode: ${sessionMode}`, opContext);
|
|
114
|
+
switch (sessionMode) {
|
|
115
|
+
case "stateless":
|
|
116
|
+
return new StatelessTransportManager(createServerInstanceFn);
|
|
117
|
+
case "stateful":
|
|
118
|
+
return new StatefulTransportManager(createServerInstanceFn);
|
|
119
|
+
case "auto":
|
|
120
|
+
default:
|
|
121
|
+
logger.info("Defaulting to 'auto' mode (stateful with stateless fallback).", opContext);
|
|
122
|
+
return new StatefulTransportManager(createServerInstanceFn);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function createHttpApp(transportManager, createServerInstanceFn, parentContext) {
|
|
126
|
+
const app = new Hono();
|
|
127
|
+
const transportContext = {
|
|
128
|
+
...parentContext,
|
|
129
|
+
component: "HttpTransportSetup",
|
|
130
|
+
};
|
|
131
|
+
logger.info("Creating Hono HTTP application.", transportContext);
|
|
132
|
+
app.use("*", cors({
|
|
133
|
+
origin: config.mcpAllowedOrigins || [],
|
|
134
|
+
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
135
|
+
allowHeaders: [
|
|
136
|
+
"Content-Type",
|
|
137
|
+
"Mcp-Session-Id",
|
|
138
|
+
"Last-Event-ID",
|
|
139
|
+
"Authorization",
|
|
140
|
+
],
|
|
141
|
+
credentials: true,
|
|
142
|
+
}));
|
|
143
|
+
app.use("*", async (c, next) => {
|
|
144
|
+
c.env.outgoing.setHeader("X-Content-Type-Options", "nosniff");
|
|
145
|
+
await next();
|
|
146
|
+
});
|
|
147
|
+
app.use(MCP_ENDPOINT_PATH, async (c, next) => {
|
|
148
|
+
const clientIp = c.req.header("x-forwarded-for")?.split(",")[0].trim() || "unknown_ip";
|
|
149
|
+
const context = requestContextService.createRequestContext({
|
|
150
|
+
operation: "httpRateLimitCheck",
|
|
151
|
+
ipAddress: clientIp,
|
|
152
|
+
});
|
|
153
|
+
try {
|
|
154
|
+
rateLimiter.check(clientIp, context);
|
|
155
|
+
logger.debug("Rate limit check passed.", context);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
logger.warning("Rate limit check failed.", {
|
|
159
|
+
...context,
|
|
160
|
+
error: error instanceof Error ? error.message : String(error),
|
|
161
|
+
});
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
await next();
|
|
165
|
+
});
|
|
166
|
+
const authStrategy = createAuthStrategy();
|
|
167
|
+
if (authStrategy) {
|
|
168
|
+
logger.info("Authentication strategy found, enabling auth middleware.", transportContext);
|
|
169
|
+
app.use(MCP_ENDPOINT_PATH, createAuthMiddleware(authStrategy));
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
logger.info("No authentication strategy found, auth middleware disabled.", transportContext);
|
|
173
|
+
}
|
|
174
|
+
app.onError(httpErrorHandler);
|
|
175
|
+
app.get("/healthz", (c) => {
|
|
176
|
+
return c.json({
|
|
177
|
+
status: "ok",
|
|
178
|
+
timestamp: new Date().toISOString(),
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
app.get(MCP_ENDPOINT_PATH, (c) => {
|
|
182
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
183
|
+
if (sessionId) {
|
|
184
|
+
return c.text("GET requests to existing sessions are not supported.", 405);
|
|
185
|
+
}
|
|
186
|
+
return c.json({
|
|
187
|
+
status: "ok",
|
|
188
|
+
mode: "stateless",
|
|
189
|
+
message: "Server is running. Provide a Mcp-Session-Id header to stream from a session.",
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
app.post(MCP_ENDPOINT_PATH, mcpTransportMiddleware(transportManager, createServerInstanceFn), (c) => {
|
|
193
|
+
const response = c.get("mcpResponse");
|
|
194
|
+
if (response.sessionId) {
|
|
195
|
+
c.header("Mcp-Session-Id", response.sessionId);
|
|
196
|
+
}
|
|
197
|
+
response.headers.forEach((value, key) => {
|
|
198
|
+
c.header(key, value);
|
|
199
|
+
});
|
|
200
|
+
c.status(response.statusCode);
|
|
201
|
+
if (response.stream) {
|
|
202
|
+
return stream(c, async (s) => {
|
|
203
|
+
if (response.stream) {
|
|
204
|
+
await s.pipe(response.stream);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
210
|
+
return c.json(response.body);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
app.delete(MCP_ENDPOINT_PATH, async (c) => {
|
|
214
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
215
|
+
const context = requestContextService.createRequestContext({
|
|
216
|
+
...transportContext,
|
|
217
|
+
operation: "handleDeleteRequest",
|
|
218
|
+
sessionId,
|
|
219
|
+
});
|
|
220
|
+
if (sessionId) {
|
|
221
|
+
if (transportManager instanceof StatefulTransportManager) {
|
|
222
|
+
const response = await transportManager.handleDeleteRequest(sessionId, context);
|
|
223
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
224
|
+
return c.json(response.body, response.statusCode);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
return c.json({
|
|
228
|
+
error: "Method Not Allowed",
|
|
229
|
+
message: "DELETE operations are not supported in this mode.",
|
|
230
|
+
}, 405);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
return c.json({
|
|
235
|
+
status: "stateless_mode",
|
|
236
|
+
message: "No sessions to delete in stateless mode",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
logger.info("Hono application setup complete.", transportContext);
|
|
241
|
+
return app;
|
|
242
|
+
}
|
|
243
|
+
export async function startHttpTransport(createServerInstanceFn, parentContext) {
|
|
244
|
+
const transportContext = {
|
|
245
|
+
...parentContext,
|
|
246
|
+
component: "HttpTransportStart",
|
|
247
|
+
};
|
|
248
|
+
logger.info("Starting HTTP transport.", transportContext);
|
|
249
|
+
const transportManager = createTransportManager(createServerInstanceFn, config.mcpSessionMode, transportContext);
|
|
250
|
+
const app = createHttpApp(transportManager, createServerInstanceFn, transportContext);
|
|
251
|
+
const server = await startHttpServerWithRetry(app, HTTP_PORT, HTTP_HOST, config.mcpHttpMaxPortRetries, transportContext);
|
|
252
|
+
logger.info("HTTP transport started successfully.", transportContext);
|
|
253
|
+
return { app, server, transportManager };
|
|
254
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Hono middleware for handling MCP transport logic.
|
|
3
|
+
* This middleware encapsulates the logic for processing MCP requests,
|
|
4
|
+
* delegating to the appropriate transport manager, and preparing the
|
|
5
|
+
* response for Hono to send.
|
|
6
|
+
* @module src/mcp-server/transports/http/mcpTransportMiddleware
|
|
7
|
+
*/
|
|
8
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { createMiddleware } from "hono/factory";
|
|
10
|
+
import { config } from "../../../config/index.js";
|
|
11
|
+
import { requestContextService } from "../../../utils/index.js";
|
|
12
|
+
import { StatelessTransportManager } from "../core/statelessTransportManager.js";
|
|
13
|
+
/**
|
|
14
|
+
* Converts a Fetch API Headers object to Node.js IncomingHttpHeaders.
|
|
15
|
+
* @param headers - The Headers object to convert.
|
|
16
|
+
* @returns An object compatible with IncomingHttpHeaders.
|
|
17
|
+
*/
|
|
18
|
+
function toIncomingHttpHeaders(headers) {
|
|
19
|
+
const result = {};
|
|
20
|
+
headers.forEach((value, key) => {
|
|
21
|
+
result[key] = value;
|
|
22
|
+
});
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Handles a stateless request by creating an ephemeral transport manager.
|
|
27
|
+
* @param createServerInstanceFn - Function to create an McpServer instance.
|
|
28
|
+
* @param headers - The request headers.
|
|
29
|
+
* @param body - The request body.
|
|
30
|
+
* @param context - The request context.
|
|
31
|
+
* @returns A promise resolving with the transport response.
|
|
32
|
+
*/
|
|
33
|
+
async function handleStatelessRequest(createServerInstanceFn, headers, body, context) {
|
|
34
|
+
const statelessManager = new StatelessTransportManager(createServerInstanceFn);
|
|
35
|
+
return statelessManager.handleRequest(toIncomingHttpHeaders(headers), body, context);
|
|
36
|
+
}
|
|
37
|
+
export const mcpTransportMiddleware = (transportManager, createServerInstanceFn) => createMiddleware(async (c, next) => {
|
|
38
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
39
|
+
const context = requestContextService.createRequestContext({
|
|
40
|
+
operation: "mcpTransportMiddleware",
|
|
41
|
+
sessionId,
|
|
42
|
+
});
|
|
43
|
+
const body = await c.req.json();
|
|
44
|
+
let response;
|
|
45
|
+
if (isInitializeRequest(body)) {
|
|
46
|
+
if (config.mcpSessionMode === "stateless") {
|
|
47
|
+
response = await handleStatelessRequest(createServerInstanceFn, c.req.raw.headers, body, context);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
response = await transportManager.initializeAndHandle(toIncomingHttpHeaders(c.req.raw.headers), body, context);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
if (sessionId) {
|
|
55
|
+
response = await transportManager.handleRequest(toIncomingHttpHeaders(c.req.raw.headers), body, context, sessionId);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
response = await handleStatelessRequest(createServerInstanceFn, c.req.raw.headers, body, context);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
c.set("mcpResponse", response);
|
|
62
|
+
await next();
|
|
63
|
+
});
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* @module src/mcp-server/transports/stdioTransport
|
|
19
19
|
*/
|
|
20
20
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
21
|
-
import { ErrorHandler, logger } from "
|
|
21
|
+
import { ErrorHandler, logger } from "../../../utils/index.js";
|
|
22
22
|
/**
|
|
23
23
|
* Connects a given `McpServer` instance to the Stdio transport.
|
|
24
24
|
* This function initializes the SDK's `StdioServerTransport`, which manages
|
|
@@ -39,13 +39,13 @@ import { ErrorHandler, logger } from "../../utils/index.js";
|
|
|
39
39
|
* @returns A promise that resolves when the Stdio transport is successfully connected.
|
|
40
40
|
* @throws {Error} If the connection fails during setup.
|
|
41
41
|
*/
|
|
42
|
-
export async function
|
|
42
|
+
export async function startStdioTransport(server, parentContext) {
|
|
43
43
|
const operationContext = {
|
|
44
44
|
...parentContext,
|
|
45
45
|
operation: "connectStdioTransport",
|
|
46
46
|
transportType: "Stdio",
|
|
47
47
|
};
|
|
48
|
-
logger.
|
|
48
|
+
logger.info("Attempting to connect stdio transport...", operationContext);
|
|
49
49
|
try {
|
|
50
50
|
logger.debug("Creating StdioServerTransport instance...", operationContext);
|
|
51
51
|
const transport = new StdioServerTransport();
|
|
@@ -57,7 +57,12 @@ export async function connectStdioTransport(server, parentContext) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
catch (err) {
|
|
60
|
-
ErrorHandler
|
|
61
|
-
throw err
|
|
60
|
+
// Let the ErrorHandler log the error with all context, then rethrow.
|
|
61
|
+
throw ErrorHandler.handleError(err, {
|
|
62
|
+
operation: "connectStdioTransport",
|
|
63
|
+
context: operationContext,
|
|
64
|
+
critical: true,
|
|
65
|
+
rethrow: true,
|
|
66
|
+
});
|
|
62
67
|
}
|
|
63
68
|
}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Defines standardized error codes, a custom error class, and related schemas
|
|
3
|
+
* for handling errors within the Model Context Protocol (MCP) server and its components.
|
|
4
|
+
* This module provides a structured way to represent and communicate errors, ensuring
|
|
5
|
+
* consistency and clarity for both server-side operations and client-side error handling.
|
|
6
|
+
* @module src/types-global/errors
|
|
7
|
+
*/
|
|
1
8
|
import { z } from "zod";
|
|
2
9
|
/**
|
|
3
|
-
* Defines a set of standardized error codes for common issues
|
|
4
|
-
*
|
|
10
|
+
* Defines a comprehensive set of standardized error codes for common issues encountered
|
|
11
|
+
* within MCP servers, tools, or related operations. These codes are designed to help
|
|
12
|
+
* clients and developers programmatically understand the nature of an error, facilitating
|
|
13
|
+
* more precise error handling and debugging.
|
|
5
14
|
*/
|
|
6
15
|
export var BaseErrorCode;
|
|
7
16
|
(function (BaseErrorCode) {
|
|
@@ -15,6 +24,8 @@ export var BaseErrorCode;
|
|
|
15
24
|
BaseErrorCode["CONFLICT"] = "CONFLICT";
|
|
16
25
|
/** The request failed due to invalid input parameters or data. */
|
|
17
26
|
BaseErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
|
27
|
+
/** The provided input is invalid for the operation. */
|
|
28
|
+
BaseErrorCode["INVALID_INPUT"] = "INVALID_INPUT";
|
|
18
29
|
/** An error occurred while parsing input data (e.g., date string, JSON). */
|
|
19
30
|
BaseErrorCode["PARSING_ERROR"] = "PARSING_ERROR";
|
|
20
31
|
/** The request was rejected because the client has exceeded rate limits. */
|
|
@@ -29,45 +40,90 @@ export var BaseErrorCode;
|
|
|
29
40
|
BaseErrorCode["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
|
|
30
41
|
/** An error occurred during the loading or validation of configuration data. */
|
|
31
42
|
BaseErrorCode["CONFIGURATION_ERROR"] = "CONFIGURATION_ERROR";
|
|
43
|
+
/** An error occurred during the initialization phase of a service or module. */
|
|
44
|
+
BaseErrorCode["INITIALIZATION_FAILED"] = "INITIALIZATION_FAILED";
|
|
45
|
+
/** A service was used before it was properly initialized. */
|
|
46
|
+
BaseErrorCode["SERVICE_NOT_INITIALIZED"] = "SERVICE_NOT_INITIALIZED";
|
|
47
|
+
/** A generic error occurred during a database operation. */
|
|
48
|
+
BaseErrorCode["DATABASE_ERROR"] = "DATABASE_ERROR";
|
|
49
|
+
/** An error occurred while loading or interacting with an extension. */
|
|
50
|
+
BaseErrorCode["EXTENSION_ERROR"] = "EXTENSION_ERROR";
|
|
51
|
+
/** An error occurred during the shutdown phase of a service or module. */
|
|
52
|
+
BaseErrorCode["SHUTDOWN_ERROR"] = "SHUTDOWN_ERROR";
|
|
53
|
+
/** A generic error occurred during the execution of an agent's task. */
|
|
54
|
+
BaseErrorCode["AGENT_EXECUTION_ERROR"] = "AGENT_EXECUTION_ERROR";
|
|
32
55
|
})(BaseErrorCode || (BaseErrorCode = {}));
|
|
33
56
|
/**
|
|
34
|
-
* Custom error class for MCP-specific errors.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
57
|
+
* Custom error class for MCP-specific errors, extending the built-in `Error` class.
|
|
58
|
+
* It standardizes error reporting by encapsulating a `BaseErrorCode`, a descriptive
|
|
59
|
+
* human-readable message, and optional structured details for more context.
|
|
60
|
+
*
|
|
61
|
+
* This class is central to error handling within the MCP framework, allowing for
|
|
62
|
+
* consistent error creation and propagation.
|
|
37
63
|
*/
|
|
38
64
|
export class McpError extends Error {
|
|
65
|
+
/**
|
|
66
|
+
* The standardized error code from {@link BaseErrorCode}.
|
|
67
|
+
*/
|
|
39
68
|
code;
|
|
69
|
+
/**
|
|
70
|
+
* Optional additional details or context about the error.
|
|
71
|
+
* This can be any structured data that helps in understanding or debugging the error.
|
|
72
|
+
*/
|
|
40
73
|
details;
|
|
41
74
|
/**
|
|
42
75
|
* Creates an instance of McpError.
|
|
43
|
-
*
|
|
44
|
-
* @param
|
|
45
|
-
* @param
|
|
76
|
+
*
|
|
77
|
+
* @param code - The standardized error code that categorizes the error.
|
|
78
|
+
* @param message - A human-readable description of the error.
|
|
79
|
+
* @param details - Optional. A record containing additional structured details about the error.
|
|
46
80
|
*/
|
|
47
81
|
constructor(code, message, details) {
|
|
48
82
|
super(message);
|
|
49
83
|
this.code = code;
|
|
50
84
|
this.details = details;
|
|
51
|
-
// Set the error name for identification
|
|
52
85
|
this.name = "McpError";
|
|
53
|
-
//
|
|
86
|
+
// Maintain a proper prototype chain.
|
|
54
87
|
Object.setPrototypeOf(this, McpError.prototype);
|
|
88
|
+
// Capture the stack trace, excluding the constructor call from it, if available.
|
|
89
|
+
if (Error.captureStackTrace) {
|
|
90
|
+
Error.captureStackTrace(this, McpError);
|
|
91
|
+
}
|
|
55
92
|
}
|
|
56
93
|
}
|
|
57
94
|
/**
|
|
58
|
-
* Zod schema for validating error objects
|
|
59
|
-
* error
|
|
95
|
+
* Zod schema for validating error objects. This schema can be used for:
|
|
96
|
+
* - Validating error structures when parsing error responses from external services.
|
|
97
|
+
* - Ensuring consistency when creating or handling error objects internally.
|
|
98
|
+
* - Generating TypeScript types for error objects.
|
|
99
|
+
*
|
|
100
|
+
* The schema enforces the presence of a `code` (from {@link BaseErrorCode}) and a `message`,
|
|
101
|
+
* and allows for optional `details`.
|
|
60
102
|
*/
|
|
61
103
|
export const ErrorSchema = z
|
|
62
104
|
.object({
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
105
|
+
/**
|
|
106
|
+
* The error code, corresponding to one of the {@link BaseErrorCode} enum values.
|
|
107
|
+
* This field is required and helps in programmatically identifying the error type.
|
|
108
|
+
*/
|
|
109
|
+
code: z
|
|
110
|
+
.nativeEnum(BaseErrorCode)
|
|
111
|
+
.describe("Standardized error code from BaseErrorCode enum"),
|
|
112
|
+
/**
|
|
113
|
+
* A human-readable, descriptive message explaining the error.
|
|
114
|
+
* This field is required and provides context to developers or users.
|
|
115
|
+
*/
|
|
116
|
+
message: z
|
|
117
|
+
.string()
|
|
118
|
+
.min(1, "Error message cannot be empty.")
|
|
119
|
+
.describe("Detailed human-readable error message"),
|
|
120
|
+
/**
|
|
121
|
+
* Optional. A record containing additional structured details or context about the error.
|
|
122
|
+
* This can include things like invalid field names, specific values that caused issues, or other relevant data.
|
|
123
|
+
*/
|
|
68
124
|
details: z
|
|
69
125
|
.record(z.unknown())
|
|
70
126
|
.optional()
|
|
71
|
-
.describe("Optional structured error
|
|
127
|
+
.describe("Optional structured details providing more context about the error"),
|
|
72
128
|
})
|
|
73
|
-
.describe("Schema for validating structured error objects.");
|
|
129
|
+
.describe("Schema for validating structured error objects, ensuring consistency in error reporting.");
|
|
@@ -133,28 +133,26 @@ export class ErrorHandler {
|
|
|
133
133
|
const { context, operation, input, rethrow = false, errorCode: explicitErrorCode, includeStack = true, critical = false, } = options;
|
|
134
134
|
// If it's already an McpError, use it directly but apply additional context
|
|
135
135
|
if (error instanceof McpError) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
error.details = { ...existingDetails, ...context };
|
|
143
|
-
}
|
|
136
|
+
const existingDetails = typeof error.details === "object" && error.details !== null
|
|
137
|
+
? error.details
|
|
138
|
+
: {};
|
|
139
|
+
const newDetails = { ...existingDetails, ...context };
|
|
140
|
+
// Create a new error to avoid mutating a readonly property
|
|
141
|
+
const updatedError = new McpError(error.code, error.message, newDetails);
|
|
144
142
|
// Log the error with sanitized input
|
|
145
|
-
logger.error(`Error ${operation}: ${
|
|
146
|
-
errorCode:
|
|
143
|
+
logger.error(`Error ${operation}: ${updatedError.message}`, {
|
|
144
|
+
errorCode: updatedError.code,
|
|
147
145
|
requestId: context?.requestId,
|
|
148
146
|
input: input ? sanitizeInputForLogging(input) : undefined,
|
|
149
|
-
stack: includeStack ?
|
|
147
|
+
stack: includeStack ? updatedError.stack : undefined,
|
|
150
148
|
critical,
|
|
151
149
|
...context,
|
|
152
150
|
});
|
|
153
151
|
if (rethrow) {
|
|
154
|
-
throw
|
|
152
|
+
throw updatedError;
|
|
155
153
|
}
|
|
156
154
|
// Ensure the function returns an Error type
|
|
157
|
-
return
|
|
155
|
+
return updatedError;
|
|
158
156
|
}
|
|
159
157
|
// Sanitize input for logging
|
|
160
158
|
const sanitizedInput = input ? sanitizeInputForLogging(input) : undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/git-mcp-server",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.3",
|
|
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": [
|
|
@@ -32,19 +32,22 @@
|
|
|
32
32
|
"format": "prettier --write \"**/*.{ts,js,json,md,html,css}\"",
|
|
33
33
|
"inspector": "npx @modelcontextprotocol/inspector --config mcp.json --server git-mcp-server",
|
|
34
34
|
"inspector:http": "npx @modelcontextprotocol/inspector --config mcp.json --server git-mcp-server-http",
|
|
35
|
-
"clean": "ts-node --esm scripts/clean.ts"
|
|
35
|
+
"clean": "ts-node --esm scripts/clean.ts",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"test:coverage": "vitest run --coverage"
|
|
36
39
|
},
|
|
37
40
|
"dependencies": {
|
|
38
41
|
"@hono/node-server": "^1.17.1",
|
|
39
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
40
|
-
"@types/node": "^24.0
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
43
|
+
"@types/node": "^24.1.0",
|
|
41
44
|
"@types/sanitize-html": "^2.16.0",
|
|
42
45
|
"@types/validator": "^13.15.2",
|
|
43
46
|
"chrono-node": "2.8.0",
|
|
44
47
|
"dotenv": "16.6.1",
|
|
45
|
-
"hono": "^4.8.
|
|
48
|
+
"hono": "^4.8.10",
|
|
46
49
|
"jose": "^6.0.12",
|
|
47
|
-
"openai": "^5.10.
|
|
50
|
+
"openai": "^5.10.2",
|
|
48
51
|
"partial-json": "^0.1.7",
|
|
49
52
|
"sanitize-html": "^2.17.0",
|
|
50
53
|
"tiktoken": "^1.0.21",
|
|
@@ -105,8 +108,16 @@
|
|
|
105
108
|
"node": ">=20.0.0"
|
|
106
109
|
},
|
|
107
110
|
"devDependencies": {
|
|
111
|
+
"@anatine/zod-mock": "^3.14.0",
|
|
112
|
+
"@faker-js/faker": "^9.9.0",
|
|
113
|
+
"@types/supertest": "^6.0.3",
|
|
114
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
115
|
+
"msw": "^2.10.4",
|
|
108
116
|
"prettier": "^3.6.2",
|
|
109
|
-
"
|
|
117
|
+
"supertest": "^7.1.4",
|
|
118
|
+
"typedoc": "^0.28.8",
|
|
119
|
+
"vite-tsconfig-paths": "^5.1.4",
|
|
120
|
+
"vitest": "^3.2.4"
|
|
110
121
|
},
|
|
111
122
|
"publishConfig": {
|
|
112
123
|
"access": "public"
|