fix: add error handling and message on mcp servers, fixed tool mode on mcp (#8717)
* Added mcp error handling * Added error param to servers * Added error display on mcp servers page * Added error display on mcp component * Added error handling for other types of errors * uv lock update and add error handling * update mcp version * fixed tool mode not working on mcp component * update mcp to 1.9.4 * Removed server parameters custom commands on connection to stdio * removed unused import * disable mcp notice * Removed drop state when home type is mcp * Added loading before showing tools * Updated mcp to 1.9.4 * Decreased mcp timeout * Implemented error surfacing with exec command * removed non default keys either way when mcp_server is used * update to session handling * [autofix.ci] apply automated fixes * updated it to check if it is the same server to not clear on startup * update to components * Update mcp_component.py * Update mcp_component.py * Update mcp_component.py --------- Co-authored-by: Edwin Jose <edwin.jose@datastax.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
parent
e1624b8c6e
commit
f4d761e63d
14 changed files with 178 additions and 45 deletions
|
|
@ -105,7 +105,7 @@ dependencies = [
|
|||
"arize-phoenix-otel>=0.6.1",
|
||||
"openinference-instrumentation-langchain>=0.1.29",
|
||||
"crewai==0.102.0",
|
||||
"mcp>=0.9.1",
|
||||
"mcp>=1.9.4",
|
||||
"uv>=0.5.7",
|
||||
"scipy>=1.14.1",
|
||||
"ag2>=0.1.0",
|
||||
|
|
|
|||
|
|
@ -122,13 +122,52 @@ async def get_servers(
|
|||
)
|
||||
server_info["mode"] = mode.lower()
|
||||
server_info["toolsCount"] = len(tool_list)
|
||||
if len(tool_list) == 0:
|
||||
server_info["error"] = "No tools found"
|
||||
except ValueError as e:
|
||||
# Configuration validation errors, invalid URLs, etc.
|
||||
logger.error(f"Configuration error for server {server_name}: {e}")
|
||||
server_info["error"] = f"Configuration error: {e}"
|
||||
except (ConnectionError, TimeoutError) as e:
|
||||
# Network connection and timeout issues
|
||||
logger.error(f"Connection error for server {server_name}: {e}")
|
||||
server_info["error"] = f"Connection failed: {e}"
|
||||
except OSError as e:
|
||||
# System-level errors (process execution, file access)
|
||||
logger.error(f"System error for server {server_name}: {e}")
|
||||
server_info["error"] = f"System error: {e}"
|
||||
except (KeyError, TypeError) as e:
|
||||
# Data parsing and access errors
|
||||
logger.error(f"Data error for server {server_name}: {e}")
|
||||
server_info["error"] = f"Configuration data error: {e}"
|
||||
except (RuntimeError, ProcessLookupError, PermissionError) as e:
|
||||
# Runtime and process-related errors
|
||||
logger.error(f"Runtime error for server {server_name}: {e}")
|
||||
server_info["error"] = f"Runtime error: {e}"
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.exception(f"Error checking server {server_name}: {e}")
|
||||
# Generic catch-all for other exceptions (including ExceptionGroup)
|
||||
if hasattr(e, "exceptions") and e.exceptions:
|
||||
# Extract the first underlying exception for a more meaningful error message
|
||||
underlying_error = e.exceptions[0]
|
||||
logger.exception(f"Error checking server {server_name}: {underlying_error}")
|
||||
server_info["error"] = f"Error loading server: {underlying_error}"
|
||||
else:
|
||||
logger.exception(f"Error checking server {server_name}: {e}")
|
||||
server_info["error"] = f"Error loading server: {e}"
|
||||
return server_info
|
||||
|
||||
async def check_server_with_timeout(server_name: str) -> dict:
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
check_server(server_name), timeout=get_settings_service().settings.mcp_server_timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"Timeout checking server {server_name}")
|
||||
return {"name": server_name, "mode": None, "toolsCount": None, "error": "Server check timed out."}
|
||||
|
||||
# Run all server checks concurrently
|
||||
tasks = [check_server(server) for server in server_list["mcpServers"]]
|
||||
return await asyncio.gather(*tasks, return_exceptions=False)
|
||||
tasks = [check_server_with_timeout(server) for server in server_list["mcpServers"]]
|
||||
return await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
|
||||
@router.get("/servers/{server_name}")
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from pydantic import BaseModel, Field, create_model
|
|||
from sqlmodel import select
|
||||
|
||||
from langflow.services.database.models.flow.model import Flow
|
||||
from langflow.services.deps import get_settings_service
|
||||
|
||||
HTTP_ERROR_STATUS_CODE = httpx_codes.BAD_REQUEST # HTTP status code for client errors
|
||||
NULLABLE_TYPE_LENGTH = 2 # Number of types in a nullable union (the type itself + null)
|
||||
|
|
@ -300,7 +301,7 @@ class MCPStdioClient:
|
|||
self._connection_params = None
|
||||
self._connected = False
|
||||
|
||||
async def connect_to_server(self, command_str: str, env: dict[str, str] | None = None) -> list[StructuredTool]:
|
||||
async def _connect_to_server(self, command_str: str, env: dict[str, str] | None = None) -> list[StructuredTool]:
|
||||
"""Connect to MCP server using stdio transport (SDK style)."""
|
||||
from mcp import StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
|
@ -320,7 +321,7 @@ class MCPStdioClient:
|
|||
else:
|
||||
server_params = StdioServerParameters(
|
||||
command="bash",
|
||||
args=["-c", f"{command_str} || echo 'Command failed with exit code $?' >&2"],
|
||||
args=["-c", f"exec {command_str} || echo 'Command failed with exit code $?' >&2"],
|
||||
env=env_data,
|
||||
)
|
||||
|
||||
|
|
@ -339,6 +340,19 @@ class MCPStdioClient:
|
|||
self._connected = False
|
||||
return []
|
||||
|
||||
async def connect_to_server(self, command_str: str, env: dict[str, str] | None = None) -> list[StructuredTool]:
|
||||
"""Connect to MCP server using stdio transport (SDK style)."""
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
self._connect_to_server(command_str, env), timeout=get_settings_service().settings.mcp_server_timeout
|
||||
)
|
||||
except (ConnectionError, TimeoutError, OSError, ValueError) as e:
|
||||
logger.error(f"Failed to connect to MCP stdio server: {e}")
|
||||
self._connection_params = None
|
||||
self._connected = False
|
||||
msg = f"Failed to connect to MCP stdio server: {e}"
|
||||
raise ValueError(msg) from e
|
||||
|
||||
async def disconnect(self):
|
||||
"""Properly close the connection and clean up resources."""
|
||||
self.session = None
|
||||
|
|
@ -425,7 +439,7 @@ class MCPSseClient:
|
|||
logger.warning(f"Error checking redirects: {e}")
|
||||
return url
|
||||
|
||||
async def connect_to_server(
|
||||
async def _connect_to_server(
|
||||
self,
|
||||
url: str | None,
|
||||
headers: dict[str, str] | None = None,
|
||||
|
|
@ -464,6 +478,19 @@ class MCPSseClient:
|
|||
response = await session.list_tools()
|
||||
self._connected = True
|
||||
return response.tools
|
||||
except (ConnectionError, TimeoutError, OSError, ValueError) as e:
|
||||
logger.error(f"Failed to connect to MCP SSE server: {e}")
|
||||
self._connection_params = None
|
||||
self._connected = False
|
||||
msg = f"Failed to connect to MCP SSE server: {e}"
|
||||
raise ValueError(msg) from e
|
||||
|
||||
async def connect_to_server(self, url: str, headers: dict[str, str] | None = None) -> list[StructuredTool]:
|
||||
"""Connect to MCP server using SSE transport (SDK style)."""
|
||||
try:
|
||||
return await asyncio.wait_for(
|
||||
self._connect_to_server(url, headers), timeout=get_settings_service().settings.mcp_server_timeout
|
||||
)
|
||||
except (ConnectionError, TimeoutError, OSError, ValueError) as e:
|
||||
logger.error(f"Failed to connect to MCP SSE server: {e}")
|
||||
self._connection_params = None
|
||||
|
|
@ -568,7 +595,10 @@ async def update_tools(
|
|||
return "", [], {}
|
||||
except (ConnectionError, TimeoutError, OSError, ValueError) as e:
|
||||
logger.error(f"Failed to connect to MCP server '{server_name}': {e}")
|
||||
return "", [], {}
|
||||
# return "", [], {}
|
||||
msg = f"Failed to connect to MCP server '{server_name}': {e}"
|
||||
logger.error(msg)
|
||||
raise ValueError(msg) from e
|
||||
|
||||
if not tools or not client or not client._connected:
|
||||
logger.warning(f"No tools available from MCP server '{server_name}' or connection failed")
|
||||
|
|
@ -598,10 +628,11 @@ async def update_tools(
|
|||
tool_cache[tool.name] = tool_obj
|
||||
except (ConnectionError, TimeoutError, OSError, ValueError) as e:
|
||||
logger.error(f"Failed to create tool '{tool.name}' from server '{server_name}': {e}")
|
||||
continue
|
||||
msg = f"Failed to create tool '{tool.name}' from server '{server_name}': {e}"
|
||||
raise ValueError(msg) from e
|
||||
|
||||
logger.info(f"Successfully loaded {len(tool_list)} tools from MCP server '{server_name}'")
|
||||
except (ConnectionError, TimeoutError, OSError, ValueError) as e:
|
||||
except (ConnectionError, TimeoutError, OSError, ValueError, AttributeError, AssertionError) as e:
|
||||
logger.error(f"Unexpected error while updating tools for MCP server '{server_name}': {e}")
|
||||
return "", [], {}
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ class MCPToolsComponent(ComponentWithCache):
|
|||
sse_client: MCPSseClient = MCPSseClient()
|
||||
tools: list = []
|
||||
_tool_cache: dict = {}
|
||||
_last_selected_server: str | None = None # Cache for the last selected server
|
||||
default_keys: list[str] = [
|
||||
"code",
|
||||
"_type",
|
||||
|
|
@ -217,12 +218,14 @@ class MCPToolsComponent(ComponentWithCache):
|
|||
if len(self.tools) == 0:
|
||||
try:
|
||||
self.tools, build_config["mcp_server"]["value"] = await self.update_tool_list()
|
||||
build_config["tool"]["options"] = [tool.name for tool in self.tools]
|
||||
except ValueError:
|
||||
build_config["tool"]["options"] = []
|
||||
build_config["tool"]["value"] = ""
|
||||
build_config["tool"]["placeholder"] = "Error on MCP Server"
|
||||
return build_config
|
||||
build_config["tool"]["placeholder"] = ""
|
||||
|
||||
if field_value == "":
|
||||
return build_config
|
||||
tool_obj = None
|
||||
|
|
@ -242,30 +245,61 @@ class MCPToolsComponent(ComponentWithCache):
|
|||
else:
|
||||
return build_config
|
||||
elif field_name == "mcp_server":
|
||||
try:
|
||||
# field_value is now a dict {name, config}
|
||||
mcp_server_value = field_value
|
||||
self.tools, build_config["mcp_server"]["value"] = await self.update_tool_list(mcp_server_value)
|
||||
except ValueError:
|
||||
if not build_config["tools_metadata"]["show"]:
|
||||
build_config["tool"]["show"] = True
|
||||
build_config["tool"]["options"] = []
|
||||
build_config["tool"]["value"] = ""
|
||||
build_config["tool"]["placeholder"] = "Error on MCP Server"
|
||||
else:
|
||||
build_config["tool"]["show"] = False
|
||||
self.remove_non_default_keys(build_config)
|
||||
return build_config
|
||||
build_config["tool"]["placeholder"] = ""
|
||||
if "tool" in build_config and len(self.tools) > 0 and not build_config["tools_metadata"]["show"]:
|
||||
build_config["tool"]["show"] = True
|
||||
build_config["tool"]["options"] = [tool.name for tool in self.tools]
|
||||
await self._update_tool_config(build_config, build_config["tool"]["value"])
|
||||
elif "tool" in build_config and len(self.tools) == 0:
|
||||
self.remove_non_default_keys(build_config)
|
||||
if not field_value:
|
||||
build_config["tool"]["show"] = False
|
||||
build_config["tool"]["options"] = []
|
||||
build_config["tool"]["value"] = ""
|
||||
build_config["tool"]["placeholder"] = ""
|
||||
self.remove_non_default_keys(build_config)
|
||||
return build_config
|
||||
|
||||
current_server_name = field_value.get("name") if isinstance(field_value, dict) else field_value
|
||||
|
||||
# To avoid unnecessary updates, only proceed if the server has actually changed
|
||||
if self._last_selected_server == current_server_name:
|
||||
return build_config
|
||||
|
||||
# Determine if "Tool Mode" is active by checking if the tool dropdown is hidden.
|
||||
# The check for _last_selected_server handles the initial state where the dropdown is
|
||||
# hidden by default but we are not yet in "Tool Mode".
|
||||
is_in_tool_mode = build_config["tools_metadata"]["value"]
|
||||
self._last_selected_server = current_server_name
|
||||
self.tools = [] # Clear previous tools
|
||||
self.remove_non_default_keys(build_config) # Clear previous tool inputs
|
||||
|
||||
# Only show the tool dropdown if not in tool_mode
|
||||
if not is_in_tool_mode:
|
||||
build_config["tool"]["show"] = True
|
||||
build_config["tool"]["placeholder"] = "Loading tools..."
|
||||
build_config["tool"]["options"] = []
|
||||
build_config["tool"]["value"] = ""
|
||||
else:
|
||||
# Keep the tool dropdown hidden if in tool_mode
|
||||
build_config["tool"]["show"] = False
|
||||
|
||||
try:
|
||||
# Fetch tools for the newly selected server
|
||||
tools, server_info = await self.update_tool_list(field_value)
|
||||
build_config["mcp_server"]["value"] = server_info
|
||||
|
||||
if tools:
|
||||
tool_names = [tool.name for tool in tools]
|
||||
if not is_in_tool_mode:
|
||||
build_config["tool"]["options"] = tool_names
|
||||
build_config["tool"]["placeholder"] = "Select a tool"
|
||||
elif not is_in_tool_mode:
|
||||
build_config["tool"]["placeholder"] = "No tools found"
|
||||
|
||||
except (ValueError, AttributeError, KeyError, TypeError, ConnectionError, TimeoutError) as e:
|
||||
logger.error(f"Failed to fetch tools for server '{current_server_name}': {e}")
|
||||
if not is_in_tool_mode:
|
||||
build_config["tool"]["placeholder"] = "Error fetching tools"
|
||||
build_config["tool"]["options"] = []
|
||||
build_config["tool"]["value"] = ""
|
||||
finally:
|
||||
# Ensure we don't show inputs for a tool that might no longer be valid
|
||||
await self._update_tool_config(build_config, "")
|
||||
|
||||
elif field_name == "tool_mode":
|
||||
try:
|
||||
self.tools, build_config["mcp_server"]["value"] = await self.update_tool_list()
|
||||
|
|
@ -429,4 +463,5 @@ class MCPToolsComponent(ComponentWithCache):
|
|||
async def _get_tools(self):
|
||||
"""Get cached tools or update if necessary."""
|
||||
mcp_server = getattr(self, "mcp_server", None)
|
||||
return await self.update_tool_list(mcp_server)
|
||||
tools, _ = await self.update_tool_list(mcp_server)
|
||||
return tools
|
||||
|
|
|
|||
|
|
@ -92,6 +92,10 @@ class Settings(BaseSettings):
|
|||
"""The number of seconds to wait before giving up on a lock to released or establishing a connection to the
|
||||
database."""
|
||||
|
||||
mcp_server_timeout: int = 20
|
||||
"""The number of seconds to wait before giving up on a lock to released or establishing a connection to the
|
||||
database."""
|
||||
|
||||
# sqlite configuration
|
||||
sqlite_pragmas: dict | None = {"synchronous": "NORMAL", "journal_mode": "WAL"}
|
||||
"""SQLite pragmas to use when connecting to the database."""
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ dependencies = [
|
|||
"validators>=0.34.0",
|
||||
"networkx>=3.4.2",
|
||||
"json-repair>=0.30.3",
|
||||
"mcp>=1.6.0",
|
||||
"mcp~=1.9.4",
|
||||
"aiosqlite>=0.20.0",
|
||||
"greenlet>=3.1.1",
|
||||
"jsonquerylang>=1.1.1",
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ export default function Dropdown({
|
|||
// We should only reset the value if it's not in options and not in filteredOptions
|
||||
// and not a recently added custom value
|
||||
if (!options.includes(value) && !filteredOptions.includes(value)) {
|
||||
if (value) onSelect("", undefined, true);
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ export default function McpComponent({
|
|||
name: server.name,
|
||||
description:
|
||||
server.toolsCount === null
|
||||
? "Loading..."
|
||||
? server.error
|
||||
? "Error"
|
||||
: "Loading..."
|
||||
: !server.toolsCount
|
||||
? "No actions found"
|
||||
: `${server.toolsCount} action${server.toolsCount === 1 ? "" : "s"}`,
|
||||
|
|
|
|||
|
|
@ -29,8 +29,16 @@ export const useGetMCPServers: useQueryFunctionType<
|
|||
if (cachedData && Array.isArray(cachedData)) {
|
||||
const merged = data.map((server) => {
|
||||
const cached = cachedData.find((s) => s.name === server.name);
|
||||
return cached && (cached.toolsCount !== null || cached.mode !== null)
|
||||
? { ...server, toolsCount: cached.toolsCount, mode: cached.mode }
|
||||
return cached &&
|
||||
(cached.toolsCount !== null ||
|
||||
cached.mode !== null ||
|
||||
cached.error !== null)
|
||||
? {
|
||||
...server,
|
||||
toolsCount: cached.toolsCount,
|
||||
mode: cached.mode,
|
||||
error: cached.error,
|
||||
}
|
||||
: server;
|
||||
});
|
||||
return merged;
|
||||
|
|
|
|||
|
|
@ -14,4 +14,4 @@ export const ENABLE_WIDGET = true;
|
|||
export const ENABLE_VOICE_ASSISTANT = true;
|
||||
export const ENABLE_IMAGE_ON_PLAYGROUND = false;
|
||||
export const ENABLE_MCP = true;
|
||||
export const ENABLE_MCP_NOTICE = true;
|
||||
export const ENABLE_MCP_NOTICE = false;
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ const HomePage = ({ type }: { type: "flows" | "components" | "mcp" }) => {
|
|||
|
||||
return (
|
||||
<CardsWrapComponent
|
||||
onFileDrop={handleFileDrop}
|
||||
onFileDrop={flowType === "mcp" ? undefined : handleFileDrop}
|
||||
dragMessage={`Drop your ${isEmptyFolder ? "flows or components" : flowType} here`}
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -14,6 +15,7 @@ import AddMcpServerModal from "@/modals/addMcpServerModal";
|
|||
import DeleteConfirmationModal from "@/modals/deleteConfirmationModal";
|
||||
import useAlertStore from "@/stores/alertStore";
|
||||
import { MCPServerInfoType } from "@/types/mcp";
|
||||
import { cn } from "@/utils/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function MCPServersPage() {
|
||||
|
|
@ -101,11 +103,20 @@ export default function MCPServersPage() {
|
|||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{server.name}</span>
|
||||
<span className="text-mmd text-muted-foreground">
|
||||
{server.toolsCount === null
|
||||
? "Loading..."
|
||||
: `${server.toolsCount} action${server.toolsCount === 1 ? "" : "s"}`}
|
||||
</span>
|
||||
<ShadTooltip content={server.error}>
|
||||
<span
|
||||
className={cn(
|
||||
"cursor-default select-none !text-mmd text-muted-foreground",
|
||||
server.error && "text-accent-red-foreground",
|
||||
)}
|
||||
>
|
||||
{server.toolsCount === null
|
||||
? server.error
|
||||
? "Error"
|
||||
: "Loading..."
|
||||
: `${server.toolsCount} action${server.toolsCount === 1 ? "" : "s"}`}
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export type MCPServerInfoType = {
|
|||
description?: string;
|
||||
mode: string | null;
|
||||
toolsCount: number | null;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export type MCPServerType = {
|
||||
|
|
|
|||
5
uv.lock
generated
5
uv.lock
generated
|
|
@ -3,6 +3,7 @@ revision = 2
|
|||
requires-python = ">=3.10, <3.14"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13' and sys_platform == 'darwin'",
|
||||
"python_version < '0'",
|
||||
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||
"(python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'linux')",
|
||||
"python_full_version >= '3.12.4' and python_full_version < '3.13' and sys_platform == 'darwin'",
|
||||
|
|
@ -5086,7 +5087,7 @@ requires-dist = [
|
|||
{ name = "llama-cpp-python", marker = "extra == 'local'", specifier = "~=0.2.0" },
|
||||
{ name = "markdown", specifier = "==3.7" },
|
||||
{ name = "markupsafe", specifier = "==3.0.2" },
|
||||
{ name = "mcp", specifier = ">=0.9.1" },
|
||||
{ name = "mcp", specifier = ">=1.9.4" },
|
||||
{ name = "mem0ai", specifier = "==0.1.34" },
|
||||
{ name = "metal-sdk", specifier = "==2.5.1" },
|
||||
{ name = "metaphor-python", specifier = "==0.1.23" },
|
||||
|
|
@ -5363,7 +5364,7 @@ requires-dist = [
|
|||
{ name = "llama-cpp-python", marker = "extra == 'all'", specifier = ">=0.2.0" },
|
||||
{ name = "llama-cpp-python", marker = "extra == 'local'", specifier = ">=0.2.0" },
|
||||
{ name = "loguru", specifier = ">=0.7.1,<1.0.0" },
|
||||
{ name = "mcp", specifier = ">=1.6.0" },
|
||||
{ name = "mcp", specifier = "~=1.9.4" },
|
||||
{ name = "multiprocess", specifier = ">=0.70.14,<1.0.0" },
|
||||
{ name = "nanoid", specifier = ">=2.0.0,<3.0.0" },
|
||||
{ name = "nest-asyncio", specifier = ">=1.6.0,<2.0.0" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue