Skip to content

Commit fd27d7d

Browse files
committed
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
1 parent 0d9a236 commit fd27d7d

2 files changed

Lines changed: 235 additions & 62 deletions

File tree

examples/debug-server/server.ts

Lines changed: 110 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
1+
import {
2+
registerAppResource,
3+
registerAppTool,
4+
RESOURCE_MIME_TYPE,
5+
} from "@modelcontextprotocol/ext-apps/server";
26
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
37
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4-
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
8+
import type {
9+
CallToolResult,
10+
ReadResourceResult,
11+
} from "@modelcontextprotocol/sdk/types.js";
512
import fs from "node:fs/promises";
13+
import { appendFileSync } from "node:fs";
614
import path from "node:path";
715
import { z } from "zod";
816
import { startServer } from "./server-utils.js";
@@ -12,18 +20,50 @@ const DIST_DIR = path.join(import.meta.dirname, "dist");
1220
// Track call counter across requests (stateful for demo purposes)
1321
let callCounter = 0;
1422

23+
// Parse --log-file argument or use default
24+
const DEFAULT_LOG_FILE = "/tmp/mcp-apps-debug-server.log";
25+
function getLogFilePath(): string {
26+
const logFileArg = process.argv.find((arg) => arg.startsWith("--log-file="));
27+
if (logFileArg) {
28+
return logFileArg.split("=")[1];
29+
}
30+
return process.env.DEBUG_LOG_FILE ?? DEFAULT_LOG_FILE;
31+
}
32+
33+
const logFilePath = getLogFilePath();
34+
35+
/**
36+
* Append a log entry to the log file
37+
*/
38+
function appendToLogFile(entry: {
39+
timestamp: string;
40+
type: string;
41+
payload: unknown;
42+
}): void {
43+
try {
44+
const line = JSON.stringify(entry) + "\n";
45+
appendFileSync(logFilePath, line, "utf-8");
46+
} catch (e) {
47+
console.error("[debug-server] Failed to write to log file:", e);
48+
}
49+
}
50+
1551
// Minimal 1x1 blue PNG (base64)
16-
const BLUE_PNG_1X1 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPj/HwADBwIAMCbHYQAAAABJRU5ErkJggg==";
52+
const BLUE_PNG_1X1 =
53+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPj/HwADBwIAMCbHYQAAAABJRU5ErkJggg==";
1754

1855
// Minimal silent WAV (base64) - 44 byte header + 1 sample
19-
const SILENT_WAV = "UklGRiYAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQIAAAAAAA==";
56+
const SILENT_WAV =
57+
"UklGRiYAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQIAAAAAAA==";
2058

2159
/**
2260
* Input schema for the debug-tool
2361
*/
2462
const DebugInputSchema = z.object({
2563
// Content configuration
26-
contentType: z.enum(["text", "image", "audio", "resource", "resourceLink", "mixed"]).default("text"),
64+
contentType: z
65+
.enum(["text", "image", "audio", "resource", "resourceLink", "mixed"])
66+
.default("text"),
2767
multipleBlocks: z.boolean().default(false),
2868
includeStructuredContent: z.boolean().default(true),
2969
includeMeta: z.boolean().default(false),
@@ -63,10 +103,18 @@ function buildContent(args: DebugInput): CallToolResult["content"] {
63103
content.push({ type: "text", text: `Debug text content${suffix}` });
64104
break;
65105
case "image":
66-
content.push({ type: "image", data: BLUE_PNG_1X1, mimeType: "image/png" });
106+
content.push({
107+
type: "image",
108+
data: BLUE_PNG_1X1,
109+
mimeType: "image/png",
110+
});
67111
break;
68112
case "audio":
69-
content.push({ type: "audio", data: SILENT_WAV, mimeType: "audio/wav" });
113+
content.push({
114+
type: "audio",
115+
data: SILENT_WAV,
116+
mimeType: "audio/wav",
117+
});
70118
break;
71119
case "resource":
72120
content.push({
@@ -111,19 +159,21 @@ export function createServer(): McpServer {
111159
const resourceUri = "ui://debug-tool/mcp-app.html";
112160

113161
// Main debug tool - exercises all result variations
114-
registerAppTool(server,
162+
registerAppTool(
163+
server,
115164
"debug-tool",
116165
{
117166
title: "Debug Tool",
118-
description: "Comprehensive debug tool for testing MCP Apps SDK. Configure content types, error simulation, delays, and more.",
167+
description:
168+
"Comprehensive debug tool for testing MCP Apps SDK. Configure content types, error simulation, delays, and more.",
119169
inputSchema: DebugInputSchema,
120170
outputSchema: DebugOutputSchema,
121171
_meta: { ui: { resourceUri } },
122172
},
123173
async (args): Promise<CallToolResult> => {
124174
// Apply delay if requested
125175
if (args.delayMs && args.delayMs > 0) {
126-
await new Promise(resolve => setTimeout(resolve, args.delayMs));
176+
await new Promise((resolve) => setTimeout(resolve, args.delayMs));
127177
}
128178

129179
// Build content based on config
@@ -138,7 +188,9 @@ export function createServer(): McpServer {
138188
config: args,
139189
timestamp: new Date().toISOString(),
140190
counter: ++callCounter,
141-
...(args.largeInput ? { largeInputLength: args.largeInput.length } : {}),
191+
...(args.largeInput
192+
? { largeInputLength: args.largeInput.length }
193+
: {}),
142194
};
143195
}
144196

@@ -162,11 +214,13 @@ export function createServer(): McpServer {
162214
);
163215

164216
// App-only refresh tool (hidden from model)
165-
registerAppTool(server,
217+
registerAppTool(
218+
server,
166219
"debug-refresh",
167220
{
168221
title: "Refresh Debug Info",
169-
description: "App-only tool for polling server state. Not visible to the model.",
222+
description:
223+
"App-only tool for polling server state. Not visible to the model.",
170224
inputSchema: z.object({}),
171225
outputSchema: z.object({ timestamp: z.string(), counter: z.number() }),
172226
_meta: {
@@ -185,13 +239,47 @@ export function createServer(): McpServer {
185239
},
186240
);
187241

242+
// App-only log tool - writes events to log file
243+
registerAppTool(
244+
server,
245+
"debug-log",
246+
{
247+
title: "Log to File",
248+
description:
249+
"App-only tool for logging events to the server log file. Not visible to the model.",
250+
inputSchema: z.object({
251+
type: z.string(),
252+
payload: z.unknown(),
253+
}),
254+
outputSchema: z.object({ logged: z.boolean(), logFile: z.string() }),
255+
_meta: {
256+
ui: {
257+
resourceUri,
258+
visibility: ["app"],
259+
},
260+
},
261+
},
262+
async (args): Promise<CallToolResult> => {
263+
const timestamp = new Date().toISOString();
264+
appendToLogFile({ timestamp, type: args.type, payload: args.payload });
265+
return {
266+
content: [{ type: "text", text: `Logged to ${logFilePath}` }],
267+
structuredContent: { logged: true, logFile: logFilePath },
268+
};
269+
},
270+
);
271+
188272
// Register the resource which returns the bundled HTML/JavaScript for the UI
189-
registerAppResource(server,
273+
registerAppResource(
274+
server,
190275
resourceUri,
191276
resourceUri,
192277
{ mimeType: RESOURCE_MIME_TYPE },
193278
async (): Promise<ReadResourceResult> => {
194-
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
279+
const html = await fs.readFile(
280+
path.join(DIST_DIR, "mcp-app.html"),
281+
"utf-8",
282+
);
195283

196284
return {
197285
contents: [
@@ -205,6 +293,13 @@ export function createServer(): McpServer {
205293
}
206294

207295
async function main() {
296+
console.log(`[debug-server] Log file: ${logFilePath}`);
297+
appendToLogFile({
298+
timestamp: new Date().toISOString(),
299+
type: "server-start",
300+
payload: { logFilePath, pid: process.pid },
301+
});
302+
208303
if (process.argv.includes("--stdio")) {
209304
await createServer().connect(new StdioServerTransport());
210305
} else {

0 commit comments

Comments
 (0)