chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
import type { NotionAddDatabaseRowParams } from '@/tools/notion/types'
import { PAGE_OUTPUT_PROPERTIES } from '@/tools/notion/types'
import type { ToolConfig } from '@/tools/types'
interface NotionAddDatabaseRowResponse {
success: boolean
output: {
id: string
url: string
title: string
created_time: string
last_edited_time: string
}
}
export const notionAddDatabaseRowTool: ToolConfig<
NotionAddDatabaseRowParams,
NotionAddDatabaseRowResponse
> = {
id: 'notion_add_database_row',
name: 'Add Notion Database Row',
description: 'Add a new row to a Notion database with specified properties',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
databaseId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the database to add the row to',
},
properties: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Row properties as JSON object matching the database schema (e.g., {"Name": {"title": [{"text": {"content": "Task 1"}}]}, "Status": {"select": {"name": "Done"}}})',
},
},
request: {
url: () => 'https://api.notion.com/v1/pages',
method: 'POST',
headers: (params: NotionAddDatabaseRowParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
body: (params: NotionAddDatabaseRowParams) => {
return {
parent: {
type: 'database_id',
database_id: params.databaseId.trim(),
},
properties: params.properties,
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
// Extract title from properties if available
let rowTitle = 'Untitled'
for (const [, value] of Object.entries(data.properties || {})) {
const prop = value as any
if (prop.type === 'title' && prop.title?.length > 0) {
rowTitle = prop.title.map((t: any) => t.plain_text || '').join('')
break
}
}
return {
success: true,
output: {
id: data.id,
url: data.url,
title: rowTitle,
created_time: data.created_time,
last_edited_time: data.last_edited_time,
},
}
},
outputs: {
id: PAGE_OUTPUT_PROPERTIES.id,
url: PAGE_OUTPUT_PROPERTIES.url,
title: { type: 'string', description: 'Row title' },
created_time: PAGE_OUTPUT_PROPERTIES.created_time,
last_edited_time: PAGE_OUTPUT_PROPERTIES.last_edited_time,
},
}
export const notionAddDatabaseRowV2Tool: ToolConfig<
NotionAddDatabaseRowParams,
NotionAddDatabaseRowResponse
> = {
...notionAddDatabaseRowTool,
id: 'notion_add_database_row_v2',
version: '2.0.0',
}
+125
View File
@@ -0,0 +1,125 @@
import type { NotionAppendBlocksParams } from '@/tools/notion/types'
import { BLOCK_LIST_RESULTS_OUTPUT, PAGINATION_OUTPUT_PROPERTIES } from '@/tools/notion/types'
import type { ToolConfig } from '@/tools/types'
interface NotionAppendBlocksResponse {
success: boolean
output: {
results: any[]
has_more: boolean
next_cursor: string | null
}
}
/**
* Coerce the children param into a block array, accepting either a JSON string
* (when called directly by an agent) or an already-parsed array.
*/
function parseChildren(children: any[] | string): any[] {
if (Array.isArray(children)) return children
if (typeof children === 'string') {
const parsed = JSON.parse(children)
if (!Array.isArray(parsed)) {
throw new Error('children must be a JSON array of Notion block objects')
}
return parsed
}
throw new Error('children must be a JSON array of Notion block objects')
}
export const notionAppendBlocksTool: ToolConfig<
NotionAppendBlocksParams,
NotionAppendBlocksResponse
> = {
id: 'notion_append_blocks',
name: 'Notion Append Block Children',
description: 'Append new block children (content) to a Notion page or block',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
blockId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the page or block to append children to',
},
children: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description: 'Array of Notion block objects to append (max 100)',
},
after: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of an existing block to append the new children after',
},
},
request: {
url: (params: NotionAppendBlocksParams) =>
`https://api.notion.com/v1/blocks/${params.blockId.trim()}/children`,
method: 'PATCH',
headers: (params: NotionAppendBlocksParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
body: (params: NotionAppendBlocksParams) => {
const body: any = { children: parseChildren(params.children) }
if (params.after) body.after = params.after.trim()
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: response.ok,
output: {
results: data.results ?? [],
has_more: data.has_more ?? false,
next_cursor: data.next_cursor ?? null,
},
}
},
outputs: {
results: BLOCK_LIST_RESULTS_OUTPUT,
has_more: PAGINATION_OUTPUT_PROPERTIES.has_more,
next_cursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
},
}
export const notionAppendBlocksV2Tool: ToolConfig<
NotionAppendBlocksParams,
NotionAppendBlocksResponse
> = {
id: 'notion_append_blocks_v2',
name: 'Notion Append Block Children',
description: 'Append new block children (content) to a Notion page or block',
version: '2.0.0',
oauth: notionAppendBlocksTool.oauth,
params: notionAppendBlocksTool.params,
request: notionAppendBlocksTool.request,
transformResponse: notionAppendBlocksTool.transformResponse,
outputs: notionAppendBlocksTool.outputs,
}
+130
View File
@@ -0,0 +1,130 @@
import type { NotionCreateCommentParams } from '@/tools/notion/types'
import { RICH_TEXT_ARRAY_OUTPUT } from '@/tools/notion/types'
import type { ToolConfig } from '@/tools/types'
interface NotionCreateCommentResponse {
success: boolean
output: {
id: string
discussion_id: string
created_time: string
content: string
rich_text: any[]
}
}
export const notionCreateCommentTool: ToolConfig<
NotionCreateCommentParams,
NotionCreateCommentResponse
> = {
id: 'notion_create_comment',
name: 'Notion Create Comment',
description: 'Create a comment on a Notion page or within an existing discussion thread',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
pageId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of the page to comment on (provide either pageId or discussionId)',
},
discussionId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'UUID of an existing discussion thread to reply to',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The text content of the comment',
},
},
request: {
url: () => 'https://api.notion.com/v1/comments',
method: 'POST',
headers: (params: NotionCreateCommentParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
body: (params: NotionCreateCommentParams) => {
const pageId = params.pageId?.trim()
const discussionId = params.discussionId?.trim()
if (!pageId && !discussionId) {
throw new Error('Either pageId or discussionId is required to create a comment')
}
const body: any = {
rich_text: [{ type: 'text', text: { content: params.content } }],
}
if (discussionId) {
body.discussion_id = discussionId
} else {
body.parent = { page_id: pageId }
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
const richText = data.rich_text ?? []
return {
success: response.ok,
output: {
id: data.id,
discussion_id: data.discussion_id ?? '',
created_time: data.created_time ?? '',
content: richText.map((t: any) => t.plain_text ?? '').join(''),
rich_text: richText,
},
}
},
outputs: {
id: { type: 'string', description: 'Comment UUID' },
discussion_id: { type: 'string', description: 'UUID of the discussion thread' },
created_time: { type: 'string', description: 'ISO 8601 creation timestamp' },
content: { type: 'string', description: 'Plain text content of the comment' },
rich_text: RICH_TEXT_ARRAY_OUTPUT,
},
}
export const notionCreateCommentV2Tool: ToolConfig<
NotionCreateCommentParams,
NotionCreateCommentResponse
> = {
id: 'notion_create_comment_v2',
name: 'Notion Create Comment',
description: 'Create a comment on a Notion page or within an existing discussion thread',
version: '2.0.0',
oauth: notionCreateCommentTool.oauth,
params: notionCreateCommentTool.params,
request: notionCreateCommentTool.request,
transformResponse: notionCreateCommentTool.transformResponse,
outputs: notionCreateCommentTool.outputs,
}
+189
View File
@@ -0,0 +1,189 @@
import type { NotionCreateDatabaseParams, NotionResponse } from '@/tools/notion/types'
import { DATABASE_OUTPUT_PROPERTIES } from '@/tools/notion/types'
import type { ToolConfig } from '@/tools/types'
export const notionCreateDatabaseTool: ToolConfig<NotionCreateDatabaseParams, NotionResponse> = {
id: 'notion_create_database',
name: 'Create Notion Database',
description: 'Create a new database in Notion with custom properties',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
parentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'ID of the parent page where the database will be created',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Title for the new database',
},
properties: {
type: 'json',
required: false,
visibility: 'user-or-llm',
description:
'Database properties as JSON object (optional, will create a default "Name" property if empty)',
},
},
request: {
url: () => 'https://api.notion.com/v1/databases',
method: 'POST',
headers: (params: NotionCreateDatabaseParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
body: (params: NotionCreateDatabaseParams) => {
// Use provided properties or default to Name property
const properties =
params.properties && Object.keys(params.properties).length > 0
? params.properties
: { Name: { title: {} } }
const body = {
parent: {
type: 'page_id',
page_id: params.parentId.trim(),
},
title: [
{
type: 'text',
text: {
content: params.title,
},
},
],
properties,
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
// Extract database title
const title = data.title?.map((t: any) => t.plain_text || '').join('') || 'Untitled Database'
// Extract properties for display
const properties = data.properties || {}
const propertyList = Object.entries(properties)
.map(([name, prop]: [string, any]) => ` ${name}: ${prop.type}`)
.join('\n')
const content = [
`Database "${title}" created successfully!`,
'',
'Properties:',
propertyList,
'',
`Database ID: ${data.id}`,
`URL: ${data.url}`,
].join('\n')
return {
success: true,
output: {
content,
metadata: {
id: data.id,
title,
url: data.url,
createdTime: data.created_time,
properties: data.properties,
},
},
}
},
outputs: {
content: {
type: 'string',
description: 'Success message with database details and properties list',
},
metadata: {
type: 'object',
description:
'Database metadata including ID, title, URL, creation time, and properties schema',
properties: {
id: DATABASE_OUTPUT_PROPERTIES.id,
title: { type: 'string', description: 'Database title' },
url: DATABASE_OUTPUT_PROPERTIES.url,
createdTime: DATABASE_OUTPUT_PROPERTIES.created_time,
properties: DATABASE_OUTPUT_PROPERTIES.properties,
},
},
},
}
// V2 Tool with API-aligned outputs
interface NotionCreateDatabaseV2Response {
success: boolean
output: {
id: string
title: string
url: string
created_time: string
properties: Record<string, any>
}
}
export const notionCreateDatabaseV2Tool: ToolConfig<
NotionCreateDatabaseParams,
NotionCreateDatabaseV2Response
> = {
id: 'notion_create_database_v2',
name: 'Create Notion Database',
description: 'Create a new database in Notion with custom properties',
version: '2.0.0',
oauth: notionCreateDatabaseTool.oauth,
params: notionCreateDatabaseTool.params,
request: notionCreateDatabaseTool.request,
transformResponse: async (response: Response) => {
const data = await response.json()
const title = data.title?.map((t: any) => t.plain_text || '').join('') || 'Untitled Database'
return {
success: true,
output: {
id: data.id,
title,
url: data.url,
created_time: data.created_time,
properties: data.properties || {},
},
}
},
outputs: {
id: DATABASE_OUTPUT_PROPERTIES.id,
title: { type: 'string', description: 'Database title' },
url: DATABASE_OUTPUT_PROPERTIES.url,
created_time: DATABASE_OUTPUT_PROPERTIES.created_time,
properties: DATABASE_OUTPUT_PROPERTIES.properties,
},
}
+213
View File
@@ -0,0 +1,213 @@
import type { NotionCreatePageParams, NotionResponse } from '@/tools/notion/types'
import { PAGE_OUTPUT_PROPERTIES } from '@/tools/notion/types'
import type { ToolConfig } from '@/tools/types'
export const notionCreatePageTool: ToolConfig<NotionCreatePageParams, NotionResponse> = {
id: 'notion_create_page',
name: 'Notion Page Creator',
description: 'Create a new page in Notion',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
parentId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the parent Notion page where this page will be created',
},
title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Title of the new page',
},
content: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional content to add to the page upon creation',
},
},
request: {
url: () => 'https://api.notion.com/v1/pages',
method: 'POST',
headers: (params: NotionCreatePageParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
body: (params: NotionCreatePageParams) => {
const body: any = {
parent: {
type: 'page_id',
page_id: params.parentId.trim(),
},
}
if (params.title) {
body.properties = {
title: {
type: 'title',
title: [
{
type: 'text',
text: {
content: params.title,
},
},
],
},
}
} else {
body.properties = {}
}
if (params.content) {
body.children = [
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [
{
type: 'text',
text: {
content: params.content,
},
},
],
},
},
]
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
let pageTitle = 'Untitled'
if (data.properties?.title) {
const titleProperty = data.properties.title
if (
titleProperty.title &&
Array.isArray(titleProperty.title) &&
titleProperty.title.length > 0
) {
pageTitle = titleProperty.title.map((t: any) => t.plain_text || '').join('')
}
}
return {
success: true,
output: {
content: `Successfully created page "${pageTitle}"`,
metadata: {
title: pageTitle,
pageId: data.id,
url: data.url,
lastEditedTime: data.last_edited_time,
createdTime: data.created_time,
},
},
}
},
outputs: {
content: {
type: 'string',
description: 'Success message confirming page creation',
},
metadata: {
type: 'object',
description: 'Page metadata including title, page ID, URL, and timestamps',
properties: {
title: { type: 'string', description: 'Page title' },
pageId: PAGE_OUTPUT_PROPERTIES.id,
url: PAGE_OUTPUT_PROPERTIES.url,
lastEditedTime: PAGE_OUTPUT_PROPERTIES.last_edited_time,
createdTime: PAGE_OUTPUT_PROPERTIES.created_time,
},
},
},
}
// V2 Tool with API-aligned outputs
interface NotionCreatePageV2Response {
success: boolean
output: {
id: string
title: string
url: string
created_time: string
last_edited_time: string
}
}
export const notionCreatePageV2Tool: ToolConfig<
NotionCreatePageParams,
NotionCreatePageV2Response
> = {
id: 'notion_create_page_v2',
name: 'Notion Page Creator',
description: 'Create a new page in Notion',
version: '2.0.0',
oauth: notionCreatePageTool.oauth,
params: notionCreatePageTool.params,
request: notionCreatePageTool.request,
transformResponse: async (response: Response) => {
const data = await response.json()
let pageTitle = 'Untitled'
if (data.properties?.title) {
const titleProperty = data.properties.title
if (
titleProperty.title &&
Array.isArray(titleProperty.title) &&
titleProperty.title.length > 0
) {
pageTitle = titleProperty.title.map((t: any) => t.plain_text || '').join('')
}
}
return {
success: true,
output: {
id: data.id,
title: pageTitle,
url: data.url,
created_time: data.created_time,
last_edited_time: data.last_edited_time,
},
}
},
outputs: {
id: PAGE_OUTPUT_PROPERTIES.id,
title: { type: 'string', description: 'Page title' },
url: PAGE_OUTPUT_PROPERTIES.url,
created_time: PAGE_OUTPUT_PROPERTIES.created_time,
last_edited_time: PAGE_OUTPUT_PROPERTIES.last_edited_time,
},
}
+87
View File
@@ -0,0 +1,87 @@
import type { NotionDeleteBlockParams } from '@/tools/notion/types'
import { BLOCK_OUTPUT_PROPERTIES } from '@/tools/notion/types'
import type { ToolConfig } from '@/tools/types'
interface NotionDeleteBlockResponse {
success: boolean
output: {
id: string
archived: boolean
}
}
export const notionDeleteBlockTool: ToolConfig<NotionDeleteBlockParams, NotionDeleteBlockResponse> =
{
id: 'notion_delete_block',
name: 'Notion Delete Block',
description: 'Delete (move to trash) a single Notion block',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
blockId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the block to delete',
},
},
request: {
url: (params: NotionDeleteBlockParams) =>
`https://api.notion.com/v1/blocks/${params.blockId.trim()}`,
method: 'DELETE',
headers: (params: NotionDeleteBlockParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: response.ok,
output: {
id: data.id,
archived: data.archived ?? true,
},
}
},
outputs: {
id: BLOCK_OUTPUT_PROPERTIES.id,
archived: { type: 'boolean', description: 'Whether the block was archived (moved to trash)' },
},
}
export const notionDeleteBlockV2Tool: ToolConfig<
NotionDeleteBlockParams,
NotionDeleteBlockResponse
> = {
id: 'notion_delete_block_v2',
name: 'Notion Delete Block',
description: 'Delete (move to trash) a single Notion block',
version: '2.0.0',
oauth: notionDeleteBlockTool.oauth,
params: notionDeleteBlockTool.params,
request: notionDeleteBlockTool.request,
transformResponse: notionDeleteBlockTool.transformResponse,
outputs: notionDeleteBlockTool.outputs,
}
+68
View File
@@ -0,0 +1,68 @@
import {
notionAddDatabaseRowTool,
notionAddDatabaseRowV2Tool,
} from '@/tools/notion/add_database_row'
import { notionAppendBlocksTool, notionAppendBlocksV2Tool } from '@/tools/notion/append_blocks'
import { notionCreateCommentTool, notionCreateCommentV2Tool } from '@/tools/notion/create_comment'
import {
notionCreateDatabaseTool,
notionCreateDatabaseV2Tool,
} from '@/tools/notion/create_database'
import { notionCreatePageTool, notionCreatePageV2Tool } from '@/tools/notion/create_page'
import { notionDeleteBlockTool, notionDeleteBlockV2Tool } from '@/tools/notion/delete_block'
import { notionListCommentsTool, notionListCommentsV2Tool } from '@/tools/notion/list_comments'
import { notionListUsersTool, notionListUsersV2Tool } from '@/tools/notion/list_users'
import { notionQueryDatabaseTool, notionQueryDatabaseV2Tool } from '@/tools/notion/query_database'
import { notionReadTool, notionReadV2Tool } from '@/tools/notion/read'
import { notionReadDatabaseTool, notionReadDatabaseV2Tool } from '@/tools/notion/read_database'
import { notionRetrieveBlockTool, notionRetrieveBlockV2Tool } from '@/tools/notion/retrieve_block'
import {
notionRetrieveBlockChildrenTool,
notionRetrieveBlockChildrenV2Tool,
} from '@/tools/notion/retrieve_block_children'
import { notionRetrieveUserTool, notionRetrieveUserV2Tool } from '@/tools/notion/retrieve_user'
import { notionSearchTool, notionSearchV2Tool } from '@/tools/notion/search'
import { notionUpdateBlockTool, notionUpdateBlockV2Tool } from '@/tools/notion/update_block'
import { notionUpdatePageTool, notionUpdatePageV2Tool } from '@/tools/notion/update_page'
import { notionWriteTool, notionWriteV2Tool } from '@/tools/notion/write'
export {
// Legacy tools
notionReadTool,
notionReadDatabaseTool,
notionWriteTool,
notionCreatePageTool,
notionUpdatePageTool,
notionQueryDatabaseTool,
notionSearchTool,
notionCreateDatabaseTool,
notionAddDatabaseRowTool,
notionAppendBlocksTool,
notionRetrieveBlockTool,
notionRetrieveBlockChildrenTool,
notionUpdateBlockTool,
notionDeleteBlockTool,
notionCreateCommentTool,
notionListCommentsTool,
notionListUsersTool,
notionRetrieveUserTool,
// V2 tools
notionReadV2Tool,
notionReadDatabaseV2Tool,
notionWriteV2Tool,
notionCreatePageV2Tool,
notionUpdatePageV2Tool,
notionQueryDatabaseV2Tool,
notionSearchV2Tool,
notionCreateDatabaseV2Tool,
notionAddDatabaseRowV2Tool,
notionAppendBlocksV2Tool,
notionRetrieveBlockV2Tool,
notionRetrieveBlockChildrenV2Tool,
notionUpdateBlockV2Tool,
notionDeleteBlockV2Tool,
notionCreateCommentV2Tool,
notionListCommentsV2Tool,
notionListUsersV2Tool,
notionRetrieveUserV2Tool,
}
+111
View File
@@ -0,0 +1,111 @@
import type { NotionListCommentsParams } from '@/tools/notion/types'
import { COMMENT_LIST_RESULTS_OUTPUT, PAGINATION_OUTPUT_PROPERTIES } from '@/tools/notion/types'
import { clampNotionPageSize } from '@/tools/notion/utils'
import type { ToolConfig } from '@/tools/types'
interface NotionListCommentsResponse {
success: boolean
output: {
results: any[]
has_more: boolean
next_cursor: string | null
}
}
export const notionListCommentsTool: ToolConfig<
NotionListCommentsParams,
NotionListCommentsResponse
> = {
id: 'notion_list_comments',
name: 'Notion List Comments',
description: 'List unresolved comments on a Notion page or block',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
blockId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the page or block whose comments to list',
},
startCursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor returned by a previous request',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to return (1-100, default 100)',
},
},
request: {
url: (params: NotionListCommentsParams) => {
const url = new URL('https://api.notion.com/v1/comments')
url.searchParams.set('block_id', params.blockId.trim())
if (params.startCursor) url.searchParams.set('start_cursor', params.startCursor.trim())
const pageSize = clampNotionPageSize(params.pageSize)
if (pageSize != null) url.searchParams.set('page_size', String(pageSize))
return url.toString()
},
method: 'GET',
headers: (params: NotionListCommentsParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: response.ok,
output: {
results: data.results ?? [],
has_more: data.has_more ?? false,
next_cursor: data.next_cursor ?? null,
},
}
},
outputs: {
results: COMMENT_LIST_RESULTS_OUTPUT,
has_more: PAGINATION_OUTPUT_PROPERTIES.has_more,
next_cursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
},
}
export const notionListCommentsV2Tool: ToolConfig<
NotionListCommentsParams,
NotionListCommentsResponse
> = {
id: 'notion_list_comments_v2',
name: 'Notion List Comments',
description: 'List unresolved comments on a Notion page or block',
version: '2.0.0',
oauth: notionListCommentsTool.oauth,
params: notionListCommentsTool.params,
request: notionListCommentsTool.request,
transformResponse: notionListCommentsTool.transformResponse,
outputs: notionListCommentsTool.outputs,
}
+98
View File
@@ -0,0 +1,98 @@
import type { NotionListUsersParams } from '@/tools/notion/types'
import { PAGINATION_OUTPUT_PROPERTIES, USER_LIST_RESULTS_OUTPUT } from '@/tools/notion/types'
import { clampNotionPageSize } from '@/tools/notion/utils'
import type { ToolConfig } from '@/tools/types'
interface NotionListUsersResponse {
success: boolean
output: {
results: any[]
has_more: boolean
next_cursor: string | null
}
}
export const notionListUsersTool: ToolConfig<NotionListUsersParams, NotionListUsersResponse> = {
id: 'notion_list_users',
name: 'Notion List Users',
description: 'List all users (members and bots) in the Notion workspace',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
startCursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor returned by a previous request',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to return (1-100, default 100)',
},
},
request: {
url: (params: NotionListUsersParams) => {
const url = new URL('https://api.notion.com/v1/users')
if (params.startCursor) url.searchParams.set('start_cursor', params.startCursor.trim())
const pageSize = clampNotionPageSize(params.pageSize)
if (pageSize != null) url.searchParams.set('page_size', String(pageSize))
return url.toString()
},
method: 'GET',
headers: (params: NotionListUsersParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: response.ok,
output: {
results: data.results ?? [],
has_more: data.has_more ?? false,
next_cursor: data.next_cursor ?? null,
},
}
},
outputs: {
results: USER_LIST_RESULTS_OUTPUT,
has_more: PAGINATION_OUTPUT_PROPERTIES.has_more,
next_cursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
},
}
export const notionListUsersV2Tool: ToolConfig<NotionListUsersParams, NotionListUsersResponse> = {
id: 'notion_list_users_v2',
name: 'Notion List Users',
description: 'List all users (members and bots) in the Notion workspace',
version: '2.0.0',
oauth: notionListUsersTool.oauth,
params: notionListUsersTool.params,
request: notionListUsersTool.request,
transformResponse: notionListUsersTool.transformResponse,
outputs: notionListUsersTool.outputs,
}
+205
View File
@@ -0,0 +1,205 @@
import { toError } from '@sim/utils/errors'
import type { NotionQueryDatabaseParams, NotionResponse } from '@/tools/notion/types'
import { DATABASE_QUERY_RESULTS_OUTPUT, PAGINATION_OUTPUT_PROPERTIES } from '@/tools/notion/types'
import { extractTitle, formatPropertyValue } from '@/tools/notion/utils'
import type { ToolConfig } from '@/tools/types'
export const notionQueryDatabaseTool: ToolConfig<NotionQueryDatabaseParams, NotionResponse> = {
id: 'notion_query_database',
name: 'Query Notion Database',
description: 'Query and filter Notion database entries with advanced filtering',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
databaseId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the Notion database to query',
},
filter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter conditions as JSON (optional)',
},
sorts: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Sort criteria as JSON array (optional)',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to return (default: 100, max: 100)',
},
startCursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor returned by a previous request',
},
},
request: {
url: (params: NotionQueryDatabaseParams) => {
return `https://api.notion.com/v1/databases/${params.databaseId.trim()}/query`
},
method: 'POST',
headers: (params: NotionQueryDatabaseParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
body: (params: NotionQueryDatabaseParams) => {
const body: any = {}
// Add filter if provided
if (params.filter) {
try {
body.filter = JSON.parse(params.filter)
} catch (error) {
throw new Error(`Invalid filter JSON: ${toError(error).message}`)
}
}
// Add sorts if provided
if (params.sorts) {
try {
body.sorts = JSON.parse(params.sorts)
} catch (error) {
throw new Error(`Invalid sorts JSON: ${toError(error).message}`)
}
}
// Add page size if provided
if (params.pageSize) {
body.page_size = Math.min(Number(params.pageSize), 100)
}
// Add pagination cursor if provided
if (params.startCursor) {
body.start_cursor = params.startCursor.trim()
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
const results = data.results || []
// Format the results into readable content
const content = results
.map((page: any, index: number) => {
const properties = page.properties || {}
const title = extractTitle(properties)
const propertyValues = Object.entries(properties)
.map(([key, value]: [string, any]) => {
const formattedValue = formatPropertyValue(value)
return ` ${key}: ${formattedValue}`
})
.join('\n')
return `Entry ${index + 1}${title ? ` - ${title}` : ''}:\n${propertyValues}`
})
.join('\n\n')
return {
success: true,
output: {
content: content || 'No results found',
metadata: {
totalResults: results.length,
hasMore: data.has_more || false,
nextCursor: data.next_cursor || null,
results: results,
},
},
}
},
outputs: {
content: {
type: 'string',
description: 'Formatted list of database entries with their properties',
},
metadata: {
type: 'object',
description:
'Query metadata including total results count, pagination info, and raw results array',
properties: {
totalResults: { type: 'number', description: 'Number of results returned' },
hasMore: PAGINATION_OUTPUT_PROPERTIES.has_more,
nextCursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
results: DATABASE_QUERY_RESULTS_OUTPUT,
},
},
},
}
// V2 Tool with API-aligned outputs
interface NotionQueryDatabaseV2Response {
success: boolean
output: {
results: any[]
has_more: boolean
next_cursor: string | null
total_results: number
}
}
export const notionQueryDatabaseV2Tool: ToolConfig<
NotionQueryDatabaseParams,
NotionQueryDatabaseV2Response
> = {
id: 'notion_query_database_v2',
name: 'Query Notion Database',
description: 'Query and filter Notion database entries with advanced filtering',
version: '2.0.0',
oauth: notionQueryDatabaseTool.oauth,
params: notionQueryDatabaseTool.params,
request: notionQueryDatabaseTool.request,
transformResponse: async (response: Response) => {
const data = await response.json()
const results = data.results || []
return {
success: true,
output: {
results,
has_more: data.has_more || false,
next_cursor: data.next_cursor || null,
total_results: results.length,
},
}
},
outputs: {
results: DATABASE_QUERY_RESULTS_OUTPUT,
has_more: PAGINATION_OUTPUT_PROPERTIES.has_more,
next_cursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
total_results: { type: 'number', description: 'Number of results returned' },
},
}
+306
View File
@@ -0,0 +1,306 @@
import type { NotionReadParams, NotionResponse } from '@/tools/notion/types'
import { PAGE_OUTPUT_PROPERTIES } from '@/tools/notion/types'
import type { ToolConfig } from '@/tools/types'
export const notionReadTool: ToolConfig<NotionReadParams, NotionResponse> = {
id: 'notion_read',
name: 'Notion Reader',
description: 'Read content from a Notion page',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
pageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the Notion page to read',
},
},
request: {
url: (params: NotionReadParams) => {
return `https://api.notion.com/v1/pages/${params.pageId.trim()}`
},
method: 'GET',
headers: (params: NotionReadParams) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response, params?: NotionReadParams) => {
const data = await response.json()
let pageTitle = 'Untitled'
// Extract title from properties
if (data.properties?.title) {
const titleProperty = data.properties.title
if (
titleProperty.title &&
Array.isArray(titleProperty.title) &&
titleProperty.title.length > 0
) {
pageTitle = titleProperty.title.map((t: any) => t.plain_text || '').join('')
}
}
// Now fetch the page content using blocks endpoint
const pageId = params?.pageId?.trim()
const accessToken = params?.accessToken
if (!pageId || !accessToken) {
return {
success: true,
output: {
content: '',
metadata: {
title: pageTitle,
lastEditedTime: data.last_edited_time,
createdTime: data.created_time,
url: data.url,
},
},
}
}
// Fetch page content using blocks endpoint
const blocksResponse = await fetch(
`https://api.notion.com/v1/blocks/${pageId}/children?page_size=100`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
},
}
)
if (!blocksResponse.ok) {
// If we can't get blocks, still return the page metadata
return {
success: true,
output: {
content: '',
metadata: {
title: pageTitle,
lastEditedTime: data.last_edited_time,
createdTime: data.created_time,
url: data.url,
},
},
}
}
const blocksData = await blocksResponse.json()
// Extract text content from blocks
const blocks = blocksData.results || []
const content = blocks
.map((block: any) => {
if (block.type === 'paragraph') {
return block.paragraph.rich_text.map((text: any) => text.plain_text).join('')
}
if (block.type === 'heading_1') {
return `# ${block.heading_1.rich_text.map((text: any) => text.plain_text).join('')}`
}
if (block.type === 'heading_2') {
return `## ${block.heading_2.rich_text.map((text: any) => text.plain_text).join('')}`
}
if (block.type === 'heading_3') {
return `### ${block.heading_3.rich_text.map((text: any) => text.plain_text).join('')}`
}
if (block.type === 'bulleted_list_item') {
return `${block.bulleted_list_item.rich_text.map((text: any) => text.plain_text).join('')}`
}
if (block.type === 'numbered_list_item') {
return `1. ${block.numbered_list_item.rich_text.map((text: any) => text.plain_text).join('')}`
}
if (block.type === 'to_do') {
const checked = block.to_do.checked ? '[x]' : '[ ]'
return `${checked} ${block.to_do.rich_text.map((text: any) => text.plain_text).join('')}`
}
return ''
})
.filter(Boolean)
.join('\n\n')
return {
success: true,
output: {
content: content,
metadata: {
title: pageTitle,
lastEditedTime: data.last_edited_time,
createdTime: data.created_time,
url: data.url,
},
},
}
},
outputs: {
content: {
type: 'string',
description: 'Page content in markdown format with headers, paragraphs, lists, and todos',
},
metadata: {
type: 'object',
description: 'Page metadata including title, URL, and timestamps',
properties: {
title: { type: 'string', description: 'Page title' },
lastEditedTime: { type: 'string', description: 'ISO 8601 last edit timestamp' },
createdTime: { type: 'string', description: 'ISO 8601 creation timestamp' },
url: { type: 'string', description: 'Notion page URL' },
},
},
},
}
// V2 Tool with API-aligned outputs
interface NotionReadV2Response {
success: boolean
output: {
content: string
title: string
url: string
created_time: string
last_edited_time: string
}
}
export const notionReadV2Tool: ToolConfig<NotionReadParams, NotionReadV2Response> = {
id: 'notion_read_v2',
name: 'Notion Reader',
description: 'Read content from a Notion page',
version: '2.0.0',
oauth: notionReadTool.oauth,
params: notionReadTool.params,
request: notionReadTool.request,
transformResponse: async (response: Response, params?: NotionReadParams) => {
const data = await response.json()
let pageTitle = 'Untitled'
if (data.properties?.title) {
const titleProperty = data.properties.title
if (
titleProperty.title &&
Array.isArray(titleProperty.title) &&
titleProperty.title.length > 0
) {
pageTitle = titleProperty.title.map((t: any) => t.plain_text || '').join('')
}
}
const pageId = params?.pageId?.trim()
const accessToken = params?.accessToken
if (!pageId || !accessToken) {
return {
success: true,
output: {
content: '',
title: pageTitle,
url: data.url,
created_time: data.created_time,
last_edited_time: data.last_edited_time,
},
}
}
const blocksResponse = await fetch(
`https://api.notion.com/v1/blocks/${pageId}/children?page_size=100`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
},
}
)
if (!blocksResponse.ok) {
return {
success: true,
output: {
content: '',
title: pageTitle,
url: data.url,
created_time: data.created_time,
last_edited_time: data.last_edited_time,
},
}
}
const blocksData = await blocksResponse.json()
const blocks = blocksData.results || []
const content = blocks
.map((block: any) => {
if (block.type === 'paragraph') {
return block.paragraph.rich_text.map((text: any) => text.plain_text).join('')
}
if (block.type === 'heading_1') {
return `# ${block.heading_1.rich_text.map((text: any) => text.plain_text).join('')}`
}
if (block.type === 'heading_2') {
return `## ${block.heading_2.rich_text.map((text: any) => text.plain_text).join('')}`
}
if (block.type === 'heading_3') {
return `### ${block.heading_3.rich_text.map((text: any) => text.plain_text).join('')}`
}
if (block.type === 'bulleted_list_item') {
return `${block.bulleted_list_item.rich_text.map((text: any) => text.plain_text).join('')}`
}
if (block.type === 'numbered_list_item') {
return `1. ${block.numbered_list_item.rich_text.map((text: any) => text.plain_text).join('')}`
}
if (block.type === 'to_do') {
const checked = block.to_do.checked ? '[x]' : '[ ]'
return `${checked} ${block.to_do.rich_text.map((text: any) => text.plain_text).join('')}`
}
return ''
})
.filter(Boolean)
.join('\n\n')
return {
success: true,
output: {
content,
title: pageTitle,
url: data.url,
created_time: data.created_time,
last_edited_time: data.last_edited_time,
},
}
},
outputs: {
content: { type: 'string', description: 'Page content in markdown format' },
title: { type: 'string', description: 'Page title' },
url: PAGE_OUTPUT_PROPERTIES.url,
created_time: PAGE_OUTPUT_PROPERTIES.created_time,
last_edited_time: PAGE_OUTPUT_PROPERTIES.last_edited_time,
},
}
+163
View File
@@ -0,0 +1,163 @@
import type { NotionResponse } from '@/tools/notion/types'
import { DATABASE_OUTPUT_PROPERTIES } from '@/tools/notion/types'
import type { ToolConfig } from '@/tools/types'
export interface NotionReadDatabaseParams {
databaseId: string
accessToken: string
}
export const notionReadDatabaseTool: ToolConfig<NotionReadDatabaseParams, NotionResponse> = {
id: 'notion_read_database',
name: 'Read Notion Database',
description: 'Read database information and structure from Notion',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
databaseId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the Notion database to read',
},
},
request: {
url: (params: NotionReadDatabaseParams) => {
return `https://api.notion.com/v1/databases/${params.databaseId.trim()}`
},
method: 'GET',
headers: (params: NotionReadDatabaseParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
// Extract database title
const title = data.title?.map((t: any) => t.plain_text || '').join('') || 'Untitled Database'
// Extract properties for display
const properties = data.properties || {}
const propertyList = Object.entries(properties)
.map(([name, prop]: [string, any]) => ` ${name}: ${prop.type}`)
.join('\n')
const content = [
`Database: ${title}`,
'',
'Properties:',
propertyList,
'',
`Database ID: ${data.id}`,
`URL: ${data.url}`,
`Created: ${data.created_time ? new Date(data.created_time).toLocaleDateString() : 'Unknown'}`,
`Last edited: ${data.last_edited_time ? new Date(data.last_edited_time).toLocaleDateString() : 'Unknown'}`,
].join('\n')
return {
success: true,
output: {
content,
metadata: {
title,
url: data.url,
id: data.id,
createdTime: data.created_time,
lastEditedTime: data.last_edited_time,
properties: data.properties,
},
},
}
},
outputs: {
content: {
type: 'string',
description: 'Database information including title, properties schema, and metadata',
},
metadata: {
type: 'object',
description: 'Database metadata including title, ID, URL, timestamps, and properties schema',
properties: {
title: { type: 'string', description: 'Database title' },
url: DATABASE_OUTPUT_PROPERTIES.url,
id: DATABASE_OUTPUT_PROPERTIES.id,
createdTime: DATABASE_OUTPUT_PROPERTIES.created_time,
lastEditedTime: DATABASE_OUTPUT_PROPERTIES.last_edited_time,
properties: DATABASE_OUTPUT_PROPERTIES.properties,
},
},
},
}
interface NotionReadDatabaseV2Response {
success: boolean
output: {
id: string
title: string
url: string
created_time: string
last_edited_time: string
properties: Record<string, any>
}
}
export const notionReadDatabaseV2Tool: ToolConfig<
NotionReadDatabaseParams,
NotionReadDatabaseV2Response
> = {
id: 'notion_read_database_v2',
name: 'Read Notion Database',
description: 'Read database information and structure from Notion',
version: '2.0.0',
oauth: notionReadDatabaseTool.oauth,
params: notionReadDatabaseTool.params,
request: notionReadDatabaseTool.request,
transformResponse: async (response: Response) => {
const data = await response.json()
const title = data.title?.map((t: any) => t.plain_text || '').join('') || 'Untitled Database'
return {
success: true,
output: {
id: data.id,
title,
url: data.url,
created_time: data.created_time,
last_edited_time: data.last_edited_time,
properties: data.properties || {},
},
}
},
outputs: {
id: DATABASE_OUTPUT_PROPERTIES.id,
title: { type: 'string', description: 'Database title' },
url: DATABASE_OUTPUT_PROPERTIES.url,
created_time: DATABASE_OUTPUT_PROPERTIES.created_time,
last_edited_time: DATABASE_OUTPUT_PROPERTIES.last_edited_time,
properties: DATABASE_OUTPUT_PROPERTIES.properties,
},
}
+107
View File
@@ -0,0 +1,107 @@
import { BLOCK_OUTPUT_PROPERTIES } from '@/tools/notion/types'
import type { ToolConfig } from '@/tools/types'
export interface NotionRetrieveBlockParams {
blockId: string
accessToken: string
}
interface NotionRetrieveBlockResponse {
success: boolean
output: {
id: string
type: string
has_children: boolean
archived: boolean
block: Record<string, any>
}
}
export const notionRetrieveBlockTool: ToolConfig<
NotionRetrieveBlockParams,
NotionRetrieveBlockResponse
> = {
id: 'notion_retrieve_block',
name: 'Notion Retrieve Block',
description: 'Retrieve a single Notion block by its UUID, including its type-specific content',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
blockId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the block to retrieve',
},
},
request: {
url: (params: NotionRetrieveBlockParams) =>
`https://api.notion.com/v1/blocks/${params.blockId.trim()}`,
method: 'GET',
headers: (params: NotionRetrieveBlockParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: response.ok,
output: {
id: data.id,
type: data.type ?? '',
has_children: data.has_children ?? false,
archived: data.archived ?? false,
block: data,
},
}
},
outputs: {
id: BLOCK_OUTPUT_PROPERTIES.id,
type: BLOCK_OUTPUT_PROPERTIES.type,
has_children: BLOCK_OUTPUT_PROPERTIES.has_children,
archived: BLOCK_OUTPUT_PROPERTIES.archived,
block: {
type: 'object',
description:
'The full Notion block object. Includes a type-specific field (e.g. paragraph, heading_1, image) whose shape varies by block type and is not enumerated below — read it directly off this object.',
properties: BLOCK_OUTPUT_PROPERTIES,
},
},
}
export const notionRetrieveBlockV2Tool: ToolConfig<
NotionRetrieveBlockParams,
NotionRetrieveBlockResponse
> = {
id: 'notion_retrieve_block_v2',
name: 'Notion Retrieve Block',
description: 'Retrieve a single Notion block by its UUID, including its type-specific content',
version: '2.0.0',
oauth: notionRetrieveBlockTool.oauth,
params: notionRetrieveBlockTool.params,
request: notionRetrieveBlockTool.request,
transformResponse: notionRetrieveBlockTool.transformResponse,
outputs: notionRetrieveBlockTool.outputs,
}
@@ -0,0 +1,110 @@
import type { NotionRetrieveBlockChildrenParams } from '@/tools/notion/types'
import { BLOCK_LIST_RESULTS_OUTPUT, PAGINATION_OUTPUT_PROPERTIES } from '@/tools/notion/types'
import { clampNotionPageSize } from '@/tools/notion/utils'
import type { ToolConfig } from '@/tools/types'
interface NotionRetrieveBlockChildrenResponse {
success: boolean
output: {
results: any[]
has_more: boolean
next_cursor: string | null
}
}
export const notionRetrieveBlockChildrenTool: ToolConfig<
NotionRetrieveBlockChildrenParams,
NotionRetrieveBlockChildrenResponse
> = {
id: 'notion_retrieve_block_children',
name: 'Notion Retrieve Block Children',
description: 'Retrieve the block children (content) of a Notion page or block',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
blockId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the page or block whose children to retrieve',
},
startCursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor returned by a previous request',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to return (1-100, default 100)',
},
},
request: {
url: (params: NotionRetrieveBlockChildrenParams) => {
const url = new URL(`https://api.notion.com/v1/blocks/${params.blockId.trim()}/children`)
if (params.startCursor) url.searchParams.set('start_cursor', params.startCursor.trim())
const pageSize = clampNotionPageSize(params.pageSize)
if (pageSize != null) url.searchParams.set('page_size', String(pageSize))
return url.toString()
},
method: 'GET',
headers: (params: NotionRetrieveBlockChildrenParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: response.ok,
output: {
results: data.results ?? [],
has_more: data.has_more ?? false,
next_cursor: data.next_cursor ?? null,
},
}
},
outputs: {
results: BLOCK_LIST_RESULTS_OUTPUT,
has_more: PAGINATION_OUTPUT_PROPERTIES.has_more,
next_cursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
},
}
export const notionRetrieveBlockChildrenV2Tool: ToolConfig<
NotionRetrieveBlockChildrenParams,
NotionRetrieveBlockChildrenResponse
> = {
id: 'notion_retrieve_block_children_v2',
name: 'Notion Retrieve Block Children',
description: 'Retrieve the block children (content) of a Notion page or block',
version: '2.0.0',
oauth: notionRetrieveBlockChildrenTool.oauth,
params: notionRetrieveBlockChildrenTool.params,
request: notionRetrieveBlockChildrenTool.request,
transformResponse: notionRetrieveBlockChildrenTool.transformResponse,
outputs: notionRetrieveBlockChildrenTool.outputs,
}
+102
View File
@@ -0,0 +1,102 @@
import type { NotionRetrieveUserParams } from '@/tools/notion/types'
import { USER_OUTPUT_PROPERTIES } from '@/tools/notion/types'
import type { ToolConfig } from '@/tools/types'
interface NotionRetrieveUserResponse {
success: boolean
output: {
id: string
type: string | null
name: string | null
avatar_url: string | null
email: string | null
}
}
export const notionRetrieveUserTool: ToolConfig<
NotionRetrieveUserParams,
NotionRetrieveUserResponse
> = {
id: 'notion_retrieve_user',
name: 'Notion Retrieve User',
description: 'Retrieve a single Notion user by their UUID',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the user to retrieve',
},
},
request: {
url: (params: NotionRetrieveUserParams) =>
`https://api.notion.com/v1/users/${params.userId.trim()}`,
method: 'GET',
headers: (params: NotionRetrieveUserParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: response.ok,
output: {
id: data.id,
type: data.type ?? null,
name: data.name ?? null,
avatar_url: data.avatar_url ?? null,
email: data.person?.email ?? null,
},
}
},
outputs: {
id: USER_OUTPUT_PROPERTIES.id,
type: USER_OUTPUT_PROPERTIES.type,
name: USER_OUTPUT_PROPERTIES.name,
avatar_url: USER_OUTPUT_PROPERTIES.avatar_url,
email: {
type: 'string',
description: 'User email address (person users only)',
optional: true,
},
},
}
export const notionRetrieveUserV2Tool: ToolConfig<
NotionRetrieveUserParams,
NotionRetrieveUserResponse
> = {
id: 'notion_retrieve_user_v2',
name: 'Notion Retrieve User',
description: 'Retrieve a single Notion user by their UUID',
version: '2.0.0',
oauth: notionRetrieveUserTool.oauth,
params: notionRetrieveUserTool.params,
request: notionRetrieveUserTool.request,
transformResponse: notionRetrieveUserTool.transformResponse,
outputs: notionRetrieveUserTool.outputs,
}
+196
View File
@@ -0,0 +1,196 @@
import type { NotionResponse, NotionSearchParams } from '@/tools/notion/types'
import { PAGINATION_OUTPUT_PROPERTIES, SEARCH_RESULTS_OUTPUT } from '@/tools/notion/types'
import { extractTitleFromItem } from '@/tools/notion/utils'
import type { ToolConfig } from '@/tools/types'
export const notionSearchTool: ToolConfig<NotionSearchParams, NotionResponse> = {
id: 'notion_search',
name: 'Search Notion Workspace',
description: 'Search across all pages and databases in Notion workspace',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search terms to find pages and databases (leave empty to get all pages)',
},
filterType: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by object type: "page", "database", or leave empty for all',
},
pageSize: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Number of results to return (default: 100, max: 100)',
},
startCursor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Pagination cursor returned by a previous request',
},
},
request: {
url: () => 'https://api.notion.com/v1/search',
method: 'POST',
headers: (params: NotionSearchParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
body: (params: NotionSearchParams) => {
const body: any = {}
// Add query if provided
if (params.query?.trim()) {
body.query = params.query.trim()
}
// Add filter if provided (skip 'all' as it means no filter)
if (
params.filterType &&
params.filterType !== 'all' &&
['page', 'database'].includes(params.filterType)
) {
body.filter = {
value: params.filterType,
property: 'object',
}
}
// Add page size if provided
if (params.pageSize) {
body.page_size = Math.min(Number(params.pageSize), 100)
}
// Add pagination cursor if provided
if (params.startCursor) {
body.start_cursor = params.startCursor.trim()
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
const results = data.results || []
// Format the results into readable content
const content = results
.map((item: any, index: number) => {
const objectType = item.object === 'page' ? 'Page' : 'Database'
const title = extractTitleFromItem(item)
const url = item.url || ''
const lastEdited = item.last_edited_time
? new Date(item.last_edited_time).toLocaleDateString()
: ''
return [
`${index + 1}. ${objectType}: ${title}`,
` URL: ${url}`,
lastEdited ? ` Last edited: ${lastEdited}` : '',
]
.filter(Boolean)
.join('\n')
})
.join('\n\n')
return {
success: true,
output: {
content: content || 'No results found',
metadata: {
totalResults: results.length,
hasMore: data.has_more || false,
nextCursor: data.next_cursor || null,
results: results,
},
},
}
},
outputs: {
content: {
type: 'string',
description: 'Formatted list of search results including pages and databases',
},
metadata: {
type: 'object',
description:
'Search metadata including total results count, pagination info, and raw results array',
properties: {
totalResults: { type: 'number', description: 'Number of results returned' },
hasMore: PAGINATION_OUTPUT_PROPERTIES.has_more,
nextCursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
results: SEARCH_RESULTS_OUTPUT,
},
},
},
}
// V2 Tool with API-aligned outputs
interface NotionSearchV2Response {
success: boolean
output: {
results: any[]
has_more: boolean
next_cursor: string | null
total_results: number
}
}
export const notionSearchV2Tool: ToolConfig<NotionSearchParams, NotionSearchV2Response> = {
id: 'notion_search_v2',
name: 'Search Notion Workspace',
description: 'Search across all pages and databases in Notion workspace',
version: '2.0.0',
oauth: notionSearchTool.oauth,
params: notionSearchTool.params,
request: notionSearchTool.request,
transformResponse: async (response: Response) => {
const data = await response.json()
const results = data.results || []
return {
success: true,
output: {
results,
has_more: data.has_more || false,
next_cursor: data.next_cursor || null,
total_results: results.length,
},
}
},
outputs: {
results: SEARCH_RESULTS_OUTPUT,
has_more: PAGINATION_OUTPUT_PROPERTIES.has_more,
next_cursor: PAGINATION_OUTPUT_PROPERTIES.next_cursor,
total_results: { type: 'number', description: 'Number of results returned' },
},
}
+594
View File
@@ -0,0 +1,594 @@
import type { OutputProperty, ToolResponse } from '@/tools/types'
/**
* Shared output property definitions for Notion API responses.
* Based on official Notion API documentation at https://developers.notion.com/reference/
*
* @see https://developers.notion.com/reference/page
* @see https://developers.notion.com/reference/database
* @see https://developers.notion.com/reference/block
* @see https://developers.notion.com/reference/user
* @see https://developers.notion.com/reference/rich-text
* @see https://developers.notion.com/reference/file-object
* @see https://developers.notion.com/reference/parent-object
*/
/**
* Partial User object properties returned by Notion API.
* Used for created_by and last_edited_by fields.
* @see https://developers.notion.com/reference/user
*/
export const PARTIAL_USER_OUTPUT_PROPERTIES = {
object: { type: 'string', description: 'Always "user"' },
id: { type: 'string', description: 'User UUID' },
} as const satisfies Record<string, OutputProperty>
/**
* Complete partial user output definition
*/
export const PARTIAL_USER_OUTPUT: OutputProperty = {
type: 'object',
description: 'Partial user object',
properties: PARTIAL_USER_OUTPUT_PROPERTIES,
}
/**
* Full User object properties returned by Notion API.
* @see https://developers.notion.com/reference/user
*/
export const USER_OUTPUT_PROPERTIES = {
object: { type: 'string', description: 'Always "user"' },
id: { type: 'string', description: 'User UUID' },
type: { type: 'string', description: 'User type: "person" or "bot"', optional: true },
name: { type: 'string', description: 'User display name', optional: true },
avatar_url: { type: 'string', description: 'URL to user avatar image', optional: true },
} as const satisfies Record<string, OutputProperty>
/**
* Complete user output definition
*/
export const USER_OUTPUT: OutputProperty = {
type: 'object',
description: 'User object',
properties: USER_OUTPUT_PROPERTIES,
}
/**
* Parent object properties for pages and databases.
* @see https://developers.notion.com/reference/parent-object
*/
export const PARENT_OUTPUT_PROPERTIES = {
type: {
type: 'string',
description:
'Parent type: "database_id", "data_source_id", "page_id", "workspace", or "block_id"',
},
database_id: {
type: 'string',
description: 'Parent database UUID (if type is database_id)',
optional: true,
},
data_source_id: {
type: 'string',
description: 'Parent data source UUID (if type is data_source_id)',
optional: true,
},
page_id: { type: 'string', description: 'Parent page UUID (if type is page_id)', optional: true },
workspace: {
type: 'boolean',
description: 'True if parent is workspace (if type is workspace)',
optional: true,
},
block_id: {
type: 'string',
description: 'Parent block UUID (if type is block_id)',
optional: true,
},
} as const satisfies Record<string, OutputProperty>
/**
* Complete parent output definition
*/
export const PARENT_OUTPUT: OutputProperty = {
type: 'object',
description: 'Parent object specifying hierarchical relationship',
properties: PARENT_OUTPUT_PROPERTIES,
}
/**
* Rich text annotation properties.
* @see https://developers.notion.com/reference/rich-text
*/
export const ANNOTATIONS_OUTPUT_PROPERTIES = {
bold: { type: 'boolean', description: 'Bold styling' },
italic: { type: 'boolean', description: 'Italic styling' },
strikethrough: { type: 'boolean', description: 'Strikethrough styling' },
underline: { type: 'boolean', description: 'Underline styling' },
code: { type: 'boolean', description: 'Code styling' },
color: {
type: 'string',
description:
'Text color (default, blue, green, red, purple, orange, pink, gray, brown, yellow, or _background variants)',
},
} as const satisfies Record<string, OutputProperty>
/**
* Rich text object properties.
* @see https://developers.notion.com/reference/rich-text
*/
export const RICH_TEXT_OUTPUT_PROPERTIES = {
type: { type: 'string', description: 'Rich text type: "text", "mention", or "equation"' },
plain_text: { type: 'string', description: 'Plain text content without annotations' },
href: { type: 'string', description: 'URL for links or Notion mentions', optional: true },
annotations: {
type: 'object',
description: 'Text styling annotations',
properties: ANNOTATIONS_OUTPUT_PROPERTIES,
},
} as const satisfies Record<string, OutputProperty>
/**
* Complete rich text array output definition
*/
export const RICH_TEXT_ARRAY_OUTPUT: OutputProperty = {
type: 'array',
description: 'Array of rich text objects',
items: {
type: 'object',
properties: RICH_TEXT_OUTPUT_PROPERTIES,
},
}
/**
* Notion-hosted file object properties (type: "file").
* @see https://developers.notion.com/reference/file-object
*/
export const NOTION_FILE_OUTPUT_PROPERTIES = {
url: { type: 'string', description: 'Authenticated URL valid for one hour' },
expiry_time: { type: 'string', description: 'ISO 8601 timestamp when URL expires' },
} as const satisfies Record<string, OutputProperty>
/**
* API-uploaded file object properties (type: "file_upload").
* @see https://developers.notion.com/reference/file-object
*/
export const FILE_UPLOAD_OUTPUT_PROPERTIES = {
id: { type: 'string', description: 'File upload UUID' },
} as const satisfies Record<string, OutputProperty>
/**
* External file object properties (type: "external").
* @see https://developers.notion.com/reference/file-object
*/
export const EXTERNAL_FILE_OUTPUT_PROPERTIES = {
url: { type: 'string', description: 'External file URL (never expires)' },
} as const satisfies Record<string, OutputProperty>
/**
* File object properties.
* @see https://developers.notion.com/reference/file-object
*/
export const FILE_OUTPUT_PROPERTIES = {
type: { type: 'string', description: 'File type: "file", "file_upload", or "external"' },
file: {
type: 'object',
description: 'Notion-hosted file object (when type is "file")',
optional: true,
properties: NOTION_FILE_OUTPUT_PROPERTIES,
},
file_upload: {
type: 'object',
description: 'API-uploaded file object (when type is "file_upload")',
optional: true,
properties: FILE_UPLOAD_OUTPUT_PROPERTIES,
},
external: {
type: 'object',
description: 'External file object (when type is "external")',
optional: true,
properties: EXTERNAL_FILE_OUTPUT_PROPERTIES,
},
} as const satisfies Record<string, OutputProperty>
/**
* Emoji object properties.
* @see https://developers.notion.com/reference/emoji-object
*/
export const EMOJI_OUTPUT_PROPERTIES = {
type: { type: 'string', description: 'Always "emoji" for standard emojis' },
emoji: { type: 'string', description: 'The emoji character' },
} as const satisfies Record<string, OutputProperty>
/**
* Custom emoji object properties.
* @see https://developers.notion.com/reference/emoji-object
*/
export const CUSTOM_EMOJI_OUTPUT_PROPERTIES = {
type: { type: 'string', description: 'Always "custom_emoji"' },
custom_emoji: {
type: 'object',
description: 'Custom emoji details',
properties: {
id: { type: 'string', description: 'Custom emoji UUID' },
name: { type: 'string', description: 'Custom emoji name', optional: true },
url: { type: 'string', description: 'URL to custom emoji image', optional: true },
},
},
} as const satisfies Record<string, OutputProperty>
/**
* Icon output (can be emoji, custom_emoji, or file)
*/
export const ICON_OUTPUT: OutputProperty = {
type: 'object',
description: 'Page/database icon (emoji, custom_emoji, or file)',
optional: true,
properties: {
type: { type: 'string', description: 'Icon type: "emoji", "custom_emoji", or "file"' },
emoji: { type: 'string', description: 'Emoji character (if type is emoji)', optional: true },
custom_emoji: {
type: 'object',
description: 'Custom emoji object (if type is custom_emoji)',
optional: true,
properties: {
id: { type: 'string', description: 'Custom emoji UUID' },
name: { type: 'string', description: 'Custom emoji name', optional: true },
url: { type: 'string', description: 'URL to custom emoji image', optional: true },
},
},
file: {
type: 'object',
description: 'Notion-hosted file (if type is file)',
optional: true,
properties: NOTION_FILE_OUTPUT_PROPERTIES,
},
external: {
type: 'object',
description: 'External file (if type is external)',
optional: true,
properties: EXTERNAL_FILE_OUTPUT_PROPERTIES,
},
},
}
/**
* Cover output (file object for page/database covers)
*/
export const COVER_OUTPUT: OutputProperty = {
type: 'object',
description: 'Page/database cover image',
optional: true,
properties: FILE_OUTPUT_PROPERTIES,
}
/**
* Common page object properties from Notion API.
* @see https://developers.notion.com/reference/page
*/
export const PAGE_OUTPUT_PROPERTIES = {
object: { type: 'string', description: 'Always "page"' },
id: { type: 'string', description: 'Page UUID' },
created_time: { type: 'string', description: 'ISO 8601 creation timestamp' },
last_edited_time: { type: 'string', description: 'ISO 8601 last edit timestamp' },
created_by: PARTIAL_USER_OUTPUT,
last_edited_by: PARTIAL_USER_OUTPUT,
archived: { type: 'boolean', description: 'Whether the page is archived' },
in_trash: { type: 'boolean', description: 'Whether the page is in trash' },
url: { type: 'string', description: 'Notion page URL' },
public_url: {
type: 'string',
description: 'Public web URL if shared, null otherwise',
optional: true,
},
parent: PARENT_OUTPUT,
icon: ICON_OUTPUT,
cover: COVER_OUTPUT,
properties: {
type: 'object',
description:
'Page property values (structure depends on parent type - database properties or title only)',
},
} as const satisfies Record<string, OutputProperty>
/**
* Complete page output definition for array items
*/
export const PAGE_OUTPUT: OutputProperty = {
type: 'object',
description: 'Notion page object',
properties: PAGE_OUTPUT_PROPERTIES,
}
/**
* Common database object properties from Notion API.
* @see https://developers.notion.com/reference/database
*/
export const DATABASE_OUTPUT_PROPERTIES = {
object: { type: 'string', description: 'Always "database"' },
id: { type: 'string', description: 'Database UUID' },
created_time: { type: 'string', description: 'ISO 8601 creation timestamp' },
last_edited_time: { type: 'string', description: 'ISO 8601 last edit timestamp' },
created_by: PARTIAL_USER_OUTPUT,
last_edited_by: PARTIAL_USER_OUTPUT,
title: RICH_TEXT_ARRAY_OUTPUT,
description: RICH_TEXT_ARRAY_OUTPUT,
icon: ICON_OUTPUT,
cover: COVER_OUTPUT,
parent: PARENT_OUTPUT,
url: { type: 'string', description: 'Notion database URL' },
public_url: {
type: 'string',
description: 'Public web URL if shared, null otherwise',
optional: true,
},
archived: { type: 'boolean', description: 'Whether the database is archived' },
in_trash: { type: 'boolean', description: 'Whether the database is in trash' },
is_inline: { type: 'boolean', description: 'Whether displayed as inline block or child page' },
properties: { type: 'object', description: 'Database properties schema' },
} as const satisfies Record<string, OutputProperty>
/**
* Complete database output definition for array items
*/
export const DATABASE_OUTPUT: OutputProperty = {
type: 'object',
description: 'Notion database object',
properties: DATABASE_OUTPUT_PROPERTIES,
}
/**
* Block object properties from Notion API.
* @see https://developers.notion.com/reference/block
*/
export const BLOCK_OUTPUT_PROPERTIES = {
object: { type: 'string', description: 'Always "block"' },
id: { type: 'string', description: 'Block UUID' },
parent: PARENT_OUTPUT,
type: {
type: 'string',
description: 'Block type (paragraph, heading_1, heading_2, heading_3, image, etc.)',
},
created_time: { type: 'string', description: 'ISO 8601 creation timestamp' },
last_edited_time: { type: 'string', description: 'ISO 8601 last edit timestamp' },
created_by: PARTIAL_USER_OUTPUT,
last_edited_by: PARTIAL_USER_OUTPUT,
archived: { type: 'boolean', description: 'Whether the block is archived' },
in_trash: { type: 'boolean', description: 'Whether the block is in trash' },
has_children: { type: 'boolean', description: 'Whether the block has nested blocks' },
} as const satisfies Record<string, OutputProperty>
/**
* Complete block output definition for array items
*/
export const BLOCK_OUTPUT: OutputProperty = {
type: 'object',
description: 'Notion block object',
properties: BLOCK_OUTPUT_PROPERTIES,
}
/**
* Pagination output properties for list responses.
* @see https://developers.notion.com/reference/intro (JSON conventions - Pagination section)
*/
export const PAGINATION_OUTPUT_PROPERTIES = {
object: { type: 'string', description: 'Always "list"' },
has_more: { type: 'boolean', description: 'Whether more results are available' },
next_cursor: { type: 'string', description: 'Cursor for next page of results', optional: true },
type: {
type: 'string',
description: 'Type of items in results (e.g., "page_or_database", "page_or_data_source")',
},
} as const satisfies Record<string, OutputProperty>
/**
* Search/query results array output with page items
*/
export const SEARCH_RESULTS_OUTPUT: OutputProperty = {
type: 'array',
description: 'Array of search results (pages and/or databases)',
items: {
type: 'object',
properties: {
object: { type: 'string', description: 'Object type: "page" or "database"' },
id: { type: 'string', description: 'Object UUID' },
created_time: { type: 'string', description: 'ISO 8601 creation timestamp' },
last_edited_time: { type: 'string', description: 'ISO 8601 last edit timestamp' },
created_by: PARTIAL_USER_OUTPUT,
last_edited_by: PARTIAL_USER_OUTPUT,
archived: { type: 'boolean', description: 'Whether the object is archived' },
in_trash: { type: 'boolean', description: 'Whether the object is in trash' },
url: { type: 'string', description: 'Object URL' },
public_url: { type: 'string', description: 'Public web URL if shared', optional: true },
parent: PARENT_OUTPUT,
properties: { type: 'object', description: 'Object properties' },
},
},
}
/**
* Database query results array output with page items
*/
export const DATABASE_QUERY_RESULTS_OUTPUT: OutputProperty = {
type: 'array',
description: 'Array of page objects from the database',
items: {
type: 'object',
properties: PAGE_OUTPUT_PROPERTIES,
},
}
/**
* Comment object properties from Notion API.
* @see https://developers.notion.com/reference/comment-object
*/
export const COMMENT_OUTPUT_PROPERTIES = {
object: { type: 'string', description: 'Always "comment"' },
id: { type: 'string', description: 'Comment UUID' },
parent: PARENT_OUTPUT,
discussion_id: { type: 'string', description: 'UUID of the discussion thread' },
created_time: { type: 'string', description: 'ISO 8601 creation timestamp' },
last_edited_time: { type: 'string', description: 'ISO 8601 last edit timestamp' },
created_by: PARTIAL_USER_OUTPUT,
rich_text: RICH_TEXT_ARRAY_OUTPUT,
} as const satisfies Record<string, OutputProperty>
/**
* Array of block objects (used by append/retrieve block children).
*/
export const BLOCK_LIST_RESULTS_OUTPUT: OutputProperty = {
type: 'array',
description: 'Array of Notion block objects',
items: {
type: 'object',
properties: BLOCK_OUTPUT_PROPERTIES,
},
}
/**
* Array of comment objects (used by list comments).
*/
export const COMMENT_LIST_RESULTS_OUTPUT: OutputProperty = {
type: 'array',
description: 'Array of Notion comment objects',
items: {
type: 'object',
properties: COMMENT_OUTPUT_PROPERTIES,
},
}
/**
* Array of user objects (used by list users).
*/
export const USER_LIST_RESULTS_OUTPUT: OutputProperty = {
type: 'array',
description: 'Array of Notion user objects',
items: {
type: 'object',
properties: USER_OUTPUT_PROPERTIES,
},
}
export interface NotionReadParams {
pageId: string
accessToken: string
}
export interface NotionResponse extends ToolResponse {
output: {
content: string
metadata?: {
title?: string
lastEditedTime?: string
createdTime?: string
url?: string
// Additional metadata for query/search operations
totalResults?: number
hasMore?: boolean
nextCursor?: string | null
results?: any[]
// Additional metadata for create operations
id?: string
properties?: Record<string, any>
}
}
}
export interface NotionWriteParams {
pageId: string
content: string
accessToken: string
}
export interface NotionCreatePageParams {
parentId: string
title?: string
content?: string
accessToken: string
}
export interface NotionUpdatePageParams {
pageId: string
properties: Record<string, any>
accessToken: string
}
export interface NotionQueryDatabaseParams {
databaseId: string
filter?: string
sorts?: string
pageSize?: number
startCursor?: string
accessToken: string
}
export interface NotionSearchParams {
query?: string
filterType?: string
pageSize?: number
startCursor?: string
accessToken: string
}
export interface NotionCreateDatabaseParams {
parentId: string
title: string
properties?: Record<string, any>
accessToken: string
}
export interface NotionAddDatabaseRowParams {
databaseId: string
properties: Record<string, any>
accessToken: string
}
export interface NotionAppendBlocksParams {
blockId: string
children: any[] | string
after?: string
accessToken: string
}
export interface NotionRetrieveBlockChildrenParams {
blockId: string
startCursor?: string
pageSize?: number
accessToken: string
}
export interface NotionUpdateBlockParams {
blockId: string
block: Record<string, any> | string
archived?: boolean
accessToken: string
}
export interface NotionDeleteBlockParams {
blockId: string
accessToken: string
}
export interface NotionCreateCommentParams {
pageId?: string
discussionId?: string
content: string
accessToken: string
}
export interface NotionListCommentsParams {
blockId: string
startCursor?: string
pageSize?: number
accessToken: string
}
export interface NotionListUsersParams {
startCursor?: string
pageSize?: number
accessToken: string
}
export interface NotionRetrieveUserParams {
userId: string
accessToken: string
}
+132
View File
@@ -0,0 +1,132 @@
import { isRecordLike } from '@sim/utils/object'
import type { NotionUpdateBlockParams } from '@/tools/notion/types'
import { BLOCK_OUTPUT_PROPERTIES } from '@/tools/notion/types'
import type { ToolConfig } from '@/tools/types'
interface NotionUpdateBlockResponse {
success: boolean
output: {
id: string
type: string
archived: boolean
block: Record<string, any>
}
}
/**
* Coerce the block param into an object, accepting either a JSON string
* (when called directly by an agent) or an already-parsed object.
*/
function parseBlock(block: Record<string, any> | string): Record<string, any> {
if (typeof block === 'string') {
const parsed = JSON.parse(block)
if (!isRecordLike(parsed)) {
throw new Error('block must be a JSON object describing the block-type fields to update')
}
return parsed
}
if (isRecordLike(block)) return block
throw new Error('block must be a JSON object describing the block-type fields to update')
}
export const notionUpdateBlockTool: ToolConfig<NotionUpdateBlockParams, NotionUpdateBlockResponse> =
{
id: 'notion_update_block',
name: 'Notion Update Block',
description: 'Update the content or archived state of a single Notion block',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
blockId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the block to update',
},
block: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Block-type object with the fields to update, e.g. {"paragraph": {"rich_text": [...]}}',
},
archived: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Set to true to archive (delete) the block, or false to restore it',
},
},
request: {
url: (params: NotionUpdateBlockParams) =>
`https://api.notion.com/v1/blocks/${params.blockId.trim()}`,
method: 'PATCH',
headers: (params: NotionUpdateBlockParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
body: (params: NotionUpdateBlockParams) => {
const body: Record<string, any> = parseBlock(params.block)
if (params.archived != null) body.archived = params.archived
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: response.ok,
output: {
id: data.id,
type: data.type ?? '',
archived: data.archived ?? false,
block: data,
},
}
},
outputs: {
id: BLOCK_OUTPUT_PROPERTIES.id,
type: BLOCK_OUTPUT_PROPERTIES.type,
archived: BLOCK_OUTPUT_PROPERTIES.archived,
block: {
type: 'object',
description: 'The full updated Notion block object',
properties: BLOCK_OUTPUT_PROPERTIES,
},
},
}
export const notionUpdateBlockV2Tool: ToolConfig<
NotionUpdateBlockParams,
NotionUpdateBlockResponse
> = {
id: 'notion_update_block_v2',
name: 'Notion Update Block',
description: 'Update the content or archived state of a single Notion block',
version: '2.0.0',
oauth: notionUpdateBlockTool.oauth,
params: notionUpdateBlockTool.params,
request: notionUpdateBlockTool.request,
transformResponse: notionUpdateBlockTool.transformResponse,
outputs: notionUpdateBlockTool.outputs,
}
+166
View File
@@ -0,0 +1,166 @@
import type { NotionResponse, NotionUpdatePageParams } from '@/tools/notion/types'
import { PAGE_OUTPUT_PROPERTIES } from '@/tools/notion/types'
import type { ToolConfig } from '@/tools/types'
export const notionUpdatePageTool: ToolConfig<NotionUpdatePageParams, NotionResponse> = {
id: 'notion_update_page',
name: 'Notion Page Updater',
description: 'Update properties of a Notion page',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
pageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the Notion page to update',
},
properties: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description: 'JSON object of properties to update',
},
},
request: {
url: (params: NotionUpdatePageParams) => {
return `https://api.notion.com/v1/pages/${params.pageId.trim()}`
},
method: 'PATCH',
headers: (params: NotionUpdatePageParams) => {
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
body: (params: NotionUpdatePageParams) => ({
properties: params.properties,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
let pageTitle = 'Untitled'
// Try to extract the title from properties
if (data.properties?.title) {
const titleProperty = data.properties.title
if (
titleProperty.title &&
Array.isArray(titleProperty.title) &&
titleProperty.title.length > 0
) {
pageTitle = titleProperty.title.map((t: any) => t.plain_text || '').join('')
}
}
return {
success: true,
output: {
content: 'Successfully updated page properties',
metadata: {
title: pageTitle,
pageId: data.id,
url: data.url,
lastEditedTime: data.last_edited_time,
updatedTime: new Date().toISOString(),
},
},
}
},
outputs: {
content: {
type: 'string',
description: 'Success message confirming page properties update',
},
metadata: {
type: 'object',
description: 'Page metadata including title, page ID, URL, and update timestamps',
properties: {
title: { type: 'string', description: 'Page title' },
pageId: PAGE_OUTPUT_PROPERTIES.id,
url: PAGE_OUTPUT_PROPERTIES.url,
lastEditedTime: PAGE_OUTPUT_PROPERTIES.last_edited_time,
updatedTime: {
type: 'string',
description: 'ISO 8601 timestamp when update was performed',
},
},
},
},
}
// V2 Tool with API-aligned outputs
interface NotionUpdatePageV2Response {
success: boolean
output: {
id: string
title: string
url: string
last_edited_time: string
}
}
export const notionUpdatePageV2Tool: ToolConfig<
NotionUpdatePageParams,
NotionUpdatePageV2Response
> = {
id: 'notion_update_page_v2',
name: 'Notion Page Updater',
description: 'Update properties of a Notion page',
version: '2.0.0',
oauth: notionUpdatePageTool.oauth,
params: notionUpdatePageTool.params,
request: notionUpdatePageTool.request,
transformResponse: async (response: Response) => {
const data = await response.json()
let pageTitle = 'Untitled'
if (data.properties?.title) {
const titleProperty = data.properties.title
if (
titleProperty.title &&
Array.isArray(titleProperty.title) &&
titleProperty.title.length > 0
) {
pageTitle = titleProperty.title.map((t: any) => t.plain_text || '').join('')
}
}
return {
success: true,
output: {
id: data.id,
title: pageTitle,
url: data.url,
last_edited_time: data.last_edited_time,
},
}
},
outputs: {
id: PAGE_OUTPUT_PROPERTIES.id,
title: { type: 'string', description: 'Page title' },
url: PAGE_OUTPUT_PROPERTIES.url,
last_edited_time: PAGE_OUTPUT_PROPERTIES.last_edited_time,
},
}
+71
View File
@@ -0,0 +1,71 @@
/**
* Clamp a Notion `page_size` value to the API's supported 1-100 range. Returns
* undefined when the input isn't a finite number so callers can omit the param.
*/
export function clampNotionPageSize(value: unknown): number | undefined {
if (value == null || value === '') return undefined
const parsed = Number(value)
if (!Number.isFinite(parsed)) return undefined
return Math.min(Math.max(Math.trunc(parsed), 1), 100)
}
export function formatPropertyValue(property: any): string {
switch (property.type) {
case 'title':
return property.title?.map((t: any) => t.plain_text || '').join('') || ''
case 'rich_text':
return property.rich_text?.map((t: any) => t.plain_text || '').join('') || ''
case 'number':
return String(property.number || '')
case 'select':
return property.select?.name || ''
case 'multi_select':
return property.multi_select?.map((s: any) => s.name).join(', ') || ''
case 'date':
return property.date?.start || ''
case 'checkbox':
return property.checkbox ? 'Yes' : 'No'
case 'url':
return property.url || ''
case 'email':
return property.email || ''
case 'phone_number':
return property.phone_number || ''
default:
return JSON.stringify(property)
}
}
export function extractTitle(properties: Record<string, any>): string {
for (const [, value] of Object.entries(properties)) {
if (
value.type === 'title' &&
value.title &&
Array.isArray(value.title) &&
value.title.length > 0
) {
return value.title.map((t: any) => t.plain_text || '').join('')
}
}
return ''
}
export function extractTitleFromItem(item: any): string {
if (item.object === 'page') {
// For pages, check properties first
if (item.properties?.title?.title && Array.isArray(item.properties.title.title)) {
const title = item.properties.title.title.map((t: any) => t.plain_text || '').join('')
if (title) return title
}
// Fallback to page title
return item.title || 'Untitled Page'
}
if (item.object === 'database') {
// For databases, get title from title array
if (item.title && Array.isArray(item.title)) {
return item.title.map((t: any) => t.plain_text || '').join('') || 'Untitled Database'
}
return 'Untitled Database'
}
return 'Untitled'
}
+121
View File
@@ -0,0 +1,121 @@
import type { NotionResponse, NotionWriteParams } from '@/tools/notion/types'
import type { ToolConfig } from '@/tools/types'
export const notionWriteTool: ToolConfig<NotionWriteParams, NotionResponse> = {
id: 'notion_write',
name: 'Notion Content Appender',
description: 'Append content to a Notion page',
version: '1.0.0',
oauth: {
required: true,
provider: 'notion',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'Notion OAuth access token',
},
pageId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UUID of the Notion page to append content to',
},
content: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The content to append to the page',
},
},
request: {
url: (params: NotionWriteParams) => {
return `https://api.notion.com/v1/blocks/${params.pageId.trim()}/children`
},
method: 'PATCH',
headers: (params: NotionWriteParams) => {
// Validate access token
if (!params.accessToken) {
throw new Error('Access token is required')
}
return {
Authorization: `Bearer ${params.accessToken}`,
'Notion-Version': '2022-06-28',
'Content-Type': 'application/json',
}
},
body: (params: NotionWriteParams) => ({
children: [
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [
{
type: 'text',
text: {
content: params.content,
},
},
],
},
},
],
}),
},
transformResponse: async (response: Response) => {
const _data = await response.json()
return {
success: response.ok,
output: {
content: 'Successfully appended content to Notion page',
},
}
},
outputs: {
content: {
type: 'string',
description: 'Success message confirming content was appended to page',
},
},
}
// V2 Tool with API-aligned outputs
interface NotionWriteV2Response {
success: boolean
output: {
appended: boolean
}
}
export const notionWriteV2Tool: ToolConfig<NotionWriteParams, NotionWriteV2Response> = {
id: 'notion_write_v2',
name: 'Notion Content Appender',
description: 'Append content to a Notion page',
version: '2.0.0',
oauth: notionWriteTool.oauth,
params: notionWriteTool.params,
request: notionWriteTool.request,
transformResponse: async (response: Response) => {
await response.json()
return {
success: response.ok,
output: {
appended: response.ok,
},
}
},
outputs: {
appended: { type: 'boolean', description: 'Whether content was successfully appended' },
},
}