mcp-rustdoc 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +391 -0
  3. package/dist/index.js +576 -0
  4. package/package.json +56 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 kieled
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,391 @@
1
+ # mcp-rustdoc
2
+
3
+ An MCP server that gives AI assistants deep access to the Rust ecosystem. It scrapes docs.rs with surgical DOM extraction (cheerio) and queries the crates.io API, exposing six tools that cover everything from high-level crate overviews to individual method signatures, feature gates, trait impls, and code examples.
4
+
5
+ ## Tools
6
+
7
+ | Tool | What it returns |
8
+ |---|---|
9
+ | `get_crate_metadata` | Version, features, default features, optional/required deps, links (crates.io API) |
10
+ | `get_crate_brief` | One-shot bundle: metadata + overview + re-exports + module list + focused module items |
11
+ | `lookup_crate_docs` | Crate overview documentation, version, sections, re-exports |
12
+ | `get_crate_items` | Items in a module with types, feature gates, and descriptions |
13
+ | `lookup_crate_item` | Item detail: signature, docs, methods, variants, optionally trait impls + examples |
14
+ | `search_crate` | Ranked symbol search (exact > prefix > substring) with canonical paths |
15
+
16
+ Every tool accepts an optional `version` parameter to pin a specific crate version instead of `latest`.
17
+
18
+ ## Install
19
+
20
+ No clone needed. Just configure your AI coding assistant with `npx`:
21
+
22
+ ```
23
+ npx -y mcp-rustdoc
24
+ ```
25
+
26
+ ### Claude Code
27
+
28
+ ```bash
29
+ claude mcp add mcp-rustdoc -- npx -y mcp-rustdoc
30
+ ```
31
+
32
+ Or add to your project's `.mcp.json`:
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "mcp-rustdoc": {
38
+ "type": "stdio",
39
+ "command": "npx",
40
+ "args": ["-y", "mcp-rustdoc"]
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ ### Gemini CLI
47
+
48
+ Add to `~/.gemini/settings.json` (global) or `.gemini/settings.json` (project):
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "mcp-rustdoc": {
54
+ "command": "npx",
55
+ "args": ["-y", "mcp-rustdoc"]
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ ### OpenAI Codex CLI
62
+
63
+ ```bash
64
+ codex mcp add mcp-rustdoc -- npx -y mcp-rustdoc
65
+ ```
66
+
67
+ Or add to `~/.codex/config.toml`:
68
+
69
+ ```toml
70
+ [mcp_servers.mcp-rustdoc]
71
+ command = "npx"
72
+ args = ["-y", "mcp-rustdoc"]
73
+ ```
74
+
75
+ ### Claude Desktop
76
+
77
+ Add to your `claude_desktop_config.json`:
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "mcp-rustdoc": {
83
+ "command": "npx",
84
+ "args": ["-y", "mcp-rustdoc"]
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ ### MCP Inspector (testing)
91
+
92
+ ```bash
93
+ npx @modelcontextprotocol/inspector -- npx -y mcp-rustdoc
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Development
99
+
100
+ ```bash
101
+ git clone https://github.com/kieled/mcp-rustdoc.git
102
+ cd mcp-rustdoc
103
+ bun install
104
+ ```
105
+
106
+ ### Run with bun (no build step)
107
+
108
+ ```bash
109
+ bun run dev
110
+ ```
111
+
112
+ ### Build with Vite and run with Node.js
113
+
114
+ ```bash
115
+ bun run build # vite build → dist/index.js (single bundled ESM file)
116
+ node dist/index.js
117
+ ```
118
+
119
+ ### Build with tsc
120
+
121
+ ```bash
122
+ bun run build:tsc # tsc → dist/ (one .js per source file)
123
+ node dist/index.js
124
+ ```
125
+
126
+ ### Type check only
127
+
128
+ ```bash
129
+ bun run typecheck # tsc --noEmit
130
+ ```
131
+
132
+ ### Publish
133
+
134
+ ```bash
135
+ npm publish # runs vite build via prepublishOnly, then publishes
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Tool reference
141
+
142
+ ### `get_crate_metadata`
143
+
144
+ Fetches structured metadata from the crates.io API.
145
+
146
+ | Parameter | Type | Required | Description |
147
+ |---|---|---|---|
148
+ | `crateName` | string | yes | Crate name |
149
+ | `version` | string | no | Pinned version |
150
+
151
+ Returns: version, description, links (docs/repo/crates.io), license, download count, full feature list with activations, optional deps (feature-gated), required deps.
152
+
153
+ ```
154
+ > get_crate_metadata({ crateName: "tokio" })
155
+
156
+ # tokio v1.49.0
157
+
158
+ An event-driven, non-blocking I/O platform for writing asynchronous applications...
159
+
160
+ ## Links
161
+ docs: https://docs.rs/tokio
162
+ repo: https://github.com/tokio-rs/tokio
163
+ license: MIT
164
+ downloads: 312,456,789
165
+
166
+ ## Features
167
+ default = [macros, rt-multi-thread]
168
+ fs = []
169
+ full = [fs, io-util, io-std, macros, net, ...]
170
+ io-util = [bytes]
171
+ ...
172
+
173
+ ## Optional Dependencies
174
+ bytes ^1 (feature-gated)
175
+ ...
176
+ ```
177
+
178
+ ---
179
+
180
+ ### `get_crate_brief`
181
+
182
+ Single call to bootstrap context for a crate. Combines metadata, overview docs, re-exports, module list, and optionally expands focused modules.
183
+
184
+ | Parameter | Type | Required | Description |
185
+ |---|---|---|---|
186
+ | `crateName` | string | yes | Crate name |
187
+ | `version` | string | no | Pinned version |
188
+ | `focusModules` | string | no | Comma-separated modules to expand (e.g. `"sync,task"`) |
189
+
190
+ ```
191
+ > get_crate_brief({ crateName: "tokio", focusModules: "sync,task" })
192
+
193
+ # tokio v1.49.0
194
+ ...
195
+ ## Features
196
+ default = [macros, rt-multi-thread]
197
+ all: bytes, fs, full, io-std, io-util, ...
198
+
199
+ ## Overview
200
+ [truncated crate doc]
201
+
202
+ ## Re-exports
203
+ pub use task::spawn;
204
+ ...
205
+
206
+ ## Modules
207
+ fs io macros net runtime signal sync task time
208
+
209
+ ## Focus: tokio::sync
210
+ [struct] Barrier — ...
211
+ [struct] Mutex [sync] — ...
212
+ [struct] Notify [sync] — ...
213
+ ...
214
+
215
+ ## Focus: tokio::task
216
+ [fn] spawn — ...
217
+ [struct] JoinHandle — ...
218
+ ...
219
+ ```
220
+
221
+ ---
222
+
223
+ ### `lookup_crate_docs`
224
+
225
+ Fetches the main documentation page for a crate.
226
+
227
+ | Parameter | Type | Required | Description |
228
+ |---|---|---|---|
229
+ | `crateName` | string | yes | Crate name |
230
+ | `version` | string | no | Pinned version |
231
+
232
+ Returns: crate version, overview documentation text, re-exports, and section list with item counts.
233
+
234
+ ---
235
+
236
+ ### `get_crate_items`
237
+
238
+ Lists all public items in a crate root or specific module.
239
+
240
+ | Parameter | Type | Required | Description |
241
+ |---|---|---|---|
242
+ | `crateName` | string | yes | Crate name |
243
+ | `modulePath` | string | no | Dot-separated path (e.g. `"sync"`, `"io.util"`) |
244
+ | `itemType` | enum | no | Filter: `mod` `struct` `enum` `trait` `fn` `macro` `type` `constant` `static` `union` `attr` `derive` |
245
+ | `version` | string | no | Pinned version |
246
+
247
+ Each item includes its type, name, feature gate (if any), and short description.
248
+
249
+ ```
250
+ > get_crate_items({ crateName: "tokio", modulePath: "sync", itemType: "struct" })
251
+
252
+ # Items in tokio::sync [struct]
253
+ [struct] Barrier — ...
254
+ [struct] Mutex [feature: sync] — ...
255
+ [struct] Notify [feature: sync] — ...
256
+ [struct] OwnedMutexGuard [feature: sync] — ...
257
+ ...
258
+ ```
259
+
260
+ ---
261
+
262
+ ### `lookup_crate_item`
263
+
264
+ Retrieves full documentation for a single item.
265
+
266
+ | Parameter | Type | Required | Description |
267
+ |---|---|---|---|
268
+ | `crateName` | string | yes | Crate name |
269
+ | `itemType` | enum | yes | Item type (see `get_crate_items`) |
270
+ | `itemName` | string | yes | Item name (e.g. `"Mutex"`, `"spawn"`) |
271
+ | `modulePath` | string | no | Dot-separated module path |
272
+ | `version` | string | no | Pinned version |
273
+ | `includeImpls` | boolean | no | Include trait implementation list |
274
+ | `includeExamples` | boolean | no | Include code examples |
275
+
276
+ Returns: feature gate (if any), type signature, documentation text, methods list, enum variants, required/provided trait methods. Optionally includes trait implementations and code examples.
277
+
278
+ ```
279
+ > lookup_crate_item({
280
+ crateName: "tokio",
281
+ itemType: "struct",
282
+ itemName: "Mutex",
283
+ modulePath: "sync",
284
+ includeImpls: true
285
+ })
286
+
287
+ # struct tokio::sync::Mutex
288
+ > Available on crate feature `sync` only.
289
+
290
+ ## Signature
291
+ pub struct Mutex<T: ?Sized> { ... }
292
+
293
+ ## Documentation
294
+ An asynchronous Mutex...
295
+
296
+ ## Methods (12)
297
+ pub fn new(t: T) -> Mutex<T>
298
+ pub fn lock(&self) -> impl Future<Output = MutexGuard<'_, T>>
299
+ pub fn try_lock(&self) -> Result<MutexGuard<'_, T>, TryLockError>
300
+ ...
301
+
302
+ ## Trait Implementations (15)
303
+ impl<T: ?Sized + Debug> Debug for Mutex<T>
304
+ impl<T> Default for Mutex<T>
305
+ impl<T> From<T> for Mutex<T>
306
+ impl<T: ?Sized> Send for Mutex<T>
307
+ impl<T: ?Sized> Sync for Mutex<T>
308
+ ...
309
+ ```
310
+
311
+ ---
312
+
313
+ ### `search_crate`
314
+
315
+ Searches all items in a crate by name. Results are ranked: exact match on the bare item name scores highest, then prefix matches, then substring matches.
316
+
317
+ | Parameter | Type | Required | Description |
318
+ |---|---|---|---|
319
+ | `crateName` | string | yes | Crate name |
320
+ | `query` | string | yes | Search query (case-insensitive) |
321
+ | `version` | string | no | Pinned version |
322
+
323
+ ```
324
+ > search_crate({ crateName: "tokio", query: "Mutex" })
325
+
326
+ # "Mutex" in tokio — 6 matches
327
+
328
+ [struct] tokio::sync::Mutex
329
+ [struct] tokio::sync::MutexGuard
330
+ [struct] tokio::sync::OwnedMutexGuard
331
+ [struct] tokio::sync::MappedMutexGuard
332
+ [enum] tokio::sync::TryLockError
333
+ ...
334
+ ```
335
+
336
+ ---
337
+
338
+ ## Recommended workflows
339
+
340
+ ### Exploring a new crate
341
+
342
+ 1. `get_crate_brief` with `focusModules` targeting the modules you care about
343
+ 2. `search_crate` to find specific types or functions
344
+ 3. `lookup_crate_item` for detailed signatures and docs
345
+
346
+ ### Understanding feature flags
347
+
348
+ 1. `get_crate_metadata` to see all features and their activations
349
+ 2. `get_crate_items` to see which items require which features
350
+
351
+ ### Finding the right type
352
+
353
+ 1. `search_crate` with a keyword
354
+ 2. `lookup_crate_item` with `includeImpls: true` to see what traits it implements
355
+ 3. `lookup_crate_item` on referenced types to chase cross-links
356
+
357
+ ---
358
+
359
+ ## Architecture
360
+
361
+ ```
362
+ src/
363
+ index.ts Entry point — registers tools + prompt, starts stdio server
364
+ lib.ts Shared: URL builders, HTTP/DOM helpers, crates.io API, extractors
365
+ types/
366
+ html-to-text.d.ts Type declarations for html-to-text
367
+ tools/
368
+ shared.ts Shared Zod schemas (itemTypeEnum, versionParam)
369
+ lookup-docs.ts lookup_crate_docs
370
+ get-items.ts get_crate_items
371
+ lookup-item.ts lookup_crate_item
372
+ search.ts search_crate
373
+ crate-metadata.ts get_crate_metadata
374
+ crate-brief.ts get_crate_brief
375
+ ```
376
+
377
+ ### Data sources
378
+
379
+ - **docs.rs** — HTML pages parsed with cheerio for surgical DOM extraction (only the elements needed, not full-page conversion)
380
+ - **crates.io API** — JSON endpoints for metadata, features, and dependencies
381
+
382
+ ### Design decisions
383
+
384
+ - **cheerio over full-page text conversion** — Extracts only specific DOM elements (`.item-decl`, `.top-doc`, `.code-header`, `.stab.portability`) to minimize token usage
385
+ - **Ranked search** — `all.html` contains every public item; scoring by exact/prefix/substring gives better results than flat substring matching
386
+ - **Version parameter everywhere** — Agents working on projects with pinned dependencies need to read docs for specific versions
387
+ - **Optional sections** — `includeImpls` and `includeExamples` default to off so the base response stays compact; agents opt in when they need more detail
388
+
389
+ ## License
390
+
391
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,576 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import axios from "axios";
6
+ import { load } from "cheerio";
7
+ import { convert } from "html-to-text";
8
+ const DOCS_BASE = "https://docs.rs";
9
+ const CRATES_IO = "https://crates.io/api/v1";
10
+ const USER_AGENT = "mcp-rust-docs/2.0.0";
11
+ const MAX_DOC_LENGTH = 6e3;
12
+ const MAX_SEARCH_RESULTS = 100;
13
+ const SECTION_TO_TYPE = {
14
+ modules: "mod",
15
+ structs: "struct",
16
+ enums: "enum",
17
+ traits: "trait",
18
+ functions: "fn",
19
+ macros: "macro",
20
+ types: "type",
21
+ constants: "constant",
22
+ statics: "static",
23
+ unions: "union",
24
+ attributes: "attr",
25
+ derives: "derive",
26
+ reexports: "reexport"
27
+ };
28
+ const TYPE_FILE_PREFIX = {
29
+ struct: "struct.",
30
+ enum: "enum.",
31
+ trait: "trait.",
32
+ fn: "fn.",
33
+ macro: "macro.",
34
+ type: "type.",
35
+ constant: "constant.",
36
+ static: "static.",
37
+ union: "union.",
38
+ attr: "attr.",
39
+ derive: "derive."
40
+ };
41
+ function crateSlug(name) {
42
+ return name.replace(/-/g, "_");
43
+ }
44
+ function docsUrl(crate, path = "index.html", version = "latest") {
45
+ return `${DOCS_BASE}/${crate}/${version}/${crateSlug(crate)}/${path}`;
46
+ }
47
+ function modToUrlPrefix(modulePath) {
48
+ return modulePath ? modulePath.replace(/\./g, "/") + "/" : "";
49
+ }
50
+ function modToRustPrefix(modulePath) {
51
+ return modulePath ? modulePath.replace(/\./g, "::") + "::" : "";
52
+ }
53
+ async function fetchDom(url) {
54
+ const { data } = await axios.get(url, { timeout: 15e3 });
55
+ return load(data);
56
+ }
57
+ function cleanHtml(html) {
58
+ return convert(html, {
59
+ wordwrap: 120,
60
+ selectors: [
61
+ { selector: "a", options: { ignoreHref: true } },
62
+ { selector: "img", format: "skip" }
63
+ ]
64
+ }).trim();
65
+ }
66
+ function truncate(text, max) {
67
+ return text.length > max ? text.slice(0, max) + "\n\n[…truncated]" : text;
68
+ }
69
+ function textResult(text) {
70
+ return { content: [{ type: "text", text }] };
71
+ }
72
+ function errorResult(msg) {
73
+ return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
74
+ }
75
+ const cratesIoHeaders = { "User-Agent": USER_AGENT };
76
+ async function fetchCrateInfo(name) {
77
+ const { data } = await axios.get(`${CRATES_IO}/crates/${name}`, {
78
+ headers: cratesIoHeaders,
79
+ timeout: 1e4
80
+ });
81
+ const c = data.crate;
82
+ return {
83
+ name: c.name,
84
+ version: c.max_stable_version || c.max_version,
85
+ description: c.description,
86
+ documentation: c.documentation,
87
+ repository: c.repository,
88
+ downloads: c.downloads
89
+ };
90
+ }
91
+ async function fetchCrateVersionInfo(name, version) {
92
+ const { data } = await axios.get(`${CRATES_IO}/crates/${name}/${version}`, {
93
+ headers: cratesIoHeaders,
94
+ timeout: 1e4
95
+ });
96
+ const v = data.version;
97
+ const features = v.features ?? {};
98
+ return {
99
+ num: v.num,
100
+ features,
101
+ defaultFeatures: features["default"] ?? [],
102
+ yanked: v.yanked,
103
+ license: v.license
104
+ };
105
+ }
106
+ async function fetchCrateDeps(name, version) {
107
+ const { data } = await axios.get(`${CRATES_IO}/crates/${name}/${version}/dependencies`, {
108
+ headers: cratesIoHeaders,
109
+ timeout: 1e4
110
+ });
111
+ return (data.dependencies ?? []).map((d) => ({
112
+ name: d.crate_id,
113
+ req: d.req,
114
+ optional: d.optional,
115
+ kind: d.kind,
116
+ features: d.features ?? []
117
+ }));
118
+ }
119
+ function extractItemFeatureGate($) {
120
+ const gate = $(".item-info .stab.portability").first().text().trim();
121
+ return gate || null;
122
+ }
123
+ function extractExamples($) {
124
+ const examples = [];
125
+ $("div.example-wrap pre.rust").each((_, el) => {
126
+ const code = $(el).text().trim();
127
+ if (code) examples.push(code);
128
+ });
129
+ return examples;
130
+ }
131
+ function extractTraitImpls($) {
132
+ const impls = [];
133
+ $("#trait-implementations-list > details > summary h3.code-header").each((_, el) => {
134
+ impls.push($(el).text().trim());
135
+ });
136
+ return impls;
137
+ }
138
+ function extractReExports($) {
139
+ const reexports = [];
140
+ $("h2#reexports").next("dl.item-table").find("dt code").each((_, el) => {
141
+ reexports.push($(el).text().trim());
142
+ });
143
+ return reexports;
144
+ }
145
+ const itemTypeEnum = z.enum([
146
+ "mod",
147
+ "struct",
148
+ "enum",
149
+ "trait",
150
+ "fn",
151
+ "macro",
152
+ "type",
153
+ "constant",
154
+ "static",
155
+ "union",
156
+ "attr",
157
+ "derive"
158
+ ]);
159
+ const versionParam = z.string().optional().describe('Crate version (e.g. "1.49.0"). Defaults to latest.');
160
+ function register$5(server2) {
161
+ server2.tool(
162
+ "lookup_crate_docs",
163
+ "Fetch the main documentation for a Rust crate. Returns overview, version, sections, and re-exports.",
164
+ {
165
+ crateName: z.string().describe('Crate name (e.g. "tokio", "serde-json")'),
166
+ version: versionParam
167
+ },
168
+ // @ts-expect-error — MCP SDK deep type instantiation with Zod schemas
169
+ async ({ crateName, version }) => {
170
+ try {
171
+ const ver = version ?? "latest";
172
+ const url = docsUrl(crateName, "index.html", ver);
173
+ const $ = await fetchDom(url);
174
+ const pageVersion = $(".sidebar-crate .version").text().trim() || ver;
175
+ const doc = truncate(
176
+ cleanHtml($("details.toggle.top-doc").html() ?? ""),
177
+ MAX_DOC_LENGTH
178
+ );
179
+ const sections = [];
180
+ $("h2.section-header").each((_, el) => {
181
+ const id = $(el).attr("id") ?? "";
182
+ const count = $(el).next("dl.item-table").find("dt").length;
183
+ if (id && count) sections.push(` ${$(el).text().trim()} (${count})`);
184
+ });
185
+ const reexports = extractReExports($);
186
+ const parts = [`# ${crateName} v${pageVersion}`, url, "", doc];
187
+ if (reexports.length) {
188
+ parts.push("", "## Re-exports", ...reexports.map((r) => ` ${r}`));
189
+ }
190
+ parts.push("", "## Sections", ...sections);
191
+ return textResult(parts.join("\n"));
192
+ } catch (e) {
193
+ return errorResult(`Could not fetch docs for "${crateName}". ${e.message}`);
194
+ }
195
+ }
196
+ );
197
+ }
198
+ function register$4(server2) {
199
+ server2.tool(
200
+ "get_crate_items",
201
+ "List public items in a crate root or module. Returns names, types, feature gates, and short descriptions.",
202
+ {
203
+ crateName: z.string().describe("Crate name"),
204
+ modulePath: z.string().optional().describe('Dot-separated module path (e.g. "sync", "io.util"). Omit for crate root.'),
205
+ itemType: itemTypeEnum.optional().describe("Filter results to a single item type"),
206
+ version: versionParam
207
+ },
208
+ async ({ crateName, modulePath, itemType, version }) => {
209
+ try {
210
+ const ver = version ?? "latest";
211
+ const url = docsUrl(crateName, `${modToUrlPrefix(modulePath)}index.html`, ver);
212
+ const $ = await fetchDom(url);
213
+ const lines = [];
214
+ $("h2.section-header").each((_, h2) => {
215
+ const sectionId = $(h2).attr("id") ?? "";
216
+ const type = SECTION_TO_TYPE[sectionId] ?? sectionId;
217
+ if (itemType && type !== itemType) return;
218
+ $(h2).next("dl.item-table").find("dt").each((_2, dt) => {
219
+ const $dt = $(dt);
220
+ const name = $dt.find("a").first().text().trim();
221
+ const desc = $dt.next("dd").text().trim();
222
+ const gate = $dt.find(".stab.portability code").first().text().trim();
223
+ if (!name) return;
224
+ const tag = gate ? ` [feature: ${gate}]` : "";
225
+ lines.push(`[${type}] ${name}${tag} — ${desc}`);
226
+ });
227
+ });
228
+ const label = modulePath ? `${crateName}::${modulePath.replace(/\./g, "::")}` : crateName;
229
+ if (!lines.length) {
230
+ return textResult(
231
+ `No items found in ${label}${itemType ? ` (type: ${itemType})` : ""}.`
232
+ );
233
+ }
234
+ return textResult(
235
+ [`# Items in ${label}${itemType ? ` [${itemType}]` : ""}`, url, "", ...lines].join("\n")
236
+ );
237
+ } catch (e) {
238
+ return errorResult(`Could not list items. ${e.message}`);
239
+ }
240
+ }
241
+ );
242
+ }
243
+ function register$3(server2) {
244
+ server2.tool(
245
+ "lookup_crate_item",
246
+ "Get detailed documentation for a specific item. Returns signature, docs, feature gate, methods, trait impls, and optionally examples.",
247
+ {
248
+ crateName: z.string().describe("Crate name"),
249
+ itemType: itemTypeEnum.describe("Item type"),
250
+ itemName: z.string().describe('Item name (e.g. "Mutex", "spawn", "Serialize")'),
251
+ modulePath: z.string().optional().describe('Dot-separated module path (e.g. "sync"). Omit if at crate root.'),
252
+ version: versionParam,
253
+ includeExamples: z.boolean().optional().describe("Include code examples from the docs. Default: false."),
254
+ includeImpls: z.boolean().optional().describe("Include trait implementation list. Default: false.")
255
+ },
256
+ // @ts-expect-error — MCP SDK deep type instantiation with Zod schemas
257
+ async ({ crateName, itemType, itemName, modulePath, version, includeExamples, includeImpls }) => {
258
+ try {
259
+ const ver = version ?? "latest";
260
+ const prefix = modToUrlPrefix(modulePath);
261
+ const page = itemType === "mod" ? `${prefix}${itemName}/index.html` : `${prefix}${TYPE_FILE_PREFIX[itemType] ?? `${itemType}.`}${itemName}.html`;
262
+ const url = docsUrl(crateName, page, ver);
263
+ const $ = await fetchDom(url);
264
+ const fullName = `${crateName}::${modToRustPrefix(modulePath)}${itemName}`;
265
+ const decl = $("pre.rust.item-decl").text().trim();
266
+ const featureGate = extractItemFeatureGate($);
267
+ const doc = truncate(
268
+ cleanHtml($("details.toggle.top-doc").html() ?? ""),
269
+ MAX_DOC_LENGTH
270
+ );
271
+ const methods = [];
272
+ $("#implementations-list section.method h4.code-header").each((_, el) => {
273
+ methods.push($(el).text().trim());
274
+ });
275
+ const required = [];
276
+ $("h2#required-methods").first().nextUntil("h2").find("section h4.code-header").each((_, el) => {
277
+ required.push($(el).text().trim());
278
+ });
279
+ const provided = [];
280
+ $("h2#provided-methods").first().nextUntil("h2").find("section h4.code-header").each((_, el) => {
281
+ provided.push($(el).text().trim());
282
+ });
283
+ const variants = [];
284
+ $("section.variant h3.code-header, div.variant h3.code-header").each((_, el) => {
285
+ variants.push($(el).text().trim());
286
+ });
287
+ const parts = [`# ${itemType} ${fullName}`, url, ""];
288
+ if (featureGate) parts.push(`> ${featureGate}`, "");
289
+ if (decl) parts.push("## Signature", "```rust", decl, "```", "");
290
+ if (doc) parts.push("## Documentation", doc, "");
291
+ if (variants.length)
292
+ parts.push(`## Variants (${variants.length})`, ...variants.map((v) => ` ${v}`), "");
293
+ if (required.length)
294
+ parts.push(
295
+ `## Required Methods (${required.length})`,
296
+ ...required.map((m) => ` ${m}`),
297
+ ""
298
+ );
299
+ if (provided.length)
300
+ parts.push(
301
+ `## Provided Methods (${provided.length})`,
302
+ ...provided.map((m) => ` ${m}`),
303
+ ""
304
+ );
305
+ if (methods.length)
306
+ parts.push(`## Methods (${methods.length})`, ...methods.map((m) => ` ${m}`), "");
307
+ if (includeImpls) {
308
+ const impls = extractTraitImpls($);
309
+ if (impls.length) {
310
+ parts.push(`## Trait Implementations (${impls.length})`, ...impls.map((i) => ` ${i}`), "");
311
+ }
312
+ }
313
+ if (includeExamples) {
314
+ const examples = extractExamples($);
315
+ if (examples.length) {
316
+ parts.push(`## Examples (${examples.length})`);
317
+ examples.forEach((ex, i) => {
318
+ parts.push(`### Example ${i + 1}`, "```rust", ex, "```", "");
319
+ });
320
+ }
321
+ }
322
+ return textResult(parts.join("\n"));
323
+ } catch (e) {
324
+ return errorResult(
325
+ `Could not fetch ${itemType} "${itemName}". ${e.message}`
326
+ );
327
+ }
328
+ }
329
+ );
330
+ }
331
+ function scoreMatch(name, query) {
332
+ const lower = name.toLowerCase();
333
+ const q = query.toLowerCase();
334
+ const bareName = lower.includes("::") ? lower.split("::").pop() : lower;
335
+ if (bareName === q) return 100;
336
+ if (lower === q) return 95;
337
+ if (bareName.startsWith(q)) return 60;
338
+ if (lower.startsWith(q)) return 55;
339
+ if (lower.includes(q)) return 20;
340
+ return 0;
341
+ }
342
+ function register$2(server2) {
343
+ server2.tool(
344
+ "search_crate",
345
+ "Search for items by name within a Rust crate. Returns ranked results with canonical paths and item types.",
346
+ {
347
+ crateName: z.string().describe("Crate name"),
348
+ query: z.string().describe("Search query (matched against item names)"),
349
+ version: versionParam
350
+ },
351
+ async ({ crateName, query, version }) => {
352
+ try {
353
+ const ver = version ?? "latest";
354
+ const url = docsUrl(crateName, "all.html", ver);
355
+ const $ = await fetchDom(url);
356
+ const hits = [];
357
+ $("h3").each((_, h3) => {
358
+ const rawId = $(h3).attr("id") ?? "";
359
+ const type = SECTION_TO_TYPE[rawId] ?? rawId;
360
+ $(h3).next("ul.all-items").find("li a").each((_2, a) => {
361
+ const name = $(a).text().trim();
362
+ const href = $(a).attr("href") ?? "";
363
+ const score = scoreMatch(name, query);
364
+ if (score > 0) hits.push({ type, name, href, score });
365
+ });
366
+ });
367
+ if (!hits.length) return textResult(`No matches for "${query}" in ${crateName}.`);
368
+ hits.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
369
+ const capped = hits.slice(0, MAX_SEARCH_RESULTS);
370
+ const lines = capped.map((h) => {
371
+ const canonical = `${crateName}::${h.name.replace(/::/g, "::")}`;
372
+ return `[${h.type}] ${canonical}`;
373
+ });
374
+ const overflow = hits.length > MAX_SEARCH_RESULTS ? ` (showing first ${MAX_SEARCH_RESULTS})` : "";
375
+ return textResult(
376
+ [`# "${query}" in ${crateName} — ${hits.length} matches${overflow}`, "", ...lines].join(
377
+ "\n"
378
+ )
379
+ );
380
+ } catch (e) {
381
+ return errorResult(`Could not search "${crateName}". ${e.message}`);
382
+ }
383
+ }
384
+ );
385
+ }
386
+ function register$1(server2) {
387
+ server2.tool(
388
+ "get_crate_metadata",
389
+ "Get crate metadata from crates.io: version, features, default features, optional dependencies, and links.",
390
+ {
391
+ crateName: z.string().describe("Crate name"),
392
+ version: versionParam
393
+ },
394
+ async ({ crateName, version }) => {
395
+ try {
396
+ const info = await fetchCrateInfo(crateName);
397
+ const ver = version ?? info.version;
398
+ const [versionInfo, deps] = await Promise.all([
399
+ fetchCrateVersionInfo(crateName, ver),
400
+ fetchCrateDeps(crateName, ver)
401
+ ]);
402
+ const parts = [
403
+ `# ${info.name} v${versionInfo.num}`,
404
+ "",
405
+ `${info.description}`,
406
+ "",
407
+ "## Links"
408
+ ];
409
+ if (info.documentation) parts.push(` docs: ${info.documentation}`);
410
+ if (info.repository) parts.push(` repo: ${info.repository}`);
411
+ parts.push(` crates.io: https://crates.io/crates/${info.name}`);
412
+ parts.push(` license: ${versionInfo.license}`);
413
+ parts.push(` downloads: ${info.downloads.toLocaleString()}`);
414
+ const { features, defaultFeatures } = versionInfo;
415
+ parts.push("", "## Features");
416
+ parts.push(` default = [${defaultFeatures.join(", ")}]`);
417
+ const featureNames = Object.keys(features).filter((f) => f !== "default").sort();
418
+ for (const name of featureNames) {
419
+ const activates = features[name];
420
+ const tag = activates.length ? ` = [${activates.join(", ")}]` : "";
421
+ parts.push(` ${name}${tag}`);
422
+ }
423
+ const optionalDeps = deps.filter((d) => d.optional && d.kind === "normal");
424
+ if (optionalDeps.length) {
425
+ parts.push("", "## Optional Dependencies");
426
+ for (const dep of optionalDeps) {
427
+ parts.push(` ${dep.name} ${dep.req} (feature-gated)`);
428
+ }
429
+ }
430
+ const requiredDeps = deps.filter((d) => !d.optional && d.kind === "normal");
431
+ if (requiredDeps.length) {
432
+ parts.push("", "## Required Dependencies");
433
+ for (const dep of requiredDeps) {
434
+ parts.push(` ${dep.name} ${dep.req}`);
435
+ }
436
+ }
437
+ if (versionInfo.yanked) {
438
+ parts.push("", "> WARNING: This version has been yanked.");
439
+ }
440
+ return textResult(parts.join("\n"));
441
+ } catch (e) {
442
+ return errorResult(`Could not fetch metadata for "${crateName}". ${e.message}`);
443
+ }
444
+ }
445
+ );
446
+ }
447
+ function register(server2) {
448
+ server2.tool(
449
+ "get_crate_brief",
450
+ "Bundle call: fetches crate metadata, overview docs, module list, re-exports, and optionally items from focused modules — all in one shot.",
451
+ {
452
+ crateName: z.string().describe("Crate name"),
453
+ version: versionParam,
454
+ focusModules: z.string().optional().describe('Comma-separated module names to expand (e.g. "sync,task,io"). Omit for overview only.')
455
+ },
456
+ async ({ crateName, version, focusModules }) => {
457
+ try {
458
+ const info = await fetchCrateInfo(crateName);
459
+ const ver = version ?? info.version;
460
+ const versionInfo = await fetchCrateVersionInfo(crateName, ver);
461
+ const rootUrl = docsUrl(crateName, "index.html", ver);
462
+ const $ = await fetchDom(rootUrl);
463
+ const doc = truncate(cleanHtml($("details.toggle.top-doc").html() ?? ""), 3e3);
464
+ const reexports = extractReExports($);
465
+ const itemsBySection = {};
466
+ $("h2.section-header").each((_, h2) => {
467
+ const sectionId = $(h2).attr("id") ?? "";
468
+ const type = SECTION_TO_TYPE[sectionId] ?? sectionId;
469
+ const items = [];
470
+ $(h2).next("dl.item-table").find("dt").each((_2, dt) => {
471
+ const $dt = $(dt);
472
+ const name = $dt.find("a").first().text().trim();
473
+ const gate = $dt.find(".stab.portability code").first().text().trim();
474
+ if (name) {
475
+ items.push(gate ? `${name} [${gate}]` : name);
476
+ }
477
+ });
478
+ if (items.length) itemsBySection[type] = items;
479
+ });
480
+ const parts = [
481
+ `# ${info.name} v${versionInfo.num}`,
482
+ rootUrl,
483
+ "",
484
+ info.description,
485
+ "",
486
+ `license: ${versionInfo.license} | downloads: ${info.downloads.toLocaleString()}`
487
+ ];
488
+ if (info.repository) parts.push(`repo: ${info.repository}`);
489
+ const { defaultFeatures, features } = versionInfo;
490
+ parts.push(
491
+ "",
492
+ "## Features",
493
+ ` default = [${defaultFeatures.join(", ")}]`,
494
+ ` all: ${Object.keys(features).filter((f) => f !== "default").sort().join(", ")}`
495
+ );
496
+ if (doc) parts.push("", "## Overview", doc);
497
+ if (reexports.length) {
498
+ parts.push("", "## Re-exports", ...reexports.map((r) => ` ${r}`));
499
+ }
500
+ if (itemsBySection["mod"]) {
501
+ parts.push("", "## Modules", ...itemsBySection["mod"].map((m) => ` ${m}`));
502
+ }
503
+ for (const [type, items] of Object.entries(itemsBySection)) {
504
+ if (type === "mod" || type === "reexport") continue;
505
+ parts.push("", `## ${type} (${items.length})`, ...items.map((i) => ` ${i}`));
506
+ }
507
+ if (focusModules) {
508
+ const modules = focusModules.split(",").map((m) => m.trim()).filter(Boolean);
509
+ for (const mod of modules) {
510
+ try {
511
+ const modUrl = docsUrl(crateName, `${modToUrlPrefix(mod)}index.html`, ver);
512
+ const $mod = await fetchDom(modUrl);
513
+ parts.push("", `## Focus: ${crateName}::${mod.replace(/\./g, "::")}`, modUrl);
514
+ $mod("h2.section-header").each((_, h2) => {
515
+ const sectionId = $mod(h2).attr("id") ?? "";
516
+ const type = SECTION_TO_TYPE[sectionId] ?? sectionId;
517
+ $mod(h2).next("dl.item-table").find("dt").each((_2, dt) => {
518
+ const $dt = $mod(dt);
519
+ const name = $dt.find("a").first().text().trim();
520
+ const desc = $dt.next("dd").text().trim();
521
+ const gate = $dt.find(".stab.portability code").first().text().trim();
522
+ if (!name) return;
523
+ const tag = gate ? ` [${gate}]` : "";
524
+ parts.push(` [${type}] ${name}${tag} — ${desc}`);
525
+ });
526
+ });
527
+ } catch {
528
+ parts.push("", `## Focus: ${mod}`, ` (module not found)`);
529
+ }
530
+ }
531
+ }
532
+ return textResult(parts.join("\n"));
533
+ } catch (e) {
534
+ return errorResult(`Could not fetch brief for "${crateName}". ${e.message}`);
535
+ }
536
+ }
537
+ );
538
+ }
539
+ console.log = (...args) => console.error(...args);
540
+ const server = new McpServer({ name: "rust-docs", version: "2.0.0" });
541
+ register$5(server);
542
+ register$4(server);
543
+ register$3(server);
544
+ register$2(server);
545
+ register$1(server);
546
+ register(server);
547
+ server.prompt(
548
+ "lookup_crate_docs",
549
+ { crateName: z.string().describe("Crate name") },
550
+ ({ crateName }) => ({
551
+ messages: [
552
+ {
553
+ role: "user",
554
+ content: {
555
+ type: "text",
556
+ text: [
557
+ `Analyze the documentation for the Rust crate '${crateName}'. Focus on:`,
558
+ "1. Main purpose and features",
559
+ "2. Key types and functions",
560
+ "3. Common usage patterns",
561
+ "4. Important notes or warnings",
562
+ "5. Latest version"
563
+ ].join("\n")
564
+ }
565
+ }
566
+ ]
567
+ })
568
+ );
569
+ async function main() {
570
+ const transport = new StdioServerTransport();
571
+ await server.connect(transport);
572
+ }
573
+ main().catch((e) => {
574
+ console.error("Failed to start server:", e);
575
+ process.exit(1);
576
+ });
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "mcp-rustdoc",
3
+ "version": "2.0.0",
4
+ "type": "module",
5
+ "description": "MCP server for fetching and browsing Rust crate documentation from docs.rs and crates.io",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "mcp-rustdoc": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "dev": "bun run src/index.ts",
17
+ "build": "vite build",
18
+ "build:tsc": "tsc",
19
+ "start": "node dist/index.js",
20
+ "typecheck": "tsc --noEmit",
21
+ "prepublishOnly": "vite build"
22
+ },
23
+ "keywords": [
24
+ "mcp",
25
+ "rust",
26
+ "docs.rs",
27
+ "crates.io",
28
+ "documentation",
29
+ "model-context-protocol",
30
+ "rustdoc",
31
+ "ai",
32
+ "llm"
33
+ ],
34
+ "author": "",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/kieled/mcp-rustdoc.git"
39
+ },
40
+ "homepage": "https://github.com/kieled/mcp-rustdoc#readme",
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "dependencies": {
45
+ "@modelcontextprotocol/sdk": "^1.6.1",
46
+ "axios": "^1.6.0",
47
+ "cheerio": "^1.0.0",
48
+ "html-to-text": "^9.0.5",
49
+ "zod": "^3.23.0"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^22.0.0",
53
+ "typescript": "^5.7.0",
54
+ "vite": "^6.0.0"
55
+ }
56
+ }