Skip to content

Repository files navigation

FastAPI Repository Chat Agent - Multi-Agent MCP System

A production-ready multi-agent system for answering questions about any Python codebase using the Model Context Protocol (MCP), Deep Agents orchestration, and Neo4j knowledge graphs. The system uses Retrieval-Augmented Generation (RAG) to retrieve code patterns from indexed repositories and generate accurate, context-aware responses.

Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│ USER │
│ "How do I create a FastMCP server?" │
└─────────────────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ ORCHESTRATOR (Port 8000) │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ Deep Agent (LangGraph ReAct) │ │
│ │ • Recursion limit: 50 (industry standard for multi-step agents) │ │
│ │ • RAG workflow: RETRIEVE → LEARN → GENERATE │ │
│ │ • Streaming: Real-time tool calls & responses via WebSocket │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────┼─────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Indexer Agent │ │ Graph Query │ │ Code Analyst │ │
│ │ (MCP Client) │ │ Agent (MCP) │ │ Agent (MCP) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
└────────────┼────────────────────┼────────────────────┼───────────────────┘
│ │ │
▼ ▼ ▼
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ Indexer MCP │ │ Graph Query MCP │ │ Code Analyst MCP │
│ Server (8001) │ │ Server (8002) │ │ Server (8003) │
│ • index_repository│ │ • find_entity │ │ • get_code_snippet│
│ • parse_python_ast│ │ • search_entities │ │ • analyze_class │
│ • extract_entities│ │ • get_dependencies│ │ • analyze_function│
└─────────┬──────────┘ └─────────┬──────────┘ └─────────┬──────────┘
│ │ │
└──────────────────────┼──────────────────────┘
▼
┌───────────────────────────────────────────┐
│ Neo4j Knowledge Graph │
│ ┌─────────────────────────────────────┐ │
│ │ Node Types: │ │
│ │ • File (587) - with metadata │ │
│ │ • Module, Class (2110 with source) │ │
│ │ • Function (2712 with source) │ │
│ │ • Method, Parameter, Decorator │ │
│ │ • Import, Docstring (2124 parsed) │ │
│ │ │ │
│ │ Relationships: │ │
│ │ • CONTAINS, IMPORTS, INHERITS_FROM │ │
│ │ • CALLS, DECORATED_BY, HAS_PARAMETER│ │
│ │ • DOCUMENTED_BY, DEPENDS_ON │ │
│ └─────────────────────────────────────┘ │
└───────────────────────────────────────────┘

Key Features

RAG-Powered Code Generation

  • Retrieve: Search indexed repositories for relevant code patterns
  • Learn: Extract imports, class structures, and coding patterns from real code
  • Generate: Create new code that follows the repository's patterns

Knowledge Graph with Source Code

  • Source code storage: Actual source code stored in Neo4j nodes (not just metadata)
  • Parsed docstrings: Structured documentation with summary, params, returns, examples
  • File tracking: Every Python file indexed with size and line count

Real-Time Streaming UI

  • Live visualization of agent thinking and tool calls
  • Color-coded agent badges (Indexer, Graph Query, Code Analyst)
  • Todo list display with status icons (✅, 🔄, ⏳)
  • Syntax-highlighted code responses

Multi-Agent MCP Architecture

  • 3 Specialized MCP Servers: Independent, scalable services
  • Deep Agents Orchestration: LangGraph ReAct with 50-step recursion limit
  • Tool-rich API: 20+ tools for code analysis and retrieval

Quick Start

Prerequisites

  • Docker and Docker Compose
  • OpenAI API key

Setup

  1. Clone the repository:
git clone <your-repo-url>cd opsly
  1. Create .env file:
cp .env.example .env
# Edit .env and add your OPENAI_API_KEY
  1. Start all services:
docker-compose up --build
  1. Access the application:

Example: Test the RAG Workflow

  1. Index a repository (e.g., FastMCP):
curl -X POST http://localhost:8000/api/index \
-H "Content-Type: application/json" \
-d '{"repo_url": "https://github.com/jlowin/fastmcp", "branch": "main"}'
  1. Ask the agent to generate code:
"How do I create a calculator MCP server using FastMCP?"
  1. Watch the agent:
    • Search for FastMCP class and tool decorator patterns
    • Retrieve actual source code from the indexed repository
    • Generate new code based on the learned patterns

The agent will produce code like:

fromfastmcpimportFastMCP# Import learned from repomcp=FastMCP("calculator")
@mcp.tool() # Pattern learned from indexed examplesdefadd(a: int, b: int) ->int:
"""Add two numbers."""returna+bif__name__=="__main__":
mcp.run()

Usage

1. Index a Repository

Using the Web UI:

  • Navigate to http://localhost:8000
  • Enter repository URL in the sidebar (e.g., https://github.com/tiangolo/fastapi.git)
  • Click "Start Indexing"

Using the API:

curl -X POST http://localhost:8000/api/index \
-H "Content-Type: application/json" \
-d '{"repo_url": "https://github.com/tiangolo/fastapi.git", "branch": "main"}'

2. Ask Questions

Simple queries:

  • "What is the FastAPI class?"
  • "Show me the Depends function"

Medium complexity:

  • "How does FastAPI handle request validation?"
  • "What classes inherit from APIRouter?"

Complex queries:

  • "Explain the complete lifecycle of a FastAPI request"
  • "How does dependency injection work and show examples"
  • "Compare Path and Query parameter implementations"

Project Structure

opsly/
├── shared/ # Shared utilities
│ ├── models/ # Pydantic models
│ ├── db/ # Neo4j driver
│ ├── config.py # Configuration
│ └── exceptions.py # Custom exceptions
├── agents/ # MCP Agent Servers
│ ├── indexer/ # Repository indexing MCP server
│ │ ├── server.py # FastMCP server
│ │ ├── tools.py # Tool implementations
│ │ └── parser.py # AST parsing
│ ├── graph_query/ # Graph query MCP server
│ │ ├── server.py # FastMCP server
│ │ ├── tools.py # Tool implementations
│ │ └── queries.py # Cypher queries
│ └── code_analyst/ # Code analysis MCP server
│ ├── server.py # FastMCP server
│ ├── tools.py # Tool implementations
│ └── analyzer.py # Code analysis logic
├── orchestrator/ # Orchestrator + API + UI
│ ├── agent/ # Deep Agent orchestration
│ │ ├── deep_agent.py # Main orchestrator with streaming
│ │ ├── mcp_tools.py # MCP tools loader
│ │ └── orchestrator_tools.py # Orchestrator-specific tools
│ ├── api/ # FastAPI routes
│ │ ├── main.py # FastAPI app
│ │ └── routes/ # API endpoints
│ └── web/ # Web UI
│ ├── static/ # JavaScript & CSS
│ └── templates/ # HTML templates
├── docker/ # Dockerfiles
├── docker-compose.yml # Service orchestration
└── pyproject.toml # Dependencies

How We Index Codebases into Neo4j

Indexing Pipeline Overview

┌──────────────────────────────────────────────────────────────────────────┐
│ USER TRIGGERS INDEXING │
│ POST /api/index {"repo_url": "...", "branch": "main"} │
└────────────────────────────────┬─────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STEP 1: Clone Repository │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ • git clone <repo_url> /tmp/repos/<repo-name>-<hash> │ │
│ │ • git checkout <branch> │ │
│ │ • Find all .py files (skip __pycache__, .venv, tests) │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬─────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STEP 2: Parse Python AST (per file) │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ parser = PythonASTParser(file_path) │ │
│ │ entities = parser.parse() │ │
│ │ │ │
│ │ Extracts: │ │
│ │ • File metadata (name, path, size, line_count) │ │
│ │ • Module (name, path, docstring) │ │
│ │ • Classes (name, bases, decorators, source_code, docstring) │ │
│ │ • Functions (name, params, returns, source_code, docstring) │ │
│ │ • Methods (name, params, returns, source_code, docstring) │ │
│ │ • Imports (module, alias, names) │ │
│ │ • Decorators (name, args) │ │
│ │ • Parameters (name, type_hint, default, kind) │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬─────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STEP 3: Extract Source Code │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ def _get_source_segment(node: ast.AST) -> str: │ │
│ │ lines = source_code.splitlines() │ │
│ │ start = node.lineno - 1 │ │
│ │ end = node.end_lineno │ │
│ │ # Include decorators if present │ │
│ │ if node.decorator_list: │ │
│ │ start = node.decorator_list[0].lineno - 1 │ │
│ │ return '\n'.join(lines[start:end]) │ │
│ │ │ │
│ │ Result: Actual Python source code stored in each node │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬─────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STEP 4: Parse Docstrings │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ def parse_docstring(raw: str) -> DocstringNode: │ │
│ │ # Parse Google/NumPy style docstrings │ │
│ │ summary = first_line │ │
│ │ params = extract_params() # Args: section │ │
│ │ returns = extract_returns() # Returns: section │ │
│ │ raises = extract_raises() # Raises: section │ │
│ │ examples = extract_examples() # Examples: section │ │
│ │ │ │
│ │ Result: Structured documentation for each entity │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬─────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STEP 5: Create Neo4j Nodes │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ // Create File node │ │
│ │ MERGE (f:File {path: $path}) │ │
│ │ SET f.name = $name, f.size_bytes = $size, f.line_count = $lines │ │
│ │ │ │
│ │ // Create Module node │ │
│ │ MERGE (m:Module {name: $name, path: $path}) │ │
│ │ SET m.docstring = $docstring │ │
│ │ │ │
│ │ // Create Class node WITH source code │ │
│ │ MERGE (c:Class {name: $name, path: $path}) │ │
│ │ SET c.lineno = $lineno, │ │
│ │ c.end_lineno = $end_lineno, │ │
│ │ c.docstring = $docstring, │ │
│ │ c.bases = $bases, │ │
│ │ c.source_code = $source_code ← ACTUAL PYTHON CODE │ │
│ │ │ │
│ │ // Create Function node WITH source code │ │
│ │ MERGE (f:Function {name: $name, path: $path}) │ │
│ │ SET f.source_code = $source_code ← ACTUAL PYTHON CODE │ │
│ │ │ │
│ │ // Create Docstring node │ │
│ │ MERGE (d:Docstring {entity_name: $name, entity_type: $type}) │ │
│ │ SET d.raw = $raw_docstring, │ │
│ │ d.summary = $summary, │ │
│ │ d.description = $description, │ │
│ │ d.params = $params_json, │ │
│ │ d.returns = $returns_json, │ │
│ │ d.examples = $examples_json │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬─────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ STEP 6: Create Relationships │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ // File → Module │ │
│ │ MATCH (f:File {path: $path}), (m:Module {name: $name}) │ │
│ │ MERGE (f)-[:CONTAINS]->(m) │ │
│ │ │ │
│ │ // Module → Class/Function │ │
│ │ MATCH (m:Module {name: $module}), (c:Class {name: $class}) │ │
│ │ MERGE (m)-[:CONTAINS]->(c) │ │
│ │ │ │
│ │ // Class → Base Class (inheritance) │ │
│ │ MATCH (c:Class {name: $child}), (b:Class {name: $base}) │ │
│ │ MERGE (c)-[:INHERITS_FROM]->(b) │ │
│ │ │ │
│ │ // Function/Method → Called Functions │ │
│ │ MATCH (f:Function {name: $caller}), (c:Function {name: $callee}) │ │
│ │ MERGE (f)-[:CALLS]->(c) │ │
│ │ │ │
│ │ // Entity → Docstring │ │
│ │ MATCH (e:Class {name: $name}), (d:Docstring {entity_name: $name}) │ │
│ │ MERGE (e)-[:DOCUMENTED_BY]->(d) │ │
│ │ │ │
│ │ // Function/Method → Parameters │ │
│ │ MATCH (f:Function {name: $func}), (p:Parameter {name: $param}) │ │
│ │ MERGE (f)-[:HAS_PARAMETER]->(p) │ │
│ │ │ │
│ │ // Entity → Decorators │ │
│ │ MATCH (f:Function {name: $func}), (d:Decorator {name: $dec}) │ │
│ │ MERGE (f)-[:DECORATED_BY]->(d) │ │
│ │ │ │
│ │ // Module → Imports │ │
│ │ MATCH (m:Module {name: $module}) │ │
│ │ MERGE (i:Import {module: $import_module}) │ │
│ │ MERGE (m)-[:IMPORTS]->(i) │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└────────────────────────────────┬─────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ RESULT: Knowledge Graph Ready for RAG │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ FastMCP Repository (587 files indexed): │ │
│ │ • 587 File nodes │ │
│ │ • 2,110 Class nodes (with source_code) │ │
│ │ • 2,712 Function nodes (with source_code) │ │
│ │ • 2,124 Docstring nodes (parsed) │ │
│ │ • ~15,000+ relationships │ │
│ │ │ │
│ │ Agent can now: │ │
│ │ 1. Search for entities (search_entities) │ │
│ │ 2. Retrieve source code (get_code_snippet) │ │
│ │ 3. Learn patterns from real code │ │
│ │ 4. Generate new code following those patterns │ │
│ └────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘

Key Implementation Details

1. Source Code Extraction (The Critical Feature)

# From agents/indexer/parser.pydef_get_source_segment(self, node: ast.AST) ->str:
"""Extract actual source code for an AST node."""ifnothasattr(node, 'lineno') ornothasattr(node, 'end_lineno'):
return""lines=self.source_code.splitlines()
start=node.lineno-1# 0-indexedend=node.end_lineno# IMPORTANT: Include decorator lines if presentifhasattr(node, 'decorator_list') andnode.decorator_list:
first_decorator=node.decorator_list[0]
ifhasattr(first_decorator, 'lineno'):
start=first_decorator.lineno-1return'\n'.join(lines[start:end])

Why this matters for RAG:

  • Agents retrieve ACTUAL code, not just metadata
  • Can learn import patterns: from fastmcp import FastMCP
  • Can learn decorator patterns: @mcp.tool()
  • Can learn class structures and method signatures

2. Docstring Parsing

# From agents/indexer/parser.pydefparse_docstring(docstring: Optional[str]) ->Optional[DocstringNode]:
"""Parse Google/NumPy style docstrings into structured format."""# Extract summary (first line)summary=lines[0].strip()
# Parse sections with regex patternsparam_pattern=re.compile(r'^\s*(?:Args?|Parameters?):\s*$')
returns_pattern=re.compile(r'^\s*(?:Returns?):\s*$')
# Google style: " param_name (type): description"google_param=re.compile(r'^\s{4,}(\w+)(?:\s*\(([^)]+)\))?:\s*(.*)$')
returnDocstringNode(
raw=docstring,
summary=summary,
params=params,
returns=returns_info,
examples=examples
)

Why this matters for RAG:

  • Agents can show parameter descriptions to users
  • Examples from docstrings become usage templates
  • Return value documentation helps understand APIs

3. Relationship Tracking

# From agents/indexer/parser.pyclassCallVisitor(ast.NodeVisitor):
"""Extract function calls from code."""defvisit_Call(self, node: ast.Call):
call_name=self._get_call_name(node.func)
self.calls.add(call_name)

Why this matters for RAG:

  • Track which functions call which (CALLS relationships)
  • Understand dependencies between code entities
  • Follow execution flows through the codebase

MCP Servers

1. Indexer Agent (Port 8001)

Responsibilities:

  • Clone Git repositories
  • Parse Python AST
  • Extract code entities with source code
  • Populate Neo4j graph with relationships

Tools:

  • index_repository(repo_url, branch) - Full repo indexing
  • index_file(file_path) - Single file indexing
  • get_index_status(job_id) - Job progress
  • parse_python_ast(file_path) - AST extraction
  • extract_entities(source_code) - Entity identification

2. Graph Query Agent (Port 8002)

Responsibilities:

  • Query Neo4j knowledge graph
  • Find code entities
  • Trace dependencies
  • Explore relationships

Tools:

  • find_entity(name, entity_type) - Find by name
  • get_dependencies(entity_name) - What it depends on
  • get_dependents(entity_name) - What depends on it
  • trace_imports(module_name) - Import chains
  • find_related(entity, relationship, direction) - Related entities
  • execute_query(cypher) - Custom Cypher queries
  • get_class_hierarchy(class_name) - Class inheritance tree
  • search_entities(pattern) - Search by pattern
  • get_statistics() - Knowledge graph statistics

3. Code Analyst Agent (Port 8003)

Responsibilities:

  • Deep code analysis
  • Pattern detection
  • AI-powered explanations
  • Complexity metrics

Tools:

  • analyze_function(function_name) - Metrics & patterns
  • analyze_class(class_name) - Class structure analysis
  • find_patterns(pattern_type) - Design pattern detection
  • get_code_snippet(entity_name) - Extract with context
  • explain_implementation(entity_name) - AI explanation
  • compare_implementations(entity1, entity2) - Compare code

4. Orchestrator Tools

Responsibilities:

  • Query analysis and classification
  • Agent routing and execution planning
  • Conversation context management
  • Response synthesis

Tools:

  • analyze_query(query) - Classify query intent, extract entities, identify required agents
  • route_to_agents(query, analysis) - Create execution plan with agent routing strategy
  • get_conversation_context(session_id) - Retrieve conversation history
  • synthesize_response(results) - Combine agent outputs into coherent response

API Endpoints

Chat

  • POST /api/chat - Send message, get response
  • WS /ws/chat - WebSocket real-time chat with streaming events:
    • thinking - Agent processing states
    • query_analysis - Query classification and entity extraction
    • plan - Execution plan with steps and strategy
    • delegation - Agent delegation events
    • tool_call - MCP tool invocations
    • tool_result - Tool execution results
    • response - Final answer

Indexing

  • POST /api/index - Trigger indexing
  • GET /api/index/status/{job_id} - Check status

Health & Monitoring

  • GET /api/health - Basic health check
  • GET /api/agents/health - All agents status
  • GET /api/graph/statistics - Graph stats

Configuration

Environment variables (see .env.example):

  • OPENAI_API_KEY - Required for Code Analyst
  • NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD - Neo4j connection
  • REDIS_HOST, REDIS_PORT - Redis for sessions
  • LOG_LEVEL - Logging level (DEBUG, INFO, WARNING, ERROR)

Development

Local Development

# Install dependencies
pip install -e ".[dev]"# Run individual services
python -m agents.indexer.server
python -m agents.graph_query.server
python -m agents.code_analyst.server
python -m orchestrator.api.main
# Run tests
pytest
# Format code
black .
ruff check .

Architecture Decisions

Decision 1: Deep Agent (LangGraph ReAct) vs MCP Orchestrator Agent

What the assignment suggested: The assignment spec described creating an "Orchestrator Agent" as a separate MCP server with tools like analyze_query, route_to_agents, synthesize_response. This would mean the orchestrator itself is an MCP server that the API calls.

What we implemented instead: We used Deep Agent pattern with LangGraph ReAct directly in the API layer, NOT as an MCP server. The orchestrator has direct access to all MCP agent tools without wrapping them.

Why we chose Deep Agent:

  1. Better Performance & Latency

    • No extra network hop for orchestration logic
    • Direct tool invocations instead of MCP protocol overhead
    • Faster response times for multi-step queries
  2. Native Streaming Support

    • LangGraph ReAct provides built-in streaming for tool calls and responses
    • Real-time WebSocket updates show agent thinking, planning, and tool execution
    • Users see live progress instead of waiting for final response
  3. Simpler Architecture

    • Orchestrator runs in the same process as the API gateway
    • No need to manage a 4th MCP server (orchestrator MCP server)
    • Fewer network connections and failure points
  4. More Flexible Tool Composition

    • Orchestrator can have its own tools (analyze_query, synthesize_response) WITHOUT wrapping them as MCP
    • Direct access to OpenAI function calling for orchestrator logic
    • Can mix MCP tools (from agents) with local tools (orchestrator-specific)
  5. Industry Best Practice

    • Deep Agent pattern is standard in LangChain/LangGraph applications
    • ReAct (Reason → Act → Observe) is proven for multi-step reasoning
    • Recursion limit of 50 is industry standard for complex queries

Trade-offs accepted:

AspectMCP OrchestratorDeep Agent (Our Choice)
Network overheadHigher (4 MCP servers)Lower (3 MCP servers)
Orchestrator scalabilityCan scale independentlyScales with API gateway
Tool protocolEverything via MCPMCP for agents, native for orchestrator
Streaming supportWould need custom implementationBuilt-in with LangGraph
Development complexityMore complex (4th MCP server)Simpler (integrated in API)
Assignment complianceLiteral interpretationPragmatic interpretation

What we kept from assignment spec:

  • ✅ Orchestrator has query analysis logic (analyze_query function)
  • ✅ Orchestrator routes to agents (via MCP tool calls)
  • ✅ Orchestrator synthesizes responses (via synthesize_response function)
  • ✅ All 3 specialized agents (Indexer, Graph Query, Code Analyst) are MCP servers

Conclusion: While the assignment suggested making the orchestrator an MCP server, we chose the Deep Agent pattern for better performance, simpler architecture, and native streaming support. The functional requirements (analyze, route, synthesize) are all met, just without the MCP protocol overhead for orchestration logic.


Decision 2: Why LangGraph ReAct vs Plain LangChain Agent

LangGraph ReAct advantages:

  • Explicit graph-based workflow with state management
  • Built-in checkpointing for conversation persistence
  • Better control over tool execution flow
  • Native support for parallel tool calls
  • Easier to debug with graph visualization

Plain LangChain Agent limitations:

  • Less control over execution flow
  • Harder to implement custom routing logic
  • Limited streaming capabilities
  • No built-in state persistence

Decision 3: Why MCP for Specialized Agents

Why use MCP protocol for Indexer, Graph Query, and Code Analyst?

  1. Standardized Communication

    • MCP provides a standard protocol for agent-to-agent communication
    • Tool definitions are self-describing (JSON-RPC format)
    • Easy to add new agents without changing orchestrator code
  2. Independent Scaling

    • Each agent runs as a separate Docker container
    • Can scale agents independently based on load
    • Indexer can handle heavy git cloning without blocking others
  3. Language Agnostic

    • MCP is language-agnostic (we use FastMCP for Python)
    • Could add agents in other languages (TypeScript, Go, Rust)
    • Agents only need to implement MCP protocol
  4. Clean Separation of Concerns

    • Each agent has a well-defined responsibility
    • Agents don't know about each other
    • Orchestrator is the single point of coordination
  5. Easy to Extend

    • Adding a new agent is just adding a new MCP server
    • No need to modify existing agents
    • New tools automatically discovered by orchestrator

Trade-offs:

  • Network latency for each MCP call (~5-10ms per tool invocation)
  • JSON-RPC serialization overhead
  • More complex deployment (3+ containers)

Why it's worth it:

  • Better separation of concerns outweighs network overhead
  • Independent scaling is critical for production
  • MCP is becoming an industry standard (Anthropic, Sourcegraph, etc.)

Decision 4: Why Neo4j for Knowledge Graph

Why Neo4j vs Alternatives (PostgreSQL with pg_graph, MongoDB)?

  1. Natural Fit for Code Relationships

    • Cypher query language is designed for graph traversal
    • Native support for relationship queries (INHERITS_FROM, CALLS, DEPENDS_ON)
    • Fast path-finding algorithms (shortest path, all paths)
  2. Performance for Graph Traversal

    • O(1) relationship lookups (vs O(n) table joins in SQL)
    • Index-free adjacency for fast navigation
    • Optimized for "find all classes that inherit from X" queries
  3. Schema Flexibility

    • Can add new node types without migrations
    • Dynamic properties on nodes
    • Easy to extend with new relationship types
  4. Cypher Query Language

    • Expressive and readable
    • Pattern matching with ASCII art syntax
    • Aggregation and filtering in single query

Example Query Power:

// Find all classes that use FastAPI's Depends function (3 hops)MATCH (depends:Function{name:'Depends'})
MATCH (m:Method)-[:CALLS]->(depends)
MATCH (c:Class)-[:CONTAINS]->(m)
RETURNc.name, c.path, c.source_code

This would be complex and slow in SQL with multiple joins.


Decision 5: Why Real-Time Streaming (WebSocket)

Why WebSocket vs HTTP Polling?

  1. Transparency in Multi-Agent Workflow

    • Users see agent thinking in real-time
    • Tool calls and results shown live
    • Better understanding of how the system works
  2. Better User Experience

    • No "loading..." spinner for 30 seconds
    • Progressive feedback builds trust
    • Users can cancel if agent goes off track
  3. Debugging & Monitoring

    • Developers can see exact tool execution sequence
    • Easy to identify where agent gets stuck
    • Performance metrics visible per tool call
  4. Event-Driven Architecture

    • Server can push updates without client polling
    • Reduced server load (no polling overhead)
    • Lower latency for updates

Events we stream:

  • thinking - Agent reasoning state
  • query_analysis - Parsed query intent
  • plan - Execution plan with steps
  • delegation - Which agent is being invoked
  • tool_call - MCP tool invocations
  • tool_result - Tool execution results
  • response - Final answer

Summary: Architecture Philosophy

Our approach:

  • Pragmatic over dogmatic: Use MCP where it adds value (specialized agents), skip it where it adds overhead (orchestrator)
  • Performance-first: Real-time streaming and low latency are critical
  • Production-ready: Independent scaling, clean separation, easy debugging
  • RAG-optimized: Store source code, parse docstrings, enable pattern learning

Result: A system that meets all functional requirements of the assignment while being faster, simpler, and more maintainable than a pure MCP-everywhere approach.

Troubleshooting

Services won't start

# Check logs
docker-compose logs -f
# Restart specific service
docker-compose restart indexer

Neo4j connection issues

# Check Neo4j is ready
docker-compose logs neo4j
# Verify connection
docker exec -it opsly-neo4j cypher-shell -u neo4j -p password

OpenAI API errors

  • Verify OPENAI_API_KEY in .env
  • Check API quota and billing

Challenges & Solutions

Challenge 1: Agent Generating Irrelevant Code (Factorial Instead of FastMCP)

Problem: When asked "How to write FastMCP code?", the agent would generate generic Python code (like factorial functions) instead of using knowledge from the indexed repository.

Root Cause: The original system prompt was too restrictive with "NEVER generate code from imagination" without clear guidance on HOW to use retrieved context.

Solution: Implemented RAG (Retrieval-Augmented Generation) workflow:

  1. Rewrote the system prompt to enforce a 3-step process: RETRIEVE → LEARN → GENERATE
  2. Agent must search and retrieve code patterns BEFORE generating any code
  3. All generated code must be based on patterns from the indexed repository
  4. Added example workflows showing correct tool usage sequence
# System prompt now enforces:# 1. search_entities(pattern="FastMCP") → Find relevant code# 2. get_code_snippet(entity_name="FastMCP") → Get actual source# 3. THEN generate code using learned patterns

Challenge 2: Source Code Not Available for Retrieval

Problem: The knowledge graph only stored metadata (class names, line numbers) but not the actual source code, making it impossible for the agent to learn patterns.

Root Cause: Parser extracted AST information but didn't capture the source text.

Solution:

  1. Added _get_source_segment() method to parser using node.lineno and node.end_lineno
  2. Added source_code and end_lineno fields to ClassNode, FunctionNode, MethodNode
  3. Updated Neo4j queries to store and retrieve source code
  4. Now 2,110 classes and 2,712 functions have full source code stored
def_get_source_segment(self, node: ast.AST) ->str:
"""Extract source code for an AST node."""lines=self.source_code.splitlines()
start=node.lineno-1end=node.end_lineno# Include decorators if presentifhasattr(node, 'decorator_list') andnode.decorator_list:
start=node.decorator_list[0].lineno-1return'\n'.join(lines[start:end])

Challenge 3: UI Displaying [object Object] for Tool Arguments

Problem: Tool arguments in the UI showed [object Object] instead of readable content, especially for complex structures like todo lists.

Root Cause: JavaScript template literals convert objects to strings using .toString(), which produces [object Object].

Solution:

  1. Created formatArgValue() function to handle different data types
  2. Special handling for todos array to display as a clean checklist
  3. Used JSON.stringify() with formatting for other objects
  4. Added CSS styling for todo items with status icons
functionformatArgValue(key,value){if(key==='todos'&&Array.isArray(value)){returnvalue.map(todo=>{conststatusIcon=todo.status==='completed' ? '✅' : todo.status==='in_progress' ? '🔄' : '⏳';return`${statusIcon}${todo.content}`;}).join('\n');}returntypeofvalue==='object' ? JSON.stringify(value,null,2) : value;}

Challenge 4: Recursion Limit Too Low for Complex Queries

Problem: Complex multi-step queries would hit the recursion limit (originally 20-30) before the agent could complete thorough research.

Root Cause: Default LangGraph recursion limits are conservative, but RAG workflows require more steps (search → retrieve multiple entities → analyze → generate).

Solution:

  • Researched industry standards for LangGraph/ReAct agents
  • Increased recursion limit to 50 (industry standard for multi-step agents)
  • This allows 5-8 tool calls per query as recommended for thorough retrieval

Challenge 5: Docstrings Not Properly Indexed

Problem: The assignment required DOCUMENTED_BY relationships, but docstrings were only stored as raw text in the entity nodes.

Solution:

  1. Created DocstringNode model with structured fields (summary, description, params, returns, examples)
  2. Implemented parse_docstring() function supporting Google/NumPy style docstrings
  3. Created DOCUMENTED_BY relationships from entities to Docstring nodes
  4. Result: 2,124 parsed docstrings with structured data

Challenge 6: Missing File Node (Assignment Requirement)

Problem: The assignment spec required File nodes, but we only had Module nodes.

Solution:

  1. Added FileNode model with name, path, size_bytes, line_count
  2. Parser now creates File node for each indexed Python file
  3. Added CONTAINS relationship from File to Module
  4. Result: 587 File nodes tracking all indexed files

Research & Information Sources

Industry Best Practices for Code Knowledge Graphs

SCIP (Sourcegraph Code Intelligence Protocol)

Microsoft GraphRAG

scip-python (Sourcegraph's Python Indexer)

LangGraph & ReAct Agents

  • Recursion limit of 50 is standard for multi-step agents
  • ReAct pattern: Reason → Act → Observe → Repeat
  • Source: LangGraph documentation, LangChain examples

AST Source Extraction

  • Python AST nodes have lineno and end_lineno attributes (Python 3.8+)
  • ast.get_source_segment() for precise extraction
  • Decorator handling requires checking decorator_list[0].lineno

Docstring Parsing

  • Google style: Args:, Returns:, Raises:, Examples:
  • NumPy style: Parameters, Returns, Raises
  • Regex patterns for section detection

Implementation Statistics

After indexing FastMCP repository:

MetricCount
Files indexed587
Classes with source code2,110
Functions with source code2,712
Parsed docstrings2,124
Total relationships~15,000+

Known Limitations

  1. Python only: Currently only indexes Python codebases
  2. File size limit: Large files (>500KB) are skipped
  3. No authentication: Web UI and API have no auth (add for production)
  4. Single repository: Currently supports one repository at a time

Future Improvements

Based on research, potential enhancements:

  1. Usage tracking: Track where symbols are referenced, not just defined
  2. Type inference: Extract parameter and return types using type stubs
  3. Community detection: Group related code using graph algorithms
  4. Cross-repo search: Support multiple repositories simultaneously

About

Production multi-agent system for codebase Q&A — MCP, Deep Agents orchestration, Neo4j knowledge graphs and RAG over indexed Python repos. Built on FastAPI.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages