fix(mcp): report version mismatches
This commit is contained in:
parent
c11d65fe55
commit
f4454aeb44
12
bun.lock
12
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",
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<string> | null = null
|
||||
|
||||
async function resolveMcpInstallCommand(): Promise<string> {
|
||||
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<string> {
|
||||
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() } : {})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<AutomationHealth | null> {
|
|||
}
|
||||
}
|
||||
|
||||
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<AutomationHealth | null> {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
await promiseTimeout(delayMs)
|
||||
|
|
@ -47,6 +62,7 @@ async function pollHealth(retries: number, delayMs: number): Promise<AutomationH
|
|||
export async function getAutomationAuthToken(): Promise<string | null> {
|
||||
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<AutomationServerHandle | null>
|
|||
|
||||
const existing = await readHealth()
|
||||
if (existing) {
|
||||
assertCompatibleMcpVersion(existing)
|
||||
runtimeAutomationAuthToken = existing.token ?? null
|
||||
return {
|
||||
disconnect: noop,
|
||||
|
|
@ -98,6 +115,7 @@ export async function spawnMCPIfNeeded(): Promise<AutomationServerHandle | null>
|
|||
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<AutomationServerHandle | null>
|
|||
|
||||
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.`
|
||||
)
|
||||
}
|
||||
|
|
|
|||
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
|
|
@ -2,6 +2,7 @@
|
|||
/// <reference types="vite-plugin-pwa/vanillajs" />
|
||||
/// <reference types="unplugin-icons/types/vue" />
|
||||
|
||||
declare const __OPENPENCIL_APP_VERSION__: string
|
||||
declare const __OPENPENCIL_LOCAL_AUTOMATION_TOKEN__: string | null
|
||||
|
||||
declare module '*.vue' {
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
Loading…
Reference in a new issue