Versions: * arcade-mcp\==1.0.0rc1 * arcade-mcp-server\==1.0.0rc1 * arcade-core\==2.5.0rc1 * arcade-tdk\==2.6.0rc1 * arcade-serve\==2.2.0rc1 ### Summary Adds first-class MCP support across Arcade, introduces a new MCP server and CLI, unifies the project under the arcade-mcp name, overhauls templates/scaffolding, and improves developer tooling, secrets management, and examples. ### Highlights - **MCP Server & Core** - New MCP server with stdio and HTTP/SSE transports, session management, resumability, and lifecycle handling. - FastAPI-like `MCPApp` for building servers with lazy init; integrated worker+MCP HTTP app option. - Middleware system (logging and error handling), robust exception hierarchy, and Pydantic-based settings. - Async-safe managers for tools, resources, and prompts backed by registries and locks. - Developer-facing, transport-agnostic runtime context interfaces (logs, tools, prompts, resources, sampling, UI, notifications). - Conversion from Arcade ToolDefinition to MCP tool schema; OpenAI JSON tool schema converter. - Parser supports `@app.tool`/`@app.tool(...)` decorators. - **CLI** - New `mcp` command to run MCP servers with stdio or HTTP/SSE. - New `secret` command to set/list/unset tool secrets (supports .env input, preserves original casing for lookups). - `new` command refactored; option to create a full toolkit package with scaffolding. - `chat` command removed. - `serve.py` imports updated to `arcade_serve.fastapi.telemetry`; version retrieval now uses `arcade-mcp`. - `show.py` refactor to use new local catalog utilities. - `display_tool_details` improved: adds “Default” column and handles nested properties. - **Configuration & Discovery** - New `configure.py` to set up Claude Desktop, Cursor, and VS Code to connect to local or Arcade Cloud MCP servers. - Discovery utilities to find/install toolkits, build `ToolCatalog`s, analyze files for tools, load kits from directories (pyproject parsing), and build minimal toolkits. - Better handling of provider API key resolution and evaluation suite loading. - **Templates & Scaffolding** - Reorganized template structure (minimal vs full); moved `.pre-commit-config.yaml`, `.ruff.toml`, license, Makefile, README, tests, and tools layout to correct paths. - Minimal template adds `.env.example` for runtime secret injection. - Template pyproject updated for MCP servers; includes sample server with greeting and secret-reveal tools. - Authorization flow in templates simplified. - **Repo-wide Renaming & Examples** - Migrates references from `arcade-ai` to `arcade-mcp` across READMEs, scripts, and package metadata. - Examples updated (LangChain/LangGraph/AI SDK/TypeScript) and package name changed to `arcade-mcp-sdk`. - **Evals & Core Utilities** - Evals now use OpenAI tooling format (`OpenAIToolList`, `to_openai`); `tool_eval` takes `provider_api_key`. - Core utilities: fixed `does_function_return_value` by dedenting before parse; version bump to `2.5.0rc1` and dependency cleanup. - **Tooling & CI** - `setup-uv-env` action splits toolkit vs contrib dependency installation. - Pre-commit: excludes `libs/arcade-mcp-server/mkdocs.yml` and `libs/tests/` from YAML and Ruff hooks; Ruff per-file ignores (e.g., C901 in `libs/**/*.py`, TRY400 in server docs paths). - Makefile updates for uv env setup, quality checks, tests, builds, and new `shell` target. - Added Makefile to MCP server library to streamline dev workflow. - **Cleanup** - Removed `claude.json` config. - Simplified stdio entrypoint; removed unused imports (`arcade_gmail`, `arcade_search`). ### Breaking Changes - **CLI**: `chat` command removed; use `mcp`, `secret`, and updated `new`. - **Naming**: All users should update references from `arcade-ai` to `arcade-mcp`. - **Templates**: File paths moved; downstream scripts referencing old template locations may need updates. ### Getting Started - Run an MCP server: - `arcade mcp --stdio --toolkits your_toolkit` - `arcade mcp --http --toolkits your_toolkit` - Manage secrets: - `arcade secret set your_toolkit KEY=value` - `arcade secret list your_toolkit` - `arcade secret unset your_toolkit KEY` - Configure clients: - `arcade configure` to set up Claude Desktop, Cursor, and VS Code for local/Arcade Cloud MCP. --------- Co-authored-by: Sam Partee <sam@arcade-ai.com> Co-authored-by: Shub <125150494+shubcodes@users.noreply.github.com>
993 lines
32 KiB
Python
993 lines
32 KiB
Python
import asyncio
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import traceback
|
|
import uuid
|
|
import webbrowser
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import httpx
|
|
import typer
|
|
from arcadepy import Arcade
|
|
from rich.console import Console
|
|
from rich.markup import escape
|
|
from rich.text import Text
|
|
from tqdm import tqdm
|
|
|
|
import arcade_cli.secret as secret
|
|
import arcade_cli.worker as worker
|
|
from arcade_cli.authn import LocalAuthCallbackServer, check_existing_login
|
|
from arcade_cli.constants import (
|
|
CREDENTIALS_FILE_PATH,
|
|
PROD_CLOUD_HOST,
|
|
PROD_ENGINE_HOST,
|
|
)
|
|
from arcade_cli.deployment import Deployment
|
|
from arcade_cli.display import (
|
|
display_eval_results,
|
|
)
|
|
from arcade_cli.show import show_logic
|
|
from arcade_cli.toolkit_docs import generate_toolkit_docs
|
|
from arcade_cli.utils import (
|
|
OrderCommands,
|
|
Provider,
|
|
compute_base_url,
|
|
compute_login_url,
|
|
get_eval_files,
|
|
load_eval_suites,
|
|
log_engine_health,
|
|
require_dependency,
|
|
resolve_provider_api_key,
|
|
validate_and_get_config,
|
|
version_callback,
|
|
)
|
|
|
|
cli = typer.Typer(
|
|
cls=OrderCommands,
|
|
add_completion=False,
|
|
no_args_is_help=True,
|
|
pretty_exceptions_enable=True,
|
|
pretty_exceptions_show_locals=False,
|
|
pretty_exceptions_short=True,
|
|
rich_markup_mode="markdown",
|
|
)
|
|
|
|
|
|
cli.add_typer(
|
|
worker.app,
|
|
name="worker",
|
|
help="Manage deployments of tool servers (logs, list, etc)",
|
|
rich_help_panel="Deployment",
|
|
)
|
|
|
|
cli.add_typer(
|
|
secret.app,
|
|
name="secret",
|
|
help="Manage tool secrets in the cloud (set, unset, list)",
|
|
rich_help_panel="Admin",
|
|
)
|
|
|
|
|
|
console = Console()
|
|
|
|
|
|
def handle_cli_error(
|
|
message: str,
|
|
error: Optional[Exception] = None,
|
|
debug: bool = True,
|
|
should_exit: bool = True,
|
|
) -> None:
|
|
"""Handle CLI error reporting with optional debug traceback and exit."""
|
|
if error and debug:
|
|
console.print(f"❌ {message}: {traceback.format_exc()}", style="bold red")
|
|
elif error:
|
|
console.print(f"❌ {message}: {escape(str(error))}", style="bold red")
|
|
else:
|
|
console.print(f"❌ {message}", style="bold red")
|
|
|
|
if should_exit:
|
|
raise typer.Exit(code=1)
|
|
|
|
|
|
@cli.command(help="Log in to Arcade Cloud", rich_help_panel="User")
|
|
def login(
|
|
host: str = typer.Option(
|
|
PROD_CLOUD_HOST,
|
|
"-h",
|
|
"--host",
|
|
help="The Arcade Cloud host to log in to.",
|
|
),
|
|
port: Optional[int] = typer.Option(
|
|
None,
|
|
"-p",
|
|
"--port",
|
|
help="The port of the Arcade Cloud host (if running locally).",
|
|
),
|
|
callback_host: str = typer.Option(
|
|
None,
|
|
"--callback-host",
|
|
help="The host to use to complete the auth flow - this should be the same as the host that the CLI is running on. Include the port if needed.",
|
|
),
|
|
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
|
) -> None:
|
|
"""
|
|
Logs the user into Arcade Cloud.
|
|
"""
|
|
|
|
if check_existing_login():
|
|
console.print("\nTo log out and delete your locally-stored credentials, use ", end="")
|
|
console.print("arcade logout", style="bold green", end="")
|
|
console.print(".\n")
|
|
return
|
|
|
|
# Start the HTTP server in a new thread
|
|
state = str(uuid.uuid4())
|
|
auth_server = LocalAuthCallbackServer(state)
|
|
server_thread = threading.Thread(target=auth_server.run_server)
|
|
server_thread.start()
|
|
|
|
try:
|
|
# Open the browser for user login
|
|
login_url = compute_login_url(host, state, port, callback_host)
|
|
|
|
console.print("Opening a browser to log you in...")
|
|
if not webbrowser.open(login_url):
|
|
console.print(
|
|
f"If a browser doesn't open automatically, copy this URL and paste it into your browser: {login_url}",
|
|
style="dim",
|
|
)
|
|
|
|
# Wait for the server thread to finish
|
|
server_thread.join()
|
|
except KeyboardInterrupt:
|
|
auth_server.shutdown_server()
|
|
except Exception as e:
|
|
handle_cli_error("Login failed", e, debug)
|
|
finally:
|
|
if server_thread.is_alive():
|
|
server_thread.join() # Ensure the server thread completes and cleans up
|
|
|
|
|
|
@cli.command(help="Log out of Arcade Cloud", rich_help_panel="User")
|
|
def logout(
|
|
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
|
) -> None:
|
|
"""
|
|
Logs the user out of Arcade Cloud.
|
|
"""
|
|
try:
|
|
# If the credentials file exists, delete it
|
|
if os.path.exists(CREDENTIALS_FILE_PATH):
|
|
os.remove(CREDENTIALS_FILE_PATH)
|
|
console.print("You're now logged out.", style="bold")
|
|
else:
|
|
console.print("You're not logged in.", style="bold red")
|
|
except Exception as e:
|
|
handle_cli_error("Logout failed", e, debug)
|
|
|
|
|
|
@cli.command(
|
|
help="Create a new toolkit package directory. Example usage: arcade new my_toolkit",
|
|
rich_help_panel="Tool Development",
|
|
)
|
|
def new(
|
|
toolkit_name: str = typer.Argument(
|
|
help="The name of the toolkit to create",
|
|
metavar="TOOLKIT_NAME",
|
|
),
|
|
directory: str = typer.Option(os.getcwd(), "--dir", help="tools directory path"),
|
|
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
|
full: bool = typer.Option(
|
|
False,
|
|
"--full",
|
|
"-f",
|
|
help="Create a toolkit package with a full scaffolding (includes evals, tests, license, etc)",
|
|
),
|
|
) -> None:
|
|
"""
|
|
Creates a new toolkit with the given name, description, and result type.
|
|
"""
|
|
from arcade_cli.new import create_new_toolkit, create_new_toolkit_minimal
|
|
|
|
try:
|
|
if not full:
|
|
create_new_toolkit_minimal(directory, toolkit_name)
|
|
else:
|
|
create_new_toolkit(directory, toolkit_name)
|
|
except Exception as e:
|
|
handle_cli_error("Failed to create new Toolkit", e, debug)
|
|
|
|
|
|
@cli.command(
|
|
name="mcp",
|
|
help="Run MCP servers with different transports",
|
|
rich_help_panel="Launch",
|
|
)
|
|
def mcp(
|
|
transport: str = typer.Argument("http", help="Transport type: stdio, http"),
|
|
host: str = typer.Option("127.0.0.1", "--host", help="Host to bind to (HTTP mode only)"),
|
|
port: int = typer.Option(8000, "--port", help="Port to bind to (HTTP mode only)"),
|
|
tool_package: Optional[str] = typer.Option(
|
|
None,
|
|
"--tool-package",
|
|
"--package",
|
|
"-p",
|
|
help="Specific tool package to load (e.g., 'github' for arcade-github)",
|
|
),
|
|
discover_installed: bool = typer.Option(
|
|
False, "--discover-installed", "--all", help="Discover all installed arcade tool packages"
|
|
),
|
|
show_packages: bool = typer.Option(
|
|
False, "--show-packages", help="Show loaded packages during discovery"
|
|
),
|
|
reload: bool = typer.Option(
|
|
False, "--reload", help="Enable auto-reload on code changes (HTTP mode only)"
|
|
),
|
|
debug: bool = typer.Option(False, "--debug", help="Enable debug mode with verbose logging"),
|
|
env_file: Optional[str] = typer.Option(None, "--env-file", help="Path to environment file"),
|
|
name: Optional[str] = typer.Option(None, "--name", help="Server name"),
|
|
version: Optional[str] = typer.Option(None, "--version", help="Server version"),
|
|
cwd: Optional[str] = typer.Option(None, "--cwd", help="Working directory to run from"),
|
|
) -> None:
|
|
"""
|
|
Run Arcade MCP Server (passthrough to arcade_mcp_server).
|
|
|
|
This command provides a unified CLI experience by passing through
|
|
all arguments to the arcade_mcp_server module.
|
|
|
|
Examples:
|
|
arcade mcp stdio
|
|
arcade mcp http --port 8080
|
|
arcade mcp --tool-package github
|
|
arcade mcp --discover-installed --show-packages
|
|
"""
|
|
# Build the command to pass through to arcade_mcp_server
|
|
cmd = [sys.executable, "-m", "arcade_mcp_server", transport]
|
|
|
|
# Add optional arguments
|
|
cmd.extend(["--host", host])
|
|
cmd.extend(["--port", str(port)])
|
|
cmd.append("--debug")
|
|
if tool_package:
|
|
cmd.extend(["--tool-package", tool_package])
|
|
if discover_installed:
|
|
cmd.append("--discover-installed")
|
|
if show_packages:
|
|
cmd.append("--show-packages")
|
|
if reload:
|
|
cmd.append("--reload")
|
|
if env_file:
|
|
cmd.extend(["--env-file", env_file])
|
|
if name:
|
|
cmd.extend(["--name", name])
|
|
if version:
|
|
cmd.extend(["--version", version])
|
|
if cwd:
|
|
cmd.extend(["--cwd", cwd])
|
|
|
|
try:
|
|
# Show what command we're running in debug mode
|
|
if debug:
|
|
console.print(f"[dim]Running: {' '.join(cmd)}[/dim]")
|
|
|
|
# Execute the command and pass through all output
|
|
result = subprocess.run(cmd, check=False)
|
|
|
|
# Exit with the same code as the subprocess
|
|
if result.returncode != 0:
|
|
handle_cli_error("Failed to run MCP server")
|
|
|
|
except KeyboardInterrupt:
|
|
console.print("\n[yellow]MCP server stopped[/yellow]")
|
|
raise typer.Exit(0)
|
|
except FileNotFoundError:
|
|
console.print(
|
|
"[red]arcade_mcp_server module not found. Make sure arcade-mcp-server is installed.[/red]"
|
|
)
|
|
raise typer.Exit(1)
|
|
except Exception as e:
|
|
console.print(f"[red]Error running MCP server: {e}[/red]")
|
|
raise typer.Exit(1)
|
|
|
|
|
|
@cli.command(
|
|
help="Show the installed toolkits or details of a specific tool",
|
|
rich_help_panel="Tool Development",
|
|
)
|
|
def show(
|
|
toolkit: Optional[str] = typer.Option(
|
|
None, "-T", "--toolkit", help="The toolkit to show the tools of"
|
|
),
|
|
tool: Optional[str] = typer.Option(
|
|
None, "-t", "--tool", help="The specific tool to show details for"
|
|
),
|
|
host: str = typer.Option(
|
|
PROD_ENGINE_HOST,
|
|
"-h",
|
|
"--host",
|
|
help="The Arcade Engine address to show the tools/toolkits of.",
|
|
),
|
|
local: bool = typer.Option(
|
|
False,
|
|
"--local",
|
|
"-l",
|
|
help="Show the local environment's catalog instead of an Arcade Engine's catalog.",
|
|
),
|
|
port: Optional[int] = typer.Option(
|
|
None,
|
|
"-p",
|
|
"--port",
|
|
help="The port of the Arcade Engine.",
|
|
),
|
|
force_tls: bool = typer.Option(
|
|
False,
|
|
"--tls",
|
|
help="Whether to force TLS for the connection to the Arcade Engine. If not specified, the connection will use TLS if the engine URL uses a 'https' scheme.",
|
|
),
|
|
force_no_tls: bool = typer.Option(
|
|
False,
|
|
"--no-tls",
|
|
help="Whether to disable TLS for the connection to the Arcade Engine.",
|
|
),
|
|
worker: bool = typer.Option(
|
|
False,
|
|
"--worker",
|
|
"-w",
|
|
help="Show full worker response structure including error, logs, and authorization fields (only applies when used with -t/--tool).",
|
|
),
|
|
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
|
) -> None:
|
|
"""
|
|
Show the available toolkits or detailed information about a specific tool.
|
|
"""
|
|
if worker and not tool:
|
|
console.print(
|
|
"⚠️ The -w/--worker flag only affects output when used with -t/--tool flag",
|
|
style="bold yellow",
|
|
)
|
|
|
|
show_logic(
|
|
toolkit=toolkit,
|
|
tool=tool,
|
|
host=host,
|
|
local=local,
|
|
port=port,
|
|
force_tls=force_tls,
|
|
force_no_tls=force_no_tls,
|
|
worker=worker,
|
|
debug=debug,
|
|
)
|
|
|
|
|
|
@cli.command(help="Run tool calling evaluations", rich_help_panel="Tool Development")
|
|
def evals(
|
|
directory: str = typer.Argument(".", help="Directory containing evaluation files"),
|
|
show_details: bool = typer.Option(False, "--details", "-d", help="Show detailed results"),
|
|
max_concurrent: int = typer.Option(
|
|
1,
|
|
"--max-concurrent",
|
|
"-c",
|
|
help="Maximum number of concurrent evaluations (default: 1)",
|
|
),
|
|
models: str = typer.Option(
|
|
"gpt-4o",
|
|
"--models",
|
|
"-m",
|
|
help="The models to use for evaluation (default: gpt-4o). Use commas to separate multiple models. All models must belong to the same provider.",
|
|
),
|
|
provider: Provider = typer.Option(
|
|
Provider.OPENAI,
|
|
"--provider",
|
|
"-p",
|
|
help="The provider of the models to use for evaluation.",
|
|
),
|
|
provider_api_key: str = typer.Option(
|
|
None,
|
|
"--provider-api-key",
|
|
"-k",
|
|
help="The model provider API key. If not provided, will look for the appropriate environment variable based on the provider (e.g., OPENAI_API_KEY for openai provider), first in the current environment, then in the current working directory's .env file.",
|
|
),
|
|
debug: bool = typer.Option(False, "--debug", help="Show debug information"),
|
|
) -> None:
|
|
"""
|
|
Find all files starting with 'eval_' in the given directory,
|
|
execute any functions decorated with @tool_eval, and display the results.
|
|
"""
|
|
require_dependency(
|
|
package_name="arcade_evals",
|
|
command_name="evals",
|
|
install_command=r"pip install 'arcade-mcp\[evals]'",
|
|
)
|
|
# Although Evals does not depend on the TDK, some evaluations import the
|
|
# ToolCatalog class from the TDK instead of from arcade_core, so we require
|
|
# the TDK to run the evals CLI command to avoid possible import errors.
|
|
require_dependency(
|
|
package_name="arcade_tdk",
|
|
command_name="evals",
|
|
install_command=r"pip install arcade-tdk",
|
|
)
|
|
|
|
models_list = models.split(",") # Use 'models_list' to avoid shadowing
|
|
|
|
# Resolve the API key for the provider
|
|
resolved_api_key = resolve_provider_api_key(provider, provider_api_key)
|
|
if not resolved_api_key:
|
|
provider_env_vars = {
|
|
Provider.OPENAI: "OPENAI_API_KEY",
|
|
}
|
|
env_var_name = provider_env_vars.get(provider, f"{provider.upper()}_API_KEY")
|
|
handle_cli_error(
|
|
f"API key not found for provider '{provider.value}'. "
|
|
f"Please provide it via --provider-api-key,-k argument, set the {env_var_name} environment variable, "
|
|
f"or add it to a .env file in the current directory.",
|
|
should_exit=True,
|
|
)
|
|
|
|
eval_files = get_eval_files(directory)
|
|
if not eval_files:
|
|
return
|
|
|
|
console.print("\nRunning evaluations", style="bold")
|
|
|
|
# Use the new function to load eval suites
|
|
eval_suites = load_eval_suites(eval_files)
|
|
|
|
if not eval_suites:
|
|
console.print("No evaluation suites to run.", style="bold yellow")
|
|
return
|
|
|
|
if show_details:
|
|
suite_label = "suite" if len(eval_suites) == 1 else "suites"
|
|
console.print(
|
|
f"\nFound {len(eval_suites)} {suite_label} in the evaluation files.",
|
|
style="bold",
|
|
)
|
|
|
|
async def run_evaluations() -> None:
|
|
all_evaluations = []
|
|
tasks = []
|
|
for suite_func in eval_suites:
|
|
console.print(
|
|
Text.assemble(
|
|
("Running evaluations in ", "bold"),
|
|
(suite_func.__name__, "bold blue"),
|
|
)
|
|
)
|
|
for model in models_list:
|
|
task = asyncio.create_task(
|
|
suite_func(
|
|
provider_api_key=resolved_api_key,
|
|
model=model,
|
|
max_concurrency=max_concurrent,
|
|
)
|
|
)
|
|
tasks.append(task)
|
|
|
|
# Track progress and results as suite functions complete
|
|
with tqdm(total=len(tasks), desc="Evaluations Progress") as pbar:
|
|
results = []
|
|
for f in asyncio.as_completed(tasks):
|
|
results.append(await f)
|
|
pbar.update(1)
|
|
|
|
# TODO error handling on each eval
|
|
all_evaluations.extend(results)
|
|
display_eval_results(all_evaluations, show_details=show_details)
|
|
|
|
try:
|
|
asyncio.run(run_evaluations())
|
|
except Exception as e:
|
|
handle_cli_error("Failed to run evaluations", e, debug)
|
|
|
|
|
|
@cli.command(
|
|
help="Start tool server worker with locally installed tools",
|
|
rich_help_panel="Launch",
|
|
hidden=True,
|
|
)
|
|
def serve(
|
|
host: str = typer.Option(
|
|
"127.0.0.1",
|
|
help="Host for the app, from settings by default.",
|
|
show_default=True,
|
|
),
|
|
port: int = typer.Option(
|
|
"8002",
|
|
"-p",
|
|
"--port",
|
|
help="Port for the app, defaults to ",
|
|
show_default=True,
|
|
),
|
|
disable_auth: bool = typer.Option(
|
|
True,
|
|
"--no-auth",
|
|
help="Disable authentication for the worker. Not recommended for production.",
|
|
show_default=True,
|
|
),
|
|
otel_enable: bool = typer.Option(
|
|
False, "--otel-enable", help="Send logs to OpenTelemetry", show_default=True
|
|
),
|
|
mcp: bool = typer.Option(
|
|
False, "--mcp", help="Run as a local MCP server over stdio", show_default=True
|
|
),
|
|
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
|
reload: bool = typer.Option(
|
|
False,
|
|
"--reload",
|
|
help="Enable auto-reloading when toolkit or server files change.",
|
|
show_default=True,
|
|
),
|
|
) -> None:
|
|
"""
|
|
Start a local Arcade Worker server.
|
|
"""
|
|
console.log(
|
|
"⚠️ This command is deprecated and will be removed in a future version.", style="yellow"
|
|
)
|
|
require_dependency(
|
|
package_name="arcade_serve",
|
|
command_name="serve",
|
|
install_command=r"pip install 'arcade-serve'",
|
|
)
|
|
|
|
from arcade_cli.serve import serve_default_worker
|
|
|
|
try:
|
|
serve_default_worker(
|
|
host,
|
|
port,
|
|
disable_auth=disable_auth,
|
|
enable_otel=otel_enable,
|
|
debug=debug,
|
|
mcp=mcp,
|
|
reload=reload,
|
|
)
|
|
except KeyboardInterrupt:
|
|
typer.Exit()
|
|
except Exception as e:
|
|
handle_cli_error("Failed to start Arcade Worker", e, debug)
|
|
|
|
|
|
@cli.command(
|
|
help="Configure MCP clients to connect to your server", rich_help_panel="Tool Development"
|
|
)
|
|
def configure(
|
|
client: str = typer.Argument(
|
|
...,
|
|
help="The MCP client to configure (claude, cursor, vscode)",
|
|
),
|
|
server_name: Optional[str] = typer.Option(
|
|
None,
|
|
"--server",
|
|
"-s",
|
|
help="Name of the server to connect to (defaults to current directory name)",
|
|
),
|
|
from_local: bool = typer.Option(
|
|
False,
|
|
"--from-local",
|
|
help="Connect to a local MCP server",
|
|
is_flag=True,
|
|
),
|
|
from_arcade: bool = typer.Option(
|
|
False,
|
|
"--from-arcade",
|
|
help="Connect to an Arcade Cloud MCP server",
|
|
is_flag=True,
|
|
),
|
|
port: int = typer.Option(
|
|
8000,
|
|
"--port",
|
|
"-p",
|
|
help="Port for local servers",
|
|
),
|
|
path: Optional[Path] = typer.Option(
|
|
None,
|
|
"--path",
|
|
"-f",
|
|
exists=False,
|
|
help="Optional path to a specific MCP client config file (overrides default path)",
|
|
),
|
|
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
|
) -> None:
|
|
"""
|
|
Configure MCP clients to connect to your server.
|
|
|
|
Examples:
|
|
arcade configure claude --from-local
|
|
arcade configure cursor --from-local --port 8080
|
|
arcade configure vscode --from-local --path .vscode/mcp.json
|
|
arcade configure claude --from-arcade --server my-toolkit
|
|
"""
|
|
from arcade_cli.configure import configure_client
|
|
|
|
try:
|
|
configure_client(
|
|
client=client,
|
|
server_name=server_name,
|
|
from_local=from_local,
|
|
from_arcade=from_arcade,
|
|
port=port,
|
|
path=path,
|
|
)
|
|
except Exception as e:
|
|
handle_cli_error(f"Failed to configure {client}", e, debug)
|
|
|
|
|
|
@cli.command(help="Deploy toolkits to Arcade Cloud", rich_help_panel="Deployment")
|
|
def deploy(
|
|
deployment_file: str = typer.Option(
|
|
"worker.toml",
|
|
"--deployment-file",
|
|
"-d",
|
|
help="The deployment file to deploy.",
|
|
),
|
|
cloud_host: str = typer.Option(
|
|
PROD_CLOUD_HOST,
|
|
"--cloud-host",
|
|
"-c",
|
|
help="The Arcade Cloud host to deploy to.",
|
|
hidden=True,
|
|
),
|
|
cloud_port: Optional[int] = typer.Option(
|
|
None,
|
|
"--cloud-port",
|
|
"-cp",
|
|
help="The port of the Arcade Cloud host.",
|
|
hidden=True,
|
|
),
|
|
host: str = typer.Option(
|
|
PROD_ENGINE_HOST,
|
|
"--host",
|
|
"-h",
|
|
help="The Arcade Engine host to register the worker to.",
|
|
),
|
|
port: Optional[int] = typer.Option(
|
|
None,
|
|
"--port",
|
|
"-p",
|
|
help="The port of the Arcade Engine host.",
|
|
),
|
|
force_tls: bool = typer.Option(
|
|
False,
|
|
"--tls",
|
|
help="Whether to force TLS for the connection to the Arcade Engine. If not specified, the connection will use TLS if the engine URL uses a 'https' scheme.",
|
|
),
|
|
force_no_tls: bool = typer.Option(
|
|
False,
|
|
"--no-tls",
|
|
help="Whether to disable TLS for the connection to the Arcade Engine.",
|
|
),
|
|
debug: bool = typer.Option(False, "--debug", help="Show debug information"),
|
|
) -> None:
|
|
"""
|
|
Deploy a worker to Arcade Cloud.
|
|
"""
|
|
|
|
config = validate_and_get_config()
|
|
engine_url = compute_base_url(force_tls, force_no_tls, host, port)
|
|
engine_client = Arcade(api_key=config.api.key, base_url=engine_url)
|
|
cloud_url = compute_base_url(force_tls, force_no_tls, cloud_host, cloud_port)
|
|
cloud_client = httpx.Client(
|
|
base_url=cloud_url, headers={"Authorization": f"Bearer {config.api.key}"}
|
|
)
|
|
|
|
# Fetch deployment configuration
|
|
try:
|
|
deployment = Deployment.from_toml(Path(deployment_file))
|
|
except Exception as e:
|
|
handle_cli_error("Failed to parse deployment file", e, debug)
|
|
|
|
with console.status(f"Deploying {len(deployment.worker)} workers"):
|
|
for worker in deployment.worker:
|
|
console.log(f"Deploying '{worker.config.id}...'", style="dim")
|
|
try:
|
|
# Discover and upload secrets
|
|
required_secret_keys = worker.get_required_secrets()
|
|
for secret_key in required_secret_keys:
|
|
secret_value = os.getenv(secret_key)
|
|
if not secret_value:
|
|
console.log(
|
|
f"⚠️ Secret '{secret_key}' not found in environment, skipping.",
|
|
style="yellow",
|
|
)
|
|
continue
|
|
try:
|
|
secret._upsert_secret_to_engine(
|
|
engine_url, config.api.key, secret_key, secret_value
|
|
)
|
|
except Exception as e:
|
|
handle_cli_error(
|
|
f"Failed to upload secret '{secret_key}'", e, debug, should_exit=False
|
|
)
|
|
else:
|
|
console.log(
|
|
f"✅ Secret '{secret_key}' uploaded successfully",
|
|
style="dim green",
|
|
)
|
|
|
|
# Attempt to deploy worker
|
|
worker.request().execute(cloud_client, engine_client)
|
|
console.log(
|
|
f"✅ Worker '{worker.config.id}' deployed successfully.",
|
|
style="dim",
|
|
)
|
|
except Exception as e:
|
|
handle_cli_error(f"Failed to deploy worker '{worker.config.id}'", e, debug)
|
|
|
|
|
|
@cli.command(help="Open the Arcade Dashboard in a web browser", rich_help_panel="User")
|
|
def dashboard(
|
|
host: str = typer.Option(
|
|
PROD_ENGINE_HOST,
|
|
"-h",
|
|
"--host",
|
|
help="The Arcade Engine host that serves the dashboard.",
|
|
),
|
|
port: Optional[int] = typer.Option(
|
|
None,
|
|
"-p",
|
|
"--port",
|
|
help="The port of the Arcade Engine.",
|
|
),
|
|
local: bool = typer.Option(
|
|
False,
|
|
"--local",
|
|
"-l",
|
|
help="Open the local dashboard instead of the default remote dashboard.",
|
|
),
|
|
force_tls: bool = typer.Option(
|
|
False,
|
|
"--tls",
|
|
help="Whether to force TLS for the connection to the Arcade Engine.",
|
|
),
|
|
force_no_tls: bool = typer.Option(
|
|
False,
|
|
"--no-tls",
|
|
help="Whether to disable TLS for the connection to the Arcade Engine.",
|
|
),
|
|
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
|
) -> None:
|
|
"""Opens the Arcade Dashboard in a web browser.
|
|
|
|
The Dashboard is a web-based Arcade user interface that is served by the Arcade Engine.
|
|
"""
|
|
try:
|
|
if local:
|
|
host = "localhost"
|
|
|
|
# Construct base URL (for both health check and dashboard)
|
|
base_url = compute_base_url(force_tls, force_no_tls, host, port)
|
|
dashboard_url = f"{base_url}/dashboard"
|
|
|
|
# Try to hit /health endpoint on engine and warn if it is down
|
|
config = validate_and_get_config()
|
|
with Arcade(api_key=config.api.key, base_url=base_url) as client:
|
|
log_engine_health(client)
|
|
|
|
# Open the dashboard in a browser
|
|
console.print(f"Opening Arcade Dashboard at {dashboard_url}")
|
|
if not webbrowser.open(dashboard_url):
|
|
console.print(
|
|
f"If a browser doesn't open automatically, copy this URL and paste it into your browser: {dashboard_url}",
|
|
style="dim",
|
|
)
|
|
except Exception as e:
|
|
handle_cli_error("Failed to open dashboard", e, debug)
|
|
|
|
|
|
@cli.command(
|
|
help=(
|
|
"Generate documentation for a toolkit. "
|
|
"Note: make sure to have the toolkit installed in your current Python environment "
|
|
"before running this command."
|
|
),
|
|
rich_help_panel="Tool Development",
|
|
)
|
|
def docs(
|
|
toolkit_name: str = typer.Option(
|
|
...,
|
|
"--toolkit-name",
|
|
"-n",
|
|
help="The name of the toolkit to generate documentation for.",
|
|
),
|
|
toolkit_dir: str = typer.Option(
|
|
...,
|
|
"--toolkit-dir",
|
|
"-t",
|
|
help=(
|
|
"The path to the toolkit root directory (where the toolkit code is implemented). "
|
|
"Works with relative and absolute paths."
|
|
),
|
|
),
|
|
docs_dir: str = typer.Option(
|
|
...,
|
|
"--docs-dir",
|
|
"-r",
|
|
help="The path to the root of the Arcade docs repository. Works with relative and absolute paths.",
|
|
),
|
|
docs_section: str = typer.Option(
|
|
"",
|
|
"--docs-section",
|
|
"-s",
|
|
help=(
|
|
"The section of the docs to generate documentation for. E.g. 'productivity', 'sales'. "
|
|
"This should be the name of the folder in /pages/toolkits. "
|
|
"Defaults to an empty string (generate the docs in the root of /pages/toolkits)"
|
|
),
|
|
),
|
|
openai_model: str = typer.Option(
|
|
"gpt-5-mini",
|
|
"--openai-model",
|
|
"-m",
|
|
help=(
|
|
"A few parts of the documentation are generated using OpenAI API. "
|
|
"Choose one of the 'gpt-4o' and 'gpt-5' series models."
|
|
),
|
|
show_default=True,
|
|
),
|
|
openai_api_key: str = typer.Option(
|
|
None,
|
|
"--openai-api-key",
|
|
"-o",
|
|
help="The OpenAI API key. If not provided, will get it from the `OPENAI_API_KEY` env var.",
|
|
),
|
|
skip_tool_call_examples: bool = typer.Option(
|
|
False,
|
|
"--skip-tool-call-examples",
|
|
"-se",
|
|
help="Whether to skip generating tool call examples in Python and Javascript.",
|
|
show_default=True,
|
|
),
|
|
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
|
) -> None:
|
|
if not openai_model.startswith("gpt-4o") and not openai_model.startswith("gpt-5"):
|
|
console.print(
|
|
f"Attention: '{openai_model}' is not a valid OpenAI model. "
|
|
"Please choose one of the 'gpt-4o' and 'gpt-5' series models.",
|
|
style="bold red",
|
|
)
|
|
raise typer.Exit()
|
|
|
|
try:
|
|
success = generate_toolkit_docs(
|
|
console=console,
|
|
toolkit_name=toolkit_name,
|
|
toolkit_dir=toolkit_dir,
|
|
docs_dir=docs_dir,
|
|
docs_section=docs_section,
|
|
openai_model=openai_model,
|
|
openai_api_key=openai_api_key,
|
|
tool_call_examples=not skip_tool_call_examples,
|
|
debug=debug,
|
|
)
|
|
except Exception as error:
|
|
handle_cli_error(
|
|
message=f"Failed to generate documentation for '{toolkit_name}' in '{docs_dir}'",
|
|
error=error,
|
|
debug=debug,
|
|
)
|
|
success = False
|
|
|
|
if success:
|
|
console.print(
|
|
f"Generated documentation for '{toolkit_name}' in '{docs_dir}'",
|
|
style="bold green",
|
|
)
|
|
else:
|
|
console.print(
|
|
f"Failed to generate documentation for '{toolkit_name}' in '{docs_dir}'",
|
|
style="bold red",
|
|
)
|
|
|
|
|
|
@cli.command(
|
|
name="generate-toolkit-docs",
|
|
help=(
|
|
"Generate documentation for a toolkit. "
|
|
"Note: make sure to have the toolkit installed in your current Python environment "
|
|
"before running this command. "
|
|
"Obs.: this command is here for backwards compatibility, use `arcade docs` instead."
|
|
),
|
|
rich_help_panel="Tool Development",
|
|
hidden=True,
|
|
)
|
|
def generate_toolkit_docs_command(
|
|
toolkit_name: str = typer.Option(
|
|
...,
|
|
"--toolkit-name",
|
|
"-n",
|
|
help="The name of the toolkit to generate documentation for.",
|
|
),
|
|
toolkit_dir: str = typer.Option(
|
|
...,
|
|
"--toolkit-dir",
|
|
"-t",
|
|
help=(
|
|
"The path to the toolkit root directory (where the toolkit code is implemented). "
|
|
"Works with relative and absolute paths."
|
|
),
|
|
),
|
|
docs_dir: str = typer.Option(
|
|
...,
|
|
"--docs-dir",
|
|
"-r",
|
|
help="The path to the root of the Arcade docs repository. Works with relative and absolute paths.",
|
|
),
|
|
docs_section: str = typer.Option(
|
|
"",
|
|
"--docs-section",
|
|
"-s",
|
|
help=(
|
|
"The section of the docs to generate documentation for. E.g. 'productivity', 'sales'. "
|
|
"This should be the name of the folder in /pages/toolkits. "
|
|
"Defaults to an empty string (generate the docs in the root of /pages/toolkits)"
|
|
),
|
|
),
|
|
openai_model: str = typer.Option(
|
|
"gpt-4o-mini",
|
|
"--openai-model",
|
|
"-m",
|
|
help=(
|
|
"A few parts of the documentation are generated using OpenAI API. "
|
|
"This argument controls which OpenAI model to use. "
|
|
"E.g. 'gpt-4o', 'gpt-4o-mini'."
|
|
),
|
|
show_default=True,
|
|
),
|
|
openai_api_key: str = typer.Option(
|
|
None,
|
|
"--openai-api-key",
|
|
"-o",
|
|
help="The OpenAI API key. If not provided, will get it from the `OPENAI_API_KEY` env var.",
|
|
),
|
|
tool_call_examples: bool = typer.Option(
|
|
True,
|
|
"--tool-call-examples",
|
|
"-e",
|
|
help="Whether to generate tool call examples in Python and Javascript.",
|
|
show_default=True,
|
|
),
|
|
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
|
) -> None:
|
|
skip_tool_call_examples = not tool_call_examples
|
|
docs(
|
|
toolkit_name=toolkit_name,
|
|
toolkit_dir=toolkit_dir,
|
|
docs_dir=docs_dir,
|
|
docs_section=docs_section,
|
|
openai_model=openai_model,
|
|
openai_api_key=openai_api_key,
|
|
skip_tool_call_examples=skip_tool_call_examples,
|
|
debug=debug,
|
|
)
|
|
|
|
|
|
@cli.callback()
|
|
def main_callback(
|
|
ctx: typer.Context,
|
|
_: Optional[bool] = typer.Option(
|
|
None,
|
|
"-v",
|
|
"--version",
|
|
callback=version_callback,
|
|
is_eager=True,
|
|
help="Print version and exit.",
|
|
),
|
|
) -> None:
|
|
# Commands that do not require a logged in user
|
|
public_commands = {
|
|
login.__name__,
|
|
logout.__name__,
|
|
dashboard.__name__,
|
|
evals.__name__,
|
|
}
|
|
if ctx.invoked_subcommand in public_commands:
|
|
return
|
|
|
|
if not check_existing_login(suppress_message=True):
|
|
console.print("Not logged in to Arcade CLI. Use ", style="bold red", end="")
|
|
console.print("arcade login", style="bold green")
|
|
raise typer.Exit()
|