This document describes the architecture, conventions, and patterns you need to know when working on docs-mcp.
docs-mcp is an MCP server (stdio transport) that provides AI agents with structured access to the Rust crate ecosystem. It is built with:
- rmcp 0.16 — MCP server framework with
#[tool]/#[tool_router]/#[tool_handler]macros - tokio — async runtime (multi-thread)
- reqwest + reqwest-middleware — HTTP with rate limiting
- serde_json — all rustdoc parsing works against
serde_json::Valuetrees - zstd — docs.rs serves rustdoc JSON as
.json.zstcompressed files
- crates.io sparse index (
sparse_index/) —https://index.crates.io/{prefix}/{name}— one line of NDJSON per version. Used for feature flags, MSRV, dep counts. Fast and no rate limit. - crates.io REST API (
cratesio/) —https://crates.io/api/v1/crates/{name}— richer metadata (download counts, full version history, dependency details). Rate-limited to 1 req/s. - docs.rs rustdoc JSON (
docsrs/) —https://docs.rs/crate/{name}/{version}/json— full API structure. Served as.json.zst. This is the most data-intensive source and is aggressively cached.
These field types differ from naive assumptions — see src/docsrs/types.rs:
| Field | Type | Notes |
|---|---|---|
RustdocJson.root |
serde_json::Value |
Integer in JSON (e.g. 98), not a string |
Item.id |
serde_json::Value |
Integer |
Item.attrs |
Vec<serde_json::Value> |
Each attr is {"other": "#[cfg(...)]"} — use Item::attr_strings() |
Item.links |
HashMap<String, serde_json::Value> |
Values are integer item IDs |
PathEntry.kind |
String |
"enum", "function", "struct", etc. |
Shared across all tool handlers. Contains:
client: reqwest_middleware::ClientWithMiddleware— rate-limited HTTP clientcache: Arc<Cache>— disk cacheresolve_version(name, version_hint)— resolvesNoneto latest stable via sparse index
The ServerHandler::get_info() method in src/server.rs returns an instructions string that MCP clients surface to agents before tool selection. Keep it short — one paragraph plus the three workflow lines. It currently reads:
DISCOVERY WORKFLOW: crate_list → crate_get → crate_readme_get
UNDERSTANDING WORKFLOW: crate_docs_get → crate_item_list → crate_item_get → crate_impls_list
DUE DILIGENCE: crate_versions_list → crate_downloads_get → crate_dependents_list → crate_dependencies_list
If you add a new tool category, add a corresponding workflow line here. Individual tool descriptions live on each #[tool(description = "...")] attribute in server.rs and are the primary documentation surface for agents — keep them precise and action-oriented.
- Create
src/tools/my_tool.rswithMyToolParams(deriveDeserialize,JsonSchema) andpub async fn execute(state: &AppState, params: MyToolParams) -> Result<CallToolResult, ErrorData>. - Add to
src/tools/mod.rs:pub mod my_tool; - Register in
src/server.rs:- Add
use crate::tools::my_tool::{self, MyToolParams}; - Add a
#[tool(description = "...")]method toDocsMcpServerthat callsmy_tool::execute(&self.state, params).await
- Add
- Write tests in
tests/or inline.
Converts a rustdoc JSON type node to a human-readable string. Key cases:
"primitive"→ bare name"resolved_path"→Name<args>orName"borrowed_ref"→&'a Tor&T"raw_pointer"→*const T/*mut T"slice"→[T]"array"→[T; N]"tuple"→(A, B, C)"impl_trait"→impl Trait + Trait2"dyn_trait"→dyn Trait + Trait2"qualified_path"→T::Name(shorthand when no trait) or<T as Trait>::Name"generic"→ bare generic name
Builds a complete function signature string including generics, params, return type, and where clauses. Handles:
- Async (
async fn) selfreceiver normalization (always&selfor&mut self, not raw pointer form)impl Traitin parameter position (synthesizes param names)- Lifetime params (
'a) - Const generic params (
const N: usize)
Builds <T: Bound, 'a, const N: usize> string for struct/enum/trait definitions. Looks up generics from item.inner.{kind}.generics. Returns empty string if no generics.
Builds a recursive module tree. Each ModuleNode has:
path: String— fully qualified module pathitem_counts: HashMap<String, usize>— kind → count (excludes"use"/"import"kinds)items: Vec<ItemSummary>— populated wheninclude_itemsis requestedchildren: Vec<ModuleNode>
Fuzzy search over item names. Scoring:
- Exact match: 100
- Starts with query: 80
- Contains query: 60
- Word boundary match: 70
- Substring in path segments: 30
Results include: path, kind, signature, doc_summary (first sentence of doc comment), feature_requirements, score.
Use DocsError from src/error.rs internally. At tool boundaries, convert to ErrorData:
ErrorData::internal_error(msg, None)— unexpected failuresErrorData::invalid_params(msg, None)— user error (bad crate name, no docs build found)
The DocsNotFound variant should produce invalid_params errors (not internal_error) since it's expected when docs.rs hasn't built the latest version yet.
Tests live in tests/:
tests/integration.rs— integration tests that make real network calls (ignored by default, run withcargo test -- --ignored)tests/unit_docsrs_parser.rs— unit tests for parser functions using JSON fixturestests/lib_tests.rs— unit tests for tool-level logic
Fixtures:
tests/fixtures/clap_4.5.59.json— 23 items (modules and use-reexports only). Use forbuild_module_treeand root doc tests.tests/fixtures/rmcp_0.16.0.json— 13771 items including 3003 functions. Use forfunction_signature,type_to_string,format_generics, and kind-specific tests.
Convention: Add a test for every bug you fix. Name tests descriptively after what they assert (not the bug number).
src/cache.rs — SHA-256 keyed, 1-day TTL, stored in platform cache dir. Cache entries are raw bytes. The cache is checked before every HTTP request.
To bust the cache during development, delete the cache directory or change the URL format slightly.
- Re-exported items: When a crate re-exports an item from another crate, the item ID appears in
pathsbut notindex.crate_item_getreturns a helpful error in this case directing users to look up the defining crate. - docs.rs build lag: The latest version of a crate may not have a docs.rs build yet.
crate_docs_getfalls back to README in this case;crate_item_listandcrate_item_getreturn aninvalid_paramserror with guidance. - Synthetic trait impls:
Send,Sync,Unpin, etc. are auto-traits that rustdoc marksis_synthetic: true. These are filtered fromcrate_item_gettrait impl lists andcrate_impls_listtype-path queries to reduce noise. - Rustdoc JSON format version: This server targets format version 57. Future rustdoc releases may change field shapes; the
RustdocJson.format_versionfield can be used for version checks.
The crates.io REST API client is rate-limited to 1 request/second using the governor crate. The sparse index and docs.rs have no artificial rate limit in this server (but be a good citizen).
# Debug build
cargo build
# Release build (required when running as MCP server, since the binary is locked while running)
cargo build --release
# Run tests
cargo test
# Run with logging
RUST_LOG=debug cargo runImportant: If you're running the MCP server and want to rebuild, you must stop the server first — the running process holds the binary file locked on Windows.