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
package/build/tools/index.js
CHANGED
|
@@ -1,23 +1,121 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { server } from "../server/index.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
3
|
+
import { initOperations, getOperation, listOperations, operationNames } from "../operations/index.js";
|
|
4
|
+
import { dispatch } from "../dispatch/index.js";
|
|
5
|
+
import { emitJsonSchema } from "../schema/emit.js";
|
|
6
|
+
import { registerAllPrompts } from "../prompts/index.js";
|
|
7
|
+
function jsonContent(value) {
|
|
8
|
+
// Compact JSON keeps the wire response small. Agents parse JSON either way,
|
|
9
|
+
// and the ~30% bloat from indentation isn't worth paying for in every reply.
|
|
10
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
11
|
+
return { content: [{ type: "text", text }] };
|
|
12
|
+
}
|
|
13
|
+
function errorContent(value) {
|
|
14
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
15
|
+
return { isError: true, content: [{ type: "text", text }] };
|
|
16
|
+
}
|
|
17
|
+
const EXECUTE_INPUT = {
|
|
18
|
+
operation: z
|
|
19
|
+
.string()
|
|
20
|
+
.describe("Operation name. See notion_describe for the schema of any operation, or read the notion://operations resource for the full menu. Common ops: set_page_title, append_blocks, get_page, search_pages, query_database."),
|
|
21
|
+
payload: z
|
|
22
|
+
.record(z.string(), z.unknown())
|
|
23
|
+
.describe("Operation parameters. Pass either single-op fields directly, or { items: [...], atomic?, idempotency_key?, concurrency? } for batch."),
|
|
23
24
|
};
|
|
25
|
+
const DESCRIBE_INPUT = {
|
|
26
|
+
operation: z.string().describe("Operation name to describe."),
|
|
27
|
+
};
|
|
28
|
+
const EXECUTE_DESCRIPTION = `Execute a Notion operation by name.
|
|
29
|
+
|
|
30
|
+
Two ways to call:
|
|
31
|
+
• Single: { operation: "set_page_title", payload: { page_id, title } }
|
|
32
|
+
• Batch: { operation: "set_page_title", payload: { items: [{page_id, title}, ...], atomic?: false, idempotency_key?: "...", concurrency?: 3 } }
|
|
33
|
+
|
|
34
|
+
If the payload is malformed, the error response includes the full schema + a working example so you can correct and retry in one round-trip. Call notion_describe(operation) ahead of time only for complex shapes (query_database filters, batch_mixed_blocks).
|
|
35
|
+
|
|
36
|
+
Most responses are slimmed by default. Pass verbose:true inside payload (single) or per-item (batch) to get the raw Notion SDK response.`;
|
|
37
|
+
const DESCRIBE_DESCRIPTION = `Return the JSON Schema and a working example for one operation. Use this BEFORE notion_execute when the payload shape is non-trivial (query filters, structured block trees, database property definitions). For simple ops, just call notion_execute — its errors carry the schema.`;
|
|
38
|
+
export async function registerAllTools() {
|
|
39
|
+
await initOperations();
|
|
40
|
+
server.registerTool("notion_execute", {
|
|
41
|
+
title: "Notion Execute",
|
|
42
|
+
description: EXECUTE_DESCRIPTION,
|
|
43
|
+
inputSchema: EXECUTE_INPUT,
|
|
44
|
+
annotations: {
|
|
45
|
+
title: "Notion Execute",
|
|
46
|
+
readOnlyHint: false,
|
|
47
|
+
destructiveHint: true,
|
|
48
|
+
openWorldHint: true,
|
|
49
|
+
},
|
|
50
|
+
}, async ({ operation, payload }) => {
|
|
51
|
+
const result = await dispatch(operation, payload);
|
|
52
|
+
// Batch results (with per-item results) always go back as structured data —
|
|
53
|
+
// a partial success is a normal outcome of the tool, not a tool error.
|
|
54
|
+
const isBatch = typeof result === "object" && result !== null && "summary" in result;
|
|
55
|
+
if (isBatch || result.ok)
|
|
56
|
+
return jsonContent(result);
|
|
57
|
+
return errorContent(result);
|
|
58
|
+
});
|
|
59
|
+
server.registerTool("notion_describe", {
|
|
60
|
+
title: "Notion Describe",
|
|
61
|
+
description: DESCRIBE_DESCRIPTION,
|
|
62
|
+
inputSchema: DESCRIBE_INPUT,
|
|
63
|
+
annotations: {
|
|
64
|
+
title: "Notion Describe",
|
|
65
|
+
readOnlyHint: true,
|
|
66
|
+
destructiveHint: false,
|
|
67
|
+
openWorldHint: false,
|
|
68
|
+
},
|
|
69
|
+
}, async ({ operation }) => {
|
|
70
|
+
const def = getOperation(operation);
|
|
71
|
+
if (!def) {
|
|
72
|
+
return errorContent({
|
|
73
|
+
ok: false,
|
|
74
|
+
error: {
|
|
75
|
+
code: "unknown_operation",
|
|
76
|
+
message: `Unknown operation: "${operation}".`,
|
|
77
|
+
fix: `Available: ${operationNames().join(", ")}`,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return jsonContent({
|
|
82
|
+
name: def.name,
|
|
83
|
+
description: def.description,
|
|
84
|
+
batchable: def.batchable,
|
|
85
|
+
schema: emitJsonSchema(def.schema),
|
|
86
|
+
example: def.example,
|
|
87
|
+
...(def.exampleBatch ? { example_batch: def.exampleBatch } : {}),
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
// Cheat-sheet resource: a markdown table of every operation
|
|
91
|
+
server.registerResource("operations-index", "notion://operations", {
|
|
92
|
+
title: "Notion operations index",
|
|
93
|
+
description: "Markdown table of every supported operation, batchability, and one-line description.",
|
|
94
|
+
mimeType: "text/markdown",
|
|
95
|
+
}, async () => ({
|
|
96
|
+
contents: [
|
|
97
|
+
{
|
|
98
|
+
uri: "notion://operations",
|
|
99
|
+
mimeType: "text/markdown",
|
|
100
|
+
text: renderOperationsIndex(),
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
}));
|
|
104
|
+
registerAllPrompts();
|
|
105
|
+
}
|
|
106
|
+
function renderOperationsIndex() {
|
|
107
|
+
const lines = [
|
|
108
|
+
"# Notion MCP — Operations",
|
|
109
|
+
"",
|
|
110
|
+
"Call `notion_execute({operation, payload})` with one of these. Use `notion_describe({operation})` for the full schema.",
|
|
111
|
+
"",
|
|
112
|
+
"| Operation | Batchable | Description |",
|
|
113
|
+
"| --- | --- | --- |",
|
|
114
|
+
];
|
|
115
|
+
for (const def of listOperations()) {
|
|
116
|
+
lines.push(`| \`${def.name}\` | ${def.batchable ? "yes" : "no"} | ${def.description} |`);
|
|
117
|
+
}
|
|
118
|
+
lines.push("", "## `query_database` WHERE DSL", "");
|
|
119
|
+
lines.push("`query_database.where` is a compact DSL that compiles to the Notion filter object. AND-by-default at the top level; nest `and`/`or`/`not` (case-insensitive — `AND`/`OR`/`NOT` also work) for boolean groups, prefix scalars with `__type` to force the property type, or fall back to raw `filter` for anything the DSL can't express.", "", "Common shapes:", "", "```jsonc", "// Single equality (property type inferred from value, or from data source schema via __type):", "{ \"where\": { \"Status\": \"Open\" } }", "", "// AND of multiple properties (top-level keys are implicit AND):", "{ \"where\": { \"Status\": \"Done\", \"Done\": true } }", "", "// Explicit operator on one property:", "{ \"where\": { \"Priority\": { \"gte\": 3 } } }", "", "// Boolean groups (lowercase or uppercase — both work):", "{ \"where\": { \"or\": [ { \"Status\": \"Open\" }, { \"Status\": \"In progress\" } ] } }", "{ \"where\": { \"and\": [ { \"Status\": \"Done\" }, { \"Priority\": { \"gte\": 5 } } ] } }", "{ \"where\": { \"not\": { \"Status\": \"Done\" } } }", "", "// in / notIn fan out to OR / AND of equals:", "{ \"where\": { \"Status\": { \"in\": [\"Open\", \"In progress\"] } } }", "", "// Force property type when value shape is ambiguous (e.g. a string that's actually a multi_select tag):", "{ \"where\": { \"Tags\": { \"__type\": \"multi_select\", \"eq\": \"alpha\" } } }", "{ \"where\": { \"Created\": { \"__type\": \"date\", \"on_or_after\": \"2026-01-01\" } } }", "```", "", "If a column is literally named `and`/`or`/`not`, wrap it as an operator object (e.g. `{ \"and\": { \"__type\": \"select\", \"eq\": \"x\" } }`) so it isn't parsed as a combinator. For anything the DSL can't express, pass `filter` (raw Notion filter object) instead of `where`.");
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
package/build/utils/error.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
2
1
|
import { APIResponseError } from "@notionhq/client";
|
|
2
|
+
import { AuthError } from "../services/auth.js";
|
|
3
3
|
/**
|
|
4
4
|
* Error codes from Notion API
|
|
5
5
|
* @see https://developers.notion.com/reference/status-codes#error-codes
|
|
6
6
|
*/
|
|
7
7
|
export var NotionErrorCode;
|
|
8
8
|
(function (NotionErrorCode) {
|
|
9
|
-
// 400 errors
|
|
10
9
|
NotionErrorCode["InvalidJson"] = "invalid_json";
|
|
11
10
|
NotionErrorCode["InvalidRequestUrl"] = "invalid_request_url";
|
|
12
11
|
NotionErrorCode["InvalidRequest"] = "invalid_request";
|
|
@@ -16,112 +15,152 @@ export var NotionErrorCode;
|
|
|
16
15
|
NotionErrorCode["UnsupportedExport"] = "unsupported_export";
|
|
17
16
|
NotionErrorCode["UnsupportedJsonType"] = "unsupported_json_type";
|
|
18
17
|
NotionErrorCode["UnsupportedJsonKey"] = "unsupported_json_key";
|
|
19
|
-
// 401 errors
|
|
20
18
|
NotionErrorCode["Unauthorized"] = "unauthorized";
|
|
21
19
|
NotionErrorCode["InvalidApiKey"] = "invalid_api_key";
|
|
22
|
-
// 403 errors
|
|
23
20
|
NotionErrorCode["RestrictedResource"] = "restricted_resource";
|
|
24
21
|
NotionErrorCode["InsufficientPermissions"] = "insufficient_permissions";
|
|
25
|
-
// 404 errors
|
|
26
22
|
NotionErrorCode["ObjectNotFound"] = "object_not_found";
|
|
27
|
-
// 409 errors
|
|
28
23
|
NotionErrorCode["ConflictError"] = "conflict_error";
|
|
29
24
|
NotionErrorCode["AlreadyExists"] = "already_exists";
|
|
30
|
-
// 429 errors
|
|
31
25
|
NotionErrorCode["RateLimited"] = "rate_limited";
|
|
32
|
-
// 500 errors
|
|
33
26
|
NotionErrorCode["InternalServerError"] = "internal_server_error";
|
|
34
|
-
// 503 errors
|
|
35
27
|
NotionErrorCode["ServiceUnavailable"] = "service_unavailable";
|
|
28
|
+
NotionErrorCode["GatewayTimeout"] = "gateway_timeout";
|
|
36
29
|
NotionErrorCode["DatabaseConnectionUnavailable"] = "database_connection_unavailable";
|
|
37
30
|
})(NotionErrorCode || (NotionErrorCode = {}));
|
|
38
31
|
/**
|
|
39
|
-
*
|
|
32
|
+
* Codes that indicate a transient failure — the request either never ran or
|
|
33
|
+
* the server is asking us to back off. Safe to retry with exponential backoff.
|
|
34
|
+
* Shared by the dispatch retry wrapper and any caller that wants to classify
|
|
35
|
+
* an error envelope without re-encoding the same rules.
|
|
40
36
|
*/
|
|
37
|
+
export const RETRYABLE_NOTION_CODES = new Set([
|
|
38
|
+
NotionErrorCode.RateLimited,
|
|
39
|
+
NotionErrorCode.InternalServerError,
|
|
40
|
+
NotionErrorCode.ServiceUnavailable,
|
|
41
|
+
NotionErrorCode.GatewayTimeout,
|
|
42
|
+
NotionErrorCode.DatabaseConnectionUnavailable,
|
|
43
|
+
]);
|
|
44
|
+
export function isRetryableNotionCode(code) {
|
|
45
|
+
return code !== undefined && RETRYABLE_NOTION_CODES.has(code);
|
|
46
|
+
}
|
|
41
47
|
const ERROR_MESSAGES = {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
[NotionErrorCode.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
[NotionErrorCode.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
[NotionErrorCode.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
[NotionErrorCode.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
[NotionErrorCode.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
[NotionErrorCode.
|
|
69
|
-
|
|
48
|
+
[NotionErrorCode.InvalidJson]: {
|
|
49
|
+
message: "The request body could not be decoded as JSON.",
|
|
50
|
+
fix: "Pass the payload as an object, not as a JSON-encoded string.",
|
|
51
|
+
},
|
|
52
|
+
[NotionErrorCode.InvalidRequestUrl]: {
|
|
53
|
+
message: "The request URL is not valid.",
|
|
54
|
+
},
|
|
55
|
+
[NotionErrorCode.InvalidRequest]: {
|
|
56
|
+
message: "This request is not supported.",
|
|
57
|
+
},
|
|
58
|
+
[NotionErrorCode.ValidationError]: {
|
|
59
|
+
message: "The request body does not match the schema for the expected parameters.",
|
|
60
|
+
fix: "Call notion_describe with this operation name to fetch the schema, then retry. Check the 'path' field on the error for the specific bad property.",
|
|
61
|
+
},
|
|
62
|
+
[NotionErrorCode.MissingVersion]: {
|
|
63
|
+
message: "The request is missing the required Notion-Version header.",
|
|
64
|
+
},
|
|
65
|
+
[NotionErrorCode.UnsupportedVersion]: {
|
|
66
|
+
message: "The specified Notion-Version is not supported.",
|
|
67
|
+
},
|
|
68
|
+
[NotionErrorCode.UnsupportedExport]: {
|
|
69
|
+
message: "The specified export type is not supported.",
|
|
70
|
+
},
|
|
71
|
+
[NotionErrorCode.UnsupportedJsonType]: {
|
|
72
|
+
message: "The specified JSON type is not supported.",
|
|
73
|
+
},
|
|
74
|
+
[NotionErrorCode.UnsupportedJsonKey]: {
|
|
75
|
+
message: "The specified JSON key is not supported.",
|
|
76
|
+
},
|
|
77
|
+
[NotionErrorCode.Unauthorized]: {
|
|
78
|
+
message: "The bearer token is not valid.",
|
|
79
|
+
fix: "Set the NOTION_TOKEN environment variable to a valid Notion integration token (starts with `ntn_` or `secret_`).",
|
|
80
|
+
},
|
|
81
|
+
[NotionErrorCode.InvalidApiKey]: {
|
|
82
|
+
message: "The API key is invalid.",
|
|
83
|
+
fix: "Generate a new internal integration token in Notion → Settings → Integrations → My integrations.",
|
|
84
|
+
},
|
|
85
|
+
[NotionErrorCode.RestrictedResource]: {
|
|
86
|
+
message: "The resource is restricted and cannot be accessed with this token.",
|
|
87
|
+
fix: "In Notion, open Settings → Connections → [your integration] → Capabilities and enable the missing scope (e.g. 'Read user information' for /users endpoints, 'Insert content' for block writes). For pages/databases, also confirm the integration is shared with the resource via the page's ••• → Add connections menu.",
|
|
88
|
+
},
|
|
89
|
+
[NotionErrorCode.InsufficientPermissions]: {
|
|
90
|
+
message: "The bearer token does not have permission to perform this operation.",
|
|
91
|
+
fix: "Open the integration in Notion → Settings → Connections and enable the missing capability, then share the target page/database with the integration.",
|
|
92
|
+
},
|
|
93
|
+
[NotionErrorCode.ObjectNotFound]: {
|
|
94
|
+
message: "The requested resource does not exist.",
|
|
95
|
+
fix: "Either the ID is wrong, the integration hasn't been shared with the page/database (use the page's ••• → Add connections menu in Notion), or the object is in trash.",
|
|
96
|
+
},
|
|
97
|
+
[NotionErrorCode.ConflictError]: {
|
|
98
|
+
message: "The transaction could not be completed due to a conflict.",
|
|
99
|
+
fix: "Retry the operation after a short delay.",
|
|
100
|
+
},
|
|
101
|
+
[NotionErrorCode.AlreadyExists]: {
|
|
102
|
+
message: "The resource already exists.",
|
|
103
|
+
},
|
|
104
|
+
[NotionErrorCode.RateLimited]: {
|
|
105
|
+
message: "The request was rate limited.",
|
|
106
|
+
fix: "Retry later with exponential backoff. Notion limits to ~3 requests per second per integration.",
|
|
107
|
+
},
|
|
108
|
+
[NotionErrorCode.InternalServerError]: {
|
|
109
|
+
message: "An unexpected error occurred on the Notion servers.",
|
|
110
|
+
fix: "Retry the operation; if it persists, check status.notion.so.",
|
|
111
|
+
},
|
|
112
|
+
[NotionErrorCode.ServiceUnavailable]: {
|
|
113
|
+
message: "The Notion service is unavailable.",
|
|
114
|
+
fix: "Retry later with exponential backoff.",
|
|
115
|
+
},
|
|
116
|
+
[NotionErrorCode.GatewayTimeout]: {
|
|
117
|
+
message: "The Notion gateway timed out before the request could complete.",
|
|
118
|
+
fix: "Retry later with exponential backoff.",
|
|
119
|
+
},
|
|
120
|
+
[NotionErrorCode.DatabaseConnectionUnavailable]: {
|
|
121
|
+
message: "The database connection is unavailable.",
|
|
122
|
+
fix: "Retry later with exponential backoff.",
|
|
123
|
+
},
|
|
70
124
|
};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
*/
|
|
74
|
-
function getErrorMessage(notionErrorCode, defaultMessage) {
|
|
75
|
-
return (ERROR_MESSAGES[notionErrorCode] ||
|
|
76
|
-
defaultMessage ||
|
|
77
|
-
"An unknown error occurred");
|
|
125
|
+
function lookup(code) {
|
|
126
|
+
return ERROR_MESSAGES[code] ?? { message: "An unknown error occurred." };
|
|
78
127
|
}
|
|
79
|
-
|
|
80
|
-
* Handles a Notion API error and returns an appropriate CallToolResult with error details
|
|
81
|
-
*/
|
|
82
|
-
export function handleNotionError(error) {
|
|
128
|
+
export function toErrorEnvelope(error) {
|
|
83
129
|
if (error instanceof APIResponseError) {
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
// Instead of mapping to specific error codes, just use InternalError as a fallback
|
|
130
|
+
const entry = lookup(error.code);
|
|
131
|
+
const body = error.body;
|
|
87
132
|
return {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
error: {
|
|
95
|
-
code: ErrorCode.InternalError,
|
|
96
|
-
message,
|
|
97
|
-
},
|
|
133
|
+
code: error.code,
|
|
134
|
+
message: entry.message + (error.message && error.message !== entry.message ? ` (${error.message})` : ""),
|
|
135
|
+
...(entry.fix ? { fix: entry.fix } : {}),
|
|
136
|
+
...(String(error.code) === NotionErrorCode.ValidationError && body
|
|
137
|
+
? { path: extractValidationPath(body) }
|
|
138
|
+
: {}),
|
|
98
139
|
};
|
|
99
140
|
}
|
|
100
|
-
if (error instanceof
|
|
141
|
+
if (error instanceof AuthError) {
|
|
101
142
|
return {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
text: `Error: ${error.message}`,
|
|
106
|
-
},
|
|
107
|
-
],
|
|
108
|
-
error: {
|
|
109
|
-
code: ErrorCode.InternalError,
|
|
110
|
-
message: error.message,
|
|
111
|
-
},
|
|
143
|
+
code: "auth_error",
|
|
144
|
+
message: `Notion auth failed: ${error.message}`,
|
|
145
|
+
fix: "Set NOTION_TOKEN env var, or configure OAuth credentials if using the auth gateway.",
|
|
112
146
|
};
|
|
113
147
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
148
|
+
if (error instanceof Error) {
|
|
149
|
+
return { code: "internal_error", message: error.message };
|
|
150
|
+
}
|
|
151
|
+
return { code: "unknown_error", message: String(error) };
|
|
152
|
+
}
|
|
153
|
+
function extractValidationPath(body) {
|
|
154
|
+
if (typeof body !== "object" || body === null)
|
|
155
|
+
return undefined;
|
|
156
|
+
const maybe = body.details;
|
|
157
|
+
if (!Array.isArray(maybe))
|
|
158
|
+
return undefined;
|
|
159
|
+
const first = maybe[0];
|
|
160
|
+
if (typeof first === "object" &&
|
|
161
|
+
first !== null &&
|
|
162
|
+
Array.isArray(first.path)) {
|
|
163
|
+
return first.path;
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
127
166
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { emitJsonSchema } from "../schema/emit.js";
|
|
2
|
+
import { sliceJsonSchema, summarizeSchema } from "./schema-slice.js";
|
|
3
|
+
// Top-level .refine() failures (XOR rules etc.) carry the whole-payload
|
|
4
|
+
// example as actionable guidance — the full JSON schema would only add
|
|
5
|
+
// noise. Skip the schema for those and any other top-level-only issue
|
|
6
|
+
// where the example is self-explanatory.
|
|
7
|
+
function shouldIncludeSchema(issues) {
|
|
8
|
+
return issues.some((i) => i.path.length > 0);
|
|
9
|
+
}
|
|
10
|
+
export function buildValidationError(def, zodError) {
|
|
11
|
+
const issues = zodError.issues.map((i) => ({
|
|
12
|
+
path: i.path,
|
|
13
|
+
message: i.message,
|
|
14
|
+
}));
|
|
15
|
+
const firstPath = issues[0]?.path ?? [];
|
|
16
|
+
const firstMsg = issues[0]?.message ?? "Validation failed";
|
|
17
|
+
const includeSchema = shouldIncludeSchema(zodError.issues);
|
|
18
|
+
// Slice the schema down to the failing field's subtree and summarize any
|
|
19
|
+
// unions so the envelope stays small — the unsliced schema for ops like
|
|
20
|
+
// set_page_property or update_database is 5-13KB.
|
|
21
|
+
let schemaForError;
|
|
22
|
+
if (includeSchema) {
|
|
23
|
+
const fullSchema = emitJsonSchema(def.schema);
|
|
24
|
+
const sliced = firstPath.length > 0 ? sliceJsonSchema(fullSchema, firstPath) : fullSchema;
|
|
25
|
+
schemaForError = summarizeSchema(sliced);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
code: "validation_error",
|
|
29
|
+
operation: def.name,
|
|
30
|
+
message: `${firstMsg}${firstPath.length ? ` at ${firstPath.join(".")}` : ""}`,
|
|
31
|
+
path: firstPath.length ? firstPath : undefined,
|
|
32
|
+
issues,
|
|
33
|
+
...(includeSchema ? { schema: schemaForError } : {}),
|
|
34
|
+
example: def.example,
|
|
35
|
+
...(def.exampleBatch ? { example_batch: def.exampleBatch } : {}),
|
|
36
|
+
fix: includeSchema
|
|
37
|
+
? "Match the example shape. The schema above shows the failing field; call notion_describe for the full operation schema. For batch mode, wrap items in { items: [...] }."
|
|
38
|
+
: "A working example is included above. Match the example shape and retry. For batch mode, wrap items in { items: [...] }.",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Bridge between Zod-validated request shapes and the SDK's strict
|
|
2
|
+
// discriminated-union request types. Zod can describe loose shapes
|
|
3
|
+
// (z.record, z.unknown), but `@notionhq/client` request types use
|
|
4
|
+
// branded discriminated unions that Zod inference can't preserve.
|
|
5
|
+
//
|
|
6
|
+
// Each helper below is a single typed boundary cast: the runtime payload
|
|
7
|
+
// has already been validated by Zod; the cast only tells TypeScript what
|
|
8
|
+
// the SDK expects on the wire. Prefer these over `as never` so the cast
|
|
9
|
+
// target stays visible and grep-able.
|
|
10
|
+
/**
|
|
11
|
+
* Cast a Zod-validated payload to its SDK request shape. The runtime value
|
|
12
|
+
* has already been narrowed by the schema; this only widens the static type.
|
|
13
|
+
*/
|
|
14
|
+
export function asSdk(value) {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Walks a Notion-style paginated endpoint until the server says has_more=false
|
|
2
|
+
// or we've fetched `limit` pages (whichever comes first). The cap protects
|
|
3
|
+
// against pathologically long walks — the caller surfaces `truncated:true` so
|
|
4
|
+
// the user can re-call with a higher `page_limit` or a narrower query.
|
|
5
|
+
//
|
|
6
|
+
// `limit` is in PAGES, not items: a fetch-page typically returns up to 100
|
|
7
|
+
// items, so the default of 10 pages caps at ~1000 items.
|
|
8
|
+
export const DEFAULT_PAGE_LIMIT = 10;
|
|
9
|
+
export async function paginateAll(fetchPage, opts = {}) {
|
|
10
|
+
const limit = opts.limit ?? DEFAULT_PAGE_LIMIT;
|
|
11
|
+
const merged = [];
|
|
12
|
+
let cursor = undefined;
|
|
13
|
+
let pages_walked = 0;
|
|
14
|
+
let truncated = false;
|
|
15
|
+
while (true) {
|
|
16
|
+
const page = await fetchPage(cursor);
|
|
17
|
+
pages_walked += 1;
|
|
18
|
+
merged.push(...page.results);
|
|
19
|
+
if (!page.has_more)
|
|
20
|
+
break;
|
|
21
|
+
if (!page.next_cursor) {
|
|
22
|
+
// Notion shouldn't return has_more=true with null next_cursor, but if it
|
|
23
|
+
// does we can't continue — surface truncation so the caller can re-query
|
|
24
|
+
// or report the gap rather than silently treating it as complete.
|
|
25
|
+
truncated = true;
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
if (pages_walked >= limit) {
|
|
29
|
+
truncated = true;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
cursor = page.next_cursor;
|
|
33
|
+
}
|
|
34
|
+
return { results: merged, truncated, pages_walked };
|
|
35
|
+
}
|