obsidian-mcp-server 2.0.3 → 2.0.5
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/CHANGELOG.md +22 -0
- package/README.md +80 -88
- package/dist/mcp-server/server.js +2 -2
- package/dist/mcp-server/tools/obsidianListFilesTool/logic.d.ts +20 -16
- package/dist/mcp-server/tools/obsidianListFilesTool/logic.js +142 -106
- package/dist/mcp-server/tools/obsidianListFilesTool/registration.d.ts +9 -5
- package/dist/mcp-server/tools/obsidianListFilesTool/registration.js +11 -9
- package/dist/mcp-server/tools/{obsidianUpdateFileTool → obsidianUpdateNoteTool}/index.d.ts +4 -4
- package/dist/mcp-server/tools/{obsidianUpdateFileTool → obsidianUpdateNoteTool}/index.js +4 -4
- package/dist/mcp-server/tools/{obsidianUpdateFileTool → obsidianUpdateNoteTool}/logic.d.ts +8 -8
- package/dist/mcp-server/tools/{obsidianUpdateFileTool → obsidianUpdateNoteTool}/logic.js +7 -7
- package/dist/mcp-server/tools/{obsidianUpdateFileTool → obsidianUpdateNoteTool}/registration.d.ts +2 -2
- package/dist/mcp-server/tools/{obsidianUpdateFileTool → obsidianUpdateNoteTool}/registration.js +13 -13
- package/dist/mcp-server/transports/{authentication → auth/core}/authContext.d.ts +2 -2
- package/dist/mcp-server/transports/{authentication → auth/core}/authContext.js +1 -1
- package/dist/mcp-server/transports/{authentication/types.d.ts → auth/core/authTypes.d.ts} +1 -1
- package/dist/mcp-server/transports/{authentication/types.js → auth/core/authTypes.js} +1 -1
- package/dist/mcp-server/transports/{authentication → auth/core}/authUtils.d.ts +1 -1
- package/dist/mcp-server/transports/{authentication → auth/core}/authUtils.js +3 -3
- package/dist/mcp-server/transports/auth/index.d.ts +10 -0
- package/dist/mcp-server/transports/auth/index.js +9 -0
- package/dist/mcp-server/transports/{authentication/authMiddleware.d.ts → auth/strategies/jwt/jwtMiddleware.d.ts} +4 -7
- package/dist/mcp-server/transports/{authentication/authMiddleware.js → auth/strategies/jwt/jwtMiddleware.js} +40 -36
- package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.d.ts +2 -6
- package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.js +33 -18
- package/dist/mcp-server/transports/httpErrorHandler.d.ts +26 -0
- package/dist/mcp-server/transports/httpErrorHandler.js +73 -0
- package/dist/mcp-server/transports/httpTransport.d.ts +11 -14
- package/dist/mcp-server/transports/httpTransport.js +91 -379
- package/package.json +11 -16
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Core logic for the 'obsidian_list_files' tool.
|
|
3
|
+
* This module defines the input schema, response types, and processing logic for
|
|
4
|
+
* recursively listing files and directories in an Obsidian vault with filtering.
|
|
5
|
+
* @module src/mcp-server/tools/obsidianListFilesTool/logic
|
|
6
|
+
*/
|
|
7
|
+
import path from "node:path";
|
|
2
8
|
import { z } from "zod";
|
|
3
9
|
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
4
10
|
import { logger, retryWithDelay, } from "../../../utils/index.js";
|
|
@@ -12,7 +18,6 @@ export const ObsidianListFilesInputSchema = z
|
|
|
12
18
|
.object({
|
|
13
19
|
/**
|
|
14
20
|
* The vault-relative path to the directory whose contents should be listed.
|
|
15
|
-
* Examples: "Attachments/Images", "Projects", "" (for vault root), "/" (for vault root).
|
|
16
21
|
* The path is treated as case-sensitive by the underlying Obsidian API.
|
|
17
22
|
*/
|
|
18
23
|
dirPath: z
|
|
@@ -20,160 +25,191 @@ export const ObsidianListFilesInputSchema = z
|
|
|
20
25
|
.describe('The vault-relative path to the directory to list (e.g., "developer/atlas-mcp-server", "/" for root). Case-sensitive.'),
|
|
21
26
|
/**
|
|
22
27
|
* Optional array of file extensions (including the leading dot) to filter the results.
|
|
23
|
-
* Only files matching one of these extensions will be included. Directories are always included
|
|
24
|
-
* Example: [".md", ".png"]
|
|
28
|
+
* Only files matching one of these extensions will be included. Directories are always included.
|
|
25
29
|
*/
|
|
26
30
|
fileExtensionFilter: z
|
|
27
31
|
.array(z.string().startsWith(".", "Extension must start with a dot '.'"))
|
|
28
32
|
.optional()
|
|
29
|
-
.describe('Optional array of file extensions (e.g., [".md") to filter files. Directories are always included.'),
|
|
33
|
+
.describe('Optional array of file extensions (e.g., [".md"]) to filter files. Directories are always included.'),
|
|
30
34
|
/**
|
|
31
35
|
* Optional JavaScript-compatible regular expression pattern string to filter results by name.
|
|
32
36
|
* Only files and directories whose names match the regex will be included.
|
|
33
|
-
* Example: "^\\d{4}-\\d{2}-\\d{2}" (matches names starting with YYYY-MM-DD)
|
|
34
37
|
*/
|
|
35
38
|
nameRegexFilter: z
|
|
36
39
|
.string()
|
|
37
40
|
.nullable()
|
|
38
|
-
.optional()
|
|
41
|
+
.optional()
|
|
39
42
|
.describe("Optional regex pattern (JavaScript syntax) to filter results by name."),
|
|
43
|
+
/**
|
|
44
|
+
* The maximum depth of subdirectories to list recursively.
|
|
45
|
+
* - A value of `0` lists only the files and directories in the specified `dirPath`.
|
|
46
|
+
* - A value of `1` lists the contents of `dirPath` and the contents of its immediate subdirectories.
|
|
47
|
+
* - A value of `-1` (the default) indicates infinite recursion, listing all subdirectories.
|
|
48
|
+
*/
|
|
49
|
+
recursionDepth: z
|
|
50
|
+
.number()
|
|
51
|
+
.int()
|
|
52
|
+
.default(-1)
|
|
53
|
+
.describe("Maximum recursion depth. 0 for no recursion, -1 for infinite (default)."),
|
|
40
54
|
})
|
|
41
|
-
.describe("Input parameters for listing files and subdirectories within a specified Obsidian vault directory, with optional filtering.");
|
|
55
|
+
.describe("Input parameters for listing files and subdirectories within a specified Obsidian vault directory, with optional filtering and recursion.");
|
|
42
56
|
// ====================================================================================
|
|
43
57
|
// Helper Functions
|
|
44
58
|
// ====================================================================================
|
|
45
59
|
/**
|
|
46
|
-
*
|
|
47
|
-
* Directories (indicated by a trailing '/') are listed first, then files, both sorted alphabetically.
|
|
60
|
+
* Recursively builds a formatted tree string from a nested array of FileTreeNode objects.
|
|
48
61
|
*
|
|
49
|
-
* @param {
|
|
50
|
-
* @
|
|
62
|
+
* @param {FileTreeNode[]} nodes - The array of nodes to format.
|
|
63
|
+
* @param {string} [indent=""] - The indentation prefix for the current level.
|
|
64
|
+
* @returns {{ tree: string, count: number }} An object containing the formatted tree string and the total count of entries.
|
|
51
65
|
*/
|
|
52
|
-
function
|
|
53
|
-
if (!fileNames || fileNames.length === 0) {
|
|
54
|
-
return "(empty directory)";
|
|
55
|
-
}
|
|
56
|
-
// Sort entries: directories first, then files, alphabetically within each group.
|
|
57
|
-
fileNames.sort((a, b) => {
|
|
58
|
-
const aIsDir = a.endsWith("/");
|
|
59
|
-
const bIsDir = b.endsWith("/");
|
|
60
|
-
// Group directories before files
|
|
61
|
-
if (aIsDir && !bIsDir)
|
|
62
|
-
return -1; // a (dir) comes before b (file)
|
|
63
|
-
if (!aIsDir && bIsDir)
|
|
64
|
-
return 1; // b (dir) comes before a (file)
|
|
65
|
-
// Within the same type (both dirs or both files), sort alphabetically.
|
|
66
|
-
// Remove trailing slash for comparison if it's a directory.
|
|
67
|
-
const nameA = aIsDir ? a.slice(0, -1) : a;
|
|
68
|
-
const nameB = bIsDir ? b.slice(0, -1) : b;
|
|
69
|
-
return nameA.localeCompare(nameB);
|
|
70
|
-
});
|
|
71
|
-
// Build the tree string with prefixes
|
|
66
|
+
function formatTree(nodes, indent = "") {
|
|
72
67
|
let treeString = "";
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const isLast = index ===
|
|
76
|
-
const prefix = isLast ? "└── " : "├── ";
|
|
77
|
-
|
|
68
|
+
let count = nodes.length;
|
|
69
|
+
nodes.forEach((node, index) => {
|
|
70
|
+
const isLast = index === nodes.length - 1;
|
|
71
|
+
const prefix = isLast ? "└── " : "├── ";
|
|
72
|
+
const childIndent = isLast ? " " : "│ ";
|
|
73
|
+
treeString += `${indent}${prefix}${node.name}\n`;
|
|
74
|
+
if (node.children && node.children.length > 0) {
|
|
75
|
+
const result = formatTree(node.children, indent + childIndent);
|
|
76
|
+
treeString += result.tree;
|
|
77
|
+
count += result.count;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return { tree: treeString, count };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Recursively builds a file tree by fetching directory contents from the Obsidian API.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} dirPath - The path of the directory to process.
|
|
86
|
+
* @param {number} currentDepth - The current recursion depth.
|
|
87
|
+
* @param {ObsidianListFilesInput} params - The original validated input parameters, including filters and max depth.
|
|
88
|
+
* @param {RequestContext} context - The request context for logging.
|
|
89
|
+
* @param {ObsidianRestApiService} obsidianService - The Obsidian API service instance.
|
|
90
|
+
* @returns {Promise<FileTreeNode[]>} A promise that resolves to an array of file tree nodes.
|
|
91
|
+
*/
|
|
92
|
+
async function buildFileTree(dirPath, currentDepth, params, context, obsidianService) {
|
|
93
|
+
const { recursionDepth, fileExtensionFilter, nameRegexFilter } = params;
|
|
94
|
+
// Stop recursion if max depth is reached (and it's not infinite)
|
|
95
|
+
if (recursionDepth !== -1 && currentDepth > recursionDepth) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
let fileNames;
|
|
99
|
+
try {
|
|
100
|
+
fileNames = await obsidianService.listFiles(dirPath, context);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error instanceof McpError && error.code === BaseErrorCode.NOT_FOUND) {
|
|
104
|
+
logger.warning(`Directory not found during recursive list: ${dirPath}. Skipping.`, context);
|
|
105
|
+
return []; // Return empty array if a subdirectory is not found
|
|
106
|
+
}
|
|
107
|
+
throw error; // Re-throw other errors
|
|
108
|
+
}
|
|
109
|
+
const regex = nameRegexFilter && nameRegexFilter.trim() !== ""
|
|
110
|
+
? new RegExp(nameRegexFilter)
|
|
111
|
+
: null;
|
|
112
|
+
const treeNodes = [];
|
|
113
|
+
for (const name of fileNames) {
|
|
114
|
+
const fullPath = path.posix.join(dirPath, name);
|
|
115
|
+
const isDirectory = name.endsWith("/");
|
|
116
|
+
const cleanName = isDirectory ? name.slice(0, -1) : name;
|
|
117
|
+
// Apply filters
|
|
118
|
+
if (regex && !regex.test(cleanName)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (!isDirectory && fileExtensionFilter && fileExtensionFilter.length > 0) {
|
|
122
|
+
const extension = path.posix.extname(name);
|
|
123
|
+
if (!fileExtensionFilter.includes(extension)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const node = {
|
|
128
|
+
name: cleanName,
|
|
129
|
+
type: isDirectory ? "directory" : "file",
|
|
130
|
+
children: [],
|
|
131
|
+
};
|
|
132
|
+
if (isDirectory) {
|
|
133
|
+
node.name += "/"; // Add trailing slash back for display
|
|
134
|
+
node.children = await buildFileTree(fullPath, currentDepth + 1, params, context, obsidianService);
|
|
135
|
+
}
|
|
136
|
+
treeNodes.push(node);
|
|
137
|
+
}
|
|
138
|
+
// Sort entries: directories first, then files, alphabetically
|
|
139
|
+
treeNodes.sort((a, b) => {
|
|
140
|
+
if (a.type === "directory" && b.type === "file")
|
|
141
|
+
return -1;
|
|
142
|
+
if (a.type === "file" && b.type === "directory")
|
|
143
|
+
return 1;
|
|
144
|
+
return a.name.localeCompare(b.name);
|
|
78
145
|
});
|
|
79
|
-
return
|
|
146
|
+
return treeNodes;
|
|
80
147
|
}
|
|
81
148
|
// ====================================================================================
|
|
82
149
|
// Core Logic Function
|
|
83
150
|
// ====================================================================================
|
|
84
151
|
/**
|
|
85
|
-
* Processes the core logic for listing files and directories within
|
|
86
|
-
* directory in the Obsidian vault. Applies optional filters and formats the output.
|
|
152
|
+
* Processes the core logic for listing files and directories recursively within the Obsidian vault.
|
|
87
153
|
*
|
|
88
154
|
* @param {ObsidianListFilesInput} params - The validated input parameters.
|
|
89
155
|
* @param {RequestContext} context - The request context for logging and correlation.
|
|
90
156
|
* @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service.
|
|
91
|
-
* @returns {Promise<ObsidianListFilesResponse>} A promise resolving to the structured success response
|
|
92
|
-
*
|
|
93
|
-
* @throws {McpError} Throws an McpError if the directory cannot be listed (e.g., not found)
|
|
94
|
-
* or if any other API interaction or validation fails.
|
|
157
|
+
* @returns {Promise<ObsidianListFilesResponse>} A promise resolving to the structured success response.
|
|
158
|
+
* @throws {McpError} Throws an McpError if the initial directory is not found or another error occurs.
|
|
95
159
|
*/
|
|
96
160
|
export const processObsidianListFiles = async (params, context, obsidianService) => {
|
|
97
|
-
const { dirPath
|
|
98
|
-
// Normalize dirPath for logging and response (use "/" for root)
|
|
161
|
+
const { dirPath } = params;
|
|
99
162
|
const dirPathForLog = dirPath === "" || dirPath === "/" ? "/" : dirPath;
|
|
100
|
-
logger.debug(`Processing obsidian_list_files request for path: ${dirPathForLog}`, { ...context,
|
|
163
|
+
logger.debug(`Processing obsidian_list_files request for path: ${dirPathForLog}`, { ...context, params });
|
|
101
164
|
try {
|
|
102
|
-
// Normalize path for the API call as well
|
|
103
165
|
const effectiveDirPath = dirPath === "" ? "/" : dirPath;
|
|
104
|
-
// --- Step 1:
|
|
105
|
-
const
|
|
106
|
-
|
|
166
|
+
// --- Step 1: Build the file tree recursively with retry for the initial call ---
|
|
167
|
+
const buildTreeContext = {
|
|
168
|
+
...context,
|
|
169
|
+
operation: "buildFileTreeWithRetry",
|
|
170
|
+
};
|
|
107
171
|
const shouldRetryNotFound = (err) => err instanceof McpError && err.code === BaseErrorCode.NOT_FOUND;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
172
|
+
const fileTree = await retryWithDelay(() => buildFileTree(effectiveDirPath, 0, // Start at depth 0
|
|
173
|
+
params, buildTreeContext, obsidianService), {
|
|
174
|
+
operationName: "buildFileTreeWithRetry",
|
|
175
|
+
context: buildTreeContext,
|
|
111
176
|
maxRetries: 3,
|
|
112
177
|
delayMs: 300,
|
|
113
178
|
shouldRetry: shouldRetryNotFound,
|
|
114
179
|
});
|
|
115
|
-
|
|
116
|
-
// --- Step 2: Apply Filters ---
|
|
117
|
-
const filterContext = { ...context, operation: "applyFilters" };
|
|
118
|
-
// Apply extension filter if provided
|
|
119
|
-
if (fileExtensionFilter && fileExtensionFilter.length > 0) {
|
|
120
|
-
const initialCount = fileNames.length;
|
|
121
|
-
fileNames = fileNames.filter((fileName) => {
|
|
122
|
-
// Always keep directories (identified by trailing '/')
|
|
123
|
-
if (fileName.endsWith("/"))
|
|
124
|
-
return true;
|
|
125
|
-
// Check if the file's extension is in the filter list
|
|
126
|
-
const extension = path.posix.extname(fileName); // Use path.posix.extname for consistency
|
|
127
|
-
return fileExtensionFilter.includes(extension);
|
|
128
|
-
});
|
|
129
|
-
logger.debug(`Applied extension filter (${fileExtensionFilter.join(", ")}). ${initialCount} -> ${fileNames.length} items remaining.`, filterContext);
|
|
130
|
-
}
|
|
131
|
-
// Apply regex name filter if provided and is a non-empty string
|
|
132
|
-
if (nameRegexFilter && nameRegexFilter.trim() !== "") {
|
|
133
|
-
const initialCount = fileNames.length;
|
|
134
|
-
try {
|
|
135
|
-
const regex = new RegExp(nameRegexFilter); // Compile the regex pattern
|
|
136
|
-
fileNames = fileNames.filter((fileName) => regex.test(fileName)); // Test each name against the regex
|
|
137
|
-
logger.debug(`Applied regex filter /${nameRegexFilter}/. ${initialCount} -> ${fileNames.length} items remaining.`, filterContext);
|
|
138
|
-
}
|
|
139
|
-
catch (regexError) {
|
|
140
|
-
// Handle invalid regex patterns provided by the user
|
|
141
|
-
logger.error(`Invalid regex pattern provided: ${nameRegexFilter}`, regexError instanceof Error ? regexError : undefined, filterContext);
|
|
142
|
-
throw new McpError(BaseErrorCode.VALIDATION_ERROR, // It's an input validation issue
|
|
143
|
-
`Invalid regex pattern provided for nameRegexFilter: ${nameRegexFilter}. Error: ${regexError instanceof Error ? regexError.message : "Unknown regex error"}`, filterContext);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
// --- Step 3: Format Output and Return ---
|
|
180
|
+
// --- Step 2: Format the tree and count entries ---
|
|
147
181
|
const formatContext = { ...context, operation: "formatResponse" };
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
182
|
+
if (fileTree.length === 0) {
|
|
183
|
+
logger.debug("Directory is empty or all items were filtered out.", formatContext);
|
|
184
|
+
return {
|
|
185
|
+
directoryPath: dirPathForLog,
|
|
186
|
+
tree: "(empty or all items filtered)",
|
|
187
|
+
totalEntries: 0,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const { tree, count } = formatTree(fileTree);
|
|
191
|
+
// --- Step 3: Construct and return the response ---
|
|
153
192
|
const response = {
|
|
154
|
-
directoryPath: dirPathForLog,
|
|
155
|
-
tree:
|
|
156
|
-
totalEntries:
|
|
193
|
+
directoryPath: dirPathForLog,
|
|
194
|
+
tree: tree.trimEnd(), // Remove trailing newline
|
|
195
|
+
totalEntries: count,
|
|
157
196
|
};
|
|
158
|
-
logger.debug(`Successfully processed list request for ${dirPathForLog}.`, context);
|
|
197
|
+
logger.debug(`Successfully processed list request for ${dirPathForLog}. Found ${count} entries.`, context);
|
|
159
198
|
return response;
|
|
160
199
|
}
|
|
161
200
|
catch (error) {
|
|
162
|
-
// Handle errors, ensuring they are McpError instances before re-throwing.
|
|
163
201
|
if (error instanceof McpError) {
|
|
164
|
-
// Provide a more specific message if the directory wasn't found
|
|
202
|
+
// Provide a more specific message if the directory wasn't found after retries
|
|
165
203
|
if (error.code === BaseErrorCode.NOT_FOUND) {
|
|
166
|
-
|
|
167
|
-
|
|
204
|
+
const notFoundMsg = `Directory not found after retries: ${dirPathForLog}`;
|
|
205
|
+
logger.error(notFoundMsg, error, context);
|
|
206
|
+
throw new McpError(error.code, notFoundMsg, context);
|
|
168
207
|
}
|
|
169
208
|
logger.error(`McpError during file listing for ${dirPathForLog}: ${error.message}`, error, context);
|
|
170
|
-
throw error;
|
|
171
|
-
}
|
|
172
|
-
else {
|
|
173
|
-
// Catch and wrap unexpected errors
|
|
174
|
-
const errorMessage = `Unexpected error listing Obsidian files in ${dirPathForLog}`;
|
|
175
|
-
logger.error(errorMessage, error instanceof Error ? error : undefined, context);
|
|
176
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `${errorMessage}: ${error instanceof Error ? error.message : String(error)}`, context);
|
|
209
|
+
throw error;
|
|
177
210
|
}
|
|
211
|
+
const errorMessage = `Unexpected error listing Obsidian files in ${dirPathForLog}`;
|
|
212
|
+
logger.error(errorMessage, error instanceof Error ? error : undefined, context);
|
|
213
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `${errorMessage}: ${error instanceof Error ? error.message : String(error)}`, context);
|
|
178
214
|
}
|
|
179
215
|
};
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Registers the 'obsidian_list_files' tool with the MCP server.
|
|
3
|
+
* This file defines the tool's metadata and sets up the handler that links
|
|
4
|
+
* the tool call to its core processing logic.
|
|
5
|
+
* @module src/mcp-server/tools/obsidianListFilesTool/registration
|
|
6
|
+
*/
|
|
1
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
8
|
import { ObsidianRestApiService } from "../../../services/obsidianRestAPI/index.js";
|
|
3
9
|
/**
|
|
4
10
|
* Registers the 'obsidian_list_files' tool with the MCP server.
|
|
5
11
|
*
|
|
6
12
|
* This tool lists the files and subdirectories within a specified directory
|
|
7
|
-
* in the user's Obsidian vault. It supports optional filtering by file extension
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* The response includes the path of the listed directory, a formatted tree string
|
|
11
|
-
* representing the contents, and the total count of entries listed after filtering.
|
|
13
|
+
* in the user's Obsidian vault. It supports optional filtering by file extension,
|
|
14
|
+
* by a regular expression matching the entry name, and recursive listing up to a
|
|
15
|
+
* specified depth.
|
|
12
16
|
*
|
|
13
17
|
* @param {McpServer} server - The MCP server instance to register the tool with.
|
|
14
18
|
* @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Registers the 'obsidian_list_files' tool with the MCP server.
|
|
3
|
+
* This file defines the tool's metadata and sets up the handler that links
|
|
4
|
+
* the tool call to its core processing logic.
|
|
5
|
+
* @module src/mcp-server/tools/obsidianListFilesTool/registration
|
|
6
|
+
*/
|
|
1
7
|
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
2
8
|
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
|
|
3
9
|
import { ObsidianListFilesInputSchema, processObsidianListFiles, } from "./logic.js";
|
|
@@ -5,11 +11,9 @@ import { ObsidianListFilesInputSchema, processObsidianListFiles, } from "./logic
|
|
|
5
11
|
* Registers the 'obsidian_list_files' tool with the MCP server.
|
|
6
12
|
*
|
|
7
13
|
* This tool lists the files and subdirectories within a specified directory
|
|
8
|
-
* in the user's Obsidian vault. It supports optional filtering by file extension
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* The response includes the path of the listed directory, a formatted tree string
|
|
12
|
-
* representing the contents, and the total count of entries listed after filtering.
|
|
14
|
+
* in the user's Obsidian vault. It supports optional filtering by file extension,
|
|
15
|
+
* by a regular expression matching the entry name, and recursive listing up to a
|
|
16
|
+
* specified depth.
|
|
13
17
|
*
|
|
14
18
|
* @param {McpServer} server - The MCP server instance to register the tool with.
|
|
15
19
|
* @param {ObsidianRestApiService} obsidianService - An instance of the Obsidian REST API service
|
|
@@ -19,8 +23,7 @@ import { ObsidianListFilesInputSchema, processObsidianListFiles, } from "./logic
|
|
|
19
23
|
*/
|
|
20
24
|
export const registerObsidianListFilesTool = async (server, obsidianService) => {
|
|
21
25
|
const toolName = "obsidian_list_files";
|
|
22
|
-
|
|
23
|
-
const toolDescription = "Lists files and subdirectories within a specified Obsidian vault folder. Supports optional filtering by extension or name regex. Returns an object containing the listed directory path, a formatted tree string of its contents, and the total entry count. Use an empty string or '/' for dirPath to list the vault root.";
|
|
26
|
+
const toolDescription = "Lists files and subdirectories within a specified Obsidian vault folder. Supports optional filtering by extension or name regex, and recursive listing to a specified depth (-1 for infinite). Returns an object containing the listed directory path, a formatted tree string of its contents, and the total entry count. Use an empty string or '/' for dirPath to list the vault root.";
|
|
24
27
|
// Create a context specifically for the registration process.
|
|
25
28
|
const registrationContext = requestContextService.createRequestContext({
|
|
26
29
|
operation: "RegisterObsidianListFilesTool",
|
|
@@ -52,17 +55,16 @@ export const registerObsidianListFilesTool = async (server, obsidianService) =>
|
|
|
52
55
|
dirPath: params.dirPath,
|
|
53
56
|
fileExtensionFilter: params.fileExtensionFilter,
|
|
54
57
|
nameRegexFilter: params.nameRegexFilter,
|
|
58
|
+
recursionDepth: params.recursionDepth,
|
|
55
59
|
},
|
|
56
60
|
});
|
|
57
61
|
logger.debug(`Handling '${toolName}' request`, handlerContext);
|
|
58
62
|
// Wrap the core logic execution in a tryCatch block.
|
|
59
63
|
return await ErrorHandler.tryCatch(async () => {
|
|
60
64
|
// Delegate the actual file listing and filtering logic to the processing function.
|
|
61
|
-
// Note: The input schema and shape are identical here, so no separate refinement parse is needed.
|
|
62
65
|
const response = await processObsidianListFiles(params, handlerContext, obsidianService);
|
|
63
66
|
logger.debug(`'${toolName}' processed successfully`, handlerContext);
|
|
64
67
|
// Format the successful response object from the logic function into the required MCP CallToolResult structure.
|
|
65
|
-
// The entire response object (directoryPath, tree, totalEntries) is serialized to JSON.
|
|
66
68
|
return {
|
|
67
69
|
content: [
|
|
68
70
|
{
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Barrel file for the '
|
|
2
|
+
* @fileoverview Barrel file for the 'obsidian_update_note' MCP tool.
|
|
3
3
|
*
|
|
4
|
-
* This file serves as the public entry point for the
|
|
5
|
-
* It re-exports the primary registration function (`
|
|
4
|
+
* This file serves as the public entry point for the obsidian_update_note tool module.
|
|
5
|
+
* It re-exports the primary registration function (`registerObsidianUpdateNoteTool`)
|
|
6
6
|
* from the './registration.js' module. This pattern simplifies imports for consumers
|
|
7
7
|
* of the tool, allowing them to import necessary components from a single location.
|
|
8
8
|
*
|
|
9
9
|
* Consumers (like the main server setup) should import the registration function
|
|
10
10
|
* from this file to integrate the tool into the MCP server instance.
|
|
11
11
|
*/
|
|
12
|
-
export {
|
|
12
|
+
export { registerObsidianUpdateNoteTool } from "./registration.js";
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Barrel file for the '
|
|
2
|
+
* @fileoverview Barrel file for the 'obsidian_update_note' MCP tool.
|
|
3
3
|
*
|
|
4
|
-
* This file serves as the public entry point for the
|
|
5
|
-
* It re-exports the primary registration function (`
|
|
4
|
+
* This file serves as the public entry point for the obsidian_update_note tool module.
|
|
5
|
+
* It re-exports the primary registration function (`registerObsidianUpdateNoteTool`)
|
|
6
6
|
* from the './registration.js' module. This pattern simplifies imports for consumers
|
|
7
7
|
* of the tool, allowing them to import necessary components from a single location.
|
|
8
8
|
*
|
|
9
9
|
* Consumers (like the main server setup) should import the registration function
|
|
10
10
|
* from this file to integrate the tool into the MCP server instance.
|
|
11
11
|
*/
|
|
12
|
-
export {
|
|
12
|
+
export { registerObsidianUpdateNoteTool } from "./registration.js";
|
|
@@ -8,7 +8,7 @@ import { RequestContext } from "../../../utils/index.js";
|
|
|
8
8
|
* relying on the refined schema (`ObsidianUpdateFileInputSchema`) for stricter validation
|
|
9
9
|
* within the handler logic.
|
|
10
10
|
*/
|
|
11
|
-
declare const
|
|
11
|
+
declare const ObsidianUpdateNoteRegistrationSchema: z.ZodObject<{
|
|
12
12
|
/** Specifies the target note: 'filePath' (requires targetIdentifier), 'activeFile' (currently open file), or 'periodicNote' (requires targetIdentifier with period like 'daily'). */
|
|
13
13
|
targetType: z.ZodEnum<["filePath", "activeFile", "periodicNote"]>;
|
|
14
14
|
/** The content for the modification. Must be a string for whole-file operations. */
|
|
@@ -48,7 +48,7 @@ declare const ObsidianUpdateFileRegistrationSchema: z.ZodObject<{
|
|
|
48
48
|
* The shape of the registration schema, used by `server.tool` for basic validation.
|
|
49
49
|
* @see ObsidianUpdateFileRegistrationSchema
|
|
50
50
|
*/
|
|
51
|
-
export declare const
|
|
51
|
+
export declare const ObsidianUpdateNoteInputSchemaShape: {
|
|
52
52
|
/** Specifies the target note: 'filePath' (requires targetIdentifier), 'activeFile' (currently open file), or 'periodicNote' (requires targetIdentifier with period like 'daily'). */
|
|
53
53
|
targetType: z.ZodEnum<["filePath", "activeFile", "periodicNote"]>;
|
|
54
54
|
/** The content for the modification. Must be a string for whole-file operations. */
|
|
@@ -71,13 +71,13 @@ export declare const ObsidianUpdateFileInputSchemaShape: {
|
|
|
71
71
|
* received by the tool handler *before* refinement.
|
|
72
72
|
* @see ObsidianUpdateFileRegistrationSchema
|
|
73
73
|
*/
|
|
74
|
-
export type
|
|
74
|
+
export type ObsidianUpdateNoteRegistrationInput = z.infer<typeof ObsidianUpdateNoteRegistrationSchema>;
|
|
75
75
|
/**
|
|
76
76
|
* Refined Zod schema used internally within the tool's logic for strict validation.
|
|
77
77
|
* It builds upon `WholeFileUpdateSchema` and adds cross-field validation rules using `.refine()`.
|
|
78
78
|
* This ensures that `targetIdentifier` is provided and valid when required by `targetType`.
|
|
79
79
|
*/
|
|
80
|
-
export declare const
|
|
80
|
+
export declare const ObsidianUpdateNoteInputSchema: z.ZodEffects<z.ZodObject<{
|
|
81
81
|
/** Specifies the type of target note. */
|
|
82
82
|
targetType: z.ZodEnum<["filePath", "activeFile", "periodicNote"]>;
|
|
83
83
|
/** The content to use for the modification. Must be a string for whole-file operations. */
|
|
@@ -140,7 +140,7 @@ export declare const ObsidianUpdateFileInputSchema: z.ZodEffects<z.ZodObject<{
|
|
|
140
140
|
* TypeScript type inferred from the *refined* input schema (`ObsidianUpdateFileInputSchema`).
|
|
141
141
|
* This type represents the validated and structured input used within the core processing logic.
|
|
142
142
|
*/
|
|
143
|
-
export type
|
|
143
|
+
export type ObsidianUpdateNoteInput = z.infer<typeof ObsidianUpdateNoteInputSchema>;
|
|
144
144
|
/**
|
|
145
145
|
* Represents the structure of file statistics after formatting, including
|
|
146
146
|
* human-readable timestamps and an estimated token count.
|
|
@@ -157,7 +157,7 @@ type FormattedStat = {
|
|
|
157
157
|
* Defines the structure of the successful response returned by the `processObsidianUpdateFile` function.
|
|
158
158
|
* This object is typically serialized to JSON and sent back to the client.
|
|
159
159
|
*/
|
|
160
|
-
export interface
|
|
160
|
+
export interface ObsidianUpdateNoteResponse {
|
|
161
161
|
/** Indicates whether the operation was successful. */
|
|
162
162
|
success: boolean;
|
|
163
163
|
/** A human-readable message describing the outcome of the operation. */
|
|
@@ -178,6 +178,6 @@ export interface ObsidianUpdateFileResponse {
|
|
|
178
178
|
* @returns {Promise<ObsidianUpdateFileResponse>} A promise resolving to the structured success response.
|
|
179
179
|
* @throws {McpError} Throws an McpError if validation fails or the API interaction results in an error.
|
|
180
180
|
*/
|
|
181
|
-
export declare const
|
|
182
|
-
context: RequestContext, obsidianService: ObsidianRestApiService, vaultCacheService: VaultCacheService | undefined) => Promise<
|
|
181
|
+
export declare const processObsidianUpdateNote: (params: ObsidianUpdateNoteInput, // Use the refined, validated type
|
|
182
|
+
context: RequestContext, obsidianService: ObsidianRestApiService, vaultCacheService: VaultCacheService | undefined) => Promise<ObsidianUpdateNoteResponse>;
|
|
183
183
|
export {};
|
|
@@ -79,7 +79,7 @@ const WholeFileUpdateSchema = BaseUpdateSchema.extend({
|
|
|
79
79
|
* relying on the refined schema (`ObsidianUpdateFileInputSchema`) for stricter validation
|
|
80
80
|
* within the handler logic.
|
|
81
81
|
*/
|
|
82
|
-
const
|
|
82
|
+
const ObsidianUpdateNoteRegistrationSchema = z
|
|
83
83
|
.object({
|
|
84
84
|
/** Specifies the target note: 'filePath' (requires targetIdentifier), 'activeFile' (currently open file), or 'periodicNote' (requires targetIdentifier with period like 'daily'). */
|
|
85
85
|
targetType: TargetTypeSchema,
|
|
@@ -122,7 +122,7 @@ const ObsidianUpdateFileRegistrationSchema = z
|
|
|
122
122
|
* The shape of the registration schema, used by `server.tool` for basic validation.
|
|
123
123
|
* @see ObsidianUpdateFileRegistrationSchema
|
|
124
124
|
*/
|
|
125
|
-
export const
|
|
125
|
+
export const ObsidianUpdateNoteInputSchemaShape = ObsidianUpdateNoteRegistrationSchema.shape;
|
|
126
126
|
// ====================================================================================
|
|
127
127
|
// Refined Schema for Internal Logic and Strict Validation
|
|
128
128
|
// ====================================================================================
|
|
@@ -131,7 +131,7 @@ export const ObsidianUpdateFileInputSchemaShape = ObsidianUpdateFileRegistration
|
|
|
131
131
|
* It builds upon `WholeFileUpdateSchema` and adds cross-field validation rules using `.refine()`.
|
|
132
132
|
* This ensures that `targetIdentifier` is provided and valid when required by `targetType`.
|
|
133
133
|
*/
|
|
134
|
-
export const
|
|
134
|
+
export const ObsidianUpdateNoteInputSchema = WholeFileUpdateSchema.refine((data) => {
|
|
135
135
|
// Rule 1: If targetType is 'filePath' or 'periodicNote', targetIdentifier must be provided.
|
|
136
136
|
if ((data.targetType === "filePath" || data.targetType === "periodicNote") &&
|
|
137
137
|
!data.targetIdentifier) {
|
|
@@ -207,9 +207,9 @@ async function getFinalState(targetType, targetIdentifier, period, obsidianServi
|
|
|
207
207
|
* @returns {Promise<ObsidianUpdateFileResponse>} A promise resolving to the structured success response.
|
|
208
208
|
* @throws {McpError} Throws an McpError if validation fails or the API interaction results in an error.
|
|
209
209
|
*/
|
|
210
|
-
export const
|
|
210
|
+
export const processObsidianUpdateNote = async (params, // Use the refined, validated type
|
|
211
211
|
context, obsidianService, vaultCacheService) => {
|
|
212
|
-
logger.debug(`Processing
|
|
212
|
+
logger.debug(`Processing obsidian_update_note request (wholeFile mode)`, {
|
|
213
213
|
...context,
|
|
214
214
|
targetType: params.targetType,
|
|
215
215
|
wholeFileMode: params.wholeFileMode,
|
|
@@ -251,7 +251,7 @@ context, obsidianService, vaultCacheService) => {
|
|
|
251
251
|
existsBefore = true;
|
|
252
252
|
logger.debug(`Target exists before operation.`, checkContext);
|
|
253
253
|
}, {
|
|
254
|
-
operationName: "
|
|
254
|
+
operationName: "existenceCheckObsidianUpdateNote",
|
|
255
255
|
context: checkContext,
|
|
256
256
|
maxRetries: 3, // Total attempts: 1 initial + 2 retries
|
|
257
257
|
delayMs: 250,
|
|
@@ -263,7 +263,7 @@ context, obsidianService, vaultCacheService) => {
|
|
|
263
263
|
params.createIfNeeded;
|
|
264
264
|
if (error instanceof McpError &&
|
|
265
265
|
error.code === BaseErrorCode.NOT_FOUND) {
|
|
266
|
-
logger.debug(`
|
|
266
|
+
logger.debug(`existenceCheckObsidianUpdateNote: shouldRetry=${should} for NOT_FOUND (createIfNeeded: ${params.createIfNeeded})`, checkContext);
|
|
267
267
|
}
|
|
268
268
|
return should;
|
|
269
269
|
},
|
package/dist/mcp-server/tools/{obsidianUpdateFileTool → obsidianUpdateNoteTool}/registration.d.ts
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { ObsidianRestApiService, VaultCacheService } from "../../../services/obsidianRestAPI/index.js";
|
|
3
3
|
/**
|
|
4
|
-
* Registers the '
|
|
4
|
+
* Registers the 'obsidian_update_note' tool with the MCP server.
|
|
5
5
|
*
|
|
6
6
|
* This tool allows modification of Obsidian notes (specified by file path,
|
|
7
7
|
* the active file, or a periodic note) using whole-file operations:
|
|
@@ -18,4 +18,4 @@ import { ObsidianRestApiService, VaultCacheService } from "../../../services/obs
|
|
|
18
18
|
* @returns {Promise<void>} A promise that resolves when the tool registration is complete or rejects on error.
|
|
19
19
|
* @throws {McpError} Throws an McpError if registration fails critically.
|
|
20
20
|
*/
|
|
21
|
-
export declare const
|
|
21
|
+
export declare const registerObsidianUpdateNoteTool: (server: McpServer, obsidianService: ObsidianRestApiService, vaultCacheService: VaultCacheService | undefined) => Promise<void>;
|