Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/filesystem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ on each tool so clients can:
- Distinguish **read‑only** tools from write‑capable tools.
- Understand which write operations are **idempotent** (safe to retry with the same arguments).
- Highlight operations that may be **destructive** (overwriting or heavily mutating data).
- Signal that a tool does **not** reach an open or external world (every filesystem tool sets `openWorldHint: false`).

The mapping for filesystem tools is:

Expand All @@ -204,7 +205,7 @@ The mapping for filesystem tools is:
| `edit_file` | `false` | `false` | `true` | Re‑applying edits can fail or double‑apply |
| `move_file` | `false` | `false` | `true` | Deletes source file |

> Note: `idempotentHint` and `destructiveHint` are meaningful only when `readOnlyHint` is `false`, as defined by the MCP spec.
> 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.

## Usage with Claude Desktop
Add this to your `claude_desktop_config.json`:
Expand Down
28 changes: 14 additions & 14 deletions src/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ server.registerTool(
description: "Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.",
inputSchema: ReadTextFileArgsSchema.shape,
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
annotations: { readOnlyHint: true, openWorldHint: false }
},
readTextFileHandler
);
Expand All @@ -240,7 +240,7 @@ server.registerTool(
head: z.number().optional().describe("If provided, returns only the first N lines of the file")
},
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
annotations: { readOnlyHint: true, openWorldHint: false }
},
readTextFileHandler
);
Expand All @@ -262,7 +262,7 @@ server.registerTool(
mimeType: z.string()
}))
},
annotations: { readOnlyHint: true }
annotations: { readOnlyHint: true, openWorldHint: false }
},
async (args: z.infer<typeof ReadMediaFileArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand Down Expand Up @@ -313,7 +313,7 @@ server.registerTool(
.describe("Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories.")
},
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
annotations: { readOnlyHint: true, openWorldHint: false }
},
async (args: z.infer<typeof ReadMultipleFilesArgsSchema>) => {
const results = await Promise.all(
Expand Down Expand Up @@ -349,7 +349,7 @@ server.registerTool(
content: z.string()
},
outputSchema: { content: z.string() },
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true }
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true, openWorldHint: false }
},
async (args: z.infer<typeof WriteFileArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand Down Expand Up @@ -379,7 +379,7 @@ server.registerTool(
dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format")
},
outputSchema: { content: z.string() },
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true }
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true, openWorldHint: false }
},
async (args: z.infer<typeof EditFileArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand All @@ -404,7 +404,7 @@ server.registerTool(
path: z.string()
},
outputSchema: { content: z.string() },
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false }
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: false }
},
async (args: z.infer<typeof CreateDirectoryArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand All @@ -430,7 +430,7 @@ server.registerTool(
path: z.string()
},
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
annotations: { readOnlyHint: true, openWorldHint: false }
},
async (args: z.infer<typeof ListDirectoryArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand Down Expand Up @@ -459,7 +459,7 @@ server.registerTool(
sortBy: z.enum(["name", "size"]).optional().default("name").describe("Sort entries by name or size")
},
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
annotations: { readOnlyHint: true, openWorldHint: false }
},
async (args: z.infer<typeof ListDirectoryWithSizesArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand Down Expand Up @@ -538,7 +538,7 @@ server.registerTool(
excludePatterns: z.array(z.string()).optional().default([])
},
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
annotations: { readOnlyHint: true, openWorldHint: false }
},
async (args: z.infer<typeof DirectoryTreeArgsSchema>) => {
interface TreeEntry {
Expand Down Expand Up @@ -608,7 +608,7 @@ server.registerTool(
destination: z.string()
},
outputSchema: { content: z.string() },
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true }
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true, openWorldHint: false }
},
async (args: z.infer<typeof MoveFileArgsSchema>) => {
const validSourcePath = await validatePath(args.source);
Expand Down Expand Up @@ -639,7 +639,7 @@ server.registerTool(
excludePatterns: z.array(z.string()).optional().default([])
},
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
annotations: { readOnlyHint: true, openWorldHint: false }
},
async (args: z.infer<typeof SearchFilesArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand All @@ -665,7 +665,7 @@ server.registerTool(
path: z.string()
},
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
annotations: { readOnlyHint: true, openWorldHint: false }
},
async (args: z.infer<typeof GetFileInfoArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand All @@ -691,7 +691,7 @@ server.registerTool(
"before trying to access files.",
inputSchema: {},
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
annotations: { readOnlyHint: true, openWorldHint: false }
},
async () => {
const text = `Allowed directories:\n${allowedDirectories.join('\n')}`;
Expand Down
Loading