MCP sounds complicated until you actually use it.
Most explanations talk about protocols, standards, and architecture diagrams. What developers really want is simpler: how does this look in a real agent?
This post walks through a minimal TypeScript example that shows where MCP fits in an actual system.
The goal is not to build a full framework. The goal is to understand the flow.
What we are building
We will build a simple agent that answers a question like:
“Find me flights to New York.”
The agent will:
- Decide it needs a flight search tool
- Call that tool through an MCP-style interface
- Return the result
The important part is not the feature. It is the structure.
The architecture in one view
flowchart LR
A[Agent Runtime] --> B[MCP Layer]
B --> C[Tools]
C --> D[Flight API]
The runtime decides what to do. MCP provides a consistent way to call tools. The tools do the actual work.
Step 1: Define a simple tool
First, define a tool the agent can use. In a real system, this could come from an MCP server. For now, we will define it locally.
type FlightSearchInput = {
from: string;
to: string;
};
const flightSearchTool = {
name: "search_flights",
execute: async (input: FlightSearchInput) => {
return {
flights: [
{ airline: "Delta", price: 320 },
{ airline: "United", price: 290 }
]
};
}
};
This is intentionally simple. The important part is that the tool has a name and an execute function.
Step 2: Create a minimal MCP layer
The MCP layer is just a consistent interface for calling tools.
In a real system, this would connect to external MCP servers. For this example, we simulate that behavior.
class MCPClient {
private tools = new Map<string, any>();
register(tool: any) {
this.tools.set(tool.name, tool);
}
async callTool(name: string, input: unknown) {
const tool = this.tools.get(name);
if (!tool) {
throw new Error(`Tool not found: ${name}`);
}
return tool.execute(input);
}
}
This is the key idea. The agent does not call tools directly. It calls them through MCP.
Step 3: Add a simple model decision
We will simulate the model deciding what to do.
In a real system, this would be an LLM call returning structured output.
type Decision = {
action: "call_tool" | "finish";
toolName?: string;
toolInput?: unknown;
};
async function decideNextStep(userInput: string): Promise<Decision> {
if (userInput.includes("flight")) {
return {
action: "call_tool",
toolName: "search_flights",
toolInput: { from: "SFO", to: "NYC" }
};
}
return { action: "finish" };
}
The important part is structure. The model returns a decision, not just text.
Step 4: Build the runtime
Now we connect everything.
The runtime asks for a decision, validates it, and executes the tool through MCP.
async function runAgent(userInput: string) {
const mcp = new MCPClient();
mcp.register(flightSearchTool);
const decision = await decideNextStep(userInput);
if (decision.action === "call_tool") {
const result = await mcp.callTool(
decision.toolName!,
decision.toolInput
);
return {
message: "Here are your flights",
data: result
};
}
return {
message: "No action needed"
};
}
That is the full flow.
The runtime decides what to do. MCP executes the tool. The result is returned.
What this example actually shows
This example is intentionally small, but it highlights the most important idea.
MCP is not the agent.
MCP is not the runtime.
MCP is not the decision layer.
MCP is the connection layer.
It standardizes how tools are called.
Everything else is still your responsibility.
What is missing (on purpose)
This example skips several things you will need in a real system.
Authorization. Not every user should be able to call every tool.
Validation. Tool inputs should be validated before execution.
Risk control. Some tools should require approval.
Retries. External tools can fail.
Observability. You need to know what happened during a run.
Memory. The agent should not be stateless.
These are not MCP problems. These are architecture problems.
Where MCP actually helps
MCP becomes valuable when your system grows.
Instead of manually wiring every tool, your agent can discover and call tools through a consistent interface.
That becomes especially useful when:
You have many tools across different services
You want reusable integrations
You are building multi-agent systems
You want to standardize tool access across teams
At that point, MCP reduces complexity.
Final takeaway
If you strip away the hype, MCP is simple.
It gives your agent a consistent way to reach tools.
It does not decide what to do.
It does not validate actions.
It does not make your system safe or reliable.
Your runtime still owns those responsibilities.
Once you understand that boundary, MCP becomes a very useful building block instead of a confusing abstraction.
And in real systems, that clarity matters more than anything else.