feat: add desktop MCP command shim

This commit is contained in:
ekko
2026-06-05 17:31:59 +08:00
committed by ekko
parent fd42ccbea4
commit 798595d664
5 changed files with 101 additions and 8 deletions
@@ -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':
+1 -1
View File
@@ -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",
+1
View File
@@ -38,6 +38,7 @@ extraResources:
to: "webui"
filter:
- "package.json"
- "bin/**"
- "dist/**"
- "node_modules/**"
# Drop other-platform node-pty prebuilds (saves ~45MB)
+85 -4
View File
@@ -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<CliShimInstallResult> {
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,
}
}
+12 -2
View File
@@ -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()