All articles

API, MCP Server, or AI Agent? Part 2: MCP Servers - The AI-Native Interface

In Part 1, we explored REST APIs and their limitations for AI integration. Now we'll dive into MCP Servers—the adaptation layer that makes data and tools accessible to AI agents in a way that supports reasoning and autonomy.


Back to Part 1


MCP Servers: The AI-Native Interface Layer

The Model Context Protocol, developed by Anthropic, addresses the gap between traditional APIs (designed for programmatic access) and AI systems (which need structured discovery, context, and tool definition). An MCP server is not a replacement for REST APIs—it's an adaptation layer that makes data and tools accessible to AI agents.

Core Characteristics of MCP Servers

Standardized Communication via JSON-RPC 2.0: Unlike REST's resource-oriented URLs, MCP uses JSON-RPC for method invocation. This provides a consistent interface regardless of what the server exposes.

Stateful Connections: MCP servers maintain persistent connections with clients. This allows for context preservation, capability negotiation, and efficient multi-turn interactions. "Stateful" here refers to connection-scoped context—the server can remember what happened earlier in the same session—not persistent storage across sessions.

Capability Negotiation: When a client connects, the server advertises what it can do (resources, tools, prompts). The client can discover capabilities dynamically rather than requiring pre-programmed knowledge.

Three Core Abstractions:

  • Resources: Data and context (files, database records, API responses)
  • Tools: Functions the agent can invoke (search, calculate, execute)
  • Prompts: Templated workflows for common tasks

Security-First Design: Explicit user consent for tool execution, data access controls, and audit trails. The protocol is designed with security as a foundation, assuming the host enforces consent and policy correctly. Tool execution requires user approval, though the UX and enforcement quality depend on the host implementation.

Composability: Multiple MCP servers can be connected to a single agent, each providing different capabilities. Servers are modular and reusable across different agents and use cases.


🔹 Model Context Protocol (MCP) Server - Detailed Breakdown

An MCP Server, per Anthropic's spec, is a component in a standardized protocol that allows AI systems (like agents or LLMs) to securely and flexibly access external data and tools. It's part of a broader architecture that includes:

  • Hosts: LLM applications initiating connections
  • Clients: Connectors within the host
  • Servers: Services providing context, tools, and resources

Key Features of MCP Server:

  • Standardized communication via JSON-RPC 2.0
  • Stateful connections with capability negotiation
  • Exposes:
  • Resources (data/context)
  • Prompts (templated workflows)
  • Tools (functions for execution)
  • Security-first design: Explicit user consent, data privacy, and tool safety
  • Composable: Can be reused across agents and workflows
  • Inspired by LSP (Language Server Protocol): Modular and ecosystem-friendly

Example: Weather Service as MCP Server

Here's our weather service from Part 1, reimagined as an MCP server:

from mcp.server import Server, McpError
from mcp.types import Resource, Tool, Prompt, PromptArgument, GetPromptResult, PromptMessage, TextContent
from typing import Any, Sequence
import json

# Initialize MCP server
server = Server("weather-service")

# Simulated weather database (same as REST example)
weather_db = {
    "seattle": {"temp": 55, "condition": "rainy", "humidity": 78},
    "miami": {"temp": 82, "condition": "sunny", "humidity": 65},
    "chicago": {"temp": 45, "condition": "cloudy", "humidity": 72}
}

# Resource: Expose current weather as structured data
@server.list_resources()
async def list_weather_resources() -> list[Resource]:
    """List available weather resources"""
    return [
        Resource(
            uri=f"weather://{city}",
            name=f"Current weather for {city.title()}",
            mimeType="application/json",
            description=f"Real-time weather data for {city.title()}"
        )
        for city in weather_db.keys()
    ]

@server.read_resource()
async def read_weather_resource(uri: str) -> str:
    """Read weather data for a specific city"""
    # Parse URI: weather://seattle -> seattle
    city = uri.split("://")[1].lower()
    
    if city not in weather_db:
        raise McpError(f"City {city} not found in weather database")
    
    data = weather_db[city]
    return json.dumps({
        "city": city,
        "temperature": data["temp"],
        "condition": data["condition"],
        "humidity": data["humidity"],
        "timestamp": "2025-01-11T10:00:00Z"
    })

# Tool: Allow agent to get weather forecast
@server.list_tools()
async def list_weather_tools() -> list[Tool]:
    """List available weather tools"""
    return [
        Tool(
            name="get_forecast",
            description="Get weather forecast for a city for specified number of days",
            inputSchema={
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "Name of the city"
                    },
                    "days": {
                        "type": "integer",
                        "description": "Number of days to forecast (1-7)",
                        "minimum": 1,
                        "maximum": 7
                    }
                },
                "required": ["city"]
            }
        ),
        Tool(
            name="compare_weather",
            description="Compare current weather between two cities",
            inputSchema={
                "type": "object",
                "properties": {
                    "city1": {"type": "string", "description": "First city"},
                    "city2": {"type": "string", "description": "Second city"}
                },
                "required": ["city1", "city2"]
            }
        )
    ]

@server.call_tool()
async def call_weather_tool(name: str, arguments: Any) -> Sequence[TextContent]:
    """Execute weather tools"""
    if name == "get_forecast":
        city = arguments["city"].lower()
        days = arguments.get("days", 3)
        
        if city not in weather_db:
            raise McpError(f"City {city} not found")
        
        data = weather_db[city]
        forecast = [
            {
                "day": i+1, 
                "temperature": data["temp"] + i,
                "condition": data["condition"]
            }
            for i in range(days)
        ]
        
        return [TextContent(
            type="text",
            text=json.dumps({
                "city": city,
                "forecast": forecast
            })
        )]
    
    elif name == "compare_weather":
        city1 = arguments["city1"].lower()
        city2 = arguments["city2"].lower()
        
        if city1 not in weather_db or city2 not in weather_db:
            raise McpError("One or both cities not found")
        
        data1 = weather_db[city1]
        data2 = weather_db[city2]
        
        comparison = {
            "cities": [city1, city2],
            "temperature_difference": abs(data1["temp"] - data2["temp"]),
            "warmer_city": city1 if data1["temp"] > data2["temp"] else city2,
            "conditions": {
                city1: data1["condition"],
                city2: data2["condition"]
            }
        }
        
        return [TextContent(
            type="text",
            text=json.dumps(comparison)
        )]
    
    raise McpError(f"Unknown tool: {name}")

# Prompt: Provide templated workflow for common tasks
@server.list_prompts()
async def list_weather_prompts() -> list[Prompt]:
    """List available weather prompts"""
    return [
        Prompt(
            name="travel_advisory",
            description="Generate travel weather advisory for a city",
            arguments=[
                PromptArgument(
                    name="city",
                    description="Destination city",
                    required=True
                ),
                PromptArgument(
                    name="days",
                    description="Trip duration in days",
                    required=True
                )
            ]
        )
    ]

@server.get_prompt()
async def get_weather_prompt(name: str, arguments: dict) -> GetPromptResult:
    """Generate prompt content"""
    if name == "travel_advisory":
        city = arguments["city"]
        days = arguments["days"]
        
        return GetPromptResult(
            description=f"Weather advisory for {days}-day trip to {city}",
            messages=[
                PromptMessage(
                    role="user",
                    content=TextContent(
                        type="text",
                        text=f"Please analyze the weather forecast for {city} over the next {days} days and provide a travel advisory including what to pack and any weather concerns."
                    )
                )
            ]
        )

Key Differences from REST API

Discovery: The MCP server advertises its capabilities (list_resources(), list_tools()). An AI agent can connect and immediately know what's available without pre-programmed knowledge.

Structured Tool Definitions: Each tool has a JSON schema describing its parameters. The AI agent can reason about which tool to use based on user intent and parameter requirements.

Stateful Context: The connection persists across multiple requests. If an agent calls get_forecast then compare_weather, the server can optimize subsequent calls or cache data.

Composable Workflows: The travel_advisory prompt shows how MCP servers can provide higher-level abstractions. Instead of just exposing raw data, they guide the agent through common workflows.

Security Layer: Tool execution requires explicit consent. When an agent wants to call get_forecast, the MCP framework can prompt the user: "Allow weather lookup for Seattle?"


REST API vs. MCP Server: Side-by-Side Comparison

FeatureREST APIMCP Server
ProtocolHTTP/RESTJSON-RPC 2.0
StateStatelessStateful
DiscoveryOpenAPI/Swagger (for humans)Capability negotiation (machine-readable)
Target ConsumerApplications, developersAI systems, agents
CommunicationClient makes HTTP requestsClient makes JSON-RPC calls
SecurityAPI keys, OAuth, rate limitsUser consent + tool safety
Tool DefinitionNone (endpoints only)Structured schemas with descriptions
ComposabilityVia API gatewaysVia MCP ecosystem
Error HandlingHTTP status codesMcpError with context
DocumentationOpenAPI, Swagger UISchema definitions, examples

When to Build an MCP Server

You should build an MCP server when:

You're Exposing Data/Tools to AI Agents: If your service will primarily be consumed by AI systems making autonomous decisions, MCP provides better abstractions than REST.

You Need Discovery: If agents shouldn't hard-code your endpoints but should dynamically discover what's available, MCP's capability negotiation is essential.

You Want Reusability: If multiple different AI agents will use your service (chatbot, automation system, research assistant), MCP's standard protocol ensures compatibility.

You're Building in the AI-Native Stack: If you're already using Claude, other MCP-compatible agents, or building within the MCP ecosystem, you get interoperability for free.

You Don't Need an MCP Server if:

  • You're building a traditional web service for human-facing applications
  • Your API is primarily consumed by non-AI clients
  • You need broad compatibility with existing HTTP tooling
  • Your service is simple enough that wrapping REST calls in agent code is sufficient

Common Pattern: REST + MCP Wrapper

In practice, many systems use both:

┌──────────────┐
│  REST API    │ ← Traditional clients (mobile apps, web)
└──────┬───────┘
       │
┌──────▼───────┐
│ MCP Adapter  │ ← AI agent clients
└──────────────┘

The REST API serves traditional clients. The MCP server wraps the same backend for AI clients. Both coexist peacefully.

What's Next

In Part 3, we'll explore AI Agents—how they consume MCP servers autonomously, make decisions, orchestrate multiple tools, and tie everything together in a complete real-world example.


Back to Part 1

Continue to Part 3


Shaped in collaboration with Claude, an AI assistant by Anthropic, during sunny Pacific Northwest afternoons where engineering problems meet philosophical questions.

Continue Reading

Part 3: AI Agents

Where capability negotiation graduates into autonomous tool selection.

← Back to Part 1

The REST baseline that MCP extends and partially replaces.

Blockchain and AI Agents

MCP delegates authentication to the host layer. This explores when that delegation is sufficient and when cross-boundary trust needs stronger guarantees.

Agentic AI Is Distributed Systems — Part 1

MCP's capability negotiation maps to service discovery patterns from Consul and etcd (2013) — full lineage included.