Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
631 changes: 631 additions & 0 deletions libs/arcade-cli/arcade_cli/deploy.py

Large diffs are not rendered by default.

500 changes: 0 additions & 500 deletions libs/arcade-cli/arcade_cli/deployment.py

This file was deleted.

158 changes: 86 additions & 72 deletions libs/arcade-cli/arcade_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pathlib import Path
from typing import Optional

import httpx
import click
import typer
from arcadepy import Arcade
from rich.console import Console
Expand All @@ -23,7 +23,6 @@
PROD_CLOUD_HOST,
PROD_ENGINE_HOST,
)
from arcade_cli.deployment import Deployment
from arcade_cli.display import (
display_eval_results,
)
Expand Down Expand Up @@ -531,106 +530,121 @@ def configure(
handle_cli_error(f"Failed to configure {client}", e, debug)


@cli.command(help="Deploy servers to Arcade Cloud", rich_help_panel="Run", hidden=True)
@cli.command(
name="deploy",
help="Deploy MCP servers to Arcade",
rich_help_panel="Run",
)
def deploy(
deployment_file: str = typer.Option(
"worker.toml",
"--deployment-file",
"-d",
help="The deployment file to deploy.",
entrypoint: str = typer.Option(
"server.py",
"--entrypoint",
"-e",
help="Relative path to the Python file that runs the MCPApp instance (relative to project root). This file must execute the `run()` method on your `MCPApp` instance when invoked directly.",
),
cloud_host: str = typer.Option(
PROD_CLOUD_HOST,
"--cloud-host",
"-c",
help="The Arcade Cloud host to deploy to.",
hidden=True,
skip_validate: bool = typer.Option(
False,
"--skip-validate",
"--yolo",
help="Skip running the server locally for health/metadata checks. "
"When set, you must provide `--server-name` and `--server-version`. "
"Secret handling is controlled by `--secrets`.",
rich_help_panel="Advanced",
),
cloud_port: Optional[int] = typer.Option(
server_name: Optional[str] = typer.Option(
None,
"--cloud-port",
"-cp",
help="The port of the Arcade Cloud host.",
hidden=True,
"--server-name",
"-n",
help="Explicit server name to use when `--skip-validate` is set. Only used when `--skip-validate` is set.",
rich_help_panel="Advanced",
),
server_version: Optional[str] = typer.Option(
None,
"--server-version",
"-v",
help="Explicit server version to use when `--skip-validate` is set. Only used when `--skip-validate` is set.",
rich_help_panel="Advanced",
),
secrets: str = typer.Option(
"auto",
"--secrets",
"-s",
help=(
"How to upsert secrets before deploy:\n"
" `auto` (default): During validation, discover required secret KEYS and upsert only those. "
"If `--skip-validate` is set, `auto` becomes `skip`.\n"
" `all`: Upsert every key/value pair from your server's .env file regardless of what the server needs.\n"
" `skip`: Do not upsert any secrets (assumes they are already present in Arcade)."
),
show_choices=True,
rich_help_panel="Advanced",
click_type=click.Choice(["auto", "all", "skip"], case_sensitive=False),
),
host: str = typer.Option(
PROD_ENGINE_HOST,
"--host",
"-h",
help="The Arcade Engine host to register the server to.",
help="The Arcade Engine host to deploy to",
hidden=True,
),
port: Optional[int] = typer.Option(
None,
"--port",
"-p",
help="The port of the Arcade Engine host.",
help="The port of the Arcade Engine",
hidden=True,
),
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.",
help="Force TLS for the connection to the Arcade Engine",
hidden=True,
),
force_no_tls: bool = typer.Option(
False,
"--no-tls",
help="Whether to disable TLS for the connection to the Arcade Engine.",
help="Disable TLS for the connection to the Arcade Engine",
hidden=True,
),
debug: bool = typer.Option(False, "--debug", help="Show debug information"),
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
) -> None:
"""
Deploy a server to Arcade Cloud.
Deploy an MCP server directly to Arcade Engine.

This command should be run from the root of your MCP server package
(the directory containing pyproject.toml).

Examples:
cd my_mcp_server/
arcade deploy
arcade deploy --entrypoint src/server.py
arcade deploy --skip-validate --server-name my_server_name --server-version 1.0.0
"""
from arcade_cli.deploy import deploy_server_logic

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}"}
)
if skip_validate and not (server_name and server_version):
handle_cli_error(
"When --skip-validate is set, you must provide --server-name and --server-version.",
should_exit=True,
)
if skip_validate and secrets == "auto":
secrets = "skip"

# Fetch deployment configuration
try:
deployment = Deployment.from_toml(Path(deployment_file))
deploy_server_logic(
entrypoint=entrypoint,
skip_validate=skip_validate,
server_name=server_name,
server_version=server_version,
secrets=secrets,
host=host,
port=port,
force_tls=force_tls,
force_no_tls=force_no_tls,
debug=debug,
)
except Exception as e:
handle_cli_error("Failed to parse deployment file", e, debug)

with console.status(f"Deploying {len(deployment.worker)} servers"):
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"✅ Server '{worker.config.id}' deployed successfully.",
style="dim",
)
except Exception as e:
handle_cli_error(f"Failed to deploy server '{worker.config.id}'", e, debug)
handle_cli_error("Failed to deploy server", e, debug)


@cli.command(help="Open the Arcade Dashboard in a web browser", rich_help_panel="User")
Expand Down
21 changes: 17 additions & 4 deletions libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,9 @@ def run(
logger.error("No tools added to the server. Use @app.tool decorator or app.add_tool().")
sys.exit(1)

host, port, transport = MCPApp._get_configuration_overrides(host, port, transport)
host, port, transport, reload = MCPApp._get_configuration_overrides(
host, port, transport, reload
)

# Since the transport could have changed since __init__, we need to setup logging again
self._setup_logging(transport == "stdio")
Expand Down Expand Up @@ -257,8 +259,8 @@ def run(

@staticmethod
def _get_configuration_overrides(
host: str, port: int, transport: TransportType
) -> tuple[str, int, TransportType]:
host: str, port: int, transport: TransportType, reload: bool
) -> tuple[str, int, TransportType, bool]:
"""Get configuration overrides from environment variables."""
if envvar_transport := os.getenv("ARCADE_SERVER_TRANSPORT"):
transport = envvar_transport
Expand All @@ -284,7 +286,18 @@ def _get_configuration_overrides(
f"Using '{port}' as port from ARCADE_SERVER_PORT environment variable"
)

return host, port, transport
if envvar_reload := os.getenv("ARCADE_SERVER_RELOAD"):
if envvar_reload.lower() not in ["0", "1"]:
logger.warning(
f"Invalid reload: '{envvar_reload}' from ARCADE_SERVER_RELOAD environment variable. Using default reload {reload}"
)
else:
reload = bool(int(envvar_reload))
logger.debug(
f"Using '{reload}' as reload from ARCADE_SERVER_RELOAD environment variable"
)

return host, port, transport, reload


class _ToolsAPI:
Expand Down
53 changes: 45 additions & 8 deletions libs/tests/arcade_mcp_server/test_mcp_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,51 +226,88 @@ def test_get_configuration_overrides(self, monkeypatch):
monkeypatch.delenv("ARCADE_SERVER_TRANSPORT", raising=False)
monkeypatch.delenv("ARCADE_SERVER_HOST", raising=False)
monkeypatch.delenv("ARCADE_SERVER_PORT", raising=False)
monkeypatch.delenv("ARCADE_SERVER_RELOAD", raising=False)

# Test default values (no environment variables)
host, port, transport = MCPApp._get_configuration_overrides("127.0.0.1", 8000, "http")
host, port, transport, reload = MCPApp._get_configuration_overrides(
"127.0.0.1", 8000, "http", False
)
assert host == "127.0.0.1"
assert port == 8000
assert transport == "http"
assert not reload

# Test transport override
monkeypatch.setenv("ARCADE_SERVER_TRANSPORT", "stdio")
host, port, transport = MCPApp._get_configuration_overrides("127.0.0.1", 8000, "http")
host, port, transport, reload = MCPApp._get_configuration_overrides(
"127.0.0.1", 8000, "http", False
)
assert transport == "stdio"
monkeypatch.delenv("ARCADE_SERVER_TRANSPORT")

# Test host override (only works with HTTP transport)
monkeypatch.setenv("ARCADE_SERVER_TRANSPORT", "http")
monkeypatch.setenv("ARCADE_SERVER_HOST", "192.168.1.1")
host, port, transport = MCPApp._get_configuration_overrides("127.0.0.1", 8000, "http")
host, port, transport, reload = MCPApp._get_configuration_overrides(
"127.0.0.1", 8000, "http", False
)
assert host == "192.168.1.1"
assert transport == "http"
monkeypatch.delenv("ARCADE_SERVER_HOST")
monkeypatch.delenv("ARCADE_SERVER_TRANSPORT")

# Test port override (only works with HTTP transport)
monkeypatch.setenv("ARCADE_SERVER_PORT", "9000")
host, port, transport = MCPApp._get_configuration_overrides("127.0.0.1", 8000, "http")
host, port, transport, reload = MCPApp._get_configuration_overrides(
"127.0.0.1", 8000, "http", False
)
assert port == 9000
monkeypatch.delenv("ARCADE_SERVER_PORT")

# Test invalid port value
monkeypatch.setenv("ARCADE_SERVER_TRANSPORT", "http")
monkeypatch.setenv("ARCADE_SERVER_PORT", "invalid_port")
host, port, transport = MCPApp._get_configuration_overrides("127.0.0.1", 8000, "http")
host, port, transport, reload = MCPApp._get_configuration_overrides(
"127.0.0.1", 8000, "http", False
)
assert port == 8000 # Should keep the default value
monkeypatch.delenv("ARCADE_SERVER_PORT")
monkeypatch.delenv("ARCADE_SERVER_TRANSPORT")

# Test host/port with stdio transport
# Test valid reload value
monkeypatch.setenv("ARCADE_SERVER_TRANSPORT", "http")
monkeypatch.setenv("ARCADE_SERVER_RELOAD", "1")
host, port, transport, reload = MCPApp._get_configuration_overrides(
"127.0.0.1", 8000, "http", False
)
assert reload
monkeypatch.delenv("ARCADE_SERVER_RELOAD")
monkeypatch.delenv("ARCADE_SERVER_TRANSPORT")

# Test invalid reload value
monkeypatch.setenv("ARCADE_SERVER_TRANSPORT", "http")
monkeypatch.setenv("ARCADE_SERVER_RELOAD", "invalid_reload")
host, port, transport, reload = MCPApp._get_configuration_overrides(
"127.0.0.1", 8000, "http", False
)
assert not reload # Should keep the default value
monkeypatch.delenv("ARCADE_SERVER_RELOAD")
monkeypatch.delenv("ARCADE_SERVER_TRANSPORT")

# Test host/port/reload with stdio transport
monkeypatch.setenv("ARCADE_SERVER_TRANSPORT", "stdio")
monkeypatch.setenv("ARCADE_SERVER_HOST", "192.168.1.1")
monkeypatch.setenv("ARCADE_SERVER_PORT", "9000")
host, port, transport = MCPApp._get_configuration_overrides("127.0.0.1", 8000, "http")
# For stdio, host and port are still returned but not used by the server
monkeypatch.setenv("ARCADE_SERVER_RELOAD", "true")
host, port, transport, reload = MCPApp._get_configuration_overrides(
"127.0.0.1", 8000, "http", False
)
# For stdio, host, port, and reload are still returned but not used by the server
assert host == "127.0.0.1" # Host should remain unchanged for stdio transport
assert port == 8000 # Port should remain unchanged for stdio transport
assert transport == "stdio"
assert not reload
monkeypatch.delenv("ARCADE_SERVER_RELOAD")
monkeypatch.delenv("ARCADE_SERVER_HOST")
monkeypatch.delenv("ARCADE_SERVER_PORT")
monkeypatch.delenv("ARCADE_SERVER_TRANSPORT")
Loading