@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,240 @@
|
|
|
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
|
+
} from "../../../shared/mcp-tool-kit.ts";
|
|
9
|
+
import {
|
|
10
|
+
getJenkinsCrumb,
|
|
11
|
+
jenkinsAuthHeader,
|
|
12
|
+
jenkinsBaseUrl,
|
|
13
|
+
jenkinsFetchJson,
|
|
14
|
+
jenkinsPost,
|
|
15
|
+
jobApiRoot,
|
|
16
|
+
} from "./jenkins-api.ts";
|
|
17
|
+
|
|
18
|
+
/** Tool names exposed by this connector — for contract/introspection tests. */
|
|
19
|
+
export const JENKINS_TOOL_NAMES = [
|
|
20
|
+
"jenkins_job_list",
|
|
21
|
+
"jenkins_job_get",
|
|
22
|
+
"jenkins_build_list",
|
|
23
|
+
"jenkins_build_get",
|
|
24
|
+
"jenkins_build_trigger",
|
|
25
|
+
"jenkins_build_abort",
|
|
26
|
+
"jenkins_build_log_tail",
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* GET one Jenkins JSON endpoint, throwing `Jenkins <status>: <body>` on a
|
|
31
|
+
* non-2xx. Module scope, not nested in the registrar: it closes over nothing.
|
|
32
|
+
*/
|
|
33
|
+
export async function jenkinsGetJsonExpectOk(url: string): Promise<unknown> {
|
|
34
|
+
const res = await jenkinsFetchJson(url, { method: "GET", authHeader: jenkinsAuthHeader() });
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
throw new Error(`Jenkins ${String(res.status)}: ${res.text.slice(0, 400)}`);
|
|
37
|
+
}
|
|
38
|
+
return res.json;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A state-changing POST to `<job root><suffix>`, carrying the CSRF crumb Jenkins requires for
|
|
43
|
+
* one. A failure throws `Jenkins <action> <status>: <body>`. Module scope for the same reason as
|
|
44
|
+
* {@link jenkinsGetJsonExpectOk}: it closes over nothing.
|
|
45
|
+
*/
|
|
46
|
+
async function postToJob(jobName: string, suffix: string, action: string): Promise<void> {
|
|
47
|
+
const base = jenkinsBaseUrl();
|
|
48
|
+
const auth = jenkinsAuthHeader();
|
|
49
|
+
const crumb = await getJenkinsCrumb(base, auth);
|
|
50
|
+
const url = `${jobApiRoot(base, jobName)}${suffix}`;
|
|
51
|
+
const res = await jenkinsPost(url, auth, crumb);
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
throw new Error(`Jenkins ${action} ${String(res.status)}: ${res.text.slice(0, 400)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function registerJenkinsTools(
|
|
58
|
+
server: ConsentServer & { tool: (...args: never) => unknown },
|
|
59
|
+
): void {
|
|
60
|
+
const reg = createZodToolRegistrar(createRegisterSimpleTool(server));
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Every MUTATING jenkins tool goes through here. Outside the gateway this adds the
|
|
64
|
+
* consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
|
|
65
|
+
* the gateway it is a pass-through, because executor.ts (I2) is the gate there.
|
|
66
|
+
*/
|
|
67
|
+
const registerWriteTool = createWriteToolRegistrar(server, {
|
|
68
|
+
connector: "jenkins",
|
|
69
|
+
scopeEnv: "NIMBUS_MCP_JENKINS_WRITE_SCOPE",
|
|
70
|
+
scopeKinds: ["job"],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const JOBS_TREE =
|
|
74
|
+
"jobs[name,fullname,url,jobs[name,fullname,url,jobs[name,fullname,url,jobs[name,fullname,url]]]]";
|
|
75
|
+
|
|
76
|
+
type JobNode = {
|
|
77
|
+
name?: string;
|
|
78
|
+
fullName?: string;
|
|
79
|
+
url?: string;
|
|
80
|
+
jobs?: JobNode[];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function jenkinsJobNodeDisplayName(n: JobNode): string {
|
|
84
|
+
if (typeof n.fullName === "string" && n.fullName !== "") {
|
|
85
|
+
return n.fullName;
|
|
86
|
+
}
|
|
87
|
+
if (typeof n.name === "string") {
|
|
88
|
+
return n.name;
|
|
89
|
+
}
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function flattenJobs(
|
|
94
|
+
nodes: JobNode[] | undefined,
|
|
95
|
+
out: { fullName: string; url?: string }[],
|
|
96
|
+
): void {
|
|
97
|
+
if (nodes === undefined) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
for (const n of nodes) {
|
|
101
|
+
const fn = jenkinsJobNodeDisplayName(n);
|
|
102
|
+
if (fn !== "") {
|
|
103
|
+
if (typeof n.url === "string") {
|
|
104
|
+
out.push({ fullName: fn, url: n.url });
|
|
105
|
+
} else {
|
|
106
|
+
out.push({ fullName: fn });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
flattenJobs(n.jobs, out);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
reg(
|
|
114
|
+
"jenkins_job_list",
|
|
115
|
+
"List Jenkins jobs (nested folders, limited depth).",
|
|
116
|
+
z.object({}),
|
|
117
|
+
async () => {
|
|
118
|
+
const base = jenkinsBaseUrl();
|
|
119
|
+
const url = `${base}/api/json?tree=${encodeURIComponent(JOBS_TREE)}`;
|
|
120
|
+
const root = await jenkinsGetJsonExpectOk(url);
|
|
121
|
+
if (root === null || typeof root !== "object" || Array.isArray(root)) {
|
|
122
|
+
throw new Error("Jenkins: invalid jobs response");
|
|
123
|
+
}
|
|
124
|
+
const jobsRaw = (root as Record<string, unknown>)["jobs"];
|
|
125
|
+
const list: { fullName: string; url?: string }[] = [];
|
|
126
|
+
flattenJobs(Array.isArray(jobsRaw) ? (jobsRaw as JobNode[]) : undefined, list);
|
|
127
|
+
return jsonResult({ jobs: list });
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const jobNameSchema = z.object({
|
|
132
|
+
jobName: z.string().min(1).describe("Job full name (e.g. folder/sub/job)"),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
reg(
|
|
136
|
+
"jenkins_job_get",
|
|
137
|
+
"Get Jenkins job metadata by full name.",
|
|
138
|
+
jobNameSchema,
|
|
139
|
+
async (parsed) => {
|
|
140
|
+
const base = jenkinsBaseUrl();
|
|
141
|
+
const root = jobApiRoot(base, parsed.jobName);
|
|
142
|
+
const url = `${root}/api/json`;
|
|
143
|
+
return jsonResult(await jenkinsGetJsonExpectOk(url));
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
reg(
|
|
148
|
+
"jenkins_build_list",
|
|
149
|
+
"List recent builds for a job.",
|
|
150
|
+
z.object({
|
|
151
|
+
jobName: z.string().min(1),
|
|
152
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
153
|
+
}),
|
|
154
|
+
async (parsed) => {
|
|
155
|
+
const base = jenkinsBaseUrl();
|
|
156
|
+
const lim = parsed.limit ?? 20;
|
|
157
|
+
const tree = encodeURIComponent(
|
|
158
|
+
`builds[number,url,result,duration,timestamp,building]{0,${String(lim)}}`,
|
|
159
|
+
);
|
|
160
|
+
const url = `${jobApiRoot(base, parsed.jobName)}/api/json?tree=${tree}`;
|
|
161
|
+
return jsonResult(await jenkinsGetJsonExpectOk(url));
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
reg(
|
|
166
|
+
"jenkins_build_get",
|
|
167
|
+
"Get a single build by job name and build number.",
|
|
168
|
+
z.object({
|
|
169
|
+
jobName: z.string().min(1),
|
|
170
|
+
buildNumber: z.number().int().min(1),
|
|
171
|
+
}),
|
|
172
|
+
async (parsed) => {
|
|
173
|
+
const base = jenkinsBaseUrl();
|
|
174
|
+
const url = `${jobApiRoot(base, parsed.jobName)}/${String(parsed.buildNumber)}/api/json`;
|
|
175
|
+
return jsonResult(await jenkinsGetJsonExpectOk(url));
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
registerWriteTool(
|
|
180
|
+
"jenkins_build_trigger",
|
|
181
|
+
{
|
|
182
|
+
mutates: "jenkins.build.trigger",
|
|
183
|
+
recoverable: true,
|
|
184
|
+
scopeTargetOf: (p) => ({ kind: "job", value: p.jobName }),
|
|
185
|
+
},
|
|
186
|
+
"Trigger a new build for a job.",
|
|
187
|
+
jobNameSchema,
|
|
188
|
+
async (parsed) => {
|
|
189
|
+
await postToJob(parsed.jobName, "/build", "trigger");
|
|
190
|
+
return jsonResult({ ok: true, jobName: parsed.jobName });
|
|
191
|
+
},
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
registerWriteTool(
|
|
195
|
+
"jenkins_build_abort",
|
|
196
|
+
{
|
|
197
|
+
mutates: "jenkins.build.abort",
|
|
198
|
+
recoverable: true,
|
|
199
|
+
scopeTargetOf: (p) => ({ kind: "job", value: p.jobName }),
|
|
200
|
+
},
|
|
201
|
+
"Abort/stop a running build.",
|
|
202
|
+
z.object({
|
|
203
|
+
jobName: z.string().min(1),
|
|
204
|
+
buildNumber: z.number().int().min(1),
|
|
205
|
+
}),
|
|
206
|
+
async (parsed) => {
|
|
207
|
+
await postToJob(parsed.jobName, `/${String(parsed.buildNumber)}/stop`, "abort");
|
|
208
|
+
return jsonResult({ ok: true, jobName: parsed.jobName, buildNumber: parsed.buildNumber });
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
reg(
|
|
213
|
+
"jenkins_build_log_tail",
|
|
214
|
+
"Fetch console text for a build (full log; tail client-side).",
|
|
215
|
+
z.object({
|
|
216
|
+
jobName: z.string().min(1),
|
|
217
|
+
buildNumber: z.number().int().min(1),
|
|
218
|
+
maxLines: z.number().int().min(1).max(5000).optional(),
|
|
219
|
+
}),
|
|
220
|
+
async (parsed) => {
|
|
221
|
+
const base = jenkinsBaseUrl();
|
|
222
|
+
const auth = jenkinsAuthHeader();
|
|
223
|
+
const url = `${jobApiRoot(base, parsed.jobName)}/${String(parsed.buildNumber)}/consoleText`;
|
|
224
|
+
const res = await fetch(url, { headers: { Authorization: auth } });
|
|
225
|
+
const text = await res.text();
|
|
226
|
+
if (!res.ok) {
|
|
227
|
+
throw new Error(`Jenkins log ${String(res.status)}: ${text.slice(0, 400)}`);
|
|
228
|
+
}
|
|
229
|
+
const maxLines = parsed.maxLines ?? 200;
|
|
230
|
+
const lines = text.split(/\r?\n/);
|
|
231
|
+
const tail = lines.slice(Math.max(0, lines.length - maxLines)).join("\n");
|
|
232
|
+
return jsonResult({
|
|
233
|
+
jobName: parsed.jobName,
|
|
234
|
+
buildNumber: parsed.buildNumber,
|
|
235
|
+
lineCount: lines.length,
|
|
236
|
+
tail,
|
|
237
|
+
});
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
}
|
|
@@ -1,309 +1,10 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
fetchAtlassianBasicAuthJsonText,
|
|
7
|
-
normalizeRequiredSiteBaseUrl,
|
|
8
|
-
requireTrimmedEnv,
|
|
9
|
-
} from "../../../shared/atlassian-json-fetch.ts";
|
|
10
|
-
import {
|
|
11
|
-
createRegisterSimpleTool,
|
|
12
|
-
createZodToolRegistrar,
|
|
13
|
-
mcpJsonResult as jsonResult,
|
|
14
|
-
mcpJsonResultFromTextIfOk,
|
|
15
|
-
} from "../../../shared/mcp-tool-kit.ts";
|
|
16
|
-
|
|
17
|
-
function requireJiraConfig(): { baseUrl: string; email: string; token: string } {
|
|
18
|
-
const baseRaw = requireTrimmedEnv("JIRA_BASE_URL", "JIRA_BASE_URL is not set");
|
|
19
|
-
const email = requireTrimmedEnv("JIRA_EMAIL", "JIRA_EMAIL is not set");
|
|
20
|
-
const token = requireTrimmedEnv("JIRA_API_TOKEN", "JIRA_API_TOKEN is not set");
|
|
21
|
-
return {
|
|
22
|
-
baseUrl: normalizeRequiredSiteBaseUrl(baseRaw, "JIRA_BASE_URL is empty"),
|
|
23
|
-
email,
|
|
24
|
-
token,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function jiraFetch(
|
|
29
|
-
baseUrl: string,
|
|
30
|
-
email: string,
|
|
31
|
-
token: string,
|
|
32
|
-
path: string,
|
|
33
|
-
init?: RequestInit,
|
|
34
|
-
): Promise<{ ok: boolean; status: number; text: string }> {
|
|
35
|
-
const relativePath = path.startsWith("/") ? path : `/${path}`;
|
|
36
|
-
const url = `${baseUrl}${relativePath}`;
|
|
37
|
-
return fetchAtlassianBasicAuthJsonText(url, email, token, init);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function plainToAdf(text: string): Record<string, unknown> {
|
|
41
|
-
return {
|
|
42
|
-
type: "doc",
|
|
43
|
-
version: 1,
|
|
44
|
-
content: [
|
|
45
|
-
{
|
|
46
|
-
type: "paragraph",
|
|
47
|
-
content: [{ type: "text", text }],
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
};
|
|
51
|
-
}
|
|
3
|
+
import { registerJiraTools } from "./tools.ts";
|
|
52
4
|
|
|
53
5
|
const server = new McpServer({ name: "nimbus-jira", version: "0.1.0" });
|
|
54
6
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
|
|
58
|
-
|
|
59
|
-
const reg = createZodToolRegistrar(registerSimpleTool);
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Every MUTATING jira tool goes through here. Outside the gateway this adds the
|
|
63
|
-
* consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
|
|
64
|
-
* the gateway it is a pass-through, because executor.ts (I2) is the gate there.
|
|
65
|
-
*/
|
|
66
|
-
const registerWriteTool = createWriteToolRegistrar(server, {
|
|
67
|
-
connector: "jira",
|
|
68
|
-
scopeEnv: "NIMBUS_MCP_JIRA_WRITE_SCOPE",
|
|
69
|
-
scopeKinds: ["project", "issue"],
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
const jiraIssueListSchema = z.object({
|
|
73
|
-
jql: z.string().min(1).optional(),
|
|
74
|
-
startAt: z.number().int().min(0).optional(),
|
|
75
|
-
maxResults: z.number().int().min(1).max(100).optional(),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
reg(
|
|
79
|
-
"jira_issue_list",
|
|
80
|
-
"Search Jira issues with JQL (Jira Cloud REST POST /rest/api/3/search).",
|
|
81
|
-
jiraIssueListSchema,
|
|
82
|
-
async (parsed) => {
|
|
83
|
-
const { baseUrl, email, token } = requireJiraConfig();
|
|
84
|
-
const jql = parsed.jql ?? "order by updated DESC";
|
|
85
|
-
const body = JSON.stringify({
|
|
86
|
-
jql,
|
|
87
|
-
startAt: parsed.startAt ?? 0,
|
|
88
|
-
maxResults: parsed.maxResults ?? 50,
|
|
89
|
-
fields: ["summary", "description", "updated", "status", "issuetype", "priority", "assignee"],
|
|
90
|
-
});
|
|
91
|
-
const res = await jiraFetch(baseUrl, email, token, "/rest/api/3/search", {
|
|
92
|
-
method: "POST",
|
|
93
|
-
body,
|
|
94
|
-
});
|
|
95
|
-
return mcpJsonResultFromTextIfOk("Jira", res, {
|
|
96
|
-
jsonParseErrorMessage: "Jira: invalid JSON from search",
|
|
97
|
-
});
|
|
98
|
-
},
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const jiraIssueKeySchema = z.object({ issueKey: z.string().min(1) });
|
|
102
|
-
|
|
103
|
-
reg(
|
|
104
|
-
"jira_issue_get",
|
|
105
|
-
"Get a Jira issue by key or id (GET /rest/api/3/issue/{issueIdOrKey}).",
|
|
106
|
-
jiraIssueKeySchema,
|
|
107
|
-
async (parsed) => {
|
|
108
|
-
const { baseUrl, email, token } = requireJiraConfig();
|
|
109
|
-
const key = encodeURIComponent(parsed.issueKey);
|
|
110
|
-
const res = await jiraFetch(
|
|
111
|
-
baseUrl,
|
|
112
|
-
email,
|
|
113
|
-
token,
|
|
114
|
-
`/rest/api/3/issue/${key}?fields=summary,description,updated,status,issuetype,priority,assignee,comment`,
|
|
115
|
-
);
|
|
116
|
-
return mcpJsonResultFromTextIfOk("Jira", res, {
|
|
117
|
-
jsonParseErrorMessage: "Jira: invalid JSON from issue get",
|
|
118
|
-
});
|
|
119
|
-
},
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
const jiraIssueCreateSchema = z.object({
|
|
123
|
-
projectKey: z.string().min(1),
|
|
124
|
-
summary: z.string().min(1),
|
|
125
|
-
description: z.string().optional(),
|
|
126
|
-
issueTypeName: z.string().min(1).optional(),
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
registerWriteTool(
|
|
130
|
-
"jira_issue_create",
|
|
131
|
-
{
|
|
132
|
-
mutates: "jira.issue.create",
|
|
133
|
-
recoverable: true,
|
|
134
|
-
scopeTargetOf: (p) => ({ kind: "project", value: p.projectKey }),
|
|
135
|
-
},
|
|
136
|
-
"Create a Jira issue (POST /rest/api/3/issue). Requires project key and summary.",
|
|
137
|
-
jiraIssueCreateSchema,
|
|
138
|
-
async (parsed) => {
|
|
139
|
-
const { baseUrl, email, token } = requireJiraConfig();
|
|
140
|
-
const fields: Record<string, unknown> = {
|
|
141
|
-
project: { key: parsed.projectKey },
|
|
142
|
-
summary: parsed.summary,
|
|
143
|
-
issuetype: { name: parsed.issueTypeName ?? "Task" },
|
|
144
|
-
};
|
|
145
|
-
if (parsed.description !== undefined && parsed.description !== "") {
|
|
146
|
-
fields["description"] = plainToAdf(parsed.description);
|
|
147
|
-
}
|
|
148
|
-
const body = JSON.stringify({ fields });
|
|
149
|
-
const res = await jiraFetch(baseUrl, email, token, "/rest/api/3/issue", {
|
|
150
|
-
method: "POST",
|
|
151
|
-
body,
|
|
152
|
-
});
|
|
153
|
-
return mcpJsonResultFromTextIfOk("Jira", res, {
|
|
154
|
-
jsonParseErrorMessage: "Jira: invalid JSON from issue create",
|
|
155
|
-
});
|
|
156
|
-
},
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
const jiraIssueUpdateSchema = z.object({
|
|
160
|
-
issueKey: z.string().min(1),
|
|
161
|
-
summary: z.string().min(1).optional(),
|
|
162
|
-
description: z.string().optional(),
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
registerWriteTool(
|
|
166
|
-
"jira_issue_update",
|
|
167
|
-
{
|
|
168
|
-
mutates: "jira.issue.update",
|
|
169
|
-
recoverable: true,
|
|
170
|
-
scopeTargetOf: (p) => ({ kind: "issue", value: p.issueKey }),
|
|
171
|
-
},
|
|
172
|
-
"Update summary and/or description on a Jira issue (PUT /rest/api/3/issue/{key}).",
|
|
173
|
-
jiraIssueUpdateSchema,
|
|
174
|
-
async (parsed) => {
|
|
175
|
-
if (parsed.summary === undefined && parsed.description === undefined) {
|
|
176
|
-
throw new Error("Provide summary and/or description to update");
|
|
177
|
-
}
|
|
178
|
-
const { baseUrl, email, token } = requireJiraConfig();
|
|
179
|
-
const fields: Record<string, unknown> = {};
|
|
180
|
-
if (parsed.summary !== undefined) {
|
|
181
|
-
fields["summary"] = parsed.summary;
|
|
182
|
-
}
|
|
183
|
-
if (parsed.description !== undefined) {
|
|
184
|
-
fields["description"] = plainToAdf(parsed.description);
|
|
185
|
-
}
|
|
186
|
-
const key = encodeURIComponent(parsed.issueKey);
|
|
187
|
-
const body = JSON.stringify({ fields });
|
|
188
|
-
const res = await jiraFetch(baseUrl, email, token, `/rest/api/3/issue/${key}`, {
|
|
189
|
-
method: "PUT",
|
|
190
|
-
body,
|
|
191
|
-
});
|
|
192
|
-
if (!res.ok) {
|
|
193
|
-
throw new Error(`Jira ${String(res.status)}: ${res.text.slice(0, 400)}`);
|
|
194
|
-
}
|
|
195
|
-
return jsonResult({ ok: true, issueKey: parsed.issueKey });
|
|
196
|
-
},
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
const jiraCommentAddSchema = z.object({
|
|
200
|
-
issueKey: z.string().min(1),
|
|
201
|
-
body: z.string().min(1),
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
registerWriteTool(
|
|
205
|
-
"jira_comment_add",
|
|
206
|
-
{
|
|
207
|
-
mutates: "jira.comment.add",
|
|
208
|
-
recoverable: true,
|
|
209
|
-
scopeTargetOf: (p) => ({ kind: "issue", value: p.issueKey }),
|
|
210
|
-
},
|
|
211
|
-
"Add a comment to a Jira issue (POST /rest/api/3/issue/{key}/comment).",
|
|
212
|
-
jiraCommentAddSchema,
|
|
213
|
-
async (parsed) => {
|
|
214
|
-
const { baseUrl, email, token } = requireJiraConfig();
|
|
215
|
-
const key = encodeURIComponent(parsed.issueKey);
|
|
216
|
-
const payload = JSON.stringify({ body: plainToAdf(parsed.body) });
|
|
217
|
-
const res = await jiraFetch(baseUrl, email, token, `/rest/api/3/issue/${key}/comment`, {
|
|
218
|
-
method: "POST",
|
|
219
|
-
body: payload,
|
|
220
|
-
});
|
|
221
|
-
return mcpJsonResultFromTextIfOk("Jira", res, {
|
|
222
|
-
jsonParseErrorMessage: "Jira: invalid JSON from comment add",
|
|
223
|
-
});
|
|
224
|
-
},
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
const jiraBoardListSchema = z.object({
|
|
228
|
-
startAt: z.number().int().min(0).optional(),
|
|
229
|
-
maxResults: z.number().int().min(1).max(50).optional(),
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
reg(
|
|
233
|
-
"jira_board_list",
|
|
234
|
-
"List Jira boards (GET /rest/agile/1.0/board).",
|
|
235
|
-
jiraBoardListSchema,
|
|
236
|
-
async (parsed) => {
|
|
237
|
-
const { baseUrl, email, token } = requireJiraConfig();
|
|
238
|
-
const start = parsed.startAt ?? 0;
|
|
239
|
-
const max = parsed.maxResults ?? 50;
|
|
240
|
-
const res = await jiraFetch(
|
|
241
|
-
baseUrl,
|
|
242
|
-
email,
|
|
243
|
-
token,
|
|
244
|
-
`/rest/agile/1.0/board?startAt=${String(start)}&maxResults=${String(max)}`,
|
|
245
|
-
);
|
|
246
|
-
return mcpJsonResultFromTextIfOk("Jira", res, {
|
|
247
|
-
jsonParseErrorMessage: "Jira: invalid JSON from board list",
|
|
248
|
-
});
|
|
249
|
-
},
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
const jiraSprintListSchema = z.object({
|
|
253
|
-
boardId: z.number().int().min(1),
|
|
254
|
-
startAt: z.number().int().min(0).optional(),
|
|
255
|
-
maxResults: z.number().int().min(1).max(50).optional(),
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
reg(
|
|
259
|
-
"jira_sprint_list",
|
|
260
|
-
"List sprints for a board (GET /rest/agile/1.0/board/{boardId}/sprint).",
|
|
261
|
-
jiraSprintListSchema,
|
|
262
|
-
async (parsed) => {
|
|
263
|
-
const { baseUrl, email, token } = requireJiraConfig();
|
|
264
|
-
const start = parsed.startAt ?? 0;
|
|
265
|
-
const max = parsed.maxResults ?? 50;
|
|
266
|
-
const bid = String(parsed.boardId);
|
|
267
|
-
const res = await jiraFetch(
|
|
268
|
-
baseUrl,
|
|
269
|
-
email,
|
|
270
|
-
token,
|
|
271
|
-
`/rest/agile/1.0/board/${bid}/sprint?startAt=${String(start)}&maxResults=${String(max)}`,
|
|
272
|
-
);
|
|
273
|
-
return mcpJsonResultFromTextIfOk("Jira", res, {
|
|
274
|
-
jsonParseErrorMessage: "Jira: invalid JSON from sprint list",
|
|
275
|
-
});
|
|
276
|
-
},
|
|
277
|
-
);
|
|
278
|
-
|
|
279
|
-
const jiraEpicListSchema = z.object({
|
|
280
|
-
projectKey: z.string().min(1),
|
|
281
|
-
maxResults: z.number().int().min(1).max(100).optional(),
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
reg(
|
|
285
|
-
"jira_epic_list",
|
|
286
|
-
"List epics in a project via JQL search.",
|
|
287
|
-
jiraEpicListSchema,
|
|
288
|
-
async (parsed) => {
|
|
289
|
-
const { baseUrl, email, token } = requireJiraConfig();
|
|
290
|
-
const pk = parsed.projectKey;
|
|
291
|
-
const jql = `project = ${pk} AND issuetype = Epic ORDER BY updated DESC`;
|
|
292
|
-
const body = JSON.stringify({
|
|
293
|
-
jql,
|
|
294
|
-
startAt: 0,
|
|
295
|
-
maxResults: parsed.maxResults ?? 50,
|
|
296
|
-
fields: ["summary", "updated", "status"],
|
|
297
|
-
});
|
|
298
|
-
const res = await jiraFetch(baseUrl, email, token, "/rest/api/3/search", {
|
|
299
|
-
method: "POST",
|
|
300
|
-
body,
|
|
301
|
-
});
|
|
302
|
-
return mcpJsonResultFromTextIfOk("Jira", res, {
|
|
303
|
-
jsonParseErrorMessage: "Jira: invalid JSON from epic search",
|
|
304
|
-
});
|
|
305
|
-
},
|
|
306
|
-
);
|
|
7
|
+
registerJiraTools(server);
|
|
307
8
|
|
|
308
9
|
const transport = new StdioServerTransport();
|
|
309
10
|
await server.connect(transport);
|