From d7112ff3b0525f66759a4e7af2a40e9574ac72c3 Mon Sep 17 00:00:00 2001 From: Eric Mey Date: Tue, 5 May 2026 18:45:31 -0400 Subject: [PATCH] feat: configure Obsidian REST API port --- .../mcp-server/src/shared/makeRequest.test.ts | 37 +++++++++ packages/mcp-server/src/shared/makeRequest.ts | 37 +++++++-- packages/mcp-server/src/types/global.d.ts | 4 + .../McpServerInstallSettings.svelte | 75 +++++++++++++++++++ .../mcp-server-install/services/config.ts | 15 +++- packages/obsidian-plugin/src/main.ts | 31 ++++++++ packages/obsidian-plugin/src/types.ts | 19 +++-- 7 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 packages/mcp-server/src/shared/makeRequest.test.ts diff --git a/packages/mcp-server/src/shared/makeRequest.test.ts b/packages/mcp-server/src/shared/makeRequest.test.ts new file mode 100644 index 00000000..5f03c034 --- /dev/null +++ b/packages/mcp-server/src/shared/makeRequest.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, test } from "bun:test"; +import { resolveBaseUrl } from "./makeRequest"; + +describe("resolveBaseUrl", () => { + test("defaults to the Local REST API HTTPS port", () => { + expect(resolveBaseUrl({})).toBe("https://127.0.0.1:27124"); + }); + + test("uses the default HTTP port when HTTP is enabled", () => { + expect(resolveBaseUrl({ OBSIDIAN_USE_HTTP: "true" })).toBe( + "http://127.0.0.1:27123", + ); + }); + + test("uses custom HTTP and HTTPS ports", () => { + expect( + resolveBaseUrl({ + OBSIDIAN_USE_HTTP: "true", + OBSIDIAN_HTTP_PORT: "27125", + }), + ).toBe("http://127.0.0.1:27125"); + + expect(resolveBaseUrl({ OBSIDIAN_HTTPS_PORT: "27126" })).toBe( + "https://127.0.0.1:27126", + ); + }); + + test("uses base URL override before host, protocol, and port fields", () => { + expect( + resolveBaseUrl({ + OBSIDIAN_BASE_URL: "http://127.0.0.1:3000/", + OBSIDIAN_USE_HTTP: "false", + OBSIDIAN_HTTPS_PORT: "27126", + }), + ).toBe("http://127.0.0.1:3000"); + }); +}); diff --git a/packages/mcp-server/src/shared/makeRequest.ts b/packages/mcp-server/src/shared/makeRequest.ts index 4fcb8e4b..1ba041ad 100644 --- a/packages/mcp-server/src/shared/makeRequest.ts +++ b/packages/mcp-server/src/shared/makeRequest.ts @@ -2,12 +2,37 @@ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; import { type, type Type } from "arktype"; import { logger } from "./logger"; -// Default to HTTPS port, fallback to HTTP if specified -const USE_HTTP = process.env.OBSIDIAN_USE_HTTP === "true"; -const PORT = USE_HTTP ? 27123 : 27124; -const PROTOCOL = USE_HTTP ? "http" : "https"; -const HOST = process.env.OBSIDIAN_HOST || "127.0.0.1"; -export const BASE_URL = `${PROTOCOL}://${HOST}:${PORT}`; +function trimTrailingSlash(value: string): string { + return value.replace(/\/$/, ""); +} + +interface BaseUrlEnv { + OBSIDIAN_BASE_URL?: string; + OBSIDIAN_HOST?: string; + OBSIDIAN_HTTP_PORT?: string; + OBSIDIAN_HTTPS_PORT?: string; + OBSIDIAN_USE_HTTP?: string; +} + +export function resolveBaseUrl(env: BaseUrlEnv = process.env): string { + const baseUrl = env.OBSIDIAN_BASE_URL?.trim(); + if (baseUrl) { + return trimTrailingSlash(baseUrl); + } + + // Default to HTTPS, fallback to HTTP if specified. Custom ports allow + // multiple Obsidian vaults to expose separate Local REST API instances. + const useHttp = env.OBSIDIAN_USE_HTTP === "true"; + const protocol = useHttp ? "http" : "https"; + const host = env.OBSIDIAN_HOST || "127.0.0.1"; + const port = useHttp + ? (env.OBSIDIAN_HTTP_PORT ?? "27123") + : (env.OBSIDIAN_HTTPS_PORT ?? "27124"); + + return `${protocol}://${host}:${port}`; +} + +export const BASE_URL = resolveBaseUrl(); // Disable TLS certificate validation for local self-signed certificates process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; diff --git a/packages/mcp-server/src/types/global.d.ts b/packages/mcp-server/src/types/global.d.ts index f74528df..f0cd1706 100644 --- a/packages/mcp-server/src/types/global.d.ts +++ b/packages/mcp-server/src/types/global.d.ts @@ -4,6 +4,10 @@ declare global { NODE_ENV: "development" | "production"; NODE_TLS_REJECT_UNAUTHORIZED: `${0 | 1}`; OBSIDIAN_API_KEY?: string; + OBSIDIAN_BASE_URL?: string; + OBSIDIAN_HOST?: string; + OBSIDIAN_HTTP_PORT?: string; + OBSIDIAN_HTTPS_PORT?: string; OBSIDIAN_USE_HTTP?: string; } } diff --git a/packages/obsidian-plugin/src/features/mcp-server-install/components/McpServerInstallSettings.svelte b/packages/obsidian-plugin/src/features/mcp-server-install/components/McpServerInstallSettings.svelte index e7c5b983..b20be800 100644 --- a/packages/obsidian-plugin/src/features/mcp-server-install/components/McpServerInstallSettings.svelte +++ b/packages/obsidian-plugin/src/features/mcp-server-install/components/McpServerInstallSettings.svelte @@ -16,6 +16,8 @@ export let plugin: McpToolsPlugin; + let localRestApi = { ...plugin.settings.localRestApi }; + // Dependencies and API key status const deps = loadDependenciesArray(plugin); @@ -51,6 +53,20 @@ } } + async function saveLocalRestApiSettings() { + plugin.settings.localRestApi = { + host: localRestApi.host || "127.0.0.1", + useHttp: !!localRestApi.useHttp, + httpPort: Number(localRestApi.httpPort) || 27123, + httpsPort: Number(localRestApi.httpsPort) || 27124, + baseUrl: localRestApi.baseUrl?.trim() || "", + }; + await plugin.saveSettings(); + new Notice( + "MCP Tools settings saved. Reinstall or update the MCP server config for changes to apply.", + ); + } + // Handle uninstall async function handleUninstall() { try { @@ -100,6 +116,42 @@ {/if} +
+

Local REST API connection

+ + + + + + + + + + + +

+ Leave the base URL empty to use host, protocol, and port fields. Set it for + multi-vault setups with custom Local REST API ports. +

+ + +
+

Dependencies

@@ -160,6 +212,29 @@ margin-bottom: 0.5em; } + .local-rest-api-settings { + margin: 1.5em 0; + } + + .local-rest-api-settings label { + display: block; + margin-bottom: 0.75em; + } + + .local-rest-api-settings input[type="text"], + .local-rest-api-settings input[type="number"], + .local-rest-api-settings input:not([type]) { + display: block; + margin-top: 0.25em; + width: min(100%, 28rem); + } + + .setting-note { + color: var(--text-muted); + font-size: var(--font-ui-small); + max-width: 36rem; + } + .installed { color: var(--text-success); } diff --git a/packages/obsidian-plugin/src/features/mcp-server-install/services/config.ts b/packages/obsidian-plugin/src/features/mcp-server-install/services/config.ts index 9edf4d66..cadcf9d1 100644 --- a/packages/obsidian-plugin/src/features/mcp-server-install/services/config.ts +++ b/packages/obsidian-plugin/src/features/mcp-server-install/services/config.ts @@ -1,7 +1,7 @@ import fsp from "fs/promises"; -import { Plugin } from "obsidian"; import os from "os"; import path from "path"; +import type McpToolsPlugin from "$/main"; import { logger } from "$/shared/logger"; import { CLAUDE_CONFIG_PATH } from "../constants"; @@ -12,6 +12,11 @@ interface ClaudeConfig { args?: string[]; env?: { OBSIDIAN_API_KEY?: string; + OBSIDIAN_BASE_URL?: string; + OBSIDIAN_HOST?: string; + OBSIDIAN_HTTP_PORT?: string; + OBSIDIAN_HTTPS_PORT?: string; + OBSIDIAN_USE_HTTP?: string; [key: string]: string | undefined; }; }; @@ -53,7 +58,7 @@ function getConfigPath(): string { * Updates the Claude Desktop config file with MCP server settings */ export async function updateClaudeConfig( - plugin: Plugin, + plugin: McpToolsPlugin, serverPath: string, apiKey?: string ): Promise { @@ -78,10 +83,16 @@ export async function updateClaudeConfig( } // Update config with our server entry + const localRestApi = plugin.settings.localRestApi ?? {}; config.mcpServers["obsidian-mcp-tools"] = { command: serverPath, env: { OBSIDIAN_API_KEY: apiKey, + OBSIDIAN_HOST: localRestApi.host, + OBSIDIAN_USE_HTTP: String(localRestApi.useHttp), + OBSIDIAN_HTTP_PORT: localRestApi.httpPort?.toString(), + OBSIDIAN_HTTPS_PORT: localRestApi.httpsPort?.toString(), + OBSIDIAN_BASE_URL: localRestApi.baseUrl || undefined, }, }; diff --git a/packages/obsidian-plugin/src/main.ts b/packages/obsidian-plugin/src/main.ts index 38069f95..79ab8246 100644 --- a/packages/obsidian-plugin/src/main.ts +++ b/packages/obsidian-plugin/src/main.ts @@ -20,8 +20,21 @@ import { type Dependencies, } from "./shared"; import { logger } from "./shared/logger"; +import type { McpToolsPluginSettings } from "./types"; + +export const DEFAULT_SETTINGS: McpToolsPluginSettings = { + localRestApi: { + host: "127.0.0.1", + useHttp: false, + httpPort: 27123, + httpsPort: 27124, + baseUrl: "", + }, +}; export default class McpToolsPlugin extends Plugin { + settings = DEFAULT_SETTINGS; + private localRestApi: Dependencies["obsidian-local-rest-api"] = { id: "obsidian-local-rest-api", name: "Local REST API", @@ -34,7 +47,25 @@ export default class McpToolsPlugin extends Plugin { return this.localRestApi.plugin?.settings?.apiKey; } + async loadSettings() { + const data = await this.loadData(); + this.settings = { + ...DEFAULT_SETTINGS, + ...data, + localRestApi: { + ...DEFAULT_SETTINGS.localRestApi, + ...data?.localRestApi, + }, + }; + } + + async saveSettings() { + await this.saveData(this.settings); + } + async onload() { + await this.loadSettings(); + // Initialize features in order await setupCore(this); await setupMcpServerInstall(this); diff --git a/packages/obsidian-plugin/src/types.ts b/packages/obsidian-plugin/src/types.ts index f9edd328..63acbc3c 100644 --- a/packages/obsidian-plugin/src/types.ts +++ b/packages/obsidian-plugin/src/types.ts @@ -1,12 +1,19 @@ -declare module "obsidian" { - interface McpToolsPluginSettings { - version?: string; - } +export interface McpToolsLocalRestApiSettings { + host: string; + useHttp: boolean; + httpPort: number; + httpsPort: number; + baseUrl?: string; +} +export interface McpToolsPluginSettings { + version?: string; + localRestApi: McpToolsLocalRestApiSettings; +} + +declare module "obsidian" { interface Plugin { loadData(): Promise; saveData(data: McpToolsPluginSettings): Promise; } } - -export {};