obsidian-mcp-server 1.5.7 → 2.0.1

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 (204) hide show
  1. package/README.md +249 -101
  2. package/dist/config/index.d.ts +41 -0
  3. package/dist/config/index.js +191 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.js +298 -0
  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 -0
  97. package/dist/utils/index.js +13 -0
  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 +72 -40
  133. package/.github/workflows/publish.yml +0 -35
  134. package/Dockerfile +0 -63
  135. package/build/index.js +0 -20
  136. package/build/mcp/handlers.js +0 -305
  137. package/build/mcp/index.js +0 -7
  138. package/build/mcp/server.js +0 -240
  139. package/build/mcp/types.js +0 -49
  140. package/build/obsidian/client.js +0 -403
  141. package/build/obsidian/errors.js +0 -75
  142. package/build/obsidian/index.js +0 -7
  143. package/build/obsidian/types.js +0 -12
  144. package/build/resources/index.js +0 -15
  145. package/build/resources/tags.js +0 -257
  146. package/build/resources/types.js +0 -5
  147. package/build/tools/base.js +0 -88
  148. package/build/tools/files/content.js +0 -171
  149. package/build/tools/files/index.js +0 -22
  150. package/build/tools/files/list.js +0 -133
  151. package/build/tools/index.js +0 -31
  152. package/build/tools/properties/index.js +0 -19
  153. package/build/tools/properties/manager.js +0 -302
  154. package/build/tools/properties/tools.js +0 -239
  155. package/build/tools/properties/types.js +0 -70
  156. package/build/tools/search/complex.js +0 -270
  157. package/build/tools/search/index.js +0 -20
  158. package/build/tools/search/simple.js +0 -127
  159. package/build/utils/errors.js +0 -59
  160. package/build/utils/idGenerator.js +0 -21
  161. package/build/utils/index.js +0 -9
  162. package/build/utils/logging.js +0 -417
  163. package/build/utils/rate-limiting.js +0 -94
  164. package/build/utils/tokenization.js +0 -75
  165. package/build/utils/validation.js +0 -92
  166. package/debug.js +0 -98
  167. package/docs/tree.md +0 -67
  168. package/env.json +0 -42
  169. package/mcp-client-config.example.json +0 -23
  170. package/repomix.config.json +0 -20
  171. package/scripts/clean.ts +0 -103
  172. package/scripts/tree.ts +0 -599
  173. package/src/index.ts +0 -23
  174. package/src/mcp/handlers.ts +0 -410
  175. package/src/mcp/index.ts +0 -7
  176. package/src/mcp/server.ts +0 -288
  177. package/src/mcp/types.ts +0 -112
  178. package/src/obsidian/client.ts +0 -505
  179. package/src/obsidian/errors.ts +0 -105
  180. package/src/obsidian/index.ts +0 -7
  181. package/src/obsidian/types.ts +0 -124
  182. package/src/resources/index.ts +0 -17
  183. package/src/resources/tags.ts +0 -283
  184. package/src/resources/types.ts +0 -30
  185. package/src/tools/base.ts +0 -123
  186. package/src/tools/files/content.ts +0 -206
  187. package/src/tools/files/index.ts +0 -31
  188. package/src/tools/files/list.ts +0 -150
  189. package/src/tools/index.ts +0 -38
  190. package/src/tools/properties/index.ts +0 -21
  191. package/src/tools/properties/manager.ts +0 -370
  192. package/src/tools/properties/tools.ts +0 -280
  193. package/src/tools/properties/types.ts +0 -117
  194. package/src/tools/search/complex.ts +0 -303
  195. package/src/tools/search/index.ts +0 -22
  196. package/src/tools/search/simple.ts +0 -147
  197. package/src/utils/errors.ts +0 -69
  198. package/src/utils/idGenerator.ts +0 -22
  199. package/src/utils/index.ts +0 -8
  200. package/src/utils/logging.ts +0 -558
  201. package/src/utils/rate-limiting.ts +0 -114
  202. package/src/utils/tokenization.ts +0 -85
  203. package/src/utils/validation.ts +0 -105
  204. package/tsconfig.json +0 -17
@@ -1,370 +0,0 @@
1
- /**
2
- * Property manager for Obsidian notes
3
- */
4
- import { EOL } from 'os';
5
- import { parse, stringify } from 'yaml';
6
- import { ObsidianClient } from '../../obsidian/client.js';
7
- import { createLogger } from '../../utils/logging.js';
8
- import {
9
- ObsidianProperties,
10
- ObsidianPropertiesSchema,
11
- PropertyManagerResult,
12
- PropertyUpdateSchema,
13
- ValidationResult
14
- } from './types.js';
15
-
16
- // Create a logger for property operations
17
- const logger = createLogger('PropertyManager');
18
-
19
- /**
20
- * Simple deep merge function for plain objects and arrays.
21
- * Merges 'source' into 'target'. Modifies 'target' in place.
22
- */
23
- function deepMerge(target: any, source: any): any {
24
- if (typeof target !== 'object' || target === null || typeof source !== 'object' || source === null) {
25
- return source; // Overwrite if not both are mergeable objects
26
- }
27
-
28
- for (const key in source) {
29
- // eslint-disable-next-line no-prototype-builtins
30
- if (source.hasOwnProperty(key)) {
31
- const targetValue = target[key];
32
- const sourceValue = source[key];
33
-
34
- if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
35
- // For arrays, concatenate and remove duplicates for simple arrays,
36
- // otherwise, replace (could be enhanced for object arrays if needed)
37
- target[key] = [...new Set([...targetValue, ...sourceValue])];
38
- } else if (typeof targetValue === 'object' && targetValue !== null && typeof sourceValue === 'object' && sourceValue !== null) {
39
- // Recursively merge nested objects
40
- deepMerge(targetValue, sourceValue);
41
- } else {
42
- // Overwrite primitive values or if types mismatch
43
- target[key] = sourceValue;
44
- }
45
- }
46
- }
47
- return target;
48
- }
49
-
50
-
51
- /**
52
- * Result type for parseProperties, including match details
53
- */
54
- interface ParseResult {
55
- properties: ObsidianProperties;
56
- match?: {
57
- startIndex: number;
58
- endIndex: number;
59
- rawFrontmatter: string; // The raw YAML string
60
- };
61
- error?: Error; // Include error if parsing/validation failed
62
- }
63
-
64
-
65
- /**
66
- * Manages YAML frontmatter properties in Obsidian notes
67
- */
68
- export class PropertyManager {
69
- constructor(private client: ObsidianClient) {}
70
-
71
- /**
72
- * Parse YAML frontmatter from note content. Finds the *first* valid block.
73
- * @param content The note content
74
- * @returns ParseResult containing properties and match details or error.
75
- */
76
- parseProperties(content: string): ParseResult {
77
- try {
78
- // Regex to find the first frontmatter block (allows leading content)
79
- // It captures the content between the --- markers.
80
- const match = content.match(/(?:^|\r?\n)---\r?\n([\s\S]*?)\r?\n---/);
81
-
82
- if (!match) {
83
- logger.debug('No frontmatter found in content');
84
- return { properties: {} };
85
- }
86
-
87
- const frontmatterYaml = match[1];
88
- // Calculate accurate start/end indices of the *entire* matched block (including ---)
89
- const blockStartIndex = match.index ?? 0;
90
- const blockEndIndex = blockStartIndex + match[0].length;
91
-
92
-
93
- // Parse YAML first
94
- const rawProperties = parse(frontmatterYaml);
95
-
96
- // Validate the raw parsed object against the schema
97
- const validationResult = ObsidianPropertiesSchema.safeParse(rawProperties);
98
-
99
- if (!validationResult.success) {
100
- const error = new Error(`Frontmatter validation failed: ${validationResult.error.message}`);
101
- logger.warn(error.message, { validationError: validationResult.error.flatten() });
102
- // Return parsed (but invalid) data along with the error and match info
103
- return {
104
- properties: rawProperties as ObsidianProperties, // Cast, knowing it's invalid
105
- match: { startIndex: blockStartIndex, endIndex: blockEndIndex, rawFrontmatter: frontmatterYaml },
106
- error
107
- };
108
- }
109
-
110
- // Use the validated data from now on
111
- const validatedProperties = validationResult.data;
112
-
113
- // Handle tags transformation (remove '#') on validated data
114
- if (validatedProperties.tags && Array.isArray(validatedProperties.tags)) {
115
- validatedProperties.tags = validatedProperties.tags.map((tag: string) =>
116
- typeof tag === 'string' && tag.startsWith('#') ? tag.substring(1) : tag
117
- );
118
- }
119
-
120
- // Return the validated properties and match info
121
- return {
122
- properties: validatedProperties,
123
- match: { startIndex: blockStartIndex, endIndex: blockEndIndex, rawFrontmatter: frontmatterYaml }
124
- };
125
- } catch (error) {
126
- const parseError = error instanceof Error ? error : new Error(String(error));
127
- logger.error('Error parsing properties:', parseError);
128
- // Return error information
129
- return {
130
- properties: {},
131
- error: parseError
132
- };
133
- }
134
- }
135
-
136
- /**
137
- * Generate YAML frontmatter from properties
138
- * @param properties The properties to convert to YAML
139
- * @returns YAML frontmatter string
140
- */
141
- generateProperties(properties: Partial<ObsidianProperties>): string {
142
- try {
143
- // Create a deep copy to avoid modifying the original object
144
- const propsToGenerate = JSON.parse(JSON.stringify(properties));
145
-
146
- // Add '#' prefix back to tags before stringifying
147
- if (propsToGenerate.tags && Array.isArray(propsToGenerate.tags)) {
148
- propsToGenerate.tags = propsToGenerate.tags.map((tag: string) =>
149
- typeof tag === 'string' && !tag.startsWith('#') ? `#${tag}` : tag
150
- );
151
- }
152
-
153
- // Remove undefined values explicitly (shouldn't be necessary with JSON.stringify/parse but safe)
154
- const cleanProperties = Object.fromEntries(
155
- Object.entries(propsToGenerate).filter(([_, v]) => v !== undefined)
156
- );
157
-
158
- // Generate YAML with platform-specific line endings
159
- const yaml = stringify(cleanProperties);
160
- // Ensure consistent EOL for the markers and the content
161
- return `---${EOL}${yaml.trimEnd()}${EOL}---`; // Return block without trailing EOL initially
162
- } catch (error) {
163
- logger.error('Error generating properties:', error instanceof Error ? error : { error: String(error) });
164
- // Re-throw to be caught by the caller
165
- throw error instanceof Error ? error : new Error(String(error));
166
- }
167
- }
168
-
169
- /**
170
- * Validate property values against schema
171
- * @param properties The properties to validate
172
- * @returns Validation result
173
- */
174
- validateProperties(properties: Partial<ObsidianProperties>): ValidationResult {
175
- const result = PropertyUpdateSchema.safeParse(properties);
176
-
177
- if (result.success) {
178
- return { valid: true, errors: [] };
179
- }
180
-
181
- return {
182
- valid: false,
183
- errors: result.error.errors.map(err =>
184
- `${err.path.join('.')}: ${err.message}`
185
- )
186
- };
187
- }
188
-
189
- /**
190
- * Merge new properties with existing ones
191
- * @param existing The existing properties
192
- * @param updates The new properties to merge
193
- * @param replace Whether to replace arrays instead of merging them
194
- * @returns The merged properties
195
- */
196
- mergeProperties(
197
- existing: ObsidianProperties,
198
- updates: Partial<ObsidianProperties>,
199
- replace: boolean = false
200
- ): ObsidianProperties {
201
- // Start with a deep copy of existing properties to avoid modifying the original
202
- const merged = JSON.parse(JSON.stringify(existing));
203
-
204
- for (const [key, value] of Object.entries(updates)) {
205
- // Skip undefined values and timestamp fields (created is handled separately)
206
- if (value === undefined || key === 'modified') continue;
207
-
208
- const currentValue = merged[key as keyof ObsidianProperties];
209
-
210
- // Handle arrays based on replace flag
211
- if (Array.isArray(value) && Array.isArray(currentValue)) {
212
- merged[key as keyof ObsidianProperties] = replace ?
213
- value : // Replace array
214
- [...new Set([...currentValue, ...value])] as any; // Merge unique elements
215
- }
216
- // Special handling for custom object - use deep merge
217
- else if (key === 'custom' && typeof value === 'object' && value !== null && typeof currentValue === 'object' && currentValue !== null) {
218
- // Ensure target 'custom' exists before merging into it
219
- if (!merged.custom) merged.custom = {};
220
- merged.custom = deepMerge(merged.custom, value); // Deep merge 'custom'
221
- }
222
- // Default case - replace value
223
- else {
224
- merged[key as keyof ObsidianProperties] = value as any;
225
- }
226
- }
227
-
228
- // Handle 'created' timestamp: preserve existing or set if new
229
- merged.created = existing.created ?? new Date().toISOString();
230
- // Always update 'modified' timestamp
231
- merged.modified = new Date().toISOString();
232
-
233
- // Remove properties that are explicitly set to null in the update
234
- for (const [key, value] of Object.entries(updates)) {
235
- if (value === null) {
236
- delete merged[key as keyof ObsidianProperties];
237
- }
238
- }
239
-
240
-
241
- return merged;
242
- }
243
-
244
- /**
245
- * Get properties from a note
246
- * @param filepath Path to the note
247
- * @returns The result including properties or errors
248
- */
249
- async getProperties(filepath: string): Promise<PropertyManagerResult> {
250
- try {
251
- logger.debug(`Getting properties from file: ${filepath}`);
252
- const content = await this.client.getFileContents(filepath);
253
- const parseResult = this.parseProperties(content);
254
-
255
- // Handle parsing/validation errors reported by parseProperties
256
- if (parseResult.error) {
257
- // If validation failed but we have a match, still return the raw (invalid) props?
258
- // For getProperties, it's better to report failure clearly.
259
- return {
260
- success: false,
261
- message: `Failed to parse or validate properties: ${parseResult.error.message}`,
262
- errors: [parseResult.error.message]
263
- };
264
- }
265
-
266
- return {
267
- success: true,
268
- message: 'Properties retrieved successfully',
269
- properties: parseResult.properties
270
- };
271
- } catch (error) {
272
- // Catch errors from getFileContents or other unexpected issues
273
- const err = error instanceof Error ? error : new Error(String(error));
274
- logger.error(`Failed to get properties from ${filepath}:`, err);
275
- return {
276
- success: false,
277
- message: `Failed to get properties: ${err.message}`,
278
- errors: [err.message]
279
- };
280
- }
281
- }
282
-
283
- /**
284
- * Update properties of a note
285
- * @param filepath Path to the note
286
- * @param newProperties The new properties to apply
287
- * @param replace Whether to replace arrays instead of merging them
288
- * @returns The result of the update operation
289
- */
290
- async updateProperties(
291
- filepath: string,
292
- newProperties: Partial<ObsidianProperties>,
293
- replace: boolean = false
294
- ): Promise<PropertyManagerResult> {
295
- try {
296
- // Validate new properties first
297
- const validation = this.validateProperties(newProperties);
298
- if (!validation.valid) {
299
- logger.warn(`Invalid properties provided for update in ${filepath}:`, { errors: validation.errors });
300
- return {
301
- success: false,
302
- message: 'Invalid properties provided for update',
303
- errors: validation.errors
304
- };
305
- }
306
-
307
- // Get existing content and parse properties
308
- logger.debug(`Updating properties for file: ${filepath}`);
309
- const content = await this.client.getFileContents(filepath);
310
- const parseResult = this.parseProperties(content);
311
-
312
- // Handle parsing/validation errors from existing frontmatter *before* merging
313
- if (parseResult.error) {
314
- logger.warn(`Could not parse existing frontmatter in ${filepath}, but proceeding with update: ${parseResult.error.message}`);
315
- // We proceed, merging with potentially empty or invalid existing properties.
316
- // The new valid frontmatter will overwrite the old invalid one.
317
- }
318
-
319
- // Merge properties
320
- const mergedProperties = this.mergeProperties(parseResult.properties, newProperties, replace);
321
-
322
- // Generate new frontmatter YAML string
323
- const newFrontmatterBlock = this.generateProperties(mergedProperties);
324
-
325
- let updatedContent: string;
326
- const bodyContentStartIndex = parseResult.match ? parseResult.match.endIndex : 0;
327
- let bodyContent = content.substring(bodyContentStartIndex);
328
-
329
- // Ensure body content starts with a newline if it's not empty
330
- if (bodyContent.length > 0 && !bodyContent.startsWith('\n') && !bodyContent.startsWith('\r\n')) {
331
- bodyContent = EOL + bodyContent;
332
- } else if (bodyContent.length === 0 && content.length > 0 && parseResult.match) {
333
- // If there was frontmatter but no body, ensure a newline after new frontmatter
334
- bodyContent = EOL;
335
- }
336
-
337
-
338
- if (parseResult.match) {
339
- // Replace the existing block accurately using start/end indices
340
- updatedContent =
341
- content.substring(0, parseResult.match.startIndex) + // Content before frontmatter
342
- newFrontmatterBlock + // New frontmatter
343
- bodyContent; // Content after frontmatter (with adjusted leading newline)
344
- } else {
345
- // No frontmatter found, prepend the new block
346
- updatedContent = newFrontmatterBlock + bodyContent;
347
- }
348
-
349
- // Update file content via client
350
- // Trim trailing whitespace from the final content before saving
351
- await this.client.updateContent(filepath, updatedContent.trimEnd() + EOL);
352
- logger.debug(`Successfully updated properties for ${filepath}`);
353
-
354
- return {
355
- success: true,
356
- message: 'Properties updated successfully',
357
- properties: mergedProperties // Return the final merged properties
358
- };
359
- } catch (error) {
360
- // Catch errors from getFileContents, generateProperties, updateContent, etc.
361
- const err = error instanceof Error ? error : new Error(String(error));
362
- logger.error(`Failed to update properties for ${filepath}:`, err);
363
- return {
364
- success: false,
365
- message: `Failed to update properties: ${err.message}`,
366
- errors: [err.message]
367
- };
368
- }
369
- }
370
- }
@@ -1,280 +0,0 @@
1
- /**
2
- * Property-related tool implementations
3
- */
4
- import { TextContent, Tool } from "@modelcontextprotocol/sdk/types.js";
5
- import { ObsidianClient } from "../../obsidian/client.js";
6
- import { createLogger, ErrorCategoryType } from "../../utils/logging.js";
7
- import { BaseToolHandler } from "../base.js";
8
- import { PropertyManager } from "./manager.js";
9
- import { ObsidianProperties } from "./types.js";
10
-
11
- // Create a logger for property tools
12
- const logger = createLogger('PropertyTools');
13
-
14
- /**
15
- * Helper function to safely convert any error to an object
16
- */
17
- function errorToObject(error: unknown): Record<string, unknown> {
18
- if (error instanceof Error) {
19
- return {
20
- name: error.name,
21
- message: error.message,
22
- stack: error.stack,
23
- errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
24
- };
25
- } else if (typeof error === 'object' && error !== null) {
26
- return error as Record<string, unknown>;
27
- } else {
28
- return {
29
- error: String(error),
30
- errorCategory: ErrorCategoryType.CATEGORY_UNKNOWN
31
- };
32
- }
33
- }
34
-
35
- /**
36
- * Tool names for property operations
37
- */
38
- export const PROPERTY_TOOL_NAMES = {
39
- GET_PROPERTIES: "obsidian_get_properties",
40
- UPDATE_PROPERTIES: "obsidian_update_properties"
41
- } as const;
42
-
43
- /**
44
- * Arguments for getting properties
45
- */
46
- interface GetPropertiesArgs {
47
- filepath: string;
48
- }
49
-
50
- /**
51
- * Arguments for updating properties
52
- */
53
- interface UpdatePropertiesArgs {
54
- filepath: string;
55
- properties: Partial<ObsidianProperties>;
56
- replace?: boolean;
57
- }
58
-
59
- /**
60
- * Tool handler for getting properties from a note
61
- */
62
- export class GetPropertiesToolHandler extends BaseToolHandler<GetPropertiesArgs> {
63
- private propertyManager: PropertyManager;
64
-
65
- constructor(client: ObsidianClient) {
66
- super(PROPERTY_TOOL_NAMES.GET_PROPERTIES, client);
67
- this.propertyManager = new PropertyManager(client);
68
- }
69
-
70
- getToolDescription(): Tool {
71
- return {
72
- name: this.name,
73
- description: "Retrieves properties (like title, tags, status) from the YAML frontmatter of a specified Obsidian note. Returns all defined properties, including any custom fields.",
74
- examples: [
75
- {
76
- description: "Get properties from a specific note",
77
- args: {
78
- filepath: "path/to/note.md"
79
- }
80
- },
81
- {
82
- description: "Get properties from a documentation file",
83
- args: {
84
- filepath: "docs/architecture.md"
85
- }
86
- }
87
- ],
88
- inputSchema: {
89
- type: "object",
90
- properties: {
91
- filepath: {
92
- type: "string",
93
- description: "Path to the note file (relative to vault root)",
94
- format: "path"
95
- }
96
- },
97
- required: ["filepath"]
98
- }
99
- };
100
- }
101
-
102
- async runTool(args: GetPropertiesArgs): Promise<Array<TextContent>> {
103
- logger.startTimer(`get_properties_${args.filepath}`);
104
-
105
- try {
106
- logger.debug(`Getting properties from: ${args.filepath}`);
107
- const result = await this.propertyManager.getProperties(args.filepath);
108
-
109
- const elapsedMs = logger.endTimer(`get_properties_${args.filepath}`);
110
- logger.logOperationResult(true, 'get_properties', elapsedMs, {
111
- filepath: args.filepath,
112
- propertyCount: Object.keys(result.properties || {}).length
113
- });
114
-
115
- return this.createResponse(result);
116
- } catch (error) {
117
- const elapsedMs = logger.endTimer(`get_properties_${args.filepath}`);
118
- logger.logOperationResult(false, 'get_properties', elapsedMs, {
119
- filepath: args.filepath
120
- });
121
-
122
- logger.error(`Error getting properties from ${args.filepath}:`, errorToObject(error));
123
- return this.handleError(error);
124
- }
125
- }
126
- }
127
-
128
- /**
129
- * Tool handler for updating properties in a note
130
- */
131
- export class UpdatePropertiesToolHandler extends BaseToolHandler<UpdatePropertiesArgs> {
132
- private propertyManager: PropertyManager;
133
-
134
- constructor(client: ObsidianClient) {
135
- super(PROPERTY_TOOL_NAMES.UPDATE_PROPERTIES, client);
136
- this.propertyManager = new PropertyManager(client);
137
- }
138
-
139
- getToolDescription(): Tool {
140
- return {
141
- name: this.name,
142
- description: "Updates properties within the YAML frontmatter of a specified Obsidian note. By default, array properties (like tags, type, status) are merged; use the 'replace' option to overwrite them instead. Handles custom fields and manages timestamps automatically. See schema for supported standard fields (title, author, tags, status, etc.).",
143
- examples: [
144
- {
145
- description: "Update title and author, merge tags",
146
- args: {
147
- filepath: "path/to/note.md",
148
- properties: {
149
- title: "Architecture Overview",
150
- author: "Development Team",
151
- type: ["architecture", "specification"]
152
- }
153
- }
154
- },
155
- {
156
- description: "Update tags and status with replace",
157
- args: {
158
- filepath: "docs/feature.md",
159
- properties: {
160
- tags: ["#feature", "#in-development", "#high-priority"],
161
- status: ["in-progress"]
162
- },
163
- replace: true
164
- }
165
- },
166
- {
167
- description: "Add custom fields",
168
- args: {
169
- filepath: "projects/project-x.md",
170
- properties: {
171
- custom: {
172
- priority: "high",
173
- reviewedBy: ["Alice", "Bob"],
174
- dueDate: "2025-03-01"
175
- }
176
- }
177
- }
178
- }
179
- ],
180
- inputSchema: {
181
- type: "object",
182
- properties: {
183
- filepath: {
184
- type: "string",
185
- description: "Path to the note file (relative to vault root)",
186
- format: "path"
187
- },
188
- properties: {
189
- type: "object",
190
- description: "Properties to update",
191
- properties: {
192
- title: { type: "string" },
193
- author: { type: "string" },
194
- type: {
195
- type: "array",
196
- items: { type: "string" }
197
- },
198
- tags: {
199
- type: "array",
200
- // Allow tags with or without leading # - manager handles normalization
201
- items: { type: "string" }
202
- },
203
- status: {
204
- type: "array",
205
- items: {
206
- type: "string",
207
- enum: ["draft", "in-progress", "review", "complete"]
208
- }
209
- },
210
- version: { type: "string" },
211
- platform: { type: "string" },
212
- repository: { type: "string", format: "uri" },
213
- dependencies: {
214
- type: "array",
215
- items: { type: "string" }
216
- },
217
- sources: {
218
- type: "array",
219
- items: { type: "string" }
220
- },
221
- urls: {
222
- type: "array",
223
- items: { type: "string", format: "uri" }
224
- },
225
- papers: {
226
- type: "array",
227
- items: { type: "string" }
228
- },
229
- custom: {
230
- type: "object",
231
- additionalProperties: true
232
- }
233
- },
234
- additionalProperties: false
235
- },
236
- replace: {
237
- type: "boolean",
238
- description: "If true, array properties (like tags, status) will be completely replaced with the provided values instead of being merged with existing values. Defaults to false (merge)."
239
- }
240
- },
241
- required: ["filepath", "properties"]
242
- }
243
- };
244
- }
245
-
246
- async runTool(args: UpdatePropertiesArgs): Promise<Array<TextContent>> {
247
- logger.startTimer(`update_properties_${args.filepath}`);
248
-
249
- try {
250
- logger.debug(`Updating properties for: ${args.filepath}`, {
251
- filepath: args.filepath,
252
- propertyCount: Object.keys(args.properties || {}).length,
253
- replace: args.replace || false
254
- });
255
-
256
- const result = await this.propertyManager.updateProperties(
257
- args.filepath,
258
- args.properties,
259
- args.replace
260
- );
261
-
262
- const elapsedMs = logger.endTimer(`update_properties_${args.filepath}`);
263
- logger.logOperationResult(true, 'update_properties', elapsedMs, {
264
- filepath: args.filepath,
265
- propertyCount: Object.keys(args.properties || {}).length,
266
- success: result.success
267
- });
268
-
269
- return this.createResponse(result);
270
- } catch (error) {
271
- const elapsedMs = logger.endTimer(`update_properties_${args.filepath}`);
272
- logger.logOperationResult(false, 'update_properties', elapsedMs, {
273
- filepath: args.filepath
274
- });
275
-
276
- logger.error(`Error updating properties for ${args.filepath}:`, errorToObject(error));
277
- return this.handleError(error);
278
- }
279
- }
280
- }