From 42ed93500c25fbb35eb6c4e90d82e9ce0ad9b0e0 Mon Sep 17 00:00:00 2001 From: Bill Date: Sun, 19 Oct 2025 21:47:30 -0400 Subject: [PATCH] docs: cleanup of documentation --- CLAUDE.md | 248 ++++++++++++++++ IMPLEMENTATION_NOTES_PHASE10.md | 337 ---------------------- IMPLEMENTATION_NOTES_PHASE5.md | 286 ------------------- README.md | 35 ++- manifest.json | 1 + old-structure/main.ts | 275 ------------------ old-structure/mcp-server.ts | 485 -------------------------------- old-structure/mcp-types.ts | 122 -------- versions.json | 3 +- 9 files changed, 278 insertions(+), 1514 deletions(-) create mode 100644 CLAUDE.md delete mode 100644 IMPLEMENTATION_NOTES_PHASE10.md delete mode 100644 IMPLEMENTATION_NOTES_PHASE5.md delete mode 100644 old-structure/main.ts delete mode 100644 old-structure/mcp-server.ts delete mode 100644 old-structure/mcp-types.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..57e5dfc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,248 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is an Obsidian plugin that exposes vault operations via the Model Context Protocol (MCP) over HTTP. It runs an Express server within Obsidian to enable AI assistants and other MCP clients to interact with the vault programmatically. + +## Development Commands + +### Building and Development +```bash +npm install # Install dependencies +npm run dev # Watch mode for development (auto-rebuild on changes) +npm run build # Production build (runs type check + esbuild) +``` + +### Testing +```bash +npm test # Run all tests +npm run test:watch # Run tests in watch mode +npm run test:coverage # Run tests with coverage report +``` + +### Type Checking +The build command includes TypeScript type checking via `tsc -noEmit -skipLibCheck`. + +### Installing in Obsidian +After building, the plugin outputs `main.js` to the root directory. To test in Obsidian: +1. Copy `main.js`, `manifest.json`, and `styles.css` to your vault's `.obsidian/plugins/obsidian-mcp-server/` directory +2. Reload Obsidian (Ctrl/Cmd + R in dev mode) +3. Enable the plugin in Settings → Community Plugins + +## Architecture + +### High-Level Structure + +The codebase follows a layered architecture: + +``` +src/ +├── main.ts # Plugin entry point (MCPServerPlugin) +├── server/ # HTTP server layer +│ ├── mcp-server.ts # Express server + MCP protocol handler +│ ├── routes.ts # Route setup +│ └── middleware.ts # Auth, CORS, origin validation +├── tools/ # MCP tool implementations +│ ├── index.ts # ToolRegistry - routes tool calls +│ ├── note-tools.ts # File operations (CRUD) +│ └── vault-tools.ts # Vault operations (search, list, metadata) +├── utils/ # Shared utilities +│ ├── path-utils.ts # Path validation and normalization +│ ├── frontmatter-utils.ts # YAML frontmatter parsing +│ ├── search-utils.ts # Search and regex utilities +│ ├── link-utils.ts # Wikilink resolution +│ ├── waypoint-utils.ts # Waypoint plugin integration +│ ├── glob-utils.ts # Glob pattern matching +│ ├── version-utils.ts # ETag/versionId for concurrency control +│ └── error-messages.ts # Consistent error messaging +├── ui/ # User interface components +│ ├── notifications.ts # NotificationManager for tool call notifications +│ └── notification-history.ts # History modal +├── types/ # TypeScript type definitions +│ ├── mcp-types.ts # MCP protocol types +│ └── settings-types.ts # Plugin settings +└── settings.ts # Settings UI tab +``` + +### Key Components + +#### 1. MCPServerPlugin (src/main.ts) +- Main plugin class that extends Obsidian's `Plugin` +- Lifecycle management: starts/stops HTTP server +- Registers commands and ribbon icons +- Manages plugin settings and notification system + +#### 2. MCPServer (src/server/mcp-server.ts) +- Wraps Express HTTP server +- Handles JSON-RPC 2.0 requests per MCP protocol +- Routes to ToolRegistry for tool execution +- Supports methods: `initialize`, `tools/list`, `tools/call`, `ping` +- Binds to `127.0.0.1` only for security + +#### 3. ToolRegistry (src/tools/index.ts) +- Central registry of all available MCP tools +- Dispatches tool calls to NoteTools or VaultTools +- Manages NotificationManager integration +- Returns tool definitions with JSON schemas + +#### 4. NoteTools (src/tools/note-tools.ts) +- File-level CRUD operations +- Tools: `read_note`, `create_note`, `update_note`, `delete_note`, `update_frontmatter`, `update_sections`, `rename_file`, `read_excalidraw` +- Implements concurrency control via versionId/ETag system +- Handles conflict strategies for creates + +#### 5. VaultTools (src/tools/vault-tools.ts) +- Vault-wide operations +- Tools: `search`, `list`, `stat`, `exists`, `get_vault_info`, `search_waypoints`, `get_folder_waypoint`, `is_folder_note`, `validate_wikilinks`, `resolve_wikilink`, `backlinks` +- Advanced search with regex and glob filtering +- Wikilink resolution using Obsidian's MetadataCache + +### Important Patterns + +#### Path Handling +- All paths are vault-relative (no leading slash) +- PathUtils validates paths against leading/trailing slashes, absolute paths, and `..` traversal +- Path normalization handles cross-platform differences + +#### Concurrency Control +- VersionUtils generates ETags based on file mtime + size +- `ifMatch` parameter on write operations enables optimistic locking +- Prevents lost updates when multiple clients modify the same file + +#### Error Handling +- ErrorMessages utility provides consistent error formatting +- All tool results return `CallToolResult` with structured content +- `isError: true` flag indicates failures + +#### Frontmatter +- FrontmatterUtils parses YAML frontmatter using regex +- `update_frontmatter` enables surgical metadata updates without full file rewrites +- Reduces race conditions vs full content updates + +#### Wikilinks +- LinkUtils handles wikilink resolution via Obsidian's MetadataCache +- Supports heading links (`[[note#heading]]`) and aliases (`[[note|alias]]`) +- `validate_wikilinks` checks all links in a note +- `backlinks` uses MetadataCache for reverse link lookup + +#### Search +- SearchUtils implements multi-file search with regex support +- GlobUtils provides file filtering via glob patterns +- Returns structured results with line/column positions and snippets + +## Testing + +Tests are located in `tests/` and use Jest with ts-jest. The test setup includes: +- Mock Obsidian API in `tests/__mocks__/obsidian.ts` +- Test files follow `*.test.ts` naming convention +- Coverage excludes type definition files + +## MCP Protocol Implementation + +The server implements MCP version `2024-11-05`: +- JSON-RPC 2.0 over HTTP POST to `/mcp` endpoint +- Capabilities: `{ tools: {} }` +- All tool schemas defined in ToolRegistry.getToolDefinitions() +- Tool call results use MCP's content array format with text/image types + +## Security Model + +- Server binds to `127.0.0.1` only (no external access) +- Origin validation prevents DNS rebinding attacks +- Optional Bearer token authentication via `enableAuth` + `apiKey` settings +- CORS configurable via settings for local MCP clients + +## Settings + +MCPPluginSettings (src/types/settings-types.ts): +- `port`: HTTP server port (default: 3000) +- `autoStart`: Start server on plugin load +- `enableCORS`: Enable CORS middleware +- `allowedOrigins`: Comma-separated origin whitelist +- `enableAuth`: Require Bearer token +- `apiKey`: Authentication token +- `notificationsEnabled`: Show tool call notifications in Obsidian UI +- `notificationDuration`: Auto-dismiss time for notifications + +## Waypoint Plugin Integration + +The plugin has special support for the Waypoint community plugin: +- Waypoints are comment blocks: `%% Begin Waypoint %% ... %% End Waypoint %%` +- Used to auto-generate folder indexes +- `search_waypoints`: Find all waypoints in vault +- `get_folder_waypoint`: Extract waypoint from specific folder note +- `is_folder_note`: Detect folder notes by basename match or waypoint presence + +## Development Guidelines + +### Code Organization Best Practices + +- **Keep `main.ts` minimal** - Focus only on plugin lifecycle (onload, onunload, command registration) +- **Delegate feature logic to separate modules** - All functionality lives in dedicated modules under `src/` +- **Split large files** - If any file exceeds ~200-300 lines, break it into smaller, focused modules +- **Use clear module boundaries** - Each file should have a single, well-defined responsibility +- **Use TypeScript strict mode** - The project uses `"strict": true` +- **Prefer async/await** over promise chains +- **Handle errors gracefully** - Provide helpful error messages to users + +### Performance Considerations + +- **Keep startup light** - Defer heavy work until needed; avoid long-running tasks during `onload` +- **Batch disk access** - Avoid excessive vault scans +- **Debounce/throttle expensive operations** - Especially for file system event handlers +- **Be mindful of memory** on mobile platforms (though this plugin is desktop-only) + +### Platform Compatibility + +This plugin is **desktop-only** (`isDesktopOnly: true`) because it uses Node.js HTTP server (Express). If extending to mobile: +- Avoid Node/Electron APIs +- Don't assume desktop-only behavior +- Test on iOS and Android + +### Security and Privacy + +- **Default to local/offline operation** - This plugin already binds to localhost only +- **No hidden telemetry** - Don't collect analytics without explicit opt-in +- **Never execute remote code** - Don't fetch and eval scripts +- **Minimize scope** - Read/write only what's necessary inside the vault +- **Do not access files outside the vault** +- **Respect user privacy** - Don't collect vault contents without consent +- **Clean up resources** - Use `this.register*` helpers so the plugin unloads safely + +### UI/UX Guidelines + +- **Use sentence case** for headings, buttons, and titles +- **Use bold** to indicate literal UI labels in documentation +- **Use arrow notation** for navigation: "Settings → Community plugins" +- **Prefer "select"** for user interactions +- Keep in-app strings short, consistent, and free of jargon + +### Versioning and Releases + +- Use **Semantic Versioning** (SemVer) for `version` in `manifest.json` +- Update `versions.json` to map plugin version → minimum Obsidian app version +- **Never change the plugin `id`** after release +- **Never rename command IDs** after release - they are stable API +- Create GitHub releases with tags that **exactly match** `manifest.json` version (no `v` prefix) +- Attach required assets to releases: `manifest.json`, `main.js`, `styles.css` + +### Build Artifacts + +- **Never commit build artifacts** to version control (`main.js`, `node_modules/`, etc.) +- All TypeScript must bundle into a single `main.js` file via esbuild +- Release artifacts must be at the top level of the plugin folder + +### Command Stability + +- **Add commands with stable IDs** - don't rename once released +- Commands are registered in `src/main.ts` with IDs like `start-mcp-server`, `stop-mcp-server`, etc. + +## References + +- **Obsidian API docs**: https://docs.obsidian.md +- **Developer policies**: https://docs.obsidian.md/Developer+policies +- **Plugin guidelines**: https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines +- **Sample plugin**: https://github.com/obsidianmd/obsidian-sample-plugin +- **Manifest validation**: https://github.com/obsidianmd/obsidian-releases/blob/master/.github/workflows/validate-plugin-entry.yml diff --git a/IMPLEMENTATION_NOTES_PHASE10.md b/IMPLEMENTATION_NOTES_PHASE10.md deleted file mode 100644 index 06b55b9..0000000 --- a/IMPLEMENTATION_NOTES_PHASE10.md +++ /dev/null @@ -1,337 +0,0 @@ -# Phase 10: UI Notifications - Implementation Notes - -**Date:** October 17, 2025 -**Status:** ✅ Complete -**Version:** 9.0.0 - -## Overview - -Phase 10 adds visual feedback for MCP tool calls with configurable notifications in the Obsidian UI. This provides transparency into API activity, easier debugging, and optional notification history tracking. - -## Implementation Summary - -### Files Created - -1. **`src/ui/notifications.ts`** - Notification Manager - - Core notification system with rate limiting - - Tool-specific icons for visual clarity - - Queue-based notification display (max 10/second) - - History tracking (last 100 entries) - - Parameter truncation and privacy controls - - Console logging support - -2. **`src/ui/notification-history.ts`** - History Modal - - Modal for viewing notification history - - Filter by tool name and type (all/success/error) - - Export history to clipboard as JSON - - Displays timestamp, duration, parameters, and errors - - Clean, scrollable UI with syntax highlighting - -### Files Modified - -1. **`src/types/settings-types.ts`** - - Added `NotificationVerbosity` type: `'off' | 'errors' | 'all'` - - Added `NotificationSettings` interface - - Extended `MCPPluginSettings` with notification settings - - Added default notification settings to `DEFAULT_SETTINGS` - -2. **`src/settings.ts`** - - Added "UI Notifications" section to settings UI - - Toggle for enabling/disabling notifications - - Dropdown for verbosity level (off/errors/all) - - Toggle for showing parameters - - Text input for notification duration - - Toggle for console logging - - Button to view notification history - - Settings only visible when notifications enabled - -3. **`src/tools/index.ts`** - - Added `NotificationManager` import - - Added `notificationManager` property to `ToolRegistry` - - Added `setNotificationManager()` method - - Wrapped `callTool()` with notification logic: - - Show notification before tool execution - - Track execution time - - Show success/error notification after completion - - Add entry to history with all details - -4. **`src/server/mcp-server.ts`** - - Added `NotificationManager` import - - Added `setNotificationManager()` method - - Passes notification manager to tool registry - -5. **`src/main.ts`** - - Added `NotificationManager` and `NotificationHistoryModal` imports - - Added `notificationManager` property - - Added `updateNotificationManager()` method - - Added `showNotificationHistory()` method - - Initialize notification manager on plugin load - - Added command: "View MCP Notification History" - - Update notification manager when settings change - -## Features - -### Notification System - -**Three Verbosity Levels:** -- `off` - No notifications (default) -- `errors` - Show only failed tool calls -- `all` - Show all tool calls and results - -**Notification Types:** -- **Tool Call** - `🔧 MCP: list({ path: "projects", recursive: true })` -- **Success** - `✅ MCP: list completed (142ms)` -- **Error** - `❌ MCP: create_note failed - Parent folder does not exist` - -**Tool Icons:** -- 📖 Read operations (`read_note`, `read_excalidraw`) -- ✏️ Write operations (`create_note`, `update_note`, `update_frontmatter`, `update_sections`) -- 🗑️ Delete operations (`delete_note`) -- 📝 Rename operations (`rename_file`) -- 🔍 Search operations (`search`, `search_waypoints`) -- 📋 List operations (`list`) -- 📊 Stat operations (`stat`, `exists`) -- ℹ️ Info operations (`get_vault_info`) -- 🗺️ Waypoint operations (`get_folder_waypoint`) -- 📁 Folder operations (`is_folder_note`) -- 🔗 Link operations (`validate_wikilinks`, `resolve_wikilink`, `backlinks`) - -### Rate Limiting - -- Queue-based notification display -- Maximum 10 notifications per second -- 100ms interval between notifications -- Prevents UI freezing during bulk operations -- Async processing doesn't block tool execution - -### History Tracking - -**Storage:** -- Last 100 tool calls stored in memory -- Automatic pruning when limit exceeded -- Cleared on plugin reload - -**History Entry:** -```typescript -interface NotificationHistoryEntry { - timestamp: number; // When the tool was called - toolName: string; // Name of the tool - args: any; // Tool parameters - success: boolean; // Whether the call succeeded - duration?: number; // Execution time in milliseconds - error?: string; // Error message (if failed) -} -``` - -**History Modal:** -- Filter by tool name (text search) -- Filter by type (all/success/error) -- Shows count of filtered entries -- Displays formatted entries with: - - Status icon (✅/❌) - - Tool name with color coding - - Timestamp and duration - - Parameters (JSON formatted) - - Error message (if failed) -- Export to clipboard as JSON -- Close button - -### Settings - -**Default Configuration:** -```typescript -{ - notificationsEnabled: false, // Disabled by default - notificationVerbosity: 'errors', // Show errors only - showParameters: false, // Hide parameters - notificationDuration: 3000, // 3 seconds - logToConsole: false // No console logging -} -``` - -**Configuration Options:** -- **Enable notifications** - Master toggle -- **Notification verbosity** - Control which notifications to show -- **Show parameters** - Include tool parameters (truncated to 50 chars) -- **Notification duration** - How long notifications stay visible (ms) -- **Log to console** - Also log to browser console for debugging - -## Technical Details - -### Performance - -**When Disabled:** -- Zero overhead -- No notification manager created -- No history tracking -- No performance impact - -**When Enabled:** -- Async notification queue -- Non-blocking display -- Minimal memory footprint (~10KB for 100 entries) -- No impact on tool execution time - -### Privacy - -**Parameter Handling:** -- Truncates long values (max 50 chars for display) -- Optional parameter hiding -- Doesn't show sensitive data (API keys, tokens) -- File content truncated in parameters - -**Console Logging:** -- Optional feature (disabled by default) -- Logs to browser console for debugging -- Always logs errors regardless of setting - -### Integration - -**Tool Call Flow:** -``` -1. Client calls tool via MCP -2. ToolRegistry.callTool() invoked -3. Show "tool call" notification (if enabled) -4. Execute tool -5. Track execution time -6. Show "success" or "error" notification -7. Add entry to history -8. Return result to client -``` - -**Notification Manager Lifecycle:** -``` -1. Plugin loads -2. Load settings -3. Create notification manager (if enabled) -4. Pass to server's tool registry -5. Settings change → update notification manager -6. Plugin unloads → cleanup -``` - -## Usage Examples - -### For Development - -**Verbose Mode:** -```json -{ - "notificationsEnabled": true, - "notificationVerbosity": "all", - "showParameters": true, - "notificationDuration": 3000, - "logToConsole": true -} -``` - -See every tool call with parameters and timing information. - -### For Production - -**Errors Only:** -```json -{ - "notificationsEnabled": true, - "notificationVerbosity": "errors", - "showParameters": false, - "notificationDuration": 5000, - "logToConsole": false -} -``` - -Only see failed operations with longer display time. - -### Disabled - -**No Notifications:** -```json -{ - "notificationsEnabled": false, - "notificationVerbosity": "off", - "showParameters": false, - "notificationDuration": 3000, - "logToConsole": false -} -``` - -Zero overhead, no visual feedback. - -## Testing - -### Manual Testing Checklist - -- [x] Enable notifications in settings -- [x] Test all verbosity levels (off/errors/all) -- [x] Test with parameters shown/hidden -- [x] Test notification duration setting -- [x] Test console logging toggle -- [x] Test notification history modal -- [x] Test history filtering by tool name -- [x] Test history filtering by type -- [x] Test history export to clipboard -- [x] Test rate limiting with rapid tool calls -- [x] Test with long parameter values -- [x] Test error notifications -- [x] Verify no performance impact when disabled -- [x] Test settings persistence across reloads - -### Integration Testing - -**Recommended Tests:** -1. Call multiple tools in rapid succession -2. Verify rate limiting prevents UI spam -3. Check history tracking accuracy -4. Test with various parameter types -5. Verify error handling and display -6. Test settings changes while server running -7. Test command palette integration - -## Known Limitations - -1. **Obsidian Notice API** - Cannot programmatically dismiss notices -2. **History Persistence** - History cleared on plugin reload (by design) -3. **Notification Queue** - Maximum 10/second (configurable in code) -4. **History Size** - Limited to 100 entries (configurable in code) -5. **Parameter Display** - Truncated to 50 chars (configurable in code) - -## Future Enhancements - -**Potential Improvements:** -- Persistent history (save to disk) -- Configurable history size -- Notification sound effects -- Desktop notifications (OS-level) -- Batch notification summaries -- Custom notification templates -- Per-tool notification settings -- Notification grouping/collapsing - -## Changelog Entry - -Added to `CHANGELOG.md` as version `9.0.0` with complete feature documentation. - -## Roadmap Updates - -- Updated priority matrix to show Phase 10 as complete -- Marked all Phase 10 tasks as complete -- Updated completion statistics -- Added implementation summary to Phase 10 section - -## Conclusion - -Phase 10 successfully implements a comprehensive notification system for MCP tool calls. The implementation is: - -✅ **Complete** - All planned features implemented -✅ **Tested** - Manual testing completed -✅ **Documented** - Full documentation in CHANGELOG and ROADMAP -✅ **Performant** - Zero impact when disabled, minimal when enabled -✅ **Flexible** - Multiple configuration options for different use cases -✅ **Privacy-Aware** - Parameter truncation and optional hiding -✅ **User-Friendly** - Clean UI, intuitive settings, helpful history modal - -The notification system provides valuable transparency into MCP API activity while remaining completely optional and configurable. It's ready for production use. - ---- - -**Implementation completed:** October 17, 2025 -**All 10 phases of the roadmap are now complete! 🎉** diff --git a/IMPLEMENTATION_NOTES_PHASE5.md b/IMPLEMENTATION_NOTES_PHASE5.md deleted file mode 100644 index 4689ad8..0000000 --- a/IMPLEMENTATION_NOTES_PHASE5.md +++ /dev/null @@ -1,286 +0,0 @@ -# Phase 5 Implementation Notes: Advanced Read Operations - -**Date:** October 16, 2025 -**Status:** ✅ Complete (Including Manual Testing) -**Estimated Effort:** 2-3 days -**Actual Effort:** ~2.5 hours (implementation + testing refinements) - -## Overview - -Phase 5 adds advanced read capabilities to the Obsidian MCP Server, including frontmatter parsing and specialized Excalidraw file support. This phase enhances the `read_note` tool and introduces a new `read_excalidraw` tool. - -## Goals Achieved - -✅ Enhanced `read_note` tool with frontmatter parsing options -✅ Created frontmatter utilities for YAML parsing -✅ Added specialized Excalidraw file support -✅ Maintained backward compatibility -✅ Added comprehensive type definitions - -## Implementation Details - -### 1. Frontmatter Utilities (`src/utils/frontmatter-utils.ts`) - -Created a new utility class for handling frontmatter operations: - -**Key Methods:** -- `extractFrontmatter(content: string)` - Extracts and parses YAML frontmatter - - Detects frontmatter delimiters (`---` or `...`) - - Separates frontmatter from content - - Parses YAML using Obsidian's built-in `parseYaml` - - Handles malformed YAML gracefully - -- `extractFrontmatterSummary(parsedFrontmatter)` - Extracts common fields - - Normalizes `title`, `tags`, `aliases` fields - - Includes custom fields - - Returns null if no frontmatter - -- `hasFrontmatter(content: string)` - Quick check for frontmatter presence - -- `parseExcalidrawMetadata(content: string)` - Parses Excalidraw files - - Detects Excalidraw plugin markers - - Extracts JSON from code blocks - - Counts drawing elements - - Identifies compressed data - -**Edge Cases Handled:** -- Files without frontmatter -- Malformed YAML (returns null for parsed data) -- Missing closing delimiter -- Empty frontmatter blocks -- Non-Excalidraw files - -### 2. Type Definitions (`src/types/mcp-types.ts`) - -Added new types for Phase 5: - -```typescript -export interface ParsedNote { - path: string; - hasFrontmatter: boolean; - frontmatter?: string; - parsedFrontmatter?: Record; - content: string; - contentWithoutFrontmatter?: string; -} - -export interface ExcalidrawMetadata { - path: string; - isExcalidraw: boolean; - elementCount?: number; - hasCompressedData?: boolean; - metadata?: Record; - preview?: string; - compressedData?: string; -} -``` - -### 3. Enhanced `read_note` Tool - -**New Parameters:** -- `withFrontmatter` (boolean, default: true) - Include frontmatter in response -- `withContent` (boolean, default: true) - Include full content -- `parseFrontmatter` (boolean, default: false) - Parse and structure frontmatter - -**Behavior:** -- **Default (parseFrontmatter: false):** Returns raw file content as plain text (backward compatible) -- **With parseFrontmatter: true:** Returns structured `ParsedNote` JSON object - -**Example Usage:** - -```typescript -// Simple read (backward compatible) -read_note({ path: "note.md" }) -// Returns: raw content as text - -// Parse frontmatter -read_note({ - path: "note.md", - parseFrontmatter: true -}) -// Returns: ParsedNote JSON with separated frontmatter - -// Get only frontmatter -read_note({ - path: "note.md", - parseFrontmatter: true, - withContent: false -}) -// Returns: ParsedNote with only frontmatter, no content -``` - -### 4. New `read_excalidraw` Tool - -Specialized tool for Excalidraw drawing files. - -**Parameters:** -- `path` (string, required) - Path to Excalidraw file -- `includeCompressed` (boolean, default: false) - Include full drawing data -- `includePreview` (boolean, default: true) - Include text elements preview - -**Features:** -- Validates file is an Excalidraw drawing -- Extracts metadata (element count, version, appState) -- Provides text preview without full data -- Optional full compressed data inclusion - -**Example Usage:** - -```typescript -// Get metadata and preview -read_excalidraw({ path: "drawing.excalidraw.md" }) -// Returns: ExcalidrawMetadata with preview - -// Get full drawing data -read_excalidraw({ - path: "drawing.excalidraw.md", - includeCompressed: true -}) -// Returns: ExcalidrawMetadata with full compressed data -``` - -### 5. Tool Registry Updates (`src/tools/index.ts`) - -**Updated `read_note` schema:** -- Added three new optional parameters -- Updated description to mention frontmatter parsing -- Maintained backward compatibility - -**Added `read_excalidraw` tool:** -- New tool definition with comprehensive schema -- Added case in `callTool` switch statement -- Passes options to `readExcalidraw` method - -## Files Modified - -1. **Created:** - - `src/utils/frontmatter-utils.ts` - Frontmatter parsing utilities - -2. **Modified:** - - `src/types/mcp-types.ts` - Added ParsedNote and ExcalidrawMetadata types - - `src/tools/note-tools.ts` - Enhanced readNote, added readExcalidraw - - `src/tools/index.ts` - Updated tool definitions and callTool - - `ROADMAP.md` - Marked Phase 5 as complete - - `CHANGELOG.md` - Added Phase 5 changes - -## Backward Compatibility - -✅ **Fully backward compatible** -- Default `read_note` behavior unchanged (returns raw content) -- Existing clients continue to work without modifications -- New features are opt-in via parameters - -## Testing Results - -✅ **All manual tests completed successfully** with the following refinements implemented based on feedback: - -### Improvements Made Post-Testing - -1. **Enhanced Error Handling for Excalidraw Files** - - Non-Excalidraw files now return structured response with `isExcalidraw: false` - - Added helpful message: "File is not an Excalidraw drawing. Use read_note instead for regular markdown files." - - Changed from error response to graceful structured response - -2. **Comprehensive Documentation** - - Enhanced tool schema description with all return fields documented - - Detailed parameter descriptions for `includeCompressed` and `includePreview` - - Clear explanation of what data is included in each field - -3. **Full Metadata Exposure Verified** - - ✅ `elementCount` - Count of drawing elements - - ✅ `hasCompressedData` - Boolean for compressed data presence - - ✅ `metadata` - Object with appState and version - - ✅ `preview` - Text elements (when requested) - - ✅ `compressedData` - Full drawing data (when requested) - -### Test Cases Validated - -Manual testing was performed for: - -1. **Frontmatter Parsing:** - - ✅ Notes with valid YAML frontmatter - - ✅ Notes without frontmatter - - ✅ Notes with malformed YAML - - ✅ Various YAML formats (arrays, objects, nested) - - ✅ Empty frontmatter blocks - -2. **Parameter Combinations:** - - ✅ `parseFrontmatter: true` with various options - - ✅ `withFrontmatter: false` + `withContent: true` - - ✅ `withFrontmatter: true` + `withContent: false` - - ✅ All parameters at default values - -3. **Excalidraw Support:** - - ✅ Valid Excalidraw files - - ✅ Non-Excalidraw markdown files (graceful handling) - - ✅ Excalidraw files with/without compressed data - - ✅ Preview text extraction - - ✅ Full data inclusion - - ✅ Metadata field exposure - - ✅ Compressed format detection (`compressed-json` code fence) - - ⚠️ **Known Limitation:** `elementCount` returns 0 for compressed files - - Most Excalidraw files use compressed base64 format - - Decompression would require pako library (not included) - - Text elements visible in preview but not counted - - Use `hasCompressedData: true` to identify compressed files - -4. **Edge Cases:** - - ✅ Very large Excalidraw files - - ✅ Files with special characters in frontmatter - - ✅ Files with multiple frontmatter blocks (invalid) - - ✅ Unicode content in frontmatter - -**All test cases passed successfully.** - -## Benefits - -1. **Better Frontmatter Handling** - - Separate frontmatter from content for easier processing - - Parse YAML into structured objects - - Access metadata without manual parsing - -2. **Excalidraw Support** - - First-class support for Excalidraw drawings - - Extract metadata without parsing full drawing - - Optional preview and compressed data - -3. **Flexibility** - - Choose what data to include in responses - - Reduce bandwidth for metadata-only requests - - Maintain backward compatibility - -4. **Type Safety** - - Structured responses with proper TypeScript types - - Clear interfaces for parsed data - - Better IDE autocomplete and validation - -## Next Steps - -Phase 5 is complete. Recommended next phases: - -1. **Phase 6: Powerful Search** (P2, 4-5 days) - - Regex search support - - Snippet extraction - - Advanced filtering - -2. **Phase 8: Write Operations & Concurrency** (P1, 5-6 days) - - Partial updates (frontmatter, sections) - - Concurrency control with ETags - - File rename/move with link updates - -3. **Phase 9: Linking & Backlinks** (P2, 3-4 days) - - Wikilink validation - - Backlink queries - - Link resolution - -## Notes - -- Uses Obsidian's built-in `parseYaml` for YAML parsing -- Frontmatter extraction follows Obsidian's conventions -- Excalidraw detection uses plugin markers -- All error cases return clear error messages -- Implementation is efficient (no unnecessary file reads) - -## Version - -This implementation is part of version **4.0.0** of the Obsidian MCP Server plugin. diff --git a/README.md b/README.md index c09ba46..d1b663b 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,32 @@ An Obsidian plugin that exposes your vault operations via the [Model Context Pro ## Available MCP Tools -- `read_note` - Read the content of a note -- `create_note` - Create a new note -- `update_note` - Update an existing note -- `delete_note` - Delete a note -- `search_notes` - Search for notes by query -- `list_notes` - List all notes or notes in a folder -- `get_vault_info` - Get vault metadata +### Note Operations +- `read_note` - Read the content of a note with optional frontmatter parsing +- `create_note` - Create a new note with conflict handling strategies +- `update_note` - Update an existing note (full content replacement) +- `delete_note` - Delete a note (soft delete to .trash or permanent) +- `update_frontmatter` - Update frontmatter fields without modifying note content +- `update_sections` - Update specific sections of a note by line range +- `rename_file` - Rename or move a file with automatic wikilink updates +- `read_excalidraw` - Read Excalidraw drawing files with metadata extraction (currently limited to uncompressed format; compressed format support is planned) + +### Vault Operations +- `search` - Search vault with advanced filtering, regex support, and snippet extraction +- `search_waypoints` - Find all Waypoint plugin markers in the vault +- `list` - List files and/or directories with advanced filtering and pagination +- `stat` - Get detailed metadata for a file or folder +- `exists` - Check if a file or folder exists at a specific path +- `get_vault_info` - Get vault metadata (name, path, file counts, total size) + +### Waypoint Integration +- `get_folder_waypoint` - Get Waypoint block from a folder note +- `is_folder_note` - Check if a note is a folder note + +### Link Management +- `validate_wikilinks` - Validate all wikilinks in a note and report unresolved links +- `resolve_wikilink` - Resolve a single wikilink from a source note to its target path +- `backlinks` - Get all backlinks to a note with optional unlinked mentions ## Installation @@ -158,7 +177,7 @@ curl -X POST http://127.0.0.1:3000/mcp \ "id": 5, "method": "tools/call", "params": { - "name": "search_notes", + "name": "search", "arguments": { "query": "search term" } diff --git a/manifest.json b/manifest.json index f028f0e..dd7d977 100644 --- a/manifest.json +++ b/manifest.json @@ -4,5 +4,6 @@ "version": "3.0.0", "minAppVersion": "0.15.0", "description": "Exposes Obsidian vault operations via Model Context Protocol (MCP) over HTTP", + "author": "Bill Ballou", "isDesktopOnly": true } diff --git a/old-structure/main.ts b/old-structure/main.ts deleted file mode 100644 index 98c8133..0000000 --- a/old-structure/main.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { App, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; -import { MCPServer, MCPServerSettings } from './mcp-server'; - -interface MCPPluginSettings extends MCPServerSettings { - autoStart: boolean; -} - -const DEFAULT_SETTINGS: MCPPluginSettings = { - port: 3000, - enableCORS: true, - allowedOrigins: ['*'], - apiKey: '', - enableAuth: false, - autoStart: false -} - -export default class MCPServerPlugin extends Plugin { - settings: MCPPluginSettings; - mcpServer: MCPServer | null = null; - statusBarItem: HTMLElement | null = null; - - async onload() { - await this.loadSettings(); - - // Add status bar item - this.statusBarItem = this.addStatusBarItem(); - this.updateStatusBar(); - - // Add ribbon icon to toggle server - this.addRibbonIcon('server', 'Toggle MCP Server', async () => { - if (this.mcpServer?.isRunning()) { - await this.stopServer(); - } else { - await this.startServer(); - } - }); - - // Add commands - this.addCommand({ - id: 'start-mcp-server', - name: 'Start MCP Server', - callback: async () => { - await this.startServer(); - } - }); - - this.addCommand({ - id: 'stop-mcp-server', - name: 'Stop MCP Server', - callback: async () => { - await this.stopServer(); - } - }); - - this.addCommand({ - id: 'restart-mcp-server', - name: 'Restart MCP Server', - callback: async () => { - await this.stopServer(); - await this.startServer(); - } - }); - - // Add settings tab - this.addSettingTab(new MCPServerSettingTab(this.app, this)); - - // Auto-start if enabled - if (this.settings.autoStart) { - await this.startServer(); - } - } - - async onunload() { - await this.stopServer(); - } - - async startServer() { - if (this.mcpServer?.isRunning()) { - new Notice('MCP Server is already running'); - return; - } - - try { - this.mcpServer = new MCPServer(this.app, this.settings); - await this.mcpServer.start(); - new Notice(`MCP Server started on port ${this.settings.port}`); - this.updateStatusBar(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - new Notice(`Failed to start MCP Server: ${message}`); - console.error('MCP Server start error:', error); - } - } - - async stopServer() { - if (!this.mcpServer?.isRunning()) { - new Notice('MCP Server is not running'); - return; - } - - try { - await this.mcpServer.stop(); - new Notice('MCP Server stopped'); - this.updateStatusBar(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - new Notice(`Failed to stop MCP Server: ${message}`); - console.error('MCP Server stop error:', error); - } - } - - updateStatusBar() { - if (this.statusBarItem) { - const isRunning = this.mcpServer?.isRunning() ?? false; - this.statusBarItem.setText( - isRunning - ? `MCP: Running (${this.settings.port})` - : 'MCP: Stopped' - ); - this.statusBarItem.addClass('mcp-status-bar'); - } - } - - async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } - - async saveSettings() { - await this.saveData(this.settings); - // Update server settings if it's running - if (this.mcpServer) { - this.mcpServer.updateSettings(this.settings); - } - } -} - -class MCPServerSettingTab extends PluginSettingTab { - plugin: MCPServerPlugin; - - constructor(app: App, plugin: MCPServerPlugin) { - super(app, plugin); - this.plugin = plugin; - } - - display(): void { - const {containerEl} = this; - - containerEl.empty(); - - containerEl.createEl('h2', {text: 'MCP Server Settings'}); - - // Auto-start setting - new Setting(containerEl) - .setName('Auto-start server') - .setDesc('Automatically start the MCP server when Obsidian launches') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.autoStart) - .onChange(async (value) => { - this.plugin.settings.autoStart = value; - await this.plugin.saveSettings(); - })); - - // Port setting - new Setting(containerEl) - .setName('Port') - .setDesc('Port number for the HTTP server (requires restart)') - .addText(text => text - .setPlaceholder('3000') - .setValue(String(this.plugin.settings.port)) - .onChange(async (value) => { - const port = parseInt(value); - if (!isNaN(port) && port > 0 && port < 65536) { - this.plugin.settings.port = port; - await this.plugin.saveSettings(); - } - })); - - // CORS setting - new Setting(containerEl) - .setName('Enable CORS') - .setDesc('Enable Cross-Origin Resource Sharing') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableCORS) - .onChange(async (value) => { - this.plugin.settings.enableCORS = value; - await this.plugin.saveSettings(); - })); - - // Allowed origins - new Setting(containerEl) - .setName('Allowed origins') - .setDesc('Comma-separated list of allowed origins (* for all)') - .addText(text => text - .setPlaceholder('*') - .setValue(this.plugin.settings.allowedOrigins.join(', ')) - .onChange(async (value) => { - this.plugin.settings.allowedOrigins = value - .split(',') - .map(s => s.trim()) - .filter(s => s.length > 0); - await this.plugin.saveSettings(); - })); - - // Authentication - new Setting(containerEl) - .setName('Enable authentication') - .setDesc('Require API key for requests') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableAuth) - .onChange(async (value) => { - this.plugin.settings.enableAuth = value; - await this.plugin.saveSettings(); - })); - - // API Key - new Setting(containerEl) - .setName('API Key') - .setDesc('API key for authentication (Bearer token)') - .addText(text => text - .setPlaceholder('Enter API key') - .setValue(this.plugin.settings.apiKey || '') - .onChange(async (value) => { - this.plugin.settings.apiKey = value; - await this.plugin.saveSettings(); - })); - - // Server status - containerEl.createEl('h3', {text: 'Server Status'}); - - const statusEl = containerEl.createEl('div', {cls: 'mcp-server-status'}); - const isRunning = this.plugin.mcpServer?.isRunning() ?? false; - - statusEl.createEl('p', { - text: isRunning - ? `✅ Server is running on http://127.0.0.1:${this.plugin.settings.port}/mcp` - : '⭕ Server is stopped' - }); - - // Control buttons - const buttonContainer = containerEl.createEl('div', {cls: 'mcp-button-container'}); - - if (isRunning) { - buttonContainer.createEl('button', {text: 'Stop Server'}) - .addEventListener('click', async () => { - await this.plugin.stopServer(); - this.display(); // Refresh display - }); - - buttonContainer.createEl('button', {text: 'Restart Server'}) - .addEventListener('click', async () => { - await this.plugin.stopServer(); - await this.plugin.startServer(); - this.display(); // Refresh display - }); - } else { - buttonContainer.createEl('button', {text: 'Start Server'}) - .addEventListener('click', async () => { - await this.plugin.startServer(); - this.display(); // Refresh display - }); - } - - // Connection info - if (isRunning) { - containerEl.createEl('h3', {text: 'Connection Information'}); - - const infoEl = containerEl.createEl('div', {cls: 'mcp-connection-info'}); - infoEl.createEl('p', {text: 'MCP Endpoint:'}); - infoEl.createEl('code', {text: `http://127.0.0.1:${this.plugin.settings.port}/mcp`}); - - infoEl.createEl('p', {text: 'Health Check:'}); - infoEl.createEl('code', {text: `http://127.0.0.1:${this.plugin.settings.port}/health`}); - } - } -} diff --git a/old-structure/mcp-server.ts b/old-structure/mcp-server.ts deleted file mode 100644 index 3f3d395..0000000 --- a/old-structure/mcp-server.ts +++ /dev/null @@ -1,485 +0,0 @@ -import { App, TFile, TFolder } from 'obsidian'; -import express, { Express, Request, Response } from 'express'; -import cors from 'cors'; -import { Server } from 'http'; -import { - JSONRPCRequest, - JSONRPCResponse, - JSONRPCError, - InitializeResult, - ListToolsResult, - CallToolResult, - Tool, - ErrorCodes, - ContentBlock -} from './mcp-types'; - -export interface MCPServerSettings { - port: number; - enableCORS: boolean; - allowedOrigins: string[]; - apiKey?: string; - enableAuth: boolean; -} - -export class MCPServer { - private app: Express; - private server: Server | null = null; - private obsidianApp: App; - private settings: MCPServerSettings; - - constructor(obsidianApp: App, settings: MCPServerSettings) { - this.obsidianApp = obsidianApp; - this.settings = settings; - this.app = express(); - this.setupMiddleware(); - this.setupRoutes(); - } - - private setupMiddleware(): void { - // Parse JSON bodies - this.app.use(express.json()); - - // CORS configuration - if (this.settings.enableCORS) { - const corsOptions = { - origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => { - // Allow requests with no origin (like mobile apps or curl requests) - if (!origin) return callback(null, true); - - if (this.settings.allowedOrigins.includes('*') || - this.settings.allowedOrigins.includes(origin)) { - callback(null, true); - } else { - callback(new Error('Not allowed by CORS')); - } - }, - credentials: true - }; - this.app.use(cors(corsOptions)); - } - - // Authentication middleware - if (this.settings.enableAuth && this.settings.apiKey) { - this.app.use((req: Request, res: Response, next: any) => { - const authHeader = req.headers.authorization; - const apiKey = authHeader?.replace('Bearer ', ''); - - if (apiKey !== this.settings.apiKey) { - return res.status(401).json(this.createErrorResponse(null, ErrorCodes.InvalidRequest, 'Unauthorized')); - } - next(); - }); - } - - // Origin validation for security (DNS rebinding protection) - this.app.use((req: Request, res: Response, next: any) => { - const origin = req.headers.origin; - const host = req.headers.host; - - // Only allow localhost connections - if (host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) { - return res.status(403).json(this.createErrorResponse(null, ErrorCodes.InvalidRequest, 'Only localhost connections allowed')); - } - - next(); - }); - } - - private setupRoutes(): void { - // Main MCP endpoint - this.app.post('/mcp', async (req: Request, res: Response) => { - try { - const request = req.body as JSONRPCRequest; - const response = await this.handleRequest(request); - res.json(response); - } catch (error) { - console.error('MCP request error:', error); - res.status(500).json(this.createErrorResponse(null, ErrorCodes.InternalError, 'Internal server error')); - } - }); - - // Health check endpoint - this.app.get('/health', (_req: Request, res: Response) => { - res.json({ status: 'ok', timestamp: Date.now() }); - }); - } - - private async handleRequest(request: JSONRPCRequest): Promise { - try { - switch (request.method) { - case 'initialize': - return this.createSuccessResponse(request.id, await this.handleInitialize(request.params)); - case 'tools/list': - return this.createSuccessResponse(request.id, await this.handleListTools()); - case 'tools/call': - return this.createSuccessResponse(request.id, await this.handleCallTool(request.params)); - case 'ping': - return this.createSuccessResponse(request.id, {}); - default: - return this.createErrorResponse(request.id, ErrorCodes.MethodNotFound, `Method not found: ${request.method}`); - } - } catch (error) { - console.error('Error handling request:', error); - return this.createErrorResponse(request.id, ErrorCodes.InternalError, error.message); - } - } - - private async handleInitialize(params: any): Promise { - return { - protocolVersion: "2024-11-05", - capabilities: { - tools: {} - }, - serverInfo: { - name: "obsidian-mcp-server", - version: "1.0.0" - } - }; - } - - private async handleListTools(): Promise { - const tools: Tool[] = [ - { - name: "read_note", - description: "Read the content of a note from the Obsidian vault", - inputSchema: { - type: "object", - properties: { - path: { - type: "string", - description: "Path to the note within the vault (e.g., 'folder/note.md')" - } - }, - required: ["path"] - } - }, - { - name: "create_note", - description: "Create a new note in the Obsidian vault", - inputSchema: { - type: "object", - properties: { - path: { - type: "string", - description: "Path for the new note (e.g., 'folder/note.md')" - }, - content: { - type: "string", - description: "Content of the note" - } - }, - required: ["path", "content"] - } - }, - { - name: "update_note", - description: "Update an existing note in the Obsidian vault", - inputSchema: { - type: "object", - properties: { - path: { - type: "string", - description: "Path to the note to update" - }, - content: { - type: "string", - description: "New content for the note" - } - }, - required: ["path", "content"] - } - }, - { - name: "delete_note", - description: "Delete a note from the Obsidian vault", - inputSchema: { - type: "object", - properties: { - path: { - type: "string", - description: "Path to the note to delete" - } - }, - required: ["path"] - } - }, - { - name: "search_notes", - description: "Search for notes in the Obsidian vault", - inputSchema: { - type: "object", - properties: { - query: { - type: "string", - description: "Search query string" - } - }, - required: ["query"] - } - }, - { - name: "get_vault_info", - description: "Get information about the Obsidian vault", - inputSchema: { - type: "object", - properties: {} - } - }, - { - name: "list_notes", - description: "List all notes in the vault or in a specific folder", - inputSchema: { - type: "object", - properties: { - folder: { - type: "string", - description: "Optional folder path to list notes from" - } - } - } - } - ]; - - return { tools }; - } - - private async handleCallTool(params: any): Promise { - const { name, arguments: args } = params; - - try { - switch (name) { - case "read_note": - return await this.readNote(args.path); - case "create_note": - return await this.createNote(args.path, args.content); - case "update_note": - return await this.updateNote(args.path, args.content); - case "delete_note": - return await this.deleteNote(args.path); - case "search_notes": - return await this.searchNotes(args.query); - case "get_vault_info": - return await this.getVaultInfo(); - case "list_notes": - return await this.listNotes(args.folder); - default: - return { - content: [{ type: "text", text: `Unknown tool: ${name}` }], - isError: true - }; - } - } catch (error) { - return { - content: [{ type: "text", text: `Error: ${error.message}` }], - isError: true - }; - } - } - - // Tool implementations - - private async readNote(path: string): Promise { - const file = this.obsidianApp.vault.getAbstractFileByPath(path); - - if (!file || !(file instanceof TFile)) { - return { - content: [{ type: "text", text: `Note not found: ${path}` }], - isError: true - }; - } - - const content = await this.obsidianApp.vault.read(file); - return { - content: [{ type: "text", text: content }] - }; - } - - private async createNote(path: string, content: string): Promise { - try { - const file = await this.obsidianApp.vault.create(path, content); - return { - content: [{ type: "text", text: `Note created successfully: ${file.path}` }] - }; - } catch (error) { - return { - content: [{ type: "text", text: `Failed to create note: ${error.message}` }], - isError: true - }; - } - } - - private async updateNote(path: string, content: string): Promise { - const file = this.obsidianApp.vault.getAbstractFileByPath(path); - - if (!file || !(file instanceof TFile)) { - return { - content: [{ type: "text", text: `Note not found: ${path}` }], - isError: true - }; - } - - await this.obsidianApp.vault.modify(file, content); - return { - content: [{ type: "text", text: `Note updated successfully: ${path}` }] - }; - } - - private async deleteNote(path: string): Promise { - const file = this.obsidianApp.vault.getAbstractFileByPath(path); - - if (!file || !(file instanceof TFile)) { - return { - content: [{ type: "text", text: `Note not found: ${path}` }], - isError: true - }; - } - - await this.obsidianApp.vault.delete(file); - return { - content: [{ type: "text", text: `Note deleted successfully: ${path}` }] - }; - } - - private async searchNotes(query: string): Promise { - const files = this.obsidianApp.vault.getMarkdownFiles(); - const results: string[] = []; - - for (const file of files) { - const content = await this.obsidianApp.vault.read(file); - if (content.toLowerCase().includes(query.toLowerCase()) || - file.basename.toLowerCase().includes(query.toLowerCase())) { - results.push(file.path); - } - } - - return { - content: [{ - type: "text", - text: results.length > 0 - ? `Found ${results.length} notes:\n${results.join('\n')}` - : 'No notes found matching the query' - }] - }; - } - - private async getVaultInfo(): Promise { - const files = this.obsidianApp.vault.getFiles(); - const markdownFiles = this.obsidianApp.vault.getMarkdownFiles(); - - const info = { - name: this.obsidianApp.vault.getName(), - totalFiles: files.length, - markdownFiles: markdownFiles.length, - rootPath: (this.obsidianApp.vault.adapter as any).basePath || 'Unknown' - }; - - return { - content: [{ - type: "text", - text: JSON.stringify(info, null, 2) - }] - }; - } - - private async listNotes(folder?: string): Promise { - let files: TFile[]; - - if (folder) { - const folderObj = this.obsidianApp.vault.getAbstractFileByPath(folder); - if (!folderObj || !(folderObj instanceof TFolder)) { - return { - content: [{ type: "text", text: `Folder not found: ${folder}` }], - isError: true - }; - } - files = []; - this.obsidianApp.vault.getMarkdownFiles().forEach((file: TFile) => { - if (file.path.startsWith(folder + '/')) { - files.push(file); - } - }); - } else { - files = this.obsidianApp.vault.getMarkdownFiles(); - } - - const noteList = files.map(f => f.path).join('\n'); - return { - content: [{ - type: "text", - text: `Found ${files.length} notes:\n${noteList}` - }] - }; - } - - // Helper methods - - private createSuccessResponse(id: string | number | undefined, result: any): JSONRPCResponse { - return { - jsonrpc: "2.0", - id: id ?? null, - result - }; - } - - private createErrorResponse(id: string | number | undefined | null, code: number, message: string, data?: any): JSONRPCResponse { - return { - jsonrpc: "2.0", - id: id ?? null, - error: { - code, - message, - data - } - }; - } - - // Server lifecycle - - public async start(): Promise { - return new Promise((resolve, reject) => { - try { - this.server = this.app.listen(this.settings.port, '127.0.0.1', () => { - console.log(`MCP Server listening on http://127.0.0.1:${this.settings.port}/mcp`); - resolve(); - }); - - this.server.on('error', (error: any) => { - if (error.code === 'EADDRINUSE') { - reject(new Error(`Port ${this.settings.port} is already in use`)); - } else { - reject(error); - } - }); - } catch (error) { - reject(error); - } - }); - } - - public async stop(): Promise { - return new Promise((resolve, reject) => { - if (this.server) { - this.server.close((err?: Error) => { - if (err) { - reject(err); - } else { - console.log('MCP Server stopped'); - this.server = null; - resolve(); - } - }); - } else { - resolve(); - } - }); - } - - public isRunning(): boolean { - return this.server !== null; - } - - public updateSettings(settings: MCPServerSettings): void { - this.settings = settings; - } -} diff --git a/old-structure/mcp-types.ts b/old-structure/mcp-types.ts deleted file mode 100644 index 1862808..0000000 --- a/old-structure/mcp-types.ts +++ /dev/null @@ -1,122 +0,0 @@ -// MCP Protocol Types based on JSON-RPC 2.0 - -export interface JSONRPCRequest { - jsonrpc: "2.0"; - id?: string | number; - method: string; - params?: any; -} - -export interface JSONRPCResponse { - jsonrpc: "2.0"; - id: string | number | null; - result?: any; - error?: JSONRPCError; -} - -export interface JSONRPCError { - code: number; - message: string; - data?: any; -} - -export interface JSONRPCNotification { - jsonrpc: "2.0"; - method: string; - params?: any; -} - -// MCP Protocol Messages - -export interface InitializeRequest { - method: "initialize"; - params: { - protocolVersion: string; - capabilities: ClientCapabilities; - clientInfo: { - name: string; - version: string; - }; - }; -} - -export interface InitializeResult { - protocolVersion: string; - capabilities: ServerCapabilities; - serverInfo: { - name: string; - version: string; - }; -} - -export interface ClientCapabilities { - roots?: { - listChanged?: boolean; - }; - sampling?: {}; - experimental?: Record; -} - -export interface ServerCapabilities { - tools?: {}; - resources?: { - subscribe?: boolean; - listChanged?: boolean; - }; - prompts?: { - listChanged?: boolean; - }; - logging?: {}; - experimental?: Record; -} - -export interface ListToolsRequest { - method: "tools/list"; - params?: { - cursor?: string; - }; -} - -export interface Tool { - name: string; - description?: string; - inputSchema: { - type: "object"; - properties?: Record; - required?: string[]; - }; -} - -export interface ListToolsResult { - tools: Tool[]; - nextCursor?: string; -} - -export interface CallToolRequest { - method: "tools/call"; - params: { - name: string; - arguments?: Record; - }; -} - -export interface CallToolResult { - content: ContentBlock[]; - isError?: boolean; -} - -export interface ContentBlock { - type: "text" | "image" | "resource"; - text?: string; - data?: string; - mimeType?: string; -} - -// Error codes -export const ErrorCodes = { - ParseError: -32700, - InvalidRequest: -32600, - MethodNotFound: -32601, - InvalidParams: -32602, - InternalError: -32603, -}; diff --git a/versions.json b/versions.json index d5306aa..d722c35 100644 --- a/versions.json +++ b/versions.json @@ -3,5 +3,6 @@ "1.1.0": "0.15.0", "1.2.0": "0.15.0", "2.0.0": "0.15.0", - "2.1.0": "0.15.0" + "2.1.0": "0.15.0", + "3.0.0": "0.15.0" }