ProdMCP
FastMCP is a modern, fast (high-performance), framework for building MCPs with Python based on standard Python type hints.
Ask AI about ProdMCP
Powered by Claude Β· Grounded in docs
I know everything about ProdMCP. Ask me about installation, configuration, usage, or troubleshooting.
0/500
Reviews
Documentation
ProdMCP
Unified production framework for both REST APIs and MCP servers. Drop-in replacement for FastAPI and FastMCP with schema validation, security, middleware, dependency injection, and OpenMCP spec generation.
π‘οΈ Enterprise Compliance & Security
ProdMCP is engineered for highly-regulated enterprise environments.
- FOSSA: 100% License Compliant, 0 Security/Dependency Vulnerabilities.
- SonarCloud SAST: Grade A (0 Bugs, 0 Vulnerabilities, 0 Security Hotspots, >80% Test Coverage).
- GitHub Advanced Security: Active CodeQL tracking.
Installation
pip install prodmcp # Core (MCP tools, prompts, resources, security)
pip install prodmcp[rest] # + FastAPI + Uvicorn for the unified server
Quick Start
from prodmcp import ProdMCP
from pydantic import BaseModel
app = ProdMCP("MyServer", version="1.0.0")
class UserResponse(BaseModel):
"""A user's public profile information."""
name: str
email: str
@app.tool(name="get_user", description="Fetch user by ID")
@app.get("/users/{user_id}")
def get_user(user_id: str) -> UserResponse:
return UserResponse(name="Alice", email="alice@example.com")
if __name__ == "__main__":
app.run() # REST at / (Swagger at /docs) + MCP at /mcp/mcp
The UserResponse docstring automatically appears in the OpenAPI responses.200.description and the OpenMCP output_description field β no extra code required.
β¨ What's New in v0.5.0
Response Descriptions from Pydantic Docstrings
ProdMCP now auto-derives the responses.200.description (OpenAPI) and output_description (OpenMCP) from your output Pydantic model's class docstring. No decorator parameter needed.
class TicketResponse(BaseModel):
"""A successfully created or retrieved support ticket."""
id: str
status: str
@app.tool(name="create_ticket", description="Open a new support ticket")
def create_ticket(payload: TicketCreate) -> TicketResponse:
...
Generated OpenAPI:
"responses": {
"200": {
"description": "A successfully created or retrieved support ticket."
}
}
Generated OpenMCP:
"output_description": "A successfully created or retrieved support ticket."
Union / Optional support:
class InvoiceResponse(BaseModel):
"""A finalized invoice record."""
...
class DraftResponse(BaseModel):
"""A draft invoice pending approval."""
...
def get_invoice(...) -> Union[InvoiceResponse, DraftResponse]:
...
# output_description β "InvoiceResponse: A finalized invoice record. | DraftResponse: A draft invoice pending approval."
Return-Annotation output_schema Fallback
_register_tool, _register_prompt, and _register_resource now read the function's return annotation (-> MyModel) as the output_schema automatically when output_schema= is not specified explicitly.
# Before (explicit β still works):
@app.tool(name="get_weather", output_schema=WeatherResponse)
def get_weather(city: str) -> WeatherResponse: ...
# After (idiomatic β docstring propagates automatically):
@app.tool(name="get_weather")
def get_weather(city: str) -> WeatherResponse: ...
Security Specification Hardening (42Crunch / MCPcrunch Compliance)
- Full OAuth2
authorizationCodeflow emitted incomponents.securitySchemesβ satisfiesOMCP-SEC-012. - Global
securityfield injected into generated OpenAPI and OpenMCP specs. - Per-operation
securitynow injected for prompts and resources, not just tools. - 401 / 403 error responses auto-added to all secured operations.
Azure AD / Entra ID Integration
ProdMCP ships a zero-boilerplate Azure Active Directory integration at prodmcp.integrations.azure.
It handles JWT validation, JWKS caching, multi-format issuer/audience support, and On-Behalf-Of (OBO) token exchange β in two lines of setup.
Setup
from prodmcp import ProdMCP, Depends
from prodmcp.integrations.azure import AzureADAuth, AzureADTokenContext
auth = AzureADAuth.from_env() # reads TENANT_ID, BACKEND_CLIENT_ID,
# BACKEND_CLIENT_SECRET, API_AUDIENCE from env
app = ProdMCP("MyServer")
app.add_security_scheme("bearer", auth.bearer_scheme)
Required environment variables
| Variable | Description |
|---|---|
TENANT_ID | Azure AD tenant GUID |
BACKEND_CLIENT_ID | Backend app registration client ID |
BACKEND_CLIENT_SECRET | Backend app registration client secret |
API_AUDIENCE | Expected aud claim (e.g. api://your-client-id) |
OBO_SCOPE | On-Behalf-Of target scope (default: https://graph.microsoft.com/.default) |
Protecting routes and MCP tools
@app.tool(name="get_data", description="Authenticated data fetch")
@app.get("/data")
@app.common(security=[{"bearer": []}])
def get_data(ctx: AzureADTokenContext = Depends(auth.require_context)) -> dict:
ctx.require_role("admin") # 403 if the user doesn't have the 'admin' role
obo = ctx.get_obo_token() # On-Behalf-Of token exchange (downstream API access)
return {
"user": ctx.user_info, # { oid, tid, name, preferred_username, roles, scp }
"obo_scope": obo.get("scope"),
}
AzureADTokenContext provides:
| Attribute / Method | Description |
|---|---|
ctx.token | Raw JWT string |
ctx.claims | Full decoded JWT payload |
ctx.user_info | Common identity fields (oid, tid, name, rolesβ¦) |
ctx.roles | List of roles from the JWT |
ctx.has_role("admin") | Boolean role check |
ctx.require_role("admin") | Raises 403 if role absent |
ctx.get_obo_token(scope=...) | On-Behalf-Of exchange |
Using with Google ADK (Agent Development Kit)
ProdMCP tools secured with Azure AD work seamlessly with ADK agents.
The Bearer token flows from the REST request β ADK's MCPToolset transport headers β ProdMCP's server-side security check β no extra auth code needed in the agent.
from google.adk.agents import LlmAgent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.tools.mcp_tool.mcp_toolset import MCPToolset, StreamableHTTPConnectionParams
session_service = InMemorySessionService()
@app.post("/api/chat")
@app.common(security=[{"bearer": []}])
async def chat(request: ChatRequest, ctx: AzureADTokenContext = Depends(auth.require_context)) -> dict:
toolset = MCPToolset(
connection_params=StreamableHTTPConnectionParams(
url=request.mcp_url,
headers={"Authorization": f"Bearer {ctx.token}"},
)
)
agent = LlmAgent(model="gemini-2.5-flash", tools=[toolset])
runner = Runner(agent=agent, app_name="myapp", session_service=session_service)
session = await session_service.create_session(app_name="myapp", user_id=ctx.claims["oid"])
from google.genai import types
result_text = ""
async for event in runner.run_async(
user_id=session.user_id,
session_id=session.id,
new_message=types.Content(role="user", parts=[types.Part(text=request.message)]),
):
if event.is_final_response() and event.content:
result_text = "".join(p.text for p in event.content.parts if hasattr(p, "text"))
await toolset.close()
return {"reply": result_text}
The headers={"Authorization": f"Bearer {ctx.token}"} line is the only auth-specific addition β everything else is standard ADK boilerplate.
Running the Example App (prodmcp-masl)
The prodmcp-masl directory contains a full reference implementation: Azure AD authentication + ADK agent + React frontend.
Prerequisites
- Python 3.11+, Node.js 18+
- An Azure AD tenant with:
- A frontend SPA app registration (public client, no secret)
- A backend API app registration (with a client secret and exposed API scope)
1. Configure environment
# prodmcp-masl/.env
TENANT_ID=your-tenant-guid
BACKEND_CLIENT_ID=your-backend-app-client-id
BACKEND_CLIENT_SECRET=your-backend-secret
API_AUDIENCE=api://your-backend-client-id
OBO_SCOPE=https://graph.microsoft.com/.default
GEMINI_API_KEY=your-gemini-api-key
ALLOWED_ORIGINS=http://localhost:5173
# prodmcp-masl/frontend/.env.local
VITE_TENANT_ID=your-tenant-guid
VITE_FRONTEND_CLIENT_ID=your-frontend-app-client-id
VITE_BACKEND_SCOPE=api://your-backend-client-id/your-scope-name
VITE_GEMINI_API_KEY=your-gemini-api-key # optional, can be entered in UI
2. Start the backend
cd prodmcp-masl
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
./run.sh
# β http://localhost:8000
# β Swagger UI: http://localhost:8000/docs
# β MCP endpoint: http://localhost:8000/mcp/mcp
3. Start the frontend
cd prodmcp-masl/frontend
npm install
npm run dev
# β http://localhost:5173
4. Test
Open http://localhost:5173 and sign in with your Azure AD account.
Sidebar β Test API Endpoints:
| Endpoint | What to verify |
|---|---|
GET /health β Run | Should return {"status": "ok"} with no auth |
GET /data β Run | Returns your identity claims + OBO token (200) |
GET /data β Run without auth | Returns 401 Unauthorized |
GET /api/tools β Run | Lists live MCP tools registered on the server |
Features
- Unified Framework β One
ProdMCPinstance replaces both FastAPI and FastMCP - Decorator Stacking β
@app.tool()+@app.get()on the same handler with@app.common()for shared config - HTTP Methods β
@app.get(),@app.post(),@app.put(),@app.delete(),@app.patch() - MCP Primitives β
@app.tool(),@app.prompt(),@app.resource() - Auto Response Descriptions β Pydantic model docstrings propagate to OpenAPI and OpenMCP specs automatically
- Return-Annotation Fallback β
output_schemainferred from-> ReturnTypeannotation automatically - Schema-First Validation β Pydantic models or raw JSON Schema for input/output
- Security Layer β Bearer, API key, OAuth2, OpenID Connect;
prodmcp.integrations.azurefor Azure AD - Middleware System β Global and per-handler before/after hooks
- Dependency Injection β
Depends()compatible with FastAPI and ProdMCP dependencies - ADK-Ready β Works out of the box with Google Agent Development Kit via
MCPToolset - OpenMCP Spec β Auto-generated machine-readable specification with
output_descriptionper capability
License
MIT
Release Notes
See CHANGELOG.md for the full version history.
v0.5.0 β Response descriptions from Pydantic docstrings + security hardening
- Pydantic model docstrings now automatically populate
responses.200.description(OpenAPI) andoutput_description(OpenMCP) β no decorator changes needed. Union[A, B]responses compose a combined description:"ModelA: desc | ModelB: desc".- Return-annotation
output_schemafallback:-> MyModelis sufficient,output_schema=MyModelin the decorator is no longer required. - Full OAuth2
authorizationCodeflow emitted in security schemes (42Crunch /OMCP-SEC-012compliant). - Global and per-operation
securityfields injected for all prompts, resources, and tools.
v0.4.0 β REST Bridge API cleanup
Renamed app.as_fastapi() β app.test_mcp_as_fastapi(). Old name removed.
v0.3.12 β Pydantic schema fix for secured MCP tools with ADK
Fixes a startup crash when ADK's MCPToolset is used with tools that have user-defined dependency types.
v0.3.11 β Azure AD / Entra ID integration (prodmcp.integrations.azure)
AzureADAuth, AzureADTokenContext, AzureADBearerScheme β complete Azure AD JWT validation, JWKS caching, multi-format issuer/audience support, role checking, and OBO token exchange in two lines of setup.
v0.3.10 β MCP tool security fix (Bug 10)
ProdMCP-secured MCP tools were raising ProdMCPSecurityError on every ADK/MCP call because __security_context__ was never injected for MCP protocol calls.
v0.3.9 β FastMCP lifespan fix (Bug 9)
Fixed RuntimeError: Task group is not initialized on startup.
v0.3.0 β Unified Framework Release
One framework for both REST and MCP. @app.common() for shared config. app.run() for the unified server.
