Building with AI10 min read

Don't Build the AI. Build What It's Allowed to Touch.

Most teams trying to bring an AI system to life start by building the AI. That is the expensive half, the fastest-moving half, and the half where you have no advantage.

Most teams trying to bring an AI system to life start by building the AI. That is the expensive half, the fastest-moving half, and the half where you have no advantage.

I rebuilt imran.mughal.com this month to test the opposite. No model, no chat interface, no agent loop. All of the engineering went into the server behind it: the typed tools an agent is allowed to call, and the approval gate between what it proposes and what the public sees.

Three layers, and only one of them is yours

The model, the client, and the server. The agent is not a fourth layer, it is what runs across them, deciding what to do and calling your tools to do it.

The model is a commodity you rent. It gets better every quarter without a line of code from you, and any effort you spend competing there is effort spent against companies whose entire business is that layer.

The client is solved, and it is interchangeable. I talk to my site through ChatGPT's voice mode when I feel like talking, and use Claude on the desktop when I'm at my machine. The same server serves both, and I did not build speech recognition, turn-taking, or a conversation interface for either one.

The server is the only layer nobody can build for you. Which operations are legal in your business, what a valid record looks like, who has to approve, what must never be touched. That is your domain knowledge expressed as software, and it is where the effort belongs.

A client I did not build, running a model I did not train, describing its own proposal and then stating the limit. Nothing is live until it is approved on the site.

A client I did not build, running a model I did not train, describing its own proposal and then stating the limit. Nothing is live until it is approved on the site.

The CMS was always the wrong abstraction

My first site build was in 1995, at watAGE, a University of Waterloo startup, putting large documentation systems into SGML while the web was still new. Paul Prescod sat beside me, and when I needed to know how something should be marked up he handed me a snippet of HTML to work from. He would go on to co-write the definitive guide to XML with Charles Goldfarb, the man who invented SGML and coined the term markup language.

After that came hand-written HTML, Dreamweaver, Joomla, Drupal, WordPress, and eventually a headless setup with a static generator. Each generation cut the effort. Not one of them changed the shape of the work.

The shape was always a human translating intent into mechanics. Which page, which plugin, which field, which theme setting, which deploy step. Maintaining my own site never required thought, it required muscle memory, and muscle memory is what decays between visits.

MCP is the first thing that changed the shape rather than the effort. I state the outcome and the agent does the translation. That only works if the mechanics have been defined as tools with contracts, which is the actual build.

The reference material moved too. What used to be a snippet of HTML passed across a desk is now a schema the agent asks for before it writes.

What a good server looks like

Named tools per business operation. Not a generic write endpoint with a prompt telling the model to behave. Reading content, proposing a revision, upserting an article, proposing a publish, listing pending changes, reporting status. Each one is a real operation with its own arguments.

Typed contracts the agent can read. Page copy lives as JSONB behind a schema per content key. A read returns the value together with its JSON schema, so the agent reads the contract before it writes. Required fields, no additional properties, length caps. A malformed write gets rejected instead of rendered.

A boundary it cannot cross. The unit of change is the block. An agent can propose replacing a block outright, rewriting the headline, changing a button, restating the whole thing. What it can never touch is how blocks compose: section order, navigation, URL paths and AI configuration are outside MCP entirely. It can change what the site says. It can never change what the site is. Ask it over voice what it is able to do and it reports the limit accurately, because it reads the boundary off the server instead of being told about it in a prompt.

A design contract in machine-readable form. Diagrams are authored with semantic node roles, actor, agent, tool, control, outcome, and colour and style directives are refused. The agent supplies what each box means. The site decides what it looks like. The diagram below was written that way, and it is the workflow this whole post describes.

Imran: Sets the goalImranSets the goalAgent: Drafts the blockAgentDrafts the blockSite MCP server: No publish toolSite MCP serverNo publish toolSchema validation: Blocks bad writesSchema validationBlocks bad writesProposal: Site unchangedProposalSite unchangedAdmin review: Rendered in placeAdmin reviewRendered in placeApproval: Browser onlyApprovalBrowser onlyRejected: No change appliedRejectedNo change appliedPublished block: Live on the sitePublished blockLive on the siteChange log: Decision recordedChange logDecision recordedImran: Sets the goalImranSets the goalAgent: Drafts the blockAgentDrafts the blockSite MCP server: No publish toolSite MCP serverNo publish toolSchema validation: Blocks bad writesSchema validationBlocks bad writesProposal: Site unchangedProposalSite unchangedAdmin review: Rendered in placeAdmin reviewRendered in placeApproval: Browser onlyApprovalBrowser onlyRejected: No change appliedRejectedNo change appliedPublished block: Live on the sitePublished blockLive on the siteChange log: Decision recordedChange logDecision recordedImran: Sets the goalImranSets the goalAgent: Drafts the blockAgentDrafts the blockSite MCP server: No publish toolSite MCP serverNo publish toolSchema validation: Blocks bad writesSchema validationBlocks bad writesProposal: Site unchangedProposalSite unchangedAdmin review: Rendered in placeAdmin reviewRendered in placeApproval: Browser onlyApprovalBrowser onlyRejected: No change appliedRejectedNo change appl…Published block: Live on the sitePublishedblockLive on the si…Change log: Decision recordedChange logDecision recorded
Step details
  • Imran — Sets the goal
  • Agent — Drafts the block
  • Site MCP server — No publish tool
  • Schema validation — Blocks bad writes
  • Proposal — Site unchanged
  • Admin review — Rendered in place
  • Approval — Browser only
  • Rejected — No change applied
  • Published block — Live on the site
  • Change log — Decision recorded

Authored by an agent under the site's own diagram contract. It chose the meaning of each box. It had no say in a single colour.

The approval gate is the product

An agent can prepare anything. Publishing requires me, signed into the site, in a browser, approving a specific revision.

Underneath, a content tool validates its input and calls a draft service, which writes to a proposal table inside a transaction rather than to live content. A Postgres advisory lock serializes edits to the same entity. The proposal holds the original live snapshot and the latest proposed one.

Review renders the pending revision inside the real site layout, with before-and-after comparison, no caching, no indexing, and framing restricted to the same origin. I see the page as visitors would see it, not a diff of JSON. Approval is per item: each proposal has to be selected before the approve button does anything.

The approval route is the interesting part. It builds authentication from cookies only, checks the request origin, and pins approval to an exact proposal and revision number. It applies only if live content still matches the baseline the proposal was built from, and a conflict forces a fresh review.

There is no approval tool in MCP, and that is enforced in code rather than in instructions. An end-to-end test creates an article over MCP, confirms the public URL returns 404, then fires an approval at the review route using the machine credential. It is refused. The article stays invisible until a browser session approves it.

The tool that writes cannot be the tool that publishes. If you take one sentence from this into your own architecture, take that one.

Granularity is the design decision underneath it. I approve blocks, not fields, which means an agent has full latitude inside the unit and none across it. That sounds permissive until you sit in review: a rewritten headline is exactly as visible as an added button, because the whole block is in front of me either way. Per-field locks would be worse. They invite an agent to work around a list I would have to keep updating, and they make a small edit look safer than it is.

The moment that matters. The change is prepared and rendered in the real layout, the published page behind it is unchanged, and approving means selecting that exact revision by hand.

The moment that matters. The change is prepared and rendered in the real layout, the published page behind it is unchanged, and approving means selecting that exact revision by hand.

What rollback actually means

Changes are recorded with before and after snapshots and the fields that changed. Each entry carries its source, MCP or the admin editor or a rollback, the person who decided, and the decision itself. Rejected proposals stay in the log beside approved ones, so the record shows what was refused and not only what shipped.

The change log supports rollback, and it restores an earlier snapshot only when current content still matches the recorded after-state. The rollback is itself logged as a change. Nothing gets silently overwritten by an undo.

My own log has an MCP edit to the services copy at 6:46 PM and a rollback of the same key at 6:47. An agent wrote something, I didn't like it, and the reversal is on the record next to the change it undid.

Two honest limits. A pending proposal keeps the latest draft and a revision counter, not an immutable copy of every intermediate edit. And this is application-level history with conflict checks, not a tamper-proof audit ledger. If your regulator needs the second thing, this design is a starting point and not an answer.

The record. Source, decision and area for every change, including the hero rewrites I turned down, with rollbacks logged as changes in their own right.

The record. Source, decision and area for every change, including the hero rewrites I turned down, with rollbacks logged as changes in their own right.

The website is not the point

A website is a convenient thing to demonstrate this on because it is small, public and reversible. The pattern is not about websites. Anything an agent should act on through your rules rather than around them takes the same shape: a CRM, a ticketing system, an internal knowledge base, a scheduling system.

What has to be true of the target system is more specific than having an API.

  • It can hold a pending state. If the system only accepts write-through updates, you build the staging layer yourself and inherit reconciliation against a system that moves underneath you.
  • The write is inert until approved. A page block does nothing until it renders. A CRM record fires workflows, sends mail, escalates cases. Rolling back a field does not unsend what the field caused.
  • Review can show consequence, not fields. The reason approving here works is that I see the change rendered as visitors would see it. A diff of field names is the version I said was worse, and for operational systems it is the version you get unless you build the alternative.
  • The volume fits a human. I approve a few changes a week. A support desk takes thousands of actions a day, and per-action approval does not survive that. It becomes risk-tiered: some classes apply on their own, some wait.

Where those hold, this design ports directly. Where they do not, the principle still holds and the work gets harder, which is the opposite of what a vendor demo will tell you.

Where a personal site ends and a regulated one begins

One thing to be precise about. The permission model uses a single broad scope rather than read and write scopes per tool, so an authenticated agent can reach private contact enquiries, and lead updates write directly instead of through the proposal workflow. Public content changes require my approval. Not every write does.

For this site that is the right size. One operator, one identity, one approver, and the person whose data it is happens to be the person reviewing the change. The control that matters is the one on publication, and it is there.

A pension plan or a bank is a different problem, and the delta is worth naming precisely, because it is what you would actually have to build.

  • Scopes per tool. Read separated from write, and sensitive record types behind their own consent rather than swept in with page copy.
  • Segregation of duties. The approver cannot be the requester. My design assumes they are the same person. Yours cannot.
  • A tamper-evident record. Application-level history with conflict checks is enough for me. Regulated retention means an audit trail the writing system cannot edit.
  • Access logging on personal data. An agent reading a client record is a disclosure event. It needs recording as one, whether or not anything changed.

The architecture does not change shape across that gap. What scales is the granularity of consent and the strength of the record. Worth remembering when a vendor demonstrates this pattern on a single-operator system and calls it enterprise-ready.

What this means if you run technology in a regulated shop

You do not need an AI platform. You need four things, and none of them are models.

Typed tools over your systems of record, so an agent acts through operations your business already recognises. A scope boundary that puts structure, configuration and anything irreversible out of reach. An approval path a machine credential cannot walk, enforced in code and proven by a test. A change history you can reverse, with the proposer and the approver both named.

Then pick a client you did not build, and let someone else fund its improvement.

You can go and look at the result. The site is imran.mughal.com , and this article went through the workflow it describes: drafted through the MCP server, held as a proposal, live only once I approved it.

I have written before about harness engineering, building the environment and feedback loops that let agents work reliably inside a codebase, and about the forward deployed engineer, the person who makes a model work inside an enterprise. This is the same admission at a third boundary. The model was never the hard part. What it is allowed to touch, and who has to say yes, always was.

Read and discuss on LinkedIn