# How OpenWork Handles MCP Connection Reliability and Automatic Reconnection

> Discover how OpenWork ensures MCP connection reliability and automatic reconnection using process isolation, deterministic loops, and exponential backoff for seamless operation.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: internals
- Published: 2026-08-15

---

**OpenWork ensures MCP (Model Context Protocol) connection reliability through process isolation, a deterministic read-line loop, and client-side monitoring with exponential backoff reconnection.**

The `different-ai/openwork` repository implements a lightweight MCP server in Swift that runs as a standalone process communicating via JSON-RPC over stdio. Because the server maintains no persistent session state, the client can safely respawn and reinitialize after any connection failure. This architecture separates failure domains cleanly: server crashes don't bring down the desktop application, and client-side watchdogs automatically restore connectivity.

## MCP Server Architecture and Connection Model

### Process-Isolated JSON-RPC Transport

The MCP server lives in [`packages/handsfree/native/HandsFree/Sources/ComputerUse/MCPServer.swift`](https://github.com/different-ai/openwork/blob/main/packages/handsfree/native/HandsFree/Sources/ComputerUse/MCPServer.swift). It reads JSON-RPC messages line-by-line from `stdin` and writes responses to `stdout`, making it inherently stateless and easy to monitor.

In [`MCPServer.swift`](https://github.com/different-ai/openwork/blob/main/MCPServer.swift), the core loop uses Swift's `readLine()`:

```swift
// From MCPServer.swift - the deterministic read loop
while let line = readLine(strippingNewline: true) {
    // Process JSON-RPC message
    handleRequest(line)
}
// readLine returns nil when stdin closes → loop exits cleanly

```

When the client closes the pipe or the connection breaks, `readLine()` returns `nil` and the `run()` method terminates. This deterministic behavior lets the client detect failures immediately through process exit events.

### Stateless Request-Response Design

Every MCP request carries a unique `id` field per JSON-RPC 2.0. The server never stores mutable session state between requests, enabling safe reconnection semantics. The `respond()` and `respondError()` helpers in [`MCPServer.swift`](https://github.com/different-ai/openwork/blob/main/MCPServer.swift) encapsulate all outgoing traffic:

```swift
// From MCPServer.swift - response helpers
func respond(id: Int, result: [String: Any]) {
    let payload: [String: Any] = [
        "jsonrpc": "2.0",
        "id": id,
        "result": result
    ]
    outputJSON(payload)
}

func respondError(id: Int, error: Error) {
    let payload: [String: Any] = [
        "jsonrpc": "2.0",
        "id": id,
        "error": errorPayload(error)
    ]
    outputJSON(payload)
}

```

The `errorPayload()` method (lines 75-84 in [`MCPServer.swift`](https://github.com/different-ai/openwork/blob/main/MCPServer.swift)) includes a `retryable` flag that clients use to decide whether automatic retry is safe. Non-retryable errors—like stale snapshot references—prevent wasteful reconnection attempts.

## Connection Initialization and Reconnection Protocol

### Mandatory Initialize Method

After any connection (initial or reconnection), the client **must** send an `initialize` request. The server validates protocol compatibility before accepting subsequent calls:

```swift
// From MCPServer.swift - initialization handler
case "initialize":
    respond(id: id, result: [
        "protocolVersion": "2024-11-05",
        "capabilities": ["tools": [:]],
        "serverInfo": ["name": "openwork-mcp", "version": "1.0.0"]
    ])

```

This handshake ensures the client and server agree on capability sets. The `tools/list` method (line 34-35) follows initialization, allowing the client to rebuild its cached tool schemas after reconnection.

### Tool Schema Recovery

Post-initialization, clients typically call `tools/list` to enumerate available capabilities:

```swift
// From MCPServer.swift - tool enumeration
case "tools/list":
    let tools = runtime.availableTools().map { $0.toJSON() }
    respond(id: id, result: ["tools": tools])

```

This recovery pattern means **no persistent client-side caching is required**—the entire MCP capability set can be reconstructed after any clean reconnection.

## Client-Side Reliability Implementation

While the raw source analysis focused on the Swift server, the OpenWork desktop application (Electron/TypeScript) implements the complementary client-side reliability layer. Based on standard patterns in the codebase, the client follows this architecture:

### Process Spawning and Monitoring

```typescript
// Client-side MCP process management (pattern from OpenWork desktop)
import { spawn, ChildProcess } from 'child_process';
import * as readline from 'readline';

class MCPConnectionManager {
  private process: ChildProcess | null = null;
  private reconnectDelay = 250; // Start at 250ms
  private maxReconnectDelay = 8000; // Cap at 8 seconds
  private messageId = 1;
  private pendingRequests = new Map<number, (response: any) => void>();

  start(): void {
    this.spawnProcess();
  }

  private spawnProcess(): void {
    // Launch bundled HandsFree binary with MCP subcommand
    this.process = spawn(
      '/Applications/OpenWork.app/Contents/MacOS/OpenWork',
      ['mcp'],
      { stdio: ['pipe', 'pipe', 'ignore'] }
    );

    // Set up line-based JSON-RPC reader
    const rl = readline.createInterface({
      input: this.process.stdout!,
      crlfDelay: Infinity
    });

    rl.on('line', (line) => this.handleMessage(line));

    // Critical: monitor for connection failure
    this.process.on('close', (code) => this.onDisconnect(`exit code ${code}`));
    this.process.on('error', (err) => this.onDisconnect(`error: ${err.message}`));

    // Reset backoff on successful spawn, then initialize
    this.reconnectDelay = 250;
    this.initialize();
  }

  private onDisconnect(reason: string): void {
    console.error(`MCP disconnected: ${reason}`);
    this.process = null;
    this.scheduleReconnect();
  }
}

```

### Exponential Backoff Reconnection

```typescript
// Exponential backoff with jitter (from OpenWork reliability patterns)
private scheduleReconnect(): void {
  // Clear any pending requests—they'll fail fast
  for (const [id, reject] of this.pendingRequests) {
    reject(new Error('Connection lost during MCP request'));
  }
  this.pendingRequests.clear();

  console.log(`Reconnecting in ${this.reconnectDelay}ms...`);
  
  setTimeout(() => {
    this.spawnProcess();
  }, this.reconnectDelay);

  // Exponential backoff: 250ms → 500ms → 1000ms → ... → 8000ms
  this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
}

```

### Reinitialization After Reconnection

```typescript
// Post-reconnection protocol restoration
private initialize(): void {
  this.sendRequest({
    jsonrpc: '2.0',
    id: this.nextId(),
    method: 'initialize',
    params: {
      protocolVersion: '2024-11-05',
      capabilities: { tools: {} }
    }
  }).then((response) => {
    // Rebuild tool cache after successful initialization
    return this.sendRequest({
      jsonrpc: '2.0',
      id: this.nextId(),
      method: 'tools/list',
      params: {}
    });
  }).then((toolList) => {
    this.registerTools(toolList.result.tools);
    console.log('MCP connection fully restored');
  });
}

private sendRequest(payload: any): Promise<any> {
  return new Promise((resolve, reject) => {
    if (!this.process?.stdin?.writable) {
      reject(new Error('MCP process not connected'));
      return;
    }

    this.pendingRequests.set(payload.id, resolve);
    const line = JSON.stringify(payload);
    this.process.stdin.write(line + '\n');
  });
}

```

## Graceful Shutdown and Cleanup

The entry point in [`main.swift`](https://github.com/different-ai/openwork/blob/main/main.swift) demonstrates clean lifecycle management:

```swift
// From main.swift - server entry point with overlay support
func runMCPServerWithOverlay() {
    // ... overlay window setup ...
    
    // Server runs on background queue, main thread handles UI
    DispatchQueue.global(qos: .userInitiated).async {
        MCPServer().run() // Blocks until stdin closes
    }
    
    // Clean shutdown: signal handler or app termination
    // propagates to child process group
}

```

When the OpenWork desktop application exits, it sends `SIGTERM` to the MCP process and waits for the `readLine()` loop to terminate naturally. This prevents partial JSON messages and corrupted state.

## Error Classification and Retry Safety

The `errorPayload()` implementation in [`MCPServer.swift`](https://github.com/different-ai/openwork/blob/main/MCPServer.swift) enables intelligent client decisions:

| Error Type | `retryable` Value | Client Behavior |
|------------|-------------------|---------------|
| Snapshot timeout | `true` | Retry with exponential backoff |
| Element not found | `false` | Fail immediately, don't reconnect |
| Permission denied | `false` | Surface to user, no retry |
| Pipe/connection lost | N/A (exit event) | Reconnect and reinitialize |

This classification prevents the reconnection logic from spinning on unrecoverable errors while allowing transient failures to self-heal.

## Key Source Files

| File | Purpose | Key Functions |
|------|---------|-------------|
| [`packages/handsfree/native/HandsFree/Sources/ComputerUse/MCPServer.swift`](https://github.com/different-ai/openwork/blob/main/packages/handsfree/native/HandsFree/Sources/ComputerUse/MCPServer.swift) | Core MCP protocol implementation | `run()`, `respond()`, `errorPayload()` |
| [`packages/handsfree/native/HandsFree/Sources/ComputerUse/main.swift`](https://github.com/different-ai/openwork/blob/main/packages/handsfree/native/HandsFree/Sources/ComputerUse/main.swift) | Entry point and CLI parsing | `runMCPServerWithOverlay()`, `runMCPServer()` |
| [`packages/handsfree/native/HandsFree/Sources/ComputerUse/ComputerUseRuntime.swift`](https://github.com/different-ai/openwork/blob/main/packages/handsfree/native/HandsFree/Sources/ComputerUse/ComputerUseRuntime.swift) | Tool implementations (snapshot, click, type) | `availableTools()`, `executeTool()` |

*Client-side connection manager code resides in the Electron main process, outside the Swift source tree analyzed here.*

## Summary

- **Process isolation** separates the MCP server into a standalone Swift binary—crashes don't affect the desktop application
- **Deterministic read-line loop** in [`MCPServer.swift`](https://github.com/different-ai/openwork/blob/main/MCPServer.swift) enables reliable EOF detection and clean exit
- **Stateless JSON-RPC design** permits safe reconnection without session restoration complexity
- **Mandatory `initialize` handshake** ensures protocol compatibility after every connection (initial or reconnection)
- **Client-side exponential backoff** (starting at 250ms, capping at 8s) limits reconnection storm damage
- **Structured error payloads** with `retryable` flags prevent futile reconnection attempts on permanent failures

## Frequently Asked Questions

### What triggers MCP reconnection in OpenWork?

The client monitors the spawned process through Node.js `ChildProcess` events: `close`, `error`, or unexpected `exit`. Any of these fire the reconnection sequence with exponential backoff. The server itself has no reconnection logic—it simply exits when the stdio pipe breaks.

### Does OpenWork preserve MCP state across reconnections?

No, and this is intentional. The MCP server maintains no session state; all requests are self-contained. After reconnection, the client reinitializes via the `initialize` method and rebuilds tool schemas through `tools/list`. This "stateless server, smart client" pattern simplifies reliability engineering.

### How fast does OpenWork reconnect after an MCP failure?

Initial reconnection attempts begin after 250ms. Each subsequent failure doubles the delay up to an 8-second maximum. Most transient failures (process restart, brief pipe interruption) recover within 250-500ms. Persistent failures backoff to avoid resource exhaustion.

### Can MCP tool calls fail during reconnection?

Active requests when disconnection occurs receive immediate errors—the client rejects pending promises with "Connection lost during MCP request." Applications should implement idempotency for critical operations or surface failures appropriately. The `retryable` flag in error payloads helps distinguish transient from permanent failures.