#!/usr/bin/env node /** * MCP Apps demo server — a zero-dependency MCP server (newline-delimited * JSON-RPC over stdio) for testing and demonstrating * @deepseek-ai/dsh-mcp-apps-host. * * It exposes two tools that exercise every host capability: * * - `demo_interactive` (inline form): the tool RESULT carries * `_meta.ui.resource.text` HTML. The card renders data injected from * `structuredContent` (via the `ui/initialize` handshake), refreshes itself * through `tools/call`, and can stage `ui/update-model-context` + send * `ui/message` into the conversation. * * - `demo_referenced` (referenced form): the tool DEFINITION carries * `_meta.ui.resourceUri`; the result carries no `_meta` at all. The host * fetches `ui://demo/referenced-card` through `resources/read` and inlines * the HTML. The card also demonstrates the `ui://`-only resources/read * security gate (a blocked URI returns a JSON-RPC error). * * Run standalone (any MCP client) or through DSH: * dsh web --patch demo/mcp-apps-demo.cordis.yml # from the repo root * * @module demo/server.mjs */ import { createInterface } from 'node:readline' const SERVER_INFO = { name: 'dsh-mcp-apps-demo', version: '0.1.0' } const REFERENCED_URI = 'ui://demo/referenced-card' const HTML_MIME = 'text/html;profile=mcp-app' // ── Card HTML ─────────────────────────────────────────────────────────────── /** * Shared card-side bridge script. Speaks JSON-RPC over postMessage with the * DSH host (McpAppCard), logs every message, and exposes `rpc()` to card * logic. Inline `render()`-style JS is avoided; this is plain script. */ function bridgeScript() { return `

`
}

/** Interactive card: lastToolResult rendering + tools/call + ui/* channels. */
function interactiveCardHtml() {
  return `

🎛 MCP Apps interactive demo

inline formtools/callui/message
${bridgeScript()} ` } /** Referenced card: HTML served from a ui:// resource; also demos the security gate. */ function referencedCardHtml() { return `

📦 MCP Apps referenced-form demo

resourceUriresources/readui:// gate
${bridgeScript()} ` } // ── Tool table ────────────────────────────────────────────────────────────── const TOOLS = [ { name: 'demo_interactive', description: 'MCP Apps demo (inline form): returns an interactive HTML card via _meta.ui.resource.text. ' + 'The card reads structuredContent through ui/initialize, refreshes itself via tools/call, ' + 'and can send ui/message into the conversation. Call this with no arguments for the default demo.', inputSchema: { type: 'object', properties: { note: { type: 'string', description: 'Free-form note shown on the card.' }, count: { type: 'number', description: 'Counter value to display (card increments it on refresh).' }, }, additionalProperties: false, }, }, { name: 'demo_referenced', description: 'MCP Apps demo (referenced form): the tool definition carries _meta.ui.resourceUri and the result ' + 'carries NO _meta — the host fetches the card HTML from the ui:// resource via resources/read. ' + 'The card demonstrates the ui://-only resources/read security gate. Call with no arguments.', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, _meta: { ui: { resourceUri: REFERENCED_URI } }, }, ] const RESOURCES = [ { uri: REFERENCED_URI, name: 'referenced-card', mimeType: HTML_MIME, text: referencedCardHtml() }, ] // ── Tool handlers ─────────────────────────────────────────────────────────── const TOOL_HANDLERS = { /** Inline-form card: HTML rides the tool result's _meta.ui. */ demo_interactive(args) { const count = typeof args.count === 'number' && args.count >= 0 ? Math.floor(args.count) : 0 const note = typeof args.note === 'string' ? args.note : 'hello from the model-side call' return { content: [{ type: 'text', text: `Interactive demo card rendered (count=${count}, note="${note}").` }], structuredContent: { count, note, // The host reads session_id from structuredContent and injects it into // every bridge tools/call the card makes (when the card omits it). session_id: 'demo-session', // Echoes whether this call arrived WITH a session_id (bridge calls do). received_session_id: typeof args.session_id === 'string' ? args.session_id : null, }, _meta: { ui: { resource: { uri: 'ui://demo/interactive-card', mimeType: HTML_MIME, text: interactiveCardHtml(), }, }, }, } }, /** Referenced-form card: no _meta on the result; the host resolves resourceUri. */ demo_referenced() { return { content: [{ type: 'text', text: 'Referenced-form demo card rendered.' }], structuredContent: { source: REFERENCED_URI }, } }, } // ── JSON-RPC plumbing (newline-delimited over stdio) ──────────────────────── const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`) const reply = (id, result) => send({ jsonrpc: '2.0', id, result }) const replyError = (id, code, message) => send({ jsonrpc: '2.0', id, error: { code, message } }) const METHODS = { initialize(params) { return { protocolVersion: params?.protocolVersion ?? '2025-06-18', capabilities: { tools: { listChanged: false }, resources: { listChanged: false } }, serverInfo: SERVER_INFO, } }, ping() { return {} }, 'tools/list'() { return { tools: TOOLS } }, 'tools/call'(params) { const handler = TOOL_HANDLERS[params?.name] if (handler === undefined) { throw Object.assign(new Error(`unknown tool: ${params?.name}`), { code: -32602 }) } return handler(params?.arguments ?? {}) }, 'resources/list'() { return { resources: RESOURCES.map(({ uri, name, mimeType }) => ({ uri, name, mimeType })) } }, 'resources/read'(params) { const resource = RESOURCES.find(r => r.uri === params?.uri) if (resource === undefined) { throw Object.assign(new Error(`unknown resource: ${params?.uri}`), { code: -32602 }) } return { contents: [{ uri: resource.uri, mimeType: resource.mimeType, text: resource.text }] } }, } createInterface({ input: process.stdin }).on('line', (line) => { const trimmed = line.trim() if (trimmed === '') return let message try { message = JSON.parse(trimmed) } catch { send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'parse error' } }) return } if (message === null || typeof message !== 'object' || typeof message.method !== 'string') return // Notifications (no id) are acknowledged by silence. if (message.id === undefined) return const handler = METHODS[message.method] if (handler === undefined) { replyError(message.id, -32601, `method not found: ${message.method}`) return } try { reply(message.id, handler(message.params)) } catch (error) { replyError(message.id, error?.code ?? -32603, String(error?.message ?? error)) } }) process.on('SIGTERM', () => process.exit(0)) console.error(`[${SERVER_INFO.name}] MCP Apps demo server on stdio (2 tools, ${RESOURCES.length} ui:// resource)`)