#!/usr/bin/env node /** * Obsidian Vault Headless REST API Server * Reads vault files directly without requiring Obsidian Desktop app * Provides REST API compatible with obsidian-mcp-server expectations */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); // Simple HTTP server (no dependencies needed) const http = require('http'); const VAULT_PATH = process.env.VAULT_PATH || '/workspace/second-brain'; const PORT = process.env.PORT || 27123; const API_KEY = process.env.OBSIDIAN_API_KEY || 'default-api-key-set-env-var'; console.log(`šŸ“š Obsidian Vault Server`); console.log(`Vault: ${VAULT_PATH}`); console.log(`Port: ${PORT}`); console.log(`API Key: ${API_KEY.slice(0, 8)}...`); // Authenticate requests const authenticateRequest = (req) => { const auth = req.headers['authorization']; if (!auth) return false; const token = auth.replace('Bearer ', ''); return token === API_KEY; }; // Read file safely const readFile = (filePath) => { const fullPath = path.join(VAULT_PATH, filePath); // Prevent directory traversal if (!fullPath.startsWith(VAULT_PATH)) { return null; } try { return fs.readFileSync(fullPath, 'utf8'); } catch (e) { return null; } }; // List files in directory const listDir = (dirPath) => { const fullPath = path.join(VAULT_PATH, dirPath); if (!fullPath.startsWith(VAULT_PATH)) { return []; } try { const entries = fs.readdirSync(fullPath, { withFileTypes: true }); return entries .filter(e => !e.name.startsWith('.')) .map(e => ({ name: e.name, isDirectory: e.isDirectory() })); } catch (e) { return []; } }; // Search files const searchVault = (query) => { const results = []; const search = (dir) => { try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.name.startsWith('.')) continue; const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { search(fullPath); } else if (entry.name.endsWith('.md')) { try { const content = fs.readFileSync(fullPath, 'utf8'); if (content.includes(query)) { const relativePath = path.relative(VAULT_PATH, fullPath); results.push({ file: relativePath, match: content.substring(0, 200) }); } } catch (e) { // Skip unreadable files } } } } catch (e) { // Skip inaccessible directories } }; search(VAULT_PATH); return results; }; // Simple JSON response const jsonResponse = (res, statusCode, data) => { res.writeHead(statusCode, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data, null, 2)); }; // Create server const server = http.createServer((req, res) => { // CORS res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } // Authenticate if (!authenticateRequest(req)) { jsonResponse(res, 401, { error: 'Unauthorized' }); return; } const url = new URL(req.url, `http://${req.headers.host}`); const pathname = url.pathname; // Routes if (pathname === '/' && req.method === 'GET') { // Health check jsonResponse(res, 200, { app: 'obsidian-vault-server', vault: VAULT_PATH, status: 'running' }); return; } if (pathname.startsWith('/vault/read') && req.method === 'GET') { // Read file const filePath = url.searchParams.get('path'); if (!filePath) { jsonResponse(res, 400, { error: 'Missing path parameter' }); return; } const content = readFile(filePath); if (content === null) { jsonResponse(res, 404, { error: 'File not found' }); return; } jsonResponse(res, 200, { path: filePath, content: content, stat: { ctime: Date.now(), mtime: Date.now(), size: content.length } }); return; } if (pathname.startsWith('/vault/search') && req.method === 'GET') { // Search vault const query = url.searchParams.get('query'); if (!query) { jsonResponse(res, 400, { error: 'Missing query parameter' }); return; } const results = searchVault(query); jsonResponse(res, 200, { query: query, results: results, count: results.length }); return; } if (pathname.startsWith('/vault/list') && req.method === 'GET') { // List directory const dirPath = url.searchParams.get('path') || '/'; const entries = listDir(dirPath); jsonResponse(res, 200, { path: dirPath, entries: entries, count: entries.length }); return; } // 404 jsonResponse(res, 404, { error: 'Not found' }); }); server.listen(PORT, () => { console.log(`āœ… Vault server listening on http://127.0.0.1:${PORT}`); console.log(`\nEndpoints:`); console.log(` GET / - Health check`); console.log(` GET /vault/read?path=file.md - Read file`); console.log(` GET /vault/list?path=/ - List directory`); console.log(` GET /vault/search?query=text - Search vault`); console.log(`\nAll requests require: Authorization: Bearer `); }); server.on('error', (err) => { console.error('āŒ Server error:', err); process.exit(1); }); // Graceful shutdown process.on('SIGINT', () => { console.log('\nšŸ‘‹ Shutting down...'); server.close(() => { console.log('Goodbye!'); process.exit(0); }); });