Out of the box, a coding agent is a remarkably capable but oddly isolated colleague. It can read and write files in the working tree, run shell commands, and reason about whatever it can see on disk. That covers a surprising amount of real work, but there is a glass wall around it. The moment a task depends on something outside the repository (the ticket that prompted the change, the schema of the database the code talks to, the error rate that spiked in production last night), the agent is reduced to guessing, or to asking you to paste context in by hand.
This is the difference between an agent that drafts code and an agent that does your job alongside you. The gain comes less from a cleverer model than from reach. An agent that can query the issue tracker, inspect the staging database, read the internal API documentation, and check the observability dashboard is operating with the same situational awareness an engineer has. The architectural question I want to look at here is how you grant that reach deliberately, without turning your agent into an unbounded, unaccountable actor inside your infrastructure.
The mechanism the ecosystem has converged on is the Model Context Protocol. It repays proper study, because it is the seam where your agent meets your stack, and seams are where both the value and the risk concentrate.
What MCP actually is
The Model Context Protocol (MCP) is an open standard for connecting an agent to external systems. Strip away the framing and it is an adapter layer: a uniform contract that lets the agent talk to a server (a small process that sits in front of some system you care about) without the agent needing to know anything about that system's bespoke API, authentication scheme, or wire format. The server speaks MCP on one side and your issue tracker, database, or internal service on the other.
An MCP server exposes two kinds of capability, and the distinction matters more than people initially expect:
- Tools are actions: things the agent can do. Create a ticket, run a query, open a pull request, post a message. A tool has a name, a description the model reads to decide when to call it, and a typed input schema. When the agent calls a tool, your server executes it and returns a structured result.
- Resources are data, meaning things the agent can read. A design document, a database schema, the contents of a wiki page, a configuration file. Resources are addressable, fetched on demand, and do not (by themselves) change anything.
The reason this split is more than tidiness: it maps almost exactly onto the security boundary you will want to reason about later. Reads are cheap to grant and easy to live with; actions are where blast radius lives. Keeping them as distinct concepts in the protocol means you can think about them as distinct concepts in your threat model.
The deeper point is that capabilities become pluggable rather than hardcoded. Without a standard, every integration is a one-off: a custom shim wired into the agent, maintained forever, incompatible with the next agent you try. With MCP, the server is the unit of reuse. Write a server for your internal deployment API once, and any MCP-aware agent can use it, whether that is Claude Code today or something else tomorrow. The adapter is the asset, and it outlives the tool that consumes it.
An agent without reach is a brilliant intern locked in a room with a laptop and no network. MCP opens the door, which is exactly why you should care who holds the key.
Local servers versus remote servers
MCP servers come in two broad shapes, and the choice between them is an early, consequential decision.
A local server runs as a child process on the developer's own machine, communicating over standard input and output (stdio). The agent spawns it, talks to it through pipes, and tears it down when the session ends. Local servers are simple to reason about: they inherit the developer's environment, their credentials, and their network position. A local server that wraps a CLI tool the engineer already has installed is often the path of least resistance: there is no deployment, no shared state, and the trust boundary is just "this developer's laptop."
A remote server runs as a long-lived service, typically reached over HTTP, and is shared across a team. This is where MCP starts to look like real infrastructure. A remote server for your issue tracker can hold a single service credential, enforce consistent scoping for everyone, log every call centrally, and be patched in one place. The cost is that it is infrastructure: it needs to be deployed, authenticated, monitored and, above all, treated as part of your attack surface, because it now sits between many agents and a real system.
The trade-off is the familiar one between convenience and control. Local servers are fast to adopt and naturally scoped to one person; they are also unmanaged, duplicated across every machine, and only as trustworthy as each laptop. Remote servers centralise governance and auditing at the price of operational weight. A pragmatic pattern is to prototype locally (wrap the system, prove the agent uses it well) and promote to a shared remote server once the integration earns its keep and several people want it.
Registering a server
Configuration is deliberately declarative. Rather than scattering integration logic through the agent, you describe the servers you want available, usually in a project-level configuration file checked into the repository, so the whole team shares the same set. The agent reads that manifest at startup and makes the declared tools and resources available.
Conceptually, a configuration entry registers a server, says how to start it (a command for a local server, a URL for a remote one), and passes any environment it needs. A project configuration might look like this:
{
"mcpServers": {
"issues": {
"command": "issue-tracker-mcp",
"args": ["--project", "PLATFORM"],
"env": {
"ISSUE_TRACKER_TOKEN": "${ISSUE_TRACKER_TOKEN}"
}
},
"metrics": {
"type": "http",
"url": "https://mcp.internal.example/observability",
"headers": {
"Authorization": "Bearer ${OBSERVABILITY_TOKEN}"
}
}
}
}
Two things are worth noticing even in a sketch this small. First, the local issues server is launched as a command and handed a token through the environment. The secret itself never appears in the file, only a reference to a variable resolved at runtime. Second, the remote metrics server is reached by URL with an explicit authorization header. The exact field names vary between tools and versions, so treat this as illustrative rather than canonical; the shape (name, transport, credentials by reference) is the durable part. Keep real secrets in your environment or a secrets manager, never committed alongside the manifest.
Build a tool, or just let it run a CLI?
Not every integration deserves a purpose-built MCP server. The agent can already run shell commands, and a great many systems already ship a perfectly good command-line client. If your team uses a CLI for the issue tracker, the agent can simply invoke it. No server, no schema, nothing to maintain. For a one-off or low-stakes integration, that is frequently the right answer, and reaching for MCP would be over-engineering.
A dedicated MCP tool earns its complexity when you need one or more of the following:
- Typed inputs. A tool declares a schema, so the agent is steered toward well-formed calls and malformed ones are rejected before they execute. Without that, the agent guesses at flags and you find out about the mistake from the shell's error output.
- Scoped permissions. A CLI typically runs with whatever the developer can do. A purpose-built tool can expose exactly one narrow capability, such as "read this table" rather than "run arbitrary SQL", and nothing more.
- Structured output. A tool returns data the agent can consume directly, rather than text the agent has to parse out of a terminal dump, which means less ambiguity and fewer failed calls.
- A gate the harness can see. A discrete, named action is something your tooling can intercept, log, require confirmation for, or refuse. An opaque
bashstring is far harder to govern, because every action looks identical from the outside.
A minimal tool definition makes the contract concrete. The model reads the description to decide when to reach for the tool, so write it to state the trigger condition, not merely what the tool does:
{
"name": "search_issues",
"description": "Search the issue tracker. Call this when the user
refers to a ticket, a bug report, or work that may
already be tracked — before assuming none exists.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Free-text search over issue titles and bodies"
},
"status": {
"type": "string",
"enum": ["open", "closed", "all"],
"description": "Restrict results by issue status"
}
},
"required": ["query"]
}
}
The schema does double duty: it constrains what the agent can send, and it documents the tool well enough that the agent uses it correctly without trial and error. Notice there is no delete_issue here and no free-form SQL; the surface is deliberately small. That restraint is what the next section is about.
MCP is part of your attack surface
Everything that makes MCP valuable also makes it dangerous, and the framing from the trust-boundary thinking in Part 3 applies directly here. In that piece the rule was simple: treat the model's output as untrusted, and put the guardrails in the harness (the layer that actually executes) rather than hoping the model behaves. MCP servers are that executing layer for everything beyond the local filesystem. A tool you expose is a capability the agent can invoke, and the agent's decisions are shaped by content it reads, including content from resources that an attacker may control. The trust boundary does not move because the action now travels over a protocol; it is exactly where it always was.
Several principles follow, and none of them are optional once a server can touch a real system:
- Authenticate every server. A remote MCP server is a service that brokers access to something valuable. It must verify who is calling it, and it must hold its own credentials securely. An unauthenticated server reachable on the network is an open door to whatever it fronts.
- Grant least privilege. Scope each server's credential to the minimum it needs. If the agent's job is to read tickets, the token behind the issues server should not be able to delete projects. The blast radius of a compromise, or simply of a confused agent, is bounded by what you handed the server, not by what you intended it to do.
- Do not expose destructive actions casually. Every irreversible capability you add (drop a table, force-push, send a customer email, tear down an environment) is one that can fire on a misread instruction or a poisoned resource. Default to read-only. Promote an action to "available" only when the value is real and the action is either reversible or gated behind explicit human confirmation.
- Treat a remote server as attack surface. It is a long-lived service holding credentials and accepting input shaped by an LLM. Apply the same scrutiny you would to any internet-adjacent service: patch it, monitor it, log its calls, and review what it is allowed to do. A tool description or a resource the agent reads can carry injected instructions; the real defence is a server that simply cannot perform the dangerous action, rather than a model you hope will ignore them.
The mental model that keeps you safe is to design the server as if the agent calling it might, on any given turn, be acting on an adversary's instructions. Most of the time it will not be. But a server scoped, authenticated, and stripped of destructive verbs will hold up on the day it is. A server wired with broad credentials and a run_arbitrary_command tool, on the other hand, is one cleverly worded resource away from a very bad afternoon.
Where this leaves us
The agent's power is bounded by what it can reach, and MCP is the deliberate, governable mechanism for extending that reach into your real systems. The protocol's split between tools and resources mirrors the action-versus-data line you should be drawing in your threat model. The local-versus-remote choice is a trade between convenience and control, and it should be made on purpose. And the decision to build a typed, scoped tool rather than lean on a CLI is justified precisely when you need the gate, the schema, or the narrow permission that a raw shell command cannot give you.
The throughline is that extension is an architectural act, not a feature you switch on. Each server you add is a new edge into your stack, and the engineering discipline lies in adding exactly the edges you need, scoped exactly as tightly as the work allows, and treating every one of them as something an adversary would love to find unguarded. Do that, and the agent stops being a clever drafter and becomes a genuine participant in your systems. The next article turns from capability to cadence: how to wire this newly-connected agent into the rhythm of your workflow so it does useful work without being micromanaged.