UNPKG

obsidian-mcp-server

Version:

Obsidian Knowledge-Management MCP (Model Context Protocol) server that enables AI agents and development tools to interact with an Obsidian vault. It provides a comprehensive suite of tools for reading, writing, searching, and managing notes, tags, and fr

192 lines (191 loc) 7.22 kB
import dotenv from "dotenv"; import { existsSync, mkdirSync, readFileSync, statSync } from "fs"; import path, { dirname, join } from "path"; import { fileURLToPath } from "url"; import { z } from "zod"; dotenv.config(); // --- Determine Project Root --- /** * Finds the project root directory by searching upwards for package.json. * @param startDir The directory to start searching from. * @returns The absolute path to the project root, or throws an error if not found. */ const findProjectRoot = (startDir) => { let currentDir = startDir; while (true) { const packageJsonPath = join(currentDir, "package.json"); if (existsSync(packageJsonPath)) { return currentDir; } const parentDir = dirname(currentDir); if (parentDir === currentDir) { // Reached the root of the filesystem without finding package.json throw new Error(`Could not find project root (package.json) starting from ${startDir}`); } currentDir = parentDir; } }; let projectRoot; try { // For ESM, __dirname is not available directly. const currentModuleDir = dirname(fileURLToPath(import.meta.url)); projectRoot = findProjectRoot(currentModuleDir); } catch (error) { console.error(`FATAL: Error determining project root: ${error.message}`); projectRoot = process.cwd(); console.warn(`Warning: Using process.cwd() (${projectRoot}) as fallback project root.`); } // --- End Determine Project Root --- const pkgPath = join(projectRoot, "package.json"); let pkg = { name: "obsidian-mcp-server", version: "0.0.0" }; try { pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); } catch (error) { if (process.stderr.isTTY) { console.error("Warning: Could not read package.json for default config values. Using hardcoded defaults.", error); } } /** * Zod schema for validating environment variables. * @private */ const EnvSchema = z.object({ MCP_SERVER_NAME: z.string().optional(), MCP_SERVER_VERSION: z.string().optional(), MCP_LOG_LEVEL: z.string().default("info"), LOGS_DIR: z.string().default(path.join(projectRoot, "logs")), NODE_ENV: z.string().default("development"), MCP_TRANSPORT_TYPE: z.enum(["stdio", "http"]).default("stdio"), MCP_HTTP_PORT: z.coerce.number().int().positive().default(3010), MCP_HTTP_HOST: z.string().default("127.0.0.1"), MCP_ALLOWED_ORIGINS: z.string().optional(), MCP_AUTH_MODE: z.enum(["jwt", "oauth"]).optional(), MCP_AUTH_SECRET_KEY: z .string() .min(32, "MCP_AUTH_SECRET_KEY must be at least 32 characters long for security") .optional(), OAUTH_ISSUER_URL: z.string().url().optional(), OAUTH_AUDIENCE: z.string().optional(), OAUTH_JWKS_URI: z.string().url().optional(), // --- Obsidian Specific Config --- OBSIDIAN_API_KEY: z.string().min(1, "OBSIDIAN_API_KEY cannot be empty"), OBSIDIAN_BASE_URL: z.string().url().default("http://127.0.0.1:27123"), OBSIDIAN_VERIFY_SSL: z .string() .transform((val) => val.toLowerCase() === "true") .default("false"), OBSIDIAN_CACHE_REFRESH_INTERVAL_MIN: z.coerce .number() .int() .positive() .default(10), OBSIDIAN_ENABLE_CACHE: z .string() .transform((val) => val.toLowerCase() === "true") .default("true"), OBSIDIAN_API_SEARCH_TIMEOUT_MS: z.coerce .number() .int() .positive() .default(30000), }); const parsedEnv = EnvSchema.safeParse(process.env); if (!parsedEnv.success) { const errorDetails = parsedEnv.error.flatten().fieldErrors; if (process.stderr.isTTY) { console.error("❌ Invalid environment variables:", errorDetails); } throw new Error(`Invalid environment configuration. Please check your .env file or environment variables. Details: ${JSON.stringify(errorDetails)}`); } const env = parsedEnv.data; // --- Directory Ensurance Function --- const ensureDirectory = (dirPath, rootDir, dirName) => { const resolvedDirPath = path.isAbsolute(dirPath) ? dirPath : path.resolve(rootDir, dirPath); if (!resolvedDirPath.startsWith(rootDir + path.sep) && resolvedDirPath !== rootDir) { if (process.stderr.isTTY) { console.error(`Error: ${dirName} path "${dirPath}" resolves to "${resolvedDirPath}", which is outside the project boundary "${rootDir}".`); } return null; } if (!existsSync(resolvedDirPath)) { try { mkdirSync(resolvedDirPath, { recursive: true }); } catch (err) { if (process.stderr.isTTY) { console.error(`Error creating ${dirName} directory at ${resolvedDirPath}: ${err instanceof Error ? err.message : String(err)}`); } return null; } } else { try { if (!statSync(resolvedDirPath).isDirectory()) { if (process.stderr.isTTY) { console.error(`Error: ${dirName} path ${resolvedDirPath} exists but is not a directory.`); } return null; } } catch (statError) { if (process.stderr.isTTY) { console.error(`Error accessing ${dirName} path ${resolvedDirPath}: ${statError.message}`); } return null; } } return resolvedDirPath; }; // --- End Directory Ensurance Function --- const validatedLogsPath = ensureDirectory(env.LOGS_DIR, projectRoot, "logs"); if (!validatedLogsPath) { if (process.stderr.isTTY) { console.error("FATAL: Logs directory configuration is invalid or could not be created. Please check permissions and path. Exiting."); } process.exit(1); } /** * Main application configuration object. */ export const config = { pkg, mcpServerName: env.MCP_SERVER_NAME || pkg.name, mcpServerVersion: env.MCP_SERVER_VERSION || pkg.version, logLevel: env.MCP_LOG_LEVEL, logsPath: validatedLogsPath, environment: env.NODE_ENV, mcpTransportType: env.MCP_TRANSPORT_TYPE, mcpHttpPort: env.MCP_HTTP_PORT, mcpHttpHost: env.MCP_HTTP_HOST, mcpAllowedOrigins: env.MCP_ALLOWED_ORIGINS?.split(",") .map((origin) => origin.trim()) .filter(Boolean), mcpAuthMode: env.MCP_AUTH_MODE, mcpAuthSecretKey: env.MCP_AUTH_SECRET_KEY, oauthIssuerUrl: env.OAUTH_ISSUER_URL, oauthAudience: env.OAUTH_AUDIENCE, oauthJwksUri: env.OAUTH_JWKS_URI, obsidianApiKey: env.OBSIDIAN_API_KEY, obsidianBaseUrl: env.OBSIDIAN_BASE_URL, obsidianVerifySsl: env.OBSIDIAN_VERIFY_SSL, obsidianCacheRefreshIntervalMin: env.OBSIDIAN_CACHE_REFRESH_INTERVAL_MIN, obsidianEnableCache: env.OBSIDIAN_ENABLE_CACHE, obsidianApiSearchTimeoutMs: env.OBSIDIAN_API_SEARCH_TIMEOUT_MS, }; /** * The configured logging level for the application. * Exported separately for convenience (e.g., logger initialization). * @type {string} */ export const logLevel = config.logLevel; /** * The configured runtime environment for the application. * Exported separately for convenience. * @type {string} */ export const environment = config.environment;