Brahim Bousnguar

Notes · Article

Every screen in this SaaS app is an MCP App

By · · 10 min read

Mathew Goldsborough put a task app on the screen: a to-do list, a sidebar, a chat panel wedged in on the right. It looked like any SaaS dashboard. Then he told the room that everything on it was an MCP server or an MCP App, composed at runtime. His company, NimbleBrain, builds the open-source host that holds those pieces together, and his talk was about that host: how an app finds its place on the screen, shows a count without a model call, learns it was installed, and ends up looking like one product.

It was the last talk of day one at AGNTCon + MCPCon North America in San Jose, on October 22. Watch it on the livestream from 7:12:34. Goldsborough is NimbleBrain's founder and CEO, and an AAIF ambassador, the only one based in Hawaii, so he gets to open with aloha. My note on Liad Yosef's talk covered MCP Apps from the client side: a seat map coming back inside your assistant. This is the other half, and I read it against NimbleBrain's code and docs, which are public.

Workspace · Ops

  • Workspace
  • Conversations
  • Files
  • Tasks
  • Apps
  • To-dos
  • People
  • Memory

Host views, then app placements

To-dos

  • Review outbound drafts
  • Renew the sending domain
  • Sync the CRM

+ Add task

ui://todos in an iframe. Buttons are tool calls

Chat

What's next on my list?

Review outbound drafts.

The agent loop, skills over MCP

The reveal, redrawn. The layout follows his demo; the to-do items are mine.

MCP is a package format now

His framing: every platform got a packaging format, and a composition layer around it. Executables on the desktop. WAR files for Java web apps, "if you were fortunate or unfortunate enough". Debian packages for Linux. Now MCP, which he says only recently matured enough to carry everything a real product needs.

He counts three pieces in the package, and calls them the triforce, for the Zelda fans. Tools, the capabilities a server hands the model. MCP Apps, rich interactive interfaces delivered over MCP. And skills over MCP, the know-how that loads with the tools; NimbleBrain rolled its own crude version before the extension existed. Add a server.json and a couple of extensions, and you have something you can ship and compose.

The quick demo shows why that matters. Ask a to-do server "what's next on my list?" and an HTML widget renders in an iframe, not a paragraph of text. Skills come back with it. Every button you click is a tool call.

  1. .exeThe desktop
  2. .warJava web apps
  3. .debLinux
  4. MCPAgents, and now whole apps
  1. 1Tools

    What the server lets the model do

  2. 2Apps

    An interface in an iframe; every button is a tool call

  3. 3Skills

    The know-how, loaded over MCP

The packageA server.json and a couple of extensions hold all three.

The history slide, then his triforce: one triangle per piece.

Chat is someone else's sandbox

The catch is that all of it lives in a chat. The package is yours and the server is yours, but you don't own the runtime. His picture: you walk into a sandbox with your toys, and someone tells you how to play, where to play, how big the walls are and how much sand you get.

Building your own host fixes that, and NimbleBrain has done it. But then you write the same plumbing every time. And the spec itself is built for one host talking to one server. There's little in it for one MCP server talking to another.

There's an audience problem too. Everybody's vibe coding in Claude Code and building their own harness, he said, but business users still want a dashboard, a button, a report, a graph. So NimbleBrain built the other half.

In a chat host

Their sandbox

Your package, your server

Their walls: where it renders, how big, what it reaches

One host to one server, no server to server

or

In your own host

Same plumbing, again

You own the runtime

Identity, files, conversations, routing: rebuilt per product

Business users still get a chat box

NimbleBrainBuild the host once, as open source, and let every server use it.

His two problems side by side. The sandbox is his picture; the plumbing stacks are mine.

The other half is a host

NimbleBrain boots into an empty shell with extension points: places in the web UI where an MCP App can render. Built-in apps come first (conversations, files, scheduled tasks), and installed apps show up next to them. One app or two hundred. Click one and its MCP App renders in the main area. You're not in chat anymore, it's a full SaaS screen, and the buttons aren't REST calls. They're MCP tool calls. His thesis, said with a grin: REST is dead, you can do everything over MCP.

Slide the chat out and the same app runs with an agent. You see the tool calls and the skills loading, and the agent can act. Or skip the web UI and connect your own MCP client, because the runtime is an MCP server too.

Under the shell sit what he calls host primitives, the things every server needs and shouldn't build itself: files, conversations, tool routing, identity. Everything splits into workspaces. Finance gets Sage and Stripe, engineering gets GitHub and Linear, and you keep a private one. Parts swap out. A customer with its own memory in a vector store plugged that in, people on his team who like Obsidian plugged Obsidian in, and Salesforce or HubSpot is a connector choice.

It runs anywhere: on-prem, a private cloud, their managed service. That puts it next to the internal APIs and the Postgres nobody has touched in twenty years. They put a small MCP broker in front of that, and one runtime reaches both the public cloud and the legacy system.

The repo backs this up. It's Apache 2.0, runs on Bun, ships as two container images, and docker compose up gives you the web UI on port 27246. The agent loop runs until the model stops calling tools, capped at 10 iterations by default and 25 at most.

Front doors

The web shellYour MCP clientCoding agents, harnesses

Host primitives

FilesConversationsTool routingIdentityWorkspaces

The MCP spec

ToolsResourcesAppsSkills

Servers

Theirs: search, memory, CRM, outboundYoursAny public oneA broker to a 20-year-old Postgres

  • FinanceSage · Stripe
  • EngineeringGitHub · Linear
  • MineGitHub

RunsOn-prem, a private cloud or their managed service, with any OpenAI-compatible model.

His architecture slide as layers, with the workspace example under it.

Placements: the app picks its slot

So how does an app end up in the sidebar? It asks. NimbleBrain defined a host extension under its reverse domain, ai.nimblebrain/host, and an app's manifest lists placements: a slot, a label, a route, and the ui:// resource to render there.

"ai.nimblebrain/host": {
  "placements": [
    { "slot": "sidebar.apps", "resourceUri": "ui://dashboard",
      "label": "Tasks", "route": "tasks" },
    { "slot": "settings", "resourceUri": "ui://settings" }
  ]
}

The host reads that at startup. It knows nothing about the server, only the interface. When you click Tasks, it loads that resource into an iframe in the main area, beside the docked chat, at a URL like /w/ops/app/tasks. The docs list the slots: an entry under Apps in the sidebar, a full page with no nav entry, a section on the connector's settings page, and a pinned item at the foot of the mobile drawer. The sidebar shows the first four apps, then a View all link. If two connectors claim the same route, the first one keeps it, and the host logs who holds it.

A business that wants a different layout can swap the template around the slots and keep the same apps.

"placements": [ { "slot": "sidebar.apps", "resourceUri": "ui://dashboard", "label": "Tasks", "route": "tasks" }, { "slot": "settings", "resourceUri": "ui://settings" } ]

/w/ops/app/tasks

  • Conversations
  • Files
  • Apps
  • Tasks
ui://dashboardIn an iframe, beside the docked chat
Settings › Connectors › Tasksui://settings
The manifest drives the shell. The slot names and the route shape are from NimbleBrain's docs.

Facets: a count without asking the model

This is the part I'd steal first. A dashboard needs numbers: tasks blocked, drafts waiting for review. You could ask the model. "Every time you ask the LLM, you spend more and more tokens," he said, so facets skip it.

A facet is an ordinary MCP resource with a marker in its _meta. The host lists the server's resources, spots the marker, reads that resource, and gets back one number:

{ "uri": "acme://facets/drafts-awaiting-review",
  "title": "Drafts awaiting review",
  "_meta": { "ai.nimblebrain/facets": { "level": "warning" } } }

→ resources/read →  { "count": 3 }

The host shows it as one row, "3 Drafts awaiting review", in a Needs attention panel on the workspace overview, and a click opens the app. The docs pin down the rest. The level (critical, warning or info) sets the colour and the order. A zero shows nothing. A read slower than five seconds shows the row as unavailable without breaking the others. Counts are cached for 60 seconds. And the title and count are treated as untrusted data that never reach a model.

  1. Host → serverresources/list
  2. A resource carries"ai.nimblebrain/facets": { "level": "warning" }
  3. Host → serverresources/read → { "count": 3 }
  4. Overview3 Drafts awaiting review

The modelNot on this path. No tokens spent.

  • 2Tasks blockedcritical
  • 3Drafts awaiting reviewwarning
  • 1Reply receivedinfo
The wire shapes and the three levels are from the facets docs; the counts are examples.

Lifecycle: installing it buys a domain

Some servers have to do something elsewhere when they're installed. NimbleBrain's go-to-market app buys domains and provisions email inboxes the moment you add it to a workspace. That's two or three layers away from the vendor, and no MCP message says "you were just installed".

The lifecycle extension doesn't do the buying. It's the hook. A server marks one tool for each event, ready and removing, and the host calls it with an ordinary tool call. Install it, and the server hears "workspace ready", then goes and buys. Uninstall it, and removing arrives while the server is still reachable, so it can release what it bought.

The docs are where this gets good, because they're honest about delivery:

  • ready arrives at least once, and twice on a fresh install, in a racy order: once with the reason install, once with resume as the connection comes up. A host restart is also resume, so a restart never looks like a new customer.
  • So the handler has to be idempotent: "make this true", an upsert, never an insert.
  • removing is best effort. If the host was down at uninstall time, it never comes, so a server that leaks a domain without it is relying on a call nothing guarantees. Pair it with a sweep of your own.
  • The handler should only record intent and return. A separate process holding the vendor credential does the actual buying, so a bug or a prompt-injected tool call can't reach that credential.
  • The host hides the marked tools from everyone else, the model included. Another host might not, so the docs say to write each handler as if any caller could reach it.
  1. Installready · reason: "install"Record "wants a domain and inboxes". A separate process buys them.
  2. Restartready · reason: "resume"The same upsert. Nothing new to buy.
  3. UninstallremovingRecord the release. Best effort, so a sweep catches the rest.

Host onlyThe model never sees these tools. The host withholds them from every other caller.

His domain-buying app, run through the delivery rules in NimbleBrain's lifecycle docs.

Ten apps should look like one

He showed two apps by two developers. One picked purple, the other green. Side by side, they looked like a hodgepodge.

The MCP Apps spec already lets a host send style variables to the iframe. NimbleBrain sends those, adds the tokens the spec has no key for, like an accent colour, and its MIT-licensed app SDK, Synapse, applies them. Flip the shell to dark mode and every app goes dark. "How many times has somebody tried to render an iframe inside of a dark mode thing and got the light mode?" The same trick gives white-label shells: a customer's colours and fonts are a style kit, not an app reload.

Toasts got the same treatment. The spec's notification message mostly ends up as a log line, so the bridge has a notify method with a level and a title, and the shell renders it like its own toasts. In the docs, a success stays five seconds, info six, and warnings and errors until dismissed.

Basic, he admitted. But it's what makes ten or twelve servers feel like one app instead of something stitched together.

To-dos

Review outbound drafts

Renew the domain

Add task

Outbound

3 drafts to review

Inbox warming

Send batch

successReport exportedSaved to Files.

Schematic apps. The two-colour problem and the dark-mode flip are from the talk, the toast from the bridge docs.

What it means for my fifty apps

I run about fifty apps on a Mac mini, each behind its own Tailscale HTTPS port, each with its own front end and its own theme. Ten MCP servers live in their own repos next to them, most of them plain HTTP clients of an app's API, so my agents can use the apps too. This site has one as well: my portfolio behind an MCP server.

So every app my agents touch has two front doors, built twice: a UI for me and an MCP server for them. Goldsborough's package is one door. Tools, UI and skills ship together, the shell docks a chat next to every screen, and the buttons and the agent call the same tools.

Yesterday I built a page that lists every app by port and checks which ones answer. That's his catalog without the composition. Facets would slot right in: a count of what's waiting in each app, on one page, with no model in the loop.

Two honest caveats. I haven't run NimbleBrain yet; everything above comes from the talk, the repo and the docs. And the extensions are NimbleBrain's, under its own prefix. Another host ignores them, which the docs make safe by design, but then you lose the feature. Goldsborough said they want to give back where the spec work already overlaps.

One answer from the Q&A is worth keeping. Asked why MCP Apps and not A2UI, he said they adopted MCP Apps when it launched and haven't seen the value of A2UI yet. And this isn't generative UI. You still design and build the app. It feels like a CRUD app, with an agent layer you get out of the box.

My setup today

Two doors per app

A front end per app, its own theme and port

An MCP server in another repo, same API

About fifty apps, ten MCP servers

vs

One package per app

One door

Tools, UI and skills ship together

The buttons and the agent call the same tools

One shell, one chat, one look

Not yet triedI haven't run NimbleBrain. This is the talk, the repo and the docs.

My counts against his package. The right card is what I'd get, not something I've built.

Try it

If you've shipped an MCP App outside a chat window, I want to see the host. And if you try facets before I do, tell me what you counted.

Plain text: every-screen-is-an-mcp-app.md · Atom feed · All notes

Contact

Say hello.

b.bousnguar@gmail.com

SAP Commerce Cloud, MuleSoft and Salesforce integration · Nantes · EN / FR