Skip to content

Commit 5c6e9da

Browse files
committed
docs: add ARCHITECTURE.md detailing project structure
Introduces a comprehensive ARCHITECTURE.md file outlining the project's motivation, purpose, architecture, workflow, and detailed descriptions of key files and source modules. Also includes minor related refactoring in doc_loader.rs for clarity in documentation processing logic.
1 parent 96d5a1a commit 5c6e9da

File tree

2 files changed

+294
-136
lines changed

2 files changed

+294
-136
lines changed

ARCHITECTURE.md

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# Rust Docs MCP Server Architecture
2+
3+
This document provides an overview of the project's architecture, motivation, and the purpose of its key files.
4+
5+
## Project Overview
6+
7+
### Motivation
8+
9+
AI coding assistants often struggle with the latest APIs of rapidly evolving libraries, particularly in ecosystems like Rust. Their training data has cutoffs, leading to outdated or incorrect suggestions. This project aims to bridge this gap by providing a dedicated, up-to-date knowledge source for specific Rust crates.
10+
11+
### Purpose
12+
13+
The `rustdocs-mcp-server` acts as a specialized Model Context Protocol (MCP) server. Each instance focuses on a single Rust crate (optionally with specific features enabled). It exposes an MCP tool (`query_rust_docs`) that allows an LLM-based coding assistant (like Roo Code, Cursor, etc.) to ask natural language questions about the crate's API or usage. The server retrieves relevant information directly from the crate's *current* documentation and uses another LLM call to synthesize an answer based *only* on that context, significantly improving the accuracy and relevance of the assistant's code generation related to that crate.
14+
15+
### Architecture and Workflow
16+
17+
The application follows these steps:
18+
19+
1. **Initialization (`src/main.rs`):**
20+
* The server is launched via the command line, parsing arguments (`clap`) for the target crate's Package ID Specification (e.g., `serde@^1.0`) and optional features (`-F feat1,feat2`).
21+
* It determines the appropriate cache directory (XDG on Linux/macOS, standard cache dir on Windows) based on the crate name, version requirement, and a hash of the requested features.
22+
2. **Cache Check (`src/main.rs`):**
23+
* It checks if pre-computed documentation content and embeddings exist in the cache file (`embeddings.bin`) for the specific crate/version/features combination.
24+
* If the cache exists and is valid (`bincode` decodable), it loads the data directly.
25+
3. **Documentation Generation & Processing (Cache Miss - `src/doc_loader.rs`, `src/main.rs`):**
26+
* If the cache is invalid or missing:
27+
* A temporary Rust project is created, depending only on the target crate with the specified features enabled in its `Cargo.toml`.
28+
* The `cargo` crate's API (`ops::doc`) is used to generate the crate's HTML documentation within the temporary project's `target/doc` directory.
29+
* The correct documentation subdirectory (e.g., `target/doc/serde/`) is located by searching for `index.html`.
30+
* The generated HTML and Markdown files are walked. `scraper` is used to parse HTML and extract text content from the main content area (`section#main-content.content`). Raw content is used for Markdown files and source code view files (`.rs.html`). Duplicate files (like multiple `index.html` files) are handled, prioritizing the root `index.html` and keeping the largest version of other duplicates.
31+
4. **Embedding Generation (Cache Miss - `src/embeddings.rs`, `src/main.rs`):**
32+
* The extracted text content for each documentation file is sent to the OpenAI API (`async-openai`) to generate vector embeddings using the `text-embedding-3-small` model. Token limits are checked using `tiktoken-rs`.
33+
5. **Caching (Cache Miss - `src/main.rs`):**
34+
* The extracted document content (`Document` struct) and the generated embeddings (`Array1<f32>`) are combined into `CachedDocumentEmbedding` structs and serialized using `bincode` into the `embeddings.bin` cache file in the previously determined path.
35+
6. **Server Startup (`src/main.rs`, `src/server.rs`):**
36+
* An instance of `RustDocsServer` is created, holding the crate name, documents, and embeddings (loaded from cache or freshly generated).
37+
* The MCP server is started using the `rmcp` library, listening for connections over standard input/output (stdio).
38+
7. **Query Handling (`src/server.rs`):**
39+
* An MCP client (e.g., Roo Code) connects and calls the `query_rust_docs` tool with a user's question.
40+
* The server generates an embedding for the question using OpenAI (`text-embedding-3-small`).
41+
* It calculates the cosine similarity between the question embedding and all cached document embeddings to find the most relevant document chunk.
42+
* The content of the best-matching document and the original question are sent to the OpenAI chat completion API (`gpt-4o-mini-2024-07-18`).
43+
* The LLM is prompted to answer the question based *only* on the provided context.
44+
* The LLM's generated answer is sent back to the MCP client as the result of the tool call.
45+
* Logging messages are sent asynchronously via MCP notifications throughout the process.
46+
47+
## File Descriptions
48+
49+
### `Cargo.toml`
50+
51+
* **Purpose:** Defines the Rust package metadata, dependencies, and build configurations.
52+
* **Key Contents:**
53+
* Package name (`rustdocs_mcp_server`), version, edition.
54+
* Core Dependencies:
55+
* `rmcp`: For MCP server implementation, communication, and macros.
56+
* `tokio`: Asynchronous runtime.
57+
* `clap`: Command-line argument parsing.
58+
* `async-openai`: Interacting with OpenAI APIs (embeddings, chat).
59+
* `cargo`: Programmatic interaction with Cargo (specifically `cargo doc`).
60+
* `scraper`: HTML parsing for content extraction.
61+
* `ndarray`: Vector operations (for embeddings).
62+
* `bincode`: Binary serialization/deserialization for caching.
63+
* `tiktoken-rs`: Token counting for OpenAI models.
64+
* `serde`, `serde_json`: Data serialization/deserialization.
65+
* `thiserror`: Error handling boilerplate.
66+
* `tempfile`: Creating temporary directories for `cargo doc`.
67+
* `walkdir`: Traversing documentation directories.
68+
* `xdg` (Linux/macOS), `dirs` (Windows): Locating appropriate cache directories.
69+
* `dotenvy`: Loading `.env` files.
70+
* Build Profiles: Optimized release profile (`opt-level = "z"`, LTO, strip) for smaller binary size.
71+
72+
### `README.md`
73+
74+
* **Purpose:** Provides a user-facing overview of the project, including its motivation, features, installation instructions, usage examples, client configuration guidance (Roo Code, Claude Desktop), caching details, and a high-level explanation of how it works.
75+
* **Key Contents:**
76+
* Motivation: Addressing LLM limitations with Rust crate APIs.
77+
* Features: Targeted docs, feature support, semantic search, LLM summarization, caching, MCP integration.
78+
* Installation: Recommends pre-compiled binaries, provides build-from-source steps.
79+
* Usage: Explains how to run the server, the importance of the Package ID Spec, feature flags (`-F`), and the initial run behavior (caching).
80+
* MCP Interaction: Details the `query_rust_docs` tool (schema, example) and the `crate://<crate_name>` resource.
81+
* Client Configuration Examples: Snippets for `mcp_settings.json` (Roo Code) and Claude Desktop settings.
82+
* Caching Explanation: Location (XDG/Windows), format (`bincode`), regeneration logic.
83+
* How it Works: Step-by-step description of the server's process flow.
84+
* License (MIT) and Sponsorship information.
85+
86+
### `CHANGELOG.md`
87+
88+
* **Purpose:** Tracks the history of changes, features, and bug fixes across different versions of the project.
89+
* **Key Contents:** Version numbers, release dates, and categorized lists of changes (Features, Bug Fixes, Code Refactoring, etc.) for each version. Useful for understanding project evolution but not directly involved in runtime execution.
90+
91+
### `src/main.rs`
92+
93+
* **Purpose:** Serves as the main entry point for the application. Orchestrates the overall process of argument parsing, cache handling, documentation/embedding generation (if needed), and server initialization/startup.
94+
* **Key Functionality:**
95+
* Uses `clap` to parse command-line arguments (package spec, features).
96+
* Parses the `PackageIdSpec` to extract crate name and version requirement.
97+
* Determines the platform-specific cache directory path using `xdg` or `dirs`, incorporating crate name, version, and a hash of features.
98+
* Attempts to load cached `CachedDocumentEmbedding` data using `bincode`.
99+
* If cache loading fails or the file doesn't exist:
100+
* Calls `doc_loader::load_documents` to generate docs and extract content.
101+
* Calls `embeddings::generate_embeddings` to get embeddings from OpenAI.
102+
* Calculates estimated OpenAI cost.
103+
* Saves the combined documents and embeddings to the cache file using `bincode`.
104+
* Initializes the global `OPENAI_CLIENT` static variable.
105+
* Instantiates the `RustDocsServer`.
106+
* Starts the MCP server using `rmcp::ServiceExt::serve` with the `stdio` transport.
107+
* Waits for the server to complete using `server_handle.waiting()`.
108+
109+
### `src/doc_loader.rs`
110+
111+
* **Purpose:** Handles the generation and processing of Rust documentation for a specified crate.
112+
* **Key Functionality:**
113+
* `load_documents`:
114+
* Creates a temporary directory (`tempfile`).
115+
* Generates a temporary `Cargo.toml` specifying the target crate and requested features.
116+
* Creates a minimal `src/lib.rs` to satisfy Cargo.
117+
* Uses the `cargo` crate's API (`ops::doc`, `CompileOptions`, `DocOptions`, `Workspace`) to run `cargo doc` within the temporary environment, targeting only the specified crate.
118+
* Calls `find_documentation_path` to locate the correct output directory within `target/doc`.
119+
* Calls `process_documentation_directory` to extract content.
120+
* `find_documentation_path`: Locates the specific subdirectory within the base `doc` path that contains the generated documentation (handles cases with multiple directories, hyphens/underscores, disambiguates using `index.html` title tag).
121+
* `process_documentation_directory`:
122+
* Walks the located documentation directory (`walkdir`).
123+
* Filters for `.html` and `.md` files.
124+
* Groups files by basename to handle duplicates (e.g., multiple `struct.MyStruct.html`).
125+
* Filters duplicates, keeping the root `index.html`/`README.md` and the largest HTML file among other duplicates.
126+
* For each final file path:
127+
* Reads the file content.
128+
* If Markdown (`.md`) or source view (`.rs.html`), uses the raw content.
129+
* If other HTML, uses `scraper` to parse the HTML and extract text from the `section#main-content.content` element.
130+
* Returns a `Vec<Document>` containing the relative path and extracted/raw content.
131+
* Defines `Document` struct (path, content) and `DocLoaderError` enum.
132+
133+
### `src/embeddings.rs`
134+
135+
* **Purpose:** Manages interactions with the OpenAI Embeddings API and provides embedding-related utilities.
136+
* **Key Functionality:**
137+
* Defines `CachedDocumentEmbedding` struct for serialization (includes path, content, vector).
138+
* `generate_embeddings`:
139+
* Takes a slice of `Document` structs.
140+
* Uses `futures::stream` for concurrent requests to the OpenAI API (up to `CONCURRENCY_LIMIT`).
141+
* Uses `tiktoken-rs` (`cl100k_base`) to check token count against `TOKEN_LIMIT` before sending requests, skipping documents that exceed the limit.
142+
* Calls the OpenAI embeddings endpoint (`text-embedding-3-small`) via `async-openai`.
143+
* Collects results, handling potential errors and skipped documents.
144+
* Returns a vector of `(String, Array1<f32>)` tuples (path, embedding vector) and the total number of tokens processed.
145+
* `cosine_similarity`: Calculates the cosine similarity between two `ndarray::ArrayView1<f32>` vectors.
146+
* `OPENAI_CLIENT`: A `OnceLock` static variable to hold the initialized `async_openai::Client`.
147+
148+
### `src/error.rs`
149+
150+
* **Purpose:** Defines a unified error type for the application using `thiserror`.
151+
* **Key Contents:**
152+
* `ServerError` enum: Consolidates potential errors from various sources.
153+
* Variants include: `MissingEnvVar`, `Config`, `Mcp` (from `rmcp::ServiceError`), `Io`, `DocLoader` (from `DocLoaderError`), `OpenAI` (from `async_openai::error::OpenAIError`), `Json` (from `serde_json::Error`), `Tiktoken`, `Xdg`, `McpRuntime`.
154+
* Uses `#[from]` attributes to automatically convert underlying error types.
155+
156+
### `src/server.rs`
157+
158+
* **Purpose:** Implements the core MCP server logic and the `query_rust_docs` tool.
159+
* **Key Functionality:**
160+
* Defines `RustDocsServer` struct: Holds shared state (`Arc`s for crate name, documents, embeddings, peer, startup message).
161+
* Implements `ServerHandler` trait for `RustDocsServer`:
162+
* `get_info`: Provides server capabilities (tools, logging) and instructions.
163+
* `list_resources`, `read_resource`: Basic implementation to expose the crate name as a resource (`crate://<crate_name>`).
164+
* Placeholder implementations for `list_prompts`, `get_prompt`, `list_resource_templates`.
165+
* Uses `rmcp::tool` macro (`#[tool(tool_box)]`, `#[tool(...)]`) to define the `query_rust_docs` tool:
166+
* Takes `QueryRustDocsArgs` (question) as input.
167+
* Sends the initial startup message via MCP log notification on the first call.
168+
* Gets the embedding for the input question using `OPENAI_CLIENT`.
169+
* Iterates through cached document embeddings, calculating `cosine_similarity` to find the best match.
170+
* Retrieves the content of the best-matching document.
171+
* Constructs system and user prompts for the OpenAI chat completion API (`gpt-4o-mini-2024-07-18`), providing the document context and the user's question.
172+
* Calls the chat API via `OPENAI_CLIENT`.
173+
* Formats the LLM's response and returns it as a successful `CallToolResult` with text content.
174+
* `send_log`: Helper method to send logging messages back to the client via MCP `logging/message` notifications.

0 commit comments

Comments
 (0)