This is page 1 of 2. Use http://codebase.md/jacksteamdev/obsidian-mcp-tools?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .clinerules ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── config.yml │ │ ├── feature_request.md │ │ └── question.md │ ├── pull_request_template.md │ └── workflows │ └── release.yml ├── .gitignore ├── .prettierrc.yaml ├── .vscode │ └── settings.json ├── bun.lock ├── CONTRIBUTING.md ├── docs │ ├── features │ │ ├── mcp-server-install.md │ │ └── prompt-requirements.md │ ├── migration-plan.md │ └── project-architecture.md ├── LICENSE ├── manifest.json ├── mise.toml ├── package.json ├── packages │ ├── mcp-server │ │ ├── .gitignore │ │ ├── package.json │ │ ├── README.md │ │ ├── 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 │ │ │ │ ├── formatMcpError.ts │ │ │ │ ├── formatString.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.ts │ │ │ │ ├── makeRequest.ts │ │ │ │ ├── parseTemplateParameters.test.ts │ │ │ │ ├── parseTemplateParameters.ts │ │ │ │ └── ToolRegistry.ts │ │ │ └── types │ │ │ └── global.d.ts │ │ └── tsconfig.json │ ├── obsidian-plugin │ │ ├── .editorconfig │ │ ├── .eslintignore │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── bun.config.ts │ │ ├── docs │ │ │ └── openapi.yaml │ │ ├── package.json │ │ ├── README.md │ │ ├── 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 │ │ ├── package.json │ │ ├── README.md │ │ ├── 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 │ ├── eslint.config.js │ ├── package.json │ ├── postcss.config.js │ ├── README.md │ ├── 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 │ └── [email protected] ├── README.md ├── scripts │ └── version.ts ├── SECURITY.md └── versions.json ``` # Files -------------------------------------------------------------------------------- /packages/test-site/.npmrc: -------------------------------------------------------------------------------- ``` 1 | engine-strict=true 2 | ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/.npmrc: -------------------------------------------------------------------------------- ``` 1 | tag-version-prefix="" ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/.eslintignore: -------------------------------------------------------------------------------- ``` 1 | node_modules/ 2 | 3 | main.js 4 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules/ 2 | build/ 3 | *.log 4 | .env* 5 | playground/ ``` -------------------------------------------------------------------------------- /packages/test-site/.prettierignore: -------------------------------------------------------------------------------- ``` 1 | # Package Managers 2 | package-lock.json 3 | pnpm-lock.yaml 4 | yarn.lock 5 | ``` -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- ```yaml 1 | trailingComma: "all" 2 | tabWidth: 2 3 | semi: true 4 | singleQuote: false 5 | printWidth: 80 6 | useTabs: false ``` -------------------------------------------------------------------------------- /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/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/.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/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/.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/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 | ``` -------------------------------------------------------------------------------- /.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 | ``` -------------------------------------------------------------------------------- /.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 | ``` -------------------------------------------------------------------------------- /packages/shared/README.md: -------------------------------------------------------------------------------- ```markdown 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/test-site/README.md: -------------------------------------------------------------------------------- ```markdown 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/obsidian-plugin/README.md: -------------------------------------------------------------------------------- ```markdown 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 <path-to-vault-config-file> 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/mcp-server/README.md: -------------------------------------------------------------------------------- ```markdown 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 <binary> 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 [[email protected]](mailto:[email protected]) 137 | 3. Follow responsible disclosure practices 138 | 139 | ## License 140 | 141 | [MIT License](LICENSE) 142 | ```` 143 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Tools for Obsidian 2 | 3 | [](https://github.com/jacksteamdev/obsidian-mcp-tools/releases/latest) 4 | [](https://github.com/jacksteamdev/obsidian-mcp-tools/actions) 5 | [](LICENSE) 6 | 7 | [Features](#features) | [Installation](#installation) | [Configuration](#configuration) | [Troubleshooting](#troubleshooting) | [Security](#security) | [Development](#development) | [Support](#support) 8 | 9 | > **🔄 Seeking Project Maintainers** 10 | > 11 | > This project is actively seeking dedicated maintainers to take over development and community management. The project will remain under the current GitHub account for Obsidian plugin store compliance, with new maintainers added as collaborators. 12 | > 13 | > **Interested?** Join our [Discord community](https://discord.gg/q59pTrN9AA) or check our [maintainer requirements](CONTRIBUTING.md#maintainer-responsibilities). 14 | > 15 | > **Timeline**: Applications open until **September 15, 2025**. Selection by **September 30, 2025**. 16 | 17 | 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] 18 | 19 | This plugin consists of two parts: 20 | 1. An Obsidian plugin that adds MCP capabilities to your vault 21 | 2. A local MCP server that handles communication with AI applications 22 | 23 | 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] 24 | 25 | > **Privacy Note**: When using Claude Desktop with this plugin, your conversations with Claude are not used to train Anthropic's models by default. [^1] 26 | 27 | ## Features 28 | 29 | When connected to an MCP client like Claude Desktop, this plugin enables: 30 | 31 | - **Vault Access**: Allows AI assistants to read and reference your notes while maintaining your vault's security [^4] 32 | - **Semantic Search**: AI assistants can search your vault based on meaning and context, not just keywords [^5] 33 | - **Template Integration**: Execute Obsidian templates through AI interactions, with dynamic parameters and content generation [^6] 34 | 35 | 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. 36 | 37 | ## Prerequisites 38 | 39 | ### Required 40 | 41 | - [Obsidian](https://obsidian.md/) v1.7.7 or higher 42 | - [Claude Desktop](https://claude.ai/download) installed and configured 43 | - [Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin installed and configured with an API key 44 | 45 | ### Recommended 46 | 47 | - [Templater](https://silentvoid13.github.io/Templater/) plugin for enhanced template functionality 48 | - [Smart Connections](https://smartconnections.app/) plugin for semantic search capabilities 49 | 50 | ## Installation 51 | 52 | > [!Important] 53 | > 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. 54 | 55 | 1. Install the plugin from Obsidian's Community Plugins 56 | 2. Enable the plugin in Obsidian settings 57 | 3. Open the plugin settings 58 | 4. Click "Install Server" to download and configure the MCP server 59 | 60 | Clicking the install button will: 61 | 62 | - Download the appropriate MCP server binary for your platform 63 | - Configure Claude Desktop to use the server 64 | - Set up necessary permissions and paths 65 | 66 | ### Installation Locations 67 | 68 | - **Server Binary**: {vault}/.obsidian/plugins/obsidian-mcp-tools/bin/ 69 | - **Log Files**: 70 | - macOS: ~/Library/Logs/obsidian-mcp-tools 71 | - Windows: %APPDATA%\obsidian-mcp-tools\logs 72 | - Linux: ~/.local/share/obsidian-mcp-tools/logs 73 | 74 | ## Configuration 75 | 76 | After clicking the "Install Server" button in the plugin settings, the plugin will automatically: 77 | 78 | 1. Download the appropriate MCP server binary 79 | 2. Use your Local REST API plugin's API key 80 | 3. Configure Claude Desktop to use the MCP server 81 | 4. Set up appropriate paths and permissions 82 | 83 | 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. 84 | 85 | ## Troubleshooting 86 | 87 | If you encounter issues: 88 | 89 | 1. Check the plugin settings to verify: 90 | - All required plugins are installed 91 | - The server is properly installed 92 | - Claude Desktop is configured 93 | 2. Review the logs: 94 | - Open plugin settings 95 | - Click "Open Logs" under Resources 96 | - Look for any error messages or warnings 97 | 3. Common Issues: 98 | - **Server won't start**: Ensure Claude Desktop is running 99 | - **Connection errors**: Verify Local REST API plugin is configured 100 | - **Permission errors**: Try reinstalling the server 101 | 102 | ## Security 103 | 104 | ### Binary Distribution 105 | 106 | - All releases are built using GitHub Actions with reproducible builds 107 | - Binaries are signed and attested using SLSA provenance 108 | - Release workflows are fully auditable in the repository 109 | 110 | ### Runtime Security 111 | 112 | - The MCP server runs with minimal required permissions 113 | - All communication is encrypted 114 | - API keys are stored securely using platform-specific credential storage 115 | 116 | ### Binary Verification 117 | 118 | 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. 119 | 120 | To verify a binary using the GitHub CLI: 121 | 122 | 1. Install GitHub CLI: 123 | 124 | ```bash 125 | # macOS (Homebrew) 126 | brew install gh 127 | 128 | # Windows (Scoop) 129 | scoop install gh 130 | 131 | # Linux 132 | sudo apt install gh # Debian/Ubuntu 133 | ``` 134 | 135 | 2. Verify the binary: 136 | ```bash 137 | gh attestation verify --owner jacksteamdev <binary path or URL> 138 | ``` 139 | 140 | The verification will show: 141 | 142 | - The binary's SHA256 hash 143 | - Confirmation that it was built by this repository's GitHub Actions workflows 144 | - The specific workflow file and version tag that created it 145 | - Compliance with SLSA Level 3 build requirements 146 | 147 | This verification ensures the binary hasn't been tampered with and was built directly from this repository's source code. 148 | 149 | ### Reporting Security Issues 150 | 151 | Please report security vulnerabilities via our [security policy](SECURITY.md). 152 | Do not report security vulnerabilities in public issues. 153 | 154 | ## Development 155 | 156 | This project uses a monorepo structure with feature-based architecture. For detailed project architecture documentation, see [.clinerules](.clinerules). 157 | 158 | ### Using Cline 159 | 160 | 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. 161 | 162 | ### Workspace 163 | 164 | This project uses a [Bun](https://bun.sh/) workspace structure: 165 | 166 | ``` 167 | packages/ 168 | ├── mcp-server/ # Server implementation 169 | ├── obsidian-plugin/ # Obsidian plugin 170 | └── shared/ # Shared utilities and types 171 | ``` 172 | 173 | ### Building 174 | 175 | 1. Install dependencies: 176 | ```bash 177 | bun install 178 | ``` 179 | 2. Build all packages: 180 | ```bash 181 | bun run build 182 | ``` 183 | 3. For development: 184 | ```bash 185 | bun run dev 186 | ``` 187 | 188 | ### Requirements 189 | 190 | - [bun](https://bun.sh/) v1.1.42 or higher 191 | - TypeScript 5.0+ 192 | 193 | ## Contributing 194 | 195 | **Before contributing, please read our [Contributing Guidelines](CONTRIBUTING.md) including our community standards and behavioral expectations.** 196 | 197 | 1. Fork the repository 198 | 2. Create a feature branch 199 | 3. Make your changes 200 | 4. Run tests: 201 | ```bash 202 | bun test 203 | ``` 204 | 5. Submit a pull request 205 | 206 | We welcome genuine contributions but maintain strict community standards. Be respectful and constructive in all interactions. 207 | 208 | ## Support 209 | 210 | - 💬 [Join our Discord](https://discord.gg/q59pTrN9AA) for questions, discussions, and community support 211 | - [Open an issue](https://github.com/jacksteamdev/obsidian-mcp-tools/issues) for bug reports and feature requests 212 | 213 | **Please read our [Contributing Guidelines](CONTRIBUTING.md) before posting.** We maintain high community standards and have zero tolerance for toxic behavior. 214 | 215 | ## Changelog 216 | 217 | See [GitHub Releases](https://github.com/jacksteamdev/obsidian-mcp-tools/releases) for detailed changelog information. 218 | 219 | ## License 220 | 221 | [MIT License](LICENSE) 222 | 223 | ## Footnotes 224 | 225 | [^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) 226 | [^2]: For more information about the Model Context Protocol, see [MCP Introduction](https://modelcontextprotocol.io/introduction) 227 | [^3]: For a list of available MCP Clients, see [MCP Example Clients](https://modelcontextprotocol.io/clients) 228 | [^4]: Requires Obsidian plugin Local REST API 229 | [^5]: Requires Obsidian plugin Smart Connections 230 | [^6]: Requires Obsidian plugin Templater 231 | ``` -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- ```markdown 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 [[email protected]]. 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 <binary_path> 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). ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown 1 | # Contributing to MCP Tools for Obsidian 2 | 3 | ## Community Standards 4 | 5 | This is a **free, open-source project** maintained by volunteers in their spare time. We welcome genuine contributions and constructive discussions, but we have **zero tolerance** for toxic behavior. 6 | 7 | ### Unacceptable Behavior 8 | - Demanding features or fixes 9 | - Rude, dismissive, or condescending language 10 | - Entitlement or treating maintainers like paid support 11 | - Shaming contributors for mistakes or decisions 12 | - Aggressive or impatient language in issues or discussions 13 | 14 | ### Consequences 15 | **One strike policy**: Any toxic, demanding, or rude behavior results in an immediate ban from both the GitHub repository and Discord server. 16 | 17 | ### Before You Post 18 | Think before you post. Ask yourself: 19 | - Am I being respectful and constructive? 20 | - Would I talk this way to a volunteer helping me for free? 21 | - Am I treating maintainers like human beings, not paid support staff? 22 | 23 | **Remember**: We don't owe anyone anything. This is a gift to the community, and we expect contributors to act accordingly. 24 | 25 | ## Getting Help & Community 26 | 27 | - **Discord**: [Join our community](https://discord.gg/q59pTrN9AA) for discussions and support 28 | - **Issues**: Use GitHub issues for bug reports and feature requests (following our guidelines) 29 | - **Discussions**: Use GitHub Discussions for questions and general help 30 | 31 | ## Development Setup 32 | 33 | 1. **Prerequisites**: 34 | - [Bun](https://bun.sh/) v1.1.42 or higher 35 | - [Obsidian](https://obsidian.md/) v1.7.7 or higher 36 | - [Claude Desktop](https://claude.ai/download) for testing 37 | 38 | 2. **Clone and Setup**: 39 | ```bash 40 | git clone https://github.com/jacksteamdev/obsidian-mcp-tools.git 41 | cd obsidian-mcp-tools 42 | bun install 43 | ``` 44 | 45 | 3. **Development**: 46 | ```bash 47 | bun run dev # Development mode with watch 48 | bun run build # Production build 49 | bun test # Run tests 50 | ``` 51 | 52 | ## Project Architecture 53 | 54 | ### Documentation Resources 55 | - **Project architecture**: `/docs/project-architecture.md` 56 | - **Feature documentation**: `/docs/features/` 57 | - **AI-generated docs**: [DeepWiki](https://deepwiki.com/jacksteamdev/obsidian-mcp-tools) 58 | - **Coding standards**: `.clinerules` 59 | 60 | ### Monorepo Structure 61 | ``` 62 | packages/ 63 | ├── mcp-server/ # TypeScript MCP server implementation 64 | ├── obsidian-plugin/ # Obsidian plugin (TypeScript/Svelte) 65 | └── shared/ # Shared utilities and types 66 | ``` 67 | 68 | ### Feature-Based Architecture 69 | - Self-contained modules in `src/features/` with standardized structure 70 | - Each feature exports a setup function for initialization 71 | - Use ArkType for runtime type validation 72 | - Follow patterns documented in `.clinerules` 73 | 74 | ## Contributing Guidelines 75 | 76 | ### Submitting Issues 77 | **Before creating an issue**: 78 | - Search existing issues to avoid duplicates 79 | - Provide clear, detailed descriptions 80 | - Include system information and steps to reproduce 81 | - Be respectful and patient - remember this is volunteer work 82 | 83 | **Good issue example**: 84 | > **Bug Report**: MCP server fails to start on macOS 14.2 85 | > 86 | > **Environment**: macOS 14.2, Obsidian 1.7.7, Claude Desktop 1.0.2 87 | > 88 | > **Steps to reproduce**: 89 | > 1. Install plugin from Community Plugins 90 | > 2. Click "Install Server" in settings 91 | > 3. Server download completes but fails to start 92 | > 93 | > **Expected**: Server starts and connects to Claude 94 | > **Actual**: Error in logs: [paste error message] 95 | > 96 | > **Additional context**: Logs attached, willing to test fixes 97 | 98 | ### Pull Requests 99 | 1. **Fork the repository** and create a feature branch 100 | 2. **Follow the architecture patterns** described in `/docs/project-architecture.md` 101 | 3. **Write tests** for new functionality 102 | 4. **Test thoroughly**: 103 | - Local Obsidian vault integration 104 | - MCP server functionality 105 | - Claude Desktop connection 106 | 5. **Submit PR** with clear description of changes 107 | 108 | ### Code Standards 109 | - **TypeScript strict mode** required 110 | - **ArkType validation** for all external data 111 | - **Error handling** with descriptive messages 112 | - **Documentation** for public APIs 113 | - **Follow existing patterns** in `.clinerules` 114 | 115 | ## Release Process (Maintainers Only) 116 | 117 | ### Creating Releases 118 | 1. **Version bump**: `bun run version [patch|minor|major]` 119 | - Automatically updates `package.json`, `manifest.json`, and `versions.json` 120 | - Creates git commit and tag 121 | - Pushes to GitHub 122 | 123 | 2. **Automated build**: GitHub Actions handles: 124 | - Cross-platform binary compilation 125 | - SLSA provenance attestation 126 | - Release artifact upload 127 | - Security verification 128 | 129 | 3. **Release notes**: GitHub automatically generates release notes from PRs 130 | 131 | ### Maintainer Responsibilities 132 | - **Code review**: Review PRs for quality, security, and architecture compliance 133 | - **Issue triage**: Respond to issues and help users (when possible) 134 | - **Release management**: Create releases following security protocols 135 | - **Community management**: Enforce community standards 136 | - **Documentation**: Keep docs current and comprehensive 137 | 138 | ### Access Requirements 139 | - **GitHub**: "Maintain" or "Admin" access to repository 140 | - **Discord**: Moderator permissions for community management 141 | - **Time commitment**: 5-10 hours per week (15-20 during releases) 142 | 143 | ## Testing Guidelines 144 | 145 | ### Local Testing 146 | ```bash 147 | # Unit tests 148 | bun test 149 | 150 | # Integration testing with local vault 151 | # 1. Set up test Obsidian vault 152 | # 2. Install plugin locally: `bun run build:plugin` 153 | # 3. Test MCP server connection with Claude Desktop 154 | # 4. Verify all features work end-to-end 155 | ``` 156 | 157 | ## Security Considerations 158 | 159 | ### Binary Security 160 | - All binaries are SLSA-attested and cryptographically signed 161 | - Use `gh attestation verify --owner jacksteamdev <binary>` to verify integrity 162 | - Report security issues to [[email protected]](mailto:[email protected]) 163 | 164 | ### Development Security 165 | - **No secrets in code**: Use environment variables for API keys 166 | - **Input validation**: Use ArkType for all external data 167 | - **Minimal permissions**: MCP server runs with least required access 168 | - **Audit dependencies**: Regularly update and audit npm packages 169 | 170 | ## Resources 171 | 172 | - **GitHub Repository**: [jacksteamdev/obsidian-mcp-tools](https://github.com/jacksteamdev/obsidian-mcp-tools) 173 | - **Discord Community**: [Join here](https://discord.gg/q59pTrN9AA) 174 | - **Release History**: [GitHub Releases](https://github.com/jacksteamdev/obsidian-mcp-tools/releases) 175 | - **Security Policy**: [SECURITY.md](SECURITY.md) 176 | - **AI Documentation**: [DeepWiki](https://deepwiki.com/jacksteamdev/obsidian-mcp-tools) 177 | 178 | ## Questions? 179 | 180 | Join our [Discord community](https://discord.gg/q59pTrN9AA) for questions and discussions. Please read this document thoroughly before asking questions that are already covered here. 181 | 182 | **Remember**: Be respectful, be patient, and remember that everyone here is volunteering their time to help make this project better. 183 | ``` -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- ```toml 1 | [tools] 2 | bun = "latest" 3 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/fetch/services/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./markdown"; 2 | ``` -------------------------------------------------------------------------------- /packages/test-site/src/routes/+layout.ts: -------------------------------------------------------------------------------- ```typescript 1 | export const prerender = true; 2 | ``` -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./logger"; 2 | export * from "./types"; 3 | ``` -------------------------------------------------------------------------------- /packages/test-site/postcss.config.js: -------------------------------------------------------------------------------- ```javascript 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | ``` -------------------------------------------------------------------------------- /packages/test-site/src/lib/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | // place files you want to import through the `$lib` alias in this folder. 2 | ``` -------------------------------------------------------------------------------- /packages/test-site/src/app.css: -------------------------------------------------------------------------------- ```css 1 | @import 'tailwindcss/base'; 2 | @import 'tailwindcss/components'; 3 | @import 'tailwindcss/utilities'; 4 | ``` -------------------------------------------------------------------------------- /packages/test-site/src/routes/+layout.svelte: -------------------------------------------------------------------------------- ``` 1 | <script lang="ts"> 2 | import '../app.css'; 3 | let { children } = $props(); 4 | </script> 5 | 6 | {@render children()} 7 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/version/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { version } from "../../../../../package.json" with { type: "json" }; 2 | 3 | export function getVersion() { 4 | return version; 5 | } 6 | ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/svelte.config.js: -------------------------------------------------------------------------------- ```javascript 1 | // @ts-check 2 | import { sveltePreprocess } from 'svelte-preprocess'; 3 | 4 | const config = { 5 | preprocess: sveltePreprocess() 6 | } 7 | 8 | export default config; ``` -------------------------------------------------------------------------------- /packages/test-site/vite.config.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/fetch/constants.ts: -------------------------------------------------------------------------------- ```typescript 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/test-site/tailwind.config.ts: -------------------------------------------------------------------------------- ```typescript 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/mcp-server/src/shared/index.ts: -------------------------------------------------------------------------------- ```typescript 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/shared/src/types/index.ts: -------------------------------------------------------------------------------- ```typescript 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/obsidian-plugin/src/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | declare module "obsidian" { 2 | interface McpToolsPluginSettings { 3 | version?: string; 4 | } 5 | 6 | interface Plugin { 7 | loadData(): Promise<McpToolsPluginSettings>; 8 | saveData(data: McpToolsPluginSettings): Promise<void>; 9 | } 10 | } 11 | 12 | export {}; 13 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/types/global.d.ts: -------------------------------------------------------------------------------- ```typescript 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/test-site/src/app.d.ts: -------------------------------------------------------------------------------- ```typescript 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/obsidian-plugin/src/features/core/components/SettingsTab.svelte: -------------------------------------------------------------------------------- ``` 1 | <script lang="ts"> 2 | import { FeatureSettings as McpServerInstallSettings } from "src/features/mcp-server-install"; 3 | import type McpServerPlugin from "src/main"; 4 | 5 | export let plugin: McpServerPlugin; 6 | </script> 7 | 8 | <div class="settings-container"> 9 | <McpServerInstallSettings {plugin} /> 10 | </div> 11 | ``` -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- ```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/test-site/src/app.html: -------------------------------------------------------------------------------- ```html 1 | <!doctype html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <link rel="icon" href="%sveltekit.assets%/favicon.png" /> 6 | <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 | %sveltekit.head% 8 | </head> 9 | <body data-sveltekit-preload-data="hover"> 10 | <div style="display: contents">%sveltekit.body%</div> 11 | </body> 12 | </html> 13 | ``` -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "id": "mcp-tools", 3 | "name": "MCP Tools", 4 | "version": "0.2.27", 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 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/logger.ts: -------------------------------------------------------------------------------- ```typescript 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/obsidian-plugin/src/features/mcp-server-install/utils/getFileSystemAdapter.ts: -------------------------------------------------------------------------------- ```typescript 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 | ``` -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- ```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 | "0.2.23": "0.15.0", 24 | "0.2.24": "0.15.0", 25 | "0.2.25": "0.15.0", 26 | "0.2.26": "0.15.0", 27 | "0.2.27": "0.15.0" 28 | } 29 | ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- ```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 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- ```yaml 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💬 Discord Community 4 | url: https://discord.gg/q59pTrN9AA 5 | about: Join our Discord for questions, discussions, and community support 6 | - name: 📖 Contributing Guidelines 7 | url: https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/CONTRIBUTING.md 8 | about: Read our community standards and development guidelines before posting 9 | - name: 🔒 Security Issues 10 | url: https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/SECURITY.md 11 | about: Report security vulnerabilities privately via email 12 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/formatMcpError.ts: -------------------------------------------------------------------------------- ```typescript 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/obsidian-plugin/src/shared/logger.ts: -------------------------------------------------------------------------------- ```typescript 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/tsconfig.json: -------------------------------------------------------------------------------- ```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/obsidian-plugin/src/features/mcp-server-install/utils/openFolder.ts: -------------------------------------------------------------------------------- ```typescript 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/shared/tsconfig.json: -------------------------------------------------------------------------------- ```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/tsconfig.json: -------------------------------------------------------------------------------- ```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/svelte.config.js: -------------------------------------------------------------------------------- ```javascript 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/obsidian-plugin/src/features/mcp-server-install/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Plugin } from "obsidian"; 2 | import type { SetupResult } from "./types"; 3 | 4 | export async function setup(plugin: Plugin): Promise<SetupResult> { 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/mcp-server/tsconfig.json: -------------------------------------------------------------------------------- ```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/test-site/eslint.config.js: -------------------------------------------------------------------------------- ```javascript 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 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "mcp-tools-for-obsidian", 3 | "version": "0.2.27", 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 | "[email protected]": "patches/[email protected]" 30 | }, 31 | "dependencies": { 32 | "caniuse-lite": "^1.0.30001724" 33 | } 34 | } 35 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/scripts/install.ts: -------------------------------------------------------------------------------- ```typescript 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 <OBSIDIAN_API_KEY>"); 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/obsidian-plugin/src/features/mcp-server-install/constants/bundle-time.ts: -------------------------------------------------------------------------------- ```typescript 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: -------------------------------------------------------------------------------- ```typescript 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/scripts/zip.ts: -------------------------------------------------------------------------------- ```typescript 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/shared/src/types/smart-search.ts: -------------------------------------------------------------------------------- ```typescript 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/test-site/package.json: -------------------------------------------------------------------------------- ```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/mcp-server/src/index.ts: -------------------------------------------------------------------------------- ```typescript 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/formatString.ts: -------------------------------------------------------------------------------- ```typescript 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/obsidian-plugin/src/features/core/index.ts: -------------------------------------------------------------------------------- ```typescript 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<SetupResult> { 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/package.json: -------------------------------------------------------------------------------- ```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/mcp-server/src/features/smart-connections/index.ts: -------------------------------------------------------------------------------- ```typescript 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/obsidian-plugin/src/features/mcp-server-install/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { Templater, SmartConnections } 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"]?: { 54 | env?: SmartConnections.SmartSearch; 55 | } & Plugin; 56 | ["templater-obsidian"]?: { 57 | templater?: Templater.ITemplater; 58 | }; 59 | }; 60 | }; 61 | } 62 | } 63 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: ✨ Feature Request 3 | about: Suggest a new feature or enhancement 4 | title: "[FEATURE] " 5 | labels: ["enhancement"] 6 | assignees: [] 7 | --- 8 | 9 | <!-- 10 | 🚨 READ FIRST: https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/CONTRIBUTING.md 11 | 12 | This is a FREE project maintained by volunteers. Be respectful and patient. 13 | Rude, demanding, or entitled behavior results in immediate bans. 14 | 15 | For discussions about ideas, use Discord: https://discord.gg/q59pTrN9AA 16 | --> 17 | 18 | ## Feature Description 19 | **Clear, concise description of the proposed feature** 20 | 21 | ## Use Case & Motivation 22 | **What problem does this solve? How would you use this feature?** 23 | 24 | ## Proposed Solution 25 | **How do you envision this working?** 26 | 27 | ## Alternatives Considered 28 | **What other approaches have you considered?** 29 | 30 | ## Implementation Notes 31 | <!-- 32 | Optional: Technical details if you have ideas about implementation 33 | - Which package would this affect? (mcp-server, obsidian-plugin, shared) 34 | - Any specific Obsidian plugins this would integrate with? 35 | - MCP protocol considerations? 36 | --> 37 | 38 | ## Maintainer Transition Note 39 | **This project is currently transitioning to new maintainers. Feature requests will be evaluated by the new maintainer team based on:** 40 | - Alignment with project goals 41 | - Implementation complexity 42 | - Community benefit 43 | - Available development time 44 | 45 | --- 46 | **Remember**: This is volunteer work. Suggestions are welcome, but demands are not. Be patient and respectful. 47 | ``` -------------------------------------------------------------------------------- /docs/features/prompt-requirements.md: -------------------------------------------------------------------------------- ```markdown 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<string, string>; 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 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/package.json: -------------------------------------------------------------------------------- ```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 | } ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: 🐛 Bug Report 3 | about: Report a technical issue or unexpected behavior 4 | title: "[BUG] " 5 | labels: ["bug"] 6 | assignees: [] 7 | --- 8 | 9 | <!-- 10 | 🚨 READ FIRST: https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/CONTRIBUTING.md 11 | 12 | This is a FREE project maintained by volunteers. Be respectful and patient. 13 | Rude, demanding, or entitled behavior results in immediate bans. 14 | 15 | For questions or general help, use Discord: https://discord.gg/q59pTrN9AA 16 | --> 17 | 18 | ## Bug Description 19 | **Clear, concise description of the bug** 20 | 21 | ## Environment 22 | - **OS**: (e.g., macOS 14.2, Windows 11, Ubuntu 22.04) 23 | - **Obsidian version**: (e.g., 1.7.7) 24 | - **Claude Desktop version**: (e.g., 1.0.2) 25 | - **Plugin version**: (e.g., 0.2.23) 26 | - **Required plugins status**: 27 | - [ ] Local REST API installed and configured 28 | - [ ] Smart Connections installed (if using semantic search) 29 | - [ ] Templater installed (if using templates) 30 | 31 | ## Steps to Reproduce 32 | 1. 33 | 2. 34 | 3. 35 | 36 | ## Expected Behavior 37 | **What should happen** 38 | 39 | ## Actual Behavior 40 | **What actually happens** 41 | 42 | ## Error Messages/Logs 43 | <!-- 44 | To access logs: 45 | 1. Open plugin settings 46 | 2. Click "Open Logs" under Resources 47 | 3. Copy relevant error messages 48 | --> 49 | 50 | ``` 51 | Paste error messages or log excerpts here 52 | ``` 53 | 54 | ## Additional Context 55 | <!-- 56 | - Screenshots (if helpful) 57 | - Vault setup details 58 | - Recent changes to your setup 59 | - Other plugins that might conflict 60 | --> 61 | 62 | ## Troubleshooting Attempted 63 | <!-- What have you already tried? --> 64 | - [ ] Restarted Obsidian 65 | - [ ] Restarted Claude Desktop 66 | - [ ] Reinstalled the MCP server via plugin settings 67 | - [ ] Checked Local REST API is running 68 | - [ ] Other: 69 | 70 | --- 71 | **Remember**: This is volunteer work. Be patient and respectful. We'll help when we can. 72 | ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/scripts/link.ts: -------------------------------------------------------------------------------- ```typescript 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 <path_to_obsidian_vault>` 11 | * @returns {Promise<void>} 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 <path_to_obsidian_vault_config_folder>", 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 | ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | name: ❓ Question or Help 3 | about: Get help with setup, usage, or general questions 4 | title: "[QUESTION] " 5 | labels: ["question"] 6 | assignees: [] 7 | --- 8 | 9 | <!-- 10 | 🚨 REDIRECT TO DISCORD: https://discord.gg/q59pTrN9AA 11 | 12 | Questions and general help requests should be posted in our Discord community, 13 | not as GitHub issues. You'll get faster responses and can engage with other users. 14 | 15 | GitHub issues are for: 16 | ✅ Bug reports (technical problems) 17 | ✅ Feature requests (new functionality) 18 | ✅ Documentation issues 19 | 20 | Discord is for: 21 | 💬 Setup help and troubleshooting 22 | 💬 Usage questions 23 | 💬 General discussions 24 | 💬 Community support 25 | 26 | READ: https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/CONTRIBUTING.md 27 | --> 28 | 29 | ## Please Use Discord Instead 30 | 31 | **For questions and help, please join our Discord community:** 32 | 🔗 **https://discord.gg/q59pTrN9AA** 33 | 34 | **Benefits of using Discord:** 35 | - ✅ Faster responses from community members 36 | - ✅ Real-time discussion and follow-up 37 | - ✅ Help from other users with similar setups 38 | - ✅ Less formal environment for troubleshooting 39 | - ✅ Doesn't clutter the issue tracker 40 | 41 | ## Before Posting Anywhere 42 | **Please check these resources first:** 43 | - 📖 [README](https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/README.md) - Setup and usage instructions 44 | - 🏗️ [DeepWiki](https://deepwiki.com/jacksteamdev/obsidian-mcp-tools) - AI-generated documentation 45 | - 🔒 [Security](https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/SECURITY.md) - Binary verification and security info 46 | - 📋 [Contributing](https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/CONTRIBUTING.md) - Community standards 47 | 48 | --- 49 | **Remember**: Be respectful when asking for help. We're all volunteers here. 50 | 51 | **This issue will be closed and redirected to Discord.** 52 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/parseTemplateParameters.test.ts: -------------------------------------------------------------------------------- ```typescript 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/features/templates/index.ts: -------------------------------------------------------------------------------- ```typescript 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<string, string>; 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/shared/src/types/prompts.ts: -------------------------------------------------------------------------------- ```typescript 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<Record<string, "string" | "string?">, {}> { 56 | return type( 57 | Object.fromEntries( 58 | args.map((arg) => [arg.name, arg.required ? "string" : "string?"]), 59 | ) as Record<string, "string" | "string?">, 60 | ); 61 | } 62 | ``` -------------------------------------------------------------------------------- /scripts/version.ts: -------------------------------------------------------------------------------- ```typescript 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 | ``` -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- ```markdown 1 | <!-- 2 | 🚨 READ FIRST: https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/CONTRIBUTING.md 3 | 4 | This is a FREE project maintained by volunteers. Be respectful and patient. 5 | Rude, demanding, or entitled behavior results in immediate bans. 6 | 7 | Join Discord for discussions: https://discord.gg/q59pTrN9AA 8 | --> 9 | 10 | ## Pull Request Description 11 | **Clear description of what this PR changes and why** 12 | 13 | ## Type of Change 14 | - [ ] 🐛 Bug fix (non-breaking change that fixes an issue) 15 | - [ ] ✨ New feature (non-breaking change that adds functionality) 16 | - [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected) 17 | - [ ] 📚 Documentation update 18 | - [ ] 🔧 Internal/tooling change 19 | 20 | ## Testing 21 | **How has this been tested?** 22 | - [ ] Local Obsidian vault testing 23 | - [ ] MCP server functionality verified 24 | - [ ] Claude Desktop integration tested 25 | - [ ] Cross-platform testing (if applicable) 26 | 27 | ## Architecture Compliance 28 | - [ ] Follows feature-based architecture patterns (see `/docs/project-architecture.md`) 29 | - [ ] Uses ArkType for runtime validation where applicable 30 | - [ ] Implements proper error handling 31 | - [ ] Includes setup function for new features 32 | - [ ] Follows coding standards in `.clinerules` 33 | 34 | ## Checklist 35 | - [ ] My code follows the project's coding standards 36 | - [ ] I have performed a self-review of my code 37 | - [ ] I have commented my code, particularly in hard-to-understand areas 38 | - [ ] I have made corresponding changes to documentation (if applicable) 39 | - [ ] My changes generate no new warnings 40 | - [ ] I have added tests that prove my fix is effective or that my feature works 41 | - [ ] New and existing unit tests pass locally with my changes 42 | 43 | ## Security Considerations 44 | - [ ] No hardcoded secrets or API keys 45 | - [ ] Input validation implemented where needed 46 | - [ ] No new security vulnerabilities introduced 47 | - [ ] Follows minimum permission principles 48 | 49 | ## Additional Context 50 | <!-- 51 | - Screenshots (if UI changes) 52 | - Performance considerations 53 | - Backward compatibility notes 54 | - Related issues or PRs 55 | --> 56 | 57 | ## For Maintainers 58 | **Review checklist:** 59 | - [ ] Code quality and architecture compliance 60 | - [ ] Security review completed 61 | - [ ] Tests adequate and passing 62 | - [ ] Documentation updated as needed 63 | - [ ] Ready for release 64 | 65 | --- 66 | **Remember**: This is volunteer work. Be patient during the review process. 67 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/core/index.ts: -------------------------------------------------------------------------------- ```typescript 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/shared/parseTemplateParameters.ts: -------------------------------------------------------------------------------- ```typescript 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 | ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml 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 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/fetch/services/markdown.test.ts: -------------------------------------------------------------------------------- ```typescript 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 = "<h1>Hello</h1><p>This is a test</p>"; 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 = '<a href="/about">About</a>'; 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 = '<img src="/images/test.png" alt="Test">'; 21 | const result = convertHtmlToMarkdown(html, baseUrl); 22 | expect(result).toBe(""); 23 | }); 24 | 25 | test("removes data URL images", () => { 26 | const html = '<img src="data:image/png;base64,abc123" alt="Test">'; 27 | const result = convertHtmlToMarkdown(html, baseUrl); 28 | expect(result).toBe(""); 29 | }); 30 | 31 | test("keeps absolute URLs unchanged", () => { 32 | const html = '<a href="https://other.com/page">Link</a>'; 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 | <header>Skip this</header> 40 | <article> 41 | <h1>Keep this</h1> 42 | <p>And this</p> 43 | </article> 44 | <footer>Skip this too</footer> 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 | <div> 53 | <header>Skip this</header> 54 | <article> 55 | <h1>Keep this</h1> 56 | <p>And this</p> 57 | </article> 58 | <footer>Skip this too</footer> 59 | </div> 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 | <div> 68 | <script>alert('test');</script> 69 | <p>Keep this</p> 70 | <style>body { color: red; }</style> 71 | </div> 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: -------------------------------------------------------------------------------- ```typescript 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("<article")) { 78 | const articleStart = html.indexOf("<article"); 79 | const articleEnd = html.lastIndexOf("</article>") + 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/obsidian-plugin/src/features/mcp-server-install/services/uninstall.ts: -------------------------------------------------------------------------------- ```typescript 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<void> { 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/mcp-server/src/shared/makeRequest.ts: -------------------------------------------------------------------------------- ```typescript 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 | const HOST = process.env.OBSIDIAN_HOST || "127.0.0.1"; 10 | export const BASE_URL = `${PROTOCOL}://${HOST}:${PORT}`; 11 | 12 | // Disable TLS certificate validation for local self-signed certificates 13 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 14 | 15 | /** 16 | * Makes a request to the Obsidian Local REST API with the provided path and optional request options. 17 | * Automatically adds the required API key to the request headers. 18 | * Throws an `McpError` if the API response is not successful. 19 | * 20 | * @param path - The path to the Obsidian API endpoint. 21 | * @param init - Optional request options to pass to the `fetch` function. 22 | * @returns The response from the Obsidian API. 23 | */ 24 | 25 | export async function makeRequest< 26 | T extends 27 | | Type<{}, {}> 28 | | Type<null | undefined, {}> 29 | | Type<{} | null | undefined, {}>, 30 | >(schema: T, path: string, init?: RequestInit): Promise<T["infer"]> { 31 | const API_KEY = process.env.OBSIDIAN_API_KEY; 32 | if (!API_KEY) { 33 | logger.error("OBSIDIAN_API_KEY environment variable is required", { 34 | env: process.env, 35 | }); 36 | throw new Error("OBSIDIAN_API_KEY environment variable is required"); 37 | } 38 | 39 | const url = `${BASE_URL}${path}`; 40 | const response = await fetch(url, { 41 | ...init, 42 | headers: { 43 | Authorization: `Bearer ${API_KEY}`, 44 | "Content-Type": "text/markdown", 45 | ...init?.headers, 46 | }, 47 | }); 48 | 49 | if (!response.ok) { 50 | const error = await response.text(); 51 | const message = `${init?.method ?? "GET"} ${path} ${response.status}: ${error}`; 52 | throw new McpError(ErrorCode.InternalError, message); 53 | } 54 | 55 | const isJSON = !!response.headers.get("Content-Type")?.includes("json"); 56 | const data = isJSON ? await response.json() : await response.text(); 57 | // 204 No Content responses should be validated as undefined 58 | const validated = response.status === 204 ? undefined : schema(data); 59 | if (validated instanceof type.errors) { 60 | const stackError = new Error(); 61 | Error.captureStackTrace(stackError, makeRequest); 62 | logger.error("Invalid response from Obsidian API", { 63 | status: response.status, 64 | error: validated.summary, 65 | stack: stackError.stack, 66 | data, 67 | }); 68 | throw new McpError( 69 | ErrorCode.InternalError, 70 | `${init?.method ?? "GET"} ${path} ${response.status}: ${validated.summary}`, 71 | ); 72 | } 73 | 74 | return validated; 75 | } 76 | ``` -------------------------------------------------------------------------------- /packages/shared/src/types/plugin-smart-connections.ts: -------------------------------------------------------------------------------- ```typescript 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<string, unknown>; 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<string>; 75 | nearest(filter?: SearchFilter): Promise<SearchResult[]>; 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<SearchResult[]>; 97 | } 98 | ``` -------------------------------------------------------------------------------- /packages/test-site/src/routes/+page.svelte: -------------------------------------------------------------------------------- ``` 1 | <svelte:head> 2 | <title>Understanding Express Routes: A Complete Guide</title> 3 | <meta name="description" content="Learn how to master Express.js routing with practical examples and best practices for building scalable Node.js applications." /> 4 | <meta property="og:title" content="Understanding Express Routes: A Complete Guide" /> 5 | <meta property="og:description" content="Learn how to master Express.js routing with practical examples and best practices for building scalable Node.js applications." /> 6 | <meta property="og:type" content="article" /> 7 | <meta property="og:url" content="https://yoursite.com/blog/express-routes-guide" /> 8 | <meta property="og:image" content="https://yoursite.com/images/express-routes-banner.jpg" /> 9 | <meta name="author" content="Jane Doe" /> 10 | <link rel="canonical" href="https://yoursite.com/blog/express-routes-guide" /> 11 | </svelte:head> 12 | 13 | <article class="blog-post"> 14 | <header> 15 | <h1>Understanding Express Routes: A Complete Guide</h1> 16 | <div class="metadata"> 17 | <address class="author"> 18 | <!-- svelte-ignore a11y_invalid_attribute --> 19 | By <a rel="author" href="#">Jane Doe</a> 20 | </address> 21 | <time datetime="2023-12-14">December 14, 2023</time> 22 | </div> 23 | </header> 24 | 25 | <section class="content"> 26 | <h2>Introduction</h2> 27 | <p>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.</p> 28 | 29 | <h2>Basic Route Structure</h2> 30 | <p>Express routes follow a simple pattern that combines HTTP methods with URL paths:</p> 31 | 32 | <pre><code>{` 33 | app.get('/users', (req, res) => { 34 | res.send('Get all users'); 35 | }); 36 | `}</code></pre> 37 | 38 | <h2>Route Parameters</h2> 39 | <p>Dynamic routes can be created using parameters:</p> 40 | 41 | <pre><code>{` 42 | app.get('/users/:id', (req, res) => { 43 | const userId = req.params.id; 44 | res.send(\`Get user \${userId}\`); 45 | }); 46 | `}</code></pre> 47 | 48 | <h2>Middleware Integration</h2> 49 | <p>Routes can include middleware functions for additional processing:</p> 50 | 51 | <pre><code>{` 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 | `}</code></pre> 61 | </section> 62 | 63 | <footer> 64 | <div class="tags"> 65 | <span class="tag">Express.js</span> 66 | <span class="tag">Node.js</span> 67 | <span class="tag">Web Development</span> 68 | </div> 69 | 70 | <div class="share"> 71 | <h3>Share this article</h3> 72 | <nav class="social-links"> 73 | <!-- svelte-ignore a11y_invalid_attribute --> 74 | <a href="#">Twitter</a> 75 | <!-- svelte-ignore a11y_invalid_attribute --> 76 | <a href="#">LinkedIn</a> 77 | <!-- svelte-ignore a11y_invalid_attribute --> 78 | <a href="#">Facebook</a> 79 | </nav> 80 | </div> 81 | </footer> 82 | </article> 83 | 84 | <style> 85 | .blog-post { 86 | max-width: 800px; 87 | margin: 0 auto; 88 | padding: 2rem; 89 | } 90 | 91 | .metadata { 92 | color: #666; 93 | margin: 1rem 0; 94 | } 95 | 96 | .content { 97 | line-height: 1.6; 98 | } 99 | 100 | .tags { 101 | margin: 2rem 0; 102 | } 103 | 104 | .tag { 105 | background: #eee; 106 | padding: 0.25rem 0.5rem; 107 | border-radius: 4px; 108 | margin-right: 0.5rem; 109 | } 110 | 111 | pre { 112 | background: #f4f4f4; 113 | padding: 1rem; 114 | border-radius: 4px; 115 | overflow-x: auto; 116 | } 117 | </style> 118 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/fetch/index.ts: -------------------------------------------------------------------------------- ```typescript 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("<html") || 47 | contentType.includes("text/html") || 48 | !contentType; 49 | 50 | let content: string; 51 | let prefix = ""; 52 | 53 | if (isHtml && !args.raw) { 54 | content = convertHtmlToMarkdown(text, args.url); 55 | } else { 56 | content = text; 57 | prefix = `Content type ${contentType} cannot be simplified to markdown, but here is the raw content:\n`; 58 | } 59 | 60 | const maxLength = args.maxLength || 5000; 61 | const startIndex = args.startIndex || 0; 62 | const totalLength = content.length; 63 | 64 | if (totalLength > maxLength) { 65 | content = content.substring(startIndex, startIndex + maxLength); 66 | content += `\n\n<error>Content truncated. Call the fetch tool with a startIndex of ${ 67 | startIndex + maxLength 68 | } to get more content.</error>`; 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/obsidian-plugin/bun.config.ts: -------------------------------------------------------------------------------- ```typescript 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/src/features/mcp-server-install/services/config.ts: -------------------------------------------------------------------------------- ```typescript 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<void> { 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<void> { 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 | ``` -------------------------------------------------------------------------------- /docs/project-architecture.md: -------------------------------------------------------------------------------- ```markdown 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<SetupResult> { 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<McpToolsPluginSettings>; 96 | saveData(data: McpToolsPluginSettings): Promise<void>; 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 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/prompts/index.ts: -------------------------------------------------------------------------------- ```typescript 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/shared/ToolRegistry.ts: -------------------------------------------------------------------------------- ```typescript 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<string, unknown>; 41 | }, 42 | {} 43 | >, 44 | THandler extends ( 45 | request: TSchema["infer"], 46 | context: HandlerContext, 47 | ) => Promise<Result>, 48 | > extends Map<TSchema, THandler> { 49 | private enabled = new Set<TSchema>(); 50 | 51 | register< 52 | Schema extends TSchema, 53 | Handler extends ( 54 | request: Schema["infer"], 55 | context: HandlerContext, 56 | ) => ResultSchema | Promise<ResultSchema>, 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 extends TSchema>(schema: Schema) => { 69 | this.enabled.add(schema); 70 | return this; 71 | }; 72 | 73 | disable = <Schema extends TSchema>(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 = <Schema extends TSchema>( 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 <Schema extends TSchema>( 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<string, unknown>; 159 | }, 160 | {} 161 | >, 162 | ( 163 | request: { 164 | name: string; 165 | arguments?: Record<string, unknown>; 166 | }, 167 | context: HandlerContext, 168 | ) => Promise<Result> 169 | >; 170 | ``` -------------------------------------------------------------------------------- /packages/shared/src/logger.ts: -------------------------------------------------------------------------------- ```typescript 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<string, unknown>, 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<string, unknown> = {}; 78 | 79 | const queue: Promise<void>[] = []; 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<InputLoggerConfig>) { 120 | config = loggerConfigMorph.assert({ ...config, ...newConfig }); 121 | logger.debug("Updated logger configuration", { config }); 122 | }, 123 | set meta(newMeta: Record<string, unknown>) { 124 | logMeta = newMeta; 125 | }, 126 | }; 127 | 128 | return logger; 129 | } 130 | ```