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.
- package/README.md +383 -192
- package/build/config/index.js +3 -1
- package/build/dispatch/concurrency.js +15 -0
- package/build/dispatch/idempotency.js +38 -0
- package/build/dispatch/index.js +175 -0
- package/build/dispatch/rate-limit.js +56 -0
- package/build/dispatch/retry.js +97 -0
- package/build/index.js +1 -1
- package/build/markdown/parse.js +265 -0
- package/build/operations/blocks.js +331 -0
- package/build/operations/comments.js +191 -0
- package/build/operations/data-sources.js +85 -0
- package/build/operations/databases.js +345 -0
- package/build/operations/files.js +239 -0
- package/build/operations/index.js +19 -0
- package/build/operations/pages.js +486 -0
- package/build/operations/registry.js +16 -0
- package/build/operations/users.js +101 -0
- package/build/prompts/index.js +105 -0
- package/build/schema/blocks.js +19 -138
- package/build/schema/database.js +27 -111
- package/build/schema/emit.js +68 -0
- package/build/schema/file.js +1 -1
- package/build/schema/filter-dsl.js +333 -0
- package/build/schema/icon.js +1 -1
- package/build/schema/page-properties.js +17 -3
- package/build/schema/page.js +12 -125
- package/build/schema/refs.js +16 -0
- package/build/schema/rich-text.js +1 -1
- package/build/server/index.js +16 -3
- package/build/services/auth.js +19 -0
- package/build/services/notion.js +14 -17
- package/build/tools/index.js +119 -21
- package/build/utils/error.js +125 -86
- package/build/utils/handler.js +11 -0
- package/build/utils/learning-error.js +40 -0
- package/build/utils/notion-types.js +16 -0
- package/build/utils/paginate.js +35 -0
- package/build/utils/schema-slice.js +156 -0
- package/build/utils/slim.js +269 -0
- package/package.json +13 -7
- package/build/resources/imageList.js +0 -62
- package/build/resources/index.js +0 -1
- package/build/resources/predictionList.js +0 -43
- package/build/resources/svgList.js +0 -69
- package/build/schema/comments.js +0 -60
- package/build/schema/notion.js +0 -57
- package/build/schema/richText.js +0 -757
- package/build/schema/tools.js +0 -17
- package/build/schema/users.js +0 -39
- package/build/services/loggs.js +0 -13
- package/build/services/replicate.js +0 -23
- package/build/tools/appendBlockChildren.js +0 -25
- package/build/tools/batchAppendBlockChildren.js +0 -33
- package/build/tools/batchDeleteBlocks.js +0 -32
- package/build/tools/batchMixedOperations.js +0 -58
- package/build/tools/batchUpdateBlocks.js +0 -33
- package/build/tools/blocks.js +0 -34
- package/build/tools/comments.js +0 -81
- package/build/tools/createDatabase.js +0 -18
- package/build/tools/createPage.js +0 -18
- package/build/tools/createPrediction.js +0 -28
- package/build/tools/database.js +0 -16
- package/build/tools/deleteBlock.js +0 -24
- package/build/tools/formatRichText.js +0 -83
- package/build/tools/generateImage.js +0 -48
- package/build/tools/generateImageVariants.js +0 -105
- package/build/tools/generateMultipleImages.js +0 -60
- package/build/tools/generateSVG.js +0 -43
- package/build/tools/getPrediction.js +0 -22
- package/build/tools/pages.js +0 -22
- package/build/tools/predictionList.js +0 -30
- package/build/tools/queryDatabase.js +0 -22
- package/build/tools/retrieveBlock.js +0 -24
- package/build/tools/retrieveBlockChildren.js +0 -32
- package/build/tools/searchPage.js +0 -24
- package/build/tools/updateBlock.js +0 -25
- package/build/tools/updateDatabase.js +0 -18
- package/build/tools/updatePage.js +0 -40
- package/build/tools/updatePageProperties.js +0 -21
- package/build/tools/users.js +0 -75
- package/build/types/blocks.js +0 -12
- package/build/types/comments.js +0 -7
- package/build/types/database.js +0 -6
- package/build/types/notion.js +0 -1
- package/build/types/page.js +0 -8
- package/build/types/richText.js +0 -1
- package/build/types/tools.js +0 -1
- package/build/types/users.js +0 -6
- package/build/utils/blob.js +0 -5
- package/build/utils/image.js +0 -34
- package/build/utils/index.js +0 -1
- package/build/utils/richText.js +0 -174
- package/build/validation/blocks.js +0 -568
- package/build/validation/notion.js +0 -51
- package/build/validation/page.js +0 -262
- package/build/validation/richText.js +0 -744
- package/build/validation/tools.js +0 -16
- /package/build/{types/index.js → operations/types.js} +0 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Typed shorthand for Notion `filter` payloads on data source queries.
|
|
3
|
+
// The user writes `{Status: "Done", Priority: {gte: 3}}` and we compile to
|
|
4
|
+
// the verbose Notion JSON. Multi-key clauses are implicit AND; explicit
|
|
5
|
+
// {AND/OR/NOT: ...} blocks compose. Property TYPE is inferred from value
|
|
6
|
+
// shape — pass `__type: "multi_select"` etc. when inference is wrong.
|
|
7
|
+
// ─── Property types — single source of truth ────────────────────────────────
|
|
8
|
+
const PROPERTY_TYPES = [
|
|
9
|
+
"title",
|
|
10
|
+
"rich_text",
|
|
11
|
+
"number",
|
|
12
|
+
"select",
|
|
13
|
+
"multi_select",
|
|
14
|
+
"status",
|
|
15
|
+
"date",
|
|
16
|
+
"checkbox",
|
|
17
|
+
"people",
|
|
18
|
+
"files",
|
|
19
|
+
"relation",
|
|
20
|
+
"url",
|
|
21
|
+
"email",
|
|
22
|
+
"phone_number",
|
|
23
|
+
"created_time",
|
|
24
|
+
"last_edited_time",
|
|
25
|
+
"created_by",
|
|
26
|
+
"last_edited_by",
|
|
27
|
+
"unique_id",
|
|
28
|
+
];
|
|
29
|
+
// ─── DSL operator surface ───────────────────────────────────────────────────
|
|
30
|
+
const SCALAR = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
31
|
+
const OPERATORS_SCHEMA = z
|
|
32
|
+
.object({
|
|
33
|
+
eq: z.union([SCALAR, z.array(SCALAR)]).optional(),
|
|
34
|
+
ne: z.union([SCALAR, z.array(SCALAR)]).optional(),
|
|
35
|
+
gt: SCALAR.optional(),
|
|
36
|
+
gte: SCALAR.optional(),
|
|
37
|
+
lt: SCALAR.optional(),
|
|
38
|
+
lte: SCALAR.optional(),
|
|
39
|
+
contains: z.string().optional(),
|
|
40
|
+
notContains: z.string().optional(),
|
|
41
|
+
startsWith: z.string().optional(),
|
|
42
|
+
endsWith: z.string().optional(),
|
|
43
|
+
in: z.array(SCALAR).optional(),
|
|
44
|
+
notIn: z.array(SCALAR).optional(),
|
|
45
|
+
is_empty: z.boolean().optional(),
|
|
46
|
+
is_not_empty: z.boolean().optional(),
|
|
47
|
+
before: z.string().optional(),
|
|
48
|
+
after: z.string().optional(),
|
|
49
|
+
on_or_before: z.string().optional(),
|
|
50
|
+
on_or_after: z.string().optional(),
|
|
51
|
+
__type: z.enum(PROPERTY_TYPES).optional(),
|
|
52
|
+
})
|
|
53
|
+
.strict();
|
|
54
|
+
export const WHERE_SCHEMA = z.record(z.string(), z.unknown()).describe("Typed filter DSL. Property names map to scalar values (equals shorthand) or operator objects like {gte:3, contains:'x'}. Top-level AND/OR arrays and NOT compose.");
|
|
55
|
+
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}/;
|
|
56
|
+
function isDateLike(v) {
|
|
57
|
+
return typeof v === "string" && ISO_DATE_RE.test(v);
|
|
58
|
+
}
|
|
59
|
+
// Single place where we narrow unknown → keyed object. Throws so the caller
|
|
60
|
+
// surfaces a helpful where_compile_error rather than silently mis-typing.
|
|
61
|
+
function asObject(value, context) {
|
|
62
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
63
|
+
throw new Error(`${context}: expected object, got ${JSON.stringify(value)}`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
// ─── Operator maps per property-type family ─────────────────────────────────
|
|
68
|
+
const NUMERIC_OP_MAP = {
|
|
69
|
+
eq: "equals",
|
|
70
|
+
ne: "does_not_equal",
|
|
71
|
+
gt: "greater_than",
|
|
72
|
+
gte: "greater_than_or_equal_to",
|
|
73
|
+
lt: "less_than",
|
|
74
|
+
lte: "less_than_or_equal_to",
|
|
75
|
+
};
|
|
76
|
+
const DATE_OP_MAP = {
|
|
77
|
+
eq: "equals",
|
|
78
|
+
ne: "does_not_equal",
|
|
79
|
+
gt: "after",
|
|
80
|
+
gte: "on_or_after",
|
|
81
|
+
lt: "before",
|
|
82
|
+
lte: "on_or_before",
|
|
83
|
+
before: "before",
|
|
84
|
+
after: "after",
|
|
85
|
+
on_or_before: "on_or_before",
|
|
86
|
+
on_or_after: "on_or_after",
|
|
87
|
+
};
|
|
88
|
+
const TEXT_OP_MAP = {
|
|
89
|
+
eq: "equals",
|
|
90
|
+
ne: "does_not_equal",
|
|
91
|
+
contains: "contains",
|
|
92
|
+
notContains: "does_not_contain",
|
|
93
|
+
startsWith: "starts_with",
|
|
94
|
+
endsWith: "ends_with",
|
|
95
|
+
};
|
|
96
|
+
const SELECT_OP_MAP = {
|
|
97
|
+
eq: "equals",
|
|
98
|
+
ne: "does_not_equal",
|
|
99
|
+
};
|
|
100
|
+
const MULTI_SELECT_OP_MAP = {
|
|
101
|
+
eq: "contains",
|
|
102
|
+
ne: "does_not_contain",
|
|
103
|
+
contains: "contains",
|
|
104
|
+
notContains: "does_not_contain",
|
|
105
|
+
};
|
|
106
|
+
const EMPTINESS_OPS = new Set(["is_empty", "is_not_empty"]);
|
|
107
|
+
const TYPE_OP_MAP = {
|
|
108
|
+
number: NUMERIC_OP_MAP,
|
|
109
|
+
unique_id: NUMERIC_OP_MAP,
|
|
110
|
+
date: DATE_OP_MAP,
|
|
111
|
+
created_time: DATE_OP_MAP,
|
|
112
|
+
last_edited_time: DATE_OP_MAP,
|
|
113
|
+
title: TEXT_OP_MAP,
|
|
114
|
+
rich_text: TEXT_OP_MAP,
|
|
115
|
+
url: TEXT_OP_MAP,
|
|
116
|
+
email: TEXT_OP_MAP,
|
|
117
|
+
phone_number: TEXT_OP_MAP,
|
|
118
|
+
select: SELECT_OP_MAP,
|
|
119
|
+
status: SELECT_OP_MAP,
|
|
120
|
+
multi_select: MULTI_SELECT_OP_MAP,
|
|
121
|
+
relation: MULTI_SELECT_OP_MAP,
|
|
122
|
+
people: MULTI_SELECT_OP_MAP,
|
|
123
|
+
files: MULTI_SELECT_OP_MAP,
|
|
124
|
+
checkbox: SELECT_OP_MAP,
|
|
125
|
+
created_by: SELECT_OP_MAP,
|
|
126
|
+
last_edited_by: SELECT_OP_MAP,
|
|
127
|
+
};
|
|
128
|
+
function opMapFor(type) {
|
|
129
|
+
return TYPE_OP_MAP[type] ?? SELECT_OP_MAP;
|
|
130
|
+
}
|
|
131
|
+
// ─── Type inference ─────────────────────────────────────────────────────────
|
|
132
|
+
const DATE_ONLY_OPS = ["before", "after", "on_or_before", "on_or_after"];
|
|
133
|
+
const TEXT_ONLY_OPS = ["contains", "notContains", "startsWith", "endsWith"];
|
|
134
|
+
function inferTypeFromScalar(v) {
|
|
135
|
+
if (typeof v === "number")
|
|
136
|
+
return "number";
|
|
137
|
+
if (typeof v === "boolean")
|
|
138
|
+
return "checkbox";
|
|
139
|
+
return isDateLike(v) ? "date" : "select";
|
|
140
|
+
}
|
|
141
|
+
function inferTypeFromOps(ops) {
|
|
142
|
+
const hint = ops.__type;
|
|
143
|
+
if (typeof hint === "string" && PROPERTY_TYPES.includes(hint)) {
|
|
144
|
+
return hint;
|
|
145
|
+
}
|
|
146
|
+
if (DATE_ONLY_OPS.some((k) => k in ops))
|
|
147
|
+
return "date";
|
|
148
|
+
if (TEXT_ONLY_OPS.some((k) => k in ops))
|
|
149
|
+
return "rich_text";
|
|
150
|
+
const sample = ops.eq ?? ops.ne ?? ops.gt ?? ops.gte ?? ops.lt ?? ops.lte ?? ops.in ?? ops.notIn;
|
|
151
|
+
if (Array.isArray(sample)) {
|
|
152
|
+
const first = sample[0];
|
|
153
|
+
if (typeof first === "number")
|
|
154
|
+
return "number";
|
|
155
|
+
if (typeof first === "string" && isDateLike(first))
|
|
156
|
+
return "date";
|
|
157
|
+
return "select";
|
|
158
|
+
}
|
|
159
|
+
if (typeof sample === "number" || typeof sample === "boolean") {
|
|
160
|
+
return inferTypeFromScalar(sample);
|
|
161
|
+
}
|
|
162
|
+
if (typeof sample === "string")
|
|
163
|
+
return inferTypeFromScalar(sample);
|
|
164
|
+
// is_empty / is_not_empty alone → rich_text default (works on most types)
|
|
165
|
+
return "rich_text";
|
|
166
|
+
}
|
|
167
|
+
// ─── Leaf compilation ───────────────────────────────────────────────────────
|
|
168
|
+
function buildLeaf(propertyName, type, condition) {
|
|
169
|
+
return { property: propertyName, [type]: condition };
|
|
170
|
+
}
|
|
171
|
+
function compilePropertyClause(propertyName, value) {
|
|
172
|
+
// Bare scalar shorthand → equals
|
|
173
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
174
|
+
const type = inferTypeFromScalar(value);
|
|
175
|
+
const opMap = opMapFor(type);
|
|
176
|
+
return buildLeaf(propertyName, type, { [opMap.eq]: value });
|
|
177
|
+
}
|
|
178
|
+
if (value === null) {
|
|
179
|
+
return buildLeaf(propertyName, "rich_text", { is_empty: true });
|
|
180
|
+
}
|
|
181
|
+
const ops = asObject(value, `filter value for property "${propertyName}"`);
|
|
182
|
+
const type = inferTypeFromOps(ops);
|
|
183
|
+
const opMap = opMapFor(type);
|
|
184
|
+
// {in: [...]} → OR of equals on the same property
|
|
185
|
+
if (Array.isArray(ops.in)) {
|
|
186
|
+
if (ops.in.length === 0) {
|
|
187
|
+
throw new Error(`"in" on property "${propertyName}" must be non-empty`);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
or: ops.in.map((v) => buildLeaf(propertyName, type, { [opMap.eq]: v })),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(ops.notIn)) {
|
|
194
|
+
if (ops.notIn.length === 0) {
|
|
195
|
+
throw new Error(`"notIn" on property "${propertyName}" must be non-empty`);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
and: ops.notIn.map((v) => buildLeaf(propertyName, type, { [opMap.ne]: v })),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const conditions = [];
|
|
202
|
+
for (const [opKey, opVal] of Object.entries(ops)) {
|
|
203
|
+
if (opKey === "__type" || opVal === undefined)
|
|
204
|
+
continue;
|
|
205
|
+
if (EMPTINESS_OPS.has(opKey)) {
|
|
206
|
+
if (opVal !== true)
|
|
207
|
+
continue;
|
|
208
|
+
conditions.push([opKey, true]);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const notionOp = opMap[opKey];
|
|
212
|
+
if (!notionOp) {
|
|
213
|
+
throw new Error(`Unsupported operator "${opKey}" on property "${propertyName}" (inferred type: ${type}). Use __type to override.`);
|
|
214
|
+
}
|
|
215
|
+
conditions.push([notionOp, opVal]);
|
|
216
|
+
}
|
|
217
|
+
if (conditions.length === 0) {
|
|
218
|
+
throw new Error(`No usable operators on property "${propertyName}"`);
|
|
219
|
+
}
|
|
220
|
+
if (conditions.length === 1) {
|
|
221
|
+
const [notionOp, opVal] = conditions[0];
|
|
222
|
+
return buildLeaf(propertyName, type, { [notionOp]: opVal });
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
and: conditions.map(([k, v]) => buildLeaf(propertyName, type, { [k]: v })),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// ─── Negation (De Morgan + per-leaf inversion) ──────────────────────────────
|
|
229
|
+
const INVERSE_LEAF_OP = {
|
|
230
|
+
equals: "does_not_equal",
|
|
231
|
+
does_not_equal: "equals",
|
|
232
|
+
contains: "does_not_contain",
|
|
233
|
+
does_not_contain: "contains",
|
|
234
|
+
is_empty: "is_not_empty",
|
|
235
|
+
is_not_empty: "is_empty",
|
|
236
|
+
greater_than: "less_than_or_equal_to",
|
|
237
|
+
less_than: "greater_than_or_equal_to",
|
|
238
|
+
greater_than_or_equal_to: "less_than",
|
|
239
|
+
less_than_or_equal_to: "greater_than",
|
|
240
|
+
before: "on_or_after",
|
|
241
|
+
after: "on_or_before",
|
|
242
|
+
on_or_before: "after",
|
|
243
|
+
on_or_after: "before",
|
|
244
|
+
};
|
|
245
|
+
function negateChildren(arr) {
|
|
246
|
+
return arr.map((f, i) => negate(asObject(f, `NOT child[${i}]`)));
|
|
247
|
+
}
|
|
248
|
+
function negate(filter) {
|
|
249
|
+
if (Array.isArray(filter.and))
|
|
250
|
+
return { or: negateChildren(filter.and) };
|
|
251
|
+
if (Array.isArray(filter.or))
|
|
252
|
+
return { and: negateChildren(filter.or) };
|
|
253
|
+
if (typeof filter.property !== "string") {
|
|
254
|
+
throw new Error(`Cannot negate filter: unsupported shape ${JSON.stringify(filter)}`);
|
|
255
|
+
}
|
|
256
|
+
const typeKeys = Object.keys(filter).filter((k) => k !== "property");
|
|
257
|
+
if (typeKeys.length !== 1) {
|
|
258
|
+
throw new Error("NOT requires a single typed leaf or a logical compound");
|
|
259
|
+
}
|
|
260
|
+
const typeKey = typeKeys[0];
|
|
261
|
+
const inner = asObject(filter[typeKey], `NOT leaf "${filter.property}"`);
|
|
262
|
+
const innerKeys = Object.keys(inner);
|
|
263
|
+
if (innerKeys.length !== 1) {
|
|
264
|
+
throw new Error("NOT cannot negate a leaf with multiple operators; rewrite as AND/OR first");
|
|
265
|
+
}
|
|
266
|
+
const op = innerKeys[0];
|
|
267
|
+
const inv = INVERSE_LEAF_OP[op];
|
|
268
|
+
if (!inv) {
|
|
269
|
+
throw new Error(`Operator "${op}" has no inverse; rewrite without NOT`);
|
|
270
|
+
}
|
|
271
|
+
return { property: filter.property, [typeKey]: { [inv]: inner[op] } };
|
|
272
|
+
}
|
|
273
|
+
// ─── Top-level recursion ────────────────────────────────────────────────────
|
|
274
|
+
// Boolean combinator keywords are case-insensitive. Lowercase is the
|
|
275
|
+
// canonical form (matches Notion's own filter JSON), but uppercase is
|
|
276
|
+
// accepted so callers who think of AND/OR/NOT as SQL-style keywords
|
|
277
|
+
// don't trip on case. A column literally named "and"/"AND" must be
|
|
278
|
+
// disambiguated with __type via an operator object — collision is
|
|
279
|
+
// documented in the resource.
|
|
280
|
+
const AND_KEYS = new Set(["and", "AND"]);
|
|
281
|
+
const OR_KEYS = new Set(["or", "OR"]);
|
|
282
|
+
const NOT_KEYS = new Set(["not", "NOT"]);
|
|
283
|
+
function compileCombinator(value, keyword, wrapKey) {
|
|
284
|
+
if (!Array.isArray(value)) {
|
|
285
|
+
throw new Error(`${keyword} must be an array of where clauses`);
|
|
286
|
+
}
|
|
287
|
+
const inner = [];
|
|
288
|
+
for (const child of value) {
|
|
289
|
+
const compiled = compileWhere(child);
|
|
290
|
+
if (compiled)
|
|
291
|
+
inner.push(compiled);
|
|
292
|
+
}
|
|
293
|
+
if (inner.length === 0)
|
|
294
|
+
return undefined;
|
|
295
|
+
if (inner.length === 1)
|
|
296
|
+
return inner[0];
|
|
297
|
+
return { [wrapKey]: inner };
|
|
298
|
+
}
|
|
299
|
+
export function compileWhere(where) {
|
|
300
|
+
if (where === undefined || where === null)
|
|
301
|
+
return undefined;
|
|
302
|
+
const obj = asObject(where, "where clause");
|
|
303
|
+
const entries = Object.entries(obj);
|
|
304
|
+
if (entries.length === 0)
|
|
305
|
+
return undefined;
|
|
306
|
+
const parts = [];
|
|
307
|
+
for (const [key, value] of entries) {
|
|
308
|
+
if (AND_KEYS.has(key)) {
|
|
309
|
+
const compiled = compileCombinator(value, key, "and");
|
|
310
|
+
if (compiled)
|
|
311
|
+
parts.push(compiled);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (OR_KEYS.has(key)) {
|
|
315
|
+
const compiled = compileCombinator(value, key, "or");
|
|
316
|
+
if (compiled)
|
|
317
|
+
parts.push(compiled);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (NOT_KEYS.has(key)) {
|
|
321
|
+
const inner = compileWhere(value);
|
|
322
|
+
if (inner)
|
|
323
|
+
parts.push(negate(inner));
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
parts.push(compilePropertyClause(key, value));
|
|
327
|
+
}
|
|
328
|
+
if (parts.length === 0)
|
|
329
|
+
return undefined;
|
|
330
|
+
if (parts.length === 1)
|
|
331
|
+
return parts[0];
|
|
332
|
+
return { and: parts };
|
|
333
|
+
}
|
package/build/schema/icon.js
CHANGED
|
@@ -10,13 +10,13 @@ export const DATE_PROPERTY_VALUE_SCHEMA = z.object({
|
|
|
10
10
|
}),
|
|
11
11
|
});
|
|
12
12
|
export const EMAIL_PROPERTY_VALUE_SCHEMA = z.object({
|
|
13
|
-
email: z.
|
|
13
|
+
email: z.email(),
|
|
14
14
|
});
|
|
15
15
|
export const FILES_PROPERTY_VALUE_SCHEMA = z.object({
|
|
16
16
|
files: z.array(z.object({
|
|
17
17
|
name: z.string(),
|
|
18
18
|
external: z.object({
|
|
19
|
-
url: z.
|
|
19
|
+
url: z.url({ protocol: /^https?$/ }),
|
|
20
20
|
}),
|
|
21
21
|
})),
|
|
22
22
|
});
|
|
@@ -56,5 +56,19 @@ export const TITLE_PROPERTY_VALUE_SCHEMA = z.object({
|
|
|
56
56
|
title: z.array(TEXT_RICH_TEXT_ITEM_REQUEST_SCHEMA),
|
|
57
57
|
});
|
|
58
58
|
export const URL_PROPERTY_VALUE_SCHEMA = z.object({
|
|
59
|
-
url: z.
|
|
59
|
+
url: z.url({ protocol: /^https?$/ }),
|
|
60
|
+
});
|
|
61
|
+
export const VERIFICATION_PROPERTY_VALUE_SCHEMA = z.object({
|
|
62
|
+
verification: z
|
|
63
|
+
.object({
|
|
64
|
+
state: z.enum(["verified", "unverified", "expired"]),
|
|
65
|
+
verified_by: z
|
|
66
|
+
.object({ id: z.string(), object: z.literal("user").optional() })
|
|
67
|
+
.optional(),
|
|
68
|
+
date: z
|
|
69
|
+
.object({ start: z.string(), end: z.string().nullable().optional() })
|
|
70
|
+
.nullable()
|
|
71
|
+
.optional(),
|
|
72
|
+
})
|
|
73
|
+
.describe("Verification property value"),
|
|
60
74
|
});
|
package/build/schema/page.js
CHANGED
|
@@ -1,18 +1,4 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getRootPageId } from "../services/notion.js";
|
|
3
|
-
import { ICON_SCHEMA } from "./icon.js";
|
|
4
|
-
import { TEXT_BLOCK_REQUEST_SCHEMA } from "./blocks.js";
|
|
5
|
-
import { preprocessJson } from "./preprocess.js";
|
|
6
|
-
import { TEXT_CONTENT_REQUEST_SCHEMA } from "./rich-text.js";
|
|
7
|
-
import { FILE_SCHEMA } from "./file.js";
|
|
8
|
-
import { CHECKBOX_PROPERTY_VALUE_SCHEMA, DATE_PROPERTY_VALUE_SCHEMA, EMAIL_PROPERTY_VALUE_SCHEMA, FILES_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, } from "./page-properties.js";
|
|
9
|
-
export const TITLE_PROPERTY_SCHEMA = z.object({
|
|
10
|
-
title: z
|
|
11
|
-
.array(z.object({
|
|
12
|
-
text: TEXT_CONTENT_REQUEST_SCHEMA.describe("Text content for title segment"),
|
|
13
|
-
}))
|
|
14
|
-
.describe("Array of text segments that make up the title"),
|
|
15
|
-
});
|
|
16
2
|
export const PARENT_SCHEMA = z.preprocess((val) => (typeof val === "string" ? JSON.parse(val) : val), z.union([
|
|
17
3
|
z.object({
|
|
18
4
|
type: z.literal("page_id").describe("Parent type for page"),
|
|
@@ -22,115 +8,16 @@ export const PARENT_SCHEMA = z.preprocess((val) => (typeof val === "string" ? JS
|
|
|
22
8
|
type: z.literal("database_id").describe("Parent type for database"),
|
|
23
9
|
database_id: z.string().describe("ID of the parent database"),
|
|
24
10
|
}),
|
|
11
|
+
z.object({
|
|
12
|
+
type: z.literal("data_source_id").describe("Parent type for data source (preferred for create_page targeting a database)"),
|
|
13
|
+
data_source_id: z.string().describe("ID of the parent data source"),
|
|
14
|
+
}),
|
|
15
|
+
z.object({
|
|
16
|
+
type: z.literal("workspace").describe("Workspace-level parent (admin contexts only)"),
|
|
17
|
+
workspace: z.literal(true),
|
|
18
|
+
}),
|
|
19
|
+
z.object({
|
|
20
|
+
type: z.literal("block_id").describe("Parent type for nested block"),
|
|
21
|
+
block_id: z.string(),
|
|
22
|
+
}),
|
|
25
23
|
]));
|
|
26
|
-
export const CREATE_PAGE_SCHEMA = {
|
|
27
|
-
parent: PARENT_SCHEMA.optional()
|
|
28
|
-
.default({
|
|
29
|
-
type: "page_id",
|
|
30
|
-
page_id: getRootPageId(),
|
|
31
|
-
})
|
|
32
|
-
.describe("Optional parent - if not provided, will use NOTION_PAGE_ID as parent page"),
|
|
33
|
-
properties: z
|
|
34
|
-
.record(z.string().describe("Property name"), z.union([
|
|
35
|
-
TITLE_PROPERTY_SCHEMA,
|
|
36
|
-
CHECKBOX_PROPERTY_VALUE_SCHEMA,
|
|
37
|
-
EMAIL_PROPERTY_VALUE_SCHEMA,
|
|
38
|
-
STATUS_PROPERTY_VALUE_SCHEMA,
|
|
39
|
-
FILES_PROPERTY_VALUE_SCHEMA,
|
|
40
|
-
DATE_PROPERTY_VALUE_SCHEMA,
|
|
41
|
-
PEOPLE_PROPERTY_VALUE_SCHEMA,
|
|
42
|
-
PHONE_NUMBER_PROPERTY_VALUE_SCHEMA,
|
|
43
|
-
RELATION_PROPERTY_VALUE_SCHEMA,
|
|
44
|
-
RICH_TEXT_PROPERTY_VALUE_SCHEMA,
|
|
45
|
-
SELECT_PROPERTY_VALUE_SCHEMA,
|
|
46
|
-
NUMBER_PROPERTY_VALUE_SCHEMA,
|
|
47
|
-
]))
|
|
48
|
-
.describe("Properties of the page"),
|
|
49
|
-
children: z
|
|
50
|
-
.array(TEXT_BLOCK_REQUEST_SCHEMA)
|
|
51
|
-
.optional()
|
|
52
|
-
.describe("Optional array of paragraph blocks to add as page content"),
|
|
53
|
-
icon: z.preprocess(preprocessJson, ICON_SCHEMA.nullable().optional().describe("Optional icon for the page")),
|
|
54
|
-
cover: z.preprocess(preprocessJson, FILE_SCHEMA.nullable()
|
|
55
|
-
.optional()
|
|
56
|
-
.describe("Optional cover image for the page")),
|
|
57
|
-
};
|
|
58
|
-
export const ARCHIVE_PAGE_SCHEMA = {
|
|
59
|
-
pageId: z.string().describe("The ID of the page to archive"),
|
|
60
|
-
};
|
|
61
|
-
export const RESTORE_PAGE_SCHEMA = {
|
|
62
|
-
pageId: z.string().describe("The ID of the page to restore"),
|
|
63
|
-
};
|
|
64
|
-
export const UPDATE_PAGE_PROPERTIES_SCHEMA = {
|
|
65
|
-
pageId: z.string().describe("The ID of the page to restore"),
|
|
66
|
-
properties: z
|
|
67
|
-
.record(z.string().describe("Property name"), z.union([
|
|
68
|
-
TITLE_PROPERTY_SCHEMA,
|
|
69
|
-
CHECKBOX_PROPERTY_VALUE_SCHEMA,
|
|
70
|
-
EMAIL_PROPERTY_VALUE_SCHEMA,
|
|
71
|
-
STATUS_PROPERTY_VALUE_SCHEMA,
|
|
72
|
-
FILES_PROPERTY_VALUE_SCHEMA,
|
|
73
|
-
DATE_PROPERTY_VALUE_SCHEMA,
|
|
74
|
-
PEOPLE_PROPERTY_VALUE_SCHEMA,
|
|
75
|
-
PHONE_NUMBER_PROPERTY_VALUE_SCHEMA,
|
|
76
|
-
RELATION_PROPERTY_VALUE_SCHEMA,
|
|
77
|
-
RICH_TEXT_PROPERTY_VALUE_SCHEMA,
|
|
78
|
-
SELECT_PROPERTY_VALUE_SCHEMA,
|
|
79
|
-
NUMBER_PROPERTY_VALUE_SCHEMA,
|
|
80
|
-
]))
|
|
81
|
-
.describe("Properties of the page"),
|
|
82
|
-
};
|
|
83
|
-
export const SEARCH_PAGES_SCHEMA = {
|
|
84
|
-
query: z.string().optional().describe("Search query for filtering by title"),
|
|
85
|
-
sort: z
|
|
86
|
-
.object({
|
|
87
|
-
direction: z.enum(["ascending", "descending"]),
|
|
88
|
-
timestamp: z.literal("last_edited_time"),
|
|
89
|
-
})
|
|
90
|
-
.optional()
|
|
91
|
-
.describe("Sort order for results"),
|
|
92
|
-
start_cursor: z.string().optional().describe("Cursor for pagination"),
|
|
93
|
-
page_size: z
|
|
94
|
-
.number()
|
|
95
|
-
.min(1)
|
|
96
|
-
.max(100)
|
|
97
|
-
.optional()
|
|
98
|
-
.describe("Number of results to return (1-100)"),
|
|
99
|
-
};
|
|
100
|
-
// Combined schema for all page operations
|
|
101
|
-
export const PAGES_OPERATION_SCHEMA = {
|
|
102
|
-
payload: z
|
|
103
|
-
.preprocess(preprocessJson, z.discriminatedUnion("action", [
|
|
104
|
-
z.object({
|
|
105
|
-
action: z
|
|
106
|
-
.literal("create_page")
|
|
107
|
-
.describe("Use this action to create a new page in the database."),
|
|
108
|
-
params: z.object(CREATE_PAGE_SCHEMA),
|
|
109
|
-
}),
|
|
110
|
-
z.object({
|
|
111
|
-
action: z
|
|
112
|
-
.literal("archive_page")
|
|
113
|
-
.describe("Use this action to archive an existing page, making it inactive."),
|
|
114
|
-
params: z.object(ARCHIVE_PAGE_SCHEMA),
|
|
115
|
-
}),
|
|
116
|
-
z.object({
|
|
117
|
-
action: z
|
|
118
|
-
.literal("restore_page")
|
|
119
|
-
.describe("Use this action to restore a previously archived page."),
|
|
120
|
-
params: z.object(RESTORE_PAGE_SCHEMA),
|
|
121
|
-
}),
|
|
122
|
-
z.object({
|
|
123
|
-
action: z
|
|
124
|
-
.literal("search_pages")
|
|
125
|
-
.describe("Use this action to search for pages based on a query."),
|
|
126
|
-
params: z.object(SEARCH_PAGES_SCHEMA),
|
|
127
|
-
}),
|
|
128
|
-
z.object({
|
|
129
|
-
action: z
|
|
130
|
-
.literal("update_page_properties")
|
|
131
|
-
.describe("Use this action to update the properties of an existing page."),
|
|
132
|
-
params: z.object(UPDATE_PAGE_PROPERTIES_SCHEMA),
|
|
133
|
-
}),
|
|
134
|
-
]))
|
|
135
|
-
.describe("A union of all possible page operations. Each operation has a specific action and corresponding parameters. Use this schema to validate the input for page operations such as creating, archiving, restoring, searching, and updating pages. Available actions include: 'create_page', 'archive_page', 'restore_page', 'search_pages', and 'update_page_properties'. Each operation requires specific parameters as defined in the corresponding schemas."),
|
|
136
|
-
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { RICH_TEXT_ITEM_REQUEST_SCHEMA } from "./rich-text.js";
|
|
2
|
+
import { PARENT_SCHEMA } from "./page.js";
|
|
3
|
+
import { ICON_SCHEMA } from "./icon.js";
|
|
4
|
+
import { FILE_SCHEMA } from "./file.js";
|
|
5
|
+
import { registerSharedRef } from "./emit.js";
|
|
6
|
+
/**
|
|
7
|
+
* Canonical sub-schemas registered for $defs hoisting in emitted JSON Schemas.
|
|
8
|
+
* When any operation's input mentions one of these structurally, the emitter
|
|
9
|
+
* replaces the inlined copy with a $ref. This is where most schema-size wins come from.
|
|
10
|
+
*/
|
|
11
|
+
export function registerSharedSubSchemas() {
|
|
12
|
+
registerSharedRef("rich_text_item", RICH_TEXT_ITEM_REQUEST_SCHEMA);
|
|
13
|
+
registerSharedRef("parent", PARENT_SCHEMA);
|
|
14
|
+
registerSharedRef("icon", ICON_SCHEMA);
|
|
15
|
+
registerSharedRef("file", FILE_SCHEMA);
|
|
16
|
+
}
|
|
@@ -14,7 +14,7 @@ export const TEXT_CONTENT_REQUEST_SCHEMA = z
|
|
|
14
14
|
content: z.string().describe("The actual text content"),
|
|
15
15
|
link: z
|
|
16
16
|
.object({
|
|
17
|
-
url: z.
|
|
17
|
+
url: z.url({ protocol: /^https?$/ }).describe("URL for the link"),
|
|
18
18
|
})
|
|
19
19
|
.optional()
|
|
20
20
|
.nullable()
|
package/build/server/index.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { CONFIG } from "../config/index.js";
|
|
4
|
+
import { getClient } from "../services/notion.js";
|
|
4
5
|
export const server = new McpServer({
|
|
5
6
|
name: CONFIG.serverName,
|
|
7
|
+
title: CONFIG.serverTitle,
|
|
6
8
|
version: CONFIG.serverVersion,
|
|
9
|
+
websiteUrl: CONFIG.serverUrl,
|
|
7
10
|
}, {
|
|
8
11
|
capabilities: {
|
|
9
|
-
resources: {},
|
|
10
12
|
tools: {},
|
|
13
|
+
prompts: {},
|
|
11
14
|
},
|
|
12
15
|
instructions: `
|
|
13
|
-
MCP server for
|
|
16
|
+
MCP server for Notion.
|
|
14
17
|
It is used to create, update and delete Notion entities.
|
|
15
18
|
`,
|
|
16
19
|
});
|
|
@@ -18,7 +21,17 @@ export async function startServer() {
|
|
|
18
21
|
try {
|
|
19
22
|
const transport = new StdioServerTransport();
|
|
20
23
|
await server.connect(transport);
|
|
21
|
-
console.
|
|
24
|
+
console.error(`${CONFIG.serverName} v${CONFIG.serverVersion} running on stdio`);
|
|
25
|
+
getClient()
|
|
26
|
+
.then((c) => c.users.me({}))
|
|
27
|
+
.then((me) => {
|
|
28
|
+
const who = "name" in me && me.name ? me.name : me.id;
|
|
29
|
+
console.error(`Notion auth OK — connected as ${who} (NOTION_TOKEN)`);
|
|
30
|
+
})
|
|
31
|
+
.catch((err) => {
|
|
32
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
33
|
+
console.error(`Notion auth check failed (server still running): ${msg}`);
|
|
34
|
+
});
|
|
22
35
|
}
|
|
23
36
|
catch (error) {
|
|
24
37
|
console.error("Server initialization error:", error instanceof Error ? error.message : String(error));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/services/auth.ts
|
|
2
|
+
export class AuthError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "AuthError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class EnvAuthProvider {
|
|
9
|
+
async getToken() {
|
|
10
|
+
const t = process.env.NOTION_TOKEN;
|
|
11
|
+
if (!t) {
|
|
12
|
+
throw new AuthError("Notion auth token is not configured. Set the NOTION_TOKEN environment variable in your MCP client config. To get a token, open Notion → Settings → My Settings → Personal Access Tokens → Generate (recommended), or Settings → Connections → Develop or manage integrations → New integration.");
|
|
13
|
+
}
|
|
14
|
+
return t;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
// Singleton — single-user assumption. v3 multi-user OAuth would require
|
|
18
|
+
// per-request provider dispatch (different pattern; out of scope for v2).
|
|
19
|
+
export const authProvider = new EnvAuthProvider();
|
package/build/services/notion.js
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { Client } from "@notionhq/client";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { authProvider } from "./auth.js";
|
|
3
|
+
let cachedClient = null;
|
|
4
|
+
let cachedToken = null;
|
|
5
|
+
export async function getClient() {
|
|
6
|
+
const token = await authProvider.getToken();
|
|
7
|
+
if (token !== cachedToken || cachedClient === null) {
|
|
8
|
+
const fresh = new Client({
|
|
9
|
+
auth: token,
|
|
10
|
+
notionVersion: "2025-09-03",
|
|
11
|
+
});
|
|
12
|
+
cachedClient = fresh;
|
|
13
|
+
cachedToken = token;
|
|
14
|
+
return fresh;
|
|
7
15
|
}
|
|
8
|
-
return
|
|
16
|
+
return cachedClient;
|
|
9
17
|
}
|
|
10
|
-
export function getRootPageId() {
|
|
11
|
-
const pageId = process.env.NOTION_PAGE_ID;
|
|
12
|
-
if (!pageId) {
|
|
13
|
-
console.error("Error: NOTION_PAGE_ID environment variable is required");
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
|
-
return pageId;
|
|
17
|
-
}
|
|
18
|
-
export const notion = new Client({
|
|
19
|
-
auth: process.env.NOTION_TOKEN,
|
|
20
|
-
});
|