mcp-rustdoc 4.0.1 → 4.0.2

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 +41 -326
  2. package/dist/index.js +17 -17
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,44 +1,28 @@
1
1
  # mcp-rustdoc
2
2
 
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 twelve tools that cover everything from high-level crate overviews to individual method signatures, feature gates, trait impls, code examples, changelogs, and source code.
4
-
5
- Zero external HTTP dependencies — uses native `fetch` (Node.js >= 20). Responses are cached with LRU eviction (500 entries, 5-minute TTL, stale-while-revalidate) and all HTTP calls retry on transient failures.
3
+ MCP server for browsing Rust crate documentation. Scrapes docs.rs, doc.rust-lang.org (`std`/`core`/`alloc`), and the crates.io API. Zero HTTP dependencies native `fetch` only.
6
4
 
7
5
  ## Tools
8
6
 
9
- | Tool | What it returns |
7
+ | Tool | Description |
10
8
  |---|---|
11
- | `get_crate_metadata` | Version, features, deps, MSRV, links (crates.io API) |
12
- | `get_crate_brief` | One-shot bundle: metadata + overview + re-exports + module list + focused module items |
13
- | `lookup_crate_docs` | Crate overview documentation, version, sections, re-exports |
14
- | `get_crate_items` | Items in a module with types, feature gates, and descriptions. Filterable by type and feature. |
15
- | `lookup_crate_item` | Item detail: signature, docs, methods, variants, trait impls, examples. Auto-discovers module path. |
16
- | `search_crate` | Ranked symbol search (exact > prefix > substring) with canonical paths |
17
- | `search_crates` | Search crates.io by keyword — returns name, description, downloads, version |
18
- | `get_crate_versions` | All published versions with dates and yanked status (crates.io API) |
19
- | `get_source_code` | Raw source code of a file from docs.rs or doc.rust-lang.org |
20
- | `batch_lookup` | Look up multiple items in one call (up to 20) — saves round-trips |
21
- | `get_crate_changelog` | Recent GitHub releases for a crate |
22
- | `resolve_type` | Resolve a full Rust type path (e.g. `tokio::sync::Mutex`) to its documentation |
23
-
24
- Every tool accepts an optional `version` parameter to pin a specific crate version instead of `latest`.
25
-
26
- ### Standard library support
27
-
28
- 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:
29
-
30
- ```
31
- > lookup_crate_docs({ crateName: "std" })
32
- > get_crate_items({ crateName: "std", modulePath: "collections" })
33
- > search_crate({ crateName: "core", query: "Option" })
34
- ```
35
-
36
- `get_crate_metadata` returns a helpful message for std crates since they aren't published on crates.io.
9
+ | `get_crate_metadata` | Version, features, deps, MSRV, links |
10
+ | `get_crate_brief` | Metadata + overview + modules + focused module items in one call |
11
+ | `lookup_crate_docs` | Crate overview, version, sections, re-exports |
12
+ | `get_crate_items` | Items in a module. Filter by type and feature gate. |
13
+ | `lookup_crate_item` | Full item docs: signature, methods, variants, impls, examples. Auto-discovers module path. |
14
+ | `search_crate` | Ranked symbol search within a crate |
15
+ | `search_crates` | Search crates.io by keyword |
16
+ | `get_crate_versions` | All published versions with dates and yanked status |
17
+ | `get_source_code` | Raw source code from docs.rs |
18
+ | `batch_lookup` | Multiple item lookups in one call (up to 20) |
19
+ | `get_crate_changelog` | GitHub releases for a crate |
20
+ | `resolve_type` | Resolve a type path like `tokio::sync::Mutex` to its docs |
21
+
22
+ All tools accept an optional `version` parameter.
37
23
 
38
24
  ## Install
39
25
 
40
- No clone needed. Just configure your AI coding assistant with `npx`:
41
-
42
26
  ```
43
27
  npx -y mcp-rustdoc
44
28
  ```
@@ -49,33 +33,10 @@ npx -y mcp-rustdoc
49
33
  claude mcp add mcp-rustdoc -- npx -y mcp-rustdoc
50
34
  ```
51
35
 
52
- Or add to your project's `.mcp.json`:
53
-
54
- ```json
55
- {
56
- "mcpServers": {
57
- "mcp-rustdoc": {
58
- "type": "stdio",
59
- "command": "npx",
60
- "args": ["-y", "mcp-rustdoc"]
61
- }
62
- }
63
- }
64
- ```
65
-
66
36
  ### Gemini CLI
67
37
 
68
- Add to `~/.gemini/settings.json` (global) or `.gemini/settings.json` (project):
69
-
70
38
  ```json
71
- {
72
- "mcpServers": {
73
- "mcp-rustdoc": {
74
- "command": "npx",
75
- "args": ["-y", "mcp-rustdoc"]
76
- }
77
- }
78
- }
39
+ { "mcpServers": { "mcp-rustdoc": { "command": "npx", "args": ["-y", "mcp-rustdoc"] } } }
79
40
  ```
80
41
 
81
42
  ### OpenAI Codex CLI
@@ -84,295 +45,49 @@ Add to `~/.gemini/settings.json` (global) or `.gemini/settings.json` (project):
84
45
  codex mcp add mcp-rustdoc -- npx -y mcp-rustdoc
85
46
  ```
86
47
 
87
- Or add to `~/.codex/config.toml`:
88
-
89
- ```toml
90
- [mcp_servers.mcp-rustdoc]
91
- command = "npx"
92
- args = ["-y", "mcp-rustdoc"]
93
- ```
94
-
95
48
  ### Claude Desktop
96
49
 
97
- Add to your `claude_desktop_config.json`:
98
-
99
50
  ```json
100
- {
101
- "mcpServers": {
102
- "mcp-rustdoc": {
103
- "command": "npx",
104
- "args": ["-y", "mcp-rustdoc"]
105
- }
106
- }
107
- }
51
+ { "mcpServers": { "mcp-rustdoc": { "command": "npx", "args": ["-y", "mcp-rustdoc"] } } }
108
52
  ```
109
53
 
110
- ### MCP Inspector (testing)
54
+ ## Parameters
111
55
 
112
- ```bash
113
- npx @modelcontextprotocol/inspector -- npx -y mcp-rustdoc
114
- ```
56
+ Every tool takes `crateName` (string, required) and `version` (string, optional). Additional parameters:
115
57
 
116
- ---
58
+ | Tool | Extra parameters |
59
+ |---|---|
60
+ | `get_crate_brief` | `focusModules` — comma-separated modules to expand |
61
+ | `get_crate_items` | `modulePath`, `itemType`, `feature` |
62
+ | `lookup_crate_item` | `itemType` (required), `itemName` (required), `modulePath`, `includeImpls`, `includeExamples` |
63
+ | `search_crate` | `query` (required) |
64
+ | `search_crates` | `query` (required), `page`, `perPage` |
65
+ | `get_source_code` | `path` (required) |
66
+ | `batch_lookup` | `items` (required) — array of `{ itemType, itemName, modulePath? }`, max 20 |
67
+ | `get_crate_changelog` | `count` — number of releases (default 5) |
68
+ | `resolve_type` | `typePath` (required) — e.g. `"tokio::sync::Mutex"` |
117
69
 
118
70
  ## Development
119
71
 
120
72
  ```bash
121
73
  git clone https://github.com/kieled/mcp-rustdoc.git
122
- cd mcp-rustdoc
123
- npm install
74
+ cd mcp-rustdoc && npm install
124
75
  ```
125
76
 
126
- ### Run with bun (no build step)
127
-
128
- ```bash
129
- bun run dev
130
- ```
131
-
132
- ### Build with Vite and run with Node.js
133
-
134
77
  ```bash
135
- npm run build # vite build dist/index.js (single bundled ESM file)
136
- node dist/index.js
78
+ bun run dev # run with bun (no build)
79
+ npm run build # vite build → dist/index.js
80
+ npm run typecheck # tsc --noEmit
81
+ npm publish # build + publish
137
82
  ```
138
83
 
139
- ### Build with tsc
140
-
141
- ```bash
142
- npm run build:tsc # tsc → dist/ (one .js per source file)
143
- node dist/index.js
144
- ```
145
-
146
- ### Type check only
147
-
148
- ```bash
149
- npm run typecheck # tsc --noEmit
150
- ```
151
-
152
- ### Publish
153
-
154
- ```bash
155
- npm publish # runs vite build via prepublishOnly, then publishes
156
- ```
157
-
158
- ---
159
-
160
- ## Tool reference
161
-
162
- ### `get_crate_metadata`
163
-
164
- Fetches structured metadata from the crates.io API.
165
-
166
- | Parameter | Type | Required | Description |
167
- |---|---|---|---|
168
- | `crateName` | string | yes | Crate name |
169
- | `version` | string | no | Pinned version |
170
-
171
- Returns: version, description, links (docs/repo/crates.io), license, download count, MSRV, full feature list with activations, optional deps (feature-gated), required deps.
172
-
173
- ---
174
-
175
- ### `get_crate_brief`
176
-
177
- Single call to bootstrap context for a crate. Combines metadata, overview docs, re-exports, module list, and optionally expands focused modules.
178
-
179
- | Parameter | Type | Required | Description |
180
- |---|---|---|---|
181
- | `crateName` | string | yes | Crate name |
182
- | `version` | string | no | Pinned version |
183
- | `focusModules` | string | no | Comma-separated modules to expand (e.g. `"sync,task"`) |
184
-
185
- ---
186
-
187
- ### `lookup_crate_docs`
188
-
189
- Fetches the main documentation page for a crate.
190
-
191
- | Parameter | Type | Required | Description |
192
- |---|---|---|---|
193
- | `crateName` | string | yes | Crate name |
194
- | `version` | string | no | Pinned version |
195
-
196
- Returns: crate version, overview documentation text, re-exports, and section list with item counts.
197
-
198
- ---
199
-
200
- ### `get_crate_items`
201
-
202
- Lists all public items in a crate root or specific module. Supports filtering by item type and feature gate.
203
-
204
- | Parameter | Type | Required | Description |
205
- |---|---|---|---|
206
- | `crateName` | string | yes | Crate name |
207
- | `modulePath` | string | no | Dot-separated path (e.g. `"sync"`, `"io.util"`) |
208
- | `itemType` | enum | no | Filter: `mod` `struct` `enum` `trait` `fn` `macro` `type` `constant` `static` `union` `attr` `derive` |
209
- | `feature` | string | no | Filter to items behind this feature gate (e.g. `"sync"`, `"fs"`) |
210
- | `version` | string | no | Pinned version |
211
-
212
- ---
213
-
214
- ### `lookup_crate_item`
215
-
216
- Retrieves full documentation for a single item. Auto-discovers the module path if omitted by searching `all.html`. Falls back to fuzzy suggestions if the exact item is not found.
217
-
218
- | Parameter | Type | Required | Description |
219
- |---|---|---|---|
220
- | `crateName` | string | yes | Crate name |
221
- | `itemType` | enum | yes | Item type (see `get_crate_items`) |
222
- | `itemName` | string | yes | Item name (e.g. `"Mutex"`, `"spawn"`) |
223
- | `modulePath` | string | no | Dot-separated module path. Auto-discovered if omitted. |
224
- | `version` | string | no | Pinned version |
225
- | `includeImpls` | boolean | no | Include trait implementation list |
226
- | `includeExamples` | boolean | no | Include code examples |
227
-
228
- ---
229
-
230
- ### `search_crate`
231
-
232
- 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.
233
-
234
- | Parameter | Type | Required | Description |
235
- |---|---|---|---|
236
- | `crateName` | string | yes | Crate name |
237
- | `query` | string | yes | Search query (case-insensitive) |
238
- | `version` | string | no | Pinned version |
239
-
240
- ---
241
-
242
- ### `search_crates`
243
-
244
- Search for Rust crates on crates.io by keyword.
245
-
246
- | Parameter | Type | Required | Description |
247
- |---|---|---|---|
248
- | `query` | string | yes | Search keywords |
249
- | `page` | number | no | Page number (default 1) |
250
- | `perPage` | number | no | Results per page (default 10, max 50) |
251
-
252
- ---
253
-
254
- ### `get_crate_versions`
255
-
256
- List all published versions of a crate from crates.io.
257
-
258
- | Parameter | Type | Required | Description |
259
- |---|---|---|---|
260
- | `crateName` | string | yes | Crate name |
261
-
262
- ---
263
-
264
- ### `get_source_code`
265
-
266
- Fetch the raw source code of a file from docs.rs (or doc.rust-lang.org for std crates).
267
-
268
- | Parameter | Type | Required | Description |
269
- |---|---|---|---|
270
- | `crateName` | string | yes | Crate name |
271
- | `path` | string | yes | Source path relative to crate root (e.g. `"src/lib.rs"`, `"src/sync/mutex.rs"`) |
272
- | `version` | string | no | Pinned version |
273
-
274
- ---
275
-
276
- ### `batch_lookup`
277
-
278
- Look up multiple items in a single call. Returns a compact summary (signature + short doc) for each item. Saves round-trips when you need several items at once.
279
-
280
- | Parameter | Type | Required | Description |
281
- |---|---|---|---|
282
- | `crateName` | string | yes | Crate name |
283
- | `items` | array | yes | Items to look up (max 20). Each: `{ itemType, itemName, modulePath? }` |
284
- | `version` | string | no | Pinned version |
285
-
286
- ---
287
-
288
- ### `get_crate_changelog`
289
-
290
- Fetch recent GitHub releases for a crate. Requires the crate to have a GitHub repository link on crates.io.
291
-
292
- | Parameter | Type | Required | Description |
293
- |---|---|---|---|
294
- | `crateName` | string | yes | Crate name |
295
- | `count` | number | no | Number of releases (default 5, max 20) |
296
-
297
- ---
298
-
299
- ### `resolve_type`
300
-
301
- Resolve a full Rust type path to its documentation. Parses the path to determine the crate, module, and item name automatically.
302
-
303
- | Parameter | Type | Required | Description |
304
- |---|---|---|---|
305
- | `typePath` | string | yes | Full Rust type path (e.g. `"tokio::sync::Mutex"`, `"std::collections::HashMap"`) |
306
- | `version` | string | no | Pinned version |
307
-
308
- ---
309
-
310
- ## Recommended workflows
311
-
312
- ### Exploring a new crate
313
-
314
- 1. `get_crate_brief` with `focusModules` targeting the modules you care about
315
- 2. `search_crate` to find specific types or functions
316
- 3. `lookup_crate_item` for detailed signatures and docs (no need to specify `modulePath` — it auto-discovers)
317
-
318
- ### Understanding feature flags
319
-
320
- 1. `get_crate_metadata` to see all features, their activations, and MSRV
321
- 2. `get_crate_items` with the `feature` parameter to see items behind a specific feature gate
322
-
323
- ### Finding the right type
324
-
325
- 1. `search_crate` with a keyword
326
- 2. `lookup_crate_item` with `includeImpls: true` to see what traits it implements
327
- 3. `resolve_type` to chase cross-crate type paths (e.g. `tokio::sync::Mutex`)
328
-
329
- ### Bulk lookups
330
-
331
- 1. `batch_lookup` to fetch signatures and docs for multiple items in one call
332
- 2. `get_crate_changelog` to see what changed in recent releases
333
-
334
- ---
335
-
336
84
  ## Architecture
337
85
 
338
- ```
339
- src/
340
- index.ts Entry pointregisters 12 tools + prompt, starts stdio server
341
- lib.ts HTTP (native fetch), URL builders, DOM helpers, crates.io API, extractors
342
- cache.ts LRU cache (500 entries, 5-min TTL, stale-while-revalidate)
343
- tools/
344
- shared.ts Shared Zod schemas (itemTypeEnum, versionParam)
345
- lookup-docs.ts lookup_crate_docs
346
- get-items.ts get_crate_items
347
- lookup-item.ts lookup_crate_item
348
- search.ts search_crate
349
- search-crates.ts search_crates
350
- crate-versions.ts get_crate_versions
351
- source-code.ts get_source_code
352
- crate-metadata.ts get_crate_metadata
353
- crate-brief.ts get_crate_brief
354
- batch-lookup.ts batch_lookup
355
- crate-changelog.ts get_crate_changelog
356
- resolve-type.ts resolve_type
357
- ```
358
-
359
- ### Data sources
360
-
361
- - **docs.rs** — HTML pages parsed with cheerio for surgical DOM extraction (only the elements needed, not full-page conversion)
362
- - **doc.rust-lang.org** — Same rustdoc HTML format, used for `std`, `core`, and `alloc`
363
- - **crates.io API** — JSON endpoints for metadata, features, dependencies, and search
364
- - **GitHub API** — Release notes for changelogs (via repository link from crates.io)
365
-
366
- ### Design decisions
367
-
368
- - **Native fetch, zero HTTP deps** — Uses Node.js built-in `fetch` with `AbortController` timeouts. No axios, no node-fetch.
369
- - **cheerio for DOM extraction** — Extracts only specific DOM elements (`.item-decl`, `.top-doc`, `.code-header`, `.stab.portability`) to minimize token usage. Also powers `cleanHtml()` as a replacement for `html-to-text`.
370
- - **LRU cache with stale-while-revalidate** — 500-entry cap with LRU eviction. Stale entries (past 5-min TTL but within 15-min grace) are served immediately while a background refresh runs.
371
- - **Retry with backoff** — Transient 5xx errors are retried up to 2 times with exponential backoff.
372
- - **Ranked search** — `all.html` contains every public item; scoring by exact/prefix/substring gives better results than flat substring matching.
373
- - **Auto-discovery** — `lookup_crate_item` searches `all.html` to find the module path when not provided, and falls back to fuzzy suggestions when an exact match fails.
374
- - **Version parameter everywhere** — Agents working on projects with pinned dependencies can read docs for specific versions.
375
- - **Optional sections** — `includeImpls` and `includeExamples` default to off so the base response stays compact; agents opt in when they need more detail.
86
+ - **cheerio** — surgical DOM extraction from rustdoc HTML
87
+ - **Native fetch** — `AbortController` timeouts, retry with backoff on 5xx
88
+ - **LRU cache** 500 entries, 5-min TTL, 15-min stale-while-revalidate
89
+ - **Ranked search** exact > prefix > substring scoring on `all.html`
90
+ - **Auto-discovery** `lookup_crate_item` finds module paths via `all.html`, fuzzy fallback on miss
376
91
 
377
92
  ## License
378
93
 
package/dist/index.js CHANGED
@@ -1,26 +1,26 @@
1
1
  #!/usr/bin/env node
2
- import{McpServer as lt}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as ut}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as g}from"zod";import{load as X}from"cheerio";const dt=300*1e3,ht=900*1e3,ft=500,D=new Map;function F(e){const t=D.get(e);if(!t)return null;const o=Date.now();return o>t.staleExpiry?(D.delete(e),null):(t.lastAccess=o,t.data)}function pt(e){const t=D.get(e);return t?Date.now()>t.expiry:!0}function P(e,t,o=dt){for(;D.size>=ft;)mt();const s=Date.now();D.set(e,{data:t,expiry:s+o,staleExpiry:s+ht,lastAccess:s})}function mt(){let e=null,t=1/0;for(const[o,s]of D)s.lastAccess<t&&(t=s.lastAccess,e=o);e&&D.delete(e)}const et="https://docs.rs",K="https://crates.io/api/v1",st="mcp-rust-docs/4.0.0",ot=6e3,V=100,H={modules:"mod",structs:"struct",enums:"enum",traits:"trait",functions:"fn",macros:"macro",types:"type",constants:"constant",statics:"static",unions:"union",attributes:"attr",derives:"derive",reexports:"reexport"},Y={struct:"struct.",enum:"enum.",trait:"trait.",fn:"fn.",macro:"macro.",type:"type.",constant:"constant.",static:"static.",union:"union.",attr:"attr.",derive:"derive."},$t=new Set(["std","core","alloc"]);function G(e){return $t.has(e)}function gt(e,t){return`https://doc.rust-lang.org/stable/${e}/${t}`}function nt(e){return e.replace(/-/g,"_")}function j(e,t="index.html",o="latest"){return G(e)?gt(e,t):`${et}/${e}/${o}/${nt(e)}/${t}`}function W(e){return e?e.replace(/\./g,"/")+"/":""}function rt(e){return e?e.replace(/\./g,"::")+"::":""}class J extends Error{constructor(t,o){super(o),this.status=t}}async function q(e,t=2,o=500){for(let s=0;s<=t;s++)try{return await e()}catch(n){if(s===t||!(n instanceof J&&n.status>=500))throw n;console.log(`[retry ${s+1}/${t}] ${n.message}`),await new Promise(l=>setTimeout(l,o*(s+1)))}throw new Error("unreachable")}async function tt(e,t=15e3){const o=new AbortController,s=setTimeout(()=>o.abort(),t);try{const n=await fetch(e,{headers:{"User-Agent":st},signal:o.signal});if(!n.ok)throw new J(n.status,`HTTP ${n.status} for ${e}`);return await n.text()}finally{clearTimeout(s)}}async function U(e,t=1e4){const o=new AbortController,s=setTimeout(()=>o.abort(),t);try{const n=await fetch(e,{headers:{"User-Agent":st},signal:o.signal});if(!n.ok)throw new J(n.status,`HTTP ${n.status} for ${e}`);return await n.json()}finally{clearTimeout(s)}}async function I(e){const t=`dom:${e}`,o=F(t);if(o)return console.log(`[cache hit] ${e}`),pt(t)&&(console.log(`[stale refresh] ${e}`),q(()=>tt(e)).then(n=>P(t,n)).catch(()=>{})),X(o);const s=await q(()=>tt(e));return P(t,s),X(s)}function z(e){const t=X(`<body>${e}</body>`);return t("img").remove(),t("summary.hideme").remove(),t("p, div, br, h1, h2, h3, h4, h5, h6, li, tr").each((o,s)=>{t(s).before(`
2
+ import{McpServer as lt}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as ut}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as g}from"zod";import{load as X}from"cheerio";const dt=300*1e3,ht=900*1e3,pt=500,D=new Map;function F(e){const t=D.get(e);if(!t)return null;const o=Date.now();return o>t.staleExpiry?(D.delete(e),null):(t.lastAccess=o,t.data)}function ft(e){const t=D.get(e);return t?Date.now()>t.expiry:!0}function P(e,t,o=dt){for(;D.size>=pt;)mt();const s=Date.now();D.set(e,{data:t,expiry:s+o,staleExpiry:s+ht,lastAccess:s})}function mt(){let e=null,t=1/0;for(const[o,s]of D)s.lastAccess<t&&(t=s.lastAccess,e=o);e&&D.delete(e)}const et="https://docs.rs",K="https://crates.io/api/v1",st="mcp-rust-docs/4.0.0",ot=6e3,V=100,H={modules:"mod",structs:"struct",enums:"enum",traits:"trait",functions:"fn",macros:"macro",types:"type",constants:"constant",statics:"static",unions:"union",attributes:"attr",derives:"derive",reexports:"reexport"},Y={struct:"struct.",enum:"enum.",trait:"trait.",fn:"fn.",macro:"macro.",type:"type.",constant:"constant.",static:"static.",union:"union.",attr:"attr.",derive:"derive."},$t=new Set(["std","core","alloc"]);function G(e){return $t.has(e)}function gt(e,t){return`https://doc.rust-lang.org/stable/${e}/${t}`}function nt(e){return e.replace(/-/g,"_")}function j(e,t="index.html",o="latest"){return G(e)?gt(e,t):`${et}/${e}/${o}/${nt(e)}/${t}`}function W(e){return e?e.replace(/\./g,"/")+"/":""}function rt(e){return e?e.replace(/\./g,"::")+"::":""}class J extends Error{constructor(t,o){super(o),this.status=t}}async function q(e,t=2,o=500){for(let s=0;s<=t;s++)try{return await e()}catch(n){if(s===t||!(n instanceof J&&n.status>=500))throw n;console.log(`[retry ${s+1}/${t}] ${n.message}`),await new Promise(l=>setTimeout(l,o*(s+1)))}throw new Error("unreachable")}async function tt(e,t=15e3){const o=new AbortController,s=setTimeout(()=>o.abort(),t);try{const n=await fetch(e,{headers:{"User-Agent":st},signal:o.signal});if(!n.ok)throw new J(n.status,`HTTP ${n.status} for ${e}`);return await n.text()}finally{clearTimeout(s)}}async function U(e,t=1e4){const o=new AbortController,s=setTimeout(()=>o.abort(),t);try{const n=await fetch(e,{headers:{"User-Agent":st},signal:o.signal});if(!n.ok)throw new J(n.status,`HTTP ${n.status} for ${e}`);return await n.json()}finally{clearTimeout(s)}}async function I(e){const t=`dom:${e}`,o=F(t);if(o)return console.log(`[cache hit] ${e}`),ft(t)&&(console.log(`[stale refresh] ${e}`),q(()=>tt(e)).then(n=>P(t,n)).catch(()=>{})),X(o);const s=await q(()=>tt(e));return P(t,s),X(s)}function z(e){const t=X(`<body>${e}</body>`);return t("img").remove(),t("summary.hideme").remove(),t("p, div, br, h1, h2, h3, h4, h5, h6, li, tr").each((o,s)=>{t(s).before(`
3
3
  `)}),t("body").text().replace(/\n{3,}/g,`
4
4
 
5
5
  `).trim()}function T(e,t){return e.length>t?e.slice(0,t)+`
6
6
 
7
- […truncated]`:e}function v(e){return{content:[{type:"text",text:e}]}}function k(e){return{content:[{type:"text",text:`Error: ${e}`}],isError:!0}}async function Q(e){const t=`crate-info:${e}`,o=F(t);if(o)return console.log(`[cache hit] crate-info ${e}`),o;const n=(await q(()=>U(`${K}/crates/${e}`))).crate,r={name:n.name,version:n.max_stable_version||n.max_version,description:n.description,documentation:n.documentation??null,repository:n.repository??null,downloads:n.downloads};return P(t,r),r}async function at(e,t){const o=`crate-version:${e}@${t}`,s=F(o);if(s)return console.log(`[cache hit] crate-version ${e}@${t}`),s;const r=(await q(()=>U(`${K}/crates/${e}/${t}`))).version,l=r.features??{},i={num:r.num,features:l,defaultFeatures:l.default??[],yanked:r.yanked,license:r.license,msrv:r.rust_version??null};return P(o,i),i}async function yt(e,t){const o=`crate-deps:${e}@${t}`,s=F(o);if(s)return console.log(`[cache hit] crate-deps ${e}@${t}`),s;const r=((await q(()=>U(`${K}/crates/${e}/${t}/dependencies`))).dependencies??[]).map(l=>({name:l.crate_id,req:l.req,optional:l.optional,kind:l.kind,features:l.features??[]}));return P(o,r),r}async function B(e,t,o="latest"){const s=j(e,"all.html",o),n=await I(s),r=t.toLowerCase(),l=[];return n("h3").each((i,c)=>{const d=n(c).attr("id")??"",h=H[d]??d;n(c).next("ul.all-items").find("li a").each((p,f)=>{const a=n(f).text().trim(),u=a.toLowerCase(),m=a.includes("::")?a.split("::").pop():a,y=m.toLowerCase();let w=0;if(y===r?w=100:u===r?w=95:y.startsWith(r)?w=60:u.startsWith(r)?w=55:u.includes(r)&&(w=20),w>0){const x=a.split("::"),b=x.length>1?x.slice(0,-1).join("."):"";l.push({type:h,name:a,path:a,modulePath:b,bareName:m,score:w})}})}),l.sort((i,c)=>c.score-i.score||i.name.localeCompare(c.name)),l}function Z(e){return e(".item-info .stab.portability").first().text().trim()||null}function bt(e){const t=[];return e("div.example-wrap pre.rust").each((o,s)=>{const n=e(s).text().trim();n&&t.push(n)}),t}function wt(e){const t=[];return e("#trait-implementations-list > details > summary h3.code-header").each((o,s)=>{t.push(e(s).text().trim())}),t}function it(e){const t=[];return e("h2#reexports").next("dl.item-table").find("dt code").each((o,s)=>{t.push(e(s).text().trim())}),t}const N=g.enum(["mod","struct","enum","trait","fn","macro","type","constant","static","union","attr","derive"]),A=g.string().optional().describe('Crate version (e.g. "1.49.0"). Defaults to latest.');function vt(e){e.tool("lookup_crate_docs","Fetch the main documentation for a Rust crate. Returns overview, version, sections, and re-exports.",{crateName:g.string().describe('Crate name (e.g. "tokio", "serde-json")'),version:A},async({crateName:t,version:o})=>{try{const s=o??"latest",n=j(t,"index.html",s),r=await I(n),l=r(".sidebar-crate .version").text().trim()||s,i=T(z(r("details.toggle.top-doc").html()??""),ot),c=[];r("h2.section-header").each((p,f)=>{const a=r(f).attr("id")??"",u=r(f).next("dl.item-table").find("dt").length;a&&u&&c.push(` ${r(f).text().trim()} (${u})`)});const d=it(r),h=[`# ${t} v${l}`,n,"",i];return d.length&&h.push("","## Re-exports",...d.map(p=>` ${p}`)),h.push("","## Sections",...c),v(h.join(`
8
- `))}catch(s){return k(`Could not fetch docs for "${t}". ${s.message}`)}})}function xt(e){e.tool("get_crate_items","List public items in a crate root or module. Returns names, types, feature gates, and short descriptions. Supports filtering by item type and feature gate.",{crateName:g.string().describe("Crate name"),modulePath:g.string().optional().describe('Dot-separated module path (e.g. "sync", "io.util"). Omit for crate root.'),itemType:N.optional().describe("Filter results to a single item type"),feature:g.string().optional().describe('Filter to items gated behind this feature (e.g. "sync", "fs")'),version:A},async({crateName:t,modulePath:o,itemType:s,feature:n,version:r})=>{try{const l=r??"latest",i=j(t,`${W(o)}index.html`,l),c=await I(i),d=[];c("h2.section-header").each((a,u)=>{const m=c(u).attr("id")??"",y=H[m]??m;s&&y!==s||c(u).next("dl.item-table").find("dt").each((w,x)=>{const b=c(x),S=b.find("a").first().text().trim(),_=b.next("dd").text().trim(),C=b.find(".stab.portability code").first().text().trim();if(!S||n&&(!C||!C.toLowerCase().includes(n.toLowerCase())))return;const $=C?` [feature: ${C}]`:"";d.push(`[${y}] ${S}${$} — ${_}`)})});const h=o?`${t}::${o.replace(/\./g,"::")}`:t,p=[];s&&p.push(`type: ${s}`),n&&p.push(`feature: ${n}`);const f=p.length?` (${p.join(", ")})`:"";return d.length?v([`# Items in ${h}${f}`,i,"",...d].join(`
9
- `)):v(`No items found in ${h}${f}.`)}catch(l){return k(`Could not list items. ${l.message}`)}})}function _t(e){e.tool("lookup_crate_item","Get detailed documentation for a specific item. Returns signature, docs, feature gate, methods, trait impls, and optionally examples. Auto-discovers modulePath if omitted.",{crateName:g.string().describe("Crate name"),itemType:N.describe("Item type"),itemName:g.string().describe('Item name (e.g. "Mutex", "spawn", "Serialize")'),modulePath:g.string().optional().describe('Dot-separated module path (e.g. "sync"). Auto-discovered if omitted.'),version:A,includeExamples:g.boolean().optional().describe("Include code examples from the docs. Default: false."),includeImpls:g.boolean().optional().describe("Include trait implementation list. Default: false.")},async({crateName:t,itemType:o,itemName:s,modulePath:n,version:r,includeExamples:l,includeImpls:i})=>{try{const c=r??"latest";let d=n;if(d===void 0){const $=await B(t,s,c),R=$.find(E=>E.bareName.toLowerCase()===s.toLowerCase()&&E.type===o)??$.find(E=>E.bareName.toLowerCase()===s.toLowerCase());R&&(d=R.modulePath||void 0,console.log(`[auto-discovery] ${s} → ${R.name} (${R.type})`))}const h=W(d),p=o==="mod"?`${h}${s}/index.html`:`${h}${Y[o]??`${o}.`}${s}.html`,f=j(t,p,c);let a;try{a=await I(f)}catch($){const E=(await B(t,s,c)).filter(M=>M.bareName.toLowerCase().includes(s.toLowerCase())||s.toLowerCase().includes(M.bareName.toLowerCase()));if(E.length){const M=E.slice(0,10).map(O=>` [${O.type}] ${t}::${O.name}`);return v([`Could not find ${o} "${s}" at the expected path.`,"","Did you mean one of these?",...M,"","Tip: use search_crate to find the exact name and module path."].join(`
10
- `))}throw $}const u=`${t}::${rt(d)}${s}`,m=a("pre.rust.item-decl").text().trim(),y=Z(a),w=T(z(a("details.toggle.top-doc").html()??""),ot),x=[];a("#implementations-list section.method h4.code-header").each(($,R)=>{x.push(a(R).text().trim())});const b=[];a("h2#required-methods").first().nextUntil("h2").find("section h4.code-header").each(($,R)=>{b.push(a(R).text().trim())});const S=[];a("h2#provided-methods").first().nextUntil("h2").find("section h4.code-header").each(($,R)=>{S.push(a(R).text().trim())});const _=[];a("section.variant h3.code-header, div.variant h3.code-header").each(($,R)=>{_.push(a(R).text().trim())});const C=[`# ${o} ${u}`,f,""];if(y&&C.push(`> ${y}`,""),m&&C.push("## Signature","```rust",m,"```",""),w&&C.push("## Documentation",w,""),_.length&&C.push(`## Variants (${_.length})`,..._.map($=>` ${$}`),""),b.length&&C.push(`## Required Methods (${b.length})`,...b.map($=>` ${$}`),""),S.length&&C.push(`## Provided Methods (${S.length})`,...S.map($=>` ${$}`),""),x.length&&C.push(`## Methods (${x.length})`,...x.map($=>` ${$}`),""),i){const $=wt(a);$.length&&C.push(`## Trait Implementations (${$.length})`,...$.map(R=>` ${R}`),"")}if(l){const $=bt(a);$.length&&(C.push(`## Examples (${$.length})`),$.forEach((R,E)=>{C.push(`### Example ${E+1}`,"```rust",R,"```","")}))}return v(C.join(`
11
- `))}catch(c){return k(`Could not fetch ${o} "${s}". ${c.message}`)}})}function Ct(e,t){const o=e.toLowerCase(),s=t.toLowerCase(),n=o.includes("::")?o.split("::").pop():o;return n===s?100:o===s?95:n.startsWith(s)?60:o.startsWith(s)?55:o.includes(s)?20:0}function Rt(e){e.tool("search_crate","Search for items by name within a Rust crate. Returns ranked results with canonical paths and item types.",{crateName:g.string().describe("Crate name"),query:g.string().describe("Search query (matched against item names)"),version:A},async({crateName:t,query:o,version:s})=>{try{const r=j(t,"all.html",s??"latest"),l=await I(r),i=[];if(l("h3").each((p,f)=>{const a=l(f).attr("id")??"",u=H[a]??a;l(f).next("ul.all-items").find("li a").each((m,y)=>{const w=l(y).text().trim(),x=l(y).attr("href")??"",b=Ct(w,o);b>0&&i.push({type:u,name:w,href:x,score:b})})}),!i.length)return v(`No matches for "${o}" in ${t}.`);i.sort((p,f)=>f.score-p.score||p.name.localeCompare(f.name));const d=i.slice(0,V).map(p=>{const f=`${t}::${p.name.replace(/::/g,"::")}`;return`[${p.type}] ${f}`}),h=i.length>V?` (showing first ${V})`:"";return v([`# "${o}" in ${t} — ${i.length} matches${h}`,"",...d].join(`
7
+ […truncated]`:e}function v(e){return{content:[{type:"text",text:e}]}}function k(e){return{content:[{type:"text",text:`Error: ${e}`}],isError:!0}}async function Q(e){const t=`crate-info:${e}`,o=F(t);if(o)return console.log(`[cache hit] crate-info ${e}`),o;const n=(await q(()=>U(`${K}/crates/${e}`))).crate,a={name:n.name,version:n.max_stable_version||n.max_version,description:n.description,documentation:n.documentation??null,repository:n.repository??null,downloads:n.downloads};return P(t,a),a}async function at(e,t){const o=`crate-version:${e}@${t}`,s=F(o);if(s)return console.log(`[cache hit] crate-version ${e}@${t}`),s;const a=(await q(()=>U(`${K}/crates/${e}/${t}`))).version,l=a.features??{},i={num:a.num,features:l,defaultFeatures:l.default??[],yanked:a.yanked,license:a.license,msrv:a.rust_version??null};return P(o,i),i}async function yt(e,t){const o=`crate-deps:${e}@${t}`,s=F(o);if(s)return console.log(`[cache hit] crate-deps ${e}@${t}`),s;const a=((await q(()=>U(`${K}/crates/${e}/${t}/dependencies`))).dependencies??[]).map(l=>({name:l.crate_id,req:l.req,optional:l.optional,kind:l.kind,features:l.features??[]}));return P(o,a),a}async function B(e,t,o="latest"){const s=j(e,"all.html",o),n=await I(s),a=t.toLowerCase(),l=[];return n("h3").each((i,c)=>{const d=n(c).attr("id")??"",p=H[d]??d;n(c).next("ul.all-items").find("li a").each((h,f)=>{const r=n(f).text().trim(),u=r.toLowerCase(),m=r.includes("::")?r.split("::").pop():r,y=m.toLowerCase();let w=0;if(y===a?w=100:u===a?w=95:y.startsWith(a)?w=60:u.startsWith(a)?w=55:u.includes(a)&&(w=20),w>0){const x=r.split("::"),b=x.length>1?x.slice(0,-1).join("."):"";l.push({type:p,name:r,path:r,modulePath:b,bareName:m,score:w})}})}),l.sort((i,c)=>c.score-i.score||i.name.localeCompare(c.name)),l}function Z(e){return e(".item-info .stab.portability").first().text().trim()||null}function bt(e){const t=[];return e("div.example-wrap pre.rust").each((o,s)=>{const n=e(s).text().trim();n&&t.push(n)}),t}function wt(e){const t=[];return e("#trait-implementations-list > details > summary h3.code-header").each((o,s)=>{t.push(e(s).text().trim())}),t}function it(e){const t=[];return e("h2#reexports").next("dl.item-table").find("dt code").each((o,s)=>{t.push(e(s).text().trim())}),t}const N=g.enum(["mod","struct","enum","trait","fn","macro","type","constant","static","union","attr","derive"]),A=g.string().optional().describe('Crate version (e.g. "1.49.0"). Defaults to latest.');function vt(e){e.tool("lookup_crate_docs","Fetch the main documentation for a Rust crate. Returns overview, version, sections, and re-exports.",{crateName:g.string().describe('Crate name (e.g. "tokio", "serde-json")'),version:A},async({crateName:t,version:o})=>{try{const s=o??"latest",n=j(t,"index.html",s),a=await I(n),l=a(".sidebar-crate .version").text().trim()||s,i=T(z(a("details.toggle.top-doc").html()??""),ot),c=[];a("h2.section-header").each((h,f)=>{const r=a(f).attr("id")??"",u=a(f).next("dl.item-table").find("dt").length;r&&u&&c.push(` ${a(f).text().trim()} (${u})`)});const d=it(a),p=[`# ${t} v${l}`,n,"",i];return d.length&&p.push("","## Re-exports",...d.map(h=>` ${h}`)),p.push("","## Sections",...c),v(p.join(`
8
+ `))}catch(s){return k(`Could not fetch docs for "${t}". ${s.message}`)}})}function xt(e){e.tool("get_crate_items","List public items in a crate root or module. Returns names, types, feature gates, and short descriptions. Supports filtering by item type and feature gate.",{crateName:g.string().describe("Crate name"),modulePath:g.string().optional().describe('Dot-separated module path (e.g. "sync", "io.util"). Omit for crate root.'),itemType:N.optional().describe("Filter results to a single item type"),feature:g.string().optional().describe('Filter to items gated behind this feature (e.g. "sync", "fs")'),version:A},async({crateName:t,modulePath:o,itemType:s,feature:n,version:a})=>{try{const l=a??"latest",i=j(t,`${W(o)}index.html`,l),c=await I(i),d=[];c("h2.section-header").each((r,u)=>{const m=c(u).attr("id")??"",y=H[m]??m;s&&y!==s||c(u).next("dl.item-table").find("dt").each((w,x)=>{const b=c(x),S=b.find("a").first().text().trim(),_=b.next("dd").text().trim(),C=b.find(".stab.portability code").first().text().trim();if(!S||n&&(!C||!C.toLowerCase().includes(n.toLowerCase())))return;const $=C?` [feature: ${C}]`:"";d.push(`[${y}] ${S}${$} — ${_}`)})});const p=o?`${t}::${o.replace(/\./g,"::")}`:t,h=[];s&&h.push(`type: ${s}`),n&&h.push(`feature: ${n}`);const f=h.length?` (${h.join(", ")})`:"";return d.length?v([`# Items in ${p}${f}`,i,"",...d].join(`
9
+ `)):v(`No items found in ${p}${f}.`)}catch(l){return k(`Could not list items. ${l.message}`)}})}function _t(e){e.tool("lookup_crate_item","Get detailed documentation for a specific item. Returns signature, docs, feature gate, methods, trait impls, and optionally examples. Auto-discovers modulePath if omitted.",{crateName:g.string().describe("Crate name"),itemType:N.describe("Item type"),itemName:g.string().describe('Item name (e.g. "Mutex", "spawn", "Serialize")'),modulePath:g.string().optional().describe('Dot-separated module path (e.g. "sync"). Auto-discovered if omitted.'),version:A,includeExamples:g.boolean().optional().describe("Include code examples from the docs. Default: false."),includeImpls:g.boolean().optional().describe("Include trait implementation list. Default: false.")},async({crateName:t,itemType:o,itemName:s,modulePath:n,version:a,includeExamples:l,includeImpls:i})=>{try{const c=a??"latest";let d=n;if(d===void 0){const $=await B(t,s,c),R=$.find(E=>E.bareName.toLowerCase()===s.toLowerCase()&&E.type===o)??$.find(E=>E.bareName.toLowerCase()===s.toLowerCase());R&&(d=R.modulePath||void 0,console.log(`[auto-discovery] ${s} → ${R.name} (${R.type})`))}const p=W(d),h=o==="mod"?`${p}${s}/index.html`:`${p}${Y[o]??`${o}.`}${s}.html`,f=j(t,h,c);let r;try{r=await I(f)}catch($){const E=(await B(t,s,c)).filter(M=>M.bareName.toLowerCase().includes(s.toLowerCase())||s.toLowerCase().includes(M.bareName.toLowerCase()));if(E.length){const M=E.slice(0,10).map(O=>` [${O.type}] ${t}::${O.name}`);return v([`Could not find ${o} "${s}" at the expected path.`,"","Did you mean one of these?",...M,"","Tip: use search_crate to find the exact name and module path."].join(`
10
+ `))}throw $}const u=`${t}::${rt(d)}${s}`,m=r("pre.rust.item-decl").text().trim(),y=Z(r),w=T(z(r("details.toggle.top-doc").html()??""),ot),x=[];r("#implementations-list section.method h4.code-header").each(($,R)=>{x.push(r(R).text().trim())});const b=[];r("h2#required-methods").first().nextUntil("h2").find("section h4.code-header").each(($,R)=>{b.push(r(R).text().trim())});const S=[];r("h2#provided-methods").first().nextUntil("h2").find("section h4.code-header").each(($,R)=>{S.push(r(R).text().trim())});const _=[];r("section.variant h3.code-header, div.variant h3.code-header").each(($,R)=>{_.push(r(R).text().trim())});const C=[`# ${o} ${u}`,f,""];if(y&&C.push(`> ${y}`,""),m&&C.push("## Signature","```rust",m,"```",""),w&&C.push("## Documentation",w,""),_.length&&C.push(`## Variants (${_.length})`,..._.map($=>` ${$}`),""),b.length&&C.push(`## Required Methods (${b.length})`,...b.map($=>` ${$}`),""),S.length&&C.push(`## Provided Methods (${S.length})`,...S.map($=>` ${$}`),""),x.length&&C.push(`## Methods (${x.length})`,...x.map($=>` ${$}`),""),i){const $=wt(r);$.length&&C.push(`## Trait Implementations (${$.length})`,...$.map(R=>` ${R}`),"")}if(l){const $=bt(r);$.length&&(C.push(`## Examples (${$.length})`),$.forEach((R,E)=>{C.push(`### Example ${E+1}`,"```rust",R,"```","")}))}return v(C.join(`
11
+ `))}catch(c){return k(`Could not fetch ${o} "${s}". ${c.message}`)}})}function Ct(e,t){const o=e.toLowerCase(),s=t.toLowerCase(),n=o.includes("::")?o.split("::").pop():o;return n===s?100:o===s?95:n.startsWith(s)?60:o.startsWith(s)?55:o.includes(s)?20:0}function Rt(e){e.tool("search_crate","Search for items by name within a Rust crate. Returns ranked results with canonical paths and item types.",{crateName:g.string().describe("Crate name"),query:g.string().describe("Search query (matched against item names)"),version:A},async({crateName:t,query:o,version:s})=>{try{const a=j(t,"all.html",s??"latest"),l=await I(a),i=[];if(l("h3").each((h,f)=>{const r=l(f).attr("id")??"",u=H[r]??r;l(f).next("ul.all-items").find("li a").each((m,y)=>{const w=l(y).text().trim(),x=l(y).attr("href")??"",b=Ct(w,o);b>0&&i.push({type:u,name:w,href:x,score:b})})}),!i.length)return v(`No matches for "${o}" in ${t}.`);i.sort((h,f)=>f.score-h.score||h.name.localeCompare(f.name));const d=i.slice(0,V).map(h=>{const f=`${t}::${h.name.replace(/::/g,"::")}`;return`[${h.type}] ${f}`}),p=i.length>V?` (showing first ${V})`:"";return v([`# "${o}" in ${t} — ${i.length} matches${p}`,"",...d].join(`
12
12
  `))}catch(n){return k(`Could not search "${t}". ${n.message}`)}})}function St(e){e.tool("get_crate_metadata","Get crate metadata from crates.io: version, features, default features, optional dependencies, and links.",{crateName:g.string().describe("Crate name"),version:A},async({crateName:t,version:o})=>{try{if(G(t))return v(`"${t}" is part of the Rust standard library and is not published on crates.io.
13
- Use lookup_crate_docs, get_crate_items, lookup_crate_item, or search_crate to browse its documentation.`);const s=await Q(t),n=o??s.version,[r,l]=await Promise.all([at(t,n),yt(t,n)]),i=[`# ${s.name} v${r.num}`,"",`${s.description}`,"","## Links"];s.documentation&&i.push(` docs: ${s.documentation}`),s.repository&&i.push(` repo: ${s.repository}`),i.push(` crates.io: https://crates.io/crates/${s.name}`),i.push(` license: ${r.license}`),i.push(` downloads: ${s.downloads.toLocaleString()}`),r.msrv&&i.push(` msrv: ${r.msrv}`);const{features:c,defaultFeatures:d}=r;i.push("","## Features"),i.push(` default = [${d.join(", ")}]`);const h=Object.keys(c).filter(a=>a!=="default").sort();for(const a of h){const u=c[a],m=u.length?` = [${u.join(", ")}]`:"";i.push(` ${a}${m}`)}const p=l.filter(a=>a.optional&&a.kind==="normal");if(p.length){i.push("","## Optional Dependencies");for(const a of p)i.push(` ${a.name} ${a.req} (feature-gated)`)}const f=l.filter(a=>!a.optional&&a.kind==="normal");if(f.length){i.push("","## Required Dependencies");for(const a of f)i.push(` ${a.name} ${a.req}`)}return r.yanked&&i.push("","> WARNING: This version has been yanked."),v(i.join(`
14
- `))}catch(s){return k(`Could not fetch metadata for "${t}". ${s.message}`)}})}function kt(e){e.tool("get_crate_brief","Bundle call: fetches crate metadata, overview docs, module list, re-exports, and optionally items from focused modules — all in one shot.",{crateName:g.string().describe("Crate name"),version:A,focusModules:g.string().optional().describe('Comma-separated module names to expand (e.g. "sync,task,io"). Omit for overview only.')},async({crateName:t,version:o,focusModules:s})=>{try{const n=G(t);let r=null,l=null;if(!n){const u=await Q(t),m=o??u.version;l=await at(t,m),r=u}const i=n?"latest":o??r.version,c=j(t,"index.html",i),d=await I(c),h=T(z(d("details.toggle.top-doc").html()??""),3e3),p=it(d),f={};d("h2.section-header").each((u,m)=>{const y=d(m).attr("id")??"",w=H[y]??y,x=[];d(m).next("dl.item-table").find("dt").each((b,S)=>{const _=d(S),C=_.find("a").first().text().trim(),$=_.find(".stab.portability code").first().text().trim();C&&x.push($?`${C} [${$}]`:C)}),x.length&&(f[w]=x)});const a=[];if(n){const u=d(".sidebar-crate .version").text().trim()||"stable";a.push(`# ${t} (Rust standard library) v${u}`,c,"",`The "${t}" crate is part of the Rust standard library.`)}else{a.push(`# ${r.name} v${l.num}`,c,"",r.description,"",`license: ${l.license} | downloads: ${r.downloads.toLocaleString()}`),r.repository&&a.push(`repo: ${r.repository}`);const{defaultFeatures:u,features:m}=l;a.push("","## Features",` default = [${u.join(", ")}]`,` all: ${Object.keys(m).filter(y=>y!=="default").sort().join(", ")}`)}h&&a.push("","## Overview",h),p.length&&a.push("","## Re-exports",...p.map(u=>` ${u}`)),f.mod&&a.push("","## Modules",...f.mod.map(u=>` ${u}`));for(const[u,m]of Object.entries(f))u==="mod"||u==="reexport"||a.push("",`## ${u} (${m.length})`,...m.map(y=>` ${y}`));if(s){const u=s.split(",").map(m=>m.trim()).filter(Boolean);for(const m of u)try{const y=j(t,`${W(m)}index.html`,i),w=await I(y);a.push("",`## Focus: ${t}::${m.replace(/\./g,"::")}`,y),w("h2.section-header").each((x,b)=>{const S=w(b).attr("id")??"",_=H[S]??S;w(b).next("dl.item-table").find("dt").each((C,$)=>{const R=w($),E=R.find("a").first().text().trim(),M=R.next("dd").text().trim(),O=R.find(".stab.portability code").first().text().trim();if(!E)return;const ct=O?` [${O}]`:"";a.push(` [${_}] ${E}${ct} — ${M}`)})})}catch{a.push("",`## Focus: ${m}`," (module not found)")}}return v(a.join(`
15
- `))}catch(n){return k(`Could not fetch brief for "${t}". ${n.message}`)}})}function Lt(e){e.tool("search_crates","Search for Rust crates on crates.io by keyword. Returns name, description, downloads, and version.",{query:g.string().describe("Search keywords"),page:g.number().min(1).optional().describe("Page number (default 1)"),perPage:g.number().min(1).max(50).optional().describe("Results per page (default 10, max 50)")},async({query:t,page:o,perPage:s})=>{const n=o??1,r=s??10;try{const l=`search-crates:${t}:${n}:${r}`,i=F(l);if(i)return console.log(`[cache hit] search-crates "${t}" page=${n}`),v(i);const c=new URLSearchParams({q:t,per_page:String(r),page:String(n)}),d=await U(`${K}/crates?${c}`),h=d.crates??[];if(!h.length)return v(`No crates found for "${t}".`);const p=d.meta?.total??h.length,f=h.map(u=>{const m=u.max_stable_version||u.max_version,y=u.downloads.toLocaleString(),w=u.description?` — ${u.description.trim()}`:"";return` ${u.name} v${m} (${y} downloads)${w}`}),a=[`# Crate search: "${t}" — ${p} results (page ${n})`,"",...f].join(`
16
- `);return P(l,a),v(a)}catch(l){return k(`Could not search crates.io for "${t}". ${l.message}`)}})}function Et(e){e.tool("get_crate_versions","List all published versions of a crate from crates.io, with yanked status and release dates.",{crateName:g.string().describe("Crate name")},async({crateName:t})=>{try{if(G(t))return v(`"${t}" is part of the Rust standard library and is not published on crates.io.
17
- Its version matches the Rust toolchain version.`);const o=`crate-versions:${t}`,s=F(o);if(s)return console.log(`[cache hit] crate-versions ${t}`),v(s);const r=((await U(`${K}/crates/${t}/versions`)).versions??[]).map(c=>({num:c.num,yanked:c.yanked,created_at:c.created_at,license:c.license??""}));if(!r.length)return v(`No versions found for "${t}".`);const l=r.map(c=>{const d=c.created_at.slice(0,10),h=c.yanked?" [YANKED]":"";return` ${c.num} ${d}${h}`}),i=[`# ${t} — ${r.length} versions`,"",...l].join(`
18
- `);return P(o,i),v(i)}catch(o){return k(`Could not fetch versions for "${t}". ${o.message}`)}})}function It(e){e.tool("get_source_code","Fetch the source code of a Rust item from docs.rs. Returns the raw source implementation.",{crateName:g.string().describe("Crate name"),path:g.string().describe('Source path relative to crate root (e.g. "src/lib.rs", "src/sync/mutex.rs")'),version:A},async({crateName:t,path:o,version:s})=>{try{if(G(t)){const c=`https://doc.rust-lang.org/stable/src/${t}/${o}`,d=await I(c),h=d("#source-code").text().trim()||d("pre.rust").text().trim();return h?v([`# Source: ${t}/${o}`,c,"","```rust",T(h,12e3),"```"].join(`
19
- `)):k(`No source code found at ${c}`)}const r=`${et}/${t}/${s??"latest"}/src/${nt(t)}/${o}`,l=await I(r),i=l("#source-code").text().trim()||l("pre.rust").text().trim()||l(".src-line-numbers + code").text().trim();return i?v([`# Source: ${t}/${o}`,r,"","```rust",T(i,12e3),"```"].join(`
20
- `)):k(`No source code found at ${r}. Check that the path is correct.`)}catch(n){return k(`Could not fetch source for "${t}/${o}". ${n.message}`)}})}const jt=g.object({itemType:N.describe("Item type"),itemName:g.string().describe("Item name"),modulePath:g.string().optional().describe("Dot-separated module path")});function At(e){e.tool("batch_lookup","Look up multiple items in a single call. Returns a compact summary (signature + short doc) for each item. Saves round-trips when you need several items.",{crateName:g.string().describe("Crate name"),items:g.array(jt).min(1).max(20).describe("Items to look up (max 20)"),version:A},async({crateName:t,items:o,version:s})=>{const n=s??"latest",r=[`# Batch lookup: ${t} (${o.length} items)`,""],l=await Promise.allSettled(o.map(async({itemType:i,itemName:c,modulePath:d})=>{let h=d;if(h===void 0){const b=await B(t,c,n),S=b.find(_=>_.bareName.toLowerCase()===c.toLowerCase()&&_.type===i)??b.find(_=>_.bareName.toLowerCase()===c.toLowerCase());S&&(h=S.modulePath||void 0)}const p=W(h),f=i==="mod"?`${p}${c}/index.html`:`${p}${Y[i]??`${i}.`}${c}.html`,a=j(t,f,n),u=await I(a),m=`${t}::${rt(h)}${c}`,y=u("pre.rust.item-decl").text().trim(),w=Z(u),x=T(z(u("details.toggle.top-doc").html()??""),500);return{itemType:i,fullName:m,decl:y,featureGate:w,doc:x,url:a}}));for(let i=0;i<l.length;i++){const c=l[i],{itemType:d,itemName:h}=o[i];if(c.status==="fulfilled"){const{fullName:p,decl:f,featureGate:a,doc:u,url:m}=c.value;r.push(`## ${d} ${p}`,m),a&&r.push(`> ${a}`),f&&r.push("```rust",f,"```"),u&&r.push(u),r.push("")}else r.push(`## ${d} ${h}`,` Error: ${c.reason?.message??"unknown error"}`,"")}return v(r.join(`
13
+ Use lookup_crate_docs, get_crate_items, lookup_crate_item, or search_crate to browse its documentation.`);const s=await Q(t),n=o??s.version,[a,l]=await Promise.all([at(t,n),yt(t,n)]),i=[`# ${s.name} v${a.num}`,"",`${s.description}`,"","## Links"];s.documentation&&i.push(` docs: ${s.documentation}`),s.repository&&i.push(` repo: ${s.repository}`),i.push(` crates.io: https://crates.io/crates/${s.name}`),i.push(` license: ${a.license}`),i.push(` downloads: ${s.downloads.toLocaleString()}`),a.msrv&&i.push(` msrv: ${a.msrv}`);const{features:c,defaultFeatures:d}=a;i.push("","## Features"),i.push(` default = [${d.join(", ")}]`);const p=Object.keys(c).filter(r=>r!=="default").sort();for(const r of p){const u=c[r],m=u.length?` = [${u.join(", ")}]`:"";i.push(` ${r}${m}`)}const h=l.filter(r=>r.optional&&r.kind==="normal");if(h.length){i.push("","## Optional Dependencies");for(const r of h)i.push(` ${r.name} ${r.req} (feature-gated)`)}const f=l.filter(r=>!r.optional&&r.kind==="normal");if(f.length){i.push("","## Required Dependencies");for(const r of f)i.push(` ${r.name} ${r.req}`)}return a.yanked&&i.push("","> WARNING: This version has been yanked."),v(i.join(`
14
+ `))}catch(s){return k(`Could not fetch metadata for "${t}". ${s.message}`)}})}function kt(e){e.tool("get_crate_brief","Bundle call: fetches crate metadata, overview docs, module list, re-exports, and optionally items from focused modules — all in one shot.",{crateName:g.string().describe("Crate name"),version:A,focusModules:g.string().optional().describe('Comma-separated module names to expand (e.g. "sync,task,io"). Omit for overview only.')},async({crateName:t,version:o,focusModules:s})=>{try{const n=G(t);let a=null,l=null;if(!n){const u=await Q(t),m=o??u.version;l=await at(t,m),a=u}const i=n?"latest":o??a.version,c=j(t,"index.html",i),d=await I(c),p=T(z(d("details.toggle.top-doc").html()??""),3e3),h=it(d),f={};d("h2.section-header").each((u,m)=>{const y=d(m).attr("id")??"",w=H[y]??y,x=[];d(m).next("dl.item-table").find("dt").each((b,S)=>{const _=d(S),C=_.find("a").first().text().trim(),$=_.find(".stab.portability code").first().text().trim();C&&x.push($?`${C} [${$}]`:C)}),x.length&&(f[w]=x)});const r=[];if(n){const u=d(".sidebar-crate .version").text().trim()||"stable";r.push(`# ${t} (Rust standard library) v${u}`,c,"",`The "${t}" crate is part of the Rust standard library.`)}else{r.push(`# ${a.name} v${l.num}`,c,"",a.description,"",`license: ${l.license} | downloads: ${a.downloads.toLocaleString()}`),a.repository&&r.push(`repo: ${a.repository}`);const{defaultFeatures:u,features:m}=l;r.push("","## Features",` default = [${u.join(", ")}]`,` all: ${Object.keys(m).filter(y=>y!=="default").sort().join(", ")}`)}p&&r.push("","## Overview",p),h.length&&r.push("","## Re-exports",...h.map(u=>` ${u}`)),f.mod&&r.push("","## Modules",...f.mod.map(u=>` ${u}`));for(const[u,m]of Object.entries(f))u==="mod"||u==="reexport"||r.push("",`## ${u} (${m.length})`,...m.map(y=>` ${y}`));if(s){const u=s.split(",").map(m=>m.trim()).filter(Boolean);for(const m of u)try{const y=j(t,`${W(m)}index.html`,i),w=await I(y);r.push("",`## Focus: ${t}::${m.replace(/\./g,"::")}`,y),w("h2.section-header").each((x,b)=>{const S=w(b).attr("id")??"",_=H[S]??S;w(b).next("dl.item-table").find("dt").each((C,$)=>{const R=w($),E=R.find("a").first().text().trim(),M=R.next("dd").text().trim(),O=R.find(".stab.portability code").first().text().trim();if(!E)return;const ct=O?` [${O}]`:"";r.push(` [${_}] ${E}${ct} — ${M}`)})})}catch{r.push("",`## Focus: ${m}`," (module not found)")}}return v(r.join(`
15
+ `))}catch(n){return k(`Could not fetch brief for "${t}". ${n.message}`)}})}function Lt(e){e.tool("search_crates","Search for Rust crates on crates.io by keyword. Returns name, description, downloads, and version.",{query:g.string().describe("Search keywords"),page:g.number().min(1).optional().describe("Page number (default 1)"),perPage:g.number().min(1).max(50).optional().describe("Results per page (default 10, max 50)")},async({query:t,page:o,perPage:s})=>{const n=o??1,a=s??10;try{const l=`search-crates:${t}:${n}:${a}`,i=F(l);if(i)return console.log(`[cache hit] search-crates "${t}" page=${n}`),v(i);const c=new URLSearchParams({q:t,per_page:String(a),page:String(n)}),d=await U(`${K}/crates?${c}`),p=d.crates??[];if(!p.length)return v(`No crates found for "${t}".`);const h=d.meta?.total??p.length,f=p.map(u=>{const m=u.max_stable_version||u.max_version,y=u.downloads.toLocaleString(),w=u.description?` — ${u.description.trim()}`:"";return` ${u.name} v${m} (${y} downloads)${w}`}),r=[`# Crate search: "${t}" — ${h} results (page ${n})`,"",...f].join(`
16
+ `);return P(l,r),v(r)}catch(l){return k(`Could not search crates.io for "${t}". ${l.message}`)}})}function Et(e){e.tool("get_crate_versions","List all published versions of a crate from crates.io, with yanked status and release dates.",{crateName:g.string().describe("Crate name")},async({crateName:t})=>{try{if(G(t))return v(`"${t}" is part of the Rust standard library and is not published on crates.io.
17
+ Its version matches the Rust toolchain version.`);const o=`crate-versions:${t}`,s=F(o);if(s)return console.log(`[cache hit] crate-versions ${t}`),v(s);const a=((await U(`${K}/crates/${t}/versions`)).versions??[]).map(c=>({num:c.num,yanked:c.yanked,created_at:c.created_at,license:c.license??""}));if(!a.length)return v(`No versions found for "${t}".`);const l=a.map(c=>{const d=c.created_at.slice(0,10),p=c.yanked?" [YANKED]":"";return` ${c.num} ${d}${p}`}),i=[`# ${t} — ${a.length} versions`,"",...l].join(`
18
+ `);return P(o,i),v(i)}catch(o){return k(`Could not fetch versions for "${t}". ${o.message}`)}})}function It(e){e.tool("get_source_code","Fetch the source code of a Rust item from docs.rs. Returns the raw source implementation.",{crateName:g.string().describe("Crate name"),path:g.string().describe('Source path relative to crate root (e.g. "src/lib.rs", "src/sync/mutex.rs")'),version:A},async({crateName:t,path:o,version:s})=>{try{const n=o.replace(/^src\//,"");if(G(t)){const d=`https://doc.rust-lang.org/stable/src/${t}/${n}.html`,p=await I(d),h=p("#source-code").text().trim()||p("pre.rust").text().trim();return h?v([`# Source: ${t}/${o}`,d,"","```rust",T(h,12e3),"```"].join(`
19
+ `)):k(`No source code found at ${d}`)}const l=`${et}/${t}/${s??"latest"}/src/${nt(t)}/${n}.html`,i=await I(l),c=i("#source-code").text().trim()||i("pre.rust").text().trim()||i(".src-line-numbers + code").text().trim();return c?v([`# Source: ${t}/${o}`,l,"","```rust",T(c,12e3),"```"].join(`
20
+ `)):k(`No source code found at ${l}. Check that the path is correct.`)}catch(n){return k(`Could not fetch source for "${t}/${o}". ${n.message}`)}})}const jt=g.object({itemType:N.describe("Item type"),itemName:g.string().describe("Item name"),modulePath:g.string().optional().describe("Dot-separated module path")});function At(e){e.tool("batch_lookup","Look up multiple items in a single call. Returns a compact summary (signature + short doc) for each item. Saves round-trips when you need several items.",{crateName:g.string().describe("Crate name"),items:g.array(jt).min(1).max(20).describe("Items to look up (max 20)"),version:A},async({crateName:t,items:o,version:s})=>{const n=s??"latest",a=[`# Batch lookup: ${t} (${o.length} items)`,""],l=await Promise.allSettled(o.map(async({itemType:i,itemName:c,modulePath:d})=>{let p=d;if(p===void 0){const b=await B(t,c,n),S=b.find(_=>_.bareName.toLowerCase()===c.toLowerCase()&&_.type===i)??b.find(_=>_.bareName.toLowerCase()===c.toLowerCase());S&&(p=S.modulePath||void 0)}const h=W(p),f=i==="mod"?`${h}${c}/index.html`:`${h}${Y[i]??`${i}.`}${c}.html`,r=j(t,f,n),u=await I(r),m=`${t}::${rt(p)}${c}`,y=u("pre.rust.item-decl").text().trim(),w=Z(u),x=T(z(u("details.toggle.top-doc").html()??""),500);return{itemType:i,fullName:m,decl:y,featureGate:w,doc:x,url:r}}));for(let i=0;i<l.length;i++){const c=l[i],{itemType:d,itemName:p}=o[i];if(c.status==="fulfilled"){const{fullName:h,decl:f,featureGate:r,doc:u,url:m}=c.value;a.push(`## ${d} ${h}`,m),r&&a.push(`> ${r}`),f&&a.push("```rust",f,"```"),u&&a.push(u),a.push("")}else a.push(`## ${d} ${p}`,` Error: ${c.reason?.message??"unknown error"}`,"")}return v(a.join(`
21
21
  `))})}function Pt(e){const t=e.match(/github\.com\/([^/]+\/[^/]+)/);return t?t[1].replace(/\.git$/,""):null}function Tt(e){e.tool("get_crate_changelog","Fetch recent GitHub releases for a crate. Requires the crate to have a GitHub repository link on crates.io.",{crateName:g.string().describe("Crate name"),count:g.number().min(1).max(20).optional().describe("Number of releases to fetch (default 5, max 20)")},async({crateName:t,count:o})=>{const s=o??5;try{if(G(t))return v(`"${t}" is part of the Rust standard library.
22
- See https://github.com/rust-lang/rust/blob/master/RELEASES.md for changelogs.`);const n=`changelog:${t}:${s}`,r=F(n);if(r)return console.log(`[cache hit] changelog ${t}`),v(r);const l=await Q(t);if(!l.repository)return k(`No repository link found for "${t}" on crates.io.`);const i=Pt(l.repository);if(!i)return k(`Repository "${l.repository}" is not a GitHub URL.`);const c=await U(`https://api.github.com/repos/${i}/releases?per_page=${s}`);if(!c.length)return v(`No GitHub releases found for ${i}.`);const d=[`# ${t} — recent releases (${i})`,""];for(const p of c){const f=p.published_at?.slice(0,10)??"unknown",a=p.name||p.tag_name,u=p.body?T(p.body.trim(),1e3):"(no release notes)";d.push(`## ${a} (${f})`,u,"")}const h=d.join(`
23
- `);return P(n,h),v(h)}catch(n){return k(`Could not fetch changelog for "${t}". ${n.message}`)}})}function Dt(e){e.tool("resolve_type",'Resolve a Rust type path (e.g. "tokio::sync::Mutex" or "std::collections::HashMap") to its documentation. Parses the path to determine the crate, module, and item name automatically.',{typePath:g.string().describe('Full Rust type path (e.g. "tokio::sync::Mutex", "std::collections::HashMap")'),version:A},async({typePath:t,version:o})=>{try{const s=t.split("::").filter(Boolean);if(s.length<2)return k(`Type path "${t}" must have at least a crate and item name (e.g. "serde::Serialize").`);const n=s[0],r=s[s.length-1],l=o??"latest",i=await B(n,r,l),c=i.find(b=>{const S=b.name.replace(/::/g,"::"),_=s.slice(1).join("::");return S===_&&b.bareName===r})??i.find(b=>b.bareName===r);if(!c){const b=i.slice(0,10).map(_=>` [${_.type}] ${n}::${_.name}`),S=[`Could not resolve "${t}".`];return b.length&&S.push("","Similar items found:",...b),v(S.join(`
24
- `))}const d=c.modulePath?c.modulePath.replace(/\./g,"/")+"/":"",h=Y[c.type]??`${c.type}.`,p=c.type==="mod"?`${d}${r}/index.html`:`${d}${h}${r}.html`,f=j(n,p,l),a=await I(f),u=a("pre.rust.item-decl").text().trim(),m=Z(a),y=T(z(a("details.toggle.top-doc").html()??""),2e3),w=`${n}::${c.name}`,x=[`# ${c.type} ${w}`,f,""];return m&&x.push(`> ${m}`,""),u&&x.push("```rust",u,"```",""),y&&x.push(y),v(x.join(`
22
+ See https://github.com/rust-lang/rust/blob/master/RELEASES.md for changelogs.`);const n=`changelog:${t}:${s}`,a=F(n);if(a)return console.log(`[cache hit] changelog ${t}`),v(a);const l=await Q(t);if(!l.repository)return k(`No repository link found for "${t}" on crates.io.`);const i=Pt(l.repository);if(!i)return k(`Repository "${l.repository}" is not a GitHub URL.`);const c=await U(`https://api.github.com/repos/${i}/releases?per_page=${s}`);if(!c.length)return v(`No GitHub releases found for ${i}.`);const d=[`# ${t} — recent releases (${i})`,""];for(const h of c){const f=h.published_at?.slice(0,10)??"unknown",r=h.name||h.tag_name,u=h.body?T(h.body.trim(),1e3):"(no release notes)";d.push(`## ${r} (${f})`,u,"")}const p=d.join(`
23
+ `);return P(n,p),v(p)}catch(n){return k(`Could not fetch changelog for "${t}". ${n.message}`)}})}function Dt(e){e.tool("resolve_type",'Resolve a Rust type path (e.g. "tokio::sync::Mutex" or "std::collections::HashMap") to its documentation. Parses the path to determine the crate, module, and item name automatically.',{typePath:g.string().describe('Full Rust type path (e.g. "tokio::sync::Mutex", "std::collections::HashMap")'),version:A},async({typePath:t,version:o})=>{try{const s=t.split("::").filter(Boolean);if(s.length<2)return k(`Type path "${t}" must have at least a crate and item name (e.g. "serde::Serialize").`);const n=s[0],a=s[s.length-1],l=o??"latest",i=await B(n,a,l),c=i.find(b=>{const S=b.name.replace(/::/g,"::"),_=s.slice(1).join("::");return S===_&&b.bareName===a})??i.find(b=>b.bareName===a);if(!c){const b=i.slice(0,10).map(_=>` [${_.type}] ${n}::${_.name}`),S=[`Could not resolve "${t}".`];return b.length&&S.push("","Similar items found:",...b),v(S.join(`
24
+ `))}const d=c.modulePath?c.modulePath.replace(/\./g,"/")+"/":"",p=Y[c.type]??`${c.type}.`,h=c.type==="mod"?`${d}${a}/index.html`:`${d}${p}${a}.html`,f=j(n,h,l),r=await I(f),u=r("pre.rust.item-decl").text().trim(),m=Z(r),y=T(z(r("details.toggle.top-doc").html()??""),2e3),w=`${n}::${c.name}`,x=[`# ${c.type} ${w}`,f,""];return m&&x.push(`> ${m}`,""),u&&x.push("```rust",u,"```",""),y&&x.push(y),v(x.join(`
25
25
  `))}catch(s){return k(`Could not resolve "${t}". ${s.message}`)}})}console.log=(...e)=>console.error(...e);const L=new lt({name:"rust-docs",version:"4.0.0"});vt(L);xt(L);_t(L);Rt(L);St(L);kt(L);Lt(L);Et(L);It(L);At(L);Tt(L);Dt(L);L.prompt("lookup_crate_docs",{crateName:g.string().describe("Crate name")},({crateName:e})=>({messages:[{role:"user",content:{type:"text",text:[`Analyze the documentation for the Rust crate '${e}'. Focus on:`,"1. Main purpose and features","2. Key types and functions","3. Common usage patterns","4. Important notes or warnings","5. Latest version"].join(`
26
26
  `)}}]}));async function Ft(){const e=new ut;await L.connect(e)}Ft().catch(e=>{console.error("Failed to start server:",e),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-rustdoc",
3
- "version": "4.0.1",
3
+ "version": "4.0.2",
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",