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.
- package/README.md +92 -2
- package/dist/index.js +255 -32
- 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
|
|
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
|
-
- **
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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: "
|
|
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": "
|
|
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": ">=
|
|
42
|
+
"node": ">=20"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@modelcontextprotocol/sdk": "
|
|
46
|
-
"axios": "
|
|
47
|
-
"cheerio": "
|
|
48
|
-
"html-to-text": "
|
|
49
|
-
"zod": "
|
|
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": "
|
|
53
|
-
"typescript": "
|
|
54
|
-
"vite": "
|
|
52
|
+
"@types/node": "25.2.1",
|
|
53
|
+
"typescript": "5.9.3",
|
|
54
|
+
"vite": "7.3.1"
|
|
55
55
|
}
|
|
56
56
|
}
|