├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── src └── index.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build output 5 | build/ 6 | 7 | # IDE files 8 | .vscode/ 9 | .idea/ 10 | *.sublime-* 11 | 12 | # Logs 13 | *.log 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Environment variables 19 | .env 20 | .env.local 21 | .env.*.local 22 | 23 | # Operating System 24 | .DS_Store 25 | Thumbs.db 26 | 27 | # Test coverage 28 | coverage/ 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCP-RTFM 2 | 3 | [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue.svg)](https://www.typescriptlang.org/) 4 | [![MCP](https://img.shields.io/badge/MCP-0.1.0-green.svg)](https://github.com/modelcontextprotocol) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | 7 | > "RTFM!" they say, but what if there's no FM to R? 🤔 Enter MCP-RTFM: an MCP server that helps you *create* the F*ing Manual everyone keeps telling people to read! Using advanced content analysis, metadata generation, and intelligent search capabilities, it transforms your non-existent or unreadable docs into an interconnected knowledge base that actually answers those "basic questions" before they're asked. 8 | 9 | > **Plot twist**: Instead of just telling people to RTFM, now you can actually give them an FM worth R-ing! Because the best response to "read the f*ing manual" is having a manual that's actually worth reading. 📚✨ 10 | 11 | ## 📚 Table of Contents 12 | 13 | - [Quick Start](#-quick-start) 14 | - [Features](#-features) 15 | - [Example Workflows](#-example-workflows) 16 | - [Installation](#-installation) 17 | - [Advanced Features](#-advanced-features) 18 | - [Development](#-development) 19 | - [Debugging](#-debugging) 20 | 21 | ## 🚀 Quick Start 22 | 23 | ```bash 24 | # Install dependencies 25 | npm install 26 | 27 | # Build the server 28 | npm run build 29 | 30 | # Add to your MCP settings and start using 31 | await use_mcp_tool({ 32 | server: "mcp-rtfm", 33 | tool: "analyze_project_with_metadata", // Enhanced initialization 34 | args: { projectPath: "/path/to/project" } 35 | }); 36 | 37 | // This will: 38 | // 1. Create documentation structure 39 | // 2. Analyze content with unified/remark 40 | // 3. Generate intelligent metadata 41 | // 4. Build search index with minisearch 42 | // 5. Add structured front matter 43 | // 6. Make your docs actually readable! 44 | ``` 45 | 46 | ## ✨ Features 47 | 48 | ### Documentation Management Tools 49 | 50 | - `analyze_existing_docs` - Analyze and enhance existing documentation with content analysis and metadata 51 | - `analyze_project_with_metadata` - Initialize documentation structure with enhanced content analysis and metadata generation 52 | - `analyze_project` - Basic initialization of documentation structure 53 | - `read_doc` - Read a documentation file (required before updating) 54 | - `update_doc` - Update documentation using diff-based changes 55 | - `get_doc_content` - Get current content of a documentation file 56 | - `get_project_info` - Get project structure and documentation status 57 | - `search_docs` - Search across documentation files with highlighted results 58 | - `update_metadata` - Update documentation metadata 59 | - `get_related_docs` - Find related documentation based on metadata and content links 60 | - `customize_template` - Create or update documentation templates 61 | 62 | ### Default Documentation Files 63 | 64 | The server automatically creates and manages these core documentation files: 65 | 66 | - `techStack.md` - Detailed inventory of tools, libraries, and configurations 67 | - `codebaseDetails.md` - Low-level explanations of code structure and logic 68 | - `workflowDetails.md` - Step-by-step workflows for key processes 69 | - `integrationGuides.md` - Instructions for external system connections 70 | - `errorHandling.md` - Troubleshooting strategies and practices 71 | - `handoff_notes.md` - Summary of key themes and next steps 72 | 73 | ### Documentation Templates 74 | 75 | Built-in templates for different documentation types: 76 | 77 | - Standard Documentation Template 78 | - API Documentation Template 79 | - Workflow Documentation Template 80 | 81 | Custom templates can be created using the `customize_template` tool. 82 | 83 | ## 📝 Example Workflows 84 | 85 | ### 1. Analyzing Existing Documentation 86 | 87 | ```typescript 88 | // Enhance existing documentation with advanced analysis 89 | await use_mcp_tool({ 90 | server: "mcp-rtfm", 91 | tool: "analyze_existing_docs", 92 | args: { projectPath: "/path/to/project" } 93 | }); 94 | 95 | // This will: 96 | // - Find all markdown files in .handoff_docs 97 | // - Analyze content structure with unified/remark 98 | // - Generate intelligent metadata 99 | // - Build search index 100 | // - Add front matter if not present 101 | // - Establish document relationships 102 | // - Preserve existing content 103 | 104 | // The results include: 105 | // - Enhanced metadata for all docs 106 | // - Search index population 107 | // - Content relationship mapping 108 | // - Git context if available 109 | ``` 110 | 111 | ### 2. Enhanced Project Documentation Setup 112 | 113 | ```typescript 114 | // Initialize documentation with advanced content analysis 115 | await use_mcp_tool({ 116 | server: "mcp-rtfm", 117 | tool: "analyze_project_with_metadata", 118 | args: { projectPath: "/path/to/project" } 119 | }); 120 | 121 | // Results include: 122 | // - Initialized documentation files 123 | // - Generated metadata from content analysis 124 | // - Established document relationships 125 | // - Populated search index 126 | // - Added structured front matter 127 | // - Git repository context 128 | 129 | // Get enhanced project information 130 | const projectInfo = await use_mcp_tool({ 131 | server: "mcp-rtfm", 132 | tool: "get_project_info", 133 | args: { projectPath: "/path/to/project" } 134 | }); 135 | 136 | // Search across documentation with intelligent results 137 | const searchResults = await use_mcp_tool({ 138 | server: "mcp-rtfm", 139 | tool: "search_docs", 140 | args: { 141 | projectPath: "/path/to/project", 142 | query: "authentication" 143 | } 144 | }); 145 | 146 | // Results include: 147 | // - Weighted matches (title matches prioritized) 148 | // - Fuzzy search results 149 | // - Full content context 150 | // - Related document suggestions 151 | ``` 152 | 153 | ### 3. Updating Documentation with Content Links 154 | 155 | ```typescript 156 | // First read the document 157 | await use_mcp_tool({ 158 | server: "mcp-rtfm", 159 | tool: "read_doc", 160 | args: { 161 | projectPath: "/path/to/project", 162 | docFile: "techStack.md" 163 | } 164 | }); 165 | 166 | // Update with content that links to other docs 167 | await use_mcp_tool({ 168 | server: "mcp-rtfm", 169 | tool: "update_doc", 170 | args: { 171 | projectPath: "/path/to/project", 172 | docFile: "techStack.md", 173 | searchContent: "[Why this domain is critical to the project]", 174 | replaceContent: "The tech stack documentation provides essential context for development. See [[workflowDetails]] for implementation steps.", 175 | continueToNext: true // Automatically move to next document 176 | } 177 | }); 178 | ``` 179 | 180 | ### 4. Managing Documentation Metadata 181 | 182 | ```typescript 183 | // Update metadata for better organization 184 | await use_mcp_tool({ 185 | server: "mcp-rtfm", 186 | tool: "update_metadata", 187 | args: { 188 | projectPath: "/path/to/project", 189 | docFile: "techStack.md", 190 | metadata: { 191 | title: "Technology Stack Overview", 192 | category: "architecture", 193 | tags: ["infrastructure", "dependencies", "configuration"] 194 | } 195 | } 196 | }); 197 | 198 | // Find related documentation 199 | const related = await use_mcp_tool({ 200 | server: "mcp-rtfm", 201 | tool: "get_related_docs", 202 | args: { 203 | projectPath: "/path/to/project", 204 | docFile: "techStack.md" 205 | } 206 | }); 207 | ``` 208 | 209 | ### 5. Searching Documentation with Context 210 | 211 | ```typescript 212 | // Search with highlighted results 213 | const results = await use_mcp_tool({ 214 | server: "mcp-rtfm", 215 | tool: "search_docs", 216 | args: { 217 | projectPath: "/path/to/project", 218 | query: "authentication" 219 | } 220 | }); 221 | 222 | // Results include: 223 | // - File name 224 | // - Line numbers 225 | // - Highlighted matches 226 | // - Context around matches 227 | ``` 228 | 229 | ### 6. Creating Custom Templates 230 | 231 | ```typescript 232 | // Create a custom template for architecture decisions 233 | await use_mcp_tool({ 234 | server: "mcp-rtfm", 235 | tool: "customize_template", 236 | args: { 237 | templateName: "architecture-decision", 238 | content: `# {title} 239 | 240 | ## Context 241 | [Background and context for the decision] 242 | 243 | ## Decision 244 | [The architecture decision made] 245 | 246 | ## Consequences 247 | [Impact and trade-offs of the decision] 248 | 249 | ## Related Decisions 250 | [Links to related architecture decisions]`, 251 | metadata: { 252 | category: "architecture", 253 | tags: ["decision-record", "design"] 254 | } 255 | } 256 | }); 257 | ``` 258 | 259 | ## 🔧 Installation 260 | 261 | ### VSCode (Roo Cline) 262 | 263 | Add to settings file at: 264 | Add to settings file at: 265 | - Windows: `%APPDATA%\Code\User\globalStorage\rooveterinaryinc.roo-cline\settings\cline_mcp_settings.json` 266 | - MacOS: `~/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json` 267 | - Linux: `~/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json` 268 | 269 | ```json 270 | { 271 | "mcpServers": { 272 | "mcp-rtfm": { 273 | "command": "node", 274 | "args": ["/build/index.js"], 275 | "disabled": false, 276 | "alwaysAllow": [] 277 | } 278 | } 279 | } 280 | ``` 281 | 282 | ### Claude Desktop 283 | 284 | Add to config file at: 285 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json` 286 | - MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 287 | - Linux: `~/.config/Claude/claude_desktop_config.json` 288 | 289 | ```json 290 | { 291 | "mcpServers": { 292 | "mcp-rtfm": { 293 | "command": "node", 294 | "args": ["/build/index.js"], 295 | "disabled": false, 296 | "alwaysAllow": [] 297 | } 298 | } 299 | } 300 | ``` 301 | 302 | ## 🎯 Advanced Features 303 | 304 | ### Content Linking 305 | 306 | Use `[[document-name]]` syntax to create links between documents. The server automatically tracks these relationships and includes them when finding related documentation. 307 | 308 | ### Metadata-Driven Organization 309 | 310 | Documents are organized using: 311 | 312 | - Categories (e.g., "architecture", "api", "workflow") 313 | - Tags for flexible grouping 314 | - Automatic relationship discovery based on shared metadata 315 | - Content link analysis 316 | 317 | ### Enhanced Content Analysis 318 | 319 | The server uses advanced libraries for better documentation management: 320 | 321 | - **unified/remark** for Markdown processing: 322 | - AST-based content analysis 323 | - Accurate heading structure detection 324 | - Code block and link extraction 325 | - Proper Markdown parsing and manipulation 326 | 327 | - **minisearch** for powerful search capabilities: 328 | - Fast fuzzy searching across all documentation 329 | - Field-weighted search (titles given higher priority) 330 | - Full content and metadata indexing 331 | - Efficient caching with TTL management 332 | - Real-time search index updates 333 | 334 | ### Intelligent Metadata Generation 335 | 336 | - Automatic content analysis for categorization 337 | - Smart tag generation based on content patterns 338 | - Structured front matter in documents 339 | - AST-based title and section detection 340 | - Code snippet identification and tagging 341 | - Context-aware result presentation 342 | 343 | ### Template System 344 | 345 | - Built-in templates for common documentation types 346 | - Custom template support with metadata defaults 347 | - Template inheritance and override capabilities 348 | - Placeholder system for consistent formatting 349 | 350 | ## 🛠️ Development 351 | 352 | ```bash 353 | # Install dependencies 354 | npm install 355 | 356 | # Build the server 357 | npm run build 358 | 359 | # Development with auto-rebuild 360 | npm run watch 361 | ``` 362 | 363 | ## 🐛 Debugging 364 | 365 | Since MCP servers communicate over stdio, debugging can be challenging. Use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector): 366 | 367 | ```bash 368 | npm run inspector 369 | ``` 370 | 371 | The Inspector will provide a URL to access debugging tools in your browser. 372 | 373 | ## 📄 License 374 | 375 | MIT © [Model Context Protocol](https://github.com/modelcontextprotocol) 376 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "handoff-docs-server", 3 | "version": "0.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "handoff-docs-server", 9 | "version": "0.1.0", 10 | "dependencies": { 11 | "@modelcontextprotocol/sdk": "0.6.0", 12 | "minisearch": "^7.1.1", 13 | "remark-parse": "^11.0.0", 14 | "remark-stringify": "^11.0.0", 15 | "unified": "^11.0.5" 16 | }, 17 | "bin": { 18 | "handoff-docs-server": "build/index.js" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^20.17.10", 22 | "typescript": "^5.3.3" 23 | } 24 | }, 25 | "node_modules/@modelcontextprotocol/sdk": { 26 | "version": "0.6.0", 27 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.6.0.tgz", 28 | "integrity": "sha512-9rsDudGhDtMbvxohPoMMyAUOmEzQsOK+XFchh6gZGqo8sx9sBuZQs+CUttXqa8RZXKDaJRCN2tUtgGof7jRkkw==", 29 | "license": "MIT", 30 | "dependencies": { 31 | "content-type": "^1.0.5", 32 | "raw-body": "^3.0.0", 33 | "zod": "^3.23.8" 34 | } 35 | }, 36 | "node_modules/@types/debug": { 37 | "version": "4.1.12", 38 | "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", 39 | "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", 40 | "license": "MIT", 41 | "dependencies": { 42 | "@types/ms": "*" 43 | } 44 | }, 45 | "node_modules/@types/mdast": { 46 | "version": "4.0.4", 47 | "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", 48 | "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", 49 | "license": "MIT", 50 | "dependencies": { 51 | "@types/unist": "*" 52 | } 53 | }, 54 | "node_modules/@types/ms": { 55 | "version": "0.7.34", 56 | "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", 57 | "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", 58 | "license": "MIT" 59 | }, 60 | "node_modules/@types/node": { 61 | "version": "20.17.10", 62 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz", 63 | "integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==", 64 | "dev": true, 65 | "license": "MIT", 66 | "dependencies": { 67 | "undici-types": "~6.19.2" 68 | } 69 | }, 70 | "node_modules/@types/unist": { 71 | "version": "3.0.3", 72 | "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", 73 | "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", 74 | "license": "MIT" 75 | }, 76 | "node_modules/bail": { 77 | "version": "2.0.2", 78 | "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", 79 | "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", 80 | "license": "MIT", 81 | "funding": { 82 | "type": "github", 83 | "url": "https://github.com/sponsors/wooorm" 84 | } 85 | }, 86 | "node_modules/bytes": { 87 | "version": "3.1.2", 88 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 89 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 90 | "license": "MIT", 91 | "engines": { 92 | "node": ">= 0.8" 93 | } 94 | }, 95 | "node_modules/character-entities": { 96 | "version": "2.0.2", 97 | "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", 98 | "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", 99 | "license": "MIT", 100 | "funding": { 101 | "type": "github", 102 | "url": "https://github.com/sponsors/wooorm" 103 | } 104 | }, 105 | "node_modules/content-type": { 106 | "version": "1.0.5", 107 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 108 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 109 | "license": "MIT", 110 | "engines": { 111 | "node": ">= 0.6" 112 | } 113 | }, 114 | "node_modules/debug": { 115 | "version": "4.4.0", 116 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 117 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 118 | "license": "MIT", 119 | "dependencies": { 120 | "ms": "^2.1.3" 121 | }, 122 | "engines": { 123 | "node": ">=6.0" 124 | }, 125 | "peerDependenciesMeta": { 126 | "supports-color": { 127 | "optional": true 128 | } 129 | } 130 | }, 131 | "node_modules/decode-named-character-reference": { 132 | "version": "1.0.2", 133 | "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", 134 | "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", 135 | "license": "MIT", 136 | "dependencies": { 137 | "character-entities": "^2.0.0" 138 | }, 139 | "funding": { 140 | "type": "github", 141 | "url": "https://github.com/sponsors/wooorm" 142 | } 143 | }, 144 | "node_modules/depd": { 145 | "version": "2.0.0", 146 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 147 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 148 | "license": "MIT", 149 | "engines": { 150 | "node": ">= 0.8" 151 | } 152 | }, 153 | "node_modules/dequal": { 154 | "version": "2.0.3", 155 | "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", 156 | "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", 157 | "license": "MIT", 158 | "engines": { 159 | "node": ">=6" 160 | } 161 | }, 162 | "node_modules/devlop": { 163 | "version": "1.1.0", 164 | "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", 165 | "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", 166 | "license": "MIT", 167 | "dependencies": { 168 | "dequal": "^2.0.0" 169 | }, 170 | "funding": { 171 | "type": "github", 172 | "url": "https://github.com/sponsors/wooorm" 173 | } 174 | }, 175 | "node_modules/extend": { 176 | "version": "3.0.2", 177 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 178 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 179 | "license": "MIT" 180 | }, 181 | "node_modules/http-errors": { 182 | "version": "2.0.0", 183 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 184 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 185 | "license": "MIT", 186 | "dependencies": { 187 | "depd": "2.0.0", 188 | "inherits": "2.0.4", 189 | "setprototypeof": "1.2.0", 190 | "statuses": "2.0.1", 191 | "toidentifier": "1.0.1" 192 | }, 193 | "engines": { 194 | "node": ">= 0.8" 195 | } 196 | }, 197 | "node_modules/iconv-lite": { 198 | "version": "0.6.3", 199 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 200 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 201 | "license": "MIT", 202 | "dependencies": { 203 | "safer-buffer": ">= 2.1.2 < 3.0.0" 204 | }, 205 | "engines": { 206 | "node": ">=0.10.0" 207 | } 208 | }, 209 | "node_modules/inherits": { 210 | "version": "2.0.4", 211 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 212 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 213 | "license": "ISC" 214 | }, 215 | "node_modules/is-plain-obj": { 216 | "version": "4.1.0", 217 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", 218 | "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", 219 | "license": "MIT", 220 | "engines": { 221 | "node": ">=12" 222 | }, 223 | "funding": { 224 | "url": "https://github.com/sponsors/sindresorhus" 225 | } 226 | }, 227 | "node_modules/longest-streak": { 228 | "version": "3.1.0", 229 | "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", 230 | "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", 231 | "license": "MIT", 232 | "funding": { 233 | "type": "github", 234 | "url": "https://github.com/sponsors/wooorm" 235 | } 236 | }, 237 | "node_modules/mdast-util-from-markdown": { 238 | "version": "2.0.2", 239 | "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", 240 | "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", 241 | "license": "MIT", 242 | "dependencies": { 243 | "@types/mdast": "^4.0.0", 244 | "@types/unist": "^3.0.0", 245 | "decode-named-character-reference": "^1.0.0", 246 | "devlop": "^1.0.0", 247 | "mdast-util-to-string": "^4.0.0", 248 | "micromark": "^4.0.0", 249 | "micromark-util-decode-numeric-character-reference": "^2.0.0", 250 | "micromark-util-decode-string": "^2.0.0", 251 | "micromark-util-normalize-identifier": "^2.0.0", 252 | "micromark-util-symbol": "^2.0.0", 253 | "micromark-util-types": "^2.0.0", 254 | "unist-util-stringify-position": "^4.0.0" 255 | }, 256 | "funding": { 257 | "type": "opencollective", 258 | "url": "https://opencollective.com/unified" 259 | } 260 | }, 261 | "node_modules/mdast-util-phrasing": { 262 | "version": "4.1.0", 263 | "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", 264 | "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", 265 | "license": "MIT", 266 | "dependencies": { 267 | "@types/mdast": "^4.0.0", 268 | "unist-util-is": "^6.0.0" 269 | }, 270 | "funding": { 271 | "type": "opencollective", 272 | "url": "https://opencollective.com/unified" 273 | } 274 | }, 275 | "node_modules/mdast-util-to-markdown": { 276 | "version": "2.1.2", 277 | "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", 278 | "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", 279 | "license": "MIT", 280 | "dependencies": { 281 | "@types/mdast": "^4.0.0", 282 | "@types/unist": "^3.0.0", 283 | "longest-streak": "^3.0.0", 284 | "mdast-util-phrasing": "^4.0.0", 285 | "mdast-util-to-string": "^4.0.0", 286 | "micromark-util-classify-character": "^2.0.0", 287 | "micromark-util-decode-string": "^2.0.0", 288 | "unist-util-visit": "^5.0.0", 289 | "zwitch": "^2.0.0" 290 | }, 291 | "funding": { 292 | "type": "opencollective", 293 | "url": "https://opencollective.com/unified" 294 | } 295 | }, 296 | "node_modules/mdast-util-to-string": { 297 | "version": "4.0.0", 298 | "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", 299 | "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", 300 | "license": "MIT", 301 | "dependencies": { 302 | "@types/mdast": "^4.0.0" 303 | }, 304 | "funding": { 305 | "type": "opencollective", 306 | "url": "https://opencollective.com/unified" 307 | } 308 | }, 309 | "node_modules/micromark": { 310 | "version": "4.0.1", 311 | "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.1.tgz", 312 | "integrity": "sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==", 313 | "funding": [ 314 | { 315 | "type": "GitHub Sponsors", 316 | "url": "https://github.com/sponsors/unifiedjs" 317 | }, 318 | { 319 | "type": "OpenCollective", 320 | "url": "https://opencollective.com/unified" 321 | } 322 | ], 323 | "license": "MIT", 324 | "dependencies": { 325 | "@types/debug": "^4.0.0", 326 | "debug": "^4.0.0", 327 | "decode-named-character-reference": "^1.0.0", 328 | "devlop": "^1.0.0", 329 | "micromark-core-commonmark": "^2.0.0", 330 | "micromark-factory-space": "^2.0.0", 331 | "micromark-util-character": "^2.0.0", 332 | "micromark-util-chunked": "^2.0.0", 333 | "micromark-util-combine-extensions": "^2.0.0", 334 | "micromark-util-decode-numeric-character-reference": "^2.0.0", 335 | "micromark-util-encode": "^2.0.0", 336 | "micromark-util-normalize-identifier": "^2.0.0", 337 | "micromark-util-resolve-all": "^2.0.0", 338 | "micromark-util-sanitize-uri": "^2.0.0", 339 | "micromark-util-subtokenize": "^2.0.0", 340 | "micromark-util-symbol": "^2.0.0", 341 | "micromark-util-types": "^2.0.0" 342 | } 343 | }, 344 | "node_modules/micromark-core-commonmark": { 345 | "version": "2.0.2", 346 | "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.2.tgz", 347 | "integrity": "sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==", 348 | "funding": [ 349 | { 350 | "type": "GitHub Sponsors", 351 | "url": "https://github.com/sponsors/unifiedjs" 352 | }, 353 | { 354 | "type": "OpenCollective", 355 | "url": "https://opencollective.com/unified" 356 | } 357 | ], 358 | "license": "MIT", 359 | "dependencies": { 360 | "decode-named-character-reference": "^1.0.0", 361 | "devlop": "^1.0.0", 362 | "micromark-factory-destination": "^2.0.0", 363 | "micromark-factory-label": "^2.0.0", 364 | "micromark-factory-space": "^2.0.0", 365 | "micromark-factory-title": "^2.0.0", 366 | "micromark-factory-whitespace": "^2.0.0", 367 | "micromark-util-character": "^2.0.0", 368 | "micromark-util-chunked": "^2.0.0", 369 | "micromark-util-classify-character": "^2.0.0", 370 | "micromark-util-html-tag-name": "^2.0.0", 371 | "micromark-util-normalize-identifier": "^2.0.0", 372 | "micromark-util-resolve-all": "^2.0.0", 373 | "micromark-util-subtokenize": "^2.0.0", 374 | "micromark-util-symbol": "^2.0.0", 375 | "micromark-util-types": "^2.0.0" 376 | } 377 | }, 378 | "node_modules/micromark-factory-destination": { 379 | "version": "2.0.1", 380 | "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", 381 | "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", 382 | "funding": [ 383 | { 384 | "type": "GitHub Sponsors", 385 | "url": "https://github.com/sponsors/unifiedjs" 386 | }, 387 | { 388 | "type": "OpenCollective", 389 | "url": "https://opencollective.com/unified" 390 | } 391 | ], 392 | "license": "MIT", 393 | "dependencies": { 394 | "micromark-util-character": "^2.0.0", 395 | "micromark-util-symbol": "^2.0.0", 396 | "micromark-util-types": "^2.0.0" 397 | } 398 | }, 399 | "node_modules/micromark-factory-label": { 400 | "version": "2.0.1", 401 | "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", 402 | "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", 403 | "funding": [ 404 | { 405 | "type": "GitHub Sponsors", 406 | "url": "https://github.com/sponsors/unifiedjs" 407 | }, 408 | { 409 | "type": "OpenCollective", 410 | "url": "https://opencollective.com/unified" 411 | } 412 | ], 413 | "license": "MIT", 414 | "dependencies": { 415 | "devlop": "^1.0.0", 416 | "micromark-util-character": "^2.0.0", 417 | "micromark-util-symbol": "^2.0.0", 418 | "micromark-util-types": "^2.0.0" 419 | } 420 | }, 421 | "node_modules/micromark-factory-space": { 422 | "version": "2.0.1", 423 | "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", 424 | "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", 425 | "funding": [ 426 | { 427 | "type": "GitHub Sponsors", 428 | "url": "https://github.com/sponsors/unifiedjs" 429 | }, 430 | { 431 | "type": "OpenCollective", 432 | "url": "https://opencollective.com/unified" 433 | } 434 | ], 435 | "license": "MIT", 436 | "dependencies": { 437 | "micromark-util-character": "^2.0.0", 438 | "micromark-util-types": "^2.0.0" 439 | } 440 | }, 441 | "node_modules/micromark-factory-title": { 442 | "version": "2.0.1", 443 | "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", 444 | "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", 445 | "funding": [ 446 | { 447 | "type": "GitHub Sponsors", 448 | "url": "https://github.com/sponsors/unifiedjs" 449 | }, 450 | { 451 | "type": "OpenCollective", 452 | "url": "https://opencollective.com/unified" 453 | } 454 | ], 455 | "license": "MIT", 456 | "dependencies": { 457 | "micromark-factory-space": "^2.0.0", 458 | "micromark-util-character": "^2.0.0", 459 | "micromark-util-symbol": "^2.0.0", 460 | "micromark-util-types": "^2.0.0" 461 | } 462 | }, 463 | "node_modules/micromark-factory-whitespace": { 464 | "version": "2.0.1", 465 | "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", 466 | "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", 467 | "funding": [ 468 | { 469 | "type": "GitHub Sponsors", 470 | "url": "https://github.com/sponsors/unifiedjs" 471 | }, 472 | { 473 | "type": "OpenCollective", 474 | "url": "https://opencollective.com/unified" 475 | } 476 | ], 477 | "license": "MIT", 478 | "dependencies": { 479 | "micromark-factory-space": "^2.0.0", 480 | "micromark-util-character": "^2.0.0", 481 | "micromark-util-symbol": "^2.0.0", 482 | "micromark-util-types": "^2.0.0" 483 | } 484 | }, 485 | "node_modules/micromark-util-character": { 486 | "version": "2.1.1", 487 | "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", 488 | "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", 489 | "funding": [ 490 | { 491 | "type": "GitHub Sponsors", 492 | "url": "https://github.com/sponsors/unifiedjs" 493 | }, 494 | { 495 | "type": "OpenCollective", 496 | "url": "https://opencollective.com/unified" 497 | } 498 | ], 499 | "license": "MIT", 500 | "dependencies": { 501 | "micromark-util-symbol": "^2.0.0", 502 | "micromark-util-types": "^2.0.0" 503 | } 504 | }, 505 | "node_modules/micromark-util-chunked": { 506 | "version": "2.0.1", 507 | "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", 508 | "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", 509 | "funding": [ 510 | { 511 | "type": "GitHub Sponsors", 512 | "url": "https://github.com/sponsors/unifiedjs" 513 | }, 514 | { 515 | "type": "OpenCollective", 516 | "url": "https://opencollective.com/unified" 517 | } 518 | ], 519 | "license": "MIT", 520 | "dependencies": { 521 | "micromark-util-symbol": "^2.0.0" 522 | } 523 | }, 524 | "node_modules/micromark-util-classify-character": { 525 | "version": "2.0.1", 526 | "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", 527 | "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", 528 | "funding": [ 529 | { 530 | "type": "GitHub Sponsors", 531 | "url": "https://github.com/sponsors/unifiedjs" 532 | }, 533 | { 534 | "type": "OpenCollective", 535 | "url": "https://opencollective.com/unified" 536 | } 537 | ], 538 | "license": "MIT", 539 | "dependencies": { 540 | "micromark-util-character": "^2.0.0", 541 | "micromark-util-symbol": "^2.0.0", 542 | "micromark-util-types": "^2.0.0" 543 | } 544 | }, 545 | "node_modules/micromark-util-combine-extensions": { 546 | "version": "2.0.1", 547 | "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", 548 | "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", 549 | "funding": [ 550 | { 551 | "type": "GitHub Sponsors", 552 | "url": "https://github.com/sponsors/unifiedjs" 553 | }, 554 | { 555 | "type": "OpenCollective", 556 | "url": "https://opencollective.com/unified" 557 | } 558 | ], 559 | "license": "MIT", 560 | "dependencies": { 561 | "micromark-util-chunked": "^2.0.0", 562 | "micromark-util-types": "^2.0.0" 563 | } 564 | }, 565 | "node_modules/micromark-util-decode-numeric-character-reference": { 566 | "version": "2.0.2", 567 | "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", 568 | "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", 569 | "funding": [ 570 | { 571 | "type": "GitHub Sponsors", 572 | "url": "https://github.com/sponsors/unifiedjs" 573 | }, 574 | { 575 | "type": "OpenCollective", 576 | "url": "https://opencollective.com/unified" 577 | } 578 | ], 579 | "license": "MIT", 580 | "dependencies": { 581 | "micromark-util-symbol": "^2.0.0" 582 | } 583 | }, 584 | "node_modules/micromark-util-decode-string": { 585 | "version": "2.0.1", 586 | "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", 587 | "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", 588 | "funding": [ 589 | { 590 | "type": "GitHub Sponsors", 591 | "url": "https://github.com/sponsors/unifiedjs" 592 | }, 593 | { 594 | "type": "OpenCollective", 595 | "url": "https://opencollective.com/unified" 596 | } 597 | ], 598 | "license": "MIT", 599 | "dependencies": { 600 | "decode-named-character-reference": "^1.0.0", 601 | "micromark-util-character": "^2.0.0", 602 | "micromark-util-decode-numeric-character-reference": "^2.0.0", 603 | "micromark-util-symbol": "^2.0.0" 604 | } 605 | }, 606 | "node_modules/micromark-util-encode": { 607 | "version": "2.0.1", 608 | "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", 609 | "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", 610 | "funding": [ 611 | { 612 | "type": "GitHub Sponsors", 613 | "url": "https://github.com/sponsors/unifiedjs" 614 | }, 615 | { 616 | "type": "OpenCollective", 617 | "url": "https://opencollective.com/unified" 618 | } 619 | ], 620 | "license": "MIT" 621 | }, 622 | "node_modules/micromark-util-html-tag-name": { 623 | "version": "2.0.1", 624 | "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", 625 | "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", 626 | "funding": [ 627 | { 628 | "type": "GitHub Sponsors", 629 | "url": "https://github.com/sponsors/unifiedjs" 630 | }, 631 | { 632 | "type": "OpenCollective", 633 | "url": "https://opencollective.com/unified" 634 | } 635 | ], 636 | "license": "MIT" 637 | }, 638 | "node_modules/micromark-util-normalize-identifier": { 639 | "version": "2.0.1", 640 | "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", 641 | "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", 642 | "funding": [ 643 | { 644 | "type": "GitHub Sponsors", 645 | "url": "https://github.com/sponsors/unifiedjs" 646 | }, 647 | { 648 | "type": "OpenCollective", 649 | "url": "https://opencollective.com/unified" 650 | } 651 | ], 652 | "license": "MIT", 653 | "dependencies": { 654 | "micromark-util-symbol": "^2.0.0" 655 | } 656 | }, 657 | "node_modules/micromark-util-resolve-all": { 658 | "version": "2.0.1", 659 | "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", 660 | "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", 661 | "funding": [ 662 | { 663 | "type": "GitHub Sponsors", 664 | "url": "https://github.com/sponsors/unifiedjs" 665 | }, 666 | { 667 | "type": "OpenCollective", 668 | "url": "https://opencollective.com/unified" 669 | } 670 | ], 671 | "license": "MIT", 672 | "dependencies": { 673 | "micromark-util-types": "^2.0.0" 674 | } 675 | }, 676 | "node_modules/micromark-util-sanitize-uri": { 677 | "version": "2.0.1", 678 | "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", 679 | "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", 680 | "funding": [ 681 | { 682 | "type": "GitHub Sponsors", 683 | "url": "https://github.com/sponsors/unifiedjs" 684 | }, 685 | { 686 | "type": "OpenCollective", 687 | "url": "https://opencollective.com/unified" 688 | } 689 | ], 690 | "license": "MIT", 691 | "dependencies": { 692 | "micromark-util-character": "^2.0.0", 693 | "micromark-util-encode": "^2.0.0", 694 | "micromark-util-symbol": "^2.0.0" 695 | } 696 | }, 697 | "node_modules/micromark-util-subtokenize": { 698 | "version": "2.0.3", 699 | "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.3.tgz", 700 | "integrity": "sha512-VXJJuNxYWSoYL6AJ6OQECCFGhIU2GGHMw8tahogePBrjkG8aCCas3ibkp7RnVOSTClg2is05/R7maAhF1XyQMg==", 701 | "funding": [ 702 | { 703 | "type": "GitHub Sponsors", 704 | "url": "https://github.com/sponsors/unifiedjs" 705 | }, 706 | { 707 | "type": "OpenCollective", 708 | "url": "https://opencollective.com/unified" 709 | } 710 | ], 711 | "license": "MIT", 712 | "dependencies": { 713 | "devlop": "^1.0.0", 714 | "micromark-util-chunked": "^2.0.0", 715 | "micromark-util-symbol": "^2.0.0", 716 | "micromark-util-types": "^2.0.0" 717 | } 718 | }, 719 | "node_modules/micromark-util-symbol": { 720 | "version": "2.0.1", 721 | "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", 722 | "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", 723 | "funding": [ 724 | { 725 | "type": "GitHub Sponsors", 726 | "url": "https://github.com/sponsors/unifiedjs" 727 | }, 728 | { 729 | "type": "OpenCollective", 730 | "url": "https://opencollective.com/unified" 731 | } 732 | ], 733 | "license": "MIT" 734 | }, 735 | "node_modules/micromark-util-types": { 736 | "version": "2.0.1", 737 | "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz", 738 | "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==", 739 | "funding": [ 740 | { 741 | "type": "GitHub Sponsors", 742 | "url": "https://github.com/sponsors/unifiedjs" 743 | }, 744 | { 745 | "type": "OpenCollective", 746 | "url": "https://opencollective.com/unified" 747 | } 748 | ], 749 | "license": "MIT" 750 | }, 751 | "node_modules/minisearch": { 752 | "version": "7.1.1", 753 | "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.1.tgz", 754 | "integrity": "sha512-b3YZEYCEH4EdCAtYP7OlDyx7FdPwNzuNwLQ34SfJpM9dlbBZzeXndGavTrC+VCiRWomL21SWfMc6SCKO/U2ZNw==", 755 | "license": "MIT" 756 | }, 757 | "node_modules/ms": { 758 | "version": "2.1.3", 759 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 760 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 761 | "license": "MIT" 762 | }, 763 | "node_modules/raw-body": { 764 | "version": "3.0.0", 765 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 766 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 767 | "license": "MIT", 768 | "dependencies": { 769 | "bytes": "3.1.2", 770 | "http-errors": "2.0.0", 771 | "iconv-lite": "0.6.3", 772 | "unpipe": "1.0.0" 773 | }, 774 | "engines": { 775 | "node": ">= 0.8" 776 | } 777 | }, 778 | "node_modules/remark-parse": { 779 | "version": "11.0.0", 780 | "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", 781 | "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", 782 | "license": "MIT", 783 | "dependencies": { 784 | "@types/mdast": "^4.0.0", 785 | "mdast-util-from-markdown": "^2.0.0", 786 | "micromark-util-types": "^2.0.0", 787 | "unified": "^11.0.0" 788 | }, 789 | "funding": { 790 | "type": "opencollective", 791 | "url": "https://opencollective.com/unified" 792 | } 793 | }, 794 | "node_modules/remark-stringify": { 795 | "version": "11.0.0", 796 | "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", 797 | "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", 798 | "license": "MIT", 799 | "dependencies": { 800 | "@types/mdast": "^4.0.0", 801 | "mdast-util-to-markdown": "^2.0.0", 802 | "unified": "^11.0.0" 803 | }, 804 | "funding": { 805 | "type": "opencollective", 806 | "url": "https://opencollective.com/unified" 807 | } 808 | }, 809 | "node_modules/safer-buffer": { 810 | "version": "2.1.2", 811 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 812 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 813 | "license": "MIT" 814 | }, 815 | "node_modules/setprototypeof": { 816 | "version": "1.2.0", 817 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 818 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 819 | "license": "ISC" 820 | }, 821 | "node_modules/statuses": { 822 | "version": "2.0.1", 823 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 824 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 825 | "license": "MIT", 826 | "engines": { 827 | "node": ">= 0.8" 828 | } 829 | }, 830 | "node_modules/toidentifier": { 831 | "version": "1.0.1", 832 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 833 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 834 | "license": "MIT", 835 | "engines": { 836 | "node": ">=0.6" 837 | } 838 | }, 839 | "node_modules/trough": { 840 | "version": "2.2.0", 841 | "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", 842 | "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", 843 | "license": "MIT", 844 | "funding": { 845 | "type": "github", 846 | "url": "https://github.com/sponsors/wooorm" 847 | } 848 | }, 849 | "node_modules/typescript": { 850 | "version": "5.7.2", 851 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", 852 | "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", 853 | "dev": true, 854 | "license": "Apache-2.0", 855 | "bin": { 856 | "tsc": "bin/tsc", 857 | "tsserver": "bin/tsserver" 858 | }, 859 | "engines": { 860 | "node": ">=14.17" 861 | } 862 | }, 863 | "node_modules/undici-types": { 864 | "version": "6.19.8", 865 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 866 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 867 | "dev": true, 868 | "license": "MIT" 869 | }, 870 | "node_modules/unified": { 871 | "version": "11.0.5", 872 | "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", 873 | "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", 874 | "license": "MIT", 875 | "dependencies": { 876 | "@types/unist": "^3.0.0", 877 | "bail": "^2.0.0", 878 | "devlop": "^1.0.0", 879 | "extend": "^3.0.0", 880 | "is-plain-obj": "^4.0.0", 881 | "trough": "^2.0.0", 882 | "vfile": "^6.0.0" 883 | }, 884 | "funding": { 885 | "type": "opencollective", 886 | "url": "https://opencollective.com/unified" 887 | } 888 | }, 889 | "node_modules/unist-util-is": { 890 | "version": "6.0.0", 891 | "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", 892 | "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", 893 | "license": "MIT", 894 | "dependencies": { 895 | "@types/unist": "^3.0.0" 896 | }, 897 | "funding": { 898 | "type": "opencollective", 899 | "url": "https://opencollective.com/unified" 900 | } 901 | }, 902 | "node_modules/unist-util-stringify-position": { 903 | "version": "4.0.0", 904 | "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", 905 | "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", 906 | "license": "MIT", 907 | "dependencies": { 908 | "@types/unist": "^3.0.0" 909 | }, 910 | "funding": { 911 | "type": "opencollective", 912 | "url": "https://opencollective.com/unified" 913 | } 914 | }, 915 | "node_modules/unist-util-visit": { 916 | "version": "5.0.0", 917 | "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", 918 | "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", 919 | "license": "MIT", 920 | "dependencies": { 921 | "@types/unist": "^3.0.0", 922 | "unist-util-is": "^6.0.0", 923 | "unist-util-visit-parents": "^6.0.0" 924 | }, 925 | "funding": { 926 | "type": "opencollective", 927 | "url": "https://opencollective.com/unified" 928 | } 929 | }, 930 | "node_modules/unist-util-visit-parents": { 931 | "version": "6.0.1", 932 | "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", 933 | "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", 934 | "license": "MIT", 935 | "dependencies": { 936 | "@types/unist": "^3.0.0", 937 | "unist-util-is": "^6.0.0" 938 | }, 939 | "funding": { 940 | "type": "opencollective", 941 | "url": "https://opencollective.com/unified" 942 | } 943 | }, 944 | "node_modules/unpipe": { 945 | "version": "1.0.0", 946 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 947 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 948 | "license": "MIT", 949 | "engines": { 950 | "node": ">= 0.8" 951 | } 952 | }, 953 | "node_modules/vfile": { 954 | "version": "6.0.3", 955 | "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", 956 | "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", 957 | "license": "MIT", 958 | "dependencies": { 959 | "@types/unist": "^3.0.0", 960 | "vfile-message": "^4.0.0" 961 | }, 962 | "funding": { 963 | "type": "opencollective", 964 | "url": "https://opencollective.com/unified" 965 | } 966 | }, 967 | "node_modules/vfile-message": { 968 | "version": "4.0.2", 969 | "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", 970 | "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", 971 | "license": "MIT", 972 | "dependencies": { 973 | "@types/unist": "^3.0.0", 974 | "unist-util-stringify-position": "^4.0.0" 975 | }, 976 | "funding": { 977 | "type": "opencollective", 978 | "url": "https://opencollective.com/unified" 979 | } 980 | }, 981 | "node_modules/zod": { 982 | "version": "3.24.1", 983 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", 984 | "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", 985 | "license": "MIT", 986 | "funding": { 987 | "url": "https://github.com/sponsors/colinhacks" 988 | } 989 | }, 990 | "node_modules/zwitch": { 991 | "version": "2.0.4", 992 | "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", 993 | "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", 994 | "license": "MIT", 995 | "funding": { 996 | "type": "github", 997 | "url": "https://github.com/sponsors/wooorm" 998 | } 999 | } 1000 | } 1001 | } 1002 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-rtfm", 3 | "version": "0.1.0", 4 | "description": "Ever been told to RTFM only to find there's no FM to R? MCP-RTFM is the solution - it helps you CREATE the F*ing Manual that people keep telling everyone to read! Transform your non-existent or unreadable docs into an intelligent, searchable knowledge base that actually answers those 'basic questions' before they're asked.", 5 | "type": "module", 6 | "bin": { 7 | "mcp-rtfm": "./build/index.js" 8 | }, 9 | "files": [ 10 | "build" 11 | ], 12 | "scripts": { 13 | "build": "tsc", 14 | "prepare": "npm run build", 15 | "watch": "tsc --watch", 16 | "inspector": "npx @modelcontextprotocol/inspector build/index.js" 17 | }, 18 | "dependencies": { 19 | "@modelcontextprotocol/sdk": "0.6.0", 20 | "minisearch": "^7.1.1", 21 | "remark-parse": "^11.0.0", 22 | "remark-stringify": "^11.0.0", 23 | "unified": "^11.0.5" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^20.17.10", 27 | "typescript": "^5.3.3" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/ryanjoachim/mcp-rtfm.git" 32 | }, 33 | "keywords": [ 34 | "mcp", 35 | "documentation", 36 | "markdown", 37 | "analysis", 38 | "search", 39 | "metadata", 40 | "knowledge-base", 41 | "handoff", 42 | "project-docs", 43 | "rtfm", 44 | "documentation-tools", 45 | "docs-as-code" 46 | ], 47 | "author": "Ryan Joachim", 48 | "license": "MIT" 49 | } 50 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { 6 | CallToolRequestSchema, 7 | ListToolsRequestSchema, 8 | ErrorCode, 9 | McpError, 10 | CallToolRequest, 11 | } from "@modelcontextprotocol/sdk/types.js"; 12 | import * as fs from "fs/promises"; 13 | import { execSync } from "child_process"; 14 | import { unified } from "unified"; 15 | import remarkParse from "remark-parse"; 16 | import remarkStringify from "remark-stringify"; 17 | import MiniSearch from "minisearch"; 18 | 19 | // Initialize unified processor for markdown 20 | const markdownProcessor = unified() 21 | .use(remarkParse) 22 | .use(remarkStringify); 23 | 24 | // Initialize search engine 25 | const searchEngine = new MiniSearch({ 26 | fields: ['title', 'content', 'category', 'tags'], 27 | storeFields: ['title', 'category', 'tags', 'lastUpdated'], 28 | searchOptions: { 29 | boost: { title: 2 }, 30 | fuzzy: 0.2 31 | } 32 | }); 33 | 34 | interface DocState { 35 | currentFile: string | null; 36 | completedFiles: string[]; 37 | inProgress: boolean; 38 | lastReadFile: string | null; 39 | lastReadContent: string | null; 40 | continueToNext: boolean; 41 | metadata: Record; 42 | contextCache: { 43 | lastQuery?: string; 44 | results?: SearchResult[]; 45 | timestamp?: number; 46 | }; 47 | templateOverrides: Record; 48 | } 49 | 50 | interface SearchResult { 51 | file: string; 52 | matches: Array<{ 53 | line: string; 54 | lineNumber: number; 55 | highlight: { 56 | start: number; 57 | end: number; 58 | }; 59 | }>; 60 | } 61 | 62 | const CACHE_TTL = 5 * 60 * 1000; // 5 minutes 63 | 64 | interface DocMetadata { 65 | title: string; 66 | category: string; 67 | tags: string[]; 68 | lastUpdated: string; 69 | relatedDocs: string[]; 70 | } 71 | 72 | interface DocTemplate { 73 | name: string; 74 | content: string; 75 | metadata: Partial; 76 | } 77 | 78 | const DEFAULT_DOCS = [ 79 | "techStack.md", 80 | "codebaseDetails.md", 81 | "workflowDetails.md", 82 | "integrationGuides.md", 83 | "errorHandling.md", 84 | "handoff_notes.md" 85 | ]; 86 | 87 | const TEMPLATES: Record = { 88 | standard: { 89 | name: "Standard Documentation", 90 | content: `# {title} 91 | 92 | ## Purpose and Overview 93 | [Why this domain is critical to the project] 94 | 95 | ## Step-by-Step Explanations 96 | [Concrete, detailed steps for implementation and maintenance] 97 | 98 | ## Annotated Examples 99 | [Code snippets, diagrams, or flowcharts for clarity] 100 | 101 | ## Contextual Notes 102 | [Historical decisions, trade-offs, and anticipated challenges] 103 | 104 | ## Actionable Advice 105 | [Gotchas, edge cases, and common pitfalls to avoid]`, 106 | metadata: { 107 | category: "documentation", 108 | tags: ["guide", "reference"] 109 | } 110 | }, 111 | api: { 112 | name: "API Documentation", 113 | content: `# {title} API Reference 114 | 115 | ## Overview 116 | [High-level description of the API] 117 | 118 | ## Authentication 119 | [Authentication requirements and methods] 120 | 121 | ## Endpoints 122 | [Detailed endpoint documentation] 123 | 124 | ## Request/Response Examples 125 | [Example API calls and responses] 126 | 127 | ## Error Handling 128 | [Error codes and handling strategies] 129 | 130 | ## Rate Limiting 131 | [Rate limiting policies and quotas]`, 132 | metadata: { 133 | category: "api", 134 | tags: ["api", "reference", "integration"] 135 | } 136 | }, 137 | workflow: { 138 | name: "Workflow Documentation", 139 | content: `# {title} Workflow 140 | 141 | ## Overview 142 | [High-level description of the workflow] 143 | 144 | ## Prerequisites 145 | [Required setup and dependencies] 146 | 147 | ## Process Flow 148 | [Step-by-step workflow description] 149 | 150 | ## Decision Points 151 | [Key decision points and criteria] 152 | 153 | ## Success Criteria 154 | [How to verify successful completion] 155 | 156 | ## Troubleshooting 157 | [Common issues and solutions]`, 158 | metadata: { 159 | category: "workflow", 160 | tags: ["process", "guide"] 161 | } 162 | } 163 | }; 164 | 165 | const TEMPLATE_CONTENT = `# {title} 166 | 167 | ## Purpose and Overview 168 | [Why this domain is critical to the project] 169 | 170 | ## Step-by-Step Explanations 171 | [Concrete, detailed steps for implementation and maintenance] 172 | 173 | ## Annotated Examples 174 | [Code snippets, diagrams, or flowcharts for clarity] 175 | 176 | ## Contextual Notes 177 | [Historical decisions, trade-offs, and anticipated challenges] 178 | 179 | ## Actionable Advice 180 | [Gotchas, edge cases, and common pitfalls to avoid] 181 | `; 182 | 183 | const server = new Server( 184 | { 185 | name: "mcp-rtfm", 186 | version: "0.1.0", 187 | }, 188 | { 189 | capabilities: { 190 | tools: {}, 191 | }, 192 | } 193 | ); 194 | 195 | // Global state 196 | // Helper functions for context and metadata management 197 | const updateMetadata = async (filePath: string, metadata: Partial) => { 198 | const fileName = filePath.split('/').pop() as string; 199 | state.metadata[fileName] = { 200 | ...state.metadata[fileName], 201 | ...metadata, 202 | lastUpdated: new Date().toISOString() 203 | } as DocMetadata; 204 | }; 205 | 206 | // Helper function to analyze markdown content 207 | const analyzeContent = async (content: string): Promise<{ 208 | title: string; 209 | headings: string[]; 210 | codeBlocks: string[]; 211 | links: string[]; 212 | }> => { 213 | const ast = await markdownProcessor.parse(content); 214 | const result = { 215 | title: '', 216 | headings: [] as string[], 217 | codeBlocks: [] as string[], 218 | links: [] as string[] 219 | }; 220 | 221 | // @ts-ignore - types are not exact but functionality works 222 | const visit = (node: any) => { 223 | if (node.type === 'heading' && node.depth === 1) { 224 | result.title = node.children?.[0]?.value || ''; 225 | } else if (node.type === 'heading') { 226 | result.headings.push(node.children?.[0]?.value || ''); 227 | } else if (node.type === 'code') { 228 | result.codeBlocks.push(node.value || ''); 229 | } else if (node.type === 'link') { 230 | result.links.push(node.url || ''); 231 | } 232 | 233 | if (node.children) { 234 | node.children.forEach(visit); 235 | } 236 | }; 237 | 238 | visit(ast); 239 | return result; 240 | }; 241 | 242 | // Helper function to determine document category and tags 243 | const categorizeContent = ( 244 | fileName: string, 245 | content: string, 246 | analysis: Awaited> 247 | ): { category: string; tags: string[] } => { 248 | const tags = new Set(); 249 | let category = 'documentation'; 250 | 251 | // Category detection based on filename and headings 252 | if (fileName.includes('api') || analysis.headings.some(h => h.toLowerCase().includes('api'))) { 253 | category = 'api'; 254 | tags.add('api'); 255 | } else if (fileName.includes('workflow') || analysis.headings.some(h => h.toLowerCase().includes('workflow'))) { 256 | category = 'workflow'; 257 | tags.add('workflow'); 258 | } else if (fileName.includes('tech') || analysis.headings.some(h => h.toLowerCase().includes('stack'))) { 259 | category = 'technology'; 260 | tags.add('technology'); 261 | } 262 | 263 | // Tag detection based on content analysis 264 | if (analysis.codeBlocks.length > 0) tags.add('code-examples'); 265 | if (analysis.links.length > 0) tags.add('references'); 266 | if (content.match(/\b(error|exception|debug|troubleshoot)\b/i)) tags.add('error-handling'); 267 | if (content.match(/\b(config|setup|installation)\b/i)) tags.add('configuration'); 268 | if (content.match(/\b(security|auth|authentication|authorization)\b/i)) tags.add('security'); 269 | 270 | return { category, tags: Array.from(tags) }; 271 | }; 272 | 273 | // Helper function to update search index 274 | const updateSearchIndex = (docFile: string, content: string, metadata: DocMetadata) => { 275 | const docId = docFile.replace('.md', ''); 276 | if (searchEngine.has({ id: docId })) 277 | searchEngine.remove({ id: docId }); 278 | searchEngine.add({ 279 | id: docId, 280 | title: metadata.title, 281 | content, 282 | category: metadata.category, 283 | tags: metadata.tags, 284 | lastUpdated: metadata.lastUpdated 285 | }); 286 | }; 287 | 288 | const findRelatedDocs = async (docFile: string, projectPath: string): Promise => { 289 | const metadata = state.metadata[docFile]; 290 | if (!metadata) return []; 291 | 292 | const related = new Set(); 293 | 294 | // Find docs with matching tags 295 | Object.entries(state.metadata).forEach(([file, meta]) => { 296 | if (file !== docFile && meta.tags.some(tag => metadata.tags.includes(tag))) { 297 | related.add(file); 298 | } 299 | }); 300 | 301 | // Find docs in same category 302 | Object.entries(state.metadata).forEach(([file, meta]) => { 303 | if (file !== docFile && meta.category === metadata.category) { 304 | related.add(file); 305 | } 306 | }); 307 | 308 | // Find docs referenced in content 309 | const content = await fs.readFile(`${projectPath}/.handoff_docs/${docFile}`, 'utf8'); 310 | const matches = content.match(/\[\[([^\]]+)\]\]/g) || []; 311 | matches.forEach(match => { 312 | const linkedDoc = match.slice(2, -2).trim() + '.md'; 313 | if (DEFAULT_DOCS.includes(linkedDoc)) { 314 | related.add(linkedDoc); 315 | } 316 | }); 317 | 318 | return Array.from(related); 319 | }; 320 | 321 | const searchDocContent = async (projectPath: string, query: string): Promise => { 322 | // Check cache first 323 | if ( 324 | state.contextCache.lastQuery === query && 325 | state.contextCache.results && 326 | state.contextCache.timestamp && 327 | Date.now() - state.contextCache.timestamp < CACHE_TTL 328 | ) { 329 | return state.contextCache.results; 330 | } 331 | 332 | const results: SearchResult[] = []; 333 | const docsPath = `${projectPath}/.handoff_docs`; 334 | const searchRegex = new RegExp(query, 'gi'); 335 | 336 | for (const doc of DEFAULT_DOCS) { 337 | try { 338 | const content = await fs.readFile(`${docsPath}/${doc}`, 'utf8'); 339 | const lines = content.split('\n'); 340 | const matches = lines 341 | .map((line, index) => { 342 | const match = searchRegex.exec(line); 343 | if (match) { 344 | return { 345 | line, 346 | lineNumber: index + 1, 347 | highlight: { 348 | start: match.index, 349 | end: match.index + match[0].length 350 | } 351 | }; 352 | } 353 | return null; 354 | }) 355 | .filter((match): match is NonNullable => match !== null); 356 | 357 | if (matches.length > 0) { 358 | results.push({ file: doc, matches }); 359 | } 360 | } catch (error) { 361 | console.error(`Error searching ${doc}:`, error); 362 | } 363 | } 364 | 365 | // Update cache 366 | state.contextCache = { 367 | lastQuery: query, 368 | results, 369 | timestamp: Date.now() 370 | }; 371 | 372 | return results; 373 | }; 374 | 375 | let state: DocState = { 376 | currentFile: null, 377 | completedFiles: [], 378 | inProgress: false, 379 | lastReadFile: null, 380 | lastReadContent: null, 381 | continueToNext: false, 382 | metadata: {}, 383 | contextCache: {}, 384 | templateOverrides: {} 385 | }; 386 | 387 | server.setRequestHandler(ListToolsRequestSchema, async () => { 388 | return { 389 | tools: [ 390 | { 391 | name: "analyze_existing_docs", 392 | description: "Analyze existing documentation files with enhanced content analysis and metadata generation", 393 | inputSchema: { 394 | type: "object", 395 | properties: { 396 | projectPath: { 397 | type: "string", 398 | description: "Path to the project root directory" 399 | } 400 | }, 401 | required: ["projectPath"] 402 | } 403 | }, 404 | { 405 | name: "analyze_project_with_metadata", 406 | description: "Analyze project structure, create initial documentation files, and enhance with metadata/context", 407 | inputSchema: { 408 | type: "object", 409 | properties: { 410 | projectPath: { 411 | type: "string", 412 | description: "Path to the project root directory" 413 | } 414 | }, 415 | required: ["projectPath"] 416 | } 417 | }, 418 | { 419 | name: "analyze_project", 420 | description: "Analyze project structure and create initial documentation files", 421 | inputSchema: { 422 | type: "object", 423 | properties: { 424 | projectPath: { 425 | type: "string", 426 | description: "Path to the project root directory" 427 | } 428 | }, 429 | required: ["projectPath"] 430 | } 431 | }, 432 | { 433 | name: "read_doc", 434 | description: "Read a documentation file (required before updating)", 435 | inputSchema: { 436 | type: "object", 437 | properties: { 438 | projectPath: { 439 | type: "string", 440 | description: "Path to the project root directory" 441 | }, 442 | docFile: { 443 | type: "string", 444 | description: "Name of the documentation file to read" 445 | } 446 | }, 447 | required: ["projectPath", "docFile"] 448 | } 449 | }, 450 | { 451 | name: "update_doc", 452 | description: "Update a specific documentation file using diff-based changes", 453 | inputSchema: { 454 | type: "object", 455 | properties: { 456 | projectPath: { 457 | type: "string", 458 | description: "Path to the project root directory" 459 | }, 460 | docFile: { 461 | type: "string", 462 | description: "Name of the documentation file to update" 463 | }, 464 | searchContent: { 465 | type: "string", 466 | description: "Content to search for in the file" 467 | }, 468 | replaceContent: { 469 | type: "string", 470 | description: "Content to replace the search content with" 471 | }, 472 | continueToNext: { 473 | type: "boolean", 474 | description: "Whether to continue to the next file after this update" 475 | } 476 | }, 477 | required: ["projectPath", "docFile", "searchContent", "replaceContent"] 478 | } 479 | }, 480 | { 481 | name: "get_doc_content", 482 | description: "Get the current content of a documentation file", 483 | inputSchema: { 484 | type: "object", 485 | properties: { 486 | projectPath: { 487 | type: "string", 488 | description: "Path to the project root directory" 489 | }, 490 | docFile: { 491 | type: "string", 492 | description: "Name of the documentation file to read" 493 | } 494 | }, 495 | required: ["projectPath", "docFile"] 496 | } 497 | }, 498 | { 499 | name: "get_project_info", 500 | description: "Get information about the project structure and files", 501 | inputSchema: { 502 | type: "object", 503 | properties: { 504 | projectPath: { 505 | type: "string", 506 | description: "Path to the project root directory" 507 | } 508 | }, 509 | required: ["projectPath"] 510 | } 511 | }, 512 | { 513 | name: "search_docs", 514 | description: "Search across documentation files with highlighted results", 515 | inputSchema: { 516 | type: "object", 517 | properties: { 518 | projectPath: { 519 | type: "string", 520 | description: "Path to the project root directory" 521 | }, 522 | query: { 523 | type: "string", 524 | description: "Search query to find in documentation" 525 | } 526 | }, 527 | required: ["projectPath", "query"] 528 | } 529 | }, 530 | { 531 | name: "update_metadata", 532 | description: "Update metadata for a documentation file", 533 | inputSchema: { 534 | type: "object", 535 | properties: { 536 | projectPath: { 537 | type: "string", 538 | description: "Path to the project root directory" 539 | }, 540 | docFile: { 541 | type: "string", 542 | description: "Name of the documentation file" 543 | }, 544 | metadata: { 545 | type: "object", 546 | description: "Metadata to update", 547 | properties: { 548 | title: { type: "string" }, 549 | category: { type: "string" }, 550 | tags: { type: "array", items: { type: "string" } } 551 | } 552 | } 553 | }, 554 | required: ["projectPath", "docFile", "metadata"] 555 | } 556 | }, 557 | { 558 | name: "get_related_docs", 559 | description: "Find related documentation files based on metadata", 560 | inputSchema: { 561 | type: "object", 562 | properties: { 563 | projectPath: { 564 | type: "string", 565 | description: "Path to the project root directory" 566 | }, 567 | docFile: { 568 | type: "string", 569 | description: "Name of the documentation file" 570 | } 571 | }, 572 | required: ["projectPath", "docFile"] 573 | } 574 | }, 575 | { 576 | name: "customize_template", 577 | description: "Create or update a custom documentation template", 578 | inputSchema: { 579 | type: "object", 580 | properties: { 581 | templateName: { 582 | type: "string", 583 | description: "Name of the template" 584 | }, 585 | content: { 586 | type: "string", 587 | description: "Template content with {title} placeholder" 588 | }, 589 | metadata: { 590 | type: "object", 591 | description: "Default metadata for the template", 592 | properties: { 593 | category: { type: "string" }, 594 | tags: { type: "array", items: { type: "string" } } 595 | } 596 | } 597 | }, 598 | required: ["templateName", "content"] 599 | } 600 | } 601 | ] 602 | }; 603 | }); 604 | 605 | server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => { 606 | switch (request.params.name) { 607 | case "analyze_existing_docs": { 608 | const { projectPath } = request.params.arguments as { projectPath: string }; 609 | const docsPath = `${projectPath}/.handoff_docs`; 610 | 611 | try { 612 | // Verify docs directory exists 613 | try { 614 | await fs.access(docsPath); 615 | } catch { 616 | throw new McpError( 617 | ErrorCode.InvalidRequest, 618 | `Documentation directory not found at ${docsPath}` 619 | ); 620 | } 621 | 622 | // Reset state 623 | state = { 624 | currentFile: null, 625 | completedFiles: [], 626 | inProgress: false, 627 | lastReadFile: null, 628 | lastReadContent: null, 629 | continueToNext: false, 630 | metadata: {}, 631 | contextCache: {}, 632 | templateOverrides: {} 633 | }; 634 | 635 | // Clear existing search index 636 | searchEngine.removeAll(); 637 | 638 | // Get list of all markdown files in the docs directory 639 | const files = await fs.readdir(docsPath); 640 | const markdownFiles = files.filter(file => file.endsWith('.md')); 641 | 642 | if (markdownFiles.length === 0) { 643 | throw new McpError( 644 | ErrorCode.InvalidRequest, 645 | `No markdown files found in ${docsPath}` 646 | ); 647 | } 648 | 649 | // Analyze each markdown file 650 | for (const doc of markdownFiles) { 651 | const filePath = `${docsPath}/${doc}`; 652 | const content = await fs.readFile(filePath, "utf8"); 653 | 654 | // Use unified/remark to analyze content structure 655 | const analysis = await analyzeContent(content); 656 | 657 | // Use enhanced categorization 658 | const { category, tags } = categorizeContent(doc, content, analysis); 659 | 660 | // Generate metadata 661 | const metadata = { 662 | title: analysis.title || doc.replace(".md", "") 663 | .split(/[_-]/) 664 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 665 | .join(" "), 666 | category, 667 | tags, 668 | lastUpdated: new Date().toISOString() 669 | }; 670 | 671 | // Update metadata for the file 672 | await updateMetadata(filePath, metadata); 673 | 674 | // Find and update related docs 675 | const relatedDocs = await findRelatedDocs(doc, projectPath); 676 | await updateMetadata(filePath, { relatedDocs }); 677 | 678 | // Update search index with full content and metadata 679 | updateSearchIndex(doc, content, { 680 | ...metadata, 681 | relatedDocs 682 | }); 683 | 684 | // Add structured front matter to content if it doesn't already have it 685 | if (!content.startsWith('---')) { 686 | const enhancedContent = `--- 687 | title: ${metadata.title} 688 | category: ${metadata.category} 689 | tags: ${metadata.tags.join(', ')} 690 | lastUpdated: ${metadata.lastUpdated} 691 | relatedDocs: ${relatedDocs.join(', ')} 692 | --- 693 | 694 | ${content}`; 695 | 696 | await fs.writeFile(filePath, enhancedContent); 697 | } 698 | 699 | state.completedFiles.push(doc); 700 | } 701 | 702 | // Get project info for additional context 703 | let gitInfo = {}; 704 | try { 705 | gitInfo = { 706 | remoteUrl: execSync("git config --get remote.origin.url", { cwd: projectPath }).toString().trim(), 707 | branch: execSync("git branch --show-current", { cwd: projectPath }).toString().trim(), 708 | lastCommit: execSync("git log -1 --format=%H", { cwd: projectPath }).toString().trim() 709 | }; 710 | } catch { 711 | // Not a git repository or git not available 712 | } 713 | 714 | return { 715 | content: [ 716 | { 717 | type: "text", 718 | text: JSON.stringify({ 719 | message: "Existing documentation analyzed and enhanced", 720 | docsPath, 721 | files: markdownFiles, 722 | metadata: state.metadata, 723 | gitInfo, 724 | contextCache: { 725 | timestamp: state.contextCache.timestamp, 726 | ttl: CACHE_TTL 727 | } 728 | }, null, 2) 729 | } 730 | ] 731 | }; 732 | } catch (error: unknown) { 733 | const errorMessage = error instanceof Error ? error.message : String(error); 734 | throw new McpError( 735 | ErrorCode.InternalError, 736 | `Error analyzing existing documentation: ${errorMessage}` 737 | ); 738 | } 739 | } 740 | 741 | case "analyze_project_with_metadata": { 742 | const { projectPath } = request.params.arguments as { projectPath: string }; 743 | const docsPath = `${projectPath}/.handoff_docs`; 744 | 745 | try { 746 | // First run the standard analyze_project workflow 747 | await fs.mkdir(docsPath, { recursive: true }); 748 | 749 | // Initialize default documentation files if they don't exist 750 | for (const doc of DEFAULT_DOCS) { 751 | const filePath = `${docsPath}/${doc}`; 752 | try { 753 | await fs.access(filePath); 754 | } catch { 755 | const title = doc.replace(".md", "") 756 | .split(/[_-]/) 757 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 758 | .join(" "); 759 | await fs.writeFile(filePath, TEMPLATE_CONTENT.replace("{title}", title)); 760 | } 761 | } 762 | 763 | // Reset state 764 | state = { 765 | currentFile: null, 766 | completedFiles: [], 767 | inProgress: false, 768 | lastReadFile: null, 769 | lastReadContent: null, 770 | continueToNext: false, 771 | metadata: {}, 772 | contextCache: {}, 773 | templateOverrides: {} 774 | }; 775 | 776 | // Clear existing search index 777 | searchEngine.removeAll(); 778 | 779 | // Now enhance each file with metadata and context 780 | for (const doc of DEFAULT_DOCS) { 781 | const filePath = `${docsPath}/${doc}`; 782 | const content = await fs.readFile(filePath, "utf8"); 783 | 784 | // Use unified/remark to analyze content structure 785 | const analysis = await analyzeContent(content); 786 | 787 | // Use enhanced categorization 788 | const { category, tags } = categorizeContent(doc, content, analysis); 789 | 790 | // Generate metadata 791 | const metadata = { 792 | title: analysis.title || doc.replace(".md", "") 793 | .split(/[_-]/) 794 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 795 | .join(" "), 796 | category, 797 | tags, 798 | lastUpdated: new Date().toISOString() 799 | }; 800 | 801 | // Update metadata for the file 802 | await updateMetadata(filePath, metadata); 803 | 804 | // Find and update related docs 805 | const relatedDocs = await findRelatedDocs(doc, projectPath); 806 | await updateMetadata(filePath, { relatedDocs }); 807 | 808 | // Update search index with full content and metadata 809 | updateSearchIndex(doc, content, { 810 | ...metadata, 811 | relatedDocs 812 | }); 813 | 814 | // Add structured front matter to content 815 | const enhancedContent = `--- 816 | title: ${metadata.title} 817 | category: ${metadata.category} 818 | tags: ${metadata.tags.join(', ')} 819 | lastUpdated: ${metadata.lastUpdated} 820 | relatedDocs: ${relatedDocs.join(', ')} 821 | --- 822 | 823 | ${content}`; 824 | 825 | // Update file with enhanced content 826 | await fs.writeFile(filePath, enhancedContent); 827 | } 828 | 829 | // Get project info for additional context 830 | let gitInfo = {}; 831 | try { 832 | gitInfo = { 833 | remoteUrl: execSync("git config --get remote.origin.url", { cwd: projectPath }).toString().trim(), 834 | branch: execSync("git branch --show-current", { cwd: projectPath }).toString().trim(), 835 | lastCommit: execSync("git log -1 --format=%H", { cwd: projectPath }).toString().trim() 836 | }; 837 | } catch { 838 | // Not a git repository or git not available 839 | } 840 | 841 | return { 842 | content: [ 843 | { 844 | type: "text", 845 | text: JSON.stringify({ 846 | message: "Documentation structure initialized with metadata and context", 847 | docsPath, 848 | files: DEFAULT_DOCS, 849 | metadata: state.metadata, 850 | gitInfo, 851 | contextCache: { 852 | timestamp: state.contextCache.timestamp, 853 | ttl: CACHE_TTL 854 | } 855 | }, null, 2) 856 | } 857 | ] 858 | }; 859 | } catch (error: unknown) { 860 | const errorMessage = error instanceof Error ? error.message : String(error); 861 | throw new McpError( 862 | ErrorCode.InternalError, 863 | `Error initializing documentation with metadata: ${errorMessage}` 864 | ); 865 | } 866 | } 867 | 868 | case "analyze_project": { 869 | const { projectPath } = request.params.arguments as { projectPath: string }; 870 | const docsPath = `${projectPath}/.handoff_docs`; 871 | 872 | try { 873 | await fs.mkdir(docsPath, { recursive: true }); 874 | 875 | // Initialize default documentation files if they don't exist 876 | for (const doc of DEFAULT_DOCS) { 877 | const filePath = `${docsPath}/${doc}`; 878 | try { 879 | await fs.access(filePath); 880 | } catch { 881 | const title = doc.replace(".md", "") 882 | .split(/[_-]/) 883 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 884 | .join(" "); 885 | await fs.writeFile(filePath, TEMPLATE_CONTENT.replace("{title}", title)); 886 | } 887 | } 888 | 889 | state = { 890 | currentFile: null, 891 | completedFiles: [], 892 | inProgress: false, 893 | lastReadFile: null, 894 | lastReadContent: null, 895 | continueToNext: false, 896 | metadata: {}, 897 | contextCache: {}, 898 | templateOverrides: {} 899 | }; 900 | 901 | return { 902 | content: [ 903 | { 904 | type: "text", 905 | text: JSON.stringify({ 906 | message: "Documentation structure initialized", 907 | docsPath, 908 | files: DEFAULT_DOCS 909 | }, null, 2) 910 | } 911 | ] 912 | }; 913 | } catch (error: unknown) { 914 | const errorMessage = error instanceof Error ? error.message : String(error); 915 | throw new McpError( 916 | ErrorCode.InternalError, 917 | `Error initializing documentation: ${errorMessage}` 918 | ); 919 | } 920 | } 921 | 922 | case "read_doc": { 923 | const { projectPath, docFile } = request.params.arguments as { 924 | projectPath: string; 925 | docFile: string; 926 | }; 927 | 928 | try { 929 | const filePath = `${projectPath}/.handoff_docs/${docFile}`; 930 | const content = await fs.readFile(filePath, "utf8"); 931 | 932 | state.lastReadFile = docFile; 933 | state.lastReadContent = content; 934 | state.currentFile = docFile; 935 | state.inProgress = true; 936 | 937 | return { 938 | content: [ 939 | { 940 | type: "text", 941 | text: content 942 | } 943 | ] 944 | }; 945 | } catch (error: unknown) { 946 | const errorMessage = error instanceof Error ? error.message : String(error); 947 | throw new McpError( 948 | ErrorCode.InternalError, 949 | `Error reading documentation: ${errorMessage}` 950 | ); 951 | } 952 | } 953 | 954 | case "update_doc": { 955 | const { projectPath, docFile, searchContent, replaceContent, continueToNext = false } = 956 | request.params.arguments as { 957 | projectPath: string; 958 | docFile: string; 959 | searchContent: string; 960 | replaceContent: string; 961 | continueToNext?: boolean; 962 | }; 963 | 964 | try { 965 | // Validate that the file was read first 966 | if (state.lastReadFile !== docFile || !state.lastReadContent) { 967 | throw new McpError( 968 | ErrorCode.InvalidRequest, 969 | `Must read ${docFile} before updating it` 970 | ); 971 | } 972 | 973 | const filePath = `${projectPath}/.handoff_docs/${docFile}`; 974 | 975 | // Verify the search content exists in the file 976 | if (!state.lastReadContent.includes(searchContent)) { 977 | throw new McpError( 978 | ErrorCode.InvalidRequest, 979 | `Search content not found in ${docFile}` 980 | ); 981 | } 982 | 983 | // Apply the diff 984 | const newContent = state.lastReadContent.replace(searchContent, replaceContent); 985 | await fs.writeFile(filePath, newContent); 986 | 987 | // Update state 988 | state.lastReadContent = newContent; 989 | if (!state.completedFiles.includes(docFile)) { 990 | state.completedFiles.push(docFile); 991 | } 992 | state.continueToNext = continueToNext; 993 | 994 | if (continueToNext) { 995 | const remainingDocs = DEFAULT_DOCS.filter(doc => !state.completedFiles.includes(doc)); 996 | if (remainingDocs.length > 0) { 997 | state.currentFile = remainingDocs[0]; 998 | } else { 999 | state.currentFile = null; 1000 | state.inProgress = false; 1001 | } 1002 | } 1003 | 1004 | return { 1005 | content: [ 1006 | { 1007 | type: "text", 1008 | text: JSON.stringify({ 1009 | message: "Documentation updated successfully", 1010 | file: docFile, 1011 | completedFiles: state.completedFiles, 1012 | nextFile: state.currentFile, 1013 | diff: { 1014 | from: searchContent, 1015 | to: replaceContent 1016 | } 1017 | }, null, 2) 1018 | } 1019 | ] 1020 | }; 1021 | } catch (error: unknown) { 1022 | const errorMessage = error instanceof Error ? error.message : String(error); 1023 | throw new McpError( 1024 | ErrorCode.InternalError, 1025 | `Error updating documentation: ${errorMessage}` 1026 | ); 1027 | } 1028 | } 1029 | 1030 | case "get_doc_content": { 1031 | const { projectPath, docFile } = request.params.arguments as { 1032 | projectPath: string; 1033 | docFile: string; 1034 | }; 1035 | 1036 | try { 1037 | const filePath = `${projectPath}/.handoff_docs/${docFile}`; 1038 | const content = await fs.readFile(filePath, "utf8"); 1039 | 1040 | return { 1041 | content: [ 1042 | { 1043 | type: "text", 1044 | text: content 1045 | } 1046 | ] 1047 | }; 1048 | } catch (error: unknown) { 1049 | const errorMessage = error instanceof Error ? error.message : String(error); 1050 | throw new McpError( 1051 | ErrorCode.InternalError, 1052 | `Error reading documentation: ${errorMessage}` 1053 | ); 1054 | } 1055 | } 1056 | 1057 | case "search_docs": { 1058 | const { projectPath, query } = request.params.arguments as { 1059 | projectPath: string; 1060 | query: string; 1061 | }; 1062 | 1063 | try { 1064 | const results = await searchDocContent(projectPath, query); 1065 | return { 1066 | content: [ 1067 | { 1068 | type: "text", 1069 | text: JSON.stringify({ 1070 | query, 1071 | results, 1072 | cache: { 1073 | timestamp: state.contextCache.timestamp, 1074 | ttl: CACHE_TTL, 1075 | expires: state.contextCache.timestamp ? 1076 | new Date(state.contextCache.timestamp + CACHE_TTL).toISOString() : 1077 | null 1078 | } 1079 | }, null, 2) 1080 | } 1081 | ] 1082 | }; 1083 | } catch (error: unknown) { 1084 | const errorMessage = error instanceof Error ? error.message : String(error); 1085 | throw new McpError( 1086 | ErrorCode.InternalError, 1087 | `Error searching documentation: ${errorMessage}` 1088 | ); 1089 | } 1090 | } 1091 | 1092 | case "update_metadata": { 1093 | const { projectPath, docFile, metadata } = request.params.arguments as { 1094 | projectPath: string; 1095 | docFile: string; 1096 | metadata: Partial; 1097 | }; 1098 | 1099 | try { 1100 | const filePath = `${projectPath}/.handoff_docs/${docFile}`; 1101 | await fs.access(filePath); // Verify file exists 1102 | await updateMetadata(filePath, metadata); 1103 | 1104 | return { 1105 | content: [ 1106 | { 1107 | type: "text", 1108 | text: JSON.stringify({ 1109 | message: "Metadata updated successfully", 1110 | file: docFile, 1111 | metadata: state.metadata[docFile] 1112 | }, null, 2) 1113 | } 1114 | ] 1115 | }; 1116 | } catch (error: unknown) { 1117 | const errorMessage = error instanceof Error ? error.message : String(error); 1118 | throw new McpError( 1119 | ErrorCode.InternalError, 1120 | `Error updating metadata: ${errorMessage}` 1121 | ); 1122 | } 1123 | } 1124 | 1125 | case "get_related_docs": { 1126 | const { projectPath, docFile } = request.params.arguments as { 1127 | projectPath: string; 1128 | docFile: string; 1129 | }; 1130 | 1131 | try { 1132 | const related = await findRelatedDocs(docFile, projectPath); 1133 | return { 1134 | content: [ 1135 | { 1136 | type: "text", 1137 | text: JSON.stringify({ 1138 | file: docFile, 1139 | relatedDocs: related, 1140 | metadata: state.metadata[docFile] 1141 | }, null, 2) 1142 | } 1143 | ] 1144 | }; 1145 | } catch (error: unknown) { 1146 | const errorMessage = error instanceof Error ? error.message : String(error); 1147 | throw new McpError( 1148 | ErrorCode.InternalError, 1149 | `Error finding related docs: ${errorMessage}` 1150 | ); 1151 | } 1152 | } 1153 | 1154 | case "customize_template": { 1155 | const { templateName, content, metadata } = request.params.arguments as { 1156 | templateName: string; 1157 | content: string; 1158 | metadata?: Partial; 1159 | }; 1160 | 1161 | try { 1162 | state.templateOverrides[templateName] = { 1163 | name: templateName, 1164 | content, 1165 | metadata: metadata || {} 1166 | }; 1167 | 1168 | return { 1169 | content: [ 1170 | { 1171 | type: "text", 1172 | text: JSON.stringify({ 1173 | message: "Template customized successfully", 1174 | templateName, 1175 | availableTemplates: [ 1176 | ...Object.keys(TEMPLATES), 1177 | ...Object.keys(state.templateOverrides) 1178 | ] 1179 | }, null, 2) 1180 | } 1181 | ] 1182 | }; 1183 | } catch (error: unknown) { 1184 | const errorMessage = error instanceof Error ? error.message : String(error); 1185 | throw new McpError( 1186 | ErrorCode.InternalError, 1187 | `Error customizing template: ${errorMessage}` 1188 | ); 1189 | } 1190 | } 1191 | 1192 | case "get_project_info": { 1193 | const { projectPath } = request.params.arguments as { projectPath: string }; 1194 | 1195 | try { 1196 | // Get git info if available 1197 | let gitInfo = {}; 1198 | try { 1199 | gitInfo = { 1200 | remoteUrl: execSync("git config --get remote.origin.url", { cwd: projectPath }).toString().trim(), 1201 | branch: execSync("git branch --show-current", { cwd: projectPath }).toString().trim(), 1202 | lastCommit: execSync("git log -1 --format=%H", { cwd: projectPath }).toString().trim() 1203 | }; 1204 | } catch { 1205 | // Not a git repository or git not available 1206 | } 1207 | 1208 | // Get package.json if it exists 1209 | let packageInfo = {}; 1210 | try { 1211 | const packageJson = await fs.readFile(`${projectPath}/package.json`, "utf8"); 1212 | packageInfo = JSON.parse(packageJson); 1213 | } catch { 1214 | // No package.json or invalid JSON 1215 | } 1216 | 1217 | // Get directory structure 1218 | const getDirectoryStructure = async (dir: string, depth = 3): Promise => { 1219 | if (depth === 0) return "..."; 1220 | 1221 | const items = await fs.readdir(dir, { withFileTypes: true }); 1222 | const structure: Record = {}; 1223 | 1224 | for (const item of items) { 1225 | if (item.name.startsWith(".") || item.name === "node_modules") continue; 1226 | 1227 | if (item.isDirectory()) { 1228 | structure[item.name] = await getDirectoryStructure(`${dir}/${item.name}`, depth - 1); 1229 | } else { 1230 | structure[item.name] = null; 1231 | } 1232 | } 1233 | 1234 | return structure; 1235 | }; 1236 | 1237 | const projectStructure = await getDirectoryStructure(projectPath); 1238 | 1239 | return { 1240 | content: [ 1241 | { 1242 | type: "text", 1243 | text: JSON.stringify({ 1244 | gitInfo, 1245 | packageInfo, 1246 | projectStructure, 1247 | docsStatus: { 1248 | completed: state.completedFiles, 1249 | current: state.currentFile, 1250 | inProgress: state.inProgress, 1251 | lastRead: state.lastReadFile, 1252 | remaining: DEFAULT_DOCS.filter(doc => !state.completedFiles.includes(doc)) 1253 | } 1254 | }, null, 2) 1255 | } 1256 | ] 1257 | }; 1258 | } catch (error: unknown) { 1259 | const errorMessage = error instanceof Error ? error.message : String(error); 1260 | throw new McpError( 1261 | ErrorCode.InternalError, 1262 | `Error getting project info: ${errorMessage}` 1263 | ); 1264 | } 1265 | } 1266 | 1267 | default: 1268 | throw new McpError( 1269 | ErrorCode.MethodNotFound, 1270 | `Unknown tool: ${request.params.name}` 1271 | ); 1272 | } 1273 | }); 1274 | 1275 | async function main() { 1276 | const transport = new StdioServerTransport(); 1277 | await server.connect(transport); 1278 | console.error("Handoff Docs MCP server running on stdio"); 1279 | } 1280 | 1281 | main().catch((error: unknown) => { 1282 | const errorMessage = error instanceof Error ? error.message : String(error); 1283 | console.error("Server error:", errorMessage); 1284 | process.exit(1); 1285 | }); 1286 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "NodeNext", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "allowJs": true, 13 | "resolveJsonModule": true, 14 | "types": ["node"] 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": ["node_modules"] 18 | } 19 | --------------------------------------------------------------------------------