obsidian-mcp-server 2.0.7 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +364 -0
- package/Dockerfile +99 -0
- package/LICENSE +4 -6
- package/README.md +246 -206
- package/changelog/3.0.x/3.0.0.md +102 -0
- package/changelog/3.1.x/3.1.0.md +26 -0
- package/changelog/template.md +51 -0
- package/dist/config/server-config.d.ts +19 -0
- package/dist/config/server-config.d.ts.map +1 -0
- package/dist/config/server-config.js +55 -0
- package/dist/config/server-config.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -295
- package/dist/index.js.map +1 -0
- package/dist/mcp-server/prompts/definitions/index.d.ts +8 -0
- package/dist/mcp-server/prompts/definitions/index.d.ts.map +1 -0
- package/dist/mcp-server/prompts/definitions/index.js +8 -0
- package/dist/mcp-server/prompts/definitions/index.js.map +1 -0
- package/dist/mcp-server/resources/definitions/index.d.ts +36 -0
- package/dist/mcp-server/resources/definitions/index.d.ts.map +1 -0
- package/dist/mcp-server/resources/definitions/index.js +9 -0
- package/dist/mcp-server/resources/definitions/index.js.map +1 -0
- package/dist/mcp-server/resources/definitions/obsidian-status.resource.d.ts +23 -0
- package/dist/mcp-server/resources/definitions/obsidian-status.resource.d.ts.map +1 -0
- package/dist/mcp-server/resources/definitions/obsidian-status.resource.js +47 -0
- package/dist/mcp-server/resources/definitions/obsidian-status.resource.js.map +1 -0
- package/dist/mcp-server/resources/definitions/obsidian-tags.resource.d.ts +13 -0
- package/dist/mcp-server/resources/definitions/obsidian-tags.resource.d.ts.map +1 -0
- package/dist/mcp-server/resources/definitions/obsidian-tags.resource.js +30 -0
- package/dist/mcp-server/resources/definitions/obsidian-tags.resource.js.map +1 -0
- package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.d.ts +21 -0
- package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.d.ts.map +1 -0
- package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.js +38 -0
- package/dist/mcp-server/resources/definitions/obsidian-vault-note.resource.js.map +1 -0
- package/dist/mcp-server/tools/definitions/_shared/schemas.d.ts +45 -0
- package/dist/mcp-server/tools/definitions/_shared/schemas.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/_shared/schemas.js +66 -0
- package/dist/mcp-server/tools/definitions/_shared/schemas.js.map +1 -0
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts +51 -0
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js +120 -0
- package/dist/mcp-server/tools/definitions/_shared/suggest-paths.js.map +1 -0
- package/dist/mcp-server/tools/definitions/index.d.ts +582 -0
- package/dist/mcp-server/tools/definitions/index.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/index.js +40 -0
- package/dist/mcp-server/tools/definitions/index.js.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts +42 -0
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js +58 -0
- package/dist/mcp-server/tools/definitions/obsidian-append-to-note.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts +46 -0
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js +66 -0
- package/dist/mcp-server/tools/definitions/obsidian-delete-note.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.d.ts +19 -0
- package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.js +43 -0
- package/dist/mcp-server/tools/definitions/obsidian-execute-command.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts +92 -0
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js +245 -0
- package/dist/mcp-server/tools/definitions/obsidian-get-note.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.d.ts +13 -0
- package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.js +38 -0
- package/dist/mcp-server/tools/definitions/obsidian-list-commands.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts +59 -0
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js +273 -0
- package/dist/mcp-server/tools/definitions/obsidian-list-notes.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.d.ts +13 -0
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js +38 -0
- package/dist/mcp-server/tools/definitions/obsidian-list-tags.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts +68 -0
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js +178 -0
- package/dist/mcp-server/tools/definitions/obsidian-manage-frontmatter.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts +77 -0
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js +172 -0
- package/dist/mcp-server/tools/definitions/obsidian-manage-tags.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts +22 -0
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js +88 -0
- package/dist/mcp-server/tools/definitions/obsidian-open-in-ui.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts +81 -0
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js +83 -0
- package/dist/mcp-server/tools/definitions/obsidian-patch-note.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts +59 -0
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js +167 -0
- package/dist/mcp-server/tools/definitions/obsidian-replace-in-note.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts +76 -0
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js +244 -0
- package/dist/mcp-server/tools/definitions/obsidian-search-notes.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts +48 -0
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js +100 -0
- package/dist/mcp-server/tools/definitions/obsidian-write-note.tool.js.map +1 -0
- package/dist/services/obsidian/frontmatter-ops.d.ts +34 -0
- package/dist/services/obsidian/frontmatter-ops.d.ts.map +1 -0
- package/dist/services/obsidian/frontmatter-ops.js +230 -0
- package/dist/services/obsidian/frontmatter-ops.js.map +1 -0
- package/dist/services/obsidian/obsidian-service.d.ts +83 -0
- package/dist/services/obsidian/obsidian-service.d.ts.map +1 -0
- package/dist/services/obsidian/obsidian-service.js +435 -0
- package/dist/services/obsidian/obsidian-service.js.map +1 -0
- package/dist/services/obsidian/section-extractor.d.ts +13 -0
- package/dist/services/obsidian/section-extractor.d.ts.map +1 -0
- package/dist/services/obsidian/section-extractor.js +124 -0
- package/dist/services/obsidian/section-extractor.js.map +1 -0
- package/dist/services/obsidian/types.d.ts +91 -0
- package/dist/services/obsidian/types.d.ts.map +1 -0
- package/dist/services/obsidian/types.js +7 -0
- package/dist/services/obsidian/types.js.map +1 -0
- package/package.json +63 -69
- package/server.json +167 -0
- package/CHANGELOG.md +0 -124
- package/dist/config/index.d.ts +0 -41
- package/dist/config/index.js +0 -191
- package/dist/mcp-server/server.d.ts +0 -33
- package/dist/mcp-server/server.js +0 -211
- package/dist/mcp-server/tools/obsidianDeleteNoteTool/index.d.ts +0 -12
- package/dist/mcp-server/tools/obsidianDeleteNoteTool/index.js +0 -12
- package/dist/mcp-server/tools/obsidianDeleteNoteTool/logic.d.ts +0 -51
- package/dist/mcp-server/tools/obsidianDeleteNoteTool/logic.js +0 -168
- package/dist/mcp-server/tools/obsidianDeleteNoteTool/registration.d.ts +0 -19
- package/dist/mcp-server/tools/obsidianDeleteNoteTool/registration.js +0 -91
- package/dist/mcp-server/tools/obsidianGlobalSearchTool/index.d.ts +0 -12
- package/dist/mcp-server/tools/obsidianGlobalSearchTool/index.js +0 -12
- package/dist/mcp-server/tools/obsidianGlobalSearchTool/logic.d.ts +0 -77
- package/dist/mcp-server/tools/obsidianGlobalSearchTool/logic.js +0 -341
- package/dist/mcp-server/tools/obsidianGlobalSearchTool/registration.d.ts +0 -18
- package/dist/mcp-server/tools/obsidianGlobalSearchTool/registration.js +0 -69
- package/dist/mcp-server/tools/obsidianListNotesTool/index.d.ts +0 -12
- package/dist/mcp-server/tools/obsidianListNotesTool/index.js +0 -12
- package/dist/mcp-server/tools/obsidianListNotesTool/logic.d.ts +0 -68
- package/dist/mcp-server/tools/obsidianListNotesTool/logic.js +0 -215
- package/dist/mcp-server/tools/obsidianListNotesTool/registration.d.ts +0 -23
- package/dist/mcp-server/tools/obsidianListNotesTool/registration.js +0 -98
- package/dist/mcp-server/tools/obsidianManageFrontmatterTool/index.d.ts +0 -3
- package/dist/mcp-server/tools/obsidianManageFrontmatterTool/index.js +0 -2
- package/dist/mcp-server/tools/obsidianManageFrontmatterTool/logic.d.ts +0 -42
- package/dist/mcp-server/tools/obsidianManageFrontmatterTool/logic.js +0 -152
- package/dist/mcp-server/tools/obsidianManageFrontmatterTool/registration.d.ts +0 -3
- package/dist/mcp-server/tools/obsidianManageFrontmatterTool/registration.js +0 -52
- package/dist/mcp-server/tools/obsidianManageTagsTool/index.d.ts +0 -3
- package/dist/mcp-server/tools/obsidianManageTagsTool/index.js +0 -2
- package/dist/mcp-server/tools/obsidianManageTagsTool/logic.d.ts +0 -28
- package/dist/mcp-server/tools/obsidianManageTagsTool/logic.js +0 -161
- package/dist/mcp-server/tools/obsidianManageTagsTool/registration.d.ts +0 -3
- package/dist/mcp-server/tools/obsidianManageTagsTool/registration.js +0 -52
- package/dist/mcp-server/tools/obsidianReadNoteTool/index.d.ts +0 -12
- package/dist/mcp-server/tools/obsidianReadNoteTool/index.js +0 -12
- package/dist/mcp-server/tools/obsidianReadNoteTool/logic.d.ts +0 -87
- package/dist/mcp-server/tools/obsidianReadNoteTool/logic.js +0 -216
- package/dist/mcp-server/tools/obsidianReadNoteTool/registration.d.ts +0 -20
- package/dist/mcp-server/tools/obsidianReadNoteTool/registration.js +0 -101
- package/dist/mcp-server/tools/obsidianSearchReplaceTool/index.d.ts +0 -12
- package/dist/mcp-server/tools/obsidianSearchReplaceTool/index.js +0 -12
- package/dist/mcp-server/tools/obsidianSearchReplaceTool/logic.d.ts +0 -255
- package/dist/mcp-server/tools/obsidianSearchReplaceTool/logic.js +0 -583
- package/dist/mcp-server/tools/obsidianSearchReplaceTool/registration.d.ts +0 -22
- package/dist/mcp-server/tools/obsidianSearchReplaceTool/registration.js +0 -111
- package/dist/mcp-server/tools/obsidianUpdateNoteTool/index.d.ts +0 -12
- package/dist/mcp-server/tools/obsidianUpdateNoteTool/index.js +0 -12
- package/dist/mcp-server/tools/obsidianUpdateNoteTool/logic.d.ts +0 -183
- package/dist/mcp-server/tools/obsidianUpdateNoteTool/logic.js +0 -490
- package/dist/mcp-server/tools/obsidianUpdateNoteTool/registration.d.ts +0 -21
- package/dist/mcp-server/tools/obsidianUpdateNoteTool/registration.js +0 -108
- package/dist/mcp-server/transports/auth/core/authContext.d.ts +0 -33
- package/dist/mcp-server/transports/auth/core/authContext.js +0 -24
- package/dist/mcp-server/transports/auth/core/authTypes.d.ts +0 -17
- package/dist/mcp-server/transports/auth/core/authTypes.js +0 -5
- package/dist/mcp-server/transports/auth/core/authUtils.d.ts +0 -18
- package/dist/mcp-server/transports/auth/core/authUtils.js +0 -45
- package/dist/mcp-server/transports/auth/index.d.ts +0 -10
- package/dist/mcp-server/transports/auth/index.js +0 -9
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.d.ts +0 -27
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +0 -149
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.d.ts +0 -20
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +0 -124
- package/dist/mcp-server/transports/httpErrorHandler.d.ts +0 -26
- package/dist/mcp-server/transports/httpErrorHandler.js +0 -73
- package/dist/mcp-server/transports/httpTransport.d.ts +0 -21
- package/dist/mcp-server/transports/httpTransport.js +0 -208
- package/dist/mcp-server/transports/stdioTransport.d.ts +0 -42
- package/dist/mcp-server/transports/stdioTransport.js +0 -63
- package/dist/services/obsidianRestAPI/index.d.ts +0 -15
- package/dist/services/obsidianRestAPI/index.js +0 -17
- package/dist/services/obsidianRestAPI/methods/activeFileMethods.d.ts +0 -38
- package/dist/services/obsidianRestAPI/methods/activeFileMethods.js +0 -62
- package/dist/services/obsidianRestAPI/methods/commandMethods.d.ts +0 -22
- package/dist/services/obsidianRestAPI/methods/commandMethods.js +0 -31
- package/dist/services/obsidianRestAPI/methods/openMethods.d.ts +0 -16
- package/dist/services/obsidianRestAPI/methods/openMethods.js +0 -21
- package/dist/services/obsidianRestAPI/methods/patchMethods.d.ts +0 -37
- package/dist/services/obsidianRestAPI/methods/patchMethods.js +0 -94
- package/dist/services/obsidianRestAPI/methods/periodicNoteMethods.d.ts +0 -42
- package/dist/services/obsidianRestAPI/methods/periodicNoteMethods.js +0 -66
- package/dist/services/obsidianRestAPI/methods/searchMethods.d.ts +0 -25
- package/dist/services/obsidianRestAPI/methods/searchMethods.js +0 -36
- package/dist/services/obsidianRestAPI/methods/vaultMethods.d.ts +0 -58
- package/dist/services/obsidianRestAPI/methods/vaultMethods.js +0 -144
- package/dist/services/obsidianRestAPI/service.d.ts +0 -195
- package/dist/services/obsidianRestAPI/service.js +0 -379
- package/dist/services/obsidianRestAPI/types.d.ts +0 -127
- package/dist/services/obsidianRestAPI/types.js +0 -7
- package/dist/services/obsidianRestAPI/vaultCache/index.d.ts +0 -4
- package/dist/services/obsidianRestAPI/vaultCache/index.js +0 -4
- package/dist/services/obsidianRestAPI/vaultCache/service.d.ts +0 -88
- package/dist/services/obsidianRestAPI/vaultCache/service.js +0 -299
- package/dist/types-global/errors.d.ts +0 -73
- package/dist/types-global/errors.js +0 -71
- package/dist/utils/index.d.ts +0 -5
- package/dist/utils/index.js +0 -13
- package/dist/utils/internal/asyncUtils.d.ts +0 -54
- package/dist/utils/internal/asyncUtils.js +0 -101
- package/dist/utils/internal/errorHandler.d.ts +0 -176
- package/dist/utils/internal/errorHandler.js +0 -351
- package/dist/utils/internal/index.d.ts +0 -4
- package/dist/utils/internal/index.js +0 -4
- package/dist/utils/internal/logger.d.ts +0 -141
- package/dist/utils/internal/logger.js +0 -406
- package/dist/utils/internal/requestContext.d.ts +0 -83
- package/dist/utils/internal/requestContext.js +0 -72
- package/dist/utils/metrics/index.d.ts +0 -1
- package/dist/utils/metrics/index.js +0 -1
- package/dist/utils/metrics/tokenCounter.d.ts +0 -27
- package/dist/utils/metrics/tokenCounter.js +0 -128
- package/dist/utils/obsidian/index.d.ts +0 -5
- package/dist/utils/obsidian/index.js +0 -5
- package/dist/utils/obsidian/obsidianApiUtils.d.ts +0 -14
- package/dist/utils/obsidian/obsidianApiUtils.js +0 -29
- package/dist/utils/obsidian/obsidianStatUtils.d.ts +0 -68
- package/dist/utils/obsidian/obsidianStatUtils.js +0 -143
- package/dist/utils/parsing/dateParser.d.ts +0 -56
- package/dist/utils/parsing/dateParser.js +0 -104
- package/dist/utils/parsing/index.d.ts +0 -2
- package/dist/utils/parsing/index.js +0 -3
- package/dist/utils/parsing/jsonParser.d.ts +0 -80
- package/dist/utils/parsing/jsonParser.js +0 -133
- package/dist/utils/security/idGenerator.d.ts +0 -140
- package/dist/utils/security/idGenerator.js +0 -194
- package/dist/utils/security/index.d.ts +0 -3
- package/dist/utils/security/index.js +0 -3
- package/dist/utils/security/rateLimiter.d.ts +0 -156
- package/dist/utils/security/rateLimiter.js +0 -235
- package/dist/utils/security/sanitization.d.ts +0 -244
- package/dist/utils/security/sanitization.js +0 -599
|
@@ -1,490 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
3
|
-
import { createFormattedStatWithTokenCount, logger, retryWithDelay, } from "../../../utils/index.js";
|
|
4
|
-
// ====================================================================================
|
|
5
|
-
// Schema Definitions for Input Validation
|
|
6
|
-
// ====================================================================================
|
|
7
|
-
/** Defines the possible types of targets for the update operation. */
|
|
8
|
-
const TargetTypeSchema = z
|
|
9
|
-
.enum(["filePath", "activeFile", "periodicNote"])
|
|
10
|
-
.describe("Specifies the target note: 'filePath', 'activeFile', or 'periodicNote'.");
|
|
11
|
-
/** Defines the only allowed modification type for this tool implementation. */
|
|
12
|
-
const ModificationTypeSchema = z
|
|
13
|
-
.literal("wholeFile")
|
|
14
|
-
.describe("Determines the modification strategy: must be 'wholeFile' for this tool.");
|
|
15
|
-
/** Defines the specific whole-file operations supported. */
|
|
16
|
-
const WholeFileModeSchema = z
|
|
17
|
-
.enum(["append", "prepend", "overwrite"])
|
|
18
|
-
.describe("Specifies the whole-file operation: 'append', 'prepend', or 'overwrite'.");
|
|
19
|
-
/** Defines the valid periods for periodic notes. */
|
|
20
|
-
const PeriodicNotePeriodSchema = z
|
|
21
|
-
.enum(["daily", "weekly", "monthly", "quarterly", "yearly"])
|
|
22
|
-
.describe("Valid periods for 'periodicNote' target type.");
|
|
23
|
-
/**
|
|
24
|
-
* Base Zod schema containing fields common to all update operations within this tool.
|
|
25
|
-
* Currently, only 'wholeFile' is supported, so this forms the basis for that mode.
|
|
26
|
-
*/
|
|
27
|
-
const BaseUpdateSchema = z.object({
|
|
28
|
-
/** Specifies the type of target note. */
|
|
29
|
-
targetType: TargetTypeSchema,
|
|
30
|
-
/** The content to use for the modification. Must be a string for whole-file operations. */
|
|
31
|
-
content: z
|
|
32
|
-
.string()
|
|
33
|
-
.describe("The content for the modification (must be a string for whole-file operations)."),
|
|
34
|
-
/**
|
|
35
|
-
* Identifier for the target. Required and must be a vault-relative path if targetType is 'filePath'.
|
|
36
|
-
* Required and must be a valid period string (e.g., 'daily') if targetType is 'periodicNote'.
|
|
37
|
-
* Not used if targetType is 'activeFile'.
|
|
38
|
-
*/
|
|
39
|
-
targetIdentifier: z
|
|
40
|
-
.string()
|
|
41
|
-
.optional()
|
|
42
|
-
.describe("Identifier for 'filePath' (vault-relative path) or 'periodicNote' (period string). Not used for 'activeFile'."),
|
|
43
|
-
});
|
|
44
|
-
/**
|
|
45
|
-
* Zod schema specifically for the 'wholeFile' modification type, extending the base schema.
|
|
46
|
-
* Includes mode-specific options like createIfNeeded and overwriteIfExists.
|
|
47
|
-
*/
|
|
48
|
-
const WholeFileUpdateSchema = BaseUpdateSchema.extend({
|
|
49
|
-
/** The modification type, fixed to 'wholeFile'. */
|
|
50
|
-
modificationType: ModificationTypeSchema,
|
|
51
|
-
/** The specific whole-file operation ('append', 'prepend', 'overwrite'). */
|
|
52
|
-
wholeFileMode: WholeFileModeSchema,
|
|
53
|
-
/** If true (default), creates the target file/note if it doesn't exist before applying the modification. If false, the operation fails if the target doesn't exist. */
|
|
54
|
-
createIfNeeded: z
|
|
55
|
-
.boolean()
|
|
56
|
-
.optional()
|
|
57
|
-
.default(true)
|
|
58
|
-
.describe("If true (default), creates the target if it doesn't exist. If false, fails if target is missing."),
|
|
59
|
-
/** Only relevant for 'overwrite' mode. If true, allows overwriting an existing file. If false (default) and the file exists, the 'overwrite' operation fails. */
|
|
60
|
-
overwriteIfExists: z
|
|
61
|
-
.boolean()
|
|
62
|
-
.optional()
|
|
63
|
-
.default(false)
|
|
64
|
-
.describe("For 'overwrite' mode: If true, allows overwriting. If false (default) and file exists, operation fails."),
|
|
65
|
-
/** If true, includes the final content of the modified file in the response. Defaults to false. */
|
|
66
|
-
returnContent: z
|
|
67
|
-
.boolean()
|
|
68
|
-
.optional()
|
|
69
|
-
.default(false)
|
|
70
|
-
.describe("If true, returns the final file content in the response."),
|
|
71
|
-
});
|
|
72
|
-
// ====================================================================================
|
|
73
|
-
// Schema for SDK Registration (Flattened for Tool Definition)
|
|
74
|
-
// ====================================================================================
|
|
75
|
-
/**
|
|
76
|
-
* Zod schema used for registering the tool with the MCP SDK (`server.tool`).
|
|
77
|
-
* This schema defines the expected input structure from the client's perspective.
|
|
78
|
-
* It flattens the structure slightly by making mode-specific fields optional at this stage,
|
|
79
|
-
* relying on the refined schema (`ObsidianUpdateFileInputSchema`) for stricter validation
|
|
80
|
-
* within the handler logic.
|
|
81
|
-
*/
|
|
82
|
-
const ObsidianUpdateNoteRegistrationSchema = z
|
|
83
|
-
.object({
|
|
84
|
-
/** Specifies the target note: 'filePath' (requires targetIdentifier), 'activeFile' (currently open file), or 'periodicNote' (requires targetIdentifier with period like 'daily'). */
|
|
85
|
-
targetType: TargetTypeSchema,
|
|
86
|
-
/** The content for the modification. Must be a string for whole-file operations. */
|
|
87
|
-
content: z
|
|
88
|
-
.string()
|
|
89
|
-
.describe("The content for the modification (must be a string)."),
|
|
90
|
-
/** Identifier for the target when targetType is 'filePath' (vault-relative path, e.g., 'Notes/My File.md') or 'periodicNote' (period string: 'daily', 'weekly', etc.). Not used for 'activeFile'. */
|
|
91
|
-
targetIdentifier: z
|
|
92
|
-
.string()
|
|
93
|
-
.optional()
|
|
94
|
-
.describe("Identifier for 'filePath' (path) or 'periodicNote' (period). Not used for 'activeFile'."),
|
|
95
|
-
/** Determines the modification strategy: must be 'wholeFile'. */
|
|
96
|
-
modificationType: ModificationTypeSchema,
|
|
97
|
-
// --- WholeFile Mode Parameters (Marked optional here, refined schema enforces if modificationType is 'wholeFile') ---
|
|
98
|
-
/** For 'wholeFile' mode: 'append', 'prepend', or 'overwrite'. Required if modificationType is 'wholeFile'. */
|
|
99
|
-
wholeFileMode: WholeFileModeSchema.optional() // Made optional here, refined schema handles requirement
|
|
100
|
-
.describe("For 'wholeFile' mode: 'append', 'prepend', or 'overwrite'. Required if modificationType is 'wholeFile'."),
|
|
101
|
-
/** For 'wholeFile' mode: If true (default), creates the target file/note if it doesn't exist before modifying. If false, fails if the target doesn't exist. */
|
|
102
|
-
createIfNeeded: z
|
|
103
|
-
.boolean()
|
|
104
|
-
.optional()
|
|
105
|
-
.default(true)
|
|
106
|
-
.describe("For 'wholeFile' mode: If true (default), creates target if needed. If false, fails if missing."),
|
|
107
|
-
/** For 'wholeFile' mode with 'overwrite': If false (default), the operation fails if the target file already exists. If true, allows overwriting the existing file. */
|
|
108
|
-
overwriteIfExists: z
|
|
109
|
-
.boolean()
|
|
110
|
-
.optional()
|
|
111
|
-
.default(false)
|
|
112
|
-
.describe("For 'wholeFile'/'overwrite' mode: If false (default), fails if target exists. If true, allows overwrite."),
|
|
113
|
-
/** If true, returns the final content of the file in the response. Defaults to false. */
|
|
114
|
-
returnContent: z
|
|
115
|
-
.boolean()
|
|
116
|
-
.optional()
|
|
117
|
-
.default(false)
|
|
118
|
-
.describe("If true, returns the final file content in the response."),
|
|
119
|
-
})
|
|
120
|
-
.describe("Tool to modify Obsidian notes (specified by file path, active file, or periodic note) using whole-file operations: 'append', 'prepend', or 'overwrite'. Options control creation and overwrite behavior.");
|
|
121
|
-
/**
|
|
122
|
-
* The shape of the registration schema, used by `server.tool` for basic validation.
|
|
123
|
-
* @see ObsidianUpdateFileRegistrationSchema
|
|
124
|
-
*/
|
|
125
|
-
export const ObsidianUpdateNoteInputSchemaShape = ObsidianUpdateNoteRegistrationSchema.shape;
|
|
126
|
-
// ====================================================================================
|
|
127
|
-
// Refined Schema for Internal Logic and Strict Validation
|
|
128
|
-
// ====================================================================================
|
|
129
|
-
/**
|
|
130
|
-
* Refined Zod schema used internally within the tool's logic for strict validation.
|
|
131
|
-
* It builds upon `WholeFileUpdateSchema` and adds cross-field validation rules using `.refine()`.
|
|
132
|
-
* This ensures that `targetIdentifier` is provided and valid when required by `targetType`.
|
|
133
|
-
*/
|
|
134
|
-
export const ObsidianUpdateNoteInputSchema = WholeFileUpdateSchema.refine((data) => {
|
|
135
|
-
// Rule 1: If targetType is 'filePath' or 'periodicNote', targetIdentifier must be provided.
|
|
136
|
-
if ((data.targetType === "filePath" || data.targetType === "periodicNote") &&
|
|
137
|
-
!data.targetIdentifier) {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
// Rule 2: If targetType is 'periodicNote', targetIdentifier must be a valid period string.
|
|
141
|
-
if (data.targetType === "periodicNote" &&
|
|
142
|
-
data.targetIdentifier &&
|
|
143
|
-
!PeriodicNotePeriodSchema.safeParse(data.targetIdentifier).success) {
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
// All checks passed
|
|
147
|
-
return true;
|
|
148
|
-
}, {
|
|
149
|
-
// Custom error message for refinement failure.
|
|
150
|
-
message: "targetIdentifier is required and must be a valid path for targetType 'filePath', or a valid period ('daily', 'weekly', etc.) for targetType 'periodicNote'.",
|
|
151
|
-
path: ["targetIdentifier"], // Associate the error with the targetIdentifier field.
|
|
152
|
-
});
|
|
153
|
-
// ====================================================================================
|
|
154
|
-
// Helper Functions
|
|
155
|
-
// ====================================================================================
|
|
156
|
-
/**
|
|
157
|
-
* Attempts to retrieve the final state (content and stats) of the target note after an update operation.
|
|
158
|
-
* Uses the appropriate Obsidian API method based on the target type.
|
|
159
|
-
* Logs a warning and returns null if fetching the final state fails, to avoid failing the entire update operation.
|
|
160
|
-
*
|
|
161
|
-
* @param {z.infer<typeof TargetTypeSchema>} targetType - The type of the target note.
|
|
162
|
-
* @param {string | undefined} targetIdentifier - The identifier (path or period) if applicable.
|
|
163
|
-
* @param {z.infer<typeof PeriodicNotePeriodSchema> | undefined} period - The parsed period if targetType is 'periodicNote'.
|
|
164
|
-
* @param {ObsidianRestApiService} obsidianService - The Obsidian API service instance.
|
|
165
|
-
* @param {RequestContext} context - The request context for logging and correlation.
|
|
166
|
-
* @returns {Promise<NoteJson | null>} A promise resolving to the NoteJson object or null if retrieval fails.
|
|
167
|
-
*/
|
|
168
|
-
async function getFinalState(targetType, targetIdentifier, period, obsidianService, context) {
|
|
169
|
-
const operation = "getFinalState";
|
|
170
|
-
logger.debug(`Attempting to retrieve final state for target: ${targetType} ${targetIdentifier ?? "(active)"}`, { ...context, operation });
|
|
171
|
-
try {
|
|
172
|
-
let noteJson = null;
|
|
173
|
-
// Call the appropriate API method based on target type
|
|
174
|
-
if (targetType === "filePath" && targetIdentifier) {
|
|
175
|
-
noteJson = (await obsidianService.getFileContent(targetIdentifier, "json", context));
|
|
176
|
-
}
|
|
177
|
-
else if (targetType === "activeFile") {
|
|
178
|
-
noteJson = (await obsidianService.getActiveFile("json", context));
|
|
179
|
-
}
|
|
180
|
-
else if (targetType === "periodicNote" && period) {
|
|
181
|
-
noteJson = (await obsidianService.getPeriodicNote(period, "json", context));
|
|
182
|
-
}
|
|
183
|
-
logger.debug(`Successfully retrieved final state`, {
|
|
184
|
-
...context,
|
|
185
|
-
operation,
|
|
186
|
-
});
|
|
187
|
-
return noteJson;
|
|
188
|
-
}
|
|
189
|
-
catch (error) {
|
|
190
|
-
// Log the error but don't let it fail the main update operation.
|
|
191
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
192
|
-
logger.warning(`Could not retrieve final state after update for target: ${targetType} ${targetIdentifier ?? "(active)"}. Error: ${errorMsg}`, { ...context, operation, error: errorMsg });
|
|
193
|
-
return null; // Return null to indicate failure without throwing
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
// ====================================================================================
|
|
197
|
-
// Core Logic Function
|
|
198
|
-
// ====================================================================================
|
|
199
|
-
/**
|
|
200
|
-
* Processes the core logic for the 'obsidian_update_file' tool when using the 'wholeFile'
|
|
201
|
-
* modification type (append, prepend, overwrite). It handles pre-checks, performs the
|
|
202
|
-
* update via the Obsidian REST API, retrieves the final state, and constructs the response.
|
|
203
|
-
*
|
|
204
|
-
* @param {ObsidianUpdateFileInput} params - The validated input parameters conforming to the refined schema.
|
|
205
|
-
* @param {RequestContext} context - The request context for logging and correlation.
|
|
206
|
-
* @param {ObsidianRestApiService} obsidianService - The instance of the Obsidian REST API service.
|
|
207
|
-
* @returns {Promise<ObsidianUpdateFileResponse>} A promise resolving to the structured success response.
|
|
208
|
-
* @throws {McpError} Throws an McpError if validation fails or the API interaction results in an error.
|
|
209
|
-
*/
|
|
210
|
-
export const processObsidianUpdateNote = async (params, // Use the refined, validated type
|
|
211
|
-
context, obsidianService, vaultCacheService) => {
|
|
212
|
-
logger.debug(`Processing obsidian_update_note request (wholeFile mode)`, {
|
|
213
|
-
...context,
|
|
214
|
-
targetType: params.targetType,
|
|
215
|
-
wholeFileMode: params.wholeFileMode,
|
|
216
|
-
});
|
|
217
|
-
const targetId = params.targetIdentifier; // Alias for clarity
|
|
218
|
-
const contentString = params.content;
|
|
219
|
-
const mode = params.wholeFileMode;
|
|
220
|
-
let wasCreated = false; // Flag to track if the file was newly created by the operation
|
|
221
|
-
let targetPeriod;
|
|
222
|
-
// Parse the period if the target is a periodic note
|
|
223
|
-
if (params.targetType === "periodicNote" && targetId) {
|
|
224
|
-
// Use safeParse for robustness, though refined schema should guarantee validity
|
|
225
|
-
const parseResult = PeriodicNotePeriodSchema.safeParse(targetId);
|
|
226
|
-
if (!parseResult.success) {
|
|
227
|
-
// This should ideally not happen due to the refined schema, but handle defensively
|
|
228
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Invalid period provided for periodicNote: ${targetId}`, context);
|
|
229
|
-
}
|
|
230
|
-
targetPeriod = parseResult.data;
|
|
231
|
-
}
|
|
232
|
-
try {
|
|
233
|
-
// --- Step 1: Pre-operation Existence Check ---
|
|
234
|
-
// Determine if the target file/note exists before attempting modification.
|
|
235
|
-
// This is crucial for overwrite safety checks and createIfNeeded logic.
|
|
236
|
-
let existsBefore = false;
|
|
237
|
-
const checkContext = { ...context, operation: "existenceCheck" };
|
|
238
|
-
logger.debug(`Checking existence of target: ${params.targetType} ${targetId ?? "(active)"}`, checkContext);
|
|
239
|
-
try {
|
|
240
|
-
await retryWithDelay(async () => {
|
|
241
|
-
if (params.targetType === "filePath" && targetId) {
|
|
242
|
-
await obsidianService.getFileContent(targetId, "json", checkContext);
|
|
243
|
-
}
|
|
244
|
-
else if (params.targetType === "activeFile") {
|
|
245
|
-
await obsidianService.getActiveFile("json", checkContext);
|
|
246
|
-
}
|
|
247
|
-
else if (params.targetType === "periodicNote" && targetPeriod) {
|
|
248
|
-
await obsidianService.getPeriodicNote(targetPeriod, "json", checkContext);
|
|
249
|
-
}
|
|
250
|
-
// If any of the above succeed without throwing, the target exists.
|
|
251
|
-
existsBefore = true;
|
|
252
|
-
logger.debug(`Target exists before operation.`, checkContext);
|
|
253
|
-
}, {
|
|
254
|
-
operationName: "existenceCheckObsidianUpdateNote",
|
|
255
|
-
context: checkContext,
|
|
256
|
-
maxRetries: 3, // Total attempts: 1 initial + 2 retries
|
|
257
|
-
delayMs: 250,
|
|
258
|
-
shouldRetry: (error) => {
|
|
259
|
-
// Only retry if it's a NOT_FOUND error AND createIfNeeded is true.
|
|
260
|
-
// If createIfNeeded is false, a NOT_FOUND error means we shouldn't proceed, so don't retry.
|
|
261
|
-
const should = error instanceof McpError &&
|
|
262
|
-
error.code === BaseErrorCode.NOT_FOUND &&
|
|
263
|
-
params.createIfNeeded;
|
|
264
|
-
if (error instanceof McpError &&
|
|
265
|
-
error.code === BaseErrorCode.NOT_FOUND) {
|
|
266
|
-
logger.debug(`existenceCheckObsidianUpdateNote: shouldRetry=${should} for NOT_FOUND (createIfNeeded: ${params.createIfNeeded})`, checkContext);
|
|
267
|
-
}
|
|
268
|
-
return should;
|
|
269
|
-
},
|
|
270
|
-
onRetry: (attempt, error) => {
|
|
271
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
272
|
-
logger.warning(`Existence check (attempt ${attempt}) failed for target '${params.targetType} ${targetId ?? ""}'. Error: ${errorMsg}. Retrying as createIfNeeded is true...`, checkContext);
|
|
273
|
-
},
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
catch (error) {
|
|
277
|
-
// This catch block is primarily for the case where retryWithDelay itself throws
|
|
278
|
-
// (e.g., all retries exhausted for NOT_FOUND with createIfNeeded=true, or an unretryable error occurred).
|
|
279
|
-
if (error instanceof McpError && error.code === BaseErrorCode.NOT_FOUND) {
|
|
280
|
-
// If it's still NOT_FOUND after retries (or if createIfNeeded was false and it failed the first time),
|
|
281
|
-
// then existsBefore should definitely be false.
|
|
282
|
-
existsBefore = false;
|
|
283
|
-
logger.debug(`Target confirmed not to exist after existence check attempts (createIfNeeded: ${params.createIfNeeded}).`, checkContext);
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
286
|
-
// For any other error type, re-throw it as it's unexpected here.
|
|
287
|
-
logger.error(`Unexpected error after existence check retries`, error instanceof Error ? error : undefined, checkContext);
|
|
288
|
-
throw error;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
// --- Step 2: Perform Safety and Configuration Checks ---
|
|
292
|
-
const safetyCheckContext = {
|
|
293
|
-
...context,
|
|
294
|
-
operation: "safetyChecks",
|
|
295
|
-
existsBefore,
|
|
296
|
-
};
|
|
297
|
-
// Check 2a: Overwrite safety
|
|
298
|
-
if (mode === "overwrite" && existsBefore && !params.overwriteIfExists) {
|
|
299
|
-
logger.warning(`Overwrite attempt failed: Target exists and overwriteIfExists is false.`, safetyCheckContext);
|
|
300
|
-
throw new McpError(BaseErrorCode.CONFLICT, // Use CONFLICT as it clashes with existing state + config
|
|
301
|
-
`Target ${params.targetType} '${targetId ?? "(active)"}' exists, and 'overwriteIfExists' is set to false. Cannot overwrite.`, safetyCheckContext);
|
|
302
|
-
}
|
|
303
|
-
// Check 2b: Not Found when creation is disabled
|
|
304
|
-
if (!existsBefore && !params.createIfNeeded) {
|
|
305
|
-
logger.warning(`Update attempt failed: Target not found and createIfNeeded is false.`, safetyCheckContext);
|
|
306
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Target ${params.targetType} '${targetId ?? "(active)"}' not found, and 'createIfNeeded' is set to false. Cannot update.`, safetyCheckContext);
|
|
307
|
-
}
|
|
308
|
-
// Determine if the operation will result in file creation
|
|
309
|
-
wasCreated = !existsBefore && params.createIfNeeded;
|
|
310
|
-
logger.debug(`Operation will proceed. File creation needed: ${wasCreated}`, safetyCheckContext);
|
|
311
|
-
// --- Step 3: Perform the Update Operation via Obsidian API ---
|
|
312
|
-
const updateContext = {
|
|
313
|
-
...context,
|
|
314
|
-
operation: `performUpdate:${mode}`,
|
|
315
|
-
wasCreated,
|
|
316
|
-
};
|
|
317
|
-
logger.debug(`Performing update operation: ${mode}`, updateContext);
|
|
318
|
-
// Handle 'prepend' and 'append' manually as Obsidian API might not directly support them atomically.
|
|
319
|
-
if (mode === "prepend" || mode === "append") {
|
|
320
|
-
let existingContent = "";
|
|
321
|
-
// Only read existing content if the file existed before the operation.
|
|
322
|
-
if (existsBefore) {
|
|
323
|
-
const readContext = { ...updateContext, subOperation: "readForModify" };
|
|
324
|
-
logger.debug(`Reading existing content for ${mode}`, readContext);
|
|
325
|
-
try {
|
|
326
|
-
if (params.targetType === "filePath" && targetId) {
|
|
327
|
-
existingContent = (await obsidianService.getFileContent(targetId, "markdown", readContext));
|
|
328
|
-
}
|
|
329
|
-
else if (params.targetType === "activeFile") {
|
|
330
|
-
existingContent = (await obsidianService.getActiveFile("markdown", readContext));
|
|
331
|
-
}
|
|
332
|
-
else if (params.targetType === "periodicNote" && targetPeriod) {
|
|
333
|
-
existingContent = (await obsidianService.getPeriodicNote(targetPeriod, "markdown", readContext));
|
|
334
|
-
}
|
|
335
|
-
logger.debug(`Successfully read existing content. Length: ${existingContent.length}`, readContext);
|
|
336
|
-
}
|
|
337
|
-
catch (readError) {
|
|
338
|
-
// This should ideally not happen if existsBefore is true, but handle defensively.
|
|
339
|
-
const errorMsg = readError instanceof Error ? readError.message : String(readError);
|
|
340
|
-
logger.error(`Error reading existing content for ${mode} despite existence check.`, readError instanceof Error ? readError : undefined, readContext);
|
|
341
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Failed to read existing content for ${mode} operation. Error: ${errorMsg}`, readContext);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
else {
|
|
345
|
-
logger.debug(`Target did not exist before, skipping read for ${mode}.`, updateContext);
|
|
346
|
-
}
|
|
347
|
-
// Combine content based on the mode.
|
|
348
|
-
const newContent = mode === "prepend"
|
|
349
|
-
? contentString + existingContent
|
|
350
|
-
: existingContent + contentString;
|
|
351
|
-
logger.debug(`Combined content length for ${mode}: ${newContent.length}`, updateContext);
|
|
352
|
-
// Overwrite the target with the newly combined content.
|
|
353
|
-
const writeContext = { ...updateContext, subOperation: "writeCombined" };
|
|
354
|
-
logger.debug(`Writing combined content back to target`, writeContext);
|
|
355
|
-
if (params.targetType === "filePath" && targetId) {
|
|
356
|
-
await obsidianService.updateFileContent(targetId, newContent, writeContext);
|
|
357
|
-
}
|
|
358
|
-
else if (params.targetType === "activeFile") {
|
|
359
|
-
await obsidianService.updateActiveFile(newContent, writeContext);
|
|
360
|
-
}
|
|
361
|
-
else if (params.targetType === "periodicNote" && targetPeriod) {
|
|
362
|
-
await obsidianService.updatePeriodicNote(targetPeriod, newContent, writeContext);
|
|
363
|
-
}
|
|
364
|
-
logger.debug(`Successfully wrote combined content for ${mode}`, writeContext);
|
|
365
|
-
if (params.targetType === "filePath" && targetId && vaultCacheService) {
|
|
366
|
-
await vaultCacheService.updateCacheForFile(targetId, writeContext);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
else {
|
|
370
|
-
// Handle 'overwrite' mode directly.
|
|
371
|
-
switch (params.targetType) {
|
|
372
|
-
case "filePath":
|
|
373
|
-
// targetId is guaranteed by refined schema check
|
|
374
|
-
await obsidianService.updateFileContent(targetId, contentString, updateContext);
|
|
375
|
-
break;
|
|
376
|
-
case "activeFile":
|
|
377
|
-
await obsidianService.updateActiveFile(contentString, updateContext);
|
|
378
|
-
break;
|
|
379
|
-
case "periodicNote":
|
|
380
|
-
// targetPeriod is guaranteed by refined schema check
|
|
381
|
-
await obsidianService.updatePeriodicNote(targetPeriod, contentString, updateContext);
|
|
382
|
-
break;
|
|
383
|
-
}
|
|
384
|
-
logger.debug(`Successfully performed overwrite on target: ${params.targetType} ${targetId ?? "(active)"}`, updateContext);
|
|
385
|
-
if (params.targetType === "filePath" && targetId && vaultCacheService) {
|
|
386
|
-
await vaultCacheService.updateCacheForFile(targetId, updateContext);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
// --- Step 4: Get Final State (Stat and Optional Content) ---
|
|
390
|
-
// Add a small delay before attempting to get the final state, to allow Obsidian API to stabilize after write.
|
|
391
|
-
const POST_UPDATE_DELAY_MS = 250;
|
|
392
|
-
logger.debug(`Waiting ${POST_UPDATE_DELAY_MS}ms before retrieving final state...`, { ...context, operation: "postUpdateDelay" });
|
|
393
|
-
await new Promise((resolve) => setTimeout(resolve, POST_UPDATE_DELAY_MS));
|
|
394
|
-
// Attempt to retrieve the file's state *after* the modification.
|
|
395
|
-
let finalState = null; // Initialize to null
|
|
396
|
-
try {
|
|
397
|
-
finalState = await retryWithDelay(async () => getFinalState(params.targetType, targetId, targetPeriod, obsidianService, context), {
|
|
398
|
-
operationName: "getFinalStateAfterUpdate",
|
|
399
|
-
context: { ...context, operation: "getFinalStateAfterUpdateAttempt" }, // Use a distinct context for retry logs
|
|
400
|
-
maxRetries: 3, // Total attempts: 1 initial + 2 retries
|
|
401
|
-
delayMs: 250, // Shorter delay
|
|
402
|
-
shouldRetry: (error) => {
|
|
403
|
-
// Retry on common transient issues or if the file might not be immediately available
|
|
404
|
-
const should = error instanceof McpError &&
|
|
405
|
-
(error.code === BaseErrorCode.NOT_FOUND || // File might not be indexed immediately
|
|
406
|
-
error.code === BaseErrorCode.SERVICE_UNAVAILABLE || // API temporarily busy
|
|
407
|
-
error.code === BaseErrorCode.TIMEOUT); // API call timed out
|
|
408
|
-
if (should) {
|
|
409
|
-
logger.debug(`getFinalStateAfterUpdate: shouldRetry=true for error code ${error.code}`, context);
|
|
410
|
-
}
|
|
411
|
-
return should;
|
|
412
|
-
},
|
|
413
|
-
onRetry: (attempt, error) => {
|
|
414
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
415
|
-
logger.warning(`getFinalState (attempt ${attempt}) failed. Error: ${errorMsg}. Retrying...`, { ...context, operation: "getFinalStateRetry" });
|
|
416
|
-
},
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
catch (error) {
|
|
420
|
-
// If retryWithDelay throws after all attempts, getFinalState effectively failed.
|
|
421
|
-
// The original getFinalState already logs a warning and returns null if it encounters an error internally
|
|
422
|
-
// and is designed not to let its failure stop the main operation.
|
|
423
|
-
// So, if retryWithDelay throws, it means even retries didn't help.
|
|
424
|
-
finalState = null; // Ensure finalState remains null
|
|
425
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
426
|
-
logger.error(`Failed to retrieve final state for target '${params.targetType} ${targetId ?? ""}' even after retries. Error: ${errorMsg}`, error instanceof Error ? error : undefined, context);
|
|
427
|
-
// Do not re-throw here, allow the main process to construct a response with a warning.
|
|
428
|
-
}
|
|
429
|
-
// --- Step 5: Construct Success Message ---
|
|
430
|
-
// Create a user-friendly message indicating what happened.
|
|
431
|
-
let messageAction;
|
|
432
|
-
if (wasCreated) {
|
|
433
|
-
// Use past tense for creation events
|
|
434
|
-
messageAction =
|
|
435
|
-
mode === "overwrite" ? "created" : `${mode}d (and created)`;
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
// Use past tense for modifications of existing files
|
|
439
|
-
messageAction = mode === "overwrite" ? "overwritten" : `${mode}ed`;
|
|
440
|
-
}
|
|
441
|
-
const targetName = params.targetType === "filePath"
|
|
442
|
-
? `'${targetId}'`
|
|
443
|
-
: params.targetType === "periodicNote"
|
|
444
|
-
? `'${targetId}' note`
|
|
445
|
-
: "the active file";
|
|
446
|
-
let successMessage = `File content successfully ${messageAction} for ${targetName}.`; // Use let
|
|
447
|
-
logger.info(successMessage, context); // Log initial success message
|
|
448
|
-
// Append a warning if the final state couldn't be retrieved
|
|
449
|
-
if (finalState === null) {
|
|
450
|
-
const warningMsg = " (Warning: Could not retrieve final file stats/content after update.)";
|
|
451
|
-
successMessage += warningMsg;
|
|
452
|
-
logger.warning(`Appending warning to response message: ${warningMsg}`, context);
|
|
453
|
-
}
|
|
454
|
-
// --- Step 6: Build and Return Response ---
|
|
455
|
-
// Format the file statistics (if available) using the shared utility.
|
|
456
|
-
const finalContentForStat = finalState?.content ?? ""; // Provide content for token counting
|
|
457
|
-
const formattedStatResult = finalState?.stat
|
|
458
|
-
? await createFormattedStatWithTokenCount(finalState.stat, finalContentForStat, context) // Await the async utility
|
|
459
|
-
: undefined;
|
|
460
|
-
// Ensure stat is undefined if the utility returned null (e.g., token counting failed)
|
|
461
|
-
const formattedStat = formattedStatResult === null ? undefined : formattedStatResult;
|
|
462
|
-
// Construct the final response object.
|
|
463
|
-
const response = {
|
|
464
|
-
success: true,
|
|
465
|
-
message: successMessage,
|
|
466
|
-
stats: formattedStat,
|
|
467
|
-
};
|
|
468
|
-
// Include final content if requested and available.
|
|
469
|
-
if (params.returnContent) {
|
|
470
|
-
response.finalContent = finalState?.content; // Assign content if available, otherwise undefined
|
|
471
|
-
logger.debug(`Including final content in response as requested.`, context);
|
|
472
|
-
}
|
|
473
|
-
return response;
|
|
474
|
-
}
|
|
475
|
-
catch (error) {
|
|
476
|
-
// Handle errors, ensuring they are McpError instances before re-throwing.
|
|
477
|
-
// Errors from obsidianService calls should already be McpErrors and logged by the service.
|
|
478
|
-
if (error instanceof McpError) {
|
|
479
|
-
// Log McpErrors specifically from this level if needed, though lower levels might have logged already
|
|
480
|
-
logger.error(`McpError during file update: ${error.message}`, error, context);
|
|
481
|
-
throw error; // Re-throw known McpError
|
|
482
|
-
}
|
|
483
|
-
else {
|
|
484
|
-
// Catch unexpected errors, log them, and wrap in a generic McpError.
|
|
485
|
-
const errorMessage = `Unexpected error updating Obsidian file/note`;
|
|
486
|
-
logger.error(errorMessage, error instanceof Error ? error : undefined, context);
|
|
487
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `${errorMessage}: ${error instanceof Error ? error.message : String(error)}`, context);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { ObsidianRestApiService, VaultCacheService } from "../../../services/obsidianRestAPI/index.js";
|
|
3
|
-
/**
|
|
4
|
-
* Registers the 'obsidian_update_note' tool with the MCP server.
|
|
5
|
-
*
|
|
6
|
-
* This tool allows modification of Obsidian notes (specified by file path,
|
|
7
|
-
* the active file, or a periodic note) using whole-file operations:
|
|
8
|
-
* 'append', 'prepend', or 'overwrite'. It includes options for creating
|
|
9
|
-
* missing files/targets and controlling overwrite behavior.
|
|
10
|
-
*
|
|
11
|
-
* The tool returns a JSON string containing the operation status, a message,
|
|
12
|
-
* a formatted timestamp of the operation, file statistics (stat), and
|
|
13
|
-
* optionally the final content of the modified file.
|
|
14
|
-
*
|
|
15
|
-
* @param {McpServer} server - The MCP server instance to register the tool with.
|
|
16
|
-
* @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service
|
|
17
|
-
* used to interact with the user's Obsidian vault.
|
|
18
|
-
* @returns {Promise<void>} A promise that resolves when the tool registration is complete or rejects on error.
|
|
19
|
-
* @throws {McpError} Throws an McpError if registration fails critically.
|
|
20
|
-
*/
|
|
21
|
-
export declare const registerObsidianUpdateNoteTool: (server: McpServer, obsidianService: ObsidianRestApiService, vaultCacheService: VaultCacheService | undefined) => Promise<void>;
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
2
|
-
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
|
|
3
|
-
// Import the Zod schema for validation and the core processing logic
|
|
4
|
-
import { ObsidianUpdateNoteInputSchema, ObsidianUpdateNoteInputSchemaShape, processObsidianUpdateNote, } from "./logic.js";
|
|
5
|
-
/**
|
|
6
|
-
* Registers the 'obsidian_update_note' tool with the MCP server.
|
|
7
|
-
*
|
|
8
|
-
* This tool allows modification of Obsidian notes (specified by file path,
|
|
9
|
-
* the active file, or a periodic note) using whole-file operations:
|
|
10
|
-
* 'append', 'prepend', or 'overwrite'. It includes options for creating
|
|
11
|
-
* missing files/targets and controlling overwrite behavior.
|
|
12
|
-
*
|
|
13
|
-
* The tool returns a JSON string containing the operation status, a message,
|
|
14
|
-
* a formatted timestamp of the operation, file statistics (stat), and
|
|
15
|
-
* optionally the final content of the modified file.
|
|
16
|
-
*
|
|
17
|
-
* @param {McpServer} server - The MCP server instance to register the tool with.
|
|
18
|
-
* @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service
|
|
19
|
-
* used to interact with the user's Obsidian vault.
|
|
20
|
-
* @returns {Promise<void>} A promise that resolves when the tool registration is complete or rejects on error.
|
|
21
|
-
* @throws {McpError} Throws an McpError if registration fails critically.
|
|
22
|
-
*/
|
|
23
|
-
export const registerObsidianUpdateNoteTool = async (server, obsidianService, vaultCacheService) => {
|
|
24
|
-
const toolName = "obsidian_update_note";
|
|
25
|
-
const toolDescription = "Tool to modify Obsidian notes (specified by file path, the active file, or a periodic note) using whole-file operations: 'append', 'prepend', or 'overwrite'. Options allow creating missing files/targets and controlling overwrite behavior. Returns success status, message, a formatted timestamp string, file stats (stats), and optionally the final file content.";
|
|
26
|
-
// Create a context for the registration process itself for better traceability.
|
|
27
|
-
const registrationContext = requestContextService.createRequestContext({
|
|
28
|
-
operation: "RegisterObsidianUpdateNoteTool",
|
|
29
|
-
toolName: toolName,
|
|
30
|
-
module: "ObsidianUpdateNoteRegistration", // Identify the module performing registration
|
|
31
|
-
});
|
|
32
|
-
logger.info(`Attempting to register tool: ${toolName}`, registrationContext);
|
|
33
|
-
// Wrap the registration in a tryCatch block for robust error handling during setup.
|
|
34
|
-
await ErrorHandler.tryCatch(async () => {
|
|
35
|
-
// Use the high-level SDK method for tool registration.
|
|
36
|
-
// This handles schema generation, validation, and routing automatically.
|
|
37
|
-
server.tool(toolName, toolDescription, ObsidianUpdateNoteInputSchemaShape, // Provide the Zod schema shape for input validation.
|
|
38
|
-
/**
|
|
39
|
-
* The handler function executed when the 'obsidian_update_note' tool is called.
|
|
40
|
-
*
|
|
41
|
-
* @param {ObsidianUpdateNoteRegistrationInput} params - The raw input parameters received from the client,
|
|
42
|
-
* matching the structure defined by ObsidianUpdateNoteInputSchemaShape.
|
|
43
|
-
* @returns {Promise<CallToolResult>} A promise resolving to the structured result for the MCP client,
|
|
44
|
-
* containing either the successful response data or an error indication.
|
|
45
|
-
*/
|
|
46
|
-
async (params) => {
|
|
47
|
-
// Create a specific context for this handler invocation.
|
|
48
|
-
const handlerContext = requestContextService.createRequestContext({
|
|
49
|
-
parentContext: registrationContext, // Link to the registration context
|
|
50
|
-
operation: "HandleObsidianUpdateNoteRequest",
|
|
51
|
-
toolName: toolName,
|
|
52
|
-
params: {
|
|
53
|
-
// Log key parameters for easier debugging, content is omitted for brevity/security
|
|
54
|
-
targetType: params.targetType,
|
|
55
|
-
modificationType: params.modificationType, // Note: Will always be 'wholeFile' due to schema
|
|
56
|
-
targetIdentifier: params.targetIdentifier,
|
|
57
|
-
wholeFileMode: params.wholeFileMode,
|
|
58
|
-
createIfNeeded: params.createIfNeeded,
|
|
59
|
-
overwriteIfExists: params.overwriteIfExists,
|
|
60
|
-
returnContent: params.returnContent,
|
|
61
|
-
},
|
|
62
|
-
});
|
|
63
|
-
logger.debug(`Handling '${toolName}' request (wholeFile mode)`, handlerContext);
|
|
64
|
-
// Wrap the core logic execution in a tryCatch block for handling errors during processing.
|
|
65
|
-
return await ErrorHandler.tryCatch(async () => {
|
|
66
|
-
// Explicitly parse and validate the incoming parameters using the full Zod schema.
|
|
67
|
-
// This ensures type safety and adherence to constraints defined in logic.ts.
|
|
68
|
-
// While server.tool performs initial validation based on the shape,
|
|
69
|
-
// this step applies any stricter rules or refinements from the full schema.
|
|
70
|
-
const validatedParams = ObsidianUpdateNoteInputSchema.parse(params);
|
|
71
|
-
// Delegate the actual file update logic to the dedicated processing function.
|
|
72
|
-
// Pass the validated parameters, the handler context, and the Obsidian service instance.
|
|
73
|
-
const response = await processObsidianUpdateNote(validatedParams, handlerContext, obsidianService, vaultCacheService);
|
|
74
|
-
logger.debug(`'${toolName}' (wholeFile mode) processed successfully`, handlerContext);
|
|
75
|
-
// Format the successful response from the logic function into the MCP CallToolResult structure.
|
|
76
|
-
// The response object (containing status, message, timestamp, stat, etc.) is serialized to JSON.
|
|
77
|
-
return {
|
|
78
|
-
content: [
|
|
79
|
-
{
|
|
80
|
-
type: "text", // Standard content type for structured data
|
|
81
|
-
text: JSON.stringify(response, null, 2), // Pretty-print JSON for readability
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
isError: false, // Indicate successful execution
|
|
85
|
-
};
|
|
86
|
-
}, {
|
|
87
|
-
// Configuration for the inner error handler (processing logic).
|
|
88
|
-
operation: `processing ${toolName} handler`,
|
|
89
|
-
context: handlerContext,
|
|
90
|
-
input: params, // Log the full raw input parameters if an error occurs during processing.
|
|
91
|
-
// Custom error mapping to ensure consistent McpError format.
|
|
92
|
-
errorMapper: (error) => new McpError(error instanceof McpError
|
|
93
|
-
? error.code
|
|
94
|
-
: BaseErrorCode.INTERNAL_ERROR, // Use INTERNAL_ERROR as the fallback
|
|
95
|
-
`Error processing ${toolName} tool: ${error instanceof Error ? error.message : "Unknown error"}`, { ...handlerContext }),
|
|
96
|
-
}); // End of inner ErrorHandler.tryCatch
|
|
97
|
-
}); // End of server.tool call
|
|
98
|
-
logger.info(`Tool registered successfully: ${toolName}`, registrationContext);
|
|
99
|
-
}, {
|
|
100
|
-
// Configuration for the outer error handler (registration process).
|
|
101
|
-
operation: `registering tool ${toolName}`,
|
|
102
|
-
context: registrationContext,
|
|
103
|
-
errorCode: BaseErrorCode.INTERNAL_ERROR, // Default error code for registration failure
|
|
104
|
-
// Custom error mapping for registration failures.
|
|
105
|
-
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 }),
|
|
106
|
-
critical: true, // Registration failure is considered critical and should likely halt server startup.
|
|
107
|
-
}); // End of outer ErrorHandler.tryCatch
|
|
108
|
-
};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Defines the AsyncLocalStorage context for authentication information.
|
|
3
|
-
* This module provides a mechanism to store and retrieve authentication details
|
|
4
|
-
* (like scopes and client ID) across asynchronous operations, making it available
|
|
5
|
-
* from the middleware layer down to the tool and resource handlers without
|
|
6
|
-
* drilling props.
|
|
7
|
-
*
|
|
8
|
-
* @module src/mcp-server/transports/auth/core/authContext
|
|
9
|
-
*/
|
|
10
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
11
|
-
import type { AuthInfo } from "./authTypes.js";
|
|
12
|
-
/**
|
|
13
|
-
* Defines the structure of the store used within the AsyncLocalStorage.
|
|
14
|
-
* It holds the authentication information for the current request context.
|
|
15
|
-
*/
|
|
16
|
-
interface AuthStore {
|
|
17
|
-
authInfo: AuthInfo;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* An instance of AsyncLocalStorage to hold the authentication context (`AuthStore`).
|
|
21
|
-
* This allows `authInfo` to be accessible throughout the async call chain of a request
|
|
22
|
-
* after being set in the authentication middleware.
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* // In middleware:
|
|
26
|
-
* await authContext.run({ authInfo }, next);
|
|
27
|
-
*
|
|
28
|
-
* // In a deeper handler:
|
|
29
|
-
* const store = authContext.getStore();
|
|
30
|
-
* const scopes = store?.authInfo.scopes;
|
|
31
|
-
*/
|
|
32
|
-
export declare const authContext: AsyncLocalStorage<AuthStore>;
|
|
33
|
-
export {};
|