UI Resources
NimbleBrain apps serve UI as HTML pages through the ui:// protocol. The platform proxies these resources from your MCP server and renders them in sandboxed iframes inside the web client.
How ui:// works
Section titled “How ui:// works”When your manifest declares a resource URI like ui://dashboard, NimbleBrain resolves it through the MCP resource protocol. Your MCP server registers a resource at ui://dashboard that returns HTML content. The web client fetches this HTML through the platform’s resource proxy and renders it in an iframe.
The flow:
- User navigates to your app (e.g.,
/app/@myorg/my-app). - The web client requests
GET /v1/apps/my-app/resources/primary. - The platform resolves
"primary"to yourprimaryView.resourceUri. - The platform calls
resources/readon your MCP server for that URI. - The HTML response is returned to the web client and loaded into an iframe.
- The MCP App Bridge initializes via
postMessage.
The primaryView
Section titled “The primaryView”The primaryView is the main HTML page for your app. Declare it in your manifest:
{ "_meta": { "ai.nimblebrain/host": { "host_version": "1.0", "name": "My App", "icon": "database", "primaryView": { "resourceUri": "ui://dashboard" } } }}Your MCP server must register a resource handler for ui://dashboard that returns an HTML string.
Python (FastMCP)
Section titled “Python (FastMCP)”from mcp.server.fastmcp import FastMCP
mcp = FastMCP("my-app")
@mcp.resource("ui://dashboard")def dashboard() -> str: return """ <!DOCTYPE html> <html> <head><meta charset="utf-8"><title>Dashboard</title></head> <body> <h1>My App Dashboard</h1> <div id="app"></div> <script> window.addEventListener('message', (event) => { const msg = event.data; if (msg?.method === 'ui/initialize') { document.getElementById('app').textContent = 'Connected!'; } }); </script> </body> </html> """TypeScript (MCP SDK)
Section titled “TypeScript (MCP SDK)”import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({ name: "my-app", version: "1.0.0" });
server.resource("dashboard", "ui://dashboard", async () => ({ contents: [{ uri: "ui://dashboard", mimeType: "text/html", text: ` <!DOCTYPE html> <html> <head><meta charset="utf-8"><title>Dashboard</title></head> <body> <h1>My App Dashboard</h1> </body> </html> ` }]}));Resource proxy
Section titled “Resource proxy”The platform API exposes app resources at:
GET /v1/apps/:name/resources/:path| Parameter | Description |
|---|---|
:name | The server name (derived from the bundle name). |
:path | The full path after ui:// in your resource URI. |
Resource URIs are independent of your app’s package name. You can name them whatever makes sense for your domain:
# Package: @nimblebraininc/synapse-crm# Resource URI doesn't need to include "synapse-crm"@mcp.resource("ui://crm/main")def crm_ui() -> str: return "<html>...</html>"GET /v1/apps/synapse-crm/resources/crm/mainReturns the HTML with Content-Type: text/html.
The virtual primary path
Section titled “The virtual primary path”The path primary is special. It resolves to the resourceUri from the first placement in your manifest:
GET /v1/apps/synapse-crm/resources/primary→ reads ui://crm/main (from placement resourceUri)This means apps don’t need to know their own resource URI at runtime. The web client uses primary when loading the main view from a placement.
Serving multiple resources
Section titled “Serving multiple resources”Your app can serve multiple HTML pages. Register each as a separate ui:// resource:
@mcp.resource("ui://crm/main")def main_view() -> str: return "<html>...</html>"
@mcp.resource("ui://crm/settings")def settings() -> str: return "<html>...</html>"
@mcp.resource("ui://crm/contact-detail")def contact_detail() -> str: return "<html>...</html>"Access them through the proxy:
GET /v1/apps/synapse-crm/resources/crm/mainGET /v1/apps/synapse-crm/resources/crm/settingsGET /v1/apps/synapse-crm/resources/crm/contact-detailUse multiple resources when you need separate views for different placements (e.g., a sidebar widget and a main view).
HTML resource guidelines
Section titled “HTML resource guidelines”Your HTML resources are rendered inside iframes. Keep these points in mind:
- Self-contained — Each resource should be a complete HTML document with its own styles and scripts. There is no shared CSS or JS between resources.
- No external dependencies required — You can include external scripts and stylesheets, but the resource must work within an iframe sandbox.
- Theme tokens — Listen for
ui/initializeandui/notifications/host-context-changedto apply the host’s CSS custom properties. See MCP App Bridge for the protocol. - Responsive — The iframe fills the available space. Design your UI to be fluid.
Core resources
Section titled “Core resources”NimbleBrain itself uses the same ui:// pattern for built-in views like the conversation list, settings, and home dashboard. These are served from in-memory templates by the core and home servers rather than from MCP subprocess resources.
When the resource proxy receives a request for core or home, it serves from the internal getCoreResource() registry instead of calling an MCP server:
GET /v1/apps/core/resources/conversations → built-in conversations panelGET /v1/apps/home/resources/primary → built-in home dashboardThird-party apps always go through the MCP resource read path.