MCP Tools for Obsidian

import type McpToolsPlugin from "$/main"; import { logger } from "$/shared/logger"; import { exec } from "child_process"; import fsp from "fs/promises"; import { Plugin } from "obsidian"; import path from "path"; import { clean, lt, valid } from "semver"; import { promisify } from "util"; import { BINARY_NAME } from "../constants"; import type { InstallationStatus, InstallPathInfo } from "../types"; import { getFileSystemAdapter } from "../utils/getFileSystemAdapter"; import { getPlatform } from "./install"; const execAsync = promisify(exec); /** * Resolves the real path of the given file path, handling cases where the path is a symlink. * * @param filepath - The file path to resolve. * @returns The real path of the file. * @throws {Error} If the file is not found or the symlink cannot be resolved. */ async function resolveSymlinks(filepath: string): Promise<string> { try { return await fsp.realpath(filepath); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") { const parts = path.normalize(filepath).split(path.sep); let resolvedParts: string[] = []; let skipCount = 1; // Skip first segment by default // Handle the root segment differently for Windows vs POSIX if (path.win32.isAbsolute(filepath)) { resolvedParts.push(parts[0]); if (parts[1] === "") { resolvedParts.push(""); skipCount = 2; // Skip two segments for UNC paths } } else if (path.posix.isAbsolute(filepath)) { resolvedParts.push("/"); } else { resolvedParts.push(parts[0]); } // Process remaining path segments for (const part of parts.slice(skipCount)) { const partialPath = path.join(...resolvedParts, part); try { const resolvedPath = await fsp.realpath(partialPath); resolvedParts = resolvedPath.split(path.sep); } catch (err) { resolvedParts.push(part); } } return path.join(...resolvedParts); } logger.error(`Failed to resolve symlink:`, { filepath, error: error instanceof Error ? error.message : error, }); throw new Error(`Failed to resolve symlink: ${filepath}`); } } export async function getInstallPath( plugin: Plugin, ): Promise<InstallPathInfo | { error: string }> { const adapter = getFileSystemAdapter(plugin); if ("error" in adapter) return adapter; const platform = getPlatform(); const originalPath = path.join( adapter.getBasePath(), plugin.app.vault.configDir, "plugins", plugin.manifest.id, "bin", ); const realDirPath = await resolveSymlinks(originalPath); const platformSpecificBinary = BINARY_NAME[platform]; const realFilePath = path.join(realDirPath, platformSpecificBinary); return { dir: realDirPath, path: realFilePath, name: platformSpecificBinary, symlinked: originalPath === realDirPath ? undefined : originalPath, }; } /** * Gets the current installation status of the MCP server */ export async function getInstallationStatus( plugin: McpToolsPlugin, ): Promise<InstallationStatus> { // Verify plugin version is valid const pluginVersion = valid(clean(plugin.manifest.version)); if (!pluginVersion) { logger.error("Invalid plugin version:", { plugin }); return { state: "error", versions: {} }; } // Check for API key const apiKey = plugin.getLocalRestApiKey(); if (!apiKey) { return { state: "no api key", versions: { plugin: pluginVersion }, }; } // Verify server binary is present const installPath = await getInstallPath(plugin); if ("error" in installPath) { return { state: "error", versions: { plugin: pluginVersion }, error: installPath.error, }; } try { await fsp.access(installPath.path, fsp.constants.X_OK); } catch (error) { logger.error("Failed to get server version:", { installPath }); return { state: "not installed", ...installPath, versions: { plugin: pluginVersion }, }; } // Check server binary version let serverVersion: string | null | undefined; try { const versionCommand = `"${installPath.path}" --version`; const { stdout } = await execAsync(versionCommand); serverVersion = clean(stdout.trim()); if (!serverVersion) throw new Error("Invalid server version string"); } catch { logger.error("Failed to get server version:", { installPath }); return { state: "error", ...installPath, versions: { plugin: pluginVersion }, }; } return { ...installPath, state: lt(serverVersion, pluginVersion) ? "outdated" : "installed", versions: { plugin: pluginVersion, server: serverVersion }, }; }