MCP vs A2A Protocol: Vertical Tools, Horizontal Agents
MCP is the vertical connection: it gives one agent more capability, each piece with a schema and a result. A2A is the horizontal one: it lets an agent hand a task to another agent it does not own, using a published card for discovery and a stateful task for the work. A gateway sits on the vertical side, because however the work is delegated, the bill is paid in tool calls.
Short answer: MCP is the vertical connection: it gives one agent more capability, each piece with a schema and a result. A2A is the horizontal one: it lets an agent hand a task to another agent it does not own, using a published card for discovery and a stateful task for the work. A gateway sits on the vertical side, because however the work is delegated, the bill is paid in tool calls.
Key takeaways
- Vertical and horizontal, not better and worse. MCP deepens a single agent; A2A reaches across a boundary that agent does not control.
- A tool call and a task are different sizes. One request and one result against a lifecycle that can run for hours, change state, and ask a question back.
- A2A peers are discovered, not configured. The Agent Card is a public document at a well-known path; the tools behind a peer are never visible to you.
- Two control planes. Meter capability access per call; authorize delegation per task, and observe it rather than compute it.
- Delegation needs local memory. A task that resumes later has to find its own state, and a remote agent will never be able to read your store.
- Next: classify your own calls as capability access or delegation, then put limits and audit on the first kind before you add the second.
The two boundaries, and why the direction matters
The official comparison draws the split by direction rather than by feature, and that framing survives contact with a real system better than a feature table (A2A and MCP). MCP is vertical: every server you attach hands the same agent one more tool, one more data source, one more skill, and the agent gets deeper. A2A is horizontal: it connects agents that sit on opposite sides of a team, a vendor, or an organisational boundary, so that one can ask another to do work.
The direction decides who owns what. A vertical connection is yours end to end — you chose the server, you can read its code, you can put a limiter in front of it. A horizontal connection is owned by somebody else: the peer agent reasons with its own model, calls its own tools, keeps its own state, and returns a result rather than a trace. That asymmetry is the whole reason the two protocols need different governance: you meter what you own and you authorize what you do not.
The demand for the comparison is not invented by the industry. The head phrase here carries
about 3,600 US searches a month, the direct comparison mcp vs a2a about 480, the protocol's
own name agent2agent about 320, and the phrases that describe the work the protocols do —
agentic rag at 1,900, rag architecture at 1,600, agent memory at 1,300 — are measured in
the same pass. Engineers are asking where the line is, and the honest answer is that it is a
boundary rather than a ranking.
agent2agent: what the A2A protocol actually carries
A2A is an open standard for communication between independent agents, and its current release is documented as 1.0.0, with 0.3.0, 0.2.6 and 0.1.0 still published alongside it (specification). Five objects carry the whole interaction model, and each one answers a question MCP never has to ask.
A client agent sends a message to a remote agent; the message has a role and one or more parts, and a part is text, a file reference, or structured data. The remote agent may answer with a message directly for a simple exchange, or create a task: a stateful unit of work with its own identifier and a lifecycle that runs through submitted, working, input-required, auth-required, completed, failed, canceled or rejected. Output that is not a reply is an artifact, itself composed of parts. A context identifier groups related tasks so a later task can be read as a continuation rather than a stranger.
Discovery is the part that has no MCP equivalent. A remote agent must publish an Agent Card
— a JSON document describing its identity, capabilities, skills, service endpoint and
authentication requirements — and the well-known location for it is
https://{server}/.well-known/agent-card.json, with registries and direct configuration as the
other two ways a client can find one. Cards can be signed, and a peer can require a caller to
fetch an extended, authenticated card before it sees the full skill list.
Updates arrive through one of three mechanisms, and choosing between them is a design decision rather than a preference. A client can poll by reading the task; it can hold a stream, which the card's capabilities must declare; or it can register a webhook and let the remote agent push status and artifact updates to it, which the card must also declare and which are ordinary HTTP POSTs regardless of which binding the peer speaks. The protocol versions each binding separately — JSON-RPC, gRPC, and HTTP with JSON bodies. What none of it provides is a tool schema, a token count, or a rate limit: those stay on your side of the boundary, which is the subject of the rest of this page.
mcp vs a2a: the surface each server publishes
A request log is where the difference becomes visible without reading a specification. The MCP side publishes one endpoint and mounts the protocol application on it:
# backend/smartgate/api/mcp.py — source lines 399–406 (mount_mcp_routes)
def mount_mcp_routes(app: FastAPI) -> None:
"""Expose POST /mcp (Streamable HTTP, stateless)."""
apply_mcp_session_compat()
streamable_app = mcp.streamable_http_app()
streamable_app.router.lifespan_context = _noop_starlette_lifespan(streamable_app)
app.mount("/mcp", streamable_app)
logger.info("MCP Streamable HTTP at POST /mcp")
Eight lines, and the docstring is the design: a single POST path carrying Streamable HTTP, with
no session to manage. There is nothing to discover first and nothing to fetch before the first
call, because the capability list arrives as a response to tools/list rather than as a
document you cache. An A2A server publishes the opposite way round: the card is public and
stable, the tasks are private and stateful, and the interesting part of a call is not the
request but the identifier you get back. Two consequences for an operator. In a gateway log, an
MCP call is complete when the response is written; an A2A exchange ends when the task reaches a
terminal state, possibly hours later and possibly after asking you a question. And the two
surfaces fail differently: an unreachable MCP endpoint fails immediately and loudly, while an
A2A peer can accept a task, go quiet, and leave you relying on the delivery mechanism you
declared. If the transports themselves are the open question, they are covered one revision at
a time in the
MCP protocol versions and transports.
langchain mcp: the declaration a framework reads at runtime
A framework that speaks MCP does not need your documentation — it needs your declaration, and a declaration is only useful when the defaults and the descriptions are in it:
# backend/smartgate/api/mcp.py — source lines 310–331 (smart_pipe)
async def smart_pipe(
template: str = Field(
default="",
description="Built-in template: research, read, or remember. Omit when using steps.",
),
steps: list[dict[str, Any]] | None = Field(
default=None,
description="Custom steps; each step has tool and params (legacy alias: args).",
),
query: str = Field(
default="",
description="Search query for research/remember templates (wired to search step).",
),
url: str = Field(
default="",
description="URL for read/research templates (wired to fetch when search is skipped).",
),
text: str = Field(
default="",
description="Optional text seed for context_gate when not produced by a prior fetch.",
),
) -> str:
This is the declaration surface for the orchestrator tool, and every field in it is read by
something other than a human. The parameter names become properties in the schema the framework
validates against; the descriptions become the text a model reads when deciding whether to call
it; the defaults become the values used when a caller omits the field. A description that says
Built-in template: research, read, or remember is doing real work, because it is the only
place a model learns what the argument may contain — and a parameter with a default that the
caller must nevertheless supply is the most common way a tool list misleads a framework. That is
why the declaration is worth reading as a contract rather than as code: the framework projects
it into its own function registry, and the projection is only as good as these few lines. The
schema layer on the other side of the same boundary is documented in the
MCP tools reference.
agent2agent orchestration you own, and the kind you do not
Before a system delegates work to another agent, it usually runs a loop of its own, and that loop is the honest baseline for what delegation changes:
# backend/smartgate/core/pipeline.py — source lines 341–360 (PipelineEngine.run)
async def run(
self,
ctx: ToolContext,
template: str = "",
steps: List[Dict] = None,
inputs: Optional[Dict[str, Any]] = None,
) -> Dict:
pipeline_ctx = PipelineContext()
ctx = ToolContext(pipeline_context=pipeline_ctx)
run_inputs = inputs or {}
results = []
if template and template in PIPELINE_TEMPLATES:
steps = PIPELINE_TEMPLATES[template]
if not steps:
return {"error": "No steps or template provided", "results": [], "pipeline_steps": []}
offset_ms = 0.0
pipeline_steps: List[Dict[str, Any]] = []
# backend/smartgate/core/pipeline.py — source lines 377–393 (PipelineEngine.run)
if params.get("from_step"):
prev_result = pipeline_ctx.get(params["from_step"])
if prev_result:
params["_pipeline_input"] = prev_result
if module_name == "search" and template == "research":
result, elapsed_ms = await self._run_search_with_research_fallback(
ctx, module, params, template,
)
elif module_name == "fetch":
result, elapsed_ms = await self._run_fetch_with_fallback(
ctx, module, params, pipeline_ctx,
)
else:
t0 = time.perf_counter()
result = await module.process(ctx, **params)
elapsed_ms = (time.perf_counter() - t0) * 1000.0
Two excerpts from one method. The first resolves what to run: an explicit step list wins, and a named template is expanded into steps only when it is a template the engine knows. The second is the wiring that makes a chain a chain — a step may name an earlier step, and that step's result is handed forward as its input — followed by the dispatch that sends each step to a module by name, with the retry-shaped paths reserved for the two tools that need them. Read it as the boundary marker: every step here is code you can read, every module is one you registered, and a failed step ends the loop in your own process. Delegation moves that loop to somebody else's process. You keep the task identifier and the delivery mechanism, you lose the step list, and the only artefact of the reasoning is whatever the peer chooses to return. That is not a defect of A2A; it is what horizontal means, and it is why the governance you can keep is the governance of the calls your own side still makes.
agentic rag: the write side a delegated task can never read
Retrieval is where an agent's working memory lives, and the write side shows what that memory actually is:
# backend/smartgate/core/resources.py — source lines 125–139 (store_vectors)
async def store_vectors(self, collection: str, vectors, payloads, ids=None) -> int:
from qdrant_client.models import PointStruct
import uuid
points = []
for i, (vec, pay) in enumerate(zip(vectors, payloads)):
points.append(PointStruct(
id=ids[i] if ids else str(uuid.uuid4()),
vector=vec,
payload=pay,
))
result = await self._client.upsert(
collection_name=collection,
points=points,
)
return len(points)
The shape is deliberately unromantic: build one point per vector-and-payload pair, reuse the caller's identifier when one was supplied and generate one otherwise, upsert the batch, and return how many points were written. Three properties matter to anyone delegating work across a boundary. The upsert is idempotent per identifier, so a retried write does not duplicate a document. Identifiers are caller-supplied when the caller has a stable key, which is what makes a later task able to find what an earlier one stored. And the count that comes back is the only confirmation of persistence — a write that returns the expected number is a write that landed. Now the delegation consequence: a peer agent you hand a task to cannot read this collection. If the task depends on what your agent already knows, the knowledge has to travel in the message, as parts of text, file or structured data — which is also why an agentic RAG step that runs inside your own boundary is cheaper than the same step run by a peer that has to be told everything from scratch. The pattern itself, as an application of MCP rather than a protocol feature, is walked through in agent memory architecture.
rag architecture: the query side, where top-k is a policy
The read side is shorter, and it is where a retrieval design becomes a decision about money and latency:
# backend/smartgate/core/resources.py — source lines 141–149 (search_vectors)
async def search_vectors(self, collection: str, query_vector, top_k: int = 10, filters=None):
from qdrant_client.models import Filter as QFilter
result = await self._client.search(
collection_name=collection,
query_vector=query_vector,
limit=top_k,
query_filter=filters,
)
return result
Six lines that fix the contract: a collection, a query vector, a limit, and an optional filter.
Two of those four are governance rather than plumbing. top_k is how much context a tool call
will return, and context is what the next model call will be billed for, so a default that
looks like a convenience is really a spend decision — the same decision a budget guard exists to
bound. The filter is the other one: passing it through to the store keeps scoping inside the
query, where it cannot be forgotten by a caller that forgets to post-process, which is the
difference between a memory search that is tenant-safe by construction and one that is
tenant-safe by convention. Nothing here is visible to a peer agent over A2A, and that is the
point worth keeping: the horizontal boundary protects your data, and it also means every piece
of context a delegated task needs has to be sent deliberately rather than looked up.
agent memory: rebuilding an index from vectors and items
A memory that outlives a process has to be reconstructible, so the constructor is part of the design rather than an implementation detail:
# backend/smartgate/modules/dedup/index.py — source lines 31–48 (from_vectors_and_items)
@classmethod
def from_vectors_and_items(
cls, vectors: np.ndarray, items: list[DictItem], backend_type: Backend | str, **kwargs: Any
) -> Index:
"""
Load the index from vectors and items.
:param vectors: The vectors of the items.
:param items: The items in the index.
:param backend_type: The type of backend to use.
:param **kwargs: Additional arguments to pass to the backend.
:return: The index.
"""
backend_class = get_backend_class(backend_type)
arguments = backend_class.argument_class(**kwargs)
backend = backend_class.from_vectors(vectors, **arguments.dict())
return cls(vectors, items, backend)
Four lines and a docstring that states the contract: vectors, items, a backend type, and extra arguments for that backend. The interesting line is the one that builds the backend from the vectors first and only then constructs the index around it — which means the same object can be rebuilt from persisted state instead of being carried in memory. For a system that delegates, that is the difference between memory and a cache. A task you hand to a peer may come back tomorrow, in a new process, after a deploy; the state that task resumes from has to be reconstructible from what was written down, and a backend that is selected by type rather than imported by name is what lets the same index run against a local implementation in development and a hosted one in production. The parameter documentation is also worth reading literally: it is the only description of what the extra arguments do, and a backend whose arguments are undocumented is a backend nobody will dare to change.
ai agent security: who may hand work to whom
Every delegation decision starts with a permission question, and it answers the same way on either protocol: identity first, then rank:
# lib/authz/team.ts — source lines 27–52 (requireTeamRole)
async function requireTeamRole(
teamId: string,
minRole: TeamAuthRole,
) {
const user = await getCurrentUser();
if (!user?.id) {
throw new TeamAuthError("Not authenticated", 401);
}
const member = await prisma.teamMember.findFirst({
where: { teamId, userId: user.id },
select: { role: true },
});
if (!member) {
throw new TeamAuthError("Not a team member", 403);
}
const memberRank = ROLE_RANK[member.role] ?? 0;
const requiredRank = ROLE_RANK[minRole] ?? 0;
if (memberRank < requiredRank) {
throw new TeamAuthError("Insufficient team role", 403);
}
return { userId: user.id, role: member.role };
}
The function is small and every branch is a refusal. No authenticated user, no membership in the team, or a role ranked below the one required — three failures with three different meanings, and the first two are answered with distinct status codes so a client can tell "you are not signed in" from "you are signed in and not allowed". The rank comparison is the part worth copying for a delegation design. Roles are compared by rank rather than by equality, so a new role can be inserted between two existing ones without rewriting every check, and the shapes that fall outside the table default to zero — which means an unknown role fails closed instead of inheriting a permission nobody intended to grant. Applied to the horizontal boundary, the same three questions become: may this agent delegate at all, may it delegate to that peer, and may that peer call back into us. The third one is the question teams forget, and it is the one a webhook makes concrete. Key scoping and the OAuth side of identity are covered in MCP OAuth and auth.
openclaw config: the capability behind whatever host is configured
Host configuration is where a delegation story usually starts, because the tool a host can reach is the tool an agent will use:
# backend/smartgate/api/mcp.py — source lines 109–126 (smart_fetch)
@server.tool(
name="smart_fetch",
description=TOOL_DESCRIPTIONS["smart_fetch"],
annotations=tool_annotations("smart_fetch"),
)
async def smart_fetch(
url: str = Field(description="Full HTTP or HTTPS URL to fetch."),
timeout: int = Field(default=30, description="HTTP timeout in seconds."),
) -> str:
_, registry = _app_state()
module = registry.get("fetch")
ctx = _tool_ctx()
return await _run_with_audit(
"fetch",
ctx,
module.process(ctx, url=url, timeout=timeout),
{"url": url},
)
Every line of that excerpt is a decision about the caller's experience rather than about the network. A name, a description pulled from a shared table so it cannot drift from the documentation, annotations derived from the tool name so a host knows whether to ask for confirmation, and required parameters declared with their descriptions. The body then does what every tool in this registry does: resolve the module, build a call context, run the coroutine, and return through the audited path with the arguments that should be recorded. The detail that matters for this page is who ends up calling it. Whether the host is Claude Desktop configured by hand, Cursor with a pasted JSON block, or an OpenClaw agent that added the server with one command, the call arrives at the same endpoint as one tool call under one key — which is exactly the unit a gateway can meter, cap and record. Delegation does not change that. A peer agent that runs its own tools is governed by its owner's controls; the calls that reach your endpoint are still yours to bound, whichever host or agent they came from.
build mcp server-side reach before you delegate
Search is the capability that gets delegated to most often, and it is worth having on your own side first:
# backend/smartgate/api/mcp.py — source lines 128–148 (smart_search)
@server.tool(
name="smart_search",
description=TOOL_DESCRIPTIONS["smart_search"],
annotations=tool_annotations("smart_search"),
)
async def smart_search(
query: str = Field(description="Search query string."),
max_results: int = Field(
default=10,
description="Maximum number of results to return (1–50).",
),
) -> str:
_, registry = _app_state()
module = registry.get("search")
ctx = _tool_ctx()
return await _run_with_audit(
"search",
ctx,
module.process(ctx, query=query, max_results=max_results),
{"query": query},
)
The pattern is the same as everywhere else in the registry — declaration, module lookup, call context, audited return — and it is the return that carries the claim. Because the call leaves through one shared function, a missing audit row means the request never reached the gateway rather than that this particular tool forgot to log, and the recorded arguments name the query that produced it. Two practical consequences when an agent is deciding whether to call a peer or a tool. First, a tool you own comes with an audit row and a cap; a peer agent comes with a task identifier and somebody else's retention policy. Second, capability duplication is the cheapest defence against unnecessary delegation: the teams that delegate least are usually the ones whose own server already answers the questions their agents ask. If you are standing that server up for the first time, the endpoint, the registry and the session questions are all in the MCP server page.
mcp oauth inbound, signatures outbound
Authorization is directional, and the two directions use different machinery. Inbound, MCP defines an optional authorization story for HTTP transports in which the server acts as an OAuth 2.1 resource server, so a client presents a token the way it would to any other protected API (authorization). Outbound, a peer that wants to push task updates to your webhook needs the mirror image: a payload you can prove came from them.
# lib/webhooks/index.ts — source lines 90–96 (signPayload)
function signPayload(payload: string, secret: string): string {
const timestamp = Math.floor(Date.now() / 1000);
const signature = createHmac("sha256", secret)
.update(`${timestamp}.${payload}`)
.digest("hex");
return `t=${timestamp},v1=${signature}`;
}
The excerpt is the pattern rather than the whole policy. A timestamp is taken in seconds, the signature covers the timestamp and the payload together, and the header that carries it announces both the scheme and the time. Two design properties are worth naming, because a naive signature has neither. Signing the timestamp alongside the body is what makes a captured payload expire: a receiver that checks the timestamp against a small window rejects a replay that would otherwise be a valid signature forever. And announcing the version in the header is what lets the scheme change without breaking the peers already deployed — a receiver reads the version it was given and verifies accordingly. A2A delivers its push notifications as plain HTTP requests, which means this is exactly the layer that has to hold: the transport gives you delivery, not provenance, and provenance is a signature you verify before you act on the payload.
llm guardrails: the part neither protocol decides
It is worth being blunt about what both protocols leave out, because the gap is where teams assume a guarantee that does not exist. Neither MCP nor A2A is a content policy. MCP describes capabilities, their schemas and their results; A2A describes tasks, their states and their artifacts. Neither one inspects a prompt for a forbidden subject, and neither one decides that a result should be withheld, because both of them are protocols about transport and structure rather than about meaning. Guardrails therefore live one layer up, in three places at once. Before a call, in what a capability is allowed to receive — the tool allowlist and the key's scope, which is what the annotations merely hint at. During a call, in what gets bounded — tokens, spend, request rate — so that a loop cannot convert a mistake into an invoice. And after a call, in what gets written down: values masked before they reach a log, and arguments recorded in a form a reviewer can act on. Delegation adds a fourth case with no local answer at all: once a task crosses the horizontal boundary, the peer's reasoning is opaque, so the controls that survive are the ones attached to the boundary itself — who may delegate, to whom, how deep, and what the result is allowed to trigger next.
When to use which, and where the gateway sits
The decision is mechanical once the direction is clear, and the table below is the version worth keeping next to an architecture diagram.
| Question | MCP | A2A |
|---|---|---|
| What it connects | One agent to a capability | One agent to another agent |
| Direction | Vertical: more depth for the same agent | Horizontal: reach across a boundary |
| Unit of work | A request with a schema and a result | A stateful task with a lifecycle |
| Peer identity | A server and a key | Another agent with its own identity and card |
| Discovery | A runtime list, read as a response | A published card at a well-known path |
| Update model | One response per call | Polling, streaming, or webhooks |
| Where you can enforce | Rate, budget, scope and audit per call | Authorization per task; observation of state |
| What it costs | Tokens per call, measurable exactly | Tokens you never see, plus your own calls |
Read the last two rows together and the placement question answers itself. A gateway belongs where the measurable unit is, which is the tool call — and in a system that uses both protocols, that single endpoint sees every call both sides make. Put limits and audit on the vertical connection first, because that is the connection you own; then decide how much horizontal reach you actually need, because every delegated task spends tokens you cannot count. The policy layer that does the capping is described in MCP gateway, and the protocol stack around both of them, from the handshake to the revision, is in Model Context Protocol explained.
How that placement works out against an orchestration framework — LangGraph, CrewAI, a hand-rolled loop, or a gateway in front of them — is compared in AI Agent Architecture.
How to get started
- Classify your own calls first. Write down which of your endpoints are capability access and which are delegation. The first list is what a gateway can meter today; the second is what needs an authorization rule and an owner.
- Point one host at the endpoint and read
tools/list. The declaration surface is the contract your agent acts on, and the generated per-host blocks are on the connect page. Seven tools come back. - Put a limit on the calls you own. Set a per-key rate and a daily cap, then trigger a tool and find its row in the logs view. A row that disagrees with the response is a server bug; a missing row with a successful response is an identity question.
- Add the peer boundary deliberately. When you do connect a remote agent, decide the webhook direction before the request direction: who verifies the signature, how long a timestamp is accepted, and which states are allowed to trigger your next call.
- Read the neighbours for the halves this page leaves out. Retention and tracing are in MCP logging and observability, the tool schemas are catalogued in MCP tools reference, and the server side of the vertical connection is in MCP server.
Start on the free tier — 2M tokens a month, all seven tools, 120 MCP requests a minute per key and seven days of logs — then compare the limits and retention windows on the pricing page.
Frequently Asked Questions
Limitations and what this does not do
- This is a comparison, not a mapping layer. Nothing here translates one protocol into the other; a system that needs both speaks both.
- A2A is a moving specification. Version numbers, card fields and binding details are defined by the specification, which is the version to trust over any summary of it.
- The excerpts are windows, not whole files. The pipeline and orchestrator excerpts show the step wiring and the declaration surface; the branches around them are described rather than quoted.
- Local memory is not shared memory. The vector and index excerpts show your own store; no amount of protocol design makes a peer able to read it.
- Delegation cannot be capped from the outside. You can bound what your side sends and accepts; the peer's own spending is its owner's problem, and the honest design says so.
- Neither protocol is a security boundary on its own. Authentication, scoping and verification are layers you configure — the protocols define where they attach, not that they are switched on.
Sources
- Agent2Agent (A2A) Protocol — specification, 1.0.0 with previous revisions: https://a2a-protocol.org/latest/specification/
- A2A — what the protocol is for, and how it relates to MCP: https://a2a-protocol.org/latest/topics/what-is-a2a/
- A2A and MCP — the vertical and horizontal comparison: https://a2a-protocol.org/latest/topics/a2a-and-mcp/
- Model Context Protocol — specification (2026-07-28): https://modelcontextprotocol.io/specification/2026-07-28
- Model Context Protocol — authorization, the OAuth 2.1 resource-server role: https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization
- Model Context Protocol — server tools (
tools/list,tools/call): https://modelcontextprotocol.io/specification/2026-07-28/server/tools - Anthropic — introducing the Model Context Protocol: https://www.anthropic.com/news/model-context-protocol
- SmartGate — docs, connect, logs and pricing: https://smartgate.network/docs · https://smartgate.network/docs/connect · https://smartgate.network/docs/logs · https://smartgate.network/pricing
Method note
The code in this article is not transcribed. Each block was cut directly out of the slice body
returned by the SmartGate slice API and re-asserted byte-for-byte as a substring of that body
before publication; the first line inside every fence records the file and the exact source
lines. Symbols were pinned by whole-name containment (rule A level 2) and confirmed by the
service's slot-proof endpoint before any prose was written. Eight excerpts are whole slice
bodies. Two are windows into longer files: the pipeline excerpt keeps the step-input wiring and
the dispatch of a long orchestration method, dropping the fallback helpers above it, and the
orchestrator excerpt keeps the tool's declaration surface, dropping the decorator above it and
the handler body below it. Sections 12:1 (llm guardrails) and 2:1 (agent guardrails)
returned several equally plausible symbols and were recorded as abstentions; they are written
from the public specifications and quote no code.
Demand figures come from this project's own keyword run, recorded in research_brief.md and
search_volume.json: the head phrase a2a protocol at 3,600 US searches a month, and the
phrases that pinned these sections — agentic rag 1,900, rag architecture 1,600, agent memory 1,300, langchain mcp 480, mcp vs a2a 480, ai agent security 480, openclaw config 390, agent2agent 320, build mcp server 320, mcp oauth 320, llm guardrails 320
and agent guardrails 70.
Slice provenance
| # | SERP keyword | Symbol | File | Source lines | How it was pinned | sha256(12) |
|---|---|---|---|---|---|---|
| 1 | agent2agent | PipelineEngine |
backend/smartgate/core/pipeline.py |
341–360, 377–393 | rule A L2 → slot-proof | 5f7a984d6b6d |
| 2 | agentic rag | store_vectors |
backend/smartgate/core/resources.py |
125–139 | rule A L2 → slot-proof | d44a629fc212 |
| 3 | rag architecture | search_vectors |
backend/smartgate/core/resources.py |
141–149 | rule A L2 → slot-proof | 0e5cc4aeeb5b |
| 4 | agent memory | from_vectors_and_items |
backend/smartgate/modules/dedup/index.py |
31–48 | rule A L2 → slot-proof | 8caa85a8f912 |
| 5 | langchain mcp | register_mcp_tools |
backend/smartgate/api/mcp.py |
310–331 | rule A L2 → slot-proof | 9d4a1623b28c |
| 6 | mcp vs a2a | mount_mcp_routes |
backend/smartgate/api/mcp.py |
399–406 | rule A L2 → slot-proof | e66f6b69a172 |
| 7 | ai agent security | requireTeamRole |
lib/authz/team.ts |
27–52 | rule A L2 → slot-proof | 08de920ed687 |
| 8 | openclaw config | smart_fetch |
backend/smartgate/api/mcp.py |
109–126 | rule A L2 → slot-proof | fdc9a4259783 |
| 9 | build mcp server | smart_search |
backend/smartgate/api/mcp.py |
128–148 | rule A L2 → slot-proof | 12366ba0d241 |
| 10 | mcp oauth | signPayload |
lib/webhooks/index.ts |
90–96 | rule A L2 → slot-proof | 3c76a4329aea |
Every fenced block above was cut from the slice body and re-asserted against it byte-for-byte before publication. 10 of 12 sections pinned, 2 abstentions, 0 misses.