{"record":{"id":"38f9e6dd35c98e15","repo":"modelcontextprotocol/servers","slug":"ambiguous-unicode-path-component-requestedpart","errorCode":null,"errorMessage":"Ambiguous Unicode path component: ${requestedPart}","messagePattern":"Ambiguous Unicode path component: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/filesystem/lib.ts","lineNumber":121,"sourceCode":"    .find(directory => isPathWithinAllowedDirectories(normalizePath(absolutePath), [directory]));\n\n  if (!allowedDirectory) {\n    return absolutePath;\n  }\n\n  let currentPath = await fs.realpath(allowedDirectory);\n  const relativeParts = path.relative(allowedDirectory, absolutePath).split(path.sep).filter(Boolean);\n\n  for (let index = 0; index < relativeParts.length; index++) {\n    const requestedPart = relativeParts[index];\n    const entries = (await fs.readdir(currentPath)) ?? [];\n    const exactMatch = entries.find(entry => entry === requestedPart);\n    const equivalentMatches = exactMatch\n      ? [exactMatch]\n      : entries.filter(entry => entry.normalize('NFC') === requestedPart.normalize('NFC'));\n\n    if (equivalentMatches.length > 1) {\n      throw new Error(`Ambiguous Unicode path component: ${requestedPart}`);\n    }\n\n    if (equivalentMatches.length === 0) {\n      // Nothing below this point exists yet, so there are no symlinks left to\n      // resolve. currentPath is already realpath'd and inside an allowed\n      // directory; append the missing tail so create_directory can mkdir -p it.\n      return path.join(currentPath, ...relativeParts.slice(index));\n    }\n\n    currentPath = await fs.realpath(path.join(currentPath, equivalentMatches[0]));\n    if (!isPathWithinAllowedDirectories(normalizePath(currentPath), allowedDirectories)) {\n      throw new Error(`Access denied - symlink target outside allowed directories: ${currentPath} not in ${allowedDirectories.join(', ')}`);\n    }\n  }\n\n  return currentPath;\n}\n","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/modelcontextprotocol/servers/blob/579c3903f30044eb702a599a74b3ae77588e722e/src/filesystem/lib.ts#L103-L139","documentation":"resolveUnicodeEquivalentPath walks the target path component by component. When an exact directory-entry match is absent but multiple entries normalize (NFC) to the same component, it refuses to guess which one was meant and throws this error. This protects against picking the wrong file among Unicode-equivalent names (e.g. 'cafe\\u0301' vs 'caf\\u00e9').","triggerScenarios":"Calling any filesystem tool (via validatePath) for a path whose component doesn't exactly exist but where two or more sibling entries have the same NFC normalization, e.g. directory contains both decomposed and precomposed spellings of the same name.","commonSituations":"Files synced from macOS (NFD) alongside files created on Linux (NFC), archives extracted with both spellings, or git checkouts with core.precomposeUnicode disabled, leaving duplicate-looking names in one directory.","solutions":["List the directory and remove/rename the duplicate Unicode-equivalent entries so only one NFC-normalized spelling remains","Use the exact on-disk spelling of the component (verified with readdir) so the exactMatch branch short-circuits ambiguity","Enable NFC normalization on the filesystem/sync tool that created the duplicate entries"],"exampleFix":"// before: requesting '/allowed/docs/caf\\u00e9' when dir has 'caf\\u00e9' (NFC) and 'cafe\\u0301' (NFD)\nawait read_file('/allowed/docs/caf\\u00e9');\n// after: disambiguate to the exact on-disk name\nawait read_file('/allowed/docs/cafe\\u0301'); // exact match, no ambiguity","handlingStrategy":"try-catch","validationCode":"const entries = await fs.readdir(path.dirname(p));\nconst target = path.basename(p);\nconst exact = entries.includes(target);\nconst equiv = entries.filter(e => e.normalize('NFC') === target.normalize('NFC'));\nif (!exact && equiv.length > 1) throw new Error(`Ambiguous Unicode component: ${target}`);","typeGuard":"function hasUniqueUnicodeMatch(entries: string[], target: string): boolean {\n  if (entries.includes(target)) return true;\n  return entries.filter(e => e.normalize('NFC') === target.normalize('NFC')).length === 1;\n}","tryCatchPattern":"try {\n  const resolved = await validatePath(p);\n} catch (err) {\n  if (err.message.startsWith('Ambiguous Unicode path component')) {\n    const entries = await fs.readdir(path.dirname(p));\n    // inspect entries and pick the exact intended spelling\n  } else throw err;\n}","preventionTips":["Normalize filenames to NFC when creating/syncing files","Periodically scan allowed roots for NFC-duplicate names","Always reference files by their exact readdir-reported names","Disable tools that decompose filenames (macOS NFD) when sharing with Linux"],"tags":["filesystem","unicode","path-resolution","ambiguity"],"backgroundTag":"unicode-normalization-ambiguity","analyzedSha":"579c3903f30044eb702a599a74b3ae77588e722e","analyzedAt":"2026-09-01T06:06:22.520Z","contentChangedAt":"2026-09-01T06:06:22.520Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}