MCP (Model Context Protocol) is an open standard that lets AI agents interact with external tools. By building a custom MCP server for your codebase, you can give AI assistants like Claude or Cursor the ability to read files, search code, and retrieve documentation – all without manual copying. This tutorial walks you through creating a lightweight MCP server using Python.
What We'll Build
We'll create an MCP server with two tools:
- read_file – reads the content of a given file path
- search_code – finds files matching a regex pattern in the project
Both will be constrained to the project directory for security.
Step 1: Set Up the Project
Create a new directory and initialize a Python project with the MCP SDK:
mkdir my-mcp-server
cd my-mcp-server
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install mcp
Step 2: Write the Server
Create server.py with the following code. We'll use the mcp library to define tools.
import os
import re
from pathlib import Path
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationOptions
# Restrict to a project directory – change this to your actual project path
PROJECT_ROOT = Path("/path/to/your/project").resolve()
app = Server("codebase-reader")
@app.list_tools()
async def list_tools():
return [
{
"name": "read_file",
"description": "Read the contents of a file relative to the project root",
"inputSchema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Relative file path (e.g., src/main.py)"}
},
"required": ["path"]
}
},
{
"name": "search_code",
"description": "Search for files matching a regex pattern in the project",
"inputSchema": {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "Regex pattern to match against file paths"}
},
"required": ["pattern"]
}
}
]
@app.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "read_file":
rel_path = arguments["path"]
full_path = (PROJECT_ROOT / rel_path).resolve()
# Prevent path traversal
if not str(full_path).startswith(str(PROJECT_ROOT)):
raise ValueError("Path outside project root")
if not full_path.exists():
raise FileNotFoundError(f"File not found: {rel_path}")
content = full_path.read_text(encoding="utf-8")
return {"content": content}
elif name == "search_code":
pattern = arguments["pattern"]
matching_files = [
str(p.relative_to(PROJECT_ROOT))
for p in PROJECT_ROOT.rglob("*")
if p.is_file() and re.search(pattern, str(p))
]
return {"files": matching_files[:50]} # limit to 50 results
else:
raise ValueError(f"Unknown tool: {name}")
if __name__ == "__main__":
app.run(transport="stdio")
PROJECT_ROOT to the actual path of your codebase. Also, the server uses stdio transport – it's the simplest for local MCP.Step 3: Test with MCP Inspector
Launch the MCP Inspector (a Node.js CLI tool) to verify:
npx @modelcontextprotocol/inspector python server.py
This opens a web UI where you can list tools and call them. Try read_file with a relative path like README.md.
Step 4: Connect to an AI Client
Configure your AI tool to use this MCP server. For example, in Claude Desktop:
- Open Settings → Developer → Edit Config
- Add to
mcpServersinclaude_desktop_config.json:
{
"mcpServers": {
"codebase-reader": {
"command": "python",
"args": ["/absolute/path/to/server.py"]
}
}
}
Restart Claude. Now you can ask Claude to “Read the main logging file” or “Find all test files” and it will use your server.
Security Note
This server only allows reading files, never writing. Always run MCP servers with minimal permissions. For production, consider adding authentication or limiting to specific file types.
Next Steps
You can extend the server with more tools:
- get_symbol_definition – parse Python/JS to extract function definitions
- list_directory – show contents of a folder
- run_command – execute shell commands (be careful!)
MCP is rapidly evolving. Check the official docs for the latest patterns.
Interesting approach, but I wonder about security implications of giving an AI agent direct filesystem access. Did you consider sandboxing the agent's read operations?