@nimbus-dev/connectors 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +14 -4
  2. package/connectors/airflow/src/search-filter.ts +2 -15
  3. package/connectors/airflow/src/server.ts +2 -87
  4. package/connectors/airflow/src/tools.ts +74 -0
  5. package/connectors/apple/src/adapters.ts +326 -0
  6. package/connectors/apple/src/apple-mail-core.ts +4 -8
  7. package/connectors/apple/src/calendar-tools.ts +28 -8
  8. package/connectors/apple/src/server.ts +23 -512
  9. package/connectors/apple/src/tools.ts +18 -44
  10. package/connectors/argocd/src/server.ts +25 -38
  11. package/connectors/athena/src/tools.ts +15 -34
  12. package/connectors/aws/src/server.ts +2 -183
  13. package/connectors/aws/src/tools.ts +197 -0
  14. package/connectors/azure/src/server.ts +2 -131
  15. package/connectors/azure/src/tools.ts +133 -0
  16. package/connectors/bigeye/src/search-filter.ts +4 -15
  17. package/connectors/bigeye/src/server.ts +38 -38
  18. package/connectors/bigquery/src/tools.ts +1 -26
  19. package/connectors/bitbucket/src/server.ts +2 -244
  20. package/connectors/bitbucket/src/tools.ts +241 -0
  21. package/connectors/bitrise/src/server.ts +2 -79
  22. package/connectors/bitrise/src/tools.ts +76 -0
  23. package/connectors/canva/src/search-filter.ts +2 -11
  24. package/connectors/canva/src/server.ts +2 -73
  25. package/connectors/canva/src/tools.ts +66 -0
  26. package/connectors/circleci/src/server.ts +2 -176
  27. package/connectors/circleci/src/tools.ts +193 -0
  28. package/connectors/cloud-logging/src/tools.ts +14 -27
  29. package/connectors/cloudwatch/src/tools.ts +15 -34
  30. package/connectors/codemagic/src/server.ts +2 -70
  31. package/connectors/codemagic/src/tools.ts +71 -0
  32. package/connectors/confluence/src/server.ts +2 -301
  33. package/connectors/confluence/src/tools.ts +320 -0
  34. package/connectors/dagster/src/server.ts +2 -214
  35. package/connectors/dagster/src/tools.ts +187 -0
  36. package/connectors/databricks/src/search-filter.ts +7 -7
  37. package/connectors/databricks/src/server.ts +2 -77
  38. package/connectors/databricks/src/tools.ts +66 -0
  39. package/connectors/datadog/src/server.ts +2 -48
  40. package/connectors/datadog/src/tools.ts +48 -0
  41. package/connectors/dataprofile/src/profile.ts +16 -46
  42. package/connectors/dbt/src/server.ts +2 -92
  43. package/connectors/dbt/src/tools.ts +80 -0
  44. package/connectors/dependencytrack/src/server.ts +2 -76
  45. package/connectors/dependencytrack/src/tools.ts +65 -0
  46. package/connectors/discord/src/server.ts +2 -85
  47. package/connectors/discord/src/tools.ts +82 -0
  48. package/connectors/elasticsearch/src/tools.ts +7 -37
  49. package/connectors/fastmail/src/jmap-client.ts +202 -0
  50. package/connectors/fastmail/src/jmap-core.ts +8 -21
  51. package/connectors/fastmail/src/server.ts +2 -187
  52. package/connectors/fastmail/src/tools.ts +4 -23
  53. package/connectors/figma/src/search-filter.ts +2 -11
  54. package/connectors/figma/src/server.ts +2 -113
  55. package/connectors/figma/src/tools.ts +99 -0
  56. package/connectors/firebase/src/server.ts +2 -90
  57. package/connectors/firebase/src/token.ts +2 -5
  58. package/connectors/firebase/src/tools.ts +106 -0
  59. package/connectors/flagsmith/src/server.ts +2 -98
  60. package/connectors/flagsmith/src/tools.ts +95 -0
  61. package/connectors/flux/src/server.ts +52 -53
  62. package/connectors/gcp/src/server.ts +2 -132
  63. package/connectors/gcp/src/tools.ts +123 -0
  64. package/connectors/github/src/server.ts +2 -328
  65. package/connectors/github/src/tools.ts +311 -0
  66. package/connectors/github-actions/src/server.ts +2 -205
  67. package/connectors/github-actions/src/tools.ts +209 -0
  68. package/connectors/gitlab/src/server.ts +2 -384
  69. package/connectors/gitlab/src/tools.ts +392 -0
  70. package/connectors/gmail/src/server.ts +2 -270
  71. package/connectors/gmail/src/tools.ts +209 -0
  72. package/connectors/google-drive/src/server.ts +2 -524
  73. package/connectors/google-drive/src/tools.ts +529 -0
  74. package/connectors/google-meet/src/server.ts +2 -82
  75. package/connectors/google-meet/src/tools.ts +87 -0
  76. package/connectors/google-photos/src/server.ts +2 -132
  77. package/connectors/google-photos/src/tools.ts +138 -0
  78. package/connectors/grafana/src/server.ts +2 -53
  79. package/connectors/grafana/src/tools.ts +53 -0
  80. package/connectors/great-expectations/src/gx-parse.ts +14 -45
  81. package/connectors/greenhouse/src/server.ts +2 -63
  82. package/connectors/greenhouse/src/tools.ts +68 -0
  83. package/connectors/hubspot/src/server.ts +2 -77
  84. package/connectors/hubspot/src/tools.ts +70 -0
  85. package/connectors/iac/src/server.ts +2 -137
  86. package/connectors/iac/src/tools.ts +152 -0
  87. package/connectors/imap/src/imap-core.ts +29 -82
  88. package/connectors/imap/src/server.ts +33 -220
  89. package/connectors/intercom/src/server.ts +2 -61
  90. package/connectors/intercom/src/tools.ts +58 -0
  91. package/connectors/jenkins/src/jenkins-api.ts +2 -6
  92. package/connectors/jenkins/src/server.ts +2 -215
  93. package/connectors/jenkins/src/tools.ts +240 -0
  94. package/connectors/jira/src/server.ts +2 -301
  95. package/connectors/jira/src/tools.ts +325 -0
  96. package/connectors/kubernetes/src/server.ts +2 -155
  97. package/connectors/kubernetes/src/tools.ts +159 -0
  98. package/connectors/launchdarkly/src/server.ts +2 -75
  99. package/connectors/launchdarkly/src/tools.ts +72 -0
  100. package/connectors/lever/src/search-filter.ts +4 -12
  101. package/connectors/lever/src/server.ts +2 -60
  102. package/connectors/lever/src/tools.ts +61 -0
  103. package/connectors/linear/src/server.ts +2 -459
  104. package/connectors/linear/src/tools.ts +482 -0
  105. package/connectors/localdb/src/sql-scan.ts +13 -41
  106. package/connectors/localdb/src/tools.ts +1 -1
  107. package/connectors/looker/src/search-filter.ts +2 -15
  108. package/connectors/looker/src/server.ts +35 -70
  109. package/connectors/mendeley/src/server.ts +2 -55
  110. package/connectors/mendeley/src/tools.ts +54 -0
  111. package/connectors/mercury/src/server.ts +2 -57
  112. package/connectors/mercury/src/tools.ts +50 -0
  113. package/connectors/metabase/src/search-filter.ts +4 -15
  114. package/connectors/metabase/src/server.ts +2 -79
  115. package/connectors/metabase/src/tools.ts +64 -0
  116. package/connectors/miro/src/server.ts +2 -72
  117. package/connectors/miro/src/tools.ts +65 -0
  118. package/connectors/mlflow/src/search-filter.ts +3 -7
  119. package/connectors/mlflow/src/server.ts +20 -40
  120. package/connectors/monte-carlo/src/server.ts +50 -57
  121. package/connectors/netlify/src/search-filter.ts +6 -14
  122. package/connectors/netlify/src/server.ts +2 -60
  123. package/connectors/netlify/src/tools.ts +53 -0
  124. package/connectors/newrelic/src/server.ts +2 -40
  125. package/connectors/newrelic/src/tools.ts +37 -0
  126. package/connectors/notion/src/server.ts +2 -335
  127. package/connectors/notion/src/tools.ts +344 -0
  128. package/connectors/obsidian/src/server.ts +2 -425
  129. package/connectors/obsidian/src/tools.ts +466 -0
  130. package/connectors/onedrive/src/server.ts +2 -249
  131. package/connectors/onedrive/src/tools.ts +253 -0
  132. package/connectors/outlook/src/server.ts +2 -351
  133. package/connectors/outlook/src/tools.ts +357 -0
  134. package/connectors/pagerduty/src/server.ts +2 -132
  135. package/connectors/pagerduty/src/tools.ts +126 -0
  136. package/connectors/pipedrive/src/server.ts +2 -58
  137. package/connectors/pipedrive/src/tools.ts +63 -0
  138. package/connectors/powerbi/src/search-filter.ts +2 -15
  139. package/connectors/powerbi/src/server.ts +43 -68
  140. package/connectors/prefect/src/search-filter.ts +3 -16
  141. package/connectors/prefect/src/server.ts +2 -90
  142. package/connectors/prefect/src/tools.ts +83 -0
  143. package/connectors/protonmail/src/mail-core.ts +16 -75
  144. package/connectors/protonmail/src/server.ts +34 -214
  145. package/connectors/raindrop/src/server.ts +2 -103
  146. package/connectors/raindrop/src/tools.ts +103 -0
  147. package/connectors/ramp/src/server.ts +2 -105
  148. package/connectors/ramp/src/tools.ts +98 -0
  149. package/connectors/readwise/src/server.ts +2 -88
  150. package/connectors/readwise/src/tools.ts +81 -0
  151. package/connectors/sagemaker/src/tools.ts +19 -38
  152. package/connectors/salesforce/src/search-filter.ts +4 -11
  153. package/connectors/salesforce/src/server.ts +2 -90
  154. package/connectors/salesforce/src/tools.ts +75 -0
  155. package/connectors/semgrep/src/search-filter.ts +3 -10
  156. package/connectors/semgrep/src/server.ts +2 -104
  157. package/connectors/semgrep/src/tools.ts +92 -0
  158. package/connectors/sentry/src/server.ts +2 -58
  159. package/connectors/sentry/src/tools.ts +53 -0
  160. package/connectors/slack/src/server.ts +2 -352
  161. package/connectors/slack/src/tools.ts +376 -0
  162. package/connectors/snowflake/src/search-filter.ts +4 -19
  163. package/connectors/snowflake/src/server.ts +6 -25
  164. package/connectors/snyk/src/server.ts +2 -140
  165. package/connectors/snyk/src/tools.ts +136 -0
  166. package/connectors/sonarqube/src/server.ts +2 -111
  167. package/connectors/sonarqube/src/tools.ts +98 -0
  168. package/connectors/stackoverflow/src/server.ts +2 -77
  169. package/connectors/stackoverflow/src/tools.ts +66 -0
  170. package/connectors/storybook/src/storybook-parse.ts +3 -6
  171. package/connectors/stripe/src/server.ts +2 -61
  172. package/connectors/stripe/src/tools.ts +54 -0
  173. package/connectors/superset/src/search-filter.ts +4 -15
  174. package/connectors/superset/src/server.ts +2 -115
  175. package/connectors/superset/src/tools.ts +98 -0
  176. package/connectors/tableau/src/search-filter.ts +2 -15
  177. package/connectors/tableau/src/server.ts +13 -65
  178. package/connectors/teams/src/server.ts +2 -249
  179. package/connectors/teams/src/tools.ts +283 -0
  180. package/connectors/testflight/src/server.ts +2 -70
  181. package/connectors/testflight/src/tools.ts +79 -0
  182. package/connectors/vercel/src/search-filter.ts +2 -10
  183. package/connectors/vercel/src/server.ts +2 -75
  184. package/connectors/vercel/src/tools.ts +73 -0
  185. package/connectors/vertex-ai/src/tools.ts +23 -33
  186. package/connectors/wiz/src/search-filter.ts +6 -30
  187. package/connectors/wiz/src/server.ts +2 -188
  188. package/connectors/wiz/src/tools.ts +168 -0
  189. package/connectors/zendesk/src/server.ts +2 -73
  190. package/connectors/zendesk/src/tools.ts +60 -0
  191. package/connectors/zoom/src/search-filter.ts +1 -7
  192. package/connectors/zoom/src/server.ts +2 -94
  193. package/connectors/zoom/src/tools.ts +93 -0
  194. package/connectors/zotero/src/server.ts +2 -78
  195. package/connectors/zotero/src/tools.ts +67 -0
  196. package/package.json +14 -12
  197. package/shared/access-token-cache.ts +123 -0
  198. package/shared/atlassian-json-fetch.ts +6 -16
  199. package/shared/audit-chain.ts +13 -6
  200. package/shared/cli-json-kit.ts +150 -0
  201. package/shared/collection-tool-kit.ts +143 -0
  202. package/shared/connector-mode.ts +3 -2
  203. package/shared/cursor-list-tool.ts +29 -0
  204. package/shared/env-json-api.ts +209 -0
  205. package/shared/fetch-json-text.ts +27 -0
  206. package/shared/github-rest.ts +62 -0
  207. package/shared/graphql-json.ts +60 -0
  208. package/shared/imap-mail-core.ts +91 -0
  209. package/shared/imap-tool-kit.ts +57 -23
  210. package/shared/imapflow-adapter.ts +417 -0
  211. package/shared/rest-tool-kit.ts +77 -7
  212. package/shared/run-cli-json.ts +38 -16
  213. package/shared/search-filter.ts +58 -18
  214. package/shared/walk-files.ts +118 -0
  215. package/standalone/src/launcher.ts +19 -7
@@ -0,0 +1,136 @@
1
+ import { z } from "zod";
2
+ import { createJsonGetter, envAuthHeaders } from "../../../shared/env-json-api.ts";
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 { filterSnykAggregatedIssues } from "./search-filter.ts";
7
+
8
+ const SNYK_API = "https://api.snyk.io";
9
+
10
+ /** Snyk wants the lowercase `token` scheme. Shared with `snykPost` below. */
11
+ const authHeader = envAuthHeaders({ env: "SNYK_TOKEN", scheme: "token" });
12
+
13
+ const snykGet = createJsonGetter({
14
+ base: SNYK_API,
15
+ label: "Snyk",
16
+ headers: authHeader,
17
+ });
18
+
19
+ async function snykPost(path: string, body: unknown): Promise<unknown> {
20
+ const res = await fetch(`${SNYK_API}${path}`, {
21
+ method: "POST",
22
+ headers: { ...authHeader(), "Content-Type": "application/json" },
23
+ body: JSON.stringify(body),
24
+ });
25
+ const text = await res.text();
26
+ if (!res.ok) {
27
+ throw new Error(`Snyk ${String(res.status)}: ${text.slice(0, 400)}`);
28
+ }
29
+ if (text.trim() === "") {
30
+ return {};
31
+ }
32
+ return JSON.parse(text) as unknown;
33
+ }
34
+
35
+ /** Every severity Snyk reports: `snyk_list`'s default filter, and the one get/search always use. */
36
+ const ALL_SEVERITIES: readonly string[] = ["critical", "high", "medium", "low"];
37
+
38
+ /**
39
+ * One project's aggregated issues (`POST …/aggregated-issues`): vulnerability and licence issues
40
+ * of the given severities that are neither ignored nor patched.
41
+ */
42
+ async function aggregatedIssues(
43
+ orgId: string,
44
+ projectId: string,
45
+ severities: readonly string[] = ALL_SEVERITIES,
46
+ ): Promise<unknown> {
47
+ const body = {
48
+ filters: {
49
+ severities,
50
+ types: ["vuln", "license"],
51
+ ignored: false,
52
+ patched: false,
53
+ },
54
+ };
55
+ const path = `/v1/org/${encodeURIComponent(orgId)}/project/${encodeURIComponent(projectId)}/aggregated-issues`;
56
+ return snykPost(path, body);
57
+ }
58
+
59
+ /** Tool names exposed by this connector — for contract/introspection tests. */
60
+ export const SNYK_TOOL_NAMES = ["snyk_get", "snyk_list", "snyk_search"] as const;
61
+
62
+ export function registerSnykTools(reg: ZodToolRegistrar): void {
63
+ reg(
64
+ "snyk_list",
65
+ "List Snyk projects for an org, or aggregated issues for a project. When projectId is omitted, returns the org's project list. Severities default to all four (critical/high/medium/low).",
66
+ z.object({
67
+ orgId: z.string().min(1),
68
+ projectId: z.string().min(1).optional(),
69
+ severities: z
70
+ .array(z.enum(["critical", "high", "medium", "low"]))
71
+ .min(1)
72
+ .optional(),
73
+ limit: z.number().int().min(1).max(1000).optional(),
74
+ }),
75
+ async (p) => {
76
+ if (p.projectId === undefined) {
77
+ return jsonResult(await snykGet(`/v1/org/${encodeURIComponent(p.orgId)}/projects`));
78
+ }
79
+ const data = await aggregatedIssues(p.orgId, p.projectId, p.severities);
80
+ if (p.limit !== undefined && data !== null && typeof data === "object") {
81
+ const issues = (data as { issues?: unknown[] }).issues;
82
+ if (Array.isArray(issues) && issues.length > p.limit) {
83
+ return jsonResult({
84
+ ...(data as Record<string, unknown>),
85
+ issues: issues.slice(0, p.limit),
86
+ });
87
+ }
88
+ }
89
+ return jsonResult(data);
90
+ },
91
+ );
92
+
93
+ reg(
94
+ "snyk_get",
95
+ "Fetch one Snyk issue by id from an org + project. Returns the aggregated-issue envelope for the matching `issue.id`; throws when no match is found.",
96
+ z.object({
97
+ orgId: z.string().min(1),
98
+ projectId: z.string().min(1),
99
+ issueId: z.string().min(1),
100
+ }),
101
+ async (p) => {
102
+ const root = await aggregatedIssues(p.orgId, p.projectId);
103
+ const issues = (root as { issues?: unknown[] } | null)?.issues;
104
+ if (!Array.isArray(issues)) {
105
+ throw new TypeError(`Snyk: project ${p.projectId} returned no issues envelope`);
106
+ }
107
+ const match = issues.find((it) => {
108
+ if (it === null || typeof it !== "object") {
109
+ return false;
110
+ }
111
+ const id = (it as { id?: unknown }).id;
112
+ return typeof id === "string" && id === p.issueId;
113
+ });
114
+ if (match === undefined) {
115
+ throw new Error(`Snyk: issue ${p.issueId} not found in project ${p.projectId}`);
116
+ }
117
+ return jsonResult(match);
118
+ },
119
+ );
120
+
121
+ reg(
122
+ "snyk_search",
123
+ "Substring search across a project's aggregated issues. Matches the query against `issueData.title`, `issueData.cve`, and `pkgName` (case-insensitive). Returns a `{ matches: [...] }` envelope.",
124
+ z.object({
125
+ orgId: z.string().min(1),
126
+ projectId: z.string().min(1),
127
+ query: z.string().min(1),
128
+ limit: z.number().int().min(1).max(200).optional(),
129
+ }),
130
+ async (p) => {
131
+ const root = await aggregatedIssues(p.orgId, p.projectId);
132
+ const issues = (root as { issues?: unknown[] } | null)?.issues;
133
+ return matchesResult(issues, filterSnykAggregatedIssues, p);
134
+ },
135
+ );
136
+ }
@@ -1,115 +1,6 @@
1
- import { z } from "zod";
2
- import { matchesResult } 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 { filterSonarIssues } from "./search-filter.ts";
6
-
7
- const DEFAULT_API = "https://sonarcloud.io";
8
- const ISSUE_TYPES = ["BUG", "VULNERABILITY", "CODE_SMELL"] as const;
9
- const OPEN_STATUSES = ["OPEN", "CONFIRMED", "REOPENED"] as const;
10
-
11
- function stripTrailingSlashes(s: string): string {
12
- let end = s.length;
13
- while (end > 0 && s.codePointAt(end - 1) === 47) end -= 1;
14
- return s.slice(0, end);
15
- }
16
-
17
- function apiBase(): string {
18
- const raw = process.env["SONARQUBE_URL"]?.trim();
19
- if (raw === undefined || raw === "") {
20
- return DEFAULT_API;
21
- }
22
- return stripTrailingSlashes(raw);
23
- }
24
-
25
- function authHeader(): Record<string, string> {
26
- const t = process.env["SONARQUBE_TOKEN"]?.trim();
27
- if (t === undefined || t === "") {
28
- throw new Error("SONARQUBE_TOKEN is not set");
29
- }
30
- return { Authorization: `Bearer ${t}`, Accept: "application/json" };
31
- }
32
-
33
- async function sonarGet(path: string): Promise<unknown> {
34
- const res = await fetch(`${apiBase()}${path}`, { headers: authHeader() });
35
- const text = await res.text();
36
- if (!res.ok) {
37
- throw new Error(`SonarQube ${String(res.status)}: ${text.slice(0, 400)}`);
38
- }
39
- return JSON.parse(text) as unknown;
40
- }
2
+ import { registerSonarqubeTools } from "./tools.ts";
41
3
 
42
4
  await runReadOnlyMcpConnector("nimbus-sonarqube", (reg) => {
43
- reg(
44
- "sonarqube_list",
45
- "List SonarQube projects, or issues for a project. When projectKey is omitted, returns the project list (SonarCloud requires `SONARQUBE_ORGANIZATION` env; self-hosted ignores it). When projectKey is provided, returns open issues (status OPEN/CONFIRMED/REOPENED) for that project.",
46
- z.object({
47
- projectKey: z.string().min(1).optional(),
48
- types: z.array(z.enum(ISSUE_TYPES)).min(1).optional(),
49
- severities: z
50
- .array(z.enum(["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"]))
51
- .min(1)
52
- .optional(),
53
- limit: z.number().int().min(1).max(500).optional(),
54
- }),
55
- async (p) => {
56
- if (p.projectKey === undefined) {
57
- const org = process.env["SONARQUBE_ORGANIZATION"]?.trim();
58
- const search = new URLSearchParams({ qualifiers: "TRK", ps: "100" });
59
- if (org !== undefined && org !== "") {
60
- search.set("organization", org);
61
- }
62
- return jsonResult(await sonarGet(`/api/components/search?${search.toString()}`));
63
- }
64
- const types = p.types ?? ISSUE_TYPES;
65
- const search = new URLSearchParams({
66
- componentKeys: p.projectKey,
67
- types: types.join(","),
68
- statuses: OPEN_STATUSES.join(","),
69
- ps: String(p.limit ?? 100),
70
- });
71
- if (p.severities !== undefined && p.severities.length > 0) {
72
- search.set("severities", p.severities.join(","));
73
- }
74
- return jsonResult(await sonarGet(`/api/issues/search?${search.toString()}`));
75
- },
76
- );
77
-
78
- reg(
79
- "sonarqube_get",
80
- "Fetch one SonarQube issue by key. Returns the matching issue envelope from `/api/issues/search?issues=<key>`; throws when no match is found.",
81
- z.object({
82
- issueKey: z.string().min(1),
83
- }),
84
- async (p) => {
85
- const search = new URLSearchParams({ issues: p.issueKey, ps: "1" });
86
- const root = await sonarGet(`/api/issues/search?${search.toString()}`);
87
- const issues = (root as { issues?: unknown[] } | null)?.issues;
88
- if (!Array.isArray(issues) || issues.length === 0) {
89
- throw new Error(`SonarQube: issue ${p.issueKey} not found`);
90
- }
91
- return jsonResult(issues[0]);
92
- },
93
- );
94
-
95
- reg(
96
- "sonarqube_search",
97
- "Substring search across a project's open issues. Matches the query against `message`, `rule`, `component`, and `tags` (case-insensitive). Returns a `{ matches: [...] }` envelope.",
98
- z.object({
99
- projectKey: z.string().min(1),
100
- query: z.string().min(1),
101
- limit: z.number().int().min(1).max(200).optional(),
102
- }),
103
- async (p) => {
104
- const search = new URLSearchParams({
105
- componentKeys: p.projectKey,
106
- types: ISSUE_TYPES.join(","),
107
- statuses: OPEN_STATUSES.join(","),
108
- ps: "500",
109
- });
110
- const root = await sonarGet(`/api/issues/search?${search.toString()}`);
111
- const issues = (root as { issues?: unknown[] } | null)?.issues;
112
- return matchesResult(issues, filterSonarIssues, p);
113
- },
114
- );
5
+ registerSonarqubeTools(reg);
115
6
  });
@@ -0,0 +1,98 @@
1
+ import { z } from "zod";
2
+ import { createJsonGetter, envAuthHeaders, optionalBaseUrl } from "../../../shared/env-json-api.ts";
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 { filterSonarIssues } from "./search-filter.ts";
7
+
8
+ const DEFAULT_API = "https://sonarcloud.io";
9
+ const ISSUE_TYPES = ["BUG", "VULNERABILITY", "CODE_SMELL"] as const;
10
+ const OPEN_STATUSES = ["OPEN", "CONFIRMED", "REOPENED"] as const;
11
+
12
+ const sonarGet = createJsonGetter({
13
+ base: () => optionalBaseUrl("SONARQUBE_URL", DEFAULT_API),
14
+ label: "SonarQube",
15
+ headers: envAuthHeaders({ env: "SONARQUBE_TOKEN" }),
16
+ });
17
+
18
+ /** Tool names exposed by this connector — for contract/introspection tests. */
19
+ export const SONARQUBE_TOOL_NAMES = [
20
+ "sonarqube_list",
21
+ "sonarqube_get",
22
+ "sonarqube_search",
23
+ ] as const;
24
+
25
+ export function registerSonarqubeTools(reg: ZodToolRegistrar): void {
26
+ reg(
27
+ "sonarqube_list",
28
+ "List SonarQube projects, or issues for a project. When projectKey is omitted, returns the project list (SonarCloud requires `SONARQUBE_ORGANIZATION` env; self-hosted ignores it). When projectKey is provided, returns open issues (status OPEN/CONFIRMED/REOPENED) for that project.",
29
+ z.object({
30
+ projectKey: z.string().min(1).optional(),
31
+ types: z.array(z.enum(ISSUE_TYPES)).min(1).optional(),
32
+ severities: z
33
+ .array(z.enum(["BLOCKER", "CRITICAL", "MAJOR", "MINOR", "INFO"]))
34
+ .min(1)
35
+ .optional(),
36
+ limit: z.number().int().min(1).max(500).optional(),
37
+ }),
38
+ async (p) => {
39
+ if (p.projectKey === undefined) {
40
+ const org = process.env["SONARQUBE_ORGANIZATION"]?.trim();
41
+ const search = new URLSearchParams({ qualifiers: "TRK", ps: "100" });
42
+ if (org !== undefined && org !== "") {
43
+ search.set("organization", org);
44
+ }
45
+ return jsonResult(await sonarGet(`/api/components/search?${search.toString()}`));
46
+ }
47
+ const types = p.types ?? ISSUE_TYPES;
48
+ const search = new URLSearchParams({
49
+ componentKeys: p.projectKey,
50
+ types: types.join(","),
51
+ statuses: OPEN_STATUSES.join(","),
52
+ ps: String(p.limit ?? 100),
53
+ });
54
+ if (p.severities !== undefined && p.severities.length > 0) {
55
+ search.set("severities", p.severities.join(","));
56
+ }
57
+ return jsonResult(await sonarGet(`/api/issues/search?${search.toString()}`));
58
+ },
59
+ );
60
+
61
+ reg(
62
+ "sonarqube_get",
63
+ "Fetch one SonarQube issue by key. Returns the matching issue envelope from `/api/issues/search?issues=<key>`; throws when no match is found.",
64
+ z.object({
65
+ issueKey: z.string().min(1),
66
+ }),
67
+ async (p) => {
68
+ const search = new URLSearchParams({ issues: p.issueKey, ps: "1" });
69
+ const root = await sonarGet(`/api/issues/search?${search.toString()}`);
70
+ const issues = (root as { issues?: unknown[] } | null)?.issues;
71
+ if (!Array.isArray(issues) || issues.length === 0) {
72
+ throw new Error(`SonarQube: issue ${p.issueKey} not found`);
73
+ }
74
+ return jsonResult(issues[0]);
75
+ },
76
+ );
77
+
78
+ reg(
79
+ "sonarqube_search",
80
+ "Substring search across a project's open issues. Matches the query against `message`, `rule`, `component`, and `tags` (case-insensitive). Returns a `{ matches: [...] }` envelope.",
81
+ z.object({
82
+ projectKey: z.string().min(1),
83
+ query: z.string().min(1),
84
+ limit: z.number().int().min(1).max(200).optional(),
85
+ }),
86
+ async (p) => {
87
+ const search = new URLSearchParams({
88
+ componentKeys: p.projectKey,
89
+ types: ISSUE_TYPES.join(","),
90
+ statuses: OPEN_STATUSES.join(","),
91
+ ps: "500",
92
+ });
93
+ const root = await sonarGet(`/api/issues/search?${search.toString()}`);
94
+ const issues = (root as { issues?: unknown[] } | null)?.issues;
95
+ return matchesResult(issues, filterSonarIssues, p);
96
+ },
97
+ );
98
+ }
@@ -1,81 +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 { filterStackOverflowQuestions } from "./search-filter.ts";
6
-
7
- const BASE = "https://api.stackoverflowteams.com";
8
-
9
- function apiToken(): string {
10
- const t = process.env["STACKOVERFLOW_TOKEN"]?.trim();
11
- if (t === undefined || t === "") {
12
- throw new Error("STACKOVERFLOW_TOKEN is not set");
13
- }
14
- return t;
15
- }
16
-
17
- function teamSlug(): string {
18
- const t = process.env["STACKOVERFLOW_TEAM"]?.trim();
19
- if (t === undefined || t === "") {
20
- throw new Error("STACKOVERFLOW_TEAM is not set");
21
- }
22
- return t;
23
- }
24
-
25
- function authHeader(): Record<string, string> {
26
- return { Authorization: `Bearer ${apiToken()}`, Accept: "application/json" };
27
- }
28
-
29
- async function stackOverflowGet(path: string): Promise<unknown> {
30
- const res = await fetch(`${BASE}${path}`, { headers: authHeader() });
31
- const text = await res.text();
32
- if (!res.ok) {
33
- throw new Error(`Stack Overflow ${String(res.status)}: ${text.slice(0, 400)}`);
34
- }
35
- return JSON.parse(text) as unknown;
36
- }
37
-
38
- function questionsBasePath(): string {
39
- return `/v3/teams/${encodeURIComponent(teamSlug())}/questions`;
40
- }
2
+ import { registerStackoverflowTools } from "./tools.ts";
41
3
 
42
4
  await runReadOnlyMcpConnector("nimbus-stackoverflow", (reg) => {
43
- reg(
44
- "stackoverflow_list",
45
- "List the team's Stack Overflow for Teams questions (`GET /v3/teams/<team>/questions?page=1&pagesize=100&sort=creation&order=desc`). Returns the `{ items: [...], totalCount, pageSize, page, totalPages, sort, order }` envelope — `items` holds the question objects.",
46
- z.object({}),
47
- async () => {
48
- return jsonResult(
49
- await stackOverflowGet(
50
- `${questionsBasePath()}?page=1&pagesize=100&sort=creation&order=desc`,
51
- ),
52
- );
53
- },
54
- );
55
-
56
- reg(
57
- "stackoverflow_get",
58
- "Fetch one Stack Overflow for Teams question by its id (`GET /v3/teams/<team>/questions/{id}`). Returns the question object directly (NOT wrapped in an `{ items }` envelope). Throws when no match is found.",
59
- z.object({
60
- id: z.string().min(1),
61
- }),
62
- async (p) => {
63
- return jsonResult(
64
- await stackOverflowGet(`${questionsBasePath()}/${encodeURIComponent(p.id)}`),
65
- );
66
- },
67
- );
68
-
69
- reg(
70
- "stackoverflow_search",
71
- "Substring search across the team's Stack Overflow for Teams questions (first page only). Matches the query against the question title, body, tags, and the asking user's name (case-insensitive). Returns a `{ matches: [...] }` envelope.",
72
- searchToolInputSchema(100),
73
- async (p) => {
74
- const root = await stackOverflowGet(
75
- `${questionsBasePath()}?page=1&pagesize=100&sort=creation&order=desc`,
76
- );
77
- const items = (root as { items?: unknown[] } | null)?.items;
78
- return matchesResult(items, filterStackOverflowQuestions, p);
79
- },
80
- );
5
+ registerStackoverflowTools(reg);
81
6
  });
@@ -0,0 +1,66 @@
1
+ import { z } from "zod";
2
+ import { createJsonGetter, envAuthHeaders, 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 { filterStackOverflowQuestions } from "./search-filter.ts";
7
+
8
+ const BASE = "https://api.stackoverflowteams.com";
9
+
10
+ const stackOverflowGet = createJsonGetter({
11
+ base: BASE,
12
+ label: "Stack Overflow",
13
+ headers: envAuthHeaders({ env: "STACKOVERFLOW_TOKEN" }),
14
+ });
15
+
16
+ function questionsBasePath(): string {
17
+ return `/v3/teams/${encodeURIComponent(requiredEnv("STACKOVERFLOW_TEAM"))}/questions`;
18
+ }
19
+
20
+ /** Tool names exposed by this connector — for contract/introspection tests. */
21
+ export const STACKOVERFLOW_TOOL_NAMES = [
22
+ "stackoverflow_list",
23
+ "stackoverflow_get",
24
+ "stackoverflow_search",
25
+ ] as const;
26
+
27
+ export function registerStackoverflowTools(reg: ZodToolRegistrar): void {
28
+ reg(
29
+ "stackoverflow_list",
30
+ "List the team's Stack Overflow for Teams questions (`GET /v3/teams/<team>/questions?page=1&pagesize=100&sort=creation&order=desc`). Returns the `{ items: [...], totalCount, pageSize, page, totalPages, sort, order }` envelope — `items` holds the question objects.",
31
+ z.object({}),
32
+ async () => {
33
+ return jsonResult(
34
+ await stackOverflowGet(
35
+ `${questionsBasePath()}?page=1&pagesize=100&sort=creation&order=desc`,
36
+ ),
37
+ );
38
+ },
39
+ );
40
+
41
+ reg(
42
+ "stackoverflow_get",
43
+ "Fetch one Stack Overflow for Teams question by its id (`GET /v3/teams/<team>/questions/{id}`). Returns the question object directly (NOT wrapped in an `{ items }` envelope). Throws when no match is found.",
44
+ z.object({
45
+ id: z.string().min(1),
46
+ }),
47
+ async (p) => {
48
+ return jsonResult(
49
+ await stackOverflowGet(`${questionsBasePath()}/${encodeURIComponent(p.id)}`),
50
+ );
51
+ },
52
+ );
53
+
54
+ reg(
55
+ "stackoverflow_search",
56
+ "Substring search across the team's Stack Overflow for Teams questions (first page only). Matches the query against the question title, body, tags, and the asking user's name (case-insensitive). Returns a `{ matches: [...] }` envelope.",
57
+ searchToolInputSchema(100),
58
+ async (p) => {
59
+ const root = await stackOverflowGet(
60
+ `${questionsBasePath()}?page=1&pagesize=100&sort=creation&order=desc`,
61
+ );
62
+ const items = (root as { items?: unknown[] } | null)?.items;
63
+ return matchesResult(items, filterStackOverflowQuestions, p);
64
+ },
65
+ );
66
+ }
@@ -1,6 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import { parseStorybookIndex, type StorybookStory } from "@nimbus-dev/sdk";
4
+ import { requiredEnv } from "../../../shared/env-json-api.ts";
4
5
 
5
6
  export type { StorybookStory };
6
7
  export { parseStorybookIndex };
@@ -23,17 +24,13 @@ const MANIFEST_NAMES = ["index.json", "stories.json"] as const;
23
24
  * the MCP tools surface a clear error rather than reading an empty path.
24
25
  */
25
26
  export function storybookDir(): string {
26
- const dir = process.env["STORYBOOK_DIR"]?.trim();
27
- if (dir === undefined || dir === "") {
28
- throw new Error("STORYBOOK_DIR is not set");
29
- }
30
- return resolve(dir);
27
+ return resolve(requiredEnv("STORYBOOK_DIR"));
31
28
  }
32
29
 
33
30
  async function readManifest(root: string): Promise<unknown> {
34
31
  for (const name of MANIFEST_NAMES) {
35
32
  try {
36
- const buf = await readFile(join(root, name));
33
+ const buf = await readFile(join(root, name)); // NOSONAR S9382: a fallback chain, not independent reads — stories.json is read only when index.json cannot be read or parsed, and index.json must win when both exist.
37
34
  if (buf.byteLength > MAX_FILE_BYTES) {
38
35
  return null;
39
36
  }
@@ -1,65 +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 { filterStripeInvoices } from "./search-filter.ts";
6
-
7
- const BASE = "https://api.stripe.com";
8
-
9
- function apiKey(): string {
10
- const t = process.env["STRIPE_API_KEY"]?.trim();
11
- if (t === undefined || t === "") {
12
- throw new Error("STRIPE_API_KEY is not set");
13
- }
14
- return t;
15
- }
16
-
17
- function authHeader(): Record<string, string> {
18
- return { Authorization: `Bearer ${apiKey()}`, Accept: "application/json" };
19
- }
20
-
21
- async function stripeGet(path: string): Promise<unknown> {
22
- const res = await fetch(`${BASE}${path}`, { headers: authHeader() });
23
- const text = await res.text();
24
- if (!res.ok) {
25
- throw new Error(`Stripe ${String(res.status)}: ${text.slice(0, 400)}`);
26
- }
27
- return JSON.parse(text) as unknown;
28
- }
2
+ import { registerStripeTools } from "./tools.ts";
29
3
 
30
4
  await runReadOnlyMcpConnector("nimbus-stripe", (reg) => {
31
- reg(
32
- "stripe_list",
33
- "List recent Stripe invoices (`GET /v1/invoices`), most-recent-first, capped at `limit` (default 100). Returns the `{ object: 'list', data, has_more }` envelope.",
34
- z.object({
35
- limit: z.number().int().min(1).max(100).optional(),
36
- }),
37
- async (p) => {
38
- const search = new URLSearchParams({ limit: String(p.limit ?? 100) });
39
- return jsonResult(await stripeGet(`/v1/invoices?${search.toString()}`));
40
- },
41
- );
42
-
43
- reg(
44
- "stripe_get",
45
- "Fetch one Stripe invoice by its id (`GET /v1/invoices/{id}`, e.g. `in_1A2b...`). Returns the invoice object directly. Throws when no match is found.",
46
- z.object({
47
- id: z.string().min(1),
48
- }),
49
- async (p) => {
50
- return jsonResult(await stripeGet(`/v1/invoices/${encodeURIComponent(p.id)}`));
51
- },
52
- );
53
-
54
- reg(
55
- "stripe_search",
56
- "Substring search across recent Stripe invoices. Matches the query against id, number, status, customer id, customer name, customer email, and the description (case-insensitive). Returns a `{ matches: [...] }` envelope.",
57
- searchToolInputSchema(100),
58
- async (p) => {
59
- const search = new URLSearchParams({ limit: "100" });
60
- const root = await stripeGet(`/v1/invoices?${search.toString()}`);
61
- const data = (root as { data?: unknown[] } | null)?.data;
62
- return matchesResult(data, filterStripeInvoices, p);
63
- },
64
- );
5
+ registerStripeTools(reg);
65
6
  });
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ import { createJsonGetter, envAuthHeaders } 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 { filterStripeInvoices } from "./search-filter.ts";
7
+
8
+ const BASE = "https://api.stripe.com";
9
+
10
+ const stripeGet = createJsonGetter({
11
+ base: BASE,
12
+ label: "Stripe",
13
+ headers: envAuthHeaders({ env: "STRIPE_API_KEY" }),
14
+ });
15
+
16
+ /** Tool names exposed by this connector — for contract/introspection tests. */
17
+ export const STRIPE_TOOL_NAMES = ["stripe_get", "stripe_list", "stripe_search"] as const;
18
+
19
+ export function registerStripeTools(reg: ZodToolRegistrar): void {
20
+ reg(
21
+ "stripe_list",
22
+ "List recent Stripe invoices (`GET /v1/invoices`), most-recent-first, capped at `limit` (default 100). Returns the `{ object: 'list', data, has_more }` envelope.",
23
+ z.object({
24
+ limit: z.number().int().min(1).max(100).optional(),
25
+ }),
26
+ async (p) => {
27
+ const search = new URLSearchParams({ limit: String(p.limit ?? 100) });
28
+ return jsonResult(await stripeGet(`/v1/invoices?${search.toString()}`));
29
+ },
30
+ );
31
+
32
+ reg(
33
+ "stripe_get",
34
+ "Fetch one Stripe invoice by its id (`GET /v1/invoices/{id}`, e.g. `in_1A2b...`). Returns the invoice object directly. Throws when no match is found.",
35
+ z.object({
36
+ id: z.string().min(1),
37
+ }),
38
+ async (p) => {
39
+ return jsonResult(await stripeGet(`/v1/invoices/${encodeURIComponent(p.id)}`));
40
+ },
41
+ );
42
+
43
+ reg(
44
+ "stripe_search",
45
+ "Substring search across recent Stripe invoices. Matches the query against id, number, status, customer id, customer name, customer email, and the description (case-insensitive). Returns a `{ matches: [...] }` envelope.",
46
+ searchToolInputSchema(100),
47
+ async (p) => {
48
+ const search = new URLSearchParams({ limit: "100" });
49
+ const root = await stripeGet(`/v1/invoices?${search.toString()}`);
50
+ const data = (root as { data?: unknown[] } | null)?.data;
51
+ return matchesResult(data, filterStripeInvoices, p);
52
+ },
53
+ );
54
+ }
@@ -1,22 +1,11 @@
1
1
  import {
2
- asRecord,
3
2
  makeQueryFilter,
3
+ recordFieldsFromKeys,
4
4
  type SearchMatchOptions,
5
5
  } from "../../../shared/search-filter.ts";
6
6
 
7
7
  export type SupersetSearchMatchOptions = SearchMatchOptions;
8
8
 
9
- function stringAt(row: Record<string, unknown>, key: string): string {
10
- const v = row[key];
11
- return typeof v === "string" ? v : "";
12
- }
13
-
14
- function fieldsOf(item: unknown): readonly string[] | null {
15
- const row = asRecord(item);
16
- if (row === undefined) {
17
- return null;
18
- }
19
- return [stringAt(row, "dashboard_title"), stringAt(row, "slug")];
20
- }
21
-
22
- export const filterSupersetDashboards = makeQueryFilter(fieldsOf);
9
+ export const filterSupersetDashboards = makeQueryFilter(
10
+ recordFieldsFromKeys(["dashboard_title", "slug"]),
11
+ );