Tutorial

How to Build a Secure Filesystem and Git MCP Agent

Build an MCP agent that safely reads files and manages git repos. Includes sandboxing, path validation, and search-grounded code generation.

MCP filesystem and git tools give AI agents direct access to your codebase, which is powerful but dangerous without guardrails. This tutorial builds a secure agent that can read files, search codebases, and manage git operations while enforcing path sandboxing, read-only defaults, and operation allowlists. Search grounding ensures any generated code is based on current documentation rather than stale training data.

Prerequisites

  • Claude Code installed
  • Node.js 18+ installed
  • A git repository to work with
  • A Scavio API key for search grounding

Walkthrough

Step 1: Configure filesystem and git MCP with sandboxing

Set up the MCP servers with restricted paths. The filesystem server only accesses an allowed directory; the git server only operates on specified repos.

Python
import json
from pathlib import Path

mcp_config = {
    'mcpServers': {
        'filesystem': {
            'command': 'npx',
            'args': ['-y', '@anthropic/mcp-filesystem',
                     '--allowed-dir', '/home/user/projects',
                     '--read-only'],
            'env': {}
        },
        'git': {
            'command': 'npx',
            'args': ['-y', '@anthropic/mcp-git',
                     '--repo-path', '/home/user/projects/my-repo',
                     '--allow-operations', 'status,log,diff,branch'],
            'env': {}
        }
    }
}

Path('.mcp.json').write_text(json.dumps(mcp_config, indent=2))
print('Filesystem MCP: read-only, sandboxed to /home/user/projects')
print('Git MCP: status, log, diff, branch only (no push, no force)')

Step 2: Build path validation middleware

Add a validation layer that rejects any path traversal attempts before they reach the filesystem MCP server.

Python
import os

ALLOWED_ROOT = '/home/user/projects'
BLOCKED_PATTERNS = ['.env', '.ssh', 'credentials', 'secrets', '.git/config']

def validate_path(requested_path: str) -> dict:
    resolved = os.path.realpath(requested_path)
    # Check sandbox
    if not resolved.startswith(ALLOWED_ROOT):
        return {'allowed': False, 'reason': f'Path escapes sandbox: {resolved}'}
    # Check blocked patterns
    for pattern in BLOCKED_PATTERNS:
        if pattern in resolved.lower():
            return {'allowed': False, 'reason': f'Blocked pattern: {pattern}'}
    # Check symlink attacks
    if os.path.islink(requested_path):
        target = os.readlink(requested_path)
        if not os.path.realpath(target).startswith(ALLOWED_ROOT):
            return {'allowed': False, 'reason': f'Symlink escapes sandbox'}
    return {'allowed': True, 'resolved': resolved}

# Test cases
test_paths = [
    '/home/user/projects/src/main.py',      # allowed
    '/home/user/projects/../.ssh/id_rsa',    # blocked
    '/home/user/projects/.env',              # blocked
    '/etc/passwd',                           # blocked
]

for p in test_paths:
    result = validate_path(p)
    status = 'ALLOWED' if result['allowed'] else f'BLOCKED ({result["reason"]})'
    print(f'  {p}: {status}')

Step 3: Add search-grounded code assistance

When the agent needs to generate or modify code, search for current documentation first to avoid hallucinating outdated APIs.

Python
import requests

SCAVIO_KEY = os.environ['SCAVIO_API_KEY']

def ground_code_generation(language: str, task: str) -> str:
    """Search for current docs before generating code."""
    resp = requests.post('https://api.scavio.dev/api/v1/search',
        headers={'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json'},
        json={'query': f'{language} {task} documentation 2026',
              'country_code': 'us', 'num_results': 3})
    results = resp.json().get('organic_results', [])
    context = '\n'.join(f'- {r["title"]}: {r.get("snippet", "")}' for r in results)
    return context

# Example: before generating Next.js code, check current API
context = ground_code_generation('Next.js', 'app router server components')
print('Grounding context for code generation:')
print(context)
print(f'\nCost: $0.005')

Python Example

Python
import os, json, requests
from pathlib import Path

ALLOWED_ROOT = '/home/user/projects'
SCAVIO_KEY = os.environ['SCAVIO_API_KEY']

def validate_path(path):
    resolved = os.path.realpath(path)
    if not resolved.startswith(ALLOWED_ROOT):
        return False
    blocked = ['.env', '.ssh', 'credentials', 'secrets']
    return not any(b in resolved.lower() for b in blocked)

def search_docs(query):
    resp = requests.post('https://api.scavio.dev/api/v1/search',
        headers={'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json'},
        json={'query': query, 'country_code': 'us', 'num_results': 3})
    return [r.get('snippet', '') for r in resp.json().get('organic_results', [])]

print(validate_path('/home/user/projects/src/main.py'))  # True
print(validate_path('/etc/passwd'))  # False
print(search_docs('python pathlib best practices 2026')[0][:80])

JavaScript Example

JavaScript
const ALLOWED_ROOT = '/home/user/projects';
const SCAVIO_KEY = process.env.SCAVIO_API_KEY;
const path = require('path');

function validatePath(p) {
  const resolved = path.resolve(p);
  if (!resolved.startsWith(ALLOWED_ROOT)) return false;
  const blocked = ['.env', '.ssh', 'credentials'];
  return !blocked.some(b => resolved.toLowerCase().includes(b));
}

async function searchDocs(query) {
  const resp = await fetch('https://api.scavio.dev/api/v1/search', {
    method: 'POST',
    headers: { 'x-api-key': SCAVIO_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, country_code: 'us', num_results: 3 })
  });
  return (await resp.json()).organic_results?.map(r => r.snippet) || [];
}

console.log(validatePath('/home/user/projects/src/main.py')); // true
console.log(validatePath('/etc/passwd')); // false

Expected Output

JSON
  /home/user/projects/src/main.py: ALLOWED
  /home/user/projects/../.ssh/id_rsa: BLOCKED (Path escapes sandbox)
  /home/user/projects/.env: BLOCKED (Blocked pattern: .env)
  /etc/passwd: BLOCKED (Path escapes sandbox)

Grounding context for code generation:
- Next.js App Router Docs: Server Components run on the server...
- Vercel Blog: Next.js 16 App Router updates...
Cost: $0.005

Related Tutorials

Frequently Asked Questions

Most developers complete this tutorial in 15 to 30 minutes. You will need a Scavio API key (free tier works) and a working Python or JavaScript environment.

Claude Code installed. Node.js 18+ installed. A git repository to work with. A Scavio API key for search grounding. A Scavio API key gives you 250 free credits per month.

Yes. The free tier includes 250 credits per month, which is more than enough to complete this tutorial and prototype a working solution.

Scavio has a native LangChain package (langchain-scavio), an MCP server, and a plain REST API that works with any HTTP client. This tutorial uses the raw REST API, but you can adapt to your framework of choice.

Start Building

Build an MCP agent that safely reads files and manages git repos. Includes sandboxing, path validation, and search-grounded code generation.