@cyanheads/git-mcp-server 2.1.0 ā 2.1.2
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 +8 -11
- package/dist/config/index.js +7 -7
- package/dist/index.js +35 -21
- package/dist/mcp-server/server.js +72 -56
- package/dist/mcp-server/tools/gitAdd/index.js +1 -1
- package/dist/mcp-server/tools/gitAdd/logic.js +88 -39
- package/dist/mcp-server/tools/gitAdd/registration.js +17 -14
- package/dist/mcp-server/tools/gitBranch/index.js +1 -1
- package/dist/mcp-server/tools/gitBranch/logic.js +213 -85
- package/dist/mcp-server/tools/gitBranch/registration.js +16 -13
- package/dist/mcp-server/tools/gitCheckout/index.js +1 -1
- package/dist/mcp-server/tools/gitCheckout/logic.js +85 -145
- package/dist/mcp-server/tools/gitCheckout/registration.js +16 -14
- package/dist/mcp-server/tools/gitCherryPick/index.js +1 -1
- package/dist/mcp-server/tools/gitCherryPick/logic.js +100 -41
- package/dist/mcp-server/tools/gitCherryPick/registration.js +21 -14
- package/dist/mcp-server/tools/gitClean/index.js +1 -1
- package/dist/mcp-server/tools/gitClean/logic.js +93 -41
- package/dist/mcp-server/tools/gitClean/registration.js +19 -16
- package/dist/mcp-server/tools/gitClearWorkingDir/index.js +1 -1
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +14 -11
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +19 -13
- package/dist/mcp-server/tools/gitClone/index.js +1 -1
- package/dist/mcp-server/tools/gitClone/logic.js +89 -30
- package/dist/mcp-server/tools/gitClone/registration.js +15 -12
- package/dist/mcp-server/tools/gitCommit/index.js +1 -1
- package/dist/mcp-server/tools/gitCommit/logic.js +198 -76
- package/dist/mcp-server/tools/gitCommit/registration.js +23 -20
- package/dist/mcp-server/tools/gitDiff/index.js +1 -1
- package/dist/mcp-server/tools/gitDiff/logic.js +124 -44
- package/dist/mcp-server/tools/gitDiff/registration.js +16 -14
- package/dist/mcp-server/tools/gitFetch/index.js +1 -1
- package/dist/mcp-server/tools/gitFetch/logic.js +78 -49
- package/dist/mcp-server/tools/gitFetch/registration.js +16 -14
- package/dist/mcp-server/tools/gitInit/index.js +1 -1
- package/dist/mcp-server/tools/gitInit/logic.js +88 -34
- package/dist/mcp-server/tools/gitInit/registration.js +32 -18
- package/dist/mcp-server/tools/gitLog/index.js +1 -1
- package/dist/mcp-server/tools/gitLog/logic.js +133 -47
- package/dist/mcp-server/tools/gitLog/registration.js +16 -14
- package/dist/mcp-server/tools/gitMerge/index.js +1 -1
- package/dist/mcp-server/tools/gitMerge/logic.js +102 -61
- package/dist/mcp-server/tools/gitMerge/registration.js +17 -14
- package/dist/mcp-server/tools/gitPull/index.js +1 -1
- package/dist/mcp-server/tools/gitPull/logic.js +90 -69
- package/dist/mcp-server/tools/gitPull/registration.js +16 -14
- package/dist/mcp-server/tools/gitPush/index.js +1 -1
- package/dist/mcp-server/tools/gitPush/logic.js +116 -100
- package/dist/mcp-server/tools/gitPush/registration.js +16 -14
- package/dist/mcp-server/tools/gitRebase/index.js +1 -1
- package/dist/mcp-server/tools/gitRebase/logic.js +121 -82
- package/dist/mcp-server/tools/gitRebase/registration.js +21 -14
- package/dist/mcp-server/tools/gitRemote/index.js +1 -1
- package/dist/mcp-server/tools/gitRemote/logic.js +108 -52
- package/dist/mcp-server/tools/gitRemote/registration.js +14 -11
- package/dist/mcp-server/tools/gitReset/index.js +1 -1
- package/dist/mcp-server/tools/gitReset/logic.js +65 -37
- package/dist/mcp-server/tools/gitReset/registration.js +14 -12
- package/dist/mcp-server/tools/gitSetWorkingDir/index.js +1 -1
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +74 -34
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +18 -11
- package/dist/mcp-server/tools/gitShow/index.js +1 -1
- package/dist/mcp-server/tools/gitShow/logic.js +78 -35
- package/dist/mcp-server/tools/gitShow/registration.js +17 -12
- package/dist/mcp-server/tools/gitStash/index.js +1 -1
- package/dist/mcp-server/tools/gitStash/logic.js +143 -58
- package/dist/mcp-server/tools/gitStash/registration.js +19 -12
- package/dist/mcp-server/tools/gitStatus/index.js +1 -1
- package/dist/mcp-server/tools/gitStatus/logic.js +100 -58
- package/dist/mcp-server/tools/gitStatus/registration.js +15 -12
- package/dist/mcp-server/tools/gitTag/index.js +1 -1
- package/dist/mcp-server/tools/gitTag/logic.js +124 -51
- package/dist/mcp-server/tools/gitTag/registration.js +14 -11
- package/dist/mcp-server/tools/gitWorktree/index.js +1 -1
- package/dist/mcp-server/tools/gitWorktree/logic.js +204 -95
- package/dist/mcp-server/tools/gitWorktree/registration.js +14 -11
- package/dist/mcp-server/tools/gitWrapupInstructions/index.js +1 -1
- package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +23 -11
- package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +14 -12
- package/dist/mcp-server/transports/httpTransport.js +187 -79
- package/dist/mcp-server/transports/stdioTransport.js +14 -8
- package/dist/types-global/errors.js +9 -4
- package/dist/utils/index.js +4 -4
- package/dist/utils/internal/errorHandler.js +62 -40
- package/dist/utils/internal/index.js +3 -3
- package/dist/utils/internal/logger.js +97 -54
- package/dist/utils/internal/requestContext.js +7 -5
- package/dist/utils/metrics/index.js +1 -1
- package/dist/utils/metrics/tokenCounter.js +18 -14
- package/dist/utils/parsing/dateParser.js +5 -5
- package/dist/utils/parsing/index.js +2 -2
- package/dist/utils/parsing/jsonParser.js +20 -11
- package/dist/utils/security/idGenerator.js +8 -10
- package/dist/utils/security/index.js +3 -3
- package/dist/utils/security/rateLimiter.js +16 -14
- package/dist/utils/security/sanitization.js +139 -82
- package/package.json +45 -23
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
* Specification Reference:
|
|
9
9
|
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
|
|
10
10
|
*/
|
|
11
|
-
import { StreamableHTTPServerTransport } from
|
|
12
|
-
import { isInitializeRequest } from
|
|
13
|
-
import express from
|
|
14
|
-
import http from
|
|
15
|
-
import { randomUUID } from
|
|
11
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
12
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; // SDK type guard for InitializeRequest
|
|
13
|
+
import express from "express";
|
|
14
|
+
import http from "http";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
16
|
// Import config and utils
|
|
17
|
-
import { config } from
|
|
18
|
-
import { logger } from
|
|
19
|
-
import { mcpAuthMiddleware } from
|
|
17
|
+
import { config } from "../../config/index.js"; // Import the validated config object
|
|
18
|
+
import { logger } from "../../utils/index.js";
|
|
19
|
+
import { mcpAuthMiddleware } from "./authentication/authMiddleware.js"; // Import the auth middleware
|
|
20
20
|
// --- Configuration Constants (Derived from imported config) ---
|
|
21
21
|
/**
|
|
22
22
|
* The port number for the HTTP transport, configured via MCP_HTTP_PORT.
|
|
@@ -36,7 +36,7 @@ const HTTP_HOST = config.mcpHttpHost;
|
|
|
36
36
|
* Supports POST, GET, DELETE, OPTIONS methods.
|
|
37
37
|
* @constant {string} MCP_ENDPOINT_PATH
|
|
38
38
|
*/
|
|
39
|
-
const MCP_ENDPOINT_PATH =
|
|
39
|
+
const MCP_ENDPOINT_PATH = "/mcp";
|
|
40
40
|
/**
|
|
41
41
|
* Maximum number of attempts to find an available port if the initial HTTP_PORT is in use.
|
|
42
42
|
* Tries ports sequentially: HTTP_PORT, HTTP_PORT + 1, ...
|
|
@@ -66,7 +66,10 @@ export function getHttpSessionWorkingDirectory(sessionId) {
|
|
|
66
66
|
*/
|
|
67
67
|
export function setHttpSessionWorkingDirectory(sessionId, dir) {
|
|
68
68
|
sessionWorkingDirectories.set(sessionId, dir);
|
|
69
|
-
logger.info(`HTTP session ${sessionId} working directory set to: ${dir}`, {
|
|
69
|
+
logger.info(`HTTP session ${sessionId} working directory set to: ${dir}`, {
|
|
70
|
+
operation: "setHttpSessionWorkingDirectory",
|
|
71
|
+
sessionId,
|
|
72
|
+
});
|
|
70
73
|
}
|
|
71
74
|
/**
|
|
72
75
|
* Checks if an incoming HTTP request's origin header is permissible.
|
|
@@ -81,20 +84,27 @@ export function setHttpSessionWorkingDirectory(sessionId, dir) {
|
|
|
81
84
|
function isOriginAllowed(req, res) {
|
|
82
85
|
const origin = req.headers.origin;
|
|
83
86
|
const host = req.hostname; // Considers Host header
|
|
84
|
-
const isLocalhostBinding = [
|
|
87
|
+
const isLocalhostBinding = ["127.0.0.1", "::1", "localhost"].includes(host);
|
|
85
88
|
const allowedOrigins = config.mcpAllowedOrigins || []; // Use parsed array from config
|
|
86
|
-
const context = {
|
|
87
|
-
|
|
89
|
+
const context = {
|
|
90
|
+
operation: "isOriginAllowed",
|
|
91
|
+
origin,
|
|
92
|
+
host,
|
|
93
|
+
isLocalhostBinding,
|
|
94
|
+
allowedOrigins,
|
|
95
|
+
};
|
|
96
|
+
logger.debug("Checking origin allowance", context);
|
|
88
97
|
// Determine if allowed based on config or localhost binding
|
|
89
|
-
const allowed = (origin && allowedOrigins.includes(origin)) ||
|
|
98
|
+
const allowed = (origin && allowedOrigins.includes(origin)) ||
|
|
99
|
+
(isLocalhostBinding && (!origin || origin === "null"));
|
|
90
100
|
if (allowed && origin) {
|
|
91
101
|
// Origin is allowed and present, set specific CORS headers.
|
|
92
|
-
res.setHeader(
|
|
102
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
93
103
|
// MCP Spec: Streamable HTTP uses POST, GET, DELETE. OPTIONS is for preflight.
|
|
94
|
-
res.setHeader(
|
|
104
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
95
105
|
// MCP Spec: Requires Mcp-Session-Id. Last-Event-ID for SSE resumption. Content-Type is standard. Authorization for security.
|
|
96
|
-
res.setHeader(
|
|
97
|
-
res.setHeader(
|
|
106
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Last-Event-ID, Authorization");
|
|
107
|
+
res.setHeader("Access-Control-Allow-Credentials", "true"); // Set based on whether auth/cookies are used
|
|
98
108
|
}
|
|
99
109
|
else if (allowed && !origin) {
|
|
100
110
|
// Allowed (e.g., localhost binding, file:// origin), but no origin header to echo back. No specific CORS needed.
|
|
@@ -114,13 +124,13 @@ function isOriginAllowed(req, res) {
|
|
|
114
124
|
* @returns {Promise<boolean>} True if port is in use (EADDRINUSE), false otherwise.
|
|
115
125
|
*/
|
|
116
126
|
async function isPortInUse(port, host, context) {
|
|
117
|
-
const checkContext = { ...context, operation:
|
|
127
|
+
const checkContext = { ...context, operation: "isPortInUse", port, host };
|
|
118
128
|
logger.debug(`Proactively checking port usability...`, checkContext);
|
|
119
129
|
return new Promise((resolve) => {
|
|
120
130
|
const tempServer = http.createServer();
|
|
121
131
|
tempServer
|
|
122
|
-
.once(
|
|
123
|
-
if (err.code ===
|
|
132
|
+
.once("error", (err) => {
|
|
133
|
+
if (err.code === "EADDRINUSE") {
|
|
124
134
|
logger.debug(`Proactive check: Port confirmed in use (EADDRINUSE).`, checkContext);
|
|
125
135
|
resolve(true); // Port is definitely in use
|
|
126
136
|
}
|
|
@@ -129,7 +139,7 @@ async function isPortInUse(port, host, context) {
|
|
|
129
139
|
resolve(false); // Other error, let main listen attempt handle it
|
|
130
140
|
}
|
|
131
141
|
})
|
|
132
|
-
.once(
|
|
142
|
+
.once("listening", () => {
|
|
133
143
|
logger.debug(`Proactive check: Port is available.`, checkContext);
|
|
134
144
|
tempServer.close(() => resolve(false)); // Port is free
|
|
135
145
|
})
|
|
@@ -149,29 +159,42 @@ async function isPortInUse(port, host, context) {
|
|
|
149
159
|
* @throws {Error} Rejects if binding fails after all retries or for non-EADDRINUSE errors.
|
|
150
160
|
*/
|
|
151
161
|
function startHttpServerWithRetry(serverInstance, initialPort, host, maxRetries, context) {
|
|
152
|
-
const startContext = {
|
|
162
|
+
const startContext = {
|
|
163
|
+
...context,
|
|
164
|
+
operation: "startHttpServerWithRetry",
|
|
165
|
+
initialPort,
|
|
166
|
+
host,
|
|
167
|
+
maxRetries,
|
|
168
|
+
};
|
|
153
169
|
logger.debug(`Attempting to start HTTP server...`, startContext);
|
|
154
170
|
return new Promise(async (resolve, reject) => {
|
|
155
171
|
let lastError = null;
|
|
156
172
|
for (let i = 0; i <= maxRetries; i++) {
|
|
157
173
|
const currentPort = initialPort + i;
|
|
158
|
-
const attemptContext = {
|
|
174
|
+
const attemptContext = {
|
|
175
|
+
...startContext,
|
|
176
|
+
port: currentPort,
|
|
177
|
+
attempt: i + 1,
|
|
178
|
+
maxAttempts: maxRetries + 1,
|
|
179
|
+
};
|
|
159
180
|
logger.debug(`Attempting port ${currentPort} (${attemptContext.attempt}/${attemptContext.maxAttempts})`, attemptContext);
|
|
160
181
|
// 1. Proactive Check
|
|
161
182
|
if (await isPortInUse(currentPort, host, attemptContext)) {
|
|
162
183
|
logger.warning(`Proactive check detected port ${currentPort} is in use, retrying...`, attemptContext);
|
|
163
184
|
lastError = new Error(`EADDRINUSE: Port ${currentPort} detected as in use by proactive check.`);
|
|
164
|
-
await new Promise(res => setTimeout(res, 100)); // Short delay
|
|
185
|
+
await new Promise((res) => setTimeout(res, 100)); // Short delay
|
|
165
186
|
continue; // Try next port
|
|
166
187
|
}
|
|
167
188
|
// 2. Attempt Main Server Bind
|
|
168
189
|
try {
|
|
169
190
|
await new Promise((listenResolve, listenReject) => {
|
|
170
|
-
serverInstance
|
|
191
|
+
serverInstance
|
|
192
|
+
.listen(currentPort, host, () => {
|
|
171
193
|
const serverAddress = `http://${host}:${currentPort}${MCP_ENDPOINT_PATH}`;
|
|
172
194
|
logger.info(`HTTP transport successfully listening on host ${host} at ${serverAddress}`, { ...attemptContext, address: serverAddress });
|
|
173
195
|
listenResolve(); // Success
|
|
174
|
-
})
|
|
196
|
+
})
|
|
197
|
+
.on("error", (err) => {
|
|
175
198
|
listenReject(err); // Forward error
|
|
176
199
|
});
|
|
177
200
|
});
|
|
@@ -181,9 +204,9 @@ function startHttpServerWithRetry(serverInstance, initialPort, host, maxRetries,
|
|
|
181
204
|
catch (err) {
|
|
182
205
|
lastError = err;
|
|
183
206
|
logger.debug(`Listen error on port ${currentPort}: Code=${err.code}, Message=${err.message}`, { ...attemptContext, errorCode: err.code, errorMessage: err.message });
|
|
184
|
-
if (err.code ===
|
|
207
|
+
if (err.code === "EADDRINUSE") {
|
|
185
208
|
logger.warning(`Port ${currentPort} already in use (EADDRINUSE), retrying...`, attemptContext);
|
|
186
|
-
await new Promise(res => setTimeout(res, 100)); // Short delay before retry
|
|
209
|
+
await new Promise((res) => setTimeout(res, 100)); // Short delay before retry
|
|
187
210
|
}
|
|
188
211
|
else {
|
|
189
212
|
logger.error(`Failed to bind to port ${currentPort} due to non-EADDRINUSE error: ${err.message}`, { ...attemptContext, error: err.message });
|
|
@@ -194,7 +217,8 @@ function startHttpServerWithRetry(serverInstance, initialPort, host, maxRetries,
|
|
|
194
217
|
}
|
|
195
218
|
// Loop finished without success
|
|
196
219
|
logger.error(`Failed to bind to any port after ${maxRetries + 1} attempts. Last error: ${lastError?.message}`, { ...startContext, error: lastError?.message });
|
|
197
|
-
reject(lastError ||
|
|
220
|
+
reject(lastError ||
|
|
221
|
+
new Error("Failed to bind to any port after multiple retries."));
|
|
198
222
|
});
|
|
199
223
|
}
|
|
200
224
|
/**
|
|
@@ -210,41 +234,51 @@ function startHttpServerWithRetry(serverInstance, initialPort, host, maxRetries,
|
|
|
210
234
|
*/
|
|
211
235
|
export async function startHttpTransport(createServerInstanceFn, context) {
|
|
212
236
|
const app = express();
|
|
213
|
-
const transportContext = { ...context, transportType:
|
|
214
|
-
logger.debug(
|
|
237
|
+
const transportContext = { ...context, transportType: "HTTP" };
|
|
238
|
+
logger.debug("Setting up Express app for HTTP transport...", transportContext);
|
|
215
239
|
// Middleware to parse JSON request bodies. Required for MCP messages.
|
|
216
240
|
app.use(express.json());
|
|
217
241
|
// --- Security Middleware Pipeline ---
|
|
218
242
|
// 1. CORS Preflight (OPTIONS) Handler
|
|
219
243
|
// Handles OPTIONS requests sent by browsers before actual GET/POST/DELETE.
|
|
220
244
|
app.options(MCP_ENDPOINT_PATH, (req, res) => {
|
|
221
|
-
const optionsContext = {
|
|
245
|
+
const optionsContext = {
|
|
246
|
+
...transportContext,
|
|
247
|
+
operation: "handleOptions",
|
|
248
|
+
origin: req.headers.origin,
|
|
249
|
+
};
|
|
222
250
|
logger.debug(`Received OPTIONS request for ${MCP_ENDPOINT_PATH}`, optionsContext);
|
|
223
251
|
if (isOriginAllowed(req, res)) {
|
|
224
252
|
// isOriginAllowed sets necessary Access-Control-* headers.
|
|
225
|
-
logger.debug(
|
|
253
|
+
logger.debug("OPTIONS request origin allowed, sending 204.", optionsContext);
|
|
226
254
|
res.sendStatus(204); // OK, No Content
|
|
227
255
|
}
|
|
228
256
|
else {
|
|
229
257
|
// isOriginAllowed logs the warning.
|
|
230
|
-
logger.debug(
|
|
231
|
-
res.status(403).send(
|
|
258
|
+
logger.debug("OPTIONS request origin denied, sending 403.", optionsContext);
|
|
259
|
+
res.status(403).send("Forbidden: Invalid Origin");
|
|
232
260
|
}
|
|
233
261
|
});
|
|
234
262
|
// 2. General Security Headers & Origin Check Middleware (for non-OPTIONS)
|
|
235
263
|
app.use((req, res, next) => {
|
|
236
|
-
const securityContext = {
|
|
264
|
+
const securityContext = {
|
|
265
|
+
...transportContext,
|
|
266
|
+
operation: "securityMiddleware",
|
|
267
|
+
path: req.path,
|
|
268
|
+
method: req.method,
|
|
269
|
+
origin: req.headers.origin,
|
|
270
|
+
};
|
|
237
271
|
logger.debug(`Applying security middleware...`, securityContext);
|
|
238
272
|
// Check origin again for non-OPTIONS requests and set CORS headers if allowed.
|
|
239
273
|
if (!isOriginAllowed(req, res)) {
|
|
240
274
|
// isOriginAllowed logs the warning.
|
|
241
|
-
logger.debug(
|
|
242
|
-
res.status(403).send(
|
|
275
|
+
logger.debug("Origin check failed, sending 403.", securityContext);
|
|
276
|
+
res.status(403).send("Forbidden: Invalid Origin");
|
|
243
277
|
return; // Block request
|
|
244
278
|
}
|
|
245
279
|
// Apply standard security headers to all allowed responses.
|
|
246
|
-
res.setHeader(
|
|
247
|
-
res.setHeader(
|
|
280
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
281
|
+
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
248
282
|
// Basic Content Security Policy (CSP). Adjust if server needs external connections.
|
|
249
283
|
// 'connect-src 'self'' allows connections back to the server's own origin (needed for SSE).
|
|
250
284
|
res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'self'; frame-src 'none'; font-src 'self'; connect-src 'self'");
|
|
@@ -252,7 +286,7 @@ export async function startHttpTransport(createServerInstanceFn, context) {
|
|
|
252
286
|
// if (config.environment === 'production') {
|
|
253
287
|
// res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); // 1 year
|
|
254
288
|
// }
|
|
255
|
-
logger.debug(
|
|
289
|
+
logger.debug("Security middleware passed.", securityContext);
|
|
256
290
|
next(); // Proceed to next middleware/handler
|
|
257
291
|
});
|
|
258
292
|
// 3. MCP Authentication Middleware (Optional, based on config)
|
|
@@ -264,18 +298,35 @@ export async function startHttpTransport(createServerInstanceFn, context) {
|
|
|
264
298
|
// MCP Spec: Server responds 202 for notification/response-only, or JSON/SSE for requests.
|
|
265
299
|
app.post(MCP_ENDPOINT_PATH, async (req, res) => {
|
|
266
300
|
// Define base context for this request
|
|
267
|
-
const basePostContext = {
|
|
268
|
-
|
|
301
|
+
const basePostContext = {
|
|
302
|
+
...transportContext,
|
|
303
|
+
operation: "handlePost",
|
|
304
|
+
method: "POST",
|
|
305
|
+
};
|
|
306
|
+
logger.debug(`Received POST request on ${MCP_ENDPOINT_PATH}`, {
|
|
307
|
+
...basePostContext,
|
|
308
|
+
headers: req.headers,
|
|
309
|
+
bodyPreview: JSON.stringify(req.body).substring(0, 100),
|
|
310
|
+
});
|
|
269
311
|
// MCP Spec: Session ID MUST be included by client after initialization.
|
|
270
|
-
const sessionId = req.headers[
|
|
312
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
271
313
|
// Log extracted session ID, adding it to the context for this specific log message
|
|
272
|
-
logger.debug(`Extracted session ID: ${sessionId}`, {
|
|
314
|
+
logger.debug(`Extracted session ID: ${sessionId}`, {
|
|
315
|
+
...basePostContext,
|
|
316
|
+
sessionId,
|
|
317
|
+
});
|
|
273
318
|
let transport = sessionId ? httpTransports[sessionId] : undefined;
|
|
274
319
|
// Log transport lookup result, adding sessionId to context
|
|
275
|
-
logger.debug(`Found existing transport for session ID: ${!!transport}`, {
|
|
320
|
+
logger.debug(`Found existing transport for session ID: ${!!transport}`, {
|
|
321
|
+
...basePostContext,
|
|
322
|
+
sessionId,
|
|
323
|
+
});
|
|
276
324
|
// Check if it's an InitializeRequest using SDK helper.
|
|
277
325
|
const isInitReq = isInitializeRequest(req.body);
|
|
278
|
-
logger.debug(`Is InitializeRequest: ${isInitReq}`, {
|
|
326
|
+
logger.debug(`Is InitializeRequest: ${isInitReq}`, {
|
|
327
|
+
...basePostContext,
|
|
328
|
+
sessionId,
|
|
329
|
+
});
|
|
279
330
|
const requestId = req.body?.id || null; // For potential error responses
|
|
280
331
|
try {
|
|
281
332
|
// --- Handle Initialization Request ---
|
|
@@ -283,11 +334,14 @@ export async function startHttpTransport(createServerInstanceFn, context) {
|
|
|
283
334
|
if (transport) {
|
|
284
335
|
// Client sent Initialize on an existing session - likely an error or recovery attempt.
|
|
285
336
|
// Close the old session cleanly before creating a new one.
|
|
286
|
-
logger.warning(
|
|
337
|
+
logger.warning("Received InitializeRequest on an existing session ID. Closing old session and creating new.", { ...basePostContext, sessionId });
|
|
287
338
|
await transport.close(); // Ensure cleanup
|
|
288
339
|
delete httpTransports[sessionId];
|
|
289
340
|
}
|
|
290
|
-
logger.info(
|
|
341
|
+
logger.info("Handling Initialize Request: Creating new session...", {
|
|
342
|
+
...basePostContext,
|
|
343
|
+
sessionId,
|
|
344
|
+
});
|
|
291
345
|
// Create new SDK transport instance for this session.
|
|
292
346
|
transport = new StreamableHTTPServerTransport({
|
|
293
347
|
// MCP Spec: Server MAY assign session ID on InitializeResponse via Mcp-Session-Id header.
|
|
@@ -300,7 +354,10 @@ export async function startHttpTransport(createServerInstanceFn, context) {
|
|
|
300
354
|
// Store the transport instance once the session ID is confirmed and sent to client.
|
|
301
355
|
logger.debug(`Session initialized callback triggered for ID: ${newId}`, { ...basePostContext, newSessionId: newId });
|
|
302
356
|
httpTransports[newId] = transport; // Store by the generated ID
|
|
303
|
-
logger.info(`HTTP Session created: ${newId}`, {
|
|
357
|
+
logger.info(`HTTP Session created: ${newId}`, {
|
|
358
|
+
...basePostContext,
|
|
359
|
+
newSessionId: newId,
|
|
360
|
+
});
|
|
304
361
|
},
|
|
305
362
|
});
|
|
306
363
|
// Define cleanup logic when the transport closes (client disconnect, DELETE, error).
|
|
@@ -314,23 +371,27 @@ export async function startHttpTransport(createServerInstanceFn, context) {
|
|
|
314
371
|
logger.info(`HTTP Session closed and state cleaned: ${closedSessionId}`, { ...basePostContext, closedSessionId });
|
|
315
372
|
}
|
|
316
373
|
else {
|
|
317
|
-
logger.debug(
|
|
374
|
+
logger.debug("onclose handler triggered for transport without session ID (likely init failure).", basePostContext);
|
|
318
375
|
}
|
|
319
376
|
};
|
|
320
377
|
// Create a dedicated McpServer instance for this new session.
|
|
321
|
-
logger.debug(
|
|
378
|
+
logger.debug("Creating McpServer instance for new session...", basePostContext);
|
|
322
379
|
const server = await createServerInstanceFn();
|
|
323
380
|
// Connect the server logic to the transport layer.
|
|
324
|
-
logger.debug(
|
|
381
|
+
logger.debug("Connecting McpServer to new transport...", basePostContext);
|
|
325
382
|
await server.connect(transport);
|
|
326
|
-
logger.debug(
|
|
383
|
+
logger.debug("McpServer connected to transport.", basePostContext);
|
|
327
384
|
// NOTE: SDK's connect/handleRequest handles sending the InitializeResult.
|
|
328
385
|
}
|
|
329
386
|
else if (!transport) {
|
|
330
387
|
// --- Handle Non-Initialize Request without Valid Session ---
|
|
331
388
|
// MCP Spec: Server SHOULD respond 400/404 if session ID is missing/invalid for non-init requests.
|
|
332
|
-
logger.warning(
|
|
333
|
-
res.status(404).json({
|
|
389
|
+
logger.warning("Invalid or missing session ID for non-initialize POST request.", { ...basePostContext, sessionId });
|
|
390
|
+
res.status(404).json({
|
|
391
|
+
jsonrpc: "2.0",
|
|
392
|
+
error: { code: -32004, message: "Invalid or expired session ID" },
|
|
393
|
+
id: requestId,
|
|
394
|
+
});
|
|
334
395
|
return; // Stop processing
|
|
335
396
|
}
|
|
336
397
|
// --- Handle Request Content (Initialize or Subsequent Message) ---
|
|
@@ -341,8 +402,15 @@ export async function startHttpTransport(createServerInstanceFn, context) {
|
|
|
341
402
|
// The SDK transport handles returning 202 for notification/response-only POSTs internally.
|
|
342
403
|
// --- Type modification for req.auth compatibility ---
|
|
343
404
|
const tempReqPost = req; // Allow modification
|
|
344
|
-
if (tempReqPost.auth &&
|
|
345
|
-
|
|
405
|
+
if (tempReqPost.auth &&
|
|
406
|
+
(typeof tempReqPost.auth === "string" ||
|
|
407
|
+
(typeof tempReqPost.auth === "object" &&
|
|
408
|
+
"devMode" in tempReqPost.auth))) {
|
|
409
|
+
logger.debug("Sanitizing req.auth for SDK compatibility (POST)", {
|
|
410
|
+
...basePostContext,
|
|
411
|
+
sessionId: currentSessionId,
|
|
412
|
+
originalAuthType: typeof tempReqPost.auth,
|
|
413
|
+
});
|
|
346
414
|
tempReqPost.auth = undefined;
|
|
347
415
|
}
|
|
348
416
|
// --- End modification ---
|
|
@@ -353,21 +421,35 @@ export async function startHttpTransport(createServerInstanceFn, context) {
|
|
|
353
421
|
// Catch-all for errors during POST handling.
|
|
354
422
|
// Include sessionId if available in the transport object at this point
|
|
355
423
|
const errorSessionId = transport?.sessionId || sessionId; // Use extracted or from transport if available
|
|
356
|
-
logger.error(
|
|
424
|
+
logger.error("Error handling POST request", {
|
|
357
425
|
...basePostContext,
|
|
358
426
|
sessionId: errorSessionId, // Add sessionId to error context
|
|
359
427
|
isInitReq, // Include isInitReq flag
|
|
360
428
|
error: err instanceof Error ? err.message : String(err),
|
|
361
|
-
stack: err instanceof Error ? err.stack : undefined
|
|
429
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
362
430
|
});
|
|
363
431
|
if (!res.headersSent) {
|
|
364
432
|
// Send generic JSON-RPC error if possible.
|
|
365
|
-
res.status(500).json({
|
|
433
|
+
res.status(500).json({
|
|
434
|
+
jsonrpc: "2.0",
|
|
435
|
+
error: {
|
|
436
|
+
code: -32603,
|
|
437
|
+
message: "Internal server error during POST handling",
|
|
438
|
+
},
|
|
439
|
+
id: requestId,
|
|
440
|
+
});
|
|
366
441
|
}
|
|
367
442
|
// Ensure transport is cleaned up if an error occurred during initialization before session ID assigned.
|
|
368
443
|
if (isInitReq && transport && !transport.sessionId) {
|
|
369
|
-
logger.debug(
|
|
370
|
-
|
|
444
|
+
logger.debug("Cleaning up transport after initialization failure.", {
|
|
445
|
+
...basePostContext,
|
|
446
|
+
sessionId: errorSessionId,
|
|
447
|
+
});
|
|
448
|
+
await transport.close().catch((closeErr) => logger.error("Error closing transport after init failure", {
|
|
449
|
+
...basePostContext,
|
|
450
|
+
sessionId: errorSessionId,
|
|
451
|
+
closeError: closeErr,
|
|
452
|
+
}));
|
|
371
453
|
}
|
|
372
454
|
}
|
|
373
455
|
});
|
|
@@ -375,19 +457,35 @@ export async function startHttpTransport(createServerInstanceFn, context) {
|
|
|
375
457
|
const handleSessionReq = async (req, res) => {
|
|
376
458
|
const method = req.method; // GET or DELETE
|
|
377
459
|
// Define base context for this request
|
|
378
|
-
const baseSessionReqContext = {
|
|
379
|
-
|
|
460
|
+
const baseSessionReqContext = {
|
|
461
|
+
...transportContext,
|
|
462
|
+
operation: `handle${method}`,
|
|
463
|
+
method,
|
|
464
|
+
};
|
|
465
|
+
logger.debug(`Received ${method} request on ${MCP_ENDPOINT_PATH}`, {
|
|
466
|
+
...baseSessionReqContext,
|
|
467
|
+
headers: req.headers,
|
|
468
|
+
});
|
|
380
469
|
// MCP Spec: Client MUST include Mcp-Session-Id header (after init).
|
|
381
|
-
const sessionId = req.headers[
|
|
470
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
382
471
|
// Log extracted session ID, adding it to the context for this specific log message
|
|
383
|
-
logger.debug(`Extracted session ID: ${sessionId}`, {
|
|
472
|
+
logger.debug(`Extracted session ID: ${sessionId}`, {
|
|
473
|
+
...baseSessionReqContext,
|
|
474
|
+
sessionId,
|
|
475
|
+
});
|
|
384
476
|
const transport = sessionId ? httpTransports[sessionId] : undefined;
|
|
385
477
|
// Log transport lookup result, adding sessionId to context
|
|
386
|
-
logger.debug(`Found existing transport for session ID: ${!!transport}`, {
|
|
478
|
+
logger.debug(`Found existing transport for session ID: ${!!transport}`, {
|
|
479
|
+
...baseSessionReqContext,
|
|
480
|
+
sessionId,
|
|
481
|
+
});
|
|
387
482
|
if (!transport) {
|
|
388
483
|
// MCP Spec: Server MUST respond 404 if session ID invalid/expired.
|
|
389
|
-
logger.warning(`Session not found for ${method} request`, {
|
|
390
|
-
|
|
484
|
+
logger.warning(`Session not found for ${method} request`, {
|
|
485
|
+
...baseSessionReqContext,
|
|
486
|
+
sessionId,
|
|
487
|
+
});
|
|
488
|
+
res.status(404).send("Session not found or expired");
|
|
391
489
|
return;
|
|
392
490
|
}
|
|
393
491
|
try {
|
|
@@ -399,8 +497,15 @@ export async function startHttpTransport(createServerInstanceFn, context) {
|
|
|
399
497
|
// This implementation supports DELETE via the SDK transport's handleRequest.
|
|
400
498
|
// --- Type modification for req.auth compatibility ---
|
|
401
499
|
const tempReqSession = req; // Allow modification
|
|
402
|
-
if (tempReqSession.auth &&
|
|
403
|
-
|
|
500
|
+
if (tempReqSession.auth &&
|
|
501
|
+
(typeof tempReqSession.auth === "string" ||
|
|
502
|
+
(typeof tempReqSession.auth === "object" &&
|
|
503
|
+
"devMode" in tempReqSession.auth))) {
|
|
504
|
+
logger.debug(`Sanitizing req.auth for SDK compatibility (${method})`, {
|
|
505
|
+
...baseSessionReqContext,
|
|
506
|
+
sessionId,
|
|
507
|
+
originalAuthType: typeof tempReqSession.auth,
|
|
508
|
+
});
|
|
404
509
|
tempReqSession.auth = undefined;
|
|
405
510
|
}
|
|
406
511
|
// --- End modification ---
|
|
@@ -414,11 +519,11 @@ export async function startHttpTransport(createServerInstanceFn, context) {
|
|
|
414
519
|
...baseSessionReqContext,
|
|
415
520
|
sessionId, // Add sessionId here
|
|
416
521
|
error: err instanceof Error ? err.message : String(err),
|
|
417
|
-
stack: err instanceof Error ? err.stack : undefined
|
|
522
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
418
523
|
});
|
|
419
524
|
if (!res.headersSent) {
|
|
420
525
|
// Generic error if response hasn't started (e.g., error before SSE connection).
|
|
421
|
-
res.status(500).send(
|
|
526
|
+
res.status(500).send("Internal Server Error");
|
|
422
527
|
}
|
|
423
528
|
// The SDK transport's handleRequest should manage errors occurring *during* an SSE stream.
|
|
424
529
|
}
|
|
@@ -427,20 +532,23 @@ export async function startHttpTransport(createServerInstanceFn, context) {
|
|
|
427
532
|
app.get(MCP_ENDPOINT_PATH, handleSessionReq);
|
|
428
533
|
app.delete(MCP_ENDPOINT_PATH, handleSessionReq);
|
|
429
534
|
// --- Start HTTP Server ---
|
|
430
|
-
logger.debug(
|
|
535
|
+
logger.debug("Creating HTTP server instance...", transportContext);
|
|
431
536
|
const serverInstance = http.createServer(app);
|
|
432
537
|
try {
|
|
433
|
-
logger.debug(
|
|
538
|
+
logger.debug("Attempting to start HTTP server with retry logic...", transportContext);
|
|
434
539
|
// Use configured host and port, with retry logic.
|
|
435
540
|
const actualPort = await startHttpServerWithRetry(serverInstance, config.mcpHttpPort, config.mcpHttpHost, MAX_PORT_RETRIES, transportContext);
|
|
436
541
|
// Determine protocol for logging (basic assumption based on HSTS possibility)
|
|
437
|
-
const protocol = config.environment ===
|
|
542
|
+
const protocol = config.environment === "production" ? "https" : "http";
|
|
438
543
|
const serverAddress = `${protocol}://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
|
|
439
544
|
// Use logger.notice for startup message to ensure MCP compliance and proper handling by clients.
|
|
440
545
|
logger.notice(`\nš MCP Server running in HTTP mode at: ${serverAddress}\n (MCP Spec: 2025-03-26 Streamable HTTP Transport)\n`, transportContext);
|
|
441
546
|
}
|
|
442
547
|
catch (err) {
|
|
443
|
-
logger.fatal(
|
|
548
|
+
logger.fatal("HTTP server failed to start after multiple port retries.", {
|
|
549
|
+
...transportContext,
|
|
550
|
+
error: err instanceof Error ? err.message : String(err),
|
|
551
|
+
});
|
|
444
552
|
throw err; // Propagate error to stop the application
|
|
445
553
|
}
|
|
446
554
|
}
|
|
@@ -16,9 +16,9 @@
|
|
|
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
18
|
*/
|
|
19
|
-
import { StdioServerTransport } from
|
|
19
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
20
|
// Import core utilities: ErrorHandler for centralized error management and logger for logging.
|
|
21
|
-
import { ErrorHandler, logger } from
|
|
21
|
+
import { ErrorHandler, logger } from "../../utils/index.js";
|
|
22
22
|
// --- Stdio Session State ---
|
|
23
23
|
// Since stdio typically involves a single, persistent connection managed by a parent,
|
|
24
24
|
// we manage a single working directory state for the entire process.
|
|
@@ -36,7 +36,9 @@ export function getStdioWorkingDirectory() {
|
|
|
36
36
|
*/
|
|
37
37
|
export function setStdioWorkingDirectory(dir) {
|
|
38
38
|
currentWorkingDirectory = dir;
|
|
39
|
-
logger.info(`Stdio working directory set to: ${dir}`, {
|
|
39
|
+
logger.info(`Stdio working directory set to: ${dir}`, {
|
|
40
|
+
operation: "setStdioWorkingDirectory",
|
|
41
|
+
});
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
42
44
|
* Connects a given McpServer instance to the Stdio transport. (Asynchronous)
|
|
@@ -60,20 +62,24 @@ export function setStdioWorkingDirectory(dir) {
|
|
|
60
62
|
*/
|
|
61
63
|
export async function connectStdioTransport(server, context) {
|
|
62
64
|
// Add a specific operation name to the context for better log filtering.
|
|
63
|
-
const operationContext = {
|
|
64
|
-
|
|
65
|
+
const operationContext = {
|
|
66
|
+
...context,
|
|
67
|
+
operation: "connectStdioTransport",
|
|
68
|
+
transportType: "Stdio",
|
|
69
|
+
};
|
|
70
|
+
logger.debug("Attempting to connect stdio transport...", operationContext);
|
|
65
71
|
try {
|
|
66
|
-
logger.debug(
|
|
72
|
+
logger.debug("Creating StdioServerTransport instance...", operationContext);
|
|
67
73
|
// Instantiate the transport provided by the SDK for standard I/O communication.
|
|
68
74
|
// This class encapsulates the logic for reading from stdin and writing to stdout
|
|
69
75
|
// according to the MCP stdio spec.
|
|
70
76
|
const transport = new StdioServerTransport();
|
|
71
|
-
logger.debug(
|
|
77
|
+
logger.debug("Connecting McpServer instance to StdioServerTransport...", operationContext);
|
|
72
78
|
// Establish the link between the server's core logic and the transport layer.
|
|
73
79
|
// This internally starts the necessary listeners on process.stdin.
|
|
74
80
|
await server.connect(transport);
|
|
75
81
|
// Log successful connection. The server is now ready to process messages via stdio.
|
|
76
|
-
logger.info(
|
|
82
|
+
logger.info("MCP Server connected and listening via stdio transport.", operationContext);
|
|
77
83
|
// Use logger.notice for startup message to ensure MCP compliance and proper handling by clients.
|
|
78
84
|
logger.notice(`\nš MCP Server running in STDIO mode.\n (MCP Spec: 2025-03-26 Stdio Transport)\n`, operationContext);
|
|
79
85
|
}
|
|
@@ -49,7 +49,7 @@ export class McpError extends Error {
|
|
|
49
49
|
this.code = code;
|
|
50
50
|
this.details = details;
|
|
51
51
|
// Set the error name for identification
|
|
52
|
-
this.name =
|
|
52
|
+
this.name = "McpError";
|
|
53
53
|
// Ensure the prototype chain is correct
|
|
54
54
|
Object.setPrototypeOf(this, McpError.prototype);
|
|
55
55
|
}
|
|
@@ -58,11 +58,16 @@ export class McpError extends Error {
|
|
|
58
58
|
* Zod schema for validating error objects, potentially used for parsing
|
|
59
59
|
* error responses or validating error structures internally.
|
|
60
60
|
*/
|
|
61
|
-
export const ErrorSchema = z
|
|
61
|
+
export const ErrorSchema = z
|
|
62
|
+
.object({
|
|
62
63
|
/** The error code, corresponding to BaseErrorCode enum values. */
|
|
63
64
|
code: z.nativeEnum(BaseErrorCode).describe("Standardized error code"),
|
|
64
65
|
/** A human-readable description of the error. */
|
|
65
66
|
message: z.string().describe("Detailed error message"),
|
|
66
67
|
/** Optional additional details or context about the error. */
|
|
67
|
-
details: z
|
|
68
|
-
|
|
68
|
+
details: z
|
|
69
|
+
.record(z.unknown())
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("Optional structured error details"),
|
|
72
|
+
})
|
|
73
|
+
.describe("Schema for validating structured error objects.");
|
package/dist/utils/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Re-export all utilities from their categorized subdirectories
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
2
|
+
export * from "./internal/index.js";
|
|
3
|
+
export * from "./parsing/index.js";
|
|
4
|
+
export * from "./security/index.js";
|
|
5
|
+
export * from "./metrics/index.js";
|
|
6
6
|
// It's good practice to have index.ts files in each subdirectory
|
|
7
7
|
// that export the contents of that directory.
|
|
8
8
|
// Assuming those will be created or already exist.
|