This guide stands up an MCP server from zero and turns AIDE's proto-mcp-server check from FAIL into PASS. Instead of conceptual prose we'll ship working code: ~80 lines of Python, a Caddy reverse-proxy block, a manifest JSON.
What AIDE actually checks
proto-mcp-server validates three things in order:
- Does
https://<domain>/.well-known/mcp.jsonreturn HTTP 200? - Does the JSON match the schema — required
transport,endpoint,authfields with correct types? - Does the
endpointURL respond to a JSON-RPC 2.0initializerequest with a spec-compliant payload?
All three pass → check passes. One missing → WARNING or FAIL. This guide solves them in that exact order.
Step 1 — Publish .well-known/mcp.json
The manifest tells the agent how to discover your server. AIDE reads this first, learning the URL and auth method.
{
"mcp_version": "2025-03-26",
"transport": "streamable_http",
"endpoint": "https://mcp.aide.tr/v1/rpc",
"name": "AIDE Scan",
"description": "AIDE scan engine — send a URL, receive an AI-readiness score.",
"auth": {
"type": "bearer",
"token_url": "https://aide.tr/account/api-keys",
"scopes": ["scan:create", "scan:read"]
},
"capabilities": {
"tools": true,
"resources": false,
"prompts": false
}
}
transport: "streamable_http" is what the 2025-Q2 spec recommends. Older stdio and sse transports aren't always supported by browser-based or cloud-hosted agents. The endpoint must be HTTPS — AIDE rejects HTTP responses.
Step 2 — FastMCP server
FastMCP is the fastest path on Python. You declare tools as decorators; FastMCP handles JSON-RPC 2.0 framing, capability negotiation, and error serialization for you.
# mcp_server.py
import os
from fastmcp import FastMCP, Context
from pydantic import BaseModel, Field
import httpx
mcp = FastMCP("AIDE Scan")
class ScanInput(BaseModel):
url: str = Field(..., description="URL to scan")
profile: str = Field(default="ai_ready", description="Scan profile")
@mcp.tool()
async def scan_site(payload: ScanInput, ctx: Context) -> dict:
"""Scan a site and return its score."""
api_key = ctx.request_context.headers.get("authorization", "").removeprefix("Bearer ")
if not api_key:
raise ValueError("AIDE API key required")
async with httpx.AsyncClient(timeout=60) as client:
resp = await client.post(
"https://api.aide.tr/v1/scans",
json={"url": payload.url, "profile": payload.profile},
headers={"Authorization": f"Bearer {api_key}"},
)
resp.raise_for_status()
return resp.json()
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=8001, path="/v1/rpc")
This server exports a single tool (scan_site). When Claude Desktop connects, it sees the tool in its catalog and can invoke it from user intent.
Step 3 — Reverse proxy + TLS
MCP clients (Claude Desktop, mcp-inspector, the OpenAI sandbox) expect HTTP/2 + TLS 1.3. The shortest Caddy block:
mcp.aide.tr {
encode gzip zstd
# MCP streamable_http transport uses SSE-like flushing —
# proxy buffering must be off, otherwise responses lag.
@rpc path /v1/rpc*
reverse_proxy @rpc localhost:8001 {
flush_interval -1
transport http {
read_timeout 30m
}
}
}
flush_interval -1 tells the proxy to stream the body without buffering. Without this AIDE's probe times out at 30 s and the check drops to WARNING.
Step 4 — initialize smoke test
Once the server is up, do exactly what AIDE will do:
curl -X POST https://mcp.aide.tr/v1/rpc \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $AIDE_API_KEY" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": {"name": "smoke-test", "version": "1.0"}
}
}'
Expected response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"capabilities": {"tools": {"listChanged": true}},
"serverInfo": {"name": "AIDE Scan", "version": "0.1.0"}
}
}
If protocolVersion differs from what you sent, that's not a mismatch — the server returned the highest version it supports. AIDE counts that as PASS.
Common mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| well-known returns 404 instead of 200 | AIDE WARNING with "manifest not found" | Serve .well-known/mcp.json from the domain root, not Next.js public/. Use Caddy handle_path or an Nginx location block. |
| Server returns 401 even when I declared auth: none | Middleware requiring Authorization header | Set auth: { "type": "none" } in the manifest and leave the path unauthenticated in your middleware |
| 406 Not Acceptable without Accept: text/event-stream | streamable_http uses SSE-style framing | Document the required Accept: application/json, text/event-stream header in your manifest |
| Cloudflare cache returns stale RPC responses | Second call sees a cached body | Add Cache-Control: no-store to RPC responses |
Production hardening
- Rate limiting: Per-API-key throttling, especially on expensive tools. Add
slowapiorfastapi-limiter. - Tool granularity: Replace one fat
scan_sitewithstart_scan+get_scan_resultso agents can parallelize. - Capability negotiation: When you add a new tool, emit a
notifications/tools/list_changedevent — Claude Desktop refreshes its tool list live. - Observability: Wire OpenTelemetry. The
mcp.tool.invocationscounter shows which tool is hot per minute.
Verifying with an AIDE scan
Once all three steps land:
curl -X POST https://api.aide.tr/v1/scans \
-H 'Content-Type: application/json' \
-d '{"url":"https://your-site.com","profile":"ai_ready"}'
The proto-mcp-server check should now report status: pass. If it's still WARNING, inspect the evidence field — it tells you which sub-step failed.
Related resources
- MCP Spec 2025-03-26
- FastMCP on GitHub
- Claude Desktop MCP setup
- AIDE check detail:
/learn/proto-mcp-server