obsidian-mcp-server 1.3.0 → 1.4.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.
Files changed (77) hide show
  1. package/build/index.js +12 -3
  2. package/build/mcp/handlers.js +138 -0
  3. package/build/mcp/index.js +7 -0
  4. package/build/mcp/server.js +131 -0
  5. package/build/mcp/types.js +7 -0
  6. package/build/{obsidian.js → obsidian/client.js} +116 -96
  7. package/build/obsidian/errors.js +75 -0
  8. package/build/obsidian/index.js +7 -0
  9. package/build/obsidian/types.js +12 -0
  10. package/build/resources/index.js +15 -0
  11. package/build/{resources.js → resources/tags.js} +31 -5
  12. package/build/resources/types.js +5 -0
  13. package/build/tools/base.js +78 -0
  14. package/build/tools/files/content.js +171 -0
  15. package/build/tools/files/index.js +22 -0
  16. package/build/tools/files/list.js +133 -0
  17. package/build/tools/index.js +31 -0
  18. package/build/tools/properties/index.js +19 -0
  19. package/build/{properties.js → tools/properties/manager.js} +40 -8
  20. package/build/{propertyTools.js → tools/properties/tools.js} +22 -6
  21. package/build/{propertyTypes.js → tools/properties/types.js} +17 -5
  22. package/build/tools/search/complex.js +203 -0
  23. package/build/tools/search/index.js +20 -0
  24. package/build/tools/search/simple.js +127 -0
  25. package/build/utils/errors.js +59 -0
  26. package/build/utils/index.js +9 -0
  27. package/build/utils/logging.js +119 -0
  28. package/build/utils/rate-limiting.js +94 -0
  29. package/build/utils/tokenization.js +62 -0
  30. package/build/utils/validation.js +88 -0
  31. package/examples/README.md +48 -0
  32. package/examples/append-content.md +63 -0
  33. package/examples/complex-search.md +117 -0
  34. package/examples/find-in-file.md +94 -0
  35. package/examples/get-file-contents.md +72 -0
  36. package/examples/get-properties.md +89 -0
  37. package/examples/list-files-in-dir.md +55 -0
  38. package/examples/list-files-in-vault.md +53 -0
  39. package/examples/patch-content.md +60 -0
  40. package/examples/update-properties.md +126 -0
  41. package/package.json +1 -1
  42. package/src/index.ts +13 -3
  43. package/src/mcp/handlers.ts +183 -0
  44. package/src/mcp/index.ts +6 -0
  45. package/src/mcp/server.ts +162 -0
  46. package/src/mcp/types.ts +46 -0
  47. package/src/{obsidian.ts → obsidian/client.ts} +132 -125
  48. package/src/obsidian/errors.ts +105 -0
  49. package/src/obsidian/index.ts +6 -0
  50. package/src/obsidian/types.ts +124 -0
  51. package/src/resources/index.ts +17 -0
  52. package/src/{resources.ts → resources/tags.ts} +38 -6
  53. package/src/resources/types.ts +30 -0
  54. package/src/tools/base.ts +112 -0
  55. package/src/tools/files/content.ts +206 -0
  56. package/src/tools/files/index.ts +31 -0
  57. package/src/tools/files/list.ts +150 -0
  58. package/src/tools/index.ts +38 -0
  59. package/src/tools/properties/index.ts +21 -0
  60. package/src/{properties.ts → tools/properties/manager.ts} +41 -8
  61. package/src/{propertyTools.ts → tools/properties/tools.ts} +33 -7
  62. package/src/{propertyTypes.ts → tools/properties/types.ts} +29 -5
  63. package/src/tools/search/complex.ts +231 -0
  64. package/src/tools/search/index.ts +22 -0
  65. package/src/tools/search/simple.ts +147 -0
  66. package/src/utils/errors.ts +69 -0
  67. package/src/utils/index.ts +8 -0
  68. package/src/utils/logging.ts +146 -0
  69. package/src/utils/rate-limiting.ts +114 -0
  70. package/src/utils/tokenization.ts +71 -0
  71. package/src/utils/validation.ts +95 -0
  72. package/build/server.js +0 -263
  73. package/build/tools.js +0 -845
  74. package/build/types.js +0 -37
  75. package/src/server.ts +0 -335
  76. package/src/tools.ts +0 -926
  77. package/src/types.ts +0 -184
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Type definitions for Obsidian client
3
+ */
4
+
5
+ /**
6
+ * Configuration for the Obsidian client
7
+ */
8
+ export interface ObsidianConfig {
9
+ apiKey: string;
10
+ verifySSL?: boolean;
11
+ timeout?: number;
12
+ maxContentLength?: number;
13
+ maxBodyLength?: number;
14
+ }
15
+
16
+ /**
17
+ * Server configuration for Obsidian
18
+ */
19
+ export interface ObsidianServerConfig {
20
+ protocol: "http" | "https";
21
+ host: string;
22
+ port: number;
23
+ }
24
+
25
+ /**
26
+ * Default configuration for Obsidian server
27
+ */
28
+ export const DEFAULT_OBSIDIAN_CONFIG: ObsidianServerConfig = {
29
+ protocol: "https", // HTTPS required by default in Obsidian REST API plugin
30
+ host: "127.0.0.1",
31
+ port: 27124
32
+ } as const;
33
+
34
+ /**
35
+ * JSON representation of an Obsidian note
36
+ */
37
+ export interface NoteJson {
38
+ content: string;
39
+ frontmatter: Record<string, unknown>;
40
+ path: string;
41
+ stat: {
42
+ ctime: number;
43
+ mtime: number;
44
+ size: number;
45
+ };
46
+ tags: string[];
47
+ }
48
+
49
+ /**
50
+ * Obsidian file/folder information
51
+ */
52
+ export interface ObsidianFile {
53
+ path: string;
54
+ type: "file" | "folder";
55
+ children?: ObsidianFile[];
56
+ }
57
+
58
+ /**
59
+ * Obsidian command
60
+ */
61
+ export interface ObsidianCommand {
62
+ id: string;
63
+ name: string;
64
+ }
65
+
66
+ /**
67
+ * Obsidian server status
68
+ */
69
+ export interface ObsidianStatus {
70
+ authenticated: boolean;
71
+ ok: string;
72
+ service: string;
73
+ versions: {
74
+ obsidian: string;
75
+ self: string;
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Period type for periodic notes
81
+ */
82
+ export interface PeriodType {
83
+ type: "daily" | "weekly" | "monthly" | "quarterly" | "yearly";
84
+ }
85
+
86
+ /**
87
+ * Search match with context
88
+ */
89
+ export interface SearchMatch {
90
+ context: string;
91
+ match: {
92
+ start: number;
93
+ end: number;
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Standard search result
99
+ */
100
+ export interface SearchResult {
101
+ filename: string;
102
+ result: unknown;
103
+ }
104
+
105
+ /**
106
+ * Simple search result with context
107
+ */
108
+ export interface SimpleSearchResult {
109
+ filename: string;
110
+ score: number;
111
+ matches: SearchMatch[];
112
+ }
113
+
114
+ /**
115
+ * Union type for search responses
116
+ */
117
+ export type SearchResponse = SearchResult | SimpleSearchResult;
118
+
119
+ /**
120
+ * JSON Logic query for complex searches
121
+ */
122
+ export interface JsonLogicQuery {
123
+ [operator: string]: unknown;
124
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Resources module exports
3
+ */
4
+ export * from './types.js';
5
+ export * from './tags.js';
6
+
7
+ import { ObsidianClient } from '../obsidian/client.js';
8
+ import { TagResource } from './tags.js';
9
+
10
+ /**
11
+ * Create and return the tag resource
12
+ * @param client The ObsidianClient instance
13
+ * @returns The tag resource
14
+ */
15
+ export function createTagResource(client: ObsidianClient): TagResource {
16
+ return new TagResource(client);
17
+ }
@@ -1,9 +1,20 @@
1
+ /**
2
+ * Tag resource implementation
3
+ */
1
4
  import { Resource, TextContent } from "@modelcontextprotocol/sdk/types.js";
2
- import { ObsidianClient } from "./obsidian.js";
3
- import { TagResponse, ObsidianFile, JsonLogicQuery } from "./types.js";
4
- import { PropertyManager } from "./properties.js";
5
+ import { ObsidianClient } from "../obsidian/client.js";
6
+ import { JsonLogicQuery } from "../obsidian/types.js";
7
+ import { PropertyManager } from "../tools/properties/manager.js";
5
8
  import { join, sep } from "path";
9
+ import { createLogger } from "../utils/logging.js";
10
+ import { TagResponse } from "./types.js";
6
11
 
12
+ // Create a logger for tag resources
13
+ const logger = createLogger('TagResource');
14
+
15
+ /**
16
+ * Resource for providing tags used in the Obsidian vault
17
+ */
7
18
  export class TagResource {
8
19
  private tagCache: Map<string, Set<string>> = new Map();
9
20
  private propertyManager: PropertyManager;
@@ -16,6 +27,9 @@ export class TagResource {
16
27
  this.initializeCache();
17
28
  }
18
29
 
30
+ /**
31
+ * Get resource description for the MCP server
32
+ */
19
33
  getResourceDescription(): Resource {
20
34
  return {
21
35
  uri: "obsidian://tags",
@@ -25,8 +39,13 @@ export class TagResource {
25
39
  };
26
40
  }
27
41
 
42
+ /**
43
+ * Initialize the tag cache
44
+ */
28
45
  private async initializeCache() {
29
46
  try {
47
+ logger.info('Initializing tag cache');
48
+
30
49
  // Get all markdown files using platform-agnostic path pattern
31
50
  const query: JsonLogicQuery = {
32
51
  "glob": [`**${sep}*.md`.replace(/\\/g, '/'), { "var": "path" }]
@@ -50,18 +69,22 @@ export class TagResource {
50
69
  });
51
70
  }
52
71
  } catch (error) {
53
- console.error(`Failed to process file ${result.filename}:`, error);
72
+ logger.error(`Failed to process file ${result.filename}:`, error);
54
73
  }
55
74
  }
56
75
 
57
76
  this.isInitialized = true;
58
77
  this.lastUpdate = Date.now();
78
+ logger.info(`Tag cache initialized with ${this.tagCache.size} unique tags`);
59
79
  } catch (error) {
60
- console.error("Failed to initialize tag cache:", error);
80
+ logger.error("Failed to initialize tag cache:", error);
61
81
  throw error;
62
82
  }
63
83
  }
64
84
 
85
+ /**
86
+ * Add a tag to the cache
87
+ */
65
88
  private addTag(tag: string, filepath: string) {
66
89
  if (!this.tagCache.has(tag)) {
67
90
  this.tagCache.set(tag, new Set());
@@ -69,16 +92,24 @@ export class TagResource {
69
92
  this.tagCache.get(tag)!.add(filepath);
70
93
  }
71
94
 
95
+ /**
96
+ * Update the cache if needed
97
+ */
72
98
  private async updateCacheIfNeeded() {
73
99
  const now = Date.now();
74
100
  if (now - this.lastUpdate > this.updateInterval) {
101
+ logger.debug('Tag cache needs update, refreshing...');
75
102
  await this.initializeCache();
76
103
  }
77
104
  }
78
105
 
106
+ /**
107
+ * Get the content for the resource
108
+ */
79
109
  async getContent(): Promise<TextContent[]> {
80
110
  try {
81
111
  if (!this.isInitialized) {
112
+ logger.info('Tag cache not initialized, initializing now');
82
113
  await this.initializeCache();
83
114
  } else {
84
115
  await this.updateCacheIfNeeded();
@@ -104,13 +135,14 @@ export class TagResource {
104
135
  }
105
136
  };
106
137
 
138
+ logger.debug(`Returning tag resource with ${response.tags.length} tags`);
107
139
  return [{
108
140
  type: "text",
109
141
  text: JSON.stringify(response, null, 2),
110
142
  uri: this.getResourceDescription().uri
111
143
  }];
112
144
  } catch (error) {
113
- console.error("Failed to get tags:", error);
145
+ logger.error("Failed to get tags:", error);
114
146
  throw error;
115
147
  }
116
148
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Resource types for the MCP server
3
+ */
4
+
5
+ /**
6
+ * Tag information structure
7
+ */
8
+ export interface TagInfo {
9
+ name: string;
10
+ count: number;
11
+ files: string[];
12
+ }
13
+
14
+ /**
15
+ * Tag metadata structure
16
+ */
17
+ export interface TagMetadata {
18
+ totalOccurrences: number;
19
+ uniqueTags: number;
20
+ scannedFiles: number;
21
+ lastUpdate: number; // Timestamp of last cache update
22
+ }
23
+
24
+ /**
25
+ * Tag resource response structure
26
+ */
27
+ export interface TagResponse {
28
+ tags: TagInfo[];
29
+ metadata: TagMetadata;
30
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Base class for all tool handlers
3
+ */
4
+ import { Tool, TextContent, ImageContent, EmbeddedResource } from "@modelcontextprotocol/sdk/types.js";
5
+ import { ObsidianClient } from "../obsidian/client.js";
6
+ import { ObsidianError } from "../utils/errors.js";
7
+ import { tokenCounter } from "../utils/tokenization.js";
8
+ import { createLogger } from "../utils/logging.js";
9
+
10
+ // Create a logger for tool operations
11
+ const logger = createLogger('Tools');
12
+
13
+ /**
14
+ * Interface that all tool handlers must implement
15
+ */
16
+ export interface ToolHandler<T = any> {
17
+ name: string;
18
+ getToolDescription(): Tool;
19
+ runTool(args: T): Promise<Array<TextContent>>;
20
+ }
21
+
22
+ /**
23
+ * Base class for all tool handlers with common functionality
24
+ */
25
+ export abstract class BaseToolHandler<T = Record<string, unknown>> implements ToolHandler<T> {
26
+ /**
27
+ * Create a new tool handler
28
+ * @param name The name of the tool
29
+ * @param client The ObsidianClient instance
30
+ */
31
+ constructor(
32
+ public readonly name: string,
33
+ protected client: ObsidianClient
34
+ ) {}
35
+
36
+ /**
37
+ * Get the tool description for MCP server
38
+ */
39
+ abstract getToolDescription(): Tool;
40
+
41
+ /**
42
+ * Run the tool with the provided arguments
43
+ */
44
+ abstract runTool(args: T): Promise<Array<TextContent>>;
45
+
46
+ /**
47
+ * Create a standardized response from any content type
48
+ * @param content The content to format for response
49
+ * @returns An array of TextContent objects
50
+ */
51
+ protected createResponse(content: unknown): TextContent[] {
52
+ let text: string;
53
+
54
+ // Handle different content types
55
+ if (typeof content === 'string') {
56
+ text = content;
57
+ } else if (content instanceof Buffer) {
58
+ text = content.toString('utf-8');
59
+ } else if (Array.isArray(content) && content.every(item => typeof item === 'string')) {
60
+ text = content.join('\n');
61
+ } else if (content instanceof Error) {
62
+ text = `Error: ${content.message}\n${content.stack || ''}`;
63
+ } else {
64
+ try {
65
+ text = JSON.stringify(content, null, 2);
66
+ } catch (error) {
67
+ text = String(content);
68
+ }
69
+ }
70
+
71
+ // Count tokens and truncate if necessary
72
+ const originalTokenCount = tokenCounter.countTokens(text);
73
+ const truncatedText = tokenCounter.truncateToTokenLimit(text);
74
+ const finalTokenCount = tokenCounter.countTokens(truncatedText);
75
+
76
+ if (originalTokenCount > finalTokenCount) {
77
+ logger.debug(
78
+ `[${this.name}] Response truncated:`,
79
+ `original tokens=${originalTokenCount}`,
80
+ `truncated tokens=${finalTokenCount}`
81
+ );
82
+ }
83
+
84
+ return [{
85
+ type: "text",
86
+ text: truncatedText
87
+ }];
88
+ }
89
+
90
+ /**
91
+ * Standard error handling for tool execution
92
+ * @param error The error to handle
93
+ * @throws ObsidianError
94
+ */
95
+ protected handleError(error: unknown): never {
96
+ if (error instanceof ObsidianError) {
97
+ throw error;
98
+ }
99
+ if (error instanceof Error) {
100
+ throw new ObsidianError(
101
+ `Tool '${this.name}' execution failed: ${error.message}`,
102
+ 50000, // Internal server error
103
+ { originalError: error.stack }
104
+ );
105
+ }
106
+ throw new ObsidianError(
107
+ `Tool '${this.name}' execution failed with unknown error`,
108
+ 50000, // Internal server error
109
+ { error }
110
+ );
111
+ }
112
+ }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * File content manipulation tools implementation
3
+ */
4
+ import { Tool } from "@modelcontextprotocol/sdk/types.js";
5
+ import { ObsidianClient } from "../../obsidian/client.js";
6
+ import { BaseToolHandler } from "../base.js";
7
+ import { createLogger } from "../../utils/logging.js";
8
+
9
+ // Create a logger for file content operations
10
+ const logger = createLogger('FileContentTools');
11
+
12
+ /**
13
+ * Tool names for file content operations
14
+ */
15
+ export const FILE_CONTENT_TOOL_NAMES = {
16
+ GET_FILE_CONTENTS: "obsidian_get_file_contents",
17
+ APPEND_CONTENT: "obsidian_append_content",
18
+ PATCH_CONTENT: "obsidian_patch_content"
19
+ } as const;
20
+
21
+ /**
22
+ * Arguments for file content operations
23
+ */
24
+ export interface FileContentsArgs {
25
+ filepath: string;
26
+ }
27
+
28
+ /**
29
+ * Arguments for appending content to a file
30
+ */
31
+ export interface AppendContentArgs {
32
+ filepath: string;
33
+ content: string;
34
+ }
35
+
36
+ /**
37
+ * Arguments for updating content of a file
38
+ */
39
+ export interface PatchContentArgs {
40
+ filepath: string;
41
+ content: string;
42
+ }
43
+
44
+ /**
45
+ * Tool handler for getting file contents
46
+ */
47
+ export class GetFileContentsToolHandler extends BaseToolHandler<FileContentsArgs> {
48
+ constructor(client: ObsidianClient) {
49
+ super(FILE_CONTENT_TOOL_NAMES.GET_FILE_CONTENTS, client);
50
+ }
51
+
52
+ getToolDescription(): Tool {
53
+ return {
54
+ name: this.name,
55
+ description: "Return the content of a single file in your vault. Supports markdown files, text files, and other readable formats. Returns the raw content including any YAML frontmatter.",
56
+ examples: [
57
+ {
58
+ description: "Get content of a markdown note",
59
+ args: {
60
+ filepath: "Projects/research.md"
61
+ }
62
+ },
63
+ {
64
+ description: "Get content of a configuration file",
65
+ args: {
66
+ filepath: "configs/settings.yml"
67
+ }
68
+ }
69
+ ],
70
+ inputSchema: {
71
+ type: "object",
72
+ properties: {
73
+ filepath: {
74
+ type: "string",
75
+ description: "Path to the relevant file (relative to your vault root).",
76
+ format: "path"
77
+ }
78
+ },
79
+ required: ["filepath"]
80
+ }
81
+ };
82
+ }
83
+
84
+ async runTool(args: FileContentsArgs): Promise<Array<any>> {
85
+ try {
86
+ logger.debug(`Getting contents of file: ${args.filepath}`);
87
+ const content = await this.client.getFileContents(args.filepath);
88
+ return this.createResponse(content);
89
+ } catch (error) {
90
+ return this.handleError(error);
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Tool handler for appending content to a file
97
+ */
98
+ export class AppendContentToolHandler extends BaseToolHandler<AppendContentArgs> {
99
+ constructor(client: ObsidianClient) {
100
+ super(FILE_CONTENT_TOOL_NAMES.APPEND_CONTENT, client);
101
+ }
102
+
103
+ getToolDescription(): Tool {
104
+ return {
105
+ name: this.name,
106
+ description: "Append content to a new or existing file in the vault.",
107
+ examples: [
108
+ {
109
+ description: "Append a new task",
110
+ args: {
111
+ filepath: "tasks.md",
112
+ content: "- [ ] New task to complete"
113
+ }
114
+ },
115
+ {
116
+ description: "Append meeting notes",
117
+ args: {
118
+ filepath: "meetings/2025-01-23.md",
119
+ content: "## Meeting Notes\n\n- Discussed project timeline\n- Assigned tasks"
120
+ }
121
+ }
122
+ ],
123
+ inputSchema: {
124
+ type: "object",
125
+ properties: {
126
+ filepath: {
127
+ type: "string",
128
+ description: "Path to the file (relative to vault root)",
129
+ format: "path"
130
+ },
131
+ content: {
132
+ type: "string",
133
+ description: "Content to append to the file"
134
+ }
135
+ },
136
+ required: ["filepath", "content"]
137
+ }
138
+ };
139
+ }
140
+
141
+ async runTool(args: AppendContentArgs): Promise<Array<any>> {
142
+ try {
143
+ logger.debug(`Appending content to file: ${args.filepath}`);
144
+ await this.client.appendContent(args.filepath, args.content);
145
+ return this.createResponse({
146
+ message: `Successfully appended content to ${args.filepath}`,
147
+ success: true
148
+ });
149
+ } catch (error) {
150
+ return this.handleError(error);
151
+ }
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Tool handler for updating file content
157
+ */
158
+ export class PatchContentToolHandler extends BaseToolHandler<PatchContentArgs> {
159
+ constructor(client: ObsidianClient) {
160
+ super(FILE_CONTENT_TOOL_NAMES.PATCH_CONTENT, client);
161
+ }
162
+
163
+ getToolDescription(): Tool {
164
+ return {
165
+ name: this.name,
166
+ description: "Update the entire content of an existing note or create a new one.",
167
+ examples: [
168
+ {
169
+ description: "Update a note's content",
170
+ args: {
171
+ filepath: "project.md",
172
+ content: "# Project Notes\n\nThis will replace the entire content of the note."
173
+ }
174
+ }
175
+ ],
176
+ inputSchema: {
177
+ type: "object",
178
+ properties: {
179
+ filepath: {
180
+ type: "string",
181
+ description: "Path to the file (relative to vault root)",
182
+ format: "path"
183
+ },
184
+ content: {
185
+ type: "string",
186
+ description: "New content for the note (replaces existing content)"
187
+ }
188
+ },
189
+ required: ["filepath", "content"]
190
+ }
191
+ };
192
+ }
193
+
194
+ async runTool(args: PatchContentArgs): Promise<Array<any>> {
195
+ try {
196
+ logger.debug(`Updating content of file: ${args.filepath}`);
197
+ await this.client.updateContent(args.filepath, args.content);
198
+ return this.createResponse({
199
+ message: `Successfully updated content in ${args.filepath}`,
200
+ success: true
201
+ });
202
+ } catch (error) {
203
+ return this.handleError(error);
204
+ }
205
+ }
206
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * File operation tools exports
3
+ */
4
+ export * from './list.js';
5
+ export * from './content.js';
6
+
7
+ import { ObsidianClient } from '../../obsidian/client.js';
8
+ import {
9
+ ListFilesInVaultToolHandler,
10
+ ListFilesInDirToolHandler
11
+ } from './list.js';
12
+ import {
13
+ GetFileContentsToolHandler,
14
+ AppendContentToolHandler,
15
+ PatchContentToolHandler
16
+ } from './content.js';
17
+
18
+ /**
19
+ * Create all file-related tool handlers
20
+ * @param client The ObsidianClient instance
21
+ * @returns Array of file tool handlers
22
+ */
23
+ export function createFileToolHandlers(client: ObsidianClient) {
24
+ return [
25
+ new ListFilesInVaultToolHandler(client),
26
+ new ListFilesInDirToolHandler(client),
27
+ new GetFileContentsToolHandler(client),
28
+ new AppendContentToolHandler(client),
29
+ new PatchContentToolHandler(client)
30
+ ];
31
+ }