obsidian-mcp-server
Version:
Model Context Protocol (MCP) server designed for LLMs to interact with Obsidian vaults. Provides secure, token-aware tools for seamless knowledge base management through a standardized interface.
247 lines • 10.8 kB
JavaScript
/**
* MCP server implementation
*/
import { config } from "dotenv";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createLogger, ErrorCategoryType } from "../utils/logging.js";
import { rateLimiter } from "../utils/rate-limiting.js";
import { createTagResource } from "../resources/index.js";
import { createToolHandlers, createToolHandlerMap } from "../tools/index.js";
import { ObsidianClient } from "../obsidian/client.js";
import { setupToolListingHandler, setupToolCallingHandler, setupResourceListingHandler, setupResourceReadingHandler } from "./handlers.js";
import { McpErrorCode } from "./types.js";
// Create a logger for the server
const logger = createLogger('McpServer');
// Load environment variables
config();
/**
* Initialize MCP server components
*/
export async function initializeServer() {
// Start initialization timer
logger.startTimer('server_init');
try {
// Verify API key exists
const API_KEY = process.env.OBSIDIAN_API_KEY;
if (!API_KEY) {
logger.error("Missing API key", {
errorCategory: ErrorCategoryType.CATEGORY_AUTHENTICATION,
errorCode: McpErrorCode.UNAUTHORIZED
});
throw new Error("OBSIDIAN_API_KEY environment variable is required");
}
// Initialize Obsidian client with environment configuration
logger.info('Initializing Obsidian client', {
verifySSL: process.env.VERIFY_SSL === 'true',
timeout: parseInt(process.env.REQUEST_TIMEOUT || '5000'),
maxContentLength: parseInt(process.env.MAX_CONTENT_LENGTH || String(50 * 1024 * 1024)),
maxBodyLength: parseInt(process.env.MAX_BODY_LENGTH || String(50 * 1024 * 1024))
});
const client = new ObsidianClient({
apiKey: API_KEY,
verifySSL: process.env.VERIFY_SSL === 'true',
timeout: parseInt(process.env.REQUEST_TIMEOUT || '5000'),
maxContentLength: parseInt(process.env.MAX_CONTENT_LENGTH || String(50 * 1024 * 1024)),
maxBodyLength: parseInt(process.env.MAX_BODY_LENGTH || String(50 * 1024 * 1024))
});
// Initialize tool handlers
logger.startTimer('init_tools');
logger.info('Initializing tool handlers');
const toolHandlers = createToolHandlers(client);
const toolHandlerMap = createToolHandlerMap(toolHandlers);
const toolsElapsedMs = logger.endTimer('init_tools');
logger.info(`Initialized ${toolHandlerMap.size} tool handlers`, {
processingTimeMs: toolsElapsedMs,
toolCount: toolHandlerMap.size
});
// Initialize resources
logger.startTimer('init_resources');
logger.info('Initializing resources');
const tagResource = createTagResource(client);
const resources = {
[tagResource.getResourceDescription().uri]: tagResource
};
const resourcesElapsedMs = logger.endTimer('init_resources');
logger.info(`Initialized ${Object.keys(resources).length} resources`, {
processingTimeMs: resourcesElapsedMs,
resourceCount: Object.keys(resources).length
});
// Create MCP server
const serverConfig = {
name: "obsidian-mcp-server",
version: process.env.npm_package_version ?? "1.1.0" // Use version from package.json
};
logger.info(`Creating MCP server: ${serverConfig.name} v${serverConfig.version}`, {
serverName: serverConfig.name,
serverVersion: serverConfig.version
});
const server = new Server(serverConfig, {
capabilities: {
tools: {},
resources
}
});
// Set up request handlers
logger.startTimer('setup_handlers');
setupToolListingHandler(server, toolHandlerMap);
setupToolCallingHandler(server, toolHandlerMap);
setupResourceListingHandler(server, resources);
setupResourceReadingHandler(server, resources);
const handlersElapsedMs = logger.endTimer('setup_handlers');
logger.info('Request handlers configured', { processingTimeMs: handlersElapsedMs });
// Set up enhanced error handler
server.onerror = (error) => {
if (error instanceof Error) {
logger.error("MCP Server Error", {
name: error.name,
message: error.message,
stack: error.stack,
errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
});
}
else {
logger.error("MCP Server Error (Unknown)", {
error: String(error),
errorCategory: ErrorCategoryType.CATEGORY_UNKNOWN
});
}
};
// Log successful initialization
const totalElapsedMs = logger.endTimer('server_init');
logger.logOperationResult(true, 'server_initialization', totalElapsedMs, {
toolCount: toolHandlerMap.size,
resourceCount: Object.keys(resources).length
});
return server;
}
catch (error) {
// Log initialization failure
const totalElapsedMs = logger.endTimer('server_init');
logger.logOperationResult(false, 'server_initialization', totalElapsedMs);
// Enhance error reporting
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
logger.error("Failed to initialize server", {
error: errorMessage,
stack: errorStack,
errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
});
throw error;
}
}
/**
* Set up graceful shutdown handling
* @param server The MCP server instance
* @param cleanupHandlers Additional cleanup handlers to run on shutdown
*/
export function setupShutdownHandling(server, cleanupHandlers = []) {
// Cleanup function for graceful shutdown
const cleanup = async (signal) => {
logger.startTimer('server_shutdown');
logger.info('Shutting down server...', { signal });
try {
// Run cleanup handlers
logger.debug('Running cleanup handlers', { handlersCount: cleanupHandlers.length });
for (const handler of cleanupHandlers) {
try {
await handler();
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Error during cleanup handler execution', {
error: errorMessage,
stack: error instanceof Error ? error.stack : undefined,
errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
});
}
}
// Dispose rate limiter
logger.debug('Disposing rate limiter');
rateLimiter.dispose();
// Close server
logger.debug('Closing MCP server connection');
await server.close();
const elapsedMs = logger.endTimer('server_shutdown');
logger.logOperationResult(true, 'server_shutdown', elapsedMs);
logger.info('Server shutdown completed successfully');
process.exit(0);
}
catch (error) {
const elapsedMs = logger.endTimer('server_shutdown');
logger.logOperationResult(false, 'server_shutdown', elapsedMs);
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Failed to shut down server gracefully', {
error: errorMessage,
stack: error instanceof Error ? error.stack : undefined,
errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
});
process.exit(1);
}
};
// Handle various termination signals
process.on('SIGINT', () => cleanup('SIGINT')); // Ctrl+C on all platforms
process.on('SIGTERM', () => cleanup('SIGTERM')); // Termination request
if (process.platform === 'win32') {
// Windows-specific handling
process.on('SIGHUP', () => cleanup('SIGHUP')); // Terminal closed
}
else {
// Unix-specific signals
process.on('SIGUSR1', () => cleanup('SIGUSR1'));
process.on('SIGUSR2', () => cleanup('SIGUSR2'));
}
// Handle uncaught errors
process.on('uncaughtException', async (error) => {
logger.error('Uncaught exception', {
name: error.name,
message: error.message,
stack: error.stack,
errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
});
await cleanup('uncaughtException');
});
process.on('unhandledRejection', async (reason, promise) => {
const errorMessage = reason instanceof Error ? reason.message : String(reason);
logger.error('Unhandled promise rejection', {
reason: errorMessage,
stack: reason instanceof Error ? reason.stack : undefined,
errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
});
await cleanup('unhandledRejection');
});
}
/**
* Run the MCP server
*/
export async function run() {
try {
logger.info("Starting Obsidian MCP server");
logger.startTimer('server_startup');
const server = await initializeServer();
setupShutdownHandling(server, []);
// Connect to transport
logger.debug("Connecting to stdio transport");
const transport = new StdioServerTransport();
await server.connect(transport);
const elapsedMs = logger.endTimer('server_startup');
logger.logOperationResult(true, 'server_startup', elapsedMs);
logger.info("Obsidian MCP server running on stdio", {
serverName: "obsidian-mcp-server",
serverVersion: process.env.npm_package_version ?? "1.1.0",
transport: "stdio"
});
}
catch (error) {
const elapsedMs = logger.endTimer('server_startup', 'Server startup failed');
logger.logOperationResult(false, 'server_startup', elapsedMs);
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error("Failed to start server", {
error: errorMessage,
stack: error instanceof Error ? error.stack : undefined,
errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
});
process.exit(1);
}
}
//# sourceMappingURL=server.js.map