feat(debug-server): add interactive debug server example (#321)
* feat(examples): add debug-server example A comprehensive debug/development server for testing MCP Apps SDK features: - Vanilla TypeScript UI with collapsible panels - Message visualization (sent, received, errors) - Server tool calling interface - Multiple tool endpoints for testing * refactor(debug-server): convert to React - Replace vanilla TypeScript with React components - Add comprehensive Host Styles panel showing all CSS variables grouped by category - Use React hooks (useApp, useHostStyles, useAutoResize) - Cleaner state management with useReducer for event log - Collapsible sections for better organization - CSS modules for scoped styling * fix(debug-server): correct server name in main.ts * Revert "refactor(debug-server): convert to React" This reverts commit 5d22d2daa9a98ba412892447202ef10fbdb62f5b. * fix(debug-server): update default values for multipleBlocks and includeMeta * fix(debug-server): correct serve script and server name - serve script now runs main.ts instead of server.ts - Updated server name to 'Debug Server' - Fixed npx command in comment * fix: increase maxHeight to 6000px and add tools capability to debug app - basic-host: Increase maxHeight from 600 to 6000px - debug-server: Add tools capability to enable oncalltool/onlisttools handlers * fix(debug-server): use consistent callback names for event tracking Event types now match callback names (ontoolinput, ontoolresult, etc.) so the Callback Status panel shows correct counts and payloads. * feat(debug-server): add Update (Image) button for model context * feat(debug-server): add expandable event log entries - Click on any event log row to expand/collapse - Arrow indicator rotates when expanded - Full JSON payload shown indented in pre block - Max height with scroll for large payloads * style(debug-server): format long line in fullPayload function * test(debug-server): add to E2E tests and screenshot specs * chore: regenerate package-lock.json for cross-platform compatibility * update dep
This commit is contained in:
parent
023f1fd233
commit
8a877b36b4
22 changed files with 2253 additions and 435 deletions
|
|
@ -271,7 +271,7 @@ export function newAppBridge(
|
|||
updateModelContext: { text: {} },
|
||||
}, {
|
||||
hostContext: {
|
||||
containerDimensions: options?.containerDimensions ?? { maxHeight: 600 },
|
||||
containerDimensions: options?.containerDimensions ?? { maxHeight: 6000 },
|
||||
displayMode: options?.displayMode ?? "inline",
|
||||
availableDisplayModes: ["inline", "fullscreen"],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -422,7 +422,7 @@ function AppIFramePanel({ toolCallInfo, isDestroying, onTeardownComplete }: AppI
|
|||
onDisplayModeChange: setDisplayMode,
|
||||
}, {
|
||||
// Provide container dimensions - maxHeight for flexible sizing
|
||||
containerDimensions: { maxHeight: 600 },
|
||||
containerDimensions: { maxHeight: 6000 },
|
||||
displayMode: "inline",
|
||||
});
|
||||
appBridgeRef.current = appBridge;
|
||||
|
|
|
|||
2
examples/debug-server/.gitignore
vendored
Normal file
2
examples/debug-server/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
node_modules/
|
||||
dist/
|
||||
55
examples/debug-server/README.md
Normal file
55
examples/debug-server/README.md
Normal file
|
|
@ -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.
|
||||
BIN
examples/debug-server/grid-cell.png
Normal file
BIN
examples/debug-server/grid-cell.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
94
examples/debug-server/main.ts
Normal file
94
examples/debug-server/main.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* Entry point for running the MCP server.
|
||||
* Run with: npx mcp-server-debug
|
||||
* Or: node dist/index.js [--stdio]
|
||||
*/
|
||||
|
||||
/**
|
||||
* Shared utilities for running MCP servers with Streamable HTTP transport.
|
||||
*/
|
||||
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
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";
|
||||
import { createServer } from "./server.js";
|
||||
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
await createServer().connect(new StdioServerTransport());
|
||||
} else {
|
||||
const port = parseInt(process.env.PORT ?? "3110", 10);
|
||||
await startServer(createServer, { port, name: "Debug Server" });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
210
examples/debug-server/mcp-app.html
Normal file
210
examples/debug-server/mcp-app.html
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Debug App</title>
|
||||
</head>
|
||||
<body>
|
||||
<main class="main">
|
||||
<!-- Section 1: Event Log -->
|
||||
<section class="section">
|
||||
<h2 class="section-header">
|
||||
<span>Event Log</span>
|
||||
<div class="header-actions">
|
||||
<select id="log-filter">
|
||||
<option value="all">All Events</option>
|
||||
<option value="ontoolinput">ontoolinput</option>
|
||||
<option value="ontoolinputpartial">ontoolinputpartial</option>
|
||||
<option value="ontoolresult">ontoolresult</option>
|
||||
<option value="ontoolcancelled">ontoolcancelled</option>
|
||||
<option value="onhostcontextchanged">onhostcontextchanged</option>
|
||||
<option value="onteardown">onteardown</option>
|
||||
<option value="oncalltool">oncalltool</option>
|
||||
<option value="onlisttools">onlisttools</option>
|
||||
<option value="onerror">onerror</option>
|
||||
<option value="connected">connected</option>
|
||||
</select>
|
||||
<button id="clear-log-btn" class="btn-small">Clear</button>
|
||||
</div>
|
||||
</h2>
|
||||
<div id="event-log" class="event-log"></div>
|
||||
</section>
|
||||
|
||||
<!-- Section 2: Host Info -->
|
||||
<section class="section collapsible">
|
||||
<h2 class="section-header" data-toggle="host-info-content">
|
||||
<span>Host Info</span>
|
||||
<span class="toggle-icon">▼</span>
|
||||
</h2>
|
||||
<div id="host-info-content" class="section-content">
|
||||
<div class="info-grid">
|
||||
<div class="info-group">
|
||||
<h3>Context</h3>
|
||||
<dl id="host-context-info"></dl>
|
||||
</div>
|
||||
<div class="info-group">
|
||||
<h3>Capabilities</h3>
|
||||
<dl id="host-capabilities-info"></dl>
|
||||
</div>
|
||||
<div class="info-group">
|
||||
<h3>Container</h3>
|
||||
<dl id="host-container-info"></dl>
|
||||
</div>
|
||||
<div class="info-group">
|
||||
<h3>Styles Sample</h3>
|
||||
<div id="host-styles-sample" class="styles-sample"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 3: Callback Status -->
|
||||
<section class="section collapsible">
|
||||
<h2 class="section-header" data-toggle="callback-status-content">
|
||||
<span>Callback Status</span>
|
||||
<span class="toggle-icon">▼</span>
|
||||
</h2>
|
||||
<div id="callback-status-content" class="section-content">
|
||||
<table class="callback-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Callback</th>
|
||||
<th>Registered</th>
|
||||
<th>Count</th>
|
||||
<th>Last Payload</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="callback-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 4: Action Buttons -->
|
||||
<section class="section collapsible">
|
||||
<h2 class="section-header" data-toggle="actions-content">
|
||||
<span>Actions</span>
|
||||
<span class="toggle-icon">▼</span>
|
||||
</h2>
|
||||
<div id="actions-content" class="section-content">
|
||||
<!-- Messages -->
|
||||
<div class="action-group">
|
||||
<h3>Messages</h3>
|
||||
<div class="action-row">
|
||||
<input type="text" id="message-text" value="Hello from debug app!" placeholder="Message text">
|
||||
<button id="send-message-text-btn">Send Text</button>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button id="send-message-image-btn">Send Test Image</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logging -->
|
||||
<div class="action-group">
|
||||
<h3>Logging</h3>
|
||||
<div class="action-row">
|
||||
<input type="text" id="log-data" value="Debug log data" placeholder="Log data">
|
||||
</div>
|
||||
<div class="action-row btn-row">
|
||||
<button id="log-debug-btn" class="btn-small">debug</button>
|
||||
<button id="log-info-btn" class="btn-small">info</button>
|
||||
<button id="log-warning-btn" class="btn-small">warning</button>
|
||||
<button id="log-error-btn" class="btn-small">error</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Context -->
|
||||
<div class="action-group">
|
||||
<h3>Model Context</h3>
|
||||
<div class="action-row">
|
||||
<input type="text" id="context-text" value="Current app state info" placeholder="Context text">
|
||||
<button id="update-context-text-btn">Update (Text)</button>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button id="update-context-structured-btn">Update (Structured)</button>
|
||||
<button id="update-context-image-btn">Update (Image)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Mode -->
|
||||
<div class="action-group">
|
||||
<h3>Display Mode</h3>
|
||||
<div class="action-row btn-row">
|
||||
<button id="display-inline-btn" class="btn-small">Inline</button>
|
||||
<button id="display-fullscreen-btn" class="btn-small">Fullscreen</button>
|
||||
<button id="display-pip-btn" class="btn-small">PiP</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Links -->
|
||||
<div class="action-group">
|
||||
<h3>Links</h3>
|
||||
<div class="action-row">
|
||||
<input type="url" id="link-url" value="https://modelcontextprotocol.io/" placeholder="URL">
|
||||
<button id="open-link-btn">Open Link</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Size Controls -->
|
||||
<div class="action-group">
|
||||
<h3>Size</h3>
|
||||
<div class="action-row">
|
||||
<label><input type="checkbox" id="auto-resize-toggle" checked> Auto-resize</label>
|
||||
</div>
|
||||
<div class="action-row btn-row">
|
||||
<button id="resize-200x100-btn" class="btn-small">200x100</button>
|
||||
<button id="resize-400x300-btn" class="btn-small">400x300</button>
|
||||
<button id="resize-800x600-btn" class="btn-small">800x600</button>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<span>Current: <code id="current-size">measuring...</code></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Server Tools -->
|
||||
<div class="action-group">
|
||||
<h3>Server Tools</h3>
|
||||
<div class="tool-config">
|
||||
<div class="config-row">
|
||||
<label>Content Type:</label>
|
||||
<select id="tool-content-type">
|
||||
<option value="text">text</option>
|
||||
<option value="image">image</option>
|
||||
<option value="audio">audio</option>
|
||||
<option value="resource">resource</option>
|
||||
<option value="resourceLink">resourceLink</option>
|
||||
<option value="mixed">mixed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label><input type="checkbox" id="tool-multiple-blocks"> Multiple blocks</label>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label><input type="checkbox" id="tool-structured-content" checked> Include structuredContent</label>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label><input type="checkbox" id="tool-include-meta"> Include _meta</label>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label><input type="checkbox" id="tool-simulate-error"> Simulate error</label>
|
||||
</div>
|
||||
<div class="config-row">
|
||||
<label>Delay (ms):</label>
|
||||
<input type="number" id="tool-delay-ms" value="0" min="0" max="10000" step="100">
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button id="call-debug-tool-btn">Call debug-tool</button>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<button id="call-debug-refresh-btn">Call debug-refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script type="module" src="/src/mcp-app.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
42
examples/debug-server/package.json
Normal file
42
examples/debug-server/package.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "@modelcontextprotocol/server-debug",
|
||||
"version": "0.4.1",
|
||||
"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",
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"",
|
||||
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
|
||||
"serve": "bun --watch main.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.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"
|
||||
}
|
||||
}
|
||||
BIN
examples/debug-server/screenshot.png
Normal file
BIN
examples/debug-server/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
294
examples/debug-server/server.ts
Normal file
294
examples/debug-server/server.ts
Normal file
|
|
@ -0,0 +1,294 @@
|
|||
import {
|
||||
registerAppResource,
|
||||
registerAppTool,
|
||||
RESOURCE_MIME_TYPE,
|
||||
} from "@modelcontextprotocol/ext-apps/server";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.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";
|
||||
|
||||
// Works both from source (server.ts) and compiled (dist/server.js)
|
||||
const DIST_DIR = import.meta.filename.endsWith(".ts")
|
||||
? path.join(import.meta.dirname, "dist")
|
||||
: import.meta.dirname;
|
||||
|
||||
// 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==";
|
||||
|
||||
// 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(true),
|
||||
includeStructuredContent: z.boolean().default(true),
|
||||
includeMeta: z.boolean().default(true),
|
||||
|
||||
// Streaming test (large input)
|
||||
largeInput: z.string().optional(),
|
||||
|
||||
// Error/delay simulation
|
||||
simulateError: z.boolean().default(false),
|
||||
delayMs: z.number().optional(),
|
||||
});
|
||||
|
||||
type DebugInput = z.infer<typeof DebugInputSchema>;
|
||||
|
||||
/**
|
||||
* 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<CallToolResult> => {
|
||||
// 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<CallToolResult> => {
|
||||
const timestamp = new Date().toISOString();
|
||||
return {
|
||||
content: [{ type: "text", text: `Server timestamp: ${timestamp}` }],
|
||||
structuredContent: { timestamp, counter: callCounter },
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// 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<CallToolResult> => {
|
||||
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,
|
||||
resourceUri,
|
||||
resourceUri,
|
||||
{ mimeType: RESOURCE_MIME_TYPE },
|
||||
async (): Promise<ReadResourceResult> => {
|
||||
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;
|
||||
}
|
||||
33
examples/debug-server/src/global.css
Normal file
33
examples/debug-server/src/global.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
367
examples/debug-server/src/mcp-app.css
Normal file
367
examples/debug-server/src/mcp-app.css
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
.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);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.log-entry:hover {
|
||||
background: var(--color-bg-hover, rgba(0, 0, 0, 0.03));
|
||||
}
|
||||
|
||||
.log-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-entry-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.log-toggle {
|
||||
color: #9ca3af;
|
||||
font-size: 0.7rem;
|
||||
width: 1rem;
|
||||
transition: transform 0.15s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.log-entry.expanded .log-toggle {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #6b7280;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.log-type {
|
||||
font-weight: 600;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.log-type.ontoolinput { color: var(--color-primary); }
|
||||
.log-type.ontoolinputpartial { color: #8b5cf6; }
|
||||
.log-type.ontoolresult { color: var(--color-success); }
|
||||
.log-type.ontoolcancelled { color: var(--color-warning); }
|
||||
.log-type.onhostcontextchanged { color: #7c3aed; }
|
||||
.log-type.onteardown { color: #f97316; }
|
||||
.log-type.oncalltool { color: #ec4899; }
|
||||
.log-type.onlisttools { color: #14b8a6; }
|
||||
.log-type.onerror { color: var(--color-error); }
|
||||
.log-type.connected { color: var(--color-success); }
|
||||
.log-type.update-context { color: #0891b2; }
|
||||
|
||||
.log-payload-preview {
|
||||
color: #6b7280;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.log-payload-full {
|
||||
display: none;
|
||||
margin: 0.5rem 0 0.25rem 1.25rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--color-bg-subtle, #f3f4f6);
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-entry.expanded .log-payload-full {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
642
examples/debug-server/src/mcp-app.ts
Normal file
642
examples/debug-server/src/mcp-app.ts
Normal file
|
|
@ -0,0 +1,642 @@
|
|||
/**
|
||||
* @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<string, number>;
|
||||
lastPayloads: Map<string, unknown>;
|
||||
autoResizeCleanup: (() => void) | null;
|
||||
logFilter: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// State
|
||||
// ============================================================================
|
||||
|
||||
const state: AppState = {
|
||||
eventLog: [],
|
||||
callbackCounts: new Map(),
|
||||
lastPayloads: new Map(),
|
||||
autoResizeCleanup: null,
|
||||
logFilter: "all",
|
||||
};
|
||||
|
||||
// Callbacks we track
|
||||
const CALLBACKS = [
|
||||
"ontoolinput",
|
||||
"ontoolinputpartial",
|
||||
"ontoolresult",
|
||||
"ontoolcancelled",
|
||||
"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 updateContextImageBtn = document.getElementById(
|
||||
"update-context-image-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")!;
|
||||
|
||||
// ============================================================================
|
||||
// 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);
|
||||
|
||||
const fullPayload = (payload: unknown) =>
|
||||
JSON.stringify(payload, null, 2)
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
eventLogEl.innerHTML = filtered
|
||||
.map(
|
||||
(entry, index) => `
|
||||
<div class="log-entry" data-index="${index}">
|
||||
<div class="log-entry-header">
|
||||
<span class="log-toggle">▶</span>
|
||||
<span class="log-time">[${formatTime(entry.time)}]</span>
|
||||
<span class="log-type ${entry.type}">${entry.type}:</span>
|
||||
<span class="log-payload-preview">${truncatePayload(entry.payload)}</span>
|
||||
</div>
|
||||
<pre class="log-payload-full">${fullPayload(entry.payload)}</pre>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
// Add click handlers for toggling
|
||||
eventLogEl.querySelectorAll(".log-entry").forEach((el) => {
|
||||
el.addEventListener("click", () => {
|
||||
el.classList.toggle("expanded");
|
||||
});
|
||||
});
|
||||
|
||||
// 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 `
|
||||
<tr>
|
||||
<td><code>${name}</code></td>
|
||||
<td class="${registered ? "registered-yes" : "registered-no"}">${registered ? "✓" : "✗"}</td>
|
||||
<td>${count}</td>
|
||||
<td class="payload-preview">${lastPayload ? truncatePayload(lastPayload) : "-"}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function renderHostInfo(): void {
|
||||
const ctx = app.getHostContext();
|
||||
const caps = app.getHostCapabilities();
|
||||
const version = app.getHostVersion();
|
||||
|
||||
// Context info
|
||||
if (ctx) {
|
||||
hostContextInfoEl.innerHTML = `
|
||||
<dt>Theme</dt><dd>${ctx.theme ?? "unknown"}</dd>
|
||||
<dt>Locale</dt><dd>${ctx.locale ?? "unknown"}</dd>
|
||||
<dt>TimeZone</dt><dd>${ctx.timeZone ?? "unknown"}</dd>
|
||||
<dt>Platform</dt><dd>${ctx.platform ?? "unknown"}</dd>
|
||||
<dt>Display Mode</dt><dd>${ctx.displayMode ?? "unknown"}</dd>
|
||||
<dt>Host</dt><dd>${version?.name ?? "unknown"} v${version?.version ?? "?"}</dd>
|
||||
`;
|
||||
} else {
|
||||
hostContextInfoEl.innerHTML = "<dd>No context available</dd>";
|
||||
}
|
||||
|
||||
// Capabilities
|
||||
if (caps) {
|
||||
hostCapabilitiesInfoEl.innerHTML = `
|
||||
<dt>openLinks</dt><dd>${caps.openLinks ? "✓" : "✗"}</dd>
|
||||
<dt>serverTools</dt><dd>${caps.serverTools ? "✓" : "✗"}</dd>
|
||||
<dt>serverResources</dt><dd>${caps.serverResources ? "✓" : "✗"}</dd>
|
||||
<dt>logging</dt><dd>${caps.logging ? "✓" : "✗"}</dd>
|
||||
<dt>message</dt><dd>${caps.message ? "✓" : "✗"}</dd>
|
||||
<dt>updateModelContext</dt><dd>${caps.updateModelContext ? "✓" : "✗"}</dd>
|
||||
`;
|
||||
} else {
|
||||
hostCapabilitiesInfoEl.innerHTML = "<dd>No capabilities available</dd>";
|
||||
}
|
||||
|
||||
// Container info
|
||||
if (ctx?.containerDimensions) {
|
||||
const dims = ctx.containerDimensions;
|
||||
hostContainerInfoEl.innerHTML = `
|
||||
<dt>Width</dt><dd>${"width" in dims ? dims.width + "px" : `max ${dims.maxWidth ?? "?"}px`}</dd>
|
||||
<dt>Height</dt><dd>${"height" in dims ? dims.height + "px" : `max ${dims.maxHeight ?? "?"}px`}</dd>
|
||||
<dt>Safe Area</dt><dd>${ctx.safeAreaInsets ? `T${ctx.safeAreaInsets.top} R${ctx.safeAreaInsets.right} B${ctx.safeAreaInsets.bottom} L${ctx.safeAreaInsets.left}` : "none"}</dd>
|
||||
`;
|
||||
} else {
|
||||
hostContainerInfoEl.innerHTML = "<dd>No container info</dd>";
|
||||
}
|
||||
|
||||
// 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 `<div class="style-swatch" style="background: ${color};" title="${key}: ${color}"></div>`;
|
||||
})
|
||||
.join("");
|
||||
} else {
|
||||
hostStylesSampleEl.innerHTML = "<span>No styles</span>";
|
||||
}
|
||||
}
|
||||
|
||||
function updateCurrentSize(): void {
|
||||
const w = document.documentElement.scrollWidth;
|
||||
const h = document.documentElement.scrollHeight;
|
||||
currentSizeEl.textContent = `${w}x${h}`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Event Logging
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Send a log entry to the server's debug-log tool (writes to file)
|
||||
*/
|
||||
async function sendToServerLog(type: string, payload: unknown): Promise<void> {
|
||||
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, type, payload });
|
||||
|
||||
// Keep log manageable (max 100 entries)
|
||||
if (state.eventLog.length > 100) {
|
||||
state.eventLog.shift();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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" },
|
||||
{ tools: { listChanged: true } }, // Declare tools capability for oncalltool/onlisttools
|
||||
{ autoResize: false }, // We'll manage auto-resize ourselves for toggle demo
|
||||
);
|
||||
|
||||
// Register ALL callbacks BEFORE connecting
|
||||
app.ontoolinput = (params) => {
|
||||
logEvent("ontoolinput", params);
|
||||
};
|
||||
|
||||
app.ontoolinputpartial = (params) => {
|
||||
logEvent("ontoolinputpartial", params);
|
||||
};
|
||||
|
||||
app.ontoolresult = (result) => {
|
||||
logEvent("ontoolresult", result);
|
||||
};
|
||||
|
||||
app.ontoolcancelled = (params) => {
|
||||
logEvent("ontoolcancelled", params);
|
||||
};
|
||||
|
||||
app.onhostcontextchanged = (ctx) => {
|
||||
logEvent("onhostcontextchanged", ctx);
|
||||
handleHostContextChanged(ctx);
|
||||
};
|
||||
|
||||
app.onteardown = async (params) => {
|
||||
logEvent("onteardown", params);
|
||||
return {};
|
||||
};
|
||||
|
||||
app.oncalltool = async (params) => {
|
||||
logEvent("oncalltool", params);
|
||||
return {
|
||||
content: [{ type: "text", text: "App handled tool call" }],
|
||||
};
|
||||
};
|
||||
|
||||
app.onlisttools = async (params) => {
|
||||
logEvent("onlisttools", params);
|
||||
return { tools: [] };
|
||||
};
|
||||
|
||||
app.onerror = (error) => {
|
||||
logEvent("onerror", 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(),
|
||||
},
|
||||
},
|
||||
});
|
||||
logEvent("update-context", { type: "structured" });
|
||||
} catch (e) {
|
||||
logEvent("error", e);
|
||||
}
|
||||
});
|
||||
|
||||
updateContextImageBtn.addEventListener("click", async () => {
|
||||
// 1x1 red PNG for testing
|
||||
const redPng =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
|
||||
try {
|
||||
await app.updateModelContext({
|
||||
content: [{ type: "image", data: redPng, mimeType: "image/png" }],
|
||||
});
|
||||
logEvent("update-context", { type: "image" });
|
||||
} catch (e) {
|
||||
logEvent("error", e);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Display Mode Actions
|
||||
// ============================================================================
|
||||
|
||||
async function requestDisplayMode(
|
||||
mode: "inline" | "fullscreen" | "pip",
|
||||
): Promise<void> {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// 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);
|
||||
});
|
||||
19
examples/debug-server/tsconfig.json
Normal file
19
examples/debug-server/tsconfig.json
Normal file
|
|
@ -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"]
|
||||
}
|
||||
17
examples/debug-server/tsconfig.server.json
Normal file
17
examples/debug-server/tsconfig.server.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": ".",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["server.ts"]
|
||||
}
|
||||
24
examples/debug-server/vite.config.ts
Normal file
24
examples/debug-server/vite.config.ts
Normal file
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
@ -6,5 +6,8 @@
|
|||
"start": "uv run server.py",
|
||||
"dev": "uv run server.py",
|
||||
"build": "echo 'No build step needed for Python server'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/ext-apps": "^0.4.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,8 @@
|
|||
"start": "uv run server.py",
|
||||
"dev": "uv run server.py",
|
||||
"build": "echo 'No build step needed for Python server'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/ext-apps": "^0.4.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
870
package-lock.json
generated
870
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -55,6 +55,7 @@ const ALL_SERVERS = [
|
|||
name: "Customer Segmentation Server",
|
||||
dir: "customer-segmentation-server",
|
||||
},
|
||||
{ key: "debug-server", name: "Debug MCP App Server", dir: "debug-server" },
|
||||
{ key: "map-server", name: "Map Server", dir: "map-server" },
|
||||
{ key: "pdf-server", name: "PDF Server", dir: "pdf-server" },
|
||||
{ key: "qr-server", name: "QR Code Server", dir: "qr-server" },
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue