We flagged the MCP Apps SDK as an MCP App
MCP Apps is the first official extension to the Model Context Protocol, published on 26 January 2026: a tool can point at an HTML view, and the host renders it in a sandboxed iframe inside the conversation. Whether a server does this is not a matter of opinion. It is written in the server's own tool list.
Until 25 September, mcprating.io showed a "UI-capable" badge on 114 of the 65,964
servers we indexed at the time. The most-downloaded of them was @modelcontextprotocol/ext-apps
— the MCP Apps SDK itself, 4.7 million downloads in the week to 26 September. It is
a library. It serves no UI, because it is not a server. Third on the list was
@mcp-ui/server, the SDK for the older MCP-UI format: also a library.
Meanwhile merill-lokka, a Microsoft Graph server that serves six MCP Apps views,
had no badge at all. We had connected to it on 28 August and stored all six views.
They had been sitting in our database for four weeks.
How you tell, according to the spec
The MCP Apps specification makes this checkable:
- A tool that has a view carries
_meta.ui.resourceUri, pointing at aui://resource. (An older flat key,_meta["ui/resourceUri"], is deprecated but still accepted by hosts.) - The view is served as
text/html;profile=mcp-app. - A client announces support by listing
io.modelcontextprotocol/uiunderextensionsin itsinitializecapabilities — and servers should hide UI tools from clients that don't.
Connect, list the tools and resources, look. That is the whole test.
What we were doing instead
The badge was set by crawlers, from package metadata: a dependency on the MCP Apps
or MCP-UI SDK, a keyword like mcp-apps, or a phrase like "interactive ui" in a
package's name or description. It went wrong three ways.
It matched libraries. The MCP Apps SDK lists mcp-apps among its own npm
keywords, so it was flagged — as were the MCP-UI SDK and middleware that mentions
either.
It matched names. Four packages published under an npm scope called
@mcp-apps were flagged because their names contain the keyword. The three we
could start serve no UI.
It never read our own measurements. Our verification harness connects to
servers and records their tools, but it stored names and schemas and dropped
_meta — the one field where the evidence lives. The ui:// views it did store
for lokka were never consulted by the badge.
Two kinds of evidence, labelled
We now accept exactly two, and the site says which one it has:
- Measured — we connected and saw a tool declaring a view, or a
ui://resource. The badge reads verified. - Package — a runtime dependency on a UI server SDK, excluding the SDKs themselves and MCP-UI's client-side packages, which are for hosts. The badge reads unconfirmed.
Keywords and descriptions are no longer evidence. A re-judging pass over the 114
existing badges promoted 5 servers whose ui:// views we had already stored, kept
25 with a real SDK dependency, removed 88 that rested on keywords or descriptions, and left
1 alone because its dependencies could not be fetched.
Did the handshake matter? Not for the servers we could compare
The spec's should is the part we expected to bite. Our harness announced no capabilities at all, so a server that follows the spec would have shown us its plain tools and hidden its UI.
The mechanism is real. A test server that follows the spec shows one tool to a client that announces nothing, and two tools plus a view to one that announces MCP Apps. The new harness (1.1.0) announces the extension, for that reason.
But five servers had runs from both harness versions, and they did not hide anything:
| server | tools, old harness | tools, 1.1.0 |
|---|---|---|
| merill-lokka | 31 | 31 |
| orchyn-mcp | 28 | 28 |
| get-technology-inc-jamf-docs-mcp-server | 6 | 6 |
| svgrid-mcp | 5 | 5 |
| metmuseum-mcp | 4 | 4 |
So our blindness was not the handshake. It was what we recorded, and what we chose to believe over our own data. We kept the announcement anyway: an instrument should declare what real hosts declare, or it measures a different server from the one users get.
We measured 79 servers
Every server whose UI we had guessed, by keyword or dependency, or inferred from stored views — minus the SDK packages themselves, and minus the ones with nothing to run. Same conditions as always: a disposable container, no credentials, 45 seconds.
| what the old badge rested on | servers | started | served UI | no UI seen |
|---|---|---|---|---|
| keywords or description text | 49 | 13 | 6 | 7 |
| a UI-SDK dependency | 25 | 11 | 9 | 2 |
ui:// views we had stored |
5 | 5 | 5 | 0 |
| total | 79 | 29 | 20 | 9 |
Of the 29 that started, 20 serve UI: 19 MCP Apps and one in MCP-UI's earlier format. The other 50 did not get as far as a tool list: 38 failed to start, 7 wanted credentials we withhold on purpose, and 5 cannot run in our container at all — another OS, or a runtime we do not install.
Three things in that table surprised us.
Keywords were sometimes right. Six servers had nothing but keywords behind
their badge and turned out to serve UI — five as MCP Apps, one in MCP-UI's format.
None of them declares a UI SDK as a runtime dependency, so the dependency rule
alone would have missed them too. Two ERP servers, casys-mcp-erpnext and
hvgllc-hvgerp-mcp, each have at least 50 tools with a view — our counter stops at
50. Our own cleanup had removed all six badges less than an hour earlier;
measuring put them back, this time with evidence.
A dependency is a declaration, and it mostly held. Nine of the 11 SDK-dependent servers that started serve UI. That is the thesis of Declared is not implemented, pointed at servers: a declaration tells you where to look, not what you will find.
Most flagged packages never behaved as servers at all. 36 of the 49 keyword-flagged packages did not produce a tool list. Some are libraries — SDKs, middleware, frameworks — and some are servers that need configuration we do not supply. From outside, those look the same.
What the site shows now
- 35 servers carry a UI badge: 20 verified, 15 unconfirmed. The 9 we measured with no UI carry none.
- The browse page has a Has UI filter.
- The weekly verification job runs the new harness, so measurements accumulate as it works through the index.
The official client list for rendering these views is Claude, Claude Desktop, VS Code (GitHub Copilot), Microsoft 365 Copilot, Goose, Postman, MCPJam and Archestra.AI.
One more thing we found
While adding that filter we noticed the browse page reflected its search and
sort query parameters into the HTML unescaped — a working reflected XSS,
confirmed on production with a harmless tag. It is fixed, and the page's tests now
render it with hostile input. We publish a scanner for poisoned tool descriptions;
this is the second time this month our own templates were the problem.
What we don't claim
- Not a census of MCP Apps. We measured 79 servers that had some prior signal. Servers with no signal were not tested, and there are certainly MCP Apps servers we do not know about.
- "No UI seen" is not "no UI". We list tools and resources without credentials. A server can add UI tools after configuration or login, or return UI inline in tool results, which listing cannot see.
- We do not render anything. We check declarations, not whether a view works in any host.
- Not that servers ignore the spec. Five servers is not a sample, and a server that keeps its UI tools visible and falls back to text is compliant.
- "Failed to start" is not "broken". Same vocabulary as post 2: it is a floor under deliberately hostile conditions, not a verdict.
- The client list is the official one, not our measurement. Cursor announced a UI extension in our handshake probe; announcing is not rendering.
- We are not neutral. We run the registry, and the wrong badges were ours.
Reproduce it
Announce the extension in initialize, so servers that follow the spec show you
their UI tools:
{ "capabilities": { "extensions": {
"io.modelcontextprotocol/ui": { "mimeTypes": ["text/html;profile=mcp-app"] } } } }
Then look:
const view = (tool) => tool._meta?.ui?.resourceUri ?? tool._meta?.["ui/resourceUri"];
const uiTools = tools.filter((t) => typeof view(t) === "string" && view(t).startsWith("ui://"));
const uiViews = resources.filter((r) => r.uri.startsWith("ui://"));
Our results are in the API: https://mcprating.io/api/v1/servers?hasUi=true
lists the badged servers, and each server's metadata.uiMeasured records the
tools and views we saw.
Sources and confidence
| claim | source | confidence |
|---|---|---|
| Spec markers and the should | ext-apps specification, 2026-01-26 | high |
| First official extension, 26 January 2026 | MCP blog | high |
| Clients that render views | official overview, as of 25 September | high, for that date |
| 4.7M SDK downloads | npm downloads API, 20–26 September (4,682,229) | high |
| Every other number | our harness and registry, 25 September | high for what it measures; scope as above |
A badge is a claim about someone else's software. Ours now says either "we saw it" or "we haven't yet" — and which.