Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ cython_debug/
# Kiro IDE
.kiro/

# Claude Code
.claude/
CLAUDE.md

# Project specific
*.log
.DS_Store
Expand Down
139 changes: 139 additions & 0 deletions .planning/codebase/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Architecture

**Analysis Date:** 2026-04-13

## Pattern Overview

**Overall:** MCP Server acting as a typed proxy to an external LightRAG REST API

**Key Characteristics:**
- Three-layer design: CLI entry → MCP Server (tool dispatch) → HTTP Client (API calls)
- Single global client instance, lazily initialized on first tool call from env vars
- All business logic lives in the external LightRAG service; this codebase is a protocol adapter
- Async throughout — server, client, and transport are all async
- All inter-layer contracts are enforced with Pydantic v2 models

## Layers

**CLI / Entry Point Layer:**
- Purpose: Process startup and shutdown; wraps asyncio event loop
- Location: `src/daniel_lightrag_mcp/cli.py`, `src/daniel_lightrag_mcp/__main__.py`
- Contains: `cli()` function (setuptools entry point), `main()` delegated from server
- Depends on: Server layer
- Used by: OS process invocation via `daniel-lightrag-mcp` console script

**MCP Server Layer:**
- Purpose: Implements the Model Context Protocol over stdio; receives tool calls from an MCP host (e.g. Claude) and dispatches them to the client
- Location: `src/daniel_lightrag_mcp/server.py` (2,214 lines)
- Contains: `Server` singleton, `handle_list_tools()`, `handle_call_tool()`, argument validators, response serializers, `main()` coroutine
- Depends on: Client layer, Models layer, `mcp` SDK
- Used by: MCP hosts via stdio transport

**HTTP Client Layer:**
- Purpose: Makes authenticated async HTTP requests to the LightRAG REST API and returns typed response objects
- Location: `src/daniel_lightrag_mcp/client.py` (520 lines)
- Contains: `LightRAGClient` class, custom exception hierarchy, `_make_request()`, `_stream_request()`
- Depends on: Models layer, `httpx`
- Used by: Server layer only

**Models Layer:**
- Purpose: Shared typed contracts for all requests and responses
- Location: `src/daniel_lightrag_mcp/models.py` (424 lines)
- Contains: Pydantic v2 `BaseModel` subclasses for every API request and response shape, plus enums (`DocStatus`, `QueryMode`, `PipelineStatus`)
- Depends on: `pydantic`
- Used by: Both Server layer and Client layer

## Data Flow

**Standard Tool Call Flow:**

1. MCP host sends JSON-RPC `tools/call` over stdin
2. `mcp` SDK deserializes and calls `handle_call_tool(tool_name, arguments)`
3. Server validates `arguments` via `_validate_tool_arguments()`
4. Server lazily initializes `LightRAGClient` from env vars (`LIGHTRAG_BASE_URL`, `LIGHTRAG_API_KEY`, `LIGHTRAG_TIMEOUT`) if not yet created
5. Server dispatches to the appropriate `LightRAGClient` method
6. Client constructs a Pydantic request model, calls `_make_request()` via `httpx.AsyncClient`
7. Client deserializes JSON response into a Pydantic response model and returns it
8. Server serializes the response model via `_create_success_response()` → `json.dumps(result.model_dump())`
9. Server returns `{"content": [{"type": "text", "text": "<json>"}]}` dict to MCP SDK
10. MCP SDK writes JSON-RPC response to stdout

**Streaming Tool Call Flow (query_text_stream):**

1. Same steps 1–5 as above
2. Client calls `_stream_request()` which opens an `httpx` streaming context
3. Each text chunk is yielded via `async for chunk in response.aiter_text()`
4. Server collects chunks and returns a single success response with concatenated content

**Error Flow:**

1. Any exception in steps 5–8 is caught by the `try/except` in `handle_call_tool`
2. Exception is passed to `_create_error_response()` which serializes to `{"content": [...], "isError": True}`
3. `LightRAGError` subclasses carry `status_code` and `response_data` for richer error context

**State Management:**
- One module-level global: `lightrag_client: Optional[LightRAGClient]` in `server.py`
- Client is created once and reused for the lifetime of the process
- No in-process caching or state beyond the client handle

## Key Abstractions

**LightRAGClient:**
- Purpose: Encapsulates all HTTP communication with the LightRAG API
- Location: `src/daniel_lightrag_mcp/client.py`
- Pattern: Async context manager (`__aenter__`/`__aexit__`) with `httpx.AsyncClient` as transport; methods grouped by domain (Document Management, Query, Knowledge Graph, System Management)

**LightRAGError hierarchy:**
- Purpose: Typed exceptions that map HTTP status codes to meaningful categories
- Location: `src/daniel_lightrag_mcp/client.py` lines 25–71
- Classes: `LightRAGError` (base), `LightRAGConnectionError`, `LightRAGAuthError`, `LightRAGValidationError`, `LightRAGAPIError`, `LightRAGTimeoutError`, `LightRAGServerError`
- Pattern: Each subclass carries `message`, `status_code`, `response_data`; `to_dict()` for serialization

**MCP Tool Registry:**
- Purpose: Declares the 22 tools exposed to MCP hosts, each with a JSON Schema `inputSchema`
- Location: `src/daniel_lightrag_mcp/server.py` — `handle_list_tools()` starting at line 288
- Pattern: Four logical groups appended to a list: Document Management (7 active), Query (2), Knowledge Graph (6 active), System Management (4 active). Several tools are commented out.

**Pydantic Models:**
- Purpose: Validate inputs and deserialize outputs at both the server-to-client and client-to-API boundaries
- Location: `src/daniel_lightrag_mcp/models.py`
- Pattern: Request models use `Field(..., description=...)` with constraints; response models use `Optional` fields extensively for partial-response tolerance

## Entry Points

**Console Script:**
- Location: `src/daniel_lightrag_mcp/cli.py` — `cli()` function
- Triggers: `daniel-lightrag-mcp` command (registered in `pyproject.toml` `[project.scripts]`)
- Responsibilities: Wraps `asyncio.run(main())`, catches `KeyboardInterrupt` and generic exceptions with `sys.exit`

**Module Execution:**
- Location: `src/daniel_lightrag_mcp/__main__.py`
- Triggers: `python -m daniel_lightrag_mcp`
- Responsibilities: Delegates to `cli.main()`

**Async Main:**
- Location: `src/daniel_lightrag_mcp/server.py` — `main()` coroutine (~line 2100)
- Triggers: Called by `cli()`
- Responsibilities: Opens `stdio_server()` context, configures server capabilities and `InitializationOptions`, enters `server.run()` loop; cleans up `lightrag_client` on exit

## Error Handling

**Strategy:** Catch-and-convert at every layer boundary; never let raw exceptions propagate to the MCP protocol wire.

**Patterns:**
- `LightRAGClient._make_request()` catches `httpx.*` exceptions and re-raises as typed `LightRAGError` subclasses
- `handle_call_tool()` wraps each tool dispatch in `try/except Exception` and calls `_create_error_response()` to produce a valid MCP error response dict
- HTTP status codes 400/401/403/404/408/422/429/5xx each map to specific exception subclasses via `_map_http_error()`
- `_validate_tool_arguments()` raises `LightRAGValidationError` before any network call for missing required args

## Cross-Cutting Concerns

**Logging:** Standard library `logging` with a structured format `%(asctime)s - %(name)s - %(levelname)s - [%(funcName)s:%(lineno)d] - %(message)s`; extremely verbose INFO/ERROR logging throughout `server.py` (including separator lines and step-by-step trace output — see CONCERNS.md)

**Validation:** Two-stage — `_validate_tool_arguments()` in the server for required-field presence and type checks, then Pydantic model construction in the client for schema enforcement

**Authentication:** Optional API key passed as `X-API-Key` header; configured via `LIGHTRAG_API_KEY` env var; absent key means unauthenticated requests

---

*Architecture analysis: 2026-04-13*
Loading