@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.
Files changed (220) hide show
  1. package/README.md +19 -7
  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 +26 -43
  12. package/connectors/aws/src/server.ts +2 -183
  13. package/connectors/aws/src/tools.ts +199 -0
  14. package/connectors/azure/src/server.ts +2 -131
  15. package/connectors/azure/src/tools.ts +134 -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 +19 -35
  29. package/connectors/cloudwatch/src/tools.ts +20 -38
  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 +153 -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 +160 -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 +21 -40
  152. package/connectors/salesforce/src/search-filter.ts +4 -11
  153. package/connectors/salesforce/src/server.ts +2 -90
  154. package/connectors/salesforce/src/tools.ts +75 -0
  155. package/connectors/semgrep/src/search-filter.ts +3 -10
  156. package/connectors/semgrep/src/server.ts +2 -104
  157. package/connectors/semgrep/src/tools.ts +92 -0
  158. package/connectors/sentry/src/server.ts +2 -58
  159. package/connectors/sentry/src/tools.ts +53 -0
  160. package/connectors/slack/src/server.ts +2 -352
  161. package/connectors/slack/src/tools.ts +376 -0
  162. package/connectors/snowflake/src/search-filter.ts +4 -19
  163. package/connectors/snowflake/src/server.ts +6 -25
  164. package/connectors/snyk/src/server.ts +2 -140
  165. package/connectors/snyk/src/tools.ts +136 -0
  166. package/connectors/sonarqube/src/server.ts +2 -111
  167. package/connectors/sonarqube/src/tools.ts +98 -0
  168. package/connectors/stackoverflow/src/server.ts +2 -77
  169. package/connectors/stackoverflow/src/tools.ts +66 -0
  170. package/connectors/storybook/src/storybook-parse.ts +3 -6
  171. package/connectors/stripe/src/server.ts +2 -61
  172. package/connectors/stripe/src/tools.ts +54 -0
  173. package/connectors/superset/src/search-filter.ts +4 -15
  174. package/connectors/superset/src/server.ts +2 -115
  175. package/connectors/superset/src/tools.ts +98 -0
  176. package/connectors/tableau/src/search-filter.ts +2 -15
  177. package/connectors/tableau/src/server.ts +13 -65
  178. package/connectors/teams/src/server.ts +2 -249
  179. package/connectors/teams/src/tools.ts +283 -0
  180. package/connectors/testflight/src/server.ts +2 -70
  181. package/connectors/testflight/src/tools.ts +79 -0
  182. package/connectors/vercel/src/search-filter.ts +2 -10
  183. package/connectors/vercel/src/server.ts +2 -75
  184. package/connectors/vercel/src/tools.ts +73 -0
  185. package/connectors/vertex-ai/src/tools.ts +23 -33
  186. package/connectors/wiz/src/search-filter.ts +6 -30
  187. package/connectors/wiz/src/server.ts +2 -188
  188. package/connectors/wiz/src/tools.ts +168 -0
  189. package/connectors/zendesk/src/server.ts +2 -73
  190. package/connectors/zendesk/src/tools.ts +60 -0
  191. package/connectors/zoom/src/search-filter.ts +1 -7
  192. package/connectors/zoom/src/server.ts +2 -94
  193. package/connectors/zoom/src/tools.ts +93 -0
  194. package/connectors/zotero/src/server.ts +2 -78
  195. package/connectors/zotero/src/tools.ts +67 -0
  196. package/package.json +14 -12
  197. package/shared/access-token-cache.ts +123 -0
  198. package/shared/atlassian-json-fetch.ts +6 -16
  199. package/shared/audit-chain.ts +601 -34
  200. package/shared/cli-json-kit.ts +191 -0
  201. package/shared/collection-tool-kit.ts +143 -0
  202. package/shared/connector-mode.ts +3 -2
  203. package/shared/consent-kit.ts +81 -26
  204. package/shared/cursor-list-tool.ts +29 -0
  205. package/shared/env-json-api.ts +209 -0
  206. package/shared/fetch-json-text.ts +27 -0
  207. package/shared/github-rest.ts +62 -0
  208. package/shared/graphql-json.ts +60 -0
  209. package/shared/imap-mail-core.ts +91 -0
  210. package/shared/imap-tool-kit.ts +57 -23
  211. package/shared/imapflow-adapter.ts +417 -0
  212. package/shared/nimbus-spawn.ts +84 -27
  213. package/shared/rest-tool-kit.ts +77 -7
  214. package/shared/run-cli-json.ts +38 -16
  215. package/shared/safe-cli-arg.ts +139 -17
  216. package/shared/search-filter.ts +58 -18
  217. package/shared/walk-files.ts +118 -0
  218. package/shared/windows-batch-args.ts +273 -0
  219. package/shared/write-budget.ts +100 -0
  220. package/standalone/src/launcher.ts +19 -7
@@ -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",
@@ -1,9 +1,10 @@
1
1
  /**
2
- * iCloud Mail + Calendar MCP server.
2
+ * iCloud Mail + Calendar MCP server — entry point.
3
3
  *
4
- * Real clients (IMAP/SMTP/CalDAV) are confined here (coverage-excluded).
5
- * All pure logic lives in apple-mail-core.ts, caldav-core.ts, calendar-tools.ts,
6
- * and tools.ts which are fully unit-tested.
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
- clampLimit,
27
- type DraftAppender,
28
- type DraftInput,
29
- type DraftResult,
30
- type EmailReadClient,
31
- type EmailSendMailer,
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 client = new AppleImapClient(email, appPw);
511
- const mailer = new AppleSmtpMailer(email, appPw);
512
- const draftAppender = new AppleDraftAppender(email, appPw);
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 tsdavClient.listCalendars();
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, { client, mailer, draftAppender, calendar, now });
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());