notion-mcp-server 1.0.0 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +383 -192
- package/build/config/index.js +3 -1
- package/build/dispatch/concurrency.js +15 -0
- package/build/dispatch/idempotency.js +38 -0
- package/build/dispatch/index.js +175 -0
- package/build/dispatch/rate-limit.js +56 -0
- package/build/dispatch/retry.js +97 -0
- package/build/index.js +1 -1
- package/build/markdown/parse.js +265 -0
- package/build/operations/blocks.js +331 -0
- package/build/operations/comments.js +191 -0
- package/build/operations/data-sources.js +85 -0
- package/build/operations/databases.js +345 -0
- package/build/operations/files.js +239 -0
- package/build/operations/index.js +19 -0
- package/build/operations/pages.js +486 -0
- package/build/operations/registry.js +16 -0
- package/build/operations/users.js +101 -0
- package/build/prompts/index.js +105 -0
- package/build/schema/blocks.js +19 -77
- package/build/schema/database.js +27 -86
- package/build/schema/emit.js +68 -0
- package/build/schema/file.js +1 -1
- package/build/schema/filter-dsl.js +333 -0
- package/build/schema/icon.js +1 -1
- package/build/schema/page-properties.js +17 -3
- package/build/schema/page.js +12 -88
- package/build/schema/refs.js +16 -0
- package/build/schema/rich-text.js +1 -1
- package/build/server/index.js +15 -2
- package/build/services/auth.js +19 -0
- package/build/services/notion.js +14 -17
- package/build/tools/index.js +119 -51
- package/build/utils/error.js +125 -86
- package/build/utils/handler.js +11 -0
- package/build/utils/learning-error.js +40 -0
- package/build/utils/notion-types.js +16 -0
- package/build/utils/paginate.js +35 -0
- package/build/utils/schema-slice.js +156 -0
- package/build/utils/slim.js +269 -0
- package/package.json +13 -7
- package/build/resources/imageList.js +0 -62
- package/build/resources/index.js +0 -1
- package/build/resources/predictionList.js +0 -43
- package/build/resources/svgList.js +0 -69
- package/build/schema/comments.js +0 -34
- package/build/schema/notion.js +0 -57
- package/build/schema/richText.js +0 -757
- package/build/schema/tools.js +0 -17
- package/build/schema/users.js +0 -13
- package/build/services/replicate.js +0 -23
- package/build/tools/appendBlockChildren.js +0 -25
- package/build/tools/batchAppendBlockChildren.js +0 -33
- package/build/tools/batchDeleteBlocks.js +0 -32
- package/build/tools/batchMixedOperations.js +0 -58
- package/build/tools/batchUpdateBlocks.js +0 -33
- package/build/tools/comments.js +0 -62
- package/build/tools/createDatabase.js +0 -18
- package/build/tools/createPage.js +0 -18
- package/build/tools/createPrediction.js +0 -28
- package/build/tools/deleteBlock.js +0 -24
- package/build/tools/formatRichText.js +0 -83
- package/build/tools/generateImage.js +0 -48
- package/build/tools/generateImageVariants.js +0 -105
- package/build/tools/generateMultipleImages.js +0 -60
- package/build/tools/generateSVG.js +0 -43
- package/build/tools/getPrediction.js +0 -22
- package/build/tools/predictionList.js +0 -30
- package/build/tools/queryDatabase.js +0 -22
- package/build/tools/retrieveBlock.js +0 -24
- package/build/tools/retrieveBlockChildren.js +0 -32
- package/build/tools/searchPage.js +0 -24
- package/build/tools/updateBlock.js +0 -25
- package/build/tools/updateDatabase.js +0 -18
- package/build/tools/updatePage.js +0 -40
- package/build/tools/updatePageProperties.js +0 -21
- package/build/tools/users.js +0 -62
- package/build/types/blocks.js +0 -11
- package/build/types/comments.js +0 -6
- package/build/types/database.js +0 -5
- package/build/types/notion.js +0 -1
- package/build/types/page.js +0 -7
- package/build/types/richText.js +0 -1
- package/build/types/tools.js +0 -1
- package/build/types/users.js +0 -5
- package/build/utils/blob.js +0 -5
- package/build/utils/image.js +0 -34
- package/build/utils/index.js +0 -1
- package/build/utils/richText.js +0 -174
- package/build/validation/blocks.js +0 -568
- package/build/validation/notion.js +0 -51
- package/build/validation/page.js +0 -262
- package/build/validation/richText.js +0 -744
- package/build/validation/tools.js +0 -16
- /package/build/{types/index.js → operations/types.js} +0 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getClient } from "../services/notion.js";
|
|
3
|
+
import { register } from "./registry.js";
|
|
4
|
+
import { tryHandler } from "../utils/handler.js";
|
|
5
|
+
import { slimPage, slimItem, slimList } from "../utils/slim.js";
|
|
6
|
+
import { paginateAll } from "../utils/paginate.js";
|
|
7
|
+
import { parseMarkdownToBlocks } from "../markdown/parse.js";
|
|
8
|
+
import { PARENT_SCHEMA } from "../schema/page.js";
|
|
9
|
+
import { ICON_SCHEMA } from "../schema/icon.js";
|
|
10
|
+
import { FILE_SCHEMA } from "../schema/file.js";
|
|
11
|
+
import { RICH_TEXT_ITEM_REQUEST_SCHEMA } from "../schema/rich-text.js";
|
|
12
|
+
import { asSdk, } from "../utils/notion-types.js";
|
|
13
|
+
import { CHECKBOX_PROPERTY_VALUE_SCHEMA, DATE_PROPERTY_VALUE_SCHEMA, EMAIL_PROPERTY_VALUE_SCHEMA, FILES_PROPERTY_VALUE_SCHEMA, MULTI_SELECT_PROPERTY_VALUE_SCHEMA, NUMBER_PROPERTY_VALUE_SCHEMA, PEOPLE_PROPERTY_VALUE_SCHEMA, PHONE_NUMBER_PROPERTY_VALUE_SCHEMA, RELATION_PROPERTY_VALUE_SCHEMA, RICH_TEXT_PROPERTY_VALUE_SCHEMA, SELECT_PROPERTY_VALUE_SCHEMA, STATUS_PROPERTY_VALUE_SCHEMA, TITLE_PROPERTY_VALUE_SCHEMA, URL_PROPERTY_VALUE_SCHEMA, VERIFICATION_PROPERTY_VALUE_SCHEMA, } from "../schema/page-properties.js";
|
|
14
|
+
const VERBOSE = z.boolean().optional();
|
|
15
|
+
const PROPERTY_VALUE_SCHEMA = z.union([
|
|
16
|
+
TITLE_PROPERTY_VALUE_SCHEMA,
|
|
17
|
+
RICH_TEXT_PROPERTY_VALUE_SCHEMA,
|
|
18
|
+
NUMBER_PROPERTY_VALUE_SCHEMA,
|
|
19
|
+
SELECT_PROPERTY_VALUE_SCHEMA,
|
|
20
|
+
MULTI_SELECT_PROPERTY_VALUE_SCHEMA,
|
|
21
|
+
STATUS_PROPERTY_VALUE_SCHEMA,
|
|
22
|
+
DATE_PROPERTY_VALUE_SCHEMA,
|
|
23
|
+
PEOPLE_PROPERTY_VALUE_SCHEMA,
|
|
24
|
+
FILES_PROPERTY_VALUE_SCHEMA,
|
|
25
|
+
CHECKBOX_PROPERTY_VALUE_SCHEMA,
|
|
26
|
+
URL_PROPERTY_VALUE_SCHEMA,
|
|
27
|
+
EMAIL_PROPERTY_VALUE_SCHEMA,
|
|
28
|
+
PHONE_NUMBER_PROPERTY_VALUE_SCHEMA,
|
|
29
|
+
RELATION_PROPERTY_VALUE_SCHEMA,
|
|
30
|
+
VERIFICATION_PROPERTY_VALUE_SCHEMA,
|
|
31
|
+
]);
|
|
32
|
+
function resolveParent(parent) {
|
|
33
|
+
if (parent)
|
|
34
|
+
return parent;
|
|
35
|
+
const envId = process.env.NOTION_PAGE_ID;
|
|
36
|
+
if (envId)
|
|
37
|
+
return { type: "page_id", page_id: envId };
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// create_page
|
|
42
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
43
|
+
const CreatePageParams = z
|
|
44
|
+
.object({
|
|
45
|
+
parent: PARENT_SCHEMA.optional(),
|
|
46
|
+
title: z.string().optional().describe("Shortcut for setting the title property."),
|
|
47
|
+
properties: z.record(z.string(), PROPERTY_VALUE_SCHEMA).optional(),
|
|
48
|
+
markdown: z.string().optional().describe("Page body as markdown. Parsed server-side."),
|
|
49
|
+
children: z.array(z.unknown()).optional().describe("Structured Notion blocks. Mutually exclusive with markdown."),
|
|
50
|
+
icon: ICON_SCHEMA.nullable().optional(),
|
|
51
|
+
cover: FILE_SCHEMA.nullable().optional(),
|
|
52
|
+
verbose: VERBOSE,
|
|
53
|
+
})
|
|
54
|
+
.refine((v) => !(v.markdown && v.children), {
|
|
55
|
+
message: "Pass either `markdown` or `children`, not both.",
|
|
56
|
+
});
|
|
57
|
+
register({
|
|
58
|
+
name: "create_page",
|
|
59
|
+
description: "Create a new Notion page. Body can be markdown (recommended) or structured blocks.",
|
|
60
|
+
batchable: true,
|
|
61
|
+
schema: CreatePageParams,
|
|
62
|
+
example: {
|
|
63
|
+
parent: { type: "page_id", page_id: "<parent-page-id>" },
|
|
64
|
+
title: "My new page",
|
|
65
|
+
markdown: "## Hello\n\nThis is the body as **markdown**.",
|
|
66
|
+
},
|
|
67
|
+
exampleBatch: {
|
|
68
|
+
items: [
|
|
69
|
+
{ title: "Page 1", markdown: "First page body." },
|
|
70
|
+
{ title: "Page 2", markdown: "Second page body." },
|
|
71
|
+
],
|
|
72
|
+
concurrency: 3,
|
|
73
|
+
},
|
|
74
|
+
rollback: async (data) => {
|
|
75
|
+
if (typeof data !== "object" || data === null)
|
|
76
|
+
return;
|
|
77
|
+
const id = data.id;
|
|
78
|
+
if (!id)
|
|
79
|
+
return;
|
|
80
|
+
const notion = await getClient();
|
|
81
|
+
await notion.pages.update(asSdk({ page_id: id, in_trash: true }));
|
|
82
|
+
},
|
|
83
|
+
handler: tryHandler(async (params) => {
|
|
84
|
+
const parent = resolveParent(params.parent);
|
|
85
|
+
if (!parent) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
error: {
|
|
89
|
+
code: "missing_parent",
|
|
90
|
+
message: "No parent specified and NOTION_PAGE_ID is not set.",
|
|
91
|
+
fix: "Pass `parent: {type:'page_id', page_id:'...'}` or set NOTION_PAGE_ID in the environment.",
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const properties = { ...(params.properties ?? {}) };
|
|
96
|
+
if (params.title && !properties.title) {
|
|
97
|
+
properties.title = {
|
|
98
|
+
title: [{ type: "text", text: { content: params.title } }],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const children = params.markdown
|
|
102
|
+
? parseMarkdownToBlocks(params.markdown)
|
|
103
|
+
: params.children;
|
|
104
|
+
const notion = await getClient();
|
|
105
|
+
const body = {
|
|
106
|
+
parent,
|
|
107
|
+
properties,
|
|
108
|
+
...(children && children.length ? { children } : {}),
|
|
109
|
+
...(params.icon !== undefined ? { icon: params.icon } : {}),
|
|
110
|
+
...(params.cover !== undefined ? { cover: params.cover } : {}),
|
|
111
|
+
};
|
|
112
|
+
const response = await notion.pages.create(asSdk(body));
|
|
113
|
+
return { ok: true, data: slimPage(response, params.verbose ?? false) };
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
117
|
+
// set_page_title
|
|
118
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
119
|
+
const SetPageTitleParams = z.object({
|
|
120
|
+
page_id: z.string(),
|
|
121
|
+
title: z.string(),
|
|
122
|
+
verbose: VERBOSE,
|
|
123
|
+
});
|
|
124
|
+
register({
|
|
125
|
+
name: "set_page_title",
|
|
126
|
+
description: "Rename a page. Updates the page's title property.",
|
|
127
|
+
batchable: true,
|
|
128
|
+
schema: SetPageTitleParams,
|
|
129
|
+
example: { page_id: "<page-id>", title: "New title" },
|
|
130
|
+
exampleBatch: {
|
|
131
|
+
items: [
|
|
132
|
+
{ page_id: "<page-id-1>", title: "Renamed 1" },
|
|
133
|
+
{ page_id: "<page-id-2>", title: "Renamed 2" },
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
handler: tryHandler(async ({ page_id, title, verbose }) => {
|
|
137
|
+
const notion = await getClient();
|
|
138
|
+
const response = await notion.pages.update(asSdk({
|
|
139
|
+
page_id,
|
|
140
|
+
properties: {
|
|
141
|
+
title: { title: [{ type: "text", text: { content: title } }] },
|
|
142
|
+
},
|
|
143
|
+
}));
|
|
144
|
+
return { ok: true, data: slimPage(response, verbose ?? false) };
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
148
|
+
// set_page_property
|
|
149
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
150
|
+
// For the title property, accept a bare string as a shorthand and wrap it into
|
|
151
|
+
// Notion's rich-text array shape. Avoids forcing the LLM to know the verbose
|
|
152
|
+
// {title:[{type:"text",text:{content}}]} form for the most common case.
|
|
153
|
+
function wrapTitleShorthand(input) {
|
|
154
|
+
if (typeof input !== "object" || input === null)
|
|
155
|
+
return input;
|
|
156
|
+
const obj = input;
|
|
157
|
+
if (obj.name === "title" && typeof obj.value === "string") {
|
|
158
|
+
return {
|
|
159
|
+
...obj,
|
|
160
|
+
value: { title: [{ type: "text", text: { content: obj.value } }] },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return input;
|
|
164
|
+
}
|
|
165
|
+
const SetPagePropertyParams = z.preprocess(wrapTitleShorthand, z.object({
|
|
166
|
+
page_id: z.string(),
|
|
167
|
+
name: z.string().describe("Property name (case-sensitive). Use `title` for the title property; you may pass value as a plain string in that case."),
|
|
168
|
+
value: PROPERTY_VALUE_SCHEMA.describe("Property value object matching the property type, e.g. {checkbox: true}, {select: {name: 'Open'}}. For `name: 'title'` a plain string is accepted as a shorthand."),
|
|
169
|
+
verbose: VERBOSE,
|
|
170
|
+
}));
|
|
171
|
+
register({
|
|
172
|
+
name: "set_page_property",
|
|
173
|
+
description: "Set one property on one page. For batch updates use items[].",
|
|
174
|
+
batchable: true,
|
|
175
|
+
schema: SetPagePropertyParams,
|
|
176
|
+
example: {
|
|
177
|
+
page_id: "<page-id>",
|
|
178
|
+
name: "Status",
|
|
179
|
+
value: { status: { name: "In progress" } },
|
|
180
|
+
},
|
|
181
|
+
exampleBatch: {
|
|
182
|
+
items: [
|
|
183
|
+
{ page_id: "<page-id>", name: "Checked", value: { checkbox: true } },
|
|
184
|
+
{ page_id: "<page-id>", name: "Score", value: { number: 42 } },
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
handler: tryHandler(async ({ page_id, name, value, verbose }) => {
|
|
188
|
+
const notion = await getClient();
|
|
189
|
+
const response = await notion.pages.update(asSdk({ page_id, properties: { [name]: value } }));
|
|
190
|
+
return { ok: true, data: slimPage(response, verbose ?? false) };
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
194
|
+
// set_page_properties (plural)
|
|
195
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
196
|
+
function wrapTitleShorthandInProperties(input) {
|
|
197
|
+
if (typeof input !== "object" || input === null)
|
|
198
|
+
return input;
|
|
199
|
+
const obj = input;
|
|
200
|
+
const props = obj.properties;
|
|
201
|
+
if (typeof props !== "object" || props === null)
|
|
202
|
+
return input;
|
|
203
|
+
const propsObj = props;
|
|
204
|
+
if (typeof propsObj.title !== "string")
|
|
205
|
+
return input;
|
|
206
|
+
return {
|
|
207
|
+
...obj,
|
|
208
|
+
properties: {
|
|
209
|
+
...propsObj,
|
|
210
|
+
title: { title: [{ type: "text", text: { content: propsObj.title } }] },
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const SetPagePropertiesParams = z.preprocess(wrapTitleShorthandInProperties, z.object({
|
|
215
|
+
page_id: z.string(),
|
|
216
|
+
properties: z
|
|
217
|
+
.record(z.string(), PROPERTY_VALUE_SCHEMA)
|
|
218
|
+
.describe("Map of property name → value, written in one API call. Use this when updating multiple properties on the same page. For the `title` key, a plain string is accepted as a shorthand."),
|
|
219
|
+
verbose: VERBOSE,
|
|
220
|
+
}));
|
|
221
|
+
register({
|
|
222
|
+
name: "set_page_properties",
|
|
223
|
+
description: "Set multiple properties on one page in a single API call. Use set_page_property for one-off updates.",
|
|
224
|
+
batchable: true,
|
|
225
|
+
schema: SetPagePropertiesParams,
|
|
226
|
+
example: {
|
|
227
|
+
page_id: "<page-id>",
|
|
228
|
+
properties: {
|
|
229
|
+
Status: { status: { name: "In progress" } },
|
|
230
|
+
Score: { number: 42 },
|
|
231
|
+
Done: { checkbox: false },
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
exampleBatch: {
|
|
235
|
+
items: [
|
|
236
|
+
{
|
|
237
|
+
page_id: "<page-id-1>",
|
|
238
|
+
properties: { Status: { status: { name: "Done" } } },
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
page_id: "<page-id-2>",
|
|
242
|
+
properties: { Status: { status: { name: "Done" } } },
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
handler: tryHandler(async ({ page_id, properties, verbose }) => {
|
|
247
|
+
const notion = await getClient();
|
|
248
|
+
const response = await notion.pages.update(asSdk({ page_id, properties }));
|
|
249
|
+
return { ok: true, data: slimPage(response, verbose ?? false) };
|
|
250
|
+
}),
|
|
251
|
+
});
|
|
252
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
253
|
+
// archive_page / restore_page
|
|
254
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
255
|
+
const PageIdParams = z.object({ page_id: z.string(), verbose: VERBOSE });
|
|
256
|
+
const archivePageHandler = tryHandler(async ({ page_id, verbose }) => {
|
|
257
|
+
const notion = await getClient();
|
|
258
|
+
const response = await notion.pages.update(asSdk({ page_id, in_trash: true }));
|
|
259
|
+
return { ok: true, data: slimPage(response, verbose ?? false) };
|
|
260
|
+
});
|
|
261
|
+
register({
|
|
262
|
+
name: "archive_page",
|
|
263
|
+
description: "Move a page to trash. Reversible via restore_page. Alias: trash_page.",
|
|
264
|
+
batchable: true,
|
|
265
|
+
schema: PageIdParams,
|
|
266
|
+
example: { page_id: "<page-id>" },
|
|
267
|
+
exampleBatch: { items: [{ page_id: "<page-id-1>" }, { page_id: "<page-id-2>" }] },
|
|
268
|
+
handler: archivePageHandler,
|
|
269
|
+
});
|
|
270
|
+
register({
|
|
271
|
+
name: "trash_page",
|
|
272
|
+
description: "Alias of archive_page (2025-09-03 surface naming). Moves a page to trash.",
|
|
273
|
+
batchable: true,
|
|
274
|
+
schema: PageIdParams,
|
|
275
|
+
example: { page_id: "<page-id>" },
|
|
276
|
+
exampleBatch: { items: [{ page_id: "<page-id-1>" }, { page_id: "<page-id-2>" }] },
|
|
277
|
+
handler: archivePageHandler,
|
|
278
|
+
});
|
|
279
|
+
register({
|
|
280
|
+
name: "restore_page",
|
|
281
|
+
description: "Restore a page previously moved to trash.",
|
|
282
|
+
batchable: true,
|
|
283
|
+
schema: PageIdParams,
|
|
284
|
+
example: { page_id: "<page-id>" },
|
|
285
|
+
handler: tryHandler(async ({ page_id, verbose }) => {
|
|
286
|
+
const notion = await getClient();
|
|
287
|
+
const response = await notion.pages.update(asSdk({ page_id, in_trash: false }));
|
|
288
|
+
return { ok: true, data: slimPage(response, verbose ?? false) };
|
|
289
|
+
}),
|
|
290
|
+
});
|
|
291
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
292
|
+
// search_pages
|
|
293
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
294
|
+
const SearchPagesParams = z.object({
|
|
295
|
+
query: z.string().optional().describe("Title substring. Notion search is title-only — it does not search page body content."),
|
|
296
|
+
sort_direction: z.enum(["ascending", "descending"]).optional(),
|
|
297
|
+
page_size: z.number().min(1).max(100).optional(),
|
|
298
|
+
start_cursor: z.string().optional(),
|
|
299
|
+
paginate: z
|
|
300
|
+
.boolean()
|
|
301
|
+
.optional()
|
|
302
|
+
.describe("Auto-walk all pages and return combined results. Ignores start_cursor when set."),
|
|
303
|
+
page_limit: z
|
|
304
|
+
.number()
|
|
305
|
+
.min(1)
|
|
306
|
+
.max(1000)
|
|
307
|
+
.optional()
|
|
308
|
+
.describe("Max pages to fetch when paginate=true (default 10, ~1000 items with page_size=100)."),
|
|
309
|
+
verbose: VERBOSE,
|
|
310
|
+
});
|
|
311
|
+
register({
|
|
312
|
+
name: "search_pages",
|
|
313
|
+
description: "Search pages and databases by title. Title-only; does NOT search page body content. Pass paginate:true to auto-walk all pages.",
|
|
314
|
+
batchable: false,
|
|
315
|
+
schema: SearchPagesParams,
|
|
316
|
+
example: { query: "smoke test", page_size: 10 },
|
|
317
|
+
handler: tryHandler(async ({ query, sort_direction, page_size, start_cursor, paginate, page_limit, verbose }) => {
|
|
318
|
+
const notion = await getClient();
|
|
319
|
+
const sort = sort_direction
|
|
320
|
+
? { sort: { direction: sort_direction, timestamp: "last_edited_time" } }
|
|
321
|
+
: {};
|
|
322
|
+
if (paginate) {
|
|
323
|
+
const { results, truncated, pages_walked } = await paginateAll(async (cursor) => {
|
|
324
|
+
const r = await notion.search({
|
|
325
|
+
query: query ?? "",
|
|
326
|
+
...sort,
|
|
327
|
+
page_size: page_size ?? 100,
|
|
328
|
+
start_cursor: cursor,
|
|
329
|
+
});
|
|
330
|
+
return { results: r.results, has_more: r.has_more, next_cursor: r.next_cursor };
|
|
331
|
+
}, { limit: page_limit });
|
|
332
|
+
return {
|
|
333
|
+
ok: true,
|
|
334
|
+
data: {
|
|
335
|
+
results: results.map((item) => slimItem(item, verbose ?? false)),
|
|
336
|
+
truncated,
|
|
337
|
+
pages_walked,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const response = await notion.search({
|
|
342
|
+
query: query ?? "",
|
|
343
|
+
...sort,
|
|
344
|
+
page_size: page_size ?? 10,
|
|
345
|
+
start_cursor,
|
|
346
|
+
});
|
|
347
|
+
return { ok: true, data: slimList(response, slimItem, verbose ?? false) };
|
|
348
|
+
}),
|
|
349
|
+
});
|
|
350
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
351
|
+
// get_page
|
|
352
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
353
|
+
const GetPageParams = z.object({
|
|
354
|
+
page_id: z.string(),
|
|
355
|
+
include_properties: z
|
|
356
|
+
.boolean()
|
|
357
|
+
.optional()
|
|
358
|
+
.describe("When true, attach the page's properties as a flattened name → value map. Off by default to keep the slim response tight; use `verbose: true` for the raw Notion SDK shape."),
|
|
359
|
+
verbose: VERBOSE,
|
|
360
|
+
});
|
|
361
|
+
register({
|
|
362
|
+
name: "get_page",
|
|
363
|
+
description: "Retrieve a page's metadata and (optionally) properties. No body blocks — use get_block_children for those. Pass `include_properties: true` to also return a flattened properties map.",
|
|
364
|
+
batchable: true,
|
|
365
|
+
schema: GetPageParams,
|
|
366
|
+
example: { page_id: "<page-id>" },
|
|
367
|
+
handler: tryHandler(async ({ page_id, include_properties, verbose }) => {
|
|
368
|
+
const notion = await getClient();
|
|
369
|
+
const response = await notion.pages.retrieve({ page_id });
|
|
370
|
+
return {
|
|
371
|
+
ok: true,
|
|
372
|
+
data: slimPage(response, verbose ?? false, include_properties ?? false),
|
|
373
|
+
};
|
|
374
|
+
}),
|
|
375
|
+
});
|
|
376
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
377
|
+
// move_page
|
|
378
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
379
|
+
const MovePageParams = z.object({
|
|
380
|
+
page_id: z.string(),
|
|
381
|
+
parent: PARENT_SCHEMA.describe("New parent (page_id or data_source_id). Same shape as create_page's `parent`."),
|
|
382
|
+
verbose: VERBOSE,
|
|
383
|
+
});
|
|
384
|
+
register({
|
|
385
|
+
name: "move_page",
|
|
386
|
+
description: "Move a page to a new parent without recreating it. Preserves the page's blocks, properties, and comments. The destination uses `parent` — same field name as create_page.",
|
|
387
|
+
batchable: true,
|
|
388
|
+
schema: MovePageParams,
|
|
389
|
+
example: {
|
|
390
|
+
page_id: "<page-id>",
|
|
391
|
+
parent: { type: "page_id", page_id: "<new-parent-id>" },
|
|
392
|
+
},
|
|
393
|
+
exampleBatch: {
|
|
394
|
+
items: [
|
|
395
|
+
{ page_id: "<p1>", parent: { type: "page_id", page_id: "<dest>" } },
|
|
396
|
+
{ page_id: "<p2>", parent: { type: "page_id", page_id: "<dest>" } },
|
|
397
|
+
],
|
|
398
|
+
},
|
|
399
|
+
handler: tryHandler(async ({ page_id, parent, verbose }) => {
|
|
400
|
+
if (parent.type !== "page_id" && parent.type !== "data_source_id") {
|
|
401
|
+
return {
|
|
402
|
+
ok: false,
|
|
403
|
+
error: {
|
|
404
|
+
code: "unsupported_parent",
|
|
405
|
+
message: `move_page only accepts page_id or data_source_id, received ${parent.type}.`,
|
|
406
|
+
fix: parent.type === "database_id"
|
|
407
|
+
? "Call list_data_sources on the database and pass the resolved data_source_id."
|
|
408
|
+
: "Set parent.type to 'page_id' or 'data_source_id'.",
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const notion = await getClient();
|
|
413
|
+
const response = await notion.pages.move(asSdk({ page_id, parent }));
|
|
414
|
+
return { ok: true, data: slimPage(response, verbose ?? false) };
|
|
415
|
+
}),
|
|
416
|
+
});
|
|
417
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
418
|
+
// get_page_markdown
|
|
419
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
420
|
+
const GetPageMarkdownParams = z.object({
|
|
421
|
+
page_id: z.string(),
|
|
422
|
+
});
|
|
423
|
+
register({
|
|
424
|
+
name: "get_page_markdown",
|
|
425
|
+
description: "Return a page's body as Notion-rendered markdown. Server-side conversion; round-trips with update_page_markdown.",
|
|
426
|
+
batchable: true,
|
|
427
|
+
schema: GetPageMarkdownParams,
|
|
428
|
+
example: { page_id: "<page-id>" },
|
|
429
|
+
handler: tryHandler(async ({ page_id }) => {
|
|
430
|
+
const notion = await getClient();
|
|
431
|
+
const response = await notion.pages.retrieveMarkdown({ page_id });
|
|
432
|
+
return { ok: true, data: { page_id, markdown: response.markdown ?? "" } };
|
|
433
|
+
}),
|
|
434
|
+
});
|
|
435
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
436
|
+
// update_page_markdown
|
|
437
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
438
|
+
const UpdatePageMarkdownParams = z.object({
|
|
439
|
+
page_id: z.string(),
|
|
440
|
+
markdown: z.string().describe("Markdown content. Replaces the existing body by default; with insert_content it is inserted instead."),
|
|
441
|
+
insert_content: z
|
|
442
|
+
.object({
|
|
443
|
+
position: z.enum(["start", "end"]).describe("Insert at start or end of the page."),
|
|
444
|
+
after: z.string().optional().describe("Block id to insert after (mutually exclusive with position in practice — Notion uses whichever is provided)."),
|
|
445
|
+
})
|
|
446
|
+
.optional(),
|
|
447
|
+
allow_deleting_content: z
|
|
448
|
+
.boolean()
|
|
449
|
+
.optional()
|
|
450
|
+
.describe("Required true when a replace would remove existing blocks; the API rejects destructive replaces without it."),
|
|
451
|
+
});
|
|
452
|
+
register({
|
|
453
|
+
name: "update_page_markdown",
|
|
454
|
+
description: "Replace (or insert into) a page's body using Notion's server-side markdown renderer. Skip the local remark pipeline.",
|
|
455
|
+
batchable: true,
|
|
456
|
+
schema: UpdatePageMarkdownParams,
|
|
457
|
+
example: {
|
|
458
|
+
page_id: "<page-id>",
|
|
459
|
+
markdown: "## Updated heading\n\nNew body.",
|
|
460
|
+
allow_deleting_content: true,
|
|
461
|
+
},
|
|
462
|
+
handler: tryHandler(async ({ page_id, markdown, insert_content, allow_deleting_content }) => {
|
|
463
|
+
const notion = await getClient();
|
|
464
|
+
const body = insert_content
|
|
465
|
+
? {
|
|
466
|
+
page_id,
|
|
467
|
+
type: "insert_content",
|
|
468
|
+
insert_content: {
|
|
469
|
+
content: markdown,
|
|
470
|
+
...(insert_content.after ? { after: insert_content.after } : {}),
|
|
471
|
+
position: { type: insert_content.position },
|
|
472
|
+
},
|
|
473
|
+
}
|
|
474
|
+
: {
|
|
475
|
+
page_id,
|
|
476
|
+
type: "replace_content",
|
|
477
|
+
replace_content: {
|
|
478
|
+
new_str: markdown,
|
|
479
|
+
...(allow_deleting_content !== undefined ? { allow_deleting_content } : {}),
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
const response = await notion.pages.updateMarkdown(asSdk(body));
|
|
483
|
+
return { ok: true, data: { page_id: response.id ?? page_id } };
|
|
484
|
+
}),
|
|
485
|
+
});
|
|
486
|
+
void RICH_TEXT_ITEM_REQUEST_SCHEMA;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const registry = new Map();
|
|
2
|
+
export function register(def) {
|
|
3
|
+
if (registry.has(def.name)) {
|
|
4
|
+
throw new Error(`Operation already registered: ${def.name}`);
|
|
5
|
+
}
|
|
6
|
+
registry.set(def.name, def);
|
|
7
|
+
}
|
|
8
|
+
export function getOperation(name) {
|
|
9
|
+
return registry.get(name);
|
|
10
|
+
}
|
|
11
|
+
export function listOperations() {
|
|
12
|
+
return [...registry.values()];
|
|
13
|
+
}
|
|
14
|
+
export function operationNames() {
|
|
15
|
+
return [...registry.keys()];
|
|
16
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getClient } from "../services/notion.js";
|
|
3
|
+
import { register } from "./registry.js";
|
|
4
|
+
import { tryHandler } from "../utils/handler.js";
|
|
5
|
+
import { slimUser, slimList } from "../utils/slim.js";
|
|
6
|
+
import { paginateAll } from "../utils/paginate.js";
|
|
7
|
+
const VERBOSE = z.boolean().optional();
|
|
8
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// list_users
|
|
10
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
11
|
+
const ListUsersParams = z.object({
|
|
12
|
+
start_cursor: z.string().optional(),
|
|
13
|
+
page_size: z.number().min(1).max(100).optional(),
|
|
14
|
+
paginate: z
|
|
15
|
+
.boolean()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Auto-walk all pages and return combined results. Ignores start_cursor when set."),
|
|
18
|
+
page_limit: z
|
|
19
|
+
.number()
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(1000)
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Max pages to fetch when paginate=true (default 10, ~1000 users with page_size=100)."),
|
|
24
|
+
verbose: VERBOSE,
|
|
25
|
+
});
|
|
26
|
+
register({
|
|
27
|
+
name: "list_users",
|
|
28
|
+
description: "List all users in the workspace. Requires the integration to have 'Read user information' capability enabled. Pass paginate:true to auto-walk all pages.",
|
|
29
|
+
batchable: false,
|
|
30
|
+
schema: ListUsersParams,
|
|
31
|
+
example: { page_size: 50 },
|
|
32
|
+
handler: tryHandler(async ({ start_cursor, page_size, paginate, page_limit, verbose, }) => {
|
|
33
|
+
const notion = await getClient();
|
|
34
|
+
if (paginate) {
|
|
35
|
+
const { results, truncated, pages_walked } = await paginateAll(async (cursor) => {
|
|
36
|
+
const r = await notion.users.list({
|
|
37
|
+
start_cursor: cursor,
|
|
38
|
+
page_size: page_size ?? 100,
|
|
39
|
+
});
|
|
40
|
+
return { results: r.results, has_more: r.has_more, next_cursor: r.next_cursor };
|
|
41
|
+
}, { limit: page_limit });
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
data: {
|
|
45
|
+
results: results.map((u) => slimUser(u, verbose ?? false)),
|
|
46
|
+
truncated,
|
|
47
|
+
pages_walked,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const response = await notion.users.list({
|
|
52
|
+
start_cursor,
|
|
53
|
+
page_size: page_size ?? 50,
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
data: slimList(response, slimUser, verbose ?? false),
|
|
58
|
+
};
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
62
|
+
// get_user
|
|
63
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
64
|
+
const GetUserParams = z.object({ user_id: z.string(), verbose: VERBOSE });
|
|
65
|
+
register({
|
|
66
|
+
name: "get_user",
|
|
67
|
+
description: "Get one user by ID. Requires 'Read user information' capability.",
|
|
68
|
+
batchable: true,
|
|
69
|
+
schema: GetUserParams,
|
|
70
|
+
example: { user_id: "<user-id>" },
|
|
71
|
+
handler: tryHandler(async ({ user_id, verbose }) => {
|
|
72
|
+
const notion = await getClient();
|
|
73
|
+
const response = await notion.users.retrieve({ user_id });
|
|
74
|
+
return { ok: true, data: slimUser(response, verbose ?? false) };
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
78
|
+
// get_bot_user
|
|
79
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
80
|
+
const GetBotUserParams = z.object({ verbose: VERBOSE });
|
|
81
|
+
const getBotUserHandler = tryHandler(async ({ verbose }) => {
|
|
82
|
+
const notion = await getClient();
|
|
83
|
+
const response = await notion.users.me({});
|
|
84
|
+
return { ok: true, data: slimUser(response, verbose ?? false) };
|
|
85
|
+
});
|
|
86
|
+
register({
|
|
87
|
+
name: "get_bot_user",
|
|
88
|
+
description: "Get the integration's bot user. Always works without extra capabilities. Alias: get_self.",
|
|
89
|
+
batchable: false,
|
|
90
|
+
schema: GetBotUserParams,
|
|
91
|
+
example: {},
|
|
92
|
+
handler: getBotUserHandler,
|
|
93
|
+
});
|
|
94
|
+
register({
|
|
95
|
+
name: "get_self",
|
|
96
|
+
description: "Alias of get_bot_user. Returns the integration's bot user (i.e. the identity behind the current token).",
|
|
97
|
+
batchable: false,
|
|
98
|
+
schema: GetBotUserParams,
|
|
99
|
+
example: {},
|
|
100
|
+
handler: getBotUserHandler,
|
|
101
|
+
});
|