Upgrading to .NET 10 LTS: Why This Is a Forced-Decision Year

If you run anything on .NET 8 or .NET 9, there’s a single date that turns “we should upgrade eventually” into “we have to upgrade this year”: November 10, 2026. On that day, both .NET 8 and .NET 9 reach end of support — simultaneously. After it, Microsoft stops shipping servicing updates, security fixes, and technical support for either version.

That simultaneity is not a typo, and it’s the crux of this series. Ordinarily you’d expect the newer release to outlive the older one. Here they converge, because of a deliberate policy change: Microsoft extended Standard Term Support from 18 to 24 months. .NET 8, a Long Term Support release from November 2023, gets its full 36 months and lands on November 10, 2026. .NET 9, a Standard Term Support release from November 2024, now gets 24 months instead of 18 — and lands on exactly the same day.

A timeline graph illustrating support periods for .NET versions 8, 9, and 10, highlighting the end of support for both .NET 8 and .NET 9 on November 10, 2026, and showing the support duration for .NET 10 until November 10, 2028.

The consequence is a genuine forcing function. It doesn’t matter which strategy you picked — the conservative shop that stayed on .NET 8 LTS and the fast-moving shop that jumped to .NET 9 are now in exactly the same boat, with the same deadline and the same target: .NET 10. Every .NET application in your portfolio needs to be on .NET 10 by November 2026, and the runway is shorter than it looks once you account for testing, third-party dependencies, and change-freeze windows.

Why “unsupported” is a real problem, not a nag

It’s tempting to treat end-of-support as a soft recommendation. It isn’t, for three concrete reasons:

  • No more security patches. After November 10, 2026, a newly disclosed CVE in the runtime or base libraries simply won’t be fixed for .NET 8 or .NET 9. You’d be running a known-vulnerable runtime with no official remedy — Microsoft’s own guidance is blunt that using out-of-support software puts your applications, data, and environment at risk.
  • Compliance failure. For regulated shops — financial services, healthcare, government contractors — running an unsupported runtime is often an automatic audit finding. “We’re planning to upgrade” doesn’t satisfy an auditor looking at a production system on an EOL framework.
  • Emergency-patching spiral. Teams that miss the deadline don’t escape the work; they just do it later, under pressure, during an incident. That’s the most expensive possible time to migrate.

There’s one release-valve worth knowing about: third-party extended-support vendors will sell you post-EOL security patches for .NET 8/9 to buy time for a genuinely un-migratable app. Treat that as a bridge for the exceptional case, not a strategy — it’s a way to stop the security clock on a mission-critical system while you finish the real migration, not a substitute for it.

How the cadence works — so this never surprises you again

Understanding the release model turns the deadline from a shock into a predictable planning input. .NET ships a new major version every November, and the parity of the version number tells you the support length:

Infographic illustrating the .NET release cadence with release dates and support durations for .NET 8 to .NET 12, highlighting LTS and STS versions.

Even-numbered releases are LTS (three years of support): .NET 8, .NET 10, .NET 12. Odd-numbered releases are STS (now two years): .NET 9, .NET 11. Crucially, Microsoft is explicit that the quality of LTS and STS releases is identical — the only difference is the length of the free support window. So the strategic question isn’t “is LTS more stable?” (it isn’t) but “how often do I want to be forced to upgrade?” For most enterprise shops the answer is LTS-to-LTS — .NET 8 → .NET 10 → .NET 12 — which minimises forced migrations and gives you the longest predictable runway each time. That’s exactly the move on the table now.

Beyond avoiding the cliff: .NET 10 earns the upgrade

Here’s the good news that makes this less of a chore: even if there were no deadline, .NET 10 would be worth moving to. It’s one of the largest .NET releases ever, and — unusually — a big chunk of the value lands for free the moment you retarget and rebuild.

The headline is that the runtime got materially faster without any code changes on your part. .NET 10 brings improvements to JIT inlining, method devirtualisation, stack allocation, and loop optimisation, plus hardware acceleration (AVX10.2 on Intel, Arm64 SVE) and write-barrier improvements that cut GC pause times by roughly 8–20%. These apply automatically at startup — you re-target, rebuild, run your existing benchmarks, and the numbers improve. In an era where every team is watching cloud spend and energy (the subject of the green-coding-and-cost series in this library), a faster runtime is a direct infrastructure-cost reduction you get for the price of an upgrade.

What’s New

.NET 10 bundles language, runtime, security, AI, web, and data improvements into a single release. Rather than an exhaustive changelog, here’s the map that matters for an upgrade decision — the things that change how you build.

Overview of new features in .NET 10, including updates in language, runtime, AI, security, web, data, and orchestration.

C# 14: productivity, not paradigm shift

C# 14 is a refinement release — no new mental model, just less friction. The standouts:

  • Field-backed properties via the field contextual keyword. This closes a long-standing annoyance: the moment you needed any custom logic in an accessor, you had to hand-write a backing field. Now the compiler generates it and you reference it as field:
// C# 14: custom logic with no explicit backing field
public string Name
{
get => field;
set => field = value?.Trim() ?? ""; // 'field' is the compiler-generated backing store
}
  • Extension members — the biggest addition. Extension blocks now support static extension methods and both static and instance extension properties, not just the instance methods C# has always had. It makes extending types you don’t own far more natural.
  • Null-conditional assignment: customer?.Address = newAddress; assigns only if customer is non-null — the mirror image of the null-conditional read you already use.
  • File-based apps: a single .cs file you run directly with dotnet run, with top-of-file directives for SDK and package references — and, new in .NET 10, they support dotnet publish and even NativeAOT. It collapses the distance between “script” and “app” and is a genuinely nice on-ramp for tooling and newcomers.

One practical note: existing projects compile unchanged, but to use new C# 14 syntax you set <LangVersion> to 14 (it’s the default with the .NET 10 SDK, but worth knowing when multi-targeting).

Runtime and NativeAOT: the free performance

As previously stressed, the runtime improvements are the ones you get without writing code — JIT inlining, method devirtualisation, better stack allocation and loop optimisation, AVX10.2 and Arm64 SVE acceleration, and GC pause reductions of 8–20%. Re-target, rebuild, re-run your benchmarks, and hot paths get cheaper. NativeAOT also advances: smaller, faster ahead-of-time-compiled binaries with improved struct-argument codegen — attractive for startup-sensitive workloads (serverless, CLIs, containers) where you want no JIT warm-up and a small footprint. The one caveat: NativeAOT trims aggressively, so re-run your trimming analysis if you rely on reflection.

The native AI story: the Microsoft Agent Framework

This is the headline feature and the reason “AI” now belongs in a .NET release note. Historically, adding AI to a .NET app meant bolting on external SDKs. .NET 10 brings a first-class, layered AI stack, anchored by the Microsoft Agent Framework (MAF), which reached 1.0 GA in April 2026 — a production-ready, open-source SDK for building agents and multi-agent workflows that unifies Semantic Kernel’s enterprise foundations with AutoGen’s orchestration.

Diagram illustrating the native AI stack in .NET 10, featuring sections on connectivity, Microsoft Agent Framework 1.0, Microsoft.Extensions.AI, and enterprise plumbing.

The layering is the point, and it maps directly onto the agent patterns from earlier series in this library:

  • Microsoft.Extensions.AI is the vendor-neutral abstraction layer — chat, embeddings, and tool-calling behind interfaces, so you can swap model providers without rewriting your app. This is the “don’t hard-code your model” discipline made into a framework.
  • MAF builds agents and multi-agent orchestration on top, with pluggable memory, middleware pipelines, and enterprise plumbing (OpenTelemetry observability, DefaultAzureCredential, Azure AI Foundry integration, a browser-based DevUI debugger).
  • MCP support is GA — your agents dynamically discover and invoke external tools over Model Context Protocol servers instead of hand-writing an HTTP wrapper per API. That’s the exact protocol the MCP series in this library covered, now native in .NET. A2A (agent-to-agent, cross-runtime) is in preview.

Two things worth flagging for the security-minded: MAF ships with guardrails that echo the agent-security series — server-initiated MCP sampling is denied by default, and file-access tools require approval — and because these APIs are young, they still see breaking changes between versions. And on the data side, EF Core 10 adds vector search for SQL Server 2025 and Azure SQL, so embeddings and similarity queries run in the database — the retrieval layer from the RAG series, now first-class in your ORM.

Post-quantum cryptography: prepare now, adopt deliberately

.NET 10 is, by Microsoft’s own description, the most significant cryptography release in years, because it brings post-quantum algorithms into the base libraries. The two that matter: ML-KEM (FIPS 203, key encapsulation) and ML-DSA (FIPS 204, digital signatures), with simplified APIs and Windows CNG backend acceleration (a cross-platform managed implementation covers Linux and macOS):

// ML-DSA (FIPS 204) post-quantum signatures — simplified .NET 10 API
using var key = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65);
byte[] signature = key.SignData(dataToSign);
bool valid = key.VerifyData(dataToSign, signature);

The most important primitive for real migrations is Composite ML-DSA, which combines a post-quantum algorithm with a classical one in a single signature — the hybrid pattern standards bodies recommend, so you gain quantum resistance without betting everything on newer algorithms. This connects to the digital-provenance and confidential-computing series: signing and attestation are exactly where “harvest now, decrypt later” threats bite, and financial, government, and healthcare shops are already being asked for quantum-resistant roadmaps.

The honest framing, which I’ll reinforce later: PQC support is preparatory. Having the primitives in the box lets you plan, prototype, and satisfy a compliance checkbox — but adopting them in production requires interoperability testing and threat modelling. It is not a flip-the-switch change, and you shouldn’t rip out working crypto to chase it before your partners and platforms are ready.

Web, data, and orchestration

Rounding out the release: ASP.NET Core 10 focuses on Blazor (WebAssembly preloading for faster perceived load, automatic memory-pool eviction to stop long-lived servers accumulating stale memory), OpenAPI v2, and Identity. EF Core 10 adds named query filters — finally allowing multiple global filters per entity (the long-standing soft-delete-plus-multi-tenancy pain) that you can selectively disable per query. Aspire 13 advances code-first orchestration for microservices and containers, with an aspire update command that scans your AppHost and packages, validates compatibility, and upgrades safely. And JSON serialisation gains strictness options — disallowing duplicate properties, stricter settings, PipeReader support — that quietly remove distributed-systems footguns.

The Migration

The good news up front: a .NET 8-or-9 to .NET 10 upgrade is, for most well-maintained applications, a retarget-and-fix exercise, not a rewrite. (Migrating from .NET Framework 4.x is a genuinely different and larger effort — a separate project involving extracting logic into .NET Standard libraries and replacing System.Web/WCF — so this I assume you’re already on modern .NET.) The workflow is well-trodden and increasingly tool-assisted.

A flowchart demonstrating the .NET 10 migration workflow, outlining six key steps: Assess, Retarget, Update packages, Fix breaks, Test, and Ship. The bottom section mentions tooling support including the .NET Upgrade Assistant and AI-powered tools.

The workflow, step by step

1. Assess. Inventory every app, its target framework, and its dependencies. The critical gate here is third-party package compatibility — a NuGet dependency without a .NET 10 build will block you. List what’s outdated before you touch anything:

dotnet list package --outdated        # see what needs bumping
dotnet list package --vulnerable # and what's a security liability today

2. Retarget. Change the Target Framework Moniker. During a phased rollout, multi-targeting keeps the old target building while you validate the new one:

<!-- straight retarget -->
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

<!-- or multi-target during the transition (recommended for libraries) -->
<PropertyGroup>
<TargetFrameworks>net9.0;net10.0</TargetFrameworks>
</PropertyGroup>

3. Update packages. Bump every Microsoft.Extensions.*, Microsoft.AspNetCore.*, and Microsoft.EntityFrameworkCore.* reference to 10.0.x (or update Directory.Packages.props if you use Central Package Management), then dotnet restore. Watch for a few .NET-10-specific NuGet behaviors: NU1510 flags a package you can now delete because it moved into the shared framework; a PackageReference with no Version is now an error; and restore audits transitive dependencies, so you may see new vulnerability warnings worth heeding.

4. Fix breaking changes. .NET categorises these as source-incompatible (won’t compile), binary-incompatible (won’t bind), and behavioural (compiles and runs, but acts differently — the dangerous kind). Common ones on this hop: obsoletions SYSLIB0058–SYSLIB0062, System.Linq.Async superseded by the built-in IAsyncEnumerable, OpenAPI v2 API changes, some cryptography renames, and — in containers — a Debian-to-Ubuntu base-image change in the official .NET images. The single best defence is to fail loudly:

<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <!-- obsolete-API warnings become build failures you must address -->
</PropertyGroup>

5. Test and benchmark. Run your full suite, and — because it’s free money — benchmark the .NET 10 build against your .NET 9 baseline with BenchmarkDotNet. The runtime gains from previous apply automatically, so this step both validates the upgrade and quantifies the performance win for your stakeholders.

6. Deploy. Update CI images, build agents, and Dockerfiles to the .NET 10 SDK, then ship.

Let the tooling carry the load

Two tools make this dramatically less manual, and using them is the norm in 2026, not a shortcut.

The .NET Upgrade Assistant (dotnet tool install -g upgrade-assistant, then upgrade-assistant upgrade MyProject.csproj) analyzes the project, updates the TFM and package versions, applies known fixes, and generates a report. It runs in-place, side-by-side, or incremental mode.

The bigger leap is the AI-powered modernize-dotnet agent for GitHub Copilot, which follows an assess → plan → execute model: it analyzes dependencies and configuration, generates a migration plan flagging specific breaking changes, then executes the transformations — updating package versions, fixing namespace changes, resolving compiler errors. Critically, it creates a Git commit at each stage, so you can review diffs, roll back individual steps, or cherry-pick, and it runs identically from VS Code, the Copilot CLI, or a GitHub pull request. This is the “AI participates in structured engineering workflows” pattern from the agentic-engineering series, pointed squarely at migration drudgery — with a human reviewing every commit rather than a black-box rewrite.

A pre-migration hygiene tip that pays off: if you’re still on Newtonsoft.Json, consider moving to System.Text.Json first, as a separate mechanical change — it’s faster, avoids a class of deserialisation vulnerabilities, and removes a variable from the main upgrade.

The honest gotchas

An upgrade this broad has real edges. None are blockers; all deserve planning.

An infographic detailing five key challenges related to modern .NET development, including topics on breaking changes, package lag, AOT compatibility, post-quantum crypto preparation, and timing considerations.
  • Behavioural breaking changes are the sneaky ones. Source and binary breaks announce themselves at build time; behavioural changes compile cleanly and misbehave at runtime. Your test suite and warnings-as-errors are the safety net.
  • Third-party lag can gate you. With 478,000+ NuGet packages, not all update on day one. One un-migrated critical dependency blocks the whole app — which is why compatibility assessment comes first, not after you’ve retargeted.
  • NativeAOT isn’t universal. Its aggressive trimming breaks reflection-heavy and dynamic-code patterns. If you adopt it, re-run trimming analysis; if your app leans on reflection, it may not be a fit — and that’s fine, AOT is opt-in.
  • PQC is a roadmap item, not a rebuild. As I’ve previously said: the primitives are in the box for planning and prototyping, but production adoption needs interop testing and threat modelling. Don’t destabilise working cryptography to chase quantum-resistance before your ecosystem is ready.
  • Timing cuts both ways. Don’t wait until November 2026 — testing, dependency chases, and change-freeze windows consume the runway fast. But don’t carelessly rush a mission-critical system either. Pilot a representative non-critical service first, profile it, learn the breaking changes on low stakes, then roll out.

The adoption playbook

  1. Inventory now. List every app, its TFM, and its dependencies; the runway to November 2026 is shorter than it looks.
  2. Assess third-party compatibility first — one blocking package can stall the whole plan, so find it early.
  3. Pilot a non-critical service end to end to learn the breaking changes cheaply.
  4. Lean on the tooling — Upgrade Assistant for the mechanics, the modernize-dotnet Copilot agent for AI-assisted, commit-by-commit migration you can review.
  5. Retarget, update packages, build with warnings-as-errors, and work the breaking-change list.
  6. Test hard and benchmark against your .NET 9 baseline to validate correctness and capture the free performance win.
  7. Sequence LTS-shaped work deliberately — adopt the runtime gains and framework updates now; treat PQC and NativeAOT as opt-in roadmap items where they fit.
  8. Update CI, containers, and DevOps tooling to the .NET 10 SDK as part of the rollout, not after.
  9. Keep a bridge only for true exceptions — third-party extended support for a genuinely un-migratable app, never as the plan.

The whole picture

Step back and there is one clear decision. .NET 8 and .NET 9 both hit end of support on November 10, 2026, which makes moving to .NET 10 LTS (supported to November 2028) a forced decision this year, not a someday-maybe. But it’s a forced decision worth making, because .NET 10 is one of the largest releases ever — free runtime performance, C# 14, a native AI stack in the Microsoft Agent Framework, post-quantum cryptography, and updates across web, data, and orchestration. And the migration itself is a tractable, well-tooled retarget-and-fix for modern .NET apps, provided you assess dependencies early, pilot first, lean on the Upgrade Assistant and the AI modernisation agent, and respect the honest gotchas.

CLI Agents vs AI-Native IDEs: The Token Economics

Here is the benchmark that’s been driving architecture decisions across the industry in 2026: a typical CLI command costs an agent around 200 tokens. The equivalent operation through an MCP server costs 32,000 to 82,000 tokens.

That’s not a typo, and it’s not cherry-picked. Independent benchmarks from Scalekit, Apideck, and others keep landing in the same range — roughly a 35× overhead for MCP on identical tasks. When “MCP is dead. Long live the CLI” hit the top of Hacker News and Perplexity’s CTO publicly described moving away from MCP internally over context waste, they were all pointing at the same arithmetic.

This series is about that arithmetic — where it’s real, where it isn’t, and how to design around it. We start with the cost itself, because once you see where the tokens go, every later decision gets easier.

Where the tokens actually go

The gap comes down to one architectural difference: when does the agent pay for a tool’s definition?

MCP loads everything, always. When an agent connects to an MCP server, the entire tool catalog — every tool’s name, description, and full input/output JSON schema — is injected into the context window. It sits there on every single completion request, whether the agent calls ten tools or zero. The GitHub MCP server exposes ~93 tools; loading it costs roughly 55,000 tokens before the agent reads its first instruction. The agent is carrying schemas for creating gists, configuring webhooks, and managing PR reviews even when all it wants is the repo’s primary language.

CLI pays only when it calls. A command-line agent starts with zero tool context. When it needs GitHub, it runs gh repo view — and the model already knows gh from training, so the command plus its output might cost 200 tokens. No catalog. No schema. No discovery step that loads 92 tools it will never touch.

Diagram illustrating the distribution of tokens in a 200,000-token context window for CLI and MCP agents, including token usage and reasoning capacity.

The stacking problem

A single server is survivable. The trouble is that real agents connect several. Add GitHub, a database connector, a project tracker, and a cloud provider, and a widely-cited Apideck measurement shows three MCP servers consuming 143,000 of a 200,000-token window — about 72% gone before the agent reads its first user message.

Now do the cost math at production scale. At roughly \$3 per million input tokens, 55,000 tokens of schema is about \$0.16 per session. Run 10,000 automated sessions a day — an unremarkable volume for a production pipeline — and you’re spending ~\$1,600 every day just loading tool definitions, before the agent solves anything. That’s the line item teams started calling the “MCP tax.”

The cost you can’t see: reasoning budget

Token cost is the headline, but it’s not the most important number. The deeper problem is cognitive.

A context window is also the agent’s working memory. Every token spent on tool schemas is a token not available for reasoning about the actual task. When 70% of the window is consumed by definitions, the model is trying to think in the cramped space that’s left — and quality degrades, especially late in a long task when accumulated tool output has pushed important context toward the edges of the window where attention is weakest.

This is why the cost gap reappears as a reliability gap. In Scalekit’s benchmark, CLI agents completed tasks with 100% reliability while the MCP equivalents came in at 72% — and most of the MCP failures weren’t logic errors but connection timeouts to a remote server. On a token-efficiency score (work completed per token spent), CLI scored 202 to MCP’s 152, a 33% advantage: the CLI agent spent its tokens on solving the problem instead of on protocol overhead.

Why CLI is good, not just cheap

It’s tempting to stop at “CLI uses fewer tokens,” but that misses the real reason it works so well. Models have been trained on decades of terminal interactions — Stack Overflow answers, GitHub histories, Dockerfiles, jq pipelines, git invocations, Kubernetes manifests. Shell tooling lives in the model’s weights as latent knowledge. When an agent composes gh pr list --json number,title | jq '.[] | select(...)', it’s operating from prior knowledge, not parsing a schema it met for the first time three tokens ago.

MCP schemas, by contrast, carry zero pretraining advantage. They’re custom JSON the model has never seen, that must be read and interpreted fresh on every run. The token savings of CLI are almost a side effect; the structural win is that the model already fluently speaks the interface.

Measure your own context budget

Before we build anything, do the one exercise that makes this concrete for your stack: measure what your tool integrations cost on idle. Here’s a quick way to tally MCP schema overhead using the same tokenizer your model uses:

# pip install tiktoken
import json, tiktoken

enc = tiktoken.get_encoding("cl100k_base") # close enough for an estimate

def tokens(text: str) -> int:
return len(enc.encode(text))

# Paste in the tool catalog your MCP client advertises (the result of tools/list,
# including each tool's full JSON schema). Many clients can dump this.
with open("mcp_tools_dump.json") as f:
catalog = json.load(f)

total = 0
for tool in catalog["tools"]:
cost = tokens(json.dumps(tool)) # name + description + input/output schema
total += cost
print(f"{tool['name']:<32} {cost:>6} tokens")

print(f"\nALWAYS-ON SCHEMA OVERHEAD: {total:,} tokens")
print(f"As share of a 200k window: {total/200_000:.0%}")
print(f"Per-session cost @ $3/1M: ${total * 3 / 1_000_000:.3f}")
print(f"Daily @ 10k sessions: ${total * 3 / 1_000_000 * 10_000:,.0f}")

Now compare against the CLI baseline: the discovery cost of a CLI tool is whatever your-tool --help returns — usually 150–600 tokens, paid once, only if the agent is unsure. Run this against your real tool catalog and the abstract benchmark becomes your actual API bill.

The honest caveat (so you don’t over-correct)

Everything above is real, but it benchmarks the slice of the world where a CLI exists and the agent controls execution. That’s a large and important slice — and it’s exactly where production pipelines live — but it isn’t everything. There is no Workday CLI, no Greenhouse CLI; for multi-tenant products where an agent acts on behalf of a specific user, the schema tax is buying something real (identity, scope, audit) that a raw shell can’t provide. We’ll give that side a full and fair hearing a bit ahead. For now, hold the cost picture clearly, because it’s the force pushing terminal-based agents into production pipelines — and it’s earned.

CLI Agents vs AI-Native IDEs: Building CLI-First Agents

A CLI-first agent is almost embarrassingly simple in concept: instead of wiring the agent to a catalog of pre-declared tools, you give it one tool — a shell — and let it compose commands. The sophistication isn’t in the plumbing; it’s in how you shape the agent’s knowledge and contain its blast radius. Let’s build it up piece by piece.

The core loop: one tool, a whole toolbox

The entire tool surface is a single bash function. The model writes a command; you run it; you feed back the output. That’s it.

import subprocess
from anthropic import Anthropic

client = Anthropic()

BASH_TOOL = {
"name": "bash",
"description": "Run a shell command and return its stdout/stderr.",
"input_schema": {
"type": "object",
"properties": {"cmd": {"type": "string"}},
"required": ["cmd"],
},
}

def run(cmd: str) -> str:
r = subprocess.run(cmd, shell=True, capture_output=True,
text=True, timeout=120)
return (r.stdout + r.stderr)[:10_000] # cap to protect the context window

def agent(task: str, system: str):
messages = [{"role": "user", "content": task}]
while True:
resp = client.messages.create(
model="claude-sonnet-4-6", max_tokens=2000,
system=system, tools=[BASH_TOOL], messages=messages,
)
messages.append({"role": "assistant", "content": resp.content})
tool_calls = [b for b in resp.content if b.type == "tool_use"]
if not tool_calls:
return resp # agent is done
results = []
for call in tool_calls:
output = run(call.input["cmd"])
results.append({"type": "tool_result",
"tool_use_id": call.id, "content": output})
messages.append({"role": "user", "content": results})

Notice what’s absent: no per-tool schemas, no MCP catalog, no discovery payload. The agent already knows gh, git, jq, az, kubectl, psql, and a thousand other commands from pretraining. The single bash tool definition costs a few dozen tokens, and that’s the entire fixed overhead — versus the 55,000 tokens a GitHub MCP server would put on every request.

Composability: the property MCP can’t match

Token cost is the famous argument, but composability is the one practitioners care about more once they’ve felt it. MCP tools don’t chain — you can’t pipe one tool’s output into another; each call is a separate round-trip with the agent shuttling intermediate state back and forth, paying tokens and latency every hop.

A shell pipeline does the whole thing in one shot:

gh pr list --state merged --json number,title,mergedAt \
| jq '[.[] | select(.mergedAt > "2026-05-01")] | length'

That’s “how many PRs merged since May 1st” as a single command. The agent composes the pipe, the shell executes it locally, and one number comes back. The MCP equivalent is: call list_pull_requests, get a large JSON blob into context, reason over it, maybe call again with pagination, filter in the agent’s head. More tokens, more latency, more failure surface.

Diagram illustrating a CLI-first agent that composes pipelines using a single bash tool. It shows the process of executing a shell command locally and integrating various local CLIs such as git, jq, psql, kubectl, and az.

This is just the Unix philosophy — small tools that do one thing, composed with pipes — and it turns out agents are excellent at it, because the training data is full of exactly these one-liners.

The 800-token skill file (the best ROI in the benchmark)

Raw CLI works, but there’s a refinement that’s almost free and pays for itself immediately. Instead of loading 28,000 tokens of MCP schema, give the agent a tiny skill file: a few hundred tokens of plain-markdown tips about the tools it has.

In Scalekit’s benchmark, an 800-token markdown file of gh tips beat 28,000 tokens of MCP schemas — the skill-augmented agent made about a third fewer tool calls and finished about a third faster than even the naive CLI agent. It was the single best return on investment they measured. A skill file looks like this:

# GitHub (gh) tips for this repo

- Auth is already configured; never run `gh auth`.
- Prefer `--json <fields>` + `jq` over scraping human-readable output.
- Useful fields: number, title, state, mergedAt, author, labels.
- List recent merged PRs:
gh pr list --state merged --limit 50 --json number,title,mergedAt
- This repo's default branch is `main`. CI is GitHub Actions.
- NEVER use: `gh repo delete`, `git push --force`.

You load that string into the system prompt. It costs a rounding error in tokens and dramatically sharpens behavior, because it encodes the two things training data can’t: your conventions and the specific flags that matter for this repo. This is also the cleanest place to put guardrails the agent should always honor.

SKILL = open("skills/github.md").read()           # ~800 tokens
SYSTEM = (
"You are a CLI agent. You have a bash tool. Prefer composing pipelines. "
"Use --help if unsure about a command's flags.\n\n" + SKILL
)
agent("How many PRs merged since May 1st?", SYSTEM)

--help as just-in-time documentation

What about a CLI the agent doesn’t know well, or a tool with unusual flags? You don’t pre-load its manual. You let the agent fetch documentation on demand: if it’s unsure, it runs some-tool --help and pays ~200 tokens for exactly the information it needs, exactly when it needs it. This is the same pay-per-use principle we’ve seen previously, applied to documentation: progressive disclosure instead of always-on schema. The agent’s instinct to do this is worth encouraging explicitly in the system prompt, as above.

The part nobody likes to talk about: a shell is a loaded gun

Here’s the honest cost of all this power. The single bash tool that makes CLI agents efficient and composable also hands the model rm -rf, git push --force, DROP TABLE, and arbitrary code execution. MCP’s much-maligned schema overhead is partly buying something: an agent can only call tools that were explicitly declared, so the blast radius is bounded by design. A shell has no such boundary. One bad generation is one bad generation away from something you can’t undo.

So a CLI-first agent is only production-ready with containment. The non-negotiables:

  • Sandbox execution. Run commands inside a container or VM with no access to production credentials, scoped to a throwaway working directory — the same isolation discipline any agentic system needs.
  • Least privilege. The environment gets only the credentials and network access the task requires. An agent summarizing PRs does not need write access to the repo or your database URL in its environment.
  • A deny list and/or approval gate. Block destructive verbs outright (rm -rf, force-push, DROP, DELETE FROM without a WHERE), and require human approval for anything that mutates state. The skill file’s “NEVER use” section helps, but never rely on the model’s compliance as your only control.
  • Output caps and timeouts. Bound stdout (as in run() above) so a runaway command can’t flood — and evict — the context window.
DENY = ("rm -rf", "git push --force", "git push -f", " drop table",
"delete from", "mkfs", ":(){", "> /dev/sd")

def run(cmd: str) -> str:
low = f" {cmd.lower()} "
if any(bad in low for bad in DENY):
return "BLOCKED: destructive command requires human approval."
# ...then execute inside the sandbox as before

A deny list is a backstop, not a security boundary — the real boundary is the sandbox. Treat the shell’s power as something you deliberately fence in, not something you trust the model to wield carefully.

What we’ve built — and what it can’t do

We now have a CLI-first agent that’s cheap (one tool, no schema tax), composable (it pipes), well-informed (a tiny skill file), and contained (sandbox + deny list). For developer-facing work and deterministic production pipelines where a mature CLI exists, this design is hard to beat.

But re-read that sentence: where a mature CLI exists and where you control execution. The moment your agent needs to act on behalf of a specific customer, inside a multi-tenant SaaS system that ships only an OAuth API and no shell, this whole approach runs out of road — and the schema tax you’ve been avoiding turns out to be the price of something you now actually need. That boundary, and how to decide on which side of it any given integration falls, is what we’ll see next.

CLI Agents vs AI-Native IDEs: When to Use Which

If you’ve read the above, you might think the verdict is in: CLI is cheaper, more reliable, more composable, so use it everywhere. That conclusion is wrong, and the teams that act on it create a different, quieter class of problem. The token benchmark is real — but it measures roughly the 5% of integrations where a CLI even exists and where you control execution. The other 95% of enterprise surfaces are a different world. Now, we’ll give that world its due and then hand you a framework that makes the choice mechanical.

Where MCP and AI-native IDEs actually win

Be fair to the other side, because the other side is right about several things.

Services with no shell. There is no Workday CLI, no Greenhouse CLI, no BambooHR CLI — and there never will be. These are SaaS systems with OAuth APIs, custom subdomain routing, refresh tokens, and org-level access control. MCP was built precisely for these. When the only integration a vendor ships is an API behind OAuth, the “just use the CLI” advice has nothing to point at.

Acting on behalf of a specific user. A CLI agent runs in your shell with your ambient credentials. That’s fine when you are the user. It’s a non-starter when an agent acts for a specific customer across a specific tenant. MCP’s model — explicit tool declarations, per-user OAuth 2.1 with PKCE, scope enforcement, the ability to revoke one user without touching everyone else — is buying governance the schema tax pays for. As one widely-shared analysis put it: the properties that make MCP expensive are the same properties that make it governable.

Audit and compliance. Structured tool calls with declared inputs produce clean audit trails. “The agent ran some bash” does not. In regulated workflows, that structure isn’t overhead — it’s the requirement.

The interactive IDE experience. AI-native IDEs (Cursor, Windsurf, Copilot-style tools) lean on always-on rich context and MCP integrations on purpose: it’s what makes inline exploration, hovering, and conversational iteration feel seamless for a human in the loop. A Sales Director shouldn’t have to read a stderr traceback. The token cost buys a UX that a headless shell simply doesn’t offer. The catch is that this advantage is about interactive use — which is exactly the part a production pipeline doesn’t have.

And the reliability gap from above deserves an asterisk: most MCP failures in the benchmarks were connection timeouts to remote servers — infrastructure problems, not protocol problems. An MCP gateway (one that filters schemas down to the relevant tools, pools connections, and centralizes auth) closes much of both the cost and reliability gap. So does lazy schema loading (Anthropic’s Tool Search, shipped late 2025), which defers pulling a tool’s full schema until it’s actually needed. The naive 55,000-token connection is a worst case, not a law of nature.

The reframe: it was never “CLI vs MCP”

Here’s the insight that makes the whole debate dissolve. MCP and CLI don’t sit on the same axis. Treating them as competing transports is a category error — like arguing whether to use an enterprise service bus or an API. They operate on different planes of the agent stack, and most well-designed systems use all of them at once.

Diagram illustrating three planes: Knowledge plane with skills and prompts, Execution plane focusing on CLI tools, and Governance plane for multi-tenant SaaS systems.
  • Execution plane → CLI. Developer-facing agents, local tooling, code operations, infrastructure-as-code — anything with a mature shell interface the base model has seen in training. Accept the modest cold-start of describing the tools; harvest the long tail of pretraining familiarity.
  • Governance plane → MCP. Customer-facing agents, multi-tenant SaaS, systems of record, regulated workflows — any surface that requires per-request identity, scope enforcement, or audit. Spend the schema tokens here; they’re buying compliance.
  • Knowledge plane → Skills. Domain procedures, company conventions, playbooks. These aren’t tools at all. They belong in skill files and prompts. Wrapping a procedure in an MCP schema or a CLI is the most common over-engineering mistake — it’s instructions cosplaying as a transport.

Confusing the planes produces the exact pathologies the industry has been cataloguing all year: MCP servers wrapping shell commands that burn tokens for zero governance benefit; CLIs bolted onto SaaS integrations that leak credentials and lose audit trails; skills written as MCP tools, duplicating schema that should have been three lines of markdown.

The decision framework

Stop asking “MCP or CLI?” Ask three questions about each tool integration, in order:

Flowchart illustrating the decision-making process for choosing a transport method in tool integration, including questions about model maturity, trust boundaries, and procedural versus tool classification.
  1. Does this tool have a mature shell interface the base model already knows? (git, gh, kubectl, psql, az, aws, jq…) → Use CLI. The token savings are a side effect; the real win is operating from pretrained knowledge.
  2. Does this action cross a trust boundary needing per-user identity, scope enforcement, or audit? (a customer’s CRM, a tenant’s billing) → Use MCP, ideally behind a gateway. The schema cost is buying something no shell provides.
  3. Is this actually a procedure or convention dressed up as a tool? (a runbook, a house style, a multi-step playbook) → Put it in a skill or prompt. Don’t wrap it in any transport.

Decide this per integration, not per system. Your agent will almost certainly use all three.

What this means for production pipelines

The reason terminal-based agents are winning production pipelines specifically falls right out of the framework. A pipeline is headless and batched — there’s no human enjoying the IDE’s interactive UX, so that entire side of MCP’s value proposition is absent. And pipelines are dominated by deterministic operations: run tests, build, lint, query a database, transform files, hit git and gh. Those are textbook execution-plane work — CLI territory — where the token efficiency compounds across thousands of runs and the 100% reliability matters because nobody’s watching.

But “pipeline = all CLI” is still too simple. A mature pipeline is a hybrid:

  • Deterministic steps run as CLI / scripts / hooks — the bulk of the work, cheap and reliable.
  • The few steps that touch a governed external system go through MCP — fetching a customer record, posting to a per-tenant SaaS — ideally via a gateway that filters schemas so you pay for the three tools you use, not the ninety you don’t.
  • The pipeline’s domain logic lives in skills — what “done” means, your conventions, the order of operations — not baked into either transport.

That’s the real end state. Not “CLI beat MCP,” but a pipeline where each integration sits on its correct plane, the deterministic majority runs as efficient shell commands, and the governed minority pays the schema tax precisely where it buys something.

The bottom line

If the integration is…UseBecause
A tool with a mature CLI the model knowsCLIPretrained fluency + ~200 tokens vs ~35× for MCP
A SaaS system with no shell, behind OAuthMCPThe only option that handles tenant identity
Acting on behalf of a specific customerMCP (gateway)Per-user auth, scope, revocation, audit
A deterministic step in a headless pipelineCLICheaper, 100% reliable, composable, no UX needed
A procedure, convention, or playbookSkillIt’s instructions, not a tool — no transport
Multi-tenant infra needing bothBothCLI execution plane + MCP governance plane

The token economics are what made everyone look, and they’re genuinely the reason CLI agents are taking over production pipelines. But the durable lesson is the one underneath: match each integration to its plane. Do that and you stop paying the MCP tax where it buys nothing, stop leaking credentials where you need governance, and stop wrapping instructions in schemas. The transport stops being a religion and goes back to being an implementation detail — which is exactly where it belongs.

MCP vs A2A: Two Protocols, Two Different Problems

If you’ve built anything with AI agents recently, you’ve heard two acronyms thrown around constantly, often with wildly conflicting takes: “MCP is the USB-C of AI.” “A2A replaces MCP.” “You need both.” “Neither is production-ready.”

Most of that noise comes from a single confusion. MCP and A2A solve completely different problems, and treating them as competitors — or worse, as interchangeable — is one of the most common and most expensive architecture mistakes in the space right now. Get the distinction wrong and your system fights you at every layer: you’ll find yourself wrapping databases as “agents,” building bespoke RPC glue between services that should just speak a standard, or reaching for the heavier protocol when the lighter one was the right call.

Here’s the entire thesis of this series in two sentences:

MCP handles how an agent talks to tools. A2A handles how agents talk to each other.

Everything else follows from that. Let’s make it precise.

Two problems that look similar and aren’t

Before either protocol existed, two distinct pain points kept showing up in agentic systems.

Problem 1 — tool integration. A single agent needs to do things in the world: query a database, hit an API, read a file, send an invoice. Every integration was a bespoke one-off. If you had M agent applications and N tools, you were maintaining M×N custom integrations, each inventing its own auth, schema, and error handling. This is a vertical problem — it’s about one agent reaching down to capabilities.

Problem 2 — agent coordination. As teams built multi-agent systems, there was no standard way for agents to discover each other, advertise what they could do, or hand off work. If you had a research agent and a writing agent, wiring them together meant gluing orchestration logic into application code. Swap one agent for another and you rewrote the integration. This is a horizontal problem — it’s about agents reaching across to peers.

MCP is the purpose-built answer to Problem 1. A2A is the purpose-built answer to Problem 2. Understanding that they sit on different axes is the key to everything else.

A diagram comparing MCP (Model Control Protocol) and A2A (Agent to Agent) communication, featuring two perpendicular axes. The diagram includes three agents (Agent B, Your Agent, and Agent C) connected by A2A links, and shows how the Your Agent communicates with a database, API, and files via MCP.

MCP: the vertical protocol

The Model Context Protocol (MCP), created by Anthropic and released in November 2024, standardizes how an AI agent connects to external tools, data, and services. Think of it as a universal connector — the analogy people reach for is USB-C: one standard plug instead of a drawer full of proprietary cables.

Concretely, an MCP server exposes capabilities through three primitives — Tools (actions the model can invoke), Resources (read-only data it can fetch), and Prompts (reusable templates) — and any MCP-compatible host can discover and use them without bespoke integration code. The M×N integration explosion collapses into M+N: build a server once, and every MCP-aware host can use it.

MCP is, at its core, synchronous and request-response: the agent asks the tool to do something and gets an answer back, usually in the same breath. (An experimental Tasks primitive for longer-running work arrived in the late-2025 spec, but the model’s center of gravity is still fast, synchronous tool calls.)

A2A: the horizontal protocol

The Agent2Agent protocol (A2A), created by Google and released in April 2025 with 50+ launch partners, standardizes how independent agents — built by different teams, on different frameworks, hosted on different networks — discover each other, delegate tasks, and exchange results.

Its defining trait is the opposite of MCP’s. A2A is intentionally stateful and asynchronous: it assumes tasks can be long-running, multi-step, and human-in-the-loop from the start. A client agent delegates work to a remote agent, and — crucially — the remote agent does that work without access to the client’s internal context, memory, or tools. That opacity is a feature: it respects the autonomy and privacy boundaries you need when one company’s agent calls another’s. The analogy here is HTTP: it doesn’t care whether the other side runs Rails, Django, or Go; it just defines the shape of the conversation.

They are designed to compose, not compete

The single most important takeaway: these protocols are complementary. Most real production multi-agent systems use both — A2A to coordinate between agents, and MCP so each agent can reach its own tools and data. They live on perpendicular axes, so they never actually overlap:

MCPA2A
Question it answersHow does my agent use a tool?How does my agent talk to another agent?
AxisVertical (agent → tools/data)Horizontal (agent ↔ agents)
RelationshipAgent → capabilityPeer ↔ peer
Interaction styleSynchronous request/responseStateful, async, long-running
Context sharingTool runs inside the agent’s contextRemote agent has no access to caller’s context
Created byAnthropic (Nov 2024)Google (Apr 2025)
On the wireJSON-RPC 2.0 over stdio / Streamable HTTPJSON-RPC 2.0 over HTTP(S) + SSE
Unit of workA tool callA Task (with a lifecycle)

Both are now neutral, governed standards

A fair worry in 2025 was vendor lock-in: MCP was “an Anthropic thing,” A2A “a Google thing.” That’s no longer the situation, and it matters for adoption decisions.

Both protocols now live under the Linux Foundation’s Agentic AI Foundation (AAIF). Google contributed A2A to the Linux Foundation in mid-2025, and Anthropic donated MCP in December 2025. Both have crossed the production-maturity threshold: MCP’s current spec revision is the November 2025 release, with 10,000+ public servers and native support across every major model provider; A2A reached a stable v1.0 in early 2026, with signed Agent Cards, SDKs in five languages, and 150+ production organizations. Neither is a moving target anymore, and neither is owned by a single vendor.

When you’re staring at a design decision, ask one question:

Is the thing on the other end a capability, or is it a peer?

  • A database, an API, a file system, a payment processor, a search index — those are capabilities. They reach down. Use MCP.
  • Another autonomous agent — owned by a different team or vendor, running its own logic, that you want to delegate a goal to and get a result back from — that’s a peer. It reaches across. Use A2A.

And when you have a fleet of agents that each need their own tools? You use both, on their respective axes.

MCP in Practice — How an Agent Talks to Tools

MCP gives a model a standardized contract for reaching the outside world. Instead of every host hand-coding every integration, a server declares what it can do, describes it with schemas, and any MCP-compatible host can discover and use it.

The architecture: host, client, server

MCP has three roles, and keeping them straight prevents most confusion:

  • Host — the AI application the user interacts with (Claude Desktop, Claude Code, Cursor, your own app). The host owns the model and the conversation.
  • Client — a connector the host instantiates, one per server, that manages a single stateful session. A host talking to three servers runs three clients.
  • Server — a program that exposes capabilities. It can run locally (a subprocess on your machine) or remotely (a hosted HTTPS service).

Underneath, MCP is JSON-RPC 2.0 split into two layers: a data layer (the message schema and primitives) and a transport layer (how those messages travel). That separation is why the same primitives work identically whether the server is a local subprocess or a remote service.

Diagram illustrating the MCP architecture and message flow, showing Host and Server interactions, including the Model and Client roles.

The three server primitives

Everything a server offers falls into exactly three buckets. Getting the mapping right is most of good MCP design.

Tools are functions the model can invoke to perform actions — they have side effects, like POST endpoints. Discovered via tools/list, executed via tools/call.

Resources are read-only data the model can fetch for context — no side effects, like GET endpoints. Discovered via resources/list, read via resources/read.

Prompts are reusable templates that structure a recurring interaction. Discovered via prompts/list, retrieved via prompts/get. (In practice this is the least-used primitive — most teams keep prompts in host-side code — but it’s the right tool for domain-specific servers that want to enforce a house workflow.)

A clean way to remember it: Tools do, Resources show, Prompts guide.

Here’s a complete small server using FastMCP (the standard Python library, 3.x as of 2026). It models a tiny CRM:

from fastmcp import FastMCP

mcp = FastMCP("crm-server")

# TOOL — an action with side effects (the model can invoke it)
@mcp.tool()
def create_lead(name: str, email: str, source: str = "web") -> dict:
"""Create a new sales lead and return its record."""
lead = db.insert("leads", {"name": name, "email": email, "source": source})
return {"id": lead.id, "status": "created"}

# RESOURCE — read-only context (the model can fetch it, no side effects)
@mcp.resource("crm://customers/{customer_id}")
def customer_record(customer_id: str) -> str:
"""Return a customer's record as JSON."""
return json.dumps(db.get("customers", customer_id))

# PROMPT — a reusable template that guides a workflow
@mcp.prompt()
def qualify_lead(lead_id: str) -> str:
"""Guide the model through qualifying a lead."""
return (f"Review lead {lead_id}. Assess fit on budget, authority, "
f"need, and timeline. Recommend next action.")

if __name__ == "__main__":
mcp.run(transport="stdio") # local; swap to "streamable-http" to host remotely

Notice what you didn’t write: no JSON-RPC plumbing, no schema by hand. The SDK infers each tool’s input schema from the function signature and advertises name, description, and schema to any client that connects. That’s the whole point — the contract is generated from ordinary typed Python.

The two transports

MCP deliberately supports only two transports, and the choice is about where the server runs, not what it does.

stdio — the host launches the server as a subprocess and exchanges JSON-RPC frames over standard in/out. No network, no ports, OS-level identity for security. This is the default for local tools — it’s how Claude Desktop and Claude Code run filesystem and git servers.

Streamable HTTP — the server is a network endpoint: a single HTTPS URL that accepts HTTP POST for requests, with optional Server-Sent Events for streaming. This is the transport for remote, multi-user, or cloud-hosted servers. It was introduced in the 2025-03-26 spec and replaced the older HTTP+SSE transport, which is being sunset across major providers through 2026 — so if you see two-endpoint HTTP+SSE examples online, they’re stale.

A practical rule: local dev tool → stdio; anything multi-tenant or hosted → Streamable HTTP behind OAuth 2.1, which is the recommended auth for remote servers. If you’re deploying serverless, prefer a stateless server (omit the Mcp-Session-Id header) so it scales horizontally without sticky sessions — stateless operation is the headline item on MCP’s 2026 roadmap.

The reverse primitives: what makes MCP interactive

Most people think of MCP as one-directional — host calls server. The feature that surprises people is that servers can call back to the host. These client-side primitives are what make MCP interactive rather than a static function catalog:

  • Sampling (sampling/createMessage) — the server asks the host to run an LLM completion on its behalf. Why? So an agentic server can reason without bundling its own model SDK or API key. A GitHub server that needs to classify an issue’s severity can ask the host to run that classification with whatever model the user is already paying for. The server stays model-independent; the user owns the billing and the approval.
  • Elicitation — the server asks the user, through the host’s UI, for missing input or confirmation. A payments server can surface “Is the user OK paying $4.99 for this?” right in the chat thread.
  • Roots — the host advertises trusted filesystem boundaries to the server.

These reverse calls always route through the host, which must show the user what’s being asked and let them edit or reject it. That consent boundary is core to MCP’s security model.

Connecting from a host

On the host side, the lifecycle is: connect → discover → invoke. Conceptually:

# Pseudocode for the host/client side
session = connect("stdio", command=["python", "crm_server.py"])
await session.initialize() # capability handshake

tools = await session.list_tools() # tools/list — dynamic discovery
result = await session.call_tool( # tools/call
"create_lead", {"name": "Ada", "email": "ada@example.com"})

schema = await session.read_resource("crm://customers/42") # resources/read

Discovery is dynamic: the host calls tools/list at runtime, so a server can add or remove tools and notify the host with a notifications/tools/list_changed message rather than requiring a redeploy. From there, when the model decides to act, the host issues tools/call with arguments matching the advertised schema, gets structured output back, and feeds it to the model. That loop — discover, decide, call, observe — is the entirety of an agent using a tool over MCP.

Where MCP stops

Notice what we still didn’t see: we never had one agent ask another agent to accomplish a goal. Every interaction was an agent reaching down to a capability it controls, inside its own context. The CRM server isn’t an autonomous peer — it’s a typed set of actions and data, and the agent is fully in charge.

The moment you want to hand a goal to an independent agent — one you don’t control, possibly run by another team or vendor, that will work asynchronously and report back without sharing your context — MCP is the wrong tool. That’s a horizontal, peer-to-peer interaction, and it’s exactly what A2A is built for.

A2A in Practice & When to Use Which

The four core concepts

A2A is built on four objects. Learn these and the protocol falls into place:

  • Agent Card — a public JSON document, served at /.well-known/agent.json, that describes an agent: its name, version, endpoint URL, the skills it offers, the input/output formats it accepts, and how to authenticate. It’s a machine-readable business card. A client fetches it (directly or via a registry) to learn who can do what and how to connect.
  • Task — the fundamental unit of work, identified by a unique taskId. Unlike an MCP tool call, a Task has an explicit, trackable lifecycle and can run for seconds or days.
  • Message — the unit of exchange inside a Task. Each message has a role (user or agent) and a body that’s an array of Parts — which can mix text, files, binary, and structured data, making A2A multimodal by design.
  • Artifact — the output of a Task: a PDF invoice, a JSON analysis, an image. Delivered to the client when the work completes.

The interaction model is client agent → remote agent. The client delegates; the remote agent executes autonomously and returns results without ever seeing the client’s internal context, memory, or tools. That isolation is the whole point of the horizontal axis — it’s what lets agents from different vendors collaborate without trusting each other with their internals.

Discovery: the Agent Card

An agent advertises itself with a card like this, served at a well-known URL:

{
"name": "research-agent",
"description": "Performs literature reviews and synthesizes findings.",
"url": "https://research.example.com/a2a",
"version": "1.0.0",
"capabilities": { "streaming": true, "pushNotifications": true },
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["application/json", "text/markdown"],
"securitySchemes": {
"oauth2": { "type": "oauth2", "flows": { "clientCredentials": { "...": "..." } } }
},
"skills": [
{
"id": "literature-review",
"name": "Literature Review",
"description": "Given a topic, returns a synthesized review with citations.",
"inputModes": ["text/plain"],
"outputModes": ["text/markdown"]
}
]
}

A client fetches this card to decide whether this agent can help and how to call it. A2A v1.0 (early 2026) added Signed Agent Cards — a cryptographic signature proving the card was issued by the domain owner. Without it, an attacker could stand up a forged card and redirect callers to a malicious endpoint (a “card forgery” attack). For any cross-organization deployment, verify the signature.

Delegating a task

With the card in hand, the client sends a message via JSON-RPC over HTTPS. The current method is message/send (for synchronous or pollable work) or message/stream (for streaming updates over SSE):

{
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [
{ "kind": "text", "text": "Review recent work on retrieval-augmented generation." }
],
"messageId": "9c1f...e7"
}
}
}

The remote agent responds with a Task object carrying a status. Because the work may be long-running, the client tracks progress through the task lifecycle rather than blocking on a single response. Using the Python a2a-sdk, the same exchange is roughly:

from a2a.client import A2AClient

# 1. Discover: fetch and (in production) verify the signed Agent Card
client = await A2AClient.from_agent_card_url(
"https://research.example.com/.well-known/agent.json",
auth=oauth_credentials,
)

# 2. Delegate: hand over a goal, not a function call
task = await client.send_message(
"Review recent work on retrieval-augmented generation."
)

# 3. Track: poll or stream until a terminal state
while task.status.state not in ("completed", "failed", "canceled", "rejected"):
task = await client.get_task(task.id) # tasks/get
await asyncio.sleep(2)

# 4. Collect the Artifact
if task.status.state == "completed":
review = task.artifacts[0] # e.g. markdown review

For real-time progress you’d use message/stream and consume SSE events instead of polling; for fire-and-forget work you’d register a push-notification webhook so the remote agent calls you back when it’s done.

The task lifecycle

The thing that most distinguishes A2A from MCP is that work is stateful. Every Task moves through an explicit lifecycle, and long-running, human-in-the-loop, and async patterns are first-class:

A flowchart illustrating the A2A task lifecycle, showing the progression of tasks from 'submitted' to various states like 'working', 'completed', 'failed', 'canceled', and 'rejected'.

A few consequences worth internalizing. A task can pause at input-required (the remote agent needs more from you) or auth-required (it needs credentials) and resume — this is how human-in-the-loop works natively. The four terminal states (completed, failed, canceled, rejected) are final: a terminal task can’t be restarted; you start a new one. And retry and circuit-breaking are deliberately left to the client — the protocol governs state and messaging, not your resilience policy.

How the two protocols compose

Here’s where the “they’re complementary” claim becomes concrete. A2A and MCP operate on perpendicular axes, so a single agent uses both at once: it speaks A2A horizontally to its peers and MCP vertically to its own tools.

Picture an orchestrator that delegates a literature review to the research agent above. That research agent, internally, is itself an MCP host: to actually do the review, it calls an arXiv search tool, a PDF-reader tool, and a vector-store resource — all over MCP. The orchestrator never sees any of that. It handed over a goal via A2A; the tools the remote agent used to satisfy it are private, on the vertical axis, exactly where MCP belongs.

Diagram illustrating the relationship between an Orchestrator agent, a Research Agent, and a Writing Agent, highlighting their roles and private tools like arXiv API, Vector store, Docs API, and Style DB, within a framework of A2A and MCP.

That picture is the answer to “do I need both?” In any non-trivial multi-agent system: yes, and they never collide, because one runs across and the other runs down.

When to use which: the decision framework

Carry one question into every design decision: is the thing on the other end a capability, or a peer?

Reach for MCP when:

  • Your agent needs to call an API, query a database, read files, or trigger an action you control.
  • The interaction is synchronous — ask, get an answer, continue.
  • The “other side” has no agency of its own; it’s a typed set of actions and data.
  • You want the tool to operate inside your agent’s context.

Reach for A2A when:

  • You’re delegating a goal to an autonomous agent, not calling a function.
  • That agent is owned by a different team, vendor, or codebase, and you shouldn’t share your internal context with it.
  • The work is long-running, async, or needs human-in-the-loop checkpoints.
  • You want to swap one agent for another without rewriting orchestration — discovery via Agent Cards gives you that.

Use both whenever you have multiple agents that each need their own tools — which is most production systems. Coordinate across with A2A; equip each agent with MCP.

The mistakes this series exists to prevent

Each of these is a real, common error, and each is just the capability/peer question answered wrong:

  • Exposing a database or API as an “agent” over A2A. It’s a capability, not a peer. That’s an MCP server. You’ve added a coordination protocol, an Agent Card, and a task lifecycle to something that should be a synchronous tool call.
  • Hand-rolling bespoke RPC between heterogeneous agents instead of using A2A. You’ll reinvent discovery, auth, and task state — badly — and re-glue it every time an agent changes.
  • Wrapping every tool as an agent because A2A feels newer or more powerful. Over-engineering: you pay statefulness and discovery overhead for a function call.
  • Trying to use MCP to coordinate independent agents across a trust boundary. MCP has no concept of an autonomous peer working without your context; you’ll end up leaking context or faking agency.

The protocols are not rivals and the choice is rarely ambiguous once you ask the right question. MCP is how your agent talks to tools. A2A is how your agents talk to each other. Build on the correct axis and the architecture stops fighting you.