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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ notion_mcp
notion_mcp/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """notion-mcp: MCP server wrapping the Notion Python SDK."""
2
+
3
+ from .server import mcp
4
+
5
+ __all__ = ["mcp"]
notion_mcp/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running as `python -m notion_mcp`."""
2
+
3
+ from .server import mcp
4
+
5
+ mcp.run()
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)