- 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>
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
spawnprovides real-time output streaming (vsexecbuffering)- 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
- Obsidian not running: "Obsidian application is not running. Please start Obsidian and try again."
- Vault not found: "Vault '{name}' not found. Check your vault name in MCP settings."
- File not found: "Note '{name}' not found. Use exact path or check spelling."
- Ambiguous name: "Multiple notes named '{name}' found: {paths}. Please specify exact path."
- Permission denied: "Cannot access {path}. Check file permissions."
- 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)
- File Operations (P1): ~15 tools - create, read, append, prepend, delete, move, rename, open, file info
- Search & Discovery (P2): ~20 tools - search, search:context, backlinks, links, unresolved, tags, aliases, properties
- Tasks & Properties (P3): ~15 tools - tasks list, task toggle, task update, property get/set/remove, properties list
- Vault Navigation (P4): ~15 tools - files, folders, vault info, recents, outline, wordcount
- 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