[codex] fix MCP management lifecycle (#1144)

* feat(mcp): add MCP server management UI

- Server CRUD: add/edit/remove with YAML/JSON Monaco editor
- raw_config passthrough: zero field loss on edit/toggle
- tool_details embedding: single-request card data (1+N → 1)
- Auto-retry exponential backoff (2s→32s, max 5 retries)
- Route safety guards (hasRoute) for dynamic sidebar
- i18n: 9 languages (de/en/es/fr/ja/ko/pt/zh/zh-TW)
- 19 unit tests + 8 UX browser tests
- 35 files, +2933 lines

* fix mcp management lifecycle

---------

Co-authored-by: Crafter-feng <succeed_happu@163.com>
This commit is contained in:
ekko
2026-05-30 11:06:08 +08:00
committed by GitHub
co-authored by Crafter-feng
parent 675ddb8282
commit b015e70b9d
37 changed files with 2717 additions and 7 deletions
@@ -0,0 +1,117 @@
import type { Context } from 'koa'
import { bridgeMcpAction } from '../../services/hermes/mcp'
function getProfile(ctx: Context): string | undefined {
return (ctx.state as any)?.profile?.name || undefined
}
/** Validate server name: non-empty, no control chars, no path separators */
function isValidServerName(name: string): boolean {
if (!name || name.trim().length === 0) return false
if (name.length > 128) return false
// Reject path separators and control characters
if (/[/\\\x00-\x1f]/.test(name)) return false
return true
}
export async function listServers(ctx: Context) {
try {
ctx.body = await bridgeMcpAction('mcp_list', {}, getProfile(ctx))
} catch (err: any) {
ctx.status = 503
ctx.body = { error: err.message || 'MCP bridge not available' }
}
}
export async function addServer(ctx: Context) {
try {
const { name, config } = (ctx.request.body || {}) as Record<string, unknown>
if (typeof name !== 'string' || !isValidServerName(name)) {
ctx.status = 400
ctx.body = { error: 'Valid server name is required' }
return
}
if (!config || typeof config !== 'object') {
ctx.status = 400
ctx.body = { error: 'config object is required' }
return
}
ctx.body = await bridgeMcpAction('mcp_server_add', { name: name.trim(), config }, getProfile(ctx))
} catch (err: any) {
ctx.status = 503
ctx.body = { error: err.message || 'Failed to add MCP server' }
}
}
export async function updateServer(ctx: Context) {
try {
const name = ctx.params.name as string
const { config } = (ctx.request.body || {}) as Record<string, unknown>
if (!name || !isValidServerName(name)) {
ctx.status = 400
ctx.body = { error: 'Valid server name is required' }
return
}
if (!config || typeof config !== 'object') {
ctx.status = 400
ctx.body = { error: 'config object is required' }
return
}
ctx.body = await bridgeMcpAction('mcp_server_update', { name, config }, getProfile(ctx))
} catch (err: any) {
ctx.status = 503
ctx.body = { error: err.message || 'Failed to update MCP server' }
}
}
export async function removeServer(ctx: Context) {
try {
const name = ctx.params.name as string
if (!name || !isValidServerName(name)) {
ctx.status = 400
ctx.body = { error: 'Valid server name is required' }
return
}
ctx.body = await bridgeMcpAction('mcp_server_remove', { name }, getProfile(ctx))
} catch (err: any) {
ctx.status = 503
ctx.body = { error: err.message || 'Failed to remove MCP server' }
}
}
export async function testServer(ctx: Context) {
try {
const name = ctx.params.name as string
if (!name || !isValidServerName(name)) {
ctx.status = 400
ctx.body = { error: 'Valid server name is required' }
return
}
ctx.body = await bridgeMcpAction('mcp_server_test', { name }, getProfile(ctx))
} catch (err: any) {
ctx.status = 503
ctx.body = { error: err.message || 'Failed to test MCP server' }
}
}
export async function listTools(ctx: Context) {
try {
const server = ctx.query.server as string | undefined
const payload = server ? { server } : {}
ctx.body = await bridgeMcpAction('mcp_tools_list', payload, getProfile(ctx))
} catch (err: any) {
ctx.status = 503
ctx.body = { error: err.message || 'MCP bridge not available' }
}
}
export async function reloadMcp(ctx: Context) {
try {
const server = ctx.query.server as string | undefined
const payload = server ? { server } : {}
ctx.body = await bridgeMcpAction('mcp_reload', payload, getProfile(ctx))
} catch (err: any) {
ctx.status = 503
ctx.body = { error: err.message || 'Failed to reload MCP' }
}
}