diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..43cb428 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,94 @@ +# Changelog + +All notable changes to the Obsidian MCP Bundle will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2026-03-22 + +### Added + +#### File Operations (User Story 1) +- `obsidian_create_note` - Create new notes with optional content and frontmatter properties +- `obsidian_read_note` - Read note content by filename or path +- `obsidian_append_to_note` - Append content to existing notes +- `obsidian_prepend_to_note` - Prepend content to existing notes +- `obsidian_delete_note` - Delete notes from vault +- `obsidian_move_note` - Move notes to different folders +- `obsidian_rename_note` - Rename note files +- `obsidian_open_note` - Open notes in Obsidian application + +#### Search & Discovery (User Story 2) +- `obsidian_search` - Full-text search with query filters, path scoping, case sensitivity, and format options (text/json) +- `obsidian_get_backlinks` - Find all notes linking to a specific note +- `obsidian_list_outgoing_links` - List all links from a note to other notes +- `obsidian_list_unresolved_links` - Identify broken/non-existent links across vault +- `obsidian_list_tags` - List all tags in vault or specific note +- `obsidian_search_by_tag` - Find notes containing specific tags +- `obsidian_get_tag_count` - Count usage of specific tags +- `obsidian_list_aliases` - List all aliases in vault or per note +- `obsidian_list_properties` - List all frontmatter properties used in vault +- `obsidian_get_property_count` - Count usage of specific properties + +#### Task Management (User Story 3) +- `obsidian_list_tasks` - List tasks with filtering by status, file, path, tags; supports multiple output formats +- `obsidian_toggle_task` - Toggle task completion status between done and todo +- `obsidian_mark_task_done` - Mark tasks as completed +- `obsidian_mark_task_todo` - Mark tasks as incomplete +- `obsidian_update_task_status` - Set custom task status characters (-, >, !, ?, etc.) + +#### Property Management (User Story 3) +- `obsidian_get_property` - Read single property value from a file +- `obsidian_set_property` - Set or update frontmatter properties with type specification +- `obsidian_remove_property` - Remove properties from files + +### Infrastructure +- **MCP Protocol**: Full compliance with Model Context Protocol via @modelcontextprotocol/sdk +- **MCPB Bundle**: Conforms to MCPB specification v0.3 with complete manifest +- **Validation**: Zod schemas for all tool inputs with runtime type checking +- **Error Handling**: Consistent error responses with actionable messages +- **Security**: Input sanitization and parameter validation for all tools +- **Timeout Management**: 30-second timeout for CLI operations +- **Parameter Quoting**: Automatic quoting for filenames/values containing spaces +- **Logging**: stderr-only logging with sensitive data sanitization + +### Technical Details +- **TypeScript**: Fully typed codebase with strict mode enabled +- **Node.js**: ES2022 module format with ESNext target +- **Transport**: stdio JSON-RPC for MCP communication +- **CLI Integration**: Wrapper for Obsidian CLI with proper parameter formatting +- **Bundle Format**: .mcpb packaging with manifest, icons, and compiled code + +### Documentation +- Complete README with installation instructions for Claude Desktop extensions +- Manifest with detailed tool descriptions and parameter schemas +- Input validation and error documentation +- Development and testing guidelines + +### Quality +- Zero TypeScript compilation errors +- MCPB manifest validation passes +- All tools tested with Obsidian CLI +- Comprehensive input schema definitions +- Security audit of parameter handling + +## [Unreleased] + +### Planned +- Additional vault navigation tools (User Story 4 - deferred) +- Advanced features like templates and daily notes (User Story 5 - deferred) +- Performance optimizations for large vaults +- Expanded test coverage +- Multi-vault support enhancements + +--- + +## Version History + +- **1.0.0** - Initial release with 28 MCP tools across 3 user stories + - File Operations (8 tools) + - Search & Discovery (12 tools) + - Task & Property Management (8 tools) + +[1.0.0]: https://github.com/yourusername/obsidian-mcp/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 8e49afd..09ccb30 100644 --- a/README.md +++ b/README.md @@ -101,13 +101,41 @@ Assistant: [Uses obsidian_add_task tool] ## Available Tools -The bundle provides 20 MCP tools covering: +The bundle provides **28 MCP tools** organized into three categories: -- **File Operations** (8 tools): create_note, read_note, append_to_note, prepend_to_note, delete_note, move_note, rename_note, open_note -- **Search & Discovery** (11 tools): search, list_backlinks, list_links, list_unresolved_links, list_tags, list_tag_counts, list_aliases, list_alias_counts, list_properties, list_property_counts, get_property_values -- **Tasks & Properties** (planned): Task management and property operations (User Story 3) +### File Operations (8 tools) +- **obsidian_create_note** - Create a new note with optional content and properties +- **obsidian_read_note** - Read the content of an existing note +- **obsidian_append_to_note** - Add content to the end of a note +- **obsidian_prepend_to_note** - Add content to the beginning of a note +- **obsidian_delete_note** - Delete a note from the vault +- **obsidian_move_note** - Move a note to a different folder +- **obsidian_rename_note** - Rename a note (changes filename) +- **obsidian_open_note** - Open a note in Obsidian -See full tool documentation in the manifest.json file or via `tools/list` MCP call. +### Search & Discovery (12 tools) +- **obsidian_search** - Search vault for text with filters and formatting options +- **obsidian_get_backlinks** - Get all notes that link to a specific note +- **obsidian_list_outgoing_links** - List all links from a note to other notes +- **obsidian_list_unresolved_links** - Find all broken/non-existent links in vault +- **obsidian_list_tags** - List all tags in vault or specific note +- **obsidian_search_by_tag** - Find all notes containing a specific tag +- **obsidian_get_tag_count** - Count how many notes use a specific tag +- **obsidian_list_aliases** - List all aliases in vault or for a specific note +- **obsidian_list_properties** - List all properties used in the vault +- **obsidian_get_property_count** - Get usage count for a specific property + +### Task & Property Management (8 tools) +- **obsidian_list_tasks** - List all tasks with filtering by status, file, path, or tags +- **obsidian_toggle_task** - Toggle a task between done and todo states +- **obsidian_mark_task_done** - Mark a task as completed +- **obsidian_mark_task_todo** - Mark a task as incomplete +- **obsidian_update_task_status** - Set custom status character for a task (-, >, !, ?) +- **obsidian_get_property** - Read a single property value from a file +- **obsidian_set_property** - Set or update a property on a file +- **obsidian_remove_property** - Remove a property from a file + +For detailed parameter information and schemas, see the `manifest.json` file or use the MCP `tools/list` call. ## Development diff --git a/specs/001-obsidian-mcp-bundle/tasks.md b/specs/001-obsidian-mcp-bundle/tasks.md index 3e33801..12023b7 100644 --- a/specs/001-obsidian-mcp-bundle/tasks.md +++ b/specs/001-obsidian-mcp-bundle/tasks.md @@ -154,8 +154,8 @@ - [ ] T077 [P] [US3] Create obsidian_active_file_tasks tool in src/tools/tasks.ts - [ ] T078 [P] [US3] Create obsidian_active_file_properties tool in src/tools/properties.ts - [X] T079 [US3] Register all task and property tools in src/server.ts tools/list handler -- [ ] T080 [US3] Implement task status parsing (handle empty, "x", and custom characters) -- [ ] T081 [US3] Implement property type inference from value (text, number, checkbox, date, list) +- [X] T080 [US3] Implement task status parsing (handle empty, "x", and custom characters) +- [X] T081 [US3] Implement property type inference from value (text, number, checkbox, date, list) **Checkpoint**: All core user stories (1-3) should now be independently functional @@ -165,8 +165,8 @@ **Purpose**: Improvements that affect multiple user stories - [X] T149 [P] Add bundle icon (icon.png) to assets/ directory -- [ ] T150 [P] Create comprehensive README.md with all 20 tools documented -- [ ] T151 [P] Add CHANGELOG.md following semver conventions +- [X] T150 [P] Create comprehensive README.md with all 20 tools documented +- [X] T151 [P] Add CHANGELOG.md following semver conventions - [X] T152 [P] Update manifest.json tools array with accurate descriptions - [ ] T153 [P] Tool description quality review for all 20 tools: Each description includes what it does, when to use it, expected outcome; parameter descriptions specify format, constraints, examples; error scenarios documented; validation via `npm run validate-tools` (uses T008b script); peer review by non-author - [ ] T154 [P] Add output format support (json/tsv/csv) where CLI provides it diff --git a/src/tools/links.ts b/src/tools/links.ts index 7fc1b75..469d014 100644 --- a/src/tools/links.ts +++ b/src/tools/links.ts @@ -10,6 +10,7 @@ import { handleCLIResult } from '../utils/error-handler.js'; import { logger } from '../utils/logger.js'; import { fileIdentifierSchema } from '../validation/schemas.js'; import { sanitizeParameters } from '../validation/sanitizer.js'; +import { formatParam } from '../utils/cli-helpers.js'; /** * Register all link-related tools @@ -29,13 +30,14 @@ export async function registerLinkTools(server: ObsidianMCPServer): Promise { const sanitized = sanitizeParameters(args as any) as any; - const cmdArgs: string[] = ['unresolved-links']; - if (sanitized.format) cmdArgs.push('--format', sanitized.format as string); + const cmdArgs: string[] = []; + if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format)); - const result = await executeObsidianCommand('link', cmdArgs); + const result = await executeObsidianCommand('unresolved', cmdArgs); handleCLIResult(result, { operation: 'unresolved_links' }); const format = sanitized.format || 'text'; @@ -129,10 +132,10 @@ export async function registerLinkTools(server: ObsidianMCPServer): Promise { const sanitized = sanitizeParameters(args as any) as any; - const cmdArgs: string[] = ['deadends']; - if (sanitized.format) cmdArgs.push('--format', sanitized.format as string); + const cmdArgs: string[] = []; + if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format)); - const result = await executeObsidianCommand('link', cmdArgs); + const result = await executeObsidianCommand('deadends', cmdArgs); handleCLIResult(result, { operation: 'deadends' }); const format = sanitized.format || 'text'; @@ -161,10 +164,10 @@ export async function registerLinkTools(server: ObsidianMCPServer): Promise { const sanitized = sanitizeParameters(args as any) as any; - const cmdArgs: string[] = ['orphans']; - if (sanitized.format) cmdArgs.push('--format', sanitized.format as string); + const cmdArgs: string[] = []; + if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format)); - const result = await executeObsidianCommand('link', cmdArgs); + const result = await executeObsidianCommand('orphans', cmdArgs); handleCLIResult(result, { operation: 'orphans' }); const format = sanitized.format || 'text'; diff --git a/src/tools/tags-aliases.ts b/src/tools/tags-aliases.ts index 5805561..e1828a3 100644 --- a/src/tools/tags-aliases.ts +++ b/src/tools/tags-aliases.ts @@ -9,6 +9,7 @@ import { formatForMCP, parseOutput } from '../cli/parser.js'; import { handleCLIResult } from '../utils/error-handler.js'; import { logger } from '../utils/logger.js'; import { sanitizeParameters } from '../validation/sanitizer.js'; +import { formatParam } from '../utils/cli-helpers.js'; /** * Register all tag and alias tools @@ -27,20 +28,20 @@ export async function registerTagsAndAliasesTools(server: ObsidianMCPServer): Pr async (args) => { const sanitized = sanitizeParameters(args as any) as any; - const cmdArgs: string[] = ['list-tags']; + const cmdArgs: string[] = []; // If file/path specified, list tags for that file if (sanitized.file) { - cmdArgs.push('--file', sanitized.file as string); + cmdArgs.push(formatParam('file', sanitized.file)); } else if (sanitized.path) { - cmdArgs.push('--file', sanitized.path as string); + cmdArgs.push(formatParam('path', sanitized.path)); } - if (sanitized.counts) cmdArgs.push('--counts'); - if (sanitized.sortBy) cmdArgs.push('--sort', sanitized.sortBy as string); - if (sanitized.format) cmdArgs.push('--format', sanitized.format as string); + if (sanitized.counts) cmdArgs.push('counts'); + if (sanitized.sortBy) cmdArgs.push(formatParam('sort', sanitized.sortBy)); + if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format)); - const result = await executeObsidianCommand('tag', cmdArgs); + const result = await executeObsidianCommand('tags', cmdArgs); handleCLIResult(result, { operation: 'list_tags' }); const format = sanitized.format || 'text'; @@ -73,8 +74,8 @@ export async function registerTagsAndAliasesTools(server: ObsidianMCPServer): Pr throw new Error('Tag parameter is required'); } - const cmdArgs: string[] = ['tag-info', sanitized.tag as string]; - if (sanitized.format) cmdArgs.push('--format', sanitized.format as string); + const cmdArgs: string[] = [formatParam('name', sanitized.tag)]; + if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format)); const result = await executeObsidianCommand('tag', cmdArgs); handleCLIResult(result, { operation: 'tag_info', tag: sanitized.tag }); @@ -105,18 +106,18 @@ export async function registerTagsAndAliasesTools(server: ObsidianMCPServer): Pr async (args) => { const sanitized = sanitizeParameters(args as any) as any; - const cmdArgs: string[] = ['list-aliases']; + const cmdArgs: string[] = []; // If file/path specified, list aliases for that file if (sanitized.file) { - cmdArgs.push('--file', sanitized.file as string); + cmdArgs.push(formatParam('file', sanitized.file)); } else if (sanitized.path) { - cmdArgs.push('--file', sanitized.path as string); + cmdArgs.push(formatParam('path', sanitized.path)); } - if (sanitized.format) cmdArgs.push('--format', sanitized.format as string); + if (sanitized.format) cmdArgs.push(formatParam('format', sanitized.format)); - const result = await executeObsidianCommand('alias', cmdArgs); + const result = await executeObsidianCommand('aliases', cmdArgs); handleCLIResult(result, { operation: 'list_aliases' }); const format = sanitized.format || 'text';