On this page
TLDR
MCP is a communication standard that lets Claude access pre-built tools, resources, and prompts from specialized servers without you writing integration code. Connect to a GitHub MCP server instead of writing GitHub API tools yourself. Servers are configured in .mcp.json (project) or ~/.claude.json (user).
What it is
Model Context Protocol (MCP) is a standardized interface for connecting Claude to external systems: files, APIs, databases, web services. Think of it as the USB-C of AI integrations. Instead of each application building custom tools, MCP defines a protocol that any tool provider implements once, and any MCP-enabled client uses immediately. A .mcp.json configuration file lists servers; each server exposes tools, resources, and prompts that Claude can invoke.
Two server categories exist: installed (community-maintained or custom, running locally) and cloud-hosted (managed by the vendor, authenticated via env vars). A local server might expose Grep, Read, Bash in the same process as your app. A cloud server (e.g. GitHub MCP) runs on the vendor's infra and authenticates via your token. Both appear in Claude's tool list identically; the distinction is who maintains the code.
The architecture is asymmetric: Claude (the client) sends tool_use blocks; the MCP server receives the request, executes the tool, and returns a result. Claude never talks directly to an external API, the server acts as a proxy. This isolation prevents token leakage (Claude never sees raw credentials), enables caching (server-side response cache), and allows request validation (the server can reject malformed calls before forwarding).
Production failures stem from stale `.mcp.json` configuration, mismatched env vars, vague tool descriptions inherited from old integrations, and forgetting that the server owns error handling. The JSON file is version-controlled; if a teammate updates GitHub's API and doesn't re-run the MCP setup, their tools silently fail. Errors must bubble to Claude as structured data, not be swallowed by the server.
How it works
At startup, your app loads .mcp.json. Each entry specifies a server: installed (local executable) or cloud (managed remote). Installed servers are spawned as child processes; cloud servers are contacted via HTTP with credential headers. Each server is interrogated: "what tools do you expose?" The server responds with names, descriptions, and JSON schemas. All tools merge into a single namespace; Claude sees them as a unified set.
When Claude's loop produces a tool_use block (e.g. {name: "Grep", input: {query: "..."}}), the MCP framework routes the request to the server that owns that tool. The server's code runs: validates input, executes the operation, returns a result. The result is wrapped in a tool_result block and appended to the message list. Claude never knows where the tool ran or what authentication was used.
Each server can expose resources: catalogs of data Claude can query without invoking a tool. A GitHub MCP server might expose a resource listing all open issues; Claude inspects that catalog to decide which tools to call. Resources reduce exploratory tool calls: instead of calling list_issues 20 times, Claude reads the resource once and calls targeted tools. Resources are read-only caches; tools are the write path.
Error handling flows back to Claude via tool_result. If the server hits an API error (timeout, 403, invalid request), it returns a structured message in content: {"error": "github_api_timeout", "hint": "retry after 30s"}. Claude reads the error and adjusts. Silent error suppression (returning empty as success) is the #1 production failure: Claude doesn't know the tool failed and re-requests indefinitely.

Where you'll see it
Team-shared GitHub integration
Project uses GitHub MCP server: PR queries, issue lists, code search, all pre-built. Configure in .mcp.json (committed to repo). Team members run claude mcp sync once. Replaces hand-rolled tool definitions and auth wrappers; tokens come from team env vars.
Custom MCP server for legacy billing
Company has a Unix-only billing API with no SDK. Build a Python MCP server wrapping the API; expose via stdio in .mcp.json. Agent calls standard MCP tools; the server hides the legacy weirdness. New team members inherit the integration automatically.
Resources for upfront schema discovery
Database MCP exposes a resources endpoint listing all tables and columns. Agent reads the resource ONCE at session start instead of calling list_tables / describe_table N times. Cuts exploratory tool calls from 8 → 0 per session.
Code examples
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"
}
},
"postgres": {
"command": "uvx",
"args": ["mcp-server-postgres", "--readonly"],
"env": {
"DATABASE_URL": "${DATABASE_URL}"
}
},
"billing-legacy": {
"command": "python3",
"args": ["./mcp-servers/billing.py"],
"env": {
"BILLING_API_HOST": "${BILLING_API_HOST}",
"BILLING_API_KEY": "${BILLING_API_KEY}"
}
}
}
}
# Minimal MCP server using the FastMCP Python library.
# Run: python billing_server.py
# Configure in .mcp.json with command: python3, args: [path/to/billing_server.py]
from mcp.server.fastmcp import FastMCP
import os
mcp = FastMCP("billing-legacy")
@mcp.tool()
def get_invoice(invoice_id: str) -> dict:
"""Fetch invoice by ID from the legacy billing API.
Use when a customer asks for invoice details. Returns
{invoice_id, customer_id, amount, status, line_items}.
"""
api_host = os.environ["BILLING_API_HOST"]
# ... call legacy API, return result
return {"invoice_id": invoice_id, "amount": 247.83, "status": "paid"}
@mcp.tool()
def issue_credit(invoice_id: str, amount: float, reason: str) -> dict:
"""Issue a credit note against an invoice. Max amount $500
without manager approval. Returns {credit_id, status}.
"""
if amount > 500:
return {"status": "blocked", "reason": "exceeds_policy"}
# ... call legacy API
return {"credit_id": "CR-9999", "status": "issued"}
@mcp.resource("billing://schema")
def get_schema() -> str:
"""Returns the billing data dictionary so the agent doesn't
need exploratory tool calls. Read once per session."""
return open("billing_schema.md").read()
if __name__ == "__main__":
mcp.run()
Looks right, isn't
Each row pairs a plausible-looking pattern with the failure it actually creates. These are the shapes exam distractors are built from.
MCP and tool-use are competing approaches; pick one.
MCP is infrastructure (who provides tools); tool-use is the mechanism (Claude calls them). They're complementary, every MCP tool fires through the tool-use protocol.
Hardcode API tokens in .mcp.json so the team has a single source of truth.
.mcp.json is committed to git. Hardcoded secrets leak. Use ${ENV_VAR} expansion; secrets stay in CI/CD or local .env.local files.
Expose 30 tools in one MCP server for completeness.
Same 18-tool degradation applies. Scope MCP servers to focused domains (one per service). Use 'resources' to expose data upfront and avoid forcing N exploratory tool calls.
Side-by-side
| Aspect | MCP server | Custom in-app tools | SDK built-ins |
|---|---|---|---|
| Setup effort | Low (config in .mcp.json) | Medium (write tool defs in code) | Zero (Read/Edit/Bash) |
| Reusability | High (any Claude client) | Per-app | Built into Claude Code |
| Best for | Standard integrations | Bespoke business logic | File ops + shell |
| Auth | Env-var expansion | Per-app config | Inherits user permissions |
Decision tree
Is this integration with a known service (GitHub, Slack, Postgres, etc.)?
Does the team need to share this integration?
Will the agent need exploratory schema/data lookups?
Question patterns

You add a new MCP server to `.mcp.json` but Claude Code doesn't see its tools. What did you forget?
.mcp.json require a restart, or a manual mcp restart command in newer versions.Two MCP servers expose tools with the same name (`Read`). What happens?
GithubRead, FileRead) or disable one of the conflicting servers.A cloud MCP server returns `{error: "rate_limited"}`. Your agent retries 5 times and exhausts budget. Better approach?
{error: "rate_limited", retry_after: 30}. Your harness reads this and waits before retrying. Don't hammer; respect the hint.You hardcoded a GitHub token in `.mcp.json`. A teammate clones the repo and your token leaks. Fix?
${GITHUB_TOKEN} env-var expansion. Each developer sets their own token in their environment. The .mcp.json is committed; the token is not.An MCP server resource lists 1000 items. Claude calls a tool 1000 times, one per item. Why?
Your custom MCP server crashes when Claude calls a tool with malformed JSON. What's the right error handling?
{error: "invalid_input", detail: "..."} as the tool_result. Claude reads it and retries with corrected input. Crashes leave the framework hanging.