├── .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 | [](https://smithery.ai/server/mcp-package-docs)
6 |
7 |
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 |
--------------------------------------------------------------------------------