Status: Accepted (Revised 2026-04-28 — consolidated dual REST + MCP exposition decision)
Date: 2024-12
Deciders: Architecture Team, Ricardo Cataldi
Supersedes: prior separate decision on Dual Exposition (REST + MCP Servers), now absorbed into this ADR
Apps must expose two types of APIs:
- REST endpoints: For traditional clients (web/mobile apps, other services)
- MCP servers: For agent-to-agent tool calling and Foundry integration
Apps must also support two client types:
- Traditional Clients: Web/mobile apps, external services (require REST)
- Agent Clients: Foundry agents, internal agent-to-agent calls (benefit from MCP)
Requirements:
- High throughput (10k+ req/s per service)
- Async I/O for parallel adapter calls
- OpenAPI docs auto-generation
- Native Python SDK support
- Both REST and MCP from every agent app
Use FastAPI for REST and fastapi-mcp for MCP server exposition. Expose both REST endpoints and MCP tool servers from every agent app.
- Implemented: FastAPI is the service framework in active apps, and agent services register MCP tools via
FastAPIMCPServerinholiday_peak_lib. - Partially diverged: The original wording implies uniform dual exposition; in practice, MCP is concentrated in agent services while some services (for example, CRUD) remain REST-only.
- No supersession: This ADR remains active for service exposition mechanics; ingress and edge policy changes are tracked separately in ADR-021.
- REST for Humans: Human-readable JSON, browser DevTools, Postman testing
- MCP for Agents: Automatic tool discovery, streaming support, Foundry native
- No Duplication: FastAPI-MCP wraps same business logic for both
| Client Type | Protocol | Use Case |
|---|---|---|
| Web UI | REST | Product detail page |
| Mobile App | REST | Cart updates |
| Agent (Foundry) | MCP | Multi-step reasoning |
| Agent-to-Agent | MCP | Inter-service tool calls |
| External Partner | REST | API integration |
from fastapi import FastAPI
from fastapi_mcp import MCPServer
app = FastAPI()
# REST endpoint
@app.get("/inventory/{sku}")
async def get_inventory(sku: str):
return await inventory_adapter.fetch(sku)
# MCP server
mcp = MCPServer(app)
@mcp.tool()
async def check_inventory(sku: str) -> dict:
return await inventory_adapter.fetch(sku)# Core logic
async def check_inventory(sku: str) -> InventoryStatus:
return await inventory_adapter.fetch(sku)
# REST endpoint
@app.get("/inventory/{sku}")
async def rest_check_inventory(sku: str):
return await check_inventory(sku)
# MCP tool
@mcp.tool()
async def mcp_check_inventory(sku: str) -> dict:
result = await check_inventory(sku)
return result.model_dump()- Performance: FastAPI is one of fastest Python frameworks (on par with Node.js)
- Async Native: Built on Starlette (async ASGI)
- Type Safety: Pydantic models enforce request/response contracts
- Auto Docs: OpenAPI/Swagger generated automatically
- MCP Support: fastapi-mcp provides zero-overhead MCP wrapping
- Flexibility: Support both traditional and agentic workflows
- Future-proof: MCP adoption without breaking REST clients
- Testing: REST clients easier to test manually
- Framework Lock-in: Replacing FastAPI requires rewriting routes
- Learning Curve: Teams unfamiliar with async/await need training
- Middleware Complexity: Custom middleware for auth/logging requires care
- Dual maintenance: Keep REST and MCP schemas in sync
- Confusion: Teams must choose right protocol
- Docs: Maintain OpenAPI + MCP schema
- Pros: Mature, large ecosystem
- Cons: Sync-only (requires gevent for async); slower than FastAPI
- Pros: Batteries-included (ORM, admin)
- Cons: Heavier than needed; async support incomplete
- Pros: Async-first, fast
- Cons: Smaller ecosystem; no MCP integration
- Pros: Efficient binary protocol
- Cons: No MCP support; requires .proto schemas; limited browser support
- Agent Interop: Foundry agents can call MCP tools natively
- Tool Discovery: MCP schema enables dynamic tool registration
- Backward Compat: REST clients unaffected by MCP layer
Tools registered via decorator:
@mcp.tool(
name="check_inventory",
description="Check product inventory levels",
parameters={
"sku": {"type": "string", "description": "Product SKU"}
}
)
async def check_inventory(sku: str) -> dict:
...| Feature | REST | MCP |
|---|---|---|
| Human-readable | ✅ JSON | ✅ JSON |
| Browser support | ✅ Yes | ❌ Agent clients only |
| Tool discovery | ❌ Manual | ✅ Automatic |
| Streaming | ✅ Native |
apps/<service>/src/
├── main.py # FastAPI app + MCP server
├── routers/ # REST route modules
│ ├── inventory.py
│ └── health.py
├── tools/ # MCP tool modules
│ ├── inventory.py
│ └── search.py
└── config.py # App settings
- Load config from environment
- Initialize adapters (inventory, pricing, etc.)
- Build memory tiers (Redis, Cosmos, Blob)
- Register REST routes
- Register MCP tools
- Start uvicorn server
- REST: Return HTTP status codes (400, 404, 500)
- MCP: Raise
MCPErrorwith error code + message
- REST:
httpx.AsyncClientfor integration tests - MCP:
mcp_clientlibrary for tool invocation tests
- ADR-001: Python 3.13 — Async language support
- ADR-005: Agent Framework — Agent consumption of dual APIs
This ADR consolidates the former Dual Exposition (REST + MCP Servers) decision.
- Sections "Dual Exposition Rationale", "When to Use Which Protocol", and "Dual Exposition Pattern" were absorbed from that decision.
- The prior dual-exposition ADR is now superseded and redirects here.