#
tokens: 48799/50000 98/108 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
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 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/jacksteamdev/obsidian-mcp-tools)](https://github.com/jacksteamdev/obsidian-mcp-tools/releases/latest)
  4 | [![Build status](https://img.shields.io/github/actions/workflow/status/jacksteamdev/obsidian-mcp-tools/release.yml)](https://github.com/jacksteamdev/obsidian-mcp-tools/actions)
  5 | [![License](https://img.shields.io/github/license/jacksteamdev/obsidian-mcp-tools)](LICENSE)
  6 | 
  7 | [Features](#features) | [Installation](#installation) | [Configuration](#configuration) | [Troubleshooting](#troubleshooting) | [Security](#security) | [Development](#development) | [Support](#support)
  8 | 
  9 | > **🔄 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("![Test](https://example.com/images/test.png)");
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 | 
```
Page 1/2FirstPrevNextLast