Skip to main content
Glama

Obsidian Semantic MCP Server

obsidian-api.ts•8.57 kB
import axios, { AxiosInstance } from 'axios'; import https from 'https'; import { ObsidianConfig, ObsidianFile, ObsidianFileResponse } from '../types/obsidian.js'; import { limitSearchResults, DEFAULT_LIMITER_CONFIG } from './response-limiter.js'; import { isImageFile as checkIsImageFile, processImageResponse } from './image-handler.js'; export class ObsidianAPI { private client: AxiosInstance; private config: ObsidianConfig; constructor(config: ObsidianConfig) { this.config = config; // Default to HTTPS on port 27124 if no URL specified const baseURL = config.apiUrl || 'https://localhost:27124'; // Create axios instance with HTTPS agent that accepts self-signed certificates this.client = axios.create({ baseURL, headers: { 'Authorization': `Bearer ${config.apiKey}` }, // Accept self-signed certificates for HTTPS httpsAgent: baseURL.startsWith('https') ? new https.Agent({ rejectUnauthorized: false }) : undefined }); } // Server info async getServerInfo() { const response = await this.client.get('/'); return response.data; } // Active file operations async getActiveFile(): Promise<ObsidianFile> { const response = await this.client.get('/active'); return response.data; } async updateActiveFile(content: string) { const response = await this.client.put('/active', content, { headers: { 'Content-Type': 'text/plain' } }); return response.data; } async appendToActiveFile(content: string) { const response = await this.client.post('/active', content, { headers: { 'Content-Type': 'text/plain' } }); return response.data; } async deleteActiveFile() { const response = await this.client.delete('/active'); return response.data; } async patchActiveFile(params: { operation: 'append' | 'prepend' | 'replace'; targetType: 'heading' | 'block' | 'frontmatter'; target: string; targetDelimiter?: string; trimTargetWhitespace?: boolean; content: string; contentType?: 'text/markdown' | 'application/json'; }) { const headers: Record<string, string> = { 'Operation': params.operation, 'Target-Type': params.targetType, 'Target': params.target, 'Create-Target-If-Missing': 'true' }; if (params.targetDelimiter) { headers['Target-Delimiter'] = params.targetDelimiter; } if (params.trimTargetWhitespace !== undefined) { headers['Trim-Target-Whitespace'] = params.trimTargetWhitespace.toString(); } if (params.contentType) { headers['Content-Type'] = params.contentType; } const response = await this.client.patch('/active/', params.content, { headers }); return response.data; } // Vault file operations async listFiles(directory?: string) { const path = directory ? `/vault/${directory}/` : '/vault/'; try { const response = await this.client.get(path); return response.data.files || []; } catch (error: any) { // Check if it's a 404 error if (error.response?.status === 404) { const dirName = directory || 'root'; throw new Error( `Directory not found: ${dirName}. ` + `Please check that the directory exists in your vault. ` + `Try 'vault(action="list")' to see available directories, or ` + `'vault(action="list", directory="<parent-directory>")' to browse the parent directory.` ); } // Check for connection errors if (error.code === 'ECONNREFUSED') { throw new Error( 'Cannot connect to Obsidian. Please ensure:\n' + '1. Obsidian is running\n' + '2. The Local REST API plugin is installed and enabled\n' + '3. The API is accessible at the configured URL' ); } // Re-throw other errors with original message throw error; } } async getFile(path: string): Promise<ObsidianFileResponse> { // Check if the file is an image based on extension if (checkIsImageFile(path)) { // For images, request raw binary data const response = await this.client.get(`/vault/${path}`, { responseType: 'arraybuffer', headers: { 'Accept': '*/*' } }); return await processImageResponse(response, path); } else { // For text files, use the existing logic const response = await this.client.get(`/vault/${path}`, { headers: { 'Accept': 'application/vnd.olrapi.note+json' } }); return response.data; } } async createFile(path: string, content: string) { const response = await this.client.put(`/vault/${path}`, content, { headers: { 'Content-Type': 'text/plain' } }); return response.data; } async updateFile(path: string, content: string) { const response = await this.client.put(`/vault/${path}`, content, { headers: { 'Content-Type': 'text/plain' } }); return response.data; } async deleteFile(path: string) { const response = await this.client.delete(`/vault/${path}`); return response.data; } async appendToFile(path: string, content: string) { const response = await this.client.post(`/vault/${path}`, content, { headers: { 'Content-Type': 'text/plain' } }); return response.data; } async patchVaultFile(path: string, params: { operation: 'append' | 'prepend' | 'replace'; targetType: 'heading' | 'block' | 'frontmatter'; target: string; targetDelimiter?: string; trimTargetWhitespace?: boolean; content: string; contentType?: 'text/markdown' | 'application/json'; }) { const headers: Record<string, string> = { 'Operation': params.operation, 'Target-Type': params.targetType, 'Target': params.target, 'Create-Target-If-Missing': 'true' }; if (params.targetDelimiter) { headers['Target-Delimiter'] = params.targetDelimiter; } if (params.trimTargetWhitespace !== undefined) { headers['Trim-Target-Whitespace'] = params.trimTargetWhitespace.toString(); } if (params.contentType) { headers['Content-Type'] = params.contentType; } const response = await this.client.patch(`/vault/${encodeURIComponent(path)}`, params.content, { headers }); return response.data; } // Search operations async searchSimple(query: string) { try { const response = await this.client.post('/search/simple', null, { params: { query } }); return response.data; } catch (error: any) { if (error.code === 'ECONNREFUSED') { throw new Error('Cannot connect to Obsidian. Is the Local REST API plugin running?'); } throw error; } } // Open file in Obsidian async openFile(path: string) { const response = await this.client.post(`/open/${encodeURIComponent(path)}`); return response.data; } // Commands async getCommands() { const response = await this.client.get('/commands/'); return response.data; } async executeCommand(commandId: string) { const response = await this.client.post(`/commands/${encodeURIComponent(commandId)}/`); return response.data; } // Search with pagination async searchPaginated(query: string, page: number = 1, pageSize: number = 10) { // Since the API doesn't support pagination natively, we'll implement it client-side const allResults = await this.searchSimple(query); if (!allResults || !Array.isArray(allResults)) { return { query, page, pageSize, totalResults: 0, totalPages: 0, results: [], method: 'api' }; } // First limit the results to prevent token overflow const { results: limitedResults, truncated, originalCount } = limitSearchResults(allResults); const totalResults = limitedResults.length; const totalPages = Math.ceil(totalResults / pageSize); const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; return { query, page, pageSize, totalResults, totalPages, results: limitedResults.slice(startIndex, endIndex), method: 'api', // Add metadata about truncation ...(truncated && { truncated: true, originalResultCount: originalCount, message: `Results limited to prevent token overflow. Showing ${limitedResults.length} of ${originalCount} results.` }) }; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aaronsb/obsidian-semantic-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server