Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/desktop/src/app/session/hooks/use-message-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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']
Expand Down
49 changes: 47 additions & 2 deletions apps/desktop/src/app/settings/mcp-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<HermesConfigRecord | null>(null)
const [selected, setSelected] = useState<string | null>(null)
const [name, setName] = useState('')
Expand All @@ -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}`,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 (
<button
Expand All @@ -216,10 +253,18 @@ export function McpSettings({ gateway, onConfigSaved }: McpSettingsProps) {
onClick={() => setSelected(serverName)}
type="button"
>
<div className="truncate text-sm font-medium">{serverName}</div>
<div className="flex items-center gap-1.5 truncate text-sm font-medium">
{isConnected && (
<span className="inline-block size-2 shrink-0 rounded-full bg-emerald-500" title="Connected" />
)}
<span className="truncate">{serverName}</span>
</div>
<div className="mt-1 flex items-center gap-1.5">
<Pill>{transportLabel(server)}</Pill>
{server.disabled === true && <Pill>{m.disabled}</Pill>}
{runtime && !runtime.connected && !runtime.disabled && (
<span className="text-red-500 text-xs">disconnected</span>
)}
</div>
</button>
)
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/chat-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/store/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,16 @@ export const $currentPersonality = atom('')
export const $availablePersonalities = atom<string[]>([])
export const $introSeed = atom(0)
export const $contextSuggestions = atom<ContextSuggestion[]>([])
export interface McpServerStatusItem {
connected: boolean
disabled: boolean
name: string
tools: number
transport: string
}

export const $mcpStatus = atom<McpServerStatusItem[]>([])

export const $modelPickerOpen = atom(false)
export const $sessionPickerOpen = atom(false)

Expand Down Expand Up @@ -257,6 +267,7 @@ export const setCurrentPersonality = (next: Updater<string>) => updateAtom($curr
export const setAvailablePersonalities = (next: Updater<string[]>) => updateAtom($availablePersonalities, next)
export const setIntroSeed = (next: Updater<number>) => updateAtom($introSeed, next)
export const setContextSuggestions = (next: Updater<ContextSuggestion[]>) => updateAtom($contextSuggestions, next)
export const setMcpStatus = (next: Updater<McpServerStatusItem[]>) => updateAtom($mcpStatus, next)
export const setModelPickerOpen = (next: Updater<boolean>) => updateAtom($modelPickerOpen, next)
export const setSessionPickerOpen = (next: Updater<boolean>) => updateAtom($sessionPickerOpen, next)

Expand Down
15 changes: 15 additions & 0 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions tui_gateway/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down