obsidian-vault-mcp 0.1.0 → 0.3.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/dist/index.js +484 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -174,6 +174,106 @@ const TOOLS = [
|
|
|
174
174
|
properties: {},
|
|
175
175
|
},
|
|
176
176
|
},
|
|
177
|
+
{
|
|
178
|
+
name: 'update_note',
|
|
179
|
+
description: 'Update an existing note content',
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
path: { type: 'string', description: 'Note path relative to vault' },
|
|
184
|
+
content: { type: 'string', description: 'New content (replaces entire file)' },
|
|
185
|
+
},
|
|
186
|
+
required: ['path', 'content'],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: 'append_to_note',
|
|
191
|
+
description: 'Append content to the end of a note (useful for daily notes)',
|
|
192
|
+
inputSchema: {
|
|
193
|
+
type: 'object',
|
|
194
|
+
properties: {
|
|
195
|
+
path: { type: 'string', description: 'Note path relative to vault' },
|
|
196
|
+
content: { type: 'string', description: 'Content to append' },
|
|
197
|
+
separator: { type: 'string', default: '\n\n', description: 'Separator before appended content' },
|
|
198
|
+
},
|
|
199
|
+
required: ['path', 'content'],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'delete_note',
|
|
204
|
+
description: 'Delete a note from the vault',
|
|
205
|
+
inputSchema: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties: {
|
|
208
|
+
path: { type: 'string', description: 'Note path relative to vault' },
|
|
209
|
+
},
|
|
210
|
+
required: ['path'],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'rename_note',
|
|
215
|
+
description: 'Rename a note and update all links pointing to it',
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
oldPath: { type: 'string', description: 'Current note path' },
|
|
220
|
+
newPath: { type: 'string', description: 'New note path' },
|
|
221
|
+
},
|
|
222
|
+
required: ['oldPath', 'newPath'],
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: 'get_outgoing_links',
|
|
227
|
+
description: 'Get all notes that the specified note links to',
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: 'object',
|
|
230
|
+
properties: {
|
|
231
|
+
path: { type: 'string', description: 'Note path relative to vault' },
|
|
232
|
+
},
|
|
233
|
+
required: ['path'],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'find_orphan_notes',
|
|
238
|
+
description: 'Find notes that have no incoming links (orphans)',
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {
|
|
242
|
+
excludeFolders: {
|
|
243
|
+
type: 'array',
|
|
244
|
+
items: { type: 'string' },
|
|
245
|
+
description: 'Folders to exclude (e.g., templates)',
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: 'find_broken_links',
|
|
252
|
+
description: 'Find all broken [[links]] in the vault',
|
|
253
|
+
inputSchema: {
|
|
254
|
+
type: 'object',
|
|
255
|
+
properties: {},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'list_all_tags',
|
|
260
|
+
description: 'List all tags in the vault with their counts',
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties: {},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: 'extract_todos',
|
|
268
|
+
description: 'Extract all TODO items from the vault',
|
|
269
|
+
inputSchema: {
|
|
270
|
+
type: 'object',
|
|
271
|
+
properties: {
|
|
272
|
+
includeCompleted: { type: 'boolean', default: false, description: 'Include completed tasks' },
|
|
273
|
+
path: { type: 'string', description: 'Limit to specific note or folder' },
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
177
277
|
];
|
|
178
278
|
// 工具实现 - 使用 Orama BM25 搜索
|
|
179
279
|
async function searchNotes(query, limit = 20) {
|
|
@@ -340,11 +440,366 @@ async function createNote(notePath, content, frontmatter) {
|
|
|
340
440
|
});
|
|
341
441
|
return `Created: ${notePath}`;
|
|
342
442
|
}
|
|
443
|
+
// 更新笔记
|
|
444
|
+
async function updateNote(notePath, content) {
|
|
445
|
+
const fullPath = path.join(vaultPath, notePath);
|
|
446
|
+
// 确保文件存在
|
|
447
|
+
await fs.access(fullPath);
|
|
448
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
449
|
+
// 需要重建索引以反映更改
|
|
450
|
+
return `Updated: ${notePath}. Consider running rebuild_index for search to reflect changes.`;
|
|
451
|
+
}
|
|
452
|
+
// 追加到笔记
|
|
453
|
+
async function appendToNote(notePath, content, separator = '\n\n') {
|
|
454
|
+
const fullPath = path.join(vaultPath, notePath);
|
|
455
|
+
const existing = await fs.readFile(fullPath, 'utf-8');
|
|
456
|
+
const newContent = existing + separator + content;
|
|
457
|
+
await fs.writeFile(fullPath, newContent, 'utf-8');
|
|
458
|
+
return `Appended to: ${notePath}`;
|
|
459
|
+
}
|
|
460
|
+
// 删除笔记
|
|
461
|
+
async function deleteNote(notePath) {
|
|
462
|
+
const fullPath = path.join(vaultPath, notePath);
|
|
463
|
+
await fs.unlink(fullPath);
|
|
464
|
+
return `Deleted: ${notePath}. Consider running rebuild_index.`;
|
|
465
|
+
}
|
|
466
|
+
// 重命名笔记并更新链接
|
|
467
|
+
async function renameNote(oldPath, newPath) {
|
|
468
|
+
const oldFullPath = path.join(vaultPath, oldPath);
|
|
469
|
+
const newFullPath = path.join(vaultPath, newPath);
|
|
470
|
+
// 确保源文件存在
|
|
471
|
+
await fs.access(oldFullPath);
|
|
472
|
+
// 确保目标目录存在
|
|
473
|
+
await fs.mkdir(path.dirname(newFullPath), { recursive: true });
|
|
474
|
+
// 移动文件
|
|
475
|
+
await fs.rename(oldFullPath, newFullPath);
|
|
476
|
+
// 更新所有指向旧路径的链接
|
|
477
|
+
const oldBasename = path.basename(oldPath, '.md');
|
|
478
|
+
const newBasename = path.basename(newPath, '.md');
|
|
479
|
+
let updatedFiles = 0;
|
|
480
|
+
async function updateLinks(dir) {
|
|
481
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
482
|
+
for (const entry of entries) {
|
|
483
|
+
const fullPath = path.join(dir, entry.name);
|
|
484
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
485
|
+
await updateLinks(fullPath);
|
|
486
|
+
}
|
|
487
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
488
|
+
try {
|
|
489
|
+
let content = await fs.readFile(fullPath, 'utf-8');
|
|
490
|
+
let modified = false;
|
|
491
|
+
// 替换 [[oldBasename]] 和 [[oldBasename|alias]]
|
|
492
|
+
const patterns = [
|
|
493
|
+
new RegExp(`\\[\\[${escapeRegExp(oldBasename)}\\]\\]`, 'g'),
|
|
494
|
+
new RegExp(`\\[\\[${escapeRegExp(oldBasename)}\\|`, 'g'),
|
|
495
|
+
new RegExp(`\\[\\[${escapeRegExp(oldPath)}\\]\\]`, 'g'),
|
|
496
|
+
new RegExp(`\\[\\[${escapeRegExp(oldPath)}\\|`, 'g'),
|
|
497
|
+
];
|
|
498
|
+
const replacements = [
|
|
499
|
+
`[[${newBasename}]]`,
|
|
500
|
+
`[[${newBasename}|`,
|
|
501
|
+
`[[${newPath}]]`,
|
|
502
|
+
`[[${newPath}|`,
|
|
503
|
+
];
|
|
504
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
505
|
+
if (patterns[i].test(content)) {
|
|
506
|
+
content = content.replace(patterns[i], replacements[i]);
|
|
507
|
+
modified = true;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (modified) {
|
|
511
|
+
await fs.writeFile(fullPath, content, 'utf-8');
|
|
512
|
+
updatedFiles++;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
// 忽略
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
await updateLinks(vaultPath);
|
|
522
|
+
return {
|
|
523
|
+
renamed: `${oldPath} -> ${newPath}`,
|
|
524
|
+
linksUpdated: updatedFiles,
|
|
525
|
+
message: 'Consider running rebuild_index.',
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
// 辅助函数:转义正则特殊字符
|
|
529
|
+
function escapeRegExp(str) {
|
|
530
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
531
|
+
}
|
|
532
|
+
// 获取外链
|
|
533
|
+
async function getOutgoingLinks(notePath) {
|
|
534
|
+
const fullPath = path.join(vaultPath, notePath);
|
|
535
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
536
|
+
// 提取 [[link]] 和 [[link|alias]]
|
|
537
|
+
const linkMatches = content.match(/\[\[([^\]]+)\]\]/g) || [];
|
|
538
|
+
const links = [];
|
|
539
|
+
for (const match of linkMatches) {
|
|
540
|
+
const inner = match.slice(2, -2);
|
|
541
|
+
const [target, alias] = inner.split('|');
|
|
542
|
+
// 检查目标是否存在
|
|
543
|
+
let targetPath = target;
|
|
544
|
+
if (!targetPath.endsWith('.md')) {
|
|
545
|
+
targetPath += '.md';
|
|
546
|
+
}
|
|
547
|
+
let exists = false;
|
|
548
|
+
try {
|
|
549
|
+
await fs.access(path.join(vaultPath, targetPath));
|
|
550
|
+
exists = true;
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
// 尝试搜索
|
|
554
|
+
try {
|
|
555
|
+
const found = await findNoteByName(target);
|
|
556
|
+
exists = found !== null;
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
exists = false;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
links.push({
|
|
563
|
+
target: target,
|
|
564
|
+
...(alias ? { alias } : {}),
|
|
565
|
+
exists,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
// 去重
|
|
569
|
+
const seen = new Set();
|
|
570
|
+
return links.filter(link => {
|
|
571
|
+
if (seen.has(link.target))
|
|
572
|
+
return false;
|
|
573
|
+
seen.add(link.target);
|
|
574
|
+
return true;
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
// 辅助函数:按名称查找笔记
|
|
578
|
+
async function findNoteByName(name) {
|
|
579
|
+
const searchName = name.endsWith('.md') ? name : `${name}.md`;
|
|
580
|
+
async function searchDir(dir) {
|
|
581
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
582
|
+
for (const entry of entries) {
|
|
583
|
+
const fullPath = path.join(dir, entry.name);
|
|
584
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
585
|
+
const found = await searchDir(fullPath);
|
|
586
|
+
if (found)
|
|
587
|
+
return found;
|
|
588
|
+
}
|
|
589
|
+
else if (entry.isFile() && entry.name === searchName) {
|
|
590
|
+
return path.relative(vaultPath, fullPath);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
return searchDir(vaultPath);
|
|
596
|
+
}
|
|
597
|
+
// 查找孤立笔记
|
|
598
|
+
async function findOrphanNotes(excludeFolders = []) {
|
|
599
|
+
const allNotes = [];
|
|
600
|
+
const linkedNotes = new Set();
|
|
601
|
+
// 收集所有笔记和链接
|
|
602
|
+
async function collectNotes(dir) {
|
|
603
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
604
|
+
for (const entry of entries) {
|
|
605
|
+
const fullPath = path.join(dir, entry.name);
|
|
606
|
+
const relativePath = path.relative(vaultPath, fullPath);
|
|
607
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
608
|
+
// 检查是否在排除列表中
|
|
609
|
+
if (excludeFolders.some(folder => relativePath.startsWith(folder))) {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
await collectNotes(fullPath);
|
|
613
|
+
}
|
|
614
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
615
|
+
allNotes.push(relativePath);
|
|
616
|
+
try {
|
|
617
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
618
|
+
const linkMatches = content.match(/\[\[([^\]|]+)/g) || [];
|
|
619
|
+
for (const match of linkMatches) {
|
|
620
|
+
const target = match.slice(2);
|
|
621
|
+
linkedNotes.add(target);
|
|
622
|
+
linkedNotes.add(target + '.md');
|
|
623
|
+
linkedNotes.add(path.basename(target));
|
|
624
|
+
linkedNotes.add(path.basename(target, '.md'));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch {
|
|
628
|
+
// 忽略
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
await collectNotes(vaultPath);
|
|
634
|
+
// 找出没有被链接的笔记
|
|
635
|
+
return allNotes.filter(note => {
|
|
636
|
+
const basename = path.basename(note, '.md');
|
|
637
|
+
return !linkedNotes.has(note) &&
|
|
638
|
+
!linkedNotes.has(basename) &&
|
|
639
|
+
!linkedNotes.has(note.replace('.md', ''));
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
// 查找损坏的链接
|
|
643
|
+
async function findBrokenLinks() {
|
|
644
|
+
const brokenLinks = [];
|
|
645
|
+
const existingNotes = new Set();
|
|
646
|
+
// 首先收集所有存在的笔记
|
|
647
|
+
async function collectExisting(dir) {
|
|
648
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
649
|
+
for (const entry of entries) {
|
|
650
|
+
const fullPath = path.join(dir, entry.name);
|
|
651
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
652
|
+
await collectExisting(fullPath);
|
|
653
|
+
}
|
|
654
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
655
|
+
const relativePath = path.relative(vaultPath, fullPath);
|
|
656
|
+
existingNotes.add(relativePath);
|
|
657
|
+
existingNotes.add(relativePath.replace('.md', ''));
|
|
658
|
+
existingNotes.add(path.basename(relativePath));
|
|
659
|
+
existingNotes.add(path.basename(relativePath, '.md'));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
await collectExisting(vaultPath);
|
|
664
|
+
// 检查所有链接
|
|
665
|
+
async function checkLinks(dir) {
|
|
666
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
667
|
+
for (const entry of entries) {
|
|
668
|
+
const fullPath = path.join(dir, entry.name);
|
|
669
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
670
|
+
await checkLinks(fullPath);
|
|
671
|
+
}
|
|
672
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
673
|
+
try {
|
|
674
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
675
|
+
const linkMatches = content.match(/\[\[([^\]|]+)/g) || [];
|
|
676
|
+
const sourcePath = path.relative(vaultPath, fullPath);
|
|
677
|
+
for (const match of linkMatches) {
|
|
678
|
+
const target = match.slice(2);
|
|
679
|
+
// 跳过外部链接和特殊链接
|
|
680
|
+
if (target.includes('://') || target.startsWith('#'))
|
|
681
|
+
continue;
|
|
682
|
+
// 检查是否存在
|
|
683
|
+
if (!existingNotes.has(target) &&
|
|
684
|
+
!existingNotes.has(target + '.md') &&
|
|
685
|
+
!existingNotes.has(path.basename(target)) &&
|
|
686
|
+
!existingNotes.has(path.basename(target, '.md'))) {
|
|
687
|
+
brokenLinks.push({ source: sourcePath, target });
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
// 忽略
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
await checkLinks(vaultPath);
|
|
698
|
+
return brokenLinks;
|
|
699
|
+
}
|
|
700
|
+
// 列出所有标签
|
|
701
|
+
async function listAllTags() {
|
|
702
|
+
const tagCounts = {};
|
|
703
|
+
async function collectTags(dir) {
|
|
704
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
705
|
+
for (const entry of entries) {
|
|
706
|
+
const fullPath = path.join(dir, entry.name);
|
|
707
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
708
|
+
await collectTags(fullPath);
|
|
709
|
+
}
|
|
710
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
711
|
+
try {
|
|
712
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
713
|
+
const tagMatches = content.match(/#[\w\u4e00-\u9fa5/-]+/g) || [];
|
|
714
|
+
for (const tag of tagMatches) {
|
|
715
|
+
const normalizedTag = tag.slice(1); // 去掉 #
|
|
716
|
+
tagCounts[normalizedTag] = (tagCounts[normalizedTag] || 0) + 1;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
// 忽略
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
await collectTags(vaultPath);
|
|
726
|
+
// 按计数排序
|
|
727
|
+
const sorted = Object.entries(tagCounts)
|
|
728
|
+
.sort((a, b) => b[1] - a[1])
|
|
729
|
+
.map(([tag, count]) => ({ tag, count }));
|
|
730
|
+
return {
|
|
731
|
+
total: sorted.length,
|
|
732
|
+
tags: sorted,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
// 提取待办事项
|
|
736
|
+
async function extractTodos(includeCompleted = false, limitPath) {
|
|
737
|
+
const todos = [];
|
|
738
|
+
const startDir = limitPath ? path.join(vaultPath, limitPath) : vaultPath;
|
|
739
|
+
async function collectTodos(dir) {
|
|
740
|
+
let entries;
|
|
741
|
+
try {
|
|
742
|
+
const stat = await fs.stat(dir);
|
|
743
|
+
if (stat.isFile()) {
|
|
744
|
+
// 如果是文件,直接处理
|
|
745
|
+
await processFile(dir);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
749
|
+
}
|
|
750
|
+
catch {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
for (const entry of entries) {
|
|
754
|
+
const fullPath = path.join(dir, entry.name);
|
|
755
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
756
|
+
await collectTodos(fullPath);
|
|
757
|
+
}
|
|
758
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
759
|
+
await processFile(fullPath);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async function processFile(fullPath) {
|
|
764
|
+
try {
|
|
765
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
766
|
+
const lines = content.split('\n');
|
|
767
|
+
const relativePath = path.relative(vaultPath, fullPath);
|
|
768
|
+
for (let i = 0; i < lines.length; i++) {
|
|
769
|
+
const line = lines[i];
|
|
770
|
+
// 匹配 - [ ] 和 - [x]
|
|
771
|
+
const unchecked = line.match(/^(\s*)-\s+\[\s*\]\s+(.+)$/);
|
|
772
|
+
const checked = line.match(/^(\s*)-\s+\[[xX]\]\s+(.+)$/);
|
|
773
|
+
if (unchecked) {
|
|
774
|
+
todos.push({
|
|
775
|
+
path: relativePath,
|
|
776
|
+
line: i + 1,
|
|
777
|
+
text: unchecked[2],
|
|
778
|
+
completed: false,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
else if (checked && includeCompleted) {
|
|
782
|
+
todos.push({
|
|
783
|
+
path: relativePath,
|
|
784
|
+
line: i + 1,
|
|
785
|
+
text: checked[2],
|
|
786
|
+
completed: true,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
catch {
|
|
792
|
+
// 忽略
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
await collectTodos(startDir);
|
|
796
|
+
return todos;
|
|
797
|
+
}
|
|
343
798
|
// 创建 MCP 服务器
|
|
344
799
|
async function main() {
|
|
345
800
|
await validateVaultPath();
|
|
346
801
|
await buildSearchIndex();
|
|
347
|
-
const server = new Server({ name: 'obsidian-vault-mcp', version: '0.
|
|
802
|
+
const server = new Server({ name: 'obsidian-vault-mcp', version: '0.3.0' }, { capabilities: { tools: {} } });
|
|
348
803
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
349
804
|
tools: TOOLS,
|
|
350
805
|
}));
|
|
@@ -378,6 +833,33 @@ async function main() {
|
|
|
378
833
|
await buildSearchIndex();
|
|
379
834
|
result = 'Index rebuilt successfully';
|
|
380
835
|
break;
|
|
836
|
+
case 'update_note':
|
|
837
|
+
result = await updateNote(args?.path, args?.content);
|
|
838
|
+
break;
|
|
839
|
+
case 'append_to_note':
|
|
840
|
+
result = await appendToNote(args?.path, args?.content, args?.separator);
|
|
841
|
+
break;
|
|
842
|
+
case 'delete_note':
|
|
843
|
+
result = await deleteNote(args?.path);
|
|
844
|
+
break;
|
|
845
|
+
case 'rename_note':
|
|
846
|
+
result = await renameNote(args?.oldPath, args?.newPath);
|
|
847
|
+
break;
|
|
848
|
+
case 'get_outgoing_links':
|
|
849
|
+
result = await getOutgoingLinks(args?.path);
|
|
850
|
+
break;
|
|
851
|
+
case 'find_orphan_notes':
|
|
852
|
+
result = await findOrphanNotes(args?.excludeFolders);
|
|
853
|
+
break;
|
|
854
|
+
case 'find_broken_links':
|
|
855
|
+
result = await findBrokenLinks();
|
|
856
|
+
break;
|
|
857
|
+
case 'list_all_tags':
|
|
858
|
+
result = await listAllTags();
|
|
859
|
+
break;
|
|
860
|
+
case 'extract_todos':
|
|
861
|
+
result = await extractTodos(args?.includeCompleted, args?.path);
|
|
862
|
+
break;
|
|
381
863
|
default:
|
|
382
864
|
throw new Error(`Unknown tool: ${name}`);
|
|
383
865
|
}
|
|
@@ -394,7 +876,7 @@ async function main() {
|
|
|
394
876
|
});
|
|
395
877
|
const transport = new StdioServerTransport();
|
|
396
878
|
await server.connect(transport);
|
|
397
|
-
console.error(`Obsidian Vault MCP Server v0.
|
|
879
|
+
console.error(`Obsidian Vault MCP Server v0.3.0 (18 tools)`);
|
|
398
880
|
console.error(`Vault: ${vaultPath}`);
|
|
399
881
|
}
|
|
400
882
|
main().catch(console.error);
|