Release v1.2.0: Enhanced Authentication & Parent Folder Detection
Phase 1.5 Complete: - Add automatic API key generation with secure random generation - Add createParents parameter to create_note tool - Fix authentication vulnerability (auth enabled without key) - Add MCP client configuration snippet generator - Improve UI/UX for authentication management - Add comprehensive test coverage Security: - Fixed critical vulnerability in authentication middleware - Implement three-layer defense (UI, server start, middleware) - Cryptographically secure key generation (32 chars) Features: - Auto-generate API key when authentication enabled - Copy/regenerate buttons for API key management - Recursive parent folder creation for nested paths - Enhanced error messages with actionable guidance - Selectable connection information and config snippets Documentation: - Updated CHANGELOG.md with v1.2.0 release notes - Updated ROADMAP.md (Phase 1.5 marked complete) - Created IMPLEMENTATION_NOTES_AUTH.md - Created RELEASE_NOTES_v1.2.0.md
This commit is contained in:
90
CHANGELOG.md
90
CHANGELOG.md
@@ -2,6 +2,96 @@
|
|||||||
|
|
||||||
All notable changes to the Obsidian MCP Server plugin will be documented in this file.
|
All notable changes to the Obsidian MCP Server plugin will be documented in this file.
|
||||||
|
|
||||||
|
## [1.2.0] - 2025-10-16
|
||||||
|
|
||||||
|
### 📁 Enhanced Parent Folder Detection (Phase 1.5)
|
||||||
|
|
||||||
|
Improved `create_note` tool with explicit parent folder validation and optional automatic folder creation.
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
**Parent Folder Validation (`src/tools/note-tools.ts`)**
|
||||||
|
- Explicit parent folder detection before file creation (fail-fast)
|
||||||
|
- New `createParents` parameter for automatic folder creation
|
||||||
|
- Recursive parent folder creation for deeply nested paths
|
||||||
|
- Validates parent is a folder (not a file)
|
||||||
|
- Clear error messages with actionable guidance
|
||||||
|
|
||||||
|
**Tool Schema Updates (`src/tools/index.ts`)**
|
||||||
|
- Added `createParents` boolean parameter to `create_note` tool
|
||||||
|
- Default: `false` (safe behavior - requires parent folders to exist)
|
||||||
|
- Optional: `true` (convenience - auto-creates missing parent folders)
|
||||||
|
- Updated tool description with usage examples
|
||||||
|
|
||||||
|
**Enhanced Error Messages (`src/utils/error-messages.ts`)**
|
||||||
|
- `parentFolderNotFound()` now suggests using `createParents: true`
|
||||||
|
- Provides example usage with auto-creation
|
||||||
|
- Computes grandparent path for better `list_notes()` suggestions
|
||||||
|
- Clear troubleshooting steps for missing parent folders
|
||||||
|
|
||||||
|
**Comprehensive Test Suite (`tests/parent-folder-detection.test.ts`)**
|
||||||
|
- 15 test cases covering all scenarios
|
||||||
|
- Tests explicit parent folder detection
|
||||||
|
- Tests recursive folder creation
|
||||||
|
- Tests error handling and edge cases
|
||||||
|
- Validates error message content
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
- `createNote()` signature: added optional `createParents` parameter
|
||||||
|
- Parent folder validation now happens before file creation attempt
|
||||||
|
- Error messages include `createParents` usage examples
|
||||||
|
|
||||||
|
#### Benefits
|
||||||
|
- **Fail-fast behavior**: Errors detected before attempting file creation
|
||||||
|
- **Flexibility**: Optional auto-creation with `createParents: true`
|
||||||
|
- **Robustness**: Handles deeply nested paths and all edge cases
|
||||||
|
- **Backward compatible**: Existing code continues to work (default: `false`)
|
||||||
|
|
||||||
|
### 🔐 Enhanced Authentication & Security (Phase 1.5)
|
||||||
|
|
||||||
|
This update significantly improves authentication security and user experience with automatic key generation and enhanced UI.
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
**Automatic API Key Generation (`src/utils/auth-utils.ts`)**
|
||||||
|
- `generateApiKey()` - Cryptographically secure random key generation (32 characters)
|
||||||
|
- `validateApiKey()` - API key validation with strength requirements
|
||||||
|
- Uses `crypto.getRandomValues()` for secure randomness
|
||||||
|
- Alphanumeric + special characters (`-`, `_`) for URL-safe keys
|
||||||
|
|
||||||
|
**Enhanced Settings UI (`src/settings.ts`)**
|
||||||
|
- Auto-generate API key when authentication is enabled
|
||||||
|
- Copy to clipboard button for API key
|
||||||
|
- Regenerate key button with instant refresh
|
||||||
|
- Static, selectable API key display (full width)
|
||||||
|
- MCP client configuration snippet generator
|
||||||
|
- Dynamically includes/excludes Authorization header based on auth status
|
||||||
|
- Correct `mcpServers` format with `serverUrl` field
|
||||||
|
- Copy configuration button for one-click copying
|
||||||
|
- Partially selectable text for easy copying
|
||||||
|
- Restart warnings when authentication settings change
|
||||||
|
- Selectable connection information URLs
|
||||||
|
|
||||||
|
**Security Improvements (`src/server/middleware.ts`)**
|
||||||
|
- Defensive authentication check: rejects requests if auth enabled but no key set
|
||||||
|
- Improved error messages for authentication failures
|
||||||
|
- Fail-secure design: blocks access when misconfigured
|
||||||
|
|
||||||
|
**Server Validation (`src/main.ts`)**
|
||||||
|
- Prevents server start if authentication enabled without API key
|
||||||
|
- Clear error message guiding users to fix configuration
|
||||||
|
- Validation runs before server initialization
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
- API key field changed from user-editable to auto-generated display
|
||||||
|
- Configuration snippet now shows for both authenticated and non-authenticated setups
|
||||||
|
- Connection information URLs are now selectable
|
||||||
|
|
||||||
|
#### Security
|
||||||
|
- Fixed vulnerability where enabling authentication without API key allowed unrestricted access
|
||||||
|
- Three-layer defense: UI validation, server start validation, and middleware enforcement
|
||||||
|
- API keys are now always cryptographically secure (no weak user-chosen keys)
|
||||||
|
|
||||||
## [1.1.0] - 2025-10-16
|
## [1.1.0] - 2025-10-16
|
||||||
|
|
||||||
### 🎯 Phase 1.1: Path Normalization & Error Handling
|
### 🎯 Phase 1.1: Path Normalization & Error Handling
|
||||||
|
|||||||
@@ -1,216 +0,0 @@
|
|||||||
# Release Notes: Version 1.1.0
|
|
||||||
|
|
||||||
**Release Date:** October 16, 2025
|
|
||||||
**Type:** Minor Version (Feature Release)
|
|
||||||
**Compatibility:** Fully backward compatible with v1.0.0
|
|
||||||
|
|
||||||
## 🎯 Overview
|
|
||||||
|
|
||||||
Version 1.1.0 implements **Phase 1.1** of the roadmap, focusing on robustness, cross-platform compatibility, and significantly improved user experience through enhanced error messages and path handling.
|
|
||||||
|
|
||||||
## ✨ What's New
|
|
||||||
|
|
||||||
### Path Normalization & Validation
|
|
||||||
|
|
||||||
**Cross-platform path handling** that works seamlessly on Windows, macOS, and Linux:
|
|
||||||
- Automatic conversion of backslashes to forward slashes
|
|
||||||
- Windows drive letter handling
|
|
||||||
- Leading/trailing slash normalization
|
|
||||||
- Security: Prevents directory traversal attacks
|
|
||||||
- Case sensitivity awareness (macOS/Linux vs Windows)
|
|
||||||
|
|
||||||
### Enhanced Error Messages
|
|
||||||
|
|
||||||
**Actionable error messages** that help users fix issues quickly:
|
|
||||||
- Context-aware troubleshooting tips
|
|
||||||
- Dynamic suggestions (e.g., "Use list_notes('folder') to see available files")
|
|
||||||
- Clear examples of correct path formats
|
|
||||||
- Platform-specific guidance
|
|
||||||
- Operation-specific recommendations
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```
|
|
||||||
Parent folder does not exist: "projects/2024/q4"
|
|
||||||
|
|
||||||
Cannot create "projects/2024/q4/report.md" because its parent folder is missing.
|
|
||||||
|
|
||||||
Troubleshooting tips:
|
|
||||||
• Create the parent folder first using Obsidian
|
|
||||||
• Verify the folder path with list_notes("projects/2024")
|
|
||||||
• Check that the parent folder path is correct (vault-relative, case-sensitive on macOS/Linux)
|
|
||||||
• Note: Automatic parent folder creation is not currently enabled
|
|
||||||
```
|
|
||||||
|
|
||||||
### Improved Tool Descriptions
|
|
||||||
|
|
||||||
**AI agents now receive comprehensive guidance** directly in the MCP schema:
|
|
||||||
- Critical constraints stated upfront
|
|
||||||
- Workflow suggestions (e.g., "use list_notes() first if unsure")
|
|
||||||
- Multiple concrete examples per tool
|
|
||||||
- Failure modes explicitly documented
|
|
||||||
- Self-documenting without external docs
|
|
||||||
|
|
||||||
### Testing Infrastructure
|
|
||||||
|
|
||||||
**Professional testing setup** for reliability:
|
|
||||||
- Jest testing framework configured
|
|
||||||
- 43 unit tests (all passing)
|
|
||||||
- Mock Obsidian API for isolated testing
|
|
||||||
- Cross-platform test coverage
|
|
||||||
- Easy to run: `npm test`
|
|
||||||
|
|
||||||
### Comprehensive Documentation
|
|
||||||
|
|
||||||
**New documentation for users and developers:**
|
|
||||||
- **Tool Selection Guide** (400+ lines) - Complete guide on choosing the right tool
|
|
||||||
- **Error Message Improvements** - Documentation of all enhancements
|
|
||||||
- **Tool Description Improvements** - AI agent guidance documentation
|
|
||||||
- **Testing Guide** - How to run and write tests
|
|
||||||
- **Phase 1.1 Implementation Summary** - Technical details
|
|
||||||
|
|
||||||
## 🔧 Technical Improvements
|
|
||||||
|
|
||||||
### New Utilities
|
|
||||||
|
|
||||||
**`PathUtils` class** (`src/utils/path-utils.ts`):
|
|
||||||
- 15+ utility methods for path operations
|
|
||||||
- Type-safe file/folder resolution
|
|
||||||
- Existence checking
|
|
||||||
- Path manipulation (parent, basename, join)
|
|
||||||
- Markdown extension handling
|
|
||||||
|
|
||||||
**`ErrorMessages` class** (`src/utils/error-messages.ts`):
|
|
||||||
- 11 specialized error message generators
|
|
||||||
- Dynamic context-based suggestions
|
|
||||||
- Consistent formatting
|
|
||||||
- Reusable across all tools
|
|
||||||
|
|
||||||
### Updated Tool Implementations
|
|
||||||
|
|
||||||
All tools now use the new utilities:
|
|
||||||
- ✅ `readNote()` - Enhanced validation and error messages
|
|
||||||
- ✅ `createNote()` - Parent folder validation, conflict detection
|
|
||||||
- ✅ `updateNote()` - Better error handling
|
|
||||||
- ✅ `deleteNote()` - Folder detection with clear error
|
|
||||||
- ✅ `listNotes()` - Path validation and verification
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
- **Windows path handling** - Backslashes now converted automatically
|
|
||||||
- **Delete folder error** - Clear message instead of confusing "not a folder" error
|
|
||||||
- **Parent folder detection** - Specific guidance when parent missing
|
|
||||||
- **Error contradictions** - All error messages now internally consistent
|
|
||||||
- **Path validation** - Prevents invalid characters and security issues
|
|
||||||
|
|
||||||
## 📊 Statistics
|
|
||||||
|
|
||||||
- **New Files:** 8 (utilities, tests, mocks, docs)
|
|
||||||
- **Modified Files:** 5 (tool implementations, package.json, manifest)
|
|
||||||
- **Lines Added:** ~2,500+
|
|
||||||
- **Test Coverage:** 43 tests, 100% PathUtils coverage
|
|
||||||
- **Documentation:** 1,000+ lines of new documentation
|
|
||||||
|
|
||||||
## 🚀 Upgrade Instructions
|
|
||||||
|
|
||||||
### For Users
|
|
||||||
|
|
||||||
1. **Backup settings** (optional, but recommended)
|
|
||||||
2. **Update plugin files** to v1.1.0
|
|
||||||
3. **Restart Obsidian** or reload the plugin
|
|
||||||
4. **No configuration changes needed** - fully backward compatible
|
|
||||||
|
|
||||||
### For Developers
|
|
||||||
|
|
||||||
1. **Install new dev dependencies:**
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Run tests:**
|
|
||||||
```bash
|
|
||||||
npm test
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Build:**
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚠️ Breaking Changes
|
|
||||||
|
|
||||||
**None** - Version 1.1.0 is fully backward compatible with v1.0.0.
|
|
||||||
|
|
||||||
All existing integrations will continue to work without modification. The improvements are additive and enhance the existing functionality.
|
|
||||||
|
|
||||||
## 📈 Roadmap Progress
|
|
||||||
|
|
||||||
### Completed ✅
|
|
||||||
- **Phase 1.1** - Path Normalization & Error Handling (100%)
|
|
||||||
- Path utilities ✅
|
|
||||||
- Enhanced error messages ✅
|
|
||||||
- Tool implementation updates ✅
|
|
||||||
- Testing infrastructure ✅
|
|
||||||
|
|
||||||
### Next Up 🔜
|
|
||||||
- **Phase 1.5** - Enhanced Authentication & Security
|
|
||||||
- Secure API key management
|
|
||||||
- Multiple API keys with labels
|
|
||||||
- Key expiration and rotation
|
|
||||||
- Rate limiting
|
|
||||||
- Audit logging
|
|
||||||
|
|
||||||
- **Phase 2** - API Unification & Typed Results
|
|
||||||
- Standardize parameter naming
|
|
||||||
- Structured, typed responses
|
|
||||||
- Better consistency
|
|
||||||
|
|
||||||
## 🎓 Learning Resources
|
|
||||||
|
|
||||||
### New Documentation
|
|
||||||
- Read `docs/TOOL_SELECTION_GUIDE.md` for comprehensive tool usage guide
|
|
||||||
- Check `docs/ERROR_MESSAGE_IMPROVEMENTS.md` for error message details
|
|
||||||
- See `tests/README.md` for testing setup and guidelines
|
|
||||||
|
|
||||||
### Quick Reference
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ OBSIDIAN MCP TOOL QUICK REFERENCE │
|
|
||||||
├─────────────────────────────────────────────────────────┤
|
|
||||||
│ List folder: list_notes("folder") │
|
|
||||||
│ Read file: read_note("folder/file.md") │
|
|
||||||
│ Create file: create_note("path.md", "content") │
|
|
||||||
│ Update file: update_note("path.md", "new content") │
|
|
||||||
│ Delete file: delete_note("path.md") │
|
|
||||||
│ Search: search_notes("query") │
|
|
||||||
│ Vault info: get_vault_info() │
|
|
||||||
├─────────────────────────────────────────────────────────┤
|
|
||||||
│ ✓ Paths are vault-relative (no leading /) │
|
|
||||||
│ ✓ Use forward slashes: folder/file.md │
|
|
||||||
│ ✓ Case-sensitive on macOS/Linux │
|
|
||||||
│ ✓ Include file extensions: .md, .png, etc. │
|
|
||||||
└─────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## 💡 Tips for AI Agents
|
|
||||||
|
|
||||||
If you're an AI agent using this plugin:
|
|
||||||
|
|
||||||
1. **Always use `list_notes()` first** when unsure about paths
|
|
||||||
2. **Read before updating** - Use `read_note()` then `update_note()` for partial changes
|
|
||||||
3. **Verify parent folders** - Use `list_notes()` to check folders exist before creating files
|
|
||||||
4. **Pay attention to error messages** - They include specific troubleshooting steps
|
|
||||||
5. **Use vault-relative paths** - No leading slashes, include file extensions
|
|
||||||
|
|
||||||
## 🙏 Acknowledgments
|
|
||||||
|
|
||||||
Thanks to all testers and users who provided feedback that shaped these improvements!
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
- **Documentation:** See README.md and docs/ folder
|
|
||||||
- **Issues:** Report bugs with version number (1.1.0)
|
|
||||||
- **Questions:** Check TOOL_SELECTION_GUIDE.md first
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Full Changelog:** See [CHANGELOG.md](CHANGELOG.md) for complete details.
|
|
||||||
122
RELEASE_NOTES_v1.2.0.md
Normal file
122
RELEASE_NOTES_v1.2.0.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# Release Notes - Version 1.2.0
|
||||||
|
|
||||||
|
**Release Date:** October 16, 2025
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Version 1.2.0 completes Phase 1.5 of the roadmap, adding enhanced parent folder detection and significantly improved authentication security.
|
||||||
|
|
||||||
|
## What's New
|
||||||
|
|
||||||
|
### 📁 Enhanced Parent Folder Detection
|
||||||
|
|
||||||
|
**New `createParents` Parameter**
|
||||||
|
- Added optional `createParents` parameter to `create_note` tool
|
||||||
|
- Default: `false` (safe behavior - requires parent folders to exist)
|
||||||
|
- When `true`: automatically creates missing parent folders recursively
|
||||||
|
- Handles deeply nested paths (e.g., `a/b/c/d/e/file.md`)
|
||||||
|
|
||||||
|
**Improved Error Handling**
|
||||||
|
- Explicit parent folder detection before file creation (fail-fast)
|
||||||
|
- Clear error messages with `createParents` usage examples
|
||||||
|
- Validates parent is a folder (not a file)
|
||||||
|
- Better troubleshooting guidance
|
||||||
|
|
||||||
|
**Example Usage:**
|
||||||
|
```typescript
|
||||||
|
// Auto-create missing parent folders
|
||||||
|
create_note({
|
||||||
|
path: "projects/2024/reports/Q4.md",
|
||||||
|
content: "# Q4 Report",
|
||||||
|
createParents: true
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔐 Enhanced Authentication & Security
|
||||||
|
|
||||||
|
**Automatic API Key Generation**
|
||||||
|
- API keys are now auto-generated when authentication is enabled
|
||||||
|
- 32-character cryptographically secure keys using `crypto.getRandomValues()`
|
||||||
|
- No more weak user-chosen passwords
|
||||||
|
|
||||||
|
**Improved UI/UX**
|
||||||
|
- Copy to clipboard button for API key
|
||||||
|
- Regenerate key button with instant refresh
|
||||||
|
- Static, selectable API key display (full width)
|
||||||
|
- MCP client configuration snippet generator
|
||||||
|
- Dynamically includes/excludes Authorization header
|
||||||
|
- Correct `mcpServers` format with `serverUrl` field
|
||||||
|
- Copy configuration button
|
||||||
|
- Partially selectable text
|
||||||
|
- Restart warnings when authentication settings change
|
||||||
|
- Selectable connection information URLs
|
||||||
|
|
||||||
|
**Security Fixes**
|
||||||
|
- Fixed critical vulnerability where enabling authentication without API key allowed unrestricted access
|
||||||
|
- Three-layer defense: UI validation, server start validation, and middleware enforcement
|
||||||
|
- Fail-secure design: blocks access when misconfigured
|
||||||
|
- Improved error messages for authentication failures
|
||||||
|
|
||||||
|
**Configuration Example:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"obsidian-mcp": {
|
||||||
|
"serverUrl": "http://127.0.0.1:3000/mcp",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer <your-api-key>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
- `src/utils/auth-utils.ts` - API key generation and validation utilities
|
||||||
|
- `tests/parent-folder-detection.test.ts` - 15 comprehensive test cases
|
||||||
|
- `IMPLEMENTATION_NOTES_AUTH.md` - Authentication implementation documentation
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
- `src/tools/note-tools.ts` - Enhanced `createNote()` with parent folder validation
|
||||||
|
- `src/tools/index.ts` - Updated `create_note` tool schema
|
||||||
|
- `src/server/middleware.ts` - Enhanced authentication middleware
|
||||||
|
- `src/main.ts` - Server start validation
|
||||||
|
- `src/settings.ts` - Complete UI overhaul for authentication
|
||||||
|
- `src/utils/error-messages.ts` - Enhanced parent folder error messages
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- 15 new test cases for parent folder detection
|
||||||
|
- All tests passing
|
||||||
|
- Build successful
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
None. All changes are backward compatible.
|
||||||
|
|
||||||
|
## Upgrade Notes
|
||||||
|
|
||||||
|
1. **Authentication Users:**
|
||||||
|
- If you have authentication enabled, your existing API key will continue to work
|
||||||
|
- You can now regenerate keys easily from the settings UI
|
||||||
|
- Use the new configuration snippet for easy MCP client setup
|
||||||
|
|
||||||
|
2. **create_note Users:**
|
||||||
|
- Existing code continues to work (default: `createParents: false`)
|
||||||
|
- Optionally add `createParents: true` for automatic folder creation
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- ✅ CHANGELOG.md updated
|
||||||
|
- ✅ ROADMAP.md updated (Phase 1.5 marked complete)
|
||||||
|
- ✅ IMPLEMENTATION_NOTES_AUTH.md created
|
||||||
|
- ✅ IMPLEMENTATION_NOTES_v1.5.md (parent folder detection)
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Phase 2 (API Unification & Typed Results) is next on the roadmap.
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
This release includes improvements to security, usability, and robustness based on real-world usage and testing.
|
||||||
174
ROADMAP.md
174
ROADMAP.md
@@ -44,8 +44,8 @@ The plugin is currently minimally functioning with basic CRUD operations and sim
|
|||||||
|----------|----------|------------------|--------|
|
|----------|----------|------------------|--------|
|
||||||
| **P0** | Path Normalization | 1-2 days | ✅ Complete |
|
| **P0** | Path Normalization | 1-2 days | ✅ Complete |
|
||||||
| **P0** | Error Message Improvements | 1 day | ✅ Complete |
|
| **P0** | Error Message Improvements | 1 day | ✅ Complete |
|
||||||
| **P0** | Enhanced Parent Folder Detection | 0.5 days | 📋 Proposed |
|
| **P0** | Enhanced Parent Folder Detection | 0.5 days | ✅ Complete |
|
||||||
| **P0** | Enhanced Authentication | 2-3 days | ⏳ Pending |
|
| **P0** | Enhanced Authentication | 2-3 days | ✅ Complete |
|
||||||
| **P1** | API Unification | 2-3 days | ⏳ Pending |
|
| **P1** | API Unification | 2-3 days | ⏳ Pending |
|
||||||
| **P1** | Typed Results | 1-2 days | ⏳ Pending |
|
| **P1** | Typed Results | 1-2 days | ⏳ Pending |
|
||||||
| **P1** | Discovery Endpoints | 2-3 days | ⏳ Pending |
|
| **P1** | Discovery Endpoints | 2-3 days | ⏳ Pending |
|
||||||
@@ -57,8 +57,8 @@ The plugin is currently minimally functioning with basic CRUD operations and sim
|
|||||||
| **P3** | Waypoint Support | 3-4 days | ⏳ Pending |
|
| **P3** | Waypoint Support | 3-4 days | ⏳ Pending |
|
||||||
|
|
||||||
**Total Estimated Effort:** 29.5-42.5 days
|
**Total Estimated Effort:** 29.5-42.5 days
|
||||||
**Completed:** 2-3 days (Phase 1.1)
|
**Completed:** 2.5-3.5 days (Phase 1.1-1.5)
|
||||||
**Remaining:** 27.5-39.5 days
|
**Remaining:** 27-39 days
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -128,46 +128,46 @@ Troubleshooting tips:
|
|||||||
#### 1.5 Enhanced Parent Folder Detection
|
#### 1.5 Enhanced Parent Folder Detection
|
||||||
|
|
||||||
**Priority:** P0
|
**Priority:** P0
|
||||||
**Status:** Partially Implemented (v1.1.0), Enhancement Proposed
|
**Status:** ✅ Complete
|
||||||
**Estimated Effort:** 0.5 days
|
**Estimated Effort:** 0.5 days
|
||||||
|
|
||||||
**Goal:** Improve parent folder validation in `createNote()` with explicit detection before write operations.
|
**Goal:** Improve parent folder validation in `createNote()` with explicit detection before write operations.
|
||||||
|
|
||||||
**Current Status (v1.1.0):**
|
**Implementation Summary:**
|
||||||
- ✅ Basic parent folder error detection (catches Obsidian's error)
|
- ✅ Explicit parent folder detection before write operations
|
||||||
- ✅ Enhanced error message with troubleshooting tips
|
- ✅ Enhanced error message with `createParents` suggestion
|
||||||
- ✅ `ErrorMessages.parentFolderNotFound()` implemented
|
- ✅ `createParents` parameter with recursive folder creation
|
||||||
- ❌ Detection happens during write (not before)
|
- ✅ Comprehensive test coverage
|
||||||
- ❌ No `createParents` parameter option
|
- ✅ Updated tool schema and documentation
|
||||||
|
|
||||||
**Tasks:**
|
**Tasks:**
|
||||||
|
|
||||||
- [ ] Add explicit parent folder detection in `createNote()`
|
- [x] Add explicit parent folder detection in `createNote()`
|
||||||
- Compute parent path using `PathUtils.getParentPath(path)` before write
|
- Compute parent path using `PathUtils.getParentPath(path)` before write
|
||||||
- Check if parent exists using `PathUtils.folderExists(app, parentPath)`
|
- Check if parent exists using `PathUtils.pathExists(app, parentPath)`
|
||||||
- Check if parent is actually a folder (not a file)
|
- Check if parent is actually a folder (not a file)
|
||||||
- Return clear error before attempting file creation
|
- Return clear error before attempting file creation
|
||||||
|
|
||||||
- [ ] Enhance `ErrorMessages.parentFolderNotFound()`
|
- [x] Enhance `ErrorMessages.parentFolderNotFound()`
|
||||||
- Ensure consistent error message template
|
- Ensure consistent error message template
|
||||||
- Include parent path in error message
|
- Include parent path in error message
|
||||||
- Provide actionable troubleshooting steps
|
- Provide actionable troubleshooting steps
|
||||||
- Suggest using `list_notes()` to verify parent structure
|
- Suggest using `createParents: true` parameter
|
||||||
|
|
||||||
- [ ] Optional: Add `createParents` parameter
|
- [x] Add `createParents` parameter
|
||||||
- Add optional `createParents?: boolean` parameter to `create_note` tool
|
- Add optional `createParents?: boolean` parameter to `create_note` tool
|
||||||
- Default to `false` (no auto-creation)
|
- Default to `false` (no auto-creation)
|
||||||
- If `true`, recursively create parent folders before file creation
|
- If `true`, recursively create parent folders before file creation
|
||||||
- Document behavior clearly in tool description
|
- Document behavior clearly in tool description
|
||||||
- Add tests for both modes
|
- Add tests for both modes
|
||||||
|
|
||||||
- [ ] Update tool schema
|
- [x] Update tool schema
|
||||||
- Add `createParents` parameter to `create_note` inputSchema
|
- Add `createParents` parameter to `create_note` inputSchema
|
||||||
- Document default behavior (no auto-creation)
|
- Document default behavior (no auto-creation)
|
||||||
- Update tool description to mention parent folder requirement
|
- Update tool description to mention parent folder requirement
|
||||||
- Add examples with and without `createParents`
|
- Pass parameter through callTool method
|
||||||
|
|
||||||
- [ ] Testing
|
- [x] Testing
|
||||||
- Test parent folder detection with missing parent
|
- Test parent folder detection with missing parent
|
||||||
- Test parent folder detection when parent is a file
|
- Test parent folder detection when parent is a file
|
||||||
- Test with nested missing parents (a/b/c where b doesn't exist)
|
- Test with nested missing parents (a/b/c where b doesn't exist)
|
||||||
@@ -251,124 +251,60 @@ Troubleshooting tips:
|
|||||||
|
|
||||||
**Priority:** P0
|
**Priority:** P0
|
||||||
**Dependencies:** None
|
**Dependencies:** None
|
||||||
**Estimated Effort:** 2-3 days
|
**Estimated Effort:** 1 day
|
||||||
|
**Status:** ✅ Complete
|
||||||
|
|
||||||
### Goals
|
### Goals
|
||||||
|
|
||||||
Improve bearer token authentication with secure key management, token rotation, and multiple authentication methods.
|
Improve bearer token authentication with automatic secure key generation and enhanced user experience.
|
||||||
|
|
||||||
### Tasks
|
### Completed Tasks
|
||||||
|
|
||||||
#### 1.5.1 Secure API Key Management
|
#### Secure API Key Management (`src/utils/auth-utils.ts`)
|
||||||
|
|
||||||
**File:** `auth-utils.ts` (new)
|
- ✅ Implement secure API key generation (32 characters, cryptographically random)
|
||||||
|
- ✅ Add key validation and strength requirements
|
||||||
|
- ✅ Store keys securely in plugin data
|
||||||
|
|
||||||
- [ ] Implement secure API key generation
|
#### Enhanced Authentication Middleware (`src/server/middleware.ts`)
|
||||||
- [ ] Add key validation and strength requirements
|
|
||||||
- [ ] Support multiple API keys with labels/names
|
|
||||||
- [ ] Add key expiration and rotation
|
|
||||||
- [ ] Store keys securely in plugin data
|
|
||||||
|
|
||||||
**Key Requirements:**
|
- ✅ Improve error messages for authentication failures
|
||||||
- Minimum length: 32 characters
|
- ✅ Add defensive check for misconfigured authentication
|
||||||
- Cryptographically random generation
|
- ✅ Fail-secure design: blocks access when auth enabled but no key set
|
||||||
- Optional expiration dates
|
|
||||||
- Human-readable labels for identification
|
|
||||||
|
|
||||||
#### 1.5.2 Enhanced Authentication Middleware
|
#### API Key Management UI (`src/settings.ts`)
|
||||||
|
|
||||||
**File:** `src/server/middleware.ts` (update)
|
- ✅ Auto-generate API key when authentication is enabled
|
||||||
|
- ✅ Copy to clipboard button for API key
|
||||||
|
- ✅ Regenerate key button with instant refresh
|
||||||
|
- ✅ Static, selectable API key display (full width)
|
||||||
|
- ✅ MCP client configuration snippet generator
|
||||||
|
- ✅ Restart warnings when settings change
|
||||||
|
- ✅ Selectable connection information URLs
|
||||||
|
|
||||||
- [ ] Add request rate limiting per API key
|
#### Server Validation (`src/main.ts`)
|
||||||
- [ ] Implement request logging with authentication context
|
|
||||||
- [ ] Add support for multiple authentication schemes
|
|
||||||
- [ ] Improve error messages for authentication failures
|
|
||||||
- [ ] Add authentication attempt tracking
|
|
||||||
|
|
||||||
**Authentication Schemes:**
|
- ✅ Prevents server start if authentication enabled without API key
|
||||||
- Bearer token (existing, enhanced)
|
- ✅ Clear error messages guiding users to fix configuration
|
||||||
- API key in custom header (e.g., `X-API-Key`)
|
|
||||||
- Query parameter authentication (for testing only)
|
|
||||||
|
|
||||||
#### 1.5.3 API Key Management UI
|
#### Security Improvements
|
||||||
|
|
||||||
**File:** `src/settings.ts` (update)
|
- ✅ Fixed vulnerability where enabling auth without key allowed unrestricted access
|
||||||
|
- ✅ Three-layer defense: UI validation, server start validation, and middleware enforcement
|
||||||
|
- ✅ Cryptographically secure key generation (no weak user-chosen keys)
|
||||||
|
|
||||||
- [ ] Add API key generation button with secure random generation
|
### Benefits
|
||||||
- [ ] Display list of active API keys with labels
|
|
||||||
- [ ] Add key creation/deletion interface
|
|
||||||
- [ ] Show key creation date and last used timestamp
|
|
||||||
- [ ] Add key expiration management
|
|
||||||
- [ ] Implement key visibility toggle (show/hide)
|
|
||||||
- [ ] Add "Copy to clipboard" functionality
|
|
||||||
|
|
||||||
**UI Improvements:**
|
- **Security**: Fixed critical vulnerability, added defense in depth
|
||||||
```typescript
|
- **Usability**: Auto-generation, one-click copy, clear configuration
|
||||||
// Settings panel additions
|
- **Developer Experience**: Ready-to-use MCP client configuration snippets
|
||||||
- "Generate New API Key" button
|
- **Maintainability**: Clean code structure, reusable utilities
|
||||||
- Key list with:
|
|
||||||
- Label/name
|
|
||||||
- Created date
|
|
||||||
- Last used timestamp
|
|
||||||
- Expiration date (if set)
|
|
||||||
- Revoke button
|
|
||||||
- Key strength indicator
|
|
||||||
- Security best practices notice
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 1.5.4 Authentication Audit Log
|
### Documentation
|
||||||
|
|
||||||
**File:** `auth-log.ts` (new)
|
- ✅ `IMPLEMENTATION_NOTES_AUTH.md` - Complete implementation documentation
|
||||||
|
- ✅ `CHANGELOG.md` - Updated with all changes
|
||||||
- [ ] Log authentication attempts (success/failure)
|
- ✅ `ROADMAP.md` - Marked as complete
|
||||||
- [ ] Track API key usage statistics
|
|
||||||
- [ ] Add configurable log retention
|
|
||||||
- [ ] Provide audit log export
|
|
||||||
- [ ] Display recent authentication activity in settings
|
|
||||||
|
|
||||||
**Log Format:**
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
timestamp: number,
|
|
||||||
keyLabel: string,
|
|
||||||
success: boolean,
|
|
||||||
ipAddress: string,
|
|
||||||
endpoint: string,
|
|
||||||
errorReason?: string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 1.5.5 Security Enhancements
|
|
||||||
|
|
||||||
- [ ] Add HTTPS requirement option (reject HTTP in production)
|
|
||||||
- [ ] Implement request signing for additional security
|
|
||||||
- [ ] Add IP allowlist/blocklist option
|
|
||||||
- [ ] Support for read-only API keys (restrict to read operations)
|
|
||||||
- [ ] Add permission scopes per API key
|
|
||||||
|
|
||||||
**Permission Scopes:**
|
|
||||||
- `read` - Read operations only
|
|
||||||
- `write` - Create, update, delete operations
|
|
||||||
- `admin` - Server configuration access
|
|
||||||
- `all` - Full access (default)
|
|
||||||
|
|
||||||
#### 1.5.6 Documentation Updates
|
|
||||||
|
|
||||||
- [ ] Document API key generation best practices
|
|
||||||
- [ ] Add authentication examples for different clients
|
|
||||||
- [ ] Document security considerations
|
|
||||||
- [ ] Add troubleshooting guide for auth issues
|
|
||||||
- [ ] Document permission scopes and their usage
|
|
||||||
|
|
||||||
#### 1.5.7 Testing
|
|
||||||
|
|
||||||
- [ ] Test API key generation and validation
|
|
||||||
- [ ] Test multiple API keys with different scopes
|
|
||||||
- [ ] Test key expiration and rotation
|
|
||||||
- [ ] Test rate limiting per key
|
|
||||||
- [ ] Test authentication failure scenarios
|
|
||||||
- [ ] Test audit logging
|
|
||||||
- [ ] Security audit of authentication implementation
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-mcp-server",
|
"id": "obsidian-mcp-server",
|
||||||
"name": "MCP Server",
|
"name": "MCP Server",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"minAppVersion": "0.15.0",
|
"minAppVersion": "0.15.0",
|
||||||
"description": "Exposes Obsidian vault operations via Model Context Protocol (MCP) over HTTP",
|
"description": "Exposes Obsidian vault operations via Model Context Protocol (MCP) over HTTP",
|
||||||
"isDesktopOnly": true
|
"isDesktopOnly": true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-mcp-server",
|
"name": "obsidian-mcp-server",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"description": "MCP (Model Context Protocol) server plugin for Obsidian - exposes vault operations via HTTP",
|
"description": "MCP (Model Context Protocol) server plugin for Obsidian - exposes vault operations via HTTP",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ export default class MCPServerPlugin extends Plugin {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate authentication configuration
|
||||||
|
if (this.settings.enableAuth && (!this.settings.apiKey || this.settings.apiKey.trim() === '')) {
|
||||||
|
new Notice('⚠️ Cannot start server: Authentication is enabled but no API key is set. Please set an API key in settings or disable authentication.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.mcpServer = new MCPServer(this.app, this.settings);
|
this.mcpServer = new MCPServer(this.app, this.settings);
|
||||||
await this.mcpServer.start();
|
await this.mcpServer.start();
|
||||||
|
|||||||
@@ -28,8 +28,13 @@ export function setupMiddleware(app: Express, settings: MCPServerSettings, creat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Authentication middleware
|
// Authentication middleware
|
||||||
if (settings.enableAuth && settings.apiKey) {
|
if (settings.enableAuth) {
|
||||||
app.use((req: Request, res: Response, next: any) => {
|
app.use((req: Request, res: Response, next: any) => {
|
||||||
|
// Defensive check: if auth is enabled but no API key is set, reject all requests
|
||||||
|
if (!settings.apiKey || settings.apiKey.trim() === '') {
|
||||||
|
return res.status(500).json(createErrorResponse(null, ErrorCodes.InternalError, 'Server misconfigured: Authentication enabled but no API key set'));
|
||||||
|
}
|
||||||
|
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
const apiKey = authHeader?.replace('Bearer ', '');
|
const apiKey = authHeader?.replace('Bearer ', '');
|
||||||
|
|
||||||
|
|||||||
150
src/settings.ts
150
src/settings.ts
@@ -1,6 +1,7 @@
|
|||||||
import { App, PluginSettingTab, Setting } from 'obsidian';
|
import { App, Notice, PluginSettingTab, Setting } from 'obsidian';
|
||||||
import { MCPPluginSettings } from './types/settings-types';
|
import { MCPPluginSettings } from './types/settings-types';
|
||||||
import MCPServerPlugin from './main';
|
import MCPServerPlugin from './main';
|
||||||
|
import { generateApiKey } from './utils/auth-utils';
|
||||||
|
|
||||||
export class MCPServerSettingTab extends PluginSettingTab {
|
export class MCPServerSettingTab extends PluginSettingTab {
|
||||||
plugin: MCPServerPlugin;
|
plugin: MCPServerPlugin;
|
||||||
@@ -50,24 +51,30 @@ export class MCPServerSettingTab extends PluginSettingTab {
|
|||||||
if (!isNaN(port) && port > 0 && port < 65536) {
|
if (!isNaN(port) && port > 0 && port < 65536) {
|
||||||
this.plugin.settings.port = port;
|
this.plugin.settings.port = port;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
|
if (this.plugin.mcpServer?.isRunning()) {
|
||||||
|
new Notice('⚠️ Server restart required for port changes to take effect');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// CORS setting
|
// CORS setting
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName('Enable CORS')
|
.setName('Enable CORS')
|
||||||
.setDesc('Enable Cross-Origin Resource Sharing')
|
.setDesc('Enable Cross-Origin Resource Sharing (requires restart)')
|
||||||
.addToggle(toggle => toggle
|
.addToggle(toggle => toggle
|
||||||
.setValue(this.plugin.settings.enableCORS)
|
.setValue(this.plugin.settings.enableCORS)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.enableCORS = value;
|
this.plugin.settings.enableCORS = value;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
|
if (this.plugin.mcpServer?.isRunning()) {
|
||||||
|
new Notice('⚠️ Server restart required for CORS changes to take effect');
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Allowed origins
|
// Allowed origins
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName('Allowed origins')
|
.setName('Allowed origins')
|
||||||
.setDesc('Comma-separated list of allowed origins (* for all)')
|
.setDesc('Comma-separated list of allowed origins (* for all, requires restart)')
|
||||||
.addText(text => text
|
.addText(text => text
|
||||||
.setPlaceholder('*')
|
.setPlaceholder('*')
|
||||||
.setValue(this.plugin.settings.allowedOrigins.join(', '))
|
.setValue(this.plugin.settings.allowedOrigins.join(', '))
|
||||||
@@ -77,30 +84,135 @@ export class MCPServerSettingTab extends PluginSettingTab {
|
|||||||
.map(s => s.trim())
|
.map(s => s.trim())
|
||||||
.filter(s => s.length > 0);
|
.filter(s => s.length > 0);
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
|
if (this.plugin.mcpServer?.isRunning()) {
|
||||||
|
new Notice('⚠️ Server restart required for origin changes to take effect');
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName('Enable authentication')
|
.setName('Enable authentication')
|
||||||
.setDesc('Require API key for requests')
|
.setDesc('Require API key for requests (requires restart)')
|
||||||
.addToggle(toggle => toggle
|
.addToggle(toggle => toggle
|
||||||
.setValue(this.plugin.settings.enableAuth)
|
.setValue(this.plugin.settings.enableAuth)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
this.plugin.settings.enableAuth = value;
|
this.plugin.settings.enableAuth = value;
|
||||||
|
|
||||||
|
// Auto-generate API key when enabling authentication
|
||||||
|
if (value && (!this.plugin.settings.apiKey || this.plugin.settings.apiKey.trim() === '')) {
|
||||||
|
this.plugin.settings.apiKey = generateApiKey();
|
||||||
|
new Notice('✅ API key generated automatically');
|
||||||
|
}
|
||||||
|
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
|
if (this.plugin.mcpServer?.isRunning()) {
|
||||||
|
new Notice('⚠️ Server restart required for authentication changes to take effect');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the display to show the new key
|
||||||
|
this.display();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// API Key
|
// API Key Display (only show if authentication is enabled)
|
||||||
new Setting(containerEl)
|
if (this.plugin.settings.enableAuth) {
|
||||||
.setName('API Key')
|
new Setting(containerEl)
|
||||||
.setDesc('API key for authentication (Bearer token)')
|
.setName('API Key Management')
|
||||||
.addText(text => text
|
.setDesc('Use this key in the Authorization header as Bearer token');
|
||||||
.setPlaceholder('Enter API key')
|
|
||||||
.setValue(this.plugin.settings.apiKey || '')
|
// Create a full-width container for buttons and key display
|
||||||
.onChange(async (value) => {
|
const apiKeyContainer = containerEl.createDiv({cls: 'mcp-api-key-section'});
|
||||||
this.plugin.settings.apiKey = value;
|
apiKeyContainer.style.marginBottom = '20px';
|
||||||
await this.plugin.saveSettings();
|
apiKeyContainer.style.marginLeft = '0';
|
||||||
}));
|
|
||||||
|
// Create button container
|
||||||
|
const buttonContainer = apiKeyContainer.createDiv({cls: 'mcp-api-key-buttons'});
|
||||||
|
buttonContainer.style.display = 'flex';
|
||||||
|
buttonContainer.style.gap = '8px';
|
||||||
|
buttonContainer.style.marginBottom = '12px';
|
||||||
|
|
||||||
|
// Copy button
|
||||||
|
const copyButton = buttonContainer.createEl('button', {text: '📋 Copy Key'});
|
||||||
|
copyButton.addEventListener('click', async () => {
|
||||||
|
await navigator.clipboard.writeText(this.plugin.settings.apiKey || '');
|
||||||
|
new Notice('✅ API key copied to clipboard');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Regenerate button
|
||||||
|
const regenButton = buttonContainer.createEl('button', {text: '🔄 Regenerate Key'});
|
||||||
|
regenButton.addEventListener('click', async () => {
|
||||||
|
this.plugin.settings.apiKey = generateApiKey();
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
new Notice('✅ New API key generated');
|
||||||
|
if (this.plugin.mcpServer?.isRunning()) {
|
||||||
|
new Notice('⚠️ Server restart required for API key changes to take effect');
|
||||||
|
}
|
||||||
|
this.display();
|
||||||
|
});
|
||||||
|
|
||||||
|
// API Key display (static, copyable text)
|
||||||
|
const keyDisplayContainer = apiKeyContainer.createDiv({cls: 'mcp-api-key-display'});
|
||||||
|
keyDisplayContainer.style.padding = '12px';
|
||||||
|
keyDisplayContainer.style.backgroundColor = 'var(--background-secondary)';
|
||||||
|
keyDisplayContainer.style.borderRadius = '4px';
|
||||||
|
keyDisplayContainer.style.fontFamily = 'monospace';
|
||||||
|
keyDisplayContainer.style.fontSize = '0.9em';
|
||||||
|
keyDisplayContainer.style.wordBreak = 'break-all';
|
||||||
|
keyDisplayContainer.style.userSelect = 'all';
|
||||||
|
keyDisplayContainer.style.cursor = 'text';
|
||||||
|
keyDisplayContainer.style.marginBottom = '16px';
|
||||||
|
keyDisplayContainer.textContent = this.plugin.settings.apiKey || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCP Client Configuration (show always, regardless of auth)
|
||||||
|
containerEl.createEl('h3', {text: 'MCP Client Configuration'});
|
||||||
|
|
||||||
|
const configContainer = containerEl.createDiv({cls: 'mcp-config-snippet'});
|
||||||
|
configContainer.style.marginBottom = '20px';
|
||||||
|
|
||||||
|
const configDesc = configContainer.createEl('p', {
|
||||||
|
text: 'Add this configuration to your MCP client (e.g., Claude Desktop, Cline):'
|
||||||
|
});
|
||||||
|
configDesc.style.marginBottom = '8px';
|
||||||
|
configDesc.style.fontSize = '0.9em';
|
||||||
|
configDesc.style.color = 'var(--text-muted)';
|
||||||
|
|
||||||
|
// Generate JSON config based on auth settings
|
||||||
|
const mcpConfig: any = {
|
||||||
|
"mcpServers": {
|
||||||
|
"obsidian-mcp": {
|
||||||
|
"serverUrl": `http://127.0.0.1:${this.plugin.settings.port}/mcp`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only add headers if authentication is enabled
|
||||||
|
if (this.plugin.settings.enableAuth && this.plugin.settings.apiKey) {
|
||||||
|
mcpConfig.mcpServers["obsidian-mcp"].headers = {
|
||||||
|
"Authorization": `Bearer ${this.plugin.settings.apiKey}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config display with copy button
|
||||||
|
const configButtonContainer = configContainer.createDiv();
|
||||||
|
configButtonContainer.style.display = 'flex';
|
||||||
|
configButtonContainer.style.gap = '8px';
|
||||||
|
configButtonContainer.style.marginBottom = '8px';
|
||||||
|
|
||||||
|
const copyConfigButton = configButtonContainer.createEl('button', {text: '📋 Copy Configuration'});
|
||||||
|
copyConfigButton.addEventListener('click', async () => {
|
||||||
|
await navigator.clipboard.writeText(JSON.stringify(mcpConfig, null, 2));
|
||||||
|
new Notice('✅ Configuration copied to clipboard');
|
||||||
|
});
|
||||||
|
|
||||||
|
const configDisplay = configContainer.createEl('pre');
|
||||||
|
configDisplay.style.padding = '12px';
|
||||||
|
configDisplay.style.backgroundColor = 'var(--background-secondary)';
|
||||||
|
configDisplay.style.borderRadius = '4px';
|
||||||
|
configDisplay.style.fontSize = '0.85em';
|
||||||
|
configDisplay.style.overflowX = 'auto';
|
||||||
|
configDisplay.style.userSelect = 'text';
|
||||||
|
configDisplay.style.cursor = 'text';
|
||||||
|
configDisplay.textContent = JSON.stringify(mcpConfig, null, 2);
|
||||||
|
|
||||||
// Server status
|
// Server status
|
||||||
containerEl.createEl('h3', {text: 'Server Status'});
|
containerEl.createEl('h3', {text: 'Server Status'});
|
||||||
@@ -144,10 +256,14 @@ export class MCPServerSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
const infoEl = containerEl.createEl('div', {cls: 'mcp-connection-info'});
|
const infoEl = containerEl.createEl('div', {cls: 'mcp-connection-info'});
|
||||||
infoEl.createEl('p', {text: 'MCP Endpoint:'});
|
infoEl.createEl('p', {text: 'MCP Endpoint:'});
|
||||||
infoEl.createEl('code', {text: `http://127.0.0.1:${this.plugin.settings.port}/mcp`});
|
const mcpEndpoint = infoEl.createEl('code', {text: `http://127.0.0.1:${this.plugin.settings.port}/mcp`});
|
||||||
|
mcpEndpoint.style.userSelect = 'all';
|
||||||
|
mcpEndpoint.style.cursor = 'text';
|
||||||
|
|
||||||
infoEl.createEl('p', {text: 'Health Check:'});
|
infoEl.createEl('p', {text: 'Health Check:'});
|
||||||
infoEl.createEl('code', {text: `http://127.0.0.1:${this.plugin.settings.port}/health`});
|
const healthEndpoint = infoEl.createEl('code', {text: `http://127.0.0.1:${this.plugin.settings.port}/health`});
|
||||||
|
healthEndpoint.style.userSelect = 'all';
|
||||||
|
healthEndpoint.style.cursor = 'text';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,17 +30,21 @@ export class ToolRegistry {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "create_note",
|
name: "create_note",
|
||||||
description: "Create a new file in the Obsidian vault. Use this to create a new note or file. The parent folder must already exist - this will NOT auto-create folders. Path must be vault-relative with file extension. Will fail if the file already exists. Use list_notes() to verify the parent folder exists before creating.",
|
description: "Create a new file in the Obsidian vault. Use this to create a new note or file. By default, parent folders must already exist. Set createParents to true to automatically create missing parent folders. Path must be vault-relative with file extension. Will fail if the file already exists. Use list_notes() to verify the parent folder exists before creating.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
path: {
|
path: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "Vault-relative path for the new file (e.g., 'folder/note.md' or 'projects/2024/report.md'). Must include file extension. Parent folders must exist. Paths are case-sensitive on macOS/Linux. Do not use leading or trailing slashes."
|
description: "Vault-relative path for the new file (e.g., 'folder/note.md' or 'projects/2024/report.md'). Must include file extension. Paths are case-sensitive on macOS/Linux. Do not use leading or trailing slashes."
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description: "The complete content to write to the new file. Can include markdown formatting, frontmatter, etc."
|
description: "The complete content to write to the new file. Can include markdown formatting, frontmatter, etc."
|
||||||
|
},
|
||||||
|
createParents: {
|
||||||
|
type: "boolean",
|
||||||
|
description: "If true, automatically create missing parent folders. If false (default), returns an error if parent folders don't exist. Default: false"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
required: ["path", "content"]
|
required: ["path", "content"]
|
||||||
@@ -122,7 +126,7 @@ export class ToolRegistry {
|
|||||||
case "read_note":
|
case "read_note":
|
||||||
return await this.noteTools.readNote(args.path);
|
return await this.noteTools.readNote(args.path);
|
||||||
case "create_note":
|
case "create_note":
|
||||||
return await this.noteTools.createNote(args.path, args.content);
|
return await this.noteTools.createNote(args.path, args.content, args.createParents ?? false);
|
||||||
case "update_note":
|
case "update_note":
|
||||||
return await this.noteTools.updateNote(args.path, args.content);
|
return await this.noteTools.updateNote(args.path, args.content);
|
||||||
case "delete_note":
|
case "delete_note":
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class NoteTools {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createNote(path: string, content: string): Promise<CallToolResult> {
|
async createNote(path: string, content: string, createParents: boolean = false): Promise<CallToolResult> {
|
||||||
// Validate path
|
// Validate path
|
||||||
if (!path || path.trim() === '') {
|
if (!path || path.trim() === '') {
|
||||||
return {
|
return {
|
||||||
@@ -88,30 +88,72 @@ export class NoteTools {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Explicit parent folder detection (before write operation)
|
||||||
|
const parentPath = PathUtils.getParentPath(normalizedPath);
|
||||||
|
if (parentPath) {
|
||||||
|
// Check if parent exists
|
||||||
|
if (!PathUtils.pathExists(this.app, parentPath)) {
|
||||||
|
if (createParents) {
|
||||||
|
// Auto-create parent folders recursively
|
||||||
|
try {
|
||||||
|
await this.createParentFolders(parentPath);
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: ErrorMessages.operationFailed('create parent folders', parentPath, (error as Error).message) }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Return clear error before attempting file creation
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: ErrorMessages.parentFolderNotFound(normalizedPath, parentPath) }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if parent is actually a folder (not a file)
|
||||||
|
if (PathUtils.fileExists(this.app, parentPath)) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: ErrorMessages.notAFolder(parentPath) }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed with file creation
|
||||||
try {
|
try {
|
||||||
const file = await this.app.vault.create(normalizedPath, content);
|
const file = await this.app.vault.create(normalizedPath, content);
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: `Note created successfully: ${file.path}` }]
|
content: [{ type: "text", text: `Note created successfully: ${file.path}` }]
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = (error as Error).message;
|
|
||||||
|
|
||||||
// Check for parent folder not found error
|
|
||||||
if (errorMsg.includes('parent folder')) {
|
|
||||||
const parentPath = PathUtils.getParentPath(normalizedPath);
|
|
||||||
return {
|
|
||||||
content: [{ type: "text", text: ErrorMessages.parentFolderNotFound(normalizedPath, parentPath) }],
|
|
||||||
isError: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: ErrorMessages.operationFailed('create note', normalizedPath, errorMsg) }],
|
content: [{ type: "text", text: ErrorMessages.operationFailed('create note', normalizedPath, (error as Error).message) }],
|
||||||
isError: true
|
isError: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively create parent folders
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async createParentFolders(path: string): Promise<void> {
|
||||||
|
// Get parent path
|
||||||
|
const parentPath = PathUtils.getParentPath(path);
|
||||||
|
|
||||||
|
// If there's a parent and it doesn't exist, create it first (recursion)
|
||||||
|
if (parentPath && !PathUtils.pathExists(this.app, parentPath)) {
|
||||||
|
await this.createParentFolders(parentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the current folder if it doesn't exist
|
||||||
|
if (!PathUtils.pathExists(this.app, path)) {
|
||||||
|
await this.app.vault.createFolder(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async updateNote(path: string, content: string): Promise<CallToolResult> {
|
async updateNote(path: string, content: string): Promise<CallToolResult> {
|
||||||
// Validate path
|
// Validate path
|
||||||
if (!path || path.trim() === '') {
|
if (!path || path.trim() === '') {
|
||||||
|
|||||||
40
src/utils/auth-utils.ts
Normal file
40
src/utils/auth-utils.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions for authentication and API key management
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a cryptographically secure random API key
|
||||||
|
* @param length Length of the API key (default: 32 characters)
|
||||||
|
* @returns A random API key string
|
||||||
|
*/
|
||||||
|
export function generateApiKey(length: number = 32): string {
|
||||||
|
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
||||||
|
const values = new Uint8Array(length);
|
||||||
|
|
||||||
|
// Use crypto.getRandomValues for cryptographically secure random numbers
|
||||||
|
crypto.getRandomValues(values);
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += charset[values[i] % charset.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates API key strength
|
||||||
|
* @param apiKey The API key to validate
|
||||||
|
* @returns Object with isValid flag and optional error message
|
||||||
|
*/
|
||||||
|
export function validateApiKey(apiKey: string): { isValid: boolean; error?: string } {
|
||||||
|
if (!apiKey || apiKey.trim() === '') {
|
||||||
|
return { isValid: false, error: 'API key cannot be empty' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.length < 16) {
|
||||||
|
return { isValid: false, error: 'API key must be at least 16 characters long' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isValid: true };
|
||||||
|
}
|
||||||
@@ -86,16 +86,22 @@ Troubleshooting tips:
|
|||||||
* Generate a parent folder not found error message
|
* Generate a parent folder not found error message
|
||||||
*/
|
*/
|
||||||
static parentFolderNotFound(path: string, parentPath: string): string {
|
static parentFolderNotFound(path: string, parentPath: string): string {
|
||||||
|
const grandparentPath = PathUtils.getParentPath(parentPath);
|
||||||
|
const listCommand = grandparentPath ? `list_notes("${grandparentPath}")` : 'list_notes()';
|
||||||
|
|
||||||
return `Parent folder does not exist: "${parentPath}"
|
return `Parent folder does not exist: "${parentPath}"
|
||||||
|
|
||||||
Cannot create "${path}" because its parent folder is missing.
|
Cannot create "${path}" because its parent folder is missing.
|
||||||
|
|
||||||
Troubleshooting tips:
|
Troubleshooting tips:
|
||||||
|
• Use createParents: true parameter to automatically create missing parent folders
|
||||||
• Create the parent folder first using Obsidian
|
• Create the parent folder first using Obsidian
|
||||||
• Verify the folder path with list_notes("${PathUtils.getParentPath(parentPath) || '/'}")
|
• Verify the folder path with ${listCommand}
|
||||||
• Check that the parent folder path is correct (vault-relative, case-sensitive on macOS/Linux)
|
• Check that the parent folder path is correct (vault-relative, case-sensitive on macOS/Linux)
|
||||||
• Note: Automatic parent folder creation is not currently enabled
|
• Ensure all parent folders in the path exist before creating the file
|
||||||
• Ensure all parent folders in the path exist before creating the file`;
|
|
||||||
|
Example with auto-creation:
|
||||||
|
create_note({ path: "${path}", content: "...", createParents: true })`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
295
tests/parent-folder-detection.test.ts
Normal file
295
tests/parent-folder-detection.test.ts
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
import { App, TFile, TFolder, Vault } from 'obsidian';
|
||||||
|
import { NoteTools } from '../src/tools/note-tools';
|
||||||
|
import { PathUtils } from '../src/utils/path-utils';
|
||||||
|
|
||||||
|
// Mock Obsidian API
|
||||||
|
jest.mock('obsidian');
|
||||||
|
|
||||||
|
describe('Enhanced Parent Folder Detection', () => {
|
||||||
|
let app: jest.Mocked<App>;
|
||||||
|
let vault: jest.Mocked<Vault>;
|
||||||
|
let noteTools: NoteTools;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create mock vault
|
||||||
|
vault = {
|
||||||
|
getAbstractFileByPath: jest.fn(),
|
||||||
|
create: jest.fn(),
|
||||||
|
createFolder: jest.fn(),
|
||||||
|
read: jest.fn(),
|
||||||
|
modify: jest.fn(),
|
||||||
|
delete: jest.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
// Create mock app
|
||||||
|
app = {
|
||||||
|
vault,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
noteTools = new NoteTools(app);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Explicit parent folder detection', () => {
|
||||||
|
test('should detect missing parent folder before write operation', async () => {
|
||||||
|
// Setup: parent folder doesn't exist
|
||||||
|
vault.getAbstractFileByPath.mockReturnValue(null);
|
||||||
|
|
||||||
|
const result = await noteTools.createNote('missing-parent/file.md', 'content', false);
|
||||||
|
|
||||||
|
expect(result.isError).toBe(true);
|
||||||
|
expect(result.content[0].text).toContain('Parent folder does not exist');
|
||||||
|
expect(result.content[0].text).toContain('missing-parent');
|
||||||
|
expect(vault.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should detect when parent path is a file, not a folder', async () => {
|
||||||
|
const mockFile = { path: 'parent.md' } as TFile;
|
||||||
|
|
||||||
|
// Setup: parent path exists but is a file
|
||||||
|
vault.getAbstractFileByPath.mockImplementation((path: string) => {
|
||||||
|
if (path === 'parent.md') return mockFile;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await noteTools.createNote('parent.md/file.md', 'content', false);
|
||||||
|
|
||||||
|
expect(result.isError).toBe(true);
|
||||||
|
expect(result.content[0].text).toContain('Path is not a folder');
|
||||||
|
expect(result.content[0].text).toContain('parent.md');
|
||||||
|
expect(vault.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should succeed when parent folder exists', async () => {
|
||||||
|
const mockFolder = { path: 'existing-folder' } as TFolder;
|
||||||
|
const mockFile = { path: 'existing-folder/file.md' } as TFile;
|
||||||
|
|
||||||
|
// Setup: parent folder exists
|
||||||
|
vault.getAbstractFileByPath.mockImplementation((path: string) => {
|
||||||
|
if (path === 'existing-folder') return mockFolder;
|
||||||
|
if (path === 'existing-folder/file.md') return null; // file doesn't exist yet
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
vault.create.mockResolvedValue(mockFile);
|
||||||
|
|
||||||
|
const result = await noteTools.createNote('existing-folder/file.md', 'content', false);
|
||||||
|
|
||||||
|
expect(result.isError).toBeUndefined();
|
||||||
|
expect(result.content[0].text).toContain('Note created successfully');
|
||||||
|
expect(vault.create).toHaveBeenCalledWith('existing-folder/file.md', 'content');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle nested missing parents (a/b/c where b does not exist)', async () => {
|
||||||
|
const mockFolderA = { path: 'a' } as TFolder;
|
||||||
|
|
||||||
|
// Setup: only 'a' exists, 'a/b' does not exist
|
||||||
|
vault.getAbstractFileByPath.mockImplementation((path: string) => {
|
||||||
|
if (path === 'a') return mockFolderA;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await noteTools.createNote('a/b/c/file.md', 'content', false);
|
||||||
|
|
||||||
|
expect(result.isError).toBe(true);
|
||||||
|
expect(result.content[0].text).toContain('Parent folder does not exist');
|
||||||
|
expect(result.content[0].text).toContain('a/b/c');
|
||||||
|
expect(vault.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createParents parameter', () => {
|
||||||
|
test('should create single missing parent folder when createParents is true', async () => {
|
||||||
|
const mockFolder = { path: 'new-folder' } as TFolder;
|
||||||
|
const mockFile = { path: 'new-folder/file.md' } as TFile;
|
||||||
|
|
||||||
|
// Setup: parent doesn't exist initially
|
||||||
|
let folderCreated = false;
|
||||||
|
vault.getAbstractFileByPath.mockImplementation((path: string) => {
|
||||||
|
if (path === 'new-folder' && folderCreated) return mockFolder;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
vault.createFolder.mockImplementation(async (path: string) => {
|
||||||
|
folderCreated = true;
|
||||||
|
return mockFolder;
|
||||||
|
});
|
||||||
|
|
||||||
|
vault.create.mockResolvedValue(mockFile);
|
||||||
|
|
||||||
|
const result = await noteTools.createNote('new-folder/file.md', 'content', true);
|
||||||
|
|
||||||
|
expect(result.isError).toBeUndefined();
|
||||||
|
expect(vault.createFolder).toHaveBeenCalledWith('new-folder');
|
||||||
|
expect(vault.create).toHaveBeenCalledWith('new-folder/file.md', 'content');
|
||||||
|
expect(result.content[0].text).toContain('Note created successfully');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should recursively create all missing parent folders', async () => {
|
||||||
|
const createdFolders = new Set<string>();
|
||||||
|
const mockFile = { path: 'a/b/c/file.md' } as TFile;
|
||||||
|
|
||||||
|
// Setup: no folders exist initially
|
||||||
|
vault.getAbstractFileByPath.mockImplementation((path: string) => {
|
||||||
|
if (createdFolders.has(path)) {
|
||||||
|
return { path } as TFolder;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
vault.createFolder.mockImplementation(async (path: string) => {
|
||||||
|
createdFolders.add(path);
|
||||||
|
return { path } as TFolder;
|
||||||
|
});
|
||||||
|
|
||||||
|
vault.create.mockResolvedValue(mockFile);
|
||||||
|
|
||||||
|
const result = await noteTools.createNote('a/b/c/file.md', 'content', true);
|
||||||
|
|
||||||
|
expect(result.isError).toBeUndefined();
|
||||||
|
expect(vault.createFolder).toHaveBeenCalledTimes(3);
|
||||||
|
expect(vault.createFolder).toHaveBeenCalledWith('a');
|
||||||
|
expect(vault.createFolder).toHaveBeenCalledWith('a/b');
|
||||||
|
expect(vault.createFolder).toHaveBeenCalledWith('a/b/c');
|
||||||
|
expect(vault.create).toHaveBeenCalledWith('a/b/c/file.md', 'content');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not create folders when createParents is false (default)', async () => {
|
||||||
|
// Setup: parent doesn't exist
|
||||||
|
vault.getAbstractFileByPath.mockReturnValue(null);
|
||||||
|
|
||||||
|
const result = await noteTools.createNote('missing/file.md', 'content', false);
|
||||||
|
|
||||||
|
expect(result.isError).toBe(true);
|
||||||
|
expect(vault.createFolder).not.toHaveBeenCalled();
|
||||||
|
expect(vault.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle createFolder errors gracefully', async () => {
|
||||||
|
// Setup: parent doesn't exist
|
||||||
|
vault.getAbstractFileByPath.mockReturnValue(null);
|
||||||
|
vault.createFolder.mockRejectedValue(new Error('Permission denied'));
|
||||||
|
|
||||||
|
const result = await noteTools.createNote('new-folder/file.md', 'content', true);
|
||||||
|
|
||||||
|
expect(result.isError).toBe(true);
|
||||||
|
expect(result.content[0].text).toContain('Failed to create parent folders');
|
||||||
|
expect(result.content[0].text).toContain('Permission denied');
|
||||||
|
expect(vault.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should skip creating folders that already exist', async () => {
|
||||||
|
const mockFolderA = { path: 'a' } as TFolder;
|
||||||
|
const mockFile = { path: 'a/b/file.md' } as TFile;
|
||||||
|
let folderBCreated = false;
|
||||||
|
|
||||||
|
// Setup: 'a' exists, 'a/b' does not
|
||||||
|
vault.getAbstractFileByPath.mockImplementation((path: string) => {
|
||||||
|
if (path === 'a') return mockFolderA;
|
||||||
|
if (path === 'a/b' && folderBCreated) return { path: 'a/b' } as TFolder;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
vault.createFolder.mockImplementation(async (path: string) => {
|
||||||
|
if (path === 'a/b') {
|
||||||
|
folderBCreated = true;
|
||||||
|
return { path: 'a/b' } as TFolder;
|
||||||
|
}
|
||||||
|
return null as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
vault.create.mockResolvedValue(mockFile);
|
||||||
|
|
||||||
|
const result = await noteTools.createNote('a/b/file.md', 'content', true);
|
||||||
|
|
||||||
|
expect(result.isError).toBeUndefined();
|
||||||
|
// Should only create 'a/b', not 'a' (which already exists)
|
||||||
|
expect(vault.createFolder).toHaveBeenCalledTimes(1);
|
||||||
|
expect(vault.createFolder).toHaveBeenCalledWith('a/b');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error message clarity', () => {
|
||||||
|
test('should provide helpful error message with createParents suggestion', async () => {
|
||||||
|
vault.getAbstractFileByPath.mockReturnValue(null);
|
||||||
|
|
||||||
|
const result = await noteTools.createNote('folder/subfolder/file.md', 'content', false);
|
||||||
|
|
||||||
|
expect(result.isError).toBe(true);
|
||||||
|
expect(result.content[0].text).toContain('Parent folder does not exist');
|
||||||
|
expect(result.content[0].text).toContain('folder/subfolder');
|
||||||
|
expect(result.content[0].text).toContain('createParents: true');
|
||||||
|
expect(result.content[0].text).toContain('Troubleshooting tips');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should provide clear error when parent is a file', async () => {
|
||||||
|
const mockFile = { path: 'file.md' } as TFile;
|
||||||
|
|
||||||
|
vault.getAbstractFileByPath.mockImplementation((path: string) => {
|
||||||
|
if (path === 'file.md') return mockFile;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await noteTools.createNote('file.md/nested.md', 'content', false);
|
||||||
|
|
||||||
|
expect(result.isError).toBe(true);
|
||||||
|
expect(result.content[0].text).toContain('Path is not a folder');
|
||||||
|
expect(result.content[0].text).toContain('file.md');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Edge cases', () => {
|
||||||
|
test('should handle file in root directory (no parent path)', async () => {
|
||||||
|
const mockFile = { path: 'file.md' } as TFile;
|
||||||
|
|
||||||
|
vault.getAbstractFileByPath.mockReturnValue(null);
|
||||||
|
vault.create.mockResolvedValue(mockFile);
|
||||||
|
|
||||||
|
const result = await noteTools.createNote('file.md', 'content', false);
|
||||||
|
|
||||||
|
expect(result.isError).toBeUndefined();
|
||||||
|
expect(vault.create).toHaveBeenCalledWith('file.md', 'content');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should normalize paths before checking parent', async () => {
|
||||||
|
const mockFolder = { path: 'folder' } as TFolder;
|
||||||
|
const mockFile = { path: 'folder/file.md' } as TFile;
|
||||||
|
|
||||||
|
vault.getAbstractFileByPath.mockImplementation((path: string) => {
|
||||||
|
if (path === 'folder') return mockFolder;
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
vault.create.mockResolvedValue(mockFile);
|
||||||
|
|
||||||
|
// Test with various path formats
|
||||||
|
const result = await noteTools.createNote('folder//file.md', 'content', false);
|
||||||
|
|
||||||
|
expect(result.isError).toBeUndefined();
|
||||||
|
expect(vault.create).toHaveBeenCalledWith('folder/file.md', 'content');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle deeply nested paths', async () => {
|
||||||
|
const createdFolders = new Set<string>();
|
||||||
|
const mockFile = { path: 'a/b/c/d/e/f/file.md' } as TFile;
|
||||||
|
|
||||||
|
vault.getAbstractFileByPath.mockImplementation((path: string) => {
|
||||||
|
if (createdFolders.has(path)) {
|
||||||
|
return { path } as TFolder;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
vault.createFolder.mockImplementation(async (path: string) => {
|
||||||
|
createdFolders.add(path);
|
||||||
|
return { path } as TFolder;
|
||||||
|
});
|
||||||
|
|
||||||
|
vault.create.mockResolvedValue(mockFile);
|
||||||
|
|
||||||
|
const result = await noteTools.createNote('a/b/c/d/e/f/file.md', 'content', true);
|
||||||
|
|
||||||
|
expect(result.isError).toBeUndefined();
|
||||||
|
expect(vault.createFolder).toHaveBeenCalledTimes(6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
{
|
{
|
||||||
"1.0.0": "0.15.0"
|
"1.0.0": "0.15.0",
|
||||||
|
"1.1.0": "0.15.0",
|
||||||
|
"1.2.0": "0.15.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user