@cyanheads/git-mcp-server 1.2.4 → 2.0.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 (105) hide show
  1. package/README.md +172 -285
  2. package/dist/config/index.js +69 -0
  3. package/dist/index.js +135 -0
  4. package/dist/mcp-server/server.js +572 -0
  5. package/dist/mcp-server/tools/gitAdd/index.js +7 -0
  6. package/dist/mcp-server/tools/gitAdd/logic.js +118 -0
  7. package/dist/mcp-server/tools/gitAdd/registration.js +73 -0
  8. package/dist/mcp-server/tools/gitBranch/index.js +7 -0
  9. package/dist/mcp-server/tools/gitBranch/logic.js +180 -0
  10. package/dist/mcp-server/tools/gitBranch/registration.js +72 -0
  11. package/dist/mcp-server/tools/gitCheckout/index.js +6 -0
  12. package/dist/mcp-server/tools/gitCheckout/logic.js +165 -0
  13. package/dist/mcp-server/tools/gitCheckout/registration.js +78 -0
  14. package/dist/mcp-server/tools/gitCherryPick/index.js +7 -0
  15. package/dist/mcp-server/tools/gitCherryPick/logic.js +115 -0
  16. package/dist/mcp-server/tools/gitCherryPick/registration.js +69 -0
  17. package/dist/mcp-server/tools/gitClean/index.js +7 -0
  18. package/dist/mcp-server/tools/gitClean/logic.js +110 -0
  19. package/dist/mcp-server/tools/gitClean/registration.js +98 -0
  20. package/dist/mcp-server/tools/gitClearWorkingDir/index.js +7 -0
  21. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +35 -0
  22. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +73 -0
  23. package/dist/mcp-server/tools/gitClone/index.js +7 -0
  24. package/dist/mcp-server/tools/gitClone/logic.js +136 -0
  25. package/dist/mcp-server/tools/gitClone/registration.js +44 -0
  26. package/dist/mcp-server/tools/gitCommit/index.js +7 -0
  27. package/dist/mcp-server/tools/gitCommit/logic.js +129 -0
  28. package/dist/mcp-server/tools/gitCommit/registration.js +100 -0
  29. package/dist/mcp-server/tools/gitDiff/index.js +6 -0
  30. package/dist/mcp-server/tools/gitDiff/logic.js +114 -0
  31. package/dist/mcp-server/tools/gitDiff/registration.js +74 -0
  32. package/dist/mcp-server/tools/gitFetch/index.js +6 -0
  33. package/dist/mcp-server/tools/gitFetch/logic.js +116 -0
  34. package/dist/mcp-server/tools/gitFetch/registration.js +71 -0
  35. package/dist/mcp-server/tools/gitInit/index.js +7 -0
  36. package/dist/mcp-server/tools/gitInit/logic.js +117 -0
  37. package/dist/mcp-server/tools/gitInit/registration.js +44 -0
  38. package/dist/mcp-server/tools/gitLog/index.js +6 -0
  39. package/dist/mcp-server/tools/gitLog/logic.js +148 -0
  40. package/dist/mcp-server/tools/gitLog/registration.js +71 -0
  41. package/dist/mcp-server/tools/gitMerge/index.js +7 -0
  42. package/dist/mcp-server/tools/gitMerge/logic.js +160 -0
  43. package/dist/mcp-server/tools/gitMerge/registration.js +77 -0
  44. package/dist/mcp-server/tools/gitPull/index.js +6 -0
  45. package/dist/mcp-server/tools/gitPull/logic.js +144 -0
  46. package/dist/mcp-server/tools/gitPull/registration.js +81 -0
  47. package/dist/mcp-server/tools/gitPush/index.js +6 -0
  48. package/dist/mcp-server/tools/gitPush/logic.js +188 -0
  49. package/dist/mcp-server/tools/gitPush/registration.js +81 -0
  50. package/dist/mcp-server/tools/gitRebase/index.js +7 -0
  51. package/dist/mcp-server/tools/gitRebase/logic.js +171 -0
  52. package/dist/mcp-server/tools/gitRebase/registration.js +72 -0
  53. package/dist/mcp-server/tools/gitRemote/index.js +7 -0
  54. package/dist/mcp-server/tools/gitRemote/logic.js +158 -0
  55. package/dist/mcp-server/tools/gitRemote/registration.js +76 -0
  56. package/dist/mcp-server/tools/gitReset/index.js +6 -0
  57. package/dist/mcp-server/tools/gitReset/logic.js +116 -0
  58. package/dist/mcp-server/tools/gitReset/registration.js +71 -0
  59. package/dist/mcp-server/tools/gitSetWorkingDir/index.js +7 -0
  60. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +91 -0
  61. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +78 -0
  62. package/dist/mcp-server/tools/gitShow/index.js +7 -0
  63. package/dist/mcp-server/tools/gitShow/logic.js +99 -0
  64. package/dist/mcp-server/tools/gitShow/registration.js +83 -0
  65. package/dist/mcp-server/tools/gitStash/index.js +7 -0
  66. package/dist/mcp-server/tools/gitStash/logic.js +161 -0
  67. package/dist/mcp-server/tools/gitStash/registration.js +84 -0
  68. package/dist/mcp-server/tools/gitStatus/index.js +7 -0
  69. package/dist/mcp-server/tools/gitStatus/logic.js +215 -0
  70. package/dist/mcp-server/tools/gitStatus/registration.js +77 -0
  71. package/dist/mcp-server/tools/gitTag/index.js +7 -0
  72. package/dist/mcp-server/tools/gitTag/logic.js +142 -0
  73. package/dist/mcp-server/tools/gitTag/registration.js +84 -0
  74. package/dist/types-global/errors.js +68 -0
  75. package/dist/types-global/mcp.js +59 -0
  76. package/dist/types-global/tool.js +1 -0
  77. package/dist/utils/errorHandler.js +237 -0
  78. package/dist/utils/idGenerator.js +148 -0
  79. package/dist/utils/index.js +11 -0
  80. package/dist/utils/jsonParser.js +78 -0
  81. package/dist/utils/logger.js +266 -0
  82. package/dist/utils/rateLimiter.js +177 -0
  83. package/dist/utils/requestContext.js +49 -0
  84. package/dist/utils/sanitization.js +371 -0
  85. package/dist/utils/tokenCounter.js +124 -0
  86. package/package.json +62 -17
  87. package/build/index.js +0 -54
  88. package/build/resources/descriptors.js +0 -77
  89. package/build/resources/diff.js +0 -241
  90. package/build/resources/file.js +0 -222
  91. package/build/resources/history.js +0 -242
  92. package/build/resources/index.js +0 -99
  93. package/build/resources/repository.js +0 -286
  94. package/build/server.js +0 -120
  95. package/build/services/error-service.js +0 -73
  96. package/build/services/git-service.js +0 -965
  97. package/build/tools/advanced.js +0 -526
  98. package/build/tools/branch.js +0 -296
  99. package/build/tools/index.js +0 -29
  100. package/build/tools/remote.js +0 -279
  101. package/build/tools/repository.js +0 -170
  102. package/build/tools/workdir.js +0 -445
  103. package/build/types/git.js +0 -7
  104. package/build/utils/global-settings.js +0 -64
  105. package/build/utils/validation.js +0 -108
@@ -0,0 +1,572 @@
1
+ /**
2
+ * @fileoverview Main entry point for the MCP (Model Context Protocol) server.
3
+ * This file sets up the server instance, configures the transport layer (stdio or HTTP),
4
+ * registers resources and tools, and handles incoming MCP requests.
5
+ * It supports both standard input/output communication and HTTP-based communication
6
+ * with Server-Sent Events (SSE) for streaming responses.
7
+ */
8
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
11
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
12
+ import express from 'express';
13
+ import http from 'http';
14
+ import { randomUUID } from 'node:crypto';
15
+ import { config, environment } from '../config/index.js';
16
+ import { ErrorHandler } from '../utils/errorHandler.js';
17
+ import { logger } from '../utils/logger.js';
18
+ import { requestContextService } from '../utils/requestContext.js';
19
+ import { registerGitAddTool } from './tools/gitAdd/index.js'; // Import git_add
20
+ import { initializeGitBranchStateAccessors, registerGitBranchTool } from './tools/gitBranch/index.js'; // Import git_branch
21
+ import { initializeGitCheckoutStateAccessors, registerGitCheckoutTool } from './tools/gitCheckout/index.js'; // Import git_checkout
22
+ import { initializeGitCherryPickStateAccessors, registerGitCherryPickTool } from './tools/gitCherryPick/index.js'; // Import git_cherry_pick
23
+ import { initializeGitCleanStateAccessors, registerGitCleanTool } from './tools/gitClean/index.js'; // Import git_clean
24
+ import { initializeGitClearWorkingDirStateAccessors, registerGitClearWorkingDirTool } from './tools/gitClearWorkingDir/index.js'; // Import git_clear_working_dir
25
+ import { registerGitCloneTool } from './tools/gitClone/index.js'; // Import git_clone
26
+ import { registerGitCommitTool } from './tools/gitCommit/index.js'; // Import git_commit
27
+ import { initializeGitDiffStateAccessors, registerGitDiffTool } from './tools/gitDiff/index.js'; // Import git_diff
28
+ import { initializeGitFetchStateAccessors, registerGitFetchTool } from './tools/gitFetch/index.js'; // Import git_fetch
29
+ import { registerGitInitTool } from './tools/gitInit/index.js'; // Import git_init
30
+ import { initializeGitLogStateAccessors, registerGitLogTool } from './tools/gitLog/index.js'; // Import git_log
31
+ import { initializeGitMergeStateAccessors, registerGitMergeTool } from './tools/gitMerge/index.js'; // Import git_merge
32
+ import { initializeGitPullStateAccessors, registerGitPullTool } from './tools/gitPull/index.js'; // Import git_pull
33
+ import { initializeGitPushStateAccessors, registerGitPushTool } from './tools/gitPush/index.js'; // Import git_push
34
+ import { initializeGitRebaseStateAccessors, registerGitRebaseTool } from './tools/gitRebase/index.js'; // Import git_rebase
35
+ import { initializeGitRemoteStateAccessors, registerGitRemoteTool } from './tools/gitRemote/index.js'; // Import git_remote
36
+ import { initializeGitResetStateAccessors, registerGitResetTool } from './tools/gitReset/index.js'; // Import git_reset
37
+ import { initializeGitSetWorkingDirStateAccessors, registerGitSetWorkingDirTool } from './tools/gitSetWorkingDir/index.js'; // Import git_set_working_dir
38
+ import { initializeGitShowStateAccessors, registerGitShowTool } from './tools/gitShow/index.js'; // Import git_show
39
+ import { initializeGitStashStateAccessors, registerGitStashTool } from './tools/gitStash/index.js'; // Import git_stash
40
+ import { registerGitStatusTool } from './tools/gitStatus/index.js'; // Import git_status
41
+ import { initializeGitTagStateAccessors, registerGitTagTool } from './tools/gitTag/index.js'; // Import git_tag
42
+ // --- Import Accessor Inits ---
43
+ import { initializeGitAddStateAccessors } from './tools/gitAdd/index.js'; // Import add accessor init
44
+ import { initializeGitCommitStateAccessors } from './tools/gitCommit/index.js'; // Import commit accessor init
45
+ import { initializeGitStatusStateAccessors } from './tools/gitStatus/index.js'; // Import status accessor init
46
+ // --- Configuration Constants ---
47
+ /**
48
+ * Determines the transport type for the MCP server based on the MCP_TRANSPORT_TYPE environment variable.
49
+ * Defaults to 'stdio' if the variable is not set. Converts the value to lowercase.
50
+ * @constant {string} TRANSPORT_TYPE - The transport type ('stdio' or 'http').
51
+ */
52
+ const TRANSPORT_TYPE = (process.env.MCP_TRANSPORT_TYPE || 'stdio').toLowerCase();
53
+ /**
54
+ * The port number for the HTTP transport, configured via the MCP_HTTP_PORT environment variable.
55
+ * Defaults to 3000 if the variable is not set or invalid.
56
+ * @constant {number} HTTP_PORT - The port number for the HTTP server.
57
+ */
58
+ const HTTP_PORT = process.env.MCP_HTTP_PORT ? parseInt(process.env.MCP_HTTP_PORT, 10) : 3000;
59
+ /**
60
+ * The host address for the HTTP transport, configured via the MCP_HTTP_HOST environment variable.
61
+ * Defaults to '127.0.0.1' (localhost) if the variable is not set.
62
+ * @constant {string} HTTP_HOST - The host address for the HTTP server.
63
+ */
64
+ const HTTP_HOST = process.env.MCP_HTTP_HOST || '127.0.0.1';
65
+ /**
66
+ * The specific endpoint path for handling MCP requests over HTTP.
67
+ * @constant {string} MCP_ENDPOINT_PATH - The URL path for MCP communication.
68
+ */
69
+ const MCP_ENDPOINT_PATH = '/mcp';
70
+ /**
71
+ * The maximum number of attempts to find an available port if the initial HTTP_PORT is in use.
72
+ * The server will try `HTTP_PORT`, `HTTP_PORT + 1`, ..., `HTTP_PORT + MAX_PORT_RETRIES`.
73
+ * @constant {number} MAX_PORT_RETRIES - Maximum retry attempts for port binding.
74
+ */
75
+ const MAX_PORT_RETRIES = 15;
76
+ /**
77
+ * A record (dictionary/map) to store active HTTP transport instances, keyed by their session ID.
78
+ * This allows associating incoming HTTP requests with the correct ongoing MCP session.
79
+ * @type {Record<string, StreamableHTTPServerTransport>}
80
+ */
81
+ const httpTransports = {};
82
+ /**
83
+ * Stores the current working directory setting for each active HTTP session.
84
+ * Keyed by session ID. Undefined means no specific working directory is set for the session.
85
+ * @type {Record<string, string | undefined>}
86
+ */
87
+ const sessionWorkingDirectories = {};
88
+ /**
89
+ * Checks if an incoming HTTP request's origin header is permissible based on configuration.
90
+ * It considers the `MCP_ALLOWED_ORIGINS` environment variable and whether the server
91
+ * is bound to a loopback address (localhost). If allowed, it sets appropriate
92
+ * Cross-Origin Resource Sharing (CORS) headers on the response.
93
+ *
94
+ * Security Note: Carefully configure `MCP_ALLOWED_ORIGINS` in production environments
95
+ * to prevent unauthorized websites from interacting with the MCP server.
96
+ *
97
+ * @param {Request} req - The Express request object, containing headers like 'origin'.
98
+ * @param {Response} res - The Express response object, used to set CORS headers.
99
+ * @returns {boolean} Returns `true` if the origin is allowed, `false` otherwise.
100
+ */
101
+ function isOriginAllowed(req, res) {
102
+ const origin = req.headers.origin;
103
+ // Use req.hostname which correctly considers the Host header or falls back
104
+ const host = req.hostname;
105
+ // Check if the server is effectively bound only to loopback addresses
106
+ const isLocalhostBinding = ['127.0.0.1', '::1', 'localhost'].includes(host);
107
+ // Retrieve allowed origins from environment variable, split into an array
108
+ const allowedOrigins = process.env.MCP_ALLOWED_ORIGINS?.split(',') || [];
109
+ // Determine if the origin is allowed:
110
+ // 1. The origin header is present AND is in the configured allowed list.
111
+ // OR
112
+ // 2. The server is bound to localhost AND the origin header is missing or 'null' (common for local file access or redirects).
113
+ const allowed = (origin && allowedOrigins.includes(origin)) || (isLocalhostBinding && (!origin || origin === 'null'));
114
+ if (allowed && origin) {
115
+ // If allowed and an origin was provided, set CORS headers to allow the specific origin.
116
+ res.setHeader('Access-Control-Allow-Origin', origin);
117
+ // Allow necessary HTTP methods for MCP communication.
118
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
119
+ // Allow standard MCP headers and Content-Type. Last-Event-ID is for SSE resumption.
120
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id, Last-Event-ID');
121
+ // Set credentials allowance if needed (e.g., if cookies or authentication headers are involved).
122
+ res.setHeader('Access-Control-Allow-Credentials', 'true'); // Adjust if using credentials
123
+ }
124
+ else if (allowed && !origin) {
125
+ // Origin is allowed (e.g., localhost binding with missing/null origin), but no origin header to echo back.
126
+ // No specific CORS headers needed in this case as there's no origin to restrict/allow.
127
+ }
128
+ else if (!allowed && origin) {
129
+ // Log a warning if an origin was provided but is not allowed.
130
+ logger.warning(`Origin denied: ${origin}`, { operation: 'isOriginAllowed', origin, host, allowedOrigins, isLocalhostBinding });
131
+ }
132
+ // Note: If !allowed and !origin, no action/logging is needed.
133
+ return allowed;
134
+ }
135
+ /**
136
+ * Creates and configures a new instance of the McpServer.
137
+ * This function encapsulates the server setup, including setting the server name,
138
+ * version, capabilities, and registering all defined resources and tools.
139
+ * It's designed to be called either once for the stdio transport or potentially
140
+ * multiple times for stateless handling in the HTTP transport (though currently
141
+ * used once per session in HTTP).
142
+ *
143
+ * @async
144
+ * @returns {Promise<McpServer>} A promise that resolves with the fully configured McpServer instance.
145
+ * @throws {Error} Throws an error if the registration of any resource or tool fails.
146
+ */
147
+ async function createMcpServerInstance() {
148
+ const context = { operation: 'createMcpServerInstance' };
149
+ logger.info('Initializing MCP server instance', context);
150
+ // Configure the request context service for associating logs/traces with specific requests or operations.
151
+ requestContextService.configure({
152
+ appName: config.mcpServerName,
153
+ appVersion: config.mcpServerVersion,
154
+ environment,
155
+ });
156
+ // Instantiate the core McpServer with its identity and declared capabilities.
157
+ // Capabilities inform the client about what features the server supports (e.g., logging).
158
+ const server = new McpServer({ name: config.mcpServerName, version: config.mcpServerVersion }, { capabilities: { logging: {}, tools: { listChanged: true } } });
159
+ try {
160
+ // Register all available tools with the server instance.
161
+ // These functions typically call `server.tool()`.
162
+ await registerGitAddTool(server); // Register git_add tool
163
+ await registerGitBranchTool(server); // Added unified git_branch registration
164
+ await registerGitCheckoutTool(server); // Register git_checkout tool
165
+ await registerGitCherryPickTool(server); // Added git_cherry_pick registration
166
+ await registerGitCleanTool(server); // Register git_clean tool
167
+ await registerGitClearWorkingDirTool(server); // Register the git_clear_working_dir tool
168
+ await registerGitCloneTool(server); // Added clone registration
169
+ await registerGitCommitTool(server); // Register git_commit tool
170
+ await registerGitDiffTool(server); // Register git_diff tool
171
+ await registerGitFetchTool(server); // Register git_fetch tool
172
+ await registerGitInitTool(server); // Added init registration
173
+ await registerGitLogTool(server); // Register git_log tool
174
+ await registerGitMergeTool(server); // Register git_merge tool
175
+ await registerGitPullTool(server); // Register git_pull tool
176
+ await registerGitPushTool(server); // Register git_push tool
177
+ await registerGitRebaseTool(server); // Added git_rebase registration
178
+ await registerGitRemoteTool(server); // Register git_remote tool
179
+ await registerGitResetTool(server); // Register git_reset tool
180
+ await registerGitSetWorkingDirTool(server); // Register git_set_working_dir tool
181
+ await registerGitShowTool(server); // Register git_show tool
182
+ await registerGitStashTool(server); // Register git_stash tool
183
+ await registerGitStatusTool(server); // Register git_status tool
184
+ await registerGitTagTool(server); // Register git_tag tool
185
+ logger.info('All Git tools registered successfully', context);
186
+ }
187
+ catch (err) {
188
+ // Log and re-throw any errors during registration, as the server cannot function correctly without them.
189
+ logger.error('Failed to register resources/tools', {
190
+ ...context,
191
+ error: err instanceof Error ? err.message : String(err),
192
+ });
193
+ throw err; // Propagate the error to the caller.
194
+ }
195
+ return server;
196
+ }
197
+ /**
198
+ * Attempts to start the Node.js HTTP server on a specified port and host.
199
+ * If the initial port is already in use (EADDRINUSE error), it increments the port
200
+ * number and retries, up to a maximum number of retries (`maxRetries`).
201
+ *
202
+ * @async
203
+ * @param {http.Server} serverInstance - The Node.js HTTP server instance to start.
204
+ * @param {number} initialPort - The first port number to attempt binding to.
205
+ * @param {string} host - The host address to bind to (e.g., '127.0.0.1').
206
+ * @param {number} maxRetries - The maximum number of additional ports to try (initialPort + 1, initialPort + 2, ...).
207
+ * @param {Record<string, any>} context - Logging context to associate with log messages.
208
+ * @returns {Promise<number>} A promise that resolves with the port number the server successfully bound to.
209
+ * @throws {Error} Rejects if the server fails to bind to any port after all retries, or if a non-EADDRINUSE error occurs.
210
+ */
211
+ function startHttpServerWithRetry(serverInstance, initialPort, host, maxRetries, context) {
212
+ return new Promise(async (resolve, reject) => {
213
+ let lastError = null;
214
+ // Loop through ports: initialPort, initialPort + 1, ..., initialPort + maxRetries
215
+ for (let i = 0; i <= maxRetries; i++) {
216
+ const currentPort = initialPort + i;
217
+ try {
218
+ // Attempt to listen on the current port and host.
219
+ await new Promise((listenResolve, listenReject) => {
220
+ serverInstance.listen(currentPort, host, () => {
221
+ // If listen succeeds immediately, log the success and resolve the inner promise.
222
+ const serverAddress = `http://${host}:${currentPort}${MCP_ENDPOINT_PATH}`;
223
+ logger.info(`HTTP transport listening at ${serverAddress}`, { ...context, port: currentPort, address: serverAddress });
224
+ listenResolve();
225
+ }).on('error', (err) => {
226
+ // If an error occurs during listen (e.g., EADDRINUSE), reject the inner promise.
227
+ listenReject(err);
228
+ });
229
+ });
230
+ // If the inner promise resolved (listen was successful), resolve the outer promise with the port used.
231
+ resolve(currentPort);
232
+ return; // Exit the loop and the function.
233
+ }
234
+ catch (err) {
235
+ lastError = err; // Store the error for potential final rejection message.
236
+ if (err.code === 'EADDRINUSE') {
237
+ // If the port is in use, log a warning and continue to the next iteration.
238
+ logger.warning(`Port ${currentPort} already in use, retrying... (${i + 1}/${maxRetries + 1})`, { ...context, port: currentPort });
239
+ // Optional delay before retrying to allow the other process potentially release the port.
240
+ await new Promise(res => setTimeout(res, 100));
241
+ }
242
+ else {
243
+ // If a different error occurred (e.g., permission denied), log it and reject immediately.
244
+ logger.error(`Failed to bind to port ${currentPort}: ${err.message}`, { ...context, port: currentPort, error: err.message });
245
+ reject(err);
246
+ return; // Exit the loop and the function.
247
+ }
248
+ }
249
+ }
250
+ // If the loop completes without successfully binding to any port.
251
+ logger.error(`Failed to bind to any port after ${maxRetries + 1} attempts. Last error: ${lastError?.message}`, { ...context, initialPort, maxRetries, error: lastError?.message });
252
+ reject(lastError || new Error('Failed to bind to any port after multiple retries.'));
253
+ });
254
+ }
255
+ /**
256
+ * Sets up and starts the MCP transport layer based on the `TRANSPORT_TYPE` constant.
257
+ *
258
+ * If `TRANSPORT_TYPE` is 'http':
259
+ * - Creates an Express application.
260
+ * - Configures middleware for JSON parsing and CORS handling (using `isOriginAllowed`).
261
+ * - Defines endpoints (`POST`, `GET`, `DELETE` at `MCP_ENDPOINT_PATH`) to handle MCP requests:
262
+ * - `POST`: Handles initialization requests (creating new sessions/transports) and subsequent message requests for existing sessions.
263
+ * - `GET`: Handles establishing the Server-Sent Events (SSE) connection for streaming responses.
264
+ * - `DELETE`: Handles session termination requests.
265
+ * - Manages session lifecycles using the `httpTransports` map.
266
+ * - Starts the HTTP server using `startHttpServerWithRetry`.
267
+ *
268
+ * If `TRANSPORT_TYPE` is 'stdio':
269
+ * - Creates a single `McpServer` instance.
270
+ * - Creates a `StdioServerTransport`.
271
+ * - Connects the server and transport to process messages via standard input/output.
272
+ * - Returns the created `McpServer` instance.
273
+ *
274
+ * @async
275
+ * @returns {Promise<McpServer | void>} For 'stdio' transport, returns the `McpServer` instance. For 'http' transport, returns `void` as the server runs indefinitely.
276
+ * @throws {Error} Throws an error if the transport type is unsupported, or if server creation/connection fails.
277
+ */
278
+ async function startTransport() {
279
+ const context = { operation: 'startTransport', transport: TRANSPORT_TYPE };
280
+ logger.info(`Starting transport: ${TRANSPORT_TYPE}`, context);
281
+ // Variable to hold the working directory for the single stdio session.
282
+ // Declared here so it's accessible in the closure of setWorkingDirectoryFn.
283
+ let stdioWorkingDirectory;
284
+ // --- Define State Accessor Functions ---
285
+ // These functions provide a bridge between the tool registration logic and the transport-specific state.
286
+ const setWorkingDirectoryFn = (sessionId, path) => {
287
+ if (TRANSPORT_TYPE === 'http') {
288
+ if (sessionId && sessionId in sessionWorkingDirectories) {
289
+ sessionWorkingDirectories[sessionId] = path;
290
+ logger.debug(`Set working directory for HTTP session ${sessionId} to ${path}`, { ...context, sessionId });
291
+ }
292
+ else {
293
+ logger.error(`Attempted to set working directory for unknown HTTP session: ${sessionId}`, { ...context, sessionId });
294
+ }
295
+ }
296
+ else if (TRANSPORT_TYPE === 'stdio') {
297
+ // For stdio, we modify the variable directly (assuming it's accessible in this scope)
298
+ stdioWorkingDirectory = path; // This relies on stdioWorkingDirectory being declared below
299
+ logger.debug(`Set working directory for stdio session to ${path}`, context);
300
+ }
301
+ };
302
+ const clearWorkingDirectoryFn = (sessionId) => {
303
+ if (TRANSPORT_TYPE === 'http') {
304
+ if (sessionId && sessionId in sessionWorkingDirectories) {
305
+ sessionWorkingDirectories[sessionId] = undefined; // Set to undefined to clear
306
+ logger.debug(`Cleared working directory for HTTP session ${sessionId}`, { ...context, sessionId });
307
+ }
308
+ else {
309
+ // Log warning instead of error, as clearing a non-existent/already cleared session isn't critical
310
+ logger.warning(`Attempted to clear working directory for unknown or already cleared HTTP session: ${sessionId}`, { ...context, sessionId });
311
+ }
312
+ }
313
+ else if (TRANSPORT_TYPE === 'stdio') {
314
+ stdioWorkingDirectory = undefined; // Set to undefined to clear
315
+ logger.debug(`Cleared working directory for stdio session`, context);
316
+ }
317
+ };
318
+ const getWorkingDirectoryFn = (sessionId) => {
319
+ if (TRANSPORT_TYPE === 'http') {
320
+ return sessionId ? sessionWorkingDirectories[sessionId] : undefined;
321
+ }
322
+ else if (TRANSPORT_TYPE === 'stdio') {
323
+ return stdioWorkingDirectory;
324
+ }
325
+ return undefined; // Should not happen
326
+ };
327
+ const getSessionIdFn = (reqContext) => {
328
+ // The SDK's callContext passed to the tool handler might contain session info.
329
+ // Alternatively, our RequestContext might have it if populated correctly.
330
+ // Let's assume it's available as 'sessionId' in the context passed to the tool handler.
331
+ // This might need refinement based on how the SDK passes context.
332
+ return reqContext?.sessionId;
333
+ };
334
+ // Initialize the state accessors for the tools that need them
335
+ initializeGitAddStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_add accessors
336
+ initializeGitBranchStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_branch accessors
337
+ initializeGitCheckoutStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_checkout accessors
338
+ initializeGitCherryPickStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_cherry_pick accessors
339
+ initializeGitCleanStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_clean accessors
340
+ initializeGitClearWorkingDirStateAccessors(clearWorkingDirectoryFn, getSessionIdFn); // Initialize git_clear_working_dir accessors
341
+ // initializeGitCloneStateAccessors - No state needed for clone
342
+ initializeGitCommitStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_commit accessors
343
+ initializeGitDiffStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_diff accessors
344
+ initializeGitFetchStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_fetch accessors
345
+ // initializeGitInitStateAccessors - No state needed for init
346
+ initializeGitLogStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_log accessors
347
+ initializeGitMergeStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_merge accessors
348
+ initializeGitPullStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_pull accessors
349
+ initializeGitPushStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_push accessors
350
+ initializeGitRebaseStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_rebase accessors
351
+ initializeGitRemoteStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_remote accessors
352
+ initializeGitResetStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_reset accessors
353
+ initializeGitSetWorkingDirStateAccessors(setWorkingDirectoryFn, getSessionIdFn); // Initialize git_set_working_dir accessors
354
+ initializeGitShowStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_show accessors
355
+ initializeGitStashStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_stash accessors
356
+ initializeGitStatusStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_status accessors
357
+ initializeGitTagStateAccessors(getWorkingDirectoryFn, getSessionIdFn); // Initialize git_tag accessors
358
+ // --- HTTP Transport Setup ---
359
+ if (TRANSPORT_TYPE === 'http') {
360
+ const app = express();
361
+ // Middleware to parse JSON request bodies.
362
+ app.use(express.json());
363
+ // Handle CORS preflight (OPTIONS) requests.
364
+ app.options(MCP_ENDPOINT_PATH, (req, res) => {
365
+ if (isOriginAllowed(req, res)) {
366
+ // Origin is allowed, send success status for preflight.
367
+ res.sendStatus(204); // No Content
368
+ }
369
+ else {
370
+ // Origin not allowed, send forbidden status. isOriginAllowed logs the warning.
371
+ res.status(403).send('Forbidden: Invalid Origin');
372
+ }
373
+ });
374
+ // Middleware for all requests to check origin and set security headers.
375
+ app.use((req, res, next) => {
376
+ if (!isOriginAllowed(req, res)) {
377
+ // Origin not allowed, block the request. isOriginAllowed logs the warning.
378
+ res.status(403).send('Forbidden: Invalid Origin');
379
+ return; // Stop processing the request.
380
+ }
381
+ // Set standard security headers for allowed requests.
382
+ res.setHeader('X-Content-Type-Options', 'nosniff'); // Prevent MIME type sniffing.
383
+ // Consider adding other headers like Content-Security-Policy (CSP), Strict-Transport-Security (HSTS) here.
384
+ next(); // Origin is allowed, proceed to the specific route handler.
385
+ });
386
+ // Handle POST requests (Initialization and subsequent messages).
387
+ app.post(MCP_ENDPOINT_PATH, async (req, res) => {
388
+ // Extract session ID from the custom MCP header.
389
+ const sessionId = req.headers['mcp-session-id'];
390
+ // Look up existing transport for this session.
391
+ let transport = sessionId ? httpTransports[sessionId] : undefined;
392
+ // Check if the request body is an MCP Initialize request.
393
+ const isInitReq = isInitializeRequest(req.body);
394
+ const requestId = req.body?.id || null; // For error responses
395
+ try {
396
+ // --- Handle Initialization Request ---
397
+ if (isInitReq) {
398
+ if (transport) {
399
+ // This indicates a potential client error or session ID collision (very unlikely).
400
+ logger.warning('Received initialize request on an existing session ID. Closing old session.', { ...context, sessionId });
401
+ // Close the old transport cleanly before creating a new one.
402
+ await transport.close(); // Assuming close is async and handles cleanup
403
+ delete httpTransports[sessionId]; // Remove from map
404
+ }
405
+ logger.info('Initializing new session via POST request', { ...context, bodyPreview: JSON.stringify(req.body).substring(0, 100) }); // Log preview for debugging
406
+ // Create a new streamable HTTP transport for this session.
407
+ transport = new StreamableHTTPServerTransport({
408
+ sessionIdGenerator: () => randomUUID(), // Generate a unique session ID.
409
+ onsessioninitialized: (newId) => {
410
+ // Store the transport instance and initialize working directory state.
411
+ httpTransports[newId] = transport;
412
+ sessionWorkingDirectories[newId] = undefined; // Initialize as undefined
413
+ logger.info(`HTTP Session created: ${newId}`, { ...context, sessionId: newId });
414
+ },
415
+ });
416
+ // Define cleanup logic when the transport closes (e.g., client disconnects, DELETE request).
417
+ transport.onclose = () => {
418
+ const closedSessionId = transport.sessionId;
419
+ if (closedSessionId) {
420
+ delete httpTransports[closedSessionId];
421
+ delete sessionWorkingDirectories[closedSessionId]; // Clean up working directory state
422
+ logger.info(`HTTP Session closed: ${closedSessionId}`, { ...context, sessionId: closedSessionId });
423
+ }
424
+ };
425
+ // Create a dedicated McpServer instance for this new session.
426
+ const server = await createMcpServerInstance();
427
+ // Connect the server logic to the transport layer.
428
+ await server.connect(transport);
429
+ // Note: The transport handles sending the initialize response internally upon connection.
430
+ // We still need to call handleRequest below to process the *content* of the initialize message.
431
+ }
432
+ else if (!transport) {
433
+ // --- Handle Non-Initialize Request without Valid Session ---
434
+ // If it's not an initialization request, but no transport was found for the session ID.
435
+ logger.warning('Invalid session ID provided for non-initialize POST request', { ...context, sessionId });
436
+ res.status(404).json({ jsonrpc: '2.0', error: { code: -32004, message: 'Invalid or expired session ID' }, id: requestId });
437
+ return; // Stop processing.
438
+ }
439
+ // --- Handle Request (Initialize or Subsequent Message) ---
440
+ // At this point, 'transport' must be defined (either found or newly created).
441
+ if (!transport) {
442
+ // Defensive check: This state should not be reachable if logic above is correct.
443
+ logger.error('Internal error: Transport is unexpectedly undefined before handleRequest', { ...context, sessionId, isInitReq });
444
+ throw new Error('Internal server error: Transport unavailable');
445
+ }
446
+ // Delegate the actual handling of the request (parsing, routing to server, sending response)
447
+ // to the transport instance. This works for both the initial initialize message and subsequent messages.
448
+ await transport.handleRequest(req, res, req.body);
449
+ }
450
+ catch (err) {
451
+ // Catch-all for errors during POST handling.
452
+ logger.error('Error handling POST request', {
453
+ ...context,
454
+ sessionId,
455
+ isInitReq,
456
+ error: err instanceof Error ? err.message : String(err),
457
+ stack: err instanceof Error ? err.stack : undefined
458
+ });
459
+ // Send a generic JSON-RPC error response if headers haven't been sent yet.
460
+ if (!res.headersSent) {
461
+ res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error during POST handling' }, id: requestId });
462
+ }
463
+ // Ensure transport is cleaned up if an error occurred during initialization
464
+ if (isInitReq && transport && !transport.sessionId) {
465
+ // If init failed before session ID was assigned, manually trigger cleanup if needed
466
+ await transport.close().catch(closeErr => logger.error('Error closing transport after init failure', { ...context, closeError: closeErr }));
467
+ }
468
+ }
469
+ });
470
+ // Unified handler for GET (SSE connection) and DELETE (session termination).
471
+ const handleSessionReq = async (req, res) => {
472
+ const sessionId = req.headers['mcp-session-id'];
473
+ const transport = sessionId ? httpTransports[sessionId] : undefined;
474
+ const method = req.method; // GET or DELETE
475
+ if (!transport) {
476
+ logger.warning(`Session not found for ${method} request`, { ...context, sessionId, method });
477
+ res.status(404).send('Session not found or expired');
478
+ return;
479
+ }
480
+ try {
481
+ // Delegate handling to the transport (establishes SSE for GET, triggers close for DELETE).
482
+ await transport.handleRequest(req, res);
483
+ logger.info(`Successfully handled ${method} request for session`, { ...context, sessionId, method });
484
+ }
485
+ catch (err) {
486
+ logger.error(`Error handling ${method} request for session`, {
487
+ ...context,
488
+ sessionId,
489
+ method,
490
+ error: err instanceof Error ? err.message : String(err),
491
+ stack: err instanceof Error ? err.stack : undefined
492
+ });
493
+ // Send generic error if headers not sent (e.g., error before SSE connection established).
494
+ if (!res.headersSent) {
495
+ res.status(500).send('Internal Server Error');
496
+ }
497
+ // Note: If SSE connection was established, errors might need different handling (e.g., sending error event).
498
+ // The transport's handleRequest should manage SSE-specific error reporting.
499
+ }
500
+ };
501
+ // Route GET and DELETE requests to the unified handler.
502
+ app.get(MCP_ENDPOINT_PATH, handleSessionReq);
503
+ app.delete(MCP_ENDPOINT_PATH, handleSessionReq);
504
+ // --- Start HTTP Server ---
505
+ const serverInstance = http.createServer(app);
506
+ try {
507
+ // Attempt to start the server, retrying ports if necessary.
508
+ const actualPort = await startHttpServerWithRetry(serverInstance, HTTP_PORT, HTTP_HOST, MAX_PORT_RETRIES, context);
509
+ // Log the final address only after successful binding.
510
+ const serverAddress = `http://${HTTP_HOST}:${actualPort}${MCP_ENDPOINT_PATH}`;
511
+ // Use console.log for prominent startup message visibility.
512
+ console.log(`\nšŸš€ MCP Server running in HTTP mode at: ${serverAddress}\n`);
513
+ }
514
+ catch (err) {
515
+ // If startHttpServerWithRetry failed after all retries.
516
+ logger.fatal('HTTP server failed to start after multiple port retries.', { ...context, error: err instanceof Error ? err.message : String(err) });
517
+ // Rethrow or exit, as the server cannot run.
518
+ throw err;
519
+ }
520
+ // For HTTP transport, the server runs indefinitely, so return void.
521
+ return;
522
+ }
523
+ // --- Stdio Transport Setup ---
524
+ if (TRANSPORT_TYPE === 'stdio') {
525
+ // stdioWorkingDirectory is declared above the state accessor functions
526
+ try {
527
+ // Create a single server instance for the stdio process.
528
+ // State accessors are already initialized above.
529
+ const server = await createMcpServerInstance();
530
+ // Create the stdio transport, which reads from stdin and writes to stdout.
531
+ const transport = new StdioServerTransport();
532
+ // Connect the server logic to the stdio transport.
533
+ await server.connect(transport);
534
+ logger.info('MCP Server connected via stdio transport', context);
535
+ // Return the server instance, as it might be needed by the calling process.
536
+ return server;
537
+ }
538
+ catch (err) {
539
+ // Handle critical errors during stdio setup.
540
+ ErrorHandler.handleError(err, { operation: 'stdioConnect', critical: true });
541
+ // Rethrow to indicate failure.
542
+ throw err;
543
+ }
544
+ }
545
+ // --- Unsupported Transport ---
546
+ // If TRANSPORT_TYPE is neither 'http' nor 'stdio'.
547
+ logger.fatal(`Unsupported transport type configured: ${TRANSPORT_TYPE}`, context);
548
+ throw new Error(`Unsupported transport type: ${TRANSPORT_TYPE}. Must be 'stdio' or 'http'.`);
549
+ }
550
+ /**
551
+ * Main application entry point.
552
+ * Calls `startTransport` to initialize and start the MCP server based on the
553
+ * configured transport type. Handles top-level errors during startup.
554
+ *
555
+ * @async
556
+ * @export
557
+ * @returns {Promise<void | McpServer>} Resolves with the McpServer instance if using stdio, or void if using http (as it runs indefinitely). Rejects on critical startup failure.
558
+ */
559
+ export async function initializeAndStartServer() {
560
+ try {
561
+ // Start the appropriate transport (stdio or http).
562
+ return await startTransport();
563
+ }
564
+ catch (err) {
565
+ // Log fatal errors during the server startup process.
566
+ logger.fatal('Failed to initialize and start MCP server', { error: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined });
567
+ // Use the global error handler for critical failures.
568
+ ErrorHandler.handleError(err, { operation: 'initializeAndStartServer', critical: true });
569
+ // Exit the process with an error code to signal failure.
570
+ process.exit(1);
571
+ }
572
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview Barrel file for the gitAdd tool.
3
+ * Exports the registration function and state accessor initialization function.
4
+ */
5
+ export { registerGitAddTool, initializeGitAddStateAccessors } from './registration.js';
6
+ // Export types if needed elsewhere, e.g.:
7
+ // export type { GitAddInput, GitAddResult } from './logic.js';