@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
@@ -1,77 +1,6 @@
1
- import { z } from "zod";
2
- import { searchToolInputSchema } 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 { filterCanvaDesigns } from "./search-filter.ts";
6
-
7
- const BASE = "https://api.canva.com";
8
-
9
- function apiToken(): string {
10
- const t = process.env["CANVA_TOKEN"]?.trim();
11
- if (t === undefined || t === "") {
12
- throw new Error("CANVA_TOKEN is not set");
13
- }
14
- return t;
15
- }
16
-
17
- function authHeader(): Record<string, string> {
18
- return { Authorization: `Bearer ${apiToken()}`, Accept: "application/json" };
19
- }
20
-
21
- async function canvaGet(path: string): Promise<unknown> {
22
- const res = await fetch(`${BASE}${path}`, { headers: authHeader() });
23
- const text = await res.text();
24
- if (!res.ok) {
25
- throw new Error(`Canva ${String(res.status)}: ${text.slice(0, 400)}`);
26
- }
27
- return JSON.parse(text) as unknown;
28
- }
29
-
30
- function designsFrom(root: unknown): unknown[] {
31
- const items = (root as { items?: unknown } | null)?.items;
32
- return Array.isArray(items) ? items : [];
33
- }
2
+ import { registerCanvaTools } from "./tools.ts";
34
3
 
35
4
  await runReadOnlyMcpConnector("nimbus-canva", (reg) => {
36
- reg(
37
- "canva_list",
38
- "List the authenticated user's Canva designs (GET /rest/v1/designs?continuation=<token>). Returns the { items: [...], continuation? } envelope — `items` holds the design objects, each shaped { id, title, created_at, updated_at, urls: { edit_url, view_url }, thumbnail: { url, width, height }, ... }; `continuation` (when present) is the opaque token for the next page.",
39
- z.object({
40
- continuation: z.string().optional(),
41
- }),
42
- async (p) => {
43
- const params = new URLSearchParams();
44
- if (p.continuation !== undefined && p.continuation !== "") {
45
- params.set("continuation", p.continuation);
46
- }
47
- const qs = params.toString();
48
- const query = qs === "" ? "" : `?${qs}`;
49
- return jsonResult(await canvaGet(`/rest/v1/designs${query}`));
50
- },
51
- );
52
-
53
- reg(
54
- "canva_get",
55
- "Fetch one Canva design by its id (GET /rest/v1/designs/{designId}). Returns the { design: { id, title, created_at, updated_at, urls, thumbnail, ... } } envelope. Throws when no design with that id exists.",
56
- z.object({
57
- id: z.string().min(1),
58
- }),
59
- async (p) => {
60
- return jsonResult(await canvaGet(`/rest/v1/designs/${encodeURIComponent(p.id)}`));
61
- },
62
- );
63
-
64
- reg(
65
- "canva_search",
66
- "**Substring search over the FIRST PAGE only** of the authenticated user's Canva designs. This tool fetches GET /rest/v1/designs once and matches the query locally (case-insensitive) against the design title. **Designs beyond the first page are not searchable here — query the local Nimbus index instead for full coverage.** Returns a { matches: [...] } envelope.",
67
- searchToolInputSchema(100),
68
- async (p) => {
69
- const root = await canvaGet("/rest/v1/designs");
70
- const matches = filterCanvaDesigns(designsFrom(root), {
71
- query: p.query,
72
- limit: p.limit,
73
- });
74
- return jsonResult({ matches });
75
- },
76
- );
5
+ registerCanvaTools(reg);
77
6
  });
@@ -0,0 +1,66 @@
1
+ import { z } from "zod";
2
+ import { createJsonGetter, envAuthHeaders } from "../../../shared/env-json-api.ts";
3
+ import { searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
4
+ import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
5
+ import type { ZodToolRegistrar } from "../../../shared/run-read-only-mcp-connector.ts";
6
+ import { filterCanvaDesigns } from "./search-filter.ts";
7
+
8
+ const BASE = "https://api.canva.com";
9
+
10
+ const canvaGet = createJsonGetter({
11
+ base: BASE,
12
+ label: "Canva",
13
+ headers: envAuthHeaders({ env: "CANVA_TOKEN" }),
14
+ });
15
+
16
+ function designsFrom(root: unknown): unknown[] {
17
+ const items = (root as { items?: unknown } | null)?.items;
18
+ return Array.isArray(items) ? items : [];
19
+ }
20
+
21
+ /** Tool names exposed by this connector — for contract/introspection tests. */
22
+ export const CANVA_TOOL_NAMES = ["canva_get", "canva_list", "canva_search"] as const;
23
+
24
+ export function registerCanvaTools(reg: ZodToolRegistrar): void {
25
+ reg(
26
+ "canva_list",
27
+ "List the authenticated user's Canva designs (GET /rest/v1/designs?continuation=<token>). Returns the { items: [...], continuation? } envelope — `items` holds the design objects, each shaped { id, title, created_at, updated_at, urls: { edit_url, view_url }, thumbnail: { url, width, height }, ... }; `continuation` (when present) is the opaque token for the next page.",
28
+ z.object({
29
+ continuation: z.string().optional(),
30
+ }),
31
+ async (p) => {
32
+ const params = new URLSearchParams();
33
+ if (p.continuation !== undefined && p.continuation !== "") {
34
+ params.set("continuation", p.continuation);
35
+ }
36
+ const qs = params.toString();
37
+ const query = qs === "" ? "" : `?${qs}`;
38
+ return jsonResult(await canvaGet(`/rest/v1/designs${query}`));
39
+ },
40
+ );
41
+
42
+ reg(
43
+ "canva_get",
44
+ "Fetch one Canva design by its id (GET /rest/v1/designs/{designId}). Returns the { design: { id, title, created_at, updated_at, urls, thumbnail, ... } } envelope. Throws when no design with that id exists.",
45
+ z.object({
46
+ id: z.string().min(1),
47
+ }),
48
+ async (p) => {
49
+ return jsonResult(await canvaGet(`/rest/v1/designs/${encodeURIComponent(p.id)}`));
50
+ },
51
+ );
52
+
53
+ reg(
54
+ "canva_search",
55
+ "**Substring search over the FIRST PAGE only** of the authenticated user's Canva designs. This tool fetches GET /rest/v1/designs once and matches the query locally (case-insensitive) against the design title. **Designs beyond the first page are not searchable here — query the local Nimbus index instead for full coverage.** Returns a { matches: [...] } envelope.",
56
+ searchToolInputSchema(100),
57
+ async (p) => {
58
+ const root = await canvaGet("/rest/v1/designs");
59
+ const matches = filterCanvaDesigns(designsFrom(root), {
60
+ query: p.query,
61
+ limit: p.limit,
62
+ });
63
+ return jsonResult({ matches });
64
+ },
65
+ );
66
+ }
@@ -1,184 +1,10 @@
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 { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
5
- import {
6
- createRegisterSimpleTool,
7
- createZodToolRegistrar,
8
- mcpJsonResult as jsonResult,
9
- requireProcessEnv,
10
- } from "../../../shared/mcp-tool-kit.ts";
11
- import { makeRestToolRegistrar } from "../../../shared/rest-tool-kit.ts";
12
-
13
- const CCI_API = "https://circleci.com/api/v2";
14
-
15
- function projectPathSegments(projectSlug: string): string {
16
- return projectSlug
17
- .split("/")
18
- .map((s) => s.trim())
19
- .filter((s) => s.length > 0)
20
- .map((s) => encodeURIComponent(s))
21
- .join("/");
22
- }
23
-
24
- async function circleciFetch(
25
- token: string,
26
- path: string,
27
- init?: RequestInit,
28
- ): Promise<{ ok: boolean; status: number; json: unknown; text: string }> {
29
- const url = path.startsWith("http") ? path : `${CCI_API}${path}`;
30
- const res = await fetch(url, {
31
- ...init,
32
- headers: {
33
- Accept: "application/json",
34
- "Circle-Token": token,
35
- ...(init?.headers as Record<string, string> | undefined),
36
- },
37
- });
38
- const text = await res.text();
39
- let json: unknown;
40
- try {
41
- json = JSON.parse(text) as unknown;
42
- } catch {
43
- json = null;
44
- }
45
- return { ok: res.ok, status: res.status, json, text };
46
- }
3
+ import { registerCircleciTools } from "./tools.ts";
47
4
 
48
5
  const mcp = new McpServer({ name: "nimbus-circleci", version: "0.1.0" });
49
- const reg = createZodToolRegistrar(createRegisterSimpleTool(mcp));
50
-
51
- /**
52
- * Every MUTATING circleci tool goes through here. Outside the gateway this adds the
53
- * consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
54
- * the gateway it is a pass-through, because executor.ts (I2) is the gate there.
55
- */
56
- const registerWriteTool = createWriteToolRegistrar(mcp, {
57
- connector: "circleci",
58
- scopeEnv: "NIMBUS_MCP_CIRCLECI_WRITE_SCOPE",
59
- scopeKinds: ["project"],
60
- });
61
-
62
- /** Standard CircleCI read tool: token → circleciFetch(buildPath) → mcpJsonResultIfOk("CircleCI"). */
63
- const registerCciTool = makeRestToolRegistrar({
64
- registrar: reg,
65
- tokenEnv: "CIRCLECI_API_TOKEN",
66
- serviceLabel: "CircleCI",
67
- fetch: circleciFetch,
68
- });
69
-
70
- const projectSlugSchema = z.object({
71
- projectSlug: z
72
- .string()
73
- .min(3)
74
- .describe("CircleCI project slug, e.g. gh/org/repo or bb/workspace/repo"),
75
- });
76
-
77
- registerCciTool(
78
- "circleci_pipeline_list",
79
- "List pipelines for a CircleCI project.",
80
- projectSlugSchema.extend({
81
- pageToken: z.string().min(1).optional(),
82
- }),
83
- (parsed) => {
84
- const base = `/project/${projectPathSegments(parsed.projectSlug)}/pipeline`;
85
- const u = new URL(`${CCI_API}${base}`);
86
- if (parsed.pageToken !== undefined) {
87
- u.searchParams.set("page-token", parsed.pageToken);
88
- }
89
- return `${u.pathname}${u.search}`;
90
- },
91
- );
92
-
93
- registerCciTool(
94
- "circleci_pipeline_get",
95
- "Get a pipeline by UUID.",
96
- z.object({ pipelineId: z.uuid() }),
97
- (parsed) => `/pipeline/${encodeURIComponent(parsed.pipelineId)}`,
98
- );
99
-
100
- registerCciTool(
101
- "circleci_workflow_list",
102
- "List workflows for a pipeline.",
103
- z.object({ pipelineId: z.uuid() }),
104
- (parsed) => `/pipeline/${encodeURIComponent(parsed.pipelineId)}/workflow`,
105
- );
106
-
107
- registerCciTool(
108
- "circleci_job_list",
109
- "List jobs for a workflow.",
110
- z.object({ workflowId: z.uuid() }),
111
- (parsed) => `/workflow/${encodeURIComponent(parsed.workflowId)}/job`,
112
- );
113
-
114
- registerCciTool(
115
- "circleci_job_artifacts",
116
- "List artifact metadata for a job number under a project.",
117
- projectSlugSchema.extend({
118
- jobNumber: z.number().int().min(1),
119
- }),
120
- (parsed) =>
121
- `/project/${projectPathSegments(parsed.projectSlug)}/job/${String(parsed.jobNumber)}/artifacts`,
122
- );
123
-
124
- registerWriteTool(
125
- "circleci_pipeline_trigger",
126
- {
127
- mutates: "circleci.pipeline.trigger",
128
- recoverable: true,
129
- scopeTargetOf: (p) => ({ kind: "project", value: p.projectSlug }),
130
- },
131
- "Trigger a new pipeline on a branch (or tag).",
132
- projectSlugSchema.extend({
133
- branch: z.string().min(1).optional(),
134
- tag: z.string().min(1).optional(),
135
- parameters: z.record(z.string(), z.string()).optional(),
136
- }),
137
- async (parsed) => {
138
- const token = requireProcessEnv("CIRCLECI_API_TOKEN");
139
- const path = `/project/${projectPathSegments(parsed.projectSlug)}/pipeline`;
140
- const body: Record<string, unknown> = {};
141
- if (parsed.tag !== undefined && parsed.tag !== "") {
142
- body["tag"] = parsed.tag;
143
- } else {
144
- body["branch"] = parsed.branch ?? "main";
145
- }
146
- if (parsed.parameters !== undefined && Object.keys(parsed.parameters).length > 0) {
147
- body["parameters"] = parsed.parameters;
148
- }
149
- const res = await circleciFetch(token, path, {
150
- method: "POST",
151
- headers: { "Content-Type": "application/json" },
152
- body: JSON.stringify(body),
153
- });
154
- if (!res.ok) {
155
- throw new Error(`CircleCI trigger ${String(res.status)}: ${res.text.slice(0, 400)}`);
156
- }
157
- return jsonResult(res.json ?? { ok: true, raw: res.text });
158
- },
159
- );
160
6
 
161
- registerWriteTool(
162
- "circleci_job_cancel",
163
- {
164
- mutates: "circleci.job.cancel",
165
- recoverable: true,
166
- scopeTargetOf: (p) => ({ kind: "project", value: p.projectSlug }),
167
- },
168
- "Cancel a running job by project slug and job number.",
169
- projectSlugSchema.extend({
170
- jobNumber: z.number().int().min(1),
171
- }),
172
- async (parsed) => {
173
- const token = requireProcessEnv("CIRCLECI_API_TOKEN");
174
- const path = `/project/${projectPathSegments(parsed.projectSlug)}/job/${String(parsed.jobNumber)}/cancel`;
175
- const res = await circleciFetch(token, path, { method: "POST" });
176
- if (!res.ok) {
177
- throw new Error(`CircleCI job cancel ${String(res.status)}: ${res.text.slice(0, 400)}`);
178
- }
179
- return jsonResult(res.json ?? { ok: true });
180
- },
181
- );
7
+ registerCircleciTools(mcp);
182
8
 
183
9
  const transport = new StdioServerTransport();
184
10
  await mcp.connect(transport);
@@ -0,0 +1,193 @@
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
+ requireProcessEnv,
9
+ } from "../../../shared/mcp-tool-kit.ts";
10
+ import {
11
+ makeRestToolRegistrar,
12
+ type RestFetchResult,
13
+ toRestFetchResult,
14
+ } from "../../../shared/rest-tool-kit.ts";
15
+
16
+ const CCI_API = "https://circleci.com/api/v2";
17
+
18
+ function projectPathSegments(projectSlug: string): string {
19
+ return projectSlug
20
+ .split("/")
21
+ .map((s) => s.trim())
22
+ .filter((s) => s.length > 0)
23
+ .map((s) => encodeURIComponent(s))
24
+ .join("/");
25
+ }
26
+
27
+ async function circleciFetch(
28
+ token: string,
29
+ path: string,
30
+ init?: RequestInit,
31
+ ): Promise<RestFetchResult> {
32
+ const url = path.startsWith("http") ? path : `${CCI_API}${path}`;
33
+ const res = await fetch(url, {
34
+ ...init,
35
+ headers: {
36
+ Accept: "application/json",
37
+ "Circle-Token": token,
38
+ ...(init?.headers as Record<string, string> | undefined),
39
+ },
40
+ });
41
+ return toRestFetchResult(res);
42
+ }
43
+
44
+ /** Tool names exposed by this connector — for contract/introspection tests. */
45
+ export const CIRCLECI_TOOL_NAMES = [
46
+ "circleci_pipeline_list",
47
+ "circleci_pipeline_get",
48
+ "circleci_workflow_list",
49
+ "circleci_job_list",
50
+ "circleci_job_artifacts",
51
+ "circleci_pipeline_trigger",
52
+ "circleci_job_cancel",
53
+ ] as const;
54
+
55
+ export function registerCircleciTools(
56
+ server: ConsentServer & { tool: (...args: never) => unknown },
57
+ ): void {
58
+ const reg = createZodToolRegistrar(createRegisterSimpleTool(server));
59
+
60
+ /**
61
+ * Every MUTATING circleci tool goes through here. Outside the gateway this adds the
62
+ * consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
63
+ * the gateway it is a pass-through, because executor.ts (I2) is the gate there.
64
+ */
65
+ const registerWriteTool = createWriteToolRegistrar(server, {
66
+ connector: "circleci",
67
+ scopeEnv: "NIMBUS_MCP_CIRCLECI_WRITE_SCOPE",
68
+ scopeKinds: ["project"],
69
+ });
70
+
71
+ /** Standard CircleCI read tool: token → circleciFetch(buildPath) → mcpJsonResultIfOk("CircleCI"). */
72
+ const registerCciTool = makeRestToolRegistrar({
73
+ registrar: reg,
74
+ tokenEnv: "CIRCLECI_API_TOKEN",
75
+ serviceLabel: "CircleCI",
76
+ fetch: circleciFetch,
77
+ });
78
+
79
+ const projectSlugSchema = z.object({
80
+ projectSlug: z
81
+ .string()
82
+ .min(3)
83
+ .describe("CircleCI project slug, e.g. gh/org/repo or bb/workspace/repo"),
84
+ });
85
+
86
+ registerCciTool(
87
+ "circleci_pipeline_list",
88
+ "List pipelines for a CircleCI project.",
89
+ projectSlugSchema.extend({
90
+ pageToken: z.string().min(1).optional(),
91
+ }),
92
+ (parsed) => {
93
+ const base = `/project/${projectPathSegments(parsed.projectSlug)}/pipeline`;
94
+ const u = new URL(`${CCI_API}${base}`);
95
+ if (parsed.pageToken !== undefined) {
96
+ u.searchParams.set("page-token", parsed.pageToken);
97
+ }
98
+ // Relative to CCI_API, like every other tool here: the fetcher prefixes the base, which
99
+ // already ends in /api/v2, so returning `u.pathname` requested /api/v2/api/v2/….
100
+ return `${base}${u.search}`;
101
+ },
102
+ );
103
+
104
+ registerCciTool(
105
+ "circleci_pipeline_get",
106
+ "Get a pipeline by UUID.",
107
+ z.object({ pipelineId: z.uuid() }),
108
+ (parsed) => `/pipeline/${encodeURIComponent(parsed.pipelineId)}`,
109
+ );
110
+
111
+ registerCciTool(
112
+ "circleci_workflow_list",
113
+ "List workflows for a pipeline.",
114
+ z.object({ pipelineId: z.uuid() }),
115
+ (parsed) => `/pipeline/${encodeURIComponent(parsed.pipelineId)}/workflow`,
116
+ );
117
+
118
+ registerCciTool(
119
+ "circleci_job_list",
120
+ "List jobs for a workflow.",
121
+ z.object({ workflowId: z.uuid() }),
122
+ (parsed) => `/workflow/${encodeURIComponent(parsed.workflowId)}/job`,
123
+ );
124
+
125
+ registerCciTool(
126
+ "circleci_job_artifacts",
127
+ "List artifact metadata for a job number under a project.",
128
+ projectSlugSchema.extend({
129
+ jobNumber: z.number().int().min(1),
130
+ }),
131
+ (parsed) =>
132
+ `/project/${projectPathSegments(parsed.projectSlug)}/job/${String(parsed.jobNumber)}/artifacts`,
133
+ );
134
+
135
+ registerWriteTool(
136
+ "circleci_pipeline_trigger",
137
+ {
138
+ mutates: "circleci.pipeline.trigger",
139
+ recoverable: true,
140
+ scopeTargetOf: (p) => ({ kind: "project", value: p.projectSlug }),
141
+ },
142
+ "Trigger a new pipeline on a branch (or tag).",
143
+ projectSlugSchema.extend({
144
+ branch: z.string().min(1).optional(),
145
+ tag: z.string().min(1).optional(),
146
+ parameters: z.record(z.string(), z.string()).optional(),
147
+ }),
148
+ async (parsed) => {
149
+ const token = requireProcessEnv("CIRCLECI_API_TOKEN");
150
+ const path = `/project/${projectPathSegments(parsed.projectSlug)}/pipeline`;
151
+ const body: Record<string, unknown> = {};
152
+ if (parsed.tag !== undefined && parsed.tag !== "") {
153
+ body["tag"] = parsed.tag;
154
+ } else {
155
+ body["branch"] = parsed.branch ?? "main";
156
+ }
157
+ if (parsed.parameters !== undefined && Object.keys(parsed.parameters).length > 0) {
158
+ body["parameters"] = parsed.parameters;
159
+ }
160
+ const res = await circleciFetch(token, path, {
161
+ method: "POST",
162
+ headers: { "Content-Type": "application/json" },
163
+ body: JSON.stringify(body),
164
+ });
165
+ if (!res.ok) {
166
+ throw new Error(`CircleCI trigger ${String(res.status)}: ${res.text.slice(0, 400)}`);
167
+ }
168
+ return jsonResult(res.json ?? { ok: true, raw: res.text });
169
+ },
170
+ );
171
+
172
+ registerWriteTool(
173
+ "circleci_job_cancel",
174
+ {
175
+ mutates: "circleci.job.cancel",
176
+ recoverable: true,
177
+ scopeTargetOf: (p) => ({ kind: "project", value: p.projectSlug }),
178
+ },
179
+ "Cancel a running job by project slug and job number.",
180
+ projectSlugSchema.extend({
181
+ jobNumber: z.number().int().min(1),
182
+ }),
183
+ async (parsed) => {
184
+ const token = requireProcessEnv("CIRCLECI_API_TOKEN");
185
+ const path = `/project/${projectPathSegments(parsed.projectSlug)}/job/${String(parsed.jobNumber)}/cancel`;
186
+ const res = await circleciFetch(token, path, { method: "POST" });
187
+ if (!res.ok) {
188
+ throw new Error(`CircleCI job cancel ${String(res.status)}: ${res.text.slice(0, 400)}`);
189
+ }
190
+ return jsonResult(res.json ?? { ok: true });
191
+ },
192
+ );
193
+ }
@@ -1,8 +1,14 @@
1
1
  import { z } from "zod";
2
+ import {
3
+ cliArg,
4
+ createCliJsonRunner,
5
+ gcloudProjectArgs,
6
+ isRecord,
7
+ strField,
8
+ } from "../../../shared/cli-json-kit.ts";
2
9
  import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
3
10
  import { nimbusSpawn } from "../../../shared/nimbus-spawn.ts";
4
11
  import type { ZodToolRegistrar } from "../../../shared/run-read-only-mcp-connector.ts";
5
- import { assertSafeCliArg } from "../../../shared/safe-cli-arg.ts";
6
12
 
7
13
  /**
8
14
  * GCP Cloud Logging (Tier-3, no-row-data) MCP tool surface. ALL tools index log
@@ -20,42 +26,23 @@ export const CLOUD_LOGGING_TOOL_NAMES = [
20
26
  "cloud_logging_search",
21
27
  ] as const;
22
28
 
23
- function isRecord(v: unknown): v is Record<string, unknown> {
24
- return v !== null && typeof v === "object" && !Array.isArray(v);
25
- }
26
-
27
- function strField(r: Record<string, unknown>, key: string): string {
28
- const v = r[key];
29
- return typeof v === "string" ? v : "";
30
- }
31
-
32
29
  function asArray(parsed: unknown): unknown[] {
33
30
  return Array.isArray(parsed) ? parsed : [];
34
31
  }
35
32
 
36
- /** The configured GCP project id, if `GOOGLE_CLOUD_PROJECT` is set at spawn. */
37
- function projectArgs(): string[] {
38
- const project = process.env["GOOGLE_CLOUD_PROJECT"]?.trim();
39
- return project !== undefined && project !== "" ? ["--project", project] : [];
40
- }
41
-
42
33
  /**
43
34
  * Run `gcloud logging <args> --format json` and parse stdout. gcloud reads
44
35
  * Application Default Credentials from `GOOGLE_APPLICATION_CREDENTIALS`
45
36
  * natively from the process env injected at spawn time. Throws on non-zero exit.
46
37
  */
47
- async function gcloudLogging(args: string[]): Promise<unknown> {
48
- const { code, stdout, stderr } = await nimbusSpawn(
49
- ["gcloud", "logging", ...args, ...projectArgs(), "--format", "json"],
50
- {},
51
- );
52
- const out = stdout.trim();
53
- const err = stderr.trim();
54
- if (code !== 0) {
55
- throw new Error(`gcloud logging ${args[0] ?? ""} failed: ${err.slice(0, 400)}`);
56
- }
57
- return out === "" ? [] : (JSON.parse(out) as unknown);
58
- }
38
+ const gcloudLogging = createCliJsonRunner(
39
+ {
40
+ argv: (args) => ["gcloud", "logging", ...args, ...gcloudProjectArgs(), "--format", "json"],
41
+ label: "gcloud logging",
42
+ emptyResult: [],
43
+ },
44
+ nimbusSpawn,
45
+ );
59
46
 
60
47
  function sinkMatches(entry: unknown, q: string): boolean {
61
48
  if (!isRecord(entry)) {
@@ -88,15 +75,12 @@ export function registerCloudLoggingTools(reg: ZodToolRegistrar): void {
88
75
  "cloud_logging_get",
89
76
  "Fetch one Cloud Logging routing sink's configuration METADATA (`gcloud logging sinks describe <sink>`). Returns the sink's `name`, `destination`, `filter`, `description`, `disabled` flag, and create/update timestamps. No log-entry contents are returned — metadata only.",
90
77
  z.object({
91
- sinkName: z.string().min(1),
78
+ // A bare positional to `gcloud logging sinks describe`, so it is refused here, at the
79
+ // schema, if gcloud would misread it: a value starting with "-" would be parsed as a flag.
80
+ sinkName: cliArg,
92
81
  }),
93
82
  async (p) => {
94
- // `sinkName` is a bare positional to `gcloud logging sinks describe`; guard
95
- // against argv flag smuggling (a value beginning with "-" would be parsed as
96
- // a gcloud flag).
97
- return jsonResult(
98
- await gcloudLogging(["sinks", "describe", assertSafeCliArg(p.sinkName, "sinkName")]),
99
- );
83
+ return jsonResult(await gcloudLogging(["sinks", "describe", p.sinkName]));
100
84
  },
101
85
  );
102
86