Production-ready FastMCP server image with dynamic loading of tools, resources, prompts, and knowledge bases from multiple sources.
Run your own MCP server anywhere — Docker, Compose, Swarm, Kubernetes, or any container runtime. Define tools in Python, mount them, and your server is live.
# Create a tool
mkdir -p tools
cat > tools/hello.py << 'EOF'
def greet(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}!"
EOF
# Run the server
docker run -d \
-p 8000:8000 \
-v $(pwd)/tools:/app/inline/tools \
docker.io/helmforge/fastmcp-server:0.4.0Your MCP server is now available at http://localhost:8000/mcp.
The built-in Web UI is at http://localhost:8000/ui.
The server loads MCP components (tools, resources, prompts, knowledge) from a workspace directory. On startup, it syncs files from one or more sources into the workspace, registers everything with FastMCP, and exposes the server over HTTP.
Sources (Inline, S3, Git)
│
▼
/app/workspace/
├── tools/*.py → registered as MCP tools
├── resources/*.py → registered as MCP resources
├── prompts/*.py → registered as MCP prompts
└── knowledge/* → served as knowledge:// resources
│
▼
FastMCP Server (:8000/mcp)
├── /ui → Web dashboard
├── /healthz → Liveness probe
├── /readyz → Readiness probe
├── /startupz → Startup probe
├── /debug/info → Server diagnostics
├── /metrics → Prometheus metrics (opt-in)
└── /api/* → JSON API for UI
The image supports three data sources with merge precedence (highest first):
| Source | Best For | Limit |
|---|---|---|
Inline (volume mount at /app/inline/) |
Quick setup, small configs | Host filesystem |
| S3 (AWS S3, MinIO, Cloudflare R2) | Teams, CI/CD pipelines, large knowledge bases | Unlimited |
| Git (any HTTPS repo) | Version-controlled tools, collaboration | Repo size |
All sources can be combined. Inline always wins on conflicts.
services:
mcp-server:
image: docker.io/helmforge/fastmcp-server:0.4.0
ports:
- "8000:8000"
volumes:
- ./tools:/app/inline/tools
- ./resources:/app/inline/resources
- ./prompts:/app/inline/prompts
- ./knowledge:/app/inline/knowledge
environment:
MCP_SERVER_NAME: my-mcp-server
MCP_AUTH_TYPE: bearer
MCP_AUTH_TOKEN: ${MCP_AUTH_TOKEN}
healthcheck:
test: ["CMD", "curl", "-sf", "http://localhost:8000/healthz"]
interval: 30s
timeout: 5s
retries: 3services:
mcp-server:
image: docker.io/helmforge/fastmcp-server:0.4.0
ports:
- "8000:8000"
environment:
SOURCE_S3_ENABLED: "true"
SOURCE_S3_ENDPOINT: http://minio:9000
SOURCE_S3_BUCKET: mcp-tools
SOURCE_S3_ACCESS_KEY: minioadmin
SOURCE_S3_SECRET_KEY: minioadmin
minio:
image: docker.io/minio/minio:RELEASE.2025-04-03T14-56-28Z
command: server /data --console-address ":9001"
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmindocker run -d \
-p 8000:8000 \
-e SOURCE_GIT_ENABLED=true \
-e SOURCE_GIT_REPOSITORY=https://github.com/myorg/mcp-tools.git \
-e SOURCE_GIT_BRANCH=main \
docker.io/helmforge/fastmcp-server:0.4.0For private repos, set SOURCE_GIT_TOKEN with a personal access token.
Create a .py file in tools/. By default, every public function defined in that module becomes an MCP tool:
def get_weather(city: str) -> str:
"""Get current weather for a city."""
import httpx
return httpx.get(f"https://wttr.in/{city}?format=3").text
def roll_dice(sides: int = 6) -> int:
"""Roll a die with the given number of sides."""
import random
return random.randint(1, sides)Utility modules are supported too. Files named like *_helpers.py are skipped by default. For any other utility module, keep helpers private (_helper) or disable auto-registration explicitly:
__mcp_auto_register__ = False
def evidence_true(data, *keys):
return TrueIf you want exact control over exports, declare a TOOLS allowlist:
TOOLS = ["deploy"]
def deploy(service: str, version: str) -> str:
"""Deploy a service."""
return f"Deployed {service}@{version}"
def helper() -> str:
return "not registered"Add optional module-level variables to control tool registration:
__tags__ = {"devops", "production"} # Categorization tags
__timeout__ = 30.0 # Execution timeout (seconds)
__annotations_mcp__ = { # MCP behavior hints
"destructiveHint": True,
"title": "Deploy Service"
}
def deploy(service: str, version: str) -> str:
"""Deploy a service to production."""
return f"Deployed {service}@{version}"Both async def and def functions work. Async is preferred for I/O-bound operations:
async def fetch_url(url: str) -> str:
"""Fetch content from a URL."""
import httpx
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.textReturn ToolResult for full control over response format:
from fastmcp.tools.tool import ToolResult
def analyze(data: str) -> ToolResult:
"""Analyze data with structured output."""
return ToolResult(
content="Analysis complete",
structured_content={"word_count": len(data.split())},
meta={"version": "1.0"}
)Create a .py file in resources/ with a RESOURCE_URI constant and a handler function:
import json
RESOURCE_URI = "config://app"
def get_config() -> str:
"""Application configuration."""
return json.dumps({"version": "1.0", "env": "production"}, indent=2)Important: Resource handlers must return
str,bytes, orlist[ResourceContent]. Returning adictdirectly will cause a serialization error.
Use {param} placeholders in URIs for parameterized resources:
import json
RESOURCE_URI = "users://{user_id}/profile"
def get_profile(user_id: str) -> str:
"""Get user profile by ID."""
return json.dumps({"user_id": user_id, "name": f"User {user_id}"}, indent=2)Use a RESOURCES dict to register multiple resources from one file:
import json
RESOURCES = {
"status://health": "get_health",
"status://version": "get_version",
}
def get_health() -> str:
return json.dumps({"status": "ok"}, indent=2)
def get_version() -> str:
return "1.0.0"Create a .py file in prompts/. Every public function becomes an MCP prompt:
def summarize(text: str) -> str:
"""Summarize the provided text."""
return f"Please provide a concise summary of:\n\n{text}"Any file placed in knowledge/ is served as a knowledge:// resource. Supports text, markdown, JSON, YAML, or any UTF-8 file.
knowledge/
├── product-overview.md
├── api-reference.json
└── troubleshooting/
├── common-errors.md
└── faq.md
These become accessible as knowledge://product-overview.md, knowledge://troubleshooting/common-errors.md, etc.
| Type | Variables | Use Case |
|---|---|---|
| None | MCP_AUTH_TYPE=none |
Development, internal networks |
| Bearer | MCP_AUTH_TYPE=bearer + MCP_AUTH_TOKEN |
API keys, service accounts |
| JWT | MCP_AUTH_TYPE=jwt + MCP_AUTH_JWT_* |
OAuth/OIDC, enterprise SSO |
The embedded dashboard at /ui provides:
- Dashboard — Server name, version, uptime, component counts, source status
- Tools Explorer — All registered tools with descriptions, parameters, tags, timeout
- Resources Explorer — Resources and templates with URIs, MIME types
- Prompts Explorer — All prompts with descriptions
Auto-refreshes every 15 seconds. Disable with MCP_UI_ENABLED=false.
| Endpoint | Purpose | When 200 |
|---|---|---|
GET /healthz |
Liveness | Always (process is running) |
GET /readyz |
Readiness | Sources synced + components loaded |
GET /startupz |
Startup | Full initialization complete |
GET /debug/info returns full server diagnostics: version, FastMCP version, uptime, component details, source status, auth type, and configuration.
Enable with MCP_METRICS_ENABLED=true. Exposes at /metrics:
mcp_tools_total— Number of registered toolsmcp_resources_total— Number of registered resourcesmcp_prompts_total— Number of registered promptsmcp_knowledge_total— Number of knowledge filesmcp_tool_calls_total{tool}— Tool invocation countermcp_tool_duration_seconds{tool}— Tool execution duration histogrammcp_tool_errors_total{tool}— Tool error countermcp_sources_sync_total{source,status}— Source sync operationsmcp_auth_requests_total{result}— Auth attempt counter
Set LOG_FORMAT=json for JSON-structured logs compatible with Loki, ELK, CloudWatch, and Datadog:
{
"timestamp": "2026-04-05T10:30:00+00:00",
"level": "INFO",
"logger": "fastmcp-server.builder",
"message": "Registered tool: greet"
}| Variable | Default | Description |
|---|---|---|
MCP_SERVER_NAME |
fastmcp-server |
Server display name |
MCP_HOST |
0.0.0.0 |
Listen address |
MCP_PORT |
8000 |
Listen port |
MCP_PATH |
/mcp |
HTTP endpoint path |
MCP_WORKSPACE |
/app/workspace |
Workspace directory |
LOG_LEVEL |
INFO |
Logging level |
LOG_FORMAT |
text |
Log format: text or json |
MCP_MASK_ERROR_DETAILS |
false |
Hide internal error details from clients |
MCP_ON_DUPLICATE_TOOLS |
warn |
Duplicate handling: warn, error, replace, ignore |
MCP_STRICT_LOADING |
false |
Fail on boot if any tool/resource has errors |
MCP_UI_ENABLED |
true |
Enable built-in Web UI at /ui |
MCP_METRICS_ENABLED |
false |
Enable Prometheus metrics at /metrics |
EXTRA_PIP_PACKAGES |
Comma-separated pip packages to install at startup |
| Variable | Default | Description |
|---|---|---|
MCP_AUTH_TYPE |
none |
bearer, jwt, or none |
MCP_AUTH_TOKEN |
Bearer token value | |
MCP_AUTH_JWT_ISSUER |
JWT issuer | |
MCP_AUTH_JWT_AUDIENCE |
JWT audience | |
MCP_AUTH_JWT_JWKS_URI |
JWKS endpoint URL |
| Variable | Default | Description |
|---|---|---|
SOURCE_S3_ENABLED |
false |
Enable S3 sync |
SOURCE_S3_ENDPOINT |
S3-compatible endpoint URL | |
SOURCE_S3_BUCKET |
Bucket name | |
SOURCE_S3_REGION |
us-east-1 |
AWS region |
SOURCE_S3_PREFIX |
Key prefix filter | |
SOURCE_S3_ACCESS_KEY |
Access key ID | |
SOURCE_S3_SECRET_KEY |
Secret access key |
| Variable | Default | Description |
|---|---|---|
SOURCE_GIT_ENABLED |
false |
Enable Git sync |
SOURCE_GIT_REPOSITORY |
Repository HTTPS URL | |
SOURCE_GIT_BRANCH |
main |
Branch to clone |
SOURCE_GIT_PATH |
Subdirectory within the repo | |
SOURCE_GIT_TOKEN |
Auth token for private repos |
For Kubernetes, separate source syncing from server startup using sync_only.py:
python /app/sync_only.pyThis script runs sync_sources() and exits. Use it as a Kubernetes init container to pre-populate the workspace volume before the main server starts.
| Method | Docs |
|---|---|
| Docker / Docker Compose / Swarm | This README |
| Kubernetes (Helm) | helmforge/charts — fastmcp-server |
Once your FastMCP server is running and accessible, connect AI assistants to it as an MCP server.
Add the server to your Claude Code settings (~/.claude/settings.json or project .claude/settings.json):
{
"mcpServers": {
"my-mcp-server": {
"type": "streamable-http",
"url": "https://mcp.example.com/mcp"
}
}
}With bearer authentication:
{
"mcpServers": {
"my-mcp-server": {
"type": "streamable-http",
"url": "https://mcp.example.com/mcp",
"headers": {
"Authorization": "Bearer <your-token>"
}
}
}
}Add to your Codex configuration file (~/.codex/config.toml):
[mcp_servers.my-mcp-server]
enabled = true
url = "https://mcp.example.com/mcp"
[mcp_servers.my-mcp-server.http_headers]
Authorization = "Bearer <your-token>"Without authentication (development):
[mcp_servers.my-mcp-server]
enabled = true
url = "https://mcp.example.com/mcp"When running locally with Docker, use http://localhost:8000/mcp as the URL and omit authentication headers if MCP_AUTH_TYPE=none.
Apache-2.0 — see LICENSE.