siyuan-mcp-server 0.1.0__tar.gz
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.
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: siyuan-mcp-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 思源笔记 MCP Server - 提供思源笔记API的MCP工具接口
|
|
5
|
+
Author: leolulu
|
|
6
|
+
Author-email: leolulu <348699103@qq.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Dist: mcp
|
|
9
|
+
Requires-Dist: requests
|
|
10
|
+
Requires-Dist: detect-secrets
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# 思源笔记 MCP 服务器 (官方 SDK 版)
|
|
15
|
+
|
|
16
|
+
本项目提供了一个基于官方 MCP Python SDK 构建的思源笔记 MCP (Model Context Protocol) 服务器。它允许 AI Agent 通过一套标准化的工具与您的思源笔记知识库进行交互。
|
|
17
|
+
|
|
18
|
+
该服务器充当一座桥梁,将 MCP 的工具调用转换为对思源笔记 API 的请求,专注于提供强大的只读查询能力。
|
|
19
|
+
|
|
20
|
+
## 功能特性
|
|
21
|
+
|
|
22
|
+
- **基于官方 SDK 构建**: 确保了兼容性并遵循最佳实践。
|
|
23
|
+
- **`FastMCP` 集成**: 使用高级的 `FastMCP` 服务器,兼具简洁与强大。
|
|
24
|
+
- **生命周期管理**: 通过 `lifespan` 机制安全地管理 `SiyuanAPI` 客户端的生命周期。
|
|
25
|
+
- **装饰器驱动的工具**: 使用 `@mcp.tool()` 装饰器,工具定义清晰简洁。
|
|
26
|
+
- **兼具高层与底层工具**: 同时提供易于使用的高级查询工具和功能强大的底层 `execute_sql` 工具,以实现最大灵活性。
|
|
27
|
+
- **敏感数据自动打码**: 自动检测并打码返回内容中的敏感信息(如 API 密钥、令牌、密码等),保护用户隐私和数据安全。
|
|
28
|
+
|
|
29
|
+
## 环境要求
|
|
30
|
+
|
|
31
|
+
- **Python 3.10+**(仅开发时需要,使用 uvx 运行时无需)
|
|
32
|
+
- **uv**(推荐使用,用于 `uvx` 命令)
|
|
33
|
+
- 思源笔记桌面客户端正在运行
|
|
34
|
+
- 思源笔记 API Token(在思源笔记设置中获取)
|
|
35
|
+
|
|
36
|
+
### 安装 uv
|
|
37
|
+
|
|
38
|
+
如果尚未安装 uv:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# macOS/Linux
|
|
42
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
43
|
+
|
|
44
|
+
# Windows
|
|
45
|
+
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
46
|
+
|
|
47
|
+
# 或使用包管理器
|
|
48
|
+
pip install uv
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 安装与配置
|
|
52
|
+
|
|
53
|
+
1. **克隆仓库:**
|
|
54
|
+
```bash
|
|
55
|
+
git clone <repository-url>
|
|
56
|
+
cd siyuan-mcp-server
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
2. **安装依赖:**
|
|
60
|
+
我们推荐使用 `uv`。
|
|
61
|
+
```bash
|
|
62
|
+
uv sync
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
## 如何运行
|
|
67
|
+
|
|
68
|
+
### 方式一:使用 uvx(推荐,无需安装)
|
|
69
|
+
|
|
70
|
+
这是最简单的方式,无需预先安装,`uvx` 会自动从 PyPI 下载并运行。
|
|
71
|
+
|
|
72
|
+
**Claude Desktop 配置**:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"siyuan": {
|
|
78
|
+
"command": "uvx",
|
|
79
|
+
"args": ["siyuan-mcp-server"],
|
|
80
|
+
"env": {
|
|
81
|
+
"SIYUAN_API_TOKEN": "your_token_here"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**指定版本**:
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"mcpServers": {
|
|
93
|
+
"siyuan": {
|
|
94
|
+
"command": "uvx",
|
|
95
|
+
"args": ["siyuan-mcp-server==0.1.0"],
|
|
96
|
+
"env": {
|
|
97
|
+
"SIYUAN_API_TOKEN": "your_token_here"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**uvx 的优势**:
|
|
105
|
+
- ✅ 无需预先安装包
|
|
106
|
+
- ✅ 自动版本管理
|
|
107
|
+
- ✅ 隔离的临时环境
|
|
108
|
+
- ✅ 自动依赖管理
|
|
109
|
+
- ✅ 快速启动(利用 uv 的缓存)
|
|
110
|
+
|
|
111
|
+
### 方式二:本地开发运行
|
|
112
|
+
|
|
113
|
+
在开发期间,可以使用 `uv run` 直接运行本地代码:
|
|
114
|
+
|
|
115
|
+
**Claude Desktop 配置**:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"mcpServers": {
|
|
120
|
+
"siyuan": {
|
|
121
|
+
"command": "uv",
|
|
122
|
+
"args": ["run", "siyuan_mcp_server"],
|
|
123
|
+
"cwd": "/path/to/siyuan-mcp-server",
|
|
124
|
+
"env": {
|
|
125
|
+
"SIYUAN_API_TOKEN": "your_token_here"
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**说明**:
|
|
133
|
+
- `cwd` 指向项目根目录
|
|
134
|
+
- `uv run` 会使用项目的虚拟环境
|
|
135
|
+
- 代码修改后无需重新构建
|
|
136
|
+
|
|
137
|
+
## 已实现的工具
|
|
138
|
+
|
|
139
|
+
所有工具均在 `siyuan_mcp_server.py` 文件中定义。
|
|
140
|
+
|
|
141
|
+
- **`find_notebooks`**: 查找并列出笔记本。
|
|
142
|
+
- **`find_documents`**: 根据笔记本、标题和日期等条件查找文档。
|
|
143
|
+
- **`search_blocks`**: 根据关键词、父块、块类型和日期等条件搜索内容块。
|
|
144
|
+
- **`get_block_content`**: 获取指定块的完整 Markdown 内容。
|
|
145
|
+
- **`get_blocks_content`**: 批量获取多个块的完整内容,比多次调用 `get_block_content` 更高效。
|
|
146
|
+
- **`execute_sql`**: 直接对数据库执行只读的 `SELECT` 查询。
|
|
147
|
+
|
|
148
|
+
## 未来计划
|
|
149
|
+
|
|
150
|
+
- [ ] 添加更多高级查询工具
|
|
151
|
+
- [ ] 支持写入操作(创建/更新文档)
|
|
152
|
+
- [ ] 添加单元测试
|
|
153
|
+
|
|
154
|
+
## 安全特性
|
|
155
|
+
|
|
156
|
+
本项目内置了敏感数据保护机制,通过 `tools.py` 中的 `mask_sensitive_data` 函数实现:
|
|
157
|
+
|
|
158
|
+
- **自动检测敏感信息**: 能够识别多种格式的敏感数据,包括:
|
|
159
|
+
- AWS Access Key ID 和 Secret Access Key
|
|
160
|
+
- GitHub Personal Access Token
|
|
161
|
+
- JWT Token
|
|
162
|
+
- UUID
|
|
163
|
+
- API Key
|
|
164
|
+
- OAuth tokens
|
|
165
|
+
- Private Key
|
|
166
|
+
- 数据库连接字符串中的密码
|
|
167
|
+
- Base64 编码的密钥
|
|
168
|
+
- 十六进制密钥
|
|
169
|
+
- 其他通用密钥格式
|
|
170
|
+
|
|
171
|
+
- **智能打码策略**: 采用中间部分打码的方式,保留字符串的开头和结尾部分,便于识别但不泄露完整信息。
|
|
172
|
+
|
|
173
|
+
- **全面保护**: 在所有返回用户数据的内容中自动应用打码处理,包括:
|
|
174
|
+
- 块内容搜索结果
|
|
175
|
+
- 块详细内容
|
|
176
|
+
- SQL 查询结果
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# 思源笔记 MCP 服务器 (官方 SDK 版)
|
|
2
|
+
|
|
3
|
+
本项目提供了一个基于官方 MCP Python SDK 构建的思源笔记 MCP (Model Context Protocol) 服务器。它允许 AI Agent 通过一套标准化的工具与您的思源笔记知识库进行交互。
|
|
4
|
+
|
|
5
|
+
该服务器充当一座桥梁,将 MCP 的工具调用转换为对思源笔记 API 的请求,专注于提供强大的只读查询能力。
|
|
6
|
+
|
|
7
|
+
## 功能特性
|
|
8
|
+
|
|
9
|
+
- **基于官方 SDK 构建**: 确保了兼容性并遵循最佳实践。
|
|
10
|
+
- **`FastMCP` 集成**: 使用高级的 `FastMCP` 服务器,兼具简洁与强大。
|
|
11
|
+
- **生命周期管理**: 通过 `lifespan` 机制安全地管理 `SiyuanAPI` 客户端的生命周期。
|
|
12
|
+
- **装饰器驱动的工具**: 使用 `@mcp.tool()` 装饰器,工具定义清晰简洁。
|
|
13
|
+
- **兼具高层与底层工具**: 同时提供易于使用的高级查询工具和功能强大的底层 `execute_sql` 工具,以实现最大灵活性。
|
|
14
|
+
- **敏感数据自动打码**: 自动检测并打码返回内容中的敏感信息(如 API 密钥、令牌、密码等),保护用户隐私和数据安全。
|
|
15
|
+
|
|
16
|
+
## 环境要求
|
|
17
|
+
|
|
18
|
+
- **Python 3.10+**(仅开发时需要,使用 uvx 运行时无需)
|
|
19
|
+
- **uv**(推荐使用,用于 `uvx` 命令)
|
|
20
|
+
- 思源笔记桌面客户端正在运行
|
|
21
|
+
- 思源笔记 API Token(在思源笔记设置中获取)
|
|
22
|
+
|
|
23
|
+
### 安装 uv
|
|
24
|
+
|
|
25
|
+
如果尚未安装 uv:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# macOS/Linux
|
|
29
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
30
|
+
|
|
31
|
+
# Windows
|
|
32
|
+
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
33
|
+
|
|
34
|
+
# 或使用包管理器
|
|
35
|
+
pip install uv
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 安装与配置
|
|
39
|
+
|
|
40
|
+
1. **克隆仓库:**
|
|
41
|
+
```bash
|
|
42
|
+
git clone <repository-url>
|
|
43
|
+
cd siyuan-mcp-server
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
2. **安装依赖:**
|
|
47
|
+
我们推荐使用 `uv`。
|
|
48
|
+
```bash
|
|
49
|
+
uv sync
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## 如何运行
|
|
54
|
+
|
|
55
|
+
### 方式一:使用 uvx(推荐,无需安装)
|
|
56
|
+
|
|
57
|
+
这是最简单的方式,无需预先安装,`uvx` 会自动从 PyPI 下载并运行。
|
|
58
|
+
|
|
59
|
+
**Claude Desktop 配置**:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"siyuan": {
|
|
65
|
+
"command": "uvx",
|
|
66
|
+
"args": ["siyuan-mcp-server"],
|
|
67
|
+
"env": {
|
|
68
|
+
"SIYUAN_API_TOKEN": "your_token_here"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**指定版本**:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"mcpServers": {
|
|
80
|
+
"siyuan": {
|
|
81
|
+
"command": "uvx",
|
|
82
|
+
"args": ["siyuan-mcp-server==0.1.0"],
|
|
83
|
+
"env": {
|
|
84
|
+
"SIYUAN_API_TOKEN": "your_token_here"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**uvx 的优势**:
|
|
92
|
+
- ✅ 无需预先安装包
|
|
93
|
+
- ✅ 自动版本管理
|
|
94
|
+
- ✅ 隔离的临时环境
|
|
95
|
+
- ✅ 自动依赖管理
|
|
96
|
+
- ✅ 快速启动(利用 uv 的缓存)
|
|
97
|
+
|
|
98
|
+
### 方式二:本地开发运行
|
|
99
|
+
|
|
100
|
+
在开发期间,可以使用 `uv run` 直接运行本地代码:
|
|
101
|
+
|
|
102
|
+
**Claude Desktop 配置**:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"mcpServers": {
|
|
107
|
+
"siyuan": {
|
|
108
|
+
"command": "uv",
|
|
109
|
+
"args": ["run", "siyuan_mcp_server"],
|
|
110
|
+
"cwd": "/path/to/siyuan-mcp-server",
|
|
111
|
+
"env": {
|
|
112
|
+
"SIYUAN_API_TOKEN": "your_token_here"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**说明**:
|
|
120
|
+
- `cwd` 指向项目根目录
|
|
121
|
+
- `uv run` 会使用项目的虚拟环境
|
|
122
|
+
- 代码修改后无需重新构建
|
|
123
|
+
|
|
124
|
+
## 已实现的工具
|
|
125
|
+
|
|
126
|
+
所有工具均在 `siyuan_mcp_server.py` 文件中定义。
|
|
127
|
+
|
|
128
|
+
- **`find_notebooks`**: 查找并列出笔记本。
|
|
129
|
+
- **`find_documents`**: 根据笔记本、标题和日期等条件查找文档。
|
|
130
|
+
- **`search_blocks`**: 根据关键词、父块、块类型和日期等条件搜索内容块。
|
|
131
|
+
- **`get_block_content`**: 获取指定块的完整 Markdown 内容。
|
|
132
|
+
- **`get_blocks_content`**: 批量获取多个块的完整内容,比多次调用 `get_block_content` 更高效。
|
|
133
|
+
- **`execute_sql`**: 直接对数据库执行只读的 `SELECT` 查询。
|
|
134
|
+
|
|
135
|
+
## 未来计划
|
|
136
|
+
|
|
137
|
+
- [ ] 添加更多高级查询工具
|
|
138
|
+
- [ ] 支持写入操作(创建/更新文档)
|
|
139
|
+
- [ ] 添加单元测试
|
|
140
|
+
|
|
141
|
+
## 安全特性
|
|
142
|
+
|
|
143
|
+
本项目内置了敏感数据保护机制,通过 `tools.py` 中的 `mask_sensitive_data` 函数实现:
|
|
144
|
+
|
|
145
|
+
- **自动检测敏感信息**: 能够识别多种格式的敏感数据,包括:
|
|
146
|
+
- AWS Access Key ID 和 Secret Access Key
|
|
147
|
+
- GitHub Personal Access Token
|
|
148
|
+
- JWT Token
|
|
149
|
+
- UUID
|
|
150
|
+
- API Key
|
|
151
|
+
- OAuth tokens
|
|
152
|
+
- Private Key
|
|
153
|
+
- 数据库连接字符串中的密码
|
|
154
|
+
- Base64 编码的密钥
|
|
155
|
+
- 十六进制密钥
|
|
156
|
+
- 其他通用密钥格式
|
|
157
|
+
|
|
158
|
+
- **智能打码策略**: 采用中间部分打码的方式,保留字符串的开头和结尾部分,便于识别但不泄露完整信息。
|
|
159
|
+
|
|
160
|
+
- **全面保护**: 在所有返回用户数据的内容中自动应用打码处理,包括:
|
|
161
|
+
- 块内容搜索结果
|
|
162
|
+
- 块详细内容
|
|
163
|
+
- SQL 查询结果
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "siyuan-mcp-server"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "思源笔记 MCP Server - 提供思源笔记API的MCP工具接口"
|
|
5
|
+
authors = [
|
|
6
|
+
{ name = "leolulu", email = "348699103@qq.com" }
|
|
7
|
+
]
|
|
8
|
+
license = { text = "MIT" }
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
readme-content-type = "text/markdown"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"mcp",
|
|
14
|
+
"requests",
|
|
15
|
+
"detect-secrets",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.9.26,<0.10.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[tool.uv.build-backend]
|
|
23
|
+
module-name = "siyuan_mcp_server"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
siyuan-mcp-server = "siyuan_mcp_server:main"
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import AsyncIterator
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import mcp
|
|
8
|
+
import requests
|
|
9
|
+
from mcp.server.fastmcp import Context, FastMCP
|
|
10
|
+
from mcp.server.session import ServerSession
|
|
11
|
+
|
|
12
|
+
from .tools import mask_sensitive_data
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# --- 1. Siyuan API Wrapper ---
|
|
16
|
+
class SiyuanAPI:
|
|
17
|
+
def __init__(self, api_token: str, base_url: str = "http://127.0.0.1:6806"):
|
|
18
|
+
self.base_url = base_url
|
|
19
|
+
self.api_token = api_token
|
|
20
|
+
self.headers = {
|
|
21
|
+
"Authorization": f"Token {self.api_token}",
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def _post(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None) -> Any:
|
|
26
|
+
url = f"{self.base_url}{endpoint}"
|
|
27
|
+
try:
|
|
28
|
+
response = requests.post(url, json=json_data, headers=self.headers)
|
|
29
|
+
response.raise_for_status()
|
|
30
|
+
api_response = response.json()
|
|
31
|
+
if api_response.get("code") != 0:
|
|
32
|
+
raise Exception(f"Siyuan API Error: {api_response.get('msg')}")
|
|
33
|
+
return api_response.get("data")
|
|
34
|
+
except requests.exceptions.RequestException as e:
|
|
35
|
+
raise ConnectionError(f"Failed to connect to Siyuan API: {e}")
|
|
36
|
+
|
|
37
|
+
def execute_sql(self, query: str) -> List[Dict[str, Any]]:
|
|
38
|
+
if not query.strip().upper().startswith("SELECT"):
|
|
39
|
+
raise ValueError("Only SELECT statements are allowed for security reasons.")
|
|
40
|
+
payload = {"stmt": query}
|
|
41
|
+
result = self._post("/api/query/sql", payload)
|
|
42
|
+
if not isinstance(result, list):
|
|
43
|
+
raise TypeError(f"Expected a list from SQL query, but got {type(result)}")
|
|
44
|
+
return result
|
|
45
|
+
|
|
46
|
+
def get_block_kramdown(self, block_id: str) -> Dict[str, Any]:
|
|
47
|
+
result = self._post("/api/block/getBlockKramdown", {"id": block_id})
|
|
48
|
+
if not isinstance(result, dict):
|
|
49
|
+
raise TypeError(f"Expected a dict for block content, but got {type(result)}")
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
def list_notebooks(self) -> List[Dict[str, Any]]:
|
|
53
|
+
"""获取笔记本列表"""
|
|
54
|
+
result = self._post("/api/notebook/lsNotebooks")
|
|
55
|
+
if not isinstance(result, dict) or "notebooks" not in result:
|
|
56
|
+
raise TypeError(f"Expected a dict with 'notebooks' key, but got {type(result)}")
|
|
57
|
+
return result["notebooks"]
|
|
58
|
+
|
|
59
|
+
def get_blocks_kramdown(self, block_ids: List[str]) -> List[Dict[str, Any]]:
|
|
60
|
+
"""批量获取多个块的内容"""
|
|
61
|
+
results = []
|
|
62
|
+
for block_id in block_ids:
|
|
63
|
+
try:
|
|
64
|
+
result = self.get_block_kramdown(block_id)
|
|
65
|
+
results.append(result)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
# 如果某个块获取失败,记录错误但继续处理其他块
|
|
68
|
+
results.append({
|
|
69
|
+
"id": block_id,
|
|
70
|
+
"error": str(e)
|
|
71
|
+
})
|
|
72
|
+
return results
|
|
73
|
+
|
|
74
|
+
# --- 2. Application Context ---
|
|
75
|
+
@dataclass
|
|
76
|
+
class AppContext:
|
|
77
|
+
siyuan_api: SiyuanAPI
|
|
78
|
+
|
|
79
|
+
# --- 3. Lifespan Management ---
|
|
80
|
+
@asynccontextmanager
|
|
81
|
+
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
|
|
82
|
+
api_token = os.getenv("SIYUAN_API_TOKEN")
|
|
83
|
+
if not api_token:
|
|
84
|
+
raise ValueError("SIYUAN_API_TOKEN environment variable not set.")
|
|
85
|
+
|
|
86
|
+
siyuan_api = SiyuanAPI(api_token=api_token)
|
|
87
|
+
try:
|
|
88
|
+
print("Siyuan API client initialized.")
|
|
89
|
+
yield AppContext(siyuan_api=siyuan_api)
|
|
90
|
+
finally:
|
|
91
|
+
print("Siyuan MCP Server shutting down.")
|
|
92
|
+
|
|
93
|
+
# --- 4. MCP Server Instance ---
|
|
94
|
+
mcp = FastMCP(
|
|
95
|
+
"siyuan-mcp-server",
|
|
96
|
+
lifespan=app_lifespan
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# --- 5. Tool Definitions ---
|
|
100
|
+
@mcp.tool()
|
|
101
|
+
def find_notebooks(
|
|
102
|
+
ctx: Context[ServerSession, AppContext],
|
|
103
|
+
name: Optional[str] = None,
|
|
104
|
+
limit: int = 10
|
|
105
|
+
) -> list:
|
|
106
|
+
"""查找并列出思源笔记中的笔记本。
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
ctx: MCP 上下文对象,自动注入。
|
|
110
|
+
name (Optional[str]): 用于模糊搜索笔记本的名称。如果省略,则列出所有笔记本。
|
|
111
|
+
limit (int): 返回结果的最大数量,默认为 10。
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
list: 包含笔记本信息的字典列表,每个字典包含 'name' 和 'id'。
|
|
115
|
+
"""
|
|
116
|
+
api = ctx.request_context.lifespan_context.siyuan_api
|
|
117
|
+
notebooks = api.list_notebooks()
|
|
118
|
+
|
|
119
|
+
# 如果指定了名称,则进行过滤
|
|
120
|
+
if name:
|
|
121
|
+
notebooks = [nb for nb in notebooks if name.lower() in nb.get("name", "").lower()]
|
|
122
|
+
|
|
123
|
+
# 限制返回结果数量
|
|
124
|
+
return notebooks[:limit]
|
|
125
|
+
|
|
126
|
+
@mcp.tool()
|
|
127
|
+
def find_documents(
|
|
128
|
+
ctx: Context[ServerSession, AppContext],
|
|
129
|
+
notebook_id: Optional[str] = None,
|
|
130
|
+
title: Optional[str] = None,
|
|
131
|
+
created_after: Optional[str] = None,
|
|
132
|
+
updated_after: Optional[str] = None,
|
|
133
|
+
limit: int = 10,
|
|
134
|
+
) -> list:
|
|
135
|
+
"""在指定的笔记本中查找文档,支持多种过滤条件。
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
ctx: MCP 上下文对象,自动注入。
|
|
139
|
+
notebook_id (Optional[str]): 在哪个笔记本中查找。如果省略,则在所有打开的笔记本中查找。
|
|
140
|
+
title (Optional[str]): 根据文档标题进行模糊匹配。
|
|
141
|
+
created_after (Optional[str]): 查找在此日期之后创建的文档,格式为 'YYYYMMDDHHMMSS'。
|
|
142
|
+
updated_after (Optional[str]): 查找在此日期之后更新的文档,格式为 'YYYYMMDDHHMMSS'。
|
|
143
|
+
limit (int): 返回结果的最大数量,默认为 10。
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
list: 包含文档信息的字典列表,每个字典包含 'name', 'id', 和 'hpath'。
|
|
147
|
+
"""
|
|
148
|
+
api = ctx.request_context.lifespan_context.siyuan_api
|
|
149
|
+
query = "SELECT name, id, hpath FROM blocks WHERE type = 'd'"
|
|
150
|
+
conditions = []
|
|
151
|
+
if notebook_id:
|
|
152
|
+
sanitized_id = notebook_id.replace("'", "''")
|
|
153
|
+
conditions.append(f"box = '{sanitized_id}'")
|
|
154
|
+
if title:
|
|
155
|
+
sanitized_title = title.replace("'", "''")
|
|
156
|
+
conditions.append(f"name LIKE '%{sanitized_title}%'")
|
|
157
|
+
if created_after:
|
|
158
|
+
sanitized_date = created_after.replace("'", "''")
|
|
159
|
+
conditions.append(f"created > '{sanitized_date}'")
|
|
160
|
+
if updated_after:
|
|
161
|
+
sanitized_date = updated_after.replace("'", "''")
|
|
162
|
+
conditions.append(f"updated > '{sanitized_date}'")
|
|
163
|
+
if conditions:
|
|
164
|
+
query += " AND " + " AND ".join(conditions)
|
|
165
|
+
query += f" LIMIT {limit}"
|
|
166
|
+
return api.execute_sql(query)
|
|
167
|
+
|
|
168
|
+
@mcp.tool()
|
|
169
|
+
def search_blocks(
|
|
170
|
+
ctx: Context[ServerSession, AppContext],
|
|
171
|
+
query: str,
|
|
172
|
+
parent_id: Optional[str] = None,
|
|
173
|
+
block_type: Optional[str] = None,
|
|
174
|
+
created_after: Optional[str] = None,
|
|
175
|
+
updated_after: Optional[str] = None,
|
|
176
|
+
limit: int = 20,
|
|
177
|
+
) -> list:
|
|
178
|
+
"""根据关键词、类型等多种条件在思源笔记中搜索内容块。
|
|
179
|
+
|
|
180
|
+
这是最核心和最灵活的查询工具。
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
ctx: MCP 上下文对象,自动注入。
|
|
184
|
+
query (str): 在块内容中搜索的关键词。
|
|
185
|
+
parent_id (Optional[str]): 在哪个文档或父块下进行搜索。如果省略,则全局搜索。
|
|
186
|
+
block_type (Optional[str]): 限制块的类型,例如 'p' (段落), 'h' (标题), 'l' (列表)。
|
|
187
|
+
created_after (Optional[str]): 查找在此日期之后创建的块,格式为 'YYYYMMDDHHMMSS'。
|
|
188
|
+
updated_after (Optional[str]): 查找在此日期之后更新的块,格式为 'YYYYMMDDHHMMSS'。
|
|
189
|
+
limit (int): 返回结果的最大数量,默认为 20。
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
list: 包含块信息的字典列表。
|
|
193
|
+
"""
|
|
194
|
+
api = ctx.request_context.lifespan_context.siyuan_api
|
|
195
|
+
sql_query = "SELECT id, content, type, subtype, hpath FROM blocks WHERE content LIKE ?"
|
|
196
|
+
params = [f"%{query}%"]
|
|
197
|
+
if parent_id:
|
|
198
|
+
sql_query += " AND parent_id = ?"
|
|
199
|
+
params.append(parent_id)
|
|
200
|
+
if block_type:
|
|
201
|
+
sql_query += " AND type = ?"
|
|
202
|
+
params.append(block_type)
|
|
203
|
+
if created_after:
|
|
204
|
+
sql_query += " AND created > ?"
|
|
205
|
+
params.append(created_after)
|
|
206
|
+
if updated_after:
|
|
207
|
+
sql_query += " AND updated > ?"
|
|
208
|
+
params.append(updated_after)
|
|
209
|
+
sql_query += f" LIMIT {limit}"
|
|
210
|
+
for param in params:
|
|
211
|
+
sanitized_param = str(param).replace("'", "''")
|
|
212
|
+
sql_query = sql_query.replace("?", f"'{sanitized_param}'", 1)
|
|
213
|
+
results = api.execute_sql(sql_query)
|
|
214
|
+
|
|
215
|
+
# 对搜索结果中的内容进行打码处理
|
|
216
|
+
for result in results:
|
|
217
|
+
if isinstance(result, dict):
|
|
218
|
+
if "content" in result:
|
|
219
|
+
result["content"] = mask_sensitive_data(result["content"])
|
|
220
|
+
|
|
221
|
+
return results
|
|
222
|
+
|
|
223
|
+
@mcp.tool()
|
|
224
|
+
def get_block_content(
|
|
225
|
+
ctx: Context[ServerSession, AppContext],
|
|
226
|
+
block_id: str
|
|
227
|
+
) -> dict:
|
|
228
|
+
"""获取指定 ID 的块的完整内容。
|
|
229
|
+
|
|
230
|
+
在通过 search_blocks 找到相关块后,使用此工具读取其详细内容。
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
ctx: MCP 上下文对象,自动注入。
|
|
234
|
+
block_id (str): 要获取内容的块的 ID。
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
dict: 包含块 Kramdown 源码等信息的字典。
|
|
238
|
+
"""
|
|
239
|
+
api = ctx.request_context.lifespan_context.siyuan_api
|
|
240
|
+
result = api.get_block_kramdown(block_id)
|
|
241
|
+
|
|
242
|
+
# 对内容进行打码处理
|
|
243
|
+
if isinstance(result, dict) and "kramdown" in result:
|
|
244
|
+
result["kramdown"] = mask_sensitive_data(result["kramdown"])
|
|
245
|
+
if isinstance(result, dict) and "content" in result:
|
|
246
|
+
result["content"] = mask_sensitive_data(result["content"])
|
|
247
|
+
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
@mcp.tool()
|
|
251
|
+
def get_blocks_content(
|
|
252
|
+
ctx: Context[ServerSession, AppContext],
|
|
253
|
+
block_ids: List[str]
|
|
254
|
+
) -> list:
|
|
255
|
+
"""批量获取多个块的完整内容。
|
|
256
|
+
|
|
257
|
+
在通过 find_documents 或 search_blocks 找到相关块后,使用此工具批量读取它们的详细内容。
|
|
258
|
+
相比多次调用 get_block_content,这个工具更高效,特别适合查询大量块。
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
ctx: MCP 上下文对象,自动注入。
|
|
262
|
+
block_ids (List[str]): 要获取内容的块的 ID 列表。
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
list: 包含多个块信息的字典列表,每个字典包含块的 Kramdown 源码等信息。
|
|
266
|
+
"""
|
|
267
|
+
api = ctx.request_context.lifespan_context.siyuan_api
|
|
268
|
+
results = api.get_blocks_kramdown(block_ids)
|
|
269
|
+
|
|
270
|
+
# 对每个块的内容进行打码处理
|
|
271
|
+
for result in results:
|
|
272
|
+
if isinstance(result, dict):
|
|
273
|
+
if "kramdown" in result:
|
|
274
|
+
result["kramdown"] = mask_sensitive_data(result["kramdown"])
|
|
275
|
+
if "content" in result:
|
|
276
|
+
result["content"] = mask_sensitive_data(result["content"])
|
|
277
|
+
|
|
278
|
+
return results
|
|
279
|
+
|
|
280
|
+
@mcp.tool()
|
|
281
|
+
def execute_sql(
|
|
282
|
+
ctx: Context[ServerSession, AppContext],
|
|
283
|
+
query: str
|
|
284
|
+
) -> list:
|
|
285
|
+
"""直接执行一条只读的 SQL 查询语句。
|
|
286
|
+
|
|
287
|
+
这是一个强大的底层工具,仅用于高级或复杂的查询场景。
|
|
288
|
+
为了安全,此工具只允许执行 'SELECT' 语句。
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
ctx: MCP 上下文对象,自动注入。
|
|
292
|
+
query (str): 要执行的 SQL 'SELECT' 语句。
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
list: 代表查询结果的字典列表。
|
|
296
|
+
"""
|
|
297
|
+
api = ctx.request_context.lifespan_context.siyuan_api
|
|
298
|
+
results = api.execute_sql(query)
|
|
299
|
+
|
|
300
|
+
# 对查询结果中的内容进行打码处理
|
|
301
|
+
for result in results:
|
|
302
|
+
if isinstance(result, dict):
|
|
303
|
+
# 对可能包含敏感信息的字段进行打码处理
|
|
304
|
+
for key, value in result.items():
|
|
305
|
+
if isinstance(value, str) and len(value) > 10:
|
|
306
|
+
# 对长字符串进行打码处理,避免误判
|
|
307
|
+
result[key] = mask_sensitive_data(value)
|
|
308
|
+
|
|
309
|
+
return results
|
|
310
|
+
|
|
311
|
+
# --- 6. Server Runner ---
|
|
312
|
+
def main():
|
|
313
|
+
"""MCP 服务器入口函数
|
|
314
|
+
|
|
315
|
+
通过 uvx 或 pip install 安装后,可通过命令行直接运行此服务器。
|
|
316
|
+
"""
|
|
317
|
+
mcp.run()
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
# 运行方式:
|
|
321
|
+
# 1. 设置 SIYUAN_API_TOKEN 环境变量
|
|
322
|
+
# 2. 使用 uv run 直接运行: uv run siyuan_mcp_server.py
|
|
323
|
+
# 3. 安装后使用: siyuan-mcp-server
|
|
324
|
+
main()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def mask_middle_third(text):
|
|
5
|
+
"""
|
|
6
|
+
只打码字符串中间1/3的部分,保留开头和结尾部分
|
|
7
|
+
|
|
8
|
+
参数:
|
|
9
|
+
text (str): 输入的字符串
|
|
10
|
+
|
|
11
|
+
返回:
|
|
12
|
+
str: 处理后的字符串,中间1/3部分被替换为*
|
|
13
|
+
"""
|
|
14
|
+
if len(text) < 6: # 如果字符串太短,直接全部打码
|
|
15
|
+
return "*" * len(text)
|
|
16
|
+
|
|
17
|
+
# 计算各个部分的长度
|
|
18
|
+
third = len(text) // 3
|
|
19
|
+
start_length = (len(text) - third) // 2
|
|
20
|
+
end_length = len(text) - third - start_length
|
|
21
|
+
|
|
22
|
+
# 构建结果字符串
|
|
23
|
+
result = text[:start_length] + ("*" * third) + text[-end_length:] if end_length > 0 else text[:start_length] + ("*" * third)
|
|
24
|
+
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def mask_sensitive_data(text):
|
|
29
|
+
"""
|
|
30
|
+
对文本中的敏感信息(密钥、API Key、Secret等)进行打码处理
|
|
31
|
+
|
|
32
|
+
参数:
|
|
33
|
+
text (str): 输入的文本
|
|
34
|
+
|
|
35
|
+
返回:
|
|
36
|
+
str: 处理后的文本,其中敏感信息被替换为*
|
|
37
|
+
"""
|
|
38
|
+
# 定义各种密钥格式的正则表达式模式
|
|
39
|
+
patterns = [
|
|
40
|
+
# AWS Access Key ID: AKIA开头,20个字符
|
|
41
|
+
(r"AKIA[0-9A-Z]{16}", lambda m: mask_middle_third(m.group())),
|
|
42
|
+
# AWS Secret Access Key: 40个字符的随机字符串
|
|
43
|
+
(r"[A-Za-z0-9/+=]{40}", lambda m: mask_middle_third(m.group())),
|
|
44
|
+
# GitHub Personal Access Token
|
|
45
|
+
(
|
|
46
|
+
r"ghp_[a-zA-Z0-9]{36}|gho_[a-zA-Z0-9]{36}|ghu_[a-zA-Z0-9]{36}|ghs_[a-zA-Z0-9]{36}|ghr_[a-zA-Z0-9]{36}",
|
|
47
|
+
lambda m: mask_middle_third(m.group()),
|
|
48
|
+
),
|
|
49
|
+
# JWT Token: 由三部分组成,用点分隔
|
|
50
|
+
(r"[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+", lambda m: mask_middle_third(m.group())),
|
|
51
|
+
# UUID
|
|
52
|
+
(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", lambda m: mask_middle_third(m.group())),
|
|
53
|
+
# API Key: 32位以上的字母数字组合
|
|
54
|
+
(r"[A-Za-z0-9]{32,}", lambda m: mask_middle_third(m.group())),
|
|
55
|
+
# OAuth tokens: 20位以上的字母数字组合
|
|
56
|
+
(r"[A-Za-z0-9]{20,}", lambda m: mask_middle_third(m.group())),
|
|
57
|
+
# Private Key
|
|
58
|
+
(r"-----BEGIN(?: RSA)? PRIVATE KEY-----.*?-----END(?: RSA)? PRIVATE KEY-----", lambda m: mask_middle_third(m.group())),
|
|
59
|
+
# Database URLs - 特殊处理,只打码密码部分
|
|
60
|
+
(
|
|
61
|
+
r"(postgresql|mysql|mongodb)://([^:]+):([^@]+)@([^/]+)/([^\s]+)",
|
|
62
|
+
lambda m: f"{m.group(1)}://{m.group(2)}:{mask_middle_third(m.group(3))}@{m.group(4)}/{m.group(5)}",
|
|
63
|
+
),
|
|
64
|
+
# API URLs with credentials - 特殊处理,只打码密钥值部分
|
|
65
|
+
(r"(api[_-]?key[=:\s]+)([^\s&]+)", lambda m: f"{m.group(1)}{mask_middle_third(m.group(2))}"),
|
|
66
|
+
# Base64编码的密钥
|
|
67
|
+
(r"[A-Za-z0-9+/]{20,}={0,2}", lambda m: mask_middle_third(m.group())),
|
|
68
|
+
# 十六进制密钥
|
|
69
|
+
(r"[0-9a-fA-F]{32,}", lambda m: mask_middle_third(m.group())),
|
|
70
|
+
# 带有引号的密钥
|
|
71
|
+
(r"([\"\'])([A-Za-z0-9+/=]{20,})(\1)", lambda m: m.group(1) + mask_middle_third(m.group(2)) + m.group(3)),
|
|
72
|
+
# 通用密钥格式:包含特殊字符的长字符串
|
|
73
|
+
(r"[\"\']?[A-Za-z0-9_\-+/=]{20,}[\"\']?", lambda m: mask_middle_third(m.group())),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# 应用所有模式
|
|
77
|
+
result = text
|
|
78
|
+
for pattern, replacement in patterns:
|
|
79
|
+
result = re.sub(pattern, replacement, result, flags=re.DOTALL)
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
test_string = """
|
|
86
|
+
- Todoist token
|
|
87
|
+
数据库连接: jdbc:mysql://localhost:3306/mydb?user=admin&password=secret123
|
|
88
|
+
API密钥: sk_test_1234567890abcdefghijklmnopqrstuvwxyz
|
|
89
|
+
令牌: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|
|
90
|
+
用户名: user1, 密码: P@ssw0rd!
|
|
91
|
+
"""
|
|
92
|
+
print(mask_sensitive_data(test_string))
|