Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dockerized It #21

Closed
wants to merge 4 commits into from
Closed
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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ mcpo makes your AI tools usable, secure, and interoperable—right now, with zer

## 🚀 Quick Usage


We recommend using uv for lightning-fast startup and zero config.

```bash
Expand All @@ -48,6 +49,30 @@ uvx mcpo --port 8000 -- uvx mcp-server-time --local-timezone=America/New_York

That’s it. Your MCP tool is now available at http://localhost:8000 with a generated OpenAPI schema — test it live at [http://localhost:8000/docs](http://localhost:8000/docs).

## 🐋 Docker

mcpo can be run in Docker with the enclosed dockerfile. Mount a volume containing config.json to /app and add custom (offline/source) servers to /servers.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(optional)


Each time the container launches it will look for new (python) tools it has not yet installed and install them + their requirements.txt with pip.

If config.json doesn't exist, the example.config.json will be used so that boot is successful.

Example docker-compose:

```yaml
services:
mcpo:
build:
context: .
dockerfile: dockerfile
container_name: mcpo
ports:
- "8080:8000"
volumes:
- ./app:/app
- ./servers:/servers
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mark as optional

```

### 🔄 Using a Config File

You can serve multiple MCP tools via a single config file that follows the [Claude Desktop](https://modelcontextprotocol.io/quickstart/user) format:
Expand Down
68 changes: 68 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
#
# This script checks /servers for any new subfolders (custom mcp servers). If it finds
# a subfolder with python requirements.txt that have not yet been installed,
# it installs them, then finally runs mcpo (or whatever command is passed).
# /servers dir also allows adding custom executables, etc. on the fly.
# TO DO: Add support for offline node modules.

set -e # Exit immediately if a command exits with a non-zero status

DIR="/servers"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should leave this as an optional env var and set the default as an empty string

if [ ! -d "$DIR" ]; then
echo "$DIR does not exist. Creating it..."
mkdir -p "$DIR"
fi

if [ -d "$DIR" ] && [ -z "$(ls -A "$DIR")" ]; then
# Servers directory is empty
echo "$DIR is empty"
fi

if ! [ -f "/app/config.json" ]; then
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should raise an exception instead of using example.config.json

echo "No config supplied, using example config"
cp /usr/local/bin/example.config.json /app/config.json
else
echo "Config file exists."
fi

# Directory to track which /servers/ subfolders we've installed
INSTALLED_DIR="/usr/local/bin/installed_servers"

# Ensure the tracking directory exists
mkdir -p "$INSTALLED_DIR"

# Temporary file to combine new requirements
NEW_REQS_FILE=$(mktemp)

# For each subfolder in /servers, if not yet installed, add its requirements
if [ -d "$DIR" ]; then
for folder in "$DIR/*"; do
if [ -d "$folder" ]; then
subfolder="$(basename "$folder")"
# Install the actual server
echo "$folder" >> "$NEW_REQS_FILE"
# If we haven't installed this subfolder yet
if [ ! -f "$INSTALLED_DIR/$subfolder" ]; then
# If it has a requirements.txt, add to the combined file
if [ -f "$folder/requirements.txt" ]; then
for l in "$folder/requirements.txt"; do (cat "${l}"; echo) >> "$NEW_REQS_FILE"; done
fi

# Mark this subfolder as installed
touch "$INSTALLED_DIR/$subfolder"
fi
fi
done
fi

# If NEW_REQS_FILE is non-empty, install new python dependencies
if [ -s "$NEW_REQS_FILE" ]; then
pip install -r "$NEW_REQS_FILE"
fi

# Clean up
rm "$NEW_REQS_FILE"

# Finally, execute the command that was passed in (defaults to "mcpo --config $CONFIG_PATH" from the Dockerfile)
exec "$@"
41 changes: 41 additions & 0 deletions dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

# Use the official Python image as a base
FROM ghcr.io/astral-sh/uv:python3.11-bookworm-slim

# Set environment variables
ENV PYTHONUNBUFFERED=1 \
CONFIG_PATH=/app/config.json \
UVICORN_PORT=8000 \
UVICORN_LOG_LEVEL=info

# Create and set the working directory
WORKDIR /app

# Copy source to container
COPY . /app

# Install mcpo from source
RUN pip install .

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better to run this after apt-get since apt-get would be cached all the time, and only the next layers will be re-built.


# Install git, curl, and npm (and clean up)
RUN apt-get update && \
apt-get install -y git curl npm && \
rm -rf /var/lib/apt/lists/*

# Copy the entrypoint script into the container
COPY docker-entrypoint.sh /usr/local/bin/

# Copy the example configuration JSON into the container in case the user does not supply one
COPY example.config.json /usr/local/bin/

# Make sure the entrypoint script is executable
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

# Expose the port mcpo will run on
EXPOSE $UVICORN_PORT

# Use the entrypoint script
ENTRYPOINT ["docker-entrypoint.sh"]

# Command to run mcpo with the specified configuration
CMD mcpo --config "$CONFIG_PATH"
12 changes: 12 additions & 0 deletions example.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"mcpServers": {
"memory": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-memory"]
},
"time": {
"command": "uvx",
"args": ["mcp-server-time"]
}
}
}
8 changes: 5 additions & 3 deletions src/mcpo/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ async def lifespan(app: FastAPI):
yield


async def run(host: str = "127.0.0.1", port: int = 8000, **kwargs):
async def run(host: str = "127.0.0.1", port: int = None, **kwargs):
if port is None:
port = int(os.getenv("UVICORN_PORT", 8000)) # Use PORT from env, or fallback to 8000
config_path = kwargs.get("config")
server_command = kwargs.get("server_command")
name = kwargs.get("name") or "MCP OpenAPI Proxy"
Expand Down Expand Up @@ -181,8 +183,8 @@ async def run(host: str = "127.0.0.1", port: int = 8000, **kwargs):

else:
raise ValueError("You must provide either server_command or config.")

config = uvicorn.Config(app=main_app, host=host, port=port, log_level="info")
log_level = os.getenv("UVICORN_LOG_LEVEL", "info") # Use UVICORN_LOG_LEVEL from env, or fallback to info
config = uvicorn.Config(app=main_app, host=host, port=port, log_level=log_level)
server = uvicorn.Server(config)

await server.serve()