@cyanheads/git-mcp-server 2.0.2 → 2.0.4

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 (73) hide show
  1. package/README.md +45 -85
  2. package/dist/config/index.js +16 -18
  3. package/dist/index.js +80 -30
  4. package/dist/mcp-server/server.js +247 -523
  5. package/dist/mcp-server/tools/gitAdd/logic.js +9 -6
  6. package/dist/mcp-server/tools/gitAdd/registration.js +7 -4
  7. package/dist/mcp-server/tools/gitBranch/logic.js +23 -12
  8. package/dist/mcp-server/tools/gitBranch/registration.js +8 -5
  9. package/dist/mcp-server/tools/gitCheckout/logic.js +92 -44
  10. package/dist/mcp-server/tools/gitCheckout/registration.js +8 -5
  11. package/dist/mcp-server/tools/gitCherryPick/logic.js +10 -7
  12. package/dist/mcp-server/tools/gitCherryPick/registration.js +8 -5
  13. package/dist/mcp-server/tools/gitClean/logic.js +9 -6
  14. package/dist/mcp-server/tools/gitClean/registration.js +8 -5
  15. package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +3 -2
  16. package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +7 -4
  17. package/dist/mcp-server/tools/gitClone/logic.js +8 -5
  18. package/dist/mcp-server/tools/gitClone/registration.js +7 -4
  19. package/dist/mcp-server/tools/gitCommit/logic.js +98 -20
  20. package/dist/mcp-server/tools/gitCommit/registration.js +22 -15
  21. package/dist/mcp-server/tools/gitDiff/logic.js +9 -6
  22. package/dist/mcp-server/tools/gitDiff/registration.js +8 -5
  23. package/dist/mcp-server/tools/gitFetch/logic.js +10 -7
  24. package/dist/mcp-server/tools/gitFetch/registration.js +8 -5
  25. package/dist/mcp-server/tools/gitInit/index.js +2 -2
  26. package/dist/mcp-server/tools/gitInit/logic.js +9 -6
  27. package/dist/mcp-server/tools/gitInit/registration.js +66 -12
  28. package/dist/mcp-server/tools/gitLog/logic.js +53 -16
  29. package/dist/mcp-server/tools/gitLog/registration.js +8 -5
  30. package/dist/mcp-server/tools/gitMerge/logic.js +9 -6
  31. package/dist/mcp-server/tools/gitMerge/registration.js +8 -5
  32. package/dist/mcp-server/tools/gitPull/logic.js +11 -8
  33. package/dist/mcp-server/tools/gitPull/registration.js +7 -4
  34. package/dist/mcp-server/tools/gitPush/logic.js +12 -9
  35. package/dist/mcp-server/tools/gitPush/registration.js +7 -4
  36. package/dist/mcp-server/tools/gitRebase/logic.js +9 -6
  37. package/dist/mcp-server/tools/gitRebase/registration.js +8 -5
  38. package/dist/mcp-server/tools/gitRemote/logic.js +4 -5
  39. package/dist/mcp-server/tools/gitRemote/registration.js +2 -4
  40. package/dist/mcp-server/tools/gitReset/logic.js +5 -6
  41. package/dist/mcp-server/tools/gitReset/registration.js +2 -4
  42. package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +5 -6
  43. package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +22 -13
  44. package/dist/mcp-server/tools/gitShow/logic.js +5 -6
  45. package/dist/mcp-server/tools/gitShow/registration.js +3 -5
  46. package/dist/mcp-server/tools/gitStash/logic.js +5 -6
  47. package/dist/mcp-server/tools/gitStash/registration.js +3 -5
  48. package/dist/mcp-server/tools/gitStatus/logic.js +5 -6
  49. package/dist/mcp-server/tools/gitStatus/registration.js +2 -4
  50. package/dist/mcp-server/tools/gitTag/logic.js +3 -4
  51. package/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/dist/types-global/errors.js +2 -2
  56. package/dist/utils/index.js +12 -11
  57. package/dist/utils/{errorHandler.js → 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/dist/utils/{requestContext.js → internal/requestContext.js} +2 -3
  61. package/dist/utils/metrics/index.js +1 -0
  62. package/dist/utils/{tokenCounter.js → 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/dist/utils/{jsonParser.js → parsing/jsonParser.js} +3 -2
  66. package/dist/utils/{idGenerator.js → security/idGenerator.js} +4 -5
  67. package/dist/utils/security/index.js +3 -0
  68. package/dist/utils/{rateLimiter.js → security/rateLimiter.js} +7 -10
  69. package/dist/utils/{sanitization.js → security/sanitization.js} +4 -3
  70. package/package.json +12 -9
  71. package/dist/types-global/mcp.js +0 -59
  72. package/dist/types-global/tool.js +0 -1
  73. package/dist/utils/logger.js +0 -266
@@ -1,572 +1,296 @@
1
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.
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
7
14
  */
8
15
  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';
16
+ // Import validated configuration and environment details.
15
17
  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;
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';
76
47
  /**
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.
48
+ * Creates and configures a new instance of the McpServer.
93
49
  *
94
- * Security Note: Carefully configure `MCP_ALLOWED_ORIGINS` in production environments
95
- * to prevent unauthorized websites from interacting with the MCP server.
50
+ * This function is central to defining the server's identity and functionality
51
+ * as presented to connecting clients during the MCP initialization phase.
96
52
  *
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).
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.
142
74
  *
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.
75
+ * @returns {Promise<McpServer>} A promise resolving with the configured McpServer instance.
76
+ * @throws {Error} If any resource or tool registration fails.
146
77
  */
78
+ // Removed sessionId parameter, it will be retrieved from context within tool handlers
147
79
  async function createMcpServerInstance() {
148
80
  const context = { operation: 'createMcpServerInstance' };
149
81
  logger.info('Initializing MCP server instance', context);
150
- // Configure the request context service for associating logs/traces with specific requests or operations.
82
+ // Configure the request context service (used for correlating logs/errors).
151
83
  requestContextService.configure({
152
84
  appName: config.mcpServerName,
153
85
  appVersion: config.mcpServerVersion,
154
86
  environment,
155
87
  });
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 } } });
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
+ }
159
171
  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);
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);
186
200
  }
187
201
  catch (err) {
188
- // Log and re-throw any errors during registration, as the server cannot function correctly without them.
202
+ // Registration is critical; log and re-throw errors.
189
203
  logger.error('Failed to register resources/tools', {
190
204
  ...context,
191
205
  error: err instanceof Error ? err.message : String(err),
206
+ stack: err instanceof Error ? err.stack : undefined, // Include stack for debugging
192
207
  });
193
- throw err; // Propagate the error to the caller.
208
+ throw err; // Propagate error to prevent server starting with incomplete capabilities.
194
209
  }
195
210
  return server;
196
211
  }
197
212
  /**
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.
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.
257
215
  *
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`.
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.
267
228
  *
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.
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.
277
231
  */
278
232
  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
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);
358
237
  // --- 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.
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.
521
244
  return;
522
245
  }
523
246
  // --- 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
- }
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;
544
257
  }
545
258
  // --- 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'.`);
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'.`);
549
262
  }
550
263
  /**
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.
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.
554
271
  *
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.
272
+ * @returns {Promise<void | McpServer>} Resolves upon successful startup (void for http, McpServer for stdio). Rejects on critical failure.
558
273
  */
559
274
  export async function initializeAndStartServer() {
275
+ const context = { operation: 'initializeAndStartServer' };
276
+ logger.info('MCP Server initialization sequence started.', context);
560
277
  try {
561
- // Start the appropriate transport (stdio or http).
562
- return await startTransport();
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;
563
282
  }
564
283
  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.
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);
570
294
  process.exit(1);
571
295
  }
572
296
  }