# Adding Custom Tools
Guide for extending the MCP server with AI workflows, integrations, and custom features.
## Quick Start
Adding a new tool is as simple as:
1. Create a new file in `src/tools/your-tool.ts`
2. Extend the `BaseTool` class
3. Import and register it in `src/index.ts`
4. Rebuild and deploy
## Example 1: Simple Custom Tool
### Create `src/tools/count-notes.ts`:
```typescript
import { BaseTool } from './base.tool.js';
export class CountNotesTool extends BaseTool {
readonly name = 'count_notes';
readonly description = 'Count total number of notes in the vault';
readonly inputSchema = {
type: 'object' as const,
properties: {
pattern: {
type: 'string',
description: 'Optional glob pattern to filter (default: **/*.md)',
default: '**/*.md',
},
},
};
async execute(params: { pattern?: string }) {
try {
const files = await this.vault.list(params.pattern ?? '**/*.md');
return {
success: true,
count: files.length,
pattern: params.pattern ?? '**/*.md',
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to count notes',
};
}
}
}
```
### Register in `src/index.ts`:
```typescript
import { CountNotesTool } from './tools/count-notes.js';
// In registerTools() method, add to the array:
const toolInstances: BaseTool[] = [
new ReadNoteTool(),
new WriteNoteTool(),
new SearchNotesTool(),
new ListNotesTool(),
new CountNotesTool(), // ← Your new tool!
];
```
Done! Rebuild and your tool is available.
## Example 2: AI Workflow - Auto-Summarize Notes
### Create `src/tools/ai-summarize.ts`:
```typescript
import { BaseTool } from './base.tool.js';
export class AISummarizeTool extends BaseTool {
readonly name = 'summarize_notes';
readonly description = 'Generate AI summary of multiple notes';
readonly inputSchema = {
type: 'object' as const,
properties: {
pattern: {
type: 'string',
description: 'Pattern to match notes (e.g., "daily/**/*.md")',
},
output: {
type: 'string',
description: 'Where to save the summary',
default: 'summaries/auto-summary.md',
},
},
required: ['pattern'],
};
async execute(params: { pattern: string; output?: string }) {
try {
// Get all matching notes
const files = await this.vault.list(params.pattern);
// Read their content
const contents: string[] = [];
for (const file of files.slice(0, 10)) { // Limit to 10 notes
const note = await this.vault.read(file);
contents.push(`## ${file}\n\n${note.content}\n\n---\n\n`);
}
// Combine for summary
const combined = contents.join('');
// In a real implementation, you'd call an AI API here
// For now, just create a simple summary
const summary = `# Auto-Generated Summary\n\nGenerated: ${new Date().toISOString()}\n\nFound ${files.length} notes matching "${params.pattern}".\n\n## Combined Content\n\n${combined}`;
// Write the summary
await this.vault.write(params.output ?? 'summaries/auto-summary.md', summary);
return {
success: true,
notesProcessed: files.length,
summaryPath: params.output ?? 'summaries/auto-summary.md',
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Summarization failed',
};
}
}
}
```
## Example 3: External Integration - Google Calendar
### Create `src/tools/calendar-sync.ts`:
```typescript
import { BaseTool } from './base.tool.js';
export class CalendarSyncTool extends BaseTool {
readonly name = 'create_calendar_event';
readonly description = 'Create a Google Calendar event from a note';
readonly inputSchema = {
type: 'object' as const,
properties: {
notePath: {
type: 'string',
description: 'Path to the note containing event details',
},
title: {
type: 'string',
description: 'Event title',
},
date: {
type: 'string',
description: 'Event date (ISO format: 2025-01-15T10:00:00)',
},
duration: {
type: 'number',
description: 'Duration in minutes (default: 60)',
default: 60,
},
},
required: ['notePath', 'title', 'date'],
};
async execute(params: { notePath: string; title: string; date: string; duration?: number }) {
try {
// Read the note for additional context
const note = await this.vault.read(params.notePath);
// Here you would integrate with Google Calendar API
// For demo purposes, we'll just create a note documenting the event
const eventNote = `---
title: ${params.title}
type: calendar-event
date: ${params.date}
duration: ${params.duration ?? 60}
---
# ${params.title}
**Date**: ${params.date}
**Duration**: ${params.duration ?? 60} minutes
## Details
${note.content}
## Status
- [ ] Event created in Google Calendar
- [ ] Reminder set
---
*Created from: ${params.notePath}*
`;
const eventPath = `events/${params.title.toLowerCase().replace(/\s+/g, '-')}.md`;
await this.vault.write(eventPath, eventNote);
return {
success: true,
message: 'Event note created',
eventPath,
// In real implementation: calendarEventId, calendarLink, etc.
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Calendar sync failed',
};
}
}
}
```
## Example 4: Task Management - Extract TODOs
### Create `src/tools/extract-tasks.ts`:
```typescript
import { BaseTool } from './base.tool.js';
export class ExtractTasksTool extends BaseTool {
readonly name = 'extract_tasks';
readonly description = 'Extract all TODO items from notes and create a task list';
readonly inputSchema = {
type: 'object' as const,
properties: {
pattern: {
type: 'string',
description: 'Pattern to search (default: all notes)',
default: '**/*.md',
},
outputPath: {
type: 'string',
description: 'Where to save the task list',
default: 'tasks/all-tasks.md',
},
},
};
async execute(params: { pattern?: string; outputPath?: string }) {
try {
const files = await this.vault.list(params.pattern ?? '**/*.md');
const allTasks: Array<{ file: string; tasks: string[] }> = [];
// Regex to match TODO items
const todoRegex = /- \[ \] (.+)/g;
for (const file of files) {
const note = await this.vault.read(file);
const tasks = Array.from(note.content.matchAll(todoRegex), m => m[1]);
if (tasks.length > 0) {
allTasks.push({ file, tasks });
}
}
// Generate task list
let taskList = `# All Tasks\n\nGenerated: ${new Date().toLocaleString()}\n\n`;
for (const { file, tasks } of allTasks) {
taskList += `## ${file}\n\n`;
for (const task of tasks) {
taskList += `- [ ] ${task} (from [[${file}]])\n`;
}
taskList += '\n';
}
taskList += `\n---\n\n**Total**: ${allTasks.reduce((sum, t) => sum + t.tasks.length, 0)} tasks from ${allTasks.length} notes`;
await this.vault.write(params.outputPath ?? 'tasks/all-tasks.md', taskList);
return {
success: true,
totalTasks: allTasks.reduce((sum, t) => sum + t.tasks.length, 0),
notesWithTasks: allTasks.length,
outputPath: params.outputPath ?? 'tasks/all-tasks.md',
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Task extraction failed',
};
}
}
}
```
## Best Practices
### 1. Error Handling
Always wrap your tool execution in try-catch:
```typescript
async execute(params: any) {
try {
// Your logic here
return { success: true, /* ...results */ };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Operation failed',
};
}
}
```
### 2. Input Validation
Use the `inputSchema` to clearly define what parameters your tool accepts:
```typescript
readonly inputSchema = {
type: 'object' as const,
properties: {
requiredParam: {
type: 'string',
description: 'Clear description of what this parameter does',
},
optionalParam: {
type: 'number',
description: 'This one is optional',
default: 42,
},
},
required: ['requiredParam'], // Specify required parameters
};
```
### 3. Return Consistent Results
Always return an object with `success` field:
```typescript
// Success case
return {
success: true,
data: /* your data */,
message: 'Operation completed',
};
// Error case
return {
success: false,
error: 'What went wrong',
};
```
### 4. Keep Tools Focused
Each tool should do one thing well. If a tool is getting complex, split it into multiple tools.
Bad:
```typescript
class MegaTool {
// Handles notes, tasks, calendar, emails, ...
}
```
Good:
```typescript
class ReadNoteTool { /* ... */ }
class CreateTaskTool { /* ... */ }
class CalendarSyncTool { /* ... */ }
```
## Deployment
After adding new tools:
```bash
# Test locally
npm run dev:http
# Build for production
npm run build
# If on server
cd /opt/obsidian-mcp-server
git pull
docker-compose down
docker-compose build
docker-compose up -d
```
## Testing
Create a simple test in Claude:
```
Use the count_notes tool to count all notes in my vault
```
Then verify the result makes sense!
## Integration Ideas
- **Spaced Repetition**: Extract notes for review based on dates
- **Knowledge Graph**: Analyze note connections and backlinks
- **Daily Digest**: Email yourself a summary of recent notes
- **Voice Notes**: Transcribe audio files and create notes
- **Weather/News**: Automatically append to daily notes
- **Habit Tracking**: Parse habit data and generate charts
- **Pomodoro Timer**: Log work sessions as notes
- **Reading List**: Extract URLs from notes and check status
The possibilities are endless! 🚀