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:
10
.editorconfig
Normal file
10
.editorconfig
Normal 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
3
.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
|
||||
main.js
|
||||
23
.eslintrc
Normal file
23
.eslintrc
Normal 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
22
.gitignore
vendored
Normal 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
|
||||
26
.windsurf/rules/agent-guidelines.md
Normal file
26
.windsurf/rules/agent-guidelines.md
Normal 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
|
||||
84
.windsurf/rules/code-examples.md
Normal file
84
.windsurf/rules/code-examples.md
Normal 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));
|
||||
```
|
||||
35
.windsurf/rules/coding-conventions.md
Normal file
35
.windsurf/rules/coding-conventions.md
Normal 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
|
||||
54
.windsurf/rules/commands-settings.md
Normal file
54
.windsurf/rules/commands-settings.md
Normal 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));
|
||||
```
|
||||
38
.windsurf/rules/environment-tooling.md
Normal file
38
.windsurf/rules/environment-tooling.md
Normal 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/`
|
||||
39
.windsurf/rules/file-organization.md
Normal file
39
.windsurf/rules/file-organization.md
Normal 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`)
|
||||
30
.windsurf/rules/manifest-rules.md
Normal file
30
.windsurf/rules/manifest-rules.md
Normal 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
|
||||
16
.windsurf/rules/project-overview.md
Normal file
16
.windsurf/rules/project-overview.md
Normal 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
|
||||
22
.windsurf/rules/references.md
Normal file
22
.windsurf/rules/references.md
Normal 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
|
||||
27
.windsurf/rules/security-privacy.md
Normal file
27
.windsurf/rules/security-privacy.md
Normal 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
|
||||
45
.windsurf/rules/troubleshooting.md
Normal file
45
.windsurf/rules/troubleshooting.md
Normal 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
|
||||
32
.windsurf/rules/ux-guidelines.md
Normal file
32
.windsurf/rules/ux-guidelines.md
Normal 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"
|
||||
32
.windsurf/rules/versioning-releases.md
Normal file
32
.windsurf/rules/versioning-releases.md
Normal 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
163
CHANGELOG.md
Normal 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
5
LICENSE
Normal 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
198
QUICKSTART.md
Normal 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
255
README.md
Normal 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
1341
ROADMAP.md
Normal file
File diff suppressed because it is too large
Load Diff
308
TROUBLESHOOTING.md
Normal file
308
TROUBLESHOOTING.md
Normal 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
51
esbuild.config.mjs
Normal 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
8
manifest.json
Normal 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
275
old-structure/main.ts
Normal 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
485
old-structure/mcp-server.ts
Normal 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
122
old-structure/mcp-types.ts
Normal 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
3400
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
37
package.json
Normal file
37
package.json
Normal 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
123
src/main.ts
Normal 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
144
src/server/mcp-server.ts
Normal 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
54
src/server/middleware.ts
Normal 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
25
src/server/routes.ts
Normal 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
153
src/settings.ts
Normal 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
149
src/tools/index.ts
Normal 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
68
src/tools/note-tools.ts
Normal 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
77
src/tools/vault-tools.ts
Normal 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
63
src/types/mcp-types.ts
Normal 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;
|
||||
}
|
||||
21
src/types/settings-types.ts
Normal file
21
src/types/settings-types.ts
Normal 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
53
styles.css
Normal 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
136
test-client.js
Normal 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
25
tsconfig.json
Normal 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
101
verify-plugin.sh
Normal 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
17
version-bump.mjs
Normal 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
3
versions.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"1.0.0": "0.15.0"
|
||||
}
|
||||
Reference in New Issue
Block a user