On this page
- Why MCP exists
- Function Calling vs MCP
- Why MCP became the industry standard
- The protocol model: host, client, server
- Lifecycle and capability negotiation
- Transports: stdio and Streamable HTTP
- Server primitives: resources, tools, and prompts
- Client primitives: sampling and elicitation
- Hello World MCP Server
- Python
- TypeScript
- Production engineering guidance
- Security engineering checklist
- How Hermes Uses MCP
- Implementation guidance
- Common Implementation Mistakes
- Treating MCP as only function calling
- Mixing Resources and Tools
- Skipping capability negotiation
- Over-trusting servers
- Poor permission handling
- Missing timeout and retry logic
- Lack of audit logging
- Weak security boundaries
- FAQ
- Is MCP the same thing as function calling?
- When should I use resources instead of tools?
- What is the difference between stdio and Streamable HTTP?
- What security controls should every MCP implementation include?
- References
Model Context Protocol is a standard interface between AI applications and external systems. It defines how hosts, clients, and servers negotiate capabilities, exchange JSON-RPC messages, expose tools, share resources, offer prompts, request sampling, elicit user input, and preserve user control across local and remote integrations.
Production MCP architecture: the host is the policy center, MCP clients maintain JSON-RPC sessions, MCP servers expose typed capabilities, and external systems stay behind server-owned boundaries.
Why MCP exists
MCP matters because every production AI system eventually needs context outside the model window. The official introduction defines MCP as an open-source standard for connecting AI applications to external systems such as files, databases, search engines, calculators, and reusable workflows (MCP introduction). The problem is not just “call an API.” The hard part is making many clients and many external systems agree on discovery, execution, permissions, result formats, and user control without a custom integration for every pair.
The specification frames MCP as an open protocol that integrates LLM applications with external data sources and tools through a standardized JSON-RPC interface (MCP specification). The “USB-C for AI” analogy is useful because MCP standardizes shape, negotiation, transport, capabilities, primitives, notifications, and security expectations. For senior engineers, the important point is that MCP is infrastructure, not a feature flag. It affects how an AI product handles trust boundaries, latency, observability, deployment topology, and long-term integration maintenance.
MCP also matters because production AI systems rarely have one integration. A senior platform team may need IDE assistants, enterprise knowledge search, database metadata, incident management, SaaS actions, browser automation, and internal developer-platform workflows. Without a shared protocol, each host needs a separate adapter for each system. With MCP, the host can connect to multiple servers through a consistent client-server model, discover capabilities dynamically, and enforce a common policy layer over different backends (architecture overview).
Function Calling vs MCP
| Dimension | Function calling | MCP |
|---|---|---|
| Primary abstraction | A model calls functions exposed by one application or provider runtime. | A host connects to servers that expose protocol-defined capabilities. |
| Scope | Usually tool invocation. | Tools plus resources, prompts, sampling, elicitation, lifecycle, transports, notifications, and utilities. |
| Discovery | Often static or application-specific. | Standardized list and capability discovery such as tools/list, resources/list, and prompts/list. |
| Ownership | The host or model provider usually owns the function registry. | Each server owns its capability surface; the host owns policy and routing. |
| User control | Implemented by the application. | Built into the interaction model: tools are model-controlled, resources are application-driven, prompts are user-controlled, and sensitive elicitation has explicit constraints. |
| Best fit | A small set of functions inside one product. | Multi-system AI applications that need consistent integration, permission, and observability boundaries. |
Engineering insight: Function calling is a useful execution primitive. MCP is an integration protocol. Treating them as the same thing usually leads teams to miss resource modeling, capability negotiation, server isolation, and user-consent design.
Why MCP became the industry standard
- November 2024: Anthropic introduced MCP as an open standard for secure two-way connections between AI-powered tools and external data sources (announcement).
- 2025: The ecosystem moved from one-off tool adapters toward MCP servers and SDKs because teams needed reusable integrations across IDEs, agents, desktop apps, and internal platforms.
- 2025-2026: The protocol matured around lifecycle negotiation, stdio and Streamable HTTP transports, server primitives, client primitives, and security guidance, making it practical for production platform teams rather than only demos (MCP specification).
- Today: MCP is the default architecture to evaluate when an AI product needs many external systems, because it standardizes the boundary while leaving each host free to enforce its own policy and user experience.
The protocol model: host, client, server
The official architecture describes three participants: the MCP host, MCP client, and MCP server (architecture overview). A host is the user-facing AI application, such as an IDE, desktop assistant, chat product, or agent runtime. A client is the connector inside that host that maintains one connection to one server. A server is the program that provides context and capabilities to that client.
Overall MCP architecture:
Overall MCP architecture rendered as a diagram: a user-facing host coordinates isolated client sessions with MCP servers and external systems.
MCP has two conceptual layers. The data layer defines JSON-RPC messages, lifecycle management, server primitives, client primitives, notifications, and utility features (architecture overview). The transport layer defines how those messages move between participants, including local subprocess communication and remote HTTP communication (architecture overview). This split is useful because the same protocol semantics can run over stdio for local tools or over Streamable HTTP for remote services.
In production, this means the host should own routing and policy rather than letting each server talk directly to the model. For example, an enterprise coding assistant might connect one client to a local Git server over stdio, one client to a remote incident-management server over Streamable HTTP, and one client to a database-metadata server. The host can expose read-only resources automatically, require confirmation for deploy or ticket-update tools, and keep each server's credentials and audit trail separate.
A design tradeoff appears here: MCP makes server integration composable, but it does not remove the need for host-level governance. If a host connects twenty servers and blindly exposes every tool to the model, the system becomes powerful but fragile. Mature hosts maintain an inventory of servers, trust levels, capabilities, permissions, timeouts, and audit policies. That inventory becomes the control plane for multi-server orchestration.
Engineering insight: The safest MCP hosts do not ask “which tools exist?” first. They ask “which server owns this capability, what trust level does it have, and what policy applies before the model can use it?”
Lifecycle and capability negotiation
Every MCP session begins with initialization. The lifecycle specification says initialization must be the first interaction between client and server, and it establishes protocol version compatibility, exchanges capabilities, and shares implementation details (lifecycle). The client sends an initialize request with its protocol version, capabilities, and client information; the server responds with a protocol version, server capabilities, server information, and optional instructions (lifecycle).
MCP lifecycle rendered as a sequence diagram: initialization, capability negotiation, discovery, approval, and tool execution.
The initialize request is used when the client first connects. In production, this request is where the host declares only the client-side features it is prepared to support. If the host does not want a server to request LLM sampling, it should not declare sampling. If the host cannot safely render URL-mode elicitation, it should not declare URL-mode elicitation.
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {
"roots": {
"listChanged": true
},
"sampling": {
"tools": {}
},
"elicitation": {
"form": {},
"url": {}
}
},
"clientInfo": {
"name": "enterprise-ai-host",
"title": "Enterprise AI Host",
"version": "2.4.0"
}
}
}
A typical server response declares the primitives it supports. The client must treat this response as the contract for the session: if tools is missing, the client should not call tools/list; if resources.subscribe is missing, the client should not attempt resource subscriptions; if prompts.listChanged is absent, the host should not expect prompt-list notifications.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-11-25",
"capabilities": {
"resources": {
"subscribe": true,
"listChanged": true
},
"tools": {
"listChanged": true
},
"prompts": {
"listChanged": true
},
"logging": {}
},
"serverInfo": {
"name": "internal-platform-server",
"title": "Internal Platform MCP Server",
"version": "1.8.3"
},
"instructions": "Use deploy tools only after explicit user approval."
}
}
After the server responds, the client sends notifications/initialized; during operation, both parties must respect the negotiated version and capabilities (lifecycle). This notification has no id because it is a JSON-RPC notification, not a request. It exists to tell the server that normal protocol operation can begin.
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
For HTTP connections, subsequent requests must include the negotiated MCP-Protocol-Version header (transports). Version negotiation is not bureaucracy. It is what prevents an old host from accidentally calling a new server feature with different semantics, and it gives platform teams a controlled upgrade path for clients and servers that ship on different release cadences.
Transports: stdio and Streamable HTTP
MCP currently defines two standard transports: stdio and Streamable HTTP, and JSON-RPC messages must be UTF-8 encoded (transports). Stdio is the local-process path. The client launches the server as a subprocess, sends JSON-RPC messages to stdin, and receives JSON-RPC messages from stdout (transports). The server may log to stderr, but stdout must contain only valid MCP messages, and messages are newline delimited without embedded newlines (transports).
Streamable HTTP is the remote-service path. The server exposes a single MCP endpoint that supports POST and GET, with POST used for client-to-server JSON-RPC messages and GET optionally used to open an SSE stream for server-to-client messages (transports). For request bodies, the server can reply with either application/json for one JSON object or text/event-stream for streaming multiple messages before the final response (transports).
A practical deployment pattern is stdio for local, user-scoped capabilities and Streamable HTTP for shared services. A desktop IDE can launch a filesystem server locally while calling a hosted observability server over HTTP, where the platform team can enforce OAuth, rate limits, tenant isolation, and centralized logging. The protocol stays consistent, but the operational model changes.
Transport choice should follow the trust boundary:
| Transport | Best fit | Production tradeoff |
|---|---|---|
| stdio | Local, user-scoped servers such as filesystem or repository context. | Low latency and simple subprocess execution, but the host must manage installation, upgrades, environment filtering, and local trust. |
| Streamable HTTP | Shared services such as knowledge systems, observability, and SaaS integrations. | Centralized deployment and policy, but with network latency, authentication, session management, and availability concerns. |
Best Practice: Use stdio when the capability naturally belongs on the user's machine. Use Streamable HTTP when the capability is shared, centrally governed, or tenant-scoped.
The transport specification explicitly calls out HTTP security requirements such as validating the Origin header, binding local servers to localhost where appropriate, and implementing authentication (transports).
Server primitives: resources, tools, and prompts
MCP servers expose three core primitives: resources, tools, and prompts (MCP specification). The difference between them is not cosmetic. Each primitive has a different control model and should map to a different product interaction.
Server primitives rendered as a diagram: resources provide context, tools execute actions, and prompts expose user-selected workflows.
Resources are application-driven context. The resource specification says resources let servers expose data to clients, identified by URIs, such as file contents, database schemas, or application-specific information (resources). Clients discover resources with resources/list, retrieve content with resources/read, can use URI templates via resources/templates/list, and may subscribe to resource updates when the server declares subscription support (resources). Use resources when the AI application needs context to read, inspect, cite, or select.
The following resources/read request retrieves a database schema resource. Use this pattern when the host needs authoritative context before the model proposes an action. In a production analytics assistant, reading schema as a resource is safer than making the model guess table names or call a SQL tool speculatively.
{
"jsonrpc": "2.0",
"id": 20,
"method": "resources/read",
"params": {
"uri": "db://warehouse/analytics/orders/schema"
}
}
A resource response can contain one or more content objects. The resource content should be treated as context, not as executable instruction. Production hosts should still apply prompt-injection defenses before passing retrieved text to the model, especially when resources contain user-authored documents or external web content.
{
"jsonrpc": "2.0",
"id": 20,
"result": {
"contents": [
{
"uri": "db://warehouse/analytics/orders/schema",
"mimeType": "application/json",
"text": "table: orders\ncolumns:\n- order_id: string\n- created_at: timestamp\n- net_revenue: decimal"
}
]
}
}
Tools are model-controlled executable functions. The tools specification says tools let servers expose functions that models can invoke to query databases, call APIs, or perform computations (tools). Clients discover tools with tools/list and invoke them with tools/call (tools). Tool definitions include a name, description, input JSON Schema, optional output schema, optional annotations, optional icons, and optional execution metadata for task support (tools). Use tools when the model needs to take an action or compute a result.
A tools/list request discovers callable tools after initialization. It exists so tools can be dynamic. A SaaS server may expose different tools based on user role, tenant, feature flags, or OAuth scopes. A host should cache tool lists carefully and refresh when the server sends a list-changed notification.
{
"jsonrpc": "2.0",
"id": 10,
"method": "tools/list",
"params": {
"cursor": "optional-page-token"
}
}
A realistic response includes schemas that the host can show to the model and to the user approval UI. The schema is not just documentation. It is the contract for validating arguments before the server touches a production system.
{
"jsonrpc": "2.0",
"id": 10,
"result": {
"tools": [
{
"name": "run_sql_readonly",
"title": "Run read-only SQL",
"description": "Execute a read-only SQL query against approved analytics views.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "A SELECT statement over approved views."
},
"maxRows": {
"type": "integer",
"minimum": 1,
"maximum": 1000
}
},
"required": ["query"]
},
"outputSchema": {
"type": "object",
"properties": {
"columns": {
"type": "array",
"items": { "type": "string" }
},
"rows": {
"type": "array"
}
},
"required": ["columns", "rows"]
}
}
]
}
}
A tools/call request executes a tool. In production, this is the request most likely to need permission checks, user confirmation, timeout handling, rate limiting, and audit logging. Even a “read-only” database query can leak sensitive data or create load, so hosts should show arguments to users for sensitive operations and servers should independently validate inputs.
{
"jsonrpc": "2.0",
"id": 11,
"method": "tools/call",
"params": {
"name": "run_sql_readonly",
"arguments": {
"query": "SELECT order_date, revenue FROM analytics.daily_revenue WHERE order_date >= '2026-07-01' ORDER BY order_date",
"maxRows": 100
}
}
}
A successful response can include both human-readable content and structured content. Tool results can also report execution errors with isError: true; clients should provide tool execution errors to models when self-correction is useful, while still distinguishing them from protocol-level JSON-RPC errors (tools).
{
"jsonrpc": "2.0",
"id": 11,
"result": {
"content": [
{
"type": "text",
"text": "Returned 7 rows of daily revenue."
}
],
"structuredContent": {
"columns": ["day", "revenue"],
"rows": [
["2026-07-02", 184233.42],
["2026-07-03", 191002.18]
]
},
"isError": false
}
}
Prompts are user-controlled templates. The prompts specification says servers can expose prompt templates that clients list and retrieve, optionally with arguments (prompts). Prompts are intended to be explicitly selected by users, often through commands in a user interface (prompts). Use prompts when the server knows a good workflow shape, such as “review this diff,” “summarize this incident,” or “generate a migration plan,” but the user should decide when to run it.
A prompts/get request retrieves a workflow template. This is useful for internal developer platforms where teams want consistent runbooks without hiding them as tools. The prompt below might be selected by a developer during a deployment review.
{
"jsonrpc": "2.0",
"id": 30,
"method": "prompts/get",
"params": {
"name": "deployment_risk_review",
"arguments": {
"service": "checkout-api",
"environment": "production"
}
}
}
A response returns messages that the host can place into the conversation. The host should still render this as a user-controlled workflow, not as a hidden server instruction.
{
"jsonrpc": "2.0",
"id": 30,
"result": {
"description": "Review deployment risk before production rollout.",
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Review the production deployment plan for checkout-api. Check recent incidents, error-budget status, database migrations, rollback readiness, and customer-impact risks."
}
}
]
}
}
A practical rule of thumb is: resources are context, tools are actions, and prompts are workflows. If you overload tools to return documents, the model may treat passive context as an action. If you overload resources to trigger side effects, the user may not get the approval UI they expect. If you hide prompts as tools, users lose discoverability and control. MCP gives these ideas separate primitives so clients can present them differently.
Engineering insight: Primitive choice is a product-security decision. The primitive determines who initiates the action, what UI the host should show, and which permission path should run.
Consider a production analytics assistant. The warehouse schema, table descriptions, and saved metric definitions should be resources because the host can load them as context. The run_sql operation should be a tool because it executes work and may need approval, timeouts, cost limits, and result validation. A “weekly revenue investigation” workflow should be a prompt because the user should explicitly choose that analysis pattern. Separating the three lets the host display schema context quietly, gate query execution visibly, and expose repeatable workflows as user-facing commands.
Client primitives: sampling and elicitation
MCP is bidirectional. Servers provide resources, tools, and prompts, but clients can also provide capabilities to servers. Sampling lets a server request LLM generation from the client while the client retains control over model access, model selection, and permissions (sampling). Elicitation lets servers request additional information from users through the client (elicitation).
Client primitives rendered as a diagram: sampling requests model generation through the host, while elicitation requests user input through the client.
A sampling/createMessage request is useful when a server needs model assistance but the host must keep control of the model. For example, an incident server may need a concise summary of log excerpts. The server can request sampling, while the host still chooses the model, applies rate limits, and lets the user review prompts and generated responses where required (sampling).
{
"jsonrpc": "2.0",
"id": 40,
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Summarize these sanitized checkout-api error logs for an incident commander. Focus on customer impact, likely root cause, and next diagnostic step."
}
}
],
"modelPreferences": {
"costPriority": 0.2,
"speedPriority": 0.7,
"intelligencePriority": 0.8
},
"systemPrompt": "You summarize incidents for senior platform engineers. Do not invent facts not present in the logs.",
"maxTokens": 500
}
}
Sampling can include tools when the client declares sampling.tools support: the server sends tool definitions, the client returns tool-use content, the server executes the tools, and the server sends matching tool results in a follow-up request (sampling). Implementations should cap tool-loop iterations because sampling with tools can otherwise become an unbounded agent loop.
Elicitation is different from sampling. It is not a way to ask the model a question. It is a way for a server to request user input through the client. Form mode is appropriate for non-sensitive structured data; URL mode is required for sensitive interactions such as passwords, API keys, access tokens, or payment credentials (elicitation).
The following elicitation/create request asks a user to choose a deployment risk level. It is suitable for form mode because it does not collect secrets.
{
"jsonrpc": "2.0",
"id": 50,
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Choose the risk level for the checkout-api production deployment.",
"requestedSchema": {
"type": "object",
"properties": {
"riskLevel": {
"type": "string",
"title": "Risk level",
"enum": ["low", "medium", "high"],
"default": "medium"
},
"reason": {
"type": "string",
"title": "Reason",
"maxLength": 500
}
},
"required": ["riskLevel"]
}
}
}
In production, elicitation is where user experience and security meet. The client must make clear which server is asking, give users decline and cancel options, and let users review form responses before sending them (elicitation). For URL mode, the client must show the target URL and obtain consent before navigation (elicitation).
A useful production example is an incident-response server. The server can expose logs and runbook pages as resources, expose create_status_update as a tool, and use elicitation when it needs the incident commander to choose a customer-facing severity. If the server needs a draft summary, sampling lets it request an LLM completion through the host, while the host still controls model choice, prompt review, and what generated text the server can see.
Hello World MCP Server
A minimal MCP server should prove the lifecycle without hiding behind a complex API. The examples below expose one tool, hello, over stdio. Stdio is appropriate here because the client launches the server as a local subprocess, and the transport specification requires stdout to contain only valid MCP messages. For debugging, write logs to stderr, not stdout (transports).
Python
Set up the project:
uv init hello-mcp
cd hello-mcp
uv add "mcp[cli]"
Create server.py:
from mcp.server.fastmcp import FastMCP
# Name the server so hosts can identify it during initialization.
mcp = FastMCP("hello-world")
@mcp.tool()
def hello(name: str = "world") -> str:
"""Return a friendly greeting."""
return f"Hello, {name}!"
if __name__ == "__main__":
# stdio keeps this local and lets an MCP host launch it as a subprocess.
mcp.run(transport="stdio")
Then start the server:
uv run server.py
This server declares a single typed tool. The Python SDK uses the function signature and docstring to derive the tool schema. A production version would add stricter input validation, structured errors, and stderr logging.
TypeScript
Install and run:
mkdir hello-mcp-ts
cd hello-mcp-ts
npm init -y
npm install @modelcontextprotocol/sdk zod@3
npm install -D typescript @types/node
npm pkg set type=module
npx tsc --init --module Node16 --moduleResolution Node16 --target ES2022 --outDir build --rootDir src --strict true
mkdir -p src
Create src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Name and version are exposed during initialization.
const server = new McpServer({
name: "hello-world",
version: "1.0.0",
});
server.registerTool(
"hello",
{
description: "Return a friendly greeting.",
inputSchema: {
name: z.string().default("world"),
},
},
async ({ name }) => ({
content: [
{
type: "text",
text: `Hello, ${name}!`,
},
],
}),
);
// stdio lets a host launch this server as a local subprocess.
const transport = new StdioServerTransport();
await server.connect(transport);
Then build and start the server:
npm exec tsc
node build/index.js
The TypeScript version shows the same protocol shape with an explicit Zod schema. The server still does not print normal logs to stdout, because stdout is reserved for MCP JSON-RPC messages in stdio mode.
Engineering insight: Hello World servers are useful for validating host configuration, transport wiring, and tool discovery. They are not a permission model. Add policy before replacing the greeting with anything that reads files, calls SaaS APIs, or changes state.
Production engineering guidance
Experienced teams use MCP as a platform boundary. The key question is not “can the model call this function?” It is “which system owns context, permission, execution, retry, and audit for this capability?”
For IDE assistants, MCP usually connects local code, project files, Git metadata, issue trackers, CI systems, and documentation. The tradeoff is local trust versus enterprise control. Local stdio servers can read a workspace with low latency, but the IDE must constrain filesystem roots and avoid leaking credentials to subprocesses. Remote HTTP servers centralize policy for GitHub, CI, or internal developer-platform actions, but they add network dependencies and OAuth scope management.
For enterprise knowledge systems, resources are often the primary primitive. Documents, policies, schemas, and runbooks should be modeled as resources because the host can let the user select or search them before adding them to model context. Tools should be reserved for operations such as querying an index, creating a ticket, or updating a knowledge base. The main tradeoff is freshness versus cost: dynamic resources and subscriptions can keep context current, but large organizations need caching, pagination, and relevance filters to avoid filling the model window with low-value content.
For internal developer platforms, tools represent operational actions: create an environment, roll back a deployment, fetch logs, run a migration check, or open a change request. Mature hosts classify tools by risk. A read-only get_deployment_status tool may run automatically. A rollback_production tool should require explicit confirmation and probably role-based authorization. MCP says hosts must obtain user consent before invoking tools and users should understand what each tool does before authorizing it (MCP specification); production platforms turn that principle into concrete approval flows.
For AI agents, MCP becomes the boundary between planning and execution. The planner may decide that a database query, browser action, or SaaS update is needed, but the MCP host should still route the action through negotiated capabilities, schemas, permission checks, timeouts, and audit logs. This helps prevent an agent from confusing untrusted resource text with authority to execute a tool. It also lets multi-agent systems share servers without sharing all credentials or all state.
For databases, model schemas as resources and queries as tools. This separation is important for performance and safety. Schema resources can be cached and summarized; query tools can enforce allowlists, read-only constraints, row limits, statement timeouts, cost controls, and result-size limits. Tool output schemas help downstream agents reason about results without parsing arbitrary text (tools).
For SaaS integrations, prefer narrow tools with explicit input schemas over broad “call API” tools. A create_linear_issue or send_slack_message tool gives the host something understandable to show to the user. A generic http_request tool may be flexible, but it expands the security review surface. If a SaaS flow needs credentials or third-party authorization, URL-mode elicitation is safer than asking users to paste secrets into a chat or form (elicitation).
Multi-server orchestration should be host-led. The host can connect to many MCP servers, but each server should remain a separate trust domain. Do not let one server silently invoke tools from another server unless the host has an explicit policy for that chain. A typical orchestration might read architecture docs from a knowledge server, query deployment status from a platform server, and create an incident update through a ticketing server. Each step should have its own tool call, argument validation, timeout, and audit event.
Observability is not optional.
| Signal | Why it matters |
|---|---|
| Server and tool name | Identifies which integration performed work. |
| Request ID, user, tenant | Supports audit trails and incident response. |
| Approval state | Shows whether a sensitive action was user-approved. |
| Duration, result size, error type | Surfaces slow tools, large responses, and retryable failures. |
| Transport events | Helps debug HTTP sessions, SSE reconnects, and stdio subprocess failures. |
For stdio servers, capture stderr as logs without treating every stderr line as failure, because the transport specification allows servers to write logs to stderr (transports).
Scalability considerations depend on primitive type. Resource-heavy systems need pagination, cache invalidation, and relevance selection. Tool-heavy systems need queueing, timeouts, rate limits, idempotency keys where side effects exist, and clear error taxonomy. Prompt-heavy systems need versioning and discoverability so users know which workflow they selected. Sampling-heavy systems need token budgets, model-selection policy, user approval controls, and loop limits.
Security engineering checklist
MCP enables arbitrary data access and code execution paths, so the specification places trust and safety at the center of implementation. It calls out user consent and control, data privacy, tool safety, and LLM sampling controls as key principles (MCP specification).
Warning: Protocol compliance does not make an integration safe. Hosts still need explicit user consent before exposing user data or invoking tools, and servers still need authorization, validation, rate limits, and output sanitization.
For tool safety, servers must validate all inputs, implement access controls, rate limit invocations, and sanitize outputs (tools). Clients should show tool inputs before calling the server, ask for confirmation on sensitive operations, validate tool results before passing them to the LLM, implement timeouts, and log tool usage for auditability (tools). Tool annotations are also explicitly untrusted unless they come from trusted servers (tools).
For HTTP transport, servers must validate the Origin header to prevent DNS rebinding attacks, should bind local servers only to localhost rather than all network interfaces, and should implement proper authentication (transports). Session IDs returned through MCP-Session-Id should be globally unique and cryptographically secure, and clients must include them on subsequent requests when provided (transports).
For elicitation, clients must make clear which server is requesting information, provide decline and cancel options, allow users to review form responses before sending, and clearly display the target domain before URL navigation (elicitation). Servers must not rely on client-provided user identity without verification, and URL-mode flows must verify that the user who opens the URL is the same user who initiated the elicitation request (elicitation).
A production permission model should combine server trust, user role, tool risk, data sensitivity, and environment. For example, a senior SRE may be allowed to call a rollback tool in staging without confirmation, but production rollback should still require an explicit approval UI. A finance assistant might read invoice resources automatically but require approval for sending or refunding invoices. A database assistant might allow schema reads and limited SELECT queries but block writes entirely.
Engineering insight: Approval prompts are not a substitute for authorization. The host can ask the user, but the server still needs to enforce identity, tenant, role, and scope.
How Hermes Uses MCP
Hermes is a useful case study because it combines a native tool system with MCP servers and publishing workflows. Hermes has first-class toolsets for filesystem operations, web search and extraction, browser automation, terminal execution, memory, messaging, delegation, and scheduling. Its native MCP client reads mcp_servers from configuration, connects to stdio or HTTP MCP servers at startup, discovers server tools, and registers them as first-class tools with names like mcp_{server}_{tool}. That means MCP tools appear beside built-in tools rather than behind a separate bridge.
In the current publishing workflow, Forge is exposed to Hermes as MCP tools. The agent saves canonical markdown through Forge, then Forge pushes the article to a WordPress target as a draft. WordPress is not edited directly by the model; the Forge tools mediate state, status, preview URLs, remote IDs, and divergence checks. This is exactly the kind of boundary MCP is good at: the model can request a content operation, but the publishing system owns the remote API details and the draft-versus-publish distinction.
Hermes publishing workflow rendered as a diagram: research, canonical markdown, Forge MCP tools, WordPress draft review, and explicit publication.
Hermes also uses multiple capability surfaces around the MCP boundary. Filesystem work can come from built-in file tools or MCP filesystem tools, depending on configuration. Search can come from built-in web search or specialized MCP/search integrations. Browser automation is a first-class toolset for web interaction. Messaging is handled through gateway platform adapters. Memory is a persistent tool/provider layer. Execution agents are handled through delegation and process tools. Forge and WordPress publishing are reached through MCP-provided Forge tools in this environment.
The architectural pattern is planner-led orchestration. A user asks for an MCP article. Hermes plans the task, researches official MCP documentation, writes or revises markdown, saves the canonical article through Forge, updates the WordPress draft, verifies remote status, and reports the preview link. In that chain, MCP is not the reasoning engine. It is the integration boundary that lets Hermes treat Forge as a typed capability with status and update operations instead of hand-editing WordPress.
The technical lesson is that MCP works best when paired with a policy-aware host. Hermes can use built-in tools for low-level filesystem and web operations, MCP tools for external systems like Forge, memory for durable preferences, and delegation for isolated execution agents. The host decides which tool schemas are visible, which skills are loaded, which operations are safe, and whether publishing is allowed. In this draft workflow, “do not publish” is enforced by calling draft-status Forge operations and verifying that WordPress remains in draft state.
Implementation guidance
| Implementation decision | Production recommendation |
|---|---|
| Primitive modeling | Use resources for context, tools for executable work, prompts for user-selected workflows, elicitation for user input, and sampling only after negotiation. |
| Capability negotiation | Treat initialize as the session contract. Do not call unadvertised features, and degrade gracefully when capabilities are missing. |
| Dynamic inventories | Refresh resources, tools, and prompts when list-change notifications are supported rather than assuming startup state is permanent. |
| Threat model | Separate local stdio risks from remote HTTP risks. Local servers need installation and environment controls; remote servers need authentication, session handling, Origin validation, and transport observability. |
| Policy | Add product policy above protocol compliance. MCP defines exchange; the host and server decide whether a specific action is allowed. |
Best Practice: Design the permission model before expanding the tool list. It is easier to add capabilities to a safe host than to retrofit policy after dozens of tools are already exposed.
Common Implementation Mistakes
These mistakes usually come from treating MCP as a demo protocol rather than a production integration boundary.
Treating MCP as only function calling
This happens because tools are the most visible primitive. It is dangerous because teams end up modeling documents, workflows, and user input as tools, which weakens UI clarity and permission boundaries. Production systems avoid it by explicitly classifying capabilities as resources, tools, prompts, sampling, or elicitation before writing server code.
Mixing Resources and Tools
This happens when a server exposes every operation through one generic interface. It is dangerous because a passive read can look like an action, or an action can bypass a confirmation UI. Production systems avoid it by making context read paths resources and side-effect or compute paths tools. Database schemas are resources; query execution is a tool. Runbooks are resources; creating an incident update is a tool.
Skipping capability negotiation
This happens when developers test against one client and one server and hard-code assumptions. It is dangerous because MCP is versioned and capability-driven; a feature that works in one host may not be available in another. Production systems avoid it by treating initialize as the session contract, feature-gating UI and tool routing from negotiated capabilities, and failing closed when required capabilities are absent.
Over-trusting servers
This happens when hosts treat tool descriptions, annotations, or returned content as authoritative. It is dangerous because servers can be compromised, misconfigured, or connected to untrusted data. The tool specification says clients must consider tool annotations untrusted unless they come from trusted servers (tools). Production systems avoid this with per-server trust levels, server allowlists, output sanitization, prompt-injection defenses, and explicit user consent for sensitive tools.
Poor permission handling
This happens when teams rely on the model to “choose responsibly” rather than enforcing policy. It is dangerous because the model may request actions based on incomplete context or malicious resource text. Production systems avoid it by combining user identity, OAuth scopes, tool risk levels, environment, and approval state. The server should enforce authorization even if the host also shows confirmation.
Missing timeout and retry logic
This happens because local demos are fast and deterministic. It is dangerous because production servers fail, networks disconnect, SSE streams resume, and long-running tools can hang. The lifecycle specification recommends request timeouts and cancellation notifications for hung requests (lifecycle). Production systems avoid this with per-tool timeouts, bounded retries, cancellation, idempotency where side effects exist, and clear user-visible failure states.
Lack of audit logging
This happens when teams treat MCP calls like internal function calls. It is dangerous because tool calls can read data, change systems, or request LLM sampling. Production systems log server name, tool name, arguments after redaction, user, tenant, approval state, duration, result size, error class, and remote request IDs. Audit logs are also essential for investigating prompt-injection incidents and unexpected side effects.
Weak security boundaries
This happens when stdio servers inherit too much environment, HTTP servers skip Origin validation, or hosts expose all tools to all users. It is dangerous because MCP servers sit near sensitive data and execution paths. Production systems avoid it by filtering subprocess environments, binding local HTTP servers to localhost, validating Origin headers, using proper authentication, sanitizing outputs, and keeping server credentials scoped to the smallest necessary capability (transports).
FAQ
Is MCP the same thing as function calling?
No. Function calling maps closest to MCP tools, but MCP also defines resources, prompts, sampling, elicitation, lifecycle negotiation, transports, notifications, and utilities (MCP specification). Treating MCP as only function calling leaves out much of the protocol.
When should I use resources instead of tools?
Use resources when the server is exposing context for the application or model to read, such as files, schemas, or records (resources). Use tools when the model is invoking executable behavior, such as querying an API, running a calculation, or changing external state (tools).
What is the difference between stdio and Streamable HTTP?
Stdio runs a local server as a subprocess and exchanges newline-delimited JSON-RPC over stdin and stdout (transports). Streamable HTTP exposes an HTTP endpoint that handles POST and optional SSE streams, which is better suited for independent remote services and multi-client deployments (transports).
What security controls should every MCP implementation include?
Clients should implement user consent, tool confirmation, result validation, timeouts, and audit logging, while servers should validate inputs, enforce access control, rate limit calls, sanitize outputs, and authenticate remote requests (MCP specification, tools, transports).