Merge branch 'main' into update-pr-template-deprecate-readme

This commit is contained in:
Ola Hungerford
2025-11-24 20:42:06 -07:00
committed by GitHub
2 changed files with 57 additions and 14 deletions

View File

@@ -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 **readonly** tools from writecapable 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` | Recreating the same dir is a noop |
| `write_file` | `false` | `true` | `true` | Overwrites existing files |
| `edit_file` | `false` | `false` | `true` | Reapplying edits can fail or doubleapply |
| `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`:

View File

@@ -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')}`;