@nimbus-dev/connectors 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -7
- 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 +26 -43
- package/connectors/aws/src/server.ts +2 -183
- package/connectors/aws/src/tools.ts +199 -0
- package/connectors/azure/src/server.ts +2 -131
- package/connectors/azure/src/tools.ts +134 -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 +19 -35
- package/connectors/cloudwatch/src/tools.ts +20 -38
- 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 +153 -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 +160 -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 +21 -40
- 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 +14 -12
- package/shared/access-token-cache.ts +123 -0
- package/shared/atlassian-json-fetch.ts +6 -16
- package/shared/audit-chain.ts +601 -34
- package/shared/cli-json-kit.ts +191 -0
- package/shared/collection-tool-kit.ts +143 -0
- package/shared/connector-mode.ts +3 -2
- package/shared/consent-kit.ts +81 -26
- 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/nimbus-spawn.ts +84 -27
- package/shared/rest-tool-kit.ts +77 -7
- package/shared/run-cli-json.ts +38 -16
- package/shared/safe-cli-arg.ts +139 -17
- package/shared/search-filter.ts +58 -18
- package/shared/walk-files.ts +118 -0
- package/shared/windows-batch-args.ts +273 -0
- package/shared/write-budget.ts +100 -0
- package/standalone/src/launcher.ts +19 -7
|
@@ -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",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* iCloud Mail + Calendar MCP server.
|
|
2
|
+
* iCloud Mail + Calendar MCP server — entry point.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The real clients live in `adapters.ts` and the pure logic in
|
|
5
|
+
* `apple-mail-core.ts`, `caldav-core.ts`, `calendar-tools.ts` and `tools.ts`.
|
|
6
|
+
* This file is only the bootstrap: read the credentials, build the clients,
|
|
7
|
+
* warm the CalDAV cache, register, connect.
|
|
7
8
|
*
|
|
8
9
|
* Credentials arrive as env vars injected by the lazy-mesh spawner — never
|
|
9
10
|
* logged, IPC'd, or written to disk.
|
|
@@ -11,531 +12,41 @@
|
|
|
11
12
|
|
|
12
13
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
14
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
|
-
import { type ParsedEvent, parseICalendar } from "@nimbus-dev/sdk";
|
|
15
|
-
import { type FetchMessageObject, type FetchQueryObject, ImapFlow } from "imapflow";
|
|
16
|
-
import { createTransport, type Transporter } from "nodemailer";
|
|
17
|
-
import { type DAVCalendar, DAVClient } from "tsdav";
|
|
18
|
-
import {
|
|
19
|
-
type BodyStructureNode,
|
|
20
|
-
extractAttachments,
|
|
21
|
-
findTextPlainPart,
|
|
22
|
-
} from "../../../shared/imap-bodystructure.ts";
|
|
23
|
-
import { previewFromParts } from "../../../shared/imap-tool-kit.ts";
|
|
24
15
|
import { requireProcessEnv } from "../../../shared/mcp-tool-kit.ts";
|
|
25
16
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
PREVIEW_FETCH_BYTES,
|
|
33
|
-
} from "./apple-mail-core.ts";
|
|
34
|
-
import {
|
|
35
|
-
type CalDavClient,
|
|
36
|
-
type CalendarRef,
|
|
37
|
-
caldavObjectFilename,
|
|
38
|
-
type EventWindow,
|
|
39
|
-
} from "./caldav-core.ts";
|
|
17
|
+
createAppleCalDavClient,
|
|
18
|
+
createAppleDraftAppender,
|
|
19
|
+
createAppleImapClient,
|
|
20
|
+
createAppleSmtpMailer,
|
|
21
|
+
icalTimestamp,
|
|
22
|
+
} from "./adapters.ts";
|
|
40
23
|
import { registerAppleTools } from "./tools.ts";
|
|
41
24
|
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
// Fixed iCloud endpoints
|
|
44
|
-
// ---------------------------------------------------------------------------
|
|
45
|
-
|
|
46
|
-
const ICLOUD_IMAP_HOST = "imap.mail.me.com";
|
|
47
|
-
const ICLOUD_IMAP_PORT = 993;
|
|
48
|
-
const ICLOUD_SMTP_HOST = "smtp.mail.me.com";
|
|
49
|
-
const ICLOUD_SMTP_PORT = 587;
|
|
50
|
-
const ICLOUD_CALDAV_BOOTSTRAP_URL = "https://caldav.icloud.com";
|
|
51
|
-
const DEFAULT_MAILBOX = "INBOX";
|
|
52
|
-
const DRAFTS_MAILBOX = "Drafts";
|
|
53
|
-
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
// imapflow message-shaping helpers
|
|
56
|
-
// (copied locally; cross-importing ../../imap/src/* is forbidden per review #1
|
|
57
|
-
// and package isolation rules; server.ts is coverage-excluded so this copy
|
|
58
|
-
// is acceptable)
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Build an address record from an imapflow envelope address, omitting
|
|
63
|
-
* name/address when absent (exactOptionalPropertyTypes is on — explicit
|
|
64
|
-
* undefined is not assignable).
|
|
65
|
-
*/
|
|
66
|
-
function toImapAddressLocal(a: { name?: string; address?: string }): {
|
|
67
|
-
name?: string;
|
|
68
|
-
address?: string;
|
|
69
|
-
} {
|
|
70
|
-
const out: { name?: string; address?: string } = {};
|
|
71
|
-
if (a.name !== undefined) {
|
|
72
|
-
out.name = a.name;
|
|
73
|
-
}
|
|
74
|
-
if (a.address !== undefined) {
|
|
75
|
-
out.address = a.address;
|
|
76
|
-
}
|
|
77
|
-
return out;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function envelopeFromImap(env: FetchMessageObject["envelope"]): {
|
|
81
|
-
date?: Date | null;
|
|
82
|
-
subject?: string | null;
|
|
83
|
-
messageId?: string | null;
|
|
84
|
-
from?: { name?: string; address?: string }[];
|
|
85
|
-
to?: { name?: string; address?: string }[];
|
|
86
|
-
cc?: { name?: string; address?: string }[];
|
|
87
|
-
} {
|
|
88
|
-
if (env === undefined) {
|
|
89
|
-
return {};
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
92
|
-
date: env.date ?? null,
|
|
93
|
-
subject: env.subject ?? null,
|
|
94
|
-
messageId: env.messageId ?? null,
|
|
95
|
-
from: (env.from ?? []).map(toImapAddressLocal),
|
|
96
|
-
to: (env.to ?? []).map(toImapAddressLocal),
|
|
97
|
-
cc: (env.cc ?? []).map(toImapAddressLocal),
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function toMessageMeta(
|
|
102
|
-
msg: FetchMessageObject,
|
|
103
|
-
mailbox: string,
|
|
104
|
-
uidValidity: string | null,
|
|
105
|
-
): {
|
|
106
|
-
uid: number;
|
|
107
|
-
mailbox: string;
|
|
108
|
-
uidValidity: string | null;
|
|
109
|
-
envelope: ReturnType<typeof envelopeFromImap>;
|
|
110
|
-
attachments: { filename: string | null; sizeBytes: number | null; mimeType: string | null }[];
|
|
111
|
-
preview: string;
|
|
112
|
-
} {
|
|
113
|
-
const structure = (msg.bodyStructure ?? null) as BodyStructureNode | null;
|
|
114
|
-
const partKey = findTextPlainPart(structure);
|
|
115
|
-
return {
|
|
116
|
-
uid: msg.uid,
|
|
117
|
-
mailbox,
|
|
118
|
-
uidValidity,
|
|
119
|
-
envelope: envelopeFromImap(msg.envelope),
|
|
120
|
-
attachments: extractAttachments(structure),
|
|
121
|
-
preview: previewFromParts(msg.bodyParts, partKey),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Fetch query requesting ONLY headers + structure + a truncated text body part.
|
|
127
|
-
* No source / full-body request by construction.
|
|
128
|
-
*/
|
|
129
|
-
function previewFetchQuery(): FetchQueryObject {
|
|
130
|
-
return {
|
|
131
|
-
uid: true,
|
|
132
|
-
envelope: true,
|
|
133
|
-
bodyStructure: true,
|
|
134
|
-
internalDate: true,
|
|
135
|
-
bodyParts: [
|
|
136
|
-
{ key: "1", start: 0, maxLength: PREVIEW_FETCH_BYTES },
|
|
137
|
-
{ key: "TEXT", start: 0, maxLength: PREVIEW_FETCH_BYTES },
|
|
138
|
-
],
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
// EmailReadClient — imapflow over imap.mail.me.com:993 (TLS)
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
|
|
146
|
-
class AppleImapClient implements EmailReadClient {
|
|
147
|
-
private readonly config: {
|
|
148
|
-
host: string;
|
|
149
|
-
port: number;
|
|
150
|
-
user: string;
|
|
151
|
-
pass: string;
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
constructor(user: string, pass: string) {
|
|
155
|
-
this.config = {
|
|
156
|
-
host: ICLOUD_IMAP_HOST,
|
|
157
|
-
port: ICLOUD_IMAP_PORT,
|
|
158
|
-
user,
|
|
159
|
-
pass,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
private newFlow(): ImapFlow {
|
|
164
|
-
return new ImapFlow({
|
|
165
|
-
host: this.config.host,
|
|
166
|
-
port: this.config.port,
|
|
167
|
-
secure: true,
|
|
168
|
-
auth: { user: this.config.user, pass: this.config.pass },
|
|
169
|
-
logger: false,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private async withMailbox<T>(
|
|
174
|
-
mailbox: string,
|
|
175
|
-
fn: (client: ImapFlow, uidValidity: string | null) => Promise<T>,
|
|
176
|
-
): Promise<T> {
|
|
177
|
-
const flow = this.newFlow();
|
|
178
|
-
await flow.connect();
|
|
179
|
-
// logout in an OUTER finally so the connection is always closed even if
|
|
180
|
-
// getMailboxLock() throws (the lock is acquired after connect()).
|
|
181
|
-
try {
|
|
182
|
-
const lock = await flow.getMailboxLock(mailbox);
|
|
183
|
-
try {
|
|
184
|
-
const mb = flow.mailbox;
|
|
185
|
-
const uidValidity = mb === false ? null : String(mb.uidValidity);
|
|
186
|
-
return await fn(flow, uidValidity);
|
|
187
|
-
} finally {
|
|
188
|
-
lock.release();
|
|
189
|
-
}
|
|
190
|
-
} finally {
|
|
191
|
-
await flow.logout();
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
async list(options: {
|
|
196
|
-
mailbox?: string;
|
|
197
|
-
limit?: number;
|
|
198
|
-
}): Promise<ReturnType<typeof toMessageMeta>[]> {
|
|
199
|
-
const mailbox = options.mailbox ?? DEFAULT_MAILBOX;
|
|
200
|
-
const limit = clampLimit(options.limit);
|
|
201
|
-
return this.withMailbox(mailbox, async (flow, uidValidity) => {
|
|
202
|
-
const out: ReturnType<typeof toMessageMeta>[] = [];
|
|
203
|
-
const status = await flow.status(mailbox, { messages: true });
|
|
204
|
-
const total = status.messages ?? 0;
|
|
205
|
-
if (total === 0) {
|
|
206
|
-
return out;
|
|
207
|
-
}
|
|
208
|
-
const start = Math.max(1, total - limit + 1);
|
|
209
|
-
const range = `${start}:${total}`;
|
|
210
|
-
for await (const msg of flow.fetch(range, previewFetchQuery())) {
|
|
211
|
-
out.push(toMessageMeta(msg, mailbox, uidValidity));
|
|
212
|
-
}
|
|
213
|
-
out.sort((a, b) => b.uid - a.uid);
|
|
214
|
-
return out;
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async get(uid: number, mailbox?: string): Promise<ReturnType<typeof toMessageMeta> | null> {
|
|
219
|
-
const box = mailbox ?? DEFAULT_MAILBOX;
|
|
220
|
-
return this.withMailbox(box, async (flow, uidValidity) => {
|
|
221
|
-
const msg = await flow.fetchOne(String(uid), previewFetchQuery(), { uid: true });
|
|
222
|
-
return msg === false ? null : toMessageMeta(msg, box, uidValidity);
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async search(options: {
|
|
227
|
-
query: string;
|
|
228
|
-
mailbox?: string;
|
|
229
|
-
limit?: number;
|
|
230
|
-
}): Promise<ReturnType<typeof toMessageMeta>[]> {
|
|
231
|
-
const mailbox = options.mailbox ?? DEFAULT_MAILBOX;
|
|
232
|
-
const limit = clampLimit(options.limit);
|
|
233
|
-
const q = options.query;
|
|
234
|
-
return this.withMailbox(mailbox, async (flow, uidValidity) => {
|
|
235
|
-
const uids = await flow.search(
|
|
236
|
-
{ or: [{ subject: q }, { from: q }, { to: q }] },
|
|
237
|
-
{ uid: true },
|
|
238
|
-
);
|
|
239
|
-
if (uids === false || uids.length === 0) {
|
|
240
|
-
return [];
|
|
241
|
-
}
|
|
242
|
-
const picked = uids.slice(-limit).sort((a, b) => b - a);
|
|
243
|
-
const out: ReturnType<typeof toMessageMeta>[] = [];
|
|
244
|
-
for await (const msg of flow.fetch(picked, previewFetchQuery(), { uid: true })) {
|
|
245
|
-
out.push(toMessageMeta(msg, mailbox, uidValidity));
|
|
246
|
-
}
|
|
247
|
-
out.sort((a, b) => b.uid - a.uid);
|
|
248
|
-
return out;
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// ---------------------------------------------------------------------------
|
|
254
|
-
// EmailSendMailer — nodemailer over smtp.mail.me.com:587 (STARTTLS)
|
|
255
|
-
// From is PINNED to the authenticated icloud_email (forced sender contract).
|
|
256
|
-
// ---------------------------------------------------------------------------
|
|
257
|
-
|
|
258
|
-
class AppleSmtpMailer implements EmailSendMailer {
|
|
259
|
-
private readonly transport: Transporter;
|
|
260
|
-
private readonly from: string;
|
|
261
|
-
|
|
262
|
-
constructor(user: string, pass: string) {
|
|
263
|
-
this.from = user;
|
|
264
|
-
this.transport = createTransport({
|
|
265
|
-
host: ICLOUD_SMTP_HOST,
|
|
266
|
-
port: ICLOUD_SMTP_PORT,
|
|
267
|
-
// STARTTLS — secure:false, port 587 (not 465). requireTLS forbids the
|
|
268
|
-
// plaintext fallback if the server fails to advertise STARTTLS (the
|
|
269
|
-
// credentials must never travel in the clear; iCloud always advertises it).
|
|
270
|
-
secure: false,
|
|
271
|
-
requireTLS: true,
|
|
272
|
-
auth: { user, pass },
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async send(input: {
|
|
277
|
-
to: string;
|
|
278
|
-
subject: string;
|
|
279
|
-
body: string;
|
|
280
|
-
cc?: string;
|
|
281
|
-
bcc?: string;
|
|
282
|
-
}): Promise<{
|
|
283
|
-
messageId: string | null;
|
|
284
|
-
accepted: readonly string[];
|
|
285
|
-
rejected: readonly string[];
|
|
286
|
-
}> {
|
|
287
|
-
const info = await this.transport.sendMail({
|
|
288
|
-
// From is always pinned to the authenticated iCloud email — callers cannot
|
|
289
|
-
// override it (forced sender contract per Global Constraints §Forced sender).
|
|
290
|
-
from: this.from,
|
|
291
|
-
to: input.to,
|
|
292
|
-
subject: input.subject,
|
|
293
|
-
text: input.body,
|
|
294
|
-
...(input.cc === undefined ? {} : { cc: input.cc }),
|
|
295
|
-
...(input.bcc === undefined ? {} : { bcc: input.bcc }),
|
|
296
|
-
});
|
|
297
|
-
return {
|
|
298
|
-
messageId: info.messageId ?? null,
|
|
299
|
-
accepted: (info.accepted ?? []).map(String),
|
|
300
|
-
rejected: (info.rejected ?? []).map(String),
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// ---------------------------------------------------------------------------
|
|
306
|
-
// DraftAppender — IMAP APPEND to "Drafts" via imapflow
|
|
307
|
-
// From is PINNED to the authenticated icloud_email (forced sender contract).
|
|
308
|
-
// ---------------------------------------------------------------------------
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Build a minimal RFC 5322 message from the given inputs.
|
|
312
|
-
* The From header is always set to `from` (the pinned icloud_email).
|
|
313
|
-
*/
|
|
314
|
-
function buildRfc822Message(from: string, input: DraftInput): string {
|
|
315
|
-
const lines: string[] = [];
|
|
316
|
-
lines.push(`From: ${from}`, `To: ${input.to}`);
|
|
317
|
-
if (input.cc !== undefined && input.cc !== "") {
|
|
318
|
-
lines.push(`Cc: ${input.cc}`);
|
|
319
|
-
}
|
|
320
|
-
if (input.bcc !== undefined && input.bcc !== "") {
|
|
321
|
-
lines.push(`Bcc: ${input.bcc}`);
|
|
322
|
-
}
|
|
323
|
-
// The body may contain non-ASCII (UTF-8) characters, so it cannot be declared
|
|
324
|
-
// 7bit. Base64-encode it (wrapped at 76 chars per RFC 2045) so the APPENDed
|
|
325
|
-
// message is well-formed regardless of content.
|
|
326
|
-
lines.push(
|
|
327
|
-
`Subject: ${input.subject}`,
|
|
328
|
-
"MIME-Version: 1.0",
|
|
329
|
-
"Content-Type: text/plain; charset=UTF-8",
|
|
330
|
-
"Content-Transfer-Encoding: base64",
|
|
331
|
-
"",
|
|
332
|
-
);
|
|
333
|
-
const encoded = Buffer.from(input.body, "utf8").toString("base64");
|
|
334
|
-
lines.push(encoded.replace(/.{76}/g, "$&\r\n"));
|
|
335
|
-
// RFC 5322 uses CRLF line endings.
|
|
336
|
-
return lines.join("\r\n");
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
class AppleDraftAppender implements DraftAppender {
|
|
340
|
-
private readonly config: {
|
|
341
|
-
host: string;
|
|
342
|
-
port: number;
|
|
343
|
-
user: string;
|
|
344
|
-
pass: string;
|
|
345
|
-
};
|
|
346
|
-
private readonly from: string;
|
|
347
|
-
|
|
348
|
-
constructor(user: string, pass: string) {
|
|
349
|
-
this.from = user;
|
|
350
|
-
this.config = {
|
|
351
|
-
host: ICLOUD_IMAP_HOST,
|
|
352
|
-
port: ICLOUD_IMAP_PORT,
|
|
353
|
-
user,
|
|
354
|
-
pass,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
async appendDraft(input: DraftInput): Promise<DraftResult> {
|
|
359
|
-
const flow = new ImapFlow({
|
|
360
|
-
host: this.config.host,
|
|
361
|
-
port: this.config.port,
|
|
362
|
-
secure: true,
|
|
363
|
-
auth: { user: this.config.user, pass: this.config.pass },
|
|
364
|
-
logger: false,
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
await flow.connect();
|
|
368
|
-
try {
|
|
369
|
-
// Build the RFC 5322 message with From pinned to the authenticated email.
|
|
370
|
-
const raw = buildRfc822Message(this.from, input);
|
|
371
|
-
const buf = Buffer.from(raw, "utf8");
|
|
372
|
-
|
|
373
|
-
// IMAP APPEND to the Drafts mailbox. imapflow returns the assigned UID
|
|
374
|
-
// when the server supports APPENDUID or UIDPLUS — it may return undefined
|
|
375
|
-
// when neither is available, which is valid.
|
|
376
|
-
const result = await flow.append(DRAFTS_MAILBOX, buf, [String.raw`\Draft`]);
|
|
377
|
-
// imapflow append() returns { uid?: number; ... } or undefined.
|
|
378
|
-
const uid = (result as { uid?: number } | undefined)?.uid ?? null;
|
|
379
|
-
return { uid: uid === undefined ? null : uid, mailbox: DRAFTS_MAILBOX };
|
|
380
|
-
} finally {
|
|
381
|
-
await flow.logout();
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// ---------------------------------------------------------------------------
|
|
387
|
-
// CalDavClient — tsdav DAVClient (two-phase: bootstrap discover then working)
|
|
388
|
-
//
|
|
389
|
-
// Per review #2: iCloud CalDAV authenticates against caldav.icloud.com but
|
|
390
|
-
// redirects to a user-specific host (p##-caldav.icloud.com). login() resolves
|
|
391
|
-
// the principal + calendar-home-set and binds the credentials to the resolved
|
|
392
|
-
// host so that Basic auth is applied to the resolved host on every subsequent
|
|
393
|
-
// request — without relying on transparent cross-host redirect forwarding.
|
|
394
|
-
// ---------------------------------------------------------------------------
|
|
395
|
-
|
|
396
|
-
class TsdavCalDavClient implements CalDavClient {
|
|
397
|
-
/**
|
|
398
|
-
* Discovered DAVCalendar objects keyed by their url. Populated after the
|
|
399
|
-
* first listCalendars() call so that putEvent/listEvents can obtain the
|
|
400
|
-
* full DAVCalendar object (which carries the account reference that tsdav
|
|
401
|
-
* needs for auth on the resolved p##-caldav.icloud.com host).
|
|
402
|
-
*/
|
|
403
|
-
private discovered: Map<string, DAVCalendar> = new Map();
|
|
404
|
-
private readonly davClient: DAVClient;
|
|
405
|
-
|
|
406
|
-
constructor(email: string, appPw: string) {
|
|
407
|
-
// login() will perform principal + calendar-home discovery and bind the
|
|
408
|
-
// credentials to all subsequent requests against the resolved
|
|
409
|
-
// p##-caldav.icloud.com host — satisfying the auth-on-redirect requirement.
|
|
410
|
-
this.davClient = new DAVClient({
|
|
411
|
-
serverUrl: ICLOUD_CALDAV_BOOTSTRAP_URL,
|
|
412
|
-
credentials: { username: email, password: appPw },
|
|
413
|
-
authMethod: "Basic",
|
|
414
|
-
defaultAccountType: "caldav",
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
async login(): Promise<void> {
|
|
419
|
-
await this.davClient.login();
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
async listCalendars(): Promise<CalendarRef[]> {
|
|
423
|
-
const cals = await this.davClient.fetchCalendars();
|
|
424
|
-
// Refresh the discovery cache.
|
|
425
|
-
this.discovered = new Map();
|
|
426
|
-
for (const cal of cals) {
|
|
427
|
-
this.discovered.set(cal.url, cal);
|
|
428
|
-
}
|
|
429
|
-
return cals.map((cal) => ({
|
|
430
|
-
url: cal.url,
|
|
431
|
-
displayName:
|
|
432
|
-
typeof cal.displayName === "string" && cal.displayName !== "" ? cal.displayName : cal.url,
|
|
433
|
-
}));
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Resolve a CalendarRef to the underlying DAVCalendar tsdav needs.
|
|
438
|
-
* If not found in cache, constructs a minimal DAVCalendar using just the url
|
|
439
|
-
* (tsdav fetchCalendarObjects only strictly requires `calendar.url`).
|
|
440
|
-
*/
|
|
441
|
-
private toDavCalendar(cal: CalendarRef): DAVCalendar {
|
|
442
|
-
const found = this.discovered.get(cal.url);
|
|
443
|
-
if (found !== undefined) {
|
|
444
|
-
return found;
|
|
445
|
-
}
|
|
446
|
-
// Construct a minimal DAVCalendar — tsdav only requires `.url` for
|
|
447
|
-
// fetchCalendarObjects, createCalendarObject, and deleteCalendarObject.
|
|
448
|
-
return { url: cal.url };
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
async listEvents(
|
|
452
|
-
cal: CalendarRef,
|
|
453
|
-
window: EventWindow,
|
|
454
|
-
): Promise<{ href: string; event: ParsedEvent }[]> {
|
|
455
|
-
const davCal = this.toDavCalendar(cal);
|
|
456
|
-
const objects = await this.davClient.fetchCalendarObjects({
|
|
457
|
-
calendar: davCal,
|
|
458
|
-
timeRange: { start: window.startUtc, end: window.endUtc },
|
|
459
|
-
// expand: true triggers server-side recurrence expansion (C:expand in
|
|
460
|
-
// the REPORT) — required per spec; no client-side RRULE engine.
|
|
461
|
-
expand: true,
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
const results: { href: string; event: ParsedEvent }[] = [];
|
|
465
|
-
for (const obj of objects) {
|
|
466
|
-
if (typeof obj.data !== "string" || obj.data === "") {
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
469
|
-
const events = parseICalendar(obj.data);
|
|
470
|
-
for (const event of events) {
|
|
471
|
-
results.push({ href: obj.url, event });
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
return results;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
async putEvent(cal: CalendarRef, uid: string, ics: string): Promise<{ href: string }> {
|
|
478
|
-
const davCal = this.toDavCalendar(cal);
|
|
479
|
-
// The UID is embedded into the object URL path segment; sanitize it so a
|
|
480
|
-
// summary-derived UID with `/`, `#`, `%`, etc. cannot corrupt the href.
|
|
481
|
-
const filename = caldavObjectFilename(uid);
|
|
482
|
-
await this.davClient.createCalendarObject({
|
|
483
|
-
calendar: davCal,
|
|
484
|
-
iCalString: ics,
|
|
485
|
-
filename,
|
|
486
|
-
});
|
|
487
|
-
// The href of the newly created object is conventionally the calendar URL
|
|
488
|
-
// joined with the filename. tsdav does not return a Location header
|
|
489
|
-
// wrapper, so we construct it from the calendar URL.
|
|
490
|
-
const href = cal.url.endsWith("/") ? `${cal.url}${filename}` : `${cal.url}/${filename}`;
|
|
491
|
-
return { href };
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
async deleteEvent(href: string): Promise<void> {
|
|
495
|
-
// etag: "" means "unconditional delete" — acceptable because we don't track
|
|
496
|
-
// etags in the CalendarRef/EventWindow flow.
|
|
497
|
-
await this.davClient.deleteCalendarObject({
|
|
498
|
-
calendarObject: { url: href, etag: "" },
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// ---------------------------------------------------------------------------
|
|
504
|
-
// Bootstrap + register + connect
|
|
505
|
-
// ---------------------------------------------------------------------------
|
|
506
|
-
|
|
507
25
|
const email = requireProcessEnv("APPLE_ICLOUD_EMAIL");
|
|
508
26
|
const appPw = requireProcessEnv("APPLE_ICLOUD_APP_PASSWORD");
|
|
509
27
|
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const tsdavClient = new TsdavCalDavClient(email, appPw);
|
|
515
|
-
// Perform CalDAV principal + calendar-home discovery so that subsequent
|
|
516
|
-
// requests are authenticated against the resolved p##-caldav.icloud.com host.
|
|
517
|
-
await tsdavClient.login();
|
|
28
|
+
const calendar = createAppleCalDavClient(email, appPw);
|
|
29
|
+
// CalDAV principal + calendar-home discovery, so subsequent requests are
|
|
30
|
+
// authenticated against the resolved p##-caldav.icloud.com host.
|
|
31
|
+
await calendar.login();
|
|
518
32
|
// Warm the calendar cache at boot so a cold apple_calendar_event_create/delete
|
|
519
33
|
// (before any apple_calendar_list) resolves a fully-formed DAVCalendar (with its
|
|
520
34
|
// account binding) rather than a bare { url } that could fail auth on the
|
|
521
35
|
// resolved host. Best-effort: ignore discovery errors here (tools re-discover).
|
|
522
36
|
try {
|
|
523
|
-
await
|
|
37
|
+
await calendar.listCalendars();
|
|
524
38
|
} catch {
|
|
525
39
|
// non-fatal; listEvents/list tools will re-discover on demand
|
|
526
40
|
}
|
|
527
41
|
|
|
528
|
-
const calendar: CalDavClient = tsdavClient;
|
|
529
|
-
// iCalendar DTSTAMP format (RFC 5545): YYYYMMDDTHHMMSSZ — buildVEvent emits the
|
|
530
|
-
// value verbatim, so it must NOT be ISO-8601 (no hyphens/colons/milliseconds).
|
|
531
|
-
const now = (): string =>
|
|
532
|
-
new Date()
|
|
533
|
-
.toISOString()
|
|
534
|
-
.replace(/[-:]/g, "")
|
|
535
|
-
.replace(/\.\d+Z$/, "Z");
|
|
536
|
-
|
|
537
42
|
const server = new McpServer({ name: "nimbus-apple", version: "0.1.0" });
|
|
538
43
|
|
|
539
|
-
registerAppleTools(server, {
|
|
44
|
+
registerAppleTools(server, {
|
|
45
|
+
client: createAppleImapClient(email, appPw),
|
|
46
|
+
mailer: createAppleSmtpMailer(email, appPw),
|
|
47
|
+
draftAppender: createAppleDraftAppender(email, appPw),
|
|
48
|
+
calendar,
|
|
49
|
+
now: () => icalTimestamp(),
|
|
50
|
+
});
|
|
540
51
|
|
|
541
52
|
await server.connect(new StdioServerTransport());
|