@nimbus-dev/connectors 0.1.0

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 (301) hide show
  1. package/LICENSE +662 -0
  2. package/NOTICE +40 -0
  3. package/README.md +111 -0
  4. package/connectors/airflow/nimbus.extension.json +16 -0
  5. package/connectors/airflow/src/search-filter.ts +38 -0
  6. package/connectors/airflow/src/server.ts +91 -0
  7. package/connectors/apple/nimbus.extension.json +40 -0
  8. package/connectors/apple/src/apple-mail-core.ts +43 -0
  9. package/connectors/apple/src/caldav-core.ts +116 -0
  10. package/connectors/apple/src/calendar-tools.ts +278 -0
  11. package/connectors/apple/src/server.ts +541 -0
  12. package/connectors/apple/src/tools.ts +138 -0
  13. package/connectors/argocd/nimbus.extension.json +16 -0
  14. package/connectors/argocd/src/search-filter.ts +24 -0
  15. package/connectors/argocd/src/server.ts +158 -0
  16. package/connectors/athena/nimbus.extension.json +15 -0
  17. package/connectors/athena/src/server.ts +6 -0
  18. package/connectors/athena/src/tools.ts +181 -0
  19. package/connectors/aws/nimbus.extension.json +21 -0
  20. package/connectors/aws/src/server.ts +191 -0
  21. package/connectors/azure/nimbus.extension.json +15 -0
  22. package/connectors/azure/src/server.ts +139 -0
  23. package/connectors/bigeye/nimbus.extension.json +16 -0
  24. package/connectors/bigeye/src/search-filter.ts +22 -0
  25. package/connectors/bigeye/src/server.ts +161 -0
  26. package/connectors/bigquery/nimbus.extension.json +15 -0
  27. package/connectors/bigquery/src/server.ts +6 -0
  28. package/connectors/bigquery/src/tools.ts +175 -0
  29. package/connectors/bitbucket/nimbus.extension.json +15 -0
  30. package/connectors/bitbucket/src/server.ts +252 -0
  31. package/connectors/bitrise/nimbus.extension.json +15 -0
  32. package/connectors/bitrise/src/search-filter.ts +11 -0
  33. package/connectors/bitrise/src/server.ts +83 -0
  34. package/connectors/canva/nimbus.extension.json +15 -0
  35. package/connectors/canva/src/search-filter.ts +23 -0
  36. package/connectors/canva/src/server.ts +77 -0
  37. package/connectors/circleci/nimbus.extension.json +15 -0
  38. package/connectors/circleci/src/server.ts +184 -0
  39. package/connectors/cloud-logging/nimbus.extension.json +15 -0
  40. package/connectors/cloud-logging/src/server.ts +6 -0
  41. package/connectors/cloud-logging/src/tools.ts +115 -0
  42. package/connectors/cloudwatch/nimbus.extension.json +15 -0
  43. package/connectors/cloudwatch/src/server.ts +6 -0
  44. package/connectors/cloudwatch/src/tools.ts +144 -0
  45. package/connectors/codemagic/nimbus.extension.json +15 -0
  46. package/connectors/codemagic/src/search-filter.ts +11 -0
  47. package/connectors/codemagic/src/server.ts +74 -0
  48. package/connectors/confluence/nimbus.extension.json +15 -0
  49. package/connectors/confluence/src/kb-append.ts +25 -0
  50. package/connectors/confluence/src/server.ts +309 -0
  51. package/connectors/dagster/nimbus.extension.json +16 -0
  52. package/connectors/dagster/src/search-filter.ts +53 -0
  53. package/connectors/dagster/src/server.ts +218 -0
  54. package/connectors/databricks/nimbus.extension.json +16 -0
  55. package/connectors/databricks/src/search-filter.ts +28 -0
  56. package/connectors/databricks/src/server.ts +81 -0
  57. package/connectors/datadog/nimbus.extension.json +15 -0
  58. package/connectors/datadog/src/server.ts +56 -0
  59. package/connectors/dataprofile/nimbus.extension.json +16 -0
  60. package/connectors/dataprofile/src/profile.ts +298 -0
  61. package/connectors/dataprofile/src/server.ts +6 -0
  62. package/connectors/dataprofile/src/tools.ts +69 -0
  63. package/connectors/dbt/nimbus.extension.json +16 -0
  64. package/connectors/dbt/src/search-filter.ts +28 -0
  65. package/connectors/dbt/src/server.ts +96 -0
  66. package/connectors/dependencytrack/nimbus.extension.json +16 -0
  67. package/connectors/dependencytrack/src/search-filter.ts +24 -0
  68. package/connectors/dependencytrack/src/server.ts +80 -0
  69. package/connectors/discord/nimbus.extension.json +15 -0
  70. package/connectors/discord/src/server.ts +93 -0
  71. package/connectors/elasticsearch/nimbus.extension.json +16 -0
  72. package/connectors/elasticsearch/src/server.ts +6 -0
  73. package/connectors/elasticsearch/src/tools.ts +110 -0
  74. package/connectors/fastmail/nimbus.extension.json +16 -0
  75. package/connectors/fastmail/src/jmap-core.ts +86 -0
  76. package/connectors/fastmail/src/server.ts +194 -0
  77. package/connectors/fastmail/src/tools.ts +143 -0
  78. package/connectors/figma/nimbus.extension.json +15 -0
  79. package/connectors/figma/src/search-filter.ts +24 -0
  80. package/connectors/figma/src/server.ts +117 -0
  81. package/connectors/firebase/nimbus.extension.json +15 -0
  82. package/connectors/firebase/src/search-filter.ts +30 -0
  83. package/connectors/firebase/src/server.ts +94 -0
  84. package/connectors/firebase/src/token.ts +41 -0
  85. package/connectors/flagsmith/nimbus.extension.json +15 -0
  86. package/connectors/flagsmith/src/search-filter.ts +29 -0
  87. package/connectors/flagsmith/src/server.ts +102 -0
  88. package/connectors/flux/nimbus.extension.json +16 -0
  89. package/connectors/flux/src/search-filter.ts +42 -0
  90. package/connectors/flux/src/server.ts +182 -0
  91. package/connectors/gcp/nimbus.extension.json +15 -0
  92. package/connectors/gcp/src/server.ts +140 -0
  93. package/connectors/github/nimbus.extension.json +15 -0
  94. package/connectors/github/src/server.ts +347 -0
  95. package/connectors/github-actions/nimbus.extension.json +15 -0
  96. package/connectors/github-actions/src/server.ts +213 -0
  97. package/connectors/gitlab/nimbus.extension.json +15 -0
  98. package/connectors/gitlab/src/server.ts +392 -0
  99. package/connectors/gmail/nimbus.extension.json +15 -0
  100. package/connectors/gmail/src/server.ts +277 -0
  101. package/connectors/google-drive/nimbus.extension.json +15 -0
  102. package/connectors/google-drive/src/drive-query.ts +3 -0
  103. package/connectors/google-drive/src/server.ts +531 -0
  104. package/connectors/google-meet/nimbus.extension.json +15 -0
  105. package/connectors/google-meet/src/server.ts +89 -0
  106. package/connectors/google-photos/nimbus.extension.json +15 -0
  107. package/connectors/google-photos/src/server.ts +139 -0
  108. package/connectors/grafana/nimbus.extension.json +15 -0
  109. package/connectors/grafana/src/server.ts +61 -0
  110. package/connectors/great-expectations/nimbus.extension.json +16 -0
  111. package/connectors/great-expectations/src/gx-parse.ts +292 -0
  112. package/connectors/great-expectations/src/server.ts +6 -0
  113. package/connectors/great-expectations/src/tools.ts +91 -0
  114. package/connectors/greenhouse/nimbus.extension.json +15 -0
  115. package/connectors/greenhouse/src/search-filter.ts +65 -0
  116. package/connectors/greenhouse/src/server.ts +67 -0
  117. package/connectors/hubspot/nimbus.extension.json +15 -0
  118. package/connectors/hubspot/src/search-filter.ts +28 -0
  119. package/connectors/hubspot/src/server.ts +81 -0
  120. package/connectors/iac/nimbus.extension.json +14 -0
  121. package/connectors/iac/src/server.ts +145 -0
  122. package/connectors/imap/nimbus.extension.json +16 -0
  123. package/connectors/imap/src/imap-core.ts +110 -0
  124. package/connectors/imap/src/server.ts +232 -0
  125. package/connectors/imap/src/tools.ts +40 -0
  126. package/connectors/intercom/nimbus.extension.json +15 -0
  127. package/connectors/intercom/src/search-filter.ts +46 -0
  128. package/connectors/intercom/src/server.ts +65 -0
  129. package/connectors/jenkins/nimbus.extension.json +15 -0
  130. package/connectors/jenkins/src/jenkins-api.ts +156 -0
  131. package/connectors/jenkins/src/server.ts +223 -0
  132. package/connectors/jira/nimbus.extension.json +15 -0
  133. package/connectors/jira/src/server.ts +309 -0
  134. package/connectors/kubernetes/nimbus.extension.json +15 -0
  135. package/connectors/kubernetes/src/server.ts +163 -0
  136. package/connectors/launchdarkly/nimbus.extension.json +15 -0
  137. package/connectors/launchdarkly/src/search-filter.ts +31 -0
  138. package/connectors/launchdarkly/src/server.ts +79 -0
  139. package/connectors/lever/nimbus.extension.json +15 -0
  140. package/connectors/lever/src/search-filter.ts +35 -0
  141. package/connectors/lever/src/server.ts +64 -0
  142. package/connectors/linear/nimbus.extension.json +15 -0
  143. package/connectors/linear/src/server.ts +467 -0
  144. package/connectors/localdb/nimbus.extension.json +16 -0
  145. package/connectors/localdb/src/server.ts +6 -0
  146. package/connectors/localdb/src/sql-scan.ts +158 -0
  147. package/connectors/localdb/src/tools.ts +67 -0
  148. package/connectors/looker/nimbus.extension.json +16 -0
  149. package/connectors/looker/src/search-filter.ts +22 -0
  150. package/connectors/looker/src/server.ts +254 -0
  151. package/connectors/mendeley/nimbus.extension.json +15 -0
  152. package/connectors/mendeley/src/search-filter.ts +68 -0
  153. package/connectors/mendeley/src/server.ts +59 -0
  154. package/connectors/mercury/nimbus.extension.json +15 -0
  155. package/connectors/mercury/src/search-filter.ts +11 -0
  156. package/connectors/mercury/src/server.ts +61 -0
  157. package/connectors/metabase/nimbus.extension.json +16 -0
  158. package/connectors/metabase/src/search-filter.ts +22 -0
  159. package/connectors/metabase/src/server.ts +83 -0
  160. package/connectors/miro/nimbus.extension.json +15 -0
  161. package/connectors/miro/src/search-filter.ts +24 -0
  162. package/connectors/miro/src/server.ts +76 -0
  163. package/connectors/mlflow/nimbus.extension.json +16 -0
  164. package/connectors/mlflow/src/search-filter.ts +38 -0
  165. package/connectors/mlflow/src/server.ts +167 -0
  166. package/connectors/monte-carlo/nimbus.extension.json +16 -0
  167. package/connectors/monte-carlo/src/search-filter.ts +11 -0
  168. package/connectors/monte-carlo/src/server.ts +241 -0
  169. package/connectors/netlify/nimbus.extension.json +15 -0
  170. package/connectors/netlify/src/search-filter.ts +37 -0
  171. package/connectors/netlify/src/server.ts +64 -0
  172. package/connectors/newrelic/nimbus.extension.json +15 -0
  173. package/connectors/newrelic/src/server.ts +48 -0
  174. package/connectors/notion/nimbus.extension.json +15 -0
  175. package/connectors/notion/src/kb-append.ts +23 -0
  176. package/connectors/notion/src/server.ts +343 -0
  177. package/connectors/obsidian/nimbus.extension.json +19 -0
  178. package/connectors/obsidian/src/server.ts +433 -0
  179. package/connectors/onedrive/nimbus.extension.json +15 -0
  180. package/connectors/onedrive/src/server.ts +256 -0
  181. package/connectors/outlook/nimbus.extension.json +15 -0
  182. package/connectors/outlook/src/server.ts +358 -0
  183. package/connectors/outlook/src/tool-scope-policy.ts +53 -0
  184. package/connectors/pagerduty/nimbus.extension.json +15 -0
  185. package/connectors/pagerduty/src/server.ts +140 -0
  186. package/connectors/pipedrive/nimbus.extension.json +15 -0
  187. package/connectors/pipedrive/src/search-filter.ts +11 -0
  188. package/connectors/pipedrive/src/server.ts +62 -0
  189. package/connectors/powerbi/nimbus.extension.json +16 -0
  190. package/connectors/powerbi/src/search-filter.ts +22 -0
  191. package/connectors/powerbi/src/server.ts +249 -0
  192. package/connectors/prefect/nimbus.extension.json +16 -0
  193. package/connectors/prefect/src/search-filter.ts +40 -0
  194. package/connectors/prefect/src/server.ts +94 -0
  195. package/connectors/protonmail/nimbus.extension.json +16 -0
  196. package/connectors/protonmail/src/mail-core.ts +96 -0
  197. package/connectors/protonmail/src/server.ts +231 -0
  198. package/connectors/protonmail/src/tools.ts +45 -0
  199. package/connectors/raindrop/nimbus.extension.json +15 -0
  200. package/connectors/raindrop/src/search-filter.ts +20 -0
  201. package/connectors/raindrop/src/server.ts +107 -0
  202. package/connectors/ramp/nimbus.extension.json +15 -0
  203. package/connectors/ramp/src/search-filter.ts +37 -0
  204. package/connectors/ramp/src/server.ts +109 -0
  205. package/connectors/readwise/nimbus.extension.json +15 -0
  206. package/connectors/readwise/src/search-filter.ts +64 -0
  207. package/connectors/readwise/src/server.ts +92 -0
  208. package/connectors/sagemaker/nimbus.extension.json +15 -0
  209. package/connectors/sagemaker/src/server.ts +6 -0
  210. package/connectors/sagemaker/src/tools.ts +121 -0
  211. package/connectors/salesforce/nimbus.extension.json +15 -0
  212. package/connectors/salesforce/src/search-filter.ts +23 -0
  213. package/connectors/salesforce/src/server.ts +94 -0
  214. package/connectors/semgrep/nimbus.extension.json +15 -0
  215. package/connectors/semgrep/src/search-filter.ts +31 -0
  216. package/connectors/semgrep/src/server.ts +108 -0
  217. package/connectors/sentry/nimbus.extension.json +15 -0
  218. package/connectors/sentry/src/server.ts +66 -0
  219. package/connectors/slack/nimbus.extension.json +15 -0
  220. package/connectors/slack/src/server.ts +360 -0
  221. package/connectors/snowflake/nimbus.extension.json +16 -0
  222. package/connectors/snowflake/src/search-filter.ts +26 -0
  223. package/connectors/snowflake/src/server.ts +236 -0
  224. package/connectors/snyk/nimbus.extension.json +15 -0
  225. package/connectors/snyk/src/search-filter.ts +28 -0
  226. package/connectors/snyk/src/server.ts +144 -0
  227. package/connectors/sonarqube/nimbus.extension.json +15 -0
  228. package/connectors/sonarqube/src/search-filter.ts +28 -0
  229. package/connectors/sonarqube/src/server.ts +115 -0
  230. package/connectors/stackoverflow/nimbus.extension.json +15 -0
  231. package/connectors/stackoverflow/src/search-filter.ts +52 -0
  232. package/connectors/stackoverflow/src/server.ts +81 -0
  233. package/connectors/storybook/nimbus.extension.json +16 -0
  234. package/connectors/storybook/src/server.ts +6 -0
  235. package/connectors/storybook/src/storybook-parse.ts +67 -0
  236. package/connectors/storybook/src/tools.ts +68 -0
  237. package/connectors/stripe/nimbus.extension.json +15 -0
  238. package/connectors/stripe/src/search-filter.ts +19 -0
  239. package/connectors/stripe/src/server.ts +65 -0
  240. package/connectors/superset/nimbus.extension.json +16 -0
  241. package/connectors/superset/src/search-filter.ts +22 -0
  242. package/connectors/superset/src/server.ts +119 -0
  243. package/connectors/tableau/nimbus.extension.json +16 -0
  244. package/connectors/tableau/src/search-filter.ts +22 -0
  245. package/connectors/tableau/src/server.ts +248 -0
  246. package/connectors/teams/nimbus.extension.json +20 -0
  247. package/connectors/teams/src/bot-send.ts +72 -0
  248. package/connectors/teams/src/server.ts +257 -0
  249. package/connectors/testflight/nimbus.extension.json +15 -0
  250. package/connectors/testflight/src/jwt.ts +27 -0
  251. package/connectors/testflight/src/search-filter.ts +30 -0
  252. package/connectors/testflight/src/server.ts +74 -0
  253. package/connectors/vercel/nimbus.extension.json +15 -0
  254. package/connectors/vercel/src/search-filter.ts +34 -0
  255. package/connectors/vercel/src/server.ts +79 -0
  256. package/connectors/vertex-ai/nimbus.extension.json +15 -0
  257. package/connectors/vertex-ai/src/server.ts +6 -0
  258. package/connectors/vertex-ai/src/tools.ts +145 -0
  259. package/connectors/wiz/nimbus.extension.json +15 -0
  260. package/connectors/wiz/src/search-filter.ts +50 -0
  261. package/connectors/wiz/src/server.ts +192 -0
  262. package/connectors/workday/nimbus.extension.json +19 -0
  263. package/connectors/workday/src/search-filter.ts +5 -0
  264. package/connectors/workday/src/server.ts +78 -0
  265. package/connectors/workday/src/tools.ts +1 -0
  266. package/connectors/zendesk/nimbus.extension.json +16 -0
  267. package/connectors/zendesk/src/search-filter.ts +11 -0
  268. package/connectors/zendesk/src/server.ts +77 -0
  269. package/connectors/zoom/nimbus.extension.json +15 -0
  270. package/connectors/zoom/src/encode-meeting-path.ts +5 -0
  271. package/connectors/zoom/src/recordings-window.ts +34 -0
  272. package/connectors/zoom/src/search-filter.ts +42 -0
  273. package/connectors/zoom/src/server.ts +98 -0
  274. package/connectors/zotero/nimbus.extension.json +15 -0
  275. package/connectors/zotero/src/search-filter.ts +81 -0
  276. package/connectors/zotero/src/server.ts +82 -0
  277. package/package.json +152 -0
  278. package/shared/atlassian-json-fetch.ts +42 -0
  279. package/shared/audit-chain.ts +133 -0
  280. package/shared/connector-mode.ts +60 -0
  281. package/shared/consent-kit.ts +323 -0
  282. package/shared/fetch-bearer-json.ts +18 -0
  283. package/shared/header-safe.ts +28 -0
  284. package/shared/imap-bodystructure.ts +113 -0
  285. package/shared/imap-mail-core.ts +57 -0
  286. package/shared/imap-tool-kit.ts +293 -0
  287. package/shared/join-api-path.ts +18 -0
  288. package/shared/kb-markdown.ts +142 -0
  289. package/shared/mcp-search-tool.ts +37 -0
  290. package/shared/mcp-tool-kit.ts +35 -0
  291. package/shared/nimbus-spawn.ts +128 -0
  292. package/shared/rest-tool-kit.ts +18 -0
  293. package/shared/run-cli-json.ts +55 -0
  294. package/shared/run-read-only-mcp-connector.ts +59 -0
  295. package/shared/safe-cli-arg.ts +55 -0
  296. package/shared/search-filter.ts +140 -0
  297. package/shared/strip-trailing-slashes.ts +7 -0
  298. package/shared/write-scope.ts +58 -0
  299. package/standalone/src/bin.ts +19 -0
  300. package/standalone/src/launcher.ts +186 -0
  301. package/standalone/tsconfig.json +8 -0
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Which entrypoint started this connector process, and therefore whether the connector's own
3
+ * consent gate is in force.
4
+ *
5
+ * `"gateway"` — started by `run-bundled-connector.ts` as the gateway's `__nimbus-connector` role.
6
+ * The gateway's executor provides HITL (I2), so the connector registers its full tool surface.
7
+ *
8
+ * `"standalone"` — anything else. The connector hardens itself: write tools register only behind a
9
+ * client that can prompt a human, and every mutation is scope-checked, budgeted and audited.
10
+ *
11
+ * Standalone is the DEFAULT deliberately. If the gateway's one wiring line is ever lost, the
12
+ * gateway degrades to read-only — loud and safe — rather than the standalone build silently
13
+ * ungating. The failure mode must point that way.
14
+ *
15
+ * The mode is derived from which entrypoint ran, never from an env var: Non-Negotiable #2 forbids a
16
+ * consent gate that can be "configured away", and an env var in a client's JSON config is exactly
17
+ * that.
18
+ */
19
+ export type ConnectorMode = "gateway" | "standalone";
20
+
21
+ let current: ConnectorMode | undefined;
22
+
23
+ /**
24
+ * Lock the mode for this process.
25
+ *
26
+ * Set-once: re-asserting the same value is a no-op, but a CONFLICTING change throws. Reading via
27
+ * `getConnectorMode` also locks, so a gateway wire that runs too late — after a connector module
28
+ * has already registered tools against the default — fails loudly instead of leaving a surface that
29
+ * half-registered under one mode and half under another.
30
+ */
31
+ export function setConnectorMode(mode: ConnectorMode): void {
32
+ if (current !== undefined && current !== mode) {
33
+ throw new Error(
34
+ `connector mode already locked to "${current}"; refusing to change it to "${mode}"`,
35
+ );
36
+ }
37
+ current = mode;
38
+ }
39
+
40
+ /** The active mode, locking in the standalone default on first read. */
41
+ export function getConnectorMode(): ConnectorMode {
42
+ current ??= "standalone";
43
+ return current;
44
+ }
45
+
46
+ /**
47
+ * TEST-ONLY: clear the lock between cases. Never called from production code — the
48
+ * `audit:connector-consent` gate permits `setConnectorMode` only in the two sanctioned entrypoints,
49
+ * and this function exists so tests never need to reach for it.
50
+ *
51
+ * This matters more than it looks. `bun test` runs MANY TEST FILES IN ONE PROCESS — verified: two
52
+ * files sharing a module observed the same pid, and state set by the first was visible to the
53
+ * second. So a file that locks the mode and does not clear it changes the behaviour of every file
54
+ * that runs after it. Any test file that touches the mode must reset in BOTH `beforeEach` and
55
+ * `afterEach`: `beforeEach` protects this file from its predecessors, `afterEach` protects the
56
+ * suite from this one.
57
+ */
58
+ export function resetConnectorModeForTests(): void {
59
+ current = undefined;
60
+ }
@@ -0,0 +1,323 @@
1
+ import type { McpListResult, ZodObjectSchema } from "@nimbus-dev/sdk/connector-kit";
2
+
3
+ import { type AuditOutcome, appendAuditEntry } from "./audit-chain.ts";
4
+ import { getConnectorMode } from "./connector-mode.ts";
5
+ import { parseWriteScope, scopeAllows } from "./write-scope.ts";
6
+
7
+ /** Mutations allowed per process lifetime when NIMBUS_MCP_WRITE_BUDGET is unset. */
8
+ const DEFAULT_BUDGET = 10;
9
+
10
+ /** The subset of the SDK's `RegisteredTool` this kit needs. */
11
+ export type ToolHandle = { disable(): void };
12
+
13
+ /**
14
+ * The structural subset of `McpServer` the consent kit uses.
15
+ *
16
+ * Narrowed deliberately so tests can supply a fake without a transport, and so the kit's real
17
+ * dependencies are visible in one place.
18
+ */
19
+ export type ConsentServer = {
20
+ readonly server: {
21
+ getClientCapabilities(): { elicitation?: unknown } | undefined;
22
+ /** Fired after the client's `initialize` — the FIRST moment capabilities are knowable. */
23
+ oninitialized?: (() => void) | undefined;
24
+ /**
25
+ * `requestedSchema` is typed to the restricted JSON-Schema subset the MCP spec allows, not
26
+ * `Record<string, unknown>`. The loose form does not satisfy the real `McpServer.elicitInput`,
27
+ * which is only discoverable by passing a real server — a hand-written fake accepts anything.
28
+ */
29
+ elicitInput(
30
+ params: {
31
+ mode: "form";
32
+ message: string;
33
+ requestedSchema: {
34
+ type: "object";
35
+ properties: Record<
36
+ string,
37
+ { type: "boolean"; title?: string; description?: string; default?: boolean }
38
+ >;
39
+ required?: string[];
40
+ };
41
+ },
42
+ options?: { timeout?: number },
43
+ ): Promise<{
44
+ action: "accept" | "decline" | "cancel";
45
+ content?: Record<string, unknown> | undefined;
46
+ }>;
47
+ };
48
+ /**
49
+ * `inputSchema` is `unknown` on purpose. The real signature wants the SDK's
50
+ * `ZodRawShapeCompat | AnySchema`, and the connector kit's `ZodObjectSchema["shape"]` is
51
+ * `Record<string, unknown>` — neither is assignable to the other, so a concrete type here makes
52
+ * a real `McpServer` fail to satisfy this interface. The value is passed straight through and
53
+ * never inspected by this kit.
54
+ */
55
+ registerTool(
56
+ name: string,
57
+ config: { description?: string; inputSchema?: unknown },
58
+ cb: (args: unknown) => Promise<McpListResult>,
59
+ ): ToolHandle;
60
+ sendToolListChanged(): void;
61
+ sendLoggingMessage(params: { level: "info" | "warning"; data: unknown }): Promise<void>;
62
+ };
63
+
64
+ export type WriteToolConfig<T> = {
65
+ /** Action type this tool performs, e.g. "repo.branch.delete". Machine-readable, not prose. */
66
+ readonly mutates: string;
67
+ /** False when the mutation cannot be undone — which is when pre-state capture is mandatory. */
68
+ readonly recoverable: boolean;
69
+ /** Required when `recoverable` is false. Runs BEFORE the mutation; result is audited verbatim. */
70
+ readonly capturePreState?: (args: T) => Promise<Record<string, unknown>>;
71
+ /** The scope target this invocation would touch, checked against the allow-list. */
72
+ readonly scopeTargetOf: (args: T) => { kind: string; value: string };
73
+ };
74
+
75
+ export type WriteToolRegistrar = <T>(
76
+ name: string,
77
+ cfg: WriteToolConfig<T>,
78
+ description: string,
79
+ schema: ZodObjectSchema<T>,
80
+ handler: (args: T) => Promise<McpListResult>,
81
+ ) => void;
82
+
83
+ function refused(why: string): McpListResult {
84
+ return { content: [{ type: "text", text: JSON.stringify({ ok: false, error: why }) }] };
85
+ }
86
+
87
+ /**
88
+ * Ask the human, through the client, and report whether they said yes.
89
+ *
90
+ * The prompt carries the VERBATIM operation and resolved parameters, never a digest — a digest is
91
+ * a rubber stamp with extra steps, and the human is the entire trust boundary here.
92
+ */
93
+ async function consented(
94
+ server: ConsentServer,
95
+ mutates: string,
96
+ params: unknown,
97
+ ): Promise<boolean> {
98
+ let res: { action: string; content?: Record<string, unknown> | undefined };
99
+ try {
100
+ res = await server.server.elicitInput({
101
+ mode: "form",
102
+ message:
103
+ `Nimbus is about to perform ${mutates} with:\n` +
104
+ `${JSON.stringify(params, null, 2)}\n\nApprove?`,
105
+ requestedSchema: {
106
+ type: "object",
107
+ properties: {
108
+ confirm: { type: "boolean", title: "Approve this action", description: mutates },
109
+ },
110
+ required: ["confirm"],
111
+ },
112
+ });
113
+ } catch {
114
+ // Timeout, transport failure, or a client that rejects the request: fail CLOSED.
115
+ return false;
116
+ }
117
+ // `action: "accept"` alone is not consent — the form's own answer must also be true, so a client
118
+ // that auto-accepts without answering the question does not clear the gate.
119
+ return res.action === "accept" && res.content?.["confirm"] === true;
120
+ }
121
+
122
+ function registerOn<T>(
123
+ server: ConsentServer,
124
+ name: string,
125
+ description: string,
126
+ schema: ZodObjectSchema<T>,
127
+ handler: (args: T) => Promise<McpListResult>,
128
+ handles: ToolHandle[],
129
+ ): void {
130
+ const handle = server.registerTool(
131
+ name,
132
+ { description, inputSchema: schema.shape },
133
+ async (args: unknown): Promise<McpListResult> => {
134
+ const parsed = schema.safeParse(args);
135
+ if (!parsed.success) {
136
+ throw new Error(parsed.error.message);
137
+ }
138
+ return handler(parsed.data);
139
+ },
140
+ );
141
+ // Collected so budget exhaustion can disable every write tool at once.
142
+ handles.push(handle);
143
+ }
144
+
145
+ /**
146
+ * Build a connector's write-tool registrar.
147
+ *
148
+ * In `"gateway"` mode the raw handler is registered immediately: `executor.ts` (I2) is the gate
149
+ * there, client capabilities are irrelevant, and the connector adds nothing.
150
+ *
151
+ * In `"standalone"` mode registration is QUEUED and flushed on `oninitialized`, because client
152
+ * capabilities are not knowable at module scope — verified against the real SDK, where
153
+ * `getClientCapabilities()` returns `undefined` before `initialize`. Deciding at registration time
154
+ * would register nothing, for every client, forever.
155
+ */
156
+ export function createWriteToolRegistrar(
157
+ server: ConsentServer,
158
+ cfg: {
159
+ readonly connector: string;
160
+ readonly scopeEnv: string;
161
+ /**
162
+ * Scope kinds this connector accepts. Declared once per connector rather than per tool: the
163
+ * allow-list is parsed at registrar construction, before any tool has registered, so a
164
+ * per-tool declaration would arrive too late to validate the env value.
165
+ */
166
+ readonly scopeKinds: readonly string[];
167
+ },
168
+ ): WriteToolRegistrar {
169
+ const handles: ToolHandle[] = [];
170
+ const pending: Array<() => void> = [];
171
+ let flushed = false;
172
+
173
+ const scope = parseWriteScope(process.env[cfg.scopeEnv], cfg.scopeKinds);
174
+ const auditLog = process.env["NIMBUS_MCP_AUDIT_LOG"];
175
+ let remaining = Number(process.env["NIMBUS_MCP_WRITE_BUDGET"] ?? DEFAULT_BUDGET);
176
+
177
+ async function record(
178
+ tool: string,
179
+ outcome: AuditOutcome,
180
+ detail: Record<string, unknown>,
181
+ ): Promise<void> {
182
+ // Client-visible channel: any MCP client can display or persist this.
183
+ await server.sendLoggingMessage({
184
+ level: outcome === "executed" ? "info" : "warning",
185
+ data: { connector: cfg.connector, tool, outcome },
186
+ });
187
+ // Durable channel: only when the operator configured a path.
188
+ if (auditLog !== undefined && auditLog !== "") {
189
+ await appendAuditEntry(auditLog, {
190
+ ts: new Date().toISOString(),
191
+ connector: cfg.connector,
192
+ tool,
193
+ outcome,
194
+ detail,
195
+ });
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Decide the write surface, once, at the first moment client capabilities are knowable.
201
+ *
202
+ * Default-DENY: if the client cannot prompt a human, the queued tools are dropped and never
203
+ * appear in `tools/list` at all. Refusing at call time instead would still advertise a tool the
204
+ * model must not use.
205
+ */
206
+ function flushWriteTools(): void {
207
+ if (flushed) return;
208
+ flushed = true;
209
+ const caps = server.server.getClientCapabilities();
210
+ if (caps?.elicitation === undefined) {
211
+ pending.length = 0;
212
+ return;
213
+ }
214
+ for (const register of pending) register();
215
+ pending.length = 0;
216
+ // The tool list changed after `initialize`, so the client must be told to re-read it.
217
+ if (handles.length > 0) server.sendToolListChanged();
218
+ }
219
+
220
+ if (getConnectorMode() === "standalone") {
221
+ // An empty scope denies every mutation, which is the correct default but looks identical to a
222
+ // broken connector from the outside. Say so on stderr — safe for a stdio server, whose
223
+ // PROTOCOL channel is stdout; writing this to stdout would corrupt the JSON-RPC stream.
224
+ if (scope.length === 0) {
225
+ process.stderr.write(
226
+ `nimbus-connector ${cfg.connector}: ${cfg.scopeEnv} is unset or empty, so every write tool ` +
227
+ `will refuse. Set it to a comma-separated list of ${cfg.scopeKinds.join("|")}:value ` +
228
+ "terms to enable writes.\n",
229
+ );
230
+ }
231
+ // Chain rather than overwrite: another module may already own this hook, and clobbering it
232
+ // would silently disable whatever it did.
233
+ const prev = server.server.oninitialized;
234
+ server.server.oninitialized = (): void => {
235
+ prev?.();
236
+ flushWriteTools();
237
+ };
238
+ }
239
+
240
+ return <T>(
241
+ name: string,
242
+ toolCfg: WriteToolConfig<T>,
243
+ description: string,
244
+ schema: ZodObjectSchema<T>,
245
+ handler: (args: T) => Promise<McpListResult>,
246
+ ): void => {
247
+ // Checked at REGISTRATION, not on first call: a missing capture on a destructive tool is a
248
+ // programming error, and it must surface at boot rather than on the one call that needed it.
249
+ if (!toolCfg.recoverable && toolCfg.capturePreState === undefined) {
250
+ throw new Error(
251
+ `${name}: capturePreState is required when recoverable is false — an unrecoverable ` +
252
+ "mutation must record enough pre-state to be undone",
253
+ );
254
+ }
255
+
256
+ if (getConnectorMode() === "gateway") {
257
+ registerOn(server, name, description, schema, handler, handles);
258
+ return;
259
+ }
260
+
261
+ const guarded = async (args: T): Promise<McpListResult> => {
262
+ const target = toolCfg.scopeTargetOf(args);
263
+
264
+ // 1. SCOPE FIRST — before prompting. Asking a human to approve something the operator
265
+ // already forbade cannot change the outcome, and training people to click through
266
+ // prompts that do not matter is how you make the prompts that do matter ineffective.
267
+ if (!scopeAllows(scope, target.kind, target.value)) {
268
+ await record(name, "refused", { reason: "out of scope", target });
269
+ return refused(`out of scope: ${target.kind}:${target.value} is not in ${cfg.scopeEnv}`);
270
+ }
271
+
272
+ // 2. BUDGET — same reasoning: a refusal that consent cannot lift comes before consent.
273
+ if (remaining <= 0) {
274
+ await record(name, "refused", { reason: "budget exhausted", target });
275
+ return refused("write budget exhausted for this session");
276
+ }
277
+
278
+ // 3. CONSENT.
279
+ await record(name, "requested", { target, params: args });
280
+ if (!(await consented(server, toolCfg.mutates, args))) {
281
+ await record(name, "declined", { target });
282
+ return refused("not approved: the operation was declined, cancelled, or timed out");
283
+ }
284
+ await record(name, "accepted", { target });
285
+
286
+ // 4. PRE-STATE — after approval, before the mutation, so an unrecoverable action leaves a
287
+ // record of what it destroyed. Capture failure is NOT fatal: refusing here would turn a
288
+ // transient read error into a blocked action the owner already approved.
289
+ let preState: Record<string, unknown> = {};
290
+ if (toolCfg.capturePreState !== undefined) {
291
+ try {
292
+ preState = await toolCfg.capturePreState(args);
293
+ } catch (e) {
294
+ preState = { captureFailed: e instanceof Error ? e.message : String(e) };
295
+ }
296
+ }
297
+
298
+ // 5. MUTATE. Decrement BEFORE the call so a throwing mutation still consumes budget —
299
+ // otherwise a failing destructive tool could be retried without limit.
300
+ remaining -= 1;
301
+ if (remaining <= 0) {
302
+ for (const h of handles) h.disable();
303
+ server.sendToolListChanged();
304
+ }
305
+ try {
306
+ const result = await handler(args);
307
+ await record(name, "executed", { target, preState });
308
+ return result;
309
+ } catch (e) {
310
+ await record(name, "failed", {
311
+ target,
312
+ preState,
313
+ error: e instanceof Error ? e.message : String(e),
314
+ });
315
+ throw e;
316
+ }
317
+ };
318
+
319
+ pending.push(() => {
320
+ registerOn(server, name, description, schema, guarded, handles);
321
+ });
322
+ };
323
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * fetch-bearer-json — re-export of the Bearer-auth JSON fetch helpers now owned
3
+ * by `@nimbus-dev/sdk/connector-kit`.
4
+ *
5
+ * These helpers originated here and were upstreamed to the SDK in 1.11.0. The
6
+ * SDK is now the single owner; this module stays as the connector-facing import
7
+ * path so the ~99 connectors keep their existing relative imports.
8
+ *
9
+ * Named re-exports, deliberately not `export *` — see the note in
10
+ * `mcp-tool-kit.ts`.
11
+ *
12
+ * `resolveUrlWithBase` remains the SSRF chokepoint that refuses a cross-origin
13
+ * absolute URL before a credential-bearing fetch; that behaviour now lives in
14
+ * the SDK and is covered by `fetch-bearer-json.test.ts` through this re-export.
15
+ */
16
+
17
+ export type { BearerJsonFetchResult } from "@nimbus-dev/sdk/connector-kit";
18
+ export { fetchBearerAuthorizedJson, resolveUrlWithBase } from "@nimbus-dev/sdk/connector-kit";
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Rejects any string containing a carriage return or line feed. CR/LF in an
5
+ * email header field (`To`/`Cc`/`Bcc`/`Subject`/attendee list) is the classic
6
+ * header / recipient-injection vector: a folded line can smuggle extra headers
7
+ * or recipients into the outgoing message. Body text legitimately contains
8
+ * newlines and must NOT use this guard.
9
+ */
10
+ const NO_CRLF = /^[^\r\n]*$/;
11
+
12
+ /**
13
+ * A Zod string constrained to a single header line (no CR/LF), with optional
14
+ * length bounds. Use for every user-supplied field that flows into an email
15
+ * header — `to`, `cc`, `bcc`, `subject`, and comma-separated recipient/attendee
16
+ * lists. The single shared definition keeps the guard consistent across every
17
+ * send/draft connector (imap, protonmail, fastmail, gmail, outlook).
18
+ */
19
+ export function headerLine(opts?: { min?: number; max?: number }): z.ZodString {
20
+ let s = z.string().regex(NO_CRLF, "must not contain a line break (CR/LF)");
21
+ if (opts?.min !== undefined) {
22
+ s = s.min(opts.min);
23
+ }
24
+ if (opts?.max !== undefined) {
25
+ s = s.max(opts.max);
26
+ }
27
+ return s;
28
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Pure BODYSTRUCTURE walkers shared by the IMAP-based mail connectors (imap,
3
+ * protonmail-via-Bridge) — extract attachment METADATA (filename, size,
4
+ * mimetype) and locate the first text/plain part for the preview. NEVER touches
5
+ * attachment bytes; operates only on the structure tree imapflow returns.
6
+ */
7
+
8
+ /**
9
+ * imapflow's parsed BODYSTRUCTURE node. Multipart nodes carry `childNodes`; leaf
10
+ * nodes carry `type` (mimetype), `size`, and `disposition`/`dispositionParameters`
11
+ * for attachments. We type the fields we read; the rest is `unknown`.
12
+ */
13
+ export interface BodyStructureNode {
14
+ readonly part?: string;
15
+ readonly type?: string;
16
+ readonly size?: number;
17
+ readonly disposition?: string | null;
18
+ readonly dispositionParameters?: Record<string, unknown> | null;
19
+ readonly parameters?: Record<string, unknown> | null;
20
+ readonly childNodes?: readonly BodyStructureNode[] | null;
21
+ }
22
+
23
+ /** Attachment METADATA only — filename, size, mimetype. NEVER the bytes. */
24
+ export interface AttachmentMeta {
25
+ readonly filename: string | null;
26
+ readonly sizeBytes: number | null;
27
+ readonly mimeType: string | null;
28
+ }
29
+
30
+ function asString(v: unknown): string | null {
31
+ return typeof v === "string" && v !== "" ? v : null;
32
+ }
33
+
34
+ function filenameOf(node: BodyStructureNode): string | null {
35
+ const fromDisp = asString(node.dispositionParameters?.["filename"]);
36
+ if (fromDisp !== null) {
37
+ return fromDisp;
38
+ }
39
+ // Some servers put the name in the content-type `name` parameter instead.
40
+ return asString(node.parameters?.["name"]);
41
+ }
42
+
43
+ function isAttachment(node: BodyStructureNode): boolean {
44
+ const disp = (node.disposition ?? "").toLowerCase();
45
+ if (disp === "attachment" || disp === "inline") {
46
+ return filenameOf(node) !== null;
47
+ }
48
+ // No disposition but a filename present → treat as attachment metadata.
49
+ return filenameOf(node) !== null;
50
+ }
51
+
52
+ /**
53
+ * Walk the BODYSTRUCTURE tree and collect attachment METADATA only — filename,
54
+ * size, mimetype. Never reads or requests the part body.
55
+ */
56
+ export function extractAttachments(root: BodyStructureNode | null | undefined): AttachmentMeta[] {
57
+ const out: AttachmentMeta[] = [];
58
+ const stack: BodyStructureNode[] = root ? [root] : [];
59
+ while (stack.length > 0) {
60
+ const node = stack.pop();
61
+ if (node === undefined) {
62
+ continue;
63
+ }
64
+ const children = node.childNodes ?? null;
65
+ if (children !== null) {
66
+ for (const c of children) {
67
+ stack.push(c);
68
+ }
69
+ continue;
70
+ }
71
+ if (isAttachment(node)) {
72
+ out.push({
73
+ filename: filenameOf(node),
74
+ sizeBytes: typeof node.size === "number" && Number.isFinite(node.size) ? node.size : null,
75
+ mimeType: asString(node.type),
76
+ });
77
+ }
78
+ }
79
+ return out;
80
+ }
81
+
82
+ /**
83
+ * Find the IMAP part key of the first `text/plain` leaf that is NOT an
84
+ * attachment. Used to fetch a single truncated body part for the preview.
85
+ * Returns `"1"` as a conservative default when no structure is available
86
+ * (a single-part text message has an implicit part "1" / "TEXT").
87
+ */
88
+ export function findTextPlainPart(root: BodyStructureNode | null | undefined): string {
89
+ const stack: BodyStructureNode[] = root ? [root] : [];
90
+ let firstAnyText: string | null = null;
91
+ while (stack.length > 0) {
92
+ const node = stack.shift();
93
+ if (node === undefined) {
94
+ continue;
95
+ }
96
+ const children = node.childNodes ?? null;
97
+ if (children !== null) {
98
+ for (const c of children) {
99
+ stack.push(c);
100
+ }
101
+ continue;
102
+ }
103
+ const type = (node.type ?? "").toLowerCase();
104
+ const part = node.part ?? "1";
105
+ if (type === "text/plain" && !isAttachment(node)) {
106
+ return part;
107
+ }
108
+ if (type.startsWith("text/") && firstAnyText === null && !isAttachment(node)) {
109
+ firstAnyText = part;
110
+ }
111
+ }
112
+ return firstAnyText ?? "1";
113
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Pure, transport-agnostic helpers shared by the IMAP-based mail connectors
3
+ * (imap, protonmail-via-Bridge). ProtonMail Bridge exposes a standard IMAP/SMTP
4
+ * interface, so both connectors share the same preview-capping, limit-clamping,
5
+ * and address-formatting logic. No external deps; never touches message bodies
6
+ * or attachment bytes.
7
+ */
8
+
9
+ /** Max characters of the plain-text body preview that is ever indexed/returned. */
10
+ export const PREVIEW_MAX_CHARS = 2000;
11
+ /** Max bytes of the text/plain body part fetched for the preview (~2 KB). */
12
+ export const PREVIEW_FETCH_BYTES = 2048;
13
+
14
+ /** One parsed address (name optional). */
15
+ export interface MailAddress {
16
+ readonly name?: string;
17
+ readonly address?: string;
18
+ }
19
+
20
+ const DEFAULT_LIST_LIMIT = 50;
21
+ const MAX_LIST_LIMIT = 200;
22
+
23
+ export function clampLimit(limit: number | undefined, fallback = DEFAULT_LIST_LIMIT): number {
24
+ if (limit === undefined || !Number.isFinite(limit)) {
25
+ return fallback;
26
+ }
27
+ const n = Math.trunc(limit);
28
+ if (n < 1) {
29
+ return 1;
30
+ }
31
+ return Math.min(n, MAX_LIST_LIMIT);
32
+ }
33
+
34
+ /**
35
+ * Cap a body preview to {@link PREVIEW_MAX_CHARS}. Normalizes CRLF and collapses
36
+ * whitespace/blank-line runs so the preview stays compact; never lengthens the
37
+ * input.
38
+ */
39
+ export function capPreview(text: string): string {
40
+ const normalized = text
41
+ .replaceAll("\r\n", "\n")
42
+ .replace(/[ \t]+/g, " ")
43
+ .replace(/\n{2,}/g, "\n")
44
+ .trim();
45
+ return normalized.length > PREVIEW_MAX_CHARS
46
+ ? normalized.slice(0, PREVIEW_MAX_CHARS)
47
+ : normalized;
48
+ }
49
+
50
+ /** Format one address as `Name <addr>` / `<addr>` for envelope display. */
51
+ export function formatAddress(a: MailAddress): string {
52
+ const addr = a.address ?? "";
53
+ if (a.name !== undefined && a.name !== "") {
54
+ return addr === "" ? a.name : `${a.name} <${addr}>`;
55
+ }
56
+ return addr;
57
+ }