diff --git a/src/commands/components/execute/execute.tsx b/src/commands/components/execute/execute.tsx index f2bdde7..93f079d 100644 --- a/src/commands/components/execute/execute.tsx +++ b/src/commands/components/execute/execute.tsx @@ -105,7 +105,20 @@ const defaultHandleMCPAdd = async (data: MCPAddFormData): Promise => { data.isGlobal, ); - await connectServer(data.name); + // Add to store with "connecting" status + appStore.addMcpServer({ + id: data.name, + name: data.name, + status: "disconnected", + description: data.command, + }); + + try { + await connectServer(data.name); + appStore.updateMcpServerStatus(data.name, "connected"); + } catch { + appStore.updateMcpServerStatus(data.name, "error"); + } }; const defaultHandleBrainSetJwtToken = async (jwtToken: string): Promise => { diff --git a/src/tui-solid/app.tsx b/src/tui-solid/app.tsx index de425d8..fa99c35 100644 --- a/src/tui-solid/app.tsx +++ b/src/tui-solid/app.tsx @@ -28,6 +28,7 @@ import { ToastProvider, Toast, useToast } from "@tui-solid/ui/toast"; import { Home } from "@tui-solid/routes/home"; import { Session } from "@tui-solid/routes/session"; import type { TuiInput, TuiOutput } from "@tui-solid/types"; +import type { MCPServerDisplay } from "@/types/tui"; import type { PermissionScope, LearningScope } from "@/types/tui"; import type { MCPAddFormData } from "@/types/mcp"; @@ -37,13 +38,6 @@ interface AgentOption { description?: string; } -interface MCPServer { - id: string; - name: string; - status: "connected" | "disconnected" | "error"; - description?: string; -} - interface AppProps extends TuiInput { onExit: (output: TuiOutput) => void; onSubmit: (input: string) => Promise; @@ -72,7 +66,7 @@ interface AppProps extends TuiInput { } | null; agents?: AgentOption[]; currentAgent?: string; - mcpServers?: MCPServer[]; + mcpServers?: MCPServerDisplay[]; files?: string[]; } @@ -450,7 +444,7 @@ export interface TuiRenderOptions extends TuiInput { } | null; agents?: AgentOption[]; currentAgent?: string; - mcpServers?: MCPServer[]; + mcpServers?: MCPServerDisplay[]; files?: string[]; } diff --git a/src/tui-solid/components/mcp-select.tsx b/src/tui-solid/components/mcp-select.tsx index e458510..f2a8c73 100644 --- a/src/tui-solid/components/mcp-select.tsx +++ b/src/tui-solid/components/mcp-select.tsx @@ -2,23 +2,17 @@ import { createSignal, createMemo, For, Show } from "solid-js"; import { useKeyboard } from "@opentui/solid"; import { TextAttributes } from "@opentui/core"; import { useTheme } from "@tui-solid/context/theme"; - -interface MCPServer { - id: string; - name: string; - status: "connected" | "disconnected" | "error"; - description?: string; -} +import type { MCPServerDisplay } from "@/types/tui"; interface MCPSelectProps { - servers: MCPServer[]; + servers: MCPServerDisplay[]; onSelect: (serverId: string) => void; onAddNew: () => void; onClose: () => void; isActive?: boolean; } -const STATUS_COLORS: Record = { +const STATUS_COLORS: Record = { connected: "success", disconnected: "textDim", error: "error", diff --git a/src/tui-solid/context/app.tsx b/src/tui-solid/context/app.tsx index db1f0e7..25a9eed 100644 --- a/src/tui-solid/context/app.tsx +++ b/src/tui-solid/context/app.tsx @@ -14,6 +14,7 @@ import type { CommandMenuState, StreamingLogState, SuggestionState, + MCPServerDisplay, } from "@/types/tui"; import type { ProviderModel } from "@/types/providers"; import type { BrainConnectionStatus, BrainUser } from "@/types/brain"; @@ -45,6 +46,7 @@ interface AppStore { streamingLog: StreamingLogState; suggestions: SuggestionState; cascadeEnabled: boolean; + mcpServers: MCPServerDisplay[]; brain: { status: BrainConnectionStatus; user: BrainUser | null; @@ -89,6 +91,7 @@ interface AppContextValue { streamingLogIsActive: Accessor; suggestions: Accessor; cascadeEnabled: Accessor; + mcpServers: Accessor; brain: Accessor<{ status: BrainConnectionStatus; user: BrainUser | null; @@ -184,6 +187,11 @@ interface AppContextValue { setBrainShowBanner: (show: boolean) => void; dismissBrainBanner: () => void; + // MCP actions + setMcpServers: (servers: MCPServerDisplay[]) => void; + addMcpServer: (server: MCPServerDisplay) => void; + updateMcpServerStatus: (id: string, status: MCPServerDisplay["status"]) => void; + // Computed isInputLocked: () => boolean; } @@ -249,6 +257,7 @@ export const { provider: AppStoreProvider, use: useAppStore } = streamingLog: createInitialStreamingState(), suggestions: createInitialSuggestionState(), cascadeEnabled: true, + mcpServers: [], brain: { status: "disconnected" as BrainConnectionStatus, user: null, @@ -303,6 +312,7 @@ export const { provider: AppStoreProvider, use: useAppStore } = const streamingLogIsActive = (): boolean => store.streamingLog.isStreaming; const suggestions = (): SuggestionState => store.suggestions; const cascadeEnabled = (): boolean => store.cascadeEnabled; + const mcpServers = (): MCPServerDisplay[] => store.mcpServers; const brain = () => store.brain; // Mode actions @@ -522,6 +532,36 @@ export const { provider: AppStoreProvider, use: useAppStore } = setStore("brain", { ...store.brain, showBanner: false }); }; + // MCP actions + const setMcpServers = (servers: MCPServerDisplay[]): void => { + setStore("mcpServers", servers); + }; + + const addMcpServer = (server: MCPServerDisplay): void => { + setStore( + produce((s) => { + // Replace if exists, otherwise add + const existingIndex = s.mcpServers.findIndex((srv) => srv.id === server.id); + if (existingIndex !== -1) { + s.mcpServers[existingIndex] = server; + } else { + s.mcpServers.push(server); + } + }), + ); + }; + + const updateMcpServerStatus = (id: string, status: MCPServerDisplay["status"]): void => { + setStore( + produce((s) => { + const server = s.mcpServers.find((srv) => srv.id === id); + if (server) { + server.status = status; + } + }), + ); + }; + // Session stats actions const startThinking = (): void => { setStore("sessionStats", { @@ -758,6 +798,7 @@ export const { provider: AppStoreProvider, use: useAppStore } = streamingLogIsActive, suggestions, cascadeEnabled, + mcpServers, brain, // Mode actions @@ -820,6 +861,11 @@ export const { provider: AppStoreProvider, use: useAppStore } = setBrainShowBanner, dismissBrainBanner, + // MCP actions + setMcpServers, + addMcpServer, + updateMcpServerStatus, + // Session stats actions startThinking, stopThinking, @@ -887,6 +933,7 @@ const defaultAppState = { isCompacting: false, streamingLog: createInitialStreamingState(), suggestions: createInitialSuggestionState(), + mcpServers: [] as MCPServerDisplay[], brain: { status: "disconnected" as BrainConnectionStatus, user: null, @@ -926,6 +973,7 @@ export const appStore = { isCompacting: storeRef.isCompacting(), streamingLog: storeRef.streamingLog(), suggestions: storeRef.suggestions(), + mcpServers: storeRef.mcpServers(), brain: storeRef.brain(), }; }, @@ -1134,4 +1182,19 @@ export const appStore = { if (!storeRef) return; storeRef.dismissBrainBanner(); }, + + setMcpServers: (servers: MCPServerDisplay[]): void => { + if (!storeRef) return; + storeRef.setMcpServers(servers); + }, + + addMcpServer: (server: MCPServerDisplay): void => { + if (!storeRef) return; + storeRef.addMcpServer(server); + }, + + updateMcpServerStatus: (id: string, status: MCPServerDisplay["status"]): void => { + if (!storeRef) return; + storeRef.updateMcpServerStatus(id, status); + }, }; diff --git a/src/tui-solid/routes/session.tsx b/src/tui-solid/routes/session.tsx index b1f229f..a56e2c1 100644 --- a/src/tui-solid/routes/session.tsx +++ b/src/tui-solid/routes/session.tsx @@ -1,4 +1,4 @@ -import { Show, Switch, Match, createSignal } from "solid-js"; +import { Show, Switch, Match, createSignal, createMemo, onMount } from "solid-js"; import { useTheme } from "@tui-solid/context/theme"; import { useAppStore } from "@tui-solid/context/app"; import { Header } from "@tui-solid/components/header"; @@ -23,7 +23,7 @@ import { CenteredModal } from "@tui-solid/components/centered-modal"; import { DebugLogPanel } from "@tui-solid/components/debug-log-panel"; import { BrainMenu } from "@tui-solid/components/brain-menu"; import { BRAIN_DISABLED } from "@constants/brain"; -import type { PermissionScope, LearningScope, InteractionMode } from "@/types/tui"; +import type { PermissionScope, LearningScope, InteractionMode, MCPServerDisplay } from "@/types/tui"; import type { MCPAddFormData } from "@/types/mcp"; interface AgentOption { @@ -32,13 +32,6 @@ interface AgentOption { description?: string; } -interface MCPServer { - id: string; - name: string; - status: "connected" | "disconnected" | "error"; - description?: string; -} - interface ProviderStatus { available: boolean; error?: string; @@ -72,7 +65,7 @@ interface SessionProps { } | null; agents?: AgentOption[]; currentAgent?: string; - mcpServers?: MCPServer[]; + mcpServers?: MCPServerDisplay[]; files?: string[]; providerStatuses?: Record; providerScores?: Record; @@ -82,6 +75,16 @@ export function Session(props: SessionProps) { const theme = useTheme(); const app = useAppStore(); + // Initialize MCP servers from props on mount + onMount(() => { + if (props.mcpServers && props.mcpServers.length > 0) { + app.setMcpServers(props.mcpServers); + } + }); + + // Use store's mcpServers (reactive, updated when servers are added) + const mcpServers = createMemo(() => app.mcpServers()); + // Local state for help menu const [selectedHelpTopic, setSelectedHelpTopic] = createSignal( null @@ -291,7 +294,7 @@ export function Session(props: SessionProps) {