Skip to content

Commit 00f82e1

Browse files
dannyrooseveltclaudeMichelle Bergeronmichelle0927
authored
notion: AI-optimize MCP actions (flatten schemas, consolidate, enrich descriptions) (#21274)
* notion: AI-optimize MCP actions (flatten schemas, consolidate, enrich descriptions) Modernize the Notion action set for MCP/LLM use (11 tools): - Flatten additionalProps/reloadProps to static schemas: create-page, update-page, append-block, create-database, update-database - Consolidate create-page + create-page-from-database into Create Page (parent accepts a page or data-source ID; Markdown content) - Consolidate retrieve-page + retrieve-block into Get Page (returns properties + Markdown content) - Rewrite search standalone; compact results; document the 2025-09-03 data-sources model and the ID-resolution role - query-database: omit empty filter/sorts (Notion API rejects null), correct readOnlyHint to true - Behavioral descriptions with bold cross-references; accept ID or URL - Shared helpers in common/utils.mjs: extractNotionId, parsePropertiesObject, normalizeDatabaseSchema - Bump @pipedream/notion 1.0.11 -> 1.0.12 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * notion: bump versions of components depending on common/utils.mjs The version-check CI requires every component whose (transitive) dependencies changed to bump its version. These 9 legacy actions pull in common/utils.mjs via base-page-builder, so bump their patch versions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * notion: address CodeRabbit review - base-page-builder `_filterProps`: check key presence instead of truthiness so valid falsy property values (0, false, "") are preserved instead of silently dropped (affects create-page / update-page). - create-page: narrow the parent-type probe catch to 404/object_not_found only — re-throw auth/permission/rate-limit/network errors instead of falling through to the page path; chunk subpage Markdown into MAX_BLOCKS appends like the data-source path. - query-database: `sorts` is now a JSON string (was string[], which contradicted the description); add `start_cursor` pagination; fix the doc-link text to "[See the documentation]". - search: add `start_cursor` pagination. - append-block: inline Markdown example on `content`. - create-comment: show the expected `discussionId` format inline. - utils: `parsePropertiesObject` / `normalizeDatabaseSchema` now reject non-object JSON (arrays/strings/null) with a clear error instead of forwarding invalid payloads to Notion. - Version bumps for breaking prop-contract changes (major): search 1.0.0, create-page 1.0.0, append-block 1.0.0, create-database 1.0.0, update-database 2.0.0, update-page 3.0.0; package.json -> 2.0.0. Re-validated: 11/11 evals pass, all 11 tools exercised. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * notion: fix query-database empty-filter bug; sharpen create-page, update-block, duplicate-page descriptions for AI agents One real bug and three description fixes, all surfaced by running the eval suite against this PR. 1. (BUG) `query-database`: when the caller omitted `filter`, the action ended up sending `filter: {}` (empty object), which the data-source query API rejects with "body.filter.or should be defined, body.filter.and should be defined, ...". Root cause: the previous code called `parseStringToJSON(filter, undefined)` to coerce the absent filter to `undefined`, but `parseStringToJSON` has `defaultValue = {}` as a default parameter — passing `undefined` as the second arg activates that default and returns `{}`. Replace the call with an explicit `typeof filter === "string" && filter.length > 0` check that defaults to a `{ and: [] }` no-op when the caller hasn't provided a filter, so "omit filter to return all rows" actually works. Tighten the prop description as well: spell out that a single-condition filter MUST include both a `property` key and a type-specific operator (`title`, `rich_text`, `select`, …), and point the caller at Retrieve Database Schema for the exact column names. AI agents were repeatedly building partial filters like `{ "title": { "contains": "..." } }` with no `property`, which the API also rejects. 2. (DESCRIPTION) `update-block`: the terse "Updates a child block object" description was getting picked up for "update a page's properties" and "append content to a page" — both of which are properly handled by Update Page and Append Block respectively. Rewrite to call out what this tool does NOT do and which tool to use instead, so the model stops reaching for it for page-level operations. 3. (DESCRIPTION) `duplicate-page`: the previous name + "Create a new page copied from an existing page block" description made this tool look like a general-purpose page creator. Rename to "Copy Page From Template" and rewrite the description to make the template-copy semantics unambiguous: this tool reuses an existing page's structure, and for brand-new content the caller should use Create Page. 4. (DESCRIPTION) `create-page`: previous parent description said "use Search to resolve a name into an ID", but agents were still passing bare strings like `"root"` or `"workspace"` and getting opaque UUID validation errors. Make the description very explicit about the value being a UUID (or a Notion URL containing one), and call out the common non-UUID placeholders the API will reject. Validated against the project's MCP eval harness: pass rate moved from ~30% to ~90% across the 20-eval Notion suite after these changes together with the eval-side fixes (Notion-Version header forwarding and verifier-block adjustments) that live in the eval-monster repo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * notion: document timestamp filter shape in query-database The previous filter description claimed every single-condition filter "MUST include both a `property` key and a type-specific operator key" — that's true for property filters but wrong for timestamp filters, which use a top-level `timestamp` key set to `created_time` or `last_edited_time` (no `property` at all). Agents that read the old description tried to wedge a `property` into timestamp filters and the API rejected them. Rewrite the description to document both single-condition shapes explicitly: - **Property filter** (unchanged guidance): `{ property, <type>: { <op>: ... } }`. - **Timestamp filter** (new): `{ timestamp: "created_time" | "last_edited_time", <same key>: { <op>: ... } }` — note the lack of `property`. Example added: `{ "timestamp": "created_time", "created_time": { "past_week": {} } }`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Michelle Bergeron <michelle.bergeron@workday.com> Co-authored-by: michelle0927 <michelle0927@users.noreply.github.com>
1 parent d7e7a92 commit 00f82e1

22 files changed

Lines changed: 572 additions & 422 deletions

File tree

Lines changed: 30 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import notion from "../../notion.app.mjs";
2+
import utils from "../../common/utils.mjs";
23
import base from "../common/base-page-builder.mjs";
3-
import { appendBlocks } from "notion-helper";
4+
5+
const MAX_BLOCKS = 100;
46

57
export default {
68
...base,
79
key: "notion-append-block",
810
name: "Append Block to Parent",
9-
description: "Append new and/or existing blocks to the specified parent. [See the documentation](https://developers.notion.com/reference/patch-block-children)",
10-
version: "0.4.3",
11+
description:
12+
"Append Markdown content to the bottom of a Notion page (or block)."
13+
+ " The `content` is parsed from Markdown into Notion blocks — use it to add paragraphs, headings, bullet/numbered lists, to-dos, quotes, code, etc."
14+
+ " Provide the page ID or URL (use **Search** to resolve a page name into an ID)."
15+
+ " To change a database row's property values instead, use **Update Page**."
16+
+ " [See the documentation](https://developers.notion.com/reference/patch-block-children)",
17+
version: "1.0.0",
1118
annotations: {
1219
destructiveHint: false,
1320
openWorldHint: true,
@@ -17,119 +24,35 @@ export default {
1724
props: {
1825
notion,
1926
pageId: {
20-
propDefinition: [
21-
notion,
22-
"pageId",
23-
],
24-
label: "Parent Block ID",
25-
description: "Select a parent block/page or provide its ID",
26-
},
27-
blockTypes: {
28-
type: "string[]",
29-
label: "Block Type(s)",
30-
description: "Select which type(s) of block you'd like to append",
31-
reloadProps: true,
32-
options: [
33-
{
34-
label: "Append existing blocks",
35-
value: "blockIds",
36-
},
37-
{
38-
label: "Provide Markdown content to create new blocks with",
39-
value: "markdownContents",
40-
},
41-
{
42-
label: "Provide Image URLs to create new image blocks",
43-
value: "imageUrls",
44-
},
45-
],
46-
},
47-
blockIds: {
48-
propDefinition: [
49-
notion,
50-
"pageId",
51-
],
52-
type: "string[]",
53-
label: "Existing Block IDs",
54-
description: "Select one or more block(s) or page(s) to append (selecting a page appends its children). You can also provide block or page IDs.",
55-
hidden: true,
56-
},
57-
markdownContents: {
58-
type: "string[]",
59-
label: "Markdown Contents",
60-
description:
61-
"Each entry is the content of a new block to append, using Markdown syntax. [See the documentation](https://www.notion.com/help/writing-and-editing-basics#markdown-and-shortcuts) for more information",
62-
hidden: true,
27+
type: "string",
28+
label: "Page ID or URL",
29+
description: "The ID (or Notion URL) of the page/block to append content to. Use **Search** to resolve a page name into an ID.",
6330
},
64-
imageUrls: {
65-
type: "string[]",
66-
label: "Image URLs",
67-
description: "One or more Image URLs to append new image blocks with. [See the documentation](https://www.notion.com/help/images-files-and-media#media-block-types) for more information",
68-
hidden: true,
31+
content: {
32+
type: "string",
33+
label: "Content",
34+
description: "The Markdown content to append. Supports headings, bullet/numbered lists, to-dos, quotes, and code. Example: `## Notes\\n- First item\\n- Second item`.",
6935
},
7036
},
71-
additionalProps(currentProps) {
72-
const { blockTypes } = this;
73-
74-
for (let prop of [
75-
"blockIds",
76-
"markdownContents",
77-
"imageUrls",
78-
]) {
79-
currentProps[prop].hidden = !blockTypes.includes(prop);
80-
}
81-
82-
return {};
83-
},
8437
async run({ $ }) {
85-
const { blockTypes } = this;
86-
const children = [];
38+
const pageId = utils.extractNotionId(this.pageId);
39+
const children = this.createBlocks(this.content);
8740

88-
// add blocks from blockIds
89-
if (blockTypes.includes("blockIds") && this.blockIds?.length > 0) {
90-
for (const id of this.blockIds) {
91-
const block = await this.notion.retrieveBlock(id);
92-
block.children = await this.notion.retrieveBlockChildren(block);
93-
const formattedChildren = await this.formatChildBlocks(block);
94-
children.push(...formattedChildren);
95-
}
96-
}
97-
98-
// add blocks from markup
99-
if (blockTypes.includes("markdownContents") && this.markdownContents?.length > 0) {
100-
for (const content of this.markdownContents) {
101-
const block = this.createBlocks(content);
102-
children.push(...block);
103-
}
104-
}
105-
106-
// add image blocks
107-
if (blockTypes.includes("imageUrls") && this.imageUrls?.length) {
108-
for (const url of this.imageUrls) {
109-
children.push({
110-
type: "image",
111-
image: {
112-
type: "external",
113-
external: {
114-
url,
115-
},
116-
},
117-
});
118-
}
119-
}
120-
121-
if (children.length === 0) {
41+
if (!children.length) {
12242
$.export("$summary", "Nothing to append");
12343
return;
12444
}
12545

126-
const response = await appendBlocks({
127-
client: await this.notion._getNotionClient(),
128-
block_id: this.pageId,
129-
children,
130-
});
46+
const responses = [];
47+
let remaining = children;
48+
while (remaining.length > 0) {
49+
responses.push(await this.notion.appendBlock(pageId, remaining.slice(0, MAX_BLOCKS)));
50+
remaining = remaining.slice(MAX_BLOCKS);
51+
}
13152

132-
$.export("$summary", "Appended blocks successfully");
133-
return response.apiResponses;
53+
$.export("$summary", `Appended ${children.length} block${children.length === 1
54+
? ""
55+
: "s"} to the page`);
56+
return responses;
13457
},
13558
};

components/notion/actions/common/base-page-builder.mjs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,18 @@ export default {
8383
* - value: the property value inputted by the user
8484
*/
8585
_filterProps(properties = {}) {
86+
// Check key presence (not truthiness) so valid falsy values — 0, false,
87+
// "" — are preserved rather than silently dropped.
8688
return Object.keys(properties)
8789
.filter((property) => this[property] != null
88-
|| (this.properties && this.properties[property]))
90+
|| (this.properties
91+
&& Object.prototype.hasOwnProperty.call(this.properties, property)))
8992
.map((property) => ({
9093
type: properties[property]?.type ?? property,
9194
label: properties[property]?.id || property,
92-
value: this[property] || this.properties?.[property],
95+
value: this[property] != null
96+
? this[property]
97+
: this.properties?.[property],
9398
name: properties[property]?.name || property,
9499
}));
95100
},

components/notion/actions/complete-file-upload/complete-file-upload.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default {
66
key: "notion-complete-file-upload",
77
name: "Complete File Upload",
88
description: "Use this action to finalize a `mode=multi_part` file upload after all of the parts have been sent successfully. [See the documentation](https://developers.notion.com/reference/complete-a-file-upload)",
9-
version: "0.0.10",
9+
version: "0.0.11",
1010
annotations: {
1111
destructiveHint: false,
1212
openWorldHint: true,

components/notion/actions/create-comment/create-comment.mjs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { ConfigurationError } from "@pipedream/platform";
22
import notion from "../../notion.app.mjs";
3+
import utils from "../../common/utils.mjs";
34

45
export default {
56
key: "notion-create-comment",
67
name: "Create Comment",
7-
description: "Create a comment in a page or existing discussion thread. [See the documentation](https://developers.notion.com/reference/create-a-comment)",
8-
version: "0.0.10",
8+
description:
9+
"Add a comment to a Notion page, or reply to an existing discussion thread."
10+
+ " Provide **either** a page ID/URL (use **Search** to resolve a page name) **or** a discussion ID — not both."
11+
+ " [See the documentation](https://developers.notion.com/reference/create-a-comment)",
12+
version: "0.1.0",
913
annotations: {
1014
destructiveHint: false,
1115
openWorldHint: true,
@@ -14,38 +18,36 @@ export default {
1418
type: "action",
1519
props: {
1620
notion,
17-
infoLabel: {
18-
type: "alert",
19-
alertType: "info",
20-
content: "Provide either a Page ID or a Discussion ID to create the comment under.",
21-
},
2221
pageId: {
23-
propDefinition: [
24-
notion,
25-
"pageId",
26-
],
22+
type: "string",
23+
label: "Page ID or URL",
24+
description: "The ID (or Notion URL) of the page to comment on. Use **Search** to resolve a page name into an ID. Provide this or a Discussion ID.",
2725
optional: true,
2826
},
2927
discussionId: {
3028
type: "string",
3129
label: "Discussion ID",
32-
description: "The ID of a discussion thread. [See the documentation](https://developers.notion.com/docs/working-with-comments#retrieving-a-discussion-id) for more information",
30+
description: "The ID of an existing discussion thread to reply to — the `discussion_id` Notion returns for a thread, a UUID like `f1c5e8a0-...`. Provide this or a Page ID.",
3331
optional: true,
3432
},
3533
comment: {
3634
type: "string",
3735
label: "Comment",
38-
description: "The comment text",
36+
description: "The comment text.",
3937
},
4038
},
4139
async run({ $ }) {
4240
if ((this.pageId && this.discussionId) || (!this.pageId && !this.discussionId)) {
4341
throw new ConfigurationError("Provide either a page ID or a discussion thread ID to create the comment under");
4442
}
4543

44+
const pageId = this.pageId
45+
? utils.extractNotionId(this.pageId)
46+
: undefined;
47+
4648
const response = await this.notion._getNotionClient().comments.create({
47-
parent: this.pageId && {
48-
page_id: this.pageId,
49+
parent: pageId && {
50+
page_id: pageId,
4951
},
5052
discussion_id: this.discussionId,
5153
rich_text: [
Lines changed: 17 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import utils from "../../common/utils.mjs";
22
import notion from "../../notion.app.mjs";
3-
import base from "../common/base-page-builder.mjs";
43

54
export default {
6-
...base,
75
key: "notion-create-database",
86
name: "Create Database",
9-
description: "Create a database and its initial data source. [See the documentation](https://developers.notion.com/reference/database-create)",
10-
version: "0.1.8",
7+
description:
8+
"Create a new Notion database (data source) as a subpage of a parent page, defining its column schema."
9+
+ " Provide the parent page ID or URL (use **Search** to resolve a page name into an ID)."
10+
+ " `properties` is a JSON object of column-name → column type. Each value is a [property schema object](https://developers.notion.com/reference/property-schema-object), or shorthand for simple types:"
11+
+ " `{ \"Name\": \"title\", \"Quantity\": \"number\", \"Category\": { \"select\": { \"options\": [ { \"name\": \"A\" }, { \"name\": \"B\" } ] } } }`."
12+
+ " Exactly one column must be the `title` type."
13+
+ " [See the documentation](https://developers.notion.com/reference/database-create)",
14+
version: "1.0.0",
1115
annotations: {
1216
destructiveHint: false,
1317
openWorldHint: true,
@@ -17,66 +21,29 @@ export default {
1721
props: {
1822
notion,
1923
parent: {
20-
propDefinition: [
21-
notion,
22-
"pageId",
23-
],
24-
label: "Parent Page ID",
25-
description: "Select a parent page or provide a page ID",
24+
type: "string",
25+
label: "Parent Page ID or URL",
26+
description: "The ID (or Notion URL) of the page the database will be created under. Use **Search** to resolve a page name into an ID.",
2627
},
2728
title: {
2829
type: "string",
2930
label: "Title",
30-
description: "Title of database as it appears in Notion. An array of [rich text objects](https://developers.notion.com/reference/rich-text).",
31+
description: "The title of the database as it appears in Notion.",
3132
optional: true,
3233
},
3334
properties: {
34-
type: "object",
35+
type: "string",
3536
label: "Properties",
36-
description: "Property schema of database. The keys are the names of properties as they appear in Notion and the values are [property schema objects](https://developers.notion.com/reference/property-schema-object).",
37+
description: "JSON object of column-name → column type. Example: `{ \"Name\": \"title\", \"Quantity\": \"number\", \"Category\": { \"select\": { \"options\": [ { \"name\": \"A\" } ] } } }`. Exactly one column must be the `title` type.",
3738
},
3839
},
3940
async run({ $ }) {
40-
const parsedProperties = utils.parseObject(this.properties);
41-
const properties = parsedProperties && typeof parsedProperties === "object"
42-
? Object.fromEntries(
43-
Object.entries(parsedProperties).map(([
44-
key,
45-
value,
46-
]) => {
47-
if (typeof value === "string") {
48-
return [
49-
key,
50-
{
51-
[value]: {},
52-
},
53-
];
54-
}
55-
// Normalize {type:"X"} objects missing their type-key: {type:"checkbox"} → {checkbox:{}}
56-
if (value && typeof value === "object" && "type" in value) {
57-
const typeKey = value.type;
58-
if (typeKey && typeof typeKey === "string" && !(typeKey in value)) {
59-
return [
60-
key,
61-
{
62-
...value,
63-
[typeKey]: {},
64-
},
65-
];
66-
}
67-
}
68-
return [
69-
key,
70-
value,
71-
];
72-
}),
73-
)
74-
: parsedProperties;
41+
const properties = utils.normalizeDatabaseSchema(this.properties);
7542

7643
const response = await this.notion.createDatabase({
7744
parent: {
7845
type: "page_id",
79-
page_id: this.parent,
46+
page_id: utils.extractNotionId(this.parent),
8047
},
8148
title: [
8249
{
@@ -91,7 +58,7 @@ export default {
9158
},
9259
});
9360

94-
$.export("$summary", `Successfully created database with ID ${response.id}`);
61+
$.export("$summary", `Successfully created database "${this.title || "Untitled"}" (ID: ${response.id})`);
9562
return response;
9663
},
9764
};

components/notion/actions/create-file-upload/create-file-upload.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default {
66
key: "notion-create-file-upload",
77
name: "Create File Upload",
88
description: "Create a file upload. [See the documentation](https://developers.notion.com/reference/create-a-file-upload)",
9-
version: "0.0.10",
9+
version: "0.0.11",
1010
annotations: {
1111
destructiveHint: false,
1212
openWorldHint: true,

components/notion/actions/create-page-from-database/create-page-from-database.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default {
1010
key: "notion-create-page-from-database",
1111
name: "Create Page from Data Source",
1212
description: "Create a page from a data source. [See the documentation](https://developers.notion.com/reference/post-page)",
13-
version: "2.0.2",
13+
version: "2.0.3",
1414
annotations: {
1515
destructiveHint: false,
1616
openWorldHint: true,

0 commit comments

Comments
 (0)