@nimbus-dev/connectors 0.2.0 → 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 +18 -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 +15 -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,482 @@
1
+ import { z } from "zod";
2
+ import type { ConsentServer } from "../../../shared/consent-kit.ts";
3
+ import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
4
+ import {
5
+ createRegisterSimpleTool,
6
+ createZodToolRegistrar,
7
+ mcpJsonResult as jsonResult,
8
+ requireProcessEnv,
9
+ } from "../../../shared/mcp-tool-kit.ts";
10
+
11
+ const LINEAR_GQL = "https://api.linear.app/graphql";
12
+
13
+ type LinearGqlResponse<T> = {
14
+ data?: T;
15
+ errors?: ReadonlyArray<{ message: string }>;
16
+ };
17
+
18
+ async function linearGraphql<T>(
19
+ apiKey: string,
20
+ query: string,
21
+ variables?: Record<string, unknown>,
22
+ ): Promise<{ ok: true; data: T; text: string } | { ok: false; status: number; text: string }> {
23
+ const res = await fetch(LINEAR_GQL, {
24
+ method: "POST",
25
+ headers: {
26
+ "Content-Type": "application/json",
27
+ Authorization: apiKey,
28
+ },
29
+ body: JSON.stringify({ query, variables }),
30
+ });
31
+ const text = await res.text();
32
+ if (!res.ok) {
33
+ return { ok: false, status: res.status, text };
34
+ }
35
+ let parsed: unknown;
36
+ try {
37
+ parsed = JSON.parse(text) as unknown;
38
+ } catch {
39
+ return { ok: false, status: res.status, text: "invalid JSON" };
40
+ }
41
+ const body = parsed as LinearGqlResponse<T>;
42
+ if (body.errors !== undefined && body.errors.length > 0) {
43
+ const msg = body.errors.map((e) => e.message).join("; ");
44
+ throw new Error(`Linear GraphQL: ${msg.slice(0, 400)}`);
45
+ }
46
+ if (body.data === undefined) {
47
+ throw new Error("Linear GraphQL: missing data");
48
+ }
49
+ return { ok: true, data: body.data, text };
50
+ }
51
+
52
+ function linearGqlData<T>(
53
+ res: { ok: true; data: T; text: string } | { ok: false; status: number; text: string },
54
+ ): T {
55
+ if (!res.ok) {
56
+ throw new Error(`Linear ${String(res.status)}: ${res.text.slice(0, 300)}`);
57
+ }
58
+ return res.data;
59
+ }
60
+
61
+ /** Tool names exposed by this connector — for contract/introspection tests. */
62
+ export const LINEAR_TOOL_NAMES = [
63
+ "linear_issue_list",
64
+ "linear_issue_get",
65
+ "linear_issue_create",
66
+ "linear_issue_update",
67
+ "linear_comment_create",
68
+ "linear_project_list",
69
+ "linear_project_get",
70
+ "linear_cycle_list",
71
+ "linear_roadmap_list",
72
+ "linear_member_list",
73
+ ] as const;
74
+
75
+ export function registerLinearTools(
76
+ server: ConsentServer & { tool: (...args: never) => unknown },
77
+ ): void {
78
+ const registerSimpleTool = createRegisterSimpleTool(server);
79
+
80
+ const reg = createZodToolRegistrar(registerSimpleTool);
81
+
82
+ /**
83
+ * Every MUTATING linear tool goes through here. Outside the gateway this adds the
84
+ * consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
85
+ * the gateway it is a pass-through, because executor.ts (I2) is the gate there.
86
+ */
87
+ const registerWriteTool = createWriteToolRegistrar(server, {
88
+ connector: "linear",
89
+ scopeEnv: "NIMBUS_MCP_LINEAR_WRITE_SCOPE",
90
+ scopeKinds: ["team", "issue"],
91
+ });
92
+
93
+ const linearIssueListSchema = z.object({
94
+ first: z.number().int().min(1).max(100).optional(),
95
+ teamId: z.string().min(1).optional(),
96
+ stateName: z.string().min(1).optional(),
97
+ assigneeId: z.string().min(1).optional(),
98
+ });
99
+
100
+ reg(
101
+ "linear_issue_list",
102
+ "List Linear issues with optional filters (team id, state name, assignee id).",
103
+ linearIssueListSchema,
104
+ async (parsed) => {
105
+ const apiKey = requireProcessEnv("LINEAR_API_KEY");
106
+ const filter: Record<string, unknown> = {};
107
+ if (parsed.teamId !== undefined) {
108
+ filter["team"] = { id: { eq: parsed.teamId } };
109
+ }
110
+ if (parsed.stateName !== undefined) {
111
+ filter["state"] = { name: { eq: parsed.stateName } };
112
+ }
113
+ if (parsed.assigneeId !== undefined) {
114
+ filter["assignee"] = { id: { eq: parsed.assigneeId } };
115
+ }
116
+ const hasFilter = Object.keys(filter).length > 0;
117
+ const q = hasFilter
118
+ ? `
119
+ query IssueList($first: Int!, $filter: IssueFilter!) {
120
+ issues(first: $first, filter: $filter, orderBy: updatedAt) {
121
+ nodes {
122
+ id
123
+ identifier
124
+ title
125
+ updatedAt
126
+ url
127
+ state { id name type }
128
+ team { id name key }
129
+ }
130
+ }
131
+ }
132
+ `
133
+ : `
134
+ query IssueList($first: Int!) {
135
+ issues(first: $first, orderBy: updatedAt) {
136
+ nodes {
137
+ id
138
+ identifier
139
+ title
140
+ updatedAt
141
+ url
142
+ state { id name type }
143
+ team { id name key }
144
+ }
145
+ }
146
+ }
147
+ `;
148
+ type Out = {
149
+ issues: {
150
+ nodes: ReadonlyArray<Record<string, unknown>>;
151
+ };
152
+ };
153
+ const vars: Record<string, unknown> = { first: parsed.first ?? 50 };
154
+ if (hasFilter) {
155
+ vars["filter"] = filter;
156
+ }
157
+ const res = await linearGraphql<Out>(apiKey, q, vars);
158
+ return jsonResult(linearGqlData(res));
159
+ },
160
+ );
161
+
162
+ const linearIssueIdSchema = z.object({ issueId: z.string().min(1) });
163
+
164
+ reg(
165
+ "linear_issue_get",
166
+ "Get a single Linear issue by UUID id.",
167
+ linearIssueIdSchema,
168
+ async (parsed) => {
169
+ const apiKey = requireProcessEnv("LINEAR_API_KEY");
170
+ const q = `
171
+ query IssueGet($id: String!) {
172
+ issue(id: $id) {
173
+ id
174
+ identifier
175
+ title
176
+ description
177
+ updatedAt
178
+ createdAt
179
+ url
180
+ priority
181
+ state { id name type }
182
+ team { id name key }
183
+ assignee { id name displayName }
184
+ }
185
+ }
186
+ `;
187
+ type Out = { issue: Record<string, unknown> | null };
188
+ const res = await linearGraphql<Out>(apiKey, q, { id: parsed.issueId });
189
+ return jsonResult(linearGqlData(res));
190
+ },
191
+ );
192
+
193
+ const linearIssueCreateSchema = z.object({
194
+ teamId: z.string().min(1),
195
+ title: z.string().min(1),
196
+ description: z.string().optional(),
197
+ priority: z.number().int().min(0).max(4).optional(),
198
+ stateId: z.string().min(1).optional(),
199
+ assigneeId: z.string().min(1).optional(),
200
+ });
201
+
202
+ registerWriteTool(
203
+ "linear_issue_create",
204
+ {
205
+ mutates: "linear.issue.create",
206
+ recoverable: true,
207
+ scopeTargetOf: (p) => ({ kind: "team", value: p.teamId }),
208
+ },
209
+ "Create a Linear issue (requires teamId and title).",
210
+ linearIssueCreateSchema,
211
+ async (parsed) => {
212
+ const apiKey = requireProcessEnv("LINEAR_API_KEY");
213
+ const input: Record<string, unknown> = {
214
+ teamId: parsed.teamId,
215
+ title: parsed.title,
216
+ };
217
+ if (parsed.description !== undefined) {
218
+ input["description"] = parsed.description;
219
+ }
220
+ if (parsed.priority !== undefined) {
221
+ input["priority"] = parsed.priority;
222
+ }
223
+ if (parsed.stateId !== undefined) {
224
+ input["stateId"] = parsed.stateId;
225
+ }
226
+ if (parsed.assigneeId !== undefined) {
227
+ input["assigneeId"] = parsed.assigneeId;
228
+ }
229
+ const q = `
230
+ mutation IssueCreate($input: IssueCreateInput!) {
231
+ issueCreate(input: $input) {
232
+ success
233
+ issue { id identifier url title }
234
+ }
235
+ }
236
+ `;
237
+ type Out = { issueCreate: { success: boolean; issue: Record<string, unknown> | null } };
238
+ const res = await linearGraphql<Out>(apiKey, q, { input });
239
+ return jsonResult(linearGqlData(res));
240
+ },
241
+ );
242
+
243
+ const linearIssueUpdateSchema = z.object({
244
+ issueId: z.string().min(1),
245
+ title: z.string().min(1).optional(),
246
+ description: z.string().optional(),
247
+ stateId: z.string().min(1).optional(),
248
+ priority: z.number().int().min(0).max(4).optional(),
249
+ assigneeId: z.string().min(1).optional(),
250
+ });
251
+
252
+ registerWriteTool(
253
+ "linear_issue_update",
254
+ {
255
+ mutates: "linear.issue.update",
256
+ recoverable: true,
257
+ scopeTargetOf: (p) => ({ kind: "issue", value: p.issueId }),
258
+ },
259
+ "Update a Linear issue by id.",
260
+ linearIssueUpdateSchema,
261
+ async (parsed) => {
262
+ const apiKey = requireProcessEnv("LINEAR_API_KEY");
263
+ const input: Record<string, unknown> = {};
264
+ if (parsed.title !== undefined) {
265
+ input["title"] = parsed.title;
266
+ }
267
+ if (parsed.description !== undefined) {
268
+ input["description"] = parsed.description;
269
+ }
270
+ if (parsed.stateId !== undefined) {
271
+ input["stateId"] = parsed.stateId;
272
+ }
273
+ if (parsed.priority !== undefined) {
274
+ input["priority"] = parsed.priority;
275
+ }
276
+ if (parsed.assigneeId !== undefined) {
277
+ input["assigneeId"] = parsed.assigneeId;
278
+ }
279
+ const q = `
280
+ mutation IssueUpdate($id: String!, $input: IssueUpdateInput!) {
281
+ issueUpdate(id: $id, input: $input) {
282
+ success
283
+ issue { id identifier url title updatedAt }
284
+ }
285
+ }
286
+ `;
287
+ type Out = {
288
+ issueUpdate: { success: boolean; issue: Record<string, unknown> | null };
289
+ };
290
+ const res = await linearGraphql<Out>(apiKey, q, {
291
+ id: parsed.issueId,
292
+ input,
293
+ });
294
+ return jsonResult(linearGqlData(res));
295
+ },
296
+ );
297
+
298
+ const linearCommentCreateSchema = z.object({
299
+ issueId: z.string().min(1),
300
+ body: z.string().min(1),
301
+ });
302
+
303
+ registerWriteTool(
304
+ "linear_comment_create",
305
+ {
306
+ mutates: "linear.comment.create",
307
+ recoverable: true,
308
+ scopeTargetOf: (p) => ({ kind: "issue", value: p.issueId }),
309
+ },
310
+ "Add a comment to a Linear issue.",
311
+ linearCommentCreateSchema,
312
+ async (parsed) => {
313
+ const apiKey = requireProcessEnv("LINEAR_API_KEY");
314
+ const q = `
315
+ mutation CommentCreate($input: CommentCreateInput!) {
316
+ commentCreate(input: $input) {
317
+ success
318
+ comment { id body createdAt }
319
+ }
320
+ }
321
+ `;
322
+ type Out = {
323
+ commentCreate: { success: boolean; comment: Record<string, unknown> | null };
324
+ };
325
+ const res = await linearGraphql<Out>(apiKey, q, {
326
+ input: { issueId: parsed.issueId, body: parsed.body },
327
+ });
328
+ return jsonResult(linearGqlData(res));
329
+ },
330
+ );
331
+
332
+ const linearFirstOptionalSchema = z.object({
333
+ first: z.number().int().min(1).max(100).optional(),
334
+ });
335
+
336
+ reg("linear_project_list", "List Linear projects.", linearFirstOptionalSchema, async (parsed) => {
337
+ const apiKey = requireProcessEnv("LINEAR_API_KEY");
338
+ const q = `
339
+ query ProjectList($first: Int!) {
340
+ projects(first: $first) {
341
+ nodes {
342
+ id
343
+ name
344
+ description
345
+ slugId
346
+ url
347
+ updatedAt
348
+ state
349
+ }
350
+ }
351
+ }
352
+ `;
353
+ type Out = { projects: { nodes: ReadonlyArray<Record<string, unknown>> } };
354
+ const res = await linearGraphql<Out>(apiKey, q, { first: parsed.first ?? 50 });
355
+ return jsonResult(linearGqlData(res));
356
+ });
357
+
358
+ const linearProjectIdSchema = z.object({ projectId: z.string().min(1) });
359
+
360
+ reg(
361
+ "linear_project_get",
362
+ "Get a Linear project by id.",
363
+ linearProjectIdSchema,
364
+ async (parsed) => {
365
+ const apiKey = requireProcessEnv("LINEAR_API_KEY");
366
+ const q = `
367
+ query ProjectGet($id: String!) {
368
+ project(id: $id) {
369
+ id
370
+ name
371
+ description
372
+ slugId
373
+ url
374
+ updatedAt
375
+ state
376
+ lead { id displayName }
377
+ }
378
+ }
379
+ `;
380
+ type Out = { project: Record<string, unknown> | null };
381
+ const res = await linearGraphql<Out>(apiKey, q, { id: parsed.projectId });
382
+ return jsonResult(linearGqlData(res));
383
+ },
384
+ );
385
+
386
+ const linearCycleListSchema = z.object({
387
+ teamId: z.string().min(1),
388
+ first: z.number().int().min(1).max(50).optional(),
389
+ });
390
+
391
+ reg(
392
+ "linear_cycle_list",
393
+ "List Linear cycles for a team.",
394
+ linearCycleListSchema,
395
+ async (parsed) => {
396
+ const apiKey = requireProcessEnv("LINEAR_API_KEY");
397
+ const q = `
398
+ query CycleList($teamId: ID!, $first: Int!) {
399
+ team(id: $teamId) {
400
+ id
401
+ cycles(first: $first) {
402
+ nodes {
403
+ id
404
+ name
405
+ number
406
+ startsAt
407
+ endsAt
408
+ completedAt
409
+ }
410
+ }
411
+ }
412
+ }
413
+ `;
414
+ type Out = {
415
+ team: {
416
+ id: string;
417
+ cycles: { nodes: ReadonlyArray<Record<string, unknown>> };
418
+ } | null;
419
+ };
420
+ const res = await linearGraphql<Out>(apiKey, q, {
421
+ teamId: parsed.teamId,
422
+ first: parsed.first ?? 20,
423
+ });
424
+ return jsonResult(linearGqlData(res));
425
+ },
426
+ );
427
+
428
+ const linearRoadmapFirstSchema = z.object({
429
+ first: z.number().int().min(1).max(50).optional(),
430
+ });
431
+
432
+ reg(
433
+ "linear_roadmap_list",
434
+ "List Linear initiatives (roadmap).",
435
+ linearRoadmapFirstSchema,
436
+ async (parsed) => {
437
+ const apiKey = requireProcessEnv("LINEAR_API_KEY");
438
+ const q = `
439
+ query InitiativeList($first: Int!) {
440
+ initiatives(first: $first) {
441
+ nodes {
442
+ id
443
+ name
444
+ description
445
+ targetDate
446
+ updatedAt
447
+ url
448
+ }
449
+ }
450
+ }
451
+ `;
452
+ type Out = { initiatives: { nodes: ReadonlyArray<Record<string, unknown>> } };
453
+ const res = await linearGraphql<Out>(apiKey, q, { first: parsed.first ?? 30 });
454
+ return jsonResult(linearGqlData(res));
455
+ },
456
+ );
457
+
458
+ reg(
459
+ "linear_member_list",
460
+ "List users in the workspace.",
461
+ linearFirstOptionalSchema,
462
+ async (parsed) => {
463
+ const apiKey = requireProcessEnv("LINEAR_API_KEY");
464
+ const q = `
465
+ query Users($first: Int!) {
466
+ users(first: $first) {
467
+ nodes {
468
+ id
469
+ name
470
+ displayName
471
+ email
472
+ active
473
+ }
474
+ }
475
+ }
476
+ `;
477
+ type Out = { users: { nodes: ReadonlyArray<Record<string, unknown>> } };
478
+ const res = await linearGraphql<Out>(apiKey, q, { first: parsed.first ?? 50 });
479
+ return jsonResult(linearGqlData(res));
480
+ },
481
+ );
482
+ }
@@ -1,5 +1,7 @@
1
- import { readdir, readFile, stat } from "node:fs/promises";
2
- import { isAbsolute, join, relative, resolve, sep } from "node:path";
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { relative, resolve } from "node:path";
3
+ import { requiredEnv } from "../../../shared/env-json-api.ts";
4
+ import { assertWithinDir, byExtension, walkFiles } from "../../../shared/walk-files.ts";
3
5
 
4
6
  /**
5
7
  * Local-only reader for saved SQL queries. This module is the MCP-server-side
@@ -41,11 +43,7 @@ export function baseTitle(relativePath: string): string {
41
43
  * the MCP tools surface a clear error rather than reading an empty path.
42
44
  */
43
45
  export function scriptsDir(): string {
44
- const dir = process.env["LOCALDB_SCRIPTS_DIR"]?.trim();
45
- if (dir === undefined || dir === "") {
46
- throw new Error("LOCALDB_SCRIPTS_DIR is not set");
47
- }
48
- return resolve(dir);
46
+ return resolve(requiredEnv("LOCALDB_SCRIPTS_DIR"));
49
47
  }
50
48
 
51
49
  /**
@@ -53,41 +51,15 @@ export function scriptsDir(): string {
53
51
  * scripts dir itself or a descendant. Rejects `..`-escaping inputs.
54
52
  */
55
53
  export function assertWithinScriptsDir(candidate: string, root: string): void {
56
- const rel = relative(root, candidate);
57
- if (rel === "") {
58
- return;
59
- }
60
- if (rel.startsWith("..") || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
61
- throw new Error("path escapes the configured local DB scripts dir");
62
- }
54
+ assertWithinDir(candidate, root, "the configured local DB scripts dir");
63
55
  }
64
56
 
65
- async function collectSqlFiles(root: string): Promise<string[]> {
66
- const found: string[] = [];
67
- async function walk(dir: string, depth: number): Promise<void> {
68
- if (depth > MAX_WALK_DEPTH || found.length >= MAX_FILES) {
69
- return;
70
- }
71
- let entries: import("node:fs").Dirent[];
72
- try {
73
- entries = await readdir(dir, { withFileTypes: true });
74
- } catch {
75
- return;
76
- }
77
- for (const entry of entries) {
78
- if (found.length >= MAX_FILES) {
79
- return;
80
- }
81
- const full = join(dir, entry.name);
82
- if (entry.isDirectory()) {
83
- await walk(full, depth + 1);
84
- } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".sql")) {
85
- found.push(full);
86
- }
87
- }
88
- }
89
- await walk(root, 0);
90
- return found;
57
+ function collectSqlFiles(root: string): Promise<string[]> {
58
+ return walkFiles(root, {
59
+ maxFiles: MAX_FILES,
60
+ maxDepth: MAX_WALK_DEPTH,
61
+ select: byExtension(".sql"),
62
+ });
91
63
  }
92
64
 
93
65
  async function readSavedQuery(path: string, root: string): Promise<SavedQuery | null> {
@@ -127,7 +99,7 @@ export async function scanSavedQueries(): Promise<SavedQuery[]> {
127
99
  const out: SavedQuery[] = [];
128
100
  for (const file of files) {
129
101
  assertWithinScriptsDir(file, root);
130
- const q = await readSavedQuery(file, root);
102
+ const q = await readSavedQuery(file, root); // NOSONAR S9382: one file at a time on purpose — up to MAX_FILES (2000) files, each read whole before the 2 MiB cap is checked, so a fan-out would open thousands of descriptors and buffer every file at once.
131
103
  if (q !== null) {
132
104
  out.push(q);
133
105
  }
@@ -13,7 +13,7 @@ import {
13
13
  * configured local DB-tool scripts dir — a pure filesystem read. No database is
14
14
  * connected to and no query is executed; the SQL TEXT is returned for recall.
15
15
  */
16
- export const LOCALDB_TOOL_NAMES = ["localdb_list", "localdb_get", "localdb_search"] as const;
16
+ export const LOCALDB_TOOL_NAMES = ["localdb_get", "localdb_list", "localdb_search"] as const;
17
17
 
18
18
  function toEnvelope(q: SavedQuery): Record<string, unknown> {
19
19
  return {
@@ -1,22 +1,9 @@
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 LookerSearchMatchOptions = 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, "title"), stringAt(row, "id")];
20
- }
21
-
22
- export const filterLookerDashboards = makeQueryFilter(fieldsOf);
9
+ export const filterLookerDashboards = makeQueryFilter(recordFieldsFromKeys(["title", "id"]));