From 36b18a9b5f13007f29eaba50f43d1adfcbfb1b05 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 9 Jan 2026 00:01:44 +0000 Subject: [PATCH 01/13] build app-with-deps --- build.bun.ts | 14 +++++++++++++- package.json | 8 ++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/build.bun.ts b/build.bun.ts index d6cccabe..0e534189 100644 --- a/build.bun.ts +++ b/build.bun.ts @@ -7,7 +7,10 @@ await $`tsc`; const isDevelopment = Bun.env.NODE_ENV === "development"; // Build all JavaScript/TypeScript files -function buildJs(entrypoint: string, opts: Record = {}) { +function buildJs( + entrypoint: string, + opts: Partial[0]> = {}, +) { return Bun.build({ entrypoints: [entrypoint], outdir: "dist", @@ -27,6 +30,10 @@ await Promise.all([ outdir: "dist/src", external: ["@modelcontextprotocol/sdk"], }), + buildJs("src/app.ts", { + outdir: "dist/src", + naming: { entry: "app-with-deps.js" }, + }), buildJs("src/app-bridge.ts", { outdir: "dist/src", external: ["@modelcontextprotocol/sdk"], @@ -35,6 +42,11 @@ await Promise.all([ outdir: "dist/src/react", external: ["react", "react-dom", "@modelcontextprotocol/sdk"], }), + buildJs("src/react/index.tsx", { + outdir: "dist/src/react", + external: ["react", "react-dom", "@modelcontextprotocol/sdk"], + naming: { entry: "react-with-deps.js" }, + }), buildJs("src/server/index.ts", { outdir: "dist/src/server", external: ["@modelcontextprotocol/sdk"], diff --git a/package.json b/package.json index 7ac95a70..fa58076d 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,18 @@ "types": "./dist/src/app.d.ts", "default": "./dist/src/app.js" }, + "./app-with-deps": { + "types": "./dist/src/app.d.ts", + "default": "./dist/src/app-with-deps.js" + }, "./react": { "types": "./dist/src/react/index.d.ts", "default": "./dist/src/react/index.js" }, + "./react-with-deps": { + "types": "./dist/src/react/index.d.ts", + "default": "./dist/src/react/react-with-deps.js" + }, "./app-bridge": { "types": "./dist/src/app-bridge.d.ts", "default": "./dist/src/app-bridge.js" From d1547714a5c851f3bfb9a4a1b4044b5381669ffe Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 9 Jan 2026 00:02:39 +0000 Subject: [PATCH 02/13] make registerAppTool more interchangeable w/ server.registerTool --- src/server/index.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/server/index.ts b/src/server/index.ts index 720cf658..df7a36e5 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -11,7 +11,9 @@ import { McpUiToolMeta, } from "../app.js"; import type { + BaseToolCallback, McpServer, + RegisteredTool, ResourceMetadata, ToolCallback, ReadResourceCallback, @@ -33,6 +35,7 @@ export interface ToolConfig { title?: string; description?: string; inputSchema?: ZodRawShapeCompat | AnySchema; + outputSchema?: ZodRawShapeCompat | AnySchema; annotations?: ToolAnnotations; _meta?: Record; } @@ -100,15 +103,17 @@ export interface McpUiAppResourceConfig extends ResourceMetadata { * ``` */ export function registerAppTool< - TInputSchema extends ZodRawShapeCompat | AnySchema | undefined = undefined, + OutputArgs extends ZodRawShapeCompat | AnySchema, + InputArgs extends undefined | ZodRawShapeCompat | AnySchema = undefined, >( server: Pick, name: string, - config: Omit & { - inputSchema?: TInputSchema; + config: McpUiAppToolConfig & { + inputSchema?: InputArgs; + outputSchema?: OutputArgs; }, - handler: ToolCallback, -): void { + cb: ToolCallback, +): RegisteredTool { // Normalize metadata for backward compatibility: // - If _meta.ui.resourceUri is set, also set the legacy flat key // - If the legacy flat key is set, also set _meta.ui.resourceUri @@ -125,7 +130,7 @@ export function registerAppTool< normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } }; } - server.registerTool(name, { ...config, _meta: normalizedMeta }, handler); + return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb); } /** From 8ad706e468e270c1819429166439aac1a4509fae Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 9 Jan 2026 00:25:54 +0000 Subject: [PATCH 03/13] feat: use structuredContent in all example servers All servers now return structuredContent alongside JSON text content. All mcp-apps now read structuredContent directly (no JSON.parse fallback). Updated servers: - basic-server-vanillajs - budget-allocator-server - cohort-heatmap-server - customer-segmentation-server - integration-server - scenario-modeler-server - system-monitor-server - threejs-server - video-resource-server - wiki-explorer-server --- examples/basic-server-vanillajs/server.ts | 4 +- .../basic-server-vanillajs/src/mcp-app.ts | 4 +- examples/budget-allocator-server/server.ts | 1 + .../budget-allocator-server/src/mcp-app.ts | 8 +-- examples/cohort-heatmap-server/server.ts | 1 + .../cohort-heatmap-server/src/mcp-app.tsx | 8 +-- .../customer-segmentation-server/server.ts | 1 + .../src/mcp-app.ts | 55 +++++++------------ examples/integration-server/server.ts | 4 +- examples/integration-server/src/mcp-app.tsx | 8 +-- examples/scenario-modeler-server/server.ts | 15 +++-- .../scenario-modeler-server/src/mcp-app.tsx | 16 +----- examples/system-monitor-server/server.ts | 1 + examples/system-monitor-server/src/mcp-app.ts | 8 +-- examples/threejs-server/server.ts | 9 +-- examples/video-resource-server/server.ts | 12 +--- examples/video-resource-server/src/mcp-app.ts | 13 +---- examples/wiki-explorer-server/server.ts | 10 +++- examples/wiki-explorer-server/src/mcp-app.ts | 52 ++++++++---------- 19 files changed, 85 insertions(+), 145 deletions(-) diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index 2717fc31..9560d6ed 100644 --- a/examples/basic-server-vanillajs/server.ts +++ b/examples/basic-server-vanillajs/server.ts @@ -32,8 +32,10 @@ export function createServer(): McpServer { }, async (): Promise => { const time = new Date().toISOString(); + const data = { time }; return { - content: [{ type: "text", text: JSON.stringify({ time }) }], + content: [{ type: "text", text: JSON.stringify(data) }], + structuredContent: data, }; }, ); diff --git a/examples/basic-server-vanillajs/src/mcp-app.ts b/examples/basic-server-vanillajs/src/mcp-app.ts index 59e71282..3fa76546 100644 --- a/examples/basic-server-vanillajs/src/mcp-app.ts +++ b/examples/basic-server-vanillajs/src/mcp-app.ts @@ -15,8 +15,8 @@ const log = { function extractTime(result: CallToolResult): string { - const { text } = result.content?.find((c) => c.type === "text")!; - return text; + const data = result.structuredContent as { time: string }; + return data.time; } diff --git a/examples/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts index 3b780d16..5c22f6d0 100755 --- a/examples/budget-allocator-server/server.ts +++ b/examples/budget-allocator-server/server.ts @@ -279,6 +279,7 @@ export function createServer(): McpServer { text: JSON.stringify(response), }, ], + structuredContent: response, }; }, ); diff --git a/examples/budget-allocator-server/src/mcp-app.ts b/examples/budget-allocator-server/src/mcp-app.ts index 4510a854..ca838df2 100644 --- a/examples/budget-allocator-server/src/mcp-app.ts +++ b/examples/budget-allocator-server/src/mcp-app.ts @@ -607,13 +607,7 @@ const app = new App({ name: "Budget Allocator", version: "1.0.0" }); app.ontoolresult = (result) => { log.info("Received tool result:", result); - const text = result - .content!.filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - const data = JSON.parse(text) as BudgetDataResponse; + const data = result.structuredContent as BudgetDataResponse; if (data?.config && data?.analytics) { initializeUI(data.config, data.analytics); } diff --git a/examples/cohort-heatmap-server/server.ts b/examples/cohort-heatmap-server/server.ts index ccbe408f..402c226a 100644 --- a/examples/cohort-heatmap-server/server.ts +++ b/examples/cohort-heatmap-server/server.ts @@ -181,6 +181,7 @@ export function createServer(): McpServer { return { content: [{ type: "text", text: JSON.stringify(data) }], + structuredContent: data, }; }, ); diff --git a/examples/cohort-heatmap-server/src/mcp-app.tsx b/examples/cohort-heatmap-server/src/mcp-app.tsx index ce5c3e1f..ab06c78a 100644 --- a/examples/cohort-heatmap-server/src/mcp-app.tsx +++ b/examples/cohort-heatmap-server/src/mcp-app.tsx @@ -123,13 +123,7 @@ function CohortHeatmapInner({ maxPeriods: 12, }, }); - const text = result - .content!.filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - setData(JSON.parse(text) as CohortData); + setData(result.structuredContent as CohortData); } catch (e) { console.error("Failed to fetch cohort data:", e); } finally { diff --git a/examples/customer-segmentation-server/server.ts b/examples/customer-segmentation-server/server.ts index 4736f61a..0e72d608 100644 --- a/examples/customer-segmentation-server/server.ts +++ b/examples/customer-segmentation-server/server.ts @@ -85,6 +85,7 @@ export function createServer(): McpServer { return { content: [{ type: "text", text: JSON.stringify(data) }], + structuredContent: data, }; }, ); diff --git a/examples/customer-segmentation-server/src/mcp-app.ts b/examples/customer-segmentation-server/src/mcp-app.ts index f02bc6bb..2bd81c2d 100644 --- a/examples/customer-segmentation-server/src/mcp-app.ts +++ b/examples/customer-segmentation-server/src/mcp-app.ts @@ -369,41 +369,31 @@ function resetDetailPanel(): void { // Create app instance const app = new App({ name: "Customer Segmentation", version: "1.0.0" }); -// Fetch data from server -async function fetchData(): Promise { - try { - const result = await app.callServerTool({ - name: "get-customer-data", - arguments: {}, - }); - - const text = result - .content!.filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - const data = JSON.parse(text) as { - customers: Customer[]; - segments: SegmentSummary[]; - }; +// Handle tool results via structuredContent +app.ontoolresult = (result) => { + const data = result.structuredContent as { + customers: Customer[]; + segments: SegmentSummary[]; + }; - state.customers = data.customers; - state.segments = data.segments; + if (!data?.customers || !data?.segments) { + log.error("Invalid data received:", result); + return; + } - // Initialize or update chart - if (!state.chart) { - state.chart = initChart(); - } else { - updateChart(); - } + state.customers = data.customers; + state.segments = data.segments; - renderLegend(); - log.info(`Loaded ${data.customers.length} customers`); - } catch (error) { - log.error("Failed to fetch data:", error); + // Initialize or update chart + if (!state.chart) { + state.chart = initChart(); + } else { + updateChart(); } -} + + renderLegend(); + log.info(`Loaded ${data.customers.length} customers`); +}; // Event handlers xAxisSelect.addEventListener("change", () => { @@ -482,6 +472,3 @@ app.connect().then(() => { handleHostContextChanged(ctx); } }); - -// Fetch data after connection -setTimeout(fetchData, 100); diff --git a/examples/integration-server/server.ts b/examples/integration-server/server.ts index d2edd23f..a9c867b6 100644 --- a/examples/integration-server/server.ts +++ b/examples/integration-server/server.ts @@ -36,13 +36,15 @@ export function createServer(): McpServer { _meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI }, }, async (): Promise => { + const time = new Date().toISOString(); return { content: [ { type: "text", - text: JSON.stringify({ time: new Date().toISOString() }), + text: JSON.stringify({ time }), }, ], + structuredContent: { time }, }; }, ); diff --git a/examples/integration-server/src/mcp-app.tsx b/examples/integration-server/src/mcp-app.tsx index fadffdbb..7304ca9c 100644 --- a/examples/integration-server/src/mcp-app.tsx +++ b/examples/integration-server/src/mcp-app.tsx @@ -17,13 +17,7 @@ const log = { }; function extractTime(callToolResult: CallToolResult): string { - const text = callToolResult - .content!.filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - const { time } = JSON.parse(text) as { time: string }; + const { time } = callToolResult.structuredContent as { time: string }; return time; } diff --git a/examples/scenario-modeler-server/server.ts b/examples/scenario-modeler-server/server.ts index 3ce2066f..da56f104 100644 --- a/examples/scenario-modeler-server/server.ts +++ b/examples/scenario-modeler-server/server.ts @@ -278,18 +278,21 @@ export function createServer(): McpServer { ? calculateScenario(args.customInputs) : undefined; + const data = { + templates: SCENARIO_TEMPLATES, + defaultInputs: DEFAULT_INPUTS, + customProjections: customScenario?.projections, + customSummary: customScenario?.summary, + }; + return { content: [ { type: "text", - text: JSON.stringify({ - templates: SCENARIO_TEMPLATES, - defaultInputs: DEFAULT_INPUTS, - customProjections: customScenario?.projections, - customSummary: customScenario?.summary, - }), + text: JSON.stringify(data), }, ], + structuredContent: data, }; }, ); diff --git a/examples/scenario-modeler-server/src/mcp-app.tsx b/examples/scenario-modeler-server/src/mcp-app.tsx index e436e5a8..c0047ffe 100644 --- a/examples/scenario-modeler-server/src/mcp-app.tsx +++ b/examples/scenario-modeler-server/src/mcp-app.tsx @@ -1,6 +1,5 @@ import type { McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { StrictMode, useState, useMemo, useCallback, useEffect } from "react"; import { createRoot } from "react-dom/client"; import { SliderRow } from "./components/SliderRow.tsx"; @@ -25,18 +24,6 @@ interface CallToolResultData { defaultInputs?: ScenarioInputs; } -/** Extract templates and defaultInputs from tool result content */ -function extractResultData(result: CallToolResult): CallToolResultData { - const text = result - .content!.filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - const { templates, defaultInputs } = JSON.parse(text) as CallToolResultData; - return { templates, defaultInputs }; -} - const APP_INFO = { name: "SaaS Scenario Modeler", version: "1.0.0" }; function getSafeAreaPaddingStyle(hostContext?: McpUiHostContext) { @@ -70,7 +57,8 @@ function ScenarioModeler() { capabilities: {}, onAppCreated: (app) => { app.ontoolresult = async (result) => { - const { templates, defaultInputs } = extractResultData(result); + const { templates, defaultInputs } = + result.structuredContent as CallToolResultData; if (templates) setTemplates(templates); if (defaultInputs) setDefaultInputs(defaultInputs); }; diff --git a/examples/system-monitor-server/server.ts b/examples/system-monitor-server/server.ts index 4cf58882..91df27c5 100644 --- a/examples/system-monitor-server/server.ts +++ b/examples/system-monitor-server/server.ts @@ -151,6 +151,7 @@ export function createServer(): McpServer { return { content: [{ type: "text", text: JSON.stringify(stats) }], + structuredContent: stats, }; }, ); diff --git a/examples/system-monitor-server/src/mcp-app.ts b/examples/system-monitor-server/src/mcp-app.ts index 39730734..44ed604c 100644 --- a/examples/system-monitor-server/src/mcp-app.ts +++ b/examples/system-monitor-server/src/mcp-app.ts @@ -265,13 +265,7 @@ async function fetchStats(): Promise { arguments: {}, }); - const text = result - .content!.filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - const stats = JSON.parse(text) as SystemStats; + const stats = result.structuredContent as SystemStats; // Initialize chart on first data if needed if (!state.chart && stats.cpu.count > 0) { diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts index e2f28c7d..5b0030eb 100644 --- a/examples/threejs-server/server.ts +++ b/examples/threejs-server/server.ts @@ -169,13 +169,10 @@ export function createServer(): McpServer { _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async ({ code, height }) => { + const data = { code, height }; return { - content: [ - { - type: "text", - text: JSON.stringify({ code, height }), - }, - ], + content: [{ type: "text", text: JSON.stringify(data) }], + structuredContent: data, }; }, ); diff --git a/examples/video-resource-server/server.ts b/examples/video-resource-server/server.ts index 70effcad..c7371c93 100644 --- a/examples/video-resource-server/server.ts +++ b/examples/video-resource-server/server.ts @@ -133,16 +133,10 @@ ${Object.entries(VIDEO_LIBRARY) }, async ({ videoId }): Promise => { const video = VIDEO_LIBRARY[videoId]; + const data = { videoUri: `videos://${videoId}`, description: video.description }; return { - content: [ - { - type: "text", - text: JSON.stringify({ - videoUri: `videos://${videoId}`, - description: video.description, - }), - }, - ], + content: [{ type: "text", text: JSON.stringify(data) }], + structuredContent: data, }; }, ); diff --git a/examples/video-resource-server/src/mcp-app.ts b/examples/video-resource-server/src/mcp-app.ts index f530ce7f..b4817dd4 100644 --- a/examples/video-resource-server/src/mcp-app.ts +++ b/examples/video-resource-server/src/mcp-app.ts @@ -28,18 +28,7 @@ const videoInfoEl = document.getElementById("video-info")!; function parseToolResult( result: CallToolResult, ): { videoUri: string; description: string } | null { - const text = result.content - ?.filter((c): c is { type: "text"; text: string } => c.type === "text") - .map((c) => c.text) - .join(""); - - if (!text) return null; - - try { - return JSON.parse(text) as { videoUri: string; description: string }; - } catch { - return null; - } + return result.structuredContent as { videoUri: string; description: string } | null; } // Show states diff --git a/examples/wiki-explorer-server/server.ts b/examples/wiki-explorer-server/server.ts index 063cacfe..f0e9e868 100644 --- a/examples/wiki-explorer-server/server.ts +++ b/examples/wiki-explorer-server/server.ts @@ -121,11 +121,17 @@ export function createServer(): McpServer { const links = extractWikiLinks(new URL(url), html); const result = { page: { url, title }, links, error: null }; - return { content: [{ type: "text", text: JSON.stringify(result) }] }; + return { + content: [{ type: "text", text: JSON.stringify(result) }], + structuredContent: result, + }; } catch (err) { const error = err instanceof Error ? err.message : String(err); const result = { page: { url, title }, links: [], error }; - return { content: [{ type: "text", text: JSON.stringify(result) }] }; + return { + content: [{ type: "text", text: JSON.stringify(result) }], + structuredContent: result, + }; } }, ); diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts index 6bd5c268..db3ff576 100644 --- a/examples/wiki-explorer-server/src/mcp-app.ts +++ b/examples/wiki-explorer-server/src/mcp-app.ts @@ -322,44 +322,36 @@ app.ontoolresult = (result) => { }; function handleToolResultData(result: CallToolResult): void { - if ( - result.isError || - !result.content?.[0] || - result.content[0].type !== "text" - ) { + if (result.isError) { console.error("Tool result error:", result); return; } - try { - const response: ToolResponse = JSON.parse(result.content[0].text); - const { page, links, error } = response; + const response = result.structuredContent as ToolResponse; + const { page, links, error } = response; - // Ensure the source node exists - addNode(page.url, page.title); - updateNodeTitle(page.url, page.title); + // Ensure the source node exists + addNode(page.url, page.title); + updateNodeTitle(page.url, page.title); - if (error) { - setNodeState(page.url, "error", error); - } else { - // Get source node position so new nodes appear nearby - const sourceNode = graphData.nodes.find((n) => n.url === page.url); - const sourcePos = sourceNode - ? { x: sourceNode.x ?? 0, y: sourceNode.y ?? 0 } - : undefined; - - // Add all linked nodes and edges - for (const link of links) { - addNode(link.url, link.title, "default", sourcePos); - addEdge(page.url, link.url); - } - setNodeState(page.url, "expanded"); + if (error) { + setNodeState(page.url, "error", error); + } else { + // Get source node position so new nodes appear nearby + const sourceNode = graphData.nodes.find((n) => n.url === page.url); + const sourcePos = sourceNode + ? { x: sourceNode.x ?? 0, y: sourceNode.y ?? 0 } + : undefined; + + // Add all linked nodes and edges + for (const link of links) { + addNode(link.url, link.title, "default", sourcePos); + addEdge(page.url, link.url); } - - updateGraph(); - } catch (e) { - console.error("Failed to parse tool result:", e); + setNodeState(page.url, "expanded"); } + + updateGraph(); } app.onerror = (err) => { From 075e14d8fea9c0aa12bf656d36a43994b05f7578 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 9 Jan 2026 00:29:59 +0000 Subject: [PATCH 04/13] feat: add outputSchema to all example servers Define output schemas for structuredContent validation: - basic-server-vanillajs: { time: string } - budget-allocator-server: BudgetDataResponseSchema - cohort-heatmap-server: CohortDataSchema - customer-segmentation-server: customers/segments schema - integration-server: { time: string } - scenario-modeler-server: templates/defaultInputs schema - system-monitor-server: SystemStatsSchema - threejs-server: { code, height } - video-resource-server: { videoUri, description } - wiki-explorer-server: { page, links, error } --- examples/basic-server-vanillajs/server.ts | 4 ++++ examples/budget-allocator-server/server.ts | 1 + .../budget-allocator-server/src/mcp-app.ts | 2 +- examples/cohort-heatmap-server/server.ts | 1 + .../cohort-heatmap-server/src/mcp-app.tsx | 2 +- .../customer-segmentation-server/server.ts | 24 +++++++++++++++++++ examples/integration-server/server.ts | 4 ++++ examples/scenario-modeler-server/server.ts | 8 +++++++ .../scenario-modeler-server/src/mcp-app.tsx | 2 +- examples/system-monitor-server/server.ts | 1 + examples/system-monitor-server/src/mcp-app.ts | 2 +- examples/threejs-server/server.ts | 4 ++++ examples/video-resource-server/server.ts | 4 ++++ examples/wiki-explorer-server/server.ts | 13 ++++++++++ examples/wiki-explorer-server/src/mcp-app.ts | 2 +- 15 files changed, 69 insertions(+), 5 deletions(-) diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index 9560d6ed..ce591dae 100644 --- a/examples/basic-server-vanillajs/server.ts +++ b/examples/basic-server-vanillajs/server.ts @@ -3,6 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; import fs from "node:fs/promises"; import path from "node:path"; +import { z } from "zod"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server"; import { startServer } from "./server-utils.js"; @@ -28,6 +29,9 @@ export function createServer(): McpServer { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", inputSchema: {}, + outputSchema: z.object({ + time: z.string(), + }), _meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI }, }, async (): Promise => { diff --git a/examples/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts index 5c22f6d0..2b022155 100755 --- a/examples/budget-allocator-server/server.ts +++ b/examples/budget-allocator-server/server.ts @@ -248,6 +248,7 @@ export function createServer(): McpServer { description: "Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage", inputSchema: {}, + outputSchema: BudgetDataResponseSchema, _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async (): Promise => { diff --git a/examples/budget-allocator-server/src/mcp-app.ts b/examples/budget-allocator-server/src/mcp-app.ts index ca838df2..4e8c62df 100644 --- a/examples/budget-allocator-server/src/mcp-app.ts +++ b/examples/budget-allocator-server/src/mcp-app.ts @@ -607,7 +607,7 @@ const app = new App({ name: "Budget Allocator", version: "1.0.0" }); app.ontoolresult = (result) => { log.info("Received tool result:", result); - const data = result.structuredContent as BudgetDataResponse; + const data = result.structuredContent as unknown as BudgetDataResponse; if (data?.config && data?.analytics) { initializeUI(data.config, data.analytics); } diff --git a/examples/cohort-heatmap-server/server.ts b/examples/cohort-heatmap-server/server.ts index 402c226a..39ac2ec9 100644 --- a/examples/cohort-heatmap-server/server.ts +++ b/examples/cohort-heatmap-server/server.ts @@ -169,6 +169,7 @@ export function createServer(): McpServer { description: "Returns cohort retention heatmap data showing customer retention over time by signup month", inputSchema: GetCohortDataInputSchema.shape, + outputSchema: CohortDataSchema.shape, _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async ({ metric, periodType, cohortCount, maxPeriods }) => { diff --git a/examples/cohort-heatmap-server/src/mcp-app.tsx b/examples/cohort-heatmap-server/src/mcp-app.tsx index ab06c78a..919b6d14 100644 --- a/examples/cohort-heatmap-server/src/mcp-app.tsx +++ b/examples/cohort-heatmap-server/src/mcp-app.tsx @@ -123,7 +123,7 @@ function CohortHeatmapInner({ maxPeriods: 12, }, }); - setData(result.structuredContent as CohortData); + setData(result.structuredContent as unknown as CohortData); } catch (e) { console.error("Failed to fetch cohort data:", e); } finally { diff --git a/examples/customer-segmentation-server/server.ts b/examples/customer-segmentation-server/server.ts index 0e72d608..527855a0 100644 --- a/examples/customer-segmentation-server/server.ts +++ b/examples/customer-segmentation-server/server.ts @@ -30,6 +30,29 @@ const GetCustomerDataInputSchema = z.object({ .describe("Filter by segment (default: All)"), }); +const CustomerSchema = z.object({ + id: z.string(), + name: z.string(), + segment: z.string(), + annualRevenue: z.number(), + employeeCount: z.number(), + accountAge: z.number(), + engagementScore: z.number(), + supportTickets: z.number(), + nps: z.number(), +}); + +const SegmentSummarySchema = z.object({ + name: z.string(), + count: z.number(), + color: z.string(), +}); + +const GetCustomerDataOutputSchema = z.object({ + customers: z.array(CustomerSchema), + segments: z.array(SegmentSummarySchema), +}); + // Cache generated data for session consistency let cachedCustomers: Customer[] | null = null; let cachedSegments: SegmentSummary[] | null = null; @@ -78,6 +101,7 @@ export function createServer(): McpServer { description: "Returns customer data with segment information for visualization. Optionally filter by segment.", inputSchema: GetCustomerDataInputSchema.shape, + outputSchema: GetCustomerDataOutputSchema.shape, _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async ({ segment }): Promise => { diff --git a/examples/integration-server/server.ts b/examples/integration-server/server.ts index a9c867b6..700f1ebb 100644 --- a/examples/integration-server/server.ts +++ b/examples/integration-server/server.ts @@ -6,6 +6,7 @@ import type { } from "@modelcontextprotocol/sdk/types.js"; import fs from "node:fs/promises"; import path from "node:path"; +import { z } from "zod"; import { registerAppTool, registerAppResource, @@ -33,6 +34,9 @@ export function createServer(): McpServer { title: "Get Time", description: "Returns the current server time.", inputSchema: {}, + outputSchema: z.object({ + time: z.string(), + }), _meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI }, }, async (): Promise => { diff --git a/examples/scenario-modeler-server/server.ts b/examples/scenario-modeler-server/server.ts index da56f104..704c5798 100644 --- a/examples/scenario-modeler-server/server.ts +++ b/examples/scenario-modeler-server/server.ts @@ -64,6 +64,13 @@ const GetScenarioDataInputSchema = z.object({ ), }); +const GetScenarioDataOutputSchema = z.object({ + templates: z.array(ScenarioTemplateSchema), + defaultInputs: ScenarioInputsSchema, + customProjections: z.array(MonthlyProjectionSchema).optional(), + customSummary: ScenarioSummarySchema.optional(), +}); + // Types derived from schemas type ScenarioInputs = z.infer; type MonthlyProjection = z.infer; @@ -269,6 +276,7 @@ export function createServer(): McpServer { description: "Returns SaaS scenario templates and optionally computes custom projections for given inputs", inputSchema: GetScenarioDataInputSchema.shape, + outputSchema: GetScenarioDataOutputSchema.shape, _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async (args: { diff --git a/examples/scenario-modeler-server/src/mcp-app.tsx b/examples/scenario-modeler-server/src/mcp-app.tsx index c0047ffe..886e8157 100644 --- a/examples/scenario-modeler-server/src/mcp-app.tsx +++ b/examples/scenario-modeler-server/src/mcp-app.tsx @@ -58,7 +58,7 @@ function ScenarioModeler() { onAppCreated: (app) => { app.ontoolresult = async (result) => { const { templates, defaultInputs } = - result.structuredContent as CallToolResultData; + result.structuredContent as unknown as CallToolResultData; if (templates) setTemplates(templates); if (defaultInputs) setDefaultInputs(defaultInputs); }; diff --git a/examples/system-monitor-server/server.ts b/examples/system-monitor-server/server.ts index 91df27c5..477a9aef 100644 --- a/examples/system-monitor-server/server.ts +++ b/examples/system-monitor-server/server.ts @@ -124,6 +124,7 @@ export function createServer(): McpServer { description: "Returns current system statistics including per-core CPU usage, memory, and system info.", inputSchema: {}, + outputSchema: SystemStatsSchema.shape, _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async (): Promise => { diff --git a/examples/system-monitor-server/src/mcp-app.ts b/examples/system-monitor-server/src/mcp-app.ts index 44ed604c..fceb73c0 100644 --- a/examples/system-monitor-server/src/mcp-app.ts +++ b/examples/system-monitor-server/src/mcp-app.ts @@ -265,7 +265,7 @@ async function fetchStats(): Promise { arguments: {}, }); - const stats = result.structuredContent as SystemStats; + const stats = result.structuredContent as unknown as SystemStats; // Initialize chart on first data if needed if (!state.chart && stats.cpu.count > 0) { diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts index 5b0030eb..8181bad6 100644 --- a/examples/threejs-server/server.ts +++ b/examples/threejs-server/server.ts @@ -166,6 +166,10 @@ export function createServer(): McpServer { .default(400) .describe("Height in pixels"), }, + outputSchema: z.object({ + code: z.string(), + height: z.number(), + }), _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async ({ code, height }) => { diff --git a/examples/video-resource-server/server.ts b/examples/video-resource-server/server.ts index c7371c93..896e383b 100644 --- a/examples/video-resource-server/server.ts +++ b/examples/video-resource-server/server.ts @@ -129,6 +129,10 @@ ${Object.entries(VIDEO_LIBRARY) `Video ID to play. Available: ${Object.keys(VIDEO_LIBRARY).join(", ")}`, ), }, + outputSchema: z.object({ + videoUri: z.string(), + description: z.string(), + }), _meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI }, }, async ({ videoId }): Promise => { diff --git a/examples/wiki-explorer-server/server.ts b/examples/wiki-explorer-server/server.ts index f0e9e868..69002c1c 100644 --- a/examples/wiki-explorer-server/server.ts +++ b/examples/wiki-explorer-server/server.ts @@ -95,6 +95,19 @@ export function createServer(): McpServer { .default("https://en.wikipedia.org/wiki/Model_Context_Protocol") .describe("Wikipedia page URL"), }), + outputSchema: z.object({ + page: z.object({ + url: z.string(), + title: z.string(), + }), + links: z.array( + z.object({ + url: z.string(), + title: z.string(), + }), + ), + error: z.string().nullable(), + }), _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async ({ url }): Promise => { diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts index db3ff576..06ab3914 100644 --- a/examples/wiki-explorer-server/src/mcp-app.ts +++ b/examples/wiki-explorer-server/src/mcp-app.ts @@ -327,7 +327,7 @@ function handleToolResultData(result: CallToolResult): void { return; } - const response = result.structuredContent as ToolResponse; + const response = result.structuredContent as unknown as ToolResponse; const { page, links, error } = response; // Ensure the source node exists From c999adab1d1a6ec0e210f6c56f2ec60fde3f7abe Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 9 Jan 2026 00:33:29 +0000 Subject: [PATCH 05/13] prettier:fix --- examples/video-resource-server/server.ts | 5 ++++- examples/video-resource-server/src/mcp-app.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/video-resource-server/server.ts b/examples/video-resource-server/server.ts index 896e383b..0d3a33cf 100644 --- a/examples/video-resource-server/server.ts +++ b/examples/video-resource-server/server.ts @@ -137,7 +137,10 @@ ${Object.entries(VIDEO_LIBRARY) }, async ({ videoId }): Promise => { const video = VIDEO_LIBRARY[videoId]; - const data = { videoUri: `videos://${videoId}`, description: video.description }; + const data = { + videoUri: `videos://${videoId}`, + description: video.description, + }; return { content: [{ type: "text", text: JSON.stringify(data) }], structuredContent: data, diff --git a/examples/video-resource-server/src/mcp-app.ts b/examples/video-resource-server/src/mcp-app.ts index b4817dd4..693fe425 100644 --- a/examples/video-resource-server/src/mcp-app.ts +++ b/examples/video-resource-server/src/mcp-app.ts @@ -28,7 +28,10 @@ const videoInfoEl = document.getElementById("video-info")!; function parseToolResult( result: CallToolResult, ): { videoUri: string; description: string } | null { - return result.structuredContent as { videoUri: string; description: string } | null; + return result.structuredContent as { + videoUri: string; + description: string; + } | null; } // Show states From 991154428ce14e9398c9f3215f37239cb6cc48dd Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 9 Jan 2026 00:02:39 +0000 Subject: [PATCH 06/13] make registerAppTool more interchangeable w/ server.registerTool --- src/server/index.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/server/index.ts b/src/server/index.ts index 720cf658..df7a36e5 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -11,7 +11,9 @@ import { McpUiToolMeta, } from "../app.js"; import type { + BaseToolCallback, McpServer, + RegisteredTool, ResourceMetadata, ToolCallback, ReadResourceCallback, @@ -33,6 +35,7 @@ export interface ToolConfig { title?: string; description?: string; inputSchema?: ZodRawShapeCompat | AnySchema; + outputSchema?: ZodRawShapeCompat | AnySchema; annotations?: ToolAnnotations; _meta?: Record; } @@ -100,15 +103,17 @@ export interface McpUiAppResourceConfig extends ResourceMetadata { * ``` */ export function registerAppTool< - TInputSchema extends ZodRawShapeCompat | AnySchema | undefined = undefined, + OutputArgs extends ZodRawShapeCompat | AnySchema, + InputArgs extends undefined | ZodRawShapeCompat | AnySchema = undefined, >( server: Pick, name: string, - config: Omit & { - inputSchema?: TInputSchema; + config: McpUiAppToolConfig & { + inputSchema?: InputArgs; + outputSchema?: OutputArgs; }, - handler: ToolCallback, -): void { + cb: ToolCallback, +): RegisteredTool { // Normalize metadata for backward compatibility: // - If _meta.ui.resourceUri is set, also set the legacy flat key // - If the legacy flat key is set, also set _meta.ui.resourceUri @@ -125,7 +130,7 @@ export function registerAppTool< normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } }; } - server.registerTool(name, { ...config, _meta: normalizedMeta }, handler); + return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb); } /** From 22441e7367f9942ce13eef6ee12d5156061e1fd0 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 9 Jan 2026 00:25:54 +0000 Subject: [PATCH 07/13] feat: use structuredContent in all example servers All servers now return structuredContent alongside JSON text content. All mcp-apps now read structuredContent directly (no JSON.parse fallback). Updated servers: - basic-server-vanillajs - budget-allocator-server - cohort-heatmap-server - customer-segmentation-server - integration-server - scenario-modeler-server - system-monitor-server - threejs-server - video-resource-server - wiki-explorer-server --- examples/basic-server-vanillajs/server.ts | 4 +- .../basic-server-vanillajs/src/mcp-app.ts | 4 +- examples/budget-allocator-server/server.ts | 1 + .../budget-allocator-server/src/mcp-app.ts | 8 +-- examples/cohort-heatmap-server/server.ts | 1 + .../cohort-heatmap-server/src/mcp-app.tsx | 8 +-- .../customer-segmentation-server/server.ts | 1 + .../src/mcp-app.ts | 55 +++++++------------ examples/integration-server/server.ts | 4 +- examples/integration-server/src/mcp-app.tsx | 8 +-- examples/scenario-modeler-server/server.ts | 15 +++-- .../scenario-modeler-server/src/mcp-app.tsx | 16 +----- examples/system-monitor-server/server.ts | 1 + examples/system-monitor-server/src/mcp-app.ts | 8 +-- examples/threejs-server/server.ts | 9 +-- examples/video-resource-server/server.ts | 12 +--- examples/video-resource-server/src/mcp-app.ts | 13 +---- examples/wiki-explorer-server/server.ts | 10 +++- examples/wiki-explorer-server/src/mcp-app.ts | 52 ++++++++---------- 19 files changed, 85 insertions(+), 145 deletions(-) diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index 2717fc31..9560d6ed 100644 --- a/examples/basic-server-vanillajs/server.ts +++ b/examples/basic-server-vanillajs/server.ts @@ -32,8 +32,10 @@ export function createServer(): McpServer { }, async (): Promise => { const time = new Date().toISOString(); + const data = { time }; return { - content: [{ type: "text", text: JSON.stringify({ time }) }], + content: [{ type: "text", text: JSON.stringify(data) }], + structuredContent: data, }; }, ); diff --git a/examples/basic-server-vanillajs/src/mcp-app.ts b/examples/basic-server-vanillajs/src/mcp-app.ts index 59e71282..3fa76546 100644 --- a/examples/basic-server-vanillajs/src/mcp-app.ts +++ b/examples/basic-server-vanillajs/src/mcp-app.ts @@ -15,8 +15,8 @@ const log = { function extractTime(result: CallToolResult): string { - const { text } = result.content?.find((c) => c.type === "text")!; - return text; + const data = result.structuredContent as { time: string }; + return data.time; } diff --git a/examples/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts index 3b780d16..5c22f6d0 100755 --- a/examples/budget-allocator-server/server.ts +++ b/examples/budget-allocator-server/server.ts @@ -279,6 +279,7 @@ export function createServer(): McpServer { text: JSON.stringify(response), }, ], + structuredContent: response, }; }, ); diff --git a/examples/budget-allocator-server/src/mcp-app.ts b/examples/budget-allocator-server/src/mcp-app.ts index 4510a854..ca838df2 100644 --- a/examples/budget-allocator-server/src/mcp-app.ts +++ b/examples/budget-allocator-server/src/mcp-app.ts @@ -607,13 +607,7 @@ const app = new App({ name: "Budget Allocator", version: "1.0.0" }); app.ontoolresult = (result) => { log.info("Received tool result:", result); - const text = result - .content!.filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - const data = JSON.parse(text) as BudgetDataResponse; + const data = result.structuredContent as BudgetDataResponse; if (data?.config && data?.analytics) { initializeUI(data.config, data.analytics); } diff --git a/examples/cohort-heatmap-server/server.ts b/examples/cohort-heatmap-server/server.ts index ccbe408f..402c226a 100644 --- a/examples/cohort-heatmap-server/server.ts +++ b/examples/cohort-heatmap-server/server.ts @@ -181,6 +181,7 @@ export function createServer(): McpServer { return { content: [{ type: "text", text: JSON.stringify(data) }], + structuredContent: data, }; }, ); diff --git a/examples/cohort-heatmap-server/src/mcp-app.tsx b/examples/cohort-heatmap-server/src/mcp-app.tsx index ce5c3e1f..ab06c78a 100644 --- a/examples/cohort-heatmap-server/src/mcp-app.tsx +++ b/examples/cohort-heatmap-server/src/mcp-app.tsx @@ -123,13 +123,7 @@ function CohortHeatmapInner({ maxPeriods: 12, }, }); - const text = result - .content!.filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - setData(JSON.parse(text) as CohortData); + setData(result.structuredContent as CohortData); } catch (e) { console.error("Failed to fetch cohort data:", e); } finally { diff --git a/examples/customer-segmentation-server/server.ts b/examples/customer-segmentation-server/server.ts index 4736f61a..0e72d608 100644 --- a/examples/customer-segmentation-server/server.ts +++ b/examples/customer-segmentation-server/server.ts @@ -85,6 +85,7 @@ export function createServer(): McpServer { return { content: [{ type: "text", text: JSON.stringify(data) }], + structuredContent: data, }; }, ); diff --git a/examples/customer-segmentation-server/src/mcp-app.ts b/examples/customer-segmentation-server/src/mcp-app.ts index f02bc6bb..2bd81c2d 100644 --- a/examples/customer-segmentation-server/src/mcp-app.ts +++ b/examples/customer-segmentation-server/src/mcp-app.ts @@ -369,41 +369,31 @@ function resetDetailPanel(): void { // Create app instance const app = new App({ name: "Customer Segmentation", version: "1.0.0" }); -// Fetch data from server -async function fetchData(): Promise { - try { - const result = await app.callServerTool({ - name: "get-customer-data", - arguments: {}, - }); - - const text = result - .content!.filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - const data = JSON.parse(text) as { - customers: Customer[]; - segments: SegmentSummary[]; - }; +// Handle tool results via structuredContent +app.ontoolresult = (result) => { + const data = result.structuredContent as { + customers: Customer[]; + segments: SegmentSummary[]; + }; - state.customers = data.customers; - state.segments = data.segments; + if (!data?.customers || !data?.segments) { + log.error("Invalid data received:", result); + return; + } - // Initialize or update chart - if (!state.chart) { - state.chart = initChart(); - } else { - updateChart(); - } + state.customers = data.customers; + state.segments = data.segments; - renderLegend(); - log.info(`Loaded ${data.customers.length} customers`); - } catch (error) { - log.error("Failed to fetch data:", error); + // Initialize or update chart + if (!state.chart) { + state.chart = initChart(); + } else { + updateChart(); } -} + + renderLegend(); + log.info(`Loaded ${data.customers.length} customers`); +}; // Event handlers xAxisSelect.addEventListener("change", () => { @@ -482,6 +472,3 @@ app.connect().then(() => { handleHostContextChanged(ctx); } }); - -// Fetch data after connection -setTimeout(fetchData, 100); diff --git a/examples/integration-server/server.ts b/examples/integration-server/server.ts index d2edd23f..a9c867b6 100644 --- a/examples/integration-server/server.ts +++ b/examples/integration-server/server.ts @@ -36,13 +36,15 @@ export function createServer(): McpServer { _meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI }, }, async (): Promise => { + const time = new Date().toISOString(); return { content: [ { type: "text", - text: JSON.stringify({ time: new Date().toISOString() }), + text: JSON.stringify({ time }), }, ], + structuredContent: { time }, }; }, ); diff --git a/examples/integration-server/src/mcp-app.tsx b/examples/integration-server/src/mcp-app.tsx index fadffdbb..7304ca9c 100644 --- a/examples/integration-server/src/mcp-app.tsx +++ b/examples/integration-server/src/mcp-app.tsx @@ -17,13 +17,7 @@ const log = { }; function extractTime(callToolResult: CallToolResult): string { - const text = callToolResult - .content!.filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - const { time } = JSON.parse(text) as { time: string }; + const { time } = callToolResult.structuredContent as { time: string }; return time; } diff --git a/examples/scenario-modeler-server/server.ts b/examples/scenario-modeler-server/server.ts index 3ce2066f..da56f104 100644 --- a/examples/scenario-modeler-server/server.ts +++ b/examples/scenario-modeler-server/server.ts @@ -278,18 +278,21 @@ export function createServer(): McpServer { ? calculateScenario(args.customInputs) : undefined; + const data = { + templates: SCENARIO_TEMPLATES, + defaultInputs: DEFAULT_INPUTS, + customProjections: customScenario?.projections, + customSummary: customScenario?.summary, + }; + return { content: [ { type: "text", - text: JSON.stringify({ - templates: SCENARIO_TEMPLATES, - defaultInputs: DEFAULT_INPUTS, - customProjections: customScenario?.projections, - customSummary: customScenario?.summary, - }), + text: JSON.stringify(data), }, ], + structuredContent: data, }; }, ); diff --git a/examples/scenario-modeler-server/src/mcp-app.tsx b/examples/scenario-modeler-server/src/mcp-app.tsx index e436e5a8..c0047ffe 100644 --- a/examples/scenario-modeler-server/src/mcp-app.tsx +++ b/examples/scenario-modeler-server/src/mcp-app.tsx @@ -1,6 +1,5 @@ import type { McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { StrictMode, useState, useMemo, useCallback, useEffect } from "react"; import { createRoot } from "react-dom/client"; import { SliderRow } from "./components/SliderRow.tsx"; @@ -25,18 +24,6 @@ interface CallToolResultData { defaultInputs?: ScenarioInputs; } -/** Extract templates and defaultInputs from tool result content */ -function extractResultData(result: CallToolResult): CallToolResultData { - const text = result - .content!.filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - const { templates, defaultInputs } = JSON.parse(text) as CallToolResultData; - return { templates, defaultInputs }; -} - const APP_INFO = { name: "SaaS Scenario Modeler", version: "1.0.0" }; function getSafeAreaPaddingStyle(hostContext?: McpUiHostContext) { @@ -70,7 +57,8 @@ function ScenarioModeler() { capabilities: {}, onAppCreated: (app) => { app.ontoolresult = async (result) => { - const { templates, defaultInputs } = extractResultData(result); + const { templates, defaultInputs } = + result.structuredContent as CallToolResultData; if (templates) setTemplates(templates); if (defaultInputs) setDefaultInputs(defaultInputs); }; diff --git a/examples/system-monitor-server/server.ts b/examples/system-monitor-server/server.ts index 4cf58882..91df27c5 100644 --- a/examples/system-monitor-server/server.ts +++ b/examples/system-monitor-server/server.ts @@ -151,6 +151,7 @@ export function createServer(): McpServer { return { content: [{ type: "text", text: JSON.stringify(stats) }], + structuredContent: stats, }; }, ); diff --git a/examples/system-monitor-server/src/mcp-app.ts b/examples/system-monitor-server/src/mcp-app.ts index 39730734..44ed604c 100644 --- a/examples/system-monitor-server/src/mcp-app.ts +++ b/examples/system-monitor-server/src/mcp-app.ts @@ -265,13 +265,7 @@ async function fetchStats(): Promise { arguments: {}, }); - const text = result - .content!.filter( - (c): c is { type: "text"; text: string } => c.type === "text", - ) - .map((c) => c.text) - .join(""); - const stats = JSON.parse(text) as SystemStats; + const stats = result.structuredContent as SystemStats; // Initialize chart on first data if needed if (!state.chart && stats.cpu.count > 0) { diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts index e2f28c7d..5b0030eb 100644 --- a/examples/threejs-server/server.ts +++ b/examples/threejs-server/server.ts @@ -169,13 +169,10 @@ export function createServer(): McpServer { _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async ({ code, height }) => { + const data = { code, height }; return { - content: [ - { - type: "text", - text: JSON.stringify({ code, height }), - }, - ], + content: [{ type: "text", text: JSON.stringify(data) }], + structuredContent: data, }; }, ); diff --git a/examples/video-resource-server/server.ts b/examples/video-resource-server/server.ts index 70effcad..c7371c93 100644 --- a/examples/video-resource-server/server.ts +++ b/examples/video-resource-server/server.ts @@ -133,16 +133,10 @@ ${Object.entries(VIDEO_LIBRARY) }, async ({ videoId }): Promise => { const video = VIDEO_LIBRARY[videoId]; + const data = { videoUri: `videos://${videoId}`, description: video.description }; return { - content: [ - { - type: "text", - text: JSON.stringify({ - videoUri: `videos://${videoId}`, - description: video.description, - }), - }, - ], + content: [{ type: "text", text: JSON.stringify(data) }], + structuredContent: data, }; }, ); diff --git a/examples/video-resource-server/src/mcp-app.ts b/examples/video-resource-server/src/mcp-app.ts index f530ce7f..b4817dd4 100644 --- a/examples/video-resource-server/src/mcp-app.ts +++ b/examples/video-resource-server/src/mcp-app.ts @@ -28,18 +28,7 @@ const videoInfoEl = document.getElementById("video-info")!; function parseToolResult( result: CallToolResult, ): { videoUri: string; description: string } | null { - const text = result.content - ?.filter((c): c is { type: "text"; text: string } => c.type === "text") - .map((c) => c.text) - .join(""); - - if (!text) return null; - - try { - return JSON.parse(text) as { videoUri: string; description: string }; - } catch { - return null; - } + return result.structuredContent as { videoUri: string; description: string } | null; } // Show states diff --git a/examples/wiki-explorer-server/server.ts b/examples/wiki-explorer-server/server.ts index 063cacfe..f0e9e868 100644 --- a/examples/wiki-explorer-server/server.ts +++ b/examples/wiki-explorer-server/server.ts @@ -121,11 +121,17 @@ export function createServer(): McpServer { const links = extractWikiLinks(new URL(url), html); const result = { page: { url, title }, links, error: null }; - return { content: [{ type: "text", text: JSON.stringify(result) }] }; + return { + content: [{ type: "text", text: JSON.stringify(result) }], + structuredContent: result, + }; } catch (err) { const error = err instanceof Error ? err.message : String(err); const result = { page: { url, title }, links: [], error }; - return { content: [{ type: "text", text: JSON.stringify(result) }] }; + return { + content: [{ type: "text", text: JSON.stringify(result) }], + structuredContent: result, + }; } }, ); diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts index 6bd5c268..db3ff576 100644 --- a/examples/wiki-explorer-server/src/mcp-app.ts +++ b/examples/wiki-explorer-server/src/mcp-app.ts @@ -322,44 +322,36 @@ app.ontoolresult = (result) => { }; function handleToolResultData(result: CallToolResult): void { - if ( - result.isError || - !result.content?.[0] || - result.content[0].type !== "text" - ) { + if (result.isError) { console.error("Tool result error:", result); return; } - try { - const response: ToolResponse = JSON.parse(result.content[0].text); - const { page, links, error } = response; + const response = result.structuredContent as ToolResponse; + const { page, links, error } = response; - // Ensure the source node exists - addNode(page.url, page.title); - updateNodeTitle(page.url, page.title); + // Ensure the source node exists + addNode(page.url, page.title); + updateNodeTitle(page.url, page.title); - if (error) { - setNodeState(page.url, "error", error); - } else { - // Get source node position so new nodes appear nearby - const sourceNode = graphData.nodes.find((n) => n.url === page.url); - const sourcePos = sourceNode - ? { x: sourceNode.x ?? 0, y: sourceNode.y ?? 0 } - : undefined; - - // Add all linked nodes and edges - for (const link of links) { - addNode(link.url, link.title, "default", sourcePos); - addEdge(page.url, link.url); - } - setNodeState(page.url, "expanded"); + if (error) { + setNodeState(page.url, "error", error); + } else { + // Get source node position so new nodes appear nearby + const sourceNode = graphData.nodes.find((n) => n.url === page.url); + const sourcePos = sourceNode + ? { x: sourceNode.x ?? 0, y: sourceNode.y ?? 0 } + : undefined; + + // Add all linked nodes and edges + for (const link of links) { + addNode(link.url, link.title, "default", sourcePos); + addEdge(page.url, link.url); } - - updateGraph(); - } catch (e) { - console.error("Failed to parse tool result:", e); + setNodeState(page.url, "expanded"); } + + updateGraph(); } app.onerror = (err) => { From a8ab501ed31a05119e28ac1834b5a4949ad4b4e1 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 9 Jan 2026 00:29:59 +0000 Subject: [PATCH 08/13] feat: add outputSchema to all example servers Define output schemas for structuredContent validation: - basic-server-vanillajs: { time: string } - budget-allocator-server: BudgetDataResponseSchema - cohort-heatmap-server: CohortDataSchema - customer-segmentation-server: customers/segments schema - integration-server: { time: string } - scenario-modeler-server: templates/defaultInputs schema - system-monitor-server: SystemStatsSchema - threejs-server: { code, height } - video-resource-server: { videoUri, description } - wiki-explorer-server: { page, links, error } --- examples/basic-server-vanillajs/server.ts | 4 ++++ examples/budget-allocator-server/server.ts | 1 + .../budget-allocator-server/src/mcp-app.ts | 2 +- examples/cohort-heatmap-server/server.ts | 1 + .../cohort-heatmap-server/src/mcp-app.tsx | 2 +- .../customer-segmentation-server/server.ts | 24 +++++++++++++++++++ examples/integration-server/server.ts | 4 ++++ examples/scenario-modeler-server/server.ts | 8 +++++++ .../scenario-modeler-server/src/mcp-app.tsx | 2 +- examples/system-monitor-server/server.ts | 1 + examples/system-monitor-server/src/mcp-app.ts | 2 +- examples/threejs-server/server.ts | 4 ++++ examples/video-resource-server/server.ts | 4 ++++ examples/wiki-explorer-server/server.ts | 13 ++++++++++ examples/wiki-explorer-server/src/mcp-app.ts | 2 +- 15 files changed, 69 insertions(+), 5 deletions(-) diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index 9560d6ed..ce591dae 100644 --- a/examples/basic-server-vanillajs/server.ts +++ b/examples/basic-server-vanillajs/server.ts @@ -3,6 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; import fs from "node:fs/promises"; import path from "node:path"; +import { z } from "zod"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server"; import { startServer } from "./server-utils.js"; @@ -28,6 +29,9 @@ export function createServer(): McpServer { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", inputSchema: {}, + outputSchema: z.object({ + time: z.string(), + }), _meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI }, }, async (): Promise => { diff --git a/examples/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts index 5c22f6d0..2b022155 100755 --- a/examples/budget-allocator-server/server.ts +++ b/examples/budget-allocator-server/server.ts @@ -248,6 +248,7 @@ export function createServer(): McpServer { description: "Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage", inputSchema: {}, + outputSchema: BudgetDataResponseSchema, _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async (): Promise => { diff --git a/examples/budget-allocator-server/src/mcp-app.ts b/examples/budget-allocator-server/src/mcp-app.ts index ca838df2..4e8c62df 100644 --- a/examples/budget-allocator-server/src/mcp-app.ts +++ b/examples/budget-allocator-server/src/mcp-app.ts @@ -607,7 +607,7 @@ const app = new App({ name: "Budget Allocator", version: "1.0.0" }); app.ontoolresult = (result) => { log.info("Received tool result:", result); - const data = result.structuredContent as BudgetDataResponse; + const data = result.structuredContent as unknown as BudgetDataResponse; if (data?.config && data?.analytics) { initializeUI(data.config, data.analytics); } diff --git a/examples/cohort-heatmap-server/server.ts b/examples/cohort-heatmap-server/server.ts index 402c226a..39ac2ec9 100644 --- a/examples/cohort-heatmap-server/server.ts +++ b/examples/cohort-heatmap-server/server.ts @@ -169,6 +169,7 @@ export function createServer(): McpServer { description: "Returns cohort retention heatmap data showing customer retention over time by signup month", inputSchema: GetCohortDataInputSchema.shape, + outputSchema: CohortDataSchema.shape, _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async ({ metric, periodType, cohortCount, maxPeriods }) => { diff --git a/examples/cohort-heatmap-server/src/mcp-app.tsx b/examples/cohort-heatmap-server/src/mcp-app.tsx index ab06c78a..919b6d14 100644 --- a/examples/cohort-heatmap-server/src/mcp-app.tsx +++ b/examples/cohort-heatmap-server/src/mcp-app.tsx @@ -123,7 +123,7 @@ function CohortHeatmapInner({ maxPeriods: 12, }, }); - setData(result.structuredContent as CohortData); + setData(result.structuredContent as unknown as CohortData); } catch (e) { console.error("Failed to fetch cohort data:", e); } finally { diff --git a/examples/customer-segmentation-server/server.ts b/examples/customer-segmentation-server/server.ts index 0e72d608..527855a0 100644 --- a/examples/customer-segmentation-server/server.ts +++ b/examples/customer-segmentation-server/server.ts @@ -30,6 +30,29 @@ const GetCustomerDataInputSchema = z.object({ .describe("Filter by segment (default: All)"), }); +const CustomerSchema = z.object({ + id: z.string(), + name: z.string(), + segment: z.string(), + annualRevenue: z.number(), + employeeCount: z.number(), + accountAge: z.number(), + engagementScore: z.number(), + supportTickets: z.number(), + nps: z.number(), +}); + +const SegmentSummarySchema = z.object({ + name: z.string(), + count: z.number(), + color: z.string(), +}); + +const GetCustomerDataOutputSchema = z.object({ + customers: z.array(CustomerSchema), + segments: z.array(SegmentSummarySchema), +}); + // Cache generated data for session consistency let cachedCustomers: Customer[] | null = null; let cachedSegments: SegmentSummary[] | null = null; @@ -78,6 +101,7 @@ export function createServer(): McpServer { description: "Returns customer data with segment information for visualization. Optionally filter by segment.", inputSchema: GetCustomerDataInputSchema.shape, + outputSchema: GetCustomerDataOutputSchema.shape, _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async ({ segment }): Promise => { diff --git a/examples/integration-server/server.ts b/examples/integration-server/server.ts index a9c867b6..700f1ebb 100644 --- a/examples/integration-server/server.ts +++ b/examples/integration-server/server.ts @@ -6,6 +6,7 @@ import type { } from "@modelcontextprotocol/sdk/types.js"; import fs from "node:fs/promises"; import path from "node:path"; +import { z } from "zod"; import { registerAppTool, registerAppResource, @@ -33,6 +34,9 @@ export function createServer(): McpServer { title: "Get Time", description: "Returns the current server time.", inputSchema: {}, + outputSchema: z.object({ + time: z.string(), + }), _meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI }, }, async (): Promise => { diff --git a/examples/scenario-modeler-server/server.ts b/examples/scenario-modeler-server/server.ts index da56f104..704c5798 100644 --- a/examples/scenario-modeler-server/server.ts +++ b/examples/scenario-modeler-server/server.ts @@ -64,6 +64,13 @@ const GetScenarioDataInputSchema = z.object({ ), }); +const GetScenarioDataOutputSchema = z.object({ + templates: z.array(ScenarioTemplateSchema), + defaultInputs: ScenarioInputsSchema, + customProjections: z.array(MonthlyProjectionSchema).optional(), + customSummary: ScenarioSummarySchema.optional(), +}); + // Types derived from schemas type ScenarioInputs = z.infer; type MonthlyProjection = z.infer; @@ -269,6 +276,7 @@ export function createServer(): McpServer { description: "Returns SaaS scenario templates and optionally computes custom projections for given inputs", inputSchema: GetScenarioDataInputSchema.shape, + outputSchema: GetScenarioDataOutputSchema.shape, _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async (args: { diff --git a/examples/scenario-modeler-server/src/mcp-app.tsx b/examples/scenario-modeler-server/src/mcp-app.tsx index c0047ffe..886e8157 100644 --- a/examples/scenario-modeler-server/src/mcp-app.tsx +++ b/examples/scenario-modeler-server/src/mcp-app.tsx @@ -58,7 +58,7 @@ function ScenarioModeler() { onAppCreated: (app) => { app.ontoolresult = async (result) => { const { templates, defaultInputs } = - result.structuredContent as CallToolResultData; + result.structuredContent as unknown as CallToolResultData; if (templates) setTemplates(templates); if (defaultInputs) setDefaultInputs(defaultInputs); }; diff --git a/examples/system-monitor-server/server.ts b/examples/system-monitor-server/server.ts index 91df27c5..477a9aef 100644 --- a/examples/system-monitor-server/server.ts +++ b/examples/system-monitor-server/server.ts @@ -124,6 +124,7 @@ export function createServer(): McpServer { description: "Returns current system statistics including per-core CPU usage, memory, and system info.", inputSchema: {}, + outputSchema: SystemStatsSchema.shape, _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async (): Promise => { diff --git a/examples/system-monitor-server/src/mcp-app.ts b/examples/system-monitor-server/src/mcp-app.ts index 44ed604c..fceb73c0 100644 --- a/examples/system-monitor-server/src/mcp-app.ts +++ b/examples/system-monitor-server/src/mcp-app.ts @@ -265,7 +265,7 @@ async function fetchStats(): Promise { arguments: {}, }); - const stats = result.structuredContent as SystemStats; + const stats = result.structuredContent as unknown as SystemStats; // Initialize chart on first data if needed if (!state.chart && stats.cpu.count > 0) { diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts index 5b0030eb..8181bad6 100644 --- a/examples/threejs-server/server.ts +++ b/examples/threejs-server/server.ts @@ -166,6 +166,10 @@ export function createServer(): McpServer { .default(400) .describe("Height in pixels"), }, + outputSchema: z.object({ + code: z.string(), + height: z.number(), + }), _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async ({ code, height }) => { diff --git a/examples/video-resource-server/server.ts b/examples/video-resource-server/server.ts index c7371c93..896e383b 100644 --- a/examples/video-resource-server/server.ts +++ b/examples/video-resource-server/server.ts @@ -129,6 +129,10 @@ ${Object.entries(VIDEO_LIBRARY) `Video ID to play. Available: ${Object.keys(VIDEO_LIBRARY).join(", ")}`, ), }, + outputSchema: z.object({ + videoUri: z.string(), + description: z.string(), + }), _meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI }, }, async ({ videoId }): Promise => { diff --git a/examples/wiki-explorer-server/server.ts b/examples/wiki-explorer-server/server.ts index f0e9e868..69002c1c 100644 --- a/examples/wiki-explorer-server/server.ts +++ b/examples/wiki-explorer-server/server.ts @@ -95,6 +95,19 @@ export function createServer(): McpServer { .default("https://en.wikipedia.org/wiki/Model_Context_Protocol") .describe("Wikipedia page URL"), }), + outputSchema: z.object({ + page: z.object({ + url: z.string(), + title: z.string(), + }), + links: z.array( + z.object({ + url: z.string(), + title: z.string(), + }), + ), + error: z.string().nullable(), + }), _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async ({ url }): Promise => { diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts index db3ff576..06ab3914 100644 --- a/examples/wiki-explorer-server/src/mcp-app.ts +++ b/examples/wiki-explorer-server/src/mcp-app.ts @@ -327,7 +327,7 @@ function handleToolResultData(result: CallToolResult): void { return; } - const response = result.structuredContent as ToolResponse; + const response = result.structuredContent as unknown as ToolResponse; const { page, links, error } = response; // Ensure the source node exists From a6c34d707e5ad69d1b2947ae70e8866c946c48e7 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 9 Jan 2026 00:33:29 +0000 Subject: [PATCH 09/13] prettier:fix --- examples/video-resource-server/server.ts | 5 ++++- examples/video-resource-server/src/mcp-app.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/video-resource-server/server.ts b/examples/video-resource-server/server.ts index 896e383b..0d3a33cf 100644 --- a/examples/video-resource-server/server.ts +++ b/examples/video-resource-server/server.ts @@ -137,7 +137,10 @@ ${Object.entries(VIDEO_LIBRARY) }, async ({ videoId }): Promise => { const video = VIDEO_LIBRARY[videoId]; - const data = { videoUri: `videos://${videoId}`, description: video.description }; + const data = { + videoUri: `videos://${videoId}`, + description: video.description, + }; return { content: [{ type: "text", text: JSON.stringify(data) }], structuredContent: data, diff --git a/examples/video-resource-server/src/mcp-app.ts b/examples/video-resource-server/src/mcp-app.ts index b4817dd4..693fe425 100644 --- a/examples/video-resource-server/src/mcp-app.ts +++ b/examples/video-resource-server/src/mcp-app.ts @@ -28,7 +28,10 @@ const videoInfoEl = document.getElementById("video-info")!; function parseToolResult( result: CallToolResult, ): { videoUri: string; description: string } | null { - return result.structuredContent as { videoUri: string; description: string } | null; + return result.structuredContent as { + videoUri: string; + description: string; + } | null; } // Show states From 68eb5d9759327a3ee63e32b0d6c4f4d8649d0bba Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 9 Jan 2026 22:10:51 +0000 Subject: [PATCH 10/13] revert: remove unrelated workflow, version, and arch changes --- .github/workflows/npm-publish.yml | 6 ++ examples/basic-server-preact/package.json | 2 +- examples/basic-server-react/package.json | 2 +- examples/basic-server-solid/package.json | 2 +- examples/basic-server-svelte/package.json | 2 +- examples/basic-server-vanillajs/package.json | 2 +- examples/basic-server-vue/package.json | 2 +- examples/budget-allocator-server/package.json | 2 +- examples/cohort-heatmap-server/package.json | 2 +- .../customer-segmentation-server/package.json | 2 +- .../src/mcp-app.ts | 55 ++++++++++++------- examples/scenario-modeler-server/package.json | 2 +- examples/sheet-music-server/package.json | 2 +- examples/system-monitor-server/package.json | 2 +- examples/threejs-server/package.json | 2 +- examples/video-resource-server/package.json | 2 +- examples/wiki-explorer-server/package.json | 2 +- package.json | 2 +- 18 files changed, 56 insertions(+), 37 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 325a838e..b86a4d10 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -98,14 +98,20 @@ jobs: fail-fast: false matrix: example: + - basic-server-preact - basic-server-react + - basic-server-solid + - basic-server-svelte - basic-server-vanillajs + - basic-server-vue - budget-allocator-server - cohort-heatmap-server - customer-segmentation-server - scenario-modeler-server + - sheet-music-server - system-monitor-server - threejs-server + - video-resource-server - wiki-explorer-server steps: diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 3b237059..db3ce2bd 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "preact": "^10.0.0", "zod": "^4.1.13" diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index d859013d..6af5c31b 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index bf5b40db..7fc004bc 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "solid-js": "^1.9.0", "zod": "^4.1.13" diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index 7269f79c..5b680002 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "svelte": "^5.0.0", "zod": "^4.1.13" diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 99d38d86..623f48f9 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "zod": "^4.1.13" }, diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index ac836b5f..757365cf 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "vue": "^3.5.0", "zod": "^4.1.13" diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index 4aee9568..b58c1d1a 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -27,7 +27,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "chart.js": "^4.4.0", "zod": "^4.1.13" diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index 6d456d62..a4253d5b 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -27,7 +27,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index b5694c7e..c6019de0 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -27,7 +27,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "chart.js": "^4.4.0", "zod": "^4.1.13" diff --git a/examples/customer-segmentation-server/src/mcp-app.ts b/examples/customer-segmentation-server/src/mcp-app.ts index 2bd81c2d..f02bc6bb 100644 --- a/examples/customer-segmentation-server/src/mcp-app.ts +++ b/examples/customer-segmentation-server/src/mcp-app.ts @@ -369,31 +369,41 @@ function resetDetailPanel(): void { // Create app instance const app = new App({ name: "Customer Segmentation", version: "1.0.0" }); -// Handle tool results via structuredContent -app.ontoolresult = (result) => { - const data = result.structuredContent as { - customers: Customer[]; - segments: SegmentSummary[]; - }; +// Fetch data from server +async function fetchData(): Promise { + try { + const result = await app.callServerTool({ + name: "get-customer-data", + arguments: {}, + }); - if (!data?.customers || !data?.segments) { - log.error("Invalid data received:", result); - return; - } + const text = result + .content!.filter( + (c): c is { type: "text"; text: string } => c.type === "text", + ) + .map((c) => c.text) + .join(""); + const data = JSON.parse(text) as { + customers: Customer[]; + segments: SegmentSummary[]; + }; - state.customers = data.customers; - state.segments = data.segments; + state.customers = data.customers; + state.segments = data.segments; - // Initialize or update chart - if (!state.chart) { - state.chart = initChart(); - } else { - updateChart(); - } + // Initialize or update chart + if (!state.chart) { + state.chart = initChart(); + } else { + updateChart(); + } - renderLegend(); - log.info(`Loaded ${data.customers.length} customers`); -}; + renderLegend(); + log.info(`Loaded ${data.customers.length} customers`); + } catch (error) { + log.error("Failed to fetch data:", error); + } +} // Event handlers xAxisSelect.addEventListener("change", () => { @@ -472,3 +482,6 @@ app.connect().then(() => { handleHostContextChanged(ctx); } }); + +// Fetch data after connection +setTimeout(fetchData, 100); diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index aa6f3da7..c581a4d8 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -27,7 +27,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "chart.js": "^4.4.0", "react": "^19.2.0", diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index bd1cefb9..11c7f9e5 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "abcjs": "^6.4.4", "zod": "^4.1.13" diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index 573c63a0..b6d6c23d 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -27,7 +27,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "chart.js": "^4.4.0", "systeminformation": "^5.27.11", diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index 909c77d0..6334d6a1 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -27,7 +27,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index c765cef2..590fd917 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "zod": "^4.1.13" }, diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 1511dd5e..60a2d688 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -27,7 +27,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.3.0", + "@modelcontextprotocol/ext-apps": "^0.3.1", "@modelcontextprotocol/sdk": "^1.24.0", "cheerio": "^1.0.0", "zod": "^4.1.13" diff --git a/package.json b/package.json index cd1d12bd..c90f05b7 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/modelcontextprotocol/ext-apps" }, "homepage": "https://github.com/modelcontextprotocol/ext-apps", - "version": "0.3.0", + "version": "0.3.1", "license": "MIT", "description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.", "type": "module", From 340100208f25cf792e08116f2e97da037f25f244 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 9 Jan 2026 22:26:35 +0000 Subject: [PATCH 11/13] chore: update package-lock.json --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 115e4154..54d049fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1632,9 +1632,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.7", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", - "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", + "version": "1.19.8", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.8.tgz", + "integrity": "sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -3351,9 +3351,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.13", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.13.tgz", - "integrity": "sha512-WhtvB2NG2wjr04+h77sg3klAIwrgOqnjS49GGudnUPGFFgg7G17y7Qecqp+2Dr5kUDxNRBca0SK7cG8JwzkWDQ==", + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", "dev": true, "license": "Apache-2.0", "bin": { From cf1ad2241067625c83de6407a5cbcc4233a9c73d Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 9 Jan 2026 23:46:38 +0000 Subject: [PATCH 12/13] refactor(basic-server-vanillajs): address PR review comments - Simplify server.ts to return plain time string in text content (not JSON), matching the quickstart guide pattern - Add defensive coding in mcp-app.ts with optional chaining and fallback to handle potential type mismatches gracefully --- examples/basic-server-vanillajs/server.ts | 5 ++--- examples/basic-server-vanillajs/src/mcp-app.ts | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index ce591dae..ef661a87 100644 --- a/examples/basic-server-vanillajs/server.ts +++ b/examples/basic-server-vanillajs/server.ts @@ -36,10 +36,9 @@ export function createServer(): McpServer { }, async (): Promise => { const time = new Date().toISOString(); - const data = { time }; return { - content: [{ type: "text", text: JSON.stringify(data) }], - structuredContent: data, + content: [{ type: "text", text: time }], + structuredContent: { time }, }; }, ); diff --git a/examples/basic-server-vanillajs/src/mcp-app.ts b/examples/basic-server-vanillajs/src/mcp-app.ts index 3fa76546..2a5500a6 100644 --- a/examples/basic-server-vanillajs/src/mcp-app.ts +++ b/examples/basic-server-vanillajs/src/mcp-app.ts @@ -15,8 +15,8 @@ const log = { function extractTime(result: CallToolResult): string { - const data = result.structuredContent as { time: string }; - return data.time; + const { time } = (result.structuredContent as { time?: string }) ?? {}; + return time ?? "[ERROR]"; } From af88f60bdb9931412e3cc031c8e5f579010b04c5 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sat, 10 Jan 2026 00:38:42 +0000 Subject: [PATCH 13/13] refactor: restore rich text formatting for LLM-readable content Restore the human-readable text formatting functions that were removed, providing meaningful summaries in the text content while keeping structuredContent for programmatic UI consumption: - budget-allocator-server: formatBudgetSummary with config overview - cohort-heatmap-server: formatCohortSummary with retention stats - scenario-modeler-server: formatScenarioSummary with financial metrics --- examples/budget-allocator-server/server.ts | 26 ++++++++- examples/cohort-heatmap-server/server.ts | 13 ++++- examples/scenario-modeler-server/server.ts | 64 +++++++++++++++++----- 3 files changed, 88 insertions(+), 15 deletions(-) diff --git a/examples/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts index 2b022155..a902eea3 100755 --- a/examples/budget-allocator-server/server.ts +++ b/examples/budget-allocator-server/server.ts @@ -224,6 +224,30 @@ function generateHistory( return months; } +// --------------------------------------------------------------------------- +// Response Formatting +// --------------------------------------------------------------------------- + +function formatBudgetSummary(data: BudgetDataResponse): string { + const lines: string[] = [ + "Budget Allocator Configuration", + "==============================", + "", + `Default Budget: ${data.config.currencySymbol}${data.config.defaultBudget.toLocaleString()}`, + `Available Presets: ${data.config.presetBudgets.map((b) => `${data.config.currencySymbol}${b.toLocaleString()}`).join(", ")}`, + "", + "Categories:", + ...data.config.categories.map( + (c) => ` - ${c.name}: ${c.defaultPercent}% default`, + ), + "", + `Historical Data: ${data.analytics.history.length} months`, + `Benchmark Stages: ${data.analytics.stages.join(", ")}`, + `Default Stage: ${data.analytics.defaultStage}`, + ]; + return lines.join("\n"); +} + // --------------------------------------------------------------------------- // MCP Server Setup // --------------------------------------------------------------------------- @@ -277,7 +301,7 @@ export function createServer(): McpServer { content: [ { type: "text", - text: JSON.stringify(response), + text: formatBudgetSummary(response), }, ], structuredContent: response, diff --git a/examples/cohort-heatmap-server/server.ts b/examples/cohort-heatmap-server/server.ts index 39ac2ec9..6a902d6f 100644 --- a/examples/cohort-heatmap-server/server.ts +++ b/examples/cohort-heatmap-server/server.ts @@ -152,6 +152,17 @@ function generateCohortData( }; } +function formatCohortSummary(data: CohortData): string { + const avgRetention = data.cohorts + .flatMap((c) => c.cells) + .filter((cell) => cell.periodIndex > 0) + .reduce((sum, cell, _, arr) => sum + cell.retention / arr.length, 0); + + return `Cohort Analysis: ${data.cohorts.length} cohorts, ${data.periods.length} periods +Average retention: ${(avgRetention * 100).toFixed(1)}% +Metric: ${data.metric}, Period: ${data.periodType}`; +} + export function createServer(): McpServer { const server = new McpServer({ name: "Cohort Heatmap Server", @@ -181,7 +192,7 @@ export function createServer(): McpServer { ); return { - content: [{ type: "text", text: JSON.stringify(data) }], + content: [{ type: "text", text: formatCohortSummary(data) }], structuredContent: data, }; }, diff --git a/examples/scenario-modeler-server/server.ts b/examples/scenario-modeler-server/server.ts index 704c5798..6abb3523 100644 --- a/examples/scenario-modeler-server/server.ts +++ b/examples/scenario-modeler-server/server.ts @@ -250,6 +250,37 @@ const DEFAULT_INPUTS: ScenarioInputs = { fixedCosts: 30000, }; +// ============================================================================ +// Formatters for text output +// ============================================================================ + +function formatCurrency(value: number): string { + const absValue = Math.abs(value); + const sign = value < 0 ? "-" : ""; + if (absValue >= 1_000_000) { + return `${sign}$${(absValue / 1_000_000).toFixed(2)}M`; + } + if (absValue >= 1_000) { + return `${sign}$${(absValue / 1_000).toFixed(1)}K`; + } + return `${sign}$${Math.round(absValue)}`; +} + +function formatScenarioSummary( + summary: ScenarioSummary, + label: string, +): string { + return [ + `${label}:`, + ` Ending MRR: ${formatCurrency(summary.endingMRR)}`, + ` ARR: ${formatCurrency(summary.arr)}`, + ` Total Revenue: ${formatCurrency(summary.totalRevenue)}`, + ` Total Profit: ${formatCurrency(summary.totalProfit)}`, + ` MRR Growth: ${summary.mrrGrowthPct.toFixed(1)}%`, + ` Break-even: ${summary.breakEvenMonth ? `Month ${summary.breakEvenMonth}` : "Not achieved"}`, + ].join("\n"); +} + // ============================================================================ // MCP Server // ============================================================================ @@ -286,21 +317,28 @@ export function createServer(): McpServer { ? calculateScenario(args.customInputs) : undefined; - const data = { - templates: SCENARIO_TEMPLATES, - defaultInputs: DEFAULT_INPUTS, - customProjections: customScenario?.projections, - customSummary: customScenario?.summary, - }; + const text = [ + "SaaS Scenario Modeler", + "=".repeat(40), + "", + "Available Templates:", + ...SCENARIO_TEMPLATES.map( + (t) => ` ${t.icon} ${t.name}: ${t.description}`, + ), + "", + customScenario + ? formatScenarioSummary(customScenario.summary, "Custom Scenario") + : "Use customInputs parameter to compute projections for a specific scenario.", + ].join("\n"); return { - content: [ - { - type: "text", - text: JSON.stringify(data), - }, - ], - structuredContent: data, + content: [{ type: "text", text }], + structuredContent: { + templates: SCENARIO_TEMPLATES, + defaultInputs: DEFAULT_INPUTS, + customProjections: customScenario?.projections, + customSummary: customScenario?.summary, + }, }; }, );