@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.
- package/README.md +18 -4
- package/connectors/airflow/src/search-filter.ts +2 -15
- package/connectors/airflow/src/server.ts +2 -87
- package/connectors/airflow/src/tools.ts +74 -0
- package/connectors/apple/src/adapters.ts +326 -0
- package/connectors/apple/src/apple-mail-core.ts +4 -8
- package/connectors/apple/src/calendar-tools.ts +28 -8
- package/connectors/apple/src/server.ts +23 -512
- package/connectors/apple/src/tools.ts +18 -44
- package/connectors/argocd/src/server.ts +25 -38
- package/connectors/athena/src/tools.ts +15 -34
- package/connectors/aws/src/server.ts +2 -183
- package/connectors/aws/src/tools.ts +197 -0
- package/connectors/azure/src/server.ts +2 -131
- package/connectors/azure/src/tools.ts +133 -0
- package/connectors/bigeye/src/search-filter.ts +4 -15
- package/connectors/bigeye/src/server.ts +38 -38
- package/connectors/bigquery/src/tools.ts +1 -26
- package/connectors/bitbucket/src/server.ts +2 -244
- package/connectors/bitbucket/src/tools.ts +241 -0
- package/connectors/bitrise/src/server.ts +2 -79
- package/connectors/bitrise/src/tools.ts +76 -0
- package/connectors/canva/src/search-filter.ts +2 -11
- package/connectors/canva/src/server.ts +2 -73
- package/connectors/canva/src/tools.ts +66 -0
- package/connectors/circleci/src/server.ts +2 -176
- package/connectors/circleci/src/tools.ts +193 -0
- package/connectors/cloud-logging/src/tools.ts +14 -27
- package/connectors/cloudwatch/src/tools.ts +15 -34
- package/connectors/codemagic/src/server.ts +2 -70
- package/connectors/codemagic/src/tools.ts +71 -0
- package/connectors/confluence/src/server.ts +2 -301
- package/connectors/confluence/src/tools.ts +320 -0
- package/connectors/dagster/src/server.ts +2 -214
- package/connectors/dagster/src/tools.ts +187 -0
- package/connectors/databricks/src/search-filter.ts +7 -7
- package/connectors/databricks/src/server.ts +2 -77
- package/connectors/databricks/src/tools.ts +66 -0
- package/connectors/datadog/src/server.ts +2 -48
- package/connectors/datadog/src/tools.ts +48 -0
- package/connectors/dataprofile/src/profile.ts +16 -46
- package/connectors/dbt/src/server.ts +2 -92
- package/connectors/dbt/src/tools.ts +80 -0
- package/connectors/dependencytrack/src/server.ts +2 -76
- package/connectors/dependencytrack/src/tools.ts +65 -0
- package/connectors/discord/src/server.ts +2 -85
- package/connectors/discord/src/tools.ts +82 -0
- package/connectors/elasticsearch/src/tools.ts +7 -37
- package/connectors/fastmail/src/jmap-client.ts +202 -0
- package/connectors/fastmail/src/jmap-core.ts +8 -21
- package/connectors/fastmail/src/server.ts +2 -187
- package/connectors/fastmail/src/tools.ts +4 -23
- package/connectors/figma/src/search-filter.ts +2 -11
- package/connectors/figma/src/server.ts +2 -113
- package/connectors/figma/src/tools.ts +99 -0
- package/connectors/firebase/src/server.ts +2 -90
- package/connectors/firebase/src/token.ts +2 -5
- package/connectors/firebase/src/tools.ts +106 -0
- package/connectors/flagsmith/src/server.ts +2 -98
- package/connectors/flagsmith/src/tools.ts +95 -0
- package/connectors/flux/src/server.ts +52 -53
- package/connectors/gcp/src/server.ts +2 -132
- package/connectors/gcp/src/tools.ts +123 -0
- package/connectors/github/src/server.ts +2 -328
- package/connectors/github/src/tools.ts +311 -0
- package/connectors/github-actions/src/server.ts +2 -205
- package/connectors/github-actions/src/tools.ts +209 -0
- package/connectors/gitlab/src/server.ts +2 -384
- package/connectors/gitlab/src/tools.ts +392 -0
- package/connectors/gmail/src/server.ts +2 -270
- package/connectors/gmail/src/tools.ts +209 -0
- package/connectors/google-drive/src/server.ts +2 -524
- package/connectors/google-drive/src/tools.ts +529 -0
- package/connectors/google-meet/src/server.ts +2 -82
- package/connectors/google-meet/src/tools.ts +87 -0
- package/connectors/google-photos/src/server.ts +2 -132
- package/connectors/google-photos/src/tools.ts +138 -0
- package/connectors/grafana/src/server.ts +2 -53
- package/connectors/grafana/src/tools.ts +53 -0
- package/connectors/great-expectations/src/gx-parse.ts +14 -45
- package/connectors/greenhouse/src/server.ts +2 -63
- package/connectors/greenhouse/src/tools.ts +68 -0
- package/connectors/hubspot/src/server.ts +2 -77
- package/connectors/hubspot/src/tools.ts +70 -0
- package/connectors/iac/src/server.ts +2 -137
- package/connectors/iac/src/tools.ts +152 -0
- package/connectors/imap/src/imap-core.ts +29 -82
- package/connectors/imap/src/server.ts +33 -220
- package/connectors/intercom/src/server.ts +2 -61
- package/connectors/intercom/src/tools.ts +58 -0
- package/connectors/jenkins/src/jenkins-api.ts +2 -6
- package/connectors/jenkins/src/server.ts +2 -215
- package/connectors/jenkins/src/tools.ts +240 -0
- package/connectors/jira/src/server.ts +2 -301
- package/connectors/jira/src/tools.ts +325 -0
- package/connectors/kubernetes/src/server.ts +2 -155
- package/connectors/kubernetes/src/tools.ts +159 -0
- package/connectors/launchdarkly/src/server.ts +2 -75
- package/connectors/launchdarkly/src/tools.ts +72 -0
- package/connectors/lever/src/search-filter.ts +4 -12
- package/connectors/lever/src/server.ts +2 -60
- package/connectors/lever/src/tools.ts +61 -0
- package/connectors/linear/src/server.ts +2 -459
- package/connectors/linear/src/tools.ts +482 -0
- package/connectors/localdb/src/sql-scan.ts +13 -41
- package/connectors/localdb/src/tools.ts +1 -1
- package/connectors/looker/src/search-filter.ts +2 -15
- package/connectors/looker/src/server.ts +35 -70
- package/connectors/mendeley/src/server.ts +2 -55
- package/connectors/mendeley/src/tools.ts +54 -0
- package/connectors/mercury/src/server.ts +2 -57
- package/connectors/mercury/src/tools.ts +50 -0
- package/connectors/metabase/src/search-filter.ts +4 -15
- package/connectors/metabase/src/server.ts +2 -79
- package/connectors/metabase/src/tools.ts +64 -0
- package/connectors/miro/src/server.ts +2 -72
- package/connectors/miro/src/tools.ts +65 -0
- package/connectors/mlflow/src/search-filter.ts +3 -7
- package/connectors/mlflow/src/server.ts +20 -40
- package/connectors/monte-carlo/src/server.ts +50 -57
- package/connectors/netlify/src/search-filter.ts +6 -14
- package/connectors/netlify/src/server.ts +2 -60
- package/connectors/netlify/src/tools.ts +53 -0
- package/connectors/newrelic/src/server.ts +2 -40
- package/connectors/newrelic/src/tools.ts +37 -0
- package/connectors/notion/src/server.ts +2 -335
- package/connectors/notion/src/tools.ts +344 -0
- package/connectors/obsidian/src/server.ts +2 -425
- package/connectors/obsidian/src/tools.ts +466 -0
- package/connectors/onedrive/src/server.ts +2 -249
- package/connectors/onedrive/src/tools.ts +253 -0
- package/connectors/outlook/src/server.ts +2 -351
- package/connectors/outlook/src/tools.ts +357 -0
- package/connectors/pagerduty/src/server.ts +2 -132
- package/connectors/pagerduty/src/tools.ts +126 -0
- package/connectors/pipedrive/src/server.ts +2 -58
- package/connectors/pipedrive/src/tools.ts +63 -0
- package/connectors/powerbi/src/search-filter.ts +2 -15
- package/connectors/powerbi/src/server.ts +43 -68
- package/connectors/prefect/src/search-filter.ts +3 -16
- package/connectors/prefect/src/server.ts +2 -90
- package/connectors/prefect/src/tools.ts +83 -0
- package/connectors/protonmail/src/mail-core.ts +16 -75
- package/connectors/protonmail/src/server.ts +34 -214
- package/connectors/raindrop/src/server.ts +2 -103
- package/connectors/raindrop/src/tools.ts +103 -0
- package/connectors/ramp/src/server.ts +2 -105
- package/connectors/ramp/src/tools.ts +98 -0
- package/connectors/readwise/src/server.ts +2 -88
- package/connectors/readwise/src/tools.ts +81 -0
- package/connectors/sagemaker/src/tools.ts +19 -38
- package/connectors/salesforce/src/search-filter.ts +4 -11
- package/connectors/salesforce/src/server.ts +2 -90
- package/connectors/salesforce/src/tools.ts +75 -0
- package/connectors/semgrep/src/search-filter.ts +3 -10
- package/connectors/semgrep/src/server.ts +2 -104
- package/connectors/semgrep/src/tools.ts +92 -0
- package/connectors/sentry/src/server.ts +2 -58
- package/connectors/sentry/src/tools.ts +53 -0
- package/connectors/slack/src/server.ts +2 -352
- package/connectors/slack/src/tools.ts +376 -0
- package/connectors/snowflake/src/search-filter.ts +4 -19
- package/connectors/snowflake/src/server.ts +6 -25
- package/connectors/snyk/src/server.ts +2 -140
- package/connectors/snyk/src/tools.ts +136 -0
- package/connectors/sonarqube/src/server.ts +2 -111
- package/connectors/sonarqube/src/tools.ts +98 -0
- package/connectors/stackoverflow/src/server.ts +2 -77
- package/connectors/stackoverflow/src/tools.ts +66 -0
- package/connectors/storybook/src/storybook-parse.ts +3 -6
- package/connectors/stripe/src/server.ts +2 -61
- package/connectors/stripe/src/tools.ts +54 -0
- package/connectors/superset/src/search-filter.ts +4 -15
- package/connectors/superset/src/server.ts +2 -115
- package/connectors/superset/src/tools.ts +98 -0
- package/connectors/tableau/src/search-filter.ts +2 -15
- package/connectors/tableau/src/server.ts +13 -65
- package/connectors/teams/src/server.ts +2 -249
- package/connectors/teams/src/tools.ts +283 -0
- package/connectors/testflight/src/server.ts +2 -70
- package/connectors/testflight/src/tools.ts +79 -0
- package/connectors/vercel/src/search-filter.ts +2 -10
- package/connectors/vercel/src/server.ts +2 -75
- package/connectors/vercel/src/tools.ts +73 -0
- package/connectors/vertex-ai/src/tools.ts +23 -33
- package/connectors/wiz/src/search-filter.ts +6 -30
- package/connectors/wiz/src/server.ts +2 -188
- package/connectors/wiz/src/tools.ts +168 -0
- package/connectors/zendesk/src/server.ts +2 -73
- package/connectors/zendesk/src/tools.ts +60 -0
- package/connectors/zoom/src/search-filter.ts +1 -7
- package/connectors/zoom/src/server.ts +2 -94
- package/connectors/zoom/src/tools.ts +93 -0
- package/connectors/zotero/src/server.ts +2 -78
- package/connectors/zotero/src/tools.ts +67 -0
- package/package.json +15 -12
- package/shared/access-token-cache.ts +123 -0
- package/shared/atlassian-json-fetch.ts +6 -16
- package/shared/audit-chain.ts +13 -6
- package/shared/cli-json-kit.ts +150 -0
- package/shared/collection-tool-kit.ts +143 -0
- package/shared/connector-mode.ts +3 -2
- package/shared/cursor-list-tool.ts +29 -0
- package/shared/env-json-api.ts +209 -0
- package/shared/fetch-json-text.ts +27 -0
- package/shared/github-rest.ts +62 -0
- package/shared/graphql-json.ts +60 -0
- package/shared/imap-mail-core.ts +91 -0
- package/shared/imap-tool-kit.ts +57 -23
- package/shared/imapflow-adapter.ts +417 -0
- package/shared/rest-tool-kit.ts +77 -7
- package/shared/run-cli-json.ts +38 -16
- package/shared/search-filter.ts +58 -18
- package/shared/walk-files.ts +118 -0
- 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
|
|
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,
|
|
90
|
+
bun run check # lint, typecheck, the four connector audits, full suite
|
|
83
91
|
```
|
|
84
92
|
|
|
85
|
-
CI runs
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
29
|
-
|
|
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 {
|
|
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"),
|
|
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 =
|
|
170
|
-
|
|
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",
|