obsidian-vault-mcp 0.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/README.md +141 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +400 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Obsidian Vault MCP
|
|
2
|
+
|
|
3
|
+
专为 Obsidian Vault 设计的 MCP 服务器,提供知识库专用工具。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g obsidian-vault-mcp
|
|
9
|
+
# 或
|
|
10
|
+
npx obsidian-vault-mcp --vault /path/to/vault
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 使用
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
obsidian-vault-mcp --vault /path/to/your/vault
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 提供的工具
|
|
20
|
+
|
|
21
|
+
| 工具 | 功能 |
|
|
22
|
+
|-----|------|
|
|
23
|
+
| `search_notes` | 按标题、内容、标签搜索笔记 |
|
|
24
|
+
| `get_note_metadata` | 获取笔记的 frontmatter 和元数据 |
|
|
25
|
+
| `get_backlinks` | 获取指向某笔记的所有反向链接 |
|
|
26
|
+
| `list_by_tag` | 按标签列出所有笔记 |
|
|
27
|
+
| `get_vault_structure` | 获取 vault 目录结构 |
|
|
28
|
+
| `get_recent_notes` | 获取最近修改的笔记 |
|
|
29
|
+
| `create_note` | 创建带 frontmatter 的新笔记 |
|
|
30
|
+
|
|
31
|
+
## 在 Claude Code ACP 中配置
|
|
32
|
+
|
|
33
|
+
在 Obsidian ACP 插件设置中添加 MCP 服务器:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"id": "vault",
|
|
38
|
+
"name": "Obsidian Vault",
|
|
39
|
+
"type": "stdio",
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": ["obsidian-vault-mcp", "--vault", "{VAULT_PATH}"],
|
|
42
|
+
"enabled": true
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 工具详情
|
|
47
|
+
|
|
48
|
+
### search_notes
|
|
49
|
+
|
|
50
|
+
搜索笔记,支持按标题、内容、标签搜索。
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"query": "搜索关键词",
|
|
55
|
+
"searchIn": "all", // title | content | tags | all
|
|
56
|
+
"limit": 20
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### get_note_metadata
|
|
61
|
+
|
|
62
|
+
获取笔记的完整元数据。
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"path": "folder/note.md"
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
返回:
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"path": "folder/note.md",
|
|
74
|
+
"frontmatter": { "title": "...", "tags": ["..."] },
|
|
75
|
+
"tags": ["tag1", "tag2"],
|
|
76
|
+
"links": ["other-note", "another"],
|
|
77
|
+
"modified": "2025-01-01T00:00:00Z",
|
|
78
|
+
"created": "2024-12-01T00:00:00Z",
|
|
79
|
+
"size": 1234
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### get_backlinks
|
|
84
|
+
|
|
85
|
+
获取所有链接到指定笔记的笔记。
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"path": "folder/note.md"
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### list_by_tag
|
|
94
|
+
|
|
95
|
+
获取包含特定标签的所有笔记。
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"tag": "project" // 不需要 #
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### get_vault_structure
|
|
104
|
+
|
|
105
|
+
获取 vault 的目录结构。
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"depth": 2 // 遍历深度
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### get_recent_notes
|
|
114
|
+
|
|
115
|
+
获取最近修改的笔记。
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"limit": 10,
|
|
120
|
+
"days": 7
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### create_note
|
|
125
|
+
|
|
126
|
+
创建新笔记。
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"path": "folder/new-note.md",
|
|
131
|
+
"content": "# New Note\n\nContent here...",
|
|
132
|
+
"frontmatter": {
|
|
133
|
+
"title": "New Note",
|
|
134
|
+
"tags": ["new", "example"]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Obsidian Vault MCP Server
|
|
4
|
+
*
|
|
5
|
+
* 提供 Obsidian Vault 专用的 MCP 工具
|
|
6
|
+
* 使用 Orama (BM25) 进行全文搜索
|
|
7
|
+
*/
|
|
8
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
9
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
import { create, insert, search } from '@orama/orama';
|
|
12
|
+
import * as fs from 'fs/promises';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
// 解析命令行参数
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
let vaultPath = process.cwd();
|
|
17
|
+
for (let i = 0; i < args.length; i++) {
|
|
18
|
+
if (args[i] === '--vault' && args[i + 1]) {
|
|
19
|
+
vaultPath = args[i + 1];
|
|
20
|
+
i++;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Orama 搜索索引
|
|
24
|
+
let searchIndex;
|
|
25
|
+
// 确保 vault 路径存在
|
|
26
|
+
async function validateVaultPath() {
|
|
27
|
+
try {
|
|
28
|
+
const stat = await fs.stat(vaultPath);
|
|
29
|
+
if (!stat.isDirectory()) {
|
|
30
|
+
console.error(`Error: ${vaultPath} is not a directory`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
console.error(`Error: ${vaultPath} does not exist`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// 构建搜索索引
|
|
40
|
+
async function buildSearchIndex() {
|
|
41
|
+
console.error('Building search index...');
|
|
42
|
+
const startTime = Date.now();
|
|
43
|
+
searchIndex = await create({
|
|
44
|
+
schema: {
|
|
45
|
+
path: 'string',
|
|
46
|
+
title: 'string',
|
|
47
|
+
content: 'string',
|
|
48
|
+
tags: 'string',
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
let count = 0;
|
|
52
|
+
async function indexDir(dir) {
|
|
53
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
const fullPath = path.join(dir, entry.name);
|
|
56
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
57
|
+
await indexDir(fullPath);
|
|
58
|
+
}
|
|
59
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
60
|
+
try {
|
|
61
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
62
|
+
const relativePath = path.relative(vaultPath, fullPath);
|
|
63
|
+
const title = path.basename(entry.name, '.md');
|
|
64
|
+
// 提取标签
|
|
65
|
+
const tagMatches = content.match(/#[\w\u4e00-\u9fa5/-]+/g) || [];
|
|
66
|
+
const tags = tagMatches.join(' ');
|
|
67
|
+
await insert(searchIndex, {
|
|
68
|
+
path: relativePath,
|
|
69
|
+
title,
|
|
70
|
+
content,
|
|
71
|
+
tags,
|
|
72
|
+
});
|
|
73
|
+
count++;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// 忽略读取错误
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
await indexDir(vaultPath);
|
|
82
|
+
const elapsed = Date.now() - startTime;
|
|
83
|
+
console.error(`Indexed ${count} notes in ${elapsed}ms`);
|
|
84
|
+
}
|
|
85
|
+
// 工具定义
|
|
86
|
+
const TOOLS = [
|
|
87
|
+
{
|
|
88
|
+
name: 'search_notes',
|
|
89
|
+
description: 'Search notes using BM25 algorithm (supports fuzzy matching)',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
query: { type: 'string', description: 'Search query (supports Chinese)' },
|
|
94
|
+
limit: { type: 'number', default: 20, description: 'Max results' },
|
|
95
|
+
},
|
|
96
|
+
required: ['query'],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'get_note_metadata',
|
|
101
|
+
description: 'Get frontmatter and metadata of a note',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
path: { type: 'string', description: 'Note path relative to vault' },
|
|
106
|
+
},
|
|
107
|
+
required: ['path'],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'get_backlinks',
|
|
112
|
+
description: 'Get all notes that link to the specified note',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
path: { type: 'string', description: 'Note path relative to vault' },
|
|
117
|
+
},
|
|
118
|
+
required: ['path'],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'list_by_tag',
|
|
123
|
+
description: 'List all notes with a specific tag',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: {
|
|
127
|
+
tag: { type: 'string', description: 'Tag to search for (without #)' },
|
|
128
|
+
},
|
|
129
|
+
required: ['tag'],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'get_vault_structure',
|
|
134
|
+
description: 'Get the folder structure of the vault',
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {
|
|
138
|
+
depth: { type: 'number', default: 2, description: 'Max depth to traverse' },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'get_recent_notes',
|
|
144
|
+
description: 'Get recently modified notes',
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: 'object',
|
|
147
|
+
properties: {
|
|
148
|
+
limit: { type: 'number', default: 10, description: 'Max results' },
|
|
149
|
+
days: { type: 'number', default: 7, description: 'Days to look back' },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'create_note',
|
|
155
|
+
description: 'Create a new note with optional frontmatter',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
properties: {
|
|
159
|
+
path: { type: 'string', description: 'Note path relative to vault' },
|
|
160
|
+
content: { type: 'string', description: 'Note content (markdown)' },
|
|
161
|
+
frontmatter: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
description: 'YAML frontmatter as key-value pairs',
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
required: ['path', 'content'],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'rebuild_index',
|
|
171
|
+
description: 'Rebuild the search index (use after adding/modifying notes)',
|
|
172
|
+
inputSchema: {
|
|
173
|
+
type: 'object',
|
|
174
|
+
properties: {},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
// 工具实现 - 使用 Orama BM25 搜索
|
|
179
|
+
async function searchNotes(query, limit = 20) {
|
|
180
|
+
const results = await search(searchIndex, {
|
|
181
|
+
term: query,
|
|
182
|
+
limit,
|
|
183
|
+
boost: {
|
|
184
|
+
title: 2, // 标题权重更高
|
|
185
|
+
tags: 1.5, // 标签次之
|
|
186
|
+
content: 1, // 内容权重正常
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
return results.hits.map(hit => ({
|
|
190
|
+
path: hit.document.path,
|
|
191
|
+
title: hit.document.title,
|
|
192
|
+
score: hit.score,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
async function getNoteMetadata(notePath) {
|
|
196
|
+
const fullPath = path.join(vaultPath, notePath);
|
|
197
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
198
|
+
const stat = await fs.stat(fullPath);
|
|
199
|
+
// 解析 frontmatter
|
|
200
|
+
let frontmatter = {};
|
|
201
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
202
|
+
if (fmMatch) {
|
|
203
|
+
const fmContent = fmMatch[1];
|
|
204
|
+
for (const line of fmContent.split('\n')) {
|
|
205
|
+
const colonIndex = line.indexOf(':');
|
|
206
|
+
if (colonIndex > 0) {
|
|
207
|
+
const key = line.slice(0, colonIndex).trim();
|
|
208
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
209
|
+
frontmatter[key] = value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// 提取标签
|
|
214
|
+
const tags = (content.match(/#[\w\u4e00-\u9fa5/-]+/g) || [])
|
|
215
|
+
.map(t => t.slice(1));
|
|
216
|
+
// 提取链接
|
|
217
|
+
const links = (content.match(/\[\[([^\]]+)\]\]/g) || [])
|
|
218
|
+
.map(l => l.slice(2, -2).split('|')[0]);
|
|
219
|
+
return {
|
|
220
|
+
path: notePath,
|
|
221
|
+
frontmatter,
|
|
222
|
+
tags: [...new Set(tags)],
|
|
223
|
+
links: [...new Set(links)],
|
|
224
|
+
modified: stat.mtime.toISOString(),
|
|
225
|
+
created: stat.birthtime.toISOString(),
|
|
226
|
+
size: stat.size,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
async function getBacklinks(notePath) {
|
|
230
|
+
const backlinks = [];
|
|
231
|
+
const noteBasename = path.basename(notePath, '.md');
|
|
232
|
+
async function searchDir(dir) {
|
|
233
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
const fullPath = path.join(dir, entry.name);
|
|
236
|
+
const relativePath = path.relative(vaultPath, fullPath);
|
|
237
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
238
|
+
await searchDir(fullPath);
|
|
239
|
+
}
|
|
240
|
+
else if (entry.isFile() && entry.name.endsWith('.md') && relativePath !== notePath) {
|
|
241
|
+
try {
|
|
242
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
243
|
+
if (content.includes(`[[${noteBasename}]]`) ||
|
|
244
|
+
content.includes(`[[${notePath}]]`) ||
|
|
245
|
+
content.includes(`[[${noteBasename}|`)) {
|
|
246
|
+
backlinks.push(relativePath);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// 忽略
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
await searchDir(vaultPath);
|
|
256
|
+
return backlinks;
|
|
257
|
+
}
|
|
258
|
+
async function listByTag(tag) {
|
|
259
|
+
// 使用 Orama 搜索标签
|
|
260
|
+
const searchTag = tag.startsWith('#') ? tag.slice(1) : tag;
|
|
261
|
+
const results = await search(searchIndex, {
|
|
262
|
+
term: searchTag,
|
|
263
|
+
properties: ['tags'],
|
|
264
|
+
limit: 100,
|
|
265
|
+
});
|
|
266
|
+
return results.hits.map(hit => hit.document.path);
|
|
267
|
+
}
|
|
268
|
+
async function getVaultStructure(maxDepth = 2) {
|
|
269
|
+
async function buildTree(dir, currentDepth) {
|
|
270
|
+
if (currentDepth > maxDepth) {
|
|
271
|
+
return { type: 'folder', truncated: true };
|
|
272
|
+
}
|
|
273
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
274
|
+
const children = {};
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
if (entry.name.startsWith('.'))
|
|
277
|
+
continue;
|
|
278
|
+
if (entry.isDirectory()) {
|
|
279
|
+
children[entry.name] = await buildTree(path.join(dir, entry.name), currentDepth + 1);
|
|
280
|
+
}
|
|
281
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
282
|
+
children[entry.name] = { type: 'note' };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { type: 'folder', children };
|
|
286
|
+
}
|
|
287
|
+
return buildTree(vaultPath, 0);
|
|
288
|
+
}
|
|
289
|
+
async function getRecentNotes(limit = 10, days = 7) {
|
|
290
|
+
const results = [];
|
|
291
|
+
const cutoff = new Date();
|
|
292
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
293
|
+
async function searchDir(dir) {
|
|
294
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
295
|
+
for (const entry of entries) {
|
|
296
|
+
const fullPath = path.join(dir, entry.name);
|
|
297
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
298
|
+
await searchDir(fullPath);
|
|
299
|
+
}
|
|
300
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
301
|
+
try {
|
|
302
|
+
const stat = await fs.stat(fullPath);
|
|
303
|
+
if (stat.mtime >= cutoff) {
|
|
304
|
+
results.push({
|
|
305
|
+
path: path.relative(vaultPath, fullPath),
|
|
306
|
+
modified: stat.mtime,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// 忽略
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
await searchDir(vaultPath);
|
|
317
|
+
return results
|
|
318
|
+
.sort((a, b) => b.modified.getTime() - a.modified.getTime())
|
|
319
|
+
.slice(0, limit)
|
|
320
|
+
.map(r => ({ path: r.path, modified: r.modified.toISOString() }));
|
|
321
|
+
}
|
|
322
|
+
async function createNote(notePath, content, frontmatter) {
|
|
323
|
+
const fullPath = path.join(vaultPath, notePath);
|
|
324
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
325
|
+
let finalContent = content;
|
|
326
|
+
if (frontmatter && Object.keys(frontmatter).length > 0) {
|
|
327
|
+
const fmLines = Object.entries(frontmatter)
|
|
328
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`);
|
|
329
|
+
finalContent = `---\n${fmLines.join('\n')}\n---\n\n${content}`;
|
|
330
|
+
}
|
|
331
|
+
await fs.writeFile(fullPath, finalContent, 'utf-8');
|
|
332
|
+
// 更新索引
|
|
333
|
+
const title = path.basename(notePath, '.md');
|
|
334
|
+
const tagMatches = finalContent.match(/#[\w\u4e00-\u9fa5/-]+/g) || [];
|
|
335
|
+
await insert(searchIndex, {
|
|
336
|
+
path: notePath,
|
|
337
|
+
title,
|
|
338
|
+
content: finalContent,
|
|
339
|
+
tags: tagMatches.join(' '),
|
|
340
|
+
});
|
|
341
|
+
return `Created: ${notePath}`;
|
|
342
|
+
}
|
|
343
|
+
// 创建 MCP 服务器
|
|
344
|
+
async function main() {
|
|
345
|
+
await validateVaultPath();
|
|
346
|
+
await buildSearchIndex();
|
|
347
|
+
const server = new Server({ name: 'obsidian-vault-mcp', version: '0.2.0' }, { capabilities: { tools: {} } });
|
|
348
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
349
|
+
tools: TOOLS,
|
|
350
|
+
}));
|
|
351
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
352
|
+
const { name, arguments: args } = request.params;
|
|
353
|
+
try {
|
|
354
|
+
let result;
|
|
355
|
+
switch (name) {
|
|
356
|
+
case 'search_notes':
|
|
357
|
+
result = await searchNotes(args?.query, args?.limit);
|
|
358
|
+
break;
|
|
359
|
+
case 'get_note_metadata':
|
|
360
|
+
result = await getNoteMetadata(args?.path);
|
|
361
|
+
break;
|
|
362
|
+
case 'get_backlinks':
|
|
363
|
+
result = await getBacklinks(args?.path);
|
|
364
|
+
break;
|
|
365
|
+
case 'list_by_tag':
|
|
366
|
+
result = await listByTag(args?.tag);
|
|
367
|
+
break;
|
|
368
|
+
case 'get_vault_structure':
|
|
369
|
+
result = await getVaultStructure(args?.depth);
|
|
370
|
+
break;
|
|
371
|
+
case 'get_recent_notes':
|
|
372
|
+
result = await getRecentNotes(args?.limit, args?.days);
|
|
373
|
+
break;
|
|
374
|
+
case 'create_note':
|
|
375
|
+
result = await createNote(args?.path, args?.content, args?.frontmatter);
|
|
376
|
+
break;
|
|
377
|
+
case 'rebuild_index':
|
|
378
|
+
await buildSearchIndex();
|
|
379
|
+
result = 'Index rebuilt successfully';
|
|
380
|
+
break;
|
|
381
|
+
default:
|
|
382
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
return {
|
|
390
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
391
|
+
isError: true,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
const transport = new StdioServerTransport();
|
|
396
|
+
await server.connect(transport);
|
|
397
|
+
console.error(`Obsidian Vault MCP Server v0.2.0 (BM25)`);
|
|
398
|
+
console.error(`Vault: ${vaultPath}`);
|
|
399
|
+
}
|
|
400
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "obsidian-vault-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Obsidian vault with BM25 search, backlinks, tags, and more",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"obsidian-vault-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"dev": "tsc --watch",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"obsidian",
|
|
23
|
+
"vault",
|
|
24
|
+
"ai",
|
|
25
|
+
"claude",
|
|
26
|
+
"bm25",
|
|
27
|
+
"search"
|
|
28
|
+
],
|
|
29
|
+
"author": "chyax",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
36
|
+
"@orama/orama": "^3.1.18"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^20.0.0",
|
|
40
|
+
"typescript": "^5.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|