fix(pdf-server): resolve Windows file:// URLs to the right path by tzipora-rose · Pull Request #816 · modelcontextprotocol/ext-apps · GitHub
Skip to content

fix(pdf-server): resolve Windows file:// URLs to the right path - #816

Open
tzipora-rose wants to merge 1 commit into
modelcontextprotocol:mainfrom
tzipora-rose:fix/pdf-server-windows-file-urls
Open

tzipora-rose wants to merge 1 commit into
modelcontextprotocol:mainfrom
tzipora-rose:fix/pdf-server-windows-file-urls

Conversation

@tzipora-rose

@tzipora-rose tzipora-rose commented Oct 20, 2026 •

Copy link
Copy Markdown

This fixes the second Windows bug reported in a comment on #644, the one #805 leaves out of scope.

fileUrlToPath() only strips the scheme. For a Windows file URL that leaves a slash before the drive letter, and path.resolve() then reads the result as a path on the current drive:

file:///C:/Users/me/project  ->  "/C:/Users/me/project"  ->  path.resolve  ->  C:\C:\Users\me\project

refreshRoots() stats that path, the stat fails, and the root is skipped without a message, so on Windows no client root is ever added. The same helper converts file:// URLs in validateUrl(), readPdfRange(), display_pdf's writability check, image annotations, save_as and save_pdf, so all of those resolve the same wrong way.

Fix

On Windows, after decoding, drop the slash when it comes right before a drive letter (/C:/x -> C:/x). Nothing else changes:

  • the URLs pathToFileUrl() builds on Windows (file://C%3A%5C..., the whole path encoded) still decode to C:\..., so CLI-arg directories still round-trip;
  • POSIX paths, and every URL on macOS and Linux, are untouched;
  • an unencoded # or ? stays part of the path, as before.

fileUrlToPath() takes the platform as an optional second argument (default process.platform), so the Windows behavior is unit-tested on every OS.

The comment on #644 suggests Node's url.fileURLToPath(). I didn't use it because it changes more than this bug: it throws on the URLs pathToFileUrl() builds (ERR_INVALID_URL for file://C%3A%5CUsers%5Ctest), and it cuts a path at an unencoded # or ? (file:///C:/Docs/Report #3.pdf -> C:\Docs\Report ), which the current code accepts. Checked in Node 24.16.0 and Bun 1.3.10.

Tests

A new describe("fileUrlToPath") in examples/pdf-server/server.test.ts:

  • drive letters (upper and lower case, a bare drive, percent-encoding, computer://) with platform set to "win32";
  • the pathToFileUrl() form and POSIX paths unchanged;
  • a round trip through node:url's pathToFileURL() and through pathToFileUrl();
  • on Windows only (it.skipIf), a client that declares roots sends a root built with pathToFileURL(), and after the initialize handshake the directory must be in allowedLocalDirs.

On Windows 10 with Bun 1.3.10: the pdf-server tests pass (96 pass, 2 skipped). With server.ts from main and the new tests, 5 of the 7 new tests fail; the two that pass check that nothing else changed. npm test: 445 pass, 2 skipped. tsc --noEmit passes for both of the example's tsconfigs, and Prettier reports no changes.

The same change, applied as a patch to the pdf-viewer extension that Claude Desktop installs, has also run inside the app on Windows 10 (Claude Desktop 2.42304.0.0, pdf-viewer 1.7.4, 1.7.5 and 2.0.0): the app's root file:///C:/ arrives and the server logs Root directory allowed: C:\. That patch is part of a toolkit we published for people who need it before a fix ships: https://github.com/tzipora-rose/pdf-local-read

Not in this PR

Investigated and written in collaboration with Claudia in Claude Code.

fileUrlToPath() only stripped the scheme, so file:///C:/x became "/C:/x",
which path.resolve() turns into C:\C:\x on Windows. Every client root was
then dropped by refreshRoots(), and file:// arguments to display_pdf,
save_as and save_pdf resolved to the wrong path.

On win32, drop the slash before a drive letter after decoding. The URLs
pathToFileUrl() builds, POSIX paths and other platforms are unchanged.

Co-Authored-By: Claudia <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant