This is page 1 of 2. Use http://codebase.md/jacksteamdev/obsidian-mcp-tools?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: -------------------------------------------------------------------------------- ``` engine-strict=true ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/.npmrc: -------------------------------------------------------------------------------- ``` tag-version-prefix="" ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/.eslintignore: -------------------------------------------------------------------------------- ``` node_modules/ main.js ``` -------------------------------------------------------------------------------- /packages/mcp-server/.gitignore: -------------------------------------------------------------------------------- ``` node_modules/ build/ *.log .env* playground/ ``` -------------------------------------------------------------------------------- /packages/test-site/.prettierignore: -------------------------------------------------------------------------------- ``` # Package Managers package-lock.json pnpm-lock.yaml yarn.lock ``` -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- ```yaml trailingComma: "all" tabWidth: 2 semi: true singleQuote: false printWidth: 80 useTabs: false ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/.editorconfig: -------------------------------------------------------------------------------- ``` # top-most EditorConfig file root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = tab indent_size = 4 tab_width = 4 ``` -------------------------------------------------------------------------------- /packages/test-site/.gitignore: -------------------------------------------------------------------------------- ``` node_modules # Output .output .vercel .netlify .wrangler /.svelte-kit /build # OS .DS_Store Thumbs.db # Env .env .env.* !.env.example !.env.test # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* ``` -------------------------------------------------------------------------------- /packages/test-site/.prettierrc: -------------------------------------------------------------------------------- ``` { "useTabs": true, "singleQuote": true, "trailingComma": "none", "printWidth": 100, "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], "overrides": [ { "files": "*.svelte", "options": { "parser": "svelte" } } ] } ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/.gitignore: -------------------------------------------------------------------------------- ``` # vscode .vscode # Intellij *.iml .idea # npm node_modules # Don't include the compiled main.js file in the repo. # They should be uploaded to GitHub releases instead. main.js bin releases/ # Exclude sourcemaps *.map # obsidian data.json # Exclude macOS Finder (System Explorer) View States .DS_Store # Scratch files playground.md ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/.eslintrc: -------------------------------------------------------------------------------- ``` { "root": true, "parser": "@typescript-eslint/parser", "env": { "node": true }, "plugins": [ "@typescript-eslint" ], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended" ], "parserOptions": { "sourceType": "module" }, "rules": { "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], "@typescript-eslint/ban-ts-comment": "off", "no-prototype-builtins": "off", "@typescript-eslint/no-empty-function": "off" } } ``` -------------------------------------------------------------------------------- /packages/shared/.gitignore: -------------------------------------------------------------------------------- ``` # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore # Logs logs _.log npm-debug.log_ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Caches .cache # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Runtime data pids _.pid _.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* # IntelliJ based IDEs .idea # Finder (MacOS) folder config .DS_Store ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore # Logs logs _.log npm-debug.log_ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Caches .cache # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Runtime data pids _.pid _.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* # IntelliJ based IDEs .idea # Finder (MacOS) folder config .DS_Store # Scratch pad playground/ main.js bin/ docs/planning/ data.json cline_docs/temp/ ``` -------------------------------------------------------------------------------- /.clinerules: -------------------------------------------------------------------------------- ``` # Project Architecture ## Structure ``` packages/ ├── mcp-server/ # Server implementation ├── obsidian-plugin/ # Obsidian plugin └── shared/ # Shared utilities and types ``` ## Features - Self-contained modules in src/features/ with structure: ``` feature/ ├── components/ # Svelte UI components ├── services/ # Core business logic ├── constants/ # Feature-specific constants ├── types.ts # Types and interfaces ├── utils.ts # Helper functions └── index.ts # Public API & setup ``` - feature/index.ts exports a setup function: - `function setup(plugin: McpToolsPlugin): { success: true } | { success: false, error: string }` - Handle dependencies and state: - Check dependencies on setup - Use Svelte stores for UI state - Persist settings via Obsidian API - Clean up on unload/errors - Extend plugin settings: ```typescript // features/some-feature/types.ts declare module "obsidian" { interface McpToolsPluginSettings { featureName?: { setting1?: string; setting2?: boolean; }; } } ``` - Export UI components: ```typescript // index.ts export { default as FeatureSettings } from "./components/SettingsTab.svelte"; export * from "./constants"; export * from "./types"; ``` ## Error Handling - Return descriptive error messages - Log errors with full context - Clean up resources on failure - Use Obsidian Notice for user feedback - Catch and handle async errors - Format errors for client responses ## Type Safety - Use ArkType for runtime validation - Define types with inference: ```typescript const schema = type({ name: "string", required: "boolean?", config: { maxSize: "number", mode: "'strict'|'loose'", }, }); type Config = typeof schema.infer; ``` - Validate external data: ```typescript const result = schema(untrustedData); if (result instanceof type.errors) { throw new Error(result.summary); } ``` - Pipe transformations: ```typescript const transformed = type("string.json.parse") .pipe(searchSchema) .to(parametersSchema); ``` - Add descriptions for better errors: ```typescript type({ query: type("string>0").describe("Search text cannot be empty"), limit: type("number>0").describe("Result limit must be positive"), }); ``` ## Development - Write in TypeScript strict mode - Use Bun for building/testing - Test features in isolation ## Core Integrations - Obsidian API for vault access - Obsidian plugins - Local REST API for communication - Smart Connections for search - Templater for templates ## Coding Style - Prefer functional over OOP - Use pure functions when possible - Keep files focused on single responsibility - Use descriptive, action-oriented names - Place shared code in shared package - Keep components small and focused # Project Guidelines ## Documentation Requirements - Update relevant documentation in /docs when modifying features - Keep README.md in sync with new capabilities - Maintain changelog entries in CHANGELOG.md ## Task Summary Records When starting a task: - Create a new Markdown file in /cline_docs - Record the initial objective - Create a checklist of subtasks Maintain the task file: - Update the checklist after completing subtasks - Record what worked and didn't work When completing a task: - Summarize the task outcome - Verify the initial objective was completed - Record final insights ## Testing Standards - Unit tests required for business logic - Integration tests for API endpoints - E2E tests for critical user flows ``` -------------------------------------------------------------------------------- /packages/shared/README.md: -------------------------------------------------------------------------------- ```markdown # shared To install dependencies: ```bash bun install ``` To run: ```bash bun run index.ts ``` This project was created using `bun init` in bun v1.1.39. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. ``` -------------------------------------------------------------------------------- /packages/test-site/README.md: -------------------------------------------------------------------------------- ```markdown # sv Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). ## Creating a project If you're seeing this, you've probably already done this step. Congrats! ```bash # create a new project in the current directory npx sv create # create a new project in my-app npx sv create my-app ``` ## Developing Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: ```bash npm run dev # or start the server and open the app in a new browser tab npm run dev -- --open ``` ## Building To create a production version of your app: ```bash npm run build ``` You can preview the production build with `npm run preview`. > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/README.md: -------------------------------------------------------------------------------- ```markdown # MCP Tools for Obsidian - Plugin The Obsidian plugin component of MCP Tools, providing secure MCP server integration for accessing Obsidian vaults through Claude Desktop and other MCP clients. ## Features - **Secure Access**: All communication encrypted and authenticated through Local REST API - **Semantic Search**: Seamless integration with Smart Connections for context-aware search - **Template Support**: Execute Templater templates through MCP clients - **File Management**: Comprehensive vault access and management capabilities - **Security First**: Binary attestation and secure key management ## Requirements ### Required - Obsidian v1.7.7 or higher - [Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin ### Recommended - [Smart Connections](https://smartconnections.app/) for semantic search - [Templater](https://silentvoid13.github.io/Templater/) for template execution ## Development This plugin is part of the MCP Tools monorepo. For development: ```bash # Install dependencies bun install # Start development build with watch mode bun run dev # Create a production build bun run build # Link plugin to your vault for testing bun run link <path-to-vault-config-file> ``` ### Project Structure ``` src/ ├── features/ # Feature modules │ ├── core/ # Plugin initialization │ ├── mcp-server/ # Server management │ └── shared/ # Common utilities ├── main.ts # Plugin entry point └── shared/ # Shared types and utilities ``` ### Adding New Features 1. Create a new feature module in `src/features/` 2. Implement the feature's setup function 3. Add any UI components to the settings tab 4. Register the feature in `main.ts` ## Security This plugin follows strict security practices: - All server binaries are signed and include SLSA provenance - Communication is encrypted using Local REST API's TLS - API keys are stored securely using platform-specific methods - Server runs with minimal required permissions ## Contributing 1. Fork the repository 2. Create a feature branch 3. Follow the project's TypeScript and Svelte guidelines 4. Submit a pull request ## License [MIT License](LICENSE) ``` -------------------------------------------------------------------------------- /packages/mcp-server/README.md: -------------------------------------------------------------------------------- ```markdown # MCP Tools for Obsidian - Server 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. ## Features ### Resource Access - Read and write vault files via `note://` URIs - Access file metadata and frontmatter - Semantic search through Smart Connections - Template execution via Templater ### Security - Binary attestation with SLSA provenance - Encrypted communication via Local REST API - Platform-specific credential storage - Minimal required permissions ### Tools - File operations (create, read, update, delete) - Semantic search with filters - Template execution with parameters - Vault directory listing ## Installation The server is typically installed automatically through the Obsidian plugin. For manual installation: ```bash # Install dependencies bun install # Build the server bun run build ``` ```` ### Configuration Server configuration is managed through Claude Desktop's config file: On macOS: ```json // ~/Library/Application Support/Claude/claude_desktop_config.json { "mcpServers": { "obsidian-mcp-tools": { "command": "/path/to/mcp-server", "env": { "OBSIDIAN_API_KEY": "your-api-key" } } } } ``` ## Development ```bash # Start development server with auto-reload bun run dev # Run tests bun test # Build for all platforms bun run build:all # Use MCP Inspector for debugging bun run inspector ``` ### Project Structure ``` src/ ├── features/ # Feature modules │ ├── core/ # Server core │ ├── fetch/ # Web content fetching │ ├── local-rest-api/# API integration │ ├── prompts/ # Prompt handling │ └── templates/ # Template execution ├── shared/ # Shared utilities └── types/ # TypeScript types ``` ### Binary Distribution Server binaries are published with SLSA Provenance attestations. To verify a binary: ```bash gh attestation verify --owner jacksteamdev <binary> ``` This verifies: - Binary's SHA256 hash - Build origin from this repository - Compliance with SLSA Level 3 ## Protocol Implementation ### Resources - `note://` - Vault file access - `template://` - Template execution - `search://` - Semantic search ### Tools - `create_note` - Create new files - `update_note` - Modify existing files - `execute_template` - Run Templater templates - `semantic_search` - Smart search integration ## Contributing 1. Fork the repository 2. Create a feature branch 3. Add tests for new functionality 4. Update documentation 5. Submit a pull request See [CONTRIBUTING.md](../CONTRIBUTING.md) for detailed guidelines. ## Security For security issues, please: 1. **DO NOT** open a public issue 2. Email [[email protected]](mailto:[email protected]) 3. Follow responsible disclosure practices ## License [MIT License](LICENSE) ```` ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # MCP Tools for Obsidian [](https://github.com/jacksteamdev/obsidian-mcp-tools/releases/latest) [](https://github.com/jacksteamdev/obsidian-mcp-tools/actions) [](LICENSE) [Features](#features) | [Installation](#installation) | [Configuration](#configuration) | [Troubleshooting](#troubleshooting) | [Security](#security) | [Development](#development) | [Support](#support) > **🔄 Seeking Project Maintainers** > > 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. > > **Interested?** Join our [Discord community](https://discord.gg/q59pTrN9AA) or check our [maintainer requirements](CONTRIBUTING.md#maintainer-responsibilities). > > **Timeline**: Applications open until **September 15, 2025**. Selection by **September 30, 2025**. 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] This plugin consists of two parts: 1. An Obsidian plugin that adds MCP capabilities to your vault 2. A local MCP server that handles communication with AI applications 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] > **Privacy Note**: When using Claude Desktop with this plugin, your conversations with Claude are not used to train Anthropic's models by default. [^1] ## Features When connected to an MCP client like Claude Desktop, this plugin enables: - **Vault Access**: Allows AI assistants to read and reference your notes while maintaining your vault's security [^4] - **Semantic Search**: AI assistants can search your vault based on meaning and context, not just keywords [^5] - **Template Integration**: Execute Obsidian templates through AI interactions, with dynamic parameters and content generation [^6] 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. ## Prerequisites ### Required - [Obsidian](https://obsidian.md/) v1.7.7 or higher - [Claude Desktop](https://claude.ai/download) installed and configured - [Local REST API](https://github.com/coddingtonbear/obsidian-local-rest-api) plugin installed and configured with an API key ### Recommended - [Templater](https://silentvoid13.github.io/Templater/) plugin for enhanced template functionality - [Smart Connections](https://smartconnections.app/) plugin for semantic search capabilities ## Installation > [!Important] > 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. 1. Install the plugin from Obsidian's Community Plugins 2. Enable the plugin in Obsidian settings 3. Open the plugin settings 4. Click "Install Server" to download and configure the MCP server Clicking the install button will: - Download the appropriate MCP server binary for your platform - Configure Claude Desktop to use the server - Set up necessary permissions and paths ### Installation Locations - **Server Binary**: {vault}/.obsidian/plugins/obsidian-mcp-tools/bin/ - **Log Files**: - macOS: ~/Library/Logs/obsidian-mcp-tools - Windows: %APPDATA%\obsidian-mcp-tools\logs - Linux: ~/.local/share/obsidian-mcp-tools/logs ## Configuration After clicking the "Install Server" button in the plugin settings, the plugin will automatically: 1. Download the appropriate MCP server binary 2. Use your Local REST API plugin's API key 3. Configure Claude Desktop to use the MCP server 4. Set up appropriate paths and permissions 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. ## Troubleshooting If you encounter issues: 1. Check the plugin settings to verify: - All required plugins are installed - The server is properly installed - Claude Desktop is configured 2. Review the logs: - Open plugin settings - Click "Open Logs" under Resources - Look for any error messages or warnings 3. Common Issues: - **Server won't start**: Ensure Claude Desktop is running - **Connection errors**: Verify Local REST API plugin is configured - **Permission errors**: Try reinstalling the server ## Security ### Binary Distribution - All releases are built using GitHub Actions with reproducible builds - Binaries are signed and attested using SLSA provenance - Release workflows are fully auditable in the repository ### Runtime Security - The MCP server runs with minimal required permissions - All communication is encrypted - API keys are stored securely using platform-specific credential storage ### Binary Verification 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. To verify a binary using the GitHub CLI: 1. Install GitHub CLI: ```bash # macOS (Homebrew) brew install gh # Windows (Scoop) scoop install gh # Linux sudo apt install gh # Debian/Ubuntu ``` 2. Verify the binary: ```bash gh attestation verify --owner jacksteamdev <binary path or URL> ``` The verification will show: - The binary's SHA256 hash - Confirmation that it was built by this repository's GitHub Actions workflows - The specific workflow file and version tag that created it - Compliance with SLSA Level 3 build requirements This verification ensures the binary hasn't been tampered with and was built directly from this repository's source code. ### Reporting Security Issues Please report security vulnerabilities via our [security policy](SECURITY.md). Do not report security vulnerabilities in public issues. ## Development This project uses a monorepo structure with feature-based architecture. For detailed project architecture documentation, see [.clinerules](.clinerules). ### Using Cline 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. ### Workspace This project uses a [Bun](https://bun.sh/) workspace structure: ``` packages/ ├── mcp-server/ # Server implementation ├── obsidian-plugin/ # Obsidian plugin └── shared/ # Shared utilities and types ``` ### Building 1. Install dependencies: ```bash bun install ``` 2. Build all packages: ```bash bun run build ``` 3. For development: ```bash bun run dev ``` ### Requirements - [bun](https://bun.sh/) v1.1.42 or higher - TypeScript 5.0+ ## Contributing **Before contributing, please read our [Contributing Guidelines](CONTRIBUTING.md) including our community standards and behavioral expectations.** 1. Fork the repository 2. Create a feature branch 3. Make your changes 4. Run tests: ```bash bun test ``` 5. Submit a pull request We welcome genuine contributions but maintain strict community standards. Be respectful and constructive in all interactions. ## Support - 💬 [Join our Discord](https://discord.gg/q59pTrN9AA) for questions, discussions, and community support - [Open an issue](https://github.com/jacksteamdev/obsidian-mcp-tools/issues) for bug reports and feature requests **Please read our [Contributing Guidelines](CONTRIBUTING.md) before posting.** We maintain high community standards and have zero tolerance for toxic behavior. ## Changelog See [GitHub Releases](https://github.com/jacksteamdev/obsidian-mcp-tools/releases) for detailed changelog information. ## License [MIT License](LICENSE) ## Footnotes [^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) [^2]: For more information about the Model Context Protocol, see [MCP Introduction](https://modelcontextprotocol.io/introduction) [^3]: For a list of available MCP Clients, see [MCP Example Clients](https://modelcontextprotocol.io/clients) [^4]: Requires Obsidian plugin Local REST API [^5]: Requires Obsidian plugin Smart Connections [^6]: Requires Obsidian plugin Templater ``` -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- ```markdown # Security Policy ## Reporting a Vulnerability The MCP Tools for Obsidian team takes security vulnerabilities seriously. If you discover a security issue, please report it by emailing [[email protected]]. **Please do not report security vulnerabilities through public GitHub issues.** When reporting a vulnerability, please include: - Description of the issue - Steps to reproduce - Potential impact - Any suggested fixes (if you have them) 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. ## Disclosure Policy When we receive a security bug report, we will: 1. Confirm the problem and determine affected versions 2. Audit code to find any similar problems 3. Prepare fixes for all supported releases 4. Release new versions and notify users ## Binary Distribution Security MCP Tools for Obsidian uses several measures to ensure secure binary distribution: 1. **SLSA Provenance**: All binaries are built using GitHub Actions with [SLSA Level 3](https://slsa.dev) provenance attestation 2. **Reproducible Builds**: Our build process is deterministic and can be reproduced from source 3. **Verification**: Users can verify binary authenticity using: ```bash gh attestation verify --owner jacksteamdev <binary_path> ``` ## Runtime Security Model The MCP server operates with the following security principles: 1. **Minimal Permissions**: - Operates only in user space - Requires access only to: - Obsidian vault directory - Claude Desktop configuration - System logging directory 2. **API Security**: - All communication is encrypted - Input validation and sanitization 3. **Data Privacy**: - No telemetry collection - No external network calls except to Claude Desktop - All processing happens locally ## Dependencies We regularly monitor and update our dependencies for security vulnerabilities: - Automated security scanning with GitHub Dependabot - Regular dependency audits - Prompt patching of known vulnerabilities ## Security Update Policy - Critical vulnerabilities: Patch within 24 hours - High severity: Patch within 7 days - Other vulnerabilities: Address in next release ## Supported Versions We provide security updates for: - Current major version: Full support - Previous major version: Critical security fixes only ## Best Practices for Users 1. **Binary Verification**: - Always verify downloaded binaries using GitHub's attestation tools - Check release signatures and hashes - Download only from official GitHub releases 2. **Configuration**: - Use unique API keys - Regularly update to the latest version - Monitor plugin settings for unexpected changes 3. **Monitoring**: - Check logs for unusual activity - Review Claude Desktop configuration changes - Keep track of plugin updates ## Security Acknowledgments We would like to thank the following individuals and organizations for responsibly disclosing security issues: - [To be added as vulnerabilities are reported and fixed] ## License This security policy is licensed under [MIT License](LICENSE). ``` -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- ```markdown # Contributing to MCP Tools for Obsidian ## Community Standards 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. ### Unacceptable Behavior - Demanding features or fixes - Rude, dismissive, or condescending language - Entitlement or treating maintainers like paid support - Shaming contributors for mistakes or decisions - Aggressive or impatient language in issues or discussions ### Consequences **One strike policy**: Any toxic, demanding, or rude behavior results in an immediate ban from both the GitHub repository and Discord server. ### Before You Post Think before you post. Ask yourself: - Am I being respectful and constructive? - Would I talk this way to a volunteer helping me for free? - Am I treating maintainers like human beings, not paid support staff? **Remember**: We don't owe anyone anything. This is a gift to the community, and we expect contributors to act accordingly. ## Getting Help & Community - **Discord**: [Join our community](https://discord.gg/q59pTrN9AA) for discussions and support - **Issues**: Use GitHub issues for bug reports and feature requests (following our guidelines) - **Discussions**: Use GitHub Discussions for questions and general help ## Development Setup 1. **Prerequisites**: - [Bun](https://bun.sh/) v1.1.42 or higher - [Obsidian](https://obsidian.md/) v1.7.7 or higher - [Claude Desktop](https://claude.ai/download) for testing 2. **Clone and Setup**: ```bash git clone https://github.com/jacksteamdev/obsidian-mcp-tools.git cd obsidian-mcp-tools bun install ``` 3. **Development**: ```bash bun run dev # Development mode with watch bun run build # Production build bun test # Run tests ``` ## Project Architecture ### Documentation Resources - **Project architecture**: `/docs/project-architecture.md` - **Feature documentation**: `/docs/features/` - **AI-generated docs**: [DeepWiki](https://deepwiki.com/jacksteamdev/obsidian-mcp-tools) - **Coding standards**: `.clinerules` ### Monorepo Structure ``` packages/ ├── mcp-server/ # TypeScript MCP server implementation ├── obsidian-plugin/ # Obsidian plugin (TypeScript/Svelte) └── shared/ # Shared utilities and types ``` ### Feature-Based Architecture - Self-contained modules in `src/features/` with standardized structure - Each feature exports a setup function for initialization - Use ArkType for runtime type validation - Follow patterns documented in `.clinerules` ## Contributing Guidelines ### Submitting Issues **Before creating an issue**: - Search existing issues to avoid duplicates - Provide clear, detailed descriptions - Include system information and steps to reproduce - Be respectful and patient - remember this is volunteer work **Good issue example**: > **Bug Report**: MCP server fails to start on macOS 14.2 > > **Environment**: macOS 14.2, Obsidian 1.7.7, Claude Desktop 1.0.2 > > **Steps to reproduce**: > 1. Install plugin from Community Plugins > 2. Click "Install Server" in settings > 3. Server download completes but fails to start > > **Expected**: Server starts and connects to Claude > **Actual**: Error in logs: [paste error message] > > **Additional context**: Logs attached, willing to test fixes ### Pull Requests 1. **Fork the repository** and create a feature branch 2. **Follow the architecture patterns** described in `/docs/project-architecture.md` 3. **Write tests** for new functionality 4. **Test thoroughly**: - Local Obsidian vault integration - MCP server functionality - Claude Desktop connection 5. **Submit PR** with clear description of changes ### Code Standards - **TypeScript strict mode** required - **ArkType validation** for all external data - **Error handling** with descriptive messages - **Documentation** for public APIs - **Follow existing patterns** in `.clinerules` ## Release Process (Maintainers Only) ### Creating Releases 1. **Version bump**: `bun run version [patch|minor|major]` - Automatically updates `package.json`, `manifest.json`, and `versions.json` - Creates git commit and tag - Pushes to GitHub 2. **Automated build**: GitHub Actions handles: - Cross-platform binary compilation - SLSA provenance attestation - Release artifact upload - Security verification 3. **Release notes**: GitHub automatically generates release notes from PRs ### Maintainer Responsibilities - **Code review**: Review PRs for quality, security, and architecture compliance - **Issue triage**: Respond to issues and help users (when possible) - **Release management**: Create releases following security protocols - **Community management**: Enforce community standards - **Documentation**: Keep docs current and comprehensive ### Access Requirements - **GitHub**: "Maintain" or "Admin" access to repository - **Discord**: Moderator permissions for community management - **Time commitment**: 5-10 hours per week (15-20 during releases) ## Testing Guidelines ### Local Testing ```bash # Unit tests bun test # Integration testing with local vault # 1. Set up test Obsidian vault # 2. Install plugin locally: `bun run build:plugin` # 3. Test MCP server connection with Claude Desktop # 4. Verify all features work end-to-end ``` ## Security Considerations ### Binary Security - All binaries are SLSA-attested and cryptographically signed - Use `gh attestation verify --owner jacksteamdev <binary>` to verify integrity - Report security issues to [[email protected]](mailto:[email protected]) ### Development Security - **No secrets in code**: Use environment variables for API keys - **Input validation**: Use ArkType for all external data - **Minimal permissions**: MCP server runs with least required access - **Audit dependencies**: Regularly update and audit npm packages ## Resources - **GitHub Repository**: [jacksteamdev/obsidian-mcp-tools](https://github.com/jacksteamdev/obsidian-mcp-tools) - **Discord Community**: [Join here](https://discord.gg/q59pTrN9AA) - **Release History**: [GitHub Releases](https://github.com/jacksteamdev/obsidian-mcp-tools/releases) - **Security Policy**: [SECURITY.md](SECURITY.md) - **AI Documentation**: [DeepWiki](https://deepwiki.com/jacksteamdev/obsidian-mcp-tools) ## Questions? 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. **Remember**: Be respectful, be patient, and remember that everyone here is volunteering their time to help make this project better. ``` -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- ```toml [tools] bun = "latest" ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/fetch/services/index.ts: -------------------------------------------------------------------------------- ```typescript export * from "./markdown"; ``` -------------------------------------------------------------------------------- /packages/test-site/src/routes/+layout.ts: -------------------------------------------------------------------------------- ```typescript export const prerender = true; ``` -------------------------------------------------------------------------------- /packages/shared/src/index.ts: -------------------------------------------------------------------------------- ```typescript export * from "./logger"; export * from "./types"; ``` -------------------------------------------------------------------------------- /packages/test-site/postcss.config.js: -------------------------------------------------------------------------------- ```javascript export default { plugins: { tailwindcss: {}, autoprefixer: {} } }; ``` -------------------------------------------------------------------------------- /packages/test-site/src/lib/index.ts: -------------------------------------------------------------------------------- ```typescript // place files you want to import through the `$lib` alias in this folder. ``` -------------------------------------------------------------------------------- /packages/test-site/src/app.css: -------------------------------------------------------------------------------- ```css @import 'tailwindcss/base'; @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; ``` -------------------------------------------------------------------------------- /packages/test-site/src/routes/+layout.svelte: -------------------------------------------------------------------------------- ``` <script lang="ts"> import '../app.css'; let { children } = $props(); </script> {@render children()} ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/version/index.ts: -------------------------------------------------------------------------------- ```typescript import { version } from "../../../../../package.json" with { type: "json" }; export function getVersion() { return version; } ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/svelte.config.js: -------------------------------------------------------------------------------- ```javascript // @ts-check import { sveltePreprocess } from 'svelte-preprocess'; const config = { preprocess: sveltePreprocess() } export default config; ``` -------------------------------------------------------------------------------- /packages/test-site/vite.config.ts: -------------------------------------------------------------------------------- ```typescript import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()] }); ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/fetch/constants.ts: -------------------------------------------------------------------------------- ```typescript 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"; ``` -------------------------------------------------------------------------------- /packages/test-site/tailwind.config.ts: -------------------------------------------------------------------------------- ```typescript import type { Config } from 'tailwindcss'; export default { content: ['./src/**/*.{html,js,svelte,ts}'], theme: { extend: {} }, plugins: [] } satisfies Config; ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/index.ts: -------------------------------------------------------------------------------- ```typescript export * from "./formatMcpError"; export * from "./formatString"; export * from "./logger"; export * from "./makeRequest"; export * from "./parseTemplateParameters"; export * from "./ToolRegistry"; ``` -------------------------------------------------------------------------------- /packages/shared/src/types/index.ts: -------------------------------------------------------------------------------- ```typescript export * as LocalRestAPI from "./plugin-local-rest-api"; export * as SmartConnections from "./plugin-smart-connections"; export * as Templater from "./plugin-templater"; export * from "./prompts"; export * from "./smart-search"; ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/types.ts: -------------------------------------------------------------------------------- ```typescript declare module "obsidian" { interface McpToolsPluginSettings { version?: string; } interface Plugin { loadData(): Promise<McpToolsPluginSettings>; saveData(data: McpToolsPluginSettings): Promise<void>; } } export {}; ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/types/global.d.ts: -------------------------------------------------------------------------------- ```typescript declare global { namespace NodeJS { interface ProcessEnv { NODE_ENV: "development" | "production"; NODE_TLS_REJECT_UNAUTHORIZED: `${0 | 1}`; OBSIDIAN_API_KEY?: string; OBSIDIAN_USE_HTTP?: string; } } } export {}; ``` -------------------------------------------------------------------------------- /packages/test-site/src/app.d.ts: -------------------------------------------------------------------------------- ```typescript // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { namespace App { // interface Error {} // interface Locals {} // interface PageData {} // interface PageState {} // interface Platform {} } } export {}; ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/core/components/SettingsTab.svelte: -------------------------------------------------------------------------------- ``` <script lang="ts"> import { FeatureSettings as McpServerInstallSettings } from "src/features/mcp-server-install"; import type McpServerPlugin from "src/main"; export let plugin: McpServerPlugin; </script> <div class="settings-container"> <McpServerInstallSettings {plugin} /> </div> ``` -------------------------------------------------------------------------------- /packages/shared/package.json: -------------------------------------------------------------------------------- ```json { "name": "shared", "type": "module", "exports": { ".": "./src/index.ts" }, "module": "src/index.ts", "scripts": { "check": "tsc --noEmit" }, "dependencies": { "arktype": "^2.0.0-rc.30" }, "devDependencies": { "@types/bun": "latest" }, "peerDependencies": { "typescript": "^5.0.0" } } ``` -------------------------------------------------------------------------------- /packages/test-site/src/app.html: -------------------------------------------------------------------------------- ```html <!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> %sveltekit.head% </head> <body data-sveltekit-preload-data="hover"> <div style="display: contents">%sveltekit.body%</div> </body> </html> ``` -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- ```json { "id": "mcp-tools", "name": "MCP Tools", "version": "0.2.27", "minAppVersion": "0.15.0", "description": "Securely connect Claude Desktop to your vault with semantic search, templates, and file management capabilities.", "author": "Jack Steam", "authorUrl": "https://github.com/jacksteamdev", "fundingUrl": "https://github.com/sponsors/jacksteamdev", "isDesktopOnly": true } ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/logger.ts: -------------------------------------------------------------------------------- ```typescript import { createLogger } from "shared"; /** * The logger instance for the MCP server application. * This logger is configured with the "obsidian-mcp-tools" app name, writes to the "mcp-server.log" file, * and uses the "INFO" log level in production environments and "DEBUG" in development environments. */ export const logger = createLogger({ appName: "Claude", filename: "mcp-server-obsidian-mcp-tools.log", level: process.env.NODE_ENV === "production" ? "INFO" : "DEBUG", }); ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/utils/getFileSystemAdapter.ts: -------------------------------------------------------------------------------- ```typescript import { Plugin, FileSystemAdapter } from "obsidian"; /** * Gets the file system adapter for the given plugin. * * @param plugin - The plugin to get the file system adapter for. * @returns The file system adapter, or `undefined` if not found. */ export function getFileSystemAdapter( plugin: Plugin, ): FileSystemAdapter | { error: string } { const adapter = plugin.app.vault.adapter; if (adapter instanceof FileSystemAdapter) { return adapter; } return { error: "Unsupported platform" }; } ``` -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- ```json { "0.1.1": "0.15.0", "0.2.0": "0.15.0", "0.2.4": "0.15.0", "0.2.5": "0.15.0", "0.2.6": "0.15.0", "0.2.7": "0.15.0", "0.2.8": "0.15.0", "0.2.9": "0.15.0", "0.2.10": "0.15.0", "0.2.11": "0.15.0", "0.2.12": "0.15.0", "0.2.13": "0.15.0", "0.2.14": "0.15.0", "0.2.15": "0.15.0", "0.2.16": "0.15.0", "0.2.17": "0.15.0", "0.2.18": "0.15.0", "0.2.19": "0.15.0", "0.2.20": "0.15.0", "0.2.21": "0.15.0", "0.2.22": "0.15.0", "0.2.23": "0.15.0", "0.2.24": "0.15.0", "0.2.25": "0.15.0", "0.2.26": "0.15.0", "0.2.27": "0.15.0" } ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- ```json { "workbench.colorCustomizations": { "activityBar.background": "#32167B", "titleBar.activeBackground": "#451FAC", "titleBar.activeForeground": "#FAF9FE" }, "typescript.tsdk": "node_modules/typescript/lib", "logViewer.watch": [ "~/Library/Logs/obsidian-mcp-tools/mcp-server.log", "~/Library/Logs/obsidian-mcp-tools/obsidian-plugin.log", "~/Library/Logs/Claude/*.log" ], "svelte.plugin.svelte.compilerWarnings": { "a11y_click_events_have_key_events": "ignore", "a11y_missing_attribute": "ignore", } } ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- ```yaml blank_issues_enabled: false contact_links: - name: 💬 Discord Community url: https://discord.gg/q59pTrN9AA about: Join our Discord for questions, discussions, and community support - name: 📖 Contributing Guidelines url: https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/CONTRIBUTING.md about: Read our community standards and development guidelines before posting - name: 🔒 Security Issues url: https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/SECURITY.md about: Report security vulnerabilities privately via email ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/formatMcpError.ts: -------------------------------------------------------------------------------- ```typescript import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import { type } from "arktype"; export function formatMcpError(error: unknown) { if (error instanceof McpError) { return error; } if (error instanceof type.errors) { const message = error.summary; return new McpError(ErrorCode.InvalidParams, message); } if (type({ message: "string" }).allows(error)) { return new McpError(ErrorCode.InternalError, error.message); } return new McpError( ErrorCode.InternalError, "An unexpected error occurred", error, ); } ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/shared/logger.ts: -------------------------------------------------------------------------------- ```typescript import { createLogger, loggerConfigMorph, type InputLoggerConfig, } from "shared"; const isProd = process.env.NODE_ENV === "production"; export const LOGGER_CONFIG: InputLoggerConfig = { appName: "Claude", filename: "obsidian-plugin-mcp-tools.log", level: "DEBUG", }; export const { filename: FULL_LOGGER_FILENAME } = loggerConfigMorph.assert(LOGGER_CONFIG); /** * In production, we use the console. During development, the logger writes logs to a file in the same folder as the server log file. */ export const logger = isProd ? console : createLogger(LOGGER_CONFIG); ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "baseUrl": ".", "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", "target": "ES2018", "allowJs": true, "noEmit": true, "noImplicitAny": true, "moduleResolution": "bundler", "importHelpers": true, "isolatedModules": true, "strict": true, "skipLibCheck": true, "lib": ["DOM", "ES5", "ES6", "ES7"], "useDefineForClassFields": true, "verbatimModuleSyntax": true, "paths": { "$/*": ["./src/*"] } }, "include": ["src/*.ts", "bun.config.ts"], "exclude": ["node_modules", "playground"] } ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/utils/openFolder.ts: -------------------------------------------------------------------------------- ```typescript import { logger } from "$/shared/logger"; import { exec } from "child_process"; import { Notice, Platform } from "obsidian"; /** * Opens a folder in the system's default file explorer */ export function openFolder(folderPath: string): void { const command = Platform.isWin ? `start "" "${folderPath}"` : Platform.isMacOS ? `open "${folderPath}"` : `xdg-open "${folderPath}"`; exec(command, (error: Error | null) => { if (error) { const message = `Failed to open folder: ${error.message}`; logger.error(message, { folderPath, error }); new Notice(message); } }); } ``` -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { // Enable latest features "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", "jsx": "react-jsx", "allowJs": true, // Bundler mode "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, // Best practices "strict": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false } } ``` -------------------------------------------------------------------------------- /packages/test-site/tsconfig.json: -------------------------------------------------------------------------------- ```json { "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { "allowJs": true, "checkJs": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, "strict": true, "moduleResolution": "bundler" } // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files // // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes // from the referenced tsconfig.json - TypeScript does not merge them in } ``` -------------------------------------------------------------------------------- /packages/test-site/svelte.config.js: -------------------------------------------------------------------------------- ```javascript import adapter from '@sveltejs/adapter-static'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { // Consult https://svelte.dev/docs/kit/integrations // for more information about preprocessors preprocess: vitePreprocess(), kit: { // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. // If your environment is not supported, or you settled on a specific environment, switch out the adapter. // See https://svelte.dev/docs/kit/adapters for more information about adapters. adapter: adapter() } }; export default config; ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/index.ts: -------------------------------------------------------------------------------- ```typescript import { Plugin } from "obsidian"; import type { SetupResult } from "./types"; export async function setup(plugin: Plugin): Promise<SetupResult> { try { return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), }; } } // Re-export types and utilities that should be available to other features export { default as FeatureSettings } from "./components/McpServerInstallSettings.svelte"; export * from "./constants"; export { updateClaudeConfig } from "./services/config"; export { installMcpServer } from "./services/install"; export { uninstallServer } from "./services/uninstall"; export * from "./types"; ``` -------------------------------------------------------------------------------- /packages/mcp-server/tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { // Enable latest features "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force", "jsx": "react-jsx", "allowJs": true, // Bundler mode "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "noEmit": true, // Best practices "strict": true, "skipLibCheck": true, "noFallthroughCasesInSwitch": true, // Some stricter flags (disabled by default) "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false, "noErrorTruncation": true, "paths": { "$/*": ["./src/*"] } }, "include": ["src"] } ``` -------------------------------------------------------------------------------- /packages/test-site/eslint.config.js: -------------------------------------------------------------------------------- ```javascript import prettier from 'eslint-config-prettier'; import js from '@eslint/js'; import { includeIgnoreFile } from '@eslint/compat'; import svelte from 'eslint-plugin-svelte'; import globals from 'globals'; import { fileURLToPath } from 'node:url'; import ts from 'typescript-eslint'; const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); export default ts.config( includeIgnoreFile(gitignorePath), js.configs.recommended, ...ts.configs.recommended, ...svelte.configs['flat/recommended'], prettier, ...svelte.configs['flat/prettier'], { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, { files: ['**/*.svelte'], languageOptions: { parserOptions: { parser: ts.parser } } } ); ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "mcp-tools-for-obsidian", "version": "0.2.27", "private": true, "description": "Securely connect Claude Desktop to your Obsidian vault with semantic search, templates, and file management capabilities.", "tags": [ "obsidian", "plugin", "semantic search", "templates", "file management", "mcp", "model context protocol" ], "workspaces": [ "packages/*" ], "scripts": { "check": "bun --filter '*' check", "dev": "bun --filter '*' dev", "version": "bun scripts/version.ts", "release": "bun --filter '*' release", "zip": "bun --filter '*' zip" }, "devDependencies": { "npm-run-all": "^4.1.5" }, "patchedDependencies": { "[email protected]": "patches/[email protected]" }, "dependencies": { "caniuse-lite": "^1.0.30001724" } } ``` -------------------------------------------------------------------------------- /packages/mcp-server/scripts/install.ts: -------------------------------------------------------------------------------- ```typescript import { readFileSync, writeFileSync } from "fs"; import path from "path"; import os from "os"; import { which } from "bun"; function main() { const args = process.argv.slice(2); if (args.length < 1) { console.error("Usage: install.ts <OBSIDIAN_API_KEY>"); process.exit(1); } const apiKey = args[0]; const configPath = path.join( os.homedir(), "Library/Application Support/Claude/claude_desktop_config.json", ); const config = JSON.parse(readFileSync(configPath, "utf-8")); config.mcpServers["obsidian-mcp-server"] = { command: which("bun"), args: [path.resolve(__dirname, "../src/index.ts")], env: { OBSIDIAN_API_KEY: apiKey, }, }; writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8"); console.log("MCP Server added successfully."); } if (import.meta.main) { main(); } ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/constants/bundle-time.ts: -------------------------------------------------------------------------------- ```typescript import { type } from "arktype"; import { clean } from "semver"; const envVar = type({ GITHUB_DOWNLOAD_URL: "string.url", GITHUB_REF_NAME: type("string").pipe((ref) => clean(ref)), }); /** * Validates a set of environment variables at build time, such as the enpoint URL for GitHub release artifacts. * Better than define since the build fails if the environment variable is not set. * * @returns An object containing the build time constants. */ export function environmentVariables() { try { const { GITHUB_DOWNLOAD_URL, GITHUB_REF_NAME } = envVar.assert({ GITHUB_DOWNLOAD_URL: process.env.GITHUB_DOWNLOAD_URL, GITHUB_REF_NAME: process.env.GITHUB_REF_NAME, }); return { GITHUB_DOWNLOAD_URL, GITHUB_REF_NAME }; } catch (error) { console.error(`Failed to get environment variables:`, { error }); throw error; } } ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/constants/index.ts: -------------------------------------------------------------------------------- ```typescript import { environmentVariables } from "./bundle-time" with { type: "macro" }; export const { GITHUB_DOWNLOAD_URL, GITHUB_REF_NAME } = environmentVariables(); export const BINARY_NAME = { windows: "mcp-server.exe", macos: "mcp-server", linux: "mcp-server", } as const; export const CLAUDE_CONFIG_PATH = { macos: "~/Library/Application Support/Claude/claude_desktop_config.json", windows: "%APPDATA%\\Claude\\claude_desktop_config.json", linux: "~/.config/claude/config.json", } as const; export const LOG_PATH = { macos: "~/Library/Logs/obsidian-mcp-tools", windows: "%APPDATA%\\obsidian-mcp-tools\\logs", linux: "~/.local/share/obsidian-mcp-tools/logs", } as const; export const PLATFORM_TYPES = ["windows", "macos", "linux"] as const; export type Platform = (typeof PLATFORM_TYPES)[number]; export const ARCH_TYPES = ["x64", "arm64"] as const; export type Arch = (typeof ARCH_TYPES)[number]; ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/scripts/zip.ts: -------------------------------------------------------------------------------- ```typescript import { create } from "archiver"; import { createWriteStream } from "fs"; import fs from "fs-extra"; import { join, resolve } from "path"; import { version } from "../../../package.json" with { type: "json" }; async function zipPlugin() { const pluginDir = resolve(import.meta.dir, ".."); const releaseDir = join(pluginDir, "releases"); fs.ensureDirSync(releaseDir); const zipFilePath = join(releaseDir, `obsidian-plugin-${version}.zip`); const output = createWriteStream(zipFilePath); const archive = create("zip", { zlib: { level: 9 } }); archive.pipe(output); // Add the required files archive.file(join(pluginDir, "main.js"), { name: "main.js" }); archive.file(join(pluginDir, "manifest.json"), { name: "manifest.json" }); archive.file(join(pluginDir, "styles.css"), { name: "styles.css" }); await archive.finalize(); console.log("Plugin files zipped successfully!"); } zipPlugin().catch(console.error); ``` -------------------------------------------------------------------------------- /packages/shared/src/types/smart-search.ts: -------------------------------------------------------------------------------- ```typescript import { type } from "arktype"; import { SmartConnections } from "shared"; const searchRequest = type({ query: type("string>0").describe("A search phrase for semantic search"), "filter?": { "folders?": type("string[]").describe( 'An array of folder names to include. For example, ["Public", "Work"]', ), "excludeFolders?": type("string[]").describe( 'An array of folder names to exclude. For example, ["Private", "Archive"]', ), "limit?": type("number>0").describe( "The maximum number of results to return", ), }, }); export const jsonSearchRequest = type("string.json.parse").to(searchRequest); const searchResponse = type({ results: type({ path: "string", text: "string", score: "number", breadcrumbs: "string", }).array(), }); export type SearchResponse = typeof searchResponse.infer; export const searchParameters = type({ query: "string", filter: SmartConnections.SmartSearchFilter, }); ``` -------------------------------------------------------------------------------- /packages/test-site/package.json: -------------------------------------------------------------------------------- ```json { "name": "test-server", "private": true, "version": "0.0.1", "type": "module", "scripts": { "dev": "vite dev", "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --write .", "lint": "prettier --check . && eslint ." }, "devDependencies": { "@eslint/compat": "^1.2.3", "@eslint/js": "^9.17.0", "@sveltejs/adapter-static": "^3.0.6", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0", "autoprefixer": "^10.4.20", "eslint": "^9.7.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0", "globals": "^15.0.0", "prettier": "^3.3.2", "prettier-plugin-svelte": "^3.2.6", "prettier-plugin-tailwindcss": "^0.6.5", "svelte": "^5.0.0", "svelte-check": "^4.0.0", "tailwindcss": "^3.4.9", "typescript": "^5.0.0", "typescript-eslint": "^8.0.0", "vite": "^5.4.11" } } ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env bun import { logger } from "$/shared"; import { ObsidianMcpServer } from "./features/core"; import { getVersion } from "./features/version" with { type: "macro" }; async function main() { try { // Verify required environment variables const API_KEY = process.env.OBSIDIAN_API_KEY; if (!API_KEY) { throw new Error("OBSIDIAN_API_KEY environment variable is required"); } logger.debug("Starting MCP Tools for Obsidian server..."); const server = new ObsidianMcpServer(); await server.run(); logger.debug("MCP Tools for Obsidian server is running"); } catch (error) { logger.fatal("Failed to start server", { error: error instanceof Error ? error.message : String(error), }); await logger.flush(); throw error; } } if (process.argv.includes("--version")) { try { console.log(getVersion()); } catch (error) { console.error(`Error getting version: ${error}`); process.exit(1); } } else { main().catch((error) => { console.error(error); process.exit(1); }); } ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/formatString.ts: -------------------------------------------------------------------------------- ```typescript import { zip } from "radash"; /** * Formats a template string with the provided values, while preserving the original indentation. * This function is used to format error messages or other string templates that need to preserve * the original formatting. * * @param strings - An array of template strings. * @param values - The values to be interpolated into the template strings. * @returns The formatted string with the values interpolated. * * @example * const f`` */ export const f = (strings: TemplateStringsArray, ...values: any[]) => { const stack = { stack: "" }; Error.captureStackTrace(stack, f); // Get the first caller's line from the stack trace const stackLine = stack.stack.split("\n")[1]; // Extract column number using regex // This matches the column number at the end of the line like: "at filename:line:column" const columnMatch = stackLine.match(/:(\d+)$/); const columnNumber = columnMatch ? parseInt(columnMatch[1]) - 1 : 0; return zip( strings.map((s) => s.replace(new RegExp(`\n\s{${columnNumber}}`), "\n")), values, ) .flat() .join("") .trim(); }; ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/core/index.ts: -------------------------------------------------------------------------------- ```typescript import { mount, unmount } from "svelte"; import type { SetupResult } from "../mcp-server-install/types"; import SettingsTab from "./components/SettingsTab.svelte"; import { App, PluginSettingTab } from "obsidian"; import type McpToolsPlugin from "../../main"; export class McpToolsSettingTab extends PluginSettingTab { plugin: McpToolsPlugin; component?: { $set?: unknown; $on?: unknown; }; constructor(app: App, plugin: McpToolsPlugin) { super(app, plugin); this.plugin = plugin; } display(): void { const { containerEl } = this; containerEl.empty(); this.component = mount(SettingsTab, { target: containerEl, props: { plugin: this.plugin }, }); } hide(): void { this.component && unmount(this.component); } } export async function setup(plugin: McpToolsPlugin): Promise<SetupResult> { try { // Add settings tab to plugin plugin.addSettingTab(new McpToolsSettingTab(plugin.app, plugin)); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error), }; } } ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/package.json: -------------------------------------------------------------------------------- ```json { "name": "@obsidian-mcp-tools/obsidian-plugin", "description": "The Obsidian plugin component for MCP Tools, enabling secure connections between Obsidian and Claude Desktop through the Model Context Protocol (MCP).", "keywords": [ "MCP", "Claude", "Chat" ], "license": "MIT", "author": "Jack Steam", "type": "module", "main": "main.js", "scripts": { "build": "bun run check && bun bun.config.ts --prod", "check": "tsc --noEmit", "dev": "bun --watch run bun.config.ts --watch", "link": "bun scripts/link.ts", "release": "run-s build zip", "zip": "bun scripts/zip.ts" }, "dependencies": { "@types/fs-extra": "^11.0.4", "arktype": "^2.0.0-rc.30", "express": "^4.21.2", "fs-extra": "^11.2.0", "obsidian-local-rest-api": "^2.5.4", "radash": "^12.1.0", "rxjs": "^7.8.1", "semver": "^7.6.3", "shared": "workspace:*", "svelte": "^5.17.5", "svelte-preprocess": "^6.0.3" }, "devDependencies": { "@types/node": "^16.11.6", "@types/semver": "^7.5.8", "@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/parser": "5.29.0", "archiver": "^7.0.1", "obsidian": "latest", "tslib": "2.4.0", "typescript": "^5.7.2" } } ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/smart-connections/index.ts: -------------------------------------------------------------------------------- ```typescript import { makeRequest, type ToolRegistry } from "$/shared"; import { type } from "arktype"; import { LocalRestAPI } from "shared"; export function registerSmartConnectionsTools(tools: ToolRegistry) { tools.register( type({ name: '"search_vault_smart"', arguments: { query: type("string>0").describe("A search phrase for semantic search"), "filter?": { "folders?": type("string[]").describe( 'An array of folder names to include. For example, ["Public", "Work"]', ), "excludeFolders?": type("string[]").describe( 'An array of folder names to exclude. For example, ["Private", "Archive"]', ), "limit?": type("number>0").describe( "The maximum number of results to return", ), }, }, }).describe("Search for documents semantically matching a text string."), async ({ arguments: args }) => { const data = await makeRequest( LocalRestAPI.ApiSmartSearchResponse, `/search/smart`, { method: "POST", body: JSON.stringify(args), }, ); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; }, ); } ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/types.ts: -------------------------------------------------------------------------------- ```typescript import type { Templater, SmartConnections } from "shared"; export interface SetupResult { success: boolean; error?: string; } export interface DownloadProgress { percentage: number; bytesReceived: number; totalBytes: number; } export interface InstallationStatus { state: | "no api key" | "not installed" | "installed" | "installing" | "outdated" | "uninstalling" | "error"; error?: string; dir?: string; path?: string; versions: { plugin?: string; server?: string; }; } export interface InstallPathInfo { /** The install directory path with all symlinks resolved */ dir: string; /** The install filepath with all symlinks resolved */ path: string; /** The platform-specific filename */ name: string; /** The symlinked install path, if symlinks were found */ symlinked?: string; } // Augment Obsidian's App type to include plugins declare module "obsidian" { interface App { plugins: { plugins: { ["obsidian-local-rest-api"]?: { settings?: { apiKey?: string; }; }; ["smart-connections"]?: { env?: SmartConnections.SmartSearch; } & Plugin; ["templater-obsidian"]?: { templater?: Templater.ITemplater; }; }; }; } } ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- ```markdown --- name: ✨ Feature Request about: Suggest a new feature or enhancement title: "[FEATURE] " labels: ["enhancement"] assignees: [] --- <!-- 🚨 READ FIRST: https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/CONTRIBUTING.md This is a FREE project maintained by volunteers. Be respectful and patient. Rude, demanding, or entitled behavior results in immediate bans. For discussions about ideas, use Discord: https://discord.gg/q59pTrN9AA --> ## Feature Description **Clear, concise description of the proposed feature** ## Use Case & Motivation **What problem does this solve? How would you use this feature?** ## Proposed Solution **How do you envision this working?** ## Alternatives Considered **What other approaches have you considered?** ## Implementation Notes <!-- Optional: Technical details if you have ideas about implementation - Which package would this affect? (mcp-server, obsidian-plugin, shared) - Any specific Obsidian plugins this would integrate with? - MCP protocol considerations? --> ## Maintainer Transition Note **This project is currently transitioning to new maintainers. Feature requests will be evaluated by the new maintainer team based on:** - Alignment with project goals - Implementation complexity - Community benefit - Available development time --- **Remember**: This is volunteer work. Suggestions are welcome, but demands are not. Be patient and respectful. ``` -------------------------------------------------------------------------------- /docs/features/prompt-requirements.md: -------------------------------------------------------------------------------- ```markdown # Prompt Feature Implementation Guide ## Overview Add functionality to load and execute prompts stored as markdown files in Obsidian. ## Implementation Areas ### 1. MCP Server Add prompt management: - List prompts from Obsidian's "Prompts" folder - Parse frontmatter for prompt metadata - Validate prompt arguments #### Schemas ```typescript interface PromptMetadata { name: string; description?: string; arguments?: { name: string; description?: string; required?: boolean; }[]; } interface ExecutePromptParams { name: string; arguments: Record<string, string>; createFile?: boolean; targetPath?: string; } ``` ### 2. Obsidian Plugin Add new endpoint `/prompts/execute`: ```typescript // Add to plugin-apis.ts export const loadTemplaterAPI = loadPluginAPI( () => app.plugins.plugins["templater-obsidian"]?.templater ); // Add to main.ts this.localRestApi .addRoute("/prompts/execute") .post(this.handlePromptExecution.bind(this)); ``` ### 3. OpenAPI Updates Add to openapi.yaml: ```yaml /prompts/execute: post: summary: Execute a prompt template requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ExecutePromptParams" responses: 200: description: Prompt executed successfully content: text/plain: schema: type: string ``` ``` -------------------------------------------------------------------------------- /packages/mcp-server/package.json: -------------------------------------------------------------------------------- ```json { "name": "@obsidian-mcp-tools/mcp-server", "description": "A secure MCP server implementation that provides standardized access to Obsidian vaults through the Model Context Protocol.", "type": "module", "module": "src/index.ts", "scripts": { "dev": "bun build ./src/index.ts --watch --compile --outfile ../../bin/mcp-server", "build": "bun build ./src/index.ts --compile --outfile dist/mcp-server", "build:linux": "bun build --compile --minify --sourcemap --target=bun-linux-x64-baseline ./src/index.ts --outfile dist/mcp-server-linux", "build:mac-arm64": "bun build --compile --minify --sourcemap --target=bun-darwin-arm64 ./src/index.ts --outfile dist/mcp-server-macos-arm64", "build:mac-x64": "bun build --compile --minify --sourcemap --target=bun-darwin-x64 ./src/index.ts --outfile dist/mcp-server-macos-x64", "build:windows": "bun build --compile --minify --sourcemap --target=bun-windows-x64-baseline ./src/index.ts --outfile dist/mcp-server-windows", "check": "tsc --noEmit", "inspector": "npx @modelcontextprotocol/inspector bun src/index.ts", "release": "run-s build:*", "setup": "bun run ./scripts/install.ts", "test": "bun test ./src/**/*.test.ts" }, "dependencies": { "@modelcontextprotocol/sdk": "1.0.4", "acorn": "^8.14.0", "acorn-walk": "^8.3.4", "arktype": "2.0.0-rc.30", "radash": "^12.1.0", "shared": "workspace:*", "turndown": "^7.2.0", "zod": "^3.24.1" }, "devDependencies": { "@types/bun": "latest", "@types/turndown": "^5.0.5", "prettier": "^3.4.2", "typescript": "^5.3.3" } } ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- ```markdown --- name: 🐛 Bug Report about: Report a technical issue or unexpected behavior title: "[BUG] " labels: ["bug"] assignees: [] --- <!-- 🚨 READ FIRST: https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/CONTRIBUTING.md This is a FREE project maintained by volunteers. Be respectful and patient. Rude, demanding, or entitled behavior results in immediate bans. For questions or general help, use Discord: https://discord.gg/q59pTrN9AA --> ## Bug Description **Clear, concise description of the bug** ## Environment - **OS**: (e.g., macOS 14.2, Windows 11, Ubuntu 22.04) - **Obsidian version**: (e.g., 1.7.7) - **Claude Desktop version**: (e.g., 1.0.2) - **Plugin version**: (e.g., 0.2.23) - **Required plugins status**: - [ ] Local REST API installed and configured - [ ] Smart Connections installed (if using semantic search) - [ ] Templater installed (if using templates) ## Steps to Reproduce 1. 2. 3. ## Expected Behavior **What should happen** ## Actual Behavior **What actually happens** ## Error Messages/Logs <!-- To access logs: 1. Open plugin settings 2. Click "Open Logs" under Resources 3. Copy relevant error messages --> ``` Paste error messages or log excerpts here ``` ## Additional Context <!-- - Screenshots (if helpful) - Vault setup details - Recent changes to your setup - Other plugins that might conflict --> ## Troubleshooting Attempted <!-- What have you already tried? --> - [ ] Restarted Obsidian - [ ] Restarted Claude Desktop - [ ] Reinstalled the MCP server via plugin settings - [ ] Checked Local REST API is running - [ ] Other: --- **Remember**: This is volunteer work. Be patient and respectful. We'll help when we can. ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/scripts/link.ts: -------------------------------------------------------------------------------- ```typescript import { symlinkSync, existsSync, mkdirSync } from "fs"; import { join, resolve } from "node:path"; /** * This development script creates a symlink to the plugin in the Obsidian vault's plugin directory. This allows you to * develop the plugin in the repository and see the changes in Obsidian without having to manually copy the files. * * This function is not included in the plugin itself. It is only used to set up local development. * * Usage: `bun scripts/link.ts <path_to_obsidian_vault>` * @returns {Promise<void>} */ async function main() { const args = process.argv.slice(2); if (args.length < 1) { console.error( "Usage: bun scripts/link.ts <path_to_obsidian_vault_config_folder>", ); process.exit(1); } const vaultConfigPath = args[0]; const projectRootDirectory = resolve(__dirname, "../../.."); const pluginManifestPath = resolve(projectRootDirectory, "manifest.json"); const pluginsDirectoryPath = join(vaultConfigPath, "plugins"); const file = Bun.file(pluginManifestPath); const manifest = await file.json(); const pluginName = manifest.id; console.log( `Creating symlink to ${projectRootDirectory} for plugin ${pluginName} in ${pluginsDirectoryPath}`, ); if (!existsSync(pluginsDirectoryPath)) { mkdirSync(pluginsDirectoryPath, { recursive: true }); } const targetPath = join(pluginsDirectoryPath, pluginName); if (existsSync(targetPath)) { console.log("Symlink already exists."); return; } symlinkSync(projectRootDirectory, targetPath, "dir"); console.log("Symlink created successfully."); console.log( "Obsidian plugin linked for local development. Please restart Obsidian.", ); } main().catch(console.error); ``` -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- ```markdown --- name: ❓ Question or Help about: Get help with setup, usage, or general questions title: "[QUESTION] " labels: ["question"] assignees: [] --- <!-- 🚨 REDIRECT TO DISCORD: https://discord.gg/q59pTrN9AA Questions and general help requests should be posted in our Discord community, not as GitHub issues. You'll get faster responses and can engage with other users. GitHub issues are for: ✅ Bug reports (technical problems) ✅ Feature requests (new functionality) ✅ Documentation issues Discord is for: 💬 Setup help and troubleshooting 💬 Usage questions 💬 General discussions 💬 Community support READ: https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/CONTRIBUTING.md --> ## Please Use Discord Instead **For questions and help, please join our Discord community:** 🔗 **https://discord.gg/q59pTrN9AA** **Benefits of using Discord:** - ✅ Faster responses from community members - ✅ Real-time discussion and follow-up - ✅ Help from other users with similar setups - ✅ Less formal environment for troubleshooting - ✅ Doesn't clutter the issue tracker ## Before Posting Anywhere **Please check these resources first:** - 📖 [README](https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/README.md) - Setup and usage instructions - 🏗️ [DeepWiki](https://deepwiki.com/jacksteamdev/obsidian-mcp-tools) - AI-generated documentation - 🔒 [Security](https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/SECURITY.md) - Binary verification and security info - 📋 [Contributing](https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/CONTRIBUTING.md) - Community standards --- **Remember**: Be respectful when asking for help. We're all volunteers here. **This issue will be closed and redirected to Discord.** ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/parseTemplateParameters.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, test } from "bun:test"; import { parseTemplateParameters } from "./parseTemplateParameters"; import { PromptParameterSchema } from "shared"; describe("parseTemplateParameters", () => { test("returns empty array for content without parameters", () => { const content = "No parameters here"; const result = parseTemplateParameters(content); PromptParameterSchema.array().assert(result); expect(result).toEqual([]); }); test("parses single parameter without description", () => { const content = '<% tp.user.promptArg("name") %>'; const result = parseTemplateParameters(content); PromptParameterSchema.array().assert(result); expect(result).toEqual([{ name: "name" }]); }); test("parses single parameter with description", () => { const content = '<% tp.user.promptArg("name", "Enter your name") %>'; const result = parseTemplateParameters(content); PromptParameterSchema.array().assert(result); expect(result).toEqual([{ name: "name", description: "Enter your name" }]); }); test("parses multiple parameters", () => { const content = ` <% tp.user.promptArg("name", "Enter your name") %> <% tp.user.promptArg("age", "Enter your age") %> `; const result = parseTemplateParameters(content); PromptParameterSchema.array().assert(result); expect(result).toEqual([ { name: "name", description: "Enter your name" }, { name: "age", description: "Enter your age" }, ]); }); test("ignores invalid template syntax", () => { const content = ` <% invalid.syntax %> <% tp.user.promptArg("name", "Enter your name") %> `; const result = parseTemplateParameters(content); PromptParameterSchema.array().assert(result); expect(result).toEqual([{ name: "name", description: "Enter your name" }]); }); }); ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/templates/index.ts: -------------------------------------------------------------------------------- ```typescript import { formatMcpError, makeRequest, parseTemplateParameters, type ToolRegistry, } from "$/shared"; import { type } from "arktype"; import { buildTemplateArgumentsSchema, LocalRestAPI } from "shared"; export function registerTemplaterTools(tools: ToolRegistry) { tools.register( type({ name: '"execute_template"', arguments: LocalRestAPI.ApiTemplateExecutionParams.omit("createFile").and( { // should be boolean but the MCP client returns a string "createFile?": type("'true'|'false'"), }, ), }).describe("Execute a Templater template with the given arguments"), async ({ arguments: args }) => { // Get prompt content const data = await makeRequest( LocalRestAPI.ApiVaultFileResponse, `/vault/${args.name}`, { headers: { Accept: LocalRestAPI.MIME_TYPE_OLRAPI_NOTE_JSON }, }, ); // Validate prompt arguments const templateParameters = parseTemplateParameters(data.content); const validArgs = buildTemplateArgumentsSchema(templateParameters)( args.arguments, ); if (validArgs instanceof type.errors) { throw formatMcpError(validArgs); } const templateExecutionArgs: { name: string; arguments: Record<string, string>; createFile: boolean; targetPath?: string; } = { name: args.name, arguments: validArgs, createFile: args.createFile === "true", targetPath: args.targetPath, }; // Process template through Templater plugin const response = await makeRequest( LocalRestAPI.ApiTemplateExecutionResponse, "/templates/execute", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(templateExecutionArgs), }, ); return { content: [{ type: "text", text: JSON.stringify(response, null, 2) }], }; }, ); } ``` -------------------------------------------------------------------------------- /packages/shared/src/types/prompts.ts: -------------------------------------------------------------------------------- ```typescript import { Type, type } from "arktype"; /** * A Templater user function that retrieves the value of the specified argument from the `params.arguments` object. In this implementation, all arguments are optional. * * @param argName - The name of the argument to retrieve. * @param argDescription - A description of the argument. * @returns The value of the specified argument. * * @example * ```markdown * <% tp.mcpTools.prompt("argName", "Argument description") %> * ``` */ export interface PromptArgAccessor { (argName: string, argDescription?: string): string; } export const PromptParameterSchema = type({ name: "string", "description?": "string", "required?": "boolean", }); export type PromptParameter = typeof PromptParameterSchema.infer; export const PromptMetadataSchema = type({ name: "string", "description?": type("string").describe("Description of the prompt"), "arguments?": PromptParameterSchema.array(), }); export type PromptMetadata = typeof PromptMetadataSchema.infer; export const PromptTemplateTag = type("'mcp-tools-prompt'"); export const PromptFrontmatterSchema = type({ tags: type("string[]").narrow((arr) => arr.some(PromptTemplateTag.allows)), "description?": type("string"), }); export type PromptFrontmatter = typeof PromptFrontmatterSchema.infer; export const PromptValidationErrorSchema = type({ type: "'MISSING_REQUIRED_ARG'|'INVALID_ARG_VALUE'", message: "string", "argumentName?": "string", }); export type PromptValidationError = typeof PromptValidationErrorSchema.infer; export const PromptExecutionResultSchema = type({ content: "string", "errors?": PromptValidationErrorSchema.array(), }); export type PromptExecutionResult = typeof PromptExecutionResultSchema.infer; export function buildTemplateArgumentsSchema( args: PromptParameter[], ): Type<Record<string, "string" | "string?">, {}> { return type( Object.fromEntries( args.map((arg) => [arg.name, arg.required ? "string" : "string?"]), ) as Record<string, "string" | "string?">, ); } ``` -------------------------------------------------------------------------------- /scripts/version.ts: -------------------------------------------------------------------------------- ```typescript import { $ } from "bun"; import { readFileSync, writeFileSync } from "fs"; // Check for uncommitted changes const status = await $`git status --porcelain`.quiet(); if (!!status.text() && !process.env.FORCE) { console.error( "There are uncommitted changes. Commit them before releasing or run with FORCE=true.", ); process.exit(1); } // Check if on main branch const currentBranch = (await $`git rev-parse --abbrev-ref HEAD`.quiet()) .text() .trim(); if (currentBranch !== "main" && !process.env.FORCE) { console.error( "Not on main branch. Switch to main before releasing or run with FORCE=true.", ); process.exit(1); } // Bump project version const semverPart = Bun.argv[3] || "patch"; const json = await Bun.file("./package.json").json(); const [major, minor, patch] = json.version.split(".").map((s) => parseInt(s)); json.version = bump([major, minor, patch], semverPart); await Bun.write("./package.json", JSON.stringify(json, null, 2) + "\n"); // Update manifest.json with new version and get minAppVersion const pluginManifestPath = "./manifest.json"; const pluginManifest = await Bun.file(pluginManifestPath).json(); const { minAppVersion } = pluginManifest; pluginManifest.version = json.version; await Bun.write( pluginManifestPath, JSON.stringify(pluginManifest, null, 2) + "\n", ); // Update versions.json with target version and minAppVersion from manifest.json const pluginVersionsPath = "./versions.json"; let versions = JSON.parse(readFileSync(pluginVersionsPath, "utf8")); versions[json.version] = minAppVersion; writeFileSync(pluginVersionsPath, JSON.stringify(versions, null, "\t") + "\n"); // Commit, tag and push await $`git add package.json ${pluginManifestPath} ${pluginVersionsPath}`; await $`git commit -m ${json.version}`; await $`git tag ${json.version}`; await $`git push`; await $`git push origin ${json.version}`; function bump(semver: [number, number, number], semverPart = "patch") { switch (semverPart) { case "major": semver[0]++; semver[1] = 0; semver[2] = 0; break; case "minor": semver[1]++; semver[2] = 0; break; case "patch": semver[2]++; break; default: throw new Error(`Invalid semver part: ${semverPart}`); } return semver.join("."); } ``` -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- ```markdown <!-- 🚨 READ FIRST: https://github.com/jacksteamdev/obsidian-mcp-tools/blob/main/CONTRIBUTING.md This is a FREE project maintained by volunteers. Be respectful and patient. Rude, demanding, or entitled behavior results in immediate bans. Join Discord for discussions: https://discord.gg/q59pTrN9AA --> ## Pull Request Description **Clear description of what this PR changes and why** ## Type of Change - [ ] 🐛 Bug fix (non-breaking change that fixes an issue) - [ ] ✨ New feature (non-breaking change that adds functionality) - [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] 📚 Documentation update - [ ] 🔧 Internal/tooling change ## Testing **How has this been tested?** - [ ] Local Obsidian vault testing - [ ] MCP server functionality verified - [ ] Claude Desktop integration tested - [ ] Cross-platform testing (if applicable) ## Architecture Compliance - [ ] Follows feature-based architecture patterns (see `/docs/project-architecture.md`) - [ ] Uses ArkType for runtime validation where applicable - [ ] Implements proper error handling - [ ] Includes setup function for new features - [ ] Follows coding standards in `.clinerules` ## Checklist - [ ] My code follows the project's coding standards - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to documentation (if applicable) - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes ## Security Considerations - [ ] No hardcoded secrets or API keys - [ ] Input validation implemented where needed - [ ] No new security vulnerabilities introduced - [ ] Follows minimum permission principles ## Additional Context <!-- - Screenshots (if UI changes) - Performance considerations - Backward compatibility notes - Related issues or PRs --> ## For Maintainers **Review checklist:** - [ ] Code quality and architecture compliance - [ ] Security review completed - [ ] Tests adequate and passing - [ ] Documentation updated as needed - [ ] Ready for release --- **Remember**: This is volunteer work. Be patient during the review process. ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/core/index.ts: -------------------------------------------------------------------------------- ```typescript import { logger, type ToolRegistry, ToolRegistryClass } from "$/shared"; import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { registerFetchTool } from "../fetch"; import { registerLocalRestApiTools } from "../local-rest-api"; import { setupObsidianPrompts } from "../prompts"; import { registerSmartConnectionsTools } from "../smart-connections"; import { registerTemplaterTools } from "../templates"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; export class ObsidianMcpServer { private server: Server; private tools: ToolRegistry; constructor() { this.server = new Server( { name: "obsidian-mcp-tools", version: "0.1.0", }, { capabilities: { tools: {}, prompts: {}, }, }, ); this.tools = new ToolRegistryClass(); this.setupHandlers(); // Error handling this.server.onerror = (error) => { logger.error("Server error", { error }); console.error("[MCP Tools Error]", error); }; process.on("SIGINT", async () => { await this.server.close(); process.exit(0); }); } private setupHandlers() { setupObsidianPrompts(this.server); registerFetchTool(this.tools, this.server); registerLocalRestApiTools(this.tools, this.server); registerSmartConnectionsTools(this.tools); registerTemplaterTools(this.tools); this.server.setRequestHandler(ListToolsRequestSchema, this.tools.list); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { logger.debug("Handling request", { request }); const response = await this.tools.dispatch(request.params, { server: this.server, }); logger.debug("Request handled", { response }); return response; }); } async run() { logger.debug("Starting server..."); const transport = new StdioServerTransport(); try { await this.server.connect(transport); logger.debug("Server started successfully"); } catch (err) { logger.fatal("Failed to start server", { error: err instanceof Error ? err.message : String(err), }); process.exit(1); } } } ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/parseTemplateParameters.ts: -------------------------------------------------------------------------------- ```typescript import { parse } from "acorn"; import { simple } from "acorn-walk"; import { type } from "arktype"; import type { PromptParameter } from "shared"; import { logger } from "./logger"; const CallExpressionSchema = type({ callee: { type: "'MemberExpression'", object: { type: "'MemberExpression'", object: { name: "'tp'" }, property: { name: "'mcpTools'" }, }, property: { name: "'prompt'" }, }, arguments: type({ type: "'Literal'", value: "string" }).array(), }); /** * Parses template arguments from the given content string. * * The function looks for template argument tags in the content string, which are * in the format `<% tp.mcpTools.prompt("name", "description") %>`, and extracts * the name and description of each argument. The extracted arguments are * returned as an array of `PromptArgument` objects. * * @param content - The content string to parse for template arguments. * @returns An array of `PromptArgument` objects representing the extracted * template arguments. */ export function parseTemplateParameters(content: string): PromptParameter[] { /** * Regular expressions for template tags. * The tags are in the format `<% tp.mcpTools.prompt("name", "description") %>` * and may contain additional modifiers. */ const TEMPLATER_START_TAG = /<%[*-_]*/g; const TEMPLATER_END_TAG = /[-_]*%>/g; // Split content by template tags const parts = content.split(TEMPLATER_START_TAG); const parameters: PromptParameter[] = []; for (const part of parts) { if (!TEMPLATER_END_TAG.test(part)) continue; const code = part.split(TEMPLATER_END_TAG)[0].trim(); try { // Parse the extracted code with AST const ast = parse(code, { ecmaVersion: "latest", sourceType: "module", }); simple(ast, { CallExpression(node) { if (CallExpressionSchema.allows(node)) { const argName = node.arguments[0].value; const argDescription = node.arguments[1]?.value; parameters.push({ name: argName, ...(argDescription ? { description: argDescription } : {}), }); } }, }); } catch (error) { logger.error("Error parsing code", { code, error }); continue; } } return parameters; } ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml name: Release on: push: tags: - "*" branches: - main jobs: release: if: github.ref_type == 'tag' runs-on: ubuntu-latest permissions: contents: write id-token: write attestations: write steps: - uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Create Release id: create_release uses: softprops/action-gh-release@v1 with: generate_release_notes: true draft: false prerelease: false - name: Install Dependencies run: bun install --frozen-lockfile - name: Run Release Script env: GITHUB_DOWNLOAD_URL: ${{ github.server_url }}/${{ github.repository }}/releases/download/${{ github.ref_name }} GITHUB_REF_NAME: ${{ github.ref_name }} run: bun run release - name: Zip Release Artifacts run: bun run zip - name: Generate artifact attestation for MCP server binaries uses: actions/attest-build-provenance@v2 with: subject-path: "packages/mcp-server/dist/*" - name: Get existing release body id: get_release_body uses: actions/github-script@v7 with: result-encoding: string # This tells the action to return a raw string script: | const release = await github.rest.repos.getRelease({ owner: context.repo.owner, repo: context.repo.repo, release_id: ${{ steps.create_release.outputs.id }} }); return release.data.body || ''; - name: Upload Release Artifacts env: GH_WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} uses: ncipollo/release-action@v1 with: allowUpdates: true omitName: true tag: ${{ github.ref_name }} artifacts: "packages/obsidian-plugin/releases/obsidian-plugin-*.zip,main.js,manifest.json,styles.css,packages/mcp-server/dist/*" body: | ${{ steps.get_release_body.outputs.result }} --- ✨ This release includes attested build artifacts. 📝 View attestation details in the [workflow run](${{ env.GH_WORKFLOW_URL }}) ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/fetch/services/markdown.test.ts: -------------------------------------------------------------------------------- ```typescript import { describe, expect, test } from "bun:test"; import { convertHtmlToMarkdown } from "./markdown"; describe("convertHtmlToMarkdown", () => { const baseUrl = "https://example.com/blog/post"; test("converts basic HTML to Markdown", () => { const html = "<h1>Hello</h1><p>This is a test</p>"; const result = convertHtmlToMarkdown(html, baseUrl); expect(result).toBe("# Hello\n\nThis is a test"); }); test("resolves relative URLs in links", () => { const html = '<a href="/about">About</a>'; const result = convertHtmlToMarkdown(html, baseUrl); expect(result).toBe("[About](https://example.com/about)"); }); test("resolves relative URLs in images", () => { const html = '<img src="/images/test.png" alt="Test">'; const result = convertHtmlToMarkdown(html, baseUrl); expect(result).toBe(""); }); test("removes data URL images", () => { const html = '<img src="data:image/png;base64,abc123" alt="Test">'; const result = convertHtmlToMarkdown(html, baseUrl); expect(result).toBe(""); }); test("keeps absolute URLs unchanged", () => { const html = '<a href="https://other.com/page">Link</a>'; const result = convertHtmlToMarkdown(html, baseUrl); expect(result).toBe("[Link](https://other.com/page)"); }); test("extracts article content when present", () => { const html = ` <header>Skip this</header> <article> <h1>Keep this</h1> <p>And this</p> </article> <footer>Skip this too</footer> `; const result = convertHtmlToMarkdown(html, baseUrl); expect(result).toBe("# Keep this\n\nAnd this"); }); test("extracts nested article content", () => { const html = ` <div> <header>Skip this</header> <article> <h1>Keep this</h1> <p>And this</p> </article> <footer>Skip this too</footer> </div> `; const result = convertHtmlToMarkdown(html, baseUrl); expect(result).toBe("# Keep this\n\nAnd this"); }); test("removes script and style elements", () => { const html = ` <div> <script>alert('test');</script> <p>Keep this</p> <style>body { color: red; }</style> </div> `; const result = convertHtmlToMarkdown(html, baseUrl); expect(result).toBe("Keep this"); }); }); ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/fetch/services/markdown.ts: -------------------------------------------------------------------------------- ```typescript import { logger } from "$/shared"; import TurndownService from "turndown"; /** * Resolves a URL path relative to a base URL. * * @param base - The base URL to use for resolving relative paths. * @param path - The URL path to be resolved. * @returns The resolved absolute URL. */ function resolveUrl(base: string, path: string): string { // Return path if it's already absolute if (path.startsWith("http://") || path.startsWith("https://")) { return path; } // Handle absolute paths that start with / if (path.startsWith("/")) { const baseUrl = new URL(base); return `${baseUrl.protocol}//${baseUrl.host}${path}`; } // Resolve relative paths const resolved = new URL(path, base); return resolved.toString(); } /** * Converts the given HTML content to Markdown format, resolving any relative URLs * using the provided base URL. * * @param html - The HTML content to be converted to Markdown. * @param baseUrl - The base URL to use for resolving relative URLs in the HTML. * @returns The Markdown representation of the input HTML. * * @example * ```ts * const html = await fetch("https://bcurio.us/resources/hdkb/gates/44"); * const md = convertHtmlToMarkdown(await html.text(), "https://bcurio.us"); * await Bun.write("playground/bcurious-gate-44.md", md); * ``` */ export function convertHtmlToMarkdown(html: string, baseUrl: string): string { const turndownService = new TurndownService({ headingStyle: "atx", hr: "---", bulletListMarker: "-", codeBlockStyle: "fenced", }); const rewriter = new HTMLRewriter() .on("script,style,meta,template,link", { element(element) { element.remove(); }, }) .on("a", { element(element) { const href = element.getAttribute("href"); if (href) { element.setAttribute("href", resolveUrl(baseUrl, href)); } }, }) .on("img", { element(element) { const src = element.getAttribute("src"); if (src?.startsWith("data:")) { element.remove(); } else if (src) { element.setAttribute("src", resolveUrl(baseUrl, src)); } }, }); let finalHtml = html; if (html.includes("<article")) { const articleStart = html.indexOf("<article"); const articleEnd = html.lastIndexOf("</article>") + 10; finalHtml = html.substring(articleStart, articleEnd); } return turndownService .turndown(rewriter.transform(finalHtml)) .replace(/\n{3,}/g, "\n\n") .replace(/\[\n+/g, "[") .replace(/\n+\]/g, "]"); } ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/services/uninstall.ts: -------------------------------------------------------------------------------- ```typescript import { logger } from "$/shared/logger"; import fsp from "fs/promises"; import { Plugin } from "obsidian"; import path from "path"; import { BINARY_NAME } from "../constants"; import { getPlatform } from "./install"; import { getFileSystemAdapter } from "../utils/getFileSystemAdapter"; /** * Uninstalls the MCP server by removing the binary and cleaning up configuration */ export async function uninstallServer(plugin: Plugin): Promise<void> { try { const adapter = getFileSystemAdapter(plugin); if ("error" in adapter) { throw new Error(adapter.error); } // Remove binary const platform = getPlatform(); const binDir = path.join( adapter.getBasePath(), plugin.app.vault.configDir, "plugins", plugin.manifest.id, "bin", ); const binaryPath = path.join(binDir, BINARY_NAME[platform]); try { await fsp.unlink(binaryPath); logger.info("Removed server binary", { binaryPath }); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") { throw error; } // File doesn't exist, continue } // Remove bin directory if empty try { await fsp.rmdir(binDir); logger.info("Removed empty bin directory", { binDir }); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOTEMPTY") { throw error; } // Directory not empty, leave it } // Remove our entry from Claude config // Note: We don't remove the entire config file since it may contain other server configs const configPath = path.join( process.env.HOME || process.env.USERPROFILE || "", "Library/Application Support/Claude/claude_desktop_config.json", ); try { const content = await fsp.readFile(configPath, "utf8"); const config = JSON.parse(content); if (config.mcpServers && config.mcpServers["obsidian-mcp-tools"]) { delete config.mcpServers["obsidian-mcp-tools"]; await fsp.writeFile(configPath, JSON.stringify(config, null, 2)); logger.info("Removed server from Claude config", { configPath }); } } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") { throw error; } // Config doesn't exist, nothing to clean up } logger.info("Server uninstall complete"); } catch (error) { logger.error("Failed to uninstall server:", { error }); throw new Error( `Failed to uninstall server: ${ error instanceof Error ? error.message : String(error) }`, ); } } ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/makeRequest.ts: -------------------------------------------------------------------------------- ```typescript import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; import { type, type Type } from "arktype"; import { logger } from "./logger"; // Default to HTTPS port, fallback to HTTP if specified const USE_HTTP = process.env.OBSIDIAN_USE_HTTP === "true"; const PORT = USE_HTTP ? 27123 : 27124; const PROTOCOL = USE_HTTP ? "http" : "https"; const HOST = process.env.OBSIDIAN_HOST || "127.0.0.1"; export const BASE_URL = `${PROTOCOL}://${HOST}:${PORT}`; // Disable TLS certificate validation for local self-signed certificates process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; /** * Makes a request to the Obsidian Local REST API with the provided path and optional request options. * Automatically adds the required API key to the request headers. * Throws an `McpError` if the API response is not successful. * * @param path - The path to the Obsidian API endpoint. * @param init - Optional request options to pass to the `fetch` function. * @returns The response from the Obsidian API. */ export async function makeRequest< T extends | Type<{}, {}> | Type<null | undefined, {}> | Type<{} | null | undefined, {}>, >(schema: T, path: string, init?: RequestInit): Promise<T["infer"]> { const API_KEY = process.env.OBSIDIAN_API_KEY; if (!API_KEY) { logger.error("OBSIDIAN_API_KEY environment variable is required", { env: process.env, }); throw new Error("OBSIDIAN_API_KEY environment variable is required"); } const url = `${BASE_URL}${path}`; const response = await fetch(url, { ...init, headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "text/markdown", ...init?.headers, }, }); if (!response.ok) { const error = await response.text(); const message = `${init?.method ?? "GET"} ${path} ${response.status}: ${error}`; throw new McpError(ErrorCode.InternalError, message); } const isJSON = !!response.headers.get("Content-Type")?.includes("json"); const data = isJSON ? await response.json() : await response.text(); // 204 No Content responses should be validated as undefined const validated = response.status === 204 ? undefined : schema(data); if (validated instanceof type.errors) { const stackError = new Error(); Error.captureStackTrace(stackError, makeRequest); logger.error("Invalid response from Obsidian API", { status: response.status, error: validated.summary, stack: stackError.stack, data, }); throw new McpError( ErrorCode.InternalError, `${init?.method ?? "GET"} ${path} ${response.status}: ${validated.summary}`, ); } return validated; } ``` -------------------------------------------------------------------------------- /packages/shared/src/types/plugin-smart-connections.ts: -------------------------------------------------------------------------------- ```typescript import { type } from "arktype"; /** * SmartSearch filter options */ export const SmartSearchFilter = type({ "exclude_key?": type("string").describe("A single key to exclude."), "exclude_keys?": type("string[]").describe( "An array of keys to exclude. If exclude_key is provided, it's added to this array.", ), "exclude_key_starts_with?": type("string").describe( "Exclude keys starting with this string.", ), "exclude_key_starts_with_any?": type("string[]").describe( "Exclude keys starting with any of these strings.", ), "exclude_key_includes?": type("string").describe( "Exclude keys that include this string.", ), "key_ends_with?": type("string").describe( "Include only keys ending with this string.", ), "key_starts_with?": type("string").describe( "Include only keys starting with this string.", ), "key_starts_with_any?": type("string[]").describe( "Include only keys starting with any of these strings.", ), "key_includes?": type("string").describe( "Include only keys that include this string.", ), "limit?": type("number").describe("Limit the number of search results."), }); export type SearchFilter = typeof SmartSearchFilter.infer; /** * Interface for the SmartBlock class which represents a single block within a SmartSource */ interface SmartBlock { // Core properties key: string; path: string; data: { text: string | null; length: number; last_read: { hash: string | null; at: number; }; embeddings: Record<string, unknown>; lines?: [number, number]; // Start and end line numbers }; // Vector-related properties vec: number[] | undefined; tokens: number | undefined; // State flags excluded: boolean; is_block: boolean; is_gone: boolean; // Content properties breadcrumbs: string; file_path: string; file_type: string; folder: string; link: string; name: string; size: number; // Methods read(): Promise<string>; nearest(filter?: SearchFilter): Promise<SearchResult[]>; } /** * Interface for a single search result */ interface SearchResult { item: SmartBlock; score: number; } /** * Interface for the SmartSearch class which provides the main search functionality */ export interface SmartSearch { /** * Searches for relevant blocks based on the provided search text * @param search_text - The text to search for * @param filter - Optional filter parameters to refine the search * @returns A promise that resolves to an array of search results, sorted by relevance score */ search(search_text: string, filter?: SearchFilter): Promise<SearchResult[]>; } ``` -------------------------------------------------------------------------------- /packages/test-site/src/routes/+page.svelte: -------------------------------------------------------------------------------- ``` <svelte:head> <title>Understanding Express Routes: A Complete Guide</title> <meta name="description" content="Learn how to master Express.js routing with practical examples and best practices for building scalable Node.js applications." /> <meta property="og:title" content="Understanding Express Routes: A Complete Guide" /> <meta property="og:description" content="Learn how to master Express.js routing with practical examples and best practices for building scalable Node.js applications." /> <meta property="og:type" content="article" /> <meta property="og:url" content="https://yoursite.com/blog/express-routes-guide" /> <meta property="og:image" content="https://yoursite.com/images/express-routes-banner.jpg" /> <meta name="author" content="Jane Doe" /> <link rel="canonical" href="https://yoursite.com/blog/express-routes-guide" /> </svelte:head> <article class="blog-post"> <header> <h1>Understanding Express Routes: A Complete Guide</h1> <div class="metadata"> <address class="author"> <!-- svelte-ignore a11y_invalid_attribute --> By <a rel="author" href="#">Jane Doe</a> </address> <time datetime="2023-12-14">December 14, 2023</time> </div> </header> <section class="content"> <h2>Introduction</h2> <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> <h2>Basic Route Structure</h2> <p>Express routes follow a simple pattern that combines HTTP methods with URL paths:</p> <pre><code>{` app.get('/users', (req, res) => { res.send('Get all users'); }); `}</code></pre> <h2>Route Parameters</h2> <p>Dynamic routes can be created using parameters:</p> <pre><code>{` app.get('/users/:id', (req, res) => { const userId = req.params.id; res.send(\`Get user \${userId}\`); }); `}</code></pre> <h2>Middleware Integration</h2> <p>Routes can include middleware functions for additional processing:</p> <pre><code>{` const authMiddleware = (req, res, next) => { // Authentication logic next(); }; app.get('/protected', authMiddleware, (req, res) => { res.send('Protected route'); }); `}</code></pre> </section> <footer> <div class="tags"> <span class="tag">Express.js</span> <span class="tag">Node.js</span> <span class="tag">Web Development</span> </div> <div class="share"> <h3>Share this article</h3> <nav class="social-links"> <!-- svelte-ignore a11y_invalid_attribute --> <a href="#">Twitter</a> <!-- svelte-ignore a11y_invalid_attribute --> <a href="#">LinkedIn</a> <!-- svelte-ignore a11y_invalid_attribute --> <a href="#">Facebook</a> </nav> </div> </footer> </article> <style> .blog-post { max-width: 800px; margin: 0 auto; padding: 2rem; } .metadata { color: #666; margin: 1rem 0; } .content { line-height: 1.6; } .tags { margin: 2rem 0; } .tag { background: #eee; padding: 0.25rem 0.5rem; border-radius: 4px; margin-right: 0.5rem; } pre { background: #f4f4f4; padding: 1rem; border-radius: 4px; overflow-x: auto; } </style> ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/fetch/index.ts: -------------------------------------------------------------------------------- ```typescript import { logger, type ToolRegistry } from "$/shared"; import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js"; import { type } from "arktype"; import { DEFAULT_USER_AGENT } from "./constants"; import { convertHtmlToMarkdown } from "./services/markdown"; export function registerFetchTool(tools: ToolRegistry, server: Server) { tools.register( type({ name: '"fetch"', arguments: { url: "string", "maxLength?": type("number").describe("Limit response length."), "startIndex?": type("number").describe( "Supports paginated retrieval of content.", ), "raw?": type("boolean").describe( "Returns raw HTML content if raw=true.", ), }, }).describe( "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.", ), async ({ arguments: args }) => { logger.info("Fetching URL", { url: args.url }); try { const response = await fetch(args.url, { headers: { "User-Agent": DEFAULT_USER_AGENT, }, }); if (!response.ok) { throw new McpError( ErrorCode.InternalError, `Failed to fetch ${args.url} - status code ${response.status}`, ); } const contentType = response.headers.get("content-type") || ""; const text = await response.text(); const isHtml = text.toLowerCase().includes("<html") || contentType.includes("text/html") || !contentType; let content: string; let prefix = ""; if (isHtml && !args.raw) { content = convertHtmlToMarkdown(text, args.url); } else { content = text; prefix = `Content type ${contentType} cannot be simplified to markdown, but here is the raw content:\n`; } const maxLength = args.maxLength || 5000; const startIndex = args.startIndex || 0; const totalLength = content.length; if (totalLength > maxLength) { content = content.substring(startIndex, startIndex + maxLength); content += `\n\n<error>Content truncated. Call the fetch tool with a startIndex of ${ startIndex + maxLength } to get more content.</error>`; } logger.debug("URL fetched successfully", { url: args.url, contentLength: content.length, }); return { content: [ { type: "text", text: `${prefix}Contents of ${args.url}:\n${content}`, }, { type: "text", text: `Pagination: ${JSON.stringify({ totalLength, startIndex, endIndex: startIndex + content.length, hasMore: true, })}`, }, ], }; } catch (error) { logger.error("Failed to fetch URL", { url: args.url, error }); throw new McpError( ErrorCode.InternalError, `Failed to fetch ${args.url}: ${error}`, ); } }, ); } ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/bun.config.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env bun import { type BuildConfig, type BunPlugin } from "bun"; import fsp from "fs/promises"; import { join, parse } from "path"; import process from "process"; import { compile, preprocess } from "svelte/compiler"; import { version } from "../../package.json" assert { type: "json" }; import svelteConfig from "./svelte.config.js"; const banner = `/* THIS IS A GENERATED/BUNDLED FILE BY BUN if you want to view the source, please visit https://github.com/jacksteamdev/obsidian-mcp-tools */ `; // Parse command line arguments const args = process.argv.slice(2); const isWatch = args.includes("--watch"); const isProd = args.includes("--prod"); // Svelte plugin implementation const sveltePlugin: BunPlugin = { name: "svelte", setup(build) { build.onLoad({ filter: /\.svelte$/ }, async ({ path }) => { try { const parsed = parse(path); const source = await Bun.file(path).text(); const preprocessed = await preprocess(source, svelteConfig.preprocess, { filename: parsed.base, }); const result = compile(preprocessed.code, { filename: parsed.base, generate: "client", css: "injected", dev: isProd, }); return { loader: "js", contents: result.js.code, }; } catch (error) { throw new Error(`Error compiling Svelte component: ${error}`); } }); }, }; const config: BuildConfig = { entrypoints: ["./src/main.ts"], outdir: "../..", minify: isProd, plugins: [sveltePlugin], external: [ "obsidian", "electron", "@codemirror/autocomplete", "@codemirror/collab", "@codemirror/commands", "@codemirror/language", "@codemirror/lint", "@codemirror/search", "@codemirror/state", "@codemirror/view", "@lezer/common", "@lezer/highlight", "@lezer/lr", ], target: "node", format: "cjs", conditions: ["browser", isProd ? "production" : "development"], sourcemap: isProd ? "none" : "inline", define: { "process.env.NODE_ENV": JSON.stringify( isProd ? "production" : "development", ), "import.meta.filename": JSON.stringify("mcp-tools-for-obsidian.ts"), // These environment variables are critical for the MCP server download functionality // They define the base URL and version for downloading the correct server binaries "process.env.GITHUB_DOWNLOAD_URL": JSON.stringify( `https://github.com/jacksteamdev/obsidian-mcp-tools/releases/download/${version}` ), "process.env.GITHUB_REF_NAME": JSON.stringify(version), }, naming: { entry: "main.js", // Match original output name }, // Add banner to output banner, }; async function build() { try { const result = await Bun.build(config); if (!result.success) { console.error("Build failed"); for (const message of result.logs) { console.error(message); } process.exit(1); } console.log("Build successful"); } catch (error) { console.error("Build failed:", error); process.exit(1); } } async function watch() { const watcher = fsp.watch(join(import.meta.dir, "src"), { recursive: true, }); console.log("Watching for changes..."); for await (const event of watcher) { console.log(`Detected ${event.eventType} in ${event.filename}`); await build(); } } async function main() { if (isWatch) { await build(); return watch(); } else { return build(); } } main().catch((err) => { console.error(err); process.exit(1); }); ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/services/config.ts: -------------------------------------------------------------------------------- ```typescript import fsp from "fs/promises"; import { Plugin } from "obsidian"; import os from "os"; import path from "path"; import { logger } from "$/shared/logger"; import { CLAUDE_CONFIG_PATH } from "../constants"; interface ClaudeConfig { mcpServers: { [key: string]: { command: string; args?: string[]; env?: { OBSIDIAN_API_KEY?: string; [key: string]: string | undefined; }; }; }; } /** * Gets the absolute path to the Claude Desktop config file */ function getConfigPath(): string { const platform = os.platform(); let configPath: string; switch (platform) { case "darwin": configPath = CLAUDE_CONFIG_PATH.macos; break; case "win32": configPath = CLAUDE_CONFIG_PATH.windows; break; default: configPath = CLAUDE_CONFIG_PATH.linux; } // Expand ~ to home directory if needed if (configPath.startsWith("~")) { configPath = path.join(os.homedir(), configPath.slice(1)); } // Expand environment variables on Windows if (platform === "win32") { configPath = configPath.replace(/%([^%]+)%/g, (_, n) => process.env[n] || ""); } return configPath; } /** * Updates the Claude Desktop config file with MCP server settings */ export async function updateClaudeConfig( plugin: Plugin, serverPath: string, apiKey?: string ): Promise<void> { try { const configPath = getConfigPath(); const configDir = path.dirname(configPath); // Ensure config directory exists await fsp.mkdir(configDir, { recursive: true }); // Read existing config or create new one let config: ClaudeConfig = { mcpServers: {} }; try { const content = await fsp.readFile(configPath, "utf8"); config = JSON.parse(content); config.mcpServers = config.mcpServers || {}; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") { throw error; } // File doesn't exist, use default empty config } // Update config with our server entry config.mcpServers["obsidian-mcp-tools"] = { command: serverPath, env: { OBSIDIAN_API_KEY: apiKey, }, }; // Write updated config await fsp.writeFile(configPath, JSON.stringify(config, null, 2)); logger.info("Updated Claude config", { configPath }); } catch (error) { logger.error("Failed to update Claude config:", { error }); throw new Error( `Failed to update Claude config: ${ error instanceof Error ? error.message : String(error) }` ); } } /** * Removes the MCP server entry from the Claude Desktop config file */ export async function removeFromClaudeConfig(): Promise<void> { try { const configPath = getConfigPath(); // Read existing config let config: ClaudeConfig; try { const content = await fsp.readFile(configPath, "utf8"); config = JSON.parse(content); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") { // File doesn't exist, nothing to remove return; } throw error; } // Remove our server entry if it exists if (config.mcpServers && "obsidian-mcp-tools" in config.mcpServers) { delete config.mcpServers["obsidian-mcp-tools"]; await fsp.writeFile(configPath, JSON.stringify(config, null, 2)); logger.info("Removed server from Claude config", { configPath }); } } catch (error) { logger.error("Failed to remove from Claude config:", { error }); throw new Error( `Failed to remove from Claude config: ${ error instanceof Error ? error.message : String(error) }` ); } } ``` -------------------------------------------------------------------------------- /docs/project-architecture.md: -------------------------------------------------------------------------------- ```markdown # Project Architecture Use the following structure and conventions for all new features. ## Monorepo Structure This project uses a monorepo with multiple packages: - `packages/mcp-server` - The MCP server implementation - `packages/obsidian-plugin` - The Obsidian plugin - `packages/shared` - Shared code between packages - `docs/` - Project documentation - `docs/features` - Feature requirements ### Package Organization ``` packages/ ├── mcp-server/ # Server implementation │ ├── dist/ # Compiled output │ ├── logs/ # Server logs │ ├── playground/ # Development testing │ ├── scripts/ # Build and utility scripts │ └── src/ # Source code │ ├── obsidian-plugin/ # Obsidian plugin │ ├── docs/ # Documentation │ ├── src/ │ │ ├── features/ # Feature modules │ │ └── main.ts # Plugin entry point │ └── manifest.json # Plugin metadata │ └── shared/ # Shared utilities and types └── src/ ├── types/ # Common interfaces ├── utils/ # Common utilities └── constants/ # Shared configuration ``` ## Feature-Based Architecture The Obsidian plugin uses a feature-based architecture where each feature is a self-contained module. ### Feature Structure ``` src/features/ ├── core/ # Plugin initialization and settings ├── mcp-server-install/ # Binary management ├── mcp-server-prompts/ # Template execution └── smart-search/ # Search functionality Each feature contains: feature/ ├── components/ # UI components ├── services/ # Business logic ├── types.ts # Feature-specific types ├── utils.ts # Feature-specific utilities ├── constants.ts # Feature-specific constants └── index.ts # Public API with setup function ``` ### Feature Management Each feature exports a setup function for initialization: ```typescript export async function setup(plugin: Plugin): Promise<SetupResult> { // Check dependencies // Initialize services // Register event handlers return { success: true } || { success: false, error: "reason" }; } ``` Features: - Initialize independently - Handle their own dependencies - Continue running if other features fail - Log failures for debugging ### McpToolsPlugin Settings Management Use TypeScript module augmentation to extend the McpToolsPluginSettings interface: ```typescript // packages/obsidian-plugin/src/types.ts declare module "obsidian" { interface McpToolsPluginSettings { version?: string; } interface Plugin { loadData(): Promise<McpToolsPluginSettings>; saveData(data: McpToolsPluginSettings): Promise<void>; } } // packages/obsidian-plugin/src/features/some-feature/types.ts declare module "obsidian" { interface McpToolsPluginSettings { featureName?: { setting1?: string; setting2?: boolean; }; } } ``` Extending the settings interface allows for type-safe access to feature settings via `McpToolsPlugin.loadData()` and `McpToolsPlugin.saveData()`. ### Version Management Unified version approach: - Plugin and server share version number - Version stored in plugin manifest - Server binaries include version in filename - Version checked during initialization ### UI Integration The core feature provides a PluginSettingTab that: - Loads UI components from each feature - Maintains consistent UI organization - Handles conditional rendering based on feature state ### Error Handling Features implement consistent error handling: - Return descriptive error messages - Log detailed information for debugging - Provide user feedback via Obsidian Notice API - Clean up resources on failure ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/prompts/index.ts: -------------------------------------------------------------------------------- ```typescript import { formatMcpError, logger, makeRequest, parseTemplateParameters, } from "$/shared"; import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { ErrorCode, GetPromptRequestSchema, ListPromptsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import { type } from "arktype"; import { buildTemplateArgumentsSchema, LocalRestAPI, PromptFrontmatterSchema, type PromptMetadata, } from "shared"; const PROMPT_DIRNAME = `Prompts`; export function setupObsidianPrompts(server: Server) { server.setRequestHandler(ListPromptsRequestSchema, async () => { try { const { files } = await makeRequest( LocalRestAPI.ApiVaultDirectoryResponse, `/vault/${PROMPT_DIRNAME}/`, ); const prompts: PromptMetadata[] = ( await Promise.all( files.map(async (filename) => { // Skip non-Markdown files if (!filename.endsWith(".md")) return []; // Retrieve frontmatter and content from vault file const file = await makeRequest( LocalRestAPI.ApiVaultFileResponse, `/vault/${PROMPT_DIRNAME}/${filename}`, { headers: { Accept: LocalRestAPI.MIME_TYPE_OLRAPI_NOTE_JSON }, }, ); // Skip files without the prompt template tag if (!file.tags.includes("mcp-tools-prompt")) { return []; } return { name: filename, description: file.frontmatter.description, arguments: parseTemplateParameters(file.content), }; }), ) ).flat(); return { prompts }; } catch (err) { const error = formatMcpError(err); logger.error("Error in ListPromptsRequestSchema handler", { error, message: error.message, }); throw error; } }); server.setRequestHandler(GetPromptRequestSchema, async ({ params }) => { try { const promptFilePath = `${PROMPT_DIRNAME}/${params.name}`; // Get prompt content const { content: template, frontmatter } = await makeRequest( LocalRestAPI.ApiVaultFileResponse, `/vault/${promptFilePath}`, { headers: { Accept: LocalRestAPI.MIME_TYPE_OLRAPI_NOTE_JSON }, }, ); const { description } = PromptFrontmatterSchema.assert(frontmatter); const templateParams = parseTemplateParameters(template); const templateParamsSchema = buildTemplateArgumentsSchema(templateParams); const templateArgs = templateParamsSchema(params.arguments); if (templateArgs instanceof type.errors) { throw new McpError( ErrorCode.InvalidParams, `Invalid arguments: ${templateArgs.summary}`, ); } const templateExecutionArgs: LocalRestAPI.ApiTemplateExecutionParamsType = { name: promptFilePath, arguments: templateArgs, }; // Process template through Templater plugin const { content } = await makeRequest( LocalRestAPI.ApiTemplateExecutionResponse, "/templates/execute", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(templateExecutionArgs), }, ); // Using unsafe assertion b/c the last element is always a string const withoutFrontmatter = content.split("---").at(-1)!.trim(); return { messages: [ { description, role: "user", content: { type: "text", text: withoutFrontmatter, }, }, ], }; } catch (err) { const error = formatMcpError(err); logger.error("Error in GetPromptRequestSchema handler", { error, message: error.message, }); throw error; } }); } ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/shared/ToolRegistry.ts: -------------------------------------------------------------------------------- ```typescript import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { ErrorCode, McpError, type Result, } from "@modelcontextprotocol/sdk/types.js"; import { type, type Type } from "arktype"; import { formatMcpError } from "./formatMcpError.js"; import { logger } from "./logger.js"; interface HandlerContext { server: Server; } const textResult = type({ type: '"text"', text: "string", }); const imageResult = type({ type: '"image"', data: "string.base64", mimeType: "string", }); const resultSchema = type({ content: textResult.or(imageResult).array(), "isError?": "boolean", }); type ResultSchema = typeof resultSchema.infer; /** * The ToolRegistry class represents a set of tools that can be used by * the server. It is a map of request schemas to request handlers * that provides a list of available tools and a method to handle requests. */ export class ToolRegistryClass< TSchema extends Type< { name: string; arguments?: Record<string, unknown>; }, {} >, THandler extends ( request: TSchema["infer"], context: HandlerContext, ) => Promise<Result>, > extends Map<TSchema, THandler> { private enabled = new Set<TSchema>(); register< Schema extends TSchema, Handler extends ( request: Schema["infer"], context: HandlerContext, ) => ResultSchema | Promise<ResultSchema>, >(schema: Schema, handler: Handler) { if (this.has(schema)) { throw new Error(`Tool already registered: ${schema.get("name")}`); } this.enable(schema); return super.set( schema as unknown as TSchema, handler as unknown as THandler, ); } enable = <Schema extends TSchema>(schema: Schema) => { this.enabled.add(schema); return this; }; disable = <Schema extends TSchema>(schema: Schema) => { this.enabled.delete(schema); return this; }; list = () => { return { tools: Array.from(this.enabled.values()).map((schema) => { return { // @ts-expect-error We know the const property is present for a string name: schema.get("name").toJsonSchema().const, description: schema.description, inputSchema: schema.get("arguments").toJsonSchema(), }; }), }; }; /** * MCP SDK sends boolean values as "true" or "false". This method coerces the boolean * values in the request parameters to the expected type. * * @param schema Arktype schema * @param params MCP request parameters * @returns MCP request parameters with corrected boolean values */ private coerceBooleanParams = <Schema extends TSchema>( schema: Schema, params: Schema["infer"], ): Schema["infer"] => { const args = params.arguments; const argsSchema = schema.get("arguments").exclude("undefined"); if (!args || !argsSchema) return params; const fixed = { ...params.arguments }; for (const [key, value] of Object.entries(args)) { const valueSchema = argsSchema.get(key).exclude("undefined"); if ( valueSchema.expression === "boolean" && typeof value === "string" && ["true", "false"].includes(value) ) { fixed[key] = value === "true"; } } return { ...params, arguments: fixed }; }; dispatch = async <Schema extends TSchema>( params: Schema["infer"], context: HandlerContext, ) => { try { for (const [schema, handler] of this.entries()) { if (schema.get("name").allows(params.name)) { const validParams = schema.assert( this.coerceBooleanParams(schema, params), ); // return await to handle runtime errors here return await handler(validParams, context); } } throw new McpError( ErrorCode.InvalidRequest, `Unknown tool: ${params.name}`, ); } catch (error) { const formattedError = formatMcpError(error); logger.error(`Error handling ${params.name}`, { ...formattedError, message: formattedError.message, stack: formattedError.stack, error, params, }); throw formattedError; } }; } export type ToolRegistry = ToolRegistryClass< Type< { name: string; arguments?: Record<string, unknown>; }, {} >, ( request: { name: string; arguments?: Record<string, unknown>; }, context: HandlerContext, ) => Promise<Result> >; ``` -------------------------------------------------------------------------------- /packages/shared/src/logger.ts: -------------------------------------------------------------------------------- ```typescript import { type } from "arktype"; import { existsSync, mkdirSync } from "fs"; import { appendFile } from "fs/promises"; import { homedir, platform } from "os"; import { dirname, resolve } from "path"; /** * Determines the appropriate log directory path based on the current operating system. * @param appName - The name of the application to use in the log directory path. * @returns The full path to the log directory for the current operating system. * @throws {Error} If the current operating system is not supported. */ export function getLogFilePath(appName: string, fileName: string) { switch (platform()) { case "darwin": // macOS return resolve(homedir(), "Library", "Logs", appName, fileName); case "win32": // Windows return resolve(homedir(), "AppData", "Local", "Logs", appName, fileName); case "linux": // Linux return resolve(homedir(), ".local", "share", "logs", appName, fileName); default: throw new Error("Unsupported operating system"); } } const ensureDirSync = (dirPath: string) => { if (!existsSync(dirPath)) { mkdirSync(dirPath, { recursive: true }); } }; const logLevels = ["DEBUG", "INFO", "WARN", "ERROR", "FATAL"] as const; export const logLevelSchema = type.enumerated(...logLevels); export type LogLevel = typeof logLevelSchema.infer; const formatMessage = ( level: LogLevel, message: unknown, meta: Record<string, unknown>, ) => { const timestamp = new Date().toISOString(); const metaStr = Object.keys(meta).length ? `\n${JSON.stringify(meta, null, 2)}` : ""; return `${timestamp} [${level.padEnd(5)}] ${JSON.stringify( message, )}${metaStr}\n`; }; const loggerConfigSchema = type({ appName: "string", filename: "string", level: logLevelSchema, }); export const loggerConfigMorph = loggerConfigSchema.pipe((config) => { const filename = getLogFilePath(config.appName, config.filename); const levels = logLevels.slice(logLevels.indexOf(config.level)); return { ...config, levels, filename }; }); export type InputLoggerConfig = typeof loggerConfigSchema.infer; export type FullLoggerConfig = typeof loggerConfigMorph.infer; /** * Creates a logger instance with configurable options for logging to a file. * The logger provides methods for logging messages at different log levels (DEBUG, INFO, WARN, ERROR, FATAL). * @param config - An object with configuration options for the logger. * @param config.filepath - The file path to use for logging to a file. * @param config.level - The minimum log level to log messages. * @returns An object with logging methods (debug, info, warn, error, fatal). */ export function createLogger(inputConfig: InputLoggerConfig) { let config: FullLoggerConfig = loggerConfigMorph.assert(inputConfig); let logMeta: Record<string, unknown> = {}; const queue: Promise<void>[] = []; const log = (level: LogLevel, message: unknown, meta?: typeof logMeta) => { if (!config.levels.includes(level)) return; ensureDirSync(dirname(getLogFilePath(config.appName, config.filename))); queue.push( appendFile( config.filename, formatMessage(level, message, { ...logMeta, ...(meta ?? {}) }), ), ); }; const debug = (message: unknown, meta?: typeof logMeta) => log("DEBUG", message, meta); const info = (message: unknown, meta?: typeof logMeta) => log("INFO", message, meta); const warn = (message: unknown, meta?: typeof logMeta) => log("WARN", message, meta); const error = (message: unknown, meta?: typeof logMeta) => log("ERROR", message, meta); const fatal = (message: unknown, meta?: typeof logMeta) => log("FATAL", message, meta); const logger = { debug, info, warn, error, fatal, flush() { return Promise.all(queue); }, get config(): FullLoggerConfig { return { ...config }; }, /** * Updates the configuration of the logger instance. * @param newConfig - A partial configuration object to merge with the existing configuration. * This method updates the log levels based on the new configuration level, and then merges the new configuration with the existing configuration. */ set config(newConfig: Partial<InputLoggerConfig>) { config = loggerConfigMorph.assert({ ...config, ...newConfig }); logger.debug("Updated logger configuration", { config }); }, set meta(newMeta: Record<string, unknown>) { logMeta = newMeta; }, }; return logger; } ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/components/McpServerInstallSettings.svelte: -------------------------------------------------------------------------------- ``` <script lang="ts"> import type McpToolsPlugin from "$/main"; import { FULL_LOGGER_FILENAME, loadDependenciesArray } from "$/shared"; import { Notice } from "obsidian"; import { dirname } from "path"; import { onMount } from "svelte"; import { removeFromClaudeConfig, updateClaudeConfig, } from "../services/config"; import { installMcpServer } from "../services/install"; import { getInstallationStatus } from "../services/status"; import { uninstallServer } from "../services/uninstall"; import type { InstallationStatus } from "../types"; import { openFolder } from "../utils/openFolder"; export let plugin: McpToolsPlugin; // Dependencies and API key status const deps = loadDependenciesArray(plugin); // Installation status let status: InstallationStatus = { state: "not installed", versions: {}, }; onMount(async () => { status = await getInstallationStatus(plugin); }); // Handle installation async function handleInstall() { try { const apiKey = await plugin.getLocalRestApiKey(); if (!apiKey) { throw new Error("Local REST API key is not configured"); } status = { ...status, state: "installing" }; const installPath = await installMcpServer(plugin); // Update Claude config await updateClaudeConfig(plugin, installPath.path, apiKey); status = await getInstallationStatus(plugin); } catch (error) { const message = error instanceof Error ? error.message : "Installation failed"; status = { ...status, state: "error", error: message }; new Notice(message); } } // Handle uninstall async function handleUninstall() { try { status = { ...status, state: "installing" }; await uninstallServer(plugin); await removeFromClaudeConfig(); status = { ...status, state: "not installed" }; } catch (error) { const message = error instanceof Error ? error.message : "Uninstallation failed"; status = { ...status, state: "error", error: message, }; new Notice(message); } } </script> <div class="installation-status"> <h3>Installation status</h3> {#if status.state === "no api key"} <div class="error-message">Please configure the Local REST API plugin</div> {:else if status.state === "not installed"} <div class="status-message"> MCP Server is not installed <button on:click={handleInstall}>Install server</button> </div> {:else if status.state === "installing"} <div class="status-message">Installing MCP server...</div> {:else if status.state === "installed"} <div class="status-message"> MCP Server v{status.versions.server} is installed <button on:click={handleUninstall}>Uninstall</button> </div> {:else if status.state === "outdated"} <div class="status-message"> Update available (v{status.versions.server} -> v{status.versions.plugin}) <button on:click={handleInstall}>Update</button> </div> {:else if status.state === "uninstalling"} <div class="status-message">Uninstalling MCP server...</div> {:else if status.state === "error"} <div class="error-message">{status.error}</div> {/if} </div> <div class="dependencies"> <h3>Dependencies</h3> {#each $deps as dep (dep.id)} <div class="dependency-item"> {#if dep.installed} ✅ {dep.name} is installed {:else} ❌ {dep.name} {dep.required ? "(Required)" : "(Optional)"} {#if dep.url}<a href={dep.url} target="_blank">How to install?</a>{/if} {/if} </div> {/each} </div> <div class="links"> <h3>Resources</h3> {#if status.path} <div class="link-item"> <!-- svelte-ignore a11y_no_static_element_interactions --> <a on:click={() => status.dir && openFolder(status.dir)}> Server install folder </a> </div> {/if} <div class="link-item"> <!-- svelte-ignore a11y_no_static_element_interactions --> <a on:click={() => openFolder(dirname(FULL_LOGGER_FILENAME))}> Server log folder </a> </div> <div class="link-item"> <a href="https://github.com/jacksteamdev/obsidian-mcp-tools" target="_blank" > GitHub repository </a> </div> </div> <style> .error-message { color: var(--text-error); margin-bottom: 1em; } .status-message { margin-bottom: 1em; } .dependency-item { margin-bottom: 0.5em; } .installed { color: var(--text-success); } .not-installed { color: var(--text-muted); } .link-item { margin-bottom: 0.5em; } button { margin-left: 0.5em; } </style> ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/services/status.ts: -------------------------------------------------------------------------------- ```typescript import type McpToolsPlugin from "$/main"; import { logger } from "$/shared/logger"; import { exec } from "child_process"; import fsp from "fs/promises"; import { Plugin } from "obsidian"; import path from "path"; import { clean, lt, valid } from "semver"; import { promisify } from "util"; import { BINARY_NAME } from "../constants"; import type { InstallationStatus, InstallPathInfo } from "../types"; import { getFileSystemAdapter } from "../utils/getFileSystemAdapter"; import { getPlatform } from "./install"; const execAsync = promisify(exec); /** * Resolves the real path of the given file path, handling cases where the path is a symlink. * * @param filepath - The file path to resolve. * @returns The real path of the file. * @throws {Error} If the file is not found or the symlink cannot be resolved. */ async function resolveSymlinks(filepath: string): Promise<string> { try { return await fsp.realpath(filepath); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") { const parts = path.normalize(filepath).split(path.sep); let resolvedParts: string[] = []; let skipCount = 1; // Skip first segment by default // Handle the root segment differently for Windows vs POSIX if (path.win32.isAbsolute(filepath)) { resolvedParts.push(parts[0]); if (parts[1] === "") { resolvedParts.push(""); skipCount = 2; // Skip two segments for UNC paths } } else if (path.posix.isAbsolute(filepath)) { resolvedParts.push("/"); } else { resolvedParts.push(parts[0]); } // Process remaining path segments for (const part of parts.slice(skipCount)) { const partialPath = path.join(...resolvedParts, part); try { const resolvedPath = await fsp.realpath(partialPath); resolvedParts = resolvedPath.split(path.sep); } catch (err) { resolvedParts.push(part); } } return path.join(...resolvedParts); } logger.error(`Failed to resolve symlink:`, { filepath, error: error instanceof Error ? error.message : error, }); throw new Error(`Failed to resolve symlink: ${filepath}`); } } export async function getInstallPath( plugin: Plugin, ): Promise<InstallPathInfo | { error: string }> { const adapter = getFileSystemAdapter(plugin); if ("error" in adapter) return adapter; const platform = getPlatform(); const originalPath = path.join( adapter.getBasePath(), plugin.app.vault.configDir, "plugins", plugin.manifest.id, "bin", ); const realDirPath = await resolveSymlinks(originalPath); const platformSpecificBinary = BINARY_NAME[platform]; const realFilePath = path.join(realDirPath, platformSpecificBinary); return { dir: realDirPath, path: realFilePath, name: platformSpecificBinary, symlinked: originalPath === realDirPath ? undefined : originalPath, }; } /** * Gets the current installation status of the MCP server */ export async function getInstallationStatus( plugin: McpToolsPlugin, ): Promise<InstallationStatus> { // Verify plugin version is valid const pluginVersion = valid(clean(plugin.manifest.version)); if (!pluginVersion) { logger.error("Invalid plugin version:", { plugin }); return { state: "error", versions: {} }; } // Check for API key const apiKey = plugin.getLocalRestApiKey(); if (!apiKey) { return { state: "no api key", versions: { plugin: pluginVersion }, }; } // Verify server binary is present const installPath = await getInstallPath(plugin); if ("error" in installPath) { return { state: "error", versions: { plugin: pluginVersion }, error: installPath.error, }; } try { await fsp.access(installPath.path, fsp.constants.X_OK); } catch (error) { logger.error("Failed to get server version:", { installPath }); return { state: "not installed", ...installPath, versions: { plugin: pluginVersion }, }; } // Check server binary version let serverVersion: string | null | undefined; try { const versionCommand = `"${installPath.path}" --version`; const { stdout } = await execAsync(versionCommand); serverVersion = clean(stdout.trim()); if (!serverVersion) throw new Error("Invalid server version string"); } catch { logger.error("Failed to get server version:", { installPath }); return { state: "error", ...installPath, versions: { plugin: pluginVersion }, }; } return { ...installPath, state: lt(serverVersion, pluginVersion) ? "outdated" : "installed", versions: { plugin: pluginVersion, server: serverVersion }, }; } ``` -------------------------------------------------------------------------------- /packages/shared/src/types/plugin-templater.ts: -------------------------------------------------------------------------------- ```typescript import { App, type MarkdownPostProcessorContext, TAbstractFile, TFile, TFolder, } from "obsidian"; export enum RunMode { CreateNewFromTemplate, AppendActiveFile, OverwriteFile, OverwriteActiveFile, DynamicProcessor, StartupTemplate, } export enum FunctionsMode { INTERNAL, USER_INTERNAL, } export type RunningConfig = { template_file: TFile | undefined; target_file: TFile; run_mode: RunMode; active_file?: TFile | null; }; interface TemplaterFunctions { app: App; config: RunningConfig; date: { /** * @param format "YYYY-MM-DD" * @param offset * @param reference * @param reference_format */ now( format: string, offset?: number | string, reference?: string, reference_format?: string, ): string; /** * @param format "YYYY-MM-DD" */ tomorrow(format: string): string; /** * @param format "YYYY-MM-DD" * @param weekday * @param reference * @param reference_format */ weekday( format: string, weekday: number, reference?: string, reference_format?: string, ): string; /** * @param format "YYYY-MM-DD" */ yesterday(format?: string): string; }; file: { content: string; /** * @param template TFile or string * @param filename * @param open_new Default: false * @param folder TFolder or string */ create_new( template: TFile | string, filename?: string, open_new?: boolean, folder?: TFolder | string, ): Promise<TFile>; /** * @param format Default: "YYYY-MM-DD HH:mm" */ creation_date(format?: string): string; /** * @param order */ cursor(order?: number): void; cursor_append(content: string): void; exists(filepath: string): boolean; find_tfile(filename: string): TFile; /** * @param absolute Default: false */ folder(absolute?: boolean): string; include(include_link: string | TFile): string; /** * @param format Default: "YYYY-MM-DD HH:mm" */ last_modified_date(format?: string): string; move(new_path: string, file_to_move?: TFile): Promise<void>; /** * @param relative Default: false */ path(relative?: boolean): string; rename(new_title: string): Promise<void>; selection(): string; tags: string[]; title: string; }; frontmatter: Record<string, unknown>; hooks: { on_all_templates_executed(cb: () => void): void; }; system: { /** * Retrieves the clipboard's content. */ clipboard(): Promise<string>; /** * @param prompt_text * @param default_value * @param throw_on_cancel Default: false * @param multiline Default: false */ prompt( prompt_text?: string, default_value?: string, throw_on_cancel?: boolean, multiline?: boolean, ): Promise<string>; /** * @param text_items String array or function mapping item to string * @param items Array of generic type T * @param throw_on_cancel Default: false * @param placeholder Default: "" * @param limit Default: undefined */ suggester<T>( text_items: string[] | ((item: T) => string), items: T[], throw_on_cancel?: boolean, placeholder?: string, limit?: number, ): Promise<T>; }; web: { /** * Retrieves daily quote from quotes database */ daily_quote(): Promise<string>; /** * @param size Image size specification * @param query Search query * @param include_size Whether to include size in URL */ random_picture( size: string, query: string, include_size: boolean, ): Promise<string>; /** * @param url Full URL to request * @param path Optional path parameter */ request(url: string, path?: string): Promise<string>; }; user: Record<string, unknown>; } export interface ITemplater { setup(): Promise<void>; /** Generate the config required to parse a template */ create_running_config( template_file: TFile | undefined, target_file: TFile, run_mode: RunMode, ): RunningConfig; /** I don't think this writes the file, but the config requires the file name */ read_and_parse_template(config: RunningConfig): Promise<string>; /** I don't think this writes the file, but the config requires the file name */ parse_template( config: RunningConfig, template_content: string, ): Promise<string>; create_new_note_from_template( template: TFile | string, folder?: TFolder | string, filename?: string, open_new_note?: boolean, ): Promise<TFile | undefined>; append_template_to_active_file(template_file: TFile): Promise<void>; write_template_to_file(template_file: TFile, file: TFile): Promise<void>; overwrite_active_file_commands(): void; overwrite_file_commands(file: TFile, active_file?: boolean): Promise<void>; process_dynamic_templates( el: HTMLElement, ctx: MarkdownPostProcessorContext, ): Promise<void>; get_new_file_template_for_folder(folder: TFolder): string | undefined; get_new_file_template_for_file(file: TFile): string | undefined; execute_startup_scripts(): Promise<void>; on_file_creation( templater: ITemplater, app: App, file: TAbstractFile, ): Promise<void>; current_functions_object: TemplaterFunctions; functions_generator: { generate_object( config: RunningConfig, functions_mode?: FunctionsMode, ): Promise<TemplaterFunctions>; }; } ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/main.ts: -------------------------------------------------------------------------------- ```typescript import { type } from "arktype"; import type { Request, Response } from "express"; import { Notice, Plugin, TFile } from "obsidian"; import { shake } from "radash"; import { lastValueFrom } from "rxjs"; import { jsonSearchRequest, LocalRestAPI, searchParameters, Templater, type PromptArgAccessor, type SearchResponse, } from "shared"; import { setup as setupCore } from "./features/core"; import { setup as setupMcpServerInstall } from "./features/mcp-server-install"; import { loadLocalRestAPI, loadSmartSearchAPI, loadTemplaterAPI, type Dependencies, } from "./shared"; import { logger } from "./shared/logger"; export default class McpToolsPlugin extends Plugin { private localRestApi: Dependencies["obsidian-local-rest-api"] = { id: "obsidian-local-rest-api", name: "Local REST API", required: true, installed: false, }; async getLocalRestApiKey(): Promise<string | undefined> { // The API key is stored in the plugin's settings return this.localRestApi.plugin?.settings?.apiKey; } async onload() { // Initialize features in order await setupCore(this); await setupMcpServerInstall(this); // Check for required dependencies lastValueFrom(loadLocalRestAPI(this)).then((localRestApi) => { this.localRestApi = localRestApi; if (!this.localRestApi.api) { new Notice( `${this.manifest.name}: Local REST API plugin is required but not found. Please install it from the community plugins and restart Obsidian.`, 0, ); return; } // Register endpoints this.localRestApi.api .addRoute("/search/smart") .post(this.handleSearchRequest.bind(this)); this.localRestApi.api .addRoute("/templates/execute") .post(this.handleTemplateExecution.bind(this)); logger.info("MCP Tools Plugin loaded"); }); } private async handleTemplateExecution(req: Request, res: Response) { try { const { api: templater } = await lastValueFrom(loadTemplaterAPI(this)); if (!templater) { new Notice( `${this.manifest.name}: Templater plugin is not available. Please install it from the community plugins.`, 0, ); logger.error("Templater plugin is not available"); res.status(503).json({ error: "Templater plugin is not available", }); return; } // Validate request body const params = LocalRestAPI.ApiTemplateExecutionParams(req.body); if (params instanceof type.errors) { const response = { error: "Invalid request body", body: req.body, summary: params.summary, }; logger.debug("Invalid request body", response); res.status(400).json(response); return; } // Get prompt content from vault const templateFile = this.app.vault.getAbstractFileByPath(params.name); if (!(templateFile instanceof TFile)) { logger.debug("Template file not found", { params, templateFile, }); res.status(404).json({ error: `File not found: ${params.name}`, }); return; } const config = templater.create_running_config( templateFile, templateFile, Templater.RunMode.CreateNewFromTemplate, ); const prompt: PromptArgAccessor = (argName: string) => { return params.arguments[argName] ?? ""; }; const oldGenerateObject = templater.functions_generator.generate_object.bind( templater.functions_generator, ); // Override generate_object to inject arg into user functions templater.functions_generator.generate_object = async function ( config, functions_mode, ) { const functions = await oldGenerateObject(config, functions_mode); Object.assign(functions, { mcpTools: { prompt } }); return functions; }; // Process template with variables const processedContent = await templater.read_and_parse_template(config); // Restore original functions generator templater.functions_generator.generate_object = oldGenerateObject; // Create new file if requested if (params.createFile && params.targetPath) { await this.app.vault.create(params.targetPath, processedContent); res.json({ message: "Prompt executed and file created successfully", content: processedContent, }); return; } res.json({ message: "Prompt executed without creating a file", content: processedContent, }); } catch (error) { logger.error("Prompt execution error:", { error: error instanceof Error ? error.message : error, body: req.body, }); res.status(503).json({ error: "An error occurred while processing the prompt", }); return; } } private async handleSearchRequest(req: Request, res: Response) { try { const dep = await lastValueFrom(loadSmartSearchAPI(this)); const smartSearch = dep.api; if (!smartSearch) { new Notice( "Smart Search REST API Plugin: smart-connections plugin is required but not found. Please install it from the community plugins.", 0, ); res.status(503).json({ error: "Smart Connections plugin is not available", }); return; } // Validate request body const requestBody = jsonSearchRequest .pipe(({ query, filter = {} }) => ({ query, filter: shake({ key_starts_with_any: filter.folders, exclude_key_starts_with_any: filter.excludeFolders, limit: filter.limit, }), })) .to(searchParameters)(req.body); if (requestBody instanceof type.errors) { res.status(400).json({ error: "Invalid request body", summary: requestBody.summary, }); return; } // Perform search const results = await smartSearch.search( requestBody.query, requestBody.filter, ); // Format response const response: SearchResponse = { results: await Promise.all( results.map(async (result) => ({ path: result.item.path, text: await result.item.read(), score: result.score, breadcrumbs: result.item.breadcrumbs, })), ), }; res.json(response); return; } catch (error) { logger.error("Smart Search API error:", { error, body: req.body }); res.status(503).json({ error: "An error occurred while processing the search request", }); return; } } onunload() { this.localRestApi.api?.unregister(); } } ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/shared/index.ts: -------------------------------------------------------------------------------- ```typescript import type { App } from "obsidian"; import { getAPI, LocalRestApiPublicApi } from "obsidian-local-rest-api"; import { distinct, interval, map, merge, scan, startWith, takeUntil, takeWhile, timer, } from "rxjs"; import type { SmartConnections, Templater } from "shared"; import type McpToolsPlugin from "src/main"; export interface Dependency<ID extends keyof App["plugins"]["plugins"], API> { id: keyof Dependencies; name: string; required: boolean; installed: boolean; url?: string; api?: API; plugin?: App["plugins"]["plugins"][ID]; } export interface Dependencies { "obsidian-local-rest-api": Dependency< "obsidian-local-rest-api", LocalRestApiPublicApi >; "smart-connections": Dependency< "smart-connections", SmartConnections.SmartSearch >; "templater-obsidian": Dependency<"templater-obsidian", Templater.ITemplater>; } // Smart Connections v3.0+ uses a Smart Environment architecture instead of window.SmartSearch declare const window: { SmartSearch?: SmartConnections.SmartSearch; } & Window; export const loadSmartSearchAPI = (plugin: McpToolsPlugin) => interval(200).pipe( takeUntil(timer(5000)), map((): Dependencies["smart-connections"] => { const smartConnectionsPlugin = plugin.app.plugins.plugins[ "smart-connections" ] as any; // Check for Smart Connections v3.0+ (uses smart environment) if (smartConnectionsPlugin?.env?.smart_sources) { const smartEnv = smartConnectionsPlugin.env; // Create a compatibility wrapper that matches the old SmartSearch interface const api: SmartConnections.SmartSearch = { search: async ( search_text: string, filter?: Record<string, string>, ) => { try { // Use the new v3.0 lookup API const results = await smartEnv.smart_sources.lookup({ hypotheticals: [search_text], filter: { limit: filter?.limit, key_starts_with_any: filter?.key_starts_with_any, exclude_key_starts_with_any: filter?.exclude_key_starts_with_any, exclude_key: filter?.exclude_key, exclude_keys: filter?.exclude_keys, exclude_key_starts_with: filter?.exclude_key_starts_with, exclude_key_includes: filter?.exclude_key_includes, key_ends_with: filter?.key_ends_with, key_starts_with: filter?.key_starts_with, key_includes: filter?.key_includes, }, }); // Transform results to match expected format return results.map((result: any) => ({ item: { path: result.item.path, name: result.item.name || result.item.key?.split("/").pop() || result.item.key, breadcrumbs: result.item.breadcrumbs || result.item.path, read: () => result.item.read(), key: result.item.key, file_path: result.item.path, link: result.item.link, size: result.item.size, }, score: result.score, })); } catch (error) { console.error("Smart Connections v3.0 search error:", error); return []; } }, }; return { id: "smart-connections", name: "Smart Connections", required: false, installed: true, api, plugin: smartConnectionsPlugin, }; } // Try window.SmartSearch first (works on some platforms for v2.x) let legacyApi = window.SmartSearch; // Fallback to plugin system (fixes Linux/cross-platform detection issues) if (!legacyApi && smartConnectionsPlugin?.env) { legacyApi = smartConnectionsPlugin.env; // Cache it for future use window.SmartSearch = legacyApi; } return { id: "smart-connections", name: "Smart Connections", required: false, installed: !!legacyApi, api: legacyApi, plugin: smartConnectionsPlugin, }; }), takeWhile((dependency) => !dependency.installed, true), distinct(({ installed }) => installed), ); export const loadLocalRestAPI = (plugin: McpToolsPlugin) => interval(200).pipe( takeUntil(timer(5000)), map((): Dependencies["obsidian-local-rest-api"] => { const api = getAPI(plugin.app, plugin.manifest); return { id: "obsidian-local-rest-api", name: "Local REST API", required: true, installed: !!api, api, plugin: plugin.app.plugins.plugins["obsidian-local-rest-api"], }; }), takeWhile((dependency) => !dependency.installed, true), distinct(({ installed }) => installed), ); export const loadTemplaterAPI = (plugin: McpToolsPlugin) => interval(200).pipe( takeUntil(timer(5000)), map((): Dependencies["templater-obsidian"] => { const api = plugin.app.plugins.plugins["templater-obsidian"]?.templater; return { id: "templater-obsidian", name: "Templater", required: false, installed: !!api, api, plugin: plugin.app.plugins.plugins["templater-obsidian"], }; }), takeWhile((dependency) => !dependency.installed, true), distinct(({ installed }) => installed), ); export const loadDependencies = (plugin: McpToolsPlugin) => { const dependencies: Dependencies = { "obsidian-local-rest-api": { id: "obsidian-local-rest-api", name: "Local REST API", required: true, installed: false, url: "https://github.com/coddingtonbear/obsidian-local-rest-api", }, "smart-connections": { id: "smart-connections", name: "Smart Connections", required: false, installed: false, url: "https://smartconnections.app/", }, "templater-obsidian": { id: "templater-obsidian", name: "Templater", required: false, installed: false, url: "https://silentvoid13.github.io/Templater/", }, }; return merge( loadLocalRestAPI(plugin), loadTemplaterAPI(plugin), loadSmartSearchAPI(plugin), ).pipe( scan((acc, dependency) => { // @ts-expect-error Dynamic key assignment acc[dependency.id] = { ...dependencies[dependency.id], ...dependency, }; return acc; }, dependencies), startWith(dependencies), ); }; export const loadDependenciesArray = (plugin: McpToolsPlugin) => loadDependencies(plugin).pipe( map((deps) => Object.values(deps) as Dependencies[keyof Dependencies][]), ); export * from "./logger"; ``` -------------------------------------------------------------------------------- /docs/features/mcp-server-install.md: -------------------------------------------------------------------------------- ```markdown # MCP Server Installation Feature Requirements ## Overview This feature enables users to install and manage the MCP server executable through the Obsidian plugin settings interface. The system handles the download of platform-specific binaries, Claude Desktop configuration, and provides clear user feedback throughout the process. ## Implementation Location The installation feature is implemented in the Obsidian plugin package under `src/features/mcp-server-install`. ## Installation Flow 1. User Prerequisites: - Claude Desktop installed - Local REST API plugin installed and configured with API key - (Optional) Templater plugin for enhanced functionality - (Optional) Smart Connections plugin for enhanced search 2. Installation Steps: - User navigates to plugin settings - Plugin verifies prerequisites and shows status - User initiates installation via button - Plugin retrieves API key from Local REST API plugin - Plugin downloads appropriate binary - Plugin updates Claude config file - Plugin confirms successful installation ## Settings UI Requirements The settings UI is implemented as a Svelte component in `components/SettingsTab.svelte`. 1. Component Structure: ```svelte <script lang="ts"> // Import Svelte stores for state management import { installationStatus } from '../stores/status'; import { dependencies } from '../stores/dependencies'; // Props from parent Settings.svelte export let plugin: Plugin; </script> <!-- Installation status and controls --> <div class="installation-status"> <!-- Dynamic content based on $installationStatus --> </div> <!-- Dependencies section --> <div class="dependencies"> <!-- Dynamic content based on $dependencies --> </div> <!-- Links section --> <div class="links"> <!-- External resource links --> </div> ``` 2. Display Elements: - Installation status indicator with version - Install/Update/Uninstall buttons - Dependency status and links - Links to: - Downloaded executable location (with folder access) - Log folder location (with folder access) - GitHub repository - Claude Desktop download page (when needed) - Required and recommended plugins 3. State Management: - Uses Svelte stores for reactive state - Status states: - Not Installed - Installing - Installed - Update Available ## Download Management 1. Binary Source: - GitHub latest release - Platform-specific naming conventions - Version number included in filename (e.g., mcp-server-1.2.3) 2. Installation Locations: - Binary: {vault}/.obsidian/plugins/{plugin-id}/bin/ - Logs: - macOS: ~/Library/Logs/obsidian-mcp-tools - Windows: %APPDATA%\obsidian-mcp-tools\logs - Linux: (platform-specific path) ## Claude Configuration 1. Config File: - Location: ~/Library/Application Support/Claude/claude_desktop_config.json - Create base structure if missing: { "mcpServers": {} } - Add/update only our config entry: ```json { "mcpServers": { "obsidian-mcp-tools": { "command": "(absolute path to executable)", "env": { "OBSIDIAN_API_KEY": "(stored api key)" } } } } ``` ## Version Management 1. Unified Version Approach: - Plugin and server share same version number - Version stored in plugin manifest - Server provides version via `--version` flag - Version checked during plugin initialization ## User Education 1. Documentation Requirements: - README.md must explain: - Binary download and installation process - GitHub source code location - Claude config file modifications - Log file locations and purpose - Settings page must link to full documentation ## Error Handling 1. Installation Errors: - Claude Desktop not installed - Download failures - Permission issues - Version mismatch 2. User Feedback: - Use Obsidian Notice API for progress/status - Clear error messages with next steps - Links to troubleshooting resources ## Uninstall Process 1. Cleanup Actions: - Remove executable - Remove our entry from Claude config - Clear stored plugin data ## Appendix: Implementation Insights ### Feature Organization The feature follows a modular structure: ``` src/features/mcp-server-install/ ├── components/ # Svelte components │ └── SettingsTab.svelte ├── services/ # Core functionality │ ├── config.ts # Claude config management │ ├── download.ts # Binary download │ ├── status.ts # Installation status │ └── uninstall.ts # Cleanup operations ├── stores/ # Svelte stores │ ├── status.ts # Installation status store │ └── dependencies.ts # Dependencies status store ├── utils/ # Shared utilities │ └── openFolder.ts ├── constants.ts # Configuration ├── types.ts # Type definitions └── index.ts # Feature setup & component export ``` ### Key Implementation Decisions 1. API Key Management - Removed manual API key input - Automatically retrieved from Local REST API plugin - Reduces user friction and potential errors 2. Symlink Resolution - Added robust symlink handling for binary paths - Ensures correct operation even with complex vault setups - Handles non-existent paths during resolution 3. Status Management - Unified status interface with version tracking - Real-time status updates during operations - Clear feedback for update availability 4. Error Handling - Comprehensive prerequisite validation - Detailed error messages with next steps - Proper cleanup on failures - Extensive logging for troubleshooting 5. User Experience - Reactive UI with Svelte components - One-click installation process - Direct access to logs and binaries - Clear dependency requirements - Links to all required and recommended plugins - Real-time status updates through Svelte stores ### Recommended Plugins Added information about recommended plugins that enhance functionality: - Templater: For template-based operations - Smart Connections: For enhanced search capabilities - Local REST API: Required for Obsidian communication ### Platform Compatibility Implemented robust platform detection and path handling: - Windows: Handles UNC paths and environment variables - macOS: Proper binary permissions and config paths - Linux: Flexible configuration for various distributions ### Future Considerations 1. Version Management - Consider automated update checks - Add update notifications - Implement rollback capability 2. Configuration - Add backup/restore of Claude config - Support custom binary locations - Allow custom log paths 3. Error Recovery - Add self-repair functionality - Implement health checks - Add diagnostic tools ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/services/install.ts: -------------------------------------------------------------------------------- ```typescript import fs from "fs"; import fsp from "fs/promises"; import https from "https"; import { Notice, Plugin } from "obsidian"; import os from "os"; import { Observable } from "rxjs"; import { logger } from "$/shared"; import { GITHUB_DOWNLOAD_URL, type Arch, type Platform } from "../constants"; import type { DownloadProgress, InstallPathInfo } from "../types"; import { getInstallPath } from "./status"; export function getPlatform(): Platform { const platform = os.platform(); switch (platform) { case "darwin": return "macos"; case "win32": return "windows"; default: return "linux"; } } export function getArch(): Arch { return os.arch() as Arch; } export function getDownloadUrl(platform: Platform, arch: Arch): string { if (platform === "windows") { return `${GITHUB_DOWNLOAD_URL}/mcp-server-windows.exe`; } else if (platform === "macos") { return `${GITHUB_DOWNLOAD_URL}/mcp-server-macos-${arch}`; } else { // linux return `${GITHUB_DOWNLOAD_URL}/mcp-server-linux`; // Linux binary doesn't include arch in filename } } /** * Ensures that the specified directory path exists and is writable. * * If the directory does not exist, it will be created recursively. If the directory * exists but is not writable, an error will be thrown. * * @param dirpath - The real directory path to ensure exists and is writable. * @throws {Error} If the directory does not exist or is not writable. */ export async function ensureDirectory(dirpath: string) { try { if (!fs.existsSync(dirpath)) { await fsp.mkdir(dirpath, { recursive: true }); } // Verify directory was created and is writable try { await fsp.access(dirpath, fs.constants.W_OK); } catch (accessError) { throw new Error(`Directory exists but is not writable: ${dirpath}`); } } catch (error) { logger.error(`Failed to ensure directory:`, { error }); throw error; } } export function downloadFile( url: string, outputPath: string, redirects = 0, ): Observable<DownloadProgress> { return new Observable((subscriber) => { if (redirects > 5) { subscriber.error(new Error("Too many redirects")); return; } let fileStream: fs.WriteStream | undefined; const cleanup = (err?: unknown) => { if (err) { logger.debug("Cleaning up incomplete download:", { outputPath, writableFinished: JSON.stringify(fileStream?.writableFinished), error: err instanceof Error ? err.message : String(err), }); fileStream?.destroy(); fsp.unlink(outputPath).catch((unlinkError) => { logger.error("Failed to clean up incomplete download:", { outputPath, error: unlinkError instanceof Error ? unlinkError.message : String(unlinkError), }); }); } else { fileStream?.close(); fsp.chmod(outputPath, 0o755).catch((chmodError) => { logger.error("Failed to set executable permissions:", { outputPath, error: chmodError instanceof Error ? chmodError.message : String(chmodError), }); }); } }; https .get(url, (response) => { try { if (!response) { throw new Error("No response received"); } const statusCode = response.statusCode ?? 0; // Handle various HTTP status codes if (statusCode >= 400) { throw new Error( `HTTP Error ${statusCode}: ${response.statusMessage}`, ); } if (statusCode === 302 || statusCode === 301) { const redirectUrl = response.headers.location; if (!redirectUrl) { throw new Error( `Redirect (${statusCode}) received but no location header found`, ); } // Handle redirect by creating a new observable downloadFile(redirectUrl, outputPath, redirects + 1).subscribe( subscriber, ); return; } if (statusCode !== 200) { throw new Error(`Unexpected status code: ${statusCode}`); } // Validate content length const contentLength = response.headers["content-length"]; const totalBytes = contentLength ? parseInt(contentLength, 10) : 0; if (contentLength && isNaN(totalBytes)) { throw new Error("Invalid content-length header"); } try { fileStream = fs.createWriteStream(outputPath, { flags: "w", }); } catch (err) { throw new Error( `Failed to create write stream: ${err instanceof Error ? err.message : String(err)}`, ); } let downloadedBytes = 0; fileStream.on("error", (err) => { const fileStreamError = new Error( `File stream error: ${err.message}`, ); cleanup(fileStreamError); subscriber.error(fileStreamError); }); response.on("data", (chunk: Buffer) => { try { if (!Buffer.isBuffer(chunk)) { throw new Error("Received invalid data chunk"); } downloadedBytes += chunk.length; const percentage = totalBytes ? (downloadedBytes / totalBytes) * 100 : 0; subscriber.next({ bytesReceived: downloadedBytes, totalBytes, percentage: Math.round(percentage * 100) / 100, }); } catch (err) { cleanup(err); subscriber.error(err); } }); response.pipe(fileStream); fileStream.on("finish", () => { cleanup(); subscriber.complete(); }); response.on("error", (err) => { cleanup(err); subscriber.error(new Error(`Response error: ${err.message}`)); }); } catch (err) { cleanup(err); subscriber.error(err instanceof Error ? err : new Error(String(err))); } }) .on("error", (err) => { cleanup(err); subscriber.error(new Error(`Network error: ${err.message}`)); }); }); } export async function installMcpServer( plugin: Plugin, ): Promise<InstallPathInfo> { try { const platform = getPlatform(); const arch = getArch(); const downloadUrl = getDownloadUrl(platform, arch); const installPath = await getInstallPath(plugin); if ("error" in installPath) throw new Error(installPath.error); await ensureDirectory(installPath.dir); const progressNotice = new Notice("Downloading MCP server...", 0); logger.debug("Downloading MCP server:", { downloadUrl, installPath }); const download$ = downloadFile(downloadUrl, installPath.path); return new Promise((resolve, reject) => { download$.subscribe({ next: (progress: DownloadProgress) => { progressNotice.setMessage( `Downloading MCP server: ${progress.percentage}%`, ); }, error: (error: Error) => { progressNotice.hide(); new Notice(`Failed to download MCP server: ${error.message}`); logger.error("Download failed:", { error, installPath }); reject(error); }, complete: () => { progressNotice.hide(); new Notice("MCP server downloaded successfully!"); logger.info("MCP server downloaded", { installPath }); resolve(installPath); }, }); }); } catch (error) { new Notice( `Failed to install MCP server: ${error instanceof Error ? error.message : String(error)}`, ); throw error; } } ``` -------------------------------------------------------------------------------- /packages/shared/src/types/plugin-local-rest-api.ts: -------------------------------------------------------------------------------- ```typescript import { type } from "arktype"; /** * Error response from the API * Content-Type: application/json * Used in various error responses across endpoints * @property errorCode - A 5-digit error code uniquely identifying this particular type of error * @property message - Message describing the error */ export const ApiError = type({ errorCode: "number", message: "string", }); /** * JSON representation of a note including parsed tag and frontmatter data as well as filesystem metadata * Content-Type: application/vnd.olrapi.note+json * GET /vault/{filename} or GET /active/ with Accept: application/vnd.olrapi.note+json */ export const ApiNoteJson = type({ content: "string", frontmatter: "Record<string, string>", path: "string", stat: { ctime: "number", mtime: "number", size: "number", }, tags: "string[]", }); /** * Defines the structure of a plugin manifest, which contains metadata about a plugin. * This type is used to represent the response from the API's root endpoint, providing * basic server details and authentication status. */ const ApiPluginManifest = type({ id: "string", name: "string", version: "string", minAppVersion: "string", description: "string", author: "string", authorUrl: "string", isDesktopOnly: "boolean", dir: "string", }); /** * Response from the root endpoint providing basic server details and authentication status * Content-Type: application/json * GET / - This is the only API request that does not require authentication */ export const ApiStatusResponse = type({ status: "string", manifest: ApiPluginManifest, versions: { obsidian: "string", self: "string", }, service: "string", authenticated: "boolean", certificateInfo: { validityDays: "number", regenerateRecommended: "boolean", }, apiExtensions: ApiPluginManifest.array(), }); /** * Response from searching vault files using advanced search * Content-Type: application/json * POST /search/ * Returns array of matching files and their results * Results are only returned for non-falsy matches */ export const ApiSearchResponse = type({ filename: "string", result: "string|number|string[]|object|boolean", }).array(); /** * Match details for simple text search results * Content-Type: application/json * Used in ApiSimpleSearchResult */ export const ApiSimpleSearchMatch = type({ match: { start: "number", end: "number", }, context: "string", }); /** * Result from searching vault files with simple text search * Content-Type: application/json * POST /search/simple/ * Returns matches with surrounding context */ export const ApiSimpleSearchResponse = type({ filename: "string", matches: ApiSimpleSearchMatch.array(), score: "number", }).array(); /** * Result entry from semantic search * Content-Type: application/json * Used in ApiSearchResponse */ export const ApiSmartSearchResult = type({ path: "string", text: "string", score: "number", breadcrumbs: "string", }); /** * Response from semantic search containing list of matching results * Content-Type: application/json * POST /search/smart/ */ export const ApiSmartSearchResponse = type({ results: ApiSmartSearchResult.array(), }); /** * Parameters for semantic search request * Content-Type: application/json * POST /search/smart/ * @property query - A search phrase for semantic search * @property filter.folders - An array of folder names to include. For example, ["Public", "Work"] * @property filter.excludeFolders - An array of folder names to exclude. For example, ["Private", "Archive"] * @property filter.limit - The maximum number of results to return */ export const ApiSearchParameters = type({ query: "string", filter: { folders: "string[]?", excludeFolders: "string[]?", limit: "number?", }, }); /** * Command information from Obsidian's command palette * Content-Type: application/json * Used in ApiCommandsResponse */ export const ApiCommand = type({ id: "string", name: "string", }); /** * Response containing list of available Obsidian commands * Content-Type: application/json * GET /commands/ */ export const ApiCommandsResponse = type({ commands: ApiCommand.array(), }); /** * Response containing list of files in a vault directory * Content-Type: application/json * GET /vault/ or GET /vault/{pathToDirectory}/ * Note that empty directories will not be returned */ export const ApiVaultDirectoryResponse = type({ files: "string[]", }); /** * Response containing vault file information * Content-Type: application/json * POST /vault/{pathToFile} * Returns array of matching files and their results * Results are only returned for non-falsy matches */ export const ApiVaultFileResponse = type({ frontmatter: { tags: "string[]", description: "string?", }, content: "string", path: "string", stat: { ctime: "number", mtime: "number", size: "number", }, tags: "string[]", }); /** * Parameters for patching a file or document in the Obsidian plugin's REST API. * This type defines the expected request body for the patch operation. * * @property operation - Specifies how to modify the content: append (add after), prepend (add before), or replace existing content * @property targetType - Identifies what to modify: a section under a heading, a referenced block, or a frontmatter field * @property target - The identifier - either heading path (e.g. 'Heading 1::Subheading 1:1'), block reference ID, or frontmatter field name * @property targetDelimiter - The separator used in heading paths to indicate nesting (default '::') * @property trimTargetWhitespace - Whether to remove whitespace from target identifier before matching (default: false) * @property content - The actual content to insert, append, or use as replacement * @property contentType - Format of the content - use application/json for structured data like table rows or frontmatter values */ export const ApiPatchParameters = type({ operation: type("'append' | 'prepend' | 'replace'").describe( "Specifies how to modify the content: append (add after), prepend (add before), or replace existing content", ), targetType: type("'heading' | 'block' | 'frontmatter'").describe( "Identifies what to modify: a section under a heading, a referenced block, or a frontmatter field", ), target: type("string").describe( "The identifier - either heading path (e.g. 'Heading 1::Subheading 1:1'), block reference ID, or frontmatter field name", ), "targetDelimiter?": type("string").describe( "The separator used in heading paths to indicate nesting (default '::')", ), "trimTargetWhitespace?": type("boolean").describe( "Whether to remove whitespace from target identifier before matching (default: false)", ), content: type("string").describe( "The actual content to insert, append, or use as replacement", ), "contentType?": type("'text/markdown' | 'application/json'").describe( "Format of the content - use application/json for structured data like table rows or frontmatter values", ), }); /** * Represents a response containing markdown content */ export const ApiContentResponse = type("string").describe("Content"); /** * Empty response for successful operations that don't return content * Content-Type: none (204 No Content) * Used by: * - PUT /vault/{filename} * - PUT /active/ * - PUT /periodic/{period}/ * - POST /commands/{commandId}/ * - DELETE endpoints * Returns 204 No Content */ export const ApiNoContentResponse = type("unknown").describe("No Content"); /** * Parameters for executing a template * Content-Type: application/json * POST /templates/execute/ * @property name - The name of the template to execute * @property arguments - A key-value object of arguments to pass to the template * @property createFile - Whether to create a new file from the template * @property targetPath - The path to save the file; required if createFile is true */ export const ApiTemplateExecutionParams = type({ name: type("string").describe("The full vault path to the template file"), arguments: "Record<string, string>", "createFile?": type("boolean").describe( "Whether to create a new file from the template", ), "targetPath?": type("string").describe( "Path to save the file; required if createFile is true", ), }); /** * Response from executing a template * Content-Type: application/json * POST /templates/execute/ * @property message - A message describing the result of the template execution */ export const ApiTemplateExecutionResponse = type({ message: "string", content: "string", }); // Export types for TypeScript usage export type ApiErrorType = typeof ApiError.infer; export type ApiNoteJsonType = typeof ApiNoteJson.infer; export type ApiStatusResponseType = typeof ApiStatusResponse.infer; export type ApiSearchResponseType = typeof ApiSearchResponse.infer; export type ApiSimpleSearchResponseType = typeof ApiSimpleSearchResponse.infer; export type ApiSmartSearchResultType = typeof ApiSmartSearchResult.infer; export type ApiSmartSearchResponseType = typeof ApiSmartSearchResponse.infer; export type ApiCommandType = typeof ApiCommand.infer; export type ApiCommandsResponseType = typeof ApiCommandsResponse.infer; export type ApiVaultDirectoryResponseType = typeof ApiVaultDirectoryResponse.infer; export type ApiVaultFileResponseType = typeof ApiVaultFileResponse.infer; export type ApiSearchParametersType = typeof ApiSearchParameters.infer; export type ApiNoContentResponseType = typeof ApiNoContentResponse.infer; export type ApiTemplateExecutionParamsType = typeof ApiTemplateExecutionParams.infer; export type ApiTemplateExecutionResponseType = typeof ApiTemplateExecutionResponse.infer; // Additional API response types can be added here export const MIME_TYPE_OLRAPI_NOTE_JSON = "application/vnd.olrapi.note+json"; ```