import {
createServer,
type IncomingMessage,
type ServerResponse,
} from "node:http";
import fs from "node:fs";
import path from "node:path";
import { URL, fileURLToPath } from "node:url";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import {
CallToolRequestSchema,
ListResourceTemplatesRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
type CallToolRequest,
type ListResourceTemplatesRequest,
type ListResourcesRequest,
type ListToolsRequest,
type ReadResourceRequest,
type Resource,
type ResourceTemplate,
type Tool,
} from "@modelcontextprotocol/sdk/types.js";
type Widget = {
id: string;
title: string;
templateUri: string;
invoking: string;
invoked: string;
html: string;
responseText: string;
};
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT_DIR = path.resolve(__dirname, "..", "..");
const ASSETS_DIR = path.resolve(ROOT_DIR, "assets");
function readWidgetHtml(componentName: string): string {
if (!fs.existsSync(ASSETS_DIR)) {
console.warn(
"Widget assets directory not found at " + ASSETS_DIR + ". Components will not have UI."
);
return "";
}
const directPath = path.join(ASSETS_DIR, componentName + ".html");
let htmlContents: string | null = null;
if (fs.existsSync(directPath)) {
htmlContents = fs.readFileSync(directPath, "utf8");
} else {
const candidates = fs
.readdirSync(ASSETS_DIR)
.filter(
(file) => file.startsWith(componentName + "-") && file.endsWith(".html")
)
.sort();
const fallback = candidates[candidates.length - 1];
if (fallback) {
htmlContents = fs.readFileSync(path.join(ASSETS_DIR, fallback), "utf8");
}
}
if (!htmlContents) {
console.warn(
'Widget HTML for "' + componentName + '" not found. Run "npm run build" to generate assets.'
);
return "";
}
return htmlContents;
}
function widgetMeta(widget: Widget) {
return {
"openai/outputTemplate": widget.templateUri,
"openai/toolInvocation/invoking": widget.invoking,
"openai/toolInvocation/invoked": widget.invoked,
"openai/widgetAccessible": true,
"openai/resultCanProduceWidget": true,
} as const;
}
// Define widgets
const widgets: Widget[] = [
{
id: "calculator",
title: "Show Calculator",
templateUri: "ui://widget/calculator.html",
invoking: "Opening calculator",
invoked: "Calculator ready",
html: readWidgetHtml("calculator"),
responseText: "Calculator widget rendered!",
},
];
const widgetsById = new Map<string, Widget>();
const widgetsByUri = new Map<string, Widget>();
widgets.forEach((widget) => {
widgetsById.set(widget.id, widget);
widgetsByUri.set(widget.templateUri, widget);
});
const calculatorInputSchema = {
type: "object",
properties: {
expression: {
type: "string",
description: "Math expression to calculate (e.g., '5 + 3', '10 * 2')",
},
},
required: ["expression"],
additionalProperties: false,
} as const;
const tools: Tool[] = [
{
name: "calculator",
description: "Show an interactive calculator widget",
inputSchema: calculatorInputSchema,
title: "Show Calculator",
_meta: widgetMeta(widgetsById.get("calculator")!),
annotations: {
destructiveHint: false,
openWorldHint: false,
readOnlyHint: true,
},
},
{
name: "get_current_time",
description: "Get the current time in ISO format",
title: "Get Current Time",
inputSchema: {
type: "object",
properties: {},
additionalProperties: false,
},
annotations: {
destructiveHint: false,
openWorldHint: false,
readOnlyHint: true,
},
},
{
name: "echo",
description: "Echo back a message",
title: "Echo Message",
inputSchema: {
type: "object",
properties: {
message: {
type: "string",
description: "The message to echo back",
},
},
required: ["message"],
additionalProperties: false,
},
annotations: {
destructiveHint: false,
openWorldHint: false,
readOnlyHint: true,
},
},
];
const resources: Resource[] = widgets.map((widget) => ({
uri: widget.templateUri,
name: widget.title,
description: widget.title + " widget markup",
mimeType: "text/html+skybridge",
_meta: widgetMeta(widget),
}));
const resourceTemplates: ResourceTemplate[] = widgets.map((widget) => ({
uriTemplate: widget.templateUri,
name: widget.title,
description: widget.title + " widget markup",
mimeType: "text/html+skybridge",
_meta: widgetMeta(widget),
}));
function createMcpServer(): Server {
const server = new Server(
{
name: "mcp-server",
version: "1.0.0",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
server.setRequestHandler(
ListResourcesRequestSchema,
async (_request: ListResourcesRequest) => ({
resources,
})
);
server.setRequestHandler(
ReadResourceRequestSchema,
async (request: ReadResourceRequest) => {
const widget = widgetsByUri.get(request.params.uri);
if (!widget) {
throw new Error("Unknown resource: " + request.params.uri);
}
return {
contents: [
{
uri: widget.templateUri,
mimeType: "text/html+skybridge",
text: widget.html,
_meta: widgetMeta(widget),
},
],
};
}
);
server.setRequestHandler(
ListResourceTemplatesRequestSchema,
async (_request: ListResourceTemplatesRequest) => ({
resourceTemplates,
})
);
server.setRequestHandler(
ListToolsRequestSchema,
async (_request: ListToolsRequest) => ({
tools,
})
);
server.setRequestHandler(
CallToolRequestSchema,
async (request: CallToolRequest) => {
const { name, arguments: args } = request.params;
switch (name) {
case "calculator": {
const widget = widgetsById.get("calculator");
if (!widget) {
throw new Error("Calculator widget not found");
}
const expression = (args as any)?.expression || "";
let result = "";
try {
// Safe eval for basic math
result = String(eval(expression));
} catch (error) {
result = "Error";
}
return {
content: [
{
type: "text",
text: widget.responseText,
},
],
structuredContent: {
expression,
result,
},
_meta: widgetMeta(widget),
};
}
case "get_current_time":
return {
content: [
{
type: "text",
text: new Date().toISOString(),
},
],
};
case "echo":
return {
content: [
{
type: "text",
text: (args as any)?.message || "",
},
],
};
default:
throw new Error("Unknown tool: " + name);
}
}
);
return server;
}
type SessionRecord = {
server: Server;
transport: SSEServerTransport;
};
const sessions = new Map<string, SessionRecord>();
const ssePath = "/mcp";
const postPath = "/mcp/messages";
async function handleSseRequest(res: ServerResponse) {
res.setHeader("Access-Control-Allow-Origin", "*");
const server = createMcpServer();
const transport = new SSEServerTransport(postPath, res);
const sessionId = transport.sessionId;
sessions.set(sessionId, { server, transport });
transport.onclose = async () => {
sessions.delete(sessionId);
await server.close();
};
transport.onerror = (error) => {
console.error("SSE transport error", error);
};
try {
await server.connect(transport);
} catch (error) {
sessions.delete(sessionId);
console.error("Failed to start SSE session", error);
if (!res.headersSent) {
res.writeHead(500).end("Failed to establish SSE connection");
}
}
}
async function handlePostMessage(
req: IncomingMessage,
res: ServerResponse,
url: URL
) {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Headers", "content-type");
const sessionId = url.searchParams.get("sessionId");
if (!sessionId) {
res.writeHead(400).end("Missing sessionId query parameter");
return;
}
const session = sessions.get(sessionId);
if (!session) {
res.writeHead(404).end("Unknown session");
return;
}
try {
await session.transport.handlePostMessage(req, res);
} catch (error) {
console.error("Failed to process message", error);
if (!res.headersSent) {
res.writeHead(500).end("Failed to process message");
}
}
}
const portEnv = Number(process.env.PORT ?? 8000);
const port = Number.isFinite(portEnv) ? portEnv : 8000;
const httpServer = createServer(
async (req: IncomingMessage, res: ServerResponse) => {
if (!req.url) {
res.writeHead(400).end("Missing URL");
return;
}
const url = new URL(req.url, "http://" + (req.headers.host ?? "localhost"));
if (
req.method === "OPTIONS" &&
(url.pathname === ssePath || url.pathname === postPath)
) {
res.writeHead(204, {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "content-type",
});
res.end();
return;
}
if (req.method === "GET" && url.pathname === ssePath) {
await handleSseRequest(res);
return;
}
if (req.method === "POST" && url.pathname === postPath) {
await handlePostMessage(req, res, url);
return;
}
res.writeHead(404).end("Not Found");
}
);
httpServer.on("clientError", (err: Error, socket) => {
console.error("HTTP client error", err);
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
});
httpServer.listen(port, () => {
console.log("MCP Server listening on http://localhost:" + port);
console.log(" SSE stream: GET http://localhost:" + port + ssePath);
console.log(
" Message post endpoint: POST http://localhost:" + port + postPath + "?sessionId=..."
);
});