Skip to content

Commit 4cc89e2

Browse files
Simplify basic-server-vanillajs to be self-contained
- Remove dependency on shared `server-utils.js` - Use Express + `StreamableHTTPServerTransport` directly - Simplify tool response to plain ISO time string - Add graceful shutdown handling - Remove artificial delay in UI teardown handler 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 75aecb8 commit 4cc89e2

File tree

2 files changed

+70
-45
lines changed

2 files changed

+70
-45
lines changed
Lines changed: 68 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,104 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
33
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
4+
import cors from "cors";
5+
import express, { type Request, type Response } from "express";
46
import fs from "node:fs/promises";
57
import path from "node:path";
68
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
7-
import { startServer } from "../shared/server-utils.js";
89

10+
const PORT = parseInt(process.env.PORT ?? "3002", 10);
911
const DIST_DIR = path.join(import.meta.dirname, "dist");
10-
const RESOURCE_URI = "ui://get-time/mcp-app.html";
11-
12-
/**
13-
* Creates a new MCP server instance with tools and resources registered.
14-
* Each HTTP session needs its own server instance because McpServer only supports one transport.
15-
*/
16-
function createServer(): McpServer {
17-
const server = new McpServer({
18-
name: "Basic MCP App Server (Vanilla JS)",
19-
version: "1.0.0",
20-
});
2112

22-
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
23-
// resource (the UI it renders). The `_meta` field on the tool links to the
24-
// resource URI, telling hosts which UI to display when the tool executes.
13+
14+
const server = new McpServer({
15+
name: "Basic MCP App Server (Vanilla JS)",
16+
version: "1.0.0",
17+
});
18+
19+
20+
{
21+
// Two-part registration: tool + resource, tied together by the resource URI.
22+
const resourceUri = "ui://get-time/mcp-app.html";
23+
24+
// Register a tool with UI metadata. When the host calls this tool, it reads
25+
// `_meta[RESOURCE_URI_META_KEY]` to know which resource to fetch and render as
26+
// an interactive UI.
2527
server.registerTool(
2628
"get-time",
2729
{
2830
title: "Get Time",
2931
description: "Returns the current server time as an ISO 8601 string.",
3032
inputSchema: {},
31-
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
33+
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
3234
},
3335
async (): Promise<CallToolResult> => {
3436
const time = new Date().toISOString();
35-
return {
36-
content: [{ type: "text", text: JSON.stringify({ time }) }],
37-
};
37+
return { content: [{ type: "text", text: time }] };
3838
},
3939
);
4040

41+
// Register the resource, which returns the bundled HTML/JavaScript for the UI.
4142
server.registerResource(
42-
RESOURCE_URI,
43-
RESOURCE_URI,
43+
resourceUri,
44+
resourceUri,
4445
{ mimeType: RESOURCE_MIME_TYPE },
4546
async (): Promise<ReadResourceResult> => {
4647
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
4748

4849
return {
4950
contents: [
50-
// Per the MCP App specification, "text/html;profile=mcp-app" signals
51-
// to the Host that this resource is indeed for an MCP App UI.
52-
{ uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html },
51+
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
5352
],
5453
};
5554
},
5655
);
57-
58-
return server;
5956
}
6057

61-
async function main() {
62-
if (process.argv.includes("--stdio")) {
63-
await createServer().connect(new StdioServerTransport());
64-
} else {
65-
const port = parseInt(process.env.PORT ?? "3102", 10);
66-
await startServer(createServer, { port, name: "Basic MCP App Server (Vanilla JS)" });
58+
59+
// Start an Express server that exposes the MCP endpoint.
60+
const expressApp = express();
61+
expressApp.use(cors());
62+
expressApp.use(express.json());
63+
64+
expressApp.post("/mcp", async (req: Request, res: Response) => {
65+
try {
66+
const transport = new StreamableHTTPServerTransport({
67+
sessionIdGenerator: undefined,
68+
enableJsonResponse: true,
69+
});
70+
res.on("close", () => { transport.close(); });
71+
72+
await server.connect(transport);
73+
74+
await transport.handleRequest(req, res, req.body);
75+
} catch (error) {
76+
console.error("Error handling MCP request:", error);
77+
if (!res.headersSent) {
78+
res.status(500).json({
79+
jsonrpc: "2.0",
80+
error: { code: -32603, message: "Internal server error" },
81+
id: null,
82+
});
83+
}
6784
}
68-
}
85+
});
6986

70-
main().catch((e) => {
71-
console.error(e);
72-
process.exit(1);
87+
const httpServer = expressApp.listen(PORT, (err) => {
88+
if (err) {
89+
console.error("Error starting server:", err);
90+
process.exit(1);
91+
}
92+
console.log(`Server listening on http://localhost:${PORT}/mcp`);
7393
});
94+
95+
function shutdown() {
96+
console.log("\nShutting down...");
97+
httpServer.close(() => {
98+
console.log("Server closed");
99+
process.exit(0);
100+
});
101+
}
102+
103+
process.on("SIGINT", shutdown);
104+
process.on("SIGTERM", shutdown);

examples/basic-server-vanillajs/src/mcp-app.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,8 @@ const log = {
1515

1616

1717
function extractTime(result: CallToolResult): string {
18-
const text = result.content!
19-
.filter((c): c is { type: "text"; text: string } => c.type === "text")
20-
.map((c) => c.text)
21-
.join("");
22-
const { time } = JSON.parse(text) as { time: string };
23-
return time;
18+
const { text } = result.content?.find((c) => c.type === "text")!;
19+
return text;
2420
}
2521

2622

@@ -40,8 +36,6 @@ const app = new App({ name: "Get Time App", version: "1.0.0" });
4036

4137
app.onteardown = async () => {
4238
log.info("App is being torn down");
43-
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate cleanup work
44-
log.info("App teardown complete");
4539
return {};
4640
};
4741

0 commit comments

Comments
 (0)