Skip to content

fix(filesystem): create_directory doesn't create parent directories - #4631

Open
rileybuilds wants to merge 1 commit into
modelcontextprotocol:mainfrom
rileybuilds:fix-filesystem-create-directory-recursive
Open

fix(filesystem): create_directory doesn't create parent directories#4631
rileybuilds wants to merge 1 commit into
modelcontextprotocol:mainfrom
rileybuilds:fix-filesystem-create-directory-recursive

Conversation

@rileybuilds

Copy link
Copy Markdown

Fixes #4629.

Bug

create_directory already calls fs.mkdir(validPath, { recursive: true }), but validatePath()'s security check for not-yet-existing paths only walked up one level before deciding a parent "does not exist":

const parentDir = path.dirname(absolute);
try {
  const realParentPath = await fs.realpath(parentDir);
  ...
} catch {
  throw new Error(`Parent directory does not exist: ${parentDir}`);
}

So creating <allowed_dir>/_diag/nested/deep when neither _diag nor _diag/nested exist yet fails with Parent directory does not exist: <allowed_dir>/_diag/nested, even though <allowed_dir> itself is a real, allowed directory one level further up — exactly the repro in #4629. Calling create_directory once per level works, confirming only the recursive case was broken, matching the issue.

Fix

Walk up through missing ancestors (not just the immediate parent) until an existing one is found, verifying via fs.realpath that it still resolves inside an allowed directory, or until the filesystem root is reached (in which case the original error is thrown). The top-level lexical containment check (isPathWithinAllowedDirectories against the full target path) already ran before this fallback, so this only extends how far the existence/symlink check climbs — it does not change what paths are considered in-bounds.

Testing

  • Updated __tests__/lib.test.ts: the existing 'rejects when parent directory does not exist' test was actually asserting the buggy single-level behavior (2 missing levels under an otherwise-real allowed directory) — replaced with a test that a real allowed ancestor further up is found and the path is accepted, plus a new test that a target with no existing ancestor anywhere (all the way to the filesystem root) still correctly rejects.
  • Proved the regression test both ways: reverted lib.ts and confirmed the new "walks past multiple missing ancestors" test fails with the exact error from the issue (Parent directory does not exist: .../nonexistent/nested), then restored the fix and confirmed it passes.
  • Full suite: 153/153 passing (npm test in src/filesystem).
  • Live smoke test against the built server (not mocked): spun up dist/index.js with the MCP SDK client, called create_directory with the issue's exact repro path (_diag/nested/deep, 3 missing levels), got a success response, and confirmed with a real fs.stat that all three levels exist as real directories on disk.

Scope note

The issue also flags that write_file has the same "parent must already exist" behavior, but is undocumented either way, and suggests deciding explicitly whether it should also become recursive. I left write_file unchanged — that's a separate documented-contract decision, whereas create_directory's README/tool-description explicitly promise recursive creation and the code just didn't do it.

validatePath()'s ENOENT fallback checked only the immediate parent
directory before allowing a new path through. Creating a path with
more than one missing level (e.g. a/b/c when none of a, b, c exist)
made the immediate parent check fail with "Parent directory does not
exist", even though fs.mkdir was already called with recursive: true
one level up in the create_directory handler and an existing allowed
ancestor was available further up the tree.

Walk up through missing ancestors until an existing one is found (or
the filesystem root is reached), verifying that ancestor still
resolves inside an allowed directory via realpath. The top-level
lexical containment check already ran against the full target path,
so this only extends how far the existence/symlink check climbs.

Fixes modelcontextprotocol#4629
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

server-filesystem: create_directory does not create parent directories

1 participant