@nimbus-dev/connectors 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +18 -4
  2. package/connectors/airflow/src/search-filter.ts +2 -15
  3. package/connectors/airflow/src/server.ts +2 -87
  4. package/connectors/airflow/src/tools.ts +74 -0
  5. package/connectors/apple/src/adapters.ts +326 -0
  6. package/connectors/apple/src/apple-mail-core.ts +4 -8
  7. package/connectors/apple/src/calendar-tools.ts +28 -8
  8. package/connectors/apple/src/server.ts +23 -512
  9. package/connectors/apple/src/tools.ts +18 -44
  10. package/connectors/argocd/src/server.ts +25 -38
  11. package/connectors/athena/src/tools.ts +15 -34
  12. package/connectors/aws/src/server.ts +2 -183
  13. package/connectors/aws/src/tools.ts +197 -0
  14. package/connectors/azure/src/server.ts +2 -131
  15. package/connectors/azure/src/tools.ts +133 -0
  16. package/connectors/bigeye/src/search-filter.ts +4 -15
  17. package/connectors/bigeye/src/server.ts +38 -38
  18. package/connectors/bigquery/src/tools.ts +1 -26
  19. package/connectors/bitbucket/src/server.ts +2 -244
  20. package/connectors/bitbucket/src/tools.ts +241 -0
  21. package/connectors/bitrise/src/server.ts +2 -79
  22. package/connectors/bitrise/src/tools.ts +76 -0
  23. package/connectors/canva/src/search-filter.ts +2 -11
  24. package/connectors/canva/src/server.ts +2 -73
  25. package/connectors/canva/src/tools.ts +66 -0
  26. package/connectors/circleci/src/server.ts +2 -176
  27. package/connectors/circleci/src/tools.ts +193 -0
  28. package/connectors/cloud-logging/src/tools.ts +14 -27
  29. package/connectors/cloudwatch/src/tools.ts +15 -34
  30. package/connectors/codemagic/src/server.ts +2 -70
  31. package/connectors/codemagic/src/tools.ts +71 -0
  32. package/connectors/confluence/src/server.ts +2 -301
  33. package/connectors/confluence/src/tools.ts +320 -0
  34. package/connectors/dagster/src/server.ts +2 -214
  35. package/connectors/dagster/src/tools.ts +187 -0
  36. package/connectors/databricks/src/search-filter.ts +7 -7
  37. package/connectors/databricks/src/server.ts +2 -77
  38. package/connectors/databricks/src/tools.ts +66 -0
  39. package/connectors/datadog/src/server.ts +2 -48
  40. package/connectors/datadog/src/tools.ts +48 -0
  41. package/connectors/dataprofile/src/profile.ts +16 -46
  42. package/connectors/dbt/src/server.ts +2 -92
  43. package/connectors/dbt/src/tools.ts +80 -0
  44. package/connectors/dependencytrack/src/server.ts +2 -76
  45. package/connectors/dependencytrack/src/tools.ts +65 -0
  46. package/connectors/discord/src/server.ts +2 -85
  47. package/connectors/discord/src/tools.ts +82 -0
  48. package/connectors/elasticsearch/src/tools.ts +7 -37
  49. package/connectors/fastmail/src/jmap-client.ts +202 -0
  50. package/connectors/fastmail/src/jmap-core.ts +8 -21
  51. package/connectors/fastmail/src/server.ts +2 -187
  52. package/connectors/fastmail/src/tools.ts +4 -23
  53. package/connectors/figma/src/search-filter.ts +2 -11
  54. package/connectors/figma/src/server.ts +2 -113
  55. package/connectors/figma/src/tools.ts +99 -0
  56. package/connectors/firebase/src/server.ts +2 -90
  57. package/connectors/firebase/src/token.ts +2 -5
  58. package/connectors/firebase/src/tools.ts +106 -0
  59. package/connectors/flagsmith/src/server.ts +2 -98
  60. package/connectors/flagsmith/src/tools.ts +95 -0
  61. package/connectors/flux/src/server.ts +52 -53
  62. package/connectors/gcp/src/server.ts +2 -132
  63. package/connectors/gcp/src/tools.ts +123 -0
  64. package/connectors/github/src/server.ts +2 -328
  65. package/connectors/github/src/tools.ts +311 -0
  66. package/connectors/github-actions/src/server.ts +2 -205
  67. package/connectors/github-actions/src/tools.ts +209 -0
  68. package/connectors/gitlab/src/server.ts +2 -384
  69. package/connectors/gitlab/src/tools.ts +392 -0
  70. package/connectors/gmail/src/server.ts +2 -270
  71. package/connectors/gmail/src/tools.ts +209 -0
  72. package/connectors/google-drive/src/server.ts +2 -524
  73. package/connectors/google-drive/src/tools.ts +529 -0
  74. package/connectors/google-meet/src/server.ts +2 -82
  75. package/connectors/google-meet/src/tools.ts +87 -0
  76. package/connectors/google-photos/src/server.ts +2 -132
  77. package/connectors/google-photos/src/tools.ts +138 -0
  78. package/connectors/grafana/src/server.ts +2 -53
  79. package/connectors/grafana/src/tools.ts +53 -0
  80. package/connectors/great-expectations/src/gx-parse.ts +14 -45
  81. package/connectors/greenhouse/src/server.ts +2 -63
  82. package/connectors/greenhouse/src/tools.ts +68 -0
  83. package/connectors/hubspot/src/server.ts +2 -77
  84. package/connectors/hubspot/src/tools.ts +70 -0
  85. package/connectors/iac/src/server.ts +2 -137
  86. package/connectors/iac/src/tools.ts +152 -0
  87. package/connectors/imap/src/imap-core.ts +29 -82
  88. package/connectors/imap/src/server.ts +33 -220
  89. package/connectors/intercom/src/server.ts +2 -61
  90. package/connectors/intercom/src/tools.ts +58 -0
  91. package/connectors/jenkins/src/jenkins-api.ts +2 -6
  92. package/connectors/jenkins/src/server.ts +2 -215
  93. package/connectors/jenkins/src/tools.ts +240 -0
  94. package/connectors/jira/src/server.ts +2 -301
  95. package/connectors/jira/src/tools.ts +325 -0
  96. package/connectors/kubernetes/src/server.ts +2 -155
  97. package/connectors/kubernetes/src/tools.ts +159 -0
  98. package/connectors/launchdarkly/src/server.ts +2 -75
  99. package/connectors/launchdarkly/src/tools.ts +72 -0
  100. package/connectors/lever/src/search-filter.ts +4 -12
  101. package/connectors/lever/src/server.ts +2 -60
  102. package/connectors/lever/src/tools.ts +61 -0
  103. package/connectors/linear/src/server.ts +2 -459
  104. package/connectors/linear/src/tools.ts +482 -0
  105. package/connectors/localdb/src/sql-scan.ts +13 -41
  106. package/connectors/localdb/src/tools.ts +1 -1
  107. package/connectors/looker/src/search-filter.ts +2 -15
  108. package/connectors/looker/src/server.ts +35 -70
  109. package/connectors/mendeley/src/server.ts +2 -55
  110. package/connectors/mendeley/src/tools.ts +54 -0
  111. package/connectors/mercury/src/server.ts +2 -57
  112. package/connectors/mercury/src/tools.ts +50 -0
  113. package/connectors/metabase/src/search-filter.ts +4 -15
  114. package/connectors/metabase/src/server.ts +2 -79
  115. package/connectors/metabase/src/tools.ts +64 -0
  116. package/connectors/miro/src/server.ts +2 -72
  117. package/connectors/miro/src/tools.ts +65 -0
  118. package/connectors/mlflow/src/search-filter.ts +3 -7
  119. package/connectors/mlflow/src/server.ts +20 -40
  120. package/connectors/monte-carlo/src/server.ts +50 -57
  121. package/connectors/netlify/src/search-filter.ts +6 -14
  122. package/connectors/netlify/src/server.ts +2 -60
  123. package/connectors/netlify/src/tools.ts +53 -0
  124. package/connectors/newrelic/src/server.ts +2 -40
  125. package/connectors/newrelic/src/tools.ts +37 -0
  126. package/connectors/notion/src/server.ts +2 -335
  127. package/connectors/notion/src/tools.ts +344 -0
  128. package/connectors/obsidian/src/server.ts +2 -425
  129. package/connectors/obsidian/src/tools.ts +466 -0
  130. package/connectors/onedrive/src/server.ts +2 -249
  131. package/connectors/onedrive/src/tools.ts +253 -0
  132. package/connectors/outlook/src/server.ts +2 -351
  133. package/connectors/outlook/src/tools.ts +357 -0
  134. package/connectors/pagerduty/src/server.ts +2 -132
  135. package/connectors/pagerduty/src/tools.ts +126 -0
  136. package/connectors/pipedrive/src/server.ts +2 -58
  137. package/connectors/pipedrive/src/tools.ts +63 -0
  138. package/connectors/powerbi/src/search-filter.ts +2 -15
  139. package/connectors/powerbi/src/server.ts +43 -68
  140. package/connectors/prefect/src/search-filter.ts +3 -16
  141. package/connectors/prefect/src/server.ts +2 -90
  142. package/connectors/prefect/src/tools.ts +83 -0
  143. package/connectors/protonmail/src/mail-core.ts +16 -75
  144. package/connectors/protonmail/src/server.ts +34 -214
  145. package/connectors/raindrop/src/server.ts +2 -103
  146. package/connectors/raindrop/src/tools.ts +103 -0
  147. package/connectors/ramp/src/server.ts +2 -105
  148. package/connectors/ramp/src/tools.ts +98 -0
  149. package/connectors/readwise/src/server.ts +2 -88
  150. package/connectors/readwise/src/tools.ts +81 -0
  151. package/connectors/sagemaker/src/tools.ts +19 -38
  152. package/connectors/salesforce/src/search-filter.ts +4 -11
  153. package/connectors/salesforce/src/server.ts +2 -90
  154. package/connectors/salesforce/src/tools.ts +75 -0
  155. package/connectors/semgrep/src/search-filter.ts +3 -10
  156. package/connectors/semgrep/src/server.ts +2 -104
  157. package/connectors/semgrep/src/tools.ts +92 -0
  158. package/connectors/sentry/src/server.ts +2 -58
  159. package/connectors/sentry/src/tools.ts +53 -0
  160. package/connectors/slack/src/server.ts +2 -352
  161. package/connectors/slack/src/tools.ts +376 -0
  162. package/connectors/snowflake/src/search-filter.ts +4 -19
  163. package/connectors/snowflake/src/server.ts +6 -25
  164. package/connectors/snyk/src/server.ts +2 -140
  165. package/connectors/snyk/src/tools.ts +136 -0
  166. package/connectors/sonarqube/src/server.ts +2 -111
  167. package/connectors/sonarqube/src/tools.ts +98 -0
  168. package/connectors/stackoverflow/src/server.ts +2 -77
  169. package/connectors/stackoverflow/src/tools.ts +66 -0
  170. package/connectors/storybook/src/storybook-parse.ts +3 -6
  171. package/connectors/stripe/src/server.ts +2 -61
  172. package/connectors/stripe/src/tools.ts +54 -0
  173. package/connectors/superset/src/search-filter.ts +4 -15
  174. package/connectors/superset/src/server.ts +2 -115
  175. package/connectors/superset/src/tools.ts +98 -0
  176. package/connectors/tableau/src/search-filter.ts +2 -15
  177. package/connectors/tableau/src/server.ts +13 -65
  178. package/connectors/teams/src/server.ts +2 -249
  179. package/connectors/teams/src/tools.ts +283 -0
  180. package/connectors/testflight/src/server.ts +2 -70
  181. package/connectors/testflight/src/tools.ts +79 -0
  182. package/connectors/vercel/src/search-filter.ts +2 -10
  183. package/connectors/vercel/src/server.ts +2 -75
  184. package/connectors/vercel/src/tools.ts +73 -0
  185. package/connectors/vertex-ai/src/tools.ts +23 -33
  186. package/connectors/wiz/src/search-filter.ts +6 -30
  187. package/connectors/wiz/src/server.ts +2 -188
  188. package/connectors/wiz/src/tools.ts +168 -0
  189. package/connectors/zendesk/src/server.ts +2 -73
  190. package/connectors/zendesk/src/tools.ts +60 -0
  191. package/connectors/zoom/src/search-filter.ts +1 -7
  192. package/connectors/zoom/src/server.ts +2 -94
  193. package/connectors/zoom/src/tools.ts +93 -0
  194. package/connectors/zotero/src/server.ts +2 -78
  195. package/connectors/zotero/src/tools.ts +67 -0
  196. package/package.json +15 -12
  197. package/shared/access-token-cache.ts +123 -0
  198. package/shared/atlassian-json-fetch.ts +6 -16
  199. package/shared/audit-chain.ts +13 -6
  200. package/shared/cli-json-kit.ts +150 -0
  201. package/shared/collection-tool-kit.ts +143 -0
  202. package/shared/connector-mode.ts +3 -2
  203. package/shared/cursor-list-tool.ts +29 -0
  204. package/shared/env-json-api.ts +209 -0
  205. package/shared/fetch-json-text.ts +27 -0
  206. package/shared/github-rest.ts +62 -0
  207. package/shared/graphql-json.ts +60 -0
  208. package/shared/imap-mail-core.ts +91 -0
  209. package/shared/imap-tool-kit.ts +57 -23
  210. package/shared/imapflow-adapter.ts +417 -0
  211. package/shared/rest-tool-kit.ts +77 -7
  212. package/shared/run-cli-json.ts +38 -16
  213. package/shared/search-filter.ts +58 -18
  214. package/shared/walk-files.ts +118 -0
  215. package/standalone/src/launcher.ts +19 -7
@@ -0,0 +1,65 @@
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 { filterMiroBoards } from "./search-filter.ts";
7
+
8
+ const BASE = "https://api.miro.com";
9
+
10
+ const miroGet = createJsonGetter({
11
+ base: BASE,
12
+ label: "Miro",
13
+ headers: envAuthHeaders({ env: "MIRO_TOKEN" }),
14
+ });
15
+
16
+ function boardsFrom(root: unknown): unknown[] {
17
+ const data = (root as { data?: unknown } | null)?.data;
18
+ return Array.isArray(data) ? data : [];
19
+ }
20
+
21
+ /** Tool names exposed by this connector — for contract/introspection tests. */
22
+ export const MIRO_TOOL_NAMES = ["miro_get", "miro_list", "miro_search"] as const;
23
+
24
+ export function registerMiroTools(reg: ZodToolRegistrar): void {
25
+ reg(
26
+ "miro_list",
27
+ "List the authenticated user's Miro boards (GET /v2/boards?limit=N&cursor=<cursor>). Returns the { data: [...], cursor?, total, size } envelope — `data` holds the board objects, each shaped { id, name, description, createdAt, modifiedAt, owner: { name }, viewLink, ... }; `cursor` (when present) is the opaque token for the next page. `limit` (default 50, max 50) caps the page_size of the single first-page request.",
28
+ z.object({
29
+ limit: z.number().int().min(1).max(50).optional(),
30
+ cursor: z.string().optional(),
31
+ }),
32
+ async (p) => {
33
+ const params = new URLSearchParams({ limit: String(p.limit ?? 50) });
34
+ if (p.cursor !== undefined && p.cursor !== "") {
35
+ params.set("cursor", p.cursor);
36
+ }
37
+ return jsonResult(await miroGet(`/v2/boards?${params.toString()}`));
38
+ },
39
+ );
40
+
41
+ reg(
42
+ "miro_get",
43
+ "Fetch one Miro board by its id (GET /v2/boards/{boardId}). Returns the board object directly (NOT wrapped in `data`). Throws when no board with that id exists.",
44
+ z.object({
45
+ id: z.string().min(1),
46
+ }),
47
+ async (p) => {
48
+ return jsonResult(await miroGet(`/v2/boards/${encodeURIComponent(p.id)}`));
49
+ },
50
+ );
51
+
52
+ reg(
53
+ "miro_search",
54
+ "**Substring search over the FIRST PAGE only** (up to 50 most-recently-listed boards) of the authenticated user's Miro boards. This tool fetches GET /v2/boards?limit=50 once and matches the query locally (case-insensitive) against the board name, description, and owner name. **Boards beyond the first page are not searchable here — query the local Nimbus index instead for full coverage.** Returns a { matches: [...] } envelope.",
55
+ searchToolInputSchema(50),
56
+ async (p) => {
57
+ const root = await miroGet("/v2/boards?limit=50");
58
+ const matches = filterMiroBoards(boardsFrom(root), {
59
+ query: p.query,
60
+ limit: p.limit,
61
+ });
62
+ return jsonResult({ matches });
63
+ },
64
+ );
65
+ }
@@ -2,15 +2,11 @@ import {
2
2
  asRecord,
3
3
  makeQueryFilter,
4
4
  type SearchMatchOptions,
5
+ stringField,
5
6
  } from "../../../shared/search-filter.ts";
6
7
 
7
8
  export type MlflowSearchMatchOptions = SearchMatchOptions;
8
9
 
9
- function stringAt(row: Record<string, unknown>, key: string): string {
10
- const v = row[key];
11
- return typeof v === "string" ? v : "";
12
- }
13
-
14
10
  function tagsHaystack(row: Record<string, unknown>): string {
15
11
  const tags = row["tags"];
16
12
  if (!Array.isArray(tags)) {
@@ -22,7 +18,7 @@ function tagsHaystack(row: Record<string, unknown>): string {
22
18
  if (tag === undefined) {
23
19
  continue;
24
20
  }
25
- parts.push(`${stringAt(tag, "key")}=${stringAt(tag, "value")}`);
21
+ parts.push(`${stringField(tag, "key")}=${stringField(tag, "value")}`);
26
22
  }
27
23
  return parts.join(" ");
28
24
  }
@@ -32,7 +28,7 @@ function fieldsOf(item: unknown): readonly string[] | null {
32
28
  if (row === undefined) {
33
29
  return null;
34
30
  }
35
- return [stringAt(row, "name"), stringAt(row, "description"), tagsHaystack(row)];
31
+ return [stringField(row, "name"), stringField(row, "description"), tagsHaystack(row)];
36
32
  }
37
33
 
38
34
  export const filterMlflowModels = makeQueryFilter(fieldsOf);
@@ -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,20 @@ import {
8
15
  } from "../../../shared/run-read-only-mcp-connector.ts";
9
16
  import { filterMlflowModels } from "./search-filter.ts";
10
17
 
11
- function trimTrailingSlash(s: string): string {
12
- return s.endsWith("/") ? s.slice(0, -1) : s;
13
- }
14
-
15
- function apiBase(): string {
16
- const v = process.env["MLFLOW_HOST"]?.trim();
17
- if (v === undefined || v === "") {
18
- throw new Error("MLFLOW_HOST is not set");
19
- }
20
- return trimTrailingSlash(v);
21
- }
22
-
23
- function authHeader(): Record<string, string> {
24
- const k = process.env["MLFLOW_TOKEN"]?.trim();
25
- if (k === undefined || k === "") {
26
- throw new Error("MLFLOW_TOKEN is not set");
27
- }
28
- return { Authorization: `Bearer ${k}`, Accept: "application/json" };
29
- }
30
-
31
- async function mlflowGet(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(`MLflow ${String(res.status)}: ${text.slice(0, 400)}`);
36
- }
37
- return JSON.parse(text) as unknown;
38
- }
18
+ /**
19
+ * One config for reads and the mutating requests alike. `fetchWithTimeout`, not the global
20
+ * fetch: MLflow is a self-hosted tracking server, and one that stops answering must fail the
21
+ * tool call rather than hang it.
22
+ */
23
+ const api: JsonApiConfig = {
24
+ base: () => requiredBaseUrl("MLFLOW_HOST"),
25
+ label: "MLflow",
26
+ headers: envAuthHeaders({ env: "MLFLOW_TOKEN" }),
27
+ fetch: fetchWithTimeout,
28
+ };
39
29
 
40
- async function mlflowPost(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(`MLflow ${path} ${String(res.status)}: ${text.slice(0, 400)}`);
49
- }
50
- return text === "" ? {} : (JSON.parse(text) as unknown);
51
- }
30
+ const mlflowGet = createJsonGetter(api);
31
+ const mlflowPost = createJsonPoster(api);
52
32
 
53
33
  const TRANSITION_PATH = "/api/2.0/mlflow/model-versions/transition-stage";
54
34
 
@@ -1,11 +1,14 @@
1
1
  import { z } from "zod";
2
2
  import { type ConsentServer, createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
3
+ import { cursorListInputSchema } from "../../../shared/cursor-list-tool.ts";
4
+ import { requiredEnv } from "../../../shared/env-json-api.ts";
3
5
  import { searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
4
6
  import { fetchWithTimeout, mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
5
7
  import {
6
8
  runReadOnlyMcpConnector,
7
9
  type ZodToolRegistrar,
8
10
  } from "../../../shared/run-read-only-mcp-connector.ts";
11
+ import { asRecord } from "../../../shared/search-filter.ts";
9
12
  import { filterMonteCarloIncidents } from "./search-filter.ts";
10
13
 
11
14
  const GRAPHQL_URL = "https://api.getmontecarlo.com/graphql";
@@ -35,20 +38,6 @@ const GET_INCIDENTS_QUERY = `
35
38
  }
36
39
  `.trim();
37
40
 
38
- function requireEnv(name: string): string {
39
- const v = process.env[name]?.trim();
40
- if (v === undefined || v === "") {
41
- throw new Error(`${name} is not set`);
42
- }
43
- return v;
44
- }
45
-
46
- function asObject(value: unknown): Record<string, unknown> | null {
47
- return value !== null && typeof value === "object" && !Array.isArray(value)
48
- ? (value as Record<string, unknown>)
49
- : null;
50
- }
51
-
52
41
  /**
53
42
  * Single POST helper for the Monte Carlo GraphQL endpoint. Sends `{ query, variables }` with the
54
43
  * `x-mcd-id`/`x-mcd-token` key-pair auth headers, throws on a non-ok HTTP status or a non-empty
@@ -75,10 +64,10 @@ async function mcGraphql(
75
64
  if (!res.ok) {
76
65
  throw new Error(`Monte Carlo API error ${String(res.status)}: ${text.slice(0, 400)}`);
77
66
  }
78
- const parsed = asObject(JSON.parse(text) as unknown) ?? {};
67
+ const parsed = asRecord(JSON.parse(text) as unknown) ?? {};
79
68
  const errors = parsed["errors"];
80
69
  if (Array.isArray(errors) && errors.length > 0) {
81
- const first = asObject(errors[0])?.["message"];
70
+ const first = asRecord(errors[0])?.["message"];
82
71
  const message = typeof first === "string" ? first : JSON.stringify(errors[0]);
83
72
  throw new Error(`Monte Carlo GraphQL error: ${message}`);
84
73
  }
@@ -92,14 +81,14 @@ interface IncidentsPage {
92
81
  }
93
82
 
94
83
  function parseIncidentsPage(parsed: unknown): IncidentsPage {
95
- const getIncidents = asObject(asObject(asObject(parsed)?.["data"])?.["getIncidents"]);
84
+ const getIncidents = asRecord(asRecord(asRecord(parsed)?.["data"])?.["getIncidents"]);
96
85
  const edges = Array.isArray(getIncidents?.["edges"]) ? (getIncidents["edges"] as unknown[]) : [];
97
86
  const incidents: unknown[] = [];
98
87
  for (const edge of edges) {
99
- const node = asObject(edge)?.["node"];
88
+ const node = asRecord(edge)?.["node"];
100
89
  if (node !== undefined) incidents.push(node);
101
90
  }
102
- const pageInfo = asObject(getIncidents?.["pageInfo"]);
91
+ const pageInfo = asRecord(getIncidents?.["pageInfo"]);
103
92
  const hasNextPage = pageInfo?.["hasNextPage"] === true;
104
93
  const endCursor = typeof pageInfo?.["endCursor"] === "string" ? pageInfo["endCursor"] : null;
105
94
  return { incidents, hasNextPage, endCursor };
@@ -151,13 +140,10 @@ export function registerMonteCarloTools(reg: ZodToolRegistrar, server: unknown):
151
140
  reg(
152
141
  "montecarlo_list",
153
142
  "List Monte Carlo data-quality incidents (relay GraphQL `getIncidents`). Paginated: `cursor` (the relay `after` token) + `limit` (default 200, max 500) → `{ items, nextCursor }`.",
154
- z.object({
155
- cursor: z.string().nullable().optional(),
156
- limit: z.number().int().min(1).max(500).optional(),
157
- }),
143
+ cursorListInputSchema(),
158
144
  async (p) => {
159
- const apiId = requireEnv("MONTECARLO_API_ID");
160
- const apiToken = requireEnv("MONTECARLO_API_TOKEN");
145
+ const apiId = requiredEnv("MONTECARLO_API_ID");
146
+ const apiToken = requiredEnv("MONTECARLO_API_TOKEN");
161
147
  const first = p.limit ?? 200;
162
148
  const after = p.cursor === undefined || p.cursor === "" ? null : p.cursor;
163
149
  const page = await fetchIncidentsPage(apiId, apiToken, first, after);
@@ -173,10 +159,10 @@ export function registerMonteCarloTools(reg: ZodToolRegistrar, server: unknown):
173
159
  id: z.string().min(1),
174
160
  }),
175
161
  async (p) => {
176
- const apiId = requireEnv("MONTECARLO_API_ID");
177
- const apiToken = requireEnv("MONTECARLO_API_TOKEN");
162
+ const apiId = requiredEnv("MONTECARLO_API_ID");
163
+ const apiToken = requiredEnv("MONTECARLO_API_TOKEN");
178
164
  const incidents = await fetchIncidents(apiId, apiToken);
179
- const found = incidents.find((inc) => asObject(inc)?.["incidentId"] === p.id);
165
+ const found = incidents.find((inc) => asRecord(inc)?.["incidentId"] === p.id);
180
166
  if (found === undefined) {
181
167
  throw new Error(`Monte Carlo incident not found: ${p.id}`);
182
168
  }
@@ -189,46 +175,53 @@ export function registerMonteCarloTools(reg: ZodToolRegistrar, server: unknown):
189
175
  "Substring search across Monte Carlo incidents. Matches the query (case-insensitive) against incidentId, status, severity, and monitoredTable. Returns a `{ matches: [...] }` envelope.",
190
176
  searchToolInputSchema(200),
191
177
  async (p) => {
192
- const apiId = requireEnv("MONTECARLO_API_ID");
193
- const apiToken = requireEnv("MONTECARLO_API_TOKEN");
178
+ const apiId = requiredEnv("MONTECARLO_API_ID");
179
+ const apiToken = requiredEnv("MONTECARLO_API_TOKEN");
194
180
  const incidents = await fetchIncidents(apiId, apiToken);
195
181
  const matches = filterMonteCarloIncidents(incidents, { query: p.query, limit: p.limit });
196
182
  return jsonResult({ matches });
197
183
  },
198
184
  );
199
185
 
200
- registerWriteTool(
186
+ /**
187
+ * Acknowledge and resolve are ONE mutation, `setIncidentFeedback`, with a different feedback
188
+ * value, so they share everything except their name, action type, description and that value.
189
+ */
190
+ function registerFeedbackTool(
191
+ name: string,
192
+ mutates: string,
193
+ description: string,
194
+ feedback: "ACKNOWLEDGED" | "RESOLVED",
195
+ ): void {
196
+ registerWriteTool(
197
+ name,
198
+ {
199
+ mutates,
200
+ recoverable: true,
201
+ scopeTargetOf: (p) => ({ kind: "incident", value: p.incidentId }),
202
+ },
203
+ description,
204
+ z.object({ incidentId: z.string().min(1) }),
205
+ async (p) => {
206
+ const apiId = requiredEnv("MONTECARLO_API_ID");
207
+ const apiToken = requiredEnv("MONTECARLO_API_TOKEN");
208
+ await setIncidentFeedback(apiId, apiToken, p.incidentId, feedback);
209
+ return jsonResult({ status: "ok", incidentId: p.incidentId });
210
+ },
211
+ );
212
+ }
213
+
214
+ registerFeedbackTool(
201
215
  "montecarlo_incident_acknowledge",
202
- {
203
- mutates: "montecarlo.incident.acknowledge",
204
- recoverable: true,
205
- scopeTargetOf: (p) => ({ kind: "incident", value: p.incidentId }),
206
- },
216
+ "montecarlo.incident.acknowledge",
207
217
  "Acknowledge a Monte Carlo incident.",
208
- z.object({ incidentId: z.string().min(1) }),
209
- async (p) => {
210
- const apiId = requireEnv("MONTECARLO_API_ID");
211
- const apiToken = requireEnv("MONTECARLO_API_TOKEN");
212
- await setIncidentFeedback(apiId, apiToken, p.incidentId, "ACKNOWLEDGED");
213
- return jsonResult({ status: "ok", incidentId: p.incidentId });
214
- },
218
+ "ACKNOWLEDGED",
215
219
  );
216
-
217
- registerWriteTool(
220
+ registerFeedbackTool(
218
221
  "montecarlo_incident_resolve",
219
- {
220
- mutates: "montecarlo.incident.resolve",
221
- recoverable: true,
222
- scopeTargetOf: (p) => ({ kind: "incident", value: p.incidentId }),
223
- },
222
+ "montecarlo.incident.resolve",
224
223
  "Resolve a Monte Carlo incident.",
225
- z.object({ incidentId: z.string().min(1) }),
226
- async (p) => {
227
- const apiId = requireEnv("MONTECARLO_API_ID");
228
- const apiToken = requireEnv("MONTECARLO_API_TOKEN");
229
- await setIncidentFeedback(apiId, apiToken, p.incidentId, "RESOLVED");
230
- return jsonResult({ status: "ok", incidentId: p.incidentId });
231
- },
224
+ "RESOLVED",
232
225
  );
233
226
  }
234
227
 
@@ -1,21 +1,13 @@
1
1
  import {
2
2
  asObjectish,
3
3
  makeQueryFilter,
4
+ nestedString,
4
5
  type SearchMatchOptions,
5
6
  stringField,
6
7
  } from "../../../shared/search-filter.ts";
7
8
 
8
9
  export type NetlifySearchMatchOptions = SearchMatchOptions;
9
10
 
10
- function subStringField(row: Record<string, unknown>, key: string, sub: string): string {
11
- const obj = row[key];
12
- if (obj === null || typeof obj !== "object" || Array.isArray(obj)) {
13
- return "";
14
- }
15
- const v = (obj as Record<string, unknown>)[sub];
16
- return typeof v === "string" ? v : "";
17
- }
18
-
19
11
  function fieldsOf(item: unknown): readonly string[] | null {
20
12
  const row = asObjectish(item);
21
13
  if (row === undefined) {
@@ -26,11 +18,11 @@ function fieldsOf(item: unknown): readonly string[] | null {
26
18
  stringField(row, "name"),
27
19
  stringField(row, "url"),
28
20
  stringField(row, "ssl_url"),
29
- subStringField(row, "build_settings", "repo_url"),
30
- subStringField(row, "build_settings", "repo_branch"),
31
- subStringField(row, "published_deploy", "state"),
32
- subStringField(row, "published_deploy", "branch"),
33
- subStringField(row, "published_deploy", "commit_ref"),
21
+ nestedString(row, ["build_settings", "repo_url"]),
22
+ nestedString(row, ["build_settings", "repo_branch"]),
23
+ nestedString(row, ["published_deploy", "state"]),
24
+ nestedString(row, ["published_deploy", "branch"]),
25
+ nestedString(row, ["published_deploy", "commit_ref"]),
34
26
  ];
35
27
  }
36
28
 
@@ -1,64 +1,6 @@
1
- import { z } from "zod";
2
- import { matchesResult, 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 { filterNetlifySites } from "./search-filter.ts";
6
-
7
- const BASE = "https://api.netlify.com";
8
-
9
- function token(): string {
10
- const t = process.env["NETLIFY_TOKEN"]?.trim();
11
- if (t === undefined || t === "") {
12
- throw new Error("NETLIFY_TOKEN is not set");
13
- }
14
- return t;
15
- }
16
-
17
- function authHeader(): Record<string, string> {
18
- return { Authorization: `Bearer ${token()}`, Accept: "application/json" };
19
- }
20
-
21
- async function netlifyGet(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(`Netlify ${String(res.status)}: ${text.slice(0, 400)}`);
26
- }
27
- return JSON.parse(text) as unknown;
28
- }
2
+ import { registerNetlifyTools } from "./tools.ts";
29
3
 
30
4
  await runReadOnlyMcpConnector("nimbus-netlify", (reg) => {
31
- reg(
32
- "netlify_list",
33
- "List Netlify sites (`GET /api/v1/sites`), capped at `per_page` (default 100). Returns a bare JSON array of site objects, each carrying the embedded `published_deploy` status and `build_settings`.",
34
- z.object({
35
- per_page: z.number().int().min(1).max(100).optional(),
36
- }),
37
- async (p) => {
38
- const search = new URLSearchParams({ per_page: String(p.per_page ?? 100), page: "1" });
39
- return jsonResult(await netlifyGet(`/api/v1/sites?${search.toString()}`));
40
- },
41
- );
42
-
43
- reg(
44
- "netlify_get",
45
- "Fetch one Netlify site by its id (`GET /api/v1/sites/{siteId}`). Returns the site object directly. Throws when no match is found.",
46
- z.object({
47
- siteId: z.string().min(1),
48
- }),
49
- async (p) => {
50
- return jsonResult(await netlifyGet(`/api/v1/sites/${encodeURIComponent(p.siteId)}`));
51
- },
52
- );
53
-
54
- reg(
55
- "netlify_search",
56
- "Substring search across Netlify sites. Matches the query against id, name, url, ssl_url, the linked git repo + branch, and the published-deploy state / branch / commit ref (case-insensitive). Returns a `{ matches: [...] }` envelope.",
57
- searchToolInputSchema(100),
58
- async (p) => {
59
- const search = new URLSearchParams({ per_page: "100", page: "1" });
60
- const root = await netlifyGet(`/api/v1/sites?${search.toString()}`);
61
- return matchesResult(root, filterNetlifySites, p);
62
- },
63
- );
5
+ registerNetlifyTools(reg);
64
6
  });
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+ import { createJsonGetter, envAuthHeaders } from "../../../shared/env-json-api.ts";
3
+ import { matchesResult, 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 { filterNetlifySites } from "./search-filter.ts";
7
+
8
+ const BASE = "https://api.netlify.com";
9
+
10
+ const netlifyGet = createJsonGetter({
11
+ base: BASE,
12
+ label: "Netlify",
13
+ headers: envAuthHeaders({ env: "NETLIFY_TOKEN" }),
14
+ });
15
+
16
+ /** Tool names exposed by this connector — for contract/introspection tests. */
17
+ export const NETLIFY_TOOL_NAMES = ["netlify_get", "netlify_list", "netlify_search"] as const;
18
+
19
+ export function registerNetlifyTools(reg: ZodToolRegistrar): void {
20
+ reg(
21
+ "netlify_list",
22
+ "List Netlify sites (`GET /api/v1/sites`), capped at `per_page` (default 100). Returns a bare JSON array of site objects, each carrying the embedded `published_deploy` status and `build_settings`.",
23
+ z.object({
24
+ per_page: z.number().int().min(1).max(100).optional(),
25
+ }),
26
+ async (p) => {
27
+ const search = new URLSearchParams({ per_page: String(p.per_page ?? 100), page: "1" });
28
+ return jsonResult(await netlifyGet(`/api/v1/sites?${search.toString()}`));
29
+ },
30
+ );
31
+
32
+ reg(
33
+ "netlify_get",
34
+ "Fetch one Netlify site by its id (`GET /api/v1/sites/{siteId}`). Returns the site object directly. Throws when no match is found.",
35
+ z.object({
36
+ siteId: z.string().min(1),
37
+ }),
38
+ async (p) => {
39
+ return jsonResult(await netlifyGet(`/api/v1/sites/${encodeURIComponent(p.siteId)}`));
40
+ },
41
+ );
42
+
43
+ reg(
44
+ "netlify_search",
45
+ "Substring search across Netlify sites. Matches the query against id, name, url, ssl_url, the linked git repo + branch, and the published-deploy state / branch / commit ref (case-insensitive). Returns a `{ matches: [...] }` envelope.",
46
+ searchToolInputSchema(100),
47
+ async (p) => {
48
+ const search = new URLSearchParams({ per_page: "100", page: "1" });
49
+ const root = await netlifyGet(`/api/v1/sites?${search.toString()}`);
50
+ return matchesResult(root, filterNetlifySites, p);
51
+ },
52
+ );
53
+ }
@@ -1,48 +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
-
5
- import {
6
- createRegisterSimpleTool,
7
- createZodToolRegistrar,
8
- mcpJsonResult as jsonResult,
9
- } from "../../../shared/mcp-tool-kit.ts";
10
-
11
- function apiKey(): string {
12
- const k = process.env["NEW_RELIC_API_KEY"]?.trim();
13
- if (k === undefined || k === "") {
14
- throw new Error("NEW_RELIC_API_KEY is not set");
15
- }
16
- return k;
17
- }
18
-
19
- async function nrGet(path: string): Promise<unknown> {
20
- const res = await fetch(`https://api.newrelic.com${path}`, {
21
- headers: { "X-Api-Key": apiKey(), Accept: "application/json" },
22
- });
23
- const text = await res.text();
24
- if (!res.ok) {
25
- throw new Error(`New Relic ${String(res.status)}: ${text.slice(0, 400)}`);
26
- }
27
- return JSON.parse(text) as unknown;
28
- }
3
+ import { registerNewrelicTools } from "./tools.ts";
29
4
 
30
5
  const mcp = new McpServer({ name: "nimbus-newrelic", version: "0.1.0" });
31
- const reg = createZodToolRegistrar(createRegisterSimpleTool(mcp));
32
-
33
- reg("newrelic_application_list", "List APM applications.", z.object({}), async () =>
34
- jsonResult(await nrGet("/v2/applications.json")),
35
- );
36
6
 
37
- reg(
38
- "newrelic_alert_violations",
39
- "List recent alert violations.",
40
- z.object({ only_open: z.boolean().optional() }),
41
- async (p) => {
42
- const only = p.only_open === true ? "true" : "false";
43
- return jsonResult(await nrGet(`/v2/alerts_violations.json?only_open=${only}`));
44
- },
45
- );
7
+ registerNewrelicTools(mcp);
46
8
 
47
9
  const transport = new StdioServerTransport();
48
10
  await mcp.connect(transport);
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ import { createJsonGetter, envAuthHeaders } from "../../../shared/env-json-api.ts";
3
+ import {
4
+ createRegisterSimpleTool,
5
+ createZodToolRegistrar,
6
+ mcpJsonResult as jsonResult,
7
+ } from "../../../shared/mcp-tool-kit.ts";
8
+
9
+ const nrGet = createJsonGetter({
10
+ base: "https://api.newrelic.com",
11
+ label: "New Relic",
12
+ headers: envAuthHeaders({ env: "NEW_RELIC_API_KEY", scheme: "", header: "X-Api-Key" }),
13
+ });
14
+
15
+ /** Tool names exposed by this connector — for contract/introspection tests. */
16
+ export const NEWRELIC_TOOL_NAMES = [
17
+ "newrelic_application_list",
18
+ "newrelic_alert_violations",
19
+ ] as const;
20
+
21
+ export function registerNewrelicTools(server: { tool: (...args: never) => unknown }): void {
22
+ const reg = createZodToolRegistrar(createRegisterSimpleTool(server));
23
+
24
+ reg("newrelic_application_list", "List APM applications.", z.object({}), async () =>
25
+ jsonResult(await nrGet("/v2/applications.json")),
26
+ );
27
+
28
+ reg(
29
+ "newrelic_alert_violations",
30
+ "List recent alert violations.",
31
+ z.object({ only_open: z.boolean().optional() }),
32
+ async (p) => {
33
+ const only = p.only_open === true ? "true" : "false";
34
+ return jsonResult(await nrGet(`/v2/alerts_violations.json?only_open=${only}`));
35
+ },
36
+ );
37
+ }