Skip to content

[MCP Bundle] Add MCP Apps support (#[AsMcpApp] / #[AsMcpAppTool])#2237

Open
chr-hertel wants to merge 7 commits into
symfony:mainfrom
chr-hertel:mcp-bundle-mcp-apps
Open

[MCP Bundle] Add MCP Apps support (#[AsMcpApp] / #[AsMcpAppTool])#2237
chr-hertel wants to merge 7 commits into
symfony:mainfrom
chr-hertel:mcp-bundle-mcp-apps

Conversation

@chr-hertel

@chr-hertel chr-hertel commented Jun 24, 2026

Copy link
Copy Markdown
Member
Q A
Bug fix? no
New feature? yes
Docs? yes
Issues
License MIT

What is this?

Wire your Symfony services + Twig into MCP Apps — interactive HTML UIs an MCP host renders in a sandboxed iframe instead of a plain-text tool result. One class is the whole app: #[AsMcpApp] registers the UI resource, the linked tool and the MCP Apps server extension, and the bundle ships the iframe bridge (@Mcp/app/base.html.twig). You write a tool method and a Twig template — no protocol plumbing, no client JS.

A minimal app (single template)

The tool method returns a context array; the bundle renders toolTemplate into the result over the wire, so no Twig touches your handler:

#[AsMcpApp(
    uri: 'ui://weather',
    name: 'get_weather',
    template: 'mcp/weather.html.twig',      // static iframe shell
    toolTemplate: 'mcp/_weather.html.twig', // rendered into the tool result
)]
final class WeatherApp
{
    public function __construct(private WeatherClient $weather) {}

    public function render(string $city): array
    {
        return ['forecast' => $this->weather->forecastFor($city)];
    }
}
{# mcp/weather.html.twig — extends the bundle base; the rendered fragment lands in #root #}
{% extends '@Mcp/app/base.html.twig' %}
{% block body %}<div id="root"></div>{% endblock %}

Adding interactive tools

Declare follow-up tools with #[AsMcpAppTool] (server side) and trigger them from the markup with the base template's declarative attributes (client side) — still no JavaScript:

#[AsMcpAppTool(name: 'set_unit', template: 'mcp/_weather.html.twig', appOnly: true)]
public function setUnit(string $city, string $unit): array
{
    return ['forecast' => $this->weather->forecastFor($city, $unit)];
}
<button data-call="set_unit" data-arg-city="{{ city }}" data-arg-unit="celsius">°C</button>
<form data-call="get_weather"><input name="city"><button>Go</button></form>

data-call invokes the tool and swaps the returned HTML into #root; data-open opens an external link. It works on buttons, submit buttons (incl. the cross-DOM form="…" pattern) and anchors — verified with a jsdom harness.

Notes

  • A ReferenceHandler decorator renders the template into the tool result while keeping the SDK's reflection-derived input schema intact; mcp.apps.enabled toggles the extension.
  • demo/ exposes the movie collection as an MCP App (MovieApp — searchable grid + detail), reusing the existing movie_search agent tool (Movie is now JsonSerializable, so it serves both the agent and the UI).

Demo

Screencast.from.2026-06-25.10-07-36.webm

@carsonbot carsonbot changed the title [McpBundle] Add MCP Apps support (#[AsMcpApp] / #[AsMcpAppTool]) [MCP Bundle] Add MCP Apps support (#[AsMcpApp] / #[AsMcpAppTool]) Jun 24, 2026
@carsonbot carsonbot changed the title [MCP Bundle] Add MCP Apps support (#[AsMcpApp] / #[AsMcpAppTool]) Add MCP Apps support (#[AsMcpApp] / #[AsMcpAppTool]) Jun 24, 2026
@chr-hertel chr-hertel added the MCP Bundle Issues & PRs about the MCP SDK integration bundle label Jun 24, 2026
@carsonbot carsonbot changed the title Add MCP Apps support (#[AsMcpApp] / #[AsMcpAppTool]) [MCP Bundle] Add MCP Apps support (#[AsMcpApp] / #[AsMcpAppTool]) Jun 24, 2026
@chr-hertel chr-hertel force-pushed the mcp-bundle-mcp-apps branch 6 times, most recently from 794f92f to 2040e89 Compare June 25, 2026 00:06
@OskarStark

Copy link
Copy Markdown
Contributor

Can you add a screencast from the new Movie MCPApp?

@chr-hertel

Copy link
Copy Markdown
Member Author

Can you add a screencast from the new Movie MCPApp?

Can show you in a few minutes in real 🥳

But will do, yes. Can you check if your MCP Apps would work with this as well? basically with less code

@chr-hertel chr-hertel force-pushed the mcp-bundle-mcp-apps branch from 2040e89 to a7e0b52 Compare June 25, 2026 07:57

@OskarStark OskarStark left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets merge it

The default render() only injected `model.html`, so an MCP App's form controls
were not kept in sync with the tool result — e.g. the movie app's search box was
empty on the first render even though the model had searched. render() now also
copies scalar model fields into matching `[name]` controls (skipping the focused
one), restoring the query in the input.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature New feature MCP Bundle Issues & PRs about the MCP SDK integration bundle Status: Reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants