#!/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);
});