Files
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

369 lines
11 KiB
Markdown

# 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<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
```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<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
```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