From 610fb00d9d37b1951a4fddff03c882306e1e5937 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 14 Jan 2026 23:03:04 +0000 Subject: [PATCH 01/10] feat(examples): Add debug-server for comprehensive SDK testing Add a debug-server example that exercises all MCP Apps SDK capabilities: Server (server.ts): - debug-tool: Configurable tool testing all content types (text, image, audio, resource, resourceLink, mixed), with options for multiple blocks, structuredContent, _meta, error simulation, and delays - debug-refresh: App-only tool (hidden from model) for polling server state Guest UI (src/mcp-app.ts): - Event Log: Real-time log of all SDK events with filtering and timestamps - Host Info: Display of context, capabilities, container dimensions, styles - Callback Status: Table showing all registered callbacks with call counts - Action Buttons: Test every SDK method: - Messages (text and image) - Logging (debug/info/warning/error) - Model context updates (text and structured) - Display mode requests (inline/fullscreen/pip) - Link opening - Manual/auto resize controls - Server tool calls with full configuration - File upload and URL retrieval This example serves as both a testing tool and reference implementation for all SDK features. --- examples/debug-server/mcp-app.html | 225 ++++++++++ examples/debug-server/package.json | 43 ++ examples/debug-server/server-utils.ts | 72 ++++ examples/debug-server/server.ts | 219 ++++++++++ examples/debug-server/src/global.css | 33 ++ examples/debug-server/src/mcp-app.css | 332 +++++++++++++++ examples/debug-server/src/mcp-app.ts | 585 ++++++++++++++++++++++++++ examples/debug-server/tsconfig.json | 19 + examples/debug-server/vite.config.ts | 24 ++ 9 files changed, 1552 insertions(+) create mode 100644 examples/debug-server/mcp-app.html create mode 100644 examples/debug-server/package.json create mode 100644 examples/debug-server/server-utils.ts create mode 100644 examples/debug-server/server.ts create mode 100644 examples/debug-server/src/global.css create mode 100644 examples/debug-server/src/mcp-app.css create mode 100644 examples/debug-server/src/mcp-app.ts create mode 100644 examples/debug-server/tsconfig.json create mode 100644 examples/debug-server/vite.config.ts diff --git a/examples/debug-server/mcp-app.html b/examples/debug-server/mcp-app.html new file mode 100644 index 00000000..6a5ca667 --- /dev/null +++ b/examples/debug-server/mcp-app.html @@ -0,0 +1,225 @@ + + + + + + + Debug App + + +
+ +
+

+ Event Log +
+ + +
+

+
+
+ + +
+

+ Host Info + +

+
+
+
+

Context

+
+
+
+

Capabilities

+
+
+
+

Container

+
+
+
+

Styles Sample

+
+
+
+
+
+ + +
+

+ Callback Status + +

+
+ + + + + + + + + + +
CallbackRegisteredCountLast Payload
+
+
+ + +
+

+ Actions + +

+
+ +
+

Messages

+
+ + +
+
+ +
+
+ + +
+

Logging

+
+ +
+
+ + + + +
+
+ + +
+

Model Context

+
+ + +
+
+ +
+
+ + +
+

Display Mode

+
+ + + +
+
+ + +
+

Links

+
+ + +
+
+ + +
+

Size

+
+ +
+
+ + + +
+
+ Current: measuring... +
+
+ + +
+

Server Tools

+
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ +
+
+ +
+
+ + +
+

Files

+
+ +
+
+ +
+
+ Last fileId: none +
+
+ +
+
+
+
+
+ + + diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json new file mode 100644 index 00000000..c2d85c3a --- /dev/null +++ b/examples/debug-server/package.json @@ -0,0 +1,43 @@ +{ + "name": "@modelcontextprotocol/server-debug", + "version": "0.4.0", + "type": "module", + "description": "Debug MCP App Server for testing all SDK capabilities", + "repository": { + "type": "git", + "url": "https://github.com/modelcontextprotocol/ext-apps", + "directory": "examples/debug-server" + }, + "license": "MIT", + "main": "server.ts", + "files": [ + "server.ts", + "server-utils.ts", + "dist" + ], + "scripts": { + "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build", + "watch": "cross-env INPUT=mcp-app.html vite build --watch", + "serve": "bun --watch server.ts", + "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "^0.4.0", + "@modelcontextprotocol/sdk": "^1.24.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "concurrently": "^9.2.1", + "cors": "^2.8.5", + "cross-env": "^10.1.0", + "express": "^5.1.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + } +} diff --git a/examples/debug-server/server-utils.ts b/examples/debug-server/server-utils.ts new file mode 100644 index 00000000..9fe9745a --- /dev/null +++ b/examples/debug-server/server-utils.ts @@ -0,0 +1,72 @@ +/** + * Shared utilities for running MCP servers with Streamable HTTP transport. + */ + +import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import cors from "cors"; +import type { Request, Response } from "express"; + +export interface ServerOptions { + port: number; + name?: string; +} + +/** + * Starts an MCP server with Streamable HTTP transport in stateless mode. + * + * @param createServer - Factory function that creates a new McpServer instance per request. + * @param options - Server configuration options. + */ +export async function startServer( + createServer: () => McpServer, + options: ServerOptions, +): Promise { + const { port, name = "MCP Server" } = options; + + const app = createMcpExpressApp({ host: "0.0.0.0" }); + app.use(cors()); + + app.all("/mcp", async (req: Request, res: Response) => { + const server = createServer(); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + + res.on("close", () => { + transport.close().catch(() => {}); + server.close().catch(() => {}); + }); + + try { + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + } catch (error) { + console.error("MCP error:", error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: "2.0", + error: { code: -32603, message: "Internal server error" }, + id: null, + }); + } + } + }); + + const httpServer = app.listen(port, (err) => { + if (err) { + console.error("Failed to start server:", err); + process.exit(1); + } + console.log(`${name} listening on http://localhost:${port}/mcp`); + }); + + const shutdown = () => { + console.log("\nShutting down..."); + httpServer.close(() => process.exit(0)); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} diff --git a/examples/debug-server/server.ts b/examples/debug-server/server.ts new file mode 100644 index 00000000..9a67fd46 --- /dev/null +++ b/examples/debug-server/server.ts @@ -0,0 +1,219 @@ +import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +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 { startServer } from "./server-utils.js"; + +const DIST_DIR = path.join(import.meta.dirname, "dist"); + +// Track call counter across requests (stateful for demo purposes) +let callCounter = 0; + +// Minimal 1x1 blue PNG (base64) +const BLUE_PNG_1X1 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPj/HwADBwIAMCbHYQAAAABJRU5ErkJggg=="; + +// Minimal silent WAV (base64) - 44 byte header + 1 sample +const SILENT_WAV = "UklGRiYAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQIAAAAAAA=="; + +/** + * Input schema for the debug-tool + */ +const DebugInputSchema = z.object({ + // Content configuration + contentType: z.enum(["text", "image", "audio", "resource", "resourceLink", "mixed"]).default("text"), + multipleBlocks: z.boolean().default(false), + includeStructuredContent: z.boolean().default(true), + includeMeta: z.boolean().default(false), + + // Streaming test (large input) + largeInput: z.string().optional(), + + // Error/delay simulation + simulateError: z.boolean().default(false), + delayMs: z.number().optional(), +}); + +type DebugInput = z.infer; + +/** + * Output schema for structured content + */ +const DebugOutputSchema = z.object({ + config: z.record(z.string(), z.unknown()), + timestamp: z.string(), + counter: z.number(), + largeInputLength: z.number().optional(), +}); + +/** + * Builds content blocks based on configuration + */ +function buildContent(args: DebugInput): CallToolResult["content"] { + const count = args.multipleBlocks ? 3 : 1; + const content: CallToolResult["content"] = []; + + for (let i = 0; i < count; i++) { + const suffix = args.multipleBlocks ? ` #${i + 1}` : ""; + + switch (args.contentType) { + case "text": + content.push({ type: "text", text: `Debug text content${suffix}` }); + break; + case "image": + content.push({ type: "image", data: BLUE_PNG_1X1, mimeType: "image/png" }); + break; + case "audio": + content.push({ type: "audio", data: SILENT_WAV, mimeType: "audio/wav" }); + break; + case "resource": + content.push({ + type: "resource", + resource: { + uri: `debug://embedded-resource${suffix.replace(/\s/g, "-")}`, + text: `Embedded resource content${suffix}`, + mimeType: "text/plain", + }, + }); + break; + case "resourceLink": + content.push({ + type: "resource_link", + uri: `debug://linked-resource${suffix.replace(/\s/g, "-")}`, + name: `Linked Resource${suffix}`, + mimeType: "text/plain", + }); + break; + case "mixed": + // Return one of each type (ignore multipleBlocks for mixed) + return [ + { type: "text", text: "Mixed content: text block" }, + { type: "image", data: BLUE_PNG_1X1, mimeType: "image/png" }, + { type: "audio", data: SILENT_WAV, mimeType: "audio/wav" }, + ]; + } + } + + return content; +} + +/** + * Creates a new MCP server instance with debug tools registered. + */ +export function createServer(): McpServer { + const server = new McpServer({ + name: "Debug MCP App Server", + version: "1.0.0", + }); + + const resourceUri = "ui://debug-tool/mcp-app.html"; + + // Main debug tool - exercises all result variations + registerAppTool(server, + "debug-tool", + { + title: "Debug Tool", + description: "Comprehensive debug tool for testing MCP Apps SDK. Configure content types, error simulation, delays, and more.", + inputSchema: DebugInputSchema, + outputSchema: DebugOutputSchema, + _meta: { ui: { resourceUri } }, + }, + async (args): Promise => { + // Apply delay if requested + if (args.delayMs && args.delayMs > 0) { + await new Promise(resolve => setTimeout(resolve, args.delayMs)); + } + + // Build content based on config + const content = buildContent(args); + + // Build result + const result: CallToolResult = { content }; + + // Add structured content if requested + if (args.includeStructuredContent) { + result.structuredContent = { + config: args, + timestamp: new Date().toISOString(), + counter: ++callCounter, + ...(args.largeInput ? { largeInputLength: args.largeInput.length } : {}), + }; + } + + // Add _meta if requested + if (args.includeMeta) { + result._meta = { + debugInfo: { + processedAt: Date.now(), + serverVersion: "1.0.0", + }, + }; + } + + // Set error flag if requested + if (args.simulateError) { + result.isError = true; + } + + return result; + }, + ); + + // App-only refresh tool (hidden from model) + registerAppTool(server, + "debug-refresh", + { + title: "Refresh Debug Info", + description: "App-only tool for polling server state. Not visible to the model.", + inputSchema: z.object({}), + outputSchema: z.object({ timestamp: z.string(), counter: z.number() }), + _meta: { + ui: { + resourceUri, + visibility: ["app"], + }, + }, + }, + async (): Promise => { + const timestamp = new Date().toISOString(); + return { + content: [{ type: "text", text: `Server timestamp: ${timestamp}` }], + structuredContent: { timestamp, counter: callCounter }, + }; + }, + ); + + // Register the resource which returns the bundled HTML/JavaScript for the UI + registerAppResource(server, + resourceUri, + resourceUri, + { mimeType: RESOURCE_MIME_TYPE }, + async (): Promise => { + const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8"); + + return { + contents: [ + { uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }, + ], + }; + }, + ); + + return server; +} + +async function main() { + if (process.argv.includes("--stdio")) { + await createServer().connect(new StdioServerTransport()); + } else { + const port = parseInt(process.env.PORT ?? "3102", 10); + await startServer(createServer, { port, name: "Debug MCP App Server" }); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/examples/debug-server/src/global.css b/examples/debug-server/src/global.css new file mode 100644 index 00000000..18863262 --- /dev/null +++ b/examples/debug-server/src/global.css @@ -0,0 +1,33 @@ +* { + box-sizing: border-box; +} + +html, body { + font-family: system-ui, -apple-system, sans-serif; + font-size: 14px; + margin: 0; + padding: 0; +} + +code { + font-family: ui-monospace, "SF Mono", Monaco, "Cascadia Code", Consolas, monospace; + font-size: 0.9em; + background: rgba(0, 0, 0, 0.05); + padding: 0.1em 0.3em; + border-radius: 3px; +} + +@media (prefers-color-scheme: dark) { + code { + background: rgba(255, 255, 255, 0.1); + } +} + +button { + cursor: pointer; +} + +input, select, button { + font-family: inherit; + font-size: inherit; +} diff --git a/examples/debug-server/src/mcp-app.css b/examples/debug-server/src/mcp-app.css new file mode 100644 index 00000000..c9d4509c --- /dev/null +++ b/examples/debug-server/src/mcp-app.css @@ -0,0 +1,332 @@ +.main { + --color-primary: #2563eb; + --color-primary-hover: #1d4ed8; + --color-success: #16a34a; + --color-warning: #ca8a04; + --color-error: #dc2626; + --color-border: #e5e7eb; + --color-bg-subtle: #f9fafb; + + width: 100%; + max-width: 800px; + padding: 1rem; + margin: 0 auto; +} + +@media (prefers-color-scheme: dark) { + .main { + --color-border: #374151; + --color-bg-subtle: #1f2937; + } +} + +/* Section styling */ +.section { + border: 1px solid var(--color-border); + border-radius: 8px; + margin-bottom: 1rem; + overflow: hidden; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + margin: 0; + font-size: 1rem; + font-weight: 600; + background: var(--color-bg-subtle); + border-bottom: 1px solid var(--color-border); +} + +.collapsible .section-header { + cursor: pointer; + user-select: none; +} + +.collapsible .section-header:hover { + background: var(--color-border); +} + +.toggle-icon { + transition: transform 0.2s; +} + +.collapsed .toggle-icon { + transform: rotate(-90deg); +} + +.collapsed .section-content { + display: none; +} + +.section-content { + padding: 1rem; +} + +.header-actions { + display: flex; + gap: 0.5rem; + align-items: center; +} + +/* Event log */ +.event-log { + max-height: 200px; + overflow-y: auto; + padding: 0.5rem; + font-family: ui-monospace, monospace; + font-size: 0.85rem; + background: var(--color-bg-subtle); +} + +.log-entry { + padding: 0.25rem 0; + border-bottom: 1px solid var(--color-border); +} + +.log-entry:last-child { + border-bottom: none; +} + +.log-time { + color: #6b7280; + margin-right: 0.5rem; +} + +.log-type { + font-weight: 600; + margin-right: 0.5rem; +} + +.log-type.tool-input { color: var(--color-primary); } +.log-type.tool-input-partial { color: #8b5cf6; } +.log-type.tool-result { color: var(--color-success); } +.log-type.tool-cancelled { color: var(--color-warning); } +.log-type.widget-state { color: #0891b2; } +.log-type.host-context-changed { color: #7c3aed; } +.log-type.teardown { color: #f97316; } +.log-type.call-tool { color: #ec4899; } +.log-type.list-tools { color: #14b8a6; } +.log-type.error { color: var(--color-error); } + +.log-payload { + color: #6b7280; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 400px; + display: inline-block; + vertical-align: bottom; + cursor: pointer; +} + +.log-payload:hover { + white-space: normal; + word-break: break-all; +} + +/* Info grid */ +.info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; +} + +.info-group h3 { + margin: 0 0 0.5rem 0; + font-size: 0.9rem; + color: #6b7280; +} + +.info-group dl { + margin: 0; + font-size: 0.85rem; +} + +.info-group dt { + font-weight: 600; + color: #374151; +} + +.info-group dd { + margin: 0 0 0.5rem 0; + color: #6b7280; +} + +@media (prefers-color-scheme: dark) { + .info-group dt { color: #d1d5db; } + .info-group dd { color: #9ca3af; } +} + +.styles-sample { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.style-swatch { + width: 24px; + height: 24px; + border-radius: 4px; + border: 1px solid var(--color-border); + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + color: white; + text-shadow: 0 0 2px black; +} + +/* Callback table */ +.callback-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; +} + +.callback-table th, +.callback-table td { + padding: 0.5rem; + text-align: left; + border-bottom: 1px solid var(--color-border); +} + +.callback-table th { + background: var(--color-bg-subtle); + font-weight: 600; +} + +.callback-table .registered-yes { + color: var(--color-success); +} + +.callback-table .registered-no { + color: #9ca3af; +} + +.callback-table .payload-preview { + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + font-family: ui-monospace, monospace; + font-size: 0.8rem; +} + +.callback-table .payload-preview:hover { + white-space: normal; + word-break: break-all; +} + +/* Action groups */ +.action-group { + margin-bottom: 1.5rem; +} + +.action-group:last-child { + margin-bottom: 0; +} + +.action-group h3 { + margin: 0 0 0.75rem 0; + font-size: 0.9rem; + font-weight: 600; + color: #374151; + border-bottom: 1px solid var(--color-border); + padding-bottom: 0.25rem; +} + +@media (prefers-color-scheme: dark) { + .action-group h3 { color: #d1d5db; } +} + +.action-row { + display: flex; + gap: 0.5rem; + align-items: center; + margin-bottom: 0.5rem; +} + +.action-row:last-child { + margin-bottom: 0; +} + +.action-row input[type="text"], +.action-row input[type="url"], +.action-row input[type="number"] { + flex: 1; + padding: 0.5rem; + border: 1px solid var(--color-border); + border-radius: 4px; + background: transparent; +} + +.action-row input[type="file"] { + flex: 1; +} + +.action-row button { + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + background: var(--color-primary); + color: white; + font-weight: 500; +} + +.action-row button:hover { + background: var(--color-primary-hover); +} + +.btn-row { + flex-wrap: wrap; +} + +.btn-small { + padding: 0.375rem 0.75rem !important; + font-size: 0.85rem; +} + +/* Tool config */ +.tool-config { + background: var(--color-bg-subtle); + padding: 0.75rem; + border-radius: 4px; + margin-bottom: 0.75rem; +} + +.config-row { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.config-row:last-child { + margin-bottom: 0; +} + +.config-row label { + min-width: 120px; + font-size: 0.85rem; +} + +.config-row select, +.config-row input[type="number"] { + padding: 0.25rem 0.5rem; + border: 1px solid var(--color-border); + border-radius: 4px; + background: transparent; +} + +/* Filter select */ +#log-filter { + padding: 0.25rem 0.5rem; + border: 1px solid var(--color-border); + border-radius: 4px; + background: transparent; + font-size: 0.85rem; +} diff --git a/examples/debug-server/src/mcp-app.ts b/examples/debug-server/src/mcp-app.ts new file mode 100644 index 00000000..1d0fbd77 --- /dev/null +++ b/examples/debug-server/src/mcp-app.ts @@ -0,0 +1,585 @@ +/** + * @file Debug App - Comprehensive testing/debugging tool for the MCP Apps SDK. + * + * This app exercises every capability, callback, and result format combination. + */ +import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps"; +import "./global.css"; +import "./mcp-app.css"; + +// ============================================================================ +// Types +// ============================================================================ + +interface LogEntry { + time: number; + type: string; + payload: unknown; +} + +interface AppState { + eventLog: LogEntry[]; + callbackCounts: Map; + lastPayloads: Map; + uploadedFileId: string | null; + autoResizeCleanup: (() => void) | null; + logFilter: string; +} + +// ============================================================================ +// State +// ============================================================================ + +const state: AppState = { + eventLog: [], + callbackCounts: new Map(), + lastPayloads: new Map(), + uploadedFileId: null, + autoResizeCleanup: null, + logFilter: "all", +}; + +// Callbacks we track +const CALLBACKS = [ + "ontoolinput", + "ontoolinputpartial", + "ontoolresult", + "ontoolcancelled", + "onwidgetstate", + "onhostcontextchanged", + "onteardown", + "oncalltool", + "onlisttools", + "onerror", +] as const; + +// ============================================================================ +// DOM Elements +// ============================================================================ + +const mainEl = document.querySelector(".main") as HTMLElement; +const eventLogEl = document.getElementById("event-log")!; +const logFilterEl = document.getElementById("log-filter") as HTMLSelectElement; +const clearLogBtn = document.getElementById("clear-log-btn")!; + +// Host info +const hostContextInfoEl = document.getElementById("host-context-info")!; +const hostCapabilitiesInfoEl = document.getElementById("host-capabilities-info")!; +const hostContainerInfoEl = document.getElementById("host-container-info")!; +const hostStylesSampleEl = document.getElementById("host-styles-sample")!; + +// Callback status +const callbackTableBodyEl = document.getElementById("callback-table-body")!; + +// Action elements +const messageTextEl = document.getElementById("message-text") as HTMLInputElement; +const sendMessageTextBtn = document.getElementById("send-message-text-btn")!; +const sendMessageImageBtn = document.getElementById("send-message-image-btn")!; + +const logDataEl = document.getElementById("log-data") as HTMLInputElement; +const logDebugBtn = document.getElementById("log-debug-btn")!; +const logInfoBtn = document.getElementById("log-info-btn")!; +const logWarningBtn = document.getElementById("log-warning-btn")!; +const logErrorBtn = document.getElementById("log-error-btn")!; + +const contextTextEl = document.getElementById("context-text") as HTMLInputElement; +const updateContextTextBtn = document.getElementById("update-context-text-btn")!; +const updateContextStructuredBtn = document.getElementById("update-context-structured-btn")!; + +const displayInlineBtn = document.getElementById("display-inline-btn")!; +const displayFullscreenBtn = document.getElementById("display-fullscreen-btn")!; +const displayPipBtn = document.getElementById("display-pip-btn")!; + +const linkUrlEl = document.getElementById("link-url") as HTMLInputElement; +const openLinkBtn = document.getElementById("open-link-btn")!; + +const autoResizeToggleEl = document.getElementById("auto-resize-toggle") as HTMLInputElement; +const resize200x100Btn = document.getElementById("resize-200x100-btn")!; +const resize400x300Btn = document.getElementById("resize-400x300-btn")!; +const resize800x600Btn = document.getElementById("resize-800x600-btn")!; +const currentSizeEl = document.getElementById("current-size")!; + +// Tool config elements +const toolContentTypeEl = document.getElementById("tool-content-type") as HTMLSelectElement; +const toolMultipleBlocksEl = document.getElementById("tool-multiple-blocks") as HTMLInputElement; +const toolStructuredContentEl = document.getElementById("tool-structured-content") as HTMLInputElement; +const toolIncludeMetaEl = document.getElementById("tool-include-meta") as HTMLInputElement; +const toolSimulateErrorEl = document.getElementById("tool-simulate-error") as HTMLInputElement; +const toolDelayMsEl = document.getElementById("tool-delay-ms") as HTMLInputElement; +const callDebugToolBtn = document.getElementById("call-debug-tool-btn")!; +const callDebugRefreshBtn = document.getElementById("call-debug-refresh-btn")!; + +// File elements +const fileInputEl = document.getElementById("file-input") as HTMLInputElement; +const uploadFileBtn = document.getElementById("upload-file-btn")!; +const lastFileIdEl = document.getElementById("last-file-id")!; +const getFileUrlBtn = document.getElementById("get-file-url-btn")!; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +function formatTime(timestamp: number): string { + const date = new Date(timestamp); + const h = date.getHours().toString().padStart(2, "0"); + const m = date.getMinutes().toString().padStart(2, "0"); + const s = date.getSeconds().toString().padStart(2, "0"); + const ms = date.getMilliseconds().toString().padStart(3, "0"); + return `${h}:${m}:${s}.${ms}`; +} + +function truncatePayload(payload: unknown): string { + const str = JSON.stringify(payload); + if (str.length > 100) { + return str.slice(0, 100) + "..."; + } + return str; +} + +// ============================================================================ +// Rendering Functions +// ============================================================================ + +function renderEventLog(): void { + const filtered = state.logFilter === "all" + ? state.eventLog + : state.eventLog.filter(e => e.type === state.logFilter); + + eventLogEl.innerHTML = filtered.map(entry => ` +
+ [${formatTime(entry.time)}] + ${entry.type}: + ${truncatePayload(entry.payload)} +
+ `).join(""); + + // Auto-scroll to bottom + eventLogEl.scrollTop = eventLogEl.scrollHeight; +} + +function renderCallbackStatus(): void { + callbackTableBodyEl.innerHTML = CALLBACKS.map(name => { + const count = state.callbackCounts.get(name) ?? 0; + const lastPayload = state.lastPayloads.get(name); + const registered = name !== "onerror"; // All callbacks are registered + + return ` + + ${name} + ${registered ? "✓" : "✗"} + ${count} + ${lastPayload ? truncatePayload(lastPayload) : "-"} + + `; + }).join(""); +} + +function renderHostInfo(): void { + const ctx = app.getHostContext(); + const caps = app.getHostCapabilities(); + const version = app.getHostVersion(); + + // Context info + if (ctx) { + hostContextInfoEl.innerHTML = ` +
Theme
${ctx.theme ?? "unknown"}
+
Locale
${ctx.locale ?? "unknown"}
+
TimeZone
${ctx.timeZone ?? "unknown"}
+
Platform
${ctx.platform ?? "unknown"}
+
Display Mode
${ctx.displayMode ?? "unknown"}
+
Host
${version?.name ?? "unknown"} v${version?.version ?? "?"}
+ `; + } else { + hostContextInfoEl.innerHTML = "
No context available
"; + } + + // Capabilities + if (caps) { + hostCapabilitiesInfoEl.innerHTML = ` +
openLinks
${caps.openLinks ? "✓" : "✗"}
+
serverTools
${caps.serverTools ? "✓" : "✗"}
+
serverResources
${caps.serverResources ? "✓" : "✗"}
+
logging
${caps.logging ? "✓" : "✗"}
+
message
${caps.message ? "✓" : "✗"}
+
updateModelContext
${caps.updateModelContext ? "✓" : "✗"}
+ `; + } else { + hostCapabilitiesInfoEl.innerHTML = "
No capabilities available
"; + } + + // Container info + if (ctx?.containerDimensions) { + const dims = ctx.containerDimensions; + hostContainerInfoEl.innerHTML = ` +
Width
${"width" in dims ? dims.width + "px" : `max ${dims.maxWidth ?? "?"}px`}
+
Height
${"height" in dims ? dims.height + "px" : `max ${dims.maxHeight ?? "?"}px`}
+
Safe Area
${ctx.safeAreaInsets ? `T${ctx.safeAreaInsets.top} R${ctx.safeAreaInsets.right} B${ctx.safeAreaInsets.bottom} L${ctx.safeAreaInsets.left}` : "none"}
+ `; + } else { + hostContainerInfoEl.innerHTML = "
No container info
"; + } + + // Styles sample + if (ctx?.styles) { + const styleVars = Object.entries(ctx.styles).slice(0, 6); + hostStylesSampleEl.innerHTML = styleVars.map(([key, value]) => { + const color = String(value); + return `
`; + }).join(""); + } else { + hostStylesSampleEl.innerHTML = "No styles"; + } +} + +function updateCurrentSize(): void { + const w = document.documentElement.scrollWidth; + const h = document.documentElement.scrollHeight; + currentSizeEl.textContent = `${w}x${h}`; +} + +// ============================================================================ +// Event Logging +// ============================================================================ + +function logEvent(type: string, payload: unknown): void { + const count = (state.callbackCounts.get(type) ?? 0) + 1; + state.callbackCounts.set(type, count); + state.lastPayloads.set(type, payload); + state.eventLog.push({ time: Date.now(), type, payload }); + + // Keep log manageable (max 100 entries) + if (state.eventLog.length > 100) { + state.eventLog.shift(); + } + + renderEventLog(); + renderCallbackStatus(); +} + +// ============================================================================ +// Safe Area Handling +// ============================================================================ + +function handleHostContextChanged(ctx: McpUiHostContext): void { + if (ctx.safeAreaInsets) { + mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`; + mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`; + mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`; + mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`; + } + renderHostInfo(); +} + +// ============================================================================ +// App Instance & Callbacks +// ============================================================================ + +const app = new App( + { name: "Debug App", version: "1.0.0" }, + {}, // capabilities + { autoResize: false }, // We'll manage auto-resize ourselves for toggle demo +); + +// Register ALL callbacks BEFORE connecting +app.ontoolinput = (params) => { + logEvent("tool-input", params); +}; + +app.ontoolinputpartial = (params) => { + logEvent("tool-input-partial", params); +}; + +app.ontoolresult = (result) => { + logEvent("tool-result", result); +}; + +app.ontoolcancelled = (params) => { + logEvent("tool-cancelled", params); +}; + +app.onwidgetstate = (params) => { + logEvent("widget-state", params); +}; + +app.onhostcontextchanged = (ctx) => { + logEvent("host-context-changed", ctx); + handleHostContextChanged(ctx); +}; + +app.onteardown = async (params) => { + logEvent("teardown", params); + return {}; +}; + +app.oncalltool = async (params) => { + logEvent("call-tool", params); + return { + content: [{ type: "text", text: "App handled tool call" }], + }; +}; + +app.onlisttools = async (params) => { + logEvent("list-tools", params); + return { tools: [] }; +}; + +app.onerror = (error) => { + logEvent("error", error); +}; + +// ============================================================================ +// Section Collapsing +// ============================================================================ + +document.querySelectorAll(".section-header[data-toggle]").forEach(header => { + header.addEventListener("click", () => { + const section = header.closest(".section"); + section?.classList.toggle("collapsed"); + }); +}); + +// ============================================================================ +// Event Log Controls +// ============================================================================ + +logFilterEl.addEventListener("change", () => { + state.logFilter = logFilterEl.value; + renderEventLog(); +}); + +clearLogBtn.addEventListener("click", () => { + state.eventLog = []; + renderEventLog(); +}); + +// ============================================================================ +// Message Actions +// ============================================================================ + +sendMessageTextBtn.addEventListener("click", async () => { + try { + const result = await app.sendMessage({ + role: "user", + content: [{ type: "text", text: messageTextEl.value }], + }); + logEvent("send-message-result", result); + } catch (e) { + logEvent("error", e); + } +}); + +sendMessageImageBtn.addEventListener("click", async () => { + // 1x1 red PNG for testing + const redPng = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; + try { + const result = await app.sendMessage({ + role: "user", + content: [{ type: "image", data: redPng, mimeType: "image/png" }], + }); + logEvent("send-message-result", result); + } catch (e) { + logEvent("error", e); + } +}); + +// ============================================================================ +// Logging Actions +// ============================================================================ + +function sendLog(level: "debug" | "info" | "warning" | "error"): void { + app.sendLog({ level, data: logDataEl.value }); + logEvent("send-log", { level, data: logDataEl.value }); +} + +logDebugBtn.addEventListener("click", () => sendLog("debug")); +logInfoBtn.addEventListener("click", () => sendLog("info")); +logWarningBtn.addEventListener("click", () => sendLog("warning")); +logErrorBtn.addEventListener("click", () => sendLog("error")); + +// ============================================================================ +// Model Context Actions +// ============================================================================ + +updateContextTextBtn.addEventListener("click", async () => { + try { + await app.updateModelContext({ + content: [{ type: "text", text: contextTextEl.value }], + }); + logEvent("update-context", { type: "text", value: contextTextEl.value }); + } catch (e) { + logEvent("error", e); + } +}); + +updateContextStructuredBtn.addEventListener("click", async () => { + try { + await app.updateModelContext({ + structuredContent: { + debugState: { + eventCount: state.eventLog.length, + timestamp: new Date().toISOString(), + uploadedFileId: state.uploadedFileId, + }, + }, + }); + logEvent("update-context", { type: "structured" }); + } catch (e) { + logEvent("error", e); + } +}); + +// ============================================================================ +// Display Mode Actions +// ============================================================================ + +async function requestDisplayMode(mode: "inline" | "fullscreen" | "pip"): Promise { + try { + const result = await app.requestDisplayMode({ mode }); + logEvent("display-mode-result", { mode, result }); + } catch (e) { + logEvent("error", e); + } +} + +displayInlineBtn.addEventListener("click", () => requestDisplayMode("inline")); +displayFullscreenBtn.addEventListener("click", () => requestDisplayMode("fullscreen")); +displayPipBtn.addEventListener("click", () => requestDisplayMode("pip")); + +// ============================================================================ +// Link Action +// ============================================================================ + +openLinkBtn.addEventListener("click", async () => { + try { + const result = await app.openLink({ url: linkUrlEl.value }); + logEvent("open-link-result", result); + } catch (e) { + logEvent("error", e); + } +}); + +// ============================================================================ +// Size Controls +// ============================================================================ + +autoResizeToggleEl.addEventListener("change", () => { + if (autoResizeToggleEl.checked) { + if (!state.autoResizeCleanup) { + state.autoResizeCleanup = app.setupSizeChangedNotifications(); + } + } else { + if (state.autoResizeCleanup) { + state.autoResizeCleanup(); + state.autoResizeCleanup = null; + } + } + logEvent("auto-resize-toggle", { enabled: autoResizeToggleEl.checked }); +}); + +function manualResize(width: number, height: number): void { + app.sendSizeChanged({ width, height }); + logEvent("manual-resize", { width, height }); +} + +resize200x100Btn.addEventListener("click", () => manualResize(200, 100)); +resize400x300Btn.addEventListener("click", () => manualResize(400, 300)); +resize800x600Btn.addEventListener("click", () => manualResize(800, 600)); + +// Update current size periodically +setInterval(updateCurrentSize, 1000); + +// ============================================================================ +// Server Tool Actions +// ============================================================================ + +callDebugToolBtn.addEventListener("click", async () => { + const args = { + contentType: toolContentTypeEl.value, + multipleBlocks: toolMultipleBlocksEl.checked, + includeStructuredContent: toolStructuredContentEl.checked, + includeMeta: toolIncludeMetaEl.checked, + simulateError: toolSimulateErrorEl.checked, + delayMs: parseInt(toolDelayMsEl.value, 10) || undefined, + }; + + try { + logEvent("call-server-tool", { name: "debug-tool", arguments: args }); + const result = await app.callServerTool({ name: "debug-tool", arguments: args }); + logEvent("server-tool-result", result); + } catch (e) { + logEvent("error", e); + } +}); + +callDebugRefreshBtn.addEventListener("click", async () => { + try { + logEvent("call-server-tool", { name: "debug-refresh", arguments: {} }); + const result = await app.callServerTool({ name: "debug-refresh", arguments: {} }); + logEvent("server-tool-result", result); + } catch (e) { + logEvent("error", e); + } +}); + +// ============================================================================ +// File Operations +// ============================================================================ + +uploadFileBtn.addEventListener("click", async () => { + const file = fileInputEl.files?.[0]; + if (!file) { + logEvent("error", { message: "No file selected" }); + return; + } + + try { + logEvent("upload-file", { name: file.name, size: file.size, type: file.type }); + const result = await app.uploadFile(file); + state.uploadedFileId = result.fileId; + lastFileIdEl.textContent = result.fileId; + logEvent("upload-file-result", result); + } catch (e) { + logEvent("error", e); + } +}); + +getFileUrlBtn.addEventListener("click", async () => { + if (!state.uploadedFileId) { + logEvent("error", { message: "No file uploaded yet" }); + return; + } + + try { + logEvent("get-file-url", { fileId: state.uploadedFileId }); + const result = await app.getFileDownloadUrl({ fileId: state.uploadedFileId }); + logEvent("get-file-url-result", result); + } catch (e) { + logEvent("error", e); + } +}); + +// ============================================================================ +// Initialization +// ============================================================================ + +// Initial render +renderCallbackStatus(); + +// Connect to host +app.connect().then(() => { + logEvent("connected", { success: true }); + + const ctx = app.getHostContext(); + if (ctx) { + handleHostContextChanged(ctx); + } + + renderHostInfo(); + updateCurrentSize(); + + // Auto-resize is enabled by default in App, capture cleanup if we want to toggle + // We'll set it up ourselves since we want toggle control + state.autoResizeCleanup = app.setupSizeChangedNotifications(); +}).catch(e => { + logEvent("error", e); +}); diff --git a/examples/debug-server/tsconfig.json b/examples/debug-server/tsconfig.json new file mode 100644 index 00000000..535267b2 --- /dev/null +++ b/examples/debug-server/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src", "server.ts"] +} diff --git a/examples/debug-server/vite.config.ts b/examples/debug-server/vite.config.ts new file mode 100644 index 00000000..6ff6d997 --- /dev/null +++ b/examples/debug-server/vite.config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from "vite"; +import { viteSingleFile } from "vite-plugin-singlefile"; + +const INPUT = process.env.INPUT; +if (!INPUT) { + throw new Error("INPUT environment variable is not set"); +} + +const isDevelopment = process.env.NODE_ENV === "development"; + +export default defineConfig({ + plugins: [viteSingleFile()], + build: { + sourcemap: isDevelopment ? "inline" : undefined, + cssMinify: !isDevelopment, + minify: !isDevelopment, + + rollupOptions: { + input: INPUT, + }, + outDir: "dist", + emptyOutDir: false, + }, +}); From 0d9a2363c0b6be3adea1861ba30fe9809295eb99 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 14 Jan 2026 23:05:34 +0000 Subject: [PATCH 02/10] docs(examples): Add README for debug-server --- examples/debug-server/README.md | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 examples/debug-server/README.md diff --git a/examples/debug-server/README.md b/examples/debug-server/README.md new file mode 100644 index 00000000..eef1ae6e --- /dev/null +++ b/examples/debug-server/README.md @@ -0,0 +1,55 @@ +# Debug Server + +A comprehensive testing/debugging tool for the MCP Apps SDK that exercises every capability, callback, and result format combination. + +## Tools + +### debug-tool + +Configurable tool for testing all result variations: + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `contentType` | `"text"` \| `"image"` \| `"audio"` \| `"resource"` \| `"resourceLink"` \| `"mixed"` | `"text"` | Content block type to return | +| `multipleBlocks` | boolean | `false` | Return 3 content blocks | +| `includeStructuredContent` | boolean | `true` | Include structuredContent in result | +| `includeMeta` | boolean | `false` | Include _meta in result | +| `largeInput` | string | - | Large text input (tests tool-input-partial) | +| `simulateError` | boolean | `false` | Return isError: true | +| `delayMs` | number | - | Delay before response (ms) | + +### debug-refresh + +App-only tool (hidden from model) for polling server state. Returns current timestamp and call counter. + +## App UI + +The debug app provides a dashboard with: + +- **Event Log**: Real-time log of all SDK events with filtering +- **Host Info**: Context, capabilities, container dimensions, styles +- **Callback Status**: Table of all callbacks with call counts +- **Actions**: Buttons to test every SDK method: + - Send messages (text/image) + - Logging (debug/info/warning/error) + - Model context updates + - Display mode requests + - Link opening + - Resize controls + - Server tool calls + - File operations + +## Usage + +```bash +# Build +npm run --workspace examples/debug-server build + +# Run standalone +npm run --workspace examples/debug-server serve + +# Run with all examples +npm start +``` + +Then open `http://localhost:8080/basic-host/` and select "Debug MCP App Server" from the dropdown. From fd27d7d23a659483d67a0509e435c6b70c9db1f6 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 14 Jan 2026 23:46:42 +0000 Subject: [PATCH 03/10] feat(debug-server): Add file logging with debug-log tool - Add --log-file argument (default: /tmp/mcp-apps-debug-server.log) - Add debug-log app-private tool for app to send logs to file - App now logs all events to console AND server log file - Wrap server log calls in try/catch to prevent failures from breaking app --- examples/debug-server/server.ts | 125 ++++++++++++++++--- examples/debug-server/src/mcp-app.ts | 172 +++++++++++++++++++-------- 2 files changed, 235 insertions(+), 62 deletions(-) diff --git a/examples/debug-server/server.ts b/examples/debug-server/server.ts index 9a67fd46..32b89f3b 100644 --- a/examples/debug-server/server.ts +++ b/examples/debug-server/server.ts @@ -1,8 +1,16 @@ -import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; +import { + registerAppResource, + registerAppTool, + RESOURCE_MIME_TYPE, +} from "@modelcontextprotocol/ext-apps/server"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import type { + CallToolResult, + ReadResourceResult, +} from "@modelcontextprotocol/sdk/types.js"; import fs from "node:fs/promises"; +import { appendFileSync } from "node:fs"; import path from "node:path"; import { z } from "zod"; import { startServer } from "./server-utils.js"; @@ -12,18 +20,50 @@ const DIST_DIR = path.join(import.meta.dirname, "dist"); // Track call counter across requests (stateful for demo purposes) let callCounter = 0; +// Parse --log-file argument or use default +const DEFAULT_LOG_FILE = "/tmp/mcp-apps-debug-server.log"; +function getLogFilePath(): string { + const logFileArg = process.argv.find((arg) => arg.startsWith("--log-file=")); + if (logFileArg) { + return logFileArg.split("=")[1]; + } + return process.env.DEBUG_LOG_FILE ?? DEFAULT_LOG_FILE; +} + +const logFilePath = getLogFilePath(); + +/** + * Append a log entry to the log file + */ +function appendToLogFile(entry: { + timestamp: string; + type: string; + payload: unknown; +}): void { + try { + const line = JSON.stringify(entry) + "\n"; + appendFileSync(logFilePath, line, "utf-8"); + } catch (e) { + console.error("[debug-server] Failed to write to log file:", e); + } +} + // Minimal 1x1 blue PNG (base64) -const BLUE_PNG_1X1 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPj/HwADBwIAMCbHYQAAAABJRU5ErkJggg=="; +const BLUE_PNG_1X1 = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPj/HwADBwIAMCbHYQAAAABJRU5ErkJggg=="; // Minimal silent WAV (base64) - 44 byte header + 1 sample -const SILENT_WAV = "UklGRiYAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQIAAAAAAA=="; +const SILENT_WAV = + "UklGRiYAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQIAAAAAAA=="; /** * Input schema for the debug-tool */ const DebugInputSchema = z.object({ // Content configuration - contentType: z.enum(["text", "image", "audio", "resource", "resourceLink", "mixed"]).default("text"), + contentType: z + .enum(["text", "image", "audio", "resource", "resourceLink", "mixed"]) + .default("text"), multipleBlocks: z.boolean().default(false), includeStructuredContent: z.boolean().default(true), includeMeta: z.boolean().default(false), @@ -63,10 +103,18 @@ function buildContent(args: DebugInput): CallToolResult["content"] { content.push({ type: "text", text: `Debug text content${suffix}` }); break; case "image": - content.push({ type: "image", data: BLUE_PNG_1X1, mimeType: "image/png" }); + content.push({ + type: "image", + data: BLUE_PNG_1X1, + mimeType: "image/png", + }); break; case "audio": - content.push({ type: "audio", data: SILENT_WAV, mimeType: "audio/wav" }); + content.push({ + type: "audio", + data: SILENT_WAV, + mimeType: "audio/wav", + }); break; case "resource": content.push({ @@ -111,11 +159,13 @@ export function createServer(): McpServer { const resourceUri = "ui://debug-tool/mcp-app.html"; // Main debug tool - exercises all result variations - registerAppTool(server, + registerAppTool( + server, "debug-tool", { title: "Debug Tool", - description: "Comprehensive debug tool for testing MCP Apps SDK. Configure content types, error simulation, delays, and more.", + description: + "Comprehensive debug tool for testing MCP Apps SDK. Configure content types, error simulation, delays, and more.", inputSchema: DebugInputSchema, outputSchema: DebugOutputSchema, _meta: { ui: { resourceUri } }, @@ -123,7 +173,7 @@ export function createServer(): McpServer { async (args): Promise => { // Apply delay if requested if (args.delayMs && args.delayMs > 0) { - await new Promise(resolve => setTimeout(resolve, args.delayMs)); + await new Promise((resolve) => setTimeout(resolve, args.delayMs)); } // Build content based on config @@ -138,7 +188,9 @@ export function createServer(): McpServer { config: args, timestamp: new Date().toISOString(), counter: ++callCounter, - ...(args.largeInput ? { largeInputLength: args.largeInput.length } : {}), + ...(args.largeInput + ? { largeInputLength: args.largeInput.length } + : {}), }; } @@ -162,11 +214,13 @@ export function createServer(): McpServer { ); // App-only refresh tool (hidden from model) - registerAppTool(server, + registerAppTool( + server, "debug-refresh", { title: "Refresh Debug Info", - description: "App-only tool for polling server state. Not visible to the model.", + description: + "App-only tool for polling server state. Not visible to the model.", inputSchema: z.object({}), outputSchema: z.object({ timestamp: z.string(), counter: z.number() }), _meta: { @@ -185,13 +239,47 @@ export function createServer(): McpServer { }, ); + // App-only log tool - writes events to log file + registerAppTool( + server, + "debug-log", + { + title: "Log to File", + description: + "App-only tool for logging events to the server log file. Not visible to the model.", + inputSchema: z.object({ + type: z.string(), + payload: z.unknown(), + }), + outputSchema: z.object({ logged: z.boolean(), logFile: z.string() }), + _meta: { + ui: { + resourceUri, + visibility: ["app"], + }, + }, + }, + async (args): Promise => { + const timestamp = new Date().toISOString(); + appendToLogFile({ timestamp, type: args.type, payload: args.payload }); + return { + content: [{ type: "text", text: `Logged to ${logFilePath}` }], + structuredContent: { logged: true, logFile: logFilePath }, + }; + }, + ); + // Register the resource which returns the bundled HTML/JavaScript for the UI - registerAppResource(server, + registerAppResource( + server, resourceUri, resourceUri, { mimeType: RESOURCE_MIME_TYPE }, async (): Promise => { - const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8"); + const html = await fs.readFile( + path.join(DIST_DIR, "mcp-app.html"), + "utf-8", + ); return { contents: [ @@ -205,6 +293,13 @@ export function createServer(): McpServer { } async function main() { + console.log(`[debug-server] Log file: ${logFilePath}`); + appendToLogFile({ + timestamp: new Date().toISOString(), + type: "server-start", + payload: { logFilePath, pid: process.pid }, + }); + if (process.argv.includes("--stdio")) { await createServer().connect(new StdioServerTransport()); } else { diff --git a/examples/debug-server/src/mcp-app.ts b/examples/debug-server/src/mcp-app.ts index 1d0fbd77..0b36e984 100644 --- a/examples/debug-server/src/mcp-app.ts +++ b/examples/debug-server/src/mcp-app.ts @@ -64,7 +64,9 @@ const clearLogBtn = document.getElementById("clear-log-btn")!; // Host info const hostContextInfoEl = document.getElementById("host-context-info")!; -const hostCapabilitiesInfoEl = document.getElementById("host-capabilities-info")!; +const hostCapabilitiesInfoEl = document.getElementById( + "host-capabilities-info", +)!; const hostContainerInfoEl = document.getElementById("host-container-info")!; const hostStylesSampleEl = document.getElementById("host-styles-sample")!; @@ -72,7 +74,9 @@ const hostStylesSampleEl = document.getElementById("host-styles-sample")!; const callbackTableBodyEl = document.getElementById("callback-table-body")!; // Action elements -const messageTextEl = document.getElementById("message-text") as HTMLInputElement; +const messageTextEl = document.getElementById( + "message-text", +) as HTMLInputElement; const sendMessageTextBtn = document.getElementById("send-message-text-btn")!; const sendMessageImageBtn = document.getElementById("send-message-image-btn")!; @@ -82,9 +86,15 @@ const logInfoBtn = document.getElementById("log-info-btn")!; const logWarningBtn = document.getElementById("log-warning-btn")!; const logErrorBtn = document.getElementById("log-error-btn")!; -const contextTextEl = document.getElementById("context-text") as HTMLInputElement; -const updateContextTextBtn = document.getElementById("update-context-text-btn")!; -const updateContextStructuredBtn = document.getElementById("update-context-structured-btn")!; +const contextTextEl = document.getElementById( + "context-text", +) as HTMLInputElement; +const updateContextTextBtn = document.getElementById( + "update-context-text-btn", +)!; +const updateContextStructuredBtn = document.getElementById( + "update-context-structured-btn", +)!; const displayInlineBtn = document.getElementById("display-inline-btn")!; const displayFullscreenBtn = document.getElementById("display-fullscreen-btn")!; @@ -93,19 +103,33 @@ const displayPipBtn = document.getElementById("display-pip-btn")!; const linkUrlEl = document.getElementById("link-url") as HTMLInputElement; const openLinkBtn = document.getElementById("open-link-btn")!; -const autoResizeToggleEl = document.getElementById("auto-resize-toggle") as HTMLInputElement; +const autoResizeToggleEl = document.getElementById( + "auto-resize-toggle", +) as HTMLInputElement; const resize200x100Btn = document.getElementById("resize-200x100-btn")!; const resize400x300Btn = document.getElementById("resize-400x300-btn")!; const resize800x600Btn = document.getElementById("resize-800x600-btn")!; const currentSizeEl = document.getElementById("current-size")!; // Tool config elements -const toolContentTypeEl = document.getElementById("tool-content-type") as HTMLSelectElement; -const toolMultipleBlocksEl = document.getElementById("tool-multiple-blocks") as HTMLInputElement; -const toolStructuredContentEl = document.getElementById("tool-structured-content") as HTMLInputElement; -const toolIncludeMetaEl = document.getElementById("tool-include-meta") as HTMLInputElement; -const toolSimulateErrorEl = document.getElementById("tool-simulate-error") as HTMLInputElement; -const toolDelayMsEl = document.getElementById("tool-delay-ms") as HTMLInputElement; +const toolContentTypeEl = document.getElementById( + "tool-content-type", +) as HTMLSelectElement; +const toolMultipleBlocksEl = document.getElementById( + "tool-multiple-blocks", +) as HTMLInputElement; +const toolStructuredContentEl = document.getElementById( + "tool-structured-content", +) as HTMLInputElement; +const toolIncludeMetaEl = document.getElementById( + "tool-include-meta", +) as HTMLInputElement; +const toolSimulateErrorEl = document.getElementById( + "tool-simulate-error", +) as HTMLInputElement; +const toolDelayMsEl = document.getElementById( + "tool-delay-ms", +) as HTMLInputElement; const callDebugToolBtn = document.getElementById("call-debug-tool-btn")!; const callDebugRefreshBtn = document.getElementById("call-debug-refresh-btn")!; @@ -141,24 +165,29 @@ function truncatePayload(payload: unknown): string { // ============================================================================ function renderEventLog(): void { - const filtered = state.logFilter === "all" - ? state.eventLog - : state.eventLog.filter(e => e.type === state.logFilter); - - eventLogEl.innerHTML = filtered.map(entry => ` + const filtered = + state.logFilter === "all" + ? state.eventLog + : state.eventLog.filter((e) => e.type === state.logFilter); + + eventLogEl.innerHTML = filtered + .map( + (entry) => `
[${formatTime(entry.time)}] ${entry.type}: ${truncatePayload(entry.payload)}
- `).join(""); + `, + ) + .join(""); // Auto-scroll to bottom eventLogEl.scrollTop = eventLogEl.scrollHeight; } function renderCallbackStatus(): void { - callbackTableBodyEl.innerHTML = CALLBACKS.map(name => { + callbackTableBodyEl.innerHTML = CALLBACKS.map((name) => { const count = state.callbackCounts.get(name) ?? 0; const lastPayload = state.lastPayloads.get(name); const registered = name !== "onerror"; // All callbacks are registered @@ -222,10 +251,12 @@ function renderHostInfo(): void { // Styles sample if (ctx?.styles) { const styleVars = Object.entries(ctx.styles).slice(0, 6); - hostStylesSampleEl.innerHTML = styleVars.map(([key, value]) => { - const color = String(value); - return `
`; - }).join(""); + hostStylesSampleEl.innerHTML = styleVars + .map(([key, value]) => { + const color = String(value); + return `
`; + }) + .join(""); } else { hostStylesSampleEl.innerHTML = "No styles"; } @@ -241,11 +272,32 @@ function updateCurrentSize(): void { // Event Logging // ============================================================================ +/** + * Send a log entry to the server's debug-log tool (writes to file) + */ +async function sendToServerLog(type: string, payload: unknown): Promise { + try { + await app.callServerTool({ + name: "debug-log", + arguments: { type, payload }, + }); + } catch (e) { + // Log to console only - don't call logEvent to avoid infinite loop + console.error("[debug-app] Failed to send log to server:", e); + } +} + function logEvent(type: string, payload: unknown): void { + const time = Date.now(); + + // Log to console + console.log(`[debug-app] ${type}:`, payload); + + // Update state const count = (state.callbackCounts.get(type) ?? 0) + 1; state.callbackCounts.set(type, count); state.lastPayloads.set(type, payload); - state.eventLog.push({ time: Date.now(), type, payload }); + state.eventLog.push({ time, type, payload }); // Keep log manageable (max 100 entries) if (state.eventLog.length > 100) { @@ -254,6 +306,12 @@ function logEvent(type: string, payload: unknown): void { renderEventLog(); renderCallbackStatus(); + + // Send to server log file (async, fire-and-forget) + // Skip sending debug-log results to avoid noise + if (type !== "server-tool-result" || (payload as { name?: string })?.name !== "debug-log") { + sendToServerLog(type, payload); + } } // ============================================================================ @@ -331,7 +389,7 @@ app.onerror = (error) => { // Section Collapsing // ============================================================================ -document.querySelectorAll(".section-header[data-toggle]").forEach(header => { +document.querySelectorAll(".section-header[data-toggle]").forEach((header) => { header.addEventListener("click", () => { const section = header.closest(".section"); section?.classList.toggle("collapsed"); @@ -370,7 +428,8 @@ sendMessageTextBtn.addEventListener("click", async () => { sendMessageImageBtn.addEventListener("click", async () => { // 1x1 red PNG for testing - const redPng = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; + const redPng = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="; try { const result = await app.sendMessage({ role: "user", @@ -432,7 +491,9 @@ updateContextStructuredBtn.addEventListener("click", async () => { // Display Mode Actions // ============================================================================ -async function requestDisplayMode(mode: "inline" | "fullscreen" | "pip"): Promise { +async function requestDisplayMode( + mode: "inline" | "fullscreen" | "pip", +): Promise { try { const result = await app.requestDisplayMode({ mode }); logEvent("display-mode-result", { mode, result }); @@ -442,7 +503,9 @@ async function requestDisplayMode(mode: "inline" | "fullscreen" | "pip"): Promis } displayInlineBtn.addEventListener("click", () => requestDisplayMode("inline")); -displayFullscreenBtn.addEventListener("click", () => requestDisplayMode("fullscreen")); +displayFullscreenBtn.addEventListener("click", () => + requestDisplayMode("fullscreen"), +); displayPipBtn.addEventListener("click", () => requestDisplayMode("pip")); // ============================================================================ @@ -504,7 +567,10 @@ callDebugToolBtn.addEventListener("click", async () => { try { logEvent("call-server-tool", { name: "debug-tool", arguments: args }); - const result = await app.callServerTool({ name: "debug-tool", arguments: args }); + const result = await app.callServerTool({ + name: "debug-tool", + arguments: args, + }); logEvent("server-tool-result", result); } catch (e) { logEvent("error", e); @@ -514,7 +580,10 @@ callDebugToolBtn.addEventListener("click", async () => { callDebugRefreshBtn.addEventListener("click", async () => { try { logEvent("call-server-tool", { name: "debug-refresh", arguments: {} }); - const result = await app.callServerTool({ name: "debug-refresh", arguments: {} }); + const result = await app.callServerTool({ + name: "debug-refresh", + arguments: {}, + }); logEvent("server-tool-result", result); } catch (e) { logEvent("error", e); @@ -533,7 +602,11 @@ uploadFileBtn.addEventListener("click", async () => { } try { - logEvent("upload-file", { name: file.name, size: file.size, type: file.type }); + logEvent("upload-file", { + name: file.name, + size: file.size, + type: file.type, + }); const result = await app.uploadFile(file); state.uploadedFileId = result.fileId; lastFileIdEl.textContent = result.fileId; @@ -551,7 +624,9 @@ getFileUrlBtn.addEventListener("click", async () => { try { logEvent("get-file-url", { fileId: state.uploadedFileId }); - const result = await app.getFileDownloadUrl({ fileId: state.uploadedFileId }); + const result = await app.getFileDownloadUrl({ + fileId: state.uploadedFileId, + }); logEvent("get-file-url-result", result); } catch (e) { logEvent("error", e); @@ -566,20 +641,23 @@ getFileUrlBtn.addEventListener("click", async () => { renderCallbackStatus(); // Connect to host -app.connect().then(() => { - logEvent("connected", { success: true }); - - const ctx = app.getHostContext(); - if (ctx) { - handleHostContextChanged(ctx); - } +app + .connect() + .then(() => { + logEvent("connected", { success: true }); + + const ctx = app.getHostContext(); + if (ctx) { + handleHostContextChanged(ctx); + } - renderHostInfo(); - updateCurrentSize(); + renderHostInfo(); + updateCurrentSize(); - // Auto-resize is enabled by default in App, capture cleanup if we want to toggle - // We'll set it up ourselves since we want toggle control - state.autoResizeCleanup = app.setupSizeChangedNotifications(); -}).catch(e => { - logEvent("error", e); -}); + // Auto-resize is enabled by default in App, capture cleanup if we want to toggle + // We'll set it up ourselves since we want toggle control + state.autoResizeCleanup = app.setupSizeChangedNotifications(); + }) + .catch((e) => { + logEvent("error", e); + }); From 37a3223f6ee1c70060471234b6d4995d45ac8458 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 16 Jan 2026 14:21:30 +0000 Subject: [PATCH 04/10] fix(debug-server): update versions to 0.4.1 --- examples/debug-server/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index c2d85c3a..957bac4b 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-debug", - "version": "0.4.0", + "version": "0.4.1", "type": "module", "description": "Debug MCP App Server for testing all SDK capabilities", "repository": { @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^0.4.0", + "@modelcontextprotocol/ext-apps": "^0.4.1", "@modelcontextprotocol/sdk": "^1.24.0", "zod": "^4.1.13" }, From b36e05e6c34ca2d0eca7421bc3ca213eb59fe261 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 16 Jan 2026 14:23:20 +0000 Subject: [PATCH 05/10] chore: update package-lock.json for debug-server --- package-lock.json | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/package-lock.json b/package-lock.json index 405f3148..8a8f205f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -490,6 +490,45 @@ "dev": true, "license": "MIT" }, + "examples/debug-server": { + "name": "@modelcontextprotocol/server-debug", + "version": "0.4.1", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/ext-apps": "^0.4.1", + "@modelcontextprotocol/sdk": "^1.24.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@types/cors": "^2.8.19", + "@types/express": "^5.0.0", + "@types/node": "^22.0.0", + "concurrently": "^9.2.1", + "cors": "^2.8.5", + "cross-env": "^10.1.0", + "express": "^5.1.0", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-singlefile": "^2.3.0" + } + }, + "examples/debug-server/node_modules/@types/node": { + "version": "22.19.3", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "examples/debug-server/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "examples/integration-server": { "version": "1.0.0", "dependencies": { @@ -2468,6 +2507,10 @@ "resolved": "examples/customer-segmentation-server", "link": true }, + "node_modules/@modelcontextprotocol/server-debug": { + "resolved": "examples/debug-server", + "link": true + }, "node_modules/@modelcontextprotocol/server-map": { "resolved": "examples/map-server", "link": true From 8391262c1c13a1c2d73354e61d509412bcd67f42 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 16 Jan 2026 14:27:21 +0000 Subject: [PATCH 06/10] chore: add debug-server to package-lock.json --- package-lock.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 8a8f205f..0d1dbed5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -517,7 +517,6 @@ "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@types/node/-/node-22.19.3.tgz", "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "dev": true, - "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } From 95c28eb9360c1395f3ca3189741e0e0d32302a83 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 16 Jan 2026 14:30:01 +0000 Subject: [PATCH 07/10] chore: regenerate package-lock.json with public registry --- package-lock.json | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0d1dbed5..ae1cfa51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -513,17 +513,18 @@ } }, "examples/debug-server/node_modules/@types/node": { - "version": "22.19.3", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/@types/node/-/node-22.19.3.tgz", - "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", + "version": "22.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, "examples/debug-server/node_modules/undici-types": { "version": "6.21.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-all/undici-types/-/undici-types-6.21.0.tgz", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" @@ -1034,6 +1035,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -3511,6 +3513,7 @@ "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.1", @@ -3717,6 +3720,7 @@ "integrity": "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3741,6 +3745,7 @@ "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4143,6 +4148,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4468,6 +4474,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5179,6 +5186,7 @@ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "dev": true, "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -5663,6 +5671,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -7216,6 +7225,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -7311,6 +7321,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -7409,6 +7420,7 @@ "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -7530,6 +7542,7 @@ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10" } @@ -7820,6 +7833,7 @@ "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz", "integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", @@ -7998,6 +8012,7 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.46.3.tgz", "integrity": "sha512-Y5juST3x+/ySty5tYJCVWa6Corkxpt25bUZQHqOceg9xfMUtDsFx6rCsG6cYf1cA6vzDi66HIvaki0byZZX95A==", "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -8325,6 +8340,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -9074,6 +9090,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9167,6 +9184,7 @@ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -9343,6 +9361,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -9461,6 +9480,7 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "license": "MIT", + "peer": true, "dependencies": { "@vue/compiler-dom": "3.5.26", "@vue/compiler-sfc": "3.5.26", @@ -9633,6 +9653,7 @@ "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "dev": true, "license": "ISC", + "peer": true, "bin": { "yaml": "bin.mjs" }, @@ -9683,6 +9704,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 58f482b6c0f3b07706924d72bfe7a9d75d8cd32d Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 16 Jan 2026 14:42:40 +0000 Subject: [PATCH 08/10] fix(debug-server): comment out features not yet in main SDK - Comment out onwidgetstate handler (not yet in SDK) - Comment out uploadFile/getFileDownloadUrl handlers (not yet in SDK) These features will be enabled when the SDK adds them. --- examples/debug-server/src/mcp-app.ts | 86 ++++++++++++++-------------- 1 file changed, 44 insertions(+), 42 deletions(-) diff --git a/examples/debug-server/src/mcp-app.ts b/examples/debug-server/src/mcp-app.ts index 0b36e984..413d4067 100644 --- a/examples/debug-server/src/mcp-app.ts +++ b/examples/debug-server/src/mcp-app.ts @@ -355,9 +355,10 @@ app.ontoolcancelled = (params) => { logEvent("tool-cancelled", params); }; -app.onwidgetstate = (params) => { - logEvent("widget-state", params); -}; +// TODO: Enable when onwidgetstate is available in the SDK +// app.onwidgetstate = (params) => { +// logEvent("widget-state", params); +// }; app.onhostcontextchanged = (ctx) => { logEvent("host-context-changed", ctx); @@ -593,45 +594,46 @@ callDebugRefreshBtn.addEventListener("click", async () => { // ============================================================================ // File Operations // ============================================================================ - -uploadFileBtn.addEventListener("click", async () => { - const file = fileInputEl.files?.[0]; - if (!file) { - logEvent("error", { message: "No file selected" }); - return; - } - - try { - logEvent("upload-file", { - name: file.name, - size: file.size, - type: file.type, - }); - const result = await app.uploadFile(file); - state.uploadedFileId = result.fileId; - lastFileIdEl.textContent = result.fileId; - logEvent("upload-file-result", result); - } catch (e) { - logEvent("error", e); - } -}); - -getFileUrlBtn.addEventListener("click", async () => { - if (!state.uploadedFileId) { - logEvent("error", { message: "No file uploaded yet" }); - return; - } - - try { - logEvent("get-file-url", { fileId: state.uploadedFileId }); - const result = await app.getFileDownloadUrl({ - fileId: state.uploadedFileId, - }); - logEvent("get-file-url-result", result); - } catch (e) { - logEvent("error", e); - } -}); +// TODO: Enable when uploadFile/getFileDownloadUrl are available in the SDK + +// uploadFileBtn.addEventListener("click", async () => { +// const file = fileInputEl.files?.[0]; +// if (!file) { +// logEvent("error", { message: "No file selected" }); +// return; +// } +// +// try { +// logEvent("upload-file", { +// name: file.name, +// size: file.size, +// type: file.type, +// }); +// const result = await app.uploadFile(file); +// state.uploadedFileId = result.fileId; +// lastFileIdEl.textContent = result.fileId; +// logEvent("upload-file-result", result); +// } catch (e) { +// logEvent("error", e); +// } +// }); +// +// getFileUrlBtn.addEventListener("click", async () => { +// if (!state.uploadedFileId) { +// logEvent("error", { message: "No file uploaded yet" }); +// return; +// } +// +// try { +// logEvent("get-file-url", { fileId: state.uploadedFileId }); +// const result = await app.getFileDownloadUrl({ +// fileId: state.uploadedFileId, +// }); +// logEvent("get-file-url-result", result); +// } catch (e) { +// logEvent("error", e); +// } +// }); // ============================================================================ // Initialization From 94da957c0409c5397309a500b12b67ce46659da9 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 16 Jan 2026 14:49:20 +0000 Subject: [PATCH 09/10] fix(debug-server): comment out unused file element declarations --- examples/debug-server/src/mcp-app.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/debug-server/src/mcp-app.ts b/examples/debug-server/src/mcp-app.ts index 413d4067..50254fa9 100644 --- a/examples/debug-server/src/mcp-app.ts +++ b/examples/debug-server/src/mcp-app.ts @@ -133,11 +133,11 @@ const toolDelayMsEl = document.getElementById( const callDebugToolBtn = document.getElementById("call-debug-tool-btn")!; const callDebugRefreshBtn = document.getElementById("call-debug-refresh-btn")!; -// File elements -const fileInputEl = document.getElementById("file-input") as HTMLInputElement; -const uploadFileBtn = document.getElementById("upload-file-btn")!; -const lastFileIdEl = document.getElementById("last-file-id")!; -const getFileUrlBtn = document.getElementById("get-file-url-btn")!; +// File elements (commented out until SDK supports uploadFile/getFileDownloadUrl) +// const fileInputEl = document.getElementById("file-input") as HTMLInputElement; +// const uploadFileBtn = document.getElementById("upload-file-btn")!; +// const lastFileIdEl = document.getElementById("last-file-id")!; +// const getFileUrlBtn = document.getElementById("get-file-url-btn")!; // ============================================================================ // Utility Functions From 18f95e1245d50c0bced090c6b296f90566de07b6 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 16 Jan 2026 14:56:07 +0000 Subject: [PATCH 10/10] style(debug-server): fix prettier formatting --- examples/debug-server/README.md | 18 +++++++++--------- examples/debug-server/src/mcp-app.ts | 5 ++++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/examples/debug-server/README.md b/examples/debug-server/README.md index eef1ae6e..89d9a7d2 100644 --- a/examples/debug-server/README.md +++ b/examples/debug-server/README.md @@ -8,15 +8,15 @@ A comprehensive testing/debugging tool for the MCP Apps SDK that exercises every Configurable tool for testing all result variations: -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `contentType` | `"text"` \| `"image"` \| `"audio"` \| `"resource"` \| `"resourceLink"` \| `"mixed"` | `"text"` | Content block type to return | -| `multipleBlocks` | boolean | `false` | Return 3 content blocks | -| `includeStructuredContent` | boolean | `true` | Include structuredContent in result | -| `includeMeta` | boolean | `false` | Include _meta in result | -| `largeInput` | string | - | Large text input (tests tool-input-partial) | -| `simulateError` | boolean | `false` | Return isError: true | -| `delayMs` | number | - | Delay before response (ms) | +| Parameter | Type | Default | Description | +| -------------------------- | ----------------------------------------------------------------------------------- | -------- | ------------------------------------------- | +| `contentType` | `"text"` \| `"image"` \| `"audio"` \| `"resource"` \| `"resourceLink"` \| `"mixed"` | `"text"` | Content block type to return | +| `multipleBlocks` | boolean | `false` | Return 3 content blocks | +| `includeStructuredContent` | boolean | `true` | Include structuredContent in result | +| `includeMeta` | boolean | `false` | Include \_meta in result | +| `largeInput` | string | - | Large text input (tests tool-input-partial) | +| `simulateError` | boolean | `false` | Return isError: true | +| `delayMs` | number | - | Delay before response (ms) | ### debug-refresh diff --git a/examples/debug-server/src/mcp-app.ts b/examples/debug-server/src/mcp-app.ts index 50254fa9..ec4c9fa0 100644 --- a/examples/debug-server/src/mcp-app.ts +++ b/examples/debug-server/src/mcp-app.ts @@ -309,7 +309,10 @@ function logEvent(type: string, payload: unknown): void { // Send to server log file (async, fire-and-forget) // Skip sending debug-log results to avoid noise - if (type !== "server-tool-result" || (payload as { name?: string })?.name !== "debug-log") { + if ( + type !== "server-tool-result" || + (payload as { name?: string })?.name !== "debug-log" + ) { sendToServerLog(type, payload); } }