docs: cleanup of documentation

This commit is contained in:
2025-10-19 21:47:30 -04:00
parent b681327970
commit 42ed93500c
9 changed files with 278 additions and 1514 deletions

248
CLAUDE.md Normal file
View File

@@ -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

View File

@@ -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! 🎉**

View File

@@ -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<string, any>;
content: string;
contentWithoutFrontmatter?: string;
}
export interface ExcalidrawMetadata {
path: string;
isExcalidraw: boolean;
elementCount?: number;
hasCompressedData?: boolean;
metadata?: Record<string, any>;
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.

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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`});
}
}
}

View File

@@ -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<JSONRPCResponse> {
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<InitializeResult> {
return {
protocolVersion: "2024-11-05",
capabilities: {
tools: {}
},
serverInfo: {
name: "obsidian-mcp-server",
version: "1.0.0"
}
};
}
private async handleListTools(): Promise<ListToolsResult> {
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<CallToolResult> {
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<CallToolResult> {
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<CallToolResult> {
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<CallToolResult> {
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<CallToolResult> {
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<CallToolResult> {
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<CallToolResult> {
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<CallToolResult> {
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<void> {
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<void> {
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;
}
}

View File

@@ -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<string, any>;
}
export interface ServerCapabilities {
tools?: {};
resources?: {
subscribe?: boolean;
listChanged?: boolean;
};
prompts?: {
listChanged?: boolean;
};
logging?: {};
experimental?: Record<string, any>;
}
export interface ListToolsRequest {
method: "tools/list";
params?: {
cursor?: string;
};
}
export interface Tool {
name: string;
description?: string;
inputSchema: {
type: "object";
properties?: Record<string, any>;
required?: string[];
};
}
export interface ListToolsResult {
tools: Tool[];
nextCursor?: string;
}
export interface CallToolRequest {
method: "tools/call";
params: {
name: string;
arguments?: Record<string, any>;
};
}
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,
};

View File

@@ -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"
}