Skip to main content
Glama

Obsidian MCP Server

Apache 2.0
181
215
  • Apple
  • Linux
.clinerules56.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._

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cyanheads/obsidian-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server