From 26f2e5d305cde10222f0a416f9203ecf746d16b1 Mon Sep 17 00:00:00 2001 From: Cameron Date: Thu, 18 Dec 2025 14:59:02 -0800 Subject: [PATCH] fix: Add missing return statement and closing brace in /mcp command handler (#309) --- src/cli/App.tsx | 15 +- src/cli/commands/mcp.ts | 49 +++--- src/cli/components/McpSelector.tsx | 274 ++++++++++++++++------------- 3 files changed, 187 insertions(+), 151 deletions(-) diff --git a/src/cli/App.tsx b/src/cli/App.tsx index ee298cc..90ec16e 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -32,6 +32,11 @@ import { executeTool, savePermissionRule, } from "../tools/manager"; +import { + handleMcpAdd, + handleMcpUsage, + type McpCommandContext, +} from "./commands/mcp"; import { addCommandResult, handlePin, @@ -42,11 +47,6 @@ import { type ProfileCommandContext, validateProfileLoad, } from "./commands/profile"; -import { - handleMcpAdd, - handleMcpUsage, - type McpCommandContext, -} from "./commands/mcp"; import { AgentSelector } from "./components/AgentSelector"; import { ApprovalDialog } from "./components/ApprovalDialogRich"; import { AssistantMessage } from "./components/AssistantMessageRich"; @@ -1791,6 +1791,8 @@ export default function App({ // Unknown subcommand handleMcpUsage(mcpCtx, msg); + return { submitted: true }; + } // Special handling for /help command - opens help dialog if (trimmed === "/help") { @@ -4658,7 +4660,8 @@ Plan file path: ${planFilePath}`; kind: "command", id: cmdId, input: "/mcp", - output: "Use /mcp add --transport [...] to add a new server", + output: + "Use /mcp add --transport [...] to add a new server", phase: "finished", success: true, }); diff --git a/src/cli/commands/mcp.ts b/src/cli/commands/mcp.ts index e125495..88f7f4e 100644 --- a/src/cli/commands/mcp.ts +++ b/src/cli/commands/mcp.ts @@ -2,9 +2,9 @@ // MCP server command handlers import type { - CreateStreamableHTTPMcpServer, CreateSseMcpServer, CreateStdioMcpServer, + CreateStreamableHTTPMcpServer, } from "@letta-ai/letta-client/resources/mcp-servers/mcp-servers"; import { getClient } from "../../agent/client"; import type { Buffers, Line } from "../helpers/accumulator"; @@ -77,11 +77,11 @@ function parseCommandArgs(commandStr: string): string[] { let current = ""; let inQuotes = false; let quoteChar = ""; - + for (let i = 0; i < commandStr.length; i++) { const char = commandStr[i]; if (!char) continue; // Skip if undefined (shouldn't happen but type safety) - + if ((char === '"' || char === "'") && !inQuotes) { // Start of quoted string inQuotes = true; @@ -101,12 +101,12 @@ function parseCommandArgs(commandStr: string): string[] { current += char; } } - + // Add final argument if any if (current) { args.push(current); } - + return args; } @@ -127,7 +127,7 @@ function parseMcpAddArgs(parts: string[]): McpAddArgs | null { let name: string | null = null; let url: string | null = null; let command: string | null = null; - let args: string[] = []; + const args: string[] = []; const headers: Record = {}; let authToken: string | null = null; @@ -153,9 +153,9 @@ function parseMcpAddArgs(parts: string[]): McpAddArgs | null { // Parse "key: value" or "key=value" const colonMatch = headerValue.match(/^([^:]+):\s*(.+)$/); const equalsMatch = headerValue.match(/^([^=]+)=(.+)$/); - if (colonMatch && colonMatch[1] && colonMatch[2]) { + if (colonMatch?.[1] && colonMatch[2]) { headers[colonMatch[1].trim()] = colonMatch[2].trim(); - } else if (equalsMatch && equalsMatch[1] && equalsMatch[2]) { + } else if (equalsMatch?.[1] && equalsMatch[2]) { headers[equalsMatch[1].trim()] = equalsMatch[2].trim(); } } @@ -194,14 +194,14 @@ function parseMcpAddArgs(parts: string[]): McpAddArgs | null { return null; } - return { - transport, - name, - url: url || null, - command: command || null, - args, - headers, - authToken: authToken || null + return { + transport, + name, + url: url || null, + command: command || null, + args, + headers, + authToken: authToken || null, }; } @@ -220,7 +220,7 @@ export async function handleMcpAdd( ctx.buffersRef, ctx.refreshDerived, msg, - "Usage: /mcp add --transport [--header \"key: value\"] [--auth token]\n\nExamples:\n /mcp add --transport http notion https://mcp.notion.com/mcp\n /mcp add --transport http secure-api https://api.example.com/mcp --header \"Authorization: Bearer token\"", + 'Usage: /mcp add --transport [--header "key: value"] [--auth token]\n\nExamples:\n /mcp add --transport http notion https://mcp.notion.com/mcp\n /mcp add --transport http secure-api https://api.example.com/mcp --header "Authorization: Bearer token"', false, ); return; @@ -253,7 +253,8 @@ export async function handleMcpAdd( mcp_server_type: "streamable_http", server_url: args.url, auth_token: args.authToken, - custom_headers: Object.keys(args.headers).length > 0 ? args.headers : null, + custom_headers: + Object.keys(args.headers).length > 0 ? args.headers : null, }; } else if (args.transport === "sse") { if (!args.url) { @@ -263,7 +264,8 @@ export async function handleMcpAdd( mcp_server_type: "sse", server_url: args.url, auth_token: args.authToken, - custom_headers: Object.keys(args.headers).length > 0 ? args.headers : null, + custom_headers: + Object.keys(args.headers).length > 0 ? args.headers : null, }; } else { // stdio @@ -307,10 +309,10 @@ export async function handleMcpAdd( try { await client.mcpServers.refresh(server.id); - + // Get tool count const tools = await client.mcpServers.tools.list(server.id); - + updateCommandResult( ctx.buffersRef, ctx.refreshDerived, @@ -321,7 +323,8 @@ export async function handleMcpAdd( ); } catch (refreshErr) { // If refresh fails, still show success but warn about tools - const errorMsg = refreshErr instanceof Error ? refreshErr.message : "Unknown error"; + const errorMsg = + refreshErr instanceof Error ? refreshErr.message : "Unknown error"; updateCommandResult( ctx.buffersRef, ctx.refreshDerived, @@ -352,7 +355,7 @@ export function handleMcpUsage(ctx: McpCommandContext, msg: string): void { ctx.buffersRef, ctx.refreshDerived, msg, - "Usage: /mcp [add ...]\n /mcp - list MCP servers\n /mcp add --transport [...] - add a new server\n\nExamples:\n /mcp add --transport http notion https://mcp.notion.com/mcp\n /mcp add --transport http api https://api.example.com --header \"Authorization: Bearer token\"", + 'Usage: /mcp [add ...]\n /mcp - list MCP servers\n /mcp add --transport [...] - add a new server\n\nExamples:\n /mcp add --transport http notion https://mcp.notion.com/mcp\n /mcp add --transport http api https://api.example.com --header "Authorization: Bearer token"', false, ); } diff --git a/src/cli/components/McpSelector.tsx b/src/cli/components/McpSelector.tsx index ee7a99a..d790b73 100644 --- a/src/cli/components/McpSelector.tsx +++ b/src/cli/components/McpSelector.tsx @@ -1,8 +1,7 @@ import type { - McpServerListResponse, - StreamableHTTPMcpServer, SseMcpServer, StdioMcpServer, + StreamableHTTPMcpServer, } from "@letta-ai/letta-client/resources/mcp-servers/mcp-servers"; import type { Tool } from "@letta-ai/letta-client/resources/tools"; import { Box, Text, useInput } from "ink"; @@ -75,11 +74,13 @@ export const McpSelector = memo(function McpSelector({ const [mode, setMode] = useState("browsing"); const [deleteConfirmIndex, setDeleteConfirmIndex] = useState(0); const [error, setError] = useState(null); - + // Tools viewing state const [viewingServer, setViewingServer] = useState(null); const [tools, setTools] = useState([]); - const [attachedToolIds, setAttachedToolIds] = useState>(new Set()); + const [attachedToolIds, setAttachedToolIds] = useState>( + new Set(), + ); const [toolsLoading, setToolsLoading] = useState(false); const [toolsError, setToolsError] = useState(null); const [toolsPage, setToolsPage] = useState(0); @@ -105,81 +106,86 @@ export const McpSelector = memo(function McpSelector({ }, []); // Load tools for a specific server - const loadTools = useCallback(async (server: McpServer) => { - if (!server.id) { - setToolsError("Server ID not available"); - return; - } - - setToolsLoading(true); - setToolsError(null); - setViewingServer(server); - setMode("viewing-tools"); - - try { - const client = await getClient(); - - // Fetch MCP server tools - const toolsList = await client.mcpServers.tools.list(server.id); - - // If no tools found, might need to refresh from server - if (toolsList.length === 0) { - setToolsError( - "No tools found. The server may need to be refreshed. Press R to sync tools from the MCP server." - ); + const loadTools = useCallback( + async (server: McpServer) => { + if (!server.id) { + setToolsError("Server ID not available"); + return; } - - setTools(toolsList); - - // Fetch agent's current tools to check which are attached - const agent = await client.agents.retrieve(agentId); - const agentToolIds = new Set(agent.tools?.map(t => t.id) || []); - setAttachedToolIds(agentToolIds); - - setToolsPage(0); - setToolsSelectedIndex(0); - } catch (err) { - setToolsError( - err instanceof Error ? err.message : "Failed to load tools", - ); - setTools([]); - } finally { - setToolsLoading(false); - } - }, [agentId]); + + setToolsLoading(true); + setToolsError(null); + setViewingServer(server); + setMode("viewing-tools"); + + try { + const client = await getClient(); + + // Fetch MCP server tools + const toolsList = await client.mcpServers.tools.list(server.id); + + // If no tools found, might need to refresh from server + if (toolsList.length === 0) { + setToolsError( + "No tools found. The server may need to be refreshed. Press R to sync tools from the MCP server.", + ); + } + + setTools(toolsList); + + // Fetch agent's current tools to check which are attached + const agent = await client.agents.retrieve(agentId); + const agentToolIds = new Set(agent.tools?.map((t) => t.id) || []); + setAttachedToolIds(agentToolIds); + + setToolsPage(0); + setToolsSelectedIndex(0); + } catch (err) { + setToolsError( + err instanceof Error ? err.message : "Failed to load tools", + ); + setTools([]); + } finally { + setToolsLoading(false); + } + }, + [agentId], + ); // Refresh tools from MCP server const refreshToolsFromServer = useCallback(async () => { if (!viewingServer?.id) return; - + setToolsLoading(true); setToolsError(null); - + try { const client = await getClient(); - + // Call refresh endpoint to sync tools from the MCP server await client.mcpServers.refresh(viewingServer.id, { agent_id: agentId }); - + // Reload tools list const toolsList = await client.mcpServers.tools.list(viewingServer.id); setTools(toolsList); - + // Refresh agent's current tools const agent = await client.agents.retrieve(agentId); - const agentToolIds = new Set(agent.tools?.map(t => t.id) || []); + const agentToolIds = new Set(agent.tools?.map((t) => t.id) || []); setAttachedToolIds(agentToolIds); - + setToolsPage(0); setToolsSelectedIndex(0); - + // Clear error if successful if (toolsList.length === 0) { setToolsError("Server refreshed but no tools available."); } } catch (err) { setToolsError( - err instanceof Error ? `Failed to refresh: ${err.message}` : "Failed to refresh tools", + err instanceof Error + ? `Failed to refresh: ${err.message}` + : "Failed to refresh tools", ); } finally { setToolsLoading(false); @@ -187,50 +193,55 @@ export const McpSelector = memo(function McpSelector({ }, [agentId, viewingServer]); // Toggle tool attachment - const toggleTool = useCallback(async (tool: Tool) => { - setIsTogglingTool(true); - try { - const client = await getClient(); - const isAttached = attachedToolIds.has(tool.id); - - if (isAttached) { - // Detach tool - await client.agents.tools.detach(tool.id, { agent_id: agentId }); - } else { - // Attach tool - await client.agents.tools.attach(tool.id, { agent_id: agentId }); + const toggleTool = useCallback( + async (tool: Tool) => { + setIsTogglingTool(true); + try { + const client = await getClient(); + const isAttached = attachedToolIds.has(tool.id); + + if (isAttached) { + // Detach tool + await client.agents.tools.detach(tool.id, { agent_id: agentId }); + } else { + // Attach tool + await client.agents.tools.attach(tool.id, { agent_id: agentId }); + } + + // Fetch agent's current tools to get accurate total count + const agent = await client.agents.retrieve(agentId); + const agentToolIds = new Set(agent.tools?.map((t) => t.id) || []); + setAttachedToolIds(agentToolIds); + } catch (err) { + setToolsError( + err instanceof Error + ? err.message + : "Failed to toggle tool attachment", + ); + } finally { + setIsTogglingTool(false); } - - // Fetch agent's current tools to get accurate total count - const agent = await client.agents.retrieve(agentId); - const agentToolIds = new Set(agent.tools?.map(t => t.id) || []); - setAttachedToolIds(agentToolIds); - } catch (err) { - setToolsError( - err instanceof Error ? err.message : "Failed to toggle tool attachment", - ); - } finally { - setIsTogglingTool(false); - } - }, [agentId, attachedToolIds]); + }, + [agentId, attachedToolIds], + ); // Attach all tools const attachAllTools = useCallback(async () => { setIsTogglingTool(true); try { const client = await getClient(); - + // Attach tools that aren't already attached - const unattachedTools = tools.filter(t => !attachedToolIds.has(t.id)); + const unattachedTools = tools.filter((t) => !attachedToolIds.has(t.id)); await Promise.all( - unattachedTools.map(tool => - client.agents.tools.attach(tool.id, { agent_id: agentId }) - ) + unattachedTools.map((tool) => + client.agents.tools.attach(tool.id, { agent_id: agentId }), + ), ); - + // Fetch agent's current tools to get accurate total count const agent = await client.agents.retrieve(agentId); - const agentToolIds = new Set(agent.tools?.map(t => t.id) || []); + const agentToolIds = new Set(agent.tools?.map((t) => t.id) || []); setAttachedToolIds(agentToolIds); } catch (err) { setToolsError( @@ -246,18 +257,18 @@ export const McpSelector = memo(function McpSelector({ setIsTogglingTool(true); try { const client = await getClient(); - + // Detach only the tools from this server that are currently attached - const attachedTools = tools.filter(t => attachedToolIds.has(t.id)); + const attachedTools = tools.filter((t) => attachedToolIds.has(t.id)); await Promise.all( - attachedTools.map(tool => - client.agents.tools.detach(tool.id, { agent_id: agentId }) - ) + attachedTools.map((tool) => + client.agents.tools.detach(tool.id, { agent_id: agentId }), + ), ); - + // Fetch agent's current tools to get accurate total count const agent = await client.agents.retrieve(agentId); - const agentToolIds = new Set(agent.tools?.map(t => t.id) || []); + const agentToolIds = new Set(agent.tools?.map((t) => t.id) || []); setAttachedToolIds(agentToolIds); } catch (err) { setToolsError( @@ -324,10 +335,13 @@ export const McpSelector = memo(function McpSelector({ // Handle viewing tools mode if (mode === "viewing-tools") { if (isTogglingTool) return; // Prevent input during toggle - + const toolsTotalPages = Math.ceil(tools.length / TOOLS_DISPLAY_PAGE_SIZE); const toolsStartIndex = toolsPage * TOOLS_DISPLAY_PAGE_SIZE; - const pageTools = tools.slice(toolsStartIndex, toolsStartIndex + TOOLS_DISPLAY_PAGE_SIZE); + const pageTools = tools.slice( + toolsStartIndex, + toolsStartIndex + TOOLS_DISPLAY_PAGE_SIZE, + ); const selectedTool = pageTools[toolsSelectedIndex]; if (key.upArrow) { @@ -339,12 +353,17 @@ export const McpSelector = memo(function McpSelector({ setToolsSelectedIndex((prev) => Math.max(0, prev - 1)); } } else if (key.downArrow) { - if (toolsSelectedIndex === pageTools.length - 1 && toolsPage < toolsTotalPages - 1) { + if ( + toolsSelectedIndex === pageTools.length - 1 && + toolsPage < toolsTotalPages - 1 + ) { // At bottom of page, go to next page setToolsPage((prev) => prev + 1); setToolsSelectedIndex(0); } else { - setToolsSelectedIndex((prev) => Math.min(pageTools.length - 1, prev + 1)); + setToolsSelectedIndex((prev) => + Math.min(pageTools.length - 1, prev + 1), + ); } } else if ((key.return || input === " ") && selectedTool) { // Space or Enter to toggle selected tool @@ -378,7 +397,10 @@ export const McpSelector = memo(function McpSelector({ setSelectedIndex((prev) => Math.max(0, prev - 1)); } } else if (key.downArrow) { - if (selectedIndex === pageServers.length - 1 && currentPage < totalPages - 1) { + if ( + selectedIndex === pageServers.length - 1 && + currentPage < totalPages - 1 + ) { // At bottom of page, go to next page setCurrentPage((prev) => prev + 1); setSelectedIndex(0); @@ -410,7 +432,10 @@ export const McpSelector = memo(function McpSelector({ if (mode === "viewing-tools" && viewingServer) { const toolsTotalPages = Math.ceil(tools.length / TOOLS_DISPLAY_PAGE_SIZE); const toolsStartIndex = toolsPage * TOOLS_DISPLAY_PAGE_SIZE; - const pageTools = tools.slice(toolsStartIndex, toolsStartIndex + TOOLS_DISPLAY_PAGE_SIZE); + const pageTools = tools.slice( + toolsStartIndex, + toolsStartIndex + TOOLS_DISPLAY_PAGE_SIZE, + ); return ( @@ -466,11 +491,7 @@ export const McpSelector = memo(function McpSelector({ const statusIndicator = isAttached ? "✓" : " "; return ( - + {/* Row 1: Selection indicator, attachment status, and tool name */} 0 && (() => { - const attachedFromThisServer = tools.filter(t => attachedToolIds.has(t.id)).length; - return ( - - - - {toolsTotalPages > 1 && `Page ${toolsPage + 1}/${toolsTotalPages} · `} - {attachedFromThisServer}/{tools.length} attached from server · {attachedToolIds.size} total on agent - + {!toolsLoading && + !toolsError && + tools.length > 0 && + (() => { + const attachedFromThisServer = tools.filter((t) => + attachedToolIds.has(t.id), + ).length; + return ( + + + + {toolsTotalPages > 1 && + `Page ${toolsPage + 1}/${toolsTotalPages} · `} + {attachedFromThisServer}/{tools.length} attached from server + · {attachedToolIds.size} total on agent + + + + + ↑↓ navigate · Space/Enter toggle · A attach all · D detach + all · R refresh · Esc back + + - - - ↑↓ navigate · Space/Enter toggle · A attach all · D detach all · R refresh · Esc back - - - - ); - })()} + ); + })()} ); } @@ -676,7 +705,8 @@ export const McpSelector = memo(function McpSelector({ )} - ↑↓ navigate · Enter view tools · A add · D delete · R refresh · Esc close + ↑↓ navigate · Enter view tools · A add · D delete · R refresh · + Esc close