Ability to run multiple uvicorn workers (#721)
<!-- CURSOR_SUMMARY --> > [!NOTE] > Adds --workers to HTTP mode with validation, refactors server startup/discovery for multi-process uvicorn, and removes all Docker-related files/configs. > > - **MCP Server (HTTP mode)** > - Add `--workers` arg to run multiple uvicorn workers; block `workers > 1` with `stdio`, and `reload` with multiple workers. > - Refactor startup: move tool discovery/config into `create_arcade_mcp_factory()` driven by env vars; use `uvicorn.run(..., workers=...)` for multi-worker/reload; retain `serve_with_force_quit()` only for single-worker. > - Adjust CLI to only discover tools in `stdio` path; HTTP path now delegates discovery to the factory. > - **MCPApp** > - Minor run path cleanup; continue using `serve_with_force_quit()` for single-worker HTTP. > - **Ops/Packaging** > - Remove `docker/` directory and all Dockerfiles, compose/configs, and docs. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c5700ac8855173c1e82c6f7e41b30ca173aaec14. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
This commit is contained in:
parent
0fc9e21308
commit
99c22f0ebb
12 changed files with 68 additions and 700 deletions
|
|
@ -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"]
|
||||
103
docker/Makefile
103
docker/Makefile
|
|
@ -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
|
||||
123
docker/README.md
123
docker/README.md
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "Starting arcade..."
|
||||
arcade serve --host $HOST --port $PORT $([ "$OTEL_ENABLE" = "true" ] && echo "--otel-enable")
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
arcade-brightdata
|
||||
arcade-linkedin
|
||||
arcade-math
|
||||
arcade-zendesk
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue