mcp-ts-template 1.1.6
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/LICENSE +201 -0
- package/README.md +233 -0
- package/dist/config/index.d.ts +73 -0
- package/dist/config/index.js +125 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +162 -0
- package/dist/mcp-client/client.d.ts +36 -0
- package/dist/mcp-client/client.js +276 -0
- package/dist/mcp-client/configLoader.d.ts +75 -0
- package/dist/mcp-client/configLoader.js +203 -0
- package/dist/mcp-client/index.d.ts +10 -0
- package/dist/mcp-client/index.js +14 -0
- package/dist/mcp-client/transport.d.ts +34 -0
- package/dist/mcp-client/transport.js +183 -0
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +38 -0
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +40 -0
- package/dist/mcp-server/resources/echoResource/index.d.ts +5 -0
- package/dist/mcp-server/resources/echoResource/index.js +5 -0
- package/dist/mcp-server/resources/echoResource/registration.d.ts +12 -0
- package/dist/mcp-server/resources/echoResource/registration.js +122 -0
- package/dist/mcp-server/server.d.ts +27 -0
- package/dist/mcp-server/server.js +176 -0
- package/dist/mcp-server/tools/echoTool/echoToolLogic.d.ts +68 -0
- package/dist/mcp-server/tools/echoTool/echoToolLogic.js +73 -0
- package/dist/mcp-server/tools/echoTool/index.d.ts +5 -0
- package/dist/mcp-server/tools/echoTool/index.js +5 -0
- package/dist/mcp-server/tools/echoTool/registration.d.ts +12 -0
- package/dist/mcp-server/tools/echoTool/registration.js +86 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +57 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.js +145 -0
- package/dist/mcp-server/transports/httpTransport.d.ts +23 -0
- package/dist/mcp-server/transports/httpTransport.js +411 -0
- package/dist/mcp-server/transports/stdioTransport.d.ts +40 -0
- package/dist/mcp-server/transports/stdioTransport.js +70 -0
- package/dist/types-global/errors.d.ts +73 -0
- package/dist/types-global/errors.js +66 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +12 -0
- package/dist/utils/internal/errorHandler.d.ts +90 -0
- package/dist/utils/internal/errorHandler.js +247 -0
- package/dist/utils/internal/index.d.ts +3 -0
- package/dist/utils/internal/index.js +3 -0
- package/dist/utils/internal/logger.d.ts +50 -0
- package/dist/utils/internal/logger.js +267 -0
- package/dist/utils/internal/requestContext.d.ts +47 -0
- package/dist/utils/internal/requestContext.js +48 -0
- package/dist/utils/metrics/index.d.ts +1 -0
- package/dist/utils/metrics/index.js +1 -0
- package/dist/utils/metrics/tokenCounter.d.ts +27 -0
- package/dist/utils/metrics/tokenCounter.js +124 -0
- package/dist/utils/parsing/dateParser.d.ts +27 -0
- package/dist/utils/parsing/dateParser.js +62 -0
- package/dist/utils/parsing/index.d.ts +2 -0
- package/dist/utils/parsing/index.js +2 -0
- package/dist/utils/parsing/jsonParser.d.ts +46 -0
- package/dist/utils/parsing/jsonParser.js +79 -0
- package/dist/utils/security/idGenerator.d.ts +93 -0
- package/dist/utils/security/idGenerator.js +147 -0
- package/dist/utils/security/index.d.ts +3 -0
- package/dist/utils/security/index.js +3 -0
- package/dist/utils/security/rateLimiter.d.ts +92 -0
- package/dist/utils/security/rateLimiter.js +171 -0
- package/dist/utils/security/sanitization.d.ts +180 -0
- package/dist/utils/security/sanitization.js +372 -0
- package/package.json +79 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Imports MUST be at the top level
|
|
3
|
+
import { logger } from "./utils/internal/logger.js"; // Import logger instance early
|
|
4
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
import { config, environment } from "./config/index.js"; // This loads .env via dotenv.config()
|
|
6
|
+
import { initializeAndStartServer } from "./mcp-server/server.js";
|
|
7
|
+
import { requestContextService } from "./utils/index.js";
|
|
8
|
+
/**
|
|
9
|
+
* The main MCP server instance.
|
|
10
|
+
* @type {McpServer | undefined}
|
|
11
|
+
*/
|
|
12
|
+
let server;
|
|
13
|
+
/**
|
|
14
|
+
* Gracefully shuts down the main MCP server.
|
|
15
|
+
* Handles process termination signals (SIGTERM, SIGINT) and critical errors.
|
|
16
|
+
*
|
|
17
|
+
* @param signal - The signal or event name that triggered the shutdown (e.g., "SIGTERM", "uncaughtException").
|
|
18
|
+
*/
|
|
19
|
+
const shutdown = async (signal) => {
|
|
20
|
+
// Define context for the shutdown operation
|
|
21
|
+
const shutdownContext = {
|
|
22
|
+
operation: 'Shutdown',
|
|
23
|
+
signal,
|
|
24
|
+
};
|
|
25
|
+
logger.info(`Received ${signal}. Starting graceful shutdown...`, shutdownContext);
|
|
26
|
+
try {
|
|
27
|
+
// Close the main MCP server
|
|
28
|
+
if (server) {
|
|
29
|
+
logger.info("Closing main MCP server...", shutdownContext);
|
|
30
|
+
await server.close();
|
|
31
|
+
logger.info("Main MCP server closed successfully", shutdownContext);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
logger.warning("Server instance not found during shutdown.", shutdownContext);
|
|
35
|
+
}
|
|
36
|
+
logger.info("Graceful shutdown completed successfully", shutdownContext);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
// Handle any errors during shutdown
|
|
41
|
+
logger.error("Critical error during shutdown", {
|
|
42
|
+
...shutdownContext,
|
|
43
|
+
error: error instanceof Error ? error.message : String(error),
|
|
44
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
45
|
+
});
|
|
46
|
+
process.exit(1); // Exit with error code if shutdown fails
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Initializes and starts the main MCP server.
|
|
51
|
+
* Sets up request context, initializes the server instance, starts the transport,
|
|
52
|
+
* and registers signal handlers for graceful shutdown and error handling.
|
|
53
|
+
*/
|
|
54
|
+
const start = async () => {
|
|
55
|
+
// --- Logger Initialization (Moved here AFTER config/dotenv is loaded) ---
|
|
56
|
+
const validMcpLogLevels = ['debug', 'info', 'notice', 'warning', 'error', 'crit', 'alert', 'emerg'];
|
|
57
|
+
// Read level from config (which read from env var or default)
|
|
58
|
+
const initialLogLevelConfig = config.logLevel;
|
|
59
|
+
// Validate the configured log level
|
|
60
|
+
let validatedMcpLogLevel = 'info'; // Default to 'info'
|
|
61
|
+
if (validMcpLogLevels.includes(initialLogLevelConfig)) {
|
|
62
|
+
validatedMcpLogLevel = initialLogLevelConfig;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Use console.warn here as logger isn't initialized yet, only if TTY
|
|
66
|
+
if (process.stdout.isTTY) {
|
|
67
|
+
console.warn(`Invalid MCP_LOG_LEVEL "${initialLogLevelConfig}" provided via config/env. Defaulting to "info".`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Initialize the logger with the validated MCP level and wait for it to complete.
|
|
71
|
+
await logger.initialize(validatedMcpLogLevel);
|
|
72
|
+
// Log initialization message using the logger itself (will go to file/console)
|
|
73
|
+
logger.info(`Logger initialized by start(). MCP logging level: ${validatedMcpLogLevel}`);
|
|
74
|
+
// --- End Logger Initialization ---
|
|
75
|
+
// Log that config is loaded (this was previously done earlier)
|
|
76
|
+
logger.debug("Configuration loaded successfully", { config });
|
|
77
|
+
// Create application-level request context using the service instance
|
|
78
|
+
// Use the validated transport type from the config object
|
|
79
|
+
const transportType = config.mcpTransportType;
|
|
80
|
+
const startupContext = requestContextService.createRequestContext({
|
|
81
|
+
operation: `ServerStartup_${transportType}`, // Include transport in operation name
|
|
82
|
+
appName: config.mcpServerName,
|
|
83
|
+
appVersion: config.mcpServerVersion,
|
|
84
|
+
environment: environment
|
|
85
|
+
});
|
|
86
|
+
logger.info(`Starting ${config.mcpServerName} v${config.mcpServerVersion} (Transport: ${transportType})...`, startupContext);
|
|
87
|
+
try {
|
|
88
|
+
// Initialize the server instance and start the selected transport
|
|
89
|
+
logger.debug("Initializing and starting MCP server transport", startupContext);
|
|
90
|
+
// Start the server transport. For stdio, this returns the server instance.
|
|
91
|
+
// For http, it sets up the listener and returns void (or potentially the http.Server).
|
|
92
|
+
// We only need to store the instance for stdio shutdown.
|
|
93
|
+
const potentialServerInstance = await initializeAndStartServer();
|
|
94
|
+
if (transportType === 'stdio' && potentialServerInstance instanceof McpServer) {
|
|
95
|
+
server = potentialServerInstance; // Store only for stdio
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// For HTTP, server instances are managed per-session in server.ts
|
|
99
|
+
// The main http server listener keeps the process alive.
|
|
100
|
+
// Shutdown for HTTP needs to handle closing the main http server.
|
|
101
|
+
// We might need to return the httpServer from initializeAndStartServer if
|
|
102
|
+
// we want to close it explicitly here during shutdown.
|
|
103
|
+
// For now, we don't store anything globally for HTTP transport.
|
|
104
|
+
}
|
|
105
|
+
// If initializeAndStartServer failed, it would have thrown an error,
|
|
106
|
+
// and execution would jump to the outer catch block.
|
|
107
|
+
logger.info(`${config.mcpServerName} is running with ${transportType} transport`, {
|
|
108
|
+
...startupContext,
|
|
109
|
+
startTime: new Date().toISOString(),
|
|
110
|
+
});
|
|
111
|
+
// --- Signal and Error Handling Setup ---
|
|
112
|
+
// Handle process signals for graceful shutdown
|
|
113
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
114
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
115
|
+
// Handle uncaught exceptions
|
|
116
|
+
process.on("uncaughtException", async (error) => {
|
|
117
|
+
const errorContext = {
|
|
118
|
+
...startupContext, // Include base context for correlation
|
|
119
|
+
event: 'uncaughtException',
|
|
120
|
+
error: error instanceof Error ? error.message : String(error),
|
|
121
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
122
|
+
};
|
|
123
|
+
logger.error("Uncaught exception detected. Initiating shutdown...", errorContext);
|
|
124
|
+
// Attempt graceful shutdown; shutdown() handles its own errors.
|
|
125
|
+
await shutdown("uncaughtException");
|
|
126
|
+
// If shutdown fails internally, it will call process.exit(1).
|
|
127
|
+
// If shutdown succeeds, it calls process.exit(0).
|
|
128
|
+
// If shutdown itself throws unexpectedly *before* exiting, this process might terminate abruptly,
|
|
129
|
+
// but the core shutdown logic is handled within shutdown().
|
|
130
|
+
});
|
|
131
|
+
// Handle unhandled promise rejections
|
|
132
|
+
process.on("unhandledRejection", async (reason) => {
|
|
133
|
+
const rejectionContext = {
|
|
134
|
+
...startupContext, // Include base context for correlation
|
|
135
|
+
event: 'unhandledRejection',
|
|
136
|
+
reason: reason instanceof Error ? reason.message : String(reason),
|
|
137
|
+
stack: reason instanceof Error ? reason.stack : undefined
|
|
138
|
+
};
|
|
139
|
+
logger.error("Unhandled promise rejection detected. Initiating shutdown...", rejectionContext);
|
|
140
|
+
// Attempt graceful shutdown; shutdown() handles its own errors.
|
|
141
|
+
await shutdown("unhandledRejection");
|
|
142
|
+
// Similar logic as uncaughtException: shutdown handles its exit codes.
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
// Handle critical startup errors (already logged by ErrorHandler or caught above)
|
|
147
|
+
// Log the final failure context, including error details, before exiting
|
|
148
|
+
logger.error("Critical error during startup, exiting.", {
|
|
149
|
+
...startupContext,
|
|
150
|
+
finalErrorContext: 'Startup Failure',
|
|
151
|
+
error: error instanceof Error ? error.message : String(error),
|
|
152
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
153
|
+
});
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
// --- Async IIFE to allow top-level await ---
|
|
158
|
+
// This remains necessary because start() is async
|
|
159
|
+
(async () => {
|
|
160
|
+
// Start the application
|
|
161
|
+
await start();
|
|
162
|
+
})(); // End async IIFE
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { McpError } from "../types-global/errors.js";
|
|
3
|
+
import { RequestContext } from "../utils/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Represents a successfully connected MCP Client instance.
|
|
6
|
+
* This is an alias for the SDK's Client class for improved readability.
|
|
7
|
+
*/
|
|
8
|
+
export type ConnectedMcpClient = Client;
|
|
9
|
+
/**
|
|
10
|
+
* Creates, connects, and returns an MCP client instance for a specified server.
|
|
11
|
+
* Implements caching: If a client for the server is already connected, it returns the existing instance.
|
|
12
|
+
* Validates server configuration before attempting connection.
|
|
13
|
+
* Follows the MCP 2025-03-26 specification for client initialization and capabilities.
|
|
14
|
+
*
|
|
15
|
+
* @param serverName - The name of the MCP server to connect to (must exist in mcp-config.json).
|
|
16
|
+
* @param parentContext - Optional parent request context for logging and tracing.
|
|
17
|
+
* @returns A promise resolving to the connected Client instance.
|
|
18
|
+
* @throws McpError if configuration is missing/invalid, transport fails, or connection/initialization fails.
|
|
19
|
+
*/
|
|
20
|
+
export declare function connectMcpClient(serverName: string, parentContext?: RequestContext | null): Promise<ConnectedMcpClient>;
|
|
21
|
+
/**
|
|
22
|
+
* Disconnects a specific MCP client, closes its transport, and removes it from the cache.
|
|
23
|
+
* Handles potential errors during the close operation.
|
|
24
|
+
*
|
|
25
|
+
* @param serverName - The name of the server whose client should be disconnected.
|
|
26
|
+
* @param parentContext - Optional parent request context for logging.
|
|
27
|
+
* @param error - Optional error that triggered the disconnect (used for logging context).
|
|
28
|
+
*/
|
|
29
|
+
export declare function disconnectMcpClient(serverName: string, parentContext?: RequestContext | null, error?: Error | McpError): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* Disconnects all currently connected MCP clients.
|
|
32
|
+
* Useful for graceful application shutdown.
|
|
33
|
+
*
|
|
34
|
+
* @param parentContext - Optional parent request context for logging.
|
|
35
|
+
*/
|
|
36
|
+
export declare function disconnectAllMcpClients(parentContext?: RequestContext | null): Promise<void>;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { BaseErrorCode } from "../types-global/errors.js";
|
|
3
|
+
// Import utils from the main barrel file (ErrorHandler, logger, RequestContext, requestContextService from ../utils/internal/*)
|
|
4
|
+
import { ErrorHandler, logger, requestContextService, } from "../utils/index.js";
|
|
5
|
+
import { getClientTransport } from "./transport.js";
|
|
6
|
+
// Import config loader for early validation
|
|
7
|
+
import { getMcpServerConfig } from "./configLoader.js";
|
|
8
|
+
/**
|
|
9
|
+
* In-memory cache for active MCP client connections.
|
|
10
|
+
* Maps server names (from config) to their connected Client instances.
|
|
11
|
+
* This prevents redundant connections to the same server.
|
|
12
|
+
*/
|
|
13
|
+
const connectedClients = new Map();
|
|
14
|
+
/**
|
|
15
|
+
* Creates, connects, and returns an MCP client instance for a specified server.
|
|
16
|
+
* Implements caching: If a client for the server is already connected, it returns the existing instance.
|
|
17
|
+
* Validates server configuration before attempting connection.
|
|
18
|
+
* Follows the MCP 2025-03-26 specification for client initialization and capabilities.
|
|
19
|
+
*
|
|
20
|
+
* @param serverName - The name of the MCP server to connect to (must exist in mcp-config.json).
|
|
21
|
+
* @param parentContext - Optional parent request context for logging and tracing.
|
|
22
|
+
* @returns A promise resolving to the connected Client instance.
|
|
23
|
+
* @throws McpError if configuration is missing/invalid, transport fails, or connection/initialization fails.
|
|
24
|
+
*/
|
|
25
|
+
export async function connectMcpClient(serverName, parentContext) {
|
|
26
|
+
const operationContext = requestContextService.createRequestContext({
|
|
27
|
+
...(parentContext ?? {}),
|
|
28
|
+
operation: "connectMcpClient",
|
|
29
|
+
targetServer: serverName,
|
|
30
|
+
});
|
|
31
|
+
// --- Check Cache ---
|
|
32
|
+
if (connectedClients.has(serverName)) {
|
|
33
|
+
logger.debug(`Returning existing connected client for server: ${serverName}`, operationContext);
|
|
34
|
+
return connectedClients.get(serverName);
|
|
35
|
+
}
|
|
36
|
+
logger.info(`Attempting to connect to MCP server: ${serverName}`, operationContext);
|
|
37
|
+
return await ErrorHandler.tryCatch(async () => {
|
|
38
|
+
// --- 1. Validate Server Configuration ---
|
|
39
|
+
// Ensure the server is defined in the configuration file before proceeding.
|
|
40
|
+
// This prevents trying to create transports for non-existent servers.
|
|
41
|
+
logger.debug(`Validating configuration for server: ${serverName}`, operationContext);
|
|
42
|
+
// getMcpServerConfig throws McpError if the serverName is not found.
|
|
43
|
+
getMcpServerConfig(serverName, operationContext);
|
|
44
|
+
logger.debug(`Configuration validated for server: ${serverName}`, operationContext);
|
|
45
|
+
// --- 2. Define Client Identity & Capabilities (MCP Spec 2025-03-26) ---
|
|
46
|
+
// The client MUST identify itself during the 'initialize' handshake.
|
|
47
|
+
// Client identity details:
|
|
48
|
+
const clientIdentity = {
|
|
49
|
+
name: `mcp-ts-template-client-for-${serverName}`,
|
|
50
|
+
version: "1.0.0", // Use actual client version if available
|
|
51
|
+
// Optional: Add other client info like supportedProtocolVersions if needed
|
|
52
|
+
// supportedProtocolVersions: ['2025-03-26']
|
|
53
|
+
};
|
|
54
|
+
// The client MUST declare its capabilities during 'initialize'.
|
|
55
|
+
// This informs the server about what features the client supports.
|
|
56
|
+
const clientCapabilities = {
|
|
57
|
+
// Resources Capability: Ability to interact with server-provided data sources.
|
|
58
|
+
resources: {
|
|
59
|
+
list: true, // Client can request a list of available resources (`resources/list`).
|
|
60
|
+
read: true, // Client can request the content of a resource (`resources/read`).
|
|
61
|
+
templates: {
|
|
62
|
+
list: true, // Client can request a list of resource templates (`resources/templates/list`).
|
|
63
|
+
},
|
|
64
|
+
// Optional resource features:
|
|
65
|
+
// subscribe: true, // Client supports subscribing to resource updates (`resources/subscribe`, `resources/unsubscribe`, `notifications/resources/updated`).
|
|
66
|
+
// listChanged: true, // Client supports receiving notifications when the list of available resources changes (`notifications/resources/list_changed`).
|
|
67
|
+
},
|
|
68
|
+
// Tools Capability: Ability to interact with server-provided executable functions.
|
|
69
|
+
tools: {
|
|
70
|
+
list: true, // Client can request a list of available tools (`tools/list`).
|
|
71
|
+
call: true, // Client can request the execution of a tool (`tools/call`).
|
|
72
|
+
// Optional tool features:
|
|
73
|
+
// listChanged: true, // Client supports receiving notifications when the list of available tools changes (`notifications/tools/list_changed`).
|
|
74
|
+
},
|
|
75
|
+
// Prompts Capability: Ability to interact with server-provided prompt templates.
|
|
76
|
+
prompts: {
|
|
77
|
+
list: true, // Client can request a list of available prompts (`prompts/list`).
|
|
78
|
+
get: true, // Client can request the content of a specific prompt (`prompts/get`).
|
|
79
|
+
// Optional prompt features:
|
|
80
|
+
// listChanged: true, // Client supports receiving notifications when the list of available prompts changes (`notifications/prompts/list_changed`).
|
|
81
|
+
},
|
|
82
|
+
// Logging Capability: Ability to interact with server-side logging.
|
|
83
|
+
logging: {
|
|
84
|
+
// Client can request the server to adjust its logging level (`logging/setLevel`).
|
|
85
|
+
// Note: The client *receives* log messages via `notifications/message` if the *server* declares the logging capability.
|
|
86
|
+
// This flag indicates the client can *send* the setLevel request.
|
|
87
|
+
setLevel: true,
|
|
88
|
+
},
|
|
89
|
+
// Roots Capability: Ability to handle filesystem root information from the server.
|
|
90
|
+
roots: {
|
|
91
|
+
// Client supports receiving notifications when the server's accessible filesystem roots change (`notifications/roots/list_changed`).
|
|
92
|
+
// The initial list of roots is provided by the server in the 'initialize' response.
|
|
93
|
+
listChanged: true,
|
|
94
|
+
},
|
|
95
|
+
// Other Standard Capabilities (Uncomment and set to true if supported):
|
|
96
|
+
// sampling: { createMessage: true }, // Client can request the server to generate text via an LLM (`sampling/createMessage`). Requires server support.
|
|
97
|
+
// completions: { complete: true }, // Client can request simple text completions from the server (`completion/complete`). Requires server support.
|
|
98
|
+
// configuration: { get: true, set: true }, // Client can get/set server configuration (`configuration/get`, `configuration/set`). Requires server support.
|
|
99
|
+
// ping: true, // Client supports the basic ping request for connectivity checks. (Implicitly supported by SDK Client)
|
|
100
|
+
// cancellation: true, // Client supports sending cancellation notifications (`notifications/cancelled`). (Implicitly supported by SDK Client)
|
|
101
|
+
// progress: true, // Client supports receiving progress notifications (`notifications/progress`). (Implicitly supported by SDK Client)
|
|
102
|
+
};
|
|
103
|
+
logger.debug("Client identity and capabilities defined", {
|
|
104
|
+
...operationContext,
|
|
105
|
+
identity: clientIdentity,
|
|
106
|
+
capabilities: clientCapabilities, // Consider logging selectively for brevity if needed
|
|
107
|
+
});
|
|
108
|
+
// --- 3. Get Transport ---
|
|
109
|
+
// Obtain the configured transport (Stdio or HTTP) for the server.
|
|
110
|
+
// This happens *after* config validation to ensure transport details are available.
|
|
111
|
+
const transport = getClientTransport(serverName, operationContext);
|
|
112
|
+
// --- 4. Create Client Instance ---
|
|
113
|
+
// Instantiate the high-level SDK Client. It requires identity and capabilities.
|
|
114
|
+
logger.debug(`Creating MCP Client instance for ${serverName}`, operationContext);
|
|
115
|
+
const client = new Client(clientIdentity, {
|
|
116
|
+
capabilities: clientCapabilities,
|
|
117
|
+
});
|
|
118
|
+
// --- 5. Setup Event Handlers ---
|
|
119
|
+
// Handle errors originating from the client or transport layer.
|
|
120
|
+
// Handle transport closure to clean up the connection state.
|
|
121
|
+
client.onerror = (clientError) => {
|
|
122
|
+
// Errors reported by the Client class itself (e.g., protocol violations)
|
|
123
|
+
const errorCode = clientError.code; // Attempt to get JSON-RPC error code
|
|
124
|
+
const errorData = clientError.data; // Attempt to get error data
|
|
125
|
+
logger.error(`MCP Client error for server ${serverName}`, {
|
|
126
|
+
...operationContext,
|
|
127
|
+
error: clientError.message,
|
|
128
|
+
code: errorCode,
|
|
129
|
+
data: errorData,
|
|
130
|
+
stack: clientError.stack,
|
|
131
|
+
});
|
|
132
|
+
// Trigger disconnection and cleanup
|
|
133
|
+
disconnectMcpClient(serverName, operationContext, clientError);
|
|
134
|
+
};
|
|
135
|
+
transport.onerror = (transportError) => {
|
|
136
|
+
// Errors reported by the underlying transport (e.g., process crash, network issue)
|
|
137
|
+
logger.error(`MCP Transport error for server ${serverName}`, {
|
|
138
|
+
...operationContext,
|
|
139
|
+
error: transportError.message,
|
|
140
|
+
stack: transportError.stack,
|
|
141
|
+
});
|
|
142
|
+
// Trigger disconnection and cleanup
|
|
143
|
+
disconnectMcpClient(serverName, operationContext, transportError);
|
|
144
|
+
};
|
|
145
|
+
transport.onclose = () => {
|
|
146
|
+
// Transport connection closed (gracefully or unexpectedly)
|
|
147
|
+
logger.info(`MCP Transport closed for server ${serverName}`, operationContext);
|
|
148
|
+
// Trigger disconnection and cleanup
|
|
149
|
+
disconnectMcpClient(serverName, operationContext);
|
|
150
|
+
};
|
|
151
|
+
// --- 6. Connect and Initialize ---
|
|
152
|
+
// Establish the connection and perform the MCP initialize handshake.
|
|
153
|
+
// The `client.connect()` method handles sending the `initialize` request
|
|
154
|
+
// with the defined identity and capabilities, and processing the server's response.
|
|
155
|
+
logger.info(`Connecting client to transport for ${serverName}...`, operationContext);
|
|
156
|
+
await client.connect(transport); // This promise resolves after successful initialization
|
|
157
|
+
logger.info(`Successfully connected and initialized with MCP server: ${serverName}`, operationContext);
|
|
158
|
+
// --- 7. Store Connection ---
|
|
159
|
+
// Cache the successfully connected client instance.
|
|
160
|
+
connectedClients.set(serverName, client);
|
|
161
|
+
return client;
|
|
162
|
+
}, {
|
|
163
|
+
operation: `connecting to MCP server ${serverName}`,
|
|
164
|
+
context: operationContext,
|
|
165
|
+
errorCode: BaseErrorCode.INTERNAL_ERROR, // Use INTERNAL_ERROR as fallback for connection issues
|
|
166
|
+
rethrow: true, // Rethrow the McpError for the caller to handle
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Disconnects a specific MCP client, closes its transport, and removes it from the cache.
|
|
171
|
+
* Handles potential errors during the close operation.
|
|
172
|
+
*
|
|
173
|
+
* @param serverName - The name of the server whose client should be disconnected.
|
|
174
|
+
* @param parentContext - Optional parent request context for logging.
|
|
175
|
+
* @param error - Optional error that triggered the disconnect (used for logging context).
|
|
176
|
+
*/
|
|
177
|
+
export async function disconnectMcpClient(serverName, parentContext, error) {
|
|
178
|
+
const context = requestContextService.createRequestContext({
|
|
179
|
+
...(parentContext ?? {}),
|
|
180
|
+
operation: "disconnectMcpClient",
|
|
181
|
+
targetServer: serverName,
|
|
182
|
+
triggerReason: error ? error.message : "explicit disconnect or close",
|
|
183
|
+
});
|
|
184
|
+
const client = connectedClients.get(serverName);
|
|
185
|
+
if (client) {
|
|
186
|
+
// Only remove from cache *before* attempting close if triggered by an error,
|
|
187
|
+
// otherwise remove *after* successful close or failed close attempt.
|
|
188
|
+
// This prevents race conditions where a new connection attempt might occur
|
|
189
|
+
// while the old one is still closing gracefully.
|
|
190
|
+
if (error) {
|
|
191
|
+
connectedClients.delete(serverName);
|
|
192
|
+
logger.debug(`Removed client ${serverName} from cache due to error trigger.`, context);
|
|
193
|
+
}
|
|
194
|
+
logger.info(`Disconnecting client for server: ${serverName}`, context);
|
|
195
|
+
try {
|
|
196
|
+
// Attempt graceful shutdown (sends 'shutdown' notification if supported, closes transport)
|
|
197
|
+
await client.close();
|
|
198
|
+
logger.info(`Client for ${serverName} closed successfully.`, context);
|
|
199
|
+
}
|
|
200
|
+
catch (closeError) {
|
|
201
|
+
logger.error(`Error closing client for ${serverName}`, {
|
|
202
|
+
...context,
|
|
203
|
+
error: closeError instanceof Error
|
|
204
|
+
? closeError.message
|
|
205
|
+
: String(closeError),
|
|
206
|
+
stack: closeError instanceof Error ? closeError.stack : undefined,
|
|
207
|
+
});
|
|
208
|
+
// Continue cleanup even if close fails
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
// Ensure removal from cache if not already removed due to error
|
|
212
|
+
if (connectedClients.has(serverName)) {
|
|
213
|
+
connectedClients.delete(serverName);
|
|
214
|
+
logger.debug(`Removed client ${serverName} from connection cache after close attempt.`, context);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// Only log warning if it wasn't triggered by an error (to avoid duplicate logs on error disconnect)
|
|
220
|
+
if (!error) {
|
|
221
|
+
logger.warning(`Client for server ${serverName} not found in cache or already disconnected.`, context);
|
|
222
|
+
}
|
|
223
|
+
// Defensive removal in case of inconsistent state
|
|
224
|
+
if (connectedClients.has(serverName)) {
|
|
225
|
+
connectedClients.delete(serverName);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Disconnects all currently connected MCP clients.
|
|
231
|
+
* Useful for graceful application shutdown.
|
|
232
|
+
*
|
|
233
|
+
* @param parentContext - Optional parent request context for logging.
|
|
234
|
+
*/
|
|
235
|
+
export async function disconnectAllMcpClients(parentContext) {
|
|
236
|
+
const context = requestContextService.createRequestContext({
|
|
237
|
+
...(parentContext ?? {}),
|
|
238
|
+
operation: "disconnectAllMcpClients",
|
|
239
|
+
});
|
|
240
|
+
logger.info("Disconnecting all MCP clients...", context);
|
|
241
|
+
const disconnectionPromises = [];
|
|
242
|
+
// Create a copy of keys to avoid issues while iterating and deleting
|
|
243
|
+
const serverNames = Array.from(connectedClients.keys());
|
|
244
|
+
for (const serverName of serverNames) {
|
|
245
|
+
// Pass the main context down to individual disconnect calls
|
|
246
|
+
disconnectionPromises.push(disconnectMcpClient(serverName, context));
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
// Wait for all disconnection attempts to complete
|
|
250
|
+
await Promise.all(disconnectionPromises);
|
|
251
|
+
logger.info("All MCP clients disconnected.", context);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
// Log if any disconnection failed, but don't necessarily halt shutdown
|
|
255
|
+
logger.error("Error during disconnection of all clients", {
|
|
256
|
+
...context,
|
|
257
|
+
error: error instanceof Error ? error.message : String(error),
|
|
258
|
+
});
|
|
259
|
+
// Decide if this should throw or just log based on application needs
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// --- Graceful Shutdown Integration ---
|
|
263
|
+
// Consider moving this logic to the main application entry point (e.g., src/index.ts)
|
|
264
|
+
// to coordinate shutdown across different parts of the application.
|
|
265
|
+
/*
|
|
266
|
+
async function gracefulShutdown(signal: string) {
|
|
267
|
+
const context = requestContextService.createRequestContext({ operation: 'gracefulShutdown', signal });
|
|
268
|
+
logger.info(`Received ${signal}. Initiating graceful shutdown...`, context);
|
|
269
|
+
await disconnectAllMcpClients(context);
|
|
270
|
+
logger.info("Graceful shutdown complete. Exiting.", context);
|
|
271
|
+
process.exit(0);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Ctrl+C
|
|
275
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); // Termination signal
|
|
276
|
+
*/
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RequestContext } from "../utils/index.js";
|
|
3
|
+
declare const McpServerConfigEntrySchema: z.ZodObject<{
|
|
4
|
+
command: z.ZodString;
|
|
5
|
+
args: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
6
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
7
|
+
transportType: z.ZodDefault<z.ZodEnum<["stdio", "http"]>>;
|
|
8
|
+
}, "strip", z.ZodTypeAny, {
|
|
9
|
+
transportType: "stdio" | "http";
|
|
10
|
+
command: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
env?: Record<string, string> | undefined;
|
|
13
|
+
}, {
|
|
14
|
+
command: string;
|
|
15
|
+
transportType?: "stdio" | "http" | undefined;
|
|
16
|
+
args?: string[] | undefined;
|
|
17
|
+
env?: Record<string, string> | undefined;
|
|
18
|
+
}>;
|
|
19
|
+
export type McpServerConfigEntry = z.infer<typeof McpServerConfigEntrySchema>;
|
|
20
|
+
declare const McpClientConfigFileSchema: z.ZodObject<{
|
|
21
|
+
mcpServers: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
22
|
+
command: z.ZodString;
|
|
23
|
+
args: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
24
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
25
|
+
transportType: z.ZodDefault<z.ZodEnum<["stdio", "http"]>>;
|
|
26
|
+
}, "strip", z.ZodTypeAny, {
|
|
27
|
+
transportType: "stdio" | "http";
|
|
28
|
+
command: string;
|
|
29
|
+
args: string[];
|
|
30
|
+
env?: Record<string, string> | undefined;
|
|
31
|
+
}, {
|
|
32
|
+
command: string;
|
|
33
|
+
transportType?: "stdio" | "http" | undefined;
|
|
34
|
+
args?: string[] | undefined;
|
|
35
|
+
env?: Record<string, string> | undefined;
|
|
36
|
+
}>>;
|
|
37
|
+
}, "strip", z.ZodTypeAny, {
|
|
38
|
+
mcpServers: Record<string, {
|
|
39
|
+
transportType: "stdio" | "http";
|
|
40
|
+
command: string;
|
|
41
|
+
args: string[];
|
|
42
|
+
env?: Record<string, string> | undefined;
|
|
43
|
+
}>;
|
|
44
|
+
}, {
|
|
45
|
+
mcpServers: Record<string, {
|
|
46
|
+
command: string;
|
|
47
|
+
transportType?: "stdio" | "http" | undefined;
|
|
48
|
+
args?: string[] | undefined;
|
|
49
|
+
env?: Record<string, string> | undefined;
|
|
50
|
+
}>;
|
|
51
|
+
}>;
|
|
52
|
+
export type McpClientConfigFile = z.infer<typeof McpClientConfigFileSchema>;
|
|
53
|
+
/**
|
|
54
|
+
* Loads, validates, and caches the MCP client configuration.
|
|
55
|
+
* It first attempts to load from `mcp-config.json`. If that file doesn't exist
|
|
56
|
+
* or fails to read, it falls back to loading `mcp-config.json.example`.
|
|
57
|
+
* The loaded content is then parsed as JSON and validated against the Zod schema.
|
|
58
|
+
*
|
|
59
|
+
* @param parentContext - Optional parent request context for logging and tracing.
|
|
60
|
+
* @returns The loaded and validated MCP server configurations object.
|
|
61
|
+
* @throws McpError if neither config file can be read, parsed, or validated successfully.
|
|
62
|
+
*/
|
|
63
|
+
export declare function loadMcpClientConfig(parentContext?: RequestContext | null): McpClientConfigFile;
|
|
64
|
+
/**
|
|
65
|
+
* Retrieves the configuration entry for a specific MCP server by its name.
|
|
66
|
+
* Ensures the main configuration is loaded (using the cached version if available)
|
|
67
|
+
* before attempting to access the specific server's details.
|
|
68
|
+
*
|
|
69
|
+
* @param serverName - The name identifier of the server (key in the 'mcpServers' map).
|
|
70
|
+
* @param parentContext - Optional parent request context for logging.
|
|
71
|
+
* @returns A copy of the configuration entry for the specified server.
|
|
72
|
+
* @throws McpError if the configuration hasn't been loaded or the server name is not found within the loaded config.
|
|
73
|
+
*/
|
|
74
|
+
export declare function getMcpServerConfig(serverName: string, parentContext?: RequestContext | null): McpServerConfigEntry;
|
|
75
|
+
export {};
|