@nimbus-dev/connectors 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +18 -4
  2. package/connectors/airflow/src/search-filter.ts +2 -15
  3. package/connectors/airflow/src/server.ts +2 -87
  4. package/connectors/airflow/src/tools.ts +74 -0
  5. package/connectors/apple/src/adapters.ts +326 -0
  6. package/connectors/apple/src/apple-mail-core.ts +4 -8
  7. package/connectors/apple/src/calendar-tools.ts +28 -8
  8. package/connectors/apple/src/server.ts +23 -512
  9. package/connectors/apple/src/tools.ts +18 -44
  10. package/connectors/argocd/src/server.ts +25 -38
  11. package/connectors/athena/src/tools.ts +15 -34
  12. package/connectors/aws/src/server.ts +2 -183
  13. package/connectors/aws/src/tools.ts +197 -0
  14. package/connectors/azure/src/server.ts +2 -131
  15. package/connectors/azure/src/tools.ts +133 -0
  16. package/connectors/bigeye/src/search-filter.ts +4 -15
  17. package/connectors/bigeye/src/server.ts +38 -38
  18. package/connectors/bigquery/src/tools.ts +1 -26
  19. package/connectors/bitbucket/src/server.ts +2 -244
  20. package/connectors/bitbucket/src/tools.ts +241 -0
  21. package/connectors/bitrise/src/server.ts +2 -79
  22. package/connectors/bitrise/src/tools.ts +76 -0
  23. package/connectors/canva/src/search-filter.ts +2 -11
  24. package/connectors/canva/src/server.ts +2 -73
  25. package/connectors/canva/src/tools.ts +66 -0
  26. package/connectors/circleci/src/server.ts +2 -176
  27. package/connectors/circleci/src/tools.ts +193 -0
  28. package/connectors/cloud-logging/src/tools.ts +14 -27
  29. package/connectors/cloudwatch/src/tools.ts +15 -34
  30. package/connectors/codemagic/src/server.ts +2 -70
  31. package/connectors/codemagic/src/tools.ts +71 -0
  32. package/connectors/confluence/src/server.ts +2 -301
  33. package/connectors/confluence/src/tools.ts +320 -0
  34. package/connectors/dagster/src/server.ts +2 -214
  35. package/connectors/dagster/src/tools.ts +187 -0
  36. package/connectors/databricks/src/search-filter.ts +7 -7
  37. package/connectors/databricks/src/server.ts +2 -77
  38. package/connectors/databricks/src/tools.ts +66 -0
  39. package/connectors/datadog/src/server.ts +2 -48
  40. package/connectors/datadog/src/tools.ts +48 -0
  41. package/connectors/dataprofile/src/profile.ts +16 -46
  42. package/connectors/dbt/src/server.ts +2 -92
  43. package/connectors/dbt/src/tools.ts +80 -0
  44. package/connectors/dependencytrack/src/server.ts +2 -76
  45. package/connectors/dependencytrack/src/tools.ts +65 -0
  46. package/connectors/discord/src/server.ts +2 -85
  47. package/connectors/discord/src/tools.ts +82 -0
  48. package/connectors/elasticsearch/src/tools.ts +7 -37
  49. package/connectors/fastmail/src/jmap-client.ts +202 -0
  50. package/connectors/fastmail/src/jmap-core.ts +8 -21
  51. package/connectors/fastmail/src/server.ts +2 -187
  52. package/connectors/fastmail/src/tools.ts +4 -23
  53. package/connectors/figma/src/search-filter.ts +2 -11
  54. package/connectors/figma/src/server.ts +2 -113
  55. package/connectors/figma/src/tools.ts +99 -0
  56. package/connectors/firebase/src/server.ts +2 -90
  57. package/connectors/firebase/src/token.ts +2 -5
  58. package/connectors/firebase/src/tools.ts +106 -0
  59. package/connectors/flagsmith/src/server.ts +2 -98
  60. package/connectors/flagsmith/src/tools.ts +95 -0
  61. package/connectors/flux/src/server.ts +52 -53
  62. package/connectors/gcp/src/server.ts +2 -132
  63. package/connectors/gcp/src/tools.ts +123 -0
  64. package/connectors/github/src/server.ts +2 -328
  65. package/connectors/github/src/tools.ts +311 -0
  66. package/connectors/github-actions/src/server.ts +2 -205
  67. package/connectors/github-actions/src/tools.ts +209 -0
  68. package/connectors/gitlab/src/server.ts +2 -384
  69. package/connectors/gitlab/src/tools.ts +392 -0
  70. package/connectors/gmail/src/server.ts +2 -270
  71. package/connectors/gmail/src/tools.ts +209 -0
  72. package/connectors/google-drive/src/server.ts +2 -524
  73. package/connectors/google-drive/src/tools.ts +529 -0
  74. package/connectors/google-meet/src/server.ts +2 -82
  75. package/connectors/google-meet/src/tools.ts +87 -0
  76. package/connectors/google-photos/src/server.ts +2 -132
  77. package/connectors/google-photos/src/tools.ts +138 -0
  78. package/connectors/grafana/src/server.ts +2 -53
  79. package/connectors/grafana/src/tools.ts +53 -0
  80. package/connectors/great-expectations/src/gx-parse.ts +14 -45
  81. package/connectors/greenhouse/src/server.ts +2 -63
  82. package/connectors/greenhouse/src/tools.ts +68 -0
  83. package/connectors/hubspot/src/server.ts +2 -77
  84. package/connectors/hubspot/src/tools.ts +70 -0
  85. package/connectors/iac/src/server.ts +2 -137
  86. package/connectors/iac/src/tools.ts +152 -0
  87. package/connectors/imap/src/imap-core.ts +29 -82
  88. package/connectors/imap/src/server.ts +33 -220
  89. package/connectors/intercom/src/server.ts +2 -61
  90. package/connectors/intercom/src/tools.ts +58 -0
  91. package/connectors/jenkins/src/jenkins-api.ts +2 -6
  92. package/connectors/jenkins/src/server.ts +2 -215
  93. package/connectors/jenkins/src/tools.ts +240 -0
  94. package/connectors/jira/src/server.ts +2 -301
  95. package/connectors/jira/src/tools.ts +325 -0
  96. package/connectors/kubernetes/src/server.ts +2 -155
  97. package/connectors/kubernetes/src/tools.ts +159 -0
  98. package/connectors/launchdarkly/src/server.ts +2 -75
  99. package/connectors/launchdarkly/src/tools.ts +72 -0
  100. package/connectors/lever/src/search-filter.ts +4 -12
  101. package/connectors/lever/src/server.ts +2 -60
  102. package/connectors/lever/src/tools.ts +61 -0
  103. package/connectors/linear/src/server.ts +2 -459
  104. package/connectors/linear/src/tools.ts +482 -0
  105. package/connectors/localdb/src/sql-scan.ts +13 -41
  106. package/connectors/localdb/src/tools.ts +1 -1
  107. package/connectors/looker/src/search-filter.ts +2 -15
  108. package/connectors/looker/src/server.ts +35 -70
  109. package/connectors/mendeley/src/server.ts +2 -55
  110. package/connectors/mendeley/src/tools.ts +54 -0
  111. package/connectors/mercury/src/server.ts +2 -57
  112. package/connectors/mercury/src/tools.ts +50 -0
  113. package/connectors/metabase/src/search-filter.ts +4 -15
  114. package/connectors/metabase/src/server.ts +2 -79
  115. package/connectors/metabase/src/tools.ts +64 -0
  116. package/connectors/miro/src/server.ts +2 -72
  117. package/connectors/miro/src/tools.ts +65 -0
  118. package/connectors/mlflow/src/search-filter.ts +3 -7
  119. package/connectors/mlflow/src/server.ts +20 -40
  120. package/connectors/monte-carlo/src/server.ts +50 -57
  121. package/connectors/netlify/src/search-filter.ts +6 -14
  122. package/connectors/netlify/src/server.ts +2 -60
  123. package/connectors/netlify/src/tools.ts +53 -0
  124. package/connectors/newrelic/src/server.ts +2 -40
  125. package/connectors/newrelic/src/tools.ts +37 -0
  126. package/connectors/notion/src/server.ts +2 -335
  127. package/connectors/notion/src/tools.ts +344 -0
  128. package/connectors/obsidian/src/server.ts +2 -425
  129. package/connectors/obsidian/src/tools.ts +466 -0
  130. package/connectors/onedrive/src/server.ts +2 -249
  131. package/connectors/onedrive/src/tools.ts +253 -0
  132. package/connectors/outlook/src/server.ts +2 -351
  133. package/connectors/outlook/src/tools.ts +357 -0
  134. package/connectors/pagerduty/src/server.ts +2 -132
  135. package/connectors/pagerduty/src/tools.ts +126 -0
  136. package/connectors/pipedrive/src/server.ts +2 -58
  137. package/connectors/pipedrive/src/tools.ts +63 -0
  138. package/connectors/powerbi/src/search-filter.ts +2 -15
  139. package/connectors/powerbi/src/server.ts +43 -68
  140. package/connectors/prefect/src/search-filter.ts +3 -16
  141. package/connectors/prefect/src/server.ts +2 -90
  142. package/connectors/prefect/src/tools.ts +83 -0
  143. package/connectors/protonmail/src/mail-core.ts +16 -75
  144. package/connectors/protonmail/src/server.ts +34 -214
  145. package/connectors/raindrop/src/server.ts +2 -103
  146. package/connectors/raindrop/src/tools.ts +103 -0
  147. package/connectors/ramp/src/server.ts +2 -105
  148. package/connectors/ramp/src/tools.ts +98 -0
  149. package/connectors/readwise/src/server.ts +2 -88
  150. package/connectors/readwise/src/tools.ts +81 -0
  151. package/connectors/sagemaker/src/tools.ts +19 -38
  152. package/connectors/salesforce/src/search-filter.ts +4 -11
  153. package/connectors/salesforce/src/server.ts +2 -90
  154. package/connectors/salesforce/src/tools.ts +75 -0
  155. package/connectors/semgrep/src/search-filter.ts +3 -10
  156. package/connectors/semgrep/src/server.ts +2 -104
  157. package/connectors/semgrep/src/tools.ts +92 -0
  158. package/connectors/sentry/src/server.ts +2 -58
  159. package/connectors/sentry/src/tools.ts +53 -0
  160. package/connectors/slack/src/server.ts +2 -352
  161. package/connectors/slack/src/tools.ts +376 -0
  162. package/connectors/snowflake/src/search-filter.ts +4 -19
  163. package/connectors/snowflake/src/server.ts +6 -25
  164. package/connectors/snyk/src/server.ts +2 -140
  165. package/connectors/snyk/src/tools.ts +136 -0
  166. package/connectors/sonarqube/src/server.ts +2 -111
  167. package/connectors/sonarqube/src/tools.ts +98 -0
  168. package/connectors/stackoverflow/src/server.ts +2 -77
  169. package/connectors/stackoverflow/src/tools.ts +66 -0
  170. package/connectors/storybook/src/storybook-parse.ts +3 -6
  171. package/connectors/stripe/src/server.ts +2 -61
  172. package/connectors/stripe/src/tools.ts +54 -0
  173. package/connectors/superset/src/search-filter.ts +4 -15
  174. package/connectors/superset/src/server.ts +2 -115
  175. package/connectors/superset/src/tools.ts +98 -0
  176. package/connectors/tableau/src/search-filter.ts +2 -15
  177. package/connectors/tableau/src/server.ts +13 -65
  178. package/connectors/teams/src/server.ts +2 -249
  179. package/connectors/teams/src/tools.ts +283 -0
  180. package/connectors/testflight/src/server.ts +2 -70
  181. package/connectors/testflight/src/tools.ts +79 -0
  182. package/connectors/vercel/src/search-filter.ts +2 -10
  183. package/connectors/vercel/src/server.ts +2 -75
  184. package/connectors/vercel/src/tools.ts +73 -0
  185. package/connectors/vertex-ai/src/tools.ts +23 -33
  186. package/connectors/wiz/src/search-filter.ts +6 -30
  187. package/connectors/wiz/src/server.ts +2 -188
  188. package/connectors/wiz/src/tools.ts +168 -0
  189. package/connectors/zendesk/src/server.ts +2 -73
  190. package/connectors/zendesk/src/tools.ts +60 -0
  191. package/connectors/zoom/src/search-filter.ts +1 -7
  192. package/connectors/zoom/src/server.ts +2 -94
  193. package/connectors/zoom/src/tools.ts +93 -0
  194. package/connectors/zotero/src/server.ts +2 -78
  195. package/connectors/zotero/src/tools.ts +67 -0
  196. package/package.json +15 -12
  197. package/shared/access-token-cache.ts +123 -0
  198. package/shared/atlassian-json-fetch.ts +6 -16
  199. package/shared/audit-chain.ts +13 -6
  200. package/shared/cli-json-kit.ts +150 -0
  201. package/shared/collection-tool-kit.ts +143 -0
  202. package/shared/connector-mode.ts +3 -2
  203. package/shared/cursor-list-tool.ts +29 -0
  204. package/shared/env-json-api.ts +209 -0
  205. package/shared/fetch-json-text.ts +27 -0
  206. package/shared/github-rest.ts +62 -0
  207. package/shared/graphql-json.ts +60 -0
  208. package/shared/imap-mail-core.ts +91 -0
  209. package/shared/imap-tool-kit.ts +57 -23
  210. package/shared/imapflow-adapter.ts +417 -0
  211. package/shared/rest-tool-kit.ts +77 -7
  212. package/shared/run-cli-json.ts +38 -16
  213. package/shared/search-filter.ts +58 -18
  214. package/shared/walk-files.ts +118 -0
  215. package/standalone/src/launcher.ts +19 -7
@@ -0,0 +1,392 @@
1
+ import { z } from "zod";
2
+ import {
3
+ type ConsentServer,
4
+ createWriteToolRegistrar,
5
+ type WriteToolConfig,
6
+ } from "../../../shared/consent-kit.ts";
7
+ import { optionalBaseUrl } from "../../../shared/env-json-api.ts";
8
+ import {
9
+ createRegisterSimpleTool,
10
+ createZodToolRegistrar,
11
+ mcpJsonResult as jsonResult,
12
+ mcpJsonResultIfOk,
13
+ requireProcessEnv,
14
+ type ZodObjectSchema,
15
+ } from "../../../shared/mcp-tool-kit.ts";
16
+ import {
17
+ makeRestToolRegistrar,
18
+ makeRestWriteToolRegistrar,
19
+ type RestFetchResult,
20
+ toRestFetchResult,
21
+ } from "../../../shared/rest-tool-kit.ts";
22
+
23
+ function apiBase(): string {
24
+ return optionalBaseUrl("GITLAB_API_BASE_URL", "https://gitlab.com/api/v4");
25
+ }
26
+
27
+ async function glFetch(token: string, path: string, init?: RequestInit): Promise<RestFetchResult> {
28
+ const base = apiBase();
29
+ const relativePath = path.startsWith("/") ? path : `/${path}`;
30
+ const url = path.startsWith("http") ? path : `${base}${relativePath}`;
31
+ const baseHeaders: Record<string, string> = { "PRIVATE-TOKEN": token };
32
+ const mergedHeaders =
33
+ init?.headers === undefined
34
+ ? baseHeaders
35
+ : { ...baseHeaders, ...(init.headers as Record<string, string>) };
36
+ const res = await fetch(url, {
37
+ ...init,
38
+ headers: mergedHeaders,
39
+ });
40
+ return toRestFetchResult(res);
41
+ }
42
+
43
+ /**
44
+ * A list endpoint's URL with the query `setQuery` sets, parameters in that order. ABSOLUTE on
45
+ * purpose: it already carries apiBase()'s `/api/v4`, and a relative path would let glFetch
46
+ * re-prefix apiBase() → `/api/v4/api/v4/…`.
47
+ */
48
+ function listUrl(path: string, setQuery: (q: URLSearchParams) => void): string {
49
+ const u = new URL(`${apiBase()}${path}`);
50
+ setQuery(u.searchParams);
51
+ return u.toString();
52
+ }
53
+
54
+ /** Paging as every list tool here sends it: `per_page` (default 30), then `page` when given. */
55
+ function setPaging(
56
+ q: URLSearchParams,
57
+ paging: { readonly perPage?: number | undefined; readonly page?: number | undefined },
58
+ ): void {
59
+ q.set("per_page", String(paging.perPage ?? 30));
60
+ if (paging.page !== undefined) {
61
+ q.set("page", String(paging.page));
62
+ }
63
+ }
64
+
65
+ /** Merge requests and issues take one list query: a state filter (default opened), then paging. */
66
+ const stateListUrl =
67
+ (collection: "merge_requests" | "issues") =>
68
+ (parsed: {
69
+ readonly projectPath: string;
70
+ readonly state?: string | undefined;
71
+ readonly perPage?: number | undefined;
72
+ readonly page?: number | undefined;
73
+ }): string =>
74
+ listUrl(`/projects/${encodeURIComponent(parsed.projectPath)}/${collection}`, (q) => {
75
+ q.set("state", parsed.state ?? "opened");
76
+ setPaging(q, parsed);
77
+ });
78
+
79
+ /** A CI job's whole plain-text trace; throws, quoting GitLab, on a non-ok status. */
80
+ async function fetchJobTrace(parsed: {
81
+ readonly projectPath: string;
82
+ readonly jobId: number;
83
+ }): Promise<string> {
84
+ const token = requireProcessEnv("GITLAB_PAT");
85
+ const enc = encodeURIComponent(parsed.projectPath);
86
+ const url = `${apiBase()}/projects/${enc}/jobs/${String(parsed.jobId)}/trace`;
87
+ const res = await fetch(url, { headers: { "PRIVATE-TOKEN": token } });
88
+ const text = await res.text();
89
+ if (!res.ok) {
90
+ throw new Error(`GitLab ${String(res.status)}: ${text.slice(0, 300)}`);
91
+ }
92
+ return text;
93
+ }
94
+
95
+ /** Tool names exposed by this connector — for contract/introspection tests. */
96
+ export const GITLAB_TOOL_NAMES = [
97
+ "gitlab_project_list",
98
+ "gitlab_mr_list",
99
+ "gitlab_mr_get",
100
+ "gitlab_mr_merge",
101
+ "gitlab_issue_list",
102
+ "gitlab_issue_get",
103
+ "gitlab_pipeline_list",
104
+ "gitlab_pipeline_get",
105
+ "gitlab_pipeline_jobs_get",
106
+ "gitlab_job_trace",
107
+ "gitlab_job_log_tail",
108
+ "gitlab_pipeline_retry",
109
+ "gitlab_pipeline_cancel",
110
+ ] as const;
111
+
112
+ export function registerGitlabTools(
113
+ server: ConsentServer & { tool: (...args: never) => unknown },
114
+ ): void {
115
+ const registerSimpleTool = createRegisterSimpleTool(server);
116
+ const reg = createZodToolRegistrar(registerSimpleTool);
117
+
118
+ /**
119
+ * Every MUTATING gitlab tool goes through here. Outside the gateway this adds the
120
+ * consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
121
+ * the gateway it is a pass-through, because executor.ts (I2) is the gate there.
122
+ */
123
+ const registerWriteTool = createWriteToolRegistrar(server, {
124
+ connector: "gitlab",
125
+ scopeEnv: "NIMBUS_MCP_GITLAB_WRITE_SCOPE",
126
+ scopeKinds: ["repo"],
127
+ });
128
+
129
+ /**
130
+ * Standard GitLab tool, read or write: `token → glFetch(buildUrl[, buildInit]) →
131
+ * mcpJsonResultIfOk("GitLab", res)`. `buildUrl` returns the relative path (or absolute URL) and
132
+ * `buildInit` the optional fetch init (method/body). Tools with a non-standard tail (raw text
133
+ * trace, custom error text) stay hand-written below.
134
+ */
135
+ const gitlabRest = { tokenEnv: "GITLAB_PAT", serviceLabel: "GitLab", fetch: glFetch } as const;
136
+ const registerGitlabTool = makeRestToolRegistrar({ registrar: reg, ...gitlabRest });
137
+ const registerRestWriteTool = makeRestWriteToolRegistrar({ registerWriteTool, ...gitlabRest });
138
+
139
+ /**
140
+ * The write-tool equivalent of `registerGitlabTool`, routed through the write registrar.
141
+ * `scopeTargetOf` is supplied here rather than per tool — every GitLab mutation is scoped to one
142
+ * project — so a new write tool cannot forget it.
143
+ */
144
+ function registerGitlabWriteTool<T extends { projectPath: string }>(
145
+ name: string,
146
+ cfg: Omit<WriteToolConfig<T>, "scopeTargetOf">,
147
+ description: string,
148
+ schema: ZodObjectSchema<T>,
149
+ buildUrl: (p: T) => string,
150
+ buildInit?: (p: T) => RequestInit,
151
+ ): void {
152
+ registerRestWriteTool(
153
+ name,
154
+ { ...cfg, scopeTargetOf: (p) => ({ kind: "repo", value: p.projectPath }) },
155
+ description,
156
+ schema,
157
+ buildUrl,
158
+ buildInit,
159
+ );
160
+ }
161
+
162
+ const projectPathArg = z.object({
163
+ projectPath: z
164
+ .string()
165
+ .min(1)
166
+ .describe("URL-encoded path or numeric project id, e.g. group/repo"),
167
+ });
168
+
169
+ const gitlabProjectListSchema = z.object({
170
+ perPage: z.number().int().min(1).max(100).optional(),
171
+ page: z.number().int().min(1).optional(),
172
+ });
173
+
174
+ registerGitlabTool(
175
+ "gitlab_project_list",
176
+ "List projects visible to the authenticated user (membership).",
177
+ gitlabProjectListSchema,
178
+ (parsed) =>
179
+ listUrl("/projects", (q) => {
180
+ q.set("membership", "true");
181
+ q.set("order_by", "last_activity_at");
182
+ q.set("sort", "desc");
183
+ setPaging(q, parsed);
184
+ }),
185
+ );
186
+
187
+ const gitlabMrListSchema = projectPathArg.extend({
188
+ state: z.enum(["opened", "closed", "locked", "merged", "all"]).optional(),
189
+ perPage: z.number().int().min(1).max(100).optional(),
190
+ page: z.number().int().min(1).optional(),
191
+ });
192
+
193
+ registerGitlabTool(
194
+ "gitlab_mr_list",
195
+ "List merge requests for a project.",
196
+ gitlabMrListSchema,
197
+ stateListUrl("merge_requests"),
198
+ );
199
+
200
+ const gitlabMrGetSchema = projectPathArg.extend({
201
+ mergeRequestIid: z.number().int().min(1),
202
+ });
203
+
204
+ registerGitlabTool(
205
+ "gitlab_mr_get",
206
+ "Get a single merge request by IID.",
207
+ gitlabMrGetSchema,
208
+ (parsed) =>
209
+ `/projects/${encodeURIComponent(parsed.projectPath)}/merge_requests/${String(parsed.mergeRequestIid)}`,
210
+ );
211
+
212
+ const gitlabMrMergeSchema = projectPathArg.extend({
213
+ mergeRequestIid: z.number().int().min(1),
214
+ mergeCommitMessage: z.string().max(10_000).optional(),
215
+ squash: z.boolean().optional(),
216
+ shouldRemoveSourceBranch: z.boolean().optional(),
217
+ });
218
+
219
+ registerGitlabWriteTool(
220
+ "gitlab_mr_merge",
221
+ {
222
+ mutates: "gitlab.mr.merge",
223
+ recoverable: true,
224
+ },
225
+ "Merge a merge request.",
226
+ gitlabMrMergeSchema,
227
+ (parsed) =>
228
+ `/projects/${encodeURIComponent(parsed.projectPath)}/merge_requests/${String(parsed.mergeRequestIid)}/merge`,
229
+ (parsed) => {
230
+ const body: Record<string, unknown> = {};
231
+ if (parsed.mergeCommitMessage !== undefined) {
232
+ body["merge_commit_message"] = parsed.mergeCommitMessage;
233
+ }
234
+ if (parsed.squash !== undefined) {
235
+ body["squash"] = parsed.squash;
236
+ }
237
+ if (parsed.shouldRemoveSourceBranch !== undefined) {
238
+ body["should_remove_source_branch"] = parsed.shouldRemoveSourceBranch;
239
+ }
240
+ return {
241
+ method: "PUT",
242
+ headers: { "Content-Type": "application/json" },
243
+ body: JSON.stringify(body),
244
+ };
245
+ },
246
+ );
247
+
248
+ const gitlabIssueListSchema = projectPathArg.extend({
249
+ state: z.enum(["opened", "closed", "all"]).optional(),
250
+ perPage: z.number().int().min(1).max(100).optional(),
251
+ page: z.number().int().min(1).optional(),
252
+ });
253
+
254
+ registerGitlabTool(
255
+ "gitlab_issue_list",
256
+ "List issues for a project.",
257
+ gitlabIssueListSchema,
258
+ stateListUrl("issues"),
259
+ );
260
+
261
+ const gitlabIssueGetSchema = projectPathArg.extend({
262
+ issueIid: z.number().int().min(1),
263
+ });
264
+
265
+ registerGitlabTool(
266
+ "gitlab_issue_get",
267
+ "Get a single issue by IID.",
268
+ gitlabIssueGetSchema,
269
+ (parsed) =>
270
+ `/projects/${encodeURIComponent(parsed.projectPath)}/issues/${String(parsed.issueIid)}`,
271
+ );
272
+
273
+ const gitlabPipelineListSchema = projectPathArg.extend({
274
+ ref: z.string().max(500).optional(),
275
+ status: z.string().max(64).optional(),
276
+ perPage: z.number().int().min(1).max(100).optional(),
277
+ page: z.number().int().min(1).optional(),
278
+ });
279
+
280
+ registerGitlabTool(
281
+ "gitlab_pipeline_list",
282
+ "List CI pipelines for a project.",
283
+ gitlabPipelineListSchema,
284
+ (parsed) =>
285
+ listUrl(`/projects/${encodeURIComponent(parsed.projectPath)}/pipelines`, (q) => {
286
+ setPaging(q, parsed);
287
+ if (parsed.ref !== undefined) {
288
+ q.set("ref", parsed.ref);
289
+ }
290
+ if (parsed.status !== undefined) {
291
+ q.set("status", parsed.status);
292
+ }
293
+ }),
294
+ );
295
+
296
+ const gitlabPipelineGetSchema = projectPathArg.extend({
297
+ pipelineId: z.number().int().min(1),
298
+ });
299
+
300
+ registerGitlabTool(
301
+ "gitlab_pipeline_get",
302
+ "Get a single pipeline by id.",
303
+ gitlabPipelineGetSchema,
304
+ (parsed) =>
305
+ `/projects/${encodeURIComponent(parsed.projectPath)}/pipelines/${String(parsed.pipelineId)}`,
306
+ );
307
+
308
+ registerGitlabTool(
309
+ "gitlab_pipeline_jobs_get",
310
+ "List jobs for a CI pipeline (by pipeline id).",
311
+ gitlabPipelineGetSchema,
312
+ (parsed) =>
313
+ `/projects/${encodeURIComponent(parsed.projectPath)}/pipelines/${String(parsed.pipelineId)}/jobs`,
314
+ );
315
+
316
+ const gitlabJobTraceSchema = projectPathArg.extend({
317
+ jobId: z.number().int().min(1),
318
+ });
319
+
320
+ reg(
321
+ "gitlab_job_trace",
322
+ "Download plain-text trace for a CI job (by job id).",
323
+ gitlabJobTraceSchema,
324
+ async (parsed) => jsonResult({ trace: await fetchJobTrace(parsed) }),
325
+ );
326
+
327
+ reg(
328
+ "gitlab_job_log_tail",
329
+ "Download job trace text, optionally keeping only the last N characters (tail).",
330
+ gitlabJobTraceSchema.extend({
331
+ maxChars: z.number().int().min(1000).max(500_000).optional(),
332
+ }),
333
+ async (parsed) => {
334
+ const text = await fetchJobTrace(parsed);
335
+ const max = parsed.maxChars ?? 64_000;
336
+ const tail = text.length > max ? text.slice(-max) : text;
337
+ return jsonResult({
338
+ jobId: parsed.jobId,
339
+ truncated: text.length > max,
340
+ totalChars: text.length,
341
+ trace: tail,
342
+ });
343
+ },
344
+ );
345
+
346
+ /**
347
+ * Retry and cancel are one POST on a pipeline, differing only in its last path segment — which
348
+ * the failure message names — so they share everything but their name, action type and text.
349
+ */
350
+ function registerPipelineActionTool(
351
+ name: string,
352
+ mutates: string,
353
+ description: string,
354
+ action: "retry" | "cancel",
355
+ ): void {
356
+ registerWriteTool(
357
+ name,
358
+ {
359
+ mutates,
360
+ recoverable: true,
361
+ scopeTargetOf: (p) => ({ kind: "repo", value: p.projectPath }),
362
+ },
363
+ description,
364
+ gitlabPipelineGetSchema,
365
+ async (parsed) => {
366
+ const token = requireProcessEnv("GITLAB_PAT");
367
+ const enc = encodeURIComponent(parsed.projectPath);
368
+ const path = `/projects/${enc}/pipelines/${String(parsed.pipelineId)}/${action}`;
369
+ const res = await glFetch(token, path, { method: "POST" });
370
+ if (!res.ok) {
371
+ throw new Error(
372
+ `GitLab pipeline ${action} ${String(res.status)}: ${res.text.slice(0, 400)}`,
373
+ );
374
+ }
375
+ return mcpJsonResultIfOk("GitLab", res);
376
+ },
377
+ );
378
+ }
379
+
380
+ registerPipelineActionTool(
381
+ "gitlab_pipeline_retry",
382
+ "gitlab.pipeline.retry",
383
+ "Retry failed jobs in a pipeline.",
384
+ "retry",
385
+ );
386
+ registerPipelineActionTool(
387
+ "gitlab_pipeline_cancel",
388
+ "gitlab.pipeline.cancel",
389
+ "Cancel a pipeline.",
390
+ "cancel",
391
+ );
392
+ }
@@ -1,277 +1,9 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { z } from "zod";
4
- import { headerLine } from "../../../shared/header-safe.ts";
5
- import {
6
- createRegisterSimpleTool,
7
- createZodToolRegistrar,
8
- mcpJsonResultIfOk,
9
- requireProcessEnv,
10
- type ZodObjectSchema,
11
- } from "../../../shared/mcp-tool-kit.ts";
12
- import { makeRestFetcher, makeRestToolRegistrar } from "../../../shared/rest-tool-kit.ts";
13
-
14
- const GMAIL_BASE = "https://gmail.googleapis.com/gmail/v1/users/me";
15
-
16
- function gmailFetch(
17
- token: string,
18
- path: string,
19
- init?: RequestInit,
20
- ): Promise<{ ok: boolean; status: number; json: unknown; text: string }> {
21
- return makeRestFetcher({ apiBase: GMAIL_BASE, token })(path, init);
22
- }
23
-
24
- function buildRfc822Message(params: {
25
- to: string;
26
- subject: string;
27
- body: string;
28
- cc?: string;
29
- bcc?: string;
30
- }): string {
31
- const lines: string[] = [
32
- `To: ${params.to}`,
33
- ...(params.cc !== undefined && params.cc !== "" ? [`Cc: ${params.cc}`] : []),
34
- ...(params.bcc !== undefined && params.bcc !== "" ? [`Bcc: ${params.bcc}`] : []),
35
- `Subject: ${params.subject}`,
36
- "Content-Type: text/plain; charset=UTF-8",
37
- "",
38
- params.body,
39
- ];
40
- return lines.join("\r\n");
41
- }
42
-
43
- function toRawBase64Url(rfc822: string): string {
44
- return Buffer.from(rfc822, "utf-8").toString("base64url");
45
- }
3
+ import { registerGmailTools } from "./tools.ts";
46
4
 
47
5
  const server = new McpServer({ name: "nimbus-gmail", version: "0.1.0" });
48
6
 
49
- const reg = createZodToolRegistrar(createRegisterSimpleTool(server));
50
-
51
- /** Standard Gmail tool: token → gmailFetch(buildPath[, buildInit]) → mcpJsonResultIfOk("Gmail API", …, 200). */
52
- import { createWriteToolRegistrar, type WriteToolConfig } from "../../../shared/consent-kit.ts";
53
-
54
- /**
55
- * Every MUTATING gmail tool goes through here. Outside the gateway this adds the
56
- * consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
57
- * the gateway it is a pass-through, because executor.ts (I2) is the gate there.
58
- */
59
- const registerWriteTool = createWriteToolRegistrar(server, {
60
- connector: "gmail",
61
- scopeEnv: "NIMBUS_MCP_GMAIL_WRITE_SCOPE",
62
- scopeKinds: ["recipient", "draft"],
63
- });
64
-
65
- /**
66
- * The write-tool equivalent of `registerGmailTool`: identical fetch and result handling, routed
67
- * through the write registrar.
68
- */
69
- function registerGmailWriteTool<T>(
70
- name: string,
71
- cfg: WriteToolConfig<T>,
72
- description: string,
73
- schema: ZodObjectSchema<T>,
74
- buildPath: (p: T) => string,
75
- buildInit?: (p: T) => RequestInit,
76
- ): void {
77
- registerWriteTool(name, cfg, description, schema, async (parsed) => {
78
- const token = requireProcessEnv("GOOGLE_OAUTH_ACCESS_TOKEN");
79
- const res = await gmailFetch(token, buildPath(parsed), buildInit?.(parsed));
80
- return mcpJsonResultIfOk("Gmail API", res, 200);
81
- });
82
- }
83
-
84
- const registerGmailTool = makeRestToolRegistrar({
85
- registrar: reg,
86
- tokenEnv: "GOOGLE_OAUTH_ACCESS_TOKEN",
87
- serviceLabel: "Gmail API",
88
- fetch: gmailFetch,
89
- snippetMax: 200,
90
- });
91
-
92
- const gmailMessageListArgs = z.object({
93
- maxResults: z.number().int().min(1).max(100).optional(),
94
- pageToken: z.string().optional(),
95
- q: z.string().max(500).optional(),
96
- labelIds: z.array(z.string()).optional(),
97
- includeSpamTrash: z.boolean().optional(),
98
- });
99
-
100
- registerGmailTool(
101
- "gmail_message_list",
102
- "List Gmail message ids (metadata). Optional Gmail search query `q` (same syntax as Gmail UI).",
103
- gmailMessageListArgs,
104
- (data) => {
105
- const u = new URL(`${GMAIL_BASE}/messages`);
106
- u.searchParams.set("maxResults", String(data.maxResults ?? 25));
107
- if (data.pageToken !== undefined && data.pageToken !== "") {
108
- u.searchParams.set("pageToken", data.pageToken);
109
- }
110
- if (data.q !== undefined && data.q !== "") {
111
- u.searchParams.set("q", data.q);
112
- }
113
- if (data.labelIds !== undefined) {
114
- for (const lid of data.labelIds) {
115
- u.searchParams.append("labelIds", lid);
116
- }
117
- }
118
- if (data.includeSpamTrash === true) {
119
- u.searchParams.set("includeSpamTrash", "true");
120
- }
121
- return u.toString();
122
- },
123
- );
124
-
125
- const gmailMessageReadArgs = z.object({
126
- messageId: z.string().min(1),
127
- format: z.enum(["minimal", "full", "metadata", "raw"]).optional(),
128
- });
129
-
130
- registerGmailTool(
131
- "gmail_message_read",
132
- "Read a single Gmail message (format minimal | metadata | full | raw).",
133
- gmailMessageReadArgs,
134
- (data) => {
135
- const fmt = data.format ?? "metadata";
136
- const u = new URL(`${GMAIL_BASE}/messages/${encodeURIComponent(data.messageId)}`);
137
- u.searchParams.set("format", fmt);
138
- if (fmt === "metadata") {
139
- u.searchParams.append("metadataHeaders", "Subject");
140
- u.searchParams.append("metadataHeaders", "From");
141
- u.searchParams.append("metadataHeaders", "To");
142
- u.searchParams.append("metadataHeaders", "Date");
143
- }
144
- return u.toString();
145
- },
146
- );
147
-
148
- const gmailThreadReadArgs = z.object({
149
- threadId: z.string().min(1),
150
- format: z.enum(["minimal", "full", "metadata"]).optional(),
151
- });
152
-
153
- registerGmailTool(
154
- "gmail_thread_read",
155
- "Read a Gmail thread and its messages.",
156
- gmailThreadReadArgs,
157
- (data) => {
158
- const fmt = data.format ?? "metadata";
159
- const u = new URL(`${GMAIL_BASE}/threads/${encodeURIComponent(data.threadId)}`);
160
- u.searchParams.set("format", fmt);
161
- return u.toString();
162
- },
163
- );
164
-
165
- const gmailLabelListArgs = z.object({});
166
-
167
- registerGmailTool(
168
- "gmail_label_list",
169
- "List all Gmail labels.",
170
- gmailLabelListArgs,
171
- () => `${GMAIL_BASE}/labels`,
172
- );
173
-
174
- const gmailDraftCreateArgs = z.object({
175
- to: headerLine({ min: 1 }),
176
- subject: headerLine({ min: 1, max: 998 }),
177
- body: z.string().max(1_000_000),
178
- cc: headerLine().optional(),
179
- bcc: headerLine().optional(),
180
- });
181
-
182
- registerGmailWriteTool(
183
- "gmail_draft_create",
184
- {
185
- mutates: "gmail.draft.create",
186
- recoverable: true,
187
- scopeTargetOf: (p) => ({ kind: "recipient", value: p.to }),
188
- },
189
- "Create a Gmail draft. Requires Gateway HITL email.draft.create.",
190
- gmailDraftCreateArgs,
191
- () => `${GMAIL_BASE}/drafts`,
192
- (data) => {
193
- const msgParams: { to: string; subject: string; body: string; cc?: string; bcc?: string } = {
194
- to: data.to,
195
- subject: data.subject,
196
- body: data.body,
197
- };
198
- if (data.cc !== undefined && data.cc !== "") {
199
- msgParams.cc = data.cc;
200
- }
201
- if (data.bcc !== undefined && data.bcc !== "") {
202
- msgParams.bcc = data.bcc;
203
- }
204
- const raw = toRawBase64Url(buildRfc822Message(msgParams));
205
- return {
206
- method: "POST",
207
- headers: { "Content-Type": "application/json" },
208
- body: JSON.stringify({ message: { raw } }),
209
- };
210
- },
211
- );
212
-
213
- const gmailDraftSendArgs = z.object({
214
- draftId: z.string().min(1),
215
- });
216
-
217
- registerGmailWriteTool(
218
- "gmail_draft_send",
219
- {
220
- mutates: "gmail.draft.send",
221
- recoverable: false,
222
- capturePreState: (p) => Promise.resolve({ draftId: p.draftId }),
223
- scopeTargetOf: (p) => ({ kind: "draft", value: p.draftId }),
224
- },
225
- "Send an existing Gmail draft by id. Requires Gateway HITL email.draft.send.",
226
- gmailDraftSendArgs,
227
- () => `${GMAIL_BASE}/drafts/send`,
228
- (data) => ({
229
- method: "POST",
230
- headers: { "Content-Type": "application/json" },
231
- body: JSON.stringify({ id: data.draftId }),
232
- }),
233
- );
234
-
235
- const gmailMessageSendArgs = z.object({
236
- to: headerLine({ min: 1 }),
237
- subject: headerLine({ min: 1, max: 998 }),
238
- body: z.string().max(1_000_000),
239
- cc: headerLine().optional(),
240
- bcc: headerLine().optional(),
241
- });
242
-
243
- registerGmailWriteTool(
244
- "gmail_message_send",
245
- {
246
- mutates: "gmail.message.send",
247
- recoverable: false,
248
- // A sent mail cannot be recalled and nothing remains to query, so the recipient and subject
249
- // ARE the pre-state.
250
- capturePreState: (p) => Promise.resolve({ to: p.to, subject: p.subject }),
251
- scopeTargetOf: (p) => ({ kind: "recipient", value: p.to }),
252
- },
253
- "Send a new Gmail message (not a draft). Requires Gateway HITL email.send.",
254
- gmailMessageSendArgs,
255
- () => `${GMAIL_BASE}/messages/send`,
256
- (data) => {
257
- const sendParams: { to: string; subject: string; body: string; cc?: string; bcc?: string } = {
258
- to: data.to,
259
- subject: data.subject,
260
- body: data.body,
261
- };
262
- if (data.cc !== undefined && data.cc !== "") {
263
- sendParams.cc = data.cc;
264
- }
265
- if (data.bcc !== undefined && data.bcc !== "") {
266
- sendParams.bcc = data.bcc;
267
- }
268
- const raw = toRawBase64Url(buildRfc822Message(sendParams));
269
- return {
270
- method: "POST",
271
- headers: { "Content-Type": "application/json" },
272
- body: JSON.stringify({ raw }),
273
- };
274
- },
275
- );
7
+ registerGmailTools(server);
276
8
 
277
9
  await server.connect(new StdioServerTransport());