Skip to content

feat(tools): add vault_read/list/write/delete_file MCP tools#39

Merged
raphasouthall merged 2 commits into
mainfrom
feat/vault-file-tools
May 11, 2026
Merged

feat(tools): add vault_read/list/write/delete_file MCP tools#39
raphasouthall merged 2 commits into
mainfrom
feat/vault-file-tools

Conversation

@raphasouthall
Copy link
Copy Markdown
Owner

Motivation

Microsoft Copilot Studio agent needs write access to the brain vault over
MCP (copilot.solidit.uk → mcp-shim → NeuroStack :8001). The existing 20
tools are all read/search/memory; nothing can author or edit a markdown file.
This PR adds the four tools needed to close that gap.

Tool surface

Tool Hints Signature
vault_read_file read_only (path) -> {path, exists, size_bytes, content}
vault_list_files read_only (directory="", pattern="*.md", recursive=True) -> {directory, files:[{path,size_bytes,modified_iso}]}
vault_write_file idempotent write (path, content, commit_message=None) -> {path, written, created, bytes_written, commit_sha, pushed, rolled_back, no_changes, git_error, index_update_needed, index_hint}
vault_delete_file destructive write (path, commit_message=None) -> {path, deleted, commit_sha, pushed, rolled_back, no_changes, git_error, index_update_needed, index_hint}

All paths are relative to config.vault_root (defaults to ~/brain).
MCP server tool count: 20 → 24.

Security model

Path safety

_safe_path rejects:

  • absolute paths
  • any segment that is empty, ., or ..
  • any segment starting with . (blocks .git, .obsidian, .trash,
    .claude, .neurostack, .gitignore)
  • any extension other than .md
  • symlinks whose target resolves outside vault_root

Frontmatter validation (writes only)

Hard-rejects if:

  • the file does not start with a ---\n...\n---\n YAML block
  • the YAML is not a mapping or fails to parse
  • any of date, tags, type is missing or None
    (empty list/empty string is accepted — agents can iterate)

Concurrency

Writes and deletes serialise under fcntl.flock on
<vault_root>/.neurostack-write.lock. The lock file is dot-prefixed so it
is filtered out by the tools' own listing rules; it never enters git.

Note

Deploy step: add .neurostack-write.lock to brain's .gitignore if not
already present.

Git flow / conflict handling

On every write or delete:

git add <path>
git commit -m "<msg>"          # default: "vault_write_file: <path> (via MCP)"
git push origin main

If the push is rejected (non-fast-forward etc.):

git pull --rebase --autostash origin main
git push origin main           # retry once

If the rebase or the second push fails, the local commit is rolled back
with git reset --hard HEAD~1 (only if our commit is still HEAD), the
working tree returns to the pre-write state via git's own restoration,
and the response carries pushed=false, rolled_back=true, git_error="...".

Tests

tests/test_file_tools.py (31 cases, all pass alongside the existing 517):

  • Path safety: traversal, absolute, dot-segment, dot-prefix filename,
    non-md extension, empty input, symlink-pointing-outside, valid path,
    _safe_dir happy + traversal.
  • Read: existing / nonexistent / rejected path.
  • List: empty dir, recursive vs non, hidden-segment exclusion,
    pattern, rejected dir.
  • Write: create new, overwrite existing, missing frontmatter rejection,
    missing required-field rejection, custom commit message, idempotent
    no-op (same content twice), rollback on push failure for both create
    and overwrite.
  • Delete: existing, nonexistent, rejected path.
  • Concurrency: two threads writing different files — both succeed,
    flock serialises commits, no git collision.

Tests run against a tmp_path git repo with a tmp_path bare remote. No
traffic to raphasouthall/brain from CI or dev machines.

uv run pytest tests/test_file_tools.py -v   # 31 passed in 1.25s
uv run pytest                                # 548 passed in 5.54s
uv run ruff check src/ tests/                # clean

End-to-end verified via uv run neurostack serve --transport http --port 8003
against a throwaway tmp repo — initialize → tools/list reports 24 tools;
tools/call round-trip on vault_write_filevault_list_files
vault_read_filevault_delete_file works (writes commit and push, reads
return content, deletes commit and push).

Deploy plan (post-merge — manual)

  1. Set git identity on LXC 122 (currently missing):
    pct exec 122 -- git config --global user.name  "Raphael Southall (LXC 122 / NeuroStack MCP)"
    pct exec 122 -- git config --global user.email "raphasouthall@gmail.com"
  2. Merge feat/vault-file-toolsmain on GitHub.
  3. On LXC 122:
    pct exec 122 -- bash -c "cd /root/neurostack && git pull origin main && systemctl restart neurostack-mcp"
  4. Add .neurostack-write.lock to ~/brain/.gitignore (one-off).
  5. Verify from pop-os:
    curl -sS https://copilot.solidit.uk/mcp \
      -H "Authorization: Bearer $(VAULT_ADDR=https://192.168.0.62:8200 \
         VAULT_SKIP_VERIFY=true vault kv get -field=bearer_secret \
         homelab/cloudflare/access/copilot-mcp)" \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
    Should return 24 tools.

Out of scope

  • vault_append_file (deferred to v2 — write+overwrite covers the agent
    use case)
  • automatic index.md updates (the tools return index_update_needed +
    index_hint; the Copilot agent's system prompt will handle the
    follow-up edit)
  • mcp-shim / Cloudflare Tunnel changes (the existing single-bearer path
    on copilot.solidit.uk is untouched)

Raphael Southall added 2 commits May 11, 2026 12:35
Four new tools for raw markdown CRUD against the brain vault, designed for
MCP clients (Microsoft Copilot Studio) that need write access without ssh.

- vault_read_file(path) — read .md file content + size
- vault_list_files(directory, pattern, recursive) — list .md files,
  hidden segments always excluded
- vault_write_file(path, content, commit_message) — create or overwrite,
  hard-rejects writes without required frontmatter (date, tags, type),
  commits + pushes origin/main under flock with rebase-on-conflict +
  rollback on push failure
- vault_delete_file(path, commit_message) — delete + commit + push

Path safety: relative-only, .md-only, no '..', no dot-prefixed segments
(.git/.obsidian/.trash/.claude/.neurostack), symlink-escape rejected.

Concurrency serialised via fcntl.flock on <vault_root>/.neurostack-write.lock.

Tool count on the MCP server: 20 → 24.

Tests: tests/test_file_tools.py covers path safety, read/list/write/delete
happy paths and rejections, custom commit messages, idempotent no-op writes,
rollback on simulated push failure for both create and overwrite, and
two-thread concurrency. Tests use a tmp git repo with a tmp bare remote;
no traffic to raphasouthall/brain.
@raphasouthall raphasouthall merged commit 412f7c9 into main May 11, 2026
5 checks passed
@raphasouthall raphasouthall deleted the feat/vault-file-tools branch May 11, 2026 11:41
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.

1 participant