Build MCP Server Claude: A Production Engineering Guide
Learn how to build MCP server Claude integrations that are secure, observable, and exam-ready. Covers transport, tool design, error handling, and CCAR-F Domain 2 skills.
By Solomon Udoh · AI Architect & Certification Lead

When you build MCP server Claude integrations for production, three decisions dominate everything else: transport choice, tool schema design, and error signalling. Get those three right and the rest of the system follows naturally. Get them wrong and you spend weeks chasing misrouted tool calls and silent failures. This guide walks through each decision with concrete patterns, security considerations, and the exam-relevant reasoning that Domain 2 of the CCAR-F tests.
The Model Context Protocol (MCP) is Anthropic's open standard for connecting Claude to external systems. As of 12 March 2026, it sits at the heart of the Claude Partner Network's $100M programme and is tested directly in Tool Design & MCP Integration, which carries 18% of the CCAR-F Architect exam.
What is the MCP architecture and how does Claude use it?
MCP separates concerns cleanly: a host (Claude Desktop, a Claude agent, or your own application) connects to one or more servers that expose tools, resources, and prompts. Claude never calls your internal APIs directly. It calls tools declared by the MCP server, and the server executes against your systems. That indirection is both the security boundary and the observability hook.
Claude (host)|| MCP protocol (JSON-RPC 2.0)vMCP Server (your code)|+-- tools/ (actions Claude can invoke)+-- resources/ (read-only content catalogs)+-- prompts/ (reusable prompt templates)|vInternal APIs, databases, file systems
The three primitive types map to distinct use cases:
| Primitive | Direction | Typical use |
|---|---|---|
| Tool | Model-initiated call | Write actions, queries, computations |
| Resource | Server-exposes, model reads | Documentation, product catalogs, config |
| Prompt | Server-exposes, user selects | Reusable workflow templates |
For most production integrations, tools carry the bulk of the work. Resources are underused; they are the right answer when Claude needs to browse a content catalog without triggering a write action. See MCP Resources for Content Catalogs for the exam-relevant distinction.
Which transport should you choose: stdio, SSE, or Streamable HTTP?
Choose stdio for local tools (Claude Desktop plugins, developer workstations). Choose Streamable HTTP for any server that multiple clients or agents will reach over a network. SSE (Server-Sent Events) was the original remote transport; the MCP spec has since moved toward Streamable HTTP, which replaces the SSE session model with a cleaner request/response plus streaming envelope. If you are migrating an SSE-era server, the main breaking point is session initialisation: SSE used a persistent event stream from the first connection, whereas Streamable HTTP negotiates capabilities per-request and streams only when the response warrants it.
# Minimal Streamable HTTP MCP server (Python, using the official MCP SDK)from mcp.server.fastmcp import FastMCPapp = FastMCP("internal-data-server")@app.tool()def get_customer(customer_id: str) -> dict:"""Return a customer record by ID. Read-only. Never returns PII beyond name and tier."""record = db.fetch_customer(customer_id)if record is None:return {"isError": True, "error": "NOT_FOUND", "customer_id": customer_id}return {"name": record.name, "tier": record.tier}if __name__ == "__main__":app.run(transport="streamable-http", host="0.0.0.0", port=8080)
The transport="streamable-http" argument is the only change needed from an SSE setup in the official Python SDK. The capability handshake is handled by the framework.
How do you write tool descriptions Claude will actually use correctly?
Tool descriptions are the primary selection mechanism. Claude reads the description to decide which tool to call; it does not inspect your source code. A vague description produces misrouted calls. A precise description produces reliable routing. This is the single highest-leverage improvement in most MCP deployments, and it is tested directly in the CCAR-F exam under Tool Descriptions as Selection Mechanism.
The pattern that works:
- State what the tool does in one sentence.
- State what it does not do (the negative constraint eliminates the most common misrouting).
- State the preconditions (what must be true before calling).
- State the side effects (read-only vs. write, idempotent vs. not).
@app.tool()def update_order_status(order_id: str, new_status: str) -> dict:"""Updates the fulfilment status of an existing order.Use this tool ONLY when the user has explicitly confirmed they want to changean order's status. Do NOT use this to look up order information; useget_order_details for reads.Preconditions: order_id must exist; new_status must be one of['processing', 'shipped', 'delivered', 'cancelled'].Side effects: WRITE. Not idempotent if transitioning from 'shipped' to'cancelled' (triggers a refund workflow)."""...
Tools are selected by the model based on their descriptions. A well-written description is the difference between a tool that works and one that causes subtle, hard-to-debug misrouting.
When two tools have overlapping descriptions, Claude will route inconsistently. The fix is Tool Splitting for Specificity: split the ambiguous tool into two narrower ones, each with a description that covers exactly one case. This is a low-effort, high-leverage change that the exam rewards.
How do you handle errors in MCP tool responses?
MCP defines a specific error signalling pattern that differs from HTTP status codes. A tool response can succeed at the protocol level (HTTP 200, no JSON-RPC error) while reporting a domain-level failure. The correct mechanism is the isError flag in the tool result content.
{"content": [{"type": "text","text": "{\"isError\": true, \"error\": \"RATE_LIMITED\", \"retry_after_seconds\": 30}"}],"isError": true}
Setting isError: true at the result level tells Claude that the tool call failed and that it should reason about recovery rather than treating the response as a successful result. Omitting it and returning an error message as plain text is the Silent Suppression Anti-Pattern: Claude may interpret the error text as a valid answer and continue incorrectly.
The four error categories the exam tests are:
| Category | Example | Recommended response |
|---|---|---|
| Access failure | Auth token expired | isError: true, structured metadata, do not retry without refresh |
| Valid empty result | Query returns zero rows | isError: false, empty array, explicit "no results" message |
| Transient failure | Network timeout | isError: true, retry_after hint |
| Permanent failure | Record not found | isError: true, no retry hint |
The distinction between an access failure and a valid empty result is one of the most commonly tested scenarios. See Access Failure vs Valid Empty Result for the full decision tree.
How do you secure an MCP server against prompt injection and over-exposure?
Security is the most active concern in production MCP deployments. Three threat vectors dominate:
Prompt injection via tool results. If your tool returns content from an untrusted source (a web page, a user-submitted document, a third-party API), that content can contain instructions that attempt to hijack Claude's behaviour. The mitigation is to sanitise tool outputs before returning them, and to structure results so that data and instructions are clearly separated.
import redef sanitise_tool_output(raw: str) -> str:# Strip any text that looks like a system-level instructioncleaned = re.sub(r"(?i)(ignore previous|you are now|system:)", "[REDACTED]", raw)return cleaned
Over-exposed tool surface. Every tool you register is a potential action Claude can take. Limit the tool set to what the current session actually needs. The MCP Scoping Hierarchy defines three levels: user-level, project-level, and global. Prefer project-level scoping so that a tool registered for one workflow is not available to unrelated sessions.
Cross-tenant leakage. In multi-tenant deployments, a tool that accepts a customer_id parameter must validate that the authenticated principal is authorised for that customer. Never trust the customer_id value Claude passes; validate it server-side against the session's auth context.
@app.tool()def get_account_balance(customer_id: str) -> dict:"""Returns account balance for the authenticated customer."""# Server-side authorisation check - never skip thisif not auth_context.can_access(customer_id):return {"isError": True, "error": "FORBIDDEN"}return {"balance": db.get_balance(customer_id)}
Environment variables for secrets should always be injected at runtime, never hardcoded. The Environment Variable Expansion in MCP Config pattern covers the correct claude_desktop_config.json approach for local servers.
How do you configure MCP servers in Claude Desktop and Claude Code?
Claude Desktop reads server configuration from claude_desktop_config.json. Claude Code uses a three-level hierarchy (user, project, system) described in Three-Level Configuration Hierarchy.
A minimal claude_desktop_config.json entry for a local stdio server:
{"mcpServers": {"internal-data": {"command": "python","args": ["/path/to/server.py"],"env": {"DB_URL": "${INTERNAL_DB_URL}","API_KEY": "${INTERNAL_API_KEY}"}}}}
For a remote Streamable HTTP server:
{"mcpServers": {"internal-data-remote": {"url": "https://mcp.internal.example.com/mcp","headers": {"Authorization": "Bearer ${MCP_TOKEN}"}}}}
The ${VAR} syntax expands from the shell environment at startup. Never commit real credentials to this file; the variable expansion exists precisely to keep secrets out of version control.
How do you observe and debug MCP tool calls in production?
Observability in MCP systems has three layers: protocol-level logging, tool-level structured output, and agent-level tracing.
At the protocol level, the MCP SDK emits JSON-RPC request and response objects. Pipe these to your log aggregator with a correlation ID that spans the full agent turn.
import loggingimport uuidlogger = logging.getLogger("mcp.server")@app.tool()def search_products(query: str, limit: int = 10) -> dict:trace_id = str(uuid.uuid4())logger.info({"event": "tool_call", "tool": "search_products","query": query, "trace_id": trace_id})results = catalog.search(query, limit=limit)logger.info({"event": "tool_result", "count": len(results),"trace_id": trace_id})return {"results": results, "trace_id": trace_id}
Returning trace_id in the tool result means Claude can include it in any structured output it produces, linking the agent's reasoning to the server-side log entry. This is the pattern the exam rewards when it asks about diagnosing partial failures in multi-step agentic workflows.
For multi-server deployments, tool name collisions are a silent failure mode. If two servers register a tool called search, Claude's selection is non-deterministic. Use namespaced tool names (catalog_search, docs_search) and document the namespace convention in each server's description.
Should you build a new MCP server or use an existing one?
The Build vs Use Decision for MCP Servers framework reduces to three questions:
- Does a well-maintained community server already cover the integration (GitHub, Slack, Postgres, etc.)? If yes, use it and invest your time in tool description tuning and security configuration.
- Does your integration require proprietary business logic, internal auth, or data that cannot leave your perimeter? If yes, build.
- Is the integration a thin wrapper around a public REST API with no auth complexity? Consider whether a generic HTTP tool with a well-scoped system prompt is sufficient before writing a full server.
The right question is not "can I build an MCP server for this?" but "does building one produce better tool descriptions, better error handling, and better security than the alternatives?"
The exam consistently rewards proportionate solutions. A custom MCP server is the right answer when it produces meaningfully better tool descriptions, tighter auth, or cleaner error semantics than the alternative. It is not the right answer when an existing server or a direct API call would serve equally well.
How does MCP integration connect to the CCAR-F exam?
Domain 2 (Tool Design & MCP Integration) carries 18% of the CCAR-F exam weight. The task statements it tests map directly to the patterns in this guide: writing effective tool descriptions, handling the four error categories, applying the isError flag correctly, scoping MCP servers to the right level, and deciding when to build versus use.
The exam is scenario-based. A typical Domain 2 item presents a deployed MCP server with a symptom (misrouted calls, silent failures, cross-tenant data appearing in responses) and asks which single change fixes the root cause. The answer is almost always one of: improve the tool description, add the isError flag, tighten the scoping, or add server-side authorisation validation. The exam rewards root-cause tracing and proportionate fixes over broad rewrites.
The full concept map for Domain 2 is available in our Tool Design & MCP Integration concept library, which covers 174 atomic concepts mapped to the five exam domains and 30 task statements. Our adaptive engine uses Bayesian Knowledge Tracing with a 0.90 mastery threshold to surface the concepts where your gap is largest before exam day.
AI Skill Certs is an independent prep platform; we are not affiliated with or endorsed by Anthropic.
Frequently asked questions
How long does it take to build a working MCP server for Claude?
What programming languages can I use to build an MCP server?
Can I run multiple MCP servers with Claude at the same time?
Does building an MCP server require a Claude API key?
How is MCP tested on the CCAR-F Architect exam?
What is the difference between MCP tools, resources, and prompts?
People also ask
How do I connect Claude to my own database using MCP?
What is the MCP isError flag and why does it matter?
Is MCP secure enough for production use with sensitive data?
What is the difference between MCP stdio and HTTP transport?
How do I debug MCP tool calls that are failing silently?
About the author
AI Architect & Certification Lead
Solomon Udoh is an AI Architect who designs and ships production agent systems on the Claude API and Claude Code. He built AI Skill Certs' adaptive engine and authored its 174-concept knowledge graph, mapping every Claude Certified Architect - Foundations objective to hands-on, exam-aligned practice.
- Designs production multi-agent systems on the Claude API and Agent SDK
- Author of the AI Skill Certs knowledge graph (174 mapped exam concepts)
- Builds with MCP, Claude Code, structured outputs, and agentic loops daily
- Reviews every concept page against the official Anthropic exam guide
You might also like
Ready to put it into practice?
Study every exam concept with an adaptive tutor.