@cyanheads/git-mcp-server 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -89
- package/{build → dist}/config/index.js +16 -18
- package/{build → dist}/index.js +80 -30
- package/dist/mcp-server/server.js +296 -0
- package/{build → dist}/mcp-server/tools/gitAdd/logic.js +9 -6
- package/{build → dist}/mcp-server/tools/gitAdd/registration.js +7 -4
- package/{build → dist}/mcp-server/tools/gitBranch/logic.js +23 -12
- package/{build → dist}/mcp-server/tools/gitBranch/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitCheckout/logic.js +92 -44
- package/{build → dist}/mcp-server/tools/gitCheckout/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitCherryPick/logic.js +10 -7
- package/{build → dist}/mcp-server/tools/gitCherryPick/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitClean/logic.js +9 -6
- package/{build → dist}/mcp-server/tools/gitClean/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitClearWorkingDir/logic.js +3 -2
- package/{build → dist}/mcp-server/tools/gitClearWorkingDir/registration.js +7 -4
- package/{build → dist}/mcp-server/tools/gitClone/logic.js +8 -5
- package/{build → dist}/mcp-server/tools/gitClone/registration.js +7 -4
- package/dist/mcp-server/tools/gitCommit/logic.js +207 -0
- package/{build → dist}/mcp-server/tools/gitCommit/registration.js +22 -15
- package/{build → dist}/mcp-server/tools/gitDiff/logic.js +9 -6
- package/{build → dist}/mcp-server/tools/gitDiff/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitFetch/logic.js +10 -7
- package/{build → dist}/mcp-server/tools/gitFetch/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitInit/index.js +2 -2
- package/{build → dist}/mcp-server/tools/gitInit/logic.js +9 -6
- package/dist/mcp-server/tools/gitInit/registration.js +98 -0
- package/{build → dist}/mcp-server/tools/gitLog/logic.js +53 -16
- package/{build → dist}/mcp-server/tools/gitLog/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitMerge/logic.js +9 -6
- package/{build → dist}/mcp-server/tools/gitMerge/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitPull/logic.js +11 -8
- package/{build → dist}/mcp-server/tools/gitPull/registration.js +7 -4
- package/{build → dist}/mcp-server/tools/gitPush/logic.js +12 -9
- package/{build → dist}/mcp-server/tools/gitPush/registration.js +7 -4
- package/{build → dist}/mcp-server/tools/gitRebase/logic.js +9 -6
- package/{build → dist}/mcp-server/tools/gitRebase/registration.js +8 -5
- package/{build → dist}/mcp-server/tools/gitRemote/logic.js +4 -5
- package/{build → dist}/mcp-server/tools/gitRemote/registration.js +2 -4
- package/{build → dist}/mcp-server/tools/gitReset/logic.js +5 -6
- package/{build → dist}/mcp-server/tools/gitReset/registration.js +2 -4
- package/{build → dist}/mcp-server/tools/gitSetWorkingDir/logic.js +5 -6
- package/{build → dist}/mcp-server/tools/gitSetWorkingDir/registration.js +22 -13
- package/{build → dist}/mcp-server/tools/gitShow/logic.js +5 -6
- package/{build → dist}/mcp-server/tools/gitShow/registration.js +3 -5
- package/{build → dist}/mcp-server/tools/gitStash/logic.js +5 -6
- package/{build → dist}/mcp-server/tools/gitStash/registration.js +3 -5
- package/{build → dist}/mcp-server/tools/gitStatus/logic.js +5 -6
- package/{build → dist}/mcp-server/tools/gitStatus/registration.js +2 -4
- package/{build → dist}/mcp-server/tools/gitTag/logic.js +3 -4
- package/{build → 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/{build → dist}/types-global/errors.js +2 -2
- package/dist/utils/index.js +12 -0
- package/{build/utils → dist/utils/internal}/errorHandler.js +18 -8
- package/dist/utils/internal/index.js +3 -0
- package/dist/utils/internal/logger.js +254 -0
- package/{build/utils → dist/utils/internal}/requestContext.js +2 -3
- package/dist/utils/metrics/index.js +1 -0
- package/{build/utils → dist/utils/metrics}/tokenCounter.js +3 -3
- package/dist/utils/parsing/dateParser.js +62 -0
- package/dist/utils/parsing/index.js +2 -0
- package/{build/utils → dist/utils/parsing}/jsonParser.js +3 -2
- package/{build/utils → dist/utils/security}/idGenerator.js +4 -5
- package/dist/utils/security/index.js +3 -0
- package/{build/utils → dist/utils/security}/rateLimiter.js +7 -10
- package/{build/utils → dist/utils/security}/sanitization.js +4 -3
- package/package.json +20 -16
- package/build/mcp-server/server.js +0 -572
- package/build/mcp-server/tools/gitCommit/logic.js +0 -129
- package/build/mcp-server/tools/gitInit/registration.js +0 -44
- package/build/types-global/mcp.js +0 -59
- package/build/types-global/tool.js +0 -1
- package/build/utils/index.js +0 -11
- package/build/utils/logger.js +0 -266
- /package/{build → dist}/mcp-server/tools/gitAdd/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitBranch/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitCheckout/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitCherryPick/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitClean/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitClearWorkingDir/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitClone/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitCommit/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitDiff/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitFetch/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitLog/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitMerge/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitPull/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitPush/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitRebase/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitRemote/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitReset/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitSetWorkingDir/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitShow/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitStash/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitStatus/index.js +0 -0
- /package/{build → dist}/mcp-server/tools/gitTag/index.js +0 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles the setup and management of the Streamable HTTP MCP transport.
|
|
3
|
+
* Implements the MCP Specification 2025-03-26 for Streamable HTTP.
|
|
4
|
+
* Includes Express server creation, middleware (CORS, Auth), request routing
|
|
5
|
+
* (POST/GET/DELETE on a single endpoint), session handling, SSE streaming,
|
|
6
|
+
* and port binding with retry logic.
|
|
7
|
+
*
|
|
8
|
+
* Specification Reference:
|
|
9
|
+
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
|
|
10
|
+
*/
|
|
11
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
12
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; // SDK type guard for InitializeRequest
|
|
13
|
+
import express from 'express';
|
|
14
|
+
import http from 'http';
|
|
15
|
+
import { randomUUID } from 'node:crypto';
|
|
16
|
+
// Import config and utils
|
|
17
|
+
import { config } from '../../config/index.js'; // Import the validated config object
|
|
18
|
+
import { logger } from '../../utils/index.js';
|
|
19
|
+
import { mcpAuthMiddleware } from './authentication/authMiddleware.js'; // Import the auth middleware
|
|
20
|
+
// --- Configuration Constants (Derived from imported config) ---
|
|
21
|
+
/**
|
|
22
|
+
* The port number for the HTTP transport, configured via MCP_HTTP_PORT.
|
|
23
|
+
* Defaults to 3010 (defined in config/index.ts).
|
|
24
|
+
* @constant {number} HTTP_PORT
|
|
25
|
+
*/
|
|
26
|
+
const HTTP_PORT = config.mcpHttpPort;
|
|
27
|
+
/**
|
|
28
|
+
* The host address for the HTTP transport, configured via MCP_HTTP_HOST.
|
|
29
|
+
* Defaults to '127.0.0.1' (defined in config/index.ts).
|
|
30
|
+
* MCP Spec Security: Recommends binding to localhost for local servers.
|
|
31
|
+
* @constant {string} HTTP_HOST
|
|
32
|
+
*/
|
|
33
|
+
const HTTP_HOST = config.mcpHttpHost;
|
|
34
|
+
/**
|
|
35
|
+
* The single HTTP endpoint path for all MCP communication, as required by the spec.
|
|
36
|
+
* Supports POST, GET, DELETE, OPTIONS methods.
|
|
37
|
+
* @constant {string} MCP_ENDPOINT_PATH
|
|
38
|
+
*/
|
|
39
|
+
const MCP_ENDPOINT_PATH = '/mcp';
|
|
40
|
+
/**
|
|
41
|
+
* Maximum number of attempts to find an available port if the initial HTTP_PORT is in use.
|
|
42
|
+
* Tries ports sequentially: HTTP_PORT, HTTP_PORT + 1, ...
|
|
43
|
+
* @constant {number} MAX_PORT_RETRIES
|
|
44
|
+
*/
|
|
45
|
+
const MAX_PORT_RETRIES = 15;
|
|
46
|
+
/**
|
|
47
|
+
* Stores active StreamableHTTPServerTransport instances, keyed by their session ID.
|
|
48
|
+
* Essential for routing subsequent requests to the correct stateful session.
|
|
49
|
+
* @type {Record<string, StreamableHTTPServerTransport>}
|
|
50
|
+
*/
|
|
51
|
+
const httpTransports = {};
|
|
52
|
+
/** Stores the working directory for each active HTTP session. */
|
|
53
|
+
const sessionWorkingDirectories = new Map();
|
|
54
|
+
/**
|
|
55
|
+
* Gets the current working directory set for a specific HTTP session.
|
|
56
|
+
* @param {string} sessionId - The ID of the session.
|
|
57
|
+
* @returns {string | undefined} The current working directory path or undefined if not set.
|
|
58
|
+
*/
|
|
59
|
+
export function getHttpSessionWorkingDirectory(sessionId) {
|
|
60
|
+
return sessionWorkingDirectories.get(sessionId);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Sets the working directory for a specific HTTP session.
|
|
64
|
+
* @param {string} sessionId - The ID of the session.
|
|
65
|
+
* @param {string} dir - The new working directory path.
|
|
66
|
+
*/
|
|
67
|
+
export function setHttpSessionWorkingDirectory(sessionId, dir) {
|
|
68
|
+
sessionWorkingDirectories.set(sessionId, dir);
|
|
69
|
+
logger.info(`HTTP session ${sessionId} working directory set to: ${dir}`, { operation: 'setHttpSessionWorkingDirectory', sessionId });
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Checks if an incoming HTTP request's origin header is permissible.
|
|
73
|
+
* MCP Spec Security: Servers MUST validate the `Origin` header.
|
|
74
|
+
* This function checks against `MCP_ALLOWED_ORIGINS` and allows requests
|
|
75
|
+
* from localhost if the server is bound locally. Sets CORS headers if allowed.
|
|
76
|
+
*
|
|
77
|
+
* @param {Request} req - Express request object.
|
|
78
|
+
* @param {Response} res - Express response object.
|
|
79
|
+
* @returns {boolean} True if the origin is allowed, false otherwise.
|
|
80
|
+
*/
|
|
81
|
+
function isOriginAllowed(req, res) {
|
|
82
|
+
const origin = req.headers.origin;
|
|
83
|
+
const host = req.hostname; // Considers Host header
|
|
84
|
+
const isLocalhostBinding = ['127.0.0.1', '::1', 'localhost'].includes(host);
|
|
85
|
+
const allowedOrigins = config.mcpAllowedOrigins || []; // Use parsed array from config
|
|
86
|
+
const context = { operation: 'isOriginAllowed', origin, host, isLocalhostBinding, allowedOrigins };
|
|
87
|
+
logger.debug('Checking origin allowance', context);
|
|
88
|
+
// Determine if allowed based on config or localhost binding
|
|
89
|
+
const allowed = (origin && allowedOrigins.includes(origin)) || (isLocalhostBinding && (!origin || origin === 'null'));
|
|
90
|
+
if (allowed && origin) {
|
|
91
|
+
// Origin is allowed and present, set specific CORS headers.
|
|
92
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
93
|
+
// MCP Spec: Streamable HTTP uses POST, GET, DELETE. OPTIONS is for preflight.
|
|
94
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
95
|
+
// MCP Spec: Requires Mcp-Session-Id. Last-Event-ID for SSE resumption. Content-Type is standard. Authorization for security.
|
|
96
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id, Last-Event-ID, Authorization');
|
|
97
|
+
res.setHeader('Access-Control-Allow-Credentials', 'true'); // Set based on whether auth/cookies are used
|
|
98
|
+
}
|
|
99
|
+
else if (allowed && !origin) {
|
|
100
|
+
// Allowed (e.g., localhost binding, file:// origin), but no origin header to echo back. No specific CORS needed.
|
|
101
|
+
}
|
|
102
|
+
else if (!allowed && origin) {
|
|
103
|
+
// Origin provided but not in allowed list. Log warning.
|
|
104
|
+
logger.warning(`Origin denied: ${origin}`, context);
|
|
105
|
+
}
|
|
106
|
+
logger.debug(`Origin check result: ${allowed}`, { ...context, allowed });
|
|
107
|
+
return allowed;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Proactively checks if a specific port is already in use. (Asynchronous)
|
|
111
|
+
* @param {number} port - Port to check.
|
|
112
|
+
* @param {string} host - Host address to check.
|
|
113
|
+
* @param {Record<string, any>} context - Logging context.
|
|
114
|
+
* @returns {Promise<boolean>} True if port is in use (EADDRINUSE), false otherwise.
|
|
115
|
+
*/
|
|
116
|
+
async function isPortInUse(port, host, context) {
|
|
117
|
+
const checkContext = { ...context, operation: 'isPortInUse', port, host };
|
|
118
|
+
logger.debug(`Proactively checking port usability...`, checkContext);
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
const tempServer = http.createServer();
|
|
121
|
+
tempServer
|
|
122
|
+
.once('error', (err) => {
|
|
123
|
+
if (err.code === 'EADDRINUSE') {
|
|
124
|
+
logger.debug(`Proactive check: Port confirmed in use (EADDRINUSE).`, checkContext);
|
|
125
|
+
resolve(true); // Port is definitely in use
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
logger.debug(`Proactive check: Non-EADDRINUSE error encountered: ${err.message}`, { ...checkContext, errorCode: err.code });
|
|
129
|
+
resolve(false); // Other error, let main listen attempt handle it
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
.once('listening', () => {
|
|
133
|
+
logger.debug(`Proactive check: Port is available.`, checkContext);
|
|
134
|
+
tempServer.close(() => resolve(false)); // Port is free
|
|
135
|
+
})
|
|
136
|
+
.listen(port, host);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Attempts to start the HTTP server, retrying on incrementing ports if EADDRINUSE occurs. (Asynchronous)
|
|
141
|
+
* Uses proactive checks before attempting to bind the main server instance.
|
|
142
|
+
*
|
|
143
|
+
* @param {http.Server} serverInstance - The Node.js HTTP server instance.
|
|
144
|
+
* @param {number} initialPort - The starting port number.
|
|
145
|
+
* @param {string} host - The host address to bind to.
|
|
146
|
+
* @param {number} maxRetries - Maximum number of additional ports to try.
|
|
147
|
+
* @param {Record<string, any>} context - Logging context.
|
|
148
|
+
* @returns {Promise<number>} Resolves with the port number successfully bound to.
|
|
149
|
+
* @throws {Error} Rejects if binding fails after all retries or for non-EADDRINUSE errors.
|
|
150
|
+
*/
|
|
151
|
+
function startHttpServerWithRetry(serverInstance, initialPort, host, maxRetries, context) {
|
|
152
|
+
const startContext = { ...context, operation: 'startHttpServerWithRetry', initialPort, host, maxRetries };
|
|
153
|
+
logger.debug(`Attempting to start HTTP server...`, startContext);
|
|
154
|
+
return new Promise(async (resolve, reject) => {
|
|
155
|
+
let lastError = null;
|
|
156
|
+
for (let i = 0; i <= maxRetries; i++) {
|
|
157
|
+
const currentPort = initialPort + i;
|
|
158
|
+
const attemptContext = { ...startContext, port: currentPort, attempt: i + 1, maxAttempts: maxRetries + 1 };
|
|
159
|
+
logger.debug(`Attempting port ${currentPort} (${attemptContext.attempt}/${attemptContext.maxAttempts})`, attemptContext);
|
|
160
|
+
// 1. Proactive Check
|
|
161
|
+
if (await isPortInUse(currentPort, host, attemptContext)) {
|
|
162
|
+
logger.warning(`Proactive check detected port ${currentPort} is in use, retrying...`, attemptContext);
|
|
163
|
+
lastError = new Error(`EADDRINUSE: Port ${currentPort} detected as in use by proactive check.`);
|
|
164
|
+
await new Promise(res => setTimeout(res, 100)); // Short delay
|
|
165
|
+
continue; // Try next port
|
|
166
|
+
}
|
|
167
|
+
// 2. Attempt Main Server Bind
|
|
168
|
+
try {
|
|
169
|
+
await new Promise((listenResolve, listenReject) => {
|
|
170
|
+
serverInstance.listen(currentPort, host, () => {
|
|
171
|
+
const serverAddress = `http://${host}:${currentPort}${MCP_ENDPOINT_PATH}`;
|
|
172
|
+
logger.info(`HTTP transport successfully listening on host ${host} at ${serverAddress}`, { ...attemptContext, address: serverAddress });
|
|
173
|
+
listenResolve(); // Success
|
|
174
|
+
}).on('error', (err) => {
|
|
175
|
+
listenReject(err); // Forward error
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
resolve(currentPort); // Listen succeeded
|
|
179
|
+
return; // Exit function
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
lastError = err;
|
|
183
|
+
logger.debug(`Listen error on port ${currentPort}: Code=${err.code}, Message=${err.message}`, { ...attemptContext, errorCode: err.code, errorMessage: err.message });
|
|
184
|
+
if (err.code === 'EADDRINUSE') {
|
|
185
|
+
logger.warning(`Port ${currentPort} already in use (EADDRINUSE), retrying...`, attemptContext);
|
|
186
|
+
await new Promise(res => setTimeout(res, 100)); // Short delay before retry
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
logger.error(`Failed to bind to port ${currentPort} due to non-EADDRINUSE error: ${err.message}`, { ...attemptContext, error: err.message });
|
|
190
|
+
reject(err); // Non-recoverable error for this port
|
|
191
|
+
return; // Exit function
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Loop finished without success
|
|
196
|
+
logger.error(`Failed to bind to any port after ${maxRetries + 1} attempts. Last error: ${lastError?.message}`, { ...startContext, error: lastError?.message });
|
|
197
|
+
reject(lastError || new Error('Failed to bind to any port after multiple retries.'));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Sets up and starts the Streamable HTTP transport layer for MCP. (Asynchronous)
|
|
202
|
+
* Creates Express app, configures middleware (CORS, Auth, Security Headers),
|
|
203
|
+
* defines the single MCP endpoint handler for POST/GET/DELETE, manages sessions,
|
|
204
|
+
* and starts the HTTP server with retry logic.
|
|
205
|
+
*
|
|
206
|
+
* @param {() => Promise<McpServer>} createServerInstanceFn - Async factory function to create a new McpServer instance per session.
|
|
207
|
+
* @param {Record<string, any>} context - Logging context.
|
|
208
|
+
* @returns {Promise<void>} Resolves when the server is listening, or rejects on failure.
|
|
209
|
+
* @throws {Error} If the server fails to start after retries.
|
|
210
|
+
*/
|
|
211
|
+
export async function startHttpTransport(createServerInstanceFn, context) {
|
|
212
|
+
const app = express();
|
|
213
|
+
const transportContext = { ...context, transportType: 'HTTP' };
|
|
214
|
+
logger.debug('Setting up Express app for HTTP transport...', transportContext);
|
|
215
|
+
// Middleware to parse JSON request bodies. Required for MCP messages.
|
|
216
|
+
app.use(express.json());
|
|
217
|
+
// --- Security Middleware Pipeline ---
|
|
218
|
+
// 1. CORS Preflight (OPTIONS) Handler
|
|
219
|
+
// Handles OPTIONS requests sent by browsers before actual GET/POST/DELETE.
|
|
220
|
+
app.options(MCP_ENDPOINT_PATH, (req, res) => {
|
|
221
|
+
const optionsContext = { ...transportContext, operation: 'handleOptions', origin: req.headers.origin };
|
|
222
|
+
logger.debug(`Received OPTIONS request for ${MCP_ENDPOINT_PATH}`, optionsContext);
|
|
223
|
+
if (isOriginAllowed(req, res)) {
|
|
224
|
+
// isOriginAllowed sets necessary Access-Control-* headers.
|
|
225
|
+
logger.debug('OPTIONS request origin allowed, sending 204.', optionsContext);
|
|
226
|
+
res.sendStatus(204); // OK, No Content
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// isOriginAllowed logs the warning.
|
|
230
|
+
logger.debug('OPTIONS request origin denied, sending 403.', optionsContext);
|
|
231
|
+
res.status(403).send('Forbidden: Invalid Origin');
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
// 2. General Security Headers & Origin Check Middleware (for non-OPTIONS)
|
|
235
|
+
app.use((req, res, next) => {
|
|
236
|
+
const securityContext = { ...transportContext, operation: 'securityMiddleware', path: req.path, method: req.method, origin: req.headers.origin };
|
|
237
|
+
logger.debug(`Applying security middleware...`, securityContext);
|
|
238
|
+
// Check origin again for non-OPTIONS requests and set CORS headers if allowed.
|
|
239
|
+
if (!isOriginAllowed(req, res)) {
|
|
240
|
+
// isOriginAllowed logs the warning.
|
|
241
|
+
logger.debug('Origin check failed, sending 403.', securityContext);
|
|
242
|
+
res.status(403).send('Forbidden: Invalid Origin');
|
|
243
|
+
return; // Block request
|
|
244
|
+
}
|
|
245
|
+
// Apply standard security headers to all allowed responses.
|
|
246
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
247
|
+
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
248
|
+
// Basic Content Security Policy (CSP). Adjust if server needs external connections.
|
|
249
|
+
// 'connect-src 'self'' allows connections back to the server's own origin (needed for SSE).
|
|
250
|
+
res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'self'; frame-src 'none'; font-src 'self'; connect-src 'self'");
|
|
251
|
+
// Strict-Transport-Security (HSTS) - IMPORTANT: Enable only if server is *always* served over HTTPS.
|
|
252
|
+
// if (config.environment === 'production') {
|
|
253
|
+
// res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); // 1 year
|
|
254
|
+
// }
|
|
255
|
+
logger.debug('Security middleware passed.', securityContext);
|
|
256
|
+
next(); // Proceed to next middleware/handler
|
|
257
|
+
});
|
|
258
|
+
// 3. MCP Authentication Middleware (Optional, based on config)
|
|
259
|
+
// Verifies Authorization header (e.g., Bearer token) if enabled.
|
|
260
|
+
app.use(mcpAuthMiddleware);
|
|
261
|
+
// --- MCP Route Handlers ---
|
|
262
|
+
// Handle POST requests: Used for Initialize and all subsequent client->server messages.
|
|
263
|
+
// MCP Spec: Client MUST use POST. Body is single message or batch.
|
|
264
|
+
// MCP Spec: Server responds 202 for notification/response-only, or JSON/SSE for requests.
|
|
265
|
+
app.post(MCP_ENDPOINT_PATH, async (req, res) => {
|
|
266
|
+
// Define base context for this request
|
|
267
|
+
const basePostContext = { ...transportContext, operation: 'handlePost', method: 'POST' };
|
|
268
|
+
logger.debug(`Received POST request on ${MCP_ENDPOINT_PATH}`, { ...basePostContext, headers: req.headers, bodyPreview: JSON.stringify(req.body).substring(0, 100) });
|
|
269
|
+
// MCP Spec: Session ID MUST be included by client after initialization.
|
|
270
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
271
|
+
// Log extracted session ID, adding it to the context for this specific log message
|
|
272
|
+
logger.debug(`Extracted session ID: ${sessionId}`, { ...basePostContext, sessionId });
|
|
273
|
+
let transport = sessionId ? httpTransports[sessionId] : undefined;
|
|
274
|
+
// Log transport lookup result, adding sessionId to context
|
|
275
|
+
logger.debug(`Found existing transport for session ID: ${!!transport}`, { ...basePostContext, sessionId });
|
|
276
|
+
// Check if it's an InitializeRequest using SDK helper.
|
|
277
|
+
const isInitReq = isInitializeRequest(req.body);
|
|
278
|
+
logger.debug(`Is InitializeRequest: ${isInitReq}`, { ...basePostContext, sessionId });
|
|
279
|
+
const requestId = req.body?.id || null; // For potential error responses
|
|
280
|
+
try {
|
|
281
|
+
// --- Handle Initialization Request ---
|
|
282
|
+
if (isInitReq) {
|
|
283
|
+
if (transport) {
|
|
284
|
+
// Client sent Initialize on an existing session - likely an error or recovery attempt.
|
|
285
|
+
// Close the old session cleanly before creating a new one.
|
|
286
|
+
logger.warning('Received InitializeRequest on an existing session ID. Closing old session and creating new.', { ...basePostContext, sessionId });
|
|
287
|
+
await transport.close(); // Ensure cleanup
|
|
288
|
+
delete httpTransports[sessionId];
|
|
289
|
+
}
|
|
290
|
+
logger.info('Handling Initialize Request: Creating new session...', { ...basePostContext, sessionId });
|
|
291
|
+
// Create new SDK transport instance for this session.
|
|
292
|
+
transport = new StreamableHTTPServerTransport({
|
|
293
|
+
// MCP Spec: Server MAY assign session ID on InitializeResponse via Mcp-Session-Id header.
|
|
294
|
+
sessionIdGenerator: () => {
|
|
295
|
+
const newId = randomUUID(); // Secure UUID generation
|
|
296
|
+
logger.debug(`Generated new session ID: ${newId}`, basePostContext); // Use base context here
|
|
297
|
+
return newId;
|
|
298
|
+
},
|
|
299
|
+
onsessioninitialized: (newId) => {
|
|
300
|
+
// Store the transport instance once the session ID is confirmed and sent to client.
|
|
301
|
+
logger.debug(`Session initialized callback triggered for ID: ${newId}`, { ...basePostContext, newSessionId: newId });
|
|
302
|
+
httpTransports[newId] = transport; // Store by the generated ID
|
|
303
|
+
logger.info(`HTTP Session created: ${newId}`, { ...basePostContext, newSessionId: newId });
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
// Define cleanup logic when the transport closes (client disconnect, DELETE, error).
|
|
307
|
+
transport.onclose = () => {
|
|
308
|
+
const closedSessionId = transport.sessionId; // Get ID before potential deletion
|
|
309
|
+
// Removed duplicate declaration below
|
|
310
|
+
if (closedSessionId) {
|
|
311
|
+
logger.debug(`onclose handler triggered for session ID: ${closedSessionId}`, { ...basePostContext, closedSessionId });
|
|
312
|
+
delete httpTransports[closedSessionId]; // Remove from active transports
|
|
313
|
+
sessionWorkingDirectories.delete(closedSessionId); // Clean up working directory state
|
|
314
|
+
logger.info(`HTTP Session closed and state cleaned: ${closedSessionId}`, { ...basePostContext, closedSessionId });
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
logger.debug('onclose handler triggered for transport without session ID (likely init failure).', basePostContext);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
// Create a dedicated McpServer instance for this new session.
|
|
321
|
+
logger.debug('Creating McpServer instance for new session...', basePostContext);
|
|
322
|
+
const server = await createServerInstanceFn();
|
|
323
|
+
// Connect the server logic to the transport layer.
|
|
324
|
+
logger.debug('Connecting McpServer to new transport...', basePostContext);
|
|
325
|
+
await server.connect(transport);
|
|
326
|
+
logger.debug('McpServer connected to transport.', basePostContext);
|
|
327
|
+
// NOTE: SDK's connect/handleRequest handles sending the InitializeResult.
|
|
328
|
+
}
|
|
329
|
+
else if (!transport) {
|
|
330
|
+
// --- Handle Non-Initialize Request without Valid Session ---
|
|
331
|
+
// MCP Spec: Server SHOULD respond 400/404 if session ID is missing/invalid for non-init requests.
|
|
332
|
+
logger.warning('Invalid or missing session ID for non-initialize POST request.', { ...basePostContext, sessionId });
|
|
333
|
+
res.status(404).json({ jsonrpc: '2.0', error: { code: -32004, message: 'Invalid or expired session ID' }, id: requestId });
|
|
334
|
+
return; // Stop processing
|
|
335
|
+
}
|
|
336
|
+
// --- Handle Request Content (Initialize or Subsequent Message) ---
|
|
337
|
+
// Use the extracted sessionId in the context for these logs
|
|
338
|
+
const currentSessionId = transport.sessionId; // Should be defined here
|
|
339
|
+
logger.debug(`Processing POST request content for session ${currentSessionId}...`, { ...basePostContext, sessionId: currentSessionId, isInitReq });
|
|
340
|
+
// Delegate the actual handling (parsing, routing, response/SSE generation) to the SDK transport instance.
|
|
341
|
+
// The SDK transport handles returning 202 for notification/response-only POSTs internally.
|
|
342
|
+
await transport.handleRequest(req, res, req.body);
|
|
343
|
+
logger.debug(`Finished processing POST request content for session ${currentSessionId}.`, { ...basePostContext, sessionId: currentSessionId });
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
// Catch-all for errors during POST handling.
|
|
347
|
+
// Include sessionId if available in the transport object at this point
|
|
348
|
+
const errorSessionId = transport?.sessionId || sessionId; // Use extracted or from transport if available
|
|
349
|
+
logger.error('Error handling POST request', {
|
|
350
|
+
...basePostContext,
|
|
351
|
+
sessionId: errorSessionId, // Add sessionId to error context
|
|
352
|
+
isInitReq, // Include isInitReq flag
|
|
353
|
+
error: err instanceof Error ? err.message : String(err),
|
|
354
|
+
stack: err instanceof Error ? err.stack : undefined
|
|
355
|
+
});
|
|
356
|
+
if (!res.headersSent) {
|
|
357
|
+
// Send generic JSON-RPC error if possible.
|
|
358
|
+
res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error during POST handling' }, id: requestId });
|
|
359
|
+
}
|
|
360
|
+
// Ensure transport is cleaned up if an error occurred during initialization before session ID assigned.
|
|
361
|
+
if (isInitReq && transport && !transport.sessionId) {
|
|
362
|
+
logger.debug('Cleaning up transport after initialization failure.', { ...basePostContext, sessionId: errorSessionId });
|
|
363
|
+
await transport.close().catch(closeErr => logger.error('Error closing transport after init failure', { ...basePostContext, sessionId: errorSessionId, closeError: closeErr }));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
// Unified handler for GET (SSE connection) and DELETE (session termination).
|
|
368
|
+
const handleSessionReq = async (req, res) => {
|
|
369
|
+
const method = req.method; // GET or DELETE
|
|
370
|
+
// Define base context for this request
|
|
371
|
+
const baseSessionReqContext = { ...transportContext, operation: `handle${method}`, method };
|
|
372
|
+
logger.debug(`Received ${method} request on ${MCP_ENDPOINT_PATH}`, { ...baseSessionReqContext, headers: req.headers });
|
|
373
|
+
// MCP Spec: Client MUST include Mcp-Session-Id header (after init).
|
|
374
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
375
|
+
// Log extracted session ID, adding it to the context for this specific log message
|
|
376
|
+
logger.debug(`Extracted session ID: ${sessionId}`, { ...baseSessionReqContext, sessionId });
|
|
377
|
+
const transport = sessionId ? httpTransports[sessionId] : undefined;
|
|
378
|
+
// Log transport lookup result, adding sessionId to context
|
|
379
|
+
logger.debug(`Found existing transport for session ID: ${!!transport}`, { ...baseSessionReqContext, sessionId });
|
|
380
|
+
if (!transport) {
|
|
381
|
+
// MCP Spec: Server MUST respond 404 if session ID invalid/expired.
|
|
382
|
+
logger.warning(`Session not found for ${method} request`, { ...baseSessionReqContext, sessionId });
|
|
383
|
+
res.status(404).send('Session not found or expired');
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
// Use the extracted sessionId in the context for these logs
|
|
388
|
+
logger.debug(`Delegating ${method} request to transport for session ${sessionId}...`, { ...baseSessionReqContext, sessionId });
|
|
389
|
+
// MCP Spec (GET): Client MAY issue GET to open SSE stream. Server MUST respond text/event-stream or 405.
|
|
390
|
+
// MCP Spec (GET): Client SHOULD include Last-Event-ID for resumption. Resumption handling depends on SDK transport.
|
|
391
|
+
// MCP Spec (DELETE): Client SHOULD send DELETE to terminate. Server MAY respond 405 if not supported.
|
|
392
|
+
// This implementation supports DELETE via the SDK transport's handleRequest.
|
|
393
|
+
await transport.handleRequest(req, res);
|
|
394
|
+
logger.info(`Successfully handled ${method} request for session ${sessionId}`, { ...baseSessionReqContext, sessionId });
|
|
395
|
+
// Note: For DELETE, the transport's handleRequest should trigger the 'onclose' handler for cleanup.
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
// Include sessionId in error context
|
|
399
|
+
logger.error(`Error handling ${method} request for session ${sessionId}`, {
|
|
400
|
+
...baseSessionReqContext,
|
|
401
|
+
sessionId, // Add sessionId here
|
|
402
|
+
error: err instanceof Error ? err.message : String(err),
|
|
403
|
+
stack: err instanceof Error ? err.stack : undefined
|
|
404
|
+
});
|
|
405
|
+
if (!res.headersSent) {
|
|
406
|
+
// Generic error if response hasn't started (e.g., error before SSE connection).
|
|
407
|
+
res.status(500).send('Internal Server Error');
|
|
408
|
+
}
|
|
409
|
+
// The SDK transport's handleRequest should manage errors occurring *during* an SSE stream.
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
// Route GET and DELETE requests to the unified handler.
|
|
413
|
+
app.get(MCP_ENDPOINT_PATH, handleSessionReq);
|
|
414
|
+
app.delete(MCP_ENDPOINT_PATH, handleSessionReq);
|
|
415
|
+
// --- Start HTTP Server ---
|
|
416
|
+
logger.debug('Creating HTTP server instance...', transportContext);
|
|
417
|
+
const serverInstance = http.createServer(app);
|
|
418
|
+
try {
|
|
419
|
+
logger.debug('Attempting to start HTTP server with retry logic...', transportContext);
|
|
420
|
+
// Use configured host and port, with retry logic.
|
|
421
|
+
const actualPort = await startHttpServerWithRetry(serverInstance, config.mcpHttpPort, config.mcpHttpHost, MAX_PORT_RETRIES, transportContext);
|
|
422
|
+
// Determine protocol for logging (basic assumption based on HSTS possibility)
|
|
423
|
+
const protocol = config.environment === 'production' ? 'https' : 'http';
|
|
424
|
+
const serverAddress = `${protocol}://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
|
|
425
|
+
// Use console.log for prominent startup message.
|
|
426
|
+
console.log(`\n🚀 MCP Server running in HTTP mode at: ${serverAddress}\n (MCP Spec: 2025-03-26 Streamable HTTP Transport)\n`);
|
|
427
|
+
}
|
|
428
|
+
catch (err) {
|
|
429
|
+
logger.fatal('HTTP server failed to start after multiple port retries.', { ...transportContext, error: err instanceof Error ? err.message : String(err) });
|
|
430
|
+
throw err; // Propagate error to stop the application
|
|
431
|
+
}
|
|
432
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handles the setup and connection for the Stdio MCP transport.
|
|
3
|
+
* Implements the MCP Specification 2025-03-26 for stdio transport.
|
|
4
|
+
* This transport communicates directly over standard input (stdin) and
|
|
5
|
+
* standard output (stdout), typically used when the MCP server is launched
|
|
6
|
+
* as a child process by a host application.
|
|
7
|
+
*
|
|
8
|
+
* Specification Reference:
|
|
9
|
+
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#stdio
|
|
10
|
+
*
|
|
11
|
+
* --- Authentication Note ---
|
|
12
|
+
* As per the MCP Authorization Specification (2025-03-26, Section 1.2),
|
|
13
|
+
* STDIO transports SHOULD NOT implement HTTP-based authentication flows.
|
|
14
|
+
* Authorization is typically handled implicitly by the host application
|
|
15
|
+
* controlling the server process. This implementation follows that guideline.
|
|
16
|
+
*
|
|
17
|
+
* @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
|
|
18
|
+
*/
|
|
19
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
20
|
+
// Import core utilities: ErrorHandler for centralized error management and logger for logging.
|
|
21
|
+
import { ErrorHandler, logger } from '../../utils/index.js';
|
|
22
|
+
// --- Stdio Session State ---
|
|
23
|
+
// Since stdio typically involves a single, persistent connection managed by a parent,
|
|
24
|
+
// we manage a single working directory state for the entire process.
|
|
25
|
+
let currentWorkingDirectory = undefined; // Initialize as undefined
|
|
26
|
+
/**
|
|
27
|
+
* Gets the current working directory set for the stdio session.
|
|
28
|
+
* @returns {string | undefined} The current working directory path or undefined if not set.
|
|
29
|
+
*/
|
|
30
|
+
export function getStdioWorkingDirectory() {
|
|
31
|
+
return currentWorkingDirectory;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Sets the working directory for the stdio session.
|
|
35
|
+
* @param {string} dir - The new working directory path.
|
|
36
|
+
*/
|
|
37
|
+
export function setStdioWorkingDirectory(dir) {
|
|
38
|
+
currentWorkingDirectory = dir;
|
|
39
|
+
logger.info(`Stdio working directory set to: ${dir}`, { operation: 'setStdioWorkingDirectory' });
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Connects a given McpServer instance to the Stdio transport. (Asynchronous)
|
|
43
|
+
* Initializes the SDK's StdioServerTransport, which handles reading newline-delimited
|
|
44
|
+
* JSON-RPC messages from process.stdin and writing corresponding messages to process.stdout,
|
|
45
|
+
* adhering to the MCP stdio transport specification.
|
|
46
|
+
*
|
|
47
|
+
* MCP Spec Points Covered by SDK's StdioServerTransport:
|
|
48
|
+
* - Reads JSON-RPC messages (requests, notifications, responses, batches) from stdin.
|
|
49
|
+
* - Writes JSON-RPC messages to stdout.
|
|
50
|
+
* - Handles newline delimiters and ensures no embedded newlines in output messages.
|
|
51
|
+
* - Ensures only valid MCP messages are written to stdout.
|
|
52
|
+
*
|
|
53
|
+
* Note: Logging via the `logger` utility MAY result in output to stderr, which is
|
|
54
|
+
* permitted by the spec for logging purposes.
|
|
55
|
+
*
|
|
56
|
+
* @param {McpServer} server - The McpServer instance containing the core logic (tools, resources).
|
|
57
|
+
* @param {Record<string, any>} context - Logging context for correlation.
|
|
58
|
+
* @returns {Promise<void>} A promise that resolves when the connection is successfully established.
|
|
59
|
+
* @throws {Error} Throws an error if the connection fails during setup (e.g., issues connecting server to transport).
|
|
60
|
+
*/
|
|
61
|
+
export async function connectStdioTransport(server, context) {
|
|
62
|
+
// Add a specific operation name to the context for better log filtering.
|
|
63
|
+
const operationContext = { ...context, operation: 'connectStdioTransport', transportType: 'Stdio' };
|
|
64
|
+
logger.debug('Attempting to connect stdio transport...', operationContext);
|
|
65
|
+
try {
|
|
66
|
+
logger.debug('Creating StdioServerTransport instance...', operationContext);
|
|
67
|
+
// Instantiate the transport provided by the SDK for standard I/O communication.
|
|
68
|
+
// This class encapsulates the logic for reading from stdin and writing to stdout
|
|
69
|
+
// according to the MCP stdio spec.
|
|
70
|
+
const transport = new StdioServerTransport();
|
|
71
|
+
logger.debug('Connecting McpServer instance to StdioServerTransport...', operationContext);
|
|
72
|
+
// Establish the link between the server's core logic and the transport layer.
|
|
73
|
+
// This internally starts the necessary listeners on process.stdin.
|
|
74
|
+
await server.connect(transport);
|
|
75
|
+
// Log successful connection. The server is now ready to process messages via stdio.
|
|
76
|
+
logger.info('MCP Server connected and listening via stdio transport.', operationContext);
|
|
77
|
+
// Use console.log for prominent startup message visibility when run directly.
|
|
78
|
+
console.log(`\n🚀 MCP Server running in STDIO mode.\n (MCP Spec: 2025-03-26 Stdio Transport)\n`);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
// Catch and handle any critical errors during the transport connection setup.
|
|
82
|
+
// Mark as critical because the server cannot function without a connected transport.
|
|
83
|
+
ErrorHandler.handleError(err, { ...operationContext, critical: true });
|
|
84
|
+
// Rethrow the error to signal the failure to the calling code (e.g., the main server startup).
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -15,6 +15,8 @@ export var BaseErrorCode;
|
|
|
15
15
|
BaseErrorCode["CONFLICT"] = "CONFLICT";
|
|
16
16
|
/** The request failed due to invalid input parameters or data. */
|
|
17
17
|
BaseErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
|
18
|
+
/** An error occurred while parsing input data (e.g., date string, JSON). */
|
|
19
|
+
BaseErrorCode["PARSING_ERROR"] = "PARSING_ERROR";
|
|
18
20
|
/** The request was rejected because the client has exceeded rate limits. */
|
|
19
21
|
BaseErrorCode["RATE_LIMITED"] = "RATE_LIMITED";
|
|
20
22
|
/** The request timed out before a response could be generated. */
|
|
@@ -27,8 +29,6 @@ export var BaseErrorCode;
|
|
|
27
29
|
BaseErrorCode["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
|
|
28
30
|
/** An error occurred during the loading or validation of configuration data. */
|
|
29
31
|
BaseErrorCode["CONFIGURATION_ERROR"] = "CONFIGURATION_ERROR";
|
|
30
|
-
/** An error occurred related to network connectivity (e.g., DNS resolution, connection refused). */
|
|
31
|
-
BaseErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
32
32
|
})(BaseErrorCode || (BaseErrorCode = {}));
|
|
33
33
|
/**
|
|
34
34
|
* Custom error class for MCP-specific errors.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Re-export all utilities from their categorized subdirectories
|
|
2
|
+
export * from './internal/index.js';
|
|
3
|
+
export * from './parsing/index.js';
|
|
4
|
+
export * from './security/index.js';
|
|
5
|
+
export * from './metrics/index.js';
|
|
6
|
+
// It's good practice to have index.ts files in each subdirectory
|
|
7
|
+
// that export the contents of that directory.
|
|
8
|
+
// Assuming those will be created or already exist.
|
|
9
|
+
// If not, this might need adjustment to export specific files, e.g.:
|
|
10
|
+
// export * from './internal/errorHandler.js';
|
|
11
|
+
// export * from './internal/logger.js';
|
|
12
|
+
// ... etc.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { BaseErrorCode, McpError } from '
|
|
1
|
+
import { BaseErrorCode, McpError } from '../../types-global/errors.js'; // Corrected path
|
|
2
2
|
import { logger } from './logger.js';
|
|
3
|
-
import { sanitizeInputForLogging } from '
|
|
3
|
+
import { sanitizeInputForLogging } from '../index.js'; // Import from main barrel file
|
|
4
4
|
/**
|
|
5
5
|
* Simple mapper that maps error types to error codes
|
|
6
6
|
*/
|
|
@@ -115,7 +115,9 @@ export class ErrorHandler {
|
|
|
115
115
|
if (error instanceof McpError) {
|
|
116
116
|
// Add any additional context
|
|
117
117
|
if (context && Object.keys(context).length > 0) {
|
|
118
|
-
|
|
118
|
+
// Ensure details is an object before spreading
|
|
119
|
+
const existingDetails = typeof error.details === 'object' && error.details !== null ? error.details : {};
|
|
120
|
+
error.details = { ...existingDetails, ...context };
|
|
119
121
|
}
|
|
120
122
|
// Log the error with sanitized input
|
|
121
123
|
logger.error(`Error ${operation}: ${error.message}`, {
|
|
@@ -129,13 +131,14 @@ export class ErrorHandler {
|
|
|
129
131
|
if (rethrow) {
|
|
130
132
|
throw error;
|
|
131
133
|
}
|
|
134
|
+
// Ensure the function returns an Error type
|
|
132
135
|
return error;
|
|
133
136
|
}
|
|
134
137
|
// Sanitize input for logging
|
|
135
138
|
const sanitizedInput = input ? sanitizeInputForLogging(input) : undefined;
|
|
136
139
|
// Log the error with consistent format
|
|
137
140
|
logger.error(`Error ${operation}`, {
|
|
138
|
-
error: error
|
|
141
|
+
error: getErrorMessage(error), // Use helper function
|
|
139
142
|
errorType: getErrorName(error),
|
|
140
143
|
input: sanitizedInput,
|
|
141
144
|
requestId: context?.requestId,
|
|
@@ -148,16 +151,22 @@ export class ErrorHandler {
|
|
|
148
151
|
ErrorHandler.determineErrorCode(error) ||
|
|
149
152
|
BaseErrorCode.INTERNAL_ERROR;
|
|
150
153
|
// Transform to appropriate error type
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
let transformedError;
|
|
155
|
+
if (options.errorMapper) {
|
|
156
|
+
transformedError = options.errorMapper(error);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
transformedError = new McpError(errorCode, `Error ${operation}: ${getErrorMessage(error)}`, // Use helper function
|
|
160
|
+
{
|
|
154
161
|
originalError: getErrorName(error),
|
|
155
162
|
...context
|
|
156
163
|
});
|
|
164
|
+
}
|
|
157
165
|
// Rethrow if requested
|
|
158
166
|
if (rethrow) {
|
|
159
167
|
throw transformedError;
|
|
160
168
|
}
|
|
169
|
+
// Ensure the function returns an Error type
|
|
161
170
|
return transformedError;
|
|
162
171
|
}
|
|
163
172
|
/**
|
|
@@ -204,7 +213,8 @@ export class ErrorHandler {
|
|
|
204
213
|
return {
|
|
205
214
|
code: error.code,
|
|
206
215
|
message: error.message,
|
|
207
|
-
|
|
216
|
+
// Ensure details is an object
|
|
217
|
+
details: typeof error.details === 'object' && error.details !== null ? error.details : {}
|
|
208
218
|
};
|
|
209
219
|
}
|
|
210
220
|
if (error instanceof Error) {
|