Skip to main content

Worker Thread

mcp.worker.ts is an independent Worker thread providing a bridge layer to manage MCP servers through the claude CLI. It complements McpService (SDK direct connection approach) by supporting reading MCP server records from Claude configuration files and performing add/remove/test operations via CLI.

File path: packages/desktop/app/main/workers/mcp.worker.ts

Architecture Diagram

graph LR
subgraph Main["Main Process"]
Router["McpRouter / Caller"]
end

subgraph Worker["Worker Thread"]
Port["MessagePort"]
McpWorkerClass["McpWorker"]
end

subgraph External["External"]
ClaudeCLI["claude CLI"]
ConfigFile["~/.claude/config.json"]
end

Router -->|postMessage| Port
Port -->|message event| McpWorkerClass
McpWorkerClass -->|spawn| ClaudeCLI
McpWorkerClass -->|readFile| ConfigFile
McpWorkerClass -->|postMessage| Port
Port -->|result/error| Router

Communication Protocol

Request Format

type WorkerAction =
| { id: number; action: 'list' }
| { id: number; action: 'add'; payload: McpServerInput }
| { id: number; action: 'addJson'; payload: McpServerJsonInput }
| { id: number; action: 'remove'; payload: McpServerRemoveInput }
| { id: number; action: 'test'; payload: McpServerRemoveInput }
| { id: number; action: 'discover'; payload: McpServerRemoveInput };

Response Format

type WorkerResponse =
| { id: number; result: unknown }
| { id: number; error: string };

Each request is paired with a response through the id field.

Supported Operations

ActionMethodCLI CommandDescription
listlist()None (read file directly)Read server list from config.json
addadd()claude mcp addAdd server
addJsonaddJson()claude mcp add-jsonBatch add from JSON
removeremove()claude mcp removeRemove server
testtest()claude mcp testTest connection
discoverdiscover()claude mcp inspectDiscover tools/resources/prompts

list — Configuration File Reading

list() doesn't call the CLI but directly reads the ~/.claude/config.json file:

private async readClaudeConfig(): Promise<ClaudeConfig | null> {
const configPath = path.join(os.homedir(), '.claude', 'config.json');
const content = await fsp.readFile(configPath, 'utf8');
return JSON.parse(content);
}

Configuration File Structure

interface ClaudeConfig {
mcpServers?: Record<string, McpServerConfig & Record<string, unknown>>;
projects?: Record<string, {
mcpServers?: Record<string, McpServerConfig & Record<string, unknown>>;
}>;
}

Server Record Mapping

Records read from config.json need to be converted to internal format:

private toServerRecord(input): McpServerRecord {
// Transport type inference: transport > type > default 'stdio'
const transport = config.transport ?? config.type ?? 'stdio';

return {
id: `${scope}:${projectPath ?? 'global'}:${name}`,
name,
scope,
type: transport,
projectPath,
status: 'configured',
config
};
}

ID format: {scope}:{projectPath|global}:{name}

Scopes:

  • user — Globally configured servers (top level mcpServers)
  • local — Project-level configured servers (projects[path].mcpServers)

add — Add Server

Add servers via the claude mcp add CLI command:

Stdio Server

claude mcp add --scope user my-server \
-e API_KEY=xxx \
npx -y @example/mcp-server

Parameter construction:

  1. ['mcp', 'add', '--scope', scope, name]
  2. Iterate env, add -e KEY=VALUE parameters
  3. Append command and args

SSE/HTTP Server

claude mcp add --scope user my-server \
--transport sse \
--header "Authorization: Bearer token" \
https://api.example.com/mcp

Parameter construction:

  1. ['mcp', 'add', '--scope', scope, name]
  2. --transport sse|http
  3. URL
  4. Iterate headers, add --header "Key: Value" parameters

discover — Discover Capabilities

Use the claude mcp inspect command to get JSON format capability inventory:

claude mcp inspect --scope user my-server --format json

JSON Extraction

Extract JSON object from CLI output:

private extractJson(output: string) {
const match = /\{[\s\S]*\}/.exec(output);
if (!match) return null;
return JSON.parse(match[0]);
}

Result Normalization

Tools/resources/prompts lists are standardized to McpToolInfo[] format:

private normalizeToolList(value: any): McpToolInfo[] {
// Support string arrays and object arrays
// string → { name: string }
// { name, description } → McpToolInfo
}

Server Identifier Parsing

Server identifiers in Worker use scope:name format (different from McpService's UUID format):

private parseServerIdentifier(id?: string, scope?: string) {
const [prefix, ...rest] = id.split(':');
const name = rest.length ? rest.join(':') : prefix;
const normalizedScope = rest.length
? prefix as 'user' | 'local'
: scope ?? 'user';
return { scope: normalizedScope, name };
}

Examples:

  • user:my-server{ scope: 'user', name: 'my-server' }
  • local:/path/to/project:db-server{ scope: 'local', name: '/path/to/project:db-server' }
  • my-server (no prefix) → { scope: 'user', name: 'my-server' }

CLI Execution

All CLI operations are executed via the runClaude() method:

private async runClaude(
args: string[],
options: { cwd?: string } = {}
): Promise<{ success: boolean; stdout: string; stderr: string }>
  • Use child_process.spawn to start claude process
  • Collect stdout and stderr
  • Determine success/failure via process exit code
  • Support working directory (cwd) specification for local scope operations

Relationship Between Worker and McpService

FeatureMcpService (SDK Direct)mcp.worker (CLI Bridge)
Connection MethodDirect use of MCP SDKThrough claude CLI
Process ModelWithin main processIndependent Worker thread
Config SourceElectron Store (mcp-config)~/.claude/config.json
Tool CallingSupportedNot supported
PurposeRuntime connections and tool callingConfiguration management and CLI compatibility
Real-timeReal-time connectionOn-demand operation

Note: In the current version, McpService (SDK direct connection) is the primary runtime implementation. Worker serves as a supplement, providing configuration interoperability with Claude CLI. The configurations managed by both are independent (stored separately in Electron Store and ~/.claude/config.json).

Error Handling

Errors in the Worker thread are transmitted via the message protocol:

port.on('message', async (message: WorkerAction) => {
try {
// Handle request...
port.postMessage({ id: message.id, result: ... });
} catch (error) {
port.postMessage({
id: message.id,
error: error instanceof Error ? error.message : String(error)
});
}
});

All exceptions are caught and converted to error responses without crashing the Worker thread.