UNPKG

obsidian-local-rest-api-mcp

Version:

AI-Native MCP server for Obsidian vaults with task-oriented, intelligent tools designed for LLM workflows

510 lines (505 loc) 20.2 kB
#!/usr/bin/env node import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; // Handle CLI arguments const args = process.argv.slice(2); if (args.includes('--version') || args.includes('-v')) { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const packagePath = join(__dirname, '..', 'package.json'); const packageJson = JSON.parse(readFileSync(packagePath, 'utf8')); console.log(packageJson.version); process.exit(0); } if (args.includes('--help') || args.includes('-h')) { console.log(` Obsidian Local REST API MCP Server Usage: obsidian-local-rest-api-mcp [options] Options: -v, --version Show version number -h, --help Show help Environment Variables: OBSIDIAN_API_URL Base URL for Obsidian REST API (default: http://obsidian-local-rest-api.test) OBSIDIAN_API_KEY Optional bearer token for authentication This is an MCP server that communicates via stdio. It should be configured in your MCP client (like Claude Desktop) rather than run directly. Example Claude Desktop configuration: { "mcpServers": { "obsidian-vault": { "command": "npx", "args": ["obsidian-local-rest-api-mcp"], "env": { "OBSIDIAN_API_URL": "http://localhost:8000" } } } } `); process.exit(0); } import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; // Configuration schema const ConfigSchema = z.object({ baseUrl: z.string().url().default("http://obsidian-local-rest-api.test"), apiKey: z.string().optional(), }); // API client for Obsidian REST API class ObsidianApiClient { baseUrl; headers; constructor(config) { this.baseUrl = `${config.baseUrl}/api`; this.headers = { "Content-Type": "application/json", "Accept": "application/json", }; if (config.apiKey) { this.headers.Authorization = `Bearer ${config.apiKey}`; } } async request(path, options = {}) { const url = `${this.baseUrl}${path}`; const response = await fetch(url, { ...options, headers: { ...this.headers, ...options.headers, }, }); if (!response.ok) { throw new Error(`API request failed: ${response.status} ${response.statusText}`); } return response.json(); } // Enhanced API client methods for AI-native operations // Directory operations async listDirectory(path = ".", recursive = false, limit = 50, offset = 0) { const params = new URLSearchParams({ path, recursive: recursive.toString(), limit: limit.toString(), offset: offset.toString(), }); return this.request(`/vault/directory?${params}`); } // File operations async readFile(path) { return this.request(`/files/${encodeURIComponent(path)}`); } async writeFile(path, content, mode = "overwrite") { return this.request("/files/write", { method: "POST", body: JSON.stringify({ path, content, mode }), }); } async deleteItem(path) { return this.request(`/files/${encodeURIComponent(path)}`, { method: "DELETE", }); } // AI-native note operations async createOrUpdateNote(path, content, frontmatter = {}) { return this.request("/notes/upsert", { method: "POST", body: JSON.stringify({ path, content, front_matter: frontmatter }), }); } async getDailyNote(date = "today") { const params = new URLSearchParams({ date }); return this.request(`/vault/notes/daily?${params}`); } async getRecentNotes(limit = 5) { const params = new URLSearchParams({ limit: limit.toString() }); return this.request(`/vault/notes/recent?${params}`); } // Enhanced search async searchVault(query, scope = ["content", "filename", "tags"], pathFilter) { const params = new URLSearchParams({ query, scope: scope.join(","), }); if (pathFilter) { params.append("path_filter", pathFilter); } return this.request(`/vault/search?${params}`); } async findRelatedNotes(path, on = ["tags", "links"]) { const params = new URLSearchParams({ on: on.join(","), }); return this.request(`/vault/notes/related/${encodeURIComponent(path)}?${params}`); } // Legacy methods for backward compatibility async listFiles() { return this.request("/files"); } async getFile(path) { return this.request(`/files/${encodeURIComponent(path)}`); } async createFile(path, content, type = "file") { return this.request("/files", { method: "POST", body: JSON.stringify({ path, content, type }), }); } async updateFile(path, content) { return this.request(`/files/${encodeURIComponent(path)}`, { method: "PUT", body: JSON.stringify({ content }), }); } async deleteFile(path) { return this.request(`/files/${encodeURIComponent(path)}`, { method: "DELETE", }); } // Notes endpoints async listNotes() { return this.request("/notes"); } async getNote(path) { return this.request(`/notes/${encodeURIComponent(path)}`); } async createNote(path, content, frontmatter) { const body = { path, content }; if (frontmatter) { body.frontmatter = frontmatter; } return this.request("/notes", { method: "POST", body: JSON.stringify(body), }); } async updateNote(path, content, frontmatter) { const body = {}; if (content !== undefined) body.content = content; if (frontmatter !== undefined) body.frontmatter = frontmatter; return this.request(`/notes/${encodeURIComponent(path)}`, { method: "PATCH", body: JSON.stringify(body), }); } async deleteNote(path) { return this.request(`/notes/${encodeURIComponent(path)}`, { method: "DELETE", }); } // Search notes async searchNotes(query) { return this.request(`/notes?search=${encodeURIComponent(query)}`); } // Metadata endpoints async getMetadataKeys() { return this.request("/metadata/keys"); } async getMetadataValues(key) { return this.request(`/metadata/values/${encodeURIComponent(key)}`); } } // MCP Server implementation class ObsidianMcpServer { server; client; constructor(config) { this.client = new ObsidianApiClient(config); this.server = new Server({ name: "obsidian-vault-mcp", version: "1.0.0", }, { capabilities: { tools: {}, }, }); this.setupTools(); } setupTools() { // AI-Native Tools Definition this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ // Directory Operations { name: "list_directory", description: "List directory contents with pagination to prevent context overflow. Shows immediate contents by default.", inputSchema: { type: "object", properties: { path: { type: "string", description: "Directory path to list", default: "." }, recursive: { type: "boolean", description: "Include subdirectories recursively", default: false }, limit: { type: "number", description: "Maximum items to return", default: 50 }, offset: { type: "number", description: "Pagination offset", default: 0 }, }, }, }, // File Operations { name: "read_file", description: "Read content of a specific file from the vault", inputSchema: { type: "object", properties: { path: { type: "string", description: "Path to the file" }, }, required: ["path"], }, }, { name: "write_file", description: "Write file content with different modes: overwrite (default), append, or prepend. Handles both create and update operations.", inputSchema: { type: "object", properties: { path: { type: "string", description: "Path to the file" }, content: { type: "string", description: "Content to write" }, mode: { type: "string", enum: ["overwrite", "append", "prepend"], description: "Write mode", default: "overwrite" }, }, required: ["path", "content"], }, }, { name: "delete_item", description: "Delete a file or directory from the vault", inputSchema: { type: "object", properties: { path: { type: "string", description: "Path to the item to delete" }, }, required: ["path"], }, }, // AI-Native Note Operations { name: "create_or_update_note", description: "Create or update a note with content and frontmatter. Performs upsert operation - creates if doesn't exist, updates if it does.", inputSchema: { type: "object", properties: { path: { type: "string", description: "Path for the note (without .md extension)" }, content: { type: "string", description: "Note content" }, frontmatter: { type: "object", description: "Frontmatter metadata", default: {} }, }, required: ["path", "content"], }, }, { name: "get_daily_note", description: "Get daily note for a specific date. Handles common daily note naming conventions and file locations.", inputSchema: { type: "object", properties: { date: { type: "string", description: "Date (today, yesterday, tomorrow, or YYYY-MM-DD)", default: "today" }, }, }, }, { name: "get_recent_notes", description: "Get recently modified notes, ordered by modification time", inputSchema: { type: "object", properties: { limit: { type: "number", description: "Number of recent notes to return", default: 5 }, }, }, }, // Enhanced Search and Discovery { name: "search_vault", description: "Search vault content across files, filenames, and metadata with advanced filtering", inputSchema: { type: "object", properties: { query: { type: "string", description: "Search query" }, scope: { type: "array", items: { type: "string", enum: ["content", "filename", "tags"] }, description: "Search scope - where to look for the query", default: ["content", "filename", "tags"] }, path_filter: { type: "string", description: "Limit search to specific path prefix" }, }, required: ["query"], }, }, { name: "find_related_notes", description: "Find notes related to a given note based on shared tags, links, or backlinks", inputSchema: { type: "object", properties: { path: { type: "string", description: "Path to the source note" }, on: { type: "array", items: { type: "string", enum: ["tags", "links"] }, description: "Relationship criteria to use for finding related notes", default: ["tags", "links"] }, }, required: ["path"], }, }, // Legacy Tools (for backward compatibility) { name: "get_note", description: "Get a specific note with its content and metadata (legacy)", inputSchema: { type: "object", properties: { path: { type: "string", description: "Path to the note" }, }, required: ["path"], }, }, { name: "list_notes", description: "List all notes in the vault with optional search filter (legacy with search support)", inputSchema: { type: "object", properties: { search: { type: "string", description: "Optional search query to filter notes" }, }, }, }, { name: "get_metadata_keys", description: "Get all available frontmatter keys from notes", inputSchema: { type: "object", properties: {}, }, }, { name: "get_metadata_values", description: "Get all unique values for a specific frontmatter key", inputSchema: { type: "object", properties: { key: { type: "string", description: "Frontmatter key" }, }, required: ["key"], }, }, ], })); // Tool call handler this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { let result; switch (name) { // AI-Native Tools case "list_directory": result = await this.client.listDirectory(args?.path, args?.recursive, args?.limit, args?.offset); break; case "read_file": result = await this.client.readFile(args?.path); break; case "write_file": result = await this.client.writeFile(args?.path, args?.content, args?.mode); break; case "delete_item": result = await this.client.deleteItem(args?.path); break; case "create_or_update_note": result = await this.client.createOrUpdateNote(args?.path, args?.content, args?.frontmatter); break; case "get_daily_note": result = await this.client.getDailyNote(args?.date); break; case "get_recent_notes": result = await this.client.getRecentNotes(args?.limit); break; case "search_vault": result = await this.client.searchVault(args?.query, args?.scope, args?.path_filter); break; case "find_related_notes": result = await this.client.findRelatedNotes(args?.path, args?.on); break; // Legacy Tools (backward compatibility) case "list_notes": const searchQuery = args?.search; if (searchQuery) { // Use the enhanced search functionality result = await this.client.searchVault(searchQuery, ["content", "filename", "tags"]); } else { result = await this.client.listNotes(); } break; case "get_note": result = await this.client.getNote(args?.path); break; case "get_metadata_keys": result = await this.client.getMetadataKeys(); break; case "get_metadata_values": result = await this.client.getMetadataValues(args?.key); break; default: throw new Error(`Unknown tool: ${name}`); } return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); } async start() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Obsidian MCP Server running on stdio"); // Keep the process alive const keepAlive = () => { setTimeout(keepAlive, 1000); }; keepAlive(); } } // Main function async function main() { // Read configuration from environment variables const config = ConfigSchema.parse({ baseUrl: process.env.OBSIDIAN_API_URL || "http://obsidian-local-rest-api.test", apiKey: process.env.OBSIDIAN_API_KEY, }); const mcpServer = new ObsidianMcpServer(config); await mcpServer.start(); } // Error handling process.on('SIGINT', () => { process.exit(0); }); process.on('SIGTERM', () => { process.exit(0); }); // Run the server if this file is executed directly main().catch((error) => { console.error("Fatal error:", error); process.exit(1); }); //# sourceMappingURL=index.js.map