#!/usr/bin/env node /** * openwork-ui-mcp * * MCP server that exposes OpenWork's semantic app context and UI control * surface as an MCP resource and tools. * Speaks MCP stdio and proxies to the OpenWork desktop bridge HTTP API. * * Requires OpenWork desktop running with the local UI control bridge active. * * Usage: * npx openwork-ui-mcp * * MCP config (OpenCode / Claude Desktop / Cursor / etc.): * { * "mcpServers": { * "openwork-ui": { * "command": "npx", * "args": ["-y", "openwork-ui-mcp"] * } * } * } */ import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { homedir, platform } from "node:os"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; // ── Bridge discovery ── const DISCOVERY_FILE = "openwork-ui-control.json"; const BRIDGE_CACHE_MS = 2_000; const BRIDGE_TIMEOUT_MS = 5_000; let cachedBridge = null; let cachedBridgeAt = 0; function userAppDataDir() { if (platform() === "darwin") return join(homedir(), "Library", "Application Support"); if (platform() === "win32") return process.env.APPDATA || join(homedir(), "AppData", "Roaming"); return process.env.XDG_CONFIG_HOME || join(homedir(), ".config"); } function discoveryPaths() { return [ process.env.OPENWORK_UI_CONTROL_DISCOVERY?.trim(), join(userAppDataDir(), "com.differentai.openwork", DISCOVERY_FILE), join(userAppDataDir(), "com.differentai.openwork.dev", DISCOVERY_FILE), ].filter(Boolean); } function clearBridgeCache() { cachedBridge = null; cachedBridgeAt = 0; } async function discoverBridge() { if (cachedBridge && Date.now() - cachedBridgeAt < BRIDGE_CACHE_MS) return cachedBridge; for (const candidate of discoveryPaths()) { try { const raw = await readFile(candidate, "utf8"); const parsed = JSON.parse(raw); if (typeof parsed.baseUrl === "string" && typeof parsed.token === "string") { cachedBridge = { baseUrl: parsed.baseUrl, token: parsed.token, path: candidate }; cachedBridgeAt = Date.now(); return cachedBridge; } } catch { // Try next } } return null; } async function bridgeRequest(path, options = {}) { const bridge = await discoverBridge(); if (!bridge) { return { ok: false, error: "OpenWork is not running. Launch the OpenWork desktop app first.", hint: "The MCP server connects to a running OpenWork instance via its local bridge.", }; } const url = `${bridge.baseUrl}${path}`; try { const response = await fetch(url, { method: options.method || "GET", signal: AbortSignal.timeout(options.timeoutMs ?? BRIDGE_TIMEOUT_MS), headers: { Authorization: `Bearer ${bridge.token}`, ...(options.body ? { "Content-Type": "application/json" } : {}), }, ...(options.body ? { body: JSON.stringify(options.body) } : {}), }); const text = await response.text(); try { const parsed = JSON.parse(text); if (!response.ok) clearBridgeCache(); return parsed; } catch { if (!response.ok) clearBridgeCache(); return { ok: false, error: text || `HTTP ${response.status}` }; } } catch (error) { clearBridgeCache(); return { ok: false, error: `Bridge unreachable at ${url}: ${error.message}` }; } } function formatArgs(action) { const lines = []; if (Array.isArray(action.args) && action.args.length > 0) { lines.push(" Args:"); for (const arg of action.args) { const required = arg.required ? "required" : "optional"; const type = arg.type || "unknown"; lines.push(` - ${arg.name} (${type}, ${required})${arg.description ? `: ${arg.description}` : ""}`); } } else if (action.requiresArgs) { lines.push(" Args: required; this action has not published detailed argument metadata yet."); } if (action.previewArgs !== undefined) { lines.push(` Example: ${JSON.stringify(action.previewArgs)}`); } return lines.join("\n"); } function formatActionLine(action) { const disabled = action.disabled ? " [disabled]" : ""; const busy = action.busy ? " [busy]" : ""; const args = formatArgs(action); return `${action.id}${disabled}${busy}\n ${action.label || ""}${action.description ? ` — ${action.description}` : ""}${args ? `\n${args}` : ""}`; } function formatExecutionResult(actionId, result) { const payload = result?.result ?? result; if (payload === true || payload === undefined || payload === null) return `Executed ${actionId}.`; if (typeof payload === "string") return payload; if (typeof payload === "number" || typeof payload === "boolean") return `Result: ${payload}`; if (typeof payload === "object") { const lines = [`Executed ${actionId}.`]; for (const [key, value] of Object.entries(payload).slice(0, 12)) { if (key === "ok" || key === "actionId") continue; const rendered = typeof value === "object" ? JSON.stringify(value) : String(value); lines.push(`${key}: ${rendered}`); } return lines.join("\n"); } return `Executed ${actionId}.`; } // ── MCP Server ── const server = new McpServer({ name: "openwork-ui", version: "0.2.0", }); // ── openwork://context/current ── server.resource( "current_openwork_context", "openwork://context/current", async (uri) => { const result = await bridgeRequest("/context"); const context = result.context ?? result; return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(context, null, 2), }], }; }, ); // ── ui.context ── server.tool( "ui_context", "Read OpenWork's semantic app context: current screen, all open conversation tabs, split-screen layout, focused pane, sidebar, side panel, settings, and currently available queries and commands. Reads app state without focusing the window.", {}, async () => { const result = await bridgeRequest("/context"); if (!result.ok && result.error) { return { content: [{ type: "text", text: `Error: ${result.error}` }], isError: true }; } return { content: [{ type: "text", text: JSON.stringify(result.context ?? result, null, 2) }] }; }, ); // ── ui.snapshot ── server.tool( "ui_snapshot", "Get a snapshot of the current OpenWork UI state: active route, narration, visible actions, and status. Use this before taking action to understand what the user sees.", {}, async () => { const result = await bridgeRequest("/snapshot"); if (!result.ok && result.error) { return { content: [{ type: "text", text: `Error: ${result.error}${result.hint ? `\n${result.hint}` : ""}` }], isError: true }; } const snapshot = result.snapshot ?? result; const lines = []; if (snapshot.route) lines.push(`Route: ${snapshot.route}`); if (snapshot.status) lines.push(`Status: ${snapshot.status}`); if (snapshot.narration) lines.push(`Narration: ${snapshot.narration}`); if (snapshot.busyActionId) lines.push(`Busy: ${snapshot.busyActionId}`); if (Array.isArray(snapshot.actions)) { lines.push(`\nActions (${snapshot.actions.length}):`); for (const action of snapshot.actions) { const args = Array.isArray(action.args) && action.args.length ? ` [${action.args.map((a) => a.name).join(", ")}]` : ""; lines.push(` ${action.id} — ${action.label || action.description || ""}${args}`); } } return { content: [{ type: "text", text: lines.join("\n") || "OpenWork is reachable, but it did not return visible UI state." }] }; } ); // ── ui.list_actions ── server.tool( "ui_list_actions", "List all UI control actions currently available in OpenWork: session navigation, composer control, transcript access, and more. Each action has an id you can pass to ui_execute_action.", {}, async () => { const result = await bridgeRequest("/actions"); if (!result.ok && result.error) { return { content: [{ type: "text", text: `Error: ${result.error}` }], isError: true }; } if (!Array.isArray(result.actions) || result.actions.length === 0) { return { content: [{ type: "text", text: "No actions available. Is OpenWork on the main screen?" }] }; } const text = result.actions.map(formatActionLine).join("\n\n"); return { content: [{ type: "text", text: `${result.actions.length} actions:\n\n${text}` }] }; } ); // ── ui.execute_action ── server.tool( "ui_execute_action", "Execute an OpenWork UI action by its id without activating the desktop window. Use ui_list_actions first to see available actions and their required arguments.", { actionId: z.string().describe("The action id from ui_list_actions, e.g. 'session.create_task' or 'composer.set_text'"), args: z.record(z.unknown()).optional().describe("JSON arguments for the action, if required"), }, async ({ actionId, args }) => { const result = await bridgeRequest("/execute", { method: "POST", body: { actionId, args: args ?? {} }, }); if (!result.ok && result.error) { return { content: [{ type: "text", text: `Error executing ${actionId}: ${result.error}` }], isError: true }; } return { content: [{ type: "text", text: formatExecutionResult(actionId, result) }] }; } ); // ── ui.query ── server.tool( "ui_query", "Run a side-effect-free OpenWork query by id. Use ui_context to inspect currently available queries. Queries never focus or navigate the app.", { id: z.string().describe("Query id from ui_context, such as 'session.list_sessions' or 'notifications.list'"), args: z.record(z.unknown()).optional().describe("JSON arguments for the query, if required"), }, async ({ id, args }) => { const result = await bridgeRequest("/query", { method: "POST", body: { id, args: args ?? {} }, }); if (!result.ok && result.error) { return { content: [{ type: "text", text: `Error querying ${id}: ${result.error}` }], isError: true }; } return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; }, ); // ── ui.command ── server.tool( "ui_command", "Execute a semantic OpenWork command by id while OpenWork remains in the background. Use ui_context first and pass its revision to prevent acting on stale UI state.", { id: z.string().describe("Command id from ui_context"), args: z.record(z.unknown()).optional().describe("JSON arguments for the command, if required"), expectedRevision: z.number().int().nonnegative().optional().describe("Revision returned by ui_context"), actor: z.string().optional().describe("Optional agent or client id for serialized-command attribution"), }, async ({ id, args, expectedRevision, actor }) => { const result = await bridgeRequest("/command", { method: "POST", body: { id, args: args ?? {}, expectedRevision, actor }, }); if (!result.ok && result.error) { return { content: [{ type: "text", text: `Error executing ${id}: ${result.error}` }], isError: true }; } return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; }, ); // ── ui.status ── server.tool( "ui_status", "Check if OpenWork is running and the bridge is reachable. Returns connection status and app info.", {}, async () => { const bridge = await discoverBridge(); if (!bridge) { return { content: [{ type: "text", text: "OpenWork is not running.\nLaunch the OpenWork desktop app to enable UI control." }], isError: true }; } try { const response = await fetch(`${bridge.baseUrl}/health`, { signal: AbortSignal.timeout(3000) }); const data = await response.json(); return { content: [{ type: "text", text: `Connected to ${data.app || "OpenWork"}\nBridge: ${bridge.baseUrl}\nVersion: ${data.version ?? "?"}` }] }; } catch (error) { clearBridgeCache(); return { content: [{ type: "text", text: `Bridge file found but not reachable: ${error.message}\nOpenWork may have quit. Relaunch it.` }], isError: true }; } } ); // ── Start ── const transport = new StdioServerTransport(); await server.connect(transport);