mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-18 00:03:23 +02:00
# feat(filesystem): add ToolAnnotations hints to filesystem tools (#3045)
**Files touched** - [src/filesystem/index.ts](../blob/HEAD/src/filesystem/index.ts) — add `annotations` metadata to each tool definition - [src/filesystem/README.md](../blob/HEAD/src/filesystem/README.md) — document ToolAnnotations mapping for all filesystem tools ## Description This change adds MCP `ToolAnnotations` (`readOnlyHint`, `idempotentHint`, `destructiveHint`) to all filesystem tools and documents the mapping in the filesystem README. MCP clients can now accurately distinguish read‑only vs. write tools, understand which operations are safe to retry, and highlight potentially destructive actions. ## Server Details - **Server**: filesystem - **Area**: tools (metadata returned via `listTools` / `ListToolsRequest`) and server docs ## Motivation and Context Previously, the filesystem server did not expose ToolAnnotations, so many clients (e.g. ChatGPT Apps) conservatively treated filesystem tools as generic write operations. This led to: - READ operations being surfaced with WRITE badges and confirmation prompts. - No way for clients to know which write tools are idempotent or potentially destructive. This PR aligns the implementation with `servers#2988` and updates the README to clearly document the semantics of each tool. Read‑only operations no longer need to be treated as writes, and destructive/idempotent behavior is explicit for UI and retry logic. ## How Has This Been Tested? - `npm run build --workspace @modelcontextprotocol/server-filesystem` - `npm test --workspaces --if-present` ## Breaking Changes None. ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update ## Checklist - [x] I have read the [MCP Protocol Documentation](https://modelcontextprotocol.io) - [x] My changes follows MCP security best practices - [x] I have updated the server's README accordingly - [x] I have tested this with an LLM client - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [ ] I have documented all environment variables and configuration options ## Additional context None. Co-authored-by: Valeriy_Pavlovich <vp@strato.space>
This commit is contained in:
committed by
GitHub
parent
33e029f1d9
commit
84c2824f03
@@ -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`:
|
||||
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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(
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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 {
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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')}`;
|
||||
|
||||
Reference in New Issue
Block a user