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.

AI Digital Ideal
AI Digital Ideal
AI Digital Ideal
Standardizing LLM Tool Integration: How MCP Makes Harness a Universal Client‑Server Protocol

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 mcp

Step 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

MCPLLM integrationModel Context ProtocolClient‑ServerHarnessTool standardization
AI Digital Ideal
Written by

AI Digital Ideal

Express ideas with code, expand imagination with AI.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.