{
  "captured_on": "2026-08-28T14:43:18.442Z",
  "repository": {
    "repo": {
      "full_name": "modelcontextprotocol/servers",
      "html_url": "https://github.com/modelcontextprotocol/servers",
      "default_branch": "main",
      "description": "Model Context Protocol Servers",
      "created_at": "2024-11-19T01:10:17Z",
      "updated_at": "2026-08-28T14:33:08Z",
      "pushed_at": "2026-08-28T02:36:45Z",
      "stargazers_count": 89931,
      "forks_count": 11516,
      "open_issues_count": 547,
      "archived": false,
      "visibility": "public",
      "license": {
        "key": "other",
        "name": "Other",
        "spdx_id": "NOASSERTION"
      }
    },
    "commit": {
      "sha": "562feeb2817266e0d155b9e4bff54a0233557f12",
      "html_url": "https://github.com/modelcontextprotocol/servers/commit/562feeb2817266e0d155b9e4bff54a0233557f12",
      "authored_at": "2026-08-28T02:36:45Z",
      "committed_at": "2026-08-28T02:36:45Z"
    }
  },
  "inspected_files": {
    "src/filesystem/README.md": {
      "sha256": null,
      "content": "# Filesystem MCP Server\n\nNode.js server implementing Model Context Protocol (MCP) for filesystem operations.\n\nPublished on npm as [`@modelcontextprotocol/server-filesystem`](https://www.npmjs.com/package/@modelcontextprotocol/server-filesystem).\n\n## Features\n\n- Read/write files\n- Create/list/delete directories\n- Move files/directories\n- Search files\n- Get file metadata\n- Dynamic directory access control via [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots)\n\n## Directory Access Control\n\nThe server uses a flexible directory access control system. Directories can be specified via command-line arguments or dynamically via [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots).\n\n### Method 1: Command-line Arguments\nSpecify Allowed directories when starting the server:\n```bash\nmcp-server-filesystem /path/to/dir1 /path/to/dir2\n```\n\n### Method 2: MCP Roots (Recommended)\nMCP clients that support [Roots](https://modelcontextprotocol.io/docs/learn/client-concepts#roots) can dynamically update the Allowed directories. \n\nRoots notified by Client to Server, completely replace any server-side Allowed directories when provided.\n\n**Important**: If server starts without command-line arguments AND client doesn't support roots protocol (or provides empty roots), the server will throw an error during initialization.\n\nThis is the recommended method, as this enables runtime directory updates via `roots/list_changed` notifications without server restart, providing a more flexible and modern integration experience.\n\n### How It Works\n\nThe server's directory access control follows this flow:\n\n1. **Server Startup**\n   - Server starts with directories from command-line arguments (if provided)\n   - If no arguments provided, server starts with empty allowed directories\n\n2. **Client Connection & Initialization**\n   - Client connects and sends `initialize` request with capabilities\n   - Server checks if client supports roots protocol (`capabilities.roots`)\n   \n3. **Roots Protocol Handling** (if client supports roots)\n   - **On initialization**: Server requests roots from client via `roots/list`\n   - Client responds with its configured roots\n   - Server replaces ALL allowed directories with client's roots\n   - **On runtime updates**: Client can send `notifications/roots/list_changed`\n   - Server requests updated roots and replaces allowed directories again\n\n4. **Fallback Behavior** (if client doesn't support roots)\n   - Server continues using command-line directories only\n   - No dynamic updates possible\n\n5. **Access Control**\n   - All filesystem operations are restricted to allowed directories\n   - Use `list_allowed_directories` tool to see current directories\n   - Server requires at least ONE allowed directory to operate\n\n**Note**: The server will only allow operations within directories specified either via `args` or via Roots.\n\n\n\n## API\n\n### Tools\n\n- **read_text_file**\n  - Read complete contents of a file as text\n  - Inputs:\n    - `path` (string)\n    - `head` (number, optional): First N lines\n    - `tail` (number, optional): Last N lines\n  - Always treats the file as UTF-8 text regardless of extension\n  - Cannot specify both `head` and `tail` simultaneously\n\n- **read_media_file**\n  - Read a file and return it as a base64-encoded content block with its MIME type\n  - Inputs:\n    - `path` (string)\n  - Streams the file and returns base64 data with the corresponding MIME type. Image and\n    audio files are returned as `image`/`audio` content; any other file type is returned as\n    an embedded `resource` (a valid MCP content block for arbitrary binary data)\n\n- **read_multiple_files**\n  - Read multiple files simultaneously\n  - Input: `paths` (string[])\n  - Failed reads won't stop the entire operation\n\n- **write_file**\n  - Create new file or overwrite existing (exercise caution with this)\n  - Inputs:\n    - `path` (string): File location\n    - `content` (string): File content\n\n- **edit_file**\n  - Make selective edits using advanced pattern matching and formatting\n  - Features:\n    - Line-based and multi-line content matching\n    - Whitespace normalization with indentation preservation\n    - Multiple simultaneous edits with correct positioning\n    - Indentation style detection and preservation\n    - Git-style diff output with context\n    - Preview changes with dry run mode\n  - Inputs:\n    - `path` (string): File to edit\n    - `edits` (array): List of edit operations\n      - `oldText` (string): Text to search for (can be substring)\n      - `newText` (string): Text to replace with\n    - `dryRun` (boolean): Preview changes without applying (default: false)\n  - Returns detailed diff and match information for dry runs, otherwise applies changes\n  - Best Practice: Always use dryRun first to preview changes before applying them\n\n- **create_directory**\n  - Create new directory or ensure it exists\n  - Input: `path` (string)\n  - Creates parent directories if needed\n  - Succeeds silently if directory exists\n\n- **list_directory**\n  - List directory contents with [FILE] or [DIR] prefixes\n  - Input: `path` (string)\n\n- **list_directory_with_sizes**\n  - List directory contents with [FILE] or [DIR] prefixes, including file sizes\n  - Inputs:\n    - `path` (string): Directory path to list\n    - `sortBy` (string, optional): Sort entries by \"name\" or \"size\" (default: \"name\")\n  - Returns detailed listing with file sizes and summary statistics\n  - Shows total files, directories, and combined size\n\n- **move_file**\n  - Move or rename files and directories\n  - Inputs:\n    - `source` (string)\n    - `destination` (string)\n  - Fails if destination exists\n\n- **search_files**\n  - Recursively search for files/directories that match or do not match patterns\n  - Inputs:\n    - `path` (string): Starting directory\n    - `pattern` (string): Search pattern\n    - `excludePatterns` (string[]): Exclude any patterns.\n  - Glob-style pattern matching\n  - Returns full paths to matches\n\n- **directory_tree**\n  - Get recursive JSON tree structure of directory contents\n  - Inputs:\n    - `path` (string): Starting directory\n    - `excludePatterns` (string[]): Exclude any patterns. Glob formats are supported.\n  - Returns:\n    - JSON array where each entry contains:\n      - `name` (string): File/directory name\n      - `type` ('file'|'directory'): Entry type\n      - `children` (array): Present only for directories\n        - Empty array for empty directories\n        - Omitted for files\n  - Output is formatted with 2-space indentation for readability\n    \n- **get_file_info**\n  - Get detailed file/directory metadata\n  - Input: `path` (string)\n  - Returns:\n    - Size\n    - Creation time\n    - Modified time\n    - Access time\n    - Type (file/directory)\n    - Permissions\n\n- **list_allowed_directories**\n  - List all directories the server is allowed to access\n  - No input required\n  - Returns:\n    - Directories that this server can read/write from\n\n### Tool annotations (MCP hints)\n\nThis server sets [MCP ToolAnnotations](https://modelcontextprotocol.io/specification/2025-03-26/server/tools#toolannotations)\non each tool so clients can:\n\n- Distinguish **read‑only** tools from write‑capable tools.\n- Understand which write operations are **idempotent** (safe to retry with the same arguments).\n- Highlight operations that may be **destructive** (overwriting or heavily mutating data).\n- Signal that a tool does **not** reach an open or external world (every filesystem tool sets `openWorldHint: false`).\n\nThe mapping for filesystem tools is:\n\n| Tool                        | readOnlyHint | idempotentHint | destructiveHint | Notes                                            |\n|-----------------------------|--------------|----------------|-----------------|--------------------------------------------------|\n| `read_text_file`            | `true`       | –              | –               | Pure read                                       |\n| `read_media_file`           | `true`       | –              | –               | Pure read                                       |\n| `read_multiple_files`       | `true`       | –              | –               | Pure read                                       |\n| `list_directory`            | `true`       | –              | –               | Pure read                                       |\n| `list_directory_with_sizes` | `true`       | –              | –               | Pure read                                       |\n| `directory_tree`            | `true`       | –              | –               | Pure read                                       |\n| `search_files`              | `true`       | –              | –               | Pure read                                       |\n| `get_file_info`             | `true`       | –              | –               | Pure read                                       |\n| `list_allowed_directories`  | `true`       | –              | –               | Pure read                                       |\n| `create_directory`          | `false`      | `true`         | `false`         | Re‑creating the same dir is a no‑op             |\n| `write_file`                | `false`      | `true`         | `true`          | Overwrites existing files                       |\n| `edit_file`                 | `false`      | `false`        | `true`          | Re‑applying edits can fail or double‑apply      |\n| `move_file`                 | `false`      | `false`        | `true`          | Deletes source file                             |\n\n> 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.\n\n## Usage with Claude Desktop\nAdd this to your `claude_desktop_config.json`:\n\nNote: you can provide sandboxed directories to the server by mounting them to `/projects`. Adding the `ro` flag will make the directory readonly by the server.\n\n### Docker\nNote: all directories must be mounted to `/projects` by default.\n\n```json\n{\n  \"mcpServers\": {\n    \"filesystem\": {\n      \"command\": \"docker\",\n      \"args\": [\n        \"run\",\n        \"-i\",\n        \"--rm\",\n        \"--mount\", \"type=bind,src=/Users/username/Desktop,dst=/projects/Desktop\",\n        \"--mount\", \"type=bind,src=/path/to/other/allowed/dir,dst=/projects/other/allowed/dir,ro\",\n        \"--mount\", \"type=bind,src=/path/to/file.txt,dst=/projects/path/to/file.txt\",\n        \"mcp/filesystem\",\n        \"/projects\"\n      ]\n    }\n  }\n}\n```\n\n### NPX\n\n```json\n{\n  \"mcpServers\": {\n    \"filesystem\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\",\n        \"@modelcontextprotocol/server-filesystem\",\n        \"/Users/username/Desktop\",\n        \"/path/to/other/allowed/dir\"\n      ]\n    }\n  }\n}\n```\n\nOn Windows, use `cmd /c` to launch `npx`:\n\n```json\n{\n  \"mcpServers\": {\n    \"filesystem\": {\n      \"command\": \"cmd\",\n      \"args\": [\n        \"/c\",\n        \"npx\",\n        \"-y\",\n        \"@modelcontextprotocol/server-filesystem\",\n        \"/Users/username/Desktop\",\n        \"/path/to/other/allowed/dir\"\n      ]\n    }\n  }\n}\n```\n\n## Usage with VS Code\n\nFor quick installation, click the installation buttons below...\n\n[![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-NPM-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-filesystem%22%2C%22%24%7BworkspaceFolder%7D%22%5D%7D) [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-NPM-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40modelcontextprotocol%2Fserver-filesystem%22%2C%22%24%7BworkspaceFolder%7D%22%5D%7D&quality=insiders)\n\n[![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Docker-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22--mount%22%2C%22type%3Dbind%2Csrc%3D%24%7BworkspaceFolder%7D%2Cdst%3D%2Fprojects%2Fworkspace%22%2C%22mcp%2Ffilesystem%22%2C%22%2Fprojects%22%5D%7D) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Docker-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=filesystem&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22--mount%22%2C%22type%3Dbind%2Csrc%3D%24%7BworkspaceFolder%7D%2Cdst%3D%2Fprojects%2Fworkspace%22%2C%22mcp%2Ffilesystem%22%2C%22%2Fprojects%22%5D%7D&quality=insiders)\n\nFor manual installation, you can configure the MCP server using one of these methods:\n\n**Method 1: User Configuration (Recommended)**\nAdd the configuration to your user-level MCP configuration file. Open the Command Palette (`Ctrl + Shift + P`) and run `MCP: Open User Configuration`. This will open your user `mcp.json` file where you can add the server configuration.\n\n**Method 2: Workspace Configuration**\nAlternatively, you can add the configuration to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.\n\n> For more details about MCP configuration in VS Code, see the [official VS Code MCP documentation](https://code.visualstudio.com/docs/copilot/customization/mcp-servers).\n\nYou can provide sandboxed directories to the server by mounting them to `/projects`. Adding the `ro` flag will make the directory readonly by the server.\n\n### Docker\nNote: all directories must be mounted to `/projects` by default. \n\n```json\n{\n  \"servers\": {\n    \"filesystem\": {\n      \"command\": \"docker\",\n      \"args\": [\n        \"run\",\n        \"-i\",\n        \"--rm\",\n        \"--mount\", \"type=bind,src=${workspaceFolder},dst=/projects/workspace\",\n        \"mcp/filesystem\",\n        \"/projects\"\n      ]\n    }\n  }\n}\n```\n\n### NPX\n\n```json\n{\n  \"servers\": {\n    \"filesystem\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\",\n        \"@modelcontextprotocol/server-filesystem\",\n        \"${workspaceFolder}\"\n      ]\n    }\n  }\n}\n```\n\nOn Windows, use:\n\n```json\n{\n  \"servers\": {\n    \"filesystem\": {\n      \"command\": \"cmd\",\n      \"args\": [\n        \"/c\",\n        \"npx\",\n        \"-y\",\n        \"@modelcontextprotocol/server-filesystem\",\n        \"${workspaceFolder}\"\n      ]\n    }\n  }\n}\n```\n\n## Build\n\nDocker build:\n\n```bash\ndocker build -t mcp/filesystem -f src/filesystem/Dockerfile .\n```\n\n## License\n\nThis MCP server is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.\n"
    },
    "src/filesystem/index.ts": {
      "sha256": null,
      "content": "#!/usr/bin/env node\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport {\n  RootsListChangedNotificationSchema,\n  type Root,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport fs from \"fs/promises\";\nimport { createReadStream } from \"fs\";\nimport path from \"path\";\nimport { pathToFileURL } from \"url\";\nimport { z } from \"zod\";\nimport { minimatch } from \"minimatch\";\nimport { normalizePath, expandHome } from './path-utils.js';\nimport { getValidRootDirectories } from './roots-utils.js';\nimport {\n  // Function imports\n  formatSize,\n  validatePath,\n  getFileStats,\n  readFileContent,\n  writeFileContent,\n  searchFilesWithValidation,\n  applyFileEdits,\n  tailFile,\n  headFile,\n  setAllowedDirectories,\n} from './lib.js';\n\n// Command line argument parsing\nconst args = process.argv.slice(2);\nif (args.length === 0) {\n  console.error(\"Usage: mcp-server-filesystem [allowed-directory] [additional-directories...]\");\n  console.error(\"Note: Allowed directories can be provided via:\");\n  console.error(\"  1. Command-line arguments (shown above)\");\n  console.error(\"  2. MCP roots protocol (if client supports it)\");\n  console.error(\"At least one directory must be provided by EITHER method for the server to operate.\");\n}\n\n// Store allowed directories in normalized and resolved form\n// We store BOTH the original path AND the resolved path to handle symlinks correctly\n// This fixes the macOS /tmp -> /private/tmp symlink issue where users specify /tmp\n// but the resolved path is /private/tmp\nlet allowedDirectories = (await Promise.all(\n  args.map(async (dir) => {\n    const expanded = expandHome(dir);\n    const absolute = path.resolve(expanded);\n    const normalizedOriginal = normalizePath(absolute);\n    try {\n      // Security: Resolve symlinks in allowed directories during startup\n      // This ensures we know the real paths and can validate against them later\n      const resolved = await fs.realpath(absolute);\n      const normalizedResolved = normalizePath(resolved);\n      // Return both original and resolved paths if they differ\n      // This allows matching against either /tmp or /private/tmp on macOS\n      if (normalizedOriginal !== normalizedResolved) {\n        return [normalizedOriginal, normalizedResolved];\n      }\n      return [normalizedResolved];\n    } catch (error) {\n      // If we can't resolve (doesn't exist), use the normalized absolute path\n      // This allows configuring allowed dirs that will be created later\n      return [normalizedOriginal];\n    }\n  })\n)).flat();\n\n// Filter to only accessible directories, warn about inaccessible ones\nconst accessibleDirectories: string[] = [];\nfor (const dir of allowedDirectories) {\n  try {\n    const stats = await fs.stat(dir);\n    if (stats.isDirectory()) {\n      accessibleDirectories.push(dir);\n    } else {\n      console.error(`Warning: ${dir} is not a directory, skipping`);\n    }\n  } catch (error) {\n    console.error(`Warning: Cannot access directory ${dir}, skipping`);\n  }\n}\n\n// Exit only if ALL paths are inaccessible (and some were specified)\nif (accessibleDirectories.length === 0 && allowedDirectories.length > 0) {\n  console.error(\"Error: None of the specified directories are accessible\");\n  process.exit(1);\n}\n\nallowedDirectories = accessibleDirectories;\n\n// Initialize the global allowedDirectories in lib.ts\nsetAllowedDirectories(allowedDirectories);\n\n// Schema definitions\nconst ReadTextFileArgsSchema = z.object({\n  path: z.string(),\n  tail: z.number().optional().describe('If provided, returns only the last N lines of the file'),\n  head: z.number().optional().describe('If provided, returns only the first N lines of the file')\n});\n\nconst ReadMediaFileArgsSchema = z.object({\n  path: z.string()\n});\n\nconst ReadMultipleFilesArgsSchema = z.object({\n  paths: z\n    .array(z.string())\n    .min(1, \"At least one file path must be provided\")\n    .describe(\"Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories.\"),\n});\n\nconst WriteFileArgsSchema = z.object({\n  path: z.string(),\n  content: z.string(),\n});\n\nconst EditOperation = z.object({\n  oldText: z.string().describe('Text to search for - must match exactly'),\n  newText: z.string().describe('Text to replace with')\n});\n\nconst EditFileArgsSchema = z.object({\n  path: z.string(),\n  edits: z.array(EditOperation),\n  dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format')\n});\n\nconst CreateDirectoryArgsSchema = z.object({\n  path: z.string(),\n});\n\nconst ListDirectoryArgsSchema = z.object({\n  path: z.string(),\n});\n\nconst ListDirectoryWithSizesArgsSchema = z.object({\n  path: z.string(),\n  sortBy: z.enum(['name', 'size']).optional().default('name').describe('Sort entries by name or size'),\n});\n\nconst DirectoryTreeArgsSchema = z.object({\n  path: z.string(),\n  excludePatterns: z.array(z.string()).optional().default([])\n});\n\nconst MoveFileArgsSchema = z.object({\n  source: z.string(),\n  destination: z.string(),\n});\n\nconst SearchFilesArgsSchema = z.object({\n  path: z.string(),\n  pattern: z.string(),\n  excludePatterns: z.array(z.string()).optional().default([])\n});\n\nconst GetFileInfoArgsSchema = z.object({\n  path: z.string(),\n});\n\n// Server setup\nconst server = new McpServer(\n  {\n    name: \"secure-filesystem-server\",\n    version: \"0.2.0\",\n  }\n);\n\n// Reads a file as a stream of buffers, concatenates them, and then encodes\n// the result to a Base64 string. This is a memory-efficient way to handle\n// binary data from a stream before the final encoding.\nasync function readFileAsBase64Stream(filePath: string): Promise<string> {\n  return new Promise((resolve, reject) => {\n    const stream = createReadStream(filePath);\n    const chunks: Buffer[] = [];\n    stream.on('data', (chunk) => {\n      chunks.push(chunk as Buffer);\n    });\n    stream.on('end', () => {\n      const finalBuffer = Buffer.concat(chunks);\n      resolve(finalBuffer.toString('base64'));\n    });\n    stream.on('error', (err) => reject(err));\n  });\n}\n\n// Tool registrations\n\n// read_file (deprecated) and read_text_file\nconst readTextFileHandler = async (args: z.infer<typeof ReadTextFileArgsSchema>) => {\n  const validPath = await validatePath(args.path);\n\n  if (args.head && args.tail) {\n    throw new Error(\"Cannot specify both head and tail parameters simultaneously\");\n  }\n\n  let content: string;\n  if (args.tail) {\n    content = await tailFile(validPath, args.tail);\n  } else if (args.head) {\n    content = await headFile(validPath, args.head);\n  } else {\n    content = await readFileContent(validPath);\n  }\n\n  return {\n    content: [{ type: \"text\" as const, text: content }],\n    structuredContent: { content }\n  };\n};\n\nserver.registerTool(\n  \"read_file\",\n  {\n    title: \"Read File (Deprecated)\",\n    description: \"Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.\",\n    inputSchema: ReadTextFileArgsSchema.shape,\n    outputSchema: { content: z.string() },\n    annotations: { readOnlyHint: true, openWorldHint: false }\n  },\n  readTextFileHandler\n);\n\nserver.registerTool(\n  \"read_text_file\",\n  {\n    title: \"Read Text File\",\n    description:\n      \"Read the complete contents of a file from the file system as text. \" +\n      \"Handles various text encodings and provides detailed error messages \" +\n      \"if the file cannot be read. Use this tool when you need to examine \" +\n      \"the contents of a single file. Use the 'head' parameter to read only \" +\n      \"the first N lines of a file, or the 'tail' parameter to read only \" +\n      \"the last N lines of a file. Operates on the file as text regardless of extension. \" +\n      \"Only works within allowed directories.\",\n    inputSchema: {\n      path: z.string(),\n      tail: z.number().optional().describe(\"If provided, returns only the last N lines of the file\"),\n      head: z.number().optional().describe(\"If provided, returns only the first N lines of the file\")\n    },\n    outputSchema: { content: z.string() },\n    annotations: { readOnlyHint: true, openWorldHint: false }\n  },\n  readTextFileHandler\n);\n\nserver.registerTool(\n  \"read_media_file\",\n  {\n    title: \"Read Media File\",\n    description:\n      \"Read a file and return it as a base64-encoded content block with its MIME type. \" +\n      \"Image and audio files are returned as image/audio content; any other file type is \" +\n      \"returned as an embedded resource. Only works within allowed directories.\",\n    inputSchema: {\n      path: z.string()\n    },\n    outputSchema: {\n      content: z.array(z.union([\n        z.object({\n          type: z.enum([\"image\", \"audio\"]),\n          data: z.string(),\n          mimeType: z.string()\n        }),\n        z.object({\n          type: z.literal(\"resource\"),\n          resource: z.object({\n            uri: z.string(),\n            // Optional, matching the SDK's BlobResourceContents shape (the handler always sets it).\n            mimeType: z.string().optional(),\n            blob: z.string()\n          })\n        })\n      ]))\n    },\n    annotations: { readOnlyHint: true, openWorldHint: false }\n  },\n  async (args: z.infer<typeof ReadMediaFileArgsSchema>) => {\n    const validPath = await validatePath(args.path);\n    const extension = path.extname(validPath).toLowerCase();\n    const mimeTypes: Record<string, string> = {\n      \".png\": \"image/png\",\n      \".jpg\": \"image/jpeg\",\n      \".jpeg\": \"image/jpeg\",\n      \".gif\": \"image/gif\",\n      \".webp\": \"image/webp\",\n      \".bmp\": \"image/bmp\",\n      \".svg\": \"image/svg+xml\",\n      \".mp3\": \"audio/mpeg\",\n      \".wav\": \"audio/wav\",\n      \".ogg\": \"audio/ogg\",\n      \".flac\": \"audio/flac\",\n    };\n    const mimeType = mimeTypes[extension] || \"application/octet-stream\";\n    const data = await readFileAsBase64Stream(validPath);\n\n    // Map the MIME type to a valid MCP content block. The spec only allows\n    // text, image, audio, resource_link, and resource — so non-image/audio\n    // binaries are returned as an embedded resource (NOT type:\"blob\", which the\n    // SDK content-block union rejects on schema validation).\n    const contentItem =\n      mimeType.startsWith(\"image/\")\n        ? { type: \"image\" as const, data, mimeType }\n        : mimeType.startsWith(\"audio/\")\n          ? { type: \"audio\" as const, data, mimeType }\n          : {\n              type: \"resource\" as const,\n              resource: { uri: pathToFileURL(validPath).href, mimeType, blob: data }\n            };\n    return {\n      content: [contentItem],\n      structuredContent: { content: [contentItem] }\n    };\n  }\n);\n\nserver.registerTool(\n  \"read_multiple_files\",\n  {\n    title: \"Read Multiple Files\",\n    description:\n      \"Read the contents of multiple files simultaneously. This is more \" +\n      \"efficient than reading files one by one when you need to analyze \" +\n      \"or compare multiple files. Each file's content is returned with its \" +\n      \"path as a reference. Failed reads for individual files won't stop \" +\n      \"the entire operation. Only works within allowed directories.\",\n    inputSchema: {\n      paths: z.array(z.string())\n        .min(1)\n        .describe(\"Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories.\")\n    },\n    outputSchema: { content: z.string() },\n    annotations: { readOnlyHint: true, openWorldHint: false }\n  },\n  async (args: z.infer<typeof ReadMultipleFilesArgsSchema>) => {\n    const results = await Promise.all(\n      args.paths.map(async (filePath: string) => {\n        try {\n          const validPath = await validatePath(filePath);\n          const content = await readFileContent(validPath);\n          return `${filePath}:\\n${content}\\n`;\n        } catch (error) {\n          const errorMessage = error instanceof Error ? error.message : String(error);\n          return `${filePath}: Error - ${errorMessage}`;\n        }\n      }),\n    );\n    const text = results.join(\"\\n---\\n\");\n    return {\n      content: [{ type: \"text\" as const, text }],\n      structuredContent: { content: text }\n    };\n  }\n);\n\nserver.registerTool(\n  \"write_file\",\n  {\n    title: \"Write File\",\n    description:\n      \"Create a new file or completely overwrite an existing file with new content. \" +\n      \"Use with caution as it will overwrite existing files without warning. \" +\n      \"Handles text content with proper encoding. Only works within allowed directories.\",\n    inputSchema: {\n      path: z.string(),\n      content: z.string()\n    },\n    outputSchema: { content: z.string() },\n    annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true, openWorldHint: false }\n  },\n  async (args: z.infer<typeof WriteFileArgsSchema>) => {\n    const validPath = await validatePath(args.path);\n    await writeFileContent(validPath, args.content);\n    const text = `Successfully wrote to ${args.path}`;\n    return {\n      content: [{ type: \"text\" as const, text }],\n      structuredContent: { content: text }\n    };\n  }\n);\n\nserver.registerTool(\n  \"edit_file\",\n  {\n    title: \"Edit File\",\n    description:\n      \"Make line-based edits to a text file. Each edit replaces exact line sequences \" +\n      \"with new content. Returns a git-style diff showing the changes made. \" +\n      \"Only works within allowed directories.\",\n    inputSchema: {\n      path: z.string(),\n      edits: z.array(z.object({\n        oldText: z.string().describe(\"Text to search for - must match exactly\"),\n        newText: z.string().describe(\"Text to replace with\")\n      })),\n      dryRun: z.boolean().default(false).describe(\"Preview changes using git-style diff format\")\n    },\n    outputSchema: { content: z.string() },\n    annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true, openWorldHint: false }\n  },\n  async (args: z.infer<typeof EditFileArgsSchema>) => {\n    const validPath = await validatePath(args.path);\n    const result = await applyFileEdits(validPath, args.edits, args.dryRun);\n    return {\n      content: [{ type: \"text\" as const, text: result }],\n      structuredContent: { content: result }\n    };\n  }\n);\n\nserver.registerTool(\n  \"create_directory\",\n  {\n    title: \"Create Directory\",\n    description:\n      \"Create a new directory or ensure a directory exists. Can create multiple \" +\n      \"nested directories in one operation. If the directory already exists, \" +\n      \"this operation will succeed silently. Perfect for setting up directory \" +\n      \"structures for projects or ensuring required paths exist. Only works within allowed directories.\",\n    inputSchema: {\n      path: z.string()\n    },\n    outputSchema: { content: z.string() },\n    annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: false }\n  },\n  async (args: z.infer<typeof CreateDirectoryArgsSchema>) => {\n    const validPath = await validatePath(args.path);\n    await fs.mkdir(validPath, { recursive: true });\n    const text = `Successfully created directory ${args.path}`;\n    return {\n      content: [{ type: \"text\" as const, text }],\n      structuredContent: { content: text }\n    };\n  }\n);\n\nserver.registerTool(\n  \"list_directory\",\n  {\n    title: \"List Directory\",\n    description:\n      \"Get a detailed listing of all files and directories in a specified path. \" +\n      \"Results clearly distinguish between files and directories with [FILE] and [DIR] \" +\n      \"prefixes. This tool is essential for understanding directory structure and \" +\n      \"finding specific files within a directory. Only works within allowed directories.\",\n    inputSchema: {\n      path: z.string()\n    },\n    outputSchema: { content: z.string() },\n    annotations: { readOnlyHint: true, openWorldHint: false }\n  },\n  async (args: z.infer<typeof ListDirectoryArgsSchema>) => {\n    const validPath = await validatePath(args.path);\n    const entries = await fs.readdir(validPath, { withFileTypes: true });\n    const formatted = entries\n      .map((entry) => `${entry.isDirectory() ? \"[DIR]\" : \"[FILE]\"} ${entry.name}`)\n      .join(\"\\n\");\n    return {\n      content: [{ type: \"text\" as const, text: formatted }],\n      structuredContent: { content: formatted }\n    };\n  }\n);\n\nserver.registerTool(\n  \"list_directory_with_sizes\",\n  {\n    title: \"List Directory with Sizes\",\n    description:\n      \"Get a detailed listing of all files and directories in a specified path, including sizes. \" +\n      \"Results clearly distinguish between files and directories with [FILE] and [DIR] \" +\n      \"prefixes. This tool is useful for understanding directory structure and \" +\n      \"finding specific files within a directory. Only works within allowed directories.\",\n    inputSchema: {\n      path: z.string(),\n      sortBy: z.enum([\"name\", \"size\"]).optional().default(\"name\").describe(\"Sort entries by name or size\")\n    },\n    outputSchema: { content: z.string() },\n    annotations: { readOnlyHint: true, openWorldHint: false }\n  },\n  async (args: z.infer<typeof ListDirectoryWithSizesArgsSchema>) => {\n    const validPath = await validatePath(args.path);\n    const entries = await fs.readdir(validPath, { withFileTypes: true });\n\n    // Get detailed information for each entry\n    const detailedEntries = await Promise.all(\n      entries.map(async (entry) => {\n        const entryPath = path.join(validPath, entry.name);\n        try {\n          const stats = await fs.stat(entryPath);\n          return {\n            name: entry.name,\n            isDirectory: entry.isDirectory(),\n            size: stats.size,\n            mtime: stats.mtime\n          };\n        } catch (error) {\n          return {\n            name: entry.name,\n            isDirectory: entry.isDirectory(),\n            size: 0,\n            mtime: new Date(0)\n          };\n        }\n      })\n    );\n\n    // Sort entries based on sortBy parameter\n    const sortedEntries = [...detailedEntries].sort((a, b) => {\n      if (args.sortBy === 'size') {\n        return b.size - a.size; // Descending by size\n      }\n      // Default sort by name\n      return a.name.localeCompare(b.name);\n    });\n\n    // Format the output\n    const formattedEntries = sortedEntries.map(entry =>\n      `${entry.isDirectory ? \"[DIR]\" : \"[FILE]\"} ${entry.name.padEnd(30)} ${\n        entry.isDirectory ? \"\" : formatSize(entry.size).padStart(10)\n      }`\n    );\n\n    // Add summary\n    const totalFiles = detailedEntries.filter(e => !e.isDirectory).length;\n    const totalDirs = detailedEntries.filter(e => e.isDirectory).length;\n    const totalSize = detailedEntries.reduce((sum, entry) => sum + (entry.isDirectory ? 0 : entry.size), 0);\n\n    const summary = [\n      \"\",\n      `Total: ${totalFiles} files, ${totalDirs} directories`,\n      `Combined size: ${formatSize(totalSize)}`\n    ];\n\n    const text = [...formattedEntries, ...summary].join(\"\\n\");\n    const contentBlock = { type: \"text\" as const, text };\n    return {\n      content: [contentBlock],\n      structuredContent: { content: text }\n    };\n  }\n);\n\nserver.registerTool(\n  \"directory_tree\",\n  {\n    title: \"Directory Tree\",\n    description:\n      \"Get a recursive tree view of files and directories as a JSON structure. \" +\n      \"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. \" +\n      \"Files have no children array, while directories always have a children array (which may be empty). \" +\n      \"The output is formatted with 2-space indentation for readability. Only works within allowed directories.\",\n    inputSchema: {\n      path: z.string(),\n      excludePatterns: z.array(z.string()).optional().default([])\n    },\n    outputSchema: { content: z.string() },\n    annotations: { readOnlyHint: true, openWorldHint: false }\n  },\n  async (args: z.infer<typeof DirectoryTreeArgsSchema>) => {\n    interface TreeEntry {\n      name: string;\n      type: 'file' | 'directory';\n      children?: TreeEntry[];\n    }\n    const rootPath = args.path;\n\n    async function buildTree(currentPath: string, excludePatterns: string[] = []): Promise<TreeEntry[]> {\n      const validPath = await validatePath(currentPath);\n      const entries = await fs.readdir(validPath, { withFileTypes: true });\n      const result: TreeEntry[] = [];\n\n      for (const entry of entries) {\n        const relativePath = path.relative(rootPath, path.join(currentPath, entry.name));\n        const shouldExclude = excludePatterns.some(pattern => {\n          if (pattern.includes('*')) {\n            return minimatch(relativePath, pattern, { dot: true });\n          }\n          // For files: match exact name or as part of path\n          // For directories: match as directory path\n          return minimatch(relativePath, pattern, { dot: true }) ||\n            minimatch(relativePath, `**/${pattern}`, { dot: true }) ||\n            minimatch(relativePath, `**/${pattern}/**`, { dot: true });\n        });\n        if (shouldExclude)\n          continue;\n\n        const entryData: TreeEntry = {\n          name: entry.name,\n          type: entry.isDirectory() ? 'directory' : 'file'\n        };\n\n        if (entry.isDirectory()) {\n          const subPath = path.join(currentPath, entry.name);\n          entryData.children = await buildTree(subPath, excludePatterns);\n        }\n\n        result.push(entryData);\n      }\n\n      return result;\n    }\n\n    const treeData = await buildTree(rootPath, args.excludePatterns);\n    const text = JSON.stringify(treeData, null, 2);\n    const contentBlock = { type: \"text\" as const, text };\n    return {\n      content: [contentBlock],\n      structuredContent: { content: text }\n    };\n  }\n);\n\nserver.registerTool(\n  \"move_file\",\n  {\n    title: \"Move File\",\n    description:\n      \"Move or rename files and directories. Can move files between directories \" +\n      \"and rename them in a single operation. If the destination exists, the \" +\n      \"operation will fail. Works across different directories and can be used \" +\n      \"for simple renaming within the same directory. Both source and destination must be within allowed directories.\",\n    inputSchema: {\n      source: z.string(),\n      destination: z.string()\n    },\n    outputSchema: { content: z.string() },\n    annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true, openWorldHint: false }\n  },\n  async (args: z.infer<typeof MoveFileArgsSchema>) => {\n    const validSourcePath = await validatePath(args.source);\n    const validDestPath = await validatePath(args.destination);\n    await fs.rename(validSourcePath, validDestPath);\n    const text = `Successfully moved ${args.source} to ${args.destination}`;\n    const contentBlock = { type: \"text\" as const, text };\n    return {\n      content: [contentBlock],\n      structuredContent: { content: text }\n    };\n  }\n);\n\nserver.registerTool(\n  \"search_files\",\n  {\n    title: \"Search Files\",\n    description:\n      \"Recursively search for files and directories matching a pattern. \" +\n      \"The patterns should be glob-style patterns that match paths relative to the working directory. \" +\n      \"Use pattern like '*.ext' to match files in current directory, and '**/*.ext' to match files in all subdirectories. \" +\n      \"Returns full paths to all matching items. Great for finding files when you don't know their exact location. \" +\n      \"Only searches within allowed directories.\",\n    inputSchema: {\n      path: z.string(),\n      pattern: z.string(),\n      excludePatterns: z.array(z.string()).optional().default([])\n    },\n    outputSchema: { content: z.string() },\n    annotations: { readOnlyHint: true, openWorldHint: false }\n  },\n  async (args: z.infer<typeof SearchFilesArgsSchema>) => {\n    const validPath = await validatePath(args.path);\n    const results = await searchFilesWithValidation(validPath, args.pattern, allowedDirectories, { excludePatterns: args.excludePatterns });\n    const text = results.length > 0 ? results.join(\"\\n\") : \"No matches found\";\n    return {\n      content: [{ type: \"text\" as const, text }],\n      structuredContent: { content: text }\n    };\n  }\n);\n\nserver.registerTool(\n  \"get_file_info\",\n  {\n    title: \"Get File Info\",\n    description:\n      \"Retrieve detailed metadata about a file or directory. Returns comprehensive \" +\n      \"information including size, creation time, last modified time, permissions, \" +\n      \"and type. This tool is perfect for understanding file characteristics \" +\n      \"without reading the actual content. Only works within allowed directories.\",\n    inputSchema: {\n      path: z.string()\n    },\n    outputSchema: { content: z.string() },\n    annotations: { readOnlyHint: true, openWorldHint: false }\n  },\n  async (args: z.infer<typeof GetFileInfoArgsSchema>) => {\n    const validPath = await validatePath(args.path);\n    const info = await getFileStats(validPath);\n    const text = Object.entries(info)\n      .map(([key, value]) => `${key}: ${value}`)\n      .join(\"\\n\");\n    return {\n      content: [{ type: \"text\" as const, text }],\n      structuredContent: { content: text }\n    };\n  }\n);\n\nserver.registerTool(\n  \"list_allowed_directories\",\n  {\n    title: \"List Allowed Directories\",\n    description:\n      \"Returns the list of directories that this server is allowed to access. \" +\n      \"Subdirectories within these allowed directories are also accessible. \" +\n      \"Use this to understand which directories and their nested paths are available \" +\n      \"before trying to access files.\",\n    inputSchema: {},\n    outputSchema: { content: z.string() },\n    annotations: { readOnlyHint: true, openWorldHint: false }\n  },\n  async () => {\n    const text = `Allowed directories:\\n${allowedDirectories.join('\\n')}`;\n    return {\n      content: [{ type: \"text\" as const, text }],\n      structuredContent: { content: text }\n    };\n  }\n);\n\n// Updates allowed directories based on MCP client roots\nasync function updateAllowedDirectoriesFromRoots(requestedRoots: Root[]) {\n  const validatedRootDirs = await getValidRootDirectories(requestedRoots);\n  if (validatedRootDirs.length > 0) {\n    allowedDirectories = [...validatedRootDirs];\n    setAllowedDirectories(allowedDirectories); // Update the global state in lib.ts\n    console.error(`Updated allowed directories from MCP roots: ${validatedRootDirs.length} valid directories`);\n  } else {\n    console.error(\"No valid root directories provided by client\");\n  }\n}\n\n// Handles dynamic roots updates during runtime, when client sends \"roots/list_changed\" notification, server fetches the updated roots and replaces all allowed directories with the new roots.\nserver.server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {\n  try {\n    // Request the updated roots list from the client\n    const response = await server.server.listRoots();\n    if (response && 'roots' in response) {\n      await updateAllowedDirectoriesFromRoots(response.roots);\n    }\n  } catch (error) {\n    console.error(\"Failed to request roots from client:\", error instanceof Error ? error.message : String(error));\n  }\n});\n\n// Handles post-initialization setup, specifically checking for and fetching MCP roots.\nserver.server.oninitialized = async () => {\n  const clientCapabilities = server.server.getClientCapabilities();\n\n  if (clientCapabilities?.roots) {\n    try {\n      const response = await server.server.listRoots();\n      if (response && 'roots' in response) {\n        await updateAllowedDirectoriesFromRoots(response.roots);\n      } else {\n        console.error(\"Client returned no roots set, keeping current settings\");\n      }\n    } catch (error) {\n      console.error(\"Failed to request initial roots from client:\", error instanceof Error ? error.message : String(error));\n    }\n  } else {\n    if (allowedDirectories.length > 0) {\n      console.error(\"Client does not support MCP Roots, using allowed directories set from server args:\", allowedDirectories);\n    }else{\n      throw new Error(`Server cannot operate: No allowed directories available. Server was started without command-line directories and client either does not support MCP roots protocol or provided empty roots. Please either: 1) Start server with directory arguments, or 2) Use a client that supports MCP roots protocol and provides valid root directories.`);\n    }\n  }\n};\n\n// Start server\nasync function runServer() {\n  const transport = new StdioServerTransport();\n  await server.connect(transport);\n  console.error(\"Secure MCP Filesystem Server running on stdio\");\n  if (allowedDirectories.length === 0) {\n    console.error(\"Started without allowed directories - waiting for client to provide roots via MCP protocol\");\n  }\n}\n\nrunServer().catch((error) => {\n  console.error(\"Fatal error running server:\", error);\n  process.exit(1);\n});\n"
    },
    "src/fetch/README.md": {
      "sha256": null,
      "content": "# Fetch MCP Server\n\n<!-- mcp-name: io.github.modelcontextprotocol/server-fetch -->\n\nA Model Context Protocol server that provides web content fetching capabilities. This server enables LLMs to retrieve and process content from web pages, converting HTML to markdown for easier consumption.\n\nSource: https://github.com/modelcontextprotocol/servers/tree/main/src/fetch\n\nRequires MCP Python SDK 1.x (`mcp>=1.29.0,<2`). SDK 2.0 renamed APIs this server uses. The port to v2 is in progress.\n\n> [!CAUTION]\n> This server can access local/internal IP addresses and may represent a security risk. Exercise caution when using this MCP server to ensure this does not expose any sensitive data.\n\nThe fetch tool will truncate the response, but by using the `start_index` argument, you can specify where to start the content extraction. This lets models read a webpage in chunks, until they find the information they need.\n\n### Available Tools\n\n- `fetch` - Fetches a URL from the internet and extracts its contents as markdown.\n    - `url` (string, required): URL to fetch\n    - `max_length` (integer, optional): Maximum number of characters to return (default: 5000)\n    - `start_index` (integer, optional): Start content from this character index (default: 0)\n    - `raw` (boolean, optional): Get raw content without markdown conversion (default: false)\n\n### Prompts\n\n- **fetch**\n  - Fetch a URL and extract its contents as markdown\n  - Arguments:\n    - `url` (string, required): URL to fetch\n\n## Installation\n\nOptionally: Install node.js, this will cause the fetch server to use a different HTML simplifier that is more robust.\n\n### Using uv (recommended)\n\nWhen using [`uv`](https://docs.astral.sh/uv/) no specific installation is needed. We will\nuse [`uvx`](https://docs.astral.sh/uv/guides/tools/) to directly run *mcp-server-fetch*.\n\n### Using PIP\n\nAlternatively you can install `mcp-server-fetch` via pip:\n\n```\npip install mcp-server-fetch\n```\n\nAfter installation, you can run it as a script using:\n\n```\npython -m mcp_server_fetch\n```\n\n## Configuration\n\n### Configure for Claude.app\n\nAdd to your Claude settings:\n\n<details>\n<summary>Using uvx</summary>\n\n```json\n{\n  \"mcpServers\": {\n    \"fetch\": {\n      \"command\": \"uvx\",\n      \"args\": [\"mcp-server-fetch\"]\n    }\n  }\n}\n```\n</details>\n\n<details>\n<summary>Using docker</summary>\n\n```json\n{\n  \"mcpServers\": {\n    \"fetch\": {\n      \"command\": \"docker\",\n      \"args\": [\"run\", \"-i\", \"--rm\", \"mcp/fetch\"]\n    }\n  }\n}\n```\n</details>\n\n<details>\n<summary>Using pip installation</summary>\n\n```json\n{\n  \"mcpServers\": {\n    \"fetch\": {\n      \"command\": \"python\",\n      \"args\": [\"-m\", \"mcp_server_fetch\"]\n    }\n  }\n}\n```\n</details>\n\n### Configure for VS Code\n\nFor quick installation, use one of the one-click install buttons below...\n\n[![Install with UV in VS Code](https://img.shields.io/badge/VS_Code-UV-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=fetch&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-fetch%22%5D%7D) [![Install with UV in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-UV-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=fetch&config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22mcp-server-fetch%22%5D%7D&quality=insiders)\n\n[![Install with Docker in VS Code](https://img.shields.io/badge/VS_Code-Docker-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=fetch&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22mcp%2Ffetch%22%5D%7D) [![Install with Docker in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Docker-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=fetch&config=%7B%22command%22%3A%22docker%22%2C%22args%22%3A%5B%22run%22%2C%22-i%22%2C%22--rm%22%2C%22mcp%2Ffetch%22%5D%7D&quality=insiders)\n\nFor manual installation, add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.\n\nOptionally, you can add it to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.\n\n> Note that the `mcp` key is needed when using the `mcp.json` file.\n\n<details>\n<summary>Using uvx</summary>\n\n```json\n{\n  \"mcp\": {\n    \"servers\": {\n      \"fetch\": {\n        \"command\": \"uvx\",\n        \"args\": [\"mcp-server-fetch\"]\n      }\n    }\n  }\n}\n```\n</details>\n\n<details>\n<summary>Using Docker</summary>\n\n```json\n{\n  \"mcp\": {\n    \"servers\": {\n      \"fetch\": {\n        \"command\": \"docker\",\n        \"args\": [\"run\", \"-i\", \"--rm\", \"mcp/fetch\"]\n      }\n    }\n  }\n}\n```\n</details>\n\n### Customization - robots.txt\n\nBy default, the server will obey a websites robots.txt file if the request came from the model (via a tool), but not if\nthe request was user initiated (via a prompt). This can be disabled by adding the argument `--ignore-robots-txt` to the\n`args` list in the configuration.\n\n### Customization - User-agent\n\nBy default, depending on if the request came from the model (via a tool), or was user initiated (via a prompt), the\nserver will use either the user-agent\n```\nModelContextProtocol/1.0 (Autonomous; +https://github.com/modelcontextprotocol/servers)\n```\nor\n```\nModelContextProtocol/1.0 (User-Specified; +https://github.com/modelcontextprotocol/servers)\n```\n\nThis can be customized by adding the argument `--user-agent=YourUserAgent` to the `args` list in the configuration.\n\n### Customization - Proxy\n\nThe server can be configured to use a proxy by using the `--proxy-url` argument.\n\n## Windows Configuration\n\nIf you're experiencing timeout issues on Windows, you may need to set the `PYTHONIOENCODING` environment variable to ensure proper character encoding:\n\n<details>\n<summary>Windows configuration (uvx)</summary>\n\n```json\n{\n  \"mcpServers\": {\n    \"fetch\": {\n      \"command\": \"uvx\",\n      \"args\": [\"mcp-server-fetch\"],\n      \"env\": {\n        \"PYTHONIOENCODING\": \"utf-8\"\n      }\n    }\n  }\n}\n```\n</details>\n\n<details>\n<summary>Windows configuration (pip)</summary>\n\n```json\n{\n  \"mcpServers\": {\n    \"fetch\": {\n      \"command\": \"python\",\n      \"args\": [\"-m\", \"mcp_server_fetch\"],\n      \"env\": {\n        \"PYTHONIOENCODING\": \"utf-8\"\n      }\n    }\n  }\n}\n```\n</details>\n\nThis addresses character encoding issues that can cause the server to timeout on Windows systems.\n\n## Debugging\n\nYou can use the MCP inspector to debug the server. For uvx installations:\n\n```\nnpx @modelcontextprotocol/inspector uvx mcp-server-fetch\n```\n\nOr if you've installed the package in a specific directory or are developing on it:\n\n```\ncd path/to/servers/src/fetch\nnpx @modelcontextprotocol/inspector uv run mcp-server-fetch\n```\n\n## Contributing\n\nWe encourage contributions to help expand and improve mcp-server-fetch. Whether you want to add new tools, enhance existing functionality, or improve documentation, your input is valuable.\n\nFor examples of other MCP servers and implementation patterns, see:\nhttps://github.com/modelcontextprotocol/servers\n\nPull requests are welcome! Feel free to contribute new ideas, bug fixes, or enhancements to make mcp-server-fetch even more powerful and useful.\n\n## License\n\nmcp-server-fetch is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.\n"
    },
    "src/fetch/src/mcp_server_fetch/server.py": {
      "sha256": null,
      "content": "from typing import Annotated, Tuple\nfrom urllib.parse import urlparse, urlunparse\n\nimport markdownify\nimport readabilipy.simple_json\nfrom mcp.shared.exceptions import McpError\nfrom mcp.server import Server\nfrom mcp.server.stdio import stdio_server\nfrom mcp.types import (\n    ErrorData,\n    GetPromptResult,\n    Prompt,\n    PromptArgument,\n    PromptMessage,\n    TextContent,\n    Tool,\n    INVALID_PARAMS,\n    INTERNAL_ERROR,\n)\nfrom protego import Protego\nfrom pydantic import BaseModel, Field, AnyUrl\n\nDEFAULT_USER_AGENT_AUTONOMOUS = \"ModelContextProtocol/1.0 (Autonomous; +https://github.com/modelcontextprotocol/servers)\"\nDEFAULT_USER_AGENT_MANUAL = \"ModelContextProtocol/1.0 (User-Specified; +https://github.com/modelcontextprotocol/servers)\"\n\n\ndef extract_content_from_html(html: str) -> str:\n    \"\"\"Extract and convert HTML content to Markdown format.\n\n    Args:\n        html: Raw HTML content to process\n\n    Returns:\n        Simplified markdown version of the content\n    \"\"\"\n    ret = readabilipy.simple_json.simple_json_from_html_string(\n        html, use_readability=True\n    )\n    if not ret[\"content\"]:\n        return \"<error>Page failed to be simplified from HTML</error>\"\n    content = markdownify.markdownify(\n        ret[\"content\"],\n        heading_style=markdownify.ATX,\n    )\n    return content\n\n\ndef get_robots_txt_url(url: str) -> str:\n    \"\"\"Get the robots.txt URL for a given website URL.\n\n    Args:\n        url: Website URL to get robots.txt for\n\n    Returns:\n        URL of the robots.txt file\n    \"\"\"\n    # Parse the URL into components\n    parsed = urlparse(url)\n\n    # Reconstruct the base URL with just scheme, netloc, and /robots.txt path\n    robots_url = urlunparse((parsed.scheme, parsed.netloc, \"/robots.txt\", \"\", \"\", \"\"))\n\n    return robots_url\n\n\nasync def check_may_autonomously_fetch_url(url: str, user_agent: str, proxy_url: str | None = None) -> None:\n    \"\"\"\n    Check if the URL can be fetched by the user agent according to the robots.txt file.\n    Raises a McpError if not.\n    \"\"\"\n    from httpx import AsyncClient, HTTPError\n\n    robot_txt_url = get_robots_txt_url(url)\n\n    async with AsyncClient(proxy=proxy_url) as client:\n        try:\n            response = await client.get(\n                robot_txt_url,\n                follow_redirects=True,\n                headers={\"User-Agent\": user_agent},\n            )\n        except HTTPError:\n            raise McpError(ErrorData(\n                code=INTERNAL_ERROR,\n                message=f\"Failed to fetch robots.txt {robot_txt_url} due to a connection issue\",\n            ))\n        if response.status_code in (401, 403):\n            raise McpError(ErrorData(\n                code=INTERNAL_ERROR,\n                message=f\"When fetching robots.txt ({robot_txt_url}), received status {response.status_code} so assuming that autonomous fetching is not allowed, the user can try manually fetching by using the fetch prompt\",\n            ))\n        elif 400 <= response.status_code < 500:\n            return\n        robot_txt = response.text\n    processed_robot_txt = \"\\n\".join(\n        line for line in robot_txt.splitlines() if not line.strip().startswith(\"#\")\n    )\n    robot_parser = Protego.parse(processed_robot_txt)\n    if not robot_parser.can_fetch(str(url), user_agent):\n        raise McpError(ErrorData(\n            code=INTERNAL_ERROR,\n            message=f\"The sites robots.txt ({robot_txt_url}), specifies that autonomous fetching of this page is not allowed, \"\n            f\"<useragent>{user_agent}</useragent>\\n\"\n            f\"<url>{url}</url>\"\n            f\"<robots>\\n{robot_txt}\\n</robots>\\n\"\n            f\"The assistant must let the user know that it failed to view the page. The assistant may provide further guidance based on the above information.\\n\"\n            f\"The assistant can tell the user that they can try manually fetching the page by using the fetch prompt within their UI.\",\n        ))\n\n\nasync def fetch_url(\n    url: str, user_agent: str, force_raw: bool = False, proxy_url: str | None = None\n) -> Tuple[str, str]:\n    \"\"\"\n    Fetch the URL and return the content in a form ready for the LLM, as well as a prefix string with status information.\n    \"\"\"\n    from httpx import AsyncClient, HTTPError\n\n    async with AsyncClient(proxy=proxy_url) as client:\n        try:\n            response = await client.get(\n                url,\n                follow_redirects=True,\n                headers={\"User-Agent\": user_agent},\n                timeout=30,\n            )\n        except HTTPError as e:\n            raise McpError(ErrorData(code=INTERNAL_ERROR, message=f\"Failed to fetch {url}: {e!r}\"))\n        if response.status_code >= 400:\n            raise McpError(ErrorData(\n                code=INTERNAL_ERROR,\n                message=f\"Failed to fetch {url} - status code {response.status_code}\",\n            ))\n\n        page_raw = response.text\n\n    content_type = response.headers.get(\"content-type\", \"\")\n    is_page_html = (\n        \"<html\" in page_raw[:100] or \"text/html\" in content_type or not content_type\n    )\n\n    if is_page_html and not force_raw:\n        return extract_content_from_html(page_raw), \"\"\n\n    return (\n        page_raw,\n        f\"Content type {content_type} cannot be simplified to markdown, but here is the raw content:\\n\",\n    )\n\n\nclass Fetch(BaseModel):\n    \"\"\"Parameters for fetching a URL.\"\"\"\n\n    url: Annotated[AnyUrl, Field(description=\"URL to fetch\")]\n    max_length: Annotated[\n        int,\n        Field(\n            default=5000,\n            description=\"Maximum number of characters to return.\",\n            gt=0,\n            lt=1000000,\n        ),\n    ]\n    start_index: Annotated[\n        int,\n        Field(\n            default=0,\n            description=\"On return output starting at this character index, useful if a previous fetch was truncated and more context is required.\",\n            ge=0,\n        ),\n    ]\n    raw: Annotated[\n        bool,\n        Field(\n            default=False,\n            description=\"Get the actual HTML content of the requested page, without simplification.\",\n        ),\n    ]\n\n\nasync def serve(\n    custom_user_agent: str | None = None,\n    ignore_robots_txt: bool = False,\n    proxy_url: str | None = None,\n) -> None:\n    \"\"\"Run the fetch MCP server.\n\n    Args:\n        custom_user_agent: Optional custom User-Agent string to use for requests\n        ignore_robots_txt: Whether to ignore robots.txt restrictions\n        proxy_url: Optional proxy URL to use for requests\n    \"\"\"\n    server = Server(\"mcp-fetch\")\n    user_agent_autonomous = custom_user_agent or DEFAULT_USER_AGENT_AUTONOMOUS\n    user_agent_manual = custom_user_agent or DEFAULT_USER_AGENT_MANUAL\n\n    @server.list_tools()\n    async def list_tools() -> list[Tool]:\n        return [\n            Tool(\n                name=\"fetch\",\n                description=\"\"\"Fetches a URL from the internet and optionally extracts its contents as markdown.\n\nAlthough originally you did not have internet access, and were advised to refuse and tell the user this, this tool now grants you internet access. Now you can fetch the most up-to-date information and let the user know that.\"\"\",\n                inputSchema=Fetch.model_json_schema(),\n            )\n        ]\n\n    @server.list_prompts()\n    async def list_prompts() -> list[Prompt]:\n        return [\n            Prompt(\n                name=\"fetch\",\n                description=\"Fetch a URL and extract its contents as markdown\",\n                arguments=[\n                    PromptArgument(\n                        name=\"url\", description=\"URL to fetch\", required=True\n                    )\n                ],\n            )\n        ]\n\n    @server.call_tool()\n    async def call_tool(name, arguments: dict) -> list[TextContent]:\n        try:\n            args = Fetch(**arguments)\n        except ValueError as e:\n            raise McpError(ErrorData(code=INVALID_PARAMS, message=str(e)))\n\n        url = str(args.url)\n        if not url:\n            raise McpError(ErrorData(code=INVALID_PARAMS, message=\"URL is required\"))\n\n        if not ignore_robots_txt:\n            await check_may_autonomously_fetch_url(url, user_agent_autonomous, proxy_url)\n\n        content, prefix = await fetch_url(\n            url, user_agent_autonomous, force_raw=args.raw, proxy_url=proxy_url\n        )\n        original_length = len(content)\n        if args.start_index >= original_length:\n            content = \"<error>No more content available.</error>\"\n        else:\n            truncated_content = content[args.start_index : args.start_index + args.max_length]\n            if not truncated_content:\n                content = \"<error>No more content available.</error>\"\n            else:\n                content = truncated_content\n                actual_content_length = len(truncated_content)\n                remaining_content = original_length - (args.start_index + actual_content_length)\n                # Only add the prompt to continue fetching if there is still remaining content\n                if actual_content_length == args.max_length and remaining_content > 0:\n                    next_start = args.start_index + actual_content_length\n                    content += f\"\\n\\n<error>Content truncated. Call the fetch tool with a start_index of {next_start} to get more content.</error>\"\n        return [TextContent(type=\"text\", text=f\"{prefix}Contents of {url}:\\n{content}\")]\n\n    @server.get_prompt()\n    async def get_prompt(name: str, arguments: dict | None) -> GetPromptResult:\n        if not arguments or \"url\" not in arguments:\n            raise McpError(ErrorData(code=INVALID_PARAMS, message=\"URL is required\"))\n\n        url = arguments[\"url\"]\n\n        try:\n            content, prefix = await fetch_url(url, user_agent_manual, proxy_url=proxy_url)\n            # TODO: after SDK bug is addressed, don't catch the exception\n        except McpError as e:\n            return GetPromptResult(\n                description=f\"Failed to fetch {url}\",\n                messages=[\n                    PromptMessage(\n                        role=\"user\",\n                        content=TextContent(type=\"text\", text=str(e)),\n                    )\n                ],\n            )\n        return GetPromptResult(\n            description=f\"Contents of {url}\",\n            messages=[\n                PromptMessage(\n                    role=\"user\", content=TextContent(type=\"text\", text=prefix + content)\n                )\n            ],\n        )\n\n    options = server.create_initialization_options()\n    async with stdio_server() as (read_stream, write_stream):\n        await server.run(read_stream, write_stream, options, raise_exceptions=False)\n"
    }
  },
  "filesystem_tools": [
    "read_text_file",
    "read_media_file",
    "read_multiple_files",
    "write_file",
    "edit_file",
    "create_directory",
    "list_directory",
    "list_directory_with_sizes",
    "move_file",
    "search_files",
    "directory_tree",
    "get_file_info",
    "list_allowed_directories"
  ],
  "fetch_tools": [
    "fetch"
  ]
}
