Managing Multiple Concurrent MCP Clients in Chat-MCP: A Complete Guide

The Chat-MCP Electron application manages multiple concurrent MCP clients by reading a JSON configuration, initializing all clients in parallel with timeout protection, and registering capability-aware IPC handlers that isolate each server's namespace.

The ai-ql/chat-mcp repository demonstrates a robust architecture for handling multiple concurrent MCP clients within an Electron application. By leveraging configuration-driven discovery and parallel async initialization, the application can connect to an arbitrary number of MCP servers at startup while preventing individual server failures from blocking the entire system.

Configuration-Driven Client Discovery

Reading the Server Configuration

The application begins by loading server definitions from a config.json file. The readConfig function in src/main/main.ts (lines 44-52) safely parses this file and returns a map of server names to their ServerConfig objects.

If parsing fails or the file is missing, the function returns null, triggering a "NO clients initialized" notification and preventing the application from starting with an invalid configuration.

Validating Config Structure

The expected configuration structure follows this pattern:

{
  "mcpServers": {
    "dev-server": { "host": "127.0.0.1", "port": 8000 },
    "staging-srv": { "host": "staging.example.com", "port": 8001 }
  }
}

Each entry under mcpServers becomes a distinct client instance with isolated transport connections and capability namespaces.

Parallel Initialization with Timeout Protection

The initClient Function

The initClient function (lines 60-78 in src/main/main.ts) orchestrates the concurrent startup of all configured MCP clients. It uses Promise.all over Object.entries(config.mcpServers) to launch initialization for every server simultaneously rather than sequentially.

This parallel approach ensures that network latency or slow server responses do not compound linearly, keeping application startup time minimal regardless of how many concurrent MCP clients are configured.

Per-Client Timeout Handling

Each client initialization is wrapped in Promise.race against a 30-second timeout promise. If a server fails to respond within this window, the specific client fails without blocking other initializations.

On success, initClient returns an array of objects containing { name, client, capabilities } for each successfully connected server, which the application then uses to register IPC handlers.

Client Bootstrap and Transport Setup

initializeClient Implementation

The initializeClient function in src/main/client.ts (lines 4-18) handles the low-level client creation for each server configuration:

export async function initializeClient(name: string, config: ServerConfig) {
  const transport = new StdioClientTransport(config);
  const client = new Client({ name: `${name}-client` });
  
  await client.connect(transport);
  
  // Register default request handler for sampling
  client.setRequestHandler(CreateMessageRequestSchema, async (request) => {
    // Handle sampling requests
  });
  
  return client;
}

StdioClientTransport Configuration

The StdioClientTransport establishes the actual connection to the MCP server using the host and port specified in the configuration. Each client receives a unique generated name (${name}-client) to prevent collisions in logging and debugging contexts.

The transport layer handles the underlying protocol details while the Client instance manages the MCP session state and capability negotiation.

Capability-Aware IPC Namespacing

Registering Isolated Handlers

After initialization, registerIpcHandlers creates isolated IPC channels for each concurrent MCP client. This function builds a feature object containing the client's name and a set of IPC channels mapped to the server's advertised capabilities (tools, prompts, resources).

For every capability reported by the server, a unique IPC handler is registered under the pattern ${serverName}-${method}. For example, a server named "prod-srv" would register handlers like prod-srv-tools/list and prod-srv-tools/call.

This namespacing design prevents method collisions between different MCP servers and allows the renderer process to address each server explicitly.

The manageRequests Bridge

The manageRequests function serves as the bridge between IPC handlers and the actual MCP client instances. When an IPC handler receives a request, it forwards to manageRequests, which invokes client.request with the proper schema for that capability.

Because each request handler is a simple async wrapper, many requests to different servers can be processed in parallel. The underlying Client implementation from @ai-ql/mcp handles its own request queue and response matching internally.

Error Handling and Graceful Degradation

If any client fails to initialize—whether due to timeout, transport error, or invalid configuration—the application aborts startup with a notification and process.exit(1). This strict failure mode ensures the UI never runs with a partially-configured set of servers, which could otherwise produce ambiguous runtime errors.

The initialization error handling in src/main/main.ts (lines 88-98) catches initialization failures and displays them to the user before terminating, preventing the application from entering an inconsistent state.

Code Examples

Sample Configuration for Multiple Servers

{
  "mcpServers": {
    "dev-server": { "host": "127.0.0.1", "port": 8000 },
    "staging-srv": { "host": "staging.example.com", "port": 8001 },
    "prod-srv": { "host": "prod.example.com", "port": 8002 }
  }
}

Invoking Server-Specific Tools from the Renderer

// renderer.js (via preload bridge)
async function listTools(serverName) {
  // IPC channel follows pattern "<server>-tools/list"
  const channel = `${serverName}-tools/list`;
  const result = await window.electron.invoke(channel);
  return result; // => { tools: [...] }
}

// Usage example
listTools('prod-srv').then(tools => console.log('Production tools:', tools));

Extending Capabilities with Custom Schemas

// In src/main/main.ts – extend registerIpcHandlers
const customCapabilitySchemas = {
  analytics: {
    report: MyAnalyticsReportSchema,
  },
};

for (const [type, actions] of Object.entries(customCapabilitySchemas)) {
  if (capabilities?.[type]) {
    feature[type] = {};
    for (const [action, schema] of Object.entries(actions)) {
      feature[type][action] = registerHandler(`${type}/${action}`, schema);
    }
  }
}

Key Implementation Files

File Role Link
src/main/main.ts Application entry-point; reads config, initializes all MCP clients, registers IPC channels main.ts
src/main/client.ts Helper that creates a Client, connects the transport, and installs a default request handler client.ts
src/main/types.ts Type definitions for Client, ServerConfig, and all request/response schemas used by the IPC bridge types.ts
src/preload/preload.ts Exposes a safe IPC bridge (window.electron.invoke) to the renderer process preload.ts
src/renderer/index.html UI that consumes the IPC calls (e.g., listing tools, prompts, resources) index.html

Summary

  • Configuration-driven architecture: The application discovers servers through a JSON config file, making it trivial to add or remove concurrent MCP clients without code changes.
  • Parallel initialization: Promise.all with per-client timeouts ensures fast startup and prevents misbehaving servers from blocking the application.
  • Isolated IPC namespaces: Each server registers handlers under ${serverName}-${method}, eliminating collisions and enabling explicit targeting of specific clients.
  • Strict failure handling: The application exits immediately if any client fails to initialize, preventing partial configurations that could cause runtime ambiguity.
  • Runtime concurrency: Stateless async handlers allow the renderer process to issue parallel requests to multiple servers, with the underlying MCP client managing internal request queues.

Frequently Asked Questions

How does the application prevent one slow MCP server from delaying startup?

The initClient function in src/main/main.ts wraps each client initialization in Promise.race against a 30-second timeout. This ensures that a single unresponsive server fails individually while Promise.all allows other servers to continue initializing in parallel.

Can the renderer process communicate with multiple MCP servers simultaneously?

Yes. The IPC architecture registers unique channels for each server using the pattern ${serverName}-${method}. The renderer can invoke these channels independently, and because each handler is an async wrapper around client.request, requests to different servers execute concurrently without blocking each other.

What happens if one MCP server fails to initialize?

The application implements strict failure handling: if any client throws an error or times out during initClient, the catch block in src/main/main.ts (lines 88-98) displays a notification and calls process.exit(1). This prevents the UI from running with a partially configured client set, which could lead to ambiguous runtime errors.

How are naming collisions between different MCP servers avoided?

Each server registers its capabilities under a unique namespace derived from its configuration name. For example, a server named "prod-srv" registers handlers like prod-srv-tools/list and prod-srv-tools/call. This ${serverName}-${method} pattern ensures that methods from different servers never collide, allowing the renderer to explicitly target specific clients.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →