@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.
Files changed (97) hide show
  1. package/README.md +8 -11
  2. package/dist/config/index.js +7 -7
  3. package/dist/index.js +35 -21
  4. package/dist/mcp-server/server.js +72 -56
  5. package/dist/mcp-server/tools/gitAdd/index.js +1 -1
  6. package/dist/mcp-server/tools/gitAdd/logic.js +88 -39
  7. package/dist/mcp-server/tools/gitAdd/registration.js +17 -14
  8. package/dist/mcp-server/tools/gitBranch/index.js +1 -1
  9. package/dist/mcp-server/tools/gitBranch/logic.js +213 -85
  10. package/dist/mcp-server/tools/gitBranch/registration.js +16 -13
  11. package/dist/mcp-server/tools/gitCheckout/index.js +1 -1
  12. package/dist/mcp-server/tools/gitCheckout/logic.js +85 -145
  13. package/dist/mcp-server/tools/gitCheckout/registration.js +16 -14
  14. package/dist/mcp-server/tools/gitCherryPick/index.js +1 -1
  15. package/dist/mcp-server/tools/gitCherryPick/logic.js +100 -41
  16. package/dist/mcp-server/tools/gitCherryPick/registration.js +21 -14
  17. package/dist/mcp-server/tools/gitClean/index.js +1 -1
  18. package/dist/mcp-server/tools/gitClean/logic.js +93 -41
  19. package/dist/mcp-server/tools/gitClean/registration.js +19 -16
  20. package/dist/mcp-server/tools/gitClearWorkingDir/index.js +1 -1
  21. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +14 -11
  22. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +19 -13
  23. package/dist/mcp-server/tools/gitClone/index.js +1 -1
  24. package/dist/mcp-server/tools/gitClone/logic.js +89 -30
  25. package/dist/mcp-server/tools/gitClone/registration.js +15 -12
  26. package/dist/mcp-server/tools/gitCommit/index.js +1 -1
  27. package/dist/mcp-server/tools/gitCommit/logic.js +198 -76
  28. package/dist/mcp-server/tools/gitCommit/registration.js +23 -20
  29. package/dist/mcp-server/tools/gitDiff/index.js +1 -1
  30. package/dist/mcp-server/tools/gitDiff/logic.js +124 -44
  31. package/dist/mcp-server/tools/gitDiff/registration.js +16 -14
  32. package/dist/mcp-server/tools/gitFetch/index.js +1 -1
  33. package/dist/mcp-server/tools/gitFetch/logic.js +78 -49
  34. package/dist/mcp-server/tools/gitFetch/registration.js +16 -14
  35. package/dist/mcp-server/tools/gitInit/index.js +1 -1
  36. package/dist/mcp-server/tools/gitInit/logic.js +88 -34
  37. package/dist/mcp-server/tools/gitInit/registration.js +32 -18
  38. package/dist/mcp-server/tools/gitLog/index.js +1 -1
  39. package/dist/mcp-server/tools/gitLog/logic.js +133 -47
  40. package/dist/mcp-server/tools/gitLog/registration.js +16 -14
  41. package/dist/mcp-server/tools/gitMerge/index.js +1 -1
  42. package/dist/mcp-server/tools/gitMerge/logic.js +102 -61
  43. package/dist/mcp-server/tools/gitMerge/registration.js +17 -14
  44. package/dist/mcp-server/tools/gitPull/index.js +1 -1
  45. package/dist/mcp-server/tools/gitPull/logic.js +90 -69
  46. package/dist/mcp-server/tools/gitPull/registration.js +16 -14
  47. package/dist/mcp-server/tools/gitPush/index.js +1 -1
  48. package/dist/mcp-server/tools/gitPush/logic.js +116 -100
  49. package/dist/mcp-server/tools/gitPush/registration.js +16 -14
  50. package/dist/mcp-server/tools/gitRebase/index.js +1 -1
  51. package/dist/mcp-server/tools/gitRebase/logic.js +121 -82
  52. package/dist/mcp-server/tools/gitRebase/registration.js +21 -14
  53. package/dist/mcp-server/tools/gitRemote/index.js +1 -1
  54. package/dist/mcp-server/tools/gitRemote/logic.js +108 -52
  55. package/dist/mcp-server/tools/gitRemote/registration.js +14 -11
  56. package/dist/mcp-server/tools/gitReset/index.js +1 -1
  57. package/dist/mcp-server/tools/gitReset/logic.js +65 -37
  58. package/dist/mcp-server/tools/gitReset/registration.js +14 -12
  59. package/dist/mcp-server/tools/gitSetWorkingDir/index.js +1 -1
  60. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +74 -34
  61. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +18 -11
  62. package/dist/mcp-server/tools/gitShow/index.js +1 -1
  63. package/dist/mcp-server/tools/gitShow/logic.js +78 -35
  64. package/dist/mcp-server/tools/gitShow/registration.js +17 -12
  65. package/dist/mcp-server/tools/gitStash/index.js +1 -1
  66. package/dist/mcp-server/tools/gitStash/logic.js +143 -58
  67. package/dist/mcp-server/tools/gitStash/registration.js +19 -12
  68. package/dist/mcp-server/tools/gitStatus/index.js +1 -1
  69. package/dist/mcp-server/tools/gitStatus/logic.js +100 -58
  70. package/dist/mcp-server/tools/gitStatus/registration.js +15 -12
  71. package/dist/mcp-server/tools/gitTag/index.js +1 -1
  72. package/dist/mcp-server/tools/gitTag/logic.js +124 -51
  73. package/dist/mcp-server/tools/gitTag/registration.js +14 -11
  74. package/dist/mcp-server/tools/gitWorktree/index.js +1 -1
  75. package/dist/mcp-server/tools/gitWorktree/logic.js +204 -95
  76. package/dist/mcp-server/tools/gitWorktree/registration.js +14 -11
  77. package/dist/mcp-server/tools/gitWrapupInstructions/index.js +1 -1
  78. package/dist/mcp-server/tools/gitWrapupInstructions/logic.js +23 -11
  79. package/dist/mcp-server/tools/gitWrapupInstructions/registration.js +14 -12
  80. package/dist/mcp-server/transports/httpTransport.js +187 -79
  81. package/dist/mcp-server/transports/stdioTransport.js +14 -8
  82. package/dist/types-global/errors.js +9 -4
  83. package/dist/utils/index.js +4 -4
  84. package/dist/utils/internal/errorHandler.js +62 -40
  85. package/dist/utils/internal/index.js +3 -3
  86. package/dist/utils/internal/logger.js +97 -54
  87. package/dist/utils/internal/requestContext.js +7 -5
  88. package/dist/utils/metrics/index.js +1 -1
  89. package/dist/utils/metrics/tokenCounter.js +18 -14
  90. package/dist/utils/parsing/dateParser.js +5 -5
  91. package/dist/utils/parsing/index.js +2 -2
  92. package/dist/utils/parsing/jsonParser.js +20 -11
  93. package/dist/utils/security/idGenerator.js +8 -10
  94. package/dist/utils/security/index.js +3 -3
  95. package/dist/utils/security/rateLimiter.js +16 -14
  96. package/dist/utils/security/sanitization.js +139 -82
  97. 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 '@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';
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 '../../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
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 = '/mcp';
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}`, { operation: 'setHttpSessionWorkingDirectory', sessionId });
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 = ['127.0.0.1', '::1', 'localhost'].includes(host);
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 = { operation: 'isOriginAllowed', origin, host, isLocalhostBinding, allowedOrigins };
87
- logger.debug('Checking origin allowance', context);
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)) || (isLocalhostBinding && (!origin || origin === 'null'));
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('Access-Control-Allow-Origin', origin);
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('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
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('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id, Last-Event-ID, Authorization');
97
- res.setHeader('Access-Control-Allow-Credentials', 'true'); // Set based on whether auth/cookies are used
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: 'isPortInUse', port, host };
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('error', (err) => {
123
- if (err.code === 'EADDRINUSE') {
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('listening', () => {
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 = { ...context, operation: 'startHttpServerWithRetry', initialPort, host, maxRetries };
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 = { ...startContext, port: currentPort, attempt: i + 1, maxAttempts: maxRetries + 1 };
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.listen(currentPort, host, () => {
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
- }).on('error', (err) => {
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 === 'EADDRINUSE') {
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 || new Error('Failed to bind to any port after multiple retries.'));
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: 'HTTP' };
214
- logger.debug('Setting up Express app for HTTP transport...', transportContext);
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 = { ...transportContext, operation: 'handleOptions', origin: req.headers.origin };
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('OPTIONS request origin allowed, sending 204.', optionsContext);
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('OPTIONS request origin denied, sending 403.', optionsContext);
231
- res.status(403).send('Forbidden: Invalid Origin');
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 = { ...transportContext, operation: 'securityMiddleware', path: req.path, method: req.method, origin: req.headers.origin };
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('Origin check failed, sending 403.', securityContext);
242
- res.status(403).send('Forbidden: Invalid Origin');
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('X-Content-Type-Options', 'nosniff');
247
- res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
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('Security middleware passed.', securityContext);
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 = { ...transportContext, operation: 'handlePost', method: 'POST' };
268
- logger.debug(`Received POST request on ${MCP_ENDPOINT_PATH}`, { ...basePostContext, headers: req.headers, bodyPreview: JSON.stringify(req.body).substring(0, 100) });
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['mcp-session-id'];
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}`, { ...basePostContext, 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}`, { ...basePostContext, sessionId });
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}`, { ...basePostContext, sessionId });
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('Received InitializeRequest on an existing session ID. Closing old session and creating new.', { ...basePostContext, sessionId });
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('Handling Initialize Request: Creating new session...', { ...basePostContext, sessionId });
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}`, { ...basePostContext, newSessionId: 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('onclose handler triggered for transport without session ID (likely init failure).', basePostContext);
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('Creating McpServer instance for new session...', basePostContext);
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('Connecting McpServer to new transport...', basePostContext);
381
+ logger.debug("Connecting McpServer to new transport...", basePostContext);
325
382
  await server.connect(transport);
326
- logger.debug('McpServer connected to transport.', basePostContext);
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('Invalid or missing session ID for non-initialize POST request.', { ...basePostContext, sessionId });
333
- res.status(404).json({ jsonrpc: '2.0', error: { code: -32004, message: 'Invalid or expired session ID' }, id: requestId });
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 && (typeof tempReqPost.auth === 'string' || (typeof tempReqPost.auth === 'object' && 'devMode' in tempReqPost.auth))) {
345
- logger.debug('Sanitizing req.auth for SDK compatibility (POST)', { ...basePostContext, sessionId: currentSessionId, originalAuthType: typeof tempReqPost.auth });
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('Error handling POST request', {
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({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error during POST handling' }, id: requestId });
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('Cleaning up transport after initialization failure.', { ...basePostContext, sessionId: errorSessionId });
370
- await transport.close().catch(closeErr => logger.error('Error closing transport after init failure', { ...basePostContext, sessionId: errorSessionId, closeError: closeErr }));
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 = { ...transportContext, operation: `handle${method}`, method };
379
- logger.debug(`Received ${method} request on ${MCP_ENDPOINT_PATH}`, { ...baseSessionReqContext, headers: req.headers });
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['mcp-session-id'];
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}`, { ...baseSessionReqContext, 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}`, { ...baseSessionReqContext, sessionId });
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`, { ...baseSessionReqContext, sessionId });
390
- res.status(404).send('Session not found or expired');
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 && (typeof tempReqSession.auth === 'string' || (typeof tempReqSession.auth === 'object' && 'devMode' in tempReqSession.auth))) {
403
- logger.debug(`Sanitizing req.auth for SDK compatibility (${method})`, { ...baseSessionReqContext, sessionId, originalAuthType: typeof tempReqSession.auth });
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('Internal Server Error');
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('Creating HTTP server instance...', transportContext);
535
+ logger.debug("Creating HTTP server instance...", transportContext);
431
536
  const serverInstance = http.createServer(app);
432
537
  try {
433
- logger.debug('Attempting to start HTTP server with retry logic...', transportContext);
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 === 'production' ? 'https' : 'http';
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('HTTP server failed to start after multiple port retries.', { ...transportContext, error: err instanceof Error ? err.message : String(err) });
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 '@modelcontextprotocol/sdk/server/stdio.js';
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 '../../utils/index.js';
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}`, { operation: 'setStdioWorkingDirectory' });
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 = { ...context, operation: 'connectStdioTransport', transportType: 'Stdio' };
64
- logger.debug('Attempting to connect stdio transport...', operationContext);
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('Creating StdioServerTransport instance...', operationContext);
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('Connecting McpServer instance to StdioServerTransport...', operationContext);
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('MCP Server connected and listening via stdio transport.', operationContext);
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 = 'McpError';
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.object({
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.record(z.unknown()).optional().describe("Optional structured error details")
68
- }).describe("Schema for validating structured error objects.");
68
+ details: z
69
+ .record(z.unknown())
70
+ .optional()
71
+ .describe("Optional structured error details"),
72
+ })
73
+ .describe("Schema for validating structured error objects.");
@@ -1,8 +1,8 @@
1
1
  // Re-export all utilities from their categorized subdirectories
2
- export * from './internal/index.js';
3
- export * from './parsing/index.js';
4
- export * from './security/index.js';
5
- export * from './metrics/index.js';
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.