@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.
- package/README.md +19 -7
- package/connectors/airflow/src/search-filter.ts +2 -15
- package/connectors/airflow/src/server.ts +2 -87
- package/connectors/airflow/src/tools.ts +74 -0
- package/connectors/apple/src/adapters.ts +326 -0
- package/connectors/apple/src/apple-mail-core.ts +4 -8
- package/connectors/apple/src/calendar-tools.ts +28 -8
- package/connectors/apple/src/server.ts +23 -512
- package/connectors/apple/src/tools.ts +18 -44
- package/connectors/argocd/src/server.ts +25 -38
- package/connectors/athena/src/tools.ts +26 -43
- package/connectors/aws/src/server.ts +2 -183
- package/connectors/aws/src/tools.ts +199 -0
- package/connectors/azure/src/server.ts +2 -131
- package/connectors/azure/src/tools.ts +134 -0
- package/connectors/bigeye/src/search-filter.ts +4 -15
- package/connectors/bigeye/src/server.ts +38 -38
- package/connectors/bigquery/src/tools.ts +1 -26
- package/connectors/bitbucket/src/server.ts +2 -244
- package/connectors/bitbucket/src/tools.ts +241 -0
- package/connectors/bitrise/src/server.ts +2 -79
- package/connectors/bitrise/src/tools.ts +76 -0
- package/connectors/canva/src/search-filter.ts +2 -11
- package/connectors/canva/src/server.ts +2 -73
- package/connectors/canva/src/tools.ts +66 -0
- package/connectors/circleci/src/server.ts +2 -176
- package/connectors/circleci/src/tools.ts +193 -0
- package/connectors/cloud-logging/src/tools.ts +19 -35
- package/connectors/cloudwatch/src/tools.ts +20 -38
- package/connectors/codemagic/src/server.ts +2 -70
- package/connectors/codemagic/src/tools.ts +71 -0
- package/connectors/confluence/src/server.ts +2 -301
- package/connectors/confluence/src/tools.ts +320 -0
- package/connectors/dagster/src/server.ts +2 -214
- package/connectors/dagster/src/tools.ts +187 -0
- package/connectors/databricks/src/search-filter.ts +7 -7
- package/connectors/databricks/src/server.ts +2 -77
- package/connectors/databricks/src/tools.ts +66 -0
- package/connectors/datadog/src/server.ts +2 -48
- package/connectors/datadog/src/tools.ts +48 -0
- package/connectors/dataprofile/src/profile.ts +16 -46
- package/connectors/dbt/src/server.ts +2 -92
- package/connectors/dbt/src/tools.ts +80 -0
- package/connectors/dependencytrack/src/server.ts +2 -76
- package/connectors/dependencytrack/src/tools.ts +65 -0
- package/connectors/discord/src/server.ts +2 -85
- package/connectors/discord/src/tools.ts +82 -0
- package/connectors/elasticsearch/src/tools.ts +7 -37
- package/connectors/fastmail/src/jmap-client.ts +202 -0
- package/connectors/fastmail/src/jmap-core.ts +8 -21
- package/connectors/fastmail/src/server.ts +2 -187
- package/connectors/fastmail/src/tools.ts +4 -23
- package/connectors/figma/src/search-filter.ts +2 -11
- package/connectors/figma/src/server.ts +2 -113
- package/connectors/figma/src/tools.ts +99 -0
- package/connectors/firebase/src/server.ts +2 -90
- package/connectors/firebase/src/token.ts +2 -5
- package/connectors/firebase/src/tools.ts +106 -0
- package/connectors/flagsmith/src/server.ts +2 -98
- package/connectors/flagsmith/src/tools.ts +95 -0
- package/connectors/flux/src/server.ts +52 -53
- package/connectors/gcp/src/server.ts +2 -132
- package/connectors/gcp/src/tools.ts +123 -0
- package/connectors/github/src/server.ts +2 -328
- package/connectors/github/src/tools.ts +311 -0
- package/connectors/github-actions/src/server.ts +2 -205
- package/connectors/github-actions/src/tools.ts +209 -0
- package/connectors/gitlab/src/server.ts +2 -384
- package/connectors/gitlab/src/tools.ts +392 -0
- package/connectors/gmail/src/server.ts +2 -270
- package/connectors/gmail/src/tools.ts +209 -0
- package/connectors/google-drive/src/server.ts +2 -524
- package/connectors/google-drive/src/tools.ts +529 -0
- package/connectors/google-meet/src/server.ts +2 -82
- package/connectors/google-meet/src/tools.ts +87 -0
- package/connectors/google-photos/src/server.ts +2 -132
- package/connectors/google-photos/src/tools.ts +138 -0
- package/connectors/grafana/src/server.ts +2 -53
- package/connectors/grafana/src/tools.ts +53 -0
- package/connectors/great-expectations/src/gx-parse.ts +14 -45
- package/connectors/greenhouse/src/server.ts +2 -63
- package/connectors/greenhouse/src/tools.ts +68 -0
- package/connectors/hubspot/src/server.ts +2 -77
- package/connectors/hubspot/src/tools.ts +70 -0
- package/connectors/iac/src/server.ts +2 -137
- package/connectors/iac/src/tools.ts +153 -0
- package/connectors/imap/src/imap-core.ts +29 -82
- package/connectors/imap/src/server.ts +33 -220
- package/connectors/intercom/src/server.ts +2 -61
- package/connectors/intercom/src/tools.ts +58 -0
- package/connectors/jenkins/src/jenkins-api.ts +2 -6
- package/connectors/jenkins/src/server.ts +2 -215
- package/connectors/jenkins/src/tools.ts +240 -0
- package/connectors/jira/src/server.ts +2 -301
- package/connectors/jira/src/tools.ts +325 -0
- package/connectors/kubernetes/src/server.ts +2 -155
- package/connectors/kubernetes/src/tools.ts +160 -0
- package/connectors/launchdarkly/src/server.ts +2 -75
- package/connectors/launchdarkly/src/tools.ts +72 -0
- package/connectors/lever/src/search-filter.ts +4 -12
- package/connectors/lever/src/server.ts +2 -60
- package/connectors/lever/src/tools.ts +61 -0
- package/connectors/linear/src/server.ts +2 -459
- package/connectors/linear/src/tools.ts +482 -0
- package/connectors/localdb/src/sql-scan.ts +13 -41
- package/connectors/localdb/src/tools.ts +1 -1
- package/connectors/looker/src/search-filter.ts +2 -15
- package/connectors/looker/src/server.ts +35 -70
- package/connectors/mendeley/src/server.ts +2 -55
- package/connectors/mendeley/src/tools.ts +54 -0
- package/connectors/mercury/src/server.ts +2 -57
- package/connectors/mercury/src/tools.ts +50 -0
- package/connectors/metabase/src/search-filter.ts +4 -15
- package/connectors/metabase/src/server.ts +2 -79
- package/connectors/metabase/src/tools.ts +64 -0
- package/connectors/miro/src/server.ts +2 -72
- package/connectors/miro/src/tools.ts +65 -0
- package/connectors/mlflow/src/search-filter.ts +3 -7
- package/connectors/mlflow/src/server.ts +20 -40
- package/connectors/monte-carlo/src/server.ts +50 -57
- package/connectors/netlify/src/search-filter.ts +6 -14
- package/connectors/netlify/src/server.ts +2 -60
- package/connectors/netlify/src/tools.ts +53 -0
- package/connectors/newrelic/src/server.ts +2 -40
- package/connectors/newrelic/src/tools.ts +37 -0
- package/connectors/notion/src/server.ts +2 -335
- package/connectors/notion/src/tools.ts +344 -0
- package/connectors/obsidian/src/server.ts +2 -425
- package/connectors/obsidian/src/tools.ts +466 -0
- package/connectors/onedrive/src/server.ts +2 -249
- package/connectors/onedrive/src/tools.ts +253 -0
- package/connectors/outlook/src/server.ts +2 -351
- package/connectors/outlook/src/tools.ts +357 -0
- package/connectors/pagerduty/src/server.ts +2 -132
- package/connectors/pagerduty/src/tools.ts +126 -0
- package/connectors/pipedrive/src/server.ts +2 -58
- package/connectors/pipedrive/src/tools.ts +63 -0
- package/connectors/powerbi/src/search-filter.ts +2 -15
- package/connectors/powerbi/src/server.ts +43 -68
- package/connectors/prefect/src/search-filter.ts +3 -16
- package/connectors/prefect/src/server.ts +2 -90
- package/connectors/prefect/src/tools.ts +83 -0
- package/connectors/protonmail/src/mail-core.ts +16 -75
- package/connectors/protonmail/src/server.ts +34 -214
- package/connectors/raindrop/src/server.ts +2 -103
- package/connectors/raindrop/src/tools.ts +103 -0
- package/connectors/ramp/src/server.ts +2 -105
- package/connectors/ramp/src/tools.ts +98 -0
- package/connectors/readwise/src/server.ts +2 -88
- package/connectors/readwise/src/tools.ts +81 -0
- package/connectors/sagemaker/src/tools.ts +21 -40
- package/connectors/salesforce/src/search-filter.ts +4 -11
- package/connectors/salesforce/src/server.ts +2 -90
- package/connectors/salesforce/src/tools.ts +75 -0
- package/connectors/semgrep/src/search-filter.ts +3 -10
- package/connectors/semgrep/src/server.ts +2 -104
- package/connectors/semgrep/src/tools.ts +92 -0
- package/connectors/sentry/src/server.ts +2 -58
- package/connectors/sentry/src/tools.ts +53 -0
- package/connectors/slack/src/server.ts +2 -352
- package/connectors/slack/src/tools.ts +376 -0
- package/connectors/snowflake/src/search-filter.ts +4 -19
- package/connectors/snowflake/src/server.ts +6 -25
- package/connectors/snyk/src/server.ts +2 -140
- package/connectors/snyk/src/tools.ts +136 -0
- package/connectors/sonarqube/src/server.ts +2 -111
- package/connectors/sonarqube/src/tools.ts +98 -0
- package/connectors/stackoverflow/src/server.ts +2 -77
- package/connectors/stackoverflow/src/tools.ts +66 -0
- package/connectors/storybook/src/storybook-parse.ts +3 -6
- package/connectors/stripe/src/server.ts +2 -61
- package/connectors/stripe/src/tools.ts +54 -0
- package/connectors/superset/src/search-filter.ts +4 -15
- package/connectors/superset/src/server.ts +2 -115
- package/connectors/superset/src/tools.ts +98 -0
- package/connectors/tableau/src/search-filter.ts +2 -15
- package/connectors/tableau/src/server.ts +13 -65
- package/connectors/teams/src/server.ts +2 -249
- package/connectors/teams/src/tools.ts +283 -0
- package/connectors/testflight/src/server.ts +2 -70
- package/connectors/testflight/src/tools.ts +79 -0
- package/connectors/vercel/src/search-filter.ts +2 -10
- package/connectors/vercel/src/server.ts +2 -75
- package/connectors/vercel/src/tools.ts +73 -0
- package/connectors/vertex-ai/src/tools.ts +23 -33
- package/connectors/wiz/src/search-filter.ts +6 -30
- package/connectors/wiz/src/server.ts +2 -188
- package/connectors/wiz/src/tools.ts +168 -0
- package/connectors/zendesk/src/server.ts +2 -73
- package/connectors/zendesk/src/tools.ts +60 -0
- package/connectors/zoom/src/search-filter.ts +1 -7
- package/connectors/zoom/src/server.ts +2 -94
- package/connectors/zoom/src/tools.ts +93 -0
- package/connectors/zotero/src/server.ts +2 -78
- package/connectors/zotero/src/tools.ts +67 -0
- package/package.json +14 -12
- package/shared/access-token-cache.ts +123 -0
- package/shared/atlassian-json-fetch.ts +6 -16
- package/shared/audit-chain.ts +601 -34
- package/shared/cli-json-kit.ts +191 -0
- package/shared/collection-tool-kit.ts +143 -0
- package/shared/connector-mode.ts +3 -2
- package/shared/consent-kit.ts +81 -26
- package/shared/cursor-list-tool.ts +29 -0
- package/shared/env-json-api.ts +209 -0
- package/shared/fetch-json-text.ts +27 -0
- package/shared/github-rest.ts +62 -0
- package/shared/graphql-json.ts +60 -0
- package/shared/imap-mail-core.ts +91 -0
- package/shared/imap-tool-kit.ts +57 -23
- package/shared/imapflow-adapter.ts +417 -0
- package/shared/nimbus-spawn.ts +84 -27
- package/shared/rest-tool-kit.ts +77 -7
- package/shared/run-cli-json.ts +38 -16
- package/shared/safe-cli-arg.ts +139 -17
- package/shared/search-filter.ts +58 -18
- package/shared/walk-files.ts +118 -0
- package/shared/windows-batch-args.ts +273 -0
- package/shared/write-budget.ts +100 -0
- 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 {
|
|
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 {
|
|
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.
|
|
3
|
+
"version": "0.2.3",
|
|
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.
|
|
29
|
-
"@nimbus-dev/sdk": "^
|
|
30
|
-
"@types/nodemailer": "^8.0.
|
|
31
|
-
"zod": "^4.
|
|
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",
|
|
@@ -139,20 +139,22 @@
|
|
|
139
139
|
"audit:connector-consent": "bun scripts/check-connector-consent.ts",
|
|
140
140
|
"audit:connector-deps": "bun scripts/check-connector-deps.ts",
|
|
141
141
|
"audit:connector-entrypoints": "bun scripts/check-connector-entrypoints.ts",
|
|
142
|
-
"
|
|
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"
|
|
143
145
|
},
|
|
144
146
|
"devDependencies": {
|
|
145
|
-
"@biomejs/biome": "^2.5.
|
|
146
|
-
"@types/bun": "^1.4.
|
|
147
|
+
"@biomejs/biome": "^2.5.15",
|
|
148
|
+
"@types/bun": "^1.4.2",
|
|
147
149
|
"typescript": "^7.0.2"
|
|
148
150
|
},
|
|
149
151
|
"engines": {
|
|
150
152
|
"bun": ">=1.2.0"
|
|
151
153
|
},
|
|
152
154
|
"optionalDependencies": {
|
|
153
|
-
"hyparquet": "^1.
|
|
154
|
-
"imapflow": "^
|
|
155
|
-
"nodemailer": "^
|
|
156
|
-
"tsdav": "^2.3.
|
|
155
|
+
"hyparquet": "^1.31.2",
|
|
156
|
+
"imapflow": "^2.2.4",
|
|
157
|
+
"nodemailer": "^10.0.14",
|
|
158
|
+
"tsdav": "^2.3.5"
|
|
157
159
|
}
|
|
158
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
|
-
|
|
27
|
-
|
|
28
|
-
Authorization: encodeBasicAuthHeader(email, token),
|
|
29
|
-
|
|
30
|
-
|
|
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
|
}
|