# Research: Obsidian MCP Bundle **Phase**: 0 (Outline & Research) **Date**: 2026-03-22 **Purpose**: Resolve technical unknowns and establish implementation patterns ## MCP SDK Integration (@modelcontextprotocol/sdk) ### Decision Use `@modelcontextprotocol/sdk` (official TypeScript SDK) version ^1.0.0 with stdio transport ### Rationale - Official SDK ensures protocol compliance and future compatibility - TypeScript support provides type safety for tool schemas and message handling - Stdio transport is the standard for local MCP servers (matches constitution requirement) - Active maintenance by Anthropic with clear documentation ### Alternatives Considered - **Implement MCP protocol from scratch**: Rejected - high risk of protocol violations, maintenance burden - **Python MCP SDK**: Rejected - Node.js preferred per MCPB guidelines (ships with Claude Desktop) ### Implementation Pattern ```typescript import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; const server = new Server({ name: "obsidian-mcp", version: "1.0.0" }, { capabilities: { tools: {} } }); // Register tools server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [/* tool definitions */] })); server.setRequestHandler(CallToolRequestSchema, async (request) => ({ /* tool execution */ })); // Start stdio transport const transport = new StdioServerTransport(); await server.connect(transport); ``` ## Obsidian CLI Execution Strategy ### Decision Use Node.js `child_process.spawn` with timeout wrapper and output streaming ### Rationale - `spawn` provides real-time output streaming (vs `exec` buffering) - Timeout wrapper enforces 30s limit per FR-018 - stderr/stdout separation enables proper logging (stderr to our stderr, stdout parsing) - Cross-platform compatible (Windows, macOS, Linux) ### Alternatives Considered - **exec/execSync**: Rejected - buffers entire output (problematic for large results), no streaming - **Shell scripts wrapper**: Rejected - adds complexity, cross-platform issues ### Implementation Pattern ```typescript import { spawn } from 'child_process'; async function executeObsidianCLI( args: string[], timeout: number = 30000 ): Promise<{ stdout: string; stderr: string }> { return new Promise((resolve, reject) => { const proc = spawn('obsidian', args); let stdout = ''; let stderr = ''; const timer = setTimeout(() => { proc.kill(); reject(new Error(`Command timeout after ${timeout}ms`)); }, timeout); proc.stdout.on('data', (data) => stdout += data); proc.stderr.on('data', (data) => { stderr += data; // Forward CLI stderr to our stderr process.stderr.write(data); }); proc.on('close', (code) => { clearTimeout(timer); if (code === 0) resolve({ stdout, stderr }); else reject(new Error(`CLI exited with code ${code}: ${stderr}`)); }); }); } ``` ## Input Validation Strategy ### Decision Use Zod for runtime schema validation with automatic type inference ### Rationale - Zod provides runtime validation matching TypeScript types - Clear error messages for validation failures (meets FR-017 requirement) - Integrates well with MCP SDK tool schema definitions - Can sanitize inputs (strip dangerous characters) before CLI execution ### Alternatives Considered - **Manual validation**: Rejected - error-prone, verbose, hard to maintain - **JSON Schema + AJV**: Rejected - less TypeScript integration, more boilerplate ### Implementation Pattern ```typescript import { z } from 'zod'; const CreateNoteSchema = z.object({ name: z.string().min(1).max(255), path: z.string().optional(), content: z.string().optional(), overwrite: z.boolean().optional() }); type CreateNoteParams = z.infer; async function createNote(params: unknown) { const validated = CreateNoteSchema.parse(params); // Throws if invalid // validated is now type-safe CreateNoteParams } ``` ## Error Handling & User Feedback ### Decision Map CLI errors to structured MCP error responses with actionable messages ### Rationale - MCP protocol supports error objects with code, message, data fields - SC-004 requires 90% of errors to be self-resolvable - Clarification specifies Obsidian-not-running should give clear instruction ### Error Categories 1. **Obsidian not running**: "Obsidian application is not running. Please start Obsidian and try again." 2. **Vault not found**: "Vault '{name}' not found. Check your vault name in MCP settings." 3. **File not found**: "Note '{name}' not found. Use exact path or check spelling." 4. **Ambiguous name**: "Multiple notes named '{name}' found: {paths}. Please specify exact path." 5. **Permission denied**: "Cannot access {path}. Check file permissions." 6. **CLI timeout**: "Operation took too long (>30s). Try with smaller scope or check Obsidian performance." ### Implementation Pattern ```typescript class ObsidianError extends Error { constructor( public code: string, message: string, public data?: unknown ) { super(message); } } function mapCLIError(stderr: string, exitCode: number): ObsidianError { if (stderr.includes('not running')) { return new ObsidianError( 'OBSIDIAN_NOT_RUNNING', 'Obsidian application is not running. Please start Obsidian and try again.' ); } // ... other mappings } ``` ## CLI Output Parsing ### Decision Support JSON, TSV, CSV formats with fallback to text parsing ### Rationale - Obsidian CLI supports multiple output formats per FR-025 - JSON is preferred (structured, no parsing ambiguity) - TSV/CSV needed for some commands that default to tabular - Text fallback for commands without format options ### Parsing Strategy ```typescript async function parseOutput(stdout: string, format: 'json' | 'tsv' | 'csv' | 'text') { switch (format) { case 'json': return JSON.parse(stdout); case 'tsv': return parseTSV(stdout); // split by tabs and newlines case 'csv': return parseCSV(stdout); // handle quoted values case 'text': return { raw: stdout }; } } ``` ## Logging & Debugging ### Decision Structured logging to stderr with sanitization (per clarification) ### Rationale - FR-020 mandates stderr-only logging - Clarification specifies: operation type, sanitized params, timestamp, success/failure - Helps debugging without violating MCP protocol (stdout must be pure JSON-RPC) ### Implementation Pattern ```typescript interface LogEntry { timestamp: string; operation: string; params: Record; // sanitized status: 'success' | 'failure'; duration?: number; error?: string; } function log(entry: LogEntry) { // Remove sensitive data const sanitized = { ...entry, params: sanitizeParams(entry.params) }; process.stderr.write(JSON.stringify(sanitized) + '\n'); } function sanitizeParams(params: Record) { const safe = { ...params }; // Remove vault paths, note content, etc. if (safe.content) safe.content = ''; if (safe.path) safe.path = ''; return safe; } ``` ## Tool Organization Strategy ### Decision Group tools by functional category matching user stories ### Rationale - Aligns implementation with prioritized user stories (P1-P5) - Each category can be developed/tested independently - Clear separation of concerns ### Tool Categories (95 tools total based on Obsidian CLI) 1. **File Operations (P1)**: ~15 tools - create, read, append, prepend, delete, move, rename, open, file info 2. **Search & Discovery (P2)**: ~20 tools - search, search:context, backlinks, links, unresolved, tags, aliases, properties 3. **Tasks & Properties (P3)**: ~15 tools - tasks list, task toggle, task update, property get/set/remove, properties list 4. **Vault Navigation (P4)**: ~15 tools - files, folders, vault info, recents, outline, wordcount 5. **Advanced Features (P5)**: ~30 tools - daily notes, templates, bookmarks, plugins, themes, history, sync, base queries ## Testing Strategy ### Decision Jest with integration tests against real Obsidian CLI + unit tests for parsing/validation ### Rationale - Jest is standard for Node.js/TypeScript projects - Integration tests validate actual Obsidian CLI behavior - Unit tests ensure error handling and edge cases ### Test Approach ```typescript describe('File Operations', () => { it('creates a note successfully', async () => { const result = await callTool('create', { name: 'Test Note', content: 'Test content' }); expect(result.success).toBe(true); }); it('returns error when Obsidian not running', async () => { // Mock CLI to simulate not running await expect(callTool('read', { file: 'test' })) .rejects.toThrow('Obsidian application is not running'); }); }); ``` ## Manifest.json Structure ### Decision Follow MCPB spec v0.3 with user_config for vault selection ### Rationale - FR-021 requires valid manifest per MCPB spec - FR-022 requires vault configuration - Constitution principle II mandates manifest integrity ### Manifest Template ```json { "manifest_version": "0.3", "name": "obsidian-mcp", "display_name": "Obsidian MCP Bundle", "version": "1.0.0", "description": "Expose Obsidian CLI to AI assistants via MCP", "author": { "name": "Author Name" }, "server": { "type": "node", "entry_point": "dist/index.js", "mcp_config": { "command": "node", "args": ["${__dirname}/dist/index.js"], "env": { "OBSIDIAN_VAULT": "${user_config.vault_name}" } } }, "user_config": { "vault_name": { "type": "string", "title": "Vault Name", "description": "Name of your Obsidian vault to target", "required": true } }, "tools_generated": false, "compatibility": { "claude_desktop": ">=1.0.0", "platforms": ["darwin", "win32", "linux"], "runtimes": { "node": ">=18.0.0" } } } ``` ## Build & Bundling ### Decision TypeScript compilation to dist/, bundle node_modules with esbuild ### Rationale - TypeScript provides type safety and better developer experience - esbuild for fast bundling and tree-shaking - Bundle node_modules ensures no external dependencies needed ### Build Process ```json { "scripts": { "build": "tsc && esbuild dist/index.js --bundle --platform=node --outfile=dist/bundle.js", "pack": "mcpb pack", "test": "jest" } } ``` ## Summary All technical unknowns resolved. Ready to proceed to Phase 1 design with: - MCP SDK integration pattern established - CLI execution strategy defined - Error handling mapped to user-friendly messages - Input validation approach confirmed - Logging strategy aligned with constitution - Tool organization matching user story priorities - Testing framework selected - Manifest structure compliant with MCPB spec v0.3