# 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 | [](https://smithery.ai/server/@cdugo/mcp-get-docs)
4 | [](https://www.npmjs.com/package/@cdugo/docs-fetcher-mcp)
5 | [](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 |
```