@nimbus-dev/connectors 0.2.1 → 0.2.3

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 (220) hide show
  1. package/README.md +19 -7
  2. package/connectors/airflow/src/search-filter.ts +2 -15
  3. package/connectors/airflow/src/server.ts +2 -87
  4. package/connectors/airflow/src/tools.ts +74 -0
  5. package/connectors/apple/src/adapters.ts +326 -0
  6. package/connectors/apple/src/apple-mail-core.ts +4 -8
  7. package/connectors/apple/src/calendar-tools.ts +28 -8
  8. package/connectors/apple/src/server.ts +23 -512
  9. package/connectors/apple/src/tools.ts +18 -44
  10. package/connectors/argocd/src/server.ts +25 -38
  11. package/connectors/athena/src/tools.ts +26 -43
  12. package/connectors/aws/src/server.ts +2 -183
  13. package/connectors/aws/src/tools.ts +199 -0
  14. package/connectors/azure/src/server.ts +2 -131
  15. package/connectors/azure/src/tools.ts +134 -0
  16. package/connectors/bigeye/src/search-filter.ts +4 -15
  17. package/connectors/bigeye/src/server.ts +38 -38
  18. package/connectors/bigquery/src/tools.ts +1 -26
  19. package/connectors/bitbucket/src/server.ts +2 -244
  20. package/connectors/bitbucket/src/tools.ts +241 -0
  21. package/connectors/bitrise/src/server.ts +2 -79
  22. package/connectors/bitrise/src/tools.ts +76 -0
  23. package/connectors/canva/src/search-filter.ts +2 -11
  24. package/connectors/canva/src/server.ts +2 -73
  25. package/connectors/canva/src/tools.ts +66 -0
  26. package/connectors/circleci/src/server.ts +2 -176
  27. package/connectors/circleci/src/tools.ts +193 -0
  28. package/connectors/cloud-logging/src/tools.ts +19 -35
  29. package/connectors/cloudwatch/src/tools.ts +20 -38
  30. package/connectors/codemagic/src/server.ts +2 -70
  31. package/connectors/codemagic/src/tools.ts +71 -0
  32. package/connectors/confluence/src/server.ts +2 -301
  33. package/connectors/confluence/src/tools.ts +320 -0
  34. package/connectors/dagster/src/server.ts +2 -214
  35. package/connectors/dagster/src/tools.ts +187 -0
  36. package/connectors/databricks/src/search-filter.ts +7 -7
  37. package/connectors/databricks/src/server.ts +2 -77
  38. package/connectors/databricks/src/tools.ts +66 -0
  39. package/connectors/datadog/src/server.ts +2 -48
  40. package/connectors/datadog/src/tools.ts +48 -0
  41. package/connectors/dataprofile/src/profile.ts +16 -46
  42. package/connectors/dbt/src/server.ts +2 -92
  43. package/connectors/dbt/src/tools.ts +80 -0
  44. package/connectors/dependencytrack/src/server.ts +2 -76
  45. package/connectors/dependencytrack/src/tools.ts +65 -0
  46. package/connectors/discord/src/server.ts +2 -85
  47. package/connectors/discord/src/tools.ts +82 -0
  48. package/connectors/elasticsearch/src/tools.ts +7 -37
  49. package/connectors/fastmail/src/jmap-client.ts +202 -0
  50. package/connectors/fastmail/src/jmap-core.ts +8 -21
  51. package/connectors/fastmail/src/server.ts +2 -187
  52. package/connectors/fastmail/src/tools.ts +4 -23
  53. package/connectors/figma/src/search-filter.ts +2 -11
  54. package/connectors/figma/src/server.ts +2 -113
  55. package/connectors/figma/src/tools.ts +99 -0
  56. package/connectors/firebase/src/server.ts +2 -90
  57. package/connectors/firebase/src/token.ts +2 -5
  58. package/connectors/firebase/src/tools.ts +106 -0
  59. package/connectors/flagsmith/src/server.ts +2 -98
  60. package/connectors/flagsmith/src/tools.ts +95 -0
  61. package/connectors/flux/src/server.ts +52 -53
  62. package/connectors/gcp/src/server.ts +2 -132
  63. package/connectors/gcp/src/tools.ts +123 -0
  64. package/connectors/github/src/server.ts +2 -328
  65. package/connectors/github/src/tools.ts +311 -0
  66. package/connectors/github-actions/src/server.ts +2 -205
  67. package/connectors/github-actions/src/tools.ts +209 -0
  68. package/connectors/gitlab/src/server.ts +2 -384
  69. package/connectors/gitlab/src/tools.ts +392 -0
  70. package/connectors/gmail/src/server.ts +2 -270
  71. package/connectors/gmail/src/tools.ts +209 -0
  72. package/connectors/google-drive/src/server.ts +2 -524
  73. package/connectors/google-drive/src/tools.ts +529 -0
  74. package/connectors/google-meet/src/server.ts +2 -82
  75. package/connectors/google-meet/src/tools.ts +87 -0
  76. package/connectors/google-photos/src/server.ts +2 -132
  77. package/connectors/google-photos/src/tools.ts +138 -0
  78. package/connectors/grafana/src/server.ts +2 -53
  79. package/connectors/grafana/src/tools.ts +53 -0
  80. package/connectors/great-expectations/src/gx-parse.ts +14 -45
  81. package/connectors/greenhouse/src/server.ts +2 -63
  82. package/connectors/greenhouse/src/tools.ts +68 -0
  83. package/connectors/hubspot/src/server.ts +2 -77
  84. package/connectors/hubspot/src/tools.ts +70 -0
  85. package/connectors/iac/src/server.ts +2 -137
  86. package/connectors/iac/src/tools.ts +153 -0
  87. package/connectors/imap/src/imap-core.ts +29 -82
  88. package/connectors/imap/src/server.ts +33 -220
  89. package/connectors/intercom/src/server.ts +2 -61
  90. package/connectors/intercom/src/tools.ts +58 -0
  91. package/connectors/jenkins/src/jenkins-api.ts +2 -6
  92. package/connectors/jenkins/src/server.ts +2 -215
  93. package/connectors/jenkins/src/tools.ts +240 -0
  94. package/connectors/jira/src/server.ts +2 -301
  95. package/connectors/jira/src/tools.ts +325 -0
  96. package/connectors/kubernetes/src/server.ts +2 -155
  97. package/connectors/kubernetes/src/tools.ts +160 -0
  98. package/connectors/launchdarkly/src/server.ts +2 -75
  99. package/connectors/launchdarkly/src/tools.ts +72 -0
  100. package/connectors/lever/src/search-filter.ts +4 -12
  101. package/connectors/lever/src/server.ts +2 -60
  102. package/connectors/lever/src/tools.ts +61 -0
  103. package/connectors/linear/src/server.ts +2 -459
  104. package/connectors/linear/src/tools.ts +482 -0
  105. package/connectors/localdb/src/sql-scan.ts +13 -41
  106. package/connectors/localdb/src/tools.ts +1 -1
  107. package/connectors/looker/src/search-filter.ts +2 -15
  108. package/connectors/looker/src/server.ts +35 -70
  109. package/connectors/mendeley/src/server.ts +2 -55
  110. package/connectors/mendeley/src/tools.ts +54 -0
  111. package/connectors/mercury/src/server.ts +2 -57
  112. package/connectors/mercury/src/tools.ts +50 -0
  113. package/connectors/metabase/src/search-filter.ts +4 -15
  114. package/connectors/metabase/src/server.ts +2 -79
  115. package/connectors/metabase/src/tools.ts +64 -0
  116. package/connectors/miro/src/server.ts +2 -72
  117. package/connectors/miro/src/tools.ts +65 -0
  118. package/connectors/mlflow/src/search-filter.ts +3 -7
  119. package/connectors/mlflow/src/server.ts +20 -40
  120. package/connectors/monte-carlo/src/server.ts +50 -57
  121. package/connectors/netlify/src/search-filter.ts +6 -14
  122. package/connectors/netlify/src/server.ts +2 -60
  123. package/connectors/netlify/src/tools.ts +53 -0
  124. package/connectors/newrelic/src/server.ts +2 -40
  125. package/connectors/newrelic/src/tools.ts +37 -0
  126. package/connectors/notion/src/server.ts +2 -335
  127. package/connectors/notion/src/tools.ts +344 -0
  128. package/connectors/obsidian/src/server.ts +2 -425
  129. package/connectors/obsidian/src/tools.ts +466 -0
  130. package/connectors/onedrive/src/server.ts +2 -249
  131. package/connectors/onedrive/src/tools.ts +253 -0
  132. package/connectors/outlook/src/server.ts +2 -351
  133. package/connectors/outlook/src/tools.ts +357 -0
  134. package/connectors/pagerduty/src/server.ts +2 -132
  135. package/connectors/pagerduty/src/tools.ts +126 -0
  136. package/connectors/pipedrive/src/server.ts +2 -58
  137. package/connectors/pipedrive/src/tools.ts +63 -0
  138. package/connectors/powerbi/src/search-filter.ts +2 -15
  139. package/connectors/powerbi/src/server.ts +43 -68
  140. package/connectors/prefect/src/search-filter.ts +3 -16
  141. package/connectors/prefect/src/server.ts +2 -90
  142. package/connectors/prefect/src/tools.ts +83 -0
  143. package/connectors/protonmail/src/mail-core.ts +16 -75
  144. package/connectors/protonmail/src/server.ts +34 -214
  145. package/connectors/raindrop/src/server.ts +2 -103
  146. package/connectors/raindrop/src/tools.ts +103 -0
  147. package/connectors/ramp/src/server.ts +2 -105
  148. package/connectors/ramp/src/tools.ts +98 -0
  149. package/connectors/readwise/src/server.ts +2 -88
  150. package/connectors/readwise/src/tools.ts +81 -0
  151. package/connectors/sagemaker/src/tools.ts +21 -40
  152. package/connectors/salesforce/src/search-filter.ts +4 -11
  153. package/connectors/salesforce/src/server.ts +2 -90
  154. package/connectors/salesforce/src/tools.ts +75 -0
  155. package/connectors/semgrep/src/search-filter.ts +3 -10
  156. package/connectors/semgrep/src/server.ts +2 -104
  157. package/connectors/semgrep/src/tools.ts +92 -0
  158. package/connectors/sentry/src/server.ts +2 -58
  159. package/connectors/sentry/src/tools.ts +53 -0
  160. package/connectors/slack/src/server.ts +2 -352
  161. package/connectors/slack/src/tools.ts +376 -0
  162. package/connectors/snowflake/src/search-filter.ts +4 -19
  163. package/connectors/snowflake/src/server.ts +6 -25
  164. package/connectors/snyk/src/server.ts +2 -140
  165. package/connectors/snyk/src/tools.ts +136 -0
  166. package/connectors/sonarqube/src/server.ts +2 -111
  167. package/connectors/sonarqube/src/tools.ts +98 -0
  168. package/connectors/stackoverflow/src/server.ts +2 -77
  169. package/connectors/stackoverflow/src/tools.ts +66 -0
  170. package/connectors/storybook/src/storybook-parse.ts +3 -6
  171. package/connectors/stripe/src/server.ts +2 -61
  172. package/connectors/stripe/src/tools.ts +54 -0
  173. package/connectors/superset/src/search-filter.ts +4 -15
  174. package/connectors/superset/src/server.ts +2 -115
  175. package/connectors/superset/src/tools.ts +98 -0
  176. package/connectors/tableau/src/search-filter.ts +2 -15
  177. package/connectors/tableau/src/server.ts +13 -65
  178. package/connectors/teams/src/server.ts +2 -249
  179. package/connectors/teams/src/tools.ts +283 -0
  180. package/connectors/testflight/src/server.ts +2 -70
  181. package/connectors/testflight/src/tools.ts +79 -0
  182. package/connectors/vercel/src/search-filter.ts +2 -10
  183. package/connectors/vercel/src/server.ts +2 -75
  184. package/connectors/vercel/src/tools.ts +73 -0
  185. package/connectors/vertex-ai/src/tools.ts +23 -33
  186. package/connectors/wiz/src/search-filter.ts +6 -30
  187. package/connectors/wiz/src/server.ts +2 -188
  188. package/connectors/wiz/src/tools.ts +168 -0
  189. package/connectors/zendesk/src/server.ts +2 -73
  190. package/connectors/zendesk/src/tools.ts +60 -0
  191. package/connectors/zoom/src/search-filter.ts +1 -7
  192. package/connectors/zoom/src/server.ts +2 -94
  193. package/connectors/zoom/src/tools.ts +93 -0
  194. package/connectors/zotero/src/server.ts +2 -78
  195. package/connectors/zotero/src/tools.ts +67 -0
  196. package/package.json +14 -12
  197. package/shared/access-token-cache.ts +123 -0
  198. package/shared/atlassian-json-fetch.ts +6 -16
  199. package/shared/audit-chain.ts +601 -34
  200. package/shared/cli-json-kit.ts +191 -0
  201. package/shared/collection-tool-kit.ts +143 -0
  202. package/shared/connector-mode.ts +3 -2
  203. package/shared/consent-kit.ts +81 -26
  204. package/shared/cursor-list-tool.ts +29 -0
  205. package/shared/env-json-api.ts +209 -0
  206. package/shared/fetch-json-text.ts +27 -0
  207. package/shared/github-rest.ts +62 -0
  208. package/shared/graphql-json.ts +60 -0
  209. package/shared/imap-mail-core.ts +91 -0
  210. package/shared/imap-tool-kit.ts +57 -23
  211. package/shared/imapflow-adapter.ts +417 -0
  212. package/shared/nimbus-spawn.ts +84 -27
  213. package/shared/rest-tool-kit.ts +77 -7
  214. package/shared/run-cli-json.ts +38 -16
  215. package/shared/safe-cli-arg.ts +139 -17
  216. package/shared/search-filter.ts +58 -18
  217. package/shared/walk-files.ts +118 -0
  218. package/shared/windows-batch-args.ts +273 -0
  219. package/shared/write-budget.ts +100 -0
  220. package/standalone/src/launcher.ts +19 -7
@@ -0,0 +1,376 @@
1
+ import { z } from "zod";
2
+ import type { ConsentServer } from "../../../shared/consent-kit.ts";
3
+ import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
4
+ import {
5
+ createRegisterSimpleTool,
6
+ createZodToolRegistrar,
7
+ mcpJsonResult as jsonResult,
8
+ type McpListResult,
9
+ putOptionalBoolean,
10
+ putOptionalNonEmptyString,
11
+ requireProcessEnv,
12
+ } from "../../../shared/mcp-tool-kit.ts";
13
+ import { asRecord } from "../../../shared/search-filter.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: SlackApiRecord = asRecord(parsed) ?? {};
38
+ const okField = json["ok"];
39
+ return { ok: okField === true && res.ok, json, text };
40
+ }
41
+
42
+ async function slackInvokeJsonWithToken(
43
+ token: string,
44
+ method: string,
45
+ body: Record<string, unknown>,
46
+ errorLabel: string,
47
+ ): Promise<McpListResult> {
48
+ const res = await slackApi(token, method, body);
49
+ if (!res.ok) {
50
+ throw new Error(`${errorLabel}: ${res.text.slice(0, 400)}`);
51
+ }
52
+ return jsonResult(res.json);
53
+ }
54
+
55
+ async function slackInvokeJson(
56
+ method: string,
57
+ body: Record<string, unknown>,
58
+ errorLabel: string,
59
+ ): Promise<McpListResult> {
60
+ return slackInvokeJsonWithToken(
61
+ requireProcessEnv("SLACK_USER_ACCESS_TOKEN"),
62
+ method,
63
+ body,
64
+ errorLabel,
65
+ );
66
+ }
67
+
68
+ const slackConversationsHistorySchema = z.object({
69
+ channel: z.string().min(1),
70
+ limit: z.number().int().min(1).max(200).optional(),
71
+ cursor: z.string().optional(),
72
+ oldest: z.string().optional(),
73
+ inclusive: z.boolean().optional(),
74
+ });
75
+
76
+ type SlackConversationsHistoryParsed = z.infer<typeof slackConversationsHistorySchema>;
77
+
78
+ function buildConversationsHistoryBody(
79
+ parsed: SlackConversationsHistoryParsed,
80
+ ): Record<string, unknown> {
81
+ const body: Record<string, unknown> = {
82
+ channel: parsed.channel,
83
+ limit: parsed.limit ?? 50,
84
+ };
85
+ putOptionalNonEmptyString(body, "cursor", parsed.cursor);
86
+ putOptionalNonEmptyString(body, "oldest", parsed.oldest);
87
+ putOptionalBoolean(body, "inclusive", parsed.inclusive);
88
+ return body;
89
+ }
90
+
91
+ /** Tool names exposed by this connector — for contract/introspection tests. */
92
+ export const SLACK_TOOL_NAMES = [
93
+ "slack_channel_list",
94
+ "slack_channel_history",
95
+ "slack_thread_replies",
96
+ "slack_dm_list",
97
+ "slack_dm_history",
98
+ "slack_user_list",
99
+ "slack_user_get",
100
+ "slack_search",
101
+ "slack_message_post",
102
+ "slack_message_post_dm",
103
+ "slack_user_info",
104
+ "slack_chat_post",
105
+ "slack_socket_open",
106
+ ] as const;
107
+
108
+ export function registerSlackTools(
109
+ server: ConsentServer & { tool: (...args: never) => unknown },
110
+ ): void {
111
+ const registerSimpleTool = createRegisterSimpleTool(server);
112
+
113
+ const reg = createZodToolRegistrar(registerSimpleTool);
114
+
115
+ /**
116
+ * Every MUTATING slack tool goes through here. Outside the gateway this adds the
117
+ * consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
118
+ * the gateway it is a pass-through, because executor.ts (I2) is the gate there.
119
+ */
120
+ const registerWriteTool = createWriteToolRegistrar(server, {
121
+ connector: "slack",
122
+ scopeEnv: "NIMBUS_MCP_SLACK_WRITE_SCOPE",
123
+ // `user` scopes a direct message by the recipient's user id, which is all a DM has before
124
+ // `conversations.open` creates its channel.
125
+ scopeKinds: ["channel", "user"],
126
+ });
127
+
128
+ const slackChannelListSchema = z.object({
129
+ types: z.string().optional(),
130
+ limit: z.number().int().min(1).max(200).optional(),
131
+ cursor: z.string().optional(),
132
+ });
133
+
134
+ reg(
135
+ "slack_channel_list",
136
+ "List channels the user is a member of (public, private, mpim, im).",
137
+ slackChannelListSchema,
138
+ async (parsed) => {
139
+ const body: Record<string, unknown> = {
140
+ types: parsed.types ?? "public_channel,private_channel,mpim,im",
141
+ limit: parsed.limit ?? 200,
142
+ exclude_archived: true,
143
+ };
144
+ putOptionalNonEmptyString(body, "cursor", parsed.cursor);
145
+ return slackInvokeJson("conversations.list", body, "Slack conversations.list");
146
+ },
147
+ );
148
+
149
+ reg(
150
+ "slack_channel_history",
151
+ "Fetch message history for a channel, DM, or mpim.",
152
+ slackConversationsHistorySchema,
153
+ async (parsed) =>
154
+ slackInvokeJson(
155
+ "conversations.history",
156
+ buildConversationsHistoryBody(parsed),
157
+ "Slack conversations.history",
158
+ ),
159
+ );
160
+
161
+ const slackThreadRepliesSchema = z.object({
162
+ channel: z.string().min(1),
163
+ ts: z.string().min(1),
164
+ limit: z.number().int().min(1).max(200).optional(),
165
+ cursor: z.string().optional(),
166
+ });
167
+
168
+ reg(
169
+ "slack_thread_replies",
170
+ "Fetch replies in a thread (channel + thread parent ts).",
171
+ slackThreadRepliesSchema,
172
+ async (parsed) => {
173
+ const body: Record<string, unknown> = {
174
+ channel: parsed.channel,
175
+ ts: parsed.ts,
176
+ limit: parsed.limit ?? 50,
177
+ };
178
+ putOptionalNonEmptyString(body, "cursor", parsed.cursor);
179
+ return slackInvokeJson("conversations.replies", body, "Slack conversations.replies");
180
+ },
181
+ );
182
+
183
+ const slackDmListSchema = z.object({
184
+ limit: z.number().int().min(1).max(200).optional(),
185
+ cursor: z.string().optional(),
186
+ });
187
+
188
+ reg(
189
+ "slack_dm_list",
190
+ "List direct message conversations (im + mpim).",
191
+ slackDmListSchema,
192
+ async (parsed) => {
193
+ const body: Record<string, unknown> = {
194
+ types: "im,mpim",
195
+ limit: parsed.limit ?? 200,
196
+ exclude_archived: true,
197
+ };
198
+ putOptionalNonEmptyString(body, "cursor", parsed.cursor);
199
+ return slackInvokeJson("conversations.list", body, "Slack dm list");
200
+ },
201
+ );
202
+
203
+ reg(
204
+ "slack_dm_history",
205
+ "Fetch DM / mpim history (same as channel history; convenience alias).",
206
+ slackConversationsHistorySchema,
207
+ async (parsed) =>
208
+ slackInvokeJson(
209
+ "conversations.history",
210
+ buildConversationsHistoryBody(parsed),
211
+ "Slack dm history",
212
+ ),
213
+ );
214
+
215
+ const slackUserListSchema = z.object({
216
+ limit: z.number().int().min(1).max(200).optional(),
217
+ cursor: z.string().optional(),
218
+ });
219
+
220
+ reg(
221
+ "slack_user_list",
222
+ "List users in the workspace (paginated).",
223
+ slackUserListSchema,
224
+ async (parsed) => {
225
+ const body: Record<string, unknown> = { limit: parsed.limit ?? 100 };
226
+ putOptionalNonEmptyString(body, "cursor", parsed.cursor);
227
+ return slackInvokeJson("users.list", body, "Slack users.list");
228
+ },
229
+ );
230
+
231
+ const slackUserGetSchema = z.object({ user: z.string().min(1) });
232
+
233
+ reg("slack_user_get", "Get a single user profile by ID.", slackUserGetSchema, async (parsed) =>
234
+ slackInvokeJson("users.info", { user: parsed.user }, "Slack users.info"),
235
+ );
236
+
237
+ const slackSearchSchema = z.object({
238
+ query: z.string().min(1),
239
+ count: z.number().int().min(1).max(100).optional(),
240
+ page: z.number().int().min(1).optional(),
241
+ sort: z.enum(["timestamp", "score"]).optional(),
242
+ sort_dir: z.enum(["asc", "desc"]).optional(),
243
+ });
244
+
245
+ reg("slack_search", "Search messages (workspace search).", slackSearchSchema, async (parsed) => {
246
+ const body: Record<string, unknown> = {
247
+ query: parsed.query,
248
+ count: parsed.count ?? 20,
249
+ page: parsed.page ?? 1,
250
+ };
251
+ if (parsed.sort !== undefined) {
252
+ body["sort"] = parsed.sort;
253
+ }
254
+ if (parsed.sort_dir !== undefined) {
255
+ body["sort_dir"] = parsed.sort_dir;
256
+ }
257
+ return slackInvokeJson("search.messages", body, "Slack search.messages");
258
+ });
259
+
260
+ const slackMessagePostSchema = z.object({
261
+ channel: z.string().min(1),
262
+ text: z.string().min(1),
263
+ thread_ts: z.string().optional(),
264
+ });
265
+
266
+ registerWriteTool(
267
+ "slack_message_post",
268
+ {
269
+ mutates: "slack.message.post",
270
+ recoverable: true,
271
+ scopeTargetOf: (p) => ({ kind: "channel", value: p.channel }),
272
+ },
273
+ "Post a message to a channel.",
274
+ slackMessagePostSchema,
275
+ async (parsed) => {
276
+ const body: Record<string, unknown> = {
277
+ channel: parsed.channel,
278
+ text: parsed.text,
279
+ };
280
+ putOptionalNonEmptyString(body, "thread_ts", parsed.thread_ts);
281
+ return slackInvokeJson("chat.postMessage", body, "Slack chat.postMessage");
282
+ },
283
+ );
284
+
285
+ const slackMessagePostDmSchema = z.object({
286
+ user_ids: z.string().min(1),
287
+ text: z.string().min(1),
288
+ });
289
+
290
+ // A WRITE: it sends a message. It was registered as a read, which in standalone mode offered it
291
+ // to every client with no consent prompt, scope check, budget or audit record.
292
+ registerWriteTool(
293
+ "slack_message_post_dm",
294
+ {
295
+ mutates: "slack.message.post",
296
+ recoverable: true,
297
+ scopeTargetOf: (p) => ({ kind: "user", value: p.user_ids }),
298
+ },
299
+ "Open or find a DM with user id(s) and send a message.",
300
+ slackMessagePostDmSchema,
301
+ async (parsed) => {
302
+ const token = requireProcessEnv("SLACK_USER_ACCESS_TOKEN");
303
+ const open = await slackApi(token, "conversations.open", {
304
+ users: parsed.user_ids,
305
+ return_im: true,
306
+ });
307
+ if (!open.ok) {
308
+ throw new Error(`Slack conversations.open: ${open.text.slice(0, 400)}`);
309
+ }
310
+ const chRec = asRecord(open.json["channel"]);
311
+ const channelId = chRec !== undefined && typeof chRec["id"] === "string" ? chRec["id"] : "";
312
+ if (channelId === "") {
313
+ throw new Error("Slack conversations.open: missing channel id");
314
+ }
315
+ const post = await slackApi(token, "chat.postMessage", {
316
+ channel: channelId,
317
+ text: parsed.text,
318
+ });
319
+ if (!post.ok) {
320
+ throw new Error(`Slack chat.postMessage (dm): ${post.text.slice(0, 400)}`);
321
+ }
322
+ return jsonResult({ open: open.json, post: post.json });
323
+ },
324
+ );
325
+
326
+ // --- ChatOps operational tools (Slice 5) -------------------------------------------------------
327
+ // These use the bot/app tokens (xoxb-/xapp-), distinct from the user token the read tools use.
328
+ // Only `chatops/reply-dispatcher.ts` + `chatops/transport/` reference `slack_chat_post` (static D17).
329
+
330
+ const slackUserInfoSchema = z.object({ user: z.string().min(1) });
331
+ reg(
332
+ "slack_user_info",
333
+ "Fetch a Slack user's profile (incl. email) by user id (ChatOps identity mapping; bot token).",
334
+ slackUserInfoSchema,
335
+ async (parsed) =>
336
+ slackInvokeJsonWithToken(
337
+ requireProcessEnv("SLACK_BOT_TOKEN"),
338
+ "users.info",
339
+ { user: parsed.user },
340
+ "Slack users.info",
341
+ ),
342
+ );
343
+
344
+ const slackChatPostSchema = z.object({ channel: z.string().min(1), text: z.string().min(1) });
345
+ registerWriteTool(
346
+ "slack_chat_post",
347
+ {
348
+ mutates: "slack.chat.post",
349
+ recoverable: true,
350
+ scopeTargetOf: (p) => ({ kind: "channel", value: p.channel }),
351
+ },
352
+ "Post an operational bot message to a channel (ChatOps reply surface; bot token).",
353
+ slackChatPostSchema,
354
+ async (parsed) =>
355
+ slackInvokeJsonWithToken(
356
+ requireProcessEnv("SLACK_BOT_TOKEN"),
357
+ "chat.postMessage",
358
+ { channel: parsed.channel, text: parsed.text },
359
+ "Slack chat.postMessage",
360
+ ),
361
+ );
362
+
363
+ const slackSocketOpenSchema = z.object({});
364
+ reg(
365
+ "slack_socket_open",
366
+ "Open a Slack Socket Mode connection and return the short-lived wss:// URL (app-level token).",
367
+ slackSocketOpenSchema,
368
+ async () =>
369
+ slackInvokeJsonWithToken(
370
+ requireProcessEnv("SLACK_APP_TOKEN"), // xapp-… (Socket Mode app-level token)
371
+ "apps.connections.open",
372
+ {},
373
+ "Slack apps.connections.open",
374
+ ),
375
+ );
376
+ }
@@ -1,26 +1,11 @@
1
1
  import {
2
- asRecord,
3
2
  makeQueryFilter,
3
+ recordFieldsFromKeys,
4
4
  type SearchMatchOptions,
5
5
  } from "../../../shared/search-filter.ts";
6
6
 
7
7
  export type SnowflakeSearchMatchOptions = SearchMatchOptions;
8
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);
9
+ export const filterSnowflakeTables = makeQueryFilter(
10
+ recordFieldsFromKeys(["table_name", "schema_name", "database_name"]),
11
+ );
@@ -1,28 +1,19 @@
1
1
  import { z } from "zod";
2
2
  import { type ConsentServer, createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
3
+ import { cursorListInputSchema } from "../../../shared/cursor-list-tool.ts";
4
+ import { requiredEnv } from "../../../shared/env-json-api.ts";
3
5
  import { searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
4
6
  import { fetchWithTimeout, mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
5
7
  import {
6
8
  runReadOnlyMcpConnector,
7
9
  type ZodToolRegistrar,
8
10
  } from "../../../shared/run-read-only-mcp-connector.ts";
11
+ import { asRecord } from "../../../shared/search-filter.ts";
9
12
  import { filterSnowflakeTables } from "./search-filter.ts";
10
13
 
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
14
  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
15
  return {
25
- Authorization: `Bearer ${t}`,
16
+ Authorization: `Bearer ${requiredEnv("SNOWFLAKE_TOKEN")}`,
26
17
  "Content-Type": "application/json",
27
18
  Accept: "application/json",
28
19
  };
@@ -34,7 +25,7 @@ function authHeader(): Record<string, string> {
34
25
  * the read-only `fetchTables` path and the HITL-gated write tools so auth/transport stays in one place.
35
26
  */
36
27
  async function executeStatement(statement: string): Promise<unknown> {
37
- const url = `https://${snowflakeAccount()}.snowflakecomputing.com/api/v2/statements`;
28
+ const url = `https://${requiredEnv("SNOWFLAKE_ACCOUNT")}.snowflakecomputing.com/api/v2/statements`;
38
29
  const res = await fetchWithTimeout(url, {
39
30
  method: "POST",
40
31
  headers: authHeader(),
@@ -68,13 +59,6 @@ const TABLES_SQL =
68
59
  "SELECT table_catalog AS database_name, table_schema AS schema_name, table_name, " +
69
60
  "row_count, last_altered FROM information_schema.tables WHERE table_schema <> 'INFORMATION_SCHEMA'";
70
61
 
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
62
  function rowsFromStatementsResponse(parsed: unknown): Record<string, unknown>[] {
79
63
  const root = asRecord(parsed);
80
64
  const meta = asRecord(root?.["resultSetMetaData"]);
@@ -141,10 +125,7 @@ export function registerSnowflakeTools(reg: ZodToolRegistrar, server: unknown):
141
125
  reg(
142
126
  "snowflake_list",
143
127
  "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
- }),
128
+ cursorListInputSchema(),
148
129
  async (p) => {
149
130
  const limit = p.limit ?? 200;
150
131
  const offset =
@@ -1,144 +1,6 @@
1
- import { z } from "zod";
2
- import { matchesResult } from "../../../shared/mcp-search-tool.ts";
3
- import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
4
1
  import { runReadOnlyMcpConnector } from "../../../shared/run-read-only-mcp-connector.ts";
5
- import { filterSnykAggregatedIssues } from "./search-filter.ts";
6
-
7
- const SNYK_API = "https://api.snyk.io";
8
-
9
- function authHeader(): Record<string, string> {
10
- const t = process.env["SNYK_TOKEN"]?.trim();
11
- if (t === undefined || t === "") {
12
- throw new Error("SNYK_TOKEN is not set");
13
- }
14
- return { Authorization: `token ${t}`, Accept: "application/json" };
15
- }
16
-
17
- async function snykGet(path: string): Promise<unknown> {
18
- const res = await fetch(`${SNYK_API}${path}`, { headers: authHeader() });
19
- const text = await res.text();
20
- if (!res.ok) {
21
- throw new Error(`Snyk ${String(res.status)}: ${text.slice(0, 400)}`);
22
- }
23
- return JSON.parse(text) as unknown;
24
- }
25
-
26
- async function snykPost(path: string, body: unknown): Promise<unknown> {
27
- const res = await fetch(`${SNYK_API}${path}`, {
28
- method: "POST",
29
- headers: { ...authHeader(), "Content-Type": "application/json" },
30
- body: JSON.stringify(body),
31
- });
32
- const text = await res.text();
33
- if (!res.ok) {
34
- throw new Error(`Snyk ${String(res.status)}: ${text.slice(0, 400)}`);
35
- }
36
- if (text.trim() === "") {
37
- return {};
38
- }
39
- return JSON.parse(text) as unknown;
40
- }
2
+ import { registerSnykTools } from "./tools.ts";
41
3
 
42
4
  await runReadOnlyMcpConnector("nimbus-snyk", (reg) => {
43
- reg(
44
- "snyk_list",
45
- "List Snyk projects for an org, or aggregated issues for a project. When projectId is omitted, returns the org's project list. Severities default to all four (critical/high/medium/low).",
46
- z.object({
47
- orgId: z.string().min(1),
48
- projectId: z.string().min(1).optional(),
49
- severities: z
50
- .array(z.enum(["critical", "high", "medium", "low"]))
51
- .min(1)
52
- .optional(),
53
- limit: z.number().int().min(1).max(1000).optional(),
54
- }),
55
- async (p) => {
56
- if (p.projectId === undefined) {
57
- return jsonResult(await snykGet(`/v1/org/${encodeURIComponent(p.orgId)}/projects`));
58
- }
59
- const severities = p.severities ?? ["critical", "high", "medium", "low"];
60
- const body = {
61
- filters: {
62
- severities,
63
- types: ["vuln", "license"],
64
- ignored: false,
65
- patched: false,
66
- },
67
- };
68
- const path = `/v1/org/${encodeURIComponent(p.orgId)}/project/${encodeURIComponent(p.projectId)}/aggregated-issues`;
69
- const data = await snykPost(path, body);
70
- if (p.limit !== undefined && data !== null && typeof data === "object") {
71
- const issues = (data as { issues?: unknown[] }).issues;
72
- if (Array.isArray(issues) && issues.length > p.limit) {
73
- return jsonResult({
74
- ...(data as Record<string, unknown>),
75
- issues: issues.slice(0, p.limit),
76
- });
77
- }
78
- }
79
- return jsonResult(data);
80
- },
81
- );
82
-
83
- reg(
84
- "snyk_get",
85
- "Fetch one Snyk issue by id from an org + project. Returns the aggregated-issue envelope for the matching `issue.id`; throws when no match is found.",
86
- z.object({
87
- orgId: z.string().min(1),
88
- projectId: z.string().min(1),
89
- issueId: z.string().min(1),
90
- }),
91
- async (p) => {
92
- const body = {
93
- filters: {
94
- severities: ["critical", "high", "medium", "low"],
95
- types: ["vuln", "license"],
96
- ignored: false,
97
- patched: false,
98
- },
99
- };
100
- const path = `/v1/org/${encodeURIComponent(p.orgId)}/project/${encodeURIComponent(p.projectId)}/aggregated-issues`;
101
- const root = await snykPost(path, body);
102
- const issues = (root as { issues?: unknown[] } | null)?.issues;
103
- if (!Array.isArray(issues)) {
104
- throw new TypeError(`Snyk: project ${p.projectId} returned no issues envelope`);
105
- }
106
- const match = issues.find((it) => {
107
- if (it === null || typeof it !== "object") {
108
- return false;
109
- }
110
- const id = (it as { id?: unknown }).id;
111
- return typeof id === "string" && id === p.issueId;
112
- });
113
- if (match === undefined) {
114
- throw new Error(`Snyk: issue ${p.issueId} not found in project ${p.projectId}`);
115
- }
116
- return jsonResult(match);
117
- },
118
- );
119
-
120
- reg(
121
- "snyk_search",
122
- "Substring search across a project's aggregated issues. Matches the query against `issueData.title`, `issueData.cve`, and `pkgName` (case-insensitive). Returns a `{ matches: [...] }` envelope.",
123
- z.object({
124
- orgId: z.string().min(1),
125
- projectId: z.string().min(1),
126
- query: z.string().min(1),
127
- limit: z.number().int().min(1).max(200).optional(),
128
- }),
129
- async (p) => {
130
- const body = {
131
- filters: {
132
- severities: ["critical", "high", "medium", "low"],
133
- types: ["vuln", "license"],
134
- ignored: false,
135
- patched: false,
136
- },
137
- };
138
- const path = `/v1/org/${encodeURIComponent(p.orgId)}/project/${encodeURIComponent(p.projectId)}/aggregated-issues`;
139
- const root = await snykPost(path, body);
140
- const issues = (root as { issues?: unknown[] } | null)?.issues;
141
- return matchesResult(issues, filterSnykAggregatedIssues, p);
142
- },
143
- );
5
+ registerSnykTools(reg);
144
6
  });