Skip to content

Commit b8832f0

Browse files
Add basic-server-solid example
Demonstrates MCP App SDK usage with Solid.js, providing another reactive framework option alongside the existing React, Vue, Svelte, and Preact examples. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4f75e91 commit b8832f0

File tree

15 files changed

+740
-0
lines changed

15 files changed

+740
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Start with these foundational examples to learn the SDK:
5252
- [`examples/basic-server-vue`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vue) — MCP server + MCP App using Vue
5353
- [`examples/basic-server-svelte`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-svelte) — MCP server + MCP App using Svelte
5454
- [`examples/basic-server-preact`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-preact) — MCP server + MCP App using Preact
55+
- [`examples/basic-server-solid`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-solid) — MCP server + MCP App using Solid
5556
- [`examples/basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) — MCP host application supporting MCP Apps
5657

5758
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 (Solid)
2+
3+
An MCP App example with a Solid UI.
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+
- Solid 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), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink)
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/mcp-app.tsx`](src/mcp-app.tsx) - Solid 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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+
<meta name="color-scheme" content="light dark">
7+
<title>Get Time App</title>
8+
<link rel="stylesheet" href="/src/global.css">
9+
</head>
10+
<body>
11+
<div id="root"></div>
12+
<script type="module" src="/src/mcp-app.tsx"></script>
13+
</body>
14+
</html>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "basic-server-solid",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build",
8+
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
9+
"serve": "bun server.ts",
10+
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
11+
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'"
12+
},
13+
"dependencies": {
14+
"@modelcontextprotocol/ext-apps": "../..",
15+
"@modelcontextprotocol/sdk": "^1.24.0",
16+
"solid-js": "^1.9.0",
17+
"zod": "^4.1.13"
18+
},
19+
"devDependencies": {
20+
"@types/cors": "^2.8.19",
21+
"@types/express": "^5.0.0",
22+
"@types/node": "^22.0.0",
23+
"concurrently": "^9.2.1",
24+
"cors": "^2.8.5",
25+
"cross-env": "^7.0.3",
26+
"express": "^5.1.0",
27+
"typescript": "^5.9.3",
28+
"vite": "^6.0.0",
29+
"vite-plugin-singlefile": "^2.3.0",
30+
"vite-plugin-solid": "^2.0.0"
31+
}
32+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
3+
import fs from "node:fs/promises";
4+
import path from "node:path";
5+
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server";
6+
import { startServer } from "./src/server-utils.js";
7+
8+
const DIST_DIR = path.join(import.meta.dirname, "dist");
9+
10+
/**
11+
* Creates a new MCP server instance with tools and resources registered.
12+
*/
13+
function createServer(): McpServer {
14+
const server = new McpServer({
15+
name: "Basic MCP App Server (Solid)",
16+
version: "1.0.0",
17+
});
18+
19+
// Two-part registration: tool + resource, tied together by the resource URI.
20+
const resourceUri = "ui://get-time/mcp-app.html";
21+
22+
// Register a tool with UI metadata. When the host calls this tool, it reads
23+
// `_meta[RESOURCE_URI_META_KEY]` to know which resource to fetch and render
24+
// as an interactive UI.
25+
registerAppTool(server,
26+
"get-time",
27+
{
28+
title: "Get Time",
29+
description: "Returns the current server time as an ISO 8601 string.",
30+
inputSchema: {},
31+
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
32+
},
33+
async (): Promise<CallToolResult> => {
34+
const time = new Date().toISOString();
35+
return { content: [{ type: "text", text: time }] };
36+
},
37+
);
38+
39+
// Register the resource, which returns the bundled HTML/JavaScript for the UI.
40+
registerAppResource(server,
41+
resourceUri,
42+
resourceUri,
43+
{ mimeType: RESOURCE_MIME_TYPE },
44+
async (): Promise<ReadResourceResult> => {
45+
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
46+
47+
return {
48+
contents: [
49+
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
50+
],
51+
};
52+
},
53+
);
54+
55+
return server;
56+
}
57+
58+
startServer(createServer);
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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
.main {
2+
--color-primary: #2563eb;
3+
--color-primary-hover: #1d4ed8;
4+
--color-notice-bg: #eff6ff;
5+
6+
width: 100%;
7+
max-width: 425px;
8+
box-sizing: border-box;
9+
10+
> * {
11+
margin-top: 0;
12+
margin-bottom: 0;
13+
}
14+
15+
> * + * {
16+
margin-top: 1.5rem;
17+
}
18+
}
19+
20+
.action {
21+
> * {
22+
margin-top: 0;
23+
margin-bottom: 0;
24+
width: 100%;
25+
}
26+
27+
> * + * {
28+
margin-top: 0.5rem;
29+
}
30+
31+
/* Consistent font for form inputs (inherits from global.css) */
32+
textarea,
33+
input {
34+
font-family: inherit;
35+
font-size: inherit;
36+
}
37+
38+
button {
39+
padding: 0.5rem 1rem;
40+
border: none;
41+
border-radius: 6px;
42+
color: white;
43+
font-weight: bold;
44+
background-color: var(--color-primary);
45+
cursor: pointer;
46+
47+
&:hover,
48+
&:focus-visible {
49+
background-color: var(--color-primary-hover);
50+
}
51+
}
52+
}
53+
54+
.notice {
55+
padding: 0.5rem 0.75rem;
56+
color: var(--color-primary);
57+
text-align: center;
58+
font-style: italic;
59+
background-color: var(--color-notice-bg);
60+
61+
&::before {
62+
content: "ℹ️ ";
63+
font-style: normal;
64+
}
65+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* @file App that demonstrates a few features using MCP Apps SDK + Solid.
3+
*/
4+
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
5+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
6+
import { createEffect, createSignal, onMount, Show } from "solid-js";
7+
import { render } from "solid-js/web";
8+
import styles from "./mcp-app.module.css";
9+
10+
11+
const IMPLEMENTATION = { name: "Get Time App", version: "1.0.0" };
12+
13+
14+
const log = {
15+
info: console.log.bind(console, "[APP]"),
16+
warn: console.warn.bind(console, "[APP]"),
17+
error: console.error.bind(console, "[APP]"),
18+
};
19+
20+
21+
function extractTime(callToolResult: CallToolResult): string {
22+
const { text } = callToolResult.content?.find((c) => c.type === "text")!;
23+
return text;
24+
}
25+
26+
27+
function GetTimeApp() {
28+
const [app, setApp] = createSignal<App | null>(null);
29+
const [error, setError] = createSignal<Error | null>(null);
30+
const [toolResult, setToolResult] = createSignal<CallToolResult | null>(null);
31+
32+
onMount(async () => {
33+
const instance = new App(IMPLEMENTATION);
34+
35+
instance.ontoolinput = async (input) => {
36+
log.info("Received tool call input:", input);
37+
};
38+
39+
instance.ontoolresult = async (result) => {
40+
log.info("Received tool call result:", result);
41+
setToolResult(result);
42+
};
43+
44+
instance.onerror = log.error;
45+
46+
try {
47+
await instance.connect(new PostMessageTransport(window.parent));
48+
setApp(instance);
49+
} catch (e) {
50+
setError(e as Error);
51+
}
52+
});
53+
54+
return (
55+
<Show when={!error()} fallback={<div><strong>ERROR:</strong> {error()!.message}</div>}>
56+
<Show when={app()} fallback={<div>Connecting...</div>}>
57+
<GetTimeAppInner app={app()!} toolResult={toolResult()} />
58+
</Show>
59+
</Show>
60+
);
61+
}
62+
63+
64+
interface GetTimeAppInnerProps {
65+
app: App;
66+
toolResult: CallToolResult | null;
67+
}
68+
function GetTimeAppInner(props: GetTimeAppInnerProps) {
69+
const [serverTime, setServerTime] = createSignal("Loading...");
70+
const [messageText, setMessageText] = createSignal("This is message text.");
71+
const [logText, setLogText] = createSignal("This is log text.");
72+
const [linkUrl, setLinkUrl] = createSignal("https://modelcontextprotocol.io/");
73+
74+
// Update serverTime when toolResult changes
75+
createEffect(() => {
76+
if (props.toolResult) {
77+
setServerTime(extractTime(props.toolResult));
78+
}
79+
});
80+
81+
async function handleGetTime() {
82+
try {
83+
log.info("Calling get-time tool...");
84+
const result = await props.app.callServerTool({ name: "get-time", arguments: {} });
85+
log.info("get-time result:", result);
86+
setServerTime(extractTime(result));
87+
} catch (e) {
88+
log.error(e);
89+
setServerTime("[ERROR]");
90+
}
91+
}
92+
93+
async function handleSendMessage() {
94+
const signal = AbortSignal.timeout(5000);
95+
try {
96+
log.info("Sending message text to Host:", messageText());
97+
const { isError } = await props.app.sendMessage(
98+
{ role: "user", content: [{ type: "text", text: messageText() }] },
99+
{ signal },
100+
);
101+
log.info("Message", isError ? "rejected" : "accepted");
102+
} catch (e) {
103+
log.error("Message send error:", signal.aborted ? "timed out" : e);
104+
}
105+
}
106+
107+
async function handleSendLog() {
108+
log.info("Sending log text to Host:", logText());
109+
await props.app.sendLog({ level: "info", data: logText() });
110+
}
111+
112+
async function handleOpenLink() {
113+
log.info("Sending open link request to Host:", linkUrl());
114+
const { isError } = await props.app.openLink({ url: linkUrl() });
115+
log.info("Open link request", isError ? "rejected" : "accepted");
116+
}
117+
118+
return (
119+
<main class={styles.main}>
120+
<p class={styles.notice}>Watch activity in the DevTools console!</p>
121+
122+
<div class={styles.action}>
123+
<p>
124+
<strong>Server Time:</strong> <code id="server-time">{serverTime()}</code>
125+
</p>
126+
<button onClick={handleGetTime}>Get Server Time</button>
127+
</div>
128+
129+
<div class={styles.action}>
130+
<textarea value={messageText()} onInput={(e) => setMessageText(e.currentTarget.value)} />
131+
<button onClick={handleSendMessage}>Send Message</button>
132+
</div>
133+
134+
<div class={styles.action}>
135+
<input type="text" value={logText()} onInput={(e) => setLogText(e.currentTarget.value)} />
136+
<button onClick={handleSendLog}>Send Log</button>
137+
</div>
138+
139+
<div class={styles.action}>
140+
<input type="url" value={linkUrl()} onInput={(e) => setLinkUrl(e.currentTarget.value)} />
141+
<button onClick={handleOpenLink}>Open Link</button>
142+
</div>
143+
</main>
144+
);
145+
}
146+
147+
148+
render(() => <GetTimeApp />, document.getElementById("root")!);

0 commit comments

Comments
 (0)