- 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>
369 lines
11 KiB
Markdown
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
|