@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
@@ -0,0 +1,202 @@
1
+ /**
2
+ * The real Fastmail JMAP client over `fetch`.
3
+ *
4
+ * Moved out of `server.ts` for the same reason as every other connector's
5
+ * transport: an entry point that constructs its client at module scope cannot
6
+ * be imported by a test, so none of the session discovery, capability checks or
7
+ * request building below was reachable from one.
8
+ */
9
+
10
+ import { optionalEnv } from "../../../shared/env-json-api.ts";
11
+ import { requireProcessEnv } from "../../../shared/mcp-tool-kit.ts";
12
+ import {
13
+ asRecord,
14
+ asString,
15
+ buildGetRequest,
16
+ buildListRequest,
17
+ buildSearchRequest,
18
+ CORE_CAPABILITY,
19
+ extractEmailList,
20
+ type JmapClient,
21
+ type JmapEmailView,
22
+ type JmapSession,
23
+ MAIL_CAPABILITY,
24
+ methodResponseArgs,
25
+ parseSession,
26
+ type SendMailInput,
27
+ type SendMailResult,
28
+ SUBMISSION_CAPABILITY,
29
+ validateApiUrl,
30
+ viewEmail,
31
+ } from "./jmap-core.ts";
32
+
33
+ const DEFAULT_BASE_URL = "https://api.fastmail.com";
34
+
35
+ /** Parse a comma-separated address list (`Name <a@x>, b@y`) → JMAP address objects. */
36
+ function parseAddressList(raw: string): Array<{ email: string }> {
37
+ return raw
38
+ .split(",")
39
+ .map((part) => {
40
+ // Extract the address inside `<...>` without a backtracking regex.
41
+ const open = part.indexOf("<");
42
+ const close = open === -1 ? -1 : part.indexOf(">", open + 1);
43
+ const inner = open !== -1 && close !== -1 ? part.slice(open + 1, close) : part;
44
+ return inner.trim();
45
+ })
46
+ .filter((email) => email !== "")
47
+ .map((email) => ({ email }));
48
+ }
49
+
50
+ /** Real Fastmail JMAP client over fetch. Discovers the session once, then reuses it. */
51
+ class FetchJmapClient implements JmapClient {
52
+ private readonly baseUrl: string;
53
+ private readonly token: string;
54
+ private session: JmapSession | null = null;
55
+
56
+ constructor() {
57
+ this.token = requireProcessEnv("FASTMAIL_API_TOKEN");
58
+ // Only ONE trailing slash is dropped here — not `optionalBaseUrl`'s strip-them-all.
59
+ this.baseUrl = optionalEnv("FASTMAIL_BASE_URL", DEFAULT_BASE_URL).replace(/\/$/, "");
60
+ }
61
+
62
+ private headers(): Record<string, string> {
63
+ return {
64
+ Authorization: `Bearer ${this.token}`,
65
+ "Content-Type": "application/json",
66
+ Accept: "application/json",
67
+ };
68
+ }
69
+
70
+ private async ensureSession(): Promise<JmapSession> {
71
+ if (this.session !== null) {
72
+ return this.session;
73
+ }
74
+ const res = await fetch(`${this.baseUrl}/jmap/session`, { headers: this.headers() });
75
+ if (!res.ok) {
76
+ throw new Error(`JMAP session failed: HTTP ${String(res.status)}`);
77
+ }
78
+ const session = parseSession(await res.json());
79
+ if (session === null) {
80
+ throw new Error("JMAP session response missing apiUrl / mail account");
81
+ }
82
+ // The apiUrl is server-controlled; pin it to the configured host before any
83
+ // authenticated POST is sent there (SSRF guard).
84
+ const validated: JmapSession = {
85
+ ...session,
86
+ apiUrl: validateApiUrl(session.apiUrl, this.baseUrl),
87
+ };
88
+ this.session = validated;
89
+ return validated;
90
+ }
91
+
92
+ private async api(body: unknown): Promise<unknown> {
93
+ const session = await this.ensureSession();
94
+ const res = await fetch(session.apiUrl, {
95
+ method: "POST",
96
+ headers: this.headers(),
97
+ body: JSON.stringify(body),
98
+ });
99
+ if (!res.ok) {
100
+ throw new Error(`JMAP api failed: HTTP ${String(res.status)}`);
101
+ }
102
+ return res.json();
103
+ }
104
+
105
+ private async views(request: unknown): Promise<JmapEmailView[]> {
106
+ const out: JmapEmailView[] = [];
107
+ for (const raw of extractEmailList(await this.api(request))) {
108
+ const v = viewEmail(raw);
109
+ if (v !== null) {
110
+ out.push(v);
111
+ }
112
+ }
113
+ return out;
114
+ }
115
+
116
+ async list(limit: number): Promise<JmapEmailView[]> {
117
+ const session = await this.ensureSession();
118
+ return this.views(buildListRequest(session.accountId, limit));
119
+ }
120
+
121
+ async search(query: string, limit: number): Promise<JmapEmailView[]> {
122
+ const session = await this.ensureSession();
123
+ return this.views(buildSearchRequest(session.accountId, query, limit));
124
+ }
125
+
126
+ async get(id: string): Promise<JmapEmailView | null> {
127
+ const session = await this.ensureSession();
128
+ const views = await this.views(buildGetRequest(session.accountId, id));
129
+ return views[0] ?? null;
130
+ }
131
+
132
+ async send(input: SendMailInput): Promise<SendMailResult> {
133
+ const session = await this.ensureSession();
134
+ const { accountId } = session;
135
+
136
+ // Round 1: discover the sending identity + the Drafts mailbox.
137
+ const discovery = await this.api({
138
+ using: [CORE_CAPABILITY, MAIL_CAPABILITY, SUBMISSION_CAPABILITY],
139
+ methodCalls: [
140
+ ["Identity/get", { accountId, ids: null }, "id"],
141
+ ["Mailbox/query", { accountId, filter: { role: "drafts" } }, "mq"],
142
+ ],
143
+ });
144
+ const identities = methodResponseArgs(discovery, "Identity/get")?.["list"];
145
+ const identity = Array.isArray(identities) ? asRecord(identities[0]) : null;
146
+ const identityId = identity === null ? null : asString(identity["id"]);
147
+ const identityEmail = identity === null ? null : asString(identity["email"]);
148
+ const mqIds = methodResponseArgs(discovery, "Mailbox/query")?.["ids"];
149
+ const draftMailboxId = Array.isArray(mqIds) ? asString(mqIds[0]) : null;
150
+ if (identityId === null || identityEmail === null || draftMailboxId === null) {
151
+ throw new Error("JMAP send: could not resolve sending identity or Drafts mailbox");
152
+ }
153
+
154
+ // Round 2: create the draft email + submit it.
155
+ const created = await this.api({
156
+ using: [CORE_CAPABILITY, MAIL_CAPABILITY, SUBMISSION_CAPABILITY],
157
+ methodCalls: [
158
+ [
159
+ "Email/set",
160
+ {
161
+ accountId,
162
+ create: {
163
+ draft: {
164
+ mailboxIds: { [draftMailboxId]: true },
165
+ keywords: { $draft: true, $seen: true },
166
+ from: [{ email: identityEmail }],
167
+ to: parseAddressList(input.to),
168
+ ...(input.cc === undefined ? {} : { cc: parseAddressList(input.cc) }),
169
+ ...(input.bcc === undefined ? {} : { bcc: parseAddressList(input.bcc) }),
170
+ subject: input.subject,
171
+ bodyStructure: { type: "text/plain", partId: "b" },
172
+ bodyValues: { b: { value: input.body } },
173
+ },
174
+ },
175
+ },
176
+ "es",
177
+ ],
178
+ [
179
+ "EmailSubmission/set",
180
+ {
181
+ accountId,
182
+ create: { sub: { emailId: "#draft", identityId } },
183
+ },
184
+ "sub",
185
+ ],
186
+ ],
187
+ });
188
+ const emailCreated = asRecord(methodResponseArgs(created, "Email/set")?.["created"]);
189
+ const emailId =
190
+ emailCreated === null ? null : asString(asRecord(emailCreated["draft"])?.["id"]);
191
+ const subCreated = asRecord(methodResponseArgs(created, "EmailSubmission/set")?.["created"]);
192
+ const submissionId = subCreated === null ? null : asString(asRecord(subCreated["sub"])?.["id"]);
193
+ return { emailId, submissionId };
194
+ }
195
+ }
196
+
197
+ /** The client the entry point uses. Credentials are read at construction. */
198
+ export function createFetchJmapClient(): JmapClient {
199
+ return new FetchJmapClient();
200
+ }
201
+
202
+ export { parseAddressList };
@@ -17,6 +17,7 @@
17
17
  */
18
18
 
19
19
  import type { JmapEmailView } from "@nimbus-dev/sdk";
20
+ import type { OutgoingMail } from "../../../shared/imap-tool-kit.ts";
20
21
 
21
22
  export {
22
23
  asRecord,
@@ -45,17 +46,14 @@ export {
45
46
  viewEmail,
46
47
  } from "@nimbus-dev/sdk";
47
48
 
48
- const DEFAULT_LIST_LIMIT = 50;
49
- const MAX_LIST_LIMIT = 200;
49
+ /**
50
+ * The list/search limit clamp (default 50, ceiling 200) is the one every mail connector
51
+ * applies; the tools' own schemas cap `limit` at the same 200.
52
+ */
53
+ export { clampLimit } from "../../../shared/imap-mail-core.ts";
50
54
 
51
- /** Outgoing message for the SMTP/JMAP submission send tool. */
52
- export interface SendMailInput {
53
- readonly to: string;
54
- readonly subject: string;
55
- readonly body: string;
56
- readonly cc?: string;
57
- readonly bcc?: string;
58
- }
55
+ /** Outgoing message for the JMAP submission send tool — the shared mail-send shape. */
56
+ export type SendMailInput = OutgoingMail;
59
57
 
60
58
  export interface SendMailResult {
61
59
  readonly emailId: string | null;
@@ -73,14 +71,3 @@ export interface JmapClient {
73
71
  search(query: string, limit: number): Promise<JmapEmailView[]>;
74
72
  send(input: SendMailInput): Promise<SendMailResult>;
75
73
  }
76
-
77
- export function clampLimit(limit: number | undefined, fallback = DEFAULT_LIST_LIMIT): number {
78
- if (limit === undefined || !Number.isFinite(limit)) {
79
- return fallback;
80
- }
81
- const n = Math.trunc(limit);
82
- if (n < 1) {
83
- return 1;
84
- }
85
- return Math.min(n, MAX_LIST_LIMIT);
86
- }
@@ -1,194 +1,9 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
-
4
- import { requireProcessEnv } from "../../../shared/mcp-tool-kit.ts";
5
- import {
6
- asRecord,
7
- asString,
8
- buildGetRequest,
9
- buildListRequest,
10
- buildSearchRequest,
11
- CORE_CAPABILITY,
12
- extractEmailList,
13
- type JmapClient,
14
- type JmapEmailView,
15
- type JmapSession,
16
- MAIL_CAPABILITY,
17
- methodResponseArgs,
18
- parseSession,
19
- type SendMailInput,
20
- type SendMailResult,
21
- SUBMISSION_CAPABILITY,
22
- validateApiUrl,
23
- viewEmail,
24
- } from "./jmap-core.ts";
3
+ import { createFetchJmapClient } from "./jmap-client.ts";
25
4
  import { registerFastmailTools } from "./tools.ts";
26
5
 
27
- const DEFAULT_BASE_URL = "https://api.fastmail.com";
28
-
29
- /** Parse a comma-separated address list (`Name <a@x>, b@y`) → JMAP address objects. */
30
- function parseAddressList(raw: string): Array<{ email: string }> {
31
- return raw
32
- .split(",")
33
- .map((part) => {
34
- // Extract the address inside `<...>` without a backtracking regex.
35
- const open = part.indexOf("<");
36
- const close = open === -1 ? -1 : part.indexOf(">", open + 1);
37
- const inner = open !== -1 && close !== -1 ? part.slice(open + 1, close) : part;
38
- return inner.trim();
39
- })
40
- .filter((email) => email !== "")
41
- .map((email) => ({ email }));
42
- }
43
-
44
- /** Real Fastmail JMAP client over fetch. Discovers the session once, then reuses it. */
45
- class FetchJmapClient implements JmapClient {
46
- private readonly baseUrl: string;
47
- private readonly token: string;
48
- private session: JmapSession | null = null;
49
-
50
- constructor() {
51
- this.token = requireProcessEnv("FASTMAIL_API_TOKEN");
52
- const base = process.env["FASTMAIL_BASE_URL"]?.trim();
53
- this.baseUrl = (base === undefined || base === "" ? DEFAULT_BASE_URL : base).replace(/\/$/, "");
54
- }
55
-
56
- private headers(): Record<string, string> {
57
- return {
58
- Authorization: `Bearer ${this.token}`,
59
- "Content-Type": "application/json",
60
- Accept: "application/json",
61
- };
62
- }
63
-
64
- private async ensureSession(): Promise<JmapSession> {
65
- if (this.session !== null) {
66
- return this.session;
67
- }
68
- const res = await fetch(`${this.baseUrl}/jmap/session`, { headers: this.headers() });
69
- if (!res.ok) {
70
- throw new Error(`JMAP session failed: HTTP ${String(res.status)}`);
71
- }
72
- const session = parseSession(await res.json());
73
- if (session === null) {
74
- throw new Error("JMAP session response missing apiUrl / mail account");
75
- }
76
- // The apiUrl is server-controlled; pin it to the configured host before any
77
- // authenticated POST is sent there (SSRF guard).
78
- const validated: JmapSession = {
79
- ...session,
80
- apiUrl: validateApiUrl(session.apiUrl, this.baseUrl),
81
- };
82
- this.session = validated;
83
- return validated;
84
- }
85
-
86
- private async api(body: unknown): Promise<unknown> {
87
- const session = await this.ensureSession();
88
- const res = await fetch(session.apiUrl, {
89
- method: "POST",
90
- headers: this.headers(),
91
- body: JSON.stringify(body),
92
- });
93
- if (!res.ok) {
94
- throw new Error(`JMAP api failed: HTTP ${String(res.status)}`);
95
- }
96
- return res.json();
97
- }
98
-
99
- private async views(request: unknown): Promise<JmapEmailView[]> {
100
- const out: JmapEmailView[] = [];
101
- for (const raw of extractEmailList(await this.api(request))) {
102
- const v = viewEmail(raw);
103
- if (v !== null) {
104
- out.push(v);
105
- }
106
- }
107
- return out;
108
- }
109
-
110
- async list(limit: number): Promise<JmapEmailView[]> {
111
- const session = await this.ensureSession();
112
- return this.views(buildListRequest(session.accountId, limit));
113
- }
114
-
115
- async search(query: string, limit: number): Promise<JmapEmailView[]> {
116
- const session = await this.ensureSession();
117
- return this.views(buildSearchRequest(session.accountId, query, limit));
118
- }
119
-
120
- async get(id: string): Promise<JmapEmailView | null> {
121
- const session = await this.ensureSession();
122
- const views = await this.views(buildGetRequest(session.accountId, id));
123
- return views[0] ?? null;
124
- }
125
-
126
- async send(input: SendMailInput): Promise<SendMailResult> {
127
- const session = await this.ensureSession();
128
- const { accountId } = session;
129
-
130
- // Round 1: discover the sending identity + the Drafts mailbox.
131
- const discovery = await this.api({
132
- using: [CORE_CAPABILITY, MAIL_CAPABILITY, SUBMISSION_CAPABILITY],
133
- methodCalls: [
134
- ["Identity/get", { accountId, ids: null }, "id"],
135
- ["Mailbox/query", { accountId, filter: { role: "drafts" } }, "mq"],
136
- ],
137
- });
138
- const identities = methodResponseArgs(discovery, "Identity/get")?.["list"];
139
- const identity = Array.isArray(identities) ? asRecord(identities[0]) : null;
140
- const identityId = identity === null ? null : asString(identity["id"]);
141
- const identityEmail = identity === null ? null : asString(identity["email"]);
142
- const mqIds = methodResponseArgs(discovery, "Mailbox/query")?.["ids"];
143
- const draftMailboxId = Array.isArray(mqIds) ? asString(mqIds[0]) : null;
144
- if (identityId === null || identityEmail === null || draftMailboxId === null) {
145
- throw new Error("JMAP send: could not resolve sending identity or Drafts mailbox");
146
- }
147
-
148
- // Round 2: create the draft email + submit it.
149
- const created = await this.api({
150
- using: [CORE_CAPABILITY, MAIL_CAPABILITY, SUBMISSION_CAPABILITY],
151
- methodCalls: [
152
- [
153
- "Email/set",
154
- {
155
- accountId,
156
- create: {
157
- draft: {
158
- mailboxIds: { [draftMailboxId]: true },
159
- keywords: { $draft: true, $seen: true },
160
- from: [{ email: identityEmail }],
161
- to: parseAddressList(input.to),
162
- ...(input.cc === undefined ? {} : { cc: parseAddressList(input.cc) }),
163
- ...(input.bcc === undefined ? {} : { bcc: parseAddressList(input.bcc) }),
164
- subject: input.subject,
165
- bodyStructure: { type: "text/plain", partId: "b" },
166
- bodyValues: { b: { value: input.body } },
167
- },
168
- },
169
- },
170
- "es",
171
- ],
172
- [
173
- "EmailSubmission/set",
174
- {
175
- accountId,
176
- create: { sub: { emailId: "#draft", identityId } },
177
- },
178
- "sub",
179
- ],
180
- ],
181
- });
182
- const emailCreated = asRecord(methodResponseArgs(created, "Email/set")?.["created"]);
183
- const emailId =
184
- emailCreated === null ? null : asString(asRecord(emailCreated["draft"])?.["id"]);
185
- const subCreated = asRecord(methodResponseArgs(created, "EmailSubmission/set")?.["created"]);
186
- const submissionId = subCreated === null ? null : asString(asRecord(subCreated["sub"])?.["id"]);
187
- return { emailId, submissionId };
188
- }
189
- }
190
-
191
6
  const server = new McpServer({ name: "nimbus-fastmail", version: "0.1.0" });
192
- registerFastmailTools(server, new FetchJmapClient());
7
+ registerFastmailTools(server, createFetchJmapClient());
193
8
 
194
9
  await server.connect(new StdioServerTransport());
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { type ConsentServer, createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
3
3
 
4
- import { emailToolSchemas } from "../../../shared/imap-tool-kit.ts";
4
+ import { emailToolSchemas, mailSendConsent, outgoingMail } from "../../../shared/imap-tool-kit.ts";
5
5
  import {
6
6
  createRegisterSimpleTool,
7
7
  type McpListResult,
@@ -105,30 +105,11 @@ export function registerFastmailTools(
105
105
 
106
106
  registerWriteTool(
107
107
  "fastmail_mail_send",
108
- {
109
- mutates: "fastmail.mail.send",
110
- // A sent mail cannot be recalled and nothing remains to query, so the recipient and
111
- // subject ARE the pre-state.
112
- recoverable: false,
113
- capturePreState: (p) => Promise.resolve({ to: p.to, subject: p.subject }),
114
- scopeTargetOf: (p) => ({ kind: "recipient", value: p.to }),
115
- },
108
+ mailSendConsent("fastmail.mail.send"),
116
109
  "Send a new email via JMAP EmailSubmission.",
117
110
  sendArgs,
118
- async (parsedData): Promise<McpListResult> => {
119
- const parsed = { success: true as const, data: parsedData };
120
- const input: { to: string; subject: string; body: string; cc?: string; bcc?: string } = {
121
- to: parsed.data.to,
122
- subject: parsed.data.subject,
123
- body: parsed.data.body,
124
- };
125
- if (parsed.data.cc !== undefined && parsed.data.cc !== "") {
126
- input.cc = parsed.data.cc;
127
- }
128
- if (parsed.data.bcc !== undefined && parsed.data.bcc !== "") {
129
- input.bcc = parsed.data.bcc;
130
- }
131
- const res = await client.send(input);
111
+ async (args): Promise<McpListResult> => {
112
+ const res = await client.send(outgoingMail(args));
132
113
  return mcpJsonResult({ emailId: res.emailId, submissionId: res.submissionId });
133
114
  },
134
115
  );
@@ -1,8 +1,7 @@
1
1
  import {
2
- asObjectish,
2
+ fieldsFromKeys,
3
3
  makeQueryFilter,
4
4
  type SearchMatchOptions,
5
- stringField,
6
5
  } from "../../../shared/search-filter.ts";
7
6
 
8
7
  export type FigmaSearchMatchOptions = SearchMatchOptions;
@@ -13,12 +12,4 @@ export type FigmaSearchMatchOptions = SearchMatchOptions;
13
12
  * a `project_name` label. Match against the file name and the project name
14
13
  * (case-insensitive substring).
15
14
  */
16
- function fieldsOf(item: unknown): readonly string[] | null {
17
- const row = asObjectish(item);
18
- if (row === undefined) {
19
- return null;
20
- }
21
- return [stringField(row, "name"), stringField(row, "project_name")];
22
- }
23
-
24
- export const filterFigmaFiles = makeQueryFilter(fieldsOf);
15
+ export const filterFigmaFiles = makeQueryFilter(fieldsFromKeys(["name", "project_name"]));
@@ -1,117 +1,6 @@
1
- import { z } from "zod";
2
- import { searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
3
- import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
4
1
  import { runReadOnlyMcpConnector } from "../../../shared/run-read-only-mcp-connector.ts";
5
- import { filterFigmaFiles } from "./search-filter.ts";
6
-
7
- const BASE = "https://api.figma.com";
8
-
9
- function apiToken(): string {
10
- const t = process.env["FIGMA_TOKEN"]?.trim();
11
- if (t === undefined || t === "") {
12
- throw new Error("FIGMA_TOKEN is not set");
13
- }
14
- return t;
15
- }
16
-
17
- function teamId(): string {
18
- const t = process.env["FIGMA_TEAM_ID"]?.trim();
19
- if (t === undefined || t === "") {
20
- throw new Error("FIGMA_TEAM_ID is not set");
21
- }
22
- return t;
23
- }
24
-
25
- function authHeader(): Record<string, string> {
26
- return { Authorization: `Bearer ${apiToken()}`, Accept: "application/json" };
27
- }
28
-
29
- async function figmaGet(path: string): Promise<unknown> {
30
- const res = await fetch(`${BASE}${path}`, { headers: authHeader() });
31
- const text = await res.text();
32
- if (!res.ok) {
33
- throw new Error(`Figma ${String(res.status)}: ${text.slice(0, 400)}`);
34
- }
35
- return JSON.parse(text) as unknown;
36
- }
37
-
38
- function projectsFrom(root: unknown): Array<{ id: string; name: string }> {
39
- const projects = (root as { projects?: unknown } | null)?.projects;
40
- if (!Array.isArray(projects)) {
41
- return [];
42
- }
43
- const out: Array<{ id: string; name: string }> = [];
44
- for (const p of projects) {
45
- const rec = p as { id?: unknown; name?: unknown } | null;
46
- const id = rec?.id;
47
- if (typeof id === "string" || typeof id === "number") {
48
- out.push({
49
- id: String(id),
50
- name: typeof rec?.name === "string" ? rec.name : "",
51
- });
52
- }
53
- }
54
- return out;
55
- }
56
-
57
- function filesFrom(root: unknown): unknown[] {
58
- const files = (root as { files?: unknown } | null)?.files;
59
- return Array.isArray(files) ? files : [];
60
- }
61
-
62
- /**
63
- * Flatten the configured team's projects into a single list of files, tagging
64
- * each file with its `project_name` for searchability. This is the two-level
65
- * fetch: GET /v1/teams/<team_id>/projects, then GET /v1/projects/<id>/files for
66
- * each project.
67
- */
68
- async function listTeamFiles(): Promise<unknown[]> {
69
- const projectsRoot = await figmaGet(`/v1/teams/${encodeURIComponent(teamId())}/projects`);
70
- const projects = projectsFrom(projectsRoot);
71
- const out: unknown[] = [];
72
- for (const project of projects) {
73
- const filesRoot = await figmaGet(`/v1/projects/${encodeURIComponent(project.id)}/files`);
74
- for (const f of filesFrom(filesRoot)) {
75
- const rec = f as Record<string, unknown> | null;
76
- out.push({
77
- ...rec,
78
- project_id: project.id,
79
- project_name: project.name,
80
- });
81
- }
82
- }
83
- return out;
84
- }
2
+ import { registerFigmaTools } from "./tools.ts";
85
3
 
86
4
  await runReadOnlyMcpConnector("nimbus-figma", (reg) => {
87
- reg(
88
- "figma_list",
89
- "List the configured team's Figma files. Two-level fetch: GET /v1/teams/<team_id>/projects (the team id is injected at spawn from the figma.team_id vault key) returns { name, projects: [{ id, name }] }; then for each project GET /v1/projects/<project_id>/files returns { name, files: [{ key, name, thumbnail_url, last_modified }] }. Returns a flattened { files: [...] } envelope where each file is shaped { key, name, thumbnail_url, last_modified, project_id, project_name }. Neither endpoint paginates — each returns the full list.",
90
- z.object({}),
91
- async () => {
92
- return jsonResult({ files: await listTeamFiles() });
93
- },
94
- );
95
-
96
- reg(
97
- "figma_get",
98
- "List the files in one Figma project by its id (GET /v1/projects/{projectId}/files). Returns the { name, files: [{ key, name, thumbnail_url, last_modified }] } envelope directly. Throws when no project with that id is accessible.",
99
- z.object({
100
- projectId: z.string().min(1),
101
- }),
102
- async (p) => {
103
- return jsonResult(await figmaGet(`/v1/projects/${encodeURIComponent(p.projectId)}/files`));
104
- },
105
- );
106
-
107
- reg(
108
- "figma_search",
109
- "Substring search across the configured team's Figma files. Runs the same two-level fetch as figma_list (team projects, then files per project), flattens the result, and matches the query locally (case-insensitive) against each file name and its project name. Returns a { matches: [...] } envelope.",
110
- searchToolInputSchema(200),
111
- async (p) => {
112
- const files = await listTeamFiles();
113
- const matches = filterFigmaFiles(files, { query: p.query, limit: p.limit });
114
- return jsonResult({ matches });
115
- },
116
- );
5
+ registerFigmaTools(reg);
117
6
  });