@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,55 @@
1
+ import { nimbusSpawn } from "./nimbus-spawn.ts";
2
+
3
+ export type RunCliJsonResult = { ok: true; data: unknown } | { ok: false; message: string };
4
+
5
+ export async function runCliOk(
6
+ command: readonly string[],
7
+ env: Record<string, string | undefined>,
8
+ ): Promise<{ ok: true } | { ok: false; message: string }> {
9
+ if (command.length === 0) {
10
+ return { ok: false, message: "empty command" };
11
+ }
12
+ const { code, stderr: err } = await nimbusSpawn(command, env);
13
+ if (code !== 0) {
14
+ return {
15
+ ok: false,
16
+ message: `${command[0] ?? "cli"} exited ${String(code)}: ${err.slice(0, 500)}`,
17
+ };
18
+ }
19
+ return { ok: true };
20
+ }
21
+
22
+ export async function runCliOkThrowing(
23
+ command: readonly string[],
24
+ env: Record<string, string | undefined>,
25
+ ): Promise<void> {
26
+ const r = await runCliOk(command, env);
27
+ if (!r.ok) {
28
+ throw new Error(r.message);
29
+ }
30
+ }
31
+
32
+ export async function runCliJson(
33
+ command: readonly string[],
34
+ env: Record<string, string | undefined>,
35
+ ): Promise<RunCliJsonResult> {
36
+ if (command.length === 0) {
37
+ return { ok: false, message: "empty command" };
38
+ }
39
+ const { code, stdout: out, stderr: err } = await nimbusSpawn(command, env);
40
+ if (code !== 0) {
41
+ return {
42
+ ok: false,
43
+ message: `${command[0] ?? "cli"} exited ${String(code)}: ${err.slice(0, 500)}`,
44
+ };
45
+ }
46
+ const trimmed = out.trim();
47
+ if (trimmed === "") {
48
+ return { ok: true, data: null };
49
+ }
50
+ try {
51
+ return { ok: true, data: JSON.parse(trimmed) as unknown };
52
+ } catch {
53
+ return { ok: false, message: `invalid JSON from CLI: ${out.slice(0, 200)}` };
54
+ }
55
+ }
@@ -0,0 +1,59 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import {
4
+ createRegisterSimpleTool,
5
+ createZodToolRegistrar,
6
+ type McpListResult,
7
+ type ZodObjectSchema,
8
+ } from "./mcp-tool-kit.ts";
9
+
10
+ export type ZodToolRegistrar = <T>(
11
+ name: string,
12
+ description: string,
13
+ schema: ZodObjectSchema<T>,
14
+ handler: (args: T) => Promise<McpListResult>,
15
+ ) => void;
16
+
17
+ export interface BuildReadOnlyMcpConnectorOptions {
18
+ readonly createServer?: (info: { readonly name: string; readonly version: string }) => unknown;
19
+ }
20
+
21
+ /**
22
+ * `createTransport` mirrors the existing `createServer` seam. Without it the
23
+ * stdio transport was constructed inline, so `runReadOnlyMcpConnector` — the
24
+ * function every connector's entry point actually calls — could not be executed
25
+ * from a test at all: it would have opened a real stdio transport on the test
26
+ * process. That left the wiring between server and transport asserted nowhere.
27
+ *
28
+ * Production callers pass nothing and get the real `StdioServerTransport`.
29
+ */
30
+ export interface RunReadOnlyMcpConnectorOptions extends BuildReadOnlyMcpConnectorOptions {
31
+ readonly createTransport?: () => unknown;
32
+ }
33
+
34
+ export function buildReadOnlyMcpConnector(
35
+ serverName: string,
36
+ // The SERVER is passed to `register` as a second argument. Despite this helper's name, several
37
+ // of its connectors expose write tools, and those need the real server to build a consent
38
+ // registrar — the read-only registrar alone cannot reach capabilities, registerTool or logging.
39
+ // Optional in the callback's own signature, so the many genuinely read-only connectors that
40
+ // ignore it are unaffected.
41
+ register: (reg: ZodToolRegistrar, server: unknown) => void,
42
+ options?: BuildReadOnlyMcpConnectorOptions,
43
+ ): unknown {
44
+ const make = options?.createServer ?? ((info) => new McpServer(info));
45
+ const mcp = make({ name: serverName, version: "0.1.0" });
46
+ const reg = createZodToolRegistrar(createRegisterSimpleTool(mcp));
47
+ register(reg, mcp);
48
+ return mcp;
49
+ }
50
+
51
+ export async function runReadOnlyMcpConnector(
52
+ serverName: string,
53
+ register: (reg: ZodToolRegistrar, server: unknown) => void,
54
+ options?: RunReadOnlyMcpConnectorOptions,
55
+ ): Promise<void> {
56
+ const mcp = buildReadOnlyMcpConnector(serverName, register, options) as McpServer;
57
+ const makeTransport = options?.createTransport ?? ((): unknown => new StdioServerTransport());
58
+ await mcp.connect(makeTransport() as StdioServerTransport);
59
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Guard a tool-supplied value before it is passed as an argument to a spawned CLI
3
+ * (`gcloud` / `aws` / `bq` / …).
4
+ *
5
+ * Spawns use an argv array (no shell), so there is no classic shell injection — but
6
+ * a value that begins with `-` would be parsed by the CLI as a FLAG rather than a
7
+ * positional / flag-value. This "argv flag smuggling" lets tool input inject
8
+ * arbitrary CLI flags (e.g. a `sinkName` of `--project=attacker` or `-h`). Reject
9
+ * any value that is empty, over-long, starts with `-`, or contains control
10
+ * characters.
11
+ *
12
+ * Resource-id charsets vary per service (sink ids are `[A-Za-z0-9_.-]`, CloudWatch
13
+ * log-group names contain `/`, BigQuery refs contain `.`), so this is the UNIVERSAL
14
+ * guard against flag smuggling; callers may layer a stricter per-resource regex on
15
+ * top. Returns the value unchanged so it can be used inline:
16
+ * `cli(["describe", assertSafeCliArg(p.id, "id")])`.
17
+ *
18
+ * @throws {Error} when the value is unsafe to pass as a CLI argument.
19
+ */
20
+ export function assertSafeCliArg(value: string, label = "argument"): string {
21
+ if (typeof value !== "string" || value.length === 0) {
22
+ throw new Error(`Invalid ${label}: must be a non-empty string`);
23
+ }
24
+ if (value.length > 1024) {
25
+ throw new Error(`Invalid ${label}: exceeds 1024 characters`);
26
+ }
27
+ if (value.startsWith("-")) {
28
+ throw new Error(
29
+ `Invalid ${label}: must not start with "-" (argv flag smuggling is not allowed)`,
30
+ );
31
+ }
32
+ for (let i = 0; i < value.length; i += 1) {
33
+ if ((value.codePointAt(i) ?? 0) < 0x20) {
34
+ throw new Error(`Invalid ${label}: must not contain control characters`);
35
+ }
36
+ }
37
+ return value;
38
+ }
39
+
40
+ /**
41
+ * Non-throwing predicate form of {@link assertSafeCliArg}, for use in a Zod
42
+ * `.refine(...)` so a `-`-prefixed / control-char value is rejected at the tool's
43
+ * schema boundary before the handler ever shells out.
44
+ */
45
+ export function isSafeCliArg(value: unknown): value is string {
46
+ if (typeof value !== "string") {
47
+ return false;
48
+ }
49
+ try {
50
+ assertSafeCliArg(value);
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
@@ -0,0 +1,140 @@
1
+ export interface SearchMatchOptions {
2
+ readonly query: string;
3
+ readonly limit?: number | undefined;
4
+ }
5
+
6
+ export interface FilterByQueryOptions<T> {
7
+ readonly query: string;
8
+ readonly limit?: number | undefined;
9
+ readonly fields: (item: T) => readonly (string | null | undefined)[] | null;
10
+ }
11
+
12
+ export function asRecord(value: unknown): Record<string, unknown> | undefined {
13
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
14
+ return undefined;
15
+ }
16
+ return value as Record<string, unknown>;
17
+ }
18
+
19
+ export function asObjectish(value: unknown): Record<string, unknown> | undefined {
20
+ if (value === null || typeof value !== "object") {
21
+ return undefined;
22
+ }
23
+ return value as Record<string, unknown>;
24
+ }
25
+
26
+ export function stringField(row: Record<string, unknown>, key: string): string {
27
+ const v = row[key];
28
+ return typeof v === "string" ? v : "";
29
+ }
30
+
31
+ export function tagText(row: Record<string, unknown>): string {
32
+ const tags = row["tags"];
33
+ if (!Array.isArray(tags)) {
34
+ return "";
35
+ }
36
+ const names: string[] = [];
37
+ for (const t of tags) {
38
+ if (typeof t === "string") {
39
+ names.push(t);
40
+ }
41
+ }
42
+ return names.join(" ");
43
+ }
44
+
45
+ /**
46
+ * Extract tag names from an array of `{name: string}` tag objects (e.g. Airflow, DependencyTrack).
47
+ * Returns "" when `tags` is absent, not an array, or contains no object entries with a string `name`.
48
+ */
49
+ export function tagNamesFromObjects(row: Record<string, unknown>): string {
50
+ const tags = row["tags"];
51
+ if (!Array.isArray(tags)) {
52
+ return "";
53
+ }
54
+ const names: string[] = [];
55
+ for (const t of tags) {
56
+ const tag = asObjectish(t);
57
+ if (tag === undefined) {
58
+ continue;
59
+ }
60
+ const name = tag["name"];
61
+ if (typeof name === "string" && name !== "") {
62
+ names.push(name);
63
+ }
64
+ }
65
+ return names.join(" ");
66
+ }
67
+
68
+ export function filterByQuery<T>(items: readonly T[], options: FilterByQueryOptions<T>): T[] {
69
+ const needle = options.query.toLowerCase();
70
+ const cap = options.limit ?? 50;
71
+ const out: T[] = [];
72
+ for (const item of items) {
73
+ const parts = options.fields(item);
74
+ if (parts === null) {
75
+ continue;
76
+ }
77
+ const haystack = parts.join(" ").toLowerCase();
78
+ if (!haystack.includes(needle)) {
79
+ continue;
80
+ }
81
+ out.push(item);
82
+ if (out.length >= cap) {
83
+ break;
84
+ }
85
+ }
86
+ return out;
87
+ }
88
+
89
+ export type FieldExtractor = (item: unknown) => readonly (string | null | undefined)[] | null;
90
+
91
+ /**
92
+ * Build a {@link FieldExtractor} that reads a fixed list of string keys off each
93
+ * objectish row, optionally appending the standard `tags` text. Collapses the
94
+ * boilerplate `fieldsOf` body shared by the simpler connectors.
95
+ */
96
+ export function fieldsFromKeys(
97
+ keys: readonly string[],
98
+ opts?: { readonly tags?: boolean },
99
+ ): FieldExtractor {
100
+ return (item: unknown) => {
101
+ const row = asObjectish(item);
102
+ if (row === undefined) {
103
+ return null;
104
+ }
105
+ const parts = keys.map((key) => stringField(row, key));
106
+ if (opts?.tags === true) {
107
+ parts.push(tagText(row));
108
+ }
109
+ return parts;
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Read a nested string field by key path off an objectish row, returning `""`
115
+ * when any path segment is missing or the leaf is not a string. Shared by the
116
+ * Kubernetes-style connectors (argocd, flux) whose resources nest fields under
117
+ * `metadata` / `spec` / `status`.
118
+ */
119
+ export function nestedString(root: Record<string, unknown>, path: readonly string[]): string {
120
+ let cur: Record<string, unknown> | undefined = root;
121
+ for (let i = 0; i < path.length - 1; i += 1) {
122
+ cur = asRecord(cur?.[path[i] ?? ""]);
123
+ if (cur === undefined) {
124
+ return "";
125
+ }
126
+ }
127
+ const leaf = cur?.[path.at(-1) ?? ""];
128
+ return typeof leaf === "string" ? leaf : "";
129
+ }
130
+
131
+ /**
132
+ * Build a `filter<Thing>(items, options)` search function from a field
133
+ * extractor. Connectors with bespoke extraction pass their own `fieldsOf`;
134
+ * simple ones pair this with {@link fieldsFromKeys}.
135
+ */
136
+ export function makeQueryFilter(
137
+ fields: FieldExtractor,
138
+ ): (items: readonly unknown[], options: SearchMatchOptions) => unknown[] {
139
+ return (items, options) => filterByQuery(items, { ...options, fields });
140
+ }
@@ -0,0 +1,7 @@
1
+ export function stripTrailingSlashes(input: string): string {
2
+ let s = input.trim();
3
+ while (s.endsWith("/")) {
4
+ s = s.slice(0, -1);
5
+ }
6
+ return s;
7
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * A single allow-list term, e.g. `repo:acme/api` or `dataset:proj.analytics`.
3
+ *
4
+ * `owner/repo` reads naturally for GitHub and generalises to nothing else — BigQuery, Notion, Slack
5
+ * and Jira have no such shape. Left per-connector this would drift into ~34 private syntaxes, so
6
+ * every scope term is typed.
7
+ */
8
+ export type ScopeTerm = { readonly kind: string; readonly value: string };
9
+
10
+ /**
11
+ * Parse `NIMBUS_MCP_<SERVICE>_WRITE_SCOPE` into terms, rejecting any kind the connector did not
12
+ * declare.
13
+ *
14
+ * An unknown kind THROWS at startup rather than parsing into a term that can never match. A rule
15
+ * that silently never matches is indistinguishable from no rule at all, which fails open — the one
16
+ * outcome an allow-list must never have.
17
+ */
18
+ export function parseWriteScope(
19
+ raw: string | undefined,
20
+ allowedKinds: readonly string[],
21
+ ): readonly ScopeTerm[] {
22
+ if (raw === undefined || raw.trim() === "") return [];
23
+ const allowed = new Set(allowedKinds);
24
+ const out: ScopeTerm[] = [];
25
+ for (const piece of raw.split(",")) {
26
+ const term = piece.trim();
27
+ if (term === "") continue;
28
+ const sep = term.indexOf(":");
29
+ if (sep === -1) {
30
+ throw new Error(`write scope term ${JSON.stringify(term)}: expected "kind:value"`);
31
+ }
32
+ const kind = term.slice(0, sep);
33
+ // Not `split(":")` — a value may itself contain colons and must survive intact.
34
+ const value = term.slice(sep + 1).trim();
35
+ if (!allowed.has(kind)) {
36
+ throw new Error(
37
+ `write scope term ${JSON.stringify(term)}: unknown scope kind ${JSON.stringify(kind)}; ` +
38
+ `this connector accepts: ${allowedKinds.join(", ")}`,
39
+ );
40
+ }
41
+ if (value === "") {
42
+ throw new Error(`write scope term ${JSON.stringify(term)}: empty value`);
43
+ }
44
+ out.push({ kind, value });
45
+ }
46
+ return out;
47
+ }
48
+
49
+ /**
50
+ * Whether the scope authorises one target. Exact match on both fields.
51
+ *
52
+ * An EMPTY scope denies everything: "unset" must mean "no mutations authorised", never
53
+ * "unrestricted". Prefix matching is deliberately absent — `acme/api` must not authorise
54
+ * `acme/api-secrets`.
55
+ */
56
+ export function scopeAllows(scope: readonly ScopeTerm[], kind: string, value: string): boolean {
57
+ return scope.some((t) => t.kind === kind && t.value === value);
58
+ }
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bun
2
+ import { runStandalone } from "./launcher.ts";
3
+
4
+ /**
5
+ * The `nimbus-connector` process entry point, and nothing else.
6
+ *
7
+ * Split from `launcher.ts` so that file stays pure, fully testable logic. An `import.meta.main`
8
+ * block cannot be exercised in-process by any test, so leaving it inside the launcher would have
9
+ * meant either an uncoverable branch or exempting the eligibility logic from coverage with it —
10
+ * and the eligibility logic is the part worth covering.
11
+ */
12
+ const code = await runStandalone(process.argv.slice(2));
13
+ // Exit ONLY on failure. Most connectors connect their stdio transport at module scope, so the
14
+ // import inside runStandalone resolves once the server is live and it returns 0 immediately —
15
+ // `process.exit(0)` here would tear down the server it just started. On success we fall through
16
+ // and the connected transport keeps the process alive.
17
+ if (code !== 0) {
18
+ process.exit(code);
19
+ }
@@ -0,0 +1,186 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ /** Connector ids are directory names: lowercase letters, digits and hyphens only. */
6
+ const ID_RE = /^[a-z0-9-]+$/;
7
+
8
+ /**
9
+ * Does this source register a write tool?
10
+ *
11
+ * A registration CALL, or the registrar handed to a shared kit — not a bare substring, which the
12
+ * registrar's own `const registerWriteTool = ...` would satisfy even with nothing registered.
13
+ * Kept in step with the twin in check-connector-consent.ts.
14
+ *
15
+ * Deliberately NOT a regular expression. The previous pattern was
16
+ * `^\s*register[A-Za-z]*WriteTool\(` under `/m`, and it drew two rounds of ReDoS reports. The
17
+ * first was real: `\s` matches a newline, so under `/m` a run of n newlines gave n start
18
+ * positions each able to consume the whole run — quadratic, measured at 21s for 128k newlines.
19
+ * Narrowing the class to horizontal whitespace made it linear, and bounding the star to
20
+ * `{0,40}` kept it linear, but `typescript:S8786` reported both: a star immediately followed by
21
+ * a literal built from the same character class is the shape the rule looks for, bounded or not,
22
+ * and the shape is worth avoiding even where this engine happens not to backtrack.
23
+ *
24
+ * Scanning line by line removes the construct rather than arguing with the analyser. Each line is
25
+ * bounded work, no star sits next to an overlapping literal, and the accepted language is
26
+ * unchanged — including the trailing-comma form, which still requires the comma to end the line.
27
+ */
28
+ function registersWriteTool(src: string): boolean {
29
+ for (const line of src.split("\n")) {
30
+ const t = line.trimStart();
31
+ if (t === "registerWriteTool,") return true;
32
+ if (!t.startsWith("register")) continue;
33
+ const at = t.indexOf("WriteTool(");
34
+ if (at < 0) continue;
35
+ // Everything between `register` and `WriteTool(` must be letters, so `registerFoo.WriteTool(`
36
+ // does not count. Anchored at BOTH ends with nothing following, so it carries none of the
37
+ // ambiguity the old pattern did.
38
+ if (/^[A-Za-z]*$/.test(t.slice("register".length, at))) return true;
39
+ }
40
+ return false;
41
+ }
42
+
43
+ /**
44
+ * The directory holding the 94 connector packages.
45
+ *
46
+ * `standalone/src/` -> repo root -> `connectors/`. This used to be just `../..`, back when the
47
+ * connectors sat directly at the repo root alongside `shared/` and `standalone/` — 97 top-level
48
+ * entries, which is why they were nested.
49
+ */
50
+ function connectorsDir(): string {
51
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "connectors");
52
+ }
53
+
54
+ /**
55
+ * Resolve a connector id to its server entrypoint.
56
+ *
57
+ * The id is validated against a strict allow-list BEFORE being joined into a path. A separator or
58
+ * `..` would otherwise let the id escape the connectors directory and import an arbitrary module.
59
+ */
60
+ export function resolveConnectorEntry(id: string): string {
61
+ if (!ID_RE.test(id)) {
62
+ throw new Error(
63
+ `invalid connector id ${JSON.stringify(id)}: expected only lowercase letters, digits and hyphens`,
64
+ );
65
+ }
66
+ return join(connectorsDir(), id, "src", "server.ts");
67
+ }
68
+
69
+ export type Eligibility =
70
+ | { readonly eligible: true; readonly reason: "no-writes" | "hardened" }
71
+ | { readonly eligible: false; readonly reason: string };
72
+
73
+ /**
74
+ * Whether a connector may run STANDALONE.
75
+ *
76
+ * Derived, never curated. A connector qualifies when it declares no mutating capability at all, or
77
+ * when its write tools have been routed through the consent kit. Anything else would expose
78
+ * ungated destructive tools the moment it started outside the gateway — the exact outcome this
79
+ * whole subsystem exists to prevent — so the launcher refuses to start it.
80
+ *
81
+ * `hitlRequired` is the AUTHORITATIVE signal, and deliberately the only one. It is authored per
82
+ * connector and transport independent: ten connectors mutate through a CLI, the filesystem or a
83
+ * mail protocol, where no scan of the source could see them.
84
+ *
85
+ * Scanning for a mutating HTTP verb was tried here and REMOVED. It cannot decide this question in
86
+ * either direction: seven read-only connectors POST for GraphQL queries, filter endpoints, OAuth
87
+ * token exchange and login (dagster, google-photos, prefect, ramp, snyk, superset, wiz), so the
88
+ * verb wrongly refused all seven; and it is blind to the ten that never issue an HTTP request at
89
+ * all. A connector that mutates without declaring it is a CONNECTOR BUG, caught at authoring time
90
+ * by `registerWriteTool` and its audit — not by a runtime heuristic that provably cannot tell.
91
+ *
92
+ * An unreadable manifest is treated as declaring a write. The cost is refusing one connector that
93
+ * might have been fine; the alternative is starting one that is not.
94
+ */
95
+ export function standaloneEligibility(
96
+ id: string,
97
+ // Injectable root so a test can point at a fixture. Tests previously used a real connector as
98
+ // their "unmigrated" example and broke the moment it was migrated — a test whose meaning depends
99
+ // on unrelated work is a test that will lie eventually.
100
+ root: string = connectorsDir(),
101
+ ): Eligibility {
102
+ const dir = join(root, id);
103
+ let declaresWrite: boolean;
104
+ try {
105
+ const parsed: unknown = JSON.parse(readFileSync(join(dir, "nimbus.extension.json"), "utf8"));
106
+ const hitl =
107
+ typeof parsed === "object" && parsed !== null
108
+ ? (parsed as Record<string, unknown>)["hitlRequired"]
109
+ : undefined;
110
+ declaresWrite = Array.isArray(hitl) && hitl.some((h) => h === "write" || h === "delete");
111
+ } catch {
112
+ declaresWrite = true;
113
+ }
114
+ if (!declaresWrite) return { eligible: true, reason: "no-writes" };
115
+
116
+ // BOTH entrypoint files. 16 connectors register their tools in `src/tools.ts` rather than
117
+ // `src/server.ts` — apple, fastmail, imap, protonmail and the CLI-backed cloud ones among them —
118
+ // so reading only server.ts would refuse a connector that IS hardened, in tools.ts.
119
+ let src = "";
120
+ for (const f of ["server.ts", "tools.ts"]) {
121
+ try {
122
+ src += readFileSync(join(dir, "src", f), "utf8");
123
+ } catch {
124
+ /* a connector need not have both; an unreadable pair falls through to the refusal below */
125
+ }
126
+ }
127
+
128
+ if (registersWriteTool(src)) {
129
+ return { eligible: true, reason: "hardened" };
130
+ }
131
+
132
+ return {
133
+ eligible: false,
134
+ reason:
135
+ `${id} declares write or delete tools in its manifest that have not been routed through ` +
136
+ "the consent kit, " +
137
+ "so running it standalone would expose ungated mutations. Run it through the Nimbus " +
138
+ "gateway, which gates them, until this connector is migrated.",
139
+ };
140
+ }
141
+
142
+ /** How a connector entrypoint is loaded. Injectable so tests need not import a real stdio server. */
143
+ export type ConnectorImporter = (entry: string) => Promise<{
144
+ startConnector?: (() => Promise<void>) | undefined;
145
+ }>;
146
+
147
+ /**
148
+ * Start one connector standalone.
149
+ *
150
+ * Deliberately does NOT call `setConnectorMode("standalone")`. Standalone is the DEFAULT, so
151
+ * asserting it here would add a second production caller — which the `audit:connector-consent`
152
+ * gate forbids — while changing nothing. Do not "fix" this omission.
153
+ */
154
+ export async function runStandalone(
155
+ argv: readonly string[],
156
+ importConnector: ConnectorImporter = (entry) => import(entry),
157
+ ): Promise<number> {
158
+ const id = argv[0];
159
+ if (id === undefined) {
160
+ process.stderr.write("usage: nimbus-connector <connector-id>\n");
161
+ return 2;
162
+ }
163
+ let entry: string;
164
+ try {
165
+ entry = resolveConnectorEntry(id);
166
+ } catch (e) {
167
+ process.stderr.write(`${e instanceof Error ? e.message : String(e)}\n`);
168
+ return 2;
169
+ }
170
+ if (!existsSync(entry)) {
171
+ process.stderr.write(`unknown connector ${JSON.stringify(id)}\n`);
172
+ return 2;
173
+ }
174
+
175
+ const verdict = standaloneEligibility(id);
176
+ if (!verdict.eligible) {
177
+ process.stderr.write(`${verdict.reason}\n`);
178
+ return 3;
179
+ }
180
+
181
+ const mod = await importConnector(entry);
182
+ // Mirrors run-bundled-connector.ts: most connectors connect their transport at module scope, ten
183
+ // guard on import.meta.main and export startConnector() instead.
184
+ await mod.startConnector?.();
185
+ return 0;
186
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "types": ["bun"]
5
+ },
6
+ "include": ["src/**/*", "../shared/**/*.ts"],
7
+ "exclude": ["node_modules", "dist"]
8
+ }