@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,106 @@
1
+ import { mintGoogleAccessToken } from "@nimbus-dev/sdk";
2
+ import { z } from "zod";
3
+ import { matchesResult } 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 { filterFirebaseReleases } from "./search-filter.ts";
7
+ import { configuredAppIds, projectNumberFromAppId, serviceAccountFromEnv } from "./token.ts";
8
+
9
+ const APP_DISTRIBUTION_API = "https://firebaseappdistribution.googleapis.com";
10
+
11
+ // App Distribution OAuth2 tokens last ~1h; cache and refresh conservatively.
12
+ let cachedToken: { token: string; expiresAt: number } | null = null;
13
+
14
+ /**
15
+ * Forget the cached access token. Tests only: the cache is module state shared by every test in
16
+ * the process, and a token one test left behind lets a later tool call skip reading its
17
+ * credential entirely.
18
+ */
19
+ export function __resetFirebaseTokenCacheForTests(): void {
20
+ cachedToken = null;
21
+ }
22
+
23
+ async function accessToken(): Promise<string> {
24
+ const now = Date.now();
25
+ if (cachedToken !== null && cachedToken.expiresAt > now + 60_000) {
26
+ return cachedToken.token;
27
+ }
28
+ const token = await mintGoogleAccessToken(serviceAccountFromEnv());
29
+ if (token === null) {
30
+ throw new Error("failed to mint a Firebase access token");
31
+ }
32
+ cachedToken = { token, expiresAt: now + 30 * 60 * 1000 };
33
+ return token;
34
+ }
35
+
36
+ async function appDistGet(path: string): Promise<unknown> {
37
+ const res = await fetch(`${APP_DISTRIBUTION_API}${path}`, {
38
+ headers: { Authorization: `Bearer ${await accessToken()}`, Accept: "application/json" },
39
+ });
40
+ const text = await res.text();
41
+ if (!res.ok) {
42
+ throw new Error(`Firebase App Distribution ${String(res.status)}: ${text.slice(0, 400)}`);
43
+ }
44
+ return JSON.parse(text) as unknown;
45
+ }
46
+
47
+ function appBasePath(appId: string): string {
48
+ const projectNumber = projectNumberFromAppId(appId);
49
+ if (projectNumber === null) {
50
+ throw new Error(`cannot derive a project number from app id: ${appId}`);
51
+ }
52
+ return `/v1/projects/${encodeURIComponent(projectNumber)}/apps/${encodeURIComponent(appId)}`;
53
+ }
54
+
55
+ function releasesPath(appId: string, pageSize: number): string {
56
+ return `${appBasePath(appId)}/releases?pageSize=${String(pageSize)}`;
57
+ }
58
+
59
+ function releasePath(appId: string, releaseId: string): string {
60
+ return `${appBasePath(appId)}/releases/${encodeURIComponent(releaseId)}`;
61
+ }
62
+
63
+ /** Tool names exposed by this connector — for contract/introspection tests. */
64
+ export const FIREBASE_TOOL_NAMES = ["firebase_get", "firebase_list", "firebase_search"] as const;
65
+
66
+ export function registerFirebaseTools(reg: ZodToolRegistrar): void {
67
+ reg(
68
+ "firebase_list",
69
+ "List Firebase App Distribution resources. When `appId` is omitted, returns the connector's configured app ids. When `appId` is provided, returns that app's most-recent releases via GET /v1/projects/<n>/apps/<appId>/releases (pageSize optional, default 50).",
70
+ z.object({
71
+ appId: z.string().min(1).optional(),
72
+ pageSize: z.number().int().min(1).max(100).optional(),
73
+ }),
74
+ async (p) => {
75
+ if (p.appId === undefined) {
76
+ return jsonResult({ appIds: configuredAppIds() });
77
+ }
78
+ return jsonResult(await appDistGet(releasesPath(p.appId, p.pageSize ?? 50)));
79
+ },
80
+ );
81
+
82
+ reg(
83
+ "firebase_get",
84
+ "Fetch a single Firebase App Distribution release via GET /v1/projects/<n>/apps/<appId>/releases/<releaseId>.",
85
+ z.object({
86
+ appId: z.string().min(1),
87
+ releaseId: z.string().min(1),
88
+ }),
89
+ async (p) => jsonResult(await appDistGet(releasePath(p.appId, p.releaseId))),
90
+ );
91
+
92
+ reg(
93
+ "firebase_search",
94
+ "Substring search across an app's recent releases. Matches the query against each release's display version, build version, release notes, and resource name (case-insensitive). Returns a `{ matches: [...] }` envelope.",
95
+ z.object({
96
+ appId: z.string().min(1),
97
+ query: z.string().min(1),
98
+ limit: z.number().int().min(1).max(100).optional(),
99
+ }),
100
+ async (p) => {
101
+ const root = await appDistGet(releasesPath(p.appId, 100));
102
+ const releases = (root as { releases?: unknown[] } | null)?.releases;
103
+ return matchesResult(releases, filterFirebaseReleases, p);
104
+ },
105
+ );
106
+ }
@@ -1,102 +1,6 @@
1
- import { z } from "zod";
2
- import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
3
1
  import { runReadOnlyMcpConnector } from "../../../shared/run-read-only-mcp-connector.ts";
4
- import { filterFlagsmithFeatures } from "./search-filter.ts";
5
-
6
- const DEFAULT_API_BASE = "https://api.flagsmith.com";
7
-
8
- function apiBase(): string {
9
- const v = process.env["FLAGSMITH_API_BASE"]?.trim();
10
- return v === undefined || v === "" ? DEFAULT_API_BASE : v;
11
- }
12
-
13
- function authHeader(): Record<string, string> {
14
- const t = process.env["FLAGSMITH_TOKEN"]?.trim();
15
- if (t === undefined || t === "") {
16
- throw new Error("FLAGSMITH_TOKEN is not set");
17
- }
18
- return { Authorization: `Token ${t}`, Accept: "application/json" };
19
- }
20
-
21
- async function fsGet(path: string): Promise<unknown> {
22
- const res = await fetch(`${apiBase()}/api/v1${path}`, { headers: authHeader() });
23
- const text = await res.text();
24
- if (!res.ok) {
25
- throw new Error(`Flagsmith ${String(res.status)}: ${text.slice(0, 400)}`);
26
- }
27
- return JSON.parse(text) as unknown;
28
- }
29
-
30
- function featuresFromPage(root: unknown): unknown[] {
31
- if (Array.isArray(root)) {
32
- return root;
33
- }
34
- const results = (root as { results?: unknown } | null)?.results;
35
- return Array.isArray(results) ? results : [];
36
- }
2
+ import { registerFlagsmithTools } from "./tools.ts";
37
3
 
38
4
  await runReadOnlyMcpConnector("nimbus-flagsmith", (reg) => {
39
- reg(
40
- "flagsmith_list",
41
- "List Flagsmith projects, or feature flags for a project. Without `projectId`, returns the account's projects (`/projects/`). With `projectId`, returns that project's feature flags (`/projects/{projectId}/features/`), capped at `limit` (default 100).",
42
- z.object({
43
- projectId: z.number().int().optional(),
44
- limit: z.number().int().min(1).max(500).optional(),
45
- }),
46
- async (p) => {
47
- if (p.projectId === undefined) {
48
- return jsonResult(await fsGet("/projects/"));
49
- }
50
- const search = new URLSearchParams({ page_size: String(p.limit ?? 100) });
51
- return jsonResult(
52
- await fsGet(
53
- `/projects/${encodeURIComponent(String(p.projectId))}/features/?${search.toString()}`,
54
- ),
55
- );
56
- },
57
- );
58
-
59
- reg(
60
- "flagsmith_get",
61
- "Fetch one Flagsmith feature flag by project id + feature name. Flagsmith has no GET-by-name endpoint, so this searches the project's features and narrows to the exact-name match. Throws when no match is found.",
62
- z.object({
63
- projectId: z.number().int(),
64
- featureName: z.string().min(1),
65
- }),
66
- async (p) => {
67
- const search = new URLSearchParams({ page_size: "100", search: p.featureName });
68
- const root = await fsGet(
69
- `/projects/${encodeURIComponent(String(p.projectId))}/features/?${search.toString()}`,
70
- );
71
- const match = featuresFromPage(root).find(
72
- (f) =>
73
- f !== null && typeof f === "object" && (f as { name?: unknown }).name === p.featureName,
74
- );
75
- if (match === undefined) {
76
- throw new Error(`Flagsmith feature not found: ${p.featureName}`);
77
- }
78
- return jsonResult(match);
79
- },
80
- );
81
-
82
- reg(
83
- "flagsmith_search",
84
- "Substring search across a project's feature flags. Matches the query against flag name, description, and tags (case-insensitive). Returns a `{ matches: [...] }` envelope.",
85
- z.object({
86
- projectId: z.number().int(),
87
- query: z.string().min(1),
88
- limit: z.number().int().min(1).max(200).optional(),
89
- }),
90
- async (p) => {
91
- const search = new URLSearchParams({ page_size: "500" });
92
- const root = await fsGet(
93
- `/projects/${encodeURIComponent(String(p.projectId))}/features/?${search.toString()}`,
94
- );
95
- const matches = filterFlagsmithFeatures(featuresFromPage(root), {
96
- query: p.query,
97
- limit: p.limit,
98
- });
99
- return jsonResult({ matches });
100
- },
101
- );
5
+ registerFlagsmithTools(reg);
102
6
  });
@@ -0,0 +1,95 @@
1
+ import { z } from "zod";
2
+ import { createJsonGetter, envAuthHeaders, optionalEnv } from "../../../shared/env-json-api.ts";
3
+ import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
4
+ import type { ZodToolRegistrar } from "../../../shared/run-read-only-mcp-connector.ts";
5
+ import { filterFlagsmithFeatures } from "./search-filter.ts";
6
+
7
+ const DEFAULT_API_BASE = "https://api.flagsmith.com";
8
+
9
+ const fsGet = createJsonGetter({
10
+ // `optionalEnv`, not `optionalBaseUrl`: a trailing slash on the override is left as given.
11
+ base: () => `${optionalEnv("FLAGSMITH_API_BASE", DEFAULT_API_BASE)}/api/v1`,
12
+ label: "Flagsmith",
13
+ headers: envAuthHeaders({ env: "FLAGSMITH_TOKEN", scheme: "Token" }),
14
+ });
15
+
16
+ function featuresFromPage(root: unknown): unknown[] {
17
+ if (Array.isArray(root)) {
18
+ return root;
19
+ }
20
+ const results = (root as { results?: unknown } | null)?.results;
21
+ return Array.isArray(results) ? results : [];
22
+ }
23
+
24
+ /** Tool names exposed by this connector — for contract/introspection tests. */
25
+ export const FLAGSMITH_TOOL_NAMES = [
26
+ "flagsmith_list",
27
+ "flagsmith_get",
28
+ "flagsmith_search",
29
+ ] as const;
30
+
31
+ export function registerFlagsmithTools(reg: ZodToolRegistrar): void {
32
+ reg(
33
+ "flagsmith_list",
34
+ "List Flagsmith projects, or feature flags for a project. Without `projectId`, returns the account's projects (`/projects/`). With `projectId`, returns that project's feature flags (`/projects/{projectId}/features/`), capped at `limit` (default 100).",
35
+ z.object({
36
+ projectId: z.number().int().optional(),
37
+ limit: z.number().int().min(1).max(500).optional(),
38
+ }),
39
+ async (p) => {
40
+ if (p.projectId === undefined) {
41
+ return jsonResult(await fsGet("/projects/"));
42
+ }
43
+ const search = new URLSearchParams({ page_size: String(p.limit ?? 100) });
44
+ return jsonResult(
45
+ await fsGet(
46
+ `/projects/${encodeURIComponent(String(p.projectId))}/features/?${search.toString()}`,
47
+ ),
48
+ );
49
+ },
50
+ );
51
+
52
+ reg(
53
+ "flagsmith_get",
54
+ "Fetch one Flagsmith feature flag by project id + feature name. Flagsmith has no GET-by-name endpoint, so this searches the project's features and narrows to the exact-name match. Throws when no match is found.",
55
+ z.object({
56
+ projectId: z.number().int(),
57
+ featureName: z.string().min(1),
58
+ }),
59
+ async (p) => {
60
+ const search = new URLSearchParams({ page_size: "100", search: p.featureName });
61
+ const root = await fsGet(
62
+ `/projects/${encodeURIComponent(String(p.projectId))}/features/?${search.toString()}`,
63
+ );
64
+ const match = featuresFromPage(root).find(
65
+ (f) =>
66
+ f !== null && typeof f === "object" && (f as { name?: unknown }).name === p.featureName,
67
+ );
68
+ if (match === undefined) {
69
+ throw new Error(`Flagsmith feature not found: ${p.featureName}`);
70
+ }
71
+ return jsonResult(match);
72
+ },
73
+ );
74
+
75
+ reg(
76
+ "flagsmith_search",
77
+ "Substring search across a project's feature flags. Matches the query against flag name, description, and tags (case-insensitive). Returns a `{ matches: [...] }` envelope.",
78
+ z.object({
79
+ projectId: z.number().int(),
80
+ query: z.string().min(1),
81
+ limit: z.number().int().min(1).max(200).optional(),
82
+ }),
83
+ async (p) => {
84
+ const search = new URLSearchParams({ page_size: "500" });
85
+ const root = await fsGet(
86
+ `/projects/${encodeURIComponent(String(p.projectId))}/features/?${search.toString()}`,
87
+ );
88
+ const matches = filterFlagsmithFeatures(featuresFromPage(root), {
89
+ query: p.query,
90
+ limit: p.limit,
91
+ });
92
+ return jsonResult({ matches });
93
+ },
94
+ );
95
+ }
@@ -1,6 +1,7 @@
1
1
  import { FLUX_KINDS, type FluxKindEntry, trimTrailingSlash } from "@nimbus-dev/sdk";
2
2
  import { z } from "zod";
3
3
  import { type ConsentServer, createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
4
+ import { createJsonGetter, envAuthHeaders, requiredEnv } from "../../../shared/env-json-api.ts";
4
5
  import { fetchWithTimeout, mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
5
6
  import {
6
7
  runReadOnlyMcpConnector,
@@ -18,30 +19,24 @@ function kindEntry(kind: string): FluxKindEntry {
18
19
  return found;
19
20
  }
20
21
 
22
+ /** Only ONE trailing slash is dropped here — not `requiredBaseUrl`'s strip-them-all. */
21
23
  function apiBase(): string {
22
- const v = process.env["FLUX_API_URL"]?.trim();
23
- if (v === undefined || v === "") {
24
- throw new Error("FLUX_API_URL is not set");
25
- }
26
- return trimTrailingSlash(v);
24
+ return trimTrailingSlash(requiredEnv("FLUX_API_URL"));
27
25
  }
28
26
 
29
- function authHeader(): Record<string, string> {
30
- const t = process.env["FLUX_TOKEN"]?.trim();
31
- if (t === undefined || t === "") {
32
- throw new Error("FLUX_TOKEN is not set");
33
- }
34
- return { Authorization: `Bearer ${t}`, Accept: "application/json" };
35
- }
27
+ /**
28
+ * `fetchWithTimeout`, not the global fetch: this is a self-hosted control plane,
29
+ * and one that stops answering must fail the tool call rather than hang it.
30
+ */
31
+ /** Shared with the mutating request below, which adds its own Content-Type. */
32
+ const authHeader = envAuthHeaders({ env: "FLUX_TOKEN" });
36
33
 
37
- async function agGet(path: string): Promise<unknown> {
38
- const res = await fetchWithTimeout(`${apiBase()}${path}`, { headers: authHeader() });
39
- const text = await res.text();
40
- if (!res.ok) {
41
- throw new Error(`Flux ${String(res.status)}: ${text.slice(0, 400)}`);
42
- }
43
- return JSON.parse(text) as unknown;
44
- }
34
+ const agGet = createJsonGetter({
35
+ base: apiBase,
36
+ label: "Flux",
37
+ headers: authHeader,
38
+ fetch: fetchWithTimeout,
39
+ });
45
40
 
46
41
  function itemsFrom(root: unknown): unknown[] {
47
42
  if (Array.isArray(root)) {
@@ -80,6 +75,24 @@ async function fluxReconcile(kind: string, namespace: string, name: string): Pro
80
75
  return requestedAt;
81
76
  }
82
77
 
78
+ /** The Flux CRs a reconcile can be requested on. */
79
+ const RECONCILABLE = [
80
+ {
81
+ tool: "flux_kustomization_reconcile",
82
+ kind: "kustomization",
83
+ mutates: "flux.kustomization.reconcile",
84
+ description:
85
+ "Request a reconcile of a Flux Kustomization by annotating reconcile.fluxcd.io/requestedAt (PATCH the CR; requires HITL flux.kustomization.reconcile, and the SA's `patch` RBAC verb on kustomizations). Async — verify via the next metadata sync.",
86
+ },
87
+ {
88
+ tool: "flux_helmrelease_reconcile",
89
+ kind: "helm_release",
90
+ mutates: "flux.helmrelease.reconcile",
91
+ description:
92
+ "Request a reconcile of a Flux HelmRelease by annotating reconcile.fluxcd.io/requestedAt (PATCH the CR; requires HITL flux.helmrelease.reconcile, and the SA's `patch` RBAC verb on helmreleases). Async — verify via the next metadata sync.",
93
+ },
94
+ ] as const;
95
+
83
96
  export function registerFluxTools(reg: ZodToolRegistrar, server: unknown): void {
84
97
  // Despite the read-only helper's name, this connector exposes write tools. The consent
85
98
  // kit needs the real server, which the helper now passes through as its second argument.
@@ -140,39 +153,25 @@ export function registerFluxTools(reg: ZodToolRegistrar, server: unknown): void
140
153
  },
141
154
  );
142
155
 
143
- registerWriteTool(
144
- "flux_kustomization_reconcile",
145
- {
146
- mutates: "flux.kustomization.reconcile",
147
- recoverable: true,
148
- scopeTargetOf: (p) => ({ kind: "namespace", value: p.namespace }),
149
- },
150
- "Request a reconcile of a Flux Kustomization by annotating reconcile.fluxcd.io/requestedAt (PATCH the CR; requires HITL flux.kustomization.reconcile, and the SA's `patch` RBAC verb on kustomizations). Async — verify via the next metadata sync.",
151
- z.object({ namespace: z.string().min(1), name: z.string().min(1) }),
152
- async (p) =>
153
- jsonResult({
154
- status: "requested",
155
- name: p.name,
156
- requestedAt: await fluxReconcile("kustomization", p.namespace, p.name),
157
- }),
158
- );
159
-
160
- registerWriteTool(
161
- "flux_helmrelease_reconcile",
162
- {
163
- mutates: "flux.helmrelease.reconcile",
164
- recoverable: true,
165
- scopeTargetOf: (p) => ({ kind: "namespace", value: p.namespace }),
166
- },
167
- "Request a reconcile of a Flux HelmRelease by annotating reconcile.fluxcd.io/requestedAt (PATCH the CR; requires HITL flux.helmrelease.reconcile, and the SA's `patch` RBAC verb on helmreleases). Async — verify via the next metadata sync.",
168
- z.object({ namespace: z.string().min(1), name: z.string().min(1) }),
169
- async (p) =>
170
- jsonResult({
171
- status: "requested",
172
- name: p.name,
173
- requestedAt: await fluxReconcile("helm_release", p.namespace, p.name),
174
- }),
175
- );
156
+ // Both reconcile tools are the same annotation PATCH against a different CR.
157
+ for (const { tool, kind, mutates, description } of RECONCILABLE) {
158
+ registerWriteTool(
159
+ tool,
160
+ {
161
+ mutates,
162
+ recoverable: true,
163
+ scopeTargetOf: (p) => ({ kind: "namespace", value: p.namespace }),
164
+ },
165
+ description,
166
+ z.object({ namespace: z.string().min(1), name: z.string().min(1) }),
167
+ async (p) =>
168
+ jsonResult({
169
+ status: "requested",
170
+ name: p.name,
171
+ requestedAt: await fluxReconcile(kind, p.namespace, p.name),
172
+ }),
173
+ );
174
+ }
176
175
  }
177
176
 
178
177
  export async function startConnector(): Promise<void> {
@@ -1,140 +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 {
5
- createRegisterSimpleTool,
6
- createZodToolRegistrar,
7
- mcpJsonResult as jsonResult,
8
- } from "../../../shared/mcp-tool-kit.ts";
9
- import { runCliJson, runCliOk } from "../../../shared/run-cli-json.ts";
10
-
11
- function gcloudEnv(): Record<string, string | undefined> {
12
- const e = { ...process.env } as Record<string, string | undefined>;
13
- const cf = process.env["GOOGLE_APPLICATION_CREDENTIALS"]?.trim();
14
- if (cf !== undefined && cf !== "") {
15
- e["GOOGLE_APPLICATION_CREDENTIALS"] = cf;
16
- }
17
- return e;
18
- }
19
-
20
- async function gcloudJson(args: string[]): Promise<unknown> {
21
- const cmd = ["gcloud", ...args, "--format", "json"];
22
- const r = await runCliJson(cmd, gcloudEnv());
23
- if (!r.ok) {
24
- throw new Error(r.message);
25
- }
26
- return r.data ?? {};
27
- }
3
+ import { registerGcpTools } from "./tools.ts";
28
4
 
29
5
  const mcp = new McpServer({ name: "nimbus-gcp", version: "0.1.0" });
30
6
 
31
- import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
32
-
33
- const reg = createZodToolRegistrar(createRegisterSimpleTool(mcp));
34
-
35
- /**
36
- * Every MUTATING gcp tool goes through here. Outside the gateway this adds the
37
- * consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
38
- * the gateway it is a pass-through, because executor.ts (I2) is the gate there.
39
- */
40
- const registerWriteTool = createWriteToolRegistrar(mcp, {
41
- connector: "gcp",
42
- scopeEnv: "NIMBUS_MCP_GCP_WRITE_SCOPE",
43
- scopeKinds: ["project"],
44
- });
45
-
46
- reg(
47
- "gcp_cloud_run_service_list",
48
- "List Cloud Run services in a region.",
49
- z.object({ projectId: z.string().min(1), region: z.string().min(1) }),
50
- async (p) =>
51
- jsonResult(
52
- await gcloudJson([
53
- "run",
54
- "services",
55
- "list",
56
- `--project=${p.projectId}`,
57
- `--region=${p.region}`,
58
- ]),
59
- ),
60
- );
61
-
62
- registerWriteTool(
63
- "gcp_cloud_run_deploy",
64
- {
65
- mutates: "gcp.cloud_run.deploy",
66
- recoverable: true,
67
- scopeTargetOf: (p) => ({ kind: "project", value: p.projectId }),
68
- },
69
- "Deploy a container image to Cloud Run.",
70
- z.object({
71
- projectId: z.string().min(1),
72
- region: z.string().min(1),
73
- service: z.string().min(1),
74
- image: z.string().min(1),
75
- }),
76
- async (p) => {
77
- const r = await runCliOk(
78
- [
79
- "gcloud",
80
- "run",
81
- "deploy",
82
- p.service,
83
- `--project=${p.projectId}`,
84
- `--region=${p.region}`,
85
- `--image=${p.image}`,
86
- "--quiet",
87
- ],
88
- gcloudEnv(),
89
- );
90
- if (!r.ok) {
91
- throw new Error(r.message);
92
- }
93
- return jsonResult({ ok: true });
94
- },
95
- );
96
-
97
- registerWriteTool(
98
- "gcp_gke_workload_restart",
99
- {
100
- mutates: "gcp.gke.workload.restart",
101
- recoverable: true,
102
- scopeTargetOf: (p) => ({ kind: "project", value: p.projectId }),
103
- },
104
- "Restart a GKE deployment rollout via kubectl (uses current cluster credentials).",
105
- z.object({
106
- projectId: z.string().min(1),
107
- location: z.string().min(1),
108
- cluster: z.string().min(1),
109
- namespace: z.string().min(1),
110
- deployment: z.string().min(1),
111
- }),
112
- async (p) => {
113
- const getCreds = await runCliOk(
114
- [
115
- "gcloud",
116
- "container",
117
- "clusters",
118
- "get-credentials",
119
- p.cluster,
120
- `--project=${p.projectId}`,
121
- `--zone=${p.location}`,
122
- ],
123
- gcloudEnv(),
124
- );
125
- if (!getCreds.ok) {
126
- throw new Error(getCreds.message);
127
- }
128
- const r = await runCliOk(
129
- ["kubectl", "rollout", "restart", "deployment", p.deployment, "-n", p.namespace],
130
- gcloudEnv(),
131
- );
132
- if (!r.ok) {
133
- throw new Error(r.message);
134
- }
135
- return jsonResult({ ok: true });
136
- },
137
- );
7
+ registerGcpTools(mcp);
138
8
 
139
9
  const transport = new StdioServerTransport();
140
10
  await mcp.connect(transport);