Spaces:
Running
Running
| """ | |
| mcp_server.py β FastMCP server exposing code-search capabilities via MCP protocol. | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| WHAT IS AN MCP SERVER? | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| An MCP server is a process that exposes capabilities (tools, data, prompts) | |
| to any MCP-compatible client. Instead of hardcoding tool logic inside the agent, | |
| we publish it here β and ANY client that speaks MCP can use it automatically. | |
| This is the same pattern Claude Code uses: it connects to MCP servers at startup, | |
| discovers their tools, and calls them during conversations. Our agent does the | |
| same thing, but WE built both sides. | |
| THREE MCP PRIMITIVES: | |
| Tools (model-controlled) | |
| βββββββββββββββββββββββββ | |
| Functions the LLM decides to call. The LLM reads the tool description and | |
| decides WHEN and HOW to use it. This is the core of agentic behavior. | |
| Examples: search_code, find_callers, get_file_chunk | |
| Resources (application-controlled) | |
| ββββββββββββββββββββββββββββββββββββ | |
| Read-only data the application exposes for the LLM to read as context. | |
| Identified by URI (like a URL). The client decides when to include them. | |
| Examples: qdrant://repos (list repos), qdrant://repos/karpathy/micrograd (browse) | |
| Prompts (user-controlled) | |
| βββββββββββββββββββββββββ | |
| Reusable prompt templates the USER explicitly invokes (like slash commands). | |
| They produce structured messages to inject into the conversation. | |
| Examples: /analyze_repo, /explain_function | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| TRANSPORT: STREAMABLE HTTP | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| We use the Streamable HTTP transport (the production-grade MCP transport): | |
| - Client sends JSON-RPC 2.0 messages as HTTP POST to /mcp | |
| - Server responds with JSON (simple) or SSE stream (long operations) | |
| - Session management via Mcp-Session-Id header | |
| vs Stdio transport (for local CLI tools): | |
| - Client spawns server as subprocess | |
| - Communication over stdin/stdout | |
| - Simpler but can't serve multiple clients | |
| We use HTTP because our server is already a FastAPI process and we want | |
| multiple clients to be able to connect simultaneously. | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MOUNTING IN FASTAPI | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| FastMCP.streamable_http_app() returns a Starlette ASGI app. | |
| We mount it inside our existing FastAPI app: | |
| app.mount("/mcp", mcp.streamable_http_app()) | |
| Now the MCP endpoint lives at http://localhost:8000/mcp alongside all | |
| the REST endpoints (/ingest, /query, /agent/stream, etc.). | |
| One process, two protocols. | |
| """ | |
| import sys | |
| from pathlib import Path | |
| from typing import Optional | |
| import requests as http_requests | |
| from mcp.server.fastmcp import FastMCP | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| from backend.config import settings | |
| # ββ Service holders ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # These are module-level singletons set during FastAPI lifespan (see main.py). | |
| # | |
| # Why not inject via constructor? | |
| # FastMCP tools are registered at import time with @mcp.tool(). | |
| # The decorated functions close over these module-level variables. | |
| # Python closures capture by reference β when a tool is called, it reads the | |
| # CURRENT value of _retrieval (set during lifespan), not the None at import time. | |
| # | |
| # This is the same pattern used in main.py for _ingestion_service etc. | |
| _retrieval = None # RetrievalService β set by init_services() | |
| _store = None # QdrantStore β set by init_services() | |
| # ββ Session working memory βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Two-layer memory: in-process dict for fast same-session access, Qdrant for | |
| # cross-session persistence. | |
| # | |
| # On each new query, clear_notes(repo) is called: | |
| # 1. Clears the in-memory dict | |
| # 2. Loads any previously persisted notes for this repo from Qdrant | |
| # so the agent can recall facts from prior sessions immediately | |
| # | |
| # note() writes to both layers. recall_notes() reads only the in-memory dict | |
| # (already warm from Qdrant load) β no per-call Qdrant round-trip. | |
| # | |
| # _current_repo tracks which repo's notes are loaded so note() knows where | |
| # to persist without changing the tool's public interface. | |
| _session_notes: dict[str, str] = {} | |
| _current_repo: str | None = None | |
| def clear_notes(repo: str | None = None) -> None: | |
| """ | |
| Reset working memory and pre-load persisted notes for the given repo. | |
| Called by AgentService.stream() at the start of every new query. | |
| If repo is provided and a QdrantStore is available, existing notes | |
| for that repo are loaded so the agent has cross-session recall. | |
| """ | |
| global _current_repo | |
| _session_notes.clear() | |
| _current_repo = repo | |
| # Pre-load persisted notes from Qdrant β agent can use recall_notes() | |
| # immediately without any extra round-trips. | |
| if repo and _store is not None: | |
| try: | |
| persisted = _store.load_notes(repo) | |
| _session_notes.update(persisted) | |
| except Exception as e: | |
| # Non-fatal: degraded to in-memory only if Qdrant is unavailable | |
| print(f"[notes] Could not load persisted notes for {repo}: {e}") | |
| def init_services(retrieval_service, qdrant_store): | |
| """ | |
| Inject shared service instances into the MCP server. | |
| Called from main.py lifespan after the embedding model has loaded | |
| and Qdrant connection is established. This avoids loading the | |
| 600MB model twice (once in FastAPI, once in the MCP server). | |
| """ | |
| global _retrieval, _store | |
| _retrieval = retrieval_service | |
| _store = qdrant_store | |
| print("MCP server: services injected.") | |
| # ββ FastMCP instance βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # FastMCP is the high-level Python SDK for building MCP servers. | |
| # It handles: JSON-RPC 2.0 protocol, tool/resource/prompt registration, | |
| # transport setup, session management, and capability negotiation. | |
| # | |
| # streamable_http_path="/" means the MCP endpoint within the sub-app is at "/" | |
| # So when mounted at "/mcp" in FastAPI, the full URL is: /mcp (not /mcp/mcp) | |
| mcp = FastMCP( | |
| name="cartographer", | |
| instructions=( | |
| "Code search and navigation server for indexed GitHub repositories. " | |
| "Each tool description specifies exactly when to use it over similar tools β " | |
| "read those descriptions to make the right tool choice." | |
| ), | |
| streamable_http_path="/", | |
| # stateless_http=True: each request is independent β no persistent sessions. | |
| # Required when mounting inside FastAPI (app.mount), because the sub-app's | |
| # lifespan doesn't automatically run the session manager task group. | |
| # Our MCPClient uses one connection per call anyway, so stateless is correct. | |
| stateless_http=True, | |
| ) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # TOOLS β functions the LLM calls to take action | |
| # | |
| # The description is what the LLM reads to decide WHEN to call the tool. | |
| # Be precise: what it does, when to use it, and when NOT to use it. | |
| # Type annotations in the function signature become the tool's JSON Schema. | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def search_code( | |
| query: str, | |
| repo: Optional[str] = None, | |
| mode: str = "hybrid", | |
| ) -> str: | |
| """ | |
| Search indexed repositories for code relevant to a natural-language query. | |
| Uses hybrid BM25 + semantic search with RRF fusion β finds code by concept | |
| (semantic) AND by exact identifier (keyword) in one call. | |
| Returns ranked chunks with file paths, function names, and line numbers. | |
| PREFER THIS OVER search_symbol when: | |
| - You have a concept, not a name ("how does the loss function backpropagate?") | |
| - You want semantically related code, not just exact name matches | |
| PREFER search_symbol OVER THIS when: | |
| - You already know the exact function/class name from a prior result | |
| - You want the definition, not callers or related code | |
| MODE GUIDE: | |
| - 'hybrid' (default) β best for most queries; combines semantic + keyword | |
| - 'semantic' β use for conceptual questions ("how does gradient accumulation work?") | |
| - 'keyword' β use for exact identifiers ("BackwardFunction", "_relu", "ValueError") | |
| PATTERN: call 2-3 searches with different angles in ONE turn β they run in parallel. | |
| Example: search_code("forward pass") + search_code("loss computation") together. | |
| Args: | |
| query: Concept or identifier to search for (natural language or code name) | |
| repo: Optional 'owner/repo' filter (e.g. 'karpathy/micrograd') | |
| mode: 'hybrid' | 'semantic' | 'keyword' | |
| """ | |
| if _retrieval is None: | |
| return "Search service not ready β backend still initializing." | |
| results = _retrieval.search( | |
| query=query, | |
| top_k=12, | |
| repo_filter=repo, | |
| mode=mode, | |
| ) | |
| if not results: | |
| return f"No results found for: '{query}'" | |
| return _retrieval.format_context(results) | |
| def find_callers(function_name: str, repo: Optional[str] = None) -> str: | |
| """ | |
| Find every function in the codebase that CALLS a given function or class. | |
| Answers: "Who uses this?" rather than "What does this do?" | |
| Essential for tracing data flow upwards and understanding how a component | |
| is actually used vs how it's designed to be used. | |
| PREFER THIS OVER search_code when: | |
| - You have a function name and want its call sites | |
| - You need to trace the execution chain upward ("what triggers forward()?") | |
| - search_code already found the definition; now you want who calls it | |
| PREFER search_code/search_symbol OVER THIS when: | |
| - You don't have an exact function name yet | |
| - You want the definition, not its callers | |
| This is a structural lookup on AST metadata β it finds exact call sites | |
| recorded during indexing, not textual matches. The function name must | |
| match exactly (case-sensitive). | |
| Args: | |
| function_name: Exact function or class name as it appears in source code | |
| repo: Optional 'owner/repo' to restrict search | |
| """ | |
| if _store is None: | |
| return "Search service not ready." | |
| callers = _store.find_callers(function_name, repo=repo) | |
| if not callers: | |
| return f"No call sites found for '{function_name}' in the 'calls' index." | |
| # Format the same way as retrieval.format_context for consistency | |
| parts = [] | |
| for i, c in enumerate(callers[:8], 1): | |
| citation = c.get("filepath", "") | |
| if c.get("name"): | |
| citation += f" β {c['name']}()" | |
| citation += f" | lines {c.get('start_line', '?')}β{c.get('end_line', '?')}" | |
| parts.append(f"[Source {i} | {c.get('repo', '')} | {citation}]\n{c.get('text', '')}") | |
| return f"Found {len(callers)} caller(s) of '{function_name}':\n\n" + \ | |
| "\n\n" + "β" * 40 + "\n\n".join(parts) | |
| def get_file_chunk( | |
| repo: str, | |
| filepath: str, | |
| start_line: int, | |
| end_line: int, | |
| ) -> str: | |
| """ | |
| Read a specific line range from a file β more targeted than read_file. | |
| Use when a previous search gave you line numbers and you need the surrounding | |
| context: the class body, the docstring, or the lines immediately after a function. | |
| PREFER THIS OVER read_file when: | |
| - You already have line numbers from search_code or search_symbol | |
| - The file is large (>300 lines) and you only need a small section | |
| - You want 20-50 lines of context around a specific function | |
| PREFER read_file OVER THIS when: | |
| - You need the full file structure (imports, all class definitions) | |
| - You don't know the line numbers yet | |
| Args: | |
| repo: 'owner/repo' β must be exactly owner/name with one slash | |
| filepath: Path within repo (e.g. 'src/engine.py' β no leading slash) | |
| start_line: First line to fetch, 1-indexed (1 = first line of file) | |
| end_line: Last line to fetch, inclusive. Keep range β€ 80 lines for focus. | |
| """ | |
| # Validate repo format β LLM-supplied args must never be passed raw into URLs. | |
| if "/" not in repo or repo.count("/") != 1: | |
| return f"Invalid repo format '{repo}'. Expected 'owner/name'." | |
| # Reject path traversal β an LLM (or prompt injection) could pass "../.env" | |
| # which would resolve to an unintended path in the GitHub API URL. | |
| from pathlib import PurePosixPath | |
| try: | |
| parts = PurePosixPath(filepath).parts | |
| except Exception: | |
| return "Invalid filepath." | |
| if ".." in parts or filepath.startswith("/"): | |
| return "Invalid filepath: path traversal not allowed." | |
| owner, name = repo.split("/", 1) | |
| url = f"https://api.github.com/repos/{owner}/{name}/contents/{filepath}" | |
| headers = {"Accept": "application/vnd.github.v3.raw"} | |
| if settings.github_token: | |
| headers["Authorization"] = f"token {settings.github_token}" | |
| try: | |
| resp = http_requests.get(url, headers=headers, timeout=15) | |
| if resp.status_code == 404: | |
| return f"File not found: {filepath} in {repo}" | |
| resp.raise_for_status() | |
| except Exception as e: | |
| return f"GitHub fetch failed: {e}" | |
| lines = resp.text.splitlines() | |
| start = max(1, start_line) | |
| end = min(len(lines), end_line) | |
| chunk = "\n".join( | |
| f"{i + start}: {line}" | |
| for i, line in enumerate(lines[start - 1:end]) | |
| ) | |
| return f"# {repo} β {filepath} (lines {start}β{end})\n\n{chunk}" | |
| def search_symbol(symbol_name: str, repo: Optional[str] = None) -> str: | |
| """ | |
| Find the exact definition of a function or class by name. | |
| This is a STRUCTURAL lookup β it searches the 'name' metadata field in | |
| Qdrant, not the vector space. Use this when you know the exact name of | |
| a function or class and want its definition immediately, without the | |
| approximation of semantic search. | |
| When to use instead of search_code: | |
| - You already know the exact name (e.g. from a previous search result) | |
| - search_code returned a caller but not the definition itself | |
| - You want to be sure you're reading the right function, not a similar one | |
| Args: | |
| symbol_name: Exact function or class name (e.g. 'backward', 'Value', '_embed') | |
| repo: Optional 'owner/repo' to restrict search | |
| """ | |
| if _store is None: | |
| return "Search service not ready." | |
| matches = _store.find_symbol(symbol_name, repo=repo) | |
| if not matches: | |
| return ( | |
| f"No indexed definition found for '{symbol_name}'. " | |
| "The symbol may not be in the index β try search_code instead." | |
| ) | |
| parts = [] | |
| for i, c in enumerate(matches[:5], 1): | |
| citation = c.get("filepath", "") | |
| citation += f" | lines {c.get('start_line', '?')}β{c.get('end_line', '?')}" | |
| parts.append(f"[Source {i} | {c.get('repo', '')} | {citation}]\n{c.get('text', '')}") | |
| return ( | |
| f"Found {len(matches)} definition(s) of '{symbol_name}':\n\n" | |
| + "\n\n" + "β" * 40 + "\n\n".join(parts) | |
| ) | |
| def read_file(repo: str, filepath: str) -> str: | |
| """ | |
| Read the ENTIRE source file from a GitHub repository. | |
| Use when you need to understand a module's full structure β all its | |
| imports, class definitions, and how functions relate to each other. | |
| This reveals things that chunked search cannot: file-level imports, | |
| module docstrings, and the order in which things are defined. | |
| When to use instead of get_file_chunk: | |
| - You don't know the line numbers yet and want the full picture | |
| - You need to see imports (they're usually not in any function chunk) | |
| - You want to understand how multiple functions in a file interact | |
| Cost: one GitHub API call. Avoid calling on very large files (>500 lines); | |
| use get_file_chunk for targeted reads after search_code gives you line numbers. | |
| Args: | |
| repo: 'owner/repo' (e.g. 'karpathy/micrograd') | |
| filepath: Path within the repo (e.g. 'micrograd/engine.py') | |
| """ | |
| if "/" not in repo or repo.count("/") != 1: | |
| return f"Invalid repo format '{repo}'. Expected 'owner/name'." | |
| from pathlib import PurePosixPath | |
| try: | |
| parts = PurePosixPath(filepath).parts | |
| except Exception: | |
| return "Invalid filepath." | |
| if ".." in parts or filepath.startswith("/"): | |
| return "Invalid filepath: path traversal not allowed." | |
| owner, name = repo.split("/", 1) | |
| url = f"https://api.github.com/repos/{owner}/{name}/contents/{filepath}" | |
| headers = {"Accept": "application/vnd.github.v3.raw"} | |
| if settings.github_token: | |
| headers["Authorization"] = f"token {settings.github_token}" | |
| try: | |
| resp = http_requests.get(url, headers=headers, timeout=15) | |
| if resp.status_code == 404: | |
| return f"File not found: {filepath} in {repo}" | |
| resp.raise_for_status() | |
| except Exception as e: | |
| return f"GitHub fetch failed: {e}" | |
| lines = resp.text.splitlines() | |
| total = len(lines) | |
| # Warn on large files so the agent can decide whether to read a chunk instead. | |
| # 300 lines is ~15k characters β comfortably within context limits. | |
| header = f"# {repo} β {filepath} ({total} lines total)\n\n" | |
| if total > 500: | |
| header += f"# β οΈ Large file ({total} lines). Consider get_file_chunk for targeted reads.\n\n" | |
| numbered = "\n".join(f"{i+1}: {line}" for i, line in enumerate(lines)) | |
| return header + numbered | |
| def list_files(repo: str, path: str = "") -> str: | |
| """ | |
| List files and directories at a path within a GitHub repository. | |
| Use at the START of exploring an unfamiliar codebase to understand its structure. | |
| Then use read_file or get_file_chunk to read specific files you find. | |
| Returns file names, types (file/dir), and sizes. Directories are marked with / | |
| so you can drill down into them with another list_files call. | |
| Args: | |
| repo: 'owner/repo' (e.g. 'karpathy/micrograd') | |
| path: Directory path within the repo (e.g. 'src/models'). Empty = repo root. | |
| """ | |
| if "/" not in repo or repo.count("/") != 1: | |
| return f"Invalid repo format '{repo}'. Expected 'owner/name'." | |
| from pathlib import PurePosixPath | |
| if path: | |
| try: | |
| parts = PurePosixPath(path).parts | |
| except Exception: | |
| return "Invalid path." | |
| if ".." in parts or path.startswith("/"): | |
| return "Invalid path: traversal not allowed." | |
| owner, name = repo.split("/", 1) | |
| url = f"https://api.github.com/repos/{owner}/{name}/contents/{path}" | |
| headers = {"Accept": "application/vnd.github.v3+json"} | |
| if settings.github_token: | |
| headers["Authorization"] = f"token {settings.github_token}" | |
| try: | |
| resp = http_requests.get(url, headers=headers, timeout=15) | |
| if resp.status_code == 404: | |
| return f"Path not found: '{path}' in {repo}" | |
| resp.raise_for_status() | |
| except Exception as e: | |
| return f"GitHub fetch failed: {e}" | |
| entries = resp.json() | |
| if not isinstance(entries, list): | |
| # Single file returned β path was a file, not a directory | |
| return f"'{path}' is a file, not a directory. Use read_file to read it." | |
| lines = [f"# {repo}/{path or ''}\n"] | |
| dirs = sorted([e for e in entries if e["type"] == "dir"], key=lambda e: e["name"]) | |
| files = sorted([e for e in entries if e["type"] == "file"], key=lambda e: e["name"]) | |
| for e in dirs: | |
| lines.append(f" {e['name']}/") | |
| for e in files: | |
| size = e.get("size", 0) | |
| size_str = f"{size // 1024}KB" if size >= 1024 else f"{size}B" | |
| lines.append(f" {e['name']} ({size_str})") | |
| lines.append(f"\n{len(dirs)} director{'ies' if len(dirs) != 1 else 'y'}, {len(files)} file{'s' if len(files) != 1 else ''}") | |
| return "\n".join(lines) | |
| def glob(repo: str, pattern: str) -> str: | |
| """ | |
| List all indexed file paths in a repository that match a glob pattern. | |
| PREFER THIS OVER list_files when: | |
| - You want all files of a type across the whole repo at once ("**/*.py") | |
| - You need to understand directory structure without drilling level by level | |
| - You want to confirm whether certain file types / directories exist | |
| PREFER list_files OVER THIS when: | |
| - You need file sizes or want to see a single directory in detail | |
| - The repo hasn't been indexed yet (glob uses the index, not GitHub API) | |
| Uses fnmatch so standard glob patterns work: | |
| glob("karpathy/micrograd", "**/*.py") β all Python files | |
| glob("karpathy/micrograd", "src/**/*") β everything under src/ | |
| glob("karpathy/micrograd", "*.md") β top-level markdown files | |
| Args: | |
| repo: 'owner/repo' (e.g. 'karpathy/micrograd') | |
| pattern: Glob pattern matched against full file paths | |
| """ | |
| import fnmatch | |
| if _store is None: | |
| return "Store not initialised." | |
| chunks = _store.scroll_repo(repo) | |
| seen: set[str] = set() | |
| paths: list[str] = [] | |
| for c in chunks: | |
| fp = c.get("filepath", "") | |
| if fp and fp not in seen: | |
| seen.add(fp) | |
| paths.append(fp) | |
| matched = sorted(p for p in paths if fnmatch.fnmatch(p, pattern)) | |
| if not matched: | |
| return f"No indexed files match '{pattern}' in {repo}." | |
| return f"Files matching '{pattern}' in {repo} ({len(matched)}):\n" + "\n".join(matched) | |
| def grep(repo: str, pattern: str) -> str: | |
| """ | |
| Search all indexed source code in a repository for a regex pattern. | |
| Returns up to 20 matches showing file path, chunk name, and the first | |
| matching line β enough to decide which file to read_file() next without | |
| reading everything. | |
| PREFER THIS OVER read_file when: | |
| - You need to find WHERE something is defined or used across the whole repo | |
| - You're looking for an entry point (grep "def main|if __name__") | |
| - You want to find all usages of a function or class name | |
| PREFER search_symbol OVER THIS when: | |
| - You already know the exact function/class name and want its definition | |
| Args: | |
| repo: 'owner/repo' (e.g. 'karpathy/micrograd') | |
| pattern: Python regex pattern (case-insensitive). Use | for OR. | |
| Examples: "def main|__main__" "class.*Model" "TODO|FIXME" | |
| """ | |
| import re | |
| if _store is None: | |
| return "Store not initialised." | |
| try: | |
| rx = re.compile(pattern, re.IGNORECASE) | |
| except re.error: | |
| return f"Invalid regex: '{pattern}'. Use a valid Python regex." | |
| chunks = _store.scroll_repo(repo) | |
| results: list[str] = [] | |
| for c in chunks: | |
| text = c.get("text", "") | |
| for line in text.splitlines(): | |
| if rx.search(line): | |
| fp = c.get("filepath", "") | |
| name = c.get("name", "") | |
| label = f"{fp} ({name})" if name else fp | |
| results.append(f"{label}:\n {line.strip()}") | |
| break # one match per chunk β keeps results scannable across files | |
| if len(results) >= 20: | |
| break | |
| if not results: | |
| return f"No matches for '{pattern}' in {repo}." | |
| suffix = "\n\n(limit reached β refine pattern to narrow results)" if len(results) >= 20 else "" | |
| return f"Matches for '{pattern}' in {repo} ({len(results)}):\n\n" + "\n\n".join(results) + suffix | |
| def note(key: str, value: str) -> str: | |
| """ | |
| Save a key fact to working memory for this session. | |
| Use this to record important discoveries so you don't need to re-search them: | |
| note("entry_point", "train.py:main() at line 234") | |
| note("optimizer", "AdamW, lr=6e-4, defined in model.py:320") | |
| note("data_flow", "DataLoader β batch_encode β model.forward β loss.backward") | |
| Keeps your reasoning grounded: note facts as you find them, then recall_notes() | |
| before answering to make sure you haven't forgotten anything discovered earlier. | |
| Args: | |
| key: Short label for the fact (e.g. 'entry_point', 'main_class') | |
| value: The fact itself β be specific: include file, line, and what it does | |
| """ | |
| _session_notes[key] = value | |
| # Write-through to Qdrant for cross-session persistence. | |
| # Non-fatal: if Qdrant is unavailable, the note lives in-memory for this session. | |
| if _current_repo and _store is not None: | |
| try: | |
| _store.save_note(_current_repo, key, value) | |
| except Exception as e: | |
| print(f"[notes] Could not persist note '{key}': {e}") | |
| return f"β Noted: {key} = {value}" | |
| def recall_notes() -> str: | |
| """ | |
| Retrieve all facts saved with note() during this session. | |
| Call this before writing your final answer to ensure you haven't | |
| forgotten any discoveries from earlier in the conversation. | |
| Also useful at the START of a follow-up question to check what you | |
| already know before deciding which searches to run. | |
| Returns all (key, value) pairs recorded so far, or a message if none. | |
| """ | |
| if not _session_notes: | |
| return "No notes yet. Use note(key, value) to record discoveries as you search." | |
| lines = ["# Session notes\n"] | |
| for k, v in _session_notes.items(): | |
| lines.append(f"**{k}**: {v}") | |
| return "\n".join(lines) | |
| def draw_diagram(description: str, diagram_type: str = "flowchart") -> str: | |
| """ | |
| Signal that you want to draw a diagram inline in the chat response. | |
| After calling this tool, include a fenced code block with language 'diagram' | |
| in your answer containing valid Mermaid syntax. The frontend renders it as an | |
| SVG diagram with an expand-to-fullscreen button. | |
| When to use: | |
| - User asks to "draw", "visualize", "diagram", or "show" a relationship | |
| - The answer is clearer as a visual: architecture, data flow, class hierarchy, | |
| call graph, sequence of steps, component dependencies | |
| IMPORTANT: Always search the codebase (search_code, search_symbol, read_file) | |
| BEFORE calling this tool. The diagram must be grounded in real code you found. | |
| Call draw_diagram LAST in your tool sequence, right before writing the diagram block. | |
| Use any Mermaid diagram type that best fits the content: | |
| flowchart / graph β components, steps, decisions, data flow | |
| classDiagram β classes, inheritance, methods | |
| sequenceDiagram β call sequences between objects | |
| stateDiagram-v2 β state machines, lifecycle | |
| erDiagram β entity relationships | |
| gitGraph β git branching | |
| mindmap β hierarchical concepts | |
| timeline β chronological events | |
| Choose whichever type communicates the idea most clearly. | |
| After calling this tool, always write 1-3 sentences describing what the diagram | |
| shows, then output the diagram block: | |
| Here is a diagram showing how X connects to Y and Z: | |
| ```diagram | |
| flowchart LR | |
| A[DataLoader] --> B[Model] | |
| B --> C[Loss] | |
| C --> D[Optimizer] | |
| ``` | |
| Never output just the code block alone β always include a description. | |
| Args: | |
| description: What you plan to draw (e.g. "class hierarchy for the model") | |
| diagram_type: Any Mermaid diagram type (flowchart, classDiagram, sequenceDiagram, | |
| graph, stateDiagram-v2, erDiagram, gitGraph, mindmap, timeline, etc.) | |
| """ | |
| # Only flowchart and graph support direction modifiers (LR/TD). | |
| # All other types use their keyword alone as the opening line. | |
| directional = {"flowchart", "graph"} | |
| if diagram_type in directional: | |
| starter = f"{diagram_type} LR" | |
| else: | |
| starter = diagram_type | |
| return ( | |
| f"Ready. Draw '{description}' using Mermaid {diagram_type} syntax.\n\n" | |
| f"Output it in your response as a fenced code block with language 'diagram':\n\n" | |
| f"```diagram\n" | |
| f"{starter}\n" | |
| f" %% nodes and edges here\n" | |
| f"```\n\n" | |
| f"IMPORTANT rules for valid Mermaid:\n" | |
| f"- Node labels must be SHORT (2-4 words max) β they are rendered as-is, no wrapping\n" | |
| f"- NEVER use <br>, <br/>, or any HTML tags in labels β they show as literal text\n" | |
| f"- NEVER use 'style' commands or 'classDef' β they break the renderer\n" | |
| f"- Keep node IDs short, no spaces (e.g. Value, MLP, Neuron)\n" | |
| f"- ALWAYS quote labels that contain parentheses, operators, or special chars:\n" | |
| f" WRONG: D[Call backward()] RIGHT: D[\"Call backward()\"]\n" | |
| f" WRONG: K[grad += x * y] RIGHT: K[\"grad += x * y\"]\n" | |
| f" Special chars that REQUIRE quotes: ( ) [ ] {{ }} + = * / % < > & | # ;\n" | |
| f"- classDiagram: always include key attributes and methods inside curly braces, then list relationships\n" | |
| f" class Foo {{\n +type attr\n +method()\n }}\n" | |
| f" WRONG: class Foo <|-- Bar (cannot combine declaration + relationship)\n" | |
| f" RIGHT: declare all classes first (with members), then relationships on their own lines\n" | |
| f" Relationship arrows: <|-- inheritance, --> association, *-- composition\n" | |
| f" Empty class boxes are useless β always populate them with real attributes/methods from the code\n" | |
| f"- flowchart/graph: use LR or TD direction, quote labels with spaces: A[\"My Label\"]\n" | |
| f"- sequenceDiagram: use participant declarations, ->> for messages\n" | |
| f"- Good label example: Value, MLP, Neuron, Layer\n" | |
| f"- Bad label example: Value Class<br>Core scalar unit β NEVER do this\n" | |
| f"The frontend renders this as a live SVG diagram with expand-to-fullscreen." | |
| ) | |
| def trace_calls( | |
| repo: str, | |
| symbol_name: str, | |
| max_depth: int = 3, | |
| ) -> str: | |
| """ | |
| Trace the execution path starting from a function β who it calls and who they call. | |
| Walks the call graph stored during AST indexing (the 'calls' payload field). | |
| This answers "what happens when X runs?" by following the chain of function | |
| calls up to max_depth levels deep. | |
| Unlike find_callers (who calls X?), trace_calls answers "what does X call?" | |
| and recursively follows those calls to map the full execution path. | |
| Use this for: | |
| - Understanding data flow: "how does the forward pass work end-to-end?" | |
| - Tracing feature pipelines: "what does train() actually do step by step?" | |
| - Debugging: "which functions does backward() touch?" | |
| Args: | |
| repo: 'owner/repo' (e.g. 'karpathy/micrograd') | |
| symbol_name: Starting function or method name (e.g. 'train', 'forward') | |
| max_depth: How many levels deep to follow calls (default 3, max 5) | |
| """ | |
| if _store is None: | |
| return "Search service not ready." | |
| max_depth = min(max_depth, 5) # hard cap to avoid runaway traversal | |
| visited: set[str] = set() | |
| lines: list[str] = [f"# Call trace from `{symbol_name}` in {repo}\n"] | |
| missing: list[str] = [] # names we saw in calls[] but couldn't find in index | |
| def _walk(name: str, depth: int, prefix: str) -> None: | |
| if depth > max_depth or name in visited: | |
| return | |
| visited.add(name) | |
| chunks = _store.find_symbol(name, repo=repo) | |
| if not chunks: | |
| missing.append(name) | |
| return | |
| c = chunks[0] # take the first definition (most repos have exactly one) | |
| loc = f"{c.get('filepath', '?')} L{c.get('start_line', '?')}" | |
| lines.append(f"{prefix}β **{name}**() `{loc}`") | |
| calls = [fn for fn in (c.get("calls") or []) if fn not in visited] | |
| # Limit fan-out to 6 per level so the trace stays readable | |
| for callee in calls[:6]: | |
| _walk(callee, depth + 1, prefix + " ") | |
| _walk(symbol_name, 0, "") | |
| if len(lines) == 1: | |
| return ( | |
| f"Symbol '{symbol_name}' not found in the index for {repo}. " | |
| "Try search_symbol() to check the exact name, or search_code() to find it." | |
| ) | |
| if missing: | |
| lines.append(f"\n_Could not find definitions for: {', '.join(missing[:8])}_") | |
| lines.append(f"\n_{len(visited)} unique symbol(s) traced Β· depth limit {max_depth}_") | |
| return "\n".join(lines) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # RESOURCES β read-only data the LLM can access as context | |
| # | |
| # Resources are identified by URI (Uniform Resource Identifier). | |
| # The client reads them and injects their content into the LLM's context. | |
| # Think of them as "live documents" β always reflecting current state. | |
| # | |
| # URI scheme we define: qdrant://repos and qdrant://repos/{slug} | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def list_indexed_repos() -> str: | |
| """ | |
| List all GitHub repositories currently indexed in the vector database. | |
| Returns repo slugs (owner/name) and chunk counts. | |
| Read this resource to discover what's available before searching. | |
| """ | |
| if _store is None: | |
| return "Store not ready." | |
| repos = _store.list_repos() | |
| if not repos: | |
| return "No repositories indexed yet. Use POST /ingest to add one." | |
| lines = ["# Indexed Repositories\n"] | |
| for slug in repos: | |
| count = _store.count(repo=slug) | |
| lines.append(f"- **{slug}** β {count:,} chunks indexed") | |
| return "\n".join(lines) | |
| def get_repo_index(slug: str) -> str: | |
| """ | |
| Browse the indexed chunks for a specific repository. | |
| Returns all function and class names grouped by file. | |
| Use to understand a repo's structure before asking questions about it. | |
| Args: | |
| slug: 'owner/name' (forward slash URL-encoded as %2F by some clients) | |
| """ | |
| if _store is None: | |
| return "Store not ready." | |
| slug = slug.replace("%2F", "/") | |
| chunks = _store.scroll_repo( | |
| repo=slug, | |
| with_payload=["filepath", "name", "chunk_type", "start_line"], | |
| ) | |
| if not chunks: | |
| return f"No chunks found for '{slug}'. Re-ingest with POST /ingest." | |
| # Group by file | |
| by_file: dict[str, list[str]] = {} | |
| for chunk in chunks: | |
| fp = chunk.get("filepath", "unknown") | |
| name = chunk.get("name", "") | |
| if name: | |
| by_file.setdefault(fp, []).append( | |
| f" {chunk.get('chunk_type','?')}: {name} (L{chunk.get('start_line','?')})" | |
| ) | |
| lines = [ | |
| f"# {slug} β Index Summary\n", | |
| f"**{len(chunks)} chunks** across {len(by_file)} files\n", | |
| ] | |
| for fp, items in sorted(by_file.items()): | |
| lines.append(f"\n### {fp}") | |
| lines.extend(items[:20]) | |
| if len(items) > 20: | |
| lines.append(f" β¦ and {len(items) - 20} more") | |
| return "\n".join(lines) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PROMPTS β reusable templates the USER explicitly invokes | |
| # | |
| # Prompts appear as slash commands in MCP clients (Claude Desktop, etc.). | |
| # Unlike tools (model-driven), prompts are user-triggered. | |
| # They return structured messages that get injected into the conversation. | |
| # | |
| # The returned string becomes a user message asking the LLM to do a task. | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def analyze_repo(repo: str) -> str: | |
| """ | |
| Generate a structured analysis prompt for any indexed repository. | |
| Produces a multi-section prompt that guides the LLM to: | |
| 1. Give a high-level overview of the project | |
| 2. Identify entry points and key modules | |
| 3. Trace the main data flow | |
| 4. Highlight the 5 most important functions/classes | |
| Args: | |
| repo: 'owner/name' slug of an indexed repository | |
| """ | |
| return f"""Perform a comprehensive analysis of the repository '{repo}'. | |
| Use search_code to explore the codebase thoroughly. Structure your response: | |
| ## 1. Overview | |
| What does this repo do? What problem does it solve? Who is the target user? | |
| ## 2. Architecture | |
| What are the main modules/packages? How are they organized? | |
| What are the key dependencies? | |
| ## 3. Entry Points | |
| Where does execution begin? What are the public APIs or CLI commands? | |
| ## 4. Core Data Flow | |
| How does data move through the system end-to-end? | |
| What are the key transformations? | |
| ## 5. The 5 Most Important Functions/Classes | |
| For each: name, file, what it does, and why it matters. | |
| Start with search_code("overview {repo}"), then search_code("main entry point"). | |
| Cite all file paths and line numbers in your response.""" | |
| def explain_function(function_name: str, repo: Optional[str] = None) -> str: | |
| """ | |
| Generate a deep-dive explanation prompt for a specific function or class. | |
| Guides the LLM to find the implementation, trace its callers, | |
| and explain both WHAT it does and WHY it's designed that way. | |
| Args: | |
| function_name: Name of the function or class to explain | |
| repo: Optional 'owner/name' to restrict search scope | |
| """ | |
| scope = f" in '{repo}'" if repo else "" | |
| return f"""Give a complete technical explanation of `{function_name}`{scope}. | |
| Use the available tools: | |
| 1. search_code("{function_name}") β find the implementation | |
| 2. get_file_chunk(...) β get surrounding context if needed | |
| 3. find_callers("{function_name}") β find where it's used | |
| Your explanation must cover: | |
| - **What it does**: Plain English, one paragraph | |
| - **Signature**: Parameters (types + purpose) and return value | |
| - **Algorithm**: Step-by-step walkthrough of the logic | |
| - **Usage patterns**: How and where it's called in the codebase | |
| - **Design rationale**: Why is it implemented this way? | |
| Be precise. Cite every file path and line number you reference.""" | |