Skip to main content
Glama
BruceWilliamChen

ChatGPT Apps MCP Server

server.ts10.5 kB
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=..." ); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/BruceWilliamChen/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server