[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>
This commit is contained in:
ekko
2026-05-31 09:00:38 +08:00
committed by GitHub
parent 9df79c33be
commit c998a53566
29 changed files with 703 additions and 189 deletions
+5 -2
View File
@@ -52,8 +52,11 @@ export async function fetchMcpServers(): Promise<McpServersResponse> {
return request<McpServersResponse>('/api/hermes/mcp/servers')
}
export async function fetchMcpTools(server?: string): Promise<McpToolsResponse> {
const query = server ? `?server=${encodeURIComponent(server)}` : ''
export async function fetchMcpTools(server?: string, raw?: boolean): Promise<McpToolsResponse> {
const params = new URLSearchParams()
if (server) params.set('server', server)
if (raw) params.set('raw', '1')
const query = params.toString() ? `?${params.toString()}` : ''
return request<McpToolsResponse>(`/api/hermes/mcp/tools${query}`)
}
@@ -15,6 +15,7 @@ const emit = defineEmits<{
reload: [name: string]
remove: [server: McpServerInfo]
toggleEnabled: [server: McpServerInfo]
manageTools: [server: McpServerInfo]
}>()
const { t } = useI18n()
@@ -80,6 +81,7 @@ const MAX_VISIBLE_TOOLS = 20
<div class="card-footer">
<div class="card-actions">
<NButton size="tiny" quaternary @click="emit('edit', server)">{{ t('mcp.edit') }}</NButton>
<NButton size="tiny" quaternary :disabled="!server.connected" @click="emit('manageTools', server)">{{ t('mcp.manageTools') }}</NButton>
<NButton size="tiny" quaternary @click="emit('test', server)">{{ t('mcp.test') }}</NButton>
<NButton size="tiny" quaternary @click="emit('reload', server.name)">{{ t('mcp.reload') }}</NButton>
<NPopconfirm @positive-click="emit('remove', server)">
+18
View File
@@ -157,6 +157,24 @@ export default {
invalidConfig: 'Ungültige Konfiguration',
invalidServerConfig: 'Ungültige Serverkonfiguration',
missingCommandOrUrl: 'Muss command oder url enthalten',
manageTools: 'Tools verwalten',
toolsVisibilityTitle: 'Tools-Sichtbarkeit verwalten',
fetchTools: 'Tools-Liste abrufen',
fetchToolsFailed: 'Tools-Liste konnte nicht abgerufen werden',
toolsMode: 'Modus:',
toolsModeAll: 'Alle',
toolsModeInclude: 'Einschließen',
toolsModeExclude: 'Ausschließen',
toolsListHeader: 'Tool-Name',
toolsEmpty: 'Keine Tools verfügbar, bitte zuerst die Tools-Liste abrufen',
toolsSummaryAll: '{count} Tools insgesamt, alle aktiviert',
toolsSummaryInclude: '{total} Tools insgesamt, {count} ausgewählt',
toolsSummaryExclude: '{total} Tools insgesamt, {count} ausgeschlossen',
toolsVisibilitySaved: 'Tools-Sichtbarkeit gespeichert',
toolsSelectAll: 'Alle auswählen',
toolsClearSelection: 'Auswahl löschen',
toolsExcludeAll: 'Alle ausschließen',
toolsClearExcluded: 'Ausschlüsse löschen',
},
// Sidebar
+18
View File
@@ -157,6 +157,24 @@ export default {
invalidConfig: 'Invalid configuration',
invalidServerConfig: 'Invalid server configuration',
missingCommandOrUrl: 'Must have command or url',
manageTools: 'Manage Tools',
toolsVisibilityTitle: 'Tools Visibility Management',
fetchTools: 'Fetch Tools List',
fetchToolsFailed: 'Failed to fetch tools list',
toolsMode: 'Mode:',
toolsModeAll: 'All',
toolsModeInclude: 'Include',
toolsModeExclude: 'Exclude',
toolsListHeader: 'Tool Name',
toolsEmpty: 'No tools available, please fetch tools list first',
toolsSummaryAll: '{count} tools total, all enabled',
toolsSummaryInclude: '{total} tools total, {count} selected',
toolsSummaryExclude: '{total} tools total, {count} excluded',
toolsVisibilitySaved: 'Tools visibility saved',
toolsSelectAll: 'Select all',
toolsClearSelection: 'Clear selection',
toolsExcludeAll: 'Exclude all',
toolsClearExcluded: 'Clear excluded',
},
// Sidebar
+18
View File
@@ -157,6 +157,24 @@ export default {
invalidConfig: 'Configuración no válida',
invalidServerConfig: 'Configuración del servidor no válida',
missingCommandOrUrl: 'Debe incluir command o url',
manageTools: 'Gestionar herramientas',
toolsVisibilityTitle: 'Gestión de visibilidad de herramientas',
fetchTools: 'Obtener lista de herramientas',
fetchToolsFailed: 'Error al obtener la lista de herramientas',
toolsMode: 'Modo:',
toolsModeAll: 'Todas',
toolsModeInclude: 'Incluir',
toolsModeExclude: 'Excluir',
toolsListHeader: 'Nombre de herramienta',
toolsEmpty: 'No hay herramientas disponibles, primero obtenga la lista de herramientas',
toolsSummaryAll: '{count} herramientas en total, todas habilitadas',
toolsSummaryInclude: '{total} herramientas en total, {count} seleccionadas',
toolsSummaryExclude: '{total} herramientas en total, {count} excluidas',
toolsVisibilitySaved: 'Visibilidad de herramientas guardada',
toolsSelectAll: 'Seleccionar todo',
toolsClearSelection: 'Borrar selección',
toolsExcludeAll: 'Excluir todo',
toolsClearExcluded: 'Borrar exclusiones',
},
// Sidebar
+18
View File
@@ -157,6 +157,24 @@ export default {
invalidConfig: 'Configuration invalide',
invalidServerConfig: 'Configuration du serveur invalide',
missingCommandOrUrl: 'Doit contenir command ou url',
manageTools: 'Gérer les outils',
toolsVisibilityTitle: 'Gestion de la visibilité des outils',
fetchTools: 'Récupérer la liste des outils',
fetchToolsFailed: 'Échec de la récupération de la liste des outils',
toolsMode: 'Mode :',
toolsModeAll: 'Tous',
toolsModeInclude: 'Inclure',
toolsModeExclude: 'Exclure',
toolsListHeader: 'Nom de l\'outil',
toolsEmpty: 'Aucun outil disponible, veuillez d\'abord récupérer la liste des outils',
toolsSummaryAll: '{count} outils au total, tous activés',
toolsSummaryInclude: '{total} outils au total, {count} sélectionnés',
toolsSummaryExclude: '{total} outils au total, {count} exclus',
toolsVisibilitySaved: 'Visibilité des outils enregistrée',
toolsSelectAll: 'Tout sélectionner',
toolsClearSelection: 'Effacer la sélection',
toolsExcludeAll: 'Tout exclure',
toolsClearExcluded: 'Effacer les exclusions',
},
// Sidebar
+18
View File
@@ -158,6 +158,24 @@ export default {
invalidConfig: '設定形式が無効です',
invalidServerConfig: 'サーバー設定が無効です',
missingCommandOrUrl: 'command または url が必要です',
manageTools: 'ツール管理',
toolsVisibilityTitle: 'ツール可視性管理',
fetchTools: 'ツールリスト取得',
fetchToolsFailed: 'ツールリストの取得に失敗しました',
toolsMode: 'モード:',
toolsModeAll: 'すべて',
toolsModeInclude: '含める',
toolsModeExclude: '除外',
toolsListHeader: 'ツール名',
toolsEmpty: 'ツールがありません。まずツールリストを取得してください',
toolsSummaryAll: '合計 {count} ツール、すべて有効',
toolsSummaryInclude: '合計 {total} ツール、{count} 選択済み',
toolsSummaryExclude: '合計 {total} ツール、{count} 除外済み',
toolsVisibilitySaved: 'ツール可視性が保存されました',
toolsSelectAll: 'すべて選択',
toolsClearSelection: '選択をクリア',
toolsExcludeAll: 'すべて除外',
toolsClearExcluded: '除外をクリア',
},
sidebar: {
+18
View File
@@ -158,6 +158,24 @@ export default {
invalidConfig: '올바르지 않은 설정',
invalidServerConfig: '서버 설정이 올바르지 않습니다',
missingCommandOrUrl: 'command 또는 url이 필요합니다',
manageTools: '도구 관리',
toolsVisibilityTitle: '도구 가시성 관리',
fetchTools: '도구 목록 가져오기',
fetchToolsFailed: '도구 목록을 가져오지 못했습니다',
toolsMode: '모드:',
toolsModeAll: '전체',
toolsModeInclude: '포함',
toolsModeExclude: '제외',
toolsListHeader: '도구 이름',
toolsEmpty: '도구가 없습니다. 먼저 도구 목록을 가져오세요',
toolsSummaryAll: '총 {count}개 도구, 모두 활성화',
toolsSummaryInclude: '총 {total}개 도구, {count}개 선택됨',
toolsSummaryExclude: '총 {total}개 도구, {count}개 제외됨',
toolsVisibilitySaved: '도구 가시성이 저장되었습니다',
toolsSelectAll: '모두 선택',
toolsClearSelection: '선택 지우기',
toolsExcludeAll: '모두 제외',
toolsClearExcluded: '제외 항목 지우기',
},
sidebar: {
+18
View File
@@ -157,6 +157,24 @@ export default {
invalidConfig: 'Configuração inválida',
invalidServerConfig: 'Configuração do servidor inválida',
missingCommandOrUrl: 'Deve conter command ou url',
manageTools: 'Gerenciar ferramentas',
toolsVisibilityTitle: 'Gerenciamento de visibilidade de ferramentas',
fetchTools: 'Obter lista de ferramentas',
fetchToolsFailed: 'Falha ao obter lista de ferramentas',
toolsMode: 'Modo:',
toolsModeAll: 'Todas',
toolsModeInclude: 'Incluir',
toolsModeExclude: 'Excluir',
toolsListHeader: 'Nome da ferramenta',
toolsEmpty: 'Nenhuma ferramenta disponível, obtenha a lista de ferramentas primeiro',
toolsSummaryAll: '{count} ferramentas no total, todas habilitadas',
toolsSummaryInclude: '{total} ferramentas no total, {count} selecionadas',
toolsSummaryExclude: '{total} ferramentas no total, {count} excluídas',
toolsVisibilitySaved: 'Visibilidade das ferramentas salva',
toolsSelectAll: 'Selecionar tudo',
toolsClearSelection: 'Limpar seleção',
toolsExcludeAll: 'Excluir tudo',
toolsClearExcluded: 'Limpar exclusões',
},
// Sidebar
+18
View File
@@ -158,6 +158,24 @@ export default {
invalidConfig: '配置格式錯誤',
invalidServerConfig: '伺服器配置無效',
missingCommandOrUrl: '必須包含 command 或 url',
manageTools: '管理工具',
toolsVisibilityTitle: '工具可見性管理',
fetchTools: '獲取工具列表',
fetchToolsFailed: '獲取工具列表失敗',
toolsMode: '模式:',
toolsModeAll: '全部',
toolsModeInclude: '包含',
toolsModeExclude: '排除',
toolsListHeader: '工具名稱',
toolsEmpty: '暫無工具,請先獲取工具列表',
toolsSummaryAll: '共 {count} 個工具,全部啟用',
toolsSummaryInclude: '共 {total} 個工具,已選 {count} 個',
toolsSummaryExclude: '共 {total} 個工具,已排除 {count} 個',
toolsVisibilitySaved: '工具可見性已儲存',
toolsSelectAll: '全選',
toolsClearSelection: '取消全選',
toolsExcludeAll: '全部排除',
toolsClearExcluded: '清空排除',
},
sidebar: {
+18
View File
@@ -158,6 +158,24 @@ export default {
invalidConfig: '配置格式错误',
invalidServerConfig: '服务器配置无效',
missingCommandOrUrl: '必须包含 command 或 url',
manageTools: '管理工具',
toolsVisibilityTitle: '工具可见性管理',
fetchTools: '获取工具列表',
fetchToolsFailed: '获取工具列表失败',
toolsMode: '模式:',
toolsModeAll: '全部',
toolsModeInclude: '包含',
toolsModeExclude: '排除',
toolsListHeader: '工具名称',
toolsEmpty: '暂无工具,请先获取工具列表',
toolsSummaryAll: '共 {count} 个工具,全部启用',
toolsSummaryInclude: '共 {total} 个工具,已选 {count} 个',
toolsSummaryExclude: '共 {total} 个工具,已排除 {count} 个',
toolsVisibilitySaved: '工具可见性已保存',
toolsSelectAll: '全选',
toolsClearSelection: '取消全选',
toolsExcludeAll: '全部排除',
toolsClearExcluded: '清空排除',
},
sidebar: {
@@ -4,11 +4,12 @@ import yaml from 'js-yaml'
import {
NAlert, NButton, NEmpty, NInput, NModal,
NSpin, NRadioGroup, NRadioButton, useMessage,
NCheckbox, NScrollbar,
} from 'naive-ui'
import { useI18n } from 'vue-i18n'
import McpServerCard from '@/components/hermes/mcp/McpServerCard.vue'
import {
fetchMcpServers, mcpServerAdd, mcpServerRemove,
fetchMcpServers, fetchMcpTools, mcpServerAdd, mcpServerRemove,
mcpServerUpdate, mcpServerTest, mcpReload,
type McpServerInfo, type McpServerConfig,
} from '@/api/hermes/mcp'
@@ -29,6 +30,14 @@ const jsonError = ref('')
const saving = ref(false)
const inputMode = ref<'json' | 'yaml'>('json')
// Tools visibility modal
const showToolsModal = ref(false)
const toolsModalServer = ref<McpServerInfo | null>(null)
const toolsMode = ref<'all' | 'include' | 'exclude'>('all')
const selectedTools = ref<string[]>([])
const allTools = ref<string[]>([])
const fetchingTools = ref(false)
const jsonPlaceholder = '{\n "my-server": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"]\n }\n}'
const yamlPlaceholder = 'my-server:\n command: npx\n args:\n - "-y"\n - "@modelcontextprotocol/server-filesystem"\n - "/path"'
@@ -195,15 +204,15 @@ async function loadServers() {
try {
const data = await fetchMcpServers()
servers.value = data.servers ?? []
// Populate toolsByServer from embedded tool_details
// Populate toolsByServer from embedded tool_details, including empty filtered results.
const nextToolsByServer: Record<string, {name: string, description: string}[]> = {}
for (const s of servers.value) {
if (s.tool_details?.length) {
toolsByServer.value[s.name] = s.tool_details.map(t => ({
name: t.name,
description: t.description || '',
}))
}
nextToolsByServer[s.name] = (s.tool_details || []).map(t => ({
name: t.name,
description: t.description || '',
}))
}
toolsByServer.value = nextToolsByServer
// Auto-retry with exponential backoff if enabled servers are still disconnected
const hasPending = servers.value.some(s => s.raw_config.enabled !== false && !s.connected)
if (hasPending && _autoRetryCount < MAX_AUTO_RETRIES) {
@@ -368,6 +377,113 @@ async function handleTest(server: McpServerInfo) {
void loadServers()
function openToolsModal(server: McpServerInfo) {
toolsModalServer.value = server
// Load mode from config
const tools = server.raw_config.tools
if (!tools || (!tools.include && !tools.exclude)) {
toolsMode.value = 'all'
selectedTools.value = [...server.tool_names]
} else if (tools.include) {
toolsMode.value = 'include'
selectedTools.value = [...tools.include]
} else {
toolsMode.value = 'exclude'
selectedTools.value = [...(tools.exclude || [])]
}
allTools.value = [...server.tool_names]
showToolsModal.value = true
}
async function fetchToolsList() {
if (!toolsModalServer.value) return
fetchingTools.value = true
try {
const res = await fetchMcpTools(toolsModalServer.value.name, true)
if (res.ok && res.results?.length) {
const serverResult = res.results.find((r: { server: string }) => r.server === toolsModalServer.value!.name)
if (serverResult?.tools) {
allTools.value = serverResult.tools.map((t: { name: string }) => t.name)
// Reset selection to current mode
if (toolsMode.value === 'all') {
selectedTools.value = [...allTools.value]
}
}
}
} catch (err: any) {
message.error(err.message || t('mcp.fetchToolsFailed'))
} finally {
fetchingTools.value = false
}
}
function handleToolsModeChange(mode: 'all' | 'include' | 'exclude') {
toolsMode.value = mode
if (mode === 'all') {
selectedTools.value = [...allTools.value]
} else {
// Load from config for include/exclude mode
const server = toolsModalServer.value
if (server) {
const tools = server.raw_config.tools
if (mode === 'include' && tools?.include) {
selectedTools.value = [...tools.include]
} else if (mode === 'exclude' && tools?.exclude) {
selectedTools.value = [...tools.exclude]
} else {
selectedTools.value = []
}
}
}
}
function handleToolCheck(tool: string, checked: boolean) {
if (checked) {
if (!selectedTools.value.includes(tool)) {
selectedTools.value.push(tool)
}
} else {
selectedTools.value = selectedTools.value.filter(t => t !== tool)
}
}
function selectAllTools() {
selectedTools.value = [...allTools.value]
}
function clearSelectedTools() {
selectedTools.value = []
}
async function saveToolsVisibility() {
const server = toolsModalServer.value
if (!server) return
const config = { ...server.raw_config }
if (toolsMode.value === 'all') {
delete config.tools
} else if (toolsMode.value === 'include') {
config.tools = { include: [...selectedTools.value] }
} else {
config.tools = { exclude: [...selectedTools.value] }
}
try {
const res = await mcpServerUpdate(server.name, config)
if (res.ok) {
message.success(t('mcp.toolsVisibilitySaved'))
showToolsModal.value = false
await loadServers()
scheduleReload()
} else {
message.error(res.error || t('mcp.updateFailed'))
}
} catch (err: any) {
message.error(err.message || t('mcp.updateFailed'))
}
}
</script>
<template>
@@ -435,6 +551,7 @@ void loadServers()
@reload="handleReload"
@remove="handleRemove"
@toggle-enabled="handleToggleEnabled"
@manage-tools="openToolsModal"
/>
</div>
<NEmpty v-else-if="!loading" :description="t('mcp.empty')" />
@@ -465,6 +582,67 @@ void loadServers()
</NButton>
</div>
</NModal>
<!-- Tools Visibility Modal -->
<NModal v-model:show="showToolsModal" :title="t('mcp.toolsVisibilityTitle')" preset="card" :style="{ width: 'min(480px, calc(100vw - 32px))' }">
<div v-if="toolsModalServer" class="tools-modal-content">
<div class="tools-modal-header">
<span class="server-name-label">{{ toolsModalServer.name }}</span>
<NButton size="small" :loading="fetchingTools" @click="fetchToolsList">
{{ t('mcp.fetchTools') }}
</NButton>
</div>
<div class="tools-mode-selector">
<span class="mode-label">{{ t('mcp.toolsMode') }}</span>
<NRadioGroup v-model:value="toolsMode" size="small" @update:value="handleToolsModeChange">
<NRadioButton value="all">{{ t('mcp.toolsModeAll') }}</NRadioButton>
<NRadioButton value="include">{{ t('mcp.toolsModeInclude') }}</NRadioButton>
<NRadioButton value="exclude">{{ t('mcp.toolsModeExclude') }}</NRadioButton>
</NRadioGroup>
</div>
<div class="tools-list-container">
<div class="tools-list-header">
<span>{{ t('mcp.toolsListHeader') }}</span>
<div v-if="toolsMode !== 'all'" class="tools-list-actions">
<NButton size="tiny" quaternary @click="selectAllTools">
{{ toolsMode === 'exclude' ? t('mcp.toolsExcludeAll') : t('mcp.toolsSelectAll') }}
</NButton>
<NButton size="tiny" quaternary @click="clearSelectedTools">
{{ toolsMode === 'exclude' ? t('mcp.toolsClearExcluded') : t('mcp.toolsClearSelection') }}
</NButton>
</div>
</div>
<NScrollbar style="max-height: 300px;">
<div v-if="allTools.length" class="tools-checkbox-list">
<div v-for="tool in allTools" :key="tool" class="tool-checkbox-item" :class="{ disabled: toolsMode === 'all' }">
<NCheckbox
:checked="selectedTools.includes(tool)"
:disabled="toolsMode === 'all'"
@update:checked="(val: boolean) => handleToolCheck(tool, val)"
/>
<span class="tool-name" :title="tool">{{ tool }}</span>
</div>
</div>
<div v-else class="tools-empty">
<span class="muted">{{ t('mcp.toolsEmpty') }}</span>
</div>
</NScrollbar>
</div>
<div class="tools-summary">
<span v-if="toolsMode === 'all'">{{ t('mcp.toolsSummaryAll', { count: allTools.length }) }}</span>
<span v-else-if="toolsMode === 'include'">{{ t('mcp.toolsSummaryInclude', { count: selectedTools.length, total: allTools.length }) }}</span>
<span v-else>{{ t('mcp.toolsSummaryExclude', { count: selectedTools.length, total: allTools.length }) }}</span>
</div>
<div class="modal-actions">
<NButton @click="showToolsModal = false">{{ t('mcp.cancel') }}</NButton>
<NButton type="primary" @click="saveToolsVisibility">{{ t('mcp.save') }}</NButton>
</div>
</div>
</NModal>
</div>
</template>
@@ -624,4 +802,110 @@ void loadServers()
grid-template-columns: 1fr;
}
}
.tools-modal-content {
display: flex;
flex-direction: column;
gap: 16px;
}
.tools-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.server-name-label {
font-weight: 600;
font-size: 14px;
}
.tools-mode-selector {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.mode-label {
font-size: 13px;
color: $text-muted;
white-space: nowrap;
}
.tools-list-container {
border: 1px solid $border-color;
border-radius: 8px;
overflow: hidden;
}
.tools-list-header {
padding: 10px 12px;
background: $bg-secondary;
font-size: 12px;
font-weight: 600;
color: $text-muted;
text-transform: uppercase;
letter-spacing: 0.04em;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.tools-list-actions {
display: flex;
align-items: center;
gap: 4px;
text-transform: none;
letter-spacing: 0;
font-weight: 400;
}
.tools-checkbox-list {
padding: 8px 0;
}
.tool-checkbox-item {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 12px;
transition: background 0.15s;
&:hover {
background: $bg-secondary;
}
&.disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.tool-name {
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tools-empty {
padding: 24px 12px;
text-align: center;
}
.tools-summary {
font-size: 12px;
color: $text-muted;
text-align: center;
}
@media (max-width: $breakpoint-mobile) {
.tools-mode-selector {
flex-direction: column;
align-items: flex-start;
}
}
</style>
@@ -97,7 +97,10 @@ export async function testServer(ctx: Context) {
export async function listTools(ctx: Context) {
try {
const server = ctx.query.server as string | undefined
const payload = server ? { server } : {}
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
@@ -609,8 +609,8 @@ export class AgentBridgeClient {
return this.request({ action: 'mcp_server_test', name, ...(profile ? { profile } : {}) }, { timeoutMs: 180_000 })
}
mcpTools(server?: string, profile?: string): Promise<McpActionResponse> {
return this.request({ action: 'mcp_tools_list', ...(server ? { server } : {}), ...(profile ? { profile } : {}) })
mcpTools(server?: string, profile?: string, raw?: boolean): Promise<McpActionResponse> {
return this.request({ action: 'mcp_tools_list', ...(server ? { server } : {}), ...(profile ? { profile } : {}), ...(raw ? { raw } : {}) })
}
mcpReload(server?: string, profile?: string): Promise<McpActionResponse> {
@@ -2427,16 +2427,18 @@ class BridgeServer:
cfg = getattr(task, "_config", {})
# Build filtered tool_details (name + description) for card display
srv_cfg = mcp_configs.get(name, {}) if isinstance(mcp_configs.get(name), dict) else {}
tools_filter = srv_cfg.get("tools") or {}
tools_filter = srv_cfg.get("tools") if isinstance(srv_cfg.get("tools"), dict) else {}
has_include_filter = "include" in tools_filter
has_exclude_filter = "exclude" in tools_filter
include_set = set(tools_filter.get("include") or [])
exclude_set = set(tools_filter.get("exclude") or [])
tool_details = []
try:
for mcp_tool in getattr(task, "_tools", []):
tname = getattr(mcp_tool, "name", "?")
if include_set and tname not in include_set:
if has_include_filter and tname not in include_set:
continue
if exclude_set and tname in exclude_set:
if has_exclude_filter and tname in exclude_set:
continue
tool_details.append({
"name": tname,
@@ -2576,6 +2578,7 @@ class BridgeServer:
def _mcp_tools_list(self, req: dict, profile: str, _servers, _lock) -> dict[str, Any]:
server_filter = str(req.get("server") or "").strip() or None
raw_mode = bool(req.get("raw")) # Return unfiltered tools for visibility management
results = []
config = self._read_mcp_config(profile)
@@ -2592,13 +2595,17 @@ class BridgeServer:
registered = set(getattr(task, "_registered_tool_names", None) or [])
tools = []
srv_cfg = mcp_configs.get(sname, {}) if isinstance(mcp_configs.get(sname), dict) else {}
tools_filter = srv_cfg.get("tools") or {}
tools_filter = srv_cfg.get("tools") if isinstance(srv_cfg.get("tools"), dict) else {}
has_include_filter = "include" in tools_filter
has_exclude_filter = "exclude" in tools_filter
include_set = set(tools_filter.get("include") or [])
exclude_set = set(tools_filter.get("exclude") or [])
def _should_include(tn):
if include_set:
if raw_mode:
return True # Skip filter in raw mode
if has_include_filter:
return tn in include_set
if exclude_set:
if has_exclude_filter:
return tn not in exclude_set
return True
try:
+1 -1
View File
@@ -54,7 +54,7 @@ export async function bridgeMcpAction(
break
}
case 'mcp_tools_list':
raw = await client.mcpTools(payload.server as string | undefined, profile)
raw = await client.mcpTools(payload.server as string | undefined, profile, payload.raw as boolean | undefined)
break
case 'mcp_reload':
raw = await client.mcpReload(payload.server as string | undefined, profile)