23 min read
Building a real SaaS like application exclusively on top of MCP with no User Interface
Posted on August 12, 2026
We set out to answer one question. How mature is the Model Context Protocol for real work, and what does building on it make possible that wasn't possible before?
So we built something. Mailoo is a drip email campaign engine with no dashboard, no login page and no canvas. You create a campaign by asking for one. You enroll contacts by asking. There is nothing to look at.
An MCP-only application is one whose entire product surface is a tool contract — there's no interface to ship, because the whole interaction happens in the 3rd party client using plain natural language. You literally talk your way to business outcomes.
We decided to build a drip email campaign software because it's a mature type of product people know what to expect from, but it is operationally complex. In order to use it effectively one must deeply know the internals of the tool, and quite literally - connect the dots to create complex workflows and campaigns.
Mailoo is internal R&D — not deployed, no customers, no production traffic. The only hypothesis we wanted to prove is, can we build a non-trivial piece of business software where the only possible interface is conversational, not graphical.
The answer is yes.
What Mailoo does: drip campaigns as graphs, run entirely by conversation
It does what Customer.io or Klaviyo do, minus the canvas. Campaigns are directed acyclic graphs. Fourteen step types — send an email, wait for an event, branch on a condition, split for an A/B test, update a contact, check a goal, end. Seven trigger types. Enrollment tracked per contact, with parallel branches, exit conditions, goals and a re-entry policy.
You ask for a five-email welcome series with a branch after email two for people who opened it, and one exists. You ask how it's performing and you get the funnel.
You manage the full complexity of a drip email campaign not via graphical interface but using plain language from your AI assistant of choice.
The fifteen agent-first design principles we wrote before any code
Before any code, we wrote down what an agent-first service should look like. Fifteen principles. They came out of one conversation with Claude Opus, and they're the part of this that transfers to a service with nothing to do with email.
Most of them read like ordinary API advice. Each one describes a habit that costs you nothing behind REST and costs you the product behind MCP, and the reason is always the same substitution: your caller can no longer find anything out except from you.
The five principles that mattered most: schema clarity, outcome-based tools, retryable errors, built-in discoverability, and consistent validation
Your schema is not a contract. It is the documentation.
Behind a REST API, a schema tells an integration team what shape to send, and if it's ambiguous they read the README, check a test, or ask in Slack. The schema is one channel among several.
An agent has none of the others. No README, no test suite, no colleague. It has your tool name, your description, your parameter schema and whatever your last error told it. That's the entire surface. A field described as "configuration object" is, to the model, a field with no meaning at all, and it will guess.
The practical rule we now apply: if typed data sits behind an untyped field anywhere in your MCP schema, you have a hole, and the agent will fall into it.
Design tools around outcomes, not endpoints.
The instinct from REST is one operation per resource — create the campaign, add a step, add a transition, add a trigger, activate. Clean, composable, and it turns one outcome into five separate calls.
Models are good at writing a complete document and poor at holding a plan across a chain of calls with interdependent parameters. Every extra hop is a chance to lose the thread. So Mailoo takes a whole campaign specification in one call, validates the entire graph, and creates everything at once.
The teams publishing about this have landed in the same place. Block collapsed a Linear MCP server from more than thirty tools to two. Salesforce's Headless 360 exposes the platform through four. We shipped twenty-one, and we'd ship twenty-one again. Fourteen step types and seven trigger types have to be nameable somewhere, and folding them behind one tool with a type parameter relocates the ambiguity into a field — which is the hole the previous principle exists to close. Tool count follows the domain, not a target.
Errors are the retry instructions.
The person reading your error message is not a person. It's a model that will try again in seconds using whatever you just told it.
"Validation failed" produces another guess. So does "Required at templateId." What produces a correct retry is the expected shape: which fields, what types, what constraints, and where to find the rest. Every error is a chance to teach the caller how to succeed, and an error that only reports failure spends tokens to produce another failure.
Errors in MCP should read more naturally and are basically instructions for the AI agent consuming the MCP server how to unblock the flow in the next step. It's an opportunity to give real context that AI agents can reason through or pull other resources rather than just sending raw, dry error codes with basic labels that are usually cryptic even for a human.
Ship a way to ask what's possible.
Any input that changes shape depending on a type field needs a tool that describes the variants. Fourteen step types means fourteen different config shapes, and an agent that can only discover them by trial and error will spend your context budget on trials.
We added describe_step_types after this bit us. It returns, for every step type and trigger type, the field names, their types, whether they're required, their constraints, their defaults, and a minimal working example. It's the single highest-value tool in the server and it does nothing except explain the others.
Your validator must reject exactly what your creator rejects.
A dry-run tool that says yes while the create path says no is worse than having no dry-run tool. The agent trusts it, commits, fails, and now has contradictory information about your system.
Ours drifted apart because structural validation lived in the MCP layer and domain validation lived in the domain layer. Clean architecture, and from the caller's side it made the validator a liar. Both paths now run the same validators.
All fifteen principles in full, and the four the spec broke six months later
| # | Principle | What REST taught us | What MCP requires | Why it matters |
|---|---|---|---|---|
| 1 | MCP-native from day one | The API is the product; docs live elsewhere | The tool contract is the product surface — and since 2026-07-28, one served without a session, a handshake or a GET stream | Agent hosts and directories are the distribution channel. If they can't find you, you don't exist |
| 2 | Design from workflows, not endpoints | One operation per resource is good hygiene | One tool per complete outcome | Models lose the thread across long chains; every hop is a chance to fail |
| 3 | Schemas are prompts | Schema is a validation contract | Schema is the only documentation that exists | There is nobody for the caller to ask |
| 4 | Token-efficient, action-oriented responses | Return the resource; the client picks fields | Return what's needed for the next decision, paginated, with detail levels | Everything you return is subtracted from the caller's room to think |
| 5 | Errors guide recovery | Errors are for a developer reading logs | Errors carry the expected shape, not just the violation | The agent retries in seconds with exactly what you gave it |
| 6 | Idempotent, deterministic, retryable | Good practice | Protocol requirement since 2026-07-28 — a broken stream loses the request and the client must re-issue | Retries are no longer optional politeness. In an email engine they're duplicate sends |
| 7 | One declarative spec per operation | Build incrementally across many calls | One document, one call, validated whole | Models write documents well and orchestrate poorly |
| 8 | Ship prompt templates as accelerators | No equivalent | Encode your domain's good patterns as invocable templates | The agent knows language, not your industry's conventions |
| 9 | Expose queryable state, and a real validator | Reads are an afterthought next to writes | The agent needs to read state to decide, and to test work before committing | It can't glance at a dashboard to see what happened |
| 10 | Annotate tool risk | The HTTP verb carries the risk | readOnlyHint and destructiveHint decide what runs without a human confirming | Your metadata builds the approval prompt. Mixing a read and a write in one tool fails review outright |
| 11 | Version everything, break nothing | Consumers upgrade on their own schedule | The caller may never update, and the protocol moves underneath you. Our original wording said to advertise capabilities during initialization — 2026-07-28 removed initialization | Backwards compatibility is the only compatibility you get |
| 12 | Observability built in | Your logs, your problem | Correlation IDs and per-call latency, because you cannot see what the agent saw | You're debugging a conversation you weren't part of |
| 13 | OAuth 2.1, not API keys | An API key is fine | OAuth 2.1 — and Dynamic Client Registration was deprecated on 2026-07-28 in favour of Client ID Metadata Documents | The identity belongs to a human and is being exercised by software on their behalf |
| 14 | Ship an SDK agents can write code against | An SDK is a convenience | An escape hatch for anything bulk or repetitive | Five hundred enrollments is a script, not a conversation |
| 15 | LLM-friendly data structures | UUIDs and numeric enums are fine | Human-readable keys, references by name, shallow nesting, string enums | The caller regenerates this structure from scratch every time |
Four of the fifteen aged in the six months since we wrote them, all because the protocol moved on 28 July 2026. Principle 1's transport advice changed — the HTTP GET stream and session IDs are gone. Principle 6 was promoted from advice to requirement. Principle 11's "advertise your capabilities during initialization" became impossible, since there's no longer an initialization. And Principle 13's recommendation to support Dynamic Client Registration now points at a deprecated mechanism.
Write your own principles with a date and a spec version at the top. Ours would have been quietly wrong otherwise.
The principle we broke: an untyped config field that stopped Claude Desktop from creating campaigns
We shipped twenty-one tools and Claude Desktop couldn't create a single campaign. The agent had no way to know what belonged in a step config, so it guessed, and every guess came back rejected.
We had violated Principle 3 — the one about schemas being documentation — in the most complete way available: the config field was an open, untyped object.
Our principles document said never put typed data behind an untyped bag. Our data model, written in the same conversation an hour apart, said this about the Step entity:
Using JSON for
configrather than separate tables per step type keeps the schema simple while allowing arbitrary type-specific data. The orchestration engine validates config against the expected schema for eachstep_typeat runtime.
Both documents were loaded. They contradicted each other. What shipped was a faithful implementation of the design note and a direct violation of the principle.
Our read, as a hypothesis: when an architecture spec and an interface-design document disagree, the one with tables in it wins. We haven't gone back through the session history to confirm the model reasoned that way. But we changed how we write specs — the storage model and the interface contract now live in one document, and disagreements get resolved before anything gets built.
A second thing turned up in that same file. The Step entity carries position_x and position_y, documented as "optional visual editor coordinate." Mailoo has no visual editor. Those fields exist because the data model was validated against Customer.io, Klaviyo, Braze Canvas, HubSpot Workflows and Iterable Journeys — five products operated by a human dragging boxes around a canvas. The assumptions travelled with the patterns.
Nobody decided to put them there. Check your own specs for the equivalent.
Fixing it took hours. Introspection tool, errors rewritten to carry the expected shape, both validation paths wired to the same domain validators. After that, campaigns got created.
Installation, not agent reasoning, was the hardest part of this whole build
The genuinely irritating hour had nothing to do with agents. We were on Claude Desktop, which meant hand-editing claude_desktop_config.json to point at a local server, and it took four or five iterations before the JSON was right and the client could find it.
Paths have to be absolute. A full quit and restart is required after every edit. And "Tool calls failing silently" is a named heading in the official troubleshooting docs, which tells you how routine that failure is.
The curve is steep and pointing the right way.
| Date | What shipped | What it replaced |
|---|---|---|
| Nov 2024 | MCP launches. Local servers only, hand-written JSON config | — |
| Mar 2025 | claude mcp add in Claude Code, then .mcp.json project scope you can commit | Hand-editing JSON; per-machine config |
| Mar 2025 | Spec 2025-03-26: Streamable HTTP and OAuth 2.1 make remote servers viable | Local-only |
| May 2025 | Remote MCP servers land in Claude | Local-server-only Claude |
| Jun 2025 | Cursor 1.0: one-click install and the "Add to Cursor" button | Hand-editing mcp.json |
| Jun 2025 | Desktop Extensions: download, double-click, confirm | Hand-editing claude_desktop_config.json |
| Jul 2025 | Claude Connectors Directory | Manual URL entry |
| Sep 2025 | .dxt becomes .mcpb; Nov 2025, Anthropic donates the format to the MCP project | Vendor-owned packaging |
| Oct 2025 | Claude Code plugins and marketplaces bundle servers together | Per-server installs |
| Jul 2026 | Spec 2026-07-28 removes sessions, so any request can hit any instance behind ordinary load balancing | Sticky sessions and shared session stores |
Installing a remote connector today is a handful of clicks and an OAuth consent — no URL, no JSON, no file on disk. A local bundle is a double-click. In under two years this went from "hand-write this config and restart the app" to "click connect."
It isn't finished. Bundles are still local-only and per-user, so a phone or a browser session gets none of it, and capability discovery over a well-known URL is still a draft proposal with three competing paths. Our bet: install stops being a topic within a year. What's left is the kind of gap that closes by shipping.
How mature is MCP really? Ready for single-call workflows, not open-ended exploration
Ready for work you can express as a small number of complete operations. Not yet ready for work that needs an agent to explore.
On MCP-Universe — 231 tasks across 11 live servers — GPT-5 solved 43.72%. On DynamicMCPBench, across 121 live servers, accuracy falls from 39% on chains of one or two tools to 13% on chains of five or more. Mailoo works anyway.
Those benchmarks measure long chains across unfamiliar tools. Mailoo never asks for one. A single call carries a complete campaign specification, so there's no thread for the model to lose.
Maturity isn't a property of the protocol. It's a property of the fit between the protocol and the job you're giving it. Shape the work into single complete operations and you're operating in the part of the curve that already works. Shape it as exploration and you're betting on a coin flip.
The same holds for the public server population. mcpqueen, a third-party monitor grading on a rubric of its own, probed 9,326 remote servers in July 2026 and found 17.2% of them unreachable. Treat published servers as a reference library, not a supply chain, and build the ones you depend on.
What it cost: eight hours of specification, two to three days of delivery
Eight hours of human time, spent almost entirely on deciding rather than producing. One conversation with Claude Opus, fewer than ten messages, each one constructed rather than typed. Out of it came the data model and the fifteen principles.
Nucleus, our delivery pipeline, built and shipped the working system from that spec in two to three days. A few more hours of conversation to polish the flow.
To be very specific, this is not a vibe-coded piece of slop. Nucleus is a strict delivery pipeline with end-to-end process and delivery practices, from scoping, specification, engineering, audit and delivery. This is a fully working, non trivial piece of software.
Implementation was the cheap half. When building gets this cheap, the quality of the specification stops being one input among several and becomes most of the product — Mailoo is a good drip engine because the data model is good, and nothing downstream could have rescued a bad one.
What MCP-only unlocks: no frontend to ship, and composition across services
Software has reached people the same way for as long as any of us have been shipping it. You install a package and open it. You go to an address. You download an app. Every time it's one bundle serving one purpose, and getting that bundle in front of someone is its own discipline with its own budget: the frontend, the design system, the store listing, the onboarding flow, the mobile build.
An MCP-only product does none of it. You expose capability and someone else's client renders the interaction.
MCP doesn't remove the interface. It relocates it: the agent host becomes the browser, and your delivery problem becomes someone else's rendering problem.
For a small team that's the difference between shipping one thing and shipping one thing plus the apparatus for showing it to people. Mailoo has no frontend because it needs none.
What replaces the bundle is better in a specific way. Instead of one application per purpose, there's a single capable client sitting in front of many outcome-based services, and you can reach across several of them in one request. Mailoo knows nothing about your CRM. That doesn't stop you asking for a campaign built from a segment that lives in one, because the client is the integration.
Composition is the prize. Conversation is only the interface. Every service you connect multiplies against every other one, and none of them had to agree on anything in advance.
MotherDuck took this to its conclusion in August 2026 with a signup endpoint that requires no authentication and no email — you POST to it and get a working token, which a human can claim later if they want the account. When your user isn't a person, the signup form isn't friction. It's a wall.
Without buttons, discovering what's possible becomes the agent's problem — and yours
A graphical interface advertises what it can do. A conversational one waits to be asked.
Buttons are self-documenting. You learn what software does by looking at it, and a menu is a map. Even a bad interface tells you the shape of the thing. Remove it and capability goes dark: the client looks identical whether it's connected to fourteen services or none, and you have to ask for everything, which means knowing what's askable.
This is the same problem we solved one level down. describe_step_types, the errors formatStepConfigError rewrote to carry an expected shape, the prompt templates encoding what a sensible welcome series looks like — all of it answers one question for the agent: what can I ask for here? The person asking the agent has exactly the same question and nothing to answer it with.
The agent's half isn't widely solved either. An audit of 856 tools across 103 MCP servers, published February 2026, found 97.1% carrying at least one defect in the tool description and 56% that never said plainly what the tool was for. Legibility is unclaimed ground.
Our expectation is that legibility becomes what these products compete on. With no screen to differentiate on, the service that best explains itself — to the model, and through the model to the person — wins by default. That's an unusual thing to design for, and we had no practice at it either.
The case against MCP-only: visual interfaces are coming back, and there's no monetization story yet
The protocol's own direction argues with us, and it's a fair argument.
MCP Apps arrived in January 2026 as the first official MCP extension, and what it does is render HTML inside the agent conversation. It exists specifically to put visual interface back where we just removed it, and it's the only extension with adoption across hosts. The official client matrix — community-maintained, and contradicted in places by the extensions' own announcement posts — lists eleven clients for MCP Apps and none for OAuth Client Credentials, the thing an agent-native product most obviously needs. Shopify built on it because, in their words, "For commerce, visual context isn't just helpful—it's essential."
They're right about commerce. Anything needing visual comparison, spatial layout or a person signing off on something irreversible should keep its screen.
The commercial case is unproven too. We went looking for a company whose only product surface is an MCP server and didn't find one. The protocol has no way to charge for anything, and monetization isn't on its 2026 roadmap. Distribution runs through host directories where ranking is usage-based, which trades a marketing problem for a gatekeeper problem rather than removing one.
Then there's oversight. Most agent damage has no attacker in it. An agent does exactly what it was asked, at a scale nobody pictured — a coding agent that deleted a production database along with its backups in seconds, an agent that rebuilt production infrastructure while troubleshooting. Which makes the cheapest safety feature available to you the annotation marking a tool destructive, and the second cheapest a design where irreversible operations are separate tools that always prompt.
Those are categories, not a verdict. Commerce needs pixels. Campaign orchestration doesn't. Nobody has ever enjoyed operating a drip campaign builder, and software nobody enjoys operating is software that doesn't need to be operated.
What would prove us wrong: adoption, not the protocol itself
Adoption, not the protocol.
The argument rests on people accepting a way of working where capability is invisible until named. If knowing-what-to-ask turns out to be harder than clicking-what-you-see, the flexibility isn't worth the burden, people go back to bundles with buttons, and MCP-only stays a developer curiosity rather than a delivery channel.
That resolves in usage data.
Asked what we'd do differently, the answer is nothing. The approach held. The friction was in the install and in one schema decision, both fixed inside the same few days. Push back on that if your own experiment went worse — we'd want to know where.
Four predictions: legibility as the product, back-office first, specification as the job, money arriving late
Legibility becomes the product surface. With no screen, what you compete on is how well your service explains itself. We expect the introspection tooling we bolted on after Mailoo broke to become a first-class design concern rather than a repair.
The first categories to go interface-free are the ones nobody enjoyed operating. Back-office, orchestration, reconciliation, campaign management, internal tooling. Where the interface was a chore rather than a pleasure, removing it is a gain.
Specification becomes the job. If eight hours of thinking and two to three days of pipeline produce a working system, the bottleneck moves permanently to knowing what to build and being able to say it precisely. We'd expect the roles that survive this shift to be the ones that were always about deciding.
Money and identity arrive late and awkwardly. There's no monetization primitive in the protocol and none planned, and an agent has no email address to put on an invoice. Whoever fills that gap will shape agent-native commerce more than the protocol does.
We'd take the other side of one common prediction: that this collapses back into apps with chat bolted on. The delivery saving is too large, and small teams feel it first.
The rule: removing the interface doesn't remove the job it was doing
Whatever your interface was doing, it was doing a job — mostly teaching people what your software can do. Delete it and the job doesn't disappear. It moves to whoever's left.
Build for that, and this is an unusually good deal. Ignore it and you'll ship something excellent that nobody knows how to ask for.
The seven-item checklist we run against every tool before it ships
Applied to every tool before it ships.
- Does the input schema expose every constraint the domain will enforce? No untyped bags standing in for typed data.
- Does the description answer all five: what it does, when to use it over another tool, what must be true first, what to call next, what will make it fail?
- Do errors include what was expected, not only what was wrong?
- If there's polymorphic input, is there an introspection tool that describes every variant?
- Does the validation tool reject everything the creation tool will reject?
- Are tools cross-referenced ("call X before this", "call Y to find IDs")?
- Did you read it as the agent, with only the name, description and schema in front of you? Could you succeed?

