@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
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { mintGoogleAccessToken } from "@nimbus-dev/sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { matchesResult } 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 { filterFirebaseReleases } from "./search-filter.ts";
|
|
7
|
+
import { configuredAppIds, projectNumberFromAppId, serviceAccountFromEnv } from "./token.ts";
|
|
8
|
+
|
|
9
|
+
const APP_DISTRIBUTION_API = "https://firebaseappdistribution.googleapis.com";
|
|
10
|
+
|
|
11
|
+
// App Distribution OAuth2 tokens last ~1h; cache and refresh conservatively.
|
|
12
|
+
let cachedToken: { token: string; expiresAt: number } | null = null;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Forget the cached access token. Tests only: the cache is module state shared by every test in
|
|
16
|
+
* the process, and a token one test left behind lets a later tool call skip reading its
|
|
17
|
+
* credential entirely.
|
|
18
|
+
*/
|
|
19
|
+
export function __resetFirebaseTokenCacheForTests(): void {
|
|
20
|
+
cachedToken = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function accessToken(): Promise<string> {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
if (cachedToken !== null && cachedToken.expiresAt > now + 60_000) {
|
|
26
|
+
return cachedToken.token;
|
|
27
|
+
}
|
|
28
|
+
const token = await mintGoogleAccessToken(serviceAccountFromEnv());
|
|
29
|
+
if (token === null) {
|
|
30
|
+
throw new Error("failed to mint a Firebase access token");
|
|
31
|
+
}
|
|
32
|
+
cachedToken = { token, expiresAt: now + 30 * 60 * 1000 };
|
|
33
|
+
return token;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function appDistGet(path: string): Promise<unknown> {
|
|
37
|
+
const res = await fetch(`${APP_DISTRIBUTION_API}${path}`, {
|
|
38
|
+
headers: { Authorization: `Bearer ${await accessToken()}`, Accept: "application/json" },
|
|
39
|
+
});
|
|
40
|
+
const text = await res.text();
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
throw new Error(`Firebase App Distribution ${String(res.status)}: ${text.slice(0, 400)}`);
|
|
43
|
+
}
|
|
44
|
+
return JSON.parse(text) as unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function appBasePath(appId: string): string {
|
|
48
|
+
const projectNumber = projectNumberFromAppId(appId);
|
|
49
|
+
if (projectNumber === null) {
|
|
50
|
+
throw new Error(`cannot derive a project number from app id: ${appId}`);
|
|
51
|
+
}
|
|
52
|
+
return `/v1/projects/${encodeURIComponent(projectNumber)}/apps/${encodeURIComponent(appId)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function releasesPath(appId: string, pageSize: number): string {
|
|
56
|
+
return `${appBasePath(appId)}/releases?pageSize=${String(pageSize)}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function releasePath(appId: string, releaseId: string): string {
|
|
60
|
+
return `${appBasePath(appId)}/releases/${encodeURIComponent(releaseId)}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Tool names exposed by this connector — for contract/introspection tests. */
|
|
64
|
+
export const FIREBASE_TOOL_NAMES = ["firebase_get", "firebase_list", "firebase_search"] as const;
|
|
65
|
+
|
|
66
|
+
export function registerFirebaseTools(reg: ZodToolRegistrar): void {
|
|
67
|
+
reg(
|
|
68
|
+
"firebase_list",
|
|
69
|
+
"List Firebase App Distribution resources. When `appId` is omitted, returns the connector's configured app ids. When `appId` is provided, returns that app's most-recent releases via GET /v1/projects/<n>/apps/<appId>/releases (pageSize optional, default 50).",
|
|
70
|
+
z.object({
|
|
71
|
+
appId: z.string().min(1).optional(),
|
|
72
|
+
pageSize: z.number().int().min(1).max(100).optional(),
|
|
73
|
+
}),
|
|
74
|
+
async (p) => {
|
|
75
|
+
if (p.appId === undefined) {
|
|
76
|
+
return jsonResult({ appIds: configuredAppIds() });
|
|
77
|
+
}
|
|
78
|
+
return jsonResult(await appDistGet(releasesPath(p.appId, p.pageSize ?? 50)));
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
reg(
|
|
83
|
+
"firebase_get",
|
|
84
|
+
"Fetch a single Firebase App Distribution release via GET /v1/projects/<n>/apps/<appId>/releases/<releaseId>.",
|
|
85
|
+
z.object({
|
|
86
|
+
appId: z.string().min(1),
|
|
87
|
+
releaseId: z.string().min(1),
|
|
88
|
+
}),
|
|
89
|
+
async (p) => jsonResult(await appDistGet(releasePath(p.appId, p.releaseId))),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
reg(
|
|
93
|
+
"firebase_search",
|
|
94
|
+
"Substring search across an app's recent releases. Matches the query against each release's display version, build version, release notes, and resource name (case-insensitive). Returns a `{ matches: [...] }` envelope.",
|
|
95
|
+
z.object({
|
|
96
|
+
appId: z.string().min(1),
|
|
97
|
+
query: z.string().min(1),
|
|
98
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
99
|
+
}),
|
|
100
|
+
async (p) => {
|
|
101
|
+
const root = await appDistGet(releasesPath(p.appId, 100));
|
|
102
|
+
const releases = (root as { releases?: unknown[] } | null)?.releases;
|
|
103
|
+
return matchesResult(releases, filterFirebaseReleases, p);
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -1,102 +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
|
-
|
|
6
|
-
const DEFAULT_API_BASE = "https://api.flagsmith.com";
|
|
7
|
-
|
|
8
|
-
function apiBase(): string {
|
|
9
|
-
const v = process.env["FLAGSMITH_API_BASE"]?.trim();
|
|
10
|
-
return v === undefined || v === "" ? DEFAULT_API_BASE : v;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function authHeader(): Record<string, string> {
|
|
14
|
-
const t = process.env["FLAGSMITH_TOKEN"]?.trim();
|
|
15
|
-
if (t === undefined || t === "") {
|
|
16
|
-
throw new Error("FLAGSMITH_TOKEN is not set");
|
|
17
|
-
}
|
|
18
|
-
return { Authorization: `Token ${t}`, Accept: "application/json" };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function fsGet(path: string): Promise<unknown> {
|
|
22
|
-
const res = await fetch(`${apiBase()}/api/v1${path}`, { headers: authHeader() });
|
|
23
|
-
const text = await res.text();
|
|
24
|
-
if (!res.ok) {
|
|
25
|
-
throw new Error(`Flagsmith ${String(res.status)}: ${text.slice(0, 400)}`);
|
|
26
|
-
}
|
|
27
|
-
return JSON.parse(text) as unknown;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function featuresFromPage(root: unknown): unknown[] {
|
|
31
|
-
if (Array.isArray(root)) {
|
|
32
|
-
return root;
|
|
33
|
-
}
|
|
34
|
-
const results = (root as { results?: unknown } | null)?.results;
|
|
35
|
-
return Array.isArray(results) ? results : [];
|
|
36
|
-
}
|
|
2
|
+
import { registerFlagsmithTools } from "./tools.ts";
|
|
37
3
|
|
|
38
4
|
await runReadOnlyMcpConnector("nimbus-flagsmith", (reg) => {
|
|
39
|
-
reg
|
|
40
|
-
"flagsmith_list",
|
|
41
|
-
"List Flagsmith projects, or feature flags for a project. Without `projectId`, returns the account's projects (`/projects/`). With `projectId`, returns that project's feature flags (`/projects/{projectId}/features/`), capped at `limit` (default 100).",
|
|
42
|
-
z.object({
|
|
43
|
-
projectId: z.number().int().optional(),
|
|
44
|
-
limit: z.number().int().min(1).max(500).optional(),
|
|
45
|
-
}),
|
|
46
|
-
async (p) => {
|
|
47
|
-
if (p.projectId === undefined) {
|
|
48
|
-
return jsonResult(await fsGet("/projects/"));
|
|
49
|
-
}
|
|
50
|
-
const search = new URLSearchParams({ page_size: String(p.limit ?? 100) });
|
|
51
|
-
return jsonResult(
|
|
52
|
-
await fsGet(
|
|
53
|
-
`/projects/${encodeURIComponent(String(p.projectId))}/features/?${search.toString()}`,
|
|
54
|
-
),
|
|
55
|
-
);
|
|
56
|
-
},
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
reg(
|
|
60
|
-
"flagsmith_get",
|
|
61
|
-
"Fetch one Flagsmith feature flag by project id + feature name. Flagsmith has no GET-by-name endpoint, so this searches the project's features and narrows to the exact-name match. Throws when no match is found.",
|
|
62
|
-
z.object({
|
|
63
|
-
projectId: z.number().int(),
|
|
64
|
-
featureName: z.string().min(1),
|
|
65
|
-
}),
|
|
66
|
-
async (p) => {
|
|
67
|
-
const search = new URLSearchParams({ page_size: "100", search: p.featureName });
|
|
68
|
-
const root = await fsGet(
|
|
69
|
-
`/projects/${encodeURIComponent(String(p.projectId))}/features/?${search.toString()}`,
|
|
70
|
-
);
|
|
71
|
-
const match = featuresFromPage(root).find(
|
|
72
|
-
(f) =>
|
|
73
|
-
f !== null && typeof f === "object" && (f as { name?: unknown }).name === p.featureName,
|
|
74
|
-
);
|
|
75
|
-
if (match === undefined) {
|
|
76
|
-
throw new Error(`Flagsmith feature not found: ${p.featureName}`);
|
|
77
|
-
}
|
|
78
|
-
return jsonResult(match);
|
|
79
|
-
},
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
reg(
|
|
83
|
-
"flagsmith_search",
|
|
84
|
-
"Substring search across a project's feature flags. Matches the query against flag name, description, and tags (case-insensitive). Returns a `{ matches: [...] }` envelope.",
|
|
85
|
-
z.object({
|
|
86
|
-
projectId: z.number().int(),
|
|
87
|
-
query: z.string().min(1),
|
|
88
|
-
limit: z.number().int().min(1).max(200).optional(),
|
|
89
|
-
}),
|
|
90
|
-
async (p) => {
|
|
91
|
-
const search = new URLSearchParams({ page_size: "500" });
|
|
92
|
-
const root = await fsGet(
|
|
93
|
-
`/projects/${encodeURIComponent(String(p.projectId))}/features/?${search.toString()}`,
|
|
94
|
-
);
|
|
95
|
-
const matches = filterFlagsmithFeatures(featuresFromPage(root), {
|
|
96
|
-
query: p.query,
|
|
97
|
-
limit: p.limit,
|
|
98
|
-
});
|
|
99
|
-
return jsonResult({ matches });
|
|
100
|
-
},
|
|
101
|
-
);
|
|
5
|
+
registerFlagsmithTools(reg);
|
|
102
6
|
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createJsonGetter, envAuthHeaders, optionalEnv } 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 { filterFlagsmithFeatures } from "./search-filter.ts";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_API_BASE = "https://api.flagsmith.com";
|
|
8
|
+
|
|
9
|
+
const fsGet = createJsonGetter({
|
|
10
|
+
// `optionalEnv`, not `optionalBaseUrl`: a trailing slash on the override is left as given.
|
|
11
|
+
base: () => `${optionalEnv("FLAGSMITH_API_BASE", DEFAULT_API_BASE)}/api/v1`,
|
|
12
|
+
label: "Flagsmith",
|
|
13
|
+
headers: envAuthHeaders({ env: "FLAGSMITH_TOKEN", scheme: "Token" }),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function featuresFromPage(root: unknown): unknown[] {
|
|
17
|
+
if (Array.isArray(root)) {
|
|
18
|
+
return root;
|
|
19
|
+
}
|
|
20
|
+
const results = (root as { results?: unknown } | null)?.results;
|
|
21
|
+
return Array.isArray(results) ? results : [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Tool names exposed by this connector — for contract/introspection tests. */
|
|
25
|
+
export const FLAGSMITH_TOOL_NAMES = [
|
|
26
|
+
"flagsmith_list",
|
|
27
|
+
"flagsmith_get",
|
|
28
|
+
"flagsmith_search",
|
|
29
|
+
] as const;
|
|
30
|
+
|
|
31
|
+
export function registerFlagsmithTools(reg: ZodToolRegistrar): void {
|
|
32
|
+
reg(
|
|
33
|
+
"flagsmith_list",
|
|
34
|
+
"List Flagsmith projects, or feature flags for a project. Without `projectId`, returns the account's projects (`/projects/`). With `projectId`, returns that project's feature flags (`/projects/{projectId}/features/`), capped at `limit` (default 100).",
|
|
35
|
+
z.object({
|
|
36
|
+
projectId: z.number().int().optional(),
|
|
37
|
+
limit: z.number().int().min(1).max(500).optional(),
|
|
38
|
+
}),
|
|
39
|
+
async (p) => {
|
|
40
|
+
if (p.projectId === undefined) {
|
|
41
|
+
return jsonResult(await fsGet("/projects/"));
|
|
42
|
+
}
|
|
43
|
+
const search = new URLSearchParams({ page_size: String(p.limit ?? 100) });
|
|
44
|
+
return jsonResult(
|
|
45
|
+
await fsGet(
|
|
46
|
+
`/projects/${encodeURIComponent(String(p.projectId))}/features/?${search.toString()}`,
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
reg(
|
|
53
|
+
"flagsmith_get",
|
|
54
|
+
"Fetch one Flagsmith feature flag by project id + feature name. Flagsmith has no GET-by-name endpoint, so this searches the project's features and narrows to the exact-name match. Throws when no match is found.",
|
|
55
|
+
z.object({
|
|
56
|
+
projectId: z.number().int(),
|
|
57
|
+
featureName: z.string().min(1),
|
|
58
|
+
}),
|
|
59
|
+
async (p) => {
|
|
60
|
+
const search = new URLSearchParams({ page_size: "100", search: p.featureName });
|
|
61
|
+
const root = await fsGet(
|
|
62
|
+
`/projects/${encodeURIComponent(String(p.projectId))}/features/?${search.toString()}`,
|
|
63
|
+
);
|
|
64
|
+
const match = featuresFromPage(root).find(
|
|
65
|
+
(f) =>
|
|
66
|
+
f !== null && typeof f === "object" && (f as { name?: unknown }).name === p.featureName,
|
|
67
|
+
);
|
|
68
|
+
if (match === undefined) {
|
|
69
|
+
throw new Error(`Flagsmith feature not found: ${p.featureName}`);
|
|
70
|
+
}
|
|
71
|
+
return jsonResult(match);
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
reg(
|
|
76
|
+
"flagsmith_search",
|
|
77
|
+
"Substring search across a project's feature flags. Matches the query against flag name, description, and tags (case-insensitive). Returns a `{ matches: [...] }` envelope.",
|
|
78
|
+
z.object({
|
|
79
|
+
projectId: z.number().int(),
|
|
80
|
+
query: z.string().min(1),
|
|
81
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
82
|
+
}),
|
|
83
|
+
async (p) => {
|
|
84
|
+
const search = new URLSearchParams({ page_size: "500" });
|
|
85
|
+
const root = await fsGet(
|
|
86
|
+
`/projects/${encodeURIComponent(String(p.projectId))}/features/?${search.toString()}`,
|
|
87
|
+
);
|
|
88
|
+
const matches = filterFlagsmithFeatures(featuresFromPage(root), {
|
|
89
|
+
query: p.query,
|
|
90
|
+
limit: p.limit,
|
|
91
|
+
});
|
|
92
|
+
return jsonResult({ matches });
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FLUX_KINDS, type FluxKindEntry, trimTrailingSlash } from "@nimbus-dev/sdk";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { type ConsentServer, createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
|
|
4
|
+
import { createJsonGetter, envAuthHeaders, requiredEnv } from "../../../shared/env-json-api.ts";
|
|
4
5
|
import { fetchWithTimeout, mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
|
|
5
6
|
import {
|
|
6
7
|
runReadOnlyMcpConnector,
|
|
@@ -18,30 +19,24 @@ function kindEntry(kind: string): FluxKindEntry {
|
|
|
18
19
|
return found;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/** Only ONE trailing slash is dropped here — not `requiredBaseUrl`'s strip-them-all. */
|
|
21
23
|
function apiBase(): string {
|
|
22
|
-
|
|
23
|
-
if (v === undefined || v === "") {
|
|
24
|
-
throw new Error("FLUX_API_URL is not set");
|
|
25
|
-
}
|
|
26
|
-
return trimTrailingSlash(v);
|
|
24
|
+
return trimTrailingSlash(requiredEnv("FLUX_API_URL"));
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
27
|
+
/**
|
|
28
|
+
* `fetchWithTimeout`, not the global fetch: this is a self-hosted control plane,
|
|
29
|
+
* and one that stops answering must fail the tool call rather than hang it.
|
|
30
|
+
*/
|
|
31
|
+
/** Shared with the mutating request below, which adds its own Content-Type. */
|
|
32
|
+
const authHeader = envAuthHeaders({ env: "FLUX_TOKEN" });
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return JSON.parse(text) as unknown;
|
|
44
|
-
}
|
|
34
|
+
const agGet = createJsonGetter({
|
|
35
|
+
base: apiBase,
|
|
36
|
+
label: "Flux",
|
|
37
|
+
headers: authHeader,
|
|
38
|
+
fetch: fetchWithTimeout,
|
|
39
|
+
});
|
|
45
40
|
|
|
46
41
|
function itemsFrom(root: unknown): unknown[] {
|
|
47
42
|
if (Array.isArray(root)) {
|
|
@@ -80,6 +75,24 @@ async function fluxReconcile(kind: string, namespace: string, name: string): Pro
|
|
|
80
75
|
return requestedAt;
|
|
81
76
|
}
|
|
82
77
|
|
|
78
|
+
/** The Flux CRs a reconcile can be requested on. */
|
|
79
|
+
const RECONCILABLE = [
|
|
80
|
+
{
|
|
81
|
+
tool: "flux_kustomization_reconcile",
|
|
82
|
+
kind: "kustomization",
|
|
83
|
+
mutates: "flux.kustomization.reconcile",
|
|
84
|
+
description:
|
|
85
|
+
"Request a reconcile of a Flux Kustomization by annotating reconcile.fluxcd.io/requestedAt (PATCH the CR; requires HITL flux.kustomization.reconcile, and the SA's `patch` RBAC verb on kustomizations). Async — verify via the next metadata sync.",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
tool: "flux_helmrelease_reconcile",
|
|
89
|
+
kind: "helm_release",
|
|
90
|
+
mutates: "flux.helmrelease.reconcile",
|
|
91
|
+
description:
|
|
92
|
+
"Request a reconcile of a Flux HelmRelease by annotating reconcile.fluxcd.io/requestedAt (PATCH the CR; requires HITL flux.helmrelease.reconcile, and the SA's `patch` RBAC verb on helmreleases). Async — verify via the next metadata sync.",
|
|
93
|
+
},
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
83
96
|
export function registerFluxTools(reg: ZodToolRegistrar, server: unknown): void {
|
|
84
97
|
// Despite the read-only helper's name, this connector exposes write tools. The consent
|
|
85
98
|
// kit needs the real server, which the helper now passes through as its second argument.
|
|
@@ -140,39 +153,25 @@ export function registerFluxTools(reg: ZodToolRegistrar, server: unknown): void
|
|
|
140
153
|
},
|
|
141
154
|
);
|
|
142
155
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
{
|
|
163
|
-
mutates: "flux.helmrelease.reconcile",
|
|
164
|
-
recoverable: true,
|
|
165
|
-
scopeTargetOf: (p) => ({ kind: "namespace", value: p.namespace }),
|
|
166
|
-
},
|
|
167
|
-
"Request a reconcile of a Flux HelmRelease by annotating reconcile.fluxcd.io/requestedAt (PATCH the CR; requires HITL flux.helmrelease.reconcile, and the SA's `patch` RBAC verb on helmreleases). Async — verify via the next metadata sync.",
|
|
168
|
-
z.object({ namespace: z.string().min(1), name: z.string().min(1) }),
|
|
169
|
-
async (p) =>
|
|
170
|
-
jsonResult({
|
|
171
|
-
status: "requested",
|
|
172
|
-
name: p.name,
|
|
173
|
-
requestedAt: await fluxReconcile("helm_release", p.namespace, p.name),
|
|
174
|
-
}),
|
|
175
|
-
);
|
|
156
|
+
// Both reconcile tools are the same annotation PATCH against a different CR.
|
|
157
|
+
for (const { tool, kind, mutates, description } of RECONCILABLE) {
|
|
158
|
+
registerWriteTool(
|
|
159
|
+
tool,
|
|
160
|
+
{
|
|
161
|
+
mutates,
|
|
162
|
+
recoverable: true,
|
|
163
|
+
scopeTargetOf: (p) => ({ kind: "namespace", value: p.namespace }),
|
|
164
|
+
},
|
|
165
|
+
description,
|
|
166
|
+
z.object({ namespace: z.string().min(1), name: z.string().min(1) }),
|
|
167
|
+
async (p) =>
|
|
168
|
+
jsonResult({
|
|
169
|
+
status: "requested",
|
|
170
|
+
name: p.name,
|
|
171
|
+
requestedAt: await fluxReconcile(kind, p.namespace, p.name),
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
176
175
|
}
|
|
177
176
|
|
|
178
177
|
export async function startConnector(): Promise<void> {
|
|
@@ -1,140 +1,10 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
createRegisterSimpleTool,
|
|
6
|
-
createZodToolRegistrar,
|
|
7
|
-
mcpJsonResult as jsonResult,
|
|
8
|
-
} from "../../../shared/mcp-tool-kit.ts";
|
|
9
|
-
import { runCliJson, runCliOk } from "../../../shared/run-cli-json.ts";
|
|
10
|
-
|
|
11
|
-
function gcloudEnv(): Record<string, string | undefined> {
|
|
12
|
-
const e = { ...process.env } as Record<string, string | undefined>;
|
|
13
|
-
const cf = process.env["GOOGLE_APPLICATION_CREDENTIALS"]?.trim();
|
|
14
|
-
if (cf !== undefined && cf !== "") {
|
|
15
|
-
e["GOOGLE_APPLICATION_CREDENTIALS"] = cf;
|
|
16
|
-
}
|
|
17
|
-
return e;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
async function gcloudJson(args: string[]): Promise<unknown> {
|
|
21
|
-
const cmd = ["gcloud", ...args, "--format", "json"];
|
|
22
|
-
const r = await runCliJson(cmd, gcloudEnv());
|
|
23
|
-
if (!r.ok) {
|
|
24
|
-
throw new Error(r.message);
|
|
25
|
-
}
|
|
26
|
-
return r.data ?? {};
|
|
27
|
-
}
|
|
3
|
+
import { registerGcpTools } from "./tools.ts";
|
|
28
4
|
|
|
29
5
|
const mcp = new McpServer({ name: "nimbus-gcp", version: "0.1.0" });
|
|
30
6
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const reg = createZodToolRegistrar(createRegisterSimpleTool(mcp));
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Every MUTATING gcp tool goes through here. Outside the gateway this adds the
|
|
37
|
-
* consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
|
|
38
|
-
* the gateway it is a pass-through, because executor.ts (I2) is the gate there.
|
|
39
|
-
*/
|
|
40
|
-
const registerWriteTool = createWriteToolRegistrar(mcp, {
|
|
41
|
-
connector: "gcp",
|
|
42
|
-
scopeEnv: "NIMBUS_MCP_GCP_WRITE_SCOPE",
|
|
43
|
-
scopeKinds: ["project"],
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
reg(
|
|
47
|
-
"gcp_cloud_run_service_list",
|
|
48
|
-
"List Cloud Run services in a region.",
|
|
49
|
-
z.object({ projectId: z.string().min(1), region: z.string().min(1) }),
|
|
50
|
-
async (p) =>
|
|
51
|
-
jsonResult(
|
|
52
|
-
await gcloudJson([
|
|
53
|
-
"run",
|
|
54
|
-
"services",
|
|
55
|
-
"list",
|
|
56
|
-
`--project=${p.projectId}`,
|
|
57
|
-
`--region=${p.region}`,
|
|
58
|
-
]),
|
|
59
|
-
),
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
registerWriteTool(
|
|
63
|
-
"gcp_cloud_run_deploy",
|
|
64
|
-
{
|
|
65
|
-
mutates: "gcp.cloud_run.deploy",
|
|
66
|
-
recoverable: true,
|
|
67
|
-
scopeTargetOf: (p) => ({ kind: "project", value: p.projectId }),
|
|
68
|
-
},
|
|
69
|
-
"Deploy a container image to Cloud Run.",
|
|
70
|
-
z.object({
|
|
71
|
-
projectId: z.string().min(1),
|
|
72
|
-
region: z.string().min(1),
|
|
73
|
-
service: z.string().min(1),
|
|
74
|
-
image: z.string().min(1),
|
|
75
|
-
}),
|
|
76
|
-
async (p) => {
|
|
77
|
-
const r = await runCliOk(
|
|
78
|
-
[
|
|
79
|
-
"gcloud",
|
|
80
|
-
"run",
|
|
81
|
-
"deploy",
|
|
82
|
-
p.service,
|
|
83
|
-
`--project=${p.projectId}`,
|
|
84
|
-
`--region=${p.region}`,
|
|
85
|
-
`--image=${p.image}`,
|
|
86
|
-
"--quiet",
|
|
87
|
-
],
|
|
88
|
-
gcloudEnv(),
|
|
89
|
-
);
|
|
90
|
-
if (!r.ok) {
|
|
91
|
-
throw new Error(r.message);
|
|
92
|
-
}
|
|
93
|
-
return jsonResult({ ok: true });
|
|
94
|
-
},
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
registerWriteTool(
|
|
98
|
-
"gcp_gke_workload_restart",
|
|
99
|
-
{
|
|
100
|
-
mutates: "gcp.gke.workload.restart",
|
|
101
|
-
recoverable: true,
|
|
102
|
-
scopeTargetOf: (p) => ({ kind: "project", value: p.projectId }),
|
|
103
|
-
},
|
|
104
|
-
"Restart a GKE deployment rollout via kubectl (uses current cluster credentials).",
|
|
105
|
-
z.object({
|
|
106
|
-
projectId: z.string().min(1),
|
|
107
|
-
location: z.string().min(1),
|
|
108
|
-
cluster: z.string().min(1),
|
|
109
|
-
namespace: z.string().min(1),
|
|
110
|
-
deployment: z.string().min(1),
|
|
111
|
-
}),
|
|
112
|
-
async (p) => {
|
|
113
|
-
const getCreds = await runCliOk(
|
|
114
|
-
[
|
|
115
|
-
"gcloud",
|
|
116
|
-
"container",
|
|
117
|
-
"clusters",
|
|
118
|
-
"get-credentials",
|
|
119
|
-
p.cluster,
|
|
120
|
-
`--project=${p.projectId}`,
|
|
121
|
-
`--zone=${p.location}`,
|
|
122
|
-
],
|
|
123
|
-
gcloudEnv(),
|
|
124
|
-
);
|
|
125
|
-
if (!getCreds.ok) {
|
|
126
|
-
throw new Error(getCreds.message);
|
|
127
|
-
}
|
|
128
|
-
const r = await runCliOk(
|
|
129
|
-
["kubectl", "rollout", "restart", "deployment", p.deployment, "-n", p.namespace],
|
|
130
|
-
gcloudEnv(),
|
|
131
|
-
);
|
|
132
|
-
if (!r.ok) {
|
|
133
|
-
throw new Error(r.message);
|
|
134
|
-
}
|
|
135
|
-
return jsonResult({ ok: true });
|
|
136
|
-
},
|
|
137
|
-
);
|
|
7
|
+
registerGcpTools(mcp);
|
|
138
8
|
|
|
139
9
|
const transport = new StdioServerTransport();
|
|
140
10
|
await mcp.connect(transport);
|