One decorator should feed all three protocols
Why we think REST, MCP, and A2A schema parity is non-negotiable for the next generation of agent platforms — and what breaks when you try to maintain three separate definitions.
Most teams I talk to who have adopted MCP have the same bug in their repo right now: their MCP tool definition has drifted from their REST endpoint. Sometimes it’s a missing field. Sometimes it’s a different type. Sometimes it’s an entirely different set of arguments. Always, it’s a source of subtle, hard-to-diagnose bugs where Claude calls a tool with inputs that look right but aren’t.
This is a solvable problem. I want to explain why FastAgentic solves it structurally, not by convention, and why I think the next generation of agent platforms will all look like this.
The three protocols that matter
The agent platform landscape in 2026 has three protocol surfaces that every serious service needs to speak:
- REST, because every client, load balancer, and IDP on earth understands it.
- MCP (Model Context Protocol), because Claude, Cursor, Zed, Continue, and every other IDE-integrated assistant discovers and calls tools over it.
- A2A (Agent-to-Agent), because agents are starting to call other agents directly and need a discovery/negotiation protocol to do it safely.
Each of these has a schema. Each schema has input types, output types, error types, and metadata. For any given capability, those three schemas have to describe the same thing. The moment they don’t, one of your clients is calling the wrong shape and you have a production bug.
The naive approach: three files
The first approach most teams take is to write each schema by hand:
# openapi_schemas.py
class ResearchInput(BaseModel):
query: str
limit: int = 10
# mcp_tools.py
RESEARCH_TOOL = {
"name": "research",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string"},
"max_results": {"type": "integer", "default": 10}, # DRIFT
},
},
}
# a2a_skills.py
research_skill = Skill(
name="research",
parameters={"query": "string", "count": "number"}, # MORE DRIFT
)
This is bad for the obvious reason (manual duplication) and worse for the subtle reason — code review culture rarely catches schema drift because the three files live in different directories and get edited in different PRs. By week six, you have two fields that are called limit in REST, max_results in MCP, and count in A2A. Your frontend works. Your Cursor integration half-works. Your agent-to-agent calls silently pass wrong values.
The slightly better approach: one file, manual fan-out
The next approach is to have a “source of truth” Pydantic model and manually regenerate the MCP and A2A schemas from it:
class ResearchInput(BaseModel):
query: str
limit: int = 10
# Run at startup:
MCP_TOOLS = [pydantic_to_mcp_tool(ResearchInput, handler=research)]
A2A_SKILLS = [pydantic_to_a2a_skill(ResearchInput, handler=research)]
This is better — at least there’s one definition — but still fragile. The converter functions are hand-written. They drift. Error types, defaults, streaming semantics, and auth requirements have to be replicated in three places. And critically, the handler function exists once but is registered three times, each registration parameterized differently.
The right approach: one decorator, three surfaces
What you actually want is a decorator that takes a single handler, infers the schema from its signature, and emits all three protocol surfaces as a byproduct. That’s what FastAgentic’s @agent_endpoint does:
@agent_endpoint("/research")
async def research(query: str, limit: int = 10) -> ResearchResult:
"""Search internal research papers."""
...
This gives you:
- A REST route at
POST /researchwith JSON body{query, limit}and typed response. - An MCP tool named
researchwith input schema{query: string, limit: integer}and the same output schema. - An A2A skill named
researchadvertised at/.well-known/agent.jsonwith the same shape.
The schemas cannot drift. They are generated from the same inspect.signature() call. Adding a parameter adds it to all three. Renaming a parameter renames it in all three. Changing a type changes it in all three. There is no second place to update, by construction.
What this unlocks
Schema parity isn’t just a bug fix. It’s what makes the following things possible:
- Cross-protocol testing. Write one contract test, run it against all three surfaces.
- Single documentation. OpenAPI spec, MCP tool catalog, and A2A skill directory are all generated from the same source.
- Protocol-aware clients. Clients that speak two protocols (e.g., a chat UI that uses REST and an agent orchestrator that uses A2A) can share types.
- Typed end-to-end. Python generic types propagate from handler to all three protocol layers, so you catch schema mistakes at import time, not runtime.
Where the industry is headed
Every agent platform I see seriously competing in 2026 will converge on this pattern. A2A is still early. MCP is ~18 months old. The hand-written-adapter era is ending already, mostly because teams got bitten and decided drift wasn’t acceptable.
The question is whether you write the generator yourself or adopt a runtime that already has it. We think “already has it” is the obvious answer — but even if you don’t pick FastAgentic, please, at least don’t hand-maintain three schema files. That path ends in a 3am debugging session where Claude is passing “count: 10” to a tool that expects “limit: 10” and silently returning nonsense.
The tl;dr
Protocol surfaces should be derived, not authored. One decorator, three protocols. If your runtime doesn’t enforce this, you will eventually have a drift bug. Plan accordingly.
Need FastAPI, LangGraph, or agent platform expertise?
Neul Labs — the team behind FastAgentic — takes on a limited number of consulting engagements each quarter. We help teams ship agents to production, fix broken LangGraph pipelines, and design governance for multi-tenant LLM platforms.