Release v1.0.0 - Initial Release

🎉 Initial release of Obsidian MCP Server plugin

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

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

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

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

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

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

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

@@ -0,0 +1,149 @@
import { App } from 'obsidian';
import { Tool, CallToolResult } from '../types/mcp-types';
import { NoteTools } from './note-tools';
import { VaultTools } from './vault-tools';
export class ToolRegistry {
private noteTools: NoteTools;
private vaultTools: VaultTools;
constructor(app: App) {
this.noteTools = new NoteTools(app);
this.vaultTools = new VaultTools(app);
}
getToolDefinitions(): Tool[] {
return [
{
name: "read_note",
description: "Read the content of a note from the Obsidian vault",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Path to the note within the vault (e.g., 'folder/note.md')"
}
},
required: ["path"]
}
},
{
name: "create_note",
description: "Create a new note in the Obsidian vault",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Path for the new note (e.g., 'folder/note.md')"
},
content: {
type: "string",
description: "Content of the note"
}
},
required: ["path", "content"]
}
},
{
name: "update_note",
description: "Update an existing note in the Obsidian vault",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Path to the note to update"
},
content: {
type: "string",
description: "New content for the note"
}
},
required: ["path", "content"]
}
},
{
name: "delete_note",
description: "Delete a note from the Obsidian vault",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Path to the note to delete"
}
},
required: ["path"]
}
},
{
name: "search_notes",
description: "Search for notes in the Obsidian vault",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query string"
}
},
required: ["query"]
}
},
{
name: "get_vault_info",
description: "Get information about the Obsidian vault",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "list_notes",
description: "List all notes in the vault or in a specific folder",
inputSchema: {
type: "object",
properties: {
folder: {
type: "string",
description: "Optional folder path to list notes from"
}
}
}
}
];
}
async callTool(name: string, args: any): Promise<CallToolResult> {
try {
switch (name) {
case "read_note":
return await this.noteTools.readNote(args.path);
case "create_note":
return await this.noteTools.createNote(args.path, args.content);
case "update_note":
return await this.noteTools.updateNote(args.path, args.content);
case "delete_note":
return await this.noteTools.deleteNote(args.path);
case "search_notes":
return await this.vaultTools.searchNotes(args.query);
case "get_vault_info":
return await this.vaultTools.getVaultInfo();
case "list_notes":
return await this.vaultTools.listNotes(args.folder);
default:
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
isError: true
};
}
} catch (error) {
return {
content: [{ type: "text", text: `Error: ${(error as Error).message}` }],
isError: true
};
}
}
}

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

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

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

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