diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 926710e37..d4707d45b 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -24,6 +24,7 @@ jobs:
- run: |
npx pkg-pr-new publish \
. \
+ ./examples/basic-server-angular \
./examples/basic-server-react \
./examples/basic-server-vanillajs \
./examples/budget-allocator-server \
diff --git a/AGENTS.md b/AGENTS.md
index 41997da16..73f707975 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -88,7 +88,7 @@ Standalone docs in `docs/` (listed in `typedoc.config.mjs` `projectDocuments`) c
Uses npm workspaces. Full examples in `examples/` are separate packages:
-- `basic-server-*` - Starter templates (vanillajs, react, vue, svelte, preact, solid). Use these as the basis for new examples.
+- `basic-server-*` - Starter templates (vanillajs, react, vue, svelte, preact, solid, angular). Use these as the basis for new examples.
- `basic-host` - Reference host implementation
- Other examples showcase specific features (charts, 3D, video, etc.)
diff --git a/README.md b/README.md
index e1429330c..8acff3061 100644
--- a/README.md
+++ b/README.md
@@ -88,7 +88,7 @@ The [`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/exa
| | |
|:---:|:---|
-| [](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) | The same app built with different frameworks — pick your favorite!
[React](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) · [Vue](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vue) · [Svelte](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-svelte) · [Preact](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-preact) · [Solid](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-solid) · [Vanilla JS](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) |
+| [](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) | The same app built with different frameworks — pick your favorite!
[React](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) · [Vue](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vue) · [Svelte](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-svelte) · [Preact](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-preact) · [Solid](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-solid) · [Angular](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-angular) · [Vanilla JS](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) |
### Running the Examples
@@ -176,6 +176,16 @@ To use these examples with MCP clients that support the stdio transport (such as
"--stdio"
]
},
+ "basic-angular": {
+ "command": "npx",
+ "args": [
+ "-y",
+ "--silent",
+ "--registry=https://registry.npmjs.org/",
+ "@modelcontextprotocol/server-basic-angular",
+ "--stdio"
+ ]
+ },
"budget-allocator": {
"command": "npx",
"args": [
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 3fb11984e..fe60115e0 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -471,5 +471,5 @@ You've built your first MCP App!
- **Continue learning**: The [`basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) example builds on this quickstart with host communication, theming, and lifecycle handlers
- **React version**: Compare with [`basic-server-react`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) for a React-based UI
-- **Other frameworks**: See also [Vue](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vue), [Svelte](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-svelte), [Preact](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-preact), and [Solid](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-solid) examples
+- **Other frameworks**: See also [Vue](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vue), [Svelte](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-svelte), [Preact](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-preact), [Solid](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-solid), and [Angular](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-angular) examples
- **API reference**: See the full [API documentation](https://modelcontextprotocol.github.io/ext-apps/api/)
diff --git a/examples/basic-server-angular/.gitignore b/examples/basic-server-angular/.gitignore
new file mode 100644
index 000000000..b94707787
--- /dev/null
+++ b/examples/basic-server-angular/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+dist/
diff --git a/examples/basic-server-angular/README.md b/examples/basic-server-angular/README.md
new file mode 100644
index 000000000..8c1f644e6
--- /dev/null
+++ b/examples/basic-server-angular/README.md
@@ -0,0 +1,75 @@
+# Example: Basic Server (Angular)
+
+
+
+An MCP App example with an Angular UI.
+
+> [!TIP]
+> Looking for a vanilla JavaScript example? See [`basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs)!
+
+## MCP Client Configuration
+
+Add to your MCP client configuration (stdio transport):
+
+```json
+{
+ "mcpServers": {
+ "basic-angular": {
+ "command": "npx",
+ "args": [
+ "-y",
+ "--silent",
+ "--registry=https://registry.npmjs.org/",
+ "@modelcontextprotocol/server-basic-angular",
+ "--stdio"
+ ]
+ }
+ }
+}
+```
+
+### Local Development
+
+To test local modifications, use this configuration (replace `~/code/ext-apps` with your clone path):
+
+```json
+{
+ "mcpServers": {
+ "basic-angular": {
+ "command": "bash",
+ "args": [
+ "-c",
+ "cd ~/code/ext-apps/examples/basic-server-angular && npm run build >&2 && node dist/index.js --stdio"
+ ]
+ }
+ }
+}
+```
+
+## Overview
+
+- Tool registration with a linked UI resource
+- Angular UI using signals and zoneless change detection
+- App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink)
+
+## Key Files
+
+- [`server.ts`](server.ts) - MCP server with tool and resource registration
+- [`mcp-app.html`](mcp-app.html) / [`src/mcp-app.ts`](src/mcp-app.ts) - Angular standalone component using MCP App SDK
+
+## Getting Started
+
+```bash
+npm install
+npm run dev
+```
+
+## How It Works
+
+1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`).
+2. When the tool is invoked, the Host renders the UI from the resource.
+3. The UI uses the MCP App SDK API to communicate with the host and call server tools.
+
+## Build System
+
+This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` and `@analogjs/vite-plugin-angular` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata.
diff --git a/examples/basic-server-angular/main.ts b/examples/basic-server-angular/main.ts
new file mode 100644
index 000000000..e2cc9d088
--- /dev/null
+++ b/examples/basic-server-angular/main.ts
@@ -0,0 +1,93 @@
+/**
+ * Entry point for running the MCP server.
+ * Run with: npx @modelcontextprotocol/server-basic-angular
+ * Or: node dist/index.js [--stdio]
+ */
+
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import cors from "cors";
+import type { Request, Response } from "express";
+import { createServer } from "./server.js";
+
+/**
+ * Starts an MCP server with Streamable HTTP transport in stateless mode.
+ *
+ * @param createServer - Factory function that creates a new McpServer instance per request.
+ */
+export async function startStreamableHTTPServer(
+ createServer: () => McpServer,
+): Promise {
+ const port = parseInt(process.env.PORT ?? "3001", 10);
+
+ const app = createMcpExpressApp({ host: "0.0.0.0" });
+ app.use(cors());
+
+ app.all("/mcp", async (req: Request, res: Response) => {
+ const server = createServer();
+ const transport = new StreamableHTTPServerTransport({
+ sessionIdGenerator: undefined,
+ });
+
+ res.on("close", () => {
+ transport.close().catch(() => {});
+ server.close().catch(() => {});
+ });
+
+ try {
+ await server.connect(transport);
+ await transport.handleRequest(req, res, req.body);
+ } catch (error) {
+ console.error("MCP error:", error);
+ if (!res.headersSent) {
+ res.status(500).json({
+ jsonrpc: "2.0",
+ error: { code: -32603, message: "Internal server error" },
+ id: null,
+ });
+ }
+ }
+ });
+
+ const httpServer = app.listen(port, (err) => {
+ if (err) {
+ console.error("Failed to start server:", err);
+ process.exit(1);
+ }
+ console.log(`MCP server listening on http://localhost:${port}/mcp`);
+ });
+
+ const shutdown = () => {
+ console.log("\nShutting down...");
+ httpServer.close(() => process.exit(0));
+ };
+
+ process.on("SIGINT", shutdown);
+ process.on("SIGTERM", shutdown);
+}
+
+/**
+ * Starts an MCP server with stdio transport.
+ *
+ * @param createServer - Factory function that creates a new McpServer instance.
+ */
+export async function startStdioServer(
+ createServer: () => McpServer,
+): Promise {
+ await createServer().connect(new StdioServerTransport());
+}
+
+async function main() {
+ if (process.argv.includes("--stdio")) {
+ await startStdioServer(createServer);
+ } else {
+ await startStreamableHTTPServer(createServer);
+ }
+}
+
+main().catch((e) => {
+ console.error(e);
+ process.exit(1);
+});
diff --git a/examples/basic-server-angular/mcp-app.html b/examples/basic-server-angular/mcp-app.html
new file mode 100644
index 000000000..7812d7509
--- /dev/null
+++ b/examples/basic-server-angular/mcp-app.html
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ Get Time App
+
+
+
+
+
+
+
diff --git a/examples/basic-server-angular/package.json b/examples/basic-server-angular/package.json
new file mode 100644
index 000000000..3b2e64bf9
--- /dev/null
+++ b/examples/basic-server-angular/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@modelcontextprotocol/server-basic-angular",
+ "version": "1.0.1",
+ "type": "module",
+ "description": "Basic MCP App Server example using Angular",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "directory": "examples/basic-server-angular"
+ },
+ "license": "MIT",
+ "main": "dist/server.js",
+ "types": "dist/server.d.ts",
+ "bin": {
+ "mcp-server-basic-angular": "dist/index.js"
+ },
+ "files": [
+ "dist"
+ ],
+ "exports": {
+ ".": {
+ "types": "./dist/server.d.ts",
+ "default": "./dist/server.js"
+ }
+ },
+ "scripts": {
+ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"",
+ "watch": "cross-env INPUT=mcp-app.html vite build --watch",
+ "serve": "bun --watch main.ts",
+ "start": "cross-env NODE_ENV=development concurrently --kill-others-on-fail \"npm run build\" \"npm run serve\"",
+ "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"",
+ "prepublishOnly": "npm run build"
+ },
+ "dependencies": {
+ "@angular/common": "^21.0.0",
+ "@angular/core": "^21.0.0",
+ "@angular/forms": "^21.0.0",
+ "@angular/platform-browser": "^21.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.24.0",
+ "cors": "^2.8.5",
+ "express": "^5.1.0",
+ "rxjs": "^7.8.1",
+ "tslib": "^2.8.0",
+ "zod": "^4.1.13"
+ },
+ "devDependencies": {
+ "@analogjs/vite-plugin-angular": "^2.2.3",
+ "@angular/build": "^21.0.0",
+ "@angular-devkit/build-angular": "^21.0.0",
+ "@angular/compiler": "^21.0.0",
+ "@angular/compiler-cli": "^21.0.0",
+ "@types/cors": "^2.8.19",
+ "@types/express": "^5.0.0",
+ "@types/node": "22.10.0",
+ "concurrently": "^9.2.1",
+ "cross-env": "^10.1.0",
+ "ts-morph": "^24.0.0",
+ "typescript": "^5.9.3",
+ "vite": "^6.0.0",
+ "vite-plugin-singlefile": "^2.3.0"
+ }
+}
diff --git a/examples/basic-server-angular/server.ts b/examples/basic-server-angular/server.ts
new file mode 100644
index 000000000..43a461914
--- /dev/null
+++ b/examples/basic-server-angular/server.ts
@@ -0,0 +1,55 @@
+import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
+import fs from "node:fs/promises";
+import path from "node:path";
+
+// Works both from source (server.ts) and compiled (dist/server.js)
+const DIST_DIR = import.meta.filename.endsWith(".ts")
+ ? path.join(import.meta.dirname, "dist")
+ : import.meta.dirname;
+
+/**
+ * Creates a new MCP server instance with tools and resources registered.
+ */
+export function createServer(): McpServer {
+ const server = new McpServer({
+ name: "Basic MCP App Server (Angular)",
+ version: "1.0.0",
+ });
+
+ // Two-part registration: tool + resource, tied together by the resource URI.
+ const resourceUri = "ui://get-time/mcp-app.html";
+
+ // Register a tool with UI metadata. When the host calls this tool, it reads
+ // `_meta.ui.resourceUri` to know which resource to fetch and render as an
+ // interactive UI.
+ registerAppTool(server,
+ "get-time",
+ {
+ title: "Get Time",
+ description: "Returns the current server time as an ISO 8601 string.",
+ inputSchema: {},
+ _meta: { ui: { resourceUri } }, // Links this tool to its UI resource
+ },
+ async (): Promise => {
+ const time = new Date().toISOString();
+ return { content: [{ type: "text", text: time }] };
+ },
+ );
+
+ // Register the resource, which returns the bundled HTML/JavaScript for the UI.
+ registerAppResource(server,
+ resourceUri,
+ resourceUri,
+ { mimeType: RESOURCE_MIME_TYPE },
+ async (): Promise => {
+ const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
+ return {
+ contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }],
+ };
+ },
+ );
+
+ return server;
+}
diff --git a/examples/basic-server-angular/src/global.css b/examples/basic-server-angular/src/global.css
new file mode 100644
index 000000000..801291f46
--- /dev/null
+++ b/examples/basic-server-angular/src/global.css
@@ -0,0 +1,92 @@
+:root {
+ color-scheme: light dark;
+
+ /*
+ * Fallbacks for host style variables used by this app.
+ * The host may provide these (and many more) via the host context.
+ */
+ --color-text-primary: light-dark(#1f2937, #f3f4f6);
+ --color-text-inverse: light-dark(#f3f4f6, #1f2937);
+ --color-text-info: light-dark(#1d4ed8, #60a5fa);
+ --color-background-primary: light-dark(#ffffff, #1a1a1a);
+ --color-background-inverse: light-dark(#1a1a1a, #ffffff);
+ --color-background-info: light-dark(#eff6ff, #1e3a5f);
+ --color-ring-primary: light-dark(#3b82f6, #60a5fa);
+ --border-radius-md: 6px;
+ --border-width-regular: 1px;
+ --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
+ --font-weight-normal: 400;
+ --font-weight-bold: 700;
+ --font-text-md-size: 1rem;
+ --font-text-md-line-height: 1.5;
+ --font-heading-3xl-size: 2.25rem;
+ --font-heading-3xl-line-height: 1.1;
+ --font-heading-2xl-size: 1.875rem;
+ --font-heading-2xl-line-height: 1.2;
+ --font-heading-xl-size: 1.5rem;
+ --font-heading-xl-line-height: 1.25;
+ --font-heading-lg-size: 1.25rem;
+ --font-heading-lg-line-height: 1.3;
+ --font-heading-md-size: 1rem;
+ --font-heading-md-line-height: 1.4;
+ --font-heading-sm-size: 0.875rem;
+ --font-heading-sm-line-height: 1.4;
+
+ /* Spacing derived from host typography */
+ --spacing-unit: var(--font-text-md-size);
+ --spacing-xs: calc(var(--spacing-unit) * 0.25);
+ --spacing-sm: calc(var(--spacing-unit) * 0.5);
+ --spacing-md: var(--spacing-unit);
+ --spacing-lg: calc(var(--spacing-unit) * 1.5);
+
+ /* App accent color (customize for your brand) */
+ --color-accent: #2563eb;
+ --color-text-on-accent: #ffffff;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html, body {
+ font-family: var(--font-sans);
+ font-size: var(--font-text-md-size);
+ font-weight: var(--font-weight-normal);
+ line-height: var(--font-text-md-line-height);
+ color: var(--color-text-primary);
+}
+
+h1 {
+ font-size: var(--font-heading-3xl-size);
+ line-height: var(--font-heading-3xl-line-height);
+}
+h2 {
+ font-size: var(--font-heading-2xl-size);
+ line-height: var(--font-heading-2xl-line-height);
+}
+h3 {
+ font-size: var(--font-heading-xl-size);
+ line-height: var(--font-heading-xl-line-height);
+}
+h4 {
+ font-size: var(--font-heading-lg-size);
+ line-height: var(--font-heading-lg-line-height);
+}
+h5 {
+ font-size: var(--font-heading-md-size);
+ line-height: var(--font-heading-md-line-height);
+}
+h6 {
+ font-size: var(--font-heading-sm-size);
+ line-height: var(--font-heading-sm-line-height);
+}
+
+code, pre, kbd {
+ font-family: var(--font-mono);
+ font-size: 1em;
+}
+
+b, strong {
+ font-weight: var(--font-weight-bold);
+}
diff --git a/examples/basic-server-angular/src/mcp-app.css b/examples/basic-server-angular/src/mcp-app.css
new file mode 100644
index 000000000..ae08a8ccf
--- /dev/null
+++ b/examples/basic-server-angular/src/mcp-app.css
@@ -0,0 +1,79 @@
+.main {
+ width: 100%;
+ max-width: 425px;
+ box-sizing: border-box;
+
+ > * {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+
+ > * + * {
+ margin-top: var(--spacing-lg);
+ }
+}
+
+.action {
+ > * {
+ margin-top: 0;
+ margin-bottom: 0;
+ width: 100%;
+ }
+
+ > * + * {
+ margin-top: var(--spacing-sm);
+ }
+
+ /* Server time row: flex layout for consistent mask width in E2E tests */
+ > p {
+ display: flex;
+ align-items: baseline;
+ gap: var(--spacing-xs);
+ }
+
+ /* Consistent font for form inputs (inherits from global.css) */
+ textarea,
+ input {
+ display: block;
+ font-family: inherit;
+ font-size: inherit;
+ }
+
+ button {
+ padding: var(--spacing-sm) var(--spacing-md);
+ border: none;
+ border-radius: var(--border-radius-md);
+ color: var(--color-text-on-accent);
+ font-weight: var(--font-weight-bold);
+ background-color: var(--color-accent);
+ cursor: pointer;
+
+ &:hover {
+ background-color: color-mix(in srgb, var(--color-accent) 85%, var(--color-background-inverse));
+ }
+
+ &:focus-visible {
+ outline: calc(var(--border-width-regular) * 2) solid var(--color-ring-primary);
+ outline-offset: var(--border-width-regular);
+ }
+ }
+}
+
+.notice {
+ padding: var(--spacing-sm) var(--spacing-md);
+ color: var(--color-text-info);
+ text-align: center;
+ font-style: italic;
+ background-color: var(--color-background-info);
+
+ &::before {
+ content: "ℹ️ ";
+ font-style: normal;
+ }
+}
+
+/* Server time fills remaining width for consistent E2E screenshot masking */
+.serverTime {
+ flex: 1;
+ min-width: 0;
+}
diff --git a/examples/basic-server-angular/src/mcp-app.ts b/examples/basic-server-angular/src/mcp-app.ts
new file mode 100644
index 000000000..6e0caa7a6
--- /dev/null
+++ b/examples/basic-server-angular/src/mcp-app.ts
@@ -0,0 +1,180 @@
+/**
+ * @file App that demonstrates a few features using MCP Apps SDK + Angular.
+ */
+import { ChangeDetectionStrategy, Component, DestroyRef, inject, model, provideZonelessChangeDetection, signal } from "@angular/core";
+import { FormsModule } from "@angular/forms";
+import { bootstrapApplication } from "@angular/platform-browser";
+import { App, PostMessageTransport, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+
+function extractTime(callToolResult: CallToolResult): string {
+ const { text } = callToolResult.content?.find((c) => c.type === "text")!;
+ return text;
+}
+
+@Component({
+ selector: "app-root",
+ imports: [FormsModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ styleUrl: "./mcp-app.css",
+ template: `
+ @if (error()) {
+