obsidian-mcp-server 1.1.1 → 1.2.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/README.md +45 -1
- package/build/properties.js +156 -0
- package/build/propertyTools.js +156 -0
- package/build/propertyTypes.js +43 -0
- package/build/server.js +4 -1
- package/package.json +4 -2
- package/src/properties.ts +188 -0
- package/src/propertyTools.ts +177 -0
- package/src/propertyTypes.ts +70 -0
- package/src/server.ts +7 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.typescriptlang.org/)
|
|
4
4
|
[](https://modelcontextprotocol.io/)
|
|
5
|
-
[]()
|
|
6
6
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
7
|
[]()
|
|
8
8
|
[](https://github.com/cyanheads/obsidian-mcp-server)
|
|
@@ -25,6 +25,12 @@ Requires the Local REST API plugin in Obsidian.
|
|
|
25
25
|
- Configurable context boundaries and token limits
|
|
26
26
|
- Optimized query processing
|
|
27
27
|
|
|
28
|
+
### Property Management
|
|
29
|
+
- YAML frontmatter parsing and validation
|
|
30
|
+
- Intelligent property merging and updates
|
|
31
|
+
- Automatic timestamp management
|
|
32
|
+
- Custom field support
|
|
33
|
+
|
|
28
34
|
### Security & Performance
|
|
29
35
|
- API key authentication and rate limiting
|
|
30
36
|
- SSL verification options
|
|
@@ -120,6 +126,38 @@ obsidian_patch_content: {
|
|
|
120
126
|
}
|
|
121
127
|
```
|
|
122
128
|
|
|
129
|
+
### Property Management
|
|
130
|
+
```typescript
|
|
131
|
+
// Get note properties
|
|
132
|
+
obsidian_get_properties: {
|
|
133
|
+
filepath: string // Path relative to vault root
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Update note properties
|
|
137
|
+
obsidian_update_properties: {
|
|
138
|
+
filepath: string, // Path relative to vault root
|
|
139
|
+
properties: {
|
|
140
|
+
title?: string,
|
|
141
|
+
created?: string, // ISO date
|
|
142
|
+
modified?: string, // ISO date (auto-updated)
|
|
143
|
+
author?: string,
|
|
144
|
+
type?: Array<"concept" | "architecture" | "specification" |
|
|
145
|
+
"protocol" | "api" | "research" | "implementation" |
|
|
146
|
+
"guide" | "reference">,
|
|
147
|
+
tags?: string[], // Must start with #
|
|
148
|
+
status?: Array<"draft" | "in-progress" | "review" | "complete">,
|
|
149
|
+
version?: string,
|
|
150
|
+
platform?: string,
|
|
151
|
+
repository?: string, // URL
|
|
152
|
+
dependencies?: string[],
|
|
153
|
+
sources?: string[],
|
|
154
|
+
urls?: string[], // URLs
|
|
155
|
+
papers?: string[],
|
|
156
|
+
custom?: Record<string, unknown>
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
123
161
|
## Best Practices
|
|
124
162
|
|
|
125
163
|
### File Operations
|
|
@@ -134,6 +172,12 @@ obsidian_patch_content: {
|
|
|
134
172
|
- Handle large result sets
|
|
135
173
|
- Consider token limits
|
|
136
174
|
|
|
175
|
+
### Property Management
|
|
176
|
+
- Validate property values before updates
|
|
177
|
+
- Use appropriate property types
|
|
178
|
+
- Handle array merging appropriately
|
|
179
|
+
- Consider custom field implications
|
|
180
|
+
|
|
137
181
|
### Error Prevention
|
|
138
182
|
- Validate inputs thoroughly
|
|
139
183
|
- Handle API errors gracefully
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { parse, stringify } from 'yaml';
|
|
2
|
+
import { ObsidianPropertiesSchema } from './propertyTypes.js';
|
|
3
|
+
export class PropertyManager {
|
|
4
|
+
client;
|
|
5
|
+
constructor(client) {
|
|
6
|
+
this.client = client;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Parse YAML frontmatter from note content
|
|
10
|
+
*/
|
|
11
|
+
parseProperties(content) {
|
|
12
|
+
try {
|
|
13
|
+
// Extract frontmatter between --- markers
|
|
14
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
15
|
+
if (!match) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
const frontmatter = match[1];
|
|
19
|
+
const properties = parse(frontmatter);
|
|
20
|
+
// Validate against schema
|
|
21
|
+
const result = ObsidianPropertiesSchema.safeParse(properties);
|
|
22
|
+
if (!result.success) {
|
|
23
|
+
console.warn('Property validation warnings:', result.error);
|
|
24
|
+
// Return partial valid properties rather than throwing
|
|
25
|
+
return properties;
|
|
26
|
+
}
|
|
27
|
+
return result.data;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error('Error parsing properties:', error);
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Generate YAML frontmatter from properties
|
|
36
|
+
*/
|
|
37
|
+
generateProperties(properties) {
|
|
38
|
+
try {
|
|
39
|
+
// Remove undefined values
|
|
40
|
+
const cleanProperties = Object.fromEntries(Object.entries(properties).filter(([_, v]) => v !== undefined));
|
|
41
|
+
// Generate YAML
|
|
42
|
+
const yaml = stringify(cleanProperties);
|
|
43
|
+
return `---\n${yaml}---\n`;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error('Error generating properties:', error);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validate property values
|
|
52
|
+
*/
|
|
53
|
+
validateProperties(properties) {
|
|
54
|
+
const result = ObsidianPropertiesSchema.safeParse(properties);
|
|
55
|
+
if (result.success) {
|
|
56
|
+
return { valid: true, errors: [] };
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
valid: false,
|
|
60
|
+
errors: result.error.errors.map(err => `${err.path.join('.')}: ${err.message}`)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Merge new properties with existing ones
|
|
65
|
+
*/
|
|
66
|
+
mergeProperties(existing, updates) {
|
|
67
|
+
const merged = { ...existing };
|
|
68
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
69
|
+
if (value === undefined)
|
|
70
|
+
continue;
|
|
71
|
+
const currentValue = merged[key];
|
|
72
|
+
// Special handling for arrays - merge rather than replace
|
|
73
|
+
if (Array.isArray(value) && Array.isArray(currentValue)) {
|
|
74
|
+
merged[key] = [
|
|
75
|
+
...new Set([...currentValue, ...value])
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
// Special handling for custom object - deep merge
|
|
79
|
+
else if (key === 'custom' && typeof value === 'object' && value !== null) {
|
|
80
|
+
merged.custom = {
|
|
81
|
+
...merged.custom,
|
|
82
|
+
...value
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// Default case - replace value
|
|
86
|
+
else {
|
|
87
|
+
merged[key] = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Always update modified date
|
|
91
|
+
merged.modified = new Date().toISOString();
|
|
92
|
+
return merged;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get properties from a note
|
|
96
|
+
*/
|
|
97
|
+
async getProperties(filepath) {
|
|
98
|
+
try {
|
|
99
|
+
const content = await this.client.getFileContents(filepath);
|
|
100
|
+
const properties = this.parseProperties(content);
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
message: 'Properties retrieved successfully',
|
|
104
|
+
properties
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
message: `Failed to get properties: ${error instanceof Error ? error.message : String(error)}`,
|
|
111
|
+
errors: [String(error)]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Update properties of a note
|
|
117
|
+
*/
|
|
118
|
+
async updateProperties(filepath, newProperties) {
|
|
119
|
+
try {
|
|
120
|
+
// Validate new properties
|
|
121
|
+
const validation = this.validateProperties(newProperties);
|
|
122
|
+
if (!validation.valid) {
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
message: 'Invalid properties',
|
|
126
|
+
errors: validation.errors
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// Get existing content and properties
|
|
130
|
+
const content = await this.client.getFileContents(filepath);
|
|
131
|
+
const existingProperties = this.parseProperties(content);
|
|
132
|
+
// Merge properties
|
|
133
|
+
const mergedProperties = this.mergeProperties(existingProperties, newProperties);
|
|
134
|
+
// Generate new frontmatter
|
|
135
|
+
const newFrontmatter = this.generateProperties(mergedProperties);
|
|
136
|
+
// Replace existing frontmatter or prepend to file
|
|
137
|
+
const newContent = content.replace(/^---[\s\S]*?---\n/, '') || '';
|
|
138
|
+
const updatedContent = newFrontmatter + newContent;
|
|
139
|
+
// Update file
|
|
140
|
+
await this.client.updateContent(filepath, updatedContent);
|
|
141
|
+
return {
|
|
142
|
+
success: true,
|
|
143
|
+
message: 'Properties updated successfully',
|
|
144
|
+
properties: mergedProperties
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
message: `Failed to update properties: ${error instanceof Error ? error.message : String(error)}`,
|
|
151
|
+
errors: [String(error)]
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=properties.js.map
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { BaseToolHandler } from "./tools.js";
|
|
2
|
+
import { PropertyManager } from "./properties.js";
|
|
3
|
+
const TOOL_NAMES = {
|
|
4
|
+
GET_PROPERTIES: "obsidian_get_properties",
|
|
5
|
+
UPDATE_PROPERTIES: "obsidian_update_properties"
|
|
6
|
+
};
|
|
7
|
+
export class GetPropertiesToolHandler extends BaseToolHandler {
|
|
8
|
+
propertyManager;
|
|
9
|
+
constructor(client) {
|
|
10
|
+
super(TOOL_NAMES.GET_PROPERTIES, client);
|
|
11
|
+
this.propertyManager = new PropertyManager(client);
|
|
12
|
+
}
|
|
13
|
+
getToolDescription() {
|
|
14
|
+
return {
|
|
15
|
+
name: this.name,
|
|
16
|
+
description: "Get properties from an Obsidian note's frontmatter.",
|
|
17
|
+
examples: [
|
|
18
|
+
{
|
|
19
|
+
description: "Get properties from a note",
|
|
20
|
+
args: {
|
|
21
|
+
filepath: "path/to/note.md"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
inputSchema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
filepath: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Path to the note file (relative to vault root)",
|
|
31
|
+
format: "path"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
required: ["filepath"]
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async runTool(args) {
|
|
39
|
+
try {
|
|
40
|
+
const result = await this.propertyManager.getProperties(args.filepath);
|
|
41
|
+
return this.createResponse(result);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return this.handleError(error);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export class UpdatePropertiesToolHandler extends BaseToolHandler {
|
|
49
|
+
propertyManager;
|
|
50
|
+
constructor(client) {
|
|
51
|
+
super(TOOL_NAMES.UPDATE_PROPERTIES, client);
|
|
52
|
+
this.propertyManager = new PropertyManager(client);
|
|
53
|
+
}
|
|
54
|
+
getToolDescription() {
|
|
55
|
+
return {
|
|
56
|
+
name: this.name,
|
|
57
|
+
description: "Update properties in an Obsidian note's frontmatter.",
|
|
58
|
+
examples: [
|
|
59
|
+
{
|
|
60
|
+
description: "Update note properties",
|
|
61
|
+
args: {
|
|
62
|
+
filepath: "path/to/note.md",
|
|
63
|
+
properties: {
|
|
64
|
+
title: "New Title",
|
|
65
|
+
tags: ["#tag1", "#tag2"],
|
|
66
|
+
status: ["in-progress"]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
filepath: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "Path to the note file (relative to vault root)",
|
|
77
|
+
format: "path"
|
|
78
|
+
},
|
|
79
|
+
properties: {
|
|
80
|
+
type: "object",
|
|
81
|
+
description: "Properties to update",
|
|
82
|
+
properties: {
|
|
83
|
+
title: { type: "string" },
|
|
84
|
+
created: { type: "string", format: "date-time" },
|
|
85
|
+
modified: { type: "string", format: "date-time" },
|
|
86
|
+
author: { type: "string" },
|
|
87
|
+
type: {
|
|
88
|
+
type: "array",
|
|
89
|
+
items: {
|
|
90
|
+
type: "string",
|
|
91
|
+
enum: [
|
|
92
|
+
"concept",
|
|
93
|
+
"architecture",
|
|
94
|
+
"specification",
|
|
95
|
+
"protocol",
|
|
96
|
+
"api",
|
|
97
|
+
"research",
|
|
98
|
+
"implementation",
|
|
99
|
+
"guide",
|
|
100
|
+
"reference"
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
tags: {
|
|
105
|
+
type: "array",
|
|
106
|
+
items: { type: "string", pattern: "^#" }
|
|
107
|
+
},
|
|
108
|
+
status: {
|
|
109
|
+
type: "array",
|
|
110
|
+
items: {
|
|
111
|
+
type: "string",
|
|
112
|
+
enum: ["draft", "in-progress", "review", "complete"]
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
version: { type: "string" },
|
|
116
|
+
platform: { type: "string" },
|
|
117
|
+
repository: { type: "string", format: "uri" },
|
|
118
|
+
dependencies: {
|
|
119
|
+
type: "array",
|
|
120
|
+
items: { type: "string" }
|
|
121
|
+
},
|
|
122
|
+
sources: {
|
|
123
|
+
type: "array",
|
|
124
|
+
items: { type: "string" }
|
|
125
|
+
},
|
|
126
|
+
urls: {
|
|
127
|
+
type: "array",
|
|
128
|
+
items: { type: "string", format: "uri" }
|
|
129
|
+
},
|
|
130
|
+
papers: {
|
|
131
|
+
type: "array",
|
|
132
|
+
items: { type: "string" }
|
|
133
|
+
},
|
|
134
|
+
custom: {
|
|
135
|
+
type: "object",
|
|
136
|
+
additionalProperties: true
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
additionalProperties: false
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
required: ["filepath", "properties"]
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async runTool(args) {
|
|
147
|
+
try {
|
|
148
|
+
const result = await this.propertyManager.updateProperties(args.filepath, args.properties);
|
|
149
|
+
return this.createResponse(result);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
return this.handleError(error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=propertyTools.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Define validation schemas
|
|
3
|
+
export const PropertyTypeEnum = z.enum([
|
|
4
|
+
"concept",
|
|
5
|
+
"architecture",
|
|
6
|
+
"specification",
|
|
7
|
+
"protocol",
|
|
8
|
+
"api",
|
|
9
|
+
"research",
|
|
10
|
+
"implementation",
|
|
11
|
+
"guide",
|
|
12
|
+
"reference"
|
|
13
|
+
]);
|
|
14
|
+
export const StatusEnum = z.enum([
|
|
15
|
+
"draft",
|
|
16
|
+
"in-progress",
|
|
17
|
+
"review",
|
|
18
|
+
"complete"
|
|
19
|
+
]);
|
|
20
|
+
export const ObsidianPropertiesSchema = z.object({
|
|
21
|
+
// Basic Metadata
|
|
22
|
+
title: z.string().optional(),
|
|
23
|
+
created: z.string().datetime().optional(),
|
|
24
|
+
modified: z.string().datetime().optional(),
|
|
25
|
+
author: z.string().optional(),
|
|
26
|
+
// Classification
|
|
27
|
+
type: z.array(PropertyTypeEnum).optional(),
|
|
28
|
+
// Organization
|
|
29
|
+
tags: z.array(z.string().startsWith("#")).optional(),
|
|
30
|
+
// Technical Metadata
|
|
31
|
+
status: z.array(StatusEnum).optional(),
|
|
32
|
+
version: z.string().optional(),
|
|
33
|
+
platform: z.string().optional(),
|
|
34
|
+
repository: z.string().url().optional(),
|
|
35
|
+
dependencies: z.array(z.string()).optional(),
|
|
36
|
+
// References
|
|
37
|
+
sources: z.array(z.string()).optional(),
|
|
38
|
+
urls: z.array(z.string().url()).optional(),
|
|
39
|
+
papers: z.array(z.string()).optional(),
|
|
40
|
+
// Custom Fields
|
|
41
|
+
custom: z.record(z.unknown()).optional()
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=propertyTypes.js.map
|
package/build/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprot
|
|
|
5
5
|
import { ObsidianClient } from "./obsidian.js";
|
|
6
6
|
import { ObsidianError, DEFAULT_RATE_LIMIT_CONFIG } from "./types.js";
|
|
7
7
|
import { ListFilesInVaultToolHandler, ListFilesInDirToolHandler, GetFileContentsToolHandler, FindInFileToolHandler, AppendContentToolHandler, PatchContentToolHandler, ComplexSearchToolHandler } from "./tools.js";
|
|
8
|
+
import { GetPropertiesToolHandler, UpdatePropertiesToolHandler } from "./propertyTools.js";
|
|
8
9
|
// Load environment variables
|
|
9
10
|
config();
|
|
10
11
|
const API_KEY = process.env.OBSIDIAN_API_KEY;
|
|
@@ -57,7 +58,9 @@ const handlers = [
|
|
|
57
58
|
new FindInFileToolHandler(client),
|
|
58
59
|
new AppendContentToolHandler(client),
|
|
59
60
|
new PatchContentToolHandler(client),
|
|
60
|
-
new ComplexSearchToolHandler(client)
|
|
61
|
+
new ComplexSearchToolHandler(client),
|
|
62
|
+
new GetPropertiesToolHandler(client),
|
|
63
|
+
new UpdatePropertiesToolHandler(client)
|
|
61
64
|
];
|
|
62
65
|
handlers.forEach(handler => toolHandlers.set(handler.name, handler));
|
|
63
66
|
// Create MCP server
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "obsidian-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Model Context Protocol server for Obsidian integration with token-aware response handling",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
"@modelcontextprotocol/sdk": "^1.4.0",
|
|
22
22
|
"axios": "^1.7.9",
|
|
23
23
|
"dotenv": "^16.4.7",
|
|
24
|
-
"tiktoken": "^1.0.18"
|
|
24
|
+
"tiktoken": "^1.0.18",
|
|
25
|
+
"yaml": "^2.7.0",
|
|
26
|
+
"zod": "^3.24.1"
|
|
25
27
|
},
|
|
26
28
|
"devDependencies": {
|
|
27
29
|
"@types/node": "^22.10.10",
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { parse, stringify } from 'yaml';
|
|
2
|
+
import { ObsidianClient } from './obsidian.js';
|
|
3
|
+
import {
|
|
4
|
+
ObsidianProperties,
|
|
5
|
+
ObsidianPropertiesSchema,
|
|
6
|
+
PropertyManagerResult,
|
|
7
|
+
ValidationResult
|
|
8
|
+
} from './propertyTypes.js';
|
|
9
|
+
|
|
10
|
+
export class PropertyManager {
|
|
11
|
+
constructor(private client: ObsidianClient) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse YAML frontmatter from note content
|
|
15
|
+
*/
|
|
16
|
+
parseProperties(content: string): ObsidianProperties {
|
|
17
|
+
try {
|
|
18
|
+
// Extract frontmatter between --- markers
|
|
19
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
20
|
+
if (!match) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const frontmatter = match[1];
|
|
25
|
+
const properties = parse(frontmatter);
|
|
26
|
+
|
|
27
|
+
// Validate against schema
|
|
28
|
+
const result = ObsidianPropertiesSchema.safeParse(properties);
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
console.warn('Property validation warnings:', result.error);
|
|
31
|
+
// Return partial valid properties rather than throwing
|
|
32
|
+
return properties;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return result.data;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Error parsing properties:', error);
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate YAML frontmatter from properties
|
|
44
|
+
*/
|
|
45
|
+
generateProperties(properties: Partial<ObsidianProperties>): string {
|
|
46
|
+
try {
|
|
47
|
+
// Remove undefined values
|
|
48
|
+
const cleanProperties = Object.fromEntries(
|
|
49
|
+
Object.entries(properties).filter(([_, v]) => v !== undefined)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Generate YAML
|
|
53
|
+
const yaml = stringify(cleanProperties);
|
|
54
|
+
return `---\n${yaml}---\n`;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Error generating properties:', error);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate property values
|
|
63
|
+
*/
|
|
64
|
+
validateProperties(properties: Partial<ObsidianProperties>): ValidationResult {
|
|
65
|
+
const result = ObsidianPropertiesSchema.safeParse(properties);
|
|
66
|
+
|
|
67
|
+
if (result.success) {
|
|
68
|
+
return { valid: true, errors: [] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
valid: false,
|
|
73
|
+
errors: result.error.errors.map(err =>
|
|
74
|
+
`${err.path.join('.')}: ${err.message}`
|
|
75
|
+
)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Merge new properties with existing ones
|
|
81
|
+
*/
|
|
82
|
+
mergeProperties(
|
|
83
|
+
existing: ObsidianProperties,
|
|
84
|
+
updates: Partial<ObsidianProperties>
|
|
85
|
+
): ObsidianProperties {
|
|
86
|
+
const merged = { ...existing };
|
|
87
|
+
|
|
88
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
89
|
+
if (value === undefined) continue;
|
|
90
|
+
|
|
91
|
+
const currentValue = merged[key as keyof ObsidianProperties];
|
|
92
|
+
|
|
93
|
+
// Special handling for arrays - merge rather than replace
|
|
94
|
+
if (Array.isArray(value) && Array.isArray(currentValue)) {
|
|
95
|
+
merged[key as keyof ObsidianProperties] = [
|
|
96
|
+
...new Set([...currentValue, ...value])
|
|
97
|
+
] as any;
|
|
98
|
+
}
|
|
99
|
+
// Special handling for custom object - deep merge
|
|
100
|
+
else if (key === 'custom' && typeof value === 'object' && value !== null) {
|
|
101
|
+
merged.custom = {
|
|
102
|
+
...merged.custom,
|
|
103
|
+
...value
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Default case - replace value
|
|
107
|
+
else {
|
|
108
|
+
merged[key as keyof ObsidianProperties] = value as any;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Always update modified date
|
|
113
|
+
merged.modified = new Date().toISOString();
|
|
114
|
+
|
|
115
|
+
return merged;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get properties from a note
|
|
120
|
+
*/
|
|
121
|
+
async getProperties(filepath: string): Promise<PropertyManagerResult> {
|
|
122
|
+
try {
|
|
123
|
+
const content = await this.client.getFileContents(filepath);
|
|
124
|
+
const properties = this.parseProperties(content);
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
success: true,
|
|
128
|
+
message: 'Properties retrieved successfully',
|
|
129
|
+
properties
|
|
130
|
+
};
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
message: `Failed to get properties: ${error instanceof Error ? error.message : String(error)}`,
|
|
135
|
+
errors: [String(error)]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Update properties of a note
|
|
142
|
+
*/
|
|
143
|
+
async updateProperties(
|
|
144
|
+
filepath: string,
|
|
145
|
+
newProperties: Partial<ObsidianProperties>
|
|
146
|
+
): Promise<PropertyManagerResult> {
|
|
147
|
+
try {
|
|
148
|
+
// Validate new properties
|
|
149
|
+
const validation = this.validateProperties(newProperties);
|
|
150
|
+
if (!validation.valid) {
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
message: 'Invalid properties',
|
|
154
|
+
errors: validation.errors
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Get existing content and properties
|
|
159
|
+
const content = await this.client.getFileContents(filepath);
|
|
160
|
+
const existingProperties = this.parseProperties(content);
|
|
161
|
+
|
|
162
|
+
// Merge properties
|
|
163
|
+
const mergedProperties = this.mergeProperties(existingProperties, newProperties);
|
|
164
|
+
|
|
165
|
+
// Generate new frontmatter
|
|
166
|
+
const newFrontmatter = this.generateProperties(mergedProperties);
|
|
167
|
+
|
|
168
|
+
// Replace existing frontmatter or prepend to file
|
|
169
|
+
const newContent = content.replace(/^---[\s\S]*?---\n/, '') || '';
|
|
170
|
+
const updatedContent = newFrontmatter + newContent;
|
|
171
|
+
|
|
172
|
+
// Update file
|
|
173
|
+
await this.client.updateContent(filepath, updatedContent);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
success: true,
|
|
177
|
+
message: 'Properties updated successfully',
|
|
178
|
+
properties: mergedProperties
|
|
179
|
+
};
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
message: `Failed to update properties: ${error instanceof Error ? error.message : String(error)}`,
|
|
184
|
+
errors: [String(error)]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Tool, TextContent } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { ObsidianClient } from "./obsidian.js";
|
|
3
|
+
import { BaseToolHandler } from "./tools.js";
|
|
4
|
+
import { PropertyManager } from "./properties.js";
|
|
5
|
+
import { ObsidianProperties } from "./propertyTypes.js";
|
|
6
|
+
|
|
7
|
+
const TOOL_NAMES = {
|
|
8
|
+
GET_PROPERTIES: "obsidian_get_properties",
|
|
9
|
+
UPDATE_PROPERTIES: "obsidian_update_properties"
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
interface GetPropertiesArgs {
|
|
13
|
+
filepath: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface UpdatePropertiesArgs {
|
|
17
|
+
filepath: string;
|
|
18
|
+
properties: Partial<ObsidianProperties>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class GetPropertiesToolHandler extends BaseToolHandler<GetPropertiesArgs> {
|
|
22
|
+
private propertyManager: PropertyManager;
|
|
23
|
+
|
|
24
|
+
constructor(client: ObsidianClient) {
|
|
25
|
+
super(TOOL_NAMES.GET_PROPERTIES, client);
|
|
26
|
+
this.propertyManager = new PropertyManager(client);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getToolDescription(): Tool {
|
|
30
|
+
return {
|
|
31
|
+
name: this.name,
|
|
32
|
+
description: "Get properties from an Obsidian note's frontmatter.",
|
|
33
|
+
examples: [
|
|
34
|
+
{
|
|
35
|
+
description: "Get properties from a note",
|
|
36
|
+
args: {
|
|
37
|
+
filepath: "path/to/note.md"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
filepath: {
|
|
45
|
+
type: "string",
|
|
46
|
+
description: "Path to the note file (relative to vault root)",
|
|
47
|
+
format: "path"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
required: ["filepath"]
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async runTool(args: GetPropertiesArgs): Promise<Array<TextContent>> {
|
|
56
|
+
try {
|
|
57
|
+
const result = await this.propertyManager.getProperties(args.filepath);
|
|
58
|
+
return this.createResponse(result);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return this.handleError(error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class UpdatePropertiesToolHandler extends BaseToolHandler<UpdatePropertiesArgs> {
|
|
66
|
+
private propertyManager: PropertyManager;
|
|
67
|
+
|
|
68
|
+
constructor(client: ObsidianClient) {
|
|
69
|
+
super(TOOL_NAMES.UPDATE_PROPERTIES, client);
|
|
70
|
+
this.propertyManager = new PropertyManager(client);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getToolDescription(): Tool {
|
|
74
|
+
return {
|
|
75
|
+
name: this.name,
|
|
76
|
+
description: "Update properties in an Obsidian note's frontmatter.",
|
|
77
|
+
examples: [
|
|
78
|
+
{
|
|
79
|
+
description: "Update note properties",
|
|
80
|
+
args: {
|
|
81
|
+
filepath: "path/to/note.md",
|
|
82
|
+
properties: {
|
|
83
|
+
title: "New Title",
|
|
84
|
+
tags: ["#tag1", "#tag2"],
|
|
85
|
+
status: ["in-progress"]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
filepath: {
|
|
94
|
+
type: "string",
|
|
95
|
+
description: "Path to the note file (relative to vault root)",
|
|
96
|
+
format: "path"
|
|
97
|
+
},
|
|
98
|
+
properties: {
|
|
99
|
+
type: "object",
|
|
100
|
+
description: "Properties to update",
|
|
101
|
+
properties: {
|
|
102
|
+
title: { type: "string" },
|
|
103
|
+
created: { type: "string", format: "date-time" },
|
|
104
|
+
modified: { type: "string", format: "date-time" },
|
|
105
|
+
author: { type: "string" },
|
|
106
|
+
type: {
|
|
107
|
+
type: "array",
|
|
108
|
+
items: {
|
|
109
|
+
type: "string",
|
|
110
|
+
enum: [
|
|
111
|
+
"concept",
|
|
112
|
+
"architecture",
|
|
113
|
+
"specification",
|
|
114
|
+
"protocol",
|
|
115
|
+
"api",
|
|
116
|
+
"research",
|
|
117
|
+
"implementation",
|
|
118
|
+
"guide",
|
|
119
|
+
"reference"
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
tags: {
|
|
124
|
+
type: "array",
|
|
125
|
+
items: { type: "string", pattern: "^#" }
|
|
126
|
+
},
|
|
127
|
+
status: {
|
|
128
|
+
type: "array",
|
|
129
|
+
items: {
|
|
130
|
+
type: "string",
|
|
131
|
+
enum: ["draft", "in-progress", "review", "complete"]
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
version: { type: "string" },
|
|
135
|
+
platform: { type: "string" },
|
|
136
|
+
repository: { type: "string", format: "uri" },
|
|
137
|
+
dependencies: {
|
|
138
|
+
type: "array",
|
|
139
|
+
items: { type: "string" }
|
|
140
|
+
},
|
|
141
|
+
sources: {
|
|
142
|
+
type: "array",
|
|
143
|
+
items: { type: "string" }
|
|
144
|
+
},
|
|
145
|
+
urls: {
|
|
146
|
+
type: "array",
|
|
147
|
+
items: { type: "string", format: "uri" }
|
|
148
|
+
},
|
|
149
|
+
papers: {
|
|
150
|
+
type: "array",
|
|
151
|
+
items: { type: "string" }
|
|
152
|
+
},
|
|
153
|
+
custom: {
|
|
154
|
+
type: "object",
|
|
155
|
+
additionalProperties: true
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
additionalProperties: false
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
required: ["filepath", "properties"]
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async runTool(args: UpdatePropertiesArgs): Promise<Array<TextContent>> {
|
|
167
|
+
try {
|
|
168
|
+
const result = await this.propertyManager.updateProperties(
|
|
169
|
+
args.filepath,
|
|
170
|
+
args.properties
|
|
171
|
+
);
|
|
172
|
+
return this.createResponse(result);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return this.handleError(error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// Define validation schemas
|
|
4
|
+
export const PropertyTypeEnum = z.enum([
|
|
5
|
+
"concept",
|
|
6
|
+
"architecture",
|
|
7
|
+
"specification",
|
|
8
|
+
"protocol",
|
|
9
|
+
"api",
|
|
10
|
+
"research",
|
|
11
|
+
"implementation",
|
|
12
|
+
"guide",
|
|
13
|
+
"reference"
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export const StatusEnum = z.enum([
|
|
17
|
+
"draft",
|
|
18
|
+
"in-progress",
|
|
19
|
+
"review",
|
|
20
|
+
"complete"
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
export const ObsidianPropertiesSchema = z.object({
|
|
24
|
+
// Basic Metadata
|
|
25
|
+
title: z.string().optional(),
|
|
26
|
+
created: z.string().datetime().optional(),
|
|
27
|
+
modified: z.string().datetime().optional(),
|
|
28
|
+
author: z.string().optional(),
|
|
29
|
+
|
|
30
|
+
// Classification
|
|
31
|
+
type: z.array(PropertyTypeEnum).optional(),
|
|
32
|
+
|
|
33
|
+
// Organization
|
|
34
|
+
tags: z.array(z.string().startsWith("#")).optional(),
|
|
35
|
+
|
|
36
|
+
// Technical Metadata
|
|
37
|
+
status: z.array(StatusEnum).optional(),
|
|
38
|
+
version: z.string().optional(),
|
|
39
|
+
platform: z.string().optional(),
|
|
40
|
+
repository: z.string().url().optional(),
|
|
41
|
+
dependencies: z.array(z.string()).optional(),
|
|
42
|
+
|
|
43
|
+
// References
|
|
44
|
+
sources: z.array(z.string()).optional(),
|
|
45
|
+
urls: z.array(z.string().url()).optional(),
|
|
46
|
+
papers: z.array(z.string()).optional(),
|
|
47
|
+
|
|
48
|
+
// Custom Fields
|
|
49
|
+
custom: z.record(z.unknown()).optional()
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export type ObsidianProperties = z.infer<typeof ObsidianPropertiesSchema>;
|
|
53
|
+
|
|
54
|
+
export interface PropertyOperation {
|
|
55
|
+
operation: 'get' | 'update' | 'patch';
|
|
56
|
+
filepath: string;
|
|
57
|
+
properties?: Partial<ObsidianProperties>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ValidationResult {
|
|
61
|
+
valid: boolean;
|
|
62
|
+
errors: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface PropertyManagerResult {
|
|
66
|
+
success: boolean;
|
|
67
|
+
message: string;
|
|
68
|
+
properties?: ObsidianProperties;
|
|
69
|
+
errors?: string[];
|
|
70
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
PatchContentToolHandler,
|
|
22
22
|
ComplexSearchToolHandler
|
|
23
23
|
} from "./tools.js";
|
|
24
|
+
import {
|
|
25
|
+
GetPropertiesToolHandler,
|
|
26
|
+
UpdatePropertiesToolHandler
|
|
27
|
+
} from "./propertyTools.js";
|
|
24
28
|
|
|
25
29
|
// Load environment variables
|
|
26
30
|
config();
|
|
@@ -87,7 +91,9 @@ const handlers: AnyToolHandler[] = [
|
|
|
87
91
|
new FindInFileToolHandler(client),
|
|
88
92
|
new AppendContentToolHandler(client),
|
|
89
93
|
new PatchContentToolHandler(client),
|
|
90
|
-
new ComplexSearchToolHandler(client)
|
|
94
|
+
new ComplexSearchToolHandler(client),
|
|
95
|
+
new GetPropertiesToolHandler(client),
|
|
96
|
+
new UpdatePropertiesToolHandler(client)
|
|
91
97
|
];
|
|
92
98
|
|
|
93
99
|
handlers.forEach(handler => toolHandlers.set(handler.name, handler));
|