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,105 +0,0 @@
1
- /**
2
- * Error handling for Obsidian client
3
- */
4
- import { AxiosError } from "axios";
5
- import type { ApiError } from '../utils/errors.js';
6
- import { ObsidianError, getErrorCodeFromStatus } from '../utils/errors.js';
7
-
8
- /**
9
- * Helper function to create a descriptive error message for SSL certificate issues
10
- */
11
- export function createSSLErrorMessage(error: Error, config: { verifySSL: boolean }): string {
12
- return (
13
- `SSL certificate verification failed. You have two options:\n\n` +
14
- `Option 1 - Enable HTTP (not recommended for production):\n` +
15
- `1. Go to Obsidian Settings > Local REST API\n` +
16
- `2. Enable "Enable Non-encrypted (HTTP) Server"\n` +
17
- `3. Update your client config to use "http" protocol\n\n` +
18
- `Option 2 - Configure HTTPS (recommended):\n` +
19
- `1. Go to Obsidian Settings > Local REST API\n` +
20
- `2. Under 'How to Access', copy the certificate\n` +
21
- `3. Add the certificate to your system's trusted certificates:\n` +
22
- ` - On macOS: Add to Keychain Access\n` +
23
- ` - On Windows: Add to Certificate Manager\n` +
24
- ` - On Linux: Add to ca-certificates\n` +
25
- ` For development only: Set verifySSL: false in client config\n\n` +
26
- `Original error: ${error.message}`
27
- );
28
- }
29
-
30
- /**
31
- * Helper function to create a descriptive error message for connection refused issues
32
- */
33
- export function createConnectionRefusedMessage(host: string, port: number): string {
34
- return (
35
- `Connection refused. To fix this:\n` +
36
- `1. Ensure Obsidian is running\n` +
37
- `2. Verify the 'Local REST API' plugin is enabled in Obsidian Settings\n` +
38
- `3. Check that you're using the correct host (${host}) and port (${port})\n` +
39
- `4. Make sure HTTPS is enabled in the plugin settings`
40
- );
41
- }
42
-
43
- /**
44
- * Helper function to create a descriptive error message for authentication failures
45
- */
46
- export function createAuthFailedMessage(): string {
47
- return (
48
- `Authentication failed. To fix this:\n` +
49
- `1. Go to Obsidian Settings > Local REST API\n` +
50
- `2. Copy your API key from the settings\n` +
51
- `3. Update your configuration with the new API key\n` +
52
- `Note: The API key changes when you regenerate certificates`
53
- );
54
- }
55
-
56
- /**
57
- * Helper function to create a descriptive error message for missing API key
58
- */
59
- export function createMissingAPIKeyMessage(): string {
60
- return (
61
- `Missing API key. To fix this:\n` +
62
- `1. Install the 'Local REST API' plugin in Obsidian\n` +
63
- `2. Enable the plugin in Obsidian Settings\n` +
64
- `3. Copy your API key from Obsidian Settings > Local REST API\n` +
65
- `4. Provide the API key in your configuration`
66
- );
67
- }
68
-
69
- /**
70
- * Helper function to handle Axios errors consistently
71
- */
72
- export function handleAxiosError(error: AxiosError<ApiError>, host: string, port: number): ObsidianError {
73
- const response = error.response;
74
- const errorData = response?.data;
75
-
76
- // Handle common connection errors with helpful messages
77
- if (error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' || error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
78
- return new ObsidianError(
79
- createSSLErrorMessage(error, { verifySSL: true }),
80
- 50001, // SSL error code
81
- { code: error.code }
82
- );
83
- }
84
-
85
- if (error.code === 'ECONNREFUSED') {
86
- return new ObsidianError(
87
- createConnectionRefusedMessage(host, port),
88
- 50002, // Connection refused
89
- { code: error.code }
90
- );
91
- }
92
-
93
- if (response?.status === 401) {
94
- return new ObsidianError(
95
- createAuthFailedMessage(),
96
- 40100, // Unauthorized
97
- { code: error.code }
98
- );
99
- }
100
-
101
- // For other errors, use API error code if available
102
- const errorCode = errorData?.errorCode ?? getErrorCodeFromStatus(response?.status ?? 500);
103
- const message = errorData?.message ?? error.message ?? "Unknown error";
104
- return new ObsidianError(message, errorCode, errorData);
105
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Exports for the Obsidian module
3
- */
4
- export * from './client.js';
5
- export * from './errors.js';
6
- export * from './types.js';
7
-
@@ -1,124 +0,0 @@
1
- /**
2
- * Type definitions for Obsidian client
3
- */
4
-
5
- /**
6
- * Configuration for the Obsidian client
7
- */
8
- export interface ObsidianConfig {
9
- apiKey: string;
10
- verifySSL?: boolean;
11
- timeout?: number;
12
- maxContentLength?: number;
13
- maxBodyLength?: number;
14
- }
15
-
16
- /**
17
- * Server configuration for Obsidian
18
- */
19
- export interface ObsidianServerConfig {
20
- protocol: "http" | "https";
21
- host: string;
22
- port: number;
23
- }
24
-
25
- /**
26
- * Default configuration for Obsidian server
27
- */
28
- export const DEFAULT_OBSIDIAN_CONFIG: ObsidianServerConfig = {
29
- protocol: "https", // HTTPS required by default in Obsidian REST API plugin
30
- host: "127.0.0.1",
31
- port: 27124
32
- } as const;
33
-
34
- /**
35
- * JSON representation of an Obsidian note
36
- */
37
- export interface NoteJson {
38
- content: string;
39
- frontmatter: Record<string, unknown>;
40
- path: string;
41
- stat: {
42
- ctime: number;
43
- mtime: number;
44
- size: number;
45
- };
46
- tags: string[];
47
- }
48
-
49
- /**
50
- * Obsidian file/folder information
51
- */
52
- export interface ObsidianFile {
53
- path: string;
54
- type: "file" | "folder";
55
- children?: ObsidianFile[];
56
- }
57
-
58
- /**
59
- * Obsidian command
60
- */
61
- export interface ObsidianCommand {
62
- id: string;
63
- name: string;
64
- }
65
-
66
- /**
67
- * Obsidian server status
68
- */
69
- export interface ObsidianStatus {
70
- authenticated: boolean;
71
- ok: string;
72
- service: string;
73
- versions: {
74
- obsidian: string;
75
- self: string;
76
- };
77
- }
78
-
79
- /**
80
- * Period type for periodic notes
81
- */
82
- export interface PeriodType {
83
- type: "daily" | "weekly" | "monthly" | "quarterly" | "yearly";
84
- }
85
-
86
- /**
87
- * Search match with context
88
- */
89
- export interface SearchMatch {
90
- context: string;
91
- match: {
92
- start: number;
93
- end: number;
94
- };
95
- }
96
-
97
- /**
98
- * Standard search result
99
- */
100
- export interface SearchResult {
101
- filename: string;
102
- result: unknown;
103
- }
104
-
105
- /**
106
- * Simple search result with context
107
- */
108
- export interface SimpleSearchResult {
109
- filename: string;
110
- score: number;
111
- matches: SearchMatch[];
112
- }
113
-
114
- /**
115
- * Union type for search responses
116
- */
117
- export type SearchResponse = SearchResult | SimpleSearchResult;
118
-
119
- /**
120
- * JSON Logic query for complex searches
121
- */
122
- export interface JsonLogicQuery {
123
- [operator: string]: unknown;
124
- }
@@ -1,17 +0,0 @@
1
- /**
2
- * Resources module exports
3
- */
4
- export * from './tags.js';
5
- export * from './types.js';
6
-
7
- import { ObsidianClient } from '../obsidian/client.js';
8
- import { TagResource } from './tags.js';
9
-
10
- /**
11
- * Create and return the tag resource
12
- * @param client The ObsidianClient instance
13
- * @returns The tag resource
14
- */
15
- export function createTagResource(client: ObsidianClient): TagResource {
16
- return new TagResource(client);
17
- }
@@ -1,283 +0,0 @@
1
- /**
2
- * Tag resource implementation
3
- */
4
- import { Resource, TextContent } from "@modelcontextprotocol/sdk/types.js";
5
- import pLimit from 'p-limit'; // Import p-limit
6
- import { sep } from "path";
7
- import { ObsidianClient } from "../obsidian/client.js";
8
- import { JsonLogicQuery } from "../obsidian/types.js";
9
- import { PropertyManager } from "../tools/properties/manager.js";
10
- import { createLogger, ErrorCategoryType } from "../utils/logging.js";
11
- import { TagResponse } from "./types.js";
12
-
13
- // Create a logger for tag resources
14
- const logger = createLogger('TagResource');
15
-
16
- /**
17
- * Helper function to safely convert any error to an object
18
- */
19
- function errorToObject(error: unknown): Record<string, unknown> {
20
- if (error instanceof Error) {
21
- return {
22
- name: error.name,
23
- message: error.message,
24
- stack: error.stack,
25
- errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
26
- };
27
- } else if (typeof error === 'object' && error !== null) {
28
- return error as Record<string, unknown>;
29
- } else {
30
- return {
31
- error: String(error),
32
- errorCategory: ErrorCategoryType.CATEGORY_UNKNOWN
33
- };
34
- }
35
- }
36
-
37
- /**
38
- * Resource for providing tags used in the Obsidian vault
39
- */
40
- export class TagResource {
41
- private tagCache: Map<string, Set<string>> = new Map();
42
- private propertyManager: PropertyManager;
43
- private isInitialized = false;
44
- private isUpdating = false; // Flag to prevent concurrent updates
45
- private lastUpdate = 0;
46
- private updateInterval = 5000; // 5 seconds
47
-
48
- constructor(private client: ObsidianClient) {
49
- this.propertyManager = new PropertyManager(client);
50
- this.initializeCache();
51
- }
52
-
53
- /**
54
- * Get resource description for the MCP server
55
- */
56
- getResourceDescription(): Resource {
57
- return {
58
- uri: "obsidian://tags",
59
- name: "Obsidian Tags",
60
- description: "List of all tags used across the Obsidian vault with their usage counts",
61
- mimeType: "application/json"
62
- };
63
- }
64
-
65
- /**
66
- * Initialize the tag cache
67
- */
68
- private async initializeCache() {
69
- logger.startTimer('init_tag_cache');
70
-
71
- try {
72
- logger.info('Initializing tag cache');
73
-
74
- // Get all markdown files using platform-agnostic path pattern
75
- const query: JsonLogicQuery = {
76
- "glob": [`**${sep}*.md`.replace(/\\/g, '/'), { "var": "path" }]
77
- };
78
-
79
- const results = await this.client.searchJson(query);
80
- this.tagCache.clear();
81
-
82
- // Create a limiter with a concurrency of 3 (reduced from 10)
83
- const limit = pLimit(3);
84
-
85
- // Create promises for processing each file with concurrency limiting
86
- const processingPromises = results
87
- .filter(result => 'filename' in result) // Ensure filename exists
88
- .map((result) => limit(async () => { // Wrap the async function with the limiter
89
- const filename = result.filename;
90
- try {
91
- // This call is now rate-limited
92
- const content = await this.client.getFileContents(filename);
93
- // Only extract tags from frontmatter YAML
94
- const parseResult = this.propertyManager.parseProperties(content);
95
- // Handle potential parsing errors before accessing properties
96
- if (parseResult.error) {
97
- logger.warn(`Skipping tags for ${filename} due to parsing error: ${parseResult.error.message}`);
98
- return { filename, tags: [] }; // Return empty tags on error
99
- }
100
- return { filename, tags: parseResult.properties.tags || [] };
101
- } catch (error) {
102
- logger.error(`Failed to process file ${filename}:`, errorToObject(error));
103
- return { filename, error: true }; // Mark as failed
104
- }
105
- })); // Close the limiter wrapper
106
-
107
- // Execute promises in parallel and wait for all to settle
108
- const processedResults = await Promise.allSettled(processingPromises);
109
-
110
- // Populate the cache from settled results
111
- processedResults.forEach(settledResult => {
112
- // Check if the promise was fulfilled and didn't encounter a processing error
113
- if (settledResult.status === 'fulfilled' && !settledResult.value.error) {
114
- const { filename, tags } = settledResult.value;
115
- // Ensure tags is an array before iterating
116
- if (tags && Array.isArray(tags)) {
117
- tags.forEach((tag: string) => {
118
- this.addTag(tag, filename);
119
- });
120
- }
121
- } else if (settledResult.status === 'rejected') {
122
- // Log unexpected rejections from the async map function itself
123
- logger.error(`Unexpected error during file processing setup:`, errorToObject(settledResult.reason));
124
- }
125
- // Errors during getFileContents/parseProperties are already logged within the map function
126
- });
127
-
128
- this.isInitialized = true;
129
- this.lastUpdate = Date.now();
130
-
131
- const elapsedMs = logger.endTimer('init_tag_cache');
132
- logger.logOperationResult(true, 'initialize_tag_cache', elapsedMs, {
133
- tagCount: this.tagCache.size,
134
- fileCount: results.length
135
- });
136
-
137
- logger.info(`Tag cache initialized with ${this.tagCache.size} unique tags`);
138
- } catch (error) {
139
- const elapsedMs = logger.endTimer('init_tag_cache');
140
- logger.logOperationResult(false, 'initialize_tag_cache', elapsedMs);
141
- logger.error("Failed to initialize tag cache:", errorToObject(error));
142
- throw error;
143
- }
144
- }
145
-
146
- /**
147
- * Add a tag to the cache
148
- */
149
- private addTag(tag: string, filepath: string) {
150
- if (!this.tagCache.has(tag)) {
151
- this.tagCache.set(tag, new Set());
152
- }
153
- this.tagCache.get(tag)!.add(filepath);
154
- }
155
-
156
- /**
157
- * Update the cache if needed, preventing race conditions.
158
- */
159
- private async updateCacheIfNeeded() {
160
- const now = Date.now();
161
- // Check if cache is fresh enough
162
- if (now - this.lastUpdate <= this.updateInterval) {
163
- return; // Cache is up-to-date
164
- }
165
-
166
- // Check if an update is already in progress
167
- if (this.isUpdating) {
168
- logger.debug('Cache update already in progress, skipping redundant update.');
169
- // Optionally, wait for the ongoing update instead of returning immediately
170
- // For now, we return to avoid complexity, potentially serving slightly stale data.
171
- return;
172
- }
173
-
174
- // Acquire the update lock
175
- this.isUpdating = true;
176
- logger.debug('Acquired cache update lock.');
177
-
178
- try {
179
- // Double-check the update condition after acquiring the lock
180
- // to handle cases where another process finished updating
181
- // while this one was waiting for the lock (though less likely with a simple flag).
182
- const nowAfterLock = Date.now();
183
- if (nowAfterLock - this.lastUpdate > this.updateInterval) {
184
- logger.info('Tag cache needs update, refreshing...');
185
- await this.initializeCache(); // This method updates this.lastUpdate internally
186
- } else {
187
- logger.debug('Cache was updated by another process while waiting for lock.');
188
- }
189
- } catch (error) {
190
- // Log error during update, but don't necessarily block other operations
191
- logger.error('Error during cache update:', errorToObject(error));
192
- // Decide if the error should be re-thrown or handled gracefully
193
- // For now, we log and continue, allowing the lock to be released.
194
- } finally {
195
- // Release the update lock
196
- this.isUpdating = false;
197
- logger.debug('Released cache update lock.');
198
- }
199
- }
200
-
201
- /**
202
- * Get the content for the resource, optionally filtering by path.
203
- * Note: The path filtering logic assumes the tool handler passes the path correctly.
204
- */
205
- // TODO: Verify how the path argument is passed from the tool handler
206
- async getContent(filterPath?: string): Promise<TextContent[]> {
207
- logger.startTimer('get_tags_content');
208
-
209
- try {
210
- if (!this.isInitialized) {
211
- logger.info('Tag cache not initialized, initializing now');
212
- await this.initializeCache();
213
- } else {
214
- await this.updateCacheIfNeeded();
215
- }
216
-
217
- let filteredTagEntries: { name: string; files: Set<string> }[] = [];
218
-
219
- // Filter tags based on the provided path
220
- if (filterPath) {
221
- // Normalize path to ensure it ends with a separator for accurate startsWith check
222
- const normalizedFilterPath = filterPath.endsWith(sep) ? filterPath : filterPath + sep;
223
- logger.debug(`Filtering tags for path: ${normalizedFilterPath}`);
224
-
225
- filteredTagEntries = Array.from(this.tagCache.entries())
226
- .map(([name, files]) => {
227
- // Filter files within each tag entry
228
- const relevantFiles = Array.from(files).filter(file => file.startsWith(normalizedFilterPath));
229
- return { name, files: new Set(relevantFiles) };
230
- })
231
- .filter(tagEntry => tagEntry.files.size > 0); // Keep only tags that exist in the filtered path
232
-
233
- logger.debug(`Found ${filteredTagEntries.length} tags after filtering.`);
234
- } else {
235
- // If no path provided, use all tags from the cache
236
- filteredTagEntries = Array.from(this.tagCache.entries()).map(([name, files]) => ({ name, files }));
237
- }
238
-
239
- const response: TagResponse = {
240
- tags: filteredTagEntries
241
- .map(({ name, files }) => ({ // Map from the potentially filtered structure
242
- name,
243
- count: files.size,
244
- files: Array.from(files).sort()
245
- }))
246
- .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)), // Sort filtered results
247
- metadata: {
248
- // Note: Metadata currently reflects the entire vault cache, even when filtered.
249
- // Adjust calculation here if path-specific metadata is desired.
250
- totalOccurrences: Array.from(this.tagCache.values())
251
- .reduce((sum, files) => sum + files.size, 0),
252
- uniqueTags: this.tagCache.size,
253
- scannedFiles: new Set(
254
- Array.from(this.tagCache.values())
255
- .flatMap(files => Array.from(files))
256
- ).size,
257
- lastUpdate: this.lastUpdate
258
- }
259
- };
260
-
261
- logger.debug(`Returning tag resource with ${response.tags.length} tags`);
262
-
263
- const elapsedMs = logger.endTimer('get_tags_content');
264
- logger.logOperationResult(true, 'get_tags', elapsedMs, {
265
- tagCount: response.tags.length,
266
- totalOccurrences: response.metadata.totalOccurrences,
267
- uniqueTags: response.metadata.uniqueTags,
268
- scannedFiles: response.metadata.scannedFiles
269
- });
270
-
271
- return [{
272
- type: "text",
273
- text: JSON.stringify(response, null, 2),
274
- uri: this.getResourceDescription().uri
275
- }];
276
- } catch (error) {
277
- const elapsedMs = logger.endTimer('get_tags_content');
278
- logger.logOperationResult(false, 'get_tags', elapsedMs);
279
- logger.error("Failed to get tags:", errorToObject(error));
280
- throw error;
281
- }
282
- }
283
- }
@@ -1,30 +0,0 @@
1
- /**
2
- * Resource types for the MCP server
3
- */
4
-
5
- /**
6
- * Tag information structure
7
- */
8
- export interface TagInfo {
9
- name: string;
10
- count: number;
11
- files: string[];
12
- }
13
-
14
- /**
15
- * Tag metadata structure
16
- */
17
- export interface TagMetadata {
18
- totalOccurrences: number;
19
- uniqueTags: number;
20
- scannedFiles: number;
21
- lastUpdate: number; // Timestamp of last cache update
22
- }
23
-
24
- /**
25
- * Tag resource response structure
26
- */
27
- export interface TagResponse {
28
- tags: TagInfo[];
29
- metadata: TagMetadata;
30
- }
package/src/tools/base.ts DELETED
@@ -1,123 +0,0 @@
1
- /**
2
- * Base class for all tool handlers
3
- */
4
- import { TextContent, Tool } from "@modelcontextprotocol/sdk/types.js";
5
- import { McpErrorCode } from "../mcp/types.js";
6
- import { ObsidianClient } from "../obsidian/client.js";
7
- import { ObsidianError } from "../utils/errors.js";
8
- import { createLogger, ErrorCategoryType } from "../utils/logging.js";
9
- import { tokenCounter } from "../utils/tokenization.js";
10
-
11
- // Create a logger for tool operations
12
- const logger = createLogger('Tools');
13
-
14
- /**
15
- * Interface that all tool handlers must implement
16
- */
17
- export interface ToolHandler<T = any> {
18
- name: string;
19
- getToolDescription(): Tool;
20
- runTool(args: T): Promise<Array<TextContent>>;
21
- }
22
-
23
- /**
24
- * Base class for all tool handlers with common functionality
25
- */
26
- export abstract class BaseToolHandler<T = Record<string, unknown>> implements ToolHandler<T> {
27
- /**
28
- * Create a new tool handler
29
- * @param name The name of the tool
30
- * @param client The ObsidianClient instance
31
- */
32
- constructor(
33
- public readonly name: string,
34
- protected client: ObsidianClient
35
- ) {}
36
-
37
- /**
38
- * Get the tool description for MCP server
39
- */
40
- abstract getToolDescription(): Tool;
41
-
42
- /**
43
- * Run the tool with the provided arguments
44
- */
45
- abstract runTool(args: T): Promise<Array<TextContent>>;
46
-
47
- /**
48
- * Create a standardized response from any content type
49
- * @param content The content to format for response
50
- * @returns An array of TextContent objects
51
- */
52
- protected createResponse(content: unknown): TextContent[] {
53
- let text: string;
54
-
55
- // Handle different content types
56
- if (typeof content === 'string') {
57
- text = content;
58
- } else if (content instanceof Buffer) {
59
- text = content.toString('utf-8');
60
- } else if (Array.isArray(content) && content.every(item => typeof item === 'string')) {
61
- text = content.join('\n');
62
- } else if (content instanceof Error) {
63
- text = `Error: ${content.message}\n${content.stack || ''}`;
64
- } else {
65
- try {
66
- text = JSON.stringify(content, null, 2);
67
- } catch (error) {
68
- text = String(content);
69
- }
70
- }
71
-
72
- // Count tokens and truncate if necessary
73
- const originalTokenCount = tokenCounter.countTokens(text);
74
- const truncatedText = tokenCounter.truncateToTokenLimit(text);
75
- const finalTokenCount = tokenCounter.countTokens(truncatedText);
76
-
77
- if (originalTokenCount > finalTokenCount) {
78
- logger.debug(`[${this.name}] Response truncated`, {
79
- toolName: this.name,
80
- originalTokens: originalTokenCount,
81
- truncatedTokens: finalTokenCount,
82
- truncationPercent: Math.round((1 - finalTokenCount / originalTokenCount) * 100)
83
- });
84
- }
85
-
86
- return [{
87
- type: "text",
88
- text: truncatedText
89
- }];
90
- }
91
-
92
- /**
93
- * Standard error handling for tool execution
94
- * @param error The error to handle
95
- * @throws ObsidianError
96
- */
97
- protected handleError(error: unknown): never {
98
- if (error instanceof ObsidianError) {
99
- throw error;
100
- }
101
-
102
- logger.error(`Tool '${this.name}' execution failed`, {
103
- toolName: this.name,
104
- errorMessage: error instanceof Error ? error.message : String(error),
105
- errorStack: error instanceof Error ? error.stack : undefined,
106
- errorCategory: ErrorCategoryType.CATEGORY_SYSTEM
107
- });
108
-
109
- if (error instanceof Error) {
110
- throw new ObsidianError(
111
- `Tool '${this.name}' execution failed: ${error.message}`,
112
- McpErrorCode.INTERNAL_SERVER_ERROR,
113
- { originalError: error.stack }
114
- );
115
- }
116
-
117
- throw new ObsidianError(
118
- `Tool '${this.name}' execution failed with unknown error`,
119
- McpErrorCode.INTERNAL_SERVER_ERROR,
120
- { error: String(error) }
121
- );
122
- }
123
- }