This is page 1 of 5. Use http://codebase.md/cyanheads/obsidian-mcp-server?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .clinerules ├── .github │ ├── FUNDING.yml │ └── workflows │ └── publish.yml ├── .gitignore ├── .ncurc.json ├── CHANGELOG.md ├── Dockerfile ├── docs │ ├── obsidian_mcp_tools_spec.md │ ├── obsidian-api │ │ ├── obsidian_rest_api_spec.json │ │ └── obsidian_rest_api_spec.yaml │ └── tree.md ├── env.json ├── LICENSE ├── mcp.json ├── package-lock.json ├── package.json ├── README.md ├── repomix.config.json ├── scripts │ ├── clean.ts │ ├── fetch-openapi-spec.ts │ ├── make-executable.ts │ └── tree.ts ├── smithery.yaml ├── src │ ├── config │ │ └── index.ts │ ├── index.ts │ ├── mcp-server │ │ ├── server.ts │ │ ├── tools │ │ │ ├── obsidianDeleteNoteTool │ │ │ │ ├── index.ts │ │ │ │ ├── logic.ts │ │ │ │ └── registration.ts │ │ │ ├── obsidianGlobalSearchTool │ │ │ │ ├── index.ts │ │ │ │ ├── logic.ts │ │ │ │ └── registration.ts │ │ │ ├── obsidianListNotesTool │ │ │ │ ├── index.ts │ │ │ │ ├── logic.ts │ │ │ │ └── registration.ts │ │ │ ├── obsidianManageFrontmatterTool │ │ │ │ ├── index.ts │ │ │ │ ├── logic.ts │ │ │ │ └── registration.ts │ │ │ ├── obsidianManageTagsTool │ │ │ │ ├── index.ts │ │ │ │ ├── logic.ts │ │ │ │ └── registration.ts │ │ │ ├── obsidianReadNoteTool │ │ │ │ ├── index.ts │ │ │ │ ├── logic.ts │ │ │ │ └── registration.ts │ │ │ ├── obsidianSearchReplaceTool │ │ │ │ ├── index.ts │ │ │ │ ├── logic.ts │ │ │ │ └── registration.ts │ │ │ └── obsidianUpdateNoteTool │ │ │ ├── index.ts │ │ │ ├── logic.ts │ │ │ └── registration.ts │ │ └── transports │ │ ├── auth │ │ │ ├── core │ │ │ │ ├── authContext.ts │ │ │ │ ├── authTypes.ts │ │ │ │ └── authUtils.ts │ │ │ ├── index.ts │ │ │ └── strategies │ │ │ ├── jwt │ │ │ │ └── jwtMiddleware.ts │ │ │ └── oauth │ │ │ └── oauthMiddleware.ts │ │ ├── httpErrorHandler.ts │ │ ├── httpTransport.ts │ │ └── stdioTransport.ts │ ├── services │ │ └── obsidianRestAPI │ │ ├── index.ts │ │ ├── methods │ │ │ ├── activeFileMethods.ts │ │ │ ├── commandMethods.ts │ │ │ ├── openMethods.ts │ │ │ ├── patchMethods.ts │ │ │ ├── periodicNoteMethods.ts │ │ │ ├── searchMethods.ts │ │ │ └── vaultMethods.ts │ │ ├── service.ts │ │ ├── types.ts │ │ └── vaultCache │ │ ├── index.ts │ │ └── service.ts │ ├── types-global │ │ └── errors.ts │ └── utils │ ├── index.ts │ ├── internal │ │ ├── asyncUtils.ts │ │ ├── errorHandler.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ └── requestContext.ts │ ├── metrics │ │ ├── index.ts │ │ └── tokenCounter.ts │ ├── obsidian │ │ ├── index.ts │ │ ├── obsidianApiUtils.ts │ │ └── obsidianStatUtils.ts │ ├── parsing │ │ ├── dateParser.ts │ │ ├── index.ts │ │ └── jsonParser.ts │ └── security │ ├── idGenerator.ts │ ├── index.ts │ ├── rateLimiter.ts │ └── sanitization.ts ├── tsconfig.json └── typedoc.json ``` # Files -------------------------------------------------------------------------------- /.ncurc.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "reject": ["chrono-node"] 3 | } 4 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Operating System Files 2 | .DS_Store 3 | .DS_Store? 4 | ._* 5 | .Spotlight-V100 6 | .Trashes 7 | ehthumbs.db 8 | Thumbs.db 9 | 10 | # IDE and Editor Files 11 | .idea/ 12 | .vscode/ 13 | *.swp 14 | *.swo 15 | *~ 16 | *.sublime-workspace 17 | *.sublime-project 18 | 19 | # TypeScript 20 | *.tsbuildinfo 21 | .tscache/ 22 | *.js.map 23 | *.tgz 24 | .npm 25 | .eslintcache 26 | .rollup.cache 27 | *.mjs.map 28 | *.cjs.map 29 | *.d.ts.map 30 | *.d.ts 31 | !*.d.ts.template 32 | .pnp.js 33 | .pnp.cjs 34 | .pnp.mjs 35 | .pnp.json 36 | .pnp.ts 37 | 38 | # Demo and Example Directories 39 | demo/ 40 | demos/ 41 | example/ 42 | examples/ 43 | samples/ 44 | .sample-env 45 | sample.* 46 | !sample.template.* 47 | 48 | # Node.js 49 | node_modules/ 50 | npm-debug.log* 51 | yarn-debug.log* 52 | yarn-error.log* 53 | .pnpm-debug.log* 54 | .env 55 | .env.local 56 | .env.development.local 57 | .env.test.local 58 | .env.production.local 59 | 60 | # Python 61 | __pycache__/ 62 | *.py[cod] 63 | *$py.class 64 | *.so 65 | .Python 66 | build/ 67 | develop-eggs/ 68 | dist/ 69 | downloads/ 70 | eggs/ 71 | .eggs/ 72 | lib/ 73 | lib64/ 74 | parts/ 75 | sdist/ 76 | var/ 77 | wheels/ 78 | *.egg-info/ 79 | .installed.cfg 80 | *.egg 81 | .pytest_cache/ 82 | .coverage 83 | htmlcov/ 84 | .tox/ 85 | .venv 86 | venv/ 87 | ENV/ 88 | 89 | # Java 90 | *.class 91 | *.log 92 | *.jar 93 | *.war 94 | *.nar 95 | *.ear 96 | *.zip 97 | *.tar.gz 98 | *.rar 99 | hs_err_pid* 100 | target/ 101 | .gradle/ 102 | build/ 103 | 104 | # Ruby 105 | *.gem 106 | *.rbc 107 | /.config 108 | /coverage/ 109 | /InstalledFiles 110 | /pkg/ 111 | /spec/reports/ 112 | /spec/examples.txt 113 | /test/tmp/ 114 | /test/version_tmp/ 115 | /tmp/ 116 | .byebug_history 117 | 118 | # Compiled Files 119 | *.com 120 | *.class 121 | *.dll 122 | *.exe 123 | *.o 124 | *.so 125 | 126 | # Package Files 127 | *.7z 128 | *.dmg 129 | *.gz 130 | *.iso 131 | *.rar 132 | *.tar 133 | *.zip 134 | 135 | # Logs and Databases 136 | *.log 137 | *.sql 138 | *.sqlite 139 | *.sqlite3 140 | 141 | # Build and Distribution 142 | dist/ 143 | build/ 144 | out/ 145 | 146 | # Testing 147 | coverage/ 148 | .nyc_output/ 149 | 150 | # Cache 151 | .cache/ 152 | .parcel-cache/ 153 | 154 | # Misc 155 | .DS_Store 156 | .env.local 157 | .env.development.local 158 | .env.test.local 159 | .env.production.local 160 | *.bak 161 | *.swp 162 | *.swo 163 | *~ 164 | .history/ 165 | repomix-output* 166 | mcp-servers.json 167 | mcp-config.json 168 | 169 | # Generated Documentation 170 | docs/api/ 171 | 172 | logs/ ``` -------------------------------------------------------------------------------- /.clinerules: -------------------------------------------------------------------------------- ``` 1 | # Obsidian MCP Server Developer Cheatsheet 2 | 3 | This cheatsheet provides quick references for common patterns, utilities, server configuration, and the Obsidian REST API service within the `obsidian-mcp-server` codebase, based on the `mcp-ts-template` and updated for MCP Spec 2025-03-26. 4 | 5 | # Instructions for using this file: 6 | 7 | 1. Carefully review this file line by line to understand this repo and Model Context Protocol (MCP). 8 | 2. If you are creating new MCP Server tools, review the files: 9 | 10 | - `src/mcp-server/tools/obsidianUpdateNoteTool` (all files) 11 | - `src/mcp-server/tools/obsidianGlobalSearchTool` (all files) 12 | - `src/services/obsidianRestAPI` (Any files relevant to the tool you are creating) 13 | - `src/services/obsidianRestAPI/vaultCache` (If the tool needs vault structure/metadata caching) 14 | 15 | 3. Keep this file updated to accurately reflect the state of the code base 16 | 17 | ## Server Transports & Configuration 18 | 19 | The server can run using different communication transports, configured via environment variables. 20 | 21 | - **`MCP_TRANSPORT_TYPE`**: Specifies the transport. 22 | - `"stdio"` (Default): Uses standard input/output for communication. Suitable for direct integration with parent processes. 23 | - `"http"`: Uses Streamable HTTP Server-Sent Events (SSE) for communication. Runs a Hono server. 24 | - **`MCP_HTTP_PORT`**: Port for the HTTP server (Default: `3010`). Used only if `MCP_TRANSPORT_TYPE=http`. 25 | - **`MCP_HTTP_HOST`**: Host address for the HTTP server (Default: `127.0.0.1`). Used only if `MCP_TRANSPORT_TYPE=http`. 26 | - **`MCP_ALLOWED_ORIGINS`**: Comma-separated list of allowed origins for HTTP requests (e.g., `http://localhost:8080,https://my-frontend.com`). Used only if `MCP_TRANSPORT_TYPE=http`. 27 | - **`MCP_LOG_LEVEL`**: Minimum logging level for the server (e.g., "debug", "info", "warning", "error", "notice", "crit", "alert", "emerg"). Defaults to "info". Affects both file logging and MCP notifications. 28 | - **`MCP_AUTH_MODE`**: Authentication strategy to use for the HTTP transport. Can be `jwt` or `oauth`. 29 | - **`MCP_AUTH_SECRET_KEY`**: **Required if `MCP_AUTH_MODE=jwt`**. Secret key (min 32 chars) for signing/verifying JWTs. **MUST be set in production for JWT mode.** 30 | - **`OAUTH_ISSUER_URL`**: **Required if `MCP_AUTH_MODE=oauth`**. The URL of the OAuth 2.1 token issuer. 31 | - **`OAUTH_AUDIENCE`**: **Required if `MCP_AUTH_MODE=oauth`**. The audience claim for the OAuth tokens. 32 | - **`OAUTH_JWKS_URI`**: Optional URI for the JSON Web Key Set. If omitted, it will be derived from the `OAUTH_ISSUER_URL`. 33 | - **`OBSIDIAN_API_KEY`**: **Required.** API key for the Obsidian Local REST API plugin. 34 | - **`OBSIDIAN_BASE_URL`**: **Required.** Base URL for the Obsidian Local REST API (e.g., `http://127.0.0.1:27123`). 35 | - **`OBSIDIAN_VERIFY_SSL`**: Set to `false` to disable SSL certificate verification for the Obsidian API (e.g., for self-signed certs). Defaults to `true`. 36 | - **`OBSIDIAN_ENABLE_CACHE`**: Set to `true` (default) or `false` to enable or disable the in-memory vault cache. 37 | - **`OBSIDIAN_CACHE_REFRESH_INTERVAL_MIN`**: Interval in minutes for the vault cache to refresh automatically. Defaults to `10`. 38 | 39 | ### HTTP Transport Details (`MCP_TRANSPORT_TYPE=http`) 40 | 41 | - **Endpoint**: A single endpoint `/mcp` handles all communication. 42 | - `POST /mcp`: Client sends requests/notifications to the server. Requires `mcp-session-id` header for subsequent requests after initialization. Server responds with JSON or initiates SSE stream. 43 | - `GET /mcp`: Client initiates SSE stream for server-sent messages. Requires `mcp-session-id` header. 44 | - `DELETE /mcp`: Client signals session termination. Requires `mcp-session-id` header. 45 | - **Session Management**: Each client connection establishes a session identified by the `mcp-session-id` header. The server maintains state per session. 46 | - **Security**: Robust origin checking is implemented via CORS middleware. Configure `MCP_ALLOWED_ORIGINS` for production environments. 47 | 48 | ### Running the Server 49 | 50 | - **Stdio**: `npm run start:stdio` 51 | - **HTTP**: `npm run start:http` (Ensure `OBSIDIAN_API_KEY` and `OBSIDIAN_BASE_URL` are set. Also configure either JWT or OAuth variables as needed. Optionally set `MCP_HTTP_PORT`, `MCP_HTTP_HOST`, `MCP_ALLOWED_ORIGINS`, `MCP_LOG_LEVEL`, `OBSIDIAN_VERIFY_SSL`). 52 | 53 | ## Model Context Protocol (MCP) Overview (Spec: 2025-03-26) 54 | 55 | MCP provides a standardized way for LLMs (via host applications) to interact with external capabilities (tools, data) exposed by dedicated servers. 56 | 57 | ### Core Concepts & Architecture 58 | 59 | - **Host:** Manages clients, LLM integration, security, and user consent (e.g., Claude Desktop, VS Code). 60 | - **Client:** Resides in the host, connects 1:1 to a server, handles protocol. 61 | - **Server:** Standalone process exposing capabilities (Resources, Tools, Prompts). Focuses on its domain, isolated from LLM/other servers. 62 | 63 | ```mermaid 64 | graph LR 65 | subgraph "Host Application Process" 66 | H[Host] 67 | C1[Client 1] 68 | C2[Client 2] 69 | H --> C1 70 | H --> C2 71 | end 72 | subgraph "Server Process 1" 73 | S1["MCP Server A<br>(e.g., Filesystem)"] 74 | R1["Local Resource<br>e.g., Files"] 75 | S1 <--> R1 76 | end 77 | subgraph "Server Process 2" 78 | S2["MCP Server B<br>(e.g., API Wrapper)"] 79 | R2["Remote Resource<br>e.g., Web API"] 80 | S2 <--> R2 81 | end 82 | C1 <-->|MCP Protocol| S1 83 | C2 <-->|MCP Protocol| S2 84 | ``` 85 | 86 | - **Key Principles:** Simplicity, Composability, Isolation, Progressive Features. 87 | 88 | ### Protocol Basics 89 | 90 | - **Communication:** JSON-RPC 2.0 over a transport (Stdio, Streamable HTTP). 91 | - **Messages:** Requests (with `id`), Responses (`id` + `result`/`error`), Notifications (no `id`). Batches MUST be supported for receiving. 92 | - **Lifecycle:** 93 | 1. **Initialization:** Client sends `initialize` (version, capabilities, clientInfo). Server responds (`initialize` response: agreed version, capabilities, serverInfo, instructions?). Client sends `initialized` notification. 94 | 2. **Operation:** Message exchange based on negotiated capabilities. 95 | 3. **Shutdown:** Transport disconnect. 96 | 97 | ### Server Capabilities 98 | 99 | Servers expose functionality via: 100 | 101 | 1. **Resources:** 102 | 103 | - **Purpose:** Expose data/content (files, DB records) as context. 104 | - **Control:** Application-controlled. 105 | - **ID:** Unique URI (e.g., `file:///path/to/doc.txt`). 106 | - **Discovery:** `resources/list` (paginated), `resources/templates/list` (paginated). 107 | - **Reading:** `resources/read` -> `ResourceContent` array (`text` or `blob`). 108 | - **Updates (Optional):** `listChanged: true` -> `notifications/resources/list_changed`. `subscribe: true` -> `resources/subscribe`, `notifications/resources/updated`, **MUST handle `resources/unsubscribe` request**. 109 | 110 | 2. **Tools:** 111 | 112 | - **Purpose:** Expose executable functions for LLM invocation (via client). 113 | - **Control:** Model-controlled. 114 | - **Definition:** `Tool` object (`name`, `description`, `inputSchema` (JSON Schema), `annotations?`). Annotations (`title`, `readOnlyHint`, etc.) are untrusted hints. 115 | - **Discovery:** `tools/list` (paginated). 116 | - **Invocation:** `tools/call` (`name`, `arguments`) -> `CallToolResult` (`content` array, `isError: boolean`). Execution errors reported via `isError: true`. **Rich schemas are crucial.** 117 | - **Updates (Optional):** `listChanged: true` -> `notifications/tools/list_changed` (MUST send after dynamic changes). 118 | 119 | 3. **Prompts:** 120 | - **Purpose:** Reusable prompt templates/workflows (e.g., slash commands). 121 | - **Control:** User-controlled. 122 | - **Definition:** `Prompt` object (`name`, `description?`, `arguments?`). 123 | - **Discovery:** `prompts/list` (paginated). 124 | - **Usage:** `prompts/get` (`name`, `arguments`) -> `GetPromptResult` (`messages` array). 125 | - **Updates (Optional):** `listChanged: true` -> `notifications/prompts/list_changed`. 126 | 127 | ### Interacting with Client Capabilities 128 | 129 | - **Roots:** Client may provide filesystem roots (`file://`). Server receives list on init, updates via `notifications/roots/list_changed` (if supported). Servers SHOULD respect roots. 130 | - **Sampling:** Server can request LLM completion via client using `sampling/createMessage`. Client SHOULD implement human-in-the-loop. 131 | 132 | ### Server Utilities 133 | 134 | - **Logging:** `logging` capability -> `notifications/message` (RFC 5424 levels: `debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency`). Client can send `logging/setLevel`. 135 | - **Pagination:** List operations use `cursor`/`nextCursor`. 136 | - **Completion:** `completions` capability -> `completion/complete`. 137 | - **Cancellation:** `notifications/cancelled` (best-effort). 138 | - **Ping:** `ping` request -> `{}` response. 139 | - **Progress:** `notifications/progress` (requires `_meta.progressToken` in original request). 140 | - **Configuration:** `configuration/get`, `configuration/set`. 141 | - **Back-pressure:** Clients debounce rapid notifications. Servers should aim for idempotency. 142 | 143 | ### SDK Usage (TypeScript) - IMPORTANT 144 | 145 | - **High-Level SDK Abstractions (Strongly Recommended):** 146 | - **Use `server.tool(name, description, zodSchemaShape, handler)`:** This is the **preferred and strongly recommended** way to define tools. It automatically handles: 147 | - Registering the tool for `tools/list`. 148 | - Generating the JSON Schema from the Zod shape. 149 | - Validating incoming `tools/call` arguments against the schema. 150 | - Routing the call to your handler with validated arguments. 151 | - Formatting the `CallToolResult`. 152 | - **Use `server.resource(regName, template, metadata, handler)`:** Similarly recommended for resources. 153 | - **Benefits:** Significantly reduces boilerplate, enforces type safety, simplifies protocol adherence. 154 | - **Low-Level SDK Handlers (AVOID unless absolutely necessary):** 155 | - Manually using `server.setRequestHandler(SchemaObject, handler)` requires you to handle schema generation, argument validation, request routing, and response formatting yourself. 156 | - **CRITICAL WARNING:** **Do NOT mix high-level (`server.tool`, `server.resource`) and low-level (`server.setRequestHandler`) approaches for the _same capability type_ (e.g., tools).** The SDK's internal state management and type handling can become confused, leading to unexpected errors or incorrect behavior. Stick to one approach per capability type, **strongly preferring the high-level abstractions.** 157 | 158 | ### Security Considerations 159 | 160 | - **Input Validation:** Use schemas (Zod), sanitize inputs (paths, HTML, SQL). 161 | - **Access Control:** Least privilege, respect roots. 162 | - **Transport Security:** 163 | - **HTTP:** Pluggable authentication (`jwt` or `oauth`) via middleware in `src/mcp-server/transports/auth/`. **Requires appropriate environment variables to be set for the chosen mode.** Validate `Origin` header (via CORS middleware). Use HTTPS in production. Bind to `127.0.0.1` for local servers. 164 | - **Stdio:** Authentication typically handled by the host process. Best practice is to not apply authentication to MCP Server stdio processes. 165 | - **Secrets Management:** Use env vars (`MCP_AUTH_SECRET_KEY`, `OBSIDIAN_API_KEY`) or secrets managers, avoid hardcoding/logging. 166 | - **Dependency Security:** Keep dependencies updated (`npm audit`). 167 | - **Rate Limiting:** Protect against abuse. 168 | 169 | ## Obsidian REST API Service (`src/services/obsidianRestAPI/`) 170 | 171 | This service provides a typed interface for interacting with the Obsidian Local REST API plugin. 172 | 173 | ### Purpose 174 | 175 | - Encapsulates all communication logic with the Obsidian REST API. 176 | - Provides methods for common Obsidian operations like reading/writing files, searching, executing commands, etc. 177 | - Handles authentication (API key) and configuration (base URL, SSL verification) based on environment variables (`OBSIDIAN_API_KEY`, `OBSIDIAN_BASE_URL`, `OBSIDIAN_VERIFY_SSL`). 178 | - Includes robust path encoding for vault files and an increased default request timeout (60s). 179 | - Performs an initial status check on server startup (`src/index.ts`). 180 | 181 | ### Architecture 182 | 183 | - **`service.ts` (`ObsidianRestApiService` class):** 184 | - The main service class. 185 | - Initializes an Axios instance for making HTTP requests. 186 | - Contains the private `_request` method which handles: 187 | - Adding the `Authorization` header. 188 | - Making the actual HTTP call. 189 | - Centralized error handling (translating HTTP errors to `McpError`). 190 | - Logging requests and responses. 191 | - Exposes public methods for each API category (e.g., `getFileContent`, `executeCommand`). 192 | - **`methods/*.ts`:** 193 | - Each file corresponds to a category of API endpoints (e.g., `vaultMethods.ts`, `commandMethods.ts`). 194 | - Contains functions that implement the logic for specific endpoints (e.g., constructing the URL, setting request body/headers). 195 | - These functions accept the `_request` function from the service instance as an argument to perform the actual HTTP call. This promotes modularity and keeps the main service class clean. 196 | - **`types.ts`:** 197 | - Defines TypeScript interfaces for API request parameters and response structures (e.g., `NoteJson`, `PatchOptions`, `ApiStatusResponse`). Based on the Obsidian Local REST API OpenAPI spec. 198 | - **`index.ts`:** 199 | - Barrel file exporting the `ObsidianRestApiService` class, all types, and the method modules. 200 | 201 | ### Usage 202 | 203 | - The `ObsidianRestApiService` is typically instantiated within the MCP server logic (e.g., inside tool handlers) where interaction with Obsidian is needed. 204 | - Tool implementations (like `obsidianReadFileTool`, `obsidianUpdateNoteTool`) use this service to perform actions within the user's vault. 205 | 206 | ```typescript 207 | // Example within an MCP Tool handler 208 | import { ObsidianRestApiService } from "../../services/obsidianRestAPI/index.js"; 209 | import { config } from "../../config/index.js"; 210 | import { RequestContext } from "../../utils/index.js"; 211 | 212 | // Assuming obsidianService is instantiated appropriately 213 | // (In a real scenario, dependency injection or a singleton pattern might be used) 214 | const obsidianService = new ObsidianRestApiService(); // Needs config loaded 215 | 216 | async function handleReadFile(filePath: string, context: RequestContext) { 217 | try { 218 | const content = await obsidianService.getFileContent( 219 | filePath, 220 | "markdown", 221 | context 222 | ); 223 | // ... process content ... 224 | return { content: [{ text: `Read content: ${content}` }], isError: false }; 225 | } catch (error) { 226 | // Error handling (McpError already thrown by service) 227 | throw error; // Re-throw for the MCP server to handle 228 | } 229 | } 230 | ``` 231 | 232 | - **Key Takeaway**: Use the methods provided by `ObsidianRestApiService` for all interactions with the Obsidian vault via the REST API. Do not make direct HTTP calls to the Obsidian API elsewhere. 233 | 234 | ## Agent Instructions for API Connection Help 235 | 236 | If the user asks for help connecting to the Obsidian API, follow these steps: 237 | 238 | 1. **Acknowledge the two connection methods**: Explain that the Obsidian Local REST API plugin offers two main connection URLs: an encrypted HTTPS one and a non-encrypted HTTP one. 239 | 2. **Recommend the HTTP Method for Simplicity**: 240 | - Advise the user to enable the "Enable Non-encrypted (HTTP) Server" option in the plugin's settings in Obsidian. 241 | - Explain that this is the simplest method as it does not require dealing with SSL certificates. 242 | - Provide the corresponding `OBSIDIAN_BASE_URL` (e.g., `http://127.0.0.1:27123`). 243 | 3. **Explain the HTTPS Method**: 244 | - If the user prefers to use the default encrypted HTTPS URL (e.g., `https://127.0.0.1:27124`), explain that it uses a self-signed certificate. 245 | - Clearly state that this will cause connection failures unless they set the `OBSIDIAN_VERIFY_SSL` environment variable to `"false"`. 246 | 4. **Provide Clear Examples**: Show example `env` configurations for both methods, as seen in the main `README.md`. 247 | 248 | ## Vault Cache Service (`src/services/obsidianRestAPI/vaultCache/`) 249 | 250 | This service provides an in-memory cache of the Obsidian vault's file content and metadata. 251 | 252 | ### Purpose 253 | 254 | - **Performance Improvement**: Reduces load on the Obsidian Local REST API and speeds up operations that frequently need file content or metadata (e.g., global search). 255 | - **API Fallback**: The `obsidianGlobalSearchTool` uses the cache as a fallback if the live API search fails or times out, ensuring greater resilience. 256 | - **Periodic Refresh**: The cache automatically refreshes in the background at a configurable interval (`OBSIDIAN_CACHE_REFRESH_INTERVAL_MIN`) to stay in sync with the vault. 257 | 258 | ### Architecture 259 | 260 | - **`service.ts` (`VaultCacheService` class):** 261 | - Takes an `ObsidianRestApiService` instance in its constructor. 262 | - Manages the cache state (a Map of file paths to their content and modification time). 263 | - Provides methods to build/rebuild the cache (`buildVaultCache`, `refreshCache`) by calling the Obsidian API (`listFiles`, `getFileContent`, `getFileMetadata`). 264 | - Exposes methods to query the cache (`isReady`, `getCache`, `getEntry`). 265 | - Manages the periodic refresh timer (`startPeriodicRefresh`, `stopPeriodicRefresh`). 266 | - **`index.ts`:** Barrel file exporting the service. 267 | 268 | ### Usage 269 | 270 | - Instantiated in `src/index.ts` and passed as a dependency to tools that can benefit from it (e.g., `obsidianGlobalSearchTool`). 271 | - The initial cache build is triggered asynchronously on server startup. Tools should check `isReady()` before relying on the cache. 272 | 273 | ## Core Utilities Integration 274 | 275 | ### 1. Logging (`src/utils/internal/logger.ts`) 276 | 277 | - **Purpose**: Structured logging compliant with MCP Spec (RFC 5424 levels). Logs to files (`logs/`) and can send `notifications/message` to connected clients supporting the `logging` capability. 278 | - **Levels**: `debug`(7), `info`(6), `notice`(5), `warning`(4), `error`(3), `crit`(2), `alert`(1), `emerg`(0). 279 | - **Usage**: Import the singleton `logger` instance from the main utils barrel file (`src/utils/index.js`). Pass a `context` object (`RequestContext`) for correlation. 280 | 281 | **Note**: The full logger implementation is provided below for reference to understand exactly how logger works, expected JSDoc structure, and integration points. 282 | 283 | ```typescript 284 | /** 285 | * @fileoverview Provides a singleton Logger class that wraps Winston for file logging 286 | * and supports sending MCP (Model Context Protocol) `notifications/message`. 287 | * It handles different log levels compliant with RFC 5424 and MCP specifications. 288 | * @module src/utils/internal/logger 289 | */ 290 | import path from "path"; 291 | import winston from "winston"; 292 | import TransportStream from "winston-transport"; 293 | import { config } from "../../config/index.js"; 294 | import { RequestContext } from "./requestContext.js"; 295 | 296 | /** 297 | * Defines the supported logging levels based on RFC 5424 Syslog severity levels, 298 | * as used by the Model Context Protocol (MCP). 299 | * Levels are: 'debug'(7), 'info'(6), 'notice'(5), 'warning'(4), 'error'(3), 'crit'(2), 'alert'(1), 'emerg'(0). 300 | * Lower numeric values indicate higher severity. 301 | */ 302 | export type McpLogLevel = 303 | | "debug" 304 | | "info" 305 | | "notice" 306 | | "warning" 307 | | "error" 308 | | "crit" 309 | | "alert" 310 | | "emerg"; 311 | 312 | /** 313 | * Numeric severity mapping for MCP log levels (lower is more severe). 314 | * @private 315 | */ 316 | const mcpLevelSeverity: Record<McpLogLevel, number> = { 317 | emerg: 0, 318 | alert: 1, 319 | crit: 2, 320 | error: 3, 321 | warning: 4, 322 | notice: 5, 323 | info: 6, 324 | debug: 7, 325 | }; 326 | 327 | /** 328 | * Maps MCP log levels to Winston's core levels for file logging. 329 | * @private 330 | */ 331 | const mcpToWinstonLevel: Record< 332 | McpLogLevel, 333 | "debug" | "info" | "warn" | "error" 334 | > = { 335 | debug: "debug", 336 | info: "info", 337 | notice: "info", 338 | warning: "warn", 339 | error: "error", 340 | crit: "error", 341 | alert: "error", 342 | emerg: "error", 343 | }; 344 | 345 | /** 346 | * Interface for a more structured error object, primarily for formatting console logs. 347 | * @private 348 | */ 349 | interface ErrorWithMessageAndStack { 350 | message?: string; 351 | stack?: string; 352 | [key: string]: any; 353 | } 354 | 355 | /** 356 | * Interface for the payload of an MCP log notification. 357 | * This structure is used when sending log data via MCP `notifications/message`. 358 | */ 359 | export interface McpLogPayload { 360 | message: string; 361 | context?: RequestContext; 362 | error?: { 363 | message: string; 364 | stack?: string; 365 | }; 366 | [key: string]: any; 367 | } 368 | 369 | /** 370 | * Type for the `data` parameter of the `McpNotificationSender` function. 371 | */ 372 | export type McpNotificationData = McpLogPayload | Record<string, unknown>; 373 | 374 | /** 375 | * Defines the signature for a function that can send MCP log notifications. 376 | * This function is typically provided by the MCP server instance. 377 | * @param level - The severity level of the log message. 378 | * @param data - The payload of the log notification. 379 | * @param loggerName - An optional name or identifier for the logger/server. 380 | */ 381 | export type McpNotificationSender = ( 382 | level: McpLogLevel, 383 | data: McpNotificationData, 384 | loggerName?: string 385 | ) => void; 386 | 387 | // The logsPath from config is already resolved and validated by src/config/index.ts 388 | const resolvedLogsDir = config.logsPath; 389 | const isLogsDirSafe = !!resolvedLogsDir; // If logsPath is set, it's considered safe by config logic. 390 | 391 | /** 392 | * Creates the Winston console log format. 393 | * @returns The Winston log format for console output. 394 | * @private 395 | */ 396 | function createWinstonConsoleFormat(): winston.Logform.Format { 397 | return winston.format.combine( 398 | winston.format.colorize(), 399 | winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), 400 | winston.format.printf(({ timestamp, level, message, ...meta }) => { 401 | let metaString = ""; 402 | const metaCopy = { ...meta }; 403 | if (metaCopy.error && typeof metaCopy.error === "object") { 404 | const errorObj = metaCopy.error as ErrorWithMessageAndStack; 405 | if (errorObj.message) metaString += `\n Error: ${errorObj.message}`; 406 | if (errorObj.stack) 407 | metaString += `\n Stack: ${String(errorObj.stack) 408 | .split("\n") 409 | .map((l: string) => ` ${l}`) 410 | .join("\n")}`; 411 | delete metaCopy.error; 412 | } 413 | if (Object.keys(metaCopy).length > 0) { 414 | try { 415 | const replacer = (_key: string, value: unknown) => 416 | typeof value === "bigint" ? value.toString() : value; 417 | const remainingMetaJson = JSON.stringify(metaCopy, replacer, 2); 418 | if (remainingMetaJson !== "{}") 419 | metaString += `\n Meta: ${remainingMetaJson}`; 420 | } catch (stringifyError: unknown) { 421 | const errorMessage = 422 | stringifyError instanceof Error 423 | ? stringifyError.message 424 | : String(stringifyError); 425 | metaString += `\n Meta: [Error stringifying metadata: ${errorMessage}]`; 426 | } 427 | } 428 | return `${timestamp} ${level}: ${message}${metaString}`; 429 | }) 430 | ); 431 | } 432 | 433 | /** 434 | * Singleton Logger class that wraps Winston for robust logging. 435 | * Supports file logging, conditional console logging, and MCP notifications. 436 | */ 437 | export class Logger { 438 | private static instance: Logger; 439 | private winstonLogger?: winston.Logger; 440 | private initialized = false; 441 | private mcpNotificationSender?: McpNotificationSender; 442 | private currentMcpLevel: McpLogLevel = "info"; 443 | private currentWinstonLevel: "debug" | "info" | "warn" | "error" = "info"; 444 | 445 | private readonly MCP_NOTIFICATION_STACK_TRACE_MAX_LENGTH = 1024; 446 | private readonly LOG_FILE_MAX_SIZE = 5 * 1024 * 1024; // 5MB 447 | private readonly LOG_MAX_FILES = 5; 448 | 449 | /** @private */ 450 | private constructor() {} 451 | 452 | /** 453 | * Initializes the Winston logger instance. 454 | * Should be called once at application startup. 455 | * @param level - The initial minimum MCP log level. 456 | */ 457 | public async initialize(level: McpLogLevel = "info"): Promise<void> { 458 | if (this.initialized) { 459 | this.warning("Logger already initialized.", { 460 | loggerSetup: true, 461 | requestId: "logger-init", 462 | timestamp: new Date().toISOString(), 463 | }); 464 | return; 465 | } 466 | 467 | // Set initialized to true at the beginning of the initialization process. 468 | this.initialized = true; 469 | 470 | this.currentMcpLevel = level; 471 | this.currentWinstonLevel = mcpToWinstonLevel[level]; 472 | 473 | // The logs directory (config.logsPath / resolvedLogsDir) is expected to be created and validated 474 | // by the configuration module (src/config/index.ts) before logger initialization. 475 | // If isLogsDirSafe is true, we assume resolvedLogsDir exists and is usable. 476 | // No redundant directory creation logic here. 477 | 478 | const fileFormat = winston.format.combine( 479 | winston.format.timestamp(), 480 | winston.format.errors({ stack: true }), 481 | winston.format.json() 482 | ); 483 | 484 | const transports: TransportStream[] = []; 485 | const fileTransportOptions = { 486 | format: fileFormat, 487 | maxsize: this.LOG_FILE_MAX_SIZE, 488 | maxFiles: this.LOG_MAX_FILES, 489 | tailable: true, 490 | }; 491 | 492 | if (isLogsDirSafe) { 493 | transports.push( 494 | new winston.transports.File({ 495 | filename: path.join(resolvedLogsDir, "error.log"), 496 | level: "error", 497 | ...fileTransportOptions, 498 | }), 499 | new winston.transports.File({ 500 | filename: path.join(resolvedLogsDir, "warn.log"), 501 | level: "warn", 502 | ...fileTransportOptions, 503 | }), 504 | new winston.transports.File({ 505 | filename: path.join(resolvedLogsDir, "info.log"), 506 | level: "info", 507 | ...fileTransportOptions, 508 | }), 509 | new winston.transports.File({ 510 | filename: path.join(resolvedLogsDir, "debug.log"), 511 | level: "debug", 512 | ...fileTransportOptions, 513 | }), 514 | new winston.transports.File({ 515 | filename: path.join(resolvedLogsDir, "combined.log"), 516 | ...fileTransportOptions, 517 | }) 518 | ); 519 | } else { 520 | if (process.stdout.isTTY) { 521 | console.warn( 522 | "File logging disabled as logsPath is not configured or invalid." 523 | ); 524 | } 525 | } 526 | 527 | this.winstonLogger = winston.createLogger({ 528 | level: this.currentWinstonLevel, 529 | transports, 530 | exitOnError: false, 531 | }); 532 | 533 | // Configure console transport after Winston logger is created 534 | const consoleStatus = this._configureConsoleTransport(); 535 | 536 | const initialContext: RequestContext = { 537 | loggerSetup: true, 538 | requestId: "logger-init-deferred", 539 | timestamp: new Date().toISOString(), 540 | }; 541 | // Removed logging of logsDirCreatedMessage as it's no longer set 542 | if (consoleStatus.message) { 543 | this.info(consoleStatus.message, initialContext); 544 | } 545 | 546 | this.initialized = true; // Ensure this is set after successful setup 547 | this.info( 548 | `Logger initialized. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${consoleStatus.enabled ? "enabled" : "disabled"}`, 549 | { 550 | loggerSetup: true, 551 | requestId: "logger-post-init", 552 | timestamp: new Date().toISOString(), 553 | logsPathUsed: resolvedLogsDir, 554 | } 555 | ); 556 | } 557 | 558 | /** 559 | * Sets the function used to send MCP 'notifications/message'. 560 | * @param sender - The function to call for sending notifications, or undefined to disable. 561 | */ 562 | public setMcpNotificationSender( 563 | sender: McpNotificationSender | undefined 564 | ): void { 565 | this.mcpNotificationSender = sender; 566 | const status = sender ? "enabled" : "disabled"; 567 | this.info(`MCP notification sending ${status}.`, { 568 | loggerSetup: true, 569 | requestId: "logger-set-sender", 570 | timestamp: new Date().toISOString(), 571 | }); 572 | } 573 | 574 | /** 575 | * Dynamically sets the minimum logging level. 576 | * @param newLevel - The new minimum MCP log level to set. 577 | */ 578 | public setLevel(newLevel: McpLogLevel): void { 579 | const setLevelContext: RequestContext = { 580 | loggerSetup: true, 581 | requestId: "logger-set-level", 582 | timestamp: new Date().toISOString(), 583 | }; 584 | if (!this.ensureInitialized()) { 585 | if (process.stdout.isTTY) { 586 | console.error("Cannot set level: Logger not initialized."); 587 | } 588 | return; 589 | } 590 | if (!(newLevel in mcpLevelSeverity)) { 591 | this.warning( 592 | `Invalid MCP log level provided: ${newLevel}. Level not changed.`, 593 | setLevelContext 594 | ); 595 | return; 596 | } 597 | 598 | const oldLevel = this.currentMcpLevel; 599 | this.currentMcpLevel = newLevel; 600 | this.currentWinstonLevel = mcpToWinstonLevel[newLevel]; 601 | if (this.winstonLogger) { 602 | // Ensure winstonLogger is defined 603 | this.winstonLogger.level = this.currentWinstonLevel; 604 | } 605 | 606 | const consoleStatus = this._configureConsoleTransport(); 607 | 608 | if (oldLevel !== newLevel) { 609 | this.info( 610 | `Log level changed. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${consoleStatus.enabled ? "enabled" : "disabled"}`, 611 | setLevelContext 612 | ); 613 | if ( 614 | consoleStatus.message && 615 | consoleStatus.message !== "Console logging status unchanged." 616 | ) { 617 | this.info(consoleStatus.message, setLevelContext); 618 | } 619 | } 620 | } 621 | 622 | /** 623 | * Configures the console transport based on the current log level and TTY status. 624 | * Adds or removes the console transport as needed. 625 | * @returns {{ enabled: boolean, message: string | null }} Status of console logging. 626 | * @private 627 | */ 628 | private _configureConsoleTransport(): { 629 | enabled: boolean; 630 | message: string | null; 631 | } { 632 | if (!this.winstonLogger) { 633 | return { 634 | enabled: false, 635 | message: "Cannot configure console: Winston logger not initialized.", 636 | }; 637 | } 638 | 639 | const consoleTransport = this.winstonLogger.transports.find( 640 | (t) => t instanceof winston.transports.Console 641 | ); 642 | const shouldHaveConsole = 643 | this.currentMcpLevel === "debug" && process.stdout.isTTY; 644 | let message: string | null = null; 645 | 646 | if (shouldHaveConsole && !consoleTransport) { 647 | const consoleFormat = createWinstonConsoleFormat(); 648 | this.winstonLogger.add( 649 | new winston.transports.Console({ 650 | level: "debug", // Console always logs debug if enabled 651 | format: consoleFormat, 652 | }) 653 | ); 654 | message = "Console logging enabled (level: debug, stdout is TTY)."; 655 | } else if (!shouldHaveConsole && consoleTransport) { 656 | this.winstonLogger.remove(consoleTransport); 657 | message = "Console logging disabled (level not debug or stdout not TTY)."; 658 | } else { 659 | message = "Console logging status unchanged."; 660 | } 661 | return { enabled: shouldHaveConsole, message }; 662 | } 663 | 664 | /** 665 | * Gets the singleton instance of the Logger. 666 | * @returns The singleton Logger instance. 667 | */ 668 | public static getInstance(): Logger { 669 | if (!Logger.instance) { 670 | Logger.instance = new Logger(); 671 | } 672 | return Logger.instance; 673 | } 674 | 675 | /** 676 | * Ensures the logger has been initialized. 677 | * @returns True if initialized, false otherwise. 678 | * @private 679 | */ 680 | private ensureInitialized(): boolean { 681 | if (!this.initialized || !this.winstonLogger) { 682 | if (process.stdout.isTTY) { 683 | console.warn("Logger not initialized; message dropped."); 684 | } 685 | return false; 686 | } 687 | return true; 688 | } 689 | 690 | /** 691 | * Centralized log processing method. 692 | * @param level - The MCP severity level of the message. 693 | * @param msg - The main log message. 694 | * @param context - Optional request context for the log. 695 | * @param error - Optional error object associated with the log. 696 | * @private 697 | */ 698 | private log( 699 | level: McpLogLevel, 700 | msg: string, 701 | context?: RequestContext, 702 | error?: Error 703 | ): void { 704 | if (!this.ensureInitialized()) return; 705 | if (mcpLevelSeverity[level] > mcpLevelSeverity[this.currentMcpLevel]) { 706 | return; // Do not log if message level is less severe than currentMcpLevel 707 | } 708 | 709 | const logData: Record<string, unknown> = { ...context }; 710 | const winstonLevel = mcpToWinstonLevel[level]; 711 | 712 | if (error) { 713 | this.winstonLogger!.log(winstonLevel, msg, { ...logData, error }); 714 | } else { 715 | this.winstonLogger!.log(winstonLevel, msg, logData); 716 | } 717 | 718 | if (this.mcpNotificationSender) { 719 | const mcpDataPayload: McpLogPayload = { message: msg }; 720 | if (context && Object.keys(context).length > 0) 721 | mcpDataPayload.context = context; 722 | if (error) { 723 | mcpDataPayload.error = { message: error.message }; 724 | // Include stack trace in debug mode for MCP notifications, truncated for brevity 725 | if (this.currentMcpLevel === "debug" && error.stack) { 726 | mcpDataPayload.error.stack = error.stack.substring( 727 | 0, 728 | this.MCP_NOTIFICATION_STACK_TRACE_MAX_LENGTH 729 | ); 730 | } 731 | } 732 | try { 733 | const serverName = 734 | config?.mcpServerName ?? "MCP_SERVER_NAME_NOT_CONFIGURED"; 735 | this.mcpNotificationSender(level, mcpDataPayload, serverName); 736 | } catch (sendError: unknown) { 737 | const errorMessage = 738 | sendError instanceof Error ? sendError.message : String(sendError); 739 | const internalErrorContext: RequestContext = { 740 | requestId: context?.requestId || "logger-internal-error", 741 | timestamp: new Date().toISOString(), 742 | originalLevel: level, 743 | originalMessage: msg, 744 | sendError: errorMessage, 745 | mcpPayload: JSON.stringify(mcpDataPayload).substring(0, 500), // Log a preview 746 | }; 747 | this.winstonLogger!.error( 748 | "Failed to send MCP log notification", 749 | internalErrorContext 750 | ); 751 | } 752 | } 753 | } 754 | 755 | /** Logs a message at the 'debug' level. */ 756 | public debug(msg: string, context?: RequestContext): void { 757 | this.log("debug", msg, context); 758 | } 759 | 760 | /** Logs a message at the 'info' level. */ 761 | public info(msg: string, context?: RequestContext): void { 762 | this.log("info", msg, context); 763 | } 764 | 765 | /** Logs a message at the 'notice' level. */ 766 | public notice(msg: string, context?: RequestContext): void { 767 | this.log("notice", msg, context); 768 | } 769 | 770 | /** Logs a message at the 'warning' level. */ 771 | public warning(msg: string, context?: RequestContext): void { 772 | this.log("warning", msg, context); 773 | } 774 | 775 | /** 776 | * Logs a message at the 'error' level. 777 | * @param msg - The main log message. 778 | * @param err - Optional. Error object or RequestContext. 779 | * @param context - Optional. RequestContext if `err` is an Error. 780 | */ 781 | public error( 782 | msg: string, 783 | err?: Error | RequestContext, 784 | context?: RequestContext 785 | ): void { 786 | const errorObj = err instanceof Error ? err : undefined; 787 | const actualContext = err instanceof Error ? context : err; 788 | this.log("error", msg, actualContext, errorObj); 789 | } 790 | 791 | /** 792 | * Logs a message at the 'crit' (critical) level. 793 | * @param msg - The main log message. 794 | * @param err - Optional. Error object or RequestContext. 795 | * @param context - Optional. RequestContext if `err` is an Error. 796 | */ 797 | public crit( 798 | msg: string, 799 | err?: Error | RequestContext, 800 | context?: RequestContext 801 | ): void { 802 | const errorObj = err instanceof Error ? err : undefined; 803 | const actualContext = err instanceof Error ? context : err; 804 | this.log("crit", msg, actualContext, errorObj); 805 | } 806 | 807 | /** 808 | * Logs a message at the 'alert' level. 809 | * @param msg - The main log message. 810 | * @param err - Optional. Error object or RequestContext. 811 | * @param context - Optional. RequestContext if `err` is an Error. 812 | */ 813 | public alert( 814 | msg: string, 815 | err?: Error | RequestContext, 816 | context?: RequestContext 817 | ): void { 818 | const errorObj = err instanceof Error ? err : undefined; 819 | const actualContext = err instanceof Error ? context : err; 820 | this.log("alert", msg, actualContext, errorObj); 821 | } 822 | 823 | /** 824 | * Logs a message at the 'emerg' (emergency) level. 825 | * @param msg - The main log message. 826 | * @param err - Optional. Error object or RequestContext. 827 | * @param context - Optional. RequestContext if `err` is an Error. 828 | */ 829 | public emerg( 830 | msg: string, 831 | err?: Error | RequestContext, 832 | context?: RequestContext 833 | ): void { 834 | const errorObj = err instanceof Error ? err : undefined; 835 | const actualContext = err instanceof Error ? context : err; 836 | this.log("emerg", msg, actualContext, errorObj); 837 | } 838 | 839 | /** 840 | * Logs a message at the 'emerg' (emergency) level, typically for fatal errors. 841 | * @param msg - The main log message. 842 | * @param err - Optional. Error object or RequestContext. 843 | * @param context - Optional. RequestContext if `err` is an Error. 844 | */ 845 | public fatal( 846 | msg: string, 847 | err?: Error | RequestContext, 848 | context?: RequestContext 849 | ): void { 850 | const errorObj = err instanceof Error ? err : undefined; 851 | const actualContext = err instanceof Error ? context : err; 852 | this.log("emerg", msg, actualContext, errorObj); 853 | } 854 | } 855 | 856 | /** 857 | * The singleton instance of the Logger. 858 | * Use this instance for all logging operations. 859 | */ 860 | export const logger = Logger.getInstance(); 861 | ``` 862 | 863 | - **Key Files**: 864 | - `src/utils/internal/logger.ts`: Logger implementation. 865 | - `logs/`: Directory where JSON log files are stored (`combined.log`, `error.log`, etc.). 866 | 867 | ### 2. Error Handling (`src/utils/internal/errorHandler.ts`) 868 | 869 | - **Purpose**: Standardized error objects (`McpError`) and centralized handling (`ErrorHandler`). Automatically determines error codes based on type/patterns. 870 | - **Usage**: 871 | - Use `ErrorHandler.tryCatch` to wrap operations that might fail. 872 | - Throw `McpError` for specific, categorized errors using `BaseErrorCode`. 873 | - `ErrorHandler` automatically logs errors (using the logger) with context and sanitized input. 874 | 875 | ```typescript 876 | // Example assuming import from a file within src/ 877 | import { 878 | ErrorHandler, 879 | RequestContext, 880 | requestContextService, 881 | } from "./utils/index.js"; 882 | import { McpError, BaseErrorCode } from "./types-global/errors.js"; 883 | 884 | async function performTask(input: any, parentContext: RequestContext) { 885 | const context = { ...parentContext, operation: "performTask" }; 886 | return await ErrorHandler.tryCatch( 887 | async () => { 888 | if (!input) { 889 | throw new McpError( 890 | BaseErrorCode.VALIDATION_ERROR, 891 | "Input cannot be empty", 892 | context 893 | ); 894 | } 895 | // ... perform task logic ... 896 | const result = await someAsyncOperation(input); 897 | return result; 898 | }, 899 | { 900 | operation: "performTask", // Redundant but good for clarity 901 | context: context, 902 | input: input, // Input is automatically sanitized for logging 903 | errorCode: BaseErrorCode.INTERNAL_ERROR, // Default code if unexpected error occurs 904 | critical: false, // Or true if failure should halt the process 905 | } 906 | ); 907 | } 908 | ``` 909 | 910 | - **Key Files**: 911 | - `src/types-global/errors.ts`: Defines `McpError` and `BaseErrorCode`. 912 | - `src/utils/internal/errorHandler.ts`: Provides `ErrorHandler.tryCatch`, `handleError`, `determineErrorCode`. 913 | 914 | ### 3. Async Operations (`src/utils/internal/asyncUtils.ts`) 915 | 916 | - **Purpose**: Provides utilities for handling asynchronous operations, most notably `retryWithDelay` for retrying failed operations. 917 | - **Usage**: Wrap an async function call in `retryWithDelay` to automatically retry it on failure, with configurable delays, attempt limits, and retry conditions. This is used in the `obsidianUpdateNoteTool` to reliably fetch file state after a write operation. 918 | 919 | ```typescript 920 | // Example assuming import from a file within src/ 921 | import { retryWithDelay, RequestContext } from "./utils/index.js"; 922 | import { McpError, BaseErrorCode } from "./types-global/errors.js"; 923 | 924 | async function fetchWithRetry(url: string, context: RequestContext) { 925 | return await retryWithDelay( 926 | async () => { 927 | const response = await fetch(url); 928 | if (!response.ok) { 929 | // Throw an error that the retry logic can inspect 930 | throw new McpError( 931 | BaseErrorCode.SERVICE_UNAVAILABLE, 932 | `Fetch failed with status ${response.status}` 933 | ); 934 | } 935 | return response.json(); 936 | }, 937 | { 938 | operationName: "fetchWithRetry", 939 | context: context, 940 | maxRetries: 3, 941 | delayMs: 500, 942 | // Only retry on specific, transient error codes 943 | shouldRetry: (err: unknown) => 944 | err instanceof McpError && 945 | err.code === BaseErrorCode.SERVICE_UNAVAILABLE, 946 | } 947 | ); 948 | } 949 | ``` 950 | 951 | - **Key Files**: 952 | - `src/utils/internal/asyncUtils.ts`: Provides `retryWithDelay`. 953 | 954 | ### 4. Request Context (`src/utils/internal/requestContext.ts`) 955 | 956 | - **Purpose**: Track and correlate operations related to a single request or workflow using a unique `requestId`. 957 | - **Usage**: 958 | - Create context at the beginning of an operation using `requestContextService.createRequestContext`. 959 | - Pass the context object down through function calls. 960 | - Include the context object when logging or creating errors. 961 | 962 | ```typescript 963 | // Example assuming import from a file within src/ 964 | import { 965 | requestContextService, 966 | RequestContext, 967 | logger, 968 | } from "./utils/index.js"; 969 | 970 | function handleIncomingRequest(requestData: any) { 971 | const context: RequestContext = requestContextService.createRequestContext({ 972 | operation: "HandleIncomingRequest", 973 | initialData: requestData.id, 974 | }); 975 | 976 | logger.info("Received request", context); 977 | processSubTask(requestData.payload, context); 978 | } 979 | 980 | function processSubTask(payload: any, parentContext: RequestContext) { 981 | const subTaskContext = { ...parentContext, subOperation: "ProcessSubTask" }; 982 | logger.debug("Processing sub-task", subTaskContext); 983 | // ... logic ... 984 | } 985 | ``` 986 | 987 | - **Key Files**: 988 | - `src/utils/internal/requestContext.ts`: Defines `RequestContext` interface and `requestContextService`. 989 | 990 | ### 5. ID Generation (`src/utils/security/idGenerator.ts`) 991 | 992 | - **Purpose**: Generate unique, prefixed IDs for different entity types and standard UUIDs. 993 | - **Usage**: Configure prefixes (if needed) and use `idGenerator.generateForEntity` or `generateUUID` from the main utils barrel file. 994 | 995 | ```typescript 996 | // Example assuming import from a file within src/ 997 | import { idGenerator, generateUUID } from "./utils/index.js"; 998 | 999 | // Prefixes are typically not needed unless distinguishing IDs across systems 1000 | // idGenerator.setEntityPrefixes({ project: 'PROJ', task: 'TASK' }); 1001 | 1002 | const someId = idGenerator.generateForEntity("request"); // e.g., "REQ_A6B3J0" 1003 | const standardUuid = generateUUID(); // e.g., "123e4567-e89b-12d3-a456-426614174000" 1004 | 1005 | const isValid = idGenerator.isValid(someId, "request"); // true 1006 | const entityType = idGenerator.getEntityType(someId); // "request" 1007 | ``` 1008 | 1009 | - **Key Files**: 1010 | - `src/utils/security/idGenerator.ts`: `IdGenerator` class, `idGenerator` instance, `generateUUID`. 1011 | 1012 | ### 6. Sanitization (`src/utils/security/sanitization.ts`) 1013 | 1014 | - **Purpose**: Clean and validate input data (HTML, paths, numbers, URLs, JSON) to prevent security issues. Also sanitizes objects for logging. 1015 | - **Usage**: Import the singleton `sanitization` instance or `sanitizeInputForLogging` from the main utils barrel file. 1016 | 1017 | ```typescript 1018 | // Example assuming import from a file within src/ 1019 | import { sanitization, sanitizeInputForLogging } from "./utils/index.js"; 1020 | 1021 | const unsafeHtml = '<script>alert("xss")</script><p>Safe content</p>'; 1022 | const safeHtml = sanitization.sanitizeHtml(unsafeHtml); // "<p>Safe content</p>" 1023 | 1024 | const sensitiveData = { 1025 | user: "admin", 1026 | password: "pwd", 1027 | token: "abc", 1028 | obsidianApiKey: "secret", 1029 | }; 1030 | const safeLogData = sanitizeInputForLogging(sensitiveData); 1031 | // safeLogData = { user: 'admin', password: '[REDACTED]', token: '[REDACTED]', obsidianApiKey: '[REDACTED]' } 1032 | ``` 1033 | 1034 | - **Key Files**: 1035 | - `src/utils/security/sanitization.ts`: `Sanitization` class, `sanitization` instance, `sanitizeInputForLogging`. 1036 | 1037 | ### 7. JSON Parsing (`src/utils/parsing/jsonParser.ts`) 1038 | 1039 | - **Purpose**: Parse potentially partial/incomplete JSON strings. Handles optional `<think>` blocks. 1040 | - **Usage**: Import `jsonParser` from the main utils barrel file. Use `Allow` constants for options. 1041 | 1042 | ```typescript 1043 | // Example assuming import from a file within src/ 1044 | import { jsonParser, Allow, RequestContext } from './utils/index.js'; 1045 | 1046 | const partialJson = '<think>Parsing...</think>{"key": "value", "incomplete": '; 1047 | const context: RequestContext = /* ... */; 1048 | 1049 | try { 1050 | const parsed = jsonParser.parse(partialJson, Allow.ALL, context); 1051 | // parsed = { key: 'value', incomplete: undefined } 1052 | } catch (error) { /* Handle McpError */ } 1053 | ``` 1054 | 1055 | - **Key Files**: 1056 | - `src/utils/parsing/jsonParser.ts`: `JsonParser` class, `jsonParser` instance, `Allow` enum. 1057 | 1058 | ### 8. Rate Limiting (`src/utils/security/rateLimiter.ts`) 1059 | 1060 | - **Purpose**: Implement rate limiting based on a key (e.g., session ID, user ID). 1061 | - **Usage**: Import `rateLimiter` from the main utils barrel file. Use `check`. 1062 | 1063 | ```typescript 1064 | // Example assuming import from a file within src/ 1065 | import { rateLimiter, RequestContext } from './utils/index.js'; 1066 | 1067 | const sessionId = 'session-abc'; // Or another identifier 1068 | const context: RequestContext = /* ... */; 1069 | 1070 | try { 1071 | rateLimiter.check(sessionId, context); 1072 | // ... proceed with operation ... 1073 | } catch (error) { /* Handle McpError (RATE_LIMITED) */ } 1074 | ``` 1075 | 1076 | - **Key Files**: 1077 | - `src/utils/security/rateLimiter.ts`: `RateLimiter` class, `rateLimiter` instance. 1078 | 1079 | ### 9. Token Counting (`src/utils/metrics/tokenCounter.ts`) 1080 | 1081 | - **Purpose**: Estimate tokens using `tiktoken` (`gpt-4o` model). Useful for tracking LLM usage or context window limits. 1082 | - **Usage**: Import `countTokens` or `countChatTokens` from the main utils barrel file. 1083 | 1084 | ```typescript 1085 | // Example assuming import from a file within src/ 1086 | import { countTokens, countChatTokens, RequestContext } from './utils/index.js'; 1087 | import { ChatCompletionMessageParam } from 'openai/resources/chat/completions'; 1088 | 1089 | const text = "Sample text to count tokens for."; 1090 | const context: RequestContext = /* ... */; 1091 | 1092 | async function calculateTokens() { 1093 | try { 1094 | const textTokens = await countTokens(text, context); 1095 | logger.info(`Text token count: ${textTokens}`, context); 1096 | } catch (error) { /* Handle McpError */ } 1097 | } 1098 | ``` 1099 | 1100 | - **Key Files**: 1101 | - `src/utils/metrics/tokenCounter.ts`: Provides `countTokens` and `countChatTokens`. 1102 | 1103 | ### 10. Obsidian Formatting (`src/utils/obsidian/obsidianStatUtils.ts`) 1104 | 1105 | - **Purpose**: Provides helpers for formatting data related to Obsidian notes, such as timestamps and token counts. 1106 | - **Usage**: Use `formatTimestamp` to create human-readable date strings and `createFormattedStatWithTokenCount` to generate a comprehensive stat object for tool responses. 1107 | 1108 | ```typescript 1109 | // Example assuming import from a file within src/ 1110 | import { 1111 | createFormattedStatWithTokenCount, 1112 | RequestContext, 1113 | } from "./utils/index.js"; 1114 | import { NoteStat } from "./services/obsidianRestAPI/index.js"; 1115 | 1116 | async function formatNoteStats( 1117 | stat: NoteStat, 1118 | content: string, 1119 | context: RequestContext 1120 | ) { 1121 | // stat = { ctime: 1672531200000, mtime: 1672617600000, size: 123 } 1122 | const formattedStat = await createFormattedStatWithTokenCount( 1123 | stat, 1124 | content, 1125 | context 1126 | ); 1127 | // formattedStat might be: 1128 | // { 1129 | // createdTime: '04:00:00 PM | 01-01-2023', 1130 | // modifiedTime: '04:00:00 PM | 01-02-2023', 1131 | // tokenCountEstimate: 30 1132 | // } 1133 | return formattedStat; 1134 | } 1135 | ``` 1136 | 1137 | - **Key Files**: 1138 | - `src/utils/obsidian/obsidianStatUtils.ts`: Provides formatting helpers. 1139 | 1140 | ## Utility Scripts (`scripts/`) 1141 | 1142 | This project includes several utility scripts located in the `scripts/` directory to aid development: 1143 | 1144 | ### 1. Clean (`scripts/clean.ts`) 1145 | 1146 | - **Purpose**: Removes build artifacts and temporary directories. 1147 | - **Usage**: `npm run rebuild` (uses this script) or `ts-node --esm scripts/clean.ts [dir1] [dir2]...` 1148 | - **Default Targets**: `dist`, `logs`. 1149 | 1150 | ### 2. Make Executable (`scripts/make-executable.ts`) 1151 | 1152 | - **Purpose**: Sets executable permissions (`chmod +x`) on specified files (Unix-like systems only). Useful for CLI entry points after building. 1153 | - **Usage**: `npm run build` (uses this script) or `ts-node --esm scripts/make-executable.ts [file1] [file2]...` 1154 | - **Default Target**: `dist/index.js`. 1155 | 1156 | ### 3. Generate Tree (`scripts/tree.ts`) 1157 | 1158 | - **Purpose**: Creates a visual directory tree markdown file (`docs/tree.md` by default), respecting `.gitignore`. 1159 | - **Usage**: `npm run tree` or `ts-node --esm scripts/tree.ts [output-path] [--depth=<number>]` 1160 | 1161 | ### 4. Fetch OpenAPI Spec (`scripts/fetch-openapi-spec.ts`) 1162 | 1163 | - **Purpose**: Fetches an OpenAPI specification (YAML/JSON) from a URL, attempts fallbacks (`/openapi.yaml`, `/openapi.json`), parses it, and saves both YAML and JSON versions locally. Used here to fetch the Obsidian Local REST API spec. 1164 | - **Usage**: `npm run fetch:spec <url> <output-base-path>` or `ts-node --esm scripts/fetch-openapi-spec.ts <url> <output-base-path>` 1165 | - **Example (for Obsidian API)**: `npm run fetch:spec http://127.0.0.1:27123/ docs/obsidian-api/obsidian_rest_api_spec` (Replace URL if your Obsidian API runs elsewhere) 1166 | - **Dependencies**: `axios`, `js-yaml`. 1167 | 1168 | ## Adding New Features 1169 | 1170 | ### Adding a Tool 1171 | 1172 | 1. **Directory**: `src/mcp-server/tools/yourToolName/` 1173 | 2. **Logic (`logic.ts`)**: Define input/output types, Zod schema, and core processing function. Use `ObsidianRestApiService` if interaction with Obsidian is needed. 1174 | 3. **Registration (`registration.ts`)**: Import logic, schema, `McpServer`, `ErrorHandler`. **Use the high-level `server.tool(name, description, schemaShape, async handler => { ... })` (SDK v1.10.2+).** Pass required services (e.g., `ObsidianRestApiService`, `VaultCacheService`) to the handler. Ensure handler returns `CallToolResult` (`{ content: [...], isError: boolean }`). Wrap handler logic and registration in `ErrorHandler.tryCatch`. 1175 | 4. **Index (`index.ts`)**: Export registration function. 1176 | 5. **Server (`src/mcp-server/server.ts`)**: Import and call registration function within `createMcpServerInstance`, passing the instantiated services. 1177 | 1178 | ### Adding a Resource 1179 | 1180 | 1. **Directory**: `src/mcp-server/resources/yourResourceName/` 1181 | 2. **Logic (`logic.ts`)**: Define params type, query schema (if needed), and core processing function (takes `uri: URL`, `params`). Use `ObsidianRestApiService` if needed. 1182 | 3. **Registration (`registration.ts`)**: Import logic, schema, `McpServer`, `ResourceTemplate`, `ErrorHandler`. Define `ResourceTemplate`. **Use the high-level `server.resource(regName, template, metadata, async handler => { ... })`.** Handler should return `{ contents: [{ uri, blob, mimeType }] }` where `blob` is Base64 encoded content. Wrap handler logic and registration in `ErrorHandler.tryCatch`. If supporting subscriptions (`subscribe: true` capability), **MUST** also handle `resources/unsubscribe` request. 1183 | 4. **Index (`index.ts`)**: Export registration function. 1184 | 5. **Server (`src/mcp-server/server.ts`)**: Import and call registration function within `createMcpServerInstance`. 1185 | 1186 | ## Key File Locations 1187 | 1188 | - **Main Entry**: `src/index.ts` (Initializes server, handles startup/shutdown) 1189 | - **Server Setup**: `src/mcp-server/server.ts` (Handles transport logic, session management, instantiates services, registers tools/resources) 1190 | - **HTTP Auth Middleware**: `src/mcp-server/transports/auth/` (contains strategies for JWT and OAuth) 1191 | - **Configuration**: `src/config/index.ts` (Loads env vars, package info, initializes logger, Obsidian API config) 1192 | - **Obsidian Service**: `src/services/obsidianRestAPI/` (Service, methods, types for Obsidian API) 1193 | - **Vault Cache Service**: `src/services/obsidianRestAPI/vaultCache/` (Service for caching vault structure) 1194 | - **Global Types**: `src/types-global/` 1195 | - **Utilities**: `src/utils/` (Main barrel file `index.ts` exporting from subdirs: `internal`, `metrics`, `parsing`, `security`, `obsidian`) 1196 | - **Tools**: `src/mcp-server/tools/` (Contains specific tool implementations like `obsidianReadFileTool`, `obsidianGlobalSearchTool`) 1197 | - **Resources**: `src/mcp-server/resources/` (Currently empty, place resource implementations here) 1198 | - **Client Config Example**: `mcp-client-config.example.json` (Example config for connecting clients) 1199 | 1200 | Remember to keep this cheatsheet updated as the codebase evolves! 1201 | 1202 | # obsidian-mcp-server - Directory Structure 1203 | 1204 | Generated on: 2025-06-13 07:41:01 1205 | 1206 | ``` 1207 | obsidian-mcp-server 1208 | ├── .github 1209 | │ ├── workflows 1210 | │ │ └── publish.yml 1211 | │ └── FUNDING.yml 1212 | ├── docs 1213 | │ ├── obsidian-api 1214 | │ │ ├── obsidian_rest_api_spec.json 1215 | │ │ └── obsidian_rest_api_spec.yaml 1216 | │ ├── obsidian_mcp_tools_spec.md 1217 | │ ├── obsidian_tools_phase2.md 1218 | │ └── tree.md 1219 | ├── scripts 1220 | │ ├── clean.ts 1221 | │ ├── fetch-openapi-spec.ts 1222 | │ ├── make-executable.ts 1223 | │ └── tree.ts 1224 | ├── src 1225 | │ ├── config 1226 | │ │ └── index.ts 1227 | │ ├── mcp-server 1228 | │ │ ├── tools 1229 | │ │ │ ├── obsidianDeleteFileTool 1230 | │ │ │ │ ├── index.ts 1231 | │ │ │ │ ├── logic.ts 1232 | │ │ │ │ └── registration.ts 1233 | │ │ │ ├── obsidianGlobalSearchTool 1234 | │ │ │ │ ├── index.ts 1235 | │ │ │ │ ├── logic.ts 1236 | │ │ │ │ └── registration.ts 1237 | │ │ │ ├── obsidianListFilesTool 1238 | │ │ │ │ ├── index.ts 1239 | │ │ │ │ ├── logic.ts 1240 | │ │ │ │ └── registration.ts 1241 | │ │ │ ├── obsidianManageFrontmatterTool 1242 | │ │ │ │ ├── index.ts 1243 | │ │ │ │ ├── logic.ts 1244 | │ │ │ │ └── registration.ts 1245 | │ │ │ ├── obsidianManageTagsTool 1246 | │ │ │ │ ├── index.ts 1247 | │ │ │ │ ├── logic.ts 1248 | │ │ │ │ └── registration.ts 1249 | │ │ │ ├── obsidianReadFileTool 1250 | │ │ │ │ ├── index.ts 1251 | │ │ │ │ ├── logic.ts 1252 | │ │ │ │ └── registration.ts 1253 | │ │ │ ├── obsidianSearchReplaceTool 1254 | │ │ │ │ ├── index.ts 1255 | │ │ │ │ ├── logic.ts 1256 | │ │ │ │ └── registration.ts 1257 | │ │ │ └── obsidianUpdateNoteTool 1258 | │ │ │ ├── index.ts 1259 | │ │ │ ├── logic.ts 1260 | │ │ │ └── registration.ts 1261 | │ │ ├── transports 1262 | │ │ │ ├── auth 1263 | │ │ │ │ ├── core 1264 | │ │ │ │ │ ├── authContext.ts 1265 | │ │ │ │ │ ├── authTypes.ts 1266 | │ │ │ │ │ └── authUtils.ts 1267 | │ │ │ │ ├── strategies 1268 | │ │ │ │ │ ├── jwt 1269 | │ │ │ │ │ │ └── jwtMiddleware.ts 1270 | │ │ │ │ │ └── oauth 1271 | │ │ │ │ │ └── oauthMiddleware.ts 1272 | │ │ │ │ └── index.ts 1273 | │ │ │ ├── httpErrorHandler.ts 1274 | │ │ │ ├── httpTransport.ts 1275 | │ │ │ └── stdioTransport.ts 1276 | │ │ └── server.ts 1277 | │ ├── services 1278 | │ │ └── obsidianRestAPI 1279 | │ │ ├── methods 1280 | │ │ │ ├── activeFileMethods.ts 1281 | │ │ │ ├── commandMethods.ts 1282 | │ │ │ ├── openMethods.ts 1283 | │ │ │ ├── patchMethods.ts 1284 | │ │ │ ├── periodicNoteMethods.ts 1285 | │ │ │ ├── searchMethods.ts 1286 | │ │ │ └── vaultMethods.ts 1287 | │ │ ├── vaultCache 1288 | │ │ │ ├── index.ts 1289 | │ │ │ └── service.ts 1290 | │ │ ├── index.ts 1291 | │ │ ├── service.ts 1292 | │ │ └── types.ts 1293 | │ ├── types-global 1294 | │ │ └── errors.ts 1295 | │ ├── utils 1296 | │ │ ├── internal 1297 | │ │ │ ├── asyncUtils.ts 1298 | │ │ │ ├── errorHandler.ts 1299 | │ │ │ ├── index.ts 1300 | │ │ │ ├── logger.ts 1301 | │ │ │ └── requestContext.ts 1302 | │ │ ├── metrics 1303 | │ │ │ ├── index.ts 1304 | │ │ │ └── tokenCounter.ts 1305 | │ │ ├── obsidian 1306 | │ │ │ ├── index.ts 1307 | │ │ │ ├── obsidianApiUtils.ts 1308 | │ │ │ └── obsidianStatUtils.ts 1309 | │ │ ├── parsing 1310 | │ │ │ ├── dateParser.ts 1311 | │ │ │ ├── index.ts 1312 | │ │ │ └── jsonParser.ts 1313 | │ │ ├── security 1314 | │ │ │ ├── idGenerator.ts 1315 | │ │ │ ├── index.ts 1316 | │ │ │ ├── rateLimiter.ts 1317 | │ │ │ └── sanitization.ts 1318 | │ │ └── index.ts 1319 | │ └── index.ts 1320 | ├── .clinerules 1321 | ├── .gitignore 1322 | ├── .ncurc.json 1323 | ├── CHANGELOG.md 1324 | ├── Dockerfile 1325 | ├── env.json 1326 | ├── LICENSE 1327 | ├── mcp.json 1328 | ├── package-lock.json 1329 | ├── package.json 1330 | ├── README.md 1331 | ├── repomix.config.json 1332 | ├── smithery.yaml 1333 | ├── tsconfig.json 1334 | └── typedoc.json 1335 | ``` 1336 | 1337 | _Note: This tree excludes files and directories matched by .gitignore and default patterns._ 1338 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Obsidian MCP Server 2 | 3 | [](https://www.typescriptlang.org/) 4 | [](https://modelcontextprotocol.io/) 5 | [](./CHANGELOG.md) 6 | [](https://opensource.org/licenses/Apache-2.0) 7 | [](https://github.com/cyanheads/obsidian-mcp-server/issues) 8 | [](https://github.com/cyanheads/obsidian-mcp-server) 9 | 10 | **Empower your AI agents and development tools with seamless Obsidian integration!** 11 | 12 | An MCP (Model Context Protocol) server providing comprehensive access to your Obsidian vault. Enables LLMs and AI agents to read, write, search, and manage your notes and files through the [Obsidian Local REST API plugin](https://github.com/coddingtonbear/obsidian-local-rest-api). 13 | 14 | Built on the [`cyanheads/mcp-ts-template`](https://github.com/cyanheads/mcp-ts-template), this server follows a modular architecture with robust error handling, logging, and security features. 15 | 16 | ## 🚀 Core Capabilities: Obsidian Tools 🛠️ 17 | 18 | This server equips your AI with specialized tools to interact with your Obsidian vault: 19 | 20 | | Tool Name | Description | Key Features | 21 | | :------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | 22 | | [`obsidian_read_note`](./src/mcp-server/tools/obsidianReadNoteTool/) | Retrieves the content and metadata of a specified note. | - Read in `markdown` or `json` format.<br/>- Case-insensitive path fallback.<br/>- Includes file stats (creation/modification time). | 23 | | [`obsidian_update_note`](./src/mcp-server/tools/obsidianUpdateNoteTool/) | Modifies notes using whole-file operations. | - `append`, `prepend`, or `overwrite` content.<br/>- Can create files if they don't exist.<br/>- Targets files by path, active note, or periodic note. | 24 | | [`obsidian_search_replace`](./src/mcp-server/tools/obsidianSearchReplaceTool/) | Performs search-and-replace operations within a target note. | - Supports string or regex search.<br/>- Options for case sensitivity, whole word, and replacing all occurrences. | 25 | | [`obsidian_global_search`](./src/mcp-server/tools/obsidianGlobalSearchTool/) | Performs a search across the entire vault. | - Text or regex search.<br/>- Filter by path and modification date.<br/>- Paginated results. | 26 | | [`obsidian_list_notes`](./src/mcp-server/tools/obsidianListNotesTool/) | Lists notes and subdirectories within a specified vault folder. | - Filter by file extension or name regex.<br/>- Provides a formatted tree view of the directory. | 27 | | [`obsidian_manage_frontmatter`](./src/mcp-server/tools/obsidianManageFrontmatterTool/) | Atomically manages a note's YAML frontmatter. | - `get`, `set`, or `delete` frontmatter keys.<br/>- Avoids rewriting the entire file for metadata changes. | 28 | | [`obsidian_manage_tags`](./src/mcp-server/tools/obsidianManageTagsTool/) | Adds, removes, or lists tags for a note. | - Manages tags in both YAML frontmatter and inline content. | 29 | | [`obsidian_delete_note`](./src/mcp-server/tools/obsidianDeleteNoteTool/) | Permanently deletes a specified note from the vault. | - Case-insensitive path fallback for safety. | 30 | 31 | --- 32 | 33 | ## Table of Contents 34 | 35 | | [Overview](#overview) | [Features](#features) | [Configuration](#configuration) | 36 | | [Project Structure](#project-structure) | [Vault Cache Service](#vault-cache-service) | 37 | | [Tools](#tools) | [Resources](#resources) | [Development](#development) | [License](#license) | 38 | 39 | ## Overview 40 | 41 | The Obsidian MCP Server acts as a bridge, allowing applications (MCP Clients) that understand the Model Context Protocol (MCP) – like advanced AI assistants (LLMs), IDE extensions, or custom scripts – to interact directly and safely with your Obsidian vault. 42 | 43 | Instead of complex scripting or manual interaction, your tools can leverage this server to: 44 | 45 | - **Automate vault management**: Read notes, update content, manage frontmatter and tags, search across files, list directories, and delete files programmatically. 46 | - **Integrate Obsidian into AI workflows**: Enable LLMs to access and modify your knowledge base as part of their research, writing, or coding tasks. 47 | - **Build custom Obsidian tools**: Create external applications that interact with your vault data in novel ways. 48 | 49 | Built on the robust `mcp-ts-template`, this server provides a standardized, secure, and efficient way to expose Obsidian functionality via the MCP standard. It achieves this by communicating with the powerful [Obsidian Local REST API plugin](https://github.com/coddingtonbear/obsidian-local-rest-api) running inside your vault. 50 | 51 | > **Developer Note**: This repository includes a [.clinerules](.clinerules) file that serves as a developer cheat sheet for your LLM coding agent with quick reference for the codebase patterns, file locations, and code snippets. 52 | 53 | ## Features 54 | 55 | ### Core Utilities 56 | 57 | Leverages the robust utilities provided by `cyanheads/mcp-ts-template`: 58 | 59 | - **Logging**: Structured, configurable logging (file rotation, console, MCP notifications) with sensitive data redaction. 60 | - **Error Handling**: Centralized error processing, standardized error types (`McpError`), and automatic logging. 61 | - **Configuration**: Environment variable loading (`dotenv`) with comprehensive validation. 62 | - **Input Validation/Sanitization**: Uses `zod` for schema validation and custom sanitization logic. 63 | - **Request Context**: Tracking and correlation of operations via unique request IDs. 64 | - **Type Safety**: Strong typing enforced by TypeScript and Zod schemas. 65 | - **HTTP Transport Option**: Built-in Hono server with SSE, session management, CORS support, and pluggable authentication strategies (JWT and OAuth 2.1). 66 | 67 | ### Obsidian Integration 68 | 69 | - **Obsidian Local REST API Integration**: Communicates directly with the Obsidian Local REST API plugin via HTTP requests managed by the `ObsidianRestApiService`. 70 | - **Comprehensive Command Coverage**: Exposes key vault operations as MCP tools (see [Tools](#tools) section). 71 | - **Vault Interaction**: Supports reading, updating (append, prepend, overwrite), searching (global text/regex, search/replace), listing, deleting, and managing frontmatter and tags. 72 | - **Targeting Flexibility**: Tools can target files by path, the currently active file in Obsidian, or periodic notes (daily, weekly, etc.). 73 | - **Vault Cache Service**: An intelligent in-memory cache that improves performance and resilience. It caches vault content, provides a fallback for the global search tool if the live API fails, and periodically refreshes to stay in sync. 74 | - **Safety Features**: Case-insensitive path fallbacks for file operations, clear distinction between modification types (append, overwrite, etc.). 75 | 76 | ## Installation 77 | 78 | ### Prerequisites 79 | 80 | 1. **Obsidian**: You need Obsidian installed. 81 | 2. **Obsidian Local REST API Plugin**: Install and enable the [Obsidian Local REST API plugin](https://github.com/coddingtonbear/obsidian-local-rest-api) within your Obsidian vault. 82 | 3. **API Key**: Configure an API key within the Local REST API plugin settings in Obsidian. You will need this key to configure the server. 83 | 4. **Node.js & npm**: Ensure you have Node.js (v18 or later recommended) and npm installed. 84 | 85 | ## Configuration 86 | 87 | ### MCP Client Settings 88 | 89 | Add the following to your MCP client's configuration file (e.g., `cline_mcp_settings.json`). This configuration uses `npx` to run the server, which will automatically download & install the package if not already present: 90 | 91 | ```json 92 | { 93 | "mcpServers": { 94 | "obsidian-mcp-server": { 95 | "command": "npx", 96 | "args": ["obsidian-mcp-server"], 97 | "env": { 98 | "OBSIDIAN_API_KEY": "YOUR_API_KEY_FROM_OBSIDIAN_PLUGIN", 99 | "OBSIDIAN_BASE_URL": "http://127.0.0.1:27123", 100 | "OBSIDIAN_VERIFY_SSL": "false", 101 | "OBSIDIAN_ENABLE_CACHE": "true" 102 | }, 103 | "disabled": false, 104 | "autoApprove": [] 105 | } 106 | } 107 | } 108 | ``` 109 | 110 | **Note**: Verify SSL is set to false here because the Obsidian Local REST API plugin uses a self-signed certificate by default. If you are deploying this in a production environment, consider using the encrypted HTTPS endpoint and set `OBSIDIAN_VERIFY_SSL` to `true` after configuring your server to trust the self-signed certificate. 111 | 112 | If you installed from source, change `command` and `args` to point to your local build: 113 | 114 | ```json 115 | { 116 | "mcpServers": { 117 | "obsidian-mcp-server": { 118 | "command": "node", 119 | "args": ["/path/to/your/obsidian-mcp-server/dist/index.js"], 120 | "env": { 121 | "OBSIDIAN_API_KEY": "YOUR_OBSIDIAN_API_KEY", 122 | "OBSIDIAN_BASE_URL": "http://127.0.0.1:27123", 123 | "OBSIDIAN_VERIFY_SSL": "false", 124 | "OBSIDIAN_ENABLE_CACHE": "true" 125 | } 126 | } 127 | } 128 | } 129 | ``` 130 | 131 | ### Environment Variables 132 | 133 | Configure the server using environment variables. These environmental variables are set within your MCP client config/settings (e.g. `cline_mcp_settings.json` for Cline, `claude_desktop_config.json` for Claude Desktop). 134 | 135 | | Variable | Description | Required | Default | 136 | | :------------------------------------ | :----------------------------------------------------------------------- | :------------------- | :----------------------- | 137 | | **`OBSIDIAN_API_KEY`** | API Key from the Obsidian Local REST API plugin. | **Yes** | `undefined` | 138 | | **`OBSIDIAN_BASE_URL`** | Base URL of your Obsidian Local REST API. | **Yes** | `http://127.0.0.1:27123` | 139 | | `MCP_TRANSPORT_TYPE` | Server transport: `stdio` or `http`. | No | `stdio` | 140 | | `MCP_HTTP_PORT` | Port for the HTTP server. | No | `3010` | 141 | | `MCP_HTTP_HOST` | Host for the HTTP server. | No | `127.0.0.1` | 142 | | `MCP_ALLOWED_ORIGINS` | Comma-separated origins for CORS. **Set for production.** | No | (none) | 143 | | `MCP_AUTH_MODE` | Authentication strategy: `jwt` or `oauth`. | No | (none) | 144 | | **`MCP_AUTH_SECRET_KEY`** | 32+ char secret for JWT. **Required for `jwt` mode.** | **Yes (if `jwt`)** | `undefined` | 145 | | `OAUTH_ISSUER_URL` | URL of the OAuth 2.1 issuer. | **Yes (if `oauth`)** | `undefined` | 146 | | `OAUTH_AUDIENCE` | Audience claim for OAuth tokens. | **Yes (if `oauth`)** | `undefined` | 147 | | `OAUTH_JWKS_URI` | URI for the JSON Web Key Set (optional, derived from issuer if omitted). | No | (derived) | 148 | | `MCP_LOG_LEVEL` | Logging level (`debug`, `info`, `error`, etc.). | No | `info` | 149 | | `OBSIDIAN_VERIFY_SSL` | Set to `false` to disable SSL verification. | No | `true` | 150 | | `OBSIDIAN_ENABLE_CACHE` | Set to `true` to enable the in-memory vault cache. | No | `true` | 151 | | `OBSIDIAN_CACHE_REFRESH_INTERVAL_MIN` | Refresh interval for the vault cache in minutes. | No | `10` | 152 | 153 | ### Connecting to the Obsidian API 154 | 155 | To connect the MCP server to your Obsidian vault, you need to configure the base URL (`OBSIDIAN_BASE_URL`) and API key (`OBSIDIAN_API_KEY`). The Obsidian Local REST API plugin offers two ways to connect: 156 | 157 | 1. **Encrypted (HTTPS) - Default**: 158 | 159 | - The plugin provides a secure `https://` endpoint (e.g., `https://127.0.0.1:27124`). 160 | - This uses a self-signed certificate, which will cause connection errors by default. 161 | - **To fix this**, you must set the `OBSIDIAN_VERIFY_SSL` environment variable to `"false"`. This tells the server to trust the self-signed certificate. 162 | 163 | 2. **Non-encrypted (HTTP) - Recommended for Simplicity**: 164 | - In the plugin's settings within Obsidian, you can enable the "Non-encrypted (HTTP) Server". 165 | - This provides a simpler `http://` endpoint (e.g., `http://127.0.0.1:27123`). 166 | - When using this URL, you do not need to worry about SSL verification. 167 | 168 | **Example `env` configuration for your MCP client:** 169 | 170 | _Using the non-encrypted HTTP URL (recommended):_ 171 | 172 | ```json 173 | "env": { 174 | "OBSIDIAN_API_KEY": "YOUR_API_KEY_FROM_OBSIDIAN_PLUGIN", 175 | "OBSIDIAN_BASE_URL": "http://127.0.0.1:27123" 176 | } 177 | ``` 178 | 179 | _Using the encrypted HTTPS URL:_ 180 | 181 | ```json 182 | "env": { 183 | "OBSIDIAN_API_KEY": "YOUR_API_KEY_FROM_OBSIDIAN_PLUGIN", 184 | "OBSIDIAN_BASE_URL": "https://127.0.0.1:27124", 185 | "OBSIDIAN_VERIFY_SSL": "false" 186 | } 187 | ``` 188 | 189 | ## Project Structure 190 | 191 | The codebase follows a modular structure within the `src/` directory: 192 | 193 | ``` 194 | src/ 195 | ├── index.ts # Entry point: Initializes and starts the server 196 | ├── config/ # Configuration loading (env vars, package info) 197 | │ └── index.ts 198 | ├── mcp-server/ # Core MCP server logic and capability registration 199 | │ ├── server.ts # Server setup, transport handling, tool/resource registration 200 | │ ├── resources/ # MCP Resource implementations (currently none) 201 | │ ├── tools/ # MCP Tool implementations (subdirs per tool) 202 | │ └── transports/ # Stdio and HTTP transport logic 203 | │ └── auth/ # Authentication strategies (JWT, OAuth) 204 | ├── services/ # Abstractions for external APIs or internal caching 205 | │ └── obsidianRestAPI/ # Typed client for Obsidian Local REST API 206 | ├── types-global/ # Shared TypeScript type definitions (errors, etc.) 207 | └── utils/ # Common utility functions (logger, error handler, security, etc.) 208 | ``` 209 | 210 | For a detailed file tree, run `npm run tree` or see [docs/tree.md](docs/tree.md). 211 | 212 | ## Vault Cache Service 213 | 214 | This server includes an intelligent **in-memory cache** designed to enhance performance and resilience when interacting with your vault. 215 | 216 | ### Purpose and Benefits 217 | 218 | - **Performance**: By caching file content and metadata, the server can perform search operations much faster, especially in large vaults. This reduces the number of direct requests to the Obsidian Local REST API, resulting in a snappier experience. 219 | - **Resilience**: The cache acts as a fallback for the `obsidian_global_search` tool. If the live API search fails or times out, the server seamlessly uses the cache to provide results, ensuring that search functionality remains available even if the Obsidian API is temporarily unresponsive. 220 | - **Efficiency**: The cache is designed to be efficient. It performs an initial build on startup and then periodically refreshes in the background by checking for file modifications, ensuring it stays reasonably up-to-date without constant, heavy API polling. 221 | 222 | ### How It Works 223 | 224 | 1. **Initialization**: When enabled, the `VaultCacheService` builds an in-memory map of all `.md` files in your vault, storing their content and modification times. 225 | 2. **Periodic Refresh**: The cache automatically refreshes at a configurable interval (defaulting to 10 minutes). During a refresh, it only fetches content for files that are new or have been modified since the last check. 226 | 3. **Proactive Updates**: After a file is modified through a tool like `obsidian_update_file`, the service proactively updates the cache for that specific file, ensuring immediate consistency. 227 | 4. **Search Fallback**: The `obsidian_global_search` tool first attempts a live API search. If this fails, it automatically falls back to searching the in-memory cache. 228 | 229 | ### Configuration 230 | 231 | The cache is enabled by default but can be configured via environment variables: 232 | 233 | - **`OBSIDIAN_ENABLE_CACHE`**: Set to `true` (default) or `false` to enable or disable the cache service. 234 | - **`OBSIDIAN_CACHE_REFRESH_INTERVAL_MIN`**: Defines the interval in minutes for the periodic background refresh. Defaults to `10`. 235 | 236 | ## Tools 237 | 238 | The Obsidian MCP Server provides a suite of tools for interacting with your vault, callable via the Model Context Protocol. 239 | 240 | | Tool Name | Description | Key Arguments | 241 | | :---------------------------- | :-------------------------------------------------------- | :------------------------------------------------------------ | 242 | | `obsidian_read_note` | Retrieves the content and metadata of a note. | `filePath`, `format?`, `includeStat?` | 243 | | `obsidian_update_note` | Modifies a file by appending, prepending, or overwriting. | `targetType`, `content`, `targetIdentifier?`, `wholeFileMode` | 244 | | `obsidian_search_replace` | Performs search-and-replace operations in a note. | `targetType`, `replacements`, `useRegex?`, `replaceAll?` | 245 | | `obsidian_global_search` | Searches the entire vault for content. | `query`, `searchInPath?`, `useRegex?`, `page?`, `pageSize?` | 246 | | `obsidian_list_notes` | Lists notes and subdirectories in a folder. | `dirPath`, `fileExtensionFilter?`, `nameRegexFilter?` | 247 | | `obsidian_manage_frontmatter` | Gets, sets, or deletes keys in a note's frontmatter. | `filePath`, `operation`, `key`, `value?` | 248 | | `obsidian_manage_tags` | Adds, removes, or lists tags in a note. | `filePath`, `operation`, `tags` | 249 | | `obsidian_delete_note` | Permanently deletes a note from the vault. | `filePath` | 250 | 251 | _Note: All tools support comprehensive error handling and return structured JSON responses._ 252 | 253 | ## Resources 254 | 255 | **MCP Resources are not implemented in this version.** 256 | 257 | This server currently focuses on providing interactive tools for vault manipulation. Future development may introduce resource capabilities (e.g., exposing notes or search results as readable resources). 258 | 259 | ## Development 260 | 261 | ### Build and Test 262 | 263 | To get started with development, clone the repository, install dependencies, and use the following scripts: 264 | 265 | ```bash 266 | # Install dependencies 267 | npm install 268 | 269 | # Build the project (compile TS to JS in dist/ and make executable) 270 | npm run rebuild 271 | 272 | # Start the server locally using stdio transport 273 | npm start:stdio 274 | 275 | # Start the server using http transport 276 | npm run start:http 277 | 278 | # Format code using Prettier 279 | npm run format 280 | 281 | # Inspect the server's capabilities using the MCP Inspector tool 282 | npm run inspect:stdio 283 | # or for the http transport: 284 | npm run inspect:http 285 | ``` 286 | 287 | ## License 288 | 289 | This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. 290 | 291 | --- 292 | 293 | <div align="center"> 294 | Built with the <a href="https://modelcontextprotocol.io/">Model Context Protocol</a> 295 | </div> 296 | ``` -------------------------------------------------------------------------------- /src/utils/metrics/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./tokenCounter.js"; 2 | ``` -------------------------------------------------------------------------------- /src/services/obsidianRestAPI/vaultCache/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Barrel file for the VaultCacheService. 3 | */ 4 | export * from "./service.js"; 5 | ``` -------------------------------------------------------------------------------- /src/utils/security/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./sanitization.js"; 2 | export * from "./rateLimiter.js"; 3 | export * from "./idGenerator.js"; 4 | ``` -------------------------------------------------------------------------------- /src/utils/parsing/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./jsonParser.js"; 2 | export * from "./dateParser.js"; 3 | // Removed export for dateUtils.js as it was moved 4 | ``` -------------------------------------------------------------------------------- /src/utils/internal/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export * from "./errorHandler.js"; 2 | export * from "./logger.js"; 3 | export * from "./requestContext.js"; 4 | export * from "./asyncUtils.js"; 5 | ``` -------------------------------------------------------------------------------- /src/utils/obsidian/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Barrel file for Obsidian-specific utilities. 3 | */ 4 | export * from "./obsidianStatUtils.js"; 5 | export * from "./obsidianApiUtils.js"; 6 | ``` -------------------------------------------------------------------------------- /src/mcp-server/tools/obsidianManageTagsTool/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export { 2 | ObsidianManageTagsInputSchemaShape, 3 | processObsidianManageTags, 4 | } from "./logic.js"; 5 | export type { 6 | ObsidianManageTagsInput, 7 | ObsidianManageTagsResponse, 8 | } from "./logic.js"; 9 | export { registerObsidianManageTagsTool } from "./registration.js"; 10 | ``` -------------------------------------------------------------------------------- /src/mcp-server/tools/obsidianManageFrontmatterTool/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | export { 2 | ObsidianManageFrontmatterInputSchemaShape, 3 | processObsidianManageFrontmatter, 4 | } from "./logic.js"; 5 | export type { 6 | ObsidianManageFrontmatterInput, 7 | ObsidianManageFrontmatterResponse, 8 | } from "./logic.js"; 9 | export { registerObsidianManageFrontmatterTool } from "./registration.js"; 10 | ``` -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "entryPoints": ["src", "scripts"], 4 | "entryPointStrategy": "expand", 5 | "out": "docs/api", 6 | "readme": "README.md", 7 | "name": "Obsidian MCP Server API Documentation", 8 | "includeVersion": true, 9 | "excludePrivate": true, 10 | "excludeProtected": true, 11 | "excludeInternal": true, 12 | "theme": "default" 13 | } 14 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "outDir": "./dist", 9 | "rootDir": "./src", 10 | "declaration": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | ``` -------------------------------------------------------------------------------- /repomix.config.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "output": { 3 | "filePath": "repomix-output.xml", 4 | "style": "xml", 5 | "removeComments": false, 6 | "removeEmptyLines": false, 7 | "topFilesLength": 5, 8 | "showLineNumbers": false, 9 | "copyToClipboard": false 10 | }, 11 | "include": [], 12 | "ignore": { 13 | "useGitignore": true, 14 | "useDefaultPatterns": true, 15 | "customPatterns": [".clinerules"] 16 | }, 17 | "security": { 18 | "enableSecurityCheck": true 19 | } 20 | } 21 | ``` -------------------------------------------------------------------------------- /src/mcp-server/transports/auth/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Barrel file for the auth module. 3 | * Exports core utilities and middleware strategies for easier imports. 4 | * @module src/mcp-server/transports/auth/index 5 | */ 6 | 7 | export { authContext } from "./core/authContext.js"; 8 | export { withRequiredScopes } from "./core/authUtils.js"; 9 | export type { AuthInfo } from "./core/authTypes.js"; 10 | 11 | export { mcpAuthMiddleware as jwtAuthMiddleware } from "./strategies/jwt/jwtMiddleware.js"; 12 | export { oauthMiddleware } from "./strategies/oauth/oauthMiddleware.js"; 13 | ``` -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Publish Package to npm 2 | on: 3 | push: 4 | tags: 5 | - "v*" 6 | 7 | jobs: 8 | build-and-publish: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: "20.x" 19 | registry-url: "https://registry.npmjs.org" 20 | cache: "npm" 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Build 26 | run: npm run build 27 | 28 | - name: Publish to npm 29 | run: npm publish 30 | env: 31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | ``` -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | // Re-export all utilities from their categorized subdirectories 2 | export * from "./internal/index.js"; 3 | export * from "./parsing/index.js"; 4 | export * from "./security/index.js"; 5 | export * from "./metrics/index.js"; 6 | export * from "./obsidian/index.js"; // Added export for obsidian utils 7 | 8 | // It's good practice to have index.ts files in each subdirectory 9 | // that export the contents of that directory. 10 | // Assuming those will be created or already exist. 11 | // If not, this might need adjustment to export specific files, e.g.: 12 | // export * from './internal/errorHandler.js'; 13 | // export * from './internal/logger.js'; 14 | // ... etc. 15 | ``` -------------------------------------------------------------------------------- /src/mcp-server/tools/obsidianReadNoteTool/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Barrel file for the 'obsidian_read_note' MCP tool. 3 | * 4 | * This file serves as the public entry point for the obsidian_read_note tool module. 5 | * It re-exports the primary registration function (`registerObsidianReadNoteTool`) 6 | * from the './registration.js' module. This pattern simplifies imports for consumers 7 | * of the tool, allowing them to import necessary components from a single location. 8 | * 9 | * Consumers (like the main server setup) should import the registration function 10 | * from this file to integrate the tool into the MCP server instance. 11 | */ 12 | export { registerObsidianReadNoteTool } from "./registration.js"; 13 | ``` -------------------------------------------------------------------------------- /src/mcp-server/tools/obsidianListNotesTool/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Barrel file for the 'obsidian_list_notes' MCP tool. 3 | * 4 | * This file serves as the public entry point for the obsidian_list_notes tool module. 5 | * It re-exports the primary registration function (`registerObsidianListNotesTool`) 6 | * from the './registration.js' module. This pattern simplifies imports for consumers 7 | * of the tool, allowing them to import necessary components from a single location. 8 | * 9 | * Consumers (like the main server setup) should import the registration function 10 | * from this file to integrate the tool into the MCP server instance. 11 | */ 12 | export { registerObsidianListNotesTool } from "./registration.js"; 13 | ``` -------------------------------------------------------------------------------- /src/mcp-server/tools/obsidianDeleteNoteTool/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Barrel file for the 'obsidian_delete_note' MCP tool. 3 | * 4 | * This file serves as the public entry point for the obsidian_delete_note tool module. 5 | * It re-exports the primary registration function (`registerObsidianDeleteNoteTool`) 6 | * from the './registration.js' module. This pattern simplifies imports for consumers 7 | * of the tool, allowing them to import necessary components from a single location. 8 | * 9 | * Consumers (like the main server setup) should import the registration function 10 | * from this file to integrate the tool into the MCP server instance. 11 | */ 12 | export { registerObsidianDeleteNoteTool } from "./registration.js"; 13 | ``` -------------------------------------------------------------------------------- /src/mcp-server/tools/obsidianUpdateNoteTool/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Barrel file for the 'obsidian_update_note' MCP tool. 3 | * 4 | * This file serves as the public entry point for the obsidian_update_note tool module. 5 | * It re-exports the primary registration function (`registerObsidianUpdateNoteTool`) 6 | * from the './registration.js' module. This pattern simplifies imports for consumers 7 | * of the tool, allowing them to import necessary components from a single location. 8 | * 9 | * Consumers (like the main server setup) should import the registration function 10 | * from this file to integrate the tool into the MCP server instance. 11 | */ 12 | export { registerObsidianUpdateNoteTool } from "./registration.js"; 13 | ``` -------------------------------------------------------------------------------- /src/mcp-server/tools/obsidianSearchReplaceTool/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Barrel file for the 'obsidian_search_replace' MCP tool. 3 | * 4 | * This file serves as the public entry point for the obsidian_search_replace tool module. 5 | * It re-exports the primary registration function (`registerObsidianSearchReplaceTool`) 6 | * from the './registration.js' module. This pattern simplifies imports for consumers 7 | * of the tool, allowing them to import necessary components from a single location. 8 | * 9 | * Consumers (like the main server setup) should import the registration function 10 | * from this file to integrate the tool into the MCP server instance. 11 | */ 12 | export { registerObsidianSearchReplaceTool } from "./registration.js"; 13 | ``` -------------------------------------------------------------------------------- /src/mcp-server/transports/auth/core/authTypes.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Shared types for authentication middleware. 3 | * @module src/mcp-server/transports/auth/core/auth.types 4 | */ 5 | 6 | import type { AuthInfo as SdkAuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js"; 7 | 8 | /** 9 | * Defines the structure for authentication information derived from a token. 10 | * It extends the base SDK type to include common optional claims. 11 | */ 12 | export type AuthInfo = SdkAuthInfo & { 13 | subject?: string; 14 | }; 15 | 16 | // Extend the Node.js IncomingMessage type to include an optional 'auth' property. 17 | // This is necessary for type-safe access when attaching the AuthInfo. 18 | declare module "http" { 19 | interface IncomingMessage { 20 | auth?: AuthInfo; 21 | } 22 | } 23 | ``` -------------------------------------------------------------------------------- /src/mcp-server/tools/obsidianGlobalSearchTool/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Barrel file for the 'obsidian_global_search' MCP tool. 3 | * 4 | * This file serves as the public entry point for the obsidian_global_search tool module. 5 | * It re-exports the primary registration function (`registerObsidianGlobalSearchTool`) 6 | * from the './registration.js' module. This pattern simplifies imports for consumers 7 | * of the tool, allowing them to import necessary components from a single location. 8 | * 9 | * Consumers (like the main server setup) should import the registration function 10 | * from this file to integrate the tool into the MCP server instance. 11 | */ 12 | 13 | export { registerObsidianGlobalSearchTool } from "./registration.js"; // Ensure '.js' extension for ES module resolution 14 | ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | build: 2 | dockerBuildPath: . # Explicitly set build context to the root directory 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | type: object 7 | required: 8 | - obsidianApiKey 9 | - obsidianBaseUrl 10 | properties: 11 | obsidianApiKey: 12 | type: string 13 | description: The API key generated by the Obsidian Local REST API plugin. 14 | obsidianBaseUrl: 15 | type: string 16 | format: uri # Ensure it's a valid URL format 17 | description: The base URL of your Obsidian Local REST API (e.g., http://127.0.0.1:27123). 18 | commandFunction: | 19 | (config) => ({ 20 | command: 'npx', 21 | args: ['obsidian-mcp-server'], 22 | env: { 23 | OBSIDIAN_API_KEY: config.obsidianApiKey, 24 | OBSIDIAN_BASE_URL: config.obsidianBaseUrl, 25 | OBSIDIAN_VERIFY_SSL: "false" 26 | } 27 | }) 28 | ``` -------------------------------------------------------------------------------- /mcp.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "mcpServers": { 3 | "obsidian-mcp-server-stdio": { 4 | "command": "npx", 5 | "args": ["obsidian-mcp-server"], 6 | "env": { 7 | "OBSIDIAN_API_KEY": "YOUR_OBSIDIAN_API_KEY_HERE", 8 | "OBSIDIAN_BASE_URL": "http://127.0.0.1:27123", 9 | "MCP_TRANSPORT_TYPE": "stdio", 10 | "MCP_LOG_LEVEL": "debug" 11 | } 12 | }, 13 | "obsidian-mcp-server-http": { 14 | "command": "npx", 15 | "args": ["obsidian-mcp-server"], 16 | "env": { 17 | "OBSIDIAN_API_KEY": "YOUR_OBSIDIAN_API_KEY_HERE", 18 | "OBSIDIAN_BASE_URL": "http://127.0.0.1:27123", 19 | "MCP_TRANSPORT_TYPE": "http", 20 | "MCP_HTTP_PORT": "3010", 21 | "MCP_HTTP_HOST": "127.0.0.1", 22 | "MCP_LOG_LEVEL": "debug", 23 | "MCP_AUTH_SECRET_KEY": "YOUR_MIN_32_CHAR_SECRET_KEY_HERE_IF_USING_HTTP_AUTH" 24 | } 25 | } 26 | } 27 | } 28 | ``` -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- ```yaml 1 | # These are supported funding model platforms 2 | 3 | github: cyanheads 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: cyanheads 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | ``` -------------------------------------------------------------------------------- /src/services/obsidianRestAPI/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @module ObsidianRestApiService Barrel File 3 | * @description 4 | * Exports the singleton instance of the Obsidian REST API service and related types. 5 | */ 6 | 7 | export * from "./types.js"; // Export all types 8 | // Removed singleton export 9 | export { ObsidianRestApiService } from "./service.js"; // Export the class itself 10 | // Export method modules if direct access is desired, though typically accessed via service instance 11 | export * as activeFileMethods from "./methods/activeFileMethods.js"; 12 | export * as commandMethods from "./methods/commandMethods.js"; 13 | export * as openMethods from "./methods/openMethods.js"; 14 | export * as patchMethods from "./methods/patchMethods.js"; 15 | export * as periodicNoteMethods from "./methods/periodicNoteMethods.js"; 16 | export * as searchMethods from "./methods/searchMethods.js"; 17 | export * as vaultMethods from "./methods/vaultMethods.js"; 18 | export { VaultCacheService } from "./vaultCache/index.js"; 19 | ``` -------------------------------------------------------------------------------- /src/services/obsidianRestAPI/methods/openMethods.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @module OpenMethods 3 | * @description 4 | * Methods for opening files in Obsidian via the REST API. 5 | */ 6 | 7 | import { RequestContext } from "../../../utils/index.js"; 8 | import { RequestFunction } from "../types.js"; 9 | 10 | /** 11 | * Opens a specific file in Obsidian. Creates the file if it doesn't exist. 12 | * @param _request - The internal request function from the service instance. 13 | * @param filePath - Vault-relative path to the file. 14 | * @param newLeaf - Whether to open the file in a new editor tab (leaf). 15 | * @param context - Request context. 16 | * @returns {Promise<void>} Resolves on success (200 OK, but no body expected). 17 | */ 18 | export async function openFile( 19 | _request: RequestFunction, 20 | filePath: string, 21 | newLeaf: boolean = false, 22 | context: RequestContext, 23 | ): Promise<void> { 24 | // This endpoint returns 200 OK, not 204 25 | await _request<void>( 26 | { 27 | method: "POST", 28 | url: `/open/${encodeURIComponent(filePath)}`, 29 | params: { newLeaf }, 30 | }, 31 | context, 32 | "openFile", 33 | ); 34 | } 35 | ``` -------------------------------------------------------------------------------- /env.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "properties": { 3 | "MAX_TOKENS": { 4 | "default": "20000", 5 | "description": "Maximum tokens per response", 6 | "type": "string" 7 | }, 8 | "NODE_ENV": { 9 | "default": "production", 10 | "description": "The Node.js environment setting", 11 | "type": "string" 12 | }, 13 | "OBSIDIAN_API_KEY": { 14 | "description": "Your API key for the Obsidian MCP Server", 15 | "type": "string" 16 | }, 17 | "OBSIDIAN_VERIFY_SSL": { 18 | "default": "false", 19 | "description": "Enable SSL verification", 20 | "type": "string" 21 | }, 22 | "RATE_LIMIT_MAX_REQUESTS": { 23 | "default": "200", 24 | "description": "Max requests per rate limit window", 25 | "type": "string" 26 | }, 27 | "RATE_LIMIT_WINDOW_MS": { 28 | "default": "900000", 29 | "description": "Rate limit window in milliseconds (default: 15 minutes)", 30 | "type": "string" 31 | }, 32 | "TOOL_TIMEOUT_MS": { 33 | "default": "60000", 34 | "description": "Tool execution timeout in milliseconds", 35 | "type": "string" 36 | } 37 | }, 38 | "required": ["OBSIDIAN_API_KEY"], 39 | "type": "object" 40 | } 41 | ``` -------------------------------------------------------------------------------- /src/mcp-server/transports/auth/core/authContext.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Defines the AsyncLocalStorage context for authentication information. 3 | * This module provides a mechanism to store and retrieve authentication details 4 | * (like scopes and client ID) across asynchronous operations, making it available 5 | * from the middleware layer down to the tool and resource handlers without 6 | * drilling props. 7 | * 8 | * @module src/mcp-server/transports/auth/core/authContext 9 | */ 10 | 11 | import { AsyncLocalStorage } from "async_hooks"; 12 | import type { AuthInfo } from "./authTypes.js"; 13 | 14 | /** 15 | * Defines the structure of the store used within the AsyncLocalStorage. 16 | * It holds the authentication information for the current request context. 17 | */ 18 | interface AuthStore { 19 | authInfo: AuthInfo; 20 | } 21 | 22 | /** 23 | * An instance of AsyncLocalStorage to hold the authentication context (`AuthStore`). 24 | * This allows `authInfo` to be accessible throughout the async call chain of a request 25 | * after being set in the authentication middleware. 26 | * 27 | * @example 28 | * // In middleware: 29 | * await authContext.run({ authInfo }, next); 30 | * 31 | * // In a deeper handler: 32 | * const store = authContext.getStore(); 33 | * const scopes = store?.authInfo.scopes; 34 | */ 35 | export const authContext = new AsyncLocalStorage<AuthStore>(); 36 | ``` -------------------------------------------------------------------------------- /src/utils/obsidian/obsidianApiUtils.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @module ObsidianApiUtils 3 | * @description 4 | * Internal utilities for the Obsidian REST API service. 5 | */ 6 | 7 | /** 8 | * Encodes a vault-relative file path correctly for API URLs. 9 | * Ensures path separators '/' are not encoded, but individual components are. 10 | * Handles leading slashes correctly. 11 | * 12 | * @param filePath - The raw vault-relative file path (e.g., "/Notes/My File.md" or "Notes/My File.md"). 13 | * @returns The URL-encoded path suitable for appending to `/vault`. 14 | */ 15 | export function encodeVaultPath(filePath: string): string { 16 | // 1. Trim whitespace and remove any leading/trailing slashes for consistent processing. 17 | const trimmedPath = filePath.trim().replace(/^\/+|\/+$/g, ""); 18 | 19 | // 2. If the original path was just '/' or empty, return an empty string (represents root for files). 20 | if (trimmedPath === "") { 21 | // For file operations, the API expects /vault/filename.md at the root, 22 | // so an empty encoded path segment is correct here. 23 | // For listFiles, we handle the root case separately. 24 | return ""; 25 | } 26 | 27 | // 3. Split into components, encode each component, then rejoin with literal '/'. 28 | const encodedComponents = trimmedPath.split("/").map(encodeURIComponent); 29 | const encodedPath = encodedComponents.join("/"); 30 | 31 | // 4. Prepend the leading slash. 32 | return `/${encodedPath}`; 33 | } 34 | ``` -------------------------------------------------------------------------------- /src/services/obsidianRestAPI/methods/commandMethods.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @module CommandMethods 3 | * @description 4 | * Methods for interacting with Obsidian commands via the REST API. 5 | */ 6 | 7 | import { RequestContext } from "../../../utils/index.js"; 8 | import { 9 | ObsidianCommand, 10 | CommandListResponse, 11 | RequestFunction, 12 | } from "../types.js"; 13 | 14 | /** 15 | * Executes a registered Obsidian command by its ID. 16 | * @param _request - The internal request function from the service instance. 17 | * @param commandId - The ID of the command (e.g., "app:go-back"). 18 | * @param context - Request context. 19 | * @returns {Promise<void>} Resolves on success (204 No Content). 20 | */ 21 | export async function executeCommand( 22 | _request: RequestFunction, 23 | commandId: string, 24 | context: RequestContext, 25 | ): Promise<void> { 26 | await _request<void>( 27 | { 28 | method: "POST", 29 | url: `/commands/${encodeURIComponent(commandId)}/`, 30 | }, 31 | context, 32 | "executeCommand", 33 | ); 34 | } 35 | 36 | /** 37 | * Lists all available Obsidian commands. 38 | * @param _request - The internal request function from the service instance. 39 | * @param context - Request context. 40 | * @returns A list of available commands. 41 | */ 42 | export async function listCommands( 43 | _request: RequestFunction, 44 | context: RequestContext, 45 | ): Promise<ObsidianCommand[]> { 46 | const response = await _request<CommandListResponse>( 47 | { 48 | method: "GET", 49 | url: "/commands/", 50 | }, 51 | context, 52 | "listCommands", 53 | ); 54 | return response.commands; 55 | } 56 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # ---- Base Node ---- 2 | # Use a specific Node.js version known to work, Alpine for smaller size 3 | FROM node:23-alpine AS base 4 | WORKDIR /usr/src/app 5 | ENV NODE_ENV=production 6 | 7 | # ---- Dependencies ---- 8 | # Install dependencies first to leverage Docker cache 9 | FROM base AS deps 10 | WORKDIR /usr/src/app 11 | COPY package.json package-lock.json* ./ 12 | # Use npm ci for deterministic installs based on lock file 13 | # Install only production dependencies in this stage for the final image 14 | RUN npm ci --only=production 15 | 16 | # ---- Builder ---- 17 | # Build the application 18 | FROM base AS builder 19 | WORKDIR /usr/src/app 20 | # Copy dependency manifests and install *all* dependencies (including dev) 21 | COPY package.json package-lock.json* ./ 22 | RUN npm ci 23 | # Copy the rest of the source code 24 | COPY . . 25 | # Build the TypeScript project 26 | RUN npm run build 27 | 28 | # ---- Runner ---- 29 | # Final stage with only production dependencies and built code 30 | FROM base AS runner 31 | WORKDIR /usr/src/app 32 | # Copy production node_modules from the 'deps' stage 33 | COPY --from=deps /usr/src/app/node_modules ./node_modules 34 | # Copy built application from the 'builder' stage 35 | COPY --from=builder /usr/src/app/dist ./dist 36 | # Copy package.json (needed for potential runtime info, like version) 37 | COPY package.json . 38 | 39 | # Create a non-root user and switch to it 40 | RUN addgroup -S appgroup && adduser -S appuser -G appgroup 41 | USER appuser 42 | 43 | # Expose port if the application runs a server (adjust if needed) 44 | # EXPOSE 3000 45 | 46 | # Command to run the application 47 | CMD ["node", "dist/index.js"] 48 | ``` -------------------------------------------------------------------------------- /src/services/obsidianRestAPI/methods/searchMethods.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @module SearchMethods 3 | * @description 4 | * Methods for performing searches via the Obsidian REST API. 5 | */ 6 | 7 | import { RequestContext } from "../../../utils/index.js"; 8 | import { 9 | SimpleSearchResult, 10 | ComplexSearchResult, 11 | RequestFunction, 12 | } from "../types.js"; 13 | 14 | /** 15 | * Performs a simple text search across the vault. 16 | * @param _request - The internal request function from the service instance. 17 | * @param query - The text query string. 18 | * @param contextLength - Number of characters surrounding each match (default 100). 19 | * @param context - Request context. 20 | * @returns An array of search results. 21 | */ 22 | export async function searchSimple( 23 | _request: RequestFunction, 24 | query: string, 25 | contextLength: number = 100, 26 | context: RequestContext, 27 | ): Promise<SimpleSearchResult[]> { 28 | return _request<SimpleSearchResult[]>( 29 | { 30 | method: "POST", 31 | url: "/search/simple/", 32 | params: { query, contextLength }, // Send as query parameters 33 | }, 34 | context, 35 | "searchSimple", 36 | ); 37 | } 38 | 39 | /** 40 | * Performs a complex search using Dataview DQL or JsonLogic. 41 | * @param _request - The internal request function from the service instance. 42 | * @param query - The query string (DQL) or JSON object (JsonLogic). 43 | * @param contentType - The content type header indicating the query format. 44 | * @param context - Request context. 45 | * @returns An array of search results. 46 | */ 47 | export async function searchComplex( 48 | _request: RequestFunction, 49 | query: string | object, 50 | contentType: 51 | | "application/vnd.olrapi.dataview.dql+txt" 52 | | "application/vnd.olrapi.jsonlogic+json", 53 | context: RequestContext, 54 | ): Promise<ComplexSearchResult[]> { 55 | return _request<ComplexSearchResult[]>( 56 | { 57 | method: "POST", 58 | url: "/search/", 59 | headers: { "Content-Type": contentType }, 60 | data: query, 61 | }, 62 | context, 63 | "searchComplex", 64 | ); 65 | } 66 | ``` -------------------------------------------------------------------------------- /src/mcp-server/transports/httpErrorHandler.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Centralized error handler for the Hono HTTP transport. 3 | * This middleware intercepts errors that occur during request processing, 4 | * standardizes them using the application's ErrorHandler utility, and 5 | * formats them into a consistent JSON-RPC error response. 6 | * @module src/mcp-server/transports/httpErrorHandler 7 | */ 8 | 9 | import { Context } from "hono"; 10 | import { StatusCode } from "hono/utils/http-status"; 11 | import { BaseErrorCode, McpError } from "../../types-global/errors.js"; 12 | import { ErrorHandler, requestContextService } from "../../utils/index.js"; 13 | 14 | /** 15 | * A centralized error handling middleware for Hono. 16 | * This function is registered with `app.onError()` and will catch any errors 17 | * thrown from preceding middleware or route handlers. 18 | * 19 | * @param err - The error that was thrown. 20 | * @param c - The Hono context object for the request. 21 | * @returns A Response object containing the formatted JSON-RPC error. 22 | */ 23 | export const httpErrorHandler = async (err: Error, c: Context) => { 24 | const context = requestContextService.createRequestContext({ 25 | operation: "httpErrorHandler", 26 | path: c.req.path, 27 | method: c.req.method, 28 | }); 29 | 30 | const handledError = ErrorHandler.handleError(err, { 31 | operation: "httpTransport", 32 | context, 33 | }); 34 | 35 | let status = 500; 36 | if (handledError instanceof McpError) { 37 | switch (handledError.code) { 38 | case BaseErrorCode.NOT_FOUND: 39 | status = 404; 40 | break; 41 | case BaseErrorCode.UNAUTHORIZED: 42 | status = 401; 43 | break; 44 | case BaseErrorCode.FORBIDDEN: 45 | status = 403; 46 | break; 47 | case BaseErrorCode.VALIDATION_ERROR: 48 | status = 400; 49 | break; 50 | case BaseErrorCode.CONFLICT: 51 | status = 409; 52 | break; 53 | case BaseErrorCode.RATE_LIMITED: 54 | status = 429; 55 | break; 56 | default: 57 | status = 500; 58 | } 59 | } 60 | 61 | // Attempt to get the request ID from the body, but don't fail if it's not there or unreadable. 62 | let requestId: string | number | null = null; 63 | try { 64 | const body = await c.req.json(); 65 | requestId = body?.id || null; 66 | } catch { 67 | // Ignore parsing errors, requestId will remain null 68 | } 69 | 70 | const errorCode = 71 | handledError instanceof McpError ? handledError.code : -32603; 72 | 73 | c.status(status as StatusCode); 74 | return c.json({ 75 | jsonrpc: "2.0", 76 | error: { 77 | code: errorCode, 78 | message: handledError.message, 79 | }, 80 | id: requestId, 81 | }); 82 | }; 83 | ``` -------------------------------------------------------------------------------- /src/mcp-server/transports/auth/core/authUtils.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Provides utility functions for authorization, specifically for 3 | * checking token scopes against required permissions for a given operation. 4 | * @module src/mcp-server/transports/auth/core/authUtils 5 | */ 6 | 7 | import { BaseErrorCode, McpError } from "../../../../types-global/errors.js"; 8 | import { logger, requestContextService } from "../../../../utils/index.js"; 9 | import { authContext } from "./authContext.js"; 10 | 11 | /** 12 | * Checks if the current authentication context contains all the specified scopes. 13 | * This function is designed to be called within tool or resource handlers to 14 | * enforce scope-based access control. It retrieves the authentication information 15 | * from `authContext` (AsyncLocalStorage). 16 | * 17 | * @param requiredScopes - An array of scope strings that are mandatory for the operation. 18 | * @throws {McpError} Throws an error with `BaseErrorCode.INTERNAL_ERROR` if the 19 | * authentication context is missing, which indicates a server configuration issue. 20 | * @throws {McpError} Throws an error with `BaseErrorCode.FORBIDDEN` if one or 21 | * more required scopes are not present in the validated token. 22 | */ 23 | export function withRequiredScopes(requiredScopes: string[]): void { 24 | const store = authContext.getStore(); 25 | 26 | if (!store || !store.authInfo) { 27 | // This is a server-side logic error; the auth middleware should always populate this. 28 | throw new McpError( 29 | BaseErrorCode.INTERNAL_ERROR, 30 | "Authentication context is missing. This indicates a server configuration error.", 31 | requestContextService.createRequestContext({ 32 | operation: "withRequiredScopesCheck", 33 | error: "AuthStore not found in AsyncLocalStorage.", 34 | }), 35 | ); 36 | } 37 | 38 | const { scopes: grantedScopes, clientId } = store.authInfo; 39 | const grantedScopeSet = new Set(grantedScopes); 40 | 41 | const missingScopes = requiredScopes.filter( 42 | (scope) => !grantedScopeSet.has(scope), 43 | ); 44 | 45 | if (missingScopes.length > 0) { 46 | const context = requestContextService.createRequestContext({ 47 | operation: "withRequiredScopesCheck", 48 | required: requiredScopes, 49 | granted: grantedScopes, 50 | missing: missingScopes, 51 | clientId: clientId, 52 | subject: store.authInfo.subject, 53 | }); 54 | logger.warning("Authorization failed: Missing required scopes.", context); 55 | throw new McpError( 56 | BaseErrorCode.FORBIDDEN, 57 | `Insufficient permissions. Missing required scopes: ${missingScopes.join(", ")}`, 58 | { requiredScopes, missingScopes }, 59 | ); 60 | } 61 | } 62 | ``` -------------------------------------------------------------------------------- /src/services/obsidianRestAPI/methods/activeFileMethods.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @module ActiveFileMethods 3 | * @description 4 | * Methods for interacting with the currently active file in Obsidian via the REST API. 5 | */ 6 | 7 | import { RequestContext } from "../../../utils/index.js"; 8 | import { NoteJson, RequestFunction } from "../types.js"; 9 | 10 | /** 11 | * Gets the content of the currently active file in Obsidian. 12 | * @param _request - The internal request function from the service instance. 13 | * @param format - 'markdown' or 'json' (for NoteJson). 14 | * @param context - Request context. 15 | * @returns The file content (string) or NoteJson object. 16 | */ 17 | export async function getActiveFile( 18 | _request: RequestFunction, 19 | format: "markdown" | "json" = "markdown", 20 | context: RequestContext, 21 | ): Promise<string | NoteJson> { 22 | const acceptHeader = 23 | format === "json" ? "application/vnd.olrapi.note+json" : "text/markdown"; 24 | return _request<string | NoteJson>( 25 | { 26 | method: "GET", 27 | url: `/active/`, 28 | headers: { Accept: acceptHeader }, 29 | }, 30 | context, 31 | "getActiveFile", 32 | ); 33 | } 34 | 35 | /** 36 | * Updates (overwrites) the content of the currently active file. 37 | * @param _request - The internal request function from the service instance. 38 | * @param content - The new content. 39 | * @param context - Request context. 40 | * @returns {Promise<void>} Resolves on success (204 No Content). 41 | */ 42 | export async function updateActiveFile( 43 | _request: RequestFunction, 44 | content: string, 45 | context: RequestContext, 46 | ): Promise<void> { 47 | await _request<void>( 48 | { 49 | method: "PUT", 50 | url: `/active/`, 51 | headers: { "Content-Type": "text/markdown" }, 52 | data: content, 53 | }, 54 | context, 55 | "updateActiveFile", 56 | ); 57 | } 58 | 59 | /** 60 | * Appends content to the end of the currently active file. 61 | * @param _request - The internal request function from the service instance. 62 | * @param content - The content to append. 63 | * @param context - Request context. 64 | * @returns {Promise<void>} Resolves on success (204 No Content). 65 | */ 66 | export async function appendActiveFile( 67 | _request: RequestFunction, 68 | content: string, 69 | context: RequestContext, 70 | ): Promise<void> { 71 | await _request<void>( 72 | { 73 | method: "POST", 74 | url: `/active/`, 75 | headers: { "Content-Type": "text/markdown" }, 76 | data: content, 77 | }, 78 | context, 79 | "appendActiveFile", 80 | ); 81 | } 82 | 83 | /** 84 | * Deletes the currently active file. 85 | * @param _request - The internal request function from the service instance. 86 | * @param context - Request context. 87 | * @returns {Promise<void>} Resolves on success (204 No Content). 88 | */ 89 | export async function deleteActiveFile( 90 | _request: RequestFunction, 91 | context: RequestContext, 92 | ): Promise<void> { 93 | await _request<void>( 94 | { 95 | method: "DELETE", 96 | url: `/active/`, 97 | }, 98 | context, 99 | "deleteActiveFile", 100 | ); 101 | } 102 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "obsidian-mcp-server", 3 | "version": "2.0.7", 4 | "description": "Obsidian Knowledge-Management MCP (Model Context Protocol) server that enables AI agents and development tools to interact with an Obsidian vault. It provides a comprehensive suite of tools for reading, writing, searching, and managing notes, tags, and frontmatter, acting as a bridge to the Obsidian Local REST API plugin.", 5 | "main": "dist/index.js", 6 | "files": [ 7 | "dist", 8 | "README.md", 9 | "LICENSE", 10 | "CHANGELOG.md" 11 | ], 12 | "bin": { 13 | "obsidian-mcp-server": "dist/index.js" 14 | }, 15 | "type": "module", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/cyanheads/obsidian-mcp-server.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/cyanheads/obsidian-mcp-server/issues" 22 | }, 23 | "homepage": "https://github.com/cyanheads/obsidian-mcp-server#readme", 24 | "scripts": { 25 | "build": "tsc && node --loader ts-node/esm scripts/make-executable.ts dist/index.js", 26 | "start": "node dist/index.js", 27 | "start:stdio": "MCP_LOG_LEVEL=debug MCP_TRANSPORT_TYPE=stdio node dist/index.js", 28 | "start:http": "MCP_LOG_LEVEL=debug MCP_TRANSPORT_TYPE=http node dist/index.js", 29 | "rebuild": "npx ts-node --esm scripts/clean.ts && npm run build", 30 | "fetch:spec": "npx ts-node --esm scripts/fetch-openapi-spec.ts", 31 | "docs:generate": "typedoc --tsconfig ./tsconfig.typedoc.json", 32 | "tree": "npx ts-node --esm scripts/tree.ts", 33 | "format": "prettier --write \"**/*.{ts,js,json,md,html,css}\"", 34 | "inspect": "mcp-inspector --config mcp.json", 35 | "inspect:stdio": "mcp-inspector --config mcp.json --server obsidian-mcp-server-stdio", 36 | "inspect:http": "mcp-inspector --config mcp.json --server obsidian-mcp-server-http" 37 | }, 38 | "dependencies": { 39 | "@hono/node-server": "^1.14.4", 40 | "@modelcontextprotocol/inspector": "^0.14.3", 41 | "@modelcontextprotocol/sdk": "^1.13.0", 42 | "@types/sanitize-html": "^2.16.0", 43 | "@types/validator": "13.15.2", 44 | "axios": "^1.10.0", 45 | "chrono-node": "2.8.0", 46 | "date-fns": "^4.1.0", 47 | "dotenv": "^16.5.0", 48 | "hono": "^4.8.2", 49 | "ignore": "^7.0.5", 50 | "jose": "^6.0.11", 51 | "js-yaml": "^4.1.0", 52 | "openai": "^5.6.0", 53 | "partial-json": "^0.1.7", 54 | "sanitize-html": "^2.17.0", 55 | "tiktoken": "^1.0.21", 56 | "ts-node": "^10.9.2", 57 | "typescript": "^5.8.3", 58 | "validator": "13.15.15", 59 | "winston": "^3.17.0", 60 | "winston-transport": "^4.9.0", 61 | "zod": "^3.25.67" 62 | }, 63 | "keywords": [ 64 | "mcp", 65 | "model-context-protocol", 66 | "obsidian", 67 | "obsidian-md", 68 | "ai", 69 | "llm", 70 | "agent", 71 | "automation", 72 | "api", 73 | "server", 74 | "typescript", 75 | "knowledge-management", 76 | "note-taking", 77 | "rest-api", 78 | "integration" 79 | ], 80 | "author": "Casey Hand @cyanheads", 81 | "license": "Apache-2.0", 82 | "engines": { 83 | "node": ">=16.0.0" 84 | }, 85 | "devDependencies": { 86 | "@types/js-yaml": "^4.0.9", 87 | "@types/node": "^24.0.3", 88 | "prettier": "^3.5.3", 89 | "typedoc": "^0.28.5" 90 | } 91 | } 92 | ``` -------------------------------------------------------------------------------- /src/services/obsidianRestAPI/methods/periodicNoteMethods.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @module PeriodicNoteMethods 3 | * @description 4 | * Methods for interacting with periodic notes (daily, weekly, etc.) via the Obsidian REST API. 5 | */ 6 | 7 | import { RequestContext } from "../../../utils/index.js"; 8 | import { NoteJson, Period, RequestFunction } from "../types.js"; 9 | 10 | /** 11 | * Gets the content of a periodic note (daily, weekly, etc.). 12 | * @param _request - The internal request function from the service instance. 13 | * @param period - The period type ('daily', 'weekly', 'monthly', 'quarterly', 'yearly'). 14 | * @param format - 'markdown' or 'json'. 15 | * @param context - Request context. 16 | * @returns The note content or NoteJson. 17 | */ 18 | export async function getPeriodicNote( 19 | _request: RequestFunction, 20 | period: Period, 21 | format: "markdown" | "json" = "markdown", 22 | context: RequestContext, 23 | ): Promise<string | NoteJson> { 24 | const acceptHeader = 25 | format === "json" ? "application/vnd.olrapi.note+json" : "text/markdown"; 26 | return _request<string | NoteJson>( 27 | { 28 | method: "GET", 29 | url: `/periodic/${period}/`, 30 | headers: { Accept: acceptHeader }, 31 | }, 32 | context, 33 | "getPeriodicNote", 34 | ); 35 | } 36 | 37 | /** 38 | * Updates (overwrites) the content of a periodic note. Creates if needed. 39 | * @param _request - The internal request function from the service instance. 40 | * @param period - The period type. 41 | * @param content - The new content. 42 | * @param context - Request context. 43 | * @returns {Promise<void>} Resolves on success (204 No Content). 44 | */ 45 | export async function updatePeriodicNote( 46 | _request: RequestFunction, 47 | period: Period, 48 | content: string, 49 | context: RequestContext, 50 | ): Promise<void> { 51 | await _request<void>( 52 | { 53 | method: "PUT", 54 | url: `/periodic/${period}/`, 55 | headers: { "Content-Type": "text/markdown" }, 56 | data: content, 57 | }, 58 | context, 59 | "updatePeriodicNote", 60 | ); 61 | } 62 | 63 | /** 64 | * Appends content to a periodic note. Creates if needed. 65 | * @param _request - The internal request function from the service instance. 66 | * @param period - The period type. 67 | * @param content - The content to append. 68 | * @param context - Request context. 69 | * @returns {Promise<void>} Resolves on success (204 No Content). 70 | */ 71 | export async function appendPeriodicNote( 72 | _request: RequestFunction, 73 | period: Period, 74 | content: string, 75 | context: RequestContext, 76 | ): Promise<void> { 77 | await _request<void>( 78 | { 79 | method: "POST", 80 | url: `/periodic/${period}/`, 81 | headers: { "Content-Type": "text/markdown" }, 82 | data: content, 83 | }, 84 | context, 85 | "appendPeriodicNote", 86 | ); 87 | } 88 | 89 | /** 90 | * Deletes a periodic note. 91 | * @param _request - The internal request function from the service instance. 92 | * @param period - The period type. 93 | * @param context - Request context. 94 | * @returns {Promise<void>} Resolves on success (204 No Content). 95 | */ 96 | export async function deletePeriodicNote( 97 | _request: RequestFunction, 98 | period: Period, 99 | context: RequestContext, 100 | ): Promise<void> { 101 | await _request<void>( 102 | { 103 | method: "DELETE", 104 | url: `/periodic/${period}/`, 105 | }, 106 | context, 107 | "deletePeriodicNote", 108 | ); 109 | } 110 | ``` -------------------------------------------------------------------------------- /scripts/clean.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * @fileoverview Utility script to clean build artifacts and temporary directories. 5 | * @module scripts/clean 6 | * By default, it removes the 'dist' and 'logs' directories. 7 | * Custom directories can be specified as command-line arguments. 8 | * Works on all platforms using Node.js path normalization. 9 | * 10 | * @example 11 | * // Add to package.json: 12 | * // "scripts": { 13 | * // "clean": "ts-node --esm scripts/clean.ts", 14 | * // "rebuild": "npm run clean && npm run build" 15 | * // } 16 | * 17 | * // Run with default directories: 18 | * // npm run clean 19 | * 20 | * // Run with custom directories: 21 | * // ts-node --esm scripts/clean.ts temp coverage 22 | */ 23 | 24 | import { rm, access } from "fs/promises"; 25 | import { join } from "path"; 26 | 27 | /** 28 | * Represents the result of a clean operation for a single directory. 29 | * @property dir - The name of the directory targeted for cleaning. 30 | * @property status - Indicates if the cleaning was successful or skipped. 31 | * @property reason - If skipped, the reason why. 32 | */ 33 | interface CleanResult { 34 | dir: string; 35 | status: "success" | "skipped"; 36 | reason?: string; 37 | } 38 | 39 | /** 40 | * Asynchronously checks if a directory exists at the given path. 41 | * @param dirPath - The absolute or relative path to the directory. 42 | * @returns A promise that resolves to `true` if the directory exists, `false` otherwise. 43 | */ 44 | async function directoryExists(dirPath: string): Promise<boolean> { 45 | try { 46 | await access(dirPath); 47 | return true; 48 | } catch { 49 | return false; 50 | } 51 | } 52 | 53 | /** 54 | * Main function to perform the cleaning operation. 55 | * It reads command line arguments for target directories or uses defaults ('dist', 'logs'). 56 | * Reports the status of each cleaning attempt. 57 | */ 58 | const clean = async (): Promise<void> => { 59 | try { 60 | let dirsToClean: string[] = ["dist", "logs"]; 61 | const args = process.argv.slice(2); 62 | 63 | if (args.length > 0) { 64 | dirsToClean = args; 65 | } 66 | 67 | console.log(`Attempting to clean directories: ${dirsToClean.join(", ")}`); 68 | 69 | const results = await Promise.allSettled( 70 | dirsToClean.map(async (dir): Promise<CleanResult> => { 71 | const dirPath = join(process.cwd(), dir); 72 | 73 | try { 74 | const exists = await directoryExists(dirPath); 75 | 76 | if (!exists) { 77 | return { dir, status: "skipped", reason: "does not exist" }; 78 | } 79 | 80 | await rm(dirPath, { recursive: true, force: true }); 81 | return { dir, status: "success" }; 82 | } catch (error) { 83 | // Rethrow to be caught by Promise.allSettled's rejection case 84 | throw error; 85 | } 86 | }), 87 | ); 88 | 89 | results.forEach((result) => { 90 | if (result.status === "fulfilled") { 91 | const { dir, status, reason } = result.value; 92 | if (status === "success") { 93 | console.log(`Successfully cleaned directory: ${dir}`); 94 | } else { 95 | console.log(`Skipped cleaning directory ${dir}: ${reason}.`); 96 | } 97 | } else { 98 | // The error here is the actual error object from the rejected promise 99 | console.error( 100 | `Error cleaning a directory (details below):\n`, 101 | result.reason, 102 | ); 103 | } 104 | }); 105 | } catch (error) { 106 | console.error( 107 | "An unexpected error occurred during the clean script execution:", 108 | error instanceof Error ? error.message : error, 109 | ); 110 | process.exit(1); 111 | } 112 | }; 113 | 114 | clean(); 115 | ``` -------------------------------------------------------------------------------- /src/mcp-server/transports/stdioTransport.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Handles the setup and connection for the Stdio MCP transport. 3 | * Implements the MCP Specification 2025-03-26 for stdio transport. 4 | * This transport communicates directly over standard input (stdin) and 5 | * standard output (stdout), typically used when the MCP server is launched 6 | * as a child process by a host application. 7 | * 8 | * Specification Reference: 9 | * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#stdio 10 | * 11 | * --- Authentication Note --- 12 | * As per the MCP Authorization Specification (2025-03-26, Section 1.2), 13 | * STDIO transports SHOULD NOT implement HTTP-based authentication flows. 14 | * Authorization is typically handled implicitly by the host application 15 | * controlling the server process. This implementation follows that guideline. 16 | * 17 | * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification} 18 | * @module src/mcp-server/transports/stdioTransport 19 | */ 20 | 21 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 22 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 23 | import { ErrorHandler, logger, RequestContext } from "../../utils/index.js"; 24 | 25 | /** 26 | * Connects a given `McpServer` instance to the Stdio transport. 27 | * This function initializes the SDK's `StdioServerTransport`, which manages 28 | * communication over `process.stdin` and `process.stdout` according to the 29 | * MCP stdio transport specification. 30 | * 31 | * MCP Spec Points Covered by SDK's `StdioServerTransport`: 32 | * - Reads JSON-RPC messages (requests, notifications, responses, batches) from stdin. 33 | * - Writes JSON-RPC messages to stdout. 34 | * - Handles newline delimiters and ensures no embedded newlines in output messages. 35 | * - Ensures only valid MCP messages are written to stdout. 36 | * 37 | * Logging via the `logger` utility MAY result in output to stderr, which is 38 | * permitted by the spec for logging purposes. 39 | * 40 | * @param server - The `McpServer` instance. 41 | * @param parentContext - The logging and tracing context from the calling function. 42 | * @returns A promise that resolves when the Stdio transport is successfully connected. 43 | * @throws {Error} If the connection fails during setup. 44 | */ 45 | export async function connectStdioTransport( 46 | server: McpServer, 47 | parentContext: RequestContext, 48 | ): Promise<void> { 49 | const operationContext = { 50 | ...parentContext, 51 | operation: "connectStdioTransport", 52 | transportType: "Stdio", 53 | }; 54 | logger.debug("Attempting to connect stdio transport...", operationContext); 55 | 56 | try { 57 | logger.debug("Creating StdioServerTransport instance...", operationContext); 58 | const transport = new StdioServerTransport(); 59 | 60 | logger.debug( 61 | "Connecting McpServer instance to StdioServerTransport...", 62 | operationContext, 63 | ); 64 | await server.connect(transport); 65 | 66 | logger.info( 67 | "MCP Server connected and listening via stdio transport.", 68 | operationContext, 69 | ); 70 | if (process.stdout.isTTY) { 71 | console.log( 72 | `\n🚀 MCP Server running in STDIO mode.\n (MCP Spec: 2025-03-26 Stdio Transport)\n`, 73 | ); 74 | } 75 | } catch (err) { 76 | ErrorHandler.handleError(err, { ...operationContext, critical: true }); 77 | throw err; // Re-throw after handling to allow caller to react if necessary 78 | } 79 | } 80 | ``` -------------------------------------------------------------------------------- /src/types-global/errors.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from "zod"; 2 | 3 | /** 4 | * Defines a set of standardized error codes for common issues within MCP servers or tools. 5 | * These codes help clients understand the nature of an error programmatically. 6 | */ 7 | export enum BaseErrorCode { 8 | /** Access denied due to invalid credentials or lack of authentication. */ 9 | UNAUTHORIZED = "UNAUTHORIZED", 10 | /** Access denied despite valid authentication, due to insufficient permissions. */ 11 | FORBIDDEN = "FORBIDDEN", 12 | /** The requested resource or entity could not be found. */ 13 | NOT_FOUND = "NOT_FOUND", 14 | /** The request could not be completed due to a conflict with the current state of the resource. */ 15 | CONFLICT = "CONFLICT", 16 | /** The request failed due to invalid input parameters or data. */ 17 | VALIDATION_ERROR = "VALIDATION_ERROR", 18 | /** An error occurred while parsing input data (e.g., date string, JSON). */ 19 | PARSING_ERROR = "PARSING_ERROR", 20 | /** The request was rejected because the client has exceeded rate limits. */ 21 | RATE_LIMITED = "RATE_LIMITED", 22 | /** The request timed out before a response could be generated. */ 23 | TIMEOUT = "TIMEOUT", 24 | /** The service is temporarily unavailable, possibly due to maintenance or overload. */ 25 | SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE", 26 | /** An unexpected error occurred on the server side. */ 27 | INTERNAL_ERROR = "INTERNAL_ERROR", 28 | /** An error occurred, but the specific cause is unknown or cannot be categorized. */ 29 | UNKNOWN_ERROR = "UNKNOWN_ERROR", 30 | /** An error occurred during the loading or validation of configuration data. */ 31 | CONFIGURATION_ERROR = "CONFIGURATION_ERROR", 32 | } 33 | 34 | /** 35 | * Custom error class for MCP-specific errors. 36 | * Encapsulates a `BaseErrorCode`, a descriptive message, and optional details. 37 | * Provides a method to format the error into a standard MCP tool response. 38 | */ 39 | export class McpError extends Error { 40 | /** 41 | * Creates an instance of McpError. 42 | * @param {BaseErrorCode} code - The standardized error code. 43 | * @param {string} message - A human-readable description of the error. 44 | * @param {Record<string, unknown>} [details] - Optional additional details about the error. 45 | */ 46 | constructor( 47 | public code: BaseErrorCode, 48 | message: string, 49 | public details?: Record<string, unknown>, 50 | ) { 51 | super(message); 52 | // Set the error name for identification 53 | this.name = "McpError"; 54 | // Ensure the prototype chain is correct 55 | Object.setPrototypeOf(this, McpError.prototype); 56 | } 57 | 58 | // Removed toResponse() method. The SDK should handle formatting errors into JSON-RPC responses. 59 | } 60 | 61 | /** 62 | * Zod schema for validating error objects, potentially used for parsing 63 | * error responses or validating error structures internally. 64 | */ 65 | export const ErrorSchema = z 66 | .object({ 67 | /** The error code, corresponding to BaseErrorCode enum values. */ 68 | code: z.nativeEnum(BaseErrorCode).describe("Standardized error code"), 69 | /** A human-readable description of the error. */ 70 | message: z.string().describe("Detailed error message"), 71 | /** Optional additional details or context about the error. */ 72 | details: z 73 | .record(z.unknown()) 74 | .optional() 75 | .describe("Optional structured error details"), 76 | }) 77 | .describe("Schema for validating structured error objects."); 78 | 79 | /** 80 | * TypeScript type inferred from `ErrorSchema`. 81 | * Represents a validated error object structure. 82 | * @typedef {z.infer<typeof ErrorSchema>} ErrorResponse 83 | */ 84 | export type ErrorResponse = z.infer<typeof ErrorSchema>; 85 | ``` -------------------------------------------------------------------------------- /src/mcp-server/tools/obsidianManageTagsTool/registration.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { 3 | ObsidianRestApiService, 4 | VaultCacheService, 5 | } from "../../../services/obsidianRestAPI/index.js"; 6 | import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; 7 | import { 8 | ErrorHandler, 9 | logger, 10 | RequestContext, 11 | requestContextService, 12 | } from "../../../utils/index.js"; 13 | import type { 14 | ObsidianManageTagsInput, 15 | ObsidianManageTagsResponse, 16 | } from "./logic.js"; 17 | import { 18 | ManageTagsInputSchema, 19 | ObsidianManageTagsInputSchemaShape, 20 | processObsidianManageTags, 21 | } from "./logic.js"; 22 | 23 | export const registerObsidianManageTagsTool = async ( 24 | server: McpServer, 25 | obsidianService: ObsidianRestApiService, 26 | vaultCacheService: VaultCacheService | undefined, 27 | ): Promise<void> => { 28 | const toolName = "obsidian_manage_tags"; 29 | const toolDescription = 30 | "Manages tags for a specified note, handling them in both the YAML frontmatter and inline content. Supports adding, removing, and listing tags to provide a comprehensive tag management solution."; 31 | 32 | const registrationContext: RequestContext = 33 | requestContextService.createRequestContext({ 34 | operation: "RegisterObsidianManageTagsTool", 35 | toolName: toolName, 36 | module: "ObsidianManageTagsRegistration", 37 | }); 38 | 39 | logger.info(`Attempting to register tool: ${toolName}`, registrationContext); 40 | 41 | await ErrorHandler.tryCatch( 42 | async () => { 43 | server.tool( 44 | toolName, 45 | toolDescription, 46 | ObsidianManageTagsInputSchemaShape, 47 | async (params: ObsidianManageTagsInput) => { 48 | const handlerContext: RequestContext = 49 | requestContextService.createRequestContext({ 50 | parentContext: registrationContext, 51 | operation: "HandleObsidianManageTagsRequest", 52 | toolName: toolName, 53 | params: params, 54 | }); 55 | logger.debug(`Handling '${toolName}' request`, handlerContext); 56 | 57 | return await ErrorHandler.tryCatch( 58 | async () => { 59 | const validatedParams = ManageTagsInputSchema.parse(params); 60 | 61 | const response: ObsidianManageTagsResponse = 62 | await processObsidianManageTags( 63 | validatedParams, 64 | handlerContext, 65 | obsidianService, 66 | vaultCacheService, 67 | ); 68 | logger.debug( 69 | `'${toolName}' processed successfully`, 70 | handlerContext, 71 | ); 72 | 73 | return { 74 | content: [ 75 | { 76 | type: "text", 77 | text: JSON.stringify(response, null, 2), 78 | }, 79 | ], 80 | isError: false, 81 | }; 82 | }, 83 | { 84 | operation: `processing ${toolName} handler`, 85 | context: handlerContext, 86 | input: params, 87 | errorMapper: (error: unknown) => 88 | new McpError( 89 | error instanceof McpError 90 | ? error.code 91 | : BaseErrorCode.INTERNAL_ERROR, 92 | `Error processing ${toolName} tool: ${error instanceof Error ? error.message : "Unknown error"}`, 93 | { ...handlerContext }, 94 | ), 95 | }, 96 | ); 97 | }, 98 | ); 99 | 100 | logger.info( 101 | `Tool registered successfully: ${toolName}`, 102 | registrationContext, 103 | ); 104 | }, 105 | { 106 | operation: `registering tool ${toolName}`, 107 | context: registrationContext, 108 | errorCode: BaseErrorCode.INTERNAL_ERROR, 109 | errorMapper: (error: unknown) => 110 | new McpError( 111 | error instanceof McpError ? error.code : BaseErrorCode.INTERNAL_ERROR, 112 | `Failed to register tool '${toolName}': ${error instanceof Error ? error.message : "Unknown error"}`, 113 | { ...registrationContext }, 114 | ), 115 | critical: true, 116 | }, 117 | ); 118 | }; 119 | ``` -------------------------------------------------------------------------------- /src/utils/internal/requestContext.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Utilities for creating and managing request contexts. 3 | * A request context is an object carrying a unique ID, timestamp, and other 4 | * relevant data for logging, tracing, and processing. It also defines 5 | * configuration and operational context structures. 6 | * @module src/utils/internal/requestContext 7 | */ 8 | 9 | import { generateUUID } from "../index.js"; 10 | import { logger } from "./logger.js"; 11 | 12 | /** 13 | * Defines the core structure for context information associated with a request or operation. 14 | * This is fundamental for logging, tracing, and passing operational data. 15 | */ 16 | export interface RequestContext { 17 | /** 18 | * Unique ID for the context instance. 19 | * Used for log correlation and request tracing. 20 | */ 21 | requestId: string; 22 | 23 | /** 24 | * ISO 8601 timestamp indicating when the context was created. 25 | */ 26 | timestamp: string; 27 | 28 | /** 29 | * Allows arbitrary key-value pairs for specific context needs. 30 | * Using `unknown` promotes type-safe access. 31 | * Consumers must type-check/assert when accessing extended properties. 32 | */ 33 | [key: string]: unknown; 34 | } 35 | 36 | /** 37 | * Configuration for the {@link requestContextService}. 38 | * Allows for future extensibility of service-wide settings. 39 | */ 40 | export interface ContextConfig { 41 | /** Custom configuration properties. Allows for arbitrary key-value pairs. */ 42 | [key: string]: unknown; 43 | } 44 | 45 | /** 46 | * Represents a broader context for a specific operation or task. 47 | * It can optionally include a base {@link RequestContext} and other custom properties 48 | * relevant to the operation. 49 | */ 50 | export interface OperationContext { 51 | /** Optional base request context data, adhering to the `RequestContext` structure. */ 52 | requestContext?: RequestContext; 53 | 54 | /** Allows for additional, custom properties specific to the operation. */ 55 | [key: string]: unknown; 56 | } 57 | 58 | /** 59 | * Singleton-like service object for managing request context operations. 60 | * @private 61 | */ 62 | const requestContextServiceInstance = { 63 | /** 64 | * Internal configuration store for the service. 65 | */ 66 | config: {} as ContextConfig, 67 | 68 | /** 69 | * Configures the request context service with new settings. 70 | * Merges the provided partial configuration with existing settings. 71 | * 72 | * @param config - A partial `ContextConfig` object containing settings to update or add. 73 | * @returns A shallow copy of the newly updated configuration. 74 | */ 75 | configure(config: Partial<ContextConfig>): ContextConfig { 76 | this.config = { 77 | ...this.config, 78 | ...config, 79 | }; 80 | const logContext = this.createRequestContext({ 81 | operation: "RequestContextService.configure", 82 | newConfigState: { ...this.config }, 83 | }); 84 | logger.debug("RequestContextService configuration updated", logContext); 85 | return { ...this.config }; 86 | }, 87 | 88 | /** 89 | * Retrieves a shallow copy of the current service configuration. 90 | * This prevents direct mutation of the internal configuration state. 91 | * 92 | * @returns A shallow copy of the current `ContextConfig`. 93 | */ 94 | getConfig(): ContextConfig { 95 | return { ...this.config }; 96 | }, 97 | 98 | /** 99 | * Creates a new {@link RequestContext} instance. 100 | * Each context is assigned a unique `requestId` (UUID) and a current `timestamp` (ISO 8601). 101 | * Additional custom properties can be merged into the context. 102 | * 103 | * @param additionalContext - An optional record of key-value pairs to be 104 | * included in the created request context. 105 | * @returns A new `RequestContext` object. 106 | */ 107 | createRequestContext( 108 | additionalContext: Record<string, unknown> = {}, 109 | ): RequestContext { 110 | const requestId = generateUUID(); 111 | const timestamp = new Date().toISOString(); 112 | 113 | const context: RequestContext = { 114 | requestId, 115 | timestamp, 116 | ...additionalContext, 117 | }; 118 | return context; 119 | }, 120 | }; 121 | 122 | /** 123 | * Primary export for request context functionalities. 124 | * This service provides methods to create and manage {@link RequestContext} instances, 125 | * which are essential for logging, tracing, and correlating operations. 126 | */ 127 | export const requestContextService = requestContextServiceInstance; 128 | ``` -------------------------------------------------------------------------------- /src/mcp-server/tools/obsidianManageFrontmatterTool/registration.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { 3 | ObsidianRestApiService, 4 | VaultCacheService, 5 | } from "../../../services/obsidianRestAPI/index.js"; 6 | import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; 7 | import { 8 | ErrorHandler, 9 | logger, 10 | RequestContext, 11 | requestContextService, 12 | } from "../../../utils/index.js"; 13 | import type { 14 | ObsidianManageFrontmatterInput, 15 | ObsidianManageFrontmatterResponse, 16 | } from "./logic.js"; 17 | import { 18 | ManageFrontmatterInputSchema, 19 | ObsidianManageFrontmatterInputSchemaShape, 20 | processObsidianManageFrontmatter, 21 | } from "./logic.js"; 22 | 23 | export const registerObsidianManageFrontmatterTool = async ( 24 | server: McpServer, 25 | obsidianService: ObsidianRestApiService, 26 | vaultCacheService: VaultCacheService | undefined, 27 | ): Promise<void> => { 28 | const toolName = "obsidian_manage_frontmatter"; 29 | const toolDescription = 30 | "Atomically manages a note's YAML frontmatter. Supports getting, setting (creating/updating), and deleting specific keys without rewriting the entire file. Ideal for efficient metadata operations on primitive or structured Obsidian frontmatter data."; 31 | 32 | const registrationContext: RequestContext = 33 | requestContextService.createRequestContext({ 34 | operation: "RegisterObsidianManageFrontmatterTool", 35 | toolName: toolName, 36 | module: "ObsidianManageFrontmatterRegistration", 37 | }); 38 | 39 | logger.info(`Attempting to register tool: ${toolName}`, registrationContext); 40 | 41 | await ErrorHandler.tryCatch( 42 | async () => { 43 | server.tool( 44 | toolName, 45 | toolDescription, 46 | ObsidianManageFrontmatterInputSchemaShape, 47 | async (params: ObsidianManageFrontmatterInput) => { 48 | const handlerContext: RequestContext = 49 | requestContextService.createRequestContext({ 50 | parentContext: registrationContext, 51 | operation: "HandleObsidianManageFrontmatterRequest", 52 | toolName: toolName, 53 | params: params, 54 | }); 55 | logger.debug(`Handling '${toolName}' request`, handlerContext); 56 | 57 | return await ErrorHandler.tryCatch( 58 | async () => { 59 | const validatedParams = 60 | ManageFrontmatterInputSchema.parse(params); 61 | 62 | const response: ObsidianManageFrontmatterResponse = 63 | await processObsidianManageFrontmatter( 64 | validatedParams, 65 | handlerContext, 66 | obsidianService, 67 | vaultCacheService, 68 | ); 69 | logger.debug( 70 | `'${toolName}' processed successfully`, 71 | handlerContext, 72 | ); 73 | 74 | return { 75 | content: [ 76 | { 77 | type: "text", 78 | text: JSON.stringify(response, null, 2), 79 | }, 80 | ], 81 | isError: false, 82 | }; 83 | }, 84 | { 85 | operation: `processing ${toolName} handler`, 86 | context: handlerContext, 87 | input: params, 88 | errorMapper: (error: unknown) => 89 | new McpError( 90 | error instanceof McpError 91 | ? error.code 92 | : BaseErrorCode.INTERNAL_ERROR, 93 | `Error processing ${toolName} tool: ${error instanceof Error ? error.message : "Unknown error"}`, 94 | { ...handlerContext }, 95 | ), 96 | }, 97 | ); 98 | }, 99 | ); 100 | 101 | logger.info( 102 | `Tool registered successfully: ${toolName}`, 103 | registrationContext, 104 | ); 105 | }, 106 | { 107 | operation: `registering tool ${toolName}`, 108 | context: registrationContext, 109 | errorCode: BaseErrorCode.INTERNAL_ERROR, 110 | errorMapper: (error: unknown) => 111 | new McpError( 112 | error instanceof McpError ? error.code : BaseErrorCode.INTERNAL_ERROR, 113 | `Failed to register tool '${toolName}': ${error instanceof Error ? error.message : "Unknown error"}`, 114 | { ...registrationContext }, 115 | ), 116 | critical: true, 117 | }, 118 | ); 119 | }; 120 | ``` -------------------------------------------------------------------------------- /src/services/obsidianRestAPI/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @module ObsidianRestApiTypes 3 | * @description 4 | * Type definitions for interacting with the Obsidian Local REST API, 5 | * based on its OpenAPI specification. 6 | */ 7 | 8 | import { AxiosRequestConfig } from "axios"; 9 | import { RequestContext } from "../../utils/index.js"; 10 | 11 | /** 12 | * Defines the signature for the internal request function passed to method implementations. 13 | * This function is bound to the `ObsidianRestApiService` instance and handles the core 14 | * logic of making an HTTP request, including authentication, error handling, and logging. 15 | * 16 | * @template T The expected return type of the API call. 17 | * @param config The Axios request configuration. 18 | * @param context The request context for logging and correlation. 19 | * @param operationName A descriptive name for the operation being performed, used for logging. 20 | * @returns A promise that resolves with the data of type `T`. 21 | */ 22 | export type RequestFunction = <T = any>( 23 | config: AxiosRequestConfig, 24 | context: RequestContext, 25 | operationName: string, 26 | ) => Promise<T>; 27 | 28 | /** 29 | * Filesystem metadata for a note. 30 | */ 31 | export interface NoteStat { 32 | ctime: number; // Creation time (Unix timestamp) 33 | mtime: number; // Modification time (Unix timestamp) 34 | size: number; // File size in bytes 35 | } 36 | 37 | /** 38 | * JSON representation of an Obsidian note. 39 | * Returned when requesting with Accept: application/vnd.olrapi.note+json 40 | */ 41 | export interface NoteJson { 42 | content: string; 43 | frontmatter: Record<string, any>; // Parsed YAML frontmatter 44 | path: string; // Vault-relative path 45 | stat: NoteStat; 46 | tags: string[]; // Tags found in the note (including frontmatter) 47 | } 48 | 49 | /** 50 | * Response structure for listing files in a directory. 51 | */ 52 | export interface FileListResponse { 53 | files: string[]; // List of file/directory names (directories end with '/') 54 | } 55 | 56 | /** 57 | * Match details within a simple search result. 58 | */ 59 | export interface SimpleSearchMatchDetail { 60 | start: number; // Start index of the match 61 | end: number; // End index of the match 62 | } 63 | 64 | /** 65 | * Contextual match information for simple search. 66 | */ 67 | export interface SimpleSearchMatch { 68 | context: string; // Text surrounding the match 69 | match: SimpleSearchMatchDetail; 70 | } 71 | 72 | /** 73 | * Result item for a simple text search. 74 | */ 75 | export interface SimpleSearchResult { 76 | filename: string; // Path to the matching file 77 | matches: SimpleSearchMatch[]; 78 | score: number; // Relevance score 79 | } 80 | 81 | /** 82 | * Result item for a complex (Dataview/JsonLogic) search. 83 | */ 84 | export interface ComplexSearchResult { 85 | filename: string; // Path to the matching file 86 | result: any; // The result returned by the query logic for this file 87 | } 88 | 89 | /** 90 | * Structure for an available Obsidian command. 91 | */ 92 | export interface ObsidianCommand { 93 | id: string; 94 | name: string; 95 | } 96 | 97 | /** 98 | * Response structure for listing available commands. 99 | */ 100 | export interface CommandListResponse { 101 | commands: ObsidianCommand[]; 102 | } 103 | 104 | /** 105 | * Basic status response from the API root. 106 | */ 107 | export interface ApiStatusResponse { 108 | authenticated: boolean; 109 | ok: string; // Should be "OK" 110 | service: string; // Should be "Obsidian Local REST API" 111 | versions: { 112 | obsidian: string; // Obsidian API version 113 | self: string; // Plugin version 114 | }; 115 | } 116 | 117 | /** 118 | * Standard error response structure from the API. 119 | */ 120 | export interface ApiError { 121 | errorCode: number; // e.g., 40149 122 | message: string; // e.g., "A brief description of the error." 123 | } 124 | 125 | /** 126 | * Options for PATCH operations. 127 | */ 128 | export interface PatchOptions { 129 | operation: "append" | "prepend" | "replace"; 130 | targetType: "heading" | "block" | "frontmatter"; 131 | target: string; // The specific heading, block ID, or frontmatter key 132 | targetDelimiter?: string; // Default '::' for nested headings 133 | trimTargetWhitespace?: boolean; // Default false 134 | /** 135 | * If true, creates the target if it's missing. 136 | * This is implemented via the `Create-Target-If-Missing` HTTP header. 137 | * Particularly useful for adding new frontmatter keys. 138 | */ 139 | createTargetIfMissing?: boolean; 140 | contentType?: "text/markdown" | "application/json"; // For request body type inference 141 | } 142 | 143 | /** 144 | * Type alias for periodic note periods. 145 | */ 146 | export type Period = "daily" | "weekly" | "monthly" | "quarterly" | "yearly"; 147 | ``` -------------------------------------------------------------------------------- /src/services/obsidianRestAPI/methods/patchMethods.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @module PatchMethods 3 | * @description 4 | * Methods for performing granular PATCH operations within notes via the Obsidian REST API. 5 | */ 6 | 7 | import { RequestContext } from "../../../utils/index.js"; 8 | import { PatchOptions, Period, RequestFunction } from "../types.js"; 9 | import { encodeVaultPath } from "../../../utils/obsidian/obsidianApiUtils.js"; 10 | 11 | /** 12 | * Helper to construct headers for PATCH requests. 13 | */ 14 | function buildPatchHeaders(options: PatchOptions): Record<string, string> { 15 | const headers: Record<string, string> = { 16 | Operation: options.operation, 17 | "Target-Type": options.targetType, 18 | // Spec requires URL encoding for non-ASCII characters in Target header 19 | Target: encodeURIComponent(options.target), 20 | }; 21 | if (options.targetDelimiter) { 22 | headers["Target-Delimiter"] = options.targetDelimiter; 23 | } 24 | if (options.trimTargetWhitespace !== undefined) { 25 | headers["Trim-Target-Whitespace"] = String(options.trimTargetWhitespace); 26 | } 27 | // Add Create-Target-If-Missing header if provided in options 28 | if (options.createTargetIfMissing !== undefined) { 29 | headers["Create-Target-If-Missing"] = String(options.createTargetIfMissing); 30 | } 31 | if (options.contentType) { 32 | headers["Content-Type"] = options.contentType; 33 | } else { 34 | // Default to markdown if not specified, especially for non-JSON content 35 | headers["Content-Type"] = "text/markdown"; 36 | } 37 | return headers; 38 | } 39 | 40 | /** 41 | * Patches a specific file in the vault. 42 | * @param _request - The internal request function from the service instance. 43 | * @param filePath - Vault-relative path to the file. 44 | * @param content - The content to insert/replace (string or JSON for tables/frontmatter). 45 | * @param options - Patch operation details (operation, targetType, target, etc.). 46 | * @param context - Request context. 47 | * @returns {Promise<void>} Resolves on success (200 OK). 48 | */ 49 | export async function patchFile( 50 | _request: RequestFunction, 51 | filePath: string, 52 | content: string | object, // Allow object for JSON content type 53 | options: PatchOptions, 54 | context: RequestContext, 55 | ): Promise<void> { 56 | const headers = buildPatchHeaders(options); 57 | const requestData = 58 | typeof content === "object" ? JSON.stringify(content) : content; 59 | const encodedPath = encodeVaultPath(filePath); 60 | 61 | // PATCH returns 200 OK according to spec 62 | await _request<void>( 63 | { 64 | method: "PATCH", 65 | url: `/vault${encodedPath}`, // Use the encoded path 66 | headers: headers, 67 | data: requestData, 68 | }, 69 | context, 70 | "patchFile", 71 | ); 72 | } 73 | 74 | /** 75 | * Patches the currently active file in Obsidian. 76 | * @param _request - The internal request function from the service instance. 77 | * @param content - The content to insert/replace. 78 | * @param options - Patch operation details. 79 | * @param context - Request context. 80 | * @returns {Promise<void>} Resolves on success (200 OK). 81 | */ 82 | export async function patchActiveFile( 83 | _request: RequestFunction, 84 | content: string | object, 85 | options: PatchOptions, 86 | context: RequestContext, 87 | ): Promise<void> { 88 | const headers = buildPatchHeaders(options); 89 | const requestData = 90 | typeof content === "object" ? JSON.stringify(content) : content; 91 | 92 | await _request<void>( 93 | { 94 | method: "PATCH", 95 | url: `/active/`, 96 | headers: headers, 97 | data: requestData, 98 | }, 99 | context, 100 | "patchActiveFile", 101 | ); 102 | } 103 | 104 | /** 105 | * Patches a periodic note. 106 | * @param _request - The internal request function from the service instance. 107 | * @param period - The period type ('daily', 'weekly', etc.). 108 | * @param content - The content to insert/replace. 109 | * @param options - Patch operation details. 110 | * @param context - Request context. 111 | * @returns {Promise<void>} Resolves on success (200 OK). 112 | */ 113 | export async function patchPeriodicNote( 114 | _request: RequestFunction, 115 | period: Period, 116 | content: string | object, 117 | options: PatchOptions, 118 | context: RequestContext, 119 | ): Promise<void> { 120 | const headers = buildPatchHeaders(options); 121 | const requestData = 122 | typeof content === "object" ? JSON.stringify(content) : content; 123 | 124 | await _request<void>( 125 | { 126 | method: "PATCH", 127 | url: `/periodic/${period}/`, 128 | headers: headers, 129 | data: requestData, 130 | }, 131 | context, 132 | "patchPeriodicNote", 133 | ); 134 | } 135 | ``` -------------------------------------------------------------------------------- /scripts/make-executable.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * @fileoverview Utility script to make files executable (chmod +x) on Unix-like systems. 5 | * @module scripts/make-executable 6 | * On Windows, this script does nothing but exits successfully. 7 | * Useful for CLI applications where built output needs executable permissions. 8 | * Default target (if no args): dist/index.js. 9 | * Ensures output paths are within the project directory for security. 10 | * 11 | * @example 12 | * // Add to package.json build script: 13 | * // "build": "tsc && ts-node --esm scripts/make-executable.ts dist/index.js" 14 | * 15 | * @example 16 | * // Run directly with custom files: 17 | * // ts-node --esm scripts/make-executable.ts path/to/script1 path/to/script2 18 | */ 19 | 20 | import fs from "fs/promises"; 21 | import os from "os"; 22 | import path from "path"; 23 | 24 | const isUnix = os.platform() !== "win32"; 25 | const projectRoot = process.cwd(); 26 | const EXECUTABLE_MODE = 0o755; // rwxr-xr-x 27 | 28 | /** 29 | * Represents the result of an attempt to make a file executable. 30 | * @property file - The relative path of the file targeted. 31 | * @property status - The outcome of the operation ('success', 'error', or 'skipped'). 32 | * @property reason - If status is 'error' or 'skipped', an explanation. 33 | */ 34 | interface ExecutableResult { 35 | file: string; 36 | status: "success" | "error" | "skipped"; 37 | reason?: string; 38 | } 39 | 40 | /** 41 | * Main function to make specified files executable. 42 | * Skips operation on Windows. Processes command-line arguments for target files 43 | * or defaults to 'dist/index.js'. Reports status for each file. 44 | */ 45 | const makeExecutable = async (): Promise<void> => { 46 | try { 47 | const targetFiles: string[] = 48 | process.argv.slice(2).length > 0 49 | ? process.argv.slice(2) 50 | : ["dist/index.js"]; 51 | 52 | if (!isUnix) { 53 | console.log( 54 | "Skipping chmod operation: Script is running on Windows (not applicable).", 55 | ); 56 | return; 57 | } 58 | 59 | console.log( 60 | `Attempting to make files executable: ${targetFiles.join(", ")}`, 61 | ); 62 | 63 | const results = await Promise.allSettled( 64 | targetFiles.map(async (targetFile): Promise<ExecutableResult> => { 65 | const normalizedPath = path.resolve(projectRoot, targetFile); 66 | 67 | if ( 68 | !normalizedPath.startsWith(projectRoot + path.sep) && 69 | normalizedPath !== projectRoot 70 | ) { 71 | return { 72 | file: targetFile, 73 | status: "error", 74 | reason: `Path resolves outside project boundary: ${normalizedPath}`, 75 | }; 76 | } 77 | 78 | try { 79 | await fs.access(normalizedPath); // Check if file exists 80 | await fs.chmod(normalizedPath, EXECUTABLE_MODE); 81 | return { file: targetFile, status: "success" }; 82 | } catch (error) { 83 | const err = error as NodeJS.ErrnoException; 84 | if (err.code === "ENOENT") { 85 | return { 86 | file: targetFile, 87 | status: "error", 88 | reason: "File not found", 89 | }; 90 | } 91 | console.error( 92 | `Error setting executable permission for ${targetFile}: ${err.message}`, 93 | ); 94 | return { file: targetFile, status: "error", reason: err.message }; 95 | } 96 | }), 97 | ); 98 | 99 | let hasErrors = false; 100 | results.forEach((result) => { 101 | if (result.status === "fulfilled") { 102 | const { file, status, reason } = result.value; 103 | if (status === "success") { 104 | console.log(`Successfully made executable: ${file}`); 105 | } else if (status === "error") { 106 | console.error(`Error for ${file}: ${reason}`); 107 | hasErrors = true; 108 | } else if (status === "skipped") { 109 | // This status is not currently generated by the mapAsync logic but kept for future flexibility 110 | console.warn(`Skipped ${file}: ${reason}`); 111 | } 112 | } else { 113 | console.error( 114 | `Unexpected failure for one of the files: ${result.reason}`, 115 | ); 116 | hasErrors = true; 117 | } 118 | }); 119 | 120 | if (hasErrors) { 121 | console.error( 122 | "One or more files could not be made executable. Please check the errors above.", 123 | ); 124 | // process.exit(1); // Uncomment to exit with error if any file fails 125 | } else { 126 | console.log("All targeted files processed successfully."); 127 | } 128 | } catch (error) { 129 | console.error( 130 | "A fatal error occurred during the make-executable script:", 131 | error instanceof Error ? error.message : error, 132 | ); 133 | process.exit(1); 134 | } 135 | }; 136 | 137 | makeExecutable(); 138 | ``` -------------------------------------------------------------------------------- /src/utils/parsing/dateParser.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Provides utilities for parsing natural language date strings 3 | * into Date objects or detailed parsing results using the 'chrono-node' library. 4 | * @module src/utils/parsing/dateParser 5 | */ 6 | 7 | import * as chrono from "chrono-node"; 8 | import { BaseErrorCode, McpError } from "../../types-global/errors.js"; 9 | import { ErrorHandler, logger, RequestContext } from "../internal/index.js"; 10 | 11 | /** 12 | * Parses a natural language date string (e.g., "tomorrow", "in 5 days", "2024-01-15") 13 | * into a JavaScript `Date` object. 14 | * 15 | * @async 16 | * @param {string} text - The natural language date string to parse. 17 | * @param {RequestContext} context - The request context for logging and error tracking. 18 | * @param {Date} [refDate] - Optional reference date for parsing relative date expressions. 19 | * Defaults to the current date and time if not provided. 20 | * @returns {Promise<Date | null>} A promise that resolves to a `Date` object representing 21 | * the parsed date, or `null` if `chrono-node` could not parse the input string into a date. 22 | * @throws {McpError} If an unexpected error occurs during the parsing process, 23 | * an `McpError` with `BaseErrorCode.PARSING_ERROR` is thrown. 24 | */ 25 | async function parseDateString( 26 | text: string, 27 | context: RequestContext, 28 | refDate?: Date, 29 | ): Promise<Date | null> { 30 | const operation = "parseDateString"; 31 | // Ensure context for logging includes all relevant details 32 | const logContext: RequestContext = { 33 | ...context, 34 | operation, 35 | inputText: text, 36 | refDate: refDate?.toISOString(), 37 | }; 38 | logger.debug(`Attempting to parse date string: "${text}"`, logContext); 39 | 40 | return await ErrorHandler.tryCatch( 41 | async () => { 42 | // chrono.parseDate returns a Date object or null if no date is found. 43 | const parsedDate = chrono.parseDate(text, refDate, { forwardDate: true }); 44 | if (parsedDate) { 45 | logger.debug( 46 | `Successfully parsed "${text}" to ${parsedDate.toISOString()}`, 47 | logContext, 48 | ); 49 | return parsedDate; 50 | } else { 51 | // This is not an error, but chrono-node couldn't find a date. 52 | logger.info( 53 | `Could not parse a date from string: "${text}"`, 54 | logContext, 55 | ); 56 | return null; 57 | } 58 | }, 59 | { 60 | operation, 61 | context: logContext, // Pass the enriched logContext 62 | input: { text, refDate: refDate?.toISOString() }, // Log refDate as ISO string for consistency 63 | errorCode: BaseErrorCode.PARSING_ERROR, // Default error code for unexpected parsing failures 64 | }, 65 | ); 66 | } 67 | 68 | /** 69 | * Parses a natural language date string and returns detailed parsing results, 70 | * including all components and their confidence levels, as provided by `chrono-node`. 71 | * 72 | * @async 73 | * @param {string} text - The natural language date string to parse. 74 | * @param {RequestContext} context - The request context for logging and error tracking. 75 | * @param {Date} [refDate] - Optional reference date for parsing relative date expressions. 76 | * Defaults to the current date and time if not provided. 77 | * @returns {Promise<chrono.ParsedResult[]>} A promise that resolves to an array of 78 | * `chrono.ParsedResult` objects. The array will be empty if no date components 79 | * could be parsed from the input string. 80 | * @throws {McpError} If an unexpected error occurs during the parsing process, 81 | * an `McpError` with `BaseErrorCode.PARSING_ERROR` is thrown. 82 | */ 83 | async function parseDateStringDetailed( 84 | text: string, 85 | context: RequestContext, 86 | refDate?: Date, 87 | ): Promise<chrono.ParsedResult[]> { 88 | const operation = "parseDateStringDetailed"; 89 | const logContext: RequestContext = { 90 | ...context, 91 | operation, 92 | inputText: text, 93 | refDate: refDate?.toISOString(), 94 | }; 95 | logger.debug( 96 | `Attempting detailed parse of date string: "${text}"`, 97 | logContext, 98 | ); 99 | 100 | return await ErrorHandler.tryCatch( 101 | async () => { 102 | // chrono.parse returns an array of results. 103 | const results = chrono.parse(text, refDate, { forwardDate: true }); 104 | logger.debug( 105 | `Detailed parse of "${text}" resulted in ${results.length} result(s).`, 106 | logContext, 107 | ); 108 | return results; 109 | }, 110 | { 111 | operation, 112 | context: logContext, 113 | input: { text, refDate: refDate?.toISOString() }, 114 | errorCode: BaseErrorCode.PARSING_ERROR, 115 | }, 116 | ); 117 | } 118 | 119 | /** 120 | * Provides methods for parsing natural language date strings. 121 | * - `parseToDate`: Parses a string to a single `Date` object or `null`. 122 | * - `getDetailedResults`: Provides comprehensive parsing results from `chrono-node`. 123 | */ 124 | export const dateParser = { 125 | /** 126 | * Parses a natural language date string into a `Date` object. 127 | * @see {@link parseDateString} 128 | */ 129 | parseToDate: parseDateString, 130 | /** 131 | * Parses a natural language date string and returns detailed `chrono.ParsedResult` objects. 132 | * @see {@link parseDateStringDetailed} 133 | */ 134 | getDetailedResults: parseDateStringDetailed, 135 | }; 136 | ```