Release v1.0.0 - Initial Release

🎉 Initial release of Obsidian MCP Server plugin

Core Features:
- MCP server implementation with HTTP transport
- JSON-RPC 2.0 message handling
- Protocol version 2024-11-05 support

MCP Tools:
- read_note, create_note, update_note, delete_note
- search_notes, list_notes, get_vault_info

Server Features:
- Configurable HTTP server (default port: 3000)
- Health check and MCP endpoints
- Auto-start option

Security:
- Origin header validation (DNS rebinding protection)
- Optional Bearer token authentication
- CORS configuration

UI:
- Settings panel with full configuration
- Status bar indicator and ribbon icon
- Start/Stop/Restart commands

Documentation:
- Comprehensive README with examples
- Quick Start Guide and Implementation Summary
- Test client script
This commit is contained in:
2025-10-16 20:52:52 -04:00
commit 08cc6e9ea6
47 changed files with 8399 additions and 0 deletions

10
.editorconfig Normal file
View File

@@ -0,0 +1,10 @@
# top-most EditorConfig file
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4

3
.eslintignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
main.js

23
.eslintrc Normal file
View File

@@ -0,0 +1,23 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}

22
.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
tag-version-prefix=""

View File

@@ -0,0 +1,26 @@
---
description: Agent-specific do's and don'ts
---
# Agent Guidelines
## Do
- Add commands with stable IDs (don't rename once released)
- Provide defaults and validation in settings
- Write idempotent code paths so reload/unload doesn't leak listeners or intervals
- Use `this.register*` helpers for everything that needs cleanup
- Keep `main.ts` minimal and focused on lifecycle management
- Split functionality across separate modules
- Organize code into logical folders (commands/, ui/, utils/)
## Don't
- Introduce network calls without an obvious user-facing reason and documentation
- Ship features that require cloud services without clear disclosure and explicit opt-in
- Store or transmit vault contents unless essential and consented
- Put all code in `main.ts` - delegate to separate modules
- Create files larger than 200-300 lines without splitting them
- Commit build artifacts to version control
- Change plugin `id` after release
- Rename command IDs after release

View File

@@ -0,0 +1,84 @@
---
trigger: always_on
description: Common code patterns and examples
---
# Code Examples
## Organize Code Across Multiple Files
### main.ts (minimal, lifecycle only)
```ts
import { Plugin } from "obsidian";
import { MySettings, DEFAULT_SETTINGS } from "./settings";
import { registerCommands } from "./commands";
export default class MyPlugin extends Plugin {
settings: MySettings;
async onload() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
registerCommands(this);
}
}
```
### settings.ts
```ts
export interface MySettings {
enabled: boolean;
apiKey: string;
}
export const DEFAULT_SETTINGS: MySettings = {
enabled: true,
apiKey: "",
};
```
### commands/index.ts
```ts
import { Plugin } from "obsidian";
import { doSomething } from "./my-command";
export function registerCommands(plugin: Plugin) {
plugin.addCommand({
id: "do-something",
name: "Do something",
callback: () => doSomething(plugin),
});
}
```
## Add a Command
```ts
this.addCommand({
id: "your-command-id",
name: "Do the thing",
callback: () => this.doTheThing(),
});
```
## Persist Settings
```ts
interface MySettings { enabled: boolean }
const DEFAULT_SETTINGS: MySettings = { enabled: true };
async onload() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
await this.saveData(this.settings);
}
```
## Register Listeners Safely
```ts
this.registerEvent(this.app.workspace.on("file-open", f => { /* ... */ }));
this.registerDomEvent(window, "resize", () => { /* ... */ });
this.registerInterval(window.setInterval(() => { /* ... */ }, 1000));
```

View File

@@ -0,0 +1,35 @@
---
trigger: always_on
description: TypeScript coding conventions and best practices
---
# Coding Conventions
## TypeScript Standards
- Use TypeScript with `"strict": true` preferred
- Bundle everything into `main.js` (no unbundled runtime deps)
- Prefer `async/await` over promise chains
- Handle errors gracefully
## Code Organization
- **Keep `main.ts` minimal** - Focus only on plugin lifecycle (onload, onunload, addCommand calls)
- **Delegate all feature logic to separate modules**
- **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
## Platform Compatibility
- Avoid Node/Electron APIs if you want mobile compatibility
- Set `isDesktopOnly` accordingly if using desktop-only features
- Test on iOS and Android where feasible
- Don't assume desktop-only behavior unless `isDesktopOnly` is `true`
## Performance
- Keep startup light - defer heavy work until needed
- Avoid long-running tasks during `onload` - use lazy initialization
- Batch disk access and avoid excessive vault scans
- Debounce/throttle expensive operations in response to file system events
- Avoid large in-memory structures on mobile - be mindful of memory and storage constraints

View File

@@ -0,0 +1,54 @@
---
trigger: always_on
description: Commands and settings implementation guidelines
---
# Commands & Settings
## Commands
- Add user-facing commands via `this.addCommand(...)`
- **Use stable command IDs** - Don't rename once released
- Ensure commands are unique and descriptive
### Example: Add a Command
```ts
this.addCommand({
id: "your-command-id",
name: "Do the thing",
callback: () => this.doTheThing(),
});
```
## Settings
- Provide a settings tab if the plugin has configuration
- Always provide sensible defaults
- Persist settings using `this.loadData()` / `this.saveData()`
- Provide defaults and validation in settings
### Example: Persist Settings
```ts
interface MySettings { enabled: boolean }
const DEFAULT_SETTINGS: MySettings = { enabled: true };
async onload() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
await this.saveData(this.settings);
}
```
## Resource Management
- Write idempotent code paths so reload/unload doesn't leak listeners or intervals
- Use `this.register*` helpers for everything that needs cleanup
### Example: Register Listeners Safely
```ts
this.registerEvent(this.app.workspace.on("file-open", f => { /* ... */ }));
this.registerDomEvent(window, "resize", () => { /* ... */ });
this.registerInterval(window.setInterval(() => { /* ... */ }, 1000));
```

View File

@@ -0,0 +1,38 @@
---
trigger: always_on
description: Development environment and tooling requirements
---
# Environment & Tooling
## Required Tools
- **Node.js**: Use current LTS (Node 18+ recommended)
- **Package manager**: npm (required for this sample - `package.json` defines npm scripts and dependencies)
- **Bundler**: esbuild (required for this sample - `esbuild.config.mjs` and build scripts depend on it)
- **Types**: `obsidian` type definitions
**Note**: This sample project has specific technical dependencies on npm and esbuild. If creating a plugin from scratch, you can choose different tools, but you'll need to replace the build configuration accordingly. Alternative bundlers like Rollup or webpack are acceptable if they bundle all external dependencies into `main.js`.
## Common Commands
### Install dependencies
```bash
npm install
```
### Development (watch mode)
```bash
npm run dev
```
### Production build
```bash
npm run build
```
## Linting
- Install eslint: `npm install -g eslint`
- Analyze project: `eslint main.ts`
- Analyze folder: `eslint ./src/`

View File

@@ -0,0 +1,39 @@
---
trigger: always_on
description: File and folder organization conventions
---
# File & Folder Organization
## Core Principles
- **Organize code into multiple files**: Split functionality across separate modules rather than putting everything in `main.ts`
- **Keep `main.ts` minimal**: Focus only on plugin lifecycle (onload, onunload, addCommand calls)
- **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
## Recommended Structure
```
src/
main.ts # Plugin entry point, lifecycle management only
settings.ts # Settings interface and defaults
commands/ # Command implementations
command1.ts
command2.ts
ui/ # UI components, modals, views
modal.ts
view.ts
utils/ # Utility functions, helpers
helpers.ts
constants.ts
types.ts # TypeScript interfaces and types
```
## Best Practices
- Source lives in `src/`
- Keep the plugin small - avoid large dependencies
- Prefer browser-compatible packages
- Generated output should be placed at the plugin root or `dist/` depending on build setup
- Release artifacts must end up at the top level of the plugin folder (`main.js`, `manifest.json`, `styles.css`)

View File

@@ -0,0 +1,30 @@
---
trigger: always_on
description: Manifest.json requirements and conventions
---
# Manifest Rules
## Required Fields
The `manifest.json` must include:
- `id` - Plugin ID; for local dev it should match the folder name
- `name` - Display name
- `version` - Semantic Versioning `x.y.z`
- `minAppVersion` - Minimum Obsidian version required
- `description` - Brief description
- `isDesktopOnly` - Boolean indicating mobile compatibility
## Optional Fields
- `author` - Plugin author name
- `authorUrl` - Author's URL
- `fundingUrl` - Funding/donation URL (string or map)
## Critical Rules
- **Never change `id` after release** - Treat it as stable API
- Keep `minAppVersion` accurate when using newer APIs
- Use Semantic Versioning for `version` field
- Canonical requirements: https://github.com/obsidianmd/obsidian-releases/blob/master/.github/workflows/validate-plugin-entry.yml

View File

@@ -0,0 +1,16 @@
---
trigger: always_on
description: Obsidian plugin project structure and requirements
---
# Project Overview
- **Target**: Obsidian Community Plugin (TypeScript → bundled JavaScript)
- **Entry point**: `main.ts` compiled to `main.js` and loaded by Obsidian
- **Required release artifacts**: `main.js`, `manifest.json`, and optional `styles.css`
## Key Requirements
- All TypeScript code must be bundled into a single `main.js` file
- Release artifacts must be placed at the top level of the plugin folder
- Never commit build artifacts (`node_modules/`, `main.js`, etc.) to version control

View File

@@ -0,0 +1,22 @@
---
trigger: always_on
description: Official documentation and reference links
---
# References
## Official Resources
- **Obsidian sample plugin**: https://github.com/obsidianmd/obsidian-sample-plugin
- **API documentation**: https://docs.obsidian.md
- **Developer policies**: https://docs.obsidian.md/Developer+policies
- **Plugin guidelines**: https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines
- **Style guide**: https://help.obsidian.md/style-guide
- **Manifest validation**: https://github.com/obsidianmd/obsidian-releases/blob/master/.github/workflows/validate-plugin-entry.yml
## When to Consult
- Check **Developer policies** before implementing features that access external services
- Review **Plugin guidelines** before submitting to the community catalog
- Reference **API documentation** when using Obsidian APIs
- Follow **Style guide** for UI text and documentation

View File

@@ -0,0 +1,27 @@
---
trigger: always_on
description: Security, privacy, and compliance requirements
---
# Security, Privacy, and Compliance
Follow Obsidian's **Developer Policies** and **Plugin Guidelines**.
## Network & External Services
- **Default to local/offline operation** - Only make network requests when essential to the feature
- **No hidden telemetry** - If you collect optional analytics or call third-party services, require explicit opt-in and document clearly in `README.md` and in settings
- **Never execute remote code** - Don't fetch and eval scripts, or auto-update plugin code outside of normal releases
- **Clearly disclose external services** - Document any external services used, data sent, and risks
## Data Access & Privacy
- **Minimize scope** - Read/write only what's necessary inside the vault
- **Do not access files outside the vault**
- **Respect user privacy** - Do not collect vault contents, filenames, or personal information unless absolutely necessary and explicitly consented
- **No deceptive patterns** - Avoid ads or spammy notifications
## Resource Management
- **Register and clean up all resources** - Use the provided `register*` helpers so the plugin unloads safely
- Clean up DOM, app, and interval listeners properly

View File

@@ -0,0 +1,45 @@
---
trigger: always_on
description: Common issues and solutions
---
# Troubleshooting
## Plugin Doesn't Load After Build
**Issue**: Plugin doesn't appear in Obsidian after building
**Solution**: Ensure `main.js` and `manifest.json` are at the top level of the plugin folder under `<Vault>/.obsidian/plugins/<plugin-id>/`
## Build Issues
**Issue**: `main.js` is missing after build
**Solution**: Run `npm run build` or `npm run dev` to compile your TypeScript source code
## Commands Not Appearing
**Issue**: Commands don't show up in command palette
**Solution**:
- Verify `addCommand` runs after `onload`
- Ensure command IDs are unique
- Check that commands are properly registered
## Settings Not Persisting
**Issue**: Settings reset after reloading Obsidian
**Solution**:
- Ensure `loadData`/`saveData` are awaited
- Re-render the UI after changes
- Verify settings are properly merged with defaults
## Mobile-Only Issues
**Issue**: Plugin works on desktop but not mobile
**Solution**:
- Confirm you're not using desktop-only APIs
- Check `isDesktopOnly` setting in manifest
- Test on actual mobile devices or adjust compatibility

View File

@@ -0,0 +1,32 @@
---
trigger: always_on
description: UX and copy guidelines for UI text
---
# UX & Copy Guidelines
For UI text, commands, and settings:
## Text Formatting
- **Prefer sentence case** for headings, buttons, and titles
- Use clear, action-oriented imperatives in step-by-step copy
- Keep in-app strings short, consistent, and free of jargon
## UI References
- Use **bold** to indicate literal UI labels
- Prefer "select" for interactions
- Use arrow notation for navigation: **Settings → Community plugins**
## Examples
✅ Good:
- "Select **Settings → Community plugins**"
- "Enable the plugin"
- "Configure your API key"
❌ Avoid:
- "Go to Settings and then Community plugins"
- "Turn on the plugin"
- "Setup your API key"

View File

@@ -0,0 +1,32 @@
---
trigger: always_on
description: Versioning and release process
---
# Versioning & Releases
## Version Management
- Bump `version` in `manifest.json` using Semantic Versioning (SemVer)
- Update `versions.json` to map plugin version → minimum app version
- Keep version numbers consistent across all release artifacts
## Release Process
1. **Create GitHub release** with tag that exactly matches `manifest.json`'s `version`
- **Do not use a leading `v`** in the tag
2. **Attach required assets** to the release:
- `manifest.json`
- `main.js`
- `styles.css` (if present)
3. After initial release, follow the process to add/update your plugin in the community catalog
## Testing Before Release
Manual install for testing:
1. Copy `main.js`, `manifest.json`, `styles.css` (if any) to:
```
<Vault>/.obsidian/plugins/<plugin-id>/
```
2. Reload Obsidian
3. Enable the plugin in **Settings → Community plugins**

163
CHANGELOG.md Normal file
View File

@@ -0,0 +1,163 @@
# Changelog
All notable changes to the Obsidian MCP Server plugin will be documented in this file.
## [1.0.0] - 2025-10-16
### 🎉 Initial Release
#### Added
**Core Features**
- MCP (Model Context Protocol) server implementation
- HTTP transport with Express.js
- JSON-RPC 2.0 message handling
- Protocol version 2024-11-05 support
**MCP Tools**
- `read_note` - Read note content from vault
- `create_note` - Create new notes
- `update_note` - Update existing notes
- `delete_note` - Delete notes
- `search_notes` - Search notes by content or filename
- `list_notes` - List all notes or notes in specific folder
- `get_vault_info` - Get vault metadata and statistics
**Server Features**
- Configurable HTTP server (default port: 3000)
- Localhost-only binding (127.0.0.1)
- Health check endpoint (`/health`)
- MCP endpoint (`/mcp`)
- Auto-start option
**Security**
- Origin header validation (DNS rebinding protection)
- Optional Bearer token authentication
- CORS configuration with allowed origins
- Request validation and error handling
**User Interface**
- Settings panel with full configuration
- Status bar indicator showing server state
- Ribbon icon for quick server toggle
- Start/Stop/Restart commands
- Real-time server status display
- Connection information display
**Documentation**
- Comprehensive README with examples
- Quick Start Guide
- Implementation Summary
- Test client script
- Example MCP requests
- Security considerations
**Developer Tools**
- TypeScript implementation
- esbuild bundler
- Test client for validation
- Health check endpoint
### Technical Details
**Dependencies**
- express: ^4.18.2
- cors: ^2.8.5
- obsidian: latest
**Build**
- TypeScript 4.7.4
- esbuild 0.17.3
- Output: 828KB bundled
**Compatibility**
- Obsidian minimum version: 0.15.0
- Desktop only (requires Node.js HTTP server)
- Protocol: MCP 2024-11-05
### Known Limitations
- Desktop only (not available on mobile)
- Single vault per server instance
- No WebSocket support (HTTP only)
- No SSL/TLS (localhost only)
---
## Future Roadmap
See [ROADMAP.md](ROADMAP.md) for detailed implementation plans.
### Phase 1: Foundation (P0-P1)
- **Path Normalization** - Consistent path handling across platforms
- **Error Message Improvements** - Clear, actionable error messages with troubleshooting tips
- **Enhanced Authentication** - Secure API key management, multiple keys with labels, expiration, rate limiting, audit logging, and permission scopes
- **API Unification** - Standardize parameter naming and return structured, typed results
- **Discovery Endpoints** - Add `stat` and `exists` tools for exploring vault structure
### Phase 2: Enhanced Operations (P1-P2)
- **Write Operations & Concurrency** - ETag-based version control, partial updates (frontmatter, sections)
- **Conflict Resolution** - Create notes with conflict strategies (error, overwrite, rename)
- **File Rename/Move** - Rename or move files with automatic wikilink updates
- **Enhanced List Operations** - Filtering, recursion control, pagination, frontmatter summaries
- **Advanced Search** - Regex search, snippet extraction, glob filtering
### Phase 3: Advanced Features (P2-P3)
- **Frontmatter Parsing** - Read and update frontmatter without modifying content
- **Linking & Backlinks** - Wikilink validation, resolution, and backlink queries
- **Waypoint Support** - Tools for working with Waypoint plugin markers
- **Excalidraw Support** - Specialized tool for reading Excalidraw drawings
### Future Considerations
- **Resources API** - Expose notes as MCP resources
- **Prompts API** - Templated prompts for common operations
- **Batch Operations** - Multiple operations in single request
- **WebSocket Transport** - Real-time updates and notifications
- **Graph API** - Enhanced graph visualization and traversal
- **Tag & Canvas APIs** - Query tags and manipulate canvas files
- **Dataview Integration** - Query vault using Dataview syntax
- **Performance Enhancements** - Indexing, caching, streaming for large vaults
---
## Version History
| Version | Date | Notes |
|---------|------|-------|
| 1.0.0 | 2025-10-16 | Initial release |
---
## Upgrade Guide
### From Development to 1.0.0
If you were using a development version:
1. Backup your settings
2. Disable the plugin
3. Delete the old plugin folder
4. Install version 1.0.0
5. Re-enable and reconfigure
### Breaking Changes
None (initial release)
---
## Support
For issues, questions, or contributions:
- Check the README.md for documentation
- Review QUICKSTART.md for setup help
- Check existing issues before creating new ones
- Include version number in bug reports
---
## Credits
- MCP Protocol: https://modelcontextprotocol.io
- Obsidian API: https://github.com/obsidianmd/obsidian-api
- Built with TypeScript, Express.js, and ❤️

5
LICENSE Normal file
View File

@@ -0,0 +1,5 @@
Copyright (C) 2020-2025 by Dynalist Inc.
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

198
QUICKSTART.md Normal file
View File

@@ -0,0 +1,198 @@
# Quick Start Guide
## 🚀 Getting Started
### 1. Enable the Plugin
1. Open Obsidian
2. Go to **Settings****Community Plugins**
3. Find **MCP Server** in the list
4. Toggle it **ON**
### 2. Start the Server
**Option A: Via Ribbon Icon**
- Click the server icon (📡) in the left sidebar
**Option B: Via Command Palette**
- Press `Ctrl/Cmd + P`
- Type "Start MCP Server"
- Press Enter
**Option C: Auto-start**
- Go to **Settings****MCP Server**
- Enable "Auto-start server"
- Server will start automatically when Obsidian launches
### 3. Verify Server is Running
Check the status bar at the bottom of Obsidian:
- **Running**: `MCP: Running (3000)`
- **Stopped**: `MCP: Stopped`
Or visit: http://127.0.0.1:3000/health
### 4. Test the Connection
Run the test client:
```bash
node test-client.js
```
Expected output:
```
🧪 Testing Obsidian MCP Server
Server: http://127.0.0.1:3000/mcp
API Key: None
1⃣ Testing initialize...
✅ Initialize successful
Server: obsidian-mcp-server 1.0.0
Protocol: 2024-11-05
2⃣ Testing tools/list...
✅ Tools list successful
Found 7 tools:
- read_note: Read the content of a note from the Obsidian vault
- create_note: Create a new note in the Obsidian vault
...
🎉 All tests passed!
```
## 🔧 Configuration
### Basic Settings
Go to **Settings****MCP Server**:
| Setting | Default | Description |
|---------|---------|-------------|
| Port | 3000 | HTTP server port |
| Auto-start | Off | Start server on Obsidian launch |
| Enable CORS | On | Allow cross-origin requests |
| Allowed Origins | * | Comma-separated list of allowed origins |
### Security Settings
| Setting | Default | Description |
|---------|---------|-------------|
| Enable Authentication | Off | Require API key for requests |
| API Key | (empty) | Bearer token for authentication |
## 🔌 Connect an MCP Client
### Claude Desktop
Edit your Claude Desktop config file:
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
Add:
```json
{
"mcpServers": {
"obsidian": {
"url": "http://127.0.0.1:3000/mcp"
}
}
}
```
Restart Claude Desktop.
### Other MCP Clients
Use the endpoint: `http://127.0.0.1:3000/mcp`
## 📝 Available Tools
Once connected, you can use these tools:
- **read_note** - Read note content
- **create_note** - Create a new note
- **update_note** - Update existing note
- **delete_note** - Delete a note
- **search_notes** - Search vault by query
- **list_notes** - List all notes or notes in a folder
- **get_vault_info** - Get vault metadata
## 🔒 Using Authentication
1. Enable authentication in settings
2. Set an API key (e.g., `my-secret-key-123`)
3. Include in requests:
```bash
curl -X POST http://127.0.0.1:3000/mcp \
-H "Authorization: Bearer my-secret-key-123" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
Or in Claude Desktop config:
```json
{
"mcpServers": {
"obsidian": {
"url": "http://127.0.0.1:3000/mcp",
"headers": {
"Authorization": "Bearer my-secret-key-123"
}
}
}
}
```
## ❓ Troubleshooting
### Server won't start
**Error: Port already in use**
- Change the port in settings
- Or stop the process using port 3000
**Error: Cannot find module**
- Run `npm install` in the plugin directory
- Rebuild with `npm run build`
### Cannot connect from client
**Check server is running**
- Look at status bar: should show "MCP: Running (3000)"
- Visit http://127.0.0.1:3000/health
**Check firewall**
- Ensure localhost connections are allowed
- Server only binds to 127.0.0.1 (localhost)
**Check authentication**
- If enabled, ensure API key is correct
- Check Authorization header format
### Tools not working
**Path errors**
- Use relative paths from vault root
- Example: `folder/note.md` not `/full/path/to/note.md`
**Permission errors**
- Ensure Obsidian has file system access
- Check vault is not read-only
## 🎯 Next Steps
- Read the full [README.md](README.md) for detailed documentation
- Explore the [MCP Protocol Documentation](https://modelcontextprotocol.io)
- Check example requests in the README
- Customize settings for your workflow
## 💡 Tips
- Use the ribbon icon for quick server toggle
- Enable auto-start for seamless integration
- Use authentication for additional security
- Monitor the status bar for server state
- Check Obsidian console (Ctrl+Shift+I) for detailed logs

255
README.md Normal file
View File

@@ -0,0 +1,255 @@
# Obsidian MCP Server Plugin
An Obsidian plugin that exposes your vault operations via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io) over HTTP. This allows AI assistants and other MCP clients to interact with your Obsidian vault programmatically.
## Features
- **HTTP MCP Server**: Runs an HTTP server implementing the MCP protocol
- **Vault Operations**: Exposes tools for reading, creating, updating, and deleting notes
- **Search Functionality**: Search notes by content or filename
- **Security**: Localhost-only binding, optional authentication, CORS configuration
- **Easy Configuration**: Simple settings UI with server status and controls
## 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
## Installation
### From Source
1. Clone this repository into your vault's plugins folder:
```bash
cd /path/to/vault/.obsidian/plugins
git clone <repository-url> obsidian-mcp-server
cd obsidian-mcp-server
```
2. Install dependencies:
```bash
npm install
```
3. Build the plugin:
```bash
npm run build
```
4. Enable the plugin in Obsidian Settings → Community Plugins
## Configuration
1. Open Obsidian Settings → MCP Server
2. Configure the following options:
- **Port**: HTTP server port (default: 3000)
- **Auto-start**: Automatically start server on Obsidian launch
- **Enable CORS**: Allow cross-origin requests
- **Allowed Origins**: Comma-separated list of allowed origins
- **Enable Authentication**: Require API key for requests
- **API Key**: Bearer token for authentication
3. Click "Start Server" or use the ribbon icon to toggle the server
## Usage
### Starting the Server
- **Via Ribbon Icon**: Click the server icon in the left sidebar
- **Via Command Palette**: Run "Start MCP Server"
- **Auto-start**: Enable in settings to start automatically
### Connecting an MCP Client
The server exposes an MCP endpoint at:
```
http://127.0.0.1:3000/mcp
```
Example client configuration (e.g., for Claude Desktop):
```json
{
"mcpServers": {
"obsidian": {
"url": "http://127.0.0.1:3000/mcp"
}
}
}
```
### Using with Authentication
If authentication is enabled, include the API key in requests:
```bash
curl -X POST http://127.0.0.1:3000/mcp \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
## Example MCP Requests
### Initialize Connection
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "example-client",
"version": "1.0.0"
}
}
}
```
### List Available Tools
```json
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}
```
### Read a Note
```json
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "read_note",
"arguments": {
"path": "folder/note.md"
}
}
}
```
### Create a Note
```json
{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "create_note",
"arguments": {
"path": "new-note.md",
"content": "# New Note\n\nThis is the content."
}
}
}
```
### Search Notes
```json
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "search_notes",
"arguments": {
"query": "search term"
}
}
}
```
## Security Considerations
- **Localhost Only**: The server binds to `127.0.0.1` to prevent external access
- **Origin Validation**: Validates request origins to prevent DNS rebinding attacks
- **Optional Authentication**: Use API keys to restrict access
- **Desktop Only**: This plugin only works on desktop (not mobile) due to HTTP server requirements
## Development
### Building from Source
```bash
npm install
npm run dev # Watch mode for development
npm run build # Production build
```
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
- Reload Obsidian to load the new version of your plugin.
- Enable plugin in settings window.
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
## Releasing new releases
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
- Publish the release.
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
## Adding your plugin to the community plugin list
- Check the [plugin guidelines](https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines).
- Publish an initial version.
- Make sure you have a `README.md` file in the root of your repo.
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
## How to use
- Clone this repo.
- Make sure your NodeJS is at least v16 (`node --version`).
- `npm i` or `yarn` to install dependencies.
- `npm run dev` to start compilation in watch mode.
## Manually installing the plugin
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
## Improve code quality with eslint (optional)
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
- To use eslint with this project, make sure to install eslint from terminal:
- `npm install -g eslint`
- To use eslint to analyze this project use this command:
- `eslint main.ts`
- eslint will then create a report with suggestions for code improvement by file and line number.
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder:
- `eslint ./src/`
## Funding URL
You can include funding URLs where people who use your plugin can financially support it.
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
```json
{
"fundingUrl": "https://buymeacoffee.com"
}
```
If you have multiple URLs, you can also do:
```json
{
"fundingUrl": {
"Buy Me a Coffee": "https://buymeacoffee.com",
"GitHub Sponsor": "https://github.com/sponsors",
"Patreon": "https://www.patreon.com/"
}
}
```
## API Documentation
See https://github.com/obsidianmd/obsidian-api

1341
ROADMAP.md Normal file

File diff suppressed because it is too large Load Diff

308
TROUBLESHOOTING.md Normal file
View File

@@ -0,0 +1,308 @@
# Troubleshooting Guide
## Plugin Won't Load
### Check Required Files
Ensure these files exist in the plugin directory:
```bash
ls -la /path/to/vault/.obsidian/plugins/obsidian-mcp-server/
```
Required files:
-`main.js` (should be ~846KB)
-`manifest.json`
-`styles.css`
### Check Obsidian Console
1. Open Obsidian
2. Press `Ctrl+Shift+I` (Windows/Linux) or `Cmd+Option+I` (Mac)
3. Go to the **Console** tab
4. Look for errors related to `obsidian-mcp-server`
Common errors:
- **Module not found**: Rebuild the plugin with `npm run build`
- **Syntax error**: Check the build completed successfully
- **Permission error**: Ensure files are readable
### Verify Plugin is Enabled
1. Go to **Settings****Community Plugins**
2. Find **MCP Server** in the list
3. Ensure the toggle is **ON**
4. If not visible, click **Reload** or restart Obsidian
### Check Manifest
Verify `manifest.json` contains:
```json
{
"id": "obsidian-mcp-server",
"name": "MCP Server",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Exposes Obsidian vault operations via Model Context Protocol (MCP) over HTTP",
"author": "",
"authorUrl": "",
"isDesktopOnly": true
}
```
### Rebuild from Source
If the plugin still won't load:
```bash
cd /path/to/vault/.obsidian/plugins/obsidian-mcp-server
npm install
npm run build
```
Then restart Obsidian.
### Check Obsidian Version
This plugin requires:
- **Minimum Obsidian version**: 0.15.0
- **Desktop only** (not mobile)
Check your version:
1. **Settings****About**
2. Look for "Current version"
### Verify Node.js Built-ins
The plugin uses Node.js modules (http, express). Ensure you're running on desktop Obsidian, not mobile.
## Plugin Loads But Shows No Info
### Check Plugin Description
If the plugin appears in the list but shows no description:
1. Check `manifest.json` has a `description` field
2. Restart Obsidian
3. Try disabling and re-enabling the plugin
### Check for Errors on Load
1. Open Console (`Ctrl+Shift+I`)
2. Disable the plugin
3. Re-enable it
4. Watch for errors in console
## Server Won't Start
### Port Already in Use
**Error**: "Port 3000 is already in use"
**Solution**:
1. Go to **Settings****MCP Server**
2. Change port to something else (e.g., 3001, 3002)
3. Try starting again
Or find and kill the process using port 3000:
```bash
# Linux/Mac
lsof -i :3000
kill -9 <PID>
# Windows
netstat -ano | findstr :3000
taskkill /PID <PID> /F
```
### Module Not Found
**Error**: "Cannot find module 'express'" or similar
**Solution**:
```bash
cd /path/to/vault/.obsidian/plugins/obsidian-mcp-server
npm install
npm run build
```
Restart Obsidian.
### Permission Denied
**Error**: "EACCES" or "Permission denied"
**Solution**:
- Try a different port (above 1024)
- Check firewall settings
- Run Obsidian with appropriate permissions
## Server Starts But Can't Connect
### Check Server is Running
Look at the status bar (bottom of Obsidian):
- Should show: `MCP: Running (3000)`
- If shows: `MCP: Stopped` - server isn't running
### Test Health Endpoint
Open browser or use curl:
```bash
curl http://127.0.0.1:3000/health
```
Should return:
```json
{"status":"ok","timestamp":1234567890}
```
### Check Localhost Binding
The server only binds to `127.0.0.1` (localhost). You cannot connect from:
- Other computers on the network
- External IP addresses
- Public internet
This is by design for security.
### Test MCP Endpoint
```bash
curl -X POST http://127.0.0.1:3000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"ping"}'
```
Should return:
```json
{"jsonrpc":"2.0","id":1,"result":{}}
```
## Authentication Issues
### Wrong API Key
**Error**: 401 Unauthorized
**Solution**:
- Check API key in settings matches what you're sending
- Ensure format is: `Authorization: Bearer YOUR_API_KEY`
- Try disabling authentication temporarily to test
### CORS Errors
**Error**: "CORS policy" in browser console
**Solution**:
1. Go to **Settings****MCP Server**
2. Ensure "Enable CORS" is **ON**
3. Check "Allowed Origins" includes your origin or `*`
4. Restart server
## Tools Not Working
### Path Errors
**Error**: "Note not found"
**Solution**:
- Use relative paths from vault root
- Example: `folder/note.md` not `/full/path/to/note.md`
- Don't include vault name in path
### Permission Errors
**Error**: "EACCES" or "Permission denied"
**Solution**:
- Check file permissions in vault
- Ensure Obsidian has file system access
- Check vault is not read-only
### Search Returns Nothing
**Issue**: `search_notes` returns no results
**Solution**:
- Check query is not empty
- Search is case-insensitive
- Searches both filename and content
- Try simpler query
## Getting Help
### Collect Debug Information
When reporting issues, include:
1. **Obsidian version**: Settings → About
2. **Plugin version**: Check manifest.json
3. **Operating System**: Windows/Mac/Linux
4. **Error messages**: From console (Ctrl+Shift+I)
5. **Steps to reproduce**: What you did before the error
### Console Logs
Enable detailed logging:
1. Open Console (`Ctrl+Shift+I`)
2. Try the failing operation
3. Copy all red error messages
4. Include in your report
### Test Client Output
Run the test client and include output:
```bash
node test-client.js
```
### Check GitHub Issues
Before creating a new issue:
1. Search existing issues
2. Check if it's already reported
3. See if there's a workaround
## Common Solutions
### "Have you tried turning it off and on again?"
Seriously, this fixes many issues:
1. Stop the server
2. Disable the plugin
3. Restart Obsidian
4. Enable the plugin
5. Start the server
### Clean Reinstall
If all else fails:
```bash
# Backup settings first!
cd /path/to/vault/.obsidian/plugins
rm -rf obsidian-mcp-server
# Re-install plugin
cd obsidian-mcp-server
npm install
npm run build
```
Restart Obsidian.
### Reset Settings
If settings are corrupted:
1. Stop server
2. Disable plugin
3. Delete `/path/to/vault/.obsidian/plugins/obsidian-mcp-server/data.json`
4. Re-enable plugin
5. Reconfigure settings
## Still Having Issues?
1. Check the README.md for documentation
2. Review QUICKSTART.md for setup steps
3. Run the test client to verify server
4. Check Obsidian console for errors
5. Try a clean rebuild
6. Create a GitHub issue with debug info

51
esbuild.config.mjs Normal file
View File

@@ -0,0 +1,51 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins
],
format: "cjs",
target: "es2018",
platform: "node",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

8
manifest.json Normal file
View File

@@ -0,0 +1,8 @@
{
"id": "obsidian-mcp-server",
"name": "MCP Server",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Exposes Obsidian vault operations via Model Context Protocol (MCP) over HTTP",
"isDesktopOnly": true
}

275
old-structure/main.ts Normal file
View File

@@ -0,0 +1,275 @@
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`});
}
}
}

485
old-structure/mcp-server.ts Normal file
View File

@@ -0,0 +1,485 @@
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;
}
}

122
old-structure/mcp-types.ts Normal file
View File

@@ -0,0 +1,122 @@
// 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,
};

3400
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "obsidian-mcp-server",
"version": "1.0.0",
"description": "MCP (Model Context Protocol) server plugin for Obsidian - exposes vault operations via HTTP",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [
"obsidian",
"mcp",
"model-context-protocol",
"ai",
"llm"
],
"author": "",
"license": "MIT",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2"
},
"devDependencies": {
"@types/body-parser": "^1.19.6",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
}

123
src/main.ts Normal file
View File

@@ -0,0 +1,123 @@
import { Notice, Plugin } from 'obsidian';
import { MCPServer } from './server/mcp-server';
import { MCPPluginSettings, DEFAULT_SETTINGS } from './types/settings-types';
import { MCPServerSettingTab } from './settings';
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();
}
});
// Register 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);
if (this.mcpServer) {
this.mcpServer.updateSettings(this.settings);
}
}
}

144
src/server/mcp-server.ts Normal file
View File

@@ -0,0 +1,144 @@
import { App } from 'obsidian';
import express, { Express } from 'express';
import { Server } from 'http';
import {
JSONRPCRequest,
JSONRPCResponse,
ErrorCodes,
InitializeResult,
ListToolsResult,
CallToolResult
} from '../types/mcp-types';
import { MCPServerSettings } from '../types/settings-types';
import { ToolRegistry } from '../tools';
import { setupMiddleware } from './middleware';
import { setupRoutes } from './routes';
export class MCPServer {
private app: Express;
private server: Server | null = null;
private obsidianApp: App;
private settings: MCPServerSettings;
private toolRegistry: ToolRegistry;
constructor(obsidianApp: App, settings: MCPServerSettings) {
this.obsidianApp = obsidianApp;
this.settings = settings;
this.app = express();
this.toolRegistry = new ToolRegistry(obsidianApp);
setupMiddleware(this.app, this.settings, this.createErrorResponse.bind(this));
setupRoutes(this.app, this.handleRequest.bind(this), this.createErrorResponse.bind(this));
}
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 as 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> {
return {
tools: this.toolRegistry.getToolDefinitions()
};
}
private async handleCallTool(params: any): Promise<CallToolResult> {
const { name, arguments: args } = params;
return await this.toolRegistry.callTool(name, args);
}
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
}
};
}
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;
}
}

54
src/server/middleware.ts Normal file
View File

@@ -0,0 +1,54 @@
import { Express, Request, Response } from 'express';
import express from 'express';
import cors from 'cors';
import { MCPServerSettings } from '../types/settings-types';
import { ErrorCodes } from '../types/mcp-types';
export function setupMiddleware(app: Express, settings: MCPServerSettings, createErrorResponse: (id: any, code: number, message: string) => any): void {
// Parse JSON bodies
app.use(express.json());
// CORS configuration
if (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 (settings.allowedOrigins.includes('*') ||
settings.allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
};
app.use(cors(corsOptions));
}
// Authentication middleware
if (settings.enableAuth && settings.apiKey) {
app.use((req: Request, res: Response, next: any) => {
const authHeader = req.headers.authorization;
const apiKey = authHeader?.replace('Bearer ', '');
if (apiKey !== settings.apiKey) {
return res.status(401).json(createErrorResponse(null, ErrorCodes.InvalidRequest, 'Unauthorized'));
}
next();
});
}
// Origin validation for security (DNS rebinding protection)
app.use((req: Request, res: Response, next: any) => {
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(createErrorResponse(null, ErrorCodes.InvalidRequest, 'Only localhost connections allowed'));
}
next();
});
}

25
src/server/routes.ts Normal file
View File

@@ -0,0 +1,25 @@
import { Express, Request, Response } from 'express';
import { JSONRPCRequest, JSONRPCResponse, ErrorCodes } from '../types/mcp-types';
export function setupRoutes(
app: Express,
handleRequest: (request: JSONRPCRequest) => Promise<JSONRPCResponse>,
createErrorResponse: (id: any, code: number, message: string) => JSONRPCResponse
): void {
// Main MCP endpoint
app.post('/mcp', async (req: Request, res: Response) => {
try {
const request = req.body as JSONRPCRequest;
const response = await handleRequest(request);
res.json(response);
} catch (error) {
console.error('MCP request error:', error);
res.status(500).json(createErrorResponse(null, ErrorCodes.InternalError, 'Internal server error'));
}
});
// Health check endpoint
app.get('/health', (_req: Request, res: Response) => {
res.json({ status: 'ok', timestamp: Date.now() });
});
}

153
src/settings.ts Normal file
View File

@@ -0,0 +1,153 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
import { MCPPluginSettings } from './types/settings-types';
import MCPServerPlugin from './main';
export 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'});
// Network disclosure
const disclosureEl = containerEl.createEl('div', {cls: 'mcp-disclosure'});
disclosureEl.createEl('p', {
text: '⚠️ This plugin runs a local HTTP server to expose vault operations via the Model Context Protocol (MCP). The server only accepts connections from localhost (127.0.0.1) for security.'
});
disclosureEl.style.backgroundColor = 'var(--background-secondary)';
disclosureEl.style.padding = '12px';
disclosureEl.style.marginBottom = '16px';
disclosureEl.style.borderRadius = '4px';
// 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();
});
buttonContainer.createEl('button', {text: 'Restart Server'})
.addEventListener('click', async () => {
await this.plugin.stopServer();
await this.plugin.startServer();
this.display();
});
} else {
buttonContainer.createEl('button', {text: 'Start Server'})
.addEventListener('click', async () => {
await this.plugin.startServer();
this.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`});
}
}
}

149
src/tools/index.ts Normal file
View File

@@ -0,0 +1,149 @@
import { App } from 'obsidian';
import { Tool, CallToolResult } from '../types/mcp-types';
import { NoteTools } from './note-tools';
import { VaultTools } from './vault-tools';
export class ToolRegistry {
private noteTools: NoteTools;
private vaultTools: VaultTools;
constructor(app: App) {
this.noteTools = new NoteTools(app);
this.vaultTools = new VaultTools(app);
}
getToolDefinitions(): Tool[] {
return [
{
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"
}
}
}
}
];
}
async callTool(name: string, args: any): Promise<CallToolResult> {
try {
switch (name) {
case "read_note":
return await this.noteTools.readNote(args.path);
case "create_note":
return await this.noteTools.createNote(args.path, args.content);
case "update_note":
return await this.noteTools.updateNote(args.path, args.content);
case "delete_note":
return await this.noteTools.deleteNote(args.path);
case "search_notes":
return await this.vaultTools.searchNotes(args.query);
case "get_vault_info":
return await this.vaultTools.getVaultInfo();
case "list_notes":
return await this.vaultTools.listNotes(args.folder);
default:
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
isError: true
};
}
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${(error as Error).message}` }],
isError: true
};
}
}
}

68
src/tools/note-tools.ts Normal file
View File

@@ -0,0 +1,68 @@
import { App, TFile } from 'obsidian';
import { CallToolResult } from '../types/mcp-types';
export class NoteTools {
constructor(private app: App) {}
async readNote(path: string): Promise<CallToolResult> {
const file = this.app.vault.getAbstractFileByPath(path);
if (!file || !(file instanceof TFile)) {
return {
content: [{ type: "text", text: `Note not found: ${path}` }],
isError: true
};
}
const content = await this.app.vault.read(file);
return {
content: [{ type: "text", text: content }]
};
}
async createNote(path: string, content: string): Promise<CallToolResult> {
try {
const file = await this.app.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 as Error).message}` }],
isError: true
};
}
}
async updateNote(path: string, content: string): Promise<CallToolResult> {
const file = this.app.vault.getAbstractFileByPath(path);
if (!file || !(file instanceof TFile)) {
return {
content: [{ type: "text", text: `Note not found: ${path}` }],
isError: true
};
}
await this.app.vault.modify(file, content);
return {
content: [{ type: "text", text: `Note updated successfully: ${path}` }]
};
}
async deleteNote(path: string): Promise<CallToolResult> {
const file = this.app.vault.getAbstractFileByPath(path);
if (!file || !(file instanceof TFile)) {
return {
content: [{ type: "text", text: `Note not found: ${path}` }],
isError: true
};
}
await this.app.vault.delete(file);
return {
content: [{ type: "text", text: `Note deleted successfully: ${path}` }]
};
}
}

77
src/tools/vault-tools.ts Normal file
View File

@@ -0,0 +1,77 @@
import { App, TFile, TFolder } from 'obsidian';
import { CallToolResult } from '../types/mcp-types';
export class VaultTools {
constructor(private app: App) {}
async searchNotes(query: string): Promise<CallToolResult> {
const files = this.app.vault.getMarkdownFiles();
const results: string[] = [];
for (const file of files) {
const content = await this.app.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'
}]
};
}
async getVaultInfo(): Promise<CallToolResult> {
const files = this.app.vault.getFiles();
const markdownFiles = this.app.vault.getMarkdownFiles();
const info = {
name: this.app.vault.getName(),
totalFiles: files.length,
markdownFiles: markdownFiles.length,
rootPath: (this.app.vault.adapter as any).basePath || 'Unknown'
};
return {
content: [{
type: "text",
text: JSON.stringify(info, null, 2)
}]
};
}
async listNotes(folder?: string): Promise<CallToolResult> {
let files: TFile[];
if (folder) {
const folderObj = this.app.vault.getAbstractFileByPath(folder);
if (!folderObj || !(folderObj instanceof TFolder)) {
return {
content: [{ type: "text", text: `Folder not found: ${folder}` }],
isError: true
};
}
files = [];
this.app.vault.getMarkdownFiles().forEach((file: TFile) => {
if (file.path.startsWith(folder + '/')) {
files.push(file);
}
});
} else {
files = this.app.vault.getMarkdownFiles();
}
const noteList = files.map(f => f.path).join('\n');
return {
content: [{
type: "text",
text: `Found ${files.length} notes:\n${noteList}`
}]
};
}
}

63
src/types/mcp-types.ts Normal file
View File

@@ -0,0 +1,63 @@
// MCP Protocol Types
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 enum ErrorCodes {
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603
}
export interface InitializeResult {
protocolVersion: string;
capabilities: {
tools?: {};
};
serverInfo: {
name: string;
version: string;
};
}
export interface Tool {
name: string;
description: string;
inputSchema: {
type: string;
properties: Record<string, any>;
required?: string[];
};
}
export interface ListToolsResult {
tools: Tool[];
}
export interface ContentBlock {
type: "text";
text: string;
}
export interface CallToolResult {
content: ContentBlock[];
isError?: boolean;
}

View File

@@ -0,0 +1,21 @@
// Settings Types
export interface MCPServerSettings {
port: number;
enableCORS: boolean;
allowedOrigins: string[];
apiKey?: string;
enableAuth: boolean;
}
export interface MCPPluginSettings extends MCPServerSettings {
autoStart: boolean;
}
export const DEFAULT_SETTINGS: MCPPluginSettings = {
port: 3000,
enableCORS: true,
allowedOrigins: ['*'],
apiKey: '',
enableAuth: false,
autoStart: false
};

53
styles.css Normal file
View File

@@ -0,0 +1,53 @@
/* MCP Server Plugin Styles */
.mcp-status-bar {
cursor: pointer;
padding: 0 8px;
}
.mcp-server-status {
margin: 1em 0;
padding: 1em;
background-color: var(--background-secondary);
border-radius: 4px;
}
.mcp-button-container {
display: flex;
gap: 8px;
margin: 1em 0;
}
.mcp-button-container button {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
}
.mcp-button-container button:hover {
background-color: var(--interactive-accent-hover);
}
.mcp-connection-info {
margin: 1em 0;
padding: 1em;
background-color: var(--background-secondary);
border-radius: 4px;
}
.mcp-connection-info code {
display: block;
margin: 0.5em 0 1em 0;
padding: 8px;
background-color: var(--background-primary);
border-radius: 4px;
font-family: var(--font-monospace);
}
.mcp-connection-info p {
margin: 0.5em 0 0.25em 0;
font-weight: 600;
}

136
test-client.js Normal file
View File

@@ -0,0 +1,136 @@
#!/usr/bin/env node
/**
* Test client for Obsidian MCP Server
* Usage: node test-client.js [port] [api-key]
*/
const http = require('http');
const PORT = process.argv[2] || 3000;
const API_KEY = process.argv[3] || '';
function makeRequest(method, params = {}) {
return new Promise((resolve, reject) => {
const data = JSON.stringify({
jsonrpc: "2.0",
id: Date.now(),
method,
params
});
const options = {
hostname: '127.0.0.1',
port: PORT,
path: '/mcp',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': data.length
}
};
if (API_KEY) {
options.headers['Authorization'] = `Bearer ${API_KEY}`;
}
const req = http.request(options, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(body));
} catch (e) {
reject(new Error(`Failed to parse response: ${body}`));
}
});
});
req.on('error', reject);
req.write(data);
req.end();
});
}
async function runTests() {
console.log('🧪 Testing Obsidian MCP Server\n');
console.log(`Server: http://127.0.0.1:${PORT}/mcp`);
console.log(`API Key: ${API_KEY ? '***' : 'None'}\n`);
try {
// Test 1: Initialize
console.log('1⃣ Testing initialize...');
const initResponse = await makeRequest('initialize', {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: {
name: "test-client",
version: "1.0.0"
}
});
console.log('✅ Initialize successful');
console.log(' Server:', initResponse.result.serverInfo.name, initResponse.result.serverInfo.version);
console.log(' Protocol:', initResponse.result.protocolVersion);
console.log();
// Test 2: List tools
console.log('2⃣ Testing tools/list...');
const toolsResponse = await makeRequest('tools/list');
console.log('✅ Tools list successful');
console.log(` Found ${toolsResponse.result.tools.length} tools:`);
toolsResponse.result.tools.forEach(tool => {
console.log(` - ${tool.name}: ${tool.description}`);
});
console.log();
// Test 3: Get vault info
console.log('3⃣ Testing get_vault_info...');
const vaultResponse = await makeRequest('tools/call', {
name: 'get_vault_info',
arguments: {}
});
console.log('✅ Vault info successful');
const vaultInfo = JSON.parse(vaultResponse.result.content[0].text);
console.log(' Vault:', vaultInfo.name);
console.log(' Total files:', vaultInfo.totalFiles);
console.log(' Markdown files:', vaultInfo.markdownFiles);
console.log();
// Test 4: List notes
console.log('4⃣ Testing list_notes...');
const listResponse = await makeRequest('tools/call', {
name: 'list_notes',
arguments: {}
});
console.log('✅ List notes successful');
const firstLine = listResponse.result.content[0].text.split('\n')[0];
console.log(' ' + firstLine);
console.log();
// Test 5: Ping
console.log('5⃣ Testing ping...');
const pingResponse = await makeRequest('ping');
console.log('✅ Ping successful');
console.log();
console.log('🎉 All tests passed!');
} catch (error) {
console.error('❌ Test failed:', error.message);
process.exit(1);
}
}
// Check if server is running first
http.get(`http://127.0.0.1:${PORT}/health`, (res) => {
if (res.statusCode === 200) {
runTests();
} else {
console.error('❌ Server health check failed');
process.exit(1);
}
}).on('error', () => {
console.error('❌ Cannot connect to server. Is it running?');
console.error(` Try: http://127.0.0.1:${PORT}/health`);
process.exit(1);
});

25
tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"strict": true,
"moduleResolution": "node",
"importHelpers": false,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"src/**/*.ts"
]
}

101
verify-plugin.sh Normal file
View File

@@ -0,0 +1,101 @@
#!/bin/bash
echo "🔍 Obsidian MCP Server Plugin Verification"
echo "=========================================="
echo ""
# Check if we're in the right directory
if [ ! -f "manifest.json" ]; then
echo "❌ Error: manifest.json not found"
echo " Run this script from the plugin directory"
exit 1
fi
echo "✅ Plugin directory found"
echo ""
# Check required files
echo "📁 Checking required files..."
files=("main.js" "manifest.json" "styles.css")
for file in "${files[@]}"; do
if [ -f "$file" ]; then
size=$(ls -lh "$file" | awk '{print $5}')
echo "$file ($size)"
else
echo "$file (missing)"
fi
done
echo ""
# Check manifest.json content
echo "📋 Checking manifest.json..."
if command -v jq &> /dev/null; then
echo " ID: $(jq -r '.id' manifest.json)"
echo " Name: $(jq -r '.name' manifest.json)"
echo " Version: $(jq -r '.version' manifest.json)"
echo " Desktop Only: $(jq -r '.isDesktopOnly' manifest.json)"
else
echo " (install jq for detailed manifest info)"
cat manifest.json
fi
echo ""
# Check if main.js is valid
echo "🔧 Checking main.js..."
if [ -f "main.js" ]; then
# Check if it's minified/bundled
if head -1 main.js | grep -q "GENERATED/BUNDLED"; then
echo " ✅ File appears to be bundled correctly"
else
echo " ⚠️ File may not be bundled correctly"
fi
# Check for export
if grep -q "export" main.js; then
echo " ✅ Contains exports"
else
echo " ⚠️ No exports found (may be an issue)"
fi
else
echo " ❌ main.js not found"
fi
echo ""
# Check node_modules
echo "📦 Checking dependencies..."
if [ -d "node_modules" ]; then
echo " ✅ node_modules exists"
if [ -d "node_modules/express" ]; then
echo " ✅ express installed"
else
echo " ❌ express not installed"
fi
if [ -d "node_modules/cors" ]; then
echo " ✅ cors installed"
else
echo " ❌ cors not installed"
fi
else
echo " ❌ node_modules not found - run 'npm install'"
fi
echo ""
# Summary
echo "📊 Summary"
echo "=========="
if [ -f "main.js" ] && [ -f "manifest.json" ] && [ -f "styles.css" ]; then
echo "✅ All required files present"
echo ""
echo "Next steps:"
echo "1. Restart Obsidian"
echo "2. Go to Settings → Community Plugins"
echo "3. Enable 'MCP Server'"
echo "4. Check for errors in Console (Ctrl+Shift+I)"
else
echo "❌ Some required files are missing"
echo ""
echo "To fix:"
echo "1. Run: npm install"
echo "2. Run: npm run build"
echo "3. Restart Obsidian"
fi

17
version-bump.mjs Normal file
View File

@@ -0,0 +1,17 @@
import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
const manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json with target version and minAppVersion from manifest.json
// but only if the target version is not already in versions.json
const versions = JSON.parse(readFileSync('versions.json', 'utf8'));
if (!Object.values(versions).includes(minAppVersion)) {
versions[targetVersion] = minAppVersion;
writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));
}

3
versions.json Normal file
View File

@@ -0,0 +1,3 @@
{
"1.0.0": "0.15.0"
}