Files
Hermes-ui/packages/server/src/controllers/hermes/mcp.ts
T
c998a53566 [codex] add MCP tools visibility management (#1170)
* feat(mcp): add tools visibility management

## Features
- Tools visibility modal with 3 modes: All, Include, Exclude
- 'Manage Tools' button on McpServerCard (enabled only when connected)
- 'Fetch Tools List' button to refresh available tools (raw mode)
- Responsive design for mobile (480px), tablet (768px), desktop (1280px)
- i18n translations for 9 languages (zh/en/zh-TW/ja/ko/de/es/fr/pt)

## Technical Details
- Add raw parameter to fetchMcpTools API for unfiltered tools
- Pass raw parameter through controller → bridgeMcpAction → client
- Backend _mcp_tools_list supports raw_mode to skip include/exclude filter
- 28 MCP unit tests pass (23 controller + 5 bridge action)

## Files Changed
- McpManagerView.vue: Tools visibility modal with mode selector
- McpServerCard.vue: Add manage tools button
- mcp.ts (client): Add raw parameter to fetchMcpTools
- mcp.ts (controller): Pass raw parameter to bridge
- mcp.ts (services): Pass raw parameter to client.mcpTools
- client.ts: Add raw parameter to mcpTools
- hermes_bridge.py: Support raw_mode in _mcp_tools_list
- 9 locale files: Add 14 translation keys each
- mcp-controller.test.ts: Add 3 new test cases
- bridge-mcp-action.test.ts: New test file for parameter passing

* Delete projects directory

chore: remove accidentally committed projects/ directory

* fix MCP tools visibility edge cases

* remove MCP docs screenshots

---------

Co-authored-by: Crafter-feng <succeed_happu@163.com>
Co-authored-by: Crafter-feng <37255449+Crafter-feng@users.noreply.github.com>
2026-05-31 09:00:38 +08:00

121 lines
3.8 KiB
TypeScript

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 raw = ctx.query.raw === '1' || ctx.query.raw === 'true'
const payload: Record<string, any> = {}
if (server) payload.server = server
if (raw) payload.raw = true
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' }
}
}