@universal-mcp-toolkit/server-notion 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,1050 @@
1
+ // src/index.ts
2
+ import { pathToFileURL } from "url";
3
+ import {
4
+ ConfigurationError,
5
+ ExternalServiceError,
6
+ ValidationError,
7
+ createServerCard,
8
+ defineTool,
9
+ loadEnv,
10
+ normalizeError,
11
+ parseRuntimeOptions,
12
+ runToolkitServer,
13
+ ToolkitServer
14
+ } from "@universal-mcp-toolkit/core";
15
+ import { z } from "zod";
16
+ var DEFAULT_NOTION_API_BASE_URL = "https://api.notion.com/v1";
17
+ var DEFAULT_NOTION_API_VERSION = "2026-03-11";
18
+ var WORKSPACE_RESOURCE_URI = "notion://workspace";
19
+ var notionToolNames = ["search-pages", "get-page", "create-page"];
20
+ var notionResourceNames = ["workspace"];
21
+ var notionPromptNames = ["summarize-doc"];
22
+ var notionEnvShape = {
23
+ NOTION_TOKEN: z.string().trim().min(1),
24
+ NOTION_DEFAULT_PARENT_PAGE_ID: z.string().trim().min(1).optional(),
25
+ NOTION_WORKSPACE_NAME: z.string().trim().min(1).optional(),
26
+ NOTION_API_BASE_URL: z.string().url().default(DEFAULT_NOTION_API_BASE_URL),
27
+ NOTION_API_VERSION: z.string().trim().min(1).default(DEFAULT_NOTION_API_VERSION)
28
+ };
29
+ var pageParentShape = {
30
+ type: z.string(),
31
+ pageId: z.string().nullable(),
32
+ databaseId: z.string().nullable(),
33
+ dataSourceId: z.string().nullable(),
34
+ workspace: z.boolean()
35
+ };
36
+ var pageSummaryShape = {
37
+ id: z.string(),
38
+ title: z.string(),
39
+ url: z.string().url(),
40
+ publicUrl: z.string().url().nullable(),
41
+ createdTime: z.string(),
42
+ lastEditedTime: z.string(),
43
+ archived: z.boolean(),
44
+ inTrash: z.boolean(),
45
+ isLocked: z.boolean(),
46
+ parent: z.object(pageParentShape)
47
+ };
48
+ var pagePropertyShape = {
49
+ name: z.string(),
50
+ type: z.string(),
51
+ valuePreview: z.string().nullable()
52
+ };
53
+ var pageContentBlockShape = {
54
+ id: z.string(),
55
+ type: z.string(),
56
+ text: z.string(),
57
+ hasChildren: z.boolean()
58
+ };
59
+ var pageDetailShape = {
60
+ ...pageSummaryShape,
61
+ properties: z.array(z.object(pagePropertyShape)),
62
+ propertyCount: z.number().int().nonnegative(),
63
+ contentBlocks: z.array(z.object(pageContentBlockShape)),
64
+ contentPreview: z.string(),
65
+ hasMoreContent: z.boolean(),
66
+ nextCursor: z.string().nullable()
67
+ };
68
+ var workspaceResourceShape = {
69
+ workspaceName: z.string().nullable(),
70
+ integration: z.object({
71
+ id: z.string(),
72
+ name: z.string().nullable(),
73
+ type: z.enum(["bot", "person"]),
74
+ avatarUrl: z.string().nullable()
75
+ }),
76
+ apiBaseUrl: z.string().url(),
77
+ apiVersion: z.string(),
78
+ defaultParentPageId: z.string().nullable(),
79
+ recentPages: z.array(z.object(pageSummaryShape))
80
+ };
81
+ var searchPagesInputShape = {
82
+ query: z.string().trim().max(200).default(""),
83
+ cursor: z.string().trim().min(1).optional(),
84
+ limit: z.number().int().min(1).max(50).default(10),
85
+ sortDirection: z.enum(["ascending", "descending"]).default("descending")
86
+ };
87
+ var searchPagesOutputShape = {
88
+ query: z.string(),
89
+ resultCount: z.number().int().nonnegative(),
90
+ nextCursor: z.string().nullable(),
91
+ hasMore: z.boolean(),
92
+ results: z.array(z.object(pageSummaryShape))
93
+ };
94
+ var getPageInputShape = {
95
+ pageId: z.string().trim().min(1).max(500),
96
+ includeContent: z.boolean().default(true),
97
+ contentLimit: z.number().int().min(1).max(100).default(10),
98
+ cursor: z.string().trim().min(1).optional()
99
+ };
100
+ var getPageOutputShape = {
101
+ page: z.object(pageDetailShape)
102
+ };
103
+ var createPageInputShape = {
104
+ title: z.string().trim().min(1).max(200),
105
+ parentPageId: z.string().trim().min(1).max(500).optional(),
106
+ content: z.string().trim().min(1).max(2e4).optional()
107
+ };
108
+ var createPageOutputShape = {
109
+ page: z.object(pageSummaryShape),
110
+ usedParentPageId: z.string(),
111
+ contentBlockCount: z.number().int().nonnegative()
112
+ };
113
+ var summarizeDocPromptArgsShape = {
114
+ pageId: z.string().trim().min(1).max(500),
115
+ audience: z.string().trim().min(1).max(120).default("a general audience"),
116
+ focus: z.string().trim().min(1).max(240).default("key ideas, decisions, action items, and risks"),
117
+ contentLimit: z.number().int().min(1).max(20).default(8)
118
+ };
119
+ var notionRichTextSchema = z.object({
120
+ plain_text: z.string().default("")
121
+ }).passthrough();
122
+ var notionDateSchema = z.object({
123
+ start: z.string(),
124
+ end: z.string().nullable().optional()
125
+ }).passthrough();
126
+ var notionParentSchema = z.object({
127
+ type: z.string(),
128
+ page_id: z.string().optional(),
129
+ database_id: z.string().optional(),
130
+ data_source_id: z.string().optional(),
131
+ workspace: z.boolean().optional()
132
+ }).passthrough();
133
+ var notionPropertySchema = z.object({
134
+ type: z.string(),
135
+ title: z.array(notionRichTextSchema).optional(),
136
+ rich_text: z.array(notionRichTextSchema).optional(),
137
+ select: z.object({
138
+ name: z.string().nullable().optional()
139
+ }).passthrough().nullable().optional(),
140
+ multi_select: z.array(
141
+ z.object({
142
+ name: z.string()
143
+ }).passthrough()
144
+ ).optional(),
145
+ status: z.object({
146
+ name: z.string().nullable().optional()
147
+ }).passthrough().nullable().optional(),
148
+ checkbox: z.boolean().optional(),
149
+ number: z.number().nullable().optional(),
150
+ url: z.string().nullable().optional(),
151
+ email: z.string().nullable().optional(),
152
+ phone_number: z.string().nullable().optional(),
153
+ date: notionDateSchema.nullable().optional(),
154
+ people: z.array(
155
+ z.object({
156
+ id: z.string(),
157
+ name: z.string().nullable().optional()
158
+ }).passthrough()
159
+ ).optional(),
160
+ relation: z.array(
161
+ z.object({
162
+ id: z.string()
163
+ }).passthrough()
164
+ ).optional(),
165
+ formula: z.object({
166
+ type: z.string(),
167
+ string: z.string().nullable().optional(),
168
+ number: z.number().nullable().optional(),
169
+ boolean: z.boolean().nullable().optional(),
170
+ date: notionDateSchema.nullable().optional()
171
+ }).passthrough().optional(),
172
+ created_time: z.string().optional(),
173
+ last_edited_time: z.string().optional()
174
+ }).passthrough();
175
+ var notionPageSchema = z.object({
176
+ object: z.literal("page").optional(),
177
+ id: z.string(),
178
+ url: z.string().url(),
179
+ public_url: z.string().url().nullable().optional(),
180
+ created_time: z.string(),
181
+ last_edited_time: z.string(),
182
+ archived: z.boolean().optional(),
183
+ in_trash: z.boolean().optional(),
184
+ is_locked: z.boolean().optional(),
185
+ parent: notionParentSchema,
186
+ properties: z.record(z.string(), notionPropertySchema)
187
+ }).passthrough();
188
+ var notionSearchResponseSchema = z.object({
189
+ results: z.array(notionPageSchema),
190
+ next_cursor: z.string().nullable(),
191
+ has_more: z.boolean()
192
+ }).passthrough();
193
+ var notionBlockTextSchema = z.object({
194
+ rich_text: z.array(notionRichTextSchema).default([])
195
+ }).passthrough();
196
+ var notionTitleOnlyBlockSchema = z.object({
197
+ title: z.string().optional()
198
+ }).passthrough();
199
+ var notionBlockSchema = z.object({
200
+ id: z.string(),
201
+ type: z.string(),
202
+ has_children: z.boolean().optional(),
203
+ paragraph: notionBlockTextSchema.optional(),
204
+ heading_1: notionBlockTextSchema.optional(),
205
+ heading_2: notionBlockTextSchema.optional(),
206
+ heading_3: notionBlockTextSchema.optional(),
207
+ bulleted_list_item: notionBlockTextSchema.optional(),
208
+ numbered_list_item: notionBlockTextSchema.optional(),
209
+ quote: notionBlockTextSchema.optional(),
210
+ to_do: notionBlockTextSchema.optional(),
211
+ toggle: notionBlockTextSchema.optional(),
212
+ callout: notionBlockTextSchema.optional(),
213
+ code: notionBlockTextSchema.optional(),
214
+ child_page: notionTitleOnlyBlockSchema.optional(),
215
+ child_database: notionTitleOnlyBlockSchema.optional()
216
+ }).passthrough();
217
+ var notionBlockListResponseSchema = z.object({
218
+ results: z.array(notionBlockSchema),
219
+ next_cursor: z.string().nullable(),
220
+ has_more: z.boolean()
221
+ }).passthrough();
222
+ var notionUserSchema = z.object({
223
+ id: z.string(),
224
+ name: z.string().nullable(),
225
+ avatar_url: z.string().nullable().optional(),
226
+ type: z.enum(["bot", "person"]),
227
+ bot: z.object({
228
+ workspace_name: z.string().nullable().optional()
229
+ }).passthrough().optional()
230
+ }).passthrough();
231
+ var notionErrorResponseSchema = z.object({
232
+ code: z.string().optional(),
233
+ message: z.string().optional(),
234
+ request_id: z.string().optional(),
235
+ additional_data: z.unknown().optional()
236
+ }).passthrough();
237
+ var searchPagesInputSchema = z.object(searchPagesInputShape);
238
+ var searchPagesOutputSchema = z.object(searchPagesOutputShape);
239
+ var getPageInputSchema = z.object(getPageInputShape);
240
+ var getPageOutputSchema = z.object(getPageOutputShape);
241
+ var createPageInputSchema = z.object(createPageInputShape);
242
+ var createPageOutputSchema = z.object(createPageOutputShape);
243
+ var summarizeDocPromptArgsSchema = z.object(summarizeDocPromptArgsShape);
244
+ var workspaceResourceSchema = z.object(workspaceResourceShape);
245
+ var pageDetailSchema = z.object(pageDetailShape);
246
+ function loadNotionConfig(source = process.env) {
247
+ const env = loadEnv(notionEnvShape, source);
248
+ return {
249
+ token: env.NOTION_TOKEN,
250
+ defaultParentPageId: env.NOTION_DEFAULT_PARENT_PAGE_ID ?? null,
251
+ workspaceName: env.NOTION_WORKSPACE_NAME ?? null,
252
+ apiBaseUrl: env.NOTION_API_BASE_URL,
253
+ apiVersion: env.NOTION_API_VERSION
254
+ };
255
+ }
256
+ function ensureRegisteredNames(kind, expected, actual) {
257
+ const expectedSorted = [...expected].sort();
258
+ const actualSorted = [...actual].sort();
259
+ const matches = expectedSorted.length === actualSorted.length && expectedSorted.every((name, index) => name === actualSorted[index]);
260
+ if (!matches) {
261
+ throw new ConfigurationError(
262
+ `Metadata ${kind} names do not match the registered ${kind} names.`,
263
+ {
264
+ expected: expectedSorted,
265
+ actual: actualSorted
266
+ }
267
+ );
268
+ }
269
+ }
270
+ function buildCanonicalNotionId(compactHex) {
271
+ return [
272
+ compactHex.slice(0, 8),
273
+ compactHex.slice(8, 12),
274
+ compactHex.slice(12, 16),
275
+ compactHex.slice(16, 20),
276
+ compactHex.slice(20)
277
+ ].join("-").toLowerCase();
278
+ }
279
+ function normalizeNotionId(value) {
280
+ const trimmed = value.trim();
281
+ let candidate = trimmed;
282
+ try {
283
+ candidate = decodeURIComponent(new URL(trimmed).pathname);
284
+ } catch {
285
+ candidate = trimmed;
286
+ }
287
+ const matches = candidate.match(/[0-9a-fA-F]{32}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g);
288
+ const matchedId = matches?.at(-1);
289
+ if (!matchedId) {
290
+ throw new ValidationError(`Expected a Notion page ID or URL, received '${value}'.`);
291
+ }
292
+ const compact = matchedId.replace(/-/g, "");
293
+ if (!/^[0-9a-fA-F]{32}$/.test(compact)) {
294
+ throw new ValidationError(`Expected a valid Notion page ID or URL, received '${value}'.`);
295
+ }
296
+ return buildCanonicalNotionId(compact);
297
+ }
298
+ function richTextToPlainText(items) {
299
+ return items.map((item) => item.plain_text).join("").trim();
300
+ }
301
+ function formatDateValue(dateValue) {
302
+ if (!dateValue) {
303
+ return null;
304
+ }
305
+ return dateValue.end ? `${dateValue.start} \u2192 ${dateValue.end}` : dateValue.start;
306
+ }
307
+ function summarizeFormulaValue(formula) {
308
+ if (!formula) {
309
+ return null;
310
+ }
311
+ switch (formula.type) {
312
+ case "string":
313
+ return formula.string ?? null;
314
+ case "number":
315
+ return formula.number === null || formula.number === void 0 ? null : String(formula.number);
316
+ case "boolean":
317
+ return formula.boolean === null || formula.boolean === void 0 ? null : String(formula.boolean);
318
+ case "date":
319
+ return formatDateValue(formula.date);
320
+ default:
321
+ return null;
322
+ }
323
+ }
324
+ function summarizePropertyValue(property) {
325
+ switch (property.type) {
326
+ case "title":
327
+ return richTextToPlainText(property.title ?? []) || null;
328
+ case "rich_text":
329
+ return richTextToPlainText(property.rich_text ?? []) || null;
330
+ case "select":
331
+ return property.select?.name ?? null;
332
+ case "multi_select":
333
+ return property.multi_select && property.multi_select.length > 0 ? property.multi_select.map((item) => item.name).join(", ") : null;
334
+ case "status":
335
+ return property.status?.name ?? null;
336
+ case "checkbox":
337
+ return property.checkbox === void 0 ? null : String(property.checkbox);
338
+ case "number":
339
+ return property.number === null || property.number === void 0 ? null : String(property.number);
340
+ case "url":
341
+ return property.url ?? null;
342
+ case "email":
343
+ return property.email ?? null;
344
+ case "phone_number":
345
+ return property.phone_number ?? null;
346
+ case "date":
347
+ return formatDateValue(property.date);
348
+ case "people":
349
+ return property.people && property.people.length > 0 ? property.people.map((person) => person.name ?? person.id).join(", ") : null;
350
+ case "relation":
351
+ return property.relation && property.relation.length > 0 ? property.relation.map((item) => item.id).join(", ") : null;
352
+ case "formula":
353
+ return summarizeFormulaValue(property.formula);
354
+ case "created_time":
355
+ return property.created_time ?? null;
356
+ case "last_edited_time":
357
+ return property.last_edited_time ?? null;
358
+ default:
359
+ return null;
360
+ }
361
+ }
362
+ function extractPageTitle(properties) {
363
+ for (const property of Object.values(properties)) {
364
+ if (property.type !== "title") {
365
+ continue;
366
+ }
367
+ const title = richTextToPlainText(property.title ?? []);
368
+ if (title.length > 0) {
369
+ return title;
370
+ }
371
+ }
372
+ return "Untitled";
373
+ }
374
+ function normalizeParent(parent) {
375
+ return {
376
+ type: parent.type,
377
+ pageId: parent.page_id ?? null,
378
+ databaseId: parent.database_id ?? null,
379
+ dataSourceId: parent.data_source_id ?? null,
380
+ workspace: parent.workspace ?? false
381
+ };
382
+ }
383
+ function normalizePageSummary(page) {
384
+ return {
385
+ id: page.id,
386
+ title: extractPageTitle(page.properties),
387
+ url: page.url,
388
+ publicUrl: page.public_url ?? null,
389
+ createdTime: page.created_time,
390
+ lastEditedTime: page.last_edited_time,
391
+ archived: page.archived ?? page.in_trash ?? false,
392
+ inTrash: page.in_trash ?? page.archived ?? false,
393
+ isLocked: page.is_locked ?? false,
394
+ parent: normalizeParent(page.parent)
395
+ };
396
+ }
397
+ function extractBlockText(block) {
398
+ switch (block.type) {
399
+ case "paragraph":
400
+ return richTextToPlainText(block.paragraph?.rich_text ?? []);
401
+ case "heading_1":
402
+ return richTextToPlainText(block.heading_1?.rich_text ?? []);
403
+ case "heading_2":
404
+ return richTextToPlainText(block.heading_2?.rich_text ?? []);
405
+ case "heading_3":
406
+ return richTextToPlainText(block.heading_3?.rich_text ?? []);
407
+ case "bulleted_list_item":
408
+ return richTextToPlainText(block.bulleted_list_item?.rich_text ?? []);
409
+ case "numbered_list_item":
410
+ return richTextToPlainText(block.numbered_list_item?.rich_text ?? []);
411
+ case "quote":
412
+ return richTextToPlainText(block.quote?.rich_text ?? []);
413
+ case "to_do":
414
+ return richTextToPlainText(block.to_do?.rich_text ?? []);
415
+ case "toggle":
416
+ return richTextToPlainText(block.toggle?.rich_text ?? []);
417
+ case "callout":
418
+ return richTextToPlainText(block.callout?.rich_text ?? []);
419
+ case "code":
420
+ return richTextToPlainText(block.code?.rich_text ?? []);
421
+ case "child_page":
422
+ return block.child_page?.title ?? "";
423
+ case "child_database":
424
+ return block.child_database?.title ?? "";
425
+ default:
426
+ return "";
427
+ }
428
+ }
429
+ function normalizeContentBlocks(blocks) {
430
+ return blocks.map((block) => ({
431
+ id: block.id,
432
+ type: block.type,
433
+ text: extractBlockText(block),
434
+ hasChildren: block.has_children ?? false
435
+ }));
436
+ }
437
+ function buildContentPreview(blocks) {
438
+ return blocks.map((block) => block.text).filter((text) => text.length > 0).join("\n").slice(0, 4e3);
439
+ }
440
+ function chunkParagraph(paragraph, maxLength = 1800) {
441
+ if (paragraph.length <= maxLength) {
442
+ return [paragraph];
443
+ }
444
+ const chunks = [];
445
+ let remaining = paragraph;
446
+ while (remaining.length > maxLength) {
447
+ let breakIndex = remaining.lastIndexOf(" ", maxLength);
448
+ if (breakIndex <= 0) {
449
+ breakIndex = maxLength;
450
+ }
451
+ chunks.push(remaining.slice(0, breakIndex).trim());
452
+ remaining = remaining.slice(breakIndex).trimStart();
453
+ }
454
+ if (remaining.length > 0) {
455
+ chunks.push(remaining);
456
+ }
457
+ return chunks;
458
+ }
459
+ function buildParagraphBlocks(content) {
460
+ if (!content) {
461
+ return [];
462
+ }
463
+ const paragraphs = content.split(/\r?\n\s*\r?\n/g).map((paragraph) => paragraph.replace(/\r?\n/g, " ").trim()).filter((paragraph) => paragraph.length > 0);
464
+ const blocks = [];
465
+ for (const paragraph of paragraphs) {
466
+ for (const chunk of chunkParagraph(paragraph)) {
467
+ if (blocks.length >= 100) {
468
+ return blocks;
469
+ }
470
+ blocks.push({
471
+ object: "block",
472
+ type: "paragraph",
473
+ paragraph: {
474
+ rich_text: [
475
+ {
476
+ type: "text",
477
+ text: {
478
+ content: chunk
479
+ }
480
+ }
481
+ ]
482
+ }
483
+ });
484
+ }
485
+ }
486
+ return blocks;
487
+ }
488
+ function formatSearchPagesText(output) {
489
+ if (output.results.length === 0) {
490
+ return output.query.length > 0 ? `No Notion pages matched '${output.query}'.` : "No Notion pages are currently visible to the integration.";
491
+ }
492
+ return [
493
+ `Found ${output.resultCount} Notion page${output.resultCount === 1 ? "" : "s"}${output.query ? ` for '${output.query}'` : ""}.`,
494
+ ...output.results.slice(0, 5).map((page, index) => `${index + 1}. ${page.title} (${page.id})`)
495
+ ].join("\n");
496
+ }
497
+ function formatPageDetailText(page) {
498
+ const preview = page.contentPreview.length > 0 ? `
499
+
500
+ Preview:
501
+ ${page.contentPreview}` : "";
502
+ return `${page.title}
503
+ ${page.url}${preview}`;
504
+ }
505
+ function formatCreatePageText(output) {
506
+ return `Created Notion page '${output.page.title}' (${output.page.url}) under parent ${output.usedParentPageId}.`;
507
+ }
508
+ function buildOperationError(action, error) {
509
+ if (error instanceof ConfigurationError || error instanceof ExternalServiceError || error instanceof ValidationError) {
510
+ return error;
511
+ }
512
+ const normalized = normalizeError(error);
513
+ return new ExternalServiceError(`Failed to ${action}. ${normalized.toClientMessage()}`, {
514
+ statusCode: normalized.statusCode,
515
+ details: normalized.details
516
+ });
517
+ }
518
+ function messageWithDetails(prefix, details) {
519
+ return details.message ? `${prefix} ${details.message}` : prefix;
520
+ }
521
+ async function parseErrorBody(response) {
522
+ const rawBody = await response.text();
523
+ if (rawBody.length === 0) {
524
+ return {
525
+ code: null,
526
+ message: null,
527
+ requestId: response.headers.get("x-request-id"),
528
+ additionalData: null,
529
+ rawBody: null
530
+ };
531
+ }
532
+ const parsedJson = (() => {
533
+ try {
534
+ return JSON.parse(rawBody);
535
+ } catch {
536
+ return null;
537
+ }
538
+ })();
539
+ const parsed = notionErrorResponseSchema.safeParse(parsedJson);
540
+ if (!parsed.success) {
541
+ return {
542
+ code: null,
543
+ message: rawBody.trim() || null,
544
+ requestId: response.headers.get("x-request-id"),
545
+ additionalData: null,
546
+ rawBody
547
+ };
548
+ }
549
+ return {
550
+ code: parsed.data.code ?? null,
551
+ message: parsed.data.message?.trim() || null,
552
+ requestId: parsed.data.request_id ?? response.headers.get("x-request-id"),
553
+ additionalData: parsed.data.additional_data ?? null,
554
+ rawBody
555
+ };
556
+ }
557
+ async function mapHttpError(response) {
558
+ const details = await parseErrorBody(response);
559
+ const errorDetails = {
560
+ status: response.status,
561
+ code: details.code,
562
+ requestId: details.requestId,
563
+ retryAfter: response.headers.get("retry-after"),
564
+ additionalData: details.additionalData,
565
+ rawBody: details.rawBody
566
+ };
567
+ switch (response.status) {
568
+ case 400:
569
+ return new ValidationError(
570
+ messageWithDetails(
571
+ "Notion rejected the request. Check the supplied identifiers, filters, and page payload.",
572
+ details
573
+ ),
574
+ errorDetails
575
+ );
576
+ case 401:
577
+ return new ExternalServiceError(
578
+ messageWithDetails("Notion authentication failed. Check NOTION_TOKEN.", details),
579
+ {
580
+ statusCode: 401,
581
+ details: errorDetails
582
+ }
583
+ );
584
+ case 403:
585
+ return new ExternalServiceError(
586
+ messageWithDetails(
587
+ "The Notion integration does not have access to this resource. Share it with the integration and try again.",
588
+ details
589
+ ),
590
+ {
591
+ statusCode: 403,
592
+ details: errorDetails
593
+ }
594
+ );
595
+ case 404:
596
+ return new ExternalServiceError(
597
+ messageWithDetails(
598
+ "Notion could not find the requested resource, or the integration cannot access it.",
599
+ details
600
+ ),
601
+ {
602
+ statusCode: 404,
603
+ details: errorDetails
604
+ }
605
+ );
606
+ case 409:
607
+ return new ExternalServiceError(
608
+ messageWithDetails("Notion reported a conflict while saving the request. Retry with fresh data.", details),
609
+ {
610
+ statusCode: 409,
611
+ details: errorDetails
612
+ }
613
+ );
614
+ case 429:
615
+ return new ExternalServiceError(
616
+ messageWithDetails("Notion rate limited the request. Slow down and retry shortly.", details),
617
+ {
618
+ statusCode: 429,
619
+ details: errorDetails
620
+ }
621
+ );
622
+ case 500:
623
+ case 502:
624
+ case 503:
625
+ case 504:
626
+ return new ExternalServiceError(
627
+ messageWithDetails("Notion is temporarily unavailable. Please retry in a moment.", details),
628
+ {
629
+ statusCode: response.status,
630
+ details: errorDetails
631
+ }
632
+ );
633
+ default:
634
+ return new ExternalServiceError(
635
+ messageWithDetails(`Notion request failed with status ${response.status}.`, details),
636
+ {
637
+ statusCode: response.status,
638
+ details: errorDetails
639
+ }
640
+ );
641
+ }
642
+ }
643
+ var NotionApiClient = class {
644
+ config;
645
+ fetchImplementation;
646
+ baseUrl;
647
+ constructor(options) {
648
+ this.config = options.config;
649
+ this.fetchImplementation = options.fetchImplementation ?? fetch;
650
+ this.baseUrl = options.config.apiBaseUrl.replace(/\/+$/, "");
651
+ }
652
+ async searchPages(input) {
653
+ const response = await this.requestJson({
654
+ method: "POST",
655
+ path: "/search",
656
+ body: {
657
+ query: input.query.length > 0 ? input.query : void 0,
658
+ filter: {
659
+ property: "object",
660
+ value: "page"
661
+ },
662
+ sort: {
663
+ timestamp: "last_edited_time",
664
+ direction: input.sortDirection
665
+ },
666
+ page_size: input.limit,
667
+ start_cursor: input.cursor
668
+ },
669
+ schema: notionSearchResponseSchema
670
+ });
671
+ const results = response.results.map((page) => normalizePageSummary(page));
672
+ return {
673
+ query: input.query,
674
+ resultCount: results.length,
675
+ nextCursor: response.next_cursor,
676
+ hasMore: response.has_more,
677
+ results
678
+ };
679
+ }
680
+ async getPage(input) {
681
+ const page = await this.requestJson({
682
+ method: "GET",
683
+ path: `/pages/${encodeURIComponent(input.pageId)}`,
684
+ schema: notionPageSchema
685
+ });
686
+ let contentBlocks = [];
687
+ let hasMoreContent = false;
688
+ let nextCursor = null;
689
+ if (input.includeContent) {
690
+ const blocks = await this.requestJson({
691
+ method: "GET",
692
+ path: `/blocks/${encodeURIComponent(input.pageId)}/children`,
693
+ query: {
694
+ page_size: input.contentLimit,
695
+ start_cursor: input.cursor
696
+ },
697
+ schema: notionBlockListResponseSchema
698
+ });
699
+ contentBlocks = normalizeContentBlocks(blocks.results);
700
+ hasMoreContent = blocks.has_more;
701
+ nextCursor = blocks.next_cursor;
702
+ }
703
+ const properties = Object.entries(page.properties).map(([name, property]) => ({
704
+ name,
705
+ type: property.type,
706
+ valuePreview: summarizePropertyValue(property)
707
+ }));
708
+ return {
709
+ ...normalizePageSummary(page),
710
+ properties,
711
+ propertyCount: properties.length,
712
+ contentBlocks,
713
+ contentPreview: buildContentPreview(contentBlocks),
714
+ hasMoreContent,
715
+ nextCursor
716
+ };
717
+ }
718
+ async createPage(input) {
719
+ const children = buildParagraphBlocks(input.content);
720
+ const page = await this.requestJson({
721
+ method: "POST",
722
+ path: "/pages",
723
+ body: {
724
+ parent: {
725
+ type: "page_id",
726
+ page_id: input.parentPageId
727
+ },
728
+ properties: {
729
+ title: {
730
+ type: "title",
731
+ title: [
732
+ {
733
+ type: "text",
734
+ text: {
735
+ content: input.title
736
+ }
737
+ }
738
+ ]
739
+ }
740
+ },
741
+ children: children.length > 0 ? children : void 0
742
+ },
743
+ schema: notionPageSchema
744
+ });
745
+ return {
746
+ page: normalizePageSummary(page),
747
+ usedParentPageId: input.parentPageId,
748
+ contentBlockCount: children.length
749
+ };
750
+ }
751
+ async getWorkspace() {
752
+ const user = await this.requestJson({
753
+ method: "GET",
754
+ path: "/users/me",
755
+ schema: notionUserSchema
756
+ });
757
+ const recentPages = await this.searchPages({
758
+ query: "",
759
+ limit: 5,
760
+ sortDirection: "descending"
761
+ });
762
+ return {
763
+ workspaceName: this.config.workspaceName ?? user.bot?.workspace_name ?? null,
764
+ integration: {
765
+ id: user.id,
766
+ name: user.name ?? null,
767
+ type: user.type,
768
+ avatarUrl: user.avatar_url ?? null
769
+ },
770
+ apiBaseUrl: this.config.apiBaseUrl,
771
+ apiVersion: this.config.apiVersion,
772
+ defaultParentPageId: this.config.defaultParentPageId,
773
+ recentPages: recentPages.results
774
+ };
775
+ }
776
+ buildUrl(path, query) {
777
+ const url = new URL(path.startsWith("http") ? path : `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`);
778
+ for (const [key, value] of Object.entries(query ?? {})) {
779
+ if (value !== void 0) {
780
+ url.searchParams.set(key, String(value));
781
+ }
782
+ }
783
+ return url;
784
+ }
785
+ async requestJson(options) {
786
+ const url = this.buildUrl(options.path, options.query);
787
+ const headers = new Headers({
788
+ Authorization: `Bearer ${this.config.token}`,
789
+ Accept: "application/json",
790
+ "Notion-Version": this.config.apiVersion
791
+ });
792
+ const requestInit = {
793
+ method: options.method,
794
+ headers
795
+ };
796
+ if (options.body !== void 0) {
797
+ headers.set("content-type", "application/json");
798
+ requestInit.body = JSON.stringify(options.body);
799
+ }
800
+ let response;
801
+ try {
802
+ response = await this.fetchImplementation(url.toString(), requestInit);
803
+ } catch (error) {
804
+ throw new ExternalServiceError("Failed to reach the Notion API. Check connectivity and NOTION_API_BASE_URL.", {
805
+ details: error instanceof Error ? error.message : error
806
+ });
807
+ }
808
+ if (!response.ok) {
809
+ throw await mapHttpError(response);
810
+ }
811
+ let payload;
812
+ try {
813
+ payload = await response.json();
814
+ } catch (error) {
815
+ throw new ExternalServiceError("Notion returned malformed JSON.", {
816
+ details: error instanceof Error ? error.message : error
817
+ });
818
+ }
819
+ const parsed = options.schema.safeParse(payload);
820
+ if (!parsed.success) {
821
+ throw new ExternalServiceError("Notion returned a response that failed schema validation.", {
822
+ details: parsed.error.flatten()
823
+ });
824
+ }
825
+ return parsed.data;
826
+ }
827
+ };
828
+ var metadata = {
829
+ id: "notion",
830
+ title: "Notion MCP Server",
831
+ description: "Search Notion pages, inspect page metadata and content, create new pages, expose workspace context, and generate summary prompts.",
832
+ version: "0.1.0",
833
+ packageName: "@universal-mcp-toolkit/server-notion",
834
+ homepage: "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit#readme",
835
+ repositoryUrl: "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit",
836
+ documentationUrl: "https://developers.notion.com/reference/intro",
837
+ envVarNames: ["NOTION_TOKEN"],
838
+ transports: ["stdio", "sse"],
839
+ toolNames: notionToolNames,
840
+ resourceNames: notionResourceNames,
841
+ promptNames: notionPromptNames
842
+ };
843
+ var serverCard = createServerCard(metadata);
844
+ var NotionServer = class extends ToolkitServer {
845
+ config;
846
+ client;
847
+ constructor(dependencies) {
848
+ super(metadata);
849
+ this.config = dependencies.config;
850
+ this.client = dependencies.client;
851
+ this.registerTool(
852
+ defineTool({
853
+ name: "search-pages",
854
+ title: "Search pages",
855
+ description: "Search Notion pages by title and indexed content with cursor-based pagination.",
856
+ annotations: {
857
+ title: "Search Notion pages",
858
+ readOnlyHint: true,
859
+ destructiveHint: false,
860
+ idempotentHint: true,
861
+ openWorldHint: true
862
+ },
863
+ inputSchema: searchPagesInputShape,
864
+ outputSchema: searchPagesOutputShape,
865
+ handler: async (input, context) => {
866
+ await context.log("debug", `Searching Notion pages for '${input.query}'.`);
867
+ try {
868
+ return await this.client.searchPages(input);
869
+ } catch (error) {
870
+ throw buildOperationError(`search Notion pages for '${input.query}'`, error);
871
+ }
872
+ },
873
+ renderText: formatSearchPagesText
874
+ })
875
+ );
876
+ this.registerTool(
877
+ defineTool({
878
+ name: "get-page",
879
+ title: "Get page",
880
+ description: "Retrieve a Notion page, its properties, and an optional excerpt of top-level content blocks.",
881
+ annotations: {
882
+ title: "Get a Notion page",
883
+ readOnlyHint: true,
884
+ destructiveHint: false,
885
+ idempotentHint: true,
886
+ openWorldHint: true
887
+ },
888
+ inputSchema: getPageInputShape,
889
+ outputSchema: getPageOutputShape,
890
+ handler: async (input, context) => {
891
+ const normalizedInput = {
892
+ ...input,
893
+ pageId: normalizeNotionId(input.pageId)
894
+ };
895
+ await context.log("debug", `Fetching Notion page ${normalizedInput.pageId}.`);
896
+ try {
897
+ return {
898
+ page: await this.client.getPage(normalizedInput)
899
+ };
900
+ } catch (error) {
901
+ throw buildOperationError(`fetch Notion page ${normalizedInput.pageId}`, error);
902
+ }
903
+ },
904
+ renderText: (output) => formatPageDetailText(output.page)
905
+ })
906
+ );
907
+ this.registerTool(
908
+ defineTool({
909
+ name: "create-page",
910
+ title: "Create page",
911
+ description: "Create a child page under a Notion parent page using env-based authentication and optional default parent fallback.",
912
+ annotations: {
913
+ title: "Create a Notion page",
914
+ readOnlyHint: false,
915
+ destructiveHint: false,
916
+ idempotentHint: false,
917
+ openWorldHint: true
918
+ },
919
+ inputSchema: createPageInputShape,
920
+ outputSchema: createPageOutputShape,
921
+ handler: async (input, context) => {
922
+ const parentPageId = input.parentPageId ? normalizeNotionId(input.parentPageId) : this.config.defaultParentPageId;
923
+ if (!parentPageId) {
924
+ throw new ValidationError(
925
+ "Provide parentPageId or configure NOTION_DEFAULT_PARENT_PAGE_ID before creating a Notion page."
926
+ );
927
+ }
928
+ await context.log("debug", `Creating Notion page '${input.title}' under ${parentPageId}.`);
929
+ try {
930
+ const createRequest = input.content ? {
931
+ title: input.title,
932
+ parentPageId,
933
+ content: input.content
934
+ } : {
935
+ title: input.title,
936
+ parentPageId
937
+ };
938
+ return await this.client.createPage(createRequest);
939
+ } catch (error) {
940
+ throw buildOperationError(`create Notion page '${input.title}'`, error);
941
+ }
942
+ },
943
+ renderText: formatCreatePageText
944
+ })
945
+ );
946
+ this.registerStaticResource(
947
+ "workspace",
948
+ WORKSPACE_RESOURCE_URI,
949
+ {
950
+ title: "Notion workspace",
951
+ description: "Workspace context, integration identity, configuration, and a sample of recent pages.",
952
+ mimeType: "application/json"
953
+ },
954
+ async (uri) => this.readWorkspaceResource(uri)
955
+ );
956
+ this.registerPrompt(
957
+ "summarize-doc",
958
+ {
959
+ title: "Summarize document",
960
+ description: "Build a summarization prompt around a specific Notion page and its latest content excerpt.",
961
+ argsSchema: summarizeDocPromptArgsShape
962
+ },
963
+ async (args) => this.buildSummarizeDocPrompt(args)
964
+ );
965
+ ensureRegisteredNames("tool", metadata.toolNames, this.getToolNames());
966
+ ensureRegisteredNames("resource", metadata.resourceNames, this.getResourceNames());
967
+ ensureRegisteredNames("prompt", metadata.promptNames, this.getPromptNames());
968
+ }
969
+ async readWorkspaceResource(uri = new URL(WORKSPACE_RESOURCE_URI)) {
970
+ try {
971
+ const payload = workspaceResourceSchema.parse(await this.client.getWorkspace());
972
+ return this.createJsonResource(uri.toString(), payload);
973
+ } catch (error) {
974
+ throw buildOperationError("load Notion workspace context", error);
975
+ }
976
+ }
977
+ async buildSummarizeDocPrompt(args) {
978
+ const parsedArgs = summarizeDocPromptArgsSchema.parse(args);
979
+ const normalizedPageId = normalizeNotionId(parsedArgs.pageId);
980
+ try {
981
+ const page = await this.client.getPage({
982
+ pageId: normalizedPageId,
983
+ includeContent: true,
984
+ contentLimit: parsedArgs.contentLimit
985
+ });
986
+ const truncationNote = page.hasMoreContent ? `Only the first ${page.contentBlocks.length} top-level blocks are included here. Call get-page with cursor '${page.nextCursor ?? ""}' if you need more context.` : "The included excerpt covers all requested top-level blocks.";
987
+ return {
988
+ messages: [
989
+ {
990
+ role: "user",
991
+ content: {
992
+ type: "text",
993
+ text: [
994
+ `Summarize the following Notion document for ${parsedArgs.audience}.`,
995
+ `Focus on: ${parsedArgs.focus}.`,
996
+ "Use a concise executive summary followed by bullet points for notable decisions, risks, and next actions.",
997
+ truncationNote,
998
+ "",
999
+ "Source document payload:",
1000
+ JSON.stringify(page, null, 2)
1001
+ ].join("\n")
1002
+ }
1003
+ }
1004
+ ]
1005
+ };
1006
+ } catch (error) {
1007
+ throw buildOperationError(`prepare a summarize-doc prompt for page ${normalizedPageId}`, error);
1008
+ }
1009
+ }
1010
+ };
1011
+ function createServer(options = {}) {
1012
+ const config = options.config ?? loadNotionConfig(options.envSource);
1013
+ const client = options.client ?? new NotionApiClient(
1014
+ options.fetchImplementation ? {
1015
+ config,
1016
+ fetchImplementation: options.fetchImplementation
1017
+ } : {
1018
+ config
1019
+ }
1020
+ );
1021
+ return new NotionServer({
1022
+ config,
1023
+ client
1024
+ });
1025
+ }
1026
+ async function main(argv = process.argv.slice(2)) {
1027
+ const runtimeOptions = parseRuntimeOptions(argv);
1028
+ await runToolkitServer(
1029
+ {
1030
+ serverCard,
1031
+ createServer
1032
+ },
1033
+ runtimeOptions
1034
+ );
1035
+ }
1036
+ if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
1037
+ void main().catch((error) => {
1038
+ const normalized = normalizeError(error);
1039
+ console.error(normalized.toClientMessage());
1040
+ process.exitCode = 1;
1041
+ });
1042
+ }
1043
+ export {
1044
+ NotionApiClient,
1045
+ NotionServer,
1046
+ createServer,
1047
+ main,
1048
+ metadata,
1049
+ serverCard
1050
+ };