ldraney-notion-mcp 0.1.1__py3-none-any.whl
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.
- ldraney_notion_mcp-0.1.1.dist-info/METADATA +150 -0
- ldraney_notion_mcp-0.1.1.dist-info/RECORD +14 -0
- ldraney_notion_mcp-0.1.1.dist-info/WHEEL +5 -0
- ldraney_notion_mcp-0.1.1.dist-info/top_level.txt +1 -0
- notion_mcp/__init__.py +5 -0
- notion_mcp/__main__.py +5 -0
- notion_mcp/server.py +74 -0
- notion_mcp/tools/__init__.py +13 -0
- notion_mcp/tools/blocks.py +100 -0
- notion_mcp/tools/comments.py +61 -0
- notion_mcp/tools/databases.py +236 -0
- notion_mcp/tools/pages.py +121 -0
- notion_mcp/tools/search.py +39 -0
- notion_mcp/tools/users.py +38 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ldraney-notion-mcp
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: MCP server wrapping the Notion Python SDK (v2025-09-03)
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: mcp>=1.0
|
|
9
|
+
Requires-Dist: ldraney-notion-sdk>=0.1.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# notion-mcp
|
|
15
|
+
|
|
16
|
+
MCP (Model Context Protocol) server that wraps [`ldraney/notion-sdk`](https://github.com/ldraney/notion-sdk) — a Python SDK for the Notion API v2025-09-03.
|
|
17
|
+
|
|
18
|
+
## Overview
|
|
19
|
+
|
|
20
|
+
This project exposes the full Notion Python SDK as MCP tools, allowing AI assistants (Claude, etc.) to interact with Notion workspaces through a standardized tool interface.
|
|
21
|
+
|
|
22
|
+
## SDK Coverage
|
|
23
|
+
|
|
24
|
+
Every method in `notion-sdk` maps to an MCP tool, organized by module:
|
|
25
|
+
|
|
26
|
+
### Pages
|
|
27
|
+
| Tool | SDK Method | Notion Endpoint |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| `create_page` | `create_page()` | `POST /v1/pages` |
|
|
30
|
+
| `get_page` | `get_page()` | `GET /v1/pages/{id}` |
|
|
31
|
+
| `update_page` | `update_page()` | `PATCH /v1/pages/{id}` |
|
|
32
|
+
| `archive_page` | `archive_page()` | `PATCH /v1/pages/{id}` |
|
|
33
|
+
| `move_page` | `move_page()` | `POST /v1/pages/{id}/move` |
|
|
34
|
+
|
|
35
|
+
### Databases
|
|
36
|
+
| Tool | SDK Method | Notion Endpoint |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `create_database` | `create_database()` | `POST /v1/databases` |
|
|
39
|
+
| `get_database` | `get_database()` | `GET /v1/databases/{id}` |
|
|
40
|
+
| `update_database` | `update_database()` | `PATCH /v1/databases/{id}` |
|
|
41
|
+
| `archive_database` | `archive_database()` | `PATCH /v1/databases/{id}` |
|
|
42
|
+
| `query_database` | `query_database()` | auto-resolves data source, then `POST /v1/data_sources/{id}/query` |
|
|
43
|
+
|
|
44
|
+
### Data Sources
|
|
45
|
+
| Tool | SDK Method | Notion Endpoint |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `get_data_source` | `get_data_source()` | `GET /v1/data_sources/{id}` |
|
|
48
|
+
| `update_data_source` | `update_data_source()` | `PATCH /v1/data_sources/{id}` |
|
|
49
|
+
| `query_data_source` | `query_data_source()` | `POST /v1/data_sources/{id}/query` |
|
|
50
|
+
| `list_data_source_templates` | `list_data_source_templates()` | `GET /v1/data_sources/{id}/templates` |
|
|
51
|
+
|
|
52
|
+
### Blocks
|
|
53
|
+
| Tool | SDK Method | Notion Endpoint |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `get_block` | `get_block()` | `GET /v1/blocks/{id}` |
|
|
56
|
+
| `get_block_children` | `get_block_children()` | `GET /v1/blocks/{id}/children` |
|
|
57
|
+
| `append_block_children` | `append_block_children()` | `PATCH /v1/blocks/{id}/children` |
|
|
58
|
+
| `update_block` | `update_block()` | `PATCH /v1/blocks/{id}` |
|
|
59
|
+
| `delete_block` | `delete_block()` | `DELETE /v1/blocks/{id}` |
|
|
60
|
+
|
|
61
|
+
### Users
|
|
62
|
+
| Tool | SDK Method | Notion Endpoint |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `get_users` | `get_users()` | `GET /v1/users` |
|
|
65
|
+
| `get_self` | `get_self()` | `GET /v1/users/me` |
|
|
66
|
+
|
|
67
|
+
### Comments
|
|
68
|
+
| Tool | SDK Method | Notion Endpoint |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `create_comment` | `create_comment()` | `POST /v1/comments` |
|
|
71
|
+
| `get_comments` | `get_comments()` | `GET /v1/comments` |
|
|
72
|
+
|
|
73
|
+
### Search
|
|
74
|
+
| Tool | SDK Method | Notion Endpoint |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `search` | `search()` | `POST /v1/search` |
|
|
77
|
+
|
|
78
|
+
**24 tools total** covering the complete Notion API v2025-09-03 surface.
|
|
79
|
+
|
|
80
|
+
## Architecture
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
notion-mcp/
|
|
84
|
+
├── src/
|
|
85
|
+
│ └── notion_mcp/
|
|
86
|
+
│ ├── server.py # MCP server entry point
|
|
87
|
+
│ ├── tools/
|
|
88
|
+
│ │ ├── pages.py # Page tools
|
|
89
|
+
│ │ ├── databases.py # Database & data source tools
|
|
90
|
+
│ │ ├── blocks.py # Block tools
|
|
91
|
+
│ │ ├── users.py # User tools
|
|
92
|
+
│ │ ├── comments.py # Comment tools
|
|
93
|
+
│ │ └── search.py # Search tools
|
|
94
|
+
│ └── ...
|
|
95
|
+
├── tests/
|
|
96
|
+
├── pyproject.toml
|
|
97
|
+
└── README.md
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Each tool module mirrors the SDK's mixin structure, keeping a clean 1:1 mapping.
|
|
101
|
+
|
|
102
|
+
## Prerequisites
|
|
103
|
+
|
|
104
|
+
- Python >= 3.10
|
|
105
|
+
- A Notion integration API key (`NOTION_API_KEY`)
|
|
106
|
+
- [`notion-sdk`](https://github.com/ldraney/notion-sdk) (installed as a dependency)
|
|
107
|
+
|
|
108
|
+
## Setup
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Clone
|
|
112
|
+
git clone https://github.com/ldraney/notion-mcp.git
|
|
113
|
+
cd notion-mcp
|
|
114
|
+
|
|
115
|
+
# Install
|
|
116
|
+
pip install -e .
|
|
117
|
+
|
|
118
|
+
# Configure
|
|
119
|
+
export NOTION_API_KEY="ntn_..."
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Usage
|
|
123
|
+
|
|
124
|
+
### With Claude Code
|
|
125
|
+
|
|
126
|
+
Add to your Claude Code MCP config (`~/.claude/settings.json` or project settings):
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"mcpServers": {
|
|
131
|
+
"notion": {
|
|
132
|
+
"command": "python",
|
|
133
|
+
"args": ["-m", "notion_mcp"],
|
|
134
|
+
"env": {
|
|
135
|
+
"NOTION_API_KEY": "ntn_..."
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Standalone
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
python -m notion_mcp
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Related
|
|
149
|
+
|
|
150
|
+
- [`ldraney/notion-sdk`](https://github.com/ldraney/notion-sdk) — The underlying Python SDK this server wraps
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
notion_mcp/__init__.py,sha256=Wy2Os0Ww79mbGvySlmb9IVPW9t-oUPO246-8PppLqUI,105
|
|
2
|
+
notion_mcp/__main__.py,sha256=8iuAFwnkWnvYNFN7qLyPeNa-qlKbzQMO6b7li5yrRFU,83
|
|
3
|
+
notion_mcp/server.py,sha256=V_1bPrjEHh2LgmPR4lZQpUMEiVJiL2yGG1j8Lxkzwpc,2242
|
|
4
|
+
notion_mcp/tools/__init__.py,sha256=Dak6lGr2H_aG5SYNAfi_1lACTqwAPDAAjIyVmGja8as,496
|
|
5
|
+
notion_mcp/tools/blocks.py,sha256=9OTGJS6XhRzZe0sZk1ArgN-gyGmfcuTMek7HnhNk-ew,2674
|
|
6
|
+
notion_mcp/tools/comments.py,sha256=eLPQZSIDd6wae9hVcnp5v_TpMu9a01k7wUJpjNBnq4E,1792
|
|
7
|
+
notion_mcp/tools/databases.py,sha256=7L_M7Y-AX82S24TuZY88sJ6SoEECnZkq3muKYTExjqk,7360
|
|
8
|
+
notion_mcp/tools/pages.py,sha256=8FuUCJwASwt17IfpqO3dJWoz-FwXarKJC8VlHz53oX4,3638
|
|
9
|
+
notion_mcp/tools/search.py,sha256=FwaiR-4vQ7SwGcCNKvVeMBk9TLTWrtlAi_Baf9HYyw8,1188
|
|
10
|
+
notion_mcp/tools/users.py,sha256=C0ytVNk7lDpQfcw4Wir39QII97LUtINf62j7fKpD-IQ,945
|
|
11
|
+
ldraney_notion_mcp-0.1.1.dist-info/METADATA,sha256=vZtf860GXTOnmRFz7hQAAT2Cm-Z1hqQhpH9mX77QvsI,4542
|
|
12
|
+
ldraney_notion_mcp-0.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
13
|
+
ldraney_notion_mcp-0.1.1.dist-info/top_level.txt,sha256=lFXGlrgRGHEtgwNKPCohd0H0PxUReLzGdpd6YzborzM,11
|
|
14
|
+
ldraney_notion_mcp-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
notion_mcp
|
notion_mcp/__init__.py
ADDED
notion_mcp/__main__.py
ADDED
notion_mcp/server.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""MCP server entry point — FastMCP app and NotionClient lifecycle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from mcp.server.fastmcp import FastMCP
|
|
10
|
+
from notion_sdk import NotionClient
|
|
11
|
+
|
|
12
|
+
mcp = FastMCP("notion")
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Shared client instance
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
_client: NotionClient | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_client() -> NotionClient:
|
|
22
|
+
"""Return the shared NotionClient, creating it on first call.
|
|
23
|
+
|
|
24
|
+
The client reads NOTION_API_KEY from the environment (via the SDK).
|
|
25
|
+
"""
|
|
26
|
+
global _client
|
|
27
|
+
if _client is None:
|
|
28
|
+
_client = NotionClient()
|
|
29
|
+
return _client
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Helpers
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _parse_json(value: str | dict | list | None, name: str) -> Any:
|
|
38
|
+
"""Parse a JSON string into a Python object, or pass through if already parsed."""
|
|
39
|
+
if value is None:
|
|
40
|
+
return None
|
|
41
|
+
if isinstance(value, (dict, list)):
|
|
42
|
+
return value
|
|
43
|
+
try:
|
|
44
|
+
return json.loads(value)
|
|
45
|
+
except json.JSONDecodeError as exc:
|
|
46
|
+
raise ValueError(f"Invalid JSON for parameter '{name}': {exc}") from exc
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _error_response(exc: Exception) -> str:
|
|
50
|
+
"""Format an exception into a user-friendly error string."""
|
|
51
|
+
if isinstance(exc, httpx.HTTPStatusError):
|
|
52
|
+
try:
|
|
53
|
+
body = exc.response.json()
|
|
54
|
+
except Exception:
|
|
55
|
+
body = exc.response.text
|
|
56
|
+
return json.dumps(
|
|
57
|
+
{
|
|
58
|
+
"error": True,
|
|
59
|
+
"status_code": exc.response.status_code,
|
|
60
|
+
"message": str(exc),
|
|
61
|
+
"details": body,
|
|
62
|
+
},
|
|
63
|
+
indent=2,
|
|
64
|
+
)
|
|
65
|
+
return json.dumps({"error": True, "message": str(exc)}, indent=2)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Register tool modules — each module calls @mcp.tool() at import time
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
from .tools import register_all_tools # noqa: E402
|
|
73
|
+
|
|
74
|
+
register_all_tools()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Tool registration — imports every tool module so @mcp.tool() decorators fire."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def register_all_tools() -> None:
|
|
7
|
+
"""Import all tool modules, which register tools via the module-level @mcp.tool() decorators."""
|
|
8
|
+
from . import pages # noqa: F401
|
|
9
|
+
from . import databases # noqa: F401
|
|
10
|
+
from . import blocks # noqa: F401
|
|
11
|
+
from . import users # noqa: F401
|
|
12
|
+
from . import comments # noqa: F401
|
|
13
|
+
from . import search # noqa: F401
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Block tools — wraps BlocksMixin methods."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..server import mcp, get_client, _parse_json, _error_response
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@mcp.tool()
|
|
12
|
+
def get_block(block_id: str) -> str:
|
|
13
|
+
"""Retrieve a Notion block by its ID.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
block_id: The UUID of the block to retrieve.
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
result = get_client().get_block(block_id)
|
|
20
|
+
return json.dumps(result, indent=2)
|
|
21
|
+
except Exception as exc:
|
|
22
|
+
return _error_response(exc)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@mcp.tool()
|
|
26
|
+
def get_block_children(
|
|
27
|
+
block_id: str,
|
|
28
|
+
start_cursor: str | None = None,
|
|
29
|
+
page_size: int | None = None,
|
|
30
|
+
) -> str:
|
|
31
|
+
"""List child blocks of a Notion block.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
block_id: The UUID of the parent block.
|
|
35
|
+
start_cursor: Optional cursor for pagination.
|
|
36
|
+
page_size: Optional number of results per page.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
result = get_client().get_block_children(
|
|
40
|
+
block_id,
|
|
41
|
+
start_cursor=start_cursor,
|
|
42
|
+
page_size=page_size,
|
|
43
|
+
)
|
|
44
|
+
return json.dumps(result, indent=2)
|
|
45
|
+
except Exception as exc:
|
|
46
|
+
return _error_response(exc)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@mcp.tool()
|
|
50
|
+
def append_block_children(block_id: str, children: str) -> str:
|
|
51
|
+
"""Append child blocks to a Notion block.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
block_id: The UUID of the parent block to append children to.
|
|
55
|
+
children: JSON string for a list of block objects to append.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
result = get_client().append_block_children(
|
|
59
|
+
block_id,
|
|
60
|
+
children=_parse_json(children, "children"),
|
|
61
|
+
)
|
|
62
|
+
return json.dumps(result, indent=2)
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
return _error_response(exc)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@mcp.tool()
|
|
68
|
+
def update_block(
|
|
69
|
+
block_id: str,
|
|
70
|
+
content: str | None = None,
|
|
71
|
+
) -> str:
|
|
72
|
+
"""Update a Notion block.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
block_id: The UUID of the block to update.
|
|
76
|
+
content: JSON string of block properties to update. The keys depend
|
|
77
|
+
on the block type (e.g. {"paragraph": {"rich_text": [...]}}).
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
kwargs: dict[str, Any] = {}
|
|
81
|
+
if content is not None:
|
|
82
|
+
kwargs = _parse_json(content, "content")
|
|
83
|
+
result = get_client().update_block(block_id, **kwargs)
|
|
84
|
+
return json.dumps(result, indent=2)
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
return _error_response(exc)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@mcp.tool()
|
|
90
|
+
def delete_block(block_id: str) -> str:
|
|
91
|
+
"""Delete (archive) a Notion block.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
block_id: The UUID of the block to delete.
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
result = get_client().delete_block(block_id)
|
|
98
|
+
return json.dumps(result, indent=2)
|
|
99
|
+
except Exception as exc:
|
|
100
|
+
return _error_response(exc)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Comment tools — wraps CommentsMixin methods."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from ..server import mcp, get_client, _parse_json, _error_response
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
def create_comment(
|
|
12
|
+
parent: str,
|
|
13
|
+
rich_text: str,
|
|
14
|
+
discussion_id: str | None = None,
|
|
15
|
+
) -> str:
|
|
16
|
+
"""Create a comment on a Notion page or block.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
parent: JSON string for the parent object,
|
|
20
|
+
e.g. {"page_id": "..."}.
|
|
21
|
+
rich_text: JSON string for the rich-text content array,
|
|
22
|
+
e.g. [{"type": "text", "text": {"content": "Hello!"}}].
|
|
23
|
+
discussion_id: Optional UUID of an existing discussion thread
|
|
24
|
+
to reply to. Omit to start a new top-level comment.
|
|
25
|
+
"""
|
|
26
|
+
try:
|
|
27
|
+
kwargs: dict[str, object] = {}
|
|
28
|
+
if discussion_id is not None:
|
|
29
|
+
kwargs["discussion_id"] = discussion_id
|
|
30
|
+
result = get_client().create_comment(
|
|
31
|
+
parent=_parse_json(parent, "parent"),
|
|
32
|
+
rich_text=_parse_json(rich_text, "rich_text"),
|
|
33
|
+
**kwargs,
|
|
34
|
+
)
|
|
35
|
+
return json.dumps(result, indent=2)
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
return _error_response(exc)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@mcp.tool()
|
|
41
|
+
def get_comments(
|
|
42
|
+
block_id: str,
|
|
43
|
+
start_cursor: str | None = None,
|
|
44
|
+
page_size: int | None = None,
|
|
45
|
+
) -> str:
|
|
46
|
+
"""List comments on a Notion block or page.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
block_id: The UUID of the block or page to list comments for.
|
|
50
|
+
start_cursor: Optional cursor for pagination.
|
|
51
|
+
page_size: Optional number of results per page.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
result = get_client().get_comments(
|
|
55
|
+
block_id,
|
|
56
|
+
start_cursor=start_cursor,
|
|
57
|
+
page_size=page_size,
|
|
58
|
+
)
|
|
59
|
+
return json.dumps(result, indent=2)
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
return _error_response(exc)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Database and data source tools — wraps DatabasesMixin methods."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..server import mcp, get_client, _parse_json, _error_response
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
# Database tools
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@mcp.tool()
|
|
17
|
+
def create_database(
|
|
18
|
+
parent: str,
|
|
19
|
+
title: str,
|
|
20
|
+
initial_data_source: str | None = None,
|
|
21
|
+
) -> str:
|
|
22
|
+
"""Create a new Notion database.
|
|
23
|
+
|
|
24
|
+
In Notion API v2025-09-03, database properties live on data sources.
|
|
25
|
+
Pass properties inside initial_data_source.properties.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
parent: JSON string for the parent object, e.g. {"type": "page_id", "page_id": "..."}.
|
|
29
|
+
title: JSON string for the title rich-text array,
|
|
30
|
+
e.g. [{"type": "text", "text": {"content": "My DB"}}].
|
|
31
|
+
initial_data_source: Optional JSON string for the initial data source
|
|
32
|
+
configuration including properties.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
result = get_client().create_database(
|
|
36
|
+
parent=_parse_json(parent, "parent"),
|
|
37
|
+
title=_parse_json(title, "title"),
|
|
38
|
+
initial_data_source=_parse_json(initial_data_source, "initial_data_source"),
|
|
39
|
+
)
|
|
40
|
+
return json.dumps(result, indent=2)
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
return _error_response(exc)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@mcp.tool()
|
|
46
|
+
def get_database(database_id: str) -> str:
|
|
47
|
+
"""Retrieve a Notion database by its ID.
|
|
48
|
+
|
|
49
|
+
Note: In v2025-09-03 the response contains data_sources but NOT
|
|
50
|
+
properties. Use get_data_source to inspect properties.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
database_id: The UUID of the database to retrieve.
|
|
54
|
+
"""
|
|
55
|
+
try:
|
|
56
|
+
result = get_client().get_database(database_id)
|
|
57
|
+
return json.dumps(result, indent=2)
|
|
58
|
+
except Exception as exc:
|
|
59
|
+
return _error_response(exc)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@mcp.tool()
|
|
63
|
+
def update_database(
|
|
64
|
+
database_id: str,
|
|
65
|
+
title: str | None = None,
|
|
66
|
+
description: str | None = None,
|
|
67
|
+
icon: str | None = None,
|
|
68
|
+
cover: str | None = None,
|
|
69
|
+
) -> str:
|
|
70
|
+
"""Update a Notion database.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
database_id: The UUID of the database to update.
|
|
74
|
+
title: Optional JSON string for the new title rich-text array.
|
|
75
|
+
description: Optional JSON string for the description rich-text array.
|
|
76
|
+
icon: Optional JSON string for the database icon.
|
|
77
|
+
cover: Optional JSON string for the database cover.
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
kwargs: dict[str, Any] = {}
|
|
81
|
+
if title is not None:
|
|
82
|
+
kwargs["title"] = _parse_json(title, "title")
|
|
83
|
+
if description is not None:
|
|
84
|
+
kwargs["description"] = _parse_json(description, "description")
|
|
85
|
+
if icon is not None:
|
|
86
|
+
kwargs["icon"] = _parse_json(icon, "icon")
|
|
87
|
+
if cover is not None:
|
|
88
|
+
kwargs["cover"] = _parse_json(cover, "cover")
|
|
89
|
+
result = get_client().update_database(database_id, **kwargs)
|
|
90
|
+
return json.dumps(result, indent=2)
|
|
91
|
+
except Exception as exc:
|
|
92
|
+
return _error_response(exc)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@mcp.tool()
|
|
96
|
+
def archive_database(database_id: str) -> str:
|
|
97
|
+
"""Archive a Notion database.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
database_id: The UUID of the database to archive.
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
result = get_client().archive_database(database_id)
|
|
104
|
+
return json.dumps(result, indent=2)
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
return _error_response(exc)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@mcp.tool()
|
|
110
|
+
def query_database(
|
|
111
|
+
database_id: str,
|
|
112
|
+
filter: str | None = None,
|
|
113
|
+
sorts: str | None = None,
|
|
114
|
+
start_cursor: str | None = None,
|
|
115
|
+
page_size: int | None = None,
|
|
116
|
+
) -> str:
|
|
117
|
+
"""Query a Notion database for pages/rows.
|
|
118
|
+
|
|
119
|
+
Automatically resolves the first data source and queries it.
|
|
120
|
+
If you already know the data source ID, use query_data_source instead.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
database_id: The UUID of the database to query.
|
|
124
|
+
filter: Optional JSON string for a filter object.
|
|
125
|
+
sorts: Optional JSON string for a list of sort objects.
|
|
126
|
+
start_cursor: Optional cursor for pagination.
|
|
127
|
+
page_size: Optional number of results per page.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
result = get_client().query_database(
|
|
131
|
+
database_id,
|
|
132
|
+
filter=_parse_json(filter, "filter"),
|
|
133
|
+
sorts=_parse_json(sorts, "sorts"),
|
|
134
|
+
start_cursor=start_cursor,
|
|
135
|
+
page_size=page_size,
|
|
136
|
+
)
|
|
137
|
+
return json.dumps(result, indent=2)
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
return _error_response(exc)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Data source tools
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@mcp.tool()
|
|
148
|
+
def get_data_source(data_source_id: str) -> str:
|
|
149
|
+
"""Retrieve a Notion data source (includes properties).
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
data_source_id: The UUID of the data source to retrieve.
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
result = get_client().get_data_source(data_source_id)
|
|
156
|
+
return json.dumps(result, indent=2)
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
return _error_response(exc)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@mcp.tool()
|
|
162
|
+
def update_data_source(
|
|
163
|
+
data_source_id: str,
|
|
164
|
+
properties: str | None = None,
|
|
165
|
+
) -> str:
|
|
166
|
+
"""Update a Notion data source.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
data_source_id: The UUID of the data source to update.
|
|
170
|
+
properties: Optional JSON string for properties to update.
|
|
171
|
+
"""
|
|
172
|
+
try:
|
|
173
|
+
kwargs: dict[str, Any] = {}
|
|
174
|
+
if properties is not None:
|
|
175
|
+
kwargs["properties"] = _parse_json(properties, "properties")
|
|
176
|
+
result = get_client().update_data_source(data_source_id, **kwargs)
|
|
177
|
+
return json.dumps(result, indent=2)
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
return _error_response(exc)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@mcp.tool()
|
|
183
|
+
def query_data_source(
|
|
184
|
+
data_source_id: str,
|
|
185
|
+
filter: str | None = None,
|
|
186
|
+
sorts: str | None = None,
|
|
187
|
+
start_cursor: str | None = None,
|
|
188
|
+
page_size: int | None = None,
|
|
189
|
+
) -> str:
|
|
190
|
+
"""Query rows in a Notion data source.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
data_source_id: The UUID of the data source to query.
|
|
194
|
+
filter: Optional JSON string for a filter object.
|
|
195
|
+
sorts: Optional JSON string for a list of sort objects.
|
|
196
|
+
start_cursor: Optional cursor for pagination.
|
|
197
|
+
page_size: Optional number of results per page.
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
result = get_client().query_data_source(
|
|
201
|
+
data_source_id,
|
|
202
|
+
filter=_parse_json(filter, "filter"),
|
|
203
|
+
sorts=_parse_json(sorts, "sorts"),
|
|
204
|
+
start_cursor=start_cursor,
|
|
205
|
+
page_size=page_size,
|
|
206
|
+
)
|
|
207
|
+
return json.dumps(result, indent=2)
|
|
208
|
+
except Exception as exc:
|
|
209
|
+
return _error_response(exc)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@mcp.tool()
|
|
213
|
+
def list_data_source_templates(
|
|
214
|
+
data_source_id: str,
|
|
215
|
+
name: str | None = None,
|
|
216
|
+
start_cursor: str | None = None,
|
|
217
|
+
page_size: int | None = None,
|
|
218
|
+
) -> str:
|
|
219
|
+
"""List templates for a Notion data source.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
data_source_id: The UUID of the data source.
|
|
223
|
+
name: Optional name filter for templates.
|
|
224
|
+
start_cursor: Optional cursor for pagination.
|
|
225
|
+
page_size: Optional number of results per page.
|
|
226
|
+
"""
|
|
227
|
+
try:
|
|
228
|
+
result = get_client().list_data_source_templates(
|
|
229
|
+
data_source_id,
|
|
230
|
+
name=name,
|
|
231
|
+
start_cursor=start_cursor,
|
|
232
|
+
page_size=page_size,
|
|
233
|
+
)
|
|
234
|
+
return json.dumps(result, indent=2)
|
|
235
|
+
except Exception as exc:
|
|
236
|
+
return _error_response(exc)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Page tools — wraps PagesMixin methods."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..server import mcp, get_client, _parse_json, _error_response
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@mcp.tool()
|
|
12
|
+
def create_page(
|
|
13
|
+
parent: str,
|
|
14
|
+
properties: str,
|
|
15
|
+
children: str | None = None,
|
|
16
|
+
template: str | None = None,
|
|
17
|
+
) -> str:
|
|
18
|
+
"""Create a new Notion page.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
parent: JSON string for the parent object, e.g. {"type": "page_id", "page_id": "..."}.
|
|
22
|
+
properties: JSON string for the page properties mapping.
|
|
23
|
+
children: Optional JSON string for a list of block children to append.
|
|
24
|
+
Cannot be used together with template.
|
|
25
|
+
template: Optional JSON string for a data-source template, e.g.
|
|
26
|
+
{"type": "none"}, {"type": "default"}, or
|
|
27
|
+
{"type": "template_id", "template_id": "<uuid>"}.
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
result = get_client().create_page(
|
|
31
|
+
parent=_parse_json(parent, "parent"),
|
|
32
|
+
properties=_parse_json(properties, "properties"),
|
|
33
|
+
children=_parse_json(children, "children"),
|
|
34
|
+
template=_parse_json(template, "template"),
|
|
35
|
+
)
|
|
36
|
+
return json.dumps(result, indent=2)
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
return _error_response(exc)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@mcp.tool()
|
|
42
|
+
def get_page(page_id: str) -> str:
|
|
43
|
+
"""Retrieve a Notion page by its ID.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
page_id: The UUID of the page to retrieve.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
result = get_client().get_page(page_id)
|
|
50
|
+
return json.dumps(result, indent=2)
|
|
51
|
+
except Exception as exc:
|
|
52
|
+
return _error_response(exc)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@mcp.tool()
|
|
56
|
+
def update_page(
|
|
57
|
+
page_id: str,
|
|
58
|
+
properties: str | None = None,
|
|
59
|
+
erase_content: bool | None = None,
|
|
60
|
+
icon: str | None = None,
|
|
61
|
+
cover: str | None = None,
|
|
62
|
+
) -> str:
|
|
63
|
+
"""Update a Notion page's properties, icon, or cover.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
page_id: The UUID of the page to update.
|
|
67
|
+
properties: Optional JSON string of properties to update.
|
|
68
|
+
erase_content: If true, clears ALL block content from the page.
|
|
69
|
+
WARNING: This is destructive and irreversible.
|
|
70
|
+
icon: Optional JSON string for the page icon.
|
|
71
|
+
cover: Optional JSON string for the page cover.
|
|
72
|
+
"""
|
|
73
|
+
try:
|
|
74
|
+
kwargs: dict[str, Any] = {}
|
|
75
|
+
if properties is not None:
|
|
76
|
+
kwargs["properties"] = _parse_json(properties, "properties")
|
|
77
|
+
if icon is not None:
|
|
78
|
+
kwargs["icon"] = _parse_json(icon, "icon")
|
|
79
|
+
if cover is not None:
|
|
80
|
+
kwargs["cover"] = _parse_json(cover, "cover")
|
|
81
|
+
result = get_client().update_page(
|
|
82
|
+
page_id,
|
|
83
|
+
erase_content=erase_content,
|
|
84
|
+
**kwargs,
|
|
85
|
+
)
|
|
86
|
+
return json.dumps(result, indent=2)
|
|
87
|
+
except Exception as exc:
|
|
88
|
+
return _error_response(exc)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@mcp.tool()
|
|
92
|
+
def archive_page(page_id: str) -> str:
|
|
93
|
+
"""Archive (soft-delete) a Notion page.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
page_id: The UUID of the page to archive.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
result = get_client().archive_page(page_id)
|
|
100
|
+
return json.dumps(result, indent=2)
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
return _error_response(exc)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@mcp.tool()
|
|
106
|
+
def move_page(page_id: str, parent: str) -> str:
|
|
107
|
+
"""Move a Notion page to a new parent.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
page_id: The UUID of the page to move.
|
|
111
|
+
parent: JSON string for the new parent object,
|
|
112
|
+
e.g. {"type": "page_id", "page_id": "..."}.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
result = get_client().move_page(
|
|
116
|
+
page_id,
|
|
117
|
+
parent=_parse_json(parent, "parent"),
|
|
118
|
+
)
|
|
119
|
+
return json.dumps(result, indent=2)
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
return _error_response(exc)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Search tool — wraps SearchMixin method."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from ..server import mcp, get_client, _parse_json, _error_response
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
def search(
|
|
12
|
+
query: str | None = None,
|
|
13
|
+
filter: str | None = None,
|
|
14
|
+
sort: str | None = None,
|
|
15
|
+
start_cursor: str | None = None,
|
|
16
|
+
page_size: int | None = None,
|
|
17
|
+
) -> str:
|
|
18
|
+
"""Search Notion pages and databases.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
query: Optional text query to search for.
|
|
22
|
+
filter: Optional JSON string for a filter object,
|
|
23
|
+
e.g. {"value": "page", "property": "object"}.
|
|
24
|
+
sort: Optional JSON string for a sort object,
|
|
25
|
+
e.g. {"direction": "ascending", "timestamp": "last_edited_time"}.
|
|
26
|
+
start_cursor: Optional cursor for pagination.
|
|
27
|
+
page_size: Optional number of results per page.
|
|
28
|
+
"""
|
|
29
|
+
try:
|
|
30
|
+
result = get_client().search(
|
|
31
|
+
query=query,
|
|
32
|
+
filter=_parse_json(filter, "filter"),
|
|
33
|
+
sort=_parse_json(sort, "sort"),
|
|
34
|
+
start_cursor=start_cursor,
|
|
35
|
+
page_size=page_size,
|
|
36
|
+
)
|
|
37
|
+
return json.dumps(result, indent=2)
|
|
38
|
+
except Exception as exc:
|
|
39
|
+
return _error_response(exc)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""User tools — wraps UsersMixin methods."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from ..server import mcp, get_client, _error_response
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
def get_users(
|
|
12
|
+
start_cursor: str | None = None,
|
|
13
|
+
page_size: int | None = None,
|
|
14
|
+
) -> str:
|
|
15
|
+
"""List all users in the Notion workspace.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
start_cursor: Optional cursor for pagination.
|
|
19
|
+
page_size: Optional number of results per page.
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
result = get_client().get_users(
|
|
23
|
+
start_cursor=start_cursor,
|
|
24
|
+
page_size=page_size,
|
|
25
|
+
)
|
|
26
|
+
return json.dumps(result, indent=2)
|
|
27
|
+
except Exception as exc:
|
|
28
|
+
return _error_response(exc)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@mcp.tool()
|
|
32
|
+
def get_self() -> str:
|
|
33
|
+
"""Retrieve the bot user associated with the current API token."""
|
|
34
|
+
try:
|
|
35
|
+
result = get_client().get_self()
|
|
36
|
+
return json.dumps(result, indent=2)
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
return _error_response(exc)
|