方寸 Portal

AI Engineer Portal

Your personal operating system for career transition.

Private mode
Back to Learning Paths
Lesson

MCP (Model Context Protocol)

The emerging standard for connecting LLMs to external tools and data: architecture, building servers and clients, and when to use MCP versus rolling your own tool integration.

45 min
ai-agents-and-toolsphase-1portfolio

Why this matters

Every AI team that builds more than one agent faces the same problem: you write a tool integration for one project, then rewrite it for the next because there is no standard way to package and share tool implementations. Direct function calling is tightly coupled to the app codebase. LangChain tools only work inside LangChain. OpenAI function schemas differ slightly from Anthropic tool schemas.

Model Context Protocol (MCP) solves this at the architectural level. It defines a single, open standard for how a client (your app, Claude Desktop, any agent) discovers and calls tools provided by a server (a separate process you control). Once you understand MCP, you can build a tool server once and reuse it across every AI product your team ships — and use the growing ecosystem of pre-built MCP servers without writing integration code.

For application engineers, MCP is now a first-class interview topic. Teams building production agent infrastructure ask candidates to demonstrate they understand when MCP is the right choice versus simpler alternatives.

Core concepts

MCP vs direct function calling vs LangChain tools

Direct function calling is the lowest abstraction: you define a JSON Schema for each function, pass the schema array to the provider API, and handle tool execution in your application code. It works for single-provider, single-app scenarios but is tightly coupled — the tool definition lives in your app, and every new consumer must copy it.

LangChain tools wrap functions in a class hierarchy. They compose well within LangChain's chain system, support callbacks, and have a large ecosystem of pre-built tools. But they are LangChain-specific. If you switch providers, drop LangChain, or want to share tools with a non-LangChain consumer, you rewrite.

MCP is a network protocol. An MCP server is a separate process (or remote service) that exposes capabilities through a standardized JSON-RPC interface. The client discovers available tools at runtime, converts them to the provider's native format, and calls the server to execute them. The server and client are fully decoupled — the server does not know which client is calling, and the client does not know how the server implements the tool.

Direct function callingLangChain toolsMCP
PortabilityApp-specificLangChain-specificAny MCP client
DiscoveryStatic (hardcoded)StaticDynamic at runtime
DeploymentIn-processIn-processOut-of-process
ReusabilityNoneWithin LangChainAcross all clients
Setup complexityLowMediumMedium-high

Use MCP when you are building tools that multiple agents or products will share, when you want to use pre-built servers from the ecosystem, or when you need out-of-process isolation (tools that access filesystems or databases should run with minimal privileges in their own process).

MCP architecture

MCP defines four layers:

Primitives: What the server exposes.

  • Tools — callable functions the model can invoke (equivalent to function calling)
  • Resources — read-only data the model can request (files, database rows, API responses)
  • Prompts — reusable prompt templates with arguments the client populates

Hosts: The application that embeds the LLM (Claude Desktop, your custom app, an IDE plugin). The host manages the conversation and decides when to forward tool calls to an MCP server.

Clients: The MCP protocol layer inside the host. Clients maintain a session with each MCP server, handle discovery, and translate between MCP wire format and provider-specific tool schemas.

Transports: How client and server communicate.

  • stdio — the host spawns the server as a subprocess; client writes to stdin, reads from stdout. Simple and secure for local tools.
  • HTTP + SSE — the server runs as a web service; client sends JSON-RPC over HTTP, receives events over Server-Sent Events. Required for remote tools or multi-user deployments.

Building an MCP server in Python

# server.py
import asyncio
import json
from mcp.server import Server
from mcp.server.models import InitializationOptions
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent, CallToolResult
from typing import Any

app = Server("file-search-server")

@app.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="search_files",
            description=(
                "Search for files in a directory by name pattern. "
                "Use when the user asks to find files, locate code, "
                "or check if a file exists."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "directory": {
                        "type": "string",
                        "description": "Absolute path to search in"
                    },
                    "pattern": {
                        "type": "string",
                        "description": "Filename glob pattern, e.g. '*.py'"
                    },
                    "max_results": {
                        "type": "integer",
                        "default": 20,
                        "description": "Maximum number of results to return"
                    }
                },
                "required": ["directory", "pattern"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
    if name != "search_files":
        return [TextContent(type="text", text=f"Unknown tool: {name}")]

    import glob
    import os

    directory = arguments["directory"]
    pattern = arguments["pattern"]
    max_results = arguments.get("max_results", 20)

    if not os.path.isdir(directory):
        return [TextContent(type="text", text=f"Directory not found: {directory}")]

    matches = glob.glob(os.path.join(directory, "**", pattern), recursive=True)
    matches = matches[:max_results]

    if not matches:
        return [TextContent(type="text", text="No files matched the pattern.")]

    result = f"Found {len(matches)} file(s):\n" + "\n".join(matches)
    return [TextContent(type="text", text=result)]

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await app.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="file-search-server",
                server_version="0.1.0",
            ),
        )

if __name__ == "__main__":
    asyncio.run(main())

Building an MCP client that discovers and calls tools

# client.py
import asyncio
import anthropic
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client


async def run_agent(user_message: str) -> str:
    server_params = StdioServerParameters(
        command="python",
        args=["server.py"]
    )

    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # Dynamic tool discovery — no hardcoded schemas
            tools_result = await session.list_tools()

            # Convert MCP tool schemas to Anthropic format
            anthropic_tools = [
                {
                    "name": tool.name,
                    "description": tool.description,
                    "input_schema": tool.inputSchema
                }
                for tool in tools_result.tools
            ]

            client = anthropic.Anthropic()
            messages = [{"role": "user", "content": user_message}]

            while True:
                response = client.messages.create(
                    model="claude-opus-4-5",
                    max_tokens=1024,
                    tools=anthropic_tools,
                    messages=messages,
                )

                if response.stop_reason == "end_turn":
                    text_blocks = [b for b in response.content if hasattr(b, "text")]
                    return text_blocks[-1].text if text_blocks else ""

                # Append assistant turn
                messages.append({"role": "assistant", "content": response.content})

                # Execute tool calls via MCP server
                tool_results = []
                for block in response.content:
                    if block.type == "tool_use":
                        result = await session.call_tool(block.name, arguments=block.input)
                        tool_results.append({
                            "type": "tool_result",
                            "tool_use_id": block.id,
                            "content": result.content[0].text if result.content else "",
                        })

                messages.append({"role": "user", "content": tool_results})


if __name__ == "__main__":
    answer = asyncio.run(run_agent("Find all Python files in /home/user/project"))
    print(answer)

When to use MCP vs rolling your own tool integration

Use MCP when:

  • Multiple agents or products need the same tool — build once, share everywhere
  • You want to use pre-built servers (databases, filesystems, APIs) from the ecosystem
  • Tools need out-of-process isolation (security boundary between agent and tool execution)
  • You are building a platform where teams will contribute their own tool servers

Roll your own when:

  • You have a single-provider, single-app tool with no reuse requirement
  • The added process overhead and stdio/HTTP complexity is not worth it for the use case
  • Your team is not yet familiar with MCP and you need to ship fast
  • Tools are purely in-memory transformations with no external I/O

The key signal: if you are writing the same tool integration code for the third time, it is time to build an MCP server.

Common mistakes

Registering too many tools on a single server. Every tool schema consumes context window tokens. A server with 50 tools sends 5,000+ tokens of schema before the user's message. Scope servers to a single domain (one server for database tools, one for file tools) and use tool filtering if discovery is dynamic.

Skipping input validation in the server. The MCP client passes tool arguments directly from the model's output. Models hallucinate parameter values. Always validate inputs server-side using Pydantic or explicit checks — JSON Schema in the tool definition tells the model what to send but cannot prevent it from sending garbage.

Using HTTP transport without authentication. Stdio transport runs locally and is sandboxed by OS process isolation. HTTP transport is exposed to the network. Add API key authentication or mTLS before deploying any HTTP MCP server that accesses sensitive data.

Treating MCP as a replacement for all tool patterns. MCP adds process and protocol overhead. For simple, single-use tools in a single application, direct function calling is faster to build and easier to debug. Adopt MCP for the reusability and isolation benefits, not because it is the newer pattern.

Try it yourself

Build a minimal MCP server that exposes two tools: read_file(path: str) -> str and write_file(path: str, content: str) -> str. Add input validation that (1) rejects paths outside a designated sandbox directory, (2) rejects writes larger than 10KB. Then write a client that connects to this server and runs a small agent loop: given the instruction "summarize the contents of README.md and save the summary to summary.txt", the agent should read the file, generate a summary using the LLM, then write it back using the tool. Verify that both the read and write paths hit the server correctly by adding a log line to each tool handler.

Lesson Deep Dive

Ask a follow-up question about this lesson and get an AI-powered explanation.

Loading previous questions...