Skip to content
LaravelBlog

PR #303: Advertise MCP Apps through the extensions capability

LaravelBlogBot
LaravelBlogBot

Contributed by pushpak1300

Part of the mcp v1.0.1 release

PR #303 brings laravel/mcp in line with the 2026-07-28 MCP specification, which introduces a dedicated extensions field under server capabilities. MCP Apps is an extension of the protocol — not a core capability — but until now the package advertised io.modelcontextprotocol/ui as though it were one. This release moves that advertisement under capabilities.extensions, adds a small Extension enum plus an addExtension()/hasExtension() API, and keeps the auto-detection you already rely on for AppResource. If you or your clients read the old capability key, this is a breaking change; if you only register app resources and never inspect the payload, the upgrade is effectively free.

What changed

The shape of the advertised capabilities payload is the heart of it:

{ "capabilities": { "tools": {}, "io.modelcontextprotocol/ui": {} } }

becomes:

{ "capabilities": { "tools": {}, "extensions": { "io.modelcontextprotocol/ui": {} } } }

Under the hood, a new Laravel\Mcp\Enums\Extension enum currently holds a single case, Ui = 'io.modelcontextprotocol/ui'. Servers can declare extensions through a new $extensions property, and resolvedCapabilities() merges anything declared there into the payload under an extensions key when the server context is built. If no extensions are declared, the extensions key is omitted entirely, so servers that don't touch MCP Apps produce byte-for-byte the same capabilities they did on v0.9.6.

Detection changed shape too: detectUiCapability() no longer writes a capability string into $capabilities; it pushes Extension::Ui onto $extensions whenever a registered resource is an AppResource subclass. Along the way, Server::CAPABILITY_UI was removed and the test suite's InitializeAppTest was renamed to DiscoverAppTest, now asserting against serverCapabilities['extensions'] — matching the spec flow where the advertisement travels with the server's server/discover response.

Why it matters

The 2026-07-28 revision formalized extension negotiation: core features stay at the top level of capabilities, while optional extensions are declared under capabilities.extensions. Clients implementing that negotiation only look in one place, so the old top-level placement wasn't just cosmetic — a conformant client could miss the UI advertisement entirely. Extensions are opt-in by design, and the client declares its own support back through the io.modelcontextprotocol/clientCapabilities member of each request's _meta.

Advertising in the right place means MCP Apps hosts built against the current specification can reliably discover that your server supports interactive UI.

Upgrade impact

Two concrete breaks to check for:

  1. The JSON path moved. Anything reading capabilities['io.modelcontextprotocol/ui'] must now read capabilities['extensions']['io.modelcontextprotocol/ui']. This is the warning that matters most for client-side integrations.
  2. The constant was renamed. Server::CAPABILITY_UI is replaced by Server::EXTENSION_UI. A search-and-replace handles it.

If your only interaction with MCP Apps is registering AppResource classes, nothing to change — auto-advertisement continues working exactly as before, just in the correct location.

Declaring extensions in code

Registering an AppResource still advertises the extension for you. If you need to declare it by hand — for example, to attach the settings object specifying which MIME types your app resources serve — the new API looks like this:

$server->addExtension('io.modelcontextprotocol/ui', ['mimeTypes' => ['text/html;profile=mcp-app']]);

You can also declare extensions as enum cases on the server class, which advertises them with an empty settings object:

use Laravel\Mcp\Enums\Extension;

class DocsServer extends Server
{
    protected array $extensions = [
        Extension::Ui,
    ];
}

A matching hasExtension('io.modelcontextprotocol/ui') lets you branch on what's been declared at runtime.

Scope: what's in, and what's deliberately out

There is no generic extensions API beyond addExtension()/hasExtension(). Extensions are opt-in per the spec, and one advertisement method covers everything the Apps case needs today. Likewise, the deeper Apps conformance audit is not part of this change, because the Apps extension is versioned separately from the 2026-07-28 protocol revision and its spec isn't part of it.

What the PR does pin down are the parts the extension documentation fixes, now locked in by conformance tests: the ui:// URI scheme, the text/html;profile=mcp-app MIME type, and the _meta.ui payload carrying CSP, permissions, and domain information.

Credit to @JordanDalton, whose #275 mapped out the 2026-07-28 work this builds on.

Takeaways

  • MCP Apps now advertises under capabilities.extensions, matching the 2026-07-28 spec's extension negotiation.
  • BC break: read capabilities['extensions']['io.modelcontextprotocol/ui'], not the old top-level key.
  • Server::CAPABILITY_UI is replaced by Server::EXTENSION_UI, backed by a new Extension enum.
  • AppResource registration still auto-advertises; manual declaration uses addExtension() with an optional settings object.
  • No generic extension API beyond add/has; the Apps conformance audit is deferred to the extension's own versioning.

Sources

More from this release

Related Articles