@nimbus-dev/connectors 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +18 -4
  2. package/connectors/airflow/src/search-filter.ts +2 -15
  3. package/connectors/airflow/src/server.ts +2 -87
  4. package/connectors/airflow/src/tools.ts +74 -0
  5. package/connectors/apple/src/adapters.ts +326 -0
  6. package/connectors/apple/src/apple-mail-core.ts +4 -8
  7. package/connectors/apple/src/calendar-tools.ts +28 -8
  8. package/connectors/apple/src/server.ts +23 -512
  9. package/connectors/apple/src/tools.ts +18 -44
  10. package/connectors/argocd/src/server.ts +25 -38
  11. package/connectors/athena/src/tools.ts +15 -34
  12. package/connectors/aws/src/server.ts +2 -183
  13. package/connectors/aws/src/tools.ts +197 -0
  14. package/connectors/azure/src/server.ts +2 -131
  15. package/connectors/azure/src/tools.ts +133 -0
  16. package/connectors/bigeye/src/search-filter.ts +4 -15
  17. package/connectors/bigeye/src/server.ts +38 -38
  18. package/connectors/bigquery/src/tools.ts +1 -26
  19. package/connectors/bitbucket/src/server.ts +2 -244
  20. package/connectors/bitbucket/src/tools.ts +241 -0
  21. package/connectors/bitrise/src/server.ts +2 -79
  22. package/connectors/bitrise/src/tools.ts +76 -0
  23. package/connectors/canva/src/search-filter.ts +2 -11
  24. package/connectors/canva/src/server.ts +2 -73
  25. package/connectors/canva/src/tools.ts +66 -0
  26. package/connectors/circleci/src/server.ts +2 -176
  27. package/connectors/circleci/src/tools.ts +193 -0
  28. package/connectors/cloud-logging/src/tools.ts +14 -27
  29. package/connectors/cloudwatch/src/tools.ts +15 -34
  30. package/connectors/codemagic/src/server.ts +2 -70
  31. package/connectors/codemagic/src/tools.ts +71 -0
  32. package/connectors/confluence/src/server.ts +2 -301
  33. package/connectors/confluence/src/tools.ts +320 -0
  34. package/connectors/dagster/src/server.ts +2 -214
  35. package/connectors/dagster/src/tools.ts +187 -0
  36. package/connectors/databricks/src/search-filter.ts +7 -7
  37. package/connectors/databricks/src/server.ts +2 -77
  38. package/connectors/databricks/src/tools.ts +66 -0
  39. package/connectors/datadog/src/server.ts +2 -48
  40. package/connectors/datadog/src/tools.ts +48 -0
  41. package/connectors/dataprofile/src/profile.ts +16 -46
  42. package/connectors/dbt/src/server.ts +2 -92
  43. package/connectors/dbt/src/tools.ts +80 -0
  44. package/connectors/dependencytrack/src/server.ts +2 -76
  45. package/connectors/dependencytrack/src/tools.ts +65 -0
  46. package/connectors/discord/src/server.ts +2 -85
  47. package/connectors/discord/src/tools.ts +82 -0
  48. package/connectors/elasticsearch/src/tools.ts +7 -37
  49. package/connectors/fastmail/src/jmap-client.ts +202 -0
  50. package/connectors/fastmail/src/jmap-core.ts +8 -21
  51. package/connectors/fastmail/src/server.ts +2 -187
  52. package/connectors/fastmail/src/tools.ts +4 -23
  53. package/connectors/figma/src/search-filter.ts +2 -11
  54. package/connectors/figma/src/server.ts +2 -113
  55. package/connectors/figma/src/tools.ts +99 -0
  56. package/connectors/firebase/src/server.ts +2 -90
  57. package/connectors/firebase/src/token.ts +2 -5
  58. package/connectors/firebase/src/tools.ts +106 -0
  59. package/connectors/flagsmith/src/server.ts +2 -98
  60. package/connectors/flagsmith/src/tools.ts +95 -0
  61. package/connectors/flux/src/server.ts +52 -53
  62. package/connectors/gcp/src/server.ts +2 -132
  63. package/connectors/gcp/src/tools.ts +123 -0
  64. package/connectors/github/src/server.ts +2 -328
  65. package/connectors/github/src/tools.ts +311 -0
  66. package/connectors/github-actions/src/server.ts +2 -205
  67. package/connectors/github-actions/src/tools.ts +209 -0
  68. package/connectors/gitlab/src/server.ts +2 -384
  69. package/connectors/gitlab/src/tools.ts +392 -0
  70. package/connectors/gmail/src/server.ts +2 -270
  71. package/connectors/gmail/src/tools.ts +209 -0
  72. package/connectors/google-drive/src/server.ts +2 -524
  73. package/connectors/google-drive/src/tools.ts +529 -0
  74. package/connectors/google-meet/src/server.ts +2 -82
  75. package/connectors/google-meet/src/tools.ts +87 -0
  76. package/connectors/google-photos/src/server.ts +2 -132
  77. package/connectors/google-photos/src/tools.ts +138 -0
  78. package/connectors/grafana/src/server.ts +2 -53
  79. package/connectors/grafana/src/tools.ts +53 -0
  80. package/connectors/great-expectations/src/gx-parse.ts +14 -45
  81. package/connectors/greenhouse/src/server.ts +2 -63
  82. package/connectors/greenhouse/src/tools.ts +68 -0
  83. package/connectors/hubspot/src/server.ts +2 -77
  84. package/connectors/hubspot/src/tools.ts +70 -0
  85. package/connectors/iac/src/server.ts +2 -137
  86. package/connectors/iac/src/tools.ts +152 -0
  87. package/connectors/imap/src/imap-core.ts +29 -82
  88. package/connectors/imap/src/server.ts +33 -220
  89. package/connectors/intercom/src/server.ts +2 -61
  90. package/connectors/intercom/src/tools.ts +58 -0
  91. package/connectors/jenkins/src/jenkins-api.ts +2 -6
  92. package/connectors/jenkins/src/server.ts +2 -215
  93. package/connectors/jenkins/src/tools.ts +240 -0
  94. package/connectors/jira/src/server.ts +2 -301
  95. package/connectors/jira/src/tools.ts +325 -0
  96. package/connectors/kubernetes/src/server.ts +2 -155
  97. package/connectors/kubernetes/src/tools.ts +159 -0
  98. package/connectors/launchdarkly/src/server.ts +2 -75
  99. package/connectors/launchdarkly/src/tools.ts +72 -0
  100. package/connectors/lever/src/search-filter.ts +4 -12
  101. package/connectors/lever/src/server.ts +2 -60
  102. package/connectors/lever/src/tools.ts +61 -0
  103. package/connectors/linear/src/server.ts +2 -459
  104. package/connectors/linear/src/tools.ts +482 -0
  105. package/connectors/localdb/src/sql-scan.ts +13 -41
  106. package/connectors/localdb/src/tools.ts +1 -1
  107. package/connectors/looker/src/search-filter.ts +2 -15
  108. package/connectors/looker/src/server.ts +35 -70
  109. package/connectors/mendeley/src/server.ts +2 -55
  110. package/connectors/mendeley/src/tools.ts +54 -0
  111. package/connectors/mercury/src/server.ts +2 -57
  112. package/connectors/mercury/src/tools.ts +50 -0
  113. package/connectors/metabase/src/search-filter.ts +4 -15
  114. package/connectors/metabase/src/server.ts +2 -79
  115. package/connectors/metabase/src/tools.ts +64 -0
  116. package/connectors/miro/src/server.ts +2 -72
  117. package/connectors/miro/src/tools.ts +65 -0
  118. package/connectors/mlflow/src/search-filter.ts +3 -7
  119. package/connectors/mlflow/src/server.ts +20 -40
  120. package/connectors/monte-carlo/src/server.ts +50 -57
  121. package/connectors/netlify/src/search-filter.ts +6 -14
  122. package/connectors/netlify/src/server.ts +2 -60
  123. package/connectors/netlify/src/tools.ts +53 -0
  124. package/connectors/newrelic/src/server.ts +2 -40
  125. package/connectors/newrelic/src/tools.ts +37 -0
  126. package/connectors/notion/src/server.ts +2 -335
  127. package/connectors/notion/src/tools.ts +344 -0
  128. package/connectors/obsidian/src/server.ts +2 -425
  129. package/connectors/obsidian/src/tools.ts +466 -0
  130. package/connectors/onedrive/src/server.ts +2 -249
  131. package/connectors/onedrive/src/tools.ts +253 -0
  132. package/connectors/outlook/src/server.ts +2 -351
  133. package/connectors/outlook/src/tools.ts +357 -0
  134. package/connectors/pagerduty/src/server.ts +2 -132
  135. package/connectors/pagerduty/src/tools.ts +126 -0
  136. package/connectors/pipedrive/src/server.ts +2 -58
  137. package/connectors/pipedrive/src/tools.ts +63 -0
  138. package/connectors/powerbi/src/search-filter.ts +2 -15
  139. package/connectors/powerbi/src/server.ts +43 -68
  140. package/connectors/prefect/src/search-filter.ts +3 -16
  141. package/connectors/prefect/src/server.ts +2 -90
  142. package/connectors/prefect/src/tools.ts +83 -0
  143. package/connectors/protonmail/src/mail-core.ts +16 -75
  144. package/connectors/protonmail/src/server.ts +34 -214
  145. package/connectors/raindrop/src/server.ts +2 -103
  146. package/connectors/raindrop/src/tools.ts +103 -0
  147. package/connectors/ramp/src/server.ts +2 -105
  148. package/connectors/ramp/src/tools.ts +98 -0
  149. package/connectors/readwise/src/server.ts +2 -88
  150. package/connectors/readwise/src/tools.ts +81 -0
  151. package/connectors/sagemaker/src/tools.ts +19 -38
  152. package/connectors/salesforce/src/search-filter.ts +4 -11
  153. package/connectors/salesforce/src/server.ts +2 -90
  154. package/connectors/salesforce/src/tools.ts +75 -0
  155. package/connectors/semgrep/src/search-filter.ts +3 -10
  156. package/connectors/semgrep/src/server.ts +2 -104
  157. package/connectors/semgrep/src/tools.ts +92 -0
  158. package/connectors/sentry/src/server.ts +2 -58
  159. package/connectors/sentry/src/tools.ts +53 -0
  160. package/connectors/slack/src/server.ts +2 -352
  161. package/connectors/slack/src/tools.ts +376 -0
  162. package/connectors/snowflake/src/search-filter.ts +4 -19
  163. package/connectors/snowflake/src/server.ts +6 -25
  164. package/connectors/snyk/src/server.ts +2 -140
  165. package/connectors/snyk/src/tools.ts +136 -0
  166. package/connectors/sonarqube/src/server.ts +2 -111
  167. package/connectors/sonarqube/src/tools.ts +98 -0
  168. package/connectors/stackoverflow/src/server.ts +2 -77
  169. package/connectors/stackoverflow/src/tools.ts +66 -0
  170. package/connectors/storybook/src/storybook-parse.ts +3 -6
  171. package/connectors/stripe/src/server.ts +2 -61
  172. package/connectors/stripe/src/tools.ts +54 -0
  173. package/connectors/superset/src/search-filter.ts +4 -15
  174. package/connectors/superset/src/server.ts +2 -115
  175. package/connectors/superset/src/tools.ts +98 -0
  176. package/connectors/tableau/src/search-filter.ts +2 -15
  177. package/connectors/tableau/src/server.ts +13 -65
  178. package/connectors/teams/src/server.ts +2 -249
  179. package/connectors/teams/src/tools.ts +283 -0
  180. package/connectors/testflight/src/server.ts +2 -70
  181. package/connectors/testflight/src/tools.ts +79 -0
  182. package/connectors/vercel/src/search-filter.ts +2 -10
  183. package/connectors/vercel/src/server.ts +2 -75
  184. package/connectors/vercel/src/tools.ts +73 -0
  185. package/connectors/vertex-ai/src/tools.ts +23 -33
  186. package/connectors/wiz/src/search-filter.ts +6 -30
  187. package/connectors/wiz/src/server.ts +2 -188
  188. package/connectors/wiz/src/tools.ts +168 -0
  189. package/connectors/zendesk/src/server.ts +2 -73
  190. package/connectors/zendesk/src/tools.ts +60 -0
  191. package/connectors/zoom/src/search-filter.ts +1 -7
  192. package/connectors/zoom/src/server.ts +2 -94
  193. package/connectors/zoom/src/tools.ts +93 -0
  194. package/connectors/zotero/src/server.ts +2 -78
  195. package/connectors/zotero/src/tools.ts +67 -0
  196. package/package.json +15 -12
  197. package/shared/access-token-cache.ts +123 -0
  198. package/shared/atlassian-json-fetch.ts +6 -16
  199. package/shared/audit-chain.ts +13 -6
  200. package/shared/cli-json-kit.ts +150 -0
  201. package/shared/collection-tool-kit.ts +143 -0
  202. package/shared/connector-mode.ts +3 -2
  203. package/shared/cursor-list-tool.ts +29 -0
  204. package/shared/env-json-api.ts +209 -0
  205. package/shared/fetch-json-text.ts +27 -0
  206. package/shared/github-rest.ts +62 -0
  207. package/shared/graphql-json.ts +60 -0
  208. package/shared/imap-mail-core.ts +91 -0
  209. package/shared/imap-tool-kit.ts +57 -23
  210. package/shared/imapflow-adapter.ts +417 -0
  211. package/shared/rest-tool-kit.ts +77 -7
  212. package/shared/run-cli-json.ts +38 -16
  213. package/shared/search-filter.ts +58 -18
  214. package/shared/walk-files.ts +118 -0
  215. package/standalone/src/launcher.ts +19 -7
@@ -1,98 +1,6 @@
1
- import { z } from "zod";
2
- import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
3
1
  import { runReadOnlyMcpConnector } from "../../../shared/run-read-only-mcp-connector.ts";
4
- import { encodeZoomMeetingPathSegment } from "./encode-meeting-path.ts";
5
- import { validateZoomRecordingsWindow } from "./recordings-window.ts";
6
- import { filterZoomMeetings } from "./search-filter.ts";
7
-
8
- const BASE = "https://api.zoom.us";
9
-
10
- function apiToken(): string {
11
- const t = process.env["ZOOM_TOKEN"]?.trim();
12
- if (t === undefined || t === "") {
13
- throw new Error("ZOOM_TOKEN is not set");
14
- }
15
- return t;
16
- }
17
-
18
- function authHeader(): Record<string, string> {
19
- return { Authorization: `Bearer ${apiToken()}`, Accept: "application/json" };
20
- }
21
-
22
- async function zoomGet(path: string): Promise<unknown> {
23
- const res = await fetch(`${BASE}${path}`, { headers: authHeader() });
24
- const text = await res.text();
25
- if (!res.ok) {
26
- throw new Error(`Zoom ${String(res.status)}: ${text.slice(0, 400)}`);
27
- }
28
- return JSON.parse(text) as unknown;
29
- }
2
+ import { registerZoomTools } from "./tools.ts";
30
3
 
31
4
  await runReadOnlyMcpConnector("nimbus-zoom", (reg) => {
32
- reg(
33
- "zoom_list",
34
- "List the authenticated user's scheduled Zoom meetings (`GET /v2/users/me/meetings?type=scheduled&page_size=100`). Returns the `{ meetings: [...], next_page_token, page_size, total_records }` envelope — `meetings` holds the meeting objects.",
35
- z.object({}),
36
- async () => {
37
- return jsonResult(await zoomGet("/v2/users/me/meetings?type=scheduled&page_size=100"));
38
- },
39
- );
40
-
41
- reg(
42
- "zoom_get",
43
- "Fetch one Zoom meeting by its numeric meeting id OR its UUID (`GET /v2/meetings/{meetingId}`). Returns the meeting object directly (NOT wrapped in `{ meetings }`). Throws when no match is found. UUIDs are auto-double-encoded when they start with `/` or contain `//` (Zoom's documented requirement).",
44
- z.object({
45
- id: z.string().min(1),
46
- }),
47
- async (p) => {
48
- return jsonResult(await zoomGet(`/v2/meetings/${encodeZoomMeetingPathSegment(p.id)}`));
49
- },
50
- );
51
-
52
- reg(
53
- "zoom_search",
54
- "**Substring search over the FIRST PAGE only** (up to 100 most recently-listed scheduled meetings) of the authenticated user's Zoom meetings. The Zoom REST API has no native text-search endpoint for meetings; this tool fetches `GET /v2/users/me/meetings?type=scheduled&page_size=100` once and matches the query locally against the meeting topic, agenda, and host id (case-insensitive). **Meetings older than the first page are not searchable here — query the local Nimbus index instead for full coverage.** Returns a `{ matches: [...] }` envelope.",
55
- z.object({
56
- query: z.string().min(1),
57
- limit: z.number().int().min(1).max(100).optional(),
58
- }),
59
- async (p) => {
60
- const root = await zoomGet("/v2/users/me/meetings?type=scheduled&page_size=100");
61
- const meetings = (root as { meetings?: unknown[] } | null)?.meetings;
62
- const filterOptions =
63
- p.limit === undefined ? { query: p.query } : { query: p.query, limit: p.limit };
64
- const matches = Array.isArray(meetings) ? filterZoomMeetings(meetings, filterOptions) : [];
65
- return jsonResult({ matches });
66
- },
67
- );
68
-
69
- reg(
70
- "zoom_recordings_list",
71
- 'List the authenticated user\'s cloud recordings in a date window (`GET /v2/users/me/recordings?from=&to=&page_size=100`). `from` and `to` are ISO-8601 date strings (YYYY-MM-DD or full ISO); **Zoom requires (to - from) <= 1 month** — this tool validates that locally and throws a clear error before consuming a Zoom API call. Returns the `{ meetings: [{ id, uuid, topic, recording_files: [{ id, file_type, recording_start, play_url, download_url, ... }] }], next_page_token, page_size, total_records, from, to }` envelope. **Transcript files (`file_type: "TRANSCRIPT"`) carry the speech-to-text content** — Nimbus indexes those as `zoom:transcript` items; for full transcript text query the local Nimbus index instead of refetching `download_url` from here.',
72
- z.object({
73
- from: z
74
- .string()
75
- .min(1)
76
- .describe("ISO-8601 start of window (e.g. '2026-05-01' or '2026-05-01T00:00:00Z')"),
77
- to: z.string().min(1).describe("ISO-8601 end of window; (to - from) must be <= 31 days"),
78
- }),
79
- async (p) => {
80
- validateZoomRecordingsWindow(p.from, p.to);
81
- const params = new URLSearchParams({ from: p.from, to: p.to, page_size: "100" });
82
- return jsonResult(await zoomGet(`/v2/users/me/recordings?${params.toString()}`));
83
- },
84
- );
85
-
86
- reg(
87
- "zoom_transcript_get",
88
- "Fetch the recording-file inventory for one meeting (`GET /v2/meetings/{meetingId}/recordings`). Use this to discover transcript file ids + metadata for a specific meeting; returns the `{ uuid, id, topic, recording_files: [...] }` shape. UUIDs are auto-double-encoded when they start with `/` or contain `//` (Zoom's documented requirement). **For the actual transcript TEXT, query the local Nimbus index** (`zoom:transcript` items, keyed by `<meeting_uuid>:<recording_file_id>`). This tool does NOT refetch + parse the VTT — that work is owned by the gateway-side syncable.",
89
- z.object({
90
- id: z.string().min(1).describe("Meeting numeric id (preferred) or meeting UUID"),
91
- }),
92
- async (p) => {
93
- return jsonResult(
94
- await zoomGet(`/v2/meetings/${encodeZoomMeetingPathSegment(p.id)}/recordings`),
95
- );
96
- },
97
- );
5
+ registerZoomTools(reg);
98
6
  });
@@ -0,0 +1,93 @@
1
+ import { z } from "zod";
2
+ import { createJsonGetter, envAuthHeaders } 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 { encodeZoomMeetingPathSegment } from "./encode-meeting-path.ts";
6
+ import { validateZoomRecordingsWindow } from "./recordings-window.ts";
7
+ import { filterZoomMeetings } from "./search-filter.ts";
8
+
9
+ const BASE = "https://api.zoom.us";
10
+
11
+ const zoomGet = createJsonGetter({
12
+ base: BASE,
13
+ label: "Zoom",
14
+ headers: envAuthHeaders({ env: "ZOOM_TOKEN" }),
15
+ });
16
+
17
+ /** Tool names exposed by this connector — for contract/introspection tests. */
18
+ export const ZOOM_TOOL_NAMES = [
19
+ "zoom_list",
20
+ "zoom_get",
21
+ "zoom_search",
22
+ "zoom_recordings_list",
23
+ "zoom_transcript_get",
24
+ ] as const;
25
+
26
+ export function registerZoomTools(reg: ZodToolRegistrar): void {
27
+ reg(
28
+ "zoom_list",
29
+ "List the authenticated user's scheduled Zoom meetings (`GET /v2/users/me/meetings?type=scheduled&page_size=100`). Returns the `{ meetings: [...], next_page_token, page_size, total_records }` envelope — `meetings` holds the meeting objects.",
30
+ z.object({}),
31
+ async () => {
32
+ return jsonResult(await zoomGet("/v2/users/me/meetings?type=scheduled&page_size=100"));
33
+ },
34
+ );
35
+
36
+ reg(
37
+ "zoom_get",
38
+ "Fetch one Zoom meeting by its numeric meeting id OR its UUID (`GET /v2/meetings/{meetingId}`). Returns the meeting object directly (NOT wrapped in `{ meetings }`). Throws when no match is found. UUIDs are auto-double-encoded when they start with `/` or contain `//` (Zoom's documented requirement).",
39
+ z.object({
40
+ id: z.string().min(1),
41
+ }),
42
+ async (p) => {
43
+ return jsonResult(await zoomGet(`/v2/meetings/${encodeZoomMeetingPathSegment(p.id)}`));
44
+ },
45
+ );
46
+
47
+ reg(
48
+ "zoom_search",
49
+ "**Substring search over the FIRST PAGE only** (up to 100 most recently-listed scheduled meetings) of the authenticated user's Zoom meetings. The Zoom REST API has no native text-search endpoint for meetings; this tool fetches `GET /v2/users/me/meetings?type=scheduled&page_size=100` once and matches the query locally against the meeting topic, agenda, and host id (case-insensitive). **Meetings older than the first page are not searchable here — query the local Nimbus index instead for full coverage.** Returns a `{ matches: [...] }` envelope.",
50
+ z.object({
51
+ query: z.string().min(1),
52
+ limit: z.number().int().min(1).max(100).optional(),
53
+ }),
54
+ async (p) => {
55
+ const root = await zoomGet("/v2/users/me/meetings?type=scheduled&page_size=100");
56
+ const meetings = (root as { meetings?: unknown[] } | null)?.meetings;
57
+ const filterOptions =
58
+ p.limit === undefined ? { query: p.query } : { query: p.query, limit: p.limit };
59
+ const matches = Array.isArray(meetings) ? filterZoomMeetings(meetings, filterOptions) : [];
60
+ return jsonResult({ matches });
61
+ },
62
+ );
63
+
64
+ reg(
65
+ "zoom_recordings_list",
66
+ 'List the authenticated user\'s cloud recordings in a date window (`GET /v2/users/me/recordings?from=&to=&page_size=100`). `from` and `to` are ISO-8601 date strings (YYYY-MM-DD or full ISO); **Zoom requires (to - from) <= 1 month** — this tool validates that locally and throws a clear error before consuming a Zoom API call. Returns the `{ meetings: [{ id, uuid, topic, recording_files: [{ id, file_type, recording_start, play_url, download_url, ... }] }], next_page_token, page_size, total_records, from, to }` envelope. **Transcript files (`file_type: "TRANSCRIPT"`) carry the speech-to-text content** — Nimbus indexes those as `zoom:transcript` items; for full transcript text query the local Nimbus index instead of refetching `download_url` from here.',
67
+ z.object({
68
+ from: z
69
+ .string()
70
+ .min(1)
71
+ .describe("ISO-8601 start of window (e.g. '2026-05-01' or '2026-05-01T00:00:00Z')"),
72
+ to: z.string().min(1).describe("ISO-8601 end of window; (to - from) must be <= 31 days"),
73
+ }),
74
+ async (p) => {
75
+ validateZoomRecordingsWindow(p.from, p.to);
76
+ const params = new URLSearchParams({ from: p.from, to: p.to, page_size: "100" });
77
+ return jsonResult(await zoomGet(`/v2/users/me/recordings?${params.toString()}`));
78
+ },
79
+ );
80
+
81
+ reg(
82
+ "zoom_transcript_get",
83
+ "Fetch the recording-file inventory for one meeting (`GET /v2/meetings/{meetingId}/recordings`). Use this to discover transcript file ids + metadata for a specific meeting; returns the `{ uuid, id, topic, recording_files: [...] }` shape. UUIDs are auto-double-encoded when they start with `/` or contain `//` (Zoom's documented requirement). **For the actual transcript TEXT, query the local Nimbus index** (`zoom:transcript` items, keyed by `<meeting_uuid>:<recording_file_id>`). This tool does NOT refetch + parse the VTT — that work is owned by the gateway-side syncable.",
84
+ z.object({
85
+ id: z.string().min(1).describe("Meeting numeric id (preferred) or meeting UUID"),
86
+ }),
87
+ async (p) => {
88
+ return jsonResult(
89
+ await zoomGet(`/v2/meetings/${encodeZoomMeetingPathSegment(p.id)}/recordings`),
90
+ );
91
+ },
92
+ );
93
+ }
@@ -1,82 +1,6 @@
1
- import { z } from "zod";
2
- import { matchesResult, 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 { filterZoteroItems } from "./search-filter.ts";
6
-
7
- const BASE = "https://api.zotero.org";
8
-
9
- function apiKey(): string {
10
- const k = process.env["ZOTERO_API_KEY"]?.trim();
11
- if (k === undefined || k === "") {
12
- throw new Error("ZOTERO_API_KEY is not set");
13
- }
14
- return k;
15
- }
16
-
17
- function library(): string {
18
- const l = process.env["ZOTERO_LIBRARY"]?.trim();
19
- if (l === undefined || l === "") {
20
- throw new Error("ZOTERO_LIBRARY is not set");
21
- }
22
- return l;
23
- }
24
-
25
- function authHeader(): Record<string, string> {
26
- return {
27
- "Zotero-API-Key": apiKey(),
28
- "Zotero-API-Version": "3",
29
- Accept: "application/json",
30
- };
31
- }
32
-
33
- async function zoteroGet(path: string): Promise<unknown> {
34
- const res = await fetch(`${BASE}${path}`, { headers: authHeader() });
35
- const text = await res.text();
36
- if (!res.ok) {
37
- throw new Error(`Zotero ${String(res.status)}: ${text.slice(0, 400)}`);
38
- }
39
- return JSON.parse(text) as unknown;
40
- }
41
-
42
- function itemsBasePath(): string {
43
- return `/${library()}/items`;
44
- }
2
+ import { registerZoteroTools } from "./tools.ts";
45
3
 
46
4
  await runReadOnlyMcpConnector("nimbus-zotero", (reg) => {
47
- reg(
48
- "zotero_list",
49
- "List the top-level items in the configured Zotero library (`GET /<library>/items?format=json&limit=100&start=0&sort=dateModified&direction=desc`). The `<library>` path segment is the configured library spec (e.g. `users/12345` or `groups/98765`). Returns the raw JSON array of item objects — each is `{ key, version, library, data: { itemType, title, creators, date, dateModified, tags, collections, DOI, url, abstractNote, ... } }`.",
50
- z.object({}),
51
- async () => {
52
- return jsonResult(
53
- await zoteroGet(
54
- `${itemsBasePath()}?format=json&limit=100&start=0&sort=dateModified&direction=desc`,
55
- ),
56
- );
57
- },
58
- );
59
-
60
- reg(
61
- "zotero_get",
62
- "Fetch one Zotero item by its item key (`GET /<library>/items/{key}`). Returns the item object directly (NOT wrapped in an array). Throws when no match is found.",
63
- z.object({
64
- key: z.string().min(1),
65
- }),
66
- async (p) => {
67
- return jsonResult(await zoteroGet(`${itemsBasePath()}/${encodeURIComponent(p.key)}`));
68
- },
69
- );
70
-
71
- reg(
72
- "zotero_search",
73
- "Substring search across the library's top-level items (first page only). Matches the query against the title, item type, abstract, DOI, publication title, formatted creator names, and tag names (case-insensitive). Returns a `{ matches: [...] }` envelope.",
74
- searchToolInputSchema(100),
75
- async (p) => {
76
- const root = await zoteroGet(
77
- `${itemsBasePath()}?format=json&limit=100&start=0&sort=dateModified&direction=desc`,
78
- );
79
- return matchesResult(root, filterZoteroItems, p);
80
- },
81
- );
5
+ registerZoteroTools(reg);
82
6
  });
@@ -0,0 +1,67 @@
1
+ import { z } from "zod";
2
+ import { createJsonGetter, requiredEnv } from "../../../shared/env-json-api.ts";
3
+ import { matchesResult, 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 { filterZoteroItems } from "./search-filter.ts";
7
+
8
+ const BASE = "https://api.zotero.org";
9
+
10
+ function authHeader(): Record<string, string> {
11
+ return {
12
+ "Zotero-API-Key": requiredEnv("ZOTERO_API_KEY"),
13
+ "Zotero-API-Version": "3",
14
+ Accept: "application/json",
15
+ };
16
+ }
17
+
18
+ const zoteroGet = createJsonGetter({
19
+ base: BASE,
20
+ label: "Zotero",
21
+ headers: authHeader,
22
+ });
23
+
24
+ function itemsBasePath(): string {
25
+ return `/${requiredEnv("ZOTERO_LIBRARY")}/items`;
26
+ }
27
+
28
+ /** Tool names exposed by this connector — for contract/introspection tests. */
29
+ export const ZOTERO_TOOL_NAMES = ["zotero_get", "zotero_list", "zotero_search"] as const;
30
+
31
+ export function registerZoteroTools(reg: ZodToolRegistrar): void {
32
+ reg(
33
+ "zotero_list",
34
+ "List the top-level items in the configured Zotero library (`GET /<library>/items?format=json&limit=100&start=0&sort=dateModified&direction=desc`). The `<library>` path segment is the configured library spec (e.g. `users/12345` or `groups/98765`). Returns the raw JSON array of item objects — each is `{ key, version, library, data: { itemType, title, creators, date, dateModified, tags, collections, DOI, url, abstractNote, ... } }`.",
35
+ z.object({}),
36
+ async () => {
37
+ return jsonResult(
38
+ await zoteroGet(
39
+ `${itemsBasePath()}?format=json&limit=100&start=0&sort=dateModified&direction=desc`,
40
+ ),
41
+ );
42
+ },
43
+ );
44
+
45
+ reg(
46
+ "zotero_get",
47
+ "Fetch one Zotero item by its item key (`GET /<library>/items/{key}`). Returns the item object directly (NOT wrapped in an array). Throws when no match is found.",
48
+ z.object({
49
+ key: z.string().min(1),
50
+ }),
51
+ async (p) => {
52
+ return jsonResult(await zoteroGet(`${itemsBasePath()}/${encodeURIComponent(p.key)}`));
53
+ },
54
+ );
55
+
56
+ reg(
57
+ "zotero_search",
58
+ "Substring search across the library's top-level items (first page only). Matches the query against the title, item type, abstract, DOI, publication title, formatted creator names, and tag names (case-insensitive). Returns a `{ matches: [...] }` envelope.",
59
+ searchToolInputSchema(100),
60
+ async (p) => {
61
+ const root = await zoteroGet(
62
+ `${itemsBasePath()}?format=json&limit=100&start=0&sort=dateModified&direction=desc`,
63
+ );
64
+ return matchesResult(root, filterZoteroItems, p);
65
+ },
66
+ );
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimbus-dev/connectors",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -25,10 +25,10 @@
25
25
  "!**/*.test.ts"
26
26
  ],
27
27
  "dependencies": {
28
- "@modelcontextprotocol/sdk": "1.30.0",
29
- "@nimbus-dev/sdk": "^1.20.0",
30
- "@types/nodemailer": "^7.0.4",
31
- "zod": "^4.4.2"
28
+ "@modelcontextprotocol/sdk": "1.32.0",
29
+ "@nimbus-dev/sdk": "^2.0.0",
30
+ "@types/nodemailer": "^8.0.2",
31
+ "zod": "^4.6.5"
32
32
  },
33
33
  "exports": {
34
34
  ".": "./standalone/src/bin.ts",
@@ -131,6 +131,7 @@
131
131
  },
132
132
  "scripts": {
133
133
  "test": "bun test",
134
+ "test:coverage": "bun test --coverage --coverage-reporter=lcov --coverage-reporter=text",
134
135
  "test:sandbox": "bun scripts/run-sandbox-contract.ts",
135
136
  "lint": "biome check --error-on-warnings .",
136
137
  "lint:fix": "biome check --write .",
@@ -138,20 +139,22 @@
138
139
  "audit:connector-consent": "bun scripts/check-connector-consent.ts",
139
140
  "audit:connector-deps": "bun scripts/check-connector-deps.ts",
140
141
  "audit:connector-entrypoints": "bun scripts/check-connector-entrypoints.ts",
141
- "check": "bun run lint && bun run typecheck && bun run audit:connector-consent && bun run audit:connector-deps && bun run audit:connector-entrypoints && bun run test"
142
+ "audit:tool-names": "bun scripts/sync-tool-names.ts --check",
143
+ "sync:tool-names": "bun scripts/sync-tool-names.ts",
144
+ "check": "bun run lint && bun run typecheck && bun run audit:connector-consent && bun run audit:connector-deps && bun run audit:connector-entrypoints && bun run audit:tool-names && bun run test"
142
145
  },
143
146
  "devDependencies": {
144
- "@biomejs/biome": "^2.5.10",
145
- "@types/bun": "^1.4.0",
147
+ "@biomejs/biome": "^2.5.15",
148
+ "@types/bun": "^1.4.2",
146
149
  "typescript": "^7.0.2"
147
150
  },
148
151
  "engines": {
149
152
  "bun": ">=1.2.0"
150
153
  },
151
154
  "optionalDependencies": {
152
- "hyparquet": "^1.29.1",
153
- "imapflow": "^1.7.2",
154
- "nodemailer": "^9.0.5",
155
- "tsdav": "^2.3.1"
155
+ "hyparquet": "^1.31.2",
156
+ "imapflow": "^2.2.4",
157
+ "nodemailer": "^10.0.14",
158
+ "tsdav": "^2.3.5"
156
159
  }
157
160
  }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * access-token-cache — the "exchange credentials for a short-lived access
3
+ * token, then reuse it" pattern.
4
+ *
5
+ * Three connectors reach an API that will not take their configured credential
6
+ * directly: Ramp and Wiz exchange a client id/secret at an OAuth token
7
+ * endpoint, Superset posts a username/password to its own `/security/login`.
8
+ * All three then cache the result for the life of the process, and all three
9
+ * had written out the same tail by hand —
10
+ *
11
+ * POST → non-2xx? throw `<label> <status>: <body>` → JSON.parse, and throw a
12
+ * distinct error when the body is not JSON → reject a missing or empty
13
+ * `access_token` → cache → return
14
+ *
15
+ * — which is four failure modes each of which is easy to get subtly wrong, and
16
+ * which Sonar flagged as duplicated between Ramp and Superset.
17
+ *
18
+ * The exchange REQUEST is not shared, only its result handling: an OAuth
19
+ * client-credentials POST and a JSON login POST have genuinely different
20
+ * bodies, headers and endpoints, and collapsing them would mean a config object
21
+ * that is really two shapes in a trench coat.
22
+ */
23
+
24
+ /** What a successful exchange yields. */
25
+ export interface AccessTokenResponse {
26
+ readonly ok: boolean;
27
+ readonly status: number;
28
+ readonly text: string;
29
+ }
30
+
31
+ export interface AccessTokenCacheConfig {
32
+ /**
33
+ * Prefix for every error, naming the exchange rather than the service — e.g.
34
+ * `"Ramp token exchange"`, `"Superset login"`. It is what tells a caller that
35
+ * the failure was authentication, not the request they actually made.
36
+ */
37
+ readonly label: string;
38
+ /** Performs the exchange. Called at most once per process, lazily. */
39
+ readonly exchange: () => Promise<AccessTokenResponse>;
40
+ /**
41
+ * Field holding the token in the parsed body. Defaults to `access_token`,
42
+ * which is what all three of these APIs return.
43
+ */
44
+ readonly tokenField?: string;
45
+ /** Body-snippet length in the thrown error. */
46
+ readonly snippetMax?: number;
47
+ }
48
+
49
+ /** Body-snippet length in the thrown error. The value every connector used. */
50
+ export const DEFAULT_SNIPPET_MAX = 400;
51
+
52
+ /** The getter {@link createAccessTokenCache} returns: call it for the token. */
53
+ export interface AccessTokenCache {
54
+ (): Promise<string>;
55
+ /**
56
+ * Forget the cached token, so the next call exchanges again. For tests: the
57
+ * cache lives as long as the module, which in a test run is the whole process,
58
+ * so a token one test obtained would otherwise answer every later test's call
59
+ * without that call's credentials ever being read.
60
+ */
61
+ clear(): void;
62
+ }
63
+
64
+ /**
65
+ * A `() => Promise<string>` that performs the exchange once and then returns
66
+ * the cached token.
67
+ *
68
+ * Only a SUCCESSFUL exchange is cached. Caching a failure would make one
69
+ * transient 503 at startup poison every later call for the life of the process,
70
+ * which for a long-running stdio connector means until the client restarts it.
71
+ *
72
+ * Concurrent first calls share ONE exchange. An MCP client may have several
73
+ * tool calls in flight at once, and each would otherwise find an empty cache
74
+ * and start its own token request — n simultaneous OAuth exchanges for one
75
+ * connector, which is both wasteful and a good way to meet a rate limit on the
76
+ * auth endpoint. The in-flight promise is cleared whichever way it settles, so
77
+ * a failed exchange stays retryable.
78
+ */
79
+ export function createAccessTokenCache(config: AccessTokenCacheConfig): AccessTokenCache {
80
+ const field = config.tokenField ?? "access_token";
81
+ const snippetMax = config.snippetMax ?? DEFAULT_SNIPPET_MAX;
82
+ let cached: string | null = null;
83
+ let inFlight: Promise<string> | null = null;
84
+
85
+ async function exchangeOnce(): Promise<string> {
86
+ const res = await config.exchange();
87
+ if (!res.ok) {
88
+ throw new Error(`${config.label} ${String(res.status)}: ${res.text.slice(0, snippetMax)}`);
89
+ }
90
+ let parsed: unknown;
91
+ try {
92
+ parsed = JSON.parse(res.text) as unknown;
93
+ } catch {
94
+ throw new Error(`${config.label}: invalid JSON response`);
95
+ }
96
+ const token = (parsed as Record<string, unknown> | null)?.[field];
97
+ if (typeof token !== "string" || token === "") {
98
+ throw new Error(`${config.label}: no ${field} in response`);
99
+ }
100
+ cached = token;
101
+ return token;
102
+ }
103
+
104
+ const get = async (): Promise<string> => {
105
+ if (cached !== null) {
106
+ return cached;
107
+ }
108
+ inFlight ??= exchangeOnce();
109
+ try {
110
+ return await inFlight;
111
+ } finally {
112
+ // Cleared on BOTH paths. Keeping a rejected promise here would turn one
113
+ // failed exchange into a permanently failing connector, which is the same
114
+ // trap as caching the failure.
115
+ inFlight = null;
116
+ }
117
+ };
118
+ return Object.assign(get, {
119
+ clear: (): void => {
120
+ cached = null;
121
+ },
122
+ });
123
+ }
@@ -1,3 +1,4 @@
1
+ import { fetchJsonText } from "./fetch-json-text.ts";
1
2
  import { encodeBasicAuthHeader } from "./mcp-tool-kit.ts";
2
3
  import { stripTrailingSlashes } from "./strip-trailing-slashes.ts";
3
4
 
@@ -23,20 +24,9 @@ export async function fetchAtlassianBasicAuthJsonText(
23
24
  token: string,
24
25
  init?: RequestInit,
25
26
  ): Promise<{ ok: boolean; status: number; text: string }> {
26
- const headers: Record<string, string> = {
27
- Accept: "application/json",
28
- Authorization: encodeBasicAuthHeader(email, token),
29
- };
30
- if (init?.body !== undefined) {
31
- headers["Content-Type"] = "application/json";
32
- }
33
- const res = await fetch(url, {
34
- ...init,
35
- headers: {
36
- ...headers,
37
- ...(init?.headers as Record<string, string> | undefined),
38
- },
39
- });
40
- const text = await res.text();
41
- return { ok: res.ok, status: res.status, text };
27
+ return fetchJsonText(
28
+ url,
29
+ { Accept: "application/json", Authorization: encodeBasicAuthHeader(email, token) },
30
+ init,
31
+ );
42
32
  }
@@ -35,12 +35,6 @@ type ChainedLine = {
35
35
  /** First link's predecessor. A fixed, all-zero digest, mirroring the gateway's audit chain. */
36
36
  export const GENESIS_HASH = "0".repeat(64);
37
37
 
38
- /**
39
- * Canonical JSON with sorted keys.
40
- *
41
- * Two structurally identical entries must hash identically regardless of insertion order —
42
- * otherwise re-serialising during verification could break a chain that was never tampered with.
43
- */
44
38
  /**
45
39
  * Key order for `canonicalJson`. Plain code-unit comparison, NOT `localeCompare`: the order has to
46
40
  * be the same on every machine and locale, or a chain written on one host fails to verify on
@@ -52,11 +46,24 @@ function compareKeys(a: string, b: string): number {
52
46
  return 0;
53
47
  }
54
48
 
49
+ /**
50
+ * Canonical JSON with sorted keys.
51
+ *
52
+ * Two structurally identical entries must hash identically regardless of insertion order —
53
+ * otherwise re-serialising during verification could break a chain that was never tampered with.
54
+ *
55
+ * For the same reason a key whose value is `undefined` is left out, as `JSON.stringify` leaves it
56
+ * out of the line that is actually written. Hashing it as `null` instead hashed something the log
57
+ * never contained, so verification — which can only re-hash what it reads back — reported the
58
+ * entry as tampered on its very first read. `kubernetes`' pod delete recorded exactly such a
59
+ * pre-state whenever the namespace was left to its default.
60
+ */
55
61
  function canonicalJson(value: unknown): string {
56
62
  if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
57
63
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
58
64
  const rec = value as Record<string, unknown>;
59
65
  const body = Object.keys(rec)
66
+ .filter((k) => rec[k] !== undefined)
60
67
  .sort(compareKeys)
61
68
  .map((k) => `${JSON.stringify(k)}:${canonicalJson(rec[k])}`)
62
69
  .join(",");