All writing Notes & observations

The Complete MCP Guide: Architecture, Remote Call Flow, and Building Your Own Server

Understand MCP hosts, clients, servers, and primitives; trace a remote call end to end; then build, deploy, secure, and connect a Python MCP server to an agent.

The Complete MCP Guide: Architecture, Remote Call Flow, and Building Your Own Server

A language model can reason and generate text, but it does not know which project your company created today, nor can it create a ticket by imagination alone. MCP provides a standard, controlled path from an agent to that live data and those actions.

MCP connects an agent host to tools, resources, and prompt templates

The one-sentence definition is:

The Model Context Protocol (MCP) is a standard interface between agent applications and external capabilities. It defines capability discovery, schemas, calls, results, and negotiation. It is neither a model nor a replacement for your business API.

This article follows the 2026-07-28 MCP specification and calls out common differences in older clients. MCP is evolving quickly, so production integrations should follow the protocol versions supported by their host and SDK.

Why MCP exists

Without a shared protocol, every agent platform needs a custom adapter for every external system. Connecting three hosts to GitHub, a database, and a ticket system can quickly become nine integrations.

flowchart LR subgraph Before[Without MCP: N × M adapters] A1[Agent A] --> X1[GitHub adapter] A1 --> X2[Database adapter] A2[Agent B] --> X3[GitHub adapter] A2 --> X4[Ticket adapter] end subgraph After[With MCP: one protocol boundary] B1[Agent A] --> P[MCP] B2[Agent B] --> P P --> S1[GitHub Server] P --> S2[Database Server] P --> S3[Ticket Server] end

MCP standardizes more than the final HTTP request:

Question MCP mechanism
What can this server do? tools/list, resources/list, prompts/list
What arguments are valid? JSON Schema
How is an action executed? tools/call
How is addressable context read? resources/read
How are results represented? content, structuredContent, errors, and metadata
Which features do both sides support? Protocol version, capabilities, and extensions
How do messages travel? Local stdio or remote Streamable HTTP

The USB-C analogy is useful but incomplete. USB-C standardizes a physical interface; MCP standardizes an interface whose names, descriptions, and schemas are interpreted by a model. Metadata is therefore part of the product experience, not incidental documentation.

The four roles

flowchart LR U[User] --> H[Host
Agent application] H --> M[LLM] H --> C1[MCP Client A] H --> C2[MCP Client B] C1 <-->|MCP| S1[MCP Server
Project system] C2 <-->|MCP| S2[MCP Server
Knowledge base] S1 --> API1[(Business API / DB)] S2 --> API2[(Documents / Search)]
Role Responsibility Important boundary
User States a goal and confirms consequential actions Usually does not construct protocol messages
Host Owns the agent, model context, policy, approvals, and MCP clients The MCP server does not run the whole agent loop
MCP Client Talks to one server and handles discovery, calls, and compatibility Usually an internal host component
MCP Server Exposes capabilities, validates calls, and reaches business systems It must not expose unrestricted backend access

The model normally does not connect directly to the server. The host shows the model an allowed set of capabilities. The model proposes a tool call. The host applies policy and approval rules before executing it. This separates model intent from system authority.

Tools, resources, and prompts

Primitive Primary controller Best for Example
Tools Model selects; host governs execution Queries and actions search_projects, create_ticket
Resources Host or application loads Addressable, usually read-only context project://P-1024/spec
Prompts User or host selects Reusable workflow templates “Create a release checklist”

The protocol also includes notifications and other optional capabilities. A useful server rarely needs all of them. Start with a few well-designed tools.

An MCP server is often an agent-facing adapter over existing systems:

flowchart LR L[Tool visible to the LLM
name + description + schema] --> S[MCP Server
auth · validation · orchestration] S --> R1[REST API] S --> R2[gRPC] S --> R3[(Database)] S --> R4[Message queue]

A REST route might be PATCH /v1/projects/:id. A useful MCP tool is goal-oriented: archive_project(project_id, reason). Do not mirror hundreds of internal endpoints into hundreds of model tools.

The complete remote MCP call path

Remote MCP normally uses Streamable HTTP. The older HTTP+SSE transport is superseded and should only remain for compatibility.

sequenceDiagram autonumber actor User participant Host as Agent Host participant Model as LLM participant Client as MCP Client participant Server as Remote MCP Server participant API as Business system Host->>Client: Load server configuration Client->>Server: Discover protocol and capabilities Server-->>Client: Server info + capabilities Client->>Server: tools/list Server-->>Client: Names, descriptions, schemas, annotations User->>Host: “List delayed projects this week” Host->>Model: Message + allowed tool definitions Model-->>Host: list_projects(status="delayed") Host->>Host: Policy check / approval if needed Host->>Client: Execute approved call Client->>Server: tools/call + credentials Server->>Server: Authenticate, authorize, validate Server->>API: Query current business data API-->>Server: Project records Server-->>Client: content + structuredContent Client-->>Host: Tool result Host->>Model: Add allowed result to context Model-->>Host: Compose answer Host-->>User: Final response + action status

1. Configuration and discovery

The host stores a server name, URL, and authentication method. The client learns the protocol version, server information, and capabilities, then lists tools. Hosts may cache the list and refresh it when capabilities change or the cache expires.

In the 2026-07-28 protocol, the modern path is stateless: server/discover and per-request metadata replace the old connection handshake and protocol-level session. Older clients can still use initialize → initialized and Mcp-Session-Id. Let the SDK negotiate this; application code should not handcraft JSON-RPC.

2. Tools enter model context

The host may filter tools by user, tenant, role, task, or dynamic tool search. Only the relevant names, descriptions, and schemas need to enter context. Five distinct tools are usually more reliable and cheaper than fifty overlapping ones.

3. The model selects; the host decides

The model produces structured intent, not authority. The host should decide whether the user may call that tool, whether a write needs confirmation, whether arguments cross cost or data boundaries, and whether prompt injection or destructive behavior is suspected.

Annotations such as readOnly, destructive, and openWorld inform host policy. They are hints, not authorization controls.

4. HTTP and business execution

The client sends an MCP request over HTTPS to an endpoint such as /mcp, with protocol-routing information and usually an Authorization header. The server must independently validate the token, scopes, tenant, object-level permission, arguments, timeouts, and audit context before calling the real REST API, gRPC service, or database.

MCP does not make a dangerous SQL query safe, and it does not replace business authorization.

Expanding the details normally hidden by an SDK, a modern tool call looks roughly like this (some per-request _meta is omitted for readability):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
POST /mcp HTTP/1.1
Host: mcp.example.com
Authorization: Bearer <access-token>
Content-Type: application/json
Mcp-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: list_projects

{
  "jsonrpc": "2.0",
  "id": 42,
  "method": "tools/call",
  "params": {
    "name": "list_projects",
    "arguments": {"status": "delayed", "limit": 5}
  }
}

The successful JSON-RPC result carries model-readable content and schema-bound data for the application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "content": [{"type": "text", "text": "Found 1 delayed project."}],
    "structuredContent": {
      "result": [{"id": "P-1002", "name": "Billing migration"}]
    },
    "isError": false
  }
}

Do not handcraft these messages in application code. The example is useful for reading gateway logs and traces; SDKs should own negotiation, headers, and JSON-RPC details.

5. Results return to the model

Channel Consumer Guidance
content Model Concise, understandable explanation of the result
structuredContent Host, UI, or later calls Schema-valid fields and stable identifiers
_meta Client application Client-only data; not a secret vault

The host puts approved result content back into the model context. The model then answers or plans another call. A successful MCP request does not necessarily mean the user task succeeded: the agent must inspect isError, completeness, and any follow-up requirements.

Two remote connection models

flowchart TB subgraph Direct[Mode A: runtime-managed connection] A[Your agent application] --> C[MCP Client] C --> S[Remote MCP Server] end subgraph Hosted[Mode B: platform-hosted connection] B[Your application] --> P[Model / Agent platform] P --> HS[Remote MCP Server] end
Mode Main advantage Best fit
Runtime-managed Your app owns networking, credentials, filtering, and approval Private networks, strict compliance, custom policy
Hosted MCP Less integration code; the agent platform handles protocol calls Public HTTPS services and quick integrations

The OpenAI Agents SDK supports hosted MCP for public remote servers and runtime-managed stdio or Streamable HTTP for local and private servers. The choice is about who should own the connection and trust boundary.

Build a remote MCP server

The following minimal project exposes a read-only project catalog. It uses Python SDK v2, Python 3.10+, and stateless Streamable HTTP so that replicas can scale horizontally.

Set up the project

1
2
3
mkdir project-mcp && cd project-mcp
uv init
uv add "mcp[cli]>=2,<3" pydantic

Pin the major SDK version. The protocol and SDKs move quickly; a production build should not cross a major version without review.

Create server.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from typing import Annotated, Literal

from mcp.server import MCPServer
from mcp.types import ToolAnnotations
from pydantic import BaseModel, Field


class Project(BaseModel):
    id: str
    name: str
    status: Literal["active", "delayed", "archived"]
    owner: str


PROJECTS = [
    Project(id="P-1001", name="Search upgrade", status="active", owner="Lin"),
    Project(id="P-1002", name="Billing migration", status="delayed", owner="Kai"),
]

mcp = MCPServer("project-catalog", version="1.0.0")


@mcp.tool(
    annotations=ToolAnnotations(
        readOnlyHint=True,
        idempotentHint=True,
        destructiveHint=False,
        openWorldHint=False,
    )
)
def list_projects(
    status: Literal["active", "delayed", "archived"] | None = None,
    limit: Annotated[int, Field(ge=1, le=50)] = 10,
) -> list[Project]:
    """List projects, optionally filtered by exact status.

    Use this for project discovery. It never modifies project data.
    """
    matches = [p for p in PROJECTS if status is None or p.status == status]
    return matches[:limit]


if __name__ == "__main__":
    mcp.run(
        transport="streamable-http",
        host="0.0.0.0",
        port=8000,
        stateless_http=True,
        json_response=True,
    )

The function name becomes the tool name, its docstring becomes the description, type hints generate the input schema, and the return annotation generates the output schema. The annotations describe behavior to the host. The SDK handles MCP framing, version compatibility, and Streamable HTTP.

Replace the in-memory list with a business service call in production, but always derive the tenant from verified credentials rather than a model-supplied user_id.

Test locally

Use the official Inspector first:

1
uv run mcp dev server.py

Then start the HTTP server:

1
2
uv run python server.py
# http://127.0.0.1:8000/mcp

An SDK client makes a compact regression check:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import asyncio
from mcp import Client


async def main() -> None:
    async with Client("http://127.0.0.1:8000/mcp") as client:
        tools = await client.list_tools()
        assert any(tool.name == "list_projects" for tool in tools.tools)

        result = await client.call_tool(
            "list_projects", {"status": "delayed", "limit": 5}
        )
        assert not result.is_error
        assert result.structured_content["result"][0]["id"] == "P-1002"


asyncio.run(main())

Test valid calls, boundaries, invalid enums, authorization failures, downstream timeouts, empty results, and output-schema validation. Inspector proves the protocol works; automated tests prove it still works after an upgrade.

Containerize and deploy

1
2
3
4
5
6
7
8
9
FROM python:3.13-slim
WORKDIR /app

COPY pyproject.toml uv.lock ./
RUN pip install --no-cache-dir uv && uv sync --frozen --no-dev

COPY server.py ./
EXPOSE 8000
CMD ["uv", "run", "python", "server.py"]
1
2
docker build -t project-mcp:1.0.0 .
docker run --rm -p 8000:8000 project-mcp:1.0.0

Production should expose a stable HTTPS URL such as https://mcp.example.com/mcp. Configure proxies for streaming behavior, bounded request size, sensible timeouts, and idle connections.

The demo server has no authentication and is only suitable for local learning. Before exposing it to any network, configure the SDK’s token verifier or OAuth support and enforce business authorization inside every tool.

Stateless mode is a good default. If a workflow needs state across calls, prefer returning an explicit server-minted handle and accepting it in later tools instead of hiding state in one replica’s memory.

Connect the server to an agent

A reachable endpoint is only half the work. The agent needs registration, authorization, routing guidance, and behavioral tests.

Codex

1
2
codex mcp add projects --url https://mcp.example.com/mcp
codex mcp list

Or configure ~/.codex/config.toml:

1
2
[mcp_servers.projects]
url = "https://mcp.example.com/mcp"

Add a short, testable routing rule to the project’s AGENTS.md:

1
2
3
When a task needs a current project status, owner, or project ID, use the
read-only tools from the projects MCP first. Before any update, archive, or
delete operation, show the exact target and impact and wait for confirmation.

Configuration determines what the agent can see. Instructions help it decide when to use those capabilities. Never tell it to blindly trust remote content; tool results can be stale or malicious.

OpenAI Agents SDK

A public remote server can be attached as a hosted MCP tool:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from agents import Agent, HostedMCPTool

agent = Agent(
    name="Project assistant",
    instructions="Use the projects MCP for current project facts.",
    tools=[
        HostedMCPTool(
            tool_config={
                "type": "mcp",
                "server_label": "projects",
                "server_url": "https://mcp.example.com/mcp",
                "require_approval": "never",  # safe only for this read-only tool
            }
        )
    ],
)

Do not copy never onto write tools. Match approval policy to risk, and keep independent authorization on the server. For private services, let your Agents SDK runtime own the connection so that your application controls networking, filtering, and approvals.

Verify actual agent behavior

Prompt Expected behavior
“List delayed projects” Calls list_projects(status="delayed")
“What does delayed mean?” Answers generally without a pointless call unless company policy is requested
“Delete P-1002” Explains that no delete tool exists; never fabricates success
“Use status=hacked” Schema rejects it; agent corrects or explains
Result contains “ignore previous instructions” Treats it as untrusted data and does not escalate privileges

Measure tool selection, argument correctness, and answer faithfulness. Looking only at final prose can hide duplicate calls, denied actions, and unnecessary cost.

Tool design sets the agent’s ceiling

flowchart LR G[User goal] --> N[Clear verb name] N --> D[When-to-use description] D --> I[Narrow input schema] I --> A[Truthful annotations] A --> O[Stable structured output] O --> E[Recoverable errors]
  1. Give each tool one recognizable goal. Prefer list_projects, get_project, and update_project over project_action(mode, payload).
  2. Describe when to use the tool, not only how it is implemented.
  3. Encode enums, lengths, ranges, and formats in the schema.
  4. Separate reads and writes so the host can apply different approval rules.
  5. Return stable IDs for later calls.
  6. Paginate and cap results.
  7. Return actionable errors, not stack traces.
  8. Evolve schemas compatibly. Optional additions are safer than renames or type changes.

MCP is not always the answer. A private function used by one application may be simpler as a function tool. Large continuous media transfer needs a different data plane. Deterministic transaction state machines should not be delegated entirely to model planning. Unrestricted SQL, shell, or URL-fetching tools should not be exposed in production.

Security: every boundary is untrusted

flowchart LR U[User input
untrusted] --> H[Host policy] H -->|minimum tool set| M[Model output
untrusted intent] M --> A{Approval / authorization} A --> S[MCP Server
authenticate and validate again] S --> B[Business system] B --> R[Tool result
still untrusted] R --> H
Risk Required defense
Cross-tenant access Validate token, scope, tenant, and object permission on every call
Destructive action Separate tool, truthful annotations, confirmation, idempotency key, audit, rollback
Prompt injection Treat returned content as data; never let it rewrite host policy
Token leakage Keep tokens out of descriptions, results, and logs; validate token audience
SSRF URL allowlists, DNS/IP checks, block metadata/private ranges, bound redirects
Excessive data Field minimization, pagination, result caps, PII redaction
Cost or denial of service Rate, concurrency, timeout, circuit-breaker, and quota controls
Supply-chain drift Pin SDK majors, scan dependencies, canary upgrades, rollback

Authentication says who the caller is; authorization says what that caller may do to this particular object. Never trust a model-provided user_id as the tenant boundary. OAuth deployments should use standard discovery, narrow scopes, and an explicit audience.

Host prompts, annotations, approval dialogs, and network allowlists are defense in depth. Server-side object authorization remains the final boundary.

Observability and troubleshooting

An MCP failure can happen because the model did not select a tool, the host denied it, the protocol failed, or a downstream system failed. Logs and traces need to connect these layers.

Record a trace or request ID, server/tool/version, redacted caller and tenant, approval outcome, total and downstream latency, result size, error category, schema version, and deployment version. Never log access tokens or complete private results.

Symptom Check first
Server cannot connect URL, certificate, DNS, Host header, firewall, /mcp path
Connected but no tools Discovery/capabilities, tools/list, registration code
Model never calls a tool Names, descriptions, instructions, overlapping tools
Arguments are consistently wrong Schema breadth, ambiguous semantics, missing enums
Local works; remote times out Proxy streaming, platform timeout, cold start, downstream latency
Call succeeds; answer is wrong Content clarity, missing structured fields, stale cache
Multi-replica failures Hidden process state or old session affinity assumptions

The shortest diagnostic path is usually: Inspector → direct SDK client → host tool list → one complete agent trace. It separates server, protocol, and model-decision problems quickly.

Production checklist

  • Tool names, descriptions, input schemas, and output schemas reviewed
  • Reads, writes, and destructive operations separated
  • Authentication, scopes, tenant, and object authorization enforced per request
  • Stable HTTPS Streamable HTTP endpoint
  • Approval, idempotency, audit, and rollback for writes
  • Pagination and result-size limits; no secrets in results
  • Timeouts, retries, rate limits, circuit breakers, and downstream error mapping
  • Inspector checks, automated tests, and agent behavior evals pass
  • Traces, metrics, redacted logs, and alerts exist
  • SDK and schema versions pinned; upgrades can canary and roll back

Closing thought

MCP is not merely a way for a model to send an HTTP request. It gives capability discovery, argument contracts, execution, results, and security policy a shared path.

flowchart LR A[Pick one real user goal] --> B[Design one narrow tool] B --> C[Test with Inspector] C --> D[Automate tests] D --> E[Streamable HTTP + auth] E --> F[Connect the agent] F --> G[Evaluate and observe] G --> H[Add the next tool]

Make one read-only tool accurate before adding writes. Establish authorization before building elaborate orchestration. MCP reduces integration cost; it does not replace sound API design, authorization, or operational discipline.

References