#
tokens: 49140/50000 59/89 files (page 1/4)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 4. Use http://codebase.md/cyanheads/obsidian-mcp-server?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
{
  "reject": ["chrono-node"]
}

```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Operating System Files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# IDE and Editor Files
.idea/
.vscode/
*.swp
*.swo
*~
*.sublime-workspace
*.sublime-project

# TypeScript
*.tsbuildinfo
.tscache/
*.js.map
*.tgz
.npm
.eslintcache
.rollup.cache
*.mjs.map
*.cjs.map
*.d.ts.map
*.d.ts
!*.d.ts.template
.pnp.js
.pnp.cjs
.pnp.mjs
.pnp.json
.pnp.ts

# Demo and Example Directories
demo/
demos/
example/
examples/
samples/
.sample-env
sample.*
!sample.template.*

# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
.pytest_cache/
.coverage
htmlcov/
.tox/
.venv
venv/
ENV/

# Java
*.class
*.log
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
hs_err_pid*
target/
.gradle/
build/

# Ruby
*.gem
*.rbc
/.config
/coverage/
/InstalledFiles
/pkg/
/spec/reports/
/spec/examples.txt
/test/tmp/
/test/version_tmp/
/tmp/
.byebug_history

# Compiled Files
*.com
*.class
*.dll
*.exe
*.o
*.so

# Package Files
*.7z
*.dmg
*.gz
*.iso
*.rar
*.tar
*.zip

# Logs and Databases
*.log
*.sql
*.sqlite
*.sqlite3

# Build and Distribution
dist/
build/
out/

# Testing
coverage/
.nyc_output/

# Cache
.cache/
.parcel-cache/

# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
*.bak
*.swp
*.swo
*~
.history/
repomix-output*
mcp-servers.json
mcp-config.json

# Generated Documentation
docs/api/

logs/
```

--------------------------------------------------------------------------------
/.clinerules:
--------------------------------------------------------------------------------

```
# Obsidian MCP Server Developer Cheatsheet

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.

# Instructions for using this file:

1.  Carefully review this file line by line to understand this repo and Model Context Protocol (MCP).
2.  If you are creating new MCP Server tools, review the files:

    - `src/mcp-server/tools/obsidianUpdateNoteTool` (all files)
    - `src/mcp-server/tools/obsidianGlobalSearchTool` (all files)
    - `src/services/obsidianRestAPI` (Any files relevant to the tool you are creating)
    - `src/services/obsidianRestAPI/vaultCache` (If the tool needs vault structure/metadata caching)

3.  Keep this file updated to accurately reflect the state of the code base

## Server Transports & Configuration

The server can run using different communication transports, configured via environment variables.

- **`MCP_TRANSPORT_TYPE`**: Specifies the transport.
  - `"stdio"` (Default): Uses standard input/output for communication. Suitable for direct integration with parent processes.
  - `"http"`: Uses Streamable HTTP Server-Sent Events (SSE) for communication. Runs a Hono server.
- **`MCP_HTTP_PORT`**: Port for the HTTP server (Default: `3010`). Used only if `MCP_TRANSPORT_TYPE=http`.
- **`MCP_HTTP_HOST`**: Host address for the HTTP server (Default: `127.0.0.1`). Used only if `MCP_TRANSPORT_TYPE=http`.
- **`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`.
- **`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.
- **`MCP_AUTH_MODE`**: Authentication strategy to use for the HTTP transport. Can be `jwt` or `oauth`.
- **`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.**
- **`OAUTH_ISSUER_URL`**: **Required if `MCP_AUTH_MODE=oauth`**. The URL of the OAuth 2.1 token issuer.
- **`OAUTH_AUDIENCE`**: **Required if `MCP_AUTH_MODE=oauth`**. The audience claim for the OAuth tokens.
- **`OAUTH_JWKS_URI`**: Optional URI for the JSON Web Key Set. If omitted, it will be derived from the `OAUTH_ISSUER_URL`.
- **`OBSIDIAN_API_KEY`**: **Required.** API key for the Obsidian Local REST API plugin.
- **`OBSIDIAN_BASE_URL`**: **Required.** Base URL for the Obsidian Local REST API (e.g., `http://127.0.0.1:27123`).
- **`OBSIDIAN_VERIFY_SSL`**: Set to `false` to disable SSL certificate verification for the Obsidian API (e.g., for self-signed certs). Defaults to `true`.
- **`OBSIDIAN_ENABLE_CACHE`**: Set to `true` (default) or `false` to enable or disable the in-memory vault cache.
- **`OBSIDIAN_CACHE_REFRESH_INTERVAL_MIN`**: Interval in minutes for the vault cache to refresh automatically. Defaults to `10`.

### HTTP Transport Details (`MCP_TRANSPORT_TYPE=http`)

- **Endpoint**: A single endpoint `/mcp` handles all communication.
  - `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.
  - `GET /mcp`: Client initiates SSE stream for server-sent messages. Requires `mcp-session-id` header.
  - `DELETE /mcp`: Client signals session termination. Requires `mcp-session-id` header.
- **Session Management**: Each client connection establishes a session identified by the `mcp-session-id` header. The server maintains state per session.
- **Security**: Robust origin checking is implemented via CORS middleware. Configure `MCP_ALLOWED_ORIGINS` for production environments.

### Running the Server

- **Stdio**: `npm run start:stdio`
- **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`).

## Model Context Protocol (MCP) Overview (Spec: 2025-03-26)

MCP provides a standardized way for LLMs (via host applications) to interact with external capabilities (tools, data) exposed by dedicated servers.

### Core Concepts & Architecture

- **Host:** Manages clients, LLM integration, security, and user consent (e.g., Claude Desktop, VS Code).
- **Client:** Resides in the host, connects 1:1 to a server, handles protocol.
- **Server:** Standalone process exposing capabilities (Resources, Tools, Prompts). Focuses on its domain, isolated from LLM/other servers.

```mermaid
graph LR
    subgraph "Host Application Process"
        H[Host]
        C1[Client 1]
        C2[Client 2]
        H --> C1
        H --> C2
    end
    subgraph "Server Process 1"
        S1["MCP Server A<br>(e.g., Filesystem)"]
        R1["Local Resource<br>e.g., Files"]
        S1 <--> R1
    end
    subgraph "Server Process 2"
        S2["MCP Server B<br>(e.g., API Wrapper)"]
        R2["Remote Resource<br>e.g., Web API"]
        S2 <--> R2
    end
    C1 <-->|MCP Protocol| S1
    C2 <-->|MCP Protocol| S2
```

- **Key Principles:** Simplicity, Composability, Isolation, Progressive Features.

### Protocol Basics

- **Communication:** JSON-RPC 2.0 over a transport (Stdio, Streamable HTTP).
- **Messages:** Requests (with `id`), Responses (`id` + `result`/`error`), Notifications (no `id`). Batches MUST be supported for receiving.
- **Lifecycle:**
  1.  **Initialization:** Client sends `initialize` (version, capabilities, clientInfo). Server responds (`initialize` response: agreed version, capabilities, serverInfo, instructions?). Client sends `initialized` notification.
  2.  **Operation:** Message exchange based on negotiated capabilities.
  3.  **Shutdown:** Transport disconnect.

### Server Capabilities

Servers expose functionality via:

1.  **Resources:**

    - **Purpose:** Expose data/content (files, DB records) as context.
    - **Control:** Application-controlled.
    - **ID:** Unique URI (e.g., `file:///path/to/doc.txt`).
    - **Discovery:** `resources/list` (paginated), `resources/templates/list` (paginated).
    - **Reading:** `resources/read` -> `ResourceContent` array (`text` or `blob`).
    - **Updates (Optional):** `listChanged: true` -> `notifications/resources/list_changed`. `subscribe: true` -> `resources/subscribe`, `notifications/resources/updated`, **MUST handle `resources/unsubscribe` request**.

2.  **Tools:**

    - **Purpose:** Expose executable functions for LLM invocation (via client).
    - **Control:** Model-controlled.
    - **Definition:** `Tool` object (`name`, `description`, `inputSchema` (JSON Schema), `annotations?`). Annotations (`title`, `readOnlyHint`, etc.) are untrusted hints.
    - **Discovery:** `tools/list` (paginated).
    - **Invocation:** `tools/call` (`name`, `arguments`) -> `CallToolResult` (`content` array, `isError: boolean`). Execution errors reported via `isError: true`. **Rich schemas are crucial.**
    - **Updates (Optional):** `listChanged: true` -> `notifications/tools/list_changed` (MUST send after dynamic changes).

3.  **Prompts:**
    - **Purpose:** Reusable prompt templates/workflows (e.g., slash commands).
    - **Control:** User-controlled.
    - **Definition:** `Prompt` object (`name`, `description?`, `arguments?`).
    - **Discovery:** `prompts/list` (paginated).
    - **Usage:** `prompts/get` (`name`, `arguments`) -> `GetPromptResult` (`messages` array).
    - **Updates (Optional):** `listChanged: true` -> `notifications/prompts/list_changed`.

### Interacting with Client Capabilities

- **Roots:** Client may provide filesystem roots (`file://`). Server receives list on init, updates via `notifications/roots/list_changed` (if supported). Servers SHOULD respect roots.
- **Sampling:** Server can request LLM completion via client using `sampling/createMessage`. Client SHOULD implement human-in-the-loop.

### Server Utilities

- **Logging:** `logging` capability -> `notifications/message` (RFC 5424 levels: `debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency`). Client can send `logging/setLevel`.
- **Pagination:** List operations use `cursor`/`nextCursor`.
- **Completion:** `completions` capability -> `completion/complete`.
- **Cancellation:** `notifications/cancelled` (best-effort).
- **Ping:** `ping` request -> `{}` response.
- **Progress:** `notifications/progress` (requires `_meta.progressToken` in original request).
- **Configuration:** `configuration/get`, `configuration/set`.
- **Back-pressure:** Clients debounce rapid notifications. Servers should aim for idempotency.

### SDK Usage (TypeScript) - IMPORTANT

- **High-Level SDK Abstractions (Strongly Recommended):**
  - **Use `server.tool(name, description, zodSchemaShape, handler)`:** This is the **preferred and strongly recommended** way to define tools. It automatically handles:
    - Registering the tool for `tools/list`.
    - Generating the JSON Schema from the Zod shape.
    - Validating incoming `tools/call` arguments against the schema.
    - Routing the call to your handler with validated arguments.
    - Formatting the `CallToolResult`.
  - **Use `server.resource(regName, template, metadata, handler)`:** Similarly recommended for resources.
  - **Benefits:** Significantly reduces boilerplate, enforces type safety, simplifies protocol adherence.
- **Low-Level SDK Handlers (AVOID unless absolutely necessary):**
  - Manually using `server.setRequestHandler(SchemaObject, handler)` requires you to handle schema generation, argument validation, request routing, and response formatting yourself.
  - **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.**

### Security Considerations

- **Input Validation:** Use schemas (Zod), sanitize inputs (paths, HTML, SQL).
- **Access Control:** Least privilege, respect roots.
- **Transport Security:**
  - **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.
  - **Stdio:** Authentication typically handled by the host process. Best practice is to not apply authentication to MCP Server stdio processes.
- **Secrets Management:** Use env vars (`MCP_AUTH_SECRET_KEY`, `OBSIDIAN_API_KEY`) or secrets managers, avoid hardcoding/logging.
- **Dependency Security:** Keep dependencies updated (`npm audit`).
- **Rate Limiting:** Protect against abuse.

## Obsidian REST API Service (`src/services/obsidianRestAPI/`)

This service provides a typed interface for interacting with the Obsidian Local REST API plugin.

### Purpose

- Encapsulates all communication logic with the Obsidian REST API.
- Provides methods for common Obsidian operations like reading/writing files, searching, executing commands, etc.
- Handles authentication (API key) and configuration (base URL, SSL verification) based on environment variables (`OBSIDIAN_API_KEY`, `OBSIDIAN_BASE_URL`, `OBSIDIAN_VERIFY_SSL`).
- Includes robust path encoding for vault files and an increased default request timeout (60s).
- Performs an initial status check on server startup (`src/index.ts`).

### Architecture

- **`service.ts` (`ObsidianRestApiService` class):**
  - The main service class.
  - Initializes an Axios instance for making HTTP requests.
  - Contains the private `_request` method which handles:
    - Adding the `Authorization` header.
    - Making the actual HTTP call.
    - Centralized error handling (translating HTTP errors to `McpError`).
    - Logging requests and responses.
  - Exposes public methods for each API category (e.g., `getFileContent`, `executeCommand`).
- **`methods/*.ts`:**
  - Each file corresponds to a category of API endpoints (e.g., `vaultMethods.ts`, `commandMethods.ts`).
  - Contains functions that implement the logic for specific endpoints (e.g., constructing the URL, setting request body/headers).
  - 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.
- **`types.ts`:**
  - Defines TypeScript interfaces for API request parameters and response structures (e.g., `NoteJson`, `PatchOptions`, `ApiStatusResponse`). Based on the Obsidian Local REST API OpenAPI spec.
- **`index.ts`:**
  - Barrel file exporting the `ObsidianRestApiService` class, all types, and the method modules.

### Usage

- The `ObsidianRestApiService` is typically instantiated within the MCP server logic (e.g., inside tool handlers) where interaction with Obsidian is needed.
- Tool implementations (like `obsidianReadFileTool`, `obsidianUpdateNoteTool`) use this service to perform actions within the user's vault.

```typescript
// Example within an MCP Tool handler
import { ObsidianRestApiService } from "../../services/obsidianRestAPI/index.js";
import { config } from "../../config/index.js";
import { RequestContext } from "../../utils/index.js";

// Assuming obsidianService is instantiated appropriately
// (In a real scenario, dependency injection or a singleton pattern might be used)
const obsidianService = new ObsidianRestApiService(); // Needs config loaded

async function handleReadFile(filePath: string, context: RequestContext) {
  try {
    const content = await obsidianService.getFileContent(
      filePath,
      "markdown",
      context
    );
    // ... process content ...
    return { content: [{ text: `Read content: ${content}` }], isError: false };
  } catch (error) {
    // Error handling (McpError already thrown by service)
    throw error; // Re-throw for the MCP server to handle
  }
}
```

- **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.

## Agent Instructions for API Connection Help

If the user asks for help connecting to the Obsidian API, follow these steps:

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.
2.  **Recommend the HTTP Method for Simplicity**:
    - Advise the user to enable the "Enable Non-encrypted (HTTP) Server" option in the plugin's settings in Obsidian.
    - Explain that this is the simplest method as it does not require dealing with SSL certificates.
    - Provide the corresponding `OBSIDIAN_BASE_URL` (e.g., `http://127.0.0.1:27123`).
3.  **Explain the HTTPS Method**:
    - 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.
    - Clearly state that this will cause connection failures unless they set the `OBSIDIAN_VERIFY_SSL` environment variable to `"false"`.
4.  **Provide Clear Examples**: Show example `env` configurations for both methods, as seen in the main `README.md`.

## Vault Cache Service (`src/services/obsidianRestAPI/vaultCache/`)

This service provides an in-memory cache of the Obsidian vault's file content and metadata.

### Purpose

- **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).
- **API Fallback**: The `obsidianGlobalSearchTool` uses the cache as a fallback if the live API search fails or times out, ensuring greater resilience.
- **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.

### Architecture

- **`service.ts` (`VaultCacheService` class):**
  - Takes an `ObsidianRestApiService` instance in its constructor.
  - Manages the cache state (a Map of file paths to their content and modification time).
  - Provides methods to build/rebuild the cache (`buildVaultCache`, `refreshCache`) by calling the Obsidian API (`listFiles`, `getFileContent`, `getFileMetadata`).
  - Exposes methods to query the cache (`isReady`, `getCache`, `getEntry`).
  - Manages the periodic refresh timer (`startPeriodicRefresh`, `stopPeriodicRefresh`).
- **`index.ts`:** Barrel file exporting the service.

### Usage

- Instantiated in `src/index.ts` and passed as a dependency to tools that can benefit from it (e.g., `obsidianGlobalSearchTool`).
- The initial cache build is triggered asynchronously on server startup. Tools should check `isReady()` before relying on the cache.

## Core Utilities Integration

### 1. Logging (`src/utils/internal/logger.ts`)

- **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.
- **Levels**: `debug`(7), `info`(6), `notice`(5), `warning`(4), `error`(3), `crit`(2), `alert`(1), `emerg`(0).
- **Usage**: Import the singleton `logger` instance from the main utils barrel file (`src/utils/index.js`). Pass a `context` object (`RequestContext`) for correlation.

**Note**: The full logger implementation is provided below for reference to understand exactly how logger works, expected JSDoc structure, and integration points.

```typescript
/**
 * @fileoverview Provides a singleton Logger class that wraps Winston for file logging
 * and supports sending MCP (Model Context Protocol) `notifications/message`.
 * It handles different log levels compliant with RFC 5424 and MCP specifications.
 * @module src/utils/internal/logger
 */
import path from "path";
import winston from "winston";
import TransportStream from "winston-transport";
import { config } from "../../config/index.js";
import { RequestContext } from "./requestContext.js";

/**
 * Defines the supported logging levels based on RFC 5424 Syslog severity levels,
 * as used by the Model Context Protocol (MCP).
 * Levels are: 'debug'(7), 'info'(6), 'notice'(5), 'warning'(4), 'error'(3), 'crit'(2), 'alert'(1), 'emerg'(0).
 * Lower numeric values indicate higher severity.
 */
export type McpLogLevel =
  | "debug"
  | "info"
  | "notice"
  | "warning"
  | "error"
  | "crit"
  | "alert"
  | "emerg";

/**
 * Numeric severity mapping for MCP log levels (lower is more severe).
 * @private
 */
const mcpLevelSeverity: Record<McpLogLevel, number> = {
  emerg: 0,
  alert: 1,
  crit: 2,
  error: 3,
  warning: 4,
  notice: 5,
  info: 6,
  debug: 7,
};

/**
 * Maps MCP log levels to Winston's core levels for file logging.
 * @private
 */
const mcpToWinstonLevel: Record<
  McpLogLevel,
  "debug" | "info" | "warn" | "error"
> = {
  debug: "debug",
  info: "info",
  notice: "info",
  warning: "warn",
  error: "error",
  crit: "error",
  alert: "error",
  emerg: "error",
};

/**
 * Interface for a more structured error object, primarily for formatting console logs.
 * @private
 */
interface ErrorWithMessageAndStack {
  message?: string;
  stack?: string;
  [key: string]: any;
}

/**
 * Interface for the payload of an MCP log notification.
 * This structure is used when sending log data via MCP `notifications/message`.
 */
export interface McpLogPayload {
  message: string;
  context?: RequestContext;
  error?: {
    message: string;
    stack?: string;
  };
  [key: string]: any;
}

/**
 * Type for the `data` parameter of the `McpNotificationSender` function.
 */
export type McpNotificationData = McpLogPayload | Record<string, unknown>;

/**
 * Defines the signature for a function that can send MCP log notifications.
 * This function is typically provided by the MCP server instance.
 * @param level - The severity level of the log message.
 * @param data - The payload of the log notification.
 * @param loggerName - An optional name or identifier for the logger/server.
 */
export type McpNotificationSender = (
  level: McpLogLevel,
  data: McpNotificationData,
  loggerName?: string
) => void;

// The logsPath from config is already resolved and validated by src/config/index.ts
const resolvedLogsDir = config.logsPath;
const isLogsDirSafe = !!resolvedLogsDir; // If logsPath is set, it's considered safe by config logic.

/**
 * Creates the Winston console log format.
 * @returns The Winston log format for console output.
 * @private
 */
function createWinstonConsoleFormat(): winston.Logform.Format {
  return winston.format.combine(
    winston.format.colorize(),
    winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
    winston.format.printf(({ timestamp, level, message, ...meta }) => {
      let metaString = "";
      const metaCopy = { ...meta };
      if (metaCopy.error && typeof metaCopy.error === "object") {
        const errorObj = metaCopy.error as ErrorWithMessageAndStack;
        if (errorObj.message) metaString += `\n  Error: ${errorObj.message}`;
        if (errorObj.stack)
          metaString += `\n  Stack: ${String(errorObj.stack)
            .split("\n")
            .map((l: string) => `    ${l}`)
            .join("\n")}`;
        delete metaCopy.error;
      }
      if (Object.keys(metaCopy).length > 0) {
        try {
          const replacer = (_key: string, value: unknown) =>
            typeof value === "bigint" ? value.toString() : value;
          const remainingMetaJson = JSON.stringify(metaCopy, replacer, 2);
          if (remainingMetaJson !== "{}")
            metaString += `\n  Meta: ${remainingMetaJson}`;
        } catch (stringifyError: unknown) {
          const errorMessage =
            stringifyError instanceof Error
              ? stringifyError.message
              : String(stringifyError);
          metaString += `\n  Meta: [Error stringifying metadata: ${errorMessage}]`;
        }
      }
      return `${timestamp} ${level}: ${message}${metaString}`;
    })
  );
}

/**
 * Singleton Logger class that wraps Winston for robust logging.
 * Supports file logging, conditional console logging, and MCP notifications.
 */
export class Logger {
  private static instance: Logger;
  private winstonLogger?: winston.Logger;
  private initialized = false;
  private mcpNotificationSender?: McpNotificationSender;
  private currentMcpLevel: McpLogLevel = "info";
  private currentWinstonLevel: "debug" | "info" | "warn" | "error" = "info";

  private readonly MCP_NOTIFICATION_STACK_TRACE_MAX_LENGTH = 1024;
  private readonly LOG_FILE_MAX_SIZE = 5 * 1024 * 1024; // 5MB
  private readonly LOG_MAX_FILES = 5;

  /** @private */
  private constructor() {}

  /**
   * Initializes the Winston logger instance.
   * Should be called once at application startup.
   * @param level - The initial minimum MCP log level.
   */
  public async initialize(level: McpLogLevel = "info"): Promise<void> {
    if (this.initialized) {
      this.warning("Logger already initialized.", {
        loggerSetup: true,
        requestId: "logger-init",
        timestamp: new Date().toISOString(),
      });
      return;
    }

    // Set initialized to true at the beginning of the initialization process.
    this.initialized = true;

    this.currentMcpLevel = level;
    this.currentWinstonLevel = mcpToWinstonLevel[level];

    // The logs directory (config.logsPath / resolvedLogsDir) is expected to be created and validated
    // by the configuration module (src/config/index.ts) before logger initialization.
    // If isLogsDirSafe is true, we assume resolvedLogsDir exists and is usable.
    // No redundant directory creation logic here.

    const fileFormat = winston.format.combine(
      winston.format.timestamp(),
      winston.format.errors({ stack: true }),
      winston.format.json()
    );

    const transports: TransportStream[] = [];
    const fileTransportOptions = {
      format: fileFormat,
      maxsize: this.LOG_FILE_MAX_SIZE,
      maxFiles: this.LOG_MAX_FILES,
      tailable: true,
    };

    if (isLogsDirSafe) {
      transports.push(
        new winston.transports.File({
          filename: path.join(resolvedLogsDir, "error.log"),
          level: "error",
          ...fileTransportOptions,
        }),
        new winston.transports.File({
          filename: path.join(resolvedLogsDir, "warn.log"),
          level: "warn",
          ...fileTransportOptions,
        }),
        new winston.transports.File({
          filename: path.join(resolvedLogsDir, "info.log"),
          level: "info",
          ...fileTransportOptions,
        }),
        new winston.transports.File({
          filename: path.join(resolvedLogsDir, "debug.log"),
          level: "debug",
          ...fileTransportOptions,
        }),
        new winston.transports.File({
          filename: path.join(resolvedLogsDir, "combined.log"),
          ...fileTransportOptions,
        })
      );
    } else {
      if (process.stdout.isTTY) {
        console.warn(
          "File logging disabled as logsPath is not configured or invalid."
        );
      }
    }

    this.winstonLogger = winston.createLogger({
      level: this.currentWinstonLevel,
      transports,
      exitOnError: false,
    });

    // Configure console transport after Winston logger is created
    const consoleStatus = this._configureConsoleTransport();

    const initialContext: RequestContext = {
      loggerSetup: true,
      requestId: "logger-init-deferred",
      timestamp: new Date().toISOString(),
    };
    // Removed logging of logsDirCreatedMessage as it's no longer set
    if (consoleStatus.message) {
      this.info(consoleStatus.message, initialContext);
    }

    this.initialized = true; // Ensure this is set after successful setup
    this.info(
      `Logger initialized. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${consoleStatus.enabled ? "enabled" : "disabled"}`,
      {
        loggerSetup: true,
        requestId: "logger-post-init",
        timestamp: new Date().toISOString(),
        logsPathUsed: resolvedLogsDir,
      }
    );
  }

  /**
   * Sets the function used to send MCP 'notifications/message'.
   * @param sender - The function to call for sending notifications, or undefined to disable.
   */
  public setMcpNotificationSender(
    sender: McpNotificationSender | undefined
  ): void {
    this.mcpNotificationSender = sender;
    const status = sender ? "enabled" : "disabled";
    this.info(`MCP notification sending ${status}.`, {
      loggerSetup: true,
      requestId: "logger-set-sender",
      timestamp: new Date().toISOString(),
    });
  }

  /**
   * Dynamically sets the minimum logging level.
   * @param newLevel - The new minimum MCP log level to set.
   */
  public setLevel(newLevel: McpLogLevel): void {
    const setLevelContext: RequestContext = {
      loggerSetup: true,
      requestId: "logger-set-level",
      timestamp: new Date().toISOString(),
    };
    if (!this.ensureInitialized()) {
      if (process.stdout.isTTY) {
        console.error("Cannot set level: Logger not initialized.");
      }
      return;
    }
    if (!(newLevel in mcpLevelSeverity)) {
      this.warning(
        `Invalid MCP log level provided: ${newLevel}. Level not changed.`,
        setLevelContext
      );
      return;
    }

    const oldLevel = this.currentMcpLevel;
    this.currentMcpLevel = newLevel;
    this.currentWinstonLevel = mcpToWinstonLevel[newLevel];
    if (this.winstonLogger) {
      // Ensure winstonLogger is defined
      this.winstonLogger.level = this.currentWinstonLevel;
    }

    const consoleStatus = this._configureConsoleTransport();

    if (oldLevel !== newLevel) {
      this.info(
        `Log level changed. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${consoleStatus.enabled ? "enabled" : "disabled"}`,
        setLevelContext
      );
      if (
        consoleStatus.message &&
        consoleStatus.message !== "Console logging status unchanged."
      ) {
        this.info(consoleStatus.message, setLevelContext);
      }
    }
  }

  /**
   * Configures the console transport based on the current log level and TTY status.
   * Adds or removes the console transport as needed.
   * @returns {{ enabled: boolean, message: string | null }} Status of console logging.
   * @private
   */
  private _configureConsoleTransport(): {
    enabled: boolean;
    message: string | null;
  } {
    if (!this.winstonLogger) {
      return {
        enabled: false,
        message: "Cannot configure console: Winston logger not initialized.",
      };
    }

    const consoleTransport = this.winstonLogger.transports.find(
      (t) => t instanceof winston.transports.Console
    );
    const shouldHaveConsole =
      this.currentMcpLevel === "debug" && process.stdout.isTTY;
    let message: string | null = null;

    if (shouldHaveConsole && !consoleTransport) {
      const consoleFormat = createWinstonConsoleFormat();
      this.winstonLogger.add(
        new winston.transports.Console({
          level: "debug", // Console always logs debug if enabled
          format: consoleFormat,
        })
      );
      message = "Console logging enabled (level: debug, stdout is TTY).";
    } else if (!shouldHaveConsole && consoleTransport) {
      this.winstonLogger.remove(consoleTransport);
      message = "Console logging disabled (level not debug or stdout not TTY).";
    } else {
      message = "Console logging status unchanged.";
    }
    return { enabled: shouldHaveConsole, message };
  }

  /**
   * Gets the singleton instance of the Logger.
   * @returns The singleton Logger instance.
   */
  public static getInstance(): Logger {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  /**
   * Ensures the logger has been initialized.
   * @returns True if initialized, false otherwise.
   * @private
   */
  private ensureInitialized(): boolean {
    if (!this.initialized || !this.winstonLogger) {
      if (process.stdout.isTTY) {
        console.warn("Logger not initialized; message dropped.");
      }
      return false;
    }
    return true;
  }

  /**
   * Centralized log processing method.
   * @param level - The MCP severity level of the message.
   * @param msg - The main log message.
   * @param context - Optional request context for the log.
   * @param error - Optional error object associated with the log.
   * @private
   */
  private log(
    level: McpLogLevel,
    msg: string,
    context?: RequestContext,
    error?: Error
  ): void {
    if (!this.ensureInitialized()) return;
    if (mcpLevelSeverity[level] > mcpLevelSeverity[this.currentMcpLevel]) {
      return; // Do not log if message level is less severe than currentMcpLevel
    }

    const logData: Record<string, unknown> = { ...context };
    const winstonLevel = mcpToWinstonLevel[level];

    if (error) {
      this.winstonLogger!.log(winstonLevel, msg, { ...logData, error });
    } else {
      this.winstonLogger!.log(winstonLevel, msg, logData);
    }

    if (this.mcpNotificationSender) {
      const mcpDataPayload: McpLogPayload = { message: msg };
      if (context && Object.keys(context).length > 0)
        mcpDataPayload.context = context;
      if (error) {
        mcpDataPayload.error = { message: error.message };
        // Include stack trace in debug mode for MCP notifications, truncated for brevity
        if (this.currentMcpLevel === "debug" && error.stack) {
          mcpDataPayload.error.stack = error.stack.substring(
            0,
            this.MCP_NOTIFICATION_STACK_TRACE_MAX_LENGTH
          );
        }
      }
      try {
        const serverName =
          config?.mcpServerName ?? "MCP_SERVER_NAME_NOT_CONFIGURED";
        this.mcpNotificationSender(level, mcpDataPayload, serverName);
      } catch (sendError: unknown) {
        const errorMessage =
          sendError instanceof Error ? sendError.message : String(sendError);
        const internalErrorContext: RequestContext = {
          requestId: context?.requestId || "logger-internal-error",
          timestamp: new Date().toISOString(),
          originalLevel: level,
          originalMessage: msg,
          sendError: errorMessage,
          mcpPayload: JSON.stringify(mcpDataPayload).substring(0, 500), // Log a preview
        };
        this.winstonLogger!.error(
          "Failed to send MCP log notification",
          internalErrorContext
        );
      }
    }
  }

  /** Logs a message at the 'debug' level. */
  public debug(msg: string, context?: RequestContext): void {
    this.log("debug", msg, context);
  }

  /** Logs a message at the 'info' level. */
  public info(msg: string, context?: RequestContext): void {
    this.log("info", msg, context);
  }

  /** Logs a message at the 'notice' level. */
  public notice(msg: string, context?: RequestContext): void {
    this.log("notice", msg, context);
  }

  /** Logs a message at the 'warning' level. */
  public warning(msg: string, context?: RequestContext): void {
    this.log("warning", msg, context);
  }

  /**
   * Logs a message at the 'error' level.
   * @param msg - The main log message.
   * @param err - Optional. Error object or RequestContext.
   * @param context - Optional. RequestContext if `err` is an Error.
   */
  public error(
    msg: string,
    err?: Error | RequestContext,
    context?: RequestContext
  ): void {
    const errorObj = err instanceof Error ? err : undefined;
    const actualContext = err instanceof Error ? context : err;
    this.log("error", msg, actualContext, errorObj);
  }

  /**
   * Logs a message at the 'crit' (critical) level.
   * @param msg - The main log message.
   * @param err - Optional. Error object or RequestContext.
   * @param context - Optional. RequestContext if `err` is an Error.
   */
  public crit(
    msg: string,
    err?: Error | RequestContext,
    context?: RequestContext
  ): void {
    const errorObj = err instanceof Error ? err : undefined;
    const actualContext = err instanceof Error ? context : err;
    this.log("crit", msg, actualContext, errorObj);
  }

  /**
   * Logs a message at the 'alert' level.
   * @param msg - The main log message.
   * @param err - Optional. Error object or RequestContext.
   * @param context - Optional. RequestContext if `err` is an Error.
   */
  public alert(
    msg: string,
    err?: Error | RequestContext,
    context?: RequestContext
  ): void {
    const errorObj = err instanceof Error ? err : undefined;
    const actualContext = err instanceof Error ? context : err;
    this.log("alert", msg, actualContext, errorObj);
  }

  /**
   * Logs a message at the 'emerg' (emergency) level.
   * @param msg - The main log message.
   * @param err - Optional. Error object or RequestContext.
   * @param context - Optional. RequestContext if `err` is an Error.
   */
  public emerg(
    msg: string,
    err?: Error | RequestContext,
    context?: RequestContext
  ): void {
    const errorObj = err instanceof Error ? err : undefined;
    const actualContext = err instanceof Error ? context : err;
    this.log("emerg", msg, actualContext, errorObj);
  }

  /**
   * Logs a message at the 'emerg' (emergency) level, typically for fatal errors.
   * @param msg - The main log message.
   * @param err - Optional. Error object or RequestContext.
   * @param context - Optional. RequestContext if `err` is an Error.
   */
  public fatal(
    msg: string,
    err?: Error | RequestContext,
    context?: RequestContext
  ): void {
    const errorObj = err instanceof Error ? err : undefined;
    const actualContext = err instanceof Error ? context : err;
    this.log("emerg", msg, actualContext, errorObj);
  }
}

/**
 * The singleton instance of the Logger.
 * Use this instance for all logging operations.
 */
export const logger = Logger.getInstance();
```

- **Key Files**:
  - `src/utils/internal/logger.ts`: Logger implementation.
  - `logs/`: Directory where JSON log files are stored (`combined.log`, `error.log`, etc.).

### 2. Error Handling (`src/utils/internal/errorHandler.ts`)

- **Purpose**: Standardized error objects (`McpError`) and centralized handling (`ErrorHandler`). Automatically determines error codes based on type/patterns.
- **Usage**:
  - Use `ErrorHandler.tryCatch` to wrap operations that might fail.
  - Throw `McpError` for specific, categorized errors using `BaseErrorCode`.
  - `ErrorHandler` automatically logs errors (using the logger) with context and sanitized input.

```typescript
// Example assuming import from a file within src/
import {
  ErrorHandler,
  RequestContext,
  requestContextService,
} from "./utils/index.js";
import { McpError, BaseErrorCode } from "./types-global/errors.js";

async function performTask(input: any, parentContext: RequestContext) {
  const context = { ...parentContext, operation: "performTask" };
  return await ErrorHandler.tryCatch(
    async () => {
      if (!input) {
        throw new McpError(
          BaseErrorCode.VALIDATION_ERROR,
          "Input cannot be empty",
          context
        );
      }
      // ... perform task logic ...
      const result = await someAsyncOperation(input);
      return result;
    },
    {
      operation: "performTask", // Redundant but good for clarity
      context: context,
      input: input, // Input is automatically sanitized for logging
      errorCode: BaseErrorCode.INTERNAL_ERROR, // Default code if unexpected error occurs
      critical: false, // Or true if failure should halt the process
    }
  );
}
```

- **Key Files**:
  - `src/types-global/errors.ts`: Defines `McpError` and `BaseErrorCode`.
  - `src/utils/internal/errorHandler.ts`: Provides `ErrorHandler.tryCatch`, `handleError`, `determineErrorCode`.

### 3. Async Operations (`src/utils/internal/asyncUtils.ts`)

- **Purpose**: Provides utilities for handling asynchronous operations, most notably `retryWithDelay` for retrying failed operations.
- **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.

```typescript
// Example assuming import from a file within src/
import { retryWithDelay, RequestContext } from "./utils/index.js";
import { McpError, BaseErrorCode } from "./types-global/errors.js";

async function fetchWithRetry(url: string, context: RequestContext) {
  return await retryWithDelay(
    async () => {
      const response = await fetch(url);
      if (!response.ok) {
        // Throw an error that the retry logic can inspect
        throw new McpError(
          BaseErrorCode.SERVICE_UNAVAILABLE,
          `Fetch failed with status ${response.status}`
        );
      }
      return response.json();
    },
    {
      operationName: "fetchWithRetry",
      context: context,
      maxRetries: 3,
      delayMs: 500,
      // Only retry on specific, transient error codes
      shouldRetry: (err: unknown) =>
        err instanceof McpError &&
        err.code === BaseErrorCode.SERVICE_UNAVAILABLE,
    }
  );
}
```

- **Key Files**:
  - `src/utils/internal/asyncUtils.ts`: Provides `retryWithDelay`.

### 4. Request Context (`src/utils/internal/requestContext.ts`)

- **Purpose**: Track and correlate operations related to a single request or workflow using a unique `requestId`.
- **Usage**:
  - Create context at the beginning of an operation using `requestContextService.createRequestContext`.
  - Pass the context object down through function calls.
  - Include the context object when logging or creating errors.

```typescript
// Example assuming import from a file within src/
import {
  requestContextService,
  RequestContext,
  logger,
} from "./utils/index.js";

function handleIncomingRequest(requestData: any) {
  const context: RequestContext = requestContextService.createRequestContext({
    operation: "HandleIncomingRequest",
    initialData: requestData.id,
  });

  logger.info("Received request", context);
  processSubTask(requestData.payload, context);
}

function processSubTask(payload: any, parentContext: RequestContext) {
  const subTaskContext = { ...parentContext, subOperation: "ProcessSubTask" };
  logger.debug("Processing sub-task", subTaskContext);
  // ... logic ...
}
```

- **Key Files**:
  - `src/utils/internal/requestContext.ts`: Defines `RequestContext` interface and `requestContextService`.

### 5. ID Generation (`src/utils/security/idGenerator.ts`)

- **Purpose**: Generate unique, prefixed IDs for different entity types and standard UUIDs.
- **Usage**: Configure prefixes (if needed) and use `idGenerator.generateForEntity` or `generateUUID` from the main utils barrel file.

```typescript
// Example assuming import from a file within src/
import { idGenerator, generateUUID } from "./utils/index.js";

// Prefixes are typically not needed unless distinguishing IDs across systems
// idGenerator.setEntityPrefixes({ project: 'PROJ', task: 'TASK' });

const someId = idGenerator.generateForEntity("request"); // e.g., "REQ_A6B3J0"
const standardUuid = generateUUID(); // e.g., "123e4567-e89b-12d3-a456-426614174000"

const isValid = idGenerator.isValid(someId, "request"); // true
const entityType = idGenerator.getEntityType(someId); // "request"
```

- **Key Files**:
  - `src/utils/security/idGenerator.ts`: `IdGenerator` class, `idGenerator` instance, `generateUUID`.

### 6. Sanitization (`src/utils/security/sanitization.ts`)

- **Purpose**: Clean and validate input data (HTML, paths, numbers, URLs, JSON) to prevent security issues. Also sanitizes objects for logging.
- **Usage**: Import the singleton `sanitization` instance or `sanitizeInputForLogging` from the main utils barrel file.

```typescript
// Example assuming import from a file within src/
import { sanitization, sanitizeInputForLogging } from "./utils/index.js";

const unsafeHtml = '<script>alert("xss")</script><p>Safe content</p>';
const safeHtml = sanitization.sanitizeHtml(unsafeHtml); // "<p>Safe content</p>"

const sensitiveData = {
  user: "admin",
  password: "pwd",
  token: "abc",
  obsidianApiKey: "secret",
};
const safeLogData = sanitizeInputForLogging(sensitiveData);
// safeLogData = { user: 'admin', password: '[REDACTED]', token: '[REDACTED]', obsidianApiKey: '[REDACTED]' }
```

- **Key Files**:
  - `src/utils/security/sanitization.ts`: `Sanitization` class, `sanitization` instance, `sanitizeInputForLogging`.

### 7. JSON Parsing (`src/utils/parsing/jsonParser.ts`)

- **Purpose**: Parse potentially partial/incomplete JSON strings. Handles optional `<think>` blocks.
- **Usage**: Import `jsonParser` from the main utils barrel file. Use `Allow` constants for options.

```typescript
// Example assuming import from a file within src/
import { jsonParser, Allow, RequestContext } from './utils/index.js';

const partialJson = '<think>Parsing...</think>{"key": "value", "incomplete": ';
const context: RequestContext = /* ... */;

try {
  const parsed = jsonParser.parse(partialJson, Allow.ALL, context);
  // parsed = { key: 'value', incomplete: undefined }
} catch (error) { /* Handle McpError */ }
```

- **Key Files**:
  - `src/utils/parsing/jsonParser.ts`: `JsonParser` class, `jsonParser` instance, `Allow` enum.

### 8. Rate Limiting (`src/utils/security/rateLimiter.ts`)

- **Purpose**: Implement rate limiting based on a key (e.g., session ID, user ID).
- **Usage**: Import `rateLimiter` from the main utils barrel file. Use `check`.

```typescript
// Example assuming import from a file within src/
import { rateLimiter, RequestContext } from './utils/index.js';

const sessionId = 'session-abc'; // Or another identifier
const context: RequestContext = /* ... */;

try {
  rateLimiter.check(sessionId, context);
  // ... proceed with operation ...
} catch (error) { /* Handle McpError (RATE_LIMITED) */ }
```

- **Key Files**:
  - `src/utils/security/rateLimiter.ts`: `RateLimiter` class, `rateLimiter` instance.

### 9. Token Counting (`src/utils/metrics/tokenCounter.ts`)

- **Purpose**: Estimate tokens using `tiktoken` (`gpt-4o` model). Useful for tracking LLM usage or context window limits.
- **Usage**: Import `countTokens` or `countChatTokens` from the main utils barrel file.

```typescript
// Example assuming import from a file within src/
import { countTokens, countChatTokens, RequestContext } from './utils/index.js';
import { ChatCompletionMessageParam } from 'openai/resources/chat/completions';

const text = "Sample text to count tokens for.";
const context: RequestContext = /* ... */;

async function calculateTokens() {
  try {
    const textTokens = await countTokens(text, context);
    logger.info(`Text token count: ${textTokens}`, context);
  } catch (error) { /* Handle McpError */ }
}
```

- **Key Files**:
  - `src/utils/metrics/tokenCounter.ts`: Provides `countTokens` and `countChatTokens`.

### 10. Obsidian Formatting (`src/utils/obsidian/obsidianStatUtils.ts`)

- **Purpose**: Provides helpers for formatting data related to Obsidian notes, such as timestamps and token counts.
- **Usage**: Use `formatTimestamp` to create human-readable date strings and `createFormattedStatWithTokenCount` to generate a comprehensive stat object for tool responses.

```typescript
// Example assuming import from a file within src/
import {
  createFormattedStatWithTokenCount,
  RequestContext,
} from "./utils/index.js";
import { NoteStat } from "./services/obsidianRestAPI/index.js";

async function formatNoteStats(
  stat: NoteStat,
  content: string,
  context: RequestContext
) {
  // stat = { ctime: 1672531200000, mtime: 1672617600000, size: 123 }
  const formattedStat = await createFormattedStatWithTokenCount(
    stat,
    content,
    context
  );
  // formattedStat might be:
  // {
  //   createdTime: '04:00:00 PM | 01-01-2023',
  //   modifiedTime: '04:00:00 PM | 01-02-2023',
  //   tokenCountEstimate: 30
  // }
  return formattedStat;
}
```

- **Key Files**:
  - `src/utils/obsidian/obsidianStatUtils.ts`: Provides formatting helpers.

## Utility Scripts (`scripts/`)

This project includes several utility scripts located in the `scripts/` directory to aid development:

### 1. Clean (`scripts/clean.ts`)

- **Purpose**: Removes build artifacts and temporary directories.
- **Usage**: `npm run rebuild` (uses this script) or `ts-node --esm scripts/clean.ts [dir1] [dir2]...`
- **Default Targets**: `dist`, `logs`.

### 2. Make Executable (`scripts/make-executable.ts`)

- **Purpose**: Sets executable permissions (`chmod +x`) on specified files (Unix-like systems only). Useful for CLI entry points after building.
- **Usage**: `npm run build` (uses this script) or `ts-node --esm scripts/make-executable.ts [file1] [file2]...`
- **Default Target**: `dist/index.js`.

### 3. Generate Tree (`scripts/tree.ts`)

- **Purpose**: Creates a visual directory tree markdown file (`docs/tree.md` by default), respecting `.gitignore`.
- **Usage**: `npm run tree` or `ts-node --esm scripts/tree.ts [output-path] [--depth=<number>]`

### 4. Fetch OpenAPI Spec (`scripts/fetch-openapi-spec.ts`)

- **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.
- **Usage**: `npm run fetch:spec <url> <output-base-path>` or `ts-node --esm scripts/fetch-openapi-spec.ts <url> <output-base-path>`
- **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)
- **Dependencies**: `axios`, `js-yaml`.

## Adding New Features

### Adding a Tool

1.  **Directory**: `src/mcp-server/tools/yourToolName/`
2.  **Logic (`logic.ts`)**: Define input/output types, Zod schema, and core processing function. Use `ObsidianRestApiService` if interaction with Obsidian is needed.
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`.
4.  **Index (`index.ts`)**: Export registration function.
5.  **Server (`src/mcp-server/server.ts`)**: Import and call registration function within `createMcpServerInstance`, passing the instantiated services.

### Adding a Resource

1.  **Directory**: `src/mcp-server/resources/yourResourceName/`
2.  **Logic (`logic.ts`)**: Define params type, query schema (if needed), and core processing function (takes `uri: URL`, `params`). Use `ObsidianRestApiService` if needed.
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.
4.  **Index (`index.ts`)**: Export registration function.
5.  **Server (`src/mcp-server/server.ts`)**: Import and call registration function within `createMcpServerInstance`.

## Key File Locations

- **Main Entry**: `src/index.ts` (Initializes server, handles startup/shutdown)
- **Server Setup**: `src/mcp-server/server.ts` (Handles transport logic, session management, instantiates services, registers tools/resources)
- **HTTP Auth Middleware**: `src/mcp-server/transports/auth/` (contains strategies for JWT and OAuth)
- **Configuration**: `src/config/index.ts` (Loads env vars, package info, initializes logger, Obsidian API config)
- **Obsidian Service**: `src/services/obsidianRestAPI/` (Service, methods, types for Obsidian API)
- **Vault Cache Service**: `src/services/obsidianRestAPI/vaultCache/` (Service for caching vault structure)
- **Global Types**: `src/types-global/`
- **Utilities**: `src/utils/` (Main barrel file `index.ts` exporting from subdirs: `internal`, `metrics`, `parsing`, `security`, `obsidian`)
- **Tools**: `src/mcp-server/tools/` (Contains specific tool implementations like `obsidianReadFileTool`, `obsidianGlobalSearchTool`)
- **Resources**: `src/mcp-server/resources/` (Currently empty, place resource implementations here)
- **Client Config Example**: `mcp-client-config.example.json` (Example config for connecting clients)

Remember to keep this cheatsheet updated as the codebase evolves!

# obsidian-mcp-server - Directory Structure

Generated on: 2025-06-13 07:41:01

```
obsidian-mcp-server
├── .github
│   ├── workflows
│   │   └── publish.yml
│   └── FUNDING.yml
├── docs
│   ├── obsidian-api
│   │   ├── obsidian_rest_api_spec.json
│   │   └── obsidian_rest_api_spec.yaml
│   ├── obsidian_mcp_tools_spec.md
│   ├── obsidian_tools_phase2.md
│   └── tree.md
├── scripts
│   ├── clean.ts
│   ├── fetch-openapi-spec.ts
│   ├── make-executable.ts
│   └── tree.ts
├── src
│   ├── config
│   │   └── index.ts
│   ├── mcp-server
│   │   ├── tools
│   │   │   ├── obsidianDeleteFileTool
│   │   │   │   ├── index.ts
│   │   │   │   ├── logic.ts
│   │   │   │   └── registration.ts
│   │   │   ├── obsidianGlobalSearchTool
│   │   │   │   ├── index.ts
│   │   │   │   ├── logic.ts
│   │   │   │   └── registration.ts
│   │   │   ├── obsidianListFilesTool
│   │   │   │   ├── index.ts
│   │   │   │   ├── logic.ts
│   │   │   │   └── registration.ts
│   │   │   ├── obsidianManageFrontmatterTool
│   │   │   │   ├── index.ts
│   │   │   │   ├── logic.ts
│   │   │   │   └── registration.ts
│   │   │   ├── obsidianManageTagsTool
│   │   │   │   ├── index.ts
│   │   │   │   ├── logic.ts
│   │   │   │   └── registration.ts
│   │   │   ├── obsidianReadFileTool
│   │   │   │   ├── 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
│   │   │   │   ├── strategies
│   │   │   │   │   ├── jwt
│   │   │   │   │   │   └── jwtMiddleware.ts
│   │   │   │   │   └── oauth
│   │   │   │   │       └── oauthMiddleware.ts
│   │   │   │   └── index.ts
│   │   │   ├── httpErrorHandler.ts
│   │   │   ├── httpTransport.ts
│   │   │   └── stdioTransport.ts
│   │   └── server.ts
│   ├── services
│   │   └── obsidianRestAPI
│   │       ├── methods
│   │       │   ├── activeFileMethods.ts
│   │       │   ├── commandMethods.ts
│   │       │   ├── openMethods.ts
│   │       │   ├── patchMethods.ts
│   │       │   ├── periodicNoteMethods.ts
│   │       │   ├── searchMethods.ts
│   │       │   └── vaultMethods.ts
│   │       ├── vaultCache
│   │       │   ├── index.ts
│   │       │   └── service.ts
│   │       ├── index.ts
│   │       ├── service.ts
│   │       └── types.ts
│   ├── types-global
│   │   └── errors.ts
│   ├── utils
│   │   ├── 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
│   │   └── index.ts
│   └── index.ts
├── .clinerules
├── .gitignore
├── .ncurc.json
├── CHANGELOG.md
├── Dockerfile
├── env.json
├── LICENSE
├── mcp.json
├── package-lock.json
├── package.json
├── README.md
├── repomix.config.json
├── smithery.yaml
├── tsconfig.json
└── typedoc.json
```

_Note: This tree excludes files and directories matched by .gitignore and default patterns._

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# Obsidian MCP Server

[![TypeScript](https://img.shields.io/badge/TypeScript-^5.8.3-blue.svg)](https://www.typescriptlang.org/)
[![Model Context Protocol](https://img.shields.io/badge/MCP%20SDK-^1.13.0-green.svg)](https://modelcontextprotocol.io/)
[![Version](https://img.shields.io/badge/Version-2.0.7-blue.svg)](./CHANGELOG.md)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Status](https://img.shields.io/badge/Status-Production-brightgreen.svg)](https://github.com/cyanheads/obsidian-mcp-server/issues)
[![GitHub](https://img.shields.io/github/stars/cyanheads/obsidian-mcp-server?style=social)](https://github.com/cyanheads/obsidian-mcp-server)

**Empower your AI agents and development tools with seamless Obsidian integration!**

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).

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.

## 🚀 Core Capabilities: Obsidian Tools 🛠️

This server equips your AI with specialized tools to interact with your Obsidian vault:

| Tool Name                                                                              | Description                                                     | Key Features                                                                                                                                           |
| :------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`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).                   |
| [`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. |
| [`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.                                      |
| [`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.                                                           |
| [`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.                                                       |
| [`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.                                             |
| [`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.                                                                                            |
| [`obsidian_delete_note`](./src/mcp-server/tools/obsidianDeleteNoteTool/)               | Permanently deletes a specified note from the vault.            | - Case-insensitive path fallback for safety.                                                                                                           |

---

## Table of Contents

| [Overview](#overview) | [Features](#features) | [Configuration](#configuration) |
| [Project Structure](#project-structure) | [Vault Cache Service](#vault-cache-service) |
| [Tools](#tools) | [Resources](#resources) | [Development](#development) | [License](#license) |

## Overview

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.

Instead of complex scripting or manual interaction, your tools can leverage this server to:

- **Automate vault management**: Read notes, update content, manage frontmatter and tags, search across files, list directories, and delete files programmatically.
- **Integrate Obsidian into AI workflows**: Enable LLMs to access and modify your knowledge base as part of their research, writing, or coding tasks.
- **Build custom Obsidian tools**: Create external applications that interact with your vault data in novel ways.

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.

> **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.

## Features

### Core Utilities

Leverages the robust utilities provided by `cyanheads/mcp-ts-template`:

- **Logging**: Structured, configurable logging (file rotation, console, MCP notifications) with sensitive data redaction.
- **Error Handling**: Centralized error processing, standardized error types (`McpError`), and automatic logging.
- **Configuration**: Environment variable loading (`dotenv`) with comprehensive validation.
- **Input Validation/Sanitization**: Uses `zod` for schema validation and custom sanitization logic.
- **Request Context**: Tracking and correlation of operations via unique request IDs.
- **Type Safety**: Strong typing enforced by TypeScript and Zod schemas.
- **HTTP Transport Option**: Built-in Hono server with SSE, session management, CORS support, and pluggable authentication strategies (JWT and OAuth 2.1).

### Obsidian Integration

- **Obsidian Local REST API Integration**: Communicates directly with the Obsidian Local REST API plugin via HTTP requests managed by the `ObsidianRestApiService`.
- **Comprehensive Command Coverage**: Exposes key vault operations as MCP tools (see [Tools](#tools) section).
- **Vault Interaction**: Supports reading, updating (append, prepend, overwrite), searching (global text/regex, search/replace), listing, deleting, and managing frontmatter and tags.
- **Targeting Flexibility**: Tools can target files by path, the currently active file in Obsidian, or periodic notes (daily, weekly, etc.).
- **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.
- **Safety Features**: Case-insensitive path fallbacks for file operations, clear distinction between modification types (append, overwrite, etc.).

## Installation

### Prerequisites

1.  **Obsidian**: You need Obsidian installed.
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.
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.
4.  **Node.js & npm**: Ensure you have Node.js (v18 or later recommended) and npm installed.

## Configuration

### MCP Client Settings

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:

```json
{
  "mcpServers": {
    "obsidian-mcp-server": {
      "command": "npx",
      "args": ["obsidian-mcp-server"],
      "env": {
        "OBSIDIAN_API_KEY": "YOUR_API_KEY_FROM_OBSIDIAN_PLUGIN",
        "OBSIDIAN_BASE_URL": "http://127.0.0.1:27123",
        "OBSIDIAN_VERIFY_SSL": "false",
        "OBSIDIAN_ENABLE_CACHE": "true"
      },
      "disabled": false,
      "autoApprove": []
    }
  }
}
```

**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.

If you installed from source, change `command` and `args` to point to your local build:

```json
{
  "mcpServers": {
    "obsidian-mcp-server": {
      "command": "node",
      "args": ["/path/to/your/obsidian-mcp-server/dist/index.js"],
      "env": {
        "OBSIDIAN_API_KEY": "YOUR_OBSIDIAN_API_KEY",
        "OBSIDIAN_BASE_URL": "http://127.0.0.1:27123",
        "OBSIDIAN_VERIFY_SSL": "false",
        "OBSIDIAN_ENABLE_CACHE": "true"
      }
    }
  }
}
```

### Environment Variables

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).

| Variable                              | Description                                                              | Required             | Default                  |
| :------------------------------------ | :----------------------------------------------------------------------- | :------------------- | :----------------------- |
| **`OBSIDIAN_API_KEY`**                | API Key from the Obsidian Local REST API plugin.                         | **Yes**              | `undefined`              |
| **`OBSIDIAN_BASE_URL`**               | Base URL of your Obsidian Local REST API.                                | **Yes**              | `http://127.0.0.1:27123` |
| `MCP_TRANSPORT_TYPE`                  | Server transport: `stdio` or `http`.                                     | No                   | `stdio`                  |
| `MCP_HTTP_PORT`                       | Port for the HTTP server.                                                | No                   | `3010`                   |
| `MCP_HTTP_HOST`                       | Host for the HTTP server.                                                | No                   | `127.0.0.1`              |
| `MCP_ALLOWED_ORIGINS`                 | Comma-separated origins for CORS. **Set for production.**                | No                   | (none)                   |
| `MCP_AUTH_MODE`                       | Authentication strategy: `jwt` or `oauth`.                               | No                   | (none)                   |
| **`MCP_AUTH_SECRET_KEY`**             | 32+ char secret for JWT. **Required for `jwt` mode.**                    | **Yes (if `jwt`)**   | `undefined`              |
| `OAUTH_ISSUER_URL`                    | URL of the OAuth 2.1 issuer.                                             | **Yes (if `oauth`)** | `undefined`              |
| `OAUTH_AUDIENCE`                      | Audience claim for OAuth tokens.                                         | **Yes (if `oauth`)** | `undefined`              |
| `OAUTH_JWKS_URI`                      | URI for the JSON Web Key Set (optional, derived from issuer if omitted). | No                   | (derived)                |
| `MCP_LOG_LEVEL`                       | Logging level (`debug`, `info`, `error`, etc.).                          | No                   | `info`                   |
| `OBSIDIAN_VERIFY_SSL`                 | Set to `false` to disable SSL verification.                              | No                   | `true`                   |
| `OBSIDIAN_ENABLE_CACHE`               | Set to `true` to enable the in-memory vault cache.                       | No                   | `true`                   |
| `OBSIDIAN_CACHE_REFRESH_INTERVAL_MIN` | Refresh interval for the vault cache in minutes.                         | No                   | `10`                     |

### Connecting to the Obsidian API

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:

1.  **Encrypted (HTTPS) - Default**:

    - The plugin provides a secure `https://` endpoint (e.g., `https://127.0.0.1:27124`).
    - This uses a self-signed certificate, which will cause connection errors by default.
    - **To fix this**, you must set the `OBSIDIAN_VERIFY_SSL` environment variable to `"false"`. This tells the server to trust the self-signed certificate.

2.  **Non-encrypted (HTTP) - Recommended for Simplicity**:
    - In the plugin's settings within Obsidian, you can enable the "Non-encrypted (HTTP) Server".
    - This provides a simpler `http://` endpoint (e.g., `http://127.0.0.1:27123`).
    - When using this URL, you do not need to worry about SSL verification.

**Example `env` configuration for your MCP client:**

_Using the non-encrypted HTTP URL (recommended):_

```json
"env": {
  "OBSIDIAN_API_KEY": "YOUR_API_KEY_FROM_OBSIDIAN_PLUGIN",
  "OBSIDIAN_BASE_URL": "http://127.0.0.1:27123"
}
```

_Using the encrypted HTTPS URL:_

```json
"env": {
  "OBSIDIAN_API_KEY": "YOUR_API_KEY_FROM_OBSIDIAN_PLUGIN",
  "OBSIDIAN_BASE_URL": "https://127.0.0.1:27124",
  "OBSIDIAN_VERIFY_SSL": "false"
}
```

## Project Structure

The codebase follows a modular structure within the `src/` directory:

```
src/
├── index.ts           # Entry point: Initializes and starts the server
├── config/            # Configuration loading (env vars, package info)
│   └── index.ts
├── mcp-server/        # Core MCP server logic and capability registration
│   ├── server.ts      # Server setup, transport handling, tool/resource registration
│   ├── resources/     # MCP Resource implementations (currently none)
│   ├── tools/         # MCP Tool implementations (subdirs per tool)
│   └── transports/    # Stdio and HTTP transport logic
│       └── auth/      # Authentication strategies (JWT, OAuth)
├── services/          # Abstractions for external APIs or internal caching
│   └── obsidianRestAPI/ # Typed client for Obsidian Local REST API
├── types-global/      # Shared TypeScript type definitions (errors, etc.)
└── utils/             # Common utility functions (logger, error handler, security, etc.)
```

For a detailed file tree, run `npm run tree` or see [docs/tree.md](docs/tree.md).

## Vault Cache Service

This server includes an intelligent **in-memory cache** designed to enhance performance and resilience when interacting with your vault.

### Purpose and Benefits

- **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.
- **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.
- **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.

### How It Works

1.  **Initialization**: When enabled, the `VaultCacheService` builds an in-memory map of all `.md` files in your vault, storing their content and modification times.
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.
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.
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.

### Configuration

The cache is enabled by default but can be configured via environment variables:

- **`OBSIDIAN_ENABLE_CACHE`**: Set to `true` (default) or `false` to enable or disable the cache service.
- **`OBSIDIAN_CACHE_REFRESH_INTERVAL_MIN`**: Defines the interval in minutes for the periodic background refresh. Defaults to `10`.

## Tools

The Obsidian MCP Server provides a suite of tools for interacting with your vault, callable via the Model Context Protocol.

| Tool Name                     | Description                                               | Key Arguments                                                 |
| :---------------------------- | :-------------------------------------------------------- | :------------------------------------------------------------ |
| `obsidian_read_note`          | Retrieves the content and metadata of a note.             | `filePath`, `format?`, `includeStat?`                         |
| `obsidian_update_note`        | Modifies a file by appending, prepending, or overwriting. | `targetType`, `content`, `targetIdentifier?`, `wholeFileMode` |
| `obsidian_search_replace`     | Performs search-and-replace operations in a note.         | `targetType`, `replacements`, `useRegex?`, `replaceAll?`      |
| `obsidian_global_search`      | Searches the entire vault for content.                    | `query`, `searchInPath?`, `useRegex?`, `page?`, `pageSize?`   |
| `obsidian_list_notes`         | Lists notes and subdirectories in a folder.               | `dirPath`, `fileExtensionFilter?`, `nameRegexFilter?`         |
| `obsidian_manage_frontmatter` | Gets, sets, or deletes keys in a note's frontmatter.      | `filePath`, `operation`, `key`, `value?`                      |
| `obsidian_manage_tags`        | Adds, removes, or lists tags in a note.                   | `filePath`, `operation`, `tags`                               |
| `obsidian_delete_note`        | Permanently deletes a note from the vault.                | `filePath`                                                    |

_Note: All tools support comprehensive error handling and return structured JSON responses._

## Resources

**MCP Resources are not implemented in this version.**

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).

## Development

### Build and Test

To get started with development, clone the repository, install dependencies, and use the following scripts:

```bash
# Install dependencies
npm install

# Build the project (compile TS to JS in dist/ and make executable)
npm run rebuild

# Start the server locally using stdio transport
npm start:stdio

# Start the server using http transport
npm run start:http

# Format code using Prettier
npm run format

# Inspect the server's capabilities using the MCP Inspector tool
npm run inspect:stdio
# or for the http transport:
npm run inspect:http
```

## License

This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.

---

<div align="center">
Built with the <a href="https://modelcontextprotocol.io/">Model Context Protocol</a>
</div>

```

--------------------------------------------------------------------------------
/src/utils/metrics/index.ts:
--------------------------------------------------------------------------------

```typescript
export * from "./tokenCounter.js";

```

--------------------------------------------------------------------------------
/src/services/obsidianRestAPI/vaultCache/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Barrel file for the VaultCacheService.
 */
export * from "./service.js";

```

--------------------------------------------------------------------------------
/src/utils/security/index.ts:
--------------------------------------------------------------------------------

```typescript
export * from "./sanitization.js";
export * from "./rateLimiter.js";
export * from "./idGenerator.js";

```

--------------------------------------------------------------------------------
/src/utils/parsing/index.ts:
--------------------------------------------------------------------------------

```typescript
export * from "./jsonParser.js";
export * from "./dateParser.js";
// Removed export for dateUtils.js as it was moved

```

--------------------------------------------------------------------------------
/src/utils/internal/index.ts:
--------------------------------------------------------------------------------

```typescript
export * from "./errorHandler.js";
export * from "./logger.js";
export * from "./requestContext.js";
export * from "./asyncUtils.js";

```

--------------------------------------------------------------------------------
/src/utils/obsidian/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * Barrel file for Obsidian-specific utilities.
 */
export * from "./obsidianStatUtils.js";
export * from "./obsidianApiUtils.js";

```

--------------------------------------------------------------------------------
/src/mcp-server/tools/obsidianManageTagsTool/index.ts:
--------------------------------------------------------------------------------

```typescript
export {
  ObsidianManageTagsInputSchemaShape,
  processObsidianManageTags,
} from "./logic.js";
export type {
  ObsidianManageTagsInput,
  ObsidianManageTagsResponse,
} from "./logic.js";
export { registerObsidianManageTagsTool } from "./registration.js";

```

--------------------------------------------------------------------------------
/src/mcp-server/tools/obsidianManageFrontmatterTool/index.ts:
--------------------------------------------------------------------------------

```typescript
export {
  ObsidianManageFrontmatterInputSchemaShape,
  processObsidianManageFrontmatter,
} from "./logic.js";
export type {
  ObsidianManageFrontmatterInput,
  ObsidianManageFrontmatterResponse,
} from "./logic.js";
export { registerObsidianManageFrontmatterTool } from "./registration.js";

```

--------------------------------------------------------------------------------
/typedoc.json:
--------------------------------------------------------------------------------

```json
{
  "$schema": "https://typedoc.org/schema.json",
  "entryPoints": ["src", "scripts"],
  "entryPointStrategy": "expand",
  "out": "docs/api",
  "readme": "README.md",
  "name": "Obsidian MCP Server API Documentation",
  "includeVersion": true,
  "excludePrivate": true,
  "excludeProtected": true,
  "excludeInternal": true,
  "theme": "default"
}

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

```

--------------------------------------------------------------------------------
/repomix.config.json:
--------------------------------------------------------------------------------

```json
{
  "output": {
    "filePath": "repomix-output.xml",
    "style": "xml",
    "removeComments": false,
    "removeEmptyLines": false,
    "topFilesLength": 5,
    "showLineNumbers": false,
    "copyToClipboard": false
  },
  "include": [],
  "ignore": {
    "useGitignore": true,
    "useDefaultPatterns": true,
    "customPatterns": [".clinerules"]
  },
  "security": {
    "enableSecurityCheck": true
  }
}

```

--------------------------------------------------------------------------------
/src/mcp-server/transports/auth/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Barrel file for the auth module.
 * Exports core utilities and middleware strategies for easier imports.
 * @module src/mcp-server/transports/auth/index
 */

export { authContext } from "./core/authContext.js";
export { withRequiredScopes } from "./core/authUtils.js";
export type { AuthInfo } from "./core/authTypes.js";

export { mcpAuthMiddleware as jwtAuthMiddleware } from "./strategies/jwt/jwtMiddleware.js";
export { oauthMiddleware } from "./strategies/oauth/oauthMiddleware.js";

```

--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------

```yaml
name: Publish Package to npm
on:
  push:
    tags:
      - "v*"

jobs:
  build-and-publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20.x"
          registry-url: "https://registry.npmjs.org"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Publish to npm
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

```

--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------

```typescript
// Re-export all utilities from their categorized subdirectories
export * from "./internal/index.js";
export * from "./parsing/index.js";
export * from "./security/index.js";
export * from "./metrics/index.js";
export * from "./obsidian/index.js"; // Added export for obsidian utils

// It's good practice to have index.ts files in each subdirectory
// that export the contents of that directory.
// Assuming those will be created or already exist.
// If not, this might need adjustment to export specific files, e.g.:
// export * from './internal/errorHandler.js';
// export * from './internal/logger.js';
// ... etc.

```

--------------------------------------------------------------------------------
/src/mcp-server/tools/obsidianReadNoteTool/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Barrel file for the 'obsidian_read_note' MCP tool.
 *
 * This file serves as the public entry point for the obsidian_read_note tool module.
 * It re-exports the primary registration function (`registerObsidianReadNoteTool`)
 * from the './registration.js' module. This pattern simplifies imports for consumers
 * of the tool, allowing them to import necessary components from a single location.
 *
 * Consumers (like the main server setup) should import the registration function
 * from this file to integrate the tool into the MCP server instance.
 */
export { registerObsidianReadNoteTool } from "./registration.js";

```

--------------------------------------------------------------------------------
/src/mcp-server/tools/obsidianListNotesTool/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Barrel file for the 'obsidian_list_notes' MCP tool.
 *
 * This file serves as the public entry point for the obsidian_list_notes tool module.
 * It re-exports the primary registration function (`registerObsidianListNotesTool`)
 * from the './registration.js' module. This pattern simplifies imports for consumers
 * of the tool, allowing them to import necessary components from a single location.
 *
 * Consumers (like the main server setup) should import the registration function
 * from this file to integrate the tool into the MCP server instance.
 */
export { registerObsidianListNotesTool } from "./registration.js";

```

--------------------------------------------------------------------------------
/src/mcp-server/tools/obsidianDeleteNoteTool/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Barrel file for the 'obsidian_delete_note' MCP tool.
 *
 * This file serves as the public entry point for the obsidian_delete_note tool module.
 * It re-exports the primary registration function (`registerObsidianDeleteNoteTool`)
 * from the './registration.js' module. This pattern simplifies imports for consumers
 * of the tool, allowing them to import necessary components from a single location.
 *
 * Consumers (like the main server setup) should import the registration function
 * from this file to integrate the tool into the MCP server instance.
 */
export { registerObsidianDeleteNoteTool } from "./registration.js";

```

--------------------------------------------------------------------------------
/src/mcp-server/tools/obsidianUpdateNoteTool/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Barrel file for the 'obsidian_update_note' MCP tool.
 *
 * This file serves as the public entry point for the obsidian_update_note tool module.
 * It re-exports the primary registration function (`registerObsidianUpdateNoteTool`)
 * from the './registration.js' module. This pattern simplifies imports for consumers
 * of the tool, allowing them to import necessary components from a single location.
 *
 * Consumers (like the main server setup) should import the registration function
 * from this file to integrate the tool into the MCP server instance.
 */
export { registerObsidianUpdateNoteTool } from "./registration.js";

```

--------------------------------------------------------------------------------
/src/mcp-server/tools/obsidianSearchReplaceTool/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Barrel file for the 'obsidian_search_replace' MCP tool.
 *
 * This file serves as the public entry point for the obsidian_search_replace tool module.
 * It re-exports the primary registration function (`registerObsidianSearchReplaceTool`)
 * from the './registration.js' module. This pattern simplifies imports for consumers
 * of the tool, allowing them to import necessary components from a single location.
 *
 * Consumers (like the main server setup) should import the registration function
 * from this file to integrate the tool into the MCP server instance.
 */
export { registerObsidianSearchReplaceTool } from "./registration.js";

```

--------------------------------------------------------------------------------
/src/mcp-server/transports/auth/core/authTypes.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Shared types for authentication middleware.
 * @module src/mcp-server/transports/auth/core/auth.types
 */

import type { AuthInfo as SdkAuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";

/**
 * Defines the structure for authentication information derived from a token.
 * It extends the base SDK type to include common optional claims.
 */
export type AuthInfo = SdkAuthInfo & {
  subject?: string;
};

// Extend the Node.js IncomingMessage type to include an optional 'auth' property.
// This is necessary for type-safe access when attaching the AuthInfo.
declare module "http" {
  interface IncomingMessage {
    auth?: AuthInfo;
  }
}

```

--------------------------------------------------------------------------------
/src/mcp-server/tools/obsidianGlobalSearchTool/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Barrel file for the 'obsidian_global_search' MCP tool.
 *
 * This file serves as the public entry point for the obsidian_global_search tool module.
 * It re-exports the primary registration function (`registerObsidianGlobalSearchTool`)
 * from the './registration.js' module. This pattern simplifies imports for consumers
 * of the tool, allowing them to import necessary components from a single location.
 *
 * Consumers (like the main server setup) should import the registration function
 * from this file to integrate the tool into the MCP server instance.
 */

export { registerObsidianGlobalSearchTool } from "./registration.js"; // Ensure '.js' extension for ES module resolution

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
build:
  dockerBuildPath: . # Explicitly set build context to the root directory
startCommand:
  type: stdio
  configSchema:
    type: object
    required:
      - obsidianApiKey
      - obsidianBaseUrl
    properties:
      obsidianApiKey:
        type: string
        description: The API key generated by the Obsidian Local REST API plugin.
      obsidianBaseUrl:
        type: string
        format: uri # Ensure it's a valid URL format
        description: The base URL of your Obsidian Local REST API (e.g., http://127.0.0.1:27123).
  commandFunction: |
    (config) => ({
      command: 'npx',
      args: ['obsidian-mcp-server'],
      env: { 
        OBSIDIAN_API_KEY: config.obsidianApiKey,
        OBSIDIAN_BASE_URL: config.obsidianBaseUrl,
        OBSIDIAN_VERIFY_SSL: "false"
      }
    })

```

--------------------------------------------------------------------------------
/mcp.json:
--------------------------------------------------------------------------------

```json
{
  "mcpServers": {
    "obsidian-mcp-server-stdio": {
      "command": "npx",
      "args": ["obsidian-mcp-server"],
      "env": {
        "OBSIDIAN_API_KEY": "YOUR_OBSIDIAN_API_KEY_HERE",
        "OBSIDIAN_BASE_URL": "http://127.0.0.1:27123",
        "MCP_TRANSPORT_TYPE": "stdio",
        "MCP_LOG_LEVEL": "debug"
      }
    },
    "obsidian-mcp-server-http": {
      "command": "npx",
      "args": ["obsidian-mcp-server"],
      "env": {
        "OBSIDIAN_API_KEY": "YOUR_OBSIDIAN_API_KEY_HERE",
        "OBSIDIAN_BASE_URL": "http://127.0.0.1:27123",
        "MCP_TRANSPORT_TYPE": "http",
        "MCP_HTTP_PORT": "3010",
        "MCP_HTTP_HOST": "127.0.0.1",
        "MCP_LOG_LEVEL": "debug",
        "MCP_AUTH_SECRET_KEY": "YOUR_MIN_32_CHAR_SECRET_KEY_HERE_IF_USING_HTTP_AUTH"
      }
    }
  }
}

```

--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------

```yaml
# These are supported funding model platforms

github: cyanheads
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: cyanheads
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

```

--------------------------------------------------------------------------------
/src/services/obsidianRestAPI/index.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @module ObsidianRestApiService Barrel File
 * @description
 * Exports the singleton instance of the Obsidian REST API service and related types.
 */

export * from "./types.js"; // Export all types
// Removed singleton export
export { ObsidianRestApiService } from "./service.js"; // Export the class itself
// Export method modules if direct access is desired, though typically accessed via service instance
export * as activeFileMethods from "./methods/activeFileMethods.js";
export * as commandMethods from "./methods/commandMethods.js";
export * as openMethods from "./methods/openMethods.js";
export * as patchMethods from "./methods/patchMethods.js";
export * as periodicNoteMethods from "./methods/periodicNoteMethods.js";
export * as searchMethods from "./methods/searchMethods.js";
export * as vaultMethods from "./methods/vaultMethods.js";
export { VaultCacheService } from "./vaultCache/index.js";

```

--------------------------------------------------------------------------------
/src/services/obsidianRestAPI/methods/openMethods.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @module OpenMethods
 * @description
 * Methods for opening files in Obsidian via the REST API.
 */

import { RequestContext } from "../../../utils/index.js";
import { RequestFunction } from "../types.js";

/**
 * Opens a specific file in Obsidian. Creates the file if it doesn't exist.
 * @param _request - The internal request function from the service instance.
 * @param filePath - Vault-relative path to the file.
 * @param newLeaf - Whether to open the file in a new editor tab (leaf).
 * @param context - Request context.
 * @returns {Promise<void>} Resolves on success (200 OK, but no body expected).
 */
export async function openFile(
  _request: RequestFunction,
  filePath: string,
  newLeaf: boolean = false,
  context: RequestContext,
): Promise<void> {
  // This endpoint returns 200 OK, not 204
  await _request<void>(
    {
      method: "POST",
      url: `/open/${encodeURIComponent(filePath)}`,
      params: { newLeaf },
    },
    context,
    "openFile",
  );
}

```

--------------------------------------------------------------------------------
/env.json:
--------------------------------------------------------------------------------

```json
{
  "properties": {
    "MAX_TOKENS": {
      "default": "20000",
      "description": "Maximum tokens per response",
      "type": "string"
    },
    "NODE_ENV": {
      "default": "production",
      "description": "The Node.js environment setting",
      "type": "string"
    },
    "OBSIDIAN_API_KEY": {
      "description": "Your API key for the Obsidian MCP Server",
      "type": "string"
    },
    "OBSIDIAN_VERIFY_SSL": {
      "default": "false",
      "description": "Enable SSL verification",
      "type": "string"
    },
    "RATE_LIMIT_MAX_REQUESTS": {
      "default": "200",
      "description": "Max requests per rate limit window",
      "type": "string"
    },
    "RATE_LIMIT_WINDOW_MS": {
      "default": "900000",
      "description": "Rate limit window in milliseconds (default: 15 minutes)",
      "type": "string"
    },
    "TOOL_TIMEOUT_MS": {
      "default": "60000",
      "description": "Tool execution timeout in milliseconds",
      "type": "string"
    }
  },
  "required": ["OBSIDIAN_API_KEY"],
  "type": "object"
}

```

--------------------------------------------------------------------------------
/src/mcp-server/transports/auth/core/authContext.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Defines the AsyncLocalStorage context for authentication information.
 * This module provides a mechanism to store and retrieve authentication details
 * (like scopes and client ID) across asynchronous operations, making it available
 * from the middleware layer down to the tool and resource handlers without
 * drilling props.
 *
 * @module src/mcp-server/transports/auth/core/authContext
 */

import { AsyncLocalStorage } from "async_hooks";
import type { AuthInfo } from "./authTypes.js";

/**
 * Defines the structure of the store used within the AsyncLocalStorage.
 * It holds the authentication information for the current request context.
 */
interface AuthStore {
  authInfo: AuthInfo;
}

/**
 * An instance of AsyncLocalStorage to hold the authentication context (`AuthStore`).
 * This allows `authInfo` to be accessible throughout the async call chain of a request
 * after being set in the authentication middleware.
 *
 * @example
 * // In middleware:
 * await authContext.run({ authInfo }, next);
 *
 * // In a deeper handler:
 * const store = authContext.getStore();
 * const scopes = store?.authInfo.scopes;
 */
export const authContext = new AsyncLocalStorage<AuthStore>();

```

--------------------------------------------------------------------------------
/src/utils/obsidian/obsidianApiUtils.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @module ObsidianApiUtils
 * @description
 * Internal utilities for the Obsidian REST API service.
 */

/**
 * Encodes a vault-relative file path correctly for API URLs.
 * Ensures path separators '/' are not encoded, but individual components are.
 * Handles leading slashes correctly.
 *
 * @param filePath - The raw vault-relative file path (e.g., "/Notes/My File.md" or "Notes/My File.md").
 * @returns The URL-encoded path suitable for appending to `/vault`.
 */
export function encodeVaultPath(filePath: string): string {
  // 1. Trim whitespace and remove any leading/trailing slashes for consistent processing.
  const trimmedPath = filePath.trim().replace(/^\/+|\/+$/g, "");

  // 2. If the original path was just '/' or empty, return an empty string (represents root for files).
  if (trimmedPath === "") {
    // For file operations, the API expects /vault/filename.md at the root,
    // so an empty encoded path segment is correct here.
    // For listFiles, we handle the root case separately.
    return "";
  }

  // 3. Split into components, encode each component, then rejoin with literal '/'.
  const encodedComponents = trimmedPath.split("/").map(encodeURIComponent);
  const encodedPath = encodedComponents.join("/");

  // 4. Prepend the leading slash.
  return `/${encodedPath}`;
}

```

--------------------------------------------------------------------------------
/src/services/obsidianRestAPI/methods/commandMethods.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @module CommandMethods
 * @description
 * Methods for interacting with Obsidian commands via the REST API.
 */

import { RequestContext } from "../../../utils/index.js";
import {
  ObsidianCommand,
  CommandListResponse,
  RequestFunction,
} from "../types.js";

/**
 * Executes a registered Obsidian command by its ID.
 * @param _request - The internal request function from the service instance.
 * @param commandId - The ID of the command (e.g., "app:go-back").
 * @param context - Request context.
 * @returns {Promise<void>} Resolves on success (204 No Content).
 */
export async function executeCommand(
  _request: RequestFunction,
  commandId: string,
  context: RequestContext,
): Promise<void> {
  await _request<void>(
    {
      method: "POST",
      url: `/commands/${encodeURIComponent(commandId)}/`,
    },
    context,
    "executeCommand",
  );
}

/**
 * Lists all available Obsidian commands.
 * @param _request - The internal request function from the service instance.
 * @param context - Request context.
 * @returns A list of available commands.
 */
export async function listCommands(
  _request: RequestFunction,
  context: RequestContext,
): Promise<ObsidianCommand[]> {
  const response = await _request<CommandListResponse>(
    {
      method: "GET",
      url: "/commands/",
    },
    context,
    "listCommands",
  );
  return response.commands;
}

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
# ---- Base Node ----
# Use a specific Node.js version known to work, Alpine for smaller size
FROM node:23-alpine AS base
WORKDIR /usr/src/app
ENV NODE_ENV=production

# ---- Dependencies ----
# Install dependencies first to leverage Docker cache
FROM base AS deps
WORKDIR /usr/src/app
COPY package.json package-lock.json* ./
# Use npm ci for deterministic installs based on lock file
# Install only production dependencies in this stage for the final image
RUN npm ci --only=production

# ---- Builder ----
# Build the application
FROM base AS builder
WORKDIR /usr/src/app
# Copy dependency manifests and install *all* dependencies (including dev)
COPY package.json package-lock.json* ./
RUN npm ci
# Copy the rest of the source code
COPY . .
# Build the TypeScript project
RUN npm run build

# ---- Runner ----
# Final stage with only production dependencies and built code
FROM base AS runner
WORKDIR /usr/src/app
# Copy production node_modules from the 'deps' stage
COPY --from=deps /usr/src/app/node_modules ./node_modules
# Copy built application from the 'builder' stage
COPY --from=builder /usr/src/app/dist ./dist
# Copy package.json (needed for potential runtime info, like version)
COPY package.json .

# Create a non-root user and switch to it
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

# Expose port if the application runs a server (adjust if needed)
# EXPOSE 3000

# Command to run the application
CMD ["node", "dist/index.js"]

```

--------------------------------------------------------------------------------
/src/services/obsidianRestAPI/methods/searchMethods.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @module SearchMethods
 * @description
 * Methods for performing searches via the Obsidian REST API.
 */

import { RequestContext } from "../../../utils/index.js";
import {
  SimpleSearchResult,
  ComplexSearchResult,
  RequestFunction,
} from "../types.js";

/**
 * Performs a simple text search across the vault.
 * @param _request - The internal request function from the service instance.
 * @param query - The text query string.
 * @param contextLength - Number of characters surrounding each match (default 100).
 * @param context - Request context.
 * @returns An array of search results.
 */
export async function searchSimple(
  _request: RequestFunction,
  query: string,
  contextLength: number = 100,
  context: RequestContext,
): Promise<SimpleSearchResult[]> {
  return _request<SimpleSearchResult[]>(
    {
      method: "POST",
      url: "/search/simple/",
      params: { query, contextLength }, // Send as query parameters
    },
    context,
    "searchSimple",
  );
}

/**
 * Performs a complex search using Dataview DQL or JsonLogic.
 * @param _request - The internal request function from the service instance.
 * @param query - The query string (DQL) or JSON object (JsonLogic).
 * @param contentType - The content type header indicating the query format.
 * @param context - Request context.
 * @returns An array of search results.
 */
export async function searchComplex(
  _request: RequestFunction,
  query: string | object,
  contentType:
    | "application/vnd.olrapi.dataview.dql+txt"
    | "application/vnd.olrapi.jsonlogic+json",
  context: RequestContext,
): Promise<ComplexSearchResult[]> {
  return _request<ComplexSearchResult[]>(
    {
      method: "POST",
      url: "/search/",
      headers: { "Content-Type": contentType },
      data: query,
    },
    context,
    "searchComplex",
  );
}

```

--------------------------------------------------------------------------------
/src/mcp-server/transports/httpErrorHandler.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Centralized error handler for the Hono HTTP transport.
 * This middleware intercepts errors that occur during request processing,
 * standardizes them using the application's ErrorHandler utility, and
 * formats them into a consistent JSON-RPC error response.
 * @module src/mcp-server/transports/httpErrorHandler
 */

import { Context } from "hono";
import { StatusCode } from "hono/utils/http-status";
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
import { ErrorHandler, requestContextService } from "../../utils/index.js";

/**
 * A centralized error handling middleware for Hono.
 * This function is registered with `app.onError()` and will catch any errors
 * thrown from preceding middleware or route handlers.
 *
 * @param err - The error that was thrown.
 * @param c - The Hono context object for the request.
 * @returns A Response object containing the formatted JSON-RPC error.
 */
export const httpErrorHandler = async (err: Error, c: Context) => {
  const context = requestContextService.createRequestContext({
    operation: "httpErrorHandler",
    path: c.req.path,
    method: c.req.method,
  });

  const handledError = ErrorHandler.handleError(err, {
    operation: "httpTransport",
    context,
  });

  let status = 500;
  if (handledError instanceof McpError) {
    switch (handledError.code) {
      case BaseErrorCode.NOT_FOUND:
        status = 404;
        break;
      case BaseErrorCode.UNAUTHORIZED:
        status = 401;
        break;
      case BaseErrorCode.FORBIDDEN:
        status = 403;
        break;
      case BaseErrorCode.VALIDATION_ERROR:
        status = 400;
        break;
      case BaseErrorCode.CONFLICT:
        status = 409;
        break;
      case BaseErrorCode.RATE_LIMITED:
        status = 429;
        break;
      default:
        status = 500;
    }
  }

  // Attempt to get the request ID from the body, but don't fail if it's not there or unreadable.
  let requestId: string | number | null = null;
  try {
    const body = await c.req.json();
    requestId = body?.id || null;
  } catch {
    // Ignore parsing errors, requestId will remain null
  }

  const errorCode =
    handledError instanceof McpError ? handledError.code : -32603;

  c.status(status as StatusCode);
  return c.json({
    jsonrpc: "2.0",
    error: {
      code: errorCode,
      message: handledError.message,
    },
    id: requestId,
  });
};

```

--------------------------------------------------------------------------------
/src/mcp-server/transports/auth/core/authUtils.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Provides utility functions for authorization, specifically for
 * checking token scopes against required permissions for a given operation.
 * @module src/mcp-server/transports/auth/core/authUtils
 */

import { BaseErrorCode, McpError } from "../../../../types-global/errors.js";
import { logger, requestContextService } from "../../../../utils/index.js";
import { authContext } from "./authContext.js";

/**
 * Checks if the current authentication context contains all the specified scopes.
 * This function is designed to be called within tool or resource handlers to
 * enforce scope-based access control. It retrieves the authentication information
 * from `authContext` (AsyncLocalStorage).
 *
 * @param requiredScopes - An array of scope strings that are mandatory for the operation.
 * @throws {McpError} Throws an error with `BaseErrorCode.INTERNAL_ERROR` if the
 *   authentication context is missing, which indicates a server configuration issue.
 * @throws {McpError} Throws an error with `BaseErrorCode.FORBIDDEN` if one or
 *   more required scopes are not present in the validated token.
 */
export function withRequiredScopes(requiredScopes: string[]): void {
  const store = authContext.getStore();

  if (!store || !store.authInfo) {
    // This is a server-side logic error; the auth middleware should always populate this.
    throw new McpError(
      BaseErrorCode.INTERNAL_ERROR,
      "Authentication context is missing. This indicates a server configuration error.",
      requestContextService.createRequestContext({
        operation: "withRequiredScopesCheck",
        error: "AuthStore not found in AsyncLocalStorage.",
      }),
    );
  }

  const { scopes: grantedScopes, clientId } = store.authInfo;
  const grantedScopeSet = new Set(grantedScopes);

  const missingScopes = requiredScopes.filter(
    (scope) => !grantedScopeSet.has(scope),
  );

  if (missingScopes.length > 0) {
    const context = requestContextService.createRequestContext({
      operation: "withRequiredScopesCheck",
      required: requiredScopes,
      granted: grantedScopes,
      missing: missingScopes,
      clientId: clientId,
      subject: store.authInfo.subject,
    });
    logger.warning("Authorization failed: Missing required scopes.", context);
    throw new McpError(
      BaseErrorCode.FORBIDDEN,
      `Insufficient permissions. Missing required scopes: ${missingScopes.join(", ")}`,
      { requiredScopes, missingScopes },
    );
  }
}

```

--------------------------------------------------------------------------------
/src/services/obsidianRestAPI/methods/activeFileMethods.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @module ActiveFileMethods
 * @description
 * Methods for interacting with the currently active file in Obsidian via the REST API.
 */

import { RequestContext } from "../../../utils/index.js";
import { NoteJson, RequestFunction } from "../types.js";

/**
 * Gets the content of the currently active file in Obsidian.
 * @param _request - The internal request function from the service instance.
 * @param format - 'markdown' or 'json' (for NoteJson).
 * @param context - Request context.
 * @returns The file content (string) or NoteJson object.
 */
export async function getActiveFile(
  _request: RequestFunction,
  format: "markdown" | "json" = "markdown",
  context: RequestContext,
): Promise<string | NoteJson> {
  const acceptHeader =
    format === "json" ? "application/vnd.olrapi.note+json" : "text/markdown";
  return _request<string | NoteJson>(
    {
      method: "GET",
      url: `/active/`,
      headers: { Accept: acceptHeader },
    },
    context,
    "getActiveFile",
  );
}

/**
 * Updates (overwrites) the content of the currently active file.
 * @param _request - The internal request function from the service instance.
 * @param content - The new content.
 * @param context - Request context.
 * @returns {Promise<void>} Resolves on success (204 No Content).
 */
export async function updateActiveFile(
  _request: RequestFunction,
  content: string,
  context: RequestContext,
): Promise<void> {
  await _request<void>(
    {
      method: "PUT",
      url: `/active/`,
      headers: { "Content-Type": "text/markdown" },
      data: content,
    },
    context,
    "updateActiveFile",
  );
}

/**
 * Appends content to the end of the currently active file.
 * @param _request - The internal request function from the service instance.
 * @param content - The content to append.
 * @param context - Request context.
 * @returns {Promise<void>} Resolves on success (204 No Content).
 */
export async function appendActiveFile(
  _request: RequestFunction,
  content: string,
  context: RequestContext,
): Promise<void> {
  await _request<void>(
    {
      method: "POST",
      url: `/active/`,
      headers: { "Content-Type": "text/markdown" },
      data: content,
    },
    context,
    "appendActiveFile",
  );
}

/**
 * Deletes the currently active file.
 * @param _request - The internal request function from the service instance.
 * @param context - Request context.
 * @returns {Promise<void>} Resolves on success (204 No Content).
 */
export async function deleteActiveFile(
  _request: RequestFunction,
  context: RequestContext,
): Promise<void> {
  await _request<void>(
    {
      method: "DELETE",
      url: `/active/`,
    },
    context,
    "deleteActiveFile",
  );
}

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "obsidian-mcp-server",
  "version": "2.0.7",
  "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.",
  "main": "dist/index.js",
  "files": [
    "dist",
    "README.md",
    "LICENSE",
    "CHANGELOG.md"
  ],
  "bin": {
    "obsidian-mcp-server": "dist/index.js"
  },
  "type": "module",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/cyanheads/obsidian-mcp-server.git"
  },
  "bugs": {
    "url": "https://github.com/cyanheads/obsidian-mcp-server/issues"
  },
  "homepage": "https://github.com/cyanheads/obsidian-mcp-server#readme",
  "scripts": {
    "build": "tsc && node --loader ts-node/esm scripts/make-executable.ts dist/index.js",
    "start": "node dist/index.js",
    "start:stdio": "MCP_LOG_LEVEL=debug MCP_TRANSPORT_TYPE=stdio node dist/index.js",
    "start:http": "MCP_LOG_LEVEL=debug MCP_TRANSPORT_TYPE=http node dist/index.js",
    "rebuild": "npx ts-node --esm scripts/clean.ts && npm run build",
    "fetch:spec": "npx ts-node --esm scripts/fetch-openapi-spec.ts",
    "docs:generate": "typedoc --tsconfig ./tsconfig.typedoc.json",
    "tree": "npx ts-node --esm scripts/tree.ts",
    "format": "prettier --write \"**/*.{ts,js,json,md,html,css}\"",
    "inspect": "mcp-inspector --config mcp.json",
    "inspect:stdio": "mcp-inspector --config mcp.json --server obsidian-mcp-server-stdio",
    "inspect:http": "mcp-inspector --config mcp.json --server obsidian-mcp-server-http"
  },
  "dependencies": {
    "@hono/node-server": "^1.14.4",
    "@modelcontextprotocol/inspector": "^0.14.3",
    "@modelcontextprotocol/sdk": "^1.13.0",
    "@types/sanitize-html": "^2.16.0",
    "@types/validator": "13.15.2",
    "axios": "^1.10.0",
    "chrono-node": "2.8.0",
    "date-fns": "^4.1.0",
    "dotenv": "^16.5.0",
    "hono": "^4.8.2",
    "ignore": "^7.0.5",
    "jose": "^6.0.11",
    "js-yaml": "^4.1.0",
    "openai": "^5.6.0",
    "partial-json": "^0.1.7",
    "sanitize-html": "^2.17.0",
    "tiktoken": "^1.0.21",
    "ts-node": "^10.9.2",
    "typescript": "^5.8.3",
    "validator": "13.15.15",
    "winston": "^3.17.0",
    "winston-transport": "^4.9.0",
    "zod": "^3.25.67"
  },
  "keywords": [
    "mcp",
    "model-context-protocol",
    "obsidian",
    "obsidian-md",
    "ai",
    "llm",
    "agent",
    "automation",
    "api",
    "server",
    "typescript",
    "knowledge-management",
    "note-taking",
    "rest-api",
    "integration"
  ],
  "author": "Casey Hand @cyanheads",
  "license": "Apache-2.0",
  "engines": {
    "node": ">=16.0.0"
  },
  "devDependencies": {
    "@types/js-yaml": "^4.0.9",
    "@types/node": "^24.0.3",
    "prettier": "^3.5.3",
    "typedoc": "^0.28.5"
  }
}

```

--------------------------------------------------------------------------------
/src/services/obsidianRestAPI/methods/periodicNoteMethods.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @module PeriodicNoteMethods
 * @description
 * Methods for interacting with periodic notes (daily, weekly, etc.) via the Obsidian REST API.
 */

import { RequestContext } from "../../../utils/index.js";
import { NoteJson, Period, RequestFunction } from "../types.js";

/**
 * Gets the content of a periodic note (daily, weekly, etc.).
 * @param _request - The internal request function from the service instance.
 * @param period - The period type ('daily', 'weekly', 'monthly', 'quarterly', 'yearly').
 * @param format - 'markdown' or 'json'.
 * @param context - Request context.
 * @returns The note content or NoteJson.
 */
export async function getPeriodicNote(
  _request: RequestFunction,
  period: Period,
  format: "markdown" | "json" = "markdown",
  context: RequestContext,
): Promise<string | NoteJson> {
  const acceptHeader =
    format === "json" ? "application/vnd.olrapi.note+json" : "text/markdown";
  return _request<string | NoteJson>(
    {
      method: "GET",
      url: `/periodic/${period}/`,
      headers: { Accept: acceptHeader },
    },
    context,
    "getPeriodicNote",
  );
}

/**
 * Updates (overwrites) the content of a periodic note. Creates if needed.
 * @param _request - The internal request function from the service instance.
 * @param period - The period type.
 * @param content - The new content.
 * @param context - Request context.
 * @returns {Promise<void>} Resolves on success (204 No Content).
 */
export async function updatePeriodicNote(
  _request: RequestFunction,
  period: Period,
  content: string,
  context: RequestContext,
): Promise<void> {
  await _request<void>(
    {
      method: "PUT",
      url: `/periodic/${period}/`,
      headers: { "Content-Type": "text/markdown" },
      data: content,
    },
    context,
    "updatePeriodicNote",
  );
}

/**
 * Appends content to a periodic note. Creates if needed.
 * @param _request - The internal request function from the service instance.
 * @param period - The period type.
 * @param content - The content to append.
 * @param context - Request context.
 * @returns {Promise<void>} Resolves on success (204 No Content).
 */
export async function appendPeriodicNote(
  _request: RequestFunction,
  period: Period,
  content: string,
  context: RequestContext,
): Promise<void> {
  await _request<void>(
    {
      method: "POST",
      url: `/periodic/${period}/`,
      headers: { "Content-Type": "text/markdown" },
      data: content,
    },
    context,
    "appendPeriodicNote",
  );
}

/**
 * Deletes a periodic note.
 * @param _request - The internal request function from the service instance.
 * @param period - The period type.
 * @param context - Request context.
 * @returns {Promise<void>} Resolves on success (204 No Content).
 */
export async function deletePeriodicNote(
  _request: RequestFunction,
  period: Period,
  context: RequestContext,
): Promise<void> {
  await _request<void>(
    {
      method: "DELETE",
      url: `/periodic/${period}/`,
    },
    context,
    "deletePeriodicNote",
  );
}

```

--------------------------------------------------------------------------------
/scripts/clean.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

/**
 * @fileoverview Utility script to clean build artifacts and temporary directories.
 * @module scripts/clean
 *   By default, it removes the 'dist' and 'logs' directories.
 *   Custom directories can be specified as command-line arguments.
 *   Works on all platforms using Node.js path normalization.
 *
 * @example
 * // Add to package.json:
 * // "scripts": {
 * //   "clean": "ts-node --esm scripts/clean.ts",
 * //   "rebuild": "npm run clean && npm run build"
 * // }
 *
 * // Run with default directories:
 * // npm run clean
 *
 * // Run with custom directories:
 * // ts-node --esm scripts/clean.ts temp coverage
 */

import { rm, access } from "fs/promises";
import { join } from "path";

/**
 * Represents the result of a clean operation for a single directory.
 * @property dir - The name of the directory targeted for cleaning.
 * @property status - Indicates if the cleaning was successful or skipped.
 * @property reason - If skipped, the reason why.
 */
interface CleanResult {
  dir: string;
  status: "success" | "skipped";
  reason?: string;
}

/**
 * Asynchronously checks if a directory exists at the given path.
 * @param dirPath - The absolute or relative path to the directory.
 * @returns A promise that resolves to `true` if the directory exists, `false` otherwise.
 */
async function directoryExists(dirPath: string): Promise<boolean> {
  try {
    await access(dirPath);
    return true;
  } catch {
    return false;
  }
}

/**
 * Main function to perform the cleaning operation.
 * It reads command line arguments for target directories or uses defaults ('dist', 'logs').
 * Reports the status of each cleaning attempt.
 */
const clean = async (): Promise<void> => {
  try {
    let dirsToClean: string[] = ["dist", "logs"];
    const args = process.argv.slice(2);

    if (args.length > 0) {
      dirsToClean = args;
    }

    console.log(`Attempting to clean directories: ${dirsToClean.join(", ")}`);

    const results = await Promise.allSettled(
      dirsToClean.map(async (dir): Promise<CleanResult> => {
        const dirPath = join(process.cwd(), dir);

        try {
          const exists = await directoryExists(dirPath);

          if (!exists) {
            return { dir, status: "skipped", reason: "does not exist" };
          }

          await rm(dirPath, { recursive: true, force: true });
          return { dir, status: "success" };
        } catch (error) {
          // Rethrow to be caught by Promise.allSettled's rejection case
          throw error;
        }
      }),
    );

    results.forEach((result) => {
      if (result.status === "fulfilled") {
        const { dir, status, reason } = result.value;
        if (status === "success") {
          console.log(`Successfully cleaned directory: ${dir}`);
        } else {
          console.log(`Skipped cleaning directory ${dir}: ${reason}.`);
        }
      } else {
        // The error here is the actual error object from the rejected promise
        console.error(
          `Error cleaning a directory (details below):\n`,
          result.reason,
        );
      }
    });
  } catch (error) {
    console.error(
      "An unexpected error occurred during the clean script execution:",
      error instanceof Error ? error.message : error,
    );
    process.exit(1);
  }
};

clean();

```

--------------------------------------------------------------------------------
/src/mcp-server/transports/stdioTransport.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Handles the setup and connection for the Stdio MCP transport.
 * Implements the MCP Specification 2025-03-26 for stdio transport.
 * This transport communicates directly over standard input (stdin) and
 * standard output (stdout), typically used when the MCP server is launched
 * as a child process by a host application.
 *
 * Specification Reference:
 * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#stdio
 *
 * --- Authentication Note ---
 * As per the MCP Authorization Specification (2025-03-26, Section 1.2),
 * STDIO transports SHOULD NOT implement HTTP-based authentication flows.
 * Authorization is typically handled implicitly by the host application
 * controlling the server process. This implementation follows that guideline.
 *
 * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
 * @module src/mcp-server/transports/stdioTransport
 */

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { ErrorHandler, logger, RequestContext } from "../../utils/index.js";

/**
 * Connects a given `McpServer` instance to the Stdio transport.
 * This function initializes the SDK's `StdioServerTransport`, which manages
 * communication over `process.stdin` and `process.stdout` according to the
 * MCP stdio transport specification.
 *
 * MCP Spec Points Covered by SDK's `StdioServerTransport`:
 * - Reads JSON-RPC messages (requests, notifications, responses, batches) from stdin.
 * - Writes JSON-RPC messages to stdout.
 * - Handles newline delimiters and ensures no embedded newlines in output messages.
 * - Ensures only valid MCP messages are written to stdout.
 *
 * Logging via the `logger` utility MAY result in output to stderr, which is
 * permitted by the spec for logging purposes.
 *
 * @param server - The `McpServer` instance.
 * @param parentContext - The logging and tracing context from the calling function.
 * @returns A promise that resolves when the Stdio transport is successfully connected.
 * @throws {Error} If the connection fails during setup.
 */
export async function connectStdioTransport(
  server: McpServer,
  parentContext: RequestContext,
): Promise<void> {
  const operationContext = {
    ...parentContext,
    operation: "connectStdioTransport",
    transportType: "Stdio",
  };
  logger.debug("Attempting to connect stdio transport...", operationContext);

  try {
    logger.debug("Creating StdioServerTransport instance...", operationContext);
    const transport = new StdioServerTransport();

    logger.debug(
      "Connecting McpServer instance to StdioServerTransport...",
      operationContext,
    );
    await server.connect(transport);

    logger.info(
      "MCP Server connected and listening via stdio transport.",
      operationContext,
    );
    if (process.stdout.isTTY) {
      console.log(
        `\n🚀 MCP Server running in STDIO mode.\n   (MCP Spec: 2025-03-26 Stdio Transport)\n`,
      );
    }
  } catch (err) {
    ErrorHandler.handleError(err, { ...operationContext, critical: true });
    throw err; // Re-throw after handling to allow caller to react if necessary
  }
}

```

--------------------------------------------------------------------------------
/src/types-global/errors.ts:
--------------------------------------------------------------------------------

```typescript
import { z } from "zod";

/**
 * Defines a set of standardized error codes for common issues within MCP servers or tools.
 * These codes help clients understand the nature of an error programmatically.
 */
export enum BaseErrorCode {
  /** Access denied due to invalid credentials or lack of authentication. */
  UNAUTHORIZED = "UNAUTHORIZED",
  /** Access denied despite valid authentication, due to insufficient permissions. */
  FORBIDDEN = "FORBIDDEN",
  /** The requested resource or entity could not be found. */
  NOT_FOUND = "NOT_FOUND",
  /** The request could not be completed due to a conflict with the current state of the resource. */
  CONFLICT = "CONFLICT",
  /** The request failed due to invalid input parameters or data. */
  VALIDATION_ERROR = "VALIDATION_ERROR",
  /** An error occurred while parsing input data (e.g., date string, JSON). */
  PARSING_ERROR = "PARSING_ERROR",
  /** The request was rejected because the client has exceeded rate limits. */
  RATE_LIMITED = "RATE_LIMITED",
  /** The request timed out before a response could be generated. */
  TIMEOUT = "TIMEOUT",
  /** The service is temporarily unavailable, possibly due to maintenance or overload. */
  SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE",
  /** An unexpected error occurred on the server side. */
  INTERNAL_ERROR = "INTERNAL_ERROR",
  /** An error occurred, but the specific cause is unknown or cannot be categorized. */
  UNKNOWN_ERROR = "UNKNOWN_ERROR",
  /** An error occurred during the loading or validation of configuration data. */
  CONFIGURATION_ERROR = "CONFIGURATION_ERROR",
}

/**
 * Custom error class for MCP-specific errors.
 * Encapsulates a `BaseErrorCode`, a descriptive message, and optional details.
 * Provides a method to format the error into a standard MCP tool response.
 */
export class McpError extends Error {
  /**
   * Creates an instance of McpError.
   * @param {BaseErrorCode} code - The standardized error code.
   * @param {string} message - A human-readable description of the error.
   * @param {Record<string, unknown>} [details] - Optional additional details about the error.
   */
  constructor(
    public code: BaseErrorCode,
    message: string,
    public details?: Record<string, unknown>,
  ) {
    super(message);
    // Set the error name for identification
    this.name = "McpError";
    // Ensure the prototype chain is correct
    Object.setPrototypeOf(this, McpError.prototype);
  }

  // Removed toResponse() method. The SDK should handle formatting errors into JSON-RPC responses.
}

/**
 * Zod schema for validating error objects, potentially used for parsing
 * error responses or validating error structures internally.
 */
export const ErrorSchema = z
  .object({
    /** The error code, corresponding to BaseErrorCode enum values. */
    code: z.nativeEnum(BaseErrorCode).describe("Standardized error code"),
    /** A human-readable description of the error. */
    message: z.string().describe("Detailed error message"),
    /** Optional additional details or context about the error. */
    details: z
      .record(z.unknown())
      .optional()
      .describe("Optional structured error details"),
  })
  .describe("Schema for validating structured error objects.");

/**
 * TypeScript type inferred from `ErrorSchema`.
 * Represents a validated error object structure.
 * @typedef {z.infer<typeof ErrorSchema>} ErrorResponse
 */
export type ErrorResponse = z.infer<typeof ErrorSchema>;

```

--------------------------------------------------------------------------------
/src/mcp-server/tools/obsidianManageTagsTool/registration.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import {
  ObsidianRestApiService,
  VaultCacheService,
} from "../../../services/obsidianRestAPI/index.js";
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
import {
  ErrorHandler,
  logger,
  RequestContext,
  requestContextService,
} from "../../../utils/index.js";
import type {
  ObsidianManageTagsInput,
  ObsidianManageTagsResponse,
} from "./logic.js";
import {
  ManageTagsInputSchema,
  ObsidianManageTagsInputSchemaShape,
  processObsidianManageTags,
} from "./logic.js";

export const registerObsidianManageTagsTool = async (
  server: McpServer,
  obsidianService: ObsidianRestApiService,
  vaultCacheService: VaultCacheService | undefined,
): Promise<void> => {
  const toolName = "obsidian_manage_tags";
  const toolDescription =
    "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.";

  const registrationContext: RequestContext =
    requestContextService.createRequestContext({
      operation: "RegisterObsidianManageTagsTool",
      toolName: toolName,
      module: "ObsidianManageTagsRegistration",
    });

  logger.info(`Attempting to register tool: ${toolName}`, registrationContext);

  await ErrorHandler.tryCatch(
    async () => {
      server.tool(
        toolName,
        toolDescription,
        ObsidianManageTagsInputSchemaShape,
        async (params: ObsidianManageTagsInput) => {
          const handlerContext: RequestContext =
            requestContextService.createRequestContext({
              parentContext: registrationContext,
              operation: "HandleObsidianManageTagsRequest",
              toolName: toolName,
              params: params,
            });
          logger.debug(`Handling '${toolName}' request`, handlerContext);

          return await ErrorHandler.tryCatch(
            async () => {
              const validatedParams = ManageTagsInputSchema.parse(params);

              const response: ObsidianManageTagsResponse =
                await processObsidianManageTags(
                  validatedParams,
                  handlerContext,
                  obsidianService,
                  vaultCacheService,
                );
              logger.debug(
                `'${toolName}' processed successfully`,
                handlerContext,
              );

              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify(response, null, 2),
                  },
                ],
                isError: false,
              };
            },
            {
              operation: `processing ${toolName} handler`,
              context: handlerContext,
              input: params,
              errorMapper: (error: unknown) =>
                new McpError(
                  error instanceof McpError
                    ? error.code
                    : BaseErrorCode.INTERNAL_ERROR,
                  `Error processing ${toolName} tool: ${error instanceof Error ? error.message : "Unknown error"}`,
                  { ...handlerContext },
                ),
            },
          );
        },
      );

      logger.info(
        `Tool registered successfully: ${toolName}`,
        registrationContext,
      );
    },
    {
      operation: `registering tool ${toolName}`,
      context: registrationContext,
      errorCode: BaseErrorCode.INTERNAL_ERROR,
      errorMapper: (error: unknown) =>
        new McpError(
          error instanceof McpError ? error.code : BaseErrorCode.INTERNAL_ERROR,
          `Failed to register tool '${toolName}': ${error instanceof Error ? error.message : "Unknown error"}`,
          { ...registrationContext },
        ),
      critical: true,
    },
  );
};

```

--------------------------------------------------------------------------------
/src/utils/internal/requestContext.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Utilities for creating and managing request contexts.
 * A request context is an object carrying a unique ID, timestamp, and other
 * relevant data for logging, tracing, and processing. It also defines
 * configuration and operational context structures.
 * @module src/utils/internal/requestContext
 */

import { generateUUID } from "../index.js";
import { logger } from "./logger.js";

/**
 * Defines the core structure for context information associated with a request or operation.
 * This is fundamental for logging, tracing, and passing operational data.
 */
export interface RequestContext {
  /**
   * Unique ID for the context instance.
   * Used for log correlation and request tracing.
   */
  requestId: string;

  /**
   * ISO 8601 timestamp indicating when the context was created.
   */
  timestamp: string;

  /**
   * Allows arbitrary key-value pairs for specific context needs.
   * Using `unknown` promotes type-safe access.
   * Consumers must type-check/assert when accessing extended properties.
   */
  [key: string]: unknown;
}

/**
 * Configuration for the {@link requestContextService}.
 * Allows for future extensibility of service-wide settings.
 */
export interface ContextConfig {
  /** Custom configuration properties. Allows for arbitrary key-value pairs. */
  [key: string]: unknown;
}

/**
 * Represents a broader context for a specific operation or task.
 * It can optionally include a base {@link RequestContext} and other custom properties
 * relevant to the operation.
 */
export interface OperationContext {
  /** Optional base request context data, adhering to the `RequestContext` structure. */
  requestContext?: RequestContext;

  /** Allows for additional, custom properties specific to the operation. */
  [key: string]: unknown;
}

/**
 * Singleton-like service object for managing request context operations.
 * @private
 */
const requestContextServiceInstance = {
  /**
   * Internal configuration store for the service.
   */
  config: {} as ContextConfig,

  /**
   * Configures the request context service with new settings.
   * Merges the provided partial configuration with existing settings.
   *
   * @param config - A partial `ContextConfig` object containing settings to update or add.
   * @returns A shallow copy of the newly updated configuration.
   */
  configure(config: Partial<ContextConfig>): ContextConfig {
    this.config = {
      ...this.config,
      ...config,
    };
    const logContext = this.createRequestContext({
      operation: "RequestContextService.configure",
      newConfigState: { ...this.config },
    });
    logger.debug("RequestContextService configuration updated", logContext);
    return { ...this.config };
  },

  /**
   * Retrieves a shallow copy of the current service configuration.
   * This prevents direct mutation of the internal configuration state.
   *
   * @returns A shallow copy of the current `ContextConfig`.
   */
  getConfig(): ContextConfig {
    return { ...this.config };
  },

  /**
   * Creates a new {@link RequestContext} instance.
   * Each context is assigned a unique `requestId` (UUID) and a current `timestamp` (ISO 8601).
   * Additional custom properties can be merged into the context.
   *
   * @param additionalContext - An optional record of key-value pairs to be
   *   included in the created request context.
   * @returns A new `RequestContext` object.
   */
  createRequestContext(
    additionalContext: Record<string, unknown> = {},
  ): RequestContext {
    const requestId = generateUUID();
    const timestamp = new Date().toISOString();

    const context: RequestContext = {
      requestId,
      timestamp,
      ...additionalContext,
    };
    return context;
  },
};

/**
 * Primary export for request context functionalities.
 * This service provides methods to create and manage {@link RequestContext} instances,
 * which are essential for logging, tracing, and correlating operations.
 */
export const requestContextService = requestContextServiceInstance;

```

--------------------------------------------------------------------------------
/src/mcp-server/tools/obsidianManageFrontmatterTool/registration.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import {
  ObsidianRestApiService,
  VaultCacheService,
} from "../../../services/obsidianRestAPI/index.js";
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
import {
  ErrorHandler,
  logger,
  RequestContext,
  requestContextService,
} from "../../../utils/index.js";
import type {
  ObsidianManageFrontmatterInput,
  ObsidianManageFrontmatterResponse,
} from "./logic.js";
import {
  ManageFrontmatterInputSchema,
  ObsidianManageFrontmatterInputSchemaShape,
  processObsidianManageFrontmatter,
} from "./logic.js";

export const registerObsidianManageFrontmatterTool = async (
  server: McpServer,
  obsidianService: ObsidianRestApiService,
  vaultCacheService: VaultCacheService | undefined,
): Promise<void> => {
  const toolName = "obsidian_manage_frontmatter";
  const toolDescription =
    "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.";

  const registrationContext: RequestContext =
    requestContextService.createRequestContext({
      operation: "RegisterObsidianManageFrontmatterTool",
      toolName: toolName,
      module: "ObsidianManageFrontmatterRegistration",
    });

  logger.info(`Attempting to register tool: ${toolName}`, registrationContext);

  await ErrorHandler.tryCatch(
    async () => {
      server.tool(
        toolName,
        toolDescription,
        ObsidianManageFrontmatterInputSchemaShape,
        async (params: ObsidianManageFrontmatterInput) => {
          const handlerContext: RequestContext =
            requestContextService.createRequestContext({
              parentContext: registrationContext,
              operation: "HandleObsidianManageFrontmatterRequest",
              toolName: toolName,
              params: params,
            });
          logger.debug(`Handling '${toolName}' request`, handlerContext);

          return await ErrorHandler.tryCatch(
            async () => {
              const validatedParams =
                ManageFrontmatterInputSchema.parse(params);

              const response: ObsidianManageFrontmatterResponse =
                await processObsidianManageFrontmatter(
                  validatedParams,
                  handlerContext,
                  obsidianService,
                  vaultCacheService,
                );
              logger.debug(
                `'${toolName}' processed successfully`,
                handlerContext,
              );

              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify(response, null, 2),
                  },
                ],
                isError: false,
              };
            },
            {
              operation: `processing ${toolName} handler`,
              context: handlerContext,
              input: params,
              errorMapper: (error: unknown) =>
                new McpError(
                  error instanceof McpError
                    ? error.code
                    : BaseErrorCode.INTERNAL_ERROR,
                  `Error processing ${toolName} tool: ${error instanceof Error ? error.message : "Unknown error"}`,
                  { ...handlerContext },
                ),
            },
          );
        },
      );

      logger.info(
        `Tool registered successfully: ${toolName}`,
        registrationContext,
      );
    },
    {
      operation: `registering tool ${toolName}`,
      context: registrationContext,
      errorCode: BaseErrorCode.INTERNAL_ERROR,
      errorMapper: (error: unknown) =>
        new McpError(
          error instanceof McpError ? error.code : BaseErrorCode.INTERNAL_ERROR,
          `Failed to register tool '${toolName}': ${error instanceof Error ? error.message : "Unknown error"}`,
          { ...registrationContext },
        ),
      critical: true,
    },
  );
};

```

--------------------------------------------------------------------------------
/src/services/obsidianRestAPI/types.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @module ObsidianRestApiTypes
 * @description
 * Type definitions for interacting with the Obsidian Local REST API,
 * based on its OpenAPI specification.
 */

import { AxiosRequestConfig } from "axios";
import { RequestContext } from "../../utils/index.js";

/**
 * Defines the signature for the internal request function passed to method implementations.
 * This function is bound to the `ObsidianRestApiService` instance and handles the core
 * logic of making an HTTP request, including authentication, error handling, and logging.
 *
 * @template T The expected return type of the API call.
 * @param config The Axios request configuration.
 * @param context The request context for logging and correlation.
 * @param operationName A descriptive name for the operation being performed, used for logging.
 * @returns A promise that resolves with the data of type `T`.
 */
export type RequestFunction = <T = any>(
  config: AxiosRequestConfig,
  context: RequestContext,
  operationName: string,
) => Promise<T>;

/**
 * Filesystem metadata for a note.
 */
export interface NoteStat {
  ctime: number; // Creation time (Unix timestamp)
  mtime: number; // Modification time (Unix timestamp)
  size: number; // File size in bytes
}

/**
 * JSON representation of an Obsidian note.
 * Returned when requesting with Accept: application/vnd.olrapi.note+json
 */
export interface NoteJson {
  content: string;
  frontmatter: Record<string, any>; // Parsed YAML frontmatter
  path: string; // Vault-relative path
  stat: NoteStat;
  tags: string[]; // Tags found in the note (including frontmatter)
}

/**
 * Response structure for listing files in a directory.
 */
export interface FileListResponse {
  files: string[]; // List of file/directory names (directories end with '/')
}

/**
 * Match details within a simple search result.
 */
export interface SimpleSearchMatchDetail {
  start: number; // Start index of the match
  end: number; // End index of the match
}

/**
 * Contextual match information for simple search.
 */
export interface SimpleSearchMatch {
  context: string; // Text surrounding the match
  match: SimpleSearchMatchDetail;
}

/**
 * Result item for a simple text search.
 */
export interface SimpleSearchResult {
  filename: string; // Path to the matching file
  matches: SimpleSearchMatch[];
  score: number; // Relevance score
}

/**
 * Result item for a complex (Dataview/JsonLogic) search.
 */
export interface ComplexSearchResult {
  filename: string; // Path to the matching file
  result: any; // The result returned by the query logic for this file
}

/**
 * Structure for an available Obsidian command.
 */
export interface ObsidianCommand {
  id: string;
  name: string;
}

/**
 * Response structure for listing available commands.
 */
export interface CommandListResponse {
  commands: ObsidianCommand[];
}

/**
 * Basic status response from the API root.
 */
export interface ApiStatusResponse {
  authenticated: boolean;
  ok: string; // Should be "OK"
  service: string; // Should be "Obsidian Local REST API"
  versions: {
    obsidian: string; // Obsidian API version
    self: string; // Plugin version
  };
}

/**
 * Standard error response structure from the API.
 */
export interface ApiError {
  errorCode: number; // e.g., 40149
  message: string; // e.g., "A brief description of the error."
}

/**
 * Options for PATCH operations.
 */
export interface PatchOptions {
  operation: "append" | "prepend" | "replace";
  targetType: "heading" | "block" | "frontmatter";
  target: string; // The specific heading, block ID, or frontmatter key
  targetDelimiter?: string; // Default '::' for nested headings
  trimTargetWhitespace?: boolean; // Default false
  /**
   * If true, creates the target if it's missing.
   * This is implemented via the `Create-Target-If-Missing` HTTP header.
   * Particularly useful for adding new frontmatter keys.
   */
  createTargetIfMissing?: boolean;
  contentType?: "text/markdown" | "application/json"; // For request body type inference
}

/**
 * Type alias for periodic note periods.
 */
export type Period = "daily" | "weekly" | "monthly" | "quarterly" | "yearly";

```

--------------------------------------------------------------------------------
/src/services/obsidianRestAPI/methods/patchMethods.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @module PatchMethods
 * @description
 * Methods for performing granular PATCH operations within notes via the Obsidian REST API.
 */

import { RequestContext } from "../../../utils/index.js";
import { PatchOptions, Period, RequestFunction } from "../types.js";
import { encodeVaultPath } from "../../../utils/obsidian/obsidianApiUtils.js";

/**
 * Helper to construct headers for PATCH requests.
 */
function buildPatchHeaders(options: PatchOptions): Record<string, string> {
  const headers: Record<string, string> = {
    Operation: options.operation,
    "Target-Type": options.targetType,
    // Spec requires URL encoding for non-ASCII characters in Target header
    Target: encodeURIComponent(options.target),
  };
  if (options.targetDelimiter) {
    headers["Target-Delimiter"] = options.targetDelimiter;
  }
  if (options.trimTargetWhitespace !== undefined) {
    headers["Trim-Target-Whitespace"] = String(options.trimTargetWhitespace);
  }
  // Add Create-Target-If-Missing header if provided in options
  if (options.createTargetIfMissing !== undefined) {
    headers["Create-Target-If-Missing"] = String(options.createTargetIfMissing);
  }
  if (options.contentType) {
    headers["Content-Type"] = options.contentType;
  } else {
    // Default to markdown if not specified, especially for non-JSON content
    headers["Content-Type"] = "text/markdown";
  }
  return headers;
}

/**
 * Patches a specific file in the vault.
 * @param _request - The internal request function from the service instance.
 * @param filePath - Vault-relative path to the file.
 * @param content - The content to insert/replace (string or JSON for tables/frontmatter).
 * @param options - Patch operation details (operation, targetType, target, etc.).
 * @param context - Request context.
 * @returns {Promise<void>} Resolves on success (200 OK).
 */
export async function patchFile(
  _request: RequestFunction,
  filePath: string,
  content: string | object, // Allow object for JSON content type
  options: PatchOptions,
  context: RequestContext,
): Promise<void> {
  const headers = buildPatchHeaders(options);
  const requestData =
    typeof content === "object" ? JSON.stringify(content) : content;
  const encodedPath = encodeVaultPath(filePath);

  // PATCH returns 200 OK according to spec
  await _request<void>(
    {
      method: "PATCH",
      url: `/vault${encodedPath}`, // Use the encoded path
      headers: headers,
      data: requestData,
    },
    context,
    "patchFile",
  );
}

/**
 * Patches the currently active file in Obsidian.
 * @param _request - The internal request function from the service instance.
 * @param content - The content to insert/replace.
 * @param options - Patch operation details.
 * @param context - Request context.
 * @returns {Promise<void>} Resolves on success (200 OK).
 */
export async function patchActiveFile(
  _request: RequestFunction,
  content: string | object,
  options: PatchOptions,
  context: RequestContext,
): Promise<void> {
  const headers = buildPatchHeaders(options);
  const requestData =
    typeof content === "object" ? JSON.stringify(content) : content;

  await _request<void>(
    {
      method: "PATCH",
      url: `/active/`,
      headers: headers,
      data: requestData,
    },
    context,
    "patchActiveFile",
  );
}

/**
 * Patches a periodic note.
 * @param _request - The internal request function from the service instance.
 * @param period - The period type ('daily', 'weekly', etc.).
 * @param content - The content to insert/replace.
 * @param options - Patch operation details.
 * @param context - Request context.
 * @returns {Promise<void>} Resolves on success (200 OK).
 */
export async function patchPeriodicNote(
  _request: RequestFunction,
  period: Period,
  content: string | object,
  options: PatchOptions,
  context: RequestContext,
): Promise<void> {
  const headers = buildPatchHeaders(options);
  const requestData =
    typeof content === "object" ? JSON.stringify(content) : content;

  await _request<void>(
    {
      method: "PATCH",
      url: `/periodic/${period}/`,
      headers: headers,
      data: requestData,
    },
    context,
    "patchPeriodicNote",
  );
}

```

--------------------------------------------------------------------------------
/scripts/make-executable.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node

/**
 * @fileoverview Utility script to make files executable (chmod +x) on Unix-like systems.
 * @module scripts/make-executable
 *   On Windows, this script does nothing but exits successfully.
 *   Useful for CLI applications where built output needs executable permissions.
 *   Default target (if no args): dist/index.js.
 *   Ensures output paths are within the project directory for security.
 *
 * @example
 * // Add to package.json build script:
 * // "build": "tsc && ts-node --esm scripts/make-executable.ts dist/index.js"
 *
 * @example
 * // Run directly with custom files:
 * // ts-node --esm scripts/make-executable.ts path/to/script1 path/to/script2
 */

import fs from "fs/promises";
import os from "os";
import path from "path";

const isUnix = os.platform() !== "win32";
const projectRoot = process.cwd();
const EXECUTABLE_MODE = 0o755; // rwxr-xr-x

/**
 * Represents the result of an attempt to make a file executable.
 * @property file - The relative path of the file targeted.
 * @property status - The outcome of the operation ('success', 'error', or 'skipped').
 * @property reason - If status is 'error' or 'skipped', an explanation.
 */
interface ExecutableResult {
  file: string;
  status: "success" | "error" | "skipped";
  reason?: string;
}

/**
 * Main function to make specified files executable.
 * Skips operation on Windows. Processes command-line arguments for target files
 * or defaults to 'dist/index.js'. Reports status for each file.
 */
const makeExecutable = async (): Promise<void> => {
  try {
    const targetFiles: string[] =
      process.argv.slice(2).length > 0
        ? process.argv.slice(2)
        : ["dist/index.js"];

    if (!isUnix) {
      console.log(
        "Skipping chmod operation: Script is running on Windows (not applicable).",
      );
      return;
    }

    console.log(
      `Attempting to make files executable: ${targetFiles.join(", ")}`,
    );

    const results = await Promise.allSettled(
      targetFiles.map(async (targetFile): Promise<ExecutableResult> => {
        const normalizedPath = path.resolve(projectRoot, targetFile);

        if (
          !normalizedPath.startsWith(projectRoot + path.sep) &&
          normalizedPath !== projectRoot
        ) {
          return {
            file: targetFile,
            status: "error",
            reason: `Path resolves outside project boundary: ${normalizedPath}`,
          };
        }

        try {
          await fs.access(normalizedPath); // Check if file exists
          await fs.chmod(normalizedPath, EXECUTABLE_MODE);
          return { file: targetFile, status: "success" };
        } catch (error) {
          const err = error as NodeJS.ErrnoException;
          if (err.code === "ENOENT") {
            return {
              file: targetFile,
              status: "error",
              reason: "File not found",
            };
          }
          console.error(
            `Error setting executable permission for ${targetFile}: ${err.message}`,
          );
          return { file: targetFile, status: "error", reason: err.message };
        }
      }),
    );

    let hasErrors = false;
    results.forEach((result) => {
      if (result.status === "fulfilled") {
        const { file, status, reason } = result.value;
        if (status === "success") {
          console.log(`Successfully made executable: ${file}`);
        } else if (status === "error") {
          console.error(`Error for ${file}: ${reason}`);
          hasErrors = true;
        } else if (status === "skipped") {
          // This status is not currently generated by the mapAsync logic but kept for future flexibility
          console.warn(`Skipped ${file}: ${reason}`);
        }
      } else {
        console.error(
          `Unexpected failure for one of the files: ${result.reason}`,
        );
        hasErrors = true;
      }
    });

    if (hasErrors) {
      console.error(
        "One or more files could not be made executable. Please check the errors above.",
      );
      // process.exit(1); // Uncomment to exit with error if any file fails
    } else {
      console.log("All targeted files processed successfully.");
    }
  } catch (error) {
    console.error(
      "A fatal error occurred during the make-executable script:",
      error instanceof Error ? error.message : error,
    );
    process.exit(1);
  }
};

makeExecutable();

```

--------------------------------------------------------------------------------
/src/utils/parsing/dateParser.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Provides utilities for parsing natural language date strings
 * into Date objects or detailed parsing results using the 'chrono-node' library.
 * @module src/utils/parsing/dateParser
 */

import * as chrono from "chrono-node";
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
import { ErrorHandler, logger, RequestContext } from "../internal/index.js";

/**
 * Parses a natural language date string (e.g., "tomorrow", "in 5 days", "2024-01-15")
 * into a JavaScript `Date` object.
 *
 * @async
 * @param {string} text - The natural language date string to parse.
 * @param {RequestContext} context - The request context for logging and error tracking.
 * @param {Date} [refDate] - Optional reference date for parsing relative date expressions.
 *   Defaults to the current date and time if not provided.
 * @returns {Promise<Date | null>} A promise that resolves to a `Date` object representing
 *   the parsed date, or `null` if `chrono-node` could not parse the input string into a date.
 * @throws {McpError} If an unexpected error occurs during the parsing process,
 *   an `McpError` with `BaseErrorCode.PARSING_ERROR` is thrown.
 */
async function parseDateString(
  text: string,
  context: RequestContext,
  refDate?: Date,
): Promise<Date | null> {
  const operation = "parseDateString";
  // Ensure context for logging includes all relevant details
  const logContext: RequestContext = {
    ...context,
    operation,
    inputText: text,
    refDate: refDate?.toISOString(),
  };
  logger.debug(`Attempting to parse date string: "${text}"`, logContext);

  return await ErrorHandler.tryCatch(
    async () => {
      // chrono.parseDate returns a Date object or null if no date is found.
      const parsedDate = chrono.parseDate(text, refDate, { forwardDate: true });
      if (parsedDate) {
        logger.debug(
          `Successfully parsed "${text}" to ${parsedDate.toISOString()}`,
          logContext,
        );
        return parsedDate;
      } else {
        // This is not an error, but chrono-node couldn't find a date.
        logger.info(
          `Could not parse a date from string: "${text}"`,
          logContext,
        );
        return null;
      }
    },
    {
      operation,
      context: logContext, // Pass the enriched logContext
      input: { text, refDate: refDate?.toISOString() }, // Log refDate as ISO string for consistency
      errorCode: BaseErrorCode.PARSING_ERROR, // Default error code for unexpected parsing failures
    },
  );
}

/**
 * Parses a natural language date string and returns detailed parsing results,
 * including all components and their confidence levels, as provided by `chrono-node`.
 *
 * @async
 * @param {string} text - The natural language date string to parse.
 * @param {RequestContext} context - The request context for logging and error tracking.
 * @param {Date} [refDate] - Optional reference date for parsing relative date expressions.
 *   Defaults to the current date and time if not provided.
 * @returns {Promise<chrono.ParsedResult[]>} A promise that resolves to an array of
 *   `chrono.ParsedResult` objects. The array will be empty if no date components
 *   could be parsed from the input string.
 * @throws {McpError} If an unexpected error occurs during the parsing process,
 *   an `McpError` with `BaseErrorCode.PARSING_ERROR` is thrown.
 */
async function parseDateStringDetailed(
  text: string,
  context: RequestContext,
  refDate?: Date,
): Promise<chrono.ParsedResult[]> {
  const operation = "parseDateStringDetailed";
  const logContext: RequestContext = {
    ...context,
    operation,
    inputText: text,
    refDate: refDate?.toISOString(),
  };
  logger.debug(
    `Attempting detailed parse of date string: "${text}"`,
    logContext,
  );

  return await ErrorHandler.tryCatch(
    async () => {
      // chrono.parse returns an array of results.
      const results = chrono.parse(text, refDate, { forwardDate: true });
      logger.debug(
        `Detailed parse of "${text}" resulted in ${results.length} result(s).`,
        logContext,
      );
      return results;
    },
    {
      operation,
      context: logContext,
      input: { text, refDate: refDate?.toISOString() },
      errorCode: BaseErrorCode.PARSING_ERROR,
    },
  );
}

/**
 * Provides methods for parsing natural language date strings.
 * - `parseToDate`: Parses a string to a single `Date` object or `null`.
 * - `getDetailedResults`: Provides comprehensive parsing results from `chrono-node`.
 */
export const dateParser = {
  /**
   * Parses a natural language date string into a `Date` object.
   * @see {@link parseDateString}
   */
  parseToDate: parseDateString,
  /**
   * Parses a natural language date string and returns detailed `chrono.ParsedResult` objects.
   * @see {@link parseDateStringDetailed}
   */
  getDetailedResults: parseDateStringDetailed,
};

```

--------------------------------------------------------------------------------
/src/mcp-server/tools/obsidianGlobalSearchTool/registration.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @module ObsidianGlobalSearchToolRegistration
 * @description Registers the 'obsidian_global_search' tool with the MCP server.
 * This tool allows searching the Obsidian vault using text/regex queries with optional date filters.
 */

import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { ObsidianRestApiService } from "../../../services/obsidianRestAPI/index.js";
import type { VaultCacheService } from "../../../services/obsidianRestAPI/vaultCache/index.js"; // Import VaultCacheService type
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
import {
  ErrorHandler,
  logger,
  RequestContext,
  requestContextService,
} from "../../../utils/index.js";
// Import types, schema shape, and the core processing logic from logic.ts
import type {
  ObsidianGlobalSearchInput,
  ObsidianGlobalSearchResponse,
} from "./logic.js"; // Ensure '.js' extension
import {
  ObsidianGlobalSearchInputSchemaShape,
  processObsidianGlobalSearch,
} from "./logic.js"; // Ensure '.js' extension

/**
 * Registers the 'obsidian_global_search' tool with the MCP server instance.
 *
 * @param {McpServer} server - The MCP server instance.
 * @param {ObsidianRestApiService} obsidianService - The instance of the Obsidian REST API service.
 * @param {VaultCacheService} vaultCacheService - The instance of the Vault Cache service.
 * @returns {Promise<void>} A promise that resolves when the tool is registered.
 * @throws {McpError} If registration fails critically.
 */
export async function registerObsidianGlobalSearchTool(
  server: McpServer,
  obsidianService: ObsidianRestApiService,
  vaultCacheService: VaultCacheService, // Now required
): Promise<void> {
  const toolName = "obsidian_global_search";
  const toolDescription = `Performs search across the Obsidian vault using text or regex, primarily relying on the Obsidian REST API's simple search. Supports filtering by modification date, optionally restricting search to a specific directory path (recursively), pagination (page, pageSize), and limiting matches shown per file (maxMatchesPerFile). Returns a JSON object containing success status, a message, pagination details (currentPage, pageSize, totalPages), total file/match counts (before pagination), and an array of results. Each result includes the file path, filename, creation timestamp (ctime), modification timestamp (mtime), and an array of match context snippets (limited by maxMatchesPerFile). If there are multiple pages of results, it also includes an 'alsoFoundInFiles' array listing filenames found on other pages.`;

  const registrationContext: RequestContext =
    requestContextService.createRequestContext({
      operation: "RegisterObsidianGlobalSearchTool",
      toolName: toolName,
      module: "ObsidianGlobalSearchRegistration",
    });

  logger.info(`Attempting to register tool: ${toolName}`, registrationContext);

  await ErrorHandler.tryCatch(
    async () => {
      server.tool(
        toolName,
        toolDescription,
        ObsidianGlobalSearchInputSchemaShape,
        async (
          params: ObsidianGlobalSearchInput,
          handlerInvocationContext: any,
        ): Promise<any> => {
          const handlerContext: RequestContext =
            requestContextService.createRequestContext({
              operation: "HandleObsidianGlobalSearchRequest",
              toolName: toolName,
              paramsSummary: {
                useRegex: params.useRegex,
                caseSensitive: params.caseSensitive,
                pageSize: params.pageSize,
                page: params.page,
                maxMatchesPerFile: params.maxMatchesPerFile,
                searchInPath: params.searchInPath,
                hasDateFilter: !!(
                  params.modified_since || params.modified_until
                ),
              },
            });
          logger.debug(`Handling '${toolName}' request`, handlerContext);

          return await ErrorHandler.tryCatch(
            async () => {
              const response: ObsidianGlobalSearchResponse =
                await processObsidianGlobalSearch(
                  params,
                  handlerContext,
                  obsidianService,
                  vaultCacheService,
                );
              logger.debug(
                `'${toolName}' processed successfully`,
                handlerContext,
              );

              return {
                content: [
                  {
                    type: "text",
                    text: JSON.stringify(response, null, 2),
                  },
                ],
                isError: false,
              };
            },
            {
              operation: `executing tool ${toolName}`,
              context: handlerContext,
              errorCode: BaseErrorCode.INTERNAL_ERROR,
            },
          );
        },
      );

      logger.info(
        `Tool registered successfully: ${toolName}`,
        registrationContext,
      );
    },
    {
      operation: `registering tool ${toolName}`,
      context: registrationContext,
      errorCode: BaseErrorCode.INTERNAL_ERROR,
      errorMapper: (error: unknown) =>
        new McpError(
          error instanceof McpError ? error.code : BaseErrorCode.INTERNAL_ERROR,
          `Failed to register tool '${toolName}': ${error instanceof Error ? error.message : "Unknown error"}`,
          { ...registrationContext },
        ),
      critical: true,
    },
  );
}

```

--------------------------------------------------------------------------------
/src/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Hono middleware for OAuth 2.1 Bearer Token validation.
 * This middleware extracts a JWT from the Authorization header, validates it against
 * a remote JWKS (JSON Web Key Set), and checks its issuer and audience claims.
 * On success, it populates an AuthInfo object and stores it in an AsyncLocalStorage
 * context for use in downstream handlers.
 *
 * @module src/mcp-server/transports/auth/strategies/oauth/oauthMiddleware
 */

import { HttpBindings } from "@hono/node-server";
import { Context, Next } from "hono";
import { createRemoteJWKSet, jwtVerify } from "jose";
import { config } from "../../../../../config/index.js";
import { BaseErrorCode, McpError } from "../../../../../types-global/errors.js";
import { logger, requestContextService } from "../../../../../utils/index.js";
import { ErrorHandler } from "../../../../../utils/internal/errorHandler.js";
import { authContext } from "../../core/authContext.js";
import type { AuthInfo } from "../../core/authTypes.js";

// --- Startup Validation ---
// Ensures that necessary OAuth configuration is present when the mode is 'oauth'.
if (config.mcpAuthMode === "oauth") {
  if (!config.oauthIssuerUrl) {
    throw new Error(
      "OAUTH_ISSUER_URL must be set when MCP_AUTH_MODE is 'oauth'",
    );
  }
  if (!config.oauthAudience) {
    throw new Error("OAUTH_AUDIENCE must be set when MCP_AUTH_MODE is 'oauth'");
  }
  logger.info(
    "OAuth 2.1 mode enabled. Verifying tokens against issuer.",
    requestContextService.createRequestContext({
      issuer: config.oauthIssuerUrl,
      audience: config.oauthAudience,
    }),
  );
}

// --- JWKS Client Initialization ---
// The remote JWK set is fetched and cached to avoid network calls on every request.
let jwks: ReturnType<typeof createRemoteJWKSet> | undefined;
if (config.mcpAuthMode === "oauth" && config.oauthIssuerUrl) {
  try {
    const jwksUrl = new URL(
      config.oauthJwksUri ||
        `${config.oauthIssuerUrl.replace(/\/$/, "")}/.well-known/jwks.json`,
    );
    jwks = createRemoteJWKSet(jwksUrl, {
      cooldownDuration: 300000, // 5 minutes
      timeoutDuration: 5000, // 5 seconds
    });
    logger.info(
      `JWKS client initialized for URL: ${jwksUrl.href}`,
      requestContextService.createRequestContext({
        operation: "oauthMiddlewareSetup",
      }),
    );
  } catch (error) {
    logger.fatal(
      "Failed to initialize JWKS client.",
      error as Error,
      requestContextService.createRequestContext({
        operation: "oauthMiddlewareSetup",
      }),
    );
    // Prevent server from starting if JWKS setup fails in oauth mode
    process.exit(1);
  }
}

/**
 * Hono middleware for verifying OAuth 2.1 JWT Bearer tokens.
 * It validates the token and uses AsyncLocalStorage to pass auth info.
 * @param c - The Hono context object.
 * @param next - The function to call to proceed to the next middleware.
 */
export async function oauthMiddleware(
  c: Context<{ Bindings: HttpBindings }>,
  next: Next,
) {
  // If OAuth is not the configured auth mode, skip this middleware.
  if (config.mcpAuthMode !== "oauth") {
    return await next();
  }

  const context = requestContextService.createRequestContext({
    operation: "oauthMiddleware",
    httpMethod: c.req.method,
    httpPath: c.req.path,
  });

  if (!jwks) {
    // This should not happen if startup validation is correct, but it's a safeguard.
    // This should not happen if startup validation is correct, but it's a safeguard.
    throw new McpError(
      BaseErrorCode.CONFIGURATION_ERROR,
      "OAuth middleware is active, but JWKS client is not initialized.",
      context,
    );
  }

  const authHeader = c.req.header("Authorization");
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    throw new McpError(
      BaseErrorCode.UNAUTHORIZED,
      "Missing or invalid token format.",
    );
  }

  const token = authHeader.substring(7);

  try {
    const { payload } = await jwtVerify(token, jwks, {
      issuer: config.oauthIssuerUrl!,
      audience: config.oauthAudience!,
    });

    // The 'scope' claim is typically a space-delimited string in OAuth 2.1.
    const scopes =
      typeof payload.scope === "string" ? payload.scope.split(" ") : [];

    if (scopes.length === 0) {
      logger.warning(
        "Authentication failed: Token contains no scopes, but scopes are required.",
        { ...context, jwtPayloadKeys: Object.keys(payload) },
      );
      throw new McpError(
        BaseErrorCode.UNAUTHORIZED,
        "Token must contain valid, non-empty scopes.",
      );
    }

    const clientId =
      typeof payload.client_id === "string" ? payload.client_id : undefined;

    if (!clientId) {
      logger.warning(
        "Authentication failed: OAuth token 'client_id' claim is missing or not a string.",
        { ...context, jwtPayloadKeys: Object.keys(payload) },
      );
      throw new McpError(
        BaseErrorCode.UNAUTHORIZED,
        "Invalid token, missing client identifier.",
      );
    }

    const authInfo: AuthInfo = {
      token,
      clientId,
      scopes,
      subject: typeof payload.sub === "string" ? payload.sub : undefined,
    };

    // Attach to the raw request for potential legacy compatibility and
    // store in AsyncLocalStorage for modern, safe access in handlers.
    c.env.incoming.auth = authInfo;
    await authContext.run({ authInfo }, next);
  } catch (error: unknown) {
    if (error instanceof Error && error.name === "JWTExpired") {
      logger.warning("Authentication failed: OAuth token expired.", context);
      throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token expired.");
    }

    const handledError = ErrorHandler.handleError(error, {
      operation: "oauthMiddleware",
      context,
      rethrow: false, // We will throw a new McpError below
    });

    // Ensure we always throw an McpError for consistency
    if (handledError instanceof McpError) {
      throw handledError;
    } else {
      throw new McpError(
        BaseErrorCode.UNAUTHORIZED,
        `Unauthorized: ${handledError.message || "Invalid token"}`,
        { originalError: handledError.name },
      );
    }
  }
}

```

--------------------------------------------------------------------------------
/src/utils/parsing/jsonParser.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Provides a utility class for parsing potentially partial JSON strings,
 * with support for handling and logging optional LLM <think> blocks.
 * It wraps the 'partial-json' library.
 * @module src/utils/parsing/jsonParser
 */

import {
  parse as parsePartialJson,
  Allow as PartialJsonAllow,
} from "partial-json";
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
import {
  logger,
  RequestContext,
  requestContextService,
} from "../internal/index.js"; // Corrected import path for internal utils

/**
 * Enum mirroring `partial-json`'s `Allow` constants. These constants specify
 * what types of partial JSON structures are permissible during parsing.
 * They can be combined using bitwise OR (e.g., `Allow.STR | Allow.OBJ`).
 *
 * - `Allow.OBJ`: Allows partial objects (e.g., `{"key": "value",`)
 * - `Allow.ARR`: Allows partial arrays (e.g., `[1, 2,`)
 * - `Allow.STR`: Allows partial strings (e.g., `"abc`)
 * - `Allow.NUM`: Allows partial numbers (e.g., `1.2e+`)
 * - `Allow.BOOL`: Allows partial booleans (e.g., `tru`)
 * - `Allow.NULL`: Allows partial nulls (e.g., `nul`)
 * - `Allow.ALL`: Allows all types of partial JSON structures (default).
 */
export const Allow = PartialJsonAllow;

// Regex to find a <think> block at the start of a string,
// capturing its content and the rest of the string.
const thinkBlockRegex = /^<think>([\s\S]*?)<\/think>\s*([\s\S]*)$/;

/**
 * Utility class for parsing JSON strings that may be partial or incomplete.
 * It wraps the 'partial-json' library to provide a consistent parsing interface
 * and includes logic to handle and log optional `<think>...</think>` blocks
 * that might precede the JSON content (often found in LLM outputs).
 */
class JsonParser {
  /**
   * Parses a JSON string, which may be partial or prefixed with an LLM `<think>` block.
   *
   * @template T The expected type of the parsed JavaScript value. Defaults to `any`.
   * @param {string} jsonString - The JSON string to parse.
   * @param {number} [allowPartial=Allow.ALL] - A bitwise OR combination of `Allow` constants
   *   specifying which types of partial JSON structures are permissible (e.g., `Allow.OBJ | Allow.ARR`).
   *   Defaults to `Allow.ALL`, permitting any form of partial JSON.
   * @param {RequestContext} [providedContext] - Optional `RequestContext` for logging,
   *   especially for capturing `<think>` block content or parsing errors.
   * @returns {T} The parsed JavaScript value.
   * @throws {McpError} Throws an `McpError` with `BaseErrorCode.VALIDATION_ERROR` if:
   *   - The string is empty after removing a `<think>` block.
   *   - The remaining content does not appear to be a valid JSON structure (object, array, or permitted primitive).
   *   - The `partial-json` library encounters a parsing error.
   */
  parse<T = any>(
    jsonString: string,
    allowPartial: number = Allow.ALL,
    providedContext?: RequestContext,
  ): T {
    const operation = "JsonParser.parse";
    // Ensure opContext is always a valid RequestContext for internal logging
    const opContext =
      providedContext ||
      requestContextService.createRequestContext({ operation });

    let stringToParse = jsonString;
    let thinkContentExtracted: string | undefined;

    const match = jsonString.match(thinkBlockRegex);

    if (match) {
      thinkContentExtracted = match[1].trim();
      const restOfString = match[2];

      if (thinkContentExtracted) {
        logger.debug("LLM <think> block content extracted.", {
          ...opContext,
          operation,
          thinkContent: thinkContentExtracted,
        });
      } else {
        logger.debug("Empty LLM <think> block detected and removed.", {
          ...opContext,
          operation,
        });
      }
      stringToParse = restOfString; // Continue parsing with the remainder of the string
    }

    stringToParse = stringToParse.trim(); // Trim whitespace from the string that will be parsed

    if (!stringToParse) {
      const errorMsg =
        "JSON string is empty after potential <think> block removal and trimming.";
      logger.warning(errorMsg, {
        ...opContext,
        operation,
        originalInput: jsonString,
      });
      throw new McpError(BaseErrorCode.VALIDATION_ERROR, errorMsg, {
        ...opContext,
        operation,
      });
    }

    try {
      // The pre-check for firstChar and specific primitive types has been removed.
      // We now directly rely on parsePartialJson to validate the structure according
      // to the 'allowPartial' flags. If parsePartialJson fails, it will throw an
      // error which is caught below and wrapped in an McpError.
      return parsePartialJson(stringToParse, allowPartial) as T;
    } catch (error: any) {
      const errorMessage = `Failed to parse JSON content: ${error.message}`;
      logger.error(errorMessage, error, {
        ...opContext, // Use the guaranteed valid opContext
        operation,
        contentAttempted: stringToParse,
        thinkContentFound: thinkContentExtracted,
      });
      throw new McpError(BaseErrorCode.VALIDATION_ERROR, errorMessage, {
        ...opContext, // Use the guaranteed valid opContext
        operation,
        originalContent: stringToParse,
        thinkContentProcessed: !!thinkContentExtracted,
        rawError:
          error instanceof Error
            ? { message: error.message, stack: error.stack }
            : String(error),
      });
    }
  }
}

/**
 * Singleton instance of the `JsonParser`.
 * Use this instance for all partial JSON parsing needs.
 *
 * Example:
 * ```typescript
 * import { jsonParser, Allow, RequestContext } from './jsonParser';
 * import { requestContextService } from '../internal'; // Assuming requestContextService is exported from internal utils
 * const context: RequestContext = requestContextService.createRequestContext({ operation: 'MyOperation' });
 * try {
 *   const data = jsonParser.parse('<think>Thinking...</think>{"key": "value", "arr": [1,', Allow.ALL, context);
 *   console.log(data); // Output: { key: "value", arr: [ 1 ] }
 * } catch (e) {
 *   console.error("Parsing failed:", e);
 * }
 * ```
 */
export const jsonParser = new JsonParser();

```

--------------------------------------------------------------------------------
/src/utils/metrics/tokenCounter.ts:
--------------------------------------------------------------------------------

```typescript
import { ChatCompletionMessageParam } from "openai/resources/chat/completions";
import { encoding_for_model, Tiktoken, TiktokenModel } from "tiktoken";
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
// Import utils from the main barrel file (ErrorHandler, logger, RequestContext from ../internal/*)
import { ErrorHandler, logger, RequestContext } from "../index.js";

// Define the model used specifically for token counting
const TOKENIZATION_MODEL: TiktokenModel = "gpt-4o"; // Note this is strictly for token counting, not the model used for inference

/**
 * Calculates the number of tokens for a given text using the 'gpt-4o' tokenizer.
 * Uses ErrorHandler for consistent error management.
 *
 * @param text - The input text to tokenize.
 * @param context - Optional request context for logging and error handling.
 * @returns The number of tokens.
 * @throws {McpError} Throws an McpError if tokenization fails.
 */
export async function countTokens(
  text: string,
  context?: RequestContext,
): Promise<number> {
  // Wrap the synchronous operation in tryCatch which handles both sync/async
  return ErrorHandler.tryCatch(
    () => {
      let encoding: Tiktoken | null = null;
      try {
        // Always use the defined TOKENIZATION_MODEL
        encoding = encoding_for_model(TOKENIZATION_MODEL);
        const tokens = encoding.encode(text);
        return tokens.length;
      } finally {
        encoding?.free(); // Ensure the encoder is freed if it was successfully created
      }
    },
    {
      operation: "countTokens",
      context: context,
      input: { textSample: text.substring(0, 50) + "..." }, // Log sanitized input
      errorCode: BaseErrorCode.INTERNAL_ERROR, // Use INTERNAL_ERROR for external lib issues
      // rethrow is implicitly true for tryCatch
      // Removed onErrorReturn as we now rethrow
    },
  );
}

/**
 * Calculates the number of tokens for chat messages using the ChatCompletionMessageParam structure
 * and the 'gpt-4o' tokenizer, considering special tokens and message overhead.
 * This implementation is based on OpenAI's guidelines for gpt-4/gpt-3.5-turbo models.
 * Uses ErrorHandler for consistent error management.
 *
 * See: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
 *
 * @param messages - An array of chat messages in the `ChatCompletionMessageParam` format.
 * @param context - Optional request context for logging and error handling.
 * @returns The estimated number of tokens.
 * @throws {McpError} Throws an McpError if tokenization fails.
 */
export async function countChatTokens(
  messages: ReadonlyArray<ChatCompletionMessageParam>, // Use the complex type
  context?: RequestContext,
): Promise<number> {
  // Wrap the synchronous operation in tryCatch
  return ErrorHandler.tryCatch(
    () => {
      let encoding: Tiktoken | null = null;
      let num_tokens = 0;
      try {
        // Always use the defined TOKENIZATION_MODEL
        encoding = encoding_for_model(TOKENIZATION_MODEL);

        // Define tokens per message/name based on gpt-4o (same as gpt-4/gpt-3.5-turbo)
        const tokens_per_message = 3;
        const tokens_per_name = 1;

        for (const message of messages) {
          num_tokens += tokens_per_message;
          // Encode role
          num_tokens += encoding.encode(message.role).length;

          // Encode content - handle potential null or array content (vision)
          if (typeof message.content === "string") {
            num_tokens += encoding.encode(message.content).length;
          } else if (Array.isArray(message.content)) {
            // Handle multi-part content (e.g., text + image) - simplified: encode text parts only
            for (const part of message.content) {
              if (part.type === "text") {
                num_tokens += encoding.encode(part.text).length;
              } else {
                // Add placeholder token count for non-text parts (e.g., images) if needed
                // This requires specific model knowledge (e.g., OpenAI vision model token costs)
                logger.warning(
                  `Non-text content part found (type: ${part.type}), token count contribution ignored.`,
                  context,
                );
                // num_tokens += IMAGE_TOKEN_COST; // Placeholder
              }
            }
          } // else: content is null, add 0 tokens

          // Encode name if present (often associated with 'tool' or 'function' roles in newer models)
          if ("name" in message && message.name) {
            num_tokens += tokens_per_name;
            num_tokens += encoding.encode(message.name).length;
          }

          // --- Handle tool calls (specific to newer models) ---
          // Assistant message requesting tool calls
          if (
            message.role === "assistant" &&
            "tool_calls" in message &&
            message.tool_calls
          ) {
            for (const tool_call of message.tool_calls) {
              // Add tokens for the function name and arguments
              if (tool_call.function.name) {
                num_tokens += encoding.encode(tool_call.function.name).length;
              }
              if (tool_call.function.arguments) {
                // Arguments are often JSON strings
                num_tokens += encoding.encode(
                  tool_call.function.arguments,
                ).length;
              }
            }
          }

          // Tool message providing results
          if (
            message.role === "tool" &&
            "tool_call_id" in message &&
            message.tool_call_id
          ) {
            num_tokens += encoding.encode(message.tool_call_id).length;
            // Content of the tool message (the result) is already handled by the string content check above
          }
        }
        num_tokens += 3; // every reply is primed with <|start|>assistant<|message|>
        return num_tokens;
      } finally {
        encoding?.free();
      }
    },
    {
      operation: "countChatTokens",
      context: context,
      input: { messageCount: messages.length }, // Log sanitized input
      errorCode: BaseErrorCode.INTERNAL_ERROR, // Use INTERNAL_ERROR
      // rethrow is implicitly true for tryCatch
      // Removed onErrorReturn
    },
  );
}

```

--------------------------------------------------------------------------------
/src/utils/internal/asyncUtils.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @fileoverview Provides utilities for handling asynchronous operations,
 * such as retrying operations with delays.
 * @module src/utils/internal/asyncUtils
 */
import { McpError, BaseErrorCode } from "../../types-global/errors.js";
import { logger } from "./logger.js";
import { RequestContext } from "./requestContext.js";

/**
 * Configuration for the {@link retryWithDelay} function, defining how retries are handled.
 */
export interface RetryConfig<T> {
  /**
   * A descriptive name for the operation being retried. Used in logging.
   * Example: "FetchUserData", "ProcessPayment".
   */
  operationName: string;
  /**
   * The request context associated with the operation, for logging and tracing.
   */
  context: RequestContext;
  /**
   * The maximum number of retry attempts before failing.
   */
  maxRetries: number;
  /**
   * The delay in milliseconds between retry attempts.
   */
  delayMs: number;
  /**
   * An optional function to determine if a retry should be attempted based on the error.
   * If not provided, retries will be attempted for any error.
   * @param error - The error that occurred during the operation.
   * @returns `true` if a retry should be attempted, `false` otherwise.
   */
  shouldRetry?: (error: unknown) => boolean;
  /**
   * An optional function to execute before each retry attempt.
   * Useful for custom logging or cleanup actions.
   * @param attempt - The current retry attempt number.
   * @param error - The error that triggered the retry.
   */
  onRetry?: (attempt: number, error: unknown) => void;
}

/**
 * Executes an asynchronous operation with a configurable retry mechanism.
 * This function will attempt the operation up to `maxRetries` times, with a specified
 * `delayMs` between attempts. It allows for custom logic to decide if an error
 * warrants a retry and for actions to be taken before each retry.
 *
 * @template T The expected return type of the asynchronous operation.
 * @param {() => Promise<T>} operation - The asynchronous function to execute.
 *   This function should return a Promise resolving to type `T`.
 * @param {RetryConfig<T>} config - Configuration options for the retry behavior,
 *   including operation name, context, retry limits, delay, and custom handlers.
 * @returns {Promise<T>} A promise that resolves with the result of the operation if successful.
 * @throws {McpError} Throws an `McpError` if the operation fails after all retry attempts,
 *   or if an unexpected error occurs during the retry logic. The error will contain details
 *   about the operation name, context, and the last encountered error.
 */
export async function retryWithDelay<T>(
  operation: () => Promise<T>,
  config: RetryConfig<T>,
): Promise<T> {
  const {
    operationName,
    context,
    maxRetries,
    delayMs,
    shouldRetry = () => true, // Default: retry on any error
    onRetry,
  } = config;

  let lastError: unknown;

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;
      // Ensure the context for logging includes attempt details
      const retryAttemptContext: RequestContext = {
        ...context, // Spread existing context
        operation: operationName, // Ensure operationName is part of the context for logger
        attempt,
        maxRetries,
        lastError: error instanceof Error ? error.message : String(error),
      };

      if (attempt < maxRetries && shouldRetry(error)) {
        if (onRetry) {
          onRetry(attempt, error); // Custom onRetry logic
        } else {
          // Default logging for retry attempt
          logger.warning(
            `Operation '${operationName}' failed on attempt ${attempt} of ${maxRetries}. Retrying in ${delayMs}ms...`,
            retryAttemptContext, // Pass the enriched context
          );
        }
        await new Promise((resolve) => setTimeout(resolve, delayMs));
      } else {
        // Max retries reached or shouldRetry returned false
        const finalErrorMsg = `Operation '${operationName}' failed definitively after ${attempt} attempt(s).`;
        // Log the final failure with the enriched context
        logger.error(
          finalErrorMsg,
          error instanceof Error ? error : undefined,
          retryAttemptContext,
        );

        if (error instanceof McpError) {
          // If the last error was already an McpError, re-throw it but ensure its details are preserved/updated.
          error.details = {
            ...(typeof error.details === "object" && error.details !== null
              ? error.details
              : {}),
            ...retryAttemptContext, // Add retry context to existing details
            finalAttempt: true,
          };
          throw error;
        }
        // For other errors, wrap in a new McpError
        throw new McpError(
          BaseErrorCode.SERVICE_UNAVAILABLE, // Default to SERVICE_UNAVAILABLE, consider making this configurable or smarter
          `${finalErrorMsg} Last error: ${error instanceof Error ? error.message : String(error)}`,
          {
            ...retryAttemptContext, // Include all retry context
            originalErrorName:
              error instanceof Error ? error.name : typeof error,
            originalErrorStack:
              error instanceof Error ? error.stack : undefined,
            finalAttempt: true,
          },
        );
      }
    }
  }

  // Fallback: This part should ideally not be reached if the loop logic is correct.
  // If it is, it implies an issue with the loop or maxRetries logic.
  const fallbackErrorContext: RequestContext = {
    ...context,
    operation: operationName,
    maxRetries,
    reason: "Fallback_Error_Path_Reached_In_Retry_Logic",
  };
  logger.crit(
    // Log as critical because this path indicates a logic flaw
    `Operation '${operationName}' failed unexpectedly after all retries (fallback path). This may indicate a logic error in retryWithDelay.`,
    lastError instanceof Error ? lastError : undefined,
    fallbackErrorContext,
  );
  throw new McpError(
    BaseErrorCode.INTERNAL_ERROR, // Indicates an issue with the retry utility itself
    `Operation '${operationName}' failed unexpectedly after all retries (fallback path). Last error: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
    {
      ...fallbackErrorContext,
      originalError:
        lastError instanceof Error
          ? {
              message: lastError.message,
              name: lastError.name,
              stack: lastError.stack,
            }
          : String(lastError),
    },
  );
}

```

--------------------------------------------------------------------------------
/src/services/obsidianRestAPI/methods/vaultMethods.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * @module VaultMethods
 * @description
 * Methods for interacting with vault files and directories via the Obsidian REST API.
 */

import { RequestContext } from "../../../utils/index.js";
import {
  NoteJson,
  FileListResponse,
  NoteStat,
  RequestFunction,
} from "../types.js";
import { encodeVaultPath } from "../../../utils/obsidian/obsidianApiUtils.js";

/**
 * Gets the content of a specific file in the vault.
 * @param _request - The internal request function from the service instance.
 * @param filePath - Vault-relative path to the file.
 * @param format - 'markdown' or 'json' (for NoteJson).
 * @param context - Request context.
 * @returns The file content (string) or NoteJson object.
 */
export async function getFileContent(
  _request: RequestFunction,
  filePath: string,
  format: "markdown" | "json" = "markdown",
  context: RequestContext,
): Promise<string | NoteJson> {
  const acceptHeader =
    format === "json" ? "application/vnd.olrapi.note+json" : "text/markdown";
  const encodedPath = encodeVaultPath(filePath); // Use the new encoding function
  return _request<string | NoteJson>(
    {
      method: "GET",
      url: `/vault${encodedPath}`,
      headers: { Accept: acceptHeader },
    },
    context,
    "getFileContent",
  );
}

/**
 * Updates (overwrites) the content of a file or creates it if it doesn't exist.
 * @param _request - The internal request function from the service instance.
 * @param filePath - Vault-relative path to the file.
 * @param content - The new content for the file.
 * @param context - Request context.
 * @returns {Promise<void>} Resolves on success (204 No Content).
 */
export async function updateFileContent(
  _request: RequestFunction,
  filePath: string,
  content: string,
  context: RequestContext,
): Promise<void> {
  const encodedPath = encodeVaultPath(filePath); // Use the new encoding function
  // PUT returns 204 No Content, so the expected type is void
  await _request<void>(
    {
      method: "PUT",
      url: `/vault${encodedPath}`, // Construct URL correctly
      headers: { "Content-Type": "text/markdown" },
      data: content,
    },
    context,
    "updateFileContent",
  );
}

/**
 * Appends content to the end of a file. Creates the file if it doesn't exist.
 * @param _request - The internal request function from the service instance.
 * @param filePath - Vault-relative path to the file.
 * @param content - The content to append.
 * @param context - Request context.
 * @returns {Promise<void>} Resolves on success (204 No Content).
 */
export async function appendFileContent(
  _request: RequestFunction,
  filePath: string,
  content: string,
  context: RequestContext,
): Promise<void> {
  const encodedPath = encodeVaultPath(filePath); // Use the new encoding function
  await _request<void>(
    {
      method: "POST",
      url: `/vault${encodedPath}`, // Construct URL correctly
      headers: { "Content-Type": "text/markdown" },
      data: content,
    },
    context,
    "appendFileContent",
  );
}

/**
 * Deletes a specific file in the vault.
 * @param _request - The internal request function from the service instance.
 * @param filePath - Vault-relative path to the file.
 * @param context - Request context.
 * @returns {Promise<void>} Resolves on success (204 No Content).
 */
export async function deleteFile(
  _request: RequestFunction,
  filePath: string,
  context: RequestContext,
): Promise<void> {
  const encodedPath = encodeVaultPath(filePath); // Use the new encoding function
  await _request<void>(
    {
      method: "DELETE",
      url: `/vault${encodedPath}`, // Construct URL correctly
    },
    context,
    "deleteFile",
  );
}

/**
 * Lists files within a specified directory in the vault.
 * @param _request - The internal request function from the service instance.
 * @param dirPath - Vault-relative path to the directory. Use empty string "" or "/" for the root.
 * @param context - Request context.
 * @returns A list of file and directory names.
 */
export async function listFiles(
  _request: RequestFunction,
  dirPath: string,
  context: RequestContext,
): Promise<string[]> {
  // Normalize path: remove leading/trailing slashes for consistency, except for root
  let pathSegment = dirPath.trim();

  // Explicitly handle root path variations ('', '/') by setting pathSegment to empty.
  // This ensures that the final URL constructed later will be '/vault/', which the API
  // uses to list the root directory contents.
  if (pathSegment === "" || pathSegment === "/") {
    pathSegment = ""; // Use empty string to signify root for URL construction
  } else {
    // For non-root paths:
    // 1. Remove any leading/trailing slashes to prevent issues like '/vault//path/' or '/vault/path//'.
    // 2. URI-encode *each component* of the remaining path segment to handle special characters safely.
    pathSegment = pathSegment
      .replace(/^\/+|\/+$/g, "")
      .split("/")
      .map(encodeURIComponent)
      .join("/");
  }

  // Construct the final URL for the API request:
  // - If pathSegment is not empty (i.e., it's a specific directory), format as '/vault/{encoded_path}/'.
  // - If pathSegment IS empty (signifying the root), format as '/vault/'.
  // The trailing slash is important for directory listing endpoints in this API.
  const url = pathSegment ? `/vault/${pathSegment}/` : "/vault/";

  const response = await _request<FileListResponse>(
    {
      method: "GET",
      url: url, // Use the correctly constructed URL
    },
    context,
    "listFiles",
  );
  return response.files;
}

/**
 * Gets the metadata (stat) of a specific file using a lightweight HEAD request.
 * @param _request - The internal request function from the service instance.
 * @param filePath - Vault-relative path to the file.
 * @param context - Request context.
 * @returns The file's metadata.
 */
export async function getFileMetadata(
  _request: RequestFunction,
  filePath: string,
  context: RequestContext,
): Promise<NoteStat | null> {
  const encodedPath = encodeVaultPath(filePath);
  try {
    const response = await _request<any>(
      {
        method: "HEAD",
        url: `/vault${encodedPath}`,
      },
      context,
      "getFileMetadata",
    );

    if (response && response.headers) {
      const headers = response.headers;
      return {
        mtime: headers["x-obsidian-mtime"]
          ? parseFloat(headers["x-obsidian-mtime"]) * 1000
          : 0,
        ctime: headers["x-obsidian-ctime"]
          ? parseFloat(headers["x-obsidian-ctime"]) * 1000
          : 0,
        size: headers["content-length"]
          ? parseInt(headers["content-length"], 10)
          : 0,
      };
    }
    return null;
  } catch (error) {
    // Errors are already logged by the _request function, so we can just return null
    return null;
  }
}

```

--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------

```typescript
import dotenv from "dotenv";
import { existsSync, mkdirSync, readFileSync, statSync } from "fs";
import path, { dirname, join } from "path";
import { fileURLToPath } from "url";
import { z } from "zod";

dotenv.config();

// --- Determine Project Root ---
/**
 * Finds the project root directory by searching upwards for package.json.
 * @param startDir The directory to start searching from.
 * @returns The absolute path to the project root, or throws an error if not found.
 */
const findProjectRoot = (startDir: string): string => {
  let currentDir = startDir;
  while (true) {
    const packageJsonPath = join(currentDir, "package.json");
    if (existsSync(packageJsonPath)) {
      return currentDir;
    }
    const parentDir = dirname(currentDir);
    if (parentDir === currentDir) {
      // Reached the root of the filesystem without finding package.json
      throw new Error(
        `Could not find project root (package.json) starting from ${startDir}`,
      );
    }
    currentDir = parentDir;
  }
};

let projectRoot: string;
try {
  // For ESM, __dirname is not available directly.
  const currentModuleDir = dirname(fileURLToPath(import.meta.url));
  projectRoot = findProjectRoot(currentModuleDir);
} catch (error: any) {
  console.error(`FATAL: Error determining project root: ${error.message}`);
  projectRoot = process.cwd();
  console.warn(
    `Warning: Using process.cwd() (${projectRoot}) as fallback project root.`,
  );
}
// --- End Determine Project Root ---

const pkgPath = join(projectRoot, "package.json");
let pkg = { name: "obsidian-mcp-server", version: "0.0.0" };

try {
  pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
} catch (error) {
  if (process.stderr.isTTY) {
    console.error(
      "Warning: Could not read package.json for default config values. Using hardcoded defaults.",
      error,
    );
  }
}

/**
 * Zod schema for validating environment variables.
 * @private
 */
const EnvSchema = z.object({
  MCP_SERVER_NAME: z.string().optional(),
  MCP_SERVER_VERSION: z.string().optional(),
  MCP_LOG_LEVEL: z.string().default("info"),
  LOGS_DIR: z.string().default(path.join(projectRoot, "logs")),
  NODE_ENV: z.string().default("development"),
  MCP_TRANSPORT_TYPE: z.enum(["stdio", "http"]).default("stdio"),
  MCP_HTTP_PORT: z.coerce.number().int().positive().default(3010),
  MCP_HTTP_HOST: z.string().default("127.0.0.1"),
  MCP_ALLOWED_ORIGINS: z.string().optional(),
  MCP_AUTH_MODE: z.enum(["jwt", "oauth"]).optional(),
  MCP_AUTH_SECRET_KEY: z
    .string()
    .min(
      32,
      "MCP_AUTH_SECRET_KEY must be at least 32 characters long for security",
    )
    .optional(),
  OAUTH_ISSUER_URL: z.string().url().optional(),
  OAUTH_AUDIENCE: z.string().optional(),
  OAUTH_JWKS_URI: z.string().url().optional(),
  // --- Obsidian Specific Config ---
  OBSIDIAN_API_KEY: z.string().min(1, "OBSIDIAN_API_KEY cannot be empty"),
  OBSIDIAN_BASE_URL: z.string().url().default("http://127.0.0.1:27123"),
  OBSIDIAN_VERIFY_SSL: z
    .string()
    .transform((val) => val.toLowerCase() === "true")
    .default("false"),
  OBSIDIAN_CACHE_REFRESH_INTERVAL_MIN: z.coerce
    .number()
    .int()
    .positive()
    .default(10),
  OBSIDIAN_ENABLE_CACHE: z
    .string()
    .transform((val) => val.toLowerCase() === "true")
    .default("true"),
  OBSIDIAN_API_SEARCH_TIMEOUT_MS: z.coerce
    .number()
    .int()
    .positive()
    .default(30000),
});

const parsedEnv = EnvSchema.safeParse(process.env);

if (!parsedEnv.success) {
  const errorDetails = parsedEnv.error.flatten().fieldErrors;
  if (process.stderr.isTTY) {
    console.error("❌ Invalid environment variables:", errorDetails);
  }
  throw new Error(
    `Invalid environment configuration. Please check your .env file or environment variables. Details: ${JSON.stringify(errorDetails)}`,
  );
}

const env = parsedEnv.data;

// --- Directory Ensurance Function ---
const ensureDirectory = (
  dirPath: string,
  rootDir: string,
  dirName: string,
): string | null => {
  const resolvedDirPath = path.isAbsolute(dirPath)
    ? dirPath
    : path.resolve(rootDir, dirPath);

  if (
    !resolvedDirPath.startsWith(rootDir + path.sep) &&
    resolvedDirPath !== rootDir
  ) {
    if (process.stderr.isTTY) {
      console.error(
        `Error: ${dirName} path "${dirPath}" resolves to "${resolvedDirPath}", which is outside the project boundary "${rootDir}".`,
      );
    }
    return null;
  }

  if (!existsSync(resolvedDirPath)) {
    try {
      mkdirSync(resolvedDirPath, { recursive: true });
    } catch (err: unknown) {
      if (process.stderr.isTTY) {
        console.error(
          `Error creating ${dirName} directory at ${resolvedDirPath}: ${err instanceof Error ? err.message : String(err)}`,
        );
      }
      return null;
    }
  } else {
    try {
      if (!statSync(resolvedDirPath).isDirectory()) {
        if (process.stderr.isTTY) {
          console.error(
            `Error: ${dirName} path ${resolvedDirPath} exists but is not a directory.`,
          );
        }
        return null;
      }
    } catch (statError: any) {
      if (process.stderr.isTTY) {
        console.error(
          `Error accessing ${dirName} path ${resolvedDirPath}: ${statError.message}`,
        );
      }
      return null;
    }
  }
  return resolvedDirPath;
};
// --- End Directory Ensurance Function ---

const validatedLogsPath = ensureDirectory(env.LOGS_DIR, projectRoot, "logs");

if (!validatedLogsPath) {
  if (process.stderr.isTTY) {
    console.error(
      "FATAL: Logs directory configuration is invalid or could not be created. Please check permissions and path. Exiting.",
    );
  }
  process.exit(1);
}

/**
 * Main application configuration object.
 */
export const config = {
  pkg,
  mcpServerName: env.MCP_SERVER_NAME || pkg.name,
  mcpServerVersion: env.MCP_SERVER_VERSION || pkg.version,
  logLevel: env.MCP_LOG_LEVEL,
  logsPath: validatedLogsPath,
  environment: env.NODE_ENV,
  mcpTransportType: env.MCP_TRANSPORT_TYPE,
  mcpHttpPort: env.MCP_HTTP_PORT,
  mcpHttpHost: env.MCP_HTTP_HOST,
  mcpAllowedOrigins: env.MCP_ALLOWED_ORIGINS?.split(",")
    .map((origin) => origin.trim())
    .filter(Boolean),
  mcpAuthMode: env.MCP_AUTH_MODE,
  mcpAuthSecretKey: env.MCP_AUTH_SECRET_KEY,
  oauthIssuerUrl: env.OAUTH_ISSUER_URL,
  oauthAudience: env.OAUTH_AUDIENCE,
  oauthJwksUri: env.OAUTH_JWKS_URI,
  obsidianApiKey: env.OBSIDIAN_API_KEY,
  obsidianBaseUrl: env.OBSIDIAN_BASE_URL,
  obsidianVerifySsl: env.OBSIDIAN_VERIFY_SSL,
  obsidianCacheRefreshIntervalMin: env.OBSIDIAN_CACHE_REFRESH_INTERVAL_MIN,
  obsidianEnableCache: env.OBSIDIAN_ENABLE_CACHE,
  obsidianApiSearchTimeoutMs: env.OBSIDIAN_API_SEARCH_TIMEOUT_MS,
};

/**
 * The configured logging level for the application.
 * Exported separately for convenience (e.g., logger initialization).
 * @type {string}
 */
export const logLevel = config.logLevel;

/**
 * The configured runtime environment for the application.
 * Exported separately for convenience.
 * @type {string}
 */
export const environment = config.environment;

```

--------------------------------------------------------------------------------
/src/mcp-server/tools/obsidianDeleteNoteTool/registration.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import {
  ObsidianRestApiService,
  VaultCacheService,
} from "../../../services/obsidianRestAPI/index.js";
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
import {
  ErrorHandler,
  logger,
  RequestContext,
  requestContextService,
} from "../../../utils/index.js";
// Import necessary types, schema, and logic function from the logic file
import type {
  ObsidianDeleteNoteInput,
  ObsidianDeleteNoteResponse,
} from "./logic.js";
import {
  ObsidianDeleteNoteInputSchema,
  processObsidianDeleteNote,
} from "./logic.js";

/**
 * Registers the 'obsidian_delete_note' tool with the MCP server.
 *
 * This tool permanently deletes a specified file from the user's Obsidian vault.
 * It requires the vault-relative path, including the file extension. The tool
 * attempts a case-sensitive deletion first, followed by a case-insensitive
 * fallback search and delete if the initial attempt fails with a 'NOT_FOUND' error.
 *
 * The response is a JSON string containing a success status and a confirmation message.
 *
 * @param {McpServer} server - The MCP server instance to register the tool with.
 * @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service
 *   used to interact with the user's Obsidian vault.
 * @returns {Promise<void>} A promise that resolves when the tool registration is complete or rejects on error.
 * @throws {McpError} Throws an McpError if registration fails critically.
 */
export const registerObsidianDeleteNoteTool = async (
  server: McpServer,
  obsidianService: ObsidianRestApiService,
  vaultCacheService: VaultCacheService | undefined,
): Promise<void> => {
  const toolName = "obsidian_delete_note";
  // Updated description to accurately reflect the response (no timestamp)
  const toolDescription =
    "Permanently deletes a specified file from the Obsidian vault. Tries the exact path first, then attempts a case-insensitive fallback if the file is not found. Requires the vault-relative path including the file extension. Returns a success message.";

  // Create a context specifically for the registration process.
  const registrationContext: RequestContext =
    requestContextService.createRequestContext({
      operation: "RegisterObsidianDeleteNoteTool",
      toolName: toolName,
      module: "ObsidianDeleteNoteRegistration", // Identify the module
    });

  logger.info(`Attempting to register tool: ${toolName}`, registrationContext);

  // Wrap the registration logic in a tryCatch block for robust error handling during server setup.
  await ErrorHandler.tryCatch(
    async () => {
      // Use the high-level SDK method `server.tool` for registration.
      server.tool(
        toolName,
        toolDescription,
        ObsidianDeleteNoteInputSchema.shape, // Provide the Zod schema shape for input definition.
        /**
         * The handler function executed when the 'obsidian_delete_note' tool is called by the client.
         *
         * @param {ObsidianDeleteNoteInput} params - The input parameters received from the client,
         *   validated against the ObsidianDeleteNoteInputSchema shape.
         * @returns {Promise<CallToolResult>} A promise resolving to the structured result for the MCP client,
         *   containing either the successful response data (serialized JSON) or an error indication.
         */
        async (params: ObsidianDeleteNoteInput) => {
          // Type matches the inferred input schema
          // Create a specific context for this handler invocation.
          const handlerContext: RequestContext =
            requestContextService.createRequestContext({
              parentContext: registrationContext, // Link to registration context
              operation: "HandleObsidianDeleteNoteRequest",
              toolName: toolName,
              params: { filePath: params.filePath }, // Log the file path being targeted
            });
          logger.debug(`Handling '${toolName}' request`, handlerContext);

          // Wrap the core logic execution in a tryCatch block.
          return await ErrorHandler.tryCatch(
            async () => {
              // Delegate the actual file deletion logic to the processing function.
              // Note: Input schema and shape are identical, no separate refinement parse needed here.
              const response: ObsidianDeleteNoteResponse =
                await processObsidianDeleteNote(
                  params,
                  handlerContext,
                  obsidianService,
                  vaultCacheService,
                );
              logger.debug(
                `'${toolName}' processed successfully`,
                handlerContext,
              );

              // Format the successful response object from the logic function into the required MCP CallToolResult structure.
              // The response object (success, message) is serialized to JSON.
              return {
                content: [
                  {
                    type: "text", // Standard content type for structured JSON data
                    text: JSON.stringify(response, null, 2), // Pretty-print JSON
                  },
                ],
                isError: false, // Indicate successful execution
              };
            },
            {
              // Configuration for the inner error handler (processing logic).
              operation: `processing ${toolName} handler`,
              context: handlerContext,
              input: params, // Log the full input parameters if an error occurs.
              // Custom error mapping for consistent error reporting.
              errorMapper: (error: unknown) =>
                new McpError(
                  error instanceof McpError
                    ? error.code
                    : BaseErrorCode.INTERNAL_ERROR,
                  `Error processing ${toolName} tool: ${error instanceof Error ? error.message : "Unknown error"}`,
                  { ...handlerContext }, // Include context
                ),
            },
          ); // End of inner ErrorHandler.tryCatch
        },
      ); // End of server.tool call

      logger.info(
        `Tool registered successfully: ${toolName}`,
        registrationContext,
      );
    },
    {
      // Configuration for the outer error handler (registration process).
      operation: `registering tool ${toolName}`,
      context: registrationContext,
      errorCode: BaseErrorCode.INTERNAL_ERROR, // Default error code for registration failure.
      // Custom error mapping for registration failures.
      errorMapper: (error: unknown) =>
        new McpError(
          error instanceof McpError ? error.code : BaseErrorCode.INTERNAL_ERROR,
          `Failed to register tool '${toolName}': ${error instanceof Error ? error.message : "Unknown error"}`,
          { ...registrationContext }, // Include context
        ),
      critical: true, // Treat registration failure as critical.
    },
  ); // End of outer ErrorHandler.tryCatch
};

```
Page 1/4FirstPrevNextLast