Skip to main content
Glama
index.ts15.1 kB
#!/usr/bin/env node /** * Second Brain MCP Server * MCP server for creating markdown notes from Claude Code sessions */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { saveNote, getAllNoteFiles, loadNoteMetadata } from './services/storage.js'; import { detectPattern } from './services/patternDetector.js'; import { getComplexity, getKeyFiles } from './services/analysisUtils.js'; import { generateWeeklyReport, formatWeeklyReport } from './services/reportGenerator.js'; import { searchNotes } from './services/searchEngine.js'; import type { SessionNote } from './types/session.js'; import { isCaptureSessionNoteArgs, isSearchNotesArgs, isGenerateWeeklyReportArgs, validatePattern, validateComplexity, } from './types/toolArguments.js'; import { logger } from './utils/logger.js'; import { truncateString, validateStringArray, clampNumber, validatePathWithinBase, MAX_LENGTHS, } from './utils/validation.js'; import { getNotesDirectory } from './services/storage.js'; /** * Create and configure the MCP server */ const server = new Server( { name: 'second-brain-mcp', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); /** * Handler for listing available tools */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'capture_session_note', description: 'Capture and save a markdown note from a Claude Code session with auto-generated description. ' + 'Include commands executed, file changes, conversation summary, and code snippets. ' + 'Automatically generates a human-like overview of what was accomplished in the session. ' + 'Notes are organized by project/topic in subdirectories.', inputSchema: { type: 'object', properties: { summary: { type: 'string', description: 'High-level summary of what was accomplished in this session', }, projectName: { type: 'string', description: 'Name of the project (used for organizing notes into subdirectories)', }, topic: { type: 'string', description: 'Optional topic or feature being worked on', }, commands: { type: 'array', description: 'List of commands that were executed', items: { type: 'object', properties: { command: { type: 'string', description: 'The command that was executed', }, description: { type: 'string', description: 'Description of what the command does', }, output: { type: 'string', description: 'Output from the command (optional)', }, timestamp: { type: 'string', description: 'When the command was executed (ISO format)', }, }, required: ['command'], }, }, fileChanges: { type: 'array', description: 'List of files that were created, modified, or deleted', items: { type: 'object', properties: { path: { type: 'string', description: 'File path', }, type: { type: 'string', enum: ['created', 'modified', 'deleted'], description: 'Type of change', }, description: { type: 'string', description: 'Description of the changes made', }, diff: { type: 'string', description: 'Git-style diff of changes (optional)', }, }, required: ['path', 'type'], }, }, codeSnippets: { type: 'array', description: 'Important code snippets from the session', items: { type: 'object', properties: { language: { type: 'string', description: 'Programming language for syntax highlighting', }, code: { type: 'string', description: 'The code snippet', }, description: { type: 'string', description: 'Description of what this code does', }, filePath: { type: 'string', description: 'Path to the file this snippet is from', }, }, required: ['language', 'code'], }, }, tags: { type: 'array', description: 'Tags for categorizing this note', items: { type: 'string', }, }, workingDirectory: { type: 'string', description: 'The working directory for this session', }, notesDirectory: { type: 'string', description: 'Custom directory to save notes (defaults to ~/notes or $SECOND_BRAIN_NOTES_DIR)', }, }, required: ['summary'], }, }, { name: 'generate_weekly_report', description: 'Generate a weekly summary report of all coding sessions. ' + 'Includes statistics, project breakdown, work patterns, complexity distribution, and top learnings.', inputSchema: { type: 'object', properties: { notesDirectory: { type: 'string', description: 'Directory containing notes (defaults to ~/notes or $SECOND_BRAIN_NOTES_DIR)', }, }, required: [], }, }, { name: 'search_notes', description: 'Search session notes with filters and relevance ranking. ' + 'Filter by project, tags, pattern, complexity, date range, or text query. ' + 'Results are sorted by relevance score.', inputSchema: { type: 'object', properties: { notesDirectory: { type: 'string', description: 'Directory containing notes (defaults to ~/notes or $SECOND_BRAIN_NOTES_DIR)', }, query: { type: 'string', description: 'Text to search for in notes', }, projectName: { type: 'string', description: 'Filter by project name', }, tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags (notes must have at least one)', }, pattern: { type: 'string', enum: ['new-feature', 'bug-fix', 'refactoring', 'documentation', 'configuration', 'testing', 'mixed'], description: 'Filter by session pattern', }, complexity: { type: 'string', enum: ['simple', 'moderate', 'complex'], description: 'Filter by complexity level', }, startDate: { type: 'string', description: 'Filter by start date (ISO format)', }, endDate: { type: 'string', description: 'Filter by end date (ISO format)', }, similarTo: { type: 'string', description: 'Path to note for similarity search. When provided, results are ranked by similarity to this note.', }, }, required: [], }, }, ], }; }); /** * Handler for tool execution */ server.setRequestHandler(CallToolRequestSchema, async (request) => { const toolName = request.params.name; const rawArgs = request.params.arguments; // Handle capture_session_note tool if (toolName === 'capture_session_note') { // Validate arguments if (!isCaptureSessionNoteArgs(rawArgs)) { return { content: [{ type: 'text', text: 'Invalid arguments: summary is required' }], isError: true, }; } // Apply input validation and sanitization const args = { summary: truncateString(rawArgs.summary, MAX_LENGTHS.summary)!, projectName: truncateString(rawArgs.projectName, MAX_LENGTHS.projectName), topic: truncateString(rawArgs.topic, MAX_LENGTHS.topic), commands: rawArgs.commands, fileChanges: rawArgs.fileChanges, codeSnippets: rawArgs.codeSnippets, tags: validateStringArray(rawArgs.tags, MAX_LENGTHS.tag), workingDirectory: rawArgs.workingDirectory, notesDirectory: rawArgs.notesDirectory, }; // Build the session note const note: SessionNote = { summary: args.summary, projectName: args.projectName, topic: args.topic, timestamp: new Date().toISOString(), commands: args.commands, fileChanges: args.fileChanges, codeSnippets: args.codeSnippets, tags: args.tags, workingDirectory: args.workingDirectory || process.cwd(), }; try { // Perform session analysis (simplified) logger.debug('Analyzing session...'); const patternResult = detectPattern(note); const { level, fileCount } = getComplexity(note.fileChanges); const keyFiles = getKeyFiles(note.fileChanges); note.analysis = { pattern: patternResult.pattern, patternConfidence: patternResult.confidence, complexity: level, fileCount, keyFiles, }; logger.debug('Analysis complete', { pattern: patternResult.pattern, complexity: level, fileCount }); // Save the note const filePath = await saveNote(note, { notesDirectory: args.notesDirectory, }); return { content: [ { type: 'text', text: `Session note saved successfully!\n\nFile: ${filePath}\n\nThe note has been saved to your second brain.`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Error saving note: ${errorMessage}`, }, ], isError: true, }; } } // Handle generate_weekly_report tool if (toolName === 'generate_weekly_report') { if (!isGenerateWeeklyReportArgs(rawArgs)) { return { content: [{ type: 'text', text: 'Invalid arguments for generate_weekly_report' }], isError: true, }; } try { // Get notes directory with consistent fallback const notesDir = getNotesDirectory({ notesDirectory: rawArgs.notesDirectory }); // Get all note files const noteFiles = await getAllNoteFiles({ notesDirectory: notesDir }); // Load metadata for all notes const notes: SessionNote[] = []; for (const filePath of noteFiles) { const metadata = await loadNoteMetadata(filePath); if (metadata) { notes.push(metadata as SessionNote); } } // Generate weekly report const report = generateWeeklyReport(notes); const reportMarkdown = formatWeeklyReport(report); return { content: [ { type: 'text', text: reportMarkdown, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Error generating weekly report: ${errorMessage}`, }, ], isError: true, }; } } // Handle search_notes tool if (toolName === 'search_notes') { if (!isSearchNotesArgs(rawArgs)) { return { content: [{ type: 'text', text: 'Invalid arguments for search_notes' }], isError: true, }; } try { // Get notes directory with consistent fallback const notesDir = getNotesDirectory({ notesDirectory: rawArgs.notesDirectory }); const filters = { query: truncateString(rawArgs.query, MAX_LENGTHS.query), projectName: rawArgs.projectName, tags: validateStringArray(rawArgs.tags, MAX_LENGTHS.tag), pattern: validatePattern(rawArgs.pattern), complexity: validateComplexity(rawArgs.complexity), startDate: rawArgs.startDate, endDate: rawArgs.endDate, }; const results = await searchNotes(notesDir, filters); if (results.length === 0) { return { content: [ { type: 'text', text: 'No notes found matching your search criteria.', }, ], }; } // Format results let output = `Found ${results.length} matching note(s):\n\n`; for (const result of results) { output += `**${result.note.projectName || 'Unnamed Project'}**`; if (result.note.topic) { output += ` - ${result.note.topic}`; } output += `\n`; output += `Relevance: ${result.relevanceScore.toFixed(1)}\n`; output += `Summary: ${result.note.summary}\n`; output += `Date: ${new Date(result.note.timestamp).toLocaleDateString()}\n`; if (result.note.tags && result.note.tags.length > 0) { output += `Tags: ${result.note.tags.join(', ')}\n`; } output += `File: ${result.filePath}\n\n`; } return { content: [ { type: 'text', text: output, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Error searching notes: ${errorMessage}`, }, ], isError: true, }; } } // Unknown tool throw new Error(`Unknown tool: ${toolName}`); }); /** * Start the server */ async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Second Brain MCP Server running on stdio'); } main().catch((error) => { console.error('Fatal error in main():', error); process.exit(1); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/VoCoufi/second-brain-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server