@nimbus-dev/connectors 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/README.md +19 -7
  2. package/connectors/airflow/src/search-filter.ts +2 -15
  3. package/connectors/airflow/src/server.ts +2 -87
  4. package/connectors/airflow/src/tools.ts +74 -0
  5. package/connectors/apple/src/adapters.ts +326 -0
  6. package/connectors/apple/src/apple-mail-core.ts +4 -8
  7. package/connectors/apple/src/calendar-tools.ts +28 -8
  8. package/connectors/apple/src/server.ts +23 -512
  9. package/connectors/apple/src/tools.ts +18 -44
  10. package/connectors/argocd/src/server.ts +25 -38
  11. package/connectors/athena/src/tools.ts +26 -43
  12. package/connectors/aws/src/server.ts +2 -183
  13. package/connectors/aws/src/tools.ts +199 -0
  14. package/connectors/azure/src/server.ts +2 -131
  15. package/connectors/azure/src/tools.ts +134 -0
  16. package/connectors/bigeye/src/search-filter.ts +4 -15
  17. package/connectors/bigeye/src/server.ts +38 -38
  18. package/connectors/bigquery/src/tools.ts +1 -26
  19. package/connectors/bitbucket/src/server.ts +2 -244
  20. package/connectors/bitbucket/src/tools.ts +241 -0
  21. package/connectors/bitrise/src/server.ts +2 -79
  22. package/connectors/bitrise/src/tools.ts +76 -0
  23. package/connectors/canva/src/search-filter.ts +2 -11
  24. package/connectors/canva/src/server.ts +2 -73
  25. package/connectors/canva/src/tools.ts +66 -0
  26. package/connectors/circleci/src/server.ts +2 -176
  27. package/connectors/circleci/src/tools.ts +193 -0
  28. package/connectors/cloud-logging/src/tools.ts +19 -35
  29. package/connectors/cloudwatch/src/tools.ts +20 -38
  30. package/connectors/codemagic/src/server.ts +2 -70
  31. package/connectors/codemagic/src/tools.ts +71 -0
  32. package/connectors/confluence/src/server.ts +2 -301
  33. package/connectors/confluence/src/tools.ts +320 -0
  34. package/connectors/dagster/src/server.ts +2 -214
  35. package/connectors/dagster/src/tools.ts +187 -0
  36. package/connectors/databricks/src/search-filter.ts +7 -7
  37. package/connectors/databricks/src/server.ts +2 -77
  38. package/connectors/databricks/src/tools.ts +66 -0
  39. package/connectors/datadog/src/server.ts +2 -48
  40. package/connectors/datadog/src/tools.ts +48 -0
  41. package/connectors/dataprofile/src/profile.ts +16 -46
  42. package/connectors/dbt/src/server.ts +2 -92
  43. package/connectors/dbt/src/tools.ts +80 -0
  44. package/connectors/dependencytrack/src/server.ts +2 -76
  45. package/connectors/dependencytrack/src/tools.ts +65 -0
  46. package/connectors/discord/src/server.ts +2 -85
  47. package/connectors/discord/src/tools.ts +82 -0
  48. package/connectors/elasticsearch/src/tools.ts +7 -37
  49. package/connectors/fastmail/src/jmap-client.ts +202 -0
  50. package/connectors/fastmail/src/jmap-core.ts +8 -21
  51. package/connectors/fastmail/src/server.ts +2 -187
  52. package/connectors/fastmail/src/tools.ts +4 -23
  53. package/connectors/figma/src/search-filter.ts +2 -11
  54. package/connectors/figma/src/server.ts +2 -113
  55. package/connectors/figma/src/tools.ts +99 -0
  56. package/connectors/firebase/src/server.ts +2 -90
  57. package/connectors/firebase/src/token.ts +2 -5
  58. package/connectors/firebase/src/tools.ts +106 -0
  59. package/connectors/flagsmith/src/server.ts +2 -98
  60. package/connectors/flagsmith/src/tools.ts +95 -0
  61. package/connectors/flux/src/server.ts +52 -53
  62. package/connectors/gcp/src/server.ts +2 -132
  63. package/connectors/gcp/src/tools.ts +123 -0
  64. package/connectors/github/src/server.ts +2 -328
  65. package/connectors/github/src/tools.ts +311 -0
  66. package/connectors/github-actions/src/server.ts +2 -205
  67. package/connectors/github-actions/src/tools.ts +209 -0
  68. package/connectors/gitlab/src/server.ts +2 -384
  69. package/connectors/gitlab/src/tools.ts +392 -0
  70. package/connectors/gmail/src/server.ts +2 -270
  71. package/connectors/gmail/src/tools.ts +209 -0
  72. package/connectors/google-drive/src/server.ts +2 -524
  73. package/connectors/google-drive/src/tools.ts +529 -0
  74. package/connectors/google-meet/src/server.ts +2 -82
  75. package/connectors/google-meet/src/tools.ts +87 -0
  76. package/connectors/google-photos/src/server.ts +2 -132
  77. package/connectors/google-photos/src/tools.ts +138 -0
  78. package/connectors/grafana/src/server.ts +2 -53
  79. package/connectors/grafana/src/tools.ts +53 -0
  80. package/connectors/great-expectations/src/gx-parse.ts +14 -45
  81. package/connectors/greenhouse/src/server.ts +2 -63
  82. package/connectors/greenhouse/src/tools.ts +68 -0
  83. package/connectors/hubspot/src/server.ts +2 -77
  84. package/connectors/hubspot/src/tools.ts +70 -0
  85. package/connectors/iac/src/server.ts +2 -137
  86. package/connectors/iac/src/tools.ts +153 -0
  87. package/connectors/imap/src/imap-core.ts +29 -82
  88. package/connectors/imap/src/server.ts +33 -220
  89. package/connectors/intercom/src/server.ts +2 -61
  90. package/connectors/intercom/src/tools.ts +58 -0
  91. package/connectors/jenkins/src/jenkins-api.ts +2 -6
  92. package/connectors/jenkins/src/server.ts +2 -215
  93. package/connectors/jenkins/src/tools.ts +240 -0
  94. package/connectors/jira/src/server.ts +2 -301
  95. package/connectors/jira/src/tools.ts +325 -0
  96. package/connectors/kubernetes/src/server.ts +2 -155
  97. package/connectors/kubernetes/src/tools.ts +160 -0
  98. package/connectors/launchdarkly/src/server.ts +2 -75
  99. package/connectors/launchdarkly/src/tools.ts +72 -0
  100. package/connectors/lever/src/search-filter.ts +4 -12
  101. package/connectors/lever/src/server.ts +2 -60
  102. package/connectors/lever/src/tools.ts +61 -0
  103. package/connectors/linear/src/server.ts +2 -459
  104. package/connectors/linear/src/tools.ts +482 -0
  105. package/connectors/localdb/src/sql-scan.ts +13 -41
  106. package/connectors/localdb/src/tools.ts +1 -1
  107. package/connectors/looker/src/search-filter.ts +2 -15
  108. package/connectors/looker/src/server.ts +35 -70
  109. package/connectors/mendeley/src/server.ts +2 -55
  110. package/connectors/mendeley/src/tools.ts +54 -0
  111. package/connectors/mercury/src/server.ts +2 -57
  112. package/connectors/mercury/src/tools.ts +50 -0
  113. package/connectors/metabase/src/search-filter.ts +4 -15
  114. package/connectors/metabase/src/server.ts +2 -79
  115. package/connectors/metabase/src/tools.ts +64 -0
  116. package/connectors/miro/src/server.ts +2 -72
  117. package/connectors/miro/src/tools.ts +65 -0
  118. package/connectors/mlflow/src/search-filter.ts +3 -7
  119. package/connectors/mlflow/src/server.ts +20 -40
  120. package/connectors/monte-carlo/src/server.ts +50 -57
  121. package/connectors/netlify/src/search-filter.ts +6 -14
  122. package/connectors/netlify/src/server.ts +2 -60
  123. package/connectors/netlify/src/tools.ts +53 -0
  124. package/connectors/newrelic/src/server.ts +2 -40
  125. package/connectors/newrelic/src/tools.ts +37 -0
  126. package/connectors/notion/src/server.ts +2 -335
  127. package/connectors/notion/src/tools.ts +344 -0
  128. package/connectors/obsidian/src/server.ts +2 -425
  129. package/connectors/obsidian/src/tools.ts +466 -0
  130. package/connectors/onedrive/src/server.ts +2 -249
  131. package/connectors/onedrive/src/tools.ts +253 -0
  132. package/connectors/outlook/src/server.ts +2 -351
  133. package/connectors/outlook/src/tools.ts +357 -0
  134. package/connectors/pagerduty/src/server.ts +2 -132
  135. package/connectors/pagerduty/src/tools.ts +126 -0
  136. package/connectors/pipedrive/src/server.ts +2 -58
  137. package/connectors/pipedrive/src/tools.ts +63 -0
  138. package/connectors/powerbi/src/search-filter.ts +2 -15
  139. package/connectors/powerbi/src/server.ts +43 -68
  140. package/connectors/prefect/src/search-filter.ts +3 -16
  141. package/connectors/prefect/src/server.ts +2 -90
  142. package/connectors/prefect/src/tools.ts +83 -0
  143. package/connectors/protonmail/src/mail-core.ts +16 -75
  144. package/connectors/protonmail/src/server.ts +34 -214
  145. package/connectors/raindrop/src/server.ts +2 -103
  146. package/connectors/raindrop/src/tools.ts +103 -0
  147. package/connectors/ramp/src/server.ts +2 -105
  148. package/connectors/ramp/src/tools.ts +98 -0
  149. package/connectors/readwise/src/server.ts +2 -88
  150. package/connectors/readwise/src/tools.ts +81 -0
  151. package/connectors/sagemaker/src/tools.ts +21 -40
  152. package/connectors/salesforce/src/search-filter.ts +4 -11
  153. package/connectors/salesforce/src/server.ts +2 -90
  154. package/connectors/salesforce/src/tools.ts +75 -0
  155. package/connectors/semgrep/src/search-filter.ts +3 -10
  156. package/connectors/semgrep/src/server.ts +2 -104
  157. package/connectors/semgrep/src/tools.ts +92 -0
  158. package/connectors/sentry/src/server.ts +2 -58
  159. package/connectors/sentry/src/tools.ts +53 -0
  160. package/connectors/slack/src/server.ts +2 -352
  161. package/connectors/slack/src/tools.ts +376 -0
  162. package/connectors/snowflake/src/search-filter.ts +4 -19
  163. package/connectors/snowflake/src/server.ts +6 -25
  164. package/connectors/snyk/src/server.ts +2 -140
  165. package/connectors/snyk/src/tools.ts +136 -0
  166. package/connectors/sonarqube/src/server.ts +2 -111
  167. package/connectors/sonarqube/src/tools.ts +98 -0
  168. package/connectors/stackoverflow/src/server.ts +2 -77
  169. package/connectors/stackoverflow/src/tools.ts +66 -0
  170. package/connectors/storybook/src/storybook-parse.ts +3 -6
  171. package/connectors/stripe/src/server.ts +2 -61
  172. package/connectors/stripe/src/tools.ts +54 -0
  173. package/connectors/superset/src/search-filter.ts +4 -15
  174. package/connectors/superset/src/server.ts +2 -115
  175. package/connectors/superset/src/tools.ts +98 -0
  176. package/connectors/tableau/src/search-filter.ts +2 -15
  177. package/connectors/tableau/src/server.ts +13 -65
  178. package/connectors/teams/src/server.ts +2 -249
  179. package/connectors/teams/src/tools.ts +283 -0
  180. package/connectors/testflight/src/server.ts +2 -70
  181. package/connectors/testflight/src/tools.ts +79 -0
  182. package/connectors/vercel/src/search-filter.ts +2 -10
  183. package/connectors/vercel/src/server.ts +2 -75
  184. package/connectors/vercel/src/tools.ts +73 -0
  185. package/connectors/vertex-ai/src/tools.ts +23 -33
  186. package/connectors/wiz/src/search-filter.ts +6 -30
  187. package/connectors/wiz/src/server.ts +2 -188
  188. package/connectors/wiz/src/tools.ts +168 -0
  189. package/connectors/zendesk/src/server.ts +2 -73
  190. package/connectors/zendesk/src/tools.ts +60 -0
  191. package/connectors/zoom/src/search-filter.ts +1 -7
  192. package/connectors/zoom/src/server.ts +2 -94
  193. package/connectors/zoom/src/tools.ts +93 -0
  194. package/connectors/zotero/src/server.ts +2 -78
  195. package/connectors/zotero/src/tools.ts +67 -0
  196. package/package.json +14 -12
  197. package/shared/access-token-cache.ts +123 -0
  198. package/shared/atlassian-json-fetch.ts +6 -16
  199. package/shared/audit-chain.ts +601 -34
  200. package/shared/cli-json-kit.ts +191 -0
  201. package/shared/collection-tool-kit.ts +143 -0
  202. package/shared/connector-mode.ts +3 -2
  203. package/shared/consent-kit.ts +81 -26
  204. package/shared/cursor-list-tool.ts +29 -0
  205. package/shared/env-json-api.ts +209 -0
  206. package/shared/fetch-json-text.ts +27 -0
  207. package/shared/github-rest.ts +62 -0
  208. package/shared/graphql-json.ts +60 -0
  209. package/shared/imap-mail-core.ts +91 -0
  210. package/shared/imap-tool-kit.ts +57 -23
  211. package/shared/imapflow-adapter.ts +417 -0
  212. package/shared/nimbus-spawn.ts +84 -27
  213. package/shared/rest-tool-kit.ts +77 -7
  214. package/shared/run-cli-json.ts +38 -16
  215. package/shared/safe-cli-arg.ts +139 -17
  216. package/shared/search-filter.ts +58 -18
  217. package/shared/walk-files.ts +118 -0
  218. package/shared/windows-batch-args.ts +273 -0
  219. package/shared/write-budget.ts +100 -0
  220. package/standalone/src/launcher.ts +19 -7
@@ -0,0 +1,80 @@
1
+ import { z } from "zod";
2
+ import { createJsonGetter, envAuthHeaders, optionalBaseUrl } from "../../../shared/env-json-api.ts";
3
+ import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
4
+ import type { ZodToolRegistrar } from "../../../shared/run-read-only-mcp-connector.ts";
5
+ import { filterDbtJobs } from "./search-filter.ts";
6
+
7
+ const DEFAULT_API_BASE = "https://cloud.getdbt.com";
8
+
9
+ const dbtGet = createJsonGetter({
10
+ base: () => `${optionalBaseUrl("DBT_API_BASE", DEFAULT_API_BASE)}/api/v2`,
11
+ label: "dbt Cloud",
12
+ headers: envAuthHeaders({ env: "DBT_TOKEN", scheme: "Token" }),
13
+ });
14
+
15
+ function dataFrom(root: unknown): unknown[] {
16
+ if (Array.isArray(root)) {
17
+ return root;
18
+ }
19
+ const data = (root as { data?: unknown } | null)?.data;
20
+ return Array.isArray(data) ? data : [];
21
+ }
22
+
23
+ /** Tool names exposed by this connector — for contract/introspection tests. */
24
+ export const DBT_TOOL_NAMES = ["dbt_get", "dbt_list", "dbt_search"] as const;
25
+
26
+ export function registerDbtTools(reg: ZodToolRegistrar): void {
27
+ reg(
28
+ "dbt_list",
29
+ "List dbt Cloud accounts, or jobs for an account. Without `accountId`, returns the account's accounts (`/accounts/`). With `accountId`, returns that account's jobs (`/accounts/{accountId}/jobs/`), capped at `limit` (default 100). Returns the raw dbt Cloud envelope (`{ data: [...] }`).",
30
+ z.object({
31
+ accountId: z.number().int().optional(),
32
+ limit: z.number().int().min(1).max(500).optional(),
33
+ }),
34
+ async (p) => {
35
+ if (p.accountId === undefined) {
36
+ return jsonResult(await dbtGet("/accounts/"));
37
+ }
38
+ const search = new URLSearchParams({ limit: String(p.limit ?? 100) });
39
+ return jsonResult(
40
+ await dbtGet(
41
+ `/accounts/${encodeURIComponent(String(p.accountId))}/jobs/?${search.toString()}`,
42
+ ),
43
+ );
44
+ },
45
+ );
46
+
47
+ reg(
48
+ "dbt_get",
49
+ "Fetch one dbt Cloud job by `accountId` + `jobId` (`/accounts/{accountId}/jobs/{jobId}/`). Returns the raw dbt Cloud envelope. Throws when no such job exists.",
50
+ z.object({
51
+ accountId: z.number().int(),
52
+ jobId: z.number().int(),
53
+ }),
54
+ async (p) => {
55
+ return jsonResult(
56
+ await dbtGet(
57
+ `/accounts/${encodeURIComponent(String(p.accountId))}/jobs/${encodeURIComponent(String(p.jobId))}/`,
58
+ ),
59
+ );
60
+ },
61
+ );
62
+
63
+ reg(
64
+ "dbt_search",
65
+ "Substring search across an account's dbt Cloud jobs. Matches the query against job name, dbt version, and id (case-insensitive). Returns a `{ matches: [...] }` envelope.",
66
+ z.object({
67
+ accountId: z.number().int(),
68
+ query: z.string().min(1),
69
+ limit: z.number().int().min(1).max(200).optional(),
70
+ }),
71
+ async (p) => {
72
+ const search = new URLSearchParams({ limit: "500" });
73
+ const root = await dbtGet(
74
+ `/accounts/${encodeURIComponent(String(p.accountId))}/jobs/?${search.toString()}`,
75
+ );
76
+ const matches = filterDbtJobs(dataFrom(root), { query: p.query, limit: p.limit });
77
+ return jsonResult({ matches });
78
+ },
79
+ );
80
+ }
@@ -1,80 +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 { 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
- }
2
+ import { registerDependencytrackTools } from "./tools.ts";
39
3
 
40
4
  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
- );
5
+ registerDependencytrackTools(reg);
80
6
  });
@@ -0,0 +1,65 @@
1
+ import { z } from "zod";
2
+ import { createJsonGetter, envAuthHeaders, requiredBaseUrl } from "../../../shared/env-json-api.ts";
3
+ import { searchToolInputSchema } from "../../../shared/mcp-search-tool.ts";
4
+ import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
5
+ import type { ZodToolRegistrar } from "../../../shared/run-read-only-mcp-connector.ts";
6
+ import { filterDependencyTrackProjects } from "./search-filter.ts";
7
+
8
+ const dtGet = createJsonGetter({
9
+ base: () => requiredBaseUrl("DEPENDENCYTRACK_URL"),
10
+ label: "Dependency-Track",
11
+ headers: envAuthHeaders({ env: "DEPENDENCYTRACK_API_KEY", scheme: "", header: "X-Api-Key" }),
12
+ });
13
+
14
+ function projectsFrom(root: unknown): unknown[] {
15
+ return Array.isArray(root) ? root : [];
16
+ }
17
+
18
+ /** Tool names exposed by this connector — for contract/introspection tests. */
19
+ export const DEPENDENCYTRACK_TOOL_NAMES = [
20
+ "dependencytrack_list",
21
+ "dependencytrack_get",
22
+ "dependencytrack_search",
23
+ ] as const;
24
+
25
+ export function registerDependencytrackTools(reg: ZodToolRegistrar): void {
26
+ reg(
27
+ "dependencytrack_list",
28
+ "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.",
29
+ z.object({
30
+ pageNumber: z.number().int().min(1).optional(),
31
+ }),
32
+ async (p) => {
33
+ const page = p.pageNumber ?? 1;
34
+ const root = await dtGet(
35
+ `/api/v1/project?pageSize=100&pageNumber=${encodeURIComponent(String(page))}&excludeInactive=false`,
36
+ );
37
+ return jsonResult({ items: projectsFrom(root) });
38
+ },
39
+ );
40
+
41
+ reg(
42
+ "dependencytrack_get",
43
+ "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.",
44
+ z.object({
45
+ uuid: z.string().min(1),
46
+ }),
47
+ async (p) => {
48
+ return jsonResult(await dtGet(`/api/v1/project/${encodeURIComponent(p.uuid)}`));
49
+ },
50
+ );
51
+
52
+ reg(
53
+ "dependencytrack_search",
54
+ "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.",
55
+ searchToolInputSchema(100),
56
+ async (p) => {
57
+ const root = await dtGet("/api/v1/project?pageSize=100&pageNumber=1&excludeInactive=false");
58
+ const matches = filterDependencyTrackProjects(projectsFrom(root), {
59
+ query: p.query,
60
+ limit: p.limit,
61
+ });
62
+ return jsonResult({ matches });
63
+ },
64
+ );
65
+ }
@@ -1,93 +1,10 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
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
- }
3
+ import { registerDiscordTools } from "./tools.ts";
38
4
 
39
5
  const server = new McpServer({ name: "nimbus-discord", version: "0.1.0" });
40
6
 
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
- );
7
+ registerDiscordTools(server);
91
8
 
92
9
  const transport = new StdioServerTransport();
93
10
  await server.connect(transport);
@@ -0,0 +1,82 @@
1
+ import { z } from "zod";
2
+ import { createRegisterSimpleTool, createZodToolRegistrar } from "../../../shared/mcp-tool-kit.ts";
3
+ import {
4
+ makeRestToolRegistrar,
5
+ type RestFetchResult,
6
+ toRestFetchResult,
7
+ } from "../../../shared/rest-tool-kit.ts";
8
+
9
+ const DISCORD_API = "https://discord.com/api/v10";
10
+
11
+ async function discordFetch(
12
+ token: string,
13
+ path: string,
14
+ init?: RequestInit,
15
+ ): Promise<RestFetchResult> {
16
+ const url = path.startsWith("http") ? path : `${DISCORD_API}${path}`;
17
+ const res = await fetch(url, {
18
+ ...init,
19
+ headers: {
20
+ Authorization: `Bot ${token}`,
21
+ "User-Agent": "NimbusMCP (https://github.com/nimbus-dev/nimbus)",
22
+ ...(init?.headers as Record<string, string> | undefined),
23
+ },
24
+ });
25
+ return toRestFetchResult(res);
26
+ }
27
+
28
+ export function registerDiscordTools(server: { tool: (...args: never) => unknown }): void {
29
+ const registerSimpleTool = createRegisterSimpleTool(server);
30
+ const reg = createZodToolRegistrar(registerSimpleTool);
31
+
32
+ /** Standard Discord read tool: token → discordFetch(buildPath) → mcpJsonResultIfOk("Discord"). */
33
+ const registerDiscordTool = makeRestToolRegistrar({
34
+ registrar: reg,
35
+ tokenEnv: "DISCORD_BOT_TOKEN",
36
+ serviceLabel: "Discord",
37
+ fetch: discordFetch,
38
+ });
39
+
40
+ registerDiscordTool(
41
+ "discord_guild_list",
42
+ "List guilds the bot is a member of.",
43
+ z.object({}),
44
+ () => "/users/@me/guilds",
45
+ );
46
+
47
+ registerDiscordTool(
48
+ "discord_channel_list",
49
+ "List channels in a guild (id, type, name).",
50
+ z.object({ guildId: z.string().min(1) }),
51
+ (parsed) => `/guilds/${encodeURIComponent(parsed.guildId)}/channels`,
52
+ );
53
+
54
+ registerDiscordTool(
55
+ "discord_channel_messages",
56
+ "List recent messages in a channel (newest first). Optional `after` snowflake for incremental fetch.",
57
+ z.object({
58
+ channelId: z.string().min(1),
59
+ limit: z.number().int().min(1).max(100).optional(),
60
+ after: z.string().optional(),
61
+ }),
62
+ (parsed) => {
63
+ const lim = parsed.limit ?? 50;
64
+ const path = `/channels/${encodeURIComponent(parsed.channelId)}/messages`;
65
+ const u = new URL(`${DISCORD_API}${path}`);
66
+ u.searchParams.set("limit", String(lim));
67
+ if (parsed.after !== undefined && parsed.after !== "") {
68
+ u.searchParams.set("after", parsed.after);
69
+ }
70
+ // Relative to DISCORD_API, like every other tool here: the fetcher prefixes the base, which
71
+ // already ends in /api/v10, so returning `u.pathname` requested /api/v10/api/v10/….
72
+ return `${path}${u.search}`;
73
+ },
74
+ );
75
+
76
+ registerDiscordTool(
77
+ "discord_thread_list",
78
+ "List active threads in a guild (includes public threads the bot can see).",
79
+ z.object({ guildId: z.string().min(1) }),
80
+ (parsed) => `/guilds/${encodeURIComponent(parsed.guildId)}/threads/active`,
81
+ );
82
+ }
@@ -1,4 +1,6 @@
1
1
  import { z } from "zod";
2
+ import { isRecord, strField } from "../../../shared/cli-json-kit.ts";
3
+ import { createJsonGetter, envAuthHeaders, requiredBaseUrl } from "../../../shared/env-json-api.ts";
2
4
  import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
3
5
  import type { ZodToolRegistrar } from "../../../shared/run-read-only-mcp-connector.ts";
4
6
 
@@ -18,43 +20,11 @@ export const ELASTICSEARCH_TOOL_NAMES = [
18
20
  "elasticsearch_search",
19
21
  ] as const;
20
22
 
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
- }
23
+ const esGet = createJsonGetter({
24
+ base: () => requiredBaseUrl("ELASTICSEARCH_URL"),
25
+ label: "Elasticsearch",
26
+ headers: envAuthHeaders({ env: "ELASTICSEARCH_API_KEY", scheme: "ApiKey" }),
27
+ });
58
28
 
59
29
  function indexEntries(parsed: unknown): unknown[] {
60
30
  return Array.isArray(parsed) ? parsed : [];
@@ -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 };