Clean up automation bridge: direct imports, upgrade zod to v4
- Replace ssrLoadModule hacks with direct imports from @open-pencil/core - Replace isBunRuntime() hack with @hono/node-server (works everywhere) - Remove ViteServer type parameter from startAutomationBridge - Upgrade @open-pencil/mcp zod dependency from v3 to v4 - Add @open-pencil/mcp as root devDependency for bridge imports
This commit is contained in:
parent
fea1f4f95d
commit
5641554ab7
9
bun.lock
9
bun.lock
|
|
@ -51,6 +51,7 @@
|
|||
"@hono/node-server": "^1.19.11",
|
||||
"@iconify-json/lucide": "^1.2.94",
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"@open-pencil/mcp": "workspace:*",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@types/culori": "^4.0.1",
|
||||
|
|
@ -135,7 +136,7 @@
|
|||
"@open-pencil/core": "workspace:*",
|
||||
"canvaskit-wasm": "^0.40.0",
|
||||
"hono": "^4.11.4",
|
||||
"zod": "^3.25.0",
|
||||
"zod": "^4.3.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
|
|
@ -2429,14 +2430,8 @@
|
|||
|
||||
"@multiformats/dns/@libp2p/interface": ["@libp2p/interface@3.1.0", "", { "dependencies": { "@multiformats/dns": "^1.0.6", "@multiformats/multiaddr": "^13.0.1", "main-event": "^1.0.1", "multiformats": "^13.4.0", "progress-events": "^1.0.1", "uint8arraylist": "^2.4.8" } }, "sha512-RE7/XyvC47fQBe1cHxhMvepYKa5bFCUyFrrpj8PuM0E7JtzxU7F+Du5j4VXbg2yLDcToe0+j8mB7jvwE2AThYw=="],
|
||||
|
||||
"@open-pencil/mcp/@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
||||
|
||||
"@open-pencil/mcp/@types/node": ["@types/node@22.19.13", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw=="],
|
||||
|
||||
"@open-pencil/mcp/hono": ["hono@4.12.3", "", {}, "sha512-SFsVSjp8sj5UumXOOFlkZOG6XS9SJDKw0TbwFeV+AJ8xlST8kxK5Z/5EYa111UY8732lK2S/xB653ceuaoGwpg=="],
|
||||
|
||||
"@open-pencil/mcp/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@rollup/plugin-node-resolve/@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
|
||||
|
||||
"@rollup/plugin-replace/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="],
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@
|
|||
"vite": "^8.0.0",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"workbox-window": "^7.4.0",
|
||||
"ws": "^8.19.0"
|
||||
"ws": "^8.19.0",
|
||||
"@open-pencil/mcp": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
"@open-pencil/core": "workspace:*",
|
||||
"canvaskit-wasm": "^0.40.0",
|
||||
"hono": "^4.11.4",
|
||||
"zod": "^3.25.0"
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0"
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ function fail(e: unknown): McpResult {
|
|||
return { content: [{ type: 'text', text: JSON.stringify({ error: msg }) }], isError: true }
|
||||
}
|
||||
|
||||
export function paramToZod(param: ParamDef): z.ZodTypeAny {
|
||||
const typeMap: Record<ParamType, () => z.ZodTypeAny> = {
|
||||
export function paramToZod(param: ParamDef): z.ZodType {
|
||||
const typeMap: Record<ParamType, () => z.ZodType> = {
|
||||
string: () =>
|
||||
param.enum
|
||||
? z.enum(param.enum as [string, ...string[]]).describe(param.description)
|
||||
|
|
@ -94,7 +94,7 @@ export function createServer(version: string, options: CreateServerOptions = {})
|
|||
}
|
||||
|
||||
function registerTool(def: ToolDef) {
|
||||
const shape: Record<string, z.ZodTypeAny> = {}
|
||||
const shape: Record<string, z.ZodType> = {}
|
||||
for (const [key, param] of Object.entries(def.params)) {
|
||||
shape[key] = paramToZod(param)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,22 +5,14 @@
|
|||
* ws WebSocket server on :7601 for the browser page.
|
||||
*
|
||||
* Flow: CLI → HTTP POST /rpc → Hono → WebSocket → browser → execute → response
|
||||
*
|
||||
* Security model: same-machine trust. Both HTTP and WebSocket bind to 127.0.0.1.
|
||||
* The browser generates a random bearer token and registers it via WebSocket.
|
||||
* GET /health exposes the token so the CLI can discover it — this is intentional:
|
||||
* any local process can access the bridge, and the token only prevents accidental
|
||||
* cross-session collisions when multiple instances are running.
|
||||
*/
|
||||
import { serve } from '@hono/node-server'
|
||||
import { Hono } from 'hono'
|
||||
import { cors } from 'hono/cors'
|
||||
import { WebSocketServer, type WebSocket } from 'ws'
|
||||
|
||||
import type { ZodTypeAny } from 'zod'
|
||||
import type { ViteDevServer } from 'vite'
|
||||
|
||||
// Can't import from @open-pencil/core here — this file is bundled by esbuild
|
||||
// as part of the Vite config, and workspace packages are externalized then
|
||||
// loaded by Node's ESM resolver which can't handle .ts source imports.
|
||||
const AUTOMATION_HTTP_PORT = 7600
|
||||
const AUTOMATION_WS_PORT = 7601
|
||||
const RPC_TIMEOUT = 30_000
|
||||
|
|
@ -31,9 +23,7 @@ interface PendingRequest {
|
|||
timer: ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
type ViteServer = { ssrLoadModule: (id: string) => Promise<Record<string, unknown>> }
|
||||
|
||||
export function startAutomationBridge(server: ViteServer) {
|
||||
export function startAutomationBridge(server: ViteDevServer) {
|
||||
const pending = new Map<string, PendingRequest>()
|
||||
let browserWs: WebSocket | null = null
|
||||
let authToken: string | null = null
|
||||
|
|
@ -119,12 +109,9 @@ export function startAutomationBridge(server: ViteServer) {
|
|||
|
||||
async function jsxToTree(jsx: string): Promise<unknown> {
|
||||
const core = await server.ssrLoadModule('@open-pencil/core')
|
||||
const buildComponent = core.buildComponent as (jsx: string) => () => unknown
|
||||
const resolveToTree = core.resolveToTree as (el: unknown) => unknown
|
||||
const createElement = core.createElement as (type: unknown, props: unknown) => unknown
|
||||
const Component = buildComponent(jsx)
|
||||
const element = createElement(Component, null)
|
||||
return resolveToTree(element)
|
||||
const Component = (core.buildComponent as (jsx: string) => () => unknown)(jsx)
|
||||
const element = (core.createElement as (type: unknown, props: unknown) => unknown)(Component, null)
|
||||
return (core.resolveToTree as (el: unknown) => unknown)(element)
|
||||
}
|
||||
|
||||
async function preprocessRpc(body: Record<string, unknown>): Promise<Record<string, unknown>> {
|
||||
|
|
@ -178,7 +165,7 @@ export function startAutomationBridge(server: ViteServer) {
|
|||
}
|
||||
})
|
||||
|
||||
// MCP Streamable HTTP endpoint — proxies tool calls through WebSocket to the live editor
|
||||
// MCP Streamable HTTP — proxies tool calls through WebSocket to the live editor
|
||||
type McpTransport = { handleRequest: (r: Request) => Promise<Response> }
|
||||
const mcpSessions = new Map<string, McpTransport>()
|
||||
|
||||
|
|
@ -186,15 +173,15 @@ export function startAutomationBridge(server: ViteServer) {
|
|||
const cached = sessionId ? mcpSessions.get(sessionId) : undefined
|
||||
if (cached) return cached
|
||||
|
||||
const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js')
|
||||
const { WebStandardStreamableHTTPServerTransport } =
|
||||
await import('@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js')
|
||||
const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js')
|
||||
const { z } = await import('zod')
|
||||
const core = await server.ssrLoadModule('@open-pencil/core') as {
|
||||
ALL_TOOLS: Array<{ name: string; description: string; params: Record<string, unknown>; mutates?: boolean }>
|
||||
ALL_TOOLS: Array<{ name: string; description: string; params: Record<string, unknown> }>
|
||||
}
|
||||
const mcp = await server.ssrLoadModule('../../packages/mcp/src/server.ts') as {
|
||||
paramToZod: (p: unknown) => ZodTypeAny
|
||||
const mcp = await server.ssrLoadModule('@open-pencil/mcp') as {
|
||||
paramToZod: (p: unknown) => ReturnType<typeof z.string>
|
||||
}
|
||||
|
||||
const id = sessionId ?? crypto.randomUUID()
|
||||
|
|
@ -202,7 +189,7 @@ export function startAutomationBridge(server: ViteServer) {
|
|||
const register = mcpServer.registerTool.bind(mcpServer) as (...a: unknown[]) => void
|
||||
|
||||
for (const def of core.ALL_TOOLS) {
|
||||
const shape: Record<string, ZodTypeAny> = {}
|
||||
const shape: Record<string, ReturnType<typeof z.string>> = {}
|
||||
for (const [key, param] of Object.entries(def.params)) {
|
||||
shape[key] = mcp.paramToZod(param)
|
||||
}
|
||||
|
|
@ -239,26 +226,9 @@ export function startAutomationBridge(server: ViteServer) {
|
|||
return transport.handleRequest(c.req.raw)
|
||||
})
|
||||
|
||||
void startServer(app)
|
||||
serve({ fetch: app.fetch, port: AUTOMATION_HTTP_PORT, hostname: '127.0.0.1' })
|
||||
|
||||
console.log(`[automation] HTTP http://127.0.0.1:${AUTOMATION_HTTP_PORT}`)
|
||||
console.log(`[automation] WS ws://127.0.0.1:${AUTOMATION_WS_PORT}`)
|
||||
console.log(`[automation] MCP http://127.0.0.1:${AUTOMATION_HTTP_PORT}/mcp`)
|
||||
}
|
||||
|
||||
function isBunRuntime(): boolean {
|
||||
return 'Bun' in globalThis
|
||||
}
|
||||
|
||||
async function startServer(app: Hono) {
|
||||
if (isBunRuntime()) {
|
||||
;(globalThis as unknown as { Bun: { serve: (opts: object) => void } }).Bun.serve({
|
||||
fetch: app.fetch,
|
||||
port: AUTOMATION_HTTP_PORT,
|
||||
hostname: '127.0.0.1'
|
||||
})
|
||||
} else {
|
||||
const { serve } = await import('@hono/node-server')
|
||||
serve({ fetch: app.fetch, port: AUTOMATION_HTTP_PORT, hostname: '127.0.0.1' })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue