Three Interfaces, One Truth: Why Every Module Needs Web, REST, and MCP

By James Aspinwall, co-written by Alfred Pennyworth (my trusted AI) — February 27, 2026, 21:16


When you build a module — say, a contact manager or a task system — you write the logic once. Then someone asks for a web page. Then an API. Then an AI agent needs to call it. Suddenly you’re maintaining three interfaces to the same functionality, and the question becomes: is this a pattern or a mistake?

After building a 37,000-line Elixir application with 120 modules, I’ve landed firmly on: it’s a pattern. But only if you do it right.

Three Consumers, Three Languages

Each interface serves a fundamentally different consumer:

Web is for humans with browsers. They want visual layouts, navigation, forms with validation feedback, and the ability to scan information quickly. A human managing contacts wants a table they can sort and filter, not a JSON payload.

REST is for machines talking to machines. Frontend apps, mobile clients, third-party integrations, cron jobs, scripts. They want predictable endpoints, consistent response shapes, and proper HTTP status codes. They don’t care about your CSS.

MCP (Model Context Protocol) is for AI agents. Claude Code, automated workflows, agent-to-agent communication. They want tool descriptions that explain what a function does, structured input schemas, and error messages that help them self-correct. They don’t care about your endpoints or your CSS — they care about your documentation.

These aren’t three versions of the same thing. They’re three translations of the same truth into three different languages.

The Architecture That Makes It Work

The key insight is that interfaces should be thin. Paper-thin. All they do is:

  1. Receive a request in their native format (HTTP form, JSON body, MCP tool call)
  2. Extract parameters and the caller’s permissions
  3. Pass both to the business logic layer
  4. Translate the result back into their native format

Here’s what this looks like in practice with Elixir:

Web Route          REST Router         MCP Server
     \                  |                  /
      \                 |                 /
       → Permissions.Nis.search(params, permissions)
                        |
                  NIS (business logic)
                        |
                    Sqler (database)

The Permissions wrapper is the single gate. It checks authorization, calls the functional module, and returns {:ok, result} or {:not_allowed, reason}. Each transport layer translates that into its own idiom — a 403 JSON response, a redirect to a login page, or an MCP error object.

The business logic never knows or cares which interface called it. It never sees an HTTP request. It never formats JSON. It never builds HTML. It just does its job.

What You Gain

Consistency by construction. When all three interfaces call the same function through the same permissions wrapper, they can’t drift. You can’t have a bug where the REST API allows something the web UI doesn’t, because they’re not implementing authorization independently.

Test once, trust everywhere. You test the Permissions wrapper and the business logic. The interfaces are so thin they barely need their own tests — they’re just translation. Your 50-line REST router isn’t where bugs hide.

Add interfaces without fear. When MCP emerged as a protocol, adding tool definitions to existing modules took hours, not weeks. The logic already existed. The permissions already worked. All I needed was a new translation layer.

AI agents get first-class access. This is the one most teams miss. If your business logic is buried inside a web controller, an AI agent can’t reach it without pretending to be a browser. With a dedicated MCP interface, agents interact with your system using tools designed for how they think — structured schemas, clear descriptions, typed parameters.

What You Pay

Three surfaces to maintain. When a function signature changes, you update three call sites. When you add a parameter, you add it to three input parsers. This is real cost.

Three sets of documentation. Web users need screenshots and click-paths. REST consumers need endpoint references and curl examples. MCP users need tool descriptions and parameter schemas. I’ve started keeping separate manuals per access method — contacts-web.md, contacts-rest.md, contacts-mcp.md — because a single doc trying to serve all three audiences serves none of them well.

Temptation to over-build. Not every module needs all three interfaces. The moment you catch yourself building a web UI for a function that only an agent will ever call, you’ve crossed from architecture into busywork.

When to Build All Three, and When Not To

Build all three for core domain modules — the ones that represent your product’s actual value. Contact management, task tracking, content systems, monitoring dashboards. These have real human users, real API consumers, and real agent use cases.

Build one or two for supporting modules. A notification sender (like Pushover) only needs REST — it’s fire-and-forget, no human browses their push notifications in your app. A WhatsApp bridge that’s IP-gated to your local network only needs MCP — adding a REST API is just adding attack surface.

Build none for internal plumbing. Your database wrapper, your encryption module, your supervision tree — these are implementation details, not interfaces.

The rule I use: if you can’t name a real person or real agent that would use this interface today, don’t build it. “Someone might need it” is the most expensive sentence in software.

The AI-Native Angle

Here’s why this matters more now than it did two years ago.

AI agents are becoming a primary consumer of your software. Not a novelty, not an experiment — a primary consumer. When I work in my own codebase, Claude Code calls the same modules through MCP that I call through the web UI. It manages tasks, searches contacts, queries monitoring data, and sends messages. It’s not a secondary interface bolted on as an afterthought — it’s a peer.

If your architecture assumes the only consumers are humans and HTTP clients, you’ll end up retrofitting agent access as an awkward shim layer. If you design for three consumers from the start, adding AI capabilities is just adding another thin interface to logic that already works.

The companies that figure this out early — that treat MCP interfaces as seriously as they treat REST APIs — will have agent-ready products while their competitors are still trying to wrap Selenium around their web UIs.

The Pattern

One module. One permissions gate. Three interfaces. Each interface is a translator, not an implementor.

Build all three for your core modules. Build selectively for everything else. Document each interface for its actual audience. And never let business logic leak into a transport layer.

Three interfaces, one truth. That’s the architecture that scales — for humans, for machines, and for agents.