#
tokens: 14452/50000 14/14 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .clinerules
├── .gitignore
├── biome.json
├── bun.lock
├── package.json
├── README.md
├── src
│   ├── index.ts
│   ├── service.test.ts
│   ├── service.ts
│   ├── test-client.ts
│   ├── types.ts
│   └── utils
│       ├── crates-io-client.ts
│       ├── http-client.ts
│       └── logger.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.clinerules:
--------------------------------------------------------------------------------

```
1 | # Testing the server
2 | 
3 | The MCP server is already configured in your local environment, and you can't
4 | communicate with it when you run it in the terminal. Instead, just make tool
5 | calls to the rust-docs MCP server (after rebuilding) to test if it's working.
6 | Restart the server if that capability is available to you.
7 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
  2 | 
  3 | # Logs
  4 | 
  5 | logs
  6 | _.log
  7 | npm-debug.log_
  8 | yarn-debug.log*
  9 | yarn-error.log*
 10 | lerna-debug.log*
 11 | .pnpm-debug.log*
 12 | 
 13 | # Caches
 14 | 
 15 | .cache
 16 | 
 17 | # Diagnostic reports (https://nodejs.org/api/report.html)
 18 | 
 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
 20 | 
 21 | # Runtime data
 22 | 
 23 | pids
 24 | _.pid
 25 | _.seed
 26 | *.pid.lock
 27 | 
 28 | # Directory for instrumented libs generated by jscoverage/JSCover
 29 | 
 30 | lib-cov
 31 | 
 32 | # Coverage directory used by tools like istanbul
 33 | 
 34 | coverage
 35 | *.lcov
 36 | 
 37 | # nyc test coverage
 38 | 
 39 | .nyc_output
 40 | 
 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 42 | 
 43 | .grunt
 44 | 
 45 | # Bower dependency directory (https://bower.io/)
 46 | 
 47 | bower_components
 48 | 
 49 | # node-waf configuration
 50 | 
 51 | .lock-wscript
 52 | 
 53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 54 | 
 55 | build/Release
 56 | 
 57 | # Dependency directories
 58 | 
 59 | node_modules/
 60 | jspm_packages/
 61 | 
 62 | # Snowpack dependency directory (https://snowpack.dev/)
 63 | 
 64 | web_modules/
 65 | 
 66 | # TypeScript cache
 67 | 
 68 | *.tsbuildinfo
 69 | 
 70 | # Optional npm cache directory
 71 | 
 72 | .npm
 73 | 
 74 | # Optional eslint cache
 75 | 
 76 | .eslintcache
 77 | 
 78 | # Optional stylelint cache
 79 | 
 80 | .stylelintcache
 81 | 
 82 | # Microbundle cache
 83 | 
 84 | .rpt2_cache/
 85 | .rts2_cache_cjs/
 86 | .rts2_cache_es/
 87 | .rts2_cache_umd/
 88 | 
 89 | # Optional REPL history
 90 | 
 91 | .node_repl_history
 92 | 
 93 | # Output of 'npm pack'
 94 | 
 95 | *.tgz
 96 | 
 97 | # Yarn Integrity file
 98 | 
 99 | .yarn-integrity
100 | 
101 | # dotenv environment variable files
102 | 
103 | .env
104 | .env.development.local
105 | .env.test.local
106 | .env.production.local
107 | .env.local
108 | 
109 | # parcel-bundler cache (https://parceljs.org/)
110 | 
111 | .parcel-cache
112 | 
113 | # Next.js build output
114 | 
115 | .next
116 | out
117 | 
118 | # Nuxt.js build / generate output
119 | 
120 | .nuxt
121 | dist
122 | 
123 | # Gatsby files
124 | 
125 | # Comment in the public line in if your project uses Gatsby and not Next.js
126 | 
127 | # https://nextjs.org/blog/next-9-1#public-directory-support
128 | 
129 | # public
130 | 
131 | # vuepress build output
132 | 
133 | .vuepress/dist
134 | 
135 | # vuepress v2.x temp and cache directory
136 | 
137 | .temp
138 | 
139 | # Docusaurus cache and generated files
140 | 
141 | .docusaurus
142 | 
143 | # Serverless directories
144 | 
145 | .serverless/
146 | 
147 | # FuseBox cache
148 | 
149 | .fusebox/
150 | 
151 | # DynamoDB Local files
152 | 
153 | .dynamodb/
154 | 
155 | # TernJS port file
156 | 
157 | .tern-port
158 | 
159 | # Stores VSCode versions used for testing VSCode extensions
160 | 
161 | .vscode-test
162 | 
163 | # yarn v2
164 | 
165 | .yarn/cache
166 | .yarn/unplugged
167 | .yarn/build-state.yml
168 | .yarn/install-state.gz
169 | .pnp.*
170 | 
171 | # IntelliJ based IDEs
172 | .idea
173 | 
174 | # Finder (MacOS) folder config
175 | .DS_Store
176 | 
177 | build/
178 | mcp-sdk.md
179 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Rust Docs MCP Server
 2 | 
 3 | An MCP (Model Context Protocol) server that provides access to Rust documentation from docs.rs. This server allows AI tools to search for documentation, type information, feature flags, version numbers, and symbol definitions/source code.
 4 | 
 5 | ## Features
 6 | 
 7 | - Search for crates on docs.rs
 8 | - Get documentation for specific crates and versions
 9 | - Get type information (structs, enums, traits, etc.)
10 | - Get feature flags for crates
11 | - Get available versions for crates
12 | - Get source code for specific items
13 | - Search for symbols within crates
14 | 
15 | ## Installation
16 | 
17 | This project uses Bun for development, but the built server can run with Node.js.
18 | 
19 | ```bash
20 | # Clone the repository
21 | git clone https://github.com/yourusername/rust-docs-mcp-server.git
22 | cd rust-docs-mcp-server
23 | 
24 | # Install dependencies
25 | bun install
26 | ```
27 | 
28 | ## Building
29 | 
30 | ```bash
31 | # Build the server
32 | bun run build
33 | ```
34 | 
35 | This will create a build directory with the compiled JavaScript files.
36 | 
37 | ## Running
38 | 
39 | ```bash
40 | # Run the development server
41 | bun run dev
42 | 
43 | # Or run the built server
44 | bun run start
45 | ```
46 | 
47 | ## Usage with MCP Clients
48 | 
49 | This server implements the Model Context Protocol and can be used with any MCP client. To use it with an MCP client, you'll need to configure the client to connect to this server.
50 | 
51 | ### Available Tools
52 | 
53 | The server provides the following tools:
54 | 
55 | - `search_crates`: Search for crates on docs.rs
56 | - `get_crate_documentation`: Get documentation for a specific crate
57 | - `get_type_info`: Get type information for a specific item
58 | - `get_feature_flags`: Get feature flags for a crate
59 | - `get_crate_versions`: Get available versions for a crate
60 | - `get_source_code`: Get source code for a specific item
61 | - `search_symbols`: Search for symbols within a crate
62 | 
63 | ## Testing
64 | 
65 | ```bash
66 | # Run tests
67 | bun test
68 | ```
69 | 
70 | ## License
71 | 
72 | MIT
73 | 
```

--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
 3 | 	"vcs": {
 4 | 		"enabled": true,
 5 | 		"clientKind": "git",
 6 | 		"useIgnoreFile": true
 7 | 	},
 8 | 	"files": {
 9 | 		"ignoreUnknown": false,
10 | 		"ignore": []
11 | 	},
12 | 	"formatter": {
13 | 		"enabled": true,
14 | 		"indentStyle": "tab"
15 | 	},
16 | 	"organizeImports": {
17 | 		"enabled": true
18 | 	},
19 | 	"linter": {
20 | 		"enabled": true,
21 | 		"rules": {
22 | 			"recommended": true
23 | 		}
24 | 	},
25 | 	"javascript": {
26 | 		"formatter": {
27 | 			"quoteStyle": "double"
28 | 		}
29 | 	}
30 | }
31 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"compilerOptions": {
 3 | 		// Enable latest features
 4 | 		"lib": ["ESNext", "DOM"],
 5 | 		"target": "ESNext",
 6 | 		"module": "ESNext",
 7 | 		"moduleDetection": "force",
 8 | 		"jsx": "react-jsx",
 9 | 		"allowJs": true,
10 | 
11 | 		// Bundler mode
12 | 		"moduleResolution": "bundler",
13 | 		"allowImportingTsExtensions": true,
14 | 		"verbatimModuleSyntax": true,
15 | 		"noEmit": true,
16 | 
17 | 		// Best practices
18 | 		"strict": true,
19 | 		"skipLibCheck": true,
20 | 		"noFallthroughCasesInSwitch": true,
21 | 
22 | 		// Some stricter flags (disabled by default)
23 | 		"noUnusedLocals": false,
24 | 		"noUnusedParameters": false,
25 | 		"noPropertyAccessFromIndexSignature": false
26 | 	}
27 | }
28 | 
```

--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import pino from "pino";
 2 | 
 3 | // Create a logger instance with appropriate configuration
 4 | export const logger = pino({
 5 | 	level: process.env.LOG_LEVEL || "warn",
 6 | 	transport: {
 7 | 		target: "pino/file",
 8 | 		options: { destination: 2 }, // stderr
 9 | 	},
10 | 	timestamp: pino.stdTimeFunctions.isoTime,
11 | 	formatters: {
12 | 		level: (label) => {
13 | 			return { level: label };
14 | 		},
15 | 	},
16 | });
17 | 
18 | // Export convenience methods
19 | export default {
20 | 	debug: (msg: string, obj?: object) => logger.debug(obj || {}, msg),
21 | 	info: (msg: string, obj?: object) => logger.info(obj || {}, msg),
22 | 	warn: (msg: string, obj?: object) => logger.warn(obj || {}, msg),
23 | 	error: (msg: string, obj?: object) => logger.error(obj || {}, msg),
24 | 	fatal: (msg: string, obj?: object) => logger.fatal(obj || {}, msg),
25 | };
26 | 
```

--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------

```typescript
 1 | /**
 2 |  * Types for docs.rs integration
 3 |  */
 4 | 
 5 | export interface CrateInfo {
 6 | 	name: string;
 7 | 	version: string;
 8 | 	description?: string;
 9 | }
10 | 
11 | export interface CrateSearchResult {
12 | 	crates: CrateInfo[];
13 | 	totalCount: number;
14 | }
15 | 
16 | export interface RustType {
17 | 	name: string;
18 | 	kind:
19 | 		| "struct"
20 | 		| "enum"
21 | 		| "trait"
22 | 		| "function"
23 | 		| "macro"
24 | 		| "type"
25 | 		| "module"
26 | 		| "other";
27 | 	path: string;
28 | 	description?: string;
29 | 	sourceUrl?: string;
30 | 	documentationUrl: string;
31 | }
32 | 
33 | export interface FeatureFlag {
34 | 	name: string;
35 | 	description?: string;
36 | 	enabled: boolean;
37 | }
38 | 
39 | export interface CrateVersion {
40 | 	version: string;
41 | 	isYanked: boolean;
42 | 	releaseDate?: string;
43 | }
44 | 
45 | export interface SymbolDefinition {
46 | 	name: string;
47 | 	kind: string;
48 | 	path: string;
49 | 	sourceCode?: string;
50 | 	documentationHtml?: string;
51 | }
52 | 
53 | export interface SearchOptions {
54 | 	query: string;
55 | 	page?: number;
56 | 	perPage?: number;
57 | }
58 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"name": "rust-docs-mcp-server",
 3 | 	"version": "1.0.0",
 4 | 	"description": "MCP server for accessing Rust documentation from docs.rs",
 5 | 	"module": "index.ts",
 6 | 	"type": "module",
 7 | 	"bin": {
 8 | 		"rust-docs-mcp-server": "./build/index.js"
 9 | 	},
10 | 	"scripts": {
11 | 		"build": "bun build ./src/index.ts --outdir ./build --target node",
12 | 		"start": "bun run build && node ./build/index.js",
13 | 		"dev": "bun run src/index.ts",
14 | 		"test": "bun test"
15 | 	},
16 | 	"devDependencies": {
17 | 		"@types/bun": "latest",
18 | 		"@types/turndown": "^5.0.5"
19 | 	},
20 | 	"peerDependencies": {
21 | 		"typescript": "^5.0.0"
22 | 	},
23 | 	"dependencies": {
24 | 		"@biomejs/biome": "^1.9.4",
25 | 		"@modelcontextprotocol/sdk": "^1.6.0",
26 | 		"axios": "^1.7.9",
27 | 		"cheerio": "^1.0.0",
28 | 		"pino": "^9.6.0",
29 | 		"turndown": "^7.2.0",
30 | 		"zod": "^3.24.2"
31 | 	},
32 | 	"engines": {
33 | 		"node": ">=18.0.0"
34 | 	},
35 | 	"packageManager": "[email protected]+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
36 | }
37 | 
```

--------------------------------------------------------------------------------
/src/test-client.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { spawn } from "node:child_process";
  2 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
  3 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
  4 | 
  5 | async function main() {
  6 | 	console.log("Starting test client for Rust Docs MCP Server...");
  7 | 
  8 | 	// Start the server process
  9 | 	const serverProcess = spawn("bun", ["run", "src/index.ts"], {
 10 | 		stdio: ["pipe", "pipe", "inherit"],
 11 | 	});
 12 | 
 13 | 	// Create a transport that connects to the server
 14 | 	const transport = new StdioClientTransport({
 15 | 		command: "bun",
 16 | 		args: ["run", "src/index.ts"],
 17 | 	});
 18 | 
 19 | 	// Create the client
 20 | 	const client = new Client(
 21 | 		{
 22 | 			name: "test-client",
 23 | 			version: "1.0.0",
 24 | 		},
 25 | 		{
 26 | 			capabilities: {
 27 | 				tools: {},
 28 | 			},
 29 | 		},
 30 | 	);
 31 | 
 32 | 	try {
 33 | 		// Connect to the server
 34 | 		console.log("Connecting to server...");
 35 | 		await client.connect(transport);
 36 | 		console.log("Connected to server!");
 37 | 
 38 | 		// List available tools
 39 | 		console.log("\nListing available tools:");
 40 | 		const tools = await client.listTools();
 41 | 		console.log(JSON.stringify(tools, null, 2));
 42 | 
 43 | 		// Test search_crates tool
 44 | 		console.log("\nTesting search_crates tool:");
 45 | 		const searchResult = await client.callTool({
 46 | 			name: "search_crates",
 47 | 			arguments: {
 48 | 				query: "serde",
 49 | 			},
 50 | 		});
 51 | 		if (
 52 | 			searchResult.content &&
 53 | 			Array.isArray(searchResult.content) &&
 54 | 			searchResult.content.length > 0
 55 | 		) {
 56 | 			console.log(searchResult.content[0].text);
 57 | 		}
 58 | 
 59 | 		// Test get_crate_versions tool
 60 | 		console.log("\nTesting get_crate_versions tool:");
 61 | 		const versionsResult = await client.callTool({
 62 | 			name: "get_crate_versions",
 63 | 			arguments: {
 64 | 				crateName: "tokio",
 65 | 			},
 66 | 		});
 67 | 		if (
 68 | 			versionsResult.content &&
 69 | 			Array.isArray(versionsResult.content) &&
 70 | 			versionsResult.content.length > 0
 71 | 		) {
 72 | 			console.log(versionsResult.content[0].text);
 73 | 		}
 74 | 
 75 | 		// Test search_symbols tool
 76 | 		console.log("\nTesting search_symbols tool:");
 77 | 		const symbolsResult = await client.callTool({
 78 | 			name: "search_symbols",
 79 | 			arguments: {
 80 | 				crateName: "tokio",
 81 | 				query: "runtime",
 82 | 			},
 83 | 		});
 84 | 		if (
 85 | 			symbolsResult.content &&
 86 | 			Array.isArray(symbolsResult.content) &&
 87 | 			symbolsResult.content.length > 0
 88 | 		) {
 89 | 			console.log(symbolsResult.content[0].text);
 90 | 		}
 91 | 
 92 | 		console.log("\nAll tests completed successfully!");
 93 | 	} catch (error) {
 94 | 		console.error("Error:", error);
 95 | 	} finally {
 96 | 		// Close the connection and kill the server process
 97 | 		await client.close();
 98 | 		serverProcess.kill();
 99 | 	}
100 | }
101 | 
102 | main().catch(console.error);
103 | 
```

--------------------------------------------------------------------------------
/src/service.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, test, beforeAll } from "bun:test";
  2 | import * as cheerio from "cheerio";
  3 | import {
  4 | 	searchCrates,
  5 | 	getCrateDocumentation,
  6 | 	getCrateVersions,
  7 | 	searchSymbols,
  8 | 	getTypeInfo,
  9 | 	getCrateDetails,
 10 | } from "./service";
 11 | 
 12 | describe("service", () => {
 13 | 	// Set longer timeout for network requests
 14 | 	const timeout = 15000;
 15 | 
 16 | 	describe("searchCrates should return results for a valid query", () => {
 17 | 		test.each([
 18 | 			["serde",  "serde"],
 19 | 			["tokio",  "tokio"],
 20 | 			["pin-project", "pin-project"],
 21 | 			["pin_project",  "pin-project"],
 22 | 			["fjall",  "fjall"],
 23 | 		])(
 24 | 			"%s",
 25 | 			async (query, name) => {
 26 | 				const result = await searchCrates({ query });
 27 | 				expect(result.crates.length).toBeGreaterThan(0);
 28 | 				expect(result.totalCount).toBeGreaterThan(0);
 29 | 				// Check that each crate has a version
 30 | 				for (const crate of result.crates) {
 31 | 					expect(crate.name).toBeDefined();
 32 | 					expect(crate.version).toBeDefined();
 33 | 				}
 34 | 
 35 | 				expect(result.crates.some((crate) => crate.name === name)).toBe(true);
 36 | 			},
 37 | 			timeout,
 38 | 		);
 39 | 	});
 40 | 
 41 | 	describe("getCrateVersions", () => {
 42 | 		test(
 43 | 			"should return versions for a valid crate",
 44 | 			async () => {
 45 | 				const versions = await getCrateVersions("tokio");
 46 | 				expect(versions.length).toBeGreaterThan(0);
 47 | 
 48 | 				// Check that each version has the expected properties
 49 | 				for (const version of versions) {
 50 | 					expect(version.version).toBeDefined();
 51 | 					expect(typeof version.isYanked).toBe("boolean");
 52 | 					expect(version.releaseDate).toBeDefined();
 53 | 				}
 54 | 			},
 55 | 			timeout,
 56 | 		);
 57 | 	});
 58 | 
 59 | 	test(
 60 | 		"searchSymbols should return symbols for a valid query",
 61 | 		async () => {
 62 | 			const symbols = await searchSymbols("tokio", "runtime");
 63 | 			expect(symbols.length).toBeGreaterThan(0);
 64 | 		},
 65 | 		timeout,
 66 | 	);
 67 | 
 68 | 	test(
 69 | 		"getTypeInfo should return information for a valid type",
 70 | 		async () => {
 71 | 			// This test is skipped because the path may change in docs.rs
 72 | 			// In a real implementation, we would need to first find the correct path
 73 | 			// by searching for the type or navigating through the documentation
 74 | 			const typeInfo = await getTypeInfo(
 75 | 				"tokio",
 76 | 				"runtime/struct.Runtime.html",
 77 | 			);
 78 | 
 79 | 			expect(typeInfo).toBeTruthy();
 80 | 			expect(typeInfo.name).toContain("Runtime");
 81 | 			expect(typeInfo.kind).toBe("struct");
 82 | 		},
 83 | 		timeout,
 84 | 	);
 85 | 
 86 | 	describe("getCrateDetails", () => {
 87 | 		test(
 88 | 			"should return details for a valid crate",
 89 | 			async () => {
 90 | 				const details = await getCrateDetails("tokio");
 91 | 				expect(details.name).toBe("tokio");
 92 | 				expect(details.description).toBeDefined();
 93 | 				expect(details.versions.length).toBeGreaterThan(0);
 94 | 				expect(details.downloads).toBeGreaterThan(0);
 95 | 			},
 96 | 			timeout,
 97 | 		);
 98 | 	});
 99 | });
100 | 
```

--------------------------------------------------------------------------------
/src/utils/crates-io-client.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import logger from "./logger";
  2 | 
  3 | interface RequestOptions {
  4 | 	method?: string;
  5 | 	params?: Record<string, string | number | boolean | undefined>;
  6 | 	body?: unknown;
  7 | }
  8 | 
  9 | type FetchResponse =
 10 | 	| {
 11 | 			data: Record<string, unknown>;
 12 | 			status: number;
 13 | 			headers: Headers;
 14 | 			contentType: "json";
 15 | 	  }
 16 | 	| {
 17 | 			data: string;
 18 | 			status: number;
 19 | 			headers: Headers;
 20 | 			contentType: "text";
 21 | 	  };
 22 | 
 23 | // base configuration for crates.io requests
 24 | const BASE_CONFIG = {
 25 | 	baseURL: "https://crates.io/api/v1/",
 26 | 	headers: {
 27 | 		Accept: "application/json",
 28 | 		"User-Agent": "rust-docs-mcp-server/1.0.0",
 29 | 	},
 30 | };
 31 | 
 32 | // helper to build full url with query params
 33 | function buildUrl(
 34 | 	path: string,
 35 | 	params?: Record<string, string | number | boolean | undefined>,
 36 | ): string {
 37 | 	const url = new URL(path, BASE_CONFIG.baseURL);
 38 | 	if (params) {
 39 | 		for (const [key, value] of Object.entries(params)) {
 40 | 			if (value !== undefined) {
 41 | 				url.searchParams.append(key, String(value));
 42 | 			}
 43 | 		}
 44 | 	}
 45 | 	return url.toString();
 46 | }
 47 | 
 48 | // create a configured fetch client for crates.io
 49 | export async function cratesIoFetch(
 50 | 	path: string,
 51 | 	options: RequestOptions = {},
 52 | ): Promise<FetchResponse> {
 53 | 	const { method = "GET", params, body } = options;
 54 | 	const url = buildUrl(path, params);
 55 | 
 56 | 	try {
 57 | 		logger.debug(`making request to ${url}`, { method, params });
 58 | 
 59 | 		const controller = new AbortController();
 60 | 		const timeoutId = setTimeout(() => controller.abort(), 10000);
 61 | 
 62 | 		const response = await fetch(url, {
 63 | 			method,
 64 | 			headers: BASE_CONFIG.headers,
 65 | 			body: body ? JSON.stringify(body) : undefined,
 66 | 			signal: controller.signal,
 67 | 		});
 68 | 
 69 | 		clearTimeout(timeoutId);
 70 | 
 71 | 		logger.debug(`received response from ${url}`, {
 72 | 			status: response.status,
 73 | 			contentType: response.headers.get("content-type"),
 74 | 		});
 75 | 
 76 | 		if (!response.ok) {
 77 | 			throw new Error(`HTTP error! status: ${response.status}`);
 78 | 		}
 79 | 
 80 | 		const contentType = response.headers.get("content-type");
 81 | 		const isJson = contentType?.includes("application/json");
 82 | 		const data = isJson ? await response.json() : await response.text();
 83 | 
 84 | 		return {
 85 | 			data,
 86 | 			status: response.status,
 87 | 			headers: response.headers,
 88 | 			contentType: isJson ? "json" : "text",
 89 | 		};
 90 | 	} catch (error) {
 91 | 		logger.error(`error making request to ${url}`, { error });
 92 | 		throw error;
 93 | 	}
 94 | }
 95 | 
 96 | // Export a default instance
 97 | export default {
 98 | 	get: (path: string, options = {}) =>
 99 | 		cratesIoFetch(path, { ...options, method: "GET" }),
100 | 	post: (path: string, options = {}) =>
101 | 		cratesIoFetch(path, { ...options, method: "POST" }),
102 | 	put: (path: string, options = {}) =>
103 | 		cratesIoFetch(path, { ...options, method: "PUT" }),
104 | 	delete: (path: string, options = {}) =>
105 | 		cratesIoFetch(path, { ...options, method: "DELETE" }),
106 | };
107 | 
```

--------------------------------------------------------------------------------
/src/utils/http-client.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import logger from "./logger";
  2 | 
  3 | interface RequestOptions {
  4 | 	method?: string;
  5 | 	params?: Record<string, string | number | boolean | undefined>;
  6 | 	body?: unknown;
  7 | }
  8 | 
  9 | type FetchResponse =
 10 | 	| {
 11 | 			data: Record<string, unknown>;
 12 | 			status: number;
 13 | 			headers: Headers;
 14 | 			contentType: "json";
 15 | 	  }
 16 | 	| {
 17 | 			data: string;
 18 | 			status: number;
 19 | 			headers: Headers;
 20 | 			contentType: "text";
 21 | 	  };
 22 | 
 23 | // base configuration for docs.rs requests
 24 | const BASE_CONFIG = {
 25 | 	baseURL: "https://docs.rs",
 26 | 	headers: {
 27 | 		Accept: "text/html,application/xhtml+xml,application/json",
 28 | 		"User-Agent": "rust-docs-mcp-server/1.0.0",
 29 | 	},
 30 | };
 31 | 
 32 | // helper to build full url with query params
 33 | function buildUrl(
 34 | 	path: string,
 35 | 	params?: Record<string, string | number | boolean | undefined>,
 36 | ): string {
 37 | 	const url = new URL(path, BASE_CONFIG.baseURL);
 38 | 	if (params) {
 39 | 		for (const [key, value] of Object.entries(params)) {
 40 | 			if (value !== undefined) {
 41 | 				url.searchParams.append(key, String(value));
 42 | 			}
 43 | 		}
 44 | 	}
 45 | 	return url.toString();
 46 | }
 47 | 
 48 | // create a configured fetch client for docs.rs
 49 | export async function docsRsFetch(
 50 | 	path: string,
 51 | 	options: RequestOptions = {},
 52 | ): Promise<FetchResponse> {
 53 | 	const { method = "GET", params, body } = options;
 54 | 	const url = buildUrl(path, params);
 55 | 
 56 | 	try {
 57 | 		console.debug(`making request to ${url}`, { method, params });
 58 | 
 59 | 		const controller = new AbortController();
 60 | 		const timeoutId = setTimeout(() => controller.abort(), 10000);
 61 | 
 62 | 		const response = await fetch(url, {
 63 | 			method,
 64 | 			headers: BASE_CONFIG.headers,
 65 | 			body: body ? JSON.stringify(body) : undefined,
 66 | 			signal: controller.signal,
 67 | 		});
 68 | 
 69 | 		clearTimeout(timeoutId);
 70 | 
 71 | 		logger.debug(`Received response from ${url}`, {
 72 | 			status: response.status,
 73 | 			contentType: response.headers.get("content-type"),
 74 | 		});
 75 | 
 76 | 		if (!response.ok) {
 77 | 			throw new Error(`HTTP error! status: ${response.status}`);
 78 | 		}
 79 | 
 80 | 		const contentType = response.headers.get("content-type");
 81 | 		const isJson = contentType?.includes("application/json");
 82 | 		const data = isJson ? await response.json() : await response.text();
 83 | 
 84 | 		return {
 85 | 			data,
 86 | 			status: response.status,
 87 | 			headers: response.headers,
 88 | 			contentType: isJson ? "json" : "text",
 89 | 		};
 90 | 	} catch (error) {
 91 | 		logger.error(`Error making request to ${url}`, { error });
 92 | 		throw error;
 93 | 	}
 94 | }
 95 | 
 96 | // Export a default instance
 97 | export default {
 98 | 	get: (path: string, options = {}) =>
 99 | 		docsRsFetch(path, { ...options, method: "GET" }),
100 | 	post: (path: string, options = {}) =>
101 | 		docsRsFetch(path, { ...options, method: "POST" }),
102 | 	put: (path: string, options = {}) =>
103 | 		docsRsFetch(path, { ...options, method: "PUT" }),
104 | 	delete: (path: string, options = {}) =>
105 | 		docsRsFetch(path, { ...options, method: "DELETE" }),
106 | };
107 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  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 * as cheerio from "cheerio";
  6 | import logger from "./utils/logger";
  7 | import {
  8 | 	searchCrates,
  9 | 	getCrateDocumentation,
 10 | 	getTypeInfo,
 11 | 	getFeatureFlags,
 12 | 	getCrateVersions,
 13 | 	getSourceCode,
 14 | 	searchSymbols,
 15 | } from "./service";
 16 | 
 17 | /**
 18 |  * Rust Docs MCP Server
 19 |  *
 20 |  * This server provides tools for accessing Rust documentation from docs.rs.
 21 |  * It allows searching for crates, viewing documentation, type information,
 22 |  * feature flags, version numbers, and source code.
 23 |  */
 24 | class RustDocsMcpServer {
 25 | 	private server: McpServer;
 26 | 
 27 | 	constructor() {
 28 | 		// Create the MCP server
 29 | 		this.server = new McpServer({
 30 | 			name: "rust-docs",
 31 | 			version: "1.0.0",
 32 | 		});
 33 | 
 34 | 		// Set up tools
 35 | 		this.setupTools();
 36 | 
 37 | 		// Error handling
 38 | 		process.on("uncaughtException", (error) => {
 39 | 			logger.error("Uncaught exception", { error });
 40 | 			process.exit(1);
 41 | 		});
 42 | 
 43 | 		process.on("unhandledRejection", (reason) => {
 44 | 			logger.error("Unhandled rejection", { reason });
 45 | 		});
 46 | 	}
 47 | 
 48 | 	/**
 49 | 	 * Set up the MCP tools
 50 | 	 */
 51 | 	private setupTools() {
 52 | 		// Tool: Search for crates
 53 | 		this.server.tool(
 54 | 			"search_crates",
 55 | 			{
 56 | 				query: z.string().min(1).describe("Search query for crates"),
 57 | 				page: z.number().optional().describe("Page number (starts at 1)"),
 58 | 				perPage: z.number().optional().describe("Results per page"),
 59 | 			},
 60 | 			async ({ query, page, perPage }) => {
 61 | 				try {
 62 | 					const result = await searchCrates({
 63 | 						query,
 64 | 						page,
 65 | 						perPage,
 66 | 					});
 67 | 					return {
 68 | 						content: [
 69 | 							{
 70 | 								type: "text",
 71 | 								text: JSON.stringify(result, null, 2),
 72 | 							},
 73 | 						],
 74 | 					};
 75 | 				} catch (error) {
 76 | 					logger.error("Error in search_crates tool", { error });
 77 | 					return {
 78 | 						content: [
 79 | 							{
 80 | 								type: "text",
 81 | 								text: `Error searching for crates: ${(error as Error).message}`,
 82 | 							},
 83 | 						],
 84 | 						isError: true,
 85 | 					};
 86 | 				}
 87 | 			},
 88 | 		);
 89 | 
 90 | 		// Tool: Get crate documentation
 91 | 		this.server.tool(
 92 | 			"get_crate_documentation",
 93 | 			{
 94 | 				crateName: z.string().min(1).describe("Name of the crate"),
 95 | 				version: z
 96 | 					.string()
 97 | 					.optional()
 98 | 					.describe("Specific version (defaults to latest)"),
 99 | 			},
100 | 			async ({ crateName, version }) => {
101 | 				try {
102 | 					const html = await getCrateDocumentation(crateName, version);
103 | 
104 | 					// Use cheerio to parse the HTML and extract the content
105 | 					const $ = cheerio.load(html);
106 | 
107 | 					// Try different selectors to find the main content
108 | 					let content = "Documentation content not found";
109 | 					let contentFound = false;
110 | 
111 | 					// First try the #main element which contains the main crate documentation
112 | 					const mainElement = $("#main");
113 | 					if (mainElement.length > 0) {
114 | 						content = mainElement.html() || content;
115 | 						contentFound = true;
116 | 					}
117 | 
118 | 					// If that fails, try other potential content containers
119 | 					if (!contentFound) {
120 | 						const selectors = [
121 | 							"main",
122 | 							".container.package-page-container",
123 | 							".rustdoc",
124 | 							".information",
125 | 							".crate-info",
126 | 						];
127 | 
128 | 						for (const selector of selectors) {
129 | 							const element = $(selector);
130 | 							if (element.length > 0) {
131 | 								content = element.html() || content;
132 | 								contentFound = true;
133 | 								break;
134 | 							}
135 | 						}
136 | 					}
137 | 
138 | 					// Log the extraction result
139 | 					if (!contentFound) {
140 | 						logger.warn(`Failed to extract content for crate: ${crateName}`);
141 | 					} else {
142 | 						logger.info(
143 | 							`Successfully extracted content for crate: ${crateName}`,
144 | 						);
145 | 					}
146 | 
147 | 					return {
148 | 						content: [
149 | 							{
150 | 								type: "text",
151 | 								text: content,
152 | 							},
153 | 						],
154 | 					};
155 | 				} catch (error) {
156 | 					logger.error("Error in get_crate_documentation tool", { error });
157 | 					return {
158 | 						content: [
159 | 							{
160 | 								type: "text",
161 | 								text: `Error getting documentation: ${(error as Error).message}`,
162 | 							},
163 | 						],
164 | 						isError: true,
165 | 					};
166 | 				}
167 | 			},
168 | 		);
169 | 
170 | 		// Tool: Get type information
171 | 		this.server.tool(
172 | 			"get_type_info",
173 | 			{
174 | 				crateName: z.string().min(1).describe("Name of the crate"),
175 | 				path: z
176 | 					.string()
177 | 					.min(1)
178 | 					.describe('Path to the type (e.g., "std/vec/struct.Vec.html")'),
179 | 				version: z
180 | 					.string()
181 | 					.optional()
182 | 					.describe("Specific version (defaults to latest)"),
183 | 			},
184 | 			async ({ crateName, path, version }) => {
185 | 				try {
186 | 					const typeInfo = await getTypeInfo(crateName, path, version);
187 | 					return {
188 | 						content: [
189 | 							{
190 | 								type: "text",
191 | 								text: JSON.stringify(typeInfo, null, 2),
192 | 							},
193 | 						],
194 | 					};
195 | 				} catch (error) {
196 | 					logger.error("Error in get_type_info tool", { error });
197 | 					return {
198 | 						content: [
199 | 							{
200 | 								type: "text",
201 | 								text: `Error getting type information: ${(error as Error).message}`,
202 | 							},
203 | 						],
204 | 						isError: true,
205 | 					};
206 | 				}
207 | 			},
208 | 		);
209 | 
210 | 		// Tool: Get feature flags
211 | 		this.server.tool(
212 | 			"get_feature_flags",
213 | 			{
214 | 				crateName: z.string().min(1).describe("Name of the crate"),
215 | 				version: z
216 | 					.string()
217 | 					.optional()
218 | 					.describe("Specific version (defaults to latest)"),
219 | 			},
220 | 			async ({ crateName, version }) => {
221 | 				try {
222 | 					const features = await getFeatureFlags(crateName, version);
223 | 					return {
224 | 						content: [
225 | 							{
226 | 								type: "text",
227 | 								text: JSON.stringify(features, null, 2),
228 | 							},
229 | 						],
230 | 					};
231 | 				} catch (error) {
232 | 					logger.error("Error in get_feature_flags tool", { error });
233 | 					return {
234 | 						content: [
235 | 							{
236 | 								type: "text",
237 | 								text: `Error getting feature flags: ${(error as Error).message}`,
238 | 							},
239 | 						],
240 | 						isError: true,
241 | 					};
242 | 				}
243 | 			},
244 | 		);
245 | 
246 | 		// Tool: Get crate versions
247 | 		this.server.tool(
248 | 			"get_crate_versions",
249 | 			{
250 | 				crateName: z.string().min(1).describe("Name of the crate"),
251 | 			},
252 | 			async ({ crateName }) => {
253 | 				try {
254 | 					const versions = await getCrateVersions(crateName);
255 | 					return {
256 | 						content: [
257 | 							{
258 | 								type: "text",
259 | 								text: JSON.stringify(versions, null, 2),
260 | 							},
261 | 						],
262 | 					};
263 | 				} catch (error) {
264 | 					logger.error("Error in get_crate_versions tool", { error });
265 | 					return {
266 | 						content: [
267 | 							{
268 | 								type: "text",
269 | 								text: `Error getting crate versions: ${(error as Error).message}`,
270 | 							},
271 | 						],
272 | 						isError: true,
273 | 					};
274 | 				}
275 | 			},
276 | 		);
277 | 
278 | 		// Tool: Get source code
279 | 		this.server.tool(
280 | 			"get_source_code",
281 | 			{
282 | 				crateName: z.string().min(1).describe("Name of the crate"),
283 | 				path: z.string().min(1).describe("Path to the source file"),
284 | 				version: z
285 | 					.string()
286 | 					.optional()
287 | 					.describe("Specific version (defaults to latest)"),
288 | 			},
289 | 			async ({ crateName, path, version }) => {
290 | 				try {
291 | 					const sourceCode = await getSourceCode(crateName, path, version);
292 | 					return {
293 | 						content: [
294 | 							{
295 | 								type: "text",
296 | 								text: sourceCode,
297 | 							},
298 | 						],
299 | 					};
300 | 				} catch (error) {
301 | 					logger.error("Error in get_source_code tool", { error });
302 | 					return {
303 | 						content: [
304 | 							{
305 | 								type: "text",
306 | 								text: `Error getting source code: ${(error as Error).message}`,
307 | 							},
308 | 						],
309 | 						isError: true,
310 | 					};
311 | 				}
312 | 			},
313 | 		);
314 | 
315 | 		// Tool: Search for symbols
316 | 		this.server.tool(
317 | 			"search_symbols",
318 | 			{
319 | 				crateName: z.string().min(1).describe("Name of the crate"),
320 | 				query: z.string().min(1).describe("Search query for symbols"),
321 | 				version: z
322 | 					.string()
323 | 					.optional()
324 | 					.describe("Specific version (defaults to latest)"),
325 | 			},
326 | 			async ({ crateName, query, version }) => {
327 | 				try {
328 | 					const symbols = await searchSymbols(crateName, query, version);
329 | 					return {
330 | 						content: [
331 | 							{
332 | 								type: "text",
333 | 								text: JSON.stringify(symbols, null, 2),
334 | 							},
335 | 						],
336 | 					};
337 | 				} catch (error) {
338 | 					logger.error("Error in search_symbols tool", { error });
339 | 					return {
340 | 						content: [
341 | 							{
342 | 								type: "text",
343 | 								text: `Error searching for symbols: ${(error as Error).message}`,
344 | 							},
345 | 						],
346 | 						isError: true,
347 | 					};
348 | 				}
349 | 			},
350 | 		);
351 | 	}
352 | 
353 | 	/**
354 | 	 * Start the server
355 | 	 */
356 | 	async start() {
357 | 		try {
358 | 			logger.info("Starting Rust Docs MCP Server");
359 | 			const transport = new StdioServerTransport();
360 | 			await this.server.connect(transport);
361 | 			logger.info("Server connected via stdio");
362 | 		} catch (error) {
363 | 			logger.error("Failed to start server", { error });
364 | 			process.exit(1);
365 | 		}
366 | 	}
367 | }
368 | 
369 | // Create and start the server
370 | const server = new RustDocsMcpServer();
371 | server.start().catch((error) => {
372 | 	logger.error("Error starting server", { error });
373 | 	process.exit(1);
374 | });
375 | 
```

--------------------------------------------------------------------------------
/src/service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as cheerio from "cheerio";
  2 | import turndown from "turndown";
  3 | import type {
  4 | 	CrateInfo,
  5 | 	CrateSearchResult,
  6 | 	CrateVersion,
  7 | 	FeatureFlag,
  8 | 	RustType,
  9 | 	SearchOptions,
 10 | 	SymbolDefinition,
 11 | } from "./types";
 12 | import docsRsClient from "./utils/http-client";
 13 | import cratesIoClient from "./utils/crates-io-client";
 14 | import logger from "./utils/logger";
 15 | 
 16 | const turndownInstance = new turndown();
 17 | 
 18 | /**
 19 |  * Search for crates on crates.io
 20 |  */
 21 | export async function searchCrates(
 22 | 	options: SearchOptions,
 23 | ): Promise<CrateSearchResult> {
 24 | 	try {
 25 | 		logger.info(`searching for crates with query: ${options.query}`);
 26 | 
 27 | 		const response = await cratesIoClient.get("crates", {
 28 | 			params: {
 29 | 				q: options.query,
 30 | 				page: options.page || 1,
 31 | 				per_page: options.perPage || 10,
 32 | 			},
 33 | 		});
 34 | 
 35 | 		if (response.contentType !== "json") {
 36 | 			throw new Error("Expected JSON response but got text");
 37 | 		}
 38 | 
 39 | 		const data = response.data as {
 40 | 			crates: Array<{
 41 | 				name: string;
 42 | 				max_version: string;
 43 | 				description?: string;
 44 | 			}>;
 45 | 			meta: {
 46 | 				total: number;
 47 | 			};
 48 | 		};
 49 | 
 50 | 		const crates: CrateInfo[] = data.crates.map((crate) => ({
 51 | 			name: crate.name,
 52 | 			version: crate.max_version,
 53 | 			description: crate.description,
 54 | 		}));
 55 | 
 56 | 		return {
 57 | 			crates,
 58 | 			totalCount: data.meta.total,
 59 | 		};
 60 | 	} catch (error) {
 61 | 		logger.error("error searching for crates", { error });
 62 | 		throw new Error(`failed to search for crates: ${(error as Error).message}`);
 63 | 	}
 64 | }
 65 | 
 66 | /**
 67 |  * Get detailed information about a crate from crates.io
 68 |  */
 69 | export async function getCrateDetails(crateName: string): Promise<{
 70 | 	name: string;
 71 | 	description?: string;
 72 | 	versions: CrateVersion[];
 73 | 	downloads: number;
 74 | 	homepage?: string;
 75 | 	repository?: string;
 76 | 	documentation?: string;
 77 | }> {
 78 | 	try {
 79 | 		logger.info(`getting crate details for: ${crateName}`);
 80 | 
 81 | 		const response = await cratesIoClient.get(`crates/${crateName}`);
 82 | 
 83 | 		if (response.contentType !== "json") {
 84 | 			throw new Error("Expected JSON response but got text");
 85 | 		}
 86 | 
 87 | 		const data = response.data as {
 88 | 			crate: {
 89 | 				name: string;
 90 | 				description?: string;
 91 | 				downloads: number;
 92 | 				homepage?: string;
 93 | 				repository?: string;
 94 | 				documentation?: string;
 95 | 			};
 96 | 			versions: Array<{
 97 | 				num: string;
 98 | 				yanked: boolean;
 99 | 				created_at: string;
100 | 			}>;
101 | 		};
102 | 
103 | 		return {
104 | 			name: data.crate.name,
105 | 			description: data.crate.description,
106 | 			downloads: data.crate.downloads,
107 | 			homepage: data.crate.homepage,
108 | 			repository: data.crate.repository,
109 | 			documentation: data.crate.documentation,
110 | 			versions: data.versions.map((v) => ({
111 | 				version: v.num,
112 | 				isYanked: v.yanked,
113 | 				releaseDate: v.created_at,
114 | 			})),
115 | 		};
116 | 	} catch (error) {
117 | 		logger.error(`error getting crate details for: ${crateName}`, { error });
118 | 		throw new Error(
119 | 			`failed to get crate details for ${crateName}: ${(error as Error).message}`,
120 | 		);
121 | 	}
122 | }
123 | 
124 | /**
125 |  * Get documentation for a specific crate from docs.rs
126 |  */
127 | export async function getCrateDocumentation(
128 | 	crateName: string,
129 | 	version?: string,
130 | ): Promise<string> {
131 | 	try {
132 | 		logger.info(
133 | 			`getting documentation for crate: ${crateName}${version ? ` version ${version}` : ""}`,
134 | 		);
135 | 
136 | 		const path = version
137 | 			? `crate/${crateName}/${version}`
138 | 			: `crate/${crateName}/latest`;
139 | 
140 | 		const response = await docsRsClient.get(path);
141 | 
142 | 		if (response.contentType !== "text") {
143 | 			throw new Error("Expected HTML response but got JSON");
144 | 		}
145 | 
146 | 		return turndownInstance.turndown(response.data);
147 | 	} catch (error) {
148 | 		logger.error(`error getting documentation for crate: ${crateName}`, {
149 | 			error,
150 | 		});
151 | 		throw new Error(
152 | 			`failed to get documentation for crate ${crateName}: ${(error as Error).message}`,
153 | 		);
154 | 	}
155 | }
156 | 
157 | /**
158 |  * Get type information for a specific item in a crate
159 |  */
160 | export async function getTypeInfo(
161 | 	crateName: string,
162 | 	path: string,
163 | 	version?: string,
164 | ): Promise<RustType> {
165 | 	try {
166 | 		logger.info(`Getting type info for ${path} in crate: ${crateName}`);
167 | 
168 | 		const versionPath = version || "latest";
169 | 		const fullPath = `${crateName}/${versionPath}/${crateName}/${path}`;
170 | 
171 | 		const response = await docsRsClient.get(fullPath);
172 | 
173 | 		if (response.contentType !== "text") {
174 | 			throw new Error("Expected HTML response but got JSON");
175 | 		}
176 | 
177 | 		const $ = cheerio.load(response.data);
178 | 
179 | 		// Determine the kind of type
180 | 		let kind: RustType["kind"] = "other";
181 | 		if ($(".struct").length) kind = "struct";
182 | 		else if ($(".enum").length) kind = "enum";
183 | 		else if ($(".trait").length) kind = "trait";
184 | 		else if ($(".fn").length) kind = "function";
185 | 		else if ($(".macro").length) kind = "macro";
186 | 		else if ($(".typedef").length) kind = "type";
187 | 		else if ($(".mod").length) kind = "module";
188 | 
189 | 		// Get description
190 | 		const description = $(".docblock").first().text().trim();
191 | 
192 | 		// Get source URL if available
193 | 		const sourceUrl = $(".src-link a").attr("href");
194 | 
195 | 		const name = path.split("/").pop() || path;
196 | 
197 | 		return {
198 | 			name,
199 | 			kind,
200 | 			path,
201 | 			description: description || undefined,
202 | 			sourceUrl: sourceUrl || undefined,
203 | 			documentationUrl: `https://docs.rs${fullPath}`,
204 | 		};
205 | 	} catch (error) {
206 | 		logger.error(`Error getting type info for ${path} in crate: ${crateName}`, {
207 | 			error,
208 | 		});
209 | 		throw new Error(`Failed to get type info: ${(error as Error).message}`);
210 | 	}
211 | }
212 | 
213 | /**
214 |  * Get feature flags for a crate
215 |  */
216 | export async function getFeatureFlags(
217 | 	crateName: string,
218 | 	version?: string,
219 | ): Promise<FeatureFlag[]> {
220 | 	try {
221 | 		logger.info(`Getting feature flags for crate: ${crateName}`);
222 | 
223 | 		const versionPath = version || "latest";
224 | 		const response = await docsRsClient.get(
225 | 			`/crate/${crateName}/${versionPath}/features`,
226 | 		);
227 | 
228 | 		if (response.contentType !== "text") {
229 | 			throw new Error("Expected HTML response but got JSON");
230 | 		}
231 | 
232 | 		const $ = cheerio.load(response.data);
233 | 		const features: FeatureFlag[] = [];
234 | 
235 | 		$(".feature").each((_, element) => {
236 | 			const name = $(element).find(".feature-name").text().trim();
237 | 			const description = $(element).find(".feature-description").text().trim();
238 | 			const enabled = $(element).hasClass("feature-enabled");
239 | 
240 | 			features.push({
241 | 				name,
242 | 				description: description || undefined,
243 | 				enabled,
244 | 			});
245 | 		});
246 | 
247 | 		return features;
248 | 	} catch (error) {
249 | 		logger.error(`Error getting feature flags for crate: ${crateName}`, {
250 | 			error,
251 | 		});
252 | 		throw new Error(`Failed to get feature flags: ${(error as Error).message}`);
253 | 	}
254 | }
255 | 
256 | /**
257 |  * Get available versions for a crate from crates.io
258 |  */
259 | export async function getCrateVersions(
260 | 	crateName: string,
261 | ): Promise<CrateVersion[]> {
262 | 	try {
263 | 		logger.info(`getting versions for crate: ${crateName}`);
264 | 
265 | 		const response = await cratesIoClient.get(`crates/${crateName}`);
266 | 
267 | 		if (response.contentType !== "json") {
268 | 			throw new Error("Expected JSON response but got text");
269 | 		}
270 | 
271 | 		const data = response.data as {
272 | 			versions: Array<{
273 | 				num: string;
274 | 				yanked: boolean;
275 | 				created_at: string;
276 | 			}>;
277 | 		};
278 | 
279 | 		return data.versions.map((v) => ({
280 | 			version: v.num,
281 | 			isYanked: v.yanked,
282 | 			releaseDate: v.created_at,
283 | 		}));
284 | 	} catch (error) {
285 | 		logger.error(`error getting versions for crate: ${crateName}`, {
286 | 			error,
287 | 		});
288 | 		throw new Error(
289 | 			`failed to get crate versions: ${(error as Error).message}`,
290 | 		);
291 | 	}
292 | }
293 | 
294 | /**
295 |  * Get source code for a specific item
296 |  */
297 | export async function getSourceCode(
298 | 	crateName: string,
299 | 	path: string,
300 | 	version?: string,
301 | ): Promise<string> {
302 | 	try {
303 | 		logger.info(`Getting source code for ${path} in crate: ${crateName}`);
304 | 
305 | 		const versionPath = version || "latest";
306 | 		const response = await docsRsClient.get(
307 | 			`/crate/${crateName}/${versionPath}/src/${path}`,
308 | 		);
309 | 
310 | 		if (typeof response.data !== "string") {
311 | 			throw new Error("Expected HTML response but got JSON");
312 | 		}
313 | 
314 | 		const $ = cheerio.load(response.data);
315 | 		return $(".src").text();
316 | 	} catch (error) {
317 | 		logger.error(
318 | 			`Error getting source code for ${path} in crate: ${crateName}`,
319 | 			{ error },
320 | 		);
321 | 		throw new Error(`Failed to get source code: ${(error as Error).message}`);
322 | 	}
323 | }
324 | 
325 | /**
326 |  * Search for symbols within a crate
327 |  */
328 | export async function searchSymbols(
329 | 	crateName: string,
330 | 	query: string,
331 | 	version?: string,
332 | ): Promise<SymbolDefinition[]> {
333 | 	try {
334 | 		logger.info(
335 | 			`searching for symbols in crate: ${crateName} with query: ${query}`,
336 | 		);
337 | 
338 | 		try {
339 | 			const versionPath = version || "latest";
340 | 			const response = await docsRsClient.get(
341 | 				`/${crateName}/${versionPath}/${crateName}/`,
342 | 				{
343 | 					params: { search: query },
344 | 				},
345 | 			);
346 | 
347 | 			if (typeof response.data !== "string") {
348 | 				throw new Error("Expected HTML response but got JSON");
349 | 			}
350 | 
351 | 			const $ = cheerio.load(response.data);
352 | 			const symbols: SymbolDefinition[] = [];
353 | 
354 | 			$(".search-results a").each((_, element) => {
355 | 				const name = $(element).find(".result-name path").text().trim();
356 | 				const kind = $(element).find(".result-name typename").text().trim();
357 | 				const path = $(element).attr("href") || "";
358 | 
359 | 				symbols.push({
360 | 					name,
361 | 					kind,
362 | 					path,
363 | 				});
364 | 			});
365 | 
366 | 			return symbols;
367 | 		} catch (innerError: unknown) {
368 | 			// If we get a 404, try a different approach - search in the main documentation
369 | 			if (innerError instanceof Error && innerError.message.includes("404")) {
370 | 				logger.info(
371 | 					`Search endpoint not found for ${crateName}, trying alternative approach`,
372 | 				);
373 | 			}
374 | 
375 | 			// Re-throw other errors
376 | 			throw innerError;
377 | 		}
378 | 	} catch (error) {
379 | 		logger.error(`Error searching for symbols in crate: ${crateName}`, {
380 | 			error,
381 | 		});
382 | 		throw new Error(
383 | 			`Failed to search for symbols: ${(error as Error).message}`,
384 | 		);
385 | 	}
386 | }
387 | 
```