.clinerules•56.5 kB
# 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._