@nimbus-dev/connectors 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +18 -4
  2. package/connectors/airflow/src/search-filter.ts +2 -15
  3. package/connectors/airflow/src/server.ts +2 -87
  4. package/connectors/airflow/src/tools.ts +74 -0
  5. package/connectors/apple/src/adapters.ts +326 -0
  6. package/connectors/apple/src/apple-mail-core.ts +4 -8
  7. package/connectors/apple/src/calendar-tools.ts +28 -8
  8. package/connectors/apple/src/server.ts +23 -512
  9. package/connectors/apple/src/tools.ts +18 -44
  10. package/connectors/argocd/src/server.ts +25 -38
  11. package/connectors/athena/src/tools.ts +15 -34
  12. package/connectors/aws/src/server.ts +2 -183
  13. package/connectors/aws/src/tools.ts +197 -0
  14. package/connectors/azure/src/server.ts +2 -131
  15. package/connectors/azure/src/tools.ts +133 -0
  16. package/connectors/bigeye/src/search-filter.ts +4 -15
  17. package/connectors/bigeye/src/server.ts +38 -38
  18. package/connectors/bigquery/src/tools.ts +1 -26
  19. package/connectors/bitbucket/src/server.ts +2 -244
  20. package/connectors/bitbucket/src/tools.ts +241 -0
  21. package/connectors/bitrise/src/server.ts +2 -79
  22. package/connectors/bitrise/src/tools.ts +76 -0
  23. package/connectors/canva/src/search-filter.ts +2 -11
  24. package/connectors/canva/src/server.ts +2 -73
  25. package/connectors/canva/src/tools.ts +66 -0
  26. package/connectors/circleci/src/server.ts +2 -176
  27. package/connectors/circleci/src/tools.ts +193 -0
  28. package/connectors/cloud-logging/src/tools.ts +14 -27
  29. package/connectors/cloudwatch/src/tools.ts +15 -34
  30. package/connectors/codemagic/src/server.ts +2 -70
  31. package/connectors/codemagic/src/tools.ts +71 -0
  32. package/connectors/confluence/src/server.ts +2 -301
  33. package/connectors/confluence/src/tools.ts +320 -0
  34. package/connectors/dagster/src/server.ts +2 -214
  35. package/connectors/dagster/src/tools.ts +187 -0
  36. package/connectors/databricks/src/search-filter.ts +7 -7
  37. package/connectors/databricks/src/server.ts +2 -77
  38. package/connectors/databricks/src/tools.ts +66 -0
  39. package/connectors/datadog/src/server.ts +2 -48
  40. package/connectors/datadog/src/tools.ts +48 -0
  41. package/connectors/dataprofile/src/profile.ts +16 -46
  42. package/connectors/dbt/src/server.ts +2 -92
  43. package/connectors/dbt/src/tools.ts +80 -0
  44. package/connectors/dependencytrack/src/server.ts +2 -76
  45. package/connectors/dependencytrack/src/tools.ts +65 -0
  46. package/connectors/discord/src/server.ts +2 -85
  47. package/connectors/discord/src/tools.ts +82 -0
  48. package/connectors/elasticsearch/src/tools.ts +7 -37
  49. package/connectors/fastmail/src/jmap-client.ts +202 -0
  50. package/connectors/fastmail/src/jmap-core.ts +8 -21
  51. package/connectors/fastmail/src/server.ts +2 -187
  52. package/connectors/fastmail/src/tools.ts +4 -23
  53. package/connectors/figma/src/search-filter.ts +2 -11
  54. package/connectors/figma/src/server.ts +2 -113
  55. package/connectors/figma/src/tools.ts +99 -0
  56. package/connectors/firebase/src/server.ts +2 -90
  57. package/connectors/firebase/src/token.ts +2 -5
  58. package/connectors/firebase/src/tools.ts +106 -0
  59. package/connectors/flagsmith/src/server.ts +2 -98
  60. package/connectors/flagsmith/src/tools.ts +95 -0
  61. package/connectors/flux/src/server.ts +52 -53
  62. package/connectors/gcp/src/server.ts +2 -132
  63. package/connectors/gcp/src/tools.ts +123 -0
  64. package/connectors/github/src/server.ts +2 -328
  65. package/connectors/github/src/tools.ts +311 -0
  66. package/connectors/github-actions/src/server.ts +2 -205
  67. package/connectors/github-actions/src/tools.ts +209 -0
  68. package/connectors/gitlab/src/server.ts +2 -384
  69. package/connectors/gitlab/src/tools.ts +392 -0
  70. package/connectors/gmail/src/server.ts +2 -270
  71. package/connectors/gmail/src/tools.ts +209 -0
  72. package/connectors/google-drive/src/server.ts +2 -524
  73. package/connectors/google-drive/src/tools.ts +529 -0
  74. package/connectors/google-meet/src/server.ts +2 -82
  75. package/connectors/google-meet/src/tools.ts +87 -0
  76. package/connectors/google-photos/src/server.ts +2 -132
  77. package/connectors/google-photos/src/tools.ts +138 -0
  78. package/connectors/grafana/src/server.ts +2 -53
  79. package/connectors/grafana/src/tools.ts +53 -0
  80. package/connectors/great-expectations/src/gx-parse.ts +14 -45
  81. package/connectors/greenhouse/src/server.ts +2 -63
  82. package/connectors/greenhouse/src/tools.ts +68 -0
  83. package/connectors/hubspot/src/server.ts +2 -77
  84. package/connectors/hubspot/src/tools.ts +70 -0
  85. package/connectors/iac/src/server.ts +2 -137
  86. package/connectors/iac/src/tools.ts +152 -0
  87. package/connectors/imap/src/imap-core.ts +29 -82
  88. package/connectors/imap/src/server.ts +33 -220
  89. package/connectors/intercom/src/server.ts +2 -61
  90. package/connectors/intercom/src/tools.ts +58 -0
  91. package/connectors/jenkins/src/jenkins-api.ts +2 -6
  92. package/connectors/jenkins/src/server.ts +2 -215
  93. package/connectors/jenkins/src/tools.ts +240 -0
  94. package/connectors/jira/src/server.ts +2 -301
  95. package/connectors/jira/src/tools.ts +325 -0
  96. package/connectors/kubernetes/src/server.ts +2 -155
  97. package/connectors/kubernetes/src/tools.ts +159 -0
  98. package/connectors/launchdarkly/src/server.ts +2 -75
  99. package/connectors/launchdarkly/src/tools.ts +72 -0
  100. package/connectors/lever/src/search-filter.ts +4 -12
  101. package/connectors/lever/src/server.ts +2 -60
  102. package/connectors/lever/src/tools.ts +61 -0
  103. package/connectors/linear/src/server.ts +2 -459
  104. package/connectors/linear/src/tools.ts +482 -0
  105. package/connectors/localdb/src/sql-scan.ts +13 -41
  106. package/connectors/localdb/src/tools.ts +1 -1
  107. package/connectors/looker/src/search-filter.ts +2 -15
  108. package/connectors/looker/src/server.ts +35 -70
  109. package/connectors/mendeley/src/server.ts +2 -55
  110. package/connectors/mendeley/src/tools.ts +54 -0
  111. package/connectors/mercury/src/server.ts +2 -57
  112. package/connectors/mercury/src/tools.ts +50 -0
  113. package/connectors/metabase/src/search-filter.ts +4 -15
  114. package/connectors/metabase/src/server.ts +2 -79
  115. package/connectors/metabase/src/tools.ts +64 -0
  116. package/connectors/miro/src/server.ts +2 -72
  117. package/connectors/miro/src/tools.ts +65 -0
  118. package/connectors/mlflow/src/search-filter.ts +3 -7
  119. package/connectors/mlflow/src/server.ts +20 -40
  120. package/connectors/monte-carlo/src/server.ts +50 -57
  121. package/connectors/netlify/src/search-filter.ts +6 -14
  122. package/connectors/netlify/src/server.ts +2 -60
  123. package/connectors/netlify/src/tools.ts +53 -0
  124. package/connectors/newrelic/src/server.ts +2 -40
  125. package/connectors/newrelic/src/tools.ts +37 -0
  126. package/connectors/notion/src/server.ts +2 -335
  127. package/connectors/notion/src/tools.ts +344 -0
  128. package/connectors/obsidian/src/server.ts +2 -425
  129. package/connectors/obsidian/src/tools.ts +466 -0
  130. package/connectors/onedrive/src/server.ts +2 -249
  131. package/connectors/onedrive/src/tools.ts +253 -0
  132. package/connectors/outlook/src/server.ts +2 -351
  133. package/connectors/outlook/src/tools.ts +357 -0
  134. package/connectors/pagerduty/src/server.ts +2 -132
  135. package/connectors/pagerduty/src/tools.ts +126 -0
  136. package/connectors/pipedrive/src/server.ts +2 -58
  137. package/connectors/pipedrive/src/tools.ts +63 -0
  138. package/connectors/powerbi/src/search-filter.ts +2 -15
  139. package/connectors/powerbi/src/server.ts +43 -68
  140. package/connectors/prefect/src/search-filter.ts +3 -16
  141. package/connectors/prefect/src/server.ts +2 -90
  142. package/connectors/prefect/src/tools.ts +83 -0
  143. package/connectors/protonmail/src/mail-core.ts +16 -75
  144. package/connectors/protonmail/src/server.ts +34 -214
  145. package/connectors/raindrop/src/server.ts +2 -103
  146. package/connectors/raindrop/src/tools.ts +103 -0
  147. package/connectors/ramp/src/server.ts +2 -105
  148. package/connectors/ramp/src/tools.ts +98 -0
  149. package/connectors/readwise/src/server.ts +2 -88
  150. package/connectors/readwise/src/tools.ts +81 -0
  151. package/connectors/sagemaker/src/tools.ts +19 -38
  152. package/connectors/salesforce/src/search-filter.ts +4 -11
  153. package/connectors/salesforce/src/server.ts +2 -90
  154. package/connectors/salesforce/src/tools.ts +75 -0
  155. package/connectors/semgrep/src/search-filter.ts +3 -10
  156. package/connectors/semgrep/src/server.ts +2 -104
  157. package/connectors/semgrep/src/tools.ts +92 -0
  158. package/connectors/sentry/src/server.ts +2 -58
  159. package/connectors/sentry/src/tools.ts +53 -0
  160. package/connectors/slack/src/server.ts +2 -352
  161. package/connectors/slack/src/tools.ts +376 -0
  162. package/connectors/snowflake/src/search-filter.ts +4 -19
  163. package/connectors/snowflake/src/server.ts +6 -25
  164. package/connectors/snyk/src/server.ts +2 -140
  165. package/connectors/snyk/src/tools.ts +136 -0
  166. package/connectors/sonarqube/src/server.ts +2 -111
  167. package/connectors/sonarqube/src/tools.ts +98 -0
  168. package/connectors/stackoverflow/src/server.ts +2 -77
  169. package/connectors/stackoverflow/src/tools.ts +66 -0
  170. package/connectors/storybook/src/storybook-parse.ts +3 -6
  171. package/connectors/stripe/src/server.ts +2 -61
  172. package/connectors/stripe/src/tools.ts +54 -0
  173. package/connectors/superset/src/search-filter.ts +4 -15
  174. package/connectors/superset/src/server.ts +2 -115
  175. package/connectors/superset/src/tools.ts +98 -0
  176. package/connectors/tableau/src/search-filter.ts +2 -15
  177. package/connectors/tableau/src/server.ts +13 -65
  178. package/connectors/teams/src/server.ts +2 -249
  179. package/connectors/teams/src/tools.ts +283 -0
  180. package/connectors/testflight/src/server.ts +2 -70
  181. package/connectors/testflight/src/tools.ts +79 -0
  182. package/connectors/vercel/src/search-filter.ts +2 -10
  183. package/connectors/vercel/src/server.ts +2 -75
  184. package/connectors/vercel/src/tools.ts +73 -0
  185. package/connectors/vertex-ai/src/tools.ts +23 -33
  186. package/connectors/wiz/src/search-filter.ts +6 -30
  187. package/connectors/wiz/src/server.ts +2 -188
  188. package/connectors/wiz/src/tools.ts +168 -0
  189. package/connectors/zendesk/src/server.ts +2 -73
  190. package/connectors/zendesk/src/tools.ts +60 -0
  191. package/connectors/zoom/src/search-filter.ts +1 -7
  192. package/connectors/zoom/src/server.ts +2 -94
  193. package/connectors/zoom/src/tools.ts +93 -0
  194. package/connectors/zotero/src/server.ts +2 -78
  195. package/connectors/zotero/src/tools.ts +67 -0
  196. package/package.json +15 -12
  197. package/shared/access-token-cache.ts +123 -0
  198. package/shared/atlassian-json-fetch.ts +6 -16
  199. package/shared/audit-chain.ts +13 -6
  200. package/shared/cli-json-kit.ts +150 -0
  201. package/shared/collection-tool-kit.ts +143 -0
  202. package/shared/connector-mode.ts +3 -2
  203. package/shared/cursor-list-tool.ts +29 -0
  204. package/shared/env-json-api.ts +209 -0
  205. package/shared/fetch-json-text.ts +27 -0
  206. package/shared/github-rest.ts +62 -0
  207. package/shared/graphql-json.ts +60 -0
  208. package/shared/imap-mail-core.ts +91 -0
  209. package/shared/imap-tool-kit.ts +57 -23
  210. package/shared/imapflow-adapter.ts +417 -0
  211. package/shared/rest-tool-kit.ts +77 -7
  212. package/shared/run-cli-json.ts +38 -16
  213. package/shared/search-filter.ts +58 -18
  214. package/shared/walk-files.ts +118 -0
  215. package/standalone/src/launcher.ts +19 -7
@@ -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());
@@ -1,8 +1,9 @@
1
- import { z } from "zod";
2
1
  import { type ConsentServer, createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
3
-
4
- import { headerLine } from "../../../shared/header-safe.ts";
5
- import { registerEmailConnectorTools } from "../../../shared/imap-tool-kit.ts";
2
+ import {
3
+ emailToolSchemas,
4
+ outgoingMail,
5
+ registerEmailConnectorTools,
6
+ } from "../../../shared/imap-tool-kit.ts";
6
7
  import { mcpJsonResult } from "../../../shared/mcp-tool-kit.ts";
7
8
  import {
8
9
  type DraftAppender,
@@ -11,19 +12,11 @@ import {
11
12
  formatAddress,
12
13
  } from "./apple-mail-core.ts";
13
14
  import type { CalDavClient } from "./caldav-core.ts";
14
- import { type CalendarToolConfig, registerAppleCalendarTools } from "./calendar-tools.ts";
15
-
16
- // ---------------------------------------------------------------------------
17
- // Zod schema for the draft tool
18
- // ---------------------------------------------------------------------------
19
-
20
- const draftArgs = z.object({
21
- to: headerLine({ min: 1 }),
22
- subject: headerLine({ min: 1, max: 998 }),
23
- body: z.string().max(1_000_000),
24
- cc: headerLine().optional(),
25
- bcc: headerLine().optional(),
26
- });
15
+ import {
16
+ APPLE_WRITE_SCOPE,
17
+ type CalendarToolConfig,
18
+ registerAppleCalendarTools,
19
+ } from "./calendar-tools.ts";
27
20
 
28
21
  // ---------------------------------------------------------------------------
29
22
  // Tool descriptions
@@ -65,13 +58,10 @@ export function registerAppleTools(
65
58
  ): void {
66
59
  const { client, mailer, draftAppender, calendar, now, calendarConfig } = params;
67
60
 
68
- // The four shared email tools (list/get/search/mail_send) via the shared kit.
69
- const registerWriteTool = createWriteToolRegistrar(server, {
70
- connector: "apple",
71
- scopeEnv: "NIMBUS_MCP_APPLE_WRITE_SCOPE",
72
- scopeKinds: ["recipient"],
73
- });
61
+ // ONE registrar for every apple write, mail and calendar alike: see APPLE_WRITE_SCOPE.
62
+ const registerWriteTool = createWriteToolRegistrar(server, APPLE_WRITE_SCOPE);
74
63
 
64
+ // The four shared email tools (list/get/search/mail_send) via the shared kit.
75
65
  registerEmailConnectorTools({
76
66
  server,
77
67
  registerWriteTool,
@@ -92,27 +82,10 @@ export function registerAppleTools(
92
82
  scopeTargetOf: (p) => ({ kind: "recipient", value: p.to }),
93
83
  },
94
84
  "Save a new email to the iCloud Mail Drafts folder via IMAP APPEND.",
95
- draftArgs,
96
- async (parsedData) => {
97
- const parsed = { success: true as const, data: parsedData };
98
- const input: {
99
- to: string;
100
- subject: string;
101
- body: string;
102
- cc?: string;
103
- bcc?: string;
104
- } = {
105
- to: parsed.data.to,
106
- subject: parsed.data.subject,
107
- body: parsed.data.body,
108
- };
109
- if (parsed.data.cc !== undefined && parsed.data.cc !== "") {
110
- input.cc = parsed.data.cc;
111
- }
112
- if (parsed.data.bcc !== undefined && parsed.data.bcc !== "") {
113
- input.bcc = parsed.data.bcc;
114
- }
115
- const result = await draftAppender.appendDraft(input);
85
+ // A draft takes exactly what a send does.
86
+ emailToolSchemas.sendArgs,
87
+ async (args) => {
88
+ const result = await draftAppender.appendDraft(outgoingMail(args));
116
89
  return mcpJsonResult({ item: result });
117
90
  },
118
91
  );
@@ -122,6 +95,7 @@ export function registerAppleTools(
122
95
  calendar,
123
96
  now,
124
97
  config: calendarConfig,
98
+ registerWriteTool,
125
99
  });
126
100
  }
127
101