@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/.well-known/mcp-server.json +49 -0
- package/LICENSE +21 -0
- package/dist/index.d.ts +294 -0
- package/dist/index.js +1050 -0
- package/package.json +53 -0
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
|
+
};
|