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>
This commit is contained in:
parent
97b70ee252
commit
d31124c982
2 changed files with 16 additions and 15 deletions
|
|
@ -187,6 +187,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:
|
||||
|
||||
|
|
@ -206,7 +207,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`:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -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
|
||||
);
|
||||
|
|
@ -274,7 +274,7 @@ server.registerTool(
|
|||
})
|
||||
]))
|
||||
},
|
||||
annotations: { readOnlyHint: true }
|
||||
annotations: { readOnlyHint: true, openWorldHint: false }
|
||||
},
|
||||
async (args: z.infer<typeof ReadMediaFileArgsSchema>) => {
|
||||
const validPath = await validatePath(args.path);
|
||||
|
|
@ -331,7 +331,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(
|
||||
|
|
@ -367,7 +367,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);
|
||||
|
|
@ -397,7 +397,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);
|
||||
|
|
@ -422,7 +422,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);
|
||||
|
|
@ -448,7 +448,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);
|
||||
|
|
@ -477,7 +477,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);
|
||||
|
|
@ -556,7 +556,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 {
|
||||
|
|
@ -626,7 +626,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);
|
||||
|
|
@ -657,7 +657,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);
|
||||
|
|
@ -683,7 +683,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);
|
||||
|
|
@ -709,7 +709,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')}`;
|
||||
|
|
|
|||
Loading…
Reference in a new issue