# Electron Integration

Model Context Playground ships an optional Electron bundle that unlocks native file system access, local process execution, and a lightweight file server while keeping the Web build untouched. This guide explains how to wire the integration into a fresh Electron/Vite project, highlights the exported helpers, and calls out the security knobs you should keep in mind.

## What the package provides

`model-context-playground/electron` exports a set of composable pieces:

- `electronIntegration` – convenience bundle containing storage helpers, additional tools, prompts, polyfills, and the local file server client.
- `registerIpcHandlers` – registers filesystem, networking, process, and child-process IPC routes in the Electron main process.
- `ensureLocalFileServer` – starts (or reuses) a bundled HTTP server that serves uploaded attachments from disk.
- `installPreload` – exposes safe renderer APIs via `contextBridge`.

Each piece can be consumed independently, but the sections below show the default pairing.

## Quick start checklist

1. **Install Electron (dev dependency)**

   ```bash
   npm install --save-dev electron
   ```

2. **Ensure bundler support for Node polyfills** – the Vite example below uses `vite-plugin-node-polyfills` to keep renderer builds happy.

3. **Create the app shell** – the renderer subscribes to Electron tools/prompts and storage; the main process registers IPC and the local file server; the preload bridges the APIs.

The snippets that follow assume a directory layout similar to

```text
electron/
  main.ts
  preload.ts
renderer/
  App.tsx
  renderer.tsx
vite.renderer.config.ts
```

### Renderer (React) entry point

```tsx
// renderer/App.tsx
import 'model-context-playground/styles.css'
import { ModelContextPlayground } from 'model-context-playground'
import { OpenAIProvider } from 'model-context-playground/providers/openai'
import { OllamaProvider } from 'model-context-playground/providers/ollama'
import { electronIntegration } from 'model-context-playground/electron'

export default function App() {
  return (
    <ModelContextPlayground
      getProviders={async () => [
        new OpenAIProvider(),
        new OllamaProvider()
      ]}
      additionalTools={[...electronIntegration.tools]}
      additionalPrompts={[...electronIntegration.prompts]}
      attachmentCallbackHandler={electronIntegration.storage.attachmentCallbackHandler}
      storageAdapter={electronIntegration.storage.adapter}
    />
  )
}
```

```tsx
// renderer/renderer.tsx
import { createRoot } from 'react-dom/client'
import App from './App'

const container = document.getElementById('root')
if (!container) {
  throw new Error('Root container not found')
}

createRoot(container).render(<App />)
```

### Preload script

```ts
// electron/preload.ts
import { contextBridge, ipcRenderer } from 'electron'
import { installPreload } from 'model-context-playground/electron'

installPreload(contextBridge, ipcRenderer)
```

### Main process

```ts
// electron/main.ts
import { app, BrowserWindow } from 'electron'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import started from 'electron-squirrel-startup'
import { registerIpcHandlers, ensureLocalFileServer } from 'model-context-playground/electron'

if (started) {
  app.quit()
}

let mainWindow: BrowserWindow | null = null
const __dirname = path.dirname(fileURLToPath(import.meta.url))

const getLocalFileServer = () => ensureLocalFileServer()

const createWindow = async (): Promise<BrowserWindow> => {
  if (mainWindow && !mainWindow.isDestroyed()) {
    return mainWindow
  }

  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      nodeIntegration: false,
      contextIsolation: true
    }
  })

  if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
    await mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL)
  } else {
    const htmlPath = path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)
    await mainWindow.loadFile(htmlPath)
  }

  mainWindow.on('closed', () => {
    mainWindow = null
  })

  return mainWindow
}

registerIpcHandlers(getLocalFileServer)

app.whenReady().then(async () => {
  await getLocalFileServer()
  await createWindow()

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      void createWindow()
    }
  })
})

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})
```

### Renderer Vite config

```ts
// vite.renderer.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
import { fileURLToPath } from 'node:url'

export default defineConfig({
  plugins: [react(), nodePolyfills()],
  resolve: {
    alias: {
      // Force the shim so the renderer bundles without bundling real Electron
      electron: fileURLToPath(new URL('./node_modules/model-context-playground/dist/electron/shim.js', import.meta.url))
    }
  },
  optimizeDeps: {
    exclude: ['electron']
  }
})
```

> ⚠️ The shim keeps bundlers from inlining the real `electron` module into the renderer build. Ensure the alias path matches your package manager’s layout.

## How the moving parts fit together

- **Preload bridge** – `installPreload` exposes typed helpers on `window` (`filesystem`, `desktopFsPromises`, `child_process`, `network`, `localFileServer`, `desktopProcess`). The renderer never touches Node APIs directly.
- **Main process IPC** – `registerIpcHandlers` wires the file system helpers, fetch proxy, bash execution, and environment helpers behind those preload APIs.
- **Local file server** – `ensureLocalFileServer` spawns an HTTP server that persists uploads to `~/.mcp-desktop/files` (overridable via options). The renderer’s storage adapter uploads attachments to that server; tools can download them via `sourceUrl`.
- **Storage adapter** – `electronIntegration.storage.adapter` mirrors the browser storage API and persists configuration JSON files under `~/.mcp-desktop/config` by default. Call `electronIntegration.storage.createAdapter({ configDirectoryPath: '/custom/path' })` to override the location when wiring the playground.
- **Additional tools and prompts** – you get filesystem-aware tools (`sys:list_directory`, `sys:read_file`, `sys:write_file`, `sys:fetch_url`, `sys:execute_bash`) and the `sys:file_tree` prompt out of the box. You can selectively import from `model-context-playground/electron/tools` or `.../prompts` if you prefer.

## Security and hardening notes

- Keep `contextIsolation: true` and `nodeIntegration: false` unless you fully trust the content you load.
- Review which tools you surface to end users. For example, `sys:execute_bash` is gated behind `needsApproval` but still executes arbitrary shell commands.
- The local file server stores uploads on disk. If that is undesirable, point `ensureLocalFileServer({ storageDir: ... })` to a sandboxed path or provide your own attachment handler.

## Troubleshooting

- **Renderer build fails with “Cannot find module 'electron'”** – confirm the alias points to the shim and `electron` is excluded from Vite dependency optimization.
- **IPC handler throws ENOENT** – ensure the preload script ran (look for `window.filesystem` in devtools) and that `registerIpcHandlers` was invoked before the renderer attempted access.
- **Attachments do not upload** – check that `ensureLocalFileServer` resolves before creating the first window and that the devtools Network tab shows a POST to `/files`.
