From fd8986187e66ef39a47b7776a5900f9827ef0f3e Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 16 Dec 2025 14:30:53 +0000 Subject: [PATCH 1/4] feat: add STRUCTURED_CONTENT_ONLY support via makeToolResult helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add shared makeToolResult helper to server-utils.ts that supports both: - Legacy format: JSON-stringified text in content[0].text - New format: data in structuredContent field (when STRUCTURED_CONTENT_ONLY=true) Updated all example servers to use makeToolResult for tool results. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- examples/basic-server-react/server.ts | 6 ++--- examples/basic-server-vanillajs/server.ts | 6 ++--- examples/budget-allocator-server/server.ts | 11 ++------ examples/cohort-heatmap-server/server.ts | 6 ++--- .../customer-segmentation-server/server.ts | 6 ++--- examples/scenario-modeler-server/server.ts | 21 ++++++---------- examples/shared/server-utils.ts | 25 +++++++++++++++++++ examples/system-monitor-server/server.ts | 6 ++--- examples/threejs-server/server.ts | 11 ++------ examples/wiki-explorer-server/server.ts | 8 +++--- 10 files changed, 49 insertions(+), 57 deletions(-) diff --git a/examples/basic-server-react/server.ts b/examples/basic-server-react/server.ts index 24a07f2a..a7babbf3 100644 --- a/examples/basic-server-react/server.ts +++ b/examples/basic-server-react/server.ts @@ -4,7 +4,7 @@ import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/s import fs from "node:fs/promises"; import path from "node:path"; import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; -import { startServer } from "../shared/server-utils.js"; +import { makeToolResult, startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); const RESOURCE_URI = "ui://get-time/mcp-app.html"; @@ -32,9 +32,7 @@ function createServer(): McpServer { }, async (): Promise => { const time = new Date().toISOString(); - return { - content: [{ type: "text", text: JSON.stringify({ time }) }], - }; + return makeToolResult({ time }); }, ); diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index 0c596955..84bc5887 100644 --- a/examples/basic-server-vanillajs/server.ts +++ b/examples/basic-server-vanillajs/server.ts @@ -4,7 +4,7 @@ import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/s import fs from "node:fs/promises"; import path from "node:path"; import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; -import { startServer } from "../shared/server-utils.js"; +import { makeToolResult, startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); const RESOURCE_URI = "ui://get-time/mcp-app.html"; @@ -32,9 +32,7 @@ function createServer(): McpServer { }, async (): Promise => { const time = new Date().toISOString(); - return { - content: [{ type: "text", text: JSON.stringify({ time }) }], - }; + return makeToolResult({ time }); }, ); diff --git a/examples/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts index ed09e1c0..325da195 100755 --- a/examples/budget-allocator-server/server.ts +++ b/examples/budget-allocator-server/server.ts @@ -14,7 +14,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; -import { startServer } from "../shared/server-utils.js"; +import { makeToolResult, startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -266,14 +266,7 @@ function createServer(): McpServer { }, }; - return { - content: [ - { - type: "text", - text: JSON.stringify(response), - }, - ], - }; + return makeToolResult(response); }, ); diff --git a/examples/cohort-heatmap-server/server.ts b/examples/cohort-heatmap-server/server.ts index ca212ed6..2bb236e1 100644 --- a/examples/cohort-heatmap-server/server.ts +++ b/examples/cohort-heatmap-server/server.ts @@ -5,7 +5,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; -import { startServer } from "../shared/server-utils.js"; +import { makeToolResult, startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -173,9 +173,7 @@ function createServer(): McpServer { maxPeriods, ); - return { - content: [{ type: "text", text: JSON.stringify(data) }], - }; + return makeToolResult(data); }, ); diff --git a/examples/customer-segmentation-server/server.ts b/examples/customer-segmentation-server/server.ts index c7537822..bd0bfc0c 100644 --- a/examples/customer-segmentation-server/server.ts +++ b/examples/customer-segmentation-server/server.ts @@ -8,7 +8,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; -import { startServer } from "../shared/server-utils.js"; +import { makeToolResult, startServer } from "../shared/server-utils.js"; import { generateCustomers, generateSegmentSummaries, @@ -77,9 +77,7 @@ function createServer(): McpServer { async ({ segment }): Promise => { const data = getCustomerData(segment); - return { - content: [{ type: "text", text: JSON.stringify(data) }], - }; + return makeToolResult(data); }, ); diff --git a/examples/scenario-modeler-server/server.ts b/examples/scenario-modeler-server/server.ts index 61b705b0..6b23a6f8 100644 --- a/examples/scenario-modeler-server/server.ts +++ b/examples/scenario-modeler-server/server.ts @@ -8,7 +8,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; -import { startServer } from "../shared/server-utils.js"; +import { makeToolResult, startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -272,19 +272,12 @@ function createServer(): McpServer { ? calculateScenario(args.customInputs) : undefined; - return { - content: [ - { - type: "text", - text: JSON.stringify({ - templates: SCENARIO_TEMPLATES, - defaultInputs: DEFAULT_INPUTS, - customProjections: customScenario?.projections, - customSummary: customScenario?.summary, - }), - }, - ], - }; + return makeToolResult({ + templates: SCENARIO_TEMPLATES, + defaultInputs: DEFAULT_INPUTS, + customProjections: customScenario?.projections, + customSummary: customScenario?.summary, + }); }, ); diff --git a/examples/shared/server-utils.ts b/examples/shared/server-utils.ts index 13e6139a..3f59b47d 100644 --- a/examples/shared/server-utils.ts +++ b/examples/shared/server-utils.ts @@ -7,6 +7,7 @@ */ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; @@ -15,6 +16,30 @@ import cors from "cors"; import { randomUUID } from "node:crypto"; import type { Request, Response } from "express"; +/** + * When true, tool results use structuredContent field instead of JSON-stringified text. + * Set via STRUCTURED_CONTENT_ONLY=true environment variable. + */ +export const STRUCTURED_CONTENT_ONLY = + process.env.STRUCTURED_CONTENT_ONLY === "true"; + +/** + * Helper to create a tool result that optionally uses structuredContent. + * When STRUCTURED_CONTENT_ONLY is true, returns data in structuredContent field. + * Otherwise returns JSON-stringified data in content[0].text (legacy format). + */ +export function makeToolResult(data: Record): CallToolResult { + if (STRUCTURED_CONTENT_ONLY) { + return { + content: [], + structuredContent: data, + }; + } + return { + content: [{ type: "text", text: JSON.stringify(data) }], + }; +} + export interface ServerOptions { /** Port to listen on (required). */ port: number; diff --git a/examples/system-monitor-server/server.ts b/examples/system-monitor-server/server.ts index 30687edb..784d87d8 100644 --- a/examples/system-monitor-server/server.ts +++ b/examples/system-monitor-server/server.ts @@ -10,7 +10,7 @@ import path from "node:path"; import si from "systeminformation"; import { z } from "zod"; import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; -import { startServer } from "../shared/server-utils.js"; +import { makeToolResult, startServer } from "../shared/server-utils.js"; // Schemas - types are derived from these using z.infer const CpuCoreSchema = z.object({ @@ -143,9 +143,7 @@ function createServer(): McpServer { timestamp: new Date().toISOString(), }; - return { - content: [{ type: "text", text: JSON.stringify(stats) }], - }; + return makeToolResult(stats); }, ); diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts index 72f19b43..653c9816 100644 --- a/examples/threejs-server/server.ts +++ b/examples/threejs-server/server.ts @@ -10,7 +10,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; -import { startServer } from "../shared/server-utils.js"; +import { makeToolResult, startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -163,14 +163,7 @@ function createServer(): McpServer { _meta: { [RESOURCE_URI_META_KEY]: resourceUri }, }, async ({ code, height }) => { - return { - content: [ - { - type: "text", - text: JSON.stringify({ code, height }), - }, - ], - }; + return makeToolResult({ code, height }); }, ); diff --git a/examples/wiki-explorer-server/server.ts b/examples/wiki-explorer-server/server.ts index 4ee56225..71c42cec 100644 --- a/examples/wiki-explorer-server/server.ts +++ b/examples/wiki-explorer-server/server.ts @@ -9,7 +9,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app"; -import { startServer } from "../shared/server-utils.js"; +import { makeToolResult, startServer } from "../shared/server-utils.js"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -114,12 +114,10 @@ function createServer(): McpServer { const html = await response.text(); const links = extractWikiLinks(new URL(url), html); - const result = { page: { url, title }, links, error: null }; - return { content: [{ type: "text", text: JSON.stringify(result) }] }; + return makeToolResult({ page: { url, title }, links, error: null }); } 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 makeToolResult({ page: { url, title }, links: [], error }); } }, ); From f65909b4cd4941445c48cdfefa2d0bbc9b77f342 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 16 Dec 2025 15:13:12 +0000 Subject: [PATCH 2/4] fix(examples): support structuredContent in client apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update all example app clients to handle STRUCTURED_CONTENT_ONLY mode. When structuredContent is present in tool results, use it directly instead of parsing JSON from content text. Affected apps: - basic-server-vanillajs - budget-allocator-server - customer-segmentation-server - system-monitor-server - wiki-explorer-server 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../basic-server-vanillajs/src/mcp-app.ts | 18 +++++++++----- .../budget-allocator-server/src/mcp-app.ts | 24 +++++++++++++------ .../src/mcp-app.ts | 23 ++++++++++-------- examples/system-monitor-server/src/mcp-app.ts | 20 ++++++++++------ examples/wiki-explorer-server/src/mcp-app.ts | 17 ++++++++----- 5 files changed, 66 insertions(+), 36 deletions(-) diff --git a/examples/basic-server-vanillajs/src/mcp-app.ts b/examples/basic-server-vanillajs/src/mcp-app.ts index 7bfa6d69..875a7e4c 100644 --- a/examples/basic-server-vanillajs/src/mcp-app.ts +++ b/examples/basic-server-vanillajs/src/mcp-app.ts @@ -15,12 +15,18 @@ const log = { function extractTime(result: CallToolResult): string { - const text = result.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 }; - return time; + // Prefer structuredContent (STRUCTURED_CONTENT_ONLY mode), fall back to parsing text + let data: { time: string }; + if (result.structuredContent) { + data = result.structuredContent as { time: string }; + } else { + const text = result.content! + .filter((c): c is { type: "text"; text: string } => c.type === "text") + .map((c) => c.text) + .join(""); + data = JSON.parse(text) as { time: string }; + } + return data.time; } diff --git a/examples/budget-allocator-server/src/mcp-app.ts b/examples/budget-allocator-server/src/mcp-app.ts index 723dc060..46bf2450 100644 --- a/examples/budget-allocator-server/src/mcp-app.ts +++ b/examples/budget-allocator-server/src/mcp-app.ts @@ -606,13 +606,23 @@ 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; + + // Prefer structuredContent (STRUCTURED_CONTENT_ONLY mode), fall back to parsing text + let data: BudgetDataResponse | undefined; + if (result.structuredContent) { + data = result.structuredContent as BudgetDataResponse; + } else { + const text = result + .content!.filter( + (c): c is { type: "text"; text: string } => c.type === "text", + ) + .map((c) => c.text) + .join(""); + if (text) { + data = JSON.parse(text) as BudgetDataResponse; + } + } + if (data?.config && data?.analytics) { initializeUI(data.config, data.analytics); } diff --git a/examples/customer-segmentation-server/src/mcp-app.ts b/examples/customer-segmentation-server/src/mcp-app.ts index 77e348de..0685dd5d 100644 --- a/examples/customer-segmentation-server/src/mcp-app.ts +++ b/examples/customer-segmentation-server/src/mcp-app.ts @@ -375,16 +375,19 @@ async function fetchData(): Promise { 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[]; - }; + // Prefer structuredContent (STRUCTURED_CONTENT_ONLY mode), fall back to parsing text + let data: { customers: Customer[]; segments: SegmentSummary[] }; + if (result.structuredContent) { + data = result.structuredContent as typeof data; + } else { + const text = result + .content!.filter( + (c): c is { type: "text"; text: string } => c.type === "text", + ) + .map((c) => c.text) + .join(""); + data = JSON.parse(text); + } state.customers = data.customers; state.segments = data.segments; diff --git a/examples/system-monitor-server/src/mcp-app.ts b/examples/system-monitor-server/src/mcp-app.ts index 19e53c6a..9d6caad3 100644 --- a/examples/system-monitor-server/src/mcp-app.ts +++ b/examples/system-monitor-server/src/mcp-app.ts @@ -264,13 +264,19 @@ 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; + // Prefer structuredContent (STRUCTURED_CONTENT_ONLY mode), fall back to parsing text + let stats: SystemStats; + if (result.structuredContent) { + stats = result.structuredContent as SystemStats; + } else { + const text = result + .content!.filter( + (c): c is { type: "text"; text: string } => c.type === "text", + ) + .map((c) => c.text) + .join(""); + stats = JSON.parse(text); + } // Initialize chart on first data if needed if (!state.chart && stats.cpu.count > 0) { diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts index 8f9026c7..3c7067c4 100644 --- a/examples/wiki-explorer-server/src/mcp-app.ts +++ b/examples/wiki-explorer-server/src/mcp-app.ts @@ -322,17 +322,22 @@ 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); + // Prefer structuredContent (STRUCTURED_CONTENT_ONLY mode), fall back to parsing text + let response: ToolResponse; + if (result.structuredContent) { + response = result.structuredContent as ToolResponse; + } else if (result.content?.[0]?.type === "text") { + response = JSON.parse(result.content[0].text); + } else { + console.error("No valid content in result:", result); + return; + } const { page, links, error } = response; // Ensure the source node exists From a0412cca22a886e6eb92de64119110215be1485a Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 16 Dec 2025 16:59:13 +0000 Subject: [PATCH 3/4] fix(examples): add robust JSON parse error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add try/catch around JSON.parse in all example apps - Log parse errors with context for debugging - Add debug logging to makeToolResult helper 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../basic-server-vanillajs/src/mcp-app.ts | 7 +++++- .../budget-allocator-server/src/mcp-app.ts | 12 +++++++++- .../src/mcp-app.ts | 7 +++++- examples/shared/server-utils.ts | 2 ++ examples/system-monitor-server/src/mcp-app.ts | 7 +++++- examples/wiki-explorer-server/src/mcp-app.ts | 22 ++++++++++++------- 6 files changed, 45 insertions(+), 12 deletions(-) diff --git a/examples/basic-server-vanillajs/src/mcp-app.ts b/examples/basic-server-vanillajs/src/mcp-app.ts index 875a7e4c..997dfdd4 100644 --- a/examples/basic-server-vanillajs/src/mcp-app.ts +++ b/examples/basic-server-vanillajs/src/mcp-app.ts @@ -24,7 +24,12 @@ function extractTime(result: CallToolResult): string { .filter((c): c is { type: "text"; text: string } => c.type === "text") .map((c) => c.text) .join(""); - data = JSON.parse(text) as { time: string }; + try { + data = JSON.parse(text) as { time: string }; + } catch (e) { + log.error("Failed to parse tool result:", text, e); + return "[PARSE ERROR]"; + } } return data.time; } diff --git a/examples/budget-allocator-server/src/mcp-app.ts b/examples/budget-allocator-server/src/mcp-app.ts index 46bf2450..b385069c 100644 --- a/examples/budget-allocator-server/src/mcp-app.ts +++ b/examples/budget-allocator-server/src/mcp-app.ts @@ -607,6 +607,11 @@ const app = new App({ name: "Budget Allocator", version: "1.0.0" }); app.ontoolresult = (result) => { log.info("Received tool result:", result); + if (result.isError) { + log.error("Tool returned error:", result); + return; + } + // Prefer structuredContent (STRUCTURED_CONTENT_ONLY mode), fall back to parsing text let data: BudgetDataResponse | undefined; if (result.structuredContent) { @@ -619,7 +624,12 @@ app.ontoolresult = (result) => { .map((c) => c.text) .join(""); if (text) { - data = JSON.parse(text) as BudgetDataResponse; + try { + data = JSON.parse(text) as BudgetDataResponse; + } catch (e) { + log.error("Failed to parse tool result:", text, e); + return; + } } } diff --git a/examples/customer-segmentation-server/src/mcp-app.ts b/examples/customer-segmentation-server/src/mcp-app.ts index 0685dd5d..47add033 100644 --- a/examples/customer-segmentation-server/src/mcp-app.ts +++ b/examples/customer-segmentation-server/src/mcp-app.ts @@ -386,7 +386,12 @@ async function fetchData(): Promise { ) .map((c) => c.text) .join(""); - data = JSON.parse(text); + try { + data = JSON.parse(text); + } catch (e) { + log.error("Failed to parse tool result:", text, e); + return; + } } state.customers = data.customers; diff --git a/examples/shared/server-utils.ts b/examples/shared/server-utils.ts index 3f59b47d..321048da 100644 --- a/examples/shared/server-utils.ts +++ b/examples/shared/server-utils.ts @@ -30,11 +30,13 @@ export const STRUCTURED_CONTENT_ONLY = */ export function makeToolResult(data: Record): CallToolResult { if (STRUCTURED_CONTENT_ONLY) { + console.log("[makeToolResult] STRUCTURED_CONTENT_ONLY mode:", { structuredContent: data }); return { content: [], structuredContent: data, }; } + console.log("[makeToolResult] Legacy mode:", { text: JSON.stringify(data).slice(0, 100) + "..." }); return { content: [{ type: "text", text: JSON.stringify(data) }], }; diff --git a/examples/system-monitor-server/src/mcp-app.ts b/examples/system-monitor-server/src/mcp-app.ts index 9d6caad3..fbaeae74 100644 --- a/examples/system-monitor-server/src/mcp-app.ts +++ b/examples/system-monitor-server/src/mcp-app.ts @@ -275,7 +275,12 @@ async function fetchStats(): Promise { ) .map((c) => c.text) .join(""); - stats = JSON.parse(text); + try { + stats = JSON.parse(text); + } catch (e) { + log.error("Failed to parse tool result:", text, e); + return; + } } // Initialize chart on first data if needed diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts index 3c7067c4..b6673830 100644 --- a/examples/wiki-explorer-server/src/mcp-app.ts +++ b/examples/wiki-explorer-server/src/mcp-app.ts @@ -327,17 +327,23 @@ function handleToolResultData(result: CallToolResult): void { return; } - try { - // Prefer structuredContent (STRUCTURED_CONTENT_ONLY mode), fall back to parsing text - let response: ToolResponse; - if (result.structuredContent) { - response = result.structuredContent as ToolResponse; - } else if (result.content?.[0]?.type === "text") { + // Prefer structuredContent (STRUCTURED_CONTENT_ONLY mode), fall back to parsing text + let response: ToolResponse; + if (result.structuredContent) { + response = result.structuredContent as ToolResponse; + } else if (result.content?.[0]?.type === "text") { + try { response = JSON.parse(result.content[0].text); - } else { - console.error("No valid content in result:", result); + } catch (e) { + console.error("Failed to parse tool result:", result.content[0].text, e); return; } + } else { + console.error("No valid content in result:", result); + return; + } + + try { const { page, links, error } = response; // Ensure the source node exists From 375c85800b4b7a1f65fe62bb8f2b245ac2df4c46 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 16 Dec 2025 23:38:46 +0000 Subject: [PATCH 4/4] fix(threejs-server): use server.registerTool for non-UI tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The learn_threejs tool doesn't have a UI component - it only returns documentation text. Using registerAppTool without _meta causes a runtime error when the function tries to access config._meta.ui. Fixes crash: "Cannot read property 'ui' of undefined" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- examples/threejs-server/server.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts index 95b711ef..2a891a22 100644 --- a/examples/threejs-server/server.ts +++ b/examples/threejs-server/server.ts @@ -173,9 +173,8 @@ function createServer(): McpServer { }, ); - // Tool 2: learn_threejs - registerAppTool( - server, + // Tool 2: learn_threejs (not a UI tool, just returns documentation) + server.registerTool( "learn_threejs", { title: "Learn Three.js",