mcp-rustdoc 2.0.0 → 3.1.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 +92 -2
  2. package/dist/index.js +255 -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 nine 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,24 @@ 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 |
16
+ | `get_crate_versions` | All published versions with dates and yanked status (crates.io API) |
17
+ | `get_source_code` | Raw source code of a file from docs.rs or doc.rust-lang.org |
15
18
 
16
19
  Every tool accepts an optional `version` parameter to pin a specific crate version instead of `latest`.
17
20
 
21
+ ### Standard library support
22
+
23
+ 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:
24
+
25
+ ```
26
+ > lookup_crate_docs({ crateName: "std" })
27
+ > get_crate_items({ crateName: "std", modulePath: "collections" })
28
+ > search_crate({ crateName: "core", query: "Option" })
29
+ ```
30
+
31
+ `get_crate_metadata` returns a helpful message for std crates since they aren't published on crates.io.
32
+
18
33
  ## Install
19
34
 
20
35
  No clone needed. Just configure your AI coding assistant with `npx`:
@@ -335,6 +350,75 @@ Searches all items in a crate by name. Results are ranked: exact match on the ba
335
350
 
336
351
  ---
337
352
 
353
+ ### `search_crates`
354
+
355
+ Search for Rust crates on crates.io by keyword.
356
+
357
+ | Parameter | Type | Required | Description |
358
+ |---|---|---|---|
359
+ | `query` | string | yes | Search keywords |
360
+ | `page` | number | no | Page number (default 1) |
361
+ | `perPage` | number | no | Results per page (default 10, max 50) |
362
+
363
+ ```
364
+ > search_crates({ query: "http" })
365
+
366
+ # Crate search: "http" — 1234 results (page 1)
367
+
368
+ http v1.2.0 (50,000,000 downloads) — A set of types for representing HTTP requests and responses.
369
+ hyper v1.5.2 (120,000,000 downloads) — A fast and correct HTTP library.
370
+ reqwest v0.12.12 (100,000,000 downloads) — higher level HTTP client library
371
+ ...
372
+ ```
373
+
374
+ ---
375
+
376
+ ### `get_crate_versions`
377
+
378
+ List all published versions of a crate from crates.io.
379
+
380
+ | Parameter | Type | Required | Description |
381
+ |---|---|---|---|
382
+ | `crateName` | string | yes | Crate name |
383
+
384
+ ```
385
+ > get_crate_versions({ crateName: "serde" })
386
+
387
+ # serde — 312 versions
388
+
389
+ 1.0.219 2025-02-01
390
+ 1.0.218 2025-01-12
391
+ 1.0.217 2024-12-23
392
+ ...
393
+ 0.1.0 2014-12-09
394
+ ```
395
+
396
+ ---
397
+
398
+ ### `get_source_code`
399
+
400
+ Fetch the raw source code of a file from docs.rs (or doc.rust-lang.org for std crates).
401
+
402
+ | Parameter | Type | Required | Description |
403
+ |---|---|---|---|
404
+ | `crateName` | string | yes | Crate name |
405
+ | `path` | string | yes | Source path relative to crate root (e.g. `"src/lib.rs"`, `"src/sync/mutex.rs"`) |
406
+ | `version` | string | no | Pinned version |
407
+
408
+ ```
409
+ > get_source_code({ crateName: "tokio", path: "src/sync/mutex.rs" })
410
+
411
+ # Source: tokio/src/sync/mutex.rs
412
+ https://docs.rs/tokio/latest/src/tokio/sync/mutex.rs
413
+
414
+ \`\`\`rust
415
+ use crate::sync::batch_semaphore as semaphore;
416
+ ...
417
+ \`\`\`
418
+ ```
419
+
420
+ ---
421
+
338
422
  ## Recommended workflows
339
423
 
340
424
  ### Exploring a new crate
@@ -362,6 +446,7 @@ Searches all items in a crate by name. Results are ranked: exact match on the ba
362
446
  src/
363
447
  index.ts Entry point — registers tools + prompt, starts stdio server
364
448
  lib.ts Shared: URL builders, HTTP/DOM helpers, crates.io API, extractors
449
+ cache.ts In-memory TTL cache (5-minute default)
365
450
  types/
366
451
  html-to-text.d.ts Type declarations for html-to-text
367
452
  tools/
@@ -370,6 +455,9 @@ src/
370
455
  get-items.ts get_crate_items
371
456
  lookup-item.ts lookup_crate_item
372
457
  search.ts search_crate
458
+ search-crates.ts search_crates
459
+ crate-versions.ts get_crate_versions
460
+ source-code.ts get_source_code
373
461
  crate-metadata.ts get_crate_metadata
374
462
  crate-brief.ts get_crate_brief
375
463
  ```
@@ -377,7 +465,8 @@ src/
377
465
  ### Data sources
378
466
 
379
467
  - **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
468
+ - **doc.rust-lang.org** — Same rustdoc HTML format, used for `std`, `core`, and `alloc`
469
+ - **crates.io API** — JSON endpoints for metadata, features, dependencies, and search
381
470
 
382
471
  ### Design decisions
383
472
 
@@ -385,6 +474,7 @@ src/
385
474
  - **Ranked search** — `all.html` contains every public item; scoring by exact/prefix/substring gives better results than flat substring matching
386
475
  - **Version parameter everywhere** — Agents working on projects with pinned dependencies need to read docs for specific versions
387
476
  - **Optional sections** — `includeImpls` and `includeExamples` default to off so the base response stays compact; agents opt in when they need more detail
477
+ - **In-memory cache** — All HTTP responses are cached for 5 minutes, avoiding redundant fetches when agents issue multiple related tool calls
388
478
 
389
479
  ## License
390
480
 
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.1.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$8(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$7(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$6(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$5(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$4(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$3(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,157 @@ function register(server2) {
536
610
  }
537
611
  );
538
612
  }
613
+ function register$2(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
+ }
661
+ function register$1(server2) {
662
+ server2.tool(
663
+ "get_crate_versions",
664
+ "List all published versions of a crate from crates.io, with yanked status and release dates.",
665
+ {
666
+ crateName: z.string().describe("Crate name")
667
+ },
668
+ async ({ crateName }) => {
669
+ try {
670
+ if (isStdCrate(crateName)) {
671
+ return textResult(
672
+ `"${crateName}" is part of the Rust standard library and is not published on crates.io.
673
+ Its version matches the Rust toolchain version.`
674
+ );
675
+ }
676
+ const cacheKey = `crate-versions:${crateName}`;
677
+ const cached = cacheGet(cacheKey);
678
+ if (cached) {
679
+ console.log(`[cache hit] crate-versions ${crateName}`);
680
+ return textResult(cached);
681
+ }
682
+ const { data } = await axios.get(`${CRATES_IO}/crates/${crateName}/versions`, {
683
+ headers: { "User-Agent": USER_AGENT },
684
+ timeout: 1e4
685
+ });
686
+ const versions = (data.versions ?? []).map((v) => ({
687
+ num: v.num,
688
+ yanked: v.yanked,
689
+ created_at: v.created_at,
690
+ license: v.license ?? ""
691
+ }));
692
+ if (!versions.length) {
693
+ return textResult(`No versions found for "${crateName}".`);
694
+ }
695
+ const lines = versions.map((v) => {
696
+ const date = v.created_at.slice(0, 10);
697
+ const yanked = v.yanked ? " [YANKED]" : "";
698
+ return ` ${v.num} ${date}${yanked}`;
699
+ });
700
+ const result = [
701
+ `# ${crateName} — ${versions.length} versions`,
702
+ "",
703
+ ...lines
704
+ ].join("\n");
705
+ cacheSet(cacheKey, result);
706
+ return textResult(result);
707
+ } catch (e) {
708
+ return errorResult(`Could not fetch versions for "${crateName}". ${e.message}`);
709
+ }
710
+ }
711
+ );
712
+ }
713
+ function register(server2) {
714
+ server2.tool(
715
+ "get_source_code",
716
+ "Fetch the source code of a Rust item from docs.rs. Returns the raw source implementation.",
717
+ {
718
+ crateName: z.string().describe("Crate name"),
719
+ path: z.string().describe('Source path relative to crate root (e.g. "src/lib.rs", "src/sync/mutex.rs")'),
720
+ version: versionParam
721
+ },
722
+ async ({ crateName, path, version }) => {
723
+ try {
724
+ if (isStdCrate(crateName)) {
725
+ const url2 = `https://doc.rust-lang.org/stable/src/${crateName}/${path}`;
726
+ const $2 = await fetchDom(url2);
727
+ const code2 = $2("#source-code").text().trim() || $2("pre.rust").text().trim();
728
+ if (!code2) return errorResult(`No source code found at ${url2}`);
729
+ return textResult([
730
+ `# Source: ${crateName}/${path}`,
731
+ url2,
732
+ "",
733
+ "```rust",
734
+ truncate(code2, 12e3),
735
+ "```"
736
+ ].join("\n"));
737
+ }
738
+ const ver = version ?? "latest";
739
+ const url = `${DOCS_BASE}/${crateName}/${ver}/src/${crateSlug(crateName)}/${path}`;
740
+ const $ = await fetchDom(url);
741
+ const code = $("#source-code").text().trim() || $("pre.rust").text().trim() || $(".src-line-numbers + code").text().trim();
742
+ if (!code) {
743
+ return errorResult(`No source code found at ${url}. Check that the path is correct.`);
744
+ }
745
+ return textResult([
746
+ `# Source: ${crateName}/${path}`,
747
+ url,
748
+ "",
749
+ "```rust",
750
+ truncate(code, 12e3),
751
+ "```"
752
+ ].join("\n"));
753
+ } catch (e) {
754
+ return errorResult(`Could not fetch source for "${crateName}/${path}". ${e.message}`);
755
+ }
756
+ }
757
+ );
758
+ }
539
759
  console.log = (...args) => console.error(...args);
540
- const server = new McpServer({ name: "rust-docs", version: "2.0.0" });
760
+ const server = new McpServer({ name: "rust-docs", version: "3.1.0" });
761
+ register$8(server);
762
+ register$7(server);
763
+ register$6(server);
541
764
  register$5(server);
542
765
  register$4(server);
543
766
  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.1.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
  }