notion-mcp-server 1.0.0 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +383 -192
  2. package/build/config/index.js +3 -1
  3. package/build/dispatch/concurrency.js +15 -0
  4. package/build/dispatch/idempotency.js +38 -0
  5. package/build/dispatch/index.js +175 -0
  6. package/build/dispatch/rate-limit.js +56 -0
  7. package/build/dispatch/retry.js +97 -0
  8. package/build/index.js +1 -1
  9. package/build/markdown/parse.js +265 -0
  10. package/build/operations/blocks.js +331 -0
  11. package/build/operations/comments.js +191 -0
  12. package/build/operations/data-sources.js +85 -0
  13. package/build/operations/databases.js +345 -0
  14. package/build/operations/files.js +239 -0
  15. package/build/operations/index.js +19 -0
  16. package/build/operations/pages.js +486 -0
  17. package/build/operations/registry.js +16 -0
  18. package/build/operations/users.js +101 -0
  19. package/build/prompts/index.js +105 -0
  20. package/build/schema/blocks.js +19 -77
  21. package/build/schema/database.js +27 -86
  22. package/build/schema/emit.js +68 -0
  23. package/build/schema/file.js +1 -1
  24. package/build/schema/filter-dsl.js +333 -0
  25. package/build/schema/icon.js +1 -1
  26. package/build/schema/page-properties.js +17 -3
  27. package/build/schema/page.js +12 -88
  28. package/build/schema/refs.js +16 -0
  29. package/build/schema/rich-text.js +1 -1
  30. package/build/server/index.js +15 -2
  31. package/build/services/auth.js +19 -0
  32. package/build/services/notion.js +14 -17
  33. package/build/tools/index.js +119 -51
  34. package/build/utils/error.js +125 -86
  35. package/build/utils/handler.js +11 -0
  36. package/build/utils/learning-error.js +40 -0
  37. package/build/utils/notion-types.js +16 -0
  38. package/build/utils/paginate.js +35 -0
  39. package/build/utils/schema-slice.js +156 -0
  40. package/build/utils/slim.js +269 -0
  41. package/package.json +13 -7
  42. package/build/resources/imageList.js +0 -62
  43. package/build/resources/index.js +0 -1
  44. package/build/resources/predictionList.js +0 -43
  45. package/build/resources/svgList.js +0 -69
  46. package/build/schema/comments.js +0 -34
  47. package/build/schema/notion.js +0 -57
  48. package/build/schema/richText.js +0 -757
  49. package/build/schema/tools.js +0 -17
  50. package/build/schema/users.js +0 -13
  51. package/build/services/replicate.js +0 -23
  52. package/build/tools/appendBlockChildren.js +0 -25
  53. package/build/tools/batchAppendBlockChildren.js +0 -33
  54. package/build/tools/batchDeleteBlocks.js +0 -32
  55. package/build/tools/batchMixedOperations.js +0 -58
  56. package/build/tools/batchUpdateBlocks.js +0 -33
  57. package/build/tools/comments.js +0 -62
  58. package/build/tools/createDatabase.js +0 -18
  59. package/build/tools/createPage.js +0 -18
  60. package/build/tools/createPrediction.js +0 -28
  61. package/build/tools/deleteBlock.js +0 -24
  62. package/build/tools/formatRichText.js +0 -83
  63. package/build/tools/generateImage.js +0 -48
  64. package/build/tools/generateImageVariants.js +0 -105
  65. package/build/tools/generateMultipleImages.js +0 -60
  66. package/build/tools/generateSVG.js +0 -43
  67. package/build/tools/getPrediction.js +0 -22
  68. package/build/tools/predictionList.js +0 -30
  69. package/build/tools/queryDatabase.js +0 -22
  70. package/build/tools/retrieveBlock.js +0 -24
  71. package/build/tools/retrieveBlockChildren.js +0 -32
  72. package/build/tools/searchPage.js +0 -24
  73. package/build/tools/updateBlock.js +0 -25
  74. package/build/tools/updateDatabase.js +0 -18
  75. package/build/tools/updatePage.js +0 -40
  76. package/build/tools/updatePageProperties.js +0 -21
  77. package/build/tools/users.js +0 -62
  78. package/build/types/blocks.js +0 -11
  79. package/build/types/comments.js +0 -6
  80. package/build/types/database.js +0 -5
  81. package/build/types/notion.js +0 -1
  82. package/build/types/page.js +0 -7
  83. package/build/types/richText.js +0 -1
  84. package/build/types/tools.js +0 -1
  85. package/build/types/users.js +0 -5
  86. package/build/utils/blob.js +0 -5
  87. package/build/utils/image.js +0 -34
  88. package/build/utils/index.js +0 -1
  89. package/build/utils/richText.js +0 -174
  90. package/build/validation/blocks.js +0 -568
  91. package/build/validation/notion.js +0 -51
  92. package/build/validation/page.js +0 -262
  93. package/build/validation/richText.js +0 -744
  94. package/build/validation/tools.js +0 -16
  95. /package/build/{types/index.js → operations/types.js} +0 -0
@@ -0,0 +1,105 @@
1
+ import { z } from "zod";
2
+ import { server } from "../server/index.js";
3
+ function userMessage(text) {
4
+ return {
5
+ messages: [
6
+ {
7
+ role: "user",
8
+ content: { type: "text", text },
9
+ },
10
+ ],
11
+ };
12
+ }
13
+ export function registerAllPrompts() {
14
+ server.registerPrompt("create_task", {
15
+ title: "Create Notion task",
16
+ description: "Create a new task page in Notion with optional status and due date.",
17
+ argsSchema: {
18
+ title: z.string().describe("Task title."),
19
+ status: z
20
+ .string()
21
+ .optional()
22
+ .describe("Status select value, e.g. Todo / In Progress / Done."),
23
+ due: z
24
+ .string()
25
+ .optional()
26
+ .describe("Due date as ISO 8601 (YYYY-MM-DD)."),
27
+ },
28
+ }, ({ title, status, due }) => {
29
+ const propLines = [`- title: ${JSON.stringify(title)}`];
30
+ if (status)
31
+ propLines.push(`- status: ${JSON.stringify(status)}`);
32
+ if (due)
33
+ propLines.push(`- due: ${JSON.stringify(due)}`);
34
+ return userMessage([
35
+ `Create a new Notion task page with these fields:`,
36
+ ...propLines,
37
+ ``,
38
+ `Steps:`,
39
+ `1. If you don't already know which database holds tasks, call notion_execute with operation "search_pages" (or query_database against a known tasks DB) to locate it.`,
40
+ `2. Call notion_execute with operation "create_page" and a payload that sets parent.database_id (or data_source_id) plus a properties object containing Title=${JSON.stringify(title)}${status ? `, Status=${JSON.stringify(status)}` : ""}${due ? `, Due=${JSON.stringify(due)}` : ""}.`,
41
+ `3. Return the new page url to the user.`,
42
+ ].join("\n"));
43
+ });
44
+ server.registerPrompt("weekly_review", {
45
+ title: "Weekly review of completed work",
46
+ description: "Summarize tasks marked Done in the last 7 days from a Notion database.",
47
+ argsSchema: {},
48
+ }, () => userMessage([
49
+ `Generate a weekly review of completed Notion tasks.`,
50
+ ``,
51
+ `Steps:`,
52
+ `1. Identify the tasks database (ask the user if you don't know its id).`,
53
+ `2. Call notion_execute with operation "query_database" using a filter for Status=Done AND Last edited time (or Created time) on_or_after the date 7 days ago. Sort by last_edited_time descending.`,
54
+ `3. Summarize the results grouped by theme or project, with bullet points and links to each page.`,
55
+ ].join("\n")));
56
+ server.registerPrompt("find_pages", {
57
+ title: "Find Notion pages by query",
58
+ description: "Search Notion and show the top 5 matching pages.",
59
+ argsSchema: {
60
+ query: z.string().describe("Text to search for across page titles."),
61
+ },
62
+ }, ({ query }) => userMessage([
63
+ `Find Notion pages matching ${JSON.stringify(query)}.`,
64
+ ``,
65
+ `Steps:`,
66
+ `1. Call notion_execute with operation "search_pages" and payload { "query": ${JSON.stringify(query)} }.`,
67
+ `2. Take the top 5 results and present them as a numbered list with each page's title and url.`,
68
+ `3. If there are no results, say so plainly.`,
69
+ ].join("\n")));
70
+ server.registerPrompt("daily_log", {
71
+ title: "Append to daily log",
72
+ description: "Append a timestamped paragraph to a daily-log page in Notion.",
73
+ argsSchema: {
74
+ date: z
75
+ .string()
76
+ .optional()
77
+ .describe("Date for the entry as ISO 8601 (defaults to today)."),
78
+ content: z
79
+ .string()
80
+ .optional()
81
+ .describe("Text to append; prompt the user if omitted."),
82
+ },
83
+ }, ({ date, content }) => {
84
+ const pageId = process.env.NOTION_DAILY_LOG_PAGE_ID;
85
+ const pageLine = pageId
86
+ ? `The daily-log page id is ${pageId} (from NOTION_DAILY_LOG_PAGE_ID).`
87
+ : `NOTION_DAILY_LOG_PAGE_ID is not set — ask the user for the daily-log page id or search for it.`;
88
+ const dateLine = date ? `Use the date ${date}.` : `Use today's date.`;
89
+ const contentLine = content
90
+ ? `Append this content: ${JSON.stringify(content)}.`
91
+ : `Ask the user what to log if the content is not yet known.`;
92
+ return userMessage([
93
+ `Append a timestamped paragraph to the user's daily-log page.`,
94
+ ``,
95
+ pageLine,
96
+ dateLine,
97
+ contentLine,
98
+ ``,
99
+ `Steps:`,
100
+ `1. Compose a paragraph block prefixed with the timestamp (date + current time).`,
101
+ `2. Call notion_execute with operation "append_blocks" and a payload of { "block_id": "<daily-log page id>", "children": [{ "type": "paragraph", "paragraph": { "rich_text": [{ "type": "text", "text": { "content": "<timestamp> — <content>" } }] } }] }.`,
102
+ `3. Report back the appended block's id.`,
103
+ ].join("\n"));
104
+ });
105
+ }
@@ -59,6 +59,23 @@ export const HEADING3_BLOCK_REQUEST_SCHEMA = BASE_BLOCK_REQUEST_SCHEMA.extend({
59
59
  .describe("Whether heading can be toggled"),
60
60
  }).describe("Heading 3 block content"),
61
61
  });
62
+ export const HEADING4_BLOCK_REQUEST_SCHEMA = BASE_BLOCK_REQUEST_SCHEMA.extend({
63
+ type: z.literal("heading_4").describe("Heading 4 block type"),
64
+ heading_4: TEXT_BLOCK_BASE_REQUEST_SCHEMA.extend({
65
+ is_toggleable: z
66
+ .boolean()
67
+ .optional()
68
+ .describe("Whether heading can be toggled"),
69
+ }).describe("Heading 4 block content"),
70
+ });
71
+ export const TAB_BLOCK_REQUEST_SCHEMA = BASE_BLOCK_REQUEST_SCHEMA.extend({
72
+ type: z.literal("tab").describe("Tab block type"),
73
+ tab: z
74
+ .object({
75
+ icon: ICON_SCHEMA.optional(),
76
+ })
77
+ .describe("Tab block content"),
78
+ });
62
79
  export const QUOTE_BLOCK_REQUEST_SCHEMA = BASE_BLOCK_REQUEST_SCHEMA.extend({
63
80
  type: z.literal("quote").describe("Quote block type"),
64
81
  quote: TEXT_BLOCK_BASE_REQUEST_SCHEMA.describe("Quote block content"),
@@ -119,6 +136,7 @@ export const TEXT_BLOCK_REQUEST_SCHEMA = z.preprocess(preprocessJson, z
119
136
  HEADING1_BLOCK_REQUEST_SCHEMA,
120
137
  HEADING2_BLOCK_REQUEST_SCHEMA,
121
138
  HEADING3_BLOCK_REQUEST_SCHEMA,
139
+ HEADING4_BLOCK_REQUEST_SCHEMA,
122
140
  QUOTE_BLOCK_REQUEST_SCHEMA,
123
141
  CALLOUT_BLOCK_REQUEST_SCHEMA,
124
142
  TOGGLE_BLOCK_REQUEST_SCHEMA,
@@ -128,82 +146,6 @@ export const TEXT_BLOCK_REQUEST_SCHEMA = z.preprocess(preprocessJson, z
128
146
  CODE_BLOCK_REQUEST_SCHEMA,
129
147
  DIVIDER_BLOCK_REQUEST_SCHEMA,
130
148
  IMAGE_BLOCK_REQUEST_SCHEMA,
149
+ TAB_BLOCK_REQUEST_SCHEMA,
131
150
  ])
132
151
  .describe("Union of all possible text block request types"));
133
- export const APPEND_BLOCK_CHILDREN_SCHEMA = {
134
- blockId: z.string().describe("The ID of the block to append children to"),
135
- children: z
136
- .array(TEXT_BLOCK_REQUEST_SCHEMA)
137
- .describe("Array of blocks to append as children"),
138
- };
139
- export const RETRIEVE_BLOCK_SCHEMA = {
140
- blockId: z.string().describe("The ID of the block to retrieve"),
141
- };
142
- export const RETRIEVE_BLOCK_CHILDREN_SCHEMA = {
143
- blockId: z.string().describe("The ID of the block to retrieve children for"),
144
- start_cursor: z.string().optional().describe("Cursor for pagination"),
145
- page_size: z
146
- .number()
147
- .min(1)
148
- .max(100)
149
- .optional()
150
- .describe("Number of results to return (1-100)"),
151
- };
152
- export const UPDATE_BLOCK_SCHEMA = {
153
- blockId: z.string().describe("The ID of the block to update"),
154
- data: TEXT_BLOCK_REQUEST_SCHEMA.describe("The block data to update"),
155
- };
156
- export const DELETE_BLOCK_SCHEMA = {
157
- blockId: z.string().describe("The ID of the block to delete/archive"),
158
- };
159
- // Batch operation schemas for multiple blocks
160
- export const BATCH_APPEND_BLOCK_CHILDREN_SCHEMA = {
161
- operations: z
162
- .array(z.object({
163
- blockId: z
164
- .string()
165
- .describe("The ID of the block to append children to"),
166
- children: z
167
- .array(TEXT_BLOCK_REQUEST_SCHEMA)
168
- .describe("Array of blocks to append as children"),
169
- }))
170
- .describe("Array of append operations to perform in a single batch"),
171
- };
172
- export const BATCH_UPDATE_BLOCKS_SCHEMA = {
173
- operations: z
174
- .array(z.object({
175
- blockId: z.string().describe("The ID of the block to update"),
176
- data: TEXT_BLOCK_REQUEST_SCHEMA.describe("The block data to update"),
177
- }))
178
- .describe("Array of update operations to perform in a single batch"),
179
- };
180
- export const BATCH_DELETE_BLOCKS_SCHEMA = {
181
- blockIds: z
182
- .array(z.string().describe("The ID of a block to delete/archive"))
183
- .describe("Array of block IDs to delete in a single batch"),
184
- };
185
- // Schema for multi-operation batches (mixed operations)
186
- export const BATCH_MIXED_OPERATIONS_SCHEMA = {
187
- operations: z
188
- .array(z.discriminatedUnion("operation", [
189
- z.object({
190
- operation: z.literal("append"),
191
- blockId: z
192
- .string()
193
- .describe("The ID of the block to append children to"),
194
- children: z
195
- .array(TEXT_BLOCK_REQUEST_SCHEMA)
196
- .describe("Array of blocks to append as children"),
197
- }),
198
- z.object({
199
- operation: z.literal("update"),
200
- blockId: z.string().describe("The ID of the block to update"),
201
- data: TEXT_BLOCK_REQUEST_SCHEMA.describe("The block data to update"),
202
- }),
203
- z.object({
204
- operation: z.literal("delete"),
205
- blockId: z.string().describe("The ID of the block to delete/archive"),
206
- }),
207
- ]))
208
- .describe("Array of mixed operations to perform in a single batch"),
209
- };
@@ -1,11 +1,6 @@
1
1
  import { z } from "zod";
2
- import { ICON_SCHEMA } from "./icon.js";
3
- import { FILE_SCHEMA } from "./file.js";
4
- import { PARENT_SCHEMA } from "./page.js";
5
- import { RICH_TEXT_ITEM_REQUEST_SCHEMA, TEXT_CONTENT_REQUEST_SCHEMA, TEXT_RICH_TEXT_ITEM_REQUEST_SCHEMA, } from "./rich-text.js";
6
2
  import { preprocessJson } from "./preprocess.js";
7
3
  import { NUMBER_FORMAT } from "./number.js";
8
- import { getRootPageId } from "../services/notion.js";
9
4
  export const EMPTY_OBJECT_SCHEMA = z.record(z.string(), z.never()).default({});
10
5
  export const SELECT_COLOR_SCHEMA = z.enum([
11
6
  "default",
@@ -19,14 +14,6 @@ export const SELECT_COLOR_SCHEMA = z.enum([
19
14
  "pink",
20
15
  "red",
21
16
  ]);
22
- // Title property for database creation
23
- export const TITLE_PROPERTY_SCHEMA = z.object({
24
- title: z
25
- .array(z.object({
26
- text: TEXT_CONTENT_REQUEST_SCHEMA.describe("Text content for title segment"),
27
- }))
28
- .describe("Array of text segments that make up the title"),
29
- });
30
17
  // Database property schemas
31
18
  // 1. Title property
32
19
  export const TITLE_DB_PROPERTY_SCHEMA = z.object({
@@ -242,6 +229,30 @@ export const STATUS_DB_PROPERTY_SCHEMA = z.object({
242
229
  .describe("Status property configuration"),
243
230
  description: z.string().optional().describe("Property description"),
244
231
  });
232
+ export const BUTTON_DB_PROPERTY_SCHEMA = z.object({
233
+ type: z.literal("button"),
234
+ button: EMPTY_OBJECT_SCHEMA,
235
+ description: z.string().optional(),
236
+ });
237
+ export const UNIQUE_ID_DB_PROPERTY_SCHEMA = z.object({
238
+ type: z.literal("unique_id"),
239
+ unique_id: z
240
+ .object({
241
+ prefix: z
242
+ .string()
243
+ .regex(/^[A-Za-z][A-Za-z0-9-]{1,9}$/, "prefix must start with a letter, then 1–9 more letters/digits/hyphens (total length 2–10)")
244
+ .nullable()
245
+ .optional()
246
+ .describe("Prefix shown before the auto-incrementing number. 2–10 chars total, must start with a letter, alphanumeric + hyphen only. Single-letter prefixes are rejected by Notion."),
247
+ })
248
+ .describe("Unique ID property configuration"),
249
+ description: z.string().optional(),
250
+ });
251
+ export const VERIFICATION_DB_PROPERTY_SCHEMA = z.object({
252
+ type: z.literal("verification"),
253
+ verification: EMPTY_OBJECT_SCHEMA,
254
+ description: z.string().optional(),
255
+ });
245
256
  // Combined database property schema
246
257
  export const DATABASE_PROPERTY_SCHEMA = z.preprocess(preprocessJson, z
247
258
  .discriminatedUnion("type", [
@@ -264,78 +275,8 @@ export const DATABASE_PROPERTY_SCHEMA = z.preprocess(preprocessJson, z
264
275
  CREATED_BY_DB_PROPERTY_SCHEMA,
265
276
  LAST_EDITED_TIME_DB_PROPERTY_SCHEMA,
266
277
  LAST_EDITED_BY_DB_PROPERTY_SCHEMA,
278
+ BUTTON_DB_PROPERTY_SCHEMA,
279
+ UNIQUE_ID_DB_PROPERTY_SCHEMA,
280
+ VERIFICATION_DB_PROPERTY_SCHEMA,
267
281
  ])
268
282
  .describe("Union of all possible database property types"));
269
- // Create database schema
270
- export const CREATE_DATABASE_SCHEMA = {
271
- parent: PARENT_SCHEMA.optional()
272
- .default({
273
- type: "page_id",
274
- page_id: getRootPageId(),
275
- })
276
- .describe("Optional parent - if not provided, will use NOTION_PAGE_ID as parent page"),
277
- title: z.array(TEXT_RICH_TEXT_ITEM_REQUEST_SCHEMA).describe("Database title"),
278
- description: z
279
- .array(TEXT_RICH_TEXT_ITEM_REQUEST_SCHEMA)
280
- .optional()
281
- .describe("Database description"),
282
- properties: z
283
- .record(z.string().describe("Property name"), DATABASE_PROPERTY_SCHEMA.describe("Property schema"))
284
- .describe("Database properties"),
285
- is_inline: z
286
- .boolean()
287
- .optional()
288
- .default(false)
289
- .describe("Whether database is inline"),
290
- icon: z.preprocess(preprocessJson, ICON_SCHEMA.nullable().optional().describe("Optional icon for the database")),
291
- cover: z.preprocess(preprocessJson, FILE_SCHEMA.nullable()
292
- .optional()
293
- .describe("Optional cover image for the database")),
294
- };
295
- // Query database schema
296
- export const QUERY_DATABASE_SCHEMA = {
297
- database_id: z.string().describe("The ID of the database to query"),
298
- filter: z
299
- .preprocess(preprocessJson, z.any())
300
- .optional()
301
- .describe("Filter criteria for the query"),
302
- sorts: z
303
- .array(z.object({
304
- property: z.string().optional().describe("Property to sort by"),
305
- timestamp: z
306
- .enum(["created_time", "last_edited_time"])
307
- .describe("Timestamp to sort by"),
308
- direction: z
309
- .enum(["ascending", "descending"])
310
- .describe("Sort direction"),
311
- }))
312
- .optional()
313
- .describe("Sort criteria for the query"),
314
- start_cursor: z.string().optional().describe("Cursor for pagination"),
315
- page_size: z
316
- .number()
317
- .min(1)
318
- .max(100)
319
- .optional()
320
- .describe("Number of results to return (1-100)"),
321
- };
322
- // Update database schema
323
- export const UPDATE_DATABASE_SCHEMA = {
324
- database_id: z.string().describe("The ID of the database to update"),
325
- title: z
326
- .array(RICH_TEXT_ITEM_REQUEST_SCHEMA)
327
- .optional()
328
- .describe("Updated database title"),
329
- description: z
330
- .array(RICH_TEXT_ITEM_REQUEST_SCHEMA)
331
- .optional()
332
- .describe("Updated database description"),
333
- properties: z
334
- .record(z.string().describe("Property name"), DATABASE_PROPERTY_SCHEMA.describe("Property schema"))
335
- .describe("Properties of the page"),
336
- is_inline: z.boolean().optional().describe("Whether database is inline"),
337
- icon: z.preprocess(preprocessJson, ICON_SCHEMA.nullable().optional().describe("Updated icon for the database")),
338
- cover: z.preprocess(preprocessJson, FILE_SCHEMA.nullable()
339
- .optional()
340
- .describe("Updated cover image for the database")),
341
- };
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ const SHARED_REFS = {};
3
+ export function registerSharedRef(name, schema) {
4
+ SHARED_REFS[name] = schema;
5
+ }
6
+ export function emitJsonSchema(schema) {
7
+ const raw = z.toJSONSchema(schema, {
8
+ target: "draft-7",
9
+ unrepresentable: "any",
10
+ });
11
+ return hoistSharedRefs(raw);
12
+ }
13
+ function hoistSharedRefs(root) {
14
+ const sharedSchemas = {};
15
+ for (const [name, zodSchema] of Object.entries(SHARED_REFS)) {
16
+ const raw = z.toJSONSchema(zodSchema, {
17
+ target: "draft-7",
18
+ unrepresentable: "any",
19
+ });
20
+ // Root-level emission adds `$schema`, but nested inline usages don't carry it.
21
+ // Strip it so the equality key matches inline sites.
22
+ const { $schema: _drop, ...rest } = raw;
23
+ void _drop;
24
+ sharedSchemas[name] = rest;
25
+ }
26
+ const refByJson = new Map();
27
+ for (const [name, json] of Object.entries(sharedSchemas)) {
28
+ refByJson.set(stableStringify(json), name);
29
+ }
30
+ let usedNames = new Set();
31
+ function walk(node) {
32
+ if (Array.isArray(node))
33
+ return node.map(walk);
34
+ if (node === null || typeof node !== "object")
35
+ return node;
36
+ const obj = node;
37
+ const key = stableStringify(obj);
38
+ const match = refByJson.get(key);
39
+ if (match) {
40
+ usedNames.add(match);
41
+ return { $ref: `#/$defs/${match}` };
42
+ }
43
+ const out = {};
44
+ for (const [k, v] of Object.entries(obj))
45
+ out[k] = walk(v);
46
+ return out;
47
+ }
48
+ const walked = walk(root);
49
+ if (usedNames.size === 0)
50
+ return walked;
51
+ const defs = walked.$defs ?? {};
52
+ for (const name of usedNames) {
53
+ if (!(name in defs))
54
+ defs[name] = walk(sharedSchemas[name]);
55
+ }
56
+ return { ...walked, $defs: defs };
57
+ }
58
+ function stableStringify(value) {
59
+ if (value === null || typeof value !== "object")
60
+ return JSON.stringify(value);
61
+ if (Array.isArray(value))
62
+ return "[" + value.map(stableStringify).join(",") + "]";
63
+ const obj = value;
64
+ const keys = Object.keys(obj).sort();
65
+ return ("{" +
66
+ keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") +
67
+ "}");
68
+ }
@@ -3,7 +3,7 @@ export const FILE_SCHEMA = z
3
3
  .object({
4
4
  external: z
5
5
  .object({
6
- url: z.string().url().describe("URL of the external file"),
6
+ url: z.url({ protocol: /^https?$/ }).describe("URL of the external file"),
7
7
  })
8
8
  .describe("External file source"),
9
9
  type: z.literal("external").describe("Type of file source"),