Skip to content

Commit 0787b39

Browse files
ochafikclaude
andcommitted
fix: use factory pattern and shared server-utils for session handling
Update new framework examples (Vue, Svelte, Preact, Solid) to match the session handling fixes from PR #115: - Replace global McpServer instance with createServer() factory function - Use shared startServer() utility instead of inline Express setup - Add --stdio support for each server - Move RESOURCE_URI to module scope Each HTTP session now gets its own McpServer instance because McpServer only supports one transport at a time. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1177357 commit 0787b39

5 files changed

Lines changed: 248 additions & 282 deletions

File tree

Lines changed: 32 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,34 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.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";
64
import fs from "node:fs/promises";
75
import path from "node:path";
86
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
7+
import { startServer } from "../shared/server-utils.js";
98

10-
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
119
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 (Preact)",
19+
version: "1.0.0",
20+
});
1221

13-
14-
const server = new McpServer({
15-
name: "Basic MCP App Server (Preact)",
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-
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.
2625
server.registerTool(
2726
"get-time",
2827
{
2928
title: "Get Time",
3029
description: "Returns the current server time as an ISO 8601 string.",
3130
inputSchema: {},
32-
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
31+
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
3332
},
3433
async (): Promise<CallToolResult> => {
3534
const time = new Date().toISOString();
@@ -40,8 +39,8 @@ const server = new McpServer({
4039
);
4140

4241
server.registerResource(
43-
resourceUri,
44-
resourceUri,
42+
RESOURCE_URI,
43+
RESOURCE_URI,
4544
{},
4645
async (): Promise<ReadResourceResult> => {
4746
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
@@ -50,56 +49,25 @@ const server = new McpServer({
5049
contents: [
5150
// Per the MCP App specification, "text/html;profile=mcp-app" signals
5251
// to the Host that this resource is indeed for an MCP App UI.
53-
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
52+
{ uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html },
5453
],
5554
};
5655
},
5756
);
58-
}
59-
60-
61-
const app = express();
62-
app.use(cors());
63-
app.use(express.json());
6457

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-
});
58+
return server;
59+
}
8760

88-
const httpServer = app.listen(PORT, (err) => {
89-
if (err) {
90-
console.error("Error starting server:", err);
91-
process.exit(1);
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 ?? "3105", 10);
66+
await startServer(createServer, { port, name: "Basic MCP App Server (Preact)" });
9267
}
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-
});
10268
}
10369

104-
process.on("SIGINT", shutdown);
105-
process.on("SIGTERM", shutdown);
70+
main().catch((e) => {
71+
console.error(e);
72+
process.exit(1);
73+
});
Lines changed: 32 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,34 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.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";
64
import fs from "node:fs/promises";
75
import path from "node:path";
86
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
7+
import { startServer } from "../shared/server-utils.js";
98

10-
const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3001;
119
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 (Solid)",
19+
version: "1.0.0",
20+
});
1221

13-
14-
const server = new McpServer({
15-
name: "Basic MCP App Server (Solid)",
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-
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.
2625
server.registerTool(
2726
"get-time",
2827
{
2928
title: "Get Time",
3029
description: "Returns the current server time as an ISO 8601 string.",
3130
inputSchema: {},
32-
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
31+
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
3332
},
3433
async (): Promise<CallToolResult> => {
3534
const time = new Date().toISOString();
@@ -40,8 +39,8 @@ const server = new McpServer({
4039
);
4140

4241
server.registerResource(
43-
resourceUri,
44-
resourceUri,
42+
RESOURCE_URI,
43+
RESOURCE_URI,
4544
{},
4645
async (): Promise<ReadResourceResult> => {
4746
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
@@ -50,56 +49,25 @@ const server = new McpServer({
5049
contents: [
5150
// Per the MCP App specification, "text/html;profile=mcp-app" signals
5251
// to the Host that this resource is indeed for an MCP App UI.
53-
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
52+
{ uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html },
5453
],
5554
};
5655
},
5756
);
58-
}
59-
60-
61-
const app = express();
62-
app.use(cors());
63-
app.use(express.json());
6457

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-
});
58+
return server;
59+
}
8760

88-
const httpServer = app.listen(PORT, (err) => {
89-
if (err) {
90-
console.error("Error starting server:", err);
91-
process.exit(1);
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 ?? "3106", 10);
66+
await startServer(createServer, { port, name: "Basic MCP App Server (Solid)" });
9267
}
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-
});
10268
}
10369

104-
process.on("SIGINT", shutdown);
105-
process.on("SIGTERM", shutdown);
70+
main().catch((e) => {
71+
console.error(e);
72+
process.exit(1);
73+
});

0 commit comments

Comments
 (0)