Skip to content

Commit 66d6e5e

Browse files
committed
feat: add inlined-app-server example
Add a minimal MCP App Server example with the UI HTML inlined directly in the server code. No build step required. - server.ts: MCP server with inlined HTML UI and tool registration - server-utils.ts: HTTP/stdio transport utilities - Added to CI publish workflows
1 parent 081dad8 commit 66d6e5e

7 files changed

Lines changed: 378 additions & 0 deletions

File tree

.github/workflows/npm-publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ jobs:
107107
- budget-allocator-server
108108
- cohort-heatmap-server
109109
- customer-segmentation-server
110+
- inlined-app-server
110111
- scenario-modeler-server
111112
- sheet-music-server
112113
- system-monitor-server

.github/workflows/publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ jobs:
2929
./examples/budget-allocator-server \
3030
./examples/cohort-heatmap-server \
3131
./examples/customer-segmentation-server \
32+
./examples/inlined-app-server \
3233
./examples/scenario-modeler-server \
3334
./examples/system-monitor-server \
3435
./examples/threejs-server \
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Example: Inlined Server
2+
3+
A minimal MCP App Server example with the UI HTML inlined directly in the server code.
4+
5+
## Overview
6+
7+
This example demonstrates the simplest possible MCP App setup:
8+
9+
- Single-file server with inlined HTML UI
10+
- No build step required (directly import the `@modelcontextprotocol/ext-apps` package from unpkg.com)
11+
- Tool registration with linked UI resource
12+
13+
## Key Files
14+
15+
- [`server.ts`](server.ts) - MCP server with inlined HTML UI
16+
- [`server-utils.ts`](server-utils.ts) - HTTP/stdio transport utilities
17+
18+
## Getting Started
19+
20+
```bash
21+
npm install
22+
npm start
23+
```
24+
25+
The server will start on `http://localhost:3001/mcp`.
26+
27+
## How It Works
28+
29+
1. The server defines the UI HTML as a template string directly in the code
30+
2. A resource handler returns this HTML when the UI resource is requested
31+
3. A tool is registered that links to this UI resource via `_meta.ui.resourceUri`
32+
4. When the tool is invoked, the Host fetches and renders the inlined UI
33+
34+
## Use Cases
35+
36+
This pattern is ideal for:
37+
38+
- Quick prototyping
39+
- Simple tools with minimal UI
40+
- Embedding servers in other applications
41+
- Testing and development
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "@modelcontextprotocol/server-inlined-app",
3+
"version": "0.1.0",
4+
"type": "module",
5+
"description": "Minimal MCP App Server example with inlined HTML UI",
6+
"repository": {
7+
"type": "git",
8+
"url": "https://github.com/modelcontextprotocol/ext-apps",
9+
"directory": "examples/inlined-app-server"
10+
},
11+
"license": "MIT",
12+
"main": "server.ts",
13+
"files": [
14+
"server.ts",
15+
"server-utils.ts"
16+
],
17+
"scripts": {
18+
"build": "tsc --noEmit",
19+
"serve": "bun server.ts",
20+
"start": "npm run serve"
21+
},
22+
"dependencies": {
23+
"@modelcontextprotocol/ext-apps": "^0.3.1",
24+
"@modelcontextprotocol/sdk": "^1.24.0",
25+
"zod": "^4.1.13"
26+
},
27+
"devDependencies": {
28+
"@types/cors": "^2.8.19",
29+
"@types/express": "^5.0.0",
30+
"@types/node": "^22.0.0",
31+
"cors": "^2.8.5",
32+
"express": "^5.1.0",
33+
"typescript": "^5.9.3"
34+
}
35+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* Shared utilities for running MCP servers with various transports.
3+
*
4+
* Supports:
5+
* - Stdio transport (--stdio flag)
6+
* - Streamable HTTP transport (/mcp) - stateless mode
7+
* - Legacy SSE transport (/sse, /messages) - for older clients (e.g., Kotlin SDK)
8+
*/
9+
10+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
11+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
13+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
15+
import cors from "cors";
16+
import type { Request, Response } from "express";
17+
18+
/** Active SSE sessions: sessionId -> { server, transport } */
19+
const sseSessions = new Map<
20+
string,
21+
{ server: McpServer; transport: SSEServerTransport }
22+
>();
23+
24+
/**
25+
* Starts an MCP server using the appropriate transport based on command-line arguments.
26+
*
27+
* If `--stdio` is passed, uses stdio transport. Otherwise, uses HTTP transports.
28+
*
29+
* @param createServer - Factory function that creates a new McpServer instance.
30+
*/
31+
export async function startServer(
32+
createServer: () => McpServer,
33+
): Promise<void> {
34+
try {
35+
if (process.argv.includes("--stdio")) {
36+
await startStdioServer(createServer);
37+
} else {
38+
await startHttpServer(createServer);
39+
}
40+
} catch (e) {
41+
console.error(e);
42+
process.exit(1);
43+
}
44+
}
45+
46+
/**
47+
* Starts an MCP server with stdio transport.
48+
*
49+
* @param createServer - Factory function that creates a new McpServer instance.
50+
*/
51+
export async function startStdioServer(
52+
createServer: () => McpServer,
53+
): Promise<void> {
54+
await createServer().connect(new StdioServerTransport());
55+
}
56+
57+
/**
58+
* Starts an MCP server with HTTP transports (Streamable HTTP + legacy SSE).
59+
*
60+
* Provides:
61+
* - /mcp (GET/POST/DELETE): Streamable HTTP transport (stateless mode)
62+
* - /sse (GET) + /messages (POST): Legacy SSE transport for older clients
63+
*
64+
* @param createServer - Factory function that creates a new McpServer instance.
65+
*/
66+
export async function startHttpServer(
67+
createServer: () => McpServer,
68+
): Promise<void> {
69+
const port = parseInt(process.env.PORT ?? "3001", 10);
70+
71+
// Express app - bind to all interfaces for development/testing
72+
const expressApp = createMcpExpressApp({ host: "0.0.0.0" });
73+
expressApp.use(cors());
74+
75+
// Streamable HTTP transport (stateless mode)
76+
expressApp.all("/mcp", async (req: Request, res: Response) => {
77+
// Create fresh server and transport for each request (stateless mode)
78+
const server = createServer();
79+
const transport = new StreamableHTTPServerTransport({
80+
sessionIdGenerator: undefined,
81+
});
82+
83+
// Clean up when response ends
84+
res.on("close", () => {
85+
transport.close().catch(() => {});
86+
server.close().catch(() => {});
87+
});
88+
89+
try {
90+
await server.connect(transport);
91+
await transport.handleRequest(req, res, req.body);
92+
} catch (error) {
93+
console.error("MCP error:", error);
94+
if (!res.headersSent) {
95+
res.status(500).json({
96+
jsonrpc: "2.0",
97+
error: { code: -32603, message: "Internal server error" },
98+
id: null,
99+
});
100+
}
101+
}
102+
});
103+
104+
// Legacy SSE transport - stream endpoint
105+
expressApp.get("/sse", async (_req: Request, res: Response) => {
106+
try {
107+
const server = createServer();
108+
const transport = new SSEServerTransport("/messages", res);
109+
sseSessions.set(transport.sessionId, { server, transport });
110+
111+
res.on("close", () => {
112+
sseSessions.delete(transport.sessionId);
113+
transport.close().catch(() => {});
114+
server.close().catch(() => {});
115+
});
116+
117+
await server.connect(transport);
118+
} catch (error) {
119+
console.error("SSE error:", error);
120+
if (!res.headersSent) res.status(500).end();
121+
}
122+
});
123+
124+
// Legacy SSE transport - message endpoint
125+
expressApp.post("/messages", async (req: Request, res: Response) => {
126+
try {
127+
const sessionId = req.query.sessionId as string;
128+
const session = sseSessions.get(sessionId);
129+
130+
if (!session) {
131+
return res.status(404).json({
132+
jsonrpc: "2.0",
133+
error: { code: -32001, message: "Session not found" },
134+
id: null,
135+
});
136+
}
137+
138+
await session.transport.handlePostMessage(req, res, req.body);
139+
} catch (error) {
140+
console.error("Message error:", error);
141+
if (!res.headersSent) {
142+
res.status(500).json({
143+
jsonrpc: "2.0",
144+
error: { code: -32603, message: "Internal server error" },
145+
id: null,
146+
});
147+
}
148+
}
149+
});
150+
151+
const { promise, resolve, reject } = Promise.withResolvers<void>();
152+
153+
const httpServer = expressApp.listen(port, (err?: Error) => {
154+
if (err) return reject(err);
155+
console.log(`Server listening on http://localhost:${port}/mcp`);
156+
console.log(` SSE endpoint: http://localhost:${port}/sse`);
157+
resolve();
158+
});
159+
160+
const shutdown = () => {
161+
console.log("\nShutting down...");
162+
// Clean up all SSE sessions
163+
sseSessions.forEach(({ server, transport }) => {
164+
transport.close().catch(() => {});
165+
server.close().catch(() => {});
166+
});
167+
sseSessions.clear();
168+
httpServer.close(() => process.exit(0));
169+
};
170+
171+
process.on("SIGINT", shutdown);
172+
process.on("SIGTERM", shutdown);
173+
174+
return promise;
175+
}
176+
177+
/**
178+
* @deprecated Use startHttpServer instead
179+
*/
180+
export const startStreamableHttpServer = startHttpServer;
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import {
2+
RESOURCE_MIME_TYPE,
3+
registerAppResource,
4+
registerAppTool,
5+
} from "@modelcontextprotocol/ext-apps/server";
6+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7+
import { z } from "zod";
8+
import { startServer } from "./server-utils.js";
9+
10+
export function createServer(): McpServer {
11+
const server = new McpServer({ name: "Example Server", version: "1.0.0" });
12+
const uiHtml = `
13+
<html>
14+
<head>
15+
<script type="module">
16+
import { App } from "https://unpkg.com/@modelcontextprotocol/ext-apps@0.3.1/dist/src/app-with-deps.js";
17+
18+
window.onload = async () => {
19+
const app = new App({name: "Example UI", version: "1.0.0"});
20+
app.ontoolresult = params => {
21+
document.getElementById("tool-result").innerText = JSON.stringify(params, null, 2);
22+
}
23+
document.getElementById("open-link-button").onclick = () => {
24+
app.openLink({url: "https://modelcontextprotocol.io"});
25+
}
26+
await app.connect();
27+
};
28+
</script>
29+
</head>
30+
<body>
31+
<div id="tool-result"></div>
32+
<button id="open-link-button">Open Link</button>
33+
</body>
34+
</html>
35+
`;
36+
const resourceUri = "ui://page";
37+
38+
registerAppResource(
39+
server,
40+
"page",
41+
resourceUri,
42+
{
43+
mimeType: RESOURCE_MIME_TYPE,
44+
_meta: {
45+
ui: {},
46+
},
47+
},
48+
() => ({
49+
contents: [
50+
{
51+
mimeType: RESOURCE_MIME_TYPE,
52+
text: uiHtml,
53+
uri: resourceUri,
54+
_meta: {
55+
ui: {
56+
csp: {
57+
connectDomains: ["https://unpkg.com"],
58+
resourceDomains: ["https://unpkg.com"],
59+
},
60+
},
61+
},
62+
},
63+
],
64+
}),
65+
);
66+
67+
registerAppTool(
68+
server,
69+
"show-inlined-example",
70+
{
71+
title: "Show Inlined Example",
72+
inputSchema: { message: z.string() },
73+
outputSchema: { message: z.string() },
74+
_meta: {
75+
ui: { resourceUri },
76+
},
77+
},
78+
({ message }: { message: string }) => ({
79+
content: [{type: 'text', text: 'Displaying an App'}],
80+
structuredContent: { message: `Server received message: ${message}` },
81+
_meta: { info: "example metadata" },
82+
}),
83+
);
84+
85+
return server;
86+
}
87+
88+
89+
async function main() {
90+
if (process.argv.includes("--stdio")) {
91+
await createServer().connect(new StdioServerTransport());
92+
} else {
93+
const port = parseInt(process.env.PORT ?? "3102", 10);
94+
await startServer(createServer, { port, name: "Basic MCP App Server (Vanilla JS)" });
95+
}
96+
}
97+
98+
main().catch((e) => {
99+
console.error(e);
100+
process.exit(1);
101+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
5+
"module": "ESNext",
6+
"moduleResolution": "bundler",
7+
"allowImportingTsExtensions": true,
8+
"resolveJsonModule": true,
9+
"isolatedModules": true,
10+
"verbatimModuleSyntax": true,
11+
"noEmit": true,
12+
"strict": true,
13+
"skipLibCheck": true,
14+
"noUnusedLocals": true,
15+
"noUnusedParameters": true,
16+
"noFallthroughCasesInSwitch": true
17+
},
18+
"include": ["server.ts", "server-utils.ts"]
19+
}

0 commit comments

Comments
 (0)