notion-mcp-server 1.0.1 → 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.
Files changed (99) hide show
  1. package/README.md +383 -192
  2. package/build/config/index.js +3 -1
  3. package/build/dispatch/concurrency.js +15 -0
  4. package/build/dispatch/idempotency.js +38 -0
  5. package/build/dispatch/index.js +175 -0
  6. package/build/dispatch/rate-limit.js +56 -0
  7. package/build/dispatch/retry.js +97 -0
  8. package/build/index.js +1 -1
  9. package/build/markdown/parse.js +265 -0
  10. package/build/operations/blocks.js +331 -0
  11. package/build/operations/comments.js +191 -0
  12. package/build/operations/data-sources.js +85 -0
  13. package/build/operations/databases.js +345 -0
  14. package/build/operations/files.js +239 -0
  15. package/build/operations/index.js +19 -0
  16. package/build/operations/pages.js +486 -0
  17. package/build/operations/registry.js +16 -0
  18. package/build/operations/users.js +101 -0
  19. package/build/prompts/index.js +105 -0
  20. package/build/schema/blocks.js +19 -138
  21. package/build/schema/database.js +27 -111
  22. package/build/schema/emit.js +68 -0
  23. package/build/schema/file.js +1 -1
  24. package/build/schema/filter-dsl.js +333 -0
  25. package/build/schema/icon.js +1 -1
  26. package/build/schema/page-properties.js +17 -3
  27. package/build/schema/page.js +12 -125
  28. package/build/schema/refs.js +16 -0
  29. package/build/schema/rich-text.js +1 -1
  30. package/build/server/index.js +16 -3
  31. package/build/services/auth.js +19 -0
  32. package/build/services/notion.js +14 -17
  33. package/build/tools/index.js +119 -21
  34. package/build/utils/error.js +125 -86
  35. package/build/utils/handler.js +11 -0
  36. package/build/utils/learning-error.js +40 -0
  37. package/build/utils/notion-types.js +16 -0
  38. package/build/utils/paginate.js +35 -0
  39. package/build/utils/schema-slice.js +156 -0
  40. package/build/utils/slim.js +269 -0
  41. package/package.json +13 -7
  42. package/build/resources/imageList.js +0 -62
  43. package/build/resources/index.js +0 -1
  44. package/build/resources/predictionList.js +0 -43
  45. package/build/resources/svgList.js +0 -69
  46. package/build/schema/comments.js +0 -60
  47. package/build/schema/notion.js +0 -57
  48. package/build/schema/richText.js +0 -757
  49. package/build/schema/tools.js +0 -17
  50. package/build/schema/users.js +0 -39
  51. package/build/services/loggs.js +0 -13
  52. package/build/services/replicate.js +0 -23
  53. package/build/tools/appendBlockChildren.js +0 -25
  54. package/build/tools/batchAppendBlockChildren.js +0 -33
  55. package/build/tools/batchDeleteBlocks.js +0 -32
  56. package/build/tools/batchMixedOperations.js +0 -58
  57. package/build/tools/batchUpdateBlocks.js +0 -33
  58. package/build/tools/blocks.js +0 -34
  59. package/build/tools/comments.js +0 -81
  60. package/build/tools/createDatabase.js +0 -18
  61. package/build/tools/createPage.js +0 -18
  62. package/build/tools/createPrediction.js +0 -28
  63. package/build/tools/database.js +0 -16
  64. package/build/tools/deleteBlock.js +0 -24
  65. package/build/tools/formatRichText.js +0 -83
  66. package/build/tools/generateImage.js +0 -48
  67. package/build/tools/generateImageVariants.js +0 -105
  68. package/build/tools/generateMultipleImages.js +0 -60
  69. package/build/tools/generateSVG.js +0 -43
  70. package/build/tools/getPrediction.js +0 -22
  71. package/build/tools/pages.js +0 -22
  72. package/build/tools/predictionList.js +0 -30
  73. package/build/tools/queryDatabase.js +0 -22
  74. package/build/tools/retrieveBlock.js +0 -24
  75. package/build/tools/retrieveBlockChildren.js +0 -32
  76. package/build/tools/searchPage.js +0 -24
  77. package/build/tools/updateBlock.js +0 -25
  78. package/build/tools/updateDatabase.js +0 -18
  79. package/build/tools/updatePage.js +0 -40
  80. package/build/tools/updatePageProperties.js +0 -21
  81. package/build/tools/users.js +0 -75
  82. package/build/types/blocks.js +0 -12
  83. package/build/types/comments.js +0 -7
  84. package/build/types/database.js +0 -6
  85. package/build/types/notion.js +0 -1
  86. package/build/types/page.js +0 -8
  87. package/build/types/richText.js +0 -1
  88. package/build/types/tools.js +0 -1
  89. package/build/types/users.js +0 -6
  90. package/build/utils/blob.js +0 -5
  91. package/build/utils/image.js +0 -34
  92. package/build/utils/index.js +0 -1
  93. package/build/utils/richText.js +0 -174
  94. package/build/validation/blocks.js +0 -568
  95. package/build/validation/notion.js +0 -51
  96. package/build/validation/page.js +0 -262
  97. package/build/validation/richText.js +0 -744
  98. package/build/validation/tools.js +0 -16
  99. /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
+ });