From d3603a62a6d421f0cb8643291d464759a8c72df7 Mon Sep 17 00:00:00 2001 From: Benjamin Shafii Date: Sat, 4 Jul 2026 00:58:48 -0700 Subject: [PATCH 1/2] feat(server): route connections and plugin skills through search_capabilities/execute_capability Land OpenWork Cloud's two-tool pattern locally so connections and plugin content no longer have to be pasted into the engine config: - MCP connections can be routing:"search": stored server-side but excluded from the engine runtime config and hot-add sync (engine disconnects on flip); POST /workspace/:id/mcp/:name/routing toggles it, with a desktop Switch ("On-demand via search") in the connection details. - New capability router (apps/server/src/capability-router.ts): indexes local connections' tools (on-demand MCP clients: stdio + streamable HTTP, pooled, 60s tools/list cache) and workspace/global skills, scored like den-api's search; executes tools just-in-time and returns skill content. - New built-in OpenCode plugin exposes bare search_capabilities / execute_capability, resolves the workspace from context (directory, then sessionID probe, then explicit workspaceId arg), and federates unmatched queries/names to the openwork-cloud connection so Den capabilities (memory bank etc.) merge into the same surface. - Server routes: GET /workspace/:id/capabilities/search, POST /workspace/:id/capabilities/execute (viewer tokens rejected). - Voiceover-first spec (evals/voiceovers/search-routed-capabilities.md), passing fraimz flow, and a zero-dependency stdio MCP fixture. Verified: openwork-server typecheck + bun test (280 pass), @openwork/app typecheck, plugin bundles in build, fraimz 8/8 frames passed on the real Electron app (local connection, plugin skill, and signed-in cloud federation all through the two verbs). --- apps/app/src/app/lib/openwork-server.ts | 11 + apps/app/src/i18n/locales/en.ts | 5 + .../react-app/domains/connections/store.ts | 22 + .../domains/settings/pages/mcp-view.tsx | 40 ++ .../src/react-app/shell/settings-route.tsx | 5 + apps/server/package.json | 3 +- apps/server/src/capability-router.test.ts | 212 ++++++ apps/server/src/capability-router.ts | 564 +++++++++++++++ apps/server/src/mcp.ts | 29 + .../openwork-capability-router.ts | 203 ++++++ .../src/openwork-extensions-plugin-path.ts | 1 + apps/server/src/openwork-runtime-config.ts | 6 +- .../src/runtime-opencode-config-store.test.ts | 33 +- .../src/runtime-opencode-config-store.ts | 14 + apps/server/src/server.ts | 110 ++- apps/server/src/validators.test.ts | 15 + apps/server/src/validators.ts | 4 + evals/fixtures/acme-glossary-mcp.mjs | 96 +++ .../flows/search-routed-capabilities.flow.mjs | 662 ++++++++++++++++++ .../voiceovers/search-routed-capabilities.md | 27 + pnpm-lock.yaml | 3 + 21 files changed, 2056 insertions(+), 9 deletions(-) create mode 100644 apps/server/src/capability-router.test.ts create mode 100644 apps/server/src/capability-router.ts create mode 100644 apps/server/src/opencode-plugins/openwork-capability-router.ts create mode 100644 evals/fixtures/acme-glossary-mcp.mjs create mode 100644 evals/flows/search-routed-capabilities.flow.mjs create mode 100644 evals/voiceovers/search-routed-capabilities.md diff --git a/apps/app/src/app/lib/openwork-server.ts b/apps/app/src/app/lib/openwork-server.ts index ee2adcea62..bc1370fe0b 100644 --- a/apps/app/src/app/lib/openwork-server.ts +++ b/apps/app/src/app/lib/openwork-server.ts @@ -1504,6 +1504,17 @@ export function createOpenworkServerClient(options: { baseUrl: string; token?: s body: { enabled }, }, ), + setMcpRouting: (workspaceId: string, name: string, routing: "direct" | "search") => + requestJson<{ items: OpenworkMcpItem[] }>( + baseUrl, + `/workspace/${workspaceId}/mcp/${encodeURIComponent(name)}/routing`, + { + token, + hostToken, + method: "POST", + body: { routing }, + }, + ), logoutMcpAuth: (workspaceId: string, name: string) => requestJson<{ ok: true }>(baseUrl, `/workspace/${workspaceId}/mcp/${encodeURIComponent(name)}/auth`, { diff --git a/apps/app/src/i18n/locales/en.ts b/apps/app/src/i18n/locales/en.ts index 0de1392fcb..7c47f76841 100644 --- a/apps/app/src/i18n/locales/en.ts +++ b/apps/app/src/i18n/locales/en.ts @@ -496,6 +496,11 @@ export default { "mcp.disable_app": "Disable", "mcp.enable_app": "Enable", "mcp.reloading_status": "Reloading MCP servers…", + "mcp.routing_badge": "On-demand", + "mcp.routing_failed": "Failed to update this connection's loading behavior.", + "mcp.routing_requires_server": "Connect to an OpenWork server to change how this connection loads.", + "mcp.routing_search_hint": "The agent finds this connection with capability search when it needs it, instead of loading its tools into every session.", + "mcp.routing_search_label": "On-demand via search", "mcp.toggle_failed": "Failed to update MCP enabled state.", "mcp.toggle_requires_server": "Connect to an OpenWork server to enable or disable MCPs.", "mcp.add_modal_subtitle": "Connect a custom MCP server by URL or local command.", diff --git a/apps/app/src/react-app/domains/connections/store.ts b/apps/app/src/react-app/domains/connections/store.ts index ed93a62fa5..3319c41204 100644 --- a/apps/app/src/react-app/domains/connections/store.ts +++ b/apps/app/src/react-app/domains/connections/store.ts @@ -1183,6 +1183,27 @@ export function createConnectionsStore(options: { } } + async function setMcpRouting(name: string, routing: "direct" | "search") { + try { + const { openworkClient, openworkWorkspaceId, canUseOpenworkServer } = + await resolveWritableOpenworkTarget(); + + if (!canUseOpenworkServer || !openworkClient || !openworkWorkspaceId) { + setStateField("mcpStatus", t("mcp.routing_requires_server")); + return; + } + + await openworkClient.setMcpRouting(openworkWorkspaceId, name, routing); + options.markReloadRequired?.("mcp", { type: "mcp", name, action: "updated" }); + await refreshMcpServers(); + } catch (error) { + setStateField( + "mcpStatus", + error instanceof Error ? error.message : t("mcp.routing_failed"), + ); + } + } + function closeMcpAuthModal() { mutateState((current) => ({ ...current, @@ -1278,6 +1299,7 @@ export function createConnectionsStore(options: { logoutMcpAuth, removeMcp, setMcpEnabled, + setMcpRouting, notifyMcpReloading, pollMcpServersAfterReload, get mcpAuthModalOpen() { diff --git a/apps/app/src/react-app/domains/settings/pages/mcp-view.tsx b/apps/app/src/react-app/domains/settings/pages/mcp-view.tsx index 2a93d3bfde..e846f9f5a4 100644 --- a/apps/app/src/react-app/domains/settings/pages/mcp-view.tsx +++ b/apps/app/src/react-app/domains/settings/pages/mcp-view.tsx @@ -48,9 +48,11 @@ import type { McpServerEntry, McpStatusMap } from "../../../../app/types"; import { formatRelativeTime, isDesktopRuntime, isWindowsPlatform } from "../../../../app/utils"; import { t } from "../../../../i18n"; import { Button } from "@/components/ui/button"; +import { Switch } from "@/components/ui/switch"; import { ConfirmModal } from "../../../design-system/modals/confirm-modal"; import { AddMcpModal } from "../../connections/modals/add-mcp-modal"; import { ClaudePluginImportModal } from "../../connections/modals/claude-plugin-import-modal"; +import { CLOUD_MCP_SERVER_NAME } from "../../connections/cloud-mcp-user-state"; import type { OpenworkClaudePluginPreview } from "../../../../app/lib/openwork-server"; import { isOpenWorkExtensionEnabled, @@ -112,6 +114,7 @@ export type McpViewProps = { logoutMcpAuth: (name: string) => Promise | void; removeMcp: (name: string) => void; setMcpEnabled?: (name: string, enabled: boolean) => Promise | void; + setMcpRouting?: (name: string, routing: "direct" | "search") => Promise | void; /** Return extension-specific config UI for the detail modal. */ configSlotForEntry?: (entry: McpDirectoryInfo) => React.ReactNode | null; /** Check if an extension-kind entry is connected/active. */ @@ -130,6 +133,14 @@ export type McpViewProps = { const builtInExtensionDisabledReason = "Disabled by organization"; +const mcpRouting = (config: McpServerEntry["config"]): "direct" | "search" => { + return "routing" in config && config.routing === "search" ? "search" : "direct"; +}; + +const canRouteMcpViaSearch = (entry: McpServerEntry) => { + return entry.source === "config.remote" && entry.name !== CLOUD_MCP_SERVER_NAME; +}; + const statusDot = (status: ReactMcpStatus) => { switch (status) { case "connected": @@ -654,6 +665,7 @@ export function McpView(props: McpViewProps) { setRemoveOpen(true); }} onToggleEnabled={props.setMcpEnabled} + onToggleRouting={props.setMcpRouting} onToggleBusy={setTogglingMcp} /> @@ -1038,6 +1050,7 @@ function McpConfiguredServersSection(props: { onRequestLogout: (name: string) => void; onRemove: (name: string) => void; onToggleEnabled?: (name: string, enabled: boolean) => Promise | void; + onToggleRouting?: (name: string, routing: "direct" | "search") => Promise | void; onToggleBusy: (value: SetStateAction) => void; }) { return ( @@ -1073,6 +1086,7 @@ function McpConfiguredServersSection(props: { onRequestLogout={props.onRequestLogout} onRemove={props.onRemove} onToggleEnabled={props.onToggleEnabled} + onToggleRouting={props.onToggleRouting} onToggleBusy={props.onToggleBusy} /> ))} @@ -1109,6 +1123,7 @@ function McpConfiguredServerRow(props: { onRequestLogout: (name: string) => void; onRemove: (name: string) => void; onToggleEnabled?: (name: string, enabled: boolean) => Promise | void; + onToggleRouting?: (name: string, routing: "direct" | "search") => Promise | void; onToggleBusy: (value: SetStateAction) => void; }) { const Icon = serviceIcon(props.entry.name); @@ -1153,6 +1168,11 @@ function McpConfiguredServerDetails(props: Parameters ) : null} + {canRouteMcpViaSearch(props.entry) && mcpRouting(props.entry.config) === "search" ? ( + + {t("mcp.routing_badge")} + + ) : null} {props.errorInfo ?
{props.errorInfo}
: null}
@@ -1166,6 +1186,26 @@ function McpConfiguredServerDetails(props: Parameters
+ {props.onToggleRouting && canRouteMcpViaSearch(props.entry) ? ( +
+
+
{t("mcp.routing_search_label")}
+
+ {t("mcp.routing_search_hint")} +
+
+ { + if (props.togglingMcp) return; + props.onToggleBusy(props.entry.name); + void Promise.resolve(props.onToggleRouting?.(props.entry.name, checked ? "search" : "direct")).finally(() => props.onToggleBusy(null)); + }} + /> +
+ ) : null}
{props.onToggleEnabled && props.entry.source !== "config.global" ? (