@nimbus-dev/connectors 0.2.1 → 0.2.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 +14 -4
- package/connectors/airflow/src/search-filter.ts +2 -15
- package/connectors/airflow/src/server.ts +2 -87
- package/connectors/airflow/src/tools.ts +74 -0
- package/connectors/apple/src/adapters.ts +326 -0
- package/connectors/apple/src/apple-mail-core.ts +4 -8
- package/connectors/apple/src/calendar-tools.ts +28 -8
- package/connectors/apple/src/server.ts +23 -512
- package/connectors/apple/src/tools.ts +18 -44
- package/connectors/argocd/src/server.ts +25 -38
- package/connectors/athena/src/tools.ts +15 -34
- package/connectors/aws/src/server.ts +2 -183
- package/connectors/aws/src/tools.ts +197 -0
- package/connectors/azure/src/server.ts +2 -131
- package/connectors/azure/src/tools.ts +133 -0
- package/connectors/bigeye/src/search-filter.ts +4 -15
- package/connectors/bigeye/src/server.ts +38 -38
- package/connectors/bigquery/src/tools.ts +1 -26
- package/connectors/bitbucket/src/server.ts +2 -244
- package/connectors/bitbucket/src/tools.ts +241 -0
- package/connectors/bitrise/src/server.ts +2 -79
- package/connectors/bitrise/src/tools.ts +76 -0
- package/connectors/canva/src/search-filter.ts +2 -11
- package/connectors/canva/src/server.ts +2 -73
- package/connectors/canva/src/tools.ts +66 -0
- package/connectors/circleci/src/server.ts +2 -176
- package/connectors/circleci/src/tools.ts +193 -0
- package/connectors/cloud-logging/src/tools.ts +14 -27
- package/connectors/cloudwatch/src/tools.ts +15 -34
- package/connectors/codemagic/src/server.ts +2 -70
- package/connectors/codemagic/src/tools.ts +71 -0
- package/connectors/confluence/src/server.ts +2 -301
- package/connectors/confluence/src/tools.ts +320 -0
- package/connectors/dagster/src/server.ts +2 -214
- package/connectors/dagster/src/tools.ts +187 -0
- package/connectors/databricks/src/search-filter.ts +7 -7
- package/connectors/databricks/src/server.ts +2 -77
- package/connectors/databricks/src/tools.ts +66 -0
- package/connectors/datadog/src/server.ts +2 -48
- package/connectors/datadog/src/tools.ts +48 -0
- package/connectors/dataprofile/src/profile.ts +16 -46
- package/connectors/dbt/src/server.ts +2 -92
- package/connectors/dbt/src/tools.ts +80 -0
- package/connectors/dependencytrack/src/server.ts +2 -76
- package/connectors/dependencytrack/src/tools.ts +65 -0
- package/connectors/discord/src/server.ts +2 -85
- package/connectors/discord/src/tools.ts +82 -0
- package/connectors/elasticsearch/src/tools.ts +7 -37
- package/connectors/fastmail/src/jmap-client.ts +202 -0
- package/connectors/fastmail/src/jmap-core.ts +8 -21
- package/connectors/fastmail/src/server.ts +2 -187
- package/connectors/fastmail/src/tools.ts +4 -23
- package/connectors/figma/src/search-filter.ts +2 -11
- package/connectors/figma/src/server.ts +2 -113
- package/connectors/figma/src/tools.ts +99 -0
- package/connectors/firebase/src/server.ts +2 -90
- package/connectors/firebase/src/token.ts +2 -5
- package/connectors/firebase/src/tools.ts +106 -0
- package/connectors/flagsmith/src/server.ts +2 -98
- package/connectors/flagsmith/src/tools.ts +95 -0
- package/connectors/flux/src/server.ts +52 -53
- package/connectors/gcp/src/server.ts +2 -132
- package/connectors/gcp/src/tools.ts +123 -0
- package/connectors/github/src/server.ts +2 -328
- package/connectors/github/src/tools.ts +311 -0
- package/connectors/github-actions/src/server.ts +2 -205
- package/connectors/github-actions/src/tools.ts +209 -0
- package/connectors/gitlab/src/server.ts +2 -384
- package/connectors/gitlab/src/tools.ts +392 -0
- package/connectors/gmail/src/server.ts +2 -270
- package/connectors/gmail/src/tools.ts +209 -0
- package/connectors/google-drive/src/server.ts +2 -524
- package/connectors/google-drive/src/tools.ts +529 -0
- package/connectors/google-meet/src/server.ts +2 -82
- package/connectors/google-meet/src/tools.ts +87 -0
- package/connectors/google-photos/src/server.ts +2 -132
- package/connectors/google-photos/src/tools.ts +138 -0
- package/connectors/grafana/src/server.ts +2 -53
- package/connectors/grafana/src/tools.ts +53 -0
- package/connectors/great-expectations/src/gx-parse.ts +14 -45
- package/connectors/greenhouse/src/server.ts +2 -63
- package/connectors/greenhouse/src/tools.ts +68 -0
- package/connectors/hubspot/src/server.ts +2 -77
- package/connectors/hubspot/src/tools.ts +70 -0
- package/connectors/iac/src/server.ts +2 -137
- package/connectors/iac/src/tools.ts +152 -0
- package/connectors/imap/src/imap-core.ts +29 -82
- package/connectors/imap/src/server.ts +33 -220
- package/connectors/intercom/src/server.ts +2 -61
- package/connectors/intercom/src/tools.ts +58 -0
- package/connectors/jenkins/src/jenkins-api.ts +2 -6
- package/connectors/jenkins/src/server.ts +2 -215
- package/connectors/jenkins/src/tools.ts +240 -0
- package/connectors/jira/src/server.ts +2 -301
- package/connectors/jira/src/tools.ts +325 -0
- package/connectors/kubernetes/src/server.ts +2 -155
- package/connectors/kubernetes/src/tools.ts +159 -0
- package/connectors/launchdarkly/src/server.ts +2 -75
- package/connectors/launchdarkly/src/tools.ts +72 -0
- package/connectors/lever/src/search-filter.ts +4 -12
- package/connectors/lever/src/server.ts +2 -60
- package/connectors/lever/src/tools.ts +61 -0
- package/connectors/linear/src/server.ts +2 -459
- package/connectors/linear/src/tools.ts +482 -0
- package/connectors/localdb/src/sql-scan.ts +13 -41
- package/connectors/localdb/src/tools.ts +1 -1
- package/connectors/looker/src/search-filter.ts +2 -15
- package/connectors/looker/src/server.ts +35 -70
- package/connectors/mendeley/src/server.ts +2 -55
- package/connectors/mendeley/src/tools.ts +54 -0
- package/connectors/mercury/src/server.ts +2 -57
- package/connectors/mercury/src/tools.ts +50 -0
- package/connectors/metabase/src/search-filter.ts +4 -15
- package/connectors/metabase/src/server.ts +2 -79
- package/connectors/metabase/src/tools.ts +64 -0
- package/connectors/miro/src/server.ts +2 -72
- package/connectors/miro/src/tools.ts +65 -0
- package/connectors/mlflow/src/search-filter.ts +3 -7
- package/connectors/mlflow/src/server.ts +20 -40
- package/connectors/monte-carlo/src/server.ts +50 -57
- package/connectors/netlify/src/search-filter.ts +6 -14
- package/connectors/netlify/src/server.ts +2 -60
- package/connectors/netlify/src/tools.ts +53 -0
- package/connectors/newrelic/src/server.ts +2 -40
- package/connectors/newrelic/src/tools.ts +37 -0
- package/connectors/notion/src/server.ts +2 -335
- package/connectors/notion/src/tools.ts +344 -0
- package/connectors/obsidian/src/server.ts +2 -425
- package/connectors/obsidian/src/tools.ts +466 -0
- package/connectors/onedrive/src/server.ts +2 -249
- package/connectors/onedrive/src/tools.ts +253 -0
- package/connectors/outlook/src/server.ts +2 -351
- package/connectors/outlook/src/tools.ts +357 -0
- package/connectors/pagerduty/src/server.ts +2 -132
- package/connectors/pagerduty/src/tools.ts +126 -0
- package/connectors/pipedrive/src/server.ts +2 -58
- package/connectors/pipedrive/src/tools.ts +63 -0
- package/connectors/powerbi/src/search-filter.ts +2 -15
- package/connectors/powerbi/src/server.ts +43 -68
- package/connectors/prefect/src/search-filter.ts +3 -16
- package/connectors/prefect/src/server.ts +2 -90
- package/connectors/prefect/src/tools.ts +83 -0
- package/connectors/protonmail/src/mail-core.ts +16 -75
- package/connectors/protonmail/src/server.ts +34 -214
- package/connectors/raindrop/src/server.ts +2 -103
- package/connectors/raindrop/src/tools.ts +103 -0
- package/connectors/ramp/src/server.ts +2 -105
- package/connectors/ramp/src/tools.ts +98 -0
- package/connectors/readwise/src/server.ts +2 -88
- package/connectors/readwise/src/tools.ts +81 -0
- package/connectors/sagemaker/src/tools.ts +19 -38
- package/connectors/salesforce/src/search-filter.ts +4 -11
- package/connectors/salesforce/src/server.ts +2 -90
- package/connectors/salesforce/src/tools.ts +75 -0
- package/connectors/semgrep/src/search-filter.ts +3 -10
- package/connectors/semgrep/src/server.ts +2 -104
- package/connectors/semgrep/src/tools.ts +92 -0
- package/connectors/sentry/src/server.ts +2 -58
- package/connectors/sentry/src/tools.ts +53 -0
- package/connectors/slack/src/server.ts +2 -352
- package/connectors/slack/src/tools.ts +376 -0
- package/connectors/snowflake/src/search-filter.ts +4 -19
- package/connectors/snowflake/src/server.ts +6 -25
- package/connectors/snyk/src/server.ts +2 -140
- package/connectors/snyk/src/tools.ts +136 -0
- package/connectors/sonarqube/src/server.ts +2 -111
- package/connectors/sonarqube/src/tools.ts +98 -0
- package/connectors/stackoverflow/src/server.ts +2 -77
- package/connectors/stackoverflow/src/tools.ts +66 -0
- package/connectors/storybook/src/storybook-parse.ts +3 -6
- package/connectors/stripe/src/server.ts +2 -61
- package/connectors/stripe/src/tools.ts +54 -0
- package/connectors/superset/src/search-filter.ts +4 -15
- package/connectors/superset/src/server.ts +2 -115
- package/connectors/superset/src/tools.ts +98 -0
- package/connectors/tableau/src/search-filter.ts +2 -15
- package/connectors/tableau/src/server.ts +13 -65
- package/connectors/teams/src/server.ts +2 -249
- package/connectors/teams/src/tools.ts +283 -0
- package/connectors/testflight/src/server.ts +2 -70
- package/connectors/testflight/src/tools.ts +79 -0
- package/connectors/vercel/src/search-filter.ts +2 -10
- package/connectors/vercel/src/server.ts +2 -75
- package/connectors/vercel/src/tools.ts +73 -0
- package/connectors/vertex-ai/src/tools.ts +23 -33
- package/connectors/wiz/src/search-filter.ts +6 -30
- package/connectors/wiz/src/server.ts +2 -188
- package/connectors/wiz/src/tools.ts +168 -0
- package/connectors/zendesk/src/server.ts +2 -73
- package/connectors/zendesk/src/tools.ts +60 -0
- package/connectors/zoom/src/search-filter.ts +1 -7
- package/connectors/zoom/src/server.ts +2 -94
- package/connectors/zoom/src/tools.ts +93 -0
- package/connectors/zotero/src/server.ts +2 -78
- package/connectors/zotero/src/tools.ts +67 -0
- package/package.json +14 -12
- package/shared/access-token-cache.ts +123 -0
- package/shared/atlassian-json-fetch.ts +6 -16
- package/shared/audit-chain.ts +13 -6
- package/shared/cli-json-kit.ts +150 -0
- package/shared/collection-tool-kit.ts +143 -0
- package/shared/connector-mode.ts +3 -2
- package/shared/cursor-list-tool.ts +29 -0
- package/shared/env-json-api.ts +209 -0
- package/shared/fetch-json-text.ts +27 -0
- package/shared/github-rest.ts +62 -0
- package/shared/graphql-json.ts +60 -0
- package/shared/imap-mail-core.ts +91 -0
- package/shared/imap-tool-kit.ts +57 -23
- package/shared/imapflow-adapter.ts +417 -0
- package/shared/rest-tool-kit.ts +77 -7
- package/shared/run-cli-json.ts +38 -16
- package/shared/search-filter.ts +58 -18
- package/shared/walk-files.ts +118 -0
- package/standalone/src/launcher.ts +19 -7
|
@@ -1,343 +1,10 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import {
|
|
4
|
-
import { joinApiPath } from "../../../shared/join-api-path.ts";
|
|
5
|
-
import { parseCitationsJson } from "../../../shared/kb-markdown.ts";
|
|
6
|
-
import {
|
|
7
|
-
createRegisterSimpleTool,
|
|
8
|
-
createZodToolRegistrar,
|
|
9
|
-
mcpJsonResult as jsonResult,
|
|
10
|
-
mcpJsonResultFromTextIfOk,
|
|
11
|
-
parseJsonTextIfOk,
|
|
12
|
-
putOptionalNonEmptyString,
|
|
13
|
-
requireProcessEnv,
|
|
14
|
-
} from "../../../shared/mcp-tool-kit.ts";
|
|
15
|
-
import { buildNotionKbPageBody } from "./kb-append.ts";
|
|
16
|
-
|
|
17
|
-
const NOTION_VERSION = "2022-06-28";
|
|
18
|
-
const API = "https://api.notion.com/v1";
|
|
19
|
-
|
|
20
|
-
async function notionFetch(
|
|
21
|
-
path: string,
|
|
22
|
-
init?: RequestInit,
|
|
23
|
-
): Promise<{ ok: boolean; status: number; text: string }> {
|
|
24
|
-
const token = requireProcessEnv("NOTION_ACCESS_TOKEN");
|
|
25
|
-
const url = joinApiPath(API, path);
|
|
26
|
-
const headers: Record<string, string> = {
|
|
27
|
-
Authorization: `Bearer ${token}`,
|
|
28
|
-
"Notion-Version": NOTION_VERSION,
|
|
29
|
-
};
|
|
30
|
-
if (init?.body !== undefined) {
|
|
31
|
-
headers["Content-Type"] = "application/json";
|
|
32
|
-
}
|
|
33
|
-
const res = await fetch(url, {
|
|
34
|
-
...init,
|
|
35
|
-
headers: {
|
|
36
|
-
...headers,
|
|
37
|
-
...(init?.headers as Record<string, string> | undefined),
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
const text = await res.text();
|
|
41
|
-
return { ok: res.ok, status: res.status, text };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function richText(content: string): ReadonlyArray<Record<string, unknown>> {
|
|
45
|
-
return [{ type: "text", text: { content: content.slice(0, 2000) } }];
|
|
46
|
-
}
|
|
3
|
+
import { registerNotionTools } from "./tools.ts";
|
|
47
4
|
|
|
48
5
|
const server = new McpServer({ name: "nimbus-notion", version: "0.1.0" });
|
|
49
6
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
|
|
53
|
-
|
|
54
|
-
const reg = createZodToolRegistrar(registerSimpleTool);
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Every MUTATING notion tool goes through here. Outside the gateway this adds the
|
|
58
|
-
* consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
|
|
59
|
-
* the gateway it is a pass-through, because executor.ts (I2) is the gate there.
|
|
60
|
-
*/
|
|
61
|
-
const registerWriteTool = createWriteToolRegistrar(server, {
|
|
62
|
-
connector: "notion",
|
|
63
|
-
scopeEnv: "NIMBUS_MCP_NOTION_WRITE_SCOPE",
|
|
64
|
-
scopeKinds: ["page", "database", "block"],
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const notionSearchSchema = z.object({
|
|
68
|
-
query: z.string().optional(),
|
|
69
|
-
pageSize: z.number().int().min(1).max(100).optional(),
|
|
70
|
-
startCursor: z.string().optional(),
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
function notionSearchPostBody(
|
|
74
|
-
objectValue: "page" | "database",
|
|
75
|
-
parsed: z.infer<typeof notionSearchSchema>,
|
|
76
|
-
): Record<string, unknown> {
|
|
77
|
-
const body: Record<string, unknown> = {
|
|
78
|
-
filter: { property: "object", value: objectValue },
|
|
79
|
-
page_size: parsed.pageSize ?? 50,
|
|
80
|
-
};
|
|
81
|
-
putOptionalNonEmptyString(body, "query", parsed.query);
|
|
82
|
-
putOptionalNonEmptyString(body, "start_cursor", parsed.startCursor);
|
|
83
|
-
return body;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
reg(
|
|
87
|
-
"notion_page_list",
|
|
88
|
-
"Search Notion pages the integration can access (POST /v1/search, object filter page).",
|
|
89
|
-
notionSearchSchema,
|
|
90
|
-
async (parsed) => {
|
|
91
|
-
const body = notionSearchPostBody("page", parsed);
|
|
92
|
-
const res = await notionFetch("/search", { method: "POST", body: JSON.stringify(body) });
|
|
93
|
-
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
94
|
-
},
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
const notionPageIdSchema = z.object({ pageId: z.string().min(1) });
|
|
98
|
-
|
|
99
|
-
reg(
|
|
100
|
-
"notion_page_get",
|
|
101
|
-
"Retrieve a Notion page and its direct child blocks.",
|
|
102
|
-
notionPageIdSchema,
|
|
103
|
-
async (parsed) => {
|
|
104
|
-
const id = encodeURIComponent(parsed.pageId);
|
|
105
|
-
const page = await notionFetch(`/pages/${id}`);
|
|
106
|
-
const blocks = await notionFetch(`/blocks/${id}/children?page_size=100`);
|
|
107
|
-
return jsonResult({
|
|
108
|
-
page: parseJsonTextIfOk("Notion", page),
|
|
109
|
-
blockChildren: parseJsonTextIfOk("Notion blocks", blocks),
|
|
110
|
-
});
|
|
111
|
-
},
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
reg(
|
|
115
|
-
"notion_database_list",
|
|
116
|
-
"Search Notion databases (POST /v1/search, object filter database).",
|
|
117
|
-
notionSearchSchema,
|
|
118
|
-
async (parsed) => {
|
|
119
|
-
const body = notionSearchPostBody("database", parsed);
|
|
120
|
-
const res = await notionFetch("/search", { method: "POST", body: JSON.stringify(body) });
|
|
121
|
-
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
122
|
-
},
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
const notionDatabaseQuerySchema = z.object({
|
|
126
|
-
databaseId: z.string().min(1),
|
|
127
|
-
pageSize: z.number().int().min(1).max(100).optional(),
|
|
128
|
-
startCursor: z.string().optional(),
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
reg(
|
|
132
|
-
"notion_database_query",
|
|
133
|
-
"Query a Notion database (POST /v1/databases/{id}/query).",
|
|
134
|
-
notionDatabaseQuerySchema,
|
|
135
|
-
async (parsed) => {
|
|
136
|
-
const id = encodeURIComponent(parsed.databaseId);
|
|
137
|
-
const body: Record<string, unknown> = {
|
|
138
|
-
page_size: parsed.pageSize ?? 50,
|
|
139
|
-
};
|
|
140
|
-
putOptionalNonEmptyString(body, "start_cursor", parsed.startCursor);
|
|
141
|
-
const res = await notionFetch(`/databases/${id}/query`, {
|
|
142
|
-
method: "POST",
|
|
143
|
-
body: JSON.stringify(body),
|
|
144
|
-
});
|
|
145
|
-
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
146
|
-
},
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
const notionBlockPagedSchema = z.object({
|
|
150
|
-
blockId: z.string().min(1),
|
|
151
|
-
pageSize: z.number().int().min(1).max(100).optional(),
|
|
152
|
-
startCursor: z.string().optional(),
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
reg(
|
|
156
|
-
"notion_block_children",
|
|
157
|
-
"List child blocks of a block or page (GET /v1/blocks/{id}/children).",
|
|
158
|
-
notionBlockPagedSchema,
|
|
159
|
-
async (parsed) => {
|
|
160
|
-
const id = encodeURIComponent(parsed.blockId);
|
|
161
|
-
const qs = new URLSearchParams({
|
|
162
|
-
page_size: String(parsed.pageSize ?? 50),
|
|
163
|
-
});
|
|
164
|
-
if (parsed.startCursor !== undefined && parsed.startCursor !== "") {
|
|
165
|
-
qs.set("start_cursor", parsed.startCursor);
|
|
166
|
-
}
|
|
167
|
-
const res = await notionFetch(`/blocks/${id}/children?${qs.toString()}`);
|
|
168
|
-
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
169
|
-
},
|
|
170
|
-
);
|
|
171
|
-
|
|
172
|
-
reg(
|
|
173
|
-
"notion_comment_list",
|
|
174
|
-
"List comments for a block or page (GET /v1/comments).",
|
|
175
|
-
notionBlockPagedSchema,
|
|
176
|
-
async (parsed) => {
|
|
177
|
-
const qs = new URLSearchParams({
|
|
178
|
-
block_id: parsed.blockId,
|
|
179
|
-
page_size: String(parsed.pageSize ?? 50),
|
|
180
|
-
});
|
|
181
|
-
if (parsed.startCursor !== undefined && parsed.startCursor !== "") {
|
|
182
|
-
qs.set("start_cursor", parsed.startCursor);
|
|
183
|
-
}
|
|
184
|
-
const res = await notionFetch(`/comments?${qs.toString()}`);
|
|
185
|
-
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
186
|
-
},
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
const notionPageCreateSchema = z.object({
|
|
190
|
-
parentPageId: z.string().min(1),
|
|
191
|
-
title: z.string().min(1),
|
|
192
|
-
titlePropertyName: z.string().min(1).optional(),
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
registerWriteTool(
|
|
196
|
-
"notion_page_create",
|
|
197
|
-
{
|
|
198
|
-
mutates: "notion.page.create",
|
|
199
|
-
recoverable: true,
|
|
200
|
-
scopeTargetOf: (p) => ({ kind: "page", value: p.parentPageId }),
|
|
201
|
-
},
|
|
202
|
-
"Create a page under a parent page (POST /v1/pages).",
|
|
203
|
-
notionPageCreateSchema,
|
|
204
|
-
async (parsed) => {
|
|
205
|
-
const prop = parsed.titlePropertyName ?? "title";
|
|
206
|
-
const body = {
|
|
207
|
-
parent: { page_id: parsed.parentPageId },
|
|
208
|
-
properties: {
|
|
209
|
-
[prop]: {
|
|
210
|
-
title: richText(parsed.title),
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
const res = await notionFetch("/pages", { method: "POST", body: JSON.stringify(body) });
|
|
215
|
-
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
216
|
-
},
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
const notionKbAppendSchema = z.object({
|
|
220
|
-
databaseId: z.string().min(1),
|
|
221
|
-
title: z.string().min(1),
|
|
222
|
-
bodyMarkdown: z.string().min(1),
|
|
223
|
-
citationsJson: z.string().optional(),
|
|
224
|
-
titlePropertyName: z.string().min(1).optional(),
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
registerWriteTool(
|
|
228
|
-
"notion_kb_append",
|
|
229
|
-
{
|
|
230
|
-
mutates: "notion.knowledge.write",
|
|
231
|
-
recoverable: true,
|
|
232
|
-
scopeTargetOf: (p) => ({ kind: "database", value: p.databaseId }),
|
|
233
|
-
},
|
|
234
|
-
"Create a knowledge-base page in a database from simple markdown + citations (POST /v1/pages). " +
|
|
235
|
-
"Used by Nimbus tribal-knowledge capture; the destination database is supplied by the gateway from local config only.",
|
|
236
|
-
notionKbAppendSchema,
|
|
237
|
-
async (parsed) => {
|
|
238
|
-
const body = buildNotionKbPageBody({
|
|
239
|
-
databaseId: parsed.databaseId,
|
|
240
|
-
title: parsed.title,
|
|
241
|
-
bodyMarkdown: parsed.bodyMarkdown,
|
|
242
|
-
citations: parseCitationsJson(parsed.citationsJson ?? "[]"),
|
|
243
|
-
...(parsed.titlePropertyName === undefined
|
|
244
|
-
? {}
|
|
245
|
-
: { titlePropertyName: parsed.titlePropertyName }),
|
|
246
|
-
});
|
|
247
|
-
const res = await notionFetch("/pages", { method: "POST", body: JSON.stringify(body) });
|
|
248
|
-
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
249
|
-
},
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
const notionPageUpdateSchema = z.object({
|
|
253
|
-
pageId: z.string().min(1),
|
|
254
|
-
propertiesJson: z.string().min(1),
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
registerWriteTool(
|
|
258
|
-
"notion_page_update",
|
|
259
|
-
{
|
|
260
|
-
mutates: "notion.page.update",
|
|
261
|
-
recoverable: true,
|
|
262
|
-
scopeTargetOf: (p) => ({ kind: "page", value: p.pageId }),
|
|
263
|
-
},
|
|
264
|
-
"Update page properties (PATCH /v1/pages/{id}). Pass properties JSON as string.",
|
|
265
|
-
notionPageUpdateSchema,
|
|
266
|
-
async (parsed) => {
|
|
267
|
-
let props: unknown;
|
|
268
|
-
try {
|
|
269
|
-
props = JSON.parse(parsed.propertiesJson) as unknown;
|
|
270
|
-
} catch {
|
|
271
|
-
throw new Error("propertiesJson must be valid JSON");
|
|
272
|
-
}
|
|
273
|
-
if (props === null || typeof props !== "object" || Array.isArray(props)) {
|
|
274
|
-
throw new Error("propertiesJson must be a JSON object");
|
|
275
|
-
}
|
|
276
|
-
const id = encodeURIComponent(parsed.pageId);
|
|
277
|
-
const res = await notionFetch(`/pages/${id}`, {
|
|
278
|
-
method: "PATCH",
|
|
279
|
-
body: JSON.stringify({ properties: props }),
|
|
280
|
-
});
|
|
281
|
-
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
282
|
-
},
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
const notionBlockAppendSchema = z.object({
|
|
286
|
-
parentBlockId: z.string().min(1),
|
|
287
|
-
childrenJson: z.string().min(1),
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
registerWriteTool(
|
|
291
|
-
"notion_block_append",
|
|
292
|
-
{
|
|
293
|
-
mutates: "notion.block.append",
|
|
294
|
-
recoverable: true,
|
|
295
|
-
scopeTargetOf: (p) => ({ kind: "block", value: p.parentBlockId }),
|
|
296
|
-
},
|
|
297
|
-
"Append blocks to a parent block (PATCH /v1/blocks/{id}/children). childrenJson is a JSON array of block objects.",
|
|
298
|
-
notionBlockAppendSchema,
|
|
299
|
-
async (parsed) => {
|
|
300
|
-
let children: unknown;
|
|
301
|
-
try {
|
|
302
|
-
children = JSON.parse(parsed.childrenJson) as unknown;
|
|
303
|
-
} catch {
|
|
304
|
-
throw new Error("childrenJson must be valid JSON");
|
|
305
|
-
}
|
|
306
|
-
if (!Array.isArray(children)) {
|
|
307
|
-
throw new TypeError("childrenJson must be a JSON array");
|
|
308
|
-
}
|
|
309
|
-
const id = encodeURIComponent(parsed.parentBlockId);
|
|
310
|
-
const res = await notionFetch(`/blocks/${id}/children`, {
|
|
311
|
-
method: "PATCH",
|
|
312
|
-
body: JSON.stringify({ children }),
|
|
313
|
-
});
|
|
314
|
-
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
315
|
-
},
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
const notionCommentCreateSchema = z.object({
|
|
319
|
-
pageId: z.string().min(1),
|
|
320
|
-
text: z.string().min(1),
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
registerWriteTool(
|
|
324
|
-
"notion_comment_create",
|
|
325
|
-
{
|
|
326
|
-
mutates: "notion.comment.create",
|
|
327
|
-
recoverable: true,
|
|
328
|
-
scopeTargetOf: (p) => ({ kind: "page", value: p.pageId }),
|
|
329
|
-
},
|
|
330
|
-
"Create a comment thread on a page (POST /v1/comments).",
|
|
331
|
-
notionCommentCreateSchema,
|
|
332
|
-
async (parsed) => {
|
|
333
|
-
const body = {
|
|
334
|
-
parent: { page_id: parsed.pageId },
|
|
335
|
-
rich_text: richText(parsed.text),
|
|
336
|
-
};
|
|
337
|
-
const res = await notionFetch("/comments", { method: "POST", body: JSON.stringify(body) });
|
|
338
|
-
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
339
|
-
},
|
|
340
|
-
);
|
|
7
|
+
registerNotionTools(server);
|
|
341
8
|
|
|
342
9
|
const transport = new StdioServerTransport();
|
|
343
10
|
await server.connect(transport);
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type ConsentServer, createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
|
|
3
|
+
import { fetchJsonText } from "../../../shared/fetch-json-text.ts";
|
|
4
|
+
import { joinApiPath } from "../../../shared/join-api-path.ts";
|
|
5
|
+
import { parseCitationsJson } from "../../../shared/kb-markdown.ts";
|
|
6
|
+
import {
|
|
7
|
+
createRegisterSimpleTool,
|
|
8
|
+
createZodToolRegistrar,
|
|
9
|
+
mcpJsonResult as jsonResult,
|
|
10
|
+
mcpJsonResultFromTextIfOk,
|
|
11
|
+
parseJsonTextIfOk,
|
|
12
|
+
putOptionalNonEmptyString,
|
|
13
|
+
requireProcessEnv,
|
|
14
|
+
} from "../../../shared/mcp-tool-kit.ts";
|
|
15
|
+
import { buildNotionKbPageBody } from "./kb-append.ts";
|
|
16
|
+
|
|
17
|
+
const NOTION_VERSION = "2022-06-28";
|
|
18
|
+
const API = "https://api.notion.com/v1";
|
|
19
|
+
|
|
20
|
+
async function notionFetch(
|
|
21
|
+
path: string,
|
|
22
|
+
init?: RequestInit,
|
|
23
|
+
): Promise<{ ok: boolean; status: number; text: string }> {
|
|
24
|
+
const token = requireProcessEnv("NOTION_ACCESS_TOKEN");
|
|
25
|
+
const url = joinApiPath(API, path);
|
|
26
|
+
return fetchJsonText(
|
|
27
|
+
url,
|
|
28
|
+
{ Authorization: `Bearer ${token}`, "Notion-Version": NOTION_VERSION },
|
|
29
|
+
init,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function richText(content: string): ReadonlyArray<Record<string, unknown>> {
|
|
34
|
+
return [{ type: "text", text: { content: content.slice(0, 2000) } }];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Tool names exposed by this connector — for contract/introspection tests. */
|
|
38
|
+
export const NOTION_TOOL_NAMES = [
|
|
39
|
+
"notion_page_list",
|
|
40
|
+
"notion_page_get",
|
|
41
|
+
"notion_database_list",
|
|
42
|
+
"notion_database_query",
|
|
43
|
+
"notion_block_children",
|
|
44
|
+
"notion_comment_list",
|
|
45
|
+
"notion_page_create",
|
|
46
|
+
"notion_kb_append",
|
|
47
|
+
"notion_page_update",
|
|
48
|
+
"notion_block_append",
|
|
49
|
+
"notion_comment_create",
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
export function registerNotionTools(
|
|
53
|
+
server: ConsentServer & { tool: (...args: never) => unknown },
|
|
54
|
+
): void {
|
|
55
|
+
const registerSimpleTool = createRegisterSimpleTool(server);
|
|
56
|
+
|
|
57
|
+
const reg = createZodToolRegistrar(registerSimpleTool);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Every MUTATING notion tool goes through here. Outside the gateway this adds the
|
|
61
|
+
* consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
|
|
62
|
+
* the gateway it is a pass-through, because executor.ts (I2) is the gate there.
|
|
63
|
+
*/
|
|
64
|
+
const registerWriteTool = createWriteToolRegistrar(server, {
|
|
65
|
+
connector: "notion",
|
|
66
|
+
scopeEnv: "NIMBUS_MCP_NOTION_WRITE_SCOPE",
|
|
67
|
+
scopeKinds: ["page", "database", "block"],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const notionSearchSchema = z.object({
|
|
71
|
+
query: z.string().optional(),
|
|
72
|
+
pageSize: z.number().int().min(1).max(100).optional(),
|
|
73
|
+
startCursor: z.string().optional(),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function notionSearchPostBody(
|
|
77
|
+
objectValue: "page" | "database",
|
|
78
|
+
parsed: z.infer<typeof notionSearchSchema>,
|
|
79
|
+
): Record<string, unknown> {
|
|
80
|
+
const body: Record<string, unknown> = {
|
|
81
|
+
filter: { property: "object", value: objectValue },
|
|
82
|
+
page_size: parsed.pageSize ?? 50,
|
|
83
|
+
};
|
|
84
|
+
putOptionalNonEmptyString(body, "query", parsed.query);
|
|
85
|
+
putOptionalNonEmptyString(body, "start_cursor", parsed.startCursor);
|
|
86
|
+
return body;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
reg(
|
|
90
|
+
"notion_page_list",
|
|
91
|
+
"Search Notion pages the integration can access (POST /v1/search, object filter page).",
|
|
92
|
+
notionSearchSchema,
|
|
93
|
+
async (parsed) => {
|
|
94
|
+
const body = notionSearchPostBody("page", parsed);
|
|
95
|
+
const res = await notionFetch("/search", { method: "POST", body: JSON.stringify(body) });
|
|
96
|
+
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const notionPageIdSchema = z.object({ pageId: z.string().min(1) });
|
|
101
|
+
|
|
102
|
+
reg(
|
|
103
|
+
"notion_page_get",
|
|
104
|
+
"Retrieve a Notion page and its direct child blocks.",
|
|
105
|
+
notionPageIdSchema,
|
|
106
|
+
async (parsed) => {
|
|
107
|
+
const id = encodeURIComponent(parsed.pageId);
|
|
108
|
+
const page = await notionFetch(`/pages/${id}`);
|
|
109
|
+
const blocks = await notionFetch(`/blocks/${id}/children?page_size=100`);
|
|
110
|
+
return jsonResult({
|
|
111
|
+
page: parseJsonTextIfOk("Notion", page),
|
|
112
|
+
blockChildren: parseJsonTextIfOk("Notion blocks", blocks),
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
reg(
|
|
118
|
+
"notion_database_list",
|
|
119
|
+
"Search Notion databases (POST /v1/search, object filter database).",
|
|
120
|
+
notionSearchSchema,
|
|
121
|
+
async (parsed) => {
|
|
122
|
+
const body = notionSearchPostBody("database", parsed);
|
|
123
|
+
const res = await notionFetch("/search", { method: "POST", body: JSON.stringify(body) });
|
|
124
|
+
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const notionDatabaseQuerySchema = z.object({
|
|
129
|
+
databaseId: z.string().min(1),
|
|
130
|
+
pageSize: z.number().int().min(1).max(100).optional(),
|
|
131
|
+
startCursor: z.string().optional(),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
reg(
|
|
135
|
+
"notion_database_query",
|
|
136
|
+
"Query a Notion database (POST /v1/databases/{id}/query).",
|
|
137
|
+
notionDatabaseQuerySchema,
|
|
138
|
+
async (parsed) => {
|
|
139
|
+
const id = encodeURIComponent(parsed.databaseId);
|
|
140
|
+
const body: Record<string, unknown> = {
|
|
141
|
+
page_size: parsed.pageSize ?? 50,
|
|
142
|
+
};
|
|
143
|
+
putOptionalNonEmptyString(body, "start_cursor", parsed.startCursor);
|
|
144
|
+
const res = await notionFetch(`/databases/${id}/query`, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
body: JSON.stringify(body),
|
|
147
|
+
});
|
|
148
|
+
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
149
|
+
},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const notionBlockPagedSchema = z.object({
|
|
153
|
+
blockId: z.string().min(1),
|
|
154
|
+
pageSize: z.number().int().min(1).max(100).optional(),
|
|
155
|
+
startCursor: z.string().optional(),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
reg(
|
|
159
|
+
"notion_block_children",
|
|
160
|
+
"List child blocks of a block or page (GET /v1/blocks/{id}/children).",
|
|
161
|
+
notionBlockPagedSchema,
|
|
162
|
+
async (parsed) => {
|
|
163
|
+
const id = encodeURIComponent(parsed.blockId);
|
|
164
|
+
const qs = new URLSearchParams({
|
|
165
|
+
page_size: String(parsed.pageSize ?? 50),
|
|
166
|
+
});
|
|
167
|
+
if (parsed.startCursor !== undefined && parsed.startCursor !== "") {
|
|
168
|
+
qs.set("start_cursor", parsed.startCursor);
|
|
169
|
+
}
|
|
170
|
+
const res = await notionFetch(`/blocks/${id}/children?${qs.toString()}`);
|
|
171
|
+
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
reg(
|
|
176
|
+
"notion_comment_list",
|
|
177
|
+
"List comments for a block or page (GET /v1/comments).",
|
|
178
|
+
notionBlockPagedSchema,
|
|
179
|
+
async (parsed) => {
|
|
180
|
+
const qs = new URLSearchParams({
|
|
181
|
+
block_id: parsed.blockId,
|
|
182
|
+
page_size: String(parsed.pageSize ?? 50),
|
|
183
|
+
});
|
|
184
|
+
if (parsed.startCursor !== undefined && parsed.startCursor !== "") {
|
|
185
|
+
qs.set("start_cursor", parsed.startCursor);
|
|
186
|
+
}
|
|
187
|
+
const res = await notionFetch(`/comments?${qs.toString()}`);
|
|
188
|
+
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const notionPageCreateSchema = z.object({
|
|
193
|
+
parentPageId: z.string().min(1),
|
|
194
|
+
title: z.string().min(1),
|
|
195
|
+
titlePropertyName: z.string().min(1).optional(),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
registerWriteTool(
|
|
199
|
+
"notion_page_create",
|
|
200
|
+
{
|
|
201
|
+
mutates: "notion.page.create",
|
|
202
|
+
recoverable: true,
|
|
203
|
+
scopeTargetOf: (p) => ({ kind: "page", value: p.parentPageId }),
|
|
204
|
+
},
|
|
205
|
+
"Create a page under a parent page (POST /v1/pages).",
|
|
206
|
+
notionPageCreateSchema,
|
|
207
|
+
async (parsed) => {
|
|
208
|
+
const prop = parsed.titlePropertyName ?? "title";
|
|
209
|
+
const body = {
|
|
210
|
+
parent: { page_id: parsed.parentPageId },
|
|
211
|
+
properties: {
|
|
212
|
+
[prop]: {
|
|
213
|
+
title: richText(parsed.title),
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
const res = await notionFetch("/pages", { method: "POST", body: JSON.stringify(body) });
|
|
218
|
+
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const notionKbAppendSchema = z.object({
|
|
223
|
+
databaseId: z.string().min(1),
|
|
224
|
+
title: z.string().min(1),
|
|
225
|
+
bodyMarkdown: z.string().min(1),
|
|
226
|
+
citationsJson: z.string().optional(),
|
|
227
|
+
titlePropertyName: z.string().min(1).optional(),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
registerWriteTool(
|
|
231
|
+
"notion_kb_append",
|
|
232
|
+
{
|
|
233
|
+
mutates: "notion.knowledge.write",
|
|
234
|
+
recoverable: true,
|
|
235
|
+
scopeTargetOf: (p) => ({ kind: "database", value: p.databaseId }),
|
|
236
|
+
},
|
|
237
|
+
"Create a knowledge-base page in a database from simple markdown + citations (POST /v1/pages). " +
|
|
238
|
+
"Used by Nimbus tribal-knowledge capture; the destination database is supplied by the gateway from local config only.",
|
|
239
|
+
notionKbAppendSchema,
|
|
240
|
+
async (parsed) => {
|
|
241
|
+
const body = buildNotionKbPageBody({
|
|
242
|
+
databaseId: parsed.databaseId,
|
|
243
|
+
title: parsed.title,
|
|
244
|
+
bodyMarkdown: parsed.bodyMarkdown,
|
|
245
|
+
citations: parseCitationsJson(parsed.citationsJson ?? "[]"),
|
|
246
|
+
...(parsed.titlePropertyName === undefined
|
|
247
|
+
? {}
|
|
248
|
+
: { titlePropertyName: parsed.titlePropertyName }),
|
|
249
|
+
});
|
|
250
|
+
const res = await notionFetch("/pages", { method: "POST", body: JSON.stringify(body) });
|
|
251
|
+
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const notionPageUpdateSchema = z.object({
|
|
256
|
+
pageId: z.string().min(1),
|
|
257
|
+
propertiesJson: z.string().min(1),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
registerWriteTool(
|
|
261
|
+
"notion_page_update",
|
|
262
|
+
{
|
|
263
|
+
mutates: "notion.page.update",
|
|
264
|
+
recoverable: true,
|
|
265
|
+
scopeTargetOf: (p) => ({ kind: "page", value: p.pageId }),
|
|
266
|
+
},
|
|
267
|
+
"Update page properties (PATCH /v1/pages/{id}). Pass properties JSON as string.",
|
|
268
|
+
notionPageUpdateSchema,
|
|
269
|
+
async (parsed) => {
|
|
270
|
+
let props: unknown;
|
|
271
|
+
try {
|
|
272
|
+
props = JSON.parse(parsed.propertiesJson) as unknown;
|
|
273
|
+
} catch {
|
|
274
|
+
throw new Error("propertiesJson must be valid JSON");
|
|
275
|
+
}
|
|
276
|
+
if (props === null || typeof props !== "object" || Array.isArray(props)) {
|
|
277
|
+
throw new Error("propertiesJson must be a JSON object");
|
|
278
|
+
}
|
|
279
|
+
const id = encodeURIComponent(parsed.pageId);
|
|
280
|
+
const res = await notionFetch(`/pages/${id}`, {
|
|
281
|
+
method: "PATCH",
|
|
282
|
+
body: JSON.stringify({ properties: props }),
|
|
283
|
+
});
|
|
284
|
+
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
285
|
+
},
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const notionBlockAppendSchema = z.object({
|
|
289
|
+
parentBlockId: z.string().min(1),
|
|
290
|
+
childrenJson: z.string().min(1),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
registerWriteTool(
|
|
294
|
+
"notion_block_append",
|
|
295
|
+
{
|
|
296
|
+
mutates: "notion.block.append",
|
|
297
|
+
recoverable: true,
|
|
298
|
+
scopeTargetOf: (p) => ({ kind: "block", value: p.parentBlockId }),
|
|
299
|
+
},
|
|
300
|
+
"Append blocks to a parent block (PATCH /v1/blocks/{id}/children). childrenJson is a JSON array of block objects.",
|
|
301
|
+
notionBlockAppendSchema,
|
|
302
|
+
async (parsed) => {
|
|
303
|
+
let children: unknown;
|
|
304
|
+
try {
|
|
305
|
+
children = JSON.parse(parsed.childrenJson) as unknown;
|
|
306
|
+
} catch {
|
|
307
|
+
throw new Error("childrenJson must be valid JSON");
|
|
308
|
+
}
|
|
309
|
+
if (!Array.isArray(children)) {
|
|
310
|
+
throw new TypeError("childrenJson must be a JSON array");
|
|
311
|
+
}
|
|
312
|
+
const id = encodeURIComponent(parsed.parentBlockId);
|
|
313
|
+
const res = await notionFetch(`/blocks/${id}/children`, {
|
|
314
|
+
method: "PATCH",
|
|
315
|
+
body: JSON.stringify({ children }),
|
|
316
|
+
});
|
|
317
|
+
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const notionCommentCreateSchema = z.object({
|
|
322
|
+
pageId: z.string().min(1),
|
|
323
|
+
text: z.string().min(1),
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
registerWriteTool(
|
|
327
|
+
"notion_comment_create",
|
|
328
|
+
{
|
|
329
|
+
mutates: "notion.comment.create",
|
|
330
|
+
recoverable: true,
|
|
331
|
+
scopeTargetOf: (p) => ({ kind: "page", value: p.pageId }),
|
|
332
|
+
},
|
|
333
|
+
"Create a comment thread on a page (POST /v1/comments).",
|
|
334
|
+
notionCommentCreateSchema,
|
|
335
|
+
async (parsed) => {
|
|
336
|
+
const body = {
|
|
337
|
+
parent: { page_id: parsed.pageId },
|
|
338
|
+
rich_text: richText(parsed.text),
|
|
339
|
+
};
|
|
340
|
+
const res = await notionFetch("/comments", { method: "POST", body: JSON.stringify(body) });
|
|
341
|
+
return mcpJsonResultFromTextIfOk("Notion", res);
|
|
342
|
+
},
|
|
343
|
+
);
|
|
344
|
+
}
|