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
app = ProdMCP("MyServer", version="1.0.0")
@app.tool(name="get_user", description="Fetch user by ID")
@app.get("/users/{user_id}")
def get_user(user_id: str) -> dict:
return {"name": "Alice", "email": "alice@example.com"}
if __name__ == "__main__":
app.run() # REST at / (Swagger at /docs) + MCP at /mcp/mcp
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:
# Forward the user's Azure AD token to the MCP server
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. ProdMCP's _mcp_secured_wrapper handles server-side token extraction transparently.
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 |
Sidebar — List Tools: Confirms the MCP server is reachable and tools are registered.
Chat box: Type a message — the ADK agent uses gemini-2.5-flash and invokes get_data via MCP when relevant.
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() - 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
License
MIT
Release Notes
See CHANGELOG.md for the full version history.
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 (e.g. AzureADTokenContext) — @functools.wraps was leaking __annotations__ from the original handler into _mcp_secured_wrapper, causing PydanticSchemaGenerationError.
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. Fixed via _mcp_secured_wrapper which extracts HTTP headers from FastMCP's Context object.
v0.3.9 — FastMCP lifespan fix (Bug 9)
Fixed RuntimeError: Task group is not initialized caused by FastAPI() being constructed before mcp_instance.http_app(), leaving the FastMCP session manager task group uninitialised.
v0.3.0 — Unified Framework Release
One framework for both REST and MCP. @app.common() for shared config. app.run() for the unified server.
Metadata
Release files for prodmcp 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| prodmcp-0.5.0.tar.gz | 66.4 kB | Without provenance | Details for prodmcp-0.5.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| prodmcp-0.5.0-py3-none-any.whl | Python 3 | none | any | Details for prodmcp-0.5.0-py3-none-any.whl |
Total release size: 136.6 kB
Release files / prodmcp-0.5.0.tar.gz
| Download URL | prodmcp-0.5.0.tar.gz |
|---|---|
| Size | 66.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4b7d463385f8da9b67f9b30ed066162fff5739c0a91beee6c1c8b386a7be3073
|
|
BLAKE2b-256 checksum How to use checksums |
42bdd6f1c29ba6f19d3c082010b2db9dbc800f5f0b326237150796958173b93f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|
Release files / prodmcp-0.5.0-py3-none-any.whl
| Download URL | prodmcp-0.5.0-py3-none-any.whl |
|---|---|
| Size | 70.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
404526c5a7319cc644e57feacbff883d22eb7f0377aed88159b025ecd216e3ef
|
|
BLAKE2b-256 checksum How to use checksums |
6f81202de18f688d37c329ee21eca14cadc43216974b5fce89e4fc31ef62c2c7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|
Security for prodmcp 0.5.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.