Files
hermes-web/packages/server/src/services/ekko-agent/mcp.ts
T
toor c93e1fe1bd
Build / build (push) Canceled after 0s
NPM Lockfile Check / npm ci --ignore-scripts (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
Phase 002: rebrand to hermes-ui and consolidate i18n to zh/en
Removes the ekko / hermes-web-ui branding from the whole tree and drops
eight locale files, so the project presents one name and one language pair.

Data compatibility is deliberate, not accidental: DB filenames, the JWT
audience, and skill manifest ownership all still recognise their pre-rename
values so existing deployments under ~/.hermes-web-ui keep working without a
re-login or a re-injection.

scripts/harness-check.mjs now scans for the legacy strings and fails the
build on any new occurrence, with an explicit allow-list (and reasons) for
the compatibility paths that must keep them.
2026-10-16 11:11:29 +08:00

117 lines
3.8 KiB
TypeScript

import { existsSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { config } from '../../config'
const MANAGED_ENV_KEY = 'HERMES_WEB_UI_MANAGED_MCP'
const MANAGED_SERVERS = [
{ name: 'hermes-studio-api', toolset: 'api' },
{ name: 'hermes-studio-devices', toolset: 'devices' },
{ name: 'hermes-studio-use', toolset: 'use' },
] as const
export type EkkoMcpServers = Record<string, unknown>
function isEnabledEnv(value: string | undefined): boolean {
return ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase())
}
function isDesktopRuntime(): boolean {
return String(process.env.HERMES_DESKTOP || '').trim().toLowerCase() === 'true'
}
function allowTransientAutoinject(): boolean {
return isEnabledEnv(process.env.HERMES_WEB_UI_ALLOW_TRANSIENT_MCP_AUTOINJECT)
}
function normalizedPathPrefix(pathname: string): string {
return pathname.replace(/\/+$/, '') + '/'
}
function isTransientAppHome(appHome: string): boolean {
const normalized = normalizedPathPrefix(appHome)
const transientRoots = [tmpdir(), '/tmp', '/private/tmp']
.filter(Boolean)
.map(root => normalizedPathPrefix(root))
return transientRoots.some(root => normalized.startsWith(root))
}
function shouldIncludeManagedMcpServers(): boolean {
if (isEnabledEnv(process.env.HERMES_WEB_UI_DISABLE_MCP_AUTOINJECT)) return false
return !isTransientAppHome(config.appHome) || allowTransientAutoinject()
}
function candidateBundledMcpScripts(): string[] {
return [
process.env.HERMES_WEB_UI_MCP_BIN,
join(process.cwd(), 'bin/hermes-ui-mcp.mjs'),
join(__dirname, '../../bin/hermes-ui-mcp.mjs'),
join(__dirname, '../../../../../bin/hermes-ui-mcp.mjs'),
join(process.cwd(), 'bin/hermes-studio-mcp.mjs'),
join(__dirname, '../../bin/hermes-studio-mcp.mjs'),
join(__dirname, '../../../../../bin/hermes-studio-mcp.mjs'),
join(process.cwd(), 'bin/hermes-web-ui-mcp.mjs'),
join(__dirname, '../../bin/hermes-web-ui-mcp.mjs'),
join(__dirname, '../../../../../bin/hermes-web-ui-mcp.mjs'),
].filter((value): value is string => !!value)
}
function bundledMcpScriptPath(): string | null {
return candidateBundledMcpScripts().find(candidate => existsSync(candidate)) || null
}
function runtimeNodePath(): string | null {
const node = process.env.HERMES_AGENT_NODE?.trim()
return node || null
}
function managedCommandConfig(toolset: string): Record<string, unknown> {
const bundledScript = bundledMcpScriptPath()
if (bundledScript) {
return { command: runtimeNodePath() || process.execPath, args: [bundledScript, toolset] }
}
if (isDesktopRuntime()) {
return { command: 'hermes-studio-mcp', args: [toolset] }
}
return { command: 'hermes-studio-mcp', args: [toolset] }
}
function managedMcpServerConfig(profile: string, serverName: string, toolset: string): Record<string, unknown> {
return {
...managedCommandConfig(toolset),
env: {
HERMES_WEB_UI_URL: `http://127.0.0.1:${config.port}`,
HERMES_WEB_UI_HOME: config.appHome,
HERMES_WEBUI_STATE_DIR: config.appHome,
HERMES_WEB_UI_PROFILE: profile,
HERMES_MCP_SERVER_NAME: serverName,
HERMES_MCP_TOOLSET: toolset,
[MANAGED_ENV_KEY]: '1',
},
enabled: true,
}
}
export function buildManagedEkkoMcpServers(profile: string): EkkoMcpServers {
if (!shouldIncludeManagedMcpServers()) return {}
return Object.fromEntries(
MANAGED_SERVERS.map(server => [
server.name,
managedMcpServerConfig(profile, server.name, server.toolset),
]),
)
}
export function resolveEkkoMcpServers(profile: string, provided?: EkkoMcpServers): EkkoMcpServers | undefined {
const managed = buildManagedEkkoMcpServers(profile)
const merged = {
...managed,
...(provided || {}),
}
return Object.keys(merged).length > 0 ? merged : undefined
}