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

```
├── .gitignore
├── .npmignore
├── build
│   ├── examples
│   │   ├── CursorAdapter.js
│   │   └── CursorCommands.js
│   ├── index.js
│   ├── services
│   │   ├── CacheService.js
│   │   ├── CursorIntegration.js
│   │   └── ScraperService.js
│   ├── types
│   │   └── index.js
│   └── utils
│       ├── extractors.js
│       ├── github.js
│       └── packageRepository.js
├── cdugo-docs-fetcher-mcp-1.0.0.tgz
├── Dockerfile
├── docs-fetcher-mcp-install.js
├── install.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   ├── index.ts
│   ├── services
│   │   ├── CacheService.ts
│   │   └── ScraperService.ts
│   ├── types
│   │   └── index.ts
│   └── utils
│       ├── extractors.ts
│       └── packageRepository.ts
└── tsconfig.json
```

# Files

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

```
 1 | # Dependency directories
 2 | node_modules/
 3 | 
 4 | # Environment variables
 5 | .env
 6 | 
 7 | # Logs
 8 | logs
 9 | *.log
10 | npm-debug.log*
11 | 
12 | # OS-specific files
13 | .DS_Store
14 | Thumbs.db
15 | 
16 | # Editor directories and files
17 | .idea/
18 | .vscode/
19 | *.swp
20 | *.swo 
```

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
 1 | # Source files
 2 | src/
 3 | 
 4 | # Development files
 5 | .git/
 6 | .gitignore
 7 | tsconfig.json
 8 | Dockerfile
 9 | smithery.yaml
10 | 
11 | # Node modules
12 | node_modules/
13 | 
14 | # IDE files
15 | .vscode/
16 | .idea/
17 | 
18 | # Logs
19 | *.log
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 | 
24 | # OS specific files
25 | .DS_Store
26 | Thumbs.db 
```

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

```markdown
  1 | # 📚 DocsFetcher MCP Server
  2 | 
  3 | [![smithery badge](https://smithery.ai/badge/@cdugo/mcp-get-docs)](https://smithery.ai/server/@cdugo/mcp-get-docs)
  4 | [![npm version](https://img.shields.io/npm/v/@cdugo/docs-fetcher-mcp.svg)](https://www.npmjs.com/package/@cdugo/docs-fetcher-mcp)
  5 | [![npm downloads](https://img.shields.io/npm/dm/@cdugo/docs-fetcher-mcp.svg)](https://www.npmjs.com/package/@cdugo/docs-fetcher-mcp)
  6 | 
  7 | An MCP server that fetches package documentation from multiple language ecosystems for LLMs like Claude without requiring API keys.
  8 | 
  9 | <a href="https://glama.ai/mcp/servers/8yfwtryuc5">
 10 |   <img width="380" height="200" src="https://glama.ai/mcp/servers/8yfwtryuc5/badge" alt="DocsFetcher Server MCP server" />
 11 | </a>
 12 | 
 13 | ## ✨ Features
 14 | 
 15 | - 🌐 Supports multiple programming languages (JavaScript, Python, Java, .NET, Ruby, PHP, Rust, Go, Swift)
 16 | - 📦 Fetches documentation for packages by name or URL
 17 | - 🔍 Crawls documentation sites to extract comprehensive information
 18 | - 📄 Extracts README, API docs, code examples, and repository info
 19 | - 🧠 Provides structured data for LLM summarization
 20 | - 💬 Includes specialized prompts for documentation analysis
 21 | - 🔑 **No API key required** - works natively with Claude Desktop and Cursor IDE
 22 | 
 23 | ## 🚀 Installation
 24 | 
 25 | ### Claude Desktop
 26 | 
 27 | 1. Open Claude Desktop → Settings → Developer
 28 | 2. Click "Edit Config" and add:
 29 | 
 30 | ```json
 31 | {
 32 |   "mcpServers": {
 33 |     "docsFetcher": {
 34 |       "command": "npx",
 35 |       "args": [
 36 |         "-y",
 37 |         "@smithery/cli@latest",
 38 |         "run",
 39 |         "@cdugo/mcp-get-docs",
 40 |         "--config",
 41 |         "'{}'"
 42 |       ]
 43 |     }
 44 |   }
 45 | }
 46 | ```
 47 | 
 48 | ### Cursor IDE Configuration
 49 | 
 50 | 1.  Open Cursor IDE → Settings → MCP -> Add New MCP Servier
 51 | 2.  Add:
 52 | 
 53 | ```json
 54 |     Name: docsFetcher
 55 |     Command: npx -y @smithery/cli@latest run @cdugo/mcp-get-docs --config "{}"
 56 | ```
 57 | 
 58 | #### Prerequisites
 59 | 
 60 | - 📋 Node.js 18 or later
 61 | 
 62 | ## 🏃‍♂️ Running Locally
 63 | 
 64 | ```bash
 65 | git clone https://github.com/cdugo/package-documentation-mcp
 66 | cd package-documentation-mcp
 67 | npm install
 68 | npm run build
 69 | ```
 70 | 
 71 | Once installed, you can run the server locally with:
 72 | 
 73 | ```bash
 74 | # From the project root directory
 75 | npm start
 76 | ```
 77 | 
 78 | For development with auto-restart on file changes:
 79 | 
 80 | ```bash
 81 | npm run dev
 82 | ```
 83 | 
 84 | The server will start on the default port (usually 3000). You should see output like:
 85 | 
 86 | ```
 87 | 🚀 DocsFetcher MCP Server running!
 88 | 📋 Ready to fetch documentation
 89 | ```
 90 | 
 91 | To specify a custom port:
 92 | 
 93 | ```bash
 94 | PORT=8080 npm start
 95 | ```
 96 | 
 97 | ## 🛠️ Available Tools
 98 | 
 99 | 1. **fetch-url-docs**: 🔗 Fetch docs from a specific URL
100 | 2. **fetch-package-docs**: 📦 Fetch docs for a package with optional language specification
101 | 3. **fetch-library-docs**: 🧠 Smart tool that works with either package name or URL
102 | 4. **fetch-multilingual-docs**: 🌍 Fetch docs for a package across multiple language ecosystems
103 | 
104 | ## 📝 Available Prompts
105 | 
106 | 1. **summarize-library-docs**: 📚 Create a comprehensive library summary
107 | 2. **explain-dependency-error**: 🐛 Generate dependency error explanations
108 | 
109 | ## 💡 Example Queries
110 | 
111 | ### Basic Library Information
112 | 
113 | - "What is Express.js and how do I use it?"
114 | - "Tell me about the React library"
115 | - "How do I use requests in Python?"
116 | 
117 | ### Multi-language Support
118 | 
119 | - "Show me documentation for lodash in JavaScript"
120 | - "Compare pandas in Python and data.table in R"
121 | 
122 | ### Using Tools
123 | 
124 | - "@fetch-package-docs with packageName='express' and language='javascript'"
125 | - "@fetch-package-docs with packageName='requests' and language='python'"
126 | - "@fetch-multilingual-docs with packageName='http' and languages=['javascript', 'python', 'rust']"
127 | 
128 | ### Using Prompts
129 | 
130 | - "@summarize-library-docs with libraryName='express'"
131 | - "@explain-dependency-error with packageName='dotenv'"
132 | 
133 | ## ❓ Troubleshooting
134 | 
135 | ### Local Installation
136 | 
137 | - **Server not showing up**: ✅ Verify absolute path in configuration
138 | - **Connection errors**: 🔄 Restart Claude Desktop or Cursor IDE
139 | - **Fetch failures**: ⚠️ Some packages may have non-standard documentation
140 | - **Language support**: 🌐 If a language isn't working, try using the package's direct URL
141 | 
142 | ## 📄 License
143 | 
144 | MIT
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM node:18-alpine
 2 | 
 3 | WORKDIR /app
 4 | 
 5 | # Copy package files
 6 | COPY package*.json ./
 7 | 
 8 | # Install dependencies
 9 | RUN npm install --ignore-scripts
10 | 
11 | # Copy application code
12 | COPY . .
13 | 
14 | # Build the application
15 | RUN npm run build
16 | 
17 | # Command will be provided by smithery.yaml
18 | CMD ["node", "build/index.js"] 
```

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

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["src/**/*"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

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

```typescript
 1 | // Types for processed documentation page
 2 | export interface ProcessedPage {
 3 |   url: string;
 4 |   title: string;
 5 |   content: string;
 6 |   links: string[];
 7 |   codeExamples: CodeExample[];
 8 |   apiSignatures: APISignature[];
 9 |   timestamp: string;
10 | }
11 | 
12 | // Interface for code examples
13 | export interface CodeExample {
14 |   code: string;
15 |   language: string;
16 |   description: string;
17 | }
18 | 
19 | // Interface for API signatures
20 | export interface APISignature {
21 |   name: string;
22 |   signature: string;
23 |   description: string;
24 | }
25 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery.ai configuration
 2 | name: docs-fetcher-mcp
 3 | description: MCP server that fetches library documentation
 4 | version: 1.0.0
 5 | 
 6 | startCommand:
 7 |   type: stdio
 8 |   configSchema:
 9 |     # JSON Schema defining the configuration options for the MCP.
10 |     type: object
11 |     properties:
12 |       cacheDirectory:
13 |         type: string
14 |         description: Directory to store cached documentation (optional)
15 |     required: []
16 |   commandFunction: |-
17 |     (config) => {
18 |       const env = {};
19 |       if (config.cacheDirectory) {
20 |         env.CACHE_DIR = config.cacheDirectory;
21 |       }
22 |       return { 
23 |         command: 'node', 
24 |         args: ['build/index.js'], 
25 |         env 
26 |       };
27 |     }
28 | 
```

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

```json
 1 | {
 2 |   "name": "@cdugo/docs-fetcher-mcp",
 3 |   "version": "1.0.0",
 4 |   "description": "MCP server that fetches package documentation from multiple language ecosystems for LLMs like Claude without requiring API keys",
 5 |   "main": "build/index.js",
 6 |   "type": "module",
 7 |   "bin": {
 8 |     "docs-fetcher-mcp": "./build/index.js",
 9 |     "docs-fetcher-mcp-install": "./docs-fetcher-mcp-install.js"
10 |   },
11 |   "scripts": {
12 |     "build": "tsc && chmod +x build/index.js && chmod +x docs-fetcher-mcp-install.js",
13 |     "dev": "tsc --watch",
14 |     "start": "node build/index.js",
15 |     "install-server": "node install.js",
16 |     "prepublishOnly": "npm run build",
17 |     "smithery": "npx -y @smithery/cli@latest run @cdugo/mcp-get-docs --config \"{}\""
18 |   },
19 |   "keywords": [
20 |     "mcp",
21 |     "documentation",
22 |     "claude",
23 |     "cursor",
24 |     "docs-fetcher",
25 |     "smithery",
26 |     "package-docs",
27 |     "library-docs"
28 |   ],
29 |   "author": "cdugo",
30 |   "license": "MIT",
31 |   "repository": {
32 |     "type": "git",
33 |     "url": "https://github.com/cdugo/package-documentation-mcp"
34 |   },
35 |   "homepage": "https://smithery.ai/server/@cdugo/mcp-get-docs/tools",
36 |   "bugs": {
37 |     "url": "https://github.com/cdugo/package-documentation-mcp/issues"
38 |   },
39 |   "dependencies": {
40 |     "@modelcontextprotocol/sdk": "^1.6.0",
41 |     "cheerio": "^1.0.0",
42 |     "node-fetch": "^3.3.2",
43 |     "zod": "^3.22.4"
44 |   },
45 |   "devDependencies": {
46 |     "@types/node": "^20.10.6",
47 |     "typescript": "^5.3.3"
48 |   },
49 |   "engines": {
50 |     "node": ">=18.0.0"
51 |   },
52 |   "files": [
53 |     "build/",
54 |     "install.js",
55 |     "docs-fetcher-mcp-install.js",
56 |     "README.md",
57 |     "LICENSE"
58 |   ],
59 |   "publishConfig": {
60 |     "access": "public"
61 |   }
62 | }
63 | 
```

--------------------------------------------------------------------------------
/src/services/CacheService.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import * as fs from "fs";
 2 | import * as path from "path";
 3 | import * as os from "os";
 4 | import { ProcessedPage } from "../types/index.js";
 5 | 
 6 | // Create a temporary directory for storing cache
 7 | const DOCS_CACHE_DIR = path.join(os.tmpdir(), "docs-fetcher-cache");
 8 | 
 9 | // Make sure the cache directory exists
10 | if (!fs.existsSync(DOCS_CACHE_DIR)) {
11 |   fs.mkdirSync(DOCS_CACHE_DIR, { recursive: true });
12 | }
13 | 
14 | export class CacheService {
15 |   constructor() {}
16 | 
17 |   /**
18 |    * Get a page from the cache
19 |    * @param url URL of the page to retrieve
20 |    * @returns The cached page, or null if not found or expired
21 |    */
22 |   public getPage(url: string): ProcessedPage | null {
23 |     const cacheKey = Buffer.from(url).toString("base64");
24 |     const cachePath = path.join(DOCS_CACHE_DIR, `${cacheKey}.json`);
25 | 
26 |     if (fs.existsSync(cachePath)) {
27 |       try {
28 |         const cacheData = fs.readFileSync(cachePath, "utf-8");
29 |         const cachedPage = JSON.parse(cacheData) as ProcessedPage;
30 | 
31 |         // Check if cache is valid (less than 24 hours old)
32 |         const cacheTime = new Date(cachedPage.timestamp).getTime();
33 |         const now = new Date().getTime();
34 |         const cacheAge = now - cacheTime;
35 | 
36 |         if (cacheAge < 24 * 60 * 60 * 1000) {
37 |           // 24 hours
38 |           return cachedPage;
39 |         }
40 |       } catch (error) {
41 |         console.error(`Error reading cache for ${url}:`, error);
42 |       }
43 |     }
44 | 
45 |     return null;
46 |   }
47 | 
48 |   /**
49 |    * Save a page to the cache
50 |    * @param url URL of the page to cache
51 |    * @param page Page data to store
52 |    */
53 |   public setPage(url: string, page: ProcessedPage): void {
54 |     const cacheKey = Buffer.from(url).toString("base64");
55 |     const cachePath = path.join(DOCS_CACHE_DIR, `${cacheKey}.json`);
56 | 
57 |     try {
58 |       fs.writeFileSync(cachePath, JSON.stringify(page, null, 2), "utf-8");
59 |     } catch (error) {
60 |       console.error(`Error writing cache for ${url}:`, error);
61 |     }
62 |   }
63 | }
64 | 
65 | // Export a singleton instance
66 | export const cacheService = new CacheService();
67 | 
```

--------------------------------------------------------------------------------
/src/utils/packageRepository.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import fetch from "node-fetch";
  2 | 
  3 | /**
  4 |  * Utility to detect if a string is a URL
  5 |  * @param str String to check
  6 |  * @returns true if the string is a valid URL
  7 |  */
  8 | export function isUrl(str: string): boolean {
  9 |   try {
 10 |     new URL(str);
 11 |     return true;
 12 |   } catch (e) {
 13 |     return false;
 14 |   }
 15 | }
 16 | 
 17 | /**
 18 |  * Get the npm package documentation URL
 19 |  * @param packageName Name of the npm package
 20 |  * @returns URL of the npm package page
 21 |  */
 22 | export function getNpmPackageUrl(packageName: string): string {
 23 |   return `https://www.npmjs.com/package/${packageName}`;
 24 | }
 25 | 
 26 | /**
 27 |  * Get package URL for different package repositories based on language
 28 |  * @param packageName Name of the package
 29 |  * @param language Programming language or repository type
 30 |  * @returns URL of the package documentation page
 31 |  */
 32 | export function getPackageUrl(
 33 |   packageName: string,
 34 |   language = "javascript"
 35 | ): string {
 36 |   const lang = language.toLowerCase().trim();
 37 | 
 38 |   switch (lang) {
 39 |     // JavaScript/TypeScript
 40 |     case "javascript":
 41 |     case "js":
 42 |     case "typescript":
 43 |     case "ts":
 44 |     case "node":
 45 |     case "nodejs":
 46 |     case "npm":
 47 |       return `https://www.npmjs.com/package/${packageName}`;
 48 | 
 49 |     // Python
 50 |     case "python":
 51 |     case "py":
 52 |     case "pypi":
 53 |       return `https://pypi.org/project/${packageName}`;
 54 | 
 55 |     // Java
 56 |     case "java":
 57 |     case "maven":
 58 |       return `https://mvnrepository.com/artifact/${packageName}`;
 59 | 
 60 |     // .NET
 61 |     case "dotnet":
 62 |     case ".net":
 63 |     case "csharp":
 64 |     case "c#":
 65 |     case "nuget":
 66 |       return `https://www.nuget.org/packages/${packageName}`;
 67 | 
 68 |     // Ruby
 69 |     case "ruby":
 70 |     case "gem":
 71 |     case "rubygem":
 72 |     case "rubygems":
 73 |       return `https://rubygems.org/gems/${packageName}`;
 74 | 
 75 |     // PHP
 76 |     case "php":
 77 |     case "composer":
 78 |     case "packagist":
 79 |       return `https://packagist.org/packages/${packageName}`;
 80 | 
 81 |     // Rust
 82 |     case "rust":
 83 |     case "cargo":
 84 |     case "crate":
 85 |     case "crates":
 86 |       return `https://crates.io/crates/${packageName}`;
 87 | 
 88 |     // Go
 89 |     case "go":
 90 |     case "golang":
 91 |       return `https://pkg.go.dev/${packageName}`;
 92 | 
 93 |     // Swift
 94 |     case "swift":
 95 |     case "cocoapods":
 96 |       return `https://cocoapods.org/pods/${packageName}`;
 97 | 
 98 |     // Default to npm
 99 |     default:
100 |       return `https://www.npmjs.com/package/${packageName}`;
101 |   }
102 | }
103 | 
104 | /**
105 |  * Get the GitHub repository URL for an npm package
106 |  * @param packageName Name of the npm package
107 |  * @returns GitHub repository URL, or null if not found
108 |  */
109 | export async function getGitHubRepoUrl(
110 |   packageName: string
111 | ): Promise<string | null> {
112 |   try {
113 |     const response = await fetch(`https://registry.npmjs.org/${packageName}`);
114 |     const data = (await response.json()) as any;
115 | 
116 |     // Try to get GitHub URL from repository field
117 |     if (
118 |       data.repository &&
119 |       typeof data.repository === "object" &&
120 |       data.repository.url
121 |     ) {
122 |       const repoUrl = data.repository.url;
123 |       if (repoUrl.includes("github.com")) {
124 |         return repoUrl
125 |           .replace("git+", "")
126 |           .replace("git://", "https://")
127 |           .replace(".git", "");
128 |       }
129 |     }
130 | 
131 |     // Try to get GitHub URL from homepage field
132 |     if (
133 |       data.homepage &&
134 |       typeof data.homepage === "string" &&
135 |       data.homepage.includes("github.com")
136 |     ) {
137 |       return data.homepage;
138 |     }
139 | 
140 |     return null;
141 |   } catch (error) {
142 |     console.error(`Error fetching GitHub repo URL for ${packageName}:`, error);
143 |     return null;
144 |   }
145 | }
146 | 
147 | /**
148 |  * Extract library name from URL
149 |  * @param url URL to extract library name from
150 |  * @returns Library name
151 |  */
152 | export function extractLibraryName(url: string): string {
153 |   let libraryName = url;
154 |   if (url.includes("npmjs.com/package/")) {
155 |     libraryName = url.split("/package/")[1].split("/")[0];
156 |   } else if (url.includes("pypi.org/project/")) {
157 |     libraryName = url.split("/project/")[1].split("/")[0];
158 |   } else if (url.includes("nuget.org/packages/")) {
159 |     libraryName = url.split("/packages/")[1].split("/")[0];
160 |   } else if (url.includes("rubygems.org/gems/")) {
161 |     libraryName = url.split("/gems/")[1].split("/")[0];
162 |   } else if (url.includes("packagist.org/packages/")) {
163 |     libraryName = url.split("/packages/")[1].split("/")[0];
164 |   } else if (url.includes("crates.io/crates/")) {
165 |     libraryName = url.split("/crates/")[1].split("/")[0];
166 |   } else if (url.includes("pkg.go.dev/")) {
167 |     libraryName = url.split("pkg.go.dev/")[1].split("/")[0];
168 |   } else if (url.includes("cocoapods.org/pods/")) {
169 |     libraryName = url.split("/pods/")[1].split("/")[0];
170 |   } else if (url.includes("mvnrepository.com/artifact/")) {
171 |     libraryName = url.split("/artifact/")[1].split("/")[0];
172 |   } else if (url.includes("github.com")) {
173 |     const parts = url.split("github.com/")[1].split("/");
174 |     if (parts.length >= 2) {
175 |       libraryName = parts[1];
176 |     }
177 |   }
178 |   return libraryName;
179 | }
180 | 
```

--------------------------------------------------------------------------------
/install.js:
--------------------------------------------------------------------------------

```javascript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { execSync } from 'child_process';
  4 | import fs from 'fs';
  5 | import path from 'path';
  6 | import os from 'os';
  7 | import readline from 'readline';
  8 | import { fileURLToPath } from 'url';
  9 | 
 10 | // Get the current file's directory path
 11 | const __filename = fileURLToPath(import.meta.url);
 12 | const __dirname = path.dirname(__filename);
 13 | 
 14 | const rl = readline.createInterface({
 15 |   input: process.stdin,
 16 |   output: process.stdout
 17 | });
 18 | 
 19 | console.log('\n🚀 DocsFetcher MCP Server Installer\n');
 20 | 
 21 | async function promptForClient() {
 22 |   return new Promise((resolve) => {
 23 |     console.log('\nWhere would you like to install this MCP server?');
 24 |     console.log('1. Claude Desktop');
 25 |     console.log('2. Cursor IDE');
 26 |     console.log('3. Both');
 27 |     console.log('4. Skip configuration');
 28 |     
 29 |     rl.question('\nEnter your choice (1-4): ', (choice) => {
 30 |       resolve(choice);
 31 |     });
 32 |   });
 33 | }
 34 | 
 35 | async function main() {
 36 |   try {
 37 |     // Build the project
 38 |     console.log('📦 Building the project...');
 39 |     execSync('npm install && npm run build', { stdio: 'inherit' });
 40 |     
 41 |     // Install globally
 42 |     console.log('\n🌐 Installing the server globally...');
 43 |     try {
 44 |       execSync('npm link', { stdio: 'inherit' });
 45 |       console.log('✅ Successfully installed globally! You can now run "docs-fetcher-mcp" from anywhere.');
 46 |     } catch (error) {
 47 |       console.error('❌ Failed to install globally. You may need to run with sudo/administrator privileges.');
 48 |       console.error('Error:', error.message);
 49 |     }
 50 |     
 51 |     // Configure clients
 52 |     const clientChoice = await promptForClient();
 53 |     const configureClaudeDesktop = ['1', '3'].includes(clientChoice);
 54 |     const configureCursorIDE = ['2', '3'].includes(clientChoice);
 55 |     
 56 |     // Absolute path to the executable
 57 |     const serverPath = path.resolve(process.cwd(), 'build', 'index.js');
 58 |     
 59 |     // Configure Claude Desktop
 60 |     if (configureClaudeDesktop) {
 61 |       console.log('\n🔧 Configuring Claude Desktop...');
 62 |       const claudeConfigDir = path.join(os.homedir(), 'Library', 'Application Support', 'Claude');
 63 |       const claudeConfigPath = path.join(claudeConfigDir, 'claude_desktop_config.json');
 64 |       
 65 |       try {
 66 |         // Create directory if it doesn't exist
 67 |         if (!fs.existsSync(claudeConfigDir)) {
 68 |           fs.mkdirSync(claudeConfigDir, { recursive: true });
 69 |         }
 70 |         
 71 |         // Read existing config or create new one
 72 |         let config = { mcpServers: {} };
 73 |         if (fs.existsSync(claudeConfigPath)) {
 74 |           config = JSON.parse(fs.readFileSync(claudeConfigPath, 'utf8'));
 75 |           if (!config.mcpServers) config.mcpServers = {};
 76 |         }
 77 |         
 78 |         // Add our server
 79 |         config.mcpServers.docsFetcher = {
 80 |           command: 'node',
 81 |           args: [serverPath]
 82 |         };
 83 |         
 84 |         // Write config
 85 |         fs.writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2));
 86 |         console.log('✅ Claude Desktop configured successfully!');
 87 |         console.log(`📄 Configuration saved to: ${claudeConfigPath}`);
 88 |       } catch (error) {
 89 |         console.error('❌ Failed to configure Claude Desktop');
 90 |         console.error('Error:', error.message);
 91 |       }
 92 |     }
 93 |     
 94 |     // Configure Cursor IDE
 95 |     if (configureCursorIDE) {
 96 |       console.log('\n🔧 Configuring Cursor IDE...');
 97 |       const cursorConfigDir = path.join(os.homedir(), '.cursor');
 98 |       const cursorConfigPath = path.join(cursorConfigDir, 'cursor_config.json');
 99 |       
100 |       try {
101 |         // Create directory if it doesn't exist
102 |         if (!fs.existsSync(cursorConfigDir)) {
103 |           fs.mkdirSync(cursorConfigDir, { recursive: true });
104 |         }
105 |         
106 |         // Read existing config or create new one
107 |         let config = { mcpServers: {} };
108 |         if (fs.existsSync(cursorConfigPath)) {
109 |           config = JSON.parse(fs.readFileSync(cursorConfigPath, 'utf8'));
110 |           if (!config.mcpServers) config.mcpServers = {};
111 |         }
112 |         
113 |         // Add our server
114 |         config.mcpServers.docsFetcher = {
115 |           command: 'node',
116 |           args: [serverPath]
117 |         };
118 |         
119 |         // Write config
120 |         fs.writeFileSync(cursorConfigPath, JSON.stringify(config, null, 2));
121 |         console.log('✅ Cursor IDE configured successfully!');
122 |         console.log(`📄 Configuration saved to: ${cursorConfigPath}`);
123 |       } catch (error) {
124 |         console.error('❌ Failed to configure Cursor IDE');
125 |         console.error('Error:', error.message);
126 |       }
127 |     }
128 |     
129 |     console.log('\n🎉 Installation complete!');
130 |     console.log('\nNext steps:');
131 |     
132 |     if (configureClaudeDesktop) {
133 |       console.log('- Restart Claude Desktop to apply changes');
134 |     }
135 |     
136 |     if (configureCursorIDE) {
137 |       console.log('- Restart Cursor IDE to apply changes');
138 |     }
139 |     
140 |     console.log('\nThank you for installing DocsFetcher MCP Server! 🙏');
141 |   } catch (error) {
142 |     console.error('❌ Installation failed:');
143 |     console.error(error);
144 |   } finally {
145 |     rl.close();
146 |   }
147 | }
148 | 
149 | main(); 
```

--------------------------------------------------------------------------------
/src/utils/extractors.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as cheerio from "cheerio";
  2 | import { APISignature, CodeExample } from "../types/index.js";
  3 | 
  4 | /**
  5 |  * Extract relevant links from HTML content
  6 |  * @param html HTML content
  7 |  * @param baseUrl Base URL of the page
  8 |  * @param libraryName Name of the library
  9 |  * @returns Array of relevant links
 10 |  */
 11 | export function extractRelevantLinks(
 12 |   html: string,
 13 |   baseUrl: string,
 14 |   libraryName: string
 15 | ): string[] {
 16 |   const $ = cheerio.load(html);
 17 |   const links = new Set<string>();
 18 |   const baseUrlObj = new URL(baseUrl);
 19 |   const libraryNameLower = libraryName.toLowerCase();
 20 | 
 21 |   // Keywords that indicate important documentation pages
 22 |   const apiKeywords = [
 23 |     "api",
 24 |     "reference",
 25 |     "doc",
 26 |     "guide",
 27 |     "tutorial",
 28 |     "example",
 29 |     "usage",
 30 |     "getting-started",
 31 |     "introduction",
 32 |     "started",
 33 |   ];
 34 | 
 35 |   $("a[href]").each((_, element) => {
 36 |     const href = $(element).attr("href");
 37 |     if (!href) return;
 38 | 
 39 |     try {
 40 |       // Convert relative URLs to absolute
 41 |       const absoluteUrl = new URL(href, baseUrl).href;
 42 |       const urlObj = new URL(absoluteUrl);
 43 | 
 44 |       // Only include links from the same hostname
 45 |       if (urlObj.hostname !== baseUrlObj.hostname) return;
 46 | 
 47 |       const linkText = $(element).text().toLowerCase();
 48 |       const linkPath = urlObj.pathname.toLowerCase();
 49 | 
 50 |       // Check if link contains relevant keywords
 51 |       const isRelevant =
 52 |         apiKeywords.some(
 53 |           (keyword) => linkPath.includes(keyword) || linkText.includes(keyword)
 54 |         ) || linkPath.includes(libraryNameLower);
 55 | 
 56 |       if (isRelevant) {
 57 |         // Avoid hash links to the same page
 58 |         if (absoluteUrl.split("#")[0] !== baseUrl.split("#")[0]) {
 59 |           links.add(absoluteUrl);
 60 |         }
 61 |       }
 62 |     } catch (error) {
 63 |       // Ignore invalid URLs
 64 |     }
 65 |   });
 66 | 
 67 |   return Array.from(links);
 68 | }
 69 | 
 70 | /**
 71 |  * Extract code examples from HTML content
 72 |  * @param html HTML content
 73 |  * @returns Array of code examples
 74 |  */
 75 | export function extractCodeExamples(html: string): CodeExample[] {
 76 |   const $ = cheerio.load(html);
 77 |   const examples: CodeExample[] = [];
 78 | 
 79 |   $(
 80 |     'pre code, pre, code, .highlight, .code-example, [class*="code"], [class*="example"]'
 81 |   ).each((_, element) => {
 82 |     const $elem = $(element);
 83 | 
 84 |     // Skip nested code elements
 85 |     if (
 86 |       $elem.parents("pre, code").length > 0 &&
 87 |       element.name !== "pre" &&
 88 |       element.name !== "code"
 89 |     ) {
 90 |       return;
 91 |     }
 92 | 
 93 |     let code = $elem.text().trim();
 94 |     if (!code || code.length < 10) return; // Skip very short code blocks
 95 | 
 96 |     let language = "";
 97 | 
 98 |     // Try to determine the language from class attributes
 99 |     const className = $elem.attr("class") || "";
100 |     const classMatch = className.match(/(language|lang|syntax)-(\w+)/i);
101 |     if (classMatch) {
102 |       language = classMatch[2];
103 |     } else if (className.includes("js") || className.includes("javascript")) {
104 |       language = "javascript";
105 |     } else if (className.includes("ts") || className.includes("typescript")) {
106 |       language = "typescript";
107 |     }
108 | 
109 |     if (!language) {
110 |       language =
111 |         $elem.attr("data-language") ||
112 |         $elem.attr("data-lang") ||
113 |         $elem.attr("language") ||
114 |         $elem.attr("lang") ||
115 |         "";
116 |     }
117 | 
118 |     // Try to find a description for this code block
119 |     let description = "";
120 |     let $heading = $elem.prev("h1, h2, h3, h4, h5, h6, p");
121 | 
122 |     if ($heading.length > 0) {
123 |       description = $heading.text().trim();
124 |     } else {
125 |       // Look for a heading in the parent element
126 |       const $parent = $elem.parent();
127 |       $heading = $parent.find("h1, h2, h3, h4, h5, h6").first();
128 |       if ($heading.length > 0) {
129 |         description = $heading.text().trim();
130 |       }
131 |     }
132 | 
133 |     examples.push({
134 |       code,
135 |       language: language.toLowerCase(),
136 |       description,
137 |     });
138 |   });
139 | 
140 |   return examples;
141 | }
142 | 
143 | /**
144 |  * Extract API signatures from HTML content
145 |  * @param html HTML content
146 |  * @param libraryName Name of the library
147 |  * @returns Array of API signatures
148 |  */
149 | export function extractAPISignatures(
150 |   html: string,
151 |   libraryName: string
152 | ): APISignature[] {
153 |   const $ = cheerio.load(html);
154 |   const signatures: APISignature[] = [];
155 | 
156 |   const cleanText = (text: string): string => text.replace(/\s+/g, " ").trim();
157 | 
158 |   $("h1, h2, h3, h4, h5, h6").each((_, heading) => {
159 |     const $heading = $(heading);
160 |     const headingText = cleanText($heading.text());
161 | 
162 |     // Skip very long headings or common sections
163 |     if (
164 |       headingText.length > 100 ||
165 |       headingText.toLowerCase().includes("introduction") ||
166 |       headingText.toLowerCase().includes("getting started")
167 |     ) {
168 |       return;
169 |     }
170 | 
171 |     let signature = "";
172 |     let description = "";
173 | 
174 |     // Look for code blocks after the heading
175 |     const $code = $heading
176 |       .nextAll("pre, code, .signature, .function-signature")
177 |       .first();
178 |     if (
179 |       $code.length > 0 &&
180 |       $code.prevAll("h1, h2, h3, h4, h5, h6").first().is($heading)
181 |     ) {
182 |       signature = cleanText($code.text());
183 |     }
184 | 
185 |     // Look for description paragraphs
186 |     const $description = $heading.nextAll("p").first();
187 |     if (
188 |       $description.length > 0 &&
189 |       $description.prevAll("h1, h2, h3, h4, h5, h6").first().is($heading)
190 |     ) {
191 |       description = cleanText($description.text());
192 |     }
193 | 
194 |     // Only add if we have either a signature or description
195 |     if (signature || description) {
196 |       signatures.push({
197 |         name: headingText,
198 |         signature,
199 |         description,
200 |       });
201 |     }
202 |   });
203 | 
204 |   return signatures;
205 | }
206 | 
```

--------------------------------------------------------------------------------
/docs-fetcher-mcp-install.js:
--------------------------------------------------------------------------------

```javascript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { execSync } from 'child_process';
  4 | import fs from 'fs';
  5 | import path from 'path';
  6 | import os from 'os';
  7 | import readline from 'readline';
  8 | import { fileURLToPath } from 'url';
  9 | 
 10 | // Get the current file's directory path
 11 | const __filename = fileURLToPath(import.meta.url);
 12 | const __dirname = path.dirname(__filename);
 13 | 
 14 | const rl = readline.createInterface({
 15 |   input: process.stdin,
 16 |   output: process.stdout
 17 | });
 18 | 
 19 | console.log('\n🚀 DocsFetcher MCP Server Installer\n');
 20 | 
 21 | async function promptForClient() {
 22 |   return new Promise((resolve) => {
 23 |     console.log('\nWhere would you like to configure this MCP server?');
 24 |     console.log('1. Claude Desktop');
 25 |     console.log('2. Cursor IDE');
 26 |     console.log('3. Both');
 27 |     console.log('4. Skip configuration');
 28 |     
 29 |     rl.question('\nEnter your choice (1-4): ', (choice) => {
 30 |       resolve(choice);
 31 |     });
 32 |   });
 33 | }
 34 | 
 35 | async function promptForInstallationType() {
 36 |   return new Promise((resolve) => {
 37 |     console.log('\nHow would you like to configure the MCP server?');
 38 |     console.log('1. Use Smithery deployment (recommended)');
 39 |     console.log('2. Use local npm installation');
 40 |     
 41 |     rl.question('\nEnter your choice (1-2): ', (choice) => {
 42 |       resolve(choice);
 43 |     });
 44 |   });
 45 | }
 46 | 
 47 | async function main() {
 48 |   try {
 49 |     // Configure clients
 50 |     const clientChoice = await promptForClient();
 51 |     const configureClaudeDesktop = ['1', '3'].includes(clientChoice);
 52 |     const configureCursorIDE = ['2', '3'].includes(clientChoice);
 53 |     
 54 |     if (clientChoice === '4') {
 55 |       console.log('\n⏭️ Skipping configuration...');
 56 |       rl.close();
 57 |       return;
 58 |     }
 59 |     
 60 |     const installationType = await promptForInstallationType();
 61 |     const useSmithery = installationType === '1';
 62 |     
 63 |     // Configure Claude Desktop
 64 |     if (configureClaudeDesktop) {
 65 |       console.log('\n🔧 Configuring Claude Desktop...');
 66 |       const claudeConfigDir = path.join(os.homedir(), 'Library', 'Application Support', 'Claude');
 67 |       const claudeConfigPath = path.join(claudeConfigDir, 'claude_desktop_config.json');
 68 |       
 69 |       try {
 70 |         // Create directory if it doesn't exist
 71 |         if (!fs.existsSync(claudeConfigDir)) {
 72 |           fs.mkdirSync(claudeConfigDir, { recursive: true });
 73 |         }
 74 |         
 75 |         // Read existing config or create new one
 76 |         let config = { mcpServers: {} };
 77 |         if (fs.existsSync(claudeConfigPath)) {
 78 |           config = JSON.parse(fs.readFileSync(claudeConfigPath, 'utf8'));
 79 |           if (!config.mcpServers) config.mcpServers = {};
 80 |         }
 81 |         
 82 |         // Add our server
 83 |         if (useSmithery) {
 84 |           config.mcpServers.docsFetcher = {
 85 |             url: "https://smithery.ai/server/@cdugo/mcp-get-docs/tools"
 86 |           };
 87 |         } else {
 88 |           config.mcpServers.docsFetcher = {
 89 |             command: "npx",
 90 |             args: [
 91 |               "-y",
 92 |               "@smithery/cli@latest",
 93 |               "run",
 94 |               "@cdugo/mcp-get-docs",
 95 |               "--config",
 96 |               "'{}'",
 97 |             ]
 98 |           };
 99 |         }
100 |         
101 |         // Write config
102 |         fs.writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2));
103 |         console.log('✅ Claude Desktop configured successfully!');
104 |         console.log(`📄 Configuration saved to: ${claudeConfigPath}`);
105 |       } catch (error) {
106 |         console.error('❌ Failed to configure Claude Desktop');
107 |         console.error('Error:', error.message);
108 |       }
109 |     }
110 |     
111 |     // Configure Cursor IDE
112 |     if (configureCursorIDE) {
113 |       console.log('\n🔧 Configuring Cursor IDE...');
114 |       const cursorConfigDir = path.join(os.homedir(), '.cursor');
115 |       const cursorConfigPath = path.join(cursorConfigDir, 'cursor_config.json');
116 |       
117 |       try {
118 |         // Create directory if it doesn't exist
119 |         if (!fs.existsSync(cursorConfigDir)) {
120 |           fs.mkdirSync(cursorConfigDir, { recursive: true });
121 |         }
122 |         
123 |         // Read existing config or create new one
124 |         let config = { mcpServers: {} };
125 |         if (fs.existsSync(cursorConfigPath)) {
126 |           config = JSON.parse(fs.readFileSync(cursorConfigPath, 'utf8'));
127 |           if (!config.mcpServers) config.mcpServers = {};
128 |         }
129 |         
130 |         // Add our server
131 |         if (useSmithery) {
132 |           config.mcpServers.docsFetcher = {
133 |             url: "https://smithery.ai/server/@cdugo/mcp-get-docs/tools"
134 |           };
135 |         } else {
136 |           config.mcpServers.docsFetcher = {
137 |             command: "npx",
138 |             args: [
139 |               "-y",
140 |               "@smithery/cli@latest",
141 |               "run",
142 |               "@cdugo/mcp-get-docs",
143 |               "--config",
144 |               "'{}'",
145 |             ]
146 |           };
147 |         }
148 |         
149 |         // Write config
150 |         fs.writeFileSync(cursorConfigPath, JSON.stringify(config, null, 2));
151 |         console.log('✅ Cursor IDE configured successfully!');
152 |         console.log(`📄 Configuration saved to: ${cursorConfigPath}`);
153 |       } catch (error) {
154 |         console.error('❌ Failed to configure Cursor IDE');
155 |         console.error('Error:', error.message);
156 |       }
157 |     }
158 |     
159 |     console.log('\n🎉 Configuration complete!');
160 |     console.log('\nNext steps:');
161 |     
162 |     if (configureClaudeDesktop) {
163 |       console.log('- Restart Claude Desktop to apply changes');
164 |     }
165 |     
166 |     if (configureCursorIDE) {
167 |       console.log('- Restart Cursor IDE to apply changes');
168 |     }
169 |     
170 |     console.log('\nThank you for installing DocsFetcher MCP Server! 🙏');
171 |   } catch (error) {
172 |     console.error('❌ Configuration failed:');
173 |     console.error(error);
174 |   } finally {
175 |     rl.close();
176 |   }
177 | }
178 | 
179 | main(); 
```

--------------------------------------------------------------------------------
/src/services/ScraperService.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import fetch from "node-fetch";
  2 | import * as cheerio from "cheerio";
  3 | import { ProcessedPage } from "../types/index.js";
  4 | import { cacheService } from "./CacheService.js";
  5 | import {
  6 |   extractRelevantLinks,
  7 |   extractCodeExamples,
  8 |   extractAPISignatures,
  9 | } from "../utils/extractors.js";
 10 | import { extractLibraryName } from "../utils/packageRepository.js";
 11 | 
 12 | export class ScraperService {
 13 |   /**
 14 |    * Fetch and process a documentation page
 15 |    * @param url URL to process
 16 |    * @param libraryName Name of the library
 17 |    * @param skipCache Whether to skip the cache
 18 |    * @returns Processed page or null if failed
 19 |    */
 20 |   public async fetchAndProcessPage(
 21 |     url: string,
 22 |     libraryName: string,
 23 |     skipCache = false
 24 |   ): Promise<ProcessedPage | null> {
 25 |     try {
 26 |       // Check cache first
 27 |       if (!skipCache) {
 28 |         const cachedPage = cacheService.getPage(url);
 29 |         if (cachedPage) {
 30 |           console.error(`Using cached version of ${url}`);
 31 |           return cachedPage;
 32 |         }
 33 |       }
 34 | 
 35 |       console.error(`Fetching documentation from ${url}`);
 36 |       const response = await fetch(url);
 37 |       const html = await response.text();
 38 | 
 39 |       // Parse HTML using cheerio
 40 |       const $ = cheerio.load(html);
 41 | 
 42 |       // Remove script and style elements
 43 |       $("script, style, noscript, iframe").remove();
 44 | 
 45 |       // Extract basic metadata
 46 |       const title = $("title").text();
 47 | 
 48 |       // Extract links for crawling
 49 |       const links = extractRelevantLinks(html, url, libraryName);
 50 | 
 51 |       // Extract code examples and API signatures
 52 |       const codeExamples = extractCodeExamples(html);
 53 |       const apiSignatures = extractAPISignatures(html, libraryName);
 54 | 
 55 |       // Extract main content
 56 |       const mainContent =
 57 |         $("main, article, .readme, .content, .documentation, #readme").html() ||
 58 |         "";
 59 | 
 60 |       // Extract text from body if no main content found
 61 |       const content = mainContent || $("body").html() || "";
 62 | 
 63 |       // Create the processed page
 64 |       const processedPage: ProcessedPage = {
 65 |         url,
 66 |         title,
 67 |         content,
 68 |         links,
 69 |         codeExamples,
 70 |         apiSignatures,
 71 |         timestamp: new Date().toISOString(),
 72 |       };
 73 | 
 74 |       // Cache the page
 75 |       cacheService.setPage(url, processedPage);
 76 | 
 77 |       return processedPage;
 78 |     } catch (error) {
 79 |       console.error(`Error processing ${url}:`, error);
 80 |       return null;
 81 |     }
 82 |   }
 83 | 
 84 |   /**
 85 |    * Crawl documentation pages starting from a URL
 86 |    * @param startUrl Starting URL for crawling
 87 |    * @param libraryName Name of the library
 88 |    * @param maxPages Maximum number of pages to crawl
 89 |    * @param skipCache Whether to skip the cache
 90 |    * @returns Array of processed pages
 91 |    */
 92 |   public async crawlDocumentation(
 93 |     startUrl: string,
 94 |     libraryName: string,
 95 |     maxPages = 5,
 96 |     skipCache = false
 97 |   ): Promise<ProcessedPage[]> {
 98 |     const visitedUrls = new Set<string>();
 99 |     const processedPages: ProcessedPage[] = [];
100 |     const urlsToVisit: string[] = [startUrl];
101 | 
102 |     while (urlsToVisit.length > 0 && processedPages.length < maxPages) {
103 |       const currentUrl = urlsToVisit.shift()!;
104 | 
105 |       if (visitedUrls.has(currentUrl)) {
106 |         continue;
107 |       }
108 | 
109 |       visitedUrls.add(currentUrl);
110 | 
111 |       const processedPage = await this.fetchAndProcessPage(
112 |         currentUrl,
113 |         libraryName,
114 |         skipCache
115 |       );
116 |       if (processedPage) {
117 |         processedPages.push(processedPage);
118 | 
119 |         // Add new URLs to visit
120 |         for (const link of processedPage.links) {
121 |           if (!visitedUrls.has(link) && !urlsToVisit.includes(link)) {
122 |             urlsToVisit.push(link);
123 |           }
124 |         }
125 |       }
126 |     }
127 | 
128 |     return processedPages;
129 |   }
130 | 
131 |   /**
132 |    * Fetch library documentation
133 |    * @param url URL or package name
134 |    * @param maxPages Maximum number of pages to crawl
135 |    * @returns Compiled markdown document
136 |    */
137 |   public async fetchLibraryDocumentation(
138 |     url: string,
139 |     maxPages = 5
140 |   ): Promise<string> {
141 |     try {
142 |       // If input is not a URL, assume it's a package name
143 |       if (!url.startsWith("http")) {
144 |         url = `https://www.npmjs.com/package/${url}`;
145 |       }
146 | 
147 |       // Extract library name from URL
148 |       const libraryName = extractLibraryName(url);
149 | 
150 |       // Crawl documentation
151 |       const pages = await this.crawlDocumentation(url, libraryName, maxPages);
152 | 
153 |       if (pages.length === 0) {
154 |         throw new Error(`Failed to fetch documentation from ${url}`);
155 |       }
156 | 
157 |       // Compile documentation into a single markdown document
158 |       const documentation = this.compileDocumentation(pages, libraryName);
159 | 
160 |       // Include instructions for using the prompt
161 |       const promptInstructions = `
162 | ---
163 | 
164 | 🔍 For better summarization, use the "summarize-library-docs" prompt with:
165 | - libraryName: "${libraryName}"
166 | - documentation: <the content above>
167 | 
168 | Example: @summarize-library-docs with libraryName="${libraryName}"
169 |       `;
170 | 
171 |       return documentation + promptInstructions;
172 |     } catch (error) {
173 |       console.error(`Error fetching URL content:`, error);
174 | 
175 |       // Extract library name from URL
176 |       const libraryName = extractLibraryName(url);
177 | 
178 |       const errorMessage = `Error fetching URL content: ${
179 |         error instanceof Error ? error.message : String(error)
180 |       }`;
181 | 
182 |       // Include error-specific prompt instructions
183 |       const promptInstructions = `
184 | ---
185 | 
186 | 🔍 For information about this library despite the fetch error, use the "summarize-library-docs" prompt with:
187 | - libraryName: "${libraryName}"
188 | - errorStatus: "${error instanceof Error ? error.message : String(error)}"
189 | 
190 | Example: @summarize-library-docs with libraryName="${libraryName}" and errorStatus="fetch failed"
191 |       `;
192 | 
193 |       return errorMessage + promptInstructions;
194 |     }
195 |   }
196 | 
197 |   /**
198 |    * Compile processed pages into a single markdown document
199 |    * @param pages Array of processed pages
200 |    * @param libraryName Name of the library
201 |    * @returns Compiled markdown document
202 |    */
203 |   private compileDocumentation(
204 |     pages: ProcessedPage[],
205 |     libraryName: string
206 |   ): string {
207 |     const $ = cheerio.load("");
208 | 
209 |     // Create a title for the documentation
210 |     let result = `# ${libraryName} Documentation\n\n`;
211 | 
212 |     // Add metadata
213 |     result += `## 📋 Documentation Overview\n\n`;
214 |     result += `Library Name: ${libraryName}\n`;
215 |     result += `Pages Analyzed: ${pages.length}\n`;
216 |     result += `Generated: ${new Date().toISOString()}\n\n`;
217 | 
218 |     // Add table of contents
219 |     result += `## 📑 Table of Contents\n\n`;
220 |     pages.forEach((page, index) => {
221 |       result += `${index + 1}. [${page.title}](#${page.title
222 |         .toLowerCase()
223 |         .replace(/[^a-z0-9]+/g, "-")})\n`;
224 |     });
225 |     result += `\n`;
226 | 
227 |     // Process each page
228 |     pages.forEach((page, index) => {
229 |       // Add page header
230 |       result += `## ${page.title}\n\n`;
231 |       result += `Source: ${page.url}\n\n`;
232 | 
233 |       // Process page content
234 |       const pageContent = cheerio.load(page.content);
235 | 
236 |       // Extract headings and their content
237 |       const headings = pageContent("h1, h2, h3, h4, h5, h6");
238 |       if (headings.length > 0) {
239 |         headings.each((_, heading) => {
240 |           const level = parseInt(heading.name.replace("h", ""));
241 |           const headingText = pageContent(heading).text().trim();
242 | 
243 |           // Add heading
244 |           result += `${"#".repeat(level + 1)} ${headingText}\n\n`;
245 | 
246 |           // Get content until next heading
247 |           let content = "";
248 |           let next = pageContent(heading).next();
249 |           while (next.length && !next.is("h1, h2, h3, h4, h5, h6")) {
250 |             if (next.is("p, ul, ol, pre, code, table")) {
251 |               content += pageContent.html(next) + "\n\n";
252 |             }
253 |             next = next.next();
254 |           }
255 | 
256 |           // Add content
257 |           if (content) {
258 |             const contentText = $("<div>").html(content).text();
259 |             result += `${contentText}\n\n`;
260 |           }
261 |         });
262 |       } else {
263 |         // If no headings, just add the whole content
264 |         const contentText = $("<div>").html(page.content).text();
265 |         result += `${contentText}\n\n`;
266 |       }
267 | 
268 |       // Add code examples if available
269 |       if (page.codeExamples.length > 0) {
270 |         result += `### Code Examples\n\n`;
271 |         page.codeExamples.forEach((example) => {
272 |           if (example.description) {
273 |             result += `#### ${example.description}\n\n`;
274 |           }
275 |           result += `\`\`\`${example.language}\n${example.code}\n\`\`\`\n\n`;
276 |         });
277 |       }
278 | 
279 |       // Add API signatures if available
280 |       if (page.apiSignatures.length > 0) {
281 |         result += `### API Reference\n\n`;
282 |         page.apiSignatures.forEach((api) => {
283 |           result += `#### ${api.name}\n\n`;
284 |           if (api.signature) {
285 |             result += `\`\`\`\n${api.signature}\n\`\`\`\n\n`;
286 |           }
287 |           if (api.description) {
288 |             result += `${api.description}\n\n`;
289 |           }
290 |         });
291 |       }
292 | 
293 |       // Add separator between pages
294 |       if (index < pages.length - 1) {
295 |         result += `---\n\n`;
296 |       }
297 |     });
298 | 
299 |     // Add instructions for the LLM at the end
300 |     result += `## 📌 Instructions for Summarization\n\n`;
301 |     result += `1. Provide a concise overview of what this library/package does\n`;
302 |     result += `2. Highlight key features and functionality\n`;
303 |     result += `3. Include basic usage examples when available\n`;
304 |     result += `4. Format the response for readability\n`;
305 |     result += `5. If any part of the documentation is unclear, mention this\n`;
306 |     result += `6. Include installation instructions if available\n`;
307 | 
308 |     return result;
309 |   }
310 | }
311 | 
312 | // Export a singleton instance
313 | export const scraperService = new ScraperService();
314 | 
```

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

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import { z } from "zod";
  6 | import * as fs from "fs";
  7 | import * as path from "path";
  8 | import * as os from "os";
  9 | import {
 10 |   isUrl,
 11 |   getNpmPackageUrl,
 12 |   getGitHubRepoUrl,
 13 |   getPackageUrl,
 14 | } from "./utils/packageRepository.js";
 15 | import { scraperService } from "./services/ScraperService.js";
 16 | 
 17 | // Create a temporary directory for storing documentation
 18 | const DOCS_DIR = path.join(os.tmpdir(), "docs-fetcher-mcp");
 19 | 
 20 | // Make sure the docs directory exists
 21 | if (!fs.existsSync(DOCS_DIR)) {
 22 |   fs.mkdirSync(DOCS_DIR, { recursive: true });
 23 | }
 24 | 
 25 | // Create the MCP server
 26 | const server = new McpServer({
 27 |   name: "DocsFetcher",
 28 |   version: "1.0.0",
 29 | });
 30 | 
 31 | // Define a prompt template for summarizing library documentation
 32 | server.prompt(
 33 |   "summarize-library-docs",
 34 |   {
 35 |     libraryName: z.string().describe("Name of the library to summarize"),
 36 |     documentation: z.string().describe("The raw documentation content"),
 37 |     errorStatus: z.string().optional().describe("Error status if applicable"),
 38 |   },
 39 |   (args) => {
 40 |     const { libraryName, documentation, errorStatus } = args;
 41 |     const hasError = errorStatus && errorStatus !== "";
 42 | 
 43 |     if (hasError) {
 44 |       return {
 45 |         messages: [
 46 |           {
 47 |             role: "user",
 48 |             content: {
 49 |               type: "text",
 50 |               text: `I was trying to learn about the ${libraryName} library, but there was an error fetching the documentation: ${errorStatus}. Can you tell me what you know about it based on your training?`,
 51 |             },
 52 |           },
 53 |         ],
 54 |       };
 55 |     }
 56 | 
 57 |     return {
 58 |       messages: [
 59 |         {
 60 |           role: "user",
 61 |           content: {
 62 |             type: "text",
 63 |             text: `I need to understand the ${libraryName} library. Here's the raw documentation:
 64 | 
 65 | ${documentation}
 66 | 
 67 | Please summarize this documentation for me with:
 68 | 1. A brief overview of what the library does
 69 | 2. Key features and capabilities
 70 | 3. Basic installation and usage examples
 71 | 4. Any important API methods or patterns
 72 | 5. Common use cases
 73 | 
 74 | Focus on the most important information that would help me understand and start using this library.`,
 75 |           },
 76 |         },
 77 |       ],
 78 |     };
 79 |   }
 80 | );
 81 | 
 82 | // Define a prompt for exploring dependency errors
 83 | server.prompt(
 84 |   "explain-dependency-error",
 85 |   {
 86 |     packageName: z
 87 |       .string()
 88 |       .describe("The package causing the dependency error"),
 89 |     documentation: z.string().describe("The package documentation"),
 90 |     errorStatus: z.string().optional().describe("Error status if applicable"),
 91 |   },
 92 |   (args) => {
 93 |     const { packageName, documentation, errorStatus } = args;
 94 |     const hasError = errorStatus && errorStatus !== "";
 95 | 
 96 |     if (hasError) {
 97 |       return {
 98 |         messages: [
 99 |           {
100 |             role: "user",
101 |             content: {
102 |               type: "text",
103 |               text: `I'm getting a dependency error for the '${packageName}' package. There was an issue fetching the detailed documentation: ${errorStatus}. Can you explain what this package does, how to install it properly, and why I might be seeing an error?`,
104 |             },
105 |           },
106 |         ],
107 |       };
108 |     }
109 | 
110 |     return {
111 |       messages: [
112 |         {
113 |           role: "user",
114 |           content: {
115 |             type: "text",
116 |             text: `I'm getting a dependency error for the '${packageName}' package. Here's the documentation:
117 | 
118 | ${documentation}
119 | 
120 | Based on this information, please:
121 | 1. Explain what this package does
122 | 2. Show me how to properly install it
123 | 3. Tell me common reasons why I might be getting a dependency error
124 | 4. Provide a simple example of how to use it correctly`,
125 |           },
126 |         },
127 |       ],
128 |     };
129 |   }
130 | );
131 | 
132 | // Tool to fetch documentation from a URL
133 | server.tool(
134 |   "fetch-url-docs",
135 |   {
136 |     url: z.string().url().describe("URL of the library documentation to fetch"),
137 |   },
138 |   async ({ url }) => {
139 |     console.error(`Fetching documentation from URL: ${url}`);
140 | 
141 |     try {
142 |       const documentationContent =
143 |         await scraperService.fetchLibraryDocumentation(url);
144 | 
145 |       return {
146 |         content: [
147 |           {
148 |             type: "text",
149 |             text: documentationContent,
150 |           },
151 |         ],
152 |       };
153 |     } catch (error) {
154 |       console.error("Error fetching URL content:", error);
155 | 
156 |       const errorMessage = `Error fetching URL content: ${
157 |         error instanceof Error ? error.message : String(error)
158 |       }`;
159 | 
160 |       return {
161 |         content: [
162 |           {
163 |             type: "text",
164 |             text: errorMessage,
165 |           },
166 |         ],
167 |         isError: true,
168 |       };
169 |     }
170 |   }
171 | );
172 | 
173 | // Tool to fetch package documentation with language support
174 | server.tool(
175 |   "fetch-package-docs",
176 |   {
177 |     packageName: z
178 |       .string()
179 |       .describe("Name of the package to fetch documentation for"),
180 |     language: z
181 |       .string()
182 |       .optional()
183 |       .describe(
184 |         "Programming language or repository type (e.g., javascript, python, java, dotnet)"
185 |       ),
186 |   },
187 |   async ({ packageName, language = "javascript" }) => {
188 |     console.error(
189 |       `Fetching documentation for package: ${packageName} (${language})`
190 |     );
191 | 
192 |     try {
193 |       const packageUrl = getPackageUrl(packageName, language);
194 |       console.error(`Using package URL: ${packageUrl}`);
195 | 
196 |       const documentationContent =
197 |         await scraperService.fetchLibraryDocumentation(packageUrl);
198 | 
199 |       return {
200 |         content: [
201 |           {
202 |             type: "text",
203 |             text: documentationContent,
204 |           },
205 |         ],
206 |       };
207 |     } catch (error) {
208 |       console.error("Error fetching package content:", error);
209 | 
210 |       const errorMessage = `Error fetching package documentation: ${
211 |         error instanceof Error ? error.message : String(error)
212 |       }`;
213 | 
214 |       return {
215 |         content: [
216 |           {
217 |             type: "text",
218 |             text: errorMessage,
219 |           },
220 |         ],
221 |         isError: true,
222 |       };
223 |     }
224 |   }
225 | );
226 | 
227 | // Tool to fetch documentation from either a package name or URL
228 | server.tool(
229 |   "fetch-library-docs",
230 |   {
231 |     library: z
232 |       .string()
233 |       .describe(
234 |         "Name of the package or URL of the library documentation to fetch"
235 |       ),
236 |     language: z
237 |       .string()
238 |       .optional()
239 |       .describe(
240 |         "Programming language or repository type if providing a package name (e.g., javascript, python, java, dotnet)"
241 |       ),
242 |   },
243 |   async ({ library, language = "javascript" }) => {
244 |     console.error(
245 |       `Fetching documentation for library: ${library} ${
246 |         language ? `(${language})` : ""
247 |       }`
248 |     );
249 | 
250 |     try {
251 |       // Determine if input is a URL or package name
252 |       const isLibraryUrl = isUrl(library);
253 |       let url = isLibraryUrl ? library : getPackageUrl(library, language);
254 | 
255 |       const documentationContent =
256 |         await scraperService.fetchLibraryDocumentation(url);
257 | 
258 |       return {
259 |         content: [
260 |           {
261 |             type: "text",
262 |             text: documentationContent,
263 |           },
264 |         ],
265 |       };
266 |     } catch (error) {
267 |       console.error("Error fetching library documentation:", error);
268 | 
269 |       const errorMessage = `Error fetching library documentation: ${
270 |         error instanceof Error ? error.message : String(error)
271 |       }`;
272 | 
273 |       return {
274 |         content: [
275 |           {
276 |             type: "text",
277 |             text: errorMessage,
278 |           },
279 |         ],
280 |         isError: true,
281 |       };
282 |     }
283 |   }
284 | );
285 | 
286 | // Tool to fetch documentation from multiple language repositories at once
287 | server.tool(
288 |   "fetch-multilingual-docs",
289 |   {
290 |     packageName: z
291 |       .string()
292 |       .describe("Name of the package to fetch documentation for"),
293 |     languages: z
294 |       .array(z.string())
295 |       .describe(
296 |         "List of programming languages or repository types to check (e.g., javascript, python, java)"
297 |       ),
298 |   },
299 |   async ({ packageName, languages }) => {
300 |     console.error(
301 |       `Fetching documentation for package: ${packageName} across languages: ${languages.join(
302 |         ", "
303 |       )}`
304 |     );
305 | 
306 |     const results: Record<string, any> = {};
307 |     let hasSuccessfulFetch = false;
308 | 
309 |     for (const language of languages) {
310 |       try {
311 |         console.error(`Trying ${language} repository...`);
312 |         const packageUrl = getPackageUrl(packageName, language);
313 | 
314 |         const documentationContent =
315 |           await scraperService.fetchLibraryDocumentation(packageUrl);
316 | 
317 |         results[language] = {
318 |           url: packageUrl,
319 |           success: true,
320 |           content: documentationContent,
321 |         };
322 | 
323 |         hasSuccessfulFetch = true;
324 |       } catch (error) {
325 |         console.error(`Error fetching ${language} documentation:`, error);
326 |         results[language] = {
327 |           success: false,
328 |           error: error instanceof Error ? error.message : String(error),
329 |         };
330 |       }
331 |     }
332 | 
333 |     if (!hasSuccessfulFetch) {
334 |       return {
335 |         content: [
336 |           {
337 |             type: "text",
338 |             text: `Failed to fetch documentation for ${packageName} in any of the requested languages: ${languages.join(
339 |               ", "
340 |             )}.`,
341 |           },
342 |         ],
343 |         isError: true,
344 |       };
345 |     }
346 | 
347 |     // Format the successful results
348 |     const bestLanguage =
349 |       Object.keys(results).find((lang) => results[lang].success) ||
350 |       languages[0];
351 |     const bestContent = results[bestLanguage].content;
352 | 
353 |     // Include a summary of all language results
354 |     const summaryLines = [
355 |       `## Documentation Search Results for '${packageName}'`,
356 |     ];
357 |     summaryLines.push("");
358 | 
359 |     for (const language of languages) {
360 |       const result = results[language];
361 |       if (result.success) {
362 |         summaryLines.push(
363 |           `✅ **${language}**: Successfully fetched documentation from ${result.url}`
364 |         );
365 |       } else {
366 |         summaryLines.push(`❌ **${language}**: Failed - ${result.error}`);
367 |       }
368 |     }
369 | 
370 |     summaryLines.push("");
371 |     summaryLines.push(`---`);
372 |     summaryLines.push("");
373 |     summaryLines.push(
374 |       `# Documentation Content (from ${bestLanguage} repository)`
375 |     );
376 |     summaryLines.push("");
377 | 
378 |     const summary = summaryLines.join("\n");
379 |     const completeContent = summary + bestContent;
380 | 
381 |     return {
382 |       content: [
383 |         {
384 |           type: "text",
385 |           text: completeContent,
386 |         },
387 |       ],
388 |     };
389 |   }
390 | );
391 | 
392 | // Create the transport and start the server
393 | const transport = new StdioServerTransport();
394 | server.connect(transport).catch((error: Error) => {
395 |   console.error("Server error:", error);
396 |   process.exit(1);
397 | });
398 | 
```