If someone asked you to count the characters in a sentence, you wouldn't take a train to the next city to ask an expert. Yet plenty of agent codebases do the equivalent every day: logic that runs locally in microseconds gets wrapped in a protocol, serialized, sent to another process, parsed, and sent back. Other codebases make the opposite mistake and pull heavy, fragile dependencies into the main process until one bad library takes the whole service down. Both problems come from treating local tools, skills, and MCP as alternatives when they are layers.
Two Ways to Get It Wrong
Picking the wrong layer tends to go one of two ways, and both show up in real projects all the time.
Everything becomes an MCP server
MCP is popular, so every function gets its own server: email extraction, string length, date formatting. Each call now pays for JSON encoding, a transport hop, decoding, and a response trip back. The logs fill with serialization errors, parameter parsing exceptions, and timeouts from services that should never have been services. It's like phoning the building next door to borrow a stapler for your own office. We'll call this communication mania.
Everything goes into the main process
The opposite habit puts every capability straight into the agent's codebase, including native libraries, GPU-specific builds, and third-party SDKs with their own dependency trees. The main service gets bigger with every feature, and one dependency conflict or memory leak in a minor tool crashes the agent for everyone.
Neither team is lazy. Both are missing a clear idea of where the boundaries in an agent system should be. Experienced reviewers and interviewers check for exactly this. They don't want definitions recited. They want to see that you can tell a description from an execution from a protocol, recognize whether a requirement needs microsecond latency or process isolation, and say what each choice gains and gives up.
First, Untangle the Word "Skill"
In the agent ecosystem, "skill" now means two related things. Both live on the same side of the line.
- A capability contract. This is the name, description, and input schema the model sees for each thing it can do: the tool definition you pass in an API call. The model reads it, decides whether it fits, and emits structured arguments. It never runs code itself.
-
An Agent Skill package. Anthropic introduced Agent Skills in late
2025 and later published the format as an open standard. A skill is a folder whose
SKILL.mdhas YAML frontmatter (at minimum anameanddescription), Markdown instructions, and optional scripts, references, and assets. Agents load these through progressive disclosure: only the name and description at startup, the full instructions when a task matches, and the bundled files only when they're needed.
Both are what the model reads to understand what it can do and how. Neither
one is the mechanism that does the work. A skill's instructions might say "run
scripts/fill_form.py" or "call the search_orders tool," but
the running happens in a tool, either in-process or behind MCP. For the rest of this
post, the skill layer means both: everything the model reads about
a capability.
The Call Path, Top to Bottom
Follow a single request through the agent and the three layers line up in order:
User request
│
▼
┌──────────────────────────────────────────────────────────────┐
│ LLM (the brain) │
│ reads ──► SKILL LAYER: names, descriptions, input schemas, │
│ SKILL.md instructions │
│ emits ──► { "tool": "lookup_order", "args": {...} } │
└──────────────────────────────────────────────────────────────┘
│
▼
Agent runtime: validate args, route the call
│
├── lightweight, in-process ──► LOCAL TOOL
│ plain function, shared memory,
│ µs–ms, no serialization
│
└── heavy, external, isolated ─► MCP CLIENT ══ JSON-RPC ══► MCP SERVER
(stdio or Streamable HTTP) separate process,
own dependencies,
own failure domain
The model reads a description, decides, and emits arguments. The runtime either does the work itself or sends it to a separate program through a standard protocol. Once you see the call path like this, "MCP vs. tools" stops being a sensible question. It makes as much sense as asking whether a phone's screen or its charging cable is better.
Layer 1: Skills Are the Menu, Not the Kitchen
A restaurant menu tells you what a dish is called, what goes into it, and what will arrive at the table. It doesn't say whether the kitchen cooks on its own stove or sends out to the restaurant next door, and as a diner you don't care. The skill layer plays the same role for the model. It is the only part of the system the model sees, and it is completely separate from how the work gets done.
A capability contract, as the model sees it:
{
"name": "lookup_order",
"description": "Fetch an order's status, items, and shipping events by order ID. Use when the user asks where an order is or what it contained. Do not use for refunds.",
"input_schema": {
"type": "object",
"properties": {
"order_id": { "type": "string", "pattern": "^ORD-[0-9]{8}$" }
},
"required": ["order_id"]
}
}
And an Agent Skill package that teaches a multi-step procedure on top of tools:
refund-handling/
├── SKILL.md
├── policy.md # loaded only when the model needs the details
└── scripts/
└── eligibility.py # executed, not read into context
--- SKILL.md ---
---
name: refund-handling
description: Decide and process customer refund requests under the 2026 returns policy. Use when a user asks for a refund, return, or chargeback.
---
1. Call lookup_order to get the order and delivery date.
2. Run scripts/eligibility.py with the order JSON; it returns ELIGIBLE / PARTIAL / DENIED.
3. For PARTIAL, read policy.md section 4 before quoting an amount.
4. Never promise a refund the script did not approve.
The quality of this layer drives tool selection more than anything else does. A vague description ("handles orders") leads to wrong calls and missed calls, while a precise one with "use when" and "do not use for" lines keeps the model on track. Treat these descriptions like API documentation that a demanding, literal-minded reader will act on, because that is exactly what they are.
Layer 2: Local Tools Do Light Work, Fast
A local tool is an ordinary function in the agent's own process. It shares memory with the runtime, so calling it costs a function call: no serialization, no context switch, no network. A few dozen lines of code finish in microseconds.
Good examples include:
- Filtering sensitive words out of user input
- Converting timestamps and time zones
- Validating JSON against a schema
- Arithmetic, unit conversion, string truncation, ID formatting
- Reading state the agent already holds in memory
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
def to_local_time(epoch_ms: int, tz: str = "America/Chicago") -> str:
"""Local tool: pure, deterministic, ~microseconds. No reason to leave the process."""
dt = datetime.fromtimestamp(epoch_ms / 1000, tz=timezone.utc)
return dt.astimezone(ZoneInfo(tz)).isoformat()
The price is tight coupling. The tool ships with the agent. Changing one regex means editing the main codebase, running the full pipeline, and redeploying the service. A mature tool someone else wrote in Go or Rust can't be called directly, so you either rewrite it or work around it. That's fine for small, stable logic. It becomes technical debt when heavy or fast-changing capabilities pile up in the main process.
Layer 3: MCP Is the Cable to Everything Else
The Model Context Protocol contains no business logic. It's an agreed-upon format for sending requests and getting responses: JSON-RPC 2.0 messages between a client in your agent and a server that exposes tools (and also resources and prompts). The server runs as a separate program. Locally it's usually a subprocess speaking over stdin/stdout. Remotely it's a service speaking Streamable HTTP. Think of MCP as a standard data cable between the agent and someone else's program.
That cable buys you two things that local tools can't offer:
- Isolation. If a video transcoder with a large native toolchain spins forever, leaks memory, or segfaults, only its own process dies. The agent sees a failed call and keeps serving everyone else. The server's dependencies never touch your runtime, and you can sandbox it, limit its permissions, and run it under its own credentials.
- Reuse. Any tool that speaks the protocol plugs in without glue code: a database auditing service another team built, a vendor's official server, a tool written in a language your agent doesn't use. It can be deployed, versioned, and scaled on its own schedule.
What the cable costs
Every MCP call goes through encoding, transport, decoding, execution, re-encoding, and the trip back. A stdio hop on the same machine is small; a remote HTTP hop adds network round trips and possibly authentication. Latency isn't the main cost, though. In an agent loop where every model turn already takes seconds, a few milliseconds of IPC rarely matter. The larger costs are elsewhere:
- New failure modes. Timeouts, disconnects, partial responses, and version skew between client and server. Each one needs a timeout, a retry policy with backoff, and a circuit breaker, none of which a local function needs.
- Harder debugging. One failed task can span several processes and logs. Without trace IDs passed across the boundary, you're piecing it together by hand.
- Context cost. Every connected server's tool definitions are usually loaded into the model's context up front. Anthropic's engineering team has written about agents with many connected servers spending large numbers of tokens on tool definitions and intermediate results before doing useful work. Their remedies are loading definitions on demand and letting the agent call tools from code. Wiring in dozens of servers "just in case" makes every request slower, more expensive, and worse at picking the right tool.
- A trust boundary. A third-party MCP server is code you didn't write, running with access you granted. Vet it the way you'd vet any dependency, and scope its credentials tightly. The same applies to Agent Skills from unknown sources, since their scripts run on your machine.
The Payoff: Swap the Kitchen, Keep the Menu
Because the skill layer is separate from execution, you get a classic design benefit:
programming to an interface. The model depends on the contract, not
the implementation. On day one, search_documents can be a local function
over an in-memory index. Six months later, when the corpus has grown and retrieval
moves to its own cluster, the same contract routes to an MCP server instead. The
prompts, the descriptions, and the model's behavior stay the same. You can even split
traffic between the two backends for an A/B test.
from dataclasses import dataclass
from typing import Any, Awaitable, Callable
@dataclass
class Capability:
spec: dict # what the model reads (name, description, schema)
handler: Callable[[dict], Awaitable[Any]] # how it actually runs
# ---- Backend A: local, in-process ----
async def search_local(args: dict) -> Any:
return memory_index.search(args["query"], k=args.get("k", 5))
# ---- Backend B: remote, behind MCP (official Python SDK client session) ----
def via_mcp(session, tool_name: str, timeout_s: float = 10.0):
async def handler(args: dict) -> Any:
result = await asyncio.wait_for(session.call_tool(tool_name, args), timeout_s)
if result.isError:
raise ToolError(result.content)
return result.content
return handler
SEARCH_SPEC = {
"name": "search_documents",
"description": "Search internal policy and product documents. Use for factual questions about company policy.",
"input_schema": {"type": "object",
"properties": {"query": {"type": "string"}, "k": {"type": "integer"}},
"required": ["query"]},
}
# Day 1
registry = {"search_documents": Capability(SEARCH_SPEC, search_local)}
# Month 6: same spec, different kitchen. The model never notices.
registry["search_documents"] = Capability(SEARCH_SPEC, via_mcp(retrieval_session, "search"))
Isolation gets the attention, but this decoupling is what makes long-lived agent systems maintainable. You can refactor, migrate, and run experiments in the execution layer without retuning prompts, because the layer the model sees doesn't change.
Side by Side
| Dimension | Skill layer | Local tools | MCP |
|---|---|---|---|
| What it is | Metadata and instructions the model reads | Native function in the agent's process | Cross-process communication protocol |
| Executes code? | No; it describes (bundled scripts run through a tool) | Yes, in-process | No; the server it talks to does |
| Call overhead | None at call time; costs context tokens | Microseconds to milliseconds | IPC or network round trip plus serialization |
| Coupling | Decoupled from implementation | Tight: ships with the agent | Loose: deployed and versioned separately |
| Failure blast radius | Bad descriptions cause wrong tool choices | A crash can take down the agent | Confined to the server process |
| Operational burden | Writing and evaluating descriptions | Lowest | Timeouts, retries, breakers, tracing, auth |
| Core value | One consistent way for the model to find and use capabilities | Fast, simple, frequently called logic | Safe, standard access to external and heavy capabilities |
In a design review or interview, answering along these lines (what it is, overhead, coupling, blast radius, and value) covers what a skeptical reviewer will ask.
When to Go Local
Keep a capability as a local tool when it has all four of these traits:
- Lightweight logic. It fits in a function, not a subsystem.
- No unusual dependencies. Standard library or packages you already ship.
- High call frequency. It runs many times per task, so overhead adds up.
- Stable. Once written, it won't change for months.
String handling, simple status lookups, math, and format validation all qualify. Don't wrap them in MCP to look modern. Over-engineering turns simple things into distributed-systems problems and adds communication and debugging costs with nothing to show for them.
When to Go MCP
Any one of these is reason enough to move a capability behind MCP:
- Heavy or specialized environment. It needs native libraries, a specific compiler toolchain, GPU acceleration, or a dependency tree that would conflict with yours. Isolate it.
- It already exists. A mature program or an official vendor server already does the job. Connect to it instead of rewriting it.
- Independent lifecycle or language. Another team owns it, writes it in Go or Rust, and ships weekly. It needs to deploy without restarting your agent.
- Permission isolation or blast radius. It touches dangerous operations (shell, production databases, payments) or untrusted input. Sandbox it with its own credentials so a failure stays contained.
A Decision Tree for New Tool Requests
When a product manager asks for a new capability, go through the questions in order. Heavy before light, external before internal:
New capability requested
│
▼
Needs heavy dependencies or a special runtime? ──── yes ──► MCP server
│ no
▼
A mature external program/server already exists? ── yes ──► MCP (reuse it)
│ no
▼
Separate team, language, or release cadence? ─────── yes ──► MCP server
│ no
▼
Dangerous permissions or needs a sandbox? ────────── yes ──► MCP server
│ no
▼
Lightweight, stable, called often? ───────────────── yes ──► Local tool
│ no / unsure
▼
Start local behind a clean contract; move to MCP when one of the above becomes true.
Whichever branch you land on: expose it through the SKILL LAYER
(clear name, "use when / don't use for" description, strict input schema).
The last branch matters. If you're unsure, start local, because the contract makes the later move cheap. Pulling an over-built microservice back into the main process is much harder than splitting out a well-encapsulated function when you actually need to.
What a Healthy Topology Looks Like
In a healthy system, a single task makes a handful of local calls that finish in microseconds and a few deliberate MCP calls to services that justify being separate. Communication mania looks very different. One model turn sets off a dozen handshakes with tiny services, every outage turns into a hunt through distributed traces, and minor network jitter produces a wall of timeouts.
- Model-facing layer: uniform, well-described, schema-validated contracts.
- Simple internal logic: local functions, called directly.
- Heavy, external, or risky work: MCP servers with timeouts, retries, circuit breakers, and trace propagation.
- Context budget: only the skills and servers relevant to the agent's job, with on-demand loading when the list gets long.
A good architecture doesn't need to look impressive. It needs the parts that should be fast to be fast, and the parts that should be stable to be stable. (For the reliability patterns around remote calls, see engineering consistency in multi-agent systems.)
The Agent as a Body
If an agent were a person, local tools would be its internal organs and reflexes: always there, very fast, and the baseline of what it can do. Skills would be its business card and handbook, telling the brain what it's capable of and how to use each capability, without the brain worrying about mechanics. MCP would be a set of standard sockets that let it plug into the rest of the world safely.
Closing Thoughts
MCP was never meant to replace local tools or skills. It filled in the missing piece: a standard, isolated way for agents to reach the outside world. The skill layer tells the model what's possible, local tools do the light work quickly, and MCP brings in heavy and external capabilities without bringing their risks along.
Protocol names will change; something will eventually follow MCP. The trade-offs between latency, coupling, isolation, and operational cost will stay the same. Good architects weigh those trade-offs for each capability instead of following the newest protocol or buzzword. If you can explain why a capability belongs in its layer and what that choice costs, you'll do well in both design reviews and interviews.
Sources and further reading:
Model Context Protocol specification ·
Agent Skills open standard ·
Anthropic: Code execution with MCP
Related reading:
AI agents are just backend microservices ·
Why multi-agent systems contradict themselves ·
From prompt engineering to graph engineering ·
Input and output guardrails in AI agents ·
Nine agent and RAG interview questions