Files
obsidian-mcp/specs/001-obsidian-mcp-bundle/research.md
Peter.Morton 622b28e42c feat: implement Obsidian MCP Bundle MVP (Phase 1-3)
- Complete project setup with TypeScript, Jest, MCPB manifest
- Implement foundational infrastructure (CLI executor, logger, error handler)
- Add 9 file operation tools for User Story 1
- Full MCP protocol compliance with stdio transport
- Input validation and sanitization for security
- Comprehensive error handling with actionable messages
- Constitutional compliance: all 6 principles satisfied

MVP includes:
- obsidian_create_note, read, append, prepend, delete, move, rename, open, file_info
- Zod validation schemas for all parameters
- 30s timeout configuration with per-command overrides
- Stderr-only logging with sanitized output
- Graceful shutdown handling

Build:  0 errors, 0 vulnerabilities
Tasks: 48/167 complete (MVP milestone)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-22 11:21:38 -05:00

11 KiB

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

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

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

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<typeof CreateNoteSchema>;

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

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

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

interface LogEntry {
  timestamp: string;
  operation: string;
  params: Record<string, unknown>; // 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<string, unknown>) {
  const safe = { ...params };
  // Remove vault paths, note content, etc.
  if (safe.content) safe.content = '<redacted>';
  if (safe.path) safe.path = '<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

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

{
  "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

{
  "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