From f4454aeb449361258a9c8ca7fe062a4799c4befe Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Tue, 19 May 2026 11:12:01 +0300 Subject: [PATCH] fix(mcp): report version mismatches --- bun.lock | 12 +++++++----- oxlint.json | 7 +++++++ packages/core/src/text/fonts.ts | 6 +++--- packages/mcp/package.json | 1 + packages/mcp/src/server.ts | 32 ++++++++++++++++++++++++++++---- src/app/automation/mcp/spawn.ts | 20 +++++++++++++++++++- src/env.d.ts | 1 + vite.config.ts | 3 +++ 8 files changed, 69 insertions(+), 13 deletions(-) diff --git a/bun.lock b/bun.lock index e45e12911..cb11e1ad4 100644 --- a/bun.lock +++ b/bun.lock @@ -98,7 +98,7 @@ }, "packages/cli": { "name": "@open-pencil/cli", - "version": "0.11.8", + "version": "0.12.0", "bin": { "openpencil": "./src/index.ts", }, @@ -114,7 +114,7 @@ }, "packages/core": { "name": "@open-pencil/core", - "version": "0.11.8", + "version": "0.12.0", "dependencies": { "@iconify/utils": "^3.1.0", "acorn": "^8.16.0", @@ -152,15 +152,17 @@ }, "packages/mcp": { "name": "@open-pencil/mcp", - "version": "0.11.8", + "version": "0.12.0", "bin": { - "openpencil-mcp": "./dist/index.js", + "openpencil-mcp": "./dist/stdio.js", + "openpencil-mcp-http": "./dist/index.js", }, "dependencies": { "@hono/node-server": "^1.19.9", "@modelcontextprotocol/sdk": "^1.25.2", "@open-pencil/core": "workspace:*", "hono": "^4.11.4", + "package-manager-detector": "^1.6.0", "ws": "^8.19.0", "zod": "^4.3.6", }, @@ -171,7 +173,7 @@ }, "packages/vue": { "name": "@open-pencil/vue", - "version": "0.11.8", + "version": "0.12.0", "dependencies": { "@nanostores/i18n": "^1.2.2", "@nanostores/vue": "^1.1.0", diff --git a/oxlint.json b/oxlint.json index 57beab259..89bb99680 100644 --- a/oxlint.json +++ b/oxlint.json @@ -45,6 +45,13 @@ { "name": "@/types", "message": "Import types directly from @open-pencil/core." + }, + { + "name": "node:module", + "importNames": [ + "createRequire" + ], + "message": "Use ESM imports or import.meta.resolve instead of createRequire/require." } ], "patterns": [ diff --git a/packages/core/src/text/fonts.ts b/packages/core/src/text/fonts.ts index 6277584b5..8eb321bc7 100644 --- a/packages/core/src/text/fonts.ts +++ b/packages/core/src/text/fonts.ts @@ -215,9 +215,9 @@ export class FontManager { } const { readFile } = await import(/* @vite-ignore */ 'node:fs/promises') const { resolve, dirname } = await import(/* @vite-ignore */ 'node:path') - const { createRequire } = await import(/* @vite-ignore */ 'node:module') - const require = createRequire(import.meta.url) - const packageRoot = dirname(require.resolve('@open-pencil/core/package.json')) + const { fileURLToPath } = await import(/* @vite-ignore */ 'node:url') + const packageJsonUrl = import.meta.resolve('@open-pencil/core/package.json') + const packageRoot = dirname(fileURLToPath(packageJsonUrl)) const assetPath = resolve(packageRoot, `assets${url}`) const buf = await readFile(assetPath) return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 33cd6b545..164db325b 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -36,6 +36,7 @@ "@modelcontextprotocol/sdk": "^1.25.2", "@open-pencil/core": "workspace:*", "hono": "^4.11.4", + "package-manager-detector": "^1.6.0", "zod": "^4.3.6", "ws": "^8.19.0" }, diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index 9339f5c13..838894114 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -1,6 +1,7 @@ -import { createRequire } from 'node:module' import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { resolveCommand } from 'package-manager-detector/commands' +import { detect, getUserAgent } from 'package-manager-detector/detect' import { Hono } from 'hono' import { cors } from 'hono/cors' import { WebSocketServer } from 'ws' @@ -10,10 +11,31 @@ import { createBrowserRpcBridge } from './browser-rpc' import { MCP_CORS_HEADERS, MCP_CORS_METHODS, MCP_EXPOSED_HEADERS } from './http-options' import { preprocessRpc } from './jsx-preprocess' import { createMcpSessionManager } from './mcp-sessions' +import packageJson from '../package.json' import { registerTools } from './tool/registration' -const require = createRequire(import.meta.url) -export const MCP_VERSION: string = (require('../package.json') as { version: string }).version +export const MCP_VERSION: string = packageJson.version + +let installCommandPromise: Promise | null = null + +async function resolveMcpInstallCommand(): Promise { + const agent = + getUserAgent() ?? + ( + await detect({ + strategies: ['install-metadata', 'lockfile', 'packageManager-field', 'devEngines-field'] + }) + )?.agent ?? + 'npm' + const resolved = resolveCommand(agent, 'global', [`@open-pencil/mcp@${MCP_VERSION}`]) + if (!resolved) return `npm install -g @open-pencil/mcp@${MCP_VERSION}` + return [resolved.command, ...resolved.args].join(' ') +} + +function mcpInstallCommand(): Promise { + installCommandPromise ??= resolveMcpInstallCommand() + return installCommandPromise +} export { fail, ok, type MCPContent, type MCPResult } from './result' @@ -84,9 +106,11 @@ export function startServer(options: ServerOptions = {}) { ) } - app.get('/health', (c) => + app.get('/health', async (c) => c.json({ status: browserRpc.isConnected() ? 'ok' : 'no_app', + version: MCP_VERSION, + installCommand: await mcpInstallCommand(), authRequired: authToken !== null, ...(browserRpc.currentRpcToken() ? { token: browserRpc.currentRpcToken() } : {}) }) diff --git a/src/app/automation/mcp/spawn.ts b/src/app/automation/mcp/spawn.ts index e43f1c9a6..fa606d776 100644 --- a/src/app/automation/mcp/spawn.ts +++ b/src/app/automation/mcp/spawn.ts @@ -8,6 +8,8 @@ import { isTauri } from '@/app/tauri/env' interface AutomationHealth { status: 'ok' | 'no_app' + version?: string + installCommand?: string authRequired?: boolean token?: string } @@ -18,6 +20,7 @@ export interface AutomationServerHandle { } const DEV_AUTOMATION_AUTH_TOKEN = import.meta.env.DEV ? __OPENPENCIL_LOCAL_AUTOMATION_TOKEN__ : null +const APP_VERSION = __OPENPENCIL_APP_VERSION__ const noop = () => undefined let runtimeAutomationAuthToken: string | null = DEV_AUTOMATION_AUTH_TOKEN @@ -35,6 +38,18 @@ async function readHealth(): Promise { } } +function assertCompatibleMcpVersion(health: AutomationHealth): void { + if (health.version === APP_VERSION) return + const runningVersion = health.version ? `v${health.version}` : 'an older version' + const updateHint = health.installCommand + ? `Run: ${health.installCommand}, then restart OpenPencil.` + : `Update the global @open-pencil/mcp package to v${APP_VERSION} with your package manager, then restart OpenPencil.` + throw new Error( + `OpenPencil desktop v${APP_VERSION} requires @open-pencil/mcp v${APP_VERSION}, ` + + `but the running MCP server is ${runningVersion}. ${updateHint}` + ) +} + async function pollHealth(retries: number, delayMs: number): Promise { for (let i = 0; i < retries; i++) { await promiseTimeout(delayMs) @@ -47,6 +62,7 @@ async function pollHealth(retries: number, delayMs: number): Promise { if (runtimeAutomationAuthToken) return runtimeAutomationAuthToken const health = await readHealth() + if (health) assertCompatibleMcpVersion(health) runtimeAutomationAuthToken = health?.token ?? null return runtimeAutomationAuthToken } @@ -60,6 +76,7 @@ export async function spawnMCPIfNeeded(): Promise const existing = await readHealth() if (existing) { + assertCompatibleMcpVersion(existing) runtimeAutomationAuthToken = existing.token ?? null return { disconnect: noop, @@ -98,6 +115,7 @@ export async function spawnMCPIfNeeded(): Promise const health = await pollHealth(5, 1000) if (health) { + assertCompatibleMcpVersion(health) runtimeAutomationAuthToken = health.token ?? authToken return { disconnect: () => { @@ -109,6 +127,6 @@ export async function spawnMCPIfNeeded(): Promise await child.kill() throw new Error( - 'Failed to start MCP server. Is openpencil-mcp-http installed? Run: npm i -g @open-pencil/mcp' + `Failed to start MCP server. Install @open-pencil/mcp@${APP_VERSION} globally with your package manager, then restart OpenPencil.` ) } diff --git a/src/env.d.ts b/src/env.d.ts index 3de35245f..c3bd7fff9 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -2,6 +2,7 @@ /// /// +declare const __OPENPENCIL_APP_VERSION__: string declare const __OPENPENCIL_LOCAL_AUTOMATION_TOKEN__: string | null declare module '*.vue' { diff --git a/vite.config.ts b/vite.config.ts index 0face9b3a..9c6a0cc24 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,7 @@ import process from 'node:process' +import packageJson from './package.json' + import tailwindcss from '@tailwindcss/vite' import vue from '@vitejs/plugin-vue' import IconsResolver from 'unplugin-icons/resolver' @@ -21,6 +23,7 @@ export default defineConfig(async ({ command }) => ({ alias: createOpenPencilAliases(__dirname) }, define: { + __OPENPENCIL_APP_VERSION__: JSON.stringify(packageJson.version), __OPENPENCIL_LOCAL_AUTOMATION_TOKEN__: JSON.stringify(localAutomationToken(command)) }, plugins: [