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.
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.
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.
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
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
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')); // falseExpected Output
/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