Fix stdio bugs (#608)
1. Updates `arcade configure claude --from-local` to create a valid json
config for claude desktop. NOTE: The `arcade configure` command needs
some re-work. It's fragile.
2. Fixes bug where stdio servers were sending logs to the wrong sink.
3. Disabled colorized logs for stdio.
4. Added missing dependency `httpx` for servers created with `arcade
new`
## Claude Desktop json configuration for stdio
Personally I like option 1 because the configuration looks the simplest
### Option 1:
Equivalent to `python server.py stdio`
```
{
"globalShortcut": "Alt+Ctrl+Space",
"mcpServers": {
"my_server": {
"command": "/path/to/my/mcp/server/directory/.venv/bin/python",
"args": [
"/path/to/my/mcp/server/directory/server.py",
"stdio"
]
}
}
}
```
### Option 2:
Equivalent to `uv run server.py stdio`
```
{
"mcpServers": {
"my_server": {
"command": "uv",
"args": [
"run",
"--directory",
"/path/to/my/mcp/server/directory",
"python",
"server.py",
"stdio"
]
}
}
}
```
### Option 3:
Equivalent to `python -m arcade_mcp_server stdio --cwd ./`
```
{
"mcpServers": {
"my_server": {
"command": "/path/to/my/mcp/server/directory/.venv/bin/python",
"args": [
"-m",
"arcade_mcp_server",
"stdio",
"--cwd",
"/path/to/my/mcp/server/directory"
]
}
}
}
```
This commit is contained in:
parent
0cf1a8bd22
commit
b780e5b807
7 changed files with 48 additions and 18 deletions
|
|
@ -56,6 +56,26 @@ def configure_claude_local(server_name: str, port: int = 8000, path: Path | None
|
||||||
config_path = path or get_claude_config_path()
|
config_path = path or get_claude_config_path()
|
||||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Assume server.py is the entry point for the server
|
||||||
|
server_file = Path.cwd() / "server.py"
|
||||||
|
|
||||||
|
# Find the Python interpreter in the virtual environment
|
||||||
|
venv_python = None
|
||||||
|
# Check for .venv first (uv default)
|
||||||
|
if (Path.cwd() / ".venv").exists():
|
||||||
|
system = platform.system()
|
||||||
|
if system == "Windows":
|
||||||
|
venv_python = Path.cwd() / ".venv" / "Scripts" / "python.exe"
|
||||||
|
else:
|
||||||
|
venv_python = Path.cwd() / ".venv" / "bin" / "python"
|
||||||
|
|
||||||
|
# Fall back to system python if no venv found
|
||||||
|
if not venv_python or not venv_python.exists():
|
||||||
|
console.print("[yellow]Warning: No .venv found, using system python[/yellow]")
|
||||||
|
import sys
|
||||||
|
|
||||||
|
venv_python = Path(sys.executable)
|
||||||
|
|
||||||
# Load existing config or create new one
|
# Load existing config or create new one
|
||||||
config = {}
|
config = {}
|
||||||
if config_path.exists():
|
if config_path.exists():
|
||||||
|
|
@ -66,10 +86,10 @@ def configure_claude_local(server_name: str, port: int = 8000, path: Path | None
|
||||||
if "mcpServers" not in config:
|
if "mcpServers" not in config:
|
||||||
config["mcpServers"] = {}
|
config["mcpServers"] = {}
|
||||||
|
|
||||||
|
# Claude Desktop uses stdio transport
|
||||||
config["mcpServers"][server_name] = {
|
config["mcpServers"][server_name] = {
|
||||||
"command": "python",
|
"command": str(venv_python),
|
||||||
"args": ["-m", "arcade_mcp_server", "stream"],
|
"args": [str(server_file), "stdio"],
|
||||||
"url": f"http://localhost:{port}/mcp",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Write updated config
|
# Write updated config
|
||||||
|
|
@ -82,7 +102,8 @@ def configure_claude_local(server_name: str, port: int = 8000, path: Path | None
|
||||||
)
|
)
|
||||||
config_file_path = config_path.as_posix().replace(" ", "\\ ")
|
config_file_path = config_path.as_posix().replace(" ", "\\ ")
|
||||||
console.print(f" MCP client config file: {config_file_path}", style="dim")
|
console.print(f" MCP client config file: {config_file_path}", style="dim")
|
||||||
console.print(f" MCP Server URL: http://localhost:{port}/mcp", style="dim")
|
console.print(f" Server file: {server_file}", style="dim")
|
||||||
|
console.print(f" Python interpreter: {venv_python}", style="dim")
|
||||||
console.print(" Restart Claude Desktop for changes to take effect.", style="yellow")
|
console.print(" Restart Claude Desktop for changes to take effect.", style="yellow")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,14 @@ try:
|
||||||
ARCADE_MCP_MAX_VERSION = str(int(ARCADE_MCP_MIN_VERSION.split(".")[0]) + 1) + ".0.0"
|
ARCADE_MCP_MAX_VERSION = str(int(ARCADE_MCP_MIN_VERSION.split(".")[0]) + 1) + ".0.0"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"[red]Failed to get arcade-mcp version: {e}[/red]")
|
console.print(f"[red]Failed to get arcade-mcp version: {e}[/red]")
|
||||||
ARCADE_MCP_MIN_VERSION = "1.0.0" # Default version if unable to fetch
|
ARCADE_MCP_MIN_VERSION = "1.1.0" # Default version if unable to fetch
|
||||||
ARCADE_MCP_MAX_VERSION = "2.0.0"
|
ARCADE_MCP_MAX_VERSION = "2.0.0"
|
||||||
|
|
||||||
ARCADE_TDK_MIN_VERSION = "3.0.0"
|
ARCADE_TDK_MIN_VERSION = "3.0.0"
|
||||||
ARCADE_TDK_MAX_VERSION = "4.0.0"
|
ARCADE_TDK_MAX_VERSION = "4.0.0"
|
||||||
ARCADE_SERVE_MIN_VERSION = "3.0.0"
|
ARCADE_SERVE_MIN_VERSION = "3.0.0"
|
||||||
ARCADE_SERVE_MAX_VERSION = "4.0.0"
|
ARCADE_SERVE_MAX_VERSION = "4.0.0"
|
||||||
ARCADE_MCP_SERVER_MIN_VERSION = "1.0.1"
|
ARCADE_MCP_SERVER_MIN_VERSION = "1.1.1"
|
||||||
ARCADE_MCP_SERVER_MAX_VERSION = "2.0.0"
|
ARCADE_MCP_SERVER_MAX_VERSION = "2.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ description = "MCP Server created with Arcade.dev"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arcade-mcp-server>={{ arcade_mcp_server_min_version }},<{{ arcade_mcp_server_max_version }}",
|
"arcade-mcp-server>={{ arcade_mcp_server_min_version }},<{{ arcade_mcp_server_max_version }}",
|
||||||
|
"httpx>=0.28.0,<1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ def setup_logging(level: str = "INFO", stdio_mode: bool = False) -> None:
|
||||||
# Remove existing handlers
|
# Remove existing handlers
|
||||||
logger.remove()
|
logger.remove()
|
||||||
|
|
||||||
# Configure output destination
|
# In stdio mode, use stderr (stdout is reserved for JSON-RPC)
|
||||||
sink = sys.stderr if stdio_mode else sys.stdout
|
sink = sys.stderr if stdio_mode else sys.stdout
|
||||||
|
|
||||||
# Add handler with appropriate format
|
# Add handler with appropriate format
|
||||||
|
|
@ -69,7 +69,7 @@ def setup_logging(level: str = "INFO", stdio_mode: bool = False) -> None:
|
||||||
sink,
|
sink,
|
||||||
format=format_str,
|
format=format_str,
|
||||||
level=level,
|
level=level,
|
||||||
colorize=True,
|
colorize=(not stdio_mode),
|
||||||
diagnose=(level == "DEBUG"),
|
diagnose=(level == "DEBUG"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,8 @@ class MCPApp:
|
||||||
self.server: MCPServer | None = None
|
self.server: MCPServer | None = None
|
||||||
|
|
||||||
self._load_env()
|
self._load_env()
|
||||||
self._setup_logging()
|
if not logger._core.handlers: # type: ignore[attr-defined]
|
||||||
|
self._setup_logging(transport == "stdio")
|
||||||
|
|
||||||
# Properties (exposed below initializer)
|
# Properties (exposed below initializer)
|
||||||
@property
|
@property
|
||||||
|
|
@ -128,17 +129,21 @@ class MCPApp:
|
||||||
load_dotenv(env_path, override=False)
|
load_dotenv(env_path, override=False)
|
||||||
logger.info(f"Loaded environment from {env_path}")
|
logger.info(f"Loaded environment from {env_path}")
|
||||||
|
|
||||||
def _setup_logging(self) -> None:
|
def _setup_logging(self, stdio_mode: bool = False) -> None:
|
||||||
logger.remove()
|
logger.remove()
|
||||||
|
|
||||||
|
# In stdio mode, use stderr (stdout is reserved for JSON-RPC)
|
||||||
|
sink = sys.stderr if stdio_mode else sys.stdout
|
||||||
|
|
||||||
if self.log_level == "DEBUG":
|
if self.log_level == "DEBUG":
|
||||||
format_str = "<level>{level: <8}</level> | <green>{time:HH:mm:ss}</green> | <cyan>{name}:{line}</cyan> | <level>{message}</level>"
|
format_str = "<level>{level: <8}</level> | <green>{time:HH:mm:ss}</green> | <cyan>{name}:{line}</cyan> | <level>{message}</level>"
|
||||||
else:
|
else:
|
||||||
format_str = "<level>{level: <8}</level> | <green>{time:HH:mm:ss}</green> | <level>{message}</level>"
|
format_str = "<level>{level: <8}</level> | <green>{time:HH:mm:ss}</green> | <level>{message}</level>"
|
||||||
logger.add(
|
logger.add(
|
||||||
sys.stdout,
|
sink,
|
||||||
format=format_str,
|
format=format_str,
|
||||||
level=self.log_level,
|
level=self.log_level,
|
||||||
colorize=True,
|
colorize=(not stdio_mode),
|
||||||
diagnose=(self.log_level == "DEBUG"),
|
diagnose=(self.log_level == "DEBUG"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -209,10 +214,13 @@ class MCPApp:
|
||||||
logger.error("No tools added to the server. Use @app.tool decorator or app.add_tool().")
|
logger.error("No tools added to the server. Use @app.tool decorator or app.add_tool().")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
logger.info(f"Starting {self.name} v{self.version} with {len(self._catalog)} tools")
|
|
||||||
|
|
||||||
host, port, transport = MCPApp._get_configuration_overrides(host, port, transport)
|
host, port, transport = MCPApp._get_configuration_overrides(host, port, transport)
|
||||||
|
|
||||||
|
# Since the transport could have changed since __init__, we need to setup logging again
|
||||||
|
self._setup_logging(transport == "stdio")
|
||||||
|
|
||||||
|
logger.info(f"Starting {self.name} v{self.version} with {len(self._catalog)} tools")
|
||||||
|
|
||||||
if transport in ["http", "streamable-http", "streamable"]:
|
if transport in ["http", "streamable-http", "streamable"]:
|
||||||
run_arcade_mcp(
|
run_arcade_mcp(
|
||||||
catalog=self._catalog,
|
catalog=self._catalog,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "arcade-mcp-server"
|
name = "arcade-mcp-server"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
description = "Model Context Protocol (MCP) server framework for Arcade.dev"
|
description = "Model Context Protocol (MCP) server framework for Arcade.dev"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [{ name = "Arcade.dev" }]
|
authors = [{ name = "Arcade.dev" }]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "arcade-mcp"
|
name = "arcade-mcp"
|
||||||
version = "1.0.2"
|
version = "1.1.0"
|
||||||
description = "Arcade.dev - Tool Calling platform for Agents"
|
description = "Arcade.dev - Tool Calling platform for Agents"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
|
|
@ -21,7 +21,7 @@ requires-python = ">=3.10"
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
# CLI dependencies
|
# CLI dependencies
|
||||||
"arcade-mcp-server>=1.1.0,<2.0.0",
|
"arcade-mcp-server>=1.1.1,<2.0.0",
|
||||||
"arcade-core>=3.0.0,<4.0.0",
|
"arcade-core>=3.0.0,<4.0.0",
|
||||||
"typer==0.10.0",
|
"typer==0.10.0",
|
||||||
"rich==13.9.4",
|
"rich==13.9.4",
|
||||||
|
|
@ -42,7 +42,7 @@ all = [
|
||||||
"pytz>=2024.1",
|
"pytz>=2024.1",
|
||||||
"python-dateutil>=2.8.2",
|
"python-dateutil>=2.8.2",
|
||||||
# mcp
|
# mcp
|
||||||
"arcade-mcp-server>=1.1.0,<2.0.0",
|
"arcade-mcp-server>=1.1.1,<2.0.0",
|
||||||
# serve
|
# serve
|
||||||
"arcade-serve>=3.0.0,<4.0.0",
|
"arcade-serve>=3.0.0,<4.0.0",
|
||||||
# tdk
|
# tdk
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue