Adding MCP to an existing app
Part 2 of 6 in Beyond the Demo.
A release is ready to publish. In the browser, the Publish button is disabled because nobody has approved the latest edit.
An assistant calls publish_release. The release goes live anyway.
Both interfaces are connected to the same database. Both are working as implemented. The application is still wrong.
Sharing data is not the same as sharing behavior.
When you add an AI-facing interface to software, the goal should not be to recreate the web interface as a collection of tool calls. It should be to expose the same application through a second interaction model, with the same decisions enforced underneath.
Put the application between the interfaces and the data
Consider a small, illustrative application called Release Desk. People use it to prepare, approve, and publish release notes. Assistants help gather changes and draft the text.
Its architecture can be deliberately ordinary:
Browser interface ── authenticated request ──┐
│
MCP tools ────────── authenticated request ──┼─> application operations
│ │
Scheduled jobs ───── delegated authority ────┘ ├─ policy checks
├─ version checks
├─ state changes
└─ durable receipts
The browser translates clicks and form fields. The MCP layer translates tool arguments and results. A scheduled job carries explicit delegated authority. None gets a private shortcut to publishing.
The operation that commits a release checks identity, project membership, publication rights, the approved content version, and the operation's current state. Its callers cannot switch off those checks by choosing a different interface.
This is an application design pattern, not a requirement that every interface run in the same process. A shared service can enforce the behavior. The important thing is having one authoritative implementation of the transition.
Model transitions, not screen gestures
A browser may walk a person through an editor, a preview, and a confirmation dialog. An assistant may prepare the same work in one conversational turn.
That difference is fine. What must remain consistent is the meaning of the operation.
For Release Desk, useful operations might be:
releases_search
releases_get
releases_prepare_publication
releases_commit_publication
releases_get_publication_status
These are example names, not built-in OpZero tools.
prepare_publication creates an immutable proposal tied to a particular draft version. It explains what would become public and where. It does not publish.
commit_publication accepts a proposal identifier and a retry key. It retrieves the proposal, verifies the required approval through trusted application state, and either commits the exact proposal or returns a clear reason it cannot.
The interface is free to be convenient. The operation is not free to be ambiguous.
Preserve the thing the human reviewed
Suppose the assistant prepares version 12 of a release. A person reads it and approves it. A teammate then edits the draft, producing version 13.
Should the earlier approval publish version 13?
For this application, the answer is no. Approval must refer to the specific content and destination the person reviewed. A mutable record identifier is insufficient.
One way to represent the proposal is:
{
"proposal_id": "proposal_82",
"release_id": "release_17",
"release_version": 12,
"destination_id": "public_changelog",
"state": "awaiting_approval"
}
The server stores the full proposed effect, including the content or its immutable reference. The object above is a minimal illustration of the returned handle.
If the underlying draft changes, the application can require a new proposal. Another design could deliberately allow publishing the older, explicitly approved snapshot. Either can be coherent. Silently substituting unreviewed content cannot.
Notice that this is not primarily an AI problem. Adding an assistant makes an existing ambiguity harder to ignore.
Return evidence the interfaces can share
A browser needs something it can render. An assistant needs something it can reason about. Both benefit from a stable result.
{
"publication_id": "publication_41",
"release_id": "release_17",
"published_version": 12,
"state": "published",
"destination_id": "public_changelog"
}
That is more useful than “Done!” because another request can refer to it. It also avoids forcing the browser to scrape a sentence or the assistant to infer which record changed.
The MCP tools specification supports structured results and optional output schemas. Use those facilities to make your application's results explicit. The schema can validate the shape; it cannot prove that a claimed publication actually happened. The operation must produce its receipt from committed state.
For queued external delivery, return a queued or pending state instead. A receipt for accepted work is not a receipt for completed delivery.
Use embedded UI where it genuinely helps
Some decisions are awkward to make through prose alone. Comparing two release drafts is a good example. A visual diff makes omissions and changes easier to inspect than a paragraph claiming to summarize them.
MCP Apps allow servers to return interactive interfaces inside supporting hosts. The extension uses a host-controlled sandbox and can keep interaction close to the conversation. That gives an application another presentation option, not a new source of authority.
A review widget should read the same proposal and call the same protected operations as the standalone application. It should not receive a long-lived secret, invent its own approval state, or assume that being displayed inside a trusted host makes every action trustworthy.
Keep a useful fallback for hosts that do not support the UI extension. The assistant should still be able to retrieve the proposal, explain its state, and direct the person to an authenticated review page. A fallback must not quietly lower the approval requirement.
Transport support, tool support, and embedded UI support are separate compatibility questions. Test the clients you intend to serve instead of inferring all three from a successful connection.
Keep the interfaces different where difference is useful
Sharing an operation does not mean making chat behave like a browser.
A human may want a searchable table of releases. An assistant may want the five relevant records with stable IDs and enough context to distinguish them. The same query service can provide both without returning the same presentation.
Likewise, browser authentication and MCP authorization may arrive through different mechanisms. Normalize them into a validated execution context, but do not equate a browser session, a model's claimed identity, and an access token. The application needs to know which principal is acting and which permissions actually apply.
The useful common denominator is the domain contract: ownership, allowed transitions, version semantics, and evidence. Keep those consistent. Let the interfaces specialize.
Test the boundary with one deliberate disagreement
You do not need an elaborate demonstration to find duplicated behavior.
Create a draft that the browser refuses to publish. Attempt the equivalent operation through MCP. Then reverse the exercise: publish an approved version through the browser and retrieve its receipt through the assistant.
Repeat with a different account and with a stale proposal. The answers should agree about what exists, what happened, and what the caller may do.
If they disagree, the bug is not that one interface needs a nicer message. The application has two competing definitions of the same operation.
A second entrance, not a second application
OpZero's public product direction describes software accessible through both a browser and an MCP endpoint. The architectural opportunity is larger than attaching a chatbot to a dashboard: the same capability can meet the user in the interface that fits the moment.
That only works when continuity lives beneath the interfaces.
Let people click. Let assistants call tools. Let scheduled work carry explicit authority. Make all three answer to the same application.
Design the shared operation first. Explore OpZero's deployment and hosted MCP tools, then write down one operation that both a person and an assistant should be able to complete—with the same result and the same limits.
Next in the series: Designing MCP tools around real workflows.