diff --git a/examples/example-server.ts b/examples/example-server.ts deleted file mode 100644 index c9f9220..0000000 --- a/examples/example-server.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { type Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; -import express, { Request, Response } from "express"; -import { randomUUID } from "node:crypto"; -import { z } from "zod"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { - CallToolResult, - GetPromptResult, - isInitializeRequest, - PrimitiveSchemaDefinition, - ReadResourceResult, - Resource, - ResourceLink, -} from "@modelcontextprotocol/sdk/types.js"; -import { InMemoryEventStore } from "@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js"; - -import cors from "cors"; -import { text } from "node:stream/consumers"; -import path from "node:path"; -import fs from "node:fs/promises"; - -// const isProduction = process.env.NODE_ENV !== 'development'; -const exampleUiPath = __filename.endsWith(".ts") - ? path.join(__dirname, "dist", "example-ui.html") - : path.join(__dirname, "example-server.html"); -const exampleUiHtml = await fs.readFile(exampleUiPath, "utf-8"); - -// Create an MCP server with implementation details -const getServer = async () => { - const server = new McpServer( - { - name: "example-mcp-ui-server", - version: "1.0.0", - icons: [ - { src: "./mcp.svg", sizes: ["512x512"], mimeType: "image/svg+xml" }, - ], - websiteUrl: "https://github.com/modelcontextprotocol/typescript-sdk", - }, - { capabilities: { logging: {} } }, - ); - - const exampleUiTemplateResource: Resource = { - name: "example-ui-template", - uri: "ui://example-ui-template", - title: "Greeting Template", - description: "A simple greeting UI", - mimeType: "text/html", - }; - - server.registerResource( - exampleUiTemplateResource.name, - exampleUiTemplateResource.uri, - exampleUiTemplateResource, - async (): Promise => { - return { - contents: [ - { - uri: exampleUiTemplateResource.uri, - mimeType: exampleUiTemplateResource.mimeType, - text: exampleUiHtml, - }, - ], - }; - }, - ); - - server.registerTool( - "create-example-ui", - { - title: "Example UI", - description: "A tool that returns a simple UI", - inputSchema: { - message: z.string().describe("Name to greet"), - }, - _meta: { - "ui/resourceUri": exampleUiTemplateResource.uri, - }, - }, - async ({ message }): Promise => { - const structuredContent = { - message, - }; - return { - // content: [], - content: [ - { - type: "text", - text: JSON.stringify(structuredContent), - }, - ], - structuredContent, - }; - }, - ); - - server.registerTool( - "get-weather", - { - title: "Get Weather", - description: - "A tool that returns the current weather for a given location", - inputSchema: { - location: z.string().describe("Location to get the weather for"), - }, - }, - async ({ location }): Promise => { - // For simplicity, return a fixed weather report - const temperature = 25; // Fixed temperature - const condition = "sunny"; - const weatherReport = `The current weather in ${location} is ${condition} with a temperature of ${temperature}°C.`; - const structuredContent = { - temperature, - condition, - }; - return { - content: [ - { - type: "text", - text: weatherReport, - }, - ], - structuredContent, - }; - }, - ); - - return server; -}; - -const MCP_PORT = process.env.MCP_PORT - ? parseInt(process.env.MCP_PORT, 10) - : 3001; -const MCP_HOST = process.env.MCP_HOST || "localhost"; - -const app = express(); -app.use(express.json()); - -// Allow CORS all domains, expose the Mcp-Session-Id header -app.use( - cors({ - origin: "*", // Allow all origins - exposedHeaders: ["Mcp-Session-Id"], - }), -); - -// Map to store transports by session ID -const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; - -const mcpPostHandler = async (req: Request, res: Response) => { - const sessionId = req.headers["mcp-session-id"] as string | undefined; - if (sessionId) { - console.log(`Received MCP request for session: ${sessionId}`); - } else { - console.log("Request body:", req.body); - } - - try { - let transport: StreamableHTTPServerTransport; - if (sessionId && transports[sessionId]) { - // Reuse existing transport - transport = transports[sessionId]; - } else if (!sessionId && isInitializeRequest(req.body)) { - // New initialization request - const eventStore = new InMemoryEventStore(); - transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: () => randomUUID(), - eventStore, // Enable resumability - onsessioninitialized: (sessionId) => { - // Store the transport by session ID when session is initialized - // This avoids race conditions where requests might come in before the session is stored - console.log(`Session initialized with ID: ${sessionId}`); - transports[sessionId] = transport; - }, - }); - - // Set up onclose handler to clean up transport when closed - transport.onclose = () => { - const sid = transport.sessionId; - if (sid && transports[sid]) { - console.log( - `Transport closed for session ${sid}, removing from transports map`, - ); - delete transports[sid]; - } - }; - - // Connect the transport to the MCP server BEFORE handling the request - // so responses can flow back through the same transport - const server = await getServer(); - await server.connect(transport); - - await transport.handleRequest(req, res, req.body); - return; // Already handled - } else { - // Invalid request - no session ID or not initialization request - res.status(400).json({ - jsonrpc: "2.0", - error: { - code: -32000, - message: "Bad Request: No valid session ID provided", - }, - id: null, - }); - return; - } - - // Handle the request with existing transport - no need to reconnect - // The existing transport is already connected to the server - await transport.handleRequest(req, res, req.body); - } catch (error) { - console.error("Error handling MCP request:", error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: "2.0", - error: { - code: -32603, - message: "Internal server error", - }, - id: null, - }); - } - } -}; - -app.post("/mcp", mcpPostHandler); - -// Handle GET requests for SSE streams (using built-in support from StreamableHTTP) -const mcpGetHandler = async (req: Request, res: Response) => { - const sessionId = req.headers["mcp-session-id"] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send("Invalid or missing session ID"); - return; - } - - // Check for Last-Event-ID header for resumability - const lastEventId = req.headers["last-event-id"] as string | undefined; - if (lastEventId) { - console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`); - } else { - console.log(`Establishing new SSE stream for session ${sessionId}`); - } - - const transport = transports[sessionId]; - await transport.handleRequest(req, res); -}; - -app.get("/mcp", mcpGetHandler); - -// Handle DELETE requests for session termination (according to MCP spec) -const mcpDeleteHandler = async (req: Request, res: Response) => { - const sessionId = req.headers["mcp-session-id"] as string | undefined; - if (!sessionId || !transports[sessionId]) { - res.status(400).send("Invalid or missing session ID"); - return; - } - - console.log(`Received session termination request for session ${sessionId}`); - - try { - const transport = transports[sessionId]; - await transport.handleRequest(req, res); - } catch (error) { - console.error("Error handling session termination:", error); - if (!res.headersSent) { - res.status(500).send("Error processing session termination"); - } - } -}; - -app.delete("/mcp", mcpDeleteHandler); - -app.listen(MCP_PORT, (error) => { - if (error) { - console.error("Failed to start server:", error); - process.exit(1); - } - console.log(`MCP Streamable HTTP Server listening on port ${MCP_PORT}`); -}); - -// Handle server shutdown -process.on("SIGINT", async () => { - console.log("Shutting down server..."); - - // Close all active transports to properly clean up resources - for (const sessionId in transports) { - try { - console.log(`Closing transport for session ${sessionId}`); - await transports[sessionId].close(); - delete transports[sessionId]; - } catch (error) { - console.error(`Error closing transport for session ${sessionId}:`, error); - } - } - console.log("Server shutdown complete"); - process.exit(0); -}); diff --git a/examples/example-ui-raw.ts b/examples/example-ui-raw.ts deleted file mode 100644 index 15bf219..0000000 --- a/examples/example-ui-raw.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { App, PostMessageTransport } from "@ant/mcp-apps"; -import { - McpUiToolInputNotificationSchema, - McpUiSizeChangeNotificationSchema, - McpUiToolResultNotificationSchema, -} from "../src/types.js"; -import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; - -let nextId = 1; -function sendRequest(method: string, params: any) { - const id = nextId++; - window.parent.postMessage({ jsonrpc: "2.0", id, method, params }, "*"); - return new Promise((resolve, reject) => { - window.addEventListener("message", function listener(event) { - const data: JSONRPCMessage = event.data; - if (event.data?.id === id) { - window.removeEventListener("message", listener); - if (event.data?.result) { - resolve(true); - } else if (event.data?.error) { - reject(new Error(event.data.error)); - } - } else { - reject(new Error(`Unsupported message: ${JSON.stringify(data)}`)); - } - }); - }); -} -function sendNotification(method: string, params: any) { - window.parent.postMessage({ jsonrpc: "2.0", method, params }, "*"); -} -function onNotification(method: string, handler: (params: any) => void) { - window.addEventListener("message", function listener(event) { - if (event.data?.method === method) { - handler(event.data.params); - } - }); -} - -window.addEventListener("load", async () => { - const root = document.getElementById("root")!; - const appendText = (textContent: string, opts = {}) => { - root.appendChild( - Object.assign(document.createElement("div"), { - textContent, - ...opts, - }), - ); - }; - const appendError = (error: unknown) => - appendText( - `Error: ${error instanceof Error ? error.message : String(error)}`, - { style: "color: red;" }, - ); - - const initializeResult = await sendRequest("ui/initialize", { - appCapabilities: {}, - hostInfo: { - name: "MCP UI Client", - version: "1.0.0", - }, - protocolVersion: "2025-06-18", - }); - - sendNotification("ui/initialized", {}); - - // onNotification(McpUiToolInputNotificationSchema.shape.method._def.value, (params: - client.setNotificationHandler( - McpUiToolResultNotificationSchema, - async ({ params: { content, structuredContent, isError } }) => { - appendText( - `Tool call result received: isError=${isError}, content=${content}, structuredContent=${JSON.stringify(structuredContent)}`, - ); - }, - ); - client.setNotificationHandler( - McpUiSizeChangeNotificationSchema, - async ({ params: { width, height } }) => { - appendText( - `Size change notification received: width=${width}, height=${height}`, - ); - }, - ); - client.setNotificationHandler( - McpUiToolInputNotificationSchema, - async ({ params }) => { - appendText( - `Tool call input received: ${JSON.stringify(params.arguments)}`, - ); - }, - ); - - document.body.addEventListener("resize", () => { - client.sendSizeChange({ - width: document.body.scrollWidth, - height: document.body.scrollHeight, - }); - }); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Buy Groceries (Intent)", - onclick: async () => { - const signal = AbortSignal.timeout(5000); - try { - const { isError } = await client.sendIntent( - { - intent: "create-task", - params: { - title: "Buy groceries", - description: "Buy groceries for the week", - }, - }, - { signal }, - ); - appendText(`Intent result: ${isError ? "error" : "success"}`); - } catch (e) { - if (signal.aborted) { - appendError("Intent request timed out"); - return; - } - appendError(e); - } - }, - }), - ); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Get Weather (Tool)", - onclick: async () => { - const signal = AbortSignal.timeout(5000); - try { - const result = await client.callTool({ - name: "get-weather", - arguments: { location: "Tokyo" }, - }); - appendText(`Weather tool result: ${JSON.stringify(result)}`); - } catch (e) { - appendError(e); - } - }, - }), - ); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Notify Cart Updated", - onclick: async () => { - try { - await client.sendNotify({ - level: "info", - data: "cart-updated", - }); - } catch (e) { - appendError(e); - } - }, - }), - ); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Prompt Weather in Tokyo", - onclick: async () => { - const signal = AbortSignal.timeout(5000); - try { - const { isError } = await client.sendPrompt( - { - role: "user", // Forced. - content: [ - { - type: "text", - text: "What is the weather in Tokyo?", - }, - ], - }, - { signal }, - ); - appendText(`Prompt result: ${isError ? "error" : "success"}`); - } catch (e) { - if (signal.aborted) { - appendError("Prompt request timed out"); - return; - } - appendError(e); - } - }, - }), - ); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Open Link to Google", - onclick: async () => { - try { - const { isError } = await client.sendOpenLink({ - url: "https://www.google.com", - }); - appendText(`Open link result: ${isError ? "error" : "success"}`); - } catch (e) { - appendError(e); - } - }, - }), - ); - - await client.connect(transport); -}); diff --git a/examples/example-ui.html b/examples/example-ui.html deleted file mode 100644 index 83b9a3a..0000000 --- a/examples/example-ui.html +++ /dev/null @@ -1,2 +0,0 @@ - -
\ No newline at end of file diff --git a/examples/example-ui.ts b/examples/example-ui.ts deleted file mode 100644 index 34dd41d..0000000 --- a/examples/example-ui.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { App, PostMessageTransport } from "../src/app.js"; -import { - McpUiToolInputNotificationSchema, - McpUiSizeChangeNotificationSchema, - McpUiToolResultNotificationSchema, -} from "../src/types.js"; - -window.addEventListener("load", async () => { - const root = document.getElementById("root")!; - const appendText = (textContent: string, opts = {}) => { - root.appendChild( - Object.assign(document.createElement("div"), { - textContent, - ...opts, - }), - ); - }; - const appendError = (error: unknown) => - appendText( - `Error: ${error instanceof Error ? error.message : String(error)}`, - { style: "color: red;" }, - ); - - const transport = new PostMessageTransport(window.parent); - const app = new App({ - name: "MCP UI Client", - version: "1.0.0", - }); - - app.setNotificationHandler( - McpUiToolResultNotificationSchema, - async ({ params: { content, structuredContent, isError } }) => { - appendText( - `Tool call result received: isError=${isError}, content=${content}, structuredContent=${JSON.stringify(structuredContent)}`, - ); - }, - ); - app.setNotificationHandler( - McpUiSizeChangeNotificationSchema, - async ({ params: { width, height } }) => { - appendText( - `Size change notification received: width=${width}, height=${height}`, - ); - }, - ); - app.setNotificationHandler( - McpUiToolInputNotificationSchema, - async ({ params }) => { - appendText( - `Tool call input received: ${JSON.stringify(params.arguments)}`, - ); - }, - ); - - document.body.addEventListener("resize", () => { - app.sendSizeChange({ - width: document.body.scrollWidth, - height: document.body.scrollHeight, - }); - }); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Get Weather (Tool)", - onclick: async () => { - const signal = AbortSignal.timeout(5000); - try { - const result = await app.callServerTool({ - name: "get-weather", - arguments: { location: "Tokyo" }, - }); - appendText(`Weather tool result: ${JSON.stringify(result)}`); - } catch (e) { - appendError(e); - } - }, - }), - ); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Notify Cart Updated", - onclick: async () => { - try { - await app.sendLog({ - level: "info", - data: "cart-updated", - }); - } catch (e) { - appendError(e); - } - }, - }), - ); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Prompt Weather in Tokyo", - onclick: async () => { - const signal = AbortSignal.timeout(5000); - try { - const { isError } = await app.sendMessage( - { - role: "user", // Forced. - content: [ - { - type: "text", - text: "What is the weather in Tokyo?", - }, - ], - }, - { signal }, - ); - appendText(`Prompt result: ${isError ? "error" : "success"}`); - } catch (e) { - if (signal.aborted) { - appendError("Prompt request timed out"); - return; - } - appendError(e); - } - }, - }), - ); - - root.appendChild( - Object.assign(document.createElement("button"), { - textContent: "Open Link to Google", - onclick: async () => { - try { - const { isError } = await app.sendOpenLink({ - url: "https://www.google.com", - }); - appendText(`Open link result: ${isError ? "error" : "success"}`); - } catch (e) { - appendError(e); - } - }, - }), - ); - - await app.connect(transport); -});