Skip to content

Commit d31124c

Browse files
fix(filesystem): declare openWorldHint on all tools (#4480)
Every filesystem tool operates only on the local filesystem within its allowed directories — none reaches an open or external world — but none declared the MCP `openWorldHint` annotation, leaving open-world-hint coverage at 0/14. Set `openWorldHint: false` on all 14 tools and document it in the README's tool annotations section. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 97b70ee commit d31124c

2 files changed

Lines changed: 16 additions & 15 deletions

File tree

src/filesystem/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ on each tool so clients can:
187187
- Distinguish **read‑only** tools from write‑capable tools.
188188
- Understand which write operations are **idempotent** (safe to retry with the same arguments).
189189
- Highlight operations that may be **destructive** (overwriting or heavily mutating data).
190+
- Signal that a tool does **not** reach an open or external world (every filesystem tool sets `openWorldHint: false`).
190191

191192
The mapping for filesystem tools is:
192193

@@ -206,7 +207,7 @@ The mapping for filesystem tools is:
206207
| `edit_file` | `false` | `false` | `true` | Re‑applying edits can fail or double‑apply |
207208
| `move_file` | `false` | `false` | `true` | Deletes source file |
208209

209-
> Note: `idempotentHint` and `destructiveHint` are meaningful only when `readOnlyHint` is `false`, as defined by the MCP spec.
210+
> Note: `idempotentHint` and `destructiveHint` are meaningful only when `readOnlyHint` is `false`, as defined by the MCP spec. Every tool also sets `openWorldHint: false` — this server only accesses the local filesystem within its allowed directories, never an open or external world.
210211
211212
## Usage with Claude Desktop
212213
Add this to your `claude_desktop_config.json`:

src/filesystem/index.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ server.registerTool(
217217
description: "Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.",
218218
inputSchema: ReadTextFileArgsSchema.shape,
219219
outputSchema: { content: z.string() },
220-
annotations: { readOnlyHint: true }
220+
annotations: { readOnlyHint: true, openWorldHint: false }
221221
},
222222
readTextFileHandler
223223
);
@@ -240,7 +240,7 @@ server.registerTool(
240240
head: z.number().optional().describe("If provided, returns only the first N lines of the file")
241241
},
242242
outputSchema: { content: z.string() },
243-
annotations: { readOnlyHint: true }
243+
annotations: { readOnlyHint: true, openWorldHint: false }
244244
},
245245
readTextFileHandler
246246
);
@@ -274,7 +274,7 @@ server.registerTool(
274274
})
275275
]))
276276
},
277-
annotations: { readOnlyHint: true }
277+
annotations: { readOnlyHint: true, openWorldHint: false }
278278
},
279279
async (args: z.infer<typeof ReadMediaFileArgsSchema>) => {
280280
const validPath = await validatePath(args.path);
@@ -331,7 +331,7 @@ server.registerTool(
331331
.describe("Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories.")
332332
},
333333
outputSchema: { content: z.string() },
334-
annotations: { readOnlyHint: true }
334+
annotations: { readOnlyHint: true, openWorldHint: false }
335335
},
336336
async (args: z.infer<typeof ReadMultipleFilesArgsSchema>) => {
337337
const results = await Promise.all(
@@ -367,7 +367,7 @@ server.registerTool(
367367
content: z.string()
368368
},
369369
outputSchema: { content: z.string() },
370-
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true }
370+
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true, openWorldHint: false }
371371
},
372372
async (args: z.infer<typeof WriteFileArgsSchema>) => {
373373
const validPath = await validatePath(args.path);
@@ -397,7 +397,7 @@ server.registerTool(
397397
dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format")
398398
},
399399
outputSchema: { content: z.string() },
400-
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true }
400+
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true, openWorldHint: false }
401401
},
402402
async (args: z.infer<typeof EditFileArgsSchema>) => {
403403
const validPath = await validatePath(args.path);
@@ -422,7 +422,7 @@ server.registerTool(
422422
path: z.string()
423423
},
424424
outputSchema: { content: z.string() },
425-
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false }
425+
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: false }
426426
},
427427
async (args: z.infer<typeof CreateDirectoryArgsSchema>) => {
428428
const validPath = await validatePath(args.path);
@@ -448,7 +448,7 @@ server.registerTool(
448448
path: z.string()
449449
},
450450
outputSchema: { content: z.string() },
451-
annotations: { readOnlyHint: true }
451+
annotations: { readOnlyHint: true, openWorldHint: false }
452452
},
453453
async (args: z.infer<typeof ListDirectoryArgsSchema>) => {
454454
const validPath = await validatePath(args.path);
@@ -477,7 +477,7 @@ server.registerTool(
477477
sortBy: z.enum(["name", "size"]).optional().default("name").describe("Sort entries by name or size")
478478
},
479479
outputSchema: { content: z.string() },
480-
annotations: { readOnlyHint: true }
480+
annotations: { readOnlyHint: true, openWorldHint: false }
481481
},
482482
async (args: z.infer<typeof ListDirectoryWithSizesArgsSchema>) => {
483483
const validPath = await validatePath(args.path);
@@ -556,7 +556,7 @@ server.registerTool(
556556
excludePatterns: z.array(z.string()).optional().default([])
557557
},
558558
outputSchema: { content: z.string() },
559-
annotations: { readOnlyHint: true }
559+
annotations: { readOnlyHint: true, openWorldHint: false }
560560
},
561561
async (args: z.infer<typeof DirectoryTreeArgsSchema>) => {
562562
interface TreeEntry {
@@ -626,7 +626,7 @@ server.registerTool(
626626
destination: z.string()
627627
},
628628
outputSchema: { content: z.string() },
629-
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true }
629+
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true, openWorldHint: false }
630630
},
631631
async (args: z.infer<typeof MoveFileArgsSchema>) => {
632632
const validSourcePath = await validatePath(args.source);
@@ -657,7 +657,7 @@ server.registerTool(
657657
excludePatterns: z.array(z.string()).optional().default([])
658658
},
659659
outputSchema: { content: z.string() },
660-
annotations: { readOnlyHint: true }
660+
annotations: { readOnlyHint: true, openWorldHint: false }
661661
},
662662
async (args: z.infer<typeof SearchFilesArgsSchema>) => {
663663
const validPath = await validatePath(args.path);
@@ -683,7 +683,7 @@ server.registerTool(
683683
path: z.string()
684684
},
685685
outputSchema: { content: z.string() },
686-
annotations: { readOnlyHint: true }
686+
annotations: { readOnlyHint: true, openWorldHint: false }
687687
},
688688
async (args: z.infer<typeof GetFileInfoArgsSchema>) => {
689689
const validPath = await validatePath(args.path);
@@ -709,7 +709,7 @@ server.registerTool(
709709
"before trying to access files.",
710710
inputSchema: {},
711711
outputSchema: { content: z.string() },
712-
annotations: { readOnlyHint: true }
712+
annotations: { readOnlyHint: true, openWorldHint: false }
713713
},
714714
async () => {
715715
const text = `Allowed directories:\n${allowedDirectories.join('\n')}`;

0 commit comments

Comments
 (0)