From aa57a52598a7dfabdc1cd286a48df810f8113384 Mon Sep 17 00:00:00 2001 From: Kamarthi Date: Tue, 12 Aug 2025 13:44:49 -0400 Subject: [PATCH] feature: Add MCP (Model Context Protocol) server for documentation access Add an MCP server that provides programmatic access to CodeChecker documentation through standardized tools. This enables AI assistants and other tools to search, retrieve, and interact with CodeChecker documentation efficiently. Features: - Search documentation by keywords and topics - Retrieve full document content - List available documentation files - Section-aware search and indexing Also updated .gitignore to exclude Node.js dependencies for future contributors working with the MCP server. --- .gitignore | 4 + codechecker_mcp.js | 364 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 16 ++ 3 files changed, 384 insertions(+) create mode 100644 codechecker_mcp.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore index b59c2b93c7..ff5c4f19dd 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,7 @@ TAGS # mkdocs site + +# Node.js / MCP server dependencies +node_modules/ +package-lock.json diff --git a/codechecker_mcp.js b/codechecker_mcp.js new file mode 100644 index 0000000000..e6c78e3178 --- /dev/null +++ b/codechecker_mcp.js @@ -0,0 +1,364 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); +const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); +const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); +const { + CallToolRequestSchema, + ErrorCode, + ListToolsRequestSchema, + McpError, +} = require('@modelcontextprotocol/sdk/types.js'); + +// Configure paths +const DOCS_PATH = path.join(__dirname, 'docs'); + +class CodeCheckerMCPServer { + constructor() { + this.server = new Server( + { + name: 'codechecker-docs', + version: '0.1.0', + }, + { + capabilities: { + tools: {}, + }, + } + ); + + this.setupToolHandlers(); + this.documentIndex = null; + this.initializeIndex(); + } + + async initializeIndex() { + console.error('Indexing CodeChecker documentation...'); + this.documentIndex = await this.buildDocumentIndex(); + console.error(`Indexed ${Object.keys(this.documentIndex).length} documents`); + } + + async buildDocumentIndex() { + const index = {}; + + const indexDirectory = (dirPath, relativePath = '') => { + const items = fs.readdirSync(dirPath); + + for (const item of items) { + const fullPath = path.join(dirPath, item); + const relativeFilePath = path.join(relativePath, item); + + if (fs.statSync(fullPath).isDirectory()) { + indexDirectory(fullPath, relativeFilePath); + } else if (item.endsWith('.md')) { + try { + const content = fs.readFileSync(fullPath, 'utf8'); + const title = this.extractTitle(content, item); + + index[relativeFilePath] = { + title, + content, + path: relativeFilePath, + size: content.length, + sections: this.extractSections(content) + }; + } catch (error) { + console.error(`Error reading ${fullPath}:`, error.message); + } + } + } + }; + + indexDirectory(DOCS_PATH); + return index; + } + + extractTitle(content, filename) { + // Try to find markdown title + const titleMatch = content.match(/^#\s+(.+)$/m); + if (titleMatch) { + return titleMatch[1].trim(); + } + + // Fallback to filename + return filename.replace('.md', '').replace(/[_-]/g, ' '); + } + + extractSections(content) { + const sections = []; + const lines = content.split('\n'); + let currentSection = null; + let currentContent = []; + + for (const line of lines) { + const headerMatch = line.match(/^(#{1,6})\s+(.+)$/); + + if (headerMatch) { + // Save previous section + if (currentSection) { + sections.push({ + ...currentSection, + content: currentContent.join('\n').trim() + }); + } + + // Start new section + currentSection = { + level: headerMatch[1].length, + title: headerMatch[2].trim(), + line: line + }; + currentContent = [line]; + } else if (currentSection) { + currentContent.push(line); + } + } + + // Add last section + if (currentSection) { + sections.push({ + ...currentSection, + content: currentContent.join('\n').trim() + }); + } + + return sections; + } + + setupToolHandlers() { + this.server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: 'search_codechecker_docs', + description: 'Search CodeChecker documentation for specific topics, configurations, or usage patterns', + inputSchema: { + type: 'object', + properties: { + query: { + type: 'string', + description: 'Search query (keywords, topics, or specific features)' + }, + section: { + type: 'string', + description: 'Optional: specific section to search (analyzer, web, tools, etc.)', + enum: ['analyzer', 'web', 'tools', 'all'] + } + }, + required: ['query'] + } + }, + { + name: 'get_codechecker_document', + description: 'Get the full content of a specific CodeChecker documentation file', + inputSchema: { + type: 'object', + properties: { + document_path: { + type: 'string', + description: 'Path to the document (e.g., "usage.md", "analyzer/user_guide.md")' + } + }, + required: ['document_path'] + } + }, + { + name: 'list_codechecker_docs', + description: 'List available CodeChecker documentation files', + inputSchema: { + type: 'object', + properties: { + filter: { + type: 'string', + description: 'Optional filter for document types (analyzer, web, tools, etc.)' + } + } + } + } + ] + }; + }); + + this.server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + try { + switch (name) { + case 'search_codechecker_docs': + return await this.handleSearchDocs(args); + case 'get_codechecker_document': + return await this.handleGetDocument(args); + case 'list_codechecker_docs': + return await this.handleListDocs(args); + default: + throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); + } + } catch (error) { + if (error instanceof McpError) { + throw error; + } + throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error.message}`); + } + }); + } + + async handleSearchDocs(args) { + const { query, section } = args; + + if (!this.documentIndex) { + throw new McpError(ErrorCode.InternalError, 'Document index not ready'); + } + + const results = []; + const searchTerms = query.toLowerCase().split(/\s+/); + + for (const [docPath, doc] of Object.entries(this.documentIndex)) { + // Filter by section if specified + if (section && section !== 'all') { + if (!docPath.startsWith(section + '/') && !docPath.includes('/' + section + '/')) { + continue; + } + } + + const docContent = doc.content.toLowerCase(); + const docTitle = doc.title.toLowerCase(); + + // Calculate relevance score + let score = 0; + let matchedSections = []; + + for (const term of searchTerms) { + // Title matches are high value + if (docTitle.includes(term)) score += 10; + + // Count content matches + const matches = (docContent.match(new RegExp(term, 'g')) || []).length; + score += matches; + + // Check section matches + for (const section of doc.sections) { + if (section.title.toLowerCase().includes(term) || + section.content.toLowerCase().includes(term)) { + matchedSections.push({ + title: section.title, + preview: this.getPreview(section.content, term, 200) + }); + } + } + } + + if (score > 0) { + results.push({ + document: docPath, + title: doc.title, + score, + preview: this.getPreview(doc.content, searchTerms[0], 300), + matchedSections: matchedSections.slice(0, 3) // Limit to top 3 sections + }); + } + } + + // Sort by relevance + results.sort((a, b) => b.score - a.score); + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + query, + section: section || 'all', + total_results: results.length, + results: results.slice(0, 10) // Top 10 results + }, null, 2) + } + ] + }; + } + + async handleGetDocument(args) { + const { document_path } = args; + + if (!this.documentIndex) { + throw new McpError(ErrorCode.InternalError, 'Document index not ready'); + } + + const doc = this.documentIndex[document_path]; + if (!doc) { + throw new McpError(ErrorCode.InvalidParams, `Document not found: ${document_path}`); + } + + return { + content: [ + { + type: 'text', + text: `# ${doc.title}\n\n${doc.content}` + } + ] + }; + } + + async handleListDocs(args) { + const { filter } = args; + + if (!this.documentIndex) { + throw new McpError(ErrorCode.InternalError, 'Document index not ready'); + } + + let docs = Object.entries(this.documentIndex).map(([path, doc]) => ({ + path, + title: doc.title, + size: doc.size, + sections: doc.sections.length + })); + + if (filter) { + docs = docs.filter(doc => + doc.path.startsWith(filter + '/') || + doc.path.includes('/' + filter + '/') || + doc.title.toLowerCase().includes(filter.toLowerCase()) + ); + } + + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + filter: filter || 'none', + total_documents: docs.length, + documents: docs + }, null, 2) + } + ] + }; + } + + getPreview(content, searchTerm, maxLength = 200) { + const index = content.toLowerCase().indexOf(searchTerm.toLowerCase()); + if (index === -1) { + return content.substring(0, maxLength) + (content.length > maxLength ? '...' : ''); + } + + const start = Math.max(0, index - 50); + const end = Math.min(content.length, start + maxLength); + const preview = content.substring(start, end); + + return (start > 0 ? '...' : '') + preview + (end < content.length ? '...' : ''); + } + + async run() { + const transport = new StdioServerTransport(); + await this.server.connect(transport); + console.error('CodeChecker MCP server running on stdio'); + } +} + +// Run the server +if (require.main === module) { + const server = new CodeCheckerMCPServer(); + server.run().catch(console.error); +} + +module.exports = { CodeCheckerMCPServer }; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000000..eef9fde9c7 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "codechecker-mcp-server", + "version": "0.1.0", + "description": "MCP Server for CodeChecker documentation", + "main": "codechecker-mcp-server.js", + "scripts": { + "start": "node codechecker-mcp-server.js", + "test": "node test-server.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^0.5.0" + }, + "keywords": ["mcp", "codechecker", "documentation"], + "author": "Vignan Kamarthi", + "license": "MIT" +} \ No newline at end of file