{"record":{"id":"7042bb0c3770eeab","repo":"microsoft/autogen","slug":"failed-to-list-mcp-tools","errorCode":null,"errorMessage":"Failed to list MCP tools","messagePattern":"Failed to list MCP tools","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"python/packages/autogen-studio/frontend/src/components/views/mcp/api.ts","lineNumber":209,"sourceCode":"\n    const data = await response.json();\n    if (!response.ok) {\n      throw new Error(data.message || \"Failed to get MCP capabilities\");\n    }\n\n    return data;\n  }\n\n  async listTools(serverParams: McpServerParams): Promise<ListToolsResponse> {\n    const response = await fetch(`${this.getBaseUrl()}/mcp/tools/list`, {\n      method: \"POST\",\n      headers: this.getHeaders(),\n      body: JSON.stringify({ server_params: serverParams }),\n    });\n\n    const data = await response.json();\n    if (!response.ok) {\n      throw new Error(data.message || \"Failed to list MCP tools\");\n    }\n\n    return data;\n  }\n\n  async callTool(\n    serverParams: McpServerParams,\n    toolName: string,\n    toolArguments: Record<string, any>\n  ): Promise<CallToolResponse> {\n    const response = await fetch(`${this.getBaseUrl()}/mcp/tools/call`, {\n      method: \"POST\",\n      headers: this.getHeaders(),\n      body: JSON.stringify({\n        server_params: serverParams,\n        tool_name: toolName,\n        arguments: toolArguments,\n      }),","sourceCodeStart":191,"sourceCodeEnd":227,"githubUrl":"https://github.com/microsoft/autogen/blob/027ecf0a379bcc1d09956d46d12d44a3ad9cee14/python/packages/autogen-studio/frontend/src/components/views/mcp/api.ts#L191-L227","documentation":"Thrown by McpAPI.listTools when the POST to /mcp/tools/list returns a non-2xx status. The server response body is parsed as JSON and its message field is surfaced; the literal string 'Failed to list MCP tools' only appears when the body has no message field (e.g. empty or HTML error body). This is a plain fetch wrapper with no timeout, no retry, and no check for network-vs-HTTP failure.","triggerScenarios":"POST {base}/mcp/tools/list with {server_params} returning 4xx/5xx: invalid server_params (bad command, missing env vars for a stdio server), unreachable SSE/HTTP MCP server URL, unauthenticated request (missing/expired Bearer token so the backend returns 401/403), or the backend MCP route not mounted.","commonSituations":"Misconfigured stdio server_params (wrong command path), MCP server URL not reachable from the backend container, running frontend against a backend on a different port without the /api proxy forwarding /mcp, expired auth_token in localStorage, backend version where the /mcp route is not registered.","solutions":["Check the browser Network tab for the actual status code and response body of the POST to /mcp/tools/list — the body's message field names the real cause","Validate server_params before calling: for stdio ensure command is on the backend PATH; for SSE/HTTP ensure the URL is reachable from the backend process, not the browser","Verify the Authorization header is present and the token valid (clear localStorage auth_token and re-login if 401)","Confirm the backend actually mounts the /mcp/tools/list route (curl POST it directly against the backend port)","If data.message is empty because the body is HTML, log await response.text() instead of relying on response.json()"],"exampleFix":"// before\nconst data = await response.json();\nif (!response.ok) {\n  throw new Error(data.message || \"Failed to list MCP tools\");\n}\n// after\nconst text = await response.text();\nlet data: any = {};\ntry { data = JSON.parse(text); } catch { /* HTML/plain error body */ }\nif (!response.ok) {\n  throw new Error(data.message || `Failed to list MCP tools (HTTP ${response.status}): ${text.slice(0, 200)}`);\n}","handlingStrategy":"validation","validationCode":"function hasValidServerParams(p: McpServerParams): boolean {\n  if (!p || typeof p !== \"object\") return false;\n  if ((p as any).command != null) {\n    return typeof (p as any).command === \"string\" && (p as any).command.length > 0;\n  }\n  if ((p as any).url != null) {\n    try { new URL((p as any).url); return true; } catch { return false; }\n  }\n  return false;\n}","typeGuard":"function isStdioParams(p: McpServerParams): p is McpServerParams & { command: string; args?: string[] } {\n  return typeof (p as any).command === \"string\";\n}","tryCatchPattern":"try {\n  const res = await mcpAPI.listTools(serverParams);\n  return res.data?.tools ?? [];\n} catch (e) {\n  reportError(e instanceof Error ? e.message : String(e), { scope: \"mcp.listTools\" });\n  return []; // treat as no tools; never surface raw error to render tree\n}","preventionTips":["Run healthCheck() once before listTools to fail fast with a clearer message","Validate server_params shape (stdio command / SSE url) in the UI form before submit","Cache the last working server_params per server entry and offer rollback"],"tags":["mcp","http","fetch","frontend","autogen-studio"],"backgroundTag":null,"analyzedSha":"027ecf0a379bcc1d09956d46d12d44a3ad9cee14","analyzedAt":"2026-08-15T03:38:00.719Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}