@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
package/README.md CHANGED
@@ -10,6 +10,9 @@ stdio with credentials supplied from the environment.
10
10
  npx @nimbus-dev/connectors github
11
11
  ```
12
12
 
13
+ You need [Bun](https://bun.sh) 1.2 or newer on your `PATH`: the package ships TypeScript, and its
14
+ `nimbus-connector` bin runs under Bun.
15
+
13
16
  ## What you get, and what you do not
14
17
 
15
18
  Run standalone, a connector gives you:
@@ -62,7 +65,12 @@ Credentials come from the environment. There is no Vault outside the gateway, so
62
65
  config holds the secret. Every variable, and the two behaviours that look like bugs and are not, are
63
66
  in [Configuration](./docs/configuration.md).
64
67
 
65
- Each connector documents its own tools and credentials at `connectors/<id>/README.md`.
68
+ Each connector has its own README at `connectors/<id>/README.md`. Most list their tools and
69
+ credentials, but some — `github`, `slack` and `jira` among them — are still only a short gateway
70
+ quickstart. Until they are filled in, let the connector tell you: a tool called without its
71
+ credential refuses before sending anything, with an error naming the variable to set
72
+ (`GITHUB_PAT is not set`). Those that drive a CLI — `aws`, `azure`, `gcp`, `kubernetes`, `iac` —
73
+ use that tool's own credentials and configuration instead.
66
74
 
67
75
  ## Documentation
68
76
 
@@ -79,10 +87,15 @@ Each connector documents its own tools and credentials at `connectors/<id>/READM
79
87
 
80
88
  ```bash
81
89
  bun install
82
- bun run check # lint, typecheck, consent audit, full suite
90
+ bun run check # lint, typecheck, the four connector audits, full suite
83
91
  ```
84
92
 
85
- CI runs those four on Ubuntu, macOS and Windows — platform equality is a Nimbus non-negotiable.
93
+ CI runs the same gates on Ubuntu, macOS and Windows — platform equality is a Nimbus
94
+ non-negotiable.
95
+
96
+ See [CONTRIBUTING](./CONTRIBUTING.md) before opening a PR, and
97
+ [SECURITY](./SECURITY.md) before reporting a vulnerability — it states what this package does and
98
+ does **not** claim, which is the difference between a security report and a documentation question.
86
99
 
87
100
  ## Relationship to the Nimbus monorepo
88
101
 
@@ -102,7 +115,8 @@ This repo was a scaffold from 2026-06-18 until the connectors landed. Its origin
102
115
  AGPL implications for downstream clients. All three were answered before the move: the connectors are
103
116
  consent-gated standalone, credentials come from the environment, and `NOTICE` states the tiering. It
104
117
  also proposed a package per connector; one package was chosen instead, because 94 packages means 94
105
- releases and forces `shared/` to become a versioned dependency that 190 files import by relative path.
118
+ releases and forces `shared/` to become a versioned dependency that 212 shipped connector source
119
+ files import by relative path.
106
120
 
107
121
  ## License
108
122
 
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  asObjectish,
3
3
  makeQueryFilter,
4
+ nonEmptyStringsText,
4
5
  type SearchMatchOptions,
5
6
  stringField,
6
7
  tagNamesFromObjects,
@@ -8,20 +9,6 @@ import {
8
9
 
9
10
  export type AirflowSearchMatchOptions = SearchMatchOptions;
10
11
 
11
- function ownerNames(row: Record<string, unknown>): string {
12
- const owners = row["owners"];
13
- if (!Array.isArray(owners)) {
14
- return "";
15
- }
16
- const names: string[] = [];
17
- for (const o of owners) {
18
- if (typeof o === "string" && o !== "") {
19
- names.push(o);
20
- }
21
- }
22
- return names.join(" ");
23
- }
24
-
25
12
  function fieldsOf(item: unknown): readonly string[] | null {
26
13
  const row = asObjectish(item);
27
14
  if (row === undefined) {
@@ -30,7 +17,7 @@ function fieldsOf(item: unknown): readonly string[] | null {
30
17
  return [
31
18
  stringField(row, "dag_id"),
32
19
  stringField(row, "description"),
33
- ownerNames(row),
20
+ nonEmptyStringsText(row, "owners"),
34
21
  tagNamesFromObjects(row),
35
22
  ];
36
23
  }
@@ -1,91 +1,6 @@
1
- import { z } from "zod";
2
- import { searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
3
- import {
4
- encodeBasicAuthHeader,
5
- mcpJsonResult as jsonResult,
6
- } from "../../../shared/mcp-tool-kit.ts";
7
1
  import { runReadOnlyMcpConnector } from "../../../shared/run-read-only-mcp-connector.ts";
8
- import { filterAirflowDags } from "./search-filter.ts";
9
-
10
- function trimTrailingSlash(s: string): string {
11
- return s.endsWith("/") ? s.slice(0, -1) : s;
12
- }
13
-
14
- function apiBase(): string {
15
- const v = process.env["AIRFLOW_URL"]?.trim();
16
- if (v === undefined || v === "") {
17
- throw new Error("AIRFLOW_URL is not set");
18
- }
19
- return trimTrailingSlash(v);
20
- }
21
-
22
- function authHeader(): Record<string, string> {
23
- const user = process.env["AIRFLOW_USERNAME"]?.trim();
24
- if (user === undefined || user === "") {
25
- throw new Error("AIRFLOW_USERNAME is not set");
26
- }
27
- const password = process.env["AIRFLOW_PASSWORD"]?.trim();
28
- if (password === undefined || password === "") {
29
- throw new Error("AIRFLOW_PASSWORD is not set");
30
- }
31
- return {
32
- Authorization: encodeBasicAuthHeader(user, password),
33
- Accept: "application/json",
34
- };
35
- }
36
-
37
- async function airflowGet(path: string): Promise<unknown> {
38
- const res = await fetch(`${apiBase()}${path}`, { headers: authHeader() });
39
- const text = await res.text();
40
- if (!res.ok) {
41
- throw new Error(`Airflow ${String(res.status)}: ${text.slice(0, 400)}`);
42
- }
43
- return JSON.parse(text) as unknown;
44
- }
45
-
46
- function dagsFrom(root: unknown): unknown[] {
47
- const dags = (root as { dags?: unknown } | null)?.dags;
48
- return Array.isArray(dags) ? dags : [];
49
- }
2
+ import { registerAirflowTools } from "./tools.ts";
50
3
 
51
4
  await runReadOnlyMcpConnector("nimbus-airflow", (reg) => {
52
- reg(
53
- "airflow_list",
54
- "List Airflow DAGs (GET /api/v1/dags?limit=100&offset=<n>). The Airflow stable REST API v1 returns the envelope { dags: [...], total_entries: <int> } — `dags` holds the DAG objects, `total_entries` the total across all pages. The optional offset arg (default 0) selects the page start; limit is fixed at 100.",
55
- z.object({
56
- offset: z.number().int().min(0).optional(),
57
- }),
58
- async (p) => {
59
- const offset = p.offset ?? 0;
60
- const root = await airflowGet(
61
- `/api/v1/dags?limit=100&offset=${encodeURIComponent(String(offset))}`,
62
- );
63
- return jsonResult({ items: dagsFrom(root) });
64
- },
65
- );
66
-
67
- reg(
68
- "airflow_get",
69
- "Fetch one Airflow DAG by its dag_id (GET /api/v1/dags/{dag_id}). Returns the DAG object directly (NOT wrapped in an envelope). Throws when no DAG with that id exists.",
70
- z.object({
71
- dag_id: z.string().min(1),
72
- }),
73
- async (p) => {
74
- return jsonResult(await airflowGet(`/api/v1/dags/${encodeURIComponent(p.dag_id)}`));
75
- },
76
- );
77
-
78
- reg(
79
- "airflow_search",
80
- "Substring search across the first page of Airflow DAGs. Matches the query (case-insensitive) against the dag_id, description, owners, and tag names. Returns a matches envelope.",
81
- searchToolInputSchema(100),
82
- async (p) => {
83
- const root = await airflowGet("/api/v1/dags?limit=100&offset=0");
84
- const matches = filterAirflowDags(dagsFrom(root), {
85
- query: p.query,
86
- limit: p.limit,
87
- });
88
- return jsonResult({ matches });
89
- },
90
- );
5
+ registerAirflowTools(reg);
91
6
  });
@@ -0,0 +1,74 @@
1
+ import { z } from "zod";
2
+ import { createJsonGetter, requiredBaseUrl, requiredEnv } from "../../../shared/env-json-api.ts";
3
+ import { searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
4
+ import {
5
+ encodeBasicAuthHeader,
6
+ mcpJsonResult as jsonResult,
7
+ } from "../../../shared/mcp-tool-kit.ts";
8
+ import type { ZodToolRegistrar } from "../../../shared/run-read-only-mcp-connector.ts";
9
+ import { filterAirflowDags } from "./search-filter.ts";
10
+
11
+ function authHeader(): Record<string, string> {
12
+ const user = requiredEnv("AIRFLOW_USERNAME");
13
+ const password = requiredEnv("AIRFLOW_PASSWORD");
14
+ return {
15
+ Authorization: encodeBasicAuthHeader(user, password),
16
+ Accept: "application/json",
17
+ };
18
+ }
19
+
20
+ const airflowGet = createJsonGetter({
21
+ base: () => requiredBaseUrl("AIRFLOW_URL"),
22
+ label: "Airflow",
23
+ headers: authHeader,
24
+ });
25
+
26
+ function dagsFrom(root: unknown): unknown[] {
27
+ const dags = (root as { dags?: unknown } | null)?.dags;
28
+ return Array.isArray(dags) ? dags : [];
29
+ }
30
+
31
+ /** Tool names exposed by this connector — for contract/introspection tests. */
32
+ export const AIRFLOW_TOOL_NAMES = ["airflow_get", "airflow_list", "airflow_search"] as const;
33
+
34
+ export function registerAirflowTools(reg: ZodToolRegistrar): void {
35
+ reg(
36
+ "airflow_list",
37
+ "List Airflow DAGs (GET /api/v1/dags?limit=100&offset=<n>). The Airflow stable REST API v1 returns the envelope { dags: [...], total_entries: <int> } — `dags` holds the DAG objects, `total_entries` the total across all pages. The optional offset arg (default 0) selects the page start; limit is fixed at 100.",
38
+ z.object({
39
+ offset: z.number().int().min(0).optional(),
40
+ }),
41
+ async (p) => {
42
+ const offset = p.offset ?? 0;
43
+ const root = await airflowGet(
44
+ `/api/v1/dags?limit=100&offset=${encodeURIComponent(String(offset))}`,
45
+ );
46
+ return jsonResult({ items: dagsFrom(root) });
47
+ },
48
+ );
49
+
50
+ reg(
51
+ "airflow_get",
52
+ "Fetch one Airflow DAG by its dag_id (GET /api/v1/dags/{dag_id}). Returns the DAG object directly (NOT wrapped in an envelope). Throws when no DAG with that id exists.",
53
+ z.object({
54
+ dag_id: z.string().min(1),
55
+ }),
56
+ async (p) => {
57
+ return jsonResult(await airflowGet(`/api/v1/dags/${encodeURIComponent(p.dag_id)}`));
58
+ },
59
+ );
60
+
61
+ reg(
62
+ "airflow_search",
63
+ "Substring search across the first page of Airflow DAGs. Matches the query (case-insensitive) against the dag_id, description, owners, and tag names. Returns a matches envelope.",
64
+ searchToolInputSchema(100),
65
+ async (p) => {
66
+ const root = await airflowGet("/api/v1/dags?limit=100&offset=0");
67
+ const matches = filterAirflowDags(dagsFrom(root), {
68
+ query: p.query,
69
+ limit: p.limit,
70
+ });
71
+ return jsonResult({ matches });
72
+ },
73
+ );
74
+ }
@@ -0,0 +1,326 @@
1
+ /**
2
+ * The real iCloud clients: IMAP, SMTP, IMAP-APPEND drafts and CalDAV.
3
+ *
4
+ * These used to live in `server.ts`, under a comment explaining that the
5
+ * imapflow message-shaping helpers were "copied locally; cross-importing
6
+ * ../../imap/src/* is forbidden … server.ts is coverage-excluded so this copy
7
+ * is acceptable". Both halves of that reasoning have since stopped holding:
8
+ * the shared copy now lives in `shared/imapflow-adapter.ts`, which every
9
+ * connector may import, and `server.ts` is no longer coverage-excluded. What
10
+ * the copy actually cost was 45 duplicated lines against the imap connector and
11
+ * a divergence that mattered — the shared `withMailbox` leaked a connection
12
+ * when the mailbox lock failed, and this file's version was the one that had it
13
+ * right.
14
+ *
15
+ * So the IMAP and SMTP clients are now the shared adapter with iCloud's
16
+ * configuration, and only what is genuinely Apple-specific is written out here:
17
+ * the Drafts APPEND and the two-phase CalDAV login.
18
+ *
19
+ * Nothing here constructs itself at import time, so a test can build any of it
20
+ * against a fake and never open a socket.
21
+ */
22
+
23
+ import { type ParsedEvent, parseICalendar } from "@nimbus-dev/sdk";
24
+ import { ImapFlow } from "imapflow";
25
+ import { type DAVCalendar, DAVClient } from "tsdav";
26
+ import {
27
+ createImapFlowClient,
28
+ createNodemailerMailer,
29
+ type ImapFlowFactory,
30
+ type TransportFactory,
31
+ } from "../../../shared/imapflow-adapter.ts";
32
+ import type {
33
+ DraftAppender,
34
+ DraftInput,
35
+ DraftResult,
36
+ EmailReadClient,
37
+ EmailSendMailer,
38
+ } from "./apple-mail-core.ts";
39
+ import {
40
+ type CalDavClient,
41
+ type CalendarRef,
42
+ caldavObjectFilename,
43
+ type EventWindow,
44
+ } from "./caldav-core.ts";
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Fixed iCloud endpoints
48
+ // ---------------------------------------------------------------------------
49
+
50
+ export const ICLOUD_IMAP_HOST = "imap.mail.me.com";
51
+ export const ICLOUD_IMAP_PORT = 993;
52
+ export const ICLOUD_SMTP_HOST = "smtp.mail.me.com";
53
+ export const ICLOUD_SMTP_PORT = 587;
54
+ export const ICLOUD_CALDAV_BOOTSTRAP_URL = "https://caldav.icloud.com";
55
+ export const DRAFTS_MAILBOX = "Drafts";
56
+
57
+ /** The iCloud IMAP read client. */
58
+ export function createAppleImapClient(
59
+ user: string,
60
+ pass: string,
61
+ newImapFlow?: ImapFlowFactory,
62
+ ): EmailReadClient {
63
+ return createImapFlowClient(
64
+ { host: ICLOUD_IMAP_HOST, port: ICLOUD_IMAP_PORT, user, pass, secure: true },
65
+ newImapFlow,
66
+ );
67
+ }
68
+
69
+ /**
70
+ * The iCloud SMTP mailer.
71
+ *
72
+ * Port 587 is STARTTLS, not implicit TLS. The shared mailer requires STARTTLS
73
+ * whenever TLS is not implicit and offers no way to opt out, so the credentials
74
+ * cannot reach the wire unencrypted; iCloud always advertises it. `from` is
75
+ * pinned to the authenticated address by the shared mailer.
76
+ */
77
+ export function createAppleSmtpMailer(
78
+ user: string,
79
+ pass: string,
80
+ makeTransport?: TransportFactory,
81
+ ): EmailSendMailer {
82
+ return createNodemailerMailer(
83
+ { host: ICLOUD_SMTP_HOST, port: ICLOUD_SMTP_PORT, user, pass, secure: false },
84
+ makeTransport,
85
+ );
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // DraftAppender — IMAP APPEND to "Drafts" via imapflow
90
+ // From is PINNED to the authenticated icloud_email (forced sender contract).
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /**
94
+ * Build a minimal RFC 5322 message from the given inputs.
95
+ * The From header is always set to `from` (the pinned icloud_email).
96
+ */
97
+ export function buildRfc822Message(from: string, input: DraftInput): string {
98
+ const lines: string[] = [];
99
+ lines.push(`From: ${from}`, `To: ${input.to}`);
100
+ if (input.cc !== undefined && input.cc !== "") {
101
+ lines.push(`Cc: ${input.cc}`);
102
+ }
103
+ if (input.bcc !== undefined && input.bcc !== "") {
104
+ lines.push(`Bcc: ${input.bcc}`);
105
+ }
106
+ // The body may contain non-ASCII (UTF-8) characters, so it cannot be declared
107
+ // 7bit. Base64-encode it (wrapped at 76 chars per RFC 2045) so the APPENDed
108
+ // message is well-formed regardless of content.
109
+ lines.push(
110
+ `Subject: ${input.subject}`,
111
+ "MIME-Version: 1.0",
112
+ "Content-Type: text/plain; charset=UTF-8",
113
+ "Content-Transfer-Encoding: base64",
114
+ "",
115
+ );
116
+ const encoded = Buffer.from(input.body, "utf8").toString("base64");
117
+ lines.push(encoded.replace(/.{76}/g, "$&\r\n"));
118
+ // RFC 5322 uses CRLF line endings.
119
+ return lines.join("\r\n");
120
+ }
121
+
122
+ /** Minimal imapflow surface the draft appender uses. */
123
+ export interface AppendCapableFlow {
124
+ connect(): Promise<void>;
125
+ logout(): Promise<void>;
126
+ append(mailbox: string, content: Buffer, flags: string[]): Promise<unknown>;
127
+ }
128
+
129
+ class AppleDraftAppender implements DraftAppender {
130
+ constructor(
131
+ private readonly user: string,
132
+ private readonly pass: string,
133
+ private readonly newFlow: (user: string, pass: string) => AppendCapableFlow,
134
+ ) {}
135
+
136
+ async appendDraft(input: DraftInput): Promise<DraftResult> {
137
+ const flow = this.newFlow(this.user, this.pass);
138
+ await flow.connect();
139
+ try {
140
+ // Build the RFC 5322 message with From pinned to the authenticated email.
141
+ const raw = buildRfc822Message(this.user, input);
142
+ const buf = Buffer.from(raw, "utf8");
143
+
144
+ // IMAP APPEND to the Drafts mailbox. imapflow returns the assigned UID
145
+ // when the server supports APPENDUID or UIDPLUS — it may return undefined
146
+ // when neither is available, which is valid.
147
+ const result = await flow.append(DRAFTS_MAILBOX, buf, [String.raw`\Draft`]);
148
+ const uid = (result as { uid?: number } | undefined)?.uid ?? null;
149
+ return { uid: uid === undefined ? null : uid, mailbox: DRAFTS_MAILBOX };
150
+ } finally {
151
+ await flow.logout();
152
+ }
153
+ }
154
+ }
155
+
156
+ function realAppendFlow(user: string, pass: string): AppendCapableFlow {
157
+ return new ImapFlow({
158
+ host: ICLOUD_IMAP_HOST,
159
+ port: ICLOUD_IMAP_PORT,
160
+ secure: true,
161
+ auth: { user, pass },
162
+ logger: false,
163
+ }) as unknown as AppendCapableFlow;
164
+ }
165
+
166
+ /** The Drafts-mailbox appender. `newFlow` is overridden in tests. */
167
+ export function createAppleDraftAppender(
168
+ user: string,
169
+ pass: string,
170
+ newFlow: (user: string, pass: string) => AppendCapableFlow = realAppendFlow,
171
+ ): DraftAppender {
172
+ return new AppleDraftAppender(user, pass, newFlow);
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // CalDavClient — tsdav DAVClient (two-phase: bootstrap discover then working)
177
+ //
178
+ // iCloud CalDAV authenticates against caldav.icloud.com but redirects to a
179
+ // user-specific host (p##-caldav.icloud.com). login() resolves the principal +
180
+ // calendar-home-set and binds the credentials to the resolved host so that
181
+ // Basic auth is applied on every subsequent request — without relying on
182
+ // transparent cross-host redirect forwarding.
183
+ // ---------------------------------------------------------------------------
184
+
185
+ /** The subset of tsdav's `DAVClient` this adapter drives. Faked in tests. */
186
+ export interface DavClientLike {
187
+ login(): Promise<unknown>;
188
+ fetchCalendars(): Promise<DAVCalendar[]>;
189
+ fetchCalendarObjects(params: {
190
+ calendar: DAVCalendar;
191
+ timeRange?: { start: string; end: string };
192
+ expand?: boolean;
193
+ }): Promise<{ url: string; data?: unknown }[]>;
194
+ createCalendarObject(params: {
195
+ calendar: DAVCalendar;
196
+ iCalString: string;
197
+ filename: string;
198
+ }): Promise<unknown>;
199
+ deleteCalendarObject(params: { calendarObject: { url: string; etag: string } }): Promise<unknown>;
200
+ }
201
+
202
+ /** A CalDAV client with the login step the iCloud redirect dance requires. */
203
+ export interface LoggingInCalDavClient extends CalDavClient {
204
+ login(): Promise<void>;
205
+ }
206
+
207
+ class TsdavCalDavClient implements LoggingInCalDavClient {
208
+ /**
209
+ * Discovered DAVCalendar objects keyed by their url. Populated after the
210
+ * first listCalendars() call so that putEvent/listEvents can obtain the
211
+ * full DAVCalendar object (which carries the account reference that tsdav
212
+ * needs for auth on the resolved p##-caldav.icloud.com host).
213
+ */
214
+ private discovered: Map<string, DAVCalendar> = new Map();
215
+
216
+ constructor(private readonly davClient: DavClientLike) {}
217
+
218
+ async login(): Promise<void> {
219
+ await this.davClient.login();
220
+ }
221
+
222
+ async listCalendars(): Promise<CalendarRef[]> {
223
+ const cals = await this.davClient.fetchCalendars();
224
+ // Refresh the discovery cache.
225
+ this.discovered = new Map();
226
+ for (const cal of cals) {
227
+ this.discovered.set(cal.url, cal);
228
+ }
229
+ return cals.map((cal) => ({
230
+ url: cal.url,
231
+ displayName:
232
+ typeof cal.displayName === "string" && cal.displayName !== "" ? cal.displayName : cal.url,
233
+ }));
234
+ }
235
+
236
+ /**
237
+ * Resolve a CalendarRef to the underlying DAVCalendar tsdav needs.
238
+ * If not found in cache, constructs a minimal DAVCalendar using just the url
239
+ * (tsdav fetchCalendarObjects only strictly requires `calendar.url`).
240
+ */
241
+ private toDavCalendar(cal: CalendarRef): DAVCalendar {
242
+ return this.discovered.get(cal.url) ?? { url: cal.url };
243
+ }
244
+
245
+ async listEvents(
246
+ cal: CalendarRef,
247
+ window: EventWindow,
248
+ ): Promise<{ href: string; event: ParsedEvent }[]> {
249
+ const objects = await this.davClient.fetchCalendarObjects({
250
+ calendar: this.toDavCalendar(cal),
251
+ timeRange: { start: window.startUtc, end: window.endUtc },
252
+ // expand: true triggers server-side recurrence expansion (C:expand in
253
+ // the REPORT) — required per spec; no client-side RRULE engine.
254
+ expand: true,
255
+ });
256
+
257
+ const results: { href: string; event: ParsedEvent }[] = [];
258
+ for (const obj of objects) {
259
+ if (typeof obj.data !== "string" || obj.data === "") {
260
+ continue;
261
+ }
262
+ for (const event of parseICalendar(obj.data)) {
263
+ results.push({ href: obj.url, event });
264
+ }
265
+ }
266
+ return results;
267
+ }
268
+
269
+ async putEvent(cal: CalendarRef, uid: string, ics: string): Promise<{ href: string }> {
270
+ // The UID is embedded into the object URL path segment; sanitize it so a
271
+ // summary-derived UID with `/`, `#`, `%`, etc. cannot corrupt the href.
272
+ const filename = caldavObjectFilename(uid);
273
+ await this.davClient.createCalendarObject({
274
+ calendar: this.toDavCalendar(cal),
275
+ iCalString: ics,
276
+ filename,
277
+ });
278
+ // The href of the newly created object is conventionally the calendar URL
279
+ // joined with the filename. tsdav does not return a Location header
280
+ // wrapper, so we construct it from the calendar URL.
281
+ const href = cal.url.endsWith("/") ? `${cal.url}${filename}` : `${cal.url}/${filename}`;
282
+ return { href };
283
+ }
284
+
285
+ async deleteEvent(href: string): Promise<void> {
286
+ // etag: "" means "unconditional delete" — acceptable because we don't track
287
+ // etags in the CalendarRef/EventWindow flow.
288
+ await this.davClient.deleteCalendarObject({
289
+ calendarObject: { url: href, etag: "" },
290
+ });
291
+ }
292
+ }
293
+
294
+ function realDavClient(email: string, appPw: string): DavClientLike {
295
+ // login() performs principal + calendar-home discovery and binds the
296
+ // credentials to all subsequent requests against the resolved
297
+ // p##-caldav.icloud.com host — satisfying the auth-on-redirect requirement.
298
+ return new DAVClient({
299
+ serverUrl: ICLOUD_CALDAV_BOOTSTRAP_URL,
300
+ credentials: { username: email, password: appPw },
301
+ authMethod: "Basic",
302
+ defaultAccountType: "caldav",
303
+ }) as unknown as DavClientLike;
304
+ }
305
+
306
+ /** The iCloud CalDAV client. `makeDavClient` is overridden in tests. */
307
+ export function createAppleCalDavClient(
308
+ email: string,
309
+ appPw: string,
310
+ makeDavClient: (email: string, appPw: string) => DavClientLike = realDavClient,
311
+ ): LoggingInCalDavClient {
312
+ return new TsdavCalDavClient(makeDavClient(email, appPw));
313
+ }
314
+
315
+ /**
316
+ * iCalendar DTSTAMP format (RFC 5545): `YYYYMMDDTHHMMSSZ`.
317
+ *
318
+ * `buildVEvent` emits the value verbatim, so it must NOT be ISO-8601 — no
319
+ * hyphens, colons or milliseconds.
320
+ */
321
+ export function icalTimestamp(at: Date = new Date()): string {
322
+ return at
323
+ .toISOString()
324
+ .replace(/[-:]/g, "")
325
+ .replace(/\.\d+Z$/, "Z");
326
+ }
@@ -9,6 +9,8 @@
9
9
  * the connector must satisfy (EmailReadClient/EmailSendMailer/EmailMessageMeta).
10
10
  */
11
11
 
12
+ import type { OutgoingMail } from "../../../shared/imap-tool-kit.ts";
13
+
12
14
  export {
13
15
  capPreview,
14
16
  clampLimit,
@@ -16,7 +18,6 @@ export {
16
18
  PREVIEW_FETCH_BYTES,
17
19
  PREVIEW_MAX_CHARS,
18
20
  } from "../../../shared/imap-mail-core.ts";
19
-
20
21
  // The shared kit already defines the structural client/mailer/message contracts
21
22
  // the connector must satisfy (EmailReadClient/EmailSendMailer/EmailMessageMeta).
22
23
  export type {
@@ -25,13 +26,8 @@ export type {
25
26
  EmailSendMailer,
26
27
  } from "../../../shared/imap-tool-kit.ts";
27
28
 
28
- export interface DraftInput {
29
- readonly to: string;
30
- readonly subject: string;
31
- readonly body: string;
32
- readonly cc?: string;
33
- readonly bcc?: string;
34
- }
29
+ /** A draft is the same outgoing message a send tool hands its transport. */
30
+ export type DraftInput = OutgoingMail;
35
31
 
36
32
  export interface DraftResult {
37
33
  readonly uid: number | null;
@@ -1,4 +1,8 @@
1
- import { type ConsentServer, createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
1
+ import {
2
+ type ConsentServer,
3
+ createWriteToolRegistrar,
4
+ type WriteToolRegistrar,
5
+ } from "../../../shared/consent-kit.ts";
2
6
  /**
3
7
  * Apple Calendar tool handlers (read + write) over an injected CalDavClient.
4
8
  *
@@ -76,6 +80,22 @@ interface ViewEvent {
76
80
 
77
81
  const DEFAULT_MAX_INSTANCES = 200;
78
82
 
83
+ /**
84
+ * The apple connector's ONE write-scope configuration, for its mail and calendar writes alike.
85
+ *
86
+ * Both read NIMBUS_MCP_APPLE_WRITE_SCOPE, and the consent kit refuses at startup a scope term
87
+ * whose kind its registrar did not declare. They used to be built by two registrars declaring one
88
+ * kind each, so every non-empty scope was fatal: a `recipient:` term crashed the calendar
89
+ * registrar and a `calendar:` term the mail one, leaving standalone apple unable to enable any
90
+ * write at all. One registrar declaring both kinds also gives the connector one write budget,
91
+ * where two had silently doubled it.
92
+ */
93
+ export const APPLE_WRITE_SCOPE = {
94
+ connector: "apple",
95
+ scopeEnv: "NIMBUS_MCP_APPLE_WRITE_SCOPE",
96
+ scopeKinds: ["recipient", "calendar"],
97
+ } as const;
98
+
79
99
  // ---------------------------------------------------------------------------
80
100
  // registerAppleCalendarTools
81
101
  // ---------------------------------------------------------------------------
@@ -86,7 +106,9 @@ const DEFAULT_MAX_INSTANCES = 200;
86
106
  * @param server - The MCP server instance.
87
107
  * @param options - Injected dependencies: the CalDAV client, a `now` factory
88
108
  * (returns the current UTC timestamp in iCalendar DTSTAMP
89
- * format, e.g. "20260601T090000Z"), and optional config.
109
+ * format, e.g. "20260601T090000Z"), optional config, and the
110
+ * write registrar to share with the connector's mail writes
111
+ * (built from {@link APPLE_WRITE_SCOPE} when omitted).
90
112
  */
91
113
  export function registerAppleCalendarTools(
92
114
  // Widened: the consent kit needs the real server surface, not just the `.tool` shim.
@@ -95,6 +117,7 @@ export function registerAppleCalendarTools(
95
117
  calendar: CalDavClient;
96
118
  now: () => string;
97
119
  config?: CalendarToolConfig | undefined;
120
+ registerWriteTool?: WriteToolRegistrar | undefined;
98
121
  },
99
122
  ): void {
100
123
  const { calendar, now, config } = options;
@@ -132,7 +155,7 @@ export function registerAppleCalendarTools(
132
155
  const items: ViewEvent[] = [];
133
156
 
134
157
  for (const cal of allCals) {
135
- const rows = await calendar.listEvents(cal, window);
158
+ const rows = await calendar.listEvents(cal, window); // NOSONAR S9382: calendars are queried in order until maxInstances is reached — each clamp depends on how many events the earlier calendars returned, and a calendar past the cap is never queried at all.
136
159
  const clamped = clampInstances(rows, maxInstances - items.length);
137
160
 
138
161
  for (const { href, event } of clamped) {
@@ -166,11 +189,8 @@ export function registerAppleCalendarTools(
166
189
  // apple_calendar_event_create
167
190
  // -------------------------------------------------------------------------
168
191
 
169
- const registerWriteTool = createWriteToolRegistrar(server, {
170
- connector: "apple",
171
- scopeEnv: "NIMBUS_MCP_APPLE_WRITE_SCOPE",
172
- scopeKinds: ["calendar"],
173
- });
192
+ const registerWriteTool =
193
+ options.registerWriteTool ?? createWriteToolRegistrar(server, APPLE_WRITE_SCOPE);
174
194
 
175
195
  registerWriteTool(
176
196
  "apple_calendar_event_create",