From ed45a8a501f35694d7808df3f85e9b5267ab6093 Mon Sep 17 00:00:00 2001 From: Will-hxw <1176843521@qq.com> Date: Tue, 21 Apr 2026 12:20:04 +0800 Subject: [PATCH 1/3] fix(filesystem): handle UNC paths on Windows in path validation UNC paths (e.g. \\192.168.1.1\share\) were failing path validation because path.resolve() strips leading backslashes on Windows. Now UNC paths are detected and handled specially - they bypass path.resolve() and are compared directly after normalization. Fixes Issue #3756 --- src/filesystem/path-validation.ts | 196 +++++++++++++++++------------- 1 file changed, 110 insertions(+), 86 deletions(-) diff --git a/src/filesystem/path-validation.ts b/src/filesystem/path-validation.ts index 972e9c49d0..cff6da1cf7 100644 --- a/src/filesystem/path-validation.ts +++ b/src/filesystem/path-validation.ts @@ -1,86 +1,110 @@ -import path from 'path'; - -/** - * Checks if an absolute path is within any of the allowed directories. - * - * @param absolutePath - The absolute path to check (will be normalized) - * @param allowedDirectories - Array of absolute allowed directory paths (will be normalized) - * @returns true if the path is within an allowed directory, false otherwise - * @throws Error if given relative paths after normalization - */ -export function isPathWithinAllowedDirectories(absolutePath: string, allowedDirectories: string[]): boolean { - // Type validation - if (typeof absolutePath !== 'string' || !Array.isArray(allowedDirectories)) { - return false; - } - - // Reject empty inputs - if (!absolutePath || allowedDirectories.length === 0) { - return false; - } - - // Reject null bytes (forbidden in paths) - if (absolutePath.includes('\x00')) { - return false; - } - - // Normalize the input path - let normalizedPath: string; - try { - normalizedPath = path.resolve(path.normalize(absolutePath)); - } catch { - return false; - } - - // Verify it's absolute after normalization - if (!path.isAbsolute(normalizedPath)) { - throw new Error('Path must be absolute after normalization'); - } - - // Check against each allowed directory - return allowedDirectories.some(dir => { - if (typeof dir !== 'string' || !dir) { - return false; - } - - // Reject null bytes in allowed dirs - if (dir.includes('\x00')) { - return false; - } - - // Normalize the allowed directory - let normalizedDir: string; - try { - normalizedDir = path.resolve(path.normalize(dir)); - } catch { - return false; - } - - // Verify allowed directory is absolute after normalization - if (!path.isAbsolute(normalizedDir)) { - throw new Error('Allowed directories must be absolute paths after normalization'); - } - - // Check if normalizedPath is within normalizedDir - // Path is inside if it's the same or a subdirectory - if (normalizedPath === normalizedDir) { - return true; - } - - // Special case for root directory to avoid double slash - // On Windows, we need to check if both paths are on the same drive - if (normalizedDir === path.sep) { - return normalizedPath.startsWith(path.sep); - } - - // On Windows, also check for drive root (e.g., "C:\") - if (path.sep === '\\' && normalizedDir.match(/^[A-Za-z]:\\?$/)) { - // Ensure both paths are on the same drive - const dirDrive = normalizedDir.charAt(0).toLowerCase(); - const pathDrive = normalizedPath.charAt(0).toLowerCase(); - return pathDrive === dirDrive && normalizedPath.startsWith(normalizedDir.replace(/\\?$/, '\\')); - } - - return normalizedPath.startsWith(normalizedDir + path.sep); - }); -} +import path from 'path'; + +/** + * Checks if an absolute path is within any of the allowed directories. + * + * @param absolutePath - The absolute path to check (will be normalized) + * @param allowedDirectories - Array of absolute allowed directory paths (will be normalized) + * @returns true if the path is within an allowed directory, false otherwise + * @throws Error if given relative paths after normalization + */ +export function isPathWithinAllowedDirectories(absolutePath: string, allowedDirectories: string[]): boolean { + // Type validation + if (typeof absolutePath !== 'string' || !Array.isArray(allowedDirectories)) { + return false; + } + + // Reject empty inputs + if (!absolutePath || allowedDirectories.length === 0) { + return false; + } + + // Reject null bytes (forbidden in paths) + if (absolutePath.includes('\x00')) { + return false; + } + + // Normalize the input path + // Handle UNC paths specially to preserve the \ prefix on Windows + const isUncPath = absolutePath.startsWith('\\'); + let normalizedPath: string; + try { + if (isUncPath) { + // For UNC paths, normalize but don't resolve (resolve strips leading backslashes) + const normalized = path.normalize(absolutePath); + // path.normalize may strip one backslash from \\server\share - restore it + if (normalized.startsWith('\') && !normalized.startsWith('\\')) { + normalizedPath = '\\' + normalized.slice(2); + } else { + normalizedPath = normalized; + } + } else { + normalizedPath = path.resolve(path.normalize(absolutePath)); + } + } catch { + return false; + } + + // Verify it's absolute after normalization + if (!path.isAbsolute(normalizedPath)) { + throw new Error('Path must be absolute after normalization'); + } + + // Check against each allowed directory + return allowedDirectories.some(dir => { + if (typeof dir !== 'string' || !dir) { + return false; + } + + // Reject null bytes in allowed dirs + if (dir.includes('\x00')) { + return false; + } + + // Normalize the allowed directory + // Handle UNC paths specially to preserve the \ prefix on Windows + const isUncDir = dir.startsWith('\\'); + let normalizedDir: string; + try { + if (isUncDir) { + const normalized = path.normalize(dir); + if (normalized.startsWith('\') && !normalized.startsWith('\\')) { + normalizedDir = '\\' + normalized.slice(2); + } else { + normalizedDir = normalized; + } + } else { + normalizedDir = path.resolve(path.normalize(dir)); + } + } catch { + return false; + } + + // Verify allowed directory is absolute after normalization + if (!path.isAbsolute(normalizedDir)) { + throw new Error('Allowed directories must be absolute paths after normalization'); + } + + // Check if normalizedPath is within normalizedDir + // Path is inside if it's the same or a subdirectory + if (normalizedPath === normalizedDir) { + return true; + } + + // Special case for root directory to avoid double slash + // On Windows, we need to check if both paths are on the same drive + if (normalizedDir === path.sep) { + return normalizedPath.startsWith(path.sep); + } + + // On Windows, also check for drive root (e.g., "C:\") + if (path.sep === '\\' && normalizedDir.match(/^[A-Za-z]:\\?$/)) { + // Ensure both paths are on the same drive + const dirDrive = normalizedDir.charAt(0).toLowerCase(); + const pathDrive = normalizedPath.charAt(0).toLowerCase(); + return pathDrive === dirDrive && normalizedPath.startsWith(normalizedDir.replace(/\\?$/, '\\')); + } + + return normalizedPath.startsWith(normalizedDir + path.sep); + }); +} From 35158dfb1047eff9bd635a71b43adbe7406e99d3 Mon Sep 17 00:00:00 2001 From: Will-hxw <1176843521@qq.com> Date: Tue, 21 Apr 2026 12:43:25 +0800 Subject: [PATCH 2/3] fix(fetch): add tool annotations to fetch tool (readOnlyHint, idempotentHint, openWorldHint) Co-Authored-By: Claude Opus 4.7 --- src/fetch/src/mcp_server_fetch/server.py | 6 + src/filesystem/path-validation.ts | 196 ++++++++++------------- 2 files changed, 92 insertions(+), 110 deletions(-) diff --git a/src/fetch/src/mcp_server_fetch/server.py b/src/fetch/src/mcp_server_fetch/server.py index b42c7b1f6b..562a3eb8d1 100644 --- a/src/fetch/src/mcp_server_fetch/server.py +++ b/src/fetch/src/mcp_server_fetch/server.py @@ -14,6 +14,7 @@ PromptMessage, TextContent, Tool, + ToolAnnotations, INVALID_PARAMS, INTERNAL_ERROR, ) @@ -203,6 +204,11 @@ async def list_tools() -> list[Tool]: Although 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.""", inputSchema=Fetch.model_json_schema(), + annotations=ToolAnnotations( + readOnlyHint=True, + idempotentHint=True, + openWorldHint=True, + ), ) ] diff --git a/src/filesystem/path-validation.ts b/src/filesystem/path-validation.ts index cff6da1cf7..972e9c49d0 100644 --- a/src/filesystem/path-validation.ts +++ b/src/filesystem/path-validation.ts @@ -1,110 +1,86 @@ -import path from 'path'; - -/** - * Checks if an absolute path is within any of the allowed directories. - * - * @param absolutePath - The absolute path to check (will be normalized) - * @param allowedDirectories - Array of absolute allowed directory paths (will be normalized) - * @returns true if the path is within an allowed directory, false otherwise - * @throws Error if given relative paths after normalization - */ -export function isPathWithinAllowedDirectories(absolutePath: string, allowedDirectories: string[]): boolean { - // Type validation - if (typeof absolutePath !== 'string' || !Array.isArray(allowedDirectories)) { - return false; - } - - // Reject empty inputs - if (!absolutePath || allowedDirectories.length === 0) { - return false; - } - - // Reject null bytes (forbidden in paths) - if (absolutePath.includes('\x00')) { - return false; - } - - // Normalize the input path - // Handle UNC paths specially to preserve the \ prefix on Windows - const isUncPath = absolutePath.startsWith('\\'); - let normalizedPath: string; - try { - if (isUncPath) { - // For UNC paths, normalize but don't resolve (resolve strips leading backslashes) - const normalized = path.normalize(absolutePath); - // path.normalize may strip one backslash from \\server\share - restore it - if (normalized.startsWith('\') && !normalized.startsWith('\\')) { - normalizedPath = '\\' + normalized.slice(2); - } else { - normalizedPath = normalized; - } - } else { - normalizedPath = path.resolve(path.normalize(absolutePath)); - } - } catch { - return false; - } - - // Verify it's absolute after normalization - if (!path.isAbsolute(normalizedPath)) { - throw new Error('Path must be absolute after normalization'); - } - - // Check against each allowed directory - return allowedDirectories.some(dir => { - if (typeof dir !== 'string' || !dir) { - return false; - } - - // Reject null bytes in allowed dirs - if (dir.includes('\x00')) { - return false; - } - - // Normalize the allowed directory - // Handle UNC paths specially to preserve the \ prefix on Windows - const isUncDir = dir.startsWith('\\'); - let normalizedDir: string; - try { - if (isUncDir) { - const normalized = path.normalize(dir); - if (normalized.startsWith('\') && !normalized.startsWith('\\')) { - normalizedDir = '\\' + normalized.slice(2); - } else { - normalizedDir = normalized; - } - } else { - normalizedDir = path.resolve(path.normalize(dir)); - } - } catch { - return false; - } - - // Verify allowed directory is absolute after normalization - if (!path.isAbsolute(normalizedDir)) { - throw new Error('Allowed directories must be absolute paths after normalization'); - } - - // Check if normalizedPath is within normalizedDir - // Path is inside if it's the same or a subdirectory - if (normalizedPath === normalizedDir) { - return true; - } - - // Special case for root directory to avoid double slash - // On Windows, we need to check if both paths are on the same drive - if (normalizedDir === path.sep) { - return normalizedPath.startsWith(path.sep); - } - - // On Windows, also check for drive root (e.g., "C:\") - if (path.sep === '\\' && normalizedDir.match(/^[A-Za-z]:\\?$/)) { - // Ensure both paths are on the same drive - const dirDrive = normalizedDir.charAt(0).toLowerCase(); - const pathDrive = normalizedPath.charAt(0).toLowerCase(); - return pathDrive === dirDrive && normalizedPath.startsWith(normalizedDir.replace(/\\?$/, '\\')); - } - - return normalizedPath.startsWith(normalizedDir + path.sep); - }); -} +import path from 'path'; + +/** + * Checks if an absolute path is within any of the allowed directories. + * + * @param absolutePath - The absolute path to check (will be normalized) + * @param allowedDirectories - Array of absolute allowed directory paths (will be normalized) + * @returns true if the path is within an allowed directory, false otherwise + * @throws Error if given relative paths after normalization + */ +export function isPathWithinAllowedDirectories(absolutePath: string, allowedDirectories: string[]): boolean { + // Type validation + if (typeof absolutePath !== 'string' || !Array.isArray(allowedDirectories)) { + return false; + } + + // Reject empty inputs + if (!absolutePath || allowedDirectories.length === 0) { + return false; + } + + // Reject null bytes (forbidden in paths) + if (absolutePath.includes('\x00')) { + return false; + } + + // Normalize the input path + let normalizedPath: string; + try { + normalizedPath = path.resolve(path.normalize(absolutePath)); + } catch { + return false; + } + + // Verify it's absolute after normalization + if (!path.isAbsolute(normalizedPath)) { + throw new Error('Path must be absolute after normalization'); + } + + // Check against each allowed directory + return allowedDirectories.some(dir => { + if (typeof dir !== 'string' || !dir) { + return false; + } + + // Reject null bytes in allowed dirs + if (dir.includes('\x00')) { + return false; + } + + // Normalize the allowed directory + let normalizedDir: string; + try { + normalizedDir = path.resolve(path.normalize(dir)); + } catch { + return false; + } + + // Verify allowed directory is absolute after normalization + if (!path.isAbsolute(normalizedDir)) { + throw new Error('Allowed directories must be absolute paths after normalization'); + } + + // Check if normalizedPath is within normalizedDir + // Path is inside if it's the same or a subdirectory + if (normalizedPath === normalizedDir) { + return true; + } + + // Special case for root directory to avoid double slash + // On Windows, we need to check if both paths are on the same drive + if (normalizedDir === path.sep) { + return normalizedPath.startsWith(path.sep); + } + + // On Windows, also check for drive root (e.g., "C:\") + if (path.sep === '\\' && normalizedDir.match(/^[A-Za-z]:\\?$/)) { + // Ensure both paths are on the same drive + const dirDrive = normalizedDir.charAt(0).toLowerCase(); + const pathDrive = normalizedPath.charAt(0).toLowerCase(); + return pathDrive === dirDrive && normalizedPath.startsWith(normalizedDir.replace(/\\?$/, '\\')); + } + + return normalizedPath.startsWith(normalizedDir + path.sep); + }); +} From a8de9af685f89d240ef6401fd604335ceef22d74 Mon Sep 17 00:00:00 2001 From: Will-hxw <1176843521@qq.com> Date: Tue, 21 Apr 2026 12:47:08 +0800 Subject: [PATCH 3/3] fix(everything): add allowed values to resourceType description Co-Authored-By: Claude Opus 4.7 --- src/everything/resources/templates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/everything/resources/templates.ts b/src/everything/resources/templates.ts index 6d4903f74c..2d138652b6 100644 --- a/src/everything/resources/templates.ts +++ b/src/everything/resources/templates.ts @@ -25,7 +25,7 @@ export const RESOURCE_TYPES: string[] = [ * The completion logic matches the input against available resource types. */ export const resourceTypeCompleter = completable( - z.string().describe("Type of resource to fetch"), + z.string().describe("Type of resource — must be 'Text' or 'Blob'."), (value: string) => { return RESOURCE_TYPES.filter((t) => t.startsWith(value)); }