Skip to main content
Glama

MCP Package Docs Server

by sammcj
rust-docs-integration.ts10.9 kB
import * as cheerio from "cheerio"; import turndown from "turndown"; import { CrateInfo, CrateSearchResult, CrateVersion, FeatureFlag, RustType, SearchOptions, SymbolDefinition, } from "./types.js"; import rustHttpClient from "./utils/rust-http-client.js"; import { McpLogger } from './logger.js' const turndownInstance = new turndown(); export class RustDocsHandler { private logger: McpLogger; constructor(logger: McpLogger) { this.logger = logger.child('RustDocs') } /** * Search for crates on crates.io */ async searchCrates( options: SearchOptions, ): Promise<CrateSearchResult> { try { this.logger.info(`searching for crates with query: ${options.query}`); const response = await rustHttpClient.cratesIoFetch("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) { this.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 */ async getCrateDetails(crateName: string): Promise<{ name: string; description?: string; versions: CrateVersion[]; downloads: number; homepage?: string; repository?: string; documentation?: string; }> { try { this.logger.info(`getting crate details for: ${crateName}`); const response = await rustHttpClient.cratesIoFetch(`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) { this.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 */ async getCrateDocumentation( crateName: string, version?: string, ): Promise<string> { try { this.logger.info( `getting documentation for crate: ${crateName}${version ? ` version ${version}` : ""}`, ); const path = version ? `crate/${crateName}/${version}` : `crate/${crateName}/latest`; const response = await rustHttpClient.docsRsFetch(path); if (response.contentType !== "text") { throw new Error("Expected HTML response but got JSON"); } return turndownInstance.turndown(response.data); } catch (error) { this.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 */ async getTypeInfo( crateName: string, path: string, version?: string, ): Promise<RustType> { try { this.logger.info(`Getting type info for ${path} in crate: ${crateName}`); const versionPath = version || "latest"; const fullPath = `${crateName}/${versionPath}/${crateName}/${path}`; const response = await rustHttpClient.docsRsFetch(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) { this.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 */ async getFeatureFlags( crateName: string, version?: string, ): Promise<FeatureFlag[]> { try { this.logger.info(`Getting feature flags for crate: ${crateName}`); const versionPath = version || "latest"; const response = await rustHttpClient.docsRsFetch( `/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((_: number, element: any) => { 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) { this.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 */ async getCrateVersions( crateName: string, ): Promise<CrateVersion[]> { try { this.logger.info(`getting versions for crate: ${crateName}`); const response = await rustHttpClient.cratesIoFetch(`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) { this.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 */ async getSourceCode( crateName: string, path: string, version?: string, ): Promise<string> { try { this.logger.info(`Getting source code for ${path} in crate: ${crateName}`); const versionPath = version || "latest"; const response = await rustHttpClient.docsRsFetch( `/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) { this.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 */ async searchSymbols( crateName: string, query: string, version?: string, ): Promise<SymbolDefinition[]> { try { this.logger.info( `searching for symbols in crate: ${crateName} with query: ${query}`, ); try { const versionPath = version || "latest"; const response = await rustHttpClient.docsRsFetch( `/${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((_: number, element: any) => { 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")) { this.logger.info( `Search endpoint not found for ${crateName}, trying alternative approach`, ); } // Re-throw other errors throw innerError; } } catch (error) { this.logger.error(`Error searching for symbols in crate: ${crateName}`, { error, }); throw new Error( `Failed to search for symbols: ${(error as Error).message}`, ); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sammcj/mcp-package-docs'

If you have feedback or need assistance with the MCP directory API, please join our Discord server