mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 23:50:17 +00:00
Add hosted MCP server support with OAuth 2.1 and token refresh
Enables connecting to official MCP servers (like Notion) instead of building custom WASM tools. Uses OAuth 2.1 with PKCE and supports Dynamic Client Registration for zero-config authentication. Key features: - OAuth 2.1 flow with PKCE for secure browser-based auth - Dynamic Client Registration (DCR) for servers without pre-configured clients - Automatic token refresh on 401 responses - Session management with Mcp-Session-Id headers - SSE streaming response handling New CLI commands: - `mcp add <name> <url>` - Add an MCP server - `mcp remove <name>` - Remove an MCP server - `mcp list` - List configured servers - `mcp auth <name>` - Authenticate with a server - `mcp test <name>` - Test connection Also removes the Notion WASM tool example since it's superseded by the Notion MCP server which provides 13 official tools. Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
5992e27507
commit
974bc8d407
@@ -1 +0,0 @@
|
||||
/target
|
||||
Generated
-401
@@ -1,401 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "leb128"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "notion-tool"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "spdx"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.220.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e913f9242315ca39eff82aee0e19ee7a372155717ff0eb082c741e435ce25ed1"
|
||||
dependencies = [
|
||||
"leb128",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.220.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "185dfcd27fa5db2e6a23906b54c28199935f71d9a27a1a27b3a88d6fee2afae7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"spdx",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.220.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d07b6a3b550fefa1a914b6d54fc175dd11c3392da11eee604e6ffc759805d25"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bitflags",
|
||||
"hashbrown 0.14.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a2b3e15cd6068f233926e7d8c7c588b2ec4fb7cc7bf3824115e7c7e2a8485a3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b632a5a0fa2409489bd49c9e6d99fcc61bb3d4ce9d1907d44662e75a28c71172"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7947d0131c7c9da3f01dfde0ab8bd4c4cf3c5bd49b6dba0ae640f1fa752572ea"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4329de4186ee30e2ef30a0533f9b3c123c019a237a7c82d692807bf1b3ee2697"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "177fb7ee1484d113b4792cc480b1ba57664bbc951b42a4beebe573502135b1fc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.220.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b505603761ed400c90ed30261f44a768317348e49f1864e82ecdc3b2744e5627"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.220.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae2a7999ed18efe59be8de2db9cb2b7f84d88b27818c79353dfc53131840fe1a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
|
||||
@@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "notion-tool"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Notion integration tool for NEAR Agent (WASM component)"
|
||||
license = "MIT OR Apache-2.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
wit-bindgen = "0.36"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
strip = true
|
||||
codegen-units = 1
|
||||
@@ -1,186 +0,0 @@
|
||||
# Notion WASM Tool
|
||||
|
||||
A WASM-sandboxed Notion integration for NEAR Agent. Follows the MCP server pattern with domain-grouped operations.
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
cargo build --release --target wasm32-wasip1
|
||||
```
|
||||
|
||||
The compiled module will be at `target/wasm32-wasip1/release/notion_tool.wasm`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
This tool requires:
|
||||
|
||||
- **HTTP**: Access to `api.notion.com/v1/*` (GET, POST, PATCH, DELETE)
|
||||
- **Secrets**: `notion_api_token` (injected as Bearer authorization)
|
||||
|
||||
See `notion_tool.capabilities.json` for the full capability configuration.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Create a Notion integration at https://www.notion.so/my-integrations
|
||||
2. Copy the "Internal Integration Secret"
|
||||
3. Add it to the agent's secrets store as `notion_api_token`
|
||||
4. Share your Notion pages/databases with the integration
|
||||
|
||||
## Supported Actions
|
||||
|
||||
### Search
|
||||
|
||||
| Action | Required Params | Optional Params |
|
||||
|--------|-----------------|-----------------|
|
||||
| `search` | - | `query`, `filter`, `page_size`, `start_cursor` |
|
||||
|
||||
### Pages
|
||||
|
||||
| Action | Required Params | Optional Params |
|
||||
|--------|-----------------|-----------------|
|
||||
| `get_page` | `page_id` | - |
|
||||
| `create_page` | `parent`, `properties` | `children`, `icon`, `cover` |
|
||||
| `update_page` | `page_id`, `properties` | `icon`, `cover` |
|
||||
| `archive_page` | `page_id` | - |
|
||||
| `restore_page` | `page_id` | - |
|
||||
|
||||
### Blocks
|
||||
|
||||
| Action | Required Params | Optional Params |
|
||||
|--------|-----------------|-----------------|
|
||||
| `get_blocks` | `block_id` | `page_size`, `start_cursor` |
|
||||
| `append_blocks` | `block_id`, `children` | `after` |
|
||||
| `get_block` | `block_id` | - |
|
||||
| `update_block` | `block_id`, `content` | - |
|
||||
| `delete_block` | `block_id` | - |
|
||||
|
||||
### Databases
|
||||
|
||||
| Action | Required Params | Optional Params |
|
||||
|--------|-----------------|-----------------|
|
||||
| `get_database` | `database_id` | - |
|
||||
| `query_database` | `database_id` | `filter`, `sorts`, `page_size`, `start_cursor` |
|
||||
| `create_database` | `parent`, `title`, `properties` | `icon`, `cover`, `is_inline` |
|
||||
| `update_database` | `database_id` | `title`, `properties` |
|
||||
|
||||
### Comments
|
||||
|
||||
| Action | Required Params | Optional Params |
|
||||
|--------|-----------------|-----------------|
|
||||
| `get_comments` | `block_id` | `page_size`, `start_cursor` |
|
||||
| `add_comment` | `parent`, `rich_text` | - |
|
||||
|
||||
### Users
|
||||
|
||||
| Action | Required Params | Optional Params |
|
||||
|--------|-----------------|-----------------|
|
||||
| `list_users` | - | `page_size`, `start_cursor` |
|
||||
| `get_user` | `user_id` | - |
|
||||
| `get_me` | - | - |
|
||||
|
||||
## Examples
|
||||
|
||||
### Search for pages
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "search",
|
||||
"query": "meeting notes",
|
||||
"filter": { "property": "object", "value": "page" },
|
||||
"page_size": 10
|
||||
}
|
||||
```
|
||||
|
||||
### Query a database with filters
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "query_database",
|
||||
"database_id": "abc123-def456-...",
|
||||
"filter": {
|
||||
"property": "Status",
|
||||
"select": { "equals": "Done" }
|
||||
},
|
||||
"sorts": [
|
||||
{ "property": "Created", "direction": "descending" }
|
||||
],
|
||||
"page_size": 20
|
||||
}
|
||||
```
|
||||
|
||||
### Create a page in a database
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "create_page",
|
||||
"parent": { "database_id": "abc123-def456-..." },
|
||||
"properties": {
|
||||
"Name": {
|
||||
"title": [{ "text": { "content": "New Task" } }]
|
||||
},
|
||||
"Status": {
|
||||
"select": { "name": "To Do" }
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"object": "block",
|
||||
"type": "paragraph",
|
||||
"paragraph": {
|
||||
"rich_text": [{ "text": { "content": "Task description here." } }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Append content to a page
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "append_blocks",
|
||||
"block_id": "page-id-here",
|
||||
"children": [
|
||||
{
|
||||
"object": "block",
|
||||
"type": "heading_2",
|
||||
"heading_2": {
|
||||
"rich_text": [{ "text": { "content": "New Section" } }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"object": "block",
|
||||
"type": "paragraph",
|
||||
"paragraph": {
|
||||
"rich_text": [{ "text": { "content": "Some content..." } }]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Add a comment
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "add_comment",
|
||||
"parent": { "page_id": "page-id-here" },
|
||||
"rich_text": [
|
||||
{ "text": { "content": "This is a comment from the agent." } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
- Runs in WASM sandbox with fuel metering and memory limits
|
||||
- API token injected at host boundary (never visible to WASM)
|
||||
- Only `api.notion.com/v1/*` endpoints allowed
|
||||
- Responses scanned for secret leakage
|
||||
- Rate limited: 50 req/min, 1000 req/hour
|
||||
|
||||
## References
|
||||
|
||||
- [Notion API Documentation](https://developers.notion.com/reference/intro)
|
||||
- [Notion MCP Server](https://github.com/makenotion/notion-mcp-server)
|
||||
- [NEAR Agent WASM Tool System](../../../src/tools/wasm/)
|
||||
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"http": {
|
||||
"allowlist": [
|
||||
{
|
||||
"host": "api.notion.com",
|
||||
"path_prefix": "/v1/",
|
||||
"methods": ["GET", "POST", "PATCH", "DELETE"]
|
||||
}
|
||||
],
|
||||
"credentials": {
|
||||
"notion_api_token": {
|
||||
"secret_name": "notion_api_token",
|
||||
"location": { "type": "bearer" },
|
||||
"host_patterns": ["api.notion.com"]
|
||||
}
|
||||
},
|
||||
"rate_limit": {
|
||||
"requests_per_minute": 50,
|
||||
"requests_per_hour": 1000
|
||||
},
|
||||
"timeout_secs": 30
|
||||
},
|
||||
"secrets": {
|
||||
"allowed_names": ["notion_api_token"]
|
||||
}
|
||||
}
|
||||
@@ -1,445 +0,0 @@
|
||||
//! Notion API implementation.
|
||||
//!
|
||||
//! All API calls go through the host's HTTP capability, which handles
|
||||
//! credential injection and rate limiting. The WASM tool never sees
|
||||
//! the actual API token.
|
||||
|
||||
use crate::near::agent::host;
|
||||
use crate::types::*;
|
||||
|
||||
const NOTION_API_BASE: &str = "https://api.notion.com/v1";
|
||||
const NOTION_VERSION: &str = "2022-06-28";
|
||||
|
||||
/// Make a Notion API GET request.
|
||||
fn notion_get(path: &str) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}{}", NOTION_API_BASE, path);
|
||||
|
||||
let headers = serde_json::json!({
|
||||
"Notion-Version": NOTION_VERSION
|
||||
});
|
||||
let headers_str = serde_json::to_string(&headers).map_err(|e| e.to_string())?;
|
||||
|
||||
host::log(host::LogLevel::Debug, &format!("Notion GET: {}", path));
|
||||
|
||||
let response = host::http_request("GET", &url, &headers_str, None)?;
|
||||
|
||||
if response.status < 200 || response.status >= 300 {
|
||||
let body = String::from_utf8_lossy(&response.body);
|
||||
return Err(format!(
|
||||
"Notion API returned status {}: {}",
|
||||
response.status, body
|
||||
));
|
||||
}
|
||||
|
||||
let body = String::from_utf8(response.body)
|
||||
.map_err(|e| format!("Invalid UTF-8 in response: {}", e))?;
|
||||
serde_json::from_str(&body).map_err(|e| format!("Invalid JSON in response: {}", e))
|
||||
}
|
||||
|
||||
/// Make a Notion API POST request.
|
||||
fn notion_post(path: &str, body: &serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}{}", NOTION_API_BASE, path);
|
||||
|
||||
let headers = serde_json::json!({
|
||||
"Notion-Version": NOTION_VERSION,
|
||||
"Content-Type": "application/json"
|
||||
});
|
||||
let headers_str = serde_json::to_string(&headers).map_err(|e| e.to_string())?;
|
||||
let body_str = serde_json::to_string(body).map_err(|e| e.to_string())?;
|
||||
|
||||
host::log(host::LogLevel::Debug, &format!("Notion POST: {}", path));
|
||||
|
||||
let response = host::http_request("POST", &url, &headers_str, Some(body_str.as_bytes()))?;
|
||||
|
||||
if response.status < 200 || response.status >= 300 {
|
||||
let body = String::from_utf8_lossy(&response.body);
|
||||
return Err(format!(
|
||||
"Notion API returned status {}: {}",
|
||||
response.status, body
|
||||
));
|
||||
}
|
||||
|
||||
let body = String::from_utf8(response.body)
|
||||
.map_err(|e| format!("Invalid UTF-8 in response: {}", e))?;
|
||||
serde_json::from_str(&body).map_err(|e| format!("Invalid JSON in response: {}", e))
|
||||
}
|
||||
|
||||
/// Make a Notion API PATCH request.
|
||||
fn notion_patch(path: &str, body: &serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}{}", NOTION_API_BASE, path);
|
||||
|
||||
let headers = serde_json::json!({
|
||||
"Notion-Version": NOTION_VERSION,
|
||||
"Content-Type": "application/json"
|
||||
});
|
||||
let headers_str = serde_json::to_string(&headers).map_err(|e| e.to_string())?;
|
||||
let body_str = serde_json::to_string(body).map_err(|e| e.to_string())?;
|
||||
|
||||
host::log(host::LogLevel::Debug, &format!("Notion PATCH: {}", path));
|
||||
|
||||
let response = host::http_request("PATCH", &url, &headers_str, Some(body_str.as_bytes()))?;
|
||||
|
||||
if response.status < 200 || response.status >= 300 {
|
||||
let body = String::from_utf8_lossy(&response.body);
|
||||
return Err(format!(
|
||||
"Notion API returned status {}: {}",
|
||||
response.status, body
|
||||
));
|
||||
}
|
||||
|
||||
let body = String::from_utf8(response.body)
|
||||
.map_err(|e| format!("Invalid UTF-8 in response: {}", e))?;
|
||||
serde_json::from_str(&body).map_err(|e| format!("Invalid JSON in response: {}", e))
|
||||
}
|
||||
|
||||
/// Make a Notion API DELETE request.
|
||||
fn notion_delete(path: &str) -> Result<serde_json::Value, String> {
|
||||
let url = format!("{}{}", NOTION_API_BASE, path);
|
||||
|
||||
let headers = serde_json::json!({
|
||||
"Notion-Version": NOTION_VERSION
|
||||
});
|
||||
let headers_str = serde_json::to_string(&headers).map_err(|e| e.to_string())?;
|
||||
|
||||
host::log(host::LogLevel::Debug, &format!("Notion DELETE: {}", path));
|
||||
|
||||
let response = host::http_request("DELETE", &url, &headers_str, None)?;
|
||||
|
||||
if response.status < 200 || response.status >= 300 {
|
||||
let body = String::from_utf8_lossy(&response.body);
|
||||
return Err(format!(
|
||||
"Notion API returned status {}: {}",
|
||||
response.status, body
|
||||
));
|
||||
}
|
||||
|
||||
let body = String::from_utf8(response.body)
|
||||
.map_err(|e| format!("Invalid UTF-8 in response: {}", e))?;
|
||||
serde_json::from_str(&body).map_err(|e| format!("Invalid JSON in response: {}", e))
|
||||
}
|
||||
|
||||
// ==================== Search ====================
|
||||
|
||||
pub fn search(
|
||||
query: &str,
|
||||
filter: Option<&SearchFilter>,
|
||||
page_size: u32,
|
||||
start_cursor: Option<&str>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let mut body = serde_json::json!({
|
||||
"page_size": page_size.min(100)
|
||||
});
|
||||
|
||||
if !query.is_empty() {
|
||||
body["query"] = serde_json::Value::String(query.to_string());
|
||||
}
|
||||
|
||||
if let Some(f) = filter {
|
||||
body["filter"] = serde_json::json!({
|
||||
"property": f.property,
|
||||
"value": f.value
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(cursor) = start_cursor {
|
||||
body["start_cursor"] = serde_json::Value::String(cursor.to_string());
|
||||
}
|
||||
|
||||
notion_post("/search", &body)
|
||||
}
|
||||
|
||||
// ==================== Pages ====================
|
||||
|
||||
pub fn get_page(page_id: &str) -> Result<serde_json::Value, String> {
|
||||
let page_id = normalize_uuid(page_id);
|
||||
notion_get(&format!("/pages/{}", page_id))
|
||||
}
|
||||
|
||||
pub fn create_page(
|
||||
parent: &serde_json::Value,
|
||||
properties: &serde_json::Value,
|
||||
children: Option<&Vec<serde_json::Value>>,
|
||||
icon: Option<&serde_json::Value>,
|
||||
cover: Option<&serde_json::Value>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let mut body = serde_json::json!({
|
||||
"parent": parent,
|
||||
"properties": properties
|
||||
});
|
||||
|
||||
if let Some(c) = children {
|
||||
if !c.is_empty() {
|
||||
body["children"] = serde_json::Value::Array(c.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(i) = icon {
|
||||
body["icon"] = i.clone();
|
||||
}
|
||||
|
||||
if let Some(c) = cover {
|
||||
body["cover"] = c.clone();
|
||||
}
|
||||
|
||||
notion_post("/pages", &body)
|
||||
}
|
||||
|
||||
pub fn update_page(
|
||||
page_id: &str,
|
||||
properties: &serde_json::Value,
|
||||
icon: Option<&serde_json::Value>,
|
||||
cover: Option<&serde_json::Value>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let page_id = normalize_uuid(page_id);
|
||||
|
||||
let mut body = serde_json::json!({
|
||||
"properties": properties
|
||||
});
|
||||
|
||||
if let Some(i) = icon {
|
||||
body["icon"] = i.clone();
|
||||
}
|
||||
|
||||
if let Some(c) = cover {
|
||||
body["cover"] = c.clone();
|
||||
}
|
||||
|
||||
notion_patch(&format!("/pages/{}", page_id), &body)
|
||||
}
|
||||
|
||||
pub fn archive_page(page_id: &str) -> Result<serde_json::Value, String> {
|
||||
let page_id = normalize_uuid(page_id);
|
||||
notion_patch(
|
||||
&format!("/pages/{}", page_id),
|
||||
&serde_json::json!({ "archived": true }),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn restore_page(page_id: &str) -> Result<serde_json::Value, String> {
|
||||
let page_id = normalize_uuid(page_id);
|
||||
notion_patch(
|
||||
&format!("/pages/{}", page_id),
|
||||
&serde_json::json!({ "archived": false }),
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== Blocks ====================
|
||||
|
||||
pub fn get_blocks(
|
||||
block_id: &str,
|
||||
page_size: u32,
|
||||
start_cursor: Option<&str>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let block_id = normalize_uuid(block_id);
|
||||
let mut path = format!(
|
||||
"/blocks/{}/children?page_size={}",
|
||||
block_id,
|
||||
page_size.min(100)
|
||||
);
|
||||
|
||||
if let Some(cursor) = start_cursor {
|
||||
path.push_str(&format!("&start_cursor={}", cursor));
|
||||
}
|
||||
|
||||
notion_get(&path)
|
||||
}
|
||||
|
||||
pub fn append_blocks(
|
||||
block_id: &str,
|
||||
children: &[serde_json::Value],
|
||||
after: Option<&str>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let block_id = normalize_uuid(block_id);
|
||||
|
||||
let mut body = serde_json::json!({
|
||||
"children": children
|
||||
});
|
||||
|
||||
if let Some(after_id) = after {
|
||||
body["after"] = serde_json::Value::String(normalize_uuid(after_id));
|
||||
}
|
||||
|
||||
notion_patch(&format!("/blocks/{}/children", block_id), &body)
|
||||
}
|
||||
|
||||
pub fn get_block(block_id: &str) -> Result<serde_json::Value, String> {
|
||||
let block_id = normalize_uuid(block_id);
|
||||
notion_get(&format!("/blocks/{}", block_id))
|
||||
}
|
||||
|
||||
pub fn update_block(
|
||||
block_id: &str,
|
||||
content: &serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let block_id = normalize_uuid(block_id);
|
||||
notion_patch(&format!("/blocks/{}", block_id), content)
|
||||
}
|
||||
|
||||
pub fn delete_block(block_id: &str) -> Result<serde_json::Value, String> {
|
||||
let block_id = normalize_uuid(block_id);
|
||||
notion_delete(&format!("/blocks/{}", block_id))
|
||||
}
|
||||
|
||||
// ==================== Databases ====================
|
||||
|
||||
pub fn get_database(database_id: &str) -> Result<serde_json::Value, String> {
|
||||
let database_id = normalize_uuid(database_id);
|
||||
notion_get(&format!("/databases/{}", database_id))
|
||||
}
|
||||
|
||||
pub fn query_database(
|
||||
database_id: &str,
|
||||
filter: Option<&serde_json::Value>,
|
||||
sorts: Option<&Vec<serde_json::Value>>,
|
||||
page_size: u32,
|
||||
start_cursor: Option<&str>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let database_id = normalize_uuid(database_id);
|
||||
|
||||
let mut body = serde_json::json!({
|
||||
"page_size": page_size.min(100)
|
||||
});
|
||||
|
||||
if let Some(f) = filter {
|
||||
body["filter"] = f.clone();
|
||||
}
|
||||
|
||||
if let Some(s) = sorts {
|
||||
if !s.is_empty() {
|
||||
body["sorts"] = serde_json::Value::Array(s.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cursor) = start_cursor {
|
||||
body["start_cursor"] = serde_json::Value::String(cursor.to_string());
|
||||
}
|
||||
|
||||
notion_post(&format!("/databases/{}/query", database_id), &body)
|
||||
}
|
||||
|
||||
pub fn create_database(
|
||||
parent: &serde_json::Value,
|
||||
title: &[serde_json::Value],
|
||||
properties: &serde_json::Value,
|
||||
icon: Option<&serde_json::Value>,
|
||||
cover: Option<&serde_json::Value>,
|
||||
is_inline: bool,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let mut body = serde_json::json!({
|
||||
"parent": parent,
|
||||
"title": title,
|
||||
"properties": properties,
|
||||
"is_inline": is_inline
|
||||
});
|
||||
|
||||
if let Some(i) = icon {
|
||||
body["icon"] = i.clone();
|
||||
}
|
||||
|
||||
if let Some(c) = cover {
|
||||
body["cover"] = c.clone();
|
||||
}
|
||||
|
||||
notion_post("/databases", &body)
|
||||
}
|
||||
|
||||
pub fn update_database(
|
||||
database_id: &str,
|
||||
title: Option<&Vec<serde_json::Value>>,
|
||||
properties: Option<&serde_json::Value>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let database_id = normalize_uuid(database_id);
|
||||
|
||||
let mut body = serde_json::json!({});
|
||||
|
||||
if let Some(t) = title {
|
||||
body["title"] = serde_json::Value::Array(t.clone());
|
||||
}
|
||||
|
||||
if let Some(p) = properties {
|
||||
body["properties"] = p.clone();
|
||||
}
|
||||
|
||||
notion_patch(&format!("/databases/{}", database_id), &body)
|
||||
}
|
||||
|
||||
// ==================== Comments ====================
|
||||
|
||||
pub fn get_comments(
|
||||
block_id: &str,
|
||||
page_size: u32,
|
||||
start_cursor: Option<&str>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let block_id = normalize_uuid(block_id);
|
||||
let mut path = format!(
|
||||
"/comments?block_id={}&page_size={}",
|
||||
block_id,
|
||||
page_size.min(100)
|
||||
);
|
||||
|
||||
if let Some(cursor) = start_cursor {
|
||||
path.push_str(&format!("&start_cursor={}", cursor));
|
||||
}
|
||||
|
||||
notion_get(&path)
|
||||
}
|
||||
|
||||
pub fn add_comment(
|
||||
parent: &serde_json::Value,
|
||||
rich_text: &[serde_json::Value],
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let body = serde_json::json!({
|
||||
"parent": parent,
|
||||
"rich_text": rich_text
|
||||
});
|
||||
|
||||
notion_post("/comments", &body)
|
||||
}
|
||||
|
||||
// ==================== Users ====================
|
||||
|
||||
pub fn list_users(page_size: u32, start_cursor: Option<&str>) -> Result<serde_json::Value, String> {
|
||||
let mut path = format!("/users?page_size={}", page_size.min(100));
|
||||
|
||||
if let Some(cursor) = start_cursor {
|
||||
path.push_str(&format!("&start_cursor={}", cursor));
|
||||
}
|
||||
|
||||
notion_get(&path)
|
||||
}
|
||||
|
||||
pub fn get_user(user_id: &str) -> Result<serde_json::Value, String> {
|
||||
let user_id = normalize_uuid(user_id);
|
||||
notion_get(&format!("/users/{}", user_id))
|
||||
}
|
||||
|
||||
pub fn get_me() -> Result<serde_json::Value, String> {
|
||||
notion_get("/users/me")
|
||||
}
|
||||
|
||||
// ==================== Helpers ====================
|
||||
|
||||
/// Normalize a UUID by removing dashes if needed.
|
||||
/// Notion accepts both formats, but we normalize for consistency.
|
||||
fn normalize_uuid(id: &str) -> String {
|
||||
// If it already has dashes in the right places, return as-is
|
||||
if id.len() == 36 && id.chars().filter(|c| *c == '-').count() == 4 {
|
||||
return id.to_string();
|
||||
}
|
||||
|
||||
// If it's 32 characters without dashes, add them
|
||||
let clean: String = id.chars().filter(|c| c.is_ascii_hexdigit()).collect();
|
||||
if clean.len() == 32 {
|
||||
return format!(
|
||||
"{}-{}-{}-{}-{}",
|
||||
&clean[0..8],
|
||||
&clean[8..12],
|
||||
&clean[12..16],
|
||||
&clean[16..20],
|
||||
&clean[20..32]
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise return as-is (let the API handle validation)
|
||||
id.to_string()
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
//! Notion WASM Tool for NEAR Agent.
|
||||
//!
|
||||
//! This is a standalone WASM component that provides Notion integration.
|
||||
//! It follows the MCP server pattern with domain-grouped operations.
|
||||
//!
|
||||
//! # Capabilities Required
|
||||
//!
|
||||
//! - HTTP: `api.notion.com/v1/*` (GET, POST, PATCH, DELETE)
|
||||
//! - Secrets: `notion_api_token` (injected automatically as Bearer token)
|
||||
//!
|
||||
//! # Supported Actions
|
||||
//!
|
||||
//! ## Search
|
||||
//! - `search`: Search across pages and databases
|
||||
//!
|
||||
//! ## Pages
|
||||
//! - `get_page`: Retrieve a page by ID
|
||||
//! - `create_page`: Create a new page in a database or as child of another page
|
||||
//! - `update_page`: Update page properties
|
||||
//! - `archive_page`: Archive (soft-delete) a page
|
||||
//! - `restore_page`: Restore an archived page
|
||||
//!
|
||||
//! ## Blocks
|
||||
//! - `get_blocks`: Get child blocks of a page/block
|
||||
//! - `append_blocks`: Append content blocks to a page/block
|
||||
//! - `get_block`: Get a single block
|
||||
//! - `update_block`: Update a block's content
|
||||
//! - `delete_block`: Delete a block
|
||||
//!
|
||||
//! ## Databases
|
||||
//! - `get_database`: Get database schema
|
||||
//! - `query_database`: Query with filters and sorts
|
||||
//! - `create_database`: Create a new database
|
||||
//! - `update_database`: Update database title/properties
|
||||
//!
|
||||
//! ## Comments
|
||||
//! - `get_comments`: Get comments on a page/block
|
||||
//! - `add_comment`: Add a comment
|
||||
//!
|
||||
//! ## Users
|
||||
//! - `list_users`: List workspace users
|
||||
//! - `get_user`: Get a specific user
|
||||
//! - `get_me`: Get the bot user
|
||||
//!
|
||||
//! # Example Usage
|
||||
//!
|
||||
//! ```json
|
||||
//! {"action": "search", "query": "meeting notes", "page_size": 5}
|
||||
//! ```
|
||||
//!
|
||||
//! ```json
|
||||
//! {
|
||||
//! "action": "query_database",
|
||||
//! "database_id": "abc123...",
|
||||
//! "filter": {"property": "Status", "select": {"equals": "Done"}},
|
||||
//! "sorts": [{"property": "Created", "direction": "descending"}]
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
mod api;
|
||||
mod types;
|
||||
|
||||
use types::NotionAction;
|
||||
|
||||
// Generate bindings from the WIT interface.
|
||||
wit_bindgen::generate!({
|
||||
world: "sandboxed-tool",
|
||||
path: "../../../wit/tool.wit",
|
||||
});
|
||||
|
||||
/// Implementation of the tool interface.
|
||||
struct NotionTool;
|
||||
|
||||
impl exports::near::agent::tool::Guest for NotionTool {
|
||||
fn execute(req: exports::near::agent::tool::Request) -> exports::near::agent::tool::Response {
|
||||
match execute_inner(&req.params) {
|
||||
Ok(result) => exports::near::agent::tool::Response {
|
||||
output: Some(result),
|
||||
error: None,
|
||||
},
|
||||
Err(e) => exports::near::agent::tool::Response {
|
||||
output: None,
|
||||
error: Some(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn schema() -> String {
|
||||
TOOL_SCHEMA.to_string()
|
||||
}
|
||||
|
||||
fn description() -> String {
|
||||
"Notion workspace integration for searching, managing pages/databases/blocks, \
|
||||
comments, and users. Requires a Notion integration token with appropriate \
|
||||
capabilities (read content, update content, insert content)."
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Inner execution logic with proper error handling.
|
||||
fn execute_inner(params: &str) -> Result<String, String> {
|
||||
// Check if the Notion token is configured
|
||||
if !crate::near::agent::host::secret_exists("notion_api_token") {
|
||||
return Err(
|
||||
"Notion API token not configured. Please add the 'notion_api_token' secret."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Parse the action from JSON
|
||||
let action: NotionAction =
|
||||
serde_json::from_str(params).map_err(|e| format!("Invalid parameters: {}", e))?;
|
||||
|
||||
crate::near::agent::host::log(
|
||||
crate::near::agent::host::LogLevel::Info,
|
||||
&format!("Executing Notion action: {:?}", action),
|
||||
);
|
||||
|
||||
// Dispatch to the appropriate handler
|
||||
let result = match action {
|
||||
// Search
|
||||
NotionAction::Search {
|
||||
query,
|
||||
filter,
|
||||
page_size,
|
||||
start_cursor,
|
||||
} => api::search(&query, filter.as_ref(), page_size, start_cursor.as_deref())?,
|
||||
|
||||
// Pages
|
||||
NotionAction::GetPage { page_id } => api::get_page(&page_id)?,
|
||||
|
||||
NotionAction::CreatePage {
|
||||
parent,
|
||||
properties,
|
||||
children,
|
||||
icon,
|
||||
cover,
|
||||
} => api::create_page(
|
||||
&parent,
|
||||
&properties,
|
||||
children.as_ref(),
|
||||
icon.as_ref(),
|
||||
cover.as_ref(),
|
||||
)?,
|
||||
|
||||
NotionAction::UpdatePage {
|
||||
page_id,
|
||||
properties,
|
||||
icon,
|
||||
cover,
|
||||
} => api::update_page(&page_id, &properties, icon.as_ref(), cover.as_ref())?,
|
||||
|
||||
NotionAction::ArchivePage { page_id } => api::archive_page(&page_id)?,
|
||||
|
||||
NotionAction::RestorePage { page_id } => api::restore_page(&page_id)?,
|
||||
|
||||
// Blocks
|
||||
NotionAction::GetBlocks {
|
||||
block_id,
|
||||
page_size,
|
||||
start_cursor,
|
||||
} => api::get_blocks(&block_id, page_size, start_cursor.as_deref())?,
|
||||
|
||||
NotionAction::AppendBlocks {
|
||||
block_id,
|
||||
children,
|
||||
after,
|
||||
} => api::append_blocks(&block_id, &children, after.as_deref())?,
|
||||
|
||||
NotionAction::GetBlock { block_id } => api::get_block(&block_id)?,
|
||||
|
||||
NotionAction::UpdateBlock { block_id, content } => api::update_block(&block_id, &content)?,
|
||||
|
||||
NotionAction::DeleteBlock { block_id } => api::delete_block(&block_id)?,
|
||||
|
||||
// Databases
|
||||
NotionAction::GetDatabase { database_id } => api::get_database(&database_id)?,
|
||||
|
||||
NotionAction::QueryDatabase {
|
||||
database_id,
|
||||
filter,
|
||||
sorts,
|
||||
page_size,
|
||||
start_cursor,
|
||||
} => api::query_database(
|
||||
&database_id,
|
||||
filter.as_ref(),
|
||||
sorts.as_ref(),
|
||||
page_size,
|
||||
start_cursor.as_deref(),
|
||||
)?,
|
||||
|
||||
NotionAction::CreateDatabase {
|
||||
parent,
|
||||
title,
|
||||
properties,
|
||||
icon,
|
||||
cover,
|
||||
is_inline,
|
||||
} => api::create_database(
|
||||
&parent,
|
||||
&title,
|
||||
&properties,
|
||||
icon.as_ref(),
|
||||
cover.as_ref(),
|
||||
is_inline,
|
||||
)?,
|
||||
|
||||
NotionAction::UpdateDatabase {
|
||||
database_id,
|
||||
title,
|
||||
properties,
|
||||
} => api::update_database(&database_id, title.as_ref(), properties.as_ref())?,
|
||||
|
||||
// Comments
|
||||
NotionAction::GetComments {
|
||||
block_id,
|
||||
page_size,
|
||||
start_cursor,
|
||||
} => api::get_comments(&block_id, page_size, start_cursor.as_deref())?,
|
||||
|
||||
NotionAction::AddComment { parent, rich_text } => api::add_comment(&parent, &rich_text)?,
|
||||
|
||||
// Users
|
||||
NotionAction::ListUsers {
|
||||
page_size,
|
||||
start_cursor,
|
||||
} => api::list_users(page_size, start_cursor.as_deref())?,
|
||||
|
||||
NotionAction::GetUser { user_id } => api::get_user(&user_id)?,
|
||||
|
||||
NotionAction::GetMe => api::get_me()?,
|
||||
};
|
||||
|
||||
serde_json::to_string(&result).map_err(|e| format!("Failed to serialize response: {}", e))
|
||||
}
|
||||
|
||||
// Export the tool implementation.
|
||||
export!(NotionTool);
|
||||
|
||||
/// JSON Schema for the tool's parameters.
|
||||
///
|
||||
/// This schema uses `oneOf` to describe each action's specific parameters.
|
||||
const TOOL_SCHEMA: &str = r#"{
|
||||
"type": "object",
|
||||
"required": ["action"],
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "search" },
|
||||
"query": { "type": "string", "description": "Text to search for" },
|
||||
"filter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"property": { "type": "string", "const": "object" },
|
||||
"value": { "type": "string", "enum": ["page", "database"] }
|
||||
},
|
||||
"description": "Filter by object type"
|
||||
},
|
||||
"page_size": { "type": "integer", "minimum": 1, "maximum": 100, "default": 10 },
|
||||
"start_cursor": { "type": "string", "description": "Pagination cursor" }
|
||||
},
|
||||
"required": ["action"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "get_page" },
|
||||
"page_id": { "type": "string", "description": "Page ID (UUID)" }
|
||||
},
|
||||
"required": ["action", "page_id"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "create_page" },
|
||||
"parent": {
|
||||
"type": "object",
|
||||
"description": "Parent reference: {\"database_id\": \"...\"} or {\"page_id\": \"...\"}"
|
||||
},
|
||||
"properties": { "type": "object", "description": "Page properties" },
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": { "type": "object" },
|
||||
"description": "Block children for page content"
|
||||
},
|
||||
"icon": { "type": "object", "description": "Page icon" },
|
||||
"cover": { "type": "object", "description": "Page cover image" }
|
||||
},
|
||||
"required": ["action", "parent", "properties"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "update_page" },
|
||||
"page_id": { "type": "string" },
|
||||
"properties": { "type": "object", "description": "Properties to update" },
|
||||
"icon": { "type": "object" },
|
||||
"cover": { "type": "object" }
|
||||
},
|
||||
"required": ["action", "page_id", "properties"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "archive_page" },
|
||||
"page_id": { "type": "string" }
|
||||
},
|
||||
"required": ["action", "page_id"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "restore_page" },
|
||||
"page_id": { "type": "string" }
|
||||
},
|
||||
"required": ["action", "page_id"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "get_blocks" },
|
||||
"block_id": { "type": "string", "description": "Block or page ID" },
|
||||
"page_size": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 },
|
||||
"start_cursor": { "type": "string" }
|
||||
},
|
||||
"required": ["action", "block_id"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "append_blocks" },
|
||||
"block_id": { "type": "string", "description": "Block or page ID to append to" },
|
||||
"children": {
|
||||
"type": "array",
|
||||
"items": { "type": "object" },
|
||||
"description": "Block objects to append"
|
||||
},
|
||||
"after": { "type": "string", "description": "Insert after this block ID" }
|
||||
},
|
||||
"required": ["action", "block_id", "children"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "get_block" },
|
||||
"block_id": { "type": "string" }
|
||||
},
|
||||
"required": ["action", "block_id"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "update_block" },
|
||||
"block_id": { "type": "string" },
|
||||
"content": { "type": "object", "description": "Block content by type" }
|
||||
},
|
||||
"required": ["action", "block_id", "content"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "delete_block" },
|
||||
"block_id": { "type": "string" }
|
||||
},
|
||||
"required": ["action", "block_id"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "get_database" },
|
||||
"database_id": { "type": "string" }
|
||||
},
|
||||
"required": ["action", "database_id"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "query_database" },
|
||||
"database_id": { "type": "string" },
|
||||
"filter": { "type": "object", "description": "Notion filter object" },
|
||||
"sorts": {
|
||||
"type": "array",
|
||||
"items": { "type": "object" },
|
||||
"description": "Sort configuration"
|
||||
},
|
||||
"page_size": { "type": "integer", "minimum": 1, "maximum": 100, "default": 10 },
|
||||
"start_cursor": { "type": "string" }
|
||||
},
|
||||
"required": ["action", "database_id"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "create_database" },
|
||||
"parent": { "type": "object", "description": "Parent page or workspace" },
|
||||
"title": {
|
||||
"type": "array",
|
||||
"items": { "type": "object" },
|
||||
"description": "Database title as rich text"
|
||||
},
|
||||
"properties": { "type": "object", "description": "Property schema" },
|
||||
"icon": { "type": "object" },
|
||||
"cover": { "type": "object" },
|
||||
"is_inline": { "type": "boolean", "default": false }
|
||||
},
|
||||
"required": ["action", "parent", "title", "properties"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "update_database" },
|
||||
"database_id": { "type": "string" },
|
||||
"title": { "type": "array", "items": { "type": "object" } },
|
||||
"properties": { "type": "object" }
|
||||
},
|
||||
"required": ["action", "database_id"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "get_comments" },
|
||||
"block_id": { "type": "string", "description": "Block or page ID" },
|
||||
"page_size": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 },
|
||||
"start_cursor": { "type": "string" }
|
||||
},
|
||||
"required": ["action", "block_id"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "add_comment" },
|
||||
"parent": {
|
||||
"type": "object",
|
||||
"description": "{\"page_id\": \"...\"} or {\"discussion_id\": \"...\"}"
|
||||
},
|
||||
"rich_text": {
|
||||
"type": "array",
|
||||
"items": { "type": "object" },
|
||||
"description": "Comment content as rich text"
|
||||
}
|
||||
},
|
||||
"required": ["action", "parent", "rich_text"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "list_users" },
|
||||
"page_size": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 },
|
||||
"start_cursor": { "type": "string" }
|
||||
},
|
||||
"required": ["action"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "get_user" },
|
||||
"user_id": { "type": "string" }
|
||||
},
|
||||
"required": ["action", "user_id"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"action": { "const": "get_me" }
|
||||
},
|
||||
"required": ["action"]
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
@@ -1,267 +0,0 @@
|
||||
//! Types for Notion API requests and responses.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Input parameters for the Notion tool.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "action", rename_all = "snake_case")]
|
||||
pub enum NotionAction {
|
||||
// ==================== Search ====================
|
||||
/// Search across all pages and databases.
|
||||
Search {
|
||||
/// Text query to search for.
|
||||
#[serde(default)]
|
||||
query: String,
|
||||
/// Filter by object type: "page" or "database".
|
||||
#[serde(default)]
|
||||
filter: Option<SearchFilter>,
|
||||
/// Max results (default: 10, max: 100).
|
||||
#[serde(default = "default_page_size")]
|
||||
page_size: u32,
|
||||
/// Pagination cursor from previous response.
|
||||
#[serde(default)]
|
||||
start_cursor: Option<String>,
|
||||
},
|
||||
|
||||
// ==================== Pages ====================
|
||||
/// Retrieve a page by ID.
|
||||
GetPage {
|
||||
/// Page ID (UUID format, with or without dashes).
|
||||
page_id: String,
|
||||
},
|
||||
|
||||
/// Create a new page.
|
||||
CreatePage {
|
||||
/// Parent reference: { "database_id": "..." } or { "page_id": "..." }.
|
||||
parent: serde_json::Value,
|
||||
/// Page properties matching the parent database schema.
|
||||
properties: serde_json::Value,
|
||||
/// Optional page content as an array of block objects.
|
||||
#[serde(default)]
|
||||
children: Option<Vec<serde_json::Value>>,
|
||||
/// Optional icon: { "emoji": "..." } or { "external": { "url": "..." } }.
|
||||
#[serde(default)]
|
||||
icon: Option<serde_json::Value>,
|
||||
/// Optional cover: { "external": { "url": "..." } }.
|
||||
#[serde(default)]
|
||||
cover: Option<serde_json::Value>,
|
||||
},
|
||||
|
||||
/// Update page properties.
|
||||
UpdatePage {
|
||||
/// Page ID.
|
||||
page_id: String,
|
||||
/// Properties to update.
|
||||
properties: serde_json::Value,
|
||||
/// Optional new icon.
|
||||
#[serde(default)]
|
||||
icon: Option<serde_json::Value>,
|
||||
/// Optional new cover.
|
||||
#[serde(default)]
|
||||
cover: Option<serde_json::Value>,
|
||||
},
|
||||
|
||||
/// Archive (soft-delete) a page.
|
||||
ArchivePage {
|
||||
/// Page ID.
|
||||
page_id: String,
|
||||
},
|
||||
|
||||
/// Restore an archived page.
|
||||
RestorePage {
|
||||
/// Page ID.
|
||||
page_id: String,
|
||||
},
|
||||
|
||||
// ==================== Blocks ====================
|
||||
/// Get child blocks of a block or page.
|
||||
GetBlocks {
|
||||
/// Block or page ID.
|
||||
block_id: String,
|
||||
/// Max results (default: 50, max: 100).
|
||||
#[serde(default = "default_block_page_size")]
|
||||
page_size: u32,
|
||||
/// Pagination cursor.
|
||||
#[serde(default)]
|
||||
start_cursor: Option<String>,
|
||||
},
|
||||
|
||||
/// Append blocks to a page or block.
|
||||
AppendBlocks {
|
||||
/// Block or page ID to append to.
|
||||
block_id: String,
|
||||
/// Array of block objects to append.
|
||||
children: Vec<serde_json::Value>,
|
||||
/// Append after this block ID (for ordering).
|
||||
#[serde(default)]
|
||||
after: Option<String>,
|
||||
},
|
||||
|
||||
/// Retrieve a single block.
|
||||
GetBlock {
|
||||
/// Block ID.
|
||||
block_id: String,
|
||||
},
|
||||
|
||||
/// Update a block's content.
|
||||
UpdateBlock {
|
||||
/// Block ID.
|
||||
block_id: String,
|
||||
/// Block content (varies by type, e.g., { "paragraph": { "rich_text": [...] } }).
|
||||
content: serde_json::Value,
|
||||
},
|
||||
|
||||
/// Delete a block.
|
||||
DeleteBlock {
|
||||
/// Block ID.
|
||||
block_id: String,
|
||||
},
|
||||
|
||||
// ==================== Databases ====================
|
||||
/// Retrieve a database schema.
|
||||
GetDatabase {
|
||||
/// Database ID.
|
||||
database_id: String,
|
||||
},
|
||||
|
||||
/// Query a database with filters and sorts.
|
||||
QueryDatabase {
|
||||
/// Database ID.
|
||||
database_id: String,
|
||||
/// Filter object (Notion filter format).
|
||||
#[serde(default)]
|
||||
filter: Option<serde_json::Value>,
|
||||
/// Sort configuration array.
|
||||
#[serde(default)]
|
||||
sorts: Option<Vec<serde_json::Value>>,
|
||||
/// Max results (default: 10, max: 100).
|
||||
#[serde(default = "default_page_size")]
|
||||
page_size: u32,
|
||||
/// Pagination cursor.
|
||||
#[serde(default)]
|
||||
start_cursor: Option<String>,
|
||||
},
|
||||
|
||||
/// Create a new database.
|
||||
CreateDatabase {
|
||||
/// Parent reference: { "page_id": "..." } or { "type": "workspace", "workspace": true }.
|
||||
parent: serde_json::Value,
|
||||
/// Database title as rich text array.
|
||||
title: Vec<serde_json::Value>,
|
||||
/// Property schema: { "Name": { "title": {} }, "Status": { "select": { "options": [...] } } }.
|
||||
properties: serde_json::Value,
|
||||
/// Optional icon.
|
||||
#[serde(default)]
|
||||
icon: Option<serde_json::Value>,
|
||||
/// Optional cover.
|
||||
#[serde(default)]
|
||||
cover: Option<serde_json::Value>,
|
||||
/// Make database inline (default: false).
|
||||
#[serde(default)]
|
||||
is_inline: bool,
|
||||
},
|
||||
|
||||
/// Update database title or properties schema.
|
||||
UpdateDatabase {
|
||||
/// Database ID.
|
||||
database_id: String,
|
||||
/// New title (optional).
|
||||
#[serde(default)]
|
||||
title: Option<Vec<serde_json::Value>>,
|
||||
/// Properties to add or update (optional).
|
||||
#[serde(default)]
|
||||
properties: Option<serde_json::Value>,
|
||||
},
|
||||
|
||||
// ==================== Comments ====================
|
||||
/// Get comments on a block or page.
|
||||
GetComments {
|
||||
/// Block or page ID.
|
||||
block_id: String,
|
||||
/// Max results (default: 50, max: 100).
|
||||
#[serde(default = "default_block_page_size")]
|
||||
page_size: u32,
|
||||
/// Pagination cursor.
|
||||
#[serde(default)]
|
||||
start_cursor: Option<String>,
|
||||
},
|
||||
|
||||
/// Add a comment to a page or discussion thread.
|
||||
AddComment {
|
||||
/// Parent reference: { "page_id": "..." } or { "discussion_id": "..." }.
|
||||
parent: serde_json::Value,
|
||||
/// Comment text as rich text array.
|
||||
rich_text: Vec<serde_json::Value>,
|
||||
},
|
||||
|
||||
// ==================== Users ====================
|
||||
/// List all users in the workspace.
|
||||
ListUsers {
|
||||
/// Max results (default: 50, max: 100).
|
||||
#[serde(default = "default_block_page_size")]
|
||||
page_size: u32,
|
||||
/// Pagination cursor.
|
||||
#[serde(default)]
|
||||
start_cursor: Option<String>,
|
||||
},
|
||||
|
||||
/// Get a specific user.
|
||||
GetUser {
|
||||
/// User ID.
|
||||
user_id: String,
|
||||
},
|
||||
|
||||
/// Get the bot user (the integration itself).
|
||||
GetMe,
|
||||
}
|
||||
|
||||
/// Search filter for limiting results to pages or databases.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct SearchFilter {
|
||||
/// "object" field.
|
||||
pub property: String,
|
||||
/// "page" or "database".
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
fn default_page_size() -> u32 {
|
||||
10
|
||||
}
|
||||
|
||||
fn default_block_page_size() -> u32 {
|
||||
50
|
||||
}
|
||||
|
||||
// ==================== Response Types ====================
|
||||
|
||||
/// Generic Notion API response with pagination.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PaginatedResponse {
|
||||
pub object: String,
|
||||
pub results: Vec<serde_json::Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_cursor: Option<String>,
|
||||
pub has_more: bool,
|
||||
}
|
||||
|
||||
/// Single object response.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ObjectResponse {
|
||||
pub object: String,
|
||||
#[serde(flatten)]
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Success response for mutations.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MutationResponse {
|
||||
pub success: bool,
|
||||
#[serde(flatten)]
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Minimal success response.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
Reference in New Issue
Block a user