Skip to content
Open
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
6 changes: 2 additions & 4 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
examples/basic-host/**/*.ts
examples/basic-host/**/*.tsx
examples/basic-server-react/**/*.ts
examples/basic-server-react/**/*.tsx
examples/basic-server-vanillajs/**/*.ts
examples/basic-server-vanillajs/**/*.tsx
examples/basic-server-*/**/*.ts
examples/basic-server-*/**/*.tsx
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@ Or edit your `package.json` manually:

Start with these foundational examples to learn the SDK:

- [`examples/basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) — Example MCP server with tools that return UI Apps (vanilla JS)
- [`examples/basic-server-react`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) — Example MCP server with tools that return UI Apps (React)
- [`examples/basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) — Bare-bones example of hosting MCP Apps
- [`examples/basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) — MCP server + MCP App using vanilla JS
- [`examples/basic-server-react`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) — MCP server + MCP App using [React](https://github.com/facebook/react)
- [`examples/basic-server-vue`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vue) — MCP server + MCP App using [Vue](https://github.com/vuejs/vue)
- [`examples/basic-server-svelte`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-svelte) — MCP server + MCP App using [Svelte](https://github.com/sveltejs/svelte)
- [`examples/basic-server-preact`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-preact) — MCP server + MCP App using [Preact](https://github.com/preactjs/preact)
- [`examples/basic-server-solid`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-solid) — MCP server + MCP App using [Solid](https://github.com/solidjs/solid)
- [`examples/basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) — MCP host application supporting MCP Apps

The [`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples) directory contains additional demo apps showcasing real-world use cases.

Expand Down
30 changes: 30 additions & 0 deletions examples/basic-server-preact/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Example: Basic Server (Preact)

An MCP App example with a Preact UI.

> [!TIP]
> Looking for a vanilla JavaScript example? See [`basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs)!

## Overview

- Tool registration with a linked UI resource
- Preact UI using the [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class
- 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), [`sendOpenLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendopenlink)

## Key Files

- [`server.ts`](server.ts) - MCP server with tool and resource registration
- [`mcp-app.html`](mcp-app.html) / [`src/mcp-app.tsx`](src/mcp-app.tsx) - Preact UI using `App` class

## 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.
13 changes: 13 additions & 0 deletions examples/basic-server-preact/mcp-app.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Get Time App</title>
<link rel="stylesheet" href="/src/global.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/mcp-app.tsx"></script>
</body>
</html>
32 changes: 32 additions & 0 deletions examples/basic-server-preact/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "basic-server-preact",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "INPUT=mcp-app.html vite build",
"watch": "INPUT=mcp-app.html vite build --watch",
"serve": "bun server.ts",
"start": "NODE_ENV=development npm run build && npm run serve",
"dev": "NODE_ENV=development concurrently 'npm run watch' 'npm run serve'"
},
"dependencies": {
"@modelcontextprotocol/ext-apps": "../..",
"@modelcontextprotocol/sdk": "^1.22.0",
"preact": "^10.0.0",
"zod": "^3.25.0"
},
"devDependencies": {
"@preact/preset-vite": "^2.0.0",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
"@types/node": "^22.0.0",
"bun": "^1.3.2",
"concurrently": "^9.2.1",
"cors": "^2.8.5",
"express": "^5.1.0",
"typescript": "^5.9.3",
"vite": "^6.0.0",
"vite-plugin-singlefile": "^2.3.0"
}
}
105 changes: 105 additions & 0 deletions examples/basic-server-preact/server.ts
Copy link
Collaborator

Choose a reason for hiding this comment

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

These need a createServer refactoring similar to #115

(McpServer instances can't be shared between sessions)

Copy link
Collaborator

Choose a reason for hiding this comment

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

😀

Copy link
Member Author

Choose a reason for hiding this comment

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

Ideally, I would like to keep these examples as simple and self-contained as basic-server-react in #157. Once we decide what to do with that PR, I'll update these example to match.

Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import cors from "cors";
import express, { type Request, type Response } from "express";
import fs from "node:fs/promises";
import path from "node:path";
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";

const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
const DIST_DIR = path.join(import.meta.dirname, "dist");


const server = new McpServer({
name: "Basic MCP App Server (Preact)",
version: "1.0.0",
});


// MCP Apps require two-part registration: a tool (what the LLM calls) and a
// resource (the UI it renders). The `_meta` field on the tool links to the
// resource URI, telling hosts which UI to display when the tool executes.
{
const resourceUri = "ui://get-time/mcp-app.html";

server.registerTool(
"get-time",
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
inputSchema: {},
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
},
async (): Promise<CallToolResult> => {
const time = new Date().toISOString();
return {
content: [{ type: "text", text: JSON.stringify({ time }) }],
};
},
);

server.registerResource(
resourceUri,
resourceUri,
{},
async (): Promise<ReadResourceResult> => {
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");

return {
contents: [
// Per the MCP App specification, "text/html;profile=mcp-app" signals
// to the Host that this resource is indeed for an MCP App UI.
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
],
};
},
);
}


const app = express();
app.use(cors());
app.use(express.json());

app.post("/mcp", async (req: Request, res: Response) => {
try {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
});
res.on("close", () => { transport.close(); });

await server.connect(transport);

await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("Error handling MCP request:", 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("Error starting server:", err);
process.exit(1);
}
console.log(`Server listening on http://localhost:${PORT}/mcp`);
});

function shutdown() {
console.log("\nShutting down...");
httpServer.close(() => {
console.log("Server closed");
process.exit(0);
});
}

process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
12 changes: 12 additions & 0 deletions examples/basic-server-preact/src/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
* {
box-sizing: border-box;
}

html, body {
font-family: system-ui, -apple-system, sans-serif;
font-size: 1rem;
}

code {
font-size: 1em;
}
63 changes: 63 additions & 0 deletions examples/basic-server-preact/src/mcp-app.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
.main {
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-notice-bg: #eff6ff;

min-width: 425px;

> * {
margin-top: 0;
margin-bottom: 0;
}

> * + * {
margin-top: 1.5rem;
}
}

.action {
> * {
margin-top: 0;
margin-bottom: 0;
width: 100%;
}

> * + * {
margin-top: 0.5rem;
}

/* Consistent font for form inputs (inherits from global.css) */
textarea,
input {
font-family: inherit;
font-size: inherit;
}

button {
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
color: white;
font-weight: bold;
background-color: var(--color-primary);
cursor: pointer;

&:hover,
&:focus-visible {
background-color: var(--color-primary-hover);
}
}
}

.notice {
padding: 0.5rem 0.75rem;
color: var(--color-primary);
text-align: center;
font-style: italic;
background-color: var(--color-notice-bg);

&::before {
content: "ℹ️ ";
font-style: normal;
}
}
Loading
Loading