Skip to content

Commit 30f79b9

Browse files
ochafikclaude
andauthored
ignore client roots by default in pdf-server (#510)
* security: ignore client roots by default in pdf-server When the pdf-server is started with --stdio, MCP clients may advertise roots that refer to directories on the *client's* file system. Because the server resolves those paths locally, accepting them by default would give the remote client access to arbitrary directories on the server's machine. This commit makes client roots opt-in via the --use-client-roots flag. Without the flag, the server logs a notice and skips roots setup entirely. The createServer() function now accepts a CreateServerOptions object with a `useClientRoots` boolean (defaults to false). https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw * Enable client roots by default for HTTP, keep off for stdio HTTP mode serves a local client so roots are safe. Stdio mode may have a remote client whose roots would resolve against the server's filesystem, so roots stay off unless --use-client-roots is passed. https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw * fix: correct transport-aware defaults for useClientRoots stdio = local client (e.g. Claude Desktop) → auto-enable roots HTTP = remote client → ignore roots unless --use-client-roots passed The previous commit had the logic inverted. https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw * refactor: inline useClientRoots per transport branch Remove the intermediate effectiveUseClientRoots variable — just pass `true` in the stdio branch and the flag value in the HTTP branch. https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw * docs: update README to match transport-aware client roots defaults Stdio always enables roots (client is local); HTTP ignores them by default. The previous README text had these reversed. https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 87e30ed commit 30f79b9

File tree

4 files changed

+103
-17
lines changed

4 files changed

+103
-17
lines changed

examples/pdf-server/README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,27 @@ bun examples/pdf-server/main.ts ./local.pdf https://arxiv.org/pdf/2401.00001.pdf
149149
bun examples/pdf-server/main.ts --stdio ./papers/
150150
```
151151

152+
## Security: Client Roots
153+
154+
MCP clients may advertise **roots**`file://` URIs pointing to directories on the client's file system. The server uses these to allow access to local files under those directories.
155+
156+
- **Stdio mode** (`--stdio`): Client roots are **always enabled** — the client is typically on the same machine (e.g. Claude Desktop), so the roots are safe.
157+
- **HTTP mode** (default): Client roots are **ignored** by default — the client may be remote, and its roots would be resolved against the server's filesystem. To opt in, pass `--use-client-roots`:
158+
159+
```bash
160+
# Trust that the HTTP client is local and its roots are safe
161+
bun examples/pdf-server/main.ts --use-client-roots
162+
```
163+
164+
When roots are ignored the server logs:
165+
166+
```
167+
[pdf-server] Client roots are ignored (default for remote transports). Pass --use-client-roots to allow the client to expose local directories.
168+
```
169+
152170
## Allowed Sources
153171

154-
- **Local files**: Must be passed as CLI arguments
172+
- **Local files**: Must be passed as CLI arguments (or via client roots when enabled)
155173
- **Remote URLs**: arxiv.org, biorxiv.org, medrxiv.org, chemrxiv.org, zenodo.org, osf.io, hal.science, ssrn.com, and more
156174

157175
## Tools

examples/pdf-server/main.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,21 @@ export async function startStdioServer(
8989
await createServer().connect(new StdioServerTransport());
9090
}
9191

92-
function parseArgs(): { urls: string[]; stdio: boolean } {
92+
function parseArgs(): {
93+
urls: string[];
94+
stdio: boolean;
95+
useClientRoots: boolean;
96+
} {
9397
const args = process.argv.slice(2);
9498
const urls: string[] = [];
9599
let stdio = false;
100+
let useClientRoots = false;
96101

97102
for (const arg of args) {
98103
if (arg === "--stdio") {
99104
stdio = true;
105+
} else if (arg === "--use-client-roots") {
106+
useClientRoots = true;
100107
} else if (!arg.startsWith("-")) {
101108
// Convert local paths to file:// URLs, normalize arxiv URLs
102109
let url = arg;
@@ -113,11 +120,15 @@ function parseArgs(): { urls: string[]; stdio: boolean } {
113120
}
114121
}
115122

116-
return { urls: urls.length > 0 ? urls : [DEFAULT_PDF], stdio };
123+
return {
124+
urls: urls.length > 0 ? urls : [DEFAULT_PDF],
125+
stdio,
126+
useClientRoots,
127+
};
117128
}
118129

119130
async function main() {
120-
const { urls, stdio } = parseArgs();
131+
const { urls, stdio, useClientRoots } = parseArgs();
121132

122133
// Register local files in whitelist
123134
for (const url of urls) {
@@ -141,9 +152,11 @@ async function main() {
141152
console.error(`[pdf-server] Ready (${urls.length} URL(s) configured)`);
142153

143154
if (stdio) {
144-
await startStdioServer(createServer);
155+
// stdio → client is local (e.g. Claude Desktop), roots are safe
156+
await startStdioServer(() => createServer({ useClientRoots: true }));
145157
} else {
146-
await startStreamableHTTPServer(createServer);
158+
// HTTP → client is remote, only honour roots with explicit opt-in
159+
await startStreamableHTTPServer(() => createServer({ useClientRoots }));
147160
}
148161
}
149162

examples/pdf-server/server.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test";
22
import path from "node:path";
33
import {
44
createPdfCache,
5+
createServer,
56
validateUrl,
67
isAncestorDir,
78
allowedLocalFiles,
@@ -421,3 +422,27 @@ describe("isAncestorDir", () => {
421422
);
422423
});
423424
});
425+
426+
describe("createServer useClientRoots option", () => {
427+
it("should not set up roots handlers by default", () => {
428+
const server = createServer();
429+
// When useClientRoots is false (default), oninitialized should NOT
430+
// be overridden by our roots logic.
431+
expect(server.server.oninitialized).toBeUndefined();
432+
server.close();
433+
});
434+
435+
it("should not set up roots handlers when useClientRoots is false", () => {
436+
const server = createServer({ useClientRoots: false });
437+
expect(server.server.oninitialized).toBeUndefined();
438+
server.close();
439+
});
440+
441+
it("should set up roots handlers when useClientRoots is true", () => {
442+
const server = createServer({ useClientRoots: true });
443+
// When useClientRoots is true, oninitialized should be set to
444+
// the roots refresh handler.
445+
expect(server.server.oninitialized).toBeFunction();
446+
server.close();
447+
});
448+
});

examples/pdf-server/server.ts

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -443,19 +443,49 @@ async function refreshRoots(server: Server): Promise<void> {
443443
// MCP Server Factory
444444
// =============================================================================
445445

446-
export function createServer(): McpServer {
446+
export interface CreateServerOptions {
447+
/**
448+
* Whether to honour MCP roots sent by the client.
449+
*
450+
* When a server is exposed over HTTP, the connecting client is
451+
* typically remote and may advertise `roots` that refer to
452+
* directories on the **client's** file system. Because the server
453+
* resolves those paths locally, accepting them by default would give
454+
* the remote client access to arbitrary directories on the
455+
* **server's** machine.
456+
*
457+
* For stdio the client is typically local (e.g. Claude Desktop on the
458+
* same machine), so roots are safe and enabled by default.
459+
*
460+
* Set this to `true` for HTTP only when you trust the client, or
461+
* pass the `--use-client-roots` CLI flag.
462+
*
463+
* @default false
464+
*/
465+
useClientRoots?: boolean;
466+
}
467+
468+
export function createServer(options: CreateServerOptions = {}): McpServer {
469+
const { useClientRoots = false } = options;
447470
const server = new McpServer({ name: "PDF Server", version: "2.0.0" });
448471

449-
// Fetch roots on initialization and subscribe to changes
450-
server.server.oninitialized = () => {
451-
refreshRoots(server.server);
452-
};
453-
server.server.setNotificationHandler(
454-
RootsListChangedNotificationSchema,
455-
async () => {
456-
await refreshRoots(server.server);
457-
},
458-
);
472+
if (useClientRoots) {
473+
// Fetch roots on initialization and subscribe to changes
474+
server.server.oninitialized = () => {
475+
refreshRoots(server.server);
476+
};
477+
server.server.setNotificationHandler(
478+
RootsListChangedNotificationSchema,
479+
async () => {
480+
await refreshRoots(server.server);
481+
},
482+
);
483+
} else {
484+
console.error(
485+
"[pdf-server] Client roots are ignored (default for remote transports). " +
486+
"Pass --use-client-roots to allow the client to expose local directories.",
487+
);
488+
}
459489

460490
// Create session-local cache (isolated per server instance)
461491
const { readPdfRange } = createPdfCache();

0 commit comments

Comments
 (0)