obsidian-mcp-server 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,101 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [2.0.4] - 2025-06-13
9
+
10
+ ### Added
11
+
12
+ - **Recursive File Listing**: The `obsidian_list_files` tool now supports recursive listing of directories with a `recursionDepth` parameter.
13
+
14
+ ### Changed
15
+
16
+ - **Documentation**:
17
+ - Consolidated tool specifications into `obsidian_mcp_tools_spec.md`.
18
+ - Updated `.clinerules` with a detailed logger implementation example for the agent.
19
+ - Updated the repository's directory tree documentation.
20
+
21
+ ## [2.0.3] - 2025-06-12
22
+
23
+ ### Fixed
24
+
25
+ - **NPM Package Display**: Explicitly included `README.md`, `LICENSE`, and `CHANGELOG.md` in the `files` array in `package.json` to ensure they are displayed correctly on the npm package page.
26
+
27
+ ## [2.0.2] - 2025-06-12
28
+
29
+ ### Fixed
30
+
31
+ - **NPM Package Version**: Bad npm package. Bumping to v2.0.2 for publishing.
32
+
33
+ ## [2.0.1] - 2025-06-12
34
+
35
+ ### Added
36
+
37
+ - **Enhanced Documentation**:
38
+ - Added a warning to the `VaultCacheService` documentation about its potential for high memory usage on large vaults.
39
+ - Added a code comment in `obsidianManageFrontmatterTool` to clarify the regex-based key deletion strategy.
40
+
41
+ ### Changed
42
+
43
+ - **Improved SSL Handling**: The `OBSIDIAN_VERIFY_SSL` environment variable is now correctly parsed as a boolean, ensuring more reliable SSL verification behavior.
44
+ - **API Service Refactoring**: Simplified the `httpsAgent` handling within the `ObsidianRestApiService` to improve code clarity and remove redundant agent creation on each request.
45
+
46
+ ### Fixed
47
+
48
+ - **Path Import Correction**: Corrected a path import in the `obsidianGlobalSearchTool` to use `node:path/posix` for better cross-platform compatibility.
49
+
50
+ ## [2.0.0] - 2025-06-12
51
+
52
+ Version 2.0.0 is a complete overhaul of the Obsidian MCP Server, migrating it to my [`cyanheads/mcp-ts-template`](https://github.com/cyanheads/mcp-ts-template). This release introduces a more robust architecture, a streamlined toolset, enhanced security, and significant performance improvements. It is a breaking change from the 1.x series.
53
+
54
+ ### Added
55
+
56
+ - **New Core Architecture**: The server is now built on the [`cyanheads/mcp-ts-template`](https://github.com/cyanheads/mcp-ts-template), providing a standardized, modular, and maintainable structure.
57
+ - **Hono HTTP Transport**: The HTTP transport has been migrated from Express to Hono, offering a more lightweight and performant server.
58
+ - **Vault Cache Service**: A new in-memory `VaultCacheService` has been introduced. It caches vault content to improve performance for search operations and provides a resilient fallback if the Obsidian API is temporarily unavailable. It also refreshes periodically.
59
+ - **Advanced Authentication**:
60
+ - Added support for **OAuth 2.1** bearer token validation alongside the existing secret key-based JWTs.
61
+ - Introduced `authContext` using `AsyncLocalStorage` for secure, request-scoped access to authentication details.
62
+ - **New Tools**:
63
+ - `obsidian_delete_file`: A new tool to permanently delete files from the vault.
64
+ - `obsidian_search_replace`: A powerful new tool to perform search and replace operations with regex support.
65
+ - **Enhanced Utilities**:
66
+ - **Request Context**: A robust request context system (`requestContextService`) for improved logging and tracing.
67
+ - **Error Handling**: A centralized `ErrorHandler` for consistent and detailed error reporting.
68
+ - **Async Utilities**: A `retryWithDelay` utility is now used across the application to make API calls more resilient.
69
+ - **New Development Scripts**: Added `docs:generate` (for TypeDoc) and `inspect:stdio`/`inspect:http` (for MCP Inspector) to `package.json`.
70
+
71
+ ### Changed
72
+
73
+ - **Project Structure**: The entire project has been reorganized to align with the [`cyanheads/mcp-ts-template`](https://github.com/cyanheads/mcp-ts-template), improving separation of concerns (e.g., `services`, `mcp-server`, `types-global`).
74
+ - **Tool Consolidation and Enhancement**: The toolset has been redesigned for clarity and power:
75
+ - `obsidian_list_files` replaces `obsidian_list_files_in_vault` and `obsidian_list_files_in_dir`, offering more flexible filtering.
76
+ - `obsidian_read_file` replaces `obsidian_get_file_contents` and now supports returning content as structured JSON.
77
+ - `obsidian_update_file` replaces `obsidian_append_content` and `obsidian_update_content` with explicit modes (`append`, `prepend`, `overwrite`).
78
+ - `obsidian_global_search` replaces `obsidian_find_in_file` with added support for path/date filtering and pagination.
79
+ - `obsidian_manage_frontmatter` replaces `obsidian_get_properties` and `obsidian_update_properties` with atomic get/set/delete operations.
80
+ - `obsidian_manage_tags` replaces `obsidian_get_tags` and now manages both frontmatter and inline tags.
81
+ - **Configuration Overhaul**: Environment variables have been renamed for consistency and clarity.
82
+ - `OBSIDIAN_BASE_URL` now consolidates protocol, host, and port.
83
+ - New variables like `MCP_TRANSPORT_TYPE`, `MCP_LOG_LEVEL`, and `MCP_AUTH_SECRET_KEY` have been introduced.
84
+ - **Dependency Updates**: All dependencies, including the MCP SDK, have been updated to their latest stable versions.
85
+ - **Obsidian API Service**: The `ObsidianRestApiService` has been completely refactored into a modular class, providing a typed, resilient, and centralized client for all interactions with the Obsidian Local REST API.
86
+
87
+ ### Removed
88
+
89
+ - **Removed Tools**: The following tools from version 1.x have been removed and their functionality integrated into the new, more comprehensive tools:
90
+ - `obsidian_list_files_in_vault`
91
+ - `obsidian_list_files_in_dir`
92
+ - `obsidian_get_file_contents`
93
+ - `obsidian_append_content`
94
+ - `obsidian_update_content`
95
+ - `obsidian_find_in_file`
96
+ - `obsidian_complex_search` (path-based searching is now a filter in `obsidian_global_search`)
97
+ - `obsidian_get_tags`
98
+ - `obsidian_get_properties`
99
+ - `obsidian_update_properties`
100
+ - **Removed Resources**: The `obsidian://tags` resource has been removed. Tag information is now available through the `obsidian_manage_tags` tool. I may add the resource back in the future if there is demand for it. Please open an issue if you would like to see it return.
101
+ - **Old Configuration**: All old, non-prefixed environment variables (e.g., `VERIFY_SSL`, `REQUEST_TIMEOUT`) have been removed in favor of the new, standardized configuration schema.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![TypeScript](https://img.shields.io/badge/TypeScript-^5.8.3-blue.svg)](https://www.typescriptlang.org/)
4
4
  [![Model Context Protocol](https://img.shields.io/badge/MCP%20SDK-^1.12.1-green.svg)](https://modelcontextprotocol.io/)
5
- [![Version](https://img.shields.io/badge/Version-2.0.2-blue.svg)](./CHANGELOG.md)
5
+ [![Version](https://img.shields.io/badge/Version-2.0.3-blue.svg)](./CHANGELOG.md)
6
6
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
7
  [![Status](https://img.shields.io/badge/Status-Production-brightgreen.svg)](https://github.com/cyanheads/obsidian-mcp-server/issues)
8
8
  [![GitHub](https://img.shields.io/github/stars/cyanheads/obsidian-mcp-server?style=social)](https://github.com/cyanheads/obsidian-mcp-server)
@@ -1,3 +1,9 @@
1
+ /**
2
+ * @fileoverview Core logic for the 'obsidian_list_files' tool.
3
+ * This module defines the input schema, response types, and processing logic for
4
+ * recursively listing files and directories in an Obsidian vault with filtering.
5
+ * @module src/mcp-server/tools/obsidianListFilesTool/logic
6
+ */
1
7
  import { z } from "zod";
2
8
  import { ObsidianRestApiService } from "../../../services/obsidianRestAPI/index.js";
3
9
  import { RequestContext } from "../../../utils/index.js";
@@ -7,58 +13,56 @@ import { RequestContext } from "../../../utils/index.js";
7
13
  export declare const ObsidianListFilesInputSchema: z.ZodObject<{
8
14
  /**
9
15
  * The vault-relative path to the directory whose contents should be listed.
10
- * Examples: "Attachments/Images", "Projects", "" (for vault root), "/" (for vault root).
11
16
  * The path is treated as case-sensitive by the underlying Obsidian API.
12
17
  */
13
18
  dirPath: z.ZodString;
14
19
  /**
15
20
  * Optional array of file extensions (including the leading dot) to filter the results.
16
- * Only files matching one of these extensions will be included. Directories are always included regardless of this filter.
17
- * Example: [".md", ".png"]
21
+ * Only files matching one of these extensions will be included. Directories are always included.
18
22
  */
19
23
  fileExtensionFilter: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
20
24
  /**
21
25
  * Optional JavaScript-compatible regular expression pattern string to filter results by name.
22
26
  * Only files and directories whose names match the regex will be included.
23
- * Example: "^\\d{4}-\\d{2}-\\d{2}" (matches names starting with YYYY-MM-DD)
24
27
  */
25
28
  nameRegexFilter: z.ZodOptional<z.ZodNullable<z.ZodString>>;
29
+ /**
30
+ * The maximum depth of subdirectories to list recursively.
31
+ * - A value of `0` lists only the files and directories in the specified `dirPath`.
32
+ * - A value of `1` lists the contents of `dirPath` and the contents of its immediate subdirectories.
33
+ * - A value of `-1` (the default) indicates infinite recursion, listing all subdirectories.
34
+ */
35
+ recursionDepth: z.ZodDefault<z.ZodNumber>;
26
36
  }, "strip", z.ZodTypeAny, {
27
37
  dirPath: string;
38
+ recursionDepth: number;
28
39
  fileExtensionFilter?: string[] | undefined;
29
40
  nameRegexFilter?: string | null | undefined;
30
41
  }, {
31
42
  dirPath: string;
32
43
  fileExtensionFilter?: string[] | undefined;
33
44
  nameRegexFilter?: string | null | undefined;
45
+ recursionDepth?: number | undefined;
34
46
  }>;
35
47
  /**
36
48
  * TypeScript type inferred from the input schema (`ObsidianListFilesInputSchema`).
37
- * Represents the validated input parameters used within the core processing logic.
38
49
  */
39
50
  export type ObsidianListFilesInput = z.infer<typeof ObsidianListFilesInputSchema>;
40
51
  /**
41
- * Defines the structure of the successful response returned by the `processObsidianListFiles` function.
42
- * This object is typically serialized to JSON and sent back to the client.
52
+ * Defines the structure of the successful response returned by the core logic function.
43
53
  */
44
54
  export interface ObsidianListFilesResponse {
45
- /** The vault-relative path of the directory whose contents were listed (normalized, e.g., "/" for root). */
46
55
  directoryPath: string;
47
- /** A string representation of the directory contents formatted as a simple tree structure. */
48
56
  tree: string;
49
- /** The total number of files and directories included in the formatted tree after filtering. */
50
57
  totalEntries: number;
51
58
  }
52
59
  /**
53
- * Processes the core logic for listing files and directories within a specified
54
- * directory in the Obsidian vault. Applies optional filters and formats the output.
60
+ * Processes the core logic for listing files and directories recursively within the Obsidian vault.
55
61
  *
56
62
  * @param {ObsidianListFilesInput} params - The validated input parameters.
57
63
  * @param {RequestContext} context - The request context for logging and correlation.
58
64
  * @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service.
59
- * @returns {Promise<ObsidianListFilesResponse>} A promise resolving to the structured success response
60
- * containing the listed directory path, a formatted tree string, and the total entry count.
61
- * @throws {McpError} Throws an McpError if the directory cannot be listed (e.g., not found)
62
- * or if any other API interaction or validation fails.
65
+ * @returns {Promise<ObsidianListFilesResponse>} A promise resolving to the structured success response.
66
+ * @throws {McpError} Throws an McpError if the initial directory is not found or another error occurs.
63
67
  */
64
68
  export declare const processObsidianListFiles: (params: ObsidianListFilesInput, context: RequestContext, obsidianService: ObsidianRestApiService) => Promise<ObsidianListFilesResponse>;
@@ -1,4 +1,10 @@
1
- import path from "node:path"; // Using POSIX path functions for vault path manipulation
1
+ /**
2
+ * @fileoverview Core logic for the 'obsidian_list_files' tool.
3
+ * This module defines the input schema, response types, and processing logic for
4
+ * recursively listing files and directories in an Obsidian vault with filtering.
5
+ * @module src/mcp-server/tools/obsidianListFilesTool/logic
6
+ */
7
+ import path from "node:path";
2
8
  import { z } from "zod";
3
9
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
4
10
  import { logger, retryWithDelay, } from "../../../utils/index.js";
@@ -12,7 +18,6 @@ export const ObsidianListFilesInputSchema = z
12
18
  .object({
13
19
  /**
14
20
  * The vault-relative path to the directory whose contents should be listed.
15
- * Examples: "Attachments/Images", "Projects", "" (for vault root), "/" (for vault root).
16
21
  * The path is treated as case-sensitive by the underlying Obsidian API.
17
22
  */
18
23
  dirPath: z
@@ -20,160 +25,190 @@ export const ObsidianListFilesInputSchema = z
20
25
  .describe('The vault-relative path to the directory to list (e.g., "developer/atlas-mcp-server", "/" for root). Case-sensitive.'),
21
26
  /**
22
27
  * Optional array of file extensions (including the leading dot) to filter the results.
23
- * Only files matching one of these extensions will be included. Directories are always included regardless of this filter.
24
- * Example: [".md", ".png"]
28
+ * Only files matching one of these extensions will be included. Directories are always included.
25
29
  */
26
30
  fileExtensionFilter: z
27
31
  .array(z.string().startsWith(".", "Extension must start with a dot '.'"))
28
32
  .optional()
29
- .describe('Optional array of file extensions (e.g., [".md") to filter files. Directories are always included.'),
33
+ .describe('Optional array of file extensions (e.g., [".md"]) to filter files. Directories are always included.'),
30
34
  /**
31
35
  * Optional JavaScript-compatible regular expression pattern string to filter results by name.
32
36
  * Only files and directories whose names match the regex will be included.
33
- * Example: "^\\d{4}-\\d{2}-\\d{2}" (matches names starting with YYYY-MM-DD)
34
37
  */
35
38
  nameRegexFilter: z
36
39
  .string()
37
40
  .nullable()
38
- .optional() // Allow null in addition to string/undefined
41
+ .optional()
39
42
  .describe("Optional regex pattern (JavaScript syntax) to filter results by name."),
43
+ /**
44
+ * The maximum depth of subdirectories to list recursively.
45
+ * - A value of `0` lists only the files and directories in the specified `dirPath`.
46
+ * - A value of `1` lists the contents of `dirPath` and the contents of its immediate subdirectories.
47
+ * - A value of `-1` (the default) indicates infinite recursion, listing all subdirectories.
48
+ */
49
+ recursionDepth: z
50
+ .number()
51
+ .int()
52
+ .default(-1)
53
+ .describe("Maximum recursion depth. 0 for no recursion, -1 for infinite (default)."),
40
54
  })
41
- .describe("Input parameters for listing files and subdirectories within a specified Obsidian vault directory, with optional filtering.");
55
+ .describe("Input parameters for listing files and subdirectories within a specified Obsidian vault directory, with optional filtering and recursion.");
42
56
  // ====================================================================================
43
57
  // Helper Functions
44
58
  // ====================================================================================
45
59
  /**
46
- * Formats a list of file and directory names into a simple tree-like string representation.
47
- * Directories (indicated by a trailing '/') are listed first, then files, both sorted alphabetically.
60
+ * Recursively builds a formatted tree string from a nested array of FileTreeNode objects.
48
61
  *
49
- * @param {string[]} fileNames - An array of file and directory names (directories should end with '/').
50
- * @returns {string} A formatted string representing the directory tree, or "(empty directory)" if the input array is empty.
62
+ * @param {FileTreeNode[]} nodes - The array of nodes to format.
63
+ * @param {string} [indent=""] - The indentation prefix for the current level.
64
+ * @returns {{ tree: string, count: number }} An object containing the formatted tree string and the total count of entries.
51
65
  */
52
- function formatAsTree(fileNames) {
53
- if (!fileNames || fileNames.length === 0) {
54
- return "(empty directory)";
55
- }
56
- // Sort entries: directories first, then files, alphabetically within each group.
57
- fileNames.sort((a, b) => {
58
- const aIsDir = a.endsWith("/");
59
- const bIsDir = b.endsWith("/");
60
- // Group directories before files
61
- if (aIsDir && !bIsDir)
62
- return -1; // a (dir) comes before b (file)
63
- if (!aIsDir && bIsDir)
64
- return 1; // b (dir) comes before a (file)
65
- // Within the same type (both dirs or both files), sort alphabetically.
66
- // Remove trailing slash for comparison if it's a directory.
67
- const nameA = aIsDir ? a.slice(0, -1) : a;
68
- const nameB = bIsDir ? b.slice(0, -1) : b;
69
- return nameA.localeCompare(nameB);
70
- });
71
- // Build the tree string with prefixes
66
+ function formatTree(nodes, indent = "") {
72
67
  let treeString = "";
73
- const lastIndex = fileNames.length - 1;
74
- fileNames.forEach((name, index) => {
75
- const isLast = index === lastIndex;
76
- const prefix = isLast ? "└── " : "├── "; // Use different connectors for the last item
77
- treeString += prefix + name + (isLast ? "" : "\n"); // Add newline except for the last item
68
+ let count = nodes.length;
69
+ nodes.forEach((node, index) => {
70
+ const isLast = index === nodes.length - 1;
71
+ const prefix = isLast ? "└── " : "├── ";
72
+ const childIndent = isLast ? " " : "";
73
+ treeString += `${indent}${prefix}${node.name}\n`;
74
+ if (node.children && node.children.length > 0) {
75
+ const result = formatTree(node.children, indent + childIndent);
76
+ treeString += result.tree;
77
+ count += result.count;
78
+ }
78
79
  });
79
- return treeString;
80
+ return { tree: treeString, count };
81
+ }
82
+ /**
83
+ * Recursively builds a file tree by fetching directory contents from the Obsidian API.
84
+ *
85
+ * @param {string} dirPath - The path of the directory to process.
86
+ * @param {number} currentDepth - The current recursion depth.
87
+ * @param {ObsidianListFilesInput} params - The original validated input parameters, including filters and max depth.
88
+ * @param {RequestContext} context - The request context for logging.
89
+ * @param {ObsidianRestApiService} obsidianService - The Obsidian API service instance.
90
+ * @returns {Promise<FileTreeNode[]>} A promise that resolves to an array of file tree nodes.
91
+ */
92
+ async function buildFileTree(dirPath, currentDepth, params, context, obsidianService) {
93
+ const { recursionDepth, fileExtensionFilter, nameRegexFilter } = params;
94
+ // Stop recursion if max depth is reached (and it's not infinite)
95
+ if (recursionDepth !== -1 && currentDepth > recursionDepth) {
96
+ return [];
97
+ }
98
+ let fileNames;
99
+ try {
100
+ fileNames = await obsidianService.listFiles(dirPath, context);
101
+ }
102
+ catch (error) {
103
+ if (error instanceof McpError && error.code === BaseErrorCode.NOT_FOUND) {
104
+ logger.warning(`Directory not found during recursive list: ${dirPath}. Skipping.`, context);
105
+ return []; // Return empty array if a subdirectory is not found
106
+ }
107
+ throw error; // Re-throw other errors
108
+ }
109
+ const regex = nameRegexFilter && nameRegexFilter.trim() !== ""
110
+ ? new RegExp(nameRegexFilter)
111
+ : null;
112
+ const treeNodes = [];
113
+ for (const name of fileNames) {
114
+ const fullPath = path.posix.join(dirPath, name);
115
+ const isDirectory = name.endsWith("/");
116
+ const cleanName = isDirectory ? name.slice(0, -1) : name;
117
+ // Apply filters
118
+ if (regex && !regex.test(cleanName)) {
119
+ continue;
120
+ }
121
+ if (!isDirectory &&
122
+ fileExtensionFilter &&
123
+ fileExtensionFilter.length > 0) {
124
+ const extension = path.posix.extname(name);
125
+ if (!fileExtensionFilter.includes(extension)) {
126
+ continue;
127
+ }
128
+ }
129
+ const node = {
130
+ name: cleanName,
131
+ type: isDirectory ? "directory" : "file",
132
+ children: [],
133
+ };
134
+ if (isDirectory) {
135
+ node.name += "/"; // Add trailing slash back for display
136
+ node.children = await buildFileTree(fullPath, currentDepth + 1, params, context, obsidianService);
137
+ }
138
+ treeNodes.push(node);
139
+ }
140
+ // Sort entries: directories first, then files, alphabetically
141
+ treeNodes.sort((a, b) => {
142
+ if (a.type === "directory" && b.type === "file")
143
+ return -1;
144
+ if (a.type === "file" && b.type === "directory")
145
+ return 1;
146
+ return a.name.localeCompare(b.name);
147
+ });
148
+ return treeNodes;
80
149
  }
81
150
  // ====================================================================================
82
151
  // Core Logic Function
83
152
  // ====================================================================================
84
153
  /**
85
- * Processes the core logic for listing files and directories within a specified
86
- * directory in the Obsidian vault. Applies optional filters and formats the output.
154
+ * Processes the core logic for listing files and directories recursively within the Obsidian vault.
87
155
  *
88
156
  * @param {ObsidianListFilesInput} params - The validated input parameters.
89
157
  * @param {RequestContext} context - The request context for logging and correlation.
90
158
  * @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service.
91
- * @returns {Promise<ObsidianListFilesResponse>} A promise resolving to the structured success response
92
- * containing the listed directory path, a formatted tree string, and the total entry count.
93
- * @throws {McpError} Throws an McpError if the directory cannot be listed (e.g., not found)
94
- * or if any other API interaction or validation fails.
159
+ * @returns {Promise<ObsidianListFilesResponse>} A promise resolving to the structured success response.
160
+ * @throws {McpError} Throws an McpError if the initial directory is not found or another error occurs.
95
161
  */
96
162
  export const processObsidianListFiles = async (params, context, obsidianService) => {
97
- const { dirPath, fileExtensionFilter, nameRegexFilter } = params;
98
- // Normalize dirPath for logging and response (use "/" for root)
163
+ const { dirPath } = params;
99
164
  const dirPathForLog = dirPath === "" || dirPath === "/" ? "/" : dirPath;
100
- logger.debug(`Processing obsidian_list_files request for path: ${dirPathForLog}`, { ...context, fileExtensionFilter, nameRegexFilter });
165
+ logger.debug(`Processing obsidian_list_files request for path: ${dirPathForLog}`, { ...context, params });
101
166
  try {
102
- // Normalize path for the API call as well
103
167
  const effectiveDirPath = dirPath === "" ? "/" : dirPath;
104
- // --- Step 1: Fetch initial list from Obsidian API ---
105
- const listContext = { ...context, operation: "listFilesApiCall" };
106
- logger.debug(`Calling Obsidian API to list directory: ${effectiveDirPath}`, listContext);
168
+ // --- Step 1: Build the file tree recursively with retry for the initial call ---
169
+ const buildTreeContext = { ...context, operation: "buildFileTreeWithRetry" };
107
170
  const shouldRetryNotFound = (err) => err instanceof McpError && err.code === BaseErrorCode.NOT_FOUND;
108
- let fileNames = await retryWithDelay(() => obsidianService.listFiles(effectiveDirPath, listContext), {
109
- operationName: "listFilesWithRetry",
110
- context: listContext,
171
+ const fileTree = await retryWithDelay(() => buildFileTree(effectiveDirPath, 0, // Start at depth 0
172
+ params, buildTreeContext, obsidianService), {
173
+ operationName: "buildFileTreeWithRetry",
174
+ context: buildTreeContext,
111
175
  maxRetries: 3,
112
176
  delayMs: 300,
113
177
  shouldRetry: shouldRetryNotFound,
114
178
  });
115
- logger.debug(`Successfully listed ${fileNames.length} initial items in: ${dirPathForLog}`, listContext);
116
- // --- Step 2: Apply Filters ---
117
- const filterContext = { ...context, operation: "applyFilters" };
118
- // Apply extension filter if provided
119
- if (fileExtensionFilter && fileExtensionFilter.length > 0) {
120
- const initialCount = fileNames.length;
121
- fileNames = fileNames.filter((fileName) => {
122
- // Always keep directories (identified by trailing '/')
123
- if (fileName.endsWith("/"))
124
- return true;
125
- // Check if the file's extension is in the filter list
126
- const extension = path.posix.extname(fileName); // Use path.posix.extname for consistency
127
- return fileExtensionFilter.includes(extension);
128
- });
129
- logger.debug(`Applied extension filter (${fileExtensionFilter.join(", ")}). ${initialCount} -> ${fileNames.length} items remaining.`, filterContext);
130
- }
131
- // Apply regex name filter if provided and is a non-empty string
132
- if (nameRegexFilter && nameRegexFilter.trim() !== "") {
133
- const initialCount = fileNames.length;
134
- try {
135
- const regex = new RegExp(nameRegexFilter); // Compile the regex pattern
136
- fileNames = fileNames.filter((fileName) => regex.test(fileName)); // Test each name against the regex
137
- logger.debug(`Applied regex filter /${nameRegexFilter}/. ${initialCount} -> ${fileNames.length} items remaining.`, filterContext);
138
- }
139
- catch (regexError) {
140
- // Handle invalid regex patterns provided by the user
141
- logger.error(`Invalid regex pattern provided: ${nameRegexFilter}`, regexError instanceof Error ? regexError : undefined, filterContext);
142
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, // It's an input validation issue
143
- `Invalid regex pattern provided for nameRegexFilter: ${nameRegexFilter}. Error: ${regexError instanceof Error ? regexError.message : "Unknown regex error"}`, filterContext);
144
- }
145
- }
146
- // --- Step 3: Format Output and Return ---
179
+ // --- Step 2: Format the tree and count entries ---
147
180
  const formatContext = { ...context, operation: "formatResponse" };
148
- const totalEntries = fileNames.length;
149
- logger.debug(`Formatting final list of ${totalEntries} entries as tree.`, formatContext);
150
- // Format the potentially filtered list into a tree string
151
- const treeString = formatAsTree(fileNames);
152
- // Construct the final response object
181
+ if (fileTree.length === 0) {
182
+ logger.debug("Directory is empty or all items were filtered out.", formatContext);
183
+ return {
184
+ directoryPath: dirPathForLog,
185
+ tree: "(empty or all items filtered)",
186
+ totalEntries: 0,
187
+ };
188
+ }
189
+ const { tree, count } = formatTree(fileTree);
190
+ // --- Step 3: Construct and return the response ---
153
191
  const response = {
154
- directoryPath: dirPathForLog, // Return the normalized path
155
- tree: treeString,
156
- totalEntries: totalEntries,
192
+ directoryPath: dirPathForLog,
193
+ tree: tree.trimEnd(), // Remove trailing newline
194
+ totalEntries: count,
157
195
  };
158
- logger.debug(`Successfully processed list request for ${dirPathForLog}.`, context);
196
+ logger.debug(`Successfully processed list request for ${dirPathForLog}. Found ${count} entries.`, context);
159
197
  return response;
160
198
  }
161
199
  catch (error) {
162
- // Handle errors, ensuring they are McpError instances before re-throwing.
163
200
  if (error instanceof McpError) {
164
- // Provide a more specific message if the directory wasn't found
201
+ // Provide a more specific message if the directory wasn't found after retries
165
202
  if (error.code === BaseErrorCode.NOT_FOUND) {
166
- logger.error(`Directory not found for listing: ${dirPathForLog}`, error, context);
167
- throw new McpError(error.code, `Directory not found for listing: ${dirPathForLog}`, context);
203
+ const notFoundMsg = `Directory not found after retries: ${dirPathForLog}`;
204
+ logger.error(notFoundMsg, error, context);
205
+ throw new McpError(error.code, notFoundMsg, context);
168
206
  }
169
207
  logger.error(`McpError during file listing for ${dirPathForLog}: ${error.message}`, error, context);
170
- throw error; // Re-throw known McpError
171
- }
172
- else {
173
- // Catch and wrap unexpected errors
174
- const errorMessage = `Unexpected error listing Obsidian files in ${dirPathForLog}`;
175
- logger.error(errorMessage, error instanceof Error ? error : undefined, context);
176
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `${errorMessage}: ${error instanceof Error ? error.message : String(error)}`, context);
208
+ throw error;
177
209
  }
210
+ const errorMessage = `Unexpected error listing Obsidian files in ${dirPathForLog}`;
211
+ logger.error(errorMessage, error instanceof Error ? error : undefined, context);
212
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `${errorMessage}: ${error instanceof Error ? error.message : String(error)}`, context);
178
213
  }
179
214
  };
@@ -1,14 +1,18 @@
1
+ /**
2
+ * @fileoverview Registers the 'obsidian_list_files' tool with the MCP server.
3
+ * This file defines the tool's metadata and sets up the handler that links
4
+ * the tool call to its core processing logic.
5
+ * @module src/mcp-server/tools/obsidianListFilesTool/registration
6
+ */
1
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
8
  import { ObsidianRestApiService } from "../../../services/obsidianRestAPI/index.js";
3
9
  /**
4
10
  * Registers the 'obsidian_list_files' tool with the MCP server.
5
11
  *
6
12
  * This tool lists the files and subdirectories within a specified directory
7
- * in the user's Obsidian vault. It supports optional filtering by file extension
8
- * or by a regular expression matching the entry name.
9
- *
10
- * The response includes the path of the listed directory, a formatted tree string
11
- * representing the contents, and the total count of entries listed after filtering.
13
+ * in the user's Obsidian vault. It supports optional filtering by file extension,
14
+ * by a regular expression matching the entry name, and recursive listing up to a
15
+ * specified depth.
12
16
  *
13
17
  * @param {McpServer} server - The MCP server instance to register the tool with.
14
18
  * @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service
@@ -1,3 +1,9 @@
1
+ /**
2
+ * @fileoverview Registers the 'obsidian_list_files' tool with the MCP server.
3
+ * This file defines the tool's metadata and sets up the handler that links
4
+ * the tool call to its core processing logic.
5
+ * @module src/mcp-server/tools/obsidianListFilesTool/registration
6
+ */
1
7
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
2
8
  import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
3
9
  import { ObsidianListFilesInputSchema, processObsidianListFiles, } from "./logic.js";
@@ -5,11 +11,9 @@ import { ObsidianListFilesInputSchema, processObsidianListFiles, } from "./logic
5
11
  * Registers the 'obsidian_list_files' tool with the MCP server.
6
12
  *
7
13
  * This tool lists the files and subdirectories within a specified directory
8
- * in the user's Obsidian vault. It supports optional filtering by file extension
9
- * or by a regular expression matching the entry name.
10
- *
11
- * The response includes the path of the listed directory, a formatted tree string
12
- * representing the contents, and the total count of entries listed after filtering.
14
+ * in the user's Obsidian vault. It supports optional filtering by file extension,
15
+ * by a regular expression matching the entry name, and recursive listing up to a
16
+ * specified depth.
13
17
  *
14
18
  * @param {McpServer} server - The MCP server instance to register the tool with.
15
19
  * @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service
@@ -19,8 +23,7 @@ import { ObsidianListFilesInputSchema, processObsidianListFiles, } from "./logic
19
23
  */
20
24
  export const registerObsidianListFilesTool = async (server, obsidianService) => {
21
25
  const toolName = "obsidian_list_files";
22
- // Updated description to reflect the simplified response (path, tree, count)
23
- const toolDescription = "Lists files and subdirectories within a specified Obsidian vault folder. Supports optional filtering by extension or name regex. Returns an object containing the listed directory path, a formatted tree string of its contents, and the total entry count. Use an empty string or '/' for dirPath to list the vault root.";
26
+ const toolDescription = "Lists files and subdirectories within a specified Obsidian vault folder. Supports optional filtering by extension or name regex, and recursive listing to a specified depth (-1 for infinite). Returns an object containing the listed directory path, a formatted tree string of its contents, and the total entry count. Use an empty string or '/' for dirPath to list the vault root.";
24
27
  // Create a context specifically for the registration process.
25
28
  const registrationContext = requestContextService.createRequestContext({
26
29
  operation: "RegisterObsidianListFilesTool",
@@ -52,17 +55,16 @@ export const registerObsidianListFilesTool = async (server, obsidianService) =>
52
55
  dirPath: params.dirPath,
53
56
  fileExtensionFilter: params.fileExtensionFilter,
54
57
  nameRegexFilter: params.nameRegexFilter,
58
+ recursionDepth: params.recursionDepth,
55
59
  },
56
60
  });
57
61
  logger.debug(`Handling '${toolName}' request`, handlerContext);
58
62
  // Wrap the core logic execution in a tryCatch block.
59
63
  return await ErrorHandler.tryCatch(async () => {
60
64
  // Delegate the actual file listing and filtering logic to the processing function.
61
- // Note: The input schema and shape are identical here, so no separate refinement parse is needed.
62
65
  const response = await processObsidianListFiles(params, handlerContext, obsidianService);
63
66
  logger.debug(`'${toolName}' processed successfully`, handlerContext);
64
67
  // Format the successful response object from the logic function into the required MCP CallToolResult structure.
65
- // The entire response object (directoryPath, tree, totalEntries) is serialized to JSON.
66
68
  return {
67
69
  content: [
68
70
  {
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "obsidian-mcp-server",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "Obsidian Knowledge-Management MCP (Model Context Protocol) server that enables AI agents and development tools to interact with an Obsidian vault. It provides a comprehensive suite of tools for reading, writing, searching, and managing notes, tags, and frontmatter, acting as a bridge to the Obsidian Local REST API plugin.",
5
5
  "main": "dist/index.js",
6
6
  "files": [
7
- "dist"
7
+ "dist",
8
+ "README.md",
9
+ "LICENSE",
10
+ "CHANGELOG.md"
8
11
  ],
9
12
  "bin": {
10
13
  "obsidian-mcp-server": "dist/index.js"