Agentic UI mit MCP Apps: Tool-Ergebnisse als interaktive Widgets

  1. AG-UI verstehen: Der Standard für Agentic User Interfaces
  2. AG-UI in der Praxis: Das SDK für TypeScript
  3. AG-UI End-2-End: Server und Client verbinden
  4. Agentic UI mit Angular, CopilotKit und AG-UI umsetzen
  5. A2UI: Wie KI dynamische UIs zur Laufzeit erzeugt
  6. A2UI mit AG-UI und CopilotKit in Angular integrieren
  7. Custom Catalogs in A2UI: Eigene Komponenten für KI-generierte UIs
  8. A2UI mit DSL: Kontrollierbare Dashboards optimiert für Performance
  9. Agentic UI mit MCP Apps: Tool-Ergebnisse als interaktive Widgets
  10. MCP Apps in Angular mit CopilotKit: Rich Chat Interfaces Instead of Text Responses

Ein Sprachmodell ist nur so gut wie der Kontext und die Aktionen, die ihm zur Verfügung stehen. Das Model Context Protocol (MCP) hat sich als der Standard etabliert, der Agenten mit externen Tools, Daten und Diensten verbindet. Es befreit uns davon, für jede Kombination aus Modell und Tool eine eigene Integration zu schreiben.

Bisher liefern diese Tools jedoch meist reinen Text oder strukturierte Daten, die der Agent als schlichte Chat-Nachricht darstellt. MCP Apps erweitert das Protokoll um interaktive Benutzeroberflächen, sodass ein Tool-Ergebnis als maßgeschneidertes Widget statt als Textwüste erscheint. Damit schließt sich die Lücke zwischen konversationellen Agenten und den ansprechenden Oberflächen, die Anwender von modernen Web-Anwendungen gewohnt sind.

Dieser erste Teil legt die Grundlagen: Er zeigt, wie MCP und MCP Apps zusammenspielen, und veranschaulicht die Funktionsweise anhand einer minimalen, auf VanillaJS basierenden Demo aus Host und App. Im zweiten Teil bauen wir darauf auf und binden den MCP Server eines Geschäftspartners an unser Flugportal an – mit Sprachmodell, Agent und AG-UI.

📂 Quellcode (branch: copilotkit)

Die Readme zeigt, wie man das Beispiel samt Backend und MCP Server startet.

MCP und MCP Apps

Bevor wir uns dem SDK und dem Code widmen, lohnt sich ein Blick auf die beiden Bausteine, auf denen unsere Lösung aufsetzt: MCP als Fundament und MCP Apps als Erweiterung für interaktive Oberflächen.

MCP

Das Model Context Protocol (MCP) ist ein offener Standard, über den Dritte Tools zur Verfügung stellen können. Anthropic hat das populäre Protokoll im November 2024 vorgestellt und Ende 2025 an die Agentic AI Foundation übergeben – einen Fonds unter dem Dach der Linux Foundation. MCP liefert sowohl Metadaten zu den bereitgestellten Tools als auch die Möglichkeit, diese aufzurufen:

Agenten binden diese Tools somit sehr einfach ein. Weitere Kernaufgaben von MCP sind das Bereitstellen von Ressourcen – wie zum Beispiel Dateien, Dokumenten oder Datenbankinhalten – sowie das Anbieten wiederverwendbarer Prompts.

MCP Apps

MCP Apps ist eine Erweiterung zu MCP, mit der sich interaktive Benutzeroberflächen für die bereitgestellten Tools anbieten lassen. Eine solche App kann ein Tool aufrufen, den Ausführungsfortschritt visualisieren und die erhaltenen Ergebnisse präsentieren. Wir nutzen sie hier für Letzteres, zumal der Aufruf in unserem System über AG-UI erfolgt.

Technisch gesehen handelt es sich bei einer MCP App um eine eigene Web-Anwendung, die auf einem beliebigen Framework oder auf VanillaJS basieren kann. MCP Apps werden via MCP als Ressourcen bereitgestellt. Die Metadaten eines Tools verweisen mit einer resourceUri auf die jeweilige App:

{
  "ui": {
    "resourceUri": "ui://hotels/results.html"
  },
  "ui/resourceUri": "ui://hotels/results.html"
}

Unter Angabe dieser URI bezieht eine Anwendung via MCP die gesamte Datei und visualisiert so das jeweilige Tool. Im gezeigten Beispiel kommt diese URI doppelt vor, wobei die zweite, flache Version (ui/resourceUri) lediglich aus Gründen der Abwärtskompatibilität vorliegt. Die verschachtelte Version entspricht dem aktuellen Standard.

Die Anwendung, die eine App einbindet, nennt sich Host. Um Konflikte zu vermeiden, isoliert der Host die App in einer Sandbox. Dazu kommt ein iframe zum Einsatz. Damit sich die App nicht wie ein Fremdkörper anfühlt, übergibt der Host einen sogenannten Host Context, der unter anderem wohldefinierte CSS-Variablen fürs Theming vorgibt oder über den verfügbaren Platz informiert. Daneben übergibt der Host die an das aufgerufene Tool übersendeten Parameter sowie das erhaltene Ergebnis.

Um eine Scrollbar im iframe zu vermeiden, informiert die App den Host über ihren Platzbedarf, sodass der Host den iframe entsprechend vergrößert. Die gesamte Kommunikation zwischen Host und App erfolgt durch den Austausch von JSON-Dokumenten. Diese versenden beide Seiten in der Regel über die postMessage-API. Die JSON-Dokumente sind im MCP-Apps-Protokoll definiert.

NOTE

Agentic UI with Angular

Wenn du MCP Apps nicht nur integrieren, sondern sauber in größere Architekturen einbetten willst:
In meinem Buch Agentic UI mit Angular gehe ich genau auf diese Patterns und Trade-offs im Detail ein.

Cover des eBooks Agentic UI with Angular

Mehr zum eBook →

Erste Demo-Anwendung

Die nachfolgenden Abschnitte veranschaulichen die Funktionsweise eines Hosts und einer App. Zur Vereinfachung kommt bei diesem ersten Beispiel kein Agent und somit auch kein Sprachmodell zum Einsatz. Stattdessen verwenden wir einen einfachen, auf VanillaJS basierenden Host, der lediglich eine ebenfalls auf VanillaJS basierende App lädt.

Der Host lädt diese App in einen iframe und übersendet ihr unter anderem ein paar Hotels:

Der gesamte Quellcode befindet sich im Demo-Repository unter mcp-apps-demo.

MCP Apps SDK

Für die App stellt es ein App-Objekt zur Verfügung, und der Host nutzt eine AppBridge:

Die beiden Objekte verbinden sich über einen sogenannten Transport. Im Lieferumfang des SDKs befindet sich ein Transport für postMessage. Sobald die beiden Partien miteinander verbunden sind, lassen sie einander Nachrichten zukommen. Zum Versenden von Nachrichten weisen beide Objekte Methoden auf; empfangen werden sie über Event-Handler.

Einen Host bereitstellen

Um eine App zu laden, erzeugt der Host zunächst einen iframe, den er über die Eigenschaft sandbox absichert. Anschließend lädt er das HTML der verknüpften Ressource hinein. Zur Vereinfachung setzen wir hier stattdessen die src-Eigenschaft auf eine hartcodierte HTML-Datei:

const frameHost = document.getElementById('iframe-host');

if (!(frameHost instanceof HTMLDivElement)) {
  throw new Error('Missing iframe host element.');
}

const iframe = document.createElement('iframe');
iframe.title = 'MCP App Demo';
iframe.sandbox.add('allow-scripts');
iframe.sandbox.add('allow-same-origin');
// iframe.srcdoc = '<html>...</html>';
iframe.src = './app.html';
frameHost.append(iframe);

Sobald die Eigenschaft sandbox vorhanden ist, verbietet der Browser im iframe Aktionen wie Downloads, das Absenden von Formularen oder Top-Level-Navigationen. Standardmäßig verbietet eine Sandbox auch das Ausführen von Skripten im iframe und weist der in den iframe geladenen Anwendung die Origin null zu. Das wäre im Fall von MCP Apps kontraproduktiv, zumal die App auf JavaScript basiert und eine Origin null die Kommunikation via postMessage verbietet. Aus diesem Grund weicht unser Host die Sandbox mit den Ausnahmen allow-scripts und allow-same-origin etwas auf.

Sobald die App in den iframe geladen wurde, baut der Host eine Verbindung mit der AppBridge auf:

import {
  AppBridge,
  PostMessageTransport,
} from '@modelcontextprotocol/ext-apps/app-bridge';

[...]

const bridge = new AppBridge(
  null,
  { name: 'MCP Apps Demo Host', version: '1.0.0' },
  { logging: { level: 'info' } },
);

bridge.onsizechange = (event) => {
  iframe.style.height = `${Math.ceil(event.height ?? 0)}px`;
};

await bridge.connect(
  new PostMessageTransport(iframe.contentWindow, iframe.contentWindow),
);

await waitForInitialization(bridge);

bridge.sendToolInput({
  arguments: {
    city: 'Graz',
  },
});

bridge.sendToolResult({
  content: [
    {
      type: 'text',
      text: 'The host sends this tool result to the app.',
    },
  ],
  structuredContent: {
    city: 'Graz',
    hotels: ['Grand Palace', 'Skyline Suites', 'Biz Hotel'],
  },
});

bridge.sendHostContextChange({
  availableDisplayModes: ['fullscreen'],
  displayMode: 'fullscreen',
  theme: 'light',
  styles: {
    variables: {
      "--color-background-primary": "#3f51b5"
    } as StyleVariables
  }
});

Das erste von der AppBridge erwartete Argument ist ein sogenannter (MCP) Client, der eine direkte Verbindung zum MCP Server erlaubt. Da wir hier jedoch keine Kopplung zum MCP Server haben wollen, sondern es lediglich die über AG-UI erhaltenen Tool-Call-Ergebnisse zu visualisieren gilt, übergeben wir hier den für solche Fälle vorgesehenen Wert null.

Den festgelegten Anwendungsnamen und die Version stellt die AppBridge der App zur Verfügung. Diese kann prüfen, ob sie mit diesem Host kommunizieren kann oder ob es sich dabei zum Beispiel um eine nicht mehr unterstützte Version handelt. Diese Prüfungen erfolgen nicht automatisch, sondern müssten bei Bedarf explizit umgesetzt werden – worauf wir in diesem Beispiel bewusst verzichten.

Das Logging-Level info gibt uns die Möglichkeit, alle ausgetauschten Nachrichten auf der JavaScript-Konsole mitzuverfolgen. Die App löst nach dem Initialisieren ohne weiteres Zutun das Event onsizechange aus. Dieses nutzt der Host, um den iframe auf die benötigte Größe zu bringen. Das vermeidet eine Scrollbar im iframe.

Die connect-Methode baut die Verbindung zum App-Objekt in der App auf. Der PostMessageTransport, der zur Kommunikation die postMessage-API nutzt, bekommt das contentWindow des iframes sowohl als Quelle der an den Host gesendeten als auch als Ziel der vom Host versendeten Nachrichten genannt.

Nach dem Aufruf von connect gilt es zu warten, bis die App das oninitialized-Event auslöst. Das erfolgt mit der Hilfsfunktion waitForInitialization, die das Event in einen Promise überführt:

function waitForInitialization(bridge: AppBridge): Promise<void> {
  return new Promise((resolve) => {
    bridge.oninitialized = () => {
      resolve();
    };
  });
}

Nach der Initialisierung übersendet der Host die ans Tool übergebenen Parameter (sendToolInput), das erhaltene Ergebnis (sendToolResult) und den Host Context (sendHostContextChange). Letzterer legt Details zur Darstellung sowie zum Verhalten der im Host eingebetteten App fest.

Die Typisierung verlangt die Angabe sämtlicher CSS-Variablen für das Styling. Um diese Demo auf nur eine Variable zu beschränken, erfolgt hier eine Type Assertion zu StyleVariables. Leider veröffentlicht das SDK diesen Typ nicht, weswegen wir ihn aus der öffentlichen API ableiten müssen:

type StyleVariables = NonNullable<
  McpUiHostContext['styles']
>['variables'];

Eine App bereitstellen

Die MCP App erzeugt eine App-Instanz, richtet Event-Handler zum Empfangen der übergebenen Parameter, des Tool-Ergebnisses und des Host Context ein und verbindet sich zum Host:

import { App } from '@modelcontextprotocol/ext-apps';

[...]

const app = new App({
  name: 'MCP Apps Demo App',
  version: '1.0.0',
});

app.ontoolinput = (input) => {
  [...]
};

app.ontoolresult = (result) => {
  [...]
};

app.onhostcontextchanged = (context) => {
  [...]
};

await app.connect();

App und Host aufräumen

Entfernt der Host eine App wieder – etwa weil der Anwender das Widget schließt oder das Gespräch weiterzieht –, sollten beide Seiten geordnet herunterfahren. Das SDK bietet dafür zwei Mechanismen: eine Teardown-Anforderung, die der App die Gelegenheit gibt, ihren Zustand zu sichern, sowie das Schließen der zugrunde liegenden Verbindung.

Auf Seite des Hosts fordert die AppBridge mit teardownResource ein geordnetes Herunterfahren an und wartet auf die Bestätigung der App. Erst danach schließt sie mit close die postMessage-Verbindung und entfernt den iframe:

await bridge.teardownResource({});
await bridge.close();
iframe.remove();

Die App reagiert über den Event-Handler onteardown auf diese Anforderung. Der Handler darf asynchron arbeiten; der Host wartet auf den zurückgegebenen Promise, bevor er den iframe entfernt. Die Verbindung ihrerseits beendet die App jederzeit selbst mit app.close():


app.onteardown = async () => {
  return {};
};

Die App kann den Teardown auch selbst anstoßen – zum Beispiel über einen Schließen-Button in ihrer Oberfläche. Dazu ruft sie requestTeardown auf:

await app.requestTeardown();

Der Host erfährt davon über das Event onrequestteardown und entscheidet, ob er das Herunterfahren tatsächlich einleitet:

bridge.onrequestteardown = async () => {
  await bridge.teardownResource({});
  iframe.remove();
};

Sowohl die AppBridge als auch das App-Objekt erben close von der Protocol-Basisklasse des MCP SDK.

Zusammenfassung und Ausblick

MCP standardisiert die Anbindung von Tools an Agenten, und MCP Apps hebt diese Idee auf die Ebene der Benutzeroberfläche: Statt eines Tool-Ergebnisses in Textform erhält der Anwender eine interaktive, maßgeschneiderte Oberfläche. Eine MCP App ist dabei eine eigenständige Web-Anwendung, auf die die Metadaten eines Tools über eine resourceUri verweisen. Der Host lädt die App in einen abgesicherten iframe und kommuniziert mit ihr über die postMessage-API – abstrahiert durch das App-Objekt auf Seite der App und die AppBridge auf Seite des Hosts.

Anhand einer minimalen, auf VanillaJS basierenden Demo haben wir den kompletten Lebenszyklus durchgespielt: vom Aufbau des Hosts über die Initialisierung, den Austausch von Tool-Parametern, -Ergebnissen und Host Context bis hin zum geordneten Teardown mit teardownResource und close. Bewusst kam dabei weder ein Agent noch ein Sprachmodell zum Einsatz, um den Blick ganz auf das Zusammenspiel von Host und App zu lenken.

Damit ist das Fundament gelegt. Im nächsten Artikel wenden wir dieses Wissen auf eine vollständige Fallstudie an: Wir binden den MCP Server eines Geschäftspartners, der auf Hotelbuchungen spezialisiert ist, an unser Flugportal an. Mit dabei sind ein Sprachmodell, das Agent-Framework Mastra sowie das AG-UI-Protokoll, das nach einem Tool Call einen ACTIVITY_SNAPSHOT zum Client transportiert – den wiederum der in CopilotKit enthaltene Activity Renderer für MCP Apps darstellt.

Nächster Artikel →

Interesse an produktionsreifen Agentic-UI-Architekturen?

In meinem Workshop beschäftigen wir uns mit AG-UI, A2UI, MCP Apps, HITL-Patterns und modernen Angular-Architekturen für reale agentische Systeme.

Workshop: Agentic AI mit Angular – AG-UI, A2UI, MCP Apps & HITL-Patterns

Alle Details →

Agentic UI with Angular

Architecting Agentic AI with Open Standards

Integriere AI-Agents in Angular mit offenen Standards.

Mehr zum Buch