UNPKG

@mseep/mcp-codex-keeper

Version:

An intelligent MCP server that serves as a guardian of development knowledge, providing AI assistants with curated access to latest documentation and best practices

575 lines 24.2 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import axios from 'axios'; import fs from 'fs/promises'; import path from 'path'; import { FileSystemError, FileSystemManager } from './utils/fs.js'; import { isValidCategory, validateAddDocArgs, validateSearchDocArgs, validateUpdateDocArgs, } from './validators/index.js'; // Environment settings from MCP configuration const ENV = { isLocal: process.env.MCP_ENV === 'local' && process.env.NODE_ENV !== 'production', cacheMaxSize: 104857600, // 100MB cacheMaxAge: 604800000, // 7 days cacheCleanupInterval: 3600000, // 1 hour storagePath: 'data', // Default storage path for local development }; // Storage paths const STORAGE_PATHS = { local: (modulePath) => path.join(modulePath, '..', ENV.storagePath), production: () => path.join(process.env.HOME || process.env.USERPROFILE || '', '.mcp-codex-keeper'), }; // Default documentation sources with best practices and essential references const defaultDocs = [ // Core Development Standards { name: 'SOLID Principles Guide', url: 'https://www.digitalocean.com/community/conceptual-articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design', description: 'Comprehensive guide to SOLID principles in software development', category: 'Standards', tags: ['solid', 'oop', 'design-principles', 'best-practices'], }, { name: 'Design Patterns Catalog', url: 'https://refactoring.guru/design-patterns/catalog', description: 'Comprehensive catalog of software design patterns with examples', category: 'Standards', tags: ['design-patterns', 'architecture', 'best-practices', 'oop'], }, { name: 'Clean Code Principles', url: 'https://gist.github.com/wojteklu/73c6914cc446146b8b533c0988cf8d29', description: 'Universal Clean Code principles for any programming language', category: 'Standards', tags: ['clean-code', 'best-practices', 'code-quality'], }, { name: 'Unit Testing Principles', url: 'https://martinfowler.com/bliki/UnitTest.html', description: "Martin Fowler's guide to unit testing principles and practices", category: 'Standards', tags: ['testing', 'unit-tests', 'best-practices', 'tdd'], }, { name: 'OWASP Top Ten', url: 'https://owasp.org/www-project-top-ten/', description: 'Top 10 web application security risks and prevention', category: 'Standards', tags: ['security', 'web', 'owasp', 'best-practices'], }, { name: 'Conventional Commits', url: 'https://www.conventionalcommits.org/', description: 'Specification for standardized commit messages', category: 'Standards', tags: ['git', 'commits', 'versioning', 'best-practices'], }, { name: 'Semantic Versioning', url: 'https://semver.org/', description: 'Semantic Versioning Specification', category: 'Standards', tags: ['versioning', 'releases', 'best-practices'], }, // Essential Tools { name: 'Git Workflow Guide', url: 'https://www.atlassian.com/git/tutorials/comparing-workflows', description: 'Comprehensive guide to Git workflows and team collaboration', category: 'Tools', tags: ['git', 'version-control', 'workflow', 'collaboration'], }, ]; /** * Main server class for the documentation keeper */ export class DocumentationServer { constructor() { this.docs = []; this.isLocal = ENV.isLocal; this.init() .then(() => this.run()) .catch(console.error); } async init() { // Use environment settings this.isLocal = ENV.isLocal; const serverName = this.isLocal ? 'local-mcp-codex-keeper' : 'aindreyway-mcp-codex-keeper'; // Get storage path based on mode const moduleURL = new URL(import.meta.url); const modulePath = path.dirname(moduleURL.pathname); const storagePath = this.isLocal ? STORAGE_PATHS.local(modulePath) : STORAGE_PATHS.production(); // Create storage directory in production mode if (!this.isLocal) { try { await fs.mkdir(storagePath, { recursive: true }); console.error('Created storage directory:', storagePath); } catch (error) { console.error('Failed to create storage directory:', error); } } this.fsManager = new FileSystemManager(storagePath, { maxSize: ENV.cacheMaxSize, maxAge: ENV.cacheMaxAge, cleanupInterval: ENV.cacheCleanupInterval, }); this.server = new Server({ name: serverName, version: '1.1.10', }, { capabilities: { tools: {}, resources: {}, }, }); if (this.isLocal) { console.error('\n' + '='.repeat(50)); console.error('🔧 RUNNING IN LOCAL DEVELOPMENT MODE'); console.error('Server name: local-codex-keeper'); console.error('Data directory: ' + path.join(modulePath, '..', ENV.storagePath)); console.error('To run production version use: npx @aindreyway/mcp-codex-keeper'); console.error('='.repeat(50) + '\n'); } // Initialize with empty array, will be populated in run() this.docs = []; this.setupToolHandlers(); this.setupResourceHandlers(); this.setupErrorHandlers(); // Log initial documentation state with version indicator console.error(`Available Documentation Categories ${this.isLocal ? '[LOCAL VERSION]' : '[PRODUCTION VERSION]'}:`); const categories = [...new Set(this.docs.map(doc => doc.category))]; categories.forEach(category => { const docsInCategory = this.docs.filter(doc => doc.category === category); console.error(`\n${category}:`); docsInCategory.forEach(doc => { console.error(`- ${doc.name}`); console.error(` ${doc.description}`); console.error(` Tags: ${doc.tags?.join(', ') || 'none'}`); }); }); } /** * Get initial documentation state * This information will be available in the environment details * when the server starts */ getInitialState() { const categories = [...new Set(this.docs.map(doc => doc.category))]; let state = 'Documentation Overview:\n\n'; categories.forEach(category => { const docsInCategory = this.docs.filter(doc => doc.category === category); state += `${category}:\n`; docsInCategory.forEach(doc => { state += `- ${doc.name}\n`; state += ` ${doc.description}\n`; if (doc.tags?.length) { state += ` Tags: ${doc.tags.join(', ')}\n`; } state += '\n'; }); }); return state; } /** * Sets up error handlers for the server */ setupErrorHandlers() { this.server.onerror = (error) => { if (error instanceof FileSystemError) { console.error('[Storage Error]', error.message, error.cause); } else if (error instanceof McpError) { console.error('[MCP Error]', error.message); } else { console.error('[Unexpected Error]', error); } }; process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } /** * Sets up tool handlers for the server */ setupResourceHandlers() { this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: 'docs://sources', name: 'Documentation Sources', description: 'List of all available documentation sources', mimeType: 'application/json', }, ], })); this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { if (request.params.uri === 'docs://sources') { return { contents: [ { uri: request.params.uri, mimeType: 'application/json', text: JSON.stringify(this.docs, null, 2), }, ], }; } throw new McpError(ErrorCode.InvalidRequest, `Unknown resource: ${request.params.uri}`); }); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'list_documentation', description: 'List all available documentation sources. Use this tool to discover relevant documentation before starting tasks to ensure best practices and standards compliance.', inputSchema: { type: 'object', properties: { category: { type: 'string', description: 'Filter documentation by category', }, tag: { type: 'string', description: 'Filter documentation by tag', }, }, }, }, { name: 'add_documentation', description: 'Add a new documentation source. When working on tasks, add any useful documentation you discover to help maintain a comprehensive knowledge base.', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name of the documentation', }, url: { type: 'string', description: 'URL of the documentation', }, description: { type: 'string', description: 'Description of the documentation', }, category: { type: 'string', description: 'Category of the documentation', }, tags: { type: 'array', items: { type: 'string' }, description: 'Tags for additional categorization', }, version: { type: 'string', description: 'Version information', }, }, required: ['name', 'url', 'category'], }, }, { name: 'update_documentation', description: 'Update documentation content from source. Always update relevant documentation before starting a task to ensure you have the latest information and best practices.', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name of the documentation to update', }, force: { type: 'boolean', description: 'Force update even if recently updated', }, }, required: ['name'], }, }, { name: 'search_documentation', description: 'Search through documentation content. Use this to find specific information, best practices, or guidelines relevant to your current task. Remember to check documentation before making important decisions.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query', }, category: { type: 'string', description: 'Filter by category', }, tag: { type: 'string', description: 'Filter by tag', }, }, required: ['query'], }, }, { name: 'remove_documentation', description: 'Remove a documentation source. Use this when you no longer need specific documentation.', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name of the documentation to remove', }, }, required: ['name'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const args = request.params.arguments || {}; switch (request.params.name) { case 'list_documentation': return this.listDocumentation({ category: isValidCategory(args.category) ? args.category : undefined, tag: typeof args.tag === 'string' ? args.tag : undefined, }); case 'add_documentation': return this.addDocumentation(validateAddDocArgs(args)); case 'update_documentation': return this.updateDocumentation(validateUpdateDocArgs(args)); case 'search_documentation': return this.searchDocumentation(validateSearchDocArgs(args)); case 'remove_documentation': return this.removeDocumentation(args.name); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } }); } /** * Lists documentation sources with optional filtering */ async listDocumentation(args) { const { category, tag } = args; let filteredDocs = this.docs; if (category) { filteredDocs = filteredDocs.filter(doc => doc.category === category); } if (tag) { filteredDocs = filteredDocs.filter(doc => doc.tags?.includes(tag)); } return { content: [ { type: 'text', text: JSON.stringify(filteredDocs, null, 2), }, ], }; } /** * Adds new documentation source */ async addDocumentation(args) { const { name, url, description, category, tags, version } = args; // Find existing doc index const existingIndex = this.docs.findIndex(doc => doc.name === name); const updatedDoc = { name, url, description, category, tags, version, lastUpdated: new Date().toISOString(), }; if (existingIndex !== -1) { // Update existing doc this.docs[existingIndex] = updatedDoc; } else { // Add new doc this.docs.push(updatedDoc); } await this.fsManager.saveSources(this.docs); return { content: [ { type: 'text', text: existingIndex !== -1 ? `Updated documentation: ${name}` : `Added documentation: ${name}`, }, ], }; } /** * Updates documentation content from source */ async updateDocumentation(args) { const { name, force } = args; const doc = this.docs.find(d => d.name === name); if (!doc) { throw new McpError(ErrorCode.InvalidRequest, `Documentation "${name}" not found`); } // Skip update if recently updated and not forced if (!force && doc.lastUpdated) { const lastUpdate = new Date(doc.lastUpdated); const hoursSinceUpdate = (Date.now() - lastUpdate.getTime()) / (1000 * 60 * 60); if (hoursSinceUpdate < 24) { return { content: [ { type: 'text', text: `Documentation "${name}" was recently updated. Use force=true to update anyway.`, }, ], }; } } try { const response = await axios.get(doc.url); await this.fsManager.saveDocumentation(name, response.data); doc.lastUpdated = new Date().toISOString(); await this.fsManager.saveSources(this.docs); return { content: [ { type: 'text', text: `Updated documentation: ${name}`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new McpError(ErrorCode.InternalError, `Failed to update documentation: ${errorMessage}`); } } /** * Searches through documentation content */ async searchDocumentation(args) { const { query, category, tag } = args; let results = []; try { const files = await this.fsManager.listDocumentationFiles(); for (const file of files) { const doc = this.docs.find(d => file === `${d.name.toLowerCase().replace(/\s+/g, '_')}.html`); if (doc) { // Apply filters if (category && doc.category !== category) continue; if (tag && !doc.tags?.includes(tag)) continue; // Search content const matches = await this.fsManager.searchInDocumentation(doc.name, query); if (matches) { results.push({ name: doc.name, url: doc.url, category: doc.category, tags: doc.tags, lastUpdated: doc.lastUpdated, }); } } } return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new McpError(ErrorCode.InternalError, `Search failed: ${errorMessage}`); } } /** * Starts the server */ async run() { console.error('\nStarting server...'); try { console.error('Ensuring directories...'); await this.fsManager.ensureDirectories(); console.error('Directories ensured'); console.error('\nLoading documentation sources...'); const savedDocs = await this.fsManager.loadSources(); console.error('Loaded docs:', savedDocs.length); if (savedDocs.length === 0) { console.error('\nFirst time setup - initializing with default documentation...'); console.error('Default docs count:', defaultDocs.length); console.error('Default docs categories:', [...new Set(defaultDocs.map(d => d.category))]); console.error('Default docs:', JSON.stringify(defaultDocs, null, 2).slice(0, 200) + '...'); this.docs = [...defaultDocs]; console.error('\nSaving default docs...'); try { await this.fsManager.saveSources(this.docs); console.error('Default docs saved successfully'); } catch (error) { console.error('Failed to save default docs:', error); if (error instanceof Error) { console.error('Error details:', error.message); console.error('Stack trace:', error.stack); } throw error; } } else { console.error('\nUsing existing docs'); console.error('Categories:', [...new Set(savedDocs.map(d => d.category))]); console.error('Docs:', JSON.stringify(savedDocs, null, 2).slice(0, 200) + '...'); this.docs = savedDocs; } } catch (error) { console.error('\nError during initialization:', error); console.error('Error details:', error instanceof Error ? error.message : String(error)); console.error('Stack trace:', error instanceof Error ? error.stack : 'No stack trace'); console.error('\nFalling back to default docs'); this.docs = [...defaultDocs]; console.error('Saving default docs as fallback...'); await this.fsManager.saveSources(this.docs); console.error('Fallback docs saved'); } // Log initial state before starting server console.error(`\nInitial Documentation State ${this.isLocal ? '[LOCAL VERSION]' : '[PRODUCTION VERSION]'}:`); console.error(this.getInitialState()); const transport = new StdioServerTransport(); await this.server.connect(transport); console.error(`Documentation MCP server running on stdio ${this.isLocal ? '[LOCAL VERSION]' : '[PRODUCTION VERSION]'}`); } /** * Removes documentation source */ async removeDocumentation(name) { const index = this.docs.findIndex(doc => doc.name === name); if (index === -1) { throw new McpError(ErrorCode.InvalidRequest, `Documentation "${name}" not found`); } // Remove from memory and storage this.docs.splice(index, 1); await this.fsManager.saveSources(this.docs); return { content: [ { type: 'text', text: `Removed documentation: ${name}`, }, ], }; } } //# sourceMappingURL=server.js.map