@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.
- package/README.md +18 -4
- package/connectors/airflow/src/search-filter.ts +2 -15
- package/connectors/airflow/src/server.ts +2 -87
- package/connectors/airflow/src/tools.ts +74 -0
- package/connectors/apple/src/adapters.ts +326 -0
- package/connectors/apple/src/apple-mail-core.ts +4 -8
- package/connectors/apple/src/calendar-tools.ts +28 -8
- package/connectors/apple/src/server.ts +23 -512
- package/connectors/apple/src/tools.ts +18 -44
- package/connectors/argocd/src/server.ts +25 -38
- package/connectors/athena/src/tools.ts +15 -34
- package/connectors/aws/src/server.ts +2 -183
- package/connectors/aws/src/tools.ts +197 -0
- package/connectors/azure/src/server.ts +2 -131
- package/connectors/azure/src/tools.ts +133 -0
- package/connectors/bigeye/src/search-filter.ts +4 -15
- package/connectors/bigeye/src/server.ts +38 -38
- package/connectors/bigquery/src/tools.ts +1 -26
- package/connectors/bitbucket/src/server.ts +2 -244
- package/connectors/bitbucket/src/tools.ts +241 -0
- package/connectors/bitrise/src/server.ts +2 -79
- package/connectors/bitrise/src/tools.ts +76 -0
- package/connectors/canva/src/search-filter.ts +2 -11
- package/connectors/canva/src/server.ts +2 -73
- package/connectors/canva/src/tools.ts +66 -0
- package/connectors/circleci/src/server.ts +2 -176
- package/connectors/circleci/src/tools.ts +193 -0
- package/connectors/cloud-logging/src/tools.ts +14 -27
- package/connectors/cloudwatch/src/tools.ts +15 -34
- package/connectors/codemagic/src/server.ts +2 -70
- package/connectors/codemagic/src/tools.ts +71 -0
- package/connectors/confluence/src/server.ts +2 -301
- package/connectors/confluence/src/tools.ts +320 -0
- package/connectors/dagster/src/server.ts +2 -214
- package/connectors/dagster/src/tools.ts +187 -0
- package/connectors/databricks/src/search-filter.ts +7 -7
- package/connectors/databricks/src/server.ts +2 -77
- package/connectors/databricks/src/tools.ts +66 -0
- package/connectors/datadog/src/server.ts +2 -48
- package/connectors/datadog/src/tools.ts +48 -0
- package/connectors/dataprofile/src/profile.ts +16 -46
- package/connectors/dbt/src/server.ts +2 -92
- package/connectors/dbt/src/tools.ts +80 -0
- package/connectors/dependencytrack/src/server.ts +2 -76
- package/connectors/dependencytrack/src/tools.ts +65 -0
- package/connectors/discord/src/server.ts +2 -85
- package/connectors/discord/src/tools.ts +82 -0
- package/connectors/elasticsearch/src/tools.ts +7 -37
- package/connectors/fastmail/src/jmap-client.ts +202 -0
- package/connectors/fastmail/src/jmap-core.ts +8 -21
- package/connectors/fastmail/src/server.ts +2 -187
- package/connectors/fastmail/src/tools.ts +4 -23
- package/connectors/figma/src/search-filter.ts +2 -11
- package/connectors/figma/src/server.ts +2 -113
- package/connectors/figma/src/tools.ts +99 -0
- package/connectors/firebase/src/server.ts +2 -90
- package/connectors/firebase/src/token.ts +2 -5
- package/connectors/firebase/src/tools.ts +106 -0
- package/connectors/flagsmith/src/server.ts +2 -98
- package/connectors/flagsmith/src/tools.ts +95 -0
- package/connectors/flux/src/server.ts +52 -53
- package/connectors/gcp/src/server.ts +2 -132
- package/connectors/gcp/src/tools.ts +123 -0
- package/connectors/github/src/server.ts +2 -328
- package/connectors/github/src/tools.ts +311 -0
- package/connectors/github-actions/src/server.ts +2 -205
- package/connectors/github-actions/src/tools.ts +209 -0
- package/connectors/gitlab/src/server.ts +2 -384
- package/connectors/gitlab/src/tools.ts +392 -0
- package/connectors/gmail/src/server.ts +2 -270
- package/connectors/gmail/src/tools.ts +209 -0
- package/connectors/google-drive/src/server.ts +2 -524
- package/connectors/google-drive/src/tools.ts +529 -0
- package/connectors/google-meet/src/server.ts +2 -82
- package/connectors/google-meet/src/tools.ts +87 -0
- package/connectors/google-photos/src/server.ts +2 -132
- package/connectors/google-photos/src/tools.ts +138 -0
- package/connectors/grafana/src/server.ts +2 -53
- package/connectors/grafana/src/tools.ts +53 -0
- package/connectors/great-expectations/src/gx-parse.ts +14 -45
- package/connectors/greenhouse/src/server.ts +2 -63
- package/connectors/greenhouse/src/tools.ts +68 -0
- package/connectors/hubspot/src/server.ts +2 -77
- package/connectors/hubspot/src/tools.ts +70 -0
- package/connectors/iac/src/server.ts +2 -137
- package/connectors/iac/src/tools.ts +152 -0
- package/connectors/imap/src/imap-core.ts +29 -82
- package/connectors/imap/src/server.ts +33 -220
- package/connectors/intercom/src/server.ts +2 -61
- package/connectors/intercom/src/tools.ts +58 -0
- package/connectors/jenkins/src/jenkins-api.ts +2 -6
- package/connectors/jenkins/src/server.ts +2 -215
- package/connectors/jenkins/src/tools.ts +240 -0
- package/connectors/jira/src/server.ts +2 -301
- package/connectors/jira/src/tools.ts +325 -0
- package/connectors/kubernetes/src/server.ts +2 -155
- package/connectors/kubernetes/src/tools.ts +159 -0
- package/connectors/launchdarkly/src/server.ts +2 -75
- package/connectors/launchdarkly/src/tools.ts +72 -0
- package/connectors/lever/src/search-filter.ts +4 -12
- package/connectors/lever/src/server.ts +2 -60
- package/connectors/lever/src/tools.ts +61 -0
- package/connectors/linear/src/server.ts +2 -459
- package/connectors/linear/src/tools.ts +482 -0
- package/connectors/localdb/src/sql-scan.ts +13 -41
- package/connectors/localdb/src/tools.ts +1 -1
- package/connectors/looker/src/search-filter.ts +2 -15
- package/connectors/looker/src/server.ts +35 -70
- package/connectors/mendeley/src/server.ts +2 -55
- package/connectors/mendeley/src/tools.ts +54 -0
- package/connectors/mercury/src/server.ts +2 -57
- package/connectors/mercury/src/tools.ts +50 -0
- package/connectors/metabase/src/search-filter.ts +4 -15
- package/connectors/metabase/src/server.ts +2 -79
- package/connectors/metabase/src/tools.ts +64 -0
- package/connectors/miro/src/server.ts +2 -72
- package/connectors/miro/src/tools.ts +65 -0
- package/connectors/mlflow/src/search-filter.ts +3 -7
- package/connectors/mlflow/src/server.ts +20 -40
- package/connectors/monte-carlo/src/server.ts +50 -57
- package/connectors/netlify/src/search-filter.ts +6 -14
- package/connectors/netlify/src/server.ts +2 -60
- package/connectors/netlify/src/tools.ts +53 -0
- package/connectors/newrelic/src/server.ts +2 -40
- package/connectors/newrelic/src/tools.ts +37 -0
- package/connectors/notion/src/server.ts +2 -335
- package/connectors/notion/src/tools.ts +344 -0
- package/connectors/obsidian/src/server.ts +2 -425
- package/connectors/obsidian/src/tools.ts +466 -0
- package/connectors/onedrive/src/server.ts +2 -249
- package/connectors/onedrive/src/tools.ts +253 -0
- package/connectors/outlook/src/server.ts +2 -351
- package/connectors/outlook/src/tools.ts +357 -0
- package/connectors/pagerduty/src/server.ts +2 -132
- package/connectors/pagerduty/src/tools.ts +126 -0
- package/connectors/pipedrive/src/server.ts +2 -58
- package/connectors/pipedrive/src/tools.ts +63 -0
- package/connectors/powerbi/src/search-filter.ts +2 -15
- package/connectors/powerbi/src/server.ts +43 -68
- package/connectors/prefect/src/search-filter.ts +3 -16
- package/connectors/prefect/src/server.ts +2 -90
- package/connectors/prefect/src/tools.ts +83 -0
- package/connectors/protonmail/src/mail-core.ts +16 -75
- package/connectors/protonmail/src/server.ts +34 -214
- package/connectors/raindrop/src/server.ts +2 -103
- package/connectors/raindrop/src/tools.ts +103 -0
- package/connectors/ramp/src/server.ts +2 -105
- package/connectors/ramp/src/tools.ts +98 -0
- package/connectors/readwise/src/server.ts +2 -88
- package/connectors/readwise/src/tools.ts +81 -0
- package/connectors/sagemaker/src/tools.ts +19 -38
- package/connectors/salesforce/src/search-filter.ts +4 -11
- package/connectors/salesforce/src/server.ts +2 -90
- package/connectors/salesforce/src/tools.ts +75 -0
- package/connectors/semgrep/src/search-filter.ts +3 -10
- package/connectors/semgrep/src/server.ts +2 -104
- package/connectors/semgrep/src/tools.ts +92 -0
- package/connectors/sentry/src/server.ts +2 -58
- package/connectors/sentry/src/tools.ts +53 -0
- package/connectors/slack/src/server.ts +2 -352
- package/connectors/slack/src/tools.ts +376 -0
- package/connectors/snowflake/src/search-filter.ts +4 -19
- package/connectors/snowflake/src/server.ts +6 -25
- package/connectors/snyk/src/server.ts +2 -140
- package/connectors/snyk/src/tools.ts +136 -0
- package/connectors/sonarqube/src/server.ts +2 -111
- package/connectors/sonarqube/src/tools.ts +98 -0
- package/connectors/stackoverflow/src/server.ts +2 -77
- package/connectors/stackoverflow/src/tools.ts +66 -0
- package/connectors/storybook/src/storybook-parse.ts +3 -6
- package/connectors/stripe/src/server.ts +2 -61
- package/connectors/stripe/src/tools.ts +54 -0
- package/connectors/superset/src/search-filter.ts +4 -15
- package/connectors/superset/src/server.ts +2 -115
- package/connectors/superset/src/tools.ts +98 -0
- package/connectors/tableau/src/search-filter.ts +2 -15
- package/connectors/tableau/src/server.ts +13 -65
- package/connectors/teams/src/server.ts +2 -249
- package/connectors/teams/src/tools.ts +283 -0
- package/connectors/testflight/src/server.ts +2 -70
- package/connectors/testflight/src/tools.ts +79 -0
- package/connectors/vercel/src/search-filter.ts +2 -10
- package/connectors/vercel/src/server.ts +2 -75
- package/connectors/vercel/src/tools.ts +73 -0
- package/connectors/vertex-ai/src/tools.ts +23 -33
- package/connectors/wiz/src/search-filter.ts +6 -30
- package/connectors/wiz/src/server.ts +2 -188
- package/connectors/wiz/src/tools.ts +168 -0
- package/connectors/zendesk/src/server.ts +2 -73
- package/connectors/zendesk/src/tools.ts +60 -0
- package/connectors/zoom/src/search-filter.ts +1 -7
- package/connectors/zoom/src/server.ts +2 -94
- package/connectors/zoom/src/tools.ts +93 -0
- package/connectors/zotero/src/server.ts +2 -78
- package/connectors/zotero/src/tools.ts +67 -0
- package/package.json +15 -12
- package/shared/access-token-cache.ts +123 -0
- package/shared/atlassian-json-fetch.ts +6 -16
- package/shared/audit-chain.ts +13 -6
- package/shared/cli-json-kit.ts +150 -0
- package/shared/collection-tool-kit.ts +143 -0
- package/shared/connector-mode.ts +3 -2
- package/shared/cursor-list-tool.ts +29 -0
- package/shared/env-json-api.ts +209 -0
- package/shared/fetch-json-text.ts +27 -0
- package/shared/github-rest.ts +62 -0
- package/shared/graphql-json.ts +60 -0
- package/shared/imap-mail-core.ts +91 -0
- package/shared/imap-tool-kit.ts +57 -23
- package/shared/imapflow-adapter.ts +417 -0
- package/shared/rest-tool-kit.ts +77 -7
- package/shared/run-cli-json.ts +38 -16
- package/shared/search-filter.ts +58 -18
- package/shared/walk-files.ts +118 -0
- 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 {
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 = ["
|
|
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
|
-
|
|
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"]));
|