@fromsko/obsidian-mcp-server 1.1.8 → 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/dist/index.js +7 -715
- package/dist/services/notes.d.ts +28 -0
- package/dist/services/notes.js +211 -0
- package/dist/services/prompt.d.ts +3 -0
- package/dist/services/prompt.js +159 -0
- package/dist/tools/registry.d.ts +210 -0
- package/dist/tools/registry.js +208 -0
- package/dist/utils/args.d.ts +3 -0
- package/dist/utils/args.js +16 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,454 +2,11 @@
|
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import { exec } from "child_process";
|
|
6
5
|
import * as fs from "fs/promises";
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import * as path from "path";
|
|
10
|
-
import { promisify } from "util";
|
|
11
|
-
const execAsync = promisify(exec);
|
|
12
|
-
// Git Commit 类型定义
|
|
13
|
-
const COMMIT_TYPES = {
|
|
14
|
-
feat: { emoji: "✨", description: "新功能" },
|
|
15
|
-
fix: { emoji: "🐛", description: "Bug 修复" },
|
|
16
|
-
docs: { emoji: "📝", description: "文档更新" },
|
|
17
|
-
style: { emoji: "🎨", description: "代码格式" },
|
|
18
|
-
refactor: { emoji: "♻️", description: "重构" },
|
|
19
|
-
perf: { emoji: "⚡", description: "性能优化" },
|
|
20
|
-
test: { emoji: "✅", description: "测试" },
|
|
21
|
-
build: { emoji: "📦", description: "构建/依赖" },
|
|
22
|
-
ci: { emoji: "👷", description: "CI/CD" },
|
|
23
|
-
chore: { emoji: "🔧", description: "杂项" },
|
|
24
|
-
revert: { emoji: "⏪", description: "回滚" },
|
|
25
|
-
};
|
|
26
|
-
// 解析命令行参数
|
|
27
|
-
function parseArgs() {
|
|
28
|
-
const args = process.argv.slice(2);
|
|
29
|
-
let vaultPath = "";
|
|
30
|
-
for (let i = 0; i < args.length; i++) {
|
|
31
|
-
if (args[i] === "--vault" && args[i + 1]) {
|
|
32
|
-
vaultPath = args[i + 1];
|
|
33
|
-
break;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
if (!vaultPath) {
|
|
37
|
-
console.error("错误: 请使用 --vault 参数指定 Obsidian 笔记库路径");
|
|
38
|
-
console.error('用法: node dist/index.js --vault "/path/to/your/vault"');
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
return { vaultPath };
|
|
42
|
-
}
|
|
6
|
+
import { parseArgs } from "./utils/args.js";
|
|
7
|
+
import { buildTools } from "./tools/registry.js";
|
|
43
8
|
const { vaultPath: VAULT_PATH } = parseArgs();
|
|
44
|
-
|
|
45
|
-
async function parseNote(filePath) {
|
|
46
|
-
try {
|
|
47
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
48
|
-
const { data } = matter(content);
|
|
49
|
-
const relativePath = path.relative(VAULT_PATH, filePath);
|
|
50
|
-
return {
|
|
51
|
-
path: relativePath,
|
|
52
|
-
name: path.basename(filePath, ".md"),
|
|
53
|
-
category: data.category,
|
|
54
|
-
tags: data.tags,
|
|
55
|
-
summary: data.summary,
|
|
56
|
-
folder: data.folder,
|
|
57
|
-
created: data.created,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
// 获取所有笔记
|
|
65
|
-
async function getAllNotes() {
|
|
66
|
-
const files = await glob("**/*.md", {
|
|
67
|
-
cwd: VAULT_PATH,
|
|
68
|
-
ignore: [".obsidian/**", ".smart-env/**", ".windsurf/**"],
|
|
69
|
-
});
|
|
70
|
-
const notes = [];
|
|
71
|
-
for (const file of files) {
|
|
72
|
-
const meta = await parseNote(path.join(VAULT_PATH, file));
|
|
73
|
-
if (meta)
|
|
74
|
-
notes.push(meta);
|
|
75
|
-
}
|
|
76
|
-
return notes;
|
|
77
|
-
}
|
|
78
|
-
// 搜索笔记
|
|
79
|
-
async function searchNotes(query, tag, category) {
|
|
80
|
-
const allNotes = await getAllNotes();
|
|
81
|
-
return allNotes.filter((note) => {
|
|
82
|
-
const matchesQuery = !query ||
|
|
83
|
-
note.name.toLowerCase().includes(query.toLowerCase()) ||
|
|
84
|
-
note.summary?.toLowerCase().includes(query.toLowerCase());
|
|
85
|
-
const matchesTag = !tag || note.tags?.includes(tag);
|
|
86
|
-
const matchesCategory = !category || note.category === category;
|
|
87
|
-
return matchesQuery && matchesTag && matchesCategory;
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
// 读取笔记内容
|
|
91
|
-
async function readNote(notePath) {
|
|
92
|
-
const fullPath = path.join(VAULT_PATH, notePath);
|
|
93
|
-
try {
|
|
94
|
-
return await fs.readFile(fullPath, "utf-8");
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
throw new Error(`笔记不存在: ${notePath}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
// 列出文件夹内容
|
|
101
|
-
async function listFolder(folderPath = "") {
|
|
102
|
-
const targetPath = path.join(VAULT_PATH, folderPath);
|
|
103
|
-
const entries = await fs.readdir(targetPath, { withFileTypes: true });
|
|
104
|
-
const folders = [];
|
|
105
|
-
const notes = [];
|
|
106
|
-
for (const entry of entries) {
|
|
107
|
-
if (entry.name.startsWith("."))
|
|
108
|
-
continue;
|
|
109
|
-
if (entry.isDirectory()) {
|
|
110
|
-
folders.push(entry.name);
|
|
111
|
-
}
|
|
112
|
-
else if (entry.name.endsWith(".md")) {
|
|
113
|
-
const meta = await parseNote(path.join(targetPath, entry.name));
|
|
114
|
-
if (meta)
|
|
115
|
-
notes.push(meta);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return { folders, notes };
|
|
119
|
-
}
|
|
120
|
-
// 获取笔记库结构
|
|
121
|
-
async function getNoteStructure() {
|
|
122
|
-
const rootContent = await listFolder("");
|
|
123
|
-
const structure = {
|
|
124
|
-
_notes: rootContent.notes.map((n) => n.name),
|
|
125
|
-
};
|
|
126
|
-
for (const folder of rootContent.folders) {
|
|
127
|
-
try {
|
|
128
|
-
const subContent = await listFolder(folder);
|
|
129
|
-
structure[folder] = {
|
|
130
|
-
folders: subContent.folders,
|
|
131
|
-
notes: subContent.notes.map((n) => n.name),
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
catch {
|
|
135
|
-
structure[folder] = { error: "无法读取" };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return structure;
|
|
139
|
-
}
|
|
140
|
-
// 创建笔记
|
|
141
|
-
async function createNote(notePath, content) {
|
|
142
|
-
const fullPath = path.join(VAULT_PATH, notePath);
|
|
143
|
-
// 确保目录存在
|
|
144
|
-
const dir = path.dirname(fullPath);
|
|
145
|
-
await fs.mkdir(dir, { recursive: true });
|
|
146
|
-
// 检查文件是否已存在
|
|
147
|
-
try {
|
|
148
|
-
await fs.access(fullPath);
|
|
149
|
-
throw new Error(`笔记已存在: ${notePath}`);
|
|
150
|
-
}
|
|
151
|
-
catch (err) {
|
|
152
|
-
if (err.code !== "ENOENT")
|
|
153
|
-
throw err;
|
|
154
|
-
}
|
|
155
|
-
// 写入文件
|
|
156
|
-
await fs.writeFile(fullPath, content, "utf-8");
|
|
157
|
-
return `笔记创建成功: ${notePath}`;
|
|
158
|
-
}
|
|
159
|
-
// 更新笔记
|
|
160
|
-
async function updateNote(notePath, content) {
|
|
161
|
-
const fullPath = path.join(VAULT_PATH, notePath);
|
|
162
|
-
// 检查文件是否存在
|
|
163
|
-
try {
|
|
164
|
-
await fs.access(fullPath);
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
throw new Error(`笔记不存在: ${notePath}`);
|
|
168
|
-
}
|
|
169
|
-
// 写入文件
|
|
170
|
-
await fs.writeFile(fullPath, content, "utf-8");
|
|
171
|
-
return `笔记更新成功: ${notePath}`;
|
|
172
|
-
}
|
|
173
|
-
// 创建文件夹
|
|
174
|
-
async function createFolder(folderPath) {
|
|
175
|
-
const fullPath = path.join(VAULT_PATH, folderPath);
|
|
176
|
-
// 检查文件夹是否已存在
|
|
177
|
-
try {
|
|
178
|
-
const stat = await fs.stat(fullPath);
|
|
179
|
-
if (stat.isDirectory()) {
|
|
180
|
-
throw new Error(`文件夹已存在: ${folderPath}`);
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
throw new Error(`路径已存在但不是文件夹: ${folderPath}`);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
catch (err) {
|
|
187
|
-
if (err.code !== "ENOENT")
|
|
188
|
-
throw err;
|
|
189
|
-
}
|
|
190
|
-
// 创建文件夹
|
|
191
|
-
await fs.mkdir(fullPath, { recursive: true });
|
|
192
|
-
return `文件夹创建成功: ${folderPath}`;
|
|
193
|
-
}
|
|
194
|
-
// 删除笔记
|
|
195
|
-
async function deleteNote(notePath) {
|
|
196
|
-
const fullPath = path.join(VAULT_PATH, notePath);
|
|
197
|
-
// 检查文件是否存在
|
|
198
|
-
try {
|
|
199
|
-
await fs.access(fullPath);
|
|
200
|
-
}
|
|
201
|
-
catch {
|
|
202
|
-
throw new Error(`笔记不存在: ${notePath}`);
|
|
203
|
-
}
|
|
204
|
-
// 删除文件
|
|
205
|
-
await fs.unlink(fullPath);
|
|
206
|
-
return `笔记删除成功: ${notePath}`;
|
|
207
|
-
}
|
|
208
|
-
// 获取提示词使用指南
|
|
209
|
-
async function getPromptGuide() {
|
|
210
|
-
const guide = `# 🗂️ Obsidian 知识库整理助手 - 使用指南
|
|
211
|
-
|
|
212
|
-
## 一、提示词简介
|
|
213
|
-
|
|
214
|
-
这是一个用于将杂乱笔记整理为标准 Obsidian Markdown 文档的提示词。它可以:
|
|
215
|
-
- 自动生成标准的 Frontmatter(YAML)
|
|
216
|
-
- 根据内容智能分类并推荐存放路径
|
|
217
|
-
- 规范化 Markdown 结构
|
|
218
|
-
- 自动推荐相关笔记链接
|
|
219
|
-
|
|
220
|
-
## 二、如何添加提示词
|
|
221
|
-
|
|
222
|
-
### 方法 1:在 AI 对话中直接使用
|
|
223
|
-
|
|
224
|
-
1. 复制下方提示词内容
|
|
225
|
-
2. 在与 AI 的对话中粘贴提示词
|
|
226
|
-
3. 在提示词末尾粘贴你要整理的笔记内容
|
|
227
|
-
4. AI 会返回整理好的 Markdown 文档
|
|
228
|
-
|
|
229
|
-
### 方法 2:保存为 Obsidian 模板
|
|
230
|
-
|
|
231
|
-
1. 在 Obsidian 中创建一个模板文件夹(如 Templates/)
|
|
232
|
-
2. 创建新笔记,命名为「知识库整理助手提示词.md」
|
|
233
|
-
3. 将提示词内容粘贴进去
|
|
234
|
-
4. 需要时复制使用
|
|
235
|
-
|
|
236
|
-
### 方法 3:配置为 AI 工具的系统提示词
|
|
237
|
-
|
|
238
|
-
如果你使用的 AI 工具支持自定义系统提示词(如 ChatGPT、Claude 等):
|
|
239
|
-
1. 进入设置/偏好设置
|
|
240
|
-
2. 找到「自定义指令」或「系统提示词」选项
|
|
241
|
-
3. 将提示词粘贴到相应位置
|
|
242
|
-
4. 保存后,AI 会自动按照提示词格式整理笔记
|
|
243
|
-
|
|
244
|
-
## 三、提示词内容
|
|
245
|
-
|
|
246
|
-
\`\`\`
|
|
247
|
-
你是一个 Obsidian 知识库整理助手。
|
|
248
|
-
|
|
249
|
-
请将下面杂乱的笔记内容,整理为「Obsidian 可直接使用的 Markdown 文档」,要求:
|
|
250
|
-
|
|
251
|
-
## 一、生成标准 Obsidian Frontmatter(YAML)
|
|
252
|
-
|
|
253
|
-
---
|
|
254
|
-
category: <主分类>
|
|
255
|
-
tags: [tag1, tag2, tag3]
|
|
256
|
-
summary: <一句话总结>
|
|
257
|
-
icon: <lucide图标名或emoji>
|
|
258
|
-
status: <draft | active | archived>
|
|
259
|
-
folder: <推荐存放路径>
|
|
260
|
-
created: <YYYY-MM-DD>
|
|
261
|
-
---
|
|
262
|
-
|
|
263
|
-
### 分类规则(category → folder 映射)
|
|
264
|
-
|
|
265
|
-
| category | 说明 | 对应文件夹路径 |
|
|
266
|
-
|----------|------|----------------|
|
|
267
|
-
| frontend | 前端开发(JS/TS/React/Vue/CSS等) | 知识点/02-前端知识/ |
|
|
268
|
-
| backend | 后端开发(Rust/Java/Scala等) | 知识点/05-后端知识/ |
|
|
269
|
-
| hardware | 硬件/嵌入式(STM32/SystemRDL/FPGA) | 知识点/03-硬件学习/ |
|
|
270
|
-
| ai | 人工智能/LLM/MCP/提示词 | 知识点/04-人工智能/ |
|
|
271
|
-
| docker | Docker/容器化 | 知识点/01-Docker/ |
|
|
272
|
-
| devops | CI/CD/Git/部署 | 知识点/00-闲置笔记/git/ |
|
|
273
|
-
| linux | Linux 系统/命令 | 知识点/00-闲置笔记/Liunx/ |
|
|
274
|
-
| database | 数据库相关 | 知识点/00-闲置笔记/数据库/ |
|
|
275
|
-
| server | 服务器环境配置 | 知识点/00-闲置笔记/服务器安装环境/ |
|
|
276
|
-
| security | 密钥/认证/安全 | 知识点/密钥/ |
|
|
277
|
-
| music | 乐理/吉他/音乐学习 | 乐理/ |
|
|
278
|
-
| misc | 杂项/临时笔记 | 知识点/00-闲置笔记/ |
|
|
279
|
-
|
|
280
|
-
## 二、正文结构要求
|
|
281
|
-
|
|
282
|
-
- 使用清晰的 Markdown 标题(# / ## / ###)
|
|
283
|
-
- 合并重复或相近内容
|
|
284
|
-
- 技术笔记优先结构化(列表 / 步骤 / 代码块)
|
|
285
|
-
- 保留原始想法,不要过度改写语义
|
|
286
|
-
- 代码块必须标注语言类型
|
|
287
|
-
|
|
288
|
-
## 三、文件命名规范
|
|
289
|
-
|
|
290
|
-
- 格式:主题-子主题.md 或 序号-主题.md
|
|
291
|
-
- 示例:Gitea-Actions-部署方案.md、01-吉他谱寻找.md
|
|
292
|
-
- 避免特殊字符,使用中划线连接
|
|
293
|
-
|
|
294
|
-
## 四、在文末新增「🔗 Related Notes」章节
|
|
295
|
-
|
|
296
|
-
- 推测 3~5 个可能相关的笔记标题
|
|
297
|
-
- 使用 Obsidian 双链格式:[[笔记名称]]
|
|
298
|
-
|
|
299
|
-
## 五、输出要求
|
|
300
|
-
|
|
301
|
-
- 只输出整理后的 Markdown
|
|
302
|
-
- 不要解释整理过程
|
|
303
|
-
- 不要输出多余说明
|
|
304
|
-
- **必须在 Frontmatter 中包含 folder 字段,精确到子目录**
|
|
305
|
-
|
|
306
|
-
---
|
|
307
|
-
|
|
308
|
-
下面是需要整理的原始笔记内容:
|
|
309
|
-
\`\`\`
|
|
310
|
-
|
|
311
|
-
## 四、使用示例
|
|
312
|
-
|
|
313
|
-
### 输入示例
|
|
314
|
-
|
|
315
|
-
\`\`\`
|
|
316
|
-
react hooks 学习笔记
|
|
317
|
-
useState 用来管理状态
|
|
318
|
-
useEffect 处理副作用,比如请求数据
|
|
319
|
-
自定义hook要用use开头
|
|
320
|
-
\`\`\`
|
|
321
|
-
|
|
322
|
-
### 输出示例
|
|
323
|
-
|
|
324
|
-
\`\`\`markdown
|
|
325
|
-
---
|
|
326
|
-
category: frontend
|
|
327
|
-
tags: [react, hooks, useState, useEffect]
|
|
328
|
-
summary: React Hooks 核心概念学习笔记
|
|
329
|
-
icon: ⚛️
|
|
330
|
-
status: active
|
|
331
|
-
folder: 知识点/02-前端知识/React/
|
|
332
|
-
created: 2024-12-26
|
|
333
|
-
---
|
|
334
|
-
|
|
335
|
-
# React Hooks 学习笔记
|
|
336
|
-
|
|
337
|
-
## useState
|
|
338
|
-
|
|
339
|
-
用于管理组件状态。
|
|
340
|
-
|
|
341
|
-
## useEffect
|
|
342
|
-
|
|
343
|
-
处理副作用,常见用途:
|
|
344
|
-
- 请求数据
|
|
345
|
-
- 订阅事件
|
|
346
|
-
- 操作 DOM
|
|
347
|
-
|
|
348
|
-
## 自定义 Hook
|
|
349
|
-
|
|
350
|
-
- 命名必须以 \`use\` 开头
|
|
351
|
-
- 可以复用状态逻辑
|
|
352
|
-
|
|
353
|
-
---
|
|
354
|
-
|
|
355
|
-
## 🔗 Related Notes
|
|
356
|
-
|
|
357
|
-
- [[React 基础入门]]
|
|
358
|
-
- [[useState 详解]]
|
|
359
|
-
- [[useEffect 最佳实践]]
|
|
360
|
-
- [[自定义 Hook 封装技巧]]
|
|
361
|
-
\`\`\`
|
|
362
|
-
|
|
363
|
-
## 五、注意事项
|
|
364
|
-
|
|
365
|
-
1. **保持原意**:提示词会保留你的原始想法,只做结构化整理
|
|
366
|
-
2. **智能分类**:根据内容自动判断分类和存放路径
|
|
367
|
-
3. **灵活调整**:生成的 folder 路径可以根据实际情况手动调整
|
|
368
|
-
4. **批量处理**:可以一次性粘贴多段笔记内容进行整理
|
|
369
|
-
`;
|
|
370
|
-
return guide;
|
|
371
|
-
}
|
|
372
|
-
// Git 相关函数
|
|
373
|
-
async function execGit(command, cwd) {
|
|
374
|
-
try {
|
|
375
|
-
return await execAsync(`git ${command}`, { cwd: cwd || VAULT_PATH });
|
|
376
|
-
}
|
|
377
|
-
catch (error) {
|
|
378
|
-
throw new Error(`Git 命令执行失败: ${error.message}`);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
// Git 提交
|
|
382
|
-
async function gitCommit(type, description, files, cwd) {
|
|
383
|
-
const workDir = cwd || VAULT_PATH;
|
|
384
|
-
// 验证提交类型
|
|
385
|
-
if (!COMMIT_TYPES[type]) {
|
|
386
|
-
const validTypes = Object.entries(COMMIT_TYPES)
|
|
387
|
-
.map(([k, v]) => `${v.emoji} ${k}: ${v.description}`)
|
|
388
|
-
.join("\n");
|
|
389
|
-
throw new Error(`无效的提交类型: ${type}\n\n可用类型:\n${validTypes}`);
|
|
390
|
-
}
|
|
391
|
-
const { emoji } = COMMIT_TYPES[type];
|
|
392
|
-
const commitMessage = `${emoji} ${type}: ${description}`;
|
|
393
|
-
// 添加文件
|
|
394
|
-
if (files && files.length > 0) {
|
|
395
|
-
for (const file of files) {
|
|
396
|
-
await execGit(`add "${file}"`, workDir);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
else {
|
|
400
|
-
// 添加所有更改
|
|
401
|
-
await execGit("add -A", workDir);
|
|
402
|
-
}
|
|
403
|
-
// 检查是否有待提交的更改
|
|
404
|
-
const { stdout: status } = await execGit("status --porcelain", workDir);
|
|
405
|
-
if (!status.trim()) {
|
|
406
|
-
return "没有需要提交的更改";
|
|
407
|
-
}
|
|
408
|
-
// 执行提交
|
|
409
|
-
await execGit(`commit -m "${commitMessage}"`, workDir);
|
|
410
|
-
return `提交成功: ${commitMessage}`;
|
|
411
|
-
}
|
|
412
|
-
// 获取 Git 状态
|
|
413
|
-
async function gitStatus(cwd) {
|
|
414
|
-
const { stdout } = await execGit("status --short", cwd || VAULT_PATH);
|
|
415
|
-
if (!stdout.trim()) {
|
|
416
|
-
return "工作区干净,没有待提交的更改";
|
|
417
|
-
}
|
|
418
|
-
return stdout;
|
|
419
|
-
}
|
|
420
|
-
// 获取提交类型列表
|
|
421
|
-
function getCommitTypes() {
|
|
422
|
-
return Object.entries(COMMIT_TYPES)
|
|
423
|
-
.map(([type, { emoji, description }]) => `${emoji} ${type}: ${description}`)
|
|
424
|
-
.join("\n");
|
|
425
|
-
}
|
|
426
|
-
// 全文搜索
|
|
427
|
-
async function fullTextSearch(keyword) {
|
|
428
|
-
const files = await glob("**/*.md", {
|
|
429
|
-
cwd: VAULT_PATH,
|
|
430
|
-
ignore: [".obsidian/**", ".smart-env/**", ".windsurf/**"],
|
|
431
|
-
});
|
|
432
|
-
const results = [];
|
|
433
|
-
for (const file of files) {
|
|
434
|
-
try {
|
|
435
|
-
const content = await fs.readFile(path.join(VAULT_PATH, file), "utf-8");
|
|
436
|
-
const lines = content.split("\n");
|
|
437
|
-
const matches = [];
|
|
438
|
-
lines.forEach((line, index) => {
|
|
439
|
-
if (line.toLowerCase().includes(keyword.toLowerCase())) {
|
|
440
|
-
matches.push(`L${index + 1}: ${line.trim().substring(0, 100)}`);
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
if (matches.length > 0) {
|
|
444
|
-
results.push({ path: file, matches: matches.slice(0, 5) });
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
catch {
|
|
448
|
-
// 忽略读取错误
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
return results.slice(0, 20);
|
|
452
|
-
}
|
|
9
|
+
const { tools, handleTool } = buildTools(VAULT_PATH);
|
|
453
10
|
// 创建 MCP 服务器
|
|
454
11
|
const server = new Server({
|
|
455
12
|
name: "obsidian-notes-server",
|
|
@@ -461,280 +18,15 @@ const server = new Server({
|
|
|
461
18
|
});
|
|
462
19
|
// 注册工具列表
|
|
463
20
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
464
|
-
tools
|
|
465
|
-
{
|
|
466
|
-
name: "search_notes",
|
|
467
|
-
description: "搜索 Obsidian 笔记库中的笔记,支持关键词、标签、分类过滤",
|
|
468
|
-
inputSchema: {
|
|
469
|
-
type: "object",
|
|
470
|
-
properties: {
|
|
471
|
-
query: {
|
|
472
|
-
type: "string",
|
|
473
|
-
description: "搜索关键词(匹配笔记名称和摘要)",
|
|
474
|
-
},
|
|
475
|
-
tag: { type: "string", description: "按标签过滤" },
|
|
476
|
-
category: {
|
|
477
|
-
type: "string",
|
|
478
|
-
description: "按分类过滤(如 hardware, ai, backend 等)",
|
|
479
|
-
},
|
|
480
|
-
},
|
|
481
|
-
},
|
|
482
|
-
},
|
|
483
|
-
{
|
|
484
|
-
name: "read_note",
|
|
485
|
-
description: "读取指定笔记的完整内容",
|
|
486
|
-
inputSchema: {
|
|
487
|
-
type: "object",
|
|
488
|
-
properties: {
|
|
489
|
-
path: {
|
|
490
|
-
type: "string",
|
|
491
|
-
description: "笔记的相对路径(如 知识点/03-硬件学习/STM32系列选型速查.md)",
|
|
492
|
-
},
|
|
493
|
-
},
|
|
494
|
-
required: ["path"],
|
|
495
|
-
},
|
|
496
|
-
},
|
|
497
|
-
{
|
|
498
|
-
name: "list_folder",
|
|
499
|
-
description: "列出指定文件夹下的子文件夹和笔记",
|
|
500
|
-
inputSchema: {
|
|
501
|
-
type: "object",
|
|
502
|
-
properties: {
|
|
503
|
-
folder: {
|
|
504
|
-
type: "string",
|
|
505
|
-
description: "文件夹路径(留空则列出根目录)",
|
|
506
|
-
},
|
|
507
|
-
},
|
|
508
|
-
},
|
|
509
|
-
},
|
|
510
|
-
{
|
|
511
|
-
name: "get_note_structure",
|
|
512
|
-
description: "获取整个笔记库的目录结构概览",
|
|
513
|
-
inputSchema: { type: "object", properties: {} },
|
|
514
|
-
},
|
|
515
|
-
{
|
|
516
|
-
name: "full_text_search",
|
|
517
|
-
description: "在所有笔记中进行全文搜索",
|
|
518
|
-
inputSchema: {
|
|
519
|
-
type: "object",
|
|
520
|
-
properties: {
|
|
521
|
-
keyword: { type: "string", description: "要搜索的关键词" },
|
|
522
|
-
},
|
|
523
|
-
required: ["keyword"],
|
|
524
|
-
},
|
|
525
|
-
},
|
|
526
|
-
{
|
|
527
|
-
name: "create_note",
|
|
528
|
-
description: "创建新笔记",
|
|
529
|
-
inputSchema: {
|
|
530
|
-
type: "object",
|
|
531
|
-
properties: {
|
|
532
|
-
path: {
|
|
533
|
-
type: "string",
|
|
534
|
-
description: "笔记的相对路径(如 知识点/04-人工智能/MCP/自己制作的MCP/新笔记.md)",
|
|
535
|
-
},
|
|
536
|
-
content: {
|
|
537
|
-
type: "string",
|
|
538
|
-
description: "笔记内容(支持 Markdown 和 Frontmatter)",
|
|
539
|
-
},
|
|
540
|
-
},
|
|
541
|
-
required: ["path", "content"],
|
|
542
|
-
},
|
|
543
|
-
},
|
|
544
|
-
{
|
|
545
|
-
name: "update_note",
|
|
546
|
-
description: "更新已存在的笔记内容",
|
|
547
|
-
inputSchema: {
|
|
548
|
-
type: "object",
|
|
549
|
-
properties: {
|
|
550
|
-
path: { type: "string", description: "笔记的相对路径" },
|
|
551
|
-
content: { type: "string", description: "新的笔记内容" },
|
|
552
|
-
},
|
|
553
|
-
required: ["path", "content"],
|
|
554
|
-
},
|
|
555
|
-
},
|
|
556
|
-
{
|
|
557
|
-
name: "delete_note",
|
|
558
|
-
description: "删除指定笔记",
|
|
559
|
-
inputSchema: {
|
|
560
|
-
type: "object",
|
|
561
|
-
properties: {
|
|
562
|
-
path: { type: "string", description: "笔记的相对路径" },
|
|
563
|
-
},
|
|
564
|
-
required: ["path"],
|
|
565
|
-
},
|
|
566
|
-
},
|
|
567
|
-
{
|
|
568
|
-
name: "create_folder",
|
|
569
|
-
description: "创建新文件夹",
|
|
570
|
-
inputSchema: {
|
|
571
|
-
type: "object",
|
|
572
|
-
properties: {
|
|
573
|
-
path: {
|
|
574
|
-
type: "string",
|
|
575
|
-
description: "文件夹的相对路径(如 知识点/04-人工智能/MCP/新文件夹)",
|
|
576
|
-
},
|
|
577
|
-
},
|
|
578
|
-
required: ["path"],
|
|
579
|
-
},
|
|
580
|
-
},
|
|
581
|
-
{
|
|
582
|
-
name: "get_prompt_guide",
|
|
583
|
-
description: "获取 Obsidian 知识库整理助手提示词的使用指南,展示如何添加和使用这个提示词",
|
|
584
|
-
inputSchema: {
|
|
585
|
-
type: "object",
|
|
586
|
-
properties: {},
|
|
587
|
-
},
|
|
588
|
-
},
|
|
589
|
-
{
|
|
590
|
-
name: "git_commit",
|
|
591
|
-
description: "按照规范格式提交 Git 更改。格式: <emoji> <type>: <description>",
|
|
592
|
-
inputSchema: {
|
|
593
|
-
type: "object",
|
|
594
|
-
properties: {
|
|
595
|
-
type: {
|
|
596
|
-
type: "string",
|
|
597
|
-
description: "提交类型: feat(新功能), fix(Bug修复), docs(文档), style(格式), refactor(重构), perf(性能), test(测试), build(构建), ci(CI/CD), chore(杂项), revert(回滚)",
|
|
598
|
-
enum: [
|
|
599
|
-
"feat",
|
|
600
|
-
"fix",
|
|
601
|
-
"docs",
|
|
602
|
-
"style",
|
|
603
|
-
"refactor",
|
|
604
|
-
"perf",
|
|
605
|
-
"test",
|
|
606
|
-
"build",
|
|
607
|
-
"ci",
|
|
608
|
-
"chore",
|
|
609
|
-
"revert",
|
|
610
|
-
],
|
|
611
|
-
},
|
|
612
|
-
description: {
|
|
613
|
-
type: "string",
|
|
614
|
-
description: "提交描述(简洁明了,使用祈使语气)",
|
|
615
|
-
},
|
|
616
|
-
files: {
|
|
617
|
-
type: "array",
|
|
618
|
-
items: { type: "string" },
|
|
619
|
-
description: "要提交的文件列表(可选,留空则提交所有更改)",
|
|
620
|
-
},
|
|
621
|
-
cwd: {
|
|
622
|
-
type: "string",
|
|
623
|
-
description: "工作目录(可选,默认为笔记库路径)",
|
|
624
|
-
},
|
|
625
|
-
},
|
|
626
|
-
required: ["type", "description"],
|
|
627
|
-
},
|
|
628
|
-
},
|
|
629
|
-
{
|
|
630
|
-
name: "git_status",
|
|
631
|
-
description: "获取 Git 工作区状态",
|
|
632
|
-
inputSchema: {
|
|
633
|
-
type: "object",
|
|
634
|
-
properties: {
|
|
635
|
-
cwd: {
|
|
636
|
-
type: "string",
|
|
637
|
-
description: "工作目录(可选,默认为笔记库路径)",
|
|
638
|
-
},
|
|
639
|
-
},
|
|
640
|
-
},
|
|
641
|
-
},
|
|
642
|
-
{
|
|
643
|
-
name: "git_commit_types",
|
|
644
|
-
description: "获取所有可用的 Git 提交类型及其说明",
|
|
645
|
-
inputSchema: {
|
|
646
|
-
type: "object",
|
|
647
|
-
properties: {},
|
|
648
|
-
},
|
|
649
|
-
},
|
|
650
|
-
],
|
|
21
|
+
tools,
|
|
651
22
|
}));
|
|
652
23
|
// 处理工具调用
|
|
653
24
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
654
25
|
const { name, arguments: args } = request.params;
|
|
655
26
|
try {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
return {
|
|
660
|
-
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
case "read_note": {
|
|
664
|
-
const content = await readNote(args?.path);
|
|
665
|
-
return {
|
|
666
|
-
content: [{ type: "text", text: content }],
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
case "list_folder": {
|
|
670
|
-
const result = await listFolder(args?.folder || "");
|
|
671
|
-
return {
|
|
672
|
-
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
case "get_note_structure": {
|
|
676
|
-
const structure = await getNoteStructure();
|
|
677
|
-
return {
|
|
678
|
-
content: [{ type: "text", text: JSON.stringify(structure, null, 2) }],
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
case "full_text_search": {
|
|
682
|
-
const results = await fullTextSearch(args?.keyword);
|
|
683
|
-
return {
|
|
684
|
-
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
case "create_note": {
|
|
688
|
-
const result = await createNote(args?.path, args?.content);
|
|
689
|
-
return {
|
|
690
|
-
content: [{ type: "text", text: result }],
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
case "update_note": {
|
|
694
|
-
const result = await updateNote(args?.path, args?.content);
|
|
695
|
-
return {
|
|
696
|
-
content: [{ type: "text", text: result }],
|
|
697
|
-
};
|
|
698
|
-
}
|
|
699
|
-
case "delete_note": {
|
|
700
|
-
const result = await deleteNote(args?.path);
|
|
701
|
-
return {
|
|
702
|
-
content: [{ type: "text", text: result }],
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
case "create_folder": {
|
|
706
|
-
const result = await createFolder(args?.path);
|
|
707
|
-
return {
|
|
708
|
-
content: [{ type: "text", text: result }],
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
case "get_prompt_guide": {
|
|
712
|
-
const guide = await getPromptGuide();
|
|
713
|
-
return {
|
|
714
|
-
content: [{ type: "text", text: guide }],
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
case "git_commit": {
|
|
718
|
-
const result = await gitCommit(args?.type, args?.description, args?.files, args?.cwd);
|
|
719
|
-
return {
|
|
720
|
-
content: [{ type: "text", text: result }],
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
case "git_status": {
|
|
724
|
-
const status = await gitStatus(args?.cwd);
|
|
725
|
-
return {
|
|
726
|
-
content: [{ type: "text", text: status }],
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
case "git_commit_types": {
|
|
730
|
-
const types = getCommitTypes();
|
|
731
|
-
return {
|
|
732
|
-
content: [{ type: "text", text: types }],
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
default:
|
|
736
|
-
throw new Error(`未知工具: ${name}`);
|
|
737
|
-
}
|
|
27
|
+
const result = await handleTool(name, args);
|
|
28
|
+
const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
29
|
+
return { content: [{ type: "text", text }] };
|
|
738
30
|
}
|
|
739
31
|
catch (error) {
|
|
740
32
|
return {
|