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