Skip to content
Draft
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
33 changes: 33 additions & 0 deletions examples/mcp_servers/authorization/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Virtual environment
.venv/
venv/
env/

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Distribution
dist/
build/
*.egg-info/

# Docker
docker/
.dockerignore
Dockerfile
docker-compose.yml
45 changes: 45 additions & 0 deletions examples/mcp_servers/authorization/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim

# Create non-root user
RUN useradd -m -u 1000 appuser

WORKDIR /app

# Copy project files
COPY pyproject.toml uv.lock ./
COPY src/ ./src/

# Auto-detect package name from pyproject.toml
# First try using Python's tomllib
# Fallback to grep/sed for compatibility
RUN PACKAGE_NAME=$(python3 -c "import tomllib; f=open('pyproject.toml','rb'); data=tomllib.load(f); print(data['project']['name'])" 2>/dev/null || \
grep -E '^name\s*=' pyproject.toml | head -1 | sed -E "s/.*name\s*=\s*[\"']([^\"']+)[\"'].*/\1/" || \
grep -E '^name\s*=' pyproject.toml | head -1 | sed -E 's/.*name\s*=\s*([^ ]+).*/\1/') && \
if [ -z "$PACKAGE_NAME" ]; then \
echo "ERROR: Could not detect package name from pyproject.toml" && exit 1; \
fi && \
echo "Detected package: $PACKAGE_NAME" && \
echo "$PACKAGE_NAME" > /tmp/package_name.txt

# Install dependencies
RUN uv sync --frozen --no-dev

# Change ownership to non-root user
RUN chown -R appuser:appuser /app

USER appuser

# Expose the port
EXPOSE 8001

# Run the server from src/<package>/server.py
CMD PACKAGE_NAME=$(cat /tmp/package_name.txt) && \
if [ -f "src/${PACKAGE_NAME}/server.py" ]; then \
uv run src/${PACKAGE_NAME}/server.py; \
else \
echo "ERROR: Could not find server.py at src/${PACKAGE_NAME}/server.py" && \
echo " Package detected: ${PACKAGE_NAME}" && \
echo " Available directories in src/:" && \
ls -la src/ 2>/dev/null || echo " src/ directory not found" && \
exit 1; \
fi
93 changes: 93 additions & 0 deletions examples/mcp_servers/authorization/docker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Docker Setup for MCP Servers

This directory contains a generalized Docker configuration template that can be used with any MCP server in this repository.

## Quick Start

1. **Copy the Docker files to your MCP server directory:**

```bash
cp -r examples/docker-template/docker your-mcp-server/
cp examples/docker-template/.dockerignore your-mcp-server/
```

2. **Build and run:**

```bash
cd your-mcp-server
docker-compose -f docker/docker-compose.yml up --build
```

## Configuration

### Package Detection

The Dockerfile uses the package name from `pyproject.toml` by reading the `[project] name` field. It expects your server file at `src/<package_name>/server.py` (where `<package_name>` is from `pyproject.toml`).

If the server file is not found at this location, then the build will fail with an error message showing the detected package name and available directories in `src/`.

### Environment Variables

- `ARCADE_SERVER_TRANSPORT`: The transport protocol to use
- Default: `http`
- Options: `http`, `stdio`
- `ARCADE_SERVER_PORT`: The port to run the server on (internal)
- Default: `8001`
- `ARCADE_SERVER_HOST`: The host to bind to
- Default: `0.0.0.0`

### Example: Simple MCP Server

```bash
# From examples/mcp_servers/simple/
docker-compose -f docker/docker-compose.yml up --build
```

The server will run internally on port 8001 but be accessible externally on port 8080 (http://localhost:8080). This demonstrates front-door auth working when the canonical URL differs from the internal bind address.

You can customize the ports by editing `docker/docker-compose.yml` and changing:
- The port mapping (e.g., "8080:8001")
- The `ARCADE_SERVER_PORT` environment variable (internal port)
- The `MCP_RESOURCE_SERVER_CANONICAL_URL` (external URL)
## Building the Image

```bash
docker build \
-f docker/Dockerfile \
-t your-mcp-server \
.
```

## Running with Docker

```bash
docker run -p 8080:8001 \
-e ARCADE_SERVER_TRANSPORT=http \
-e ARCADE_SERVER_HOST=0.0.0.0 \
-e ARCADE_SERVER_PORT=8001 \
your-mcp-server
```

## Features

- **Automatic package detection**: Reads package name from `pyproject.toml`
- **Standard server location**: Expects server file at `src/<package>/server.py`
- **Secure by default**: Runs as non-root user
- **Arcade environment variable support**: Uses `ARCADE_SERVER_*` environment variables
- **Environment-based config**: Easy customization via environment variables
- **uv integration**: Uses uv for fast dependency management
- **Lightweight**: Based on Python 3.11 Bookworm slim image with uv

## Connecting from Cursor

Add to your `~/.cursor/mcp.json`:

```json
"your-server-name": {
"name": "your-server-name",
"type": "stream",
"url": "http://localhost:8080/mcp"
}
```

Then restart Cursor to connect to the server.
12 changes: 12 additions & 0 deletions examples/mcp_servers/authorization/docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
mcp-server:
build:
context: ..
dockerfile: docker/Dockerfile
ports:
- "8080:8001" # External port 8080 maps to internal port 8001
environment:
- ARCADE_SERVER_TRANSPORT=http
- ARCADE_SERVER_HOST=0.0.0.0
- ARCADE_SERVER_PORT=8001
- MCP_RESOURCE_SERVER_CANONICAL_URL=http://127.0.0.1:8080/mcp
45 changes: 45 additions & 0 deletions examples/mcp_servers/authorization/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
[project]
name = "authorization"
version = "0.1.0"
description = "MCP Server created with Arcade.dev"
requires-python = ">=3.10"
dependencies = [
"arcade-mcp-server>=1.8.0,<2.0.0",
"httpx>=0.28.0,<1.0.0",
]

[project.optional-dependencies]
dev = [
"arcade-mcp[all]>=1.5.2,<2.0.0",
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"mypy>=1.0.0",
"ruff>=0.1.0",
]

# Tell Arcade.dev that this package has Arcade tools
[project.entry-points.arcade_toolkits]
toolkit_name = "authorization"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/authorization"]

[tool.ruff]
line-length = 100
target-version = "py312"

[tool.mypy]
python_version = "3.12"
warn_unused_configs = true
disallow_untyped_defs = false

# # Uncomment the following if you are developing inside of the arcade-mcp repo & want to use editable mode
# # Otherwise, you will install the following packages from PyPI
# [tool.uv.sources]
# arcade-mcp = { path = "../../../", editable = true }
# arcade-serve = { path = "../../../libs/arcade-serve/", editable = true }
# arcade-mcp-server = { path = "../../../libs/arcade-mcp-server/", editable = true }
16 changes: 16 additions & 0 deletions examples/mcp_servers/authorization/src/authorization/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Server Auth environment variables
MCP_RESOURCE_SERVER_CANONICAL_URL="http://127.0.0.1:8000/mcp"
MCP_RESOURCE_SERVER_AUTHORIZATION_SERVERS='[
{
"authorization_server_url": "https://your-workos.authkit.app",
"issuer": "https://your-workos.authkit.app",
"jwks_uri": "https://your-workos.authkit.app/oauth2/jwks",
"algorithm": "RS256",
"verify_options": {
"verify_aud": false
}
}
]'

# Tool Secrets
MY_SECRET_KEY="Your tools can have secrets injected at runtime!"
Empty file.
109 changes: 109 additions & 0 deletions examples/mcp_servers/authorization/src/authorization/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""authorization MCP server"""

from typing import Annotated

import httpx
from arcade_mcp_server import Context, MCPApp
from arcade_mcp_server.auth import Reddit
from arcade_mcp_server.resource_server import (
AccessTokenValidationOptions,
AuthorizationServerEntry,
ResourceServer,
)

# Option 1: Single authorization server example
resource_server = ResourceServer(
canonical_url="http://127.0.0.1:8000/mcp",
authorization_servers=[
AuthorizationServerEntry(
authorization_server_url="https://your-workos.authkit.app",
issuer="https://your-workos.authkit.app",
jwks_uri="https://your-workos.authkit.app/oauth2/jwks",
algorithm="RS256",
validation_options=AccessTokenValidationOptions(
verify_aud=False,
),
)
],
)

# Option 2: Multiple authorization servers with different keys (e.g., multi-IdP)
# resource_server = ResourceServer(
# canonical_url="http://127.0.0.1:8000/mcp",
# authorization_servers=[
# AuthorizationServerEntry(
# authorization_server_url="https://your-workos.authkit.app",
# issuer="https://your-workos.authkit.app",
# jwks_uri="https://your-workos.authkit.app/oauth2/jwks",
# ),
# AuthorizationServerEntry(
# authorization_server_url="https://github.com/login/oauth",
# issuer="https://github.com",
# jwks_uri="https://token.actions.githubusercontent.com/.well-known/jwks",
# ),
# ],
# )

# Option 3: Authoriation via env vars (place in your .env file)
# ```bash
# MCP_RESOURCE_SERVER_CANONICAL_URL=http://127.0.0.1:8000/mcp
# MCP_RESOURCE_SERVER_AUTHORIZATION_SERVERS='[
# {
# "authorization_server_url": "https://your-workos.authkit.app",
# "issuer": "https://your-workos.authkit.app",
# "jwks_uri": "https://your-workos.authkit.app/oauth2/jwks",
# "algorithm": "RS256",
# "validation_options": {
# "verify_aud": false
# }
# }
# ]'
# ```
# resource_server = ResourceServer()

app = MCPApp(name="authorization", version="1.0.0", log_level="DEBUG", auth=resource_server)


@app.tool
def greet(name: Annotated[str, "The name of the person to greet"]) -> str:
"""Greet a person by name."""
return f"Hello, {name}!"


@app.tool(requires_secrets=["MY_SECRET_KEY"])
def whisper_secret(context: Context) -> Annotated[str, "The last 4 characters of the secret"]:
"""Reveal the last 4 characters of a secret"""
try:
secret = context.get_secret("MY_SECRET_KEY")
except Exception as e:
return str(e)

return "The last 4 characters of the secret are: " + secret[-4:]


# To use this tool locally, you need to install the Arcade CLI (uv tool install arcade-mcp)
# and then run 'arcade login' to authenticate.
@app.tool(requires_auth=Reddit(scopes=["read"]))
async def get_posts_in_subreddit(
context: Context, subreddit: Annotated[str, "The name of the subreddit"]
) -> dict:
"""Get posts from a specific subreddit"""
subreddit = subreddit.lower().replace("r/", "").replace(" ", "")
oauth_token = context.get_auth_token_or_empty()
headers = {
"Authorization": f"Bearer {oauth_token}",
"User-Agent": "authorization-mcp-server",
}
params = {"limit": 5}
url = f"https://oauth.reddit.com/r/{subreddit}/hot"

async with httpx.AsyncClient() as client:
response = await client.get(url, headers=headers, params=params)
response.raise_for_status()

return response.json()


if __name__ == "__main__":
app.run(transport="http", host="127.0.0.1", port=8000)
4 changes: 0 additions & 4 deletions libs/arcade-mcp-server/arcade_mcp_server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,11 @@

__all__ = [
"Context",
# FastAPI-like interface
"MCPApp",
# MCP Server implementation
"MCPServer",
"MCPSettings",
# Integrated Factory and Runner
"create_arcade_mcp",
"run_arcade_mcp",
# Re-exported TDK functionality
"tool",
]

Expand Down
Loading