Skip to content

Commit b1705ea

Browse files
Add basic-server-svelte example
Demonstrates MCP App SDK usage with Svelte, providing parity with the existing basic-server-react and basic-server-vue examples. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1d00e60 commit b1705ea

File tree

12 files changed

+653
-5
lines changed

12 files changed

+653
-5
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Start with these foundational examples to learn the SDK:
5050
- [`examples/basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) — MCP server + MCP App using vanilla JS
5151
- [`examples/basic-server-react`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) — MCP server + MCP App using React
5252
- [`examples/basic-server-vue`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vue) — MCP server + MCP App using Vue
53+
- [`examples/basic-server-svelte`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-svelte) — MCP server + MCP App using Svelte
5354
- [`examples/basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) — MCP host application supporting MCP Apps
5455

5556
The [`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples) directory contains additional demo apps showcasing real-world use cases.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Example: Basic Server (Svelte)
2+
3+
An MCP App example with a Svelte 5 UI using runes for reactivity.
4+
5+
> [!TIP]
6+
> Looking for a vanilla JavaScript example? See [`basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs)!
7+
8+
## Overview
9+
10+
- Tool registration with a linked UI resource
11+
- Svelte 5 UI using the [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class
12+
- 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)
13+
14+
## Key Files
15+
16+
- [`server.ts`](server.ts) - MCP server with tool and resource registration
17+
- [`mcp-app.html`](mcp-app.html) / [`src/App.svelte`](src/App.svelte) - Svelte 5 UI using `App` class
18+
19+
## Getting Started
20+
21+
```bash
22+
npm install
23+
npm run dev
24+
```
25+
26+
## How It Works
27+
28+
1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`).
29+
2. When the tool is invoked, the Host renders the UI from the resource.
30+
3. The UI uses the MCP App SDK API to communicate with the host and call server tools.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Get Time App</title>
7+
<link rel="stylesheet" href="/src/global.css">
8+
</head>
9+
<body>
10+
<div id="app"></div>
11+
<script type="module" src="/src/mcp-app.ts"></script>
12+
</body>
13+
</html>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "basic-server-svelte",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "INPUT=mcp-app.html vite build",
8+
"watch": "INPUT=mcp-app.html vite build --watch",
9+
"serve": "bun server.ts",
10+
"start": "NODE_ENV=development npm run build && npm run serve",
11+
"dev": "NODE_ENV=development concurrently 'npm run watch' 'npm run serve'"
12+
},
13+
"dependencies": {
14+
"@modelcontextprotocol/ext-apps": "../..",
15+
"@modelcontextprotocol/sdk": "^1.22.0",
16+
"svelte": "^5.0.0",
17+
"zod": "^3.25.0"
18+
},
19+
"devDependencies": {
20+
"@sveltejs/vite-plugin-svelte": "^5.0.0",
21+
"@types/cors": "^2.8.19",
22+
"@types/express": "^5.0.0",
23+
"@types/node": "^22.0.0",
24+
"bun": "^1.3.2",
25+
"concurrently": "^9.2.1",
26+
"cors": "^2.8.5",
27+
"express": "^5.1.0",
28+
"typescript": "^5.9.3",
29+
"vite": "^6.0.0",
30+
"vite-plugin-singlefile": "^2.3.0"
31+
}
32+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3+
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
4+
import cors from "cors";
5+
import express, { type Request, type Response } from "express";
6+
import fs from "node:fs/promises";
7+
import path from "node:path";
8+
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
9+
10+
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
11+
const DIST_DIR = path.join(import.meta.dirname, "dist");
12+
13+
14+
const server = new McpServer({
15+
name: "Basic MCP App Server (Svelte)",
16+
version: "1.0.0",
17+
});
18+
19+
20+
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
21+
// resource (the UI it renders). The `_meta` field on the tool links to the
22+
// resource URI, telling hosts which UI to display when the tool executes.
23+
{
24+
const resourceUri = "ui://get-time/mcp-app.html";
25+
26+
server.registerTool(
27+
"get-time",
28+
{
29+
title: "Get Time",
30+
description: "Returns the current server time as an ISO 8601 string.",
31+
inputSchema: {},
32+
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
33+
},
34+
async (): Promise<CallToolResult> => {
35+
const time = new Date().toISOString();
36+
return {
37+
content: [{ type: "text", text: JSON.stringify({ time }) }],
38+
};
39+
},
40+
);
41+
42+
server.registerResource(
43+
resourceUri,
44+
resourceUri,
45+
{},
46+
async (): Promise<ReadResourceResult> => {
47+
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
48+
49+
return {
50+
contents: [
51+
// Per the MCP App specification, "text/html;profile=mcp-app" signals
52+
// to the Host that this resource is indeed for an MCP App UI.
53+
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
54+
],
55+
};
56+
},
57+
);
58+
}
59+
60+
61+
const app = express();
62+
app.use(cors());
63+
app.use(express.json());
64+
65+
app.post("/mcp", async (req: Request, res: Response) => {
66+
try {
67+
const transport = new StreamableHTTPServerTransport({
68+
sessionIdGenerator: undefined,
69+
enableJsonResponse: true,
70+
});
71+
res.on("close", () => { transport.close(); });
72+
73+
await server.connect(transport);
74+
75+
await transport.handleRequest(req, res, req.body);
76+
} catch (error) {
77+
console.error("Error handling MCP request:", error);
78+
if (!res.headersSent) {
79+
res.status(500).json({
80+
jsonrpc: "2.0",
81+
error: { code: -32603, message: "Internal server error" },
82+
id: null,
83+
});
84+
}
85+
}
86+
});
87+
88+
const httpServer = app.listen(PORT, (err) => {
89+
if (err) {
90+
console.error("Error starting server:", err);
91+
process.exit(1);
92+
}
93+
console.log(`Server listening on http://localhost:${PORT}/mcp`);
94+
});
95+
96+
function shutdown() {
97+
console.log("\nShutting down...");
98+
httpServer.close(() => {
99+
console.log("Server closed");
100+
process.exit(0);
101+
});
102+
}
103+
104+
process.on("SIGINT", shutdown);
105+
process.on("SIGTERM", shutdown);
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<script lang="ts">
2+
import { onMount } from "svelte";
3+
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
4+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
5+
6+
const log = {
7+
info: console.log.bind(console, "[APP]"),
8+
warn: console.warn.bind(console, "[APP]"),
9+
error: console.error.bind(console, "[APP]"),
10+
};
11+
12+
function extractTime(result: CallToolResult): string {
13+
const text = result.content!
14+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
15+
.map((c) => c.text)
16+
.join("");
17+
const { time } = JSON.parse(text) as { time: string };
18+
return time;
19+
}
20+
21+
let app = $state<App | null>(null);
22+
let serverTime = $state("Loading...");
23+
let messageText = $state("This is message text.");
24+
let logText = $state("This is log text.");
25+
let linkUrl = $state("https://modelcontextprotocol.io/");
26+
27+
onMount(async () => {
28+
const instance = new App({ name: "Get Time App", version: "1.0.0" });
29+
30+
instance.ontoolinput = (params) => {
31+
log.info("Received tool call input:", params);
32+
};
33+
34+
instance.ontoolresult = (result) => {
35+
log.info("Received tool call result:", result);
36+
serverTime = extractTime(result);
37+
};
38+
39+
instance.onerror = log.error;
40+
41+
await instance.connect(new PostMessageTransport(window.parent));
42+
app = instance;
43+
});
44+
45+
async function handleGetTime() {
46+
if (!app) return;
47+
try {
48+
log.info("Calling get-time tool...");
49+
const result = await app.callServerTool({ name: "get-time", arguments: {} });
50+
log.info("get-time result:", result);
51+
serverTime = extractTime(result);
52+
} catch (e) {
53+
log.error(e);
54+
serverTime = "[ERROR]";
55+
}
56+
}
57+
58+
async function handleSendMessage() {
59+
if (!app) return;
60+
const signal = AbortSignal.timeout(5000);
61+
try {
62+
log.info("Sending message text to Host:", messageText);
63+
const { isError } = await app.sendMessage(
64+
{ role: "user", content: [{ type: "text", text: messageText }] },
65+
{ signal },
66+
);
67+
log.info("Message", isError ? "rejected" : "accepted");
68+
} catch (e) {
69+
log.error("Message send error:", signal.aborted ? "timed out" : e);
70+
}
71+
}
72+
73+
async function handleSendLog() {
74+
if (!app) return;
75+
log.info("Sending log text to Host:", logText);
76+
await app.sendLog({ level: "info", data: logText });
77+
}
78+
79+
async function handleOpenLink() {
80+
if (!app) return;
81+
log.info("Sending open link request to Host:", linkUrl);
82+
const { isError } = await app.sendOpenLink({ url: linkUrl });
83+
log.info("Open link request", isError ? "rejected" : "accepted");
84+
}
85+
</script>
86+
87+
<main class="main">
88+
<p class="notice">Watch activity in the DevTools console!</p>
89+
90+
<div class="action">
91+
<p><strong>Server Time:</strong> <code>{serverTime}</code></p>
92+
<button onclick={handleGetTime}>Get Server Time</button>
93+
</div>
94+
95+
<div class="action">
96+
<textarea bind:value={messageText}></textarea>
97+
<button onclick={handleSendMessage}>Send Message</button>
98+
</div>
99+
100+
<div class="action">
101+
<input type="text" bind:value={logText}>
102+
<button onclick={handleSendLog}>Send Log</button>
103+
</div>
104+
105+
<div class="action">
106+
<input type="url" bind:value={linkUrl}>
107+
<button onclick={handleOpenLink}>Open Link</button>
108+
</div>
109+
</main>
110+
111+
<style>
112+
.main {
113+
--color-primary: #2563eb;
114+
--color-primary-hover: #1d4ed8;
115+
--color-notice-bg: #eff6ff;
116+
117+
min-width: 425px;
118+
119+
> * {
120+
margin-top: 0;
121+
margin-bottom: 0;
122+
}
123+
124+
> * + * {
125+
margin-top: 1.5rem;
126+
}
127+
}
128+
129+
.action {
130+
> * {
131+
margin-top: 0;
132+
margin-bottom: 0;
133+
width: 100%;
134+
}
135+
136+
> * + * {
137+
margin-top: 0.5rem;
138+
}
139+
140+
textarea,
141+
input {
142+
font-family: inherit;
143+
font-size: inherit;
144+
}
145+
146+
button {
147+
padding: 0.5rem 1rem;
148+
border: none;
149+
border-radius: 6px;
150+
color: white;
151+
font-weight: bold;
152+
background-color: var(--color-primary);
153+
cursor: pointer;
154+
155+
&:hover,
156+
&:focus-visible {
157+
background-color: var(--color-primary-hover);
158+
}
159+
}
160+
}
161+
162+
.notice {
163+
padding: 0.5rem 0.75rem;
164+
color: var(--color-primary);
165+
text-align: center;
166+
font-style: italic;
167+
background-color: var(--color-notice-bg);
168+
169+
&::before {
170+
content: "ℹ️ ";
171+
font-style: normal;
172+
}
173+
}
174+
</style>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
* {
2+
box-sizing: border-box;
3+
}
4+
5+
html, body {
6+
font-family: system-ui, -apple-system, sans-serif;
7+
font-size: 1rem;
8+
}
9+
10+
code {
11+
font-size: 1em;
12+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { mount } from "svelte";
2+
import App from "./App.svelte";
3+
import "./global.css";
4+
5+
mount(App, { target: document.getElementById("app")! });
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
2+
3+
export default {
4+
preprocess: vitePreprocess(),
5+
};

0 commit comments

Comments
 (0)