Claude + MCP: Enabling Local File Context in AI Workflows

Claude + MCP: Enabling Local File Context in AI Workflows

The Problem (The “Why”)

Claude can’t access your local files, codebase, or file system natively, making it impossible to analyze your actual project structure, debug real code, or provide context-aware suggestions without manually copy-pasting everything. You need AI that can read your .env files, scan directory structures, analyze multiple related files simultaneously, watch for changes, and execute local scripts—but implementing this securely is complex. Most developers resort to uploading files one-by-one (slow and incomplete context) or use third-party tools that expose their entire filesystem (security nightmare). The Model Context Protocol (MCP) solves this: it’s Anthropic’s standardized interface for connecting Claude to local tools, filesystems, databases, and APIs. The real challenge is implementing MCP servers that provide granular file access, respect .gitignore patterns, implement proper sandboxing, handle large codebases efficiently, and work seamlessly with Claude Desktop. This integration delivers production-ready MCP servers for filesystem access, Git operations, database queries, and custom tool execution—all with security boundaries.

Tech Stack & Prerequisites

  • Claude Desktop app (latest version)
  • Node.js v20+ and npm/pnpm
  • TypeScript 5+
  • @modelcontextprotocol/sdk v0.5+
  • chokidar v3+ for file watching
  • fast-glob v3+ for pattern matching
  • gray-matter v4+ for frontmatter parsing
  • Git installed locally
  • macOS, Windows, or Linux

Step-by-Step Implementation

Step 1: Setup

Install Claude Desktop app:

  • macOS: Download from claude.ai/download
  • Windows: Download from official site
  • Linux: Currently in beta

Create MCP server project:

bash
mkdir claude-mcp-filesystem
cd claude-mcp-filesystem
npm init -y
npm install @modelcontextprotocol/sdk chokidar fast-glob gray-matter dotenv
npm install -D typescript @types/node tsx

Initialize TypeScript:

bash
npx tsc --init

Update tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "node",
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Create project structure:

bash
mkdir -p src/servers src/tools src/utils
touch src/index.ts src/servers/filesystem.ts src/servers/git.ts src/tools/search.ts

Update package.json:

json
{
  "name": "claude-mcp-filesystem",
  "version": "1.0.0",
  "type": "module",
  "bin": {
    "mcp-filesystem": "./dist/index.js"
  },
  "scripts": {
    "build": "tsc && chmod +x dist/index.js",
    "dev": "tsx watch src/index.ts",
    "start": "node dist/index.js"
  }
}

Step 2: Configuration

2.1: Create MCP Server Configuration

Create src/config.ts:

typescript
// src/config.ts
import path from 'path';
import os from 'os';

export interface MCPConfig {
  allowedPaths: string[];
  deniedPaths: string[];
  maxFileSize: number;
  watchEnabled: boolean;
  gitEnabled: boolean;
}

export const defaultConfig: MCPConfig = {
  // Paths that MCP can access
  allowedPaths: [
    path.join(os.homedir(), 'Documents'),
    path.join(os.homedir(), 'Projects'),
    path.join(os.homedir(), 'Desktop'),
  ],
  
  // Paths to explicitly deny (sensitive directories)
  deniedPaths: [
    path.join(os.homedir(), '.ssh'),
    path.join(os.homedir(), '.aws'),
    path.join(os.homedir(), '.config'),
    '/etc',
    '/var',
    '/usr',
  ],
  
  // Max file size to read (10MB)
  maxFileSize: 10 * 1024 * 1024,
  
  // Enable file watching
  watchEnabled: true,
  
  // Enable Git operations
  gitEnabled: true,
};

/**
 * Patterns to ignore (respects .gitignore)
 */
export const ignorePatterns = [
  '**/node_modules/**',
  '**/.git/**',
  '**/.next/**',
  '**/dist/**',
  '**/build/**',
  '**/.DS_Store',
  '**/coverage/**',
  '**/*.log',
  '.env',
  '.env.*',
];

/**
 * Validate if path is allowed
 */
export function isPathAllowed(filePath: string, config: MCPConfig): boolean {
  const normalizedPath = path.normalize(filePath);
  
  // Check denied paths first
  if (config.deniedPaths.some(denied => normalizedPath.startsWith(denied))) {
    return false;
  }
  
  // Check allowed paths
  return config.allowedPaths.some(allowed => 
    normalizedPath.startsWith(allowed)
  );
}

2.2: Configure Claude Desktop

Create or edit Claude Desktop configuration:

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json

Windows: %APPDATA%\Claude\claude_desktop_config.json

Linux: ~/.config/Claude/claude_desktop_config.json

json
{
  "mcpServers": {
    "filesystem": {
      "command": "node",
      "args": ["/absolute/path/to/claude-mcp-filesystem/dist/index.js"],
      "env": {
        "ALLOWED_PATHS": "/Users/yourname/Projects,/Users/yourname/Documents"
      }
    }
  }
}

Step 3: Core Logic

3.1: Filesystem MCP Server

Create src/servers/filesystem.ts:

typescript
// src/servers/filesystem.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  Tool,
} from '@modelcontextprotocol/sdk/types.js';
import fs from 'fs/promises';
import path from 'path';
import { glob } from 'fast-glob';
import { defaultConfig, isPathAllowed, ignorePatterns } from '../config.js';

export class FilesystemServer {
  private server: Server;

  constructor() {
    this.server = new Server(
      {
        name: 'filesystem-server',
        version: '1.0.0',
      },
      {
        capabilities: {
          tools: {},
        },
      }
    );

    this.setupTools();
    this.setupHandlers();
  }

  private setupTools() {
    const tools: Tool[] = [
      {
        name: 'read_file',
        description: 'Read contents of a file from the local filesystem',
        inputSchema: {
          type: 'object',
          properties: {
            path: {
              type: 'string',
              description: 'Absolute or relative path to the file',
            },
          },
          required: ['path'],
        },
      },
      {
        name: 'write_file',
        description: 'Write content to a file on the local filesystem',
        inputSchema: {
          type: 'object',
          properties: {
            path: {
              type: 'string',
              description: 'Path to write the file',
            },
            content: {
              type: 'string',
              description: 'Content to write to the file',
            },
          },
          required: ['path', 'content'],
        },
      },
      {
        name: 'list_directory',
        description: 'List contents of a directory',
        inputSchema: {
          type: 'object',
          properties: {
            path: {
              type: 'string',
              description: 'Directory path to list',
            },
            recursive: {
              type: 'boolean',
              description: 'List recursively',
              default: false,
            },
          },
          required: ['path'],
        },
      },
      {
        name: 'search_files',
        description: 'Search for files matching a pattern',
        inputSchema: {
          type: 'object',
          properties: {
            pattern: {
              type: 'string',
              description: 'Glob pattern to match (e.g., "**/*.ts")',
            },
            directory: {
              type: 'string',
              description: 'Directory to search in',
            },
          },
          required: ['pattern', 'directory'],
        },
      },
      {
        name: 'get_file_info',
        description: 'Get metadata about a file or directory',
        inputSchema: {
          type: 'object',
          properties: {
            path: {
              type: 'string',
              description: 'Path to get info about',
            },
          },
          required: ['path'],
        },
      },
    ];

    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools,
    }));
  }

  private setupHandlers() {
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;

      try {
        switch (name) {
          case 'read_file':
            return await this.readFile(args.path as string);

          case 'write_file':
            return await this.writeFile(
              args.path as string,
              args.content as string
            );

          case 'list_directory':
            return await this.listDirectory(
              args.path as string,
              args.recursive as boolean
            );

          case 'search_files':
            return await this.searchFiles(
              args.pattern as string,
              args.directory as string
            );

          case 'get_file_info':
            return await this.getFileInfo(args.path as string);

          default:
            throw new Error(`Unknown tool: ${name}`);
        }
      } catch (error: any) {
        return {
          content: [
            {
              type: 'text',
              text: `Error: ${error.message}`,
            },
          ],
          isError: true,
        };
      }
    });
  }

  /**
   * Read file contents
   */
  private async readFile(filePath: string) {
    const absolutePath = path.resolve(filePath);

    if (!isPathAllowed(absolutePath, defaultConfig)) {
      throw new Error(`Access denied: ${filePath}`);
    }

    const stats = await fs.stat(absolutePath);

    if (stats.size > defaultConfig.maxFileSize) {
      throw new Error(
        `File too large: ${stats.size} bytes (max: ${defaultConfig.maxFileSize})`
      );
    }

    const content = await fs.readFile(absolutePath, 'utf-8');

    return {
      content: [
        {
          type: 'text',
          text: content,
        },
      ],
    };
  }

  /**
   * Write file contents
   */
  private async writeFile(filePath: string, content: string) {
    const absolutePath = path.resolve(filePath);

    if (!isPathAllowed(absolutePath, defaultConfig)) {
      throw new Error(`Access denied: ${filePath}`);
    }

    // Create directory if it doesn't exist
    await fs.mkdir(path.dirname(absolutePath), { recursive: true });

    await fs.writeFile(absolutePath, content, 'utf-8');

    return {
      content: [
        {
          type: 'text',
          text: `Successfully wrote to ${filePath}`,
        },
      ],
    };
  }

  /**
   * List directory contents
   */
  private async listDirectory(dirPath: string, recursive: boolean = false) {
    const absolutePath = path.resolve(dirPath);

    if (!isPathAllowed(absolutePath, defaultConfig)) {
      throw new Error(`Access denied: ${dirPath}`);
    }

    const pattern = recursive ? '**/*' : '*';
    const files = await glob(pattern, {
      cwd: absolutePath,
      ignore: ignorePatterns,
      onlyFiles: false,
      markDirectories: true,
    });

    const fileList = files.map((file) => ({
      name: file,
      type: file.endsWith('/') ? 'directory' : 'file',
    }));

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(fileList, null, 2),
        },
      ],
    };
  }

  /**
   * Search for files matching pattern
   */
  private async searchFiles(pattern: string, directory: string) {
    const absolutePath = path.resolve(directory);

    if (!isPathAllowed(absolutePath, defaultConfig)) {
      throw new Error(`Access denied: ${directory}`);
    }

    const files = await glob(pattern, {
      cwd: absolutePath,
      ignore: ignorePatterns,
      absolute: false,
    });

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(files, null, 2),
        },
      ],
    };
  }

  /**
   * Get file/directory info
   */
  private async getFileInfo(filePath: string) {
    const absolutePath = path.resolve(filePath);

    if (!isPathAllowed(absolutePath, defaultConfig)) {
      throw new Error(`Access denied: ${filePath}`);
    }

    const stats = await fs.stat(absolutePath);

    const info = {
      path: absolutePath,
      size: stats.size,
      created: stats.birthtime,
      modified: stats.mtime,
      isFile: stats.isFile(),
      isDirectory: stats.isDirectory(),
      permissions: stats.mode.toString(8),
    };

    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(info, null, 2),
        },
      ],
    };
  }

  async start() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error('Filesystem MCP server running on stdio');
  }
}

3.2: Git Operations Server

Create src/servers/git.ts:

typescript
// src/servers/git.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { exec } from 'child_process';
import { promisify } from 'util';
import path from 'path';
import { isPathAllowed, defaultConfig } from '../config.js';

const execAsync = promisify(exec);

export class GitServer {
  private server: Server;

  constructor() {
    this.server = new Server(
      {
        name: 'git-server',
        version: '1.0.0',
      },
      {
        capabilities: {
          tools: {},
        },
      }
    );

    this.setupTools();
    this.setupHandlers();
  }

  private setupTools() {
    const tools = [
      {
        name: 'git_status',
        description: 'Get Git repository status',
        inputSchema: {
          type: 'object',
          properties: {
            path: {
              type: 'string',
              description: 'Repository path',
            },
          },
          required: ['path'],
        },
      },
      {
        name: 'git_diff',
        description: 'Get Git diff for staged or unstaged changes',
        inputSchema: {
          type: 'object',
          properties: {
            path: {
              type: 'string',
              description: 'Repository path',
            },
            staged: {
              type: 'boolean',
              description: 'Show staged changes',
              default: false,
            },
          },
          required: ['path'],
        },
      },
      {
        name: 'git_log',
        description: 'Get Git commit history',
        inputSchema: {
          type: 'object',
          properties: {
            path: {
              type: 'string',
              description: 'Repository path',
            },
            limit: {
              type: 'number',
              description: 'Number of commits to show',
              default: 10,
            },
          },
          required: ['path'],
        },
      },
      {
        name: 'git_branch',
        description: 'List Git branches',
        inputSchema: {
          type: 'object',
          properties: {
            path: {
              type: 'string',
              description: 'Repository path',
            },
          },
          required: ['path'],
        },
      },
    ];

    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools,
    }));
  }

  private setupHandlers() {
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;

      try {
        switch (name) {
          case 'git_status':
            return await this.gitStatus(args.path as string);

          case 'git_diff':
            return await this.gitDiff(
              args.path as string,
              args.staged as boolean
            );

          case 'git_log':
            return await this.gitLog(
              args.path as string,
              args.limit as number
            );

          case 'git_branch':
            return await this.gitBranch(args.path as string);

          default:
            throw new Error(`Unknown tool: ${name}`);
        }
      } catch (error: any) {
        return {
          content: [
            {
              type: 'text',
              text: `Error: ${error.message}`,
            },
          ],
          isError: true,
        };
      }
    });
  }

  private async executeGit(repoPath: string, command: string) {
    const absolutePath = path.resolve(repoPath);

    if (!isPathAllowed(absolutePath, defaultConfig)) {
      throw new Error(`Access denied: ${repoPath}`);
    }

    const { stdout, stderr } = await execAsync(command, {
      cwd: absolutePath,
    });

    if (stderr && !stdout) {
      throw new Error(stderr);
    }

    return stdout;
  }

  private async gitStatus(repoPath: string) {
    const output = await this.executeGit(repoPath, 'git status --short');

    return {
      content: [
        {
          type: 'text',
          text: output || 'No changes',
        },
      ],
    };
  }

  private async gitDiff(repoPath: string, staged: boolean = false) {
    const command = staged ? 'git diff --cached' : 'git diff';
    const output = await this.executeGit(repoPath, command);

    return {
      content: [
        {
          type: 'text',
          text: output || 'No changes',
        },
      ],
    };
  }

  private async gitLog(repoPath: string, limit: number = 10) {
    const command = `git log --oneline -n ${limit}`;
    const output = await this.executeGit(repoPath, command);

    return {
      content: [
        {
          type: 'text',
          text: output,
        },
      ],
    };
  }

  private async gitBranch(repoPath: string) {
    const output = await this.executeGit(repoPath, 'git branch -a');

    return {
      content: [
        {
          type: 'text',
          text: output,
        },
      ],
    };
  }

  async start() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error('Git MCP server running on stdio');
  }
}

3.3: Main Entry Point

Create src/index.ts:

typescript
// src/index.ts
#!/usr/bin/env node

import { FilesystemServer } from './servers/filesystem.js';
import { GitServer } from './servers/git.js';

async function main() {
  const serverType = process.env.MCP_SERVER_TYPE || 'filesystem';

  try {
    if (serverType === 'filesystem') {
      const server = new FilesystemServer();
      await server.start();
    } else if (serverType === 'git') {
      const server = new GitServer();
      await server.start();
    } else {
      console.error(`Unknown server type: ${serverType}`);
      process.exit(1);
    }
  } catch (error) {
    console.error('Server error:', error);
    process.exit(1);
  }
}

main();

Step 4: Testing

4.1: Build and Install

Build the project:

bash
npm run build

Link globally for testing:

bash
npm link

Verify installation:

bash
which mcp-filesystem
```

#### 4.2: Test with Claude Desktop

1. **Restart Claude Desktop** after updating config
2. Open Claude Desktop
3. Check for MCP server connection in status bar
4. Test filesystem commands:
```
Can you read the file at ~/Projects/my-app/package.json?

Can you list all TypeScript files in ~/Projects/my-app?

What files are in ~/Documents?
```

Expected behavior: Claude should read and list files from allowed directories.

#### 4.3: Test Security Boundaries

Try accessing denied paths:
```
Can you read ~/.ssh/id_rsa?
```

Expected: Claude should respond with "Access denied" error.

Try accessing outside allowed paths:
```
Can you list files in /etc?
```

Expected: Should be denied.

#### 4.4: Test Git Operations
```
What's the Git status of ~/Projects/my-app?

Show me the last 5 commits in ~/Projects/my-app

What branches exist in this repository?
```

#### 4.5: Test File Search
```
Find all .env files in ~/Projects

Search for all React components (*.tsx) in ~/Projects/my-app/src

List all markdown files recursively in ~/Documents

4.6: Debug MCP Connection

Check Claude Desktop logs:

macOS: ~/Library/Logs/Claude/mcp*.log

bash
tail -f ~/Library/Logs/Claude/mcp-filesystem.log

Test server directly:

bash
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/index.js

Common Errors & Troubleshooting

1. Error: “MCP server not found” in Claude Desktop

Cause: Incorrect path in claude_desktop_config.json or server not built.

Fix: Use absolute paths and verify build:

json
{
  "mcpServers": {
    "filesystem": {
      "command": "node",
      "args": ["/Users/yourname/path/to/claude-mcp-filesystem/dist/index.js"],
      "env": {}
    }
  }
}

Verify path exists:

bash
ls -la /Users/yourname/path/to/claude-mcp-filesystem/dist/index.js

Check permissions:

bash
chmod +x /Users/yourname/path/to/claude-mcp-filesystem/dist/index.js

Alternative – use npx with global install:

bash
# Install globally
npm install -g .

# Update config
{
  "mcpServers": {
    "filesystem": {
      "command": "mcp-filesystem"
    }
  }
}

2. Error: “Access denied” for all paths

Cause: Allowed paths not configured correctly or environment variables not passed.

Fix: Verify environment variables in config:

json
{
  "mcpServers": {
    "filesystem": {
      "command": "node",
      "args": ["/path/to/dist/index.js"],
      "env": {
        "ALLOWED_PATHS": "/Users/yourname/Projects,/Users/yourname/Documents",
        "NODE_ENV": "production"
      }
    }
  }
}

Update config.ts to read from env:

typescript
// src/config.ts
export const defaultConfig: MCPConfig = {
  allowedPaths: process.env.ALLOWED_PATHS
    ? process.env.ALLOWED_PATHS.split(',').map(p => path.resolve(p.trim()))
    : [
        path.join(os.homedir(), 'Documents'),
        path.join(os.homedir(), 'Projects'),
      ],
  // ...rest of config
};

3. Error: “Server disconnected” or frequent reconnections

Cause: Server crashing due to unhandled errors or stdio buffer issues.

Fix: Implement proper error handling and logging:

typescript
// In src/index.ts
process.on('uncaughtException', (error) => {
  console.error('Uncaught exception:', error);
  // Don't exit - let MCP handle reconnection
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled rejection at:', promise, 'reason:', reason);
});

// Increase stdio buffer
process.stdin.setMaxListeners(20);
process.stdout.setMaxListeners(20);

Add reconnection logic in server:

typescript
// In FilesystemServer constructor
this.server.onerror = (error) => {
  console.error('Server error:', error);
  // Log but don't crash
};

Check Claude Desktop logs for crash details:

bash
# macOS
tail -100 ~/Library/Logs/Claude/mcp-filesystem.log

# Look for stack traces
grep -A 20 "Error" ~/Library/Logs/Claude/mcp-filesystem.log

Security Checklist

  • Implement path validation for all file operations
  • Use absolute paths to prevent directory traversal attacks
  • Respect .gitignore patterns to avoid exposing secrets
  • Set file size limits to prevent memory exhaustion
  • Deny access to system directories (.ssh, .aws, /etc)
  • Validate file types before reading (reject binaries if not expected)
  • Implement rate limiting on file operations
  • Log all file access for audit trails
  • Use read-only mode when possible
  • Sandbox execution of any code execution tools
  • Never expose credentials in file contents or logs
  • Implement timeout limits on long-running operations
  • Use separate MCP servers for different security contexts
  • Review allowed paths regularly and minimize scope
  • Monitor for suspicious patterns (rapid file access, large reads)
  • Implement user confirmation for write operations
  • Use content filtering to detect sensitive data exposure
  • Encrypt logs containing file paths or metadata
  • Rotate MCP server keys if implementing authentication
  • Document security boundaries clearly for users

Leave a Comment

Your email address will not be published. Required fields are marked *

banner
Scroll to Top