diff --git a/.gitignore b/.gitignore index f19ddb4..98669c8 100644 --- a/.gitignore +++ b/.gitignore @@ -181,3 +181,14 @@ bin/ docs/planning/ data.json cline_docs/temp/ +cline_docs/codebaseSummary.md +cline_docs/currentTask.md +cline_docs/projectRoadmap.md +cline_docs/techStack.md +cline_docs/projectVision.md +cline_docs/task_setup_github_mcp_server.md +packages/mcp-server/package.json +bun.lock +packages/mcp-server/package.json +packages/obsidian-plugin/package.json +packages/obsidian-plugin/package.json diff --git a/bun.lock b/bun.lock index 498da90..07c40e2 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ }, "devDependencies": { "@types/bun": "latest", + "@types/node": "^24.0.0", "@types/turndown": "^5.0.5", "prettier": "^3.4.2", "typescript": "^5.3.3", @@ -28,6 +29,7 @@ "packages/obsidian-plugin": { "name": "@obsidian-mcp-tools/obsidian-plugin", "dependencies": { + "@obsidian-mcp-tools/mcp-server": "workspace:*", "@types/fs-extra": "^11.0.4", "arktype": "^2.0.0-rc.30", "express": "^4.21.2", @@ -266,7 +268,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.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="], + "@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="], "@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], @@ -290,7 +292,7 @@ "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], - "@types/node": ["@types/node@16.18.122", "", {}, "sha512-rF6rUBS80n4oK16EW8nE75U+9fw0SSUgoPtWSvHhPXdT7itbvmS7UjB/jyM8i3AkvI6yeSM5qCwo+xN0npGDHg=="], + "@types/node": ["@types/node@24.0.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg=="], "@types/qs": ["@types/qs@6.9.17", "", {}, "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ=="], @@ -308,8 +310,6 @@ "@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="], - "@types/ws": ["@types/ws@8.5.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@5.29.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "5.29.0", "@typescript-eslint/type-utils": "5.29.0", "@typescript-eslint/utils": "5.29.0", "debug": "^4.3.4", "functional-red-black-tree": "^1.0.1", "ignore": "^5.2.0", "regexpp": "^3.2.0", "semver": "^7.3.7", "tsutils": "^3.21.0" }, "peerDependencies": { "@typescript-eslint/parser": "^5.0.0", "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-kgTsISt9pM53yRFQmLZ4npj99yGl3x3Pl7z4eA66OuTzAGC4bQB5H5fuLwPnqTKU3yyrrg4MIhjF17UYnL4c0w=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@5.29.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "5.29.0", "@typescript-eslint/types": "5.29.0", "@typescript-eslint/typescript-estree": "5.29.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw=="], @@ -400,7 +400,7 @@ "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], - "bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="], + "bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -1154,7 +1154,7 @@ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], - "undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], @@ -1216,6 +1216,8 @@ "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + "@obsidian-mcp-tools/obsidian-plugin/@types/node": ["@types/node@16.18.122", "", {}, "sha512-rF6rUBS80n4oK16EW8nE75U+9fw0SSUgoPtWSvHhPXdT7itbvmS7UjB/jyM8i3AkvI6yeSM5qCwo+xN0npGDHg=="], + "@sveltejs/kit/cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], "@sveltejs/kit/esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], @@ -1236,8 +1238,6 @@ "@types/serve-static/@types/node": ["@types/node@20.17.10", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA=="], - "@types/ws/@types/node": ["@types/node@20.17.10", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA=="], - "@typescript-eslint/eslint-plugin/eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], "@typescript-eslint/parser/eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], @@ -1254,8 +1254,6 @@ "body-parser/raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], - "bun-types/@types/node": ["@types/node@20.17.10", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA=="], - "chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], "cross-spawn/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], @@ -1348,6 +1346,22 @@ "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "@types/body-parser/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + + "@types/connect/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + + "@types/express-serve-static-core/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + + "@types/fs-extra/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + + "@types/jsonfile/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + + "@types/response-time/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + + "@types/send/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + + "@types/serve-static/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + "@typescript-eslint/eslint-plugin/eslint/@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], "@typescript-eslint/eslint-plugin/eslint/@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], diff --git a/packages/mcp-server/README_ClaudeDesktopSetup.md b/packages/mcp-server/README_ClaudeDesktopSetup.md new file mode 100644 index 0000000..d9cbf55 --- /dev/null +++ b/packages/mcp-server/README_ClaudeDesktopSetup.md @@ -0,0 +1,89 @@ +# Setting up Obsidian MCP Tools Server with Claude Desktop + +This guide explains how to compile and configure the Obsidian MCP Tools server to work with Claude Desktop. + +## 1. Compile the MCP Server + +The MCP server is located in the `packages/mcp-server` directory. You will need Bun installed to compile it. + +1. **Navigate to the server directory:** + ```bash + cd packages/mcp-server + ``` +2. **Install dependencies:** + ```bash + bun install + ``` +3. **Compile the server:** + ```bash + bun run build + ``` + This will create an executable file (e.g., `mcp-server`) in the `packages/mcp-server/dist` directory. + +## 2. Configure Vaults (`vaults.json`) + +The MCP server needs to know which Obsidian vaults it can access. This is configured via a `vaults.json` file. + +1. **Create the `vaults.json` file:** + This file should be placed in a location accessible by the server. A common practice is to place it in a user's configuration directory or alongside the compiled server executable. For example, on macOS, you might place it in `~/Library/Application Support/obsidian-mcp-tools/vaults.json`. + +2. **Example `vaults.json` content:** + ```json + { + "defaultVaultId": "your-default-vault-id", + "vaults": [ + { + "vaultId": "your-first-vault-id", + "name": "My Main Obsidian Vault", + "path": "/path/to/your/Obsidian/Vault" + }, + { + "vaultId": "your-second-vault-id", + "name": "My Second Obsidian Vault", + "path": "/path/to/your/Another/Obsidian/Vault" + } + ] + } + ``` + * Replace `"your-default-vault-id"`, `"your-first-vault-id"`, `"your-second-vault-id"` with unique identifiers for your vaults. These IDs are used by Claude Desktop to specify which vault to interact with. + * Replace `"My Main Obsidian Vault"` and `"My Second Obsidian Vault"` with human-readable names for your vaults. + * Replace `"/path/to/your/Obsidian/Vault"` with the actual absolute path to your Obsidian vault folder on your system. + +## 3. Configure Claude Desktop + +To connect Claude Desktop to your MCP server, you need to add an entry to its `config.json` file. + +1. **Locate Claude Desktop's `config.json`:** + The location of this file varies by operating system. + * **macOS:** `~/Library/Application Support/Claude/config.json` + * **Windows:** `%APPDATA%\Claude\config.json` + * **Linux:** `~/.config/Claude/config.json` + +2. **Add the server configuration:** + Open `config.json` and add an entry under the `mcp` section. If the `mcp` section doesn't exist, create it. + + ```json + { + "mcp": { + "servers": [ + { + "name": "obsidian-mcp-tools", + "command": "/path/to/your/compiled/mcp-server/dist/mcp-server", + "environment": { + "MCP_VAULTS_CONFIG_PATH": "/path/to/your/vaults.json" + } + } + ] + } + // ... other Claude config ... + } + ``` + * Replace `"/path/to/your/compiled/mcp-server/dist/mcp-server"` with the absolute path to the executable you compiled in step 1. + * Replace `"/path/to/your/vaults.json"` with the absolute path to the `vaults.json` file you created in step 2. This environment variable tells the MCP server where to find its configuration. + +## 4. Start Claude Desktop and Test + +After saving the `config.json` file, restart Claude Desktop. The `obsidian-mcp-tools` server should now be available. You can test it by trying to use a tool that lists vaults, for example, if such a tool is exposed by the server. + +--- +**Reference:** This server is part of the larger [obsidian-mcp-tools](https://github.com/jacksteamdev/obsidian-mcp-tools) project. diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 899e799..77fd72f 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -3,16 +3,18 @@ "description": "A secure MCP server implementation that provides standardized access to Obsidian vaults through the Model Context Protocol.", "type": "module", "module": "src/index.ts", + "types": "dist/types/src/index.d.ts", "scripts": { "dev": "bun build ./src/index.ts --watch --compile --outfile ../../bin/mcp-server", - "build": "bun build ./src/index.ts --compile --outfile dist/mcp-server", - "build:linux": "bun build --compile --minify --sourcemap --target=bun-linux-x64-baseline ./src/index.ts --outfile dist/mcp-server-linux", - "build:mac-arm64": "bun build --compile --minify --sourcemap --target=bun-darwin-arm64 ./src/index.ts --outfile dist/mcp-server-macos-arm64", - "build:mac-x64": "bun build --compile --minify --sourcemap --target=bun-darwin-x64 ./src/index.ts --outfile dist/mcp-server-macos-x64", - "build:windows": "bun build --compile --minify --sourcemap --target=bun-windows-x64-baseline ./src/index.ts --outfile dist/mcp-server-windows", + "build": "bun run build:declarations && bun build ./src/index.ts --compile --outfile dist/mcp-server", + "build:declarations": "tsc -p tsconfig.json", + "build:linux": "bun run build:declarations && bun build --compile --minify --sourcemap --target=bun-linux-x64-baseline ./src/index.ts --outfile dist/mcp-server-linux", + "build:mac-arm64": "bun run build:declarations && bun build --compile --minify --sourcemap --target=bun-darwin-arm64 ./src/index.ts --outfile dist/mcp-server-macos-arm64", + "build:mac-x64": "bun run build:declarations && bun build --compile --minify --sourcemap --target=bun-darwin-x64 ./src/index.ts --outfile dist/mcp-server-macos-x64", + "build:windows": "bun run build:declarations && bun build --compile --minify --sourcemap --target=bun-windows-x64-baseline ./src/index.ts --outfile dist/mcp-server-windows", "check": "tsc --noEmit", "inspector": "npx @modelcontextprotocol/inspector bun src/index.ts", - "release": "run-s build:*", + "release": "bun run build:declarations && run-s build:linux build:mac-arm64 build:mac-x64 build:windows", "setup": "bun run ./scripts/install.ts", "test": "bun test ./src/**/*.test.ts" }, @@ -28,8 +30,9 @@ }, "devDependencies": { "@types/bun": "latest", + "@types/node": "^24.0.0", "@types/turndown": "^5.0.5", "prettier": "^3.4.2", "typescript": "^5.3.3" } -} \ No newline at end of file +} diff --git a/packages/mcp-server/src/features/core/index.ts b/packages/mcp-server/src/features/core/index.ts index 4d68661..2f479c9 100644 --- a/packages/mcp-server/src/features/core/index.ts +++ b/packages/mcp-server/src/features/core/index.ts @@ -1,21 +1,46 @@ import { logger, type ToolRegistry, ToolRegistryClass } from "$/shared"; -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { loadVaultsConfig, type LoadedConfig } from "$/shared/configManager"; // Import the new config loader and type +import { type VaultConfigEntry } from "$/types/config"; // Import VaultConfigEntry type +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; // Removed 'type Tool' import import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; +import { type } from "arktype"; // Added ArkType import { registerFetchTool } from "../fetch"; -import { registerLocalRestApiTools } from "../local-rest-api"; +import { registerLocalRestApiTools } from "../local-rest-api"; // Fixed typo here import { setupObsidianPrompts } from "../prompts"; import { registerSmartConnectionsTools } from "../smart-connections"; import { registerTemplaterTools } from "../templates"; import { CallToolRequestSchema, ListToolsRequestSchema, + type Result, // Import Result } from "@modelcontextprotocol/sdk/types.js"; +// Define types for the list_configured_vaults tool handler return value +type ListVaultsToolResultContent = ({ type: "text", text: string } | { type: "image", data: string, mimeType: string })[]; +type ListVaultsToolReturnValue = { content: ListVaultsToolResultContent, isError?: boolean }; + +// Define schema for the list_configured_vaults tool output +const VaultInfoSchema = type({ + vaultId: "string", + name: "string", +}); +const ListConfiguredVaultsOutputSchema = VaultInfoSchema.array(); + +// Define the schema for the list_configured_vaults tool itself +export const ListConfiguredVaultsToolSchema = type({ + name: "'list_configured_vaults'", // Tool name is part of the schema + "arguments?": type({}).as>() // Optional empty record with correct type +}).describe("Lists all configured Obsidian vaults with their ID and name."); // Use .describe() for description + export class ObsidianMcpServer { private server: Server; private tools: ToolRegistry; + private vaultsConfig: VaultConfigEntry[] = []; // Store loaded vault configurations + private defaultVaultId?: string; // Store the default vault ID constructor() { + logger.debug("ObsidianMcpServer: Constructor called."); this.server = new Server( { name: "obsidian-mcp-tools", @@ -31,49 +56,219 @@ export class ObsidianMcpServer { this.tools = new ToolRegistryClass(); - this.setupHandlers(); - // Error handling this.server.onerror = (error) => { - logger.error("Server error", { error }); + logger.error("ObsidianMcpServer: Server error occurred.", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + code: (error instanceof McpError) ? error.code : undefined, + }); console.error("[MCP Tools Error]", error); }; process.on("SIGINT", async () => { + logger.debug("ObsidianMcpServer: SIGINT received, closing server."); await this.server.close(); + logger.debug("ObsidianMcpServer: Server closed, exiting process."); process.exit(0); }); + logger.debug("ObsidianMcpServer: Constructor finished."); + } + + private async initializeConfig() { + logger.debug("ObsidianMcpServer: Initializing configuration..."); + try { + const config = await loadVaultsConfig(); + this.vaultsConfig = config.vaults; + this.defaultVaultId = config.defaultVaultId; + + if (this.vaultsConfig.length === 0) { + logger.warn("ObsidianMcpServer: No vaults configured. Server will run but may not be able to perform vault-specific operations."); + } else { + logger.info(`ObsidianMcpServer: Loaded configuration for ${this.vaultsConfig.length} vault(s).`); + if (this.defaultVaultId) { + logger.info(`ObsidianMcpServer: Default vault ID set to: ${this.defaultVaultId}`); + } + } + logger.debug("ObsidianMcpServer: Configuration initialization finished."); + } catch (error) { + logger.fatal("ObsidianMcpServer: Failed to load vaults configuration during server startup.", { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); + // Depending on desired behavior, we might want to exit if config is critical and fails to load. + // For now, log fatal and continue, server might be unusable for vault operations. + // Consider throwing the error to halt server startup if config is absolutely essential. + } + } + + /** + * Gets the configuration for a vault by its ID. + * If no vaultId is provided, it will: + * 1. Use the configured default vault ID if available + * 2. Fall back to the first available vault if no default is set + * 3. Throw an error if no vaults are configured + * + * @param vaultId Optional vault ID to look up + * @returns The vault configuration entry + * @throws McpError if the vault is not found or no vaults are configured + */ + public getVaultConfig(vaultId?: string): VaultConfigEntry { + logger.debug(`ObsidianMcpServer: Attempting to get vault config for ID: "${vaultId || 'default/first available'}"`); + // Case 1: Specific vault ID provided + if (vaultId) { + const vault = this.vaultsConfig.find(v => v.vaultId === vaultId); + if (vault) { + logger.debug(`ObsidianMcpServer: Found vault config for ID: "${vaultId}"`); + return vault; + } + + logger.error(`ObsidianMcpServer: Vault config not found for ID: "${vaultId}"`); + throw new McpError( + ErrorCode.InvalidRequest, + `Configuration for vaultId "${vaultId}" not found or server not configured for this vault.` + ); + } + + // Case 2: Use configured default vault + if (this.defaultVaultId) { + const defaultVault = this.vaultsConfig.find(v => v.vaultId === this.defaultVaultId); + if (defaultVault) { + logger.debug(`ObsidianMcpServer: Using default vault: ${this.defaultVaultId}`); + return defaultVault; + } + logger.warn(`ObsidianMcpServer: Default vault ID "${this.defaultVaultId}" is configured but not found in vaults list. Falling back to first available.`); + } + + // Case 3: Use first available vault + if (this.vaultsConfig.length > 0) { + logger.debug(`ObsidianMcpServer: No vault ID provided or default not found, using first available vault: ${this.vaultsConfig[0].vaultId}`); + return this.vaultsConfig[0]; + } + + // Case 4: No vaults available + logger.error("ObsidianMcpServer: No vaults configured. Cannot get vault config."); + throw new McpError( + ErrorCode.InvalidRequest, + "No vaults configured. Please configure at least one vault." + ); + } + + public getVaultsConfig(): Readonly { // Public getter for all vault configs + logger.debug("ObsidianMcpServer: getVaultsConfig called."); + return this.vaultsConfig; + } + + // Handler for the list_configured_vaults tool + private async listConfiguredVaultsHandler( + _request: typeof ListConfiguredVaultsToolSchema.infer, + _context: { server: Server } + ): Promise { + logger.debug("ObsidianMcpServer: list_configured_vaults tool handler called."); + const vaultsConfig = this.getVaultsConfig(); + const dataToReturn = vaultsConfig + ? vaultsConfig.map(v => ({ vaultId: v.vaultId, name: v.name })) + : []; + + logger.debug("ObsidianMcpServer: list_configured_vaults tool returning data.", { + vaultCount: vaultsConfig.length, + data: dataToReturn + }); + + return { + content: [{ type: "text", text: JSON.stringify(dataToReturn) }] + }; } private setupHandlers() { - setupObsidianPrompts(this.server); + logger.debug("ObsidianMcpServer: Setting up handlers and registering tools..."); + // Pass 'this' (ObsidianMcpServer instance) to all feature setup/registration functions + // to provide access to vault configurations via getVaultConfig. + setupObsidianPrompts(this.server, this); // Pass ObsidianMcpServer instance + registerFetchTool(this.tools, this); // Pass ObsidianMcpServer instance + registerLocalRestApiTools(this.tools, this); // Fixed typo here + registerSmartConnectionsTools(this.tools, this); // Pass ObsidianMcpServer instance + registerTemplaterTools(this.tools, this); // Pass ObsidianMcpServer instance - registerFetchTool(this.tools, this.server); - registerLocalRestApiTools(this.tools, this.server); - registerSmartConnectionsTools(this.tools); - registerTemplaterTools(this.tools); + // Register the list_configured_vaults tool + logger.debug("ObsidianMcpServer: Registering list_configured_vaults tool."); + this.tools.register( + ListConfiguredVaultsToolSchema, + this.listConfiguredVaultsHandler.bind(this) + ); + logger.debug("ObsidianMcpServer: Setting request handler for ListToolsRequestSchema."); this.server.setRequestHandler(ListToolsRequestSchema, this.tools.list); + + logger.debug("ObsidianMcpServer: Setting request handler for CallToolRequestSchema."); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { - logger.debug("Handling request", { request }); - const response = await this.tools.dispatch(request.params, { - server: this.server, + logger.debug("ObsidianMcpServer: Handling CallToolRequest.", { + toolName: request.params.name, + toolArguments: request.params.arguments }); - logger.debug("Request handled", { response }); - return response; + // The dispatcher will need to be vault-aware or tools themselves will be. + // For now, this part remains, but tools.dispatch or individual tools + // will need to extract vaultId from request.params.arguments. + try { + const response = await this.tools.dispatch(request.params, { + server: this.server, // The core MCP server instance + // obsidianServer: this, // Removed: this context is passed via .bind() to the tool handler + }); + logger.debug("ObsidianMcpServer: CallToolRequest handled successfully.", { + toolName: request.params.name, + response: response + }); + return response; + } catch (dispatchError) { + logger.error("ObsidianMcpServer: Error dispatching tool.", { + toolName: request.params.name, + error: dispatchError instanceof Error ? dispatchError.message : String(dispatchError), + stack: dispatchError instanceof Error ? dispatchError.stack : undefined, + }); + // Re-throw or return an error response as appropriate for MCP protocol + throw dispatchError; + } }); + logger.debug("ObsidianMcpServer: Handlers setup finished."); } async run() { - logger.debug("Starting server..."); + logger.debug("ObsidianMcpServer: Starting server run process..."); + logger.debug("ObsidianMcpServer: Initializing server configuration..."); + await this.initializeConfig(); // Load config before connecting + + logger.debug("ObsidianMcpServer: Setting up handlers and registering tools..."); + this.setupHandlers(); // Setup handlers after config is loaded + + // Log the list of registered tools for debugging + const toolsList = this.tools.list(); + logger.debug("ObsidianMcpServer: Registered tools summary.", { + toolCount: toolsList.tools.length, + tools: toolsList.tools.map(t => t.name) + }); + + logger.debug("ObsidianMcpServer: Starting server transport connection..."); const transport = new StdioServerTransport(); try { await this.server.connect(transport); - logger.debug("Server started successfully"); + logger.debug("ObsidianMcpServer: Server started successfully and connected to transport."); } catch (err) { - logger.fatal("Failed to start server", { - error: err instanceof Error ? err.message : String(err), - }); - process.exit(1); + // Catch specific JSON parsing errors from stdin during initial connection + if (err instanceof SyntaxError && err.message.includes('JSON Parse error')) { + logger.error("ObsidianMcpServer: Malformed JSON input detected during server connection. Please ensure valid JSON is provided via stdin.", { + error: err.message, + stack: err.stack, + }); + // Do not exit, allow the server to continue running and wait for valid input + // The server might still be in a state to receive further input. + // If the server truly cannot recover, the outer process.on('uncaughtException') will catch it. + } else { + logger.fatal("ObsidianMcpServer: Failed to start server or connect to transport.", { + error: err instanceof Error ? err.message : String(err), + stack: err instanceof Error ? err.stack : undefined, + }); + process.exit(1); // Exit for other critical startup errors + } } + logger.debug("ObsidianMcpServer: Server run process finished."); } } diff --git a/packages/mcp-server/src/features/fetch/index.ts b/packages/mcp-server/src/features/fetch/index.ts index 628fd79..088910d 100644 --- a/packages/mcp-server/src/features/fetch/index.ts +++ b/packages/mcp-server/src/features/fetch/index.ts @@ -1,11 +1,13 @@ import { logger, type ToolRegistry } from "$/shared"; -import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; +// Import ObsidianMcpServer to match the updated signature from core/index.ts +import type { ObsidianMcpServer } from "../core"; import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; import { type } from "arktype"; import { DEFAULT_USER_AGENT } from "./constants"; import { convertHtmlToMarkdown } from "./services/markdown"; -export function registerFetchTool(tools: ToolRegistry, server: Server) { +// Signature updated to accept ObsidianMcpServer instance, though it's not used in this tool's current logic. +export function registerFetchTool(tools: ToolRegistry, obsServer: ObsidianMcpServer) { tools.register( type({ name: '"fetch"', 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..f961165 100644 --- a/packages/mcp-server/src/features/local-rest-api/index.ts +++ b/packages/mcp-server/src/features/local-rest-api/index.ts @@ -1,21 +1,34 @@ import { makeRequest, type ToolRegistry } from "$/shared"; -import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; +// Import ObsidianMcpServer to access its getVaultConfig method +import type { ObsidianMcpServer } from "$/features/core"; import { type } from "arktype"; import { LocalRestAPI } from "shared"; -export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { +// Modify function to accept obsidianServer instance +export function registerLocalRestApiTools(tools: ToolRegistry, obsidianServer: ObsidianMcpServer) { + const vaultConfigProvider = obsidianServer.getVaultConfig.bind(obsidianServer); + // GET Status tools.register( type({ name: '"get_server_info"', - arguments: "Record", + // Add vaultId to arguments + arguments: { vaultId: "string>0" }, }).describe( - "Returns basic details about the Obsidian Local REST API and authentication status. This is the only API request that does not require authentication.", + "Returns basic details about the Obsidian Local REST API and authentication status for a specific vault. This is the only API request that does not require authentication with the vault's API key, but vaultId is needed to target the correct Local REST API instance.", ), - async () => { - const data = await makeRequest(LocalRestAPI.ApiStatusResponse, "/"); + async ({ arguments: args }) => { + // Extract vaultId + const { vaultId } = args; + // Call makeRequest with vaultId and vaultConfigProvider + // Note: get_server_info might not strictly need the API key from vaultConfig for its specific endpoint, + // but it needs the correct localRestApiBaseUrl from the vault's config. + const data = await makeRequest(vaultId, LocalRestAPI.ApiStatusResponse, "/", vaultConfigProvider); return { - content: [{ type: "text", text: JSON.stringify(data, null, 2) }], + content: [ + { type: "text", text: JSON.stringify(data, null, 2) }, + { type: "text", text: `Vault used: ${vaultId}` } + ], }; }, ); @@ -25,19 +38,23 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { type({ name: '"get_active_file"', arguments: { + vaultId: "string>0", // Added vaultId format: type('"markdown" | "json"').optional(), }, }).describe( - "Returns the content of the currently active file in Obsidian. Can return either markdown content or a JSON representation including parsed tags and frontmatter.", + "Returns the content of the currently active file in the specified Obsidian vault. Can return either markdown content or a JSON representation including parsed tags and frontmatter.", ), async ({ arguments: args }) => { + const { vaultId } = args; // Extract vaultId const format = args?.format === "json" ? "application/vnd.olrapi.note+json" : "text/markdown"; const data = await makeRequest( + vaultId, // Pass vaultId LocalRestAPI.ApiNoteJson.or("string"), "/active/", + vaultConfigProvider, // Pass vaultConfigProvider { headers: { Accept: format }, }, @@ -53,14 +70,22 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { type({ name: '"update_active_file"', arguments: { + vaultId: "string>0", // Added vaultId content: "string", }, - }).describe("Update the content of the active file open in Obsidian."), + }).describe("Update the content of the active file open in the specified Obsidian vault."), async ({ arguments: args }) => { - await makeRequest(LocalRestAPI.ApiNoContentResponse, "/active/", { - method: "PUT", - body: args.content, - }); + const { vaultId, content } = args; // Extract vaultId + await makeRequest( + vaultId, // Pass vaultId + LocalRestAPI.ApiNoContentResponse, + "/active/", + vaultConfigProvider, // Pass vaultConfigProvider + { + method: "PUT", + body: content, + }, + ); return { content: [{ type: "text", text: "File updated successfully" }], }; @@ -72,14 +97,22 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { type({ name: '"append_to_active_file"', arguments: { + vaultId: "string>0", // Added vaultId content: "string", }, - }).describe("Append content to the end of the currently-open note."), + }).describe("Append content to the end of the currently-open note in the specified vault."), async ({ arguments: args }) => { - await makeRequest(LocalRestAPI.ApiNoContentResponse, "/active/", { - method: "POST", - body: args.content, - }); + const { vaultId, content } = args; // Extract vaultId + await makeRequest( + vaultId, // Pass vaultId + LocalRestAPI.ApiNoContentResponse, + "/active/", + vaultConfigProvider, // Pass vaultConfigProvider + { + method: "POST", + body: content, + }, + ); return { content: [{ type: "text", text: "Content appended successfully" }], }; @@ -90,41 +123,47 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { tools.register( type({ name: '"patch_active_file"', - arguments: LocalRestAPI.ApiPatchParameters, + // Add vaultId to the existing ApiPatchParameters + arguments: type({ vaultId: "string>0" }).and(LocalRestAPI.ApiPatchParameters), }).describe( - "Insert or modify content in the currently-open note relative to a heading, block reference, or frontmatter field.", + "Insert or modify content in the currently-open note in the specified vault, relative to a heading, block reference, or frontmatter field.", ), async ({ arguments: args }) => { + const { vaultId, ...patchArgs } = args; // Extract vaultId, rest are patchArgs const headers: Record = { - Operation: args.operation, - "Target-Type": args.targetType, - Target: args.target, + Operation: patchArgs.operation, + "Target-Type": patchArgs.targetType, + Target: patchArgs.target, "Create-Target-If-Missing": "true", }; - if (args.targetDelimiter) { - headers["Target-Delimiter"] = args.targetDelimiter; + if (patchArgs.targetDelimiter) { + headers["Target-Delimiter"] = patchArgs.targetDelimiter; } - if (args.trimTargetWhitespace !== undefined) { - headers["Trim-Target-Whitespace"] = String(args.trimTargetWhitespace); + if (patchArgs.trimTargetWhitespace !== undefined) { + headers["Trim-Target-Whitespace"] = String(patchArgs.trimTargetWhitespace); } - if (args.contentType) { - headers["Content-Type"] = args.contentType; + if (patchArgs.contentType) { + headers["Content-Type"] = patchArgs.contentType; } const response = await makeRequest( + vaultId, // Pass vaultId LocalRestAPI.ApiContentResponse, "/active/", + vaultConfigProvider, // Pass vaultConfigProvider { method: "PATCH", headers, - body: args.content, + body: patchArgs.content, }, ); + // Ensure 'response' is a string before returning. If it's an object, stringify it. + const responseText = typeof response === 'string' ? response : JSON.stringify(response); return { content: [ { type: "text", text: "File patched successfully" }, - { type: "text", text: response }, + { type: "text", text: responseText }, ], }; }, @@ -134,12 +173,19 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { tools.register( type({ name: '"delete_active_file"', - arguments: "Record", - }).describe("Delete the currently-active file in Obsidian."), - async () => { - await makeRequest(LocalRestAPI.ApiNoContentResponse, "/active/", { - method: "DELETE", - }); + arguments: { vaultId: "string>0" }, // Added vaultId + }).describe("Delete the currently-active file in the specified Obsidian vault."), + async ({ arguments: args }) => { + const { vaultId } = args; // Extract vaultId + await makeRequest( + vaultId, // Pass vaultId + LocalRestAPI.ApiNoContentResponse, + "/active/", + vaultConfigProvider, // Pass vaultConfigProvider + { + method: "DELETE", + }, + ); return { content: [{ type: "text", text: "File deleted successfully" }], }; @@ -151,18 +197,22 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { type({ name: '"show_file_in_obsidian"', arguments: { + vaultId: "string>0", // Added vaultId filename: "string", "newLeaf?": "boolean", }, }).describe( - "Open a document in the Obsidian UI. Creates a new document if it doesn't exist. Returns a confirmation if the file was opened successfully.", + "Open a document in the Obsidian UI for the specified vault. Creates a new document if it doesn't exist. Returns a confirmation if the file was opened successfully.", ), async ({ arguments: args }) => { + const { vaultId, filename } = args; // Extract vaultId and filename const query = args.newLeaf ? "?newLeaf=true" : ""; await makeRequest( + vaultId, // Pass vaultId LocalRestAPI.ApiNoContentResponse, - `/open/${encodeURIComponent(args.filename)}${query}`, + `/open/${encodeURIComponent(filename)}${query}`, + vaultConfigProvider, // Pass vaultConfigProvider { method: "POST", }, @@ -179,25 +229,29 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { type({ name: '"search_vault"', arguments: { + vaultId: "string>0", // Added vaultId queryType: '"dataview" | "jsonlogic"', query: "string", }, }).describe( - "Search for documents matching a specified query using either Dataview DQL or JsonLogic.", + "Search for documents in the specified vault matching a query using either Dataview DQL or JsonLogic.", ), async ({ arguments: args }) => { + const { vaultId, queryType, query } = args; // Extract vaultId const contentType = - args.queryType === "dataview" + queryType === "dataview" ? "application/vnd.olrapi.dataview.dql+txt" : "application/vnd.olrapi.jsonlogic+json"; const data = await makeRequest( + vaultId, // Pass vaultId LocalRestAPI.ApiSearchResponse, "/search/", + vaultConfigProvider, // Pass vaultConfigProvider { method: "POST", headers: { "Content-Type": contentType }, - body: args.query, + body: query, }, ); @@ -212,23 +266,27 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { type({ name: '"search_vault_simple"', arguments: { + vaultId: "string>0", // Added vaultId query: "string", "contextLength?": "number", }, - }).describe("Search for documents matching a text query."), + }).describe("Search for documents in the specified vault matching a text query."), async ({ arguments: args }) => { - const query = new URLSearchParams({ - query: args.query, - ...(args.contextLength + const { vaultId, ...searchArgs } = args; // Extract vaultId + const queryParams = new URLSearchParams({ + query: searchArgs.query, + ...(searchArgs.contextLength ? { - contextLength: String(args.contextLength), + contextLength: String(searchArgs.contextLength), } : {}), }); const data = await makeRequest( + vaultId, // Pass vaultId LocalRestAPI.ApiSimpleSearchResponse, - `/search/simple/?${query}`, + `/search/simple/?${queryParams}`, + vaultConfigProvider, // Pass vaultConfigProvider { method: "POST", }, @@ -245,18 +303,22 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { type({ name: '"list_vault_files"', arguments: { + vaultId: "string>0", // Added vaultId "directory?": "string", }, }).describe( - "List files in the root directory or a specified subdirectory of your vault.", + "List files in the root directory or a specified subdirectory of the specified vault.", ), async ({ arguments: args }) => { - const path = args.directory ? `${args.directory}/` : ""; + const { vaultId, directory } = args; // Extract vaultId + const path = directory ? `${directory}/` : ""; const data = await makeRequest( + vaultId, // Pass vaultId LocalRestAPI.ApiVaultFileResponse.or( LocalRestAPI.ApiVaultDirectoryResponse, ), `/vault/${path}`, + vaultConfigProvider, // Pass vaultConfigProvider ); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], @@ -269,20 +331,24 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { type({ name: '"get_vault_file"', arguments: { + vaultId: "string>0", // Added vaultId filename: "string", "format?": '"markdown" | "json"', }, - }).describe("Get the content of a file from your vault."), + }).describe("Get the content of a file from the specified vault."), async ({ arguments: args }) => { - const isJson = args.format === "json"; - const format = isJson + const { vaultId, filename, format: argFormat } = args; // Extract vaultId + const isJson = argFormat === "json"; + const acceptHeader = isJson ? "application/vnd.olrapi.note+json" : "text/markdown"; const data = await makeRequest( + vaultId, // Pass vaultId isJson ? LocalRestAPI.ApiNoteJson : LocalRestAPI.ApiContentResponse, - `/vault/${encodeURIComponent(args.filename)}`, + `/vault/${encodeURIComponent(filename)}`, + vaultConfigProvider, // Pass vaultConfigProvider { - headers: { Accept: format }, + headers: { Accept: acceptHeader }, }, ); return { @@ -302,17 +368,21 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { type({ name: '"create_vault_file"', arguments: { + vaultId: "string>0", // Added vaultId filename: "string", content: "string", }, - }).describe("Create a new file in your vault or update an existing one."), + }).describe("Create a new file in the specified vault or update an existing one."), async ({ arguments: args }) => { + const { vaultId, filename, content } = args; // Extract vaultId await makeRequest( + vaultId, // Pass vaultId LocalRestAPI.ApiNoContentResponse, - `/vault/${encodeURIComponent(args.filename)}`, + `/vault/${encodeURIComponent(filename)}`, + vaultConfigProvider, // Pass vaultConfigProvider { method: "PUT", - body: args.content, + body: content, }, ); return { @@ -326,17 +396,21 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { type({ name: '"append_to_vault_file"', arguments: { + vaultId: "string>0", // Added vaultId filename: "string", content: "string", }, - }).describe("Append content to a new or existing file."), + }).describe("Append content to a new or existing file in the specified vault."), // Updated description async ({ arguments: args }) => { + const { vaultId, filename, content } = args; // Extract vaultId await makeRequest( + vaultId, // Pass vaultId LocalRestAPI.ApiNoContentResponse, - `/vault/${encodeURIComponent(args.filename)}`, + `/vault/${encodeURIComponent(filename)}`, + vaultConfigProvider, // Pass vaultConfigProvider { method: "POST", - body: args.content, + body: content, }, ); return { @@ -349,44 +423,47 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { tools.register( type({ name: '"patch_vault_file"', - arguments: type({ - filename: "string", - }).and(LocalRestAPI.ApiPatchParameters), + // Add vaultId to the existing arguments + arguments: type({ vaultId: "string>0", filename: "string" }).and(LocalRestAPI.ApiPatchParameters), }).describe( - "Insert or modify content in a file relative to a heading, block reference, or frontmatter field.", + "Insert or modify content in a file in the specified vault, relative to a heading, block reference, or frontmatter field.", // Updated description ), async ({ arguments: args }) => { - const headers: HeadersInit = { - Operation: args.operation, - "Target-Type": args.targetType, - Target: args.target, + const { vaultId, filename, ...patchArgs } = args; // Extract vaultId and filename, rest are patchArgs + const headers: Record = { // Changed HeadersInit to Record for consistency + Operation: patchArgs.operation, + "Target-Type": patchArgs.targetType, + Target: patchArgs.target, "Create-Target-If-Missing": "true", }; - if (args.targetDelimiter) { - headers["Target-Delimiter"] = args.targetDelimiter; + if (patchArgs.targetDelimiter) { + headers["Target-Delimiter"] = patchArgs.targetDelimiter; } - if (args.trimTargetWhitespace !== undefined) { - headers["Trim-Target-Whitespace"] = String(args.trimTargetWhitespace); + if (patchArgs.trimTargetWhitespace !== undefined) { + headers["Trim-Target-Whitespace"] = String(patchArgs.trimTargetWhitespace); } - if (args.contentType) { - headers["Content-Type"] = args.contentType; + if (patchArgs.contentType) { + headers["Content-Type"] = patchArgs.contentType; } const response = await makeRequest( + vaultId, // Pass vaultId LocalRestAPI.ApiContentResponse, - `/vault/${encodeURIComponent(args.filename)}`, + `/vault/${encodeURIComponent(filename)}`, // Use filename + vaultConfigProvider, // Pass vaultConfigProvider { method: "PATCH", headers, - body: args.content, + body: patchArgs.content, }, ); - + // Ensure 'response' is a string before returning. If it's an object, stringify it. + const responseText = typeof response === 'string' ? response : JSON.stringify(response); return { content: [ { type: "text", text: "File patched successfully" }, - { type: "text", text: response }, + { type: "text", text: responseText }, ], }; }, @@ -397,13 +474,17 @@ export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { type({ name: '"delete_vault_file"', arguments: { + vaultId: "string>0", // Added vaultId filename: "string", }, - }).describe("Delete a file from your vault."), + }).describe("Delete a file from the specified vault."), // Updated description async ({ arguments: args }) => { + const { vaultId, filename } = args; // Extract vaultId await makeRequest( + vaultId, // Pass vaultId LocalRestAPI.ApiNoContentResponse, - `/vault/${encodeURIComponent(args.filename)}`, + `/vault/${encodeURIComponent(filename)}`, + vaultConfigProvider, // Pass vaultConfigProvider { method: "DELETE", }, diff --git a/packages/mcp-server/src/features/prompts/index.ts b/packages/mcp-server/src/features/prompts/index.ts index 652d3af..697d607 100644 --- a/packages/mcp-server/src/features/prompts/index.ts +++ b/packages/mcp-server/src/features/prompts/index.ts @@ -18,48 +18,97 @@ import { PromptFrontmatterSchema, type PromptMetadata, } from "shared"; +import type { ObsidianMcpServer } from "../core"; // Import ObsidianMcpServer const PROMPT_DIRNAME = `Prompts`; -export function setupObsidianPrompts(server: Server) { - server.setRequestHandler(ListPromptsRequestSchema, async () => { +const GLOBAL_PROMPTS: Record = { + "list_configured_vaults": { + name: "list_configured_vaults", + description: "Displays a list of all Obsidian vaults currently configured and accessible by this server.", + arguments: [], // No arguments needed + }, + // Add other global prompts here if needed +}; + +export function setupObsidianPrompts(server: Server, obsServer: ObsidianMcpServer) { + const vaultConfigProvider = obsServer.getVaultConfig.bind(obsServer); + + server.setRequestHandler(ListPromptsRequestSchema, async ({ params }) => { try { - const { files } = await makeRequest( - LocalRestAPI.ApiVaultDirectoryResponse, - `/vault/${PROMPT_DIRNAME}/`, - ); - const prompts: PromptMetadata[] = ( - await Promise.all( - files.map(async (filename) => { - // Skip non-Markdown files - if (!filename.endsWith(".md")) return []; + const requestedVaultId = (params as any)?.vaultId || (params as any)?.arguments?.vaultId; + let promptsToShow: PromptMetadata[] = [...Object.values(GLOBAL_PROMPTS)]; // Start with global prompts + + const processVault = async (vaultId: string, prefixName = false): Promise => { + try { + const { files } = await makeRequest( + vaultId, + LocalRestAPI.ApiVaultDirectoryResponse, + `/vault/${PROMPT_DIRNAME}/`, + vaultConfigProvider + ); + const fileList = (files as string[]) || []; + const vaultPromptsPromises = fileList.map(async (filename) => { + if (!filename.endsWith(".md")) return null; - // Retrieve frontmatter and content from vault file const file = await makeRequest( - LocalRestAPI.ApiVaultFileResponse, + vaultId, + LocalRestAPI.ApiNoteJson, `/vault/${PROMPT_DIRNAME}/${filename}`, - { - headers: { Accept: LocalRestAPI.MIME_TYPE_OLRAPI_NOTE_JSON }, - }, - ); - - // Skip files without the prompt template tag - if (!file.tags.includes("mcp-tools-prompt")) { - return []; - } + vaultConfigProvider, + { headers: { Accept: LocalRestAPI.MIME_TYPE_OLRAPI_NOTE_JSON } }, + ) as LocalRestAPI.ApiNoteJsonType; + if (!file.tags || !file.tags.includes("mcp-tools-prompt")) return null; + + const promptName = prefixName ? `${vaultId}/${filename}` : filename; return { - name: filename, - description: file.frontmatter.description, - arguments: parseTemplateParameters(file.content), + name: promptName, + description: file.frontmatter?.description || "", + arguments: parseTemplateParameters(file.content || ""), }; - }), - ) - ).flat(); - return { prompts }; + }); + const resolvedPrompts = await Promise.all(vaultPromptsPromises); + return resolvedPrompts.filter(p => p !== null) as PromptMetadata[]; + } catch (vaultError: any) { + const errorMessage = String(vaultError?.message || vaultError || "").toLowerCase(); + // Check for common indicators of a "Not Found" error from Local REST API or fetch. + // The Local REST API might return a specific message or status that makeRequest translates. + const isNotFoundError = + errorMessage.includes("404") || + errorMessage.includes("not found") || + errorMessage.includes("no such file or directory"); + + if (isNotFoundError) { + logger.debug(`'Prompts' directory not found in vault ${vaultId} or error listing its contents (likely 404). No file-based prompts will be loaded from this vault. Error details: ${String(vaultError)}`); + } else { + logger.warn(`Failed to list prompts for vault ${vaultId}. This could be due to Local REST API issues, incorrect permissions, or other errors not related to a missing directory.`, { error: vaultError }); + } + return []; // Return empty for this vault if any error occurs while listing directory contents + } + }; + + if (requestedVaultId) { + // If a specific vaultId is requested, get its prompts (names not prefixed) + const vaultSpecificPrompts = await processVault(requestedVaultId, false); + promptsToShow.push(...vaultSpecificPrompts); + } else { + // If no vaultId is requested, process all configured vaults (names prefixed) + const allVaultConfigs = obsServer.getVaultsConfig(); + if (allVaultConfigs && allVaultConfigs.length > 0) { + for (const vaultConfig of allVaultConfigs) { + const vaultSpecificPrompts = await processVault(vaultConfig.vaultId, true); + promptsToShow.push(...vaultSpecificPrompts); + } + } else { + logger.info("ListPrompts called without vaultId and no vaults are configured on the server. Returning global prompts only."); + } + } + + return { prompts: promptsToShow }; } catch (err) { const error = formatMcpError(err); - logger.error("Error in ListPromptsRequestSchema handler", { + logger.error("Error in ListPromptsRequestSchema handler (outer)", { error, message: error.message, }); @@ -69,19 +118,72 @@ export function setupObsidianPrompts(server: Server) { server.setRequestHandler(GetPromptRequestSchema, async ({ params }) => { try { - const promptFilePath = `${PROMPT_DIRNAME}/${params.name}`; + const requestedPromptName = params.name; + + // Handle Global Prompts + if (GLOBAL_PROMPTS[requestedPromptName]) { + const globalPrompt = GLOBAL_PROMPTS[requestedPromptName]; + if (requestedPromptName === "list_configured_vaults") { + const allVaultConfigs = obsServer.getVaultsConfig(); + const vaultListText = allVaultConfigs.map((vc, index) => + `${index + 1}. ID: ${vc.vaultId}\n Name: ${vc.name}\n Path: ${vc.path || 'N/A'}` + ).join("\n\n"); + + return { + description: globalPrompt.description, + messages: [{ + role: "assistant", + content: { type: "text", text: `Configured vaults:\n\n${vaultListText || "No vaults configured."}` } + }] + }; + } + // Handle other global prompts here if they have specific logic + // For now, just return its definition if it's not list_configured_vaults + return { + description: globalPrompt.description, + messages: [{ role: "system", content: { type: "text", text: `Global prompt '${globalPrompt.name}' selected. Implementation pending.`} }] + }; + } + + // Handle Vault-Specific Prompts (potentially prefixed) + let vaultIdToUse: string | undefined = (params.arguments as any)?.vaultId; + let actualPromptName = requestedPromptName; + + if (!vaultIdToUse) { + const parts = requestedPromptName.split('/'); + if (parts.length > 1) { + // Check if the first part is a valid vaultId + try { + obsServer.getVaultConfig(parts[0]); // This will throw if parts[0] is not a valid vaultId + vaultIdToUse = parts[0]; + actualPromptName = parts.slice(1).join('/'); + logger.debug(`Extracted vaultId '${vaultIdToUse}' and promptName '${actualPromptName}' from prefixed name.`); + } catch { + // parts[0] is not a valid vaultId, so assume requestedPromptName is not prefixed + // and vaultId is genuinely missing. + } + } + } + + if (!vaultIdToUse) { + // This implies Claude Desktop (or other client) did not send vaultId in arguments, + // and the prompt name was not a prefixed one from which we could infer a vaultId. + // This also means no default vaultId was injected by Claude's config into params.arguments. + throw new McpError(ErrorCode.InvalidRequest, `vaultId is required for vault-specific prompt '${actualPromptName}' and was not provided or inferable.`); + } + + const promptFilePath = `${PROMPT_DIRNAME}/${actualPromptName}`; - // Get prompt content - const { content: template, frontmatter } = await makeRequest( - LocalRestAPI.ApiVaultFileResponse, + const fileData = await makeRequest( + vaultIdToUse, + LocalRestAPI.ApiNoteJson, `/vault/${promptFilePath}`, - { - headers: { Accept: LocalRestAPI.MIME_TYPE_OLRAPI_NOTE_JSON }, - }, - ); + vaultConfigProvider, + { headers: { Accept: LocalRestAPI.MIME_TYPE_OLRAPI_NOTE_JSON } }, + ) as LocalRestAPI.ApiNoteJsonType; - const { description } = PromptFrontmatterSchema.assert(frontmatter); - const templateParams = parseTemplateParameters(template); + const { description } = PromptFrontmatterSchema.assert(fileData.frontmatter || {}); + const templateParams = parseTemplateParameters(fileData.content || ""); const templateParamsSchema = buildTemplateArgumentsSchema(templateParams); const templateArgs = templateParamsSchema(params.arguments); if (templateArgs instanceof type.errors) { @@ -98,9 +200,14 @@ export function setupObsidianPrompts(server: Server) { }; // Process template through Templater plugin + // This makeRequest call is problematic as /templates/execute is an internal tool, not a Local REST API endpoint. + // For now, making it syntactically correct for makeRequest, but this needs a proper fix (e.g. server.callTool). + // const templateExecuteArgsWithVaultId = { ...templateExecutionArgs, vaultId: vaultIdToUse }; // Not needed as vaultIdToUse is passed directly to makeRequest const { content } = await makeRequest( - LocalRestAPI.ApiTemplateExecutionResponse, - "/templates/execute", + vaultIdToUse, // Corrected: Use vaultIdToUse + LocalRestAPI.ApiTemplateExecutionResponse, // Schema for the expected response + "/templates/execute", // This is an internal MCP path + vaultConfigProvider, // Passing for makeRequest signature, actual use is questionable here { method: "POST", headers: { "Content-Type": "application/json" }, diff --git a/packages/mcp-server/src/features/smart-connections/index.ts b/packages/mcp-server/src/features/smart-connections/index.ts index b017d16..f3ce0e3 100644 --- a/packages/mcp-server/src/features/smart-connections/index.ts +++ b/packages/mcp-server/src/features/smart-connections/index.ts @@ -1,12 +1,16 @@ import { makeRequest, type ToolRegistry } from "$/shared"; import { type } from "arktype"; import { LocalRestAPI } from "shared"; +import type { ObsidianMcpServer } from "../core"; // Import ObsidianMcpServer + +export function registerSmartConnectionsTools(tools: ToolRegistry, obsServer: ObsidianMcpServer) { // Modified signature + const vaultConfigProvider = obsServer.getVaultConfig.bind(obsServer); // Get vaultConfigProvider -export function registerSmartConnectionsTools(tools: ToolRegistry) { tools.register( type({ name: '"search_vault_smart"', arguments: { + vaultId: "string>0", // ADDED vaultId query: type("string>0").describe("A search phrase for semantic search"), "filter?": { "folders?": type("string[]").describe( @@ -20,19 +24,27 @@ export function registerSmartConnectionsTools(tools: ToolRegistry) { ), }, }, - }).describe("Search for documents semantically matching a text string."), + }).describe("Search for documents in a specific vault semantically matching a text string."), // Updated description async ({ arguments: args }) => { + const { vaultId, ...searchBodyArgs } = args; // Extract vaultId, rest is body + const data = await makeRequest( + vaultId, // Pass vaultId LocalRestAPI.ApiSmartSearchResponse, `/search/smart`, + vaultConfigProvider, // Pass vaultConfigProvider { method: "POST", - body: JSON.stringify(args), + body: JSON.stringify(searchBodyArgs), // Pass remaining args as body + headers: { "Content-Type": "application/json" } // Ensure Content-Type }, ); return { - content: [{ type: "text", text: JSON.stringify(data, null, 2) }], + content: [ + { type: "text", text: JSON.stringify(data, null, 2) }, + { type: "text", text: `Vault used: ${vaultId}` } + ], }; }, ); diff --git a/packages/mcp-server/src/features/templates/index.ts b/packages/mcp-server/src/features/templates/index.ts index 60b7722..d9b4ec1 100644 --- a/packages/mcp-server/src/features/templates/index.ts +++ b/packages/mcp-server/src/features/templates/index.ts @@ -6,62 +6,72 @@ import { } from "$/shared"; import { type } from "arktype"; import { buildTemplateArgumentsSchema, LocalRestAPI } from "shared"; +import type { ObsidianMcpServer } from "../core"; // Import ObsidianMcpServer + +export function registerTemplaterTools(tools: ToolRegistry, obsServer: ObsidianMcpServer) { // Modified signature + const vaultConfigProvider = obsServer.getVaultConfig.bind(obsServer); // Get vaultConfigProvider -export function registerTemplaterTools(tools: ToolRegistry) { tools.register( type({ name: '"execute_template"', arguments: LocalRestAPI.ApiTemplateExecutionParams.omit("createFile").and( { + vaultId: "string>0", // ADDED vaultId // should be boolean but the MCP client returns a string "createFile?": type("'true'|'false'"), }, ), - }).describe("Execute a Templater template with the given arguments"), + }).describe("Execute a Templater template in a specific vault with the given arguments"), // Updated description async ({ arguments: args }) => { - // Get prompt content + const { vaultId, ...templateArgsForRest } = args; // Extract vaultId + + // Get template content from the specified vault const data = await makeRequest( - LocalRestAPI.ApiVaultFileResponse, - `/vault/${args.name}`, + vaultId, // Pass vaultId + LocalRestAPI.ApiNoteJson, // Expect ApiNoteJson for .content + `/vault/${templateArgsForRest.name}`, // Use name from remaining args + vaultConfigProvider, // Pass vaultConfigProvider { headers: { Accept: LocalRestAPI.MIME_TYPE_OLRAPI_NOTE_JSON }, }, - ); + ) as LocalRestAPI.ApiNoteJsonType; // Cast for type safety // Validate prompt arguments - const templateParameters = parseTemplateParameters(data.content); + const templateParameters = parseTemplateParameters(data.content || ""); // Add null check for content const validArgs = buildTemplateArgumentsSchema(templateParameters)( - args.arguments, + templateArgsForRest.arguments, // Use arguments from remaining args ); if (validArgs instanceof type.errors) { throw formatMcpError(validArgs); } - const templateExecutionArgs: { - name: string; - arguments: Record; - createFile: boolean; - targetPath?: string; - } = { - name: args.name, + // Prepare arguments for the target Local REST API's /templates/execute endpoint + // This body should NOT contain vaultId. + const finalTemplateExecutionArgs: LocalRestAPI.ApiTemplateExecutionParamsType = { + name: templateArgsForRest.name, arguments: validArgs, - createFile: args.createFile === "true", - targetPath: args.targetPath, + createFile: templateArgsForRest.createFile === "true", + targetPath: templateArgsForRest.targetPath, }; - - // Process template through Templater plugin + + // Process template through Templater plugin via the specified vault's Local REST API const response = await makeRequest( + vaultId, // This vaultId directs makeRequest to the correct vault's Local REST API LocalRestAPI.ApiTemplateExecutionResponse, - "/templates/execute", + "/templates/execute", // Path on the target vault's Local REST API + vaultConfigProvider, // Pass vaultConfigProvider { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify(templateExecutionArgs), + body: JSON.stringify(finalTemplateExecutionArgs), }, ); return { - content: [{ type: "text", text: JSON.stringify(response, null, 2) }], + content: [ + { type: "text", text: JSON.stringify(response, null, 2) }, + { type: "text", text: `Vault used: ${vaultId}` } + ], }; }, ); diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts index 8f709f3..cca61a0 100644 --- a/packages/mcp-server/src/index.ts +++ b/packages/mcp-server/src/index.ts @@ -3,13 +3,41 @@ import { logger } from "$/shared"; import { ObsidianMcpServer } from "./features/core"; import { getVersion } from "./features/version" with { type: "macro" }; +// Global Uncaught Exception and Unhandled Rejection Handlers +process.on('uncaughtException', async (error) => { + logger.fatal('Uncaught exception detected, attempting graceful shutdown.', { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); + await logger.flush(); + process.exit(1); // Exit with a failure code +}); + +process.on('unhandledRejection', async (reason, promise) => { + logger.fatal('Unhandled promise rejection detected, attempting graceful shutdown.', { + reason: reason instanceof Error ? reason.message : String(reason), + stack: reason instanceof Error ? reason.stack : undefined, + promise, + }); + await logger.flush(); + process.exit(1); // Exit with a failure code +}); + +// Signal handlers for graceful shutdown +const shutdown = async (signal: string) => { + logger.info(`Received ${signal}, initiating graceful shutdown.`); + // Perform any necessary cleanup here before exiting + await logger.flush(); // Ensure all logs are written + process.exit(0); // Exit gracefully +}; + +process.on('SIGTERM', shutdown); +process.on('SIGINT', shutdown); + async function main() { try { - // Verify required environment variables - const API_KEY = process.env.OBSIDIAN_API_KEY; - if (!API_KEY) { - throw new Error("OBSIDIAN_API_KEY environment variable is required"); - } + // Environment variable check for OBSIDIAN_API_KEY removed. + // Configuration is now handled by ObsidianMcpServer loading vaults.json. logger.debug("Starting MCP Tools for Obsidian server..."); const server = new ObsidianMcpServer(); diff --git a/packages/mcp-server/src/shared/ToolRegistry.ts b/packages/mcp-server/src/shared/ToolRegistry.ts index 14dd574..11e72ba 100644 --- a/packages/mcp-server/src/shared/ToolRegistry.ts +++ b/packages/mcp-server/src/shared/ToolRegistry.ts @@ -78,12 +78,53 @@ export class ToolRegistryClass< list = () => { return { tools: Array.from(this.enabled.values()).map((schema) => { - return { - // @ts-expect-error We know the const property is present for a string - name: schema.get("name").toJsonSchema().const, - description: schema.description, - inputSchema: schema.get("arguments").toJsonSchema(), - }; + try { + // Get the name schema + const nameSchema = schema.get("name"); + + // Get the name as a string for special case handling + let nameValue = "unknown"; + try { + // @ts-expect-error We know the const property is present for a string + nameValue = nameSchema.toJsonSchema().const; + } catch (error) { + logger.warn(`Failed to get name for tool schema`, { error }); + } + + // Special case for list_configured_vaults tool + if (nameValue === "list_configured_vaults") { + return { + name: "list_configured_vaults", + description: schema.description || "Lists all configured Obsidian vaults with their ID and name.", + inputSchema: { type: "object", properties: {} }, + }; + } + + // For all other tools, try to get the arguments schema + let inputSchema: any = { type: "object", properties: {} }; + try { + const argsSchema = schema.get("arguments"); + if (argsSchema) { + inputSchema = argsSchema.toJsonSchema(); + } + } catch (error) { + logger.warn(`Failed to convert arguments schema to JSON Schema for tool: ${nameValue}`, { error }); + } + + return { + name: nameValue, + description: schema.description || "", + inputSchema, + }; + } catch (error) { + logger.error(`Failed to process schema for tool list`, { error }); + // Return a minimal valid tool definition to avoid breaking the list + return { + name: "unknown", + description: "Error processing tool schema", + inputSchema: { type: "object", properties: {} }, + }; + } }), }; }; @@ -101,18 +142,30 @@ export class ToolRegistryClass< params: Schema["infer"], ): Schema["infer"] => { const args = params.arguments; - const argsSchema = schema.get("arguments").exclude("undefined"); - if (!args || !argsSchema) return params; + // Get the arguments schema, handling the case where it might be undefined + const argsSchemaRaw = schema.get("arguments"); + if (!args || !argsSchemaRaw) return params; + + const argsSchema = argsSchemaRaw.exclude("undefined"); + if (!argsSchema) return params; const fixed = { ...params.arguments }; for (const [key, value] of Object.entries(args)) { - const valueSchema = argsSchema.get(key).exclude("undefined"); - if ( - valueSchema.expression === "boolean" && - typeof value === "string" && - ["true", "false"].includes(value) - ) { - fixed[key] = value === "true"; + try { + const keySchema = argsSchema.get(key); + if (!keySchema) continue; + + const valueSchema = keySchema.exclude("undefined"); + if ( + valueSchema.expression === "boolean" && + typeof value === "string" && + ["true", "false"].includes(value) + ) { + fixed[key] = value === "true"; + } + } catch (error) { + // Skip this key if there's an error accessing its schema + continue; } } diff --git a/packages/mcp-server/src/shared/configManager.ts b/packages/mcp-server/src/shared/configManager.ts new file mode 100644 index 0000000..2b1475e --- /dev/null +++ b/packages/mcp-server/src/shared/configManager.ts @@ -0,0 +1,125 @@ +import { type } from "arktype"; +import { homedir } from "node:os"; +import { join } from "node:path"; +import { readFile } from "node:fs/promises"; +import { logger } from "./logger"; +import { + type VaultsConfig, + VaultsConfigSchema, + type ConfigFile, + ConfigFileSchema +} from "../types/config"; +import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; + +const CONFIG_DIR_PATH = join(homedir(), ".config", "mcp-tools"); +const CONFIG_FILE_PATH = join(CONFIG_DIR_PATH, "vaults.json"); + +/** + * Represents the configuration loaded from the config file + */ +export interface LoadedConfig { + vaults: VaultsConfig; + defaultVaultId?: string; +} + +/** + * Loads and validates the configuration from ~/.config/mcp-tools/vaults.json. + * Supports both the new format (with vaults array and defaultVaultId) and + * the legacy format (just an array of vault entries). + * + * @returns A promise that resolves with the validated configuration. + * Returns an empty config if the config file is not found (ENOENT). + * @throws McpError if the file is invalid (parsing, validation, duplicates) or another read error occurs. + */ +export async function loadVaultsConfig(): Promise { + try { + logger.debug(`Attempting to load vaults config from: ${CONFIG_FILE_PATH}`); + const fileContent = await readFile(CONFIG_FILE_PATH, "utf-8"); + const jsonData = JSON.parse(fileContent); + + // Try to validate as new config format first + const newFormatResult = ConfigFileSchema(jsonData); + + if (!(newFormatResult instanceof type.errors)) { + // New format validation succeeded + const configData = newFormatResult; + + // Check for duplicate vaultIds + const vaultIds = new Set(); + for (const vault of configData.vaults) { + if (vaultIds.has(vault.vaultId)) { + throw new McpError( + ErrorCode.InternalError, + `Duplicate vaultId "${vault.vaultId}" found in configuration file at ${CONFIG_FILE_PATH}. vaultId must be unique.`, + ); + } + vaultIds.add(vault.vaultId); + } + + // Validate defaultVaultId if present + if (configData.defaultVaultId && !vaultIds.has(configData.defaultVaultId)) { + logger.warn(`Default vault ID "${configData.defaultVaultId}" does not match any configured vault. It will be ignored.`); + // We don't throw an error, just ignore the invalid defaultVaultId + configData.defaultVaultId = undefined; + } + + logger.info(`Successfully loaded and validated vaults configuration from: ${CONFIG_FILE_PATH} (new format)`); + return { + vaults: configData.vaults, + defaultVaultId: configData.defaultVaultId + }; + } + + // Try legacy format (array of vault entries) + const legacyFormatResult = VaultsConfigSchema(jsonData); + + if (legacyFormatResult instanceof type.errors) { + // Both formats failed validation + logger.error("Vaults configuration validation failed for both formats", { + path: CONFIG_FILE_PATH, + newFormatError: newFormatResult.summary, + legacyFormatError: legacyFormatResult.summary, + }); + throw new McpError( + ErrorCode.InternalError, + `Invalid vaults configuration file at ${CONFIG_FILE_PATH}: ${legacyFormatResult.summary}`, + ); + } + + // Legacy format validation succeeded + const vaultEntries = legacyFormatResult; + + // Check for duplicate vaultIds + const vaultIds = new Set(); + for (const vault of vaultEntries) { + if (vaultIds.has(vault.vaultId)) { + throw new McpError( + ErrorCode.InternalError, + `Duplicate vaultId "${vault.vaultId}" found in configuration file at ${CONFIG_FILE_PATH}. vaultId must be unique.`, + ); + } + vaultIds.add(vault.vaultId); + } + + logger.info(`Successfully loaded and validated vaults configuration from: ${CONFIG_FILE_PATH} (legacy format)`); + return { + vaults: vaultEntries, + defaultVaultId: undefined + }; + + } catch (error: any) { + if (error.code === 'ENOENT') { + logger.warn(`Vaults configuration file not found at ${CONFIG_FILE_PATH}. Server will start with no vaults configured.`); + return { vaults: [] }; // Return empty config + } else if (error instanceof McpError) { + // Re-throw McpErrors (like validation or duplicate ID errors) + throw error; + } else if (error instanceof SyntaxError) { + logger.error(`Failed to parse vaults configuration file at ${CONFIG_FILE_PATH}`, { error: error.message }); + throw new McpError(ErrorCode.InternalError, `Failed to parse vaults configuration file at ${CONFIG_FILE_PATH}: Invalid JSON.`); + } else { + logger.error(`Failed to load vaults configuration from ${CONFIG_FILE_PATH}`, { error: error.message }); + throw new McpError(ErrorCode.InternalError, `Failed to load vaults configuration from ${CONFIG_FILE_PATH}: ${error.message}`); + } + } +} diff --git a/packages/mcp-server/src/shared/logger.ts b/packages/mcp-server/src/shared/logger.ts index 24e483b..f4a7cea 100644 --- a/packages/mcp-server/src/shared/logger.ts +++ b/packages/mcp-server/src/shared/logger.ts @@ -3,10 +3,10 @@ import { createLogger } from "shared"; /** * The logger instance for the MCP server application. * This logger is configured with the "obsidian-mcp-tools" app name, writes to the "mcp-server.log" file, - * and uses the "INFO" log level in production environments and "DEBUG" in development environments. + * and uses the "INFO" log level for all environments to reduce verbosity now that testing is complete. */ export const logger = createLogger({ appName: "Claude", filename: "mcp-server-obsidian-mcp-tools.log", - level: process.env.NODE_ENV === "production" ? "INFO" : "DEBUG", + level: "DEBUG", // Increased logging level for debugging connection issues }); diff --git a/packages/mcp-server/src/shared/makeRequest.ts b/packages/mcp-server/src/shared/makeRequest.ts index dbe24de..70c8f55 100644 --- a/packages/mcp-server/src/shared/makeRequest.ts +++ b/packages/mcp-server/src/shared/makeRequest.ts @@ -1,46 +1,75 @@ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; import { type, type Type } from "arktype"; import { logger } from "./logger"; +// We need getVaultConfig, but it's not exported from configManager.ts yet. +// For now, assume ObsidianMcpServer instance will provide it or be passed around. +// This will be resolved when ObsidianMcpServer is updated. +// import { getVaultConfig } from "./configManager"; // This will be used later -// 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"; -export const BASE_URL = `${PROTOCOL}://127.0.0.1:${PORT}`; +// Remove global BASE_URL and API_KEY logic, as it's now per-vault. // Disable TLS certificate validation for local self-signed certificates +// This should ideally be configurable per vault if needed, or handled carefully. process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; /** - * Makes a request to the Obsidian Local REST API with the provided path and optional request options. - * Automatically adds the required API key to the request headers. + * Makes a request to a specific Obsidian vault's Local REST API. + * Retrieves vault-specific connection details (API key, base URL) using the vaultId. + * If no vaultId is provided, it will use the default vault or first available vault. * Throws an `McpError` if the API response is not successful. * - * @param path - The path to the Obsidian API endpoint. + * @param vaultId - The ID of the target vault (must match an ID in vaults.json). If not provided, uses default or first vault. + * @param schema - The ArkType schema to validate the response. + * @param path - The path to the Obsidian API endpoint (e.g., "/active/"). + * @param vaultConfigProvider - Function that provides vault configuration details. * @param init - Optional request options to pass to the `fetch` function. - * @returns The response from the Obsidian API. + * @returns The validated response from the Obsidian API. */ - export async function makeRequest< T extends | Type<{}, {}> | Type | Type<{} | null | undefined, {}>, ->(schema: T, path: string, init?: RequestInit): Promise { - const API_KEY = process.env.OBSIDIAN_API_KEY; - if (!API_KEY) { - logger.error("OBSIDIAN_API_KEY environment variable is required", { - env: process.env, - }); - throw new Error("OBSIDIAN_API_KEY environment variable is required"); +>( + vaultId: string | undefined, // Optional vaultId parameter + schema: T, + path: string, + // vaultConfigProvider is required, so it comes before optional 'init' + vaultConfigProvider: (id?: string) => { apiKey: string; localRestApiBaseUrl: string }, + init?: RequestInit, +): Promise { + let vaultDetails; + let actualVaultId: string; + + try { + // Get the vault details + vaultDetails = vaultConfigProvider(vaultId); + + // If vaultId was not provided, we can't determine the actual ID from just the connection details + if (!vaultId) { + actualVaultId = "default"; // We don't know the actual ID if default was used + logger.debug("Using default vault"); + } else { + actualVaultId = vaultId; + } + } catch (error: any) { + // Catch errors from vaultConfigProvider (e.g., vaultId not found) + logger.error(`Failed to get configuration for vaultId "${vaultId || 'default'}"`, { error: error.message }); + if (error instanceof McpError) throw error; + throw new McpError(ErrorCode.InvalidRequest, `Configuration error for vaultId "${vaultId || 'default'}": ${error.message}`); } + const { apiKey: API_KEY, localRestApiBaseUrl: BASE_URL } = vaultDetails; + + // API_KEY and BASE_URL are guaranteed by VaultConfigEntrySchema to be non-empty strings + // and localRestApiBaseUrl is a valid URL string. + const url = `${BASE_URL}${path}`; const response = await fetch(url, { ...init, headers: { - Authorization: `Bearer ${API_KEY}`, - "Content-Type": "text/markdown", + Authorization: `Bearer ${API_KEY}`, // Use vault-specific API_KEY + "Content-Type": "text/markdown", // Default, can be overridden by init.headers ...init?.headers, }, }); @@ -70,5 +99,12 @@ export async function makeRequest< ); } + // Add the vault ID to the response if it's an object + if (typeof validated === 'object' && validated !== null) { + // We can't directly modify the validated object due to type constraints, + // but we can log the vault ID for debugging + logger.debug(`Request to ${path} used vault: ${actualVaultId}`); + } + return validated; } diff --git a/packages/mcp-server/src/types/config.ts b/packages/mcp-server/src/types/config.ts new file mode 100644 index 0000000..f420625 --- /dev/null +++ b/packages/mcp-server/src/types/config.ts @@ -0,0 +1,29 @@ +import { type } from "arktype"; + +// Schema for a single vault entry in the configuration file +export const VaultConfigEntrySchema = type({ + vaultId: "string>0", // User-defined unique ID, non-empty + name: "string>0", // User-friendly display name, non-empty + path: "string?", // Optional filesystem path (for reference) + localRestApiBaseUrl: "string.url", // Use ArkType's built-in URL validation + apiKey: "string>0", // API Key for the vault's Local REST API, non-empty +}); + +// Type inferred from the schema +export type VaultConfigEntry = typeof VaultConfigEntrySchema.infer; + +// Schema for the configuration file structure +export const ConfigFileSchema = type({ + vaults: type(VaultConfigEntrySchema).array(), + defaultVaultId: "string?", // Optional default vault ID +}); + +// Type inferred for the config file structure +export type ConfigFile = typeof ConfigFileSchema.infer; + +// For backward compatibility, we'll keep VaultsConfigSchema as an array +// but in the future, we should migrate to using ConfigFileSchema +export const VaultsConfigSchema = type(VaultConfigEntrySchema).array(); + +// Type inferred for the whole config +export type VaultsConfig = typeof VaultsConfigSchema.infer; diff --git a/packages/mcp-server/tsconfig.json b/packages/mcp-server/tsconfig.json index bd8543d..88a0383 100644 --- a/packages/mcp-server/tsconfig.json +++ b/packages/mcp-server/tsconfig.json @@ -12,7 +12,10 @@ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, - "noEmit": true, + // "noEmit": true, // Remove or set to false to allow .d.ts file generation + "declaration": true, // Enable .d.ts file generation + "declarationDir": "./dist/types", // Output directory for .d.ts files + "emitDeclarationOnly": true, // Only emit .d.ts files, bun will handle JS compilation // Best practices "strict": true, diff --git a/packages/obsidian-plugin/package.json b/packages/obsidian-plugin/package.json index 86a11d7..2b41400 100644 --- a/packages/obsidian-plugin/package.json +++ b/packages/obsidian-plugin/package.json @@ -28,6 +28,7 @@ "rxjs": "^7.8.1", "semver": "^7.6.3", "shared": "workspace:*", + "@obsidian-mcp-tools/mcp-server": "workspace:*", "svelte": "^5.17.5", "svelte-preprocess": "^6.0.3" }, @@ -41,4 +42,4 @@ "tslib": "2.4.0", "typescript": "^5.7.2" } -} \ No newline at end of file +} 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 e7c5b98..a9d3436 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 @@ -11,6 +11,7 @@ import { installMcpServer } from "../services/install"; import { getInstallationStatus } from "../services/status"; import { uninstallServer } from "../services/uninstall"; + import { writeInternalVaultsConfig } from "../services/vaultConfigManager"; // Import new service import type { InstallationStatus } from "../types"; import { openFolder } from "../utils/openFolder"; @@ -39,8 +40,11 @@ status = { ...status, state: "installing" }; const installPath = await installMcpServer(plugin); - // Update Claude config - await updateClaudeConfig(plugin, installPath.path, apiKey); + // Write the internal vaults.json configuration + await writeInternalVaultsConfig(plugin); + + // Update Claude config (will be modified to not pass specific API key) + await updateClaudeConfig(plugin, installPath.path /* apiKey removed here, will be removed in function def */); status = await getInstallationStatus(plugin); } catch (error) { @@ -100,6 +104,43 @@ {/if} +
+

Vault Settings

+ + {#if plugin.settings.vaults && plugin.settings.vaults.length > 0} +
+
+
Default Vault
+
+ Select the default vault to use when no vault ID is specified +
+
+
+ +
+
+ {:else} +
+
+
+ No vaults configured. Please configure at least one vault. +
+
+
+ {/if} +
+

Dependencies

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..92b6f7d 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 @@ -53,9 +53,9 @@ function getConfigPath(): string { * Updates the Claude Desktop config file with MCP server settings */ export async function updateClaudeConfig( - plugin: Plugin, - serverPath: string, - apiKey?: string + plugin: Plugin, // plugin parameter is kept for now, though not directly used for apiKey + serverPath: string + // apiKey parameter removed ): Promise { try { const configPath = getConfigPath(); @@ -78,10 +78,11 @@ export async function updateClaudeConfig( } // Update config with our server entry + // OBSIDIAN_API_KEY is removed as the server will now use vaults.json config.mcpServers["obsidian-mcp-tools"] = { command: serverPath, env: { - OBSIDIAN_API_KEY: apiKey, + // No OBSIDIAN_API_KEY here }, }; diff --git a/packages/obsidian-plugin/src/features/mcp-server-install/services/vaultConfigManager.ts b/packages/obsidian-plugin/src/features/mcp-server-install/services/vaultConfigManager.ts new file mode 100644 index 0000000..637675d --- /dev/null +++ b/packages/obsidian-plugin/src/features/mcp-server-install/services/vaultConfigManager.ts @@ -0,0 +1,111 @@ +import type McpToolsPlugin from "../../../main"; // Adjusted path +import { logger } from "../../../shared/logger"; // Adjusted path +import fsp from "fs/promises"; +import os from "os"; +import path from "path"; +// Define the types locally to avoid import issues +// These should match the types in packages/mcp-server/src/types/config.ts +interface VaultConfigEntry { + vaultId: string; + name: string; + path?: string; + localRestApiBaseUrl: string; + apiKey: string; +} + +interface ConfigFile { + vaults: VaultConfigEntry[]; + defaultVaultId?: string; +} + +// Path to the mcp-tools configuration directory and vaults.json file +// This should align with what configManager.ts on the server-side expects. +const MCP_TOOLS_CONFIG_DIR = path.join(os.homedir(), ".config", "mcp-tools"); +const VAULTS_JSON_PATH = path.join(MCP_TOOLS_CONFIG_DIR, "vaults.json"); + +/** + * Writes the vault configurations from plugin settings to the central vaults.json file. + * This file is read by the MCP server to know about available vaults. + * @param pluginInstance The instance of McpToolsPlugin containing settings. + */ +export async function writeInternalVaultsConfig(pluginInstance: McpToolsPlugin): Promise { + try { + const vaultConfigsForPlugin = pluginInstance.settings.vaults || []; + const defaultVaultId = pluginInstance.settings.defaultVaultId; + + // Transform VaultConfigForPlugin to VaultConfigEntry + // This assumes VaultConfigForPlugin includes id, name, path, localRestApiBaseUrl, and apiKey + const vaultEntries: VaultConfigEntry[] = vaultConfigsForPlugin.map(vc => ({ + vaultId: vc.id, // Map id to vaultId + name: vc.name, + path: vc.path, + localRestApiBaseUrl: vc.localRestApiBaseUrl, + apiKey: vc.apiKey, // Ensure apiKey is present in VaultConfigForPlugin + })); + + // Create the config file structure + const configFile: ConfigFile = { + vaults: vaultEntries, + defaultVaultId: defaultVaultId + }; + + // Ensure the .config/mcp-tools directory exists + await fsp.mkdir(MCP_TOOLS_CONFIG_DIR, { recursive: true }); + + // Write the config object to vaults.json + await fsp.writeFile(VAULTS_JSON_PATH, JSON.stringify(configFile, null, 2)); + logger.info(`Successfully wrote vaults configuration to ${VAULTS_JSON_PATH}`); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`Failed to write internal vaults configuration to ${VAULTS_JSON_PATH}:`, { error: errorMessage }); + // Decide if this should throw or just log, potentially notify user. + // For now, let's throw to make it visible during development. + throw new Error(`Failed to write vaults.json: ${errorMessage}`); + } +} + +/** + * Reads the vault configurations from the central vaults.json file. + * @returns The vault configurations and default vault ID. + */ +export async function readInternalVaultsConfig(): Promise<{ vaults: VaultConfigEntry[], defaultVaultId?: string }> { + try { + // Check if the file exists + try { + await fsp.access(VAULTS_JSON_PATH); + } catch (e) { + // File doesn't exist, return empty config + return { vaults: [] }; + } + + // Read and parse the file + const fileContent = await fsp.readFile(VAULTS_JSON_PATH, 'utf-8'); + const jsonData = JSON.parse(fileContent); + + // Handle both new format (object with vaults array) and legacy format (just array) + if (Array.isArray(jsonData)) { + // Legacy format + return { + vaults: jsonData.map(v => ({ + vaultId: v.vaultId || v.id, // Handle both vaultId and id for backward compatibility + name: v.name, + path: v.path, + localRestApiBaseUrl: v.localRestApiBaseUrl, + apiKey: v.apiKey + })) + }; + } else { + // New format + return { + vaults: jsonData.vaults || [], + defaultVaultId: jsonData.defaultVaultId + }; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`Failed to read internal vaults configuration from ${VAULTS_JSON_PATH}:`, { error: errorMessage }); + // Return empty config on error + return { vaults: [] }; + } +} diff --git a/packages/obsidian-plugin/src/main.ts b/packages/obsidian-plugin/src/main.ts index 38069f9..4aee7f8 100644 --- a/packages/obsidian-plugin/src/main.ts +++ b/packages/obsidian-plugin/src/main.ts @@ -1,6 +1,7 @@ import { type } from "arktype"; import type { Request, Response } from "express"; -import { Notice, Plugin, TFile } from "obsidian"; +// Import Plugin from 'obsidian' to ensure augmented types are recognized +import { Notice, Plugin, TFile, type McpToolsPluginSettings } from "obsidian"; import { shake } from "radash"; import { lastValueFrom } from "rxjs"; import { @@ -21,7 +22,15 @@ import { } from "./shared"; import { logger } from "./shared/logger"; +// Define DEFAULT_SETTINGS according to the McpToolsPluginSettings interface +// McpToolsPluginSettings should now be available via the 'obsidian' import +const DEFAULT_SETTINGS: McpToolsPluginSettings = { + version: "", // Initialize with current plugin version or leave empty + vaults: [], // Initialize vaults as an empty array +}; + export default class McpToolsPlugin extends Plugin { + settings!: McpToolsPluginSettings; // Add settings property private localRestApi: Dependencies["obsidian-local-rest-api"] = { id: "obsidian-local-rest-api", name: "Local REST API", @@ -35,6 +44,9 @@ export default class McpToolsPlugin extends Plugin { } async onload() { + // Load settings + await this.loadSettings(); + // Initialize features in order await setupCore(this); await setupMcpServerInstall(this); @@ -229,4 +241,12 @@ export default class McpToolsPlugin extends Plugin { onunload() { this.localRestApi.api?.unregister(); } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveSettings() { + await this.saveData(this.settings); + } } diff --git a/packages/obsidian-plugin/src/types.ts b/packages/obsidian-plugin/src/types.ts index f9edd32..c57792c 100644 --- a/packages/obsidian-plugin/src/types.ts +++ b/packages/obsidian-plugin/src/types.ts @@ -1,6 +1,17 @@ declare module "obsidian" { + // Interface for individual vault configuration stored in plugin settings + interface VaultConfigForPlugin { + id: string; // Unique identifier for the vault + name: string; // User-friendly name for the vault + path: string; // Absolute path to the vault + localRestApiBaseUrl: string; // Base URL for the Local REST API of this vault + apiKey: string; // API key for this vault's Local REST API + } + interface McpToolsPluginSettings { version?: string; + vaults?: VaultConfigForPlugin[]; // Array to store configurations for multiple vaults + defaultVaultId?: string; // Optional ID of the default vault to use when no vault is specified } interface Plugin {