#
tokens: 9871/50000 14/14 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
--------------------------------------------------------------------------------

```
# Testing the server

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

```

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

```
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore

# Logs

logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Caches

.cache

# Diagnostic reports (https://nodejs.org/api/report.html)

report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json

# Runtime data

pids
_.pid
_.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover

lib-cov

# Coverage directory used by tools like istanbul

coverage
*.lcov

# nyc test coverage

.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)

.grunt

# Bower dependency directory (https://bower.io/)

bower_components

# node-waf configuration

.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)

build/Release

# Dependency directories

node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)

web_modules/

# TypeScript cache

*.tsbuildinfo

# Optional npm cache directory

.npm

# Optional eslint cache

.eslintcache

# Optional stylelint cache

.stylelintcache

# Microbundle cache

.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history

.node_repl_history

# Output of 'npm pack'

*.tgz

# Yarn Integrity file

.yarn-integrity

# dotenv environment variable files

.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)

.parcel-cache

# Next.js build output

.next
out

# Nuxt.js build / generate output

.nuxt
dist

# Gatsby files

# Comment in the public line in if your project uses Gatsby and not Next.js

# https://nextjs.org/blog/next-9-1#public-directory-support

# public

# vuepress build output

.vuepress/dist

# vuepress v2.x temp and cache directory

.temp

# Docusaurus cache and generated files

.docusaurus

# Serverless directories

.serverless/

# FuseBox cache

.fusebox/

# DynamoDB Local files

.dynamodb/

# TernJS port file

.tern-port

# Stores VSCode versions used for testing VSCode extensions

.vscode-test

# yarn v2

.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# IntelliJ based IDEs
.idea

# Finder (MacOS) folder config
.DS_Store

build/
mcp-sdk.md

```

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

```markdown
# Rust Docs MCP Server

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.

## Features

- Search for crates on docs.rs
- Get documentation for specific crates and versions
- Get type information (structs, enums, traits, etc.)
- Get feature flags for crates
- Get available versions for crates
- Get source code for specific items
- Search for symbols within crates

## Installation

This project uses Bun for development, but the built server can run with Node.js.

```bash
# Clone the repository
git clone https://github.com/yourusername/rust-docs-mcp-server.git
cd rust-docs-mcp-server

# Install dependencies
bun install
```

## Building

```bash
# Build the server
bun run build
```

This will create a build directory with the compiled JavaScript files.

## Running

```bash
# Run the development server
bun run dev

# Or run the built server
bun run start
```

## Usage with MCP Clients

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.

### Available Tools

The server provides the following tools:

- `search_crates`: Search for crates on docs.rs
- `get_crate_documentation`: Get documentation for a specific crate
- `get_type_info`: Get type information for a specific item
- `get_feature_flags`: Get feature flags for a crate
- `get_crate_versions`: Get available versions for a crate
- `get_source_code`: Get source code for a specific item
- `search_symbols`: Search for symbols within a crate

## Testing

```bash
# Run tests
bun test
```

## License

MIT

```

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

```json
{
	"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
	"vcs": {
		"enabled": true,
		"clientKind": "git",
		"useIgnoreFile": true
	},
	"files": {
		"ignoreUnknown": false,
		"ignore": []
	},
	"formatter": {
		"enabled": true,
		"indentStyle": "tab"
	},
	"organizeImports": {
		"enabled": true
	},
	"linter": {
		"enabled": true,
		"rules": {
			"recommended": true
		}
	},
	"javascript": {
		"formatter": {
			"quoteStyle": "double"
		}
	}
}

```

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

```json
{
	"compilerOptions": {
		// Enable latest features
		"lib": ["ESNext", "DOM"],
		"target": "ESNext",
		"module": "ESNext",
		"moduleDetection": "force",
		"jsx": "react-jsx",
		"allowJs": true,

		// Bundler mode
		"moduleResolution": "bundler",
		"allowImportingTsExtensions": true,
		"verbatimModuleSyntax": true,
		"noEmit": true,

		// Best practices
		"strict": true,
		"skipLibCheck": true,
		"noFallthroughCasesInSwitch": true,

		// Some stricter flags (disabled by default)
		"noUnusedLocals": false,
		"noUnusedParameters": false,
		"noPropertyAccessFromIndexSignature": false
	}
}

```

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

```typescript
import pino from "pino";

// Create a logger instance with appropriate configuration
export const logger = pino({
	level: process.env.LOG_LEVEL || "warn",
	transport: {
		target: "pino/file",
		options: { destination: 2 }, // stderr
	},
	timestamp: pino.stdTimeFunctions.isoTime,
	formatters: {
		level: (label) => {
			return { level: label };
		},
	},
});

// Export convenience methods
export default {
	debug: (msg: string, obj?: object) => logger.debug(obj || {}, msg),
	info: (msg: string, obj?: object) => logger.info(obj || {}, msg),
	warn: (msg: string, obj?: object) => logger.warn(obj || {}, msg),
	error: (msg: string, obj?: object) => logger.error(obj || {}, msg),
	fatal: (msg: string, obj?: object) => logger.fatal(obj || {}, msg),
};

```

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

```typescript
/**
 * Types for docs.rs integration
 */

export interface CrateInfo {
	name: string;
	version: string;
	description?: string;
}

export interface CrateSearchResult {
	crates: CrateInfo[];
	totalCount: number;
}

export interface RustType {
	name: string;
	kind:
		| "struct"
		| "enum"
		| "trait"
		| "function"
		| "macro"
		| "type"
		| "module"
		| "other";
	path: string;
	description?: string;
	sourceUrl?: string;
	documentationUrl: string;
}

export interface FeatureFlag {
	name: string;
	description?: string;
	enabled: boolean;
}

export interface CrateVersion {
	version: string;
	isYanked: boolean;
	releaseDate?: string;
}

export interface SymbolDefinition {
	name: string;
	kind: string;
	path: string;
	sourceCode?: string;
	documentationHtml?: string;
}

export interface SearchOptions {
	query: string;
	page?: number;
	perPage?: number;
}

```

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

```json
{
	"name": "rust-docs-mcp-server",
	"version": "1.0.0",
	"description": "MCP server for accessing Rust documentation from docs.rs",
	"module": "index.ts",
	"type": "module",
	"bin": {
		"rust-docs-mcp-server": "./build/index.js"
	},
	"scripts": {
		"build": "bun build ./src/index.ts --outdir ./build --target node",
		"start": "bun run build && node ./build/index.js",
		"dev": "bun run src/index.ts",
		"test": "bun test"
	},
	"devDependencies": {
		"@types/bun": "latest",
		"@types/turndown": "^5.0.5"
	},
	"peerDependencies": {
		"typescript": "^5.0.0"
	},
	"dependencies": {
		"@biomejs/biome": "^1.9.4",
		"@modelcontextprotocol/sdk": "^1.6.0",
		"axios": "^1.7.9",
		"cheerio": "^1.0.0",
		"pino": "^9.6.0",
		"turndown": "^7.2.0",
		"zod": "^3.24.2"
	},
	"engines": {
		"node": ">=18.0.0"
	},
	"packageManager": "[email protected]+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
}

```

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

```typescript
import { spawn } from "node:child_process";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

async function main() {
	console.log("Starting test client for Rust Docs MCP Server...");

	// Start the server process
	const serverProcess = spawn("bun", ["run", "src/index.ts"], {
		stdio: ["pipe", "pipe", "inherit"],
	});

	// Create a transport that connects to the server
	const transport = new StdioClientTransport({
		command: "bun",
		args: ["run", "src/index.ts"],
	});

	// Create the client
	const client = new Client(
		{
			name: "test-client",
			version: "1.0.0",
		},
		{
			capabilities: {
				tools: {},
			},
		},
	);

	try {
		// Connect to the server
		console.log("Connecting to server...");
		await client.connect(transport);
		console.log("Connected to server!");

		// List available tools
		console.log("\nListing available tools:");
		const tools = await client.listTools();
		console.log(JSON.stringify(tools, null, 2));

		// Test search_crates tool
		console.log("\nTesting search_crates tool:");
		const searchResult = await client.callTool({
			name: "search_crates",
			arguments: {
				query: "serde",
			},
		});
		if (
			searchResult.content &&
			Array.isArray(searchResult.content) &&
			searchResult.content.length > 0
		) {
			console.log(searchResult.content[0].text);
		}

		// Test get_crate_versions tool
		console.log("\nTesting get_crate_versions tool:");
		const versionsResult = await client.callTool({
			name: "get_crate_versions",
			arguments: {
				crateName: "tokio",
			},
		});
		if (
			versionsResult.content &&
			Array.isArray(versionsResult.content) &&
			versionsResult.content.length > 0
		) {
			console.log(versionsResult.content[0].text);
		}

		// Test search_symbols tool
		console.log("\nTesting search_symbols tool:");
		const symbolsResult = await client.callTool({
			name: "search_symbols",
			arguments: {
				crateName: "tokio",
				query: "runtime",
			},
		});
		if (
			symbolsResult.content &&
			Array.isArray(symbolsResult.content) &&
			symbolsResult.content.length > 0
		) {
			console.log(symbolsResult.content[0].text);
		}

		console.log("\nAll tests completed successfully!");
	} catch (error) {
		console.error("Error:", error);
	} finally {
		// Close the connection and kill the server process
		await client.close();
		serverProcess.kill();
	}
}

main().catch(console.error);

```

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

```typescript
import { describe, expect, test, beforeAll } from "bun:test";
import * as cheerio from "cheerio";
import {
	searchCrates,
	getCrateDocumentation,
	getCrateVersions,
	searchSymbols,
	getTypeInfo,
	getCrateDetails,
} from "./service";

describe("service", () => {
	// Set longer timeout for network requests
	const timeout = 15000;

	describe("searchCrates should return results for a valid query", () => {
		test.each([
			["serde",  "serde"],
			["tokio",  "tokio"],
			["pin-project", "pin-project"],
			["pin_project",  "pin-project"],
			["fjall",  "fjall"],
		])(
			"%s",
			async (query, name) => {
				const result = await searchCrates({ query });
				expect(result.crates.length).toBeGreaterThan(0);
				expect(result.totalCount).toBeGreaterThan(0);
				// Check that each crate has a version
				for (const crate of result.crates) {
					expect(crate.name).toBeDefined();
					expect(crate.version).toBeDefined();
				}

				expect(result.crates.some((crate) => crate.name === name)).toBe(true);
			},
			timeout,
		);
	});

	describe("getCrateVersions", () => {
		test(
			"should return versions for a valid crate",
			async () => {
				const versions = await getCrateVersions("tokio");
				expect(versions.length).toBeGreaterThan(0);

				// Check that each version has the expected properties
				for (const version of versions) {
					expect(version.version).toBeDefined();
					expect(typeof version.isYanked).toBe("boolean");
					expect(version.releaseDate).toBeDefined();
				}
			},
			timeout,
		);
	});

	test(
		"searchSymbols should return symbols for a valid query",
		async () => {
			const symbols = await searchSymbols("tokio", "runtime");
			expect(symbols.length).toBeGreaterThan(0);
		},
		timeout,
	);

	test(
		"getTypeInfo should return information for a valid type",
		async () => {
			// This test is skipped because the path may change in docs.rs
			// In a real implementation, we would need to first find the correct path
			// by searching for the type or navigating through the documentation
			const typeInfo = await getTypeInfo(
				"tokio",
				"runtime/struct.Runtime.html",
			);

			expect(typeInfo).toBeTruthy();
			expect(typeInfo.name).toContain("Runtime");
			expect(typeInfo.kind).toBe("struct");
		},
		timeout,
	);

	describe("getCrateDetails", () => {
		test(
			"should return details for a valid crate",
			async () => {
				const details = await getCrateDetails("tokio");
				expect(details.name).toBe("tokio");
				expect(details.description).toBeDefined();
				expect(details.versions.length).toBeGreaterThan(0);
				expect(details.downloads).toBeGreaterThan(0);
			},
			timeout,
		);
	});
});

```

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

```typescript
import logger from "./logger";

interface RequestOptions {
	method?: string;
	params?: Record<string, string | number | boolean | undefined>;
	body?: unknown;
}

type FetchResponse =
	| {
			data: Record<string, unknown>;
			status: number;
			headers: Headers;
			contentType: "json";
	  }
	| {
			data: string;
			status: number;
			headers: Headers;
			contentType: "text";
	  };

// base configuration for crates.io requests
const BASE_CONFIG = {
	baseURL: "https://crates.io/api/v1/",
	headers: {
		Accept: "application/json",
		"User-Agent": "rust-docs-mcp-server/1.0.0",
	},
};

// helper to build full url with query params
function buildUrl(
	path: string,
	params?: Record<string, string | number | boolean | undefined>,
): string {
	const url = new URL(path, BASE_CONFIG.baseURL);
	if (params) {
		for (const [key, value] of Object.entries(params)) {
			if (value !== undefined) {
				url.searchParams.append(key, String(value));
			}
		}
	}
	return url.toString();
}

// create a configured fetch client for crates.io
export async function cratesIoFetch(
	path: string,
	options: RequestOptions = {},
): Promise<FetchResponse> {
	const { method = "GET", params, body } = options;
	const url = buildUrl(path, params);

	try {
		logger.debug(`making request to ${url}`, { method, params });

		const controller = new AbortController();
		const timeoutId = setTimeout(() => controller.abort(), 10000);

		const response = await fetch(url, {
			method,
			headers: BASE_CONFIG.headers,
			body: body ? JSON.stringify(body) : undefined,
			signal: controller.signal,
		});

		clearTimeout(timeoutId);

		logger.debug(`received response from ${url}`, {
			status: response.status,
			contentType: response.headers.get("content-type"),
		});

		if (!response.ok) {
			throw new Error(`HTTP error! status: ${response.status}`);
		}

		const contentType = response.headers.get("content-type");
		const isJson = contentType?.includes("application/json");
		const data = isJson ? await response.json() : await response.text();

		return {
			data,
			status: response.status,
			headers: response.headers,
			contentType: isJson ? "json" : "text",
		};
	} catch (error) {
		logger.error(`error making request to ${url}`, { error });
		throw error;
	}
}

// Export a default instance
export default {
	get: (path: string, options = {}) =>
		cratesIoFetch(path, { ...options, method: "GET" }),
	post: (path: string, options = {}) =>
		cratesIoFetch(path, { ...options, method: "POST" }),
	put: (path: string, options = {}) =>
		cratesIoFetch(path, { ...options, method: "PUT" }),
	delete: (path: string, options = {}) =>
		cratesIoFetch(path, { ...options, method: "DELETE" }),
};

```

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

```typescript
import logger from "./logger";

interface RequestOptions {
	method?: string;
	params?: Record<string, string | number | boolean | undefined>;
	body?: unknown;
}

type FetchResponse =
	| {
			data: Record<string, unknown>;
			status: number;
			headers: Headers;
			contentType: "json";
	  }
	| {
			data: string;
			status: number;
			headers: Headers;
			contentType: "text";
	  };

// base configuration for docs.rs requests
const BASE_CONFIG = {
	baseURL: "https://docs.rs",
	headers: {
		Accept: "text/html,application/xhtml+xml,application/json",
		"User-Agent": "rust-docs-mcp-server/1.0.0",
	},
};

// helper to build full url with query params
function buildUrl(
	path: string,
	params?: Record<string, string | number | boolean | undefined>,
): string {
	const url = new URL(path, BASE_CONFIG.baseURL);
	if (params) {
		for (const [key, value] of Object.entries(params)) {
			if (value !== undefined) {
				url.searchParams.append(key, String(value));
			}
		}
	}
	return url.toString();
}

// create a configured fetch client for docs.rs
export async function docsRsFetch(
	path: string,
	options: RequestOptions = {},
): Promise<FetchResponse> {
	const { method = "GET", params, body } = options;
	const url = buildUrl(path, params);

	try {
		console.debug(`making request to ${url}`, { method, params });

		const controller = new AbortController();
		const timeoutId = setTimeout(() => controller.abort(), 10000);

		const response = await fetch(url, {
			method,
			headers: BASE_CONFIG.headers,
			body: body ? JSON.stringify(body) : undefined,
			signal: controller.signal,
		});

		clearTimeout(timeoutId);

		logger.debug(`Received response from ${url}`, {
			status: response.status,
			contentType: response.headers.get("content-type"),
		});

		if (!response.ok) {
			throw new Error(`HTTP error! status: ${response.status}`);
		}

		const contentType = response.headers.get("content-type");
		const isJson = contentType?.includes("application/json");
		const data = isJson ? await response.json() : await response.text();

		return {
			data,
			status: response.status,
			headers: response.headers,
			contentType: isJson ? "json" : "text",
		};
	} catch (error) {
		logger.error(`Error making request to ${url}`, { error });
		throw error;
	}
}

// Export a default instance
export default {
	get: (path: string, options = {}) =>
		docsRsFetch(path, { ...options, method: "GET" }),
	post: (path: string, options = {}) =>
		docsRsFetch(path, { ...options, method: "POST" }),
	put: (path: string, options = {}) =>
		docsRsFetch(path, { ...options, method: "PUT" }),
	delete: (path: string, options = {}) =>
		docsRsFetch(path, { ...options, method: "DELETE" }),
};

```

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

```typescript
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as cheerio from "cheerio";
import logger from "./utils/logger";
import {
	searchCrates,
	getCrateDocumentation,
	getTypeInfo,
	getFeatureFlags,
	getCrateVersions,
	getSourceCode,
	searchSymbols,
} from "./service";

/**
 * Rust Docs MCP Server
 *
 * This server provides tools for accessing Rust documentation from docs.rs.
 * It allows searching for crates, viewing documentation, type information,
 * feature flags, version numbers, and source code.
 */
class RustDocsMcpServer {
	private server: McpServer;

	constructor() {
		// Create the MCP server
		this.server = new McpServer({
			name: "rust-docs",
			version: "1.0.0",
		});

		// Set up tools
		this.setupTools();

		// Error handling
		process.on("uncaughtException", (error) => {
			logger.error("Uncaught exception", { error });
			process.exit(1);
		});

		process.on("unhandledRejection", (reason) => {
			logger.error("Unhandled rejection", { reason });
		});
	}

	/**
	 * Set up the MCP tools
	 */
	private setupTools() {
		// Tool: Search for crates
		this.server.tool(
			"search_crates",
			{
				query: z.string().min(1).describe("Search query for crates"),
				page: z.number().optional().describe("Page number (starts at 1)"),
				perPage: z.number().optional().describe("Results per page"),
			},
			async ({ query, page, perPage }) => {
				try {
					const result = await searchCrates({
						query,
						page,
						perPage,
					});
					return {
						content: [
							{
								type: "text",
								text: JSON.stringify(result, null, 2),
							},
						],
					};
				} catch (error) {
					logger.error("Error in search_crates tool", { error });
					return {
						content: [
							{
								type: "text",
								text: `Error searching for crates: ${(error as Error).message}`,
							},
						],
						isError: true,
					};
				}
			},
		);

		// Tool: Get crate documentation
		this.server.tool(
			"get_crate_documentation",
			{
				crateName: z.string().min(1).describe("Name of the crate"),
				version: z
					.string()
					.optional()
					.describe("Specific version (defaults to latest)"),
			},
			async ({ crateName, version }) => {
				try {
					const html = await getCrateDocumentation(crateName, version);

					// Use cheerio to parse the HTML and extract the content
					const $ = cheerio.load(html);

					// Try different selectors to find the main content
					let content = "Documentation content not found";
					let contentFound = false;

					// First try the #main element which contains the main crate documentation
					const mainElement = $("#main");
					if (mainElement.length > 0) {
						content = mainElement.html() || content;
						contentFound = true;
					}

					// If that fails, try other potential content containers
					if (!contentFound) {
						const selectors = [
							"main",
							".container.package-page-container",
							".rustdoc",
							".information",
							".crate-info",
						];

						for (const selector of selectors) {
							const element = $(selector);
							if (element.length > 0) {
								content = element.html() || content;
								contentFound = true;
								break;
							}
						}
					}

					// Log the extraction result
					if (!contentFound) {
						logger.warn(`Failed to extract content for crate: ${crateName}`);
					} else {
						logger.info(
							`Successfully extracted content for crate: ${crateName}`,
						);
					}

					return {
						content: [
							{
								type: "text",
								text: content,
							},
						],
					};
				} catch (error) {
					logger.error("Error in get_crate_documentation tool", { error });
					return {
						content: [
							{
								type: "text",
								text: `Error getting documentation: ${(error as Error).message}`,
							},
						],
						isError: true,
					};
				}
			},
		);

		// Tool: Get type information
		this.server.tool(
			"get_type_info",
			{
				crateName: z.string().min(1).describe("Name of the crate"),
				path: z
					.string()
					.min(1)
					.describe('Path to the type (e.g., "std/vec/struct.Vec.html")'),
				version: z
					.string()
					.optional()
					.describe("Specific version (defaults to latest)"),
			},
			async ({ crateName, path, version }) => {
				try {
					const typeInfo = await getTypeInfo(crateName, path, version);
					return {
						content: [
							{
								type: "text",
								text: JSON.stringify(typeInfo, null, 2),
							},
						],
					};
				} catch (error) {
					logger.error("Error in get_type_info tool", { error });
					return {
						content: [
							{
								type: "text",
								text: `Error getting type information: ${(error as Error).message}`,
							},
						],
						isError: true,
					};
				}
			},
		);

		// Tool: Get feature flags
		this.server.tool(
			"get_feature_flags",
			{
				crateName: z.string().min(1).describe("Name of the crate"),
				version: z
					.string()
					.optional()
					.describe("Specific version (defaults to latest)"),
			},
			async ({ crateName, version }) => {
				try {
					const features = await getFeatureFlags(crateName, version);
					return {
						content: [
							{
								type: "text",
								text: JSON.stringify(features, null, 2),
							},
						],
					};
				} catch (error) {
					logger.error("Error in get_feature_flags tool", { error });
					return {
						content: [
							{
								type: "text",
								text: `Error getting feature flags: ${(error as Error).message}`,
							},
						],
						isError: true,
					};
				}
			},
		);

		// Tool: Get crate versions
		this.server.tool(
			"get_crate_versions",
			{
				crateName: z.string().min(1).describe("Name of the crate"),
			},
			async ({ crateName }) => {
				try {
					const versions = await getCrateVersions(crateName);
					return {
						content: [
							{
								type: "text",
								text: JSON.stringify(versions, null, 2),
							},
						],
					};
				} catch (error) {
					logger.error("Error in get_crate_versions tool", { error });
					return {
						content: [
							{
								type: "text",
								text: `Error getting crate versions: ${(error as Error).message}`,
							},
						],
						isError: true,
					};
				}
			},
		);

		// Tool: Get source code
		this.server.tool(
			"get_source_code",
			{
				crateName: z.string().min(1).describe("Name of the crate"),
				path: z.string().min(1).describe("Path to the source file"),
				version: z
					.string()
					.optional()
					.describe("Specific version (defaults to latest)"),
			},
			async ({ crateName, path, version }) => {
				try {
					const sourceCode = await getSourceCode(crateName, path, version);
					return {
						content: [
							{
								type: "text",
								text: sourceCode,
							},
						],
					};
				} catch (error) {
					logger.error("Error in get_source_code tool", { error });
					return {
						content: [
							{
								type: "text",
								text: `Error getting source code: ${(error as Error).message}`,
							},
						],
						isError: true,
					};
				}
			},
		);

		// Tool: Search for symbols
		this.server.tool(
			"search_symbols",
			{
				crateName: z.string().min(1).describe("Name of the crate"),
				query: z.string().min(1).describe("Search query for symbols"),
				version: z
					.string()
					.optional()
					.describe("Specific version (defaults to latest)"),
			},
			async ({ crateName, query, version }) => {
				try {
					const symbols = await searchSymbols(crateName, query, version);
					return {
						content: [
							{
								type: "text",
								text: JSON.stringify(symbols, null, 2),
							},
						],
					};
				} catch (error) {
					logger.error("Error in search_symbols tool", { error });
					return {
						content: [
							{
								type: "text",
								text: `Error searching for symbols: ${(error as Error).message}`,
							},
						],
						isError: true,
					};
				}
			},
		);
	}

	/**
	 * Start the server
	 */
	async start() {
		try {
			logger.info("Starting Rust Docs MCP Server");
			const transport = new StdioServerTransport();
			await this.server.connect(transport);
			logger.info("Server connected via stdio");
		} catch (error) {
			logger.error("Failed to start server", { error });
			process.exit(1);
		}
	}
}

// Create and start the server
const server = new RustDocsMcpServer();
server.start().catch((error) => {
	logger.error("Error starting server", { error });
	process.exit(1);
});

```

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

```typescript
import * as cheerio from "cheerio";
import turndown from "turndown";
import type {
	CrateInfo,
	CrateSearchResult,
	CrateVersion,
	FeatureFlag,
	RustType,
	SearchOptions,
	SymbolDefinition,
} from "./types";
import docsRsClient from "./utils/http-client";
import cratesIoClient from "./utils/crates-io-client";
import logger from "./utils/logger";

const turndownInstance = new turndown();

/**
 * Search for crates on crates.io
 */
export async function searchCrates(
	options: SearchOptions,
): Promise<CrateSearchResult> {
	try {
		logger.info(`searching for crates with query: ${options.query}`);

		const response = await cratesIoClient.get("crates", {
			params: {
				q: options.query,
				page: options.page || 1,
				per_page: options.perPage || 10,
			},
		});

		if (response.contentType !== "json") {
			throw new Error("Expected JSON response but got text");
		}

		const data = response.data as {
			crates: Array<{
				name: string;
				max_version: string;
				description?: string;
			}>;
			meta: {
				total: number;
			};
		};

		const crates: CrateInfo[] = data.crates.map((crate) => ({
			name: crate.name,
			version: crate.max_version,
			description: crate.description,
		}));

		return {
			crates,
			totalCount: data.meta.total,
		};
	} catch (error) {
		logger.error("error searching for crates", { error });
		throw new Error(`failed to search for crates: ${(error as Error).message}`);
	}
}

/**
 * Get detailed information about a crate from crates.io
 */
export async function getCrateDetails(crateName: string): Promise<{
	name: string;
	description?: string;
	versions: CrateVersion[];
	downloads: number;
	homepage?: string;
	repository?: string;
	documentation?: string;
}> {
	try {
		logger.info(`getting crate details for: ${crateName}`);

		const response = await cratesIoClient.get(`crates/${crateName}`);

		if (response.contentType !== "json") {
			throw new Error("Expected JSON response but got text");
		}

		const data = response.data as {
			crate: {
				name: string;
				description?: string;
				downloads: number;
				homepage?: string;
				repository?: string;
				documentation?: string;
			};
			versions: Array<{
				num: string;
				yanked: boolean;
				created_at: string;
			}>;
		};

		return {
			name: data.crate.name,
			description: data.crate.description,
			downloads: data.crate.downloads,
			homepage: data.crate.homepage,
			repository: data.crate.repository,
			documentation: data.crate.documentation,
			versions: data.versions.map((v) => ({
				version: v.num,
				isYanked: v.yanked,
				releaseDate: v.created_at,
			})),
		};
	} catch (error) {
		logger.error(`error getting crate details for: ${crateName}`, { error });
		throw new Error(
			`failed to get crate details for ${crateName}: ${(error as Error).message}`,
		);
	}
}

/**
 * Get documentation for a specific crate from docs.rs
 */
export async function getCrateDocumentation(
	crateName: string,
	version?: string,
): Promise<string> {
	try {
		logger.info(
			`getting documentation for crate: ${crateName}${version ? ` version ${version}` : ""}`,
		);

		const path = version
			? `crate/${crateName}/${version}`
			: `crate/${crateName}/latest`;

		const response = await docsRsClient.get(path);

		if (response.contentType !== "text") {
			throw new Error("Expected HTML response but got JSON");
		}

		return turndownInstance.turndown(response.data);
	} catch (error) {
		logger.error(`error getting documentation for crate: ${crateName}`, {
			error,
		});
		throw new Error(
			`failed to get documentation for crate ${crateName}: ${(error as Error).message}`,
		);
	}
}

/**
 * Get type information for a specific item in a crate
 */
export async function getTypeInfo(
	crateName: string,
	path: string,
	version?: string,
): Promise<RustType> {
	try {
		logger.info(`Getting type info for ${path} in crate: ${crateName}`);

		const versionPath = version || "latest";
		const fullPath = `${crateName}/${versionPath}/${crateName}/${path}`;

		const response = await docsRsClient.get(fullPath);

		if (response.contentType !== "text") {
			throw new Error("Expected HTML response but got JSON");
		}

		const $ = cheerio.load(response.data);

		// Determine the kind of type
		let kind: RustType["kind"] = "other";
		if ($(".struct").length) kind = "struct";
		else if ($(".enum").length) kind = "enum";
		else if ($(".trait").length) kind = "trait";
		else if ($(".fn").length) kind = "function";
		else if ($(".macro").length) kind = "macro";
		else if ($(".typedef").length) kind = "type";
		else if ($(".mod").length) kind = "module";

		// Get description
		const description = $(".docblock").first().text().trim();

		// Get source URL if available
		const sourceUrl = $(".src-link a").attr("href");

		const name = path.split("/").pop() || path;

		return {
			name,
			kind,
			path,
			description: description || undefined,
			sourceUrl: sourceUrl || undefined,
			documentationUrl: `https://docs.rs${fullPath}`,
		};
	} catch (error) {
		logger.error(`Error getting type info for ${path} in crate: ${crateName}`, {
			error,
		});
		throw new Error(`Failed to get type info: ${(error as Error).message}`);
	}
}

/**
 * Get feature flags for a crate
 */
export async function getFeatureFlags(
	crateName: string,
	version?: string,
): Promise<FeatureFlag[]> {
	try {
		logger.info(`Getting feature flags for crate: ${crateName}`);

		const versionPath = version || "latest";
		const response = await docsRsClient.get(
			`/crate/${crateName}/${versionPath}/features`,
		);

		if (response.contentType !== "text") {
			throw new Error("Expected HTML response but got JSON");
		}

		const $ = cheerio.load(response.data);
		const features: FeatureFlag[] = [];

		$(".feature").each((_, element) => {
			const name = $(element).find(".feature-name").text().trim();
			const description = $(element).find(".feature-description").text().trim();
			const enabled = $(element).hasClass("feature-enabled");

			features.push({
				name,
				description: description || undefined,
				enabled,
			});
		});

		return features;
	} catch (error) {
		logger.error(`Error getting feature flags for crate: ${crateName}`, {
			error,
		});
		throw new Error(`Failed to get feature flags: ${(error as Error).message}`);
	}
}

/**
 * Get available versions for a crate from crates.io
 */
export async function getCrateVersions(
	crateName: string,
): Promise<CrateVersion[]> {
	try {
		logger.info(`getting versions for crate: ${crateName}`);

		const response = await cratesIoClient.get(`crates/${crateName}`);

		if (response.contentType !== "json") {
			throw new Error("Expected JSON response but got text");
		}

		const data = response.data as {
			versions: Array<{
				num: string;
				yanked: boolean;
				created_at: string;
			}>;
		};

		return data.versions.map((v) => ({
			version: v.num,
			isYanked: v.yanked,
			releaseDate: v.created_at,
		}));
	} catch (error) {
		logger.error(`error getting versions for crate: ${crateName}`, {
			error,
		});
		throw new Error(
			`failed to get crate versions: ${(error as Error).message}`,
		);
	}
}

/**
 * Get source code for a specific item
 */
export async function getSourceCode(
	crateName: string,
	path: string,
	version?: string,
): Promise<string> {
	try {
		logger.info(`Getting source code for ${path} in crate: ${crateName}`);

		const versionPath = version || "latest";
		const response = await docsRsClient.get(
			`/crate/${crateName}/${versionPath}/src/${path}`,
		);

		if (typeof response.data !== "string") {
			throw new Error("Expected HTML response but got JSON");
		}

		const $ = cheerio.load(response.data);
		return $(".src").text();
	} catch (error) {
		logger.error(
			`Error getting source code for ${path} in crate: ${crateName}`,
			{ error },
		);
		throw new Error(`Failed to get source code: ${(error as Error).message}`);
	}
}

/**
 * Search for symbols within a crate
 */
export async function searchSymbols(
	crateName: string,
	query: string,
	version?: string,
): Promise<SymbolDefinition[]> {
	try {
		logger.info(
			`searching for symbols in crate: ${crateName} with query: ${query}`,
		);

		try {
			const versionPath = version || "latest";
			const response = await docsRsClient.get(
				`/${crateName}/${versionPath}/${crateName}/`,
				{
					params: { search: query },
				},
			);

			if (typeof response.data !== "string") {
				throw new Error("Expected HTML response but got JSON");
			}

			const $ = cheerio.load(response.data);
			const symbols: SymbolDefinition[] = [];

			$(".search-results a").each((_, element) => {
				const name = $(element).find(".result-name path").text().trim();
				const kind = $(element).find(".result-name typename").text().trim();
				const path = $(element).attr("href") || "";

				symbols.push({
					name,
					kind,
					path,
				});
			});

			return symbols;
		} catch (innerError: unknown) {
			// If we get a 404, try a different approach - search in the main documentation
			if (innerError instanceof Error && innerError.message.includes("404")) {
				logger.info(
					`Search endpoint not found for ${crateName}, trying alternative approach`,
				);
			}

			// Re-throw other errors
			throw innerError;
		}
	} catch (error) {
		logger.error(`Error searching for symbols in crate: ${crateName}`, {
			error,
		});
		throw new Error(
			`Failed to search for symbols: ${(error as Error).message}`,
		);
	}
}

```