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",
|
"--cwd",
|
||||||
help="Directory to change to before running (for tool discovery)",
|
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()
|
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"
|
log_level = "DEBUG" if args.debug else "INFO"
|
||||||
setup_logging(level=log_level, stdio_mode=(args.transport == "stdio"))
|
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
|
# Build kwargs for server
|
||||||
server_kwargs = {}
|
server_kwargs = {}
|
||||||
if args.name:
|
if args.name:
|
||||||
|
|
@ -307,18 +317,17 @@ Auto-discovery looks for Python files with @tool decorated functions in:
|
||||||
if args.version:
|
if args.version:
|
||||||
server_kwargs["version"] = 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
|
# Run appropriate server
|
||||||
try:
|
try:
|
||||||
if args.transport == "stdio":
|
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")
|
logger.info("Starting MCP server with stdio transport")
|
||||||
asyncio.run(
|
asyncio.run(
|
||||||
run_stdio_server(catalog, debug=args.debug, env_file=args.env_file, **server_kwargs)
|
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
|
from arcade_mcp_server.worker import run_arcade_mcp
|
||||||
|
|
||||||
run_arcade_mcp(
|
run_arcade_mcp(
|
||||||
catalog=catalog,
|
|
||||||
host=args.host,
|
host=args.host,
|
||||||
port=args.port,
|
port=args.port,
|
||||||
reload=args.reload,
|
reload=args.reload,
|
||||||
|
|
@ -337,6 +345,7 @@ Auto-discovery looks for Python files with @tool decorated functions in:
|
||||||
tool_package=args.tool_package,
|
tool_package=args.tool_package,
|
||||||
discover_installed=args.discover_installed,
|
discover_installed=args.discover_installed,
|
||||||
show_packages=args.show_packages,
|
show_packages=args.show_packages,
|
||||||
|
workers=args.workers,
|
||||||
**server_kwargs,
|
**server_kwargs,
|
||||||
)
|
)
|
||||||
except (KeyboardInterrupt, asyncio.CancelledError):
|
except (KeyboardInterrupt, asyncio.CancelledError):
|
||||||
|
|
|
||||||
|
|
@ -414,14 +414,7 @@ class MCPApp:
|
||||||
tool_count=len(self._catalog),
|
tool_count=len(self._catalog),
|
||||||
)
|
)
|
||||||
|
|
||||||
asyncio.run(
|
asyncio.run(serve_with_force_quit(app=app, host=host, port=port, log_level=log_level))
|
||||||
serve_with_force_quit(
|
|
||||||
app=app,
|
|
||||||
host=host,
|
|
||||||
port=port,
|
|
||||||
log_level=log_level,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_configuration_overrides(
|
def _get_configuration_overrides(
|
||||||
|
|
|
||||||
|
|
@ -347,10 +347,10 @@ async def serve_with_force_quit(
|
||||||
|
|
||||||
|
|
||||||
def run_arcade_mcp(
|
def run_arcade_mcp(
|
||||||
catalog: ToolCatalog,
|
|
||||||
host: str = "127.0.0.1",
|
host: str = "127.0.0.1",
|
||||||
port: int = 8000,
|
port: int = 8000,
|
||||||
reload: bool = False,
|
reload: bool = False,
|
||||||
|
workers: int = 1,
|
||||||
debug: bool = False,
|
debug: bool = False,
|
||||||
otel_enable: bool = False,
|
otel_enable: bool = False,
|
||||||
tool_package: str | None = None,
|
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.
|
This is used for module execution (`arcade mcp` and `python -m arcade_mcp_server`) only.
|
||||||
MCPApp has its own reload mechanism.
|
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"
|
log_level = "debug" if debug else "info"
|
||||||
|
|
||||||
if reload:
|
# Set env vars for the app factory to read
|
||||||
# TODO: This reload path uses uvicorn.run(), which bypasses serve_with_force_quit().
|
os.environ["ARCADE_MCP_DEBUG"] = str(debug)
|
||||||
# This means that the server will not be able to force quit when there are active
|
os.environ["ARCADE_MCP_OTEL_ENABLE"] = str(otel_enable)
|
||||||
# tool executions or active connections with MCP clients. For this reason, prefer
|
if tool_package:
|
||||||
# to use MCPApp.run() for reload mode.
|
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
|
# Handle server name/version from mcp_settings or kwargs
|
||||||
os.environ["ARCADE_MCP_DEBUG"] = str(debug)
|
server_name = kwargs.get("name")
|
||||||
os.environ["ARCADE_MCP_OTEL_ENABLE"] = str(otel_enable)
|
server_version = kwargs.get("version")
|
||||||
if tool_package:
|
if mcp_settings:
|
||||||
os.environ["ARCADE_MCP_TOOL_PACKAGE"] = tool_package
|
os.environ["ARCADE_MCP_SERVER_NAME"] = mcp_settings.server.name
|
||||||
os.environ["ARCADE_MCP_DISCOVER_INSTALLED"] = str(discover_installed)
|
os.environ["ARCADE_MCP_SERVER_VERSION"] = mcp_settings.server.version
|
||||||
os.environ["ARCADE_MCP_SHOW_PACKAGES"] = str(show_packages)
|
if mcp_settings.server.title:
|
||||||
if mcp_settings:
|
os.environ["ARCADE_MCP_SERVER_TITLE"] = mcp_settings.server.title
|
||||||
os.environ["ARCADE_MCP_SERVER_NAME"] = mcp_settings.server.name
|
if mcp_settings.server.instructions:
|
||||||
os.environ["ARCADE_MCP_SERVER_VERSION"] = mcp_settings.server.version
|
os.environ["ARCADE_MCP_SERVER_INSTRUCTIONS"] = mcp_settings.server.instructions
|
||||||
if mcp_settings.server.title:
|
else:
|
||||||
os.environ["ARCADE_MCP_SERVER_TITLE"] = mcp_settings.server.title
|
if server_name:
|
||||||
if mcp_settings.server.instructions:
|
os.environ["ARCADE_MCP_SERVER_NAME"] = server_name
|
||||||
os.environ["ARCADE_MCP_SERVER_INSTRUCTIONS"] = mcp_settings.server.instructions
|
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(
|
uvicorn.run(
|
||||||
app_import_string,
|
app_import_string,
|
||||||
factory=True,
|
factory=True,
|
||||||
|
|
@ -399,16 +421,12 @@ def run_arcade_mcp(
|
||||||
log_level=log_level,
|
log_level=log_level,
|
||||||
reload=reload,
|
reload=reload,
|
||||||
lifespan="on",
|
lifespan="on",
|
||||||
|
workers=workers,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
app = create_arcade_mcp(
|
# Single-worker production mode uses serve_with_force_quit() for graceful
|
||||||
catalog=catalog,
|
# shutdown with force-quit capability on second SIGINT/SIGTERM
|
||||||
mcp_settings=mcp_settings,
|
app = create_arcade_mcp_factory()
|
||||||
debug=debug,
|
|
||||||
otel_enable=otel_enable,
|
|
||||||
**kwargs,
|
|
||||||
)
|
|
||||||
|
|
||||||
asyncio.run(
|
asyncio.run(
|
||||||
serve_with_force_quit(
|
serve_with_force_quit(
|
||||||
app=app,
|
app=app,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue