A production-ready MCP (Model Context Protocol) server template built on FastMCP 3.x, with filesystem-based component discovery, standalone decorators, and seamless OpenShift deployment.
- Standalone decorators (
@tool,@resource,@prompt) with automatic filesystem discovery - Resource subdirectories for organizing related resources
- Python-based prompts with type safety and Pydantic Field annotations
- Class-based middleware for cross-cutting concerns
- Generator system for scaffolding new components with non-interactive CLI
- Selective updates - patch infrastructure without losing custom code
- One-command OpenShift deployment
- Hot-reload via
FileSystemProvider(reload=True)for local development - Local STDIO and OpenShift HTTP transports
- JWT authentication (optional) with built-in
JWTVerifierandRemoteAuthProvider - Full test suite with pytest
# Install and run locally
make install
make run-local
# Test with cmcp (in another terminal)
cmcp ".venv/bin/python -m src.main" tools/list# IMPORTANT: Remove examples before first deployment
./remove_examples.sh
# One-command deployment
make deploy
# Or deploy to specific project
make deploy PROJECT=my-projectNote: Running
./remove_examples.shbefore deployment removes example code and cache files, significantly reducing build context size and preventing deployment timeouts.
This template includes slash commands for Claude Code that provide a structured workflow for developing MCP tools.
/plan-tools -> TOOLS_PLAN.md (planning, no code)
|
/create-tools -> Generate + implement tools in parallel
|
/exercise-tools -> Test ergonomics as consuming agent
|
/deploy-mcp PROJECT=x -> Deploy to OpenShift (optional)
| Command | Description |
|---|---|
/plan-tools |
Reads Anthropic's tool design guidance and your proposal, then creates TOOLS_PLAN.md with tool specifications. Planning only - no code. |
/create-tools |
Reads TOOLS_PLAN.md, generates scaffolds with fips-agents, and implements each tool in parallel using subagents for efficiency. |
/exercise-tools |
Role-plays as the agent that will consume these tools, testing ergonomics, error messages, and composability. Provides structured feedback and makes improvements. |
/deploy-mcp PROJECT=name |
Runs pre-flight checks (permissions, tests), deploys to OpenShift, and verifies with mcp-test-mcp. |
- Starting a new MCP server: Run
/plan-toolsfirst to design your tools before writing any code - After planning is approved: Run
/create-toolsto implement everything in parallel - Before deployment: Run
/exercise-toolsto catch usability issues - For remote MCP servers: Run
/deploy-mcpto deploy to OpenShift
See CLAUDE.md for detailed documentation on the workflow, known issues, and troubleshooting.
src/
core/ # Server bootstrap and auth configuration
tools/ # Tool implementations (standalone @tool decorators)
examples/ # Example tools (removed before deployment)
resources/ # Resource implementations (supports subdirectories)
examples/ # Example resources
prompts/ # Python-based prompt definitions
examples/ # Example prompts
middleware/ # Middleware classes
tests/ # Test suite
.fips-agents-cli/ # Generator templates
.template-info # Template version tracking (for updates)
Containerfile # Container definition
openshift.yaml # OpenShift manifests
deploy.sh # Deployment script
requirements.txt # Python dependencies
Makefile # Common tasks
Create a Python file in src/tools/. Tools use the standalone @tool decorator from FastMCP 3.x -- no shared server instance needed:
from typing import Annotated
from pydantic import Field
from fastmcp import Context
from fastmcp.tools import tool
from fastmcp.exceptions import ToolError
@tool(
annotations={
"readOnlyHint": True,
"idempotentHint": True,
"openWorldHint": False,
}
)
async def my_tool(
param: Annotated[str, Field(description="Parameter description", min_length=1, max_length=100)],
ctx: Context,
) -> str:
"""Tool description for the LLM."""
await ctx.info("Processing request")
if not param.strip():
raise ToolError("Parameter cannot be empty")
return f"Result: {param}"Best Practices:
- Use
Annotatedfor parameter descriptions - Add Pydantic
Fieldconstraints for validation - Use tool
annotationsfor hints about behavior - Include
ctx: Contextfor logging and capabilities - Raise
ToolErrorfor user-facing validation errors - Use structured output (dataclasses) for complex results
Auth-protected tools:
from fastmcp.server.auth import require_scopes
from fastmcp.tools import tool
@tool(auth=require_scopes("admin"))
async def admin_tool() -> str:
"""Only accessible with admin scope."""
return "secret data"Generator examples:
Note:
fips-agentsis a global CLI tool (installed via pipx). Run it directly - do NOT use.venv/bin/fips-agents.
# Simple tool
fips-agents generate tool my_tool \
--description "Tool description" \
--async
# Tool with context
fips-agents generate tool search_documents \
--description "Search through documents" \
--async \
--with-context
# Tool with authentication
fips-agents generate tool protected_operation \
--description "Protected operation" \
--async \
--with-auth
# Tool with parameters from JSON file
fips-agents generate tool complex_tool \
--description "Complex tool with multiple params" \
--params params.json \
--with-context
# Advanced tool with all options
fips-agents generate tool advanced_tool \
--description "Advanced tool example" \
--async \
--with-context \
--with-auth \
--return-type "dict" \
--read-only \
--idempotentResources can be organized in subdirectories for better structure. Create files in src/resources/ or any subdirectory:
Simple resource:
from fastmcp.resources import resource
@resource("resource://my-resource")
async def get_my_resource() -> str:
return "Resource content"JSON resource with metadata:
from fastmcp.resources import resource
@resource(
"data://config",
mime_type="application/json",
description="Application configuration data"
)
async def get_config() -> dict:
return {"version": "1.0", "features": ["tools", "resources"]}Resource template (parameterized):
from fastmcp.resources import resource
@resource("weather://{city}/current")
async def get_weather(city: str) -> dict:
"""Weather information for a specific city."""
return {"city": city, "temperature": 22, "condition": "Sunny"}Organizing resources in subdirectories:
src/resources/
country_profiles/
__init__.py
japan.py # country-profiles://JP
france.py # country-profiles://FR
checklists/
__init__.py
travel.py # travel-checklists://first-trip
emergency_protocols/
__init__.py
passport.py # emergency-protocols://passport-lost
Generator examples:
# Simple resource
fips-agents generate resource my_resource \
--description "My resource description" \
--uri "resource://my-resource" \
--mime-type "text/plain"
# JSON resource
fips-agents generate resource config_data \
--description "Application configuration" \
--uri "data://config" \
--mime-type "application/json"
# Resource in subdirectory (creates country_profiles/japan.py)
fips-agents generate resource country-profiles/japan \
--description "Japan country profile" \
--uri "country-profiles://JP" \
--mime-type "application/json"
# Resource template with async and context
fips-agents generate resource weather \
--async \
--with-context \
--description "Weather data by city" \
--uri "weather://{city}/current" \
--mime-type "application/json"Subdirectories are automatically discovered by FileSystemProvider -- no manual registration needed.
Create Python files in src/prompts/. Prompts use the standalone @prompt decorator:
Basic String Prompt:
from pydantic import Field
from fastmcp.prompts import prompt
@prompt
def my_prompt(
query: str = Field(description="User query"),
) -> str:
"""Purpose of this prompt"""
return f"Please answer: {query}"Async Prompt with Context:
from pydantic import Field
from fastmcp import Context
from fastmcp.prompts import prompt
@prompt
async def fetch_prompt(
url: str = Field(description="Data source URL"),
ctx: Context = None,
) -> str:
"""Fetch data and create prompt"""
return f"Analyze data from {url}"Structured Message Prompt:
from pydantic import Field
from fastmcp.prompts.prompt import PromptMessage, TextContent
from fastmcp.prompts import prompt
@prompt
def structured_prompt(
task: str = Field(description="Task description"),
) -> PromptMessage:
"""Create structured message"""
return PromptMessage(
role="user",
content=TextContent(type="text", text=f"Task: {task}")
)Advanced with Metadata:
from pydantic import Field
from fastmcp.prompts import prompt
@prompt(
name="custom_name",
title="Human Readable Title",
description="Custom description",
tags={"analysis", "reporting"},
meta={"version": "1.0", "author": "team"}
)
def advanced_prompt(
data: dict[str, str] = Field(description="Data to process"),
) -> str:
"""Advanced prompt with full metadata"""
return f"Analyze: {data}"Generator Examples:
# Basic prompt
fips-agents generate prompt summarize_text \
--description "Summarize text content"
# Async with Context
fips-agents generate prompt fetch_and_analyze \
--async --with-context \
--return-type PromptMessage
# With parameters file
fips-agents generate prompt analyze_data \
--params params.json --with-schema
# Advanced with metadata
fips-agents generate prompt report_generator \
--async --with-context \
--prompt-name "generate_report" \
--title "Report Generator" \
--tags "reporting,analysis" \
--meta '{"version": "2.0"}'Return Types:
str- Simple string prompt (default)PromptMessage- Structured message with rolelist[PromptMessage]- Multi-turn conversationPromptResult- Full prompt result object
See CLAUDE.md for comprehensive prompt generation documentation and src/prompts/ for working examples.
Middleware in FastMCP 3.x uses class-based middleware that extends the Middleware base class. Create a file in src/middleware/ and register it in src/core/server.py:
import mcp.types as mt
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
from fastmcp.tools.tool import ToolResult
class MyMiddleware(Middleware):
async def on_call_tool(
self,
context: MiddlewareContext[mt.CallToolRequestParams],
call_next: CallNext[mt.CallToolRequestParams, ToolResult],
) -> ToolResult:
# Pre-execution logic
result = await call_next(context)
# Post-execution logic
return resultMiddleware is passed to the FastMCP constructor in src/core/server.py:
from fastmcp.server.middleware.logging import LoggingMiddleware
mcp = FastMCP(
name,
middleware=[LoggingMiddleware(), MyMiddleware()],
...
)Generator examples:
# Async middleware
fips-agents generate middleware logging_middleware \
--description "Request logging middleware" \
--async
# Sync middleware
fips-agents generate middleware rate_limiter \
--description "Rate limiting middleware" \
--sync# Run server
make run-local
# Test with cmcp
make test-local
# Run unit tests
make test# Deploy
make deploy
# Test with MCP Inspector
npx @modelcontextprotocol/inspector https://<route-url>/mcp/See TESTING.md for detailed testing instructions.
This template is actively maintained with improvements to infrastructure, generators, and documentation. You can selectively update your project from template changes without losing your custom code.
# See what's changed since project creation
fips-agents patch checkThis shows available updates organized by category (generators, core, docs, build).
# Update generator templates (safe - your code is untouched)
fips-agents patch generators
# Update core infrastructure (shows diffs, asks for approval)
fips-agents patch core
# Update documentation and examples (safe)
fips-agents patch docs
# Update build and deployment files (shows diffs, asks for approval)
fips-agents patch build
# Preview changes without applying (dry run)
fips-agents patch core --dry-run# Interactively update all categories
fips-agents patch all
# Skip confirmation prompts (use with caution)
fips-agents patch all --skip-confirmationAutomatically updated (no confirmation):
.fips-agents-cli/generators/- Code generator templatesdocs/- Documentation files- Example files in
src/*/examples/
Asks before updating (shows diffs):
src/core/server.py- Server bootstrap codesrc/*/__ init__.py- Package initialization filesMakefile,Containerfile,openshift.yaml- Build files
Never updated (your code is protected):
src/tools/*.py- Your tool implementationssrc/resources/*.py- Your resource implementationssrc/prompts/*.py- Your prompt definitionssrc/middleware/*.py- Your middleware implementationstests/- Your test filesREADME.md,pyproject.toml,.env- Project configurationsrc/core/app.py,src/core/auth.py,src/core/logging.py- User-customizable core files
Imagine the template adds new authentication capabilities in a future update:
# Check what's new
fips-agents patch check
# Pull in updated generators so you can generate auth-enabled tools
fips-agents patch generators
# Review and apply core infrastructure updates
fips-agents patch core # Shows diffs, you decide what to apply
# Your existing tools, resources, and prompts remain untouched!The .template-info file tracks which template version your project was created from, enabling smart updates.
MCP supports multiple transport protocols. The server defines which transport to expose--clients must connect using the matching transport type.
The MCP_TRANSPORT environment variable controls which transport the server runs:
| Transport | Use Case | Client Connection |
|---|---|---|
stdio |
Local development, CLI tools like cmcp |
Spawns server as subprocess |
http |
Remote access, OpenShift deployment | HTTP request to http://host:port/mcp/ |
The same codebase supports both transports. The server reads MCP_TRANSPORT at startup and exposes only that transport--there's no negotiation or auto-detection.
# Server runs in STDIO mode (default)
MCP_TRANSPORT=stdio .venv/bin/python -m src.main
# Client spawns the server as a subprocess
cmcp ".venv/bin/python -m src.main" tools/listSTDIO is ideal for local testing because the client manages the server lifecycle directly.
# Server runs in HTTP mode
MCP_TRANSPORT=http MCP_HTTP_HOST=0.0.0.0 MCP_HTTP_PORT=8080 python -m src.main
# Client connects via HTTP
# (configure your MCP client to use the HTTP endpoint)In OpenShift, the Containerfile sets MCP_TRANSPORT=http automatically. The Route exposes the /mcp/ endpoint with TLS termination.
Your MCP client configuration must specify the correct transport:
For STDIO (local):
{
"mcpServers": {
"my-server": {
"command": ".venv/bin/python",
"args": ["-m", "src.main"]
}
}
}For HTTP (remote):
{
"mcpServers": {
"my-server": {
"url": "https://my-server-route.apps.cluster.example.com/mcp/"
}
}
}MCP_TRANSPORT=stdio- Use STDIO transportMCP_HOT_RELOAD=1- Enable hot-reload
MCP_TRANSPORT=http- Use HTTP transport (set in Containerfile)MCP_HTTP_HOST=0.0.0.0- HTTP server hostMCP_HTTP_PORT=8080- HTTP server portMCP_HTTP_PATH=/mcp/- HTTP endpoint path
MCP_AUTH_JWT_ALG- JWT algorithm (RS256, HS256, etc.)MCP_AUTH_JWT_SECRET- JWT secret for symmetric signingMCP_AUTH_JWT_PUBLIC_KEY- JWT public key for asymmetricMCP_AUTH_JWT_JWKS_URI- JWKS endpoint URLMCP_AUTH_JWT_ISSUER- Expected token issuerMCP_AUTH_JWT_AUDIENCE- Expected token audienceMCP_AUTH_REQUIRED_SCOPES- Comma-separated required scopesMCP_AUTH_AUTHORIZATION_SERVERS- Comma-separated authorization server URLsMCP_AUTH_BASE_URL- This server's base URL for OAuth metadata
make help # Show all available commands
make install # Install dependencies
make run-local # Run locally with STDIO
make test # Run test suite
make deploy # Deploy to OpenShift
make clean # Clean up OpenShift deploymentThe server uses FastMCP 3.x with:
FileSystemProviderfor automatic discovery of tools, resources, and prompts- Standalone decorators (
@tool,@resource,@prompt) -- no shared server instance needed - Hot-reload via
FileSystemProvider(reload=True)in development mode - Class-based middleware passed to the
FastMCPconstructor - Built-in
JWTVerifierandRemoteAuthProviderfor authentication - Generator system with Jinja2 templates for scaffolding
- Support for both STDIO (local) and HTTP (OpenShift) transports
See ARCHITECTURE.md for detailed architecture information and GENERATOR_PLAN.md for generator system documentation.
- Python 3.11+
- OpenShift CLI (
oc) for deployment - cmcp for local testing:
pip install cmcp
We welcome contributions! Please see our Contributing Guide for details on how to get started, development setup, and submission guidelines.
This project is licensed under the MIT License - see the LICENSE file for details.