@nimbus-dev/connectors 0.2.0 → 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.
Files changed (215) hide show
  1. package/README.md +18 -4
  2. package/connectors/airflow/src/search-filter.ts +2 -15
  3. package/connectors/airflow/src/server.ts +2 -87
  4. package/connectors/airflow/src/tools.ts +74 -0
  5. package/connectors/apple/src/adapters.ts +326 -0
  6. package/connectors/apple/src/apple-mail-core.ts +4 -8
  7. package/connectors/apple/src/calendar-tools.ts +28 -8
  8. package/connectors/apple/src/server.ts +23 -512
  9. package/connectors/apple/src/tools.ts +18 -44
  10. package/connectors/argocd/src/server.ts +25 -38
  11. package/connectors/athena/src/tools.ts +15 -34
  12. package/connectors/aws/src/server.ts +2 -183
  13. package/connectors/aws/src/tools.ts +197 -0
  14. package/connectors/azure/src/server.ts +2 -131
  15. package/connectors/azure/src/tools.ts +133 -0
  16. package/connectors/bigeye/src/search-filter.ts +4 -15
  17. package/connectors/bigeye/src/server.ts +38 -38
  18. package/connectors/bigquery/src/tools.ts +1 -26
  19. package/connectors/bitbucket/src/server.ts +2 -244
  20. package/connectors/bitbucket/src/tools.ts +241 -0
  21. package/connectors/bitrise/src/server.ts +2 -79
  22. package/connectors/bitrise/src/tools.ts +76 -0
  23. package/connectors/canva/src/search-filter.ts +2 -11
  24. package/connectors/canva/src/server.ts +2 -73
  25. package/connectors/canva/src/tools.ts +66 -0
  26. package/connectors/circleci/src/server.ts +2 -176
  27. package/connectors/circleci/src/tools.ts +193 -0
  28. package/connectors/cloud-logging/src/tools.ts +14 -27
  29. package/connectors/cloudwatch/src/tools.ts +15 -34
  30. package/connectors/codemagic/src/server.ts +2 -70
  31. package/connectors/codemagic/src/tools.ts +71 -0
  32. package/connectors/confluence/src/server.ts +2 -301
  33. package/connectors/confluence/src/tools.ts +320 -0
  34. package/connectors/dagster/src/server.ts +2 -214
  35. package/connectors/dagster/src/tools.ts +187 -0
  36. package/connectors/databricks/src/search-filter.ts +7 -7
  37. package/connectors/databricks/src/server.ts +2 -77
  38. package/connectors/databricks/src/tools.ts +66 -0
  39. package/connectors/datadog/src/server.ts +2 -48
  40. package/connectors/datadog/src/tools.ts +48 -0
  41. package/connectors/dataprofile/src/profile.ts +16 -46
  42. package/connectors/dbt/src/server.ts +2 -92
  43. package/connectors/dbt/src/tools.ts +80 -0
  44. package/connectors/dependencytrack/src/server.ts +2 -76
  45. package/connectors/dependencytrack/src/tools.ts +65 -0
  46. package/connectors/discord/src/server.ts +2 -85
  47. package/connectors/discord/src/tools.ts +82 -0
  48. package/connectors/elasticsearch/src/tools.ts +7 -37
  49. package/connectors/fastmail/src/jmap-client.ts +202 -0
  50. package/connectors/fastmail/src/jmap-core.ts +8 -21
  51. package/connectors/fastmail/src/server.ts +2 -187
  52. package/connectors/fastmail/src/tools.ts +4 -23
  53. package/connectors/figma/src/search-filter.ts +2 -11
  54. package/connectors/figma/src/server.ts +2 -113
  55. package/connectors/figma/src/tools.ts +99 -0
  56. package/connectors/firebase/src/server.ts +2 -90
  57. package/connectors/firebase/src/token.ts +2 -5
  58. package/connectors/firebase/src/tools.ts +106 -0
  59. package/connectors/flagsmith/src/server.ts +2 -98
  60. package/connectors/flagsmith/src/tools.ts +95 -0
  61. package/connectors/flux/src/server.ts +52 -53
  62. package/connectors/gcp/src/server.ts +2 -132
  63. package/connectors/gcp/src/tools.ts +123 -0
  64. package/connectors/github/src/server.ts +2 -328
  65. package/connectors/github/src/tools.ts +311 -0
  66. package/connectors/github-actions/src/server.ts +2 -205
  67. package/connectors/github-actions/src/tools.ts +209 -0
  68. package/connectors/gitlab/src/server.ts +2 -384
  69. package/connectors/gitlab/src/tools.ts +392 -0
  70. package/connectors/gmail/src/server.ts +2 -270
  71. package/connectors/gmail/src/tools.ts +209 -0
  72. package/connectors/google-drive/src/server.ts +2 -524
  73. package/connectors/google-drive/src/tools.ts +529 -0
  74. package/connectors/google-meet/src/server.ts +2 -82
  75. package/connectors/google-meet/src/tools.ts +87 -0
  76. package/connectors/google-photos/src/server.ts +2 -132
  77. package/connectors/google-photos/src/tools.ts +138 -0
  78. package/connectors/grafana/src/server.ts +2 -53
  79. package/connectors/grafana/src/tools.ts +53 -0
  80. package/connectors/great-expectations/src/gx-parse.ts +14 -45
  81. package/connectors/greenhouse/src/server.ts +2 -63
  82. package/connectors/greenhouse/src/tools.ts +68 -0
  83. package/connectors/hubspot/src/server.ts +2 -77
  84. package/connectors/hubspot/src/tools.ts +70 -0
  85. package/connectors/iac/src/server.ts +2 -137
  86. package/connectors/iac/src/tools.ts +152 -0
  87. package/connectors/imap/src/imap-core.ts +29 -82
  88. package/connectors/imap/src/server.ts +33 -220
  89. package/connectors/intercom/src/server.ts +2 -61
  90. package/connectors/intercom/src/tools.ts +58 -0
  91. package/connectors/jenkins/src/jenkins-api.ts +2 -6
  92. package/connectors/jenkins/src/server.ts +2 -215
  93. package/connectors/jenkins/src/tools.ts +240 -0
  94. package/connectors/jira/src/server.ts +2 -301
  95. package/connectors/jira/src/tools.ts +325 -0
  96. package/connectors/kubernetes/src/server.ts +2 -155
  97. package/connectors/kubernetes/src/tools.ts +159 -0
  98. package/connectors/launchdarkly/src/server.ts +2 -75
  99. package/connectors/launchdarkly/src/tools.ts +72 -0
  100. package/connectors/lever/src/search-filter.ts +4 -12
  101. package/connectors/lever/src/server.ts +2 -60
  102. package/connectors/lever/src/tools.ts +61 -0
  103. package/connectors/linear/src/server.ts +2 -459
  104. package/connectors/linear/src/tools.ts +482 -0
  105. package/connectors/localdb/src/sql-scan.ts +13 -41
  106. package/connectors/localdb/src/tools.ts +1 -1
  107. package/connectors/looker/src/search-filter.ts +2 -15
  108. package/connectors/looker/src/server.ts +35 -70
  109. package/connectors/mendeley/src/server.ts +2 -55
  110. package/connectors/mendeley/src/tools.ts +54 -0
  111. package/connectors/mercury/src/server.ts +2 -57
  112. package/connectors/mercury/src/tools.ts +50 -0
  113. package/connectors/metabase/src/search-filter.ts +4 -15
  114. package/connectors/metabase/src/server.ts +2 -79
  115. package/connectors/metabase/src/tools.ts +64 -0
  116. package/connectors/miro/src/server.ts +2 -72
  117. package/connectors/miro/src/tools.ts +65 -0
  118. package/connectors/mlflow/src/search-filter.ts +3 -7
  119. package/connectors/mlflow/src/server.ts +20 -40
  120. package/connectors/monte-carlo/src/server.ts +50 -57
  121. package/connectors/netlify/src/search-filter.ts +6 -14
  122. package/connectors/netlify/src/server.ts +2 -60
  123. package/connectors/netlify/src/tools.ts +53 -0
  124. package/connectors/newrelic/src/server.ts +2 -40
  125. package/connectors/newrelic/src/tools.ts +37 -0
  126. package/connectors/notion/src/server.ts +2 -335
  127. package/connectors/notion/src/tools.ts +344 -0
  128. package/connectors/obsidian/src/server.ts +2 -425
  129. package/connectors/obsidian/src/tools.ts +466 -0
  130. package/connectors/onedrive/src/server.ts +2 -249
  131. package/connectors/onedrive/src/tools.ts +253 -0
  132. package/connectors/outlook/src/server.ts +2 -351
  133. package/connectors/outlook/src/tools.ts +357 -0
  134. package/connectors/pagerduty/src/server.ts +2 -132
  135. package/connectors/pagerduty/src/tools.ts +126 -0
  136. package/connectors/pipedrive/src/server.ts +2 -58
  137. package/connectors/pipedrive/src/tools.ts +63 -0
  138. package/connectors/powerbi/src/search-filter.ts +2 -15
  139. package/connectors/powerbi/src/server.ts +43 -68
  140. package/connectors/prefect/src/search-filter.ts +3 -16
  141. package/connectors/prefect/src/server.ts +2 -90
  142. package/connectors/prefect/src/tools.ts +83 -0
  143. package/connectors/protonmail/src/mail-core.ts +16 -75
  144. package/connectors/protonmail/src/server.ts +34 -214
  145. package/connectors/raindrop/src/server.ts +2 -103
  146. package/connectors/raindrop/src/tools.ts +103 -0
  147. package/connectors/ramp/src/server.ts +2 -105
  148. package/connectors/ramp/src/tools.ts +98 -0
  149. package/connectors/readwise/src/server.ts +2 -88
  150. package/connectors/readwise/src/tools.ts +81 -0
  151. package/connectors/sagemaker/src/tools.ts +19 -38
  152. package/connectors/salesforce/src/search-filter.ts +4 -11
  153. package/connectors/salesforce/src/server.ts +2 -90
  154. package/connectors/salesforce/src/tools.ts +75 -0
  155. package/connectors/semgrep/src/search-filter.ts +3 -10
  156. package/connectors/semgrep/src/server.ts +2 -104
  157. package/connectors/semgrep/src/tools.ts +92 -0
  158. package/connectors/sentry/src/server.ts +2 -58
  159. package/connectors/sentry/src/tools.ts +53 -0
  160. package/connectors/slack/src/server.ts +2 -352
  161. package/connectors/slack/src/tools.ts +376 -0
  162. package/connectors/snowflake/src/search-filter.ts +4 -19
  163. package/connectors/snowflake/src/server.ts +6 -25
  164. package/connectors/snyk/src/server.ts +2 -140
  165. package/connectors/snyk/src/tools.ts +136 -0
  166. package/connectors/sonarqube/src/server.ts +2 -111
  167. package/connectors/sonarqube/src/tools.ts +98 -0
  168. package/connectors/stackoverflow/src/server.ts +2 -77
  169. package/connectors/stackoverflow/src/tools.ts +66 -0
  170. package/connectors/storybook/src/storybook-parse.ts +3 -6
  171. package/connectors/stripe/src/server.ts +2 -61
  172. package/connectors/stripe/src/tools.ts +54 -0
  173. package/connectors/superset/src/search-filter.ts +4 -15
  174. package/connectors/superset/src/server.ts +2 -115
  175. package/connectors/superset/src/tools.ts +98 -0
  176. package/connectors/tableau/src/search-filter.ts +2 -15
  177. package/connectors/tableau/src/server.ts +13 -65
  178. package/connectors/teams/src/server.ts +2 -249
  179. package/connectors/teams/src/tools.ts +283 -0
  180. package/connectors/testflight/src/server.ts +2 -70
  181. package/connectors/testflight/src/tools.ts +79 -0
  182. package/connectors/vercel/src/search-filter.ts +2 -10
  183. package/connectors/vercel/src/server.ts +2 -75
  184. package/connectors/vercel/src/tools.ts +73 -0
  185. package/connectors/vertex-ai/src/tools.ts +23 -33
  186. package/connectors/wiz/src/search-filter.ts +6 -30
  187. package/connectors/wiz/src/server.ts +2 -188
  188. package/connectors/wiz/src/tools.ts +168 -0
  189. package/connectors/zendesk/src/server.ts +2 -73
  190. package/connectors/zendesk/src/tools.ts +60 -0
  191. package/connectors/zoom/src/search-filter.ts +1 -7
  192. package/connectors/zoom/src/server.ts +2 -94
  193. package/connectors/zoom/src/tools.ts +93 -0
  194. package/connectors/zotero/src/server.ts +2 -78
  195. package/connectors/zotero/src/tools.ts +67 -0
  196. package/package.json +15 -12
  197. package/shared/access-token-cache.ts +123 -0
  198. package/shared/atlassian-json-fetch.ts +6 -16
  199. package/shared/audit-chain.ts +13 -6
  200. package/shared/cli-json-kit.ts +150 -0
  201. package/shared/collection-tool-kit.ts +143 -0
  202. package/shared/connector-mode.ts +3 -2
  203. package/shared/cursor-list-tool.ts +29 -0
  204. package/shared/env-json-api.ts +209 -0
  205. package/shared/fetch-json-text.ts +27 -0
  206. package/shared/github-rest.ts +62 -0
  207. package/shared/graphql-json.ts +60 -0
  208. package/shared/imap-mail-core.ts +91 -0
  209. package/shared/imap-tool-kit.ts +57 -23
  210. package/shared/imapflow-adapter.ts +417 -0
  211. package/shared/rest-tool-kit.ts +77 -7
  212. package/shared/run-cli-json.ts +38 -16
  213. package/shared/search-filter.ts +58 -18
  214. package/shared/walk-files.ts +118 -0
  215. 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 { z } from "zod";
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
- const registerSimpleTool = createRegisterSimpleTool(server);
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
+ }