diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index c4e66c03..00000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,74 +0,0 @@ -FROM python:3.11-slim - -# Define build arguments with default values -ARG PORT=8001 -ARG HOST=0.0.0.0 -ARG INSTALL_TOOLKITS=true - -# Set environment variables using the build arguments -ENV PORT=${PORT} -ENV HOST=${HOST} -ENV OTEL_ENABLE=false -ENV ARCADE_WORK_DIR=/app - -# Install system dependencies -RUN apt-get update && apt-get upgrade -y && apt-get install -y \ - libssl-dev \ - python3-dev \ - curl \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -RUN pip install --upgrade setuptools>=78.1.1 - -WORKDIR /app - -# Copy the dist directory contents into the container -COPY ./dist /app/dist/ - -# Copy the toolkits.txt file into the container -COPY ./docker/toolkits.txt /app/ - -# Expose the port -EXPOSE $PORT - -# List wheel files for debugging purposes -RUN ls -la /app/dist/ - -# Install the worker and CLI package -RUN python -m pip install \ - /app/dist/arcade_serve-*.whl \ - /app/dist/arcade_mcp-*.whl - /app/dist/arcade_mcp_server-*.whl - -# Conditionally install toolkit wheels from dist directory if INSTALL_TOOLKITS is true and the toolkit is in toolkits.txt -RUN if [ "$INSTALL_TOOLKITS" = "true" ] ; then \ - while IFS= read -r toolkit; do \ - # Skip empty lines and comments (lines starting with #) - if [ -n "$toolkit" ] && [ "${toolkit#\#}" = "$toolkit" ]; then \ - # Convert toolkit name to match wheel filename format (replace - with _) - wheel_name=$(echo "$toolkit" | sed 's/-/_/g'); \ - wheel_file="/app/dist/${wheel_name}-"*.whl; \ - # Check if this is not a core package and if the wheel file exists - if [ "$wheel_name" != "arcade_core" ] && \ - [ "$wheel_name" != "arcade_serve" ] && \ - [ "$wheel_name" != "arcade_mcp" ] && \ - [ "$wheel_name" != "arcade_mcp_server" ] && \ - [ "$wheel_name" != "arcade_tdk" ]; then \ - if ls $wheel_file 1> /dev/null 2>&1; then \ - echo "Installing $toolkit from $wheel_file"; \ - python -m pip install $wheel_file; \ - else \ - echo "Warning: Wheel file not found for $toolkit (looked for $wheel_file)"; \ - fi; \ - else \ - echo "Skipping core package: $toolkit"; \ - fi; \ - fi; \ - done < /app/toolkits.txt ; \ - fi - -# Run the arcade worker -COPY docker/start.sh /app/start.sh -RUN chmod +x /app/start.sh -CMD ["/app/start.sh"] diff --git a/docker/Makefile b/docker/Makefile deleted file mode 100644 index 126700ec..00000000 --- a/docker/Makefile +++ /dev/null @@ -1,103 +0,0 @@ -VENDOR ?= ArcadeAI -PROJECT ?= ArcadeAI -SOURCE ?= https://github.com/ArcadeAI/arcade-mcp -LICENSE ?= MIT -DESCRIPTION ?= "Arcade Worker for LLM Tool Serving" -REPOSITORY ?= arcadeai/worker -ARCH ?= $(shell uname -m) - -VERSION ?= 0.1.0.dev0 -COMMIT ?= $(shell git describe --dirty --always --abbrev=15) -BUILD_DATE ?= $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") -IMAGE_NAME ?= worker -PORT ?= 8002 -INSTALL_TOOLKITS ?= true - -# If INSTALL_TOOLKITS is true, we are building the worker image with all toolkits -# Otherwise, we are building the worker image with no toolkits -ifeq ($(INSTALL_TOOLKITS), true) - REPOSITORY := $(REPOSITORY) -else - REPOSITORY := $(REPOSITORY)-base -endif - -VERSION_TAG := $(VERSION)-$(ARCH) - -.PHONY: docker-build -docker-build: ## Build the Docker container - @echo "šŸ› ļø Building Docker image ($(VERSION_TAG)).." - @echo "- Commit: $(COMMIT)" - @echo "- Build Date: $(BUILD_DATE)" - @docker build --build-arg PORT=$(PORT) -f Dockerfile -t $(REPOSITORY):$(VERSION_TAG) \ - --build-arg INSTALL_TOOLKITS=$(INSTALL_TOOLKITS) \ - --build-arg PORT=$(PORT) \ - --build-arg VERSION="$(VERSION)" \ - --build-arg COMMIT="$(COMMIT)" \ - --build-arg BUILD_DATE="$(BUILD_DATE)" \ - --label=org.opencontainers.image.vendor="$(VENDOR)" \ - --label=org.opencontainers.image.title="$(PROJECT)" \ - --label=org.opencontainers.image.revision="$(COMMIT)" \ - --label=org.opencontainers.image.version="$(VERSION_TAG)" \ - --label=org.opencontainers.image.created="$(BUILD_DATE)" \ - --label=org.opencontainers.image.source="$(SOURCE)" \ - --label=org.opencontainers.image.licenses="$(LICENSE)" \ - --label=org.opencontainers.image.description=$(DESCRIPTION) \ - .. - -ghcr-manifest: ## Make a manifest file for the image - @echo "šŸ› ļø Build manifest file for $(REPOSITORY):$(VERSION).." - @echo "- Commit: $(COMMIT)" - @echo "- Build Date: $(BUILD_DATE)" - @export DOCKER_CLI_EXPERIMENTAL=enabled - @echo "- Creating manifest ghcr.io/$(REPOSITORY):$(VERSION)" - @docker manifest create ghcr.io/$(REPOSITORY):$(VERSION) \ - --amend ghcr.io/$(REPOSITORY):$(VERSION)-arm64 \ - --amend ghcr.io/$(REPOSITORY):$(VERSION)-amd64 - @echo "- Creating manifest ghcr.io/$(REPOSITORY):latest" - @docker manifest create ghcr.io/$(REPOSITORY):latest \ - --amend ghcr.io/$(REPOSITORY):$(VERSION)-arm64 \ - --amend ghcr.io/$(REPOSITORY):$(VERSION)-amd64 - @echo "- Inspecting manifest ghcr.io/$(REPOSITORY):$(VERSION)" - @docker manifest inspect ghcr.io/$(REPOSITORY):$(VERSION) - @echo "- Inspecting manifest ghcr.io/$(REPOSITORY):latest" - @docker manifest inspect ghcr.io/$(REPOSITORY):latest - @echo "- Pushing manifest ghcr.io/$(REPOSITORY):$(VERSION)" - @docker manifest push ghcr.io/$(REPOSITORY):$(VERSION) - @echo "- Pushing manifest ghcr.io/$(REPOSITORY):latest" - @docker manifest push ghcr.io/$(REPOSITORY):latest - - -.PHONY: docker-run -docker-run: ## Run the Docker container - @echo "\nšŸš€ Run the container with the following ..." - @echo ">>> docker run -d -p $(PORT):$(PORT) $(REPOSITORY):$(VERSION_TAG)" - - -.PHONY: publish-ghcr -publish-ghcr: - @echo "🚚 Pushing the Agent image to GHCR.." - @docker tag $(REPOSITORY):$(VERSION_TAG) ghcr.io/$(REPOSITORY):$(VERSION_TAG) - @echo "- pushing ghcr.io/$(REPOSITORY):$(VERSION_TAG)" - @docker push ghcr.io/$(REPOSITORY):$(VERSION_TAG) - @echo $(VERSION) | grep -q $(RC_PART) || { \ - docker tag $(REPOSITORY):$(VERSION_TAG) ghcr.io/$(REPOSITORY):latest-$(ARCH); \ - echo "- pushing ghcr.io/$(REPOSITORY):latest-$(ARCH)"; \ - docker push ghcr.io/$(REPOSITORY):latest-$(ARCH); \ - } - - -.PHONY: gh-login -gh-login: ## Login to GHCR - @echo "🚚 Logging in to GHCR..." - @if [ -z "$(GHCR_PAT)" ]; then \ - echo "Error: GHCR_PAT environment variable is not set"; \ - exit 1; \ - fi - @echo $(GHCR_PAT) | docker login ghcr.io -u arcadeai --password-stdin - -.PHONY: help -help: - @echo "šŸ› ļø Worker Docker Commands:\n" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' - -.DEFAULT_GOAL := help diff --git a/docker/README.md b/docker/README.md deleted file mode 100644 index 65f99b77..00000000 --- a/docker/README.md +++ /dev/null @@ -1,123 +0,0 @@ -# Arcade Docker Compose Guide - -This guide provides detailed instructions on how to set up and run Arcade using Docker Compose. - -## Prerequisites - -- **Docker** installed on your system. [Install Docker](https://docs.docker.com/get-docker/) -- **Docker Compose** installed. It comes bundled with Docker Desktop on Windows and macOS. For Linux, follow the [Docker Compose installation guide](https://docs.docker.com/compose/install/). - -## Getting Started - -### 1. Clone the Repository - -Begin by cloning the Arcade repository: - -```bash -git clone https://github.com/ArcadeAI/arcade-mcp.git -``` - -### 2. Build package wheels - -From the root of the arcade-mcp repository: - -```bash -make full-dist -``` - -### 3. Copy and Configure Environment Variables - -Change to the `docker` directory: - -```bash -cd arcade-mcp/docker -``` - -Copy the example environment file to `.env`: - -```bash -cp env.example .env -``` - -Open the `.env` file in your preferred text editor and fill in the required values. At a minimum, you **must** provide the `OPENAI_API_KEY`: - -```env:.env -### LLM ### - -OPENAI_API_KEY=your_openai_api_key_here -``` - -If you plan to use other Large Language Model (LLM) providers, add their API keys as well: - -```env:.env -ANTHROPIC_API_KEY=your_anthropic_api_key_here -``` - -### 4. Run Docker Compose - -Start the Arcade services using Docker Compose: - -```bash -docker compose up -``` - -This command will build and start all the services defined in the `docker-compose.yml` file and make their ports available to your host machine. - -### 5. Verify the Engine is Running - -In a separate terminal window, check if the engine is running: - -```bash -curl http://localhost:9099/v1/health -``` - -You should receive a response indicating that the engine is healthy: - -```json -{ "healthy": "true" } -``` - -Open a browser and navigate to http://localhost:9099/dashboard to view the Arcade dashboard. - -## Adding Authentication Providers - -Arcade supports various authentication providers. To add an auth provider, follow these steps: - -### 1. Enable the Auth Provider in the Configuration - -Edit the `docker.engine.yaml` file to enable the desired auth provider. For example, to enable Google authentication, modify the file as follows: - -```yaml:docker.engine.yaml -auth: - providers: - - id: google - enabled: true # Change from false to true -``` - -### 2. Add Client ID and Secret to the `.env` File - -Obtain the client ID and client secret from your auth provider and add them to the `.env` file: - -```env:.env -GOOGLE_CLIENT_ID="your_google_client_id" -GOOGLE_CLIENT_SECRET="your_google_client_secret" -``` - -Repeat this step for any other auth providers you wish to enable. - -### 3. Restart the Docker Compose Services - -After making changes to the configuration, restart the services: - -```bash -docker compose down -docker compose up -``` - -## Troubleshooting - -- **Engine Health Check Fails**: Ensure that all environment variables are correctly set in the `.env` file and that the services have started without errors. -- **Port Conflicts**: If the default ports are already in use, modify the ports in the `docker-compose.yml` file. -- **Authentication Errors**: Double-check the client IDs and secrets provided for auth providers. - -NOTE: `arcade login` will not work within a docker container, you must copy your credentials into the container if you would like to use it. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index 522d3e83..00000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,61 +0,0 @@ -version: '3.8' - -services: - engine: - image: ghcr.io/arcadeai/engine:latest - container_name: arcade-engine - volumes: - - ./docker.engine.yaml:/bin/engine.yaml - - ./.env:/bin/.env - - ./db/:/app/ - ports: - - "9099:9099" - depends_on: - redis: - condition: service_healthy - worker: - condition: service_started - networks: - arcade-network: - command: /bin/arcade-engine --config /bin/engine.yaml --env /bin/.env --migrate - - worker: - image: ghcr.io/arcadeai/worker:latest - container_name: arcade-worker - ports: - - "8002:8002" - networks: - arcade-network: - - redis: - image: redis/redis-stack:latest - container_name: arcade-redis - ports: - - "6379:6379" - - "8004:8002" - depends_on: - worker: - condition: service_started - healthcheck: - test: [ "CMD", "redis-cli", "ping" ] - interval: 3s - timeout: 3s - retries: 5 - networks: - arcade-network: - - nginx: - image: nginx:stable-alpine - container_name: arcade-nginx - ports: - - "80:80" - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf:ro - depends_on: - - engine - networks: - - arcade-network - -networks: - arcade-network: - driver: bridge diff --git a/docker/docker.engine.yaml b/docker/docker.engine.yaml deleted file mode 100644 index a9633fa7..00000000 --- a/docker/docker.engine.yaml +++ /dev/null @@ -1,185 +0,0 @@ -telemetry: - environment: local - version: ${env:VERSION} - logging: - level: debug # debug, info, warn, error - encoding: console - -api: - development: true - host: 0.0.0.0 - port: 9099 - analytics: - enabled: false - # Uncomment to enable rate limiter - # rate_limit: - # redis: - # addr: "localhost:6379" - # password: "" - # db: 2 - # time_unit: m - # limit: 2 - # write_timeout: 10 - # read_timeout: 10 - -llm: - models: - - id: oai - openai: - api_key: ${env:OPENAI_API_KEY} - # - id: anthropic - # anthropic: - # api_key: ${env:ANTHROPIC_API_KEY} - # # model: "claude-3-5-sonnet-20240620" - #- id: ollama - # openai: - # base_url: "http://localhost:11434/v1" - # api_key: "ollama" - -tools: - directors: - - id: default - enabled: true - max_tools: 64 - workers: - - id: "localworker" - enabled: true - http: - uri: ${env:WORKER_URL} - timeout: 30 - retry: 3 - secret: ${env:ARCADE_WORKER_SECRET} # If not set, defaults to "dev" in development mode only - # Uncomment mock and comment http to start engine without live worker - # mock: - # enabled: true - -security: - root_keys: - - id: key1 - default: true - value: ${env:ROOT_KEY_1} - # - id: key2 - # value: ${env:ROOT_KEY_2} - -storage: - # postgres: - # user: postgres - # password: 123456 - # host: localhost - # port: 5432 - # db: arcade_engine - # sslmode: disable - sqlite: # Default for local development - connection_string: "/app/arcade-engine.sqlite3" - -cache: - api_key_ttl: "10s" - redis: - addr: ${env:REDIS_HOST}:${env:REDIS_PORT} - password: ${env:REDIS_PASSWORD} - -auth: - token_store: - redis: - addr: ${env:REDIS_HOST}:${env:REDIS_PORT} - password: ${env:REDIS_PASSWORD} - - providers: - - id: default-atlassian - description: "The default Atlassian provider" - enabled: false - type: oauth2 - provider_id: atlassian - client_id: ${env:ATLASSIAN_CLIENT_ID} - client_secret: ${env:ATLASSIAN_CLIENT_SECRET} - - - id: default-discord - description: "The default Discord provider" - enabled: false - type: oauth2 - provider_id: discord - client_id: ${env:DISCORD_CLIENT_ID} - client_secret: ${env:DISCORD_CLIENT_SECRET} - - - id: default-dropbox - description: "The default Dropbox provider" - enabled: false - type: oauth2 - provider_id: dropbox - client_id: ${env:DROPBOX_CLIENT_ID} - client_secret: ${env:DROPBOX_CLIENT_SECRET} - - - id: default-github - description: "The default GitHub provider" - enabled: false - type: oauth2 - provider_id: github - client_id: ${env:GITHUB_CLIENT_ID} - client_secret: ${env:GITHUB_CLIENT_SECRET} - - - id: default-google - description: "The default Google provider" - enabled: false - type: oauth2 - provider_id: google - client_id: ${env:GOOGLE_CLIENT_ID} - client_secret: ${env:GOOGLE_CLIENT_SECRET} - - - id: default-linkedin - description: "The default LinkedIn provider" - enabled: false - type: oauth2 - provider_id: linkedin - client_id: ${env:LINKEDIN_CLIENT_ID} - client_secret: ${env:LINKEDIN_CLIENT_SECRET} - - - id: arcade-linear - description: "The Linear provider for Arcade" - enabled: false - type: oauth2 - provider_id: linear - client_id: ${env:LINEAR_CLIENT_ID} - client_secret: ${env:LINEAR_CLIENT_SECRET} - - - id: default-microsoft - description: "The default Microsoft provider" - enabled: false - type: oauth2 - provider_id: microsoft - client_id: ${env:MICROSOFT_CLIENT_ID} - client_secret: ${env:MICROSOFT_CLIENT_SECRET} - - - id: default-slack - description: "The default Slack provider" - enabled: false - type: oauth2 - provider_id: slack - client_id: ${env:SLACK_CLIENT_ID} - client_secret: ${env:SLACK_CLIENT_SECRET} - - - id: default-spotify - description: "The default Spotify provider" - enabled: false - type: oauth2 - provider_id: spotify - client_id: ${env:SPOTIFY_CLIENT_ID} - client_secret: ${env:SPOTIFY_CLIENT_SECRET} - - - id: default-x - description: "The default X provider" - enabled: false - type: oauth2 - provider_id: x - client_id: ${env:X_CLIENT_ID} - client_secret: ${env:X_CLIENT_SECRET} - - - id: default-zoom - description: "The default Zoom provider" - enabled: false - type: oauth2 - provider_id: zoom - client_id: ${env:ZOOM_CLIENT_ID} - client_secret: ${env:ZOOM_CLIENT_SECRET} - -dashboard: - redirect_uri: "http://localhost:9099/dashboard" diff --git a/docker/env.example b/docker/env.example deleted file mode 100644 index 4963688e..00000000 --- a/docker/env.example +++ /dev/null @@ -1,51 +0,0 @@ - -### LLM ### - -OPENAI_API_KEY= -ANTHROPIC_API_KEY= - -### Auth providers ### - -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= - -GOOGLE_CLIENT_ID="" -GOOGLE_CLIENT_SECRET= - -LINKEDIN_CLIENT_ID= -LINKEDIN_CLIENT_SECRET="" - -LINEAR_CLIENT_ID= -LINEAR_CLIENT_SECRET= - -MICROSOFT_CLIENT_ID= -MICROSOFT_CLIENT_SECRET="" - -SLACK_CLIENT_ID="" -SLACK_CLIENT_SECRET= - -SPOTIFY_CLIENT_ID= -SPOTIFY_CLIENT_SECRET= - -X_CLIENT_ID= -X_CLIENT_SECRET= - -ZOOM_CLIENT_ID= -ZOOM_CLIENT_SECRET= - -### WORKER ### - -WORKER_URL="http://arcade-worker:8002" - -### Redis ### - -REDIS_HOST=arcade-redis -REDIS_PORT=6379 -REDIS_PASSWORD="" -RATE_LIMIT_REQS_PER_MIN=100 - -### ARCADE ### - -ARCADE_HOME=/app -ARCADE_WORKER_SECRET=dev -ROOT_KEY_1=supersecret diff --git a/docker/nginx.conf b/docker/nginx.conf deleted file mode 100644 index f6bb98ff..00000000 --- a/docker/nginx.conf +++ /dev/null @@ -1,47 +0,0 @@ -user nginx; -worker_processes auto; -error_log /var/log/nginx/error.log; -pid /run/nginx.pid; - -include /usr/share/nginx/modules/*.conf; - -events { - worker_connections 1024; -} - -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - sendfile on; - client_max_body_size 5M; - client_body_buffer_size 5M; - - gzip on; - gzip_comp_level 2; - gzip_types text/plain text/css text/javascript application/javascript application/xml image/jpeg image/gif image/png; - gzip_vary on; - - keepalive_timeout 300; - - server { - listen 80 default_server; - listen [::]:80 default_server; - server_name arcade-engine:9099; - - root /app; - client_max_body_size 10m; - - location / { - proxy_pass http://arcade-engine:9099; - - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - proxy_connect_timeout 300s; - proxy_send_timeout 300s; - proxy_read_timeout 300s; - } - } -} diff --git a/docker/start.sh b/docker/start.sh deleted file mode 100755 index 7a11f33b..00000000 --- a/docker/start.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -echo "Starting arcade..." -arcade serve --host $HOST --port $PORT $([ "$OTEL_ENABLE" = "true" ] && echo "--otel-enable") diff --git a/docker/toolkits.txt b/docker/toolkits.txt deleted file mode 100644 index 8c2efed2..00000000 --- a/docker/toolkits.txt +++ /dev/null @@ -1,4 +0,0 @@ -arcade-brightdata -arcade-linkedin -arcade-math -arcade-zendesk diff --git a/libs/arcade-mcp-server/arcade_mcp_server/__main__.py b/libs/arcade-mcp-server/arcade_mcp_server/__main__.py index 15b1ebae..389c186b 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/__main__.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/__main__.py @@ -277,6 +277,12 @@ Auto-discovery looks for Python files with @tool decorated functions in: "--cwd", help="Directory to change to before running (for tool discovery)", ) + parser.add_argument( + "--workers", + default=1, + type=int, + help=argparse.SUPPRESS, + ) args = parser.parse_args() @@ -300,6 +306,10 @@ Auto-discovery looks for Python files with @tool decorated functions in: log_level = "DEBUG" if args.debug else "INFO" setup_logging(level=log_level, stdio_mode=(args.transport == "stdio")) + if args.workers > 1 and args.transport == "stdio": + logger.error("Cannot use --workers > 1 with stdio transport") + sys.exit(1) + # Build kwargs for server server_kwargs = {} if args.name: @@ -307,18 +317,17 @@ Auto-discovery looks for Python files with @tool decorated functions in: if args.version: server_kwargs["version"] = args.version - # Discover tools - catalog = initialize_tool_catalog( - tool_package=args.tool_package, - show_packages=args.show_packages, - discover_installed=args.discover_installed, - server_name=server_kwargs.get("name"), - server_version=server_kwargs.get("version"), - ) - # Run appropriate server try: if args.transport == "stdio": + # Discover tools only for stdio mode (HTTP mode handles its own discovery) + catalog = initialize_tool_catalog( + tool_package=args.tool_package, + show_packages=args.show_packages, + discover_installed=args.discover_installed, + server_name=server_kwargs.get("name"), + server_version=server_kwargs.get("version"), + ) logger.info("Starting MCP server with stdio transport") asyncio.run( run_stdio_server(catalog, debug=args.debug, env_file=args.env_file, **server_kwargs) @@ -328,7 +337,6 @@ Auto-discovery looks for Python files with @tool decorated functions in: from arcade_mcp_server.worker import run_arcade_mcp run_arcade_mcp( - catalog=catalog, host=args.host, port=args.port, reload=args.reload, @@ -337,6 +345,7 @@ Auto-discovery looks for Python files with @tool decorated functions in: tool_package=args.tool_package, discover_installed=args.discover_installed, show_packages=args.show_packages, + workers=args.workers, **server_kwargs, ) except (KeyboardInterrupt, asyncio.CancelledError): diff --git a/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py b/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py index d1e405b7..3c4fb555 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/mcp_app.py @@ -414,14 +414,7 @@ class MCPApp: tool_count=len(self._catalog), ) - asyncio.run( - serve_with_force_quit( - app=app, - host=host, - port=port, - log_level=log_level, - ) - ) + asyncio.run(serve_with_force_quit(app=app, host=host, port=port, log_level=log_level)) @staticmethod def _get_configuration_overrides( diff --git a/libs/arcade-mcp-server/arcade_mcp_server/worker.py b/libs/arcade-mcp-server/arcade_mcp_server/worker.py index 633b2a46..b5f949ad 100644 --- a/libs/arcade-mcp-server/arcade_mcp_server/worker.py +++ b/libs/arcade-mcp-server/arcade_mcp_server/worker.py @@ -347,10 +347,10 @@ async def serve_with_force_quit( def run_arcade_mcp( - catalog: ToolCatalog, host: str = "127.0.0.1", port: int = 8000, reload: bool = False, + workers: int = 1, debug: bool = False, otel_enable: bool = False, tool_package: str | None = None, @@ -364,33 +364,55 @@ def run_arcade_mcp( This is used for module execution (`arcade mcp` and `python -m arcade_mcp_server`) only. MCPApp has its own reload mechanism. + + Args: + workers: Number of uvicorn worker processes. When workers > 1, force-quit + capability is disabled (standard uvicorn signal handling is used). + Cannot be combined with reload=True. + + Raises: + ValueError: If both reload=True and workers > 1 are specified, as uvicorn + does not support multiple workers in reload mode. """ + if reload and workers > 1: + raise ValueError( + "Cannot use reload=True with workers > 1. " + "Uvicorn does not support multiple workers in reload mode." + ) + log_level = "debug" if debug else "info" - if reload: - # TODO: This reload path uses uvicorn.run(), which bypasses serve_with_force_quit(). - # This means that the server will not be able to force quit when there are active - # tool executions or active connections with MCP clients. For this reason, prefer - # to use MCPApp.run() for reload mode. + # Set env vars for the app factory to read + os.environ["ARCADE_MCP_DEBUG"] = str(debug) + os.environ["ARCADE_MCP_OTEL_ENABLE"] = str(otel_enable) + if tool_package: + os.environ["ARCADE_MCP_TOOL_PACKAGE"] = tool_package + os.environ["ARCADE_MCP_DISCOVER_INSTALLED"] = str(discover_installed) + os.environ["ARCADE_MCP_SHOW_PACKAGES"] = str(show_packages) - # Set env vars for the app factory to read later - os.environ["ARCADE_MCP_DEBUG"] = str(debug) - os.environ["ARCADE_MCP_OTEL_ENABLE"] = str(otel_enable) - if tool_package: - os.environ["ARCADE_MCP_TOOL_PACKAGE"] = tool_package - os.environ["ARCADE_MCP_DISCOVER_INSTALLED"] = str(discover_installed) - os.environ["ARCADE_MCP_SHOW_PACKAGES"] = str(show_packages) - if mcp_settings: - os.environ["ARCADE_MCP_SERVER_NAME"] = mcp_settings.server.name - os.environ["ARCADE_MCP_SERVER_VERSION"] = mcp_settings.server.version - if mcp_settings.server.title: - os.environ["ARCADE_MCP_SERVER_TITLE"] = mcp_settings.server.title - if mcp_settings.server.instructions: - os.environ["ARCADE_MCP_SERVER_INSTRUCTIONS"] = mcp_settings.server.instructions + # Handle server name/version from mcp_settings or kwargs + server_name = kwargs.get("name") + server_version = kwargs.get("version") + if mcp_settings: + os.environ["ARCADE_MCP_SERVER_NAME"] = mcp_settings.server.name + os.environ["ARCADE_MCP_SERVER_VERSION"] = mcp_settings.server.version + if mcp_settings.server.title: + os.environ["ARCADE_MCP_SERVER_TITLE"] = mcp_settings.server.title + if mcp_settings.server.instructions: + os.environ["ARCADE_MCP_SERVER_INSTRUCTIONS"] = mcp_settings.server.instructions + else: + if server_name: + os.environ["ARCADE_MCP_SERVER_NAME"] = server_name + if server_version: + os.environ["ARCADE_MCP_SERVER_VERSION"] = server_version - # import string is required for reload mode - app_import_string = "arcade_mcp_server.worker:create_arcade_mcp_factory" + app_import_string = "arcade_mcp_server.worker:create_arcade_mcp_factory" + if reload or workers > 1: + # Reload mode and multi-worker mode (mutually exclusive, validated above) + # use uvicorn.run() which bypasses serve_with_force_quit(). This means the + # server will not be able to force quit when there are active tool executions + # or active connections with MCP clients. For reload mode, prefer MCPApp.run(). uvicorn.run( app_import_string, factory=True, @@ -399,16 +421,12 @@ def run_arcade_mcp( log_level=log_level, reload=reload, lifespan="on", + workers=workers, ) else: - app = create_arcade_mcp( - catalog=catalog, - mcp_settings=mcp_settings, - debug=debug, - otel_enable=otel_enable, - **kwargs, - ) - + # Single-worker production mode uses serve_with_force_quit() for graceful + # shutdown with force-quit capability on second SIGINT/SIGTERM + app = create_arcade_mcp_factory() asyncio.run( serve_with_force_quit( app=app,