Standardizing LLM Tool Integration: How MCP Makes Harness a Universal Client‑Server Protocol
This tutorial explains the Model Context Protocol (MCP) introduced by Anthropic, showing how its three‑layer Host/Client/Server architecture and six primitives turn Harness into a standardized, USB‑like interface for connecting any data source or tool to large language models, with concrete examples, transport choices, and step‑by‑step implementation guidance.
Why a New Protocol?
After six lessons covering Harness components, hooks, subagents, and skills, Claude Code can only manipulate local files and run Bash. To let it query internal wikis, create GitHub PRs, read Slack history, or access Linear/Jira, Anthropic introduced the Model Context Protocol (MCP), a standardized client‑server protocol that acts like a USB interface for LLMs.
Theme 1 – Architecture
Problem with the old approach
Each IDE required a different tool definition.
Developers had to write three adapters for the same wiki tool.
Every IDE upgrade meant changing three places.
This leads to an "M × N" problem : M agent frameworks × N external tools = M × N integrations. A single standard protocol reduces this to M + N.
How MCP solves it
MCP defines three roles with single responsibilities:
MCP Host : the AI application that manages multiple clients.
MCP Client : a lightweight object inside the host that maintains a 1:1 long‑running connection to a server.
MCP Server : a JSON‑RPC service that exposes tools or data.
Example: Anthropic’s official GitHub MCP server can be used by Claude Code, Cursor, and Claude Desktop without any code changes.
Theme 2 – Primitives
Why six primitives instead of one generic "tool"?
Different use‑cases require different semantics:
Tools – execute commands (e.g., create_issue).
Resources – read‑only data sources (e.g., read a file).
Prompts – reusable prompt templates (e.g., PR description).
Sampling – server‑side LLM calls (e.g., summarization).
Elicitation – server asks the user for data (e.g., OAuth token).
Logging – server sends debug logs to the client.
Separating these prevents security and context‑size issues. For instance, OAuth login must be handled by Elicitation so the token never appears in the model’s context.
Theme 3 – Transport
Old pain points
Local tools (filesystem, git) cannot use HTTP.
Company‑wide services need a networked solution.
Streaming logs require a persistent connection.
Three transport options
stdio : client spawns the server as a subprocess and communicates via stdin/stdout. Ideal for personal, local tools.
Streamable HTTP : HTTP POST/GET with optional Server‑Sent Events. Suitable for remote services (e.g., Sentry, Notion) and team‑wide sharing.
SSE (deprecated) : older HTTP + SSE combo removed in the 2025‑11‑25 protocol version.
Choosing the right transport is a trade‑off between simplicity (stdio) and scalability (HTTP).
Theme 4 – Choosing Between MCP, Skill, and Subagent
A comparison matrix (converted to a list) clarifies the three mechanisms:
Skill : reusable knowledge or workflow; loaded on demand; defined in a markdown file.
Subagent : isolated task execution; launched when dispatched; runs in its own context.
MCP : external data or tool integration; registers at connection time; requires a JSON‑RPC server.
They are orthogonal and can be combined (e.g., MCP provides tools, Skill teaches the model how to use them, Subagent isolates heavy work).
Hands‑On: Build a Working MCP Server in 25 minutes
Goal
Create a "Project Stats" server exposing two tools: count_files (count files by extension) and find_todos (list TODO/FIXME comments).
Step 1 – Install the SDK
pip install mcpStep 2 – Write server.py
from mcp.server.mcpserver import MCPServer
from pathlib import Path
mcp = MCPServer("Project Stats")
@mcp.tool()
def count_files(directory: str, extension: str = "") -> str:
"""Count files in a directory, optionally filtered by extension.
Args:
directory: absolute path to scan
extension: file extension without dot (e.g. "py"). Empty = all files.
"""
p = Path(directory)
if not p.exists():
return f"Error: Directory not found: {directory}"
if extension:
files = list(p.rglob(f"*.{extension}"))
return f"{len(files)} .{extension} files in {directory}"
else:
files = list(p.rglob("*"))
return f"{len(files)} total files in {directory}"
@mcp.tool()
def find_todos(directory: str) -> str:
"""Find all TODO/FIXME comments in Python source files.
Returns file:line references for each match.
Args:
directory: absolute path to scan
"""
p = Path(directory)
if not p.exists():
return f"Error: Directory not found: {directory}"
todos = []
for f in p.rglob("*.py"):
try:
for i, line in enumerate(f.read_text(encoding="utf-8").splitlines(), 1):
if "TODO" in line or "FIXME" in line:
todos.append(f"{f}:{i}: {line.strip()}")
except (UnicodeDecodeError, PermissionError):
continue
if not todos:
return "No TODO/FIXME found"
return "
".join(todos[:20])
if __name__ == "__main__":
mcp.run(transport="stdio")Step 3 – Register the server in .mcp.json
{
"mcpServers": {
"project-stats": {
"type": "stdio",
"command": "python",
"args": ["${workspaceFolder}/server.py"]
}
}
}Using ${workspaceFolder} makes the config portable across machines.
Step 4 – Verify in Claude Code
Restart Claude Code.
Ask "list current MCP servers" → should show project‑stats.
Ask "use project‑stats to count .py files in the current directory" → should return a number.
Ask "use project‑stats to find all TODOs" → should return file:line list.
Step 5 (Bonus) – Switch to HTTP transport
Change the run call:
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="127.0.0.1", port=8000)Start the server ( python server.py) and update .mcp.json:
{
"mcpServers": {
"project-stats": {
"type": "http",
"url": "http://127.0.0.1:8000/mcp"
}
}
}Now any team member can connect via HTTP, demonstrating the plug‑in nature of the transport layer.
Debugging Checklist (3 steps)
Server discovery : If Claude Code cannot list the server, check that .mcp.json exists, is valid JSON, and the path is correct.
Standalone launch : Run python server.py manually; it should stay alive (stdio) or show the HTTP listening address.
Message inspection : Use the official MCP Inspector to view raw JSON‑RPC traffic and spot protocol mismatches.
Mini‑Quiz
Question 1 – OAuth login flow
Correct primitive: Elicitation . It lets the server ask the user for a token without exposing it to the model.
Question 2 – Team‑wide knowledge‑base server
Correct transport: Streamable HTTP . It enables a single remote server to serve many clients.
Question 3 – Querying large Slack history
Correct mechanism: MCP . The server exposes a search tool; the model calls it and receives truncated results.
Lesson 7 Summary
Key takeaways : Host → Client → Server three‑layer architecture eliminates the M × N integration explosion. Six primitives split responsibilities (Tools/Resources/Prompts vs. Sampling/Elicitation/Logging). Three transports (stdio, Streamable HTTP, SSE) let you choose between local and shared deployments. Selecting between Skill, Subagent, and MCP depends on whether you need knowledge reuse, task isolation, or external‑tool integration. The hands‑on example shows how to build a real MCP server, start with stdio, then upgrade to HTTP for team sharing.
Recommended Reading
MCP Architecture – deep dive into Host/Client/Server and the six primitives.
MCP Transports Spec – stdio and Streamable HTTP details.
MCP Server Implementations – SDK examples in Rust, Java, Python.
Claude Code MCP configuration ( .mcp.json) and Claude Desktop integration.
MCP Python SDK reference (MCPServer class, migration guide).
MCP Inspector – essential debugging tool.
FAQ
How does MCP differ from OpenAI Function Calling?
Function Calling is a model‑level capability that lets the model output a structured call. MCP is an application‑level protocol that standardizes how tools expose their schema to any agent, handling registration, transport, and security.
Will many MCP tools blow up the model context?
Yes, just like built‑in tools. Use the same token‑saving strategy from Lesson 3: keep core tools always loaded and load long‑tail tools on demand (e.g., alwaysLoad: true/false in the server config).
Can one MCP server serve multiple clients?
Only with HTTP (or Streamable HTTP). stdio is limited to a single client process.
How are permissions handled?
Exactly like built‑in tools, via permissions.allow / permissions.deny in the tool name (e.g., deny: ["mcp__filesystem__write_file"]). Hooks such as PreToolUse and PostToolUse also apply to MCP tools.
Is alwaysLoad: true the same as disable-model-invocation ?
No. alwaysLoad controls whether a server’s tools are always visible in the model’s context (MCP config). disable-model-invocation is a Skill‑level flag that prevents the model from automatically invoking that skill.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
