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/synapse": { "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 resource path, matching the part after ui:// in your resource URI. |
For example, if your app’s server name is my-app and it serves ui://dashboard:
GET /v1/apps/my-app/resources/dashboardReturns 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 whatever URI is set in primaryView.resourceUri:
GET /v1/apps/my-app/resources/primary→ reads ui://dashboard (from primaryView.resourceUri)This means apps don’t need to know their own resource URI at runtime. The web client always requests primary when loading the main view.
The resolution logic strips the ui:// prefix from the primaryView.resourceUri and uses the remainder as the actual resource path:
// From handleResourceProxy in routes.tsif (resourcePath === "primary" && instance.ui?.primaryView?.resourceUri) { resolvedPath = instance.ui.primaryView.resourceUri.replace(/^ui:\/\//, "");}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://dashboard")def dashboard() -> str: return "<html>...</html>"
@mcp.resource("ui://settings")def settings() -> str: return "<html>...</html>"
@mcp.resource("ui://detail")def detail() -> str: return "<html>...</html>"Access them through the proxy:
GET /v1/apps/my-app/resources/dashboardGET /v1/apps/my-app/resources/settingsGET /v1/apps/my-app/resources/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.