obsidian-mcp

repository·main·Indexed 20 days ago

https://github.com/stevenstavrakis/obsidian-mcp

An MCP (Model Context Protocol) server that enables AI assistants to interact with Obsidian vaults. It provides tools for reading, creating, editing, and managing notes and tags, including specific operations like search-vault, read-note, and manage-tags. The documentation covers manual installation for Claude Desktop, requirements (Node.js 20+), and detailed guidelines for developers to implement new tools using Zod schemas and standardized error handling.

Tokens
18.4K
Snippets
52
Records
69
Agent score
71%

What's inside obsidian-mcp

  1. Design JSON Schema-compatible Zod schemas

    main

    Since schemas are converted to JSON Schema for the MCP interface, you must follow specific patterns to ensure compatibility:

    • Use Discriminated Unions: For operations with different requirements (e.g., 'delete' vs 'edit'), use z.discriminatedUnion instead of superRefine. This translates well to JSON Schema.
    • Avoid superRefine for logic: Do not use complex refinements that rely on parent context or cross-field logic that cannot be expressed in JSON Schema.
    • Be Explicit: Always use .describe() on parameters to provide documentation for the LLM.
    • Strictness: Use .strict() on objects to prevent unexpected inputs.
    • Standard Types: Stick to standard Zod types that have clear JSON Schema equivalents.
    // ✅ DO: Use discriminated unions
    const schema = z.discriminatedUnion('operation', [
      z.object({ operation: z.literal('delete'), target: z.string() }),
      z.object({ operation: z.literal('edit'), target: z.string(), content: z.string() })
    ]).strict();
    
    // ❌ DON'T: Use complex refinements for conditional logic
    const schema = z.object({
      operation: z.enum(['delete', 'edit']),
      content: z.string().superRefine((val, ctx) => { /* ... */ })
    });
  2. Implement a new tool in Obsidian MCP

    main

    To create a new tool, follow a four-step pattern: create a dedicated directory, define a Zod input schema, implement the core logic in a private function, and export a factory function that returns a Tool object.

    Tool Structure Overview:

    1. Input validation: Use Zod schemas via createSchemaHandler.
    2. Core functionality: An async function that performs the actual work.
    3. Tool factory: A function that returns the Tool interface containing the name, description, schema, and handler.
    4. Error handling: Standardized responses using McpError and utility functions.
    // 1. Define Schema
    const schema = z.object({
      param1: z.string().describe("Description"),
    }).strict();
    const schemaHandler = createSchemaHandler(schema);
    
    // 2. Core Logic
    async function performOperation(param1: string): Promise<OperationResult> {
      // ... logic
      return { success: true, message: "Done" };
    }
    
    // 3. Factory
    export function createYourTool(vaultPath: string): Tool {
      return {
        name: "your-tool-name",
        description: "What it does",
        inputSchema: schemaHandler,
        handler: async (args) => {
          const validated = schemaHandler.parse(args);
          const result = await performOperation(validated.param1);
          return createToolResponse(formatOperationResult(result));
        }
      };
    }
  3. Implement a Search Tool correctly

    main

    When implementing a search tool (like search-files), follow these best practices:

    1. Comprehensive Schema: Define an optional configuration in your Zod schema, such as caseSensitive (boolean) or path (string) to limit the search scope.
    2. Scoped Search: If a path is provided in the arguments, join it with the vaultPath and use validateVaultPath to ensure the search remains within the vault.
    3. Structured Results: Return a SearchOperationResult containing success, message, results, totalMatches, and matchedFiles. Use formatSearchResult() to prepare this for the tool response.
    4. Avoid Unbounded Recursion: Ensure search implementations have limits or use optimized search methods rather than simple, unbounded recursive directory walking which can lead to performance issues or crashes.
    const schema = z.object({
      query: z.string()
        .min(1, "Search query cannot be empty")
        .describe("Text to search for"),
      caseSensitive: z.boolean()
        .optional()
        .describe("Whether to perform case-sensitive search"),
      path: z.string()
        .optional()
        .describe("Optional subfolder to limit search scope")
    }).strict();
    
    const schemaHandler = createSchemaHandler(schema);
    
    export function createSearchTool(vaultPath: string): Tool {
      return {
        name: "search-files",
        description: "Search for text in vault files",
        inputSchema: schemaHandler,
        handler: async (args) => {
          const validated = schemaHandler.parse(args);
          const result = await searchFiles(vaultPath, validated.query, {
            caseSensitive: validated.caseSensitive,
            path: validated.path
          });
          return createToolResponse(formatSearchResult(result));
        }
      };
    }
  4. Implement a File Operation Tool correctly

    main

    When implementing a tool for file operations (like write-file), follow these best practices:

    1. Strict Schema Definition: Use zod to define a strict schema. Include validations like .min(1) for required strings and .refine() to prevent directory traversal attacks (e.g., checking that paths do not contain ..).
    2. Use Schema Handlers: Utilize createSchemaHandler(schema) to manage the inputSchema property of the Tool object.
    3. Path Validation: Always use validateVaultPath(vaultPath, fullPath) after joining the vault root with the provided relative path to ensure the operation stays within the vault boundaries.
    4. Directory Safety: Before writing a file, use ensureDirectory(path.dirname(fullPath)) to create any missing parent directories.
    5. Standardized Responses: Use createToolResponse() combined with specialized formatters like formatFileResult() to ensure consistent output.
    6. Error Handling: Wrap file system operations in try/catch blocks and use handleFsError(error, 'operation name') to transform low-level errors into meaningful MCP errors.
    import { z } from "zod";
    import { Tool, FileOperationResult } from "../../types.js";
    import { validateVaultPath } from "../../utils/path.js";
    import { handleFsError } from "../../utils/errors.js";
    import { createToolResponse, formatFileResult } from "../../utils/responses.js";
    import { createSchemaHandler } from "../../utils/schema.js";
    
    const schema = z.object({
      path: z.string()
        .min(1, "Path cannot be empty")
        .refine(path => !path.includes('..'), "Path cannot contain '..' ")
        .describe("Path to the file relative to vault root"),
      content: z.string()
        .min(1, "Content cannot be empty")
        .describe("File content to write")
    }).strict();
    
    const schemaHandler = createSchemaHandler(schema);
    
    export function createWriteFileTool(vaultPath: string): Tool {
      return {
        name: "write-file",
        description: "Write content to a file in the vault",
        inputSchema: schemaHandler,
        handler: async (args) => {
          const validated = schemaHandler.parse(args);
          // ... implementation using validateVaultPath and handleFsError
          return createToolResponse(formatFileResult(result));
        }
      };
    }
  5. Handle errors and format responses in tools

    main

    To maintain consistency and prevent the server from crashing, follow these error handling and response patterns:

    Error Handling:

    • Convert FS Errors: Use handleFsError(error, 'operation name') to wrap filesystem errors into McpError types.
    • Catch Zod Errors: In the tool handler, catch z.ZodError and re-throw it as an McpError with ErrorCode.InvalidRequest so the client receives a clear validation message.
    • Avoid Raw Throws: Never throw raw errors; always ensure they are wrapped or converted to McpError.

    Response Formatting:

    • Use Utilities: Use createToolResponse() and formatOperationResult() to ensure the output matches the expected MCP structure.
    • Be Informative: Avoid vague messages like "Done"; include relevant operation details in the success response.
  6. Integrate a new tool into the server

    main

    Once a tool is implemented in src/tools/your-tool-name/index.ts, you must perform the following steps to make it available:

    1. Export the tool: Add the tool's factory function to the exports in src/tools/index.ts.
    2. Register the tool: Import and register the tool instance in src/server.ts.
    3. Verify: Ensure the tool is correctly picked up by the MCP server registration logic.
  7. Install Obsidian MCP Server via Smithery

    main

    You can attempt to install the server automatically for Claude Desktop using the Smithery CLI. Note that the author recommends manual installation over this method as it is not officially tested.

    npx -y @smithery/cli install obsidian-mcp --client claude
  8. Install Obsidian MCP Server manually

    main

    To use the Obsidian MCP server with Claude Desktop, add it to your claude_desktop_config.json file. You must provide the absolute path to your Obsidian vault as an argument. You can provide multiple vault paths if needed.

    Configuration Locations:

    • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
    • Windows: %APPDATA%\Claude\claude_desktop_config.json

    Important: Use absolute paths. On Windows, ensure you use double backslashes (e.g., C:\Users\...).

    After saving the configuration, restart Claude Desktop. A hammer icon should appear if the connection is successful.

    {
        "mcpServers": {
            "obsidian": {
                "command": "npx",
                "args": ["-y", "obsidian-mcp", "/path/to/your/vault", "/path/to/your/vault2"]
            }
        }
    }
  9. How ObsidianServer handles requests

    main

    The ObsidianServer implements several MCP standard handlers:

    • Prompts: Supports listPrompts and getPrompt. Prompts are retrieved via the prompt-factory and can include metadata like promptName and timestamp.
    • Tools: Supports listTools and callTool. Tools are validated using Zod schemas. If validation fails, an McpError with ErrorCode.InvalidParams is returned containing formatted error details.
    • Resources: Supports listResources and readResource. Resources are identified by URIs following the obsidian-vault:// format. The server resolves these URIs to specific files within the configured vaults.
  10. Understand the `rename-tag` tool output

    main

    The rename-tag tool returns a text summary of the operation. The output includes:

    1. Backup Information: If createBackup was true, it provides the path to the created backup.
    2. Success Summary: A list of files where changes occurred, specifying the location (frontmatter or content) and the specific tag transformation (e.g., old-tag -> new-tag). If the change was in the content, the line number is also provided.
    3. Error List: A list of files that could not be processed, along with the specific error message encountered.
  11. Understand VaultResource and VaultListResource data structures

    main

    The Obsidian MCP server uses two primary interfaces to represent vault-related resources.

    1. VaultResource: Represents an individual Obsidian vault. It includes the vault's URI, name, MIME type, and metadata containing the filesystem path and isAccessible status.
    2. VaultListResource: A special root resource used to aggregate all available vaults. Its URI is always obsidian-vault://. Its metadata contains totalVaults (a count) and a vaults array containing the name, path, and accessibility status for every configured vault.
    export interface VaultResource {
      uri: string;
      name: string;
      mimeType: string;
      description?: string;
      metadata?: {
        path: string;
        isAccessible: boolean;
      };
    }
    
    export interface VaultListResource {
      uri: string;
      name: string;
      mimeType: string;
      description: string;
      metadata?: {
        totalVaults: number;
        vaults: Array<{
          name: string;
          path: string;
          isAccessible: boolean;
        }>;
      };
    }