@nimbus-dev/connectors 0.2.1 → 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 +14 -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 +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 +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
@@ -1,5 +1,12 @@
1
1
  import { z } from "zod";
2
2
  import { type ConsentServer, createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
3
+ import {
4
+ createJsonGetter,
5
+ createJsonPoster,
6
+ envAuthHeaders,
7
+ type JsonApiConfig,
8
+ requiredBaseUrl,
9
+ } from "../../../shared/env-json-api.ts";
3
10
  import { searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
4
11
  import { fetchWithTimeout, mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
5
12
  import {
@@ -8,47 +15,24 @@ import {
8
15
  } from "../../../shared/run-read-only-mcp-connector.ts";
9
16
  import { filterArgocdApplications } from "./search-filter.ts";
10
17
 
11
- function trimTrailingSlash(s: string): string {
12
- return s.endsWith("/") ? s.slice(0, -1) : s;
13
- }
14
-
15
18
  function apiBase(): string {
16
- const v = process.env["ARGOCD_URL"]?.trim();
17
- if (v === undefined || v === "") {
18
- throw new Error("ARGOCD_URL is not set");
19
- }
20
- return `${trimTrailingSlash(v)}/api/v1`;
21
- }
22
-
23
- function authHeader(): Record<string, string> {
24
- const t = process.env["ARGOCD_TOKEN"]?.trim();
25
- if (t === undefined || t === "") {
26
- throw new Error("ARGOCD_TOKEN is not set");
27
- }
28
- return { Authorization: `Bearer ${t}`, Accept: "application/json" };
19
+ return `${requiredBaseUrl("ARGOCD_URL")}/api/v1`;
29
20
  }
30
21
 
31
- async function agGet(path: string): Promise<unknown> {
32
- const res = await fetchWithTimeout(`${apiBase()}${path}`, { headers: authHeader() });
33
- const text = await res.text();
34
- if (!res.ok) {
35
- throw new Error(`ArgoCD ${String(res.status)}: ${text.slice(0, 400)}`);
36
- }
37
- return JSON.parse(text) as unknown;
38
- }
22
+ /**
23
+ * One config for reads and the mutating requests alike. `fetchWithTimeout`, not the global
24
+ * fetch: this is a self-hosted control plane, and one that stops answering must fail the tool
25
+ * call rather than hang it.
26
+ */
27
+ const api: JsonApiConfig = {
28
+ base: apiBase,
29
+ label: "ArgoCD",
30
+ headers: envAuthHeaders({ env: "ARGOCD_TOKEN" }),
31
+ fetch: fetchWithTimeout,
32
+ };
39
33
 
40
- async function agPost(path: string, body: unknown): Promise<unknown> {
41
- const res = await fetchWithTimeout(`${apiBase()}${path}`, {
42
- method: "POST",
43
- headers: { ...authHeader(), "Content-Type": "application/json" },
44
- body: JSON.stringify(body),
45
- });
46
- const text = await res.text();
47
- if (!res.ok) {
48
- throw new Error(`ArgoCD ${path} ${String(res.status)}: ${text.slice(0, 400)}`);
49
- }
50
- return text === "" ? {} : (JSON.parse(text) as unknown);
51
- }
34
+ const agGet = createJsonGetter(api);
35
+ const agPost = createJsonPoster(api);
52
36
 
53
37
  function applicationsFrom(root: unknown): unknown[] {
54
38
  if (Array.isArray(root)) {
@@ -63,7 +47,10 @@ export function registerArgocdTools(reg: ZodToolRegistrar, server: unknown): voi
63
47
  // kit needs the real server, which the helper now passes through as its second argument.
64
48
  const registerWriteTool = createWriteToolRegistrar(server as ConsentServer, {
65
49
  connector: "argocd",
66
- scopeEnv: "NIMBUS_MCP_APP_WRITE_SCOPE",
50
+ // NIMBUS_MCP_<SERVICE>_WRITE_SCOPE, as docs/configuration.md documents for every connector.
51
+ // This read NIMBUS_MCP_APP_WRITE_SCOPE (the scope KIND, not the service), so the documented
52
+ // variable was ignored and every argocd write refused as out of scope.
53
+ scopeEnv: "NIMBUS_MCP_ARGOCD_WRITE_SCOPE",
67
54
  scopeKinds: ["app"],
68
55
  });
69
56
 
@@ -1,18 +1,20 @@
1
1
  import { z } from "zod";
2
+ import {
3
+ asArray,
4
+ cliArg,
5
+ createCliJsonRunner,
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 { isSafeCliArg } from "../../../shared/safe-cli-arg.ts";
6
12
 
7
13
  /**
8
14
  * A catalog / database / table name passed as a value to the `aws athena` CLI.
9
15
  * Rejected at the schema boundary if it begins with `-` (argv flag smuggling) or
10
16
  * contains control characters.
11
17
  */
12
- const cliArg = z
13
- .string()
14
- .min(1)
15
- .refine(isSafeCliArg, { message: 'must not start with "-" or contain control characters' });
16
18
 
17
19
  /**
18
20
  * Amazon Athena (Tier-3, metadata-only) MCP tool surface. ALL tools index
@@ -27,40 +29,19 @@ export const ATHENA_TOOL_NAMES = ["athena_list", "athena_get", "athena_search"]
27
29
 
28
30
  const PAGE = "200";
29
31
 
30
- function isRecord(v: unknown): v is Record<string, unknown> {
31
- return v !== null && typeof v === "object" && !Array.isArray(v);
32
- }
33
-
34
- function strField(r: Record<string, unknown>, key: string): string {
35
- const v = r[key];
36
- return typeof v === "string" ? v : "";
37
- }
38
-
39
- function asArray(parsed: unknown, key: string): unknown[] {
40
- if (!isRecord(parsed)) {
41
- return [];
42
- }
43
- const arr = parsed[key];
44
- return Array.isArray(arr) ? arr : [];
45
- }
46
-
47
32
  /**
48
33
  * Run `aws athena <args> --output json` and parse stdout. The AWS CLI reads
49
34
  * AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_DEFAULT_REGION / AWS_PROFILE
50
35
  * natively from the process env injected at spawn time. Throws on non-zero exit.
51
36
  */
52
- async function athenaCli(args: string[]): Promise<unknown> {
53
- const { code, stdout, stderr } = await nimbusSpawn(
54
- ["aws", "athena", ...args, "--output", "json"],
55
- {},
56
- );
57
- const out = stdout.trim();
58
- const err = stderr.trim();
59
- if (code !== 0) {
60
- throw new Error(`aws athena ${args[0] ?? ""} failed: ${err.slice(0, 400)}`);
61
- }
62
- return out === "" ? {} : (JSON.parse(out) as unknown);
63
- }
37
+ const athenaCli = createCliJsonRunner(
38
+ {
39
+ argv: (args) => ["aws", "athena", ...args, "--output", "json"],
40
+ label: "aws athena",
41
+ emptyResult: {},
42
+ },
43
+ nimbusSpawn,
44
+ );
64
45
 
65
46
  function catalogMatches(entry: unknown, q: string): boolean {
66
47
  if (!isRecord(entry)) {
@@ -1,191 +1,10 @@
1
- import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
-
5
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
- import { z } from "zod";
8
- import {
9
- createRegisterSimpleTool,
10
- createZodToolRegistrar,
11
- mcpJsonResult as jsonResult,
12
- } from "../../../shared/mcp-tool-kit.ts";
13
- import { runCliJson, runCliOk } from "../../../shared/run-cli-json.ts";
14
-
15
- function awsEnv(): Record<string, string | undefined> {
16
- const e = { ...process.env } as Record<string, string | undefined>;
17
- const ak = process.env["AWS_ACCESS_KEY_ID"]?.trim();
18
- const sk = process.env["AWS_SECRET_ACCESS_KEY"]?.trim();
19
- const rg = process.env["AWS_DEFAULT_REGION"]?.trim();
20
- const profile = process.env["AWS_PROFILE"]?.trim();
21
- if (ak !== undefined && ak !== "") {
22
- e["AWS_ACCESS_KEY_ID"] = ak;
23
- }
24
- if (sk !== undefined && sk !== "") {
25
- e["AWS_SECRET_ACCESS_KEY"] = sk;
26
- }
27
- if (rg !== undefined && rg !== "") {
28
- e["AWS_DEFAULT_REGION"] = rg;
29
- }
30
- if (profile !== undefined && profile !== "") {
31
- e["AWS_PROFILE"] = profile;
32
- }
33
- return e;
34
- }
35
-
36
- async function awsJson(args: string[]): Promise<unknown> {
37
- const cmd = ["aws", ...args, "--output", "json"];
38
- const r = await runCliJson(cmd, awsEnv());
39
- if (!r.ok) {
40
- throw new Error(r.message);
41
- }
42
- return r.data ?? {};
43
- }
3
+ import { registerAwsTools } from "./tools.ts";
44
4
 
45
5
  const mcp = new McpServer({ name: "nimbus-aws", version: "0.1.0" });
46
6
 
47
- import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
48
-
49
- const reg = createZodToolRegistrar(createRegisterSimpleTool(mcp));
50
-
51
- /**
52
- * Every MUTATING aws 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: "aws",
58
- scopeEnv: "NIMBUS_MCP_AWS_WRITE_SCOPE",
59
- scopeKinds: ["cluster", "function"],
60
- });
61
-
62
- reg(
63
- "aws_ecs_service_list",
64
- "List ECS services in a cluster.",
65
- z.object({ cluster: z.string().min(1) }),
66
- async (p) => jsonResult(await awsJson(["ecs", "list-services", "--cluster", p.cluster])),
67
- );
68
-
69
- reg("aws_lambda_list", "List Lambda functions (first page).", z.object({}), async () =>
70
- jsonResult(await awsJson(["lambda", "list-functions"])),
71
- );
72
-
73
- registerWriteTool(
74
- "aws_ecs_service_update",
75
- {
76
- mutates: "aws.ecs.service.update",
77
- recoverable: true,
78
- scopeTargetOf: (p) => ({ kind: "cluster", value: p.cluster }),
79
- },
80
- "Update ECS service (e.g. new task definition).",
81
- z.object({
82
- cluster: z.string().min(1),
83
- service: z.string().min(1),
84
- taskDefinition: z.string().min(1),
85
- }),
86
- async (p) => {
87
- const cmd = [
88
- "aws",
89
- "ecs",
90
- "update-service",
91
- "--cluster",
92
- p.cluster,
93
- "--service",
94
- p.service,
95
- "--task-definition",
96
- p.taskDefinition,
97
- "--force-new-deployment",
98
- ];
99
- const r = await runCliOk(cmd, awsEnv());
100
- if (!r.ok) {
101
- throw new Error(r.message);
102
- }
103
- return jsonResult({ ok: true });
104
- },
105
- );
106
-
107
- registerWriteTool(
108
- "aws_lambda_invoke",
109
- {
110
- mutates: "aws.lambda.invoke",
111
- recoverable: true,
112
- scopeTargetOf: (p) => ({ kind: "function", value: p.functionName }),
113
- },
114
- "Invoke a Lambda function.",
115
- z.object({
116
- functionName: z.string().min(1),
117
- payloadJson: z.string().optional(),
118
- }),
119
- async (p) => {
120
- const dir = mkdtempSync(join(tmpdir(), "nimbus-aws-lambda-"));
121
- const outFile = join(dir, "response.json");
122
- if (p.payloadJson !== undefined && p.payloadJson !== "") {
123
- const pf = join(dir, "payload.json");
124
- writeFileSync(pf, p.payloadJson, "utf8");
125
- const cmd = [
126
- "aws",
127
- "lambda",
128
- "invoke",
129
- "--function-name",
130
- p.functionName,
131
- "--payload",
132
- `file://${pf}`,
133
- outFile,
134
- ];
135
- const r = await runCliOk(cmd, awsEnv());
136
- if (!r.ok) {
137
- throw new Error(r.message);
138
- }
139
- } else {
140
- const r = await runCliOk(
141
- ["aws", "lambda", "invoke", "--function-name", p.functionName, outFile],
142
- awsEnv(),
143
- );
144
- if (!r.ok) {
145
- throw new Error(r.message);
146
- }
147
- }
148
- let body: unknown;
149
- try {
150
- body = JSON.parse(readFileSync(outFile, "utf8")) as unknown;
151
- } catch {
152
- body = { ok: true };
153
- }
154
- return jsonResult(body);
155
- },
156
- );
157
-
158
- reg(
159
- "aws_ec2_instance_stop",
160
- "Stop EC2 instances. HITL.",
161
- z.object({ instanceIds: z.string().min(1) }),
162
- async (p) => {
163
- const r = await runCliOk(
164
- ["aws", "ec2", "stop-instances", "--instance-ids", p.instanceIds],
165
- awsEnv(),
166
- );
167
- if (!r.ok) {
168
- throw new Error(r.message);
169
- }
170
- return jsonResult({ ok: true });
171
- },
172
- );
173
-
174
- reg(
175
- "aws_ec2_instance_start",
176
- "Start EC2 instances. HITL.",
177
- z.object({ instanceIds: z.string().min(1) }),
178
- async (p) => {
179
- const r = await runCliOk(
180
- ["aws", "ec2", "start-instances", "--instance-ids", p.instanceIds],
181
- awsEnv(),
182
- );
183
- if (!r.ok) {
184
- throw new Error(r.message);
185
- }
186
- return jsonResult({ ok: true });
187
- },
188
- );
7
+ registerAwsTools(mcp);
189
8
 
190
9
  const transport = new StdioServerTransport();
191
10
  await mcp.connect(transport);
@@ -0,0 +1,197 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { z } from "zod";
5
+ import type { ConsentServer } from "../../../shared/consent-kit.ts";
6
+ import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
7
+ import {
8
+ createRegisterSimpleTool,
9
+ createZodToolRegistrar,
10
+ mcpJsonResult as jsonResult,
11
+ } from "../../../shared/mcp-tool-kit.ts";
12
+ import { runCliJsonThrowing, runCliOkThrowing } from "../../../shared/run-cli-json.ts";
13
+
14
+ function awsEnv(): Record<string, string | undefined> {
15
+ const e = { ...process.env } as Record<string, string | undefined>;
16
+ const ak = process.env["AWS_ACCESS_KEY_ID"]?.trim();
17
+ const sk = process.env["AWS_SECRET_ACCESS_KEY"]?.trim();
18
+ const rg = process.env["AWS_DEFAULT_REGION"]?.trim();
19
+ const profile = process.env["AWS_PROFILE"]?.trim();
20
+ if (ak !== undefined && ak !== "") {
21
+ e["AWS_ACCESS_KEY_ID"] = ak;
22
+ }
23
+ if (sk !== undefined && sk !== "") {
24
+ e["AWS_SECRET_ACCESS_KEY"] = sk;
25
+ }
26
+ if (rg !== undefined && rg !== "") {
27
+ e["AWS_DEFAULT_REGION"] = rg;
28
+ }
29
+ if (profile !== undefined && profile !== "") {
30
+ e["AWS_PROFILE"] = profile;
31
+ }
32
+ return e;
33
+ }
34
+
35
+ async function awsJson(args: string[]): Promise<unknown> {
36
+ const cmd = ["aws", ...args, "--output", "json"];
37
+ return (await runCliJsonThrowing(cmd, awsEnv())) ?? {};
38
+ }
39
+
40
+ /** Tool names exposed by this connector — for contract/introspection tests. */
41
+ export const AWS_TOOL_NAMES = [
42
+ "aws_ecs_service_list",
43
+ "aws_lambda_list",
44
+ "aws_ecs_service_update",
45
+ "aws_lambda_invoke",
46
+ "aws_ec2_instance_stop",
47
+ "aws_ec2_instance_start",
48
+ ] as const;
49
+
50
+ export function registerAwsTools(
51
+ server: ConsentServer & { tool: (...args: never) => unknown },
52
+ ): void {
53
+ const reg = createZodToolRegistrar(createRegisterSimpleTool(server));
54
+
55
+ /**
56
+ * Every MUTATING aws tool goes through here. Outside the gateway this adds the
57
+ * consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
58
+ * the gateway it is a pass-through, because executor.ts (I2) is the gate there.
59
+ */
60
+ const registerWriteTool = createWriteToolRegistrar(server, {
61
+ connector: "aws",
62
+ scopeEnv: "NIMBUS_MCP_AWS_WRITE_SCOPE",
63
+ scopeKinds: ["cluster", "function", "instance"],
64
+ });
65
+
66
+ reg(
67
+ "aws_ecs_service_list",
68
+ "List ECS services in a cluster.",
69
+ z.object({ cluster: z.string().min(1) }),
70
+ async (p) => jsonResult(await awsJson(["ecs", "list-services", "--cluster", p.cluster])),
71
+ );
72
+
73
+ reg("aws_lambda_list", "List Lambda functions (first page).", z.object({}), async () =>
74
+ jsonResult(await awsJson(["lambda", "list-functions"])),
75
+ );
76
+
77
+ registerWriteTool(
78
+ "aws_ecs_service_update",
79
+ {
80
+ mutates: "aws.ecs.service.update",
81
+ recoverable: true,
82
+ scopeTargetOf: (p) => ({ kind: "cluster", value: p.cluster }),
83
+ },
84
+ "Update ECS service (e.g. new task definition).",
85
+ z.object({
86
+ cluster: z.string().min(1),
87
+ service: z.string().min(1),
88
+ taskDefinition: z.string().min(1),
89
+ }),
90
+ async (p) => {
91
+ const cmd = [
92
+ "aws",
93
+ "ecs",
94
+ "update-service",
95
+ "--cluster",
96
+ p.cluster,
97
+ "--service",
98
+ p.service,
99
+ "--task-definition",
100
+ p.taskDefinition,
101
+ "--force-new-deployment",
102
+ ];
103
+ await runCliOkThrowing(cmd, awsEnv());
104
+ return jsonResult({ ok: true });
105
+ },
106
+ );
107
+
108
+ registerWriteTool(
109
+ "aws_lambda_invoke",
110
+ {
111
+ mutates: "aws.lambda.invoke",
112
+ recoverable: true,
113
+ scopeTargetOf: (p) => ({ kind: "function", value: p.functionName }),
114
+ },
115
+ "Invoke a Lambda function.",
116
+ z.object({
117
+ functionName: z.string().min(1),
118
+ payloadJson: z.string().optional(),
119
+ }),
120
+ async (p) => {
121
+ // The CLI reads the payload from a file and writes the response to one. Both are the
122
+ // caller's data, so the directory holding them is removed however the call ends — it used
123
+ // to be left in the temp dir on every invocation.
124
+ const dir = mkdtempSync(join(tmpdir(), "nimbus-aws-lambda-"));
125
+ try {
126
+ const outFile = join(dir, "response.json");
127
+ if (p.payloadJson !== undefined && p.payloadJson !== "") {
128
+ const pf = join(dir, "payload.json");
129
+ writeFileSync(pf, p.payloadJson, "utf8");
130
+ const cmd = [
131
+ "aws",
132
+ "lambda",
133
+ "invoke",
134
+ "--function-name",
135
+ p.functionName,
136
+ "--payload",
137
+ `file://${pf}`,
138
+ outFile,
139
+ ];
140
+ await runCliOkThrowing(cmd, awsEnv());
141
+ } else {
142
+ await runCliOkThrowing(
143
+ ["aws", "lambda", "invoke", "--function-name", p.functionName, outFile],
144
+ awsEnv(),
145
+ );
146
+ }
147
+ let body: unknown;
148
+ try {
149
+ body = JSON.parse(readFileSync(outFile, "utf8")) as unknown;
150
+ } catch {
151
+ body = { ok: true };
152
+ }
153
+ return jsonResult(body);
154
+ } finally {
155
+ rmSync(dir, { recursive: true, force: true });
156
+ }
157
+ },
158
+ );
159
+
160
+ // The two EC2 actions are WRITES. They were registered as reads, which in standalone mode offered
161
+ // them to every client with no consent prompt, scope check, budget or audit record.
162
+ registerWriteTool(
163
+ "aws_ec2_instance_stop",
164
+ {
165
+ mutates: "aws.ec2.instance.stop",
166
+ recoverable: true,
167
+ scopeTargetOf: (p) => ({ kind: "instance", value: p.instanceIds }),
168
+ },
169
+ "Stop EC2 instances.",
170
+ z.object({ instanceIds: z.string().min(1) }),
171
+ async (p) => {
172
+ await runCliOkThrowing(
173
+ ["aws", "ec2", "stop-instances", "--instance-ids", p.instanceIds],
174
+ awsEnv(),
175
+ );
176
+ return jsonResult({ ok: true });
177
+ },
178
+ );
179
+
180
+ registerWriteTool(
181
+ "aws_ec2_instance_start",
182
+ {
183
+ mutates: "aws.ec2.instance.start",
184
+ recoverable: true,
185
+ scopeTargetOf: (p) => ({ kind: "instance", value: p.instanceIds }),
186
+ },
187
+ "Start EC2 instances.",
188
+ z.object({ instanceIds: z.string().min(1) }),
189
+ async (p) => {
190
+ await runCliOkThrowing(
191
+ ["aws", "ec2", "start-instances", "--instance-ids", p.instanceIds],
192
+ awsEnv(),
193
+ );
194
+ return jsonResult({ ok: true });
195
+ },
196
+ );
197
+ }
@@ -1,139 +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 azEnv(): Record<string, string | undefined> {
12
- return { ...process.env } as Record<string, string | undefined>;
13
- }
14
-
15
- async function azJson(args: string[]): Promise<unknown> {
16
- const cmd = ["az", ...args, "-o", "json"];
17
- const r = await runCliJson(cmd, azEnv());
18
- if (!r.ok) {
19
- throw new Error(r.message);
20
- }
21
- return r.data ?? {};
22
- }
3
+ import { registerAzureTools } from "./tools.ts";
23
4
 
24
5
  const mcp = new McpServer({ name: "nimbus-azure", version: "0.1.0" });
25
6
 
26
- import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
27
-
28
- const reg = createZodToolRegistrar(createRegisterSimpleTool(mcp));
29
-
30
- /**
31
- * Every MUTATING azure tool goes through here. Outside the gateway this adds the
32
- * consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
33
- * the gateway it is a pass-through, because executor.ts (I2) is the gate there.
34
- */
35
- const registerWriteTool = createWriteToolRegistrar(mcp, {
36
- connector: "azure",
37
- scopeEnv: "NIMBUS_MCP_AZURE_WRITE_SCOPE",
38
- scopeKinds: ["resource_group"],
39
- });
40
-
41
- reg(
42
- "azure_app_service_list",
43
- "List App Services in a resource group.",
44
- z.object({
45
- subscriptionId: z.string().min(1),
46
- resourceGroup: z.string().min(1),
47
- }),
48
- async (p) =>
49
- jsonResult(
50
- await azJson([
51
- "webapp",
52
- "list",
53
- "--subscription",
54
- p.subscriptionId,
55
- "--resource-group",
56
- p.resourceGroup,
57
- ]),
58
- ),
59
- );
60
-
61
- registerWriteTool(
62
- "azure_app_service_restart",
63
- {
64
- mutates: "azure.app_service.restart",
65
- recoverable: true,
66
- scopeTargetOf: (p) => ({ kind: "resource_group", value: p.resourceGroup }),
67
- },
68
- "Restart an App Service.",
69
- z.object({
70
- subscriptionId: z.string().min(1),
71
- resourceGroup: z.string().min(1),
72
- name: z.string().min(1),
73
- }),
74
- async (p) => {
75
- const r = await runCliOk(
76
- [
77
- "az",
78
- "webapp",
79
- "restart",
80
- "--subscription",
81
- p.subscriptionId,
82
- "--resource-group",
83
- p.resourceGroup,
84
- "--name",
85
- p.name,
86
- ],
87
- azEnv(),
88
- );
89
- if (!r.ok) {
90
- throw new Error(r.message);
91
- }
92
- return jsonResult({ ok: true });
93
- },
94
- );
95
-
96
- registerWriteTool(
97
- "azure_aks_node_pool_scale",
98
- {
99
- mutates: "azure.aks.node_pool.scale",
100
- recoverable: true,
101
- scopeTargetOf: (p) => ({ kind: "resource_group", value: p.resourceGroup }),
102
- },
103
- "Scale an AKS node pool.",
104
- z.object({
105
- subscriptionId: z.string().min(1),
106
- resourceGroup: z.string().min(1),
107
- clusterName: z.string().min(1),
108
- poolName: z.string().min(1),
109
- nodeCount: z.number().int().min(0),
110
- }),
111
- async (p) => {
112
- const r = await runCliOk(
113
- [
114
- "az",
115
- "aks",
116
- "nodepool",
117
- "scale",
118
- "--subscription",
119
- p.subscriptionId,
120
- "--resource-group",
121
- p.resourceGroup,
122
- "--cluster-name",
123
- p.clusterName,
124
- "--name",
125
- p.poolName,
126
- "--node-count",
127
- String(p.nodeCount),
128
- ],
129
- azEnv(),
130
- );
131
- if (!r.ok) {
132
- throw new Error(r.message);
133
- }
134
- return jsonResult({ ok: true });
135
- },
136
- );
7
+ registerAzureTools(mcp);
137
8
 
138
9
  const transport = new StdioServerTransport();
139
10
  await mcp.connect(transport);