DEV Community

Cover image for REST APIs Are Terrible for AI Agents. I Switched to MCP. Here Is Why.
Kevin
Kevin

Posted on

REST APIs Are Terrible for AI Agents. I Switched to MCP. Here Is Why.

REST APIs Are Terrible for AI Agents. I Switched to MCP. Here Is Why.

TL;DR: REST APIs force AI agents to guess endpoints, parse unpredictable responses, and break on every schema change. I migrated my agent tooling to the Model Context Protocol (MCP) and eliminated an entire class of integration failures. Here is what REST got wrong, what MCP does differently, and a real migration in under 100 lines of code.


The Problem I Kept Running Into

When I started building AI agents that interact with external services, I did what every developer does: I wrote REST wrappers. The agent calls a tool, the tool hits an API, the API returns JSON, the agent reads the JSON. Simple.

Except it was not simple. Here is what actually happened:

  1. The agent calls search_customers with {"name": "Müller"}.
  2. The tool sends GET /api/v2/customers?q=Müller.
  3. The API returns paginated results wrapped in { "data": { "items": [...] } }.
  4. The agent cannot parse the nested structure and returns: "I could not find any customers."
  5. I check the logs. The API returned 14 results. The agent just could not read them.

This happened daily. Every new API integration required writing a translation layer between what the API returned and what the LLM could understand. Schema changes on the API side broke the agent silently. Error responses were cryptic JSON blobs the LLM interpreted as valid data.

I was spending more time writing glue code than building features. So I switched to MCP.

What MCP Does Differently

The Model Context Protocol (MCP) is a standard for how AI agents discover and use tools. Instead of an agent guessing endpoints and parsing raw JSON, MCP provides:

  • Self-describing tools. The server tells the agent exactly what tools exist, what parameters they accept, and what they return. No OpenAPI spec drift, no guesswork.
  • Structured output. Responses follow a consistent envelope. The agent knows what success and failure look like without regex-parsing HTTP status codes.
  • Streaming and progress. Long-running operations report progress, so the agent does not time out waiting for a synchronous REST call.

The key insight: MCP treats the agent as a first-class client, not an afterthought bolted onto a human-facing API.

Before and After: A Real Migration

Here is what a typical REST-based tool looks like for an agent:

# REST approach: fragile, verbose, manual
import requests

def search_customers_rest(query: str) -> str:
    try:
        r = requests.get(
            "https://api.example.com/v2/customers",
            params={"q": query},
            headers={"Authorization": f"Bearer {API_KEY}"},
            timeout=10
        )
        r.raise_for_status()
    except requests.Timeout:
        return "Error: API timed out."
    except requests.HTTPError as e:
        return f"Error: API returned {r.status_code}."

    data = r.json()
    # Three levels of nesting because REST APIs love wrapping
    items = data.get("data", {}).get("items", [])
    if not items:
        return "No customers found."

    # Manual formatting for the LLM
    lines = []
    for c in items[:5]:
        name = c.get("attributes", {}).get("display_name", "Unknown")
        email = c.get("attributes", {}).get("email", "N/A")
        lines.append(f"- {name} ({email})")

    result = "\n".join(lines)
    if len(items) > 5:
        result += f"\n\n... and {len(items) - 5} more. Narrow your search."
    return result
Enter fullscreen mode Exit fullscreen mode

Every integration looked like this. Twenty lines of error handling, status code interpretation, response unwrapping, and LLM-friendly formatting. Per endpoint. And any API change broke it at runtime with no warning.

Now here is the MCP version:

# MCP approach: self-describing, structured, resilient
from mcp import Client

mcp = Client("https://api.example.com/mcp")

# Tools are discovered automatically. The agent sees:
#   search_customers(query: str) → CustomerList
# No OpenAPI docs needed. The tool describes itself.

result = await mcp.call_tool("search_customers", {"query": "Müller"})

if result.error:
    return f"Error: {result.error.message}"  # Structured, not status codes

# The result is typed. No unwrapping needed.
customers = result.content
if not customers:
    return "No customers found."

lines = [f"- {c.display_name} ({c.email})" for c in customers[:5]]
return "\n".join(lines)
Enter fullscreen mode Exit fullscreen mode

The difference is not just fewer lines. It is that every failure mode is now explicit. The agent can reason about result.error.message because it is structured. Schema changes are caught at connection time when the server describes its tools, not at runtime when the agent tries to parse a response.

How MCP Tool Discovery Works

When an MCP client connects, the server sends a JSON manifest describing every available tool:

{
  "tools": [
    {
      "name": "search_customers",
      "description": "Search customers by name, email, or company.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "Search term (name, email, or company)"
          },
          "limit": {
            "type": "integer",
            "description": "Max results (default 10, max 50)",
            "default": 10
          }
        },
        "required": ["query"]
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This manifest becomes the agent's function definition automatically. I do not write OpenAPI specs by hand. I do not maintain a separate tool registry. The server is the source of truth, and every agent that connects to it stays in sync.

When MCP Is Not the Answer

MCP is not a silver bullet. Here is when REST still makes sense:

Scenario Use REST Use MCP
Human-facing API with UI
Agent-only tool server
Existing third-party service (Stripe, GitHub)
Internal services you control
Simple read-only data fetch
Multi-step, stateful operations

For services I control (internal APIs, microservices, data pipelines), I now build MCP servers by default. For third-party APIs, I wrap them in an MCP server that handles the REST-to-MCP translation once, centrally, instead of in every agent.

What I Would Tell My Past Self

A year ago, I was writing REST wrappers for every agent integration. I had a folder called tools/ with 47 files, each one a fragile translation between an API and an LLM. Every API update broke something.

If I could send a message back in time:

  1. Build an MCP server, not a wrapper. One server that describes all your tools. Every agent connects to the same source of truth.
  2. Let the server handle formatting. The agent should receive structured data, never raw API responses. Error handling lives in the server, not duplicated across agents.
  3. Test tool descriptions like you test code. If search_customers returns a name field today and a display_name tomorrow, your tool description should catch that mismatch at connection time, not at 3 AM when a customer complains.

The Bottom Line

REST APIs were designed for humans and frontend apps. They assume a client that understands pagination, error codes, and nested response envelopes. AI agents do not. They need structured, self-describing interfaces that surface errors clearly and adapt to schema changes.

MCP gives agents exactly that. If you are building AI agents that talk to services, stop writing REST wrappers. Build an MCP server. Your agents will break less, your code will be simpler, and you will spend your time on features instead of glue code.


I build AI agent infrastructure at centerbit. If you are interested in MCP, agent tooling, or HITL workflows, more at centerbit.co.

Top comments (0)