Vibe coding falls apart when your AI agent doesn't understand your project's real structure. Default tools only see files you open. A Model Context Protocol (MCP) server fixes this by letting you expose custom, read-only views of your codebase to any MCP-compatible coding agent (Claude Desktop, Cursor, VS Code Copilot, etc.). In this tutorial, you'll build a minimal MCP server that gives an agent three superpowers:
- find_symbol - locate any function, class, or constant definition
- get_file_tree - see a colored, git-ignored tree of your repo
- run_tests - run a single test file or a skipped test
Step 1: Scaffold the MCP Server Project
Create a new folder for your MCP server. You'll build it independently from your main codebase so it stays lightweight.
mkdir my-codebase-mcp && cd my-codebase-mcp
python -m venv .venv
source .venv/bin/activate
pip install mcp uvicorn fastapiFastMCP is the easiest way to expose Python functions as MCP tools. We'll use it with the HTTP transport (SSE) for easy integration with modern clients.
# server.py
from mcp.server.fastmcp import FastMCP
from pathlib import Path
import os
# Point this to your project root!
PROJECT_ROOT = Path("/path/to/your/actual/codebase").resolve()
mcp = FastMCP(
"codebase-context",
host="127.0.0.1",
port=8888,
transport="sse"
)Make sure PROJECT_ROOT is an absolute path. Later we'll make this configurable via environment variable.
Step 2: Add a Symbol Finder Tool
The most useful tool is a quick grep-based symbol locator. Instead of asking the agent to guess file names, you give it a tool that returns exact locations.
import subprocess
@mcp.tool()
def find_symbol(name: str) -> str:
"""Find exact definitions of a function, class, or variable in the project.
Returns file paths and line numbers.
"""
if name.strip() == "":
return "Error: symbol name cannot be empty."
# Use grep -rn for a fast, language-agnostic search
result = subprocess.run(
["grep", "-rn", f"(def |class |const |func )*?{name}", "."],
cwd=PROJECT_ROOT,
capture_output=True,
text=True
)
if result.returncode != 0:
return f"No symbol '{name}' found."
# Limit output length to avoid flooding the model context
lines = result.stdout.strip().split("\n")[:30]
return "\n".join(lines) if lines else f"No symbol '{name}' found."Notice the grep pattern is a rough approximation. For a real project, you can improve this using ctags or a language server, but grep is surprisingly effective for many codebases.
127.0.0.1 and never expose this server publicly.Step 3: Add File Tree and Code Reader Tools
Agents need to understand the overall layout and then peek into files without you opening them manually.
import json
import os
@mcp.tool()
def get_file_tree(max_depth: int = 3) -> str:
"""Return a JSON representation of the project directory tree,
respecting .gitignore and skipping common heavy dirs."""
tree = []
PROJECT_ROOT.resolve()
# Basic walk with skip logic
for root, dirs, files in os.walk(PROJECT_ROOT):
# Skip hidden and heavy directories
dirs[:] = [d for d in dirs if not d.startswith(".") and d not in ("node_modules", "__pycache__", ".venv", "dist", "build")]
depth = root[len(str(PROJECT_ROOT)):].count(os.sep)
if depth >= max_depth:
dirs[:] = [] # don't go deeper
continue
tree.append({
"path": os.path.relpath(root, PROJECT_ROOT),
"files": [f for f in files if not f.startswith(".")]
})
return json.dumps(tree, indent=2)
@mcp.tool()
def read_file(relative_path: str, lines: int = 100) -> str:
"""Read a file from the codebase, showing only the first `lines` lines.
relative_path must be relative to project root.
"""
# Prevent path traversal
full_path = (PROJECT_ROOT / relative_path).resolve()
if not full_path.is_file():
return f"File not found: {relative_path}"
# Read first N lines
with open(full_path, "r", encoding="utf-8", errors="replace") as f:
content = f.readlines()
return "".join(content[:lines])The path traversal guard is critical. We resolve the absolute path and verify it's actually a file. This prevents the agent (or a malicious prompt) from asking for ../../../.ssh/id_rsa.
python server.pyThen in another terminal:
which mcp-client # if unavailable: pip install mcp-client
mcp-client --url http://127.0.0.1:8888/sseType tools to list the tools, then call find_symbol with a function name from your actual repo.
Step 4: Run Tests on Request
One of the biggest wins is letting your agent run the exact test file it's editing. With MCP, it can do this without you switching windows.
@mcp.tool()
def run_tests(test_path: str = "") -> str:
"""Run unit tests. Give a relative path to a test file or directory.
If empty, runs everything."""
test_path = test_path.strip()
command = "python -m pytest"
if test_path:
full_test = (PROJECT_ROOT / test_path)
if not full_test.exists():
return f"Test path does not exist: {test_path}"
command += f" {test_path}"
result = subprocess.run(
command.split() + ["--maxfail=3"],
cwd=PROJECT_ROOT,
capture_output=True,
text=True,
timeout=120
)
if result.returncode == 0:
return f"All tests passed!\n{result.stdout[-2000:]}"
else:
return f"Tests failed. stdout:\n{result.stdout[-2000:]}\nstderr:\n{result.stderr[-2000:]}"Always set a timeout. AI agents can accidentally trigger infinite test loops. The --maxfail=3 mode keeps output small enough for context windows.
Step 5: Make Your Project Root Dynamic
You don't want to hardcode a path. Let the server read an environment variable.
import os
PROJECT_ROOT = Path(os.environ.get("CODEBASE_PATH", ".")).resolve()
if __name__ == "__main__":
# Validate the path early
if not PROJECT_ROOT.is_dir():
raise SystemExit(f"Invalid CODEBASE_PATH: {PROJECT_ROOT}")
print(f"MCP serving {PROJECT_ROOT}")
mcp.run()Now start it from anywhere:
export CODEBASE_PATH=/path/to/your/project
python server.pyStep 6: Connect to Your AI Coding Agent
For Claude Desktop, add an MCP server entry to its config file:
# claude_desktop_config.json
{
"mcpServers": {
"codebase": {
"command": "python",
"args": ["/absolute/path/to/my-codebase-mcp/server.py"],
"env": {
"CODEBASE_PATH": "/absolute/path/to/your/actual/project"
}
}
}
}In Cursor, use the MCP panel to add a new server and select the SSE type (if you're running the HTTP transport).
Now Use It
Restart your AI client. Ask a question like:
"Use find_symbol to find the function that validates emails, read the file, and then run the tests for that file."The agent will naturally chain your custom tools together. Because the tools are read-only and return compact results, you'll see fewer hallucinated file paths and less context bloat.
Best Next Steps
- Add a
search_commit_historytool that shows recent git log entries. - Use
ctagsinstead of grep for more accurate symbol locations. - Wrap the server in Docker if you have a complex monorepo with many services.
Your AI companion now has the same head start you'd give a human colleague: a clear map of the codebase and a safe way to run checks. That's the difference between vague guessing and grounded, actionable edits.
Comments
No comments yet
Connect with Google to comment or reply.
Connect with Google