pocketsmith-mcp
Version:
MCP server for managing budgets via PocketSmith API
116 lines (115 loc) • 6.33 kB
JavaScript
/**
* @fileoverview Handles the detailed logic for establishing a new MCP client connection.
* This module encapsulates the steps involved in connecting to an MCP server,
* including configuration validation, client identity setup, transport acquisition,
* client instantiation, event handling, and the MCP initialization handshake.
* @module src/mcp-client/core/clientConnectionLogic
*/
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
import { logger,
// ErrorHandler will be used by the caller (clientManager)
} from "../../utils/index.js";
import { getClientTransport } from "../transports/index.js";
import { getMcpServerConfig } from "../client-config/configLoader.js";
// Client version remains hard-coded as per user instruction.
const CLIENT_VERSION = "1.0.0";
/**
* Establishes a new connection to the specified MCP server.
* This function performs the core steps of connecting, including configuration validation,
* client identity setup, transport acquisition, SDK client instantiation, event handler setup,
* and the MCP initialization handshake.
*
* IMPORTANT: This function is intended to be wrapped by a higher-level error handler
* (e.g., ErrorHandler.tryCatch in clientManager.ts) to manage exceptions.
*
* @param serverName - The unique name of the MCP server to connect to.
* @param operationContext - The request context for this connection attempt.
* @param disconnectFn - A function to call for disconnecting the client (passed from clientManager).
* @returns A promise that resolves to the connected and initialized `ConnectedMcpClient` instance.
* @throws {McpError} If server configuration is missing/invalid, transport cannot be established,
* or if the connection/MCP initialization fails.
*/
export async function establishNewMcpConnection(serverName, operationContext, disconnectFn) {
// --- Step 1: Validate Server Configuration ---
logger.debug(`Validating configuration for server: ${serverName}`, operationContext);
const serverConfig = getMcpServerConfig(serverName, operationContext);
logger.debug(`Configuration successfully validated for server: ${serverName}`, operationContext);
if (serverConfig.disabled) {
logger.warning(`Connection to server "${serverName}" aborted: Server is marked as disabled in configuration.`, operationContext);
throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, `Server "${serverName}" is disabled.`, operationContext);
}
// --- Step 2: Define Client Identity & Capabilities (MCP Spec 2025-03-26) ---
const clientIdentity = {
name: `mcp-ts-template-client-for-${serverName}`,
version: CLIENT_VERSION,
};
const clientCapabilities = {
resources: { list: true, read: true, templates: { list: true } },
tools: { list: true, call: true },
prompts: { list: true, get: true },
logging: { setLevel: true },
roots: { listChanged: true },
// ping, cancellation, progress are implicitly supported by SDK Client
};
logger.debug("Client identity and capabilities defined", {
...operationContext,
identity: clientIdentity,
capabilities: clientCapabilities,
});
// --- Step 3: Get Transport ---
const transport = getClientTransport(serverName, operationContext);
logger.debug(`Transport acquired for server ${serverName}: ${transport.constructor.name}`, operationContext);
// --- Step 4: Create Client Instance ---
const client = new Client(clientIdentity, {
capabilities: clientCapabilities,
});
// --- Step 5: Setup Event Handlers ---
// These handlers are crucial for reacting to errors and closures.
// They will call the main disconnectMcpClient from clientManager.ts for cleanup.
client.onerror = (clientError) => {
const errorCode = typeof clientError === "object" &&
clientError !== null &&
"code" in clientError
? clientError.code
: "UNKNOWN";
const errorData = typeof clientError === "object" &&
clientError !== null &&
"data" in clientError
? clientError.data
: undefined;
logger.error(`MCP SDK Client error for server ${serverName}`, {
...operationContext,
error: clientError.message,
code: errorCode,
data: errorData,
stack: clientError.stack,
});
// disconnectFn (passed from clientManager) will handle cache removal.
disconnectFn(serverName, operationContext, clientError).catch((disconnectErr) => {
logger.error(`Error during disconnect triggered by client.onerror (via disconnectFn) for ${serverName}`, { ...operationContext, disconnectError: disconnectErr });
});
};
transport.onerror = (transportError) => {
logger.error(`MCP Transport layer error for server ${serverName}`, {
...operationContext,
error: transportError.message,
stack: transportError.stack,
});
disconnectFn(serverName, operationContext, transportError).catch((disconnectErr) => {
logger.error(`Error during disconnect triggered by transport.onerror (via disconnectFn) for ${serverName}`, { ...operationContext, disconnectError: disconnectErr });
});
};
transport.onclose = () => {
logger.info(`MCP Transport closed for server ${serverName}. Initiating client disconnect.`, operationContext);
disconnectFn(serverName, operationContext).catch((disconnectErr) => {
logger.error(`Error during disconnect triggered by transport.onclose (via disconnectFn) for ${serverName}`, { ...operationContext, disconnectError: disconnectErr });
});
};
logger.debug(`Event handlers (onerror, onclose) set up for client and transport for ${serverName}`, operationContext);
// --- Step 6: Connect and Initialize ---
logger.info(`Connecting client to transport and performing MCP initialization for ${serverName}...`, operationContext);
await client.connect(transport);
logger.info(`Successfully connected and initialized with MCP server: ${serverName}`, operationContext);
return client;
}