diff --git a/src/filesystem/README.md b/src/filesystem/README.md index 973f76ef66..91d217af2b 100644 --- a/src/filesystem/README.md +++ b/src/filesystem/README.md @@ -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`: diff --git a/src/filesystem/index.ts b/src/filesystem/index.ts index 4f521aa58f..79a7b2fe4c 100644 --- a/src/filesystem/index.ts +++ b/src/filesystem/index.ts @@ -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) => { 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) => { 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) => { 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) => { 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) => { 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) => { 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) => { 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) => { 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) => { 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) => { 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) => { 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')}`;