Skip to content

Commit c557635

Browse files
committed
feat(pdf-server): add --debug flag, fix interact timeout, skip persist for read-only cmds
- Wire --debug CLI flag through to createServer; _debug diagnostic block in display_pdf _meta is now only emitted when --debug is set. - Viewer: showDebugBubble renders _debug payload as a fixed overlay. - Fix interact get_pages timeout: await handleGetPages instead of fire-and-forget, so submit_page_data doesn't queue behind the next 30s long-poll on serialized host connections. - Skip persistAnnotations for read-only commands (get_pages, file_changed).
1 parent dc72bea commit c557635

2 files changed

Lines changed: 74 additions & 11 deletions

File tree

examples/pdf-server/main.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
cliLocalFiles,
2424
DEFAULT_PDF,
2525
allowedLocalDirs,
26+
writeFlags,
2627
} from "./server.js";
2728

2829
/**
@@ -95,12 +96,14 @@ function parseArgs(): {
9596
stdio: boolean;
9697
useClientRoots: boolean;
9798
enableInteract: boolean;
99+
debug: boolean;
98100
} {
99101
const args = process.argv.slice(2);
100102
const urls: string[] = [];
101103
let stdio = false;
102104
let useClientRoots = false;
103105
let enableInteract = false;
106+
let debug = false;
104107

105108
for (const arg of args) {
106109
if (arg === "--stdio") {
@@ -113,6 +116,12 @@ function parseArgs(): {
113116
// the command queue is in-memory per-process, so stateless
114117
// multi-instance deployments will drop commands.
115118
enableInteract = true;
119+
} else if (arg === "--debug") {
120+
debug = true;
121+
} else if (arg === "--writeable-uploads-root") {
122+
// Claude Desktop mounts attachments under a dir root named "uploads";
123+
// by default we refuse to write there. This flag opts back in.
124+
writeFlags.allowUploadsRoot = true;
116125
} else if (!arg.startsWith("-")) {
117126
// Convert local paths to file:// URLs, normalize arxiv URLs
118127
let url = arg;
@@ -134,11 +143,12 @@ function parseArgs(): {
134143
stdio,
135144
useClientRoots,
136145
enableInteract,
146+
debug,
137147
};
138148
}
139149

140150
async function main() {
141-
const { urls, stdio, useClientRoots, enableInteract } = parseArgs();
151+
const { urls, stdio, useClientRoots, enableInteract, debug } = parseArgs();
142152

143153
// Register local files in whitelist
144154
for (const url of urls) {
@@ -165,7 +175,7 @@ async function main() {
165175
if (stdio) {
166176
// stdio → client is local (e.g. Claude Desktop), roots are safe
167177
await startStdioServer(() =>
168-
createServer({ enableInteract: true, useClientRoots: true }),
178+
createServer({ enableInteract: true, useClientRoots: true, debug }),
169179
);
170180
} else {
171181
// HTTP → client is remote, only honour roots with explicit opt-in
@@ -176,7 +186,7 @@ async function main() {
176186
);
177187
}
178188
await startStreamableHTTPServer(() =>
179-
createServer({ useClientRoots, enableInteract }),
189+
createServer({ useClientRoots, enableInteract, debug }),
180190
);
181191
}
182192
}

examples/pdf-server/src/mcp-app.ts

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,46 @@ function updateTitleDisplay(): void {
761761
titleEl.title = pdfUrl;
762762
}
763763

764+
/**
765+
* Debug overlay: fixed-position bubble, bottom-left. Pretty-printed JSON
766+
* dump of whatever the server stuffed into `_meta._debug`. Tooltips inside
767+
* sandboxed iframes are unreliable; this survives the cross-origin barrier
768+
* and shows up in screenshots.
769+
*/
770+
function showDebugBubble(debug: unknown): void {
771+
const bubble = document.createElement("div");
772+
const base =
773+
"position:fixed;bottom:8px;left:8px;z-index:99999;" +
774+
"background:rgba(20,20,30,0.92);color:#cfe;padding:8px 12px;" +
775+
"font:11px/1.4 monospace;border-radius:6px;" +
776+
"box-shadow:0 2px 8px rgba(0,0,0,0.4);white-space:pre;cursor:pointer;" +
777+
"transition:max-width 0.15s ease;";
778+
// Collapsed: clip to 60vw. Hover: expand to fit full paths (up to ~96vw),
779+
// scrollable both axes in case the JSON is tall.
780+
const collapsed =
781+
base +
782+
"max-width:60vw;max-height:40vh;overflow:hidden;text-overflow:ellipsis;";
783+
const expanded =
784+
base + "max-width:calc(100vw - 32px);max-height:80vh;overflow:auto;";
785+
bubble.style.cssText = collapsed;
786+
// Latch expanded on click so hover-collapse doesn't fight text selection.
787+
let pinned = false;
788+
bubble.onmouseenter = () => {
789+
bubble.style.cssText = expanded;
790+
};
791+
bubble.onmouseleave = () => {
792+
if (!pinned) bubble.style.cssText = collapsed;
793+
};
794+
bubble.onclick = () => {
795+
pinned = true;
796+
bubble.style.cssText = expanded;
797+
};
798+
bubble.ondblclick = () => bubble.remove();
799+
bubble.title = "Click: pin open • Double-click: dismiss";
800+
bubble.textContent = "🐞 " + JSON.stringify(debug, null, 2);
801+
document.body.appendChild(bubble);
802+
}
803+
764804
function updateControls() {
765805
// Show URL with CSS ellipsis, full URL as tooltip, clickable to open
766806
updateTitleDisplay();
@@ -4872,6 +4912,8 @@ app.ontoolresult = async (result: CallToolResult) => {
48724912
viewUUID = result._meta?.viewUUID ? String(result._meta.viewUUID) : undefined;
48734913
interactEnabled = result._meta?.interactEnabled === true;
48744914
fileWritable = result._meta?.writable === true;
4915+
// TODO remove — debug: dump writability inputs so we can eyeball the mismatch
4916+
if (result._meta?._debug !== undefined) showDebugBubble(result._meta._debug);
48754917

48764918
// Restore saved page or use initial page
48774919
const savedPage = loadSavedPage();
@@ -5128,18 +5170,23 @@ async function processCommands(commands: PdfCommand[]): Promise<void> {
51285170
renderAnnotationPanel();
51295171
break;
51305172
case "get_pages":
5131-
// Handle async — don't block the poll loop. But if it rejects,
5132-
// submit an empty payload so interact returns an error promptly
5133-
// instead of blocking 45s in waitForPageData.
5134-
handleGetPages(cmd).catch((err) => {
5173+
// Await so the next poll doesn't start until submit_page_data has
5174+
// been SENT. The host (Claude Desktop/Nest) serializes iframe→server
5175+
// tool calls — if we re-poll immediately, submit_page_data queues
5176+
// behind the 30s long-poll and interact times out. Awaiting costs a
5177+
// few seconds of poll gap, but interact is blocked in waitForPageData
5178+
// anyway so no commands are lost.
5179+
try {
5180+
await handleGetPages(cmd);
5181+
} catch (err) {
51355182
log.error("get_pages failed — submitting empty result:", err);
5136-
app
5183+
await app
51375184
.callServerTool({
51385185
name: "submit_page_data",
51395186
arguments: { requestId: cmd.requestId, pages: [] },
51405187
})
51415188
.catch(() => {});
5142-
});
5189+
}
51435190
break;
51445191
case "file_changed": {
51455192
// Skip our own save_pdf echo: either save is still in flight, or the
@@ -5179,8 +5226,14 @@ async function processCommands(commands: PdfCommand[]): Promise<void> {
51795226
}
51805227
}
51815228

5182-
// Persist after processing batch
5183-
persistAnnotations();
5229+
// Persist after processing batch — but only if anything mutated.
5230+
// get_pages / file_changed are read-only; writing localStorage and
5231+
// recomputing the diff for them is wasted work.
5232+
if (
5233+
commands.some((c) => c.type !== "get_pages" && c.type !== "file_changed")
5234+
) {
5235+
persistAnnotations();
5236+
}
51845237
}
51855238

51865239
let polling = false;

0 commit comments

Comments
 (0)