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>
This commit is contained in:
2026-03-22 11:21:38 -05:00
parent e9e0112240
commit 622b28e42c
35 changed files with 5139 additions and 35 deletions

157
src/server.ts Normal file
View File

@@ -0,0 +1,157 @@
/**
* MCP Server for Obsidian CLI Bundle
* Constitutional Principle I: MCP Protocol Compliance
* Constitutional Principle VI: Stdio Transport Standard
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { logger } from './utils/logger.js';
import { createErrorResponse } from './utils/error-handler.js';
import { ToolOutput } from './utils/types.js';
/**
* MCP Server instance
*/
export class ObsidianMCPServer {
private server: Server;
private tools: Map<string, ToolHandler> = new Map();
constructor() {
this.server = new Server(
{
name: 'obsidian-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
/**
* Register a tool handler
*/
registerTool(
name: string,
_description: string,
_inputSchema: Record<string, unknown>,
handler: ToolHandler
): void {
this.tools.set(name, handler);
logger.debug('Registered tool', { name });
}
/**
* Setup MCP protocol handlers
*/
private setupHandlers(): void {
// Handle tools/list request
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
logger.debug('Handling tools/list request');
const toolsList = Array.from(this.tools.entries()).map(([name, handler]) => ({
name,
description: handler.description || `Tool: ${name}`,
inputSchema: handler.inputSchema || {
type: 'object',
properties: {},
},
}));
logger.info('Returning tools list', { count: toolsList.length });
return {
tools: toolsList,
};
});
// Handle tools/call request
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
logger.info('Handling tool call', { tool: name });
try {
const handler = this.tools.get(name);
if (!handler) {
throw new Error(`Tool not found: ${name}`);
}
// Execute tool handler
const result = await handler.execute(args || {});
logger.debug('Tool call succeeded', { tool: name });
// MCP expects a specific response format
return {
content: result.content,
isError: result.isError || false,
};
} catch (error) {
logger.error('Tool call failed', {
tool: name,
error: error instanceof Error ? error.message : String(error),
});
const errorResponse = createErrorResponse(error);
return {
content: errorResponse.content,
isError: true,
};
}
});
logger.info('MCP server handlers configured');
}
/**
* Connect to stdio transport
*/
async connect(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
logger.info('MCP server connected to stdio transport');
}
/**
* Close server connection
*/
async close(): Promise<void> {
await this.server.close();
logger.info('MCP server closed');
}
}
/**
* Tool handler interface
*/
export interface ToolHandler {
description: string;
inputSchema: Record<string, unknown>;
execute(args: Record<string, unknown>): Promise<ToolOutput>;
}
/**
* Create a tool handler
*/
export function createToolHandler(
description: string,
inputSchema: Record<string, unknown>,
execute: (args: Record<string, unknown>) => Promise<ToolOutput>
): ToolHandler {
return {
description,
inputSchema,
execute,
};
}