@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.
Files changed (301) hide show
  1. package/LICENSE +662 -0
  2. package/NOTICE +40 -0
  3. package/README.md +111 -0
  4. package/connectors/airflow/nimbus.extension.json +16 -0
  5. package/connectors/airflow/src/search-filter.ts +38 -0
  6. package/connectors/airflow/src/server.ts +91 -0
  7. package/connectors/apple/nimbus.extension.json +40 -0
  8. package/connectors/apple/src/apple-mail-core.ts +43 -0
  9. package/connectors/apple/src/caldav-core.ts +116 -0
  10. package/connectors/apple/src/calendar-tools.ts +278 -0
  11. package/connectors/apple/src/server.ts +541 -0
  12. package/connectors/apple/src/tools.ts +138 -0
  13. package/connectors/argocd/nimbus.extension.json +16 -0
  14. package/connectors/argocd/src/search-filter.ts +24 -0
  15. package/connectors/argocd/src/server.ts +158 -0
  16. package/connectors/athena/nimbus.extension.json +15 -0
  17. package/connectors/athena/src/server.ts +6 -0
  18. package/connectors/athena/src/tools.ts +181 -0
  19. package/connectors/aws/nimbus.extension.json +21 -0
  20. package/connectors/aws/src/server.ts +191 -0
  21. package/connectors/azure/nimbus.extension.json +15 -0
  22. package/connectors/azure/src/server.ts +139 -0
  23. package/connectors/bigeye/nimbus.extension.json +16 -0
  24. package/connectors/bigeye/src/search-filter.ts +22 -0
  25. package/connectors/bigeye/src/server.ts +161 -0
  26. package/connectors/bigquery/nimbus.extension.json +15 -0
  27. package/connectors/bigquery/src/server.ts +6 -0
  28. package/connectors/bigquery/src/tools.ts +175 -0
  29. package/connectors/bitbucket/nimbus.extension.json +15 -0
  30. package/connectors/bitbucket/src/server.ts +252 -0
  31. package/connectors/bitrise/nimbus.extension.json +15 -0
  32. package/connectors/bitrise/src/search-filter.ts +11 -0
  33. package/connectors/bitrise/src/server.ts +83 -0
  34. package/connectors/canva/nimbus.extension.json +15 -0
  35. package/connectors/canva/src/search-filter.ts +23 -0
  36. package/connectors/canva/src/server.ts +77 -0
  37. package/connectors/circleci/nimbus.extension.json +15 -0
  38. package/connectors/circleci/src/server.ts +184 -0
  39. package/connectors/cloud-logging/nimbus.extension.json +15 -0
  40. package/connectors/cloud-logging/src/server.ts +6 -0
  41. package/connectors/cloud-logging/src/tools.ts +115 -0
  42. package/connectors/cloudwatch/nimbus.extension.json +15 -0
  43. package/connectors/cloudwatch/src/server.ts +6 -0
  44. package/connectors/cloudwatch/src/tools.ts +144 -0
  45. package/connectors/codemagic/nimbus.extension.json +15 -0
  46. package/connectors/codemagic/src/search-filter.ts +11 -0
  47. package/connectors/codemagic/src/server.ts +74 -0
  48. package/connectors/confluence/nimbus.extension.json +15 -0
  49. package/connectors/confluence/src/kb-append.ts +25 -0
  50. package/connectors/confluence/src/server.ts +309 -0
  51. package/connectors/dagster/nimbus.extension.json +16 -0
  52. package/connectors/dagster/src/search-filter.ts +53 -0
  53. package/connectors/dagster/src/server.ts +218 -0
  54. package/connectors/databricks/nimbus.extension.json +16 -0
  55. package/connectors/databricks/src/search-filter.ts +28 -0
  56. package/connectors/databricks/src/server.ts +81 -0
  57. package/connectors/datadog/nimbus.extension.json +15 -0
  58. package/connectors/datadog/src/server.ts +56 -0
  59. package/connectors/dataprofile/nimbus.extension.json +16 -0
  60. package/connectors/dataprofile/src/profile.ts +298 -0
  61. package/connectors/dataprofile/src/server.ts +6 -0
  62. package/connectors/dataprofile/src/tools.ts +69 -0
  63. package/connectors/dbt/nimbus.extension.json +16 -0
  64. package/connectors/dbt/src/search-filter.ts +28 -0
  65. package/connectors/dbt/src/server.ts +96 -0
  66. package/connectors/dependencytrack/nimbus.extension.json +16 -0
  67. package/connectors/dependencytrack/src/search-filter.ts +24 -0
  68. package/connectors/dependencytrack/src/server.ts +80 -0
  69. package/connectors/discord/nimbus.extension.json +15 -0
  70. package/connectors/discord/src/server.ts +93 -0
  71. package/connectors/elasticsearch/nimbus.extension.json +16 -0
  72. package/connectors/elasticsearch/src/server.ts +6 -0
  73. package/connectors/elasticsearch/src/tools.ts +110 -0
  74. package/connectors/fastmail/nimbus.extension.json +16 -0
  75. package/connectors/fastmail/src/jmap-core.ts +86 -0
  76. package/connectors/fastmail/src/server.ts +194 -0
  77. package/connectors/fastmail/src/tools.ts +143 -0
  78. package/connectors/figma/nimbus.extension.json +15 -0
  79. package/connectors/figma/src/search-filter.ts +24 -0
  80. package/connectors/figma/src/server.ts +117 -0
  81. package/connectors/firebase/nimbus.extension.json +15 -0
  82. package/connectors/firebase/src/search-filter.ts +30 -0
  83. package/connectors/firebase/src/server.ts +94 -0
  84. package/connectors/firebase/src/token.ts +41 -0
  85. package/connectors/flagsmith/nimbus.extension.json +15 -0
  86. package/connectors/flagsmith/src/search-filter.ts +29 -0
  87. package/connectors/flagsmith/src/server.ts +102 -0
  88. package/connectors/flux/nimbus.extension.json +16 -0
  89. package/connectors/flux/src/search-filter.ts +42 -0
  90. package/connectors/flux/src/server.ts +182 -0
  91. package/connectors/gcp/nimbus.extension.json +15 -0
  92. package/connectors/gcp/src/server.ts +140 -0
  93. package/connectors/github/nimbus.extension.json +15 -0
  94. package/connectors/github/src/server.ts +347 -0
  95. package/connectors/github-actions/nimbus.extension.json +15 -0
  96. package/connectors/github-actions/src/server.ts +213 -0
  97. package/connectors/gitlab/nimbus.extension.json +15 -0
  98. package/connectors/gitlab/src/server.ts +392 -0
  99. package/connectors/gmail/nimbus.extension.json +15 -0
  100. package/connectors/gmail/src/server.ts +277 -0
  101. package/connectors/google-drive/nimbus.extension.json +15 -0
  102. package/connectors/google-drive/src/drive-query.ts +3 -0
  103. package/connectors/google-drive/src/server.ts +531 -0
  104. package/connectors/google-meet/nimbus.extension.json +15 -0
  105. package/connectors/google-meet/src/server.ts +89 -0
  106. package/connectors/google-photos/nimbus.extension.json +15 -0
  107. package/connectors/google-photos/src/server.ts +139 -0
  108. package/connectors/grafana/nimbus.extension.json +15 -0
  109. package/connectors/grafana/src/server.ts +61 -0
  110. package/connectors/great-expectations/nimbus.extension.json +16 -0
  111. package/connectors/great-expectations/src/gx-parse.ts +292 -0
  112. package/connectors/great-expectations/src/server.ts +6 -0
  113. package/connectors/great-expectations/src/tools.ts +91 -0
  114. package/connectors/greenhouse/nimbus.extension.json +15 -0
  115. package/connectors/greenhouse/src/search-filter.ts +65 -0
  116. package/connectors/greenhouse/src/server.ts +67 -0
  117. package/connectors/hubspot/nimbus.extension.json +15 -0
  118. package/connectors/hubspot/src/search-filter.ts +28 -0
  119. package/connectors/hubspot/src/server.ts +81 -0
  120. package/connectors/iac/nimbus.extension.json +14 -0
  121. package/connectors/iac/src/server.ts +145 -0
  122. package/connectors/imap/nimbus.extension.json +16 -0
  123. package/connectors/imap/src/imap-core.ts +110 -0
  124. package/connectors/imap/src/server.ts +232 -0
  125. package/connectors/imap/src/tools.ts +40 -0
  126. package/connectors/intercom/nimbus.extension.json +15 -0
  127. package/connectors/intercom/src/search-filter.ts +46 -0
  128. package/connectors/intercom/src/server.ts +65 -0
  129. package/connectors/jenkins/nimbus.extension.json +15 -0
  130. package/connectors/jenkins/src/jenkins-api.ts +156 -0
  131. package/connectors/jenkins/src/server.ts +223 -0
  132. package/connectors/jira/nimbus.extension.json +15 -0
  133. package/connectors/jira/src/server.ts +309 -0
  134. package/connectors/kubernetes/nimbus.extension.json +15 -0
  135. package/connectors/kubernetes/src/server.ts +163 -0
  136. package/connectors/launchdarkly/nimbus.extension.json +15 -0
  137. package/connectors/launchdarkly/src/search-filter.ts +31 -0
  138. package/connectors/launchdarkly/src/server.ts +79 -0
  139. package/connectors/lever/nimbus.extension.json +15 -0
  140. package/connectors/lever/src/search-filter.ts +35 -0
  141. package/connectors/lever/src/server.ts +64 -0
  142. package/connectors/linear/nimbus.extension.json +15 -0
  143. package/connectors/linear/src/server.ts +467 -0
  144. package/connectors/localdb/nimbus.extension.json +16 -0
  145. package/connectors/localdb/src/server.ts +6 -0
  146. package/connectors/localdb/src/sql-scan.ts +158 -0
  147. package/connectors/localdb/src/tools.ts +67 -0
  148. package/connectors/looker/nimbus.extension.json +16 -0
  149. package/connectors/looker/src/search-filter.ts +22 -0
  150. package/connectors/looker/src/server.ts +254 -0
  151. package/connectors/mendeley/nimbus.extension.json +15 -0
  152. package/connectors/mendeley/src/search-filter.ts +68 -0
  153. package/connectors/mendeley/src/server.ts +59 -0
  154. package/connectors/mercury/nimbus.extension.json +15 -0
  155. package/connectors/mercury/src/search-filter.ts +11 -0
  156. package/connectors/mercury/src/server.ts +61 -0
  157. package/connectors/metabase/nimbus.extension.json +16 -0
  158. package/connectors/metabase/src/search-filter.ts +22 -0
  159. package/connectors/metabase/src/server.ts +83 -0
  160. package/connectors/miro/nimbus.extension.json +15 -0
  161. package/connectors/miro/src/search-filter.ts +24 -0
  162. package/connectors/miro/src/server.ts +76 -0
  163. package/connectors/mlflow/nimbus.extension.json +16 -0
  164. package/connectors/mlflow/src/search-filter.ts +38 -0
  165. package/connectors/mlflow/src/server.ts +167 -0
  166. package/connectors/monte-carlo/nimbus.extension.json +16 -0
  167. package/connectors/monte-carlo/src/search-filter.ts +11 -0
  168. package/connectors/monte-carlo/src/server.ts +241 -0
  169. package/connectors/netlify/nimbus.extension.json +15 -0
  170. package/connectors/netlify/src/search-filter.ts +37 -0
  171. package/connectors/netlify/src/server.ts +64 -0
  172. package/connectors/newrelic/nimbus.extension.json +15 -0
  173. package/connectors/newrelic/src/server.ts +48 -0
  174. package/connectors/notion/nimbus.extension.json +15 -0
  175. package/connectors/notion/src/kb-append.ts +23 -0
  176. package/connectors/notion/src/server.ts +343 -0
  177. package/connectors/obsidian/nimbus.extension.json +19 -0
  178. package/connectors/obsidian/src/server.ts +433 -0
  179. package/connectors/onedrive/nimbus.extension.json +15 -0
  180. package/connectors/onedrive/src/server.ts +256 -0
  181. package/connectors/outlook/nimbus.extension.json +15 -0
  182. package/connectors/outlook/src/server.ts +358 -0
  183. package/connectors/outlook/src/tool-scope-policy.ts +53 -0
  184. package/connectors/pagerduty/nimbus.extension.json +15 -0
  185. package/connectors/pagerduty/src/server.ts +140 -0
  186. package/connectors/pipedrive/nimbus.extension.json +15 -0
  187. package/connectors/pipedrive/src/search-filter.ts +11 -0
  188. package/connectors/pipedrive/src/server.ts +62 -0
  189. package/connectors/powerbi/nimbus.extension.json +16 -0
  190. package/connectors/powerbi/src/search-filter.ts +22 -0
  191. package/connectors/powerbi/src/server.ts +249 -0
  192. package/connectors/prefect/nimbus.extension.json +16 -0
  193. package/connectors/prefect/src/search-filter.ts +40 -0
  194. package/connectors/prefect/src/server.ts +94 -0
  195. package/connectors/protonmail/nimbus.extension.json +16 -0
  196. package/connectors/protonmail/src/mail-core.ts +96 -0
  197. package/connectors/protonmail/src/server.ts +231 -0
  198. package/connectors/protonmail/src/tools.ts +45 -0
  199. package/connectors/raindrop/nimbus.extension.json +15 -0
  200. package/connectors/raindrop/src/search-filter.ts +20 -0
  201. package/connectors/raindrop/src/server.ts +107 -0
  202. package/connectors/ramp/nimbus.extension.json +15 -0
  203. package/connectors/ramp/src/search-filter.ts +37 -0
  204. package/connectors/ramp/src/server.ts +109 -0
  205. package/connectors/readwise/nimbus.extension.json +15 -0
  206. package/connectors/readwise/src/search-filter.ts +64 -0
  207. package/connectors/readwise/src/server.ts +92 -0
  208. package/connectors/sagemaker/nimbus.extension.json +15 -0
  209. package/connectors/sagemaker/src/server.ts +6 -0
  210. package/connectors/sagemaker/src/tools.ts +121 -0
  211. package/connectors/salesforce/nimbus.extension.json +15 -0
  212. package/connectors/salesforce/src/search-filter.ts +23 -0
  213. package/connectors/salesforce/src/server.ts +94 -0
  214. package/connectors/semgrep/nimbus.extension.json +15 -0
  215. package/connectors/semgrep/src/search-filter.ts +31 -0
  216. package/connectors/semgrep/src/server.ts +108 -0
  217. package/connectors/sentry/nimbus.extension.json +15 -0
  218. package/connectors/sentry/src/server.ts +66 -0
  219. package/connectors/slack/nimbus.extension.json +15 -0
  220. package/connectors/slack/src/server.ts +360 -0
  221. package/connectors/snowflake/nimbus.extension.json +16 -0
  222. package/connectors/snowflake/src/search-filter.ts +26 -0
  223. package/connectors/snowflake/src/server.ts +236 -0
  224. package/connectors/snyk/nimbus.extension.json +15 -0
  225. package/connectors/snyk/src/search-filter.ts +28 -0
  226. package/connectors/snyk/src/server.ts +144 -0
  227. package/connectors/sonarqube/nimbus.extension.json +15 -0
  228. package/connectors/sonarqube/src/search-filter.ts +28 -0
  229. package/connectors/sonarqube/src/server.ts +115 -0
  230. package/connectors/stackoverflow/nimbus.extension.json +15 -0
  231. package/connectors/stackoverflow/src/search-filter.ts +52 -0
  232. package/connectors/stackoverflow/src/server.ts +81 -0
  233. package/connectors/storybook/nimbus.extension.json +16 -0
  234. package/connectors/storybook/src/server.ts +6 -0
  235. package/connectors/storybook/src/storybook-parse.ts +67 -0
  236. package/connectors/storybook/src/tools.ts +68 -0
  237. package/connectors/stripe/nimbus.extension.json +15 -0
  238. package/connectors/stripe/src/search-filter.ts +19 -0
  239. package/connectors/stripe/src/server.ts +65 -0
  240. package/connectors/superset/nimbus.extension.json +16 -0
  241. package/connectors/superset/src/search-filter.ts +22 -0
  242. package/connectors/superset/src/server.ts +119 -0
  243. package/connectors/tableau/nimbus.extension.json +16 -0
  244. package/connectors/tableau/src/search-filter.ts +22 -0
  245. package/connectors/tableau/src/server.ts +248 -0
  246. package/connectors/teams/nimbus.extension.json +20 -0
  247. package/connectors/teams/src/bot-send.ts +72 -0
  248. package/connectors/teams/src/server.ts +257 -0
  249. package/connectors/testflight/nimbus.extension.json +15 -0
  250. package/connectors/testflight/src/jwt.ts +27 -0
  251. package/connectors/testflight/src/search-filter.ts +30 -0
  252. package/connectors/testflight/src/server.ts +74 -0
  253. package/connectors/vercel/nimbus.extension.json +15 -0
  254. package/connectors/vercel/src/search-filter.ts +34 -0
  255. package/connectors/vercel/src/server.ts +79 -0
  256. package/connectors/vertex-ai/nimbus.extension.json +15 -0
  257. package/connectors/vertex-ai/src/server.ts +6 -0
  258. package/connectors/vertex-ai/src/tools.ts +145 -0
  259. package/connectors/wiz/nimbus.extension.json +15 -0
  260. package/connectors/wiz/src/search-filter.ts +50 -0
  261. package/connectors/wiz/src/server.ts +192 -0
  262. package/connectors/workday/nimbus.extension.json +19 -0
  263. package/connectors/workday/src/search-filter.ts +5 -0
  264. package/connectors/workday/src/server.ts +78 -0
  265. package/connectors/workday/src/tools.ts +1 -0
  266. package/connectors/zendesk/nimbus.extension.json +16 -0
  267. package/connectors/zendesk/src/search-filter.ts +11 -0
  268. package/connectors/zendesk/src/server.ts +77 -0
  269. package/connectors/zoom/nimbus.extension.json +15 -0
  270. package/connectors/zoom/src/encode-meeting-path.ts +5 -0
  271. package/connectors/zoom/src/recordings-window.ts +34 -0
  272. package/connectors/zoom/src/search-filter.ts +42 -0
  273. package/connectors/zoom/src/server.ts +98 -0
  274. package/connectors/zotero/nimbus.extension.json +15 -0
  275. package/connectors/zotero/src/search-filter.ts +81 -0
  276. package/connectors/zotero/src/server.ts +82 -0
  277. package/package.json +152 -0
  278. package/shared/atlassian-json-fetch.ts +42 -0
  279. package/shared/audit-chain.ts +133 -0
  280. package/shared/connector-mode.ts +60 -0
  281. package/shared/consent-kit.ts +323 -0
  282. package/shared/fetch-bearer-json.ts +18 -0
  283. package/shared/header-safe.ts +28 -0
  284. package/shared/imap-bodystructure.ts +113 -0
  285. package/shared/imap-mail-core.ts +57 -0
  286. package/shared/imap-tool-kit.ts +293 -0
  287. package/shared/join-api-path.ts +18 -0
  288. package/shared/kb-markdown.ts +142 -0
  289. package/shared/mcp-search-tool.ts +37 -0
  290. package/shared/mcp-tool-kit.ts +35 -0
  291. package/shared/nimbus-spawn.ts +128 -0
  292. package/shared/rest-tool-kit.ts +18 -0
  293. package/shared/run-cli-json.ts +55 -0
  294. package/shared/run-read-only-mcp-connector.ts +59 -0
  295. package/shared/safe-cli-arg.ts +55 -0
  296. package/shared/search-filter.ts +140 -0
  297. package/shared/strip-trailing-slashes.ts +7 -0
  298. package/shared/write-scope.ts +58 -0
  299. package/standalone/src/bin.ts +19 -0
  300. package/standalone/src/launcher.ts +186 -0
  301. package/standalone/tsconfig.json +8 -0
@@ -0,0 +1,81 @@
1
+ import { z } from "zod";
2
+ import { 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 { filterDatabricksJobs } from "./search-filter.ts";
6
+
7
+ function trimTrailingSlash(s: string): string {
8
+ return s.endsWith("/") ? s.slice(0, -1) : s;
9
+ }
10
+
11
+ function apiBase(): string {
12
+ const v = process.env["DATABRICKS_HOST"]?.trim();
13
+ if (v === undefined || v === "") {
14
+ throw new Error("DATABRICKS_HOST is not set");
15
+ }
16
+ return trimTrailingSlash(v);
17
+ }
18
+
19
+ function authHeader(): Record<string, string> {
20
+ const k = process.env["DATABRICKS_TOKEN"]?.trim();
21
+ if (k === undefined || k === "") {
22
+ throw new Error("DATABRICKS_TOKEN is not set");
23
+ }
24
+ return { Authorization: `Bearer ${k}`, Accept: "application/json" };
25
+ }
26
+
27
+ async function dbGet(path: string): Promise<unknown> {
28
+ const res = await fetch(`${apiBase()}${path}`, { headers: authHeader() });
29
+ const text = await res.text();
30
+ if (!res.ok) {
31
+ throw new Error(`Databricks ${String(res.status)}: ${text.slice(0, 400)}`);
32
+ }
33
+ return JSON.parse(text) as unknown;
34
+ }
35
+
36
+ function jobsFrom(root: unknown): unknown[] {
37
+ const jobs = (root as { jobs?: unknown } | null)?.jobs;
38
+ return Array.isArray(jobs) ? jobs : [];
39
+ }
40
+
41
+ await runReadOnlyMcpConnector("nimbus-databricks", (reg) => {
42
+ reg(
43
+ "databricks_list",
44
+ "List Databricks jobs (`GET /api/2.1/jobs/list`). Returns a single page; `limit` (1..100, default 25) caps the page size. Returns the raw `{ jobs, has_more, next_page_token }` envelope.",
45
+ z.object({
46
+ limit: z.number().int().min(1).max(100).optional(),
47
+ }),
48
+ async (p) => {
49
+ const limit = p.limit ?? 25;
50
+ const params = new URLSearchParams({ limit: String(limit) });
51
+ return jsonResult(await dbGet(`/api/2.1/jobs/list?${params.toString()}`));
52
+ },
53
+ );
54
+
55
+ reg(
56
+ "databricks_get",
57
+ "Fetch one Databricks job by numeric `jobId` (`GET /api/2.1/jobs/get?job_id={jobId}`). Throws when no job with that id exists.",
58
+ z.object({
59
+ jobId: z.number().int(),
60
+ }),
61
+ async (p) => {
62
+ const params = new URLSearchParams({ job_id: String(p.jobId) });
63
+ return jsonResult(await dbGet(`/api/2.1/jobs/get?${params.toString()}`));
64
+ },
65
+ );
66
+
67
+ reg(
68
+ "databricks_search",
69
+ "Substring search across Databricks jobs. Matches the query (case-insensitive) against the job's `settings.name`, `creator_user_name`, and `job_id`. Returns a `{ matches: [...] }` envelope.",
70
+ searchToolInputSchema(100),
71
+ async (p) => {
72
+ const params = new URLSearchParams({ limit: "100" });
73
+ const root = await dbGet(`/api/2.1/jobs/list?${params.toString()}`);
74
+ const matches = filterDatabricksJobs(jobsFrom(root), {
75
+ query: p.query,
76
+ limit: p.limit,
77
+ });
78
+ return jsonResult({ matches });
79
+ },
80
+ );
81
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ "id": "com.nimbus.datadog",
3
+ "displayName": "Datadog",
4
+ "version": "0.1.0",
5
+ "description": "Datadog API v1/v2 connector (monitors, incidents). Non-us1 regional sites (DD_SITE = eu1/us3/us5/ap1/gov) are a known Task 14 follow-up. Read-focused.",
6
+ "author": "Nimbus",
7
+ "entrypoint": "dist/server.js",
8
+ "runtime": "bun",
9
+ "permissions": {
10
+ "network": ["api.datadoghq.com"]
11
+ },
12
+ "hitlRequired": [],
13
+ "syncInterval": 300,
14
+ "minNimbusVersion": "0.2.0"
15
+ }
@@ -0,0 +1,56 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+
5
+ import {
6
+ createRegisterSimpleTool,
7
+ createZodToolRegistrar,
8
+ mcpJsonResult as jsonResult,
9
+ } from "../../../shared/mcp-tool-kit.ts";
10
+
11
+ function siteHost(): string {
12
+ const s = process.env["DD_SITE"]?.trim() || "datadoghq.com";
13
+ return `api.${s}`;
14
+ }
15
+
16
+ function headers(): Record<string, string> {
17
+ const ak = process.env["DD_API_KEY"]?.trim();
18
+ const app = process.env["DD_APP_KEY"]?.trim();
19
+ if (ak === undefined || ak === "" || app === undefined || app === "") {
20
+ throw new Error("DD_API_KEY and DD_APP_KEY must be set");
21
+ }
22
+ return {
23
+ "DD-API-KEY": ak,
24
+ "DD-APPLICATION-KEY": app,
25
+ Accept: "application/json",
26
+ };
27
+ }
28
+
29
+ async function ddGet(path: string): Promise<unknown> {
30
+ const res = await fetch(`https://${siteHost()}${path}`, { headers: headers() });
31
+ const text = await res.text();
32
+ if (!res.ok) {
33
+ throw new Error(`Datadog ${String(res.status)}: ${text.slice(0, 400)}`);
34
+ }
35
+ return JSON.parse(text) as unknown;
36
+ }
37
+
38
+ const mcp = new McpServer({ name: "nimbus-datadog", version: "0.1.0" });
39
+ const reg = createZodToolRegistrar(createRegisterSimpleTool(mcp));
40
+
41
+ reg("datadog_monitor_list", "List monitors.", z.object({}), async () =>
42
+ jsonResult(await ddGet("/api/v1/monitor")),
43
+ );
44
+
45
+ reg(
46
+ "datadog_incident_list",
47
+ "List incidents (v2).",
48
+ z.object({ limit: z.number().int().min(1).max(50).optional() }),
49
+ async (p) => {
50
+ const lim = p.limit ?? 10;
51
+ return jsonResult(await ddGet(`/api/v2/incidents?page[size]=${String(lim)}`));
52
+ },
53
+ );
54
+
55
+ const transport = new StdioServerTransport();
56
+ await mcp.connect(transport);
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "com.nimbus.dataprofile",
3
+ "displayName": "Local Data Profiling",
4
+ "version": "0.1.0",
5
+ "description": "Local data profiling connector (Tier-5 local, no-row-data class). Profiles local data files — `.parquet`, `.csv`, `.jsonl`/`.ndjson`, `.json` — under a configured directory into `dataprofile:data_model` items carrying the SCHEMA only: column names, column types, column count, a row-count ESTIMATE, and file size. It NEVER indexes cell values, row samples, first-N-row previews, or header-row data values. Parquet schema + row count come from the file FOOTER metadata (read via hyparquet — no row data crosses the wire); CSV column names come from the header line; JSONL/JSON field names + JS types come from the top-level structure (keys/types only). It is a pure, bounded, path-traversal-guarded filesystem read: NO database, NO network, NO row-fetch or row-sample tool (a contract test asserts the surface has none). The user points the connector at the directory holding their data files via the non-secret path `dataprofile.dir`; read tools are `dataprofile_list`, `dataprofile_get`, `dataprofile_search`. The configured directory arrives as the scoped environment variable DATAPROFILE_DIR and is added to the sandbox filesystem.read allow-list at spawn time. No network access. (ORC is deferred — no maintained pure-JS ORC schema reader exists.)",
6
+ "author": "Nimbus",
7
+ "entrypoint": "dist/server.js",
8
+ "runtime": "bun",
9
+ "permissions": {
10
+ "network": [],
11
+ "filesystem": { "read": [], "write": [] }
12
+ },
13
+ "hitlRequired": [],
14
+ "syncInterval": 600,
15
+ "minNimbusVersion": "0.2.0"
16
+ }
@@ -0,0 +1,298 @@
1
+ import type { FileHandle } from "node:fs/promises";
2
+ import { open, readdir } from "node:fs/promises";
3
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import {
5
+ type DataColumn,
6
+ firstLineAndRows,
7
+ type ParquetMetadataLike,
8
+ parquetColumnsFromMetadata,
9
+ parseCsvHeader,
10
+ parseJsonColumns,
11
+ parseJsonlColumns,
12
+ } from "@nimbus-dev/sdk";
13
+
14
+ /**
15
+ * Local data profiling reader (Tier-5, no-row-data). The MCP-server-side
16
+ * counterpart of the gateway's `mapDataModelToItem` mapper: it profiles local
17
+ * data files (`.parquet`, `.csv`, `.jsonl`, `.json`) under the configured dir
18
+ * into SCHEMA-only views — column names/types, column count, a row-count
19
+ * ESTIMATE, file size.
20
+ *
21
+ * HARD SCOPE CONSTRAINT (security): NEVER reads cell values / row samples /
22
+ * first-N-row previews. Parquet schema comes from the footer; CSV column names
23
+ * from the header line; JSONL/JSON field names + JS kinds from the top-level
24
+ * structure (keys/types only). A pure, bounded, path-traversal-guarded read.
25
+ */
26
+
27
+ const MAX_FILES = 2000;
28
+ const MAX_WALK_DEPTH = 12;
29
+ const MAX_TEXT_BYTES = 64 * 1024 * 1024;
30
+ const HEADER_PEEK_BYTES = 64 * 1024;
31
+
32
+ export type DataFileFormat = "parquet" | "csv" | "jsonl" | "json";
33
+
34
+ const EXT_FORMAT: Record<string, DataFileFormat> = {
35
+ ".parquet": "parquet",
36
+ ".csv": "csv",
37
+ ".jsonl": "jsonl",
38
+ ".ndjson": "jsonl",
39
+ ".json": "json",
40
+ };
41
+
42
+ export { jsKind } from "@nimbus-dev/sdk";
43
+ export type { DataColumn, ParquetMetadataLike };
44
+ export { parquetColumnsFromMetadata, parseCsvHeader, parseJsonColumns, parseJsonlColumns };
45
+
46
+ export interface DataModel {
47
+ readonly relativePath: string;
48
+ readonly format: DataFileFormat;
49
+ readonly columns: readonly DataColumn[];
50
+ readonly columnCount: number;
51
+ readonly rowCountEstimate: number | null;
52
+ readonly sizeBytes: number;
53
+ }
54
+
55
+ export function dataDir(): string {
56
+ const dir = process.env["DATAPROFILE_DIR"]?.trim();
57
+ if (dir === undefined || dir === "") {
58
+ throw new Error("DATAPROFILE_DIR is not set");
59
+ }
60
+ return resolve(dir);
61
+ }
62
+
63
+ export function assertWithinDataDir(candidate: string, root: string): void {
64
+ const rel = relative(root, candidate);
65
+ if (rel === "") {
66
+ return;
67
+ }
68
+ if (rel.startsWith("..") || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
69
+ throw new Error("path escapes the configured data-profile dir");
70
+ }
71
+ }
72
+
73
+ function extOf(name: string): string {
74
+ const i = name.lastIndexOf(".");
75
+ return i < 0 ? "" : name.slice(i).toLowerCase();
76
+ }
77
+
78
+ /** Reads Parquet footer metadata (schema + row count) WITHOUT reading row data. Injectable for tests. */
79
+ export type ParquetMetadataReader = (path: string) => Promise<ParquetMetadataLike | null>;
80
+
81
+ async function readParquetMetadata(path: string): Promise<ParquetMetadataLike | null> {
82
+ const { asyncBufferFromFile, parquetMetadataAsync } = await import("hyparquet");
83
+ try {
84
+ const buf = await asyncBufferFromFile(path);
85
+ return await parquetMetadataAsync(buf);
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ interface FileSlurp {
92
+ readonly text: string;
93
+ readonly sizeBytes: number;
94
+ readonly truncated: boolean;
95
+ }
96
+
97
+ /**
98
+ * Open the file ONCE and read its content + size from that single handle
99
+ * (`fh.stat()` + `fh.readFile()`/`fh.read()`). Operating on one open descriptor
100
+ * avoids a check-then-use (TOCTOU) race — there is no window where the path is
101
+ * stat-ed and then re-opened. Whole content for files ≤ cap; a header-only peek
102
+ * (no row-count estimate) for larger files.
103
+ */
104
+ async function slurpFile(path: string): Promise<FileSlurp | null> {
105
+ let fh: FileHandle;
106
+ try {
107
+ fh = await open(path, "r");
108
+ } catch {
109
+ return null;
110
+ }
111
+ try {
112
+ const st = await fh.stat();
113
+ const sizeBytes = Number.isFinite(st.size) ? st.size : 0;
114
+ if (sizeBytes <= MAX_TEXT_BYTES) {
115
+ const buf = await fh.readFile();
116
+ return { text: buf.toString("utf8"), sizeBytes, truncated: false };
117
+ }
118
+ const { buffer, bytesRead } = await fh.read(
119
+ Buffer.alloc(HEADER_PEEK_BYTES),
120
+ 0,
121
+ HEADER_PEEK_BYTES,
122
+ 0,
123
+ );
124
+ return { text: buffer.subarray(0, bytesRead).toString("utf8"), sizeBytes, truncated: true };
125
+ } catch {
126
+ return null;
127
+ } finally {
128
+ await fh.close();
129
+ }
130
+ }
131
+
132
+ /** fstat-only via an open handle (race-free) — for parquet, whose content hyparquet reads itself. */
133
+ async function sizeViaHandle(path: string): Promise<number | null> {
134
+ let fh: FileHandle;
135
+ try {
136
+ fh = await open(path, "r");
137
+ } catch {
138
+ return null;
139
+ }
140
+ try {
141
+ const st = await fh.stat();
142
+ return Number.isFinite(st.size) ? st.size : 0;
143
+ } finally {
144
+ await fh.close();
145
+ }
146
+ }
147
+
148
+ /** Profile fields extracted from a file's content (path/format applied by the caller). */
149
+ interface ProfileFields {
150
+ columns: DataColumn[];
151
+ rowCountEstimate: number | null;
152
+ sizeBytes: number;
153
+ }
154
+
155
+ async function profileParquet(
156
+ path: string,
157
+ readParquet: ParquetMetadataReader,
158
+ ): Promise<ProfileFields | null> {
159
+ const meta = await readParquet(path);
160
+ if (meta === null) {
161
+ return null;
162
+ }
163
+ const { columns, rowCountEstimate } = parquetColumnsFromMetadata(meta);
164
+ const size = await sizeViaHandle(path);
165
+ if (size === null) {
166
+ return null;
167
+ }
168
+ return { columns, rowCountEstimate, sizeBytes: size };
169
+ }
170
+
171
+ async function profileTextFile(
172
+ path: string,
173
+ format: Exclude<DataFileFormat, "parquet">,
174
+ ): Promise<ProfileFields | null> {
175
+ const slurp = await slurpFile(path);
176
+ if (slurp === null) {
177
+ return null;
178
+ }
179
+ if (format === "json") {
180
+ if (slurp.truncated) {
181
+ return null;
182
+ }
183
+ const { columns, rowCountEstimate } = parseJsonColumns(JSON.parse(slurp.text) as unknown);
184
+ return { columns, rowCountEstimate, sizeBytes: slurp.sizeBytes };
185
+ }
186
+ const { firstLine, rowCountEstimate: rows } = firstLineAndRows(slurp.text, slurp.truncated);
187
+ const columns = format === "csv" ? parseCsvHeader(firstLine) : parseJsonlColumns(firstLine);
188
+ const rowCountEstimate = format === "csv" && rows !== null ? Math.max(0, rows - 1) : rows;
189
+ return { columns, rowCountEstimate, sizeBytes: slurp.sizeBytes };
190
+ }
191
+
192
+ async function profileFile(
193
+ path: string,
194
+ root: string,
195
+ format: DataFileFormat,
196
+ readParquet: ParquetMetadataReader,
197
+ ): Promise<DataModel | null> {
198
+ const relativePath = relative(root, path);
199
+ let fields: ProfileFields | null;
200
+ try {
201
+ fields =
202
+ format === "parquet"
203
+ ? await profileParquet(path, readParquet)
204
+ : await profileTextFile(path, format);
205
+ } catch {
206
+ return null;
207
+ }
208
+ if (fields === null) {
209
+ return null;
210
+ }
211
+ return {
212
+ relativePath,
213
+ format,
214
+ columns: fields.columns,
215
+ columnCount: fields.columns.length,
216
+ rowCountEstimate: fields.rowCountEstimate,
217
+ sizeBytes: fields.sizeBytes,
218
+ };
219
+ }
220
+
221
+ async function collectDataFiles(
222
+ root: string,
223
+ ): Promise<Array<{ path: string; format: DataFileFormat }>> {
224
+ const found: Array<{ path: string; format: DataFileFormat }> = [];
225
+ async function walk(dir: string, depth: number): Promise<void> {
226
+ if (depth > MAX_WALK_DEPTH || found.length >= MAX_FILES) {
227
+ return;
228
+ }
229
+ let entries: import("node:fs").Dirent[];
230
+ try {
231
+ entries = await readdir(dir, { withFileTypes: true });
232
+ } catch {
233
+ return;
234
+ }
235
+ for (const entry of entries) {
236
+ if (found.length >= MAX_FILES) {
237
+ return;
238
+ }
239
+ const full = join(dir, entry.name);
240
+ if (entry.isDirectory()) {
241
+ await walk(full, depth + 1);
242
+ } else if (entry.isFile()) {
243
+ const format = EXT_FORMAT[extOf(entry.name)];
244
+ if (format !== undefined) {
245
+ found.push({ path: full, format });
246
+ }
247
+ }
248
+ }
249
+ }
250
+ await walk(root, 0);
251
+ return found;
252
+ }
253
+
254
+ /** Profile all data files under the configured dir into schema-only `DataModel` views. */
255
+ export async function listDataModels(
256
+ readParquet: ParquetMetadataReader = readParquetMetadata,
257
+ ): Promise<DataModel[]> {
258
+ const root = dataDir();
259
+ const files = await collectDataFiles(root);
260
+ const out: DataModel[] = [];
261
+ for (const { path, format } of files) {
262
+ assertWithinDataDir(path, root);
263
+ const model = await profileFile(path, root, format, readParquet);
264
+ if (model !== null) {
265
+ out.push(model);
266
+ }
267
+ }
268
+ return out;
269
+ }
270
+
271
+ /** Profile one data file by its relative path (path-guarded). */
272
+ export async function getDataModel(
273
+ relativePath: string,
274
+ readParquet: ParquetMetadataReader = readParquetMetadata,
275
+ ): Promise<DataModel | null> {
276
+ const root = dataDir();
277
+ const candidate = resolve(root, relativePath);
278
+ assertWithinDataDir(candidate, root);
279
+ const format = EXT_FORMAT[extOf(candidate)];
280
+ if (format === undefined) {
281
+ return null;
282
+ }
283
+ return profileFile(candidate, root, format, readParquet);
284
+ }
285
+
286
+ /** Substring search over data-model relative path / format / column names. */
287
+ export function filterDataModels(models: readonly DataModel[], query: string): DataModel[] {
288
+ const q = query.trim().toLowerCase();
289
+ if (q === "") {
290
+ return [...models];
291
+ }
292
+ return models.filter(
293
+ (m) =>
294
+ m.relativePath.toLowerCase().includes(q) ||
295
+ m.format.includes(q) ||
296
+ m.columns.some((c) => c.name.toLowerCase().includes(q)),
297
+ );
298
+ }
@@ -0,0 +1,6 @@
1
+ import { runReadOnlyMcpConnector } from "../../../shared/run-read-only-mcp-connector.ts";
2
+ import { registerDataprofileTools } from "./tools.ts";
3
+
4
+ await runReadOnlyMcpConnector("nimbus-dataprofile", (reg) => {
5
+ registerDataprofileTools(reg);
6
+ });
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ import { mcpJsonResult as jsonResult } from "../../../shared/mcp-tool-kit.ts";
3
+ import type { ZodToolRegistrar } from "../../../shared/run-read-only-mcp-connector.ts";
4
+ import { type DataModel, filterDataModels, getDataModel, listDataModels } from "./profile.ts";
5
+
6
+ /**
7
+ * Local data profiling MCP tool surface (Tier-5, no-row-data). Profiles local
8
+ * data files into SCHEMA-only views — column names/types, column count,
9
+ * row-count estimate, file size. NEVER returns cell values, row samples, or
10
+ * first-N-row previews; the tool names carry no row-fetch/sample segment (the
11
+ * `assertNoRowDataTools` contract test enforces this).
12
+ */
13
+ export const DATAPROFILE_TOOL_NAMES = [
14
+ "dataprofile_list",
15
+ "dataprofile_get",
16
+ "dataprofile_search",
17
+ ] as const;
18
+
19
+ function toEnvelope(m: DataModel): Record<string, unknown> {
20
+ return {
21
+ relativePath: m.relativePath,
22
+ format: m.format,
23
+ columns: m.columns.map((c) => ({ name: c.name, type: c.type })),
24
+ columnCount: m.columnCount,
25
+ rowCountEstimate: m.rowCountEstimate,
26
+ sizeBytes: m.sizeBytes,
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Register the read-only data-profiling tools onto the given registrar. Shared
32
+ * between `server.ts` (live) and the contract test (introspection).
33
+ */
34
+ export function registerDataprofileTools(reg: ZodToolRegistrar): void {
35
+ reg(
36
+ "dataprofile_list",
37
+ "List local data files (.parquet/.csv/.jsonl/.json) profiled under the configured dir. Returns each file's SCHEMA only — relative path, format, column names + types, column count, a row-count estimate, and file size. NEVER returns cell values, row samples, or row previews.",
38
+ z.object({ limit: z.number().int().min(1).max(2000).optional() }),
39
+ async (p) => {
40
+ const all = await listDataModels();
41
+ const limit = p.limit ?? 200;
42
+ return jsonResult({ items: all.slice(0, limit).map(toEnvelope) });
43
+ },
44
+ );
45
+
46
+ reg(
47
+ "dataprofile_get",
48
+ "Profile one local data file by its relative path (within the configured dir). Returns the SCHEMA only (columns + types + row-count estimate + size). NEVER returns cell values or row data.",
49
+ z.object({ relativePath: z.string().min(1).max(1024) }),
50
+ async (p) => {
51
+ const m = await getDataModel(p.relativePath);
52
+ return jsonResult(m === null ? { item: null } : { item: toEnvelope(m) });
53
+ },
54
+ );
55
+
56
+ reg(
57
+ "dataprofile_search",
58
+ "Substring search over profiled data files (matches relative path, format, and column names). Returns the same schema-only view as dataprofile_list.",
59
+ z.object({
60
+ query: z.string().min(1).max(500),
61
+ limit: z.number().int().min(1).max(2000).optional(),
62
+ }),
63
+ async (p) => {
64
+ const all = await listDataModels();
65
+ const matches = filterDataModels(all, p.query).slice(0, p.limit ?? 200);
66
+ return jsonResult({ matches: matches.map(toEnvelope) });
67
+ },
68
+ );
69
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "id": "com.nimbus.dbt",
3
+ "displayName": "dbt Cloud",
4
+ "version": "0.1.0",
5
+ "description": "dbt Cloud connector (read-only). Surfaces dbt Cloud jobs and their latest run status across accounts via the Administrative API v2 (`/api/v2/accounts/ → /api/v2/accounts/{id}/jobs/`, offset-paged) into the local index as `dbt:job` items (external_id `<accountId>:<jobId>`). Each job carries name, account/project/environment ids, dbt version, state (active/deleted), schedule cron, triggers, created/updated timestamps, the most-recent run status when present, and a canonical dbt Cloud job-UI URL. Authenticates with a dbt Cloud service/user API token sent as `Authorization: Token <token>` (the literal word `Token`, a space, then the raw token — not `Bearer`). Default host `https://cloud.getdbt.com`; regional / custom-access-URL instances (e.g. https://emea.dbt.com, https://au.dbt.com) override `dbt.api_base` — those non-default hosts inherit the same Task-14 runtime-merge follow-up as `sentry.url` (only the SaaS default host is in the static sandbox manifest today). Restrict to a single account via the optional `dbt.account_id` vault key; otherwise all accounts are discovered. v1 indexes jobs + run status only — model-level lineage (`data_model` items with upstream/downstream refs) requires the separate dbt Discovery GraphQL API and is a deferred follow-up. The `dbt.job.trigger` write tool is deferred to Phase 6 behind HITL.",
6
+ "author": "Nimbus",
7
+ "entrypoint": "dist/server.js",
8
+ "runtime": "bun",
9
+ "permissions": {
10
+ "network": ["cloud.getdbt.com"],
11
+ "filesystem": { "read": [], "write": [] }
12
+ },
13
+ "hitlRequired": [],
14
+ "syncInterval": 600,
15
+ "minNimbusVersion": "0.2.0"
16
+ }
@@ -0,0 +1,28 @@
1
+ import {
2
+ asObjectish,
3
+ makeQueryFilter,
4
+ type SearchMatchOptions,
5
+ } from "../../../shared/search-filter.ts";
6
+
7
+ export type DbtSearchMatchOptions = SearchMatchOptions;
8
+
9
+ function field(row: Record<string, unknown>, key: string): string {
10
+ const v = row[key];
11
+ if (typeof v === "string") {
12
+ return v;
13
+ }
14
+ if (typeof v === "number" && Number.isFinite(v)) {
15
+ return String(v);
16
+ }
17
+ return "";
18
+ }
19
+
20
+ function fieldsOf(item: unknown): readonly string[] | null {
21
+ const row = asObjectish(item);
22
+ if (row === undefined) {
23
+ return null;
24
+ }
25
+ return [field(row, "name"), field(row, "dbt_version"), field(row, "id")];
26
+ }
27
+
28
+ export const filterDbtJobs = makeQueryFilter(fieldsOf);
@@ -0,0 +1,96 @@
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 { filterDbtJobs } from "./search-filter.ts";
5
+
6
+ const DEFAULT_API_BASE = "https://cloud.getdbt.com";
7
+
8
+ function trimTrailingSlash(s: string): string {
9
+ return s.endsWith("/") ? s.slice(0, -1) : s;
10
+ }
11
+
12
+ function apiBase(): string {
13
+ const v = process.env["DBT_API_BASE"]?.trim();
14
+ return v === undefined || v === "" ? DEFAULT_API_BASE : trimTrailingSlash(v);
15
+ }
16
+
17
+ function authHeader(): Record<string, string> {
18
+ const t = process.env["DBT_TOKEN"]?.trim();
19
+ if (t === undefined || t === "") {
20
+ throw new Error("DBT_TOKEN is not set");
21
+ }
22
+ return { Authorization: `Token ${t}`, Accept: "application/json" };
23
+ }
24
+
25
+ async function dbtGet(path: string): Promise<unknown> {
26
+ const res = await fetch(`${apiBase()}/api/v2${path}`, { headers: authHeader() });
27
+ const text = await res.text();
28
+ if (!res.ok) {
29
+ throw new Error(`dbt Cloud ${String(res.status)}: ${text.slice(0, 400)}`);
30
+ }
31
+ return JSON.parse(text) as unknown;
32
+ }
33
+
34
+ function dataFrom(root: unknown): unknown[] {
35
+ if (Array.isArray(root)) {
36
+ return root;
37
+ }
38
+ const data = (root as { data?: unknown } | null)?.data;
39
+ return Array.isArray(data) ? data : [];
40
+ }
41
+
42
+ await runReadOnlyMcpConnector("nimbus-dbt", (reg) => {
43
+ reg(
44
+ "dbt_list",
45
+ "List dbt Cloud accounts, or jobs for an account. Without `accountId`, returns the account's accounts (`/accounts/`). With `accountId`, returns that account's jobs (`/accounts/{accountId}/jobs/`), capped at `limit` (default 100). Returns the raw dbt Cloud envelope (`{ data: [...] }`).",
46
+ z.object({
47
+ accountId: z.number().int().optional(),
48
+ limit: z.number().int().min(1).max(500).optional(),
49
+ }),
50
+ async (p) => {
51
+ if (p.accountId === undefined) {
52
+ return jsonResult(await dbtGet("/accounts/"));
53
+ }
54
+ const search = new URLSearchParams({ limit: String(p.limit ?? 100) });
55
+ return jsonResult(
56
+ await dbtGet(
57
+ `/accounts/${encodeURIComponent(String(p.accountId))}/jobs/?${search.toString()}`,
58
+ ),
59
+ );
60
+ },
61
+ );
62
+
63
+ reg(
64
+ "dbt_get",
65
+ "Fetch one dbt Cloud job by `accountId` + `jobId` (`/accounts/{accountId}/jobs/{jobId}/`). Returns the raw dbt Cloud envelope. Throws when no such job exists.",
66
+ z.object({
67
+ accountId: z.number().int(),
68
+ jobId: z.number().int(),
69
+ }),
70
+ async (p) => {
71
+ return jsonResult(
72
+ await dbtGet(
73
+ `/accounts/${encodeURIComponent(String(p.accountId))}/jobs/${encodeURIComponent(String(p.jobId))}/`,
74
+ ),
75
+ );
76
+ },
77
+ );
78
+
79
+ reg(
80
+ "dbt_search",
81
+ "Substring search across an account's dbt Cloud jobs. Matches the query against job name, dbt version, and id (case-insensitive). Returns a `{ matches: [...] }` envelope.",
82
+ z.object({
83
+ accountId: z.number().int(),
84
+ query: z.string().min(1),
85
+ limit: z.number().int().min(1).max(200).optional(),
86
+ }),
87
+ async (p) => {
88
+ const search = new URLSearchParams({ limit: "500" });
89
+ const root = await dbtGet(
90
+ `/accounts/${encodeURIComponent(String(p.accountId))}/jobs/?${search.toString()}`,
91
+ );
92
+ const matches = filterDbtJobs(dataFrom(root), { query: p.query, limit: p.limit });
93
+ return jsonResult({ matches });
94
+ },
95
+ );
96
+ });