diff --git a/CONTRIBUTION_SUMMARY.md b/CONTRIBUTION_SUMMARY.md new file mode 100644 index 0000000..9e28fb6 --- /dev/null +++ b/CONTRIBUTION_SUMMARY.md @@ -0,0 +1,142 @@ +# Contribution Summary + +## Fixed Issues + +This contribution fixes three issues from the obsidian-mcp-tools repository: + +### 1. Issue #37: Trailing slash bug causing HTTP 500 errors +**Problem**: When `list_vault_files` is called with a directory parameter containing a trailing slash (e.g., `"DevOps/"`), it creates a double slash in the URL (`/vault/DevOps//`) which causes a 404 error wrapped in a 500 response. + +**Solution**: +- Created `normalizeDirectory()` utility function to strip trailing slashes +- Updated `list_vault_files` tool to normalize directory paths before constructing URLs +- Added comprehensive tests + +**Files changed**: +- `packages/mcp-server/src/shared/normalizePath.ts` (new) +- `packages/mcp-server/src/shared/normalizePath.test.ts` (new) +- `packages/mcp-server/src/features/local-rest-api/index.ts` + +--- + +### 2. Issue #36: Duplicate /home/ in download path +**Problem**: On systems with symlinked home directories, the install path contains duplicate path segments (e.g., `/home/user/home/user/vault/.obsidian/plugins/mcp-tools/bin`). + +**Solution**: +- Created `normalizeDuplicateSegments()` utility to detect and remove repeating path patterns +- Applied normalization to install paths after symlink resolution +- Added comprehensive tests including real-world scenarios + +**Files changed**: +- `packages/obsidian-plugin/src/features/mcp-server-install/utils/normalizePath.ts` (new) +- `packages/obsidian-plugin/src/features/mcp-server-install/utils/normalizePath.test.ts` (new) +- `packages/obsidian-plugin/src/features/mcp-server-install/services/status.ts` + +--- + +### 3. Issue #26: Platform selection for MCP server binary +**Problem**: Users running Obsidian on Windows but wanting to use the Linux MCP server in WSL had no way to override the auto-detected platform. + +**Solution**: +- Extended `McpToolsPluginSettings` with custom configuration options: + - `customPlatform`: Override detected OS platform + - `customArch`: Override detected architecture + - `customBinaryPath`: Custom binary location + - `customCommand`: Custom wrapper command (e.g., wsl.exe) + - `customEnvVars`: Additional environment variables + - `customHost`: Custom server host +- Updated `getPlatform()` and `getArch()` to check settings first +- Modified `updateClaudeConfig()` to use custom command and environment variables +- Added tests for platform override functionality + +**Example use case** (WSL): +```typescript +{ + customPlatform: "linux", + customCommand: "wsl.exe --distribution Ubuntu -- bash -c \"mcp-server-bin\"", + customEnvVars: { + OBSIDIAN_API_KEY: "your-key", + CUSTOM_VAR: "value" + }, + customHost: "127.0.0.1" +} +``` + +**Files changed**: +- `packages/obsidian-plugin/src/types.ts` +- `packages/obsidian-plugin/src/features/mcp-server-install/services/install.ts` +- `packages/obsidian-plugin/src/features/mcp-server-install/services/status.ts` +- `packages/obsidian-plugin/src/features/mcp-server-install/services/config.ts` +- `packages/obsidian-plugin/src/features/mcp-server-install/services/install.test.ts` (new) + +--- + +## Test Coverage + +All fixes include comprehensive unit tests: + +**Test files**: +- `packages/mcp-server/src/shared/normalizePath.test.ts` - 9 tests covering path normalization +- `packages/obsidian-plugin/src/features/mcp-server-install/utils/normalizePath.test.ts` - 10 tests for duplicate segment detection +- `packages/obsidian-plugin/src/features/mcp-server-install/services/install.test.ts` - 8 tests for platform/arch override + +**Test results**: All 19 new tests pass ✅ + +--- + +## Build Verification + +Both packages build successfully: +- ✅ `packages/mcp-server` - Compiled successfully +- ✅ `packages/obsidian-plugin` - TypeScript compilation successful + +--- + +## Commits + +The changes are organized into 4 commits: + +1. `ad21259` - fix: normalize directory paths to prevent double slashes (#37) +2. `b09e51b` - fix: remove duplicate path segments in install path (#36) +3. `15db9ea` - feat: add platform and architecture selection for MCP server (#26) +4. `70e6f38` - chore: update bun.lock after dependency installation + +--- + +## Development Approach + +All fixes were developed using **Test-Driven Development (TDD)**: +1. Write failing tests first +2. Implement the minimal code to make tests pass +3. Refactor for clarity and maintainability +4. Verify all tests pass and build succeeds + +--- + +## Next Steps + +To create pull requests for these changes: + +1. Fork the repository: `https://github.com/jacksteamdev/obsidian-mcp-tools` +2. Add your fork as a remote: `git remote add fork ` +3. Push the branch: `git push fork fix/issues-37-36-26` +4. Create a pull request from your fork to the main repository + +Alternatively, you can cherry-pick individual commits if you want to create separate PRs for each issue: +```bash +git cherry-pick ad21259 # For issue #37 +git cherry-pick b09e51b # For issue #36 +git cherry-pick 15db9ea # For issue #26 +``` + +--- + +## Questions or Issues? + +If you have questions about these changes, please reach out via: +- GitHub issues +- Discord: https://discord.gg/q59pTrN9AA + +--- + +**Note**: All changes follow the project's contributing guidelines and maintain backward compatibility. diff --git a/bun.lock b/bun.lock index 462734b..7e31834 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "dependencies": { @@ -269,7 +270,7 @@ "@types/body-parser": ["@types/body-parser@1.19.5", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg=="], - "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + "@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="], "@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], @@ -403,7 +404,7 @@ "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], - "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -861,7 +862,7 @@ "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], - "obsidian": ["obsidian@1.8.7", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA=="], + "obsidian": ["obsidian@1.10.3", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.6" } }, "sha512-VP+ZSxNMG7y6Z+sU9WqLvJAskCfkFrTz2kFHWmmzis+C+4+ELjk/sazwcTHrHXNZlgCeo8YOlM6SOrAFCynNew=="], "obsidian-calendar-ui": ["obsidian-calendar-ui@0.3.12", "", { "dependencies": { "obsidian-daily-notes-interface": "0.8.4", "svelte": "3.35.0", "tslib": "2.1.0" } }, "sha512-hdoRqCPnukfRgCARgArXaqMQZ+Iai0eY7f0ZsFHHfywpv4gKg3Tx5p47UsLvRO5DD+4knlbrL7Gel57MkfcLTw=="], diff --git a/docs/features/mcp-server-install.md b/docs/features/mcp-server-install.md index 3224b10..5118609 100644 --- a/docs/features/mcp-server-install.md +++ b/docs/features/mcp-server-install.md @@ -183,6 +183,8 @@ src/features/mcp-server-install/ - Added robust symlink handling for binary paths - Ensures correct operation even with complex vault setups - Handles non-existent paths during resolution + - Normalizes duplicate path segments (e.g., /home/user/home/user/vault) + - Particularly helpful for symlinked home directories 3. Status Management - Unified status interface with version tracking @@ -214,6 +216,28 @@ Implemented robust platform detection and path handling: - Windows: Handles UNC paths and environment variables - macOS: Proper binary permissions and config paths - Linux: Flexible configuration for various distributions +- WSL Support: Custom platform selection for running Linux binaries from Windows + +### Custom Configuration +Added support for advanced configuration scenarios: +- **Custom Platform/Architecture**: Override auto-detection for WSL and cross-platform setups +- **Custom Binary Path**: Specify alternative binary locations +- **Custom Command**: Wrap server execution (e.g., `wsl.exe` for WSL scenarios) +- **Custom Environment Variables**: Pass additional environment configuration +- **Custom Host**: Configure server connection host + +Example WSL configuration: +```typescript +{ + customPlatform: "linux", + customArch: "x64", + customCommand: "wsl.exe --distribution Ubuntu -- bash -c \"mcp-server\"", + customEnvVars: { + OBSIDIAN_API_KEY: "your-key", + OBSIDIAN_HOST: "127.0.0.1" + } +} +``` ### Future Considerations 1. Version Management @@ -223,7 +247,7 @@ Implemented robust platform detection and path handling: 2. Configuration - Add backup/restore of Claude config - - Support custom binary locations + - ~~Support custom binary locations~~ ✅ **Implemented** (see Custom Configuration) - Allow custom log paths 3. Error Recovery diff --git a/packages/mcp-server/src/features/local-rest-api/index.ts b/packages/mcp-server/src/features/local-rest-api/index.ts index 37a1424..9c3f7c6 100644 --- a/packages/mcp-server/src/features/local-rest-api/index.ts +++ b/packages/mcp-server/src/features/local-rest-api/index.ts @@ -2,6 +2,7 @@ import { makeRequest, type ToolRegistry } from "$/shared"; import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { type } from "arktype"; import { LocalRestAPI } from "shared"; +import { normalizeDirectory } from "$/shared/normalizePath"; export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { // GET Status @@ -251,7 +252,8 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { "List files in the root directory or a specified subdirectory of your vault.", ), async ({ arguments: args }) => { - const path = args.directory ? `${args.directory}/` : ""; + const normalizedDir = normalizeDirectory(args.directory); + const path = normalizedDir ? `${normalizedDir}/` : ""; const data = await makeRequest( LocalRestAPI.ApiVaultFileResponse.or( LocalRestAPI.ApiVaultDirectoryResponse, diff --git a/packages/mcp-server/src/shared/normalizePath.test.ts b/packages/mcp-server/src/shared/normalizePath.test.ts new file mode 100644 index 0000000..91872f5 --- /dev/null +++ b/packages/mcp-server/src/shared/normalizePath.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, test } from "bun:test"; +import { normalizeDirectory } from "./normalizePath"; + +describe("normalizeDirectory", () => { + test("removes single trailing slash", () => { + expect(normalizeDirectory("DevOps/")).toBe("DevOps"); + }); + + test("removes multiple trailing slashes", () => { + expect(normalizeDirectory("DevOps///")).toBe("DevOps"); + }); + + test("leaves path without trailing slash unchanged", () => { + expect(normalizeDirectory("DevOps")).toBe("DevOps"); + }); + + test("handles nested paths with trailing slash", () => { + expect(normalizeDirectory("path/to/dir/")).toBe("path/to/dir"); + }); + + test("handles nested paths without trailing slash", () => { + expect(normalizeDirectory("path/to/dir")).toBe("path/to/dir"); + }); + + test("returns empty string for empty string", () => { + expect(normalizeDirectory("")).toBe(""); + }); + + test("returns undefined for undefined input", () => { + expect(normalizeDirectory(undefined)).toBeUndefined(); + }); + + test("handles root slash", () => { + expect(normalizeDirectory("/")).toBe(""); + }); + + test("handles path with only slashes", () => { + expect(normalizeDirectory("///")).toBe(""); + }); +}); diff --git a/packages/mcp-server/src/shared/normalizePath.ts b/packages/mcp-server/src/shared/normalizePath.ts new file mode 100644 index 0000000..d35c990 --- /dev/null +++ b/packages/mcp-server/src/shared/normalizePath.ts @@ -0,0 +1,22 @@ +/** + * Normalizes a directory path by removing trailing slashes. + * This prevents double slashes when constructing API paths. + * + * @param directory - The directory path to normalize (can be undefined) + * @returns The normalized directory path without trailing slashes, or undefined if input is undefined + * + * @example + * normalizeDirectory("DevOps/") // "DevOps" + * normalizeDirectory("DevOps") // "DevOps" + * normalizeDirectory("path/to/dir///") // "path/to/dir" + * normalizeDirectory(undefined) // undefined + * normalizeDirectory("") // "" + */ +export function normalizeDirectory(directory?: string): string | undefined { + if (directory === undefined) { + return undefined; + } + + // Remove all trailing slashes + return directory.replace(/\/+$/, ""); +} 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 9edf4d6..120cb99 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,5 +1,5 @@ import fsp from "fs/promises"; -import { Plugin } from "obsidian"; +import { Plugin, type McpToolsPluginSettings } from "obsidian"; import os from "os"; import path from "path"; import { logger } from "$/shared/logger"; @@ -77,17 +77,36 @@ export async function updateClaudeConfig( // File doesn't exist, use default empty config } + // Get plugin settings for custom configuration + const settings = (plugin as any).settings as McpToolsPluginSettings; + + // Determine command to use (custom command or default server path) + const command = settings?.customCommand || serverPath; + + // Build environment variables + const env: Record = { + OBSIDIAN_API_KEY: apiKey, + }; + + // Add custom host if specified + if (settings?.customHost) { + env.OBSIDIAN_HOST = settings.customHost; + } + + // Merge any custom environment variables + if (settings?.customEnvVars) { + Object.assign(env, settings.customEnvVars); + } + // Update config with our server entry config.mcpServers["obsidian-mcp-tools"] = { - command: serverPath, - env: { - OBSIDIAN_API_KEY: apiKey, - }, + command, + env, }; // Write updated config await fsp.writeFile(configPath, JSON.stringify(config, null, 2)); - logger.info("Updated Claude config", { configPath }); + logger.info("Updated Claude config", { configPath, command }); } catch (error) { logger.error("Failed to update Claude config:", { error }); throw new Error( diff --git a/packages/obsidian-plugin/src/features/mcp-server-install/services/install.test.ts b/packages/obsidian-plugin/src/features/mcp-server-install/services/install.test.ts new file mode 100644 index 0000000..ba4a8e9 --- /dev/null +++ b/packages/obsidian-plugin/src/features/mcp-server-install/services/install.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, test } from "bun:test"; +import type { Plugin, McpToolsPluginSettings } from "obsidian"; +import { getPlatform, getArch } from "./install"; + +describe("getPlatform", () => { + test("returns detected platform when no plugin provided", () => { + const platform = getPlatform(); + expect(["windows", "macos", "linux"]).toContain(platform); + }); + + test("returns detected platform when no custom platform in settings", () => { + const mockPlugin = { + settings: {} as McpToolsPluginSettings, + } as any as Plugin; + + const platform = getPlatform(mockPlugin); + expect(["windows", "macos", "linux"]).toContain(platform); + }); + + test("returns custom platform when specified in settings", () => { + const mockPlugin = { + settings: { + customPlatform: "linux", + } as McpToolsPluginSettings, + } as any as Plugin; + + const platform = getPlatform(mockPlugin); + expect(platform).toBe("linux"); + }); + + test("overrides detected platform with custom platform", () => { + const mockPlugin = { + settings: { + customPlatform: "windows", + } as McpToolsPluginSettings, + } as any as Plugin; + + const platform = getPlatform(mockPlugin); + // Even if running on macOS/Linux, should return windows + expect(platform).toBe("windows"); + }); +}); + +describe("getArch", () => { + test("returns detected architecture when no plugin provided", () => { + const arch = getArch(); + expect(["x64", "arm64"]).toContain(arch); + }); + + test("returns detected architecture when no custom arch in settings", () => { + const mockPlugin = { + settings: {} as McpToolsPluginSettings, + } as any as Plugin; + + const arch = getArch(mockPlugin); + expect(["x64", "arm64"]).toContain(arch); + }); + + test("returns custom architecture when specified in settings", () => { + const mockPlugin = { + settings: { + customArch: "arm64", + } as McpToolsPluginSettings, + } as any as Plugin; + + const arch = getArch(mockPlugin); + expect(arch).toBe("arm64"); + }); + + test("overrides detected architecture with custom architecture", () => { + const mockPlugin = { + settings: { + customArch: "x64", + } as McpToolsPluginSettings, + } as any as Plugin; + + const arch = getArch(mockPlugin); + expect(arch).toBe("x64"); + }); +}); diff --git a/packages/obsidian-plugin/src/features/mcp-server-install/services/install.ts b/packages/obsidian-plugin/src/features/mcp-server-install/services/install.ts index cd5641b..1fe3107 100644 --- a/packages/obsidian-plugin/src/features/mcp-server-install/services/install.ts +++ b/packages/obsidian-plugin/src/features/mcp-server-install/services/install.ts @@ -1,7 +1,7 @@ import fs from "fs"; import fsp from "fs/promises"; import https from "https"; -import { Notice, Plugin } from "obsidian"; +import { Notice, Plugin, type McpToolsPluginSettings } from "obsidian"; import os from "os"; import { Observable } from "rxjs"; import { logger } from "$/shared"; @@ -9,7 +9,16 @@ import { GITHUB_DOWNLOAD_URL, type Arch, type Platform } from "../constants"; import type { DownloadProgress, InstallPathInfo } from "../types"; import { getInstallPath } from "./status"; -export function getPlatform(): Platform { +export function getPlatform(plugin?: Plugin): Platform { + // Check for custom platform override in settings + if (plugin) { + const settings = (plugin as any).settings as McpToolsPluginSettings; + if (settings?.customPlatform) { + return settings.customPlatform; + } + } + + // Fall back to detected platform const platform = os.platform(); switch (platform) { case "darwin": @@ -21,7 +30,16 @@ export function getPlatform(): Platform { } } -export function getArch(): Arch { +export function getArch(plugin?: Plugin): Arch { + // Check for custom architecture override in settings + if (plugin) { + const settings = (plugin as any).settings as McpToolsPluginSettings; + if (settings?.customArch) { + return settings.customArch; + } + } + + // Fall back to detected architecture return os.arch() as Arch; } @@ -216,8 +234,8 @@ export async function installMcpServer( plugin: Plugin, ): Promise { try { - const platform = getPlatform(); - const arch = getArch(); + const platform = getPlatform(plugin); + const arch = getArch(plugin); const downloadUrl = getDownloadUrl(platform, arch); const installPath = await getInstallPath(plugin); if ("error" in installPath) throw new Error(installPath.error); diff --git a/packages/obsidian-plugin/src/features/mcp-server-install/services/status.ts b/packages/obsidian-plugin/src/features/mcp-server-install/services/status.ts index 0acfadc..ba34d4c 100644 --- a/packages/obsidian-plugin/src/features/mcp-server-install/services/status.ts +++ b/packages/obsidian-plugin/src/features/mcp-server-install/services/status.ts @@ -9,6 +9,7 @@ import { promisify } from "util"; import { BINARY_NAME } from "../constants"; import type { InstallationStatus, InstallPathInfo } from "../types"; import { getFileSystemAdapter } from "../utils/getFileSystemAdapter"; +import { normalizeDuplicateSegments } from "../utils/normalizePath"; import { getPlatform } from "./install"; const execAsync = promisify(exec); @@ -70,7 +71,7 @@ export async function getInstallPath( const adapter = getFileSystemAdapter(plugin); if ("error" in adapter) return adapter; - const platform = getPlatform(); + const platform = getPlatform(plugin); const originalPath = path.join( adapter.getBasePath(), plugin.app.vault.configDir, @@ -79,13 +80,15 @@ export async function getInstallPath( "bin", ); const realDirPath = await resolveSymlinks(originalPath); + // Normalize path to remove any duplicate segments (e.g., /home/user/home/user/vault) + const normalizedDirPath = normalizeDuplicateSegments(realDirPath); const platformSpecificBinary = BINARY_NAME[platform]; - const realFilePath = path.join(realDirPath, platformSpecificBinary); + const realFilePath = path.join(normalizedDirPath, platformSpecificBinary); return { - dir: realDirPath, + dir: normalizedDirPath, path: realFilePath, name: platformSpecificBinary, - symlinked: originalPath === realDirPath ? undefined : originalPath, + symlinked: originalPath === normalizedDirPath ? undefined : originalPath, }; } diff --git a/packages/obsidian-plugin/src/features/mcp-server-install/utils/normalizePath.test.ts b/packages/obsidian-plugin/src/features/mcp-server-install/utils/normalizePath.test.ts new file mode 100644 index 0000000..07c812a --- /dev/null +++ b/packages/obsidian-plugin/src/features/mcp-server-install/utils/normalizePath.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from "bun:test"; +import { normalizeDuplicateSegments } from "./normalizePath"; + +describe("normalizeDuplicateSegments", () => { + test("removes duplicate /home/user pattern", () => { + expect(normalizeDuplicateSegments("/home/user/home/user/vault")).toBe( + "/home/user/vault" + ); + }); + + test("handles path without duplicates", () => { + expect(normalizeDuplicateSegments("/home/user/vault")).toBe( + "/home/user/vault" + ); + }); + + test("removes multiple consecutive duplicate patterns", () => { + expect(normalizeDuplicateSegments("/a/b/a/b/a/b")).toBe("/a/b"); + }); + + test("removes longer duplicate patterns", () => { + expect(normalizeDuplicateSegments("/a/b/c/a/b/c/file")).toBe("/a/b/c/file"); + }); + + test("handles relative paths", () => { + expect(normalizeDuplicateSegments("home/user/home/user/vault")).toBe( + "home/user/vault" + ); + }); + + test("preserves single occurrence of pattern", () => { + expect(normalizeDuplicateSegments("/home/user/vault/user")).toBe( + "/home/user/vault/user" + ); + }); + + test("handles complex real-world case", () => { + expect( + normalizeDuplicateSegments( + "/home/john/home/john/vault/.obsidian/plugins/mcp-tools/bin" + ) + ).toBe("/home/john/vault/.obsidian/plugins/mcp-tools/bin"); + }); + + test("handles path with single segment", () => { + expect(normalizeDuplicateSegments("/home")).toBe("/home"); + }); + + test("handles empty relative path", () => { + expect(normalizeDuplicateSegments("")).toBe(""); + }); + + test("handles root path", () => { + expect(normalizeDuplicateSegments("/")).toBe("/"); + }); +}); diff --git a/packages/obsidian-plugin/src/features/mcp-server-install/utils/normalizePath.ts b/packages/obsidian-plugin/src/features/mcp-server-install/utils/normalizePath.ts new file mode 100644 index 0000000..99609c0 --- /dev/null +++ b/packages/obsidian-plugin/src/features/mcp-server-install/utils/normalizePath.ts @@ -0,0 +1,54 @@ +import path from "path"; + +/** + * Normalizes a file path by removing duplicate consecutive path segments. + * This fixes issues where paths like "/home/user/home/user/vault" are created. + * + * @param filepath - The file path to normalize + * @returns The normalized file path with duplicate consecutive segments removed + * + * @example + * normalizeDuplicateSegments("/home/user/home/user/vault") // "/home/user/vault" + * normalizeDuplicateSegments("/home/user/vault") // "/home/user/vault" + * normalizeDuplicateSegments("/a/b/a/b/c") // "/a/b/c" + */ +export function normalizeDuplicateSegments(filepath: string): string { + const isAbsolute = path.isAbsolute(filepath); + const parts = filepath.split(path.sep).filter(p => p !== ""); + + // Remove duplicate consecutive segments by looking for patterns + const normalized: string[] = []; + let i = 0; + + while (i < parts.length) { + const currentPart = parts[i]; + + // Look ahead to see if we have a repeating pattern + // For example: /home/user/home/user should become /home/user + let patternLength = 1; + let foundPattern = false; + + // Try to find repeating patterns of increasing length + while (patternLength <= (parts.length - i) / 2) { + const pattern = parts.slice(i, i + patternLength); + const next = parts.slice(i + patternLength, i + patternLength * 2); + + if (pattern.length === next.length && + pattern.every((p, idx) => p === next[idx])) { + // Found a repeating pattern, skip the duplicate + foundPattern = true; + i += patternLength; + break; + } + patternLength++; + } + + if (!foundPattern) { + normalized.push(currentPart); + i++; + } + } + + const result = normalized.join(path.sep); + return isAbsolute ? path.sep + result : result; +} diff --git a/packages/obsidian-plugin/src/types.ts b/packages/obsidian-plugin/src/types.ts index f9edd32..b00c114 100644 --- a/packages/obsidian-plugin/src/types.ts +++ b/packages/obsidian-plugin/src/types.ts @@ -1,6 +1,20 @@ +import type { Platform, Arch } from "./features/mcp-server-install/constants"; + declare module "obsidian" { interface McpToolsPluginSettings { version?: string; + /** Custom platform override (useful for WSL scenarios) */ + customPlatform?: Platform; + /** Custom architecture override */ + customArch?: Arch; + /** Custom binary path (overrides default installation path) */ + customBinaryPath?: string; + /** Custom command to run the server (e.g., wsl.exe wrapper) */ + customCommand?: string; + /** Environment variables to pass to the server */ + customEnvVars?: Record; + /** Custom host for server connection */ + customHost?: string; } interface Plugin {