@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,360 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+
5
+ import {
6
+ createRegisterSimpleTool,
7
+ createZodToolRegistrar,
8
+ mcpJsonResult as jsonResult,
9
+ type McpListResult,
10
+ putOptionalBoolean,
11
+ putOptionalNonEmptyString,
12
+ requireProcessEnv,
13
+ } from "../../../shared/mcp-tool-kit.ts";
14
+
15
+ type SlackApiRecord = Record<string, unknown>;
16
+
17
+ async function slackApi(
18
+ token: string,
19
+ method: string,
20
+ body: Record<string, unknown>,
21
+ ): Promise<{ ok: boolean; json: SlackApiRecord; text: string }> {
22
+ const res = await fetch(`https://slack.com/api/${method}`, {
23
+ method: "POST",
24
+ headers: {
25
+ Authorization: `Bearer ${token}`,
26
+ "Content-Type": "application/json; charset=utf-8",
27
+ },
28
+ body: JSON.stringify(body),
29
+ });
30
+ const text = await res.text();
31
+ let parsed: unknown;
32
+ try {
33
+ parsed = JSON.parse(text) as unknown;
34
+ } catch {
35
+ return { ok: false, json: {}, text };
36
+ }
37
+ const json =
38
+ parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
39
+ ? (parsed as SlackApiRecord)
40
+ : {};
41
+ const okField = json["ok"];
42
+ return { ok: okField === true && res.ok, json, text };
43
+ }
44
+
45
+ async function slackInvokeJsonWithToken(
46
+ token: string,
47
+ method: string,
48
+ body: Record<string, unknown>,
49
+ errorLabel: string,
50
+ ): Promise<McpListResult> {
51
+ const res = await slackApi(token, method, body);
52
+ if (!res.ok) {
53
+ throw new Error(`${errorLabel}: ${res.text.slice(0, 400)}`);
54
+ }
55
+ return jsonResult(res.json);
56
+ }
57
+
58
+ async function slackInvokeJson(
59
+ method: string,
60
+ body: Record<string, unknown>,
61
+ errorLabel: string,
62
+ ): Promise<McpListResult> {
63
+ return slackInvokeJsonWithToken(
64
+ requireProcessEnv("SLACK_USER_ACCESS_TOKEN"),
65
+ method,
66
+ body,
67
+ errorLabel,
68
+ );
69
+ }
70
+
71
+ const slackConversationsHistorySchema = z.object({
72
+ channel: z.string().min(1),
73
+ limit: z.number().int().min(1).max(200).optional(),
74
+ cursor: z.string().optional(),
75
+ oldest: z.string().optional(),
76
+ inclusive: z.boolean().optional(),
77
+ });
78
+
79
+ type SlackConversationsHistoryParsed = z.infer<typeof slackConversationsHistorySchema>;
80
+
81
+ function buildConversationsHistoryBody(
82
+ parsed: SlackConversationsHistoryParsed,
83
+ ): Record<string, unknown> {
84
+ const body: Record<string, unknown> = {
85
+ channel: parsed.channel,
86
+ limit: parsed.limit ?? 50,
87
+ };
88
+ putOptionalNonEmptyString(body, "cursor", parsed.cursor);
89
+ putOptionalNonEmptyString(body, "oldest", parsed.oldest);
90
+ putOptionalBoolean(body, "inclusive", parsed.inclusive);
91
+ return body;
92
+ }
93
+
94
+ const server = new McpServer({ name: "nimbus-slack", version: "0.1.0" });
95
+
96
+ const registerSimpleTool = createRegisterSimpleTool(server);
97
+
98
+ import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
99
+
100
+ const reg = createZodToolRegistrar(registerSimpleTool);
101
+
102
+ /**
103
+ * Every MUTATING slack tool goes through here. Outside the gateway this adds the
104
+ * consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
105
+ * the gateway it is a pass-through, because executor.ts (I2) is the gate there.
106
+ */
107
+ const registerWriteTool = createWriteToolRegistrar(server, {
108
+ connector: "slack",
109
+ scopeEnv: "NIMBUS_MCP_SLACK_WRITE_SCOPE",
110
+ scopeKinds: ["channel"],
111
+ });
112
+
113
+ const slackChannelListSchema = z.object({
114
+ types: z.string().optional(),
115
+ limit: z.number().int().min(1).max(200).optional(),
116
+ cursor: z.string().optional(),
117
+ });
118
+
119
+ reg(
120
+ "slack_channel_list",
121
+ "List channels the user is a member of (public, private, mpim, im).",
122
+ slackChannelListSchema,
123
+ async (parsed) => {
124
+ const body: Record<string, unknown> = {
125
+ types: parsed.types ?? "public_channel,private_channel,mpim,im",
126
+ limit: parsed.limit ?? 200,
127
+ exclude_archived: true,
128
+ };
129
+ putOptionalNonEmptyString(body, "cursor", parsed.cursor);
130
+ return slackInvokeJson("conversations.list", body, "Slack conversations.list");
131
+ },
132
+ );
133
+
134
+ reg(
135
+ "slack_channel_history",
136
+ "Fetch message history for a channel, DM, or mpim.",
137
+ slackConversationsHistorySchema,
138
+ async (parsed) =>
139
+ slackInvokeJson(
140
+ "conversations.history",
141
+ buildConversationsHistoryBody(parsed),
142
+ "Slack conversations.history",
143
+ ),
144
+ );
145
+
146
+ const slackThreadRepliesSchema = z.object({
147
+ channel: z.string().min(1),
148
+ ts: z.string().min(1),
149
+ limit: z.number().int().min(1).max(200).optional(),
150
+ cursor: z.string().optional(),
151
+ });
152
+
153
+ reg(
154
+ "slack_thread_replies",
155
+ "Fetch replies in a thread (channel + thread parent ts).",
156
+ slackThreadRepliesSchema,
157
+ async (parsed) => {
158
+ const body: Record<string, unknown> = {
159
+ channel: parsed.channel,
160
+ ts: parsed.ts,
161
+ limit: parsed.limit ?? 50,
162
+ };
163
+ putOptionalNonEmptyString(body, "cursor", parsed.cursor);
164
+ return slackInvokeJson("conversations.replies", body, "Slack conversations.replies");
165
+ },
166
+ );
167
+
168
+ const slackDmListSchema = z.object({
169
+ limit: z.number().int().min(1).max(200).optional(),
170
+ cursor: z.string().optional(),
171
+ });
172
+
173
+ reg(
174
+ "slack_dm_list",
175
+ "List direct message conversations (im + mpim).",
176
+ slackDmListSchema,
177
+ async (parsed) => {
178
+ const body: Record<string, unknown> = {
179
+ types: "im,mpim",
180
+ limit: parsed.limit ?? 200,
181
+ exclude_archived: true,
182
+ };
183
+ putOptionalNonEmptyString(body, "cursor", parsed.cursor);
184
+ return slackInvokeJson("conversations.list", body, "Slack dm list");
185
+ },
186
+ );
187
+
188
+ reg(
189
+ "slack_dm_history",
190
+ "Fetch DM / mpim history (same as channel history; convenience alias).",
191
+ slackConversationsHistorySchema,
192
+ async (parsed) =>
193
+ slackInvokeJson(
194
+ "conversations.history",
195
+ buildConversationsHistoryBody(parsed),
196
+ "Slack dm history",
197
+ ),
198
+ );
199
+
200
+ const slackUserListSchema = z.object({
201
+ limit: z.number().int().min(1).max(200).optional(),
202
+ cursor: z.string().optional(),
203
+ });
204
+
205
+ reg(
206
+ "slack_user_list",
207
+ "List users in the workspace (paginated).",
208
+ slackUserListSchema,
209
+ async (parsed) => {
210
+ const body: Record<string, unknown> = { limit: parsed.limit ?? 100 };
211
+ putOptionalNonEmptyString(body, "cursor", parsed.cursor);
212
+ return slackInvokeJson("users.list", body, "Slack users.list");
213
+ },
214
+ );
215
+
216
+ const slackUserGetSchema = z.object({ user: z.string().min(1) });
217
+
218
+ reg("slack_user_get", "Get a single user profile by ID.", slackUserGetSchema, async (parsed) =>
219
+ slackInvokeJson("users.info", { user: parsed.user }, "Slack users.info"),
220
+ );
221
+
222
+ const slackSearchSchema = z.object({
223
+ query: z.string().min(1),
224
+ count: z.number().int().min(1).max(100).optional(),
225
+ page: z.number().int().min(1).optional(),
226
+ sort: z.enum(["timestamp", "score"]).optional(),
227
+ sort_dir: z.enum(["asc", "desc"]).optional(),
228
+ });
229
+
230
+ reg("slack_search", "Search messages (workspace search).", slackSearchSchema, async (parsed) => {
231
+ const body: Record<string, unknown> = {
232
+ query: parsed.query,
233
+ count: parsed.count ?? 20,
234
+ page: parsed.page ?? 1,
235
+ };
236
+ if (parsed.sort !== undefined) {
237
+ body["sort"] = parsed.sort;
238
+ }
239
+ if (parsed.sort_dir !== undefined) {
240
+ body["sort_dir"] = parsed.sort_dir;
241
+ }
242
+ return slackInvokeJson("search.messages", body, "Slack search.messages");
243
+ });
244
+
245
+ const slackMessagePostSchema = z.object({
246
+ channel: z.string().min(1),
247
+ text: z.string().min(1),
248
+ thread_ts: z.string().optional(),
249
+ });
250
+
251
+ registerWriteTool(
252
+ "slack_message_post",
253
+ {
254
+ mutates: "slack.message.post",
255
+ recoverable: true,
256
+ scopeTargetOf: (p) => ({ kind: "channel", value: p.channel }),
257
+ },
258
+ "Post a message to a channel.",
259
+ slackMessagePostSchema,
260
+ async (parsed) => {
261
+ const body: Record<string, unknown> = {
262
+ channel: parsed.channel,
263
+ text: parsed.text,
264
+ };
265
+ putOptionalNonEmptyString(body, "thread_ts", parsed.thread_ts);
266
+ return slackInvokeJson("chat.postMessage", body, "Slack chat.postMessage");
267
+ },
268
+ );
269
+
270
+ const slackMessagePostDmSchema = z.object({
271
+ user_ids: z.string().min(1),
272
+ text: z.string().min(1),
273
+ });
274
+
275
+ reg(
276
+ "slack_message_post_dm",
277
+ "Open or find a DM with user id(s) and send a message (requires HITL slack.message.post).",
278
+ slackMessagePostDmSchema,
279
+ async (parsed) => {
280
+ const token = requireProcessEnv("SLACK_USER_ACCESS_TOKEN");
281
+ const open = await slackApi(token, "conversations.open", {
282
+ users: parsed.user_ids,
283
+ return_im: true,
284
+ });
285
+ if (!open.ok) {
286
+ throw new Error(`Slack conversations.open: ${open.text.slice(0, 400)}`);
287
+ }
288
+ const ch = open.json["channel"];
289
+ const chRec =
290
+ ch !== null && typeof ch === "object" && !Array.isArray(ch)
291
+ ? (ch as SlackApiRecord)
292
+ : undefined;
293
+ const channelId = chRec !== undefined && typeof chRec["id"] === "string" ? chRec["id"] : "";
294
+ if (channelId === "") {
295
+ throw new Error("Slack conversations.open: missing channel id");
296
+ }
297
+ const post = await slackApi(token, "chat.postMessage", {
298
+ channel: channelId,
299
+ text: parsed.text,
300
+ });
301
+ if (!post.ok) {
302
+ throw new Error(`Slack chat.postMessage (dm): ${post.text.slice(0, 400)}`);
303
+ }
304
+ return jsonResult({ open: open.json, post: post.json });
305
+ },
306
+ );
307
+
308
+ // --- ChatOps operational tools (Slice 5) -------------------------------------------------------
309
+ // These use the bot/app tokens (xoxb-/xapp-), distinct from the user token the read tools use.
310
+ // Only `chatops/reply-dispatcher.ts` + `chatops/transport/` reference `slack_chat_post` (static D17).
311
+
312
+ const slackUserInfoSchema = z.object({ user: z.string().min(1) });
313
+ reg(
314
+ "slack_user_info",
315
+ "Fetch a Slack user's profile (incl. email) by user id (ChatOps identity mapping; bot token).",
316
+ slackUserInfoSchema,
317
+ async (parsed) =>
318
+ slackInvokeJsonWithToken(
319
+ requireProcessEnv("SLACK_BOT_TOKEN"),
320
+ "users.info",
321
+ { user: parsed.user },
322
+ "Slack users.info",
323
+ ),
324
+ );
325
+
326
+ const slackChatPostSchema = z.object({ channel: z.string().min(1), text: z.string().min(1) });
327
+ registerWriteTool(
328
+ "slack_chat_post",
329
+ {
330
+ mutates: "slack.chat.post",
331
+ recoverable: true,
332
+ scopeTargetOf: (p) => ({ kind: "channel", value: p.channel }),
333
+ },
334
+ "Post an operational bot message to a channel (ChatOps reply surface; bot token).",
335
+ slackChatPostSchema,
336
+ async (parsed) =>
337
+ slackInvokeJsonWithToken(
338
+ requireProcessEnv("SLACK_BOT_TOKEN"),
339
+ "chat.postMessage",
340
+ { channel: parsed.channel, text: parsed.text },
341
+ "Slack chat.postMessage",
342
+ ),
343
+ );
344
+
345
+ const slackSocketOpenSchema = z.object({});
346
+ reg(
347
+ "slack_socket_open",
348
+ "Open a Slack Socket Mode connection and return the short-lived wss:// URL (app-level token).",
349
+ slackSocketOpenSchema,
350
+ async () =>
351
+ slackInvokeJsonWithToken(
352
+ requireProcessEnv("SLACK_APP_TOKEN"), // xapp-… (Socket Mode app-level token)
353
+ "apps.connections.open",
354
+ {},
355
+ "Slack apps.connections.open",
356
+ ),
357
+ );
358
+
359
+ const transport = new StdioServerTransport();
360
+ await server.connect(transport);
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "com.nimbus.snowflake",
3
+ "displayName": "Snowflake",
4
+ "version": "0.1.0",
5
+ "description": "Snowflake connector. Indexes Snowflake databases/schemas/tables (column names + tags only, NEVER row data) as `snowflake:data_model` items in the local index and exposes read tools (`snowflake_list`, `snowflake_get`, `snowflake_search`) plus HITL-gated governance write tools (`snowflake_tag_set`, `snowflake_comment_set`). Uses the Snowflake SQL REST API (`POST /api/v2/statements`) with OAuth token or key-pair JWT auth. The account identifier is supplied via the `snowflake.account` vault key and added to the sandbox network list at spawn time by `phase3AddSnowflakeMcp`, so the static manifest network list is intentionally empty.",
6
+ "author": "Nimbus",
7
+ "entrypoint": "dist/server.js",
8
+ "runtime": "bun",
9
+ "permissions": {
10
+ "network": [],
11
+ "filesystem": { "read": [], "write": [] }
12
+ },
13
+ "hitlRequired": ["write"],
14
+ "syncInterval": 600,
15
+ "minNimbusVersion": "0.5.0"
16
+ }
@@ -0,0 +1,26 @@
1
+ import {
2
+ asRecord,
3
+ makeQueryFilter,
4
+ type SearchMatchOptions,
5
+ } from "../../../shared/search-filter.ts";
6
+
7
+ export type SnowflakeSearchMatchOptions = SearchMatchOptions;
8
+
9
+ function stringAt(row: Record<string, unknown>, key: string): string {
10
+ const v = row[key];
11
+ return typeof v === "string" ? v : "";
12
+ }
13
+
14
+ function fieldsOf(item: unknown): readonly string[] | null {
15
+ const row = asRecord(item);
16
+ if (row === undefined) {
17
+ return null;
18
+ }
19
+ return [
20
+ stringAt(row, "table_name"),
21
+ stringAt(row, "schema_name"),
22
+ stringAt(row, "database_name"),
23
+ ];
24
+ }
25
+
26
+ export const filterSnowflakeTables = makeQueryFilter(fieldsOf);
@@ -0,0 +1,236 @@
1
+ import { z } from "zod";
2
+ import { type ConsentServer, createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
3
+ import { searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
4
+ import { fetchWithTimeout, mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
5
+ import {
6
+ runReadOnlyMcpConnector,
7
+ type ZodToolRegistrar,
8
+ } from "../../../shared/run-read-only-mcp-connector.ts";
9
+ import { filterSnowflakeTables } from "./search-filter.ts";
10
+
11
+ function snowflakeAccount(): string {
12
+ const v = process.env["SNOWFLAKE_ACCOUNT"]?.trim();
13
+ if (v === undefined || v === "") {
14
+ throw new Error("SNOWFLAKE_ACCOUNT is not set");
15
+ }
16
+ return v;
17
+ }
18
+
19
+ function authHeader(): Record<string, string> {
20
+ const t = process.env["SNOWFLAKE_TOKEN"]?.trim();
21
+ if (t === undefined || t === "") {
22
+ throw new Error("SNOWFLAKE_TOKEN is not set");
23
+ }
24
+ return {
25
+ Authorization: `Bearer ${t}`,
26
+ "Content-Type": "application/json",
27
+ Accept: "application/json",
28
+ };
29
+ }
30
+
31
+ /**
32
+ * POST a single SQL statement to the Snowflake SQL REST API (`/api/v2/statements`) with the
33
+ * connector's Bearer auth. Returns the parsed JSON response; throws on a non-OK status. Shared by
34
+ * the read-only `fetchTables` path and the HITL-gated write tools so auth/transport stays in one place.
35
+ */
36
+ async function executeStatement(statement: string): Promise<unknown> {
37
+ const url = `https://${snowflakeAccount()}.snowflakecomputing.com/api/v2/statements`;
38
+ const res = await fetchWithTimeout(url, {
39
+ method: "POST",
40
+ headers: authHeader(),
41
+ body: JSON.stringify({ statement, timeout: 60 }),
42
+ });
43
+ const text = await res.text();
44
+ if (!res.ok) {
45
+ throw new Error(`Snowflake ${String(res.status)}: ${text.slice(0, 400)}`);
46
+ }
47
+ return JSON.parse(text) as unknown;
48
+ }
49
+
50
+ // Each dot-separated part is an unquoted identifier OR a fully double-quoted token (no embedded ").
51
+ const SF_IDENT_PART = /^(?:[A-Za-z_][A-Za-z0-9_$]*|"[^"]+")$/;
52
+ function assertSfIdentifier(name: string, label: string): string {
53
+ const parts = name.split(".");
54
+ for (const part of parts) {
55
+ if (!SF_IDENT_PART.test(part)) {
56
+ throw new Error(`unsafe Snowflake identifier component for ${label}: ${part} in ${name}`);
57
+ }
58
+ }
59
+ return name;
60
+ }
61
+
62
+ // String literals are escaped by doubling single quotes (the only Snowflake literal-injection vector).
63
+ function sfLiteral(v: string): string {
64
+ return `'${v.replaceAll("'", "''")}'`;
65
+ }
66
+
67
+ const TABLES_SQL =
68
+ "SELECT table_catalog AS database_name, table_schema AS schema_name, table_name, " +
69
+ "row_count, last_altered FROM information_schema.tables WHERE table_schema <> 'INFORMATION_SCHEMA'";
70
+
71
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
72
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
73
+ return undefined;
74
+ }
75
+ return value as Record<string, unknown>;
76
+ }
77
+
78
+ function rowsFromStatementsResponse(parsed: unknown): Record<string, unknown>[] {
79
+ const root = asRecord(parsed);
80
+ const meta = asRecord(root?.["resultSetMetaData"]);
81
+ const rowType = Array.isArray(meta?.["rowType"]) ? meta["rowType"] : [];
82
+ const names = rowType.map(
83
+ (c) => (asRecord(c)?.["name"] as string | undefined)?.toLowerCase() ?? "",
84
+ );
85
+ const data = Array.isArray(root?.["data"]) ? root["data"] : [];
86
+ const out: Record<string, unknown>[] = [];
87
+ for (const rr of data) {
88
+ if (!Array.isArray(rr)) continue;
89
+ const obj: Record<string, unknown> = {};
90
+ names.forEach((name, i) => {
91
+ if (name !== "") obj[name] = rr[i];
92
+ });
93
+ if (typeof obj["row_count"] === "string" && obj["row_count"] !== "") {
94
+ obj["row_count"] = Number(obj["row_count"]);
95
+ }
96
+ out.push(obj);
97
+ }
98
+ return out;
99
+ }
100
+
101
+ /**
102
+ * Fetch table metadata. With no `page`, returns the full set (used by `snowflake_get` /
103
+ * `snowflake_search`, which must scan every table). With a `page`, appends a deterministic
104
+ * `ORDER BY … LIMIT … OFFSET …` so `snowflake_list` can drain pages — only numbers are
105
+ * interpolated, so there is no SQL-injection surface.
106
+ */
107
+ async function fetchTables(page?: {
108
+ limit: number;
109
+ offset: number;
110
+ }): Promise<Record<string, unknown>[]> {
111
+ const statement =
112
+ page === undefined
113
+ ? TABLES_SQL
114
+ : // (table_catalog, table_schema, table_name) uniquely identifies a table in INFORMATION_SCHEMA
115
+ // — a TOTAL order, so OFFSET paging cannot skip or duplicate rows across pages.
116
+ `${TABLES_SQL} ORDER BY table_catalog, table_schema, table_name LIMIT ${page.limit} OFFSET ${page.offset}`;
117
+ return rowsFromStatementsResponse(await executeStatement(statement));
118
+ }
119
+
120
+ function tableKey(row: Record<string, unknown>): string {
121
+ const db = typeof row["database_name"] === "string" ? row["database_name"].toLowerCase() : "";
122
+ const schema = typeof row["schema_name"] === "string" ? row["schema_name"].toLowerCase() : "";
123
+ const table = typeof row["table_name"] === "string" ? row["table_name"].toLowerCase() : "";
124
+ return `${db}.${schema}.${table}`;
125
+ }
126
+
127
+ /**
128
+ * Register the three read-only Snowflake tools on `reg`. Exported (rather than inlined into the
129
+ * `runReadOnlyMcpConnector` call) so the tool handlers can be exercised in unit tests without
130
+ * starting a real stdio transport.
131
+ */
132
+ export function registerSnowflakeTools(reg: ZodToolRegistrar, server: unknown): void {
133
+ // Despite the read-only helper's name, this connector exposes write tools. The consent
134
+ // kit needs the real server, which the helper now passes through as its second argument.
135
+ const registerWriteTool = createWriteToolRegistrar(server as ConsentServer, {
136
+ connector: "snowflake",
137
+ scopeEnv: "NIMBUS_MCP_SNOWFLAKE_WRITE_SCOPE",
138
+ scopeKinds: ["object"],
139
+ });
140
+
141
+ reg(
142
+ "snowflake_list",
143
+ "List Snowflake tables across all databases and schemas. Paginated: `cursor` (opaque offset) + `limit` (default 200, max 500) → `{ items, nextCursor }`.",
144
+ z.object({
145
+ cursor: z.string().nullable().optional(),
146
+ limit: z.number().int().min(1).max(500).optional(),
147
+ }),
148
+ async (p) => {
149
+ const limit = p.limit ?? 200;
150
+ const offset =
151
+ p.cursor === null || p.cursor === undefined || p.cursor === ""
152
+ ? 0
153
+ : Math.max(0, Number.parseInt(p.cursor, 10) || 0);
154
+ const items = await fetchTables({ limit, offset });
155
+ const nextCursor = items.length === limit ? String(offset + limit) : null;
156
+ return jsonResult({ items, nextCursor });
157
+ },
158
+ );
159
+
160
+ reg(
161
+ "snowflake_get",
162
+ "Fetch one Snowflake table by its fully-qualified id (`database.schema.table`, case-insensitive). Throws when the table is not found.",
163
+ z.object({
164
+ id: z.string().min(1),
165
+ }),
166
+ async (p) => {
167
+ const tables = await fetchTables();
168
+ const needle = p.id.toLowerCase();
169
+ const found = tables.find((row) => tableKey(row) === needle);
170
+ if (found === undefined) {
171
+ throw new Error(`Snowflake table not found: ${p.id}`);
172
+ }
173
+ return jsonResult(found);
174
+ },
175
+ );
176
+
177
+ reg(
178
+ "snowflake_search",
179
+ "Substring search across Snowflake tables. Matches the query (case-insensitive) against table name, schema name, and database name. Returns a `{ matches: [...] }` envelope.",
180
+ searchToolInputSchema(200),
181
+ async (p) => {
182
+ const tables = await fetchTables();
183
+ const matches = filterSnowflakeTables(tables, { query: p.query, limit: p.limit });
184
+ return jsonResult({ matches });
185
+ },
186
+ );
187
+
188
+ registerWriteTool(
189
+ "snowflake_tag_set",
190
+ {
191
+ mutates: "snowflake.tag.set",
192
+ recoverable: true,
193
+ scopeTargetOf: (p) => ({ kind: "object", value: p.object }),
194
+ },
195
+ "Set or unset a governance TAG on a table. `ALTER TABLE <object> SET TAG <tag> = '<value>'`; omit `value` to UNSET.",
196
+ z.object({
197
+ object: z.string().min(1),
198
+ tag: z.string().min(1),
199
+ value: z.string().optional(),
200
+ }),
201
+ async (p) => {
202
+ const obj = assertSfIdentifier(p.object, "object");
203
+ const tag = assertSfIdentifier(p.tag, "tag");
204
+ const statement =
205
+ p.value === undefined
206
+ ? `ALTER TABLE ${obj} UNSET TAG ${tag}`
207
+ : `ALTER TABLE ${obj} SET TAG ${tag} = ${sfLiteral(p.value)}`;
208
+ await executeStatement(statement);
209
+ return jsonResult({ status: "ok", object: obj, tag });
210
+ },
211
+ );
212
+
213
+ registerWriteTool(
214
+ "snowflake_comment_set",
215
+ {
216
+ mutates: "snowflake.comment.set",
217
+ recoverable: true,
218
+ scopeTargetOf: (p) => ({ kind: "object", value: p.object }),
219
+ },
220
+ "Set a COMMENT on a table. `COMMENT ON TABLE <object> IS '<comment>'`.",
221
+ z.object({ object: z.string().min(1), comment: z.string() }),
222
+ async (p) => {
223
+ const obj = assertSfIdentifier(p.object, "object");
224
+ await executeStatement(`COMMENT ON TABLE ${obj} IS ${sfLiteral(p.comment)}`);
225
+ return jsonResult({ status: "ok", object: obj });
226
+ },
227
+ );
228
+ }
229
+
230
+ // Exported so the bundled-connector registry can start this server explicitly: `import.meta.main`
231
+ // is false under an import, and the module must stay importable by tests without connecting stdio.
232
+ export async function startConnector(): Promise<void> {
233
+ await runReadOnlyMcpConnector("nimbus-snowflake", registerSnowflakeTools);
234
+ }
235
+
236
+ if (import.meta.main) await startConnector();
@@ -0,0 +1,15 @@
1
+ {
2
+ "id": "com.nimbus.snyk",
3
+ "displayName": "Snyk",
4
+ "version": "0.1.0",
5
+ "description": "Snyk REST connector (issues, projects). Read-only; enables CVE-to-repo-to-PR correlation queries from the local index. Self-hosted Snyk endpoints are a Task 14 follow-up.",
6
+ "author": "Nimbus",
7
+ "entrypoint": "dist/server.js",
8
+ "runtime": "bun",
9
+ "permissions": {
10
+ "network": ["api.snyk.io"]
11
+ },
12
+ "hitlRequired": [],
13
+ "syncInterval": 600,
14
+ "minNimbusVersion": "0.2.0"
15
+ }
@@ -0,0 +1,28 @@
1
+ import {
2
+ asObjectish,
3
+ makeQueryFilter,
4
+ type SearchMatchOptions,
5
+ } from "../../../shared/search-filter.ts";
6
+
7
+ export type SnykSearchMatchOptions = SearchMatchOptions;
8
+
9
+ function fieldsOf(item: unknown): readonly string[] | null {
10
+ const row = asObjectish(item);
11
+ if (row === undefined) {
12
+ return null;
13
+ }
14
+ const dataField = row["issueData"];
15
+ const data =
16
+ dataField !== null && typeof dataField === "object"
17
+ ? (dataField as Record<string, unknown>)
18
+ : {};
19
+ const title = typeof data["title"] === "string" ? data["title"] : "";
20
+ const cveField = data["cve"];
21
+ const cves = Array.isArray(cveField)
22
+ ? cveField.filter((c): c is string => typeof c === "string")
23
+ : [];
24
+ const pkg = typeof row["pkgName"] === "string" ? row["pkgName"] : "";
25
+ return [title, cves.join(" "), pkg];
26
+ }
27
+
28
+ export const filterSnykAggregatedIssues = makeQueryFilter(fieldsOf);