@nimbus-dev/connectors 0.1.0

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 (301) hide show
  1. package/LICENSE +662 -0
  2. package/NOTICE +40 -0
  3. package/README.md +111 -0
  4. package/connectors/airflow/nimbus.extension.json +16 -0
  5. package/connectors/airflow/src/search-filter.ts +38 -0
  6. package/connectors/airflow/src/server.ts +91 -0
  7. package/connectors/apple/nimbus.extension.json +40 -0
  8. package/connectors/apple/src/apple-mail-core.ts +43 -0
  9. package/connectors/apple/src/caldav-core.ts +116 -0
  10. package/connectors/apple/src/calendar-tools.ts +278 -0
  11. package/connectors/apple/src/server.ts +541 -0
  12. package/connectors/apple/src/tools.ts +138 -0
  13. package/connectors/argocd/nimbus.extension.json +16 -0
  14. package/connectors/argocd/src/search-filter.ts +24 -0
  15. package/connectors/argocd/src/server.ts +158 -0
  16. package/connectors/athena/nimbus.extension.json +15 -0
  17. package/connectors/athena/src/server.ts +6 -0
  18. package/connectors/athena/src/tools.ts +181 -0
  19. package/connectors/aws/nimbus.extension.json +21 -0
  20. package/connectors/aws/src/server.ts +191 -0
  21. package/connectors/azure/nimbus.extension.json +15 -0
  22. package/connectors/azure/src/server.ts +139 -0
  23. package/connectors/bigeye/nimbus.extension.json +16 -0
  24. package/connectors/bigeye/src/search-filter.ts +22 -0
  25. package/connectors/bigeye/src/server.ts +161 -0
  26. package/connectors/bigquery/nimbus.extension.json +15 -0
  27. package/connectors/bigquery/src/server.ts +6 -0
  28. package/connectors/bigquery/src/tools.ts +175 -0
  29. package/connectors/bitbucket/nimbus.extension.json +15 -0
  30. package/connectors/bitbucket/src/server.ts +252 -0
  31. package/connectors/bitrise/nimbus.extension.json +15 -0
  32. package/connectors/bitrise/src/search-filter.ts +11 -0
  33. package/connectors/bitrise/src/server.ts +83 -0
  34. package/connectors/canva/nimbus.extension.json +15 -0
  35. package/connectors/canva/src/search-filter.ts +23 -0
  36. package/connectors/canva/src/server.ts +77 -0
  37. package/connectors/circleci/nimbus.extension.json +15 -0
  38. package/connectors/circleci/src/server.ts +184 -0
  39. package/connectors/cloud-logging/nimbus.extension.json +15 -0
  40. package/connectors/cloud-logging/src/server.ts +6 -0
  41. package/connectors/cloud-logging/src/tools.ts +115 -0
  42. package/connectors/cloudwatch/nimbus.extension.json +15 -0
  43. package/connectors/cloudwatch/src/server.ts +6 -0
  44. package/connectors/cloudwatch/src/tools.ts +144 -0
  45. package/connectors/codemagic/nimbus.extension.json +15 -0
  46. package/connectors/codemagic/src/search-filter.ts +11 -0
  47. package/connectors/codemagic/src/server.ts +74 -0
  48. package/connectors/confluence/nimbus.extension.json +15 -0
  49. package/connectors/confluence/src/kb-append.ts +25 -0
  50. package/connectors/confluence/src/server.ts +309 -0
  51. package/connectors/dagster/nimbus.extension.json +16 -0
  52. package/connectors/dagster/src/search-filter.ts +53 -0
  53. package/connectors/dagster/src/server.ts +218 -0
  54. package/connectors/databricks/nimbus.extension.json +16 -0
  55. package/connectors/databricks/src/search-filter.ts +28 -0
  56. package/connectors/databricks/src/server.ts +81 -0
  57. package/connectors/datadog/nimbus.extension.json +15 -0
  58. package/connectors/datadog/src/server.ts +56 -0
  59. package/connectors/dataprofile/nimbus.extension.json +16 -0
  60. package/connectors/dataprofile/src/profile.ts +298 -0
  61. package/connectors/dataprofile/src/server.ts +6 -0
  62. package/connectors/dataprofile/src/tools.ts +69 -0
  63. package/connectors/dbt/nimbus.extension.json +16 -0
  64. package/connectors/dbt/src/search-filter.ts +28 -0
  65. package/connectors/dbt/src/server.ts +96 -0
  66. package/connectors/dependencytrack/nimbus.extension.json +16 -0
  67. package/connectors/dependencytrack/src/search-filter.ts +24 -0
  68. package/connectors/dependencytrack/src/server.ts +80 -0
  69. package/connectors/discord/nimbus.extension.json +15 -0
  70. package/connectors/discord/src/server.ts +93 -0
  71. package/connectors/elasticsearch/nimbus.extension.json +16 -0
  72. package/connectors/elasticsearch/src/server.ts +6 -0
  73. package/connectors/elasticsearch/src/tools.ts +110 -0
  74. package/connectors/fastmail/nimbus.extension.json +16 -0
  75. package/connectors/fastmail/src/jmap-core.ts +86 -0
  76. package/connectors/fastmail/src/server.ts +194 -0
  77. package/connectors/fastmail/src/tools.ts +143 -0
  78. package/connectors/figma/nimbus.extension.json +15 -0
  79. package/connectors/figma/src/search-filter.ts +24 -0
  80. package/connectors/figma/src/server.ts +117 -0
  81. package/connectors/firebase/nimbus.extension.json +15 -0
  82. package/connectors/firebase/src/search-filter.ts +30 -0
  83. package/connectors/firebase/src/server.ts +94 -0
  84. package/connectors/firebase/src/token.ts +41 -0
  85. package/connectors/flagsmith/nimbus.extension.json +15 -0
  86. package/connectors/flagsmith/src/search-filter.ts +29 -0
  87. package/connectors/flagsmith/src/server.ts +102 -0
  88. package/connectors/flux/nimbus.extension.json +16 -0
  89. package/connectors/flux/src/search-filter.ts +42 -0
  90. package/connectors/flux/src/server.ts +182 -0
  91. package/connectors/gcp/nimbus.extension.json +15 -0
  92. package/connectors/gcp/src/server.ts +140 -0
  93. package/connectors/github/nimbus.extension.json +15 -0
  94. package/connectors/github/src/server.ts +347 -0
  95. package/connectors/github-actions/nimbus.extension.json +15 -0
  96. package/connectors/github-actions/src/server.ts +213 -0
  97. package/connectors/gitlab/nimbus.extension.json +15 -0
  98. package/connectors/gitlab/src/server.ts +392 -0
  99. package/connectors/gmail/nimbus.extension.json +15 -0
  100. package/connectors/gmail/src/server.ts +277 -0
  101. package/connectors/google-drive/nimbus.extension.json +15 -0
  102. package/connectors/google-drive/src/drive-query.ts +3 -0
  103. package/connectors/google-drive/src/server.ts +531 -0
  104. package/connectors/google-meet/nimbus.extension.json +15 -0
  105. package/connectors/google-meet/src/server.ts +89 -0
  106. package/connectors/google-photos/nimbus.extension.json +15 -0
  107. package/connectors/google-photos/src/server.ts +139 -0
  108. package/connectors/grafana/nimbus.extension.json +15 -0
  109. package/connectors/grafana/src/server.ts +61 -0
  110. package/connectors/great-expectations/nimbus.extension.json +16 -0
  111. package/connectors/great-expectations/src/gx-parse.ts +292 -0
  112. package/connectors/great-expectations/src/server.ts +6 -0
  113. package/connectors/great-expectations/src/tools.ts +91 -0
  114. package/connectors/greenhouse/nimbus.extension.json +15 -0
  115. package/connectors/greenhouse/src/search-filter.ts +65 -0
  116. package/connectors/greenhouse/src/server.ts +67 -0
  117. package/connectors/hubspot/nimbus.extension.json +15 -0
  118. package/connectors/hubspot/src/search-filter.ts +28 -0
  119. package/connectors/hubspot/src/server.ts +81 -0
  120. package/connectors/iac/nimbus.extension.json +14 -0
  121. package/connectors/iac/src/server.ts +145 -0
  122. package/connectors/imap/nimbus.extension.json +16 -0
  123. package/connectors/imap/src/imap-core.ts +110 -0
  124. package/connectors/imap/src/server.ts +232 -0
  125. package/connectors/imap/src/tools.ts +40 -0
  126. package/connectors/intercom/nimbus.extension.json +15 -0
  127. package/connectors/intercom/src/search-filter.ts +46 -0
  128. package/connectors/intercom/src/server.ts +65 -0
  129. package/connectors/jenkins/nimbus.extension.json +15 -0
  130. package/connectors/jenkins/src/jenkins-api.ts +156 -0
  131. package/connectors/jenkins/src/server.ts +223 -0
  132. package/connectors/jira/nimbus.extension.json +15 -0
  133. package/connectors/jira/src/server.ts +309 -0
  134. package/connectors/kubernetes/nimbus.extension.json +15 -0
  135. package/connectors/kubernetes/src/server.ts +163 -0
  136. package/connectors/launchdarkly/nimbus.extension.json +15 -0
  137. package/connectors/launchdarkly/src/search-filter.ts +31 -0
  138. package/connectors/launchdarkly/src/server.ts +79 -0
  139. package/connectors/lever/nimbus.extension.json +15 -0
  140. package/connectors/lever/src/search-filter.ts +35 -0
  141. package/connectors/lever/src/server.ts +64 -0
  142. package/connectors/linear/nimbus.extension.json +15 -0
  143. package/connectors/linear/src/server.ts +467 -0
  144. package/connectors/localdb/nimbus.extension.json +16 -0
  145. package/connectors/localdb/src/server.ts +6 -0
  146. package/connectors/localdb/src/sql-scan.ts +158 -0
  147. package/connectors/localdb/src/tools.ts +67 -0
  148. package/connectors/looker/nimbus.extension.json +16 -0
  149. package/connectors/looker/src/search-filter.ts +22 -0
  150. package/connectors/looker/src/server.ts +254 -0
  151. package/connectors/mendeley/nimbus.extension.json +15 -0
  152. package/connectors/mendeley/src/search-filter.ts +68 -0
  153. package/connectors/mendeley/src/server.ts +59 -0
  154. package/connectors/mercury/nimbus.extension.json +15 -0
  155. package/connectors/mercury/src/search-filter.ts +11 -0
  156. package/connectors/mercury/src/server.ts +61 -0
  157. package/connectors/metabase/nimbus.extension.json +16 -0
  158. package/connectors/metabase/src/search-filter.ts +22 -0
  159. package/connectors/metabase/src/server.ts +83 -0
  160. package/connectors/miro/nimbus.extension.json +15 -0
  161. package/connectors/miro/src/search-filter.ts +24 -0
  162. package/connectors/miro/src/server.ts +76 -0
  163. package/connectors/mlflow/nimbus.extension.json +16 -0
  164. package/connectors/mlflow/src/search-filter.ts +38 -0
  165. package/connectors/mlflow/src/server.ts +167 -0
  166. package/connectors/monte-carlo/nimbus.extension.json +16 -0
  167. package/connectors/monte-carlo/src/search-filter.ts +11 -0
  168. package/connectors/monte-carlo/src/server.ts +241 -0
  169. package/connectors/netlify/nimbus.extension.json +15 -0
  170. package/connectors/netlify/src/search-filter.ts +37 -0
  171. package/connectors/netlify/src/server.ts +64 -0
  172. package/connectors/newrelic/nimbus.extension.json +15 -0
  173. package/connectors/newrelic/src/server.ts +48 -0
  174. package/connectors/notion/nimbus.extension.json +15 -0
  175. package/connectors/notion/src/kb-append.ts +23 -0
  176. package/connectors/notion/src/server.ts +343 -0
  177. package/connectors/obsidian/nimbus.extension.json +19 -0
  178. package/connectors/obsidian/src/server.ts +433 -0
  179. package/connectors/onedrive/nimbus.extension.json +15 -0
  180. package/connectors/onedrive/src/server.ts +256 -0
  181. package/connectors/outlook/nimbus.extension.json +15 -0
  182. package/connectors/outlook/src/server.ts +358 -0
  183. package/connectors/outlook/src/tool-scope-policy.ts +53 -0
  184. package/connectors/pagerduty/nimbus.extension.json +15 -0
  185. package/connectors/pagerduty/src/server.ts +140 -0
  186. package/connectors/pipedrive/nimbus.extension.json +15 -0
  187. package/connectors/pipedrive/src/search-filter.ts +11 -0
  188. package/connectors/pipedrive/src/server.ts +62 -0
  189. package/connectors/powerbi/nimbus.extension.json +16 -0
  190. package/connectors/powerbi/src/search-filter.ts +22 -0
  191. package/connectors/powerbi/src/server.ts +249 -0
  192. package/connectors/prefect/nimbus.extension.json +16 -0
  193. package/connectors/prefect/src/search-filter.ts +40 -0
  194. package/connectors/prefect/src/server.ts +94 -0
  195. package/connectors/protonmail/nimbus.extension.json +16 -0
  196. package/connectors/protonmail/src/mail-core.ts +96 -0
  197. package/connectors/protonmail/src/server.ts +231 -0
  198. package/connectors/protonmail/src/tools.ts +45 -0
  199. package/connectors/raindrop/nimbus.extension.json +15 -0
  200. package/connectors/raindrop/src/search-filter.ts +20 -0
  201. package/connectors/raindrop/src/server.ts +107 -0
  202. package/connectors/ramp/nimbus.extension.json +15 -0
  203. package/connectors/ramp/src/search-filter.ts +37 -0
  204. package/connectors/ramp/src/server.ts +109 -0
  205. package/connectors/readwise/nimbus.extension.json +15 -0
  206. package/connectors/readwise/src/search-filter.ts +64 -0
  207. package/connectors/readwise/src/server.ts +92 -0
  208. package/connectors/sagemaker/nimbus.extension.json +15 -0
  209. package/connectors/sagemaker/src/server.ts +6 -0
  210. package/connectors/sagemaker/src/tools.ts +121 -0
  211. package/connectors/salesforce/nimbus.extension.json +15 -0
  212. package/connectors/salesforce/src/search-filter.ts +23 -0
  213. package/connectors/salesforce/src/server.ts +94 -0
  214. package/connectors/semgrep/nimbus.extension.json +15 -0
  215. package/connectors/semgrep/src/search-filter.ts +31 -0
  216. package/connectors/semgrep/src/server.ts +108 -0
  217. package/connectors/sentry/nimbus.extension.json +15 -0
  218. package/connectors/sentry/src/server.ts +66 -0
  219. package/connectors/slack/nimbus.extension.json +15 -0
  220. package/connectors/slack/src/server.ts +360 -0
  221. package/connectors/snowflake/nimbus.extension.json +16 -0
  222. package/connectors/snowflake/src/search-filter.ts +26 -0
  223. package/connectors/snowflake/src/server.ts +236 -0
  224. package/connectors/snyk/nimbus.extension.json +15 -0
  225. package/connectors/snyk/src/search-filter.ts +28 -0
  226. package/connectors/snyk/src/server.ts +144 -0
  227. package/connectors/sonarqube/nimbus.extension.json +15 -0
  228. package/connectors/sonarqube/src/search-filter.ts +28 -0
  229. package/connectors/sonarqube/src/server.ts +115 -0
  230. package/connectors/stackoverflow/nimbus.extension.json +15 -0
  231. package/connectors/stackoverflow/src/search-filter.ts +52 -0
  232. package/connectors/stackoverflow/src/server.ts +81 -0
  233. package/connectors/storybook/nimbus.extension.json +16 -0
  234. package/connectors/storybook/src/server.ts +6 -0
  235. package/connectors/storybook/src/storybook-parse.ts +67 -0
  236. package/connectors/storybook/src/tools.ts +68 -0
  237. package/connectors/stripe/nimbus.extension.json +15 -0
  238. package/connectors/stripe/src/search-filter.ts +19 -0
  239. package/connectors/stripe/src/server.ts +65 -0
  240. package/connectors/superset/nimbus.extension.json +16 -0
  241. package/connectors/superset/src/search-filter.ts +22 -0
  242. package/connectors/superset/src/server.ts +119 -0
  243. package/connectors/tableau/nimbus.extension.json +16 -0
  244. package/connectors/tableau/src/search-filter.ts +22 -0
  245. package/connectors/tableau/src/server.ts +248 -0
  246. package/connectors/teams/nimbus.extension.json +20 -0
  247. package/connectors/teams/src/bot-send.ts +72 -0
  248. package/connectors/teams/src/server.ts +257 -0
  249. package/connectors/testflight/nimbus.extension.json +15 -0
  250. package/connectors/testflight/src/jwt.ts +27 -0
  251. package/connectors/testflight/src/search-filter.ts +30 -0
  252. package/connectors/testflight/src/server.ts +74 -0
  253. package/connectors/vercel/nimbus.extension.json +15 -0
  254. package/connectors/vercel/src/search-filter.ts +34 -0
  255. package/connectors/vercel/src/server.ts +79 -0
  256. package/connectors/vertex-ai/nimbus.extension.json +15 -0
  257. package/connectors/vertex-ai/src/server.ts +6 -0
  258. package/connectors/vertex-ai/src/tools.ts +145 -0
  259. package/connectors/wiz/nimbus.extension.json +15 -0
  260. package/connectors/wiz/src/search-filter.ts +50 -0
  261. package/connectors/wiz/src/server.ts +192 -0
  262. package/connectors/workday/nimbus.extension.json +19 -0
  263. package/connectors/workday/src/search-filter.ts +5 -0
  264. package/connectors/workday/src/server.ts +78 -0
  265. package/connectors/workday/src/tools.ts +1 -0
  266. package/connectors/zendesk/nimbus.extension.json +16 -0
  267. package/connectors/zendesk/src/search-filter.ts +11 -0
  268. package/connectors/zendesk/src/server.ts +77 -0
  269. package/connectors/zoom/nimbus.extension.json +15 -0
  270. package/connectors/zoom/src/encode-meeting-path.ts +5 -0
  271. package/connectors/zoom/src/recordings-window.ts +34 -0
  272. package/connectors/zoom/src/search-filter.ts +42 -0
  273. package/connectors/zoom/src/server.ts +98 -0
  274. package/connectors/zotero/nimbus.extension.json +15 -0
  275. package/connectors/zotero/src/search-filter.ts +81 -0
  276. package/connectors/zotero/src/server.ts +82 -0
  277. package/package.json +152 -0
  278. package/shared/atlassian-json-fetch.ts +42 -0
  279. package/shared/audit-chain.ts +133 -0
  280. package/shared/connector-mode.ts +60 -0
  281. package/shared/consent-kit.ts +323 -0
  282. package/shared/fetch-bearer-json.ts +18 -0
  283. package/shared/header-safe.ts +28 -0
  284. package/shared/imap-bodystructure.ts +113 -0
  285. package/shared/imap-mail-core.ts +57 -0
  286. package/shared/imap-tool-kit.ts +293 -0
  287. package/shared/join-api-path.ts +18 -0
  288. package/shared/kb-markdown.ts +142 -0
  289. package/shared/mcp-search-tool.ts +37 -0
  290. package/shared/mcp-tool-kit.ts +35 -0
  291. package/shared/nimbus-spawn.ts +128 -0
  292. package/shared/rest-tool-kit.ts +18 -0
  293. package/shared/run-cli-json.ts +55 -0
  294. package/shared/run-read-only-mcp-connector.ts +59 -0
  295. package/shared/safe-cli-arg.ts +55 -0
  296. package/shared/search-filter.ts +140 -0
  297. package/shared/strip-trailing-slashes.ts +7 -0
  298. package/shared/write-scope.ts +58 -0
  299. package/standalone/src/bin.ts +19 -0
  300. package/standalone/src/launcher.ts +186 -0
  301. package/standalone/tsconfig.json +8 -0
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "com.nimbus.dependencytrack",
3
+ "displayName": "Dependency-Track",
4
+ "version": "0.1.0",
5
+ "description": "OWASP Dependency-Track connector (read-only). Surfaces the user's software-supply-chain projects from a self-hosted Dependency-Track instance as `dependencytrack:project` items in the local index — the project name, version, classifier (APPLICATION / LIBRARY / CONTAINER / …), active flag, last BOM-import timestamp, tags, and the embedded vulnerability metrics (critical / high / medium / low counts, total vulnerabilities, and component count). Useful for answering supply-chain questions (\"which projects have critical vulnerabilities?\", \"what's the SBOM coverage for the payment service?\") without leaving Nimbus. Dependency-Track API-key auth (sent as the X-Api-Key request header). Dependency-Track has no universal SaaS host: the server host is supplied via the dependencytrack base-url vault key and added to the sandbox network list at spawn time by phase3AddDependencytrackMcp, so the static manifest network list is intentionally empty. The /api/v1/project endpoint paginates by pageNumber / pageSize (pageSize max 100); the connector walks a single forward page-number pass per cycle, capped. v1 indexes projects only — individual findings and components are a deferred follow-up. No write tools.",
6
+ "author": "Nimbus",
7
+ "entrypoint": "dist/server.js",
8
+ "runtime": "bun",
9
+ "permissions": {
10
+ "network": [],
11
+ "filesystem": { "read": [], "write": [] }
12
+ },
13
+ "hitlRequired": [],
14
+ "syncInterval": 600,
15
+ "minNimbusVersion": "0.2.0"
16
+ }
@@ -0,0 +1,24 @@
1
+ import {
2
+ asObjectish,
3
+ makeQueryFilter,
4
+ type SearchMatchOptions,
5
+ stringField,
6
+ tagNamesFromObjects,
7
+ } from "../../../shared/search-filter.ts";
8
+
9
+ export type DependencyTrackSearchMatchOptions = SearchMatchOptions;
10
+
11
+ function fieldsOf(item: unknown): readonly string[] | null {
12
+ const row = asObjectish(item);
13
+ if (row === undefined) {
14
+ return null;
15
+ }
16
+ return [
17
+ stringField(row, "name"),
18
+ stringField(row, "version"),
19
+ stringField(row, "classifier"),
20
+ tagNamesFromObjects(row),
21
+ ];
22
+ }
23
+
24
+ export const filterDependencyTrackProjects = makeQueryFilter(fieldsOf);
@@ -0,0 +1,80 @@
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
+ import { runReadOnlyMcpConnector } from "../../../shared/run-read-only-mcp-connector.ts";
5
+ import { filterDependencyTrackProjects } from "./search-filter.ts";
6
+
7
+ function trimTrailingSlash(s: string): string {
8
+ return s.endsWith("/") ? s.slice(0, -1) : s;
9
+ }
10
+
11
+ function apiBase(): string {
12
+ const v = process.env["DEPENDENCYTRACK_URL"]?.trim();
13
+ if (v === undefined || v === "") {
14
+ throw new Error("DEPENDENCYTRACK_URL is not set");
15
+ }
16
+ return trimTrailingSlash(v);
17
+ }
18
+
19
+ function authHeader(): Record<string, string> {
20
+ const k = process.env["DEPENDENCYTRACK_API_KEY"]?.trim();
21
+ if (k === undefined || k === "") {
22
+ throw new Error("DEPENDENCYTRACK_API_KEY is not set");
23
+ }
24
+ return { "X-Api-Key": k, Accept: "application/json" };
25
+ }
26
+
27
+ async function dtGet(path: string): Promise<unknown> {
28
+ const res = await fetch(`${apiBase()}${path}`, { headers: authHeader() });
29
+ const text = await res.text();
30
+ if (!res.ok) {
31
+ throw new Error(`Dependency-Track ${String(res.status)}: ${text.slice(0, 400)}`);
32
+ }
33
+ return JSON.parse(text) as unknown;
34
+ }
35
+
36
+ function projectsFrom(root: unknown): unknown[] {
37
+ return Array.isArray(root) ? root : [];
38
+ }
39
+
40
+ await runReadOnlyMcpConnector("nimbus-dependencytrack", (reg) => {
41
+ reg(
42
+ "dependencytrack_list",
43
+ "List Dependency-Track projects (GET /api/v1/project?pageSize=100&pageNumber=1&excludeInactive=false). Dependency-Track returns a JSON array of project objects, each embedding its current vulnerability metrics. The optional pageNumber arg (default 1) selects the page; pageSize is fixed at 100.",
44
+ z.object({
45
+ pageNumber: z.number().int().min(1).optional(),
46
+ }),
47
+ async (p) => {
48
+ const page = p.pageNumber ?? 1;
49
+ const root = await dtGet(
50
+ `/api/v1/project?pageSize=100&pageNumber=${encodeURIComponent(String(page))}&excludeInactive=false`,
51
+ );
52
+ return jsonResult({ items: projectsFrom(root) });
53
+ },
54
+ );
55
+
56
+ reg(
57
+ "dependencytrack_get",
58
+ "Fetch one Dependency-Track project by its UUID (GET /api/v1/project/{uuid}). Returns the project object directly (NOT wrapped in an array). Throws when no project with that UUID exists.",
59
+ z.object({
60
+ uuid: z.string().min(1),
61
+ }),
62
+ async (p) => {
63
+ return jsonResult(await dtGet(`/api/v1/project/${encodeURIComponent(p.uuid)}`));
64
+ },
65
+ );
66
+
67
+ reg(
68
+ "dependencytrack_search",
69
+ "Substring search across the first page of Dependency-Track projects. Matches the query (case-insensitive) against the project name, version, classifier, and tag names. Returns a matches envelope.",
70
+ searchToolInputSchema(100),
71
+ async (p) => {
72
+ const root = await dtGet("/api/v1/project?pageSize=100&pageNumber=1&excludeInactive=false");
73
+ const matches = filterDependencyTrackProjects(projectsFrom(root), {
74
+ query: p.query,
75
+ limit: p.limit,
76
+ });
77
+ return jsonResult({ matches });
78
+ },
79
+ );
80
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ "id": "com.nimbus.discord",
3
+ "displayName": "Discord",
4
+ "version": "0.1.0",
5
+ "description": "Discord opt-in connector (guilds, channels, messages). Disabled by default. Mutations require Gateway HITL.",
6
+ "author": "Nimbus",
7
+ "entrypoint": "dist/server.js",
8
+ "runtime": "bun",
9
+ "permissions": {
10
+ "network": ["discord.com", "gateway.discord.gg", "cdn.discordapp.com"]
11
+ },
12
+ "hitlRequired": [],
13
+ "syncInterval": 120,
14
+ "minNimbusVersion": "0.2.0"
15
+ }
@@ -0,0 +1,93 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+
5
+ import { createRegisterSimpleTool, createZodToolRegistrar } from "../../../shared/mcp-tool-kit.ts";
6
+ import { makeRestToolRegistrar } from "../../../shared/rest-tool-kit.ts";
7
+
8
+ const DISCORD_API = "https://discord.com/api/v10";
9
+
10
+ async function discordFetch(
11
+ token: string,
12
+ path: string,
13
+ init?: RequestInit,
14
+ ): Promise<{
15
+ ok: boolean;
16
+ status: number;
17
+ json: unknown;
18
+ text: string;
19
+ }> {
20
+ const url = path.startsWith("http") ? path : `${DISCORD_API}${path}`;
21
+ const res = await fetch(url, {
22
+ ...init,
23
+ headers: {
24
+ Authorization: `Bot ${token}`,
25
+ "User-Agent": "NimbusMCP (https://github.com/nimbus-dev/nimbus)",
26
+ ...(init?.headers as Record<string, string> | undefined),
27
+ },
28
+ });
29
+ const text = await res.text();
30
+ let json: unknown;
31
+ try {
32
+ json = JSON.parse(text) as unknown;
33
+ } catch {
34
+ json = null;
35
+ }
36
+ return { ok: res.ok, status: res.status, json, text };
37
+ }
38
+
39
+ const server = new McpServer({ name: "nimbus-discord", version: "0.1.0" });
40
+
41
+ const registerSimpleTool = createRegisterSimpleTool(server);
42
+ const reg = createZodToolRegistrar(registerSimpleTool);
43
+
44
+ /** Standard Discord read tool: token → discordFetch(buildPath) → mcpJsonResultIfOk("Discord"). */
45
+ const registerDiscordTool = makeRestToolRegistrar({
46
+ registrar: reg,
47
+ tokenEnv: "DISCORD_BOT_TOKEN",
48
+ serviceLabel: "Discord",
49
+ fetch: discordFetch,
50
+ });
51
+
52
+ registerDiscordTool(
53
+ "discord_guild_list",
54
+ "List guilds the bot is a member of.",
55
+ z.object({}),
56
+ () => "/users/@me/guilds",
57
+ );
58
+
59
+ registerDiscordTool(
60
+ "discord_channel_list",
61
+ "List channels in a guild (id, type, name).",
62
+ z.object({ guildId: z.string().min(1) }),
63
+ (parsed) => `/guilds/${encodeURIComponent(parsed.guildId)}/channels`,
64
+ );
65
+
66
+ registerDiscordTool(
67
+ "discord_channel_messages",
68
+ "List recent messages in a channel (newest first). Optional `after` snowflake for incremental fetch.",
69
+ z.object({
70
+ channelId: z.string().min(1),
71
+ limit: z.number().int().min(1).max(100).optional(),
72
+ after: z.string().optional(),
73
+ }),
74
+ (parsed) => {
75
+ const lim = parsed.limit ?? 50;
76
+ const u = new URL(`${DISCORD_API}/channels/${encodeURIComponent(parsed.channelId)}/messages`);
77
+ u.searchParams.set("limit", String(lim));
78
+ if (parsed.after !== undefined && parsed.after !== "") {
79
+ u.searchParams.set("after", parsed.after);
80
+ }
81
+ return `${u.pathname}${u.search}`;
82
+ },
83
+ );
84
+
85
+ registerDiscordTool(
86
+ "discord_thread_list",
87
+ "List active threads in a guild (includes public threads the bot can see).",
88
+ z.object({ guildId: z.string().min(1) }),
89
+ (parsed) => `/guilds/${encodeURIComponent(parsed.guildId)}/threads/active`,
90
+ );
91
+
92
+ const transport = new StdioServerTransport();
93
+ await server.connect(transport);
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "com.nimbus.elasticsearch",
3
+ "displayName": "Elasticsearch / Kibana",
4
+ "version": "0.1.0",
5
+ "description": "Elasticsearch / Kibana connector (read-only, Tier-3 metadata-only). Surfaces the user's Elasticsearch indices as `elasticsearch:index` items in the local index — the index name, health (green/yellow/red), status (open/close), document COUNT, store size in bytes, primary-shard and replica counts, UUID, and the index's field names + types from its mapping (SCHEMA metadata). Useful for answering operational/schema questions (\"which indices are red?\", \"what fields does the orders index have?\", \"how big is the logs-2026.05 index?\") without leaving Nimbus. This is a NO-ROW-DATA (Tier-3) connector: it indexes index METADATA only and NEVER document contents — it does not call `_search`, `_doc`, `_mget`, `_sql`, `_scroll`, or any endpoint that returns document SOURCE. Elasticsearch API-key auth (sent as the `Authorization: ApiKey <key>` request header). Elasticsearch has no universal SaaS host (self-hosted or Elastic Cloud): the cluster host is supplied via the elasticsearch url vault key and added to the sandbox network list at spawn time by phase3AddElasticsearchMcp, so the static manifest network list is intentionally empty. The connector walks `_cat/indices` for the metadata listing and per-index `_mapping` for field names + types, both capped. ES system indices (names starting with `.`) are skipped by default. No write tools.",
6
+ "author": "Nimbus",
7
+ "entrypoint": "dist/server.js",
8
+ "runtime": "bun",
9
+ "permissions": {
10
+ "network": [],
11
+ "filesystem": { "read": [], "write": [] }
12
+ },
13
+ "hitlRequired": [],
14
+ "syncInterval": 600,
15
+ "minNimbusVersion": "0.2.0"
16
+ }
@@ -0,0 +1,6 @@
1
+ import { runReadOnlyMcpConnector } from "../../../shared/run-read-only-mcp-connector.ts";
2
+ import { registerElasticsearchTools } from "./tools.ts";
3
+
4
+ await runReadOnlyMcpConnector("nimbus-elasticsearch", (reg) => {
5
+ registerElasticsearchTools(reg);
6
+ });
@@ -0,0 +1,110 @@
1
+ import { z } from "zod";
2
+ import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
3
+ import type { ZodToolRegistrar } from "../../../shared/run-read-only-mcp-connector.ts";
4
+
5
+ /**
6
+ * Elasticsearch / Kibana (Tier-3, metadata-only) MCP tool surface. ALL tools
7
+ * index schema / METADATA only — index names, health, status, document COUNTS,
8
+ * store sizes, shard/replica counts, and field names + types from the index
9
+ * mapping. NONE fetch document / row / hit data: no `_search`, no `_doc`, no
10
+ * `_mget`, no `_sql`, no `_scroll`. `elasticsearch_search` searches index NAMES,
11
+ * not document contents. The names are introspected by the `assertNoRowDataTools`
12
+ * contract test, so they must never contain a row-data segment
13
+ * (query/rows/sample/scan/preview/head/export/records/event/…).
14
+ */
15
+ export const ELASTICSEARCH_TOOL_NAMES = [
16
+ "elasticsearch_list",
17
+ "elasticsearch_get",
18
+ "elasticsearch_search",
19
+ ] as const;
20
+
21
+ function trimTrailingSlash(s: string): string {
22
+ return s.endsWith("/") ? s.slice(0, -1) : s;
23
+ }
24
+
25
+ function baseUrl(): string {
26
+ const v = process.env["ELASTICSEARCH_URL"]?.trim();
27
+ if (v === undefined || v === "") {
28
+ throw new Error("ELASTICSEARCH_URL is not set");
29
+ }
30
+ return trimTrailingSlash(v);
31
+ }
32
+
33
+ function authHeaders(): Record<string, string> {
34
+ const k = process.env["ELASTICSEARCH_API_KEY"]?.trim();
35
+ if (k === undefined || k === "") {
36
+ throw new Error("ELASTICSEARCH_API_KEY is not set");
37
+ }
38
+ return { Authorization: `ApiKey ${k}`, Accept: "application/json" };
39
+ }
40
+
41
+ async function esGet(path: string): Promise<unknown> {
42
+ const res = await fetch(`${baseUrl()}${path}`, { headers: authHeaders() });
43
+ const text = await res.text();
44
+ if (!res.ok) {
45
+ throw new Error(`Elasticsearch ${String(res.status)}: ${text.slice(0, 400)}`);
46
+ }
47
+ return JSON.parse(text) as unknown;
48
+ }
49
+
50
+ function isRecord(v: unknown): v is Record<string, unknown> {
51
+ return v !== null && typeof v === "object" && !Array.isArray(v);
52
+ }
53
+
54
+ function strField(r: Record<string, unknown>, key: string): string {
55
+ const v = r[key];
56
+ return typeof v === "string" ? v : "";
57
+ }
58
+
59
+ function indexEntries(parsed: unknown): unknown[] {
60
+ return Array.isArray(parsed) ? parsed : [];
61
+ }
62
+
63
+ function indexMatches(entry: unknown, q: string): boolean {
64
+ if (!isRecord(entry)) {
65
+ return false;
66
+ }
67
+ return strField(entry, "index").toLowerCase().includes(q.toLowerCase());
68
+ }
69
+
70
+ /**
71
+ * Register the read-only, metadata-only Elasticsearch tools onto the given
72
+ * registrar. Shared between `server.ts` (live) and the contract test
73
+ * (introspection).
74
+ */
75
+ export function registerElasticsearchTools(reg: ZodToolRegistrar): void {
76
+ reg(
77
+ "elasticsearch_list",
78
+ "List Elasticsearch indices — METADATA ONLY (`GET /_cat/indices?format=json&bytes=b`). Each entry carries the index name, `health` (green/yellow/red), `status` (open/close), `docs.count` (a single document COUNT integer — metadata, NOT documents), `store.size` (bytes), primary-shard count `pri`, replica count `rep`, and `uuid`. Never returns document/hit source.",
79
+ z.object({}),
80
+ async () => {
81
+ return jsonResult({ items: indexEntries(await esGet("/_cat/indices?format=json&bytes=b")) });
82
+ },
83
+ );
84
+
85
+ reg(
86
+ "elasticsearch_get",
87
+ "Fetch one index's field MAPPING (schema) — METADATA ONLY (`GET /<index>/_mapping`). Returns the mapping object `{ <index>: { mappings: { properties: { <field>: { type } } } } }` — field names + types only, NO document values. The index name is URL-path-encoded.",
88
+ z.object({
89
+ index: z.string().min(1),
90
+ }),
91
+ async (p) => {
92
+ return jsonResult(await esGet(`/${encodeURIComponent(p.index)}/_mapping`));
93
+ },
94
+ );
95
+
96
+ reg(
97
+ "elasticsearch_search",
98
+ "Substring search over index NAMES (case-insensitive) — METADATA ONLY. Lists indices via `GET /_cat/indices?format=json&bytes=b` and filters by name. Returns a `{ matches: [...] }` envelope of index metadata entries. This searches index names/metadata only and NEVER document contents — it does not run a query against documents.",
99
+ z.object({
100
+ query: z.string().min(1),
101
+ limit: z.number().int().min(1).max(500).optional(),
102
+ }),
103
+ async (p) => {
104
+ const all = indexEntries(await esGet("/_cat/indices?format=json&bytes=b"));
105
+ const filtered = all.filter((e) => indexMatches(e, p.query));
106
+ const matches = p.limit === undefined ? filtered : filtered.slice(0, p.limit);
107
+ return jsonResult({ matches });
108
+ },
109
+ );
110
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "com.nimbus.fastmail",
3
+ "displayName": "Fastmail (JMAP)",
4
+ "version": "0.1.0",
5
+ "description": "Fastmail email connector over native JMAP (Tier-4 EMAIL class). Indexes the user's Fastmail mailbox as `fastmail:email` items — message HEADERS (subject, from, to/cc, received date, message-id), a short capped plain-text body PREVIEW (~2 KB / 2000 chars), and attachment METADATA only (filename, size, mimetype). It NEVER downloads or parses attachment bytes and never fetches the full message body: the JMAP `Email/get` calls request `maxBodyValueBytes` so the server truncates the body value before it crosses the wire, and only the `attachments` body-part metadata is read — the `blobId` download URL is never dereferenced. Read tools (`fastmail_list`, `fastmail_get`, `fastmail_search`) plus one HITL-gated write tool (`fastmail_mail_send`) that submits a new email via JMAP EmailSubmission. Auth is a single Fastmail API token, injected as the scoped environment variable FASTMAIL_API_TOKEN at spawn time (with an optional FASTMAIL_BASE_URL override) — the connector never reads the vault. JMAP is pure HTTPS to api.fastmail.com (the session, api, and any upload/download endpoints are all under that host), so the sandbox network allow-list is a single static 443 host.",
6
+ "author": "Nimbus",
7
+ "entrypoint": "dist/server.js",
8
+ "runtime": "bun",
9
+ "permissions": {
10
+ "network": ["api.fastmail.com"],
11
+ "filesystem": { "read": [], "write": [] }
12
+ },
13
+ "hitlRequired": ["write"],
14
+ "syncInterval": 300,
15
+ "minNimbusVersion": "0.2.0"
16
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * JMAP connector core — transport-agnostic logic for the Fastmail read tools
3
+ * (`fastmail_list`, `fastmail_get`, `fastmail_search`) and the HITL-gated send
4
+ * tool (`fastmail_mail_send`).
5
+ *
6
+ * HARD SCOPE CONSTRAINT (security): this connector indexes/returns HEADERS + a
7
+ * short capped plain-text body PREVIEW + attachment METADATA only. The JMAP
8
+ * `Email/get` calls request `maxBodyValueBytes` (the server truncates the body
9
+ * value, so a full body never crosses the wire) and store only the `attachments`
10
+ * body-part METADATA (name/size/type) — the `blobId` download URL is NEVER
11
+ * dereferenced. There is no surface to fetch attachment bytes or a full body.
12
+ *
13
+ * The JMAP transport (real over `fetch`) is injected so tests never open a socket.
14
+ *
15
+ * Pure JMAP parsing and request-building is provided by `@nimbus-dev/sdk`
16
+ * (gateway ↔ mcp boundary — neither package can import the other).
17
+ */
18
+
19
+ import type { JmapEmailView } from "@nimbus-dev/sdk";
20
+
21
+ export {
22
+ asRecord,
23
+ asString,
24
+ buildGetRequest,
25
+ buildListRequest,
26
+ buildSearchRequest,
27
+ CORE_CAPABILITY,
28
+ capPreview,
29
+ EMAIL_PROPERTIES,
30
+ extractAttachments,
31
+ extractEmailList,
32
+ formatAddress,
33
+ formatAddresses,
34
+ type JmapAttachmentMeta,
35
+ type JmapEmailView,
36
+ type JmapSession,
37
+ MAIL_CAPABILITY,
38
+ MAX_BODY_VALUE_BYTES,
39
+ methodResponseArgs,
40
+ PREVIEW_MAX_CHARS,
41
+ parseSession,
42
+ previewFor,
43
+ SUBMISSION_CAPABILITY,
44
+ validateApiUrl,
45
+ viewEmail,
46
+ } from "@nimbus-dev/sdk";
47
+
48
+ const DEFAULT_LIST_LIMIT = 50;
49
+ const MAX_LIST_LIMIT = 200;
50
+
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
+ }
59
+
60
+ export interface SendMailResult {
61
+ readonly emailId: string | null;
62
+ readonly submissionId: string | null;
63
+ }
64
+
65
+ /**
66
+ * Minimal Fastmail/JMAP client surface the tools depend on. Implemented for real
67
+ * by the fetch adapter in `server.ts` and by a fake in tests. Deliberately
68
+ * exposes ONLY header/attachment-metadata + capped-preview reads + a send.
69
+ */
70
+ export interface JmapClient {
71
+ list(limit: number): Promise<JmapEmailView[]>;
72
+ get(id: string): Promise<JmapEmailView | null>;
73
+ search(query: string, limit: number): Promise<JmapEmailView[]>;
74
+ send(input: SendMailInput): Promise<SendMailResult>;
75
+ }
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
+ }
@@ -0,0 +1,194 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
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";
25
+ import { registerFastmailTools } from "./tools.ts";
26
+
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
+ const server = new McpServer({ name: "nimbus-fastmail", version: "0.1.0" });
192
+ registerFastmailTools(server, new FetchJmapClient());
193
+
194
+ await server.connect(new StdioServerTransport());