Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion examples/basic-host/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@ sandboxApp.get(["/", "/sandbox.html"], (req, res) => {
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", "0");

res.sendFile(join(DIRECTORY, "sandbox.html"));
// Pass the file relative to `root` — with an absolute path, express applies
// its dotfiles="ignore" rule to every segment and 404s when the repo checkout
// lives under a dot-directory (e.g. git worktrees in .claude/worktrees).
res.sendFile("sandbox.html", { root: DIRECTORY });
});

sandboxApp.use((_req, res) => {
Expand Down
2 changes: 2 additions & 0 deletions examples/dynamic-content-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
66 changes: 66 additions & 0 deletions examples/dynamic-content-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Example: Dynamic View Content

An MCP App example demonstrating **Dynamic View Content**: a generic, predeclared renderer view driven by typed payloads that tools return as embedded resources marked with `_meta.ui.content`.

Instead of baking presentation into the template and passing data via `structuredContent`, the server generates a declarative UI document at tool-call time. The host forwards the payload (unmodified, and excluded from model context) to the renderer view, which interprets it. Button clicks in the rendered surface are bridged back into `tools/call` requests to an app-visibility tool, whose responses carry new payloads — closing the interactive loop.

This is the pattern used by generative UI formats such as [A2UI](https://a2ui.org) (`application/a2ui+json`). This example uses a deliberately tiny stand-in format (`application/vnd.example.dynamic-ui+json`, defined in [`dynamic-ui.ts`](dynamic-ui.ts)) so the plumbing stays easy to follow.

## MCP Client Configuration

Add to your MCP client configuration (stdio transport):

```json
{
"mcpServers": {
"dynamic-content": {
"command": "npx",
"args": [
"-y",
"--silent",
"--registry=https://registry.npmjs.org/",
"@modelcontextprotocol/server-dynamic-content",
"--stdio"
]
}
}
}
```

### Local Development

To test local modifications, use this configuration (replace `~/code/ext-apps` with your clone path):

```json
{
"mcpServers": {
"dynamic-content": {
"command": "bash",
"args": [
"-c",
"cd ~/code/ext-apps/examples/dynamic-content-server && npm run build >&2 && node dist/index.js --stdio"
]
}
}
}
```

## Overview

- A renderer resource declaring the payload types it renders via `contentMimeTypes` in its `_meta.ui`
- A model-visible tool (`search-flights`) returning a text fallback for model context plus a marked payload created with [`createViewContentBlock`](https://apps.extensions.modelcontextprotocol.io/api/functions/app.createViewContentBlock.html)
- An app-visibility tool (`select-flight`, hidden from the model) that the renderer's event bridge calls; its response carries the next payload
- A generic renderer extracting payloads with [`getViewContentBlocks`](https://apps.extensions.modelcontextprotocol.io/api/functions/app.getViewContentBlocks.html) and building DOM with `createElement`/`textContent` only — payloads are untrusted input

## Key Files

- [`dynamic-ui.ts`](dynamic-ui.ts) - The example payload format (MIME type + component types)
- [`server.ts`](server.ts) - Renderer resource + tools returning marked payloads
- [`mcp-app.html`](mcp-app.html) / [`src/mcp-app.ts`](src/mcp-app.ts) - The generic renderer view and its event bridge

## Getting Started

```bash
npm install
npm run dev
```
47 changes: 47 additions & 0 deletions examples/dynamic-content-server/dynamic-ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @file Shared definition of this example's dynamic content payload format.
*
* This is a deliberately tiny declarative UI language, standing in for real
* generative UI formats such as A2UI (`application/a2ui+json`). The server
* produces documents in this format at tool-call time; the generic renderer
* view interprets them. The MCP Apps plumbing is identical for any format:
* the payload rides in tool results as an embedded resource marked with
* `_meta.ui.content`, typed by its MIME type.
*/

/** MIME type of this example's dynamic content payloads. */
export const DYNAMIC_UI_MIME_TYPE = "application/vnd.example.dynamic-ui+json";

/** A node in the declarative component tree. */
export type UiComponent =
| {
kind: "text";
text: string;
variant?: "title" | "body" | "note";
}
| {
kind: "row" | "column";
children: UiComponent[];
}
| {
kind: "card";
children: UiComponent[];
}
| {
kind: "button";
label: string;
/**
* The event bridge: the renderer translates a click into a `tools/call`
* request for this (typically app-visibility) tool. The response carries
* new marked payloads, which the renderer applies.
*/
action: {
tool: string;
arguments: Record<string, unknown>;
};
};

/** A dynamic content payload document: a surface to render. */
export type UiSurface = {
surface: UiComponent[];
};
93 changes: 93 additions & 0 deletions examples/dynamic-content-server/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Entry point for running the MCP server.
* Run with: npx mcp-server-dynamic-content
* Or: node dist/index.js [--stdio]
*/

import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.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<void> {
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<void> {
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);
});
17 changes: 17 additions & 0 deletions examples/dynamic-content-server/mcp-app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<title>Dynamic Content Renderer</title>
</head>
<body>
<main class="main">
<div id="surface" class="surface">
<p class="status">Waiting for content…</p>
</div>
</main>
<script type="module" src="/src/mcp-app.ts"></script>
</body>
</html>
53 changes: 53 additions & 0 deletions examples/dynamic-content-server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@modelcontextprotocol/server-dynamic-content",
"version": "1.7.4",
"type": "module",
"description": "MCP App Server example demonstrating Dynamic View Content: a generic renderer view driven by typed payloads returned from tool calls as marked embedded resources",
"repository": {
"type": "git",
"url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/dynamic-content-server"
},
"license": "MIT",
"main": "dist/server.js",
"files": [
"dist"
],
"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",
"serve:stdio": "bun main.ts --stdio",
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
"start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio",
"dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"",
"prepublishOnly": "npm run build"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.7.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
"@types/node": "22.10.0",
"concurrently": "^9.2.1",
"cross-env": "^10.1.0",
"typescript": "^5.9.3",
"vite": "^6.0.0",
"vite-plugin-singlefile": "^2.3.0"
},
"types": "dist/server.d.ts",
"exports": {
".": {
"types": "./dist/server.d.ts",
"default": "./dist/server.js"
}
},
"bin": {
"mcp-server-dynamic-content": "dist/index.js"
}
}
Loading
Loading