import { Tool } from "@modelcontextprotocol/sdk/types.js";
import {
getVaultPath,
slugify,
timestamp,
writeNote,
appendToNote,
searchNotes,
readNote,
getDailyNotePath,
findBacklinks,
listRecentNotes,
type SearchResult,
} from "../utils/vault.js";
// Tool definitions
export const tools: Tool[] = [
{
name: "write_session_report",
description: `Write a session report to the Obsidian vault. Use this at the end of coding sessions,
after completing significant tasks, or when wrapping up work phases. The report will be saved
with proper frontmatter and timestamps.`,
inputSchema: {
type: "object",
properties: {
title: {
type: "string",
description: "Short title for the session (e.g., 'Refactored auth module')",
},
summary: {
type: "string",
description: "Brief summary of what was accomplished",
},
details: {
type: "string",
description: "Detailed description of changes, decisions, and context",
},
files_changed: {
type: "array",
items: { type: "string" },
description: "List of files that were modified",
},
next_steps: {
type: "array",
items: { type: "string" },
description: "List of follow-up tasks or TODOs",
},
tags: {
type: "array",
items: { type: "string" },
description: "Tags for categorization (e.g., ['refactoring', 'auth', 'security'])",
},
project: {
type: "string",
description: "Project name for organization",
},
folder: {
type: "string",
description: "Subfolder within Claude-Sessions (default: uses project name or root)",
},
},
required: ["title", "summary"],
},
},
{
name: "write_note",
description: `Write or create a note in the Obsidian vault. Can be used for documentation,
meeting notes, ideas, or any other content.`,
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Relative path in vault (e.g., 'Projects/my-project/notes.md')",
},
title: {
type: "string",
description: "Note title (used in frontmatter)",
},
content: {
type: "string",
description: "Markdown content of the note",
},
tags: {
type: "array",
items: { type: "string" },
description: "Tags for the note",
},
frontmatter: {
type: "object",
description: "Additional frontmatter fields",
},
},
required: ["path", "content"],
},
},
{
name: "append_to_note",
description: `Append content to an existing note. Useful for adding entries to running logs,
daily notes, or accumulating information.`,
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Relative path to the note",
},
content: {
type: "string",
description: "Content to append",
},
add_timestamp: {
type: "boolean",
description: "Whether to add a timestamp header (default: true)",
},
},
required: ["path", "content"],
},
},
{
name: "append_to_daily_note",
description: `Append content to today's daily note. Creates the daily note if it doesn't exist.`,
inputSchema: {
type: "object",
properties: {
content: {
type: "string",
description: "Content to append to daily note",
},
section: {
type: "string",
description: "Section header to append under (e.g., '## Claude Sessions')",
},
daily_folder: {
type: "string",
description: "Folder for daily notes (default: 'Daily Notes')",
},
},
required: ["content"],
},
},
{
name: "search_notes",
description: `Search notes in the vault by content, tags, or folder.`,
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query (searches in content and titles)",
},
tags: {
type: "array",
items: { type: "string" },
description: "Filter by tags",
},
folder: {
type: "string",
description: "Limit search to a specific folder",
},
limit: {
type: "number",
description: "Maximum number of results (default: 20)",
},
},
required: ["query"],
},
},
{
name: "read_note",
description: `Read the content of a specific note.`,
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Relative path to the note",
},
},
required: ["path"],
},
},
{
name: "list_recent_notes",
description: `List recently modified notes in the vault.`,
inputSchema: {
type: "object",
properties: {
limit: {
type: "number",
description: "Number of notes to return (default: 20)",
},
folder: {
type: "string",
description: "Limit to a specific folder",
},
},
},
},
{
name: "find_backlinks",
description: `Find all notes that link to a specific note.`,
inputSchema: {
type: "object",
properties: {
note_name: {
type: "string",
description: "Name of the note (without .md extension)",
},
},
required: ["note_name"],
},
},
{
name: "create_todo",
description: `Create or add a TODO item. Can create a new TODO note or append to existing TODO list.`,
inputSchema: {
type: "object",
properties: {
task: {
type: "string",
description: "The TODO task description",
},
project: {
type: "string",
description: "Project name for organization",
},
priority: {
type: "string",
enum: ["high", "medium", "low"],
description: "Task priority",
},
due_date: {
type: "string",
description: "Due date (YYYY-MM-DD format)",
},
context: {
type: "string",
description: "Additional context about the task",
},
todo_file: {
type: "string",
description: "Path to TODO file (default: 'TODOs.md')",
},
},
required: ["task"],
},
},
{
name: "link_notes",
description: `Get suggestions for notes that might be related to given content or keywords.`,
inputSchema: {
type: "object",
properties: {
keywords: {
type: "array",
items: { type: "string" },
description: "Keywords to find related notes for",
},
content: {
type: "string",
description: "Content to analyze for potential links",
},
limit: {
type: "number",
description: "Maximum number of suggestions (default: 10)",
},
},
required: ["keywords"],
},
},
];
// Tool handler
export async function handleToolCall(
name: string,
args: Record<string, unknown> | undefined
): Promise<{ content: Array<{ type: string; text: string }> }> {
const vaultPath = getVaultPath();
try {
switch (name) {
case "write_session_report": {
const {
title,
summary,
details,
files_changed,
next_steps,
tags = [],
project,
folder,
} = args as {
title: string;
summary: string;
details?: string;
files_changed?: string[];
next_steps?: string[];
tags?: string[];
project?: string;
folder?: string;
};
const ts = timestamp();
const targetFolder = folder || project || "";
const fileName = `${ts}-${slugify(title)}.md`;
const relativePath = `Claude-Sessions/${targetFolder}/${fileName}`.replace(
/\/+/g,
"/"
);
let content = `# ${title}\n\n`;
content += `## Summary\n\n${summary}\n\n`;
if (details) {
content += `## Details\n\n${details}\n\n`;
}
if (files_changed && files_changed.length > 0) {
content += `## Files Changed\n\n`;
for (const file of files_changed) {
content += `- \`${file}\`\n`;
}
content += "\n";
}
if (next_steps && next_steps.length > 0) {
content += `## Next Steps\n\n`;
for (const step of next_steps) {
content += `- [ ] ${step}\n`;
}
content += "\n";
}
const frontmatter: Record<string, unknown> = {
date: new Date().toISOString(),
type: "session-report",
tags: ["claude-code", "session-report", ...(tags || [])],
};
if (project) frontmatter.project = project;
const fullPath = await writeNote(
vaultPath,
relativePath,
content,
frontmatter
);
return {
content: [
{
type: "text",
text: `Session report created: ${relativePath}\nFull path: ${fullPath}`,
},
],
};
}
case "write_note": {
const { path: notePath, title, content, tags, frontmatter = {} } = args as {
path: string;
title?: string;
content: string;
tags?: string[];
frontmatter?: Record<string, unknown>;
};
const fm: Record<string, unknown> = {
...frontmatter,
date: new Date().toISOString(),
};
if (title) fm.title = title;
if (tags) fm.tags = tags;
const fullPath = await writeNote(vaultPath, notePath, content, fm);
return {
content: [
{
type: "text",
text: `Note written: ${notePath}\nFull path: ${fullPath}`,
},
],
};
}
case "append_to_note": {
const { path: notePath, content, add_timestamp = true } = args as {
path: string;
content: string;
add_timestamp?: boolean;
};
let appendContent = content;
if (add_timestamp) {
const ts = new Date().toISOString().slice(0, 19).replace("T", " ");
appendContent = `\n---\n*${ts}*\n\n${content}`;
}
await appendToNote(vaultPath, notePath, appendContent);
return {
content: [
{
type: "text",
text: `Content appended to: ${notePath}`,
},
],
};
}
case "append_to_daily_note": {
const { content, section, daily_folder = "Daily Notes" } = args as {
content: string;
section?: string;
daily_folder?: string;
};
const dailyPath = await getDailyNotePath(vaultPath, daily_folder);
const ts = new Date().toISOString().slice(11, 19);
let appendContent = "";
if (section) {
appendContent = `\n${section}\n\n*${ts}*\n${content}`;
} else {
appendContent = `\n*${ts}*\n${content}`;
}
await appendToNote(vaultPath, dailyPath, appendContent);
return {
content: [
{
type: "text",
text: `Appended to daily note: ${dailyPath}`,
},
],
};
}
case "search_notes": {
const { query, tags, folder, limit = 20 } = args as {
query: string;
tags?: string[];
folder?: string;
limit?: number;
};
const results = await searchNotes(vaultPath, query, { tags, folder, limit });
if (results.length === 0) {
return {
content: [
{
type: "text",
text: `No notes found matching "${query}"`,
},
],
};
}
let output = `Found ${results.length} notes:\n\n`;
for (const result of results) {
output += `## [[${result.title}]]\n`;
output += `Path: ${result.relativePath}\n`;
if (result.matches.length > 0) {
output += `Matches:\n`;
for (const match of result.matches) {
output += ` ${match}\n`;
}
}
output += "\n";
}
return {
content: [{ type: "text", text: output }],
};
}
case "read_note": {
const { path: notePath } = args as { path: string };
const { content, frontmatter } = await readNote(vaultPath, notePath);
let output = "";
if (Object.keys(frontmatter).length > 0) {
output += `Frontmatter: ${JSON.stringify(frontmatter, null, 2)}\n\n---\n\n`;
}
output += content;
return {
content: [{ type: "text", text: output }],
};
}
case "list_recent_notes": {
const { limit = 20 } = args as { limit?: number };
const notes = await listRecentNotes(vaultPath, limit);
let output = `Recent ${notes.length} notes:\n\n`;
for (const note of notes) {
output += `- **${note.title}** (${note.relativePath})\n`;
output += ` Modified: ${note.modified.toISOString()}\n`;
}
return {
content: [{ type: "text", text: output }],
};
}
case "find_backlinks": {
const { note_name } = args as { note_name: string };
const backlinks = await findBacklinks(vaultPath, note_name);
if (backlinks.length === 0) {
return {
content: [
{
type: "text",
text: `No backlinks found for "${note_name}"`,
},
],
};
}
let output = `Found ${backlinks.length} backlinks to "${note_name}":\n\n`;
for (const link of backlinks) {
output += `- [[${link}]]\n`;
}
return {
content: [{ type: "text", text: output }],
};
}
case "create_todo": {
const {
task,
project,
priority,
due_date,
context,
todo_file = "TODOs.md",
} = args as {
task: string;
project?: string;
priority?: string;
due_date?: string;
context?: string;
todo_file?: string;
};
let todoLine = `- [ ] ${task}`;
if (priority) todoLine += ` #${priority}`;
if (project) todoLine += ` #${project}`;
if (due_date) todoLine += ` 📅 ${due_date}`;
if (context) todoLine += `\n - ${context}`;
await appendToNote(vaultPath, todo_file, todoLine + "\n");
return {
content: [
{
type: "text",
text: `TODO added to ${todo_file}:\n${todoLine}`,
},
],
};
}
case "link_notes": {
const { keywords, limit = 10 } = args as {
keywords: string[];
content?: string;
limit?: number;
};
const allResults: SearchResult[] = [];
for (const keyword of keywords) {
const results = await searchNotes(vaultPath, keyword, { limit: 5 });
allResults.push(...results);
}
// Deduplicate by path
const seen = new Set<string>();
const uniqueResults = allResults.filter((r) => {
if (seen.has(r.relativePath)) return false;
seen.add(r.relativePath);
return true;
});
if (uniqueResults.length === 0) {
return {
content: [
{
type: "text",
text: `No related notes found for keywords: ${keywords.join(", ")}`,
},
],
};
}
let output = `Related notes for keywords [${keywords.join(", ")}]:\n\n`;
for (const result of uniqueResults.slice(0, limit)) {
output += `- [[${result.title}]] - ${result.relativePath}\n`;
}
return {
content: [{ type: "text", text: output }],
};
}
default:
return {
content: [
{
type: "text",
text: `Unknown tool: ${name}`,
},
],
};
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error: ${message}`,
},
],
};
}
}