Skip to main content
Glama

Obsidian MCP Server

Apache 2.0
338
222
  • Apple
  • Linux
index.ts14.4 kB
#!/usr/bin/env node // Imports MUST be at the top level import { ServerType } from "@hono/node-server"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { config, environment } from "./config/index.js"; // This loads .env via dotenv.config() import { initializeAndStartServer } from "./mcp-server/server.js"; import { requestContextService, retryWithDelay } from "./utils/index.js"; import { logger, McpLogLevel } from "./utils/internal/logger.js"; // Import logger instance early // Import Services import { ObsidianRestApiService } from "./services/obsidianRestAPI/index.js"; import { VaultCacheService } from "./services/obsidianRestAPI/vaultCache/index.js"; // Import VaultCacheService /** * The main MCP server instance (only stored globally for stdio shutdown). * @type {McpServer | undefined} */ let server: McpServer | undefined; /** * The main HTTP server instance (only stored globally for http shutdown). * @type {ServerType | undefined} */ let httpServerInstance: ServerType | undefined; /** * Shared Obsidian REST API service instance. * @type {ObsidianRestApiService | undefined} */ let obsidianService: ObsidianRestApiService | undefined; /** * Shared Vault Cache service instance. * @type {VaultCacheService | undefined} */ let vaultCacheService: VaultCacheService | undefined; /** * Gracefully shuts down the main MCP server. * Handles process termination signals (SIGTERM, SIGINT) and critical errors. * * @param signal - The signal or event name that triggered the shutdown (e.g., "SIGTERM", "uncaughtException"). */ const shutdown = async (signal: string) => { // Define context for the shutdown operation const shutdownContext = requestContextService.createRequestContext({ operation: "Shutdown", signal, }); logger.info( `Received ${signal}. Starting graceful shutdown...`, shutdownContext, ); try { // Stop cache refresh timer first if (config.obsidianEnableCache && vaultCacheService) { vaultCacheService.stopPeriodicRefresh(); } // Close the main MCP server (only relevant for stdio) if (server) { logger.info("Closing main MCP server (stdio)...", shutdownContext); await server.close(); logger.info( "Main MCP server (stdio) closed successfully", shutdownContext, ); } // Close the main HTTP server instance (if it exists) if (httpServerInstance) { logger.info("Closing main HTTP server...", shutdownContext); await new Promise<void>((resolve, reject) => { httpServerInstance!.close((err?: Error) => { if (err) { logger.error("Error closing HTTP server", err, shutdownContext); reject(err); return; } logger.info("Main HTTP server closed successfully", shutdownContext); resolve(); }); }); } if (!server && !httpServerInstance) { logger.warning( "No server instance (Stdio or HTTP) found to close during shutdown.", shutdownContext, ); } // Add any other necessary cleanup here (e.g., closing database connections if added later) logger.info("Graceful shutdown completed successfully", shutdownContext); process.exit(0); } catch (error) { // Handle any errors during shutdown logger.error( "Critical error during shutdown", error instanceof Error ? error : undefined, { ...shutdownContext, // Spread the existing RequestContext // error field is handled by logger.error's second argument }, ); process.exit(1); // Exit with error code if shutdown fails } }; /** * Initializes and starts the main MCP server. * Sets up request context, initializes the server instance, starts the transport, * and registers signal handlers for graceful shutdown and error handling. */ const start = async () => { // --- Logger Initialization (Moved here AFTER config/dotenv is loaded) --- const validMcpLogLevels: McpLogLevel[] = [ "debug", "info", "notice", "warning", "error", "crit", "alert", "emerg", ]; // Read level from config (which read from env var or default) const initialLogLevelConfig = config.logLevel; // Validate the configured log level let validatedMcpLogLevel: McpLogLevel = "info"; // Default to 'info' if (validMcpLogLevels.includes(initialLogLevelConfig as McpLogLevel)) { validatedMcpLogLevel = initialLogLevelConfig as McpLogLevel; } else { // Use console.warn here as logger isn't initialized yet console.warn( `Invalid MCP_LOG_LEVEL "${initialLogLevelConfig}" provided via config/env. Defaulting to "info".`, ); } // Initialize the logger with the validated MCP level and wait for it to complete. await logger.initialize(validatedMcpLogLevel); // Log initialization message using the logger itself (will go to file/console) logger.info( `Logger initialized by start(). MCP logging level: ${validatedMcpLogLevel}`, ); // --- End Logger Initialization --- // Log that config is loaded (this was previously done earlier) logger.debug( "Configuration loaded successfully", requestContextService.createRequestContext({ configLoaded: true, configSummary: { serverName: config.mcpServerName, transport: config.mcpTransportType, logLevel: config.logLevel, }, }), ); // Create application-level request context using the service instance // Use the validated transport type from the config object const transportType = config.mcpTransportType; const startupContext = requestContextService.createRequestContext({ operation: `ServerStartup_${transportType}`, // Include transport in operation name appName: config.mcpServerName, appVersion: config.mcpServerVersion, environment: environment, }); logger.info( `Starting ${config.mcpServerName} v${config.mcpServerVersion} (Transport: ${transportType})...`, startupContext, ); try { // --- Instantiate Shared Services --- logger.debug("Instantiating shared services...", startupContext); obsidianService = new ObsidianRestApiService(); // Instantiate Obsidian Service // --- Perform Initial Obsidian API Status Check --- try { logger.info( "Performing initial Obsidian API status check with retries...", startupContext, ); const status = await retryWithDelay( async () => { if (!obsidianService) { // This case should not happen in practice, but it satisfies the type checker. throw new Error("Obsidian service not initialized."); } const checkStatusContext = { ...startupContext, operation: "checkStatusAttempt", }; const currentStatus = await obsidianService.checkStatus(checkStatusContext); if ( currentStatus?.service !== "Obsidian Local REST API" || !currentStatus?.authenticated ) { // Throw an error to trigger a retry throw new Error( `Obsidian API status check failed or indicates authentication issue. Status: ${JSON.stringify( currentStatus, )}`, ); } return currentStatus; }, { operationName: "initialObsidianApiCheck", context: startupContext, maxRetries: 5, // Retry up to 5 times delayMs: 3000, // Wait 3 seconds between retries }, ); logger.info("Obsidian API status check successful.", { ...startupContext, obsidianVersion: status.versions.obsidian, pluginVersion: status.versions.self, }); } catch (statusError) { logger.error( "Critical error during initial Obsidian API status check after multiple retries. Check OBSIDIAN_BASE_URL, OBSIDIAN_API_KEY, and plugin status.", { ...startupContext, error: statusError instanceof Error ? statusError.message : String(statusError), stack: statusError instanceof Error ? statusError.stack : undefined, }, ); // Re-throw the final error to be caught by the main startup catch block, which will exit the process. throw statusError; } // --- End Status Check --- if (config.obsidianEnableCache) { vaultCacheService = new VaultCacheService(obsidianService); // Instantiate Cache Service, passing Obsidian Service logger.info( "Vault cache is enabled and service is instantiated.", startupContext, ); } else { logger.info("Vault cache is disabled by configuration.", startupContext); } logger.info("Shared services instantiated.", startupContext); // --- End Service Instantiation --- // Initialize the server instance and start the selected transport logger.debug( "Initializing and starting MCP server transport", startupContext, ); // Start the server transport. Services are instantiated here and passed down. // For stdio, this returns the McpServer instance. // For http, it returns the http.Server instance. const serverOrHttpInstance = await initializeAndStartServer( obsidianService, vaultCacheService, ); if ( transportType === "stdio" && serverOrHttpInstance instanceof McpServer ) { server = serverOrHttpInstance; // Store McpServer for stdio logger.debug( "Stored McpServer instance for stdio transport.", startupContext, ); } else if (transportType === "http" && serverOrHttpInstance) { // The instance is of ServerType (http.Server or https.Server) httpServerInstance = serverOrHttpInstance as ServerType; // Store ServerType for http transport logger.debug( "Stored http.Server instance for http transport.", startupContext, ); } else if (transportType === "http") { // This case should ideally not be reached if startHttpTransport always returns an http.Server logger.warning( "HTTP transport selected, but initializeAndStartServer did not return an http.Server instance.", startupContext, ); } // If initializeAndStartServer failed, it would have thrown an error, // and execution would jump to the outer catch block. logger.info( `${config.mcpServerName} is running with ${transportType} transport`, { ...startupContext, startTime: new Date().toISOString(), }, ); // --- Trigger Background Cache Build --- if (config.obsidianEnableCache && vaultCacheService) { // Start building the cache, but don't wait for it to finish. // The server will be operational while the cache builds. // Tools needing the cache should check its readiness state. logger.info("Triggering background vault cache build...", startupContext); // No 'await' here - run in background vaultCacheService .buildVaultCache() .then(() => { // Once the initial build is done, start the periodic refresh vaultCacheService?.startPeriodicRefresh(); }) .catch((cacheBuildError) => { // Log errors during the background build process logger.error("Error occurred during background vault cache build", { ...startupContext, // Use startup context for correlation operation: "BackgroundCacheBuild", error: cacheBuildError instanceof Error ? cacheBuildError.message : String(cacheBuildError), stack: cacheBuildError instanceof Error ? cacheBuildError.stack : undefined, }); }); } // --- End Cache Build Trigger --- // --- Signal and Error Handling Setup --- // Handle process signals for graceful shutdown process.on("SIGTERM", () => shutdown("SIGTERM")); process.on("SIGINT", () => shutdown("SIGINT")); // Handle uncaught exceptions process.on("uncaughtException", async (error) => { const errorContext = { ...startupContext, // Include base context for correlation event: "uncaughtException", error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }; logger.error( "Uncaught exception detected. Initiating shutdown...", errorContext, ); // Attempt graceful shutdown; shutdown() handles its own errors. await shutdown("uncaughtException"); // If shutdown fails internally, it will call process.exit(1). // If shutdown succeeds, it calls process.exit(0). // If shutdown itself throws unexpectedly *before* exiting, this process might terminate abruptly, // but the core shutdown logic is handled within shutdown(). }); // Handle unhandled promise rejections process.on("unhandledRejection", async (reason: unknown) => { const rejectionContext = { ...startupContext, // Include base context for correlation event: "unhandledRejection", reason: reason instanceof Error ? reason.message : String(reason), stack: reason instanceof Error ? reason.stack : undefined, }; logger.error( "Unhandled promise rejection detected. Initiating shutdown...", rejectionContext, ); // Attempt graceful shutdown; shutdown() handles its own errors. await shutdown("unhandledRejection"); // Similar logic as uncaughtException: shutdown handles its exit codes. }); } catch (error) { // Handle critical startup errors (already logged by ErrorHandler or caught above) // Log the final failure context, including error details, before exiting logger.error("Critical error during startup, exiting.", { ...startupContext, finalErrorContext: "Startup Failure", error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); process.exit(1); } }; // --- Async IIFE to allow top-level await --- // This remains necessary because start() is async (async () => { // Start the application await start(); })(); // End async IIFE

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cyanheads/obsidian-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server