@nimbus-dev/connectors 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +662 -0
- package/NOTICE +40 -0
- package/README.md +111 -0
- package/connectors/airflow/nimbus.extension.json +16 -0
- package/connectors/airflow/src/search-filter.ts +38 -0
- package/connectors/airflow/src/server.ts +91 -0
- package/connectors/apple/nimbus.extension.json +40 -0
- package/connectors/apple/src/apple-mail-core.ts +43 -0
- package/connectors/apple/src/caldav-core.ts +116 -0
- package/connectors/apple/src/calendar-tools.ts +278 -0
- package/connectors/apple/src/server.ts +541 -0
- package/connectors/apple/src/tools.ts +138 -0
- package/connectors/argocd/nimbus.extension.json +16 -0
- package/connectors/argocd/src/search-filter.ts +24 -0
- package/connectors/argocd/src/server.ts +158 -0
- package/connectors/athena/nimbus.extension.json +15 -0
- package/connectors/athena/src/server.ts +6 -0
- package/connectors/athena/src/tools.ts +181 -0
- package/connectors/aws/nimbus.extension.json +21 -0
- package/connectors/aws/src/server.ts +191 -0
- package/connectors/azure/nimbus.extension.json +15 -0
- package/connectors/azure/src/server.ts +139 -0
- package/connectors/bigeye/nimbus.extension.json +16 -0
- package/connectors/bigeye/src/search-filter.ts +22 -0
- package/connectors/bigeye/src/server.ts +161 -0
- package/connectors/bigquery/nimbus.extension.json +15 -0
- package/connectors/bigquery/src/server.ts +6 -0
- package/connectors/bigquery/src/tools.ts +175 -0
- package/connectors/bitbucket/nimbus.extension.json +15 -0
- package/connectors/bitbucket/src/server.ts +252 -0
- package/connectors/bitrise/nimbus.extension.json +15 -0
- package/connectors/bitrise/src/search-filter.ts +11 -0
- package/connectors/bitrise/src/server.ts +83 -0
- package/connectors/canva/nimbus.extension.json +15 -0
- package/connectors/canva/src/search-filter.ts +23 -0
- package/connectors/canva/src/server.ts +77 -0
- package/connectors/circleci/nimbus.extension.json +15 -0
- package/connectors/circleci/src/server.ts +184 -0
- package/connectors/cloud-logging/nimbus.extension.json +15 -0
- package/connectors/cloud-logging/src/server.ts +6 -0
- package/connectors/cloud-logging/src/tools.ts +115 -0
- package/connectors/cloudwatch/nimbus.extension.json +15 -0
- package/connectors/cloudwatch/src/server.ts +6 -0
- package/connectors/cloudwatch/src/tools.ts +144 -0
- package/connectors/codemagic/nimbus.extension.json +15 -0
- package/connectors/codemagic/src/search-filter.ts +11 -0
- package/connectors/codemagic/src/server.ts +74 -0
- package/connectors/confluence/nimbus.extension.json +15 -0
- package/connectors/confluence/src/kb-append.ts +25 -0
- package/connectors/confluence/src/server.ts +309 -0
- package/connectors/dagster/nimbus.extension.json +16 -0
- package/connectors/dagster/src/search-filter.ts +53 -0
- package/connectors/dagster/src/server.ts +218 -0
- package/connectors/databricks/nimbus.extension.json +16 -0
- package/connectors/databricks/src/search-filter.ts +28 -0
- package/connectors/databricks/src/server.ts +81 -0
- package/connectors/datadog/nimbus.extension.json +15 -0
- package/connectors/datadog/src/server.ts +56 -0
- package/connectors/dataprofile/nimbus.extension.json +16 -0
- package/connectors/dataprofile/src/profile.ts +298 -0
- package/connectors/dataprofile/src/server.ts +6 -0
- package/connectors/dataprofile/src/tools.ts +69 -0
- package/connectors/dbt/nimbus.extension.json +16 -0
- package/connectors/dbt/src/search-filter.ts +28 -0
- package/connectors/dbt/src/server.ts +96 -0
- package/connectors/dependencytrack/nimbus.extension.json +16 -0
- package/connectors/dependencytrack/src/search-filter.ts +24 -0
- package/connectors/dependencytrack/src/server.ts +80 -0
- package/connectors/discord/nimbus.extension.json +15 -0
- package/connectors/discord/src/server.ts +93 -0
- package/connectors/elasticsearch/nimbus.extension.json +16 -0
- package/connectors/elasticsearch/src/server.ts +6 -0
- package/connectors/elasticsearch/src/tools.ts +110 -0
- package/connectors/fastmail/nimbus.extension.json +16 -0
- package/connectors/fastmail/src/jmap-core.ts +86 -0
- package/connectors/fastmail/src/server.ts +194 -0
- package/connectors/fastmail/src/tools.ts +143 -0
- package/connectors/figma/nimbus.extension.json +15 -0
- package/connectors/figma/src/search-filter.ts +24 -0
- package/connectors/figma/src/server.ts +117 -0
- package/connectors/firebase/nimbus.extension.json +15 -0
- package/connectors/firebase/src/search-filter.ts +30 -0
- package/connectors/firebase/src/server.ts +94 -0
- package/connectors/firebase/src/token.ts +41 -0
- package/connectors/flagsmith/nimbus.extension.json +15 -0
- package/connectors/flagsmith/src/search-filter.ts +29 -0
- package/connectors/flagsmith/src/server.ts +102 -0
- package/connectors/flux/nimbus.extension.json +16 -0
- package/connectors/flux/src/search-filter.ts +42 -0
- package/connectors/flux/src/server.ts +182 -0
- package/connectors/gcp/nimbus.extension.json +15 -0
- package/connectors/gcp/src/server.ts +140 -0
- package/connectors/github/nimbus.extension.json +15 -0
- package/connectors/github/src/server.ts +347 -0
- package/connectors/github-actions/nimbus.extension.json +15 -0
- package/connectors/github-actions/src/server.ts +213 -0
- package/connectors/gitlab/nimbus.extension.json +15 -0
- package/connectors/gitlab/src/server.ts +392 -0
- package/connectors/gmail/nimbus.extension.json +15 -0
- package/connectors/gmail/src/server.ts +277 -0
- package/connectors/google-drive/nimbus.extension.json +15 -0
- package/connectors/google-drive/src/drive-query.ts +3 -0
- package/connectors/google-drive/src/server.ts +531 -0
- package/connectors/google-meet/nimbus.extension.json +15 -0
- package/connectors/google-meet/src/server.ts +89 -0
- package/connectors/google-photos/nimbus.extension.json +15 -0
- package/connectors/google-photos/src/server.ts +139 -0
- package/connectors/grafana/nimbus.extension.json +15 -0
- package/connectors/grafana/src/server.ts +61 -0
- package/connectors/great-expectations/nimbus.extension.json +16 -0
- package/connectors/great-expectations/src/gx-parse.ts +292 -0
- package/connectors/great-expectations/src/server.ts +6 -0
- package/connectors/great-expectations/src/tools.ts +91 -0
- package/connectors/greenhouse/nimbus.extension.json +15 -0
- package/connectors/greenhouse/src/search-filter.ts +65 -0
- package/connectors/greenhouse/src/server.ts +67 -0
- package/connectors/hubspot/nimbus.extension.json +15 -0
- package/connectors/hubspot/src/search-filter.ts +28 -0
- package/connectors/hubspot/src/server.ts +81 -0
- package/connectors/iac/nimbus.extension.json +14 -0
- package/connectors/iac/src/server.ts +145 -0
- package/connectors/imap/nimbus.extension.json +16 -0
- package/connectors/imap/src/imap-core.ts +110 -0
- package/connectors/imap/src/server.ts +232 -0
- package/connectors/imap/src/tools.ts +40 -0
- package/connectors/intercom/nimbus.extension.json +15 -0
- package/connectors/intercom/src/search-filter.ts +46 -0
- package/connectors/intercom/src/server.ts +65 -0
- package/connectors/jenkins/nimbus.extension.json +15 -0
- package/connectors/jenkins/src/jenkins-api.ts +156 -0
- package/connectors/jenkins/src/server.ts +223 -0
- package/connectors/jira/nimbus.extension.json +15 -0
- package/connectors/jira/src/server.ts +309 -0
- package/connectors/kubernetes/nimbus.extension.json +15 -0
- package/connectors/kubernetes/src/server.ts +163 -0
- package/connectors/launchdarkly/nimbus.extension.json +15 -0
- package/connectors/launchdarkly/src/search-filter.ts +31 -0
- package/connectors/launchdarkly/src/server.ts +79 -0
- package/connectors/lever/nimbus.extension.json +15 -0
- package/connectors/lever/src/search-filter.ts +35 -0
- package/connectors/lever/src/server.ts +64 -0
- package/connectors/linear/nimbus.extension.json +15 -0
- package/connectors/linear/src/server.ts +467 -0
- package/connectors/localdb/nimbus.extension.json +16 -0
- package/connectors/localdb/src/server.ts +6 -0
- package/connectors/localdb/src/sql-scan.ts +158 -0
- package/connectors/localdb/src/tools.ts +67 -0
- package/connectors/looker/nimbus.extension.json +16 -0
- package/connectors/looker/src/search-filter.ts +22 -0
- package/connectors/looker/src/server.ts +254 -0
- package/connectors/mendeley/nimbus.extension.json +15 -0
- package/connectors/mendeley/src/search-filter.ts +68 -0
- package/connectors/mendeley/src/server.ts +59 -0
- package/connectors/mercury/nimbus.extension.json +15 -0
- package/connectors/mercury/src/search-filter.ts +11 -0
- package/connectors/mercury/src/server.ts +61 -0
- package/connectors/metabase/nimbus.extension.json +16 -0
- package/connectors/metabase/src/search-filter.ts +22 -0
- package/connectors/metabase/src/server.ts +83 -0
- package/connectors/miro/nimbus.extension.json +15 -0
- package/connectors/miro/src/search-filter.ts +24 -0
- package/connectors/miro/src/server.ts +76 -0
- package/connectors/mlflow/nimbus.extension.json +16 -0
- package/connectors/mlflow/src/search-filter.ts +38 -0
- package/connectors/mlflow/src/server.ts +167 -0
- package/connectors/monte-carlo/nimbus.extension.json +16 -0
- package/connectors/monte-carlo/src/search-filter.ts +11 -0
- package/connectors/monte-carlo/src/server.ts +241 -0
- package/connectors/netlify/nimbus.extension.json +15 -0
- package/connectors/netlify/src/search-filter.ts +37 -0
- package/connectors/netlify/src/server.ts +64 -0
- package/connectors/newrelic/nimbus.extension.json +15 -0
- package/connectors/newrelic/src/server.ts +48 -0
- package/connectors/notion/nimbus.extension.json +15 -0
- package/connectors/notion/src/kb-append.ts +23 -0
- package/connectors/notion/src/server.ts +343 -0
- package/connectors/obsidian/nimbus.extension.json +19 -0
- package/connectors/obsidian/src/server.ts +433 -0
- package/connectors/onedrive/nimbus.extension.json +15 -0
- package/connectors/onedrive/src/server.ts +256 -0
- package/connectors/outlook/nimbus.extension.json +15 -0
- package/connectors/outlook/src/server.ts +358 -0
- package/connectors/outlook/src/tool-scope-policy.ts +53 -0
- package/connectors/pagerduty/nimbus.extension.json +15 -0
- package/connectors/pagerduty/src/server.ts +140 -0
- package/connectors/pipedrive/nimbus.extension.json +15 -0
- package/connectors/pipedrive/src/search-filter.ts +11 -0
- package/connectors/pipedrive/src/server.ts +62 -0
- package/connectors/powerbi/nimbus.extension.json +16 -0
- package/connectors/powerbi/src/search-filter.ts +22 -0
- package/connectors/powerbi/src/server.ts +249 -0
- package/connectors/prefect/nimbus.extension.json +16 -0
- package/connectors/prefect/src/search-filter.ts +40 -0
- package/connectors/prefect/src/server.ts +94 -0
- package/connectors/protonmail/nimbus.extension.json +16 -0
- package/connectors/protonmail/src/mail-core.ts +96 -0
- package/connectors/protonmail/src/server.ts +231 -0
- package/connectors/protonmail/src/tools.ts +45 -0
- package/connectors/raindrop/nimbus.extension.json +15 -0
- package/connectors/raindrop/src/search-filter.ts +20 -0
- package/connectors/raindrop/src/server.ts +107 -0
- package/connectors/ramp/nimbus.extension.json +15 -0
- package/connectors/ramp/src/search-filter.ts +37 -0
- package/connectors/ramp/src/server.ts +109 -0
- package/connectors/readwise/nimbus.extension.json +15 -0
- package/connectors/readwise/src/search-filter.ts +64 -0
- package/connectors/readwise/src/server.ts +92 -0
- package/connectors/sagemaker/nimbus.extension.json +15 -0
- package/connectors/sagemaker/src/server.ts +6 -0
- package/connectors/sagemaker/src/tools.ts +121 -0
- package/connectors/salesforce/nimbus.extension.json +15 -0
- package/connectors/salesforce/src/search-filter.ts +23 -0
- package/connectors/salesforce/src/server.ts +94 -0
- package/connectors/semgrep/nimbus.extension.json +15 -0
- package/connectors/semgrep/src/search-filter.ts +31 -0
- package/connectors/semgrep/src/server.ts +108 -0
- package/connectors/sentry/nimbus.extension.json +15 -0
- package/connectors/sentry/src/server.ts +66 -0
- package/connectors/slack/nimbus.extension.json +15 -0
- package/connectors/slack/src/server.ts +360 -0
- package/connectors/snowflake/nimbus.extension.json +16 -0
- package/connectors/snowflake/src/search-filter.ts +26 -0
- package/connectors/snowflake/src/server.ts +236 -0
- package/connectors/snyk/nimbus.extension.json +15 -0
- package/connectors/snyk/src/search-filter.ts +28 -0
- package/connectors/snyk/src/server.ts +144 -0
- package/connectors/sonarqube/nimbus.extension.json +15 -0
- package/connectors/sonarqube/src/search-filter.ts +28 -0
- package/connectors/sonarqube/src/server.ts +115 -0
- package/connectors/stackoverflow/nimbus.extension.json +15 -0
- package/connectors/stackoverflow/src/search-filter.ts +52 -0
- package/connectors/stackoverflow/src/server.ts +81 -0
- package/connectors/storybook/nimbus.extension.json +16 -0
- package/connectors/storybook/src/server.ts +6 -0
- package/connectors/storybook/src/storybook-parse.ts +67 -0
- package/connectors/storybook/src/tools.ts +68 -0
- package/connectors/stripe/nimbus.extension.json +15 -0
- package/connectors/stripe/src/search-filter.ts +19 -0
- package/connectors/stripe/src/server.ts +65 -0
- package/connectors/superset/nimbus.extension.json +16 -0
- package/connectors/superset/src/search-filter.ts +22 -0
- package/connectors/superset/src/server.ts +119 -0
- package/connectors/tableau/nimbus.extension.json +16 -0
- package/connectors/tableau/src/search-filter.ts +22 -0
- package/connectors/tableau/src/server.ts +248 -0
- package/connectors/teams/nimbus.extension.json +20 -0
- package/connectors/teams/src/bot-send.ts +72 -0
- package/connectors/teams/src/server.ts +257 -0
- package/connectors/testflight/nimbus.extension.json +15 -0
- package/connectors/testflight/src/jwt.ts +27 -0
- package/connectors/testflight/src/search-filter.ts +30 -0
- package/connectors/testflight/src/server.ts +74 -0
- package/connectors/vercel/nimbus.extension.json +15 -0
- package/connectors/vercel/src/search-filter.ts +34 -0
- package/connectors/vercel/src/server.ts +79 -0
- package/connectors/vertex-ai/nimbus.extension.json +15 -0
- package/connectors/vertex-ai/src/server.ts +6 -0
- package/connectors/vertex-ai/src/tools.ts +145 -0
- package/connectors/wiz/nimbus.extension.json +15 -0
- package/connectors/wiz/src/search-filter.ts +50 -0
- package/connectors/wiz/src/server.ts +192 -0
- package/connectors/workday/nimbus.extension.json +19 -0
- package/connectors/workday/src/search-filter.ts +5 -0
- package/connectors/workday/src/server.ts +78 -0
- package/connectors/workday/src/tools.ts +1 -0
- package/connectors/zendesk/nimbus.extension.json +16 -0
- package/connectors/zendesk/src/search-filter.ts +11 -0
- package/connectors/zendesk/src/server.ts +77 -0
- package/connectors/zoom/nimbus.extension.json +15 -0
- package/connectors/zoom/src/encode-meeting-path.ts +5 -0
- package/connectors/zoom/src/recordings-window.ts +34 -0
- package/connectors/zoom/src/search-filter.ts +42 -0
- package/connectors/zoom/src/server.ts +98 -0
- package/connectors/zotero/nimbus.extension.json +15 -0
- package/connectors/zotero/src/search-filter.ts +81 -0
- package/connectors/zotero/src/server.ts +82 -0
- package/package.json +152 -0
- package/shared/atlassian-json-fetch.ts +42 -0
- package/shared/audit-chain.ts +133 -0
- package/shared/connector-mode.ts +60 -0
- package/shared/consent-kit.ts +323 -0
- package/shared/fetch-bearer-json.ts +18 -0
- package/shared/header-safe.ts +28 -0
- package/shared/imap-bodystructure.ts +113 -0
- package/shared/imap-mail-core.ts +57 -0
- package/shared/imap-tool-kit.ts +293 -0
- package/shared/join-api-path.ts +18 -0
- package/shared/kb-markdown.ts +142 -0
- package/shared/mcp-search-tool.ts +37 -0
- package/shared/mcp-tool-kit.ts +35 -0
- package/shared/nimbus-spawn.ts +128 -0
- package/shared/rest-tool-kit.ts +18 -0
- package/shared/run-cli-json.ts +55 -0
- package/shared/run-read-only-mcp-connector.ts +59 -0
- package/shared/safe-cli-arg.ts +55 -0
- package/shared/search-filter.ts +140 -0
- package/shared/strip-trailing-slashes.ts +7 -0
- package/shared/write-scope.ts +58 -0
- package/standalone/src/bin.ts +19 -0
- package/standalone/src/launcher.ts +186 -0
- package/standalone/tsconfig.json +8 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { stringField } from "../../../shared/search-filter.ts";
|
|
2
|
+
|
|
3
|
+
export interface ZoomSearchOptions {
|
|
4
|
+
readonly query: string;
|
|
5
|
+
readonly limit?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function asRecord(v: unknown): Record<string, unknown> | undefined {
|
|
9
|
+
return v !== null && typeof v === "object" && !Array.isArray(v)
|
|
10
|
+
? (v as Record<string, unknown>)
|
|
11
|
+
: undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function filterZoomMeetings(
|
|
15
|
+
meetings: readonly unknown[],
|
|
16
|
+
options: ZoomSearchOptions,
|
|
17
|
+
): unknown[] {
|
|
18
|
+
const q = options.query.trim().toLowerCase();
|
|
19
|
+
if (q === "") {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const limit = options.limit ?? 50;
|
|
23
|
+
if (limit <= 0) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const matches: unknown[] = [];
|
|
27
|
+
for (const m of meetings) {
|
|
28
|
+
const row = asRecord(m);
|
|
29
|
+
if (row === undefined) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const haystack =
|
|
33
|
+
`${stringField(row, "topic")} ${stringField(row, "agenda")} ${stringField(row, "host_id")}`.toLowerCase();
|
|
34
|
+
if (haystack.includes(q)) {
|
|
35
|
+
matches.push(m);
|
|
36
|
+
if (matches.length >= limit) {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return matches;
|
|
42
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
|
|
3
|
+
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
|
+
}
|
|
30
|
+
|
|
31
|
+
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
|
+
);
|
|
98
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "com.nimbus.zotero",
|
|
3
|
+
"displayName": "Zotero",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Zotero connector (read-only). Surfaces the user's bibliographic references from a Zotero library (`users/<id>` or `groups/<id>`) as `zotero:reference` items in the local index — the title, the formatted creator/author list, the publication date, the item type (journalArticle / book / conferencePaper / …), tags, collections, DOI, source URL, and a truncated abstract. Useful for answering research questions (\"what did I save about retrieval-augmented generation?\", \"find my reference with DOI 10.1145/…\") without leaving Nimbus. Two-identifier auth (mirrors the Stack Overflow shape): `Zotero-API-Key: <key>` header (the secret) plus a non-secret `zotero.library` spec selecting the library path. The SaaS host (api.zotero.org) is fixed; Zotero-API-Version is pinned to 3. The Zotero Web API v3 `/<library>/items` endpoint paginates by `start`/`limit` (limit max 100, `sort=dateModified&direction=desc`); the connector walks a single forward pass per cycle, page-capped. Attachments and standalone notes are skipped — v1 indexes top-level bibliographic references only.",
|
|
6
|
+
"author": "Nimbus",
|
|
7
|
+
"entrypoint": "dist/server.js",
|
|
8
|
+
"runtime": "bun",
|
|
9
|
+
"permissions": {
|
|
10
|
+
"network": ["api.zotero.org"]
|
|
11
|
+
},
|
|
12
|
+
"hitlRequired": [],
|
|
13
|
+
"syncInterval": 600,
|
|
14
|
+
"minNimbusVersion": "0.2.0"
|
|
15
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asObjectish,
|
|
3
|
+
makeQueryFilter,
|
|
4
|
+
type SearchMatchOptions,
|
|
5
|
+
stringField,
|
|
6
|
+
} from "../../../shared/search-filter.ts";
|
|
7
|
+
|
|
8
|
+
export type ZoteroSearchMatchOptions = SearchMatchOptions;
|
|
9
|
+
|
|
10
|
+
function nonEmptyString(v: unknown): string | null {
|
|
11
|
+
return typeof v === "string" && v !== "" ? v : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function creatorName(c: unknown): string | null {
|
|
15
|
+
if (c === null || typeof c !== "object") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const row = c as Record<string, unknown>;
|
|
19
|
+
const name = nonEmptyString(row["name"]);
|
|
20
|
+
if (name !== null) {
|
|
21
|
+
return name;
|
|
22
|
+
}
|
|
23
|
+
const parts = [nonEmptyString(row["firstName"]), nonEmptyString(row["lastName"])].filter(
|
|
24
|
+
(p): p is string => p !== null,
|
|
25
|
+
);
|
|
26
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function creatorNames(data: Record<string, unknown>): string {
|
|
30
|
+
const creators = data["creators"];
|
|
31
|
+
if (!Array.isArray(creators)) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
const names: string[] = [];
|
|
35
|
+
for (const c of creators) {
|
|
36
|
+
const name = creatorName(c);
|
|
37
|
+
if (name !== null) {
|
|
38
|
+
names.push(name);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return names.join(" ");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function tagNames(data: Record<string, unknown>): string {
|
|
45
|
+
const tags = data["tags"];
|
|
46
|
+
if (!Array.isArray(tags)) {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
const names: string[] = [];
|
|
50
|
+
for (const t of tags) {
|
|
51
|
+
if (t !== null && typeof t === "object") {
|
|
52
|
+
const tag = (t as Record<string, unknown>)["tag"];
|
|
53
|
+
if (typeof tag === "string") {
|
|
54
|
+
names.push(tag);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return names.join(" ");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fieldsOf(item: unknown): readonly string[] | null {
|
|
62
|
+
const row = asObjectish(item);
|
|
63
|
+
if (row === undefined) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const data = asObjectish(row["data"]);
|
|
67
|
+
if (data === undefined) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return [
|
|
71
|
+
stringField(data, "title"),
|
|
72
|
+
stringField(data, "itemType"),
|
|
73
|
+
stringField(data, "abstractNote"),
|
|
74
|
+
stringField(data, "DOI"),
|
|
75
|
+
stringField(data, "publicationTitle"),
|
|
76
|
+
creatorNames(data),
|
|
77
|
+
tagNames(data),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const filterZoteroItems = makeQueryFilter(fieldsOf);
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
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
|
+
}
|
|
45
|
+
|
|
46
|
+
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
|
+
);
|
|
82
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nimbus-dev/connectors",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"license": "AGPL-3.0-only",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"description": "Nimbus first-party MCP connectors, runnable standalone by any MCP client.",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/nimbus-agent/nimbus-mcp-servers.git"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"nimbus-connector": "./standalone/src/bin.ts"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"connectors/*/src/**",
|
|
19
|
+
"connectors/*/nimbus.extension.json",
|
|
20
|
+
"shared/**",
|
|
21
|
+
"standalone/**",
|
|
22
|
+
"NOTICE",
|
|
23
|
+
"README.md",
|
|
24
|
+
"!standalone/package.json",
|
|
25
|
+
"!**/*.test.ts"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
29
|
+
"@nimbus-dev/sdk": "^1.20.0",
|
|
30
|
+
"zod": "^4.4.2"
|
|
31
|
+
},
|
|
32
|
+
"exports": {
|
|
33
|
+
".": "./standalone/src/bin.ts",
|
|
34
|
+
"./airflow": "./connectors/airflow/src/server.ts",
|
|
35
|
+
"./apple": "./connectors/apple/src/server.ts",
|
|
36
|
+
"./argocd": "./connectors/argocd/src/server.ts",
|
|
37
|
+
"./athena": "./connectors/athena/src/server.ts",
|
|
38
|
+
"./aws": "./connectors/aws/src/server.ts",
|
|
39
|
+
"./azure": "./connectors/azure/src/server.ts",
|
|
40
|
+
"./bigeye": "./connectors/bigeye/src/server.ts",
|
|
41
|
+
"./bigquery": "./connectors/bigquery/src/server.ts",
|
|
42
|
+
"./bitbucket": "./connectors/bitbucket/src/server.ts",
|
|
43
|
+
"./bitrise": "./connectors/bitrise/src/server.ts",
|
|
44
|
+
"./canva": "./connectors/canva/src/server.ts",
|
|
45
|
+
"./circleci": "./connectors/circleci/src/server.ts",
|
|
46
|
+
"./cloud-logging": "./connectors/cloud-logging/src/server.ts",
|
|
47
|
+
"./cloudwatch": "./connectors/cloudwatch/src/server.ts",
|
|
48
|
+
"./codemagic": "./connectors/codemagic/src/server.ts",
|
|
49
|
+
"./confluence": "./connectors/confluence/src/server.ts",
|
|
50
|
+
"./dagster": "./connectors/dagster/src/server.ts",
|
|
51
|
+
"./databricks": "./connectors/databricks/src/server.ts",
|
|
52
|
+
"./datadog": "./connectors/datadog/src/server.ts",
|
|
53
|
+
"./dataprofile": "./connectors/dataprofile/src/server.ts",
|
|
54
|
+
"./dbt": "./connectors/dbt/src/server.ts",
|
|
55
|
+
"./dependencytrack": "./connectors/dependencytrack/src/server.ts",
|
|
56
|
+
"./discord": "./connectors/discord/src/server.ts",
|
|
57
|
+
"./elasticsearch": "./connectors/elasticsearch/src/server.ts",
|
|
58
|
+
"./fastmail": "./connectors/fastmail/src/server.ts",
|
|
59
|
+
"./figma": "./connectors/figma/src/server.ts",
|
|
60
|
+
"./firebase": "./connectors/firebase/src/server.ts",
|
|
61
|
+
"./flagsmith": "./connectors/flagsmith/src/server.ts",
|
|
62
|
+
"./flux": "./connectors/flux/src/server.ts",
|
|
63
|
+
"./gcp": "./connectors/gcp/src/server.ts",
|
|
64
|
+
"./github": "./connectors/github/src/server.ts",
|
|
65
|
+
"./github-actions": "./connectors/github-actions/src/server.ts",
|
|
66
|
+
"./gitlab": "./connectors/gitlab/src/server.ts",
|
|
67
|
+
"./gmail": "./connectors/gmail/src/server.ts",
|
|
68
|
+
"./google-drive": "./connectors/google-drive/src/server.ts",
|
|
69
|
+
"./google-meet": "./connectors/google-meet/src/server.ts",
|
|
70
|
+
"./google-photos": "./connectors/google-photos/src/server.ts",
|
|
71
|
+
"./grafana": "./connectors/grafana/src/server.ts",
|
|
72
|
+
"./great-expectations": "./connectors/great-expectations/src/server.ts",
|
|
73
|
+
"./greenhouse": "./connectors/greenhouse/src/server.ts",
|
|
74
|
+
"./hubspot": "./connectors/hubspot/src/server.ts",
|
|
75
|
+
"./iac": "./connectors/iac/src/server.ts",
|
|
76
|
+
"./imap": "./connectors/imap/src/server.ts",
|
|
77
|
+
"./intercom": "./connectors/intercom/src/server.ts",
|
|
78
|
+
"./jenkins": "./connectors/jenkins/src/server.ts",
|
|
79
|
+
"./jira": "./connectors/jira/src/server.ts",
|
|
80
|
+
"./kubernetes": "./connectors/kubernetes/src/server.ts",
|
|
81
|
+
"./launchdarkly": "./connectors/launchdarkly/src/server.ts",
|
|
82
|
+
"./lever": "./connectors/lever/src/server.ts",
|
|
83
|
+
"./linear": "./connectors/linear/src/server.ts",
|
|
84
|
+
"./localdb": "./connectors/localdb/src/server.ts",
|
|
85
|
+
"./looker": "./connectors/looker/src/server.ts",
|
|
86
|
+
"./mendeley": "./connectors/mendeley/src/server.ts",
|
|
87
|
+
"./mercury": "./connectors/mercury/src/server.ts",
|
|
88
|
+
"./metabase": "./connectors/metabase/src/server.ts",
|
|
89
|
+
"./miro": "./connectors/miro/src/server.ts",
|
|
90
|
+
"./mlflow": "./connectors/mlflow/src/server.ts",
|
|
91
|
+
"./monte-carlo": "./connectors/monte-carlo/src/server.ts",
|
|
92
|
+
"./netlify": "./connectors/netlify/src/server.ts",
|
|
93
|
+
"./newrelic": "./connectors/newrelic/src/server.ts",
|
|
94
|
+
"./notion": "./connectors/notion/src/server.ts",
|
|
95
|
+
"./obsidian": "./connectors/obsidian/src/server.ts",
|
|
96
|
+
"./onedrive": "./connectors/onedrive/src/server.ts",
|
|
97
|
+
"./outlook": "./connectors/outlook/src/server.ts",
|
|
98
|
+
"./pagerduty": "./connectors/pagerduty/src/server.ts",
|
|
99
|
+
"./pipedrive": "./connectors/pipedrive/src/server.ts",
|
|
100
|
+
"./powerbi": "./connectors/powerbi/src/server.ts",
|
|
101
|
+
"./prefect": "./connectors/prefect/src/server.ts",
|
|
102
|
+
"./protonmail": "./connectors/protonmail/src/server.ts",
|
|
103
|
+
"./raindrop": "./connectors/raindrop/src/server.ts",
|
|
104
|
+
"./ramp": "./connectors/ramp/src/server.ts",
|
|
105
|
+
"./readwise": "./connectors/readwise/src/server.ts",
|
|
106
|
+
"./sagemaker": "./connectors/sagemaker/src/server.ts",
|
|
107
|
+
"./salesforce": "./connectors/salesforce/src/server.ts",
|
|
108
|
+
"./semgrep": "./connectors/semgrep/src/server.ts",
|
|
109
|
+
"./sentry": "./connectors/sentry/src/server.ts",
|
|
110
|
+
"./slack": "./connectors/slack/src/server.ts",
|
|
111
|
+
"./snowflake": "./connectors/snowflake/src/server.ts",
|
|
112
|
+
"./snyk": "./connectors/snyk/src/server.ts",
|
|
113
|
+
"./sonarqube": "./connectors/sonarqube/src/server.ts",
|
|
114
|
+
"./stackoverflow": "./connectors/stackoverflow/src/server.ts",
|
|
115
|
+
"./storybook": "./connectors/storybook/src/server.ts",
|
|
116
|
+
"./stripe": "./connectors/stripe/src/server.ts",
|
|
117
|
+
"./superset": "./connectors/superset/src/server.ts",
|
|
118
|
+
"./tableau": "./connectors/tableau/src/server.ts",
|
|
119
|
+
"./teams": "./connectors/teams/src/server.ts",
|
|
120
|
+
"./testflight": "./connectors/testflight/src/server.ts",
|
|
121
|
+
"./vercel": "./connectors/vercel/src/server.ts",
|
|
122
|
+
"./vertex-ai": "./connectors/vertex-ai/src/server.ts",
|
|
123
|
+
"./wiz": "./connectors/wiz/src/server.ts",
|
|
124
|
+
"./workday": "./connectors/workday/src/server.ts",
|
|
125
|
+
"./zendesk": "./connectors/zendesk/src/server.ts",
|
|
126
|
+
"./zoom": "./connectors/zoom/src/server.ts",
|
|
127
|
+
"./zotero": "./connectors/zotero/src/server.ts"
|
|
128
|
+
},
|
|
129
|
+
"scripts": {
|
|
130
|
+
"test": "bun test",
|
|
131
|
+
"lint": "biome check --error-on-warnings .",
|
|
132
|
+
"lint:fix": "biome check --write .",
|
|
133
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
134
|
+
"audit:connector-consent": "bun scripts/check-connector-consent.ts",
|
|
135
|
+
"check": "bun run lint && bun run typecheck && bun run audit:connector-consent && bun run test"
|
|
136
|
+
},
|
|
137
|
+
"devDependencies": {
|
|
138
|
+
"@biomejs/biome": "^2.5.10",
|
|
139
|
+
"@types/bun": "^1.4.0",
|
|
140
|
+
"@types/nodemailer": "^7.0.4",
|
|
141
|
+
"typescript": "^7.0.2"
|
|
142
|
+
},
|
|
143
|
+
"engines": {
|
|
144
|
+
"bun": ">=1.2.0"
|
|
145
|
+
},
|
|
146
|
+
"optionalDependencies": {
|
|
147
|
+
"hyparquet": "^1.29.1",
|
|
148
|
+
"imapflow": "^1.7.2",
|
|
149
|
+
"nodemailer": "^9.0.5",
|
|
150
|
+
"tsdav": "^2.3.1"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { encodeBasicAuthHeader } from "./mcp-tool-kit.ts";
|
|
2
|
+
import { stripTrailingSlashes } from "./strip-trailing-slashes.ts";
|
|
3
|
+
|
|
4
|
+
export function normalizeRequiredSiteBaseUrl(raw: string, emptyMessage: string): string {
|
|
5
|
+
const t = stripTrailingSlashes(raw);
|
|
6
|
+
if (t === "") {
|
|
7
|
+
throw new Error(emptyMessage);
|
|
8
|
+
}
|
|
9
|
+
return t.startsWith("http") ? t : `https://${t}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function requireTrimmedEnv(name: string, notSetMessage: string): string {
|
|
13
|
+
const v = process.env[name];
|
|
14
|
+
if (v === undefined || v.trim() === "") {
|
|
15
|
+
throw new Error(notSetMessage);
|
|
16
|
+
}
|
|
17
|
+
return v.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function fetchAtlassianBasicAuthJsonText(
|
|
21
|
+
url: string,
|
|
22
|
+
email: string,
|
|
23
|
+
token: string,
|
|
24
|
+
init?: RequestInit,
|
|
25
|
+
): 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 };
|
|
42
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { appendFile, readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What happened to one gated action.
|
|
6
|
+
*
|
|
7
|
+
* `refused` is a server-side denial (out of scope, budget exhausted, or no elicitation-capable
|
|
8
|
+
* client); `declined` is a human saying no. They are kept distinct because only one of them means a
|
|
9
|
+
* person was actually asked.
|
|
10
|
+
*/
|
|
11
|
+
export type AuditOutcome =
|
|
12
|
+
| "requested"
|
|
13
|
+
| "accepted"
|
|
14
|
+
| "declined"
|
|
15
|
+
| "refused"
|
|
16
|
+
| "executed"
|
|
17
|
+
| "failed";
|
|
18
|
+
|
|
19
|
+
export type AuditEntry = {
|
|
20
|
+
readonly ts: string;
|
|
21
|
+
readonly connector: string;
|
|
22
|
+
readonly tool: string;
|
|
23
|
+
readonly outcome: AuditOutcome;
|
|
24
|
+
/** Free-form per-outcome detail: resolved params, refusal reason, captured pre-state. */
|
|
25
|
+
readonly detail: Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type ChainedLine = {
|
|
29
|
+
readonly seq: number;
|
|
30
|
+
readonly prev: string;
|
|
31
|
+
readonly hash: string;
|
|
32
|
+
readonly entry: AuditEntry;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** First link's predecessor. A fixed, all-zero digest, mirroring the gateway's audit chain. */
|
|
36
|
+
export const GENESIS_HASH = "0".repeat(64);
|
|
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
|
+
/**
|
|
45
|
+
* Key order for `canonicalJson`. Plain code-unit comparison, NOT `localeCompare`: the order has to
|
|
46
|
+
* be the same on every machine and locale, or a chain written on one host fails to verify on
|
|
47
|
+
* another.
|
|
48
|
+
*/
|
|
49
|
+
function compareKeys(a: string, b: string): number {
|
|
50
|
+
if (a < b) return -1;
|
|
51
|
+
if (a > b) return 1;
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function canonicalJson(value: unknown): string {
|
|
56
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
57
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
58
|
+
const rec = value as Record<string, unknown>;
|
|
59
|
+
const body = Object.keys(rec)
|
|
60
|
+
.sort(compareKeys)
|
|
61
|
+
.map((k) => `${JSON.stringify(k)}:${canonicalJson(rec[k])}`)
|
|
62
|
+
.join(",");
|
|
63
|
+
return `{${body}}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Hash over the predecessor plus the entry's canonical JSON.
|
|
68
|
+
*
|
|
69
|
+
* SHA-256 via `node:crypto`, deliberately NOT the gateway's BLAKE3: `@noble/hashes` is not in
|
|
70
|
+
* `ALLOWED_CONNECTOR_DEPS`, and the standalone artifact must run under Node. Same construction,
|
|
71
|
+
* different primitive.
|
|
72
|
+
*/
|
|
73
|
+
function linkHash(prev: string, entry: AuditEntry): string {
|
|
74
|
+
return createHash("sha256").update(prev).update(canonicalJson(entry)).digest("hex");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Constant-time digest comparison (I10). A length mismatch is a mismatch, checked first. */
|
|
78
|
+
function hashEquals(a: string, b: string): boolean {
|
|
79
|
+
const ab = Buffer.from(a, "utf8");
|
|
80
|
+
const bb = Buffer.from(b, "utf8");
|
|
81
|
+
return ab.length === bb.length && timingSafeEqual(ab, bb);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function readLines(path: string): Promise<string[]> {
|
|
85
|
+
let text: string;
|
|
86
|
+
try {
|
|
87
|
+
text = await readFile(path, "utf8");
|
|
88
|
+
} catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const trimmed = text.trimEnd();
|
|
92
|
+
return trimmed === "" ? [] : trimmed.split("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Append one entry, linked to the current tail. */
|
|
96
|
+
export async function appendAuditEntry(path: string, entry: AuditEntry): Promise<void> {
|
|
97
|
+
const lines = await readLines(path);
|
|
98
|
+
const last = lines.at(-1);
|
|
99
|
+
const prev = last === undefined ? GENESIS_HASH : (JSON.parse(last) as ChainedLine).hash;
|
|
100
|
+
const line: ChainedLine = {
|
|
101
|
+
seq: lines.length + 1,
|
|
102
|
+
prev,
|
|
103
|
+
hash: linkHash(prev, entry),
|
|
104
|
+
entry,
|
|
105
|
+
};
|
|
106
|
+
await appendFile(path, `${JSON.stringify(line)}\n`, "utf8");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Walk the chain.
|
|
111
|
+
*
|
|
112
|
+
* Returns the 1-based line where the links first stop agreeing, which covers both a tampered entry
|
|
113
|
+
* and a deleted one — a deletion breaks the successor's `prev` link.
|
|
114
|
+
*/
|
|
115
|
+
export async function verifyAuditChain(
|
|
116
|
+
path: string,
|
|
117
|
+
): Promise<{ ok: true; count: number } | { ok: false; brokenAtLine: number }> {
|
|
118
|
+
const lines = await readLines(path);
|
|
119
|
+
let prev = GENESIS_HASH;
|
|
120
|
+
for (const [i, raw] of lines.entries()) {
|
|
121
|
+
let line: ChainedLine;
|
|
122
|
+
try {
|
|
123
|
+
line = JSON.parse(raw) as ChainedLine;
|
|
124
|
+
} catch {
|
|
125
|
+
return { ok: false, brokenAtLine: i + 1 };
|
|
126
|
+
}
|
|
127
|
+
if (!hashEquals(line.prev, prev) || !hashEquals(line.hash, linkHash(prev, line.entry))) {
|
|
128
|
+
return { ok: false, brokenAtLine: i + 1 };
|
|
129
|
+
}
|
|
130
|
+
prev = line.hash;
|
|
131
|
+
}
|
|
132
|
+
return { ok: true, count: lines.length };
|
|
133
|
+
}
|