UNPKG

claude-historian

Version:

an MCP server for Claude Code conversation history

423 lines (420 loc) 19.2 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { HistorySearchEngine } from './search.js'; import { BeautifulFormatter } from './formatter.js'; class ClaudeHistorianServer { server; searchEngine; formatter; constructor() { this.server = new Server({ name: 'claude-historian', version: '1.0.0', }, { capabilities: { tools: {}, }, }); this.searchEngine = new HistorySearchEngine(); this.formatter = new BeautifulFormatter(); this.setupToolHandlers(); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'search_conversations', description: 'Search through Claude Code conversation history', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query to find relevant conversations', }, project: { type: 'string', description: 'Optional project name to filter results', }, timeframe: { type: 'string', description: 'Time range filter (today, week, month)', }, limit: { type: 'number', description: 'Maximum number of results (default: 50)', default: 50, }, }, required: ['query'], }, }, { name: 'find_file_context', description: 'Find conversations related to a specific file', inputSchema: { type: 'object', properties: { filepath: { type: 'string', description: 'File path to search for in conversation history', }, limit: { type: 'number', description: 'Maximum number of results (default: 20)', default: 20, }, }, required: ['filepath'], }, }, { name: 'find_similar_queries', description: 'Find previous similar questions or queries', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Query to find similar previous questions', }, limit: { type: 'number', description: 'Maximum number of results (default: 10)', default: 10, }, }, required: ['query'], }, }, { name: 'get_error_solutions', description: 'Find how similar errors were resolved in the past', inputSchema: { type: 'object', properties: { error_pattern: { type: 'string', description: 'Error message or pattern to search for solutions', }, limit: { type: 'number', description: 'Maximum number of results (default: 10)', default: 10, }, }, required: ['error_pattern'], }, }, { name: 'list_recent_sessions', description: 'List recent conversation sessions', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of sessions (default: 10)', default: 10, }, }, }, }, { name: 'extract_compact_summary', description: 'Get a compact summary of a conversation session', inputSchema: { type: 'object', properties: { session_id: { type: 'string', description: 'Session ID to summarize', }, max_messages: { type: 'number', description: 'Maximum messages to include in summary (default: 10)', default: 10, }, }, required: ['session_id'], }, }, { name: 'find_tool_patterns', description: 'Find patterns of successful tool usage', inputSchema: { type: 'object', properties: { tool_name: { type: 'string', description: 'Optional specific tool name to analyze', }, limit: { type: 'number', description: 'Maximum number of patterns (default: 20)', default: 20, }, }, }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const { name, arguments: args } = request.params; switch (name) { case 'search_conversations': { const searchResult = await this.searchEngine.searchConversations(args?.query, args?.project, args?.timeframe, args?.limit || 50); return { content: [ { type: 'text', text: this.formatter.formatSearchConversations(searchResult), }, ], }; } case 'find_file_context': { const fileContexts = await this.searchEngine.findFileContext(args?.filepath, args?.limit || 20); return { content: [ { type: 'text', text: this.formatter.formatFileContext(fileContexts, args?.filepath), }, ], }; } case 'find_similar_queries': { const similarQueries = await this.searchEngine.findSimilarQueries(args?.query, args?.limit || 10); return { content: [ { type: 'text', text: this.formatter.formatSimilarQueries(similarQueries, args?.query), }, ], }; } case 'get_error_solutions': { const errorSolutions = await this.searchEngine.getErrorSolutions(args?.error_pattern, args?.limit || 10); return { content: [ { type: 'text', text: this.formatter.formatErrorSolutions(errorSolutions, args?.error_pattern), }, ], }; } case 'list_recent_sessions': { // Enhanced session listing with smart activity detection const limit = args?.limit || 10; const smartSessions = await this.getSmartRecentSessions(limit); return { content: [ { type: 'text', text: smartSessions, }, ], }; } case 'extract_compact_summary': { const sessionId = args?.session_id; const maxMessages = args?.max_messages || 10; // Generate intelligent summary from recent tool usage and outcomes const summary = await this.generateSmartSummary(sessionId, maxMessages); return { content: [ { type: 'text', text: summary, }, ], }; } case 'find_tool_patterns': { const toolPatterns = await this.searchEngine.getToolPatterns(args?.tool_name, args?.limit || 20); return { content: [ { type: 'text', text: this.formatter.formatToolPatterns(toolPatterns, args?.tool_name), }, ], }; } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { console.error('Tool execution error:', error); throw new McpError(ErrorCode.InternalError, `Error executing ${request.params.name}: ${error}`); } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Claude Historian MCP server running on stdio'); } async generateSmartSummary(sessionId, maxMessages) { try { // Search for recent activity related to the session const recentActivity = await this.searchEngine.searchConversations(sessionId || 'recent tool usage outcomes', undefined, 'today', maxMessages); if (!recentActivity.messages.length) { return `[⌐◉_◉] No recent activity found for session: ${sessionId}`; } // Extract key information: tools used, files changed, outcomes const toolsUsed = new Set(); const filesReferenced = new Set(); const outcomes = []; recentActivity.messages.forEach((msg) => { // Collect tools used msg.context?.toolsUsed?.forEach((tool) => toolsUsed.add(tool)); // Collect files referenced msg.context?.filesReferenced?.forEach((file) => filesReferenced.add(file)); // Extract key outcomes from assistant messages if (msg.type === 'assistant' && msg.content.length > 100) { const lines = msg.content .split('\n') .filter((line) => line.includes('✅') || line.includes('Fixed') || line.includes('Created') || line.includes('Updated') || line.includes('Completed')); outcomes.push(...lines.slice(0, 2)); // Max 2 outcomes per message } }); // Generate compact summary let summary = `[⌐◉_◉] Smart Summary (${recentActivity.messages.length} messages)\n\n`; if (toolsUsed.size > 0) { summary += `**Tools Used:** ${Array.from(toolsUsed).slice(0, 5).join(', ')}\n`; } if (filesReferenced.size > 0) { summary += `**Files Modified:** ${Array.from(filesReferenced).slice(0, 3).join(', ')}\n`; } if (outcomes.length > 0) { summary += `**Key Outcomes:**\n${outcomes .slice(0, 3) .map((o) => `• ${o.replace(/[✅🔧]/gu, '').trim()}`) .join('\n')}\n`; } summary += `\n**Duration:** ${Math.round(recentActivity.executionTime)}ms | **Relevance:** High`; return summary; } catch (error) { return `[⌐◉_◉] Summary generation failed: ${error}`; } } async getSmartRecentSessions(limit) { try { // Get recent activity instead of just session metadata const recentActivity = await this.searchEngine.searchConversations('recent activity projects files tools', undefined, 'week', limit * 3); if (!recentActivity.messages.length) { return `[⌐○_○] No recent sessions found`; } // Group by project and extract session info const projectSessions = new Map(); recentActivity.messages.forEach((msg) => { const project = msg.projectPath || 'unknown'; if (!projectSessions.has(project)) { projectSessions.set(project, { lastActivity: msg.timestamp || 'unknown', messageCount: 0, toolsUsed: new Set(), filesModified: new Set(), }); } const session = projectSessions.get(project); session.messageCount++; msg.context?.toolsUsed?.forEach((tool) => session.toolsUsed.add(tool)); msg.context?.filesReferenced?.forEach((file) => session.filesModified.add(file)); // Update to most recent activity if (msg.timestamp && msg.timestamp > session.lastActivity) { session.lastActivity = msg.timestamp; } }); // Format as smart session list let result = `[⌐○_○] Smart Sessions (${projectSessions.size} active projects)\n\n`; const sortedProjects = Array.from(projectSessions.entries()) .sort(([, a], [, b]) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()) .slice(0, limit); sortedProjects.forEach(([project, session], index) => { const timeAgo = this.getTimeAgo(session.lastActivity); const toolsList = Array.from(session.toolsUsed).slice(0, 3).join(', '); const filesList = Array.from(session.filesModified).slice(0, 2).join(', '); result += `${index + 1}. **${project}** - ${timeAgo}\n`; result += ` ${session.messageCount} messages`; if (toolsList) result += ` | Tools: ${toolsList}`; if (filesList) result += ` | Files: ${filesList}`; result += `\n\n`; }); return result; } catch (error) { return `[⌐○_○] Session listing failed: ${error}`; } } getTimeAgo(timestamp) { try { const now = new Date(); const then = new Date(timestamp); const diffMs = now.getTime() - then.getTime(); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffHours / 24); if (diffDays > 0) return `${diffDays}d ago`; if (diffHours > 0) return `${diffHours}h ago`; return 'Recent'; } catch { return 'Unknown time'; } } } // Handle command line arguments const args = process.argv.slice(2); if (args.includes('--help') || args.includes('-h')) { console.log(` Claude Historian - MCP Server for Claude Code History Search Usage: npx claude-historian # Start MCP server (stdio mode) npx claude-historian --config # Show configuration snippet npx claude-historian --help # Show this help Installation: claude mcp add claude-historian -- npx claude-historian Configuration snippet for ~/.claude/.claude.json: { "claude-historian": { "command": "npx", "args": ["claude-historian"], "env": {} } } `); process.exit(0); } if (args.includes('--config')) { console.log(JSON.stringify({ 'claude-historian': { command: 'npx', args: ['claude-historian'], env: {}, }, }, null, 2)); process.exit(0); } // Start the server const server = new ClaudeHistorianServer(); server.run().catch(console.error); //# sourceMappingURL=index.js.map