# 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}`, ); } } ```