@cyanheads/git-mcp-server 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +55 -89
  2. package/{build → dist}/config/index.js +16 -18
  3. package/{build → dist}/index.js +80 -30
  4. package/dist/mcp-server/server.js +296 -0
  5. package/{build → dist}/mcp-server/tools/gitAdd/logic.js +9 -6
  6. package/{build → dist}/mcp-server/tools/gitAdd/registration.js +7 -4
  7. package/{build → dist}/mcp-server/tools/gitBranch/logic.js +23 -12
  8. package/{build → dist}/mcp-server/tools/gitBranch/registration.js +8 -5
  9. package/{build → dist}/mcp-server/tools/gitCheckout/logic.js +92 -44
  10. package/{build → dist}/mcp-server/tools/gitCheckout/registration.js +8 -5
  11. package/{build → dist}/mcp-server/tools/gitCherryPick/logic.js +10 -7
  12. package/{build → dist}/mcp-server/tools/gitCherryPick/registration.js +8 -5
  13. package/{build → dist}/mcp-server/tools/gitClean/logic.js +9 -6
  14. package/{build → dist}/mcp-server/tools/gitClean/registration.js +8 -5
  15. package/{build → dist}/mcp-server/tools/gitClearWorkingDir/logic.js +3 -2
  16. package/{build → dist}/mcp-server/tools/gitClearWorkingDir/registration.js +7 -4
  17. package/{build → dist}/mcp-server/tools/gitClone/logic.js +8 -5
  18. package/{build → dist}/mcp-server/tools/gitClone/registration.js +7 -4
  19. package/dist/mcp-server/tools/gitCommit/logic.js +207 -0
  20. package/{build → dist}/mcp-server/tools/gitCommit/registration.js +22 -15
  21. package/{build → dist}/mcp-server/tools/gitDiff/logic.js +9 -6
  22. package/{build → dist}/mcp-server/tools/gitDiff/registration.js +8 -5
  23. package/{build → dist}/mcp-server/tools/gitFetch/logic.js +10 -7
  24. package/{build → dist}/mcp-server/tools/gitFetch/registration.js +8 -5
  25. package/{build → dist}/mcp-server/tools/gitInit/index.js +2 -2
  26. package/{build → dist}/mcp-server/tools/gitInit/logic.js +9 -6
  27. package/dist/mcp-server/tools/gitInit/registration.js +98 -0
  28. package/{build → dist}/mcp-server/tools/gitLog/logic.js +53 -16
  29. package/{build → dist}/mcp-server/tools/gitLog/registration.js +8 -5
  30. package/{build → dist}/mcp-server/tools/gitMerge/logic.js +9 -6
  31. package/{build → dist}/mcp-server/tools/gitMerge/registration.js +8 -5
  32. package/{build → dist}/mcp-server/tools/gitPull/logic.js +11 -8
  33. package/{build → dist}/mcp-server/tools/gitPull/registration.js +7 -4
  34. package/{build → dist}/mcp-server/tools/gitPush/logic.js +12 -9
  35. package/{build → dist}/mcp-server/tools/gitPush/registration.js +7 -4
  36. package/{build → dist}/mcp-server/tools/gitRebase/logic.js +9 -6
  37. package/{build → dist}/mcp-server/tools/gitRebase/registration.js +8 -5
  38. package/{build → dist}/mcp-server/tools/gitRemote/logic.js +4 -5
  39. package/{build → dist}/mcp-server/tools/gitRemote/registration.js +2 -4
  40. package/{build → dist}/mcp-server/tools/gitReset/logic.js +5 -6
  41. package/{build → dist}/mcp-server/tools/gitReset/registration.js +2 -4
  42. package/{build → dist}/mcp-server/tools/gitSetWorkingDir/logic.js +5 -6
  43. package/{build → dist}/mcp-server/tools/gitSetWorkingDir/registration.js +22 -13
  44. package/{build → dist}/mcp-server/tools/gitShow/logic.js +5 -6
  45. package/{build → dist}/mcp-server/tools/gitShow/registration.js +3 -5
  46. package/{build → dist}/mcp-server/tools/gitStash/logic.js +5 -6
  47. package/{build → dist}/mcp-server/tools/gitStash/registration.js +3 -5
  48. package/{build → dist}/mcp-server/tools/gitStatus/logic.js +5 -6
  49. package/{build → dist}/mcp-server/tools/gitStatus/registration.js +2 -4
  50. package/{build → dist}/mcp-server/tools/gitTag/logic.js +3 -4
  51. package/{build → dist}/mcp-server/tools/gitTag/registration.js +2 -4
  52. package/dist/mcp-server/transports/authentication/authMiddleware.js +145 -0
  53. package/dist/mcp-server/transports/httpTransport.js +432 -0
  54. package/dist/mcp-server/transports/stdioTransport.js +87 -0
  55. package/{build → dist}/types-global/errors.js +2 -2
  56. package/dist/utils/index.js +12 -0
  57. package/{build/utils → dist/utils/internal}/errorHandler.js +18 -8
  58. package/dist/utils/internal/index.js +3 -0
  59. package/dist/utils/internal/logger.js +254 -0
  60. package/{build/utils → dist/utils/internal}/requestContext.js +2 -3
  61. package/dist/utils/metrics/index.js +1 -0
  62. package/{build/utils → dist/utils/metrics}/tokenCounter.js +3 -3
  63. package/dist/utils/parsing/dateParser.js +62 -0
  64. package/dist/utils/parsing/index.js +2 -0
  65. package/{build/utils → dist/utils/parsing}/jsonParser.js +3 -2
  66. package/{build/utils → dist/utils/security}/idGenerator.js +4 -5
  67. package/dist/utils/security/index.js +3 -0
  68. package/{build/utils → dist/utils/security}/rateLimiter.js +7 -10
  69. package/{build/utils → dist/utils/security}/sanitization.js +4 -3
  70. package/package.json +20 -16
  71. package/build/mcp-server/server.js +0 -572
  72. package/build/mcp-server/tools/gitCommit/logic.js +0 -129
  73. package/build/mcp-server/tools/gitInit/registration.js +0 -44
  74. package/build/types-global/mcp.js +0 -59
  75. package/build/types-global/tool.js +0 -1
  76. package/build/utils/index.js +0 -11
  77. package/build/utils/logger.js +0 -266
  78. /package/{build → dist}/mcp-server/tools/gitAdd/index.js +0 -0
  79. /package/{build → dist}/mcp-server/tools/gitBranch/index.js +0 -0
  80. /package/{build → dist}/mcp-server/tools/gitCheckout/index.js +0 -0
  81. /package/{build → dist}/mcp-server/tools/gitCherryPick/index.js +0 -0
  82. /package/{build → dist}/mcp-server/tools/gitClean/index.js +0 -0
  83. /package/{build → dist}/mcp-server/tools/gitClearWorkingDir/index.js +0 -0
  84. /package/{build → dist}/mcp-server/tools/gitClone/index.js +0 -0
  85. /package/{build → dist}/mcp-server/tools/gitCommit/index.js +0 -0
  86. /package/{build → dist}/mcp-server/tools/gitDiff/index.js +0 -0
  87. /package/{build → dist}/mcp-server/tools/gitFetch/index.js +0 -0
  88. /package/{build → dist}/mcp-server/tools/gitLog/index.js +0 -0
  89. /package/{build → dist}/mcp-server/tools/gitMerge/index.js +0 -0
  90. /package/{build → dist}/mcp-server/tools/gitPull/index.js +0 -0
  91. /package/{build → dist}/mcp-server/tools/gitPush/index.js +0 -0
  92. /package/{build → dist}/mcp-server/tools/gitRebase/index.js +0 -0
  93. /package/{build → dist}/mcp-server/tools/gitRemote/index.js +0 -0
  94. /package/{build → dist}/mcp-server/tools/gitReset/index.js +0 -0
  95. /package/{build → dist}/mcp-server/tools/gitSetWorkingDir/index.js +0 -0
  96. /package/{build → dist}/mcp-server/tools/gitShow/index.js +0 -0
  97. /package/{build → dist}/mcp-server/tools/gitStash/index.js +0 -0
  98. /package/{build → dist}/mcp-server/tools/gitStatus/index.js +0 -0
  99. /package/{build → dist}/mcp-server/tools/gitTag/index.js +0 -0
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Main entry point for the MCP (Model Context Protocol) server.
3
+ * This file orchestrates the server's lifecycle:
4
+ * 1. Initializes the core McpServer instance with its identity and capabilities.
5
+ * 2. Registers available resources and tools, making them discoverable and usable by clients.
6
+ * 3. Selects and starts the appropriate communication transport (stdio or Streamable HTTP)
7
+ * based on configuration.
8
+ * 4. Handles top-level error management during startup.
9
+ *
10
+ * MCP Specification References:
11
+ * - Lifecycle: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/lifecycle.mdx
12
+ * - Overview (Capabilities): https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/index.mdx
13
+ * - Transports: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx
14
+ */
15
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
16
+ // Import validated configuration and environment details.
17
+ import { config, environment } from '../config/index.js';
18
+ // Import core utilities: ErrorHandler, logger, requestContextService.
19
+ import { ErrorHandler, logger, requestContextService } from '../utils/index.js'; // Added RequestContext
20
+ // Import registration AND state initialization functions for ALL Git tools (assuming pattern)
21
+ import { registerGitAddTool, initializeGitAddStateAccessors } from './tools/gitAdd/index.js';
22
+ import { registerGitBranchTool, initializeGitBranchStateAccessors } from './tools/gitBranch/index.js';
23
+ import { registerGitCheckoutTool, initializeGitCheckoutStateAccessors } from './tools/gitCheckout/index.js'; // Assumed initializer
24
+ import { registerGitCherryPickTool, initializeGitCherryPickStateAccessors } from './tools/gitCherryPick/index.js'; // Assumed initializer
25
+ import { registerGitCleanTool, initializeGitCleanStateAccessors } from './tools/gitClean/index.js'; // Assumed initializer
26
+ import { registerGitClearWorkingDirTool, initializeGitClearWorkingDirStateAccessors } from './tools/gitClearWorkingDir/index.js'; // Assumed initializer
27
+ import { registerGitCloneTool } from './tools/gitClone/index.js'; // Removed initializer import
28
+ import { registerGitCommitTool, initializeGitCommitStateAccessors } from './tools/gitCommit/index.js'; // Assumed initializer
29
+ import { registerGitDiffTool, initializeGitDiffStateAccessors } from './tools/gitDiff/index.js'; // Assumed initializer
30
+ import { registerGitFetchTool, initializeGitFetchStateAccessors } from './tools/gitFetch/index.js';
31
+ import { registerGitInitTool, initializeGitInitStateAccessors } from './tools/gitInit/index.js'; // Added initializer import
32
+ import { registerGitLogTool, initializeGitLogStateAccessors } from './tools/gitLog/index.js'; // Assumed initializer
33
+ import { registerGitMergeTool, initializeGitMergeStateAccessors } from './tools/gitMerge/index.js'; // Assumed initializer
34
+ import { registerGitPullTool, initializeGitPullStateAccessors } from './tools/gitPull/index.js';
35
+ import { registerGitPushTool, initializeGitPushStateAccessors } from './tools/gitPush/index.js';
36
+ import { registerGitRebaseTool, initializeGitRebaseStateAccessors } from './tools/gitRebase/index.js'; // Assumed initializer
37
+ import { registerGitRemoteTool, initializeGitRemoteStateAccessors } from './tools/gitRemote/index.js'; // Assumed initializer
38
+ import { registerGitResetTool, initializeGitResetStateAccessors } from './tools/gitReset/index.js'; // Assumed initializer
39
+ import { registerGitSetWorkingDirTool, initializeGitSetWorkingDirStateAccessors } from './tools/gitSetWorkingDir/index.js';
40
+ import { registerGitShowTool, initializeGitShowStateAccessors } from './tools/gitShow/index.js'; // Assumed initializer
41
+ import { registerGitStashTool, initializeGitStashStateAccessors } from './tools/gitStash/index.js'; // Assumed initializer
42
+ import { registerGitStatusTool, initializeGitStatusStateAccessors } from './tools/gitStatus/index.js'; // Assumed initializer
43
+ import { registerGitTagTool, initializeGitTagStateAccessors } from './tools/gitTag/index.js'; // Assumed initializer
44
+ // Import transport setup functions AND state accessors
45
+ import { startHttpTransport, getHttpSessionWorkingDirectory, setHttpSessionWorkingDirectory } from './transports/httpTransport.js';
46
+ import { connectStdioTransport, getStdioWorkingDirectory, setStdioWorkingDirectory } from './transports/stdioTransport.js';
47
+ /**
48
+ * Creates and configures a new instance of the McpServer.
49
+ *
50
+ * This function is central to defining the server's identity and functionality
51
+ * as presented to connecting clients during the MCP initialization phase.
52
+ *
53
+ * MCP Spec Relevance:
54
+ * - Server Identity (`serverInfo`): The `name` and `version` provided here are part
55
+ * of the `ServerInformation` object returned in the `InitializeResult` message,
56
+ * allowing clients to identify the server they are connected to.
57
+ * - Capabilities Declaration: The `capabilities` object declares the features this
58
+ * server supports, enabling clients to tailor their interactions.
59
+ * - `logging: {}`: Indicates the server can receive `logging/setLevel` requests
60
+ * and may send `notifications/message` log messages (handled by the logger utility).
61
+ * - `resources: { listChanged: true }`: Signals that the server supports dynamic
62
+ * resource lists and will send `notifications/resources/list_changed` if the
63
+ * available resources change after initialization. (Currently no resources registered)
64
+ * - `tools: { listChanged: true }`: Signals support for dynamic tool lists and
65
+ * `notifications/tools/list_changed`.
66
+ * - Resource/Tool Registration: This function calls specific registration functions
67
+ * (e.g., `registerGitAdd`) which use SDK methods (`server.resource`, `server.tool`)
68
+ * to make capabilities available for discovery (`resources/list`, `tools/list`) and
69
+ * invocation (`resources/read`, `tools/call`).
70
+ *
71
+ * Design Note: This factory function is used to create server instances. For the 'stdio'
72
+ * transport, it's called once. For the 'http' transport, it's passed to `startHttpTransport`
73
+ * and called *per session* to ensure session isolation.
74
+ *
75
+ * @returns {Promise<McpServer>} A promise resolving with the configured McpServer instance.
76
+ * @throws {Error} If any resource or tool registration fails.
77
+ */
78
+ // Removed sessionId parameter, it will be retrieved from context within tool handlers
79
+ async function createMcpServerInstance() {
80
+ const context = { operation: 'createMcpServerInstance' };
81
+ logger.info('Initializing MCP server instance', context);
82
+ // Configure the request context service (used for correlating logs/errors).
83
+ requestContextService.configure({
84
+ appName: config.mcpServerName,
85
+ appVersion: config.mcpServerVersion,
86
+ environment,
87
+ });
88
+ // Instantiate the core McpServer using the SDK.
89
+ // Provide server identity (name, version) and declare supported capabilities.
90
+ // Note: Resources capability declared, but none are registered currently.
91
+ logger.debug('Instantiating McpServer with capabilities', { ...context, serverInfo: { name: config.mcpServerName, version: config.mcpServerVersion }, capabilities: { logging: {}, resources: { listChanged: true }, tools: { listChanged: true } } });
92
+ const server = new McpServer({ name: config.mcpServerName, version: config.mcpServerVersion }, // ServerInformation part of InitializeResult
93
+ { capabilities: { logging: {}, resources: { listChanged: true }, tools: { listChanged: true } } } // Declared capabilities
94
+ );
95
+ // --- Define Unified State Accessor Functions ---
96
+ // These functions abstract away the transport type to get/set session state.
97
+ /** Gets the session ID from the tool's execution context. */
98
+ const getSessionIdFromContext = (toolContext) => {
99
+ // The RequestContext created by the tool registration wrapper should contain the sessionId.
100
+ return toolContext?.sessionId;
101
+ };
102
+ /** Gets the working directory based on transport type and session ID. */
103
+ const getWorkingDirectory = (sessionId) => {
104
+ if (config.mcpTransportType === 'http') {
105
+ if (!sessionId) {
106
+ logger.warning('Attempted to get HTTP working directory without session ID', { ...context, caller: 'getWorkingDirectory' });
107
+ return undefined;
108
+ }
109
+ return getHttpSessionWorkingDirectory(sessionId);
110
+ }
111
+ else {
112
+ // For stdio, there's only one implicit session, ID is not needed.
113
+ return getStdioWorkingDirectory();
114
+ }
115
+ };
116
+ /** Sets the working directory based on transport type and session ID. */
117
+ const setWorkingDirectory = (sessionId, dir) => {
118
+ if (config.mcpTransportType === 'http') {
119
+ if (!sessionId) {
120
+ logger.error('Attempted to set HTTP working directory without session ID', { ...context, caller: 'setWorkingDirectory', dir });
121
+ // Optionally throw an error or just log
122
+ return;
123
+ }
124
+ setHttpSessionWorkingDirectory(sessionId, dir);
125
+ }
126
+ else {
127
+ // For stdio, set the single session's directory.
128
+ setStdioWorkingDirectory(dir);
129
+ }
130
+ };
131
+ // --- Initialize Tool State Accessors BEFORE Registration ---
132
+ // Pass the defined unified accessor functions to the initializers.
133
+ logger.debug('Initializing state accessors for tools...', context);
134
+ try {
135
+ // Call initializers for all tools that likely need state access.
136
+ // If an initializer doesn't exist, the import would have failed earlier (or build will fail).
137
+ initializeGitAddStateAccessors(getWorkingDirectory, getSessionIdFromContext);
138
+ initializeGitBranchStateAccessors(getWorkingDirectory, getSessionIdFromContext);
139
+ initializeGitCheckoutStateAccessors(getWorkingDirectory, getSessionIdFromContext);
140
+ initializeGitCherryPickStateAccessors(getWorkingDirectory, getSessionIdFromContext);
141
+ initializeGitCleanStateAccessors(getWorkingDirectory, getSessionIdFromContext);
142
+ initializeGitClearWorkingDirStateAccessors(getWorkingDirectory, getSessionIdFromContext);
143
+ // initializeGitCloneStateAccessors(getWorkingDirectory, getSessionIdFromContext); // Removed call
144
+ initializeGitCommitStateAccessors(getWorkingDirectory, getSessionIdFromContext);
145
+ initializeGitDiffStateAccessors(getWorkingDirectory, getSessionIdFromContext);
146
+ initializeGitFetchStateAccessors(getWorkingDirectory, getSessionIdFromContext);
147
+ initializeGitInitStateAccessors(getWorkingDirectory, getSessionIdFromContext); // Added call
148
+ initializeGitLogStateAccessors(getWorkingDirectory, getSessionIdFromContext);
149
+ initializeGitMergeStateAccessors(getWorkingDirectory, getSessionIdFromContext);
150
+ initializeGitPullStateAccessors(getWorkingDirectory, getSessionIdFromContext);
151
+ initializeGitPushStateAccessors(getWorkingDirectory, getSessionIdFromContext);
152
+ initializeGitRebaseStateAccessors(getWorkingDirectory, getSessionIdFromContext);
153
+ initializeGitRemoteStateAccessors(getWorkingDirectory, getSessionIdFromContext);
154
+ initializeGitResetStateAccessors(getWorkingDirectory, getSessionIdFromContext);
155
+ initializeGitSetWorkingDirStateAccessors(getWorkingDirectory, setWorkingDirectory, getSessionIdFromContext); // Special case
156
+ initializeGitShowStateAccessors(getWorkingDirectory, getSessionIdFromContext);
157
+ initializeGitStashStateAccessors(getWorkingDirectory, getSessionIdFromContext);
158
+ initializeGitStatusStateAccessors(getWorkingDirectory, getSessionIdFromContext);
159
+ initializeGitTagStateAccessors(getWorkingDirectory, getSessionIdFromContext);
160
+ logger.debug('State accessors initialized successfully.', context);
161
+ }
162
+ catch (initError) {
163
+ // Catch errors specifically during initialization phase
164
+ logger.error('Failed during state accessor initialization', {
165
+ ...context,
166
+ error: initError instanceof Error ? initError.message : String(initError),
167
+ stack: initError instanceof Error ? initError.stack : undefined,
168
+ });
169
+ throw initError; // Re-throw to prevent server starting incorrectly
170
+ }
171
+ try {
172
+ // Register all defined Git tools. These calls populate the server's
173
+ // internal registry, making them available via MCP methods like 'tools/list'.
174
+ logger.debug('Registering Git tools...', context);
175
+ await registerGitAddTool(server);
176
+ await registerGitBranchTool(server);
177
+ await registerGitCheckoutTool(server);
178
+ await registerGitCherryPickTool(server);
179
+ await registerGitCleanTool(server);
180
+ await registerGitClearWorkingDirTool(server);
181
+ await registerGitCloneTool(server);
182
+ await registerGitCommitTool(server);
183
+ await registerGitDiffTool(server);
184
+ await registerGitFetchTool(server);
185
+ await registerGitInitTool(server);
186
+ await registerGitLogTool(server);
187
+ await registerGitMergeTool(server);
188
+ await registerGitPullTool(server);
189
+ await registerGitPushTool(server);
190
+ await registerGitRebaseTool(server);
191
+ await registerGitRemoteTool(server);
192
+ await registerGitResetTool(server);
193
+ await registerGitSetWorkingDirTool(server);
194
+ await registerGitShowTool(server);
195
+ await registerGitStashTool(server);
196
+ await registerGitStatusTool(server);
197
+ await registerGitTagTool(server);
198
+ // Add calls to register other resources/tools here if needed in the future.
199
+ logger.info('Git tools registered successfully', context);
200
+ }
201
+ catch (err) {
202
+ // Registration is critical; log and re-throw errors.
203
+ logger.error('Failed to register resources/tools', {
204
+ ...context,
205
+ error: err instanceof Error ? err.message : String(err),
206
+ stack: err instanceof Error ? err.stack : undefined, // Include stack for debugging
207
+ });
208
+ throw err; // Propagate error to prevent server starting with incomplete capabilities.
209
+ }
210
+ return server;
211
+ }
212
+ /**
213
+ * Selects, sets up, and starts the appropriate MCP transport layer based on configuration.
214
+ * This function acts as the bridge between the core server logic and the communication channel.
215
+ *
216
+ * MCP Spec Relevance:
217
+ * - Transport Selection: Reads `config.mcpTransportType` ('stdio' or 'http') to determine
218
+ * which transport mechanism defined in the MCP specification to use.
219
+ * - Transport Connection: Calls dedicated functions (`connectStdioTransport` or `startHttpTransport`)
220
+ * which handle the specifics of establishing communication according to the chosen
221
+ * transport's rules (e.g., stdin/stdout handling for 'stdio', HTTP server setup and
222
+ * endpoint handling for 'http').
223
+ * - Server Instance Lifecycle:
224
+ * - For 'stdio', creates a single `McpServer` instance for the lifetime of the process.
225
+ * - For 'http', passes the `createMcpServerInstance` factory function to `startHttpTransport`,
226
+ * allowing the HTTP transport to create a new, isolated server instance for each client session,
227
+ * aligning with the stateful session management described in the Streamable HTTP spec.
228
+ *
229
+ * @returns {Promise<McpServer | void>} Resolves with the McpServer instance for 'stdio', or void for 'http'.
230
+ * @throws {Error} If the configured transport type is unsupported or if transport setup fails.
231
+ */
232
+ async function startTransport() {
233
+ // Determine the transport type from the validated configuration.
234
+ const transportType = config.mcpTransportType;
235
+ const context = { operation: 'startTransport', transport: transportType };
236
+ logger.info(`Starting transport: ${transportType}`, context);
237
+ // --- HTTP Transport Setup ---
238
+ if (transportType === 'http') {
239
+ logger.debug('Delegating to startHttpTransport...', context);
240
+ // For HTTP, the transport layer manages its own lifecycle and potentially multiple sessions.
241
+ // We pass the factory function to allow the HTTP transport to create server instances as needed (per session).
242
+ await startHttpTransport(createMcpServerInstance, context);
243
+ // The HTTP server runs indefinitely, listening for connections, so this function returns void.
244
+ return;
245
+ }
246
+ // --- Stdio Transport Setup ---
247
+ if (transportType === 'stdio') {
248
+ logger.debug('Creating single McpServer instance for stdio transport...', context);
249
+ // For stdio, there's typically one persistent connection managed by a parent process.
250
+ // Create a single McpServer instance for the entire process lifetime.
251
+ const server = await createMcpServerInstance();
252
+ logger.debug('Delegating to connectStdioTransport...', context);
253
+ // Connect the server instance to the stdio transport handler.
254
+ await connectStdioTransport(server, context);
255
+ // Return the server instance; the caller (main entry point) might hold onto it.
256
+ return server;
257
+ }
258
+ // --- Unsupported Transport ---
259
+ // This case should theoretically not be reached due to config validation, but acts as a safeguard.
260
+ logger.fatal(`Unsupported transport type configured: ${transportType}`, context);
261
+ throw new Error(`Unsupported transport type: ${transportType}. Must be 'stdio' or 'http'.`);
262
+ }
263
+ /**
264
+ * Main application entry point. Initializes and starts the MCP server.
265
+ *
266
+ * MCP Spec Relevance:
267
+ * - Orchestrates the server startup sequence, culminating in a server ready to accept
268
+ * connections and process MCP messages according to the chosen transport's rules.
269
+ * - Implements top-level error handling for critical startup failures, ensuring the
270
+ * process exits appropriately if it cannot initialize correctly.
271
+ *
272
+ * @returns {Promise<void | McpServer>} Resolves upon successful startup (void for http, McpServer for stdio). Rejects on critical failure.
273
+ */
274
+ export async function initializeAndStartServer() {
275
+ const context = { operation: 'initializeAndStartServer' };
276
+ logger.info('MCP Server initialization sequence started.', context);
277
+ try {
278
+ // Initiate the transport setup based on configuration.
279
+ const result = await startTransport();
280
+ logger.info('MCP Server initialization sequence completed successfully.', context);
281
+ return result;
282
+ }
283
+ catch (err) {
284
+ // Catch any errors that occurred during server instance creation or transport setup.
285
+ logger.fatal('Critical error during MCP server initialization.', {
286
+ ...context,
287
+ error: err instanceof Error ? err.message : String(err),
288
+ stack: err instanceof Error ? err.stack : undefined,
289
+ });
290
+ // Use the centralized error handler for consistent critical error reporting.
291
+ ErrorHandler.handleError(err, { ...context, critical: true });
292
+ // Exit the process with a non-zero code to indicate failure.
293
+ logger.info('Exiting process due to critical initialization error.', context);
294
+ process.exit(1);
295
+ }
296
+ }
@@ -1,13 +1,16 @@
1
- import { z } from 'zod';
2
- import { promisify } from 'util';
3
1
  import { exec } from 'child_process';
4
- import { logger } from '../../../utils/logger.js';
5
- import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
6
- import { sanitization } from '../../../utils/sanitization.js';
2
+ import { promisify } from 'util';
3
+ import { z } from 'zod';
4
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
5
+ import { logger } from '../../../utils/index.js';
6
+ // Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
7
+ import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
8
+ // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
+ import { sanitization } from '../../../utils/index.js';
7
10
  const execAsync = promisify(exec);
8
11
  // Define the input schema for the git_add tool using Zod
9
12
  export const GitAddInputSchema = z.object({
10
- path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the session's working directory if set via `git_set_working_dir`, otherwise defaults to the server's current working directory (`.`)."),
13
+ path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
11
14
  files: z.union([
12
15
  z.string().min(1),
13
16
  z.array(z.string().min(1))
@@ -1,9 +1,12 @@
1
- import { logger } from '../../../utils/logger.js';
2
- import { ErrorHandler } from '../../../utils/errorHandler.js';
3
- import { requestContextService } from '../../../utils/requestContext.js';
1
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
2
+ import { logger } from '../../../utils/index.js';
3
+ // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
4
+ import { ErrorHandler } from '../../../utils/index.js';
5
+ // Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
6
+ import { requestContextService } from '../../../utils/index.js';
4
7
  // Import the result type along with the function and input schema
5
- import { addGitFiles, GitAddInputSchema } from './logic.js';
6
8
  import { BaseErrorCode } from '../../../types-global/errors.js'; // Import BaseErrorCode
9
+ import { addGitFiles, GitAddInputSchema } from './logic.js';
7
10
  let _getWorkingDirectory;
8
11
  let _getSessionId;
9
12
  /**
@@ -1,17 +1,20 @@
1
- import { z } from 'zod';
2
- import { promisify } from 'util';
3
1
  import { exec } from 'child_process';
4
- import { logger } from '../../../utils/logger.js';
5
- import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
6
- import { sanitization } from '../../../utils/sanitization.js';
2
+ import { promisify } from 'util';
3
+ import { z } from 'zod';
4
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
5
+ import { logger } from '../../../utils/index.js';
6
+ // Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
7
+ import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
8
+ // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
+ import { sanitization } from '../../../utils/index.js';
7
10
  const execAsync = promisify(exec);
8
11
  // Define the BASE input schema for the git_branch tool using Zod
9
12
  export const GitBranchBaseSchema = z.object({
10
- path: z.string().min(1).optional().default('.').describe("Path to the local Git repository. If omitted, defaults to the path set by `git_set_working_dir` for the current session, or the server's CWD if no session path is set."),
13
+ path: z.string().min(1).optional().default('.').describe("Path to the local Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
11
14
  mode: z.enum(['list', 'create', 'delete', 'rename', 'show-current']).describe("The branch operation to perform: 'list', 'create', 'delete', 'rename', 'show-current'."),
12
- branchName: z.string().min(1).optional().describe("The name of the branch. Required for 'create', 'delete', 'rename' modes."),
13
- newBranchName: z.string().min(1).optional().describe("The new name for the branch. Required for 'rename' mode."),
14
- startPoint: z.string().min(1).optional().describe("Optional commit hash, tag, or existing branch name to start the new branch from. Used only in 'create' mode. Defaults to HEAD."),
15
+ branchName: z.string().min(1).optional().describe("The name of the branch (e.g., 'feat/new-login', 'main'). Required for 'create', 'delete', 'rename' modes."),
16
+ newBranchName: z.string().min(1).optional().describe("The new name for the branch (e.g., 'fix/typo-in-readme'). Required for 'rename' mode."),
17
+ startPoint: z.string().min(1).optional().describe("Optional commit hash, tag, or existing branch name (e.g., 'main', 'v1.0.0', 'commit-hash') to start the new branch from. Used only in 'create' mode. Defaults to HEAD."),
15
18
  force: z.boolean().default(false).describe("Force the operation. Use -D for delete, -M for rename, -f for create (if branch exists). Use with caution, as forcing operations can lead to data loss."),
16
19
  all: z.boolean().default(false).describe("List both local and remote-tracking branches. Used only in 'list' mode."),
17
20
  remote: z.boolean().default(false).describe("Act on remote-tracking branches. Used with 'list' (-r) or 'delete' (-r)."),
@@ -80,12 +83,20 @@ export async function gitBranchLogic(input, context) {
80
83
  .map(line => {
81
84
  const isCurrent = line.startsWith('* ');
82
85
  const trimmedLine = line.replace(/^\*?\s+/, ''); // Remove leading '*' and spaces
86
+ // Determine isRemote based on the raw trimmed line BEFORE splitting
87
+ const isRemote = trimmedLine.startsWith('remotes/');
83
88
  const parts = trimmedLine.split(/\s+/);
84
- const name = parts[0];
85
- const isRemote = name.startsWith('remotes/');
89
+ const name = parts[0]; // This might be 'remotes/origin/main' or just 'main'
86
90
  const commitHash = parts[1] || undefined; // Verbose gives hash
87
91
  const commitSubject = parts.slice(2).join(' ') || undefined; // Verbose gives subject
88
- return { name, isCurrent, isRemote, commitHash, commitSubject };
92
+ // Return the correct name (without 'remotes/' prefix if it was remote) and the isRemote flag
93
+ return {
94
+ name: isRemote ? name.split('/').slice(2).join('/') : name, // e.g., 'origin/main' or 'main'
95
+ isCurrent,
96
+ isRemote, // Use the flag determined before splitting
97
+ commitHash,
98
+ commitSubject
99
+ };
89
100
  });
90
101
  const currentBranch = branches.find(b => b.isCurrent)?.name;
91
102
  result = { success: true, mode: 'list', branches, currentBranch };
@@ -1,8 +1,11 @@
1
- import { logger } from '../../../utils/logger.js';
2
- import { ErrorHandler } from '../../../utils/errorHandler.js';
3
- import { requestContextService } from '../../../utils/requestContext.js';
4
- import { gitBranchLogic, GitBranchBaseSchema } from './logic.js';
5
- import { BaseErrorCode } from '../../../types-global/errors.js';
1
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
2
+ import { logger } from '../../../utils/index.js';
3
+ // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
4
+ import { ErrorHandler } from '../../../utils/index.js';
5
+ // Import utils from barrel (requestContextService from ../utils/internal/requestContext.js)
6
+ import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
7
+ import { requestContextService } from '../../../utils/index.js';
8
+ import { GitBranchBaseSchema, gitBranchLogic } from './logic.js';
6
9
  let _getWorkingDirectory;
7
10
  let _getSessionId;
8
11
  /**
@@ -1,15 +1,18 @@
1
- import { z } from 'zod';
2
- import { promisify } from 'util';
3
1
  import { exec } from 'child_process';
4
- import { logger } from '../../../utils/logger.js';
5
- import { McpError, BaseErrorCode } from '../../../types-global/errors.js';
6
- import { sanitization } from '../../../utils/sanitization.js';
2
+ import { promisify } from 'util';
3
+ import { z } from 'zod';
4
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
5
+ import { logger } from '../../../utils/index.js';
6
+ // Import utils from barrel (RequestContext from ../utils/internal/requestContext.js)
7
+ import { BaseErrorCode, McpError } from '../../../types-global/errors.js'; // Keep direct import for types-global
8
+ // Import utils from barrel (sanitization from ../utils/security/sanitization.js)
9
+ import { sanitization } from '../../../utils/index.js';
7
10
  const execAsync = promisify(exec);
8
11
  // Define the input schema for the git_checkout tool using Zod
9
12
  export const GitCheckoutInputSchema = z.object({
10
- path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the session's working directory if set."),
11
- branchOrPath: z.string().min(1).describe("The branch name, commit hash, tag, or file path(s) to checkout."),
12
- newBranch: z.string().optional().describe("Create a new branch named <new_branch> and start it at <branchOrPath>."),
13
+ path: z.string().min(1).optional().default('.').describe("Path to the Git repository. Defaults to the directory set via `git_set_working_dir` for the session; set 'git_set_working_dir' if not set."),
14
+ branchOrPath: z.string().min(1).describe("The branch name (e.g., 'main'), commit hash, tag, or file path(s) (e.g., './src/file.ts') to checkout."),
15
+ newBranch: z.string().optional().describe("Create a new branch named <new_branch> (e.g., 'feat/new-feature') and start it at <branchOrPath>."),
13
16
  force: z.boolean().optional().default(false).describe("Force checkout even if there are uncommitted changes (use with caution, discards local changes)."),
14
17
  // Add other relevant git checkout options as needed (e.g., --track, -b for new branch shorthand)
15
18
  });
@@ -72,52 +75,53 @@ export async function checkoutGit(input, context) {
72
75
  let currentBranch = undefined;
73
76
  let newBranchCreated = !!input.newBranch;
74
77
  let filesRestored = undefined;
78
+ let isDetachedHead = false;
79
+ let isFileCheckout = false;
80
+ // --- Initial analysis of checkout output ---
75
81
  // Extract previous branch if available
76
82
  const prevBranchMatch = stderr.match(/Switched to.*? from ['"]?(.*?)['"]?/);
77
83
  if (prevBranchMatch) {
78
84
  previousBranch = prevBranchMatch[1];
79
85
  }
80
- // Extract current branch/state
81
- if (stderr.includes('Switched to branch')) {
82
- const currentBranchMatch = stderr.match(/Switched to branch ['"]?(.*?)['"]?/);
83
- if (currentBranchMatch)
84
- currentBranch = currentBranchMatch[1];
85
- message = `Switched to branch '${currentBranch || input.branchOrPath}'.`;
86
- }
87
- else if (stderr.includes('Switched to a new branch')) {
86
+ // Determine primary outcome from stderr/stdout
87
+ if (stderr.includes('Switched to a new branch')) {
88
88
  const currentBranchMatch = stderr.match(/Switched to a new branch ['"]?(.*?)['"]?/);
89
- if (currentBranchMatch)
90
- currentBranch = currentBranchMatch[1];
91
- message = `Switched to new branch '${currentBranch || input.newBranch}'.`;
92
- newBranchCreated = true; // Confirm creation
89
+ currentBranch = currentBranchMatch ? currentBranchMatch[1] : input.newBranch; // Use matched or input
90
+ message = `Switched to new branch '${currentBranch}'.`;
91
+ newBranchCreated = true;
92
+ }
93
+ else if (stderr.includes('Switched to branch')) {
94
+ const currentBranchMatch = stderr.match(/Switched to branch ['"]?(.*?)['"]?/);
95
+ currentBranch = currentBranchMatch ? currentBranchMatch[1] : input.branchOrPath; // Use matched or input
96
+ message = `Switched to branch '${currentBranch}'.`;
93
97
  }
94
98
  else if (stderr.includes('Already on')) {
95
99
  const currentBranchMatch = stderr.match(/Already on ['"]?(.*?)['"]?/);
96
- if (currentBranchMatch)
97
- currentBranch = currentBranchMatch[1];
98
- message = `Already on '${currentBranch || input.branchOrPath}'.`;
99
- }
100
- else if (stderr.includes('Updated N path') || stdout.includes('Updated N path')) { // Checking out files
101
- message = `Restored path(s): ${input.branchOrPath}`;
102
- // Potentially list the files if input.branchOrPath was specific enough
103
- // Assume input.branchOrPath contains file paths separated by newlines
104
- filesRestored = input.branchOrPath.split('\n').filter(p => p.trim().length > 0); // Split by newline and filter out empty entries
105
- // Try to get current branch after file checkout
106
- try {
107
- const statusResult = await execAsync(`git -C "${targetPath}" branch --show-current`);
108
- currentBranch = statusResult.stdout.trim();
100
+ currentBranch = currentBranchMatch ? currentBranchMatch[1] : input.branchOrPath; // Use matched or input
101
+ message = `Already on '${currentBranch}'.`;
102
+ }
103
+ else if (stderr.includes('Updated N path') || stdout.includes('Updated N path') || stderr.includes('Your branch is up to date with')) { // Checking out files or confirming current state
104
+ // Check if the input looks like file paths rather than a branch/commit
105
+ // This is heuristic - might need refinement if branch names look like paths
106
+ if (input.branchOrPath.includes('/') || input.branchOrPath.includes('.')) {
107
+ isFileCheckout = true;
108
+ message = `Restored or checked path(s): ${input.branchOrPath}`;
109
+ filesRestored = input.branchOrPath.split('\n').map(p => p.trim()).filter(p => p.length > 0);
109
110
  }
110
- catch (statusError) {
111
- logger.warning('Could not determine current branch after file checkout', { ...context, operation, statusError });
111
+ else {
112
+ // Assume it was just confirming the current branch state
113
+ message = stderr.trim() || stdout.trim() || `Checked out ${input.branchOrPath}.`;
112
114
  }
113
115
  }
114
116
  else if (stderr.includes('Previous HEAD position was') && stderr.includes('HEAD is now at')) { // Detached HEAD
115
117
  message = `Checked out commit ${input.branchOrPath} (Detached HEAD state).`;
116
- currentBranch = 'Detached HEAD'; // Indicate detached state
118
+ currentBranch = 'Detached HEAD';
119
+ isDetachedHead = true;
117
120
  }
118
- else if (stderr.includes('Note: switching to')) { // Another detached HEAD message variant
121
+ else if (stderr.includes('Note: switching to') || stderr.includes('Note: checking out')) { // Other detached HEAD variants
119
122
  message = `Checked out ${input.branchOrPath} (Detached HEAD state).`;
120
123
  currentBranch = 'Detached HEAD';
124
+ isDetachedHead = true;
121
125
  }
122
126
  else if (message.includes('fatal:')) {
123
127
  success = false;
@@ -125,19 +129,63 @@ export async function checkoutGit(input, context) {
125
129
  logger.error(`Git checkout command indicated failure: ${message}`, { ...context, operation, stdout, stderr });
126
130
  }
127
131
  else if (!message && !stdout && !stderr) {
128
- message = 'Checkout command executed, but no output received.';
129
- logger.warning(message, { ...context, operation });
130
- // Attempt to get current branch as confirmation
132
+ message = 'Checkout command executed silently.'; // Assume success, will verify branch below
133
+ logger.info(message, { ...context, operation });
134
+ }
135
+ else {
136
+ // Some other message, treat as informational for now
137
+ message = stderr.trim() || stdout.trim();
138
+ logger.info(`Git checkout produced message: ${message}`, { ...context, operation });
139
+ }
140
+ // --- Get definitive current branch IF checkout was successful AND not file checkout/detached HEAD ---
141
+ if (success && !isFileCheckout && !isDetachedHead) {
131
142
  try {
143
+ logger.debug('Attempting to get current branch via git branch --show-current', { ...context, operation });
132
144
  const statusResult = await execAsync(`git -C "${targetPath}" branch --show-current`);
133
- currentBranch = statusResult.stdout.trim();
134
- message += ` Current branch is '${currentBranch}'.`;
145
+ const definitiveCurrentBranch = statusResult.stdout.trim();
146
+ if (definitiveCurrentBranch) {
147
+ currentBranch = definitiveCurrentBranch;
148
+ logger.info(`Confirmed current branch: ${currentBranch}`, { ...context, operation });
149
+ // Refine message if it wasn't specific before
150
+ if (message.startsWith('Checkout command executed silently') || message.startsWith('Checked out ')) {
151
+ message = `Checked out '${currentBranch}'.`;
152
+ }
153
+ else if (message.startsWith('Already on') && !message.includes(`'${currentBranch}'`)) {
154
+ message = `Already on '${currentBranch}'.`; // Update if initial parse was wrong
155
+ }
156
+ else if (message.startsWith('Switched to branch') && !message.includes(`'${currentBranch}'`)) {
157
+ message = `Switched to branch '${currentBranch}'.`; // Update if initial parse was wrong
158
+ }
159
+ }
160
+ else {
161
+ // Command succeeded but returned empty - might be detached HEAD after all?
162
+ logger.warning('git branch --show-current returned empty, possibly detached HEAD?', { ...context, operation });
163
+ // Keep potentially parsed 'Detached HEAD' or fallback to input if needed
164
+ currentBranch = currentBranch || 'Unknown (possibly detached)';
165
+ if (!message.includes('Detached HEAD'))
166
+ message += ' (Could not confirm branch name).';
167
+ }
135
168
  }
136
169
  catch (statusError) {
137
- logger.warning('Could not determine current branch after silent checkout', { ...context, operation, statusError });
170
+ logger.warning('Could not determine current branch after checkout', { ...context, operation, error: statusError.message });
171
+ // Keep potentially parsed 'Detached HEAD' or fallback to input if needed
172
+ currentBranch = currentBranch || 'Unknown (error checking)';
173
+ if (!message.includes('Detached HEAD'))
174
+ message += ' (Error checking branch name).';
175
+ }
176
+ }
177
+ else if (success && isFileCheckout) {
178
+ // If it was a file checkout, still try to get the branch name for context
179
+ try {
180
+ const statusResult = await execAsync(`git -C "${targetPath}" branch --show-current`);
181
+ currentBranch = statusResult.stdout.trim() || 'Unknown (possibly detached)';
182
+ }
183
+ catch {
184
+ currentBranch = 'Unknown (error checking)';
138
185
  }
186
+ logger.info(`Current branch after file checkout: ${currentBranch}`, { ...context, operation });
139
187
  }
140
- logger.info(`${operation} completed`, { ...context, operation, path: targetPath, success, message });
188
+ logger.info(`${operation} completed`, { ...context, operation, path: targetPath, success, message, currentBranch });
141
189
  return { success, message, previousBranch, currentBranch, newBranchCreated, filesRestored };
142
190
  }
143
191
  catch (error) {
@@ -1,8 +1,11 @@
1
- import { ErrorHandler } from '../../../utils/errorHandler.js';
2
- import { logger } from '../../../utils/logger.js';
3
- import { requestContextService } from '../../../utils/requestContext.js';
4
- import { GitCheckoutInputSchema, checkoutGit } from './logic.js';
5
- import { BaseErrorCode } from '../../../types-global/errors.js';
1
+ // Import utils from barrel (ErrorHandler from ../utils/internal/errorHandler.js)
2
+ import { ErrorHandler } from '../../../utils/index.js';
3
+ // Import utils from barrel (logger from ../utils/internal/logger.js)
4
+ import { logger } from '../../../utils/index.js';
5
+ // Import utils from barrel (requestContextService, RequestContext from ../utils/internal/requestContext.js)
6
+ import { BaseErrorCode } from '../../../types-global/errors.js'; // Keep direct import for types-global
7
+ import { requestContextService } from '../../../utils/index.js';
8
+ import { checkoutGit, GitCheckoutInputSchema } from './logic.js';
6
9
  let _getWorkingDirectory;
7
10
  let _getSessionId;
8
11
  /**