How the Preload Script Exposes MCP APIs via contextBridge in Electron
The preload script acts as a secure intermediary that exposes curated MCP (Modular Chat Platform) APIs to the renderer process through Electron's contextBridge, enabling the web UI to invoke main process capabilities without direct Node.js access.
In the ai-ql/chat-mcp repository, the preload script plays a critical role in the Electron security model. Since renderer processes run in a sandboxed environment with restricted privileges, the preload script exposing MCP APIs via contextBridge provides the only sanctioned pathway for the frontend to communicate with MCP servers and access tools, prompts, and resources managed by the main process.
Why Electron Requires a Preload Script for MCP Access
Electron enforces a strict security boundary between the main process (full Node.js/Electron access) and the renderer process (sandboxed web environment). Direct access to ipcRenderer or Node modules from the frontend would violate security best practices and expose the application to remote code execution risks.
The preload script runs in an isolated context with elevated privileges, allowing it to import Electron modules and build a controlled API surface. By using contextBridge.exposeInMainWorld, the script selectively exposes only the necessary MCP functionality to the renderer, maintaining the principle of least privilege while enabling rich desktop integration.
How the Preload Script Works in chat-mcp
Located at src/preload/preload.ts, the preload script implements a four-stage pipeline to marshal MCP capabilities from the main process to the renderer.
Loading IPC Helpers from Electron
The script begins by importing the essential Electron modules required for inter-process communication and API exposure.
const { contextBridge, ipcRenderer } = require('electron');
This import establishes the foundation for all subsequent MCP API bridging, with ipcRenderer handling message passing and contextBridge managing secure exposure to the renderer.
Querying Registered MCP Clients
The preload script queries the main process for available MCP client configurations using a specific IPC channel. This discovery mechanism allows the script to dynamically adapt to whatever MCP servers are registered at runtime.
async function listClients(): Promise<CLIENT[]> {
return await ipcRenderer.invoke('list-clients');
}
The list-clients handler, implemented in src/main/main.ts, returns an array of client objects containing metadata about available tools, prompts, and resources for each MCP server.
Building Dynamic API Wrappers
For each client discovered, the preload script generates thin wrapper functions that forward calls to the main process. The createAPIMethods utility dynamically constructs these wrappers based on method mappings provided by the main process.
const createAPIMethods = (methods: Record<string, string>) => {
const result: Record<string, (...args: any) => Promise<any>> = {};
Object.keys(methods).forEach(key => {
const methodName = methods[key];
result[key] = (...args: any) => ipcRenderer.invoke(methodName, ...args);
});
return result;
};
This approach encapsulates all IPC plumbing, presenting a clean, promise-based API to the renderer while maintaining strict control over which main process methods are accessible.
Exposing the API via contextBridge
Once the API object is fully constructed with tools, prompts, and resources namespaces for each client, the script exposes it to the renderer through the context bridge.
contextBridge.exposeInMainWorld('mcpServers', api);
This makes the MCP API available globally as window.mcpServers in the renderer process, allowing the React frontend to interact with MCP servers without importing Electron modules or breaking sandbox security.
Accessing MCP APIs in the Renderer Process
With the preload script exposing MCP APIs via contextBridge, the renderer can access MCP functionality through the global window.mcpServers object. The TypeScript definitions below illustrate how a React component would consume these APIs.
declare global {
interface Window {
mcpServers: {
[clientName: string]: {
tools?: Record<string, (...args: any) => Promise<any>>;
prompts?: Record<string, (...args: any) => Promise<any>>;
resources?: Record<string, (...args: any) => Promise<any>>;
};
};
}
}
// Example: Invoke the "list" method on the "search" tool from the "openai" client
async function listOpenAiSearchTools() {
const result = await window.mcpServers.openai.tools?.list?.();
console.log('OpenAI search tools:', result);
}
This pattern ensures that the renderer remains sandboxed while still enabling rich integration with MCP servers managed by the main process.
Summary
- The preload script in
src/preload/preload.tsserves as the secure intermediary between Electron's main and renderer processes. - It uses
contextBridge.exposeInMainWorldto safely expose MCP APIs to the frontend without granting direct Node.js access. - The script dynamically discovers available MCP clients via
ipcRenderer.invoke('list-clients')and builds wrapper functions for each tool, prompt, and resource. - This architecture maintains sandbox security while enabling the React frontend to interact with MCP servers through the global
window.mcpServersobject.
Frequently Asked Questions
What is the contextBridge in Electron and why is it necessary?
The contextBridge is an Electron module that allows the preload script to expose specific APIs to the renderer process in a secure, controlled manner. It is necessary because the renderer process runs in a sandboxed environment without direct access to Node.js or Electron internals. Without contextBridge, the renderer cannot communicate with the main process or access desktop capabilities, making it essential for building secure Electron applications with rich functionality.
How does the preload script discover available MCP clients?
The preload script discovers MCP clients by invoking the list-clients IPC channel through ipcRenderer.invoke('list-clients'). This sends a message to the main process, which returns an array of client configurations containing metadata about available tools, prompts, and resources. The preload script then dynamically generates API wrappers for each client based on this configuration, ensuring the renderer always has access to the current set of registered MCP servers without hardcoding client-specific logic.
Can the renderer process directly call MCP servers without the preload script?
No, the renderer process cannot directly call MCP servers without the preload script. Electron's security model isolates the renderer in a sandbox that prohibits direct access to Node.js modules, network sockets, or the file system. MCP servers typically require these capabilities to function. The preload script acts as the only authorized intermediary, using contextBridge to expose carefully controlled methods that forward requests to the main process, which then handles the actual MCP server communication.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →