├── .clinerules ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .prettierrc.yaml ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── SECURITY.md ├── bun.lock ├── docs ├── features │ ├── mcp-server-install.md │ └── prompt-requirements.md ├── migration-plan.md └── project-architecture.md ├── manifest.json ├── package.json ├── packages ├── mcp-server │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── scripts │ │ └── install.ts │ ├── src │ │ ├── features │ │ │ ├── core │ │ │ │ └── index.ts │ │ │ ├── fetch │ │ │ │ ├── constants.ts │ │ │ │ ├── index.ts │ │ │ │ └── services │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── markdown.test.ts │ │ │ │ │ └── markdown.ts │ │ │ ├── local-rest-api │ │ │ │ └── index.ts │ │ │ ├── prompts │ │ │ │ └── index.ts │ │ │ ├── smart-connections │ │ │ │ └── index.ts │ │ │ ├── templates │ │ │ │ └── index.ts │ │ │ └── version │ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── shared │ │ │ ├── ToolRegistry.ts │ │ │ ├── formatMcpError.ts │ │ │ ├── formatString.ts │ │ │ ├── index.ts │ │ │ ├── logger.ts │ │ │ ├── makeRequest.ts │ │ │ ├── parseTemplateParameters.test.ts │ │ │ └── parseTemplateParameters.ts │ │ └── types │ │ │ └── global.d.ts │ └── tsconfig.json ├── obsidian-plugin │ ├── .editorconfig │ ├── .eslintignore │ ├── .eslintrc │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── bun.config.ts │ ├── docs │ │ └── openapi.yaml │ ├── package.json │ ├── scripts │ │ ├── link.ts │ │ └── zip.ts │ ├── src │ │ ├── features │ │ │ ├── core │ │ │ │ ├── components │ │ │ │ │ └── SettingsTab.svelte │ │ │ │ └── index.ts │ │ │ └── mcp-server-install │ │ │ │ ├── components │ │ │ │ └── McpServerInstallSettings.svelte │ │ │ │ ├── constants │ │ │ │ ├── bundle-time.ts │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── services │ │ │ │ ├── config.ts │ │ │ │ ├── install.ts │ │ │ │ ├── status.ts │ │ │ │ └── uninstall.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils │ │ │ │ ├── getFileSystemAdapter.ts │ │ │ │ └── openFolder.ts │ │ ├── main.ts │ │ ├── shared │ │ │ ├── index.ts │ │ │ └── logger.ts │ │ └── types.ts │ ├── svelte.config.js │ └── tsconfig.json ├── shared │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── logger.ts │ │ └── types │ │ │ ├── index.ts │ │ │ ├── plugin-local-rest-api.ts │ │ │ ├── plugin-smart-connections.ts │ │ │ ├── plugin-templater.ts │ │ │ ├── prompts.ts │ │ │ └── smart-search.ts │ └── tsconfig.json └── test-site │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── README.md │ ├── eslint.config.js │ ├── package.json │ ├── postcss.config.js │ ├── src │ ├── app.css │ ├── app.d.ts │ ├── app.html │ ├── lib │ │ └── index.ts │ └── routes │ │ ├── +layout.svelte │ │ ├── +layout.ts │ │ └── +page.svelte │ ├── static │ └── favicon.png │ ├── svelte.config.js │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── vite.config.ts ├── patches └── svelte@5.16.0.patch ├── scripts └── version.ts └── versions.json /.clinerules: -------------------------------------------------------------------------------- 1 | # Project Architecture 2 | 3 | ## Structure 4 | 5 | ``` 6 | packages/ 7 | ├── mcp-server/ # Server implementation 8 | ├── obsidian-plugin/ # Obsidian plugin 9 | └── shared/ # Shared utilities and types 10 | ``` 11 | 12 | ## Features 13 | 14 | - Self-contained modules in src/features/ with structure: 15 | 16 | ``` 17 | feature/ 18 | ├── components/ # Svelte UI components 19 | ├── services/ # Core business logic 20 | ├── constants/ # Feature-specific constants 21 | ├── types.ts # Types and interfaces 22 | ├── utils.ts # Helper functions 23 | └── index.ts # Public API & setup 24 | ``` 25 | 26 | - feature/index.ts exports a setup function: 27 | 28 | - `function setup(plugin: McpToolsPlugin): { success: true } | { success: false, error: string }` 29 | 30 | - Handle dependencies and state: 31 | 32 | - Check dependencies on setup 33 | - Use Svelte stores for UI state 34 | - Persist settings via Obsidian API 35 | - Clean up on unload/errors 36 | 37 | - Extend plugin settings: 38 | 39 | ```typescript 40 | // features/some-feature/types.ts 41 | declare module "obsidian" { 42 | interface McpToolsPluginSettings { 43 | featureName?: { 44 | setting1?: string; 45 | setting2?: boolean; 46 | }; 47 | } 48 | } 49 | ``` 50 | 51 | - Export UI components: 52 | 53 | ```typescript 54 | // index.ts 55 | export { default as FeatureSettings } from "./components/SettingsTab.svelte"; 56 | export * from "./constants"; 57 | export * from "./types"; 58 | ``` 59 | 60 | ## Error Handling 61 | 62 | - Return descriptive error messages 63 | - Log errors with full context 64 | - Clean up resources on failure 65 | - Use Obsidian Notice for user feedback 66 | - Catch and handle async errors 67 | - Format errors for client responses 68 | 69 | ## Type Safety 70 | 71 | - Use ArkType for runtime validation 72 | - Define types with inference: 73 | 74 | ```typescript 75 | const schema = type({ 76 | name: "string", 77 | required: "boolean?", 78 | config: { 79 | maxSize: "number", 80 | mode: "'strict'|'loose'", 81 | }, 82 | }); 83 | type Config = typeof schema.infer; 84 | ``` 85 | 86 | - Validate external data: 87 | 88 | ```typescript 89 | const result = schema(untrustedData); 90 | if (result instanceof type.errors) { 91 | throw new Error(result.summary); 92 | } 93 | ``` 94 | 95 | - Pipe transformations: 96 | 97 | ```typescript 98 | const transformed = type("string.json.parse") 99 | .pipe(searchSchema) 100 | .to(parametersSchema); 101 | ``` 102 | 103 | - Add descriptions for better errors: 104 | 105 | ```typescript 106 | type({ 107 | query: type("string>0").describe("Search text cannot be empty"), 108 | limit: type("number>0").describe("Result limit must be positive"), 109 | }); 110 | ``` 111 | 112 | ## Development 113 | 114 | - Write in TypeScript strict mode 115 | - Use Bun for building/testing 116 | - Test features in isolation 117 | 118 | ## Core Integrations 119 | 120 | - Obsidian API for vault access 121 | - Obsidian plugins 122 | - Local REST API for communication 123 | - Smart Connections for search 124 | - Templater for templates 125 | 126 | ## Coding Style 127 | 128 | - Prefer functional over OOP 129 | - Use pure functions when possible 130 | - Keep files focused on single responsibility 131 | - Use descriptive, action-oriented names 132 | - Place shared code in shared package 133 | - Keep components small and focused 134 | 135 | # Project Guidelines 136 | 137 | ## Documentation Requirements 138 | 139 | - Update relevant documentation in /docs when modifying features 140 | - Keep README.md in sync with new capabilities 141 | - Maintain changelog entries in CHANGELOG.md 142 | 143 | ## Task Summary Records 144 | 145 | When starting a task: 146 | 147 | - Create a new Markdown file in /cline_docs 148 | - Record the initial objective 149 | - Create a checklist of subtasks 150 | 151 | Maintain the task file: 152 | 153 | - Update the checklist after completing subtasks 154 | - Record what worked and didn't work 155 | 156 | When completing a task: 157 | 158 | - Summarize the task outcome 159 | - Verify the initial objective was completed 160 | - Record final insights 161 | 162 | ## Testing Standards 163 | 164 | - Unit tests required for business logic 165 | - Integration tests for API endpoints 166 | - E2E tests for critical user flows 167 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | branches: 8 | - main 9 | 10 | jobs: 11 | release: 12 | if: github.ref_type == 'tag' 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | id-token: write 17 | attestations: write 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Setup Bun 22 | uses: oven-sh/setup-bun@v2 23 | with: 24 | bun-version: latest 25 | 26 | - name: Create Release 27 | id: create_release 28 | uses: softprops/action-gh-release@v1 29 | with: 30 | generate_release_notes: true 31 | draft: false 32 | prerelease: false 33 | 34 | - name: Install Dependencies 35 | run: bun install --frozen-lockfile 36 | 37 | - name: Run Release Script 38 | env: 39 | GITHUB_DOWNLOAD_URL: ${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.ref_name }} 40 | GITHUB_REF_NAME: ${{ github.ref_name }} 41 | run: bun run release 42 | 43 | - name: Zip Release Artifacts 44 | run: bun run zip 45 | 46 | - name: Generate artifact attestation for MCP server binaries 47 | uses: actions/attest-build-provenance@v2 48 | with: 49 | subject-path: "packages/mcp-server/dist/*" 50 | 51 | - name: Get existing release body 52 | id: get_release_body 53 | uses: actions/github-script@v7 54 | with: 55 | result-encoding: string # This tells the action to return a raw string 56 | script: | 57 | const release = await github.rest.repos.getRelease({ 58 | owner: context.repo.owner, 59 | repo: context.repo.repo, 60 | release_id: ${{ steps.create_release.outputs.id }} 61 | }); 62 | return release.data.body || ''; 63 | 64 | - name: Upload Release Artifacts 65 | env: 66 | GH_WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 67 | uses: ncipollo/release-action@v1 68 | with: 69 | allowUpdates: true 70 | omitName: true 71 | tag: ${{ github.ref_name }} 72 | artifacts: "packages/obsidian-plugin/releases/obsidian-plugin-*.zip,main.js,manifest.json,styles.css,packages/mcp-server/dist/*" 73 | body: | 74 | ${{ steps.get_release_body.outputs.result }} 75 | 76 | --- 77 | ✨ This release includes attested build artifacts. 78 | 📝 View attestation details in the [workflow run](${{ env.GH_WORKFLOW_URL }}) 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | 177 | # Scratch pad 178 | playground/ 179 | main.js 180 | bin/ 181 | docs/planning/ 182 | data.json 183 | cline_docs/temp/ 184 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | trailingComma: "all" 2 | tabWidth: 2 3 | semi: true 4 | singleQuote: false 5 | printWidth: 80 6 | useTabs: false -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.background": "#32167B", 4 | "titleBar.activeBackground": "#451FAC", 5 | "titleBar.activeForeground": "#FAF9FE" 6 | }, 7 | "typescript.tsdk": "node_modules/typescript/lib", 8 | "logViewer.watch": [ 9 | "~/Library/Logs/obsidian-mcp-tools/mcp-server.log", 10 | "~/Library/Logs/obsidian-mcp-tools/obsidian-plugin.log", 11 | "~/Library/Logs/Claude/*.log" 12 | ], 13 | "svelte.plugin.svelte.compilerWarnings": { 14 | "a11y_click_events_have_key_events": "ignore", 15 | "a11y_missing_attribute": "ignore", 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jack Steam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCP Tools for Obsidian 2 | 3 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/jacksteamdev/obsidian-mcp-tools)](https://github.com/jacksteamdev/obsidian-mcp-tools/releases/latest) 4 | [![Build status](https://img.shields.io/github/actions/workflow/status/jacksteamdev/obsidian-mcp-tools/release.yml)](https://github.com/jacksteamdev/obsidian-mcp-tools/actions) 5 | [![License](https://img.shields.io/github/license/jacksteamdev/obsidian-mcp-tools)](LICENSE) 6 | 7 | [Features](#features) | [Installation](#installation) | [Configuration](#configuration) | [Troubleshooting](#troubleshooting) | [Security](#security) | [Development](#development) | [Support](#support) 8 | 9 | MCP Tools for Obsidian enables AI applications like Claude Desktop to securely access and work with your Obsidian vault through the Model Context Protocol (MCP). MCP is an open protocol that standardizes how AI applications can interact with external data sources and tools while maintaining security and user control. [^2] 10 | 11 | This plugin consists of two parts: 12 | 1. An Obsidian plugin that adds MCP capabilities to your vault 13 | 2. A local MCP server that handles communication with AI applications 14 | 15 | When you install this plugin, it will help you set up both components. The MCP server acts as a secure bridge between your vault and AI applications like Claude Desktop. This means AI assistants can read your notes, execute templates, and perform semantic searches - but only when you allow it and only through the server's secure API. The server never gives AI applications direct access to your vault files. [^3] 16 | 17 | > **Privacy Note**: When using Claude Desktop with this plugin, your conversations with Claude are not used to train Anthropic's models by default. [^1] 18 | 19 | ## Features 20 | 21 | When connected to an MCP client like Claude Desktop, this plugin enables: 22 | 23 | - **Vault Access**: Allows AI assistants to read and reference your notes while maintaining your vault's security [^4] 24 | - **Semantic Search**: AI assistants can search your vault based on meaning and context, not just keywords [^5] 25 | - **Template Integration**: Execute Obsidian templates through AI interactions, with dynamic parameters and content generation [^6] 26 | 27 | All features require an MCP-compatible client like Claude Desktop, as this plugin provides the server component that enables these integrations. The plugin does not modify Obsidian's functionality directly - instead, it creates a secure bridge that allows AI applications to work with your vault in powerful ways. 28 | 29 | ## Prerequisites 30 | 31 | ### Required 32 | 33 | - [Obsidian](https://obsidian.md/) v1.7.7 or higher 34 | - [Claude Desktop](https://claude.ai/download) installed and configured 35 | - [Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin installed and configured with an API key 36 | 37 | ### Recommended 38 | 39 | - [Templater](https://silentvoid13.github.io/Templater/) plugin for enhanced template functionality 40 | - [Smart Connections](https://smartconnections.app/) plugin for semantic search capabilities 41 | 42 | ## Installation 43 | 44 | > [!Important] 45 | > This plugin requires a secure server component that runs locally on your computer. The server is distributed as a signed executable, with its complete source code available in `packages/mcp-server/`. For details about our security measures and code signing process, see the [Security](#security) section. 46 | 47 | 1. Install the plugin from Obsidian's Community Plugins 48 | 2. Enable the plugin in Obsidian settings 49 | 3. Open the plugin settings 50 | 4. Click "Install Server" to download and configure the MCP server 51 | 52 | Clicking the install button will: 53 | 54 | - Download the appropriate MCP server binary for your platform 55 | - Configure Claude Desktop to use the server 56 | - Set up necessary permissions and paths 57 | 58 | ### Installation Locations 59 | 60 | - **Server Binary**: {vault}/.obsidian/plugins/obsidian-mcp-tools/bin/ 61 | - **Log Files**: 62 | - macOS: ~/Library/Logs/obsidian-mcp-tools 63 | - Windows: %APPDATA%\obsidian-mcp-tools\logs 64 | - Linux: ~/.local/share/obsidian-mcp-tools/logs 65 | 66 | ## Configuration 67 | 68 | After clicking the "Install Server" button in the plugin settings, the plugin will automatically: 69 | 70 | 1. Download the appropriate MCP server binary 71 | 2. Use your Local REST API plugin's API key 72 | 3. Configure Claude Desktop to use the MCP server 73 | 4. Set up appropriate paths and permissions 74 | 75 | While the configuration process is automated, it requires your explicit permission to install the server binary and modify the Claude Desktop configuration. No additional manual configuration is required beyond this initial setup step. 76 | 77 | ## Troubleshooting 78 | 79 | If you encounter issues: 80 | 81 | 1. Check the plugin settings to verify: 82 | - All required plugins are installed 83 | - The server is properly installed 84 | - Claude Desktop is configured 85 | 2. Review the logs: 86 | - Open plugin settings 87 | - Click "Open Logs" under Resources 88 | - Look for any error messages or warnings 89 | 3. Common Issues: 90 | - **Server won't start**: Ensure Claude Desktop is running 91 | - **Connection errors**: Verify Local REST API plugin is configured 92 | - **Permission errors**: Try reinstalling the server 93 | 94 | ## Security 95 | 96 | ### Binary Distribution 97 | 98 | - All releases are built using GitHub Actions with reproducible builds 99 | - Binaries are signed and attested using SLSA provenance 100 | - Release workflows are fully auditable in the repository 101 | 102 | ### Runtime Security 103 | 104 | - The MCP server runs with minimal required permissions 105 | - All communication is encrypted 106 | - API keys are stored securely using platform-specific credential storage 107 | 108 | ### Binary Verification 109 | 110 | The MCP server binaries are published with [SLSA Provenance attestations](https://slsa.dev/provenance/v1), which provide cryptographic proof of where and how the binaries were built. This helps ensure the integrity and provenance of the binaries you download. 111 | 112 | To verify a binary using the GitHub CLI: 113 | 114 | 1. Install GitHub CLI: 115 | 116 | ```bash 117 | # macOS (Homebrew) 118 | brew install gh 119 | 120 | # Windows (Scoop) 121 | scoop install gh 122 | 123 | # Linux 124 | sudo apt install gh # Debian/Ubuntu 125 | ``` 126 | 127 | 2. Verify the binary: 128 | ```bash 129 | gh attestation verify --owner jacksteamdev 130 | ``` 131 | 132 | The verification will show: 133 | 134 | - The binary's SHA256 hash 135 | - Confirmation that it was built by this repository's GitHub Actions workflows 136 | - The specific workflow file and version tag that created it 137 | - Compliance with SLSA Level 3 build requirements 138 | 139 | This verification ensures the binary hasn't been tampered with and was built directly from this repository's source code. 140 | 141 | ### Reporting Security Issues 142 | 143 | Please report security vulnerabilities via our [security policy](SECURITY.md). 144 | Do not report security vulnerabilities in public issues. 145 | 146 | ## Development 147 | 148 | This project uses a monorepo structure with feature-based architecture. For detailed project architecture documentation, see [.clinerules](.clinerules). 149 | 150 | ### Using Cline 151 | 152 | Some code in this project was implemented using the AI coding agent [Cline](https://cline.bot). Cline uses `cline_docs/` and the `.clinerules` file to understand project architecture and patterns when implementing new features. 153 | 154 | ### Workspace 155 | 156 | This project uses a [Bun](https://bun.sh/) workspace structure: 157 | 158 | ``` 159 | packages/ 160 | ├── mcp-server/ # Server implementation 161 | ├── obsidian-plugin/ # Obsidian plugin 162 | └── shared/ # Shared utilities and types 163 | ``` 164 | 165 | ### Building 166 | 167 | 1. Install dependencies: 168 | ```bash 169 | bun install 170 | ``` 171 | 2. Build all packages: 172 | ```bash 173 | bun run build 174 | ``` 175 | 3. For development: 176 | ```bash 177 | bun run dev 178 | ``` 179 | 180 | ### Requirements 181 | 182 | - [bun](https://bun.sh/) v1.1.42 or higher 183 | - TypeScript 5.0+ 184 | 185 | ## Contributing 186 | 187 | 1. Fork the repository 188 | 2. Create a feature branch 189 | 3. Make your changes 190 | 4. Run tests: 191 | ```bash 192 | bun test 193 | ``` 194 | 5. Submit a pull request 195 | 196 | Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. 197 | 198 | ## Support 199 | 200 | - [Open an issue](https://github.com/jacksteamdev/obsidian-mcp-tools/issues) for bug reports and feature requests 201 | - [Start a discussion](https://github.com/jacksteamdev/obsidian-mcp-tools/discussions) for questions and general help 202 | 203 | ## Changelog 204 | 205 | See [CHANGELOG.md](CHANGELOG.md) for a list of changes in each release. 206 | 207 | ## License 208 | 209 | [MIT License](LICENSE) 210 | 211 | ## Footnotes 212 | 213 | [^1]: For information about Claude data privacy and security, see [Claude AI's data usage policy](https://support.anthropic.com/en/articles/8325621-i-would-like-to-input-sensitive-data-into-free-claude-ai-or-claude-pro-who-can-view-my-conversations) 214 | [^2]: For more information about the Model Context Protocol, see [MCP Introduction](https://modelcontextprotocol.io/introduction) 215 | [^3]: For a list of available MCP Clients, see [MCP Example Clients](https://modelcontextprotocol.io/clients) 216 | [^4]: Requires Obsidian plugin Local REST API 217 | [^5]: Requires Obsidian plugin Smart Connections 218 | [^6]: Requires Obsidian plugin Templater 219 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | The MCP Tools for Obsidian team takes security vulnerabilities seriously. If you discover a security issue, please report it by emailing [jacksteamdev+security@gmail.com]. 6 | 7 | **Please do not report security vulnerabilities through public GitHub issues.** 8 | 9 | When reporting a vulnerability, please include: 10 | - Description of the issue 11 | - Steps to reproduce 12 | - Potential impact 13 | - Any suggested fixes (if you have them) 14 | 15 | You should receive a response within 48 hours. If for some reason you do not, please follow up via email to ensure we received your original message. 16 | 17 | ## Disclosure Policy 18 | 19 | When we receive a security bug report, we will: 20 | 1. Confirm the problem and determine affected versions 21 | 2. Audit code to find any similar problems 22 | 3. Prepare fixes for all supported releases 23 | 4. Release new versions and notify users 24 | 25 | ## Binary Distribution Security 26 | 27 | MCP Tools for Obsidian uses several measures to ensure secure binary distribution: 28 | 29 | 1. **SLSA Provenance**: All binaries are built using GitHub Actions with [SLSA Level 3](https://slsa.dev) provenance attestation 30 | 2. **Reproducible Builds**: Our build process is deterministic and can be reproduced from source 31 | 3. **Verification**: Users can verify binary authenticity using: 32 | ```bash 33 | gh attestation verify --owner jacksteamdev 34 | ``` 35 | 36 | ## Runtime Security Model 37 | 38 | The MCP server operates with the following security principles: 39 | 40 | 1. **Minimal Permissions**: 41 | - Operates only in user space 42 | - Requires access only to: 43 | - Obsidian vault directory 44 | - Claude Desktop configuration 45 | - System logging directory 46 | 47 | 2. **API Security**: 48 | - All communication is encrypted 49 | - Input validation and sanitization 50 | 51 | 3. **Data Privacy**: 52 | - No telemetry collection 53 | - No external network calls except to Claude Desktop 54 | - All processing happens locally 55 | 56 | ## Dependencies 57 | 58 | We regularly monitor and update our dependencies for security vulnerabilities: 59 | - Automated security scanning with GitHub Dependabot 60 | - Regular dependency audits 61 | - Prompt patching of known vulnerabilities 62 | 63 | ## Security Update Policy 64 | 65 | - Critical vulnerabilities: Patch within 24 hours 66 | - High severity: Patch within 7 days 67 | - Other vulnerabilities: Address in next release 68 | 69 | ## Supported Versions 70 | 71 | We provide security updates for: 72 | - Current major version: Full support 73 | - Previous major version: Critical security fixes only 74 | 75 | ## Best Practices for Users 76 | 77 | 1. **Binary Verification**: 78 | - Always verify downloaded binaries using GitHub's attestation tools 79 | - Check release signatures and hashes 80 | - Download only from official GitHub releases 81 | 82 | 2. **Configuration**: 83 | - Use unique API keys 84 | - Regularly update to the latest version 85 | - Monitor plugin settings for unexpected changes 86 | 87 | 3. **Monitoring**: 88 | - Check logs for unusual activity 89 | - Review Claude Desktop configuration changes 90 | - Keep track of plugin updates 91 | 92 | ## Security Acknowledgments 93 | 94 | We would like to thank the following individuals and organizations for responsibly disclosing security issues: 95 | 96 | - [To be added as vulnerabilities are reported and fixed] 97 | 98 | ## License 99 | 100 | This security policy is licensed under [MIT License](LICENSE). -------------------------------------------------------------------------------- /docs/features/mcp-server-install.md: -------------------------------------------------------------------------------- 1 | # MCP Server Installation Feature Requirements 2 | 3 | ## Overview 4 | 5 | This feature enables users to install and manage the MCP server executable through the Obsidian plugin settings interface. The system handles the download of platform-specific binaries, Claude Desktop configuration, and provides clear user feedback throughout the process. 6 | 7 | ## Implementation Location 8 | 9 | The installation feature is implemented in the Obsidian plugin package under `src/features/mcp-server-install`. 10 | 11 | ## Installation Flow 12 | 13 | 1. User Prerequisites: 14 | 15 | - Claude Desktop installed 16 | - Local REST API plugin installed and configured with API key 17 | - (Optional) Templater plugin for enhanced functionality 18 | - (Optional) Smart Connections plugin for enhanced search 19 | 20 | 2. Installation Steps: 21 | - User navigates to plugin settings 22 | - Plugin verifies prerequisites and shows status 23 | - User initiates installation via button 24 | - Plugin retrieves API key from Local REST API plugin 25 | - Plugin downloads appropriate binary 26 | - Plugin updates Claude config file 27 | - Plugin confirms successful installation 28 | 29 | ## Settings UI Requirements 30 | 31 | The settings UI is implemented as a Svelte component in `components/SettingsTab.svelte`. 32 | 33 | 1. Component Structure: 34 | ```svelte 35 | 43 | 44 | 45 |
46 | 47 |
48 | 49 | 50 |
51 | 52 |
53 | 54 | 55 | 58 | ``` 59 | 60 | 2. Display Elements: 61 | - Installation status indicator with version 62 | - Install/Update/Uninstall buttons 63 | - Dependency status and links 64 | - Links to: 65 | - Downloaded executable location (with folder access) 66 | - Log folder location (with folder access) 67 | - GitHub repository 68 | - Claude Desktop download page (when needed) 69 | - Required and recommended plugins 70 | 71 | 3. State Management: 72 | - Uses Svelte stores for reactive state 73 | - Status states: 74 | - Not Installed 75 | - Installing 76 | - Installed 77 | - Update Available 78 | 79 | ## Download Management 80 | 81 | 1. Binary Source: 82 | 83 | - GitHub latest release 84 | - Platform-specific naming conventions 85 | - Version number included in filename (e.g., mcp-server-1.2.3) 86 | 87 | 2. Installation Locations: 88 | - Binary: {vault}/.obsidian/plugins/{plugin-id}/bin/ 89 | - Logs: 90 | - macOS: ~/Library/Logs/obsidian-mcp-tools 91 | - Windows: %APPDATA%\obsidian-mcp-tools\logs 92 | - Linux: (platform-specific path) 93 | 94 | ## Claude Configuration 95 | 96 | 1. Config File: 97 | - Location: ~/Library/Application Support/Claude/claude_desktop_config.json 98 | - Create base structure if missing: { "mcpServers": {} } 99 | - Add/update only our config entry: 100 | ```json 101 | { 102 | "mcpServers": { 103 | "obsidian-mcp-tools": { 104 | "command": "(absolute path to executable)", 105 | "env": { 106 | "OBSIDIAN_API_KEY": "(stored api key)" 107 | } 108 | } 109 | } 110 | } 111 | ``` 112 | 113 | ## Version Management 114 | 115 | 1. Unified Version Approach: 116 | - Plugin and server share same version number 117 | - Version stored in plugin manifest 118 | - Server provides version via `--version` flag 119 | - Version checked during plugin initialization 120 | 121 | ## User Education 122 | 123 | 1. Documentation Requirements: 124 | - README.md must explain: 125 | - Binary download and installation process 126 | - GitHub source code location 127 | - Claude config file modifications 128 | - Log file locations and purpose 129 | - Settings page must link to full documentation 130 | 131 | ## Error Handling 132 | 133 | 1. Installation Errors: 134 | 135 | - Claude Desktop not installed 136 | - Download failures 137 | - Permission issues 138 | - Version mismatch 139 | 140 | 2. User Feedback: 141 | - Use Obsidian Notice API for progress/status 142 | - Clear error messages with next steps 143 | - Links to troubleshooting resources 144 | 145 | ## Uninstall Process 146 | 147 | 1. Cleanup Actions: 148 | - Remove executable 149 | - Remove our entry from Claude config 150 | - Clear stored plugin data 151 | 152 | ## Appendix: Implementation Insights 153 | 154 | ### Feature Organization 155 | The feature follows a modular structure: 156 | ``` 157 | src/features/mcp-server-install/ 158 | ├── components/ # Svelte components 159 | │ └── SettingsTab.svelte 160 | ├── services/ # Core functionality 161 | │ ├── config.ts # Claude config management 162 | │ ├── download.ts # Binary download 163 | │ ├── status.ts # Installation status 164 | │ └── uninstall.ts # Cleanup operations 165 | ├── stores/ # Svelte stores 166 | │ ├── status.ts # Installation status store 167 | │ └── dependencies.ts # Dependencies status store 168 | ├── utils/ # Shared utilities 169 | │ └── openFolder.ts 170 | ├── constants.ts # Configuration 171 | ├── types.ts # Type definitions 172 | └── index.ts # Feature setup & component export 173 | ``` 174 | 175 | ### Key Implementation Decisions 176 | 177 | 1. API Key Management 178 | - Removed manual API key input 179 | - Automatically retrieved from Local REST API plugin 180 | - Reduces user friction and potential errors 181 | 182 | 2. Symlink Resolution 183 | - Added robust symlink handling for binary paths 184 | - Ensures correct operation even with complex vault setups 185 | - Handles non-existent paths during resolution 186 | 187 | 3. Status Management 188 | - Unified status interface with version tracking 189 | - Real-time status updates during operations 190 | - Clear feedback for update availability 191 | 192 | 4. Error Handling 193 | - Comprehensive prerequisite validation 194 | - Detailed error messages with next steps 195 | - Proper cleanup on failures 196 | - Extensive logging for troubleshooting 197 | 198 | 5. User Experience 199 | - Reactive UI with Svelte components 200 | - One-click installation process 201 | - Direct access to logs and binaries 202 | - Clear dependency requirements 203 | - Links to all required and recommended plugins 204 | - Real-time status updates through Svelte stores 205 | 206 | ### Recommended Plugins 207 | Added information about recommended plugins that enhance functionality: 208 | - Templater: For template-based operations 209 | - Smart Connections: For enhanced search capabilities 210 | - Local REST API: Required for Obsidian communication 211 | 212 | ### Platform Compatibility 213 | Implemented robust platform detection and path handling: 214 | - Windows: Handles UNC paths and environment variables 215 | - macOS: Proper binary permissions and config paths 216 | - Linux: Flexible configuration for various distributions 217 | 218 | ### Future Considerations 219 | 1. Version Management 220 | - Consider automated update checks 221 | - Add update notifications 222 | - Implement rollback capability 223 | 224 | 2. Configuration 225 | - Add backup/restore of Claude config 226 | - Support custom binary locations 227 | - Allow custom log paths 228 | 229 | 3. Error Recovery 230 | - Add self-repair functionality 231 | - Implement health checks 232 | - Add diagnostic tools 233 | -------------------------------------------------------------------------------- /docs/features/prompt-requirements.md: -------------------------------------------------------------------------------- 1 | # Prompt Feature Implementation Guide 2 | 3 | ## Overview 4 | 5 | Add functionality to load and execute prompts stored as markdown files in Obsidian. 6 | 7 | ## Implementation Areas 8 | 9 | ### 1. MCP Server 10 | 11 | Add prompt management: 12 | 13 | - List prompts from Obsidian's "Prompts" folder 14 | - Parse frontmatter for prompt metadata 15 | - Validate prompt arguments 16 | 17 | #### Schemas 18 | 19 | ```typescript 20 | interface PromptMetadata { 21 | name: string; 22 | description?: string; 23 | arguments?: { 24 | name: string; 25 | description?: string; 26 | required?: boolean; 27 | }[]; 28 | } 29 | 30 | interface ExecutePromptParams { 31 | name: string; 32 | arguments: Record; 33 | createFile?: boolean; 34 | targetPath?: string; 35 | } 36 | ``` 37 | 38 | ### 2. Obsidian Plugin 39 | 40 | Add new endpoint `/prompts/execute`: 41 | 42 | ```typescript 43 | // Add to plugin-apis.ts 44 | export const loadTemplaterAPI = loadPluginAPI( 45 | () => app.plugins.plugins["templater-obsidian"]?.templater 46 | ); 47 | 48 | // Add to main.ts 49 | this.localRestApi 50 | .addRoute("/prompts/execute") 51 | .post(this.handlePromptExecution.bind(this)); 52 | ``` 53 | 54 | ### 3. OpenAPI Updates 55 | 56 | Add to openapi.yaml: 57 | 58 | ```yaml 59 | /prompts/execute: 60 | post: 61 | summary: Execute a prompt template 62 | requestBody: 63 | required: true 64 | content: 65 | application/json: 66 | schema: 67 | $ref: "#/components/schemas/ExecutePromptParams" 68 | responses: 69 | 200: 70 | description: Prompt executed successfully 71 | content: 72 | text/plain: 73 | schema: 74 | type: string 75 | ``` 76 | -------------------------------------------------------------------------------- /docs/migration-plan.md: -------------------------------------------------------------------------------- 1 | # Migration Plan 2 | 3 | This document outlines the step-by-step plan to migrate the current codebase to the new project architecture. 4 | 5 | ## Phase 1: Project Structure Setup 6 | 7 | ### 1. Create Shared Package Structure 8 | ``` 9 | packages/shared/ 10 | ├── src/ 11 | │ ├── types/ 12 | │ │ ├── settings.ts # Base settings interface 13 | │ │ ├── plugin.ts # Plugin-related types 14 | │ │ └── server.ts # Server-related types 15 | │ ├── utils/ 16 | │ │ ├── logger.ts # Shared logging utilities 17 | │ │ └── version.ts # Version management utilities 18 | │ └── constants/ 19 | └── config.ts # Shared configuration constants 20 | ``` 21 | 22 | Tasks: 23 | - [ ] Move common types from plugin and server to shared/types 24 | - [ ] Create base settings interface 25 | - [ ] Extract shared utilities 26 | - [ ] Set up shared tsconfig.json 27 | - [ ] Update package.json dependencies 28 | 29 | ### 2. Reorganize Plugin Structure 30 | ``` 31 | packages/obsidian-plugin/ 32 | ├── src/ 33 | │ ├── features/ 34 | │ │ ├── core/ 35 | │ │ │ ├── components/ 36 | │ │ │ ├── services/ 37 | │ │ │ ├── types.ts 38 | │ │ │ └── index.ts 39 | │ │ ├── mcp-server-install/ 40 | │ │ │ ├── components/ 41 | │ │ │ ├── services/ 42 | │ │ │ │ └── download.ts 43 | │ │ │ ├── types.ts 44 | │ │ │ └── index.ts 45 | │ │ ├── mcp-server-prompts/ 46 | │ │ │ ├── components/ 47 | │ │ │ ├── services/ 48 | │ │ │ ├── types.ts 49 | │ │ │ └── index.ts 50 | │ │ └── smart-search/ 51 | │ │ ├── components/ 52 | │ │ ├── services/ 53 | │ │ ├── types.ts 54 | │ │ └── index.ts 55 | │ └── main.ts 56 | ``` 57 | 58 | Tasks: 59 | - [ ] Create feature module directories 60 | - [ ] Move download.ts to mcp-server-install feature 61 | - [ ] Move template handling to mcp-server-prompts feature 62 | - [ ] Move smart search to smart-search feature 63 | - [ ] Create core feature for plugin initialization 64 | - [ ] Update imports and dependencies 65 | 66 | ### 3. Reorganize Server Structure 67 | ``` 68 | packages/mcp-server/ 69 | ├── src/ 70 | │ ├── features/ 71 | │ │ ├── core/ 72 | │ │ │ ├── services/ 73 | │ │ │ │ └── server.ts 74 | │ │ │ ├── types.ts 75 | │ │ │ └── index.ts 76 | │ │ ├── prompts/ 77 | │ │ │ ├── services/ 78 | │ │ │ ├── types.ts 79 | │ │ │ └── index.ts 80 | │ │ └── tools/ 81 | │ │ ├── services/ 82 | │ │ ├── types.ts 83 | │ │ └── index.ts 84 | │ └── index.ts 85 | ``` 86 | 87 | Tasks: 88 | - [ ] Create feature module directories 89 | - [ ] Move server.ts to core feature 90 | - [ ] Move prompt handling to prompts feature 91 | - [ ] Move tool handling to tools feature 92 | - [ ] Update imports and dependencies 93 | 94 | ## Phase 2: Feature Implementation 95 | 96 | ### 1. Core Feature 97 | Tasks: 98 | - [ ] Implement plugin settings management 99 | - [ ] Create PluginSettingTab with feature UI loading 100 | - [ ] Set up version management system 101 | - [ ] Implement consistent error handling 102 | 103 | ### 2. MCP Server Install Feature 104 | Tasks: 105 | - [ ] Refactor download functionality into service 106 | - [ ] Add version checking 107 | - [ ] Implement error handling and logging 108 | - [ ] Create settings UI component 109 | 110 | ### 3. MCP Server Prompts Feature 111 | Tasks: 112 | - [ ] Implement prompt template management 113 | - [ ] Add argument validation 114 | - [ ] Create prompt execution service 115 | - [ ] Add settings UI for prompt configuration 116 | 117 | ### 4. Smart Search Feature 118 | Tasks: 119 | - [ ] Refactor search functionality into service 120 | - [ ] Implement proper error handling 121 | - [ ] Add search settings management 122 | - [ ] Create search UI components 123 | 124 | ## Phase 3: Build and Testing Setup 125 | 126 | ### 1. Build Configuration 127 | Tasks: 128 | - [ ] Set up shared tsconfig settings 129 | - [ ] Configure ESBuild for plugin bundling 130 | - [ ] Create build scripts for each package 131 | - [ ] Set up version management through versions.json 132 | 133 | ### 2. Testing Environment 134 | Tasks: 135 | - [ ] Create playground environments for each package 136 | - [ ] Set up test configurations 137 | - [ ] Add example test files 138 | - [ ] Create test documentation 139 | 140 | ## Phase 4: Documentation 141 | 142 | ### 1. Code Documentation 143 | Tasks: 144 | - [ ] Add JSDoc comments to all public APIs 145 | - [ ] Create API documentation 146 | - [ ] Document feature configurations 147 | - [ ] Add usage examples 148 | 149 | ### 2. Development Documentation 150 | Tasks: 151 | - [ ] Create development setup guide 152 | - [ ] Document build and test processes 153 | - [ ] Add feature development guide 154 | - [ ] Create troubleshooting guide 155 | 156 | ## Migration Strategy 157 | 158 | 1. **Preparation** 159 | - Create new directory structure 160 | - Set up build configurations 161 | - Create shared package 162 | 163 | 2. **Feature Migration** 164 | - Migrate one feature at a time 165 | - Start with core feature 166 | - Add tests for each feature 167 | - Maintain backwards compatibility 168 | 169 | 3. **Testing** 170 | - Test each migrated feature 171 | - Run integration tests 172 | - Verify plugin functionality 173 | - Test error handling 174 | 175 | 4. **Cleanup** 176 | - Remove old files 177 | - Update documentation 178 | - Verify all features working 179 | - Release new version 180 | 181 | ## Timeline Estimate 182 | 183 | - Phase 1: 1-2 weeks 184 | - Phase 2: 2-3 weeks 185 | - Phase 3: 1 week 186 | - Phase 4: 1 week 187 | 188 | Total estimated time: 5-7 weeks 189 | 190 | ## Risk Management 191 | 192 | 1. **Compatibility Risks** 193 | - Maintain version checks 194 | - Test with different Obsidian versions 195 | - Keep fallback mechanisms 196 | 197 | 2. **Data Migration Risks** 198 | - Back up user settings 199 | - Provide migration utilities 200 | - Document upgrade process 201 | 202 | 3. **Performance Risks** 203 | - Monitor bundle size 204 | - Test with large vaults 205 | - Profile feature performance 206 | 207 | ## Success Criteria 208 | 209 | 1. All features working as before 210 | 2. Improved error handling 211 | 3. Better code organization 212 | 4. Comprehensive documentation 213 | 5. Full test coverage 214 | 6. Smooth upgrade path for users 215 | -------------------------------------------------------------------------------- /docs/project-architecture.md: -------------------------------------------------------------------------------- 1 | # Project Architecture 2 | 3 | Use the following structure and conventions for all new features. 4 | 5 | ## Monorepo Structure 6 | 7 | This project uses a monorepo with multiple packages: 8 | 9 | - `packages/mcp-server` - The MCP server implementation 10 | - `packages/obsidian-plugin` - The Obsidian plugin 11 | - `packages/shared` - Shared code between packages 12 | - `docs/` - Project documentation 13 | - `docs/features` - Feature requirements 14 | 15 | ### Package Organization 16 | 17 | ``` 18 | packages/ 19 | ├── mcp-server/ # Server implementation 20 | │ ├── dist/ # Compiled output 21 | │ ├── logs/ # Server logs 22 | │ ├── playground/ # Development testing 23 | │ ├── scripts/ # Build and utility scripts 24 | │ └── src/ # Source code 25 | │ 26 | ├── obsidian-plugin/ # Obsidian plugin 27 | │ ├── docs/ # Documentation 28 | │ ├── src/ 29 | │ │ ├── features/ # Feature modules 30 | │ │ └── main.ts # Plugin entry point 31 | │ └── manifest.json # Plugin metadata 32 | │ 33 | └── shared/ # Shared utilities and types 34 | └── src/ 35 | ├── types/ # Common interfaces 36 | ├── utils/ # Common utilities 37 | └── constants/ # Shared configuration 38 | ``` 39 | 40 | ## Feature-Based Architecture 41 | 42 | The Obsidian plugin uses a feature-based architecture where each feature is a self-contained module. 43 | 44 | ### Feature Structure 45 | 46 | ``` 47 | src/features/ 48 | ├── core/ # Plugin initialization and settings 49 | ├── mcp-server-install/ # Binary management 50 | ├── mcp-server-prompts/ # Template execution 51 | └── smart-search/ # Search functionality 52 | 53 | Each feature contains: 54 | feature/ 55 | ├── components/ # UI components 56 | ├── services/ # Business logic 57 | ├── types.ts # Feature-specific types 58 | ├── utils.ts # Feature-specific utilities 59 | ├── constants.ts # Feature-specific constants 60 | └── index.ts # Public API with setup function 61 | ``` 62 | 63 | ### Feature Management 64 | 65 | Each feature exports a setup function for initialization: 66 | 67 | ```typescript 68 | export async function setup(plugin: Plugin): Promise { 69 | // Check dependencies 70 | // Initialize services 71 | // Register event handlers 72 | return { success: true } || { success: false, error: "reason" }; 73 | } 74 | ``` 75 | 76 | Features: 77 | 78 | - Initialize independently 79 | - Handle their own dependencies 80 | - Continue running if other features fail 81 | - Log failures for debugging 82 | 83 | ### McpToolsPlugin Settings Management 84 | 85 | Use TypeScript module augmentation to extend the McpToolsPluginSettings interface: 86 | 87 | ```typescript 88 | // packages/obsidian-plugin/src/types.ts 89 | declare module "obsidian" { 90 | interface McpToolsPluginSettings { 91 | version?: string; 92 | } 93 | 94 | interface Plugin { 95 | loadData(): Promise; 96 | saveData(data: McpToolsPluginSettings): Promise; 97 | } 98 | } 99 | 100 | // packages/obsidian-plugin/src/features/some-feature/types.ts 101 | declare module "obsidian" { 102 | interface McpToolsPluginSettings { 103 | featureName?: { 104 | setting1?: string; 105 | setting2?: boolean; 106 | }; 107 | } 108 | } 109 | ``` 110 | 111 | Extending the settings interface allows for type-safe access to feature settings 112 | via `McpToolsPlugin.loadData()` and `McpToolsPlugin.saveData()`. 113 | 114 | ### Version Management 115 | 116 | Unified version approach: 117 | 118 | - Plugin and server share version number 119 | - Version stored in plugin manifest 120 | - Server binaries include version in filename 121 | - Version checked during initialization 122 | 123 | ### UI Integration 124 | 125 | The core feature provides a PluginSettingTab that: 126 | 127 | - Loads UI components from each feature 128 | - Maintains consistent UI organization 129 | - Handles conditional rendering based on feature state 130 | 131 | ### Error Handling 132 | 133 | Features implement consistent error handling: 134 | 135 | - Return descriptive error messages 136 | - Log detailed information for debugging 137 | - Provide user feedback via Obsidian Notice API 138 | - Clean up resources on failure 139 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "mcp-tools", 3 | "name": "MCP Tools", 4 | "version": "0.2.22", 5 | "minAppVersion": "0.15.0", 6 | "description": "Securely connect Claude Desktop to your vault with semantic search, templates, and file management capabilities.", 7 | "author": "Jack Steam", 8 | "authorUrl": "https://github.com/jacksteamdev", 9 | "fundingUrl": "https://github.com/sponsors/jacksteamdev", 10 | "isDesktopOnly": true 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-tools-for-obsidian", 3 | "version": "0.2.22", 4 | "private": true, 5 | "description": "Securely connect Claude Desktop to your Obsidian vault with semantic search, templates, and file management capabilities.", 6 | "tags": [ 7 | "obsidian", 8 | "plugin", 9 | "semantic search", 10 | "templates", 11 | "file management", 12 | "mcp", 13 | "model context protocol" 14 | ], 15 | "workspaces": [ 16 | "packages/*" 17 | ], 18 | "scripts": { 19 | "check": "bun --filter '*' check", 20 | "dev": "bun --filter '*' dev", 21 | "version": "bun scripts/version.ts", 22 | "release": "bun --filter '*' release", 23 | "zip": "bun --filter '*' zip" 24 | }, 25 | "devDependencies": { 26 | "npm-run-all": "^4.1.5" 27 | }, 28 | "patchedDependencies": { 29 | "svelte@5.16.0": "patches/svelte@5.16.0.patch" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/mcp-server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | *.log 4 | .env* 5 | playground/ -------------------------------------------------------------------------------- /packages/mcp-server/README.md: -------------------------------------------------------------------------------- 1 | # MCP Tools for Obsidian - Server 2 | 3 | A secure Model Context Protocol (MCP) server that provides authenticated access to Obsidian vaults. This server implements MCP endpoints for accessing notes, executing templates, and performing semantic search through Claude Desktop and other MCP clients. 4 | 5 | ## Features 6 | 7 | ### Resource Access 8 | 9 | - Read and write vault files via `note://` URIs 10 | - Access file metadata and frontmatter 11 | - Semantic search through Smart Connections 12 | - Template execution via Templater 13 | 14 | ### Security 15 | 16 | - Binary attestation with SLSA provenance 17 | - Encrypted communication via Local REST API 18 | - Platform-specific credential storage 19 | - Minimal required permissions 20 | 21 | ### Tools 22 | 23 | - File operations (create, read, update, delete) 24 | - Semantic search with filters 25 | - Template execution with parameters 26 | - Vault directory listing 27 | 28 | ## Installation 29 | 30 | The server is typically installed automatically through the Obsidian plugin. For manual installation: 31 | 32 | ```bash 33 | # Install dependencies 34 | bun install 35 | 36 | # Build the server 37 | bun run build 38 | ``` 39 | 40 | ```` 41 | 42 | ### Configuration 43 | 44 | Server configuration is managed through Claude Desktop's config file: 45 | 46 | On macOS: 47 | 48 | ```json 49 | // ~/Library/Application Support/Claude/claude_desktop_config.json 50 | { 51 | "mcpServers": { 52 | "obsidian-mcp-tools": { 53 | "command": "/path/to/mcp-server", 54 | "env": { 55 | "OBSIDIAN_API_KEY": "your-api-key" 56 | } 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | ## Development 63 | 64 | ```bash 65 | # Start development server with auto-reload 66 | bun run dev 67 | 68 | # Run tests 69 | bun test 70 | 71 | # Build for all platforms 72 | bun run build:all 73 | 74 | # Use MCP Inspector for debugging 75 | bun run inspector 76 | ``` 77 | 78 | ### Project Structure 79 | 80 | ``` 81 | src/ 82 | ├── features/ # Feature modules 83 | │ ├── core/ # Server core 84 | │ ├── fetch/ # Web content fetching 85 | │ ├── local-rest-api/# API integration 86 | │ ├── prompts/ # Prompt handling 87 | │ └── templates/ # Template execution 88 | ├── shared/ # Shared utilities 89 | └── types/ # TypeScript types 90 | ``` 91 | 92 | ### Binary Distribution 93 | 94 | Server binaries are published with SLSA Provenance attestations. To verify a binary: 95 | 96 | ```bash 97 | gh attestation verify --owner jacksteamdev 98 | ``` 99 | 100 | This verifies: 101 | 102 | - Binary's SHA256 hash 103 | - Build origin from this repository 104 | - Compliance with SLSA Level 3 105 | 106 | ## Protocol Implementation 107 | 108 | ### Resources 109 | 110 | - `note://` - Vault file access 111 | - `template://` - Template execution 112 | - `search://` - Semantic search 113 | 114 | ### Tools 115 | 116 | - `create_note` - Create new files 117 | - `update_note` - Modify existing files 118 | - `execute_template` - Run Templater templates 119 | - `semantic_search` - Smart search integration 120 | 121 | ## Contributing 122 | 123 | 1. Fork the repository 124 | 2. Create a feature branch 125 | 3. Add tests for new functionality 126 | 4. Update documentation 127 | 5. Submit a pull request 128 | 129 | See [CONTRIBUTING.md](../CONTRIBUTING.md) for detailed guidelines. 130 | 131 | ## Security 132 | 133 | For security issues, please: 134 | 135 | 1. **DO NOT** open a public issue 136 | 2. Email [jacksteamdev+security@gmail.com](mailto:jacksteamdev+security@gmail.com) 137 | 3. Follow responsible disclosure practices 138 | 139 | ## License 140 | 141 | [MIT License](LICENSE) 142 | ```` 143 | -------------------------------------------------------------------------------- /packages/mcp-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@obsidian-mcp-tools/mcp-server", 3 | "description": "A secure MCP server implementation that provides standardized access to Obsidian vaults through the Model Context Protocol.", 4 | "type": "module", 5 | "module": "src/index.ts", 6 | "scripts": { 7 | "dev": "bun build ./src/index.ts --watch --compile --outfile ../../bin/mcp-server", 8 | "build": "bun build ./src/index.ts --compile --outfile dist/mcp-server", 9 | "build:linux": "bun build --compile --minify --sourcemap --target=bun-linux-x64-baseline ./src/index.ts --outfile dist/mcp-server-linux", 10 | "build:mac-arm64": "bun build --compile --minify --sourcemap --target=bun-darwin-arm64 ./src/index.ts --outfile dist/mcp-server-macos-arm64", 11 | "build:mac-x64": "bun build --compile --minify --sourcemap --target=bun-darwin-x64 ./src/index.ts --outfile dist/mcp-server-macos-x64", 12 | "build:windows": "bun build --compile --minify --sourcemap --target=bun-windows-x64-baseline ./src/index.ts --outfile dist/mcp-server-windows", 13 | "check": "tsc --noEmit", 14 | "inspector": "npx @modelcontextprotocol/inspector bun src/index.ts", 15 | "release": "run-s build:*", 16 | "setup": "bun run ./scripts/install.ts", 17 | "test": "bun test ./src/**/*.test.ts" 18 | }, 19 | "dependencies": { 20 | "@modelcontextprotocol/sdk": "1.0.4", 21 | "acorn": "^8.14.0", 22 | "acorn-walk": "^8.3.4", 23 | "arktype": "2.0.0-rc.30", 24 | "radash": "^12.1.0", 25 | "shared": "workspace:*", 26 | "turndown": "^7.2.0", 27 | "zod": "^3.24.1" 28 | }, 29 | "devDependencies": { 30 | "@types/bun": "latest", 31 | "@types/turndown": "^5.0.5", 32 | "prettier": "^3.4.2", 33 | "typescript": "^5.3.3" 34 | } 35 | } -------------------------------------------------------------------------------- /packages/mcp-server/scripts/install.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | import path from "path"; 3 | import os from "os"; 4 | import { which } from "bun"; 5 | 6 | function main() { 7 | const args = process.argv.slice(2); 8 | if (args.length < 1) { 9 | console.error("Usage: install.ts "); 10 | process.exit(1); 11 | } 12 | const apiKey = args[0]; 13 | 14 | const configPath = path.join( 15 | os.homedir(), 16 | "Library/Application Support/Claude/claude_desktop_config.json", 17 | ); 18 | 19 | const config = JSON.parse(readFileSync(configPath, "utf-8")); 20 | config.mcpServers["obsidian-mcp-server"] = { 21 | command: which("bun"), 22 | args: [path.resolve(__dirname, "../src/index.ts")], 23 | env: { 24 | OBSIDIAN_API_KEY: apiKey, 25 | }, 26 | }; 27 | 28 | writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8"); 29 | console.log("MCP Server added successfully."); 30 | } 31 | 32 | if (import.meta.main) { 33 | main(); 34 | } 35 | -------------------------------------------------------------------------------- /packages/mcp-server/src/features/core/index.ts: -------------------------------------------------------------------------------- 1 | import { logger, type ToolRegistry, ToolRegistryClass } from "$/shared"; 2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { registerFetchTool } from "../fetch"; 5 | import { registerLocalRestApiTools } from "../local-rest-api"; 6 | import { setupObsidianPrompts } from "../prompts"; 7 | import { registerSmartConnectionsTools } from "../smart-connections"; 8 | import { registerTemplaterTools } from "../templates"; 9 | import { 10 | CallToolRequestSchema, 11 | ListToolsRequestSchema, 12 | } from "@modelcontextprotocol/sdk/types.js"; 13 | 14 | export class ObsidianMcpServer { 15 | private server: Server; 16 | private tools: ToolRegistry; 17 | 18 | constructor() { 19 | this.server = new Server( 20 | { 21 | name: "obsidian-mcp-tools", 22 | version: "0.1.0", 23 | }, 24 | { 25 | capabilities: { 26 | tools: {}, 27 | prompts: {}, 28 | }, 29 | }, 30 | ); 31 | 32 | this.tools = new ToolRegistryClass(); 33 | 34 | this.setupHandlers(); 35 | 36 | // Error handling 37 | this.server.onerror = (error) => { 38 | logger.error("Server error", { error }); 39 | console.error("[MCP Tools Error]", error); 40 | }; 41 | process.on("SIGINT", async () => { 42 | await this.server.close(); 43 | process.exit(0); 44 | }); 45 | } 46 | 47 | private setupHandlers() { 48 | setupObsidianPrompts(this.server); 49 | 50 | registerFetchTool(this.tools, this.server); 51 | registerLocalRestApiTools(this.tools, this.server); 52 | registerSmartConnectionsTools(this.tools); 53 | registerTemplaterTools(this.tools); 54 | 55 | this.server.setRequestHandler(ListToolsRequestSchema, this.tools.list); 56 | this.server.setRequestHandler(CallToolRequestSchema, async (request) => { 57 | logger.debug("Handling request", { request }); 58 | const response = await this.tools.dispatch(request.params, { 59 | server: this.server, 60 | }); 61 | logger.debug("Request handled", { response }); 62 | return response; 63 | }); 64 | } 65 | 66 | async run() { 67 | logger.debug("Starting server..."); 68 | const transport = new StdioServerTransport(); 69 | try { 70 | await this.server.connect(transport); 71 | logger.debug("Server started successfully"); 72 | } catch (err) { 73 | logger.fatal("Failed to start server", { 74 | error: err instanceof Error ? err.message : String(err), 75 | }); 76 | process.exit(1); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/mcp-server/src/features/fetch/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"; 2 | -------------------------------------------------------------------------------- /packages/mcp-server/src/features/fetch/index.ts: -------------------------------------------------------------------------------- 1 | import { logger, type ToolRegistry } from "$/shared"; 2 | import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; 3 | import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; 4 | import { type } from "arktype"; 5 | import { DEFAULT_USER_AGENT } from "./constants"; 6 | import { convertHtmlToMarkdown } from "./services/markdown"; 7 | 8 | export function registerFetchTool(tools: ToolRegistry, server: Server) { 9 | tools.register( 10 | type({ 11 | name: '"fetch"', 12 | arguments: { 13 | url: "string", 14 | "maxLength?": type("number").describe("Limit response length."), 15 | "startIndex?": type("number").describe( 16 | "Supports paginated retrieval of content.", 17 | ), 18 | "raw?": type("boolean").describe( 19 | "Returns raw HTML content if raw=true.", 20 | ), 21 | }, 22 | }).describe( 23 | "Reads and returns the content of any web page. Returns the content in Markdown format by default, or can return raw HTML if raw=true parameter is set. Supports pagination through maxLength and startIndex parameters.", 24 | ), 25 | async ({ arguments: args }) => { 26 | logger.info("Fetching URL", { url: args.url }); 27 | 28 | try { 29 | const response = await fetch(args.url, { 30 | headers: { 31 | "User-Agent": DEFAULT_USER_AGENT, 32 | }, 33 | }); 34 | 35 | if (!response.ok) { 36 | throw new McpError( 37 | ErrorCode.InternalError, 38 | `Failed to fetch ${args.url} - status code ${response.status}`, 39 | ); 40 | } 41 | 42 | const contentType = response.headers.get("content-type") || ""; 43 | const text = await response.text(); 44 | 45 | const isHtml = 46 | text.toLowerCase().includes(" maxLength) { 65 | content = content.substring(startIndex, startIndex + maxLength); 66 | content += `\n\nContent truncated. Call the fetch tool with a startIndex of ${ 67 | startIndex + maxLength 68 | } to get more content.`; 69 | } 70 | 71 | logger.debug("URL fetched successfully", { 72 | url: args.url, 73 | contentLength: content.length, 74 | }); 75 | 76 | return { 77 | content: [ 78 | { 79 | type: "text", 80 | text: `${prefix}Contents of ${args.url}:\n${content}`, 81 | }, 82 | { 83 | type: "text", 84 | text: `Pagination: ${JSON.stringify({ 85 | totalLength, 86 | startIndex, 87 | endIndex: startIndex + content.length, 88 | hasMore: true, 89 | })}`, 90 | }, 91 | ], 92 | }; 93 | } catch (error) { 94 | logger.error("Failed to fetch URL", { url: args.url, error }); 95 | throw new McpError( 96 | ErrorCode.InternalError, 97 | `Failed to fetch ${args.url}: ${error}`, 98 | ); 99 | } 100 | }, 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /packages/mcp-server/src/features/fetch/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./markdown"; 2 | -------------------------------------------------------------------------------- /packages/mcp-server/src/features/fetch/services/markdown.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { convertHtmlToMarkdown } from "./markdown"; 3 | 4 | describe("convertHtmlToMarkdown", () => { 5 | const baseUrl = "https://example.com/blog/post"; 6 | 7 | test("converts basic HTML to Markdown", () => { 8 | const html = "

Hello

This is a test

"; 9 | const result = convertHtmlToMarkdown(html, baseUrl); 10 | expect(result).toBe("# Hello\n\nThis is a test"); 11 | }); 12 | 13 | test("resolves relative URLs in links", () => { 14 | const html = 'About'; 15 | const result = convertHtmlToMarkdown(html, baseUrl); 16 | expect(result).toBe("[About](https://example.com/about)"); 17 | }); 18 | 19 | test("resolves relative URLs in images", () => { 20 | const html = 'Test'; 21 | const result = convertHtmlToMarkdown(html, baseUrl); 22 | expect(result).toBe("![Test](https://example.com/images/test.png)"); 23 | }); 24 | 25 | test("removes data URL images", () => { 26 | const html = 'Test'; 27 | const result = convertHtmlToMarkdown(html, baseUrl); 28 | expect(result).toBe(""); 29 | }); 30 | 31 | test("keeps absolute URLs unchanged", () => { 32 | const html = 'Link'; 33 | const result = convertHtmlToMarkdown(html, baseUrl); 34 | expect(result).toBe("[Link](https://other.com/page)"); 35 | }); 36 | 37 | test("extracts article content when present", () => { 38 | const html = ` 39 |
Skip this
40 |
41 |

Keep this

42 |

And this

43 |
44 |
Skip this too
45 | `; 46 | const result = convertHtmlToMarkdown(html, baseUrl); 47 | expect(result).toBe("# Keep this\n\nAnd this"); 48 | }); 49 | 50 | test("extracts nested article content", () => { 51 | const html = ` 52 |
53 |
Skip this
54 |
55 |

Keep this

56 |

And this

57 |
58 |
Skip this too
59 |
60 | `; 61 | const result = convertHtmlToMarkdown(html, baseUrl); 62 | expect(result).toBe("# Keep this\n\nAnd this"); 63 | }); 64 | 65 | test("removes script and style elements", () => { 66 | const html = ` 67 |
68 | 69 |

Keep this

70 | 71 |
72 | `; 73 | const result = convertHtmlToMarkdown(html, baseUrl); 74 | expect(result).toBe("Keep this"); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/mcp-server/src/features/fetch/services/markdown.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "$/shared"; 2 | import TurndownService from "turndown"; 3 | 4 | /** 5 | * Resolves a URL path relative to a base URL. 6 | * 7 | * @param base - The base URL to use for resolving relative paths. 8 | * @param path - The URL path to be resolved. 9 | * @returns The resolved absolute URL. 10 | */ 11 | function resolveUrl(base: string, path: string): string { 12 | // Return path if it's already absolute 13 | if (path.startsWith("http://") || path.startsWith("https://")) { 14 | return path; 15 | } 16 | 17 | // Handle absolute paths that start with / 18 | if (path.startsWith("/")) { 19 | const baseUrl = new URL(base); 20 | return `${baseUrl.protocol}//${baseUrl.host}${path}`; 21 | } 22 | 23 | // Resolve relative paths 24 | const resolved = new URL(path, base); 25 | return resolved.toString(); 26 | } 27 | 28 | /** 29 | * Converts the given HTML content to Markdown format, resolving any relative URLs 30 | * using the provided base URL. 31 | * 32 | * @param html - The HTML content to be converted to Markdown. 33 | * @param baseUrl - The base URL to use for resolving relative URLs in the HTML. 34 | * @returns The Markdown representation of the input HTML. 35 | * 36 | * @example 37 | * ```ts 38 | * const html = await fetch("https://bcurio.us/resources/hdkb/gates/44"); 39 | * const md = convertHtmlToMarkdown(await html.text(), "https://bcurio.us"); 40 | * await Bun.write("playground/bcurious-gate-44.md", md); 41 | * ``` 42 | */ 43 | export function convertHtmlToMarkdown(html: string, baseUrl: string): string { 44 | const turndownService = new TurndownService({ 45 | headingStyle: "atx", 46 | hr: "---", 47 | bulletListMarker: "-", 48 | codeBlockStyle: "fenced", 49 | }); 50 | 51 | const rewriter = new HTMLRewriter() 52 | .on("script,style,meta,template,link", { 53 | element(element) { 54 | element.remove(); 55 | }, 56 | }) 57 | .on("a", { 58 | element(element) { 59 | const href = element.getAttribute("href"); 60 | if (href) { 61 | element.setAttribute("href", resolveUrl(baseUrl, href)); 62 | } 63 | }, 64 | }) 65 | .on("img", { 66 | element(element) { 67 | const src = element.getAttribute("src"); 68 | if (src?.startsWith("data:")) { 69 | element.remove(); 70 | } else if (src) { 71 | element.setAttribute("src", resolveUrl(baseUrl, src)); 72 | } 73 | }, 74 | }); 75 | 76 | let finalHtml = html; 77 | if (html.includes("") + 10; 80 | finalHtml = html.substring(articleStart, articleEnd); 81 | } 82 | 83 | return turndownService 84 | .turndown(rewriter.transform(finalHtml)) 85 | .replace(/\n{3,}/g, "\n\n") 86 | .replace(/\[\n+/g, "[") 87 | .replace(/\n+\]/g, "]"); 88 | } 89 | -------------------------------------------------------------------------------- /packages/mcp-server/src/features/local-rest-api/index.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest, type ToolRegistry } from "$/shared"; 2 | import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; 3 | import { type } from "arktype"; 4 | import { LocalRestAPI } from "shared"; 5 | 6 | export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { 7 | // GET Status 8 | tools.register( 9 | type({ 10 | name: '"get_server_info"', 11 | arguments: "Record", 12 | }).describe( 13 | "Returns basic details about the Obsidian Local REST API and authentication status. This is the only API request that does not require authentication.", 14 | ), 15 | async () => { 16 | const data = await makeRequest(LocalRestAPI.ApiStatusResponse, "/"); 17 | return { 18 | content: [{ type: "text", text: JSON.stringify(data, null, 2) }], 19 | }; 20 | }, 21 | ); 22 | 23 | // GET Active File 24 | tools.register( 25 | type({ 26 | name: '"get_active_file"', 27 | arguments: { 28 | format: type('"markdown" | "json"').optional(), 29 | }, 30 | }).describe( 31 | "Returns the content of the currently active file in Obsidian. Can return either markdown content or a JSON representation including parsed tags and frontmatter.", 32 | ), 33 | async ({ arguments: args }) => { 34 | const format = 35 | args?.format === "json" 36 | ? "application/vnd.olrapi.note+json" 37 | : "text/markdown"; 38 | const data = await makeRequest( 39 | LocalRestAPI.ApiNoteJson.or("string"), 40 | "/active/", 41 | { 42 | headers: { Accept: format }, 43 | }, 44 | ); 45 | const content = 46 | typeof data === "string" ? data : JSON.stringify(data, null, 2); 47 | return { content: [{ type: "text", text: content }] }; 48 | }, 49 | ); 50 | 51 | // PUT Active File 52 | tools.register( 53 | type({ 54 | name: '"update_active_file"', 55 | arguments: { 56 | content: "string", 57 | }, 58 | }).describe("Update the content of the active file open in Obsidian."), 59 | async ({ arguments: args }) => { 60 | await makeRequest(LocalRestAPI.ApiNoContentResponse, "/active/", { 61 | method: "PUT", 62 | body: args.content, 63 | }); 64 | return { 65 | content: [{ type: "text", text: "File updated successfully" }], 66 | }; 67 | }, 68 | ); 69 | 70 | // POST Active File 71 | tools.register( 72 | type({ 73 | name: '"append_to_active_file"', 74 | arguments: { 75 | content: "string", 76 | }, 77 | }).describe("Append content to the end of the currently-open note."), 78 | async ({ arguments: args }) => { 79 | await makeRequest(LocalRestAPI.ApiNoContentResponse, "/active/", { 80 | method: "POST", 81 | body: args.content, 82 | }); 83 | return { 84 | content: [{ type: "text", text: "Content appended successfully" }], 85 | }; 86 | }, 87 | ); 88 | 89 | // PATCH Active File 90 | tools.register( 91 | type({ 92 | name: '"patch_active_file"', 93 | arguments: LocalRestAPI.ApiPatchParameters, 94 | }).describe( 95 | "Insert or modify content in the currently-open note relative to a heading, block reference, or frontmatter field.", 96 | ), 97 | async ({ arguments: args }) => { 98 | const headers: Record = { 99 | Operation: args.operation, 100 | "Target-Type": args.targetType, 101 | Target: args.target, 102 | "Create-Target-If-Missing": "true", 103 | }; 104 | 105 | if (args.targetDelimiter) { 106 | headers["Target-Delimiter"] = args.targetDelimiter; 107 | } 108 | if (args.trimTargetWhitespace !== undefined) { 109 | headers["Trim-Target-Whitespace"] = String(args.trimTargetWhitespace); 110 | } 111 | if (args.contentType) { 112 | headers["Content-Type"] = args.contentType; 113 | } 114 | 115 | const response = await makeRequest( 116 | LocalRestAPI.ApiContentResponse, 117 | "/active/", 118 | { 119 | method: "PATCH", 120 | headers, 121 | body: args.content, 122 | }, 123 | ); 124 | return { 125 | content: [ 126 | { type: "text", text: "File patched successfully" }, 127 | { type: "text", text: response }, 128 | ], 129 | }; 130 | }, 131 | ); 132 | 133 | // DELETE Active File 134 | tools.register( 135 | type({ 136 | name: '"delete_active_file"', 137 | arguments: "Record", 138 | }).describe("Delete the currently-active file in Obsidian."), 139 | async () => { 140 | await makeRequest(LocalRestAPI.ApiNoContentResponse, "/active/", { 141 | method: "DELETE", 142 | }); 143 | return { 144 | content: [{ type: "text", text: "File deleted successfully" }], 145 | }; 146 | }, 147 | ); 148 | 149 | // POST Open File in Obsidian UI 150 | tools.register( 151 | type({ 152 | name: '"show_file_in_obsidian"', 153 | arguments: { 154 | filename: "string", 155 | "newLeaf?": "boolean", 156 | }, 157 | }).describe( 158 | "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.", 159 | ), 160 | async ({ arguments: args }) => { 161 | const query = args.newLeaf ? "?newLeaf=true" : ""; 162 | 163 | await makeRequest( 164 | LocalRestAPI.ApiNoContentResponse, 165 | `/open/${encodeURIComponent(args.filename)}${query}`, 166 | { 167 | method: "POST", 168 | }, 169 | ); 170 | 171 | return { 172 | content: [{ type: "text", text: "File opened successfully" }], 173 | }; 174 | }, 175 | ); 176 | 177 | // POST Search via Dataview or JsonLogic 178 | tools.register( 179 | type({ 180 | name: '"search_vault"', 181 | arguments: { 182 | queryType: '"dataview" | "jsonlogic"', 183 | query: "string", 184 | }, 185 | }).describe( 186 | "Search for documents matching a specified query using either Dataview DQL or JsonLogic.", 187 | ), 188 | async ({ arguments: args }) => { 189 | const contentType = 190 | args.queryType === "dataview" 191 | ? "application/vnd.olrapi.dataview.dql+txt" 192 | : "application/vnd.olrapi.jsonlogic+json"; 193 | 194 | const data = await makeRequest( 195 | LocalRestAPI.ApiSearchResponse, 196 | "/search/", 197 | { 198 | method: "POST", 199 | headers: { "Content-Type": contentType }, 200 | body: args.query, 201 | }, 202 | ); 203 | 204 | return { 205 | content: [{ type: "text", text: JSON.stringify(data, null, 2) }], 206 | }; 207 | }, 208 | ); 209 | 210 | // POST Simple Search 211 | tools.register( 212 | type({ 213 | name: '"search_vault_simple"', 214 | arguments: { 215 | query: "string", 216 | "contextLength?": "number", 217 | }, 218 | }).describe("Search for documents matching a text query."), 219 | async ({ arguments: args }) => { 220 | const query = new URLSearchParams({ 221 | query: args.query, 222 | ...(args.contextLength 223 | ? { 224 | contextLength: String(args.contextLength), 225 | } 226 | : {}), 227 | }); 228 | 229 | const data = await makeRequest( 230 | LocalRestAPI.ApiSimpleSearchResponse, 231 | `/search/simple/?${query}`, 232 | { 233 | method: "POST", 234 | }, 235 | ); 236 | 237 | return { 238 | content: [{ type: "text", text: JSON.stringify(data, null, 2) }], 239 | }; 240 | }, 241 | ); 242 | 243 | // GET Vault Files or Directories List 244 | tools.register( 245 | type({ 246 | name: '"list_vault_files"', 247 | arguments: { 248 | "directory?": "string", 249 | }, 250 | }).describe( 251 | "List files in the root directory or a specified subdirectory of your vault.", 252 | ), 253 | async ({ arguments: args }) => { 254 | const path = args.directory ? `${args.directory}/` : ""; 255 | const data = await makeRequest( 256 | LocalRestAPI.ApiVaultFileResponse.or( 257 | LocalRestAPI.ApiVaultDirectoryResponse, 258 | ), 259 | `/vault/${path}`, 260 | ); 261 | return { 262 | content: [{ type: "text", text: JSON.stringify(data, null, 2) }], 263 | }; 264 | }, 265 | ); 266 | 267 | // GET Vault File Content 268 | tools.register( 269 | type({ 270 | name: '"get_vault_file"', 271 | arguments: { 272 | filename: "string", 273 | "format?": '"markdown" | "json"', 274 | }, 275 | }).describe("Get the content of a file from your vault."), 276 | async ({ arguments: args }) => { 277 | const isJson = args.format === "json"; 278 | const format = isJson 279 | ? "application/vnd.olrapi.note+json" 280 | : "text/markdown"; 281 | const data = await makeRequest( 282 | isJson ? LocalRestAPI.ApiNoteJson : LocalRestAPI.ApiContentResponse, 283 | `/vault/${encodeURIComponent(args.filename)}`, 284 | { 285 | headers: { Accept: format }, 286 | }, 287 | ); 288 | return { 289 | content: [ 290 | { 291 | type: "text", 292 | text: 293 | typeof data === "string" ? data : JSON.stringify(data, null, 2), 294 | }, 295 | ], 296 | }; 297 | }, 298 | ); 299 | 300 | // PUT Vault File Content 301 | tools.register( 302 | type({ 303 | name: '"create_vault_file"', 304 | arguments: { 305 | filename: "string", 306 | content: "string", 307 | }, 308 | }).describe("Create a new file in your vault or update an existing one."), 309 | async ({ arguments: args }) => { 310 | await makeRequest( 311 | LocalRestAPI.ApiNoContentResponse, 312 | `/vault/${encodeURIComponent(args.filename)}`, 313 | { 314 | method: "PUT", 315 | body: args.content, 316 | }, 317 | ); 318 | return { 319 | content: [{ type: "text", text: "File created successfully" }], 320 | }; 321 | }, 322 | ); 323 | 324 | // POST Vault File Content 325 | tools.register( 326 | type({ 327 | name: '"append_to_vault_file"', 328 | arguments: { 329 | filename: "string", 330 | content: "string", 331 | }, 332 | }).describe("Append content to a new or existing file."), 333 | async ({ arguments: args }) => { 334 | await makeRequest( 335 | LocalRestAPI.ApiNoContentResponse, 336 | `/vault/${encodeURIComponent(args.filename)}`, 337 | { 338 | method: "POST", 339 | body: args.content, 340 | }, 341 | ); 342 | return { 343 | content: [{ type: "text", text: "Content appended successfully" }], 344 | }; 345 | }, 346 | ); 347 | 348 | // PATCH Vault File Content 349 | tools.register( 350 | type({ 351 | name: '"patch_vault_file"', 352 | arguments: type({ 353 | filename: "string", 354 | }).and(LocalRestAPI.ApiPatchParameters), 355 | }).describe( 356 | "Insert or modify content in a file relative to a heading, block reference, or frontmatter field.", 357 | ), 358 | async ({ arguments: args }) => { 359 | const headers: HeadersInit = { 360 | Operation: args.operation, 361 | "Target-Type": args.targetType, 362 | Target: args.target, 363 | "Create-Target-If-Missing": "true", 364 | }; 365 | 366 | if (args.targetDelimiter) { 367 | headers["Target-Delimiter"] = args.targetDelimiter; 368 | } 369 | if (args.trimTargetWhitespace !== undefined) { 370 | headers["Trim-Target-Whitespace"] = String(args.trimTargetWhitespace); 371 | } 372 | if (args.contentType) { 373 | headers["Content-Type"] = args.contentType; 374 | } 375 | 376 | const response = await makeRequest( 377 | LocalRestAPI.ApiContentResponse, 378 | `/vault/${encodeURIComponent(args.filename)}`, 379 | { 380 | method: "PATCH", 381 | headers, 382 | body: args.content, 383 | }, 384 | ); 385 | 386 | return { 387 | content: [ 388 | { type: "text", text: "File patched successfully" }, 389 | { type: "text", text: response }, 390 | ], 391 | }; 392 | }, 393 | ); 394 | 395 | // DELETE Vault File Content 396 | tools.register( 397 | type({ 398 | name: '"delete_vault_file"', 399 | arguments: { 400 | filename: "string", 401 | }, 402 | }).describe("Delete a file from your vault."), 403 | async ({ arguments: args }) => { 404 | await makeRequest( 405 | LocalRestAPI.ApiNoContentResponse, 406 | `/vault/${encodeURIComponent(args.filename)}`, 407 | { 408 | method: "DELETE", 409 | }, 410 | ); 411 | return { 412 | content: [{ type: "text", text: "File deleted successfully" }], 413 | }; 414 | }, 415 | ); 416 | } 417 | -------------------------------------------------------------------------------- /packages/mcp-server/src/features/prompts/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | formatMcpError, 3 | logger, 4 | makeRequest, 5 | parseTemplateParameters, 6 | } from "$/shared"; 7 | import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; 8 | import { 9 | ErrorCode, 10 | GetPromptRequestSchema, 11 | ListPromptsRequestSchema, 12 | McpError, 13 | } from "@modelcontextprotocol/sdk/types.js"; 14 | import { type } from "arktype"; 15 | import { 16 | buildTemplateArgumentsSchema, 17 | LocalRestAPI, 18 | PromptFrontmatterSchema, 19 | type PromptMetadata, 20 | } from "shared"; 21 | 22 | const PROMPT_DIRNAME = `Prompts`; 23 | 24 | export function setupObsidianPrompts(server: Server) { 25 | server.setRequestHandler(ListPromptsRequestSchema, async () => { 26 | try { 27 | const { files } = await makeRequest( 28 | LocalRestAPI.ApiVaultDirectoryResponse, 29 | `/vault/${PROMPT_DIRNAME}/`, 30 | ); 31 | const prompts: PromptMetadata[] = ( 32 | await Promise.all( 33 | files.map(async (filename) => { 34 | // Skip non-Markdown files 35 | if (!filename.endsWith(".md")) return []; 36 | 37 | // Retrieve frontmatter and content from vault file 38 | const file = await makeRequest( 39 | LocalRestAPI.ApiVaultFileResponse, 40 | `/vault/${PROMPT_DIRNAME}/${filename}`, 41 | { 42 | headers: { Accept: LocalRestAPI.MIME_TYPE_OLRAPI_NOTE_JSON }, 43 | }, 44 | ); 45 | 46 | // Skip files without the prompt template tag 47 | if (!file.tags.includes("mcp-tools-prompt")) { 48 | return []; 49 | } 50 | 51 | return { 52 | name: filename, 53 | description: file.frontmatter.description, 54 | arguments: parseTemplateParameters(file.content), 55 | }; 56 | }), 57 | ) 58 | ).flat(); 59 | return { prompts }; 60 | } catch (err) { 61 | const error = formatMcpError(err); 62 | logger.error("Error in ListPromptsRequestSchema handler", { 63 | error, 64 | message: error.message, 65 | }); 66 | throw error; 67 | } 68 | }); 69 | 70 | server.setRequestHandler(GetPromptRequestSchema, async ({ params }) => { 71 | try { 72 | const promptFilePath = `${PROMPT_DIRNAME}/${params.name}`; 73 | 74 | // Get prompt content 75 | const { content: template, frontmatter } = await makeRequest( 76 | LocalRestAPI.ApiVaultFileResponse, 77 | `/vault/${promptFilePath}`, 78 | { 79 | headers: { Accept: LocalRestAPI.MIME_TYPE_OLRAPI_NOTE_JSON }, 80 | }, 81 | ); 82 | 83 | const { description } = PromptFrontmatterSchema.assert(frontmatter); 84 | const templateParams = parseTemplateParameters(template); 85 | const templateParamsSchema = buildTemplateArgumentsSchema(templateParams); 86 | const templateArgs = templateParamsSchema(params.arguments); 87 | if (templateArgs instanceof type.errors) { 88 | throw new McpError( 89 | ErrorCode.InvalidParams, 90 | `Invalid arguments: ${templateArgs.summary}`, 91 | ); 92 | } 93 | 94 | const templateExecutionArgs: LocalRestAPI.ApiTemplateExecutionParamsType = 95 | { 96 | name: promptFilePath, 97 | arguments: templateArgs, 98 | }; 99 | 100 | // Process template through Templater plugin 101 | const { content } = await makeRequest( 102 | LocalRestAPI.ApiTemplateExecutionResponse, 103 | "/templates/execute", 104 | { 105 | method: "POST", 106 | headers: { "Content-Type": "application/json" }, 107 | body: JSON.stringify(templateExecutionArgs), 108 | }, 109 | ); 110 | 111 | // Using unsafe assertion b/c the last element is always a string 112 | const withoutFrontmatter = content.split("---").at(-1)!.trim(); 113 | 114 | return { 115 | messages: [ 116 | { 117 | description, 118 | role: "user", 119 | content: { 120 | type: "text", 121 | text: withoutFrontmatter, 122 | }, 123 | }, 124 | ], 125 | }; 126 | } catch (err) { 127 | const error = formatMcpError(err); 128 | logger.error("Error in GetPromptRequestSchema handler", { 129 | error, 130 | message: error.message, 131 | }); 132 | throw error; 133 | } 134 | }); 135 | } 136 | -------------------------------------------------------------------------------- /packages/mcp-server/src/features/smart-connections/index.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest, type ToolRegistry } from "$/shared"; 2 | import { type } from "arktype"; 3 | import { LocalRestAPI } from "shared"; 4 | 5 | export function registerSmartConnectionsTools(tools: ToolRegistry) { 6 | tools.register( 7 | type({ 8 | name: '"search_vault_smart"', 9 | arguments: { 10 | query: type("string>0").describe("A search phrase for semantic search"), 11 | "filter?": { 12 | "folders?": type("string[]").describe( 13 | 'An array of folder names to include. For example, ["Public", "Work"]', 14 | ), 15 | "excludeFolders?": type("string[]").describe( 16 | 'An array of folder names to exclude. For example, ["Private", "Archive"]', 17 | ), 18 | "limit?": type("number>0").describe( 19 | "The maximum number of results to return", 20 | ), 21 | }, 22 | }, 23 | }).describe("Search for documents semantically matching a text string."), 24 | async ({ arguments: args }) => { 25 | const data = await makeRequest( 26 | LocalRestAPI.ApiSmartSearchResponse, 27 | `/search/smart`, 28 | { 29 | method: "POST", 30 | body: JSON.stringify(args), 31 | }, 32 | ); 33 | 34 | return { 35 | content: [{ type: "text", text: JSON.stringify(data, null, 2) }], 36 | }; 37 | }, 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /packages/mcp-server/src/features/templates/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | formatMcpError, 3 | makeRequest, 4 | parseTemplateParameters, 5 | type ToolRegistry, 6 | } from "$/shared"; 7 | import { type } from "arktype"; 8 | import { buildTemplateArgumentsSchema, LocalRestAPI } from "shared"; 9 | 10 | export function registerTemplaterTools(tools: ToolRegistry) { 11 | tools.register( 12 | type({ 13 | name: '"execute_template"', 14 | arguments: LocalRestAPI.ApiTemplateExecutionParams.omit("createFile").and( 15 | { 16 | // should be boolean but the MCP client returns a string 17 | "createFile?": type("'true'|'false'"), 18 | }, 19 | ), 20 | }).describe("Execute a Templater template with the given arguments"), 21 | async ({ arguments: args }) => { 22 | // Get prompt content 23 | const data = await makeRequest( 24 | LocalRestAPI.ApiVaultFileResponse, 25 | `/vault/${args.name}`, 26 | { 27 | headers: { Accept: LocalRestAPI.MIME_TYPE_OLRAPI_NOTE_JSON }, 28 | }, 29 | ); 30 | 31 | // Validate prompt arguments 32 | const templateParameters = parseTemplateParameters(data.content); 33 | const validArgs = buildTemplateArgumentsSchema(templateParameters)( 34 | args.arguments, 35 | ); 36 | if (validArgs instanceof type.errors) { 37 | throw formatMcpError(validArgs); 38 | } 39 | 40 | const templateExecutionArgs: { 41 | name: string; 42 | arguments: Record; 43 | createFile: boolean; 44 | targetPath?: string; 45 | } = { 46 | name: args.name, 47 | arguments: validArgs, 48 | createFile: args.createFile === "true", 49 | targetPath: args.targetPath, 50 | }; 51 | 52 | // Process template through Templater plugin 53 | const response = await makeRequest( 54 | LocalRestAPI.ApiTemplateExecutionResponse, 55 | "/templates/execute", 56 | { 57 | method: "POST", 58 | headers: { "Content-Type": "application/json" }, 59 | body: JSON.stringify(templateExecutionArgs), 60 | }, 61 | ); 62 | 63 | return { 64 | content: [{ type: "text", text: JSON.stringify(response, null, 2) }], 65 | }; 66 | }, 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /packages/mcp-server/src/features/version/index.ts: -------------------------------------------------------------------------------- 1 | import { version } from "../../../../../package.json" with { type: "json" }; 2 | 3 | export function getVersion() { 4 | return version; 5 | } 6 | -------------------------------------------------------------------------------- /packages/mcp-server/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bun 2 | import { logger } from "$/shared"; 3 | import { ObsidianMcpServer } from "./features/core"; 4 | import { getVersion } from "./features/version" with { type: "macro" }; 5 | 6 | async function main() { 7 | try { 8 | // Verify required environment variables 9 | const API_KEY = process.env.OBSIDIAN_API_KEY; 10 | if (!API_KEY) { 11 | throw new Error("OBSIDIAN_API_KEY environment variable is required"); 12 | } 13 | 14 | logger.debug("Starting MCP Tools for Obsidian server..."); 15 | const server = new ObsidianMcpServer(); 16 | await server.run(); 17 | logger.debug("MCP Tools for Obsidian server is running"); 18 | } catch (error) { 19 | logger.fatal("Failed to start server", { 20 | error: error instanceof Error ? error.message : String(error), 21 | }); 22 | await logger.flush(); 23 | throw error; 24 | } 25 | } 26 | 27 | if (process.argv.includes("--version")) { 28 | try { 29 | console.log(getVersion()); 30 | } catch (error) { 31 | console.error(`Error getting version: ${error}`); 32 | process.exit(1); 33 | } 34 | } else { 35 | main().catch((error) => { 36 | console.error(error); 37 | process.exit(1); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/ToolRegistry.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2 | import { 3 | ErrorCode, 4 | McpError, 5 | type Result, 6 | } from "@modelcontextprotocol/sdk/types.js"; 7 | import { type, type Type } from "arktype"; 8 | import { formatMcpError } from "./formatMcpError.js"; 9 | import { logger } from "./logger.js"; 10 | 11 | interface HandlerContext { 12 | server: Server; 13 | } 14 | 15 | const textResult = type({ 16 | type: '"text"', 17 | text: "string", 18 | }); 19 | const imageResult = type({ 20 | type: '"image"', 21 | data: "string.base64", 22 | mimeType: "string", 23 | }); 24 | const resultSchema = type({ 25 | content: textResult.or(imageResult).array(), 26 | "isError?": "boolean", 27 | }); 28 | 29 | type ResultSchema = typeof resultSchema.infer; 30 | 31 | /** 32 | * The ToolRegistry class represents a set of tools that can be used by 33 | * the server. It is a map of request schemas to request handlers 34 | * that provides a list of available tools and a method to handle requests. 35 | */ 36 | export class ToolRegistryClass< 37 | TSchema extends Type< 38 | { 39 | name: string; 40 | arguments?: Record; 41 | }, 42 | {} 43 | >, 44 | THandler extends ( 45 | request: TSchema["infer"], 46 | context: HandlerContext, 47 | ) => Promise, 48 | > extends Map { 49 | private enabled = new Set(); 50 | 51 | register< 52 | Schema extends TSchema, 53 | Handler extends ( 54 | request: Schema["infer"], 55 | context: HandlerContext, 56 | ) => ResultSchema | Promise, 57 | >(schema: Schema, handler: Handler) { 58 | if (this.has(schema)) { 59 | throw new Error(`Tool already registered: ${schema.get("name")}`); 60 | } 61 | this.enable(schema); 62 | return super.set( 63 | schema as unknown as TSchema, 64 | handler as unknown as THandler, 65 | ); 66 | } 67 | 68 | enable = (schema: Schema) => { 69 | this.enabled.add(schema); 70 | return this; 71 | }; 72 | 73 | disable = (schema: Schema) => { 74 | this.enabled.delete(schema); 75 | return this; 76 | }; 77 | 78 | list = () => { 79 | return { 80 | tools: Array.from(this.enabled.values()).map((schema) => { 81 | return { 82 | // @ts-expect-error We know the const property is present for a string 83 | name: schema.get("name").toJsonSchema().const, 84 | description: schema.description, 85 | inputSchema: schema.get("arguments").toJsonSchema(), 86 | }; 87 | }), 88 | }; 89 | }; 90 | 91 | /** 92 | * MCP SDK sends boolean values as "true" or "false". This method coerces the boolean 93 | * values in the request parameters to the expected type. 94 | * 95 | * @param schema Arktype schema 96 | * @param params MCP request parameters 97 | * @returns MCP request parameters with corrected boolean values 98 | */ 99 | private coerceBooleanParams = ( 100 | schema: Schema, 101 | params: Schema["infer"], 102 | ): Schema["infer"] => { 103 | const args = params.arguments; 104 | const argsSchema = schema.get("arguments").exclude("undefined"); 105 | if (!args || !argsSchema) return params; 106 | 107 | const fixed = { ...params.arguments }; 108 | for (const [key, value] of Object.entries(args)) { 109 | const valueSchema = argsSchema.get(key).exclude("undefined"); 110 | if ( 111 | valueSchema.expression === "boolean" && 112 | typeof value === "string" && 113 | ["true", "false"].includes(value) 114 | ) { 115 | fixed[key] = value === "true"; 116 | } 117 | } 118 | 119 | return { ...params, arguments: fixed }; 120 | }; 121 | 122 | dispatch = async ( 123 | params: Schema["infer"], 124 | context: HandlerContext, 125 | ) => { 126 | try { 127 | for (const [schema, handler] of this.entries()) { 128 | if (schema.get("name").allows(params.name)) { 129 | const validParams = schema.assert( 130 | this.coerceBooleanParams(schema, params), 131 | ); 132 | // return await to handle runtime errors here 133 | return await handler(validParams, context); 134 | } 135 | } 136 | throw new McpError( 137 | ErrorCode.InvalidRequest, 138 | `Unknown tool: ${params.name}`, 139 | ); 140 | } catch (error) { 141 | const formattedError = formatMcpError(error); 142 | logger.error(`Error handling ${params.name}`, { 143 | ...formattedError, 144 | message: formattedError.message, 145 | stack: formattedError.stack, 146 | error, 147 | params, 148 | }); 149 | throw formattedError; 150 | } 151 | }; 152 | } 153 | 154 | export type ToolRegistry = ToolRegistryClass< 155 | Type< 156 | { 157 | name: string; 158 | arguments?: Record; 159 | }, 160 | {} 161 | >, 162 | ( 163 | request: { 164 | name: string; 165 | arguments?: Record; 166 | }, 167 | context: HandlerContext, 168 | ) => Promise 169 | >; 170 | -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/formatMcpError.ts: -------------------------------------------------------------------------------- 1 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 2 | import { type } from "arktype"; 3 | 4 | export function formatMcpError(error: unknown) { 5 | if (error instanceof McpError) { 6 | return error; 7 | } 8 | 9 | if (error instanceof type.errors) { 10 | const message = error.summary; 11 | return new McpError(ErrorCode.InvalidParams, message); 12 | } 13 | 14 | if (type({ message: "string" }).allows(error)) { 15 | return new McpError(ErrorCode.InternalError, error.message); 16 | } 17 | 18 | return new McpError( 19 | ErrorCode.InternalError, 20 | "An unexpected error occurred", 21 | error, 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/formatString.ts: -------------------------------------------------------------------------------- 1 | import { zip } from "radash"; 2 | 3 | /** 4 | * Formats a template string with the provided values, while preserving the original indentation. 5 | * This function is used to format error messages or other string templates that need to preserve 6 | * the original formatting. 7 | * 8 | * @param strings - An array of template strings. 9 | * @param values - The values to be interpolated into the template strings. 10 | * @returns The formatted string with the values interpolated. 11 | * 12 | * @example 13 | * const f`` 14 | */ 15 | export const f = (strings: TemplateStringsArray, ...values: any[]) => { 16 | const stack = { stack: "" }; 17 | Error.captureStackTrace(stack, f); 18 | 19 | // Get the first caller's line from the stack trace 20 | const stackLine = stack.stack.split("\n")[1]; 21 | 22 | // Extract column number using regex 23 | // This matches the column number at the end of the line like: "at filename:line:column" 24 | const columnMatch = stackLine.match(/:(\d+)$/); 25 | const columnNumber = columnMatch ? parseInt(columnMatch[1]) - 1 : 0; 26 | 27 | return zip( 28 | strings.map((s) => s.replace(new RegExp(`\n\s{${columnNumber}}`), "\n")), 29 | values, 30 | ) 31 | .flat() 32 | .join("") 33 | .trim(); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./formatMcpError"; 2 | export * from "./formatString"; 3 | export * from "./logger"; 4 | export * from "./makeRequest"; 5 | export * from "./parseTemplateParameters"; 6 | export * from "./ToolRegistry"; 7 | -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from "shared"; 2 | 3 | /** 4 | * The logger instance for the MCP server application. 5 | * This logger is configured with the "obsidian-mcp-tools" app name, writes to the "mcp-server.log" file, 6 | * and uses the "INFO" log level in production environments and "DEBUG" in development environments. 7 | */ 8 | export const logger = createLogger({ 9 | appName: "Claude", 10 | filename: "mcp-server-obsidian-mcp-tools.log", 11 | level: process.env.NODE_ENV === "production" ? "INFO" : "DEBUG", 12 | }); 13 | -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/makeRequest.ts: -------------------------------------------------------------------------------- 1 | import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; 2 | import { type, type Type } from "arktype"; 3 | import { logger } from "./logger"; 4 | 5 | // Default to HTTPS port, fallback to HTTP if specified 6 | const USE_HTTP = process.env.OBSIDIAN_USE_HTTP === "true"; 7 | const PORT = USE_HTTP ? 27123 : 27124; 8 | const PROTOCOL = USE_HTTP ? "http" : "https"; 9 | export const BASE_URL = `${PROTOCOL}://127.0.0.1:${PORT}`; 10 | 11 | // Disable TLS certificate validation for local self-signed certificates 12 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 13 | 14 | /** 15 | * Makes a request to the Obsidian Local REST API with the provided path and optional request options. 16 | * Automatically adds the required API key to the request headers. 17 | * Throws an `McpError` if the API response is not successful. 18 | * 19 | * @param path - The path to the Obsidian API endpoint. 20 | * @param init - Optional request options to pass to the `fetch` function. 21 | * @returns The response from the Obsidian API. 22 | */ 23 | 24 | export async function makeRequest< 25 | T extends 26 | | Type<{}, {}> 27 | | Type 28 | | Type<{} | null | undefined, {}>, 29 | >(schema: T, path: string, init?: RequestInit): Promise { 30 | const API_KEY = process.env.OBSIDIAN_API_KEY; 31 | if (!API_KEY) { 32 | logger.error("OBSIDIAN_API_KEY environment variable is required", { 33 | env: process.env, 34 | }); 35 | throw new Error("OBSIDIAN_API_KEY environment variable is required"); 36 | } 37 | 38 | const url = `${BASE_URL}${path}`; 39 | const response = await fetch(url, { 40 | ...init, 41 | headers: { 42 | Authorization: `Bearer ${API_KEY}`, 43 | "Content-Type": "text/markdown", 44 | ...init?.headers, 45 | }, 46 | }); 47 | 48 | if (!response.ok) { 49 | const error = await response.text(); 50 | const message = `${init?.method ?? "GET"} ${path} ${response.status}: ${error}`; 51 | throw new McpError(ErrorCode.InternalError, message); 52 | } 53 | 54 | const isJSON = !!response.headers.get("Content-Type")?.includes("json"); 55 | const data = isJSON ? await response.json() : await response.text(); 56 | // 204 No Content responses should be validated as undefined 57 | const validated = response.status === 204 ? undefined : schema(data); 58 | if (validated instanceof type.errors) { 59 | const stackError = new Error(); 60 | Error.captureStackTrace(stackError, makeRequest); 61 | logger.error("Invalid response from Obsidian API", { 62 | status: response.status, 63 | error: validated.summary, 64 | stack: stackError.stack, 65 | data, 66 | }); 67 | throw new McpError( 68 | ErrorCode.InternalError, 69 | `${init?.method ?? "GET"} ${path} ${response.status}: ${validated.summary}`, 70 | ); 71 | } 72 | 73 | return validated; 74 | } 75 | -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/parseTemplateParameters.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { parseTemplateParameters } from "./parseTemplateParameters"; 3 | import { PromptParameterSchema } from "shared"; 4 | 5 | describe("parseTemplateParameters", () => { 6 | test("returns empty array for content without parameters", () => { 7 | const content = "No parameters here"; 8 | const result = parseTemplateParameters(content); 9 | PromptParameterSchema.array().assert(result); 10 | expect(result).toEqual([]); 11 | }); 12 | 13 | test("parses single parameter without description", () => { 14 | const content = '<% tp.user.promptArg("name") %>'; 15 | const result = parseTemplateParameters(content); 16 | PromptParameterSchema.array().assert(result); 17 | expect(result).toEqual([{ name: "name" }]); 18 | }); 19 | 20 | test("parses single parameter with description", () => { 21 | const content = '<% tp.user.promptArg("name", "Enter your name") %>'; 22 | const result = parseTemplateParameters(content); 23 | PromptParameterSchema.array().assert(result); 24 | expect(result).toEqual([{ name: "name", description: "Enter your name" }]); 25 | }); 26 | 27 | test("parses multiple parameters", () => { 28 | const content = ` 29 | <% tp.user.promptArg("name", "Enter your name") %> 30 | <% tp.user.promptArg("age", "Enter your age") %> 31 | `; 32 | const result = parseTemplateParameters(content); 33 | PromptParameterSchema.array().assert(result); 34 | expect(result).toEqual([ 35 | { name: "name", description: "Enter your name" }, 36 | { name: "age", description: "Enter your age" }, 37 | ]); 38 | }); 39 | 40 | test("ignores invalid template syntax", () => { 41 | const content = ` 42 | <% invalid.syntax %> 43 | <% tp.user.promptArg("name", "Enter your name") %> 44 | `; 45 | const result = parseTemplateParameters(content); 46 | PromptParameterSchema.array().assert(result); 47 | expect(result).toEqual([{ name: "name", description: "Enter your name" }]); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/parseTemplateParameters.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "acorn"; 2 | import { simple } from "acorn-walk"; 3 | import { type } from "arktype"; 4 | import type { PromptParameter } from "shared"; 5 | import { logger } from "./logger"; 6 | 7 | const CallExpressionSchema = type({ 8 | callee: { 9 | type: "'MemberExpression'", 10 | object: { 11 | type: "'MemberExpression'", 12 | object: { name: "'tp'" }, 13 | property: { name: "'mcpTools'" }, 14 | }, 15 | property: { name: "'prompt'" }, 16 | }, 17 | arguments: type({ type: "'Literal'", value: "string" }).array(), 18 | }); 19 | 20 | /** 21 | * Parses template arguments from the given content string. 22 | * 23 | * The function looks for template argument tags in the content string, which are 24 | * in the format `<% tp.mcpTools.prompt("name", "description") %>`, and extracts 25 | * the name and description of each argument. The extracted arguments are 26 | * returned as an array of `PromptArgument` objects. 27 | * 28 | * @param content - The content string to parse for template arguments. 29 | * @returns An array of `PromptArgument` objects representing the extracted 30 | * template arguments. 31 | */ 32 | export function parseTemplateParameters(content: string): PromptParameter[] { 33 | /** 34 | * Regular expressions for template tags. 35 | * The tags are in the format `<% tp.mcpTools.prompt("name", "description") %>` 36 | * and may contain additional modifiers. 37 | */ 38 | const TEMPLATER_START_TAG = /<%[*-_]*/g; 39 | const TEMPLATER_END_TAG = /[-_]*%>/g; 40 | 41 | // Split content by template tags 42 | const parts = content.split(TEMPLATER_START_TAG); 43 | const parameters: PromptParameter[] = []; 44 | for (const part of parts) { 45 | if (!TEMPLATER_END_TAG.test(part)) continue; 46 | const code = part.split(TEMPLATER_END_TAG)[0].trim(); 47 | 48 | try { 49 | // Parse the extracted code with AST 50 | const ast = parse(code, { 51 | ecmaVersion: "latest", 52 | sourceType: "module", 53 | }); 54 | 55 | simple(ast, { 56 | CallExpression(node) { 57 | if (CallExpressionSchema.allows(node)) { 58 | const argName = node.arguments[0].value; 59 | const argDescription = node.arguments[1]?.value; 60 | parameters.push({ 61 | name: argName, 62 | ...(argDescription ? { description: argDescription } : {}), 63 | }); 64 | } 65 | }, 66 | }); 67 | } catch (error) { 68 | logger.error("Error parsing code", { code, error }); 69 | continue; 70 | } 71 | } 72 | 73 | return parameters; 74 | } 75 | -------------------------------------------------------------------------------- /packages/mcp-server/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | NODE_ENV: "development" | "production"; 5 | NODE_TLS_REJECT_UNAUTHORIZED: `${0 | 1}`; 6 | OBSIDIAN_API_KEY?: string; 7 | OBSIDIAN_USE_HTTP?: string; 8 | } 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /packages/mcp-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false, 26 | 27 | "noErrorTruncation": true, 28 | 29 | "paths": { 30 | "$/*": ["./src/*"] 31 | } 32 | }, 33 | "include": ["src"] 34 | } 35 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /packages/obsidian-plugin/.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | bin 15 | releases/ 16 | 17 | # Exclude sourcemaps 18 | *.map 19 | 20 | # obsidian 21 | data.json 22 | 23 | # Exclude macOS Finder (System Explorer) View States 24 | .DS_Store 25 | 26 | # Scratch files 27 | playground.md 28 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /packages/obsidian-plugin/README.md: -------------------------------------------------------------------------------- 1 | # MCP Tools for Obsidian - Plugin 2 | 3 | The Obsidian plugin component of MCP Tools, providing secure MCP server integration for accessing Obsidian vaults through Claude Desktop and other MCP clients. 4 | 5 | ## Features 6 | 7 | - **Secure Access**: All communication encrypted and authenticated through Local REST API 8 | - **Semantic Search**: Seamless integration with Smart Connections for context-aware search 9 | - **Template Support**: Execute Templater templates through MCP clients 10 | - **File Management**: Comprehensive vault access and management capabilities 11 | - **Security First**: Binary attestation and secure key management 12 | 13 | ## Requirements 14 | 15 | ### Required 16 | 17 | - Obsidian v1.7.7 or higher 18 | - [Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin 19 | 20 | ### Recommended 21 | 22 | - [Smart Connections](https://smartconnections.app/) for semantic search 23 | - [Templater](https://silentvoid13.github.io/Templater/) for template execution 24 | 25 | ## Development 26 | 27 | This plugin is part of the MCP Tools monorepo. For development: 28 | 29 | ```bash 30 | # Install dependencies 31 | bun install 32 | 33 | # Start development build with watch mode 34 | bun run dev 35 | 36 | # Create a production build 37 | bun run build 38 | 39 | # Link plugin to your vault for testing 40 | bun run link 41 | ``` 42 | 43 | ### Project Structure 44 | 45 | ``` 46 | src/ 47 | ├── features/ # Feature modules 48 | │ ├── core/ # Plugin initialization 49 | │ ├── mcp-server/ # Server management 50 | │ └── shared/ # Common utilities 51 | ├── main.ts # Plugin entry point 52 | └── shared/ # Shared types and utilities 53 | ``` 54 | 55 | ### Adding New Features 56 | 57 | 1. Create a new feature module in `src/features/` 58 | 2. Implement the feature's setup function 59 | 3. Add any UI components to the settings tab 60 | 4. Register the feature in `main.ts` 61 | 62 | ## Security 63 | 64 | This plugin follows strict security practices: 65 | 66 | - All server binaries are signed and include SLSA provenance 67 | - Communication is encrypted using Local REST API's TLS 68 | - API keys are stored securely using platform-specific methods 69 | - Server runs with minimal required permissions 70 | 71 | ## Contributing 72 | 73 | 1. Fork the repository 74 | 2. Create a feature branch 75 | 3. Follow the project's TypeScript and Svelte guidelines 76 | 4. Submit a pull request 77 | 78 | ## License 79 | 80 | [MIT License](LICENSE) 81 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/bun.config.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bun 2 | 3 | import { type BuildConfig, type BunPlugin } from "bun"; 4 | import fsp from "fs/promises"; 5 | import { join, parse } from "path"; 6 | import process from "process"; 7 | import { compile, preprocess } from "svelte/compiler"; 8 | import { version } from "../../package.json" assert { type: "json" }; 9 | import svelteConfig from "./svelte.config.js"; 10 | 11 | const banner = `/* 12 | THIS IS A GENERATED/BUNDLED FILE BY BUN 13 | if you want to view the source, please visit https://github.com/jacksteamdev/obsidian-mcp-tools 14 | */ 15 | `; 16 | 17 | // Parse command line arguments 18 | const args = process.argv.slice(2); 19 | const isWatch = args.includes("--watch"); 20 | const isProd = args.includes("--prod"); 21 | 22 | // Svelte plugin implementation 23 | const sveltePlugin: BunPlugin = { 24 | name: "svelte", 25 | setup(build) { 26 | build.onLoad({ filter: /\.svelte$/ }, async ({ path }) => { 27 | try { 28 | const parsed = parse(path); 29 | const source = await Bun.file(path).text(); 30 | const preprocessed = await preprocess(source, svelteConfig.preprocess, { 31 | filename: parsed.base, 32 | }); 33 | const result = compile(preprocessed.code, { 34 | filename: parsed.base, 35 | generate: "client", 36 | css: "injected", 37 | dev: isProd, 38 | }); 39 | 40 | return { 41 | loader: "js", 42 | contents: result.js.code, 43 | }; 44 | } catch (error) { 45 | throw new Error(`Error compiling Svelte component: ${error}`); 46 | } 47 | }); 48 | }, 49 | }; 50 | 51 | const config: BuildConfig = { 52 | entrypoints: ["./src/main.ts"], 53 | outdir: "../..", 54 | minify: isProd, 55 | plugins: [sveltePlugin], 56 | external: [ 57 | "obsidian", 58 | "electron", 59 | "@codemirror/autocomplete", 60 | "@codemirror/collab", 61 | "@codemirror/commands", 62 | "@codemirror/language", 63 | "@codemirror/lint", 64 | "@codemirror/search", 65 | "@codemirror/state", 66 | "@codemirror/view", 67 | "@lezer/common", 68 | "@lezer/highlight", 69 | "@lezer/lr", 70 | ], 71 | target: "node", 72 | format: "cjs", 73 | conditions: ["browser", isProd ? "production" : "development"], 74 | sourcemap: isProd ? "none" : "inline", 75 | define: { 76 | "process.env.NODE_ENV": JSON.stringify( 77 | isProd ? "production" : "development", 78 | ), 79 | "import.meta.filename": JSON.stringify("mcp-tools-for-obsidian.ts"), 80 | // These environment variables are critical for the MCP server download functionality 81 | // They define the base URL and version for downloading the correct server binaries 82 | "process.env.GITHUB_DOWNLOAD_URL": JSON.stringify( 83 | `https://github.com/jacksteamdev/obsidian-mcp-tools/releases/download/${version}` 84 | ), 85 | "process.env.GITHUB_REF_NAME": JSON.stringify(version), 86 | }, 87 | naming: { 88 | entry: "main.js", // Match original output name 89 | }, 90 | // Add banner to output 91 | banner, 92 | }; 93 | 94 | async function build() { 95 | try { 96 | const result = await Bun.build(config); 97 | 98 | if (!result.success) { 99 | console.error("Build failed"); 100 | for (const message of result.logs) { 101 | console.error(message); 102 | } 103 | process.exit(1); 104 | } 105 | 106 | console.log("Build successful"); 107 | } catch (error) { 108 | console.error("Build failed:", error); 109 | process.exit(1); 110 | } 111 | } 112 | 113 | async function watch() { 114 | const watcher = fsp.watch(join(import.meta.dir, "src"), { 115 | recursive: true, 116 | }); 117 | console.log("Watching for changes..."); 118 | for await (const event of watcher) { 119 | console.log(`Detected ${event.eventType} in ${event.filename}`); 120 | await build(); 121 | } 122 | } 123 | 124 | async function main() { 125 | if (isWatch) { 126 | await build(); 127 | return watch(); 128 | } else { 129 | return build(); 130 | } 131 | } 132 | 133 | main().catch((err) => { 134 | console.error(err); 135 | process.exit(1); 136 | }); 137 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@obsidian-mcp-tools/obsidian-plugin", 3 | "description": "The Obsidian plugin component for MCP Tools, enabling secure connections between Obsidian and Claude Desktop through the Model Context Protocol (MCP).", 4 | "keywords": [ 5 | "MCP", 6 | "Claude", 7 | "Chat" 8 | ], 9 | "license": "MIT", 10 | "author": "Jack Steam", 11 | "type": "module", 12 | "main": "main.js", 13 | "scripts": { 14 | "build": "bun run check && bun bun.config.ts --prod", 15 | "check": "tsc --noEmit", 16 | "dev": "bun --watch run bun.config.ts --watch", 17 | "link": "bun scripts/link.ts", 18 | "release": "run-s build zip", 19 | "zip": "bun scripts/zip.ts" 20 | }, 21 | "dependencies": { 22 | "@types/fs-extra": "^11.0.4", 23 | "arktype": "^2.0.0-rc.30", 24 | "express": "^4.21.2", 25 | "fs-extra": "^11.2.0", 26 | "obsidian-local-rest-api": "^2.5.4", 27 | "radash": "^12.1.0", 28 | "rxjs": "^7.8.1", 29 | "semver": "^7.6.3", 30 | "shared": "workspace:*", 31 | "svelte": "^5.17.5", 32 | "svelte-preprocess": "^6.0.3" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^16.11.6", 36 | "@types/semver": "^7.5.8", 37 | "@typescript-eslint/eslint-plugin": "5.29.0", 38 | "@typescript-eslint/parser": "5.29.0", 39 | "archiver": "^7.0.1", 40 | "obsidian": "latest", 41 | "tslib": "2.4.0", 42 | "typescript": "^5.7.2" 43 | } 44 | } -------------------------------------------------------------------------------- /packages/obsidian-plugin/scripts/link.ts: -------------------------------------------------------------------------------- 1 | import { symlinkSync, existsSync, mkdirSync } from "fs"; 2 | import { join, resolve } from "node:path"; 3 | 4 | /** 5 | * This development script creates a symlink to the plugin in the Obsidian vault's plugin directory. This allows you to 6 | * develop the plugin in the repository and see the changes in Obsidian without having to manually copy the files. 7 | * 8 | * This function is not included in the plugin itself. It is only used to set up local development. 9 | * 10 | * Usage: `bun scripts/link.ts ` 11 | * @returns {Promise} 12 | */ 13 | async function main() { 14 | const args = process.argv.slice(2); 15 | if (args.length < 1) { 16 | console.error( 17 | "Usage: bun scripts/link.ts ", 18 | ); 19 | process.exit(1); 20 | } 21 | 22 | const vaultConfigPath = args[0]; 23 | const projectRootDirectory = resolve(__dirname, "../../.."); 24 | const pluginManifestPath = resolve(projectRootDirectory, "manifest.json"); 25 | const pluginsDirectoryPath = join(vaultConfigPath, "plugins"); 26 | 27 | const file = Bun.file(pluginManifestPath); 28 | const manifest = await file.json(); 29 | 30 | const pluginName = manifest.id; 31 | console.log( 32 | `Creating symlink to ${projectRootDirectory} for plugin ${pluginName} in ${pluginsDirectoryPath}`, 33 | ); 34 | 35 | if (!existsSync(pluginsDirectoryPath)) { 36 | mkdirSync(pluginsDirectoryPath, { recursive: true }); 37 | } 38 | 39 | const targetPath = join(pluginsDirectoryPath, pluginName); 40 | 41 | if (existsSync(targetPath)) { 42 | console.log("Symlink already exists."); 43 | return; 44 | } 45 | 46 | symlinkSync(projectRootDirectory, targetPath, "dir"); 47 | console.log("Symlink created successfully."); 48 | 49 | console.log( 50 | "Obsidian plugin linked for local development. Please restart Obsidian.", 51 | ); 52 | } 53 | 54 | main().catch(console.error); 55 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/scripts/zip.ts: -------------------------------------------------------------------------------- 1 | import { create } from "archiver"; 2 | import { createWriteStream } from "fs"; 3 | import fs from "fs-extra"; 4 | import { join, resolve } from "path"; 5 | import { version } from "../../../package.json" with { type: "json" }; 6 | 7 | async function zipPlugin() { 8 | const pluginDir = resolve(import.meta.dir, ".."); 9 | 10 | const releaseDir = join(pluginDir, "releases"); 11 | fs.ensureDirSync(releaseDir); 12 | 13 | const zipFilePath = join(releaseDir, `obsidian-plugin-${version}.zip`); 14 | const output = createWriteStream(zipFilePath); 15 | 16 | const archive = create("zip", { zlib: { level: 9 } }); 17 | archive.pipe(output); 18 | 19 | // Add the required files 20 | archive.file(join(pluginDir, "main.js"), { name: "main.js" }); 21 | archive.file(join(pluginDir, "manifest.json"), { name: "manifest.json" }); 22 | archive.file(join(pluginDir, "styles.css"), { name: "styles.css" }); 23 | 24 | await archive.finalize(); 25 | console.log("Plugin files zipped successfully!"); 26 | } 27 | 28 | zipPlugin().catch(console.error); 29 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/core/components/SettingsTab.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 |
11 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/core/index.ts: -------------------------------------------------------------------------------- 1 | import { mount, unmount } from "svelte"; 2 | import type { SetupResult } from "../mcp-server-install/types"; 3 | import SettingsTab from "./components/SettingsTab.svelte"; 4 | 5 | import { App, PluginSettingTab } from "obsidian"; 6 | import type McpToolsPlugin from "../../main"; 7 | 8 | export class McpToolsSettingTab extends PluginSettingTab { 9 | plugin: McpToolsPlugin; 10 | component?: { 11 | $set?: unknown; 12 | $on?: unknown; 13 | }; 14 | 15 | constructor(app: App, plugin: McpToolsPlugin) { 16 | super(app, plugin); 17 | this.plugin = plugin; 18 | } 19 | 20 | display(): void { 21 | const { containerEl } = this; 22 | containerEl.empty(); 23 | 24 | this.component = mount(SettingsTab, { 25 | target: containerEl, 26 | props: { plugin: this.plugin }, 27 | }); 28 | } 29 | 30 | hide(): void { 31 | this.component && unmount(this.component); 32 | } 33 | } 34 | 35 | export async function setup(plugin: McpToolsPlugin): Promise { 36 | try { 37 | // Add settings tab to plugin 38 | plugin.addSettingTab(new McpToolsSettingTab(plugin.app, plugin)); 39 | 40 | return { success: true }; 41 | } catch (error) { 42 | return { 43 | success: false, 44 | error: error instanceof Error ? error.message : String(error), 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/components/McpServerInstallSettings.svelte: -------------------------------------------------------------------------------- 1 | 73 | 74 |
75 |

Installation status

76 | 77 | {#if status.state === "no api key"} 78 |
Please configure the Local REST API plugin
79 | {:else if status.state === "not installed"} 80 |
81 | MCP Server is not installed 82 | 83 |
84 | {:else if status.state === "installing"} 85 |
Installing MCP server...
86 | {:else if status.state === "installed"} 87 |
88 | MCP Server v{status.versions.server} is installed 89 | 90 |
91 | {:else if status.state === "outdated"} 92 |
93 | Update available (v{status.versions.server} -> v{status.versions.plugin}) 94 | 95 |
96 | {:else if status.state === "uninstalling"} 97 |
Uninstalling MCP server...
98 | {:else if status.state === "error"} 99 |
{status.error}
100 | {/if} 101 |
102 | 103 |
104 |

Dependencies

105 | 106 | {#each $deps as dep (dep.id)} 107 |
108 | {#if dep.installed} 109 | ✅ {dep.name} is installed 110 | {:else} 111 | ❌ 112 | {dep.name} 113 | {dep.required ? "(Required)" : "(Optional)"} 114 | {#if dep.url}How to install?{/if} 115 | {/if} 116 |
117 | {/each} 118 |
119 | 120 | 148 | 149 | 179 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/constants/bundle-time.ts: -------------------------------------------------------------------------------- 1 | import { type } from "arktype"; 2 | import { clean } from "semver"; 3 | 4 | const envVar = type({ 5 | GITHUB_DOWNLOAD_URL: "string.url", 6 | GITHUB_REF_NAME: type("string").pipe((ref) => clean(ref)), 7 | }); 8 | 9 | /** 10 | * Validates a set of environment variables at build time, such as the enpoint URL for GitHub release artifacts. 11 | * Better than define since the build fails if the environment variable is not set. 12 | * 13 | * @returns An object containing the build time constants. 14 | */ 15 | export function environmentVariables() { 16 | try { 17 | const { GITHUB_DOWNLOAD_URL, GITHUB_REF_NAME } = envVar.assert({ 18 | GITHUB_DOWNLOAD_URL: process.env.GITHUB_DOWNLOAD_URL, 19 | GITHUB_REF_NAME: process.env.GITHUB_REF_NAME, 20 | }); 21 | return { GITHUB_DOWNLOAD_URL, GITHUB_REF_NAME }; 22 | } catch (error) { 23 | console.error(`Failed to get environment variables:`, { error }); 24 | throw error; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/constants/index.ts: -------------------------------------------------------------------------------- 1 | import { environmentVariables } from "./bundle-time" with { type: "macro" }; 2 | 3 | export const { GITHUB_DOWNLOAD_URL, GITHUB_REF_NAME } = 4 | environmentVariables(); 5 | 6 | export const BINARY_NAME = { 7 | windows: "mcp-server.exe", 8 | macos: "mcp-server", 9 | linux: "mcp-server", 10 | } as const; 11 | 12 | export const CLAUDE_CONFIG_PATH = { 13 | macos: "~/Library/Application Support/Claude/claude_desktop_config.json", 14 | windows: "%APPDATA%\\Claude\\claude_desktop_config.json", 15 | linux: "~/.config/claude/config.json", 16 | } as const; 17 | 18 | export const LOG_PATH = { 19 | macos: "~/Library/Logs/obsidian-mcp-tools", 20 | windows: "%APPDATA%\\obsidian-mcp-tools\\logs", 21 | linux: "~/.local/share/obsidian-mcp-tools/logs", 22 | } as const; 23 | 24 | export const PLATFORM_TYPES = ["windows", "macos", "linux"] as const; 25 | export type Platform = (typeof PLATFORM_TYPES)[number]; 26 | 27 | export const ARCH_TYPES = ["x64", "arm64"] as const; 28 | export type Arch = (typeof ARCH_TYPES)[number]; 29 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/index.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "obsidian"; 2 | import type { SetupResult } from "./types"; 3 | 4 | export async function setup(plugin: Plugin): Promise { 5 | try { 6 | return { success: true }; 7 | } catch (error) { 8 | return { 9 | success: false, 10 | error: error instanceof Error ? error.message : String(error), 11 | }; 12 | } 13 | } 14 | 15 | // Re-export types and utilities that should be available to other features 16 | export { default as FeatureSettings } from "./components/McpServerInstallSettings.svelte"; 17 | export * from "./constants"; 18 | export { updateClaudeConfig } from "./services/config"; 19 | export { installMcpServer } from "./services/install"; 20 | export { uninstallServer } from "./services/uninstall"; 21 | export * from "./types"; 22 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/services/config.ts: -------------------------------------------------------------------------------- 1 | import fsp from "fs/promises"; 2 | import { Plugin } from "obsidian"; 3 | import os from "os"; 4 | import path from "path"; 5 | import { logger } from "$/shared/logger"; 6 | import { CLAUDE_CONFIG_PATH } from "../constants"; 7 | 8 | interface ClaudeConfig { 9 | mcpServers: { 10 | [key: string]: { 11 | command: string; 12 | args?: string[]; 13 | env?: { 14 | OBSIDIAN_API_KEY?: string; 15 | [key: string]: string | undefined; 16 | }; 17 | }; 18 | }; 19 | } 20 | 21 | /** 22 | * Gets the absolute path to the Claude Desktop config file 23 | */ 24 | function getConfigPath(): string { 25 | const platform = os.platform(); 26 | let configPath: string; 27 | 28 | switch (platform) { 29 | case "darwin": 30 | configPath = CLAUDE_CONFIG_PATH.macos; 31 | break; 32 | case "win32": 33 | configPath = CLAUDE_CONFIG_PATH.windows; 34 | break; 35 | default: 36 | configPath = CLAUDE_CONFIG_PATH.linux; 37 | } 38 | 39 | // Expand ~ to home directory if needed 40 | if (configPath.startsWith("~")) { 41 | configPath = path.join(os.homedir(), configPath.slice(1)); 42 | } 43 | 44 | // Expand environment variables on Windows 45 | if (platform === "win32") { 46 | configPath = configPath.replace(/%([^%]+)%/g, (_, n) => process.env[n] || ""); 47 | } 48 | 49 | return configPath; 50 | } 51 | 52 | /** 53 | * Updates the Claude Desktop config file with MCP server settings 54 | */ 55 | export async function updateClaudeConfig( 56 | plugin: Plugin, 57 | serverPath: string, 58 | apiKey?: string 59 | ): Promise { 60 | try { 61 | const configPath = getConfigPath(); 62 | const configDir = path.dirname(configPath); 63 | 64 | // Ensure config directory exists 65 | await fsp.mkdir(configDir, { recursive: true }); 66 | 67 | // Read existing config or create new one 68 | let config: ClaudeConfig = { mcpServers: {} }; 69 | try { 70 | const content = await fsp.readFile(configPath, "utf8"); 71 | config = JSON.parse(content); 72 | config.mcpServers = config.mcpServers || {}; 73 | } catch (error) { 74 | if ((error as NodeJS.ErrnoException).code !== "ENOENT") { 75 | throw error; 76 | } 77 | // File doesn't exist, use default empty config 78 | } 79 | 80 | // Update config with our server entry 81 | config.mcpServers["obsidian-mcp-tools"] = { 82 | command: serverPath, 83 | env: { 84 | OBSIDIAN_API_KEY: apiKey, 85 | }, 86 | }; 87 | 88 | // Write updated config 89 | await fsp.writeFile(configPath, JSON.stringify(config, null, 2)); 90 | logger.info("Updated Claude config", { configPath }); 91 | } catch (error) { 92 | logger.error("Failed to update Claude config:", { error }); 93 | throw new Error( 94 | `Failed to update Claude config: ${ 95 | error instanceof Error ? error.message : String(error) 96 | }` 97 | ); 98 | } 99 | } 100 | 101 | /** 102 | * Removes the MCP server entry from the Claude Desktop config file 103 | */ 104 | export async function removeFromClaudeConfig(): Promise { 105 | try { 106 | const configPath = getConfigPath(); 107 | 108 | // Read existing config 109 | let config: ClaudeConfig; 110 | try { 111 | const content = await fsp.readFile(configPath, "utf8"); 112 | config = JSON.parse(content); 113 | } catch (error) { 114 | if ((error as NodeJS.ErrnoException).code === "ENOENT") { 115 | // File doesn't exist, nothing to remove 116 | return; 117 | } 118 | throw error; 119 | } 120 | 121 | // Remove our server entry if it exists 122 | if (config.mcpServers && "obsidian-mcp-tools" in config.mcpServers) { 123 | delete config.mcpServers["obsidian-mcp-tools"]; 124 | await fsp.writeFile(configPath, JSON.stringify(config, null, 2)); 125 | logger.info("Removed server from Claude config", { configPath }); 126 | } 127 | } catch (error) { 128 | logger.error("Failed to remove from Claude config:", { error }); 129 | throw new Error( 130 | `Failed to remove from Claude config: ${ 131 | error instanceof Error ? error.message : String(error) 132 | }` 133 | ); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/services/install.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import fsp from "fs/promises"; 3 | import https from "https"; 4 | import { Notice, Plugin } from "obsidian"; 5 | import os from "os"; 6 | import { Observable } from "rxjs"; 7 | import { logger } from "$/shared"; 8 | import { GITHUB_DOWNLOAD_URL, type Arch, type Platform } from "../constants"; 9 | import type { DownloadProgress, InstallPathInfo } from "../types"; 10 | import { getInstallPath } from "./status"; 11 | 12 | export function getPlatform(): Platform { 13 | const platform = os.platform(); 14 | switch (platform) { 15 | case "darwin": 16 | return "macos"; 17 | case "win32": 18 | return "windows"; 19 | default: 20 | return "linux"; 21 | } 22 | } 23 | 24 | export function getArch(): Arch { 25 | return os.arch() as Arch; 26 | } 27 | 28 | export function getDownloadUrl(platform: Platform, arch: Arch): string { 29 | if (platform === "windows") { 30 | return `${GITHUB_DOWNLOAD_URL}/mcp-server-windows.exe`; 31 | } else if (platform === "macos") { 32 | return `${GITHUB_DOWNLOAD_URL}/mcp-server-macos-${arch}`; 33 | } else { // linux 34 | return `${GITHUB_DOWNLOAD_URL}/mcp-server-linux`; // Linux binary doesn't include arch in filename 35 | } 36 | } 37 | 38 | /** 39 | * Ensures that the specified directory path exists and is writable. 40 | * 41 | * If the directory does not exist, it will be created recursively. If the directory 42 | * exists but is not writable, an error will be thrown. 43 | * 44 | * @param dirpath - The real directory path to ensure exists and is writable. 45 | * @throws {Error} If the directory does not exist or is not writable. 46 | */ 47 | export async function ensureDirectory(dirpath: string) { 48 | try { 49 | if (!fs.existsSync(dirpath)) { 50 | await fsp.mkdir(dirpath, { recursive: true }); 51 | } 52 | 53 | // Verify directory was created and is writable 54 | try { 55 | await fsp.access(dirpath, fs.constants.W_OK); 56 | } catch (accessError) { 57 | throw new Error(`Directory exists but is not writable: ${dirpath}`); 58 | } 59 | } catch (error) { 60 | logger.error(`Failed to ensure directory:`, { error }); 61 | throw error; 62 | } 63 | } 64 | 65 | export function downloadFile( 66 | url: string, 67 | outputPath: string, 68 | redirects = 0, 69 | ): Observable { 70 | return new Observable((subscriber) => { 71 | if (redirects > 5) { 72 | subscriber.error(new Error("Too many redirects")); 73 | return; 74 | } 75 | 76 | let fileStream: fs.WriteStream | undefined; 77 | const cleanup = (err?: unknown) => { 78 | if (err) { 79 | logger.debug("Cleaning up incomplete download:", { 80 | outputPath, 81 | writableFinished: JSON.stringify(fileStream?.writableFinished), 82 | error: err instanceof Error ? err.message : String(err), 83 | }); 84 | fileStream?.destroy(); 85 | fsp.unlink(outputPath).catch((unlinkError) => { 86 | logger.error("Failed to clean up incomplete download:", { 87 | outputPath, 88 | error: 89 | unlinkError instanceof Error 90 | ? unlinkError.message 91 | : String(unlinkError), 92 | }); 93 | }); 94 | } else { 95 | fileStream?.close(); 96 | fsp.chmod(outputPath, 0o755).catch((chmodError) => { 97 | logger.error("Failed to set executable permissions:", { 98 | outputPath, 99 | error: 100 | chmodError instanceof Error 101 | ? chmodError.message 102 | : String(chmodError), 103 | }); 104 | }); 105 | } 106 | }; 107 | 108 | https 109 | .get(url, (response) => { 110 | try { 111 | if (!response) { 112 | throw new Error("No response received"); 113 | } 114 | 115 | const statusCode = response.statusCode ?? 0; 116 | 117 | // Handle various HTTP status codes 118 | if (statusCode >= 400) { 119 | throw new Error( 120 | `HTTP Error ${statusCode}: ${response.statusMessage}`, 121 | ); 122 | } 123 | 124 | if (statusCode === 302 || statusCode === 301) { 125 | const redirectUrl = response.headers.location; 126 | if (!redirectUrl) { 127 | throw new Error( 128 | `Redirect (${statusCode}) received but no location header found`, 129 | ); 130 | } 131 | 132 | // Handle redirect by creating a new observable 133 | downloadFile(redirectUrl, outputPath, redirects + 1).subscribe( 134 | subscriber, 135 | ); 136 | return; 137 | } 138 | 139 | if (statusCode !== 200) { 140 | throw new Error(`Unexpected status code: ${statusCode}`); 141 | } 142 | 143 | // Validate content length 144 | const contentLength = response.headers["content-length"]; 145 | const totalBytes = contentLength ? parseInt(contentLength, 10) : 0; 146 | if (contentLength && isNaN(totalBytes)) { 147 | throw new Error("Invalid content-length header"); 148 | } 149 | 150 | try { 151 | fileStream = fs.createWriteStream(outputPath, { 152 | flags: "w", 153 | }); 154 | } catch (err) { 155 | throw new Error( 156 | `Failed to create write stream: ${err instanceof Error ? err.message : String(err)}`, 157 | ); 158 | } 159 | 160 | let downloadedBytes = 0; 161 | 162 | fileStream.on("error", (err) => { 163 | const fileStreamError = new Error( 164 | `File stream error: ${err.message}`, 165 | ); 166 | cleanup(fileStreamError); 167 | subscriber.error(fileStreamError); 168 | }); 169 | 170 | response.on("data", (chunk: Buffer) => { 171 | try { 172 | if (!Buffer.isBuffer(chunk)) { 173 | throw new Error("Received invalid data chunk"); 174 | } 175 | 176 | downloadedBytes += chunk.length; 177 | const percentage = totalBytes 178 | ? (downloadedBytes / totalBytes) * 100 179 | : 0; 180 | 181 | subscriber.next({ 182 | bytesReceived: downloadedBytes, 183 | totalBytes, 184 | percentage: Math.round(percentage * 100) / 100, 185 | }); 186 | } catch (err) { 187 | cleanup(err); 188 | subscriber.error(err); 189 | } 190 | }); 191 | 192 | response.pipe(fileStream); 193 | 194 | fileStream.on("finish", () => { 195 | cleanup(); 196 | subscriber.complete(); 197 | }); 198 | 199 | response.on("error", (err) => { 200 | cleanup(err); 201 | subscriber.error(new Error(`Response error: ${err.message}`)); 202 | }); 203 | } catch (err) { 204 | cleanup(err); 205 | subscriber.error(err instanceof Error ? err : new Error(String(err))); 206 | } 207 | }) 208 | .on("error", (err) => { 209 | cleanup(err); 210 | subscriber.error(new Error(`Network error: ${err.message}`)); 211 | }); 212 | }); 213 | } 214 | 215 | export async function installMcpServer( 216 | plugin: Plugin, 217 | ): Promise { 218 | try { 219 | const platform = getPlatform(); 220 | const arch = getArch(); 221 | const downloadUrl = getDownloadUrl(platform, arch); 222 | const installPath = await getInstallPath(plugin); 223 | if ("error" in installPath) throw new Error(installPath.error); 224 | 225 | await ensureDirectory(installPath.dir); 226 | 227 | const progressNotice = new Notice("Downloading MCP server...", 0); 228 | logger.debug("Downloading MCP server:", { downloadUrl, installPath }); 229 | 230 | const download$ = downloadFile(downloadUrl, installPath.path); 231 | 232 | return new Promise((resolve, reject) => { 233 | download$.subscribe({ 234 | next: (progress: DownloadProgress) => { 235 | progressNotice.setMessage( 236 | `Downloading MCP server: ${progress.percentage}%`, 237 | ); 238 | }, 239 | error: (error: Error) => { 240 | progressNotice.hide(); 241 | new Notice(`Failed to download MCP server: ${error.message}`); 242 | logger.error("Download failed:", { error, installPath }); 243 | reject(error); 244 | }, 245 | complete: () => { 246 | progressNotice.hide(); 247 | new Notice("MCP server downloaded successfully!"); 248 | logger.info("MCP server downloaded", { installPath }); 249 | resolve(installPath); 250 | }, 251 | }); 252 | }); 253 | } catch (error) { 254 | new Notice( 255 | `Failed to install MCP server: ${error instanceof Error ? error.message : String(error)}`, 256 | ); 257 | throw error; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/services/status.ts: -------------------------------------------------------------------------------- 1 | import type McpToolsPlugin from "$/main"; 2 | import { logger } from "$/shared/logger"; 3 | import { exec } from "child_process"; 4 | import fsp from "fs/promises"; 5 | import { Plugin } from "obsidian"; 6 | import path from "path"; 7 | import { clean, lt, valid } from "semver"; 8 | import { promisify } from "util"; 9 | import { BINARY_NAME } from "../constants"; 10 | import type { InstallationStatus, InstallPathInfo } from "../types"; 11 | import { getFileSystemAdapter } from "../utils/getFileSystemAdapter"; 12 | import { getPlatform } from "./install"; 13 | 14 | const execAsync = promisify(exec); 15 | 16 | /** 17 | * Resolves the real path of the given file path, handling cases where the path is a symlink. 18 | * 19 | * @param filepath - The file path to resolve. 20 | * @returns The real path of the file. 21 | * @throws {Error} If the file is not found or the symlink cannot be resolved. 22 | */ 23 | async function resolveSymlinks(filepath: string): Promise { 24 | try { 25 | return await fsp.realpath(filepath); 26 | } catch (error) { 27 | if ((error as NodeJS.ErrnoException).code === "ENOENT") { 28 | const parts = path.normalize(filepath).split(path.sep); 29 | let resolvedParts: string[] = []; 30 | let skipCount = 1; // Skip first segment by default 31 | 32 | // Handle the root segment differently for Windows vs POSIX 33 | if (path.win32.isAbsolute(filepath)) { 34 | resolvedParts.push(parts[0]); 35 | if (parts[1] === "") { 36 | resolvedParts.push(""); 37 | skipCount = 2; // Skip two segments for UNC paths 38 | } 39 | } else if (path.posix.isAbsolute(filepath)) { 40 | resolvedParts.push("/"); 41 | } else { 42 | resolvedParts.push(parts[0]); 43 | } 44 | 45 | // Process remaining path segments 46 | for (const part of parts.slice(skipCount)) { 47 | const partialPath = path.join(...resolvedParts, part); 48 | try { 49 | const resolvedPath = await fsp.realpath(partialPath); 50 | resolvedParts = resolvedPath.split(path.sep); 51 | } catch (err) { 52 | resolvedParts.push(part); 53 | } 54 | } 55 | 56 | return path.join(...resolvedParts); 57 | } 58 | 59 | logger.error(`Failed to resolve symlink:`, { 60 | filepath, 61 | error: error instanceof Error ? error.message : error, 62 | }); 63 | throw new Error(`Failed to resolve symlink: ${filepath}`); 64 | } 65 | } 66 | 67 | export async function getInstallPath( 68 | plugin: Plugin, 69 | ): Promise { 70 | const adapter = getFileSystemAdapter(plugin); 71 | if ("error" in adapter) return adapter; 72 | 73 | const platform = getPlatform(); 74 | const originalPath = path.join( 75 | adapter.getBasePath(), 76 | plugin.app.vault.configDir, 77 | "plugins", 78 | plugin.manifest.id, 79 | "bin", 80 | ); 81 | const realDirPath = await resolveSymlinks(originalPath); 82 | const platformSpecificBinary = BINARY_NAME[platform]; 83 | const realFilePath = path.join(realDirPath, platformSpecificBinary); 84 | return { 85 | dir: realDirPath, 86 | path: realFilePath, 87 | name: platformSpecificBinary, 88 | symlinked: originalPath === realDirPath ? undefined : originalPath, 89 | }; 90 | } 91 | 92 | /** 93 | * Gets the current installation status of the MCP server 94 | */ 95 | export async function getInstallationStatus( 96 | plugin: McpToolsPlugin, 97 | ): Promise { 98 | // Verify plugin version is valid 99 | const pluginVersion = valid(clean(plugin.manifest.version)); 100 | if (!pluginVersion) { 101 | logger.error("Invalid plugin version:", { plugin }); 102 | return { state: "error", versions: {} }; 103 | } 104 | 105 | // Check for API key 106 | const apiKey = plugin.getLocalRestApiKey(); 107 | if (!apiKey) { 108 | return { 109 | state: "no api key", 110 | versions: { plugin: pluginVersion }, 111 | }; 112 | } 113 | 114 | // Verify server binary is present 115 | const installPath = await getInstallPath(plugin); 116 | if ("error" in installPath) { 117 | return { 118 | state: "error", 119 | versions: { plugin: pluginVersion }, 120 | error: installPath.error, 121 | }; 122 | } 123 | 124 | try { 125 | await fsp.access(installPath.path, fsp.constants.X_OK); 126 | } catch (error) { 127 | logger.error("Failed to get server version:", { installPath }); 128 | return { 129 | state: "not installed", 130 | ...installPath, 131 | versions: { plugin: pluginVersion }, 132 | }; 133 | } 134 | 135 | // Check server binary version 136 | let serverVersion: string | null | undefined; 137 | try { 138 | const versionCommand = `"${installPath.path}" --version`; 139 | const { stdout } = await execAsync(versionCommand); 140 | serverVersion = clean(stdout.trim()); 141 | if (!serverVersion) throw new Error("Invalid server version string"); 142 | } catch { 143 | logger.error("Failed to get server version:", { installPath }); 144 | return { 145 | state: "error", 146 | ...installPath, 147 | versions: { plugin: pluginVersion }, 148 | }; 149 | } 150 | 151 | return { 152 | ...installPath, 153 | state: lt(serverVersion, pluginVersion) ? "outdated" : "installed", 154 | versions: { plugin: pluginVersion, server: serverVersion }, 155 | }; 156 | } 157 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/services/uninstall.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "$/shared/logger"; 2 | import fsp from "fs/promises"; 3 | import { Plugin } from "obsidian"; 4 | import path from "path"; 5 | import { BINARY_NAME } from "../constants"; 6 | import { getPlatform } from "./install"; 7 | import { getFileSystemAdapter } from "../utils/getFileSystemAdapter"; 8 | 9 | /** 10 | * Uninstalls the MCP server by removing the binary and cleaning up configuration 11 | */ 12 | export async function uninstallServer(plugin: Plugin): Promise { 13 | try { 14 | const adapter = getFileSystemAdapter(plugin); 15 | if ("error" in adapter) { 16 | throw new Error(adapter.error); 17 | } 18 | 19 | // Remove binary 20 | const platform = getPlatform(); 21 | const binDir = path.join( 22 | adapter.getBasePath(), 23 | plugin.app.vault.configDir, 24 | "plugins", 25 | plugin.manifest.id, 26 | "bin", 27 | ); 28 | const binaryPath = path.join(binDir, BINARY_NAME[platform]); 29 | 30 | try { 31 | await fsp.unlink(binaryPath); 32 | logger.info("Removed server binary", { binaryPath }); 33 | } catch (error) { 34 | if ((error as NodeJS.ErrnoException).code !== "ENOENT") { 35 | throw error; 36 | } 37 | // File doesn't exist, continue 38 | } 39 | 40 | // Remove bin directory if empty 41 | try { 42 | await fsp.rmdir(binDir); 43 | logger.info("Removed empty bin directory", { binDir }); 44 | } catch (error) { 45 | if ((error as NodeJS.ErrnoException).code !== "ENOTEMPTY") { 46 | throw error; 47 | } 48 | // Directory not empty, leave it 49 | } 50 | 51 | // Remove our entry from Claude config 52 | // Note: We don't remove the entire config file since it may contain other server configs 53 | const configPath = path.join( 54 | process.env.HOME || process.env.USERPROFILE || "", 55 | "Library/Application Support/Claude/claude_desktop_config.json", 56 | ); 57 | 58 | try { 59 | const content = await fsp.readFile(configPath, "utf8"); 60 | const config = JSON.parse(content); 61 | 62 | if (config.mcpServers && config.mcpServers["obsidian-mcp-tools"]) { 63 | delete config.mcpServers["obsidian-mcp-tools"]; 64 | await fsp.writeFile(configPath, JSON.stringify(config, null, 2)); 65 | logger.info("Removed server from Claude config", { configPath }); 66 | } 67 | } catch (error) { 68 | if ((error as NodeJS.ErrnoException).code !== "ENOENT") { 69 | throw error; 70 | } 71 | // Config doesn't exist, nothing to clean up 72 | } 73 | 74 | logger.info("Server uninstall complete"); 75 | } catch (error) { 76 | logger.error("Failed to uninstall server:", { error }); 77 | throw new Error( 78 | `Failed to uninstall server: ${ 79 | error instanceof Error ? error.message : String(error) 80 | }`, 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/types.ts: -------------------------------------------------------------------------------- 1 | import type { Templater } from "shared"; 2 | 3 | export interface SetupResult { 4 | success: boolean; 5 | error?: string; 6 | } 7 | 8 | export interface DownloadProgress { 9 | percentage: number; 10 | bytesReceived: number; 11 | totalBytes: number; 12 | } 13 | 14 | export interface InstallationStatus { 15 | state: 16 | | "no api key" 17 | | "not installed" 18 | | "installed" 19 | | "installing" 20 | | "outdated" 21 | | "uninstalling" 22 | | "error"; 23 | error?: string; 24 | dir?: string; 25 | path?: string; 26 | versions: { 27 | plugin?: string; 28 | server?: string; 29 | }; 30 | } 31 | 32 | export interface InstallPathInfo { 33 | /** The install directory path with all symlinks resolved */ 34 | dir: string; 35 | /** The install filepath with all symlinks resolved */ 36 | path: string; 37 | /** The platform-specific filename */ 38 | name: string; 39 | /** The symlinked install path, if symlinks were found */ 40 | symlinked?: string; 41 | } 42 | 43 | // Augment Obsidian's App type to include plugins 44 | declare module "obsidian" { 45 | interface App { 46 | plugins: { 47 | plugins: { 48 | ["obsidian-local-rest-api"]?: { 49 | settings?: { 50 | apiKey?: string; 51 | }; 52 | }; 53 | ["smart-connections"]?: Plugin; 54 | ["templater-obsidian"]?: { 55 | templater?: Templater.ITemplater; 56 | }; 57 | }; 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/utils/getFileSystemAdapter.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, FileSystemAdapter } from "obsidian"; 2 | 3 | /** 4 | * Gets the file system adapter for the given plugin. 5 | * 6 | * @param plugin - The plugin to get the file system adapter for. 7 | * @returns The file system adapter, or `undefined` if not found. 8 | */ 9 | export function getFileSystemAdapter( 10 | plugin: Plugin, 11 | ): FileSystemAdapter | { error: string } { 12 | const adapter = plugin.app.vault.adapter; 13 | if (adapter instanceof FileSystemAdapter) { 14 | return adapter; 15 | } 16 | return { error: "Unsupported platform" }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/utils/openFolder.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "$/shared/logger"; 2 | import { exec } from "child_process"; 3 | import { Notice, Platform } from "obsidian"; 4 | 5 | /** 6 | * Opens a folder in the system's default file explorer 7 | */ 8 | export function openFolder(folderPath: string): void { 9 | const command = Platform.isWin 10 | ? `start "" "${folderPath}"` 11 | : Platform.isMacOS 12 | ? `open "${folderPath}"` 13 | : `xdg-open "${folderPath}"`; 14 | 15 | exec(command, (error: Error | null) => { 16 | if (error) { 17 | const message = `Failed to open folder: ${error.message}`; 18 | logger.error(message, { folderPath, error }); 19 | new Notice(message); 20 | } 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/main.ts: -------------------------------------------------------------------------------- 1 | import { type } from "arktype"; 2 | import type { Request, Response } from "express"; 3 | import { Notice, Plugin, TFile } from "obsidian"; 4 | import { shake } from "radash"; 5 | import { lastValueFrom } from "rxjs"; 6 | import { 7 | jsonSearchRequest, 8 | LocalRestAPI, 9 | searchParameters, 10 | Templater, 11 | type PromptArgAccessor, 12 | type SearchResponse, 13 | } from "shared"; 14 | import { setup as setupCore } from "./features/core"; 15 | import { setup as setupMcpServerInstall } from "./features/mcp-server-install"; 16 | import { 17 | loadLocalRestAPI, 18 | loadSmartSearchAPI, 19 | loadTemplaterAPI, 20 | type Dependencies, 21 | } from "./shared"; 22 | import { logger } from "./shared/logger"; 23 | 24 | export default class McpToolsPlugin extends Plugin { 25 | private localRestApi: Dependencies["obsidian-local-rest-api"] = { 26 | id: "obsidian-local-rest-api", 27 | name: "Local REST API", 28 | required: true, 29 | installed: false, 30 | }; 31 | 32 | async getLocalRestApiKey(): Promise { 33 | // The API key is stored in the plugin's settings 34 | return this.localRestApi.plugin?.settings?.apiKey; 35 | } 36 | 37 | async onload() { 38 | // Initialize features in order 39 | await setupCore(this); 40 | await setupMcpServerInstall(this); 41 | 42 | // Check for required dependencies 43 | lastValueFrom(loadLocalRestAPI(this)).then((localRestApi) => { 44 | this.localRestApi = localRestApi; 45 | 46 | if (!this.localRestApi.api) { 47 | new Notice( 48 | `${this.manifest.name}: Local REST API plugin is required but not found. Please install it from the community plugins and restart Obsidian.`, 49 | 0, 50 | ); 51 | return; 52 | } 53 | 54 | // Register endpoints 55 | this.localRestApi.api 56 | .addRoute("/search/smart") 57 | .post(this.handleSearchRequest.bind(this)); 58 | 59 | this.localRestApi.api 60 | .addRoute("/templates/execute") 61 | .post(this.handleTemplateExecution.bind(this)); 62 | 63 | logger.info("MCP Tools Plugin loaded"); 64 | }); 65 | } 66 | 67 | private async handleTemplateExecution(req: Request, res: Response) { 68 | try { 69 | const { api: templater } = await lastValueFrom(loadTemplaterAPI(this)); 70 | if (!templater) { 71 | new Notice( 72 | `${this.manifest.name}: Templater plugin is not available. Please install it from the community plugins.`, 73 | 0, 74 | ); 75 | logger.error("Templater plugin is not available"); 76 | res.status(503).json({ 77 | error: "Templater plugin is not available", 78 | }); 79 | return; 80 | } 81 | 82 | // Validate request body 83 | const params = LocalRestAPI.ApiTemplateExecutionParams(req.body); 84 | 85 | if (params instanceof type.errors) { 86 | const response = { 87 | error: "Invalid request body", 88 | body: req.body, 89 | summary: params.summary, 90 | }; 91 | logger.debug("Invalid request body", response); 92 | res.status(400).json(response); 93 | return; 94 | } 95 | 96 | // Get prompt content from vault 97 | const templateFile = this.app.vault.getAbstractFileByPath(params.name); 98 | if (!(templateFile instanceof TFile)) { 99 | logger.debug("Template file not found", { 100 | params, 101 | templateFile, 102 | }); 103 | res.status(404).json({ 104 | error: `File not found: ${params.name}`, 105 | }); 106 | return; 107 | } 108 | 109 | const config = templater.create_running_config( 110 | templateFile, 111 | templateFile, 112 | Templater.RunMode.CreateNewFromTemplate, 113 | ); 114 | 115 | const prompt: PromptArgAccessor = (argName: string) => { 116 | return params.arguments[argName] ?? ""; 117 | }; 118 | 119 | const oldGenerateObject = 120 | templater.functions_generator.generate_object.bind( 121 | templater.functions_generator, 122 | ); 123 | 124 | // Override generate_object to inject arg into user functions 125 | templater.functions_generator.generate_object = async function ( 126 | config, 127 | functions_mode, 128 | ) { 129 | const functions = await oldGenerateObject(config, functions_mode); 130 | Object.assign(functions, { mcpTools: { prompt } }); 131 | return functions; 132 | }; 133 | 134 | // Process template with variables 135 | const processedContent = await templater.read_and_parse_template(config); 136 | 137 | // Restore original functions generator 138 | templater.functions_generator.generate_object = oldGenerateObject; 139 | 140 | // Create new file if requested 141 | if (params.createFile && params.targetPath) { 142 | await this.app.vault.create(params.targetPath, processedContent); 143 | res.json({ 144 | message: "Prompt executed and file created successfully", 145 | content: processedContent, 146 | }); 147 | return; 148 | } 149 | 150 | res.json({ 151 | message: "Prompt executed without creating a file", 152 | content: processedContent, 153 | }); 154 | } catch (error) { 155 | logger.error("Prompt execution error:", { 156 | error: error instanceof Error ? error.message : error, 157 | body: req.body, 158 | }); 159 | res.status(503).json({ 160 | error: "An error occurred while processing the prompt", 161 | }); 162 | return; 163 | } 164 | } 165 | 166 | private async handleSearchRequest(req: Request, res: Response) { 167 | try { 168 | const dep = await lastValueFrom(loadSmartSearchAPI(this)); 169 | const smartSearch = dep.api; 170 | if (!smartSearch) { 171 | new Notice( 172 | "Smart Search REST API Plugin: smart-connections plugin is required but not found. Please install it from the community plugins.", 173 | 0, 174 | ); 175 | res.status(503).json({ 176 | error: "Smart Connections plugin is not available", 177 | }); 178 | return; 179 | } 180 | 181 | // Validate request body 182 | const requestBody = jsonSearchRequest 183 | .pipe(({ query, filter = {} }) => ({ 184 | query, 185 | filter: shake({ 186 | key_starts_with_any: filter.folders, 187 | exclude_key_starts_with_any: filter.excludeFolders, 188 | limit: filter.limit, 189 | }), 190 | })) 191 | .to(searchParameters)(req.body); 192 | if (requestBody instanceof type.errors) { 193 | res.status(400).json({ 194 | error: "Invalid request body", 195 | summary: requestBody.summary, 196 | }); 197 | return; 198 | } 199 | 200 | // Perform search 201 | const results = await smartSearch.search( 202 | requestBody.query, 203 | requestBody.filter, 204 | ); 205 | 206 | // Format response 207 | const response: SearchResponse = { 208 | results: await Promise.all( 209 | results.map(async (result) => ({ 210 | path: result.item.path, 211 | text: await result.item.read(), 212 | score: result.score, 213 | breadcrumbs: result.item.breadcrumbs, 214 | })), 215 | ), 216 | }; 217 | 218 | res.json(response); 219 | return; 220 | } catch (error) { 221 | logger.error("Smart Search API error:", { error, body: req.body }); 222 | res.status(503).json({ 223 | error: "An error occurred while processing the search request", 224 | }); 225 | return; 226 | } 227 | } 228 | 229 | onunload() { 230 | this.localRestApi.api?.unregister(); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/shared/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "obsidian"; 2 | import { getAPI, LocalRestApiPublicApi } from "obsidian-local-rest-api"; 3 | import { 4 | distinct, 5 | interval, 6 | map, 7 | merge, 8 | scan, 9 | startWith, 10 | takeUntil, 11 | takeWhile, 12 | timer, 13 | } from "rxjs"; 14 | import type { SmartConnections, Templater } from "shared"; 15 | import type McpToolsPlugin from "src/main"; 16 | 17 | export interface Dependency { 18 | id: keyof Dependencies; 19 | name: string; 20 | required: boolean; 21 | installed: boolean; 22 | url?: string; 23 | api?: API; 24 | plugin?: App["plugins"]["plugins"][ID]; 25 | } 26 | 27 | export interface Dependencies { 28 | "obsidian-local-rest-api": Dependency< 29 | "obsidian-local-rest-api", 30 | LocalRestApiPublicApi 31 | >; 32 | "smart-connections": Dependency< 33 | "smart-connections", 34 | SmartConnections.SmartSearch 35 | >; 36 | "templater-obsidian": Dependency<"templater-obsidian", Templater.ITemplater>; 37 | } 38 | 39 | // The Smart Connections plugin exposes a global variable `window.SmartSearch` but it's not guaranteed to be available. 40 | declare const window: { 41 | SmartSearch?: SmartConnections.SmartSearch; 42 | } & Window; 43 | 44 | export const loadSmartSearchAPI = (plugin: McpToolsPlugin) => 45 | interval(200).pipe( 46 | takeUntil(timer(5000)), 47 | map((): Dependencies["smart-connections"] => { 48 | const api = window.SmartSearch; 49 | return { 50 | id: "smart-connections", 51 | name: "Smart Connections", 52 | required: false, 53 | installed: !!api, 54 | api, 55 | plugin: plugin.app.plugins.plugins["smart-connections"], 56 | }; 57 | }), 58 | takeWhile((dependency) => !dependency.installed, true), 59 | distinct(({ installed }) => installed), 60 | ); 61 | 62 | export const loadLocalRestAPI = (plugin: McpToolsPlugin) => 63 | interval(200).pipe( 64 | takeUntil(timer(5000)), 65 | map((): Dependencies["obsidian-local-rest-api"] => { 66 | const api = getAPI(plugin.app, plugin.manifest); 67 | return { 68 | id: "obsidian-local-rest-api", 69 | name: "Local REST API", 70 | required: true, 71 | installed: !!api, 72 | api, 73 | plugin: plugin.app.plugins.plugins["obsidian-local-rest-api"], 74 | }; 75 | }), 76 | takeWhile((dependency) => !dependency.installed, true), 77 | distinct(({ installed }) => installed), 78 | ); 79 | 80 | export const loadTemplaterAPI = (plugin: McpToolsPlugin) => 81 | interval(200).pipe( 82 | takeUntil(timer(5000)), 83 | map((): Dependencies["templater-obsidian"] => { 84 | const api = plugin.app.plugins.plugins["templater-obsidian"]?.templater; 85 | return { 86 | id: "templater-obsidian", 87 | name: "Templater", 88 | required: false, 89 | installed: !!api, 90 | api, 91 | plugin: plugin.app.plugins.plugins["templater-obsidian"], 92 | }; 93 | }), 94 | takeWhile((dependency) => !dependency.installed, true), 95 | distinct(({ installed }) => installed), 96 | ); 97 | 98 | export const loadDependencies = (plugin: McpToolsPlugin) => { 99 | const dependencies: Dependencies = { 100 | "obsidian-local-rest-api": { 101 | id: "obsidian-local-rest-api", 102 | name: "Local REST API", 103 | required: true, 104 | installed: false, 105 | url: "https://github.com/coddingtonbear/obsidian-local-rest-api", 106 | }, 107 | "smart-connections": { 108 | id: "smart-connections", 109 | name: "Smart Connections", 110 | required: false, 111 | installed: false, 112 | url: "https://smartconnections.app/", 113 | }, 114 | "templater-obsidian": { 115 | id: "templater-obsidian", 116 | name: "Templater", 117 | required: false, 118 | installed: false, 119 | url: "https://silentvoid13.github.io/Templater/", 120 | }, 121 | }; 122 | return merge( 123 | loadLocalRestAPI(plugin), 124 | loadTemplaterAPI(plugin), 125 | loadSmartSearchAPI(plugin), 126 | ).pipe( 127 | scan((acc, dependency) => { 128 | // @ts-expect-error Dynamic key assignment 129 | acc[dependency.id] = { 130 | ...dependencies[dependency.id], 131 | ...dependency, 132 | }; 133 | return acc; 134 | }, dependencies), 135 | startWith(dependencies), 136 | ); 137 | }; 138 | 139 | export const loadDependenciesArray = (plugin: McpToolsPlugin) => 140 | loadDependencies(plugin).pipe( 141 | map((deps) => Object.values(deps) as Dependencies[keyof Dependencies][]), 142 | ); 143 | 144 | export * from "./logger"; 145 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/shared/logger.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createLogger, 3 | loggerConfigMorph, 4 | type InputLoggerConfig, 5 | } from "shared"; 6 | 7 | const isProd = process.env.NODE_ENV === "production"; 8 | 9 | export const LOGGER_CONFIG: InputLoggerConfig = { 10 | appName: "Claude", 11 | filename: "obsidian-plugin-mcp-tools.log", 12 | level: "DEBUG", 13 | }; 14 | 15 | export const { filename: FULL_LOGGER_FILENAME } = 16 | loggerConfigMorph.assert(LOGGER_CONFIG); 17 | 18 | /** 19 | * In production, we use the console. During development, the logger writes logs to a file in the same folder as the server log file. 20 | */ 21 | export const logger = isProd ? console : createLogger(LOGGER_CONFIG); 22 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/types.ts: -------------------------------------------------------------------------------- 1 | declare module "obsidian" { 2 | interface McpToolsPluginSettings { 3 | version?: string; 4 | } 5 | 6 | interface Plugin { 7 | loadData(): Promise; 8 | saveData(data: McpToolsPluginSettings): Promise; 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /packages/obsidian-plugin/svelte.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { sveltePreprocess } from 'svelte-preprocess'; 3 | 4 | const config = { 5 | preprocess: sveltePreprocess() 6 | } 7 | 8 | export default config; -------------------------------------------------------------------------------- /packages/obsidian-plugin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES2018", 8 | "allowJs": true, 9 | "noEmit": true, 10 | "noImplicitAny": true, 11 | "moduleResolution": "bundler", 12 | "importHelpers": true, 13 | "isolatedModules": true, 14 | "strict": true, 15 | "skipLibCheck": true, 16 | "lib": ["DOM", "ES5", "ES6", "ES7"], 17 | "useDefineForClassFields": true, 18 | "verbatimModuleSyntax": true, 19 | "paths": { 20 | "$/*": ["./src/*"] 21 | } 22 | }, 23 | "include": ["src/*.ts", "bun.config.ts"], 24 | "exclude": ["node_modules", "playground"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/shared/.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /packages/shared/README.md: -------------------------------------------------------------------------------- 1 | # shared 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | bun run index.ts 13 | ``` 14 | 15 | This project was created using `bun init` in bun v1.1.39. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shared", 3 | "type": "module", 4 | "exports": { 5 | ".": "./src/index.ts" 6 | }, 7 | "module": "src/index.ts", 8 | "scripts": { 9 | "check": "tsc --noEmit" 10 | }, 11 | "dependencies": { 12 | "arktype": "^2.0.0-rc.30" 13 | }, 14 | "devDependencies": { 15 | "@types/bun": "latest" 16 | }, 17 | "peerDependencies": { 18 | "typescript": "^5.0.0" 19 | } 20 | } -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./logger"; 2 | export * from "./types"; 3 | -------------------------------------------------------------------------------- /packages/shared/src/logger.ts: -------------------------------------------------------------------------------- 1 | import { type } from "arktype"; 2 | import { existsSync, mkdirSync } from "fs"; 3 | import { appendFile } from "fs/promises"; 4 | import { homedir, platform } from "os"; 5 | import { dirname, resolve } from "path"; 6 | 7 | /** 8 | * Determines the appropriate log directory path based on the current operating system. 9 | * @param appName - The name of the application to use in the log directory path. 10 | * @returns The full path to the log directory for the current operating system. 11 | * @throws {Error} If the current operating system is not supported. 12 | */ 13 | export function getLogFilePath(appName: string, fileName: string) { 14 | switch (platform()) { 15 | case "darwin": // macOS 16 | return resolve(homedir(), "Library", "Logs", appName, fileName); 17 | 18 | case "win32": // Windows 19 | return resolve(homedir(), "AppData", "Local", "Logs", appName, fileName); 20 | 21 | case "linux": // Linux 22 | return resolve(homedir(), ".local", "share", "logs", appName, fileName); 23 | 24 | default: 25 | throw new Error("Unsupported operating system"); 26 | } 27 | } 28 | 29 | const ensureDirSync = (dirPath: string) => { 30 | if (!existsSync(dirPath)) { 31 | mkdirSync(dirPath, { recursive: true }); 32 | } 33 | }; 34 | 35 | const logLevels = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"] as const; 36 | export const logLevelSchema = type.enumerated(...logLevels); 37 | export type LogLevel = typeof logLevelSchema.infer; 38 | 39 | const formatMessage = ( 40 | level: LogLevel, 41 | message: unknown, 42 | meta: Record, 43 | ) => { 44 | const timestamp = new Date().toISOString(); 45 | const metaStr = Object.keys(meta).length 46 | ? `\n${JSON.stringify(meta, null, 2)}` 47 | : ""; 48 | return `${timestamp} [${level.padEnd(5)}] ${JSON.stringify( 49 | message, 50 | )}${metaStr}\n`; 51 | }; 52 | 53 | const loggerConfigSchema = type({ 54 | appName: "string", 55 | filename: "string", 56 | level: logLevelSchema, 57 | }); 58 | export const loggerConfigMorph = loggerConfigSchema.pipe((config) => { 59 | const filename = getLogFilePath(config.appName, config.filename); 60 | const levels = logLevels.slice(logLevels.indexOf(config.level)); 61 | return { ...config, levels, filename }; 62 | }); 63 | 64 | export type InputLoggerConfig = typeof loggerConfigSchema.infer; 65 | export type FullLoggerConfig = typeof loggerConfigMorph.infer; 66 | 67 | /** 68 | * Creates a logger instance with configurable options for logging to a file. 69 | * The logger provides methods for logging messages at different log levels (DEBUG, INFO, WARN, ERROR, FATAL). 70 | * @param config - An object with configuration options for the logger. 71 | * @param config.filepath - The file path to use for logging to a file. 72 | * @param config.level - The minimum log level to log messages. 73 | * @returns An object with logging methods (debug, info, warn, error, fatal). 74 | */ 75 | export function createLogger(inputConfig: InputLoggerConfig) { 76 | let config: FullLoggerConfig = loggerConfigMorph.assert(inputConfig); 77 | let logMeta: Record = {}; 78 | 79 | const queue: Promise[] = []; 80 | const log = (level: LogLevel, message: unknown, meta?: typeof logMeta) => { 81 | if (!config.levels.includes(level)) return; 82 | ensureDirSync(dirname(getLogFilePath(config.appName, config.filename))); 83 | queue.push( 84 | appendFile( 85 | config.filename, 86 | formatMessage(level, message, { ...logMeta, ...(meta ?? {}) }), 87 | ), 88 | ); 89 | }; 90 | 91 | const debug = (message: unknown, meta?: typeof logMeta) => 92 | log("DEBUG", message, meta); 93 | const info = (message: unknown, meta?: typeof logMeta) => 94 | log("INFO", message, meta); 95 | const warn = (message: unknown, meta?: typeof logMeta) => 96 | log("WARN", message, meta); 97 | const error = (message: unknown, meta?: typeof logMeta) => 98 | log("ERROR", message, meta); 99 | const fatal = (message: unknown, meta?: typeof logMeta) => 100 | log("FATAL", message, meta); 101 | 102 | const logger = { 103 | debug, 104 | info, 105 | warn, 106 | error, 107 | fatal, 108 | flush() { 109 | return Promise.all(queue); 110 | }, 111 | get config(): FullLoggerConfig { 112 | return { ...config }; 113 | }, 114 | /** 115 | * Updates the configuration of the logger instance. 116 | * @param newConfig - A partial configuration object to merge with the existing configuration. 117 | * This method updates the log levels based on the new configuration level, and then merges the new configuration with the existing configuration. 118 | */ 119 | set config(newConfig: Partial) { 120 | config = loggerConfigMorph.assert({ ...config, ...newConfig }); 121 | logger.debug("Updated logger configuration", { config }); 122 | }, 123 | set meta(newMeta: Record) { 124 | logMeta = newMeta; 125 | }, 126 | }; 127 | 128 | return logger; 129 | } 130 | -------------------------------------------------------------------------------- /packages/shared/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * as LocalRestAPI from "./plugin-local-rest-api"; 2 | export * as SmartConnections from "./plugin-smart-connections"; 3 | export * as Templater from "./plugin-templater"; 4 | export * from "./prompts"; 5 | export * from "./smart-search"; 6 | -------------------------------------------------------------------------------- /packages/shared/src/types/plugin-local-rest-api.ts: -------------------------------------------------------------------------------- 1 | import { type } from "arktype"; 2 | 3 | /** 4 | * Error response from the API 5 | * Content-Type: application/json 6 | * Used in various error responses across endpoints 7 | * @property errorCode - A 5-digit error code uniquely identifying this particular type of error 8 | * @property message - Message describing the error 9 | */ 10 | export const ApiError = type({ 11 | errorCode: "number", 12 | message: "string", 13 | }); 14 | 15 | /** 16 | * JSON representation of a note including parsed tag and frontmatter data as well as filesystem metadata 17 | * Content-Type: application/vnd.olrapi.note+json 18 | * GET /vault/{filename} or GET /active/ with Accept: application/vnd.olrapi.note+json 19 | */ 20 | export const ApiNoteJson = type({ 21 | content: "string", 22 | frontmatter: "Record", 23 | path: "string", 24 | stat: { 25 | ctime: "number", 26 | mtime: "number", 27 | size: "number", 28 | }, 29 | tags: "string[]", 30 | }); 31 | 32 | /** 33 | * Defines the structure of a plugin manifest, which contains metadata about a plugin. 34 | * This type is used to represent the response from the API's root endpoint, providing 35 | * basic server details and authentication status. 36 | */ 37 | const ApiPluginManifest = type({ 38 | id: "string", 39 | name: "string", 40 | version: "string", 41 | minAppVersion: "string", 42 | description: "string", 43 | author: "string", 44 | authorUrl: "string", 45 | isDesktopOnly: "boolean", 46 | dir: "string", 47 | }); 48 | 49 | /** 50 | * Response from the root endpoint providing basic server details and authentication status 51 | * Content-Type: application/json 52 | * GET / - This is the only API request that does not require authentication 53 | */ 54 | export const ApiStatusResponse = type({ 55 | status: "string", 56 | manifest: ApiPluginManifest, 57 | versions: { 58 | obsidian: "string", 59 | self: "string", 60 | }, 61 | service: "string", 62 | authenticated: "boolean", 63 | certificateInfo: { 64 | validityDays: "number", 65 | regenerateRecommended: "boolean", 66 | }, 67 | apiExtensions: ApiPluginManifest.array(), 68 | }); 69 | 70 | /** 71 | * Response from searching vault files using advanced search 72 | * Content-Type: application/json 73 | * POST /search/ 74 | * Returns array of matching files and their results 75 | * Results are only returned for non-falsy matches 76 | */ 77 | export const ApiSearchResponse = type({ 78 | filename: "string", 79 | result: "string|number|string[]|object|boolean", 80 | }).array(); 81 | 82 | /** 83 | * Match details for simple text search results 84 | * Content-Type: application/json 85 | * Used in ApiSimpleSearchResult 86 | */ 87 | export const ApiSimpleSearchMatch = type({ 88 | match: { 89 | start: "number", 90 | end: "number", 91 | }, 92 | context: "string", 93 | }); 94 | 95 | /** 96 | * Result from searching vault files with simple text search 97 | * Content-Type: application/json 98 | * POST /search/simple/ 99 | * Returns matches with surrounding context 100 | */ 101 | export const ApiSimpleSearchResponse = type({ 102 | filename: "string", 103 | matches: ApiSimpleSearchMatch.array(), 104 | score: "number", 105 | }).array(); 106 | 107 | /** 108 | * Result entry from semantic search 109 | * Content-Type: application/json 110 | * Used in ApiSearchResponse 111 | */ 112 | export const ApiSmartSearchResult = type({ 113 | path: "string", 114 | text: "string", 115 | score: "number", 116 | breadcrumbs: "string", 117 | }); 118 | 119 | /** 120 | * Response from semantic search containing list of matching results 121 | * Content-Type: application/json 122 | * POST /search/smart/ 123 | */ 124 | export const ApiSmartSearchResponse = type({ 125 | results: ApiSmartSearchResult.array(), 126 | }); 127 | 128 | /** 129 | * Parameters for semantic search request 130 | * Content-Type: application/json 131 | * POST /search/smart/ 132 | * @property query - A search phrase for semantic search 133 | * @property filter.folders - An array of folder names to include. For example, ["Public", "Work"] 134 | * @property filter.excludeFolders - An array of folder names to exclude. For example, ["Private", "Archive"] 135 | * @property filter.limit - The maximum number of results to return 136 | */ 137 | export const ApiSearchParameters = type({ 138 | query: "string", 139 | filter: { 140 | folders: "string[]?", 141 | excludeFolders: "string[]?", 142 | limit: "number?", 143 | }, 144 | }); 145 | 146 | /** 147 | * Command information from Obsidian's command palette 148 | * Content-Type: application/json 149 | * Used in ApiCommandsResponse 150 | */ 151 | export const ApiCommand = type({ 152 | id: "string", 153 | name: "string", 154 | }); 155 | 156 | /** 157 | * Response containing list of available Obsidian commands 158 | * Content-Type: application/json 159 | * GET /commands/ 160 | */ 161 | export const ApiCommandsResponse = type({ 162 | commands: ApiCommand.array(), 163 | }); 164 | 165 | /** 166 | * Response containing list of files in a vault directory 167 | * Content-Type: application/json 168 | * GET /vault/ or GET /vault/{pathToDirectory}/ 169 | * Note that empty directories will not be returned 170 | */ 171 | export const ApiVaultDirectoryResponse = type({ 172 | files: "string[]", 173 | }); 174 | 175 | /** 176 | * Response containing vault file information 177 | * Content-Type: application/json 178 | * POST /vault/{pathToFile} 179 | * Returns array of matching files and their results 180 | * Results are only returned for non-falsy matches 181 | */ 182 | export const ApiVaultFileResponse = type({ 183 | frontmatter: { 184 | tags: "string[]", 185 | description: "string?", 186 | }, 187 | content: "string", 188 | path: "string", 189 | stat: { 190 | ctime: "number", 191 | mtime: "number", 192 | size: "number", 193 | }, 194 | tags: "string[]", 195 | }); 196 | 197 | /** 198 | * Parameters for patching a file or document in the Obsidian plugin's REST API. 199 | * This type defines the expected request body for the patch operation. 200 | * 201 | * @property operation - Specifies how to modify the content: append (add after), prepend (add before), or replace existing content 202 | * @property targetType - Identifies what to modify: a section under a heading, a referenced block, or a frontmatter field 203 | * @property target - The identifier - either heading path (e.g. 'Heading 1::Subheading 1:1'), block reference ID, or frontmatter field name 204 | * @property targetDelimiter - The separator used in heading paths to indicate nesting (default '::') 205 | * @property trimTargetWhitespace - Whether to remove whitespace from target identifier before matching (default: false) 206 | * @property content - The actual content to insert, append, or use as replacement 207 | * @property contentType - Format of the content - use application/json for structured data like table rows or frontmatter values 208 | */ 209 | export const ApiPatchParameters = type({ 210 | operation: type("'append' | 'prepend' | 'replace'").describe( 211 | "Specifies how to modify the content: append (add after), prepend (add before), or replace existing content", 212 | ), 213 | targetType: type("'heading' | 'block' | 'frontmatter'").describe( 214 | "Identifies what to modify: a section under a heading, a referenced block, or a frontmatter field", 215 | ), 216 | target: type("string").describe( 217 | "The identifier - either heading path (e.g. 'Heading 1::Subheading 1:1'), block reference ID, or frontmatter field name", 218 | ), 219 | "targetDelimiter?": type("string").describe( 220 | "The separator used in heading paths to indicate nesting (default '::')", 221 | ), 222 | "trimTargetWhitespace?": type("boolean").describe( 223 | "Whether to remove whitespace from target identifier before matching (default: false)", 224 | ), 225 | content: type("string").describe( 226 | "The actual content to insert, append, or use as replacement", 227 | ), 228 | "contentType?": type("'text/markdown' | 'application/json'").describe( 229 | "Format of the content - use application/json for structured data like table rows or frontmatter values", 230 | ), 231 | }); 232 | 233 | /** 234 | * Represents a response containing markdown content 235 | */ 236 | export const ApiContentResponse = type("string").describe("Content"); 237 | 238 | /** 239 | * Empty response for successful operations that don't return content 240 | * Content-Type: none (204 No Content) 241 | * Used by: 242 | * - PUT /vault/{filename} 243 | * - PUT /active/ 244 | * - PUT /periodic/{period}/ 245 | * - POST /commands/{commandId}/ 246 | * - DELETE endpoints 247 | * Returns 204 No Content 248 | */ 249 | export const ApiNoContentResponse = type("unknown").describe("No Content"); 250 | 251 | /** 252 | * Parameters for executing a template 253 | * Content-Type: application/json 254 | * POST /templates/execute/ 255 | * @property name - The name of the template to execute 256 | * @property arguments - A key-value object of arguments to pass to the template 257 | * @property createFile - Whether to create a new file from the template 258 | * @property targetPath - The path to save the file; required if createFile is true 259 | */ 260 | export const ApiTemplateExecutionParams = type({ 261 | name: type("string").describe("The full vault path to the template file"), 262 | arguments: "Record", 263 | "createFile?": type("boolean").describe( 264 | "Whether to create a new file from the template", 265 | ), 266 | "targetPath?": type("string").describe( 267 | "Path to save the file; required if createFile is true", 268 | ), 269 | }); 270 | 271 | /** 272 | * Response from executing a template 273 | * Content-Type: application/json 274 | * POST /templates/execute/ 275 | * @property message - A message describing the result of the template execution 276 | */ 277 | export const ApiTemplateExecutionResponse = type({ 278 | message: "string", 279 | content: "string", 280 | }); 281 | 282 | // Export types for TypeScript usage 283 | export type ApiErrorType = typeof ApiError.infer; 284 | export type ApiNoteJsonType = typeof ApiNoteJson.infer; 285 | export type ApiStatusResponseType = typeof ApiStatusResponse.infer; 286 | export type ApiSearchResponseType = typeof ApiSearchResponse.infer; 287 | export type ApiSimpleSearchResponseType = typeof ApiSimpleSearchResponse.infer; 288 | export type ApiSmartSearchResultType = typeof ApiSmartSearchResult.infer; 289 | export type ApiSmartSearchResponseType = typeof ApiSmartSearchResponse.infer; 290 | export type ApiCommandType = typeof ApiCommand.infer; 291 | export type ApiCommandsResponseType = typeof ApiCommandsResponse.infer; 292 | export type ApiVaultDirectoryResponseType = 293 | typeof ApiVaultDirectoryResponse.infer; 294 | export type ApiVaultFileResponseType = typeof ApiVaultFileResponse.infer; 295 | export type ApiSearchParametersType = typeof ApiSearchParameters.infer; 296 | export type ApiNoContentResponseType = typeof ApiNoContentResponse.infer; 297 | export type ApiTemplateExecutionParamsType = 298 | typeof ApiTemplateExecutionParams.infer; 299 | export type ApiTemplateExecutionResponseType = 300 | typeof ApiTemplateExecutionResponse.infer; 301 | 302 | // Additional API response types can be added here 303 | export const MIME_TYPE_OLRAPI_NOTE_JSON = "application/vnd.olrapi.note+json"; 304 | -------------------------------------------------------------------------------- /packages/shared/src/types/plugin-smart-connections.ts: -------------------------------------------------------------------------------- 1 | import { type } from "arktype"; 2 | 3 | /** 4 | * SmartSearch filter options 5 | */ 6 | export const SmartSearchFilter = type({ 7 | "exclude_key?": type("string").describe("A single key to exclude."), 8 | "exclude_keys?": type("string[]").describe( 9 | "An array of keys to exclude. If exclude_key is provided, it's added to this array.", 10 | ), 11 | "exclude_key_starts_with?": type("string").describe( 12 | "Exclude keys starting with this string.", 13 | ), 14 | "exclude_key_starts_with_any?": type("string[]").describe( 15 | "Exclude keys starting with any of these strings.", 16 | ), 17 | "exclude_key_includes?": type("string").describe( 18 | "Exclude keys that include this string.", 19 | ), 20 | "key_ends_with?": type("string").describe( 21 | "Include only keys ending with this string.", 22 | ), 23 | "key_starts_with?": type("string").describe( 24 | "Include only keys starting with this string.", 25 | ), 26 | "key_starts_with_any?": type("string[]").describe( 27 | "Include only keys starting with any of these strings.", 28 | ), 29 | "key_includes?": type("string").describe( 30 | "Include only keys that include this string.", 31 | ), 32 | "limit?": type("number").describe("Limit the number of search results."), 33 | }); 34 | 35 | export type SearchFilter = typeof SmartSearchFilter.infer; 36 | 37 | /** 38 | * Interface for the SmartBlock class which represents a single block within a SmartSource 39 | */ 40 | interface SmartBlock { 41 | // Core properties 42 | key: string; 43 | path: string; 44 | data: { 45 | text: string | null; 46 | length: number; 47 | last_read: { 48 | hash: string | null; 49 | at: number; 50 | }; 51 | embeddings: Record; 52 | lines?: [number, number]; // Start and end line numbers 53 | }; 54 | 55 | // Vector-related properties 56 | vec: number[] | undefined; 57 | tokens: number | undefined; 58 | 59 | // State flags 60 | excluded: boolean; 61 | is_block: boolean; 62 | is_gone: boolean; 63 | 64 | // Content properties 65 | breadcrumbs: string; 66 | file_path: string; 67 | file_type: string; 68 | folder: string; 69 | link: string; 70 | name: string; 71 | size: number; 72 | 73 | // Methods 74 | read(): Promise; 75 | nearest(filter?: SearchFilter): Promise; 76 | } 77 | 78 | /** 79 | * Interface for a single search result 80 | */ 81 | interface SearchResult { 82 | item: SmartBlock; 83 | score: number; 84 | } 85 | 86 | /** 87 | * Interface for the SmartSearch class which provides the main search functionality 88 | */ 89 | export interface SmartSearch { 90 | /** 91 | * Searches for relevant blocks based on the provided search text 92 | * @param search_text - The text to search for 93 | * @param filter - Optional filter parameters to refine the search 94 | * @returns A promise that resolves to an array of search results, sorted by relevance score 95 | */ 96 | search(search_text: string, filter?: SearchFilter): Promise; 97 | } 98 | -------------------------------------------------------------------------------- /packages/shared/src/types/plugin-templater.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | type MarkdownPostProcessorContext, 4 | TAbstractFile, 5 | TFile, 6 | TFolder, 7 | } from "obsidian"; 8 | 9 | export enum RunMode { 10 | CreateNewFromTemplate, 11 | AppendActiveFile, 12 | OverwriteFile, 13 | OverwriteActiveFile, 14 | DynamicProcessor, 15 | StartupTemplate, 16 | } 17 | 18 | export enum FunctionsMode { 19 | INTERNAL, 20 | USER_INTERNAL, 21 | } 22 | 23 | export type RunningConfig = { 24 | template_file: TFile | undefined; 25 | target_file: TFile; 26 | run_mode: RunMode; 27 | active_file?: TFile | null; 28 | }; 29 | 30 | interface TemplaterFunctions { 31 | app: App; 32 | config: RunningConfig; 33 | date: { 34 | /** 35 | * @param format "YYYY-MM-DD" 36 | * @param offset 37 | * @param reference 38 | * @param reference_format 39 | */ 40 | now( 41 | format: string, 42 | offset?: number | string, 43 | reference?: string, 44 | reference_format?: string, 45 | ): string; 46 | /** 47 | * @param format "YYYY-MM-DD" 48 | */ 49 | tomorrow(format: string): string; 50 | /** 51 | * @param format "YYYY-MM-DD" 52 | * @param weekday 53 | * @param reference 54 | * @param reference_format 55 | */ 56 | weekday( 57 | format: string, 58 | weekday: number, 59 | reference?: string, 60 | reference_format?: string, 61 | ): string; 62 | /** 63 | * @param format "YYYY-MM-DD" 64 | */ 65 | yesterday(format?: string): string; 66 | }; 67 | file: { 68 | content: string; 69 | /** 70 | * @param template TFile or string 71 | * @param filename 72 | * @param open_new Default: false 73 | * @param folder TFolder or string 74 | */ 75 | create_new( 76 | template: TFile | string, 77 | filename?: string, 78 | open_new?: boolean, 79 | folder?: TFolder | string, 80 | ): Promise; 81 | /** 82 | * @param format Default: "YYYY-MM-DD HH:mm" 83 | */ 84 | creation_date(format?: string): string; 85 | /** 86 | * @param order 87 | */ 88 | cursor(order?: number): void; 89 | cursor_append(content: string): void; 90 | exists(filepath: string): boolean; 91 | find_tfile(filename: string): TFile; 92 | /** 93 | * @param absolute Default: false 94 | */ 95 | folder(absolute?: boolean): string; 96 | include(include_link: string | TFile): string; 97 | /** 98 | * @param format Default: "YYYY-MM-DD HH:mm" 99 | */ 100 | last_modified_date(format?: string): string; 101 | move(new_path: string, file_to_move?: TFile): Promise; 102 | /** 103 | * @param relative Default: false 104 | */ 105 | path(relative?: boolean): string; 106 | rename(new_title: string): Promise; 107 | selection(): string; 108 | tags: string[]; 109 | title: string; 110 | }; 111 | frontmatter: Record; 112 | hooks: { 113 | on_all_templates_executed(cb: () => void): void; 114 | }; 115 | system: { 116 | /** 117 | * Retrieves the clipboard's content. 118 | */ 119 | clipboard(): Promise; 120 | 121 | /** 122 | * @param prompt_text 123 | * @param default_value 124 | * @param throw_on_cancel Default: false 125 | * @param multiline Default: false 126 | */ 127 | prompt( 128 | prompt_text?: string, 129 | default_value?: string, 130 | throw_on_cancel?: boolean, 131 | multiline?: boolean, 132 | ): Promise; 133 | 134 | /** 135 | * @param text_items String array or function mapping item to string 136 | * @param items Array of generic type T 137 | * @param throw_on_cancel Default: false 138 | * @param placeholder Default: "" 139 | * @param limit Default: undefined 140 | */ 141 | suggester( 142 | text_items: string[] | ((item: T) => string), 143 | items: T[], 144 | throw_on_cancel?: boolean, 145 | placeholder?: string, 146 | limit?: number, 147 | ): Promise; 148 | }; 149 | web: { 150 | /** 151 | * Retrieves daily quote from quotes database 152 | */ 153 | daily_quote(): Promise; 154 | 155 | /** 156 | * @param size Image size specification 157 | * @param query Search query 158 | * @param include_size Whether to include size in URL 159 | */ 160 | random_picture( 161 | size: string, 162 | query: string, 163 | include_size: boolean, 164 | ): Promise; 165 | 166 | /** 167 | * @param url Full URL to request 168 | * @param path Optional path parameter 169 | */ 170 | request(url: string, path?: string): Promise; 171 | }; 172 | user: Record; 173 | } 174 | 175 | export interface ITemplater { 176 | setup(): Promise; 177 | /** Generate the config required to parse a template */ 178 | create_running_config( 179 | template_file: TFile | undefined, 180 | target_file: TFile, 181 | run_mode: RunMode, 182 | ): RunningConfig; 183 | /** I don't think this writes the file, but the config requires the file name */ 184 | read_and_parse_template(config: RunningConfig): Promise; 185 | /** I don't think this writes the file, but the config requires the file name */ 186 | parse_template( 187 | config: RunningConfig, 188 | template_content: string, 189 | ): Promise; 190 | create_new_note_from_template( 191 | template: TFile | string, 192 | folder?: TFolder | string, 193 | filename?: string, 194 | open_new_note?: boolean, 195 | ): Promise; 196 | append_template_to_active_file(template_file: TFile): Promise; 197 | write_template_to_file(template_file: TFile, file: TFile): Promise; 198 | overwrite_active_file_commands(): void; 199 | overwrite_file_commands(file: TFile, active_file?: boolean): Promise; 200 | process_dynamic_templates( 201 | el: HTMLElement, 202 | ctx: MarkdownPostProcessorContext, 203 | ): Promise; 204 | get_new_file_template_for_folder(folder: TFolder): string | undefined; 205 | get_new_file_template_for_file(file: TFile): string | undefined; 206 | execute_startup_scripts(): Promise; 207 | 208 | on_file_creation( 209 | templater: ITemplater, 210 | app: App, 211 | file: TAbstractFile, 212 | ): Promise; 213 | 214 | current_functions_object: TemplaterFunctions; 215 | functions_generator: { 216 | generate_object( 217 | config: RunningConfig, 218 | functions_mode?: FunctionsMode, 219 | ): Promise; 220 | }; 221 | } 222 | -------------------------------------------------------------------------------- /packages/shared/src/types/prompts.ts: -------------------------------------------------------------------------------- 1 | import { Type, type } from "arktype"; 2 | 3 | /** 4 | * A Templater user function that retrieves the value of the specified argument from the `params.arguments` object. In this implementation, all arguments are optional. 5 | * 6 | * @param argName - The name of the argument to retrieve. 7 | * @param argDescription - A description of the argument. 8 | * @returns The value of the specified argument. 9 | * 10 | * @example 11 | * ```markdown 12 | * <% tp.mcpTools.prompt("argName", "Argument description") %> 13 | * ``` 14 | */ 15 | export interface PromptArgAccessor { 16 | (argName: string, argDescription?: string): string; 17 | } 18 | 19 | export const PromptParameterSchema = type({ 20 | name: "string", 21 | "description?": "string", 22 | "required?": "boolean", 23 | }); 24 | export type PromptParameter = typeof PromptParameterSchema.infer; 25 | 26 | export const PromptMetadataSchema = type({ 27 | name: "string", 28 | "description?": type("string").describe("Description of the prompt"), 29 | "arguments?": PromptParameterSchema.array(), 30 | }); 31 | export type PromptMetadata = typeof PromptMetadataSchema.infer; 32 | 33 | export const PromptTemplateTag = type("'mcp-tools-prompt'"); 34 | export const PromptFrontmatterSchema = type({ 35 | tags: type("string[]").narrow((arr) => arr.some(PromptTemplateTag.allows)), 36 | "description?": type("string"), 37 | }); 38 | export type PromptFrontmatter = typeof PromptFrontmatterSchema.infer; 39 | 40 | export const PromptValidationErrorSchema = type({ 41 | type: "'MISSING_REQUIRED_ARG'|'INVALID_ARG_VALUE'", 42 | message: "string", 43 | "argumentName?": "string", 44 | }); 45 | export type PromptValidationError = typeof PromptValidationErrorSchema.infer; 46 | 47 | export const PromptExecutionResultSchema = type({ 48 | content: "string", 49 | "errors?": PromptValidationErrorSchema.array(), 50 | }); 51 | export type PromptExecutionResult = typeof PromptExecutionResultSchema.infer; 52 | 53 | export function buildTemplateArgumentsSchema( 54 | args: PromptParameter[], 55 | ): Type, {}> { 56 | return type( 57 | Object.fromEntries( 58 | args.map((arg) => [arg.name, arg.required ? "string" : "string?"]), 59 | ) as Record, 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /packages/shared/src/types/smart-search.ts: -------------------------------------------------------------------------------- 1 | import { type } from "arktype"; 2 | import { SmartConnections } from "shared"; 3 | 4 | const searchRequest = type({ 5 | query: type("string>0").describe("A search phrase for semantic search"), 6 | "filter?": { 7 | "folders?": type("string[]").describe( 8 | 'An array of folder names to include. For example, ["Public", "Work"]', 9 | ), 10 | "excludeFolders?": type("string[]").describe( 11 | 'An array of folder names to exclude. For example, ["Private", "Archive"]', 12 | ), 13 | "limit?": type("number>0").describe( 14 | "The maximum number of results to return", 15 | ), 16 | }, 17 | }); 18 | export const jsonSearchRequest = type("string.json.parse").to(searchRequest); 19 | 20 | const searchResponse = type({ 21 | results: type({ 22 | path: "string", 23 | text: "string", 24 | score: "number", 25 | breadcrumbs: "string", 26 | }).array(), 27 | }); 28 | export type SearchResponse = typeof searchResponse.infer; 29 | 30 | export const searchParameters = type({ 31 | query: "string", 32 | filter: SmartConnections.SmartSearchFilter, 33 | }); -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/test-site/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | -------------------------------------------------------------------------------- /packages/test-site/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /packages/test-site/.prettierignore: -------------------------------------------------------------------------------- 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /packages/test-site/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], 7 | "overrides": [ 8 | { 9 | "files": "*.svelte", 10 | "options": { 11 | "parser": "svelte" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/test-site/README.md: -------------------------------------------------------------------------------- 1 | # sv 2 | 3 | Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npx sv create 12 | 13 | # create a new project in my-app 14 | npx sv create my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /packages/test-site/eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from 'eslint-config-prettier'; 2 | import js from '@eslint/js'; 3 | import { includeIgnoreFile } from '@eslint/compat'; 4 | import svelte from 'eslint-plugin-svelte'; 5 | import globals from 'globals'; 6 | import { fileURLToPath } from 'node:url'; 7 | import ts from 'typescript-eslint'; 8 | const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); 9 | 10 | export default ts.config( 11 | includeIgnoreFile(gitignorePath), 12 | js.configs.recommended, 13 | ...ts.configs.recommended, 14 | ...svelte.configs['flat/recommended'], 15 | prettier, 16 | ...svelte.configs['flat/prettier'], 17 | { 18 | languageOptions: { 19 | globals: { 20 | ...globals.browser, 21 | ...globals.node 22 | } 23 | } 24 | }, 25 | { 26 | files: ['**/*.svelte'], 27 | 28 | languageOptions: { 29 | parserOptions: { 30 | parser: ts.parser 31 | } 32 | } 33 | } 34 | ); 35 | -------------------------------------------------------------------------------- /packages/test-site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-server", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 | "format": "prettier --write .", 13 | "lint": "prettier --check . && eslint ." 14 | }, 15 | "devDependencies": { 16 | "@eslint/compat": "^1.2.3", 17 | "@eslint/js": "^9.17.0", 18 | "@sveltejs/adapter-static": "^3.0.6", 19 | "@sveltejs/kit": "^2.0.0", 20 | "@sveltejs/vite-plugin-svelte": "^4.0.0", 21 | "autoprefixer": "^10.4.20", 22 | "eslint": "^9.7.0", 23 | "eslint-config-prettier": "^9.1.0", 24 | "eslint-plugin-svelte": "^2.36.0", 25 | "globals": "^15.0.0", 26 | "prettier": "^3.3.2", 27 | "prettier-plugin-svelte": "^3.2.6", 28 | "prettier-plugin-tailwindcss": "^0.6.5", 29 | "svelte": "^5.0.0", 30 | "svelte-check": "^4.0.0", 31 | "tailwindcss": "^3.4.9", 32 | "typescript": "^5.0.0", 33 | "typescript-eslint": "^8.0.0", 34 | "vite": "^5.4.11" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/test-site/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /packages/test-site/src/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | -------------------------------------------------------------------------------- /packages/test-site/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /packages/test-site/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /packages/test-site/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /packages/test-site/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | {@render children()} 7 | -------------------------------------------------------------------------------- /packages/test-site/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /packages/test-site/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 2 | Understanding Express Routes: A Complete Guide 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

Understanding Express Routes: A Complete Guide

16 | 23 |
24 | 25 |
26 |

Introduction

27 |

Express.js has become the de facto standard for building web applications with Node.js. At its core, routing is one of the most fundamental concepts you need to master.

28 | 29 |

Basic Route Structure

30 |

Express routes follow a simple pattern that combines HTTP methods with URL paths:

31 | 32 |
{`
 33 | app.get('/users', (req, res) => {
 34 |   res.send('Get all users');
 35 | });
 36 | `}
37 | 38 |

Route Parameters

39 |

Dynamic routes can be created using parameters:

40 | 41 |
{`
 42 | app.get('/users/:id', (req, res) => {
 43 |   const userId = req.params.id;
 44 |   res.send(\`Get user \${userId}\`);
 45 | });
 46 | `}
47 | 48 |

Middleware Integration

49 |

Routes can include middleware functions for additional processing:

50 | 51 |
{`
 52 | const authMiddleware = (req, res, next) => {
 53 |   // Authentication logic
 54 |   next();
 55 | };
 56 | 
 57 | app.get('/protected', authMiddleware, (req, res) => {
 58 |   res.send('Protected route');
 59 | });
 60 | `}
61 |
62 | 63 |
64 |
65 | Express.js 66 | Node.js 67 | Web Development 68 |
69 | 70 | 81 |
82 |
83 | 84 | 118 | -------------------------------------------------------------------------------- /packages/test-site/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacksteamdev/obsidian-mcp-tools/a167072f4a000a40469d99acadeb3982578d7eab/packages/test-site/static/favicon.png -------------------------------------------------------------------------------- /packages/test-site/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 14 | adapter: adapter() 15 | } 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /packages/test-site/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | export default { 4 | content: ['./src/**/*.{html,js,svelte,ts}'], 5 | 6 | theme: { 7 | extend: {} 8 | }, 9 | 10 | plugins: [] 11 | } satisfies Config; 12 | -------------------------------------------------------------------------------- /packages/test-site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /packages/test-site/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /patches/svelte@5.16.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index 2a492711ba24e6924a23c0dde76062e16c2a5248..e7044b105ef8a297413c3e7d0ffe4232f1417fa5 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -21,9 +21,9 @@ 6 | "exports": { 7 | ".": { 8 | "types": "./types/index.d.ts", 9 | - "worker": "./src/index-server.js", 10 | + "worker": "./src/index-client.js", 11 | "browser": "./src/index-client.js", 12 | - "default": "./src/index-server.js" 13 | + "default": "./src/index-client.js" 14 | }, 15 | "./package.json": "./package.json", 16 | "./action": { 17 | -------------------------------------------------------------------------------- /scripts/version.ts: -------------------------------------------------------------------------------- 1 | import { $ } from "bun"; 2 | import { readFileSync, writeFileSync } from "fs"; 3 | 4 | // Check for uncommitted changes 5 | const status = await $`git status --porcelain`.quiet(); 6 | if (!!status.text() && !process.env.FORCE) { 7 | console.error( 8 | "There are uncommitted changes. Commit them before releasing or run with FORCE=true.", 9 | ); 10 | process.exit(1); 11 | } 12 | 13 | // Check if on main branch 14 | const currentBranch = (await $`git rev-parse --abbrev-ref HEAD`.quiet()) 15 | .text() 16 | .trim(); 17 | if (currentBranch !== "main" && !process.env.FORCE) { 18 | console.error( 19 | "Not on main branch. Switch to main before releasing or run with FORCE=true.", 20 | ); 21 | process.exit(1); 22 | } 23 | 24 | // Bump project version 25 | const semverPart = Bun.argv[3] || "patch"; 26 | const json = await Bun.file("./package.json").json(); 27 | const [major, minor, patch] = json.version.split(".").map((s) => parseInt(s)); 28 | json.version = bump([major, minor, patch], semverPart); 29 | await Bun.write("./package.json", JSON.stringify(json, null, 2) + "\n"); 30 | 31 | // Update manifest.json with new version and get minAppVersion 32 | const pluginManifestPath = "./manifest.json"; 33 | const pluginManifest = await Bun.file(pluginManifestPath).json(); 34 | const { minAppVersion } = pluginManifest; 35 | pluginManifest.version = json.version; 36 | await Bun.write( 37 | pluginManifestPath, 38 | JSON.stringify(pluginManifest, null, 2) + "\n", 39 | ); 40 | 41 | // Update versions.json with target version and minAppVersion from manifest.json 42 | const pluginVersionsPath = "./versions.json"; 43 | let versions = JSON.parse(readFileSync(pluginVersionsPath, "utf8")); 44 | versions[json.version] = minAppVersion; 45 | writeFileSync(pluginVersionsPath, JSON.stringify(versions, null, "\t") + "\n"); 46 | 47 | // Commit, tag and push 48 | await $`git add package.json ${pluginManifestPath} ${pluginVersionsPath}`; 49 | await $`git commit -m ${json.version}`; 50 | await $`git tag ${json.version}`; 51 | await $`git push`; 52 | await $`git push origin ${json.version}`; 53 | 54 | function bump(semver: [number, number, number], semverPart = "patch") { 55 | switch (semverPart) { 56 | case "major": 57 | semver[0]++; 58 | semver[1] = 0; 59 | semver[2] = 0; 60 | break; 61 | case "minor": 62 | semver[1]++; 63 | semver[2] = 0; 64 | break; 65 | case "patch": 66 | semver[2]++; 67 | break; 68 | default: 69 | throw new Error(`Invalid semver part: ${semverPart}`); 70 | } 71 | 72 | return semver.join("."); 73 | } 74 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.1.1": "0.15.0", 3 | "0.2.0": "0.15.0", 4 | "0.2.4": "0.15.0", 5 | "0.2.5": "0.15.0", 6 | "0.2.6": "0.15.0", 7 | "0.2.7": "0.15.0", 8 | "0.2.8": "0.15.0", 9 | "0.2.9": "0.15.0", 10 | "0.2.10": "0.15.0", 11 | "0.2.11": "0.15.0", 12 | "0.2.12": "0.15.0", 13 | "0.2.13": "0.15.0", 14 | "0.2.14": "0.15.0", 15 | "0.2.15": "0.15.0", 16 | "0.2.16": "0.15.0", 17 | "0.2.17": "0.15.0", 18 | "0.2.18": "0.15.0", 19 | "0.2.19": "0.15.0", 20 | "0.2.20": "0.15.0", 21 | "0.2.21": "0.15.0", 22 | "0.2.22": "0.15.0" 23 | } 24 | --------------------------------------------------------------------------------