From 4140565b77207f2731eab28e6bd19d039203fb5d Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 12 Jan 2026 17:06:44 -0600 Subject: [PATCH] Improve consistency and organization of examples for AI learning - Add `.gitignore` files to all 19 examples (`node_modules/`, `dist/`) - Replace custom `log` objects with standard `console` methods - Add `ontoolcancelled` handler to all `basic-server-*` examples - Add host styling integration (`applyDocumentTheme`, `applyHostFonts`, `applyHostStyleVariables`) to non-React `basic-server-*` examples - Add section headers for better code organization in feature examples - Update READMEs with Build System section explaining single-file bundling These changes make examples more consistent and easier to adapt. Co-Authored-By: Claude Opus 4.5 --- examples/basic-host/.gitignore | 2 + examples/basic-server-preact/.gitignore | 2 + examples/basic-server-preact/README.md | 4 + examples/basic-server-preact/src/mcp-app.tsx | 62 ++++++---- examples/basic-server-react/.gitignore | 2 + examples/basic-server-react/README.md | 4 + examples/basic-server-react/src/mcp-app.tsx | 45 ++++--- examples/basic-server-solid/.gitignore | 2 + examples/basic-server-solid/README.md | 4 + examples/basic-server-solid/src/mcp-app.tsx | 63 ++++++---- examples/basic-server-svelte/.gitignore | 2 + examples/basic-server-svelte/README.md | 4 + examples/basic-server-svelte/src/App.svelte | 56 ++++++--- examples/basic-server-vanillajs/.gitignore | 2 + examples/basic-server-vanillajs/README.md | 27 +--- .../basic-server-vanillajs/src/mcp-app.ts | 64 ++++++---- examples/basic-server-vue/.gitignore | 2 + examples/basic-server-vue/README.md | 4 + examples/basic-server-vue/src/App.vue | 59 ++++++--- examples/budget-allocator-server/.gitignore | 2 + examples/cohort-heatmap-server/.gitignore | 2 + .../customer-segmentation-server/.gitignore | 2 + examples/integration-server/.gitignore | 2 + examples/map-server/.gitignore | 2 + examples/scenario-modeler-server/.gitignore | 2 + examples/shadertoy-server/.gitignore | 2 + examples/sheet-music-server/.gitignore | 2 + examples/sheet-music-server/src/mcp-app.ts | 30 +++-- examples/system-monitor-server/.gitignore | 2 + examples/system-monitor-server/src/mcp-app.ts | 116 ++++++++++-------- examples/threejs-server/.gitignore | 2 + examples/threejs-server/server.ts | 8 ++ .../threejs-server/src/mcp-app-wrapper.tsx | 10 +- examples/threejs-server/src/threejs-app.tsx | 23 +++- examples/video-resource-server/.gitignore | 2 + examples/video-resource-server/server.ts | 17 ++- examples/video-resource-server/src/mcp-app.ts | 36 +++--- .../video-resource-server/src/server-utils.ts | 110 ----------------- examples/wiki-explorer-server/.gitignore | 2 + examples/wiki-explorer-server/src/mcp-app.ts | 28 ++++- 40 files changed, 449 insertions(+), 363 deletions(-) create mode 100644 examples/basic-host/.gitignore create mode 100644 examples/basic-server-preact/.gitignore create mode 100644 examples/basic-server-react/.gitignore create mode 100644 examples/basic-server-solid/.gitignore create mode 100644 examples/basic-server-svelte/.gitignore create mode 100644 examples/basic-server-vanillajs/.gitignore create mode 100644 examples/basic-server-vue/.gitignore create mode 100644 examples/budget-allocator-server/.gitignore create mode 100644 examples/cohort-heatmap-server/.gitignore create mode 100644 examples/customer-segmentation-server/.gitignore create mode 100644 examples/integration-server/.gitignore create mode 100644 examples/map-server/.gitignore create mode 100644 examples/scenario-modeler-server/.gitignore create mode 100644 examples/shadertoy-server/.gitignore create mode 100644 examples/sheet-music-server/.gitignore create mode 100644 examples/system-monitor-server/.gitignore create mode 100644 examples/threejs-server/.gitignore create mode 100644 examples/video-resource-server/.gitignore delete mode 100644 examples/video-resource-server/src/server-utils.ts create mode 100644 examples/wiki-explorer-server/.gitignore diff --git a/examples/basic-host/.gitignore b/examples/basic-host/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/basic-host/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/basic-server-preact/.gitignore b/examples/basic-server-preact/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/basic-server-preact/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/basic-server-preact/README.md b/examples/basic-server-preact/README.md index cc98ecb..8091ad8 100644 --- a/examples/basic-server-preact/README.md +++ b/examples/basic-server-preact/README.md @@ -28,3 +28,7 @@ npm run dev 1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`). 2. When the tool is invoked, the Host renders the UI from the resource. 3. The UI uses the MCP App SDK API to communicate with the host and call server tools. + +## Build System + +This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. diff --git a/examples/basic-server-preact/src/mcp-app.tsx b/examples/basic-server-preact/src/mcp-app.tsx index e00b8df..d5b6b60 100644 --- a/examples/basic-server-preact/src/mcp-app.tsx +++ b/examples/basic-server-preact/src/mcp-app.tsx @@ -1,23 +1,18 @@ /** * @file App that demonstrates a few features using MCP Apps SDK + Preact. */ -import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps"; +import { + App, + applyDocumentTheme, + applyHostFonts, + applyHostStyleVariables, + type McpUiHostContext, +} from "@modelcontextprotocol/ext-apps"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { useCallback, useEffect, useState } from "preact/hooks"; import { render } from "preact"; import styles from "./mcp-app.module.css"; - -const IMPLEMENTATION = { name: "Get Time App", version: "1.0.0" }; - - -const log = { - info: console.log.bind(console, "[APP]"), - warn: console.warn.bind(console, "[APP]"), - error: console.error.bind(console, "[APP]"), -}; - - function extractTime(callToolResult: CallToolResult): string { const { text } = callToolResult.content?.find((c) => c.type === "text")!; return text; @@ -30,19 +25,36 @@ function GetTimeApp() { const [toolResult, setToolResult] = useState(null); const [hostContext, setHostContext] = useState(); + // Apply host styles reactively when hostContext changes useEffect(() => { - const instance = new App(IMPLEMENTATION); + if (hostContext?.theme) { + applyDocumentTheme(hostContext.theme); + } + if (hostContext?.styles?.variables) { + applyHostStyleVariables(hostContext.styles.variables); + } + if (hostContext?.styles?.css?.fonts) { + applyHostFonts(hostContext.styles.css.fonts); + } + }, [hostContext]); + + useEffect(() => { + const instance = new App({ name: "Get Time App", version: "1.0.0" }); instance.ontoolinput = async (input) => { - log.info("Received tool call input:", input); + console.info("Received tool call input:", input); }; instance.ontoolresult = async (result) => { - log.info("Received tool call result:", result); + console.info("Received tool call result:", result); setToolResult(result); }; - instance.onerror = log.error; + instance.ontoolcancelled = (params) => { + console.info("Tool call cancelled:", params.reason); + }; + + instance.onerror = console.error; instance.onhostcontextchanged = (params) => { setHostContext((prev) => ({ ...prev, ...params })); @@ -83,12 +95,12 @@ function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps) const handleGetTime = useCallback(async () => { try { - log.info("Calling get-time tool..."); + console.info("Calling get-time tool..."); const result = await app.callServerTool({ name: "get-time", arguments: {} }); - log.info("get-time result:", result); + console.info("get-time result:", result); setServerTime(extractTime(result)); } catch (e) { - log.error(e); + console.error(e); setServerTime("[ERROR]"); } }, [app]); @@ -96,26 +108,26 @@ function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps) const handleSendMessage = useCallback(async () => { const signal = AbortSignal.timeout(5000); try { - log.info("Sending message text to Host:", messageText); + console.info("Sending message text to Host:", messageText); const { isError } = await app.sendMessage( { role: "user", content: [{ type: "text", text: messageText }] }, { signal }, ); - log.info("Message", isError ? "rejected" : "accepted"); + console.info("Message", isError ? "rejected" : "accepted"); } catch (e) { - log.error("Message send error:", signal.aborted ? "timed out" : e); + console.error("Message send error:", signal.aborted ? "timed out" : e); } }, [app, messageText]); const handleSendLog = useCallback(async () => { - log.info("Sending log text to Host:", logText); + console.info("Sending log text to Host:", logText); await app.sendLog({ level: "info", data: logText }); }, [app, logText]); const handleOpenLink = useCallback(async () => { - log.info("Sending open link request to Host:", linkUrl); + console.info("Sending open link request to Host:", linkUrl); const { isError } = await app.openLink({ url: linkUrl }); - log.info("Open link request", isError ? "rejected" : "accepted"); + console.info("Open link request", isError ? "rejected" : "accepted"); }, [app, linkUrl]); return ( diff --git a/examples/basic-server-react/.gitignore b/examples/basic-server-react/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/basic-server-react/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/basic-server-react/README.md b/examples/basic-server-react/README.md index c5be8e9..d29073d 100644 --- a/examples/basic-server-react/README.md +++ b/examples/basic-server-react/README.md @@ -30,3 +30,7 @@ npm run dev 1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`). 2. When the tool is invoked, the Host renders the UI from the resource. 3. The UI uses the MCP App SDK API to communicate with the host and call server tools. + +## Build System + +This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. diff --git a/examples/basic-server-react/src/mcp-app.tsx b/examples/basic-server-react/src/mcp-app.tsx index 04598c7..a42fce7 100644 --- a/examples/basic-server-react/src/mcp-app.tsx +++ b/examples/basic-server-react/src/mcp-app.tsx @@ -9,16 +9,6 @@ import { createRoot } from "react-dom/client"; import styles from "./mcp-app.module.css"; -const IMPLEMENTATION = { name: "Get Time App", version: "1.0.0" }; - - -const log = { - info: console.log.bind(console, "[APP]"), - warn: console.warn.bind(console, "[APP]"), - error: console.error.bind(console, "[APP]"), -}; - - function extractTime(callToolResult: CallToolResult): string { const { text } = callToolResult.content?.find((c) => c.type === "text")!; return text; @@ -28,24 +18,31 @@ function extractTime(callToolResult: CallToolResult): string { function GetTimeApp() { const [toolResult, setToolResult] = useState(null); const [hostContext, setHostContext] = useState(); + + // `useApp` (1) creates an `App` instance, (2) calls `onAppCreated` to + // register handlers, and (3) calls `connect()` on the `App` instance. const { app, error } = useApp({ - appInfo: IMPLEMENTATION, + appInfo: { name: "Get Time App", version: "1.0.0" }, capabilities: {}, onAppCreated: (app) => { app.onteardown = async () => { - log.info("App is being torn down"); + console.info("App is being torn down"); return {}; }; app.ontoolinput = async (input) => { - log.info("Received tool call input:", input); + console.info("Received tool call input:", input); }; app.ontoolresult = async (result) => { - log.info("Received tool call result:", result); + console.info("Received tool call result:", result); setToolResult(result); }; - app.onerror = log.error; + app.ontoolcancelled = (params) => { + console.info("Tool call cancelled:", params.reason); + }; + + app.onerror = console.error; app.onhostcontextchanged = (params) => { setHostContext((prev) => ({ ...prev, ...params })); @@ -85,12 +82,12 @@ function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps) const handleGetTime = useCallback(async () => { try { - log.info("Calling get-time tool..."); + console.info("Calling get-time tool..."); const result = await app.callServerTool({ name: "get-time", arguments: {} }); - log.info("get-time result:", result); + console.info("get-time result:", result); setServerTime(extractTime(result)); } catch (e) { - log.error(e); + console.error(e); setServerTime("[ERROR]"); } }, [app]); @@ -98,26 +95,26 @@ function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps) const handleSendMessage = useCallback(async () => { const signal = AbortSignal.timeout(5000); try { - log.info("Sending message text to Host:", messageText); + console.info("Sending message text to Host:", messageText); const { isError } = await app.sendMessage( { role: "user", content: [{ type: "text", text: messageText }] }, { signal }, ); - log.info("Message", isError ? "rejected" : "accepted"); + console.info("Message", isError ? "rejected" : "accepted"); } catch (e) { - log.error("Message send error:", signal.aborted ? "timed out" : e); + console.error("Message send error:", signal.aborted ? "timed out" : e); } }, [app, messageText]); const handleSendLog = useCallback(async () => { - log.info("Sending log text to Host:", logText); + console.info("Sending log text to Host:", logText); await app.sendLog({ level: "info", data: logText }); }, [app, logText]); const handleOpenLink = useCallback(async () => { - log.info("Sending open link request to Host:", linkUrl); + console.info("Sending open link request to Host:", linkUrl); const { isError } = await app.openLink({ url: linkUrl }); - log.info("Open link request", isError ? "rejected" : "accepted"); + console.info("Open link request", isError ? "rejected" : "accepted"); }, [app, linkUrl]); return ( diff --git a/examples/basic-server-solid/.gitignore b/examples/basic-server-solid/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/basic-server-solid/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/basic-server-solid/README.md b/examples/basic-server-solid/README.md index 39b5b7e..e0e7584 100644 --- a/examples/basic-server-solid/README.md +++ b/examples/basic-server-solid/README.md @@ -28,3 +28,7 @@ npm run dev 1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`). 2. When the tool is invoked, the Host renders the UI from the resource. 3. The UI uses the MCP App SDK API to communicate with the host and call server tools. + +## Build System + +This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. diff --git a/examples/basic-server-solid/src/mcp-app.tsx b/examples/basic-server-solid/src/mcp-app.tsx index 509cb0e..fa054e6 100644 --- a/examples/basic-server-solid/src/mcp-app.tsx +++ b/examples/basic-server-solid/src/mcp-app.tsx @@ -1,23 +1,18 @@ /** * @file App that demonstrates a few features using MCP Apps SDK + Solid. */ -import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps"; +import { + App, + applyDocumentTheme, + applyHostFonts, + applyHostStyleVariables, + type McpUiHostContext, +} from "@modelcontextprotocol/ext-apps"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { createEffect, createSignal, onMount, Show } from "solid-js"; import { render } from "solid-js/web"; import styles from "./mcp-app.module.css"; - -const IMPLEMENTATION = { name: "Get Time App", version: "1.0.0" }; - - -const log = { - info: console.log.bind(console, "[APP]"), - warn: console.warn.bind(console, "[APP]"), - error: console.error.bind(console, "[APP]"), -}; - - function extractTime(callToolResult: CallToolResult): string { const { text } = callToolResult.content?.find((c) => c.type === "text")!; return text; @@ -30,19 +25,37 @@ function GetTimeApp() { const [toolResult, setToolResult] = createSignal(null); const [hostContext, setHostContext] = createSignal(); + // Apply host styles reactively when hostContext changes + createEffect(() => { + const ctx = hostContext(); + if (ctx?.theme) { + applyDocumentTheme(ctx.theme); + } + if (ctx?.styles?.variables) { + applyHostStyleVariables(ctx.styles.variables); + } + if (ctx?.styles?.css?.fonts) { + applyHostFonts(ctx.styles.css.fonts); + } + }); + onMount(async () => { - const instance = new App(IMPLEMENTATION); + const instance = new App({ name: "Get Time App", version: "1.0.0" }); instance.ontoolinput = async (input) => { - log.info("Received tool call input:", input); + console.info("Received tool call input:", input); }; instance.ontoolresult = async (result) => { - log.info("Received tool call result:", result); + console.info("Received tool call result:", result); setToolResult(result); }; - instance.onerror = log.error; + instance.ontoolcancelled = (params) => { + console.info("Tool call cancelled:", params.reason); + }; + + instance.onerror = console.error; instance.onhostcontextchanged = (params) => { setHostContext((prev) => ({ ...prev, ...params })); @@ -87,12 +100,12 @@ function GetTimeAppInner(props: GetTimeAppInnerProps) { async function handleGetTime() { try { - log.info("Calling get-time tool..."); + console.info("Calling get-time tool..."); const result = await props.app.callServerTool({ name: "get-time", arguments: {} }); - log.info("get-time result:", result); + console.info("get-time result:", result); setServerTime(extractTime(result)); } catch (e) { - log.error(e); + console.error(e); setServerTime("[ERROR]"); } } @@ -100,26 +113,26 @@ function GetTimeAppInner(props: GetTimeAppInnerProps) { async function handleSendMessage() { const signal = AbortSignal.timeout(5000); try { - log.info("Sending message text to Host:", messageText()); + console.info("Sending message text to Host:", messageText()); const { isError } = await props.app.sendMessage( { role: "user", content: [{ type: "text", text: messageText() }] }, { signal }, ); - log.info("Message", isError ? "rejected" : "accepted"); + console.info("Message", isError ? "rejected" : "accepted"); } catch (e) { - log.error("Message send error:", signal.aborted ? "timed out" : e); + console.error("Message send error:", signal.aborted ? "timed out" : e); } } async function handleSendLog() { - log.info("Sending log text to Host:", logText()); + console.info("Sending log text to Host:", logText()); await props.app.sendLog({ level: "info", data: logText() }); } async function handleOpenLink() { - log.info("Sending open link request to Host:", linkUrl()); + console.info("Sending open link request to Host:", linkUrl()); const { isError } = await props.app.openLink({ url: linkUrl() }); - log.info("Open link request", isError ? "rejected" : "accepted"); + console.info("Open link request", isError ? "rejected" : "accepted"); } return ( diff --git a/examples/basic-server-svelte/.gitignore b/examples/basic-server-svelte/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/basic-server-svelte/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/basic-server-svelte/README.md b/examples/basic-server-svelte/README.md index 21e7d99..22505c8 100644 --- a/examples/basic-server-svelte/README.md +++ b/examples/basic-server-svelte/README.md @@ -28,3 +28,7 @@ npm run dev 1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`). 2. When the tool is invoked, the Host renders the UI from the resource. 3. The UI uses the MCP App SDK API to communicate with the host and call server tools. + +## Build System + +This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. diff --git a/examples/basic-server-svelte/src/App.svelte b/examples/basic-server-svelte/src/App.svelte index 0e221a3..31fe972 100644 --- a/examples/basic-server-svelte/src/App.svelte +++ b/examples/basic-server-svelte/src/App.svelte @@ -1,19 +1,20 @@ diff --git a/examples/basic-server-vanillajs/.gitignore b/examples/basic-server-vanillajs/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/basic-server-vanillajs/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/basic-server-vanillajs/README.md b/examples/basic-server-vanillajs/README.md index a95c829..41e23e3 100644 --- a/examples/basic-server-vanillajs/README.md +++ b/examples/basic-server-vanillajs/README.md @@ -10,6 +10,7 @@ An MCP App example with a vanilla JavaScript UI (no framework). - Tool registration with a linked UI resource - Vanilla JS UI using the [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class directly - App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink) +- Theme integration via [`applyDocumentTheme()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyDocumentTheme.html), [`applyHostStyleVariables()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyHostStyleVariables.html), and [`applyHostFonts()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyHostFonts.html) ## Key Files @@ -29,28 +30,6 @@ npm run dev 2. When the tool is invoked, the Host renders the UI from the resource. 3. The UI uses the MCP App SDK API to communicate with the host and call server tools. -## Vanilla JS Pattern +## Build System -```typescript -import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps"; - -// Get element references from static HTML -const button = document.getElementById("my-button")!; - -// Create app instance -const app = new App({ name: "My App", version: "1.0.0" }); - -// Register handlers BEFORE connecting -app.ontoolresult = (result) => { - /* handle result */ -}; -app.onerror = console.error; - -// Add event listeners -button.addEventListener("click", () => { - /* ... */ -}); - -// Connect to host -app.connect(); -``` +This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. diff --git a/examples/basic-server-vanillajs/src/mcp-app.ts b/examples/basic-server-vanillajs/src/mcp-app.ts index 2a5500a..c3168c2 100644 --- a/examples/basic-server-vanillajs/src/mcp-app.ts +++ b/examples/basic-server-vanillajs/src/mcp-app.ts @@ -1,26 +1,24 @@ /** * @file App that demonstrates a few features using MCP Apps SDK with vanilla JS. */ -import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps"; +import { + App, + applyDocumentTheme, + applyHostFonts, + applyHostStyleVariables, + type McpUiHostContext, +} from "@modelcontextprotocol/ext-apps"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import "./global.css"; import "./mcp-app.css"; -const log = { - info: console.log.bind(console, "[APP]"), - warn: console.warn.bind(console, "[APP]"), - error: console.error.bind(console, "[APP]"), -}; - - function extractTime(result: CallToolResult): string { const { time } = (result.structuredContent as { time?: string }) ?? {}; return time ?? "[ERROR]"; } -// Get element references const mainEl = document.querySelector(".main") as HTMLElement; const serverTimeEl = document.getElementById("server-time")!; const getTimeBtn = document.getElementById("get-time-btn")!; @@ -31,7 +29,17 @@ const sendLogBtn = document.getElementById("send-log-btn")!; const linkUrl = document.getElementById("link-url") as HTMLInputElement; const openLinkBtn = document.getElementById("open-link-btn")!; + function handleHostContextChanged(ctx: McpUiHostContext) { + if (ctx.theme) { + applyDocumentTheme(ctx.theme); + } + if (ctx.styles?.variables) { + applyHostStyleVariables(ctx.styles.variables); + } + if (ctx.styles?.css?.fonts) { + applyHostFonts(ctx.styles.css.fonts); + } if (ctx.safeAreaInsets) { mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`; mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`; @@ -41,38 +49,42 @@ function handleHostContextChanged(ctx: McpUiHostContext) { } -// Create app instance +// 1. Create app instance const app = new App({ name: "Get Time App", version: "1.0.0" }); + +// 2. Register handlers BEFORE connecting app.onteardown = async () => { - log.info("App is being torn down"); + console.info("App is being torn down"); return {}; }; -// Register handlers BEFORE connecting app.ontoolinput = (params) => { - log.info("Received tool call input:", params); + console.info("Received tool call input:", params); }; app.ontoolresult = (result) => { - log.info("Received tool call result:", result); + console.info("Received tool call result:", result); serverTimeEl.textContent = extractTime(result); }; -app.onerror = log.error; +app.ontoolcancelled = (params) => { + console.info("Tool call cancelled:", params.reason); +}; + +app.onerror = console.error; app.onhostcontextchanged = handleHostContextChanged; -// Add event listeners getTimeBtn.addEventListener("click", async () => { try { - log.info("Calling get-time tool..."); + console.info("Calling get-time tool..."); const result = await app.callServerTool({ name: "get-time", arguments: {} }); - log.info("get-time result:", result); + console.info("get-time result:", result); serverTimeEl.textContent = extractTime(result); } catch (e) { - log.error(e); + console.error(e); serverTimeEl.textContent = "[ERROR]"; } }); @@ -80,30 +92,30 @@ getTimeBtn.addEventListener("click", async () => { sendMessageBtn.addEventListener("click", async () => { const signal = AbortSignal.timeout(5000); try { - log.info("Sending message text to Host:", messageText.value); + console.info("Sending message text to Host:", messageText.value); const { isError } = await app.sendMessage( { role: "user", content: [{ type: "text", text: messageText.value }] }, { signal }, ); - log.info("Message", isError ? "rejected" : "accepted"); + console.info("Message", isError ? "rejected" : "accepted"); } catch (e) { - log.error("Message send error:", signal.aborted ? "timed out" : e); + console.error("Message send error:", signal.aborted ? "timed out" : e); } }); sendLogBtn.addEventListener("click", async () => { - log.info("Sending log text to Host:", logText.value); + console.info("Sending log text to Host:", logText.value); await app.sendLog({ level: "info", data: logText.value }); }); openLinkBtn.addEventListener("click", async () => { - log.info("Sending open link request to Host:", linkUrl.value); + console.info("Sending open link request to Host:", linkUrl.value); const { isError } = await app.openLink({ url: linkUrl.value }); - log.info("Open link request", isError ? "rejected" : "accepted"); + console.info("Open link request", isError ? "rejected" : "accepted"); }); -// Connect to host +// 3. Connect to host app.connect().then(() => { const ctx = app.getHostContext(); if (ctx) { diff --git a/examples/basic-server-vue/.gitignore b/examples/basic-server-vue/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/basic-server-vue/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/basic-server-vue/README.md b/examples/basic-server-vue/README.md index 1f96fba..2090973 100644 --- a/examples/basic-server-vue/README.md +++ b/examples/basic-server-vue/README.md @@ -28,3 +28,7 @@ npm run dev 1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`). 2. When the tool is invoked, the Host renders the UI from the resource. 3. The UI uses the MCP App SDK API to communicate with the host and call server tools. + +## Build System + +This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. diff --git a/examples/basic-server-vue/src/App.vue b/examples/basic-server-vue/src/App.vue index 3962bb4..bfffaee 100644 --- a/examples/basic-server-vue/src/App.vue +++ b/examples/basic-server-vue/src/App.vue @@ -1,19 +1,20 @@ diff --git a/examples/budget-allocator-server/.gitignore b/examples/budget-allocator-server/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/budget-allocator-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/cohort-heatmap-server/.gitignore b/examples/cohort-heatmap-server/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/cohort-heatmap-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/customer-segmentation-server/.gitignore b/examples/customer-segmentation-server/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/customer-segmentation-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/integration-server/.gitignore b/examples/integration-server/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/integration-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/map-server/.gitignore b/examples/map-server/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/map-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/scenario-modeler-server/.gitignore b/examples/scenario-modeler-server/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/scenario-modeler-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/shadertoy-server/.gitignore b/examples/shadertoy-server/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/shadertoy-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/sheet-music-server/.gitignore b/examples/sheet-music-server/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/sheet-music-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/sheet-music-server/src/mcp-app.ts b/examples/sheet-music-server/src/mcp-app.ts index a6efade..04fd16e 100644 --- a/examples/sheet-music-server/src/mcp-app.ts +++ b/examples/sheet-music-server/src/mcp-app.ts @@ -7,12 +7,10 @@ import "abcjs/abcjs-audio.css"; import "./global.css"; import "./mcp-app.css"; -const log = { - info: console.log.bind(console, "[APP]"), - error: console.error.bind(console, "[APP]"), -}; +// ============================================================================= +// State +// ============================================================================= -// State management interface AppState { visualObj: ABCJS.TuneObject[] | null; synthControl: ABCJS.SynthObjectController | null; @@ -23,12 +21,19 @@ const state: AppState = { synthControl: null, }; -// DOM element references +// ============================================================================= +// DOM References +// ============================================================================= + const mainEl = document.querySelector(".main") as HTMLElement; const statusEl = document.getElementById("status")!; const sheetMusicEl = document.getElementById("sheet-music")!; const audioControlsEl = document.getElementById("audio-controls")!; +// ============================================================================= +// ABC Rendering +// ============================================================================= + /** * Updates the status display. */ @@ -77,18 +82,21 @@ async function renderAbc(abcNotation: string): Promise { setStatus("Ready to play!"); } catch (error) { - log.error("Render error:", error); + console.error("Render error:", error); setStatus(`Error: ${(error as Error).message}`, true); audioControlsEl.innerHTML = ""; } } -// Create app instance +// ============================================================================= +// MCP Apps SDK Integration +// ============================================================================= + const app = new App({ name: "Sheet Music App", version: "1.0.0" }); // Handle tool input - receives ABC notation from the host app.ontoolinput = (params) => { - log.info("Received tool input:", params); + console.info("Received tool input:", params); const abcNotation = params.arguments?.abcNotation as string | undefined; @@ -99,8 +107,7 @@ app.ontoolinput = (params) => { } }; -// Error handler -app.onerror = log.error; +app.onerror = console.error; app.onhostcontextchanged = (params) => { if (params.safeAreaInsets) { @@ -111,7 +118,6 @@ app.onhostcontextchanged = (params) => { } }; -// Connect to host app.connect().then(() => { const ctx = app.getHostContext(); if (ctx) { diff --git a/examples/system-monitor-server/.gitignore b/examples/system-monitor-server/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/system-monitor-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/system-monitor-server/src/mcp-app.ts b/examples/system-monitor-server/src/mcp-app.ts index fceb73c..a39ac15 100644 --- a/examples/system-monitor-server/src/mcp-app.ts +++ b/examples/system-monitor-server/src/mcp-app.ts @@ -9,12 +9,10 @@ import "./mcp-app.css"; // Register Chart.js components Chart.register(...registerables); -const log = { - info: console.log.bind(console, "[APP]"), - error: console.error.bind(console, "[APP]"), -}; +// ============================================================================= +// Types +// ============================================================================= -// Types for system stats response interface SystemStats { cpu: { cores: Array<{ idle: number; total: number }>; @@ -39,7 +37,20 @@ interface SystemStats { timestamp: string; } -// DOM element references +interface PollingState { + isPolling: boolean; + intervalId: number | null; + cpuHistory: number[][]; // [timestamp][coreIndex] = usage% + labels: string[]; + coreCount: number; + chart: Chart | null; + previousCpuSnapshots: Array<{ idle: number; total: number }> | null; +} + +// ============================================================================= +// DOM References +// ============================================================================= + const mainEl = document.querySelector(".main") as HTMLElement; const pollToggleBtn = document.getElementById("poll-toggle-btn")!; const statusIndicator = document.getElementById("status-indicator")!; @@ -54,30 +65,13 @@ const infoHostname = document.getElementById("info-hostname")!; const infoPlatform = document.getElementById("info-platform")!; const infoUptime = document.getElementById("info-uptime")!; -// Polling state +// ============================================================================= +// Constants & State +// ============================================================================= + const HISTORY_LENGTH = 30; const POLL_INTERVAL = 2000; -interface PollingState { - isPolling: boolean; - intervalId: number | null; - cpuHistory: number[][]; // [timestamp][coreIndex] = usage% - labels: string[]; - coreCount: number; - chart: Chart | null; - previousCpuSnapshots: Array<{ idle: number; total: number }> | null; -} - -const state: PollingState = { - isPolling: false, - intervalId: null, - cpuHistory: [], - labels: [], - coreCount: 0, - chart: null, - previousCpuSnapshots: null, -}; - // Color palette for CPU cores (distinct colors) const CORE_COLORS = [ "rgba(59, 130, 246, 0.7)", // blue @@ -98,22 +92,19 @@ const CORE_COLORS = [ "rgba(244, 114, 182, 0.7)", // pink-light ]; -// Calculate CPU usage percentages from raw timing data -function calculateCpuUsage( - current: Array<{ idle: number; total: number }>, - previous: Array<{ idle: number; total: number }> | null, -): number[] { - if (!previous || previous.length !== current.length) { - return current.map(() => 0); - } - return current.map((cur, i) => { - const prev = previous[i]; - const idleDiff = cur.idle - prev.idle; - const totalDiff = cur.total - prev.total; - if (totalDiff === 0) return 0; - return Math.round((1 - idleDiff / totalDiff) * 100); - }); -} +const state: PollingState = { + isPolling: false, + intervalId: null, + cpuHistory: [], + labels: [], + coreCount: 0, + chart: null, + previousCpuSnapshots: null, +}; + +// ============================================================================= +// Chart.js Setup +// ============================================================================= function initChart(coreCount: number): Chart { const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches; @@ -222,6 +213,10 @@ function updateChart(cpuHistory: number[][], labels: string[]): void { state.chart.update("none"); } +// ============================================================================= +// UI Updates +// ============================================================================= + function updateMemoryBar(memory: SystemStats["memory"]): void { const percent = memory.usedPercent; @@ -255,9 +250,28 @@ function updateStatus(text: string, isPolling = false, isError = false): void { } } -// Create app instance +// ============================================================================= +// MCP App & Data Fetching +// ============================================================================= + const app = new App({ name: "System Monitor", version: "1.0.0" }); +function calculateCpuUsage( + current: Array<{ idle: number; total: number }>, + previous: Array<{ idle: number; total: number }> | null, +): number[] { + if (!previous || previous.length !== current.length) { + return current.map(() => 0); + } + return current.map((cur, i) => { + const prev = previous[i]; + const idleDiff = cur.idle - prev.idle; + const totalDiff = cur.total - prev.total; + if (totalDiff === 0) return 0; + return Math.round((1 - idleDiff / totalDiff) * 100); + }); +} + async function fetchStats(): Promise { try { const result = await app.callServerTool({ @@ -296,11 +310,15 @@ async function fetchStats(): Promise { const time = new Date().toLocaleTimeString("en-US", { hour12: false }); updateStatus(time, true); } catch (error) { - log.error("Failed to fetch stats:", error); + console.error("Failed to fetch stats:", error); updateStatus("Error", false, true); } } +// ============================================================================= +// Polling Control +// ============================================================================= + function startPolling(): void { if (state.isPolling) return; @@ -338,7 +356,10 @@ function togglePolling(): void { } } -// Event listeners +// ============================================================================= +// Event Handlers & Initialization +// ============================================================================= + pollToggleBtn.addEventListener("click", togglePolling); // Handle theme changes @@ -352,8 +373,7 @@ window } }); -// Register handlers and connect -app.onerror = log.error; +app.onerror = console.error; function handleHostContextChanged(ctx: McpUiHostContext) { if (ctx.safeAreaInsets) { diff --git a/examples/threejs-server/.gitignore b/examples/threejs-server/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/threejs-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts index d359968..75ef787 100644 --- a/examples/threejs-server/server.ts +++ b/examples/threejs-server/server.ts @@ -16,6 +16,10 @@ import path from "node:path"; import { z } from "zod"; import { startServer } from "./server-utils.js"; +// ============================================================================= +// Constants +// ============================================================================= + const DIST_DIR = path.join(import.meta.dirname, "dist"); // Default code example for the Three.js widget @@ -135,6 +139,10 @@ animate(); const resourceUri = "ui://threejs/mcp-app.html"; +// ============================================================================= +// Server Setup +// ============================================================================= + /** * Creates a new MCP server instance with tools and resources registered. * Each HTTP session needs its own server instance because McpServer only supports one transport. diff --git a/examples/threejs-server/src/mcp-app-wrapper.tsx b/examples/threejs-server/src/mcp-app-wrapper.tsx index beef4d8..fe94721 100644 --- a/examples/threejs-server/src/mcp-app-wrapper.tsx +++ b/examples/threejs-server/src/mcp-app-wrapper.tsx @@ -12,7 +12,9 @@ import { createRoot } from "react-dom/client"; import ThreeJSApp from "./threejs-app.tsx"; import "./global.css"; -const APP_INFO = { name: "Three.js Widget", version: "1.0.0" }; +// ============================================================================= +// Types +// ============================================================================= /** * Props passed to the widget component. @@ -37,6 +39,10 @@ export interface WidgetProps> { sendLog: App["sendLog"]; } +// ============================================================================= +// MCP App Wrapper +// ============================================================================= + function McpAppWrapper() { const [toolInputs, setToolInputs] = useState | null>( null, @@ -49,7 +55,7 @@ function McpAppWrapper() { const [hostContext, setHostContext] = useState(null); const { app, error } = useApp({ - appInfo: APP_INFO, + appInfo: { name: "Three.js Widget", version: "1.0.0" }, capabilities: {}, onAppCreated: (app) => { // Complete tool input (streaming finished) diff --git a/examples/threejs-server/src/threejs-app.tsx b/examples/threejs-server/src/threejs-app.tsx index 3fb5016..e6b773e 100644 --- a/examples/threejs-server/src/threejs-app.tsx +++ b/examples/threejs-server/src/threejs-app.tsx @@ -12,11 +12,21 @@ import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js"; import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js"; import type { WidgetProps } from "./mcp-app-wrapper.tsx"; +// ============================================================================= +// Types +// ============================================================================= + interface ThreeJSToolInput { code?: string; height?: number; } +type ThreeJSAppProps = WidgetProps; + +// ============================================================================= +// Constants +// ============================================================================= + // Default demo code shown when no code is provided const DEFAULT_THREEJS_CODE = `const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); @@ -52,7 +62,9 @@ function animate() { } animate();`; -type ThreeJSAppProps = WidgetProps; +// ============================================================================= +// Streaming Preview +// ============================================================================= const SHIMMER_STYLE = ` @keyframes shimmer { @@ -119,7 +131,10 @@ function LoadingShimmer({ height, code }: { height: number; code?: string }) { ); } -// Context object passed to user code +// ============================================================================= +// Three.js Execution +// ============================================================================= + const threeContext = { THREE, OrbitControls, @@ -145,6 +160,10 @@ async function executeThreeCode( await fn(threeContext, canvas, width, height); } +// ============================================================================= +// Main Component +// ============================================================================= + export default function ThreeJSApp({ toolInputs, toolInputsPartial, diff --git a/examples/video-resource-server/.gitignore b/examples/video-resource-server/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/video-resource-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/video-resource-server/server.ts b/examples/video-resource-server/server.ts index cd997da..7a793f2 100644 --- a/examples/video-resource-server/server.ts +++ b/examples/video-resource-server/server.ts @@ -62,8 +62,11 @@ function createServer(): McpServer { version: "1.0.0", }); - // Register video resource template - // This fetches video from CDN and returns as base64 blob + // =========================================================================== + // Binary Blob Resource - Binary content is fetched and returned as a + // base64-encoded blob. + // =========================================================================== + server.registerResource( "video", new ResourceTemplate("videos://{id}", { list: undefined }), @@ -109,7 +112,10 @@ function createServer(): McpServer { }, ); - // Register the video player tool + // =========================================================================== + // Tool Registration + // =========================================================================== + registerAppTool( server, "play_video", @@ -147,7 +153,10 @@ ${Object.entries(VIDEO_LIBRARY) }, ); - // Register the MCP App resource (the UI) + // =========================================================================== + // UI Resource Registration + // =========================================================================== + registerAppResource( server, RESOURCE_URI, diff --git a/examples/video-resource-server/src/mcp-app.ts b/examples/video-resource-server/src/mcp-app.ts index 76210ab..bc7688a 100644 --- a/examples/video-resource-server/src/mcp-app.ts +++ b/examples/video-resource-server/src/mcp-app.ts @@ -10,12 +10,10 @@ import { ReadResourceResultSchema } from "@modelcontextprotocol/sdk/types.js"; import "./global.css"; import "./mcp-app.css"; -const log = { - info: console.log.bind(console, "[VIDEO]"), - error: console.error.bind(console, "[VIDEO]"), -}; +// ============================================================================= +// DOM References +// ============================================================================= -// Get element references const mainEl = document.querySelector(".main") as HTMLElement; const loadingEl = document.getElementById("loading")!; const loadingTextEl = document.getElementById("loading-text")!; @@ -25,7 +23,10 @@ const playerEl = document.getElementById("player")!; const videoEl = document.getElementById("video") as HTMLVideoElement; const videoInfoEl = document.getElementById("video-info")!; -// Parse tool result to get video URI +// ============================================================================= +// UI State Helpers +// ============================================================================= + function parseToolResult( result: CallToolResult, ): { videoUri: string; description: string } | null { @@ -58,12 +59,16 @@ function showPlayer(dataUri: string, info: string) { playerEl.style.display = "block"; } -// Create app instance +// ============================================================================= +// MCP Apps SDK Integration +// ============================================================================= + const app = new App({ name: "Video Resource Player", version: "1.0.0" }); -// Handle tool result - this is called when the tool execution completes +// Handle tool result - Requests a resource via resources/read and converts the +// base64 blob to a data URI for use in the browser. app.ontoolresult = async (result) => { - log.info("Received tool result:", result); + console.info("Received tool result:", result); const parsed = parseToolResult(result); if (!parsed) { @@ -72,12 +77,12 @@ app.ontoolresult = async (result) => { } const { videoUri, description } = parsed; - log.info("Video URI:", videoUri, "Description:", description); + console.info("Video URI:", videoUri, "Description:", description); showLoading("Fetching video from MCP resource..."); try { - log.info("Requesting resource:", videoUri); + console.info("Requesting resource:", videoUri); const resourceResult = await app.request( { method: "resources/read", params: { uri: videoUri } }, @@ -89,24 +94,24 @@ app.ontoolresult = async (result) => { throw new Error("Resource response did not contain blob data"); } - log.info("Resource received, blob size:", content.blob.length); + console.info("Resource received, blob size:", content.blob.length); showLoading("Converting to data URI..."); const mimeType = content.mimeType || "video/mp4"; const dataUri = `data:${mimeType};base64,${content.blob}`; - log.info("Data URI created, length:", dataUri.length); + console.info("Data URI created, length:", dataUri.length); showPlayer(dataUri, `Loaded via MCP resource (${description})`); } catch (err) { - log.error("Error fetching resource:", err); + console.error("Error fetching resource:", err); showError(err instanceof Error ? err.message : String(err)); } }; app.onerror = (err) => { - log.error("App error:", err); + console.error("App error:", err); showError(err instanceof Error ? err.message : String(err)); }; @@ -121,7 +126,6 @@ function handleHostContextChanged(ctx: McpUiHostContext) { app.onhostcontextchanged = handleHostContextChanged; -// Connect to host app.connect().then(() => { const ctx = app.getHostContext(); if (ctx) { diff --git a/examples/video-resource-server/src/server-utils.ts b/examples/video-resource-server/src/server-utils.ts deleted file mode 100644 index 4052423..0000000 --- a/examples/video-resource-server/src/server-utils.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Shared utilities for running MCP servers with various transports. - */ - -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import cors from "cors"; -import type { Request, Response } from "express"; - -/** - * Starts an MCP server using the appropriate transport based on command-line arguments. - * - * If `--stdio` is passed, uses stdio transport. Otherwise, uses Streamable HTTP transport. - * - * @param createServer - Factory function that creates a new McpServer instance. - */ -export async function startServer( - createServer: () => McpServer, -): Promise { - try { - if (process.argv.includes("--stdio")) { - await startStdioServer(createServer); - } else { - await startStreamableHttpServer(createServer); - } - } catch (e) { - console.error(e); - process.exit(1); - } -} - -/** - * Starts an MCP server with stdio transport. - * - * @param createServer - Factory function that creates a new McpServer instance. - */ -export async function startStdioServer( - createServer: () => McpServer, -): Promise { - await createServer().connect(new StdioServerTransport()); -} - -/** - * Starts an MCP server with Streamable HTTP transport in stateless mode. - * - * Each request creates a fresh server and transport instance, which are - * closed when the response ends (no session tracking). - * - * The server listens on the port specified by the PORT environment variable, - * defaulting to 3001 if not set. - * - * @param createServer - Factory function that creates a new McpServer instance per request. - */ -export async function startStreamableHttpServer( - createServer: () => McpServer, -): Promise { - const port = parseInt(process.env.PORT ?? "3001", 10); - - // Express app - bind to all interfaces for development/testing - const expressApp = createMcpExpressApp({ host: "0.0.0.0" }); - expressApp.use(cors()); - - expressApp.all("/mcp", async (req: Request, res: Response) => { - // Create fresh server and transport for each request (stateless mode) - const server = createServer(); - const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, - }); - - // Clean up when response ends - res.on("close", () => { - transport.close().catch(() => {}); - server.close().catch(() => {}); - }); - - try { - await server.connect(transport); - await transport.handleRequest(req, res, req.body); - } catch (error) { - console.error("MCP error:", error); - if (!res.headersSent) { - res.status(500).json({ - jsonrpc: "2.0", - error: { code: -32603, message: "Internal server error" }, - id: null, - }); - } - } - }); - - const { promise, resolve, reject } = Promise.withResolvers(); - - const httpServer = expressApp.listen(port, (err?: Error) => { - if (err) return reject(err); - console.log(`Server listening on http://localhost:${port}/mcp`); - resolve(); - }); - - const shutdown = () => { - console.log("\nShutting down..."); - httpServer.close(() => process.exit(0)); - }; - - process.on("SIGINT", shutdown); - process.on("SIGTERM", shutdown); - - return promise; -} diff --git a/examples/wiki-explorer-server/.gitignore b/examples/wiki-explorer-server/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/examples/wiki-explorer-server/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts index 06ab391..7d95cb8 100644 --- a/examples/wiki-explorer-server/src/mcp-app.ts +++ b/examples/wiki-explorer-server/src/mcp-app.ts @@ -13,6 +13,10 @@ import ForceGraph, { type LinkObject, type NodeObject } from "force-graph"; import "./global.css"; import "./mcp-app.css"; +// ============================================================================= +// Helpers & Types +// ============================================================================= + // Helper to resolve CSS variables for canvas rendering function getCSSColor(varName: string): string { return ( @@ -49,7 +53,9 @@ type ToolResponse = { error: string | null; }; -// Graph state +// ============================================================================= +// Graph State & DOM References +// ============================================================================= const graphData: GraphData = { nodes: [], links: [] }; let selectedNodeUrl: string | null = null; let initialUrl: string | null = null; @@ -65,7 +71,9 @@ const zoomInBtn = document.getElementById("zoom-in")!; const zoomOutBtn = document.getElementById("zoom-out")!; const resetBtn = document.getElementById("reset-graph")!; -// Initialize force-graph +// ============================================================================= +// Force-Graph Initialization +// ============================================================================= const graph = new ForceGraph(container) .nodeId("url") .nodeLabel("title") @@ -111,7 +119,9 @@ function handleResize() { window.addEventListener("resize", handleResize); handleResize(); -// Node management functions +// ============================================================================= +// Graph Data Management +// ============================================================================= function addNode( url: string, title: string, @@ -170,7 +180,9 @@ function updateGraph(): void { graph.graphData({ nodes: [...graphData.nodes], links: [...graphData.links] }); } -// Popup management +// ============================================================================= +// Popup Management +// ============================================================================= function showPopup(node: NodeData, x: number, y: number): void { popupTitle.textContent = node.title; @@ -215,7 +227,9 @@ function hidePopup(): void { selectedNodeUrl = null; } -// Event handlers +// ============================================================================= +// UI Event Handlers +// ============================================================================= function handleNodeClick(node: NodeData, event: MouseEvent): void { // Toggle popup if clicking same node if (selectedNodeUrl === node.url) { @@ -245,7 +259,9 @@ zoomOutBtn.addEventListener("click", () => { graph.zoom(currentZoom / ZOOM_FACTOR, 200); }); -// Initialize App SDK +// ============================================================================= +// MCP Apps SDK Integration +// ============================================================================= const app = new App({ name: "Wiki Explorer", version: "1.0.0" }); // Reset button - clears graph and reloads from initial URL