├── .github ├── FUNDING.yml └── workflows │ └── publish.yml ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── eslint.config.js ├── package.json ├── smithery.yaml ├── src ├── index.ts ├── logger.ts ├── lsp │ └── typescript-lsp-client.ts ├── npm-docs-enhancer.ts ├── npm-docs-integration.ts ├── package-docs-server.ts ├── registry-utils.ts ├── rust-docs-integration.ts ├── search-utils.ts ├── tool-handlers.ts ├── types.ts └── utils │ └── rust-http-client.ts ├── test-go-docs-direct.js ├── test-lsp-cli.js ├── test-lsp.js ├── test-npm-docs.js ├── test-typescript.ts └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [sammcj] 4 | buy_me_a_coffee: sam.mcleod 5 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to npmjs 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | push: 8 | branches: 9 | - main 10 | 11 | # cancel previous runs if a new one is triggered 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: write 20 | id-token: write 21 | steps: 22 | - uses: actions/checkout@v4 23 | # Setup .npmrc file to publish to npm 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: "22.x" 27 | registry-url: "https://registry.npmjs.org" 28 | - run: npm install 29 | 30 | - name: Get version from package.json 31 | id: version 32 | run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT 33 | 34 | - name: Create Git tag 35 | # if: ${{ !contains(github.event.head_commit.message, 'chore(release)') && github.event_name != 'workflow_dispatch' }} 36 | if: ${{ contains(github.event.head_commit.message, 'chore(release)') || github.event_name == 'workflow_dispatch' }} 37 | run: | 38 | git config --local user.email "action@github.com" 39 | git config --local user.name "GitHub Action" 40 | git tag -a "v${{ steps.version.outputs.version }}" -m "Release v${{ steps.version.outputs.version }}" 41 | git push origin "v${{ steps.version.outputs.version }}" 42 | 43 | - name: Create GitHub Release 44 | # if: ${{ !contains(github.event.head_commit.message, 'chore(release)') && github.event_name != 'workflow_dispatch' }} 45 | if: ${{ contains(github.event.head_commit.message, 'chore(release)') || github.event_name == 'workflow_dispatch' }} 46 | uses: softprops/action-gh-release@v1 47 | with: 48 | tag_name: v${{ steps.version.outputs.version }} 49 | name: Release v${{ steps.version.outputs.version }} 50 | generate_release_notes: true 51 | 52 | - run: npm publish --provenance --access public 53 | # only if the commit message contains chore(release), or if manually triggered with workflow_dispatch 54 | if: ${{ contains(github.event.head_commit.message, 'chore(release)') || github.event_name == 'workflow_dispatch' }} 55 | env: 56 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | package-lock.json 4 | yarn.lock 5 | 6 | # Build output 7 | build/ 8 | dist/ 9 | *.tsbuildinfo 10 | 11 | # IDE and editor files 12 | .idea/ 13 | .vscode/ 14 | *.swp 15 | *.swo 16 | .DS_Store 17 | 18 | # Logs 19 | logs/ 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Test coverage 26 | coverage/ 27 | 28 | # Environment variables 29 | .env 30 | .env.local 31 | .env.*.local 32 | 33 | # Temporary files 34 | *.tmp 35 | *.temp 36 | .cache/ 37 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.1.26](https://github.com/sammcj/mcp-package-docs/compare/v0.1.25...v0.1.26) (2025-04-04) 6 | 7 | ### [0.1.25](https://github.com/sammcj/mcp-package-docs/compare/v0.1.24...v0.1.25) (2025-03-18) 8 | 9 | ### [0.1.24](https://github.com/sammcj/mcp-package-docs/compare/v0.1.23...v0.1.24) (2025-03-15) 10 | 11 | 12 | ### Features 13 | 14 | * initial support for rust ([#11](https://github.com/sammcj/mcp-package-docs/issues/11)) ([b9529c3](https://github.com/sammcj/mcp-package-docs/commit/b9529c397945b657f2fbaa258baa00df934375e4)) 15 | 16 | ### [0.1.23](https://github.com/sammcj/mcp-package-docs/compare/v0.1.21...v0.1.23) (2025-03-15) 17 | 18 | 19 | ### Features 20 | 21 | * swift support ([#10](https://github.com/sammcj/mcp-package-docs/issues/10)) ([ba67733](https://github.com/sammcj/mcp-package-docs/commit/ba67733565fd3dc6e9514d7fcaec6656c3f2aa14)) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * typescript package fixes [#9](https://github.com/sammcj/mcp-package-docs/issues/9) ([f6269c8](https://github.com/sammcj/mcp-package-docs/commit/f6269c822e747c4cdd5cd333e139969104902995)) 27 | 28 | ### [0.1.22](https://github.com/sammcj/mcp-package-docs/compare/v0.1.21...v0.1.22) (2025-03-15) 29 | 30 | 31 | ### Features 32 | 33 | * swift support ([#10](https://github.com/sammcj/mcp-package-docs/issues/10)) ([ba67733](https://github.com/sammcj/mcp-package-docs/commit/ba67733565fd3dc6e9514d7fcaec6656c3f2aa14)) 34 | 35 | ### [0.1.21](https://github.com/sammcj/mcp-package-docs/compare/v0.1.20...v0.1.21) (2025-03-05) 36 | 37 | 38 | ### Bug Fixes 39 | 40 | * docs parsing for npm ([#8](https://github.com/sammcj/mcp-package-docs/issues/8)) ([c9407cf](https://github.com/sammcj/mcp-package-docs/commit/c9407cfba336bbf5270ba219c160547c79066cd8)) 41 | 42 | ### [0.1.20](https://github.com/sammcj/mcp-package-docs/compare/v0.1.19...v0.1.20) (2025-03-02) 43 | 44 | 45 | ### Bug Fixes 46 | 47 | * improve npm docs and lookup ([4e184de](https://github.com/sammcj/mcp-package-docs/commit/4e184de0ee1324f58e71becc192edabedcd095b1)) 48 | 49 | ### [0.1.19](https://github.com/sammcj/mcp-package-docs/compare/v0.1.18...v0.1.19) (2025-03-02) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * improve npm docs and lookup ([aa12065](https://github.com/sammcj/mcp-package-docs/commit/aa12065ff880a1e837993bd10d561fcde7d63191)) 55 | 56 | ### [0.1.18](https://github.com/sammcj/mcp-package-docs/compare/v0.1.17...v0.1.18) (2025-02-26) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * mcp protocol logging ([e256846](https://github.com/sammcj/mcp-package-docs/commit/e25684670e65c0a8261f1c577affea4f4110e992)) 62 | 63 | ### [0.1.17](https://github.com/sammcj/mcp-package-docs/compare/v0.1.16...v0.1.17) (2025-02-26) 64 | 65 | ### 0.1.16 (2025-02-25) 66 | 67 | 68 | ### Features 69 | 70 | * lsp server ([9566c89](https://github.com/sammcj/mcp-package-docs/commit/9566c8907f082a16b299f2b83df239fe2665cf0d)) 71 | 72 | ### [0.1.15](https://github.com/sammcj/mcp-package-docs/compare/v0.1.14...v0.1.15) (2025-02-25) 73 | 74 | 75 | ### Features 76 | 77 | * local docs ([ef80531](https://github.com/sammcj/mcp-package-docs/commit/ef805319774583032d9fe08648ba6697c5766d03)) 78 | 79 | ### [0.1.14](https://github.com/sammcj/mcp-package-docs/compare/v0.1.13...v0.1.14) (2025-02-24) 80 | 81 | 82 | ### Features 83 | 84 | * **npm:** support private npm repos ([8ae7d30](https://github.com/sammcj/mcp-package-docs/commit/8ae7d30d332ba7ebf119ac8e4486051bdef70fd5)) 85 | 86 | ### [0.1.13](https://github.com/sammcj/mcp-package-docs/compare/v0.1.12...v0.1.13) (2025-02-24) 87 | 88 | 89 | ### Features 90 | 91 | * **npm:** support private npm repos ([c695b7c](https://github.com/sammcj/mcp-package-docs/commit/c695b7c60a2edd98bd0bb07b0a26956fde75af46)) 92 | * **npm:** support private npm repos ([a8ad9b4](https://github.com/sammcj/mcp-package-docs/commit/a8ad9b47cd8045d0e55dfd051b1c3c0a64fccb73)) 93 | 94 | ### [0.1.12](https://github.com/sammcj/mcp-package-docs/compare/v0.1.11...v0.1.12) (2025-02-24) 95 | 96 | 97 | ### Features 98 | 99 | * **npm:** support private npm repos ([638b4eb](https://github.com/sammcj/mcp-package-docs/commit/638b4eb48a7cad7d364663087daed90f1cda6e7d)) 100 | 101 | ### [0.1.11](https://github.com/sammcj/mcp-package-docs/compare/v0.1.8...v0.1.11) (2025-02-24) 102 | 103 | ### [0.1.10](https://github.com/sammcj/mcp-package-docs/compare/v0.1.7...v0.1.10) (2025-02-24) 104 | 105 | 106 | ### Features 107 | 108 | * **npm:** support private npm repos ([dc7f01c](https://github.com/sammcj/mcp-package-docs/commit/dc7f01c34540868ab9388c905eea1294c272ee78)) 109 | * **npm:** support private npm repos ([3522008](https://github.com/sammcj/mcp-package-docs/commit/3522008dd0bb7dbae612a879c86305281823577b)) 110 | * **npm:** support private npm repos ([19c002c](https://github.com/sammcj/mcp-package-docs/commit/19c002c3e0e4747059a2a1aac001511ab4f0b664)) 111 | 112 | ### [0.1.9](https://github.com/sammcj/mcp-package-docs/compare/v0.1.7...v0.1.9) (2025-02-24) 113 | 114 | 115 | ### Features 116 | 117 | * **npm:** support private npm repos ([3522008](https://github.com/sammcj/mcp-package-docs/commit/3522008dd0bb7dbae612a879c86305281823577b)) 118 | * **npm:** support private npm repos ([19c002c](https://github.com/sammcj/mcp-package-docs/commit/19c002c3e0e4747059a2a1aac001511ab4f0b664)) 119 | 120 | ### [0.1.8](https://github.com/sammcj/mcp-package-docs/compare/v0.1.7...v0.1.8) (2025-02-24) 121 | 122 | 123 | ### Features 124 | 125 | * **npm:** support private npm repos ([19c002c](https://github.com/sammcj/mcp-package-docs/commit/19c002c3e0e4747059a2a1aac001511ab4f0b664)) 126 | 127 | ### [0.1.7](https://github.com/sammcj/mcp-package-docs/compare/v0.1.6...v0.1.7) (2025-02-24) 128 | 129 | 130 | ### Features 131 | 132 | * **npm:** support private npm repos ([#5](https://github.com/sammcj/mcp-package-docs/issues/5)) ([dd3917b](https://github.com/sammcj/mcp-package-docs/commit/dd3917ba403e9a0acc48ed2619f1f186416c6ab5)) 133 | 134 | ### [0.1.6](https://github.com/sammcj/mcp-package-docs/compare/v0.1.4...v0.1.6) (2025-02-02) 135 | 136 | ### [0.1.4](https://github.com/sammcj/mcp-package-docs/compare/v0.1.1...v0.1.4) (2024-12-29) 137 | 138 | ### [0.1.3](https://github.com/sammcj/mcp-package-docs/compare/v0.1.1...v0.1.3) (2024-12-29) 139 | 140 | ### [0.1.2](https://github.com/sammcj/mcp-package-docs/compare/v0.1.1...v0.1.2) (2024-12-29) 141 | 142 | ### 0.1.1 (2024-12-29) 143 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM node:20-alpine AS builder 3 | 4 | # Set the working directory 5 | WORKDIR /app 6 | 7 | # Copy the package.json and package-lock.json files 8 | COPY package.json package.json 9 | 10 | # Install dependencies 11 | RUN --mount=type=cache,target=/root/.npm npm install --ignore-scripts 12 | 13 | # Copy the rest of the application code 14 | COPY src/ src/ 15 | COPY tsconfig.json tsconfig.json 16 | 17 | # Build the TypeScript code 18 | RUN npm run build 19 | 20 | FROM node:20-alpine AS runner 21 | 22 | WORKDIR /app 23 | 24 | # Copy the build output and package.json to the runner stage 25 | COPY --from=builder /app/build build 26 | COPY --from=builder /app/package.json package.json 27 | COPY --from=builder /app/package-lock.json package-lock.json 28 | 29 | # Install production dependencies 30 | RUN npm ci --ignore-scripts --omit=dev 31 | 32 | # Expose port 3000 (if the server listens on this port) 33 | EXPOSE 3000 34 | 35 | # Command to run the MCP server 36 | ENTRYPOINT ["node", "build/index.js"] 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Sam McLeod 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Package Documentation MCP Server 2 | 3 | An MCP (Model Context Protocol) server that provides LLMs with efficient access to package documentation across multiple programming languages and language server protocol (LSP) capabilities. 4 | 5 | [![smithery badge](https://smithery.ai/badge/mcp-package-docs)](https://smithery.ai/server/mcp-package-docs) 6 | 7 | Package Docs Server MCP server 8 | 9 | ## Features 10 | 11 | - **Multi-Language Support**: 12 | - Go packages via `go doc` 13 | - Python libraries via built-in `help()` 14 | - NPM packages via registry documentation (including private registries) 15 | - Rust crates via crates.io and docs.rs 16 | 17 | - **Smart Documentation Parsing**: 18 | - Structured output with description, usage, and examples 19 | - Focused information to avoid context overload 20 | - Support for specific symbol/function lookups 21 | - Fuzzy and exact search capabilities across documentation 22 | 23 | - **Advanced Search Features**: 24 | - Search within package documentation 25 | - Fuzzy matching for flexible queries 26 | - Context-aware results with relevance scoring 27 | - Symbol extraction from search results 28 | 29 | - **Language Server Protocol (LSP) Support**: 30 | - Hover information for code symbols 31 | - Code completions 32 | - Diagnostics (errors and warnings) 33 | - Currently supports TypeScript/JavaScript 34 | - Extensible for other languages 35 | 36 | - **Performance Optimised**: 37 | - Built-in caching 38 | - Efficient parsing 39 | - Minimal memory footprint 40 | 41 | ## Installation 42 | 43 | ```bash 44 | npx -y mcp-package-docs 45 | ``` 46 | 47 | ### Installing via Smithery 48 | 49 | To install Package Docs for Claude Desktop automatically via [Smithery](https://smithery.ai/server/mcp-package-docs): 50 | 51 | ```bash 52 | npx -y @smithery/cli install mcp-package-docs --client claude 53 | ``` 54 | 55 | ## Usage 56 | 57 | ### As an MCP Server 58 | 59 | 1. Add to your MCP settings configuration: 60 | 61 | ```json 62 | { 63 | "mcpServers": { 64 | "package-docs": { 65 | "command": "npx", 66 | "args": ["-y", "mcp-package-docs"], 67 | "env": { 68 | "ENABLE_LSP": "true" // Optional: Enable Language Server Protocol support 69 | } 70 | } 71 | } 72 | } 73 | ``` 74 | 75 | 2. The LSP functionality includes default configurations for common language servers: 76 | 77 | - TypeScript/JavaScript: `typescript-language-server --stdio` 78 | - HTML: `vscode-html-language-server --stdio` 79 | - CSS: `vscode-css-language-server --stdio` 80 | - JSON: `vscode-json-language-server --stdio` 81 | 82 | You can override these defaults if needed: 83 | 84 | ```json 85 | { 86 | "mcpServers": { 87 | "package-docs": { 88 | "command": "npx", 89 | "args": ["-y", "mcp-package-docs"], 90 | "env": { 91 | "ENABLE_LSP": "true", 92 | "TYPESCRIPT_SERVER": "{\"command\":\"/custom/path/typescript-language-server\",\"args\":[\"--stdio\"]}" 93 | } 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | 3. The server provides the following tools: 100 | 101 | #### lookup_go_doc / describe_go_package 102 | 103 | Fetches Go package documentation 104 | ```typescript 105 | { 106 | "name": "describe_go_package", 107 | "arguments": { 108 | "package": "encoding/json", // required 109 | "symbol": "Marshal" // optional 110 | } 111 | } 112 | ``` 113 | 114 | #### lookup_python_doc / describe_python_package 115 | 116 | Fetches Python package documentation 117 | ```typescript 118 | { 119 | "name": "describe_python_package", 120 | "arguments": { 121 | "package": "requests", // required 122 | "symbol": "get" // optional 123 | } 124 | } 125 | ``` 126 | 127 | #### describe_rust_package 128 | 129 | Fetches Rust crate documentation from crates.io and docs.rs 130 | ```typescript 131 | { 132 | "name": "describe_rust_package", 133 | "arguments": { 134 | "package": "serde", // required: crate name 135 | "version": "1.0.219" // optional: specific version 136 | } 137 | } 138 | ``` 139 | 140 | #### search_package_docs 141 | 142 | Search within package documentation 143 | ```typescript 144 | { 145 | "name": "search_package_docs", 146 | "arguments": { 147 | "package": "requests", // required: package name 148 | "query": "authentication", // required: search query 149 | "language": "python", // required: "go", "python", "npm", "swift", or "rust" 150 | "fuzzy": true // optional: enable fuzzy matching (default: true) 151 | } 152 | } 153 | ``` 154 | 155 | #### lookup_npm_doc / describe_npm_package 156 | 157 | Fetches NPM package documentation from both public and private registries. Automatically uses the appropriate registry based on your .npmrc configuration. 158 | 159 | ```typescript 160 | { 161 | "name": "describe_npm_package", 162 | "arguments": { 163 | "package": "axios", // required - supports both scoped (@org/pkg) and unscoped packages 164 | "version": "1.6.0" // optional 165 | } 166 | } 167 | ``` 168 | 169 | The tool reads your ~/.npmrc file to determine the correct registry for each package: 170 | 171 | - Uses scoped registry configurations (e.g., @mycompany:registry=...) 172 | - Supports private registries (GitHub Packages, GitLab, Nexus, Artifactory, etc.) 173 | - Falls back to the default npm registry if no custom registry is configured 174 | 175 | Example .npmrc configurations: 176 | 177 | ```npmrc 178 | registry=https://nexus.mycompany.com/repository/npm-group/ 179 | @mycompany:registry=https://nexus.mycompany.com/repository/npm-private/ 180 | @mycompany-ct:registry=https://npm.pkg.github.com/ 181 | ``` 182 | 183 | ### Language Server Protocol (LSP) Tools 184 | 185 | When LSP support is enabled, the following additional tools become available: 186 | 187 | #### get_hover 188 | 189 | Get hover information for a position in a document 190 | ```typescript 191 | { 192 | "name": "get_hover", 193 | "arguments": { 194 | "languageId": "typescript", // required: language identifier (e.g., "typescript", "javascript") 195 | "filePath": "src/index.ts", // required: path to the source file 196 | "content": "const x = 1;", // required: content of the file 197 | "line": 0, // required: zero-based line number 198 | "character": 6, // required: zero-based character position 199 | "projectRoot": "/path/to/project" // optional: project root directory 200 | } 201 | } 202 | ``` 203 | 204 | #### get_completions 205 | 206 | Get completion suggestions for a position in a document 207 | ```typescript 208 | { 209 | "name": "get_completions", 210 | "arguments": { 211 | "languageId": "typescript", // required: language identifier 212 | "filePath": "src/index.ts", // required: path to the source file 213 | "content": "const arr = []; arr.", // required: content of the file 214 | "line": 0, // required: zero-based line number 215 | "character": 16, // required: zero-based character position 216 | "projectRoot": "/path/to/project" // optional: project root directory 217 | } 218 | } 219 | ``` 220 | 221 | #### get_diagnostics 222 | 223 | Get diagnostic information (errors, warnings) for a document 224 | ```typescript 225 | { 226 | "name": "get_diagnostics", 227 | "arguments": { 228 | "languageId": "typescript", // required: language identifier 229 | "filePath": "src/index.ts", // required: path to the source file 230 | "content": "const x: string = 1;", // required: content of the file 231 | "projectRoot": "/path/to/project" // optional: project root directory 232 | } 233 | } 234 | ``` 235 | 236 | ### Example Usage in an LLM 237 | 238 | #### Looking up Documentation 239 | 240 | ```typescript 241 | // Looking up Go documentation 242 | const goDocResult = await use_mcp_tool({ 243 | server_name: "package-docs", 244 | tool_name: "describe_go_package", 245 | arguments: { 246 | package: "encoding/json", 247 | symbol: "Marshal" 248 | } 249 | }); 250 | 251 | // Looking up Python documentation 252 | const pythonDocResult = await use_mcp_tool({ 253 | server_name: "package-docs", 254 | tool_name: "describe_python_package", 255 | arguments: { 256 | package: "requests", 257 | symbol: "post" 258 | } 259 | }); 260 | 261 | // Looking up Rust documentation 262 | const rustDocResult = await use_mcp_tool({ 263 | server_name: "package-docs", 264 | tool_name: "describe_rust_package", 265 | arguments: { 266 | package: "serde" 267 | } 268 | }); 269 | 270 | // Searching within documentation 271 | const searchResult = await use_mcp_tool({ 272 | server_name: "package-docs", 273 | tool_name: "search_package_docs", 274 | arguments: { 275 | package: "serde", 276 | query: "serialize", 277 | language: "rust", 278 | fuzzy: true 279 | } 280 | }); 281 | 282 | // Using LSP for hover information (when LSP is enabled) 283 | const hoverResult = await use_mcp_tool({ 284 | server_name: "package-docs", 285 | tool_name: "get_hover", 286 | arguments: { 287 | languageId: "typescript", 288 | filePath: "src/index.ts", 289 | content: "const axios = require('axios');\naxios.get", 290 | line: 1, 291 | character: 7 292 | } 293 | }); 294 | ``` 295 | 296 | ## Requirements 297 | 298 | - Node.js >= 20 299 | - Go (for Go package documentation) 300 | - Python 3 (for Python package documentation) 301 | - Internet connection (for NPM package documentation and Rust crate documentation) 302 | - Language servers (for LSP functionality): 303 | - TypeScript/JavaScript: `npm install -g typescript-language-server typescript` 304 | - HTML/CSS/JSON: `npm install -g vscode-langservers-extracted` 305 | 306 | ## Development 307 | 308 | ```bash 309 | # Install dependencies 310 | npm i 311 | 312 | # Build 313 | npm run build 314 | 315 | # Watch mode 316 | npm run watch 317 | ``` 318 | 319 | ## Contributing 320 | 321 | 1. Fork the repository 322 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 323 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 324 | 4. Push to the branch (`git push origin feature/amazing-feature`) 325 | 5. Open a Pull Request 326 | 327 | ## License 328 | 329 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 330 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | import tseslint from "typescript-eslint"; 4 | 5 | 6 | /** @type {import('eslint').Linter.Config[]} */ 7 | export default [ 8 | {files: ["**/*.{js,mjs,cjs,ts}"]}, 9 | {languageOptions: { globals: globals.browser }}, 10 | pluginJs.configs.recommended, 11 | ...tseslint.configs.recommended, 12 | ]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-package-docs", 3 | "version": "0.1.26", 4 | "description": "An MCP server that provides LLMs with efficient access to package documentation across multiple programming languages", 5 | "main": "build/index.js", 6 | "type": "module", 7 | "bin": { 8 | "mcp-package-docs": "build/index.js" 9 | }, 10 | "files": [ 11 | "build", 12 | "README.md", 13 | "LICENSE" 14 | ], 15 | "scripts": { 16 | "build": "tsc && chmod +x build/index.js", 17 | "watch": "tsc -w", 18 | "serve": "node build/index.js", 19 | "format": "prettier --write \"src/**/*.ts\"", 20 | "inspector": "npx @modelcontextprotocol/inspector build/index.js", 21 | "bump": "npx -y standard-version --skip.tag && git add . ; git commit -m 'chore: bump version' ; git push", 22 | "prepublishOnly": "npm run build", 23 | "test": "node test-npm-docs.js" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/sammcj/mcp-package-docs.git" 28 | }, 29 | "keywords": [ 30 | "mcp", 31 | "documentation", 32 | "llm", 33 | "ai", 34 | "package", 35 | "docs", 36 | "go", 37 | "python", 38 | "npm", 39 | "sammcj", 40 | "smcleod" 41 | ], 42 | "author": { 43 | "name": "Sam McLeod", 44 | "url": "https://smcleod.net" 45 | }, 46 | "license": "MIT", 47 | "bugs": { 48 | "url": "https://github.com/sammcj/mcp-package-docs/issues" 49 | }, 50 | "homepage": "https://github.com/sammcj/mcp-package-docs#readme", 51 | "dependencies": { 52 | "@modelcontextprotocol/sdk": "^1.7.0", 53 | "axios": "^1.8.3", 54 | "fuse.js": "7.1.0", 55 | "node-html-markdown": "1.3.0", 56 | "typescript": "5.8.2", 57 | "typescript-language-server": "^4.3.4", 58 | "vscode-languageserver-protocol": "^3.17.5" 59 | }, 60 | "devDependencies": { 61 | "@eslint/js": "10.0.0", 62 | "@types/node": "^22.13.10", 63 | "@types/turndown": "5.0.5", 64 | "@typescript-eslint/eslint-plugin": "8.26.1", 65 | "@typescript-eslint/parser": "8.26.1", 66 | "eslint": "9.22.0", 67 | "globals": "16.0.0", 68 | "prettier": "^3.5.3", 69 | "typescript-eslint": "8.26.1" 70 | }, 71 | "engines": { 72 | "node": ">=20" 73 | }, 74 | "publishConfig": { 75 | "access": "public" 76 | }, 77 | "packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6" 78 | } 79 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: [] 9 | properties: {} 10 | commandFunction: 11 | # A function that produces the CLI command to start the MCP on stdio. 12 | |- 13 | (config) => ({command:'node', args:['build/index.js']}) 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { logger } from './logger.js' 4 | import { PackageDocsServer } from './package-docs-server.js'; 5 | 6 | // Initialise and run the server 7 | async function main() { 8 | try { 9 | const server = new PackageDocsServer(); 10 | const transport = new StdioServerTransport(); 11 | await server.connect(transport); 12 | logger.debug("Package docs MCP server running on stdio"); 13 | } catch (error) { 14 | logger.error("Failed to start server:", error); 15 | process.exit(1); 16 | } 17 | } 18 | 19 | main().catch((error) => { 20 | logger.error("Unhandled error:", error); 21 | process.exit(1); 22 | }); 23 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | // Simple MCP-compliant logger 2 | // Ensures stdout is kept clean for JSON-RPC messages by routing all logs to stderr 3 | // This follows the pattern used by other CLI tools that need to maintain clean stdout 4 | export class McpLogger { 5 | private prefix: string; 6 | private silent: boolean; 7 | 8 | constructor(prefix: string = '', silent: boolean = false) { 9 | this.prefix = prefix ? `[${prefix}] ` : ''; 10 | this.silent = silent || process.env.MCP_SILENT_LOGS === 'true'; 11 | } 12 | 13 | info(...args: unknown[]): void { 14 | if (!this.silent) { 15 | console.error(`${this.prefix}INFO:`, ...args) 16 | } 17 | } 18 | 19 | debug(...args: unknown[]): void { 20 | if (!this.silent) { 21 | console.error(`${this.prefix}DEBUG:`, ...args) 22 | } 23 | } 24 | 25 | warn(...args: unknown[]): void { 26 | if (!this.silent) { 27 | console.error(`${this.prefix}WARN:`, ...args) 28 | } 29 | } 30 | 31 | error(...args: unknown[]): void { 32 | // Always log errors, even in silent mode 33 | console.error(`${this.prefix}ERROR:`, ...args); 34 | } 35 | 36 | // Create a child logger with a new prefix 37 | child(prefix: string): McpLogger { 38 | return new McpLogger(prefix, this.silent) 39 | } 40 | 41 | // Set silent mode 42 | setSilent(silent: boolean): void { 43 | this.silent = silent; 44 | } 45 | } 46 | 47 | // Check if we're running as an MCP server 48 | const isMcpServer = process.env.MCP_SERVER != 'false' 49 | 50 | // Create root logger - silent by default when running as MCP server 51 | export const logger = new McpLogger('MCP', isMcpServer) 52 | -------------------------------------------------------------------------------- /src/lsp/typescript-lsp-client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createMessageConnection, 3 | MessageConnection, 4 | StreamMessageReader, 5 | StreamMessageWriter, 6 | CompletionItem, 7 | CompletionParams, 8 | DidOpenTextDocumentParams, 9 | Hover, 10 | InitializeParams, 11 | PublishDiagnosticsParams, 12 | TextDocumentIdentifier, 13 | TextDocumentItem, 14 | } from 'vscode-languageserver-protocol/node.js'; 15 | import { logger, McpLogger } from '../logger.js'; 16 | import * as childProcess from 'child_process'; 17 | import { dirname, join, isAbsolute } from 'path'; 18 | import { mkdirSync, existsSync } from 'fs'; 19 | import { promisify } from 'util'; 20 | 21 | export interface LanguageServerConfig { 22 | command: string; 23 | args: string[]; 24 | } 25 | 26 | // Helper function to check if a command exists 27 | async function commandExists(command: string): Promise { 28 | try { 29 | const exec = promisify(childProcess.exec); 30 | const platform = process.platform; 31 | 32 | if (platform === 'win32') { 33 | // Windows 34 | await exec(`where ${command}`); 35 | } else { 36 | // Unix-like (macOS, Linux) 37 | await exec(`which ${command}`); 38 | } 39 | return true; 40 | } catch (error) { 41 | return false; 42 | } 43 | } 44 | 45 | // Helper function to install a package using npm 46 | async function installPackage(packageName: string): Promise { 47 | try { 48 | logger.debug(`Installing ${packageName}...`); 49 | const exec = promisify(childProcess.exec); 50 | await exec(`npm install --no-save ${packageName}`); 51 | logger.debug(`Successfully installed ${packageName}`); 52 | } catch (error) { 53 | logger.error(`Failed to install ${packageName}:`, error); 54 | throw error; 55 | } 56 | } 57 | 58 | export interface LanguageServerInstance { 59 | connection: MessageConnection; 60 | process: ReturnType; 61 | workspaceRoot: string; 62 | } 63 | 64 | export class TypeScriptLspClient { 65 | private languageServers: Map; 66 | private diagnosticsListeners: Map void)[]>; 67 | private logger: McpLogger; 68 | 69 | constructor() { 70 | this.languageServers = new Map(); 71 | this.diagnosticsListeners = new Map(); 72 | this.logger = logger.child('LSP'); 73 | } 74 | 75 | private getServerKey(languageId: string, projectRoot?: string): string { 76 | return `${languageId}:${projectRoot || 'default'}`; 77 | } 78 | 79 | private async getLanguageServerConfig(languageId: string): Promise { 80 | this.logger.debug(`Getting config for ${languageId}`); 81 | 82 | // Default configurations for common language servers 83 | const defaultConfigs: Record = { 84 | typescript: { 85 | command: 'typescript-language-server', 86 | args: ['--stdio'] 87 | }, 88 | javascript: { 89 | command: 'typescript-language-server', 90 | args: ['--stdio'] 91 | }, 92 | html: { 93 | command: 'vscode-html-language-server', 94 | args: ['--stdio'] 95 | }, 96 | css: { 97 | command: 'vscode-css-language-server', 98 | args: ['--stdio'] 99 | }, 100 | json: { 101 | command: 'vscode-json-language-server', 102 | args: ['--stdio'] 103 | } 104 | }; 105 | 106 | // Check if there's an environment variable override 107 | const configStr = process.env[`${languageId.toUpperCase()}_SERVER`]; 108 | this.logger.debug(`Raw config for ${languageId}:`, configStr); 109 | 110 | if (configStr) { 111 | try { 112 | // Try to parse the environment variable configuration 113 | const config = JSON.parse(configStr); 114 | this.logger.debug(`Using custom config for ${languageId}:`, config); 115 | return config; 116 | } catch (error) { 117 | if (error instanceof Error) { 118 | this.logger.error(`Invalid config for ${languageId}:`, error.message); 119 | } else { 120 | this.logger.error(`Invalid config for ${languageId}:`, error); 121 | } 122 | // Fall back to default if parsing fails 123 | this.logger.debug(`Falling back to default config for ${languageId}`); 124 | } 125 | } 126 | 127 | // Get default config 128 | const defaultConfig = defaultConfigs[languageId.toLowerCase()]; 129 | if (!defaultConfig) { 130 | this.logger.debug(`No config found for ${languageId}`); 131 | return undefined; 132 | } 133 | 134 | // Check if the command exists 135 | const commandName = defaultConfig.command; 136 | const commandAvailable = await commandExists(commandName); 137 | 138 | if (!commandAvailable) { 139 | this.logger.debug(`Command ${commandName} not found, attempting to install...`); 140 | 141 | // Map language server commands to npm packages 142 | const packageMap: Record = { 143 | 'typescript-language-server': 'typescript-language-server typescript', 144 | 'vscode-html-language-server': 'vscode-langservers-extracted', 145 | 'vscode-css-language-server': 'vscode-langservers-extracted', 146 | 'vscode-json-language-server': 'vscode-langservers-extracted' 147 | }; 148 | 149 | const packageToInstall = packageMap[commandName]; 150 | if (packageToInstall) { 151 | try { 152 | await installPackage(packageToInstall); 153 | this.logger.debug(`Successfully installed ${packageToInstall}`); 154 | 155 | // For locally installed packages, use npx to run them 156 | return { 157 | command: 'npx', 158 | args: [commandName, ...defaultConfig.args] 159 | }; 160 | } catch (error) { 161 | this.logger.error(`Failed to install ${packageToInstall}:`, error); 162 | } 163 | } 164 | } 165 | 166 | this.logger.debug(`Using default config for ${languageId}`); 167 | return defaultConfig; 168 | } 169 | 170 | public async getOrCreateServer(languageId: string, projectRoot?: string): Promise { 171 | const serverKey = this.getServerKey(languageId, projectRoot); 172 | this.logger.debug(`Request for ${serverKey}`); 173 | 174 | if (this.languageServers.has(serverKey)) { 175 | this.logger.debug(`Returning existing ${serverKey} server`); 176 | return this.languageServers.get(serverKey)!; 177 | } 178 | 179 | const config = await this.getLanguageServerConfig(languageId); 180 | if (!config) { 181 | throw new Error(`No language server configured for ${languageId}`); 182 | } 183 | 184 | this.logger.debug(`Spawning ${serverKey} server:`, config); 185 | const serverProcess = childProcess.spawn(config.command, config.args); 186 | 187 | serverProcess.on('error', (error) => { 188 | this.logger.error(`[${serverKey} process] Error:`, error); 189 | }); 190 | 191 | serverProcess.stderr.on('data', (data) => { 192 | this.logger.error(`[${serverKey} stderr]`, data.toString()); 193 | }); 194 | 195 | // Create message connection 196 | this.logger.debug(`Creating message connection for ${serverKey}`); 197 | const connection = createMessageConnection( 198 | new StreamMessageReader(serverProcess.stdout), 199 | new StreamMessageWriter(serverProcess.stdin) 200 | ); 201 | 202 | // Debug logging for messages 203 | connection.onNotification((method, params) => { 204 | this.logger.debug(`[${serverKey}] Notification received:`, method, params); 205 | }); 206 | 207 | connection.onRequest((method, params) => { 208 | this.logger.debug(`[${serverKey}] Request received:`, method, params); 209 | }); 210 | 211 | // If projectRoot is not provided, default to current working directory 212 | const actualRoot = projectRoot && existsSync(projectRoot) ? projectRoot : process.cwd(); 213 | 214 | // Initialize connection 215 | this.logger.debug(`Starting connection for ${serverKey}`); 216 | connection.listen(); 217 | 218 | // Initialize language server 219 | this.logger.debug(`Initializing ${serverKey} server`); 220 | try { 221 | const initializeResult = await connection.sendRequest('initialize', { 222 | processId: process.pid, 223 | rootUri: `file://${actualRoot}`, 224 | workspaceFolders: [{ 225 | uri: `file://${actualRoot}`, 226 | name: `${languageId}-workspace` 227 | }], 228 | capabilities: { 229 | workspace: { 230 | configuration: true, 231 | didChangeConfiguration: { dynamicRegistration: true }, 232 | workspaceFolders: true, 233 | didChangeWatchedFiles: { dynamicRegistration: true }, 234 | }, 235 | textDocument: { 236 | synchronization: { 237 | dynamicRegistration: true, 238 | willSave: true, 239 | willSaveWaitUntil: true, 240 | didSave: true 241 | }, 242 | completion: { 243 | dynamicRegistration: true, 244 | completionItem: { 245 | snippetSupport: true, 246 | commitCharactersSupport: true, 247 | documentationFormat: ['markdown', 'plaintext'], 248 | deprecatedSupport: true, 249 | preselectSupport: true 250 | }, 251 | contextSupport: true 252 | }, 253 | hover: { 254 | dynamicRegistration: true, 255 | contentFormat: ['markdown', 'plaintext'] 256 | }, 257 | signatureHelp: { 258 | dynamicRegistration: true, 259 | signatureInformation: { 260 | documentationFormat: ['markdown', 'plaintext'] 261 | } 262 | }, 263 | declaration: { dynamicRegistration: true, linkSupport: true }, 264 | definition: { dynamicRegistration: true, linkSupport: true }, 265 | typeDefinition: { dynamicRegistration: true, linkSupport: true }, 266 | implementation: { dynamicRegistration: true, linkSupport: true }, 267 | references: { dynamicRegistration: true }, 268 | documentHighlight: { dynamicRegistration: true }, 269 | documentSymbol: { dynamicRegistration: true, hierarchicalDocumentSymbolSupport: true }, 270 | codeAction: { 271 | dynamicRegistration: true, 272 | codeActionLiteralSupport: { 273 | codeActionKind: { valueSet: [] } 274 | } 275 | }, 276 | codeLens: { dynamicRegistration: true }, 277 | formatting: { dynamicRegistration: true }, 278 | rangeFormatting: { dynamicRegistration: true }, 279 | onTypeFormatting: { dynamicRegistration: true }, 280 | rename: { dynamicRegistration: true }, 281 | documentLink: { dynamicRegistration: true }, 282 | colorProvider: { dynamicRegistration: true }, 283 | foldingRange: { dynamicRegistration: true }, 284 | publishDiagnostics: { 285 | relatedInformation: true, 286 | tagSupport: { valueSet: [1, 2] }, 287 | versionSupport: true 288 | } 289 | } 290 | }, 291 | initializationOptions: null, 292 | } as InitializeParams); 293 | 294 | this.logger.debug(`Initialize result for ${serverKey}:`, initializeResult); 295 | await connection.sendNotification('initialized'); 296 | this.logger.debug(`Sent initialized notification for ${serverKey}`); 297 | 298 | // Optional: send workspace configuration changes if needed 299 | if (languageId === 'typescript') { 300 | await connection.sendNotification('workspace/didChangeConfiguration', { 301 | settings: { 302 | typescript: { 303 | format: { 304 | enable: true 305 | }, 306 | suggest: { 307 | enabled: true, 308 | includeCompletionsForModuleExports: true 309 | }, 310 | validate: { 311 | enable: true 312 | } 313 | } 314 | } 315 | }); 316 | } 317 | } catch (error) { 318 | this.logger.error(`Failed to initialize ${serverKey} server:`, error); 319 | throw error; 320 | } 321 | 322 | // Set up diagnostics handler 323 | connection.onNotification( 324 | 'textDocument/publishDiagnostics', 325 | (params: PublishDiagnosticsParams) => { 326 | this.logger.debug(`[${serverKey}] Received diagnostics:`, params); 327 | const listeners = this.diagnosticsListeners.get(params.uri) || []; 328 | listeners.forEach(listener => listener(params)); 329 | } 330 | ); 331 | 332 | const server = { connection, process: serverProcess, workspaceRoot: actualRoot }; 333 | this.languageServers.set(serverKey, server); 334 | this.logger.debug(`Successfully created ${serverKey} server`); 335 | return server; 336 | } 337 | 338 | public async getHover( 339 | languageId: string, 340 | filePath: string, 341 | content: string, 342 | line: number, 343 | character: number, 344 | projectRoot?: string 345 | ): Promise { 346 | this.logger.debug(`Processing hover request for ${languageId}`); 347 | 348 | const server = await this.getOrCreateServer(languageId, projectRoot); 349 | const actualRoot = server.workspaceRoot; 350 | 351 | const absolutePath = isAbsolute(filePath) ? filePath : join(actualRoot, filePath); 352 | const uri = `file://${absolutePath}`; 353 | 354 | // Ensure directory exists (for languages that may require file presence) 355 | const dir = dirname(absolutePath); 356 | if (!existsSync(dir)) { 357 | mkdirSync(dir, { recursive: true }); 358 | } 359 | 360 | const textDocument: TextDocumentItem = { 361 | uri, 362 | languageId, 363 | version: 1, 364 | text: content, 365 | }; 366 | 367 | this.logger.debug(`Sending document to server:`, textDocument); 368 | await server.connection.sendNotification('textDocument/didOpen', { 369 | textDocument, 370 | } as DidOpenTextDocumentParams); 371 | 372 | try { 373 | this.logger.debug(`Requesting hover information`); 374 | const hover: Hover = await server.connection.sendRequest('textDocument/hover', { 375 | textDocument: { uri } as TextDocumentIdentifier, 376 | position: { line, character }, 377 | }); 378 | 379 | this.logger.debug(`Received hover response:`, hover); 380 | return hover; 381 | } catch (error) { 382 | this.logger.error('Hover request failed:', error); 383 | throw error; 384 | } 385 | } 386 | 387 | public async getCompletions( 388 | languageId: string, 389 | filePath: string, 390 | content: string, 391 | line: number, 392 | character: number, 393 | projectRoot?: string 394 | ): Promise { 395 | this.logger.debug(`Processing completions request for ${languageId}`); 396 | 397 | const server = await this.getOrCreateServer(languageId, projectRoot); 398 | const actualRoot = server.workspaceRoot; 399 | 400 | const absolutePath = isAbsolute(filePath) ? filePath : join(actualRoot, filePath); 401 | const uri = `file://${absolutePath}`; 402 | 403 | // Ensure directory exists 404 | const dir = dirname(absolutePath); 405 | if (!existsSync(dir)) { 406 | mkdirSync(dir, { recursive: true }); 407 | } 408 | 409 | const textDocument: TextDocumentItem = { 410 | uri, 411 | languageId, 412 | version: 1, 413 | text: content, 414 | }; 415 | 416 | this.logger.debug(`Sending document to server:`, textDocument); 417 | await server.connection.sendNotification('textDocument/didOpen', { 418 | textDocument, 419 | } as DidOpenTextDocumentParams); 420 | 421 | try { 422 | this.logger.debug(`Requesting completions`); 423 | const completionParams: CompletionParams = { 424 | textDocument: { uri }, 425 | position: { line, character }, 426 | }; 427 | 428 | const completions: CompletionItem[] | null = await server.connection.sendRequest( 429 | 'textDocument/completion', 430 | completionParams 431 | ); 432 | 433 | this.logger.debug(`Received completions:`, completions); 434 | return completions || []; 435 | } catch (error) { 436 | this.logger.error('Completions request failed:', error); 437 | throw error; 438 | } 439 | } 440 | 441 | public async getDiagnostics( 442 | languageId: string, 443 | filePath: string, 444 | content: string, 445 | projectRoot?: string 446 | ): Promise { 447 | this.logger.debug(`Processing diagnostics request for ${languageId}`); 448 | 449 | const server = await this.getOrCreateServer(languageId, projectRoot); 450 | const actualRoot = server.workspaceRoot; 451 | 452 | const absolutePath = isAbsolute(filePath) ? filePath : join(actualRoot, filePath); 453 | const uri = `file://${absolutePath}`; 454 | 455 | // Ensure directory exists 456 | const fileDir = dirname(absolutePath); 457 | if (!existsSync(fileDir)) { 458 | mkdirSync(fileDir, { recursive: true }); 459 | } 460 | 461 | const textDocument: TextDocumentItem = { 462 | uri, 463 | languageId, 464 | version: 1, 465 | text: content, 466 | }; 467 | 468 | this.logger.debug(`Setting up diagnostics listener for ${uri}`); 469 | return new Promise((resolve, reject) => { 470 | const listeners = this.diagnosticsListeners.get(uri) || []; 471 | const listener = (params: PublishDiagnosticsParams) => { 472 | this.logger.debug(`Received diagnostics for ${uri}:`, params); 473 | resolve(params.diagnostics); 474 | 475 | // Remove listener after receiving diagnostics 476 | const index = listeners.indexOf(listener); 477 | if (index !== -1) { 478 | listeners.splice(index, 1); 479 | } 480 | }; 481 | listeners.push(listener); 482 | this.diagnosticsListeners.set(uri, listeners); 483 | 484 | // Send document to trigger diagnostics 485 | this.logger.debug(`Sending document to server:`, textDocument); 486 | server.connection.sendNotification('textDocument/didOpen', { 487 | textDocument, 488 | } as DidOpenTextDocumentParams); 489 | 490 | // Set timeout 491 | setTimeout(() => { 492 | this.logger.debug(`Timeout reached for ${uri}`); 493 | const index = listeners.indexOf(listener); 494 | if (index !== -1) { 495 | listeners.splice(index, 1); 496 | resolve([]); 497 | } 498 | }, 2000); 499 | }); 500 | } 501 | 502 | public cleanup(): void { 503 | this.logger.debug('Disposing language servers...'); 504 | for (const [id, server] of this.languageServers.entries()) { 505 | this.logger.debug(`Disposing ${id} server...`); 506 | server.connection.dispose(); 507 | server.process.kill(); 508 | } 509 | this.languageServers.clear(); 510 | this.diagnosticsListeners.clear(); 511 | } 512 | } 513 | 514 | export default TypeScriptLspClient; 515 | -------------------------------------------------------------------------------- /src/npm-docs-enhancer.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as ts from 'typescript'; 3 | import { NodeHtmlMarkdown } from 'node-html-markdown'; 4 | import { McpLogger } from './logger.js'; 5 | 6 | // Initialize HTML to Markdown converter with custom options 7 | const nhm = new NodeHtmlMarkdown({ 8 | useInlineLinks: true, 9 | maxConsecutiveNewlines: 2, 10 | bulletMarker: '-', 11 | codeBlockStyle: 'fenced', 12 | emDelimiter: '*', 13 | strongDelimiter: '**', 14 | keepDataImages: false 15 | }); 16 | 17 | // Interface for structured API documentation 18 | export interface ApiDocumentation { 19 | name: string; 20 | description?: string; 21 | type: 'function' | 'class' | 'interface' | 'type' | 'variable' | 'namespace' | 'enum' | 'unknown'; 22 | signature?: string; 23 | parameters?: ApiParameter[]; 24 | returnType?: string; 25 | examples?: string[]; 26 | properties?: ApiProperty[]; 27 | methods?: ApiMethod[]; 28 | typeDefinition?: string; 29 | isExported: boolean; 30 | } 31 | 32 | export interface ApiParameter { 33 | name: string; 34 | type?: string; 35 | description?: string; 36 | optional?: boolean; 37 | defaultValue?: string; 38 | } 39 | 40 | export interface ApiProperty { 41 | name: string; 42 | type?: string; 43 | description?: string; 44 | optional?: boolean; 45 | } 46 | 47 | export interface ApiMethod { 48 | name: string; 49 | description?: string; 50 | signature?: string; 51 | parameters?: ApiParameter[]; 52 | returnType?: string; 53 | } 54 | 55 | // Interface for package API documentation 56 | export interface PackageApiDocumentation { 57 | packageName: string; 58 | version?: string; 59 | description?: string; 60 | mainExport?: string; 61 | exports: ApiDocumentation[]; 62 | types: ApiDocumentation[]; 63 | examples?: string[]; 64 | } 65 | 66 | export interface DocResult { 67 | description?: string; 68 | usage?: string; 69 | example?: string; 70 | error?: string; 71 | apiDocumentation?: PackageApiDocumentation; 72 | } 73 | 74 | export class NpmDocsEnhancer { 75 | private logger: McpLogger; 76 | 77 | constructor(logger: McpLogger) { 78 | this.logger = logger.child('NpmDocsEnhancer'); 79 | } 80 | 81 | /** 82 | * Convert HTML content to Markdown 83 | */ 84 | public convertHtmlToMarkdown(html: string): string { 85 | try { 86 | // Check if the content is HTML by looking for common HTML tags 87 | const hasHtmlTags = /<\/?[a-z][\s\S]*>/i.test(html); 88 | if (!hasHtmlTags) { 89 | return html; // Return as-is if it doesn't appear to be HTML 90 | } 91 | 92 | // Convert HTML to Markdown 93 | return nhm.translate(html); 94 | } catch (error) { 95 | this.logger.error('Error converting HTML to Markdown:', error); 96 | return html; // Return original content if conversion fails 97 | } 98 | } 99 | 100 | /** 101 | * Extract structured API documentation from TypeScript definition files 102 | */ 103 | public async extractApiDocumentation( 104 | packageName: string, 105 | typesContent: string, 106 | ): Promise { 107 | this.logger.debug(`Extracting API documentation for ${packageName}`); 108 | 109 | const result: PackageApiDocumentation = { 110 | packageName, 111 | exports: [], 112 | types: [] 113 | }; 114 | 115 | try { 116 | // Create a virtual TypeScript program to analyze the .d.ts file 117 | const fileName = `${packageName}.d.ts`; 118 | const sourceFile = ts.createSourceFile( 119 | fileName, 120 | typesContent, 121 | ts.ScriptTarget.Latest, 122 | true 123 | ); 124 | 125 | // Extract exported declarations 126 | this.extractDeclarations(sourceFile, result); 127 | 128 | return result; 129 | } catch (error) { 130 | this.logger.error(`Error extracting API documentation for ${packageName}:`, error); 131 | return result; 132 | } 133 | } 134 | 135 | /** 136 | * Extract declarations from TypeScript source file 137 | */ 138 | private extractDeclarations(sourceFile: ts.SourceFile, result: PackageApiDocumentation): void { 139 | // Visit each node in the source file 140 | ts.forEachChild(sourceFile, (node) => { 141 | // Check if the node is exported 142 | const isExported = this.isNodeExported(node); 143 | 144 | if (ts.isFunctionDeclaration(node)) { 145 | // Extract function declaration 146 | const funcDoc = this.extractFunctionDeclaration(node, isExported); 147 | if (funcDoc) { 148 | if (isExported) { 149 | result.exports.push(funcDoc); 150 | } else { 151 | result.types.push(funcDoc); 152 | } 153 | } 154 | } else if (ts.isClassDeclaration(node)) { 155 | // Extract class declaration 156 | const classDoc = this.extractClassDeclaration(node, isExported); 157 | if (classDoc) { 158 | if (isExported) { 159 | result.exports.push(classDoc); 160 | } else { 161 | result.types.push(classDoc); 162 | } 163 | } 164 | } else if (ts.isInterfaceDeclaration(node)) { 165 | // Extract interface declaration 166 | const interfaceDoc = this.extractInterfaceDeclaration(node, isExported); 167 | if (interfaceDoc) { 168 | result.types.push(interfaceDoc); 169 | } 170 | } else if (ts.isTypeAliasDeclaration(node)) { 171 | // Extract type alias declaration 172 | const typeDoc = this.extractTypeAliasDeclaration(node, isExported); 173 | if (typeDoc) { 174 | result.types.push(typeDoc); 175 | } 176 | } else if (ts.isVariableStatement(node)) { 177 | // Extract variable declarations 178 | const varDocs = this.extractVariableDeclarations(node, isExported); 179 | if (varDocs.length > 0) { 180 | if (isExported) { 181 | result.exports.push(...varDocs); 182 | } else { 183 | result.types.push(...varDocs); 184 | } 185 | } 186 | } else if (ts.isModuleDeclaration(node)) { 187 | // Extract namespace/module declarations 188 | const namespaceDoc = this.extractNamespaceDeclaration(node, isExported); 189 | if (namespaceDoc) { 190 | if (isExported) { 191 | result.exports.push(namespaceDoc); 192 | } else { 193 | result.types.push(namespaceDoc); 194 | } 195 | } 196 | } else if (ts.isEnumDeclaration(node)) { 197 | // Extract enum declarations 198 | const enumDoc = this.extractEnumDeclaration(node, isExported); 199 | if (enumDoc) { 200 | if (isExported) { 201 | result.exports.push(enumDoc); 202 | } else { 203 | result.types.push(enumDoc); 204 | } 205 | } 206 | } 207 | }); 208 | } 209 | 210 | /** 211 | * Check if a node is exported 212 | */ 213 | private isNodeExported(node: ts.Node): boolean { 214 | return ( 215 | (ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0 || 216 | (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) 217 | ); 218 | } 219 | 220 | /** 221 | * Extract function declaration 222 | */ 223 | private extractFunctionDeclaration(node: ts.FunctionDeclaration, isExported: boolean): ApiDocumentation | undefined { 224 | if (!node.name) return undefined; 225 | 226 | const name = node.name.text; 227 | const signature = node.getText().split('{')[0].trim(); 228 | const parameters = node.parameters.map(param => this.extractParameter(param)); 229 | const returnType = node.type ? node.type.getText() : 'any'; 230 | 231 | // Extract JSDoc comments if available 232 | const description = this.extractJSDocComment(node); 233 | 234 | return { 235 | name, 236 | description, 237 | type: 'function', 238 | signature, 239 | parameters, 240 | returnType, 241 | isExported 242 | }; 243 | } 244 | 245 | /** 246 | * Extract class declaration 247 | */ 248 | private extractClassDeclaration(node: ts.ClassDeclaration, isExported: boolean): ApiDocumentation | undefined { 249 | if (!node.name) return undefined; 250 | 251 | const name = node.name.text; 252 | const description = this.extractJSDocComment(node); 253 | const methods: ApiMethod[] = []; 254 | const properties: ApiProperty[] = []; 255 | 256 | // Extract methods and properties 257 | node.members.forEach(member => { 258 | if (ts.isMethodDeclaration(member)) { 259 | if (member.name) { 260 | const methodName = member.name.getText(); 261 | const methodSignature = member.getText().split('{')[0].trim(); 262 | const methodParams = member.parameters.map(param => this.extractParameter(param)); 263 | const methodReturnType = member.type ? member.type.getText() : 'any'; 264 | const methodDescription = this.extractJSDocComment(member); 265 | 266 | methods.push({ 267 | name: methodName, 268 | description: methodDescription, 269 | signature: methodSignature, 270 | parameters: methodParams, 271 | returnType: methodReturnType 272 | }); 273 | } 274 | } else if (ts.isPropertyDeclaration(member)) { 275 | if (member.name) { 276 | const propName = member.name.getText(); 277 | const propType = member.type ? member.type.getText() : 'any'; 278 | const propDescription = this.extractJSDocComment(member); 279 | const optional = member.questionToken !== undefined; 280 | 281 | properties.push({ 282 | name: propName, 283 | type: propType, 284 | description: propDescription, 285 | optional 286 | }); 287 | } 288 | } 289 | }); 290 | 291 | return { 292 | name, 293 | description, 294 | type: 'class', 295 | methods, 296 | properties, 297 | isExported 298 | }; 299 | } 300 | 301 | /** 302 | * Extract interface declaration 303 | */ 304 | private extractInterfaceDeclaration(node: ts.InterfaceDeclaration, isExported: boolean): ApiDocumentation | undefined { 305 | const name = node.name.text; 306 | const description = this.extractJSDocComment(node); 307 | const properties: ApiProperty[] = []; 308 | 309 | // Extract properties 310 | node.members.forEach(member => { 311 | if (ts.isPropertySignature(member)) { 312 | if (member.name) { 313 | const propName = member.name.getText(); 314 | const propType = member.type ? member.type.getText() : 'any'; 315 | const propDescription = this.extractJSDocComment(member); 316 | const optional = member.questionToken !== undefined; 317 | 318 | properties.push({ 319 | name: propName, 320 | type: propType, 321 | description: propDescription, 322 | optional 323 | }); 324 | } 325 | } else if (ts.isMethodSignature(member)) { 326 | if (member.name) { 327 | const methodName = member.name.getText(); 328 | const methodParams = member.parameters.map(param => this.extractParameter(param)); 329 | const methodReturnType = member.type ? member.type.getText() : 'any'; 330 | const methodDescription = this.extractJSDocComment(member); 331 | 332 | properties.push({ 333 | name: `${methodName}()`, 334 | type: `(${methodParams.map(p => `${p.name}: ${p.type}`).join(', ')}) => ${methodReturnType}`, 335 | description: methodDescription, 336 | optional: false 337 | }); 338 | } 339 | } 340 | }); 341 | 342 | return { 343 | name, 344 | description, 345 | type: 'interface', 346 | properties, 347 | typeDefinition: node.getText(), 348 | isExported 349 | }; 350 | } 351 | 352 | /** 353 | * Extract type alias declaration 354 | */ 355 | private extractTypeAliasDeclaration(node: ts.TypeAliasDeclaration, isExported: boolean): ApiDocumentation | undefined { 356 | const name = node.name.text; 357 | const description = this.extractJSDocComment(node); 358 | const typeDefinition = node.getText(); 359 | 360 | return { 361 | name, 362 | description, 363 | type: 'type', 364 | typeDefinition, 365 | isExported 366 | }; 367 | } 368 | 369 | /** 370 | * Extract variable declarations 371 | */ 372 | private extractVariableDeclarations(node: ts.VariableStatement, isExported: boolean): ApiDocumentation[] { 373 | const result: ApiDocumentation[] = []; 374 | const description = this.extractJSDocComment(node); 375 | 376 | node.declarationList.declarations.forEach(declaration => { 377 | if (declaration.name && ts.isIdentifier(declaration.name)) { 378 | const name = declaration.name.text; 379 | const type = declaration.type ? declaration.type.getText() : 'any'; 380 | 381 | result.push({ 382 | name, 383 | description, 384 | type: 'variable', 385 | typeDefinition: type, 386 | isExported 387 | }); 388 | } 389 | }); 390 | 391 | return result; 392 | } 393 | 394 | /** 395 | * Extract namespace declaration 396 | */ 397 | private extractNamespaceDeclaration(node: ts.ModuleDeclaration, isExported: boolean): ApiDocumentation | undefined { 398 | if (!node.name || !ts.isIdentifier(node.name)) return undefined; 399 | 400 | const name = node.name.text; 401 | const description = this.extractJSDocComment(node); 402 | 403 | return { 404 | name, 405 | description, 406 | type: 'namespace', 407 | isExported 408 | }; 409 | } 410 | 411 | /** 412 | * Extract enum declaration 413 | */ 414 | private extractEnumDeclaration(node: ts.EnumDeclaration, isExported: boolean): ApiDocumentation | undefined { 415 | const name = node.name.text; 416 | const description = this.extractJSDocComment(node); 417 | const properties: ApiProperty[] = []; 418 | 419 | // Extract enum members 420 | node.members.forEach(member => { 421 | if (member.name) { 422 | const memberName = member.name.getText(); 423 | const memberDescription = this.extractJSDocComment(member); 424 | 425 | properties.push({ 426 | name: memberName, 427 | description: memberDescription, 428 | optional: false 429 | }); 430 | } 431 | }); 432 | 433 | return { 434 | name, 435 | description, 436 | type: 'enum', 437 | properties, 438 | isExported 439 | }; 440 | } 441 | 442 | /** 443 | * Extract parameter information 444 | */ 445 | private extractParameter(param: ts.ParameterDeclaration): ApiParameter { 446 | const name = param.name.getText(); 447 | const type = param.type ? param.type.getText() : 'any'; 448 | const optional = param.questionToken !== undefined || param.initializer !== undefined; 449 | const defaultValue = param.initializer ? param.initializer.getText() : undefined; 450 | 451 | return { 452 | name, 453 | type, 454 | optional, 455 | defaultValue 456 | }; 457 | } 458 | 459 | /** 460 | * Extract JSDoc comments 461 | */ 462 | private extractJSDocComment(node: ts.Node): string | undefined { 463 | const jsDocTags = ts.getJSDocTags(node); 464 | if (jsDocTags.length === 0) return undefined; 465 | 466 | const comments: string[] = []; 467 | 468 | jsDocTags.forEach(tag => { 469 | if (tag.comment) { 470 | const tagName = tag.tagName ? tag.tagName.text : ''; 471 | const comment = typeof tag.comment === 'string' ? tag.comment : tag.comment.map(c => c.text).join(' '); 472 | 473 | if (tagName) { 474 | comments.push(`@${tagName} ${comment}`); 475 | } else { 476 | comments.push(comment); 477 | } 478 | } 479 | }); 480 | 481 | return comments.join('\n'); 482 | } 483 | 484 | /** 485 | * Fetch TypeScript definition file from unpkg.com 486 | */ 487 | public async fetchTypeDefinition(packageName: string, version?: string): Promise { 488 | try { 489 | this.logger.debug(`Fetching TypeScript definition for ${packageName}${version ? `@${version}` : ""}`); 490 | 491 | // First, try to get package.json to find the types field 492 | const packageJsonUrl = `https://unpkg.com/${packageName}${version ? `@${version}` : ""}/package.json`; 493 | const packageJsonResponse = await axios.get(packageJsonUrl); 494 | 495 | if (packageJsonResponse.data) { 496 | const typesField = packageJsonResponse.data.types || packageJsonResponse.data.typings; 497 | 498 | if (typesField) { 499 | // Fetch the types file 500 | const typesUrl = `https://unpkg.com/${packageName}${version ? `@${version}` : ""}/${typesField}`; 501 | const typesResponse = await axios.get(typesUrl); 502 | 503 | if (typesResponse.data) { 504 | return typesResponse.data; 505 | } 506 | } 507 | } 508 | 509 | // If no types field, try common locations 510 | const commonTypesPaths = [ 511 | 'index.d.ts', 512 | 'dist/index.d.ts', 513 | 'lib/index.d.ts', 514 | 'types/index.d.ts', 515 | `${packageName}.d.ts` 516 | ]; 517 | 518 | for (const path of commonTypesPaths) { 519 | try { 520 | const url = `https://unpkg.com/${packageName}${version ? `@${version}` : ""}/${path}`; 521 | const response = await axios.get(url); 522 | 523 | if (response.data) { 524 | return response.data; 525 | } 526 | } catch { 527 | // Continue to next path 528 | } 529 | } 530 | 531 | // If still not found, try to get the @types package 532 | try { 533 | const typesPackageUrl = `https://unpkg.com/@types/${packageName}/index.d.ts`; 534 | const typesPackageResponse = await axios.get(typesPackageUrl); 535 | 536 | if (typesPackageResponse.data) { 537 | return typesPackageResponse.data; 538 | } 539 | } catch { 540 | // No @types package found 541 | } 542 | 543 | return undefined; 544 | } catch (error) { 545 | this.logger.error(`Error fetching TypeScript definition for ${packageName}:`, error); 546 | return undefined; 547 | } 548 | } 549 | 550 | /** 551 | * Fetch example code from unpkg.com 552 | */ 553 | public async fetchExamples(packageName: string, version?: string): Promise { 554 | try { 555 | this.logger.debug(`Fetching examples for ${packageName}${version ? `@${version}` : ""}`); 556 | 557 | const examples: string[] = []; 558 | 559 | // Try to fetch examples from common locations 560 | const commonExamplePaths = [ 561 | 'examples/', 562 | 'example/', 563 | 'docs/examples/', 564 | 'demo/' 565 | ]; 566 | 567 | for (const path of commonExamplePaths) { 568 | try { 569 | const url = `https://unpkg.com/${packageName}${version ? `@${version}` : ""}/${path}`; 570 | const response = await axios.get(url); 571 | 572 | if (response.data) { 573 | // If it's a directory listing, try to find JavaScript or TypeScript files 574 | if (typeof response.data === 'string' && response.data.includes('')) { 575 | // This is likely a directory listing, try to extract file links 576 | const fileLinks = response.data.match(/href="([^"]+\.(js|ts))"/g); 577 | 578 | if (fileLinks && fileLinks.length > 0) { 579 | // Get up to 3 example files 580 | for (let i = 0; i < Math.min(3, fileLinks.length); i++) { 581 | const fileMatch = fileLinks[i].match(/href="([^"]+)"/); 582 | if (fileMatch && fileMatch[1]) { 583 | const fileName = fileMatch[1]; 584 | const fileUrl = `https://unpkg.com/${packageName}${version ? `@${version}` : ""}/${path}${fileName}`; 585 | 586 | try { 587 | const fileResponse = await axios.get(fileUrl); 588 | if (fileResponse.data) { 589 | examples.push(`// Example: ${fileName}\n${fileResponse.data}`); 590 | } 591 | } catch { 592 | // Skip this file 593 | } 594 | } 595 | } 596 | } 597 | } 598 | } 599 | } catch { 600 | // Continue to next path 601 | } 602 | } 603 | 604 | // If no examples found in dedicated directories, try to extract from README 605 | if (examples.length === 0) { 606 | try { 607 | const readmeUrl = `https://unpkg.com/${packageName}${version ? `@${version}` : ""}/README.md`; 608 | const readmeResponse = await axios.get(readmeUrl); 609 | 610 | if (readmeResponse.data) { 611 | const readme = readmeResponse.data; 612 | const codeBlocks = readme.match(/```(?:js|javascript|typescript)[\s\S]*?```/g); 613 | 614 | if (codeBlocks && codeBlocks.length > 0) { 615 | // Get up to 3 code examples 616 | for (let i = 0; i < Math.min(3, codeBlocks.length); i++) { 617 | const codeBlock = codeBlocks[i].replace(/```(?:js|javascript|typescript)\n/, '').replace(/```$/, ''); 618 | examples.push(`// Example ${i + 1} from README\n${codeBlock}`); 619 | } 620 | } 621 | } 622 | } catch { 623 | // No README found 624 | } 625 | } 626 | 627 | return examples; 628 | } catch (error) { 629 | this.logger.error(`Error fetching examples for ${packageName}:`, error); 630 | return []; 631 | } 632 | } 633 | 634 | /** 635 | * Format API documentation as markdown 636 | */ 637 | public formatApiDocumentationAsMarkdown(apiDoc: PackageApiDocumentation): string { 638 | let markdown = `# ${apiDoc.packageName} API Documentation\n\n`; 639 | 640 | if (apiDoc.description) { 641 | markdown += `${apiDoc.description}\n\n`; 642 | } 643 | 644 | if (apiDoc.version) { 645 | markdown += `**Version:** ${apiDoc.version}\n\n`; 646 | } 647 | 648 | // Add exported items 649 | if (apiDoc.exports.length > 0) { 650 | markdown += `## Exports\n\n`; 651 | 652 | apiDoc.exports.forEach(item => { 653 | markdown += this.formatApiItemAsMarkdown(item); 654 | }); 655 | } 656 | 657 | // Add types 658 | if (apiDoc.types.length > 0) { 659 | markdown += `## Types\n\n`; 660 | 661 | apiDoc.types.forEach(item => { 662 | markdown += this.formatApiItemAsMarkdown(item); 663 | }); 664 | } 665 | 666 | // Add examples 667 | if (apiDoc.examples && apiDoc.examples.length > 0) { 668 | markdown += `## Examples\n\n`; 669 | 670 | apiDoc.examples.forEach((example, index) => { 671 | markdown += `### Example ${index + 1}\n\n\`\`\`javascript\n${example}\n\`\`\`\n\n`; 672 | }); 673 | } 674 | 675 | return markdown; 676 | } 677 | 678 | /** 679 | * Format API item as markdown 680 | */ 681 | private formatApiItemAsMarkdown(item: ApiDocumentation): string { 682 | let markdown = `### ${item.name}\n\n`; 683 | 684 | if (item.description) { 685 | markdown += `${item.description}\n\n`; 686 | } 687 | 688 | if (item.type === 'function') { 689 | markdown += `**Type:** Function\n\n`; 690 | 691 | if (item.signature) { 692 | markdown += `**Signature:**\n\`\`\`typescript\n${item.signature}\n\`\`\`\n\n`; 693 | } 694 | 695 | if (item.parameters && item.parameters.length > 0) { 696 | markdown += `**Parameters:**\n\n`; 697 | 698 | item.parameters.forEach(param => { 699 | markdown += `- \`${param.name}${param.optional ? '?' : ''}: ${param.type || 'any'}\``; 700 | if (param.defaultValue) { 701 | markdown += ` (default: ${param.defaultValue})`; 702 | } 703 | if (param.description) { 704 | markdown += ` - ${param.description}`; 705 | } 706 | markdown += `\n`; 707 | }); 708 | 709 | markdown += `\n`; 710 | } 711 | 712 | if (item.returnType) { 713 | markdown += `**Returns:** \`${item.returnType}\`\n\n`; 714 | } 715 | } else if (item.type === 'class') { 716 | markdown += `**Type:** Class\n\n`; 717 | 718 | if (item.properties && item.properties.length > 0) { 719 | markdown += `**Properties:**\n\n`; 720 | 721 | item.properties.forEach(prop => { 722 | markdown += `- \`${prop.name}${prop.optional ? '?' : ''}: ${prop.type || 'any'}\``; 723 | if (prop.description) { 724 | markdown += ` - ${prop.description}`; 725 | } 726 | markdown += `\n`; 727 | }); 728 | 729 | markdown += `\n`; 730 | } 731 | 732 | if (item.methods && item.methods.length > 0) { 733 | markdown += `**Methods:**\n\n`; 734 | 735 | item.methods.forEach(method => { 736 | markdown += `#### ${method.name}\n\n`; 737 | 738 | if (method.description) { 739 | markdown += `${method.description}\n\n`; 740 | } 741 | 742 | if (method.signature) { 743 | markdown += `**Signature:**\n\`\`\`typescript\n${method.signature}\n\`\`\`\n\n`; 744 | } 745 | 746 | if (method.parameters && method.parameters.length > 0) { 747 | markdown += `**Parameters:**\n\n`; 748 | 749 | method.parameters.forEach(param => { 750 | markdown += `- \`${param.name}${param.optional ? '?' : ''}: ${param.type || 'any'}\``; 751 | if (param.defaultValue) { 752 | markdown += ` (default: ${param.defaultValue})`; 753 | } 754 | if (param.description) { 755 | markdown += ` - ${param.description}`; 756 | } 757 | markdown += `\n`; 758 | }); 759 | 760 | markdown += `\n`; 761 | } 762 | 763 | if (method.returnType) { 764 | markdown += `**Returns:** \`${method.returnType}\`\n\n`; 765 | } 766 | }); 767 | } 768 | } else if (item.type === 'interface' || item.type === 'type') { 769 | markdown += `**Type:** ${item.type === 'interface' ? 'Interface' : 'Type'}\n\n`; 770 | 771 | if (item.typeDefinition) { 772 | markdown += `**Definition:**\n\`\`\`typescript\n${item.typeDefinition}\n\`\`\`\n\n`; 773 | } 774 | 775 | if (item.properties && item.properties.length > 0) { 776 | markdown += `**Properties:**\n\n`; 777 | 778 | item.properties.forEach(prop => { 779 | markdown += `- \`${prop.name}${prop.optional ? '?' : ''}: ${prop.type || 'any'}\``; 780 | if (prop.description) { 781 | markdown += ` - ${prop.description}`; 782 | } 783 | markdown += `\n`; 784 | }); 785 | 786 | markdown += `\n`; 787 | } 788 | } else if (item.type === 'enum') { 789 | markdown += `**Type:** Enum\n\n`; 790 | 791 | if (item.properties && item.properties.length > 0) { 792 | markdown += `**Values:**\n\n`; 793 | 794 | item.properties.forEach(prop => { 795 | markdown += `- \`${prop.name}\``; 796 | if (prop.description) { 797 | markdown += ` - ${prop.description}`; 798 | } 799 | markdown += `\n`; 800 | }); 801 | 802 | markdown += `\n`; 803 | } 804 | } 805 | 806 | return markdown; 807 | } 808 | } 809 | -------------------------------------------------------------------------------- /src/npm-docs-integration.ts: -------------------------------------------------------------------------------- 1 | import { NpmDocsEnhancer, PackageApiDocumentation } from './npm-docs-enhancer.js'; 2 | import { logger } from './logger.js'; 3 | import axios from 'axios'; 4 | import { readFileSync, existsSync } from 'fs'; 5 | import { join } from 'path'; 6 | 7 | // Enhanced version of NpmDocArgs interface 8 | export interface NpmDocArgs { 9 | package: string; 10 | version?: string; 11 | projectPath?: string; 12 | section?: string; 13 | maxLength?: number; 14 | query?: string; 15 | includeTypes?: boolean; // Whether to include TypeScript type definitions 16 | includeExamples?: boolean; // Whether to include code examples 17 | } 18 | 19 | // Enhanced version of isNpmDocArgs function 20 | export const isNpmDocArgs = (args: unknown): args is NpmDocArgs => { 21 | return ( 22 | typeof args === "object" && 23 | args !== null && 24 | typeof (args as NpmDocArgs).package === "string" && 25 | (typeof (args as NpmDocArgs).version === "string" || 26 | (args as NpmDocArgs).version === undefined) && 27 | (typeof (args as NpmDocArgs).projectPath === "string" || 28 | (args as NpmDocArgs).projectPath === undefined) && 29 | (typeof (args as NpmDocArgs).section === "string" || 30 | (args as NpmDocArgs).section === undefined) && 31 | (typeof (args as NpmDocArgs).maxLength === "number" || 32 | (args as NpmDocArgs).maxLength === undefined) && 33 | (typeof (args as NpmDocArgs).query === "string" || 34 | (args as NpmDocArgs).query === undefined) && 35 | (typeof (args as NpmDocArgs).includeTypes === "boolean" || 36 | (args as NpmDocArgs).includeTypes === undefined) && 37 | (typeof (args as NpmDocArgs).includeExamples === "boolean" || 38 | (args as NpmDocArgs).includeExamples === undefined) 39 | ); 40 | }; 41 | 42 | // Interface for registry configuration 43 | export interface NpmConfig { 44 | registry: string; 45 | token?: string; 46 | } 47 | 48 | // Interface for documentation result 49 | export interface DocResult { 50 | description?: string; 51 | usage?: string; 52 | example?: string; 53 | error?: string; 54 | searchResults?: SearchResults; 55 | suggestInstall?: boolean; 56 | apiDocumentation?: PackageApiDocumentation; 57 | } 58 | 59 | // Interface for search results 60 | export interface SearchResults { 61 | results: SearchResult[]; 62 | totalResults: number; 63 | error?: string; 64 | suggestInstall?: boolean; 65 | } 66 | 67 | // Interface for search result 68 | export interface SearchResult { 69 | symbol?: string; 70 | match: string; 71 | context?: string; 72 | score: number; 73 | type?: string; 74 | } 75 | 76 | // Class to handle NPM package documentation 77 | export class NpmDocsHandler { 78 | private enhancer: NpmDocsEnhancer; 79 | 80 | constructor() { 81 | this.enhancer = new NpmDocsEnhancer(logger); 82 | } 83 | 84 | /** 85 | * Get documentation for an NPM package 86 | * Enhanced to return structured API documentation 87 | */ 88 | public async describeNpmPackage( 89 | args: NpmDocArgs, 90 | getRegistryConfigForPackage: (packageName: string, projectPath?: string) => NpmConfig, 91 | isNpmPackageInstalledLocally: (packageName: string, projectPath?: string) => boolean, 92 | getLocalNpmDoc: (packageName: string, projectPath?: string) => DocResult 93 | ): Promise { 94 | const { package: packageName, version, projectPath, includeTypes = true, includeExamples = true } = args; 95 | logger.debug(`Getting NPM documentation for ${packageName}${version ? `@${version}` : ""}`); 96 | 97 | try { 98 | // Check if package is installed locally first 99 | const isInstalled = isNpmPackageInstalledLocally(packageName, projectPath); 100 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 101 | let packageInfo: any; 102 | let apiDocumentation: PackageApiDocumentation | undefined; 103 | let examples: string[] = []; 104 | 105 | if (isInstalled) { 106 | logger.debug(`Using local documentation for ${packageName}`); 107 | const localDoc = getLocalNpmDoc(packageName, projectPath); 108 | 109 | // Try to extract TypeScript definitions from local installation 110 | if (includeTypes) { 111 | const basePath = projectPath || process.cwd(); 112 | const packagePath = join(basePath, "node_modules", packageName); 113 | const packageJsonPath = join(packagePath, "package.json"); 114 | 115 | if (existsSync(packageJsonPath)) { 116 | packageInfo = JSON.parse(readFileSync(packageJsonPath, "utf-8")); 117 | 118 | // Check for TypeScript definitions 119 | if (packageInfo.types || packageInfo.typings) { 120 | const typesPath = join(packagePath, packageInfo.types || packageInfo.typings); 121 | 122 | if (existsSync(typesPath)) { 123 | const typesContent = readFileSync(typesPath, "utf-8"); 124 | apiDocumentation = await this.enhancer.extractApiDocumentation(packageName, typesContent); 125 | 126 | // Add API documentation to the result 127 | if (apiDocumentation && apiDocumentation.exports.length > 0) { 128 | const apiMarkdown = this.enhancer.formatApiDocumentationAsMarkdown(apiDocumentation); 129 | localDoc.usage = localDoc.usage ? `${localDoc.usage}\n\n${apiMarkdown}` : apiMarkdown; 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | return localDoc; 137 | } 138 | 139 | // If not installed, fetch from npm registry 140 | logger.debug(`Fetching NPM documentation for ${packageName} from registry`); 141 | 142 | try { 143 | const config = getRegistryConfigForPackage(packageName, projectPath); 144 | const headers: Record = {}; 145 | if (config.token) { 146 | headers.Authorization = `Bearer ${config.token}`; 147 | } 148 | 149 | const versionSuffix = version ? `/${version}` : ""; 150 | const url = `${config.registry}/${packageName}${versionSuffix}`; 151 | 152 | const response = await axios.get(url, { headers }); 153 | packageInfo = response.data; 154 | 155 | if (packageInfo) { 156 | const result: DocResult = { 157 | description: packageInfo.description || "No description available" 158 | }; 159 | 160 | // Extract usage and examples from README if available 161 | if (packageInfo.readme) { 162 | // Convert HTML to Markdown if needed 163 | const readme = this.enhancer.convertHtmlToMarkdown(packageInfo.readme); 164 | const sections = readme.split(/#+\s/); 165 | 166 | for (const section of sections) { 167 | const lower = section.toLowerCase(); 168 | if (lower.startsWith("usage") || lower.startsWith("getting started")) { 169 | // Truncate usage section to a reasonable length 170 | const usage = section.split("\n").slice(1).join("\n").trim(); 171 | result.usage = usage.length > 1000 172 | ? usage.substring(0, 1000) + "... (truncated)" 173 | : usage; 174 | } else if (lower.startsWith("example")) { 175 | // Truncate example section to a reasonable length 176 | const example = section.split("\n").slice(1).join("\n").trim(); 177 | result.example = example.length > 1000 178 | ? example.substring(0, 1000) + "... (truncated)" 179 | : example; 180 | } 181 | } 182 | } 183 | 184 | // Fetch TypeScript definitions from unpkg.com if requested 185 | if (includeTypes) { 186 | const typesContent = await this.enhancer.fetchTypeDefinition(packageName, version); 187 | 188 | if (typesContent) { 189 | apiDocumentation = await this.enhancer.extractApiDocumentation(packageName, typesContent); 190 | 191 | // Add API documentation to the result 192 | if (apiDocumentation && apiDocumentation.exports.length > 0) { 193 | const apiMarkdown = this.enhancer.formatApiDocumentationAsMarkdown(apiDocumentation); 194 | result.usage = result.usage ? `${result.usage}\n\n${apiMarkdown}` : apiMarkdown; 195 | } 196 | } 197 | } 198 | 199 | // Fetch examples from unpkg.com if requested 200 | if (includeExamples) { 201 | examples = await this.enhancer.fetchExamples(packageName, version); 202 | 203 | if (examples.length > 0) { 204 | const examplesMarkdown = examples.map((example, index) => 205 | `### Example ${index + 1}\n\n\`\`\`javascript\n${example}\n\`\`\`` 206 | ).join("\n\n"); 207 | 208 | result.example = result.example ? `${result.example}\n\n${examplesMarkdown}` : examplesMarkdown; 209 | } 210 | } 211 | 212 | return result; 213 | } else { 214 | return { 215 | error: `No documentation found for ${packageName} in npm registry`, 216 | suggestInstall: true 217 | }; 218 | } 219 | } catch (error) { 220 | if (axios.isAxiosError(error) && error.response?.status === 404) { 221 | return { 222 | error: `Package ${packageName} not found. Try installing it with 'npm install ${packageName}'`, 223 | suggestInstall: true 224 | }; 225 | } 226 | throw error; 227 | } 228 | } catch (error) { 229 | const errorMessage = error instanceof Error ? error.message : String(error); 230 | logger.error(`Error getting NPM documentation for ${packageName}:`, error); 231 | return { 232 | error: `Failed to fetch NPM documentation: ${errorMessage}` 233 | }; 234 | } 235 | } 236 | 237 | /** 238 | * Get full documentation for an NPM package 239 | * Enhanced to provide comprehensive information for LLMs 240 | */ 241 | public async getNpmPackageDoc( 242 | args: NpmDocArgs, 243 | getRegistryConfigForPackage: (packageName: string, projectPath?: string) => NpmConfig, 244 | isNpmPackageInstalledLocally: (packageName: string, projectPath?: string) => boolean, 245 | getLocalNpmDoc: (packageName: string, projectPath?: string) => DocResult 246 | ): Promise { 247 | const { 248 | package: packageName, 249 | version, 250 | projectPath, 251 | section, 252 | maxLength = 20000, 253 | query, 254 | includeTypes = true, 255 | includeExamples = true 256 | } = args; 257 | 258 | logger.debug(`Getting full NPM documentation for ${packageName}${version ? `@${version}` : ""}`); 259 | 260 | try { 261 | // Check if package is installed locally first 262 | const isInstalled = isNpmPackageInstalledLocally(packageName, projectPath); 263 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 264 | let packageInfo: any; 265 | let apiDocumentation: PackageApiDocumentation | undefined; 266 | let examples: string[] = []; 267 | 268 | if (isInstalled) { 269 | logger.debug(`Using local documentation for ${packageName}`); 270 | const localDoc = getLocalNpmDoc(packageName, projectPath); 271 | 272 | // Try to extract TypeScript definitions from local installation 273 | if (includeTypes) { 274 | const basePath = projectPath || process.cwd(); 275 | const packagePath = join(basePath, "node_modules", packageName); 276 | const packageJsonPath = join(packagePath, "package.json"); 277 | 278 | if (existsSync(packageJsonPath)) { 279 | packageInfo = JSON.parse(readFileSync(packageJsonPath, "utf-8")); 280 | 281 | // Check for TypeScript definitions 282 | if (packageInfo.types || packageInfo.typings) { 283 | const typesPath = join(packagePath, packageInfo.types || packageInfo.typings); 284 | 285 | if (existsSync(typesPath)) { 286 | const typesContent = readFileSync(typesPath, "utf-8"); 287 | apiDocumentation = await this.enhancer.extractApiDocumentation(packageName, typesContent); 288 | localDoc.apiDocumentation = apiDocumentation; 289 | } 290 | } 291 | } 292 | } 293 | 294 | return localDoc; 295 | } 296 | 297 | // If not installed, fetch from npm registry 298 | logger.debug(`Fetching NPM documentation for ${packageName} from registry`); 299 | 300 | try { 301 | const config = getRegistryConfigForPackage(packageName, projectPath); 302 | const headers: Record = {}; 303 | if (config.token) { 304 | headers.Authorization = `Bearer ${config.token}`; 305 | } 306 | 307 | const versionSuffix = version ? `/${version}` : ""; 308 | const url = `${config.registry}/${packageName}${versionSuffix}`; 309 | 310 | const response = await axios.get(url, { headers }); 311 | packageInfo = response.data; 312 | 313 | if (!packageInfo) { 314 | return { 315 | error: `No documentation found for ${packageName}`, 316 | suggestInstall: !isInstalled 317 | }; 318 | } 319 | 320 | // Build the result 321 | const result: DocResult = { 322 | description: packageInfo.description || "No description available" 323 | }; 324 | 325 | // Create a comprehensive documentation format 326 | let formattedDoc = `# ${packageName}\n\n`; 327 | formattedDoc += `${result.description}\n\n`; 328 | 329 | // Add version, homepage, and keywords if available 330 | if (packageInfo.version) { 331 | formattedDoc += `**Version:** ${packageInfo.version}\n\n`; 332 | } 333 | 334 | if (packageInfo.homepage) { 335 | formattedDoc += `**Homepage:** ${packageInfo.homepage}\n\n`; 336 | } 337 | 338 | // Add installation instructions 339 | formattedDoc += `## Installation\n\n\`\`\`bash\nnpm install ${packageName}\n\`\`\`\n\n`; 340 | 341 | // Fetch TypeScript definitions if requested 342 | if (includeTypes) { 343 | const typesContent = await this.enhancer.fetchTypeDefinition(packageName, version); 344 | 345 | if (typesContent) { 346 | apiDocumentation = await this.enhancer.extractApiDocumentation(packageName, typesContent); 347 | result.apiDocumentation = apiDocumentation; 348 | 349 | // Add API documentation to the formatted doc 350 | if (apiDocumentation && (apiDocumentation.exports.length > 0 || apiDocumentation.types.length > 0)) { 351 | formattedDoc += `## API Documentation\n\n`; 352 | 353 | // Add exports 354 | if (apiDocumentation.exports.length > 0) { 355 | formattedDoc += `### Exports\n\n`; 356 | 357 | apiDocumentation.exports.slice(0, 10).forEach(item => { 358 | formattedDoc += `#### ${item.name}\n\n`; 359 | 360 | if (item.description) { 361 | formattedDoc += `${item.description}\n\n`; 362 | } 363 | 364 | if (item.signature) { 365 | formattedDoc += `\`\`\`typescript\n${item.signature}\n\`\`\`\n\n`; 366 | } 367 | }); 368 | } 369 | 370 | // Add types 371 | if (apiDocumentation.types.length > 0) { 372 | formattedDoc += `### Types\n\n`; 373 | 374 | apiDocumentation.types.slice(0, 10).forEach(item => { 375 | formattedDoc += `#### ${item.name}\n\n`; 376 | 377 | if (item.description) { 378 | formattedDoc += `${item.description}\n\n`; 379 | } 380 | 381 | if (item.typeDefinition) { 382 | formattedDoc += `\`\`\`typescript\n${item.typeDefinition}\n\`\`\`\n\n`; 383 | } 384 | }); 385 | } 386 | } 387 | } 388 | } 389 | 390 | // Fetch examples if requested 391 | if (includeExamples) { 392 | examples = await this.enhancer.fetchExamples(packageName, version); 393 | 394 | if (examples.length > 0) { 395 | formattedDoc += `## Examples\n\n`; 396 | 397 | examples.forEach((example, index) => { 398 | formattedDoc += `### Example ${index + 1}\n\n\`\`\`javascript\n${example}\n\`\`\`\n\n`; 399 | }); 400 | } 401 | } 402 | 403 | // Process README content if available 404 | if (packageInfo.readme) { 405 | // Convert HTML to Markdown if needed 406 | const readme = this.enhancer.convertHtmlToMarkdown(packageInfo.readme); 407 | 408 | // If a specific section was requested 409 | if (section) { 410 | // Try different variations of the section name for better matching 411 | const sectionVariations = [ 412 | section, 413 | section.toLowerCase(), 414 | section.toUpperCase(), 415 | section.charAt(0).toUpperCase() + section.slice(1), 416 | `${section} API`, 417 | `${section.toUpperCase()} API`, 418 | `${section} api`, 419 | `${packageName} ${section}`, 420 | `${packageName}.${section}` 421 | ]; 422 | 423 | let found = false; 424 | for (const sectionVar of sectionVariations) { 425 | const sectionRegex = new RegExp(`#+\\s+.*${sectionVar}.*(?:\\s|$|:)([\\s\\S]*?)(?:#+\\s+|$)`, 'i'); 426 | const match = readme ? readme.match(sectionRegex) : null; 427 | 428 | if (match && match[1]) { 429 | result.usage = match[1].trim(); 430 | found = true; 431 | break; 432 | } 433 | } 434 | 435 | if (!found) { 436 | result.error = `Section '${section}' not found in documentation`; 437 | // Still provide the formatted doc as usage 438 | result.usage = formattedDoc; 439 | } 440 | } 441 | // If a search query was provided 442 | else if (query && readme) { 443 | const lines = readme.split('\n'); 444 | const matchingLines: string[] = []; 445 | const matchedSections: Set = new Set(); 446 | 447 | // First pass: find all matching lines 448 | for (let i = 0; i < lines.length; i++) { 449 | if (lines[i].toLowerCase().includes(query.toLowerCase())) { 450 | matchedSections.add(i); 451 | } 452 | } 453 | 454 | // Second pass: extract sections around matches with more context 455 | for (const lineIndex of matchedSections) { 456 | // Find the start of the section (heading) 457 | let sectionStart = lineIndex; 458 | while (sectionStart > 0 && !lines[sectionStart].startsWith('#')) { 459 | sectionStart--; 460 | } 461 | 462 | // Find the end of the section (next heading or end of file) 463 | let sectionEnd = lineIndex; 464 | while (sectionEnd < lines.length - 1 && !lines[sectionEnd + 1].startsWith('#')) { 465 | sectionEnd++; 466 | } 467 | 468 | // Extract the section with context 469 | const contextStart = Math.max(sectionStart, lineIndex - 10); 470 | const contextEnd = Math.min(sectionEnd, lineIndex + 20); 471 | 472 | // Add section heading if available 473 | if (sectionStart >= 0 && lines[sectionStart].startsWith('#')) { 474 | matchingLines.push(lines[sectionStart]); 475 | } 476 | 477 | // Add context lines 478 | matchingLines.push(...lines.slice(contextStart, contextEnd + 1), ""); 479 | } 480 | 481 | if (matchingLines.length > 0) { 482 | const content = matchingLines.join('\n'); 483 | result.usage = content.length > maxLength 484 | ? content.substring(0, maxLength) + "... (truncated)" 485 | : content; 486 | } else { 487 | result.error = `No matches found for '${query}' in documentation`; 488 | // Still provide the formatted doc as usage 489 | result.usage = formattedDoc; 490 | } 491 | } 492 | // Otherwise use our formatted documentation 493 | else { 494 | result.usage = formattedDoc; 495 | } 496 | } else { 497 | // If no README, use our formatted documentation 498 | result.usage = formattedDoc; 499 | } 500 | 501 | // Truncate if necessary 502 | if (result.usage && result.usage.length > maxLength) { 503 | result.usage = result.usage.substring(0, maxLength) + "... (truncated)"; 504 | } 505 | 506 | // Always include the full formatted documentation in the result 507 | result.example = formattedDoc; 508 | 509 | // Make sure usage is populated with at least the basic information if it's empty 510 | if (!result.usage || result.usage.trim() === '') { 511 | result.usage = formattedDoc; 512 | } 513 | 514 | return result; 515 | } catch (error) { 516 | const errorMessage = error instanceof Error ? error.message : String(error); 517 | logger.error(`Error getting full NPM documentation for ${packageName}:`, error); 518 | return { 519 | error: `Failed to fetch NPM documentation: ${errorMessage}` 520 | }; 521 | } 522 | } catch (error) { 523 | const errorMessage = error instanceof Error ? error.message : String(error); 524 | logger.error(`Error getting full NPM documentation for ${packageName}:`, error); 525 | return { 526 | error: `Failed to fetch NPM documentation: ${errorMessage}` 527 | }; 528 | } 529 | } 530 | } 531 | -------------------------------------------------------------------------------- /src/registry-utils.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, existsSync } from 'fs'; 2 | import { homedir } from 'os'; 3 | import { join as pathJoin, dirname } from 'path'; 4 | import { McpLogger } from './logger.js'; 5 | 6 | export interface NpmConfig { 7 | registry: string; 8 | token?: string; 9 | } 10 | 11 | export class RegistryUtils { 12 | private logger: McpLogger; 13 | private registryMap: Map; 14 | 15 | constructor(logger: McpLogger) { 16 | this.logger = logger.child('RegistryUtils'); 17 | this.registryMap = this.loadNpmConfig(); 18 | } 19 | 20 | /** 21 | * Get registry configuration for a package 22 | */ 23 | public getRegistryConfigForPackage(packageName: string, projectPath?: string): NpmConfig { 24 | // Load fresh config if project path is provided 25 | if (projectPath) { 26 | this.registryMap = this.loadNpmConfig(projectPath); 27 | } 28 | 29 | if (packageName.startsWith("@")) { 30 | const scope = packageName.split("/")[0]; 31 | return this.registryMap.get(scope) || this.registryMap.get("default") || { registry: "https://registry.npmjs.org" }; 32 | } 33 | return this.registryMap.get("default") || { registry: "https://registry.npmjs.org" }; 34 | } 35 | 36 | /** 37 | * Load npm configuration from .npmrc files 38 | */ 39 | private loadNpmConfig(projectPath?: string): Map { 40 | const registryMap = new Map(); 41 | registryMap.set("default", { registry: "https://registry.npmjs.org" }); 42 | 43 | const scopeToRegistry = new Map(); 44 | const registryToToken = new Map(); 45 | 46 | this.logger.debug("Loading npm configuration...") 47 | this.logger.debug("Project directory:", projectPath || "not specified"); 48 | 49 | // First read global .npmrc as base configuration 50 | const globalNpmrcPath = pathJoin(homedir(), ".npmrc"); 51 | this.logger.debug("Checking global .npmrc at:", globalNpmrcPath); 52 | if (existsSync(globalNpmrcPath)) { 53 | this.logger.debug("Found global .npmrc"); 54 | try { 55 | const npmrcContent = readFileSync(globalNpmrcPath, "utf-8"); 56 | this.parseNpmrcContent(npmrcContent, scopeToRegistry, registryToToken, registryMap); 57 | } catch (error) { 58 | this.logger.error("Error reading global .npmrc:", error); 59 | } 60 | } 61 | 62 | // Then read from root to project directory, so local configs take precedence 63 | if (projectPath) { 64 | const paths: string[] = []; 65 | let currentDir = projectPath; 66 | const root = dirname(currentDir); 67 | 68 | // Collect all paths first 69 | while (currentDir !== root) { 70 | paths.push(currentDir); 71 | currentDir = dirname(currentDir); 72 | } 73 | paths.push(root); 74 | 75 | // Process paths in reverse order (root to local) 76 | for (const dir of paths.reverse()) { 77 | const localNpmrcPath = pathJoin(dir, ".npmrc"); 78 | this.logger.debug("Checking for .npmrc at:", localNpmrcPath); 79 | if (existsSync(localNpmrcPath)) { 80 | this.logger.debug("Found .npmrc at:", localNpmrcPath); 81 | try { 82 | const npmrcContent = readFileSync(localNpmrcPath, "utf-8"); 83 | this.parseNpmrcContent(npmrcContent, scopeToRegistry, registryToToken, registryMap); 84 | } catch (error) { 85 | this.logger.error(`Error reading local .npmrc at ${localNpmrcPath}:`, error); 86 | } 87 | } 88 | } 89 | } 90 | 91 | try { 92 | // Associate tokens with registries 93 | for (const [scope, registry] of scopeToRegistry.entries()) { 94 | const hostname = new URL(registry).host; 95 | const token = registryToToken.get(hostname); 96 | this.logger.debug(`Setting config for scope ${scope}:`, { registry, token: token ? "[REDACTED]" : undefined }); 97 | registryMap.set(scope, { registry, token }); 98 | } 99 | 100 | // Ensure default registry has its token if available 101 | const defaultConfig = registryMap.get("default"); 102 | if (defaultConfig) { 103 | const hostname = new URL(defaultConfig.registry).host; 104 | const token = registryToToken.get(hostname); 105 | if (token) { 106 | this.logger.debug("Setting token for default registry"); 107 | registryMap.set("default", { ...defaultConfig, token }); 108 | } 109 | } 110 | 111 | this.logger.debug("Final registry configurations:", 112 | Object.fromEntries(Array.from(registryMap.entries()).map(([k, v]) => [ 113 | k, 114 | { registry: v.registry, token: v.token ? "[REDACTED]" : undefined } 115 | ])) 116 | ); 117 | } catch (error) { 118 | this.logger.error("Error processing .npmrc configurations:", error); 119 | } 120 | 121 | return registryMap; 122 | } 123 | 124 | /** 125 | * Parse .npmrc content 126 | */ 127 | private parseNpmrcContent( 128 | content: string, 129 | scopeToRegistry: Map, 130 | registryToToken: Map, 131 | registryMap: Map 132 | ): void { 133 | const lines = content.split("\n"); 134 | 135 | for (const line of lines) { 136 | const trimmedLine = line.trim(); 137 | if (!trimmedLine || trimmedLine.startsWith("#")) continue; 138 | 139 | // Handle registry configurations 140 | // Match patterns like: 141 | // @scope:registry=https://registry.example.com 142 | // registry=https://registry.example.com 143 | const registryMatch = trimmedLine.match(/^(?:@([^:]+):)?registry=(.+)$/); 144 | if (registryMatch) { 145 | const [, scope, registry] = registryMatch; 146 | const cleanRegistry = registry.replace(/\/$/, ""); 147 | if (scope) { 148 | scopeToRegistry.set(`@${scope}`, cleanRegistry); 149 | } else { 150 | registryMap.set("default", { registry: cleanRegistry }); 151 | } 152 | continue; 153 | } 154 | 155 | // Handle authentication tokens 156 | // Match patterns like: 157 | // //registry.example.com/:_authToken=token 158 | // @scope:_authToken=token 159 | // _authToken=token 160 | const tokenMatch = trimmedLine.match(/^(?:\/\/([^/]+)\/:|@([^:]+):)?_authToken=(.+)$/); 161 | if (tokenMatch) { 162 | const [, registry, scope, token] = tokenMatch; 163 | if (registry) { 164 | // Store token for specific registry 165 | // Handle both protocol and non-protocol URLs 166 | registryToToken.set(registry, token); 167 | if (!registry.includes("://")) { 168 | registryToToken.set(`https://${registry}`, token); 169 | registryToToken.set(`http://${registry}`, token); 170 | } 171 | } else if (scope) { 172 | // Store token for scope, we'll resolve the registry later 173 | const scopeRegistry = scopeToRegistry.get(`@${scope}`); 174 | if (scopeRegistry) { 175 | try { 176 | // Try parsing as URL first 177 | const url = new URL(scopeRegistry); 178 | registryToToken.set(url.host, token); 179 | } catch { 180 | // If not a URL, treat as hostname 181 | registryToToken.set(scopeRegistry, token); 182 | registryToToken.set(`https://${scopeRegistry}`, token); 183 | registryToToken.set(`http://${scopeRegistry}`, token); 184 | } 185 | } 186 | } else { 187 | // Default token 188 | const defaultRegistry = registryMap.get("default")?.registry; 189 | if (defaultRegistry) { 190 | try { 191 | // Try parsing as URL first 192 | const url = new URL(defaultRegistry); 193 | registryToToken.set(url.host, token); 194 | } catch { 195 | // If not a URL, treat as hostname 196 | registryToToken.set(defaultRegistry, token); 197 | registryToToken.set(`https://${defaultRegistry}`, token); 198 | registryToToken.set(`http://${defaultRegistry}`, token); 199 | } 200 | } 201 | } 202 | } 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/rust-docs-integration.ts: -------------------------------------------------------------------------------- 1 | import * as cheerio from "cheerio"; 2 | import turndown from "turndown"; 3 | import { 4 | CrateInfo, 5 | CrateSearchResult, 6 | CrateVersion, 7 | FeatureFlag, 8 | RustType, 9 | SearchOptions, 10 | SymbolDefinition, 11 | } from "./types.js"; 12 | import rustHttpClient from "./utils/rust-http-client.js"; 13 | import { McpLogger } from './logger.js' 14 | 15 | const turndownInstance = new turndown(); 16 | 17 | export class RustDocsHandler { 18 | private logger: McpLogger; 19 | 20 | constructor(logger: McpLogger) { 21 | this.logger = logger.child('RustDocs') 22 | } 23 | 24 | /** 25 | * Search for crates on crates.io 26 | */ 27 | async searchCrates( 28 | options: SearchOptions, 29 | ): Promise { 30 | try { 31 | this.logger.info(`searching for crates with query: ${options.query}`); 32 | 33 | const response = await rustHttpClient.cratesIoFetch("crates", { 34 | params: { 35 | q: options.query, 36 | page: options.page || 1, 37 | per_page: options.perPage || 10, 38 | }, 39 | }); 40 | 41 | if (response.contentType !== "json") { 42 | throw new Error("Expected JSON response but got text"); 43 | } 44 | 45 | const data = response.data as { 46 | crates: Array<{ 47 | name: string; 48 | max_version: string; 49 | description?: string; 50 | }>; 51 | meta: { 52 | total: number; 53 | }; 54 | }; 55 | 56 | const crates: CrateInfo[] = data.crates.map((crate) => ({ 57 | name: crate.name, 58 | version: crate.max_version, 59 | description: crate.description, 60 | })); 61 | 62 | return { 63 | crates, 64 | totalCount: data.meta.total, 65 | }; 66 | } catch (error) { 67 | this.logger.error("error searching for crates", { error }); 68 | throw new Error(`failed to search for crates: ${(error as Error).message}`); 69 | } 70 | } 71 | 72 | /** 73 | * Get detailed information about a crate from crates.io 74 | */ 75 | async getCrateDetails(crateName: string): Promise<{ 76 | name: string; 77 | description?: string; 78 | versions: CrateVersion[]; 79 | downloads: number; 80 | homepage?: string; 81 | repository?: string; 82 | documentation?: string; 83 | }> { 84 | try { 85 | this.logger.info(`getting crate details for: ${crateName}`); 86 | 87 | const response = await rustHttpClient.cratesIoFetch(`crates/${crateName}`); 88 | 89 | if (response.contentType !== "json") { 90 | throw new Error("Expected JSON response but got text"); 91 | } 92 | 93 | const data = response.data as { 94 | crate: { 95 | name: string; 96 | description?: string; 97 | downloads: number; 98 | homepage?: string; 99 | repository?: string; 100 | documentation?: string; 101 | }; 102 | versions: Array<{ 103 | num: string; 104 | yanked: boolean; 105 | created_at: string; 106 | }>; 107 | }; 108 | 109 | return { 110 | name: data.crate.name, 111 | description: data.crate.description, 112 | downloads: data.crate.downloads, 113 | homepage: data.crate.homepage, 114 | repository: data.crate.repository, 115 | documentation: data.crate.documentation, 116 | versions: data.versions.map((v) => ({ 117 | version: v.num, 118 | isYanked: v.yanked, 119 | releaseDate: v.created_at, 120 | })), 121 | }; 122 | } catch (error) { 123 | this.logger.error(`error getting crate details for: ${crateName}`, { error }); 124 | throw new Error( 125 | `failed to get crate details for ${crateName}: ${(error as Error).message}`, 126 | ); 127 | } 128 | } 129 | 130 | /** 131 | * Get documentation for a specific crate from docs.rs 132 | */ 133 | async getCrateDocumentation( 134 | crateName: string, 135 | version?: string, 136 | ): Promise { 137 | try { 138 | this.logger.info( 139 | `getting documentation for crate: ${crateName}${version ? ` version ${version}` : ""}`, 140 | ); 141 | 142 | const path = version 143 | ? `crate/${crateName}/${version}` 144 | : `crate/${crateName}/latest`; 145 | 146 | const response = await rustHttpClient.docsRsFetch(path); 147 | 148 | if (response.contentType !== "text") { 149 | throw new Error("Expected HTML response but got JSON"); 150 | } 151 | 152 | return turndownInstance.turndown(response.data); 153 | } catch (error) { 154 | this.logger.error(`error getting documentation for crate: ${crateName}`, { 155 | error, 156 | }); 157 | throw new Error( 158 | `failed to get documentation for crate ${crateName}: ${(error as Error).message}`, 159 | ); 160 | } 161 | } 162 | 163 | /** 164 | * Get type information for a specific item in a crate 165 | */ 166 | async getTypeInfo( 167 | crateName: string, 168 | path: string, 169 | version?: string, 170 | ): Promise { 171 | try { 172 | this.logger.info(`Getting type info for ${path} in crate: ${crateName}`); 173 | 174 | const versionPath = version || "latest"; 175 | const fullPath = `${crateName}/${versionPath}/${crateName}/${path}`; 176 | 177 | const response = await rustHttpClient.docsRsFetch(fullPath); 178 | 179 | if (response.contentType !== "text") { 180 | throw new Error("Expected HTML response but got JSON"); 181 | } 182 | 183 | const $ = cheerio.load(response.data); 184 | 185 | // Determine the kind of type 186 | let kind: RustType["kind"] = "other"; 187 | if ($(".struct").length) kind = "struct"; 188 | else if ($(".enum").length) kind = "enum"; 189 | else if ($(".trait").length) kind = "trait"; 190 | else if ($(".fn").length) kind = "function"; 191 | else if ($(".macro").length) kind = "macro"; 192 | else if ($(".typedef").length) kind = "type"; 193 | else if ($(".mod").length) kind = "module"; 194 | 195 | // Get description 196 | const description = $(".docblock").first().text().trim(); 197 | 198 | // Get source URL if available 199 | const sourceUrl = $(".src-link a").attr("href"); 200 | 201 | const name = path.split("/").pop() || path; 202 | 203 | return { 204 | name, 205 | kind, 206 | path, 207 | description: description || undefined, 208 | sourceUrl: sourceUrl || undefined, 209 | documentationUrl: `https://docs.rs${fullPath}`, 210 | }; 211 | } catch (error) { 212 | this.logger.error(`Error getting type info for ${path} in crate: ${crateName}`, { 213 | error, 214 | }); 215 | throw new Error(`Failed to get type info: ${(error as Error).message}`); 216 | } 217 | } 218 | 219 | /** 220 | * Get feature flags for a crate 221 | */ 222 | async getFeatureFlags( 223 | crateName: string, 224 | version?: string, 225 | ): Promise { 226 | try { 227 | this.logger.info(`Getting feature flags for crate: ${crateName}`); 228 | 229 | const versionPath = version || "latest"; 230 | const response = await rustHttpClient.docsRsFetch( 231 | `/crate/${crateName}/${versionPath}/features`, 232 | ); 233 | 234 | if (response.contentType !== "text") { 235 | throw new Error("Expected HTML response but got JSON"); 236 | } 237 | 238 | const $ = cheerio.load(response.data); 239 | const features: FeatureFlag[] = []; 240 | 241 | $(".feature").each((_, element) => { 242 | const name = $(element).find(".feature-name").text().trim(); 243 | const description = $(element).find(".feature-description").text().trim(); 244 | const enabled = $(element).hasClass("feature-enabled"); 245 | 246 | features.push({ 247 | name, 248 | description: description || undefined, 249 | enabled, 250 | }); 251 | }); 252 | 253 | return features; 254 | } catch (error) { 255 | this.logger.error(`Error getting feature flags for crate: ${crateName}`, { 256 | error, 257 | }); 258 | throw new Error(`Failed to get feature flags: ${(error as Error).message}`); 259 | } 260 | } 261 | 262 | /** 263 | * Get available versions for a crate from crates.io 264 | */ 265 | async getCrateVersions( 266 | crateName: string, 267 | ): Promise { 268 | try { 269 | this.logger.info(`getting versions for crate: ${crateName}`); 270 | 271 | const response = await rustHttpClient.cratesIoFetch(`crates/${crateName}`); 272 | 273 | if (response.contentType !== "json") { 274 | throw new Error("Expected JSON response but got text"); 275 | } 276 | 277 | const data = response.data as { 278 | versions: Array<{ 279 | num: string; 280 | yanked: boolean; 281 | created_at: string; 282 | }>; 283 | }; 284 | 285 | return data.versions.map((v) => ({ 286 | version: v.num, 287 | isYanked: v.yanked, 288 | releaseDate: v.created_at, 289 | })); 290 | } catch (error) { 291 | this.logger.error(`error getting versions for crate: ${crateName}`, { 292 | error, 293 | }); 294 | throw new Error( 295 | `failed to get crate versions: ${(error as Error).message}`, 296 | ); 297 | } 298 | } 299 | 300 | /** 301 | * Get source code for a specific item 302 | */ 303 | async getSourceCode( 304 | crateName: string, 305 | path: string, 306 | version?: string, 307 | ): Promise { 308 | try { 309 | this.logger.info(`Getting source code for ${path} in crate: ${crateName}`); 310 | 311 | const versionPath = version || "latest"; 312 | const response = await rustHttpClient.docsRsFetch( 313 | `/crate/${crateName}/${versionPath}/src/${path}`, 314 | ); 315 | 316 | if (typeof response.data !== "string") { 317 | throw new Error("Expected HTML response but got JSON"); 318 | } 319 | 320 | const $ = cheerio.load(response.data); 321 | return $(".src").text(); 322 | } catch (error) { 323 | this.logger.error( 324 | `Error getting source code for ${path} in crate: ${crateName}`, 325 | { error }, 326 | ); 327 | throw new Error(`Failed to get source code: ${(error as Error).message}`); 328 | } 329 | } 330 | 331 | /** 332 | * Search for symbols within a crate 333 | */ 334 | async searchSymbols( 335 | crateName: string, 336 | query: string, 337 | version?: string, 338 | ): Promise { 339 | try { 340 | this.logger.info( 341 | `searching for symbols in crate: ${crateName} with query: ${query}`, 342 | ); 343 | 344 | try { 345 | const versionPath = version || "latest"; 346 | const response = await rustHttpClient.docsRsFetch( 347 | `/${crateName}/${versionPath}/${crateName}/`, 348 | { 349 | params: { search: query }, 350 | }, 351 | ); 352 | 353 | if (typeof response.data !== "string") { 354 | throw new Error("Expected HTML response but got JSON"); 355 | } 356 | 357 | const $ = cheerio.load(response.data); 358 | const symbols: SymbolDefinition[] = []; 359 | 360 | $(".search-results a").each((_, element) => { 361 | const name = $(element).find(".result-name path").text().trim(); 362 | const kind = $(element).find(".result-name typename").text().trim(); 363 | const path = $(element).attr("href") || ""; 364 | 365 | symbols.push({ 366 | name, 367 | kind, 368 | path, 369 | }); 370 | }); 371 | 372 | return symbols; 373 | } catch (innerError: unknown) { 374 | // If we get a 404, try a different approach - search in the main documentation 375 | if (innerError instanceof Error && innerError.message.includes("404")) { 376 | this.logger.info( 377 | `Search endpoint not found for ${crateName}, trying alternative approach`, 378 | ); 379 | } 380 | 381 | // Re-throw other errors 382 | throw innerError; 383 | } 384 | } catch (error) { 385 | this.logger.error(`Error searching for symbols in crate: ${crateName}`, { 386 | error, 387 | }); 388 | throw new Error( 389 | `Failed to search for symbols: ${(error as Error).message}`, 390 | ); 391 | } 392 | } 393 | } 394 | 395 | -------------------------------------------------------------------------------- /src/search-utils.ts: -------------------------------------------------------------------------------- 1 | import { McpLogger } from './logger.js' 2 | 3 | export interface DocResult { 4 | description?: string 5 | usage?: string 6 | example?: string 7 | error?: string 8 | searchResults?: SearchResults 9 | suggestInstall?: boolean // Flag to indicate if we should suggest package installation 10 | } 11 | 12 | export interface SearchResults { 13 | results: SearchResult[] 14 | totalResults: number 15 | error?: string 16 | suggestInstall?: boolean 17 | } 18 | 19 | export interface SearchResult { 20 | symbol?: string 21 | match: string 22 | context?: string // Make context optional to save space 23 | score: number 24 | type?: string // Type of the section (function, class, etc.) 25 | } 26 | 27 | export interface SearchDocArgs { 28 | package: string 29 | query: string 30 | language: "go" | "python" | "npm" | "swift" | "rust" 31 | fuzzy?: boolean 32 | projectPath?: string 33 | } 34 | 35 | export const isSearchDocArgs = (args: unknown): args is SearchDocArgs => { 36 | return ( 37 | typeof args === "object" && 38 | args !== null && 39 | typeof (args as SearchDocArgs).package === "string" && 40 | typeof (args as SearchDocArgs).query === "string" && 41 | ["go", "python", "npm", "swift", "rust"].includes((args as SearchDocArgs).language) && 42 | (typeof (args as SearchDocArgs).fuzzy === "boolean" || 43 | (args as SearchDocArgs).fuzzy === undefined) && 44 | (typeof (args as SearchDocArgs).projectPath === "string" || 45 | (args as SearchDocArgs).projectPath === undefined) 46 | ) 47 | } 48 | 49 | export interface GoDocArgs { 50 | package: string 51 | symbol?: string 52 | projectPath?: string 53 | } 54 | 55 | export interface PythonDocArgs { 56 | package: string 57 | symbol?: string 58 | projectPath?: string 59 | } 60 | 61 | export interface NpmDocArgs { 62 | package: string 63 | version?: string 64 | projectPath?: string 65 | section?: string 66 | maxLength?: number 67 | query?: string 68 | } 69 | 70 | export interface SwiftDocArgs { 71 | package: string 72 | symbol?: string 73 | projectPath?: string 74 | } 75 | 76 | export const isGoDocArgs = (args: unknown): args is GoDocArgs => { 77 | return ( 78 | typeof args === "object" && 79 | args !== null && 80 | typeof (args as GoDocArgs).package === "string" && 81 | (typeof (args as GoDocArgs).symbol === "string" || 82 | (args as GoDocArgs).symbol === undefined) && 83 | (typeof (args as GoDocArgs).projectPath === "string" || 84 | (args as GoDocArgs).projectPath === undefined) 85 | ) 86 | } 87 | 88 | export const isSwiftDocArgs = (args: unknown): args is SwiftDocArgs => { 89 | return ( 90 | typeof args === "object" && 91 | args !== null && 92 | typeof (args as SwiftDocArgs).package === "string" && 93 | (typeof (args as SwiftDocArgs).symbol === "string" || 94 | (args as SwiftDocArgs).symbol === undefined) && 95 | (typeof (args as SwiftDocArgs).projectPath === "string" || 96 | (args as SwiftDocArgs).projectPath === undefined) 97 | ) 98 | } 99 | 100 | export const isPythonDocArgs = (args: unknown): args is PythonDocArgs => { 101 | return ( 102 | typeof args === "object" && 103 | args !== null && 104 | typeof (args as PythonDocArgs).package === "string" && 105 | (typeof (args as PythonDocArgs).symbol === "string" || 106 | (args as PythonDocArgs).symbol === undefined) && 107 | (typeof (args as PythonDocArgs).projectPath === "string" || 108 | (args as PythonDocArgs).projectPath === undefined) 109 | ) 110 | } 111 | 112 | export const isNpmDocArgs = (args: unknown): args is NpmDocArgs => { 113 | return ( 114 | typeof args === "object" && 115 | args !== null && 116 | typeof (args as NpmDocArgs).package === "string" && 117 | (typeof (args as NpmDocArgs).version === "string" || 118 | (args as NpmDocArgs).version === undefined) && 119 | (typeof (args as NpmDocArgs).projectPath === "string" || 120 | (args as NpmDocArgs).projectPath === undefined) && 121 | (typeof (args as NpmDocArgs).section === "string" || 122 | (args as NpmDocArgs).section === undefined) && 123 | (typeof (args as NpmDocArgs).maxLength === "number" || 124 | (args as NpmDocArgs).maxLength === undefined) && 125 | (typeof (args as NpmDocArgs).query === "string" || 126 | (args as NpmDocArgs).query === undefined) 127 | ) 128 | } 129 | 130 | export class SearchUtils { 131 | private logger: McpLogger 132 | 133 | constructor(logger: McpLogger) { 134 | this.logger = logger.child('SearchUtils') 135 | } 136 | 137 | /** 138 | * Simple fuzzy matching algorithm 139 | */ 140 | public fuzzyMatch(text: string, pattern: string): boolean { 141 | const textLower = text.toLowerCase() 142 | const patternLower = pattern.toLowerCase() 143 | 144 | let textIndex = 0 145 | let patternIndex = 0 146 | 147 | while (textIndex < text.length && patternIndex < pattern.length) { 148 | if (textLower[textIndex] === patternLower[patternIndex]) { 149 | patternIndex++ 150 | } 151 | textIndex++ 152 | } 153 | 154 | return patternIndex === pattern.length 155 | } 156 | 157 | /** 158 | * Extract symbol from text based on language 159 | */ 160 | public extractSymbol(text: string, language: string): string | undefined { 161 | const firstLine = text.split('\n')[0] 162 | switch (language) { 163 | case "go": { 164 | const goMatch = firstLine.match(/^(func|type|var|const)\s+(\w+)/) 165 | return goMatch?.[2] 166 | } 167 | case "python": { 168 | const pyMatch = firstLine.match(/^(class|def)\s+(\w+)/) 169 | return pyMatch?.[2] 170 | } 171 | case "npm": { 172 | // Extract symbol from markdown headings or code blocks 173 | const npmMatch = firstLine.match(/^#+\s*(?:`([^`]+)`|(\w+))/) 174 | return npmMatch?.[1] || npmMatch?.[2] 175 | } 176 | case "swift": { 177 | const swiftMatch = firstLine.match(/^(class|struct|enum|protocol|func|var|let)\s+(\w+)/) 178 | return swiftMatch?.[2] 179 | } 180 | case "rust": { 181 | const rustMatch = firstLine.match(/^(pub\s+)?(struct|enum|trait|impl|fn|mod|type)\s+(\w+)/) 182 | return rustMatch?.[3] 183 | } 184 | default: 185 | return undefined 186 | } 187 | } 188 | 189 | /** 190 | * Parse Go documentation into sections 191 | */ 192 | public parseGoDoc(doc: string): Array<{ content: string; type: string }> { 193 | const sections: Array<{ content: string; type: string }> = [] 194 | let currentSection = '' 195 | let currentType = 'description' 196 | 197 | const lines = doc.split('\n') 198 | for (const line of lines) { 199 | if (line.startsWith('func ')) { 200 | if (currentSection) { 201 | sections.push({ content: currentSection.trim(), type: currentType }) 202 | } 203 | currentSection = line 204 | currentType = 'function' 205 | } else if (line.startsWith('type ')) { 206 | if (currentSection) { 207 | sections.push({ content: currentSection.trim(), type: currentType }) 208 | } 209 | currentSection = line 210 | currentType = 'type' 211 | } else if (line.startsWith('var ') || line.startsWith('const ')) { 212 | if (currentSection) { 213 | sections.push({ content: currentSection.trim(), type: currentType }) 214 | } 215 | currentSection = line 216 | currentType = 'variable' 217 | } else { 218 | currentSection += '\n' + line 219 | } 220 | } 221 | 222 | if (currentSection) { 223 | sections.push({ content: currentSection.trim(), type: currentType }) 224 | } 225 | 226 | return sections 227 | } 228 | 229 | /** 230 | * Parse Python documentation into sections 231 | */ 232 | public parsePythonDoc(doc: string): Array<{ content: string; type: string }> { 233 | const sections: Array<{ content: string; type: string }> = [] 234 | let currentSection = '' 235 | let currentType = 'description' 236 | 237 | const lines = doc.split('\n') 238 | for (const line of lines) { 239 | if (line.startsWith('class ')) { 240 | if (currentSection) { 241 | sections.push({ content: currentSection.trim(), type: currentType }) 242 | } 243 | currentSection = line 244 | currentType = 'class' 245 | } else if (line.startsWith('def ')) { 246 | if (currentSection) { 247 | sections.push({ content: currentSection.trim(), type: currentType }) 248 | } 249 | currentSection = line 250 | currentType = 'function' 251 | } else if (line.match(/^[A-Z_]+\s*=/)) { 252 | if (currentSection) { 253 | sections.push({ content: currentSection.trim(), type: currentType }) 254 | } 255 | currentSection = line 256 | currentType = 'constant' 257 | } else { 258 | currentSection += '\n' + line 259 | } 260 | } 261 | 262 | if (currentSection) { 263 | sections.push({ content: currentSection.trim(), type: currentType }) 264 | } 265 | 266 | return sections 267 | } 268 | 269 | /** 270 | * Parse Swift documentation into sections 271 | */ 272 | public parseSwiftDoc(doc: string): Array<{ content: string; type: string }> { 273 | const sections: Array<{ content: string; type: string }> = [] 274 | let currentSection = '' 275 | let currentType = 'description' 276 | 277 | const lines = doc.split('\n') 278 | for (const line of lines) { 279 | if (line.startsWith('class ') || line.startsWith('struct ') || line.startsWith('enum ') || line.startsWith('protocol ')) { 280 | if (currentSection) { 281 | sections.push({ content: currentSection.trim(), type: currentType }) 282 | } 283 | currentSection = line 284 | currentType = 'type' 285 | } else if (line.startsWith('func ')) { 286 | if (currentSection) { 287 | sections.push({ content: currentSection.trim(), type: currentType }) 288 | } 289 | currentSection = line 290 | currentType = 'function' 291 | } else if (line.startsWith('var ') || line.startsWith('let ')) { 292 | if (currentSection) { 293 | sections.push({ content: currentSection.trim(), type: currentType }) 294 | } 295 | currentSection = line 296 | currentType = 'property' 297 | } else if (line.startsWith('extension ')) { 298 | if (currentSection) { 299 | sections.push({ content: currentSection.trim(), type: currentType }) 300 | } 301 | currentSection = line 302 | currentType = 'extension' 303 | } else { 304 | currentSection += '\n' + line 305 | } 306 | } 307 | 308 | if (currentSection) { 309 | sections.push({ content: currentSection.trim(), type: currentType }) 310 | } 311 | 312 | return sections 313 | } 314 | 315 | /** 316 | * Parse NPM documentation into sections 317 | */ 318 | public parseNpmDoc(data: { description?: string; readme?: string }): Array<{ content: string; type: string }> { 319 | const sections: Array<{ content: string; type: string }> = [] 320 | 321 | // Add package description 322 | if (data.description) { 323 | sections.push({ 324 | content: data.description, 325 | type: 'description' 326 | }) 327 | } 328 | 329 | // Parse README into sections 330 | if (data.readme) { 331 | const readmeSections = data.readme.split(/(?=^#+ )/m) 332 | for (const section of readmeSections) { 333 | const lines = section.split('\n') 334 | const heading = lines[0] 335 | const content = lines.slice(1).join('\n').trim() 336 | 337 | if (content) { 338 | // Skip sections that are likely not useful for coding 339 | const lowerHeading = heading.toLowerCase() 340 | if ( 341 | lowerHeading.includes('sponsor') || 342 | lowerHeading.includes('author') || 343 | lowerHeading.includes('contributor') || 344 | lowerHeading.includes('license') || 345 | lowerHeading.includes('changelog') || 346 | lowerHeading.includes('people') || 347 | lowerHeading.includes('community') || 348 | lowerHeading.includes('triager') || 349 | lowerHeading.includes('tc ') || 350 | lowerHeading.includes('committee') || 351 | lowerHeading.includes('security') || 352 | lowerHeading.includes('test') || 353 | lowerHeading.includes('contributing') 354 | ) { 355 | continue 356 | } 357 | 358 | let type = 'general' 359 | if (lowerHeading.includes('install')) type = 'installation' 360 | else if (lowerHeading.includes('usage') || lowerHeading.includes('api')) type = 'usage' 361 | else if (lowerHeading.includes('example')) type = 'example' 362 | else if (lowerHeading.includes('config')) type = 'configuration' 363 | else if (lowerHeading.includes('method') || lowerHeading.includes('function')) type = 'api' 364 | else if (lowerHeading.includes('quick start')) type = 'quickstart' 365 | else if (lowerHeading.includes('getting started')) type = 'quickstart' 366 | 367 | sections.push({ 368 | content: `${heading}\n${content}`, 369 | type 370 | }) 371 | } 372 | } 373 | } 374 | 375 | return sections 376 | } 377 | 378 | /** 379 | * Extract documentation sections from README content 380 | * Identifies and extracts key sections like usage, API, examples, etc. 381 | */ 382 | public extractDocSections(readme: string): { 383 | usage: string 384 | api: string 385 | examples: string 386 | configuration: string 387 | other: Record 388 | } { 389 | const result = { 390 | usage: '', 391 | api: '', 392 | examples: '', 393 | configuration: '', 394 | other: {} as Record 395 | } 396 | 397 | // Split the readme into sections based on headings 398 | const sections = readme.split(/(?=^#+\s+)/m) 399 | 400 | // Process each section 401 | for (const section of sections) { 402 | if (!section.trim()) continue 403 | 404 | const lines = section.split('\n') 405 | const heading = lines[0].toLowerCase() 406 | const content = lines.slice(1).join('\n').trim() 407 | 408 | if (!content) continue 409 | 410 | // Skip sections that are likely not useful for coding 411 | if ( 412 | heading.includes('sponsor') || 413 | heading.includes('author') || 414 | heading.includes('contributor') || 415 | heading.includes('license') || 416 | heading.includes('changelog') || 417 | heading.includes('people') || 418 | heading.includes('community') || 419 | heading.includes('triager') || 420 | heading.includes('tc ') || 421 | heading.includes('committee') || 422 | heading.includes('security') || 423 | heading.includes('test') || 424 | heading.includes('contributing') || 425 | heading.includes('badge') || 426 | heading.includes('build status') || 427 | heading.includes('coverage') || 428 | heading.includes('donate') 429 | ) { 430 | continue 431 | } 432 | 433 | // Categorize the section based on its heading 434 | if ( 435 | heading.includes('usage') || 436 | heading.includes('getting started') || 437 | heading.includes('quick start') 438 | ) { 439 | result.usage = content 440 | } 441 | else if ( 442 | heading.includes('api') || 443 | heading.includes('method') || 444 | heading.includes('function') || 445 | heading.includes('class') || 446 | heading.includes('interface') || 447 | heading.includes('request config') || 448 | heading.includes('response schema') || 449 | heading.includes('config defaults') || 450 | heading.includes('interceptors') 451 | ) { 452 | // If we already have API content, append this section 453 | if (result.api) { 454 | result.api += '\n\n## ' + lines[0].replace(/^#+\s+/, '') + '\n\n' + content 455 | } else { 456 | result.api = content 457 | } 458 | } 459 | else if ( 460 | heading.includes('example') || 461 | heading.includes('demo') 462 | ) { 463 | // If we already have examples content, append this section 464 | if (result.examples) { 465 | result.examples += '\n\n## ' + lines[0].replace(/^#+\s+/, '') + '\n\n' + content 466 | } else { 467 | result.examples = content 468 | } 469 | } 470 | else if ( 471 | heading.includes('config') || 472 | heading.includes('option') || 473 | heading.includes('setting') 474 | ) { 475 | result.configuration = content 476 | } 477 | // Store other potentially useful sections 478 | else if ( 479 | heading.includes('feature') || 480 | heading.includes('overview') || 481 | heading.includes('guide') || 482 | heading.includes('tutorial') || 483 | heading.includes('how to') || 484 | heading.includes('advanced') || 485 | heading.includes('request') || 486 | heading.includes('response') || 487 | heading.includes('error') || 488 | heading.includes('handling') || 489 | heading.includes('interceptor') || 490 | heading.includes('middleware') || 491 | heading.includes('plugin') 492 | ) { 493 | // Extract section name from heading (remove # characters) 494 | const sectionName = lines[0].replace(/^#+\s+/, '') 495 | result.other[sectionName] = content 496 | } 497 | } 498 | 499 | // If we couldn't find API sections by heading, try to find them by content 500 | if (!result.api) { 501 | // Look for code blocks that might contain API usage 502 | const apiCodeBlocks = readme.match(/```(?:js|javascript)[\s\S]*?axios\.(?:get|post|put|delete|patch|request)[\s\S]*?```/g) 503 | if (apiCodeBlocks && apiCodeBlocks.length > 0) { 504 | result.api = "## API Usage Examples\n\n" + apiCodeBlocks.slice(0, 3).join('\n\n') 505 | } 506 | 507 | // Look for sections that might describe request/response objects 508 | if (readme.includes('axios.request(config)') || readme.includes('axios(config)')) { 509 | const configSection = readme.match(/(?:Request|Config) (?:Config|Options)[\s\S]*?```[\s\S]*?```/i) 510 | if (configSection) { 511 | result.api += "\n\n## Request Config\n\n" + configSection[0] 512 | } 513 | } 514 | } 515 | 516 | return result 517 | } 518 | 519 | /** 520 | * Extract only the most relevant content from a README for coding purposes 521 | */ 522 | public extractRelevantContent(readme: string): string { 523 | this.logger.debug("Extracting relevant content from README") 524 | 525 | // First, remove all badge links and reference-style links 526 | const cleanedReadme = readme 527 | .replace(/\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)/g, '') // Remove badge links 528 | .replace(/\[[^\]]*\]:\s*https?:\/\/[^\s]+/g, '') // Remove reference links 529 | 530 | // Split the readme into sections 531 | const sections = cleanedReadme.split(/(?=^#+ )/m) 532 | this.logger.debug(`Found ${sections.length} sections in README`) 533 | 534 | // Always include the first code example if it exists 535 | const firstCodeExample = readme.match(/```[\s\S]*?```/) 536 | let hasIncludedCodeExample = false 537 | 538 | const relevantSections: string[] = [] 539 | 540 | // Process the content before any headings (intro) 541 | if (sections.length > 0 && !sections[0].startsWith('#')) { 542 | // Include the intro section, but limit to first few paragraphs 543 | const introParagraphs = sections[0].split('\n\n') 544 | const introContent = introParagraphs.slice(0, Math.min(3, introParagraphs.length)).join('\n\n') 545 | if (introContent.trim()) { 546 | relevantSections.push(introContent.trim()) 547 | this.logger.debug("Added intro section") 548 | } 549 | 550 | // If there's a code example in the intro, include it 551 | if (firstCodeExample && sections[0].includes(firstCodeExample[0])) { 552 | hasIncludedCodeExample = true 553 | } 554 | } 555 | 556 | // Define keywords for sections we want to keep 557 | const usefulKeywords = [ 558 | 'install', 'usage', 'api', 'example', 'quick start', 'getting started', 559 | 'guide', 'method', 'function', 'config', 'option', 'feature', 'overview', 560 | 'basic', 'tutorial', 'how to' 561 | ] 562 | 563 | // Define keywords for sections we want to skip 564 | const skipKeywords = [ 565 | 'sponsor', 'author', 'contributor', 'license', 'changelog', 'people', 566 | 'community', 'triager', 'tc ', 'committee', 'security', 'test', 567 | 'contributing', 'badge', 'build status', 'coverage', 'donate', 568 | 'acknowledgement', 'credit', 'support', 'backers', 'funding' 569 | ] 570 | 571 | // Process each section with a heading 572 | for (let i = 0; i < sections.length; i++) { 573 | const section = sections[i] 574 | if (!section.startsWith('#')) continue 575 | 576 | const lines = section.split('\n') 577 | const heading = lines[0].toLowerCase() 578 | 579 | // Skip sections that are likely not useful for coding 580 | let shouldSkip = false 581 | for (const keyword of skipKeywords) { 582 | if (heading.includes(keyword)) { 583 | shouldSkip = true 584 | break 585 | } 586 | } 587 | if (shouldSkip) { 588 | this.logger.debug(`Skipping section: ${heading}`) 589 | continue 590 | } 591 | 592 | // Include sections that are likely useful for coding 593 | let shouldInclude = false 594 | for (const keyword of usefulKeywords) { 595 | if (heading.includes(keyword)) { 596 | shouldInclude = true 597 | this.logger.debug(`Including section due to keyword match: ${heading} (matched: ${keyword})`) 598 | break 599 | } 600 | } 601 | 602 | // Also include sections with code examples even if they don't match keywords 603 | if (!shouldInclude && section.includes('```')) { 604 | shouldInclude = true 605 | this.logger.debug(`Including section due to code example: ${heading}`) 606 | } 607 | 608 | // If this is a short section with a simple heading (likely important), include it 609 | if (!shouldInclude && section.length < 500 && heading.split(' ').length <= 3) { 610 | shouldInclude = true 611 | this.logger.debug(`Including short section with simple heading: ${heading}`) 612 | } 613 | 614 | if (shouldInclude) { 615 | relevantSections.push(section) 616 | } 617 | } 618 | 619 | // If we didn't find any relevant sections with headings, be less strict 620 | if (relevantSections.length <= 1) { 621 | this.logger.debug("Few relevant sections found, being less strict") 622 | 623 | // Include any section with a code example 624 | for (let i = 0; i < sections.length; i++) { 625 | const section = sections[i] 626 | if (!section.startsWith('#')) continue 627 | 628 | if (section.includes('```') && !relevantSections.includes(section)) { 629 | relevantSections.push(section) 630 | this.logger.debug(`Added section with code example`) 631 | } 632 | } 633 | 634 | // If still no sections, include the first few sections regardless 635 | if (relevantSections.length <= 1) { 636 | for (let i = 0; i < Math.min(3, sections.length); i++) { 637 | if (sections[i].startsWith('#') && !relevantSections.includes(sections[i])) { 638 | relevantSections.push(sections[i]) 639 | this.logger.debug(`Added section ${i} as fallback`) 640 | } 641 | } 642 | } 643 | } 644 | 645 | // If we still don't have any code examples, add the first one we found 646 | if (!hasIncludedCodeExample && firstCodeExample) { 647 | relevantSections.push(`## Code Example\n\n${firstCodeExample[0]}`) 648 | this.logger.debug("Added first code example") 649 | } 650 | 651 | // If we still have nothing, just return a portion of the original README 652 | if (relevantSections.length === 0) { 653 | this.logger.debug("No relevant sections found, returning truncated README") 654 | // Return the first 2000 characters of the README 655 | return readme.substring(0, 2000) + "... (truncated)" 656 | } 657 | 658 | // Join the relevant sections 659 | let content = relevantSections.join('\n\n') 660 | 661 | // Remove any remaining badge links or reference links that might be in the content 662 | content = content 663 | .replace(/\[!\[[^\]]*\]\([^)]*\)\]\([^)]*\)/g, '') 664 | .replace(/\[[^\]]*\]:\s*https?:\/\/[^\s]+/g, '') 665 | 666 | this.logger.debug(`Extracted ${content.length} characters of relevant content`) 667 | 668 | return content 669 | } 670 | } 671 | -------------------------------------------------------------------------------- /src/tool-handlers.ts: -------------------------------------------------------------------------------- 1 | import TypeScriptLspClient from './lsp/typescript-lsp-client.js' 2 | 3 | /** 4 | * Get tool definitions for the package docs server 5 | */ 6 | export function getToolDefinitions(lspEnabled: boolean, lspClient: TypeScriptLspClient | undefined) { 7 | // Define the main tools 8 | const baseTools = [ 9 | { 10 | name: "search_package_docs", 11 | description: "Search for symbols or content within package documentation", 12 | inputSchema: { 13 | type: "object", 14 | properties: { 15 | package: { 16 | type: "string", 17 | description: "Package name to search within" 18 | }, 19 | query: { 20 | type: "string", 21 | description: "Search query" 22 | }, 23 | language: { 24 | type: "string", 25 | enum: ["go", "python", "npm", "swift", "rust"], 26 | description: "Package language/ecosystem" 27 | }, 28 | fuzzy: { 29 | type: "boolean", 30 | description: "Enable fuzzy matching", 31 | default: true 32 | }, 33 | projectPath: { 34 | type: "string", 35 | description: "Optional path to project directory for local .npmrc files" 36 | } 37 | }, 38 | required: ["package", "query", "language"] 39 | } 40 | }, 41 | { 42 | name: "describe_go_package", 43 | description: "Get a brief description of a Go package", 44 | inputSchema: { 45 | type: "object", 46 | properties: { 47 | package: { 48 | type: "string", 49 | description: "Full package import path (e.g. encoding/json)", 50 | }, 51 | symbol: { 52 | type: "string", 53 | description: 54 | "Optional symbol name to look up specific documentation", 55 | }, 56 | projectPath: { 57 | type: "string", 58 | description: "Optional path to project directory for local .npmrc files" 59 | } 60 | }, 61 | required: ["package"], 62 | }, 63 | }, 64 | { 65 | name: "describe_rust_package", 66 | description: "Get a brief description of a Rust package", 67 | inputSchema: { 68 | type: "object", 69 | properties: { 70 | package: { 71 | type: "string", 72 | description: "Crate name (e.g. serde)", 73 | }, 74 | version: { 75 | type: "string", 76 | description: 77 | "Optional crate version", 78 | }, 79 | }, 80 | required: ["package"], 81 | }, 82 | }, 83 | { 84 | name: "describe_python_package", 85 | description: "Get a brief description of a Python package", 86 | inputSchema: { 87 | type: "object", 88 | properties: { 89 | package: { 90 | type: "string", 91 | description: "Package name (e.g. requests)", 92 | }, 93 | symbol: { 94 | type: "string", 95 | description: 96 | "Optional symbol name to look up specific documentation", 97 | }, 98 | projectPath: { 99 | type: "string", 100 | description: "Optional path to project directory for local .npmrc files" 101 | } 102 | }, 103 | required: ["package"], 104 | }, 105 | }, 106 | { 107 | name: "describe_npm_package", 108 | description: "Get a brief description of an NPM package", 109 | inputSchema: { 110 | type: "object", 111 | properties: { 112 | package: { 113 | type: "string", 114 | description: "Package name (e.g. axios)", 115 | }, 116 | version: { 117 | type: "string", 118 | description: "Optional package version", 119 | }, 120 | projectPath: { 121 | type: "string", 122 | description: "Optional path to project directory for local .npmrc files" 123 | } 124 | }, 125 | required: ["package"], 126 | }, 127 | }, 128 | { 129 | name: "describe_swift_package", 130 | description: "Get a brief description of a Swift package", 131 | inputSchema: { 132 | type: "object", 133 | properties: { 134 | package: { 135 | type: "string", 136 | description: "Package URL (e.g. https://github.com/apple/swift-argument-parser)", 137 | }, 138 | symbol: { 139 | type: "string", 140 | description: "Optional symbol name to look up specific documentation", 141 | }, 142 | projectPath: { 143 | type: "string", 144 | description: "Optional path to project directory for Package.swift file" 145 | } 146 | }, 147 | required: ["package"], 148 | }, 149 | }, 150 | { 151 | name: "get_npm_package_doc", 152 | description: "Get full documentation for an NPM package", 153 | inputSchema: { 154 | type: "object", 155 | properties: { 156 | package: { 157 | type: "string", 158 | description: "Package name (e.g. axios)", 159 | }, 160 | version: { 161 | type: "string", 162 | description: "Optional package version", 163 | }, 164 | projectPath: { 165 | type: "string", 166 | description: "Optional path to project directory for local .npmrc files" 167 | }, 168 | section: { 169 | type: "string", 170 | description: "Optional section to retrieve (e.g. 'installation', 'api', 'examples')" 171 | }, 172 | maxLength: { 173 | type: "number", 174 | description: "Optional maximum length of the returned documentation" 175 | }, 176 | query: { 177 | type: "string", 178 | description: "Optional search query to filter documentation content" 179 | } 180 | }, 181 | required: ["package"], 182 | }, 183 | }, 184 | ] 185 | 186 | // Add legacy tools for backward compatibility 187 | const legacyTools = [ 188 | { 189 | name: "lookup_go_doc", 190 | description: "[DEPRECATED] Use describe_go_package instead. Get a brief description of a Go package", 191 | inputSchema: { 192 | type: "object", 193 | properties: { 194 | package: { 195 | type: "string", 196 | description: "Full package import path (e.g. encoding/json)", 197 | }, 198 | symbol: { 199 | type: "string", 200 | description: 201 | "Optional symbol name to look up specific documentation", 202 | }, 203 | projectPath: { 204 | type: "string", 205 | description: "Optional path to project directory for local .npmrc files" 206 | } 207 | }, 208 | required: ["package"], 209 | }, 210 | }, 211 | { 212 | name: "lookup_python_doc", 213 | description: "[DEPRECATED] Use describe_python_package instead. Get a brief description of a Python package", 214 | inputSchema: { 215 | type: "object", 216 | properties: { 217 | package: { 218 | type: "string", 219 | description: "Package name (e.g. requests)", 220 | }, 221 | symbol: { 222 | type: "string", 223 | description: 224 | "Optional symbol name to look up specific documentation", 225 | }, 226 | projectPath: { 227 | type: "string", 228 | description: "Optional path to project directory for local .npmrc files" 229 | } 230 | }, 231 | required: ["package"], 232 | }, 233 | }, 234 | { 235 | name: "lookup_npm_doc", 236 | description: "[DEPRECATED] Use describe_npm_package instead. Get a brief description of an NPM package", 237 | inputSchema: { 238 | type: "object", 239 | properties: { 240 | package: { 241 | type: "string", 242 | description: "Package name (e.g. axios)", 243 | }, 244 | version: { 245 | type: "string", 246 | description: "Optional package version", 247 | }, 248 | projectPath: { 249 | type: "string", 250 | description: "Optional path to project directory for local .npmrc files" 251 | } 252 | }, 253 | required: ["package"], 254 | }, 255 | }, 256 | ] 257 | 258 | // Combine main tools with legacy tools 259 | const allTools = [...baseTools, ...legacyTools] 260 | 261 | // Add LSP tools if enabled 262 | if (lspEnabled && lspClient) { 263 | const lspTools = [ 264 | { 265 | name: "get_hover", 266 | description: "Get hover information for a position in a document using Language Server Protocol", 267 | inputSchema: { 268 | type: "object", 269 | properties: { 270 | languageId: { 271 | type: "string", 272 | description: "The language identifier (e.g., 'typescript', 'javascript')" 273 | }, 274 | filePath: { 275 | type: "string", 276 | description: "Absolute or relative path to the source file" 277 | }, 278 | content: { 279 | type: "string", 280 | description: "The current content of the file" 281 | }, 282 | line: { 283 | type: "number", 284 | description: "Zero-based line number for hover position" 285 | }, 286 | character: { 287 | type: "number", 288 | description: "Zero-based character offset for hover position" 289 | }, 290 | projectRoot: { 291 | type: "string", 292 | description: "Root directory of the project for resolving imports and node_modules" 293 | }, 294 | }, 295 | required: ["languageId", "filePath", "content", "line", "character"], 296 | }, 297 | }, 298 | { 299 | name: "get_completions", 300 | description: "Get completion suggestions for a position in a document using Language Server Protocol", 301 | inputSchema: { 302 | type: "object", 303 | properties: { 304 | languageId: { 305 | type: "string", 306 | description: "The language identifier (e.g., 'typescript', 'javascript')" 307 | }, 308 | filePath: { 309 | type: "string", 310 | description: "Absolute or relative path to the source file" 311 | }, 312 | content: { 313 | type: "string", 314 | description: "The current content of the file" 315 | }, 316 | line: { 317 | type: "number", 318 | description: "Zero-based line number for completion position" 319 | }, 320 | character: { 321 | type: "number", 322 | description: "Zero-based character offset for completion position" 323 | }, 324 | projectRoot: { 325 | type: "string", 326 | description: "Root directory of the project for resolving imports and node_modules" 327 | }, 328 | }, 329 | required: ["languageId", "filePath", "content", "line", "character"], 330 | }, 331 | }, 332 | { 333 | name: "get_diagnostics", 334 | description: "Get diagnostic information for a document using Language Server Protocol", 335 | inputSchema: { 336 | type: "object", 337 | properties: { 338 | languageId: { 339 | type: "string", 340 | description: "The language identifier (e.g., 'typescript', 'javascript')" 341 | }, 342 | filePath: { 343 | type: "string", 344 | description: "Absolute or relative path to the source file" 345 | }, 346 | content: { 347 | type: "string", 348 | description: "The current content of the file" 349 | }, 350 | projectRoot: { 351 | type: "string", 352 | description: "Root directory of the project for resolving imports and node_modules" 353 | }, 354 | }, 355 | required: ["languageId", "filePath", "content"], 356 | }, 357 | }, 358 | ] 359 | 360 | return { tools: [...allTools, ...lspTools] } 361 | } 362 | 363 | return { tools: allTools } 364 | } 365 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Types for docs.rs integration 3 | */ 4 | 5 | export interface CrateInfo { 6 | name: string; 7 | version: string; 8 | description?: string; 9 | } 10 | 11 | export interface CrateSearchResult { 12 | crates: CrateInfo[]; 13 | totalCount: number; 14 | } 15 | 16 | export interface RustType { 17 | name: string; 18 | kind: 19 | | "struct" 20 | | "enum" 21 | | "trait" 22 | | "function" 23 | | "macro" 24 | | "type" 25 | | "module" 26 | | "other"; 27 | path: string; 28 | description?: string; 29 | sourceUrl?: string; 30 | documentationUrl: string; 31 | } 32 | 33 | export interface FeatureFlag { 34 | name: string; 35 | description?: string; 36 | enabled: boolean; 37 | } 38 | 39 | export interface CrateVersion { 40 | version: string; 41 | isYanked: boolean; 42 | releaseDate?: string; 43 | } 44 | 45 | export interface SymbolDefinition { 46 | name: string; 47 | kind: string; 48 | path: string; 49 | sourceCode?: string; 50 | documentationHtml?: string; 51 | } 52 | 53 | export interface SearchOptions { 54 | query: string; 55 | page?: number; 56 | perPage?: number; 57 | } 58 | 59 | export interface RustDocArgs { 60 | crateName: string; 61 | version?: string; 62 | } 63 | 64 | export interface RustCrateSearchArgs { 65 | query: string; 66 | page?: number; 67 | perPage?: number; 68 | } 69 | 70 | export function isRustDocArgs(args: unknown): args is RustDocArgs { 71 | return typeof args === 'object' && args !== null && 72 | typeof (args as RustDocArgs).crateName === 'string' && 73 | ((args as RustDocArgs).version === undefined || typeof (args as RustDocArgs).version === 'string'); 74 | } 75 | 76 | export function isRustCrateSearchArgs(args: unknown): args is RustCrateSearchArgs { 77 | return typeof args === 'object' && args !== null && 78 | typeof (args as RustCrateSearchArgs).query === 'string' && 79 | ((args as RustCrateSearchArgs).page === undefined || typeof (args as RustCrateSearchArgs).page === 'number') && 80 | ((args as RustCrateSearchArgs).perPage === undefined || typeof (args as RustCrateSearchArgs).perPage === 'number'); 81 | } 82 | -------------------------------------------------------------------------------- /src/utils/rust-http-client.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../logger.js'; 2 | 3 | interface RequestOptions { 4 | method?: string; 5 | params?: Record; 6 | body?: unknown; 7 | } 8 | 9 | type FetchResponse = 10 | | { 11 | data: Record; 12 | status: number; 13 | headers: Headers; 14 | contentType: "json"; 15 | } 16 | | { 17 | data: string; 18 | status: number; 19 | headers: Headers; 20 | contentType: "text"; 21 | }; 22 | 23 | // Base configurations for crates.io and docs.rs 24 | const CRATES_IO_CONFIG = { 25 | baseURL: "https://crates.io/api/v1/", 26 | headers: { 27 | Accept: "application/json", 28 | "User-Agent": "mcp-package-docs/1.0.0 (Rust Docs)", // Use a descriptive user agent 29 | }, 30 | }; 31 | 32 | const DOCS_RS_CONFIG = { 33 | baseURL: "https://docs.rs", 34 | headers: { 35 | Accept: "text/html,application/xhtml+xml,application/json", 36 | "User-Agent": "mcp-package-docs/1.0.0 (Rust Docs)", // Use a descriptive user agent 37 | }, 38 | }; 39 | 40 | // Helper to build full URL with query params 41 | function buildUrl( 42 | baseURL: string, 43 | path: string, 44 | params?: Record, 45 | ): string { 46 | const url = new URL(path, baseURL); 47 | if (params) { 48 | for (const [key, value] of Object.entries(params)) { 49 | if (value !== undefined) { 50 | url.searchParams.append(key, String(value)); 51 | } 52 | } 53 | } 54 | return url.toString(); 55 | } 56 | 57 | // Create a configured fetch client 58 | async function rustFetch( 59 | baseURL: string, 60 | path: string, 61 | options: RequestOptions = {}, 62 | ): Promise { 63 | const { method = "GET", params, body } = options; 64 | const url = buildUrl(baseURL, path, params); 65 | 66 | try { 67 | logger.debug(`Making request to ${url}`, { method, params }); 68 | 69 | const controller = new AbortController(); 70 | const timeoutId = setTimeout(() => controller.abort(), 10000); // 10-second timeout 71 | 72 | const response = await fetch(url, { 73 | method, 74 | headers: baseURL === CRATES_IO_CONFIG.baseURL ? CRATES_IO_CONFIG.headers : DOCS_RS_CONFIG.headers, 75 | body: body ? JSON.stringify(body) : undefined, 76 | signal: controller.signal, 77 | }); 78 | 79 | clearTimeout(timeoutId); 80 | 81 | logger.debug(`Received response from ${url}`, { 82 | status: response.status, 83 | contentType: response.headers.get("content-type"), 84 | }); 85 | 86 | if (!response.ok) { 87 | throw new Error(`HTTP error! status: ${response.status}`); 88 | } 89 | 90 | const contentType = response.headers.get("content-type"); 91 | const isJson = contentType?.includes("application/json"); 92 | const data = isJson ? await response.json() : await response.text(); 93 | 94 | return { 95 | data, 96 | status: response.status, 97 | headers: response.headers, 98 | contentType: isJson ? "json" : "text", 99 | }; 100 | } catch (error) { 101 | logger.error(`Error making request to ${url}`, { error }); 102 | throw error; 103 | } 104 | } 105 | 106 | // Export a default instance with methods for crates.io and docs.rs 107 | export default { 108 | cratesIoFetch: (path: string, options = {}) => 109 | rustFetch(CRATES_IO_CONFIG.baseURL, path, { ...options, method: "GET" }), 110 | docsRsFetch: (path: string, options = {}) => 111 | rustFetch(DOCS_RS_CONFIG.baseURL, path, { ...options, method: "GET" }), 112 | }; 113 | -------------------------------------------------------------------------------- /test-go-docs-direct.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import axios from 'axios'; 3 | import { promisify } from 'util'; 4 | import { exec } from 'child_process'; 5 | 6 | const execAsync = promisify(exec); 7 | 8 | // Mock logger 9 | const logger = { 10 | debug: (...args) => console.log('[DEBUG]', ...args), 11 | info: (...args) => console.log('[INFO]', ...args), 12 | error: (...args) => console.error('[ERROR]', ...args), 13 | child: () => logger 14 | }; 15 | 16 | /** 17 | * Test function to fetch Go package documentation 18 | */ 19 | async function testGoPackageDoc(packageName) { 20 | console.log(`Testing Go package documentation retrieval for: ${packageName}`); 21 | 22 | try { 23 | // First try using go doc command (works for standard library and cached modules) 24 | try { 25 | console.log('Attempting to use go doc command...'); 26 | const cmd = `go doc ${packageName}`; 27 | const { stdout } = await execAsync(cmd); 28 | console.log('Successfully retrieved documentation using go doc command:'); 29 | console.log('---'); 30 | console.log(stdout); 31 | console.log('---'); 32 | return; 33 | } catch (cmdError) { 34 | console.log(`go doc command failed: ${cmdError.message}`); 35 | } 36 | 37 | // If go doc command fails, try to fetch from pkg.go.dev API 38 | try { 39 | console.log('Attempting to fetch from pkg.go.dev API...'); 40 | const url = `https://pkg.go.dev/api/packages/${encodeURIComponent(packageName)}`; 41 | console.log(`Fetching from: ${url}`); 42 | 43 | const response = await axios.get(url); 44 | 45 | if (response.data) { 46 | console.log('Successfully retrieved documentation from pkg.go.dev API:'); 47 | console.log('---'); 48 | console.log('Synopsis:', response.data.Synopsis || 'None'); 49 | console.log('Documentation available:', response.data.Documentation ? 'Yes' : 'No'); 50 | console.log('---'); 51 | return; 52 | } 53 | } catch (apiError) { 54 | console.log(`Error fetching from pkg.go.dev API: ${apiError.message}`); 55 | } 56 | 57 | // If API fails, try web scraping approach 58 | try { 59 | console.log('Attempting to fetch documentation from pkg.go.dev website...'); 60 | const url = `https://pkg.go.dev/${encodeURIComponent(packageName)}`; 61 | console.log(`Fetching from: ${url}`); 62 | 63 | const response = await axios.get(url); 64 | 65 | if (response.data) { 66 | console.log('Successfully retrieved HTML from pkg.go.dev website'); 67 | 68 | // Simple extraction of package description 69 | const descriptionMatch = response.data.match(/ { 91 | console.log('\n\nTesting with a well-known package:'); 92 | // Test with a well-known package 93 | return testGoPackageDoc('github.com/spf13/cobra'); 94 | }) 95 | .then(() => console.log('Test completed')) 96 | .catch(err => console.error('Test failed:', err)); 97 | -------------------------------------------------------------------------------- /test-lsp-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import TypeScriptLspClient from './build/lsp/typescript-lsp-client.js'; 3 | 4 | // Sample TypeScript code with a type error 5 | const sampleCode = ` 6 | // Test file for TypeScript language server functionality 7 | import * as fs from 'fs'; 8 | 9 | // Define a class 10 | class Person { 11 | name: string; 12 | age: number; 13 | 14 | constructor(name: string, age: number) { 15 | this.name = name; 16 | this.age = age; 17 | } 18 | 19 | greet(): string { 20 | return \`Hello, my name is \${this.name} and I am \${this.age} years old.\`; 21 | } 22 | } 23 | 24 | // Create an instance of the class 25 | const person = new Person('John', 30); 26 | 27 | // Call a method on the instance 28 | const greeting = person.greet(); 29 | 30 | // Type error for testing diagnostics 31 | const stringValue: string = 42; 32 | `; 33 | 34 | async function main() { 35 | console.log('Creating TypeScript LSP client...'); 36 | const lspClient = new TypeScriptLspClient(); 37 | 38 | try { 39 | // Test diagnostics functionality 40 | console.log('\n--- Testing diagnostics functionality ---'); 41 | const diagnosticsResult = await lspClient.getDiagnostics( 42 | 'typescript', 43 | 'test-file.ts', 44 | sampleCode, 45 | process.cwd() 46 | ); 47 | console.log('Diagnostics result:', JSON.stringify(diagnosticsResult, null, 2)); 48 | 49 | // Success! 50 | console.log('\n--- Test completed successfully ---'); 51 | console.log('The TypeScript language server is working correctly!'); 52 | console.log('You can now use the LSP tools in the MCP server.'); 53 | 54 | } catch (error) { 55 | console.error('Error testing LSP functionality:', error); 56 | } finally { 57 | // Clean up 58 | lspClient.cleanup(); 59 | } 60 | } 61 | 62 | main().catch(console.error); 63 | -------------------------------------------------------------------------------- /test-lsp.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import TypeScriptLspClient from './build/lsp/typescript-lsp-client.js'; 3 | 4 | // Sample TypeScript code to test 5 | const sampleCode = ` 6 | // Test file for TypeScript language server functionality 7 | import * as fs from 'fs'; 8 | 9 | // Define a class 10 | class Person { 11 | name: string; 12 | age: number; 13 | 14 | constructor(name: string, age: number) { 15 | this.name = name; 16 | this.age = age; 17 | } 18 | 19 | greet(): string { 20 | return \`Hello, my name is \${this.name} and I am \${this.age} years old.\`; 21 | } 22 | } 23 | 24 | // Create an instance of the class 25 | const person = new Person('John', 30); 26 | 27 | // Call a method on the instance 28 | const greeting = person.greet(); 29 | 30 | // Use the fs module 31 | const fileContent = fs.readFileSync('test-typescript.ts', 'utf-8'); 32 | 33 | // Array with methods 34 | const numbers = [1, 2, 3, 4, 5]; 35 | numbers.map(n => n * 2); 36 | 37 | // Type error for testing diagnostics 38 | const stringValue: string = 42; 39 | `; 40 | 41 | async function testLsp() { 42 | console.log('Creating TypeScript LSP client...'); 43 | const lspClient = new TypeScriptLspClient(); 44 | 45 | try { 46 | // Test hover functionality 47 | console.log('\n--- Testing hover functionality ---'); 48 | const hoverResult = await lspClient.getHover( 49 | 'typescript', 50 | 'test-file.ts', 51 | sampleCode, 52 | 7, // Line for "name: string;" 53 | 6, // Character position for "name" 54 | process.cwd() 55 | ); 56 | console.log('Hover result:', JSON.stringify(hoverResult, null, 2)); 57 | 58 | // Test completions functionality 59 | console.log('\n--- Testing completions functionality ---'); 60 | const completionsResult = await lspClient.getCompletions( 61 | 'typescript', 62 | 'test-file.ts', 63 | sampleCode, 64 | 31, // Line for "numbers.map" 65 | 12, // Character position after "numbers." 66 | process.cwd() 67 | ); 68 | console.log('Completions result:', JSON.stringify(completionsResult.slice(0, 3), null, 2)); // Show first 3 completions 69 | 70 | // Test diagnostics functionality 71 | console.log('\n--- Testing diagnostics functionality ---'); 72 | const diagnosticsResult = await lspClient.getDiagnostics( 73 | 'typescript', 74 | 'test-file.ts', 75 | sampleCode, 76 | process.cwd() 77 | ); 78 | console.log('Diagnostics result:', JSON.stringify(diagnosticsResult, null, 2)); 79 | 80 | } catch (error) { 81 | console.error('Error testing LSP functionality:', error); 82 | } finally { 83 | // Clean up 84 | lspClient.cleanup(); 85 | } 86 | } 87 | 88 | testLsp().catch(console.error); 89 | -------------------------------------------------------------------------------- /test-npm-docs.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { NpmDocsEnhancer } from './build/npm-docs-enhancer.js'; 3 | import { NpmDocsHandler } from './build/npm-docs-integration.js'; 4 | import { logger } from './build/logger.js'; 5 | 6 | // Simple test script to verify the NPM documentation functionality 7 | 8 | async function testNpmDocs() { 9 | try { 10 | console.log('Testing NPM documentation functionality...'); 11 | 12 | // Test the NpmDocsEnhancer 13 | const enhancer = new NpmDocsEnhancer(logger); 14 | 15 | // Test fetching TypeScript definitions 16 | console.log('\nTesting TypeScript definition fetching...'); 17 | const typesContent = await enhancer.fetchTypeDefinition('axios'); 18 | console.log(`Fetched TypeScript definitions: ${typesContent ? 'Yes' : 'No'}`); 19 | 20 | if (typesContent) { 21 | // Test extracting API documentation 22 | console.log('\nTesting API documentation extraction...'); 23 | const apiDoc = await enhancer.extractApiDocumentation('axios', typesContent); 24 | console.log(`Extracted API documentation: ${apiDoc.exports.length} exports, ${apiDoc.types.length} types`); 25 | 26 | // Print a sample of the exports 27 | if (apiDoc.exports.length > 0) { 28 | console.log('\nSample exports:'); 29 | apiDoc.exports.slice(0, 3).forEach(exp => { 30 | console.log(`- ${exp.name} (${exp.type})`); 31 | }); 32 | } 33 | 34 | // Print a sample of the types 35 | if (apiDoc.types.length > 0) { 36 | console.log('\nSample types:'); 37 | apiDoc.types.slice(0, 3).forEach(type => { 38 | console.log(`- ${type.name} (${type.type})`); 39 | }); 40 | } 41 | } 42 | 43 | // Test fetching examples 44 | console.log('\nTesting example fetching...'); 45 | const examples = await enhancer.fetchExamples('axios'); 46 | console.log(`Fetched examples: ${examples.length}`); 47 | 48 | if (examples.length > 0) { 49 | console.log('\nFirst example snippet:'); 50 | console.log(examples[0].split('\n').slice(0, 5).join('\n') + '...'); 51 | } 52 | 53 | // Test the NpmDocsHandler with mock functions 54 | console.log('\nTesting NpmDocsHandler...'); 55 | const handler = new NpmDocsHandler(); 56 | 57 | // Mock functions 58 | const getRegistryConfigForPackage = () => ({ registry: 'https://registry.npmjs.org' }); 59 | const isNpmPackageInstalledLocally = () => false; 60 | const getLocalNpmDoc = () => ({ description: 'Mock local documentation' }); 61 | 62 | // Test describeNpmPackage 63 | console.log('\nTesting describeNpmPackage...'); 64 | const describeResult = await handler.describeNpmPackage( 65 | { package: 'axios', includeTypes: true, includeExamples: true }, 66 | getRegistryConfigForPackage, 67 | isNpmPackageInstalledLocally, 68 | getLocalNpmDoc 69 | ); 70 | 71 | console.log('Description:', describeResult.description); 72 | console.log('Has usage:', !!describeResult.usage); 73 | console.log('Has example:', !!describeResult.example); 74 | console.log('Has error:', !!describeResult.error); 75 | 76 | console.log('\nTest completed successfully!'); 77 | } catch (error) { 78 | console.error('Error during testing:', error); 79 | } 80 | } 81 | 82 | testNpmDocs(); 83 | -------------------------------------------------------------------------------- /test-typescript.ts: -------------------------------------------------------------------------------- 1 | // Test file for TypeScript language server functionality 2 | 3 | // Import a module 4 | import * as fs from 'fs'; 5 | 6 | // Define a class 7 | class Person { 8 | name: string; 9 | age: number; 10 | 11 | constructor(name: string, age: number) { 12 | this.name = name; 13 | this.age = age; 14 | } 15 | 16 | greet(): string { 17 | return `Hello, my name is ${this.name} and I am ${this.age} years old.`; 18 | } 19 | } 20 | 21 | // Create an instance of the class 22 | const person = new Person('John', 30); 23 | 24 | // Call a method on the instance 25 | const greeting = person.greet(); 26 | 27 | // Use the fs module 28 | const fileContent = fs.readFileSync('test-typescript.ts', 'utf-8'); 29 | 30 | // Array with methods 31 | const numbers = [1, 2, 3, 4, 5]; 32 | numbers.map(n => n * 2); 33 | -------------------------------------------------------------------------------- /tsconfig.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 | --------------------------------------------------------------------------------