diff --git a/apps/desktop/src/app/session/hooks/use-message-stream.ts b/apps/desktop/src/app/session/hooks/use-message-stream.ts index 75ff43b5ee86..c19360c7fdf2 100644 --- a/apps/desktop/src/app/session/hooks/use-message-stream.ts +++ b/apps/desktop/src/app/session/hooks/use-message-stream.ts @@ -33,9 +33,11 @@ import { setCurrentReasoningEffort, setCurrentServiceTier, setCurrentUsage, + setMcpStatus, setTurnStartedAt, setYoloActive } from '@/store/session' +import type { McpServerStatusItem } from '@/store/session' import { clearSessionSubagents, pruneDelegateFallbackSubagents, upsertSubagent } from '@/store/subagents' import { recordToolDiff } from '@/store/tool-diffs' import type { RpcEvent } from '@/types/hermes' @@ -730,6 +732,10 @@ export function useMessageStream({ void refreshHermesConfig() + if (Array.isArray(payload?.mcp_servers)) { + setMcpStatus(payload.mcp_servers as McpServerStatusItem[]) + } + if (modelChanged || providerChanged) { void queryClient.invalidateQueries({ queryKey: explicitSid && sessionId ? ['model-options', sessionId] : ['model-options'] diff --git a/apps/desktop/src/app/settings/mcp-settings.tsx b/apps/desktop/src/app/settings/mcp-settings.tsx index d342428ed659..58239ea613c8 100644 --- a/apps/desktop/src/app/settings/mcp-settings.tsx +++ b/apps/desktop/src/app/settings/mcp-settings.tsx @@ -9,7 +9,8 @@ import { useI18n } from '@/i18n' import { Wrench } from '@/lib/icons' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' -import { $activeSessionId } from '@/store/session' +import { $activeSessionId, $mcpStatus, setMcpStatus } from '@/store/session' +import type { McpServerStatusItem } from '@/store/session' import type { HermesConfigRecord } from '@/types/hermes' import { EmptyState, LoadingState, Pill, SettingsContent } from './primitives' @@ -47,6 +48,7 @@ export function McpSettings({ gateway, onConfigSaved }: McpSettingsProps) { const { t } = useI18n() const m = t.settings.mcp const activeSessionId = useStore($activeSessionId) + const mcpRuntimeStatus = useStore($mcpStatus) const [config, setConfig] = useState(null) const [selected, setSelected] = useState(null) const [name, setName] = useState('') @@ -72,9 +74,33 @@ export function McpSettings({ gateway, onConfigSaved }: McpSettingsProps) { return () => void (cancelled = true) }, []) + useEffect(() => { + if (!gateway) { + return + } + + let cancelled = false + + gateway + .request<{ servers: McpServerStatusItem[] }>('mcp.status') + .then(res => { + if (!cancelled && Array.isArray(res?.servers)) { + setMcpStatus(res.servers) + } + }) + .catch(() => {}) + + return () => void (cancelled = true) + }, [gateway]) + const servers = useMemo(() => getServers(config), [config]) const names = useMemo(() => Object.keys(servers).sort(), [servers]) + const mcpStatusByName = useMemo( + () => Object.fromEntries(mcpRuntimeStatus.map(s => [s.name, s])), + [mcpRuntimeStatus] + ) + useDeepLinkHighlight({ block: 'nearest', elementId: serverName => `mcp-server-${serverName}`, @@ -176,6 +202,15 @@ export function McpSettings({ gateway, onConfigSaved }: McpSettingsProps) { confirm: true, session_id: activeSessionId ?? undefined }) + + try { + const status = await gateway.request<{ servers: McpServerStatusItem[] }>('mcp.status') + + if (Array.isArray(status?.servers)) { + setMcpStatus(status.servers) + } + } catch {} + notify({ kind: 'success', title: m.reloadedTitle, message: m.reloadedMessage }) } catch (err) { notifyError(err, m.reloadFailed) @@ -204,6 +239,8 @@ export function McpSettings({ gateway, onConfigSaved }: McpSettingsProps) { {names.map(serverName => { const server = servers[serverName] const active = selected === serverName + const runtime = mcpStatusByName[serverName] + const isConnected = runtime?.connected === true return ( ) diff --git a/apps/desktop/src/lib/chat-messages.ts b/apps/desktop/src/lib/chat-messages.ts index 5e3a725f303f..780b0c1f19eb 100644 --- a/apps/desktop/src/lib/chat-messages.ts +++ b/apps/desktop/src/lib/chat-messages.ts @@ -61,6 +61,8 @@ export type GatewayEventPayload = { // secret.request (skill credential capture) env_var?: string prompt?: string + // session.info mcp status + mcp_servers?: unknown[] // terminal.read.request (GUI agent reading the in-app terminal pane) start?: number count?: number diff --git a/apps/desktop/src/store/session.ts b/apps/desktop/src/store/session.ts index ed28b92cb883..ef879af618b2 100644 --- a/apps/desktop/src/store/session.ts +++ b/apps/desktop/src/store/session.ts @@ -207,6 +207,16 @@ export const $currentPersonality = atom('') export const $availablePersonalities = atom([]) export const $introSeed = atom(0) export const $contextSuggestions = atom([]) +export interface McpServerStatusItem { + connected: boolean + disabled: boolean + name: string + tools: number + transport: string +} + +export const $mcpStatus = atom([]) + export const $modelPickerOpen = atom(false) export const $sessionPickerOpen = atom(false) @@ -257,6 +267,7 @@ export const setCurrentPersonality = (next: Updater) => updateAtom($curr export const setAvailablePersonalities = (next: Updater) => updateAtom($availablePersonalities, next) export const setIntroSeed = (next: Updater) => updateAtom($introSeed, next) export const setContextSuggestions = (next: Updater) => updateAtom($contextSuggestions, next) +export const setMcpStatus = (next: Updater) => updateAtom($mcpStatus, next) export const setModelPickerOpen = (next: Updater) => updateAtom($modelPickerOpen, next) export const setSessionPickerOpen = (next: Updater) => updateAtom($sessionPickerOpen, next) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 534410559589..fc4263041ff2 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -10303,6 +10303,21 @@ def cmd_dashboard(args): # the missing-provider state if it matters. print(f"⚠ Plugin discovery failed: {exc}", file=sys.stderr) + # Discover MCP tools in the background so the dashboard/desktop backend + # has them available for chat sessions via the embedded gateway + # WebSocket. The CLI chat path does this in _prepare_agent_startup(), + # but the dashboard command does not call that function, so we trigger + # it here before start_server(). A background thread avoids blocking + # dashboard startup when an MCP server is slow to respond. + try: + from hermes_cli.mcp_startup import start_background_mcp_discovery + start_background_mcp_discovery( + logger=logger, + thread_name="dashboard-mcp-discovery", + ) + except Exception as exc: + print(f"⚠ Background MCP tool discovery failed: {exc}", file=sys.stderr) + from hermes_cli.web_server import start_server # The in-browser Chat tab (the embedded TUI over PTY/WebSocket) is always diff --git a/tui_gateway/server.py b/tui_gateway/server.py index d932e98510f5..29caaa3f14df 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -7324,6 +7324,16 @@ def _(rid, params: dict) -> dict: return _err(rid, 5015, str(e)) +@method("mcp.status") +def _(rid, _params: dict) -> dict: + try: + from tools.mcp_tool import get_mcp_status + + return _ok(rid, {"servers": get_mcp_status()}) + except Exception as e: + return _err(rid, 5016, str(e)) + + @method("reload.env") def _(rid, params: dict) -> dict: """Re-read ``~/.hermes/.env`` into the gateway process via