Architecting MCP Info Pages: Service Boundaries & Security Trade-offs
The Need for Root Info Pages
When deploying an MCP (Model Context Protocol) service, it's highly encouraged to serve an informational HTML page on the root GET / route. This seemingly simple requirement introduces complex architectural decisions regarding service boundaries, reverse proxy configurations, and internal security.
stdio or streamable-http). When a human operator or AI agent attempts to connect to a new MCP service, discovering the connection details, required parameters, and current status is essential.
Architectural Approaches & Trade-offs
Approach 1: Pure NGINX Static Serving
High PerformanceThe most performant and secure approach is to have NGINX serve a static HTML file at the root route, completely bypassing the backend Python application.
Advantages
- Zero Compute Overhead: Does not consume backend event-loop cycles.
- Absolute Isolation: An HTML route failure cannot crash the MCP service.
- Security: Impossible for the static route to access internal application state or secrets.
Limitations
- No Dynamic Context: The HTML cannot display real-time queue depths, verify API key validity, or auto-update when new tools are added.
- Deployment Friction: The static file must be pushed to NGINX directories, decoupling it from the Python application code.
Approach 2: The Dual-Port Architecture
Security ComplexA common pattern for production microservices is to bind two separate ports: one for the public-facing application (for MCP protocol traffic) and one for internal diagnostics (for Health, metrics, and HTML info).
The Complication: If the HTML info page lives on the diagnostics port, the public cannot view it. If you reverse-proxy the info route from the diagnostics port to the public port, you risk accidentally misconfiguring the proxy and exposing sensitive /diagnostics endpoints.
Approach 3: Embedding within the MCP Service
RecommendedThe chosen architecture for the image generation service involves injecting a custom HTML route directly into the underlying HTTP application created by the MCP framework, serving it on the same public port as the MCP protocol itself.
# Registering a custom route within the MCP service
async def root_info_handler(request: Request):
job_metrics = await client.job_manager.get_metrics()
# Inject metrics into HTML template...
return HTMLResponse(content=html)
mcp.custom_route("/", ["GET"], name="root_info")(root_info_handler)
Real-time Context
Dynamic injection of state (queue depths, API status) directly into the browser reduces debugging time.
Controlled Scope
Read-only access to high-level metrics without mutation capabilities keeps attack surface minimal.
Deployment Simplicity
HTML bundled with Python code in same container; single NGINX rule reduces complexity.
Architecture Comparison
| Approach | Performance | Security | Maintenance | Flexibility |
|---|---|---|---|---|
| Pure NGINX | Excellent | Excellent | Medium | Low |
| Dual-Port | Good | Complex | High | Medium |
| Embedded | Medium | Medium | Low | High |
Conclusion
System design is the art of acceptable compromise. While pure static proxying is technically superior from a security and performance standpoint, the operational reality is that visibility drives reliability.
By embedding a lightweight, dynamic info page directly into the MCP service scope, we provide immediate, actionable context to developers and AI agents without compromising the integrity of the core diagnostics boundary.
Key Takeaway
Choose architecture based on your specific needs: security isolation vs. operational visibility. For most MCP services, the embedded approach provides the best balance of simplicity and functionality.