Skip to content

Instantly share code, notes, and snippets.

@gautiermichelin
Last active March 16, 2026 11:59
Show Gist options
  • Select an option

  • Save gautiermichelin/52092a59045897c1390843a10159de2b to your computer and use it in GitHub Desktop.

Select an option

Save gautiermichelin/52092a59045897c1390843a10159de2b to your computer and use it in GitHub Desktop.
MCP Filesystem Server — Unicode NFC patch for macOS (fixes accented chars in paths like 'Drive partagés')

MCP Filesystem Server — Unicode NFC Patch for macOS

Problem

The official MCP filesystem server (@modelcontextprotocol/server-filesystem) fails to access directories containing accented characters on macOS (e.g. Drive partagés from Google Drive).

Root cause: macOS stores filenames in Unicode NFD (decomposed) form where é = e + combining accent (U+0301), while user input and the MCP client send paths in NFC (composed) form where é = single codepoint U+00E9. The server compares these byte-different strings and rejects the path as "outside allowed directories".

This affects any macOS path with accented characters (French, German, Spanish, Korean, etc.).

Fix

Three surgical changes to normalize all paths to NFC before comparison:

  • unicode-nfc.ts — New helper module: wraps String.prototype.normalize('NFC') (native Node.js, zero dependencies)
  • path-utils.tsnormalizePath() now returns NFC-normalized strings (3 return points patched)
  • path-validation.tsisPathWithinAllowedDirectories() NFC-normalizes both the input path and allowed directories before comparison

Setup

Prerequisites

  • Node.js ≥ 18
  • npm
  • git

Installation

# 1. Download all files from this gist into a folder
mkdir -p ~/claude/mcp-filesystem-nfc-patch
cd ~/claude/mcp-filesystem-nfc-patch
# (download the 4 files here: setup script + 3 .ts files)

# 2. Run the setup script
bash setup-mcp-filesystem-nfc.sh

The script will:

  1. Clone the official MCP servers repo (sparse checkout, only src/filesystem)
  2. Copy the 3 patched TypeScript files over the originals
  3. Run npm install and npm run build
  4. Verify NFC normalization is present in the compiled output
  5. Print the Claude Desktop config to use

Claude Desktop config

After the script completes, update your Claude Desktop config (Settings → Developer → Edit Config):

{
  "mcpServers": {
    "filesystem": {
      "command": "node",
      "args": [
        "/Users/YOUR_USER/claude/mcp-filesystem-patched/src/filesystem/dist/index.js",
        "/Users/YOUR_USER/Library/CloudStorage/GoogleDrive-xxx@yyy.com/Drive partagés",
        "/Users/YOUR_USER/Desktop"
      ]
    }
  }
}

Then restart Claude Desktop (Cmd+Q, reopen).

Updating

To update to a newer upstream version while keeping the patch:

cd ~/claude/mcp-filesystem-patched
git pull origin main
cd src/filesystem
cp ~/claude/mcp-filesystem-nfc-patch/unicode-nfc.ts .
cp ~/claude/mcp-filesystem-nfc-patch/path-utils.ts .
cp ~/claude/mcp-filesystem-nfc-patch/path-validation.ts .
npm install && npm run build

Upstream

Based on @modelcontextprotocol/server-filesystem v0.6.3 from modelcontextprotocol/servers.

A PR should be submitted to fix this upstream. The fix is backward-compatible: NFC normalization on already-NFC strings is a no-op.

import { normalizeUnicodeNFC } from "./unicode-nfc.js";
import path from "path";
import os from 'os';
/**
* Converts WSL or Unix-style Windows paths to Windows format
* @param p The path to convert
* @returns Converted Windows path
*/
export function convertToWindowsPath(p: string): string {
// Handle WSL paths (/mnt/c/...)
// NEVER convert WSL paths - they are valid Linux paths that work with Node.js fs operations in WSL
// Converting them to Windows format (C:\...) breaks fs operations inside WSL
if (p.startsWith('/mnt/')) {
return p; // Leave WSL paths unchanged
}
// Handle Unix-style Windows paths (/c/...)
// Only convert when running on Windows
if (p.match(/^\/[a-zA-Z]\//) && process.platform === 'win32') {
const driveLetter = p.charAt(1).toUpperCase();
const pathPart = p.slice(2).replace(/\//g, '\\');
return `${driveLetter}:${pathPart}`;
}
// Handle standard Windows paths, ensuring backslashes
if (p.match(/^[a-zA-Z]:/)) {
return p.replace(/\//g, '\\');
}
// Leave non-Windows paths unchanged
return p;
}
/**
* Normalizes path by standardizing format while preserving OS-specific behavior
* @param p The path to normalize
* @returns Normalized path
*/
export function normalizePath(p: string): string {
// Remove any surrounding quotes and whitespace
p = p.trim().replace(/^["']|["']$/g, '');
// Check if this is a Unix path that should not be converted
// WSL paths (/mnt/) should ALWAYS be preserved as they work correctly in WSL with Node.js fs
// Regular Unix paths should also be preserved
const isUnixPath = p.startsWith('/') && (
// Always preserve WSL paths (/mnt/c/, /mnt/d/, etc.)
p.match(/^\/mnt\/[a-z]\//i) ||
// On non-Windows platforms, treat all absolute paths as Unix paths
(process.platform !== 'win32') ||
// On Windows, preserve Unix paths that aren't Unix-style Windows paths (/c/, /d/, etc.)
(process.platform === 'win32' && !p.match(/^\/[a-zA-Z]\//))
);
if (isUnixPath) {
// For Unix paths, just normalize without converting to Windows format
// Replace double slashes with single slashes and remove trailing slashes
return normalizeUnicodeNFC(p.replace(/\/+/g, '/').replace(/(?<!^)\/$/, ''));
}
// Convert Unix-style Windows paths (/c/, /d/) to Windows format if on Windows
// This function will now leave /mnt/ paths unchanged
p = convertToWindowsPath(p);
// Handle double backslashes, preserving leading UNC \\
if (p.startsWith('\\\\')) {
// For UNC paths, first normalize any excessive leading backslashes to exactly \\
// Then normalize double backslashes in the rest of the path
let uncPath = p;
// Replace multiple leading backslashes with exactly two
uncPath = uncPath.replace(/^\\{2,}/, '\\\\');
// Now normalize any remaining double backslashes in the rest of the path
const restOfPath = uncPath.substring(2).replace(/\\\\/g, '\\');
p = '\\\\' + restOfPath;
} else {
// For non-UNC paths, normalize all double backslashes
p = p.replace(/\\\\/g, '\\');
}
// Use Node's path normalization, which handles . and .. segments
let normalized = path.normalize(p);
// Fix UNC paths after normalization (path.normalize can remove a leading backslash)
if (p.startsWith('\\\\') && !normalized.startsWith('\\\\')) {
normalized = '\\' + normalized;
}
// Handle Windows paths: convert slashes and ensure drive letter is capitalized
if (normalized.match(/^[a-zA-Z]:/)) {
let result = normalized.replace(/\//g, '\\');
// Capitalize drive letter if present
if (/^[a-z]:/.test(result)) {
result = result.charAt(0).toUpperCase() + result.slice(1);
}
return normalizeUnicodeNFC(result);
}
// On Windows, convert forward slashes to backslashes for relative paths
// On Linux/Unix, preserve forward slashes
if (process.platform === 'win32') {
return normalized.replace(/\//g, '\\');
}
// On non-Windows platforms, normalize Unicode to NFC (fixes macOS NFD/NFC mismatch)
return normalizeUnicodeNFC(normalized);
}
/**
* Expands home directory tildes in paths
* @param filepath The path to expand
* @returns Expanded path
*/
export function expandHome(filepath: string): string {
if (filepath.startsWith('~/') || filepath === '~') {
return path.join(os.homedir(), filepath.slice(1));
}
return filepath;
}
import { normalizeUnicodeNFC } from "./unicode-nfc.js";
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 = normalizeUnicodeNFC(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 = normalizeUnicodeNFC(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);
});
}
#!/bin/bash
# =============================================================================
# MCP Filesystem Server — Unicode NFC patch for macOS
# Fixes: accented chars in paths (e.g. "Drive partagés") cause access denied
#
# Run: bash setup-mcp-filesystem-nfc.sh
# =============================================================================
set -euo pipefail
INSTALL_DIR="$HOME/claude/mcp-filesystem-patched"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "=== MCP Filesystem Server — NFC Patch ==="
# 1. Clone
if [ ! -d "$INSTALL_DIR/.git" ]; then
echo "[1/5] Cloning (sparse: src/filesystem only)..."
mkdir -p "$(dirname "$INSTALL_DIR")"
git clone --depth 1 --filter=blob:none --sparse \
https://github.com/modelcontextprotocol/servers.git "$INSTALL_DIR" 2>&1 | tail -2
cd "$INSTALL_DIR"
git sparse-checkout set src/filesystem
else
echo "[1/5] Already cloned, skipping"
cd "$INSTALL_DIR"
fi
FS_DIR="$INSTALL_DIR/src/filesystem"
cd "$FS_DIR"
# 2. Copy patched files
echo "[2/5] Applying NFC patch (3 files)..."
cp "$SCRIPT_DIR/unicode-nfc.ts" "$FS_DIR/unicode-nfc.ts"
cp "$SCRIPT_DIR/path-utils.ts" "$FS_DIR/path-utils.ts"
cp "$SCRIPT_DIR/path-validation.ts" "$FS_DIR/path-validation.ts"
echo " ✓ unicode-nfc.ts, path-utils.ts, path-validation.ts"
# 3. Install
echo "[3/5] npm install..."
npm install 2>&1 | tail -3
# 4. Build
echo "[4/5] npm run build..."
npm run build 2>&1 | tail -3
# 5. Verify
echo "[5/5] Verifying..."
if grep -q 'normalizeUnicodeNFC' dist/path-utils.js && \
grep -q 'normalizeUnicodeNFC' dist/path-validation.js; then
echo " ✅ Build OK — NFC normalization active"
else
echo " ❌ ERROR: NFC not found in build output"
exit 1
fi
DIST="$FS_DIR/dist/index.js"
echo ""
echo "=========================================="
echo "SUCCESS — Server built at:"
echo " $DIST"
echo ""
echo "Claude Desktop config (paste in Settings > Developer > Edit Config):"
echo ""
echo '{
"mcpServers": {
"filesystem": {
"command": "node",
"args": [
"'$DIST'",
"/Users/gautier/Library/CloudStorage/GoogleDrive-gm@ideesculture.com/Drive partagés",
"/Users/gautier/Desktop"
]
}
}
}'
echo ""
echo "Then RESTART Claude Desktop (Cmd+Q, reopen)."
echo "=========================================="
/**
* Unicode NFC normalization for macOS path compatibility.
*
* macOS stores filenames in NFD (decomposed) form: "é" = "e" + U+0301
* User input and most systems use NFC (composed): "é" = U+00E9
*
* This causes path comparison failures in the MCP filesystem server when
* allowed directories contain accented characters (e.g. "Drive partagés").
*
* Fix: normalize all paths to NFC before comparison.
*/
export function normalizeUnicodeNFC(s: string): string {
return s.normalize('NFC');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment