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,77 @@
1
+ import { z } from "zod";
2
+ import { ObsidianRestApiService } from "../../../services/obsidianRestAPI/index.js";
3
+ import { VaultCacheService } from "../../../services/obsidianRestAPI/vaultCache/index.js";
4
+ import { RequestContext } from "../../../utils/index.js";
5
+ declare const ObsidianGlobalSearchInputSchema: z.ZodObject<{
6
+ query: z.ZodString;
7
+ searchInPath: z.ZodOptional<z.ZodString>;
8
+ contextLength: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
9
+ modified_since: z.ZodOptional<z.ZodString>;
10
+ modified_until: z.ZodOptional<z.ZodString>;
11
+ useRegex: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
12
+ caseSensitive: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
13
+ pageSize: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
14
+ page: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
15
+ maxMatchesPerFile: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
16
+ }, "strip", z.ZodTypeAny, {
17
+ caseSensitive: boolean;
18
+ query: string;
19
+ contextLength: number;
20
+ useRegex: boolean;
21
+ pageSize: number;
22
+ page: number;
23
+ maxMatchesPerFile: number;
24
+ searchInPath?: string | undefined;
25
+ modified_since?: string | undefined;
26
+ modified_until?: string | undefined;
27
+ }, {
28
+ query: string;
29
+ caseSensitive?: boolean | undefined;
30
+ searchInPath?: string | undefined;
31
+ contextLength?: number | undefined;
32
+ modified_since?: string | undefined;
33
+ modified_until?: string | undefined;
34
+ useRegex?: boolean | undefined;
35
+ pageSize?: number | undefined;
36
+ page?: number | undefined;
37
+ maxMatchesPerFile?: number | undefined;
38
+ }>;
39
+ export declare const ObsidianGlobalSearchInputSchemaShape: {
40
+ query: z.ZodString;
41
+ searchInPath: z.ZodOptional<z.ZodString>;
42
+ contextLength: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
43
+ modified_since: z.ZodOptional<z.ZodString>;
44
+ modified_until: z.ZodOptional<z.ZodString>;
45
+ useRegex: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
46
+ caseSensitive: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
47
+ pageSize: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
48
+ page: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
49
+ maxMatchesPerFile: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
50
+ };
51
+ export type ObsidianGlobalSearchInput = z.infer<typeof ObsidianGlobalSearchInputSchema>;
52
+ export interface MatchContext {
53
+ context: string;
54
+ matchText?: string;
55
+ position?: number;
56
+ }
57
+ export interface GlobalSearchResult {
58
+ path: string;
59
+ filename: string;
60
+ matches: MatchContext[];
61
+ modifiedTime: string;
62
+ createdTime: string;
63
+ numericMtime: number;
64
+ }
65
+ export interface ObsidianGlobalSearchResponse {
66
+ success: boolean;
67
+ message: string;
68
+ results: GlobalSearchResult[];
69
+ totalFilesFound: number;
70
+ totalMatchesFound: number;
71
+ currentPage: number;
72
+ pageSize: number;
73
+ totalPages: number;
74
+ alsoFoundInFiles?: string[];
75
+ }
76
+ export declare const processObsidianGlobalSearch: (params: ObsidianGlobalSearchInput, context: RequestContext, obsidianService: ObsidianRestApiService, vaultCacheService: VaultCacheService | undefined) => Promise<ObsidianGlobalSearchResponse>;
77
+ export {};
@@ -0,0 +1,341 @@
1
+ import path from "node:path/posix";
2
+ import { z } from "zod";
3
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
4
+ // Import formatTimestamp utility
5
+ import { config } from "../../../config/index.js";
6
+ import { dateParser, formatTimestamp, logger, retryWithDelay, sanitizeInputForLogging, } from "../../../utils/index.js";
7
+ // ====================================================================================
8
+ // Schema Definitions (Updated for Pagination, Match Limit, and Path Filter)
9
+ // ====================================================================================
10
+ const ObsidianGlobalSearchInputSchema = z
11
+ .object({
12
+ query: z
13
+ .string()
14
+ .min(1)
15
+ .describe("The search query (text or regex pattern)."),
16
+ searchInPath: z
17
+ .string()
18
+ .optional()
19
+ .describe("Optional vault-relative path to recursively search within (e.g., 'Notes/Projects'). If omitted, searches the entire vault."),
20
+ contextLength: z
21
+ .number()
22
+ .int()
23
+ .positive()
24
+ .optional()
25
+ .default(100)
26
+ .describe("Characters of context around matches."),
27
+ modified_since: z
28
+ .string()
29
+ .optional()
30
+ .describe("Filter files modified *since* this date/time (e.g., '2 weeks ago', '2024-01-15')."),
31
+ modified_until: z
32
+ .string()
33
+ .optional()
34
+ .describe("Filter files modified *until* this date/time (e.g., 'today', '2024-03-20 17:00')."),
35
+ useRegex: z
36
+ .boolean()
37
+ .optional()
38
+ .default(false)
39
+ .describe("Treat 'query' as regex. Defaults to false."),
40
+ caseSensitive: z
41
+ .boolean()
42
+ .optional()
43
+ .default(false)
44
+ .describe("Perform case-sensitive search. Defaults to false."),
45
+ pageSize: z
46
+ .number()
47
+ .int()
48
+ .positive()
49
+ .optional()
50
+ .default(50)
51
+ .describe("Maximum number of result files per page. Defaults to 50."),
52
+ page: z
53
+ .number()
54
+ .int()
55
+ .positive()
56
+ .optional()
57
+ .default(1)
58
+ .describe("Page number of results to return. Defaults to 1."),
59
+ maxMatchesPerFile: z
60
+ .number()
61
+ .int()
62
+ .positive()
63
+ .optional()
64
+ .default(5)
65
+ .describe("Maximum number of matches to show per file. Defaults to 5."),
66
+ })
67
+ .describe("Performs search across vault content using text or regex. Supports filtering by modification date, directory path, pagination, and limiting matches per file.");
68
+ export const ObsidianGlobalSearchInputSchemaShape = ObsidianGlobalSearchInputSchema.shape;
69
+ // ====================================================================================
70
+ // Helper Function (findMatchesInContent - for Cache Fallback)
71
+ // ====================================================================================
72
+ // Removed lineNumber calculation and return
73
+ function findMatchesInContent(content, query, useRegex, caseSensitive, contextLength, context) {
74
+ const matches = [];
75
+ let regex;
76
+ const operation = "findMatchesInContent";
77
+ const opContext = { ...context, operation };
78
+ try {
79
+ const flags = `g${caseSensitive ? "" : "i"}`;
80
+ regex = useRegex
81
+ ? new RegExp(query, flags)
82
+ : new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), flags);
83
+ }
84
+ catch (e) {
85
+ const errorMsg = `[${operation}] Invalid regex pattern: ${query}`;
86
+ logger.error(errorMsg, e instanceof Error ? e : undefined, opContext);
87
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid regex pattern: ${query}`, opContext);
88
+ }
89
+ let match;
90
+ // Removed line number calculation logic
91
+ while ((match = regex.exec(content)) !== null) {
92
+ const matchIndex = match.index;
93
+ const matchText = match[0];
94
+ const startIndex = Math.max(0, matchIndex - contextLength);
95
+ const endIndex = Math.min(content.length, matchIndex + matchText.length + contextLength);
96
+ const contextSnippet = content.substring(startIndex, endIndex);
97
+ // Find position *within* the snippet for consistency with API fallback
98
+ const positionInSnippet = contextSnippet
99
+ .toLowerCase()
100
+ .indexOf(matchText.toLowerCase());
101
+ matches.push({
102
+ // lineNumber removed
103
+ context: contextSnippet,
104
+ matchText: matchText, // Included for cache search
105
+ position: positionInSnippet >= 0 ? positionInSnippet : 0, // Included for cache search
106
+ });
107
+ if (matchText.length === 0)
108
+ regex.lastIndex++;
109
+ }
110
+ return matches;
111
+ }
112
+ // ====================================================================================
113
+ // Core Logic Function (API-First with Cache Fallback)
114
+ // ====================================================================================
115
+ const API_SEARCH_TIMEOUT_MS = config.obsidianApiSearchTimeoutMs;
116
+ export const processObsidianGlobalSearch = async (params, context, obsidianService, vaultCacheService) => {
117
+ const operation = "processObsidianGlobalSearch";
118
+ const opContext = { ...context, operation };
119
+ logger.info(`Processing obsidian_global_search request: "${params.query}" (API-first)`, { ...opContext, params: sanitizeInputForLogging(params) });
120
+ let sinceDate = null;
121
+ let untilDate = null;
122
+ let strategyMessage = "";
123
+ let allFilteredResults = []; // Store all results matching filters before pagination
124
+ let totalMatchesCount = 0; // Total matches across all files before limiting per file
125
+ // Normalize searchInPath: remove leading/trailing slashes and ensure it ends with a slash if not empty
126
+ const searchPathPrefix = params.searchInPath
127
+ ? params.searchInPath.replace(/^\/+|\/+$/g, "") +
128
+ (params.searchInPath === "/" ? "" : "/")
129
+ : ""; // Empty string means search entire vault
130
+ // 1. Parse Date Filters
131
+ const dateParseContext = { ...opContext, subOperation: "parseDates" };
132
+ try {
133
+ if (params.modified_since)
134
+ sinceDate = await dateParser.parseToDate(params.modified_since, dateParseContext);
135
+ if (params.modified_until)
136
+ untilDate = await dateParser.parseToDate(params.modified_until, dateParseContext);
137
+ }
138
+ catch (error) {
139
+ const errMsg = `Invalid date format provided`;
140
+ logger.error(errMsg, error instanceof Error ? error : undefined, dateParseContext);
141
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, errMsg, dateParseContext);
142
+ }
143
+ // 2. Attempt API Search with Retries and Timeout
144
+ let apiFailedOrTimedOut = false;
145
+ try {
146
+ strategyMessage = `Attempting live API search with retries (timeout: ${API_SEARCH_TIMEOUT_MS / 1000}s per attempt). `;
147
+ const apiSearchContext = {
148
+ ...opContext,
149
+ subOperation: "searchApiSimpleWithRetry",
150
+ };
151
+ const apiResults = await retryWithDelay(async () => {
152
+ logger.info(`Calling obsidianService.searchSimple for query: "${params.query}"`, apiSearchContext);
153
+ const apiCallPromise = obsidianService.searchSimple(params.query, params.contextLength, apiSearchContext);
154
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`API search timed out after ${API_SEARCH_TIMEOUT_MS}ms`)), API_SEARCH_TIMEOUT_MS));
155
+ return await Promise.race([apiCallPromise, timeoutPromise]);
156
+ }, {
157
+ operationName: "obsidianService.searchSimple",
158
+ context: apiSearchContext,
159
+ maxRetries: 2, // Total of 3 attempts
160
+ delayMs: 500,
161
+ shouldRetry: (err) => {
162
+ // Retry on any error during the API call phase
163
+ logger.warning(`API search attempt failed. Retrying...`, {
164
+ ...apiSearchContext,
165
+ error: err instanceof Error ? err.message : String(err),
166
+ });
167
+ return true;
168
+ },
169
+ });
170
+ strategyMessage += `API search successful, returned ${apiResults.length} potential files. `;
171
+ logger.info(`API searchSimple returned ${apiResults.length} files with potential matches.`, apiSearchContext);
172
+ // Process API results (fetch stats for date filtering and inclusion)
173
+ const fetchStatsContext = {
174
+ ...opContext,
175
+ subOperation: "fetchStatsForApiResults",
176
+ };
177
+ let processedCount = 0;
178
+ for (const apiResult of apiResults) {
179
+ const filePathFromApi = apiResult.filename; // API uses 'filename' for the full path
180
+ // Apply path filter
181
+ if (searchPathPrefix && !filePathFromApi.startsWith(searchPathPrefix)) {
182
+ continue; // Skip if file is not in the specified path
183
+ }
184
+ let mtime;
185
+ let ctime;
186
+ // Fetch stats regardless of date filtering to include in results
187
+ try {
188
+ const noteJson = (await obsidianService.getFileContent(filePathFromApi, "json", fetchStatsContext));
189
+ mtime = noteJson.stat.mtime;
190
+ ctime = noteJson.stat.ctime; // Get ctime
191
+ // Apply date filtering if needed
192
+ if ((sinceDate && mtime < sinceDate.getTime()) ||
193
+ (untilDate && mtime > untilDate.getTime())) {
194
+ continue; // Skip due to date filter
195
+ }
196
+ }
197
+ catch (statError) {
198
+ logger.warning(`Failed to fetch stats for file ${filePathFromApi}. Skipping file. Error: ${statError instanceof Error ? statError.message : String(statError)}`, fetchStatsContext);
199
+ continue; // Skip if stats cannot be fetched
200
+ }
201
+ // Transform SimpleSearchMatch[] to MatchContext[] - OMITTING matchText and position
202
+ const transformedMatches = [];
203
+ for (const apiMatch of apiResult.matches) {
204
+ transformedMatches.push({
205
+ // lineNumber removed
206
+ context: apiMatch.context, // Use the context provided by the API
207
+ // matchText and position are omitted as they cannot be reliably determined from API result
208
+ });
209
+ }
210
+ // Apply match limit per file
211
+ const limitedMatches = transformedMatches.slice(0, params.maxMatchesPerFile);
212
+ // Only add if we actually found matches after transformation/filtering
213
+ if (limitedMatches.length > 0) {
214
+ allFilteredResults.push({
215
+ // Add to the unfiltered list first
216
+ path: filePathFromApi,
217
+ filename: path.basename(filePathFromApi),
218
+ matches: limitedMatches, // Use limited matches
219
+ modifiedTime: formatTimestamp(mtime, fetchStatsContext), // Format mtime
220
+ createdTime: formatTimestamp(ctime, fetchStatsContext), // Format ctime
221
+ numericMtime: mtime, // Store numeric mtime
222
+ });
223
+ totalMatchesCount += transformedMatches.length; // Count *all* matches before limiting for total count
224
+ processedCount++;
225
+ }
226
+ }
227
+ strategyMessage += `Processed ${processedCount} files matching all filters (including path: '${searchPathPrefix || "entire vault"}'). `;
228
+ }
229
+ catch (apiError) {
230
+ // API call failed or timed out internally
231
+ apiFailedOrTimedOut = true;
232
+ strategyMessage += `API search failed or timed out (${apiError instanceof Error ? apiError.message : String(apiError)}). `;
233
+ logger.warning(strategyMessage, {
234
+ ...opContext,
235
+ subOperation: "apiSearchFailedOrTimedOut",
236
+ });
237
+ }
238
+ // 3. Fallback to Cache if API Failed/Timed Out
239
+ if (apiFailedOrTimedOut) {
240
+ if (vaultCacheService && vaultCacheService.isReady()) {
241
+ strategyMessage += "Falling back to in-memory cache. ";
242
+ logger.info("API search failed/timed out. Falling back to in-memory cache.", opContext);
243
+ const cache = vaultCacheService.getCache();
244
+ const cacheSearchContext = {
245
+ ...opContext,
246
+ subOperation: "searchCacheFallback",
247
+ };
248
+ allFilteredResults = []; // Reset results for cache search
249
+ totalMatchesCount = 0;
250
+ let processedCount = 0;
251
+ for (const [filePath, cacheEntry] of cache.entries()) {
252
+ // Apply path filter
253
+ if (searchPathPrefix && !filePath.startsWith(searchPathPrefix)) {
254
+ continue; // Skip if file is not in the specified path
255
+ }
256
+ const mtime = cacheEntry.mtime; // Get mtime from cache
257
+ // Apply date filtering
258
+ if ((sinceDate && mtime < sinceDate.getTime()) ||
259
+ (untilDate && mtime > untilDate.getTime())) {
260
+ continue;
261
+ }
262
+ try {
263
+ const matches = findMatchesInContent(cacheEntry.content, params.query, params.useRegex, params.caseSensitive, params.contextLength, cacheSearchContext);
264
+ // Apply match limit per file
265
+ const limitedMatches = matches.slice(0, params.maxMatchesPerFile);
266
+ if (limitedMatches.length > 0) {
267
+ let ctime = null;
268
+ // Attempt to fetch ctime as cache likely doesn't have it
269
+ try {
270
+ const noteJson = (await obsidianService.getFileContent(filePath, "json", cacheSearchContext));
271
+ ctime = noteJson.stat.ctime;
272
+ }
273
+ catch (statError) {
274
+ logger.warning(`Failed to fetch ctime for cached file ${filePath} during fallback. Error: ${statError instanceof Error ? statError.message : String(statError)}`, cacheSearchContext);
275
+ // Proceed without ctime if fetch fails
276
+ }
277
+ allFilteredResults.push({
278
+ // Add to unfiltered list
279
+ path: filePath,
280
+ filename: path.basename(filePath),
281
+ modifiedTime: formatTimestamp(mtime, cacheSearchContext), // Format mtime
282
+ createdTime: formatTimestamp(ctime ?? mtime, cacheSearchContext), // Format ctime (or mtime fallback)
283
+ matches: limitedMatches, // Use limited matches
284
+ numericMtime: mtime, // Store numeric mtime from cache
285
+ });
286
+ totalMatchesCount += matches.length; // Count *all* matches before limiting
287
+ processedCount++;
288
+ }
289
+ }
290
+ catch (matchError) {
291
+ logger.warning(`Error matching content in cached file ${filePath} during fallback: ${matchError instanceof Error ? matchError.message : String(matchError)}`, cacheSearchContext);
292
+ }
293
+ }
294
+ strategyMessage += `Searched ${cache.size} cached files, processed ${processedCount} matching all filters (including path: '${searchPathPrefix || "entire vault"}'). `;
295
+ }
296
+ else {
297
+ // This block now handles both "cache disabled" and "cache not ready"
298
+ const reason = vaultCacheService ? "is not ready" : "is disabled";
299
+ strategyMessage += `Cache not available (${reason}), unable to fallback. `;
300
+ logger.error(`API search failed and cache ${reason}. Cannot perform search.`, opContext);
301
+ // Throw a specific error because the tool cannot function without a data source.
302
+ throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, `Live API search failed and the cache is currently ${reason}. Please ensure the Obsidian REST API is running and reachable, and that the cache is enabled and has had time to build.`, opContext);
303
+ }
304
+ }
305
+ // 4. Apply Pagination and Sorting
306
+ const totalFilesFound = allFilteredResults.length;
307
+ const pageSize = params.pageSize;
308
+ const currentPage = params.page;
309
+ const totalPages = Math.ceil(totalFilesFound / pageSize);
310
+ const startIndex = (currentPage - 1) * pageSize;
311
+ const endIndex = startIndex + pageSize;
312
+ // Sort results by numeric modified time (descending) *before* pagination
313
+ allFilteredResults.sort((a, b) => {
314
+ return b.numericMtime - a.numericMtime; // Descending
315
+ });
316
+ const paginatedResults = allFilteredResults.slice(startIndex, endIndex);
317
+ // 5. Determine alsoFoundInFiles
318
+ let alsoFoundInFiles = undefined;
319
+ if (totalPages > 1) {
320
+ const paginatedFilePaths = new Set(paginatedResults.map((r) => r.path));
321
+ alsoFoundInFiles = allFilteredResults
322
+ .filter((r) => !paginatedFilePaths.has(r.path)) // Get files not on the current page
323
+ .map((r) => r.filename); // Then get their filenames
324
+ alsoFoundInFiles = [...new Set(alsoFoundInFiles)]; // Ensure unique filenames in the final list
325
+ }
326
+ // 6. Construct Final Response
327
+ const finalMessage = `${strategyMessage}Found ${totalMatchesCount} matches across ${totalFilesFound} files matching all criteria. Returning page ${currentPage} of ${totalPages} (${paginatedResults.length} files on this page, page size ${pageSize}, max matches per file ${params.maxMatchesPerFile}). Results sorted by modification date (newest first).`;
328
+ const response = {
329
+ success: true, // Indicate overall tool success, even if fallback was used or results are empty
330
+ message: finalMessage,
331
+ results: paginatedResults,
332
+ totalFilesFound: totalFilesFound,
333
+ totalMatchesFound: totalMatchesCount,
334
+ currentPage: currentPage,
335
+ pageSize: pageSize,
336
+ totalPages: totalPages,
337
+ alsoFoundInFiles: alsoFoundInFiles, // Add the list here
338
+ };
339
+ logger.info(`Global search processing completed. ${finalMessage}`, opContext);
340
+ return response;
341
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * @module ObsidianGlobalSearchToolRegistration
3
+ * @description Registers the 'obsidian_global_search' tool with the MCP server.
4
+ * This tool allows searching the Obsidian vault using text/regex queries with optional date filters.
5
+ */
6
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import type { ObsidianRestApiService } from "../../../services/obsidianRestAPI/index.js";
8
+ import type { VaultCacheService } from "../../../services/obsidianRestAPI/vaultCache/index.js";
9
+ /**
10
+ * Registers the 'obsidian_global_search' tool with the MCP server instance.
11
+ *
12
+ * @param {McpServer} server - The MCP server instance.
13
+ * @param {ObsidianRestApiService} obsidianService - The instance of the Obsidian REST API service.
14
+ * @param {VaultCacheService} vaultCacheService - The instance of the Vault Cache service.
15
+ * @returns {Promise<void>} A promise that resolves when the tool is registered.
16
+ * @throws {McpError} If registration fails critically.
17
+ */
18
+ export declare function registerObsidianGlobalSearchTool(server: McpServer, obsidianService: ObsidianRestApiService, vaultCacheService: VaultCacheService): Promise<void>;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @module ObsidianGlobalSearchToolRegistration
3
+ * @description Registers the 'obsidian_global_search' tool with the MCP server.
4
+ * This tool allows searching the Obsidian vault using text/regex queries with optional date filters.
5
+ */
6
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
7
+ import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
8
+ import { ObsidianGlobalSearchInputSchemaShape, processObsidianGlobalSearch, } from "./logic.js"; // Ensure '.js' extension
9
+ /**
10
+ * Registers the 'obsidian_global_search' tool with the MCP server instance.
11
+ *
12
+ * @param {McpServer} server - The MCP server instance.
13
+ * @param {ObsidianRestApiService} obsidianService - The instance of the Obsidian REST API service.
14
+ * @param {VaultCacheService} vaultCacheService - The instance of the Vault Cache service.
15
+ * @returns {Promise<void>} A promise that resolves when the tool is registered.
16
+ * @throws {McpError} If registration fails critically.
17
+ */
18
+ export async function registerObsidianGlobalSearchTool(server, obsidianService, vaultCacheService) {
19
+ const toolName = "obsidian_global_search";
20
+ const toolDescription = `Performs search across the Obsidian vault using text or regex, primarily relying on the Obsidian REST API's simple search. Supports filtering by modification date, optionally restricting search to a specific directory path (recursively), pagination (page, pageSize), and limiting matches shown per file (maxMatchesPerFile). Returns a JSON object containing success status, a message, pagination details (currentPage, pageSize, totalPages), total file/match counts (before pagination), and an array of results. Each result includes the file path, filename, creation timestamp (ctime), modification timestamp (mtime), and an array of match context snippets (limited by maxMatchesPerFile). If there are multiple pages of results, it also includes an 'alsoFoundInFiles' array listing filenames found on other pages.`;
21
+ const registrationContext = requestContextService.createRequestContext({
22
+ operation: "RegisterObsidianGlobalSearchTool",
23
+ toolName: toolName,
24
+ module: "ObsidianGlobalSearchRegistration",
25
+ });
26
+ logger.info(`Attempting to register tool: ${toolName}`, registrationContext);
27
+ await ErrorHandler.tryCatch(async () => {
28
+ server.tool(toolName, toolDescription, ObsidianGlobalSearchInputSchemaShape, async (params, handlerInvocationContext) => {
29
+ const handlerContext = requestContextService.createRequestContext({
30
+ operation: "HandleObsidianGlobalSearchRequest",
31
+ toolName: toolName,
32
+ paramsSummary: {
33
+ useRegex: params.useRegex,
34
+ caseSensitive: params.caseSensitive,
35
+ pageSize: params.pageSize,
36
+ page: params.page,
37
+ maxMatchesPerFile: params.maxMatchesPerFile,
38
+ searchInPath: params.searchInPath,
39
+ hasDateFilter: !!(params.modified_since || params.modified_until),
40
+ },
41
+ });
42
+ logger.debug(`Handling '${toolName}' request`, handlerContext);
43
+ return await ErrorHandler.tryCatch(async () => {
44
+ const response = await processObsidianGlobalSearch(params, handlerContext, obsidianService, vaultCacheService);
45
+ logger.debug(`'${toolName}' processed successfully`, handlerContext);
46
+ return {
47
+ content: [
48
+ {
49
+ type: "text",
50
+ text: JSON.stringify(response, null, 2),
51
+ },
52
+ ],
53
+ isError: false,
54
+ };
55
+ }, {
56
+ operation: `executing tool ${toolName}`,
57
+ context: handlerContext,
58
+ errorCode: BaseErrorCode.INTERNAL_ERROR,
59
+ });
60
+ });
61
+ logger.info(`Tool registered successfully: ${toolName}`, registrationContext);
62
+ }, {
63
+ operation: `registering tool ${toolName}`,
64
+ context: registrationContext,
65
+ errorCode: BaseErrorCode.INTERNAL_ERROR,
66
+ 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 }),
67
+ critical: true,
68
+ });
69
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @fileoverview Barrel file for the 'obsidian_list_files' MCP tool.
3
+ *
4
+ * This file serves as the public entry point for the obsidian_list_files tool module.
5
+ * It re-exports the primary registration function (`registerObsidianListFilesTool`)
6
+ * from the './registration.js' module. This pattern simplifies imports for consumers
7
+ * of the tool, allowing them to import necessary components from a single location.
8
+ *
9
+ * Consumers (like the main server setup) should import the registration function
10
+ * from this file to integrate the tool into the MCP server instance.
11
+ */
12
+ export { registerObsidianListFilesTool } from "./registration.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @fileoverview Barrel file for the 'obsidian_list_files' MCP tool.
3
+ *
4
+ * This file serves as the public entry point for the obsidian_list_files tool module.
5
+ * It re-exports the primary registration function (`registerObsidianListFilesTool`)
6
+ * from the './registration.js' module. This pattern simplifies imports for consumers
7
+ * of the tool, allowing them to import necessary components from a single location.
8
+ *
9
+ * Consumers (like the main server setup) should import the registration function
10
+ * from this file to integrate the tool into the MCP server instance.
11
+ */
12
+ export { registerObsidianListFilesTool } from "./registration.js";
@@ -0,0 +1,64 @@
1
+ import { z } from "zod";
2
+ import { ObsidianRestApiService } from "../../../services/obsidianRestAPI/index.js";
3
+ import { RequestContext } from "../../../utils/index.js";
4
+ /**
5
+ * Zod schema for validating the input parameters of the 'obsidian_list_files' tool.
6
+ */
7
+ export declare const ObsidianListFilesInputSchema: z.ZodObject<{
8
+ /**
9
+ * The vault-relative path to the directory whose contents should be listed.
10
+ * Examples: "Attachments/Images", "Projects", "" (for vault root), "/" (for vault root).
11
+ * The path is treated as case-sensitive by the underlying Obsidian API.
12
+ */
13
+ dirPath: z.ZodString;
14
+ /**
15
+ * Optional array of file extensions (including the leading dot) to filter the results.
16
+ * Only files matching one of these extensions will be included. Directories are always included regardless of this filter.
17
+ * Example: [".md", ".png"]
18
+ */
19
+ fileExtensionFilter: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
20
+ /**
21
+ * Optional JavaScript-compatible regular expression pattern string to filter results by name.
22
+ * Only files and directories whose names match the regex will be included.
23
+ * Example: "^\\d{4}-\\d{2}-\\d{2}" (matches names starting with YYYY-MM-DD)
24
+ */
25
+ nameRegexFilter: z.ZodOptional<z.ZodNullable<z.ZodString>>;
26
+ }, "strip", z.ZodTypeAny, {
27
+ dirPath: string;
28
+ fileExtensionFilter?: string[] | undefined;
29
+ nameRegexFilter?: string | null | undefined;
30
+ }, {
31
+ dirPath: string;
32
+ fileExtensionFilter?: string[] | undefined;
33
+ nameRegexFilter?: string | null | undefined;
34
+ }>;
35
+ /**
36
+ * TypeScript type inferred from the input schema (`ObsidianListFilesInputSchema`).
37
+ * Represents the validated input parameters used within the core processing logic.
38
+ */
39
+ export type ObsidianListFilesInput = z.infer<typeof ObsidianListFilesInputSchema>;
40
+ /**
41
+ * Defines the structure of the successful response returned by the `processObsidianListFiles` function.
42
+ * This object is typically serialized to JSON and sent back to the client.
43
+ */
44
+ export interface ObsidianListFilesResponse {
45
+ /** The vault-relative path of the directory whose contents were listed (normalized, e.g., "/" for root). */
46
+ directoryPath: string;
47
+ /** A string representation of the directory contents formatted as a simple tree structure. */
48
+ tree: string;
49
+ /** The total number of files and directories included in the formatted tree after filtering. */
50
+ totalEntries: number;
51
+ }
52
+ /**
53
+ * Processes the core logic for listing files and directories within a specified
54
+ * directory in the Obsidian vault. Applies optional filters and formats the output.
55
+ *
56
+ * @param {ObsidianListFilesInput} params - The validated input parameters.
57
+ * @param {RequestContext} context - The request context for logging and correlation.
58
+ * @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service.
59
+ * @returns {Promise<ObsidianListFilesResponse>} A promise resolving to the structured success response
60
+ * containing the listed directory path, a formatted tree string, and the total entry count.
61
+ * @throws {McpError} Throws an McpError if the directory cannot be listed (e.g., not found)
62
+ * or if any other API interaction or validation fails.
63
+ */
64
+ export declare const processObsidianListFiles: (params: ObsidianListFilesInput, context: RequestContext, obsidianService: ObsidianRestApiService) => Promise<ObsidianListFilesResponse>;