[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:
@@ -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' }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user