obsidian-mcp-server 1.5.8 → 2.0.2

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.
Files changed (221) hide show
  1. package/README.md +248 -104
  2. package/dist/config/index.d.ts +41 -0
  3. package/dist/config/index.js +191 -0
  4. package/dist/index.d.ts +1 -5
  5. package/dist/index.js +296 -18
  6. package/dist/mcp-server/server.d.ts +33 -0
  7. package/dist/mcp-server/server.js +211 -0
  8. package/dist/mcp-server/tools/obsidianDeleteFileTool/index.d.ts +12 -0
  9. package/dist/mcp-server/tools/obsidianDeleteFileTool/index.js +12 -0
  10. package/dist/mcp-server/tools/obsidianDeleteFileTool/logic.d.ts +51 -0
  11. package/dist/mcp-server/tools/obsidianDeleteFileTool/logic.js +168 -0
  12. package/dist/mcp-server/tools/obsidianDeleteFileTool/registration.d.ts +19 -0
  13. package/dist/mcp-server/tools/obsidianDeleteFileTool/registration.js +91 -0
  14. package/dist/mcp-server/tools/obsidianGlobalSearchTool/index.d.ts +12 -0
  15. package/dist/mcp-server/tools/obsidianGlobalSearchTool/index.js +12 -0
  16. package/dist/mcp-server/tools/obsidianGlobalSearchTool/logic.d.ts +77 -0
  17. package/dist/mcp-server/tools/obsidianGlobalSearchTool/logic.js +341 -0
  18. package/dist/mcp-server/tools/obsidianGlobalSearchTool/registration.d.ts +18 -0
  19. package/dist/mcp-server/tools/obsidianGlobalSearchTool/registration.js +69 -0
  20. package/dist/mcp-server/tools/obsidianListFilesTool/index.d.ts +12 -0
  21. package/dist/mcp-server/tools/obsidianListFilesTool/index.js +12 -0
  22. package/dist/mcp-server/tools/obsidianListFilesTool/logic.d.ts +64 -0
  23. package/dist/mcp-server/tools/obsidianListFilesTool/logic.js +179 -0
  24. package/dist/mcp-server/tools/obsidianListFilesTool/registration.d.ts +19 -0
  25. package/dist/mcp-server/tools/obsidianListFilesTool/registration.js +96 -0
  26. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/index.d.ts +3 -0
  27. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/index.js +2 -0
  28. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/logic.d.ts +42 -0
  29. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/logic.js +152 -0
  30. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/registration.d.ts +3 -0
  31. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/registration.js +52 -0
  32. package/dist/mcp-server/tools/obsidianManageTagsTool/index.d.ts +3 -0
  33. package/dist/mcp-server/tools/obsidianManageTagsTool/index.js +2 -0
  34. package/dist/mcp-server/tools/obsidianManageTagsTool/logic.d.ts +28 -0
  35. package/dist/mcp-server/tools/obsidianManageTagsTool/logic.js +161 -0
  36. package/dist/mcp-server/tools/obsidianManageTagsTool/registration.d.ts +3 -0
  37. package/dist/mcp-server/tools/obsidianManageTagsTool/registration.js +52 -0
  38. package/dist/mcp-server/tools/obsidianReadFileTool/index.d.ts +12 -0
  39. package/dist/mcp-server/tools/obsidianReadFileTool/index.js +12 -0
  40. package/dist/mcp-server/tools/obsidianReadFileTool/logic.d.ts +87 -0
  41. package/dist/mcp-server/tools/obsidianReadFileTool/logic.js +216 -0
  42. package/dist/mcp-server/tools/obsidianReadFileTool/registration.d.ts +20 -0
  43. package/dist/mcp-server/tools/obsidianReadFileTool/registration.js +101 -0
  44. package/dist/mcp-server/tools/obsidianSearchReplaceTool/index.d.ts +12 -0
  45. package/dist/mcp-server/tools/obsidianSearchReplaceTool/index.js +12 -0
  46. package/dist/mcp-server/tools/obsidianSearchReplaceTool/logic.d.ts +255 -0
  47. package/dist/mcp-server/tools/obsidianSearchReplaceTool/logic.js +583 -0
  48. package/dist/mcp-server/tools/obsidianSearchReplaceTool/registration.d.ts +22 -0
  49. package/dist/mcp-server/tools/obsidianSearchReplaceTool/registration.js +111 -0
  50. package/dist/mcp-server/tools/obsidianUpdateFileTool/index.d.ts +12 -0
  51. package/dist/mcp-server/tools/obsidianUpdateFileTool/index.js +12 -0
  52. package/dist/mcp-server/tools/obsidianUpdateFileTool/logic.d.ts +183 -0
  53. package/dist/mcp-server/tools/obsidianUpdateFileTool/logic.js +490 -0
  54. package/dist/mcp-server/tools/obsidianUpdateFileTool/registration.d.ts +21 -0
  55. package/dist/mcp-server/tools/obsidianUpdateFileTool/registration.js +108 -0
  56. package/dist/mcp-server/transports/authentication/authContext.d.ts +33 -0
  57. package/dist/mcp-server/transports/authentication/authContext.js +24 -0
  58. package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +30 -0
  59. package/dist/mcp-server/transports/authentication/authMiddleware.js +145 -0
  60. package/dist/mcp-server/transports/authentication/authUtils.d.ts +18 -0
  61. package/dist/mcp-server/transports/authentication/authUtils.js +45 -0
  62. package/dist/mcp-server/transports/authentication/oauthMiddleware.d.ts +24 -0
  63. package/dist/mcp-server/transports/authentication/oauthMiddleware.js +109 -0
  64. package/dist/mcp-server/transports/authentication/types.d.ts +17 -0
  65. package/dist/mcp-server/transports/authentication/types.js +5 -0
  66. package/dist/mcp-server/transports/httpTransport.d.ts +24 -0
  67. package/dist/mcp-server/transports/httpTransport.js +496 -0
  68. package/dist/mcp-server/transports/stdioTransport.d.ts +42 -0
  69. package/dist/mcp-server/transports/stdioTransport.js +63 -0
  70. package/dist/services/obsidianRestAPI/index.d.ts +15 -0
  71. package/dist/services/obsidianRestAPI/index.js +17 -0
  72. package/dist/services/obsidianRestAPI/methods/activeFileMethods.d.ts +38 -0
  73. package/dist/services/obsidianRestAPI/methods/activeFileMethods.js +62 -0
  74. package/dist/services/obsidianRestAPI/methods/commandMethods.d.ts +22 -0
  75. package/dist/services/obsidianRestAPI/methods/commandMethods.js +31 -0
  76. package/dist/services/obsidianRestAPI/methods/openMethods.d.ts +16 -0
  77. package/dist/services/obsidianRestAPI/methods/openMethods.js +21 -0
  78. package/dist/services/obsidianRestAPI/methods/patchMethods.d.ts +37 -0
  79. package/dist/services/obsidianRestAPI/methods/patchMethods.js +94 -0
  80. package/dist/services/obsidianRestAPI/methods/periodicNoteMethods.d.ts +42 -0
  81. package/dist/services/obsidianRestAPI/methods/periodicNoteMethods.js +66 -0
  82. package/dist/services/obsidianRestAPI/methods/searchMethods.d.ts +25 -0
  83. package/dist/services/obsidianRestAPI/methods/searchMethods.js +36 -0
  84. package/dist/services/obsidianRestAPI/methods/vaultMethods.d.ts +58 -0
  85. package/dist/services/obsidianRestAPI/methods/vaultMethods.js +144 -0
  86. package/dist/services/obsidianRestAPI/service.d.ts +195 -0
  87. package/dist/services/obsidianRestAPI/service.js +379 -0
  88. package/dist/services/obsidianRestAPI/types.d.ts +127 -0
  89. package/dist/services/obsidianRestAPI/types.js +7 -0
  90. package/dist/services/obsidianRestAPI/vaultCache/index.d.ts +4 -0
  91. package/dist/services/obsidianRestAPI/vaultCache/index.js +4 -0
  92. package/dist/services/obsidianRestAPI/vaultCache/service.d.ts +88 -0
  93. package/dist/services/obsidianRestAPI/vaultCache/service.js +299 -0
  94. package/dist/types-global/errors.d.ts +73 -0
  95. package/dist/types-global/errors.js +71 -0
  96. package/dist/utils/index.d.ts +5 -8
  97. package/dist/utils/index.js +13 -9
  98. package/dist/utils/internal/asyncUtils.d.ts +54 -0
  99. package/dist/utils/internal/asyncUtils.js +101 -0
  100. package/dist/utils/internal/errorHandler.d.ts +176 -0
  101. package/dist/utils/internal/errorHandler.js +351 -0
  102. package/dist/utils/internal/index.d.ts +4 -0
  103. package/dist/utils/internal/index.js +4 -0
  104. package/dist/utils/internal/logger.d.ts +141 -0
  105. package/dist/utils/internal/logger.js +406 -0
  106. package/dist/utils/internal/requestContext.d.ts +83 -0
  107. package/dist/utils/internal/requestContext.js +72 -0
  108. package/dist/utils/metrics/index.d.ts +1 -0
  109. package/dist/utils/metrics/index.js +1 -0
  110. package/dist/utils/metrics/tokenCounter.d.ts +27 -0
  111. package/dist/utils/metrics/tokenCounter.js +128 -0
  112. package/dist/utils/obsidian/index.d.ts +5 -0
  113. package/dist/utils/obsidian/index.js +5 -0
  114. package/dist/utils/obsidian/obsidianApiUtils.d.ts +14 -0
  115. package/dist/utils/obsidian/obsidianApiUtils.js +29 -0
  116. package/dist/utils/obsidian/obsidianStatUtils.d.ts +68 -0
  117. package/dist/utils/obsidian/obsidianStatUtils.js +143 -0
  118. package/dist/utils/parsing/dateParser.d.ts +56 -0
  119. package/dist/utils/parsing/dateParser.js +104 -0
  120. package/dist/utils/parsing/index.d.ts +2 -0
  121. package/dist/utils/parsing/index.js +3 -0
  122. package/dist/utils/parsing/jsonParser.d.ts +80 -0
  123. package/dist/utils/parsing/jsonParser.js +133 -0
  124. package/dist/utils/security/idGenerator.d.ts +140 -0
  125. package/dist/utils/security/idGenerator.js +194 -0
  126. package/dist/utils/security/index.d.ts +3 -0
  127. package/dist/utils/security/index.js +3 -0
  128. package/dist/utils/security/rateLimiter.d.ts +156 -0
  129. package/dist/utils/security/rateLimiter.js +235 -0
  130. package/dist/utils/security/sanitization.d.ts +244 -0
  131. package/dist/utils/security/sanitization.js +599 -0
  132. package/package.json +58 -37
  133. package/dist/index.js.map +0 -1
  134. package/dist/mcp/handlers.d.ts +0 -29
  135. package/dist/mcp/handlers.js +0 -305
  136. package/dist/mcp/handlers.js.map +0 -1
  137. package/dist/mcp/index.d.ts +0 -6
  138. package/dist/mcp/index.js +0 -7
  139. package/dist/mcp/index.js.map +0 -1
  140. package/dist/mcp/server.d.ts +0 -18
  141. package/dist/mcp/server.js +0 -240
  142. package/dist/mcp/server.js.map +0 -1
  143. package/dist/mcp/types.d.ts +0 -70
  144. package/dist/mcp/types.js +0 -49
  145. package/dist/mcp/types.js.map +0 -1
  146. package/dist/obsidian/client.d.ts +0 -109
  147. package/dist/obsidian/client.js +0 -403
  148. package/dist/obsidian/client.js.map +0 -1
  149. package/dist/obsidian/errors.d.ts +0 -28
  150. package/dist/obsidian/errors.js +0 -75
  151. package/dist/obsidian/errors.js.map +0 -1
  152. package/dist/obsidian/index.d.ts +0 -6
  153. package/dist/obsidian/index.js +0 -7
  154. package/dist/obsidian/index.js.map +0 -1
  155. package/dist/obsidian/types.d.ts +0 -107
  156. package/dist/obsidian/types.js +0 -12
  157. package/dist/obsidian/types.js.map +0 -1
  158. package/dist/resources/index.d.ts +0 -13
  159. package/dist/resources/index.js +0 -15
  160. package/dist/resources/index.js.map +0 -1
  161. package/dist/resources/tags.d.ts +0 -39
  162. package/dist/resources/tags.js +0 -257
  163. package/dist/resources/tags.js.map +0 -1
  164. package/dist/resources/types.d.ts +0 -27
  165. package/dist/resources/types.js +0 -5
  166. package/dist/resources/types.js.map +0 -1
  167. package/dist/tools/base.d.ts +0 -46
  168. package/dist/tools/base.js +0 -88
  169. package/dist/tools/base.js.map +0 -1
  170. package/dist/tools/files/content.d.ts +0 -58
  171. package/dist/tools/files/content.js +0 -171
  172. package/dist/tools/files/content.js.map +0 -1
  173. package/dist/tools/files/index.d.ts +0 -14
  174. package/dist/tools/files/index.js +0 -22
  175. package/dist/tools/files/index.js.map +0 -1
  176. package/dist/tools/files/list.d.ts +0 -35
  177. package/dist/tools/files/list.js +0 -133
  178. package/dist/tools/files/list.js.map +0 -1
  179. package/dist/tools/index.d.ts +0 -21
  180. package/dist/tools/index.js +0 -31
  181. package/dist/tools/index.js.map +0 -1
  182. package/dist/tools/properties/index.d.ts +0 -14
  183. package/dist/tools/properties/index.js +0 -19
  184. package/dist/tools/properties/index.js.map +0 -1
  185. package/dist/tools/properties/manager.d.ts +0 -62
  186. package/dist/tools/properties/manager.js +0 -302
  187. package/dist/tools/properties/manager.js.map +0 -1
  188. package/dist/tools/properties/tools.d.ts +0 -47
  189. package/dist/tools/properties/tools.js +0 -239
  190. package/dist/tools/properties/tools.js.map +0 -1
  191. package/dist/tools/properties/types.d.ts +0 -141
  192. package/dist/tools/properties/types.js +0 -70
  193. package/dist/tools/properties/types.js.map +0 -1
  194. package/dist/tools/search/complex.d.ts +0 -38
  195. package/dist/tools/search/complex.js +0 -270
  196. package/dist/tools/search/complex.js.map +0 -1
  197. package/dist/tools/search/index.d.ts +0 -14
  198. package/dist/tools/search/index.js +0 -20
  199. package/dist/tools/search/index.js.map +0 -1
  200. package/dist/tools/search/simple.d.ts +0 -25
  201. package/dist/tools/search/simple.js +0 -127
  202. package/dist/tools/search/simple.js.map +0 -1
  203. package/dist/utils/errors.d.ts +0 -24
  204. package/dist/utils/errors.js +0 -59
  205. package/dist/utils/errors.js.map +0 -1
  206. package/dist/utils/idGenerator.d.ts +0 -15
  207. package/dist/utils/idGenerator.js +0 -21
  208. package/dist/utils/idGenerator.js.map +0 -1
  209. package/dist/utils/index.js.map +0 -1
  210. package/dist/utils/logging.d.ts +0 -245
  211. package/dist/utils/logging.js +0 -417
  212. package/dist/utils/logging.js.map +0 -1
  213. package/dist/utils/rate-limiting.d.ts +0 -50
  214. package/dist/utils/rate-limiting.js +0 -94
  215. package/dist/utils/rate-limiting.js.map +0 -1
  216. package/dist/utils/tokenization.d.ts +0 -28
  217. package/dist/utils/tokenization.js +0 -75
  218. package/dist/utils/tokenization.js.map +0 -1
  219. package/dist/utils/validation.d.ts +0 -22
  220. package/dist/utils/validation.js +0 -92
  221. package/dist/utils/validation.js.map +0 -1
@@ -0,0 +1,179 @@
1
+ import path from "node:path"; // Using POSIX path functions for vault path manipulation
2
+ import { z } from "zod";
3
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
4
+ import { logger, retryWithDelay, } from "../../../utils/index.js";
5
+ // ====================================================================================
6
+ // Schema Definitions for Input Validation
7
+ // ====================================================================================
8
+ /**
9
+ * Zod schema for validating the input parameters of the 'obsidian_list_files' tool.
10
+ */
11
+ export const ObsidianListFilesInputSchema = z
12
+ .object({
13
+ /**
14
+ * The vault-relative path to the directory whose contents should be listed.
15
+ * Examples: "Attachments/Images", "Projects", "" (for vault root), "/" (for vault root).
16
+ * The path is treated as case-sensitive by the underlying Obsidian API.
17
+ */
18
+ dirPath: z
19
+ .string()
20
+ .describe('The vault-relative path to the directory to list (e.g., "developer/atlas-mcp-server", "/" for root). Case-sensitive.'),
21
+ /**
22
+ * 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"]
25
+ */
26
+ fileExtensionFilter: z
27
+ .array(z.string().startsWith(".", "Extension must start with a dot '.'"))
28
+ .optional()
29
+ .describe('Optional array of file extensions (e.g., [".md") to filter files. Directories are always included.'),
30
+ /**
31
+ * Optional JavaScript-compatible regular expression pattern string to filter results by name.
32
+ * 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
+ */
35
+ nameRegexFilter: z
36
+ .string()
37
+ .nullable()
38
+ .optional() // Allow null in addition to string/undefined
39
+ .describe("Optional regex pattern (JavaScript syntax) to filter results by name."),
40
+ })
41
+ .describe("Input parameters for listing files and subdirectories within a specified Obsidian vault directory, with optional filtering.");
42
+ // ====================================================================================
43
+ // Helper Functions
44
+ // ====================================================================================
45
+ /**
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.
48
+ *
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.
51
+ */
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
72
+ 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
78
+ });
79
+ return treeString;
80
+ }
81
+ // ====================================================================================
82
+ // Core Logic Function
83
+ // ====================================================================================
84
+ /**
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.
87
+ *
88
+ * @param {ObsidianListFilesInput} params - The validated input parameters.
89
+ * @param {RequestContext} context - The request context for logging and correlation.
90
+ * @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.
95
+ */
96
+ export const processObsidianListFiles = async (params, context, obsidianService) => {
97
+ const { dirPath, fileExtensionFilter, nameRegexFilter } = params;
98
+ // Normalize dirPath for logging and response (use "/" for root)
99
+ const dirPathForLog = dirPath === "" || dirPath === "/" ? "/" : dirPath;
100
+ logger.debug(`Processing obsidian_list_files request for path: ${dirPathForLog}`, { ...context, fileExtensionFilter, nameRegexFilter });
101
+ try {
102
+ // Normalize path for the API call as well
103
+ 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);
107
+ 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,
111
+ maxRetries: 3,
112
+ delayMs: 300,
113
+ shouldRetry: shouldRetryNotFound,
114
+ });
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 ---
147
+ 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
153
+ const response = {
154
+ directoryPath: dirPathForLog, // Return the normalized path
155
+ tree: treeString,
156
+ totalEntries: totalEntries,
157
+ };
158
+ logger.debug(`Successfully processed list request for ${dirPathForLog}.`, context);
159
+ return response;
160
+ }
161
+ catch (error) {
162
+ // Handle errors, ensuring they are McpError instances before re-throwing.
163
+ if (error instanceof McpError) {
164
+ // Provide a more specific message if the directory wasn't found
165
+ 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);
168
+ }
169
+ 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);
177
+ }
178
+ }
179
+ };
@@ -0,0 +1,19 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ObsidianRestApiService } from "../../../services/obsidianRestAPI/index.js";
3
+ /**
4
+ * Registers the 'obsidian_list_files' tool with the MCP server.
5
+ *
6
+ * 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.
12
+ *
13
+ * @param {McpServer} server - The MCP server instance to register the tool with.
14
+ * @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service
15
+ * used to interact with the user's Obsidian vault.
16
+ * @returns {Promise<void>} A promise that resolves when the tool registration is complete or rejects on error.
17
+ * @throws {McpError} Throws an McpError if registration fails critically.
18
+ */
19
+ export declare const registerObsidianListFilesTool: (server: McpServer, obsidianService: ObsidianRestApiService) => Promise<void>;
@@ -0,0 +1,96 @@
1
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
2
+ import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
3
+ import { ObsidianListFilesInputSchema, processObsidianListFiles, } from "./logic.js";
4
+ /**
5
+ * Registers the 'obsidian_list_files' tool with the MCP server.
6
+ *
7
+ * 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.
13
+ *
14
+ * @param {McpServer} server - The MCP server instance to register the tool with.
15
+ * @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service
16
+ * used to interact with the user's Obsidian vault.
17
+ * @returns {Promise<void>} A promise that resolves when the tool registration is complete or rejects on error.
18
+ * @throws {McpError} Throws an McpError if registration fails critically.
19
+ */
20
+ export const registerObsidianListFilesTool = async (server, obsidianService) => {
21
+ 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.";
24
+ // Create a context specifically for the registration process.
25
+ const registrationContext = requestContextService.createRequestContext({
26
+ operation: "RegisterObsidianListFilesTool",
27
+ toolName: toolName,
28
+ module: "ObsidianListFilesRegistration", // Identify the module
29
+ });
30
+ logger.info(`Attempting to register tool: ${toolName}`, registrationContext);
31
+ // Wrap the registration logic in a tryCatch block for robust error handling during server setup.
32
+ await ErrorHandler.tryCatch(async () => {
33
+ // Use the high-level SDK method `server.tool` for registration.
34
+ server.tool(toolName, toolDescription, ObsidianListFilesInputSchema.shape, // Provide the Zod schema shape for input definition.
35
+ /**
36
+ * The handler function executed when the 'obsidian_list_files' tool is called by the client.
37
+ *
38
+ * @param {ObsidianListFilesInput} params - The input parameters received from the client,
39
+ * validated against the ObsidianListFilesInputSchema shape.
40
+ * @returns {Promise<CallToolResult>} A promise resolving to the structured result for the MCP client,
41
+ * containing either the successful response data (serialized JSON) or an error indication.
42
+ */
43
+ async (params) => {
44
+ // Type matches the inferred input schema
45
+ // Create a specific context for this handler invocation.
46
+ const handlerContext = requestContextService.createRequestContext({
47
+ parentContext: registrationContext, // Link to registration context
48
+ operation: "HandleObsidianListFilesRequest",
49
+ toolName: toolName,
50
+ params: {
51
+ // Log all relevant parameters for debugging
52
+ dirPath: params.dirPath,
53
+ fileExtensionFilter: params.fileExtensionFilter,
54
+ nameRegexFilter: params.nameRegexFilter,
55
+ },
56
+ });
57
+ logger.debug(`Handling '${toolName}' request`, handlerContext);
58
+ // Wrap the core logic execution in a tryCatch block.
59
+ return await ErrorHandler.tryCatch(async () => {
60
+ // 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
+ const response = await processObsidianListFiles(params, handlerContext, obsidianService);
63
+ logger.debug(`'${toolName}' processed successfully`, handlerContext);
64
+ // 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
+ return {
67
+ content: [
68
+ {
69
+ type: "text", // Standard content type for structured JSON data
70
+ text: JSON.stringify(response, null, 2), // Pretty-print JSON
71
+ },
72
+ ],
73
+ isError: false, // Indicate successful execution
74
+ };
75
+ }, {
76
+ // Configuration for the inner error handler (processing logic).
77
+ operation: `processing ${toolName} handler`,
78
+ context: handlerContext,
79
+ input: params, // Log the full input parameters if an error occurs.
80
+ // Custom error mapping for consistent error reporting.
81
+ errorMapper: (error) => new McpError(error instanceof McpError
82
+ ? error.code
83
+ : BaseErrorCode.INTERNAL_ERROR, `Error processing ${toolName} tool: ${error instanceof Error ? error.message : "Unknown error"}`, { ...handlerContext }),
84
+ }); // End of inner ErrorHandler.tryCatch
85
+ }); // End of server.tool call
86
+ logger.info(`Tool registered successfully: ${toolName}`, registrationContext);
87
+ }, {
88
+ // Configuration for the outer error handler (registration process).
89
+ operation: `registering tool ${toolName}`,
90
+ context: registrationContext,
91
+ errorCode: BaseErrorCode.INTERNAL_ERROR, // Default error code for registration failure.
92
+ // Custom error mapping for registration failures.
93
+ errorMapper: (error) => new McpError(error instanceof McpError ? error.code : BaseErrorCode.INTERNAL_ERROR, `Failed to register tool '${toolName}': ${error instanceof Error ? error.message : "Unknown error"}`, { ...registrationContext }),
94
+ critical: true, // Treat registration failure as critical.
95
+ }); // End of outer ErrorHandler.tryCatch
96
+ };
@@ -0,0 +1,3 @@
1
+ export { ObsidianManageFrontmatterInputSchemaShape, processObsidianManageFrontmatter, } from "./logic.js";
2
+ export type { ObsidianManageFrontmatterInput, ObsidianManageFrontmatterResponse, } from "./logic.js";
3
+ export { registerObsidianManageFrontmatterTool } from "./registration.js";
@@ -0,0 +1,2 @@
1
+ export { ObsidianManageFrontmatterInputSchemaShape, processObsidianManageFrontmatter, } from "./logic.js";
2
+ export { registerObsidianManageFrontmatterTool } from "./registration.js";
@@ -0,0 +1,42 @@
1
+ import { z } from "zod";
2
+ import { ObsidianRestApiService, VaultCacheService } from "../../../services/obsidianRestAPI/index.js";
3
+ import { RequestContext } from "../../../utils/index.js";
4
+ export declare const ObsidianManageFrontmatterInputSchemaShape: {
5
+ filePath: z.ZodString;
6
+ operation: z.ZodEnum<["get", "set", "delete"]>;
7
+ key: z.ZodString;
8
+ value: z.ZodOptional<z.ZodAny>;
9
+ };
10
+ export declare const ManageFrontmatterInputSchema: z.ZodEffects<z.ZodObject<{
11
+ filePath: z.ZodString;
12
+ operation: z.ZodEnum<["get", "set", "delete"]>;
13
+ key: z.ZodString;
14
+ value: z.ZodOptional<z.ZodAny>;
15
+ }, "strip", z.ZodTypeAny, {
16
+ operation: "get" | "delete" | "set";
17
+ key: string;
18
+ filePath: string;
19
+ value?: any;
20
+ }, {
21
+ operation: "get" | "delete" | "set";
22
+ key: string;
23
+ filePath: string;
24
+ value?: any;
25
+ }>, {
26
+ operation: "get" | "delete" | "set";
27
+ key: string;
28
+ filePath: string;
29
+ value?: any;
30
+ }, {
31
+ operation: "get" | "delete" | "set";
32
+ key: string;
33
+ filePath: string;
34
+ value?: any;
35
+ }>;
36
+ export type ObsidianManageFrontmatterInput = z.infer<typeof ManageFrontmatterInputSchema>;
37
+ export interface ObsidianManageFrontmatterResponse {
38
+ success: boolean;
39
+ message: string;
40
+ value?: any;
41
+ }
42
+ export declare const processObsidianManageFrontmatter: (params: ObsidianManageFrontmatterInput, context: RequestContext, obsidianService: ObsidianRestApiService, vaultCacheService: VaultCacheService | undefined) => Promise<ObsidianManageFrontmatterResponse>;
@@ -0,0 +1,152 @@
1
+ import { z } from "zod";
2
+ import { dump } from "js-yaml";
3
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
4
+ import { logger, retryWithDelay, } from "../../../utils/index.js";
5
+ // ====================================================================================
6
+ // Schema Definitions
7
+ // ====================================================================================
8
+ const ManageFrontmatterInputSchemaBase = z.object({
9
+ filePath: z
10
+ .string()
11
+ .min(1)
12
+ .describe("The vault-relative path to the target note (e.g., 'Projects/Active/My Note.md')."),
13
+ operation: z
14
+ .enum(["get", "set", "delete"])
15
+ .describe("The operation to perform on the frontmatter: 'get' to read a key, 'set' to create or update a key, or 'delete' to remove a key."),
16
+ key: z
17
+ .string()
18
+ .min(1)
19
+ .describe("The name of the frontmatter key to target, such as 'status', 'tags', or 'aliases'."),
20
+ value: z
21
+ .any()
22
+ .optional()
23
+ .describe("The value to assign when using the 'set' operation. Can be a string, number, boolean, array, or a JSON object."),
24
+ });
25
+ export const ObsidianManageFrontmatterInputSchemaShape = ManageFrontmatterInputSchemaBase.shape;
26
+ export const ManageFrontmatterInputSchema = ManageFrontmatterInputSchemaBase.refine((data) => {
27
+ if (data.operation === "set" && data.value === undefined) {
28
+ return false;
29
+ }
30
+ return true;
31
+ }, {
32
+ message: "A 'value' is required when the 'operation' is 'set'.",
33
+ path: ["value"],
34
+ });
35
+ // ====================================================================================
36
+ // Core Logic Function
37
+ // ====================================================================================
38
+ export const processObsidianManageFrontmatter = async (params, context, obsidianService, vaultCacheService) => {
39
+ logger.debug(`Processing obsidian_manage_frontmatter request`, {
40
+ ...context,
41
+ operation: params.operation,
42
+ filePath: params.filePath,
43
+ key: params.key,
44
+ });
45
+ const { filePath, operation, key, value } = params;
46
+ const shouldRetryNotFound = (err) => err instanceof McpError && err.code === BaseErrorCode.NOT_FOUND;
47
+ const getFileWithRetry = async (opContext, format = "json") => {
48
+ return await retryWithDelay(() => obsidianService.getFileContent(filePath, format, opContext), {
49
+ operationName: `getFileContentForFrontmatter`,
50
+ context: opContext,
51
+ maxRetries: 3,
52
+ delayMs: 300,
53
+ shouldRetry: shouldRetryNotFound,
54
+ });
55
+ };
56
+ switch (operation) {
57
+ case "get": {
58
+ const note = (await getFileWithRetry(context));
59
+ const frontmatter = note.frontmatter ?? {};
60
+ const retrievedValue = frontmatter[key];
61
+ return {
62
+ success: true,
63
+ message: `Successfully retrieved key '${key}' from frontmatter.`,
64
+ value: retrievedValue,
65
+ };
66
+ }
67
+ case "set": {
68
+ const patchOptions = {
69
+ operation: "replace",
70
+ targetType: "frontmatter",
71
+ target: key,
72
+ createTargetIfMissing: true,
73
+ contentType: typeof value === "object" ? "application/json" : "text/markdown",
74
+ };
75
+ const content = typeof value === "object" ? JSON.stringify(value) : String(value);
76
+ await retryWithDelay(() => obsidianService.patchFile(filePath, content, patchOptions, context), {
77
+ operationName: `patchFileForFrontmatterSet`,
78
+ context,
79
+ maxRetries: 3,
80
+ delayMs: 300,
81
+ shouldRetry: shouldRetryNotFound,
82
+ });
83
+ if (vaultCacheService) {
84
+ await vaultCacheService.updateCacheForFile(filePath, context);
85
+ }
86
+ return {
87
+ success: true,
88
+ message: `Successfully set key '${key}' in frontmatter.`,
89
+ value: { [key]: value },
90
+ };
91
+ }
92
+ case "delete": {
93
+ // Note on deletion strategy: The Obsidian REST API's PATCH endpoint for frontmatter
94
+ // supports adding/updating keys but does not have a dedicated "delete key" operation.
95
+ // Therefore, deletion is handled by reading the note content, parsing the frontmatter,
96
+ // removing the key from the JavaScript object, and then overwriting the entire note
97
+ // with the updated frontmatter block. This regex-based replacement is a workaround
98
+ // for the current API limitations.
99
+ const noteJson = (await getFileWithRetry(context, "json"));
100
+ const frontmatter = noteJson.frontmatter;
101
+ if (!frontmatter || frontmatter[key] === undefined) {
102
+ return {
103
+ success: true,
104
+ message: `Key '${key}' not found in frontmatter. No action taken.`,
105
+ value: {},
106
+ };
107
+ }
108
+ delete frontmatter[key];
109
+ const noteContent = (await getFileWithRetry(context, "markdown"));
110
+ const frontmatterRegex = /^---\n([\s\S]*?)\n---\n/;
111
+ const match = noteContent.match(frontmatterRegex);
112
+ let newContent;
113
+ const newFrontmatterString = Object.keys(frontmatter).length > 0 ? dump(frontmatter) : "";
114
+ if (match) {
115
+ // Frontmatter exists, replace it
116
+ if (newFrontmatterString) {
117
+ newContent = noteContent.replace(frontmatterRegex, `---\n${newFrontmatterString}---\n`);
118
+ }
119
+ else {
120
+ // If frontmatter is now empty, remove the block entirely
121
+ newContent = noteContent.replace(frontmatterRegex, "");
122
+ }
123
+ }
124
+ else {
125
+ // This case should be rare given the initial check, but handle it defensively
126
+ logger.warning("Frontmatter key existed in JSON but block not found in markdown. No action taken.", context);
127
+ return {
128
+ success: false,
129
+ message: `Could not find frontmatter block to update, though key '${key}' was detected.`,
130
+ value: {},
131
+ };
132
+ }
133
+ await retryWithDelay(() => obsidianService.updateFileContent(filePath, newContent, context), {
134
+ operationName: `updateFileForFrontmatterDelete`,
135
+ context,
136
+ maxRetries: 3,
137
+ delayMs: 300,
138
+ shouldRetry: shouldRetryNotFound,
139
+ });
140
+ if (vaultCacheService) {
141
+ await vaultCacheService.updateCacheForFile(filePath, context);
142
+ }
143
+ return {
144
+ success: true,
145
+ message: `Successfully deleted key '${key}' from frontmatter.`,
146
+ value: {},
147
+ };
148
+ }
149
+ default:
150
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid operation: ${operation}`, context);
151
+ }
152
+ };
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ObsidianRestApiService, VaultCacheService } from "../../../services/obsidianRestAPI/index.js";
3
+ export declare const registerObsidianManageFrontmatterTool: (server: McpServer, obsidianService: ObsidianRestApiService, vaultCacheService: VaultCacheService | undefined) => Promise<void>;
@@ -0,0 +1,52 @@
1
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
2
+ import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
3
+ import { ManageFrontmatterInputSchema, ObsidianManageFrontmatterInputSchemaShape, processObsidianManageFrontmatter, } from "./logic.js";
4
+ export const registerObsidianManageFrontmatterTool = async (server, obsidianService, vaultCacheService) => {
5
+ const toolName = "obsidian_manage_frontmatter";
6
+ const toolDescription = "Atomically manages a note's YAML frontmatter. Supports getting, setting (creating/updating), and deleting specific keys without rewriting the entire file. Ideal for efficient metadata operations on primitive or structured Obsidian frontmatter data.";
7
+ const registrationContext = requestContextService.createRequestContext({
8
+ operation: "RegisterObsidianManageFrontmatterTool",
9
+ toolName: toolName,
10
+ module: "ObsidianManageFrontmatterRegistration",
11
+ });
12
+ logger.info(`Attempting to register tool: ${toolName}`, registrationContext);
13
+ await ErrorHandler.tryCatch(async () => {
14
+ server.tool(toolName, toolDescription, ObsidianManageFrontmatterInputSchemaShape, async (params) => {
15
+ const handlerContext = requestContextService.createRequestContext({
16
+ parentContext: registrationContext,
17
+ operation: "HandleObsidianManageFrontmatterRequest",
18
+ toolName: toolName,
19
+ params: params,
20
+ });
21
+ logger.debug(`Handling '${toolName}' request`, handlerContext);
22
+ return await ErrorHandler.tryCatch(async () => {
23
+ const validatedParams = ManageFrontmatterInputSchema.parse(params);
24
+ const response = await processObsidianManageFrontmatter(validatedParams, handlerContext, obsidianService, vaultCacheService);
25
+ logger.debug(`'${toolName}' processed successfully`, handlerContext);
26
+ return {
27
+ content: [
28
+ {
29
+ type: "text",
30
+ text: JSON.stringify(response, null, 2),
31
+ },
32
+ ],
33
+ isError: false,
34
+ };
35
+ }, {
36
+ operation: `processing ${toolName} handler`,
37
+ context: handlerContext,
38
+ input: params,
39
+ errorMapper: (error) => new McpError(error instanceof McpError
40
+ ? error.code
41
+ : BaseErrorCode.INTERNAL_ERROR, `Error processing ${toolName} tool: ${error instanceof Error ? error.message : "Unknown error"}`, { ...handlerContext }),
42
+ });
43
+ });
44
+ logger.info(`Tool registered successfully: ${toolName}`, registrationContext);
45
+ }, {
46
+ operation: `registering tool ${toolName}`,
47
+ context: registrationContext,
48
+ errorCode: BaseErrorCode.INTERNAL_ERROR,
49
+ errorMapper: (error) => new McpError(error instanceof McpError ? error.code : BaseErrorCode.INTERNAL_ERROR, `Failed to register tool '${toolName}': ${error instanceof Error ? error.message : "Unknown error"}`, { ...registrationContext }),
50
+ critical: true,
51
+ });
52
+ };
@@ -0,0 +1,3 @@
1
+ export { ObsidianManageTagsInputSchemaShape, processObsidianManageTags, } from "./logic.js";
2
+ export type { ObsidianManageTagsInput, ObsidianManageTagsResponse, } from "./logic.js";
3
+ export { registerObsidianManageTagsTool } from "./registration.js";
@@ -0,0 +1,2 @@
1
+ export { ObsidianManageTagsInputSchemaShape, processObsidianManageTags, } from "./logic.js";
2
+ export { registerObsidianManageTagsTool } from "./registration.js";
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ import { ObsidianRestApiService, VaultCacheService } from "../../../services/obsidianRestAPI/index.js";
3
+ import { RequestContext } from "../../../utils/index.js";
4
+ export declare const ObsidianManageTagsInputSchemaShape: {
5
+ filePath: z.ZodString;
6
+ operation: z.ZodEnum<["add", "remove", "list"]>;
7
+ tags: z.ZodArray<z.ZodString, "many">;
8
+ };
9
+ export declare const ManageTagsInputSchema: z.ZodObject<{
10
+ filePath: z.ZodString;
11
+ operation: z.ZodEnum<["add", "remove", "list"]>;
12
+ tags: z.ZodArray<z.ZodString, "many">;
13
+ }, "strip", z.ZodTypeAny, {
14
+ operation: "add" | "remove" | "list";
15
+ filePath: string;
16
+ tags: string[];
17
+ }, {
18
+ operation: "add" | "remove" | "list";
19
+ filePath: string;
20
+ tags: string[];
21
+ }>;
22
+ export type ObsidianManageTagsInput = z.infer<typeof ManageTagsInputSchema>;
23
+ export interface ObsidianManageTagsResponse {
24
+ success: boolean;
25
+ message: string;
26
+ currentTags: string[];
27
+ }
28
+ export declare const processObsidianManageTags: (params: ObsidianManageTagsInput, context: RequestContext, obsidianService: ObsidianRestApiService, vaultCacheService: VaultCacheService | undefined) => Promise<ObsidianManageTagsResponse>;