From 798595d6646bc97acdc06804b7edeccde99bb653 Mon Sep 17 00:00:00 2001 From: ekko Date: Fri, 5 Jun 2026 17:31:59 +0800 Subject: [PATCH] feat: add desktop MCP command shim --- ...lan-peer-mcp.mjs => hermes-web-ui-mcp.mjs} | 3 +- package.json | 2 +- packages/desktop/electron-builder.yml | 1 + packages/desktop/src/main/cli-shim.ts | 89 ++++++++++++++++++- packages/desktop/src/main/index.ts | 14 ++- 5 files changed, 101 insertions(+), 8 deletions(-) rename bin/{hermes-lan-peer-mcp.mjs => hermes-web-ui-mcp.mjs} (98%) diff --git a/bin/hermes-lan-peer-mcp.mjs b/bin/hermes-web-ui-mcp.mjs similarity index 98% rename from bin/hermes-lan-peer-mcp.mjs rename to bin/hermes-web-ui-mcp.mjs index a05aecb8..eca5dd30 100755 --- a/bin/hermes-lan-peer-mcp.mjs +++ b/bin/hermes-web-ui-mcp.mjs @@ -6,6 +6,7 @@ import { join } from 'node:path' const DEFAULT_PORT = process.env.HERMES_WEB_UI_PORT || process.env.PORT || '8648' const DEFAULT_BASE_URL = `http://127.0.0.1:${DEFAULT_PORT}` +const SERVER_NAME = process.env.HERMES_MCP_SERVER_NAME || 'hermes-web-ui-mcp' function appHome() { return process.env.HERMES_WEB_UI_HOME || @@ -274,7 +275,7 @@ async function handle(message) { result: { protocolVersion: message.params?.protocolVersion || '2024-11-05', capabilities: { tools: {} }, - serverInfo: { name: 'hermes-lan-peer-mcp', version: '0.1.0' }, + serverInfo: { name: SERVER_NAME, version: '0.1.0' }, }, } case 'tools/list': diff --git a/package.json b/package.json index 2aeac1f7..8374e8a0 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ ], "bin": { "hermes-web-ui": "./bin/hermes-web-ui.mjs", - "hermes-lan-peer-mcp": "./bin/hermes-lan-peer-mcp.mjs" + "hermes-web-ui-mcp": "./bin/hermes-web-ui-mcp.mjs" }, "scripts": { "start": "vite --host --port 8648", diff --git a/packages/desktop/electron-builder.yml b/packages/desktop/electron-builder.yml index c7189e63..6f8b278f 100644 --- a/packages/desktop/electron-builder.yml +++ b/packages/desktop/electron-builder.yml @@ -38,6 +38,7 @@ extraResources: to: "webui" filter: - "package.json" + - "bin/**" - "dist/**" - "node_modules/**" # Drop other-platform node-pty prebuilds (saves ~45MB) diff --git a/packages/desktop/src/main/cli-shim.ts b/packages/desktop/src/main/cli-shim.ts index 921fde59..b67350bd 100644 --- a/packages/desktop/src/main/cli-shim.ts +++ b/packages/desktop/src/main/cli-shim.ts @@ -15,6 +15,7 @@ import { HERMES_CLI_ARG } from './cli-constants' const execFileAsync = promisify(execFile) const SHIM_MARKER = 'HERMES_STUDIO_CLI_SHIM' +const MCP_SHIM_MARKER = 'HERMES_STUDIO_MCP_SHIM' const PATH_MARKER_START = '# >>> Hermes Studio CLI shim >>>' const PATH_MARKER_END = '# <<< Hermes Studio CLI shim <<<' @@ -34,6 +35,11 @@ interface CliShimInstallOptions { platform?: NodeJS.Platform } +interface McpShimInstallOptions extends CliShimInstallOptions { + nodePath?: string + scriptPath?: string +} + function platformDelimiter(platform: NodeJS.Platform): string { return platform === 'win32' ? ';' : delimiter } @@ -62,6 +68,10 @@ export function shimPathForPlatform(binDir: string, platform: NodeJS.Platform = return join(binDir, platform === 'win32' ? 'hermes-studio.cmd' : 'hermes-studio') } +export function mcpShimPathForPlatform(binDir: string, platform: NodeJS.Platform = process.platform): string { + return join(binDir, platform === 'win32' ? 'hermes-studio-mcp.cmd' : 'hermes-studio-mcp') +} + function shellQuote(value: string): string { return `'${value.replace(/'/g, `'\\''`)}'` } @@ -112,15 +122,62 @@ export function createShimContent( ].join('\n') } -function isManagedShim(content: string): boolean { - return content.includes(SHIM_MARKER) +export function createMcpShimContent( + nodePath: string, + scriptPath: string, + platform: NodeJS.Platform = process.platform, +): string { + if (platform === 'win32') { + return [ + '@echo off', + `rem ${MCP_SHIM_MARKER}`, + `set "NODE=${nodePath}"`, + `set "SCRIPT=${scriptPath}"`, + 'if not exist "%NODE%" (', + ' echo Hermes Studio Node runtime not found at "%NODE%" 1>&2', + ' echo Open Hermes Studio once to finish runtime setup, then retry hermes-studio-mcp. 1>&2', + ' exit /b 127', + ')', + 'if not exist "%SCRIPT%" (', + ' echo Hermes Studio MCP script not found at "%SCRIPT%" 1>&2', + ' exit /b 127', + ')', + 'set "HERMES_MCP_SERVER_NAME=hermes-studio-mcp"', + '"%NODE%" "%SCRIPT%" %*', + 'exit /b %ERRORLEVEL%', + '', + ].join('\r\n') + } + + return [ + '#!/bin/sh', + `# ${MCP_SHIM_MARKER}`, + `NODE=${shellQuote(nodePath)}`, + `SCRIPT=${shellQuote(scriptPath)}`, + 'if [ ! -x "$NODE" ]; then', + ' echo "Hermes Studio Node runtime not found at $NODE" >&2', + ' echo "Open Hermes Studio once to finish runtime setup, then retry hermes-studio-mcp." >&2', + ' exit 127', + 'fi', + 'if [ ! -f "$SCRIPT" ]; then', + ' echo "Hermes Studio MCP script not found at $SCRIPT" >&2', + ' exit 127', + 'fi', + 'export HERMES_MCP_SERVER_NAME=hermes-studio-mcp', + 'exec "$NODE" "$SCRIPT" "$@"', + '', + ].join('\n') } -function writeShim(shimPath: string, content: string, platform: NodeJS.Platform): ShimInstallStatus { +function isManagedShim(content: string, marker: string): boolean { + return content.includes(marker) +} + +function writeShim(shimPath: string, content: string, platform: NodeJS.Platform, marker = SHIM_MARKER): ShimInstallStatus { if (existsSync(shimPath)) { const existing = readFileSync(shimPath, 'utf-8') if (existing === content) return 'unchanged' - if (!isManagedShim(existing)) return 'skipped' + if (!isManagedShim(existing, marker)) return 'skipped' writeFileSync(shimPath, content, 'utf-8') if (platform !== 'win32') chmodSync(shimPath, 0o755) return 'updated' @@ -245,3 +302,27 @@ export async function installHermesStudioCliShim(options: CliShimInstallOptions reason: status === 'skipped' ? 'existing hermes-studio shim is not managed by Hermes Studio' : undefined, } } + +export async function installHermesStudioMcpShim(options: McpShimInstallOptions = {}): Promise { + const platform = options.platform || process.platform + const env = options.env || process.env + const homeDir = options.homeDir || homedir() + const binDir = resolve(homeDir, 'bin') + const shimPath = mcpShimPathForPlatform(binDir, platform) + const nodePath = options.nodePath || process.execPath + const scriptPath = options.scriptPath || resolve(process.cwd(), 'bin', 'hermes-web-ui-mcp.mjs') + + mkdirSync(binDir, { recursive: true }) + const status = writeShim(shimPath, createMcpShimContent(nodePath, scriptPath, platform), platform, MCP_SHIM_MARKER) + const pathUpdated = await ensureUserBinOnPath(homeDir, binDir, platform, env).catch((err) => { + console.warn(`[cli-shim] failed to update PATH: ${err instanceof Error ? err.message : String(err)}`) + return false + }) + + return { + shimPath, + status, + pathUpdated, + reason: status === 'skipped' ? 'existing hermes-studio-mcp shim is not managed by Hermes Studio' : undefined, + } +} diff --git a/packages/desktop/src/main/index.ts b/packages/desktop/src/main/index.ts index b9c42641..888ea750 100644 --- a/packages/desktop/src/main/index.ts +++ b/packages/desktop/src/main/index.ts @@ -1,10 +1,10 @@ import { app, BrowserWindow, Menu, Tray, shell, ipcMain, nativeImage } from 'electron' import { join } from 'node:path' import { startWebUiServer, stopWebUiServer, getToken } from './webui-server' -import { desktopIcon, desktopTrayTemplateIcon, desktopWindowsTrayIcon, hermesBinExists, hermesBin } from './paths' +import { bundledNode, desktopIcon, desktopTrayTemplateIcon, desktopWindowsTrayIcon, hermesBinExists, hermesBin, webuiDir } from './paths' import { checkForDesktopUpdates, initAutoUpdater } from './updater' import { t } from './desktop-i18n' -import { installHermesStudioCliShim } from './cli-shim' +import { installHermesStudioCliShim, installHermesStudioMcpShim } from './cli-shim' import { parseHermesCliArgs, runBundledHermesCli } from './hermes-cli' import { cachedRuntimeNeedsPackagedReleaseUpdate, @@ -451,6 +451,16 @@ function runDesktopApp() { }).catch(err => { console.warn(`[cli-shim] failed to install hermes-studio command: ${err instanceof Error ? err.message : String(err)}`) }) + installHermesStudioMcpShim({ + nodePath: bundledNode(), + scriptPath: join(webuiDir(), 'bin', 'hermes-web-ui-mcp.mjs'), + }).then(result => { + if (result.status === 'skipped') { + console.warn(`[cli-shim] ${result.reason}: ${result.shimPath}`) + } + }).catch(err => { + console.warn(`[cli-shim] failed to install hermes-studio-mcp command: ${err instanceof Error ? err.message : String(err)}`) + }) } createTray() createWindow()