Connect AI to Any Local Tool with MCP in 20 Minutes

Build AI agents that access your filesystem, databases, and APIs using Model Context Protocol. Step-by-step guide with working examples.

Problem: Your AI Can't Touch Your Local Files or Tools

You're building with Claude or other LLMs, but they can't read your project files, query your database, or call your internal APIs without copying everything into prompts.

You'll learn:

  • What MCP is and why it beats manual context injection
  • How to connect Claude to your filesystem in under 5 minutes
  • Building custom MCP servers for databases and APIs
  • Production deployment patterns developers actually use

Time: 20 min | Level: Intermediate


Why This Happens

LLMs run in isolated environments with no filesystem or network access. Model Context Protocol (MCP) is Anthropic's open standard that lets AI models securely call tools on your machine through a client-server architecture.

Common symptoms:

  • Copying file contents into every prompt
  • Can't query databases without manual exports
  • No way to trigger local scripts or APIs
  • Context windows fill up with repetitive data

What MCP solves: Gives models on-demand access to local resources through standardized tool interfaces, without sending everything upfront.


How MCP Works

MCP uses three components that talk over stdio or HTTP:

┌─────────────┠     ┌─────────────┠     ┌──────────────┐
│   Client    │────▶│ MCP Server  │────▶│ Local Tool   │
│ (Claude.ai) │◀────│  (Python)   │◀────│ (Filesystem) │
└─────────────┘     └─────────────┘     └──────────────┘

Client: AI application (Claude.ai, custom apps) that needs data
Server: Lightweight process exposing tools via MCP protocol
Tools: Your actual resources (files, databases, APIs)

The server translates AI requests into function calls, returns results in a structured format.


Solution

Step 1: Install the MCP Inspector

The official debugging tool shows you exactly what your server exposes:

npm install -g @modelcontextprotocol/inspector

Expected: Installs mcp-inspector command globally

Why this first: You'll use it to test servers before connecting to Claude


Step 2: Create a Filesystem Server

Build a server that lets AI read/write files in a specific directory:

# mcp_filesystem_server.py
import asyncio
import json
from pathlib import Path
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

# Only allow access to this directory
ALLOWED_DIR = Path.home() / "mcp-workspace"
ALLOWED_DIR.mkdir(exist_ok=True)

app = Server("filesystem-server")

@app.list_tools()
async def list_tools() -> list[Tool]:
    """Expose available file operations"""
    return [
        Tool(
            name="read_file",
            description="Read contents of a file in workspace",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "Relative file path"}
                },
                "required": ["path"]
            }
        ),
        Tool(
            name="write_file",
            description="Write content to a file in workspace",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {"type": "string"},
                    "content": {"type": "string"}
                },
                "required": ["path", "content"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict):
    """Execute the requested file operation"""
    path = ALLOWED_DIR / arguments["path"]
    
    # Security: prevent path traversal
    if not path.resolve().is_relative_to(ALLOWED_DIR):
        raise ValueError("Path outside workspace")
    
    if name == "read_file":
        content = path.read_text()
        return [TextContent(type="text", text=content)]
    
    elif name == "write_file":
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(arguments["content"])
        return [TextContent(type="text", text=f"Wrote {len(arguments['content'])} chars")]

async def main():
    async with stdio_server() as (read, write):
        await app.run(read, write, app.create_initialization_options())

if __name__ == "__main__":
    asyncio.run(main())

Why this works: The @app.list_tools() decorator tells clients what tools exist. @app.call_tool() executes them. Security enforced by path validation.

Install dependencies:

pip install mcp --break-system-packages

If it fails:

  • Error: "No module named 'mcp'": Use pip3 instead of pip
  • Permission denied: Add sudo on Linux/Mac

Step 3: Test with MCP Inspector

Run the inspector to verify your server:

mcp-inspector python mcp_filesystem_server.py

Expected: Browser opens to http://localhost:5173 showing your two tools

Try the read_file tool in the inspector:

  1. Click "read_file"
  2. Enter {"path": "test.txt"}
  3. If file doesn't exist, use write_file first

You should see: Tool responses in the inspector UI proving the server works


Step 4: Connect to Claude.ai

Add your server to Claude's config file:

On macOS/Linux:

mkdir -p ~/Library/Application\ Support/Claude
nano ~/Library/Application\ Support/Claude/claude_desktop_config.json

On Windows:

mkdir "$env:APPDATA\Claude"
notepad "$env:APPDATA\Claude\claude_desktop_config.json"

Add this configuration:

{
  "mcpServers": {
    "filesystem": {
      "command": "python",
      "args": ["/absolute/path/to/mcp_filesystem_server.py"]
    }
  }
}

Critical: Use absolute paths. ~ won't expand. Find it with pwd in the server directory.

Restart Claude desktop app completely (Cmd+Q on Mac, close from system tray on Windows).


Step 5: Verify in Claude

Open a new conversation and type:

"What files are in my MCP workspace?"

Claude should use the read_file tool (you'll see a tool use indicator).

If it fails:

  • No tool access: Check Claude desktop logs at ~/Library/Logs/Claude (Mac) or %APPDATA%\Claude\logs (Windows)
  • Server crashes: Run python mcp_filesystem_server.py directly to see errors
  • Wrong Python: Specify full path like "command": "/usr/bin/python3"

Real-World MCP Servers

Database Query Server

Connect Claude to PostgreSQL:

# mcp_database_server.py
import asyncio
import psycopg2
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

app = Server("database-server")

DB_CONFIG = {
    "host": "localhost",
    "database": "myapp",
    "user": "readonly_user",  # Use read-only credentials
    "password": "safe_password"
}

@app.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="query_db",
            description="Execute SQL SELECT query (read-only)",
            inputSchema={
                "type": "object",
                "properties": {
                    "sql": {"type": "string", "description": "SELECT query"}
                },
                "required": ["sql"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict):
    # Security: block dangerous keywords
    sql = arguments["sql"].upper()
    if any(word in sql for word in ["DROP", "DELETE", "UPDATE", "INSERT"]):
        raise ValueError("Only SELECT queries allowed")
    
    conn = psycopg2.connect(**DB_CONFIG)
    cursor = conn.cursor()
    cursor.execute(arguments["sql"])
    results = cursor.fetchall()
    cursor.close()
    conn.close()
    
    # Format as markdown table
    if results:
        headers = [desc[0] for desc in cursor.description]
        table = "| " + " | ".join(headers) + " |\n"
        table += "|" + "|".join(["---"] * len(headers)) + "|\n"
        for row in results:
            table += "| " + " | ".join(str(val) for val in row) + " |\n"
        return [TextContent(type="text", text=table)]
    
    return [TextContent(type="text", text="No results")]

async def main():
    async with stdio_server() as (read, write):
        await app.run(read, write, app.create_initialization_options())

if __name__ == "__main__":
    asyncio.run(main())

Security notes: Always use read-only database users. Validate queries before execution. Never expose production write access.


API Integration Server

Let Claude call your internal APIs:

# mcp_api_server.py
import asyncio
import httpx
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

app = Server("api-server")

API_BASE = "https://api.internal.company.com"
API_KEY = "your-key-here"  # Better: load from environment

@app.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="get_user",
            description="Fetch user details by ID",
            inputSchema={
                "type": "object",
                "properties": {
                    "user_id": {"type": "string"}
                },
                "required": ["user_id"]
            }
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict):
    async with httpx.AsyncClient() as client:
        response = await client.get(
            f"{API_BASE}/users/{arguments['user_id']}",
            headers={"Authorization": f"Bearer {API_KEY}"}
        )
        response.raise_for_status()
        return [TextContent(type="text", text=response.text)]

async def main():
    async with stdio_server() as (read, write):
        await app.run(read, write, app.create_initialization_options())

if __name__ == "__main__":
    asyncio.run(main())

Install httpx for async requests:

pip install httpx --break-system-packages

Production Patterns

Environment-Based Configuration

Never hardcode credentials:

import os
from pathlib import Path

# Load from .env file
from dotenv import load_dotenv
load_dotenv()

DB_CONFIG = {
    "host": os.getenv("DB_HOST", "localhost"),
    "database": os.getenv("DB_NAME"),
    "user": os.getenv("DB_USER"),
    "password": os.getenv("DB_PASSWORD")
}

# Validate required vars
if not all([DB_CONFIG["database"], DB_CONFIG["user"]]):
    raise ValueError("Missing required environment variables")

Create .env:

DB_HOST=localhost
DB_NAME=myapp
DB_USER=readonly_user
DB_PASSWORD=safe_password

Install dependency:

pip install python-dotenv --break-system-packages

Logging and Debugging

Add structured logging to servers:

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('mcp_server.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

@app.call_tool()
async def call_tool(name: str, arguments: dict):
    logger.info(f"Tool called: {name} with args: {arguments}")
    try:
        # ... tool logic ...
        logger.info(f"Tool {name} completed successfully")
    except Exception as e:
        logger.error(f"Tool {name} failed: {e}")
        raise

Check logs: tail -f mcp_server.log while Claude uses the server


Multi-Server Configuration

Run multiple servers for different capabilities:

{
  "mcpServers": {
    "filesystem": {
      "command": "python",
      "args": ["/path/to/mcp_filesystem_server.py"]
    },
    "database": {
      "command": "python",
      "args": ["/path/to/mcp_database_server.py"],
      "env": {
        "DB_HOST": "localhost"
      }
    },
    "api": {
      "command": "python",
      "args": ["/path/to/mcp_api_server.py"]
    }
  }
}

Claude can now use tools from all three servers simultaneously.


Verification

Test end-to-end workflow:

  1. Start inspector:

    mcp-inspector python mcp_filesystem_server.py
    

    Confirm tools appear

  2. Test in Claude:

    "Create a file called notes.txt with content: Hello MCP"

    You should see: Claude uses write_file tool, confirms creation

  3. Verify locally:

    cat ~/mcp-workspace/notes.txt
    

    Expected: File exists with "Hello MCP" content

  4. Read back:

    "What's in notes.txt?"

    You should see: Claude uses read_file, shows content


Common Issues

Server Won't Start

Symptom: Claude shows "MCP server connection failed"

Debug:

# Run server directly to see errors
python mcp_filesystem_server.py

# Check Python path
which python

Fix: Update claude_desktop_config.json with correct Python path


Tools Not Appearing

Symptom: Claude doesn't see your tools

Debug:

  1. Check server implements @app.list_tools() decorator
  2. Verify schema has required name, description, inputSchema fields
  3. Test with MCP inspector before Claude

Permission Errors

Symptom: "Permission denied" when accessing files/database

Fix:

  • Files: Check ALLOWED_DIR permissions with ls -la
  • Database: Verify user has SELECT grants with SHOW GRANTS
  • API: Confirm token hasn't expired

What You Learned

  • MCP enables AI to call local tools without copying data into prompts
  • Servers expose tools via @app.list_tools() and execute via @app.call_tool()
  • Security is critical: validate inputs, use read-only credentials, restrict paths
  • Production servers need logging, environment configs, and error handling

Limitations:

  • Servers must be running when Claude starts (no auto-restart yet)
  • stdio transport means one server per process
  • No built-in authentication beyond what you implement

When NOT to use MCP:

  • Public APIs better accessed via Claude's built-in web tools
  • One-off tasks where copying is faster than server setup
  • Sensitive data that shouldn't be AI-accessible at all

Advanced: HTTP Transport

For production deployments, use HTTP instead of stdio:

# mcp_http_server.py
from mcp.server import Server
from mcp.server.sse import sse_server

app = Server("http-filesystem-server")

# ... same tool definitions ...

async def main():
    # Runs on http://localhost:8000/sse
    await sse_server(app, host="0.0.0.0", port=8000)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(main(), host="0.0.0.0", port=8000)

Configure Claude to use HTTP endpoint:

{
  "mcpServers": {
    "filesystem": {
      "transport": "sse",
      "url": "http://localhost:8000/sse"
    }
  }
}

Benefits: Server runs independently, supports multiple clients, easier monitoring

Dependencies:

pip install uvicorn --break-system-packages

Architecture Diagram

Architecture Diagram

Tested on Python 3.11+, MCP SDK 0.9.0, Claude Desktop 1.2.0, macOS Sonoma & Ubuntu 22.04