mcp-rustdoc 2.0.0 → 3.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 (3) hide show
  1. package/README.md +42 -2
  2. package/dist/index.js +155 -32
  3. package/package.json +10 -10
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # mcp-rustdoc
2
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.
3
+ An MCP server that gives AI assistants deep access to the Rust ecosystem. It scrapes docs.rs (and `doc.rust-lang.org` for `std`/`core`/`alloc`) with surgical DOM extraction (cheerio) and queries the crates.io API, exposing seven tools that cover everything from high-level crate overviews to individual method signatures, feature gates, trait impls, and code examples. Responses are cached in memory (5-minute TTL) to avoid redundant fetches.
4
4
 
5
5
  ## Tools
6
6
 
@@ -12,9 +12,22 @@ An MCP server that gives AI assistants deep access to the Rust ecosystem. It scr
12
12
  | `get_crate_items` | Items in a module with types, feature gates, and descriptions |
13
13
  | `lookup_crate_item` | Item detail: signature, docs, methods, variants, optionally trait impls + examples |
14
14
  | `search_crate` | Ranked symbol search (exact > prefix > substring) with canonical paths |
15
+ | `search_crates` | Search crates.io by keyword — returns name, description, downloads, version |
15
16
 
16
17
  Every tool accepts an optional `version` parameter to pin a specific crate version instead of `latest`.
17
18
 
19
+ ### Standard library support
20
+
21
+ All documentation tools work with `std`, `core`, and `alloc` — the Rust standard library crates hosted at `doc.rust-lang.org`. Use them exactly like any other crate:
22
+
23
+ ```
24
+ > lookup_crate_docs({ crateName: "std" })
25
+ > get_crate_items({ crateName: "std", modulePath: "collections" })
26
+ > search_crate({ crateName: "core", query: "Option" })
27
+ ```
28
+
29
+ `get_crate_metadata` returns a helpful message for std crates since they aren't published on crates.io.
30
+
18
31
  ## Install
19
32
 
20
33
  No clone needed. Just configure your AI coding assistant with `npx`:
@@ -335,6 +348,29 @@ Searches all items in a crate by name. Results are ranked: exact match on the ba
335
348
 
336
349
  ---
337
350
 
351
+ ### `search_crates`
352
+
353
+ Search for Rust crates on crates.io by keyword.
354
+
355
+ | Parameter | Type | Required | Description |
356
+ |---|---|---|---|
357
+ | `query` | string | yes | Search keywords |
358
+ | `page` | number | no | Page number (default 1) |
359
+ | `perPage` | number | no | Results per page (default 10, max 50) |
360
+
361
+ ```
362
+ > search_crates({ query: "http" })
363
+
364
+ # Crate search: "http" — 1234 results (page 1)
365
+
366
+ http v1.2.0 (50,000,000 downloads) — A set of types for representing HTTP requests and responses.
367
+ hyper v1.5.2 (120,000,000 downloads) — A fast and correct HTTP library.
368
+ reqwest v0.12.12 (100,000,000 downloads) — higher level HTTP client library
369
+ ...
370
+ ```
371
+
372
+ ---
373
+
338
374
  ## Recommended workflows
339
375
 
340
376
  ### Exploring a new crate
@@ -362,6 +398,7 @@ Searches all items in a crate by name. Results are ranked: exact match on the ba
362
398
  src/
363
399
  index.ts Entry point — registers tools + prompt, starts stdio server
364
400
  lib.ts Shared: URL builders, HTTP/DOM helpers, crates.io API, extractors
401
+ cache.ts In-memory TTL cache (5-minute default)
365
402
  types/
366
403
  html-to-text.d.ts Type declarations for html-to-text
367
404
  tools/
@@ -370,6 +407,7 @@ src/
370
407
  get-items.ts get_crate_items
371
408
  lookup-item.ts lookup_crate_item
372
409
  search.ts search_crate
410
+ search-crates.ts search_crates
373
411
  crate-metadata.ts get_crate_metadata
374
412
  crate-brief.ts get_crate_brief
375
413
  ```
@@ -377,7 +415,8 @@ src/
377
415
  ### Data sources
378
416
 
379
417
  - **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
418
+ - **doc.rust-lang.org** — Same rustdoc HTML format, used for `std`, `core`, and `alloc`
419
+ - **crates.io API** — JSON endpoints for metadata, features, dependencies, and search
381
420
 
382
421
  ### Design decisions
383
422
 
@@ -385,6 +424,7 @@ src/
385
424
  - **Ranked search** — `all.html` contains every public item; scoring by exact/prefix/substring gives better results than flat substring matching
386
425
  - **Version parameter everywhere** — Agents working on projects with pinned dependencies need to read docs for specific versions
387
426
  - **Optional sections** — `includeImpls` and `includeExamples` default to off so the base response stays compact; agents opt in when they need more detail
427
+ - **In-memory cache** — All HTTP responses are cached for 5 minutes, avoiding redundant fetches when agents issue multiple related tool calls
388
428
 
389
429
  ## License
390
430
 
package/dist/index.js CHANGED
@@ -5,9 +5,23 @@ import { z } from "zod";
5
5
  import axios from "axios";
6
6
  import { load } from "cheerio";
7
7
  import { convert } from "html-to-text";
8
+ const DEFAULT_TTL = 5 * 60 * 1e3;
9
+ const store = /* @__PURE__ */ new Map();
10
+ function cacheGet(key) {
11
+ const entry = store.get(key);
12
+ if (!entry) return null;
13
+ if (Date.now() > entry.expiry) {
14
+ store.delete(key);
15
+ return null;
16
+ }
17
+ return entry.data;
18
+ }
19
+ function cacheSet(key, data, ttl = DEFAULT_TTL) {
20
+ store.set(key, { data, expiry: Date.now() + ttl });
21
+ }
8
22
  const DOCS_BASE = "https://docs.rs";
9
23
  const CRATES_IO = "https://crates.io/api/v1";
10
- const USER_AGENT = "mcp-rust-docs/2.0.0";
24
+ const USER_AGENT = "mcp-rust-docs/3.0.0";
11
25
  const MAX_DOC_LENGTH = 6e3;
12
26
  const MAX_SEARCH_RESULTS = 100;
13
27
  const SECTION_TO_TYPE = {
@@ -38,10 +52,18 @@ const TYPE_FILE_PREFIX = {
38
52
  attr: "attr.",
39
53
  derive: "derive."
40
54
  };
55
+ const STD_CRATES = /* @__PURE__ */ new Set(["std", "core", "alloc"]);
56
+ function isStdCrate(name) {
57
+ return STD_CRATES.has(name);
58
+ }
59
+ function stdDocsUrl(crate, path) {
60
+ return `https://doc.rust-lang.org/stable/${crate}/${path}`;
61
+ }
41
62
  function crateSlug(name) {
42
63
  return name.replace(/-/g, "_");
43
64
  }
44
65
  function docsUrl(crate, path = "index.html", version = "latest") {
66
+ if (isStdCrate(crate)) return stdDocsUrl(crate, path);
45
67
  return `${DOCS_BASE}/${crate}/${version}/${crateSlug(crate)}/${path}`;
46
68
  }
47
69
  function modToUrlPrefix(modulePath) {
@@ -51,7 +73,13 @@ function modToRustPrefix(modulePath) {
51
73
  return modulePath ? modulePath.replace(/\./g, "::") + "::" : "";
52
74
  }
53
75
  async function fetchDom(url) {
76
+ const cached = cacheGet(`dom:${url}`);
77
+ if (cached) {
78
+ console.log(`[cache hit] ${url}`);
79
+ return load(cached);
80
+ }
54
81
  const { data } = await axios.get(url, { timeout: 15e3 });
82
+ cacheSet(`dom:${url}`, data);
55
83
  return load(data);
56
84
  }
57
85
  function cleanHtml(html) {
@@ -74,12 +102,18 @@ function errorResult(msg) {
74
102
  }
75
103
  const cratesIoHeaders = { "User-Agent": USER_AGENT };
76
104
  async function fetchCrateInfo(name) {
105
+ const cacheKey = `crate-info:${name}`;
106
+ const cached = cacheGet(cacheKey);
107
+ if (cached) {
108
+ console.log(`[cache hit] crate-info ${name}`);
109
+ return cached;
110
+ }
77
111
  const { data } = await axios.get(`${CRATES_IO}/crates/${name}`, {
78
112
  headers: cratesIoHeaders,
79
113
  timeout: 1e4
80
114
  });
81
115
  const c = data.crate;
82
- return {
116
+ const info = {
83
117
  name: c.name,
84
118
  version: c.max_stable_version || c.max_version,
85
119
  description: c.description,
@@ -87,34 +121,52 @@ async function fetchCrateInfo(name) {
87
121
  repository: c.repository,
88
122
  downloads: c.downloads
89
123
  };
124
+ cacheSet(cacheKey, info);
125
+ return info;
90
126
  }
91
127
  async function fetchCrateVersionInfo(name, version) {
128
+ const cacheKey = `crate-version:${name}@${version}`;
129
+ const cached = cacheGet(cacheKey);
130
+ if (cached) {
131
+ console.log(`[cache hit] crate-version ${name}@${version}`);
132
+ return cached;
133
+ }
92
134
  const { data } = await axios.get(`${CRATES_IO}/crates/${name}/${version}`, {
93
135
  headers: cratesIoHeaders,
94
136
  timeout: 1e4
95
137
  });
96
138
  const v = data.version;
97
139
  const features = v.features ?? {};
98
- return {
140
+ const info = {
99
141
  num: v.num,
100
142
  features,
101
143
  defaultFeatures: features["default"] ?? [],
102
144
  yanked: v.yanked,
103
145
  license: v.license
104
146
  };
147
+ cacheSet(cacheKey, info);
148
+ return info;
105
149
  }
106
150
  async function fetchCrateDeps(name, version) {
151
+ const cacheKey = `crate-deps:${name}@${version}`;
152
+ const cached = cacheGet(cacheKey);
153
+ if (cached) {
154
+ console.log(`[cache hit] crate-deps ${name}@${version}`);
155
+ return cached;
156
+ }
107
157
  const { data } = await axios.get(`${CRATES_IO}/crates/${name}/${version}/dependencies`, {
108
158
  headers: cratesIoHeaders,
109
159
  timeout: 1e4
110
160
  });
111
- return (data.dependencies ?? []).map((d) => ({
161
+ const deps = (data.dependencies ?? []).map((d) => ({
112
162
  name: d.crate_id,
113
163
  req: d.req,
114
164
  optional: d.optional,
115
165
  kind: d.kind,
116
166
  features: d.features ?? []
117
167
  }));
168
+ cacheSet(cacheKey, deps);
169
+ return deps;
118
170
  }
119
171
  function extractItemFeatureGate($) {
120
172
  const gate = $(".item-info .stab.portability").first().text().trim();
@@ -157,7 +209,7 @@ const itemTypeEnum = z.enum([
157
209
  "derive"
158
210
  ]);
159
211
  const versionParam = z.string().optional().describe('Crate version (e.g. "1.49.0"). Defaults to latest.');
160
- function register$5(server2) {
212
+ function register$6(server2) {
161
213
  server2.tool(
162
214
  "lookup_crate_docs",
163
215
  "Fetch the main documentation for a Rust crate. Returns overview, version, sections, and re-exports.",
@@ -165,7 +217,6 @@ function register$5(server2) {
165
217
  crateName: z.string().describe('Crate name (e.g. "tokio", "serde-json")'),
166
218
  version: versionParam
167
219
  },
168
- // @ts-expect-error — MCP SDK deep type instantiation with Zod schemas
169
220
  async ({ crateName, version }) => {
170
221
  try {
171
222
  const ver = version ?? "latest";
@@ -195,7 +246,7 @@ function register$5(server2) {
195
246
  }
196
247
  );
197
248
  }
198
- function register$4(server2) {
249
+ function register$5(server2) {
199
250
  server2.tool(
200
251
  "get_crate_items",
201
252
  "List public items in a crate root or module. Returns names, types, feature gates, and short descriptions.",
@@ -240,7 +291,7 @@ function register$4(server2) {
240
291
  }
241
292
  );
242
293
  }
243
- function register$3(server2) {
294
+ function register$4(server2) {
244
295
  server2.tool(
245
296
  "lookup_crate_item",
246
297
  "Get detailed documentation for a specific item. Returns signature, docs, feature gate, methods, trait impls, and optionally examples.",
@@ -253,7 +304,6 @@ function register$3(server2) {
253
304
  includeExamples: z.boolean().optional().describe("Include code examples from the docs. Default: false."),
254
305
  includeImpls: z.boolean().optional().describe("Include trait implementation list. Default: false.")
255
306
  },
256
- // @ts-expect-error — MCP SDK deep type instantiation with Zod schemas
257
307
  async ({ crateName, itemType, itemName, modulePath, version, includeExamples, includeImpls }) => {
258
308
  try {
259
309
  const ver = version ?? "latest";
@@ -339,7 +389,7 @@ function scoreMatch(name, query) {
339
389
  if (lower.includes(q)) return 20;
340
390
  return 0;
341
391
  }
342
- function register$2(server2) {
392
+ function register$3(server2) {
343
393
  server2.tool(
344
394
  "search_crate",
345
395
  "Search for items by name within a Rust crate. Returns ranked results with canonical paths and item types.",
@@ -383,7 +433,7 @@ function register$2(server2) {
383
433
  }
384
434
  );
385
435
  }
386
- function register$1(server2) {
436
+ function register$2(server2) {
387
437
  server2.tool(
388
438
  "get_crate_metadata",
389
439
  "Get crate metadata from crates.io: version, features, default features, optional dependencies, and links.",
@@ -393,6 +443,12 @@ function register$1(server2) {
393
443
  },
394
444
  async ({ crateName, version }) => {
395
445
  try {
446
+ if (isStdCrate(crateName)) {
447
+ return textResult(
448
+ `"${crateName}" is part of the Rust standard library and is not published on crates.io.
449
+ Use lookup_crate_docs, get_crate_items, lookup_crate_item, or search_crate to browse its documentation.`
450
+ );
451
+ }
396
452
  const info = await fetchCrateInfo(crateName);
397
453
  const ver = version ?? info.version;
398
454
  const [versionInfo, deps] = await Promise.all([
@@ -444,7 +500,7 @@ function register$1(server2) {
444
500
  }
445
501
  );
446
502
  }
447
- function register(server2) {
503
+ function register$1(server2) {
448
504
  server2.tool(
449
505
  "get_crate_brief",
450
506
  "Bundle call: fetches crate metadata, overview docs, module list, re-exports, and optionally items from focused modules — all in one shot.",
@@ -455,9 +511,16 @@ function register(server2) {
455
511
  },
456
512
  async ({ crateName, version, focusModules }) => {
457
513
  try {
458
- const info = await fetchCrateInfo(crateName);
459
- const ver = version ?? info.version;
460
- const versionInfo = await fetchCrateVersionInfo(crateName, ver);
514
+ const isStd = isStdCrate(crateName);
515
+ let info = null;
516
+ let versionInfo = null;
517
+ if (!isStd) {
518
+ const crateInfo = await fetchCrateInfo(crateName);
519
+ const ver2 = version ?? crateInfo.version;
520
+ versionInfo = await fetchCrateVersionInfo(crateName, ver2);
521
+ info = crateInfo;
522
+ }
523
+ const ver = isStd ? "latest" : version ?? info.version;
461
524
  const rootUrl = docsUrl(crateName, "index.html", ver);
462
525
  const $ = await fetchDom(rootUrl);
463
526
  const doc = truncate(cleanHtml($("details.toggle.top-doc").html() ?? ""), 3e3);
@@ -477,22 +540,33 @@ function register(server2) {
477
540
  });
478
541
  if (items.length) itemsBySection[type] = items;
479
542
  });
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
- );
543
+ const parts = [];
544
+ if (isStd) {
545
+ const pageVersion = $(".sidebar-crate .version").text().trim() || "stable";
546
+ parts.push(
547
+ `# ${crateName} (Rust standard library) v${pageVersion}`,
548
+ rootUrl,
549
+ "",
550
+ `The "${crateName}" crate is part of the Rust standard library.`
551
+ );
552
+ } else {
553
+ parts.push(
554
+ `# ${info.name} v${versionInfo.num}`,
555
+ rootUrl,
556
+ "",
557
+ info.description,
558
+ "",
559
+ `license: ${versionInfo.license} | downloads: ${info.downloads.toLocaleString()}`
560
+ );
561
+ if (info.repository) parts.push(`repo: ${info.repository}`);
562
+ const { defaultFeatures, features } = versionInfo;
563
+ parts.push(
564
+ "",
565
+ "## Features",
566
+ ` default = [${defaultFeatures.join(", ")}]`,
567
+ ` all: ${Object.keys(features).filter((f) => f !== "default").sort().join(", ")}`
568
+ );
569
+ }
496
570
  if (doc) parts.push("", "## Overview", doc);
497
571
  if (reexports.length) {
498
572
  parts.push("", "## Re-exports", ...reexports.map((r) => ` ${r}`));
@@ -536,8 +610,57 @@ function register(server2) {
536
610
  }
537
611
  );
538
612
  }
613
+ function register(server2) {
614
+ server2.tool(
615
+ "search_crates",
616
+ "Search for Rust crates on crates.io by keyword. Returns name, description, downloads, and version.",
617
+ {
618
+ query: z.string().describe("Search keywords"),
619
+ page: z.number().min(1).optional().describe("Page number (default 1)"),
620
+ perPage: z.number().min(1).max(50).optional().describe("Results per page (default 10, max 50)")
621
+ },
622
+ async ({ query, page: rawPage, perPage: rawPerPage }) => {
623
+ const page = rawPage ?? 1;
624
+ const perPage = rawPerPage ?? 10;
625
+ try {
626
+ const cacheKey = `search-crates:${query}:${page}:${perPage}`;
627
+ const cached = cacheGet(cacheKey);
628
+ if (cached) {
629
+ console.log(`[cache hit] search-crates "${query}" page=${page}`);
630
+ return textResult(cached);
631
+ }
632
+ const { data } = await axios.get(`${CRATES_IO}/crates`, {
633
+ params: { q: query, per_page: perPage, page },
634
+ headers: { "User-Agent": USER_AGENT },
635
+ timeout: 1e4
636
+ });
637
+ const crates = data.crates ?? [];
638
+ if (!crates.length) {
639
+ return textResult(`No crates found for "${query}".`);
640
+ }
641
+ const total = data.meta?.total ?? crates.length;
642
+ const lines = crates.map((c) => {
643
+ const ver = c.max_stable_version || c.max_version;
644
+ const dl = c.downloads.toLocaleString();
645
+ const desc = c.description ? ` — ${c.description.trim()}` : "";
646
+ return ` ${c.name} v${ver} (${dl} downloads)${desc}`;
647
+ });
648
+ const result = [
649
+ `# Crate search: "${query}" — ${total} results (page ${page})`,
650
+ "",
651
+ ...lines
652
+ ].join("\n");
653
+ cacheSet(cacheKey, result);
654
+ return textResult(result);
655
+ } catch (e) {
656
+ return errorResult(`Could not search crates.io for "${query}". ${e.message}`);
657
+ }
658
+ }
659
+ );
660
+ }
539
661
  console.log = (...args) => console.error(...args);
540
- const server = new McpServer({ name: "rust-docs", version: "2.0.0" });
662
+ const server = new McpServer({ name: "rust-docs", version: "3.0.0" });
663
+ register$6(server);
541
664
  register$5(server);
542
665
  register$4(server);
543
666
  register$3(server);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-rustdoc",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "description": "MCP server for fetching and browsing Rust crate documentation from docs.rs and crates.io",
6
6
  "main": "dist/index.js",
@@ -39,18 +39,18 @@
39
39
  },
40
40
  "homepage": "https://github.com/kieled/mcp-rustdoc#readme",
41
41
  "engines": {
42
- "node": ">=18"
42
+ "node": ">=20"
43
43
  },
44
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"
45
+ "@modelcontextprotocol/sdk": "1.26.0",
46
+ "axios": "1.13.4",
47
+ "cheerio": "1.2.0",
48
+ "html-to-text": "9.0.5",
49
+ "zod": "4.3.6"
50
50
  },
51
51
  "devDependencies": {
52
- "@types/node": "^22.0.0",
53
- "typescript": "^5.7.0",
54
- "vite": "^6.0.0"
52
+ "@types/node": "25.2.1",
53
+ "typescript": "5.9.3",
54
+ "vite": "7.3.1"
55
55
  }
56
56
  }