@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.
- package/README.md +45 -85
- package/dist/config/index.js +16 -18
- package/dist/index.js +80 -30
- package/dist/mcp-server/server.js +247 -523
- package/dist/mcp-server/tools/gitAdd/logic.js +9 -6
- package/dist/mcp-server/tools/gitAdd/registration.js +7 -4
- package/dist/mcp-server/tools/gitBranch/logic.js +23 -12
- package/dist/mcp-server/tools/gitBranch/registration.js +8 -5
- package/dist/mcp-server/tools/gitCheckout/logic.js +92 -44
- package/dist/mcp-server/tools/gitCheckout/registration.js +8 -5
- package/dist/mcp-server/tools/gitCherryPick/logic.js +10 -7
- package/dist/mcp-server/tools/gitCherryPick/registration.js +8 -5
- package/dist/mcp-server/tools/gitClean/logic.js +9 -6
- package/dist/mcp-server/tools/gitClean/registration.js +8 -5
- package/dist/mcp-server/tools/gitClearWorkingDir/logic.js +3 -2
- package/dist/mcp-server/tools/gitClearWorkingDir/registration.js +7 -4
- package/dist/mcp-server/tools/gitClone/logic.js +8 -5
- package/dist/mcp-server/tools/gitClone/registration.js +7 -4
- package/dist/mcp-server/tools/gitCommit/logic.js +98 -20
- package/dist/mcp-server/tools/gitCommit/registration.js +22 -15
- package/dist/mcp-server/tools/gitDiff/logic.js +9 -6
- package/dist/mcp-server/tools/gitDiff/registration.js +8 -5
- package/dist/mcp-server/tools/gitFetch/logic.js +10 -7
- package/dist/mcp-server/tools/gitFetch/registration.js +8 -5
- package/dist/mcp-server/tools/gitInit/index.js +2 -2
- package/dist/mcp-server/tools/gitInit/logic.js +9 -6
- package/dist/mcp-server/tools/gitInit/registration.js +66 -12
- package/dist/mcp-server/tools/gitLog/logic.js +53 -16
- package/dist/mcp-server/tools/gitLog/registration.js +8 -5
- package/dist/mcp-server/tools/gitMerge/logic.js +9 -6
- package/dist/mcp-server/tools/gitMerge/registration.js +8 -5
- package/dist/mcp-server/tools/gitPull/logic.js +11 -8
- package/dist/mcp-server/tools/gitPull/registration.js +7 -4
- package/dist/mcp-server/tools/gitPush/logic.js +12 -9
- package/dist/mcp-server/tools/gitPush/registration.js +7 -4
- package/dist/mcp-server/tools/gitRebase/logic.js +9 -6
- package/dist/mcp-server/tools/gitRebase/registration.js +8 -5
- package/dist/mcp-server/tools/gitRemote/logic.js +4 -5
- package/dist/mcp-server/tools/gitRemote/registration.js +2 -4
- package/dist/mcp-server/tools/gitReset/logic.js +5 -6
- package/dist/mcp-server/tools/gitReset/registration.js +2 -4
- package/dist/mcp-server/tools/gitSetWorkingDir/logic.js +5 -6
- package/dist/mcp-server/tools/gitSetWorkingDir/registration.js +22 -13
- package/dist/mcp-server/tools/gitShow/logic.js +5 -6
- package/dist/mcp-server/tools/gitShow/registration.js +3 -5
- package/dist/mcp-server/tools/gitStash/logic.js +5 -6
- package/dist/mcp-server/tools/gitStash/registration.js +3 -5
- package/dist/mcp-server/tools/gitStatus/logic.js +5 -6
- package/dist/mcp-server/tools/gitStatus/registration.js +2 -4
- package/dist/mcp-server/tools/gitTag/logic.js +3 -4
- package/dist/mcp-server/tools/gitTag/registration.js +2 -4
- package/dist/mcp-server/transports/authentication/authMiddleware.js +145 -0
- package/dist/mcp-server/transports/httpTransport.js +432 -0
- package/dist/mcp-server/transports/stdioTransport.js +87 -0
- package/dist/types-global/errors.js +2 -2
- package/dist/utils/index.js +12 -11
- package/dist/utils/{errorHandler.js → internal/errorHandler.js} +18 -8
- package/dist/utils/internal/index.js +3 -0
- package/dist/utils/internal/logger.js +254 -0
- package/dist/utils/{requestContext.js → internal/requestContext.js} +2 -3
- package/dist/utils/metrics/index.js +1 -0
- package/dist/utils/{tokenCounter.js → metrics/tokenCounter.js} +3 -3
- package/dist/utils/parsing/dateParser.js +62 -0
- package/dist/utils/parsing/index.js +2 -0
- package/dist/utils/{jsonParser.js → parsing/jsonParser.js} +3 -2
- package/dist/utils/{idGenerator.js → security/idGenerator.js} +4 -5
- package/dist/utils/security/index.js +3 -0
- package/dist/utils/{rateLimiter.js → security/rateLimiter.js} +7 -10
- package/dist/utils/{sanitization.js → security/sanitization.js} +4 -3
- package/package.json +12 -9
- package/dist/types-global/mcp.js +0 -59
- package/dist/types-global/tool.js +0 -1
- package/dist/utils/logger.js +0 -266
|
@@ -1,572 +1,296 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* This file
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
import { logger } from '../utils/
|
|
18
|
-
|
|
19
|
-
import { registerGitAddTool } from './tools/gitAdd/index.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { registerGitCloneTool } from './tools/gitClone/index.js'; //
|
|
26
|
-
import { registerGitCommitTool } from './tools/gitCommit/index.js'; //
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import { registerGitInitTool } from './tools/gitInit/index.js'; //
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import { registerGitStatusTool } from './tools/gitStatus/index.js'; //
|
|
41
|
-
import {
|
|
42
|
-
//
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
95
|
-
*
|
|
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
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
* @
|
|
144
|
-
* @
|
|
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
|
|
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
|
|
157
|
-
//
|
|
158
|
-
|
|
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
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
await
|
|
164
|
-
await
|
|
165
|
-
await
|
|
166
|
-
await
|
|
167
|
-
await
|
|
168
|
-
await
|
|
169
|
-
await
|
|
170
|
-
await
|
|
171
|
-
await
|
|
172
|
-
await
|
|
173
|
-
await
|
|
174
|
-
await
|
|
175
|
-
await
|
|
176
|
-
await
|
|
177
|
-
await
|
|
178
|
-
await
|
|
179
|
-
await
|
|
180
|
-
await
|
|
181
|
-
await
|
|
182
|
-
await
|
|
183
|
-
await
|
|
184
|
-
await
|
|
185
|
-
|
|
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
|
-
//
|
|
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
|
|
208
|
+
throw err; // Propagate error to prevent server starting with incomplete capabilities.
|
|
194
209
|
}
|
|
195
210
|
return server;
|
|
196
211
|
}
|
|
197
212
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
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
|
-
*
|
|
259
|
-
* -
|
|
260
|
-
*
|
|
261
|
-
* -
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
* -
|
|
266
|
-
*
|
|
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
|
-
*
|
|
269
|
-
*
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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 (
|
|
360
|
-
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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 (
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
//
|
|
547
|
-
logger.fatal(`Unsupported transport type configured: ${
|
|
548
|
-
throw new Error(`Unsupported transport type: ${
|
|
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
|
-
*
|
|
553
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
//
|
|
562
|
-
|
|
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
|
-
//
|
|
566
|
-
logger.fatal('
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
}
|