notion-multi-mcp 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.
- notion_multi_mcp-0.1.0/.env.example +1 -0
- notion_multi_mcp-0.1.0/.gitignore +19 -0
- notion_multi_mcp-0.1.0/LICENSE +21 -0
- notion_multi_mcp-0.1.0/PKG-INFO +325 -0
- notion_multi_mcp-0.1.0/README.md +299 -0
- notion_multi_mcp-0.1.0/notion_multi_mcp.py +413 -0
- notion_multi_mcp-0.1.0/pyproject.toml +39 -0
- notion_multi_mcp-0.1.0/requirements.txt +3 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
NOTION_ACCOUNTS=work:ntn_your_work_key_here,personal:ntn_your_personal_key_here
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kerwin Lin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: notion-multi-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for managing multiple Notion accounts simultaneously
|
|
5
|
+
Project-URL: Homepage, https://github.com/kerwin77106/Notion-Multi-MCP
|
|
6
|
+
Project-URL: Repository, https://github.com/kerwin77106/Notion-Multi-MCP
|
|
7
|
+
Project-URL: Issues, https://github.com/kerwin77106/Notion-Multi-MCP/issues
|
|
8
|
+
Author-email: Kerwin Lin <kerwin77106@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: mcp,model-context-protocol,multi-account,notion
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: httpx
|
|
23
|
+
Requires-Dist: mcp
|
|
24
|
+
Requires-Dist: notion-client
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# notion-multi-mcp
|
|
28
|
+
|
|
29
|
+
[English](#english) | [繁體中文](#繁體中文)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## English
|
|
34
|
+
|
|
35
|
+
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that lets AI assistants operate **multiple Notion accounts** simultaneously. Each account gets its own prefixed toolset — no conflicts, no mix-ups.
|
|
36
|
+
|
|
37
|
+
### Features
|
|
38
|
+
|
|
39
|
+
- **Multi-account** — connect 2, 3, or more Notion workspaces in a single MCP server
|
|
40
|
+
- **Custom prefixes** — you name each account (e.g. `work`, `personal`, `team`), tools are auto-generated as `work_search`, `personal_create_page`, `team_query_database`, etc.
|
|
41
|
+
- **22 tools per account** — full Notion API coverage: pages, databases, blocks, comments, data sources, search
|
|
42
|
+
- **Zero conflict** — each account is fully isolated with its own API key and client instance
|
|
43
|
+
|
|
44
|
+
### Quick Start
|
|
45
|
+
|
|
46
|
+
#### Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install notion-multi-mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or run directly without installing:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uvx notion-multi-mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### Configure in Claude Code
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
claude mcp add notion-multi -- uvx notion-multi-mcp
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Then set the environment variable in your Claude Code settings (`~/.claude/settings.json`):
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"notion-multi": {
|
|
70
|
+
"command": "uvx",
|
|
71
|
+
"args": ["notion-multi-mcp"],
|
|
72
|
+
"env": {
|
|
73
|
+
"NOTION_ACCOUNTS": "work:ntn_your_work_key,personal:ntn_your_personal_key"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### Configure in Cursor / VS Code
|
|
81
|
+
|
|
82
|
+
Add to `.cursor/mcp.json` or `.vscode/mcp.json`:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"mcpServers": {
|
|
87
|
+
"notion-multi": {
|
|
88
|
+
"command": "uvx",
|
|
89
|
+
"args": ["notion-multi-mcp"],
|
|
90
|
+
"env": {
|
|
91
|
+
"NOTION_ACCOUNTS": "work:ntn_your_work_key,personal:ntn_your_personal_key"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Configuration
|
|
99
|
+
|
|
100
|
+
Set `NOTION_ACCOUNTS` with comma-separated `prefix:api_key` pairs:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
NOTION_ACCOUNTS=work:ntn_abc123,personal:ntn_def456,team:ntn_ghi789
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
This example creates **3 accounts × 22 tools = 66 tools**:
|
|
107
|
+
|
|
108
|
+
- `work_search`, `work_create_page`, `work_query_database`, ...
|
|
109
|
+
- `personal_search`, `personal_create_page`, `personal_query_database`, ...
|
|
110
|
+
- `team_search`, `team_create_page`, `team_query_database`, ...
|
|
111
|
+
|
|
112
|
+
### Getting Notion API Keys
|
|
113
|
+
|
|
114
|
+
1. Go to [https://www.notion.so/my-integrations](https://www.notion.so/my-integrations)
|
|
115
|
+
2. Click **"New integration"** for each workspace
|
|
116
|
+
3. Copy the **Internal Integration Secret** (starts with `ntn_`)
|
|
117
|
+
4. **Important**: In Notion, share the pages/databases you want to access with your integration
|
|
118
|
+
|
|
119
|
+
### Available Tools (per account)
|
|
120
|
+
|
|
121
|
+
Each connected account gets all 22 tools, prefixed with the account name:
|
|
122
|
+
|
|
123
|
+
| # | Tool | Description |
|
|
124
|
+
|---|------|-------------|
|
|
125
|
+
| 1 | `{prefix}_search` | Search pages and databases |
|
|
126
|
+
| 2 | `{prefix}_query_database` | Query database contents with filters and sorts |
|
|
127
|
+
| 3 | `{prefix}_create_page` | Create a new page |
|
|
128
|
+
| 4 | `{prefix}_retrieve_page` | Get page information |
|
|
129
|
+
| 5 | `{prefix}_update_page` | Update page properties |
|
|
130
|
+
| 6 | `{prefix}_retrieve_page_property` | Get a specific page property |
|
|
131
|
+
| 7 | `{prefix}_move_page` | Move a page to a new parent |
|
|
132
|
+
| 8 | `{prefix}_retrieve_block` | Get block information |
|
|
133
|
+
| 9 | `{prefix}_update_block` | Update a block |
|
|
134
|
+
| 10 | `{prefix}_delete_block` | Delete a block |
|
|
135
|
+
| 11 | `{prefix}_get_block_children` | List child blocks |
|
|
136
|
+
| 12 | `{prefix}_append_block_children` | Append child blocks |
|
|
137
|
+
| 13 | `{prefix}_retrieve_database` | Get database schema |
|
|
138
|
+
| 14 | `{prefix}_create_database` | Create a new database |
|
|
139
|
+
| 15 | `{prefix}_update_database` | Update database properties |
|
|
140
|
+
| 16 | `{prefix}_query_data_source` | Query a data source |
|
|
141
|
+
| 17 | `{prefix}_retrieve_data_source` | Get data source info |
|
|
142
|
+
| 18 | `{prefix}_list_data_source_templates` | List data source templates |
|
|
143
|
+
| 19 | `{prefix}_update_data_source` | Update a data source |
|
|
144
|
+
| 20 | `{prefix}_create_comment` | Create a comment |
|
|
145
|
+
| 21 | `{prefix}_retrieve_comments` | List comments |
|
|
146
|
+
| 22 | `{prefix}_get_self` | Get bot user info |
|
|
147
|
+
|
|
148
|
+
### Usage Examples
|
|
149
|
+
|
|
150
|
+
Once configured, you can ask your AI assistant:
|
|
151
|
+
|
|
152
|
+
- *"Search for 'Q1 Report' in my work Notion"* → `work_search`
|
|
153
|
+
- *"Create a new page in my personal Notion"* → `personal_create_page`
|
|
154
|
+
- *"Copy the database schema from work to team"* → `work_retrieve_database` + `team_create_database`
|
|
155
|
+
- *"List all pages in both accounts"* → `work_search` + `personal_search` in parallel
|
|
156
|
+
|
|
157
|
+
### Requirements
|
|
158
|
+
|
|
159
|
+
- Python 3.10+
|
|
160
|
+
- Notion integration API keys ([create here](https://www.notion.so/my-integrations))
|
|
161
|
+
|
|
162
|
+
### Development
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
git clone https://github.com/kerwin77106/Notion-Multi-MCP.git
|
|
166
|
+
cd notion-multi-mcp
|
|
167
|
+
|
|
168
|
+
pip install -r requirements.txt
|
|
169
|
+
|
|
170
|
+
export NOTION_ACCOUNTS="dev:ntn_your_key_here"
|
|
171
|
+
|
|
172
|
+
python notion_multi_mcp.py
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### License
|
|
176
|
+
|
|
177
|
+
[MIT](LICENSE)
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 繁體中文
|
|
182
|
+
|
|
183
|
+
一個 [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) 伺服器,讓 AI 助手能**同時操作多個 Notion 帳號**。每個帳號擁有獨立的前綴工具集,不會混淆、不會衝突。
|
|
184
|
+
|
|
185
|
+
### 功能特色
|
|
186
|
+
|
|
187
|
+
- **多帳號支援** — 可連接 2 個、3 個甚至更多 Notion 工作區
|
|
188
|
+
- **自訂前綴** — 自由命名帳號(如 `work`、`personal`、`team`),工具會自動產生為 `work_search`、`personal_create_page`、`team_query_database` 等
|
|
189
|
+
- **每帳號 22 個工具** — 完整覆蓋 Notion API:頁面、資料庫、區塊、評論、資料來源、搜尋
|
|
190
|
+
- **完全隔離** — 每個帳號使用獨立的 API Key 和 Client 實例
|
|
191
|
+
|
|
192
|
+
### 快速開始
|
|
193
|
+
|
|
194
|
+
#### 安裝
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
pip install notion-multi-mcp
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
或直接執行(不需安裝):
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
uvx notion-multi-mcp
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### 在 Claude Code 中設定
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
claude mcp add notion-multi -- uvx notion-multi-mcp
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
然後在 Claude Code 設定檔(`~/.claude/settings.json`)中設定環境變數:
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"mcpServers": {
|
|
217
|
+
"notion-multi": {
|
|
218
|
+
"command": "uvx",
|
|
219
|
+
"args": ["notion-multi-mcp"],
|
|
220
|
+
"env": {
|
|
221
|
+
"NOTION_ACCOUNTS": "work:ntn_你的工作帳號金鑰,personal:ntn_你的個人帳號金鑰"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### 在 Cursor / VS Code 中設定
|
|
229
|
+
|
|
230
|
+
新增到 `.cursor/mcp.json` 或 `.vscode/mcp.json`:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"mcpServers": {
|
|
235
|
+
"notion-multi": {
|
|
236
|
+
"command": "uvx",
|
|
237
|
+
"args": ["notion-multi-mcp"],
|
|
238
|
+
"env": {
|
|
239
|
+
"NOTION_ACCOUNTS": "work:ntn_你的工作帳號金鑰,personal:ntn_你的個人帳號金鑰"
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### 設定方式
|
|
247
|
+
|
|
248
|
+
設定 `NOTION_ACCOUNTS` 環境變數,用逗號分隔 `前綴:API金鑰` 組合:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
NOTION_ACCOUNTS=work:ntn_abc123,personal:ntn_def456,team:ntn_ghi789
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
以上範例會產生 **3 個帳號 × 22 個工具 = 66 個工具**:
|
|
255
|
+
|
|
256
|
+
- `work_search`、`work_create_page`、`work_query_database`⋯
|
|
257
|
+
- `personal_search`、`personal_create_page`、`personal_query_database`⋯
|
|
258
|
+
- `team_search`、`team_create_page`、`team_query_database`⋯
|
|
259
|
+
|
|
260
|
+
### 取得 Notion API Key
|
|
261
|
+
|
|
262
|
+
1. 前往 [https://www.notion.so/my-integrations](https://www.notion.so/my-integrations)
|
|
263
|
+
2. 為每個要連接的工作區點擊 **「New integration」**
|
|
264
|
+
3. 複製 **Internal Integration Secret**(以 `ntn_` 開頭)
|
|
265
|
+
4. **重要**:在 Notion 中,將你要存取的頁面/資料庫分享給你建立的 Integration
|
|
266
|
+
|
|
267
|
+
### 可用工具(每個帳號各一套)
|
|
268
|
+
|
|
269
|
+
每個連接的帳號都會取得全部 22 個工具,工具名稱加上帳號前綴:
|
|
270
|
+
|
|
271
|
+
| # | 工具名稱 | 說明 |
|
|
272
|
+
|---|---------|------|
|
|
273
|
+
| 1 | `{前綴}_search` | 搜尋頁面和資料庫 |
|
|
274
|
+
| 2 | `{前綴}_query_database` | 查詢資料庫內容(支援篩選和排序) |
|
|
275
|
+
| 3 | `{前綴}_create_page` | 建立新頁面 |
|
|
276
|
+
| 4 | `{前綴}_retrieve_page` | 取得頁面資訊 |
|
|
277
|
+
| 5 | `{前綴}_update_page` | 更新頁面屬性 |
|
|
278
|
+
| 6 | `{前綴}_retrieve_page_property` | 取得特定頁面屬性 |
|
|
279
|
+
| 7 | `{前綴}_move_page` | 將頁面移動到新的父頁面 |
|
|
280
|
+
| 8 | `{前綴}_retrieve_block` | 取得區塊資訊 |
|
|
281
|
+
| 9 | `{前綴}_update_block` | 更新區塊 |
|
|
282
|
+
| 10 | `{前綴}_delete_block` | 刪除區塊 |
|
|
283
|
+
| 11 | `{前綴}_get_block_children` | 列出子區塊 |
|
|
284
|
+
| 12 | `{前綴}_append_block_children` | 追加子區塊 |
|
|
285
|
+
| 13 | `{前綴}_retrieve_database` | 取得資料庫結構 |
|
|
286
|
+
| 14 | `{前綴}_create_database` | 建立新資料庫 |
|
|
287
|
+
| 15 | `{前綴}_update_database` | 更新資料庫屬性 |
|
|
288
|
+
| 16 | `{前綴}_query_data_source` | 查詢資料來源 |
|
|
289
|
+
| 17 | `{前綴}_retrieve_data_source` | 取得資料來源資訊 |
|
|
290
|
+
| 18 | `{前綴}_list_data_source_templates` | 列出資料來源範本 |
|
|
291
|
+
| 19 | `{前綴}_update_data_source` | 更新資料來源 |
|
|
292
|
+
| 20 | `{前綴}_create_comment` | 建立評論 |
|
|
293
|
+
| 21 | `{前綴}_retrieve_comments` | 列出評論 |
|
|
294
|
+
| 22 | `{前綴}_get_self` | 取得 Bot 使用者資訊 |
|
|
295
|
+
|
|
296
|
+
### 使用範例
|
|
297
|
+
|
|
298
|
+
設定完成後,你可以對 AI 助手說:
|
|
299
|
+
|
|
300
|
+
- 「在我的 work Notion 搜尋『Q1 報告』」→ 呼叫 `work_search`
|
|
301
|
+
- 「在 personal Notion 建一個新頁面」→ 呼叫 `personal_create_page`
|
|
302
|
+
- 「把 work 的資料庫結構複製到 team」→ 呼叫 `work_retrieve_database` + `team_create_database`
|
|
303
|
+
- 「列出兩個帳號的所有頁面」→ 同時呼叫 `work_search` 和 `personal_search`
|
|
304
|
+
|
|
305
|
+
### 系統需求
|
|
306
|
+
|
|
307
|
+
- Python 3.10+
|
|
308
|
+
- Notion Integration API Key([在此建立](https://www.notion.so/my-integrations))
|
|
309
|
+
|
|
310
|
+
### 開發
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
git clone https://github.com/kerwin77106/Notion-Multi-MCP.git
|
|
314
|
+
cd notion-multi-mcp
|
|
315
|
+
|
|
316
|
+
pip install -r requirements.txt
|
|
317
|
+
|
|
318
|
+
export NOTION_ACCOUNTS="dev:ntn_你的金鑰"
|
|
319
|
+
|
|
320
|
+
python notion_multi_mcp.py
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### 授權
|
|
324
|
+
|
|
325
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# notion-multi-mcp
|
|
2
|
+
|
|
3
|
+
[English](#english) | [繁體中文](#繁體中文)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## English
|
|
8
|
+
|
|
9
|
+
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that lets AI assistants operate **multiple Notion accounts** simultaneously. Each account gets its own prefixed toolset — no conflicts, no mix-ups.
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- **Multi-account** — connect 2, 3, or more Notion workspaces in a single MCP server
|
|
14
|
+
- **Custom prefixes** — you name each account (e.g. `work`, `personal`, `team`), tools are auto-generated as `work_search`, `personal_create_page`, `team_query_database`, etc.
|
|
15
|
+
- **22 tools per account** — full Notion API coverage: pages, databases, blocks, comments, data sources, search
|
|
16
|
+
- **Zero conflict** — each account is fully isolated with its own API key and client instance
|
|
17
|
+
|
|
18
|
+
### Quick Start
|
|
19
|
+
|
|
20
|
+
#### Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install notion-multi-mcp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or run directly without installing:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uvx notion-multi-mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
#### Configure in Claude Code
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
claude mcp add notion-multi -- uvx notion-multi-mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then set the environment variable in your Claude Code settings (`~/.claude/settings.json`):
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"notion-multi": {
|
|
44
|
+
"command": "uvx",
|
|
45
|
+
"args": ["notion-multi-mcp"],
|
|
46
|
+
"env": {
|
|
47
|
+
"NOTION_ACCOUNTS": "work:ntn_your_work_key,personal:ntn_your_personal_key"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
#### Configure in Cursor / VS Code
|
|
55
|
+
|
|
56
|
+
Add to `.cursor/mcp.json` or `.vscode/mcp.json`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"notion-multi": {
|
|
62
|
+
"command": "uvx",
|
|
63
|
+
"args": ["notion-multi-mcp"],
|
|
64
|
+
"env": {
|
|
65
|
+
"NOTION_ACCOUNTS": "work:ntn_your_work_key,personal:ntn_your_personal_key"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Configuration
|
|
73
|
+
|
|
74
|
+
Set `NOTION_ACCOUNTS` with comma-separated `prefix:api_key` pairs:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
NOTION_ACCOUNTS=work:ntn_abc123,personal:ntn_def456,team:ntn_ghi789
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This example creates **3 accounts × 22 tools = 66 tools**:
|
|
81
|
+
|
|
82
|
+
- `work_search`, `work_create_page`, `work_query_database`, ...
|
|
83
|
+
- `personal_search`, `personal_create_page`, `personal_query_database`, ...
|
|
84
|
+
- `team_search`, `team_create_page`, `team_query_database`, ...
|
|
85
|
+
|
|
86
|
+
### Getting Notion API Keys
|
|
87
|
+
|
|
88
|
+
1. Go to [https://www.notion.so/my-integrations](https://www.notion.so/my-integrations)
|
|
89
|
+
2. Click **"New integration"** for each workspace
|
|
90
|
+
3. Copy the **Internal Integration Secret** (starts with `ntn_`)
|
|
91
|
+
4. **Important**: In Notion, share the pages/databases you want to access with your integration
|
|
92
|
+
|
|
93
|
+
### Available Tools (per account)
|
|
94
|
+
|
|
95
|
+
Each connected account gets all 22 tools, prefixed with the account name:
|
|
96
|
+
|
|
97
|
+
| # | Tool | Description |
|
|
98
|
+
|---|------|-------------|
|
|
99
|
+
| 1 | `{prefix}_search` | Search pages and databases |
|
|
100
|
+
| 2 | `{prefix}_query_database` | Query database contents with filters and sorts |
|
|
101
|
+
| 3 | `{prefix}_create_page` | Create a new page |
|
|
102
|
+
| 4 | `{prefix}_retrieve_page` | Get page information |
|
|
103
|
+
| 5 | `{prefix}_update_page` | Update page properties |
|
|
104
|
+
| 6 | `{prefix}_retrieve_page_property` | Get a specific page property |
|
|
105
|
+
| 7 | `{prefix}_move_page` | Move a page to a new parent |
|
|
106
|
+
| 8 | `{prefix}_retrieve_block` | Get block information |
|
|
107
|
+
| 9 | `{prefix}_update_block` | Update a block |
|
|
108
|
+
| 10 | `{prefix}_delete_block` | Delete a block |
|
|
109
|
+
| 11 | `{prefix}_get_block_children` | List child blocks |
|
|
110
|
+
| 12 | `{prefix}_append_block_children` | Append child blocks |
|
|
111
|
+
| 13 | `{prefix}_retrieve_database` | Get database schema |
|
|
112
|
+
| 14 | `{prefix}_create_database` | Create a new database |
|
|
113
|
+
| 15 | `{prefix}_update_database` | Update database properties |
|
|
114
|
+
| 16 | `{prefix}_query_data_source` | Query a data source |
|
|
115
|
+
| 17 | `{prefix}_retrieve_data_source` | Get data source info |
|
|
116
|
+
| 18 | `{prefix}_list_data_source_templates` | List data source templates |
|
|
117
|
+
| 19 | `{prefix}_update_data_source` | Update a data source |
|
|
118
|
+
| 20 | `{prefix}_create_comment` | Create a comment |
|
|
119
|
+
| 21 | `{prefix}_retrieve_comments` | List comments |
|
|
120
|
+
| 22 | `{prefix}_get_self` | Get bot user info |
|
|
121
|
+
|
|
122
|
+
### Usage Examples
|
|
123
|
+
|
|
124
|
+
Once configured, you can ask your AI assistant:
|
|
125
|
+
|
|
126
|
+
- *"Search for 'Q1 Report' in my work Notion"* → `work_search`
|
|
127
|
+
- *"Create a new page in my personal Notion"* → `personal_create_page`
|
|
128
|
+
- *"Copy the database schema from work to team"* → `work_retrieve_database` + `team_create_database`
|
|
129
|
+
- *"List all pages in both accounts"* → `work_search` + `personal_search` in parallel
|
|
130
|
+
|
|
131
|
+
### Requirements
|
|
132
|
+
|
|
133
|
+
- Python 3.10+
|
|
134
|
+
- Notion integration API keys ([create here](https://www.notion.so/my-integrations))
|
|
135
|
+
|
|
136
|
+
### Development
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
git clone https://github.com/kerwin77106/Notion-Multi-MCP.git
|
|
140
|
+
cd notion-multi-mcp
|
|
141
|
+
|
|
142
|
+
pip install -r requirements.txt
|
|
143
|
+
|
|
144
|
+
export NOTION_ACCOUNTS="dev:ntn_your_key_here"
|
|
145
|
+
|
|
146
|
+
python notion_multi_mcp.py
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### License
|
|
150
|
+
|
|
151
|
+
[MIT](LICENSE)
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 繁體中文
|
|
156
|
+
|
|
157
|
+
一個 [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) 伺服器,讓 AI 助手能**同時操作多個 Notion 帳號**。每個帳號擁有獨立的前綴工具集,不會混淆、不會衝突。
|
|
158
|
+
|
|
159
|
+
### 功能特色
|
|
160
|
+
|
|
161
|
+
- **多帳號支援** — 可連接 2 個、3 個甚至更多 Notion 工作區
|
|
162
|
+
- **自訂前綴** — 自由命名帳號(如 `work`、`personal`、`team`),工具會自動產生為 `work_search`、`personal_create_page`、`team_query_database` 等
|
|
163
|
+
- **每帳號 22 個工具** — 完整覆蓋 Notion API:頁面、資料庫、區塊、評論、資料來源、搜尋
|
|
164
|
+
- **完全隔離** — 每個帳號使用獨立的 API Key 和 Client 實例
|
|
165
|
+
|
|
166
|
+
### 快速開始
|
|
167
|
+
|
|
168
|
+
#### 安裝
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
pip install notion-multi-mcp
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
或直接執行(不需安裝):
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
uvx notion-multi-mcp
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### 在 Claude Code 中設定
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
claude mcp add notion-multi -- uvx notion-multi-mcp
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
然後在 Claude Code 設定檔(`~/.claude/settings.json`)中設定環境變數:
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"mcpServers": {
|
|
191
|
+
"notion-multi": {
|
|
192
|
+
"command": "uvx",
|
|
193
|
+
"args": ["notion-multi-mcp"],
|
|
194
|
+
"env": {
|
|
195
|
+
"NOTION_ACCOUNTS": "work:ntn_你的工作帳號金鑰,personal:ntn_你的個人帳號金鑰"
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### 在 Cursor / VS Code 中設定
|
|
203
|
+
|
|
204
|
+
新增到 `.cursor/mcp.json` 或 `.vscode/mcp.json`:
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"mcpServers": {
|
|
209
|
+
"notion-multi": {
|
|
210
|
+
"command": "uvx",
|
|
211
|
+
"args": ["notion-multi-mcp"],
|
|
212
|
+
"env": {
|
|
213
|
+
"NOTION_ACCOUNTS": "work:ntn_你的工作帳號金鑰,personal:ntn_你的個人帳號金鑰"
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### 設定方式
|
|
221
|
+
|
|
222
|
+
設定 `NOTION_ACCOUNTS` 環境變數,用逗號分隔 `前綴:API金鑰` 組合:
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
NOTION_ACCOUNTS=work:ntn_abc123,personal:ntn_def456,team:ntn_ghi789
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
以上範例會產生 **3 個帳號 × 22 個工具 = 66 個工具**:
|
|
229
|
+
|
|
230
|
+
- `work_search`、`work_create_page`、`work_query_database`⋯
|
|
231
|
+
- `personal_search`、`personal_create_page`、`personal_query_database`⋯
|
|
232
|
+
- `team_search`、`team_create_page`、`team_query_database`⋯
|
|
233
|
+
|
|
234
|
+
### 取得 Notion API Key
|
|
235
|
+
|
|
236
|
+
1. 前往 [https://www.notion.so/my-integrations](https://www.notion.so/my-integrations)
|
|
237
|
+
2. 為每個要連接的工作區點擊 **「New integration」**
|
|
238
|
+
3. 複製 **Internal Integration Secret**(以 `ntn_` 開頭)
|
|
239
|
+
4. **重要**:在 Notion 中,將你要存取的頁面/資料庫分享給你建立的 Integration
|
|
240
|
+
|
|
241
|
+
### 可用工具(每個帳號各一套)
|
|
242
|
+
|
|
243
|
+
每個連接的帳號都會取得全部 22 個工具,工具名稱加上帳號前綴:
|
|
244
|
+
|
|
245
|
+
| # | 工具名稱 | 說明 |
|
|
246
|
+
|---|---------|------|
|
|
247
|
+
| 1 | `{前綴}_search` | 搜尋頁面和資料庫 |
|
|
248
|
+
| 2 | `{前綴}_query_database` | 查詢資料庫內容(支援篩選和排序) |
|
|
249
|
+
| 3 | `{前綴}_create_page` | 建立新頁面 |
|
|
250
|
+
| 4 | `{前綴}_retrieve_page` | 取得頁面資訊 |
|
|
251
|
+
| 5 | `{前綴}_update_page` | 更新頁面屬性 |
|
|
252
|
+
| 6 | `{前綴}_retrieve_page_property` | 取得特定頁面屬性 |
|
|
253
|
+
| 7 | `{前綴}_move_page` | 將頁面移動到新的父頁面 |
|
|
254
|
+
| 8 | `{前綴}_retrieve_block` | 取得區塊資訊 |
|
|
255
|
+
| 9 | `{前綴}_update_block` | 更新區塊 |
|
|
256
|
+
| 10 | `{前綴}_delete_block` | 刪除區塊 |
|
|
257
|
+
| 11 | `{前綴}_get_block_children` | 列出子區塊 |
|
|
258
|
+
| 12 | `{前綴}_append_block_children` | 追加子區塊 |
|
|
259
|
+
| 13 | `{前綴}_retrieve_database` | 取得資料庫結構 |
|
|
260
|
+
| 14 | `{前綴}_create_database` | 建立新資料庫 |
|
|
261
|
+
| 15 | `{前綴}_update_database` | 更新資料庫屬性 |
|
|
262
|
+
| 16 | `{前綴}_query_data_source` | 查詢資料來源 |
|
|
263
|
+
| 17 | `{前綴}_retrieve_data_source` | 取得資料來源資訊 |
|
|
264
|
+
| 18 | `{前綴}_list_data_source_templates` | 列出資料來源範本 |
|
|
265
|
+
| 19 | `{前綴}_update_data_source` | 更新資料來源 |
|
|
266
|
+
| 20 | `{前綴}_create_comment` | 建立評論 |
|
|
267
|
+
| 21 | `{前綴}_retrieve_comments` | 列出評論 |
|
|
268
|
+
| 22 | `{前綴}_get_self` | 取得 Bot 使用者資訊 |
|
|
269
|
+
|
|
270
|
+
### 使用範例
|
|
271
|
+
|
|
272
|
+
設定完成後,你可以對 AI 助手說:
|
|
273
|
+
|
|
274
|
+
- 「在我的 work Notion 搜尋『Q1 報告』」→ 呼叫 `work_search`
|
|
275
|
+
- 「在 personal Notion 建一個新頁面」→ 呼叫 `personal_create_page`
|
|
276
|
+
- 「把 work 的資料庫結構複製到 team」→ 呼叫 `work_retrieve_database` + `team_create_database`
|
|
277
|
+
- 「列出兩個帳號的所有頁面」→ 同時呼叫 `work_search` 和 `personal_search`
|
|
278
|
+
|
|
279
|
+
### 系統需求
|
|
280
|
+
|
|
281
|
+
- Python 3.10+
|
|
282
|
+
- Notion Integration API Key([在此建立](https://www.notion.so/my-integrations))
|
|
283
|
+
|
|
284
|
+
### 開發
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
git clone https://github.com/kerwin77106/Notion-Multi-MCP.git
|
|
288
|
+
cd notion-multi-mcp
|
|
289
|
+
|
|
290
|
+
pip install -r requirements.txt
|
|
291
|
+
|
|
292
|
+
export NOTION_ACCOUNTS="dev:ntn_你的金鑰"
|
|
293
|
+
|
|
294
|
+
python notion_multi_mcp.py
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### 授權
|
|
298
|
+
|
|
299
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""notion-multi-mcp — MCP Server for multiple Notion accounts."""
|
|
2
|
+
|
|
3
|
+
# ── Import ──────────────────────────────────────────────────────
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from mcp.server.fastmcp import FastMCP
|
|
10
|
+
|
|
11
|
+
from notion_client import Client
|
|
12
|
+
|
|
13
|
+
# ── Parse NOTION_ACCOUNTS env var ───────────────────────────────
|
|
14
|
+
# Format: "prefix1:ntn_key1,prefix2:ntn_key2,prefix3:ntn_key3"
|
|
15
|
+
accounts_raw = os.environ.get("NOTION_ACCOUNTS", "")
|
|
16
|
+
|
|
17
|
+
if not accounts_raw:
|
|
18
|
+
print(
|
|
19
|
+
"Error: NOTION_ACCOUNTS is not set.\n"
|
|
20
|
+
"Format: NOTION_ACCOUNTS=\"work:ntn_xxx,personal:ntn_yyy\"\n"
|
|
21
|
+
"See README for details.",
|
|
22
|
+
file=sys.stderr,
|
|
23
|
+
)
|
|
24
|
+
sys.exit(1)
|
|
25
|
+
|
|
26
|
+
accounts: list[tuple[str, str]] = []
|
|
27
|
+
for pair in accounts_raw.split(","):
|
|
28
|
+
pair = pair.strip()
|
|
29
|
+
if ":" not in pair:
|
|
30
|
+
print(
|
|
31
|
+
f"Error: invalid account format '{pair}'. Expected 'prefix:api_key'.",
|
|
32
|
+
file=sys.stderr,
|
|
33
|
+
)
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
prefix, api_key = pair.split(":", 1)
|
|
36
|
+
prefix = prefix.strip()
|
|
37
|
+
api_key = api_key.strip()
|
|
38
|
+
if not prefix or not api_key:
|
|
39
|
+
print(
|
|
40
|
+
f"Error: empty prefix or api_key in '{pair}'.",
|
|
41
|
+
file=sys.stderr,
|
|
42
|
+
)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
accounts.append((prefix, api_key))
|
|
45
|
+
|
|
46
|
+
# ── FastMCP Server ──────────────────────────────────────────────
|
|
47
|
+
mcp = FastMCP("notion-multi-mcp")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ── register_tools() factory ────────────────────────────────────
|
|
51
|
+
def register_tools(prefix: str, client: Client, api_key: str) -> None:
|
|
52
|
+
"""Register all 22 Notion tools for one account with the given prefix."""
|
|
53
|
+
|
|
54
|
+
def _raw_request(method: str, path: str, body: dict | None = None) -> dict:
|
|
55
|
+
headers = {
|
|
56
|
+
"Authorization": f"Bearer {api_key}",
|
|
57
|
+
"Notion-Version": "2022-06-28",
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
}
|
|
60
|
+
url = f"https://api.notion.com/v1/{path}"
|
|
61
|
+
if method == "GET":
|
|
62
|
+
resp = httpx.get(url, headers=headers, timeout=30.0)
|
|
63
|
+
elif method == "POST":
|
|
64
|
+
resp = httpx.post(url, headers=headers, json=body or {}, timeout=30.0)
|
|
65
|
+
elif method == "PATCH":
|
|
66
|
+
resp = httpx.patch(url, headers=headers, json=body or {}, timeout=30.0)
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError(f"Unsupported HTTP method: {method}")
|
|
69
|
+
resp.raise_for_status()
|
|
70
|
+
return resp.json()
|
|
71
|
+
|
|
72
|
+
# ── 1: search ────────────────────────────────────────────────
|
|
73
|
+
@mcp.tool(name=f"{prefix}_search", description=f"[{prefix}] Search pages and databases")
|
|
74
|
+
def search(
|
|
75
|
+
query: str = "",
|
|
76
|
+
filter_json: str | None = None,
|
|
77
|
+
sort_json: str | None = None,
|
|
78
|
+
start_cursor: str | None = None,
|
|
79
|
+
page_size: int = 100,
|
|
80
|
+
) -> str:
|
|
81
|
+
try:
|
|
82
|
+
kwargs: dict = {"query": query, "page_size": page_size}
|
|
83
|
+
if filter_json is not None:
|
|
84
|
+
kwargs["filter"] = json.loads(filter_json)
|
|
85
|
+
if sort_json is not None:
|
|
86
|
+
kwargs["sort"] = json.loads(sort_json)
|
|
87
|
+
if start_cursor is not None:
|
|
88
|
+
kwargs["start_cursor"] = start_cursor
|
|
89
|
+
return json.dumps(client.search(**kwargs), ensure_ascii=False)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
92
|
+
|
|
93
|
+
# ── 2: query_database ────────────────────────────────────────
|
|
94
|
+
@mcp.tool(name=f"{prefix}_query_database", description=f"[{prefix}] Query database contents")
|
|
95
|
+
def query_database(
|
|
96
|
+
database_id: str,
|
|
97
|
+
filter_json: str | None = None,
|
|
98
|
+
sorts_json: str | None = None,
|
|
99
|
+
start_cursor: str | None = None,
|
|
100
|
+
page_size: int = 100,
|
|
101
|
+
) -> str:
|
|
102
|
+
try:
|
|
103
|
+
kwargs: dict = {"database_id": database_id, "page_size": page_size}
|
|
104
|
+
if filter_json is not None:
|
|
105
|
+
kwargs["filter"] = json.loads(filter_json)
|
|
106
|
+
if sorts_json is not None:
|
|
107
|
+
kwargs["sorts"] = json.loads(sorts_json)
|
|
108
|
+
if start_cursor is not None:
|
|
109
|
+
kwargs["start_cursor"] = start_cursor
|
|
110
|
+
return json.dumps(client.databases.query(**kwargs), ensure_ascii=False)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
113
|
+
|
|
114
|
+
# ── 3: create_page ───────────────────────────────────────────
|
|
115
|
+
@mcp.tool(name=f"{prefix}_create_page", description=f"[{prefix}] Create a new page")
|
|
116
|
+
def create_page(
|
|
117
|
+
parent_json: str,
|
|
118
|
+
properties_json: str,
|
|
119
|
+
children_json: str | None = None,
|
|
120
|
+
icon_json: str | None = None,
|
|
121
|
+
cover_json: str | None = None,
|
|
122
|
+
) -> str:
|
|
123
|
+
try:
|
|
124
|
+
kwargs: dict = {
|
|
125
|
+
"parent": json.loads(parent_json),
|
|
126
|
+
"properties": json.loads(properties_json),
|
|
127
|
+
}
|
|
128
|
+
if children_json is not None:
|
|
129
|
+
kwargs["children"] = json.loads(children_json)
|
|
130
|
+
if icon_json is not None:
|
|
131
|
+
kwargs["icon"] = json.loads(icon_json)
|
|
132
|
+
if cover_json is not None:
|
|
133
|
+
kwargs["cover"] = json.loads(cover_json)
|
|
134
|
+
return json.dumps(client.pages.create(**kwargs), ensure_ascii=False)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
137
|
+
|
|
138
|
+
# ── 4: retrieve_page ─────────────────────────────────────────
|
|
139
|
+
@mcp.tool(name=f"{prefix}_retrieve_page", description=f"[{prefix}] Get page information")
|
|
140
|
+
def retrieve_page(page_id: str) -> str:
|
|
141
|
+
try:
|
|
142
|
+
return json.dumps(client.pages.retrieve(page_id=page_id), ensure_ascii=False)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
145
|
+
|
|
146
|
+
# ── 5: update_page ───────────────────────────────────────────
|
|
147
|
+
@mcp.tool(name=f"{prefix}_update_page", description=f"[{prefix}] Update page properties")
|
|
148
|
+
def update_page(
|
|
149
|
+
page_id: str,
|
|
150
|
+
properties_json: str | None = None,
|
|
151
|
+
icon_json: str | None = None,
|
|
152
|
+
cover_json: str | None = None,
|
|
153
|
+
archived: bool | None = None,
|
|
154
|
+
) -> str:
|
|
155
|
+
try:
|
|
156
|
+
kwargs: dict = {"page_id": page_id}
|
|
157
|
+
if properties_json is not None:
|
|
158
|
+
kwargs["properties"] = json.loads(properties_json)
|
|
159
|
+
if icon_json is not None:
|
|
160
|
+
kwargs["icon"] = json.loads(icon_json)
|
|
161
|
+
if cover_json is not None:
|
|
162
|
+
kwargs["cover"] = json.loads(cover_json)
|
|
163
|
+
if archived is not None:
|
|
164
|
+
kwargs["archived"] = archived
|
|
165
|
+
return json.dumps(client.pages.update(**kwargs), ensure_ascii=False)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
168
|
+
|
|
169
|
+
# ── 6: retrieve_page_property ────────────────────────────────
|
|
170
|
+
@mcp.tool(name=f"{prefix}_retrieve_page_property", description=f"[{prefix}] Get a specific page property")
|
|
171
|
+
def retrieve_page_property(
|
|
172
|
+
page_id: str,
|
|
173
|
+
property_id: str,
|
|
174
|
+
start_cursor: str | None = None,
|
|
175
|
+
page_size: int | None = None,
|
|
176
|
+
) -> str:
|
|
177
|
+
try:
|
|
178
|
+
kwargs: dict = {"page_id": page_id, "property_id": property_id}
|
|
179
|
+
if start_cursor is not None:
|
|
180
|
+
kwargs["start_cursor"] = start_cursor
|
|
181
|
+
if page_size is not None:
|
|
182
|
+
kwargs["page_size"] = page_size
|
|
183
|
+
return json.dumps(client.pages.properties.retrieve(**kwargs), ensure_ascii=False)
|
|
184
|
+
except Exception as e:
|
|
185
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
186
|
+
|
|
187
|
+
# ── 7: move_page ─────────────────────────────────────────────
|
|
188
|
+
@mcp.tool(name=f"{prefix}_move_page", description=f"[{prefix}] Move a page to a new parent")
|
|
189
|
+
def move_page(page_id: str, new_parent_json: str) -> str:
|
|
190
|
+
try:
|
|
191
|
+
return json.dumps(
|
|
192
|
+
_raw_request("POST", f"pages/{page_id}/move", body={"parent": json.loads(new_parent_json)}),
|
|
193
|
+
ensure_ascii=False,
|
|
194
|
+
)
|
|
195
|
+
except Exception as e:
|
|
196
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
197
|
+
|
|
198
|
+
# ── 8: retrieve_block ────────────────────────────────────────
|
|
199
|
+
@mcp.tool(name=f"{prefix}_retrieve_block", description=f"[{prefix}] Get block information")
|
|
200
|
+
def retrieve_block(block_id: str) -> str:
|
|
201
|
+
try:
|
|
202
|
+
return json.dumps(client.blocks.retrieve(block_id=block_id), ensure_ascii=False)
|
|
203
|
+
except Exception as e:
|
|
204
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
205
|
+
|
|
206
|
+
# ── 9: update_block ──────────────────────────────────────────
|
|
207
|
+
@mcp.tool(name=f"{prefix}_update_block", description=f"[{prefix}] Update a block")
|
|
208
|
+
def update_block(block_id: str, block_json: str) -> str:
|
|
209
|
+
try:
|
|
210
|
+
return json.dumps(client.blocks.update(block_id=block_id, **json.loads(block_json)), ensure_ascii=False)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
213
|
+
|
|
214
|
+
# ── 10: delete_block ─────────────────────────────────────────
|
|
215
|
+
@mcp.tool(name=f"{prefix}_delete_block", description=f"[{prefix}] Delete a block")
|
|
216
|
+
def delete_block(block_id: str) -> str:
|
|
217
|
+
try:
|
|
218
|
+
return json.dumps(client.blocks.delete(block_id=block_id), ensure_ascii=False)
|
|
219
|
+
except Exception as e:
|
|
220
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
221
|
+
|
|
222
|
+
# ── 11: get_block_children ───────────────────────────────────
|
|
223
|
+
@mcp.tool(name=f"{prefix}_get_block_children", description=f"[{prefix}] List child blocks")
|
|
224
|
+
def get_block_children(
|
|
225
|
+
block_id: str,
|
|
226
|
+
start_cursor: str | None = None,
|
|
227
|
+
page_size: int = 100,
|
|
228
|
+
) -> str:
|
|
229
|
+
try:
|
|
230
|
+
kwargs: dict = {"block_id": block_id, "page_size": page_size}
|
|
231
|
+
if start_cursor is not None:
|
|
232
|
+
kwargs["start_cursor"] = start_cursor
|
|
233
|
+
return json.dumps(client.blocks.children.list(**kwargs), ensure_ascii=False)
|
|
234
|
+
except Exception as e:
|
|
235
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
236
|
+
|
|
237
|
+
# ── 12: append_block_children ────────────────────────────────
|
|
238
|
+
@mcp.tool(name=f"{prefix}_append_block_children", description=f"[{prefix}] Append child blocks")
|
|
239
|
+
def append_block_children(block_id: str, children_json: str) -> str:
|
|
240
|
+
try:
|
|
241
|
+
return json.dumps(
|
|
242
|
+
client.blocks.children.append(block_id=block_id, children=json.loads(children_json)),
|
|
243
|
+
ensure_ascii=False,
|
|
244
|
+
)
|
|
245
|
+
except Exception as e:
|
|
246
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
247
|
+
|
|
248
|
+
# ── 13: retrieve_database ────────────────────────────────────
|
|
249
|
+
@mcp.tool(name=f"{prefix}_retrieve_database", description=f"[{prefix}] Get database schema")
|
|
250
|
+
def retrieve_database(database_id: str) -> str:
|
|
251
|
+
try:
|
|
252
|
+
return json.dumps(client.databases.retrieve(database_id=database_id), ensure_ascii=False)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
255
|
+
|
|
256
|
+
# ── 14: create_database ──────────────────────────────────────
|
|
257
|
+
@mcp.tool(name=f"{prefix}_create_database", description=f"[{prefix}] Create a new database")
|
|
258
|
+
def create_database(
|
|
259
|
+
parent_json: str,
|
|
260
|
+
title_json: str,
|
|
261
|
+
properties_json: str,
|
|
262
|
+
icon_json: str | None = None,
|
|
263
|
+
cover_json: str | None = None,
|
|
264
|
+
) -> str:
|
|
265
|
+
try:
|
|
266
|
+
kwargs: dict = {
|
|
267
|
+
"parent": json.loads(parent_json),
|
|
268
|
+
"title": json.loads(title_json),
|
|
269
|
+
"properties": json.loads(properties_json),
|
|
270
|
+
}
|
|
271
|
+
if icon_json is not None:
|
|
272
|
+
kwargs["icon"] = json.loads(icon_json)
|
|
273
|
+
if cover_json is not None:
|
|
274
|
+
kwargs["cover"] = json.loads(cover_json)
|
|
275
|
+
return json.dumps(client.databases.create(**kwargs), ensure_ascii=False)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
278
|
+
|
|
279
|
+
# ── 15: update_database ──────────────────────────────────────
|
|
280
|
+
@mcp.tool(name=f"{prefix}_update_database", description=f"[{prefix}] Update database properties")
|
|
281
|
+
def update_database(
|
|
282
|
+
database_id: str,
|
|
283
|
+
title_json: str | None = None,
|
|
284
|
+
properties_json: str | None = None,
|
|
285
|
+
icon_json: str | None = None,
|
|
286
|
+
cover_json: str | None = None,
|
|
287
|
+
) -> str:
|
|
288
|
+
try:
|
|
289
|
+
kwargs: dict = {"database_id": database_id}
|
|
290
|
+
if title_json is not None:
|
|
291
|
+
kwargs["title"] = json.loads(title_json)
|
|
292
|
+
if properties_json is not None:
|
|
293
|
+
kwargs["properties"] = json.loads(properties_json)
|
|
294
|
+
if icon_json is not None:
|
|
295
|
+
kwargs["icon"] = json.loads(icon_json)
|
|
296
|
+
if cover_json is not None:
|
|
297
|
+
kwargs["cover"] = json.loads(cover_json)
|
|
298
|
+
return json.dumps(client.databases.update(**kwargs), ensure_ascii=False)
|
|
299
|
+
except Exception as e:
|
|
300
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
301
|
+
|
|
302
|
+
# ── 16: query_data_source ────────────────────────────────────
|
|
303
|
+
@mcp.tool(name=f"{prefix}_query_data_source", description=f"[{prefix}] Query a data source")
|
|
304
|
+
def query_data_source(
|
|
305
|
+
data_source_id: str,
|
|
306
|
+
filter_json: str | None = None,
|
|
307
|
+
sorts_json: str | None = None,
|
|
308
|
+
start_cursor: str | None = None,
|
|
309
|
+
page_size: int | None = None,
|
|
310
|
+
) -> str:
|
|
311
|
+
try:
|
|
312
|
+
body: dict = {}
|
|
313
|
+
if filter_json is not None:
|
|
314
|
+
body["filter"] = json.loads(filter_json)
|
|
315
|
+
if sorts_json is not None:
|
|
316
|
+
body["sorts"] = json.loads(sorts_json)
|
|
317
|
+
if start_cursor is not None:
|
|
318
|
+
body["start_cursor"] = start_cursor
|
|
319
|
+
if page_size is not None:
|
|
320
|
+
body["page_size"] = page_size
|
|
321
|
+
return json.dumps(_raw_request("POST", f"data_sources/{data_source_id}/query", body=body), ensure_ascii=False)
|
|
322
|
+
except Exception as e:
|
|
323
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
324
|
+
|
|
325
|
+
# ── 17: retrieve_data_source ─────────────────────────────────
|
|
326
|
+
@mcp.tool(name=f"{prefix}_retrieve_data_source", description=f"[{prefix}] Get data source info")
|
|
327
|
+
def retrieve_data_source(data_source_id: str) -> str:
|
|
328
|
+
try:
|
|
329
|
+
return json.dumps(_raw_request("GET", f"data_sources/{data_source_id}"), ensure_ascii=False)
|
|
330
|
+
except Exception as e:
|
|
331
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
332
|
+
|
|
333
|
+
# ── 18: list_data_source_templates ───────────────────────────
|
|
334
|
+
@mcp.tool(name=f"{prefix}_list_data_source_templates", description=f"[{prefix}] List data source templates")
|
|
335
|
+
def list_data_source_templates(data_source_id: str) -> str:
|
|
336
|
+
try:
|
|
337
|
+
return json.dumps(_raw_request("GET", f"data_sources/{data_source_id}/templates"), ensure_ascii=False)
|
|
338
|
+
except Exception as e:
|
|
339
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
340
|
+
|
|
341
|
+
# ── 19: update_data_source ───────────────────────────────────
|
|
342
|
+
@mcp.tool(name=f"{prefix}_update_data_source", description=f"[{prefix}] Update a data source")
|
|
343
|
+
def update_data_source(data_source_id: str, data_json: str) -> str:
|
|
344
|
+
try:
|
|
345
|
+
return json.dumps(
|
|
346
|
+
_raw_request("PATCH", f"data_sources/{data_source_id}", body=json.loads(data_json)),
|
|
347
|
+
ensure_ascii=False,
|
|
348
|
+
)
|
|
349
|
+
except Exception as e:
|
|
350
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
351
|
+
|
|
352
|
+
# ── 20: create_comment ───────────────────────────────────────
|
|
353
|
+
@mcp.tool(name=f"{prefix}_create_comment", description=f"[{prefix}] Create a comment")
|
|
354
|
+
def create_comment(
|
|
355
|
+
rich_text_json: str,
|
|
356
|
+
parent_json: str | None = None,
|
|
357
|
+
discussion_id: str | None = None,
|
|
358
|
+
) -> str:
|
|
359
|
+
try:
|
|
360
|
+
kwargs: dict = {"rich_text": json.loads(rich_text_json)}
|
|
361
|
+
if parent_json is not None:
|
|
362
|
+
kwargs["parent"] = json.loads(parent_json)
|
|
363
|
+
if discussion_id is not None:
|
|
364
|
+
kwargs["discussion_id"] = discussion_id
|
|
365
|
+
return json.dumps(client.comments.create(**kwargs), ensure_ascii=False)
|
|
366
|
+
except Exception as e:
|
|
367
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
368
|
+
|
|
369
|
+
# ── 21: retrieve_comments ────────────────────────────────────
|
|
370
|
+
@mcp.tool(name=f"{prefix}_retrieve_comments", description=f"[{prefix}] List comments")
|
|
371
|
+
def retrieve_comments(
|
|
372
|
+
block_id: str,
|
|
373
|
+
start_cursor: str | None = None,
|
|
374
|
+
page_size: int | None = None,
|
|
375
|
+
) -> str:
|
|
376
|
+
try:
|
|
377
|
+
kwargs: dict = {"block_id": block_id}
|
|
378
|
+
if start_cursor is not None:
|
|
379
|
+
kwargs["start_cursor"] = start_cursor
|
|
380
|
+
if page_size is not None:
|
|
381
|
+
kwargs["page_size"] = page_size
|
|
382
|
+
return json.dumps(client.comments.list(**kwargs), ensure_ascii=False)
|
|
383
|
+
except Exception as e:
|
|
384
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
385
|
+
|
|
386
|
+
# ── 22: get_self ─────────────────────────────────────────────
|
|
387
|
+
@mcp.tool(name=f"{prefix}_get_self", description=f"[{prefix}] Get bot user info")
|
|
388
|
+
def get_self() -> str:
|
|
389
|
+
try:
|
|
390
|
+
return json.dumps(client.users.me(), ensure_ascii=False)
|
|
391
|
+
except Exception as e:
|
|
392
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# ── Register tools for each account ────────────────────────────
|
|
396
|
+
for _prefix, _api_key in accounts:
|
|
397
|
+
_client = Client(auth=_api_key)
|
|
398
|
+
register_tools(_prefix, _client, _api_key)
|
|
399
|
+
|
|
400
|
+
print(
|
|
401
|
+
f"notion-multi-mcp: loaded {len(accounts)} account(s) "
|
|
402
|
+
f"({', '.join(p for p, _ in accounts)}) — {len(accounts) * 22} tools ready.",
|
|
403
|
+
file=sys.stderr,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# ── Entry point ─────────────────────────────────────────────────
|
|
408
|
+
def main():
|
|
409
|
+
mcp.run(transport="stdio")
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
if __name__ == "__main__":
|
|
413
|
+
main()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "notion-multi-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server for managing multiple Notion accounts simultaneously"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Kerwin Lin", email = "kerwin77106@gmail.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["notion", "mcp", "multi-account", "model-context-protocol"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"mcp",
|
|
29
|
+
"notion-client",
|
|
30
|
+
"httpx",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/kerwin77106/Notion-Multi-MCP"
|
|
35
|
+
Repository = "https://github.com/kerwin77106/Notion-Multi-MCP"
|
|
36
|
+
Issues = "https://github.com/kerwin77106/Notion-Multi-MCP/issues"
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
notion-multi-mcp = "notion_multi_mcp:main"
|