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
29 changes: 29 additions & 0 deletions src/filesystem/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,35 @@ The server's directory access control follows this flow:
- Returns:
- Directories that this server can read/write from

### Tool annotations (MCP hints)

This server sets [MCP ToolAnnotations](https://modelcontextprotocol.io/specification/2025-03-26/server/tools#toolannotations)
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).

The mapping for filesystem tools is:

| Tool | readOnlyHint | idempotentHint | destructiveHint | Notes |
|-----------------------------|--------------|----------------|-----------------|--------------------------------------------------|
| `read_text_file` | `true` | – | – | Pure read |
| `read_media_file` | `true` | – | – | Pure read |
| `read_multiple_files` | `true` | – | – | Pure read |
| `list_directory` | `true` | – | – | Pure read |
| `list_directory_with_sizes` | `true` | – | – | Pure read |
| `directory_tree` | `true` | – | – | Pure read |
| `search_files` | `true` | – | – | Pure read |
| `get_file_info` | `true` | – | – | Pure read |
| `list_allowed_directories` | `true` | – | – | Pure read |
| `create_directory` | `false` | `true` | `false` | Re‑creating the same dir is a no‑op |
| `write_file` | `false` | `true` | `true` | Overwrites existing files |
| `edit_file` | `false` | `false` | `true` | Re‑applying edits can fail or double‑apply |
| `move_file` | `false` | `false` | `false` | Move/rename only; repeat usually errors |

> Note: `idempotentHint` and `destructiveHint` are meaningful only when `readOnlyHint` is `false`, as defined by the MCP spec.

## Usage with Claude Desktop
Add this to your `claude_desktop_config.json`:

Expand Down
42 changes: 28 additions & 14 deletions src/filesystem/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ server.registerTool(
title: "Read File (Deprecated)",
description: "Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.",
inputSchema: ReadTextFileArgsSchema.shape,
outputSchema: { content: z.string() }
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
},
readTextFileHandler
);
Expand All @@ -219,7 +220,8 @@ server.registerTool(
tail: z.number().optional().describe("If provided, returns only the last N lines of the file"),
head: z.number().optional().describe("If provided, returns only the first N lines of the file")
},
outputSchema: { content: z.string() }
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
},
readTextFileHandler
);
Expand All @@ -240,7 +242,8 @@ server.registerTool(
data: z.string(),
mimeType: z.string()
}))
}
},
annotations: { readOnlyHint: true }
},
async (args: z.infer<typeof ReadMediaFileArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand Down Expand Up @@ -290,7 +293,8 @@ server.registerTool(
.min(1)
.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() }
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
},
async (args: z.infer<typeof ReadMultipleFilesArgsSchema>) => {
const results = await Promise.all(
Expand Down Expand Up @@ -325,7 +329,8 @@ server.registerTool(
path: z.string(),
content: z.string()
},
outputSchema: { content: z.string() }
outputSchema: { content: z.string() },
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true }
},
async (args: z.infer<typeof WriteFileArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand Down Expand Up @@ -354,7 +359,8 @@ server.registerTool(
})),
dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format")
},
outputSchema: { content: z.string() }
outputSchema: { content: z.string() },
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true }
},
async (args: z.infer<typeof EditFileArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand All @@ -378,7 +384,8 @@ server.registerTool(
inputSchema: {
path: z.string()
},
outputSchema: { content: z.string() }
outputSchema: { content: z.string() },
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false }
},
async (args: z.infer<typeof CreateDirectoryArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand All @@ -403,7 +410,8 @@ server.registerTool(
inputSchema: {
path: z.string()
},
outputSchema: { content: z.string() }
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
},
async (args: z.infer<typeof ListDirectoryArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand Down Expand Up @@ -431,7 +439,8 @@ server.registerTool(
path: z.string(),
sortBy: z.enum(["name", "size"]).optional().default("name").describe("Sort entries by name or size")
},
outputSchema: { content: z.string() }
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
},
async (args: z.infer<typeof ListDirectoryWithSizesArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand Down Expand Up @@ -509,7 +518,8 @@ server.registerTool(
path: z.string(),
excludePatterns: z.array(z.string()).optional().default([])
},
outputSchema: { content: z.string() }
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
},
async (args: z.infer<typeof DirectoryTreeArgsSchema>) => {
interface TreeEntry {
Expand Down Expand Up @@ -578,7 +588,8 @@ server.registerTool(
source: z.string(),
destination: z.string()
},
outputSchema: { content: z.string() }
outputSchema: { content: z.string() },
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: false }
},
async (args: z.infer<typeof MoveFileArgsSchema>) => {
const validSourcePath = await validatePath(args.source);
Expand Down Expand Up @@ -608,7 +619,8 @@ server.registerTool(
pattern: z.string(),
excludePatterns: z.array(z.string()).optional().default([])
},
outputSchema: { content: z.string() }
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
},
async (args: z.infer<typeof SearchFilesArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand All @@ -633,7 +645,8 @@ server.registerTool(
inputSchema: {
path: z.string()
},
outputSchema: { content: z.string() }
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
},
async (args: z.infer<typeof GetFileInfoArgsSchema>) => {
const validPath = await validatePath(args.path);
Expand All @@ -658,7 +671,8 @@ server.registerTool(
"Use this to understand which directories and their nested paths are available " +
"before trying to access files.",
inputSchema: {},
outputSchema: { content: z.string() }
outputSchema: { content: z.string() },
annotations: { readOnlyHint: true }
},
async () => {
const text = `Allowed directories:\n${allowedDirectories.join('\n')}`;
Expand Down