obsidian-mcp-server 2.0.6 → 3.0.0

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 (253) hide show
  1. package/CLAUDE.md +364 -0
  2. package/Dockerfile +99 -0
  3. package/LICENSE +4 -6
  4. package/README.md +246 -206
  5. package/changelog/3.0.x/3.0.0.md +102 -0
  6. package/changelog/template.md +51 -0
  7. package/dist/config/server-config.d.ts +19 -0
  8. package/dist/config/server-config.d.ts.map +1 -0
  9. package/dist/config/server-config.js +55 -0
  10. package/dist/config/server-config.js.map +1 -0
  11. package/dist/index.d.ts +7 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +23 -295
  14. package/dist/index.js.map +1 -0
  15. package/dist/mcp-server/prompts/definitions/index.d.ts +8 -0
  16. package/dist/mcp-server/prompts/definitions/index.d.ts.map +1 -0
  17. package/dist/mcp-server/prompts/definitions/index.js +8 -0
  18. package/dist/mcp-server/prompts/definitions/index.js.map +1 -0
  19. package/dist/mcp-server/resources/definitions/index.d.ts +36 -0
  20. package/dist/mcp-server/resources/definitions/index.d.ts.map +1 -0
  21. package/dist/mcp-server/resources/definitions/index.js +9 -0
  22. package/dist/mcp-server/resources/definitions/index.js.map +1 -0
  23. package/dist/mcp-server/resources/definitions/obsidian-status.resource.d.ts +23 -0
  24. package/dist/mcp-server/resources/definitions/obsidian-status.resource.d.ts.map +1 -0
  25. package/dist/mcp-server/resources/definitions/obsidian-status.resource.js +47 -0
  26. package/dist/mcp-server/resources/definitions/obsidian-status.resource.js.map +1 -0
  27. package/dist/mcp-server/resources/definitions/obsidian-tags.resource.d.ts +13 -0
  28. package/dist/mcp-server/resources/definitions/obsidian-tags.resource.d.ts.map +1 -0
  29. package/dist/mcp-server/resources/definitions/obsidian-tags.resource.js +30 -0
  30. package/dist/mcp-server/resources/definitions/obsidian-tags.resource.js.map +1 -0
  31. package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.d.ts +21 -0
  32. package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.d.ts.map +1 -0
  33. package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.js +38 -0
  34. package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.js.map +1 -0
  35. package/dist/mcp-server/tools/definitions/_shared/schemas.d.ts +45 -0
  36. package/dist/mcp-server/tools/definitions/_shared/schemas.d.ts.map +1 -0
  37. package/dist/mcp-server/tools/definitions/_shared/schemas.js +66 -0
  38. package/dist/mcp-server/tools/definitions/_shared/schemas.js.map +1 -0
  39. package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts +51 -0
  40. package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts.map +1 -0
  41. package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js +120 -0
  42. package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js.map +1 -0
  43. package/dist/mcp-server/tools/definitions/index.d.ts +531 -0
  44. package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -0
  45. package/dist/mcp-server/tools/definitions/index.js +38 -0
  46. package/dist/mcp-server/tools/definitions/index.js.map +1 -0
  47. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts +42 -0
  48. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts.map +1 -0
  49. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js +58 -0
  50. package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js.map +1 -0
  51. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts +46 -0
  52. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts.map +1 -0
  53. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js +66 -0
  54. package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js.map +1 -0
  55. package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.d.ts +19 -0
  56. package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.d.ts.map +1 -0
  57. package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.js +43 -0
  58. package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.js.map +1 -0
  59. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts +92 -0
  60. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts.map +1 -0
  61. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js +245 -0
  62. package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js.map +1 -0
  63. package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.d.ts +13 -0
  64. package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.d.ts.map +1 -0
  65. package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.js +38 -0
  66. package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.js.map +1 -0
  67. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts +59 -0
  68. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts.map +1 -0
  69. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js +273 -0
  70. package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js.map +1 -0
  71. package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.d.ts +13 -0
  72. package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.d.ts.map +1 -0
  73. package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js +38 -0
  74. package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js.map +1 -0
  75. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts +68 -0
  76. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts.map +1 -0
  77. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js +178 -0
  78. package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js.map +1 -0
  79. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts +77 -0
  80. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts.map +1 -0
  81. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js +172 -0
  82. package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js.map +1 -0
  83. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts +22 -0
  84. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts.map +1 -0
  85. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js +88 -0
  86. package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js.map +1 -0
  87. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts +81 -0
  88. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts.map +1 -0
  89. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js +83 -0
  90. package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js.map +1 -0
  91. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts +59 -0
  92. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts.map +1 -0
  93. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js +167 -0
  94. package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js.map +1 -0
  95. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts +76 -0
  96. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts.map +1 -0
  97. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js +244 -0
  98. package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js.map +1 -0
  99. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts +41 -0
  100. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts.map +1 -0
  101. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js +76 -0
  102. package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js.map +1 -0
  103. package/dist/services/obsidian/frontmatter-ops.d.ts +34 -0
  104. package/dist/services/obsidian/frontmatter-ops.d.ts.map +1 -0
  105. package/dist/services/obsidian/frontmatter-ops.js +230 -0
  106. package/dist/services/obsidian/frontmatter-ops.js.map +1 -0
  107. package/dist/services/obsidian/obsidian-service.d.ts +73 -0
  108. package/dist/services/obsidian/obsidian-service.d.ts.map +1 -0
  109. package/dist/services/obsidian/obsidian-service.js +399 -0
  110. package/dist/services/obsidian/obsidian-service.js.map +1 -0
  111. package/dist/services/obsidian/section-extractor.d.ts +13 -0
  112. package/dist/services/obsidian/section-extractor.d.ts.map +1 -0
  113. package/dist/services/obsidian/section-extractor.js +124 -0
  114. package/dist/services/obsidian/section-extractor.js.map +1 -0
  115. package/dist/services/obsidian/types.d.ts +91 -0
  116. package/dist/services/obsidian/types.d.ts.map +1 -0
  117. package/dist/services/obsidian/types.js +7 -0
  118. package/dist/services/obsidian/types.js.map +1 -0
  119. package/package.json +63 -69
  120. package/server.json +167 -0
  121. package/CHANGELOG.md +0 -118
  122. package/dist/config/index.d.ts +0 -41
  123. package/dist/config/index.js +0 -191
  124. package/dist/mcp-server/server.d.ts +0 -33
  125. package/dist/mcp-server/server.js +0 -211
  126. package/dist/mcp-server/tools/obsidianDeleteNoteTool/index.d.ts +0 -12
  127. package/dist/mcp-server/tools/obsidianDeleteNoteTool/index.js +0 -12
  128. package/dist/mcp-server/tools/obsidianDeleteNoteTool/logic.d.ts +0 -51
  129. package/dist/mcp-server/tools/obsidianDeleteNoteTool/logic.js +0 -168
  130. package/dist/mcp-server/tools/obsidianDeleteNoteTool/registration.d.ts +0 -19
  131. package/dist/mcp-server/tools/obsidianDeleteNoteTool/registration.js +0 -91
  132. package/dist/mcp-server/tools/obsidianGlobalSearchTool/index.d.ts +0 -12
  133. package/dist/mcp-server/tools/obsidianGlobalSearchTool/index.js +0 -12
  134. package/dist/mcp-server/tools/obsidianGlobalSearchTool/logic.d.ts +0 -77
  135. package/dist/mcp-server/tools/obsidianGlobalSearchTool/logic.js +0 -341
  136. package/dist/mcp-server/tools/obsidianGlobalSearchTool/registration.d.ts +0 -18
  137. package/dist/mcp-server/tools/obsidianGlobalSearchTool/registration.js +0 -69
  138. package/dist/mcp-server/tools/obsidianListNotesTool/index.d.ts +0 -12
  139. package/dist/mcp-server/tools/obsidianListNotesTool/index.js +0 -12
  140. package/dist/mcp-server/tools/obsidianListNotesTool/logic.d.ts +0 -68
  141. package/dist/mcp-server/tools/obsidianListNotesTool/logic.js +0 -215
  142. package/dist/mcp-server/tools/obsidianListNotesTool/registration.d.ts +0 -23
  143. package/dist/mcp-server/tools/obsidianListNotesTool/registration.js +0 -98
  144. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/index.d.ts +0 -3
  145. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/index.js +0 -2
  146. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/logic.d.ts +0 -42
  147. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/logic.js +0 -152
  148. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/registration.d.ts +0 -3
  149. package/dist/mcp-server/tools/obsidianManageFrontmatterTool/registration.js +0 -52
  150. package/dist/mcp-server/tools/obsidianManageTagsTool/index.d.ts +0 -3
  151. package/dist/mcp-server/tools/obsidianManageTagsTool/index.js +0 -2
  152. package/dist/mcp-server/tools/obsidianManageTagsTool/logic.d.ts +0 -28
  153. package/dist/mcp-server/tools/obsidianManageTagsTool/logic.js +0 -161
  154. package/dist/mcp-server/tools/obsidianManageTagsTool/registration.d.ts +0 -3
  155. package/dist/mcp-server/tools/obsidianManageTagsTool/registration.js +0 -52
  156. package/dist/mcp-server/tools/obsidianReadNoteTool/index.d.ts +0 -12
  157. package/dist/mcp-server/tools/obsidianReadNoteTool/index.js +0 -12
  158. package/dist/mcp-server/tools/obsidianReadNoteTool/logic.d.ts +0 -87
  159. package/dist/mcp-server/tools/obsidianReadNoteTool/logic.js +0 -216
  160. package/dist/mcp-server/tools/obsidianReadNoteTool/registration.d.ts +0 -20
  161. package/dist/mcp-server/tools/obsidianReadNoteTool/registration.js +0 -101
  162. package/dist/mcp-server/tools/obsidianSearchReplaceTool/index.d.ts +0 -12
  163. package/dist/mcp-server/tools/obsidianSearchReplaceTool/index.js +0 -12
  164. package/dist/mcp-server/tools/obsidianSearchReplaceTool/logic.d.ts +0 -255
  165. package/dist/mcp-server/tools/obsidianSearchReplaceTool/logic.js +0 -583
  166. package/dist/mcp-server/tools/obsidianSearchReplaceTool/registration.d.ts +0 -22
  167. package/dist/mcp-server/tools/obsidianSearchReplaceTool/registration.js +0 -111
  168. package/dist/mcp-server/tools/obsidianUpdateNoteTool/index.d.ts +0 -12
  169. package/dist/mcp-server/tools/obsidianUpdateNoteTool/index.js +0 -12
  170. package/dist/mcp-server/tools/obsidianUpdateNoteTool/logic.d.ts +0 -183
  171. package/dist/mcp-server/tools/obsidianUpdateNoteTool/logic.js +0 -490
  172. package/dist/mcp-server/tools/obsidianUpdateNoteTool/registration.d.ts +0 -21
  173. package/dist/mcp-server/tools/obsidianUpdateNoteTool/registration.js +0 -108
  174. package/dist/mcp-server/transports/auth/core/authContext.d.ts +0 -33
  175. package/dist/mcp-server/transports/auth/core/authContext.js +0 -24
  176. package/dist/mcp-server/transports/auth/core/authTypes.d.ts +0 -17
  177. package/dist/mcp-server/transports/auth/core/authTypes.js +0 -5
  178. package/dist/mcp-server/transports/auth/core/authUtils.d.ts +0 -18
  179. package/dist/mcp-server/transports/auth/core/authUtils.js +0 -45
  180. package/dist/mcp-server/transports/auth/index.d.ts +0 -10
  181. package/dist/mcp-server/transports/auth/index.js +0 -9
  182. package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.d.ts +0 -27
  183. package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +0 -149
  184. package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.d.ts +0 -20
  185. package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +0 -124
  186. package/dist/mcp-server/transports/httpErrorHandler.d.ts +0 -26
  187. package/dist/mcp-server/transports/httpErrorHandler.js +0 -73
  188. package/dist/mcp-server/transports/httpTransport.d.ts +0 -21
  189. package/dist/mcp-server/transports/httpTransport.js +0 -208
  190. package/dist/mcp-server/transports/stdioTransport.d.ts +0 -42
  191. package/dist/mcp-server/transports/stdioTransport.js +0 -63
  192. package/dist/services/obsidianRestAPI/index.d.ts +0 -15
  193. package/dist/services/obsidianRestAPI/index.js +0 -17
  194. package/dist/services/obsidianRestAPI/methods/activeFileMethods.d.ts +0 -38
  195. package/dist/services/obsidianRestAPI/methods/activeFileMethods.js +0 -62
  196. package/dist/services/obsidianRestAPI/methods/commandMethods.d.ts +0 -22
  197. package/dist/services/obsidianRestAPI/methods/commandMethods.js +0 -31
  198. package/dist/services/obsidianRestAPI/methods/openMethods.d.ts +0 -16
  199. package/dist/services/obsidianRestAPI/methods/openMethods.js +0 -21
  200. package/dist/services/obsidianRestAPI/methods/patchMethods.d.ts +0 -37
  201. package/dist/services/obsidianRestAPI/methods/patchMethods.js +0 -94
  202. package/dist/services/obsidianRestAPI/methods/periodicNoteMethods.d.ts +0 -42
  203. package/dist/services/obsidianRestAPI/methods/periodicNoteMethods.js +0 -66
  204. package/dist/services/obsidianRestAPI/methods/searchMethods.d.ts +0 -25
  205. package/dist/services/obsidianRestAPI/methods/searchMethods.js +0 -36
  206. package/dist/services/obsidianRestAPI/methods/vaultMethods.d.ts +0 -58
  207. package/dist/services/obsidianRestAPI/methods/vaultMethods.js +0 -144
  208. package/dist/services/obsidianRestAPI/service.d.ts +0 -195
  209. package/dist/services/obsidianRestAPI/service.js +0 -379
  210. package/dist/services/obsidianRestAPI/types.d.ts +0 -127
  211. package/dist/services/obsidianRestAPI/types.js +0 -7
  212. package/dist/services/obsidianRestAPI/vaultCache/index.d.ts +0 -4
  213. package/dist/services/obsidianRestAPI/vaultCache/index.js +0 -4
  214. package/dist/services/obsidianRestAPI/vaultCache/service.d.ts +0 -88
  215. package/dist/services/obsidianRestAPI/vaultCache/service.js +0 -299
  216. package/dist/types-global/errors.d.ts +0 -73
  217. package/dist/types-global/errors.js +0 -71
  218. package/dist/utils/index.d.ts +0 -5
  219. package/dist/utils/index.js +0 -13
  220. package/dist/utils/internal/asyncUtils.d.ts +0 -54
  221. package/dist/utils/internal/asyncUtils.js +0 -101
  222. package/dist/utils/internal/errorHandler.d.ts +0 -176
  223. package/dist/utils/internal/errorHandler.js +0 -351
  224. package/dist/utils/internal/index.d.ts +0 -4
  225. package/dist/utils/internal/index.js +0 -4
  226. package/dist/utils/internal/logger.d.ts +0 -141
  227. package/dist/utils/internal/logger.js +0 -406
  228. package/dist/utils/internal/requestContext.d.ts +0 -83
  229. package/dist/utils/internal/requestContext.js +0 -72
  230. package/dist/utils/metrics/index.d.ts +0 -1
  231. package/dist/utils/metrics/index.js +0 -1
  232. package/dist/utils/metrics/tokenCounter.d.ts +0 -27
  233. package/dist/utils/metrics/tokenCounter.js +0 -128
  234. package/dist/utils/obsidian/index.d.ts +0 -5
  235. package/dist/utils/obsidian/index.js +0 -5
  236. package/dist/utils/obsidian/obsidianApiUtils.d.ts +0 -14
  237. package/dist/utils/obsidian/obsidianApiUtils.js +0 -29
  238. package/dist/utils/obsidian/obsidianStatUtils.d.ts +0 -68
  239. package/dist/utils/obsidian/obsidianStatUtils.js +0 -143
  240. package/dist/utils/parsing/dateParser.d.ts +0 -56
  241. package/dist/utils/parsing/dateParser.js +0 -104
  242. package/dist/utils/parsing/index.d.ts +0 -2
  243. package/dist/utils/parsing/index.js +0 -3
  244. package/dist/utils/parsing/jsonParser.d.ts +0 -80
  245. package/dist/utils/parsing/jsonParser.js +0 -133
  246. package/dist/utils/security/idGenerator.d.ts +0 -140
  247. package/dist/utils/security/idGenerator.js +0 -194
  248. package/dist/utils/security/index.d.ts +0 -3
  249. package/dist/utils/security/index.js +0 -3
  250. package/dist/utils/security/rateLimiter.d.ts +0 -156
  251. package/dist/utils/security/rateLimiter.js +0 -235
  252. package/dist/utils/security/sanitization.d.ts +0 -244
  253. package/dist/utils/security/sanitization.js +0 -599
@@ -1,583 +0,0 @@
1
- import path from "node:path"; // For file path fallback logic using POSIX separators
2
- import { z } from "zod";
3
- import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
4
- import { createFormattedStatWithTokenCount, logger, retryWithDelay, } from "../../../utils/index.js";
5
- // ====================================================================================
6
- // Schema Definitions for Input Validation
7
- // ====================================================================================
8
- /** Defines the possible types of targets for the search/replace operation. */
9
- const TargetTypeSchema = z
10
- .enum(["filePath", "activeFile", "periodicNote"])
11
- .describe("Specifies the target note: 'filePath', 'activeFile', or 'periodicNote'.");
12
- /** Defines the valid periods for periodic notes. */
13
- const PeriodicNotePeriodSchema = z
14
- .enum(["daily", "weekly", "monthly", "quarterly", "yearly"])
15
- .describe("Valid periods for 'periodicNote' target type.");
16
- /**
17
- * Defines the structure for a single search and replace operation block.
18
- */
19
- const ReplacementBlockSchema = z.object({
20
- /** The exact string or regex pattern to search for within the note content. Cannot be empty. */
21
- search: z
22
- .string()
23
- .min(1, "Search pattern cannot be empty.")
24
- .describe("The exact string or regex pattern to search for."),
25
- /** The string to replace each match with. An empty string effectively deletes the matched text. */
26
- replace: z.string().describe("The string to replace matches with."),
27
- });
28
- /**
29
- * Base Zod schema object containing fields common to the search/replace tool input.
30
- * This is used as the foundation for both the registration shape and the refined internal schema.
31
- */
32
- const BaseObsidianSearchReplaceInputSchema = z.object({
33
- /** Specifies the target note: 'filePath', 'activeFile', or 'periodicNote'. */
34
- targetType: TargetTypeSchema,
35
- /**
36
- * Identifier for the target. Required and must be a vault-relative path if targetType is 'filePath'.
37
- * Required and must be a valid period string (e.g., 'daily') if targetType is 'periodicNote'.
38
- * Not used if targetType is 'activeFile'. The tool attempts a case-insensitive fallback if the exact filePath is not found.
39
- */
40
- targetIdentifier: z
41
- .string()
42
- .optional()
43
- .describe("Required if targetType is 'filePath' (vault-relative path) or 'periodicNote' (period string: 'daily', etc.). Tries case-insensitive fallback for filePath."),
44
- /** An array of one or more search/replace operations to perform sequentially on the note content. */
45
- replacements: z
46
- .array(ReplacementBlockSchema)
47
- .min(1, "Replacements array cannot be empty.")
48
- .describe("An array of search/replace operations to perform sequentially."),
49
- /** If true, treats the 'search' field in each replacement block as a JavaScript regular expression pattern. Defaults to false (exact string matching). */
50
- useRegex: z
51
- .boolean()
52
- .optional()
53
- .default(false)
54
- .describe("If true, treat the 'search' field in replacements as JavaScript regex patterns. Defaults to false (exact string matching)."),
55
- /** If true (default), replaces all occurrences matching each search pattern within the note. If false, replaces only the first occurrence of each pattern. */
56
- replaceAll: z
57
- .boolean()
58
- .optional()
59
- .default(true)
60
- .describe("If true (default), replace all occurrences for each search pattern. If false, replace only the first occurrence."),
61
- /** If true (default), the search operation is case-sensitive. If false, it's case-insensitive. Applies to both string and regex searches. */
62
- caseSensitive: z
63
- .boolean()
64
- .optional()
65
- .default(true)
66
- .describe("If true (default), the search is case-sensitive. If false, it's case-insensitive. Applies to both string and regex search."),
67
- /** If true (and useRegex is false), treats sequences of whitespace in the search string as matching one or more whitespace characters (\s+). Defaults to false. Cannot be true if useRegex is true. */
68
- flexibleWhitespace: z
69
- .boolean()
70
- .optional()
71
- .default(false)
72
- .describe("If true (and useRegex=false), treats sequences of whitespace in the search string as matching one or more whitespace characters (\\s+). Defaults to false."),
73
- /** If true, ensures the search term matches only whole words by implicitly adding word boundaries (\b) around the pattern (unless boundaries already exist in regex). Applies to both regex and non-regex modes. Defaults to false. */
74
- wholeWord: z
75
- .boolean()
76
- .optional()
77
- .default(false)
78
- .describe("If true, ensures the search term matches only whole words using word boundaries (\\b). Applies to both regex and non-regex modes. Defaults to false."),
79
- /** If true, includes the final content of the modified file in the response. Defaults to false. */
80
- returnContent: z
81
- .boolean()
82
- .optional()
83
- .default(false)
84
- .describe("If true, returns the final content of the file in the response. Defaults to false."),
85
- });
86
- // ====================================================================================
87
- // Refined Schema for Internal Logic and Strict Validation
88
- // ====================================================================================
89
- /**
90
- * Refined Zod schema used internally within the tool's logic for strict validation.
91
- * It builds upon `BaseObsidianSearchReplaceInputSchema` and adds cross-field validation rules:
92
- * 1. Ensures `targetIdentifier` is provided and valid when required by `targetType`.
93
- * 2. Ensures `flexibleWhitespace` is not used concurrently with `useRegex`.
94
- */
95
- export const ObsidianSearchReplaceInputSchema = BaseObsidianSearchReplaceInputSchema.refine((data) => {
96
- // Rule 1: Validate targetIdentifier based on targetType
97
- if ((data.targetType === "filePath" ||
98
- data.targetType === "periodicNote") &&
99
- !data.targetIdentifier) {
100
- return false; // Missing targetIdentifier
101
- }
102
- if (data.targetType === "periodicNote" &&
103
- data.targetIdentifier &&
104
- !PeriodicNotePeriodSchema.safeParse(data.targetIdentifier).success) {
105
- return false; // Invalid period
106
- }
107
- // Rule 2: flexibleWhitespace cannot be true if useRegex is true
108
- if (data.flexibleWhitespace && data.useRegex) {
109
- return false; // Conflicting options
110
- }
111
- return true; // All checks passed
112
- }, {
113
- // Custom error message for refinement failures
114
- message: "Validation failed: targetIdentifier is required and must be valid for 'filePath' or 'periodicNote'. Also, 'flexibleWhitespace' cannot be true if 'useRegex' is true.",
115
- // Point error reporting to potentially problematic fields
116
- path: ["targetIdentifier", "flexibleWhitespace", "useRegex"],
117
- }).describe("Performs one or more search-and-replace operations within a target Obsidian note (file path, active, or periodic). Reads the file, applies replacements sequentially in memory, and writes the modified content back, overwriting the original. Supports string/regex search, case sensitivity toggle, replacing first/all occurrences, flexible whitespace matching (non-regex), and whole word matching.");
118
- // ====================================================================================
119
- // Schema Shape and Type Exports for Registration and Logic
120
- // ====================================================================================
121
- /**
122
- * The shape of the base input schema, used by `server.tool` for registration and initial validation.
123
- * @see BaseObsidianSearchReplaceInputSchema
124
- */
125
- export const ObsidianSearchReplaceInputSchemaShape = BaseObsidianSearchReplaceInputSchema.shape;
126
- // ====================================================================================
127
- // Helper Functions
128
- // ====================================================================================
129
- /**
130
- * Escapes characters that have special meaning in regular expressions.
131
- * This allows treating a literal string as a pattern in a regex.
132
- *
133
- * @param {string} str - The input string to escape.
134
- * @returns {string} The string with regex special characters escaped.
135
- */
136
- function escapeRegex(str) {
137
- // Escape characters: . * + ? ^ $ { } ( ) | [ ] \ -
138
- return str.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&"); // $& inserts the matched character
139
- }
140
- /**
141
- * Attempts to retrieve the final state (content and stats) of the target note after a search/replace operation.
142
- * Uses the appropriate Obsidian API method based on the target type and the potentially corrected file path.
143
- * Logs a warning and returns null if fetching the final state fails, preventing failure of the entire operation.
144
- *
145
- * @param {z.infer<typeof TargetTypeSchema>} targetType - The type of the target note.
146
- * @param {string | undefined} effectiveFilePath - The vault-relative path (potentially corrected by case-insensitive fallback). Undefined for non-filePath targets.
147
- * @param {z.infer<typeof PeriodicNotePeriodSchema> | undefined} period - The parsed period if targetType is 'periodicNote'.
148
- * @param {ObsidianRestApiService} obsidianService - The Obsidian API service instance.
149
- * @param {RequestContext} context - The request context for logging and correlation.
150
- * @returns {Promise<NoteJson | null>} A promise resolving to the NoteJson object or null if retrieval fails.
151
- */
152
- async function getFinalState(targetType, effectiveFilePath, period, obsidianService, context) {
153
- const operation = "getFinalStateAfterSearchReplace";
154
- const targetDesc = effectiveFilePath ?? (period ? `periodic ${period}` : "active file");
155
- logger.debug(`Attempting to retrieve final state for target: ${targetDesc}`, {
156
- ...context,
157
- operation,
158
- });
159
- try {
160
- let noteJson = null;
161
- // Call the appropriate API method
162
- if (targetType === "filePath" && effectiveFilePath) {
163
- noteJson = (await obsidianService.getFileContent(effectiveFilePath, "json", context));
164
- }
165
- else if (targetType === "activeFile") {
166
- noteJson = (await obsidianService.getActiveFile("json", context));
167
- }
168
- else if (targetType === "periodicNote" && period) {
169
- noteJson = (await obsidianService.getPeriodicNote(period, "json", context));
170
- }
171
- logger.debug(`Successfully retrieved final state for ${targetDesc}`, {
172
- ...context,
173
- operation,
174
- });
175
- return noteJson;
176
- }
177
- catch (error) {
178
- // Log the error but return null to avoid failing the main operation.
179
- const errorMsg = error instanceof Error ? error.message : String(error);
180
- logger.warning(`Could not retrieve final state after search/replace for target: ${targetDesc}. Error: ${errorMsg}`, { ...context, operation, error: errorMsg });
181
- return null;
182
- }
183
- }
184
- // ====================================================================================
185
- // Core Logic Function
186
- // ====================================================================================
187
- /**
188
- * Processes the core logic for the 'obsidian_search_replace' tool.
189
- * Reads the target note content, performs a sequence of search and replace operations
190
- * based on the provided parameters (handling regex, case sensitivity, etc.),
191
- * writes the modified content back to Obsidian, retrieves the final state,
192
- * and constructs the response object.
193
- *
194
- * @param {ObsidianSearchReplaceInput} params - The validated input parameters conforming to the refined schema.
195
- * @param {RequestContext} context - The request context for logging and correlation.
196
- * @param {ObsidianRestApiService} obsidianService - The instance of the Obsidian REST API service.
197
- * @returns {Promise<ObsidianSearchReplaceResponse>} A promise resolving to the structured success response.
198
- * @throws {McpError} Throws an McpError if validation fails, reading/writing fails, or an unexpected error occurs during processing.
199
- */
200
- export const processObsidianSearchReplace = async (params, // Use the refined, validated type
201
- context, obsidianService, vaultCacheService) => {
202
- // Destructure validated parameters for easier access
203
- const { targetType, targetIdentifier, replacements, useRegex: initialUseRegex, // Rename to avoid shadowing loop variable
204
- replaceAll, caseSensitive, flexibleWhitespace, // Note: Cannot be true if initialUseRegex is true (enforced by schema)
205
- wholeWord, returnContent, } = params;
206
- let effectiveFilePath = targetIdentifier; // Store the path used (might be updated by fallback)
207
- let targetDescription = targetIdentifier ?? "active file"; // For logging and error messages
208
- let targetPeriod;
209
- logger.debug(`Processing obsidian_search_replace request`, {
210
- ...context,
211
- targetType,
212
- targetIdentifier,
213
- initialUseRegex,
214
- flexibleWhitespace,
215
- wholeWord,
216
- returnContent,
217
- });
218
- // --- Step 1: Read Initial Content (with case-insensitive fallback for filePath) ---
219
- let originalContent;
220
- const readContext = { ...context, operation: "readFileContent" };
221
- try {
222
- if (targetType === "filePath") {
223
- if (!targetIdentifier) {
224
- // Should be caught by schema, but double-check
225
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, "targetIdentifier is required for targetType 'filePath'.", readContext);
226
- }
227
- targetDescription = targetIdentifier; // Initial description
228
- try {
229
- // Attempt 1: Case-sensitive read
230
- logger.debug(`Attempting to read file (case-sensitive): ${targetIdentifier}`, readContext);
231
- originalContent = (await obsidianService.getFileContent(targetIdentifier, "markdown", readContext));
232
- effectiveFilePath = targetIdentifier; // Confirm exact path worked
233
- logger.debug(`Successfully read file using exact path: ${targetIdentifier}`, readContext);
234
- }
235
- catch (readError) {
236
- // Attempt 2: Case-insensitive fallback if NOT_FOUND
237
- if (readError instanceof McpError &&
238
- readError.code === BaseErrorCode.NOT_FOUND) {
239
- logger.info(`File not found with exact path: ${targetIdentifier}. Attempting case-insensitive fallback.`, readContext);
240
- const dirname = path.posix.dirname(targetIdentifier);
241
- const filenameLower = path.posix
242
- .basename(targetIdentifier)
243
- .toLowerCase();
244
- // List directory contents (use root '/' if dirname is '.')
245
- const dirToList = dirname === "." ? "/" : dirname;
246
- const filesInDir = await obsidianService.listFiles(dirToList, readContext);
247
- // Filter for files matching the lowercase basename
248
- const matches = filesInDir.filter((f) => !f.endsWith("/") &&
249
- path.posix.basename(f).toLowerCase() === filenameLower);
250
- if (matches.length === 1) {
251
- // Found exactly one match
252
- const correctFilename = path.posix.basename(matches[0]);
253
- effectiveFilePath = path.posix.join(dirname, correctFilename); // Construct the correct path
254
- targetDescription = effectiveFilePath; // Update description for subsequent logs/errors
255
- logger.info(`Found case-insensitive match: ${effectiveFilePath}. Reading content.`, readContext);
256
- originalContent = (await obsidianService.getFileContent(effectiveFilePath, "markdown", readContext));
257
- logger.debug(`Successfully read file using fallback path: ${effectiveFilePath}`, readContext);
258
- }
259
- else {
260
- // Handle ambiguous (multiple matches) or no match found
261
- const errorMsg = matches.length > 1
262
- ? `Read failed: Ambiguous case-insensitive matches found for '${targetIdentifier}' in directory '${dirToList}'. Matches: [${matches.join(", ")}]`
263
- : `Read failed: File not found for '${targetIdentifier}' (case-insensitive fallback also failed in directory '${dirToList}').`;
264
- logger.error(errorMsg, { ...readContext, matches });
265
- // Use NOT_FOUND for no match, CONFLICT for ambiguity
266
- throw new McpError(matches.length > 1
267
- ? BaseErrorCode.CONFLICT
268
- : BaseErrorCode.NOT_FOUND, errorMsg, readContext);
269
- }
270
- }
271
- else {
272
- // Re-throw errors other than NOT_FOUND during the initial read attempt
273
- throw readError;
274
- }
275
- }
276
- }
277
- else if (targetType === "activeFile") {
278
- logger.debug(`Reading content from active file.`, readContext);
279
- originalContent = (await obsidianService.getActiveFile("markdown", readContext));
280
- targetDescription = "the active file";
281
- effectiveFilePath = undefined; // Not applicable
282
- logger.debug(`Successfully read active file content.`, readContext);
283
- }
284
- else {
285
- // periodicNote
286
- if (!targetIdentifier) {
287
- // Should be caught by schema
288
- throw new McpError(BaseErrorCode.VALIDATION_ERROR, "targetIdentifier is required for targetType 'periodicNote'.", readContext);
289
- }
290
- // Parse period (already validated by refined schema)
291
- targetPeriod = PeriodicNotePeriodSchema.parse(targetIdentifier);
292
- targetDescription = `periodic note '${targetPeriod}'`;
293
- effectiveFilePath = undefined; // Not applicable
294
- logger.debug(`Reading content from ${targetDescription}.`, readContext);
295
- originalContent = (await obsidianService.getPeriodicNote(targetPeriod, "markdown", readContext));
296
- logger.debug(`Successfully read ${targetDescription} content.`, readContext);
297
- }
298
- }
299
- catch (error) {
300
- // Catch and handle errors during the initial read phase
301
- if (error instanceof McpError)
302
- throw error; // Re-throw known McpErrors
303
- const errorMessage = `Unexpected error reading target ${targetDescription} before search/replace.`;
304
- logger.error(errorMessage, error instanceof Error ? error : undefined, readContext);
305
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `${errorMessage}: ${error instanceof Error ? error.message : String(error)}`, readContext);
306
- }
307
- // --- Step 2: Perform Sequential Replacements ---
308
- let modifiedContent = originalContent;
309
- let totalReplacementsMade = 0;
310
- const replaceContext = { ...context, operation: "performReplacements" };
311
- logger.debug(`Starting ${replacements.length} replacement operations.`, replaceContext);
312
- for (let i = 0; i < replacements.length; i++) {
313
- const rep = replacements[i];
314
- const repContext = {
315
- ...replaceContext,
316
- replacementIndex: i,
317
- searchPattern: rep.search,
318
- };
319
- let currentReplacementsInBlock = 0;
320
- let finalSearchPattern = rep.search; // Start with the raw search string
321
- let useRegexForThisRep = initialUseRegex; // Use the overall setting initially
322
- try {
323
- // --- 2a: Prepare the Search Pattern (Apply options) ---
324
- const patternPrepContext = {
325
- ...repContext,
326
- subOperation: "prepareSearchPattern",
327
- };
328
- if (!initialUseRegex) {
329
- // Handle non-regex specific options: flexibleWhitespace and wholeWord
330
- let searchStr = rep.search; // Work with a mutable string
331
- if (flexibleWhitespace) {
332
- // Convert to a regex string: escape special chars, then replace whitespace sequences with \s+
333
- searchStr = escapeRegex(searchStr).replace(/\s+/g, "\\s+");
334
- useRegexForThisRep = true; // Now treat it as a regex pattern string
335
- logger.debug(`Applying flexibleWhitespace: "${rep.search}" -> /${searchStr}/`, patternPrepContext);
336
- }
337
- if (wholeWord) {
338
- // Add word boundaries (\b) to the pattern string
339
- // If flexibleWhitespace was applied, searchStr is already a regex string.
340
- // Otherwise, escape the original search string first.
341
- const basePattern = useRegexForThisRep
342
- ? searchStr
343
- : escapeRegex(searchStr);
344
- searchStr = `\\b${basePattern}\\b`;
345
- useRegexForThisRep = true; // Definitely treat as a regex pattern string now
346
- logger.debug(`Applying wholeWord: "${rep.search}" -> /${searchStr}/`, patternPrepContext);
347
- }
348
- finalSearchPattern = searchStr; // Update the pattern to use
349
- }
350
- else if (wholeWord) {
351
- // Initial useRegex is true, but wholeWord is also requested.
352
- // Apply wholeWord boundaries if the user's regex doesn't obviously have them.
353
- let searchStr = rep.search;
354
- // Heuristic check: Does the pattern likely already account for boundaries?
355
- // Looks for ^, $, \b at start/end, or patterns matching full lines/non-whitespace sequences.
356
- const hasBoundary = /(?:^|\\b)\S.*\S(?:$|\\b)|^\S$|^\S.*\S$|^$/.test(searchStr) ||
357
- /^\^|\\b/.test(searchStr) ||
358
- /\$|\\b$/.test(searchStr);
359
- if (!hasBoundary) {
360
- searchStr = `\\b${searchStr}\\b`;
361
- // Log a warning as this might interfere with complex user regex.
362
- logger.warning(`Applying wholeWord=true to user-provided regex. Original: /${rep.search}/, Modified: /${searchStr}/. This might affect complex regex behavior.`, patternPrepContext);
363
- finalSearchPattern = searchStr; // Update the pattern string
364
- }
365
- else {
366
- logger.debug(`wholeWord=true requested, but user regex /${searchStr}/ appears to already contain boundary anchors. Using original regex.`, patternPrepContext);
367
- finalSearchPattern = rep.search; // Keep original regex string
368
- }
369
- }
370
- // If it's still not treated as regex, finalSearchPattern remains the original rep.search string.
371
- // --- 2b: Execute the Replacement ---
372
- const execContext = {
373
- ...repContext,
374
- subOperation: "executeReplacement",
375
- isRegex: useRegexForThisRep,
376
- };
377
- if (useRegexForThisRep) {
378
- // --- Regex Replacement ---
379
- let flags = "";
380
- if (replaceAll)
381
- flags += "g"; // Global flag for all matches
382
- if (!caseSensitive)
383
- flags += "i"; // Ignore case flag
384
- const regex = new RegExp(finalSearchPattern, flags); // Create RegExp object
385
- logger.debug(`Executing regex replacement: /${finalSearchPattern}/${flags}`, execContext);
386
- // Count matches *before* replacing to report accurately
387
- const matches = modifiedContent.match(regex);
388
- currentReplacementsInBlock = matches ? matches.length : 0;
389
- // If replaceAll is false, we only perform/count one replacement, even if regex matches more
390
- if (!replaceAll && currentReplacementsInBlock > 0) {
391
- currentReplacementsInBlock = 1;
392
- }
393
- // Perform the replacement
394
- if (currentReplacementsInBlock > 0) {
395
- if (replaceAll) {
396
- modifiedContent = modifiedContent.replace(regex, rep.replace);
397
- }
398
- else {
399
- // Replace only the first occurrence found by the regex
400
- modifiedContent = modifiedContent.replace(regex, rep.replace);
401
- }
402
- }
403
- }
404
- else {
405
- // --- Simple String Replacement ---
406
- // Note: wholeWord and flexibleWhitespace would have set useRegexForThisRep = true
407
- const searchString = finalSearchPattern; // It's just a string here
408
- const comparisonString = caseSensitive
409
- ? searchString
410
- : searchString.toLowerCase();
411
- let startIndex = 0;
412
- logger.debug(`Executing string replacement: "${searchString}" (caseSensitive: ${caseSensitive})`, execContext);
413
- while (true) {
414
- const contentToSearch = caseSensitive
415
- ? modifiedContent
416
- : modifiedContent.toLowerCase();
417
- const index = contentToSearch.indexOf(comparisonString, startIndex);
418
- if (index === -1) {
419
- break; // No more occurrences found
420
- }
421
- currentReplacementsInBlock++;
422
- // Perform replacement using original indices and search string length
423
- modifiedContent =
424
- modifiedContent.substring(0, index) +
425
- rep.replace +
426
- modifiedContent.substring(index + searchString.length);
427
- if (!replaceAll) {
428
- break; // Stop after the first replacement
429
- }
430
- // Move start index past the inserted replacement to find the next match
431
- startIndex = index + rep.replace.length;
432
- // Safety break for empty search string or potential infinite loops
433
- if (searchString.length === 0) {
434
- logger.warning(`Search string is empty. Breaking replacement loop to prevent infinite execution.`, execContext);
435
- break;
436
- }
437
- // Basic check if replacement could cause infinite loop (e.g., replacing 'a' with 'ba')
438
- if (rep.replace.includes(searchString) &&
439
- rep.replace.length >= searchString.length) {
440
- // This is a heuristic, might not catch all cases but prevents common ones.
441
- logger.warning(`Replacement string "${rep.replace}" contains search string "${searchString}". Potential infinite loop detected. Breaking loop for this block.`, execContext);
442
- break;
443
- }
444
- }
445
- }
446
- totalReplacementsMade += currentReplacementsInBlock;
447
- logger.debug(`Block ${i}: Performed ${currentReplacementsInBlock} replacements for search: "${rep.search}"`, repContext);
448
- }
449
- catch (error) {
450
- // Catch errors during a specific replacement block
451
- const errorMessage = `Error during replacement block ${i} (search: "${rep.search}")`;
452
- logger.error(errorMessage, error instanceof Error ? error : undefined, repContext);
453
- // Fail fast: Stop processing further replacements if one block fails.
454
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `${errorMessage}: ${error instanceof Error ? error.message : "Unknown error"}`, repContext);
455
- }
456
- } // End of replacements loop
457
- logger.debug(`Finished all replacement operations. Total replacements made: ${totalReplacementsMade}`, replaceContext);
458
- // --- Step 3: Write Modified Content Back to Obsidian ---
459
- let finalState = null;
460
- const POST_UPDATE_DELAY_MS = 500; // Delay before trying to read the file back
461
- // Only write back if the content actually changed to avoid unnecessary file operations.
462
- if (modifiedContent !== originalContent) {
463
- const writeContext = { ...context, operation: "writeFileContent" };
464
- try {
465
- logger.debug(`Content changed. Writing modified content back to ${targetDescription}`, writeContext);
466
- // Use the effectiveFilePath determined during the read phase for filePath targets
467
- if (targetType === "filePath") {
468
- await obsidianService.updateFileContent(effectiveFilePath, modifiedContent, writeContext);
469
- if (vaultCacheService) {
470
- await vaultCacheService.updateCacheForFile(effectiveFilePath, writeContext);
471
- }
472
- }
473
- else if (targetType === "activeFile") {
474
- await obsidianService.updateActiveFile(modifiedContent, writeContext);
475
- }
476
- else {
477
- // periodicNote
478
- await obsidianService.updatePeriodicNote(targetPeriod, modifiedContent, writeContext);
479
- }
480
- logger.info(`Successfully updated ${targetDescription} with ${totalReplacementsMade} replacement(s).`, writeContext);
481
- // Attempt to get the final state *after* successfully writing.
482
- logger.debug(`Waiting ${POST_UPDATE_DELAY_MS}ms before retrieving final state after write...`, { ...writeContext, subOperation: "postWriteDelay" });
483
- await new Promise((resolve) => setTimeout(resolve, POST_UPDATE_DELAY_MS));
484
- try {
485
- finalState = await retryWithDelay(async () => getFinalState(targetType, effectiveFilePath, targetPeriod, obsidianService, context), {
486
- operationName: "getFinalStateAfterSearchReplaceWrite",
487
- context: {
488
- ...context,
489
- operation: "getFinalStateAfterSearchReplaceWriteAttempt",
490
- },
491
- maxRetries: 3,
492
- delayMs: 300,
493
- shouldRetry: (err) => err instanceof McpError &&
494
- (err.code === BaseErrorCode.NOT_FOUND ||
495
- err.code === BaseErrorCode.SERVICE_UNAVAILABLE ||
496
- err.code === BaseErrorCode.TIMEOUT),
497
- onRetry: (attempt, err) => logger.warning(`getFinalStateAfterSearchReplaceWrite (attempt ${attempt}) failed. Error: ${err.message}. Retrying...`, writeContext),
498
- });
499
- }
500
- catch (retryError) {
501
- finalState = null;
502
- logger.error(`Failed to retrieve final state for ${targetDescription} after write, even after retries. Error: ${retryError.message}`, retryError instanceof Error ? retryError : undefined, writeContext);
503
- }
504
- }
505
- catch (error) {
506
- // Handle errors during the write phase
507
- if (error instanceof McpError)
508
- throw error; // Re-throw known McpErrors
509
- const errorMessage = `Unexpected error writing modified content to ${targetDescription}.`;
510
- logger.error(errorMessage, error instanceof Error ? error : undefined, writeContext);
511
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `${errorMessage}: ${error instanceof Error ? error.message : String(error)}`, writeContext);
512
- }
513
- }
514
- else {
515
- // Content did not change, no need to write.
516
- logger.info(`No changes detected in ${targetDescription} after search/replace operations. Skipping write.`, context);
517
- // Still attempt to get the state, as the user might want stats even if content is unchanged.
518
- logger.debug(`Waiting ${POST_UPDATE_DELAY_MS}ms before retrieving final state (no change)...`, { ...context, subOperation: "postNoChangeDelay" });
519
- await new Promise((resolve) => setTimeout(resolve, POST_UPDATE_DELAY_MS));
520
- try {
521
- finalState = await retryWithDelay(async () => getFinalState(targetType, effectiveFilePath, targetPeriod, obsidianService, context), {
522
- operationName: "getFinalStateAfterSearchReplaceNoChange",
523
- context: {
524
- ...context,
525
- operation: "getFinalStateAfterSearchReplaceNoChangeAttempt",
526
- },
527
- maxRetries: 3,
528
- delayMs: 300,
529
- shouldRetry: (err) => err instanceof McpError &&
530
- (err.code === BaseErrorCode.NOT_FOUND ||
531
- err.code === BaseErrorCode.SERVICE_UNAVAILABLE ||
532
- err.code === BaseErrorCode.TIMEOUT),
533
- onRetry: (attempt, err) => logger.warning(`getFinalStateAfterSearchReplaceNoChange (attempt ${attempt}) failed. Error: ${err.message}. Retrying...`, context),
534
- });
535
- }
536
- catch (retryError) {
537
- finalState = null;
538
- logger.error(`Failed to retrieve final state for ${targetDescription} (no change), even after retries. Error: ${retryError.message}`, retryError instanceof Error ? retryError : undefined, context);
539
- }
540
- }
541
- // --- Step 4: Construct and Return the Response ---
542
- const responseContext = { ...context, operation: "buildResponse" };
543
- let message;
544
- if (totalReplacementsMade > 0) {
545
- message = `Search/replace completed on ${targetDescription}. Successfully made ${totalReplacementsMade} replacement(s).`;
546
- }
547
- else if (modifiedContent !== originalContent) {
548
- // This case should ideally not happen if totalReplacementsMade is 0, but as a safeguard:
549
- message = `Search/replace completed on ${targetDescription}. Content was modified, but replacement count is zero. Please review.`;
550
- }
551
- else {
552
- message = `Search/replace completed on ${targetDescription}. No matching text was found, so no replacements were made.`;
553
- }
554
- // Append a warning if the final state couldn't be retrieved
555
- if (finalState === null) {
556
- const warningMsg = " (Warning: Could not retrieve final file stats/content after update.)";
557
- message += warningMsg;
558
- logger.warning(`Appending warning to response message: ${warningMsg}`, responseContext);
559
- }
560
- // Format the file statistics using the shared utility.
561
- // Use final state content if available, otherwise use the (potentially modified) content in memory for token count.
562
- const finalContentForStat = finalState?.content ?? modifiedContent;
563
- const formattedStatResult = finalState?.stat
564
- ? await createFormattedStatWithTokenCount(finalState.stat, finalContentForStat, responseContext) // Await the async utility
565
- : undefined;
566
- // Ensure stat is undefined if the utility returned null (e.g., token counting failed)
567
- const formattedStat = formattedStatResult === null ? undefined : formattedStatResult;
568
- // Build the final response object
569
- const response = {
570
- success: true,
571
- message: message,
572
- totalReplacementsMade,
573
- stats: formattedStat,
574
- };
575
- // Include final content if requested and available.
576
- if (returnContent) {
577
- // Prefer content from final state read, fallback to in-memory modified content.
578
- response.finalContent = finalState?.content ?? modifiedContent;
579
- logger.debug(`Including final content in response as requested.`, responseContext);
580
- }
581
- logger.debug(`Search/replace process completed successfully.`, responseContext);
582
- return response;
583
- };
@@ -1,22 +0,0 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { ObsidianRestApiService, VaultCacheService } from "../../../services/obsidianRestAPI/index.js";
3
- /**
4
- * Registers the 'obsidian_search_replace' tool with the MCP server.
5
- *
6
- * This tool performs one or more search-and-replace operations within a specified
7
- * Obsidian note (identified by file path, the active file, or a periodic note).
8
- * It reads the note content, applies the replacements sequentially based on the
9
- * provided options (regex, case sensitivity, etc.), writes the modified content
10
- * back to the vault, and returns the operation results.
11
- *
12
- * The response includes success status, a summary message, the total number of
13
- * replacements made, formatted file statistics (timestamp, token count), and
14
- * optionally the final content of the note.
15
- *
16
- * @param {McpServer} server - The MCP server instance to register the tool with.
17
- * @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service
18
- * used to interact with the user's Obsidian vault.
19
- * @returns {Promise<void>} A promise that resolves when the tool registration is complete or rejects on error.
20
- * @throws {McpError} Throws an McpError if registration fails critically.
21
- */
22
- export declare const registerObsidianSearchReplaceTool: (server: McpServer, obsidianService: ObsidianRestApiService, vaultCacheService: VaultCacheService | undefined) => Promise<void>;