Playbook: MCP in Practice — Connect an Agent to an Internal Tool
Listen to study
generated on playGenerated only on first play
Powered by Amazon Polly + OmniVoice
The Model Context Protocol eliminates the hand-rolled adapter between agent and tool: an MCP server exposes tools with name, description, and schema; the client discovers and calls them without direct coupling. This playbook covers the three concrete steps to stand up an MCP server, describe a tool properly, and connect the agent — with special attention to security, which is where most implementations fail.
Every new tool your agent needed to call became a hand-rolled adapter: code coupled to the model, hardcoded schema, authentication hacked in place. The Model Context Protocol fixes this with a standard contract — the server announces what it can do, the agent discovers and calls it. One protocol, N tools, zero rewrite when you swap models.
What you'll be able to decide and do
Quick Reference — MCP
- Creator / Origin
- Anthropic — announced November 2024, open specification
- Supported transport
- stdio (local/subprocess), HTTP + SSE (remote), WebSocket (under spec review)
- Official SDKs
- Python, TypeScript/Node, Java, Kotlin, C# (all open-source, Apache 2.0)
- AWS Integration
- Bedrock AgentCore Gateway — manages MCP servers as tool endpoints for Bedrock agents
- Protocol primitives
- Tools (calls), Resources (data/context), Prompts (templates), Sampling (server requests the model)
- Message format
- JSON-RPC 2.0 over the chosen transport
- Authentication (spec)
- OAuth 2.1 for remote servers; stdio inherits parent process context
The mental model that unlocks everything
Before MCP, integrating a tool into an agent was a three-layer problem solved by hand in every project: you needed to (1) serialize the call in the format the model expected, (2) execute the tool logic, and (3) return the result in a format the model could interpret. Each LLM had its own function-calling dialect. Swapping models meant rewriting adapters. Adding a new tool meant touching the agent's code.
MCP inverts the dependency. Instead of the agent knowing how to call each tool, the MCP server announces what it can do — and the agent discovers it at runtime via tools/list. The contract is simple: each tool has a name, a description (free text, read by the model), and an inputSchema (JSON Schema). The agent calls tools/call with the name and arguments; the server executes and returns the result. JSON-RPC 2.0 over stdio or HTTP+SSE.
The analogy I use: think of the MCP server as a microservice with a self-describing contract. The client needs no external documentation — the server itself declares its capabilities. This is what allows a generic agent (Claude, GPT-4, a model on Bedrock) to consume any tool without recompilation. The protocol does the translation work.
One detail that changes everything in practice: the agent chooses which tool to call based on the description, not the name. The model reads the description field as part of the context and decides whether that tool solves what the user asked. This means a bad description — vague, overly technical, lacking examples of when to use it — will cause the agent to ignore the tool or call it at the wrong moment. Treat the description as a fragment of the system prompt, not as a docstring.
Ad-hoc Integration vs. MCP
| Criterion | Ad-hoc Integration (hand-rolled) | MCP | |
|---|---|---|---|
| Model coupling | High — function call schema specific per LLM | Low — model-agnostic protocol | — |
| Model swap | Adapter rewrite required | Zero change to the MCP server | — |
| Tool discovery | Hardcoded in agent code | Dynamic via tools/list at runtime | — |
| Adding a new tool | Touches agent code + redeploy | New MCP server or new tool on existing server — agent unchanged | — |
| Security / access control | Implemented externally, inconsistent across tools | Auth layer at the MCP server edge (OAuth 2.1 or IAM on Bedrock Gateway) | — |
| Argument validation | Manual or absent | JSON Schema declared in contract — validatable before execution | — |
| Long-term maintenance | Grows linearly with number of tools × models | Grows linearly with tools, independent of models | — |
| When to prefer | One tool, one model, short deadline, no plan to scale | Multiple tools, multiple agents/models, or any audit requirement | — |
Why the tool description is part of the prompt — not documentation
When the agent receives the list of available tools via tools/list, the model sees something like this in context:
{
"name": "search_internal_knowledge_base",
"description": "Searches documents in the company's internal knowledge base. Use when the user asks about internal policies, HR processes, product technical documentation, or project history. Do NOT use for real-time data questions or information external to the company.",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search text in natural language" },
"department": { "type": "string", "enum": ["engineering", "hr", "finance", "all"], "default": "all" }
},
"required": ["query"]
}
}
The model reads the description field as an instruction. Three elements make an effective description:
1. When to use (positive): describe the use case in natural language, close to how the user will ask. "Use when the user asks about internal policies" is better than "searches the KB".
2. When NOT to use (negative): this reduces false positives — the agent calling the tool in wrong contexts. Spelling out the negative scope is as important as the positive.
3. Parameter descriptions in inputSchema: each description field inside the schema is also read by the model. "Search text in natural language" instructs the model not to send a technical ID where it should send a phrase.
A common mistake: copying the function name as the description. "description": "search_kb" is useless — the model already has the name. The description needs to add semantic context that the name doesn't carry.
Another mistake: descriptions that are too generic. If you have three search tools and all say "searches for information", the agent will choose randomly or always use the first one. Differentiate with precision.
The 3 Steps: From Zero to an Agent with an Internal Tool
- 1
Step 1 — Stand up the MCP server exposing your tool
Choose the transport: stdio for local tools (CLI, scripts, development), HTTP+SSE for remote tools (internal APIs, production services). For production on AWS, use HTTP+SSE behind an ALB or via Bedrock AgentCore Gateway. Install the SDK:
pip install mcp(Python) ornpm install @modelcontextprotocol/sdk(Node). Implement the minimal server (Python): ```python from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent import json app = Server("internal-kb-server") @app.list_tools() async def list_tools(): return [ Tool( name="search_internal_kb", description="Searches documents in the internal knowledge base. - 2
Step 2 — Write descriptions the agent actually uses
Effective description checklist — apply to each tool before publishing: - [ ] Use case in natural language: starts with "Use when..." or "Returns..." - [ ] Explicit negative scope: "Do NOT use when..." — at least one exclusion case - [ ] Differentiation from similar tools: if you have
search_kbandsearch_tickets, each description must make the difference clear - [ ] Parameters with semantic description: each inputSchema field has adescriptionexplaining what to put, not just the type - [ ] No unnecessary technical jargon: the model interprets the description as natural language; internal IDs and acronyms without context confuse it - [ ] Length: 2-5 sentences. More than that dilutes the signal; less is insufficient - 3
Step 3 — Connect the agent (MCP client) securely
Connect the client to the server: ``
python # Using the Python SDK as client from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client server_params = StdioServerParameters( command="python", args=["server.py"], env={"KB_API_KEY": os.environ["KB_API_KEY"]} # secrets via env, never hardcoded ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools() # discovers tools # pass tools to the LLM as part of the context``
MCP Architecture: Client ↔ Server ↔ Tools (with authorization layer)
Complete flow of a tool call via MCP in production on AWS. The agent (client) discovers and calls tools without knowing the internal implementation. Authorization happens at the MCP server edge, before any execution.
- User · request
- Application · (orchestrator)
- LLM · (Claude / Bedrock)
- MCP Client · SDK
- Bedrock AgentCore · Gateway / Auth Layer
- IAM Policy · + OAuth 2.1
- MCP Server · (tools/list, tools/call)
- Arg Validator · + Rate Limiter
- Audit Logger · (CloudWatch)
- Internal KB · Search API
- Internal DB · (read-only)
- Internal · REST API
Security: a tool is power — and power needs control
An MCP tool is not just another API endpoint. It's a capability that a language model can trigger autonomously, based on its interpretation of a natural language instruction. This fundamentally changes the threat model.
In a traditional API, the caller is a deterministic system you control. In an agent with MCP tools, the caller is a model that interprets natural language — and natural language can be manipulated. Prompt injection is the most direct attack: a malicious document in the knowledge base contains instructions for the model to call a tool with specific arguments. If the tool has too much power, the damage is real.
Three principles I apply to any MCP integration before going to production:
1. Least privilege on the server, not the agent. Restricting the agent is pointless if the MCP server has write access to the entire database. The identity running the MCP server should have only the permissions the exposed tools need — and nothing more. On AWS, this means an IAM role with a specific inline policy, not AdministratorAccess.
2. Explicit tool allowlist per agent. An MCP server can expose 20 tools. A specific agent may need 3. Don't expose all of them — configure the client to filter tools/list and present to the model only the tools relevant to the context. This reduces the attack surface and improves the quality of the model's choices.
3. Server-side validation is mandatory — not optional. The JSON Schema in the MCP contract is a description for the model, not a security barrier. The model can generate arguments that pass the schema but are semantically invalid or malicious (path traversal in a file field, SQL injection in a query field). Validate in the handler, before executing any operation with side effects.
Bedrock AgentCore Gateway adds a managed control layer: OAuth 2.1 or IAM authentication, centralized logging, and the ability to revoke access to a specific tool without changing the agent code. For enterprise environments with audit requirements, this layer is worth the additional cost.
Anti-patterns that destroy MCP integrations in production
1. Tool sprawl — a server with 30+ tools.
The model receives all tools in context. Each tool consumes tokens. With too many tools, the model gets confused, latency increases, and cost explodes. Rule: if a server has more than 10-12 tools, split by domain. Specialized agents with few tools outperform generic agents with many.
2. Vague or missing description.
"description": "tool for data" is a silent bug. The agent will call the wrong tool, at the wrong time, with wrong arguments — and you'll blame the model. The description is code. Review it with the same rigor.
3. Excessive privilege on the server.
The most dangerous mistake: the MCP server runs with admin credentials because "it's easier". A search tool doesn't need write permission. A data read tool doesn't need access to other AWS accounts. Define the minimum role before writing the first line of tool code — not after.
4. No argument validation before executing.
The JSON Schema is documentation for the model, not a security barrier. Always validate in the handler. Especially: free-text fields going into queries, file paths, or parameters that build commands.
5. Secrets as tool arguments.
If the model needs to pass an API key as an argument to a tool, the design is wrong. Secrets stay in the MCP server's environment (Secrets Manager, environment variables injected at deploy time). The model should never see or transmit credentials.
Rule of Thumb
"The tool description is a prompt. The schema is a weak barrier. Authorization lives on the server — never on the agent." If you only remember three things about MCP: (1) write the description as if it were an instruction to the model, because it is; (2) validate arguments on the server before executing anything; (3) the identity running the MCP server has least privilege — full stop.
MCP solves a real problem I've seen repeat itself in agent projects: every integration became a hand-rolled adapter, coupled to the model, impossible to reuse. The standard protocol idea is solid — and Anthropic's execution is good enough to adopt. What I do differently from the basic tutorial: I start with the description, not the code. Before writing a line of MCP server, I write the tool description in natural language and ask a colleague (or the model itself) to tell me in what situation they would call that tool. If the answer doesn't match what I want, the description is wrong — and it's easier to fix text than code. I treat the MCP server as a production microservice from day one. Logging, health check, timeout, rate limiting, least privilege. Not as an integration script that "we'll improve later". Tools in production are attack surface — and in financial systems, attack surface is regulatory risk. On AWS, I use Bedrock AgentCore Gateway for anything going to production. Managing OAuth 2.1 authentication by hand on a remote MCP server is work the Gateway already does. The cost of not having centralized logging of tool calls in an auditable environment is much greater than the cost of the service. I limit the number of tools per agent aggressively. My personal heuristic: if an agent has more than 8 tools, I question the design. It's probably one agent doing the work of two or three specialized agents. Agents with narrow scope and well-described tools are more reliable, cheaper, and easier to audit. MCP is still young — the spec is evolving, the server ecosystem is exploding (good and bad). My recommendation: adopt for internal tools where you control the server; be selective with third-party MCP servers (you're giving the model access to external systems — evaluate the risk).
Verdict
MCP is not framework hype — it's an integration contract that cleanly solves the coupling problem between agents and tools, in a model-agnostic way. The real value isn't in the protocol itself, but in what it enables: you can swap the LLM, add a new tool, or revoke access to a tool without touching the agent's code. That's the difference between an integration that scales and one that becomes technical debt. But the protocol doesn't solve security for you. A tool is power. Power without control in a natural-language-driven system is risk. Least privilege, argument validation, and audit logging are not optional — they are the price of putting an agent into production. The final rule: if your tool's description doesn't clearly explain when to use it AND when not to use it, it's not ready for production. The agent will decide based on that text. Treat it like code.
Post-mortems, ADRs and architecture deep dives in your inbox — the way an architect reads them.
No spam · unsubscribe anytime
Ask Fernando about this
Get a focused answer about this study from my AI assistant, grounded in my work.
Join the conversation
Sign in to comment
Verify your email to join in — you'll also get the newsletter. No password.