How to Implement Custom Request Handlers for MCP Protocol Messages in chat-mcp

You can implement custom request handlers for MCP protocol messages by registering a handler function with client.setRequestHandler() in src/main/client.ts, then exposing the capability through Electron's IPC bridge to make it callable from the renderer process.

The chat-mcp repository provides an Electron-based interface for Model Context Protocol (MCP) integrations, enabling seamless communication between language models and external tools. Implementing custom request handlers allows you to extend the protocol with bespoke operations that fit your specific workflow requirements, leveraging the existing three-layer architecture that connects the MCP client to the UI.

Understanding the MCP Handler Architecture

The implementation relies on a specific flow across three core files that handle client initialization, IPC registration, and renderer exposure.

The Three-Layer Architecture

Layer 1: Client Construction (src/main/client.ts)

The initializeClient function creates the MCP Client instance using StdioClientTransport and registers default request handlers. This is where you attach custom logic using client.setRequestHandler(). The built-in sampling handler (lines 20-30) demonstrates this pattern by registering a handler for CreateMessageRequestSchema.

Layer 2: IPC Registration (src/main/main.ts)

The registerIpcHandlers function creates Electron ipcMain.handle channels for every MCP method. It forwards requests to the client via manageRequests, which acts as the bridge between Electron's IPC system and the MCP client instance. This loop (lines 48-55) automatically generates channel names in the format <serverName>-<method>.

Layer 3: Renderer Bridge (src/preload/preload.ts)

The exposeAPIs function builds an object exposing all MCP servers as window.mcpServers. Each server contains callable methods that use ipcRenderer.invoke to communicate with the registered IPC handlers, making the custom handlers accessible from frontend code as ordinary async functions.

Implementing a Custom Request Handler

Follow these steps to add a new request handler for a custom MCP protocol message.

Step 1: Define the Request Schema

Create a Zod schema that validates the incoming request structure. While optional for simple handlers, explicit typing ensures compatibility with the MCP specification.

// src/main/types.ts
import { z } from 'zod';

export const MyCustomRequestSchema = z.object({
  action: z.string(),
  payload: z.record(z.any()),
});

export const MyCustomResultSchema = z.object({
  status: z.enum(['ok', 'error']),
  data: z.record(z.any()),
});

Step 2: Register the Handler in the Client

Import your schema into src/main/client.ts and register the handler within initializeClient using client.setRequestHandler().

// src/main/client.ts (inside initializeClient)
import { MyCustomRequestSchema, MyCustomResultSchema } from './types.js';

client.setRequestHandler(MyCustomRequestSchema, async (request) => {
  console.log('Received custom MCP request:', request);

  // Implement your business logic
  const processed = await myBusinessLogic(request.payload);

  return {
    status: 'ok',
    data: processed,
  } as z.infer<typeof MyCustomResultSchema>;
});

This follows the same pattern as the built-in sampling handler at lines 20-30, attaching your function to the client instance before it connects to the transport.

Step 3: Configure the Server Capability

Add the new method to your server configuration so that registerIpcHandlers in src/main/main.ts automatically creates the IPC channel.

// src/main/config.json (example snippet)
{
  "mcpServers": {
    "myServer": {
      "command": "node my-server.js",
      "customMethod": "myServer-tools-customMethod"
    }
  }
}

The key customMethod maps to the IPC channel name format <serverName>-<method>, which the registration loop uses to create the ipcMain.handle listener.

Step 4: Invoke from the Renderer

After the preload script exposes the APIs, call your custom handler from the frontend using the window.mcpServers global.

// In a Vue component or renderer script
async function invokeCustom() {
  const result = await window.mcpServers.myServer.tools.customMethod({
    action: 'process',
    payload: { foo: 'bar' }
  });
  console.log('Custom result:', result);
}

The preload bridge converts this ordinary async function call into an ipcRenderer.invoke message, which travels through the IPC channel to your registered handler in the main process.

Key Files and Their Roles

File Purpose Key Functions
src/main/client.ts Creates the MCP client and registers request handlers initializeClient, client.setRequestHandler()
src/main/main.ts Registers Electron IPC handlers for each MCP method registerIpcHandlers, manageRequests
src/preload/preload.ts Bridges IPC to the renderer process exposeAPIs, window.mcpServers

These files form the complete pathway from custom MCP request handling in the main process to frontend invocation in the renderer.

Summary

  • Register handlers using client.setRequestHandler() in src/main/client.ts to process custom MCP protocol messages according to your business logic.
  • Define schemas with Zod to ensure type safety and validation for incoming requests and outgoing responses.
  • Expose capabilities through the automatic IPC registration in src/main/main.ts by adding methods to your server configuration.
  • Invoke from UI using the window.mcpServers API exposed by src/preload/preload.ts, treating MCP methods as ordinary async functions.

Frequently Asked Questions

What is the MCP protocol used for in chat-mcp?

The Model Context Protocol (MCP) in chat-mcp enables standardized communication between the Electron application and language model servers. It allows the UI to request context, tools, and sampling capabilities from external MCP-compatible servers through a typed request-response pattern.

Where should I register a custom request handler?

You should register custom request handlers inside the initializeClient function in src/main/client.ts. This ensures the handler is attached to the client instance immediately after creation but before the transport connection is established, following the same pattern used for the built-in CreateMessageRequestSchema handler.

How does the frontend communicate with custom MCP handlers?

The frontend communicates through the window.mcpServers global object exposed by the preload script. When you call a method like window.mcpServers.myServer.tools.customMethod(), the preload script uses ipcRenderer.invoke to send the request to the main process, where manageRequests in src/main/main.ts forwards it to the registered handler.

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 →