← Back to list

HATEOAS as the Cure for MCP Tool Bloat?

Why an old REST principle is the most elegant answer to one of the AI agent ecosystem’s newest problems

Jay Hamilton · 2026-04-21 13:22 · 0 claps · 6.1 min read
#hateoas #model-context-protocol #rest-api #llm-agent #agentic-ai-architecture
Open on Medium ↗
Wiki topics: LLM · Large Language Models AGT · AI Agents 🏛️ · Architecture

HATEOAS as the Cure for MCP Tool Bloat?

Why an old REST principle is the most elegant answer to one of the AI agent ecosystem’s newest problems

In a prior article, I explored the value of HATEOAS in system design. Here, I extend that thinking into the GenAI space — specifically, how it applies to the Model Context Protocol.

The Model Context Protocol (MCP) has quickly become the standard plumbing for connecting large language models to the outside world. It’s elegant in concept: give an LLM a set of tools, and it can autonomously call APIs, query databases, and trigger actions on your behalf. But there’s a quiet architectural problem lurking inside most MCP server implementations — one that gets uglier as APIs grow.

The fix, it turns out, was already described in Roy Fielding’s 2000 dissertation. It just took the AI agent era to make us care about it again.

The Problem: MCP Servers That Mirror REST APIs One-for-One

When developers build MCP servers on top of existing REST APIs, the path of least resistance is to map every endpoint to a tool. A modest e-commerce API produces something like this tool manifest:

// Typical naive MCP tool manifest for a REST API
[
  "list_products",   "get_product",     "create_product",
  "update_product",  "delete_product",   "list_categories",
  "get_category",    "list_orders",      "get_order",
  "create_order",    "cancel_order",     "ship_order",
  "list_users",      "get_user",         "suspend_user",
  "list_payments",   "refund_payment",   "get_invoice"
  // ...grows with every new endpoint
]

⚠ The Four Failure Modes

1. Context window saturation. Tool definitions consume tokens. At scale, the tool manifest alone eats a significant fraction of your available context window.

2. Decision quality degradation. LLM tool-selection accuracy drops measurably as tool count increases. 150 tools is meaningfully worse than 15, even with good descriptions.

3. Tight API coupling. The MCP server must encode precise knowledge of every URL, HTTP method, and parameter. API changes break the server silently.

4. No contextual gating. The full tool set is always exposed. An LLM can attempt to cancel an already-shipped order because cancel_order is always in the manifest.

“The LLM shouldn’t need to know what’s possible upfront — the API should tell it what’s possible right now.”

HATEOAS: A Quick Primer

HATEOAS — Hypermedia As The Engine Of Application State — is the REST constraint that most APIs quietly skip. The idea: responses include links to the actions available from the current state. A client starting at a root URL and following links can discover and use the entire API without out-of-band documentation.

🚫 Plain REST Response

{
  "id": 9021,
  "status": "processing",
  "total": 149.99
}
  • Client must know valid actions
  • No state context in response
  • Must consult docs for transitions

✅ HATEOAS Response (HAL)

{
  "id": 9021,
  "status": "processing",
  "total": 149.99,
  "_links": {
    "self":   { "href": "/orders/9021" },
    "cancel": { "href": "/orders/9021/cancel",
                 "method": "POST" },
    "items":  { "href": "/orders/9021/items" }
  }
}

A shipped order returns a track link and return link, but not cancel. The API surface is a function of state — exactly the property we want to exploit in MCP.

The HATEOAS-Driven MCP Pattern

Instead of mapping every API endpoint to a distinct MCP tool, expose one general-purpose navigation tool and let the LLM traverse the API’s hypermedia graph.

The Single-Tool Manifest

// The entire MCP tool manifest — one tool replaces hundreds
{
  "name": "api_navigate",
  "description": "Navigate the API hypermedia graph. Start at '/' to discover resources, then follow _links in responses. Each response tells you what actions are valid from the current state.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "href":   { "type": "string", "description": "URL from a _links entry" },
      "method": { "type": "string", "enum": ["GET","POST","PUT","PATCH","DELETE"], "default": "GET" },
      "body":   { "type": "object", "description": "Payload for mutations" }
    },
    "required": ["href"]
  }
}

A Traversal Session in Action

Task: “Cancel order 9021 if it’s still cancellable.”

LLM Navigation Session

1 api_navigate({ href: “/orders/9021” })

Fetch the order resource directly

2 Response includes _links.cancel — status is “processing”

Link presence tells the LLM cancellation is valid right now

3 api_navigate({ href: “/orders/9021/cancel”, method: “POST” })

LLM follows the link — no URL construction from memory

4 Response: status “cancelled”, _links.cancel is gone

State machine enforced server-side; confirmation arrives naturally

If the order had already shipped, step 2 returns no cancel link. The LLM reports it can't be cancelled — without any special error logic in the MCP server.

Implementation: Java Spring Boot

Spring HATEOAS makes contextual link generation clean and declarative:

// OrderAssembler.java — contextual links per resource state
import org.springframework.hateoas.*;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
@Component
public class OrderAssembler
    extends RepresentationModelAssemblerSupport<Order, EntityModel<Order>> {
  @Override
  public EntityModel<Order> toModel(Order order) {
    // Self link is always present
    EntityModel<Order> model = EntityModel.of(order,
        linkTo(methodOn(OrderController.class)
            .getOrder(order.getId())).withSelfRel());
    // State-gated links — business rules live here, not in MCP
    if (order.isCancellable()) {
      model.add(linkTo(methodOn(OrderController.class)
          .cancelOrder(order.getId())).withRel("cancel"));
    }
    if (order.isShipped()) {
      model.add(linkTo(methodOn(OrderController.class)
          .trackOrder(order.getId())).withRel("track"));
      model.add(linkTo(methodOn(OrderController.class)
          .initiateReturn(order.getId())).withRel("return"));
    }
    if (order.isRefundable()) {
      model.add(linkTo(methodOn(OrderController.class)
          .refundOrder(order.getId())).withRel("refund"));
    }
    return model;
  }
}

The MCP Server Becomes a Thin Proxy

# Python MCP server — the entire tool implementation
import httpx
from mcp.server import Server
from mcp.types import Tool, TextContent
app = Server("hateoas-mcp")
BASE_URL = "https://api.example.com"
@app.list_tools()
async def list_tools():
    return [Tool(
        name="api_navigate",
        description="Navigate the API hypermedia graph. Start at '/' then follow _links.",
        inputSchema={
            "type": "object",
            "properties": {
                "href":   {"type": "string"},
                "method": {"type": "string", "default": "GET"},
                "body":   {"type": "object"}
            },
            "required": ["href"]
        }
    )]
@app.call_tool()
async def call_tool(name: str, arguments: dict):
    async with httpx.AsyncClient() as client:
        response = await client.request(
            method=arguments.get("method", "GET"),
            url=BASE_URL + arguments["href"],
            json=arguments.get("body"),
            headers={"Accept": "application/hal+json"}
        )
    # Pass HAL response through — links are self-describing
    return [TextContent(type="text", text=response.text)]

The MCP server has zero knowledge of routes, resource shapes, or business rules. It’s a credentialed HTTP proxy. All intelligence lives in the API’s hypermedia responses — exactly where it belongs.

Addressing the Four Failure Modes

✅ How HATEOAS Resolves Each Problem

Tool bloat → One tool. A single api_navigate replaces N endpoint-specific tools. Context window usage is dominated by response data, not tool definitions.

Decision degradation → Eliminated. There’s no multi-tool selection problem. The LLM picks from links in the current response, not a global manifest of every possible operation.

Tight coupling → Decoupled. The MCP server is a dumb proxy. API routes change? Nothing in the MCP layer needs updating.

No contextual gating → Server-enforced. Business rules about valid transitions live in the API’s link-generation logic. The LLM can only invoke actions that are surfaced as links. Invalid state transitions become structurally impossible.

Hybrid Approaches: When to Blend

Semantic entry points

Rather than one global tool, expose a handful of domain-level entry points: navigate_orders, navigate_users, navigate_catalog. Each roots navigation at a specific aggregate. This preserves hypermedia benefits while giving the LLM cleaner intent-routing without enumerating leaf operations.

Write-optimised shortcuts

For high-frequency mutations with well-known, stable schemas (e.g., place_order), a dedicated tool with a typed input schema improves reliability over free-form body construction. Reserve these for stable, high-volume operations — not the long tail.

Tool-per-bounded-context

In a microservices architecture, one api_navigate tool per domain service (orders, inventory, identity) gives the LLM a clean mental model while keeping each service's internal graph navigable via hypermedia.

· · ·

The Frontend Parallel: Links Over Hard-Coded Paths

This pattern mirrors a principle any frontend developer knows intuitively. When you build UI navigation, you don’t hard-code absolute API paths into every component — you follow references the server gives you and let routing logic live where it belongs. The component declares intent; the server resolves the path.

The same inversion of control applies to LLM agents. Instead of encoding every endpoint upfront, the agent follows relations the API surfaces in each response. The server owns what’s valid. The client — human or AI — simply follows.

Practical Considerations

APIs that aren’t HATEOAS-ready

For third-party APIs without hypermedia, you can inject a static link-map in the MCP proxy layer — a JSON config describing which links attach to which resource types and states. It’s not as dynamic as real HATEOAS, but it preserves the single-tool pattern and moves coupling out of the tool manifest into configuration.

Response verbosity

HAL responses are larger. For context-sensitive scenarios, your MCP proxy can trim _links entries to a condensed summary (relation name + href only) before passing them to the LLM — a few extra lines of Python that pay for themselves in reduced context pressure.

System prompt contract

Explicitly describe the navigation contract in your agent’s system prompt: “When you receive a response, look for a _links field. The keys are relation names. Follow links using api_navigate. Never construct URLs manually." This small addition dramatically improves traversal reliability.

Closing Thoughts

The irony is rich: HATEOAS was considered impractical for human-driven clients for years — frontend developers didn’t want to traverse link graphs, they wanted predictable, bookmarkable REST endpoints. But LLM agents are not human developers. They don’t need stable, memorisable URLs. They are perfectly suited to exploratory, link-following traversal.

The constraint that felt like friction for humans turns out to be a superpower for AI agents. The MCP ecosystem is young and patterns are still settling — but the core insight holds: the right level of abstraction for an LLM is not an endpoint, it’s a graph of possibilities. HATEOAS gives you that graph. It always did. We finally have a client that knows how to use it.


메타데이터
post_id
7c60dfdbde05
slug
hateoas-as-the-cure-for-mcp-tool-bloat-7c60dfdbde05
url
https://medium.com/@jaystevenhamilton/hateoas-as-the-cure-for-mcp-tool-bloat-7c60dfdbde05
canonical_url
https://medium.com/@jaystevenhamilton/hateoas-as-the-cure-for-mcp-tool-bloat-7c60dfdbde05
author_url
https://medium.com/@jaystevenhamilton
status
ok
fetched_at
2026-06-09 15:37:30