Skip to main content

Publishing an MCP App

Most MCP servers return text or JSON, and the AI assistant reads it back to the user. An MCP App goes further: your server ships an HTML surface that the host renders in an iframe next to the conversation — a dashboard, a map, a chart, a form.

Servers that ship one get a Web UI badge on their directory listing and appear on the MCP Apps page.

This guide covers what MCPBundles looks for and how to make sure it finds it. For the listing itself, start with Publishing & Claiming MCP Servers.

What MCPBundles looks for

Your server advertises an app through the standard MCP Apps metadata (SEP-1865). MCPBundles treats either of these as an app:

SignalWhere it lives
A resource whose mimeType is text/html;profile=mcp-appYour resources/list response
A tool carrying _meta.ui.resourceUri pointing at a ui:// resourceYour tools/list response

Most servers ship both, and that is the combination we recommend: the resource is the app, and the tools tell the host when to open it.

Only these two structural signals count. Naming a tool parameter resourceUri does not make your server an app, and will not earn the badge.

Ship the UI resource

Expose the app as a resource with a ui:// URI and the MCP App profile MIME type:

{
"uri": "ui://acme/dashboard.html",
"name": "Acme Dashboard",
"mimeType": "text/html;profile=mcp-app",
"_meta": {
"ui": {
"csp": {
"connectDomains": ["https://api.acme.com"],
"resourceDomains": ["https://cdn.acme.com"]
},
"prefersBorder": true
}
}
}

ui.csp.connectDomains lists the origins your iframe may call, and ui.csp.resourceDomains the origins it may load fonts, images and scripts from. Hosts enforce these, so a missing entry shows up as a blank or half-rendered panel rather than an error.

Reading the resource must return the HTML document itself.

Point your tools at it

Every tool whose result should render in the app declares the same ui:// URI:

{
"name": "get_sales_summary",
"description": "Sales totals for a period",
"_meta": {
"ui": {
"resourceUri": "ui://acme/dashboard.html"
}
}
}

Note the leading underscore on _meta — that is the MCP wire format, and it is the single most common thing to get wrong.

You do not need a separate "open the dashboard" tool. The host opens the iframe on the first tool call that declares the URI, then forwards later results into the already-open app.

Make the app visible on your listing

MCPBundles reads your server's advertised tools and resources from a live connection, not from anything you type into the listing form. A published listing therefore shows the badge once we have actually seen your server describe the app.

  1. Publish the listing, then claim it to prove you control the server. See Publishing & Claiming MCP Servers.
  2. Connect the server from your workspace so MCPBundles can complete discovery. For servers that need OAuth or a key, this is the step where you sign in.
  3. Check the listing. The Web UI badge appears on your /skills/<slug> page, and the server joins /mcp-apps.

If you change the app later — a new resource URI, or app metadata on different tools — re-run discovery from the maintainer panel so the listing picks it up.

Tool counts on app listings

Remote MCP servers show Live tools rather than a number on their public listing. Tool lists are discovered per connection, and an OAuth server can legitimately expose a different set to each account, so a single public count would be wrong for somebody. The count appears once a visitor connects the server to their own workspace.

Troubleshooting

No Web UI badge. Check tools/list and resources/list directly against your server. Confirm the resource MIME type is exactly text/html;profile=mcp-app, and that tool metadata uses _meta with the underscore, nested as ui.resourceUri. If both look right, the listing may not have completed discovery yet — connect the server and re-check.

Badge shows, app does not render. This is usually content security policy. Add every origin the iframe contacts to ui.csp.connectDomains, and every origin it loads assets from to ui.csp.resourceDomains.

App renders but stays empty. The tools returning your data need the same ui.resourceUri as the resource. A tool without it runs normally but its result never reaches the open app.