@nimbus-dev/connectors 0.2.1 → 0.2.3
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 +19 -7
- 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 +26 -43
- package/connectors/aws/src/server.ts +2 -183
- package/connectors/aws/src/tools.ts +199 -0
- package/connectors/azure/src/server.ts +2 -131
- package/connectors/azure/src/tools.ts +134 -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 +19 -35
- package/connectors/cloudwatch/src/tools.ts +20 -38
- 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 +153 -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 +160 -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 +21 -40
- 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 +14 -12
- package/shared/access-token-cache.ts +123 -0
- package/shared/atlassian-json-fetch.ts +6 -16
- package/shared/audit-chain.ts +601 -34
- package/shared/cli-json-kit.ts +191 -0
- package/shared/collection-tool-kit.ts +143 -0
- package/shared/connector-mode.ts +3 -2
- package/shared/consent-kit.ts +81 -26
- 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/nimbus-spawn.ts +84 -27
- package/shared/rest-tool-kit.ts +77 -7
- package/shared/run-cli-json.ts +38 -16
- package/shared/safe-cli-arg.ts +139 -17
- package/shared/search-filter.ts +58 -18
- package/shared/walk-files.ts +118 -0
- package/shared/windows-batch-args.ts +273 -0
- package/shared/write-budget.ts +100 -0
- package/standalone/src/launcher.ts +19 -7
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
fetchAtlassianBasicAuthJsonText,
|
|
4
|
+
normalizeRequiredSiteBaseUrl,
|
|
5
|
+
requireTrimmedEnv,
|
|
6
|
+
} from "../../../shared/atlassian-json-fetch.ts";
|
|
7
|
+
import type { ConsentServer } from "../../../shared/consent-kit.ts";
|
|
8
|
+
import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
|
|
9
|
+
import {
|
|
10
|
+
createRegisterSimpleTool,
|
|
11
|
+
createZodToolRegistrar,
|
|
12
|
+
mcpJsonResult as jsonResult,
|
|
13
|
+
mcpJsonResultFromTextIfOk,
|
|
14
|
+
} from "../../../shared/mcp-tool-kit.ts";
|
|
15
|
+
|
|
16
|
+
function requireJiraConfig(): { baseUrl: string; email: string; token: string } {
|
|
17
|
+
const baseRaw = requireTrimmedEnv("JIRA_BASE_URL", "JIRA_BASE_URL is not set");
|
|
18
|
+
const email = requireTrimmedEnv("JIRA_EMAIL", "JIRA_EMAIL is not set");
|
|
19
|
+
const token = requireTrimmedEnv("JIRA_API_TOKEN", "JIRA_API_TOKEN is not set");
|
|
20
|
+
return {
|
|
21
|
+
baseUrl: normalizeRequiredSiteBaseUrl(baseRaw, "JIRA_BASE_URL is empty"),
|
|
22
|
+
email,
|
|
23
|
+
token,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function jiraFetch(
|
|
28
|
+
baseUrl: string,
|
|
29
|
+
email: string,
|
|
30
|
+
token: string,
|
|
31
|
+
path: string,
|
|
32
|
+
init?: RequestInit,
|
|
33
|
+
): Promise<{ ok: boolean; status: number; text: string }> {
|
|
34
|
+
const relativePath = path.startsWith("/") ? path : `/${path}`;
|
|
35
|
+
const url = `${baseUrl}${relativePath}`;
|
|
36
|
+
return fetchAtlassianBasicAuthJsonText(url, email, token, init);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function plainToAdf(text: string): Record<string, unknown> {
|
|
40
|
+
return {
|
|
41
|
+
type: "doc",
|
|
42
|
+
version: 1,
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "paragraph",
|
|
46
|
+
content: [{ type: "text", text }],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Tool names exposed by this connector — for contract/introspection tests. */
|
|
53
|
+
export const JIRA_TOOL_NAMES = [
|
|
54
|
+
"jira_issue_list",
|
|
55
|
+
"jira_issue_get",
|
|
56
|
+
"jira_issue_create",
|
|
57
|
+
"jira_issue_update",
|
|
58
|
+
"jira_comment_add",
|
|
59
|
+
"jira_board_list",
|
|
60
|
+
"jira_sprint_list",
|
|
61
|
+
"jira_epic_list",
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
64
|
+
export function registerJiraTools(
|
|
65
|
+
server: ConsentServer & { tool: (...args: never) => unknown },
|
|
66
|
+
): void {
|
|
67
|
+
const registerSimpleTool = createRegisterSimpleTool(server);
|
|
68
|
+
|
|
69
|
+
const reg = createZodToolRegistrar(registerSimpleTool);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Every MUTATING jira tool goes through here. Outside the gateway this adds the
|
|
73
|
+
* consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
|
|
74
|
+
* the gateway it is a pass-through, because executor.ts (I2) is the gate there.
|
|
75
|
+
*/
|
|
76
|
+
const registerWriteTool = createWriteToolRegistrar(server, {
|
|
77
|
+
connector: "jira",
|
|
78
|
+
scopeEnv: "NIMBUS_MCP_JIRA_WRITE_SCOPE",
|
|
79
|
+
scopeKinds: ["project", "issue"],
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const jiraIssueListSchema = z.object({
|
|
83
|
+
jql: z.string().min(1).optional(),
|
|
84
|
+
startAt: z.number().int().min(0).optional(),
|
|
85
|
+
maxResults: z.number().int().min(1).max(100).optional(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
reg(
|
|
89
|
+
"jira_issue_list",
|
|
90
|
+
"Search Jira issues with JQL (Jira Cloud REST POST /rest/api/3/search).",
|
|
91
|
+
jiraIssueListSchema,
|
|
92
|
+
async (parsed) => {
|
|
93
|
+
const { baseUrl, email, token } = requireJiraConfig();
|
|
94
|
+
const jql = parsed.jql ?? "order by updated DESC";
|
|
95
|
+
const body = JSON.stringify({
|
|
96
|
+
jql,
|
|
97
|
+
startAt: parsed.startAt ?? 0,
|
|
98
|
+
maxResults: parsed.maxResults ?? 50,
|
|
99
|
+
fields: [
|
|
100
|
+
"summary",
|
|
101
|
+
"description",
|
|
102
|
+
"updated",
|
|
103
|
+
"status",
|
|
104
|
+
"issuetype",
|
|
105
|
+
"priority",
|
|
106
|
+
"assignee",
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
const res = await jiraFetch(baseUrl, email, token, "/rest/api/3/search", {
|
|
110
|
+
method: "POST",
|
|
111
|
+
body,
|
|
112
|
+
});
|
|
113
|
+
return mcpJsonResultFromTextIfOk("Jira", res, {
|
|
114
|
+
jsonParseErrorMessage: "Jira: invalid JSON from search",
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const jiraIssueKeySchema = z.object({ issueKey: z.string().min(1) });
|
|
120
|
+
|
|
121
|
+
reg(
|
|
122
|
+
"jira_issue_get",
|
|
123
|
+
"Get a Jira issue by key or id (GET /rest/api/3/issue/{issueIdOrKey}).",
|
|
124
|
+
jiraIssueKeySchema,
|
|
125
|
+
async (parsed) => {
|
|
126
|
+
const { baseUrl, email, token } = requireJiraConfig();
|
|
127
|
+
const key = encodeURIComponent(parsed.issueKey);
|
|
128
|
+
const res = await jiraFetch(
|
|
129
|
+
baseUrl,
|
|
130
|
+
email,
|
|
131
|
+
token,
|
|
132
|
+
`/rest/api/3/issue/${key}?fields=summary,description,updated,status,issuetype,priority,assignee,comment`,
|
|
133
|
+
);
|
|
134
|
+
return mcpJsonResultFromTextIfOk("Jira", res, {
|
|
135
|
+
jsonParseErrorMessage: "Jira: invalid JSON from issue get",
|
|
136
|
+
});
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const jiraIssueCreateSchema = z.object({
|
|
141
|
+
projectKey: z.string().min(1),
|
|
142
|
+
summary: z.string().min(1),
|
|
143
|
+
description: z.string().optional(),
|
|
144
|
+
issueTypeName: z.string().min(1).optional(),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
registerWriteTool(
|
|
148
|
+
"jira_issue_create",
|
|
149
|
+
{
|
|
150
|
+
mutates: "jira.issue.create",
|
|
151
|
+
recoverable: true,
|
|
152
|
+
scopeTargetOf: (p) => ({ kind: "project", value: p.projectKey }),
|
|
153
|
+
},
|
|
154
|
+
"Create a Jira issue (POST /rest/api/3/issue). Requires project key and summary.",
|
|
155
|
+
jiraIssueCreateSchema,
|
|
156
|
+
async (parsed) => {
|
|
157
|
+
const { baseUrl, email, token } = requireJiraConfig();
|
|
158
|
+
const fields: Record<string, unknown> = {
|
|
159
|
+
project: { key: parsed.projectKey },
|
|
160
|
+
summary: parsed.summary,
|
|
161
|
+
issuetype: { name: parsed.issueTypeName ?? "Task" },
|
|
162
|
+
};
|
|
163
|
+
if (parsed.description !== undefined && parsed.description !== "") {
|
|
164
|
+
fields["description"] = plainToAdf(parsed.description);
|
|
165
|
+
}
|
|
166
|
+
const body = JSON.stringify({ fields });
|
|
167
|
+
const res = await jiraFetch(baseUrl, email, token, "/rest/api/3/issue", {
|
|
168
|
+
method: "POST",
|
|
169
|
+
body,
|
|
170
|
+
});
|
|
171
|
+
return mcpJsonResultFromTextIfOk("Jira", res, {
|
|
172
|
+
jsonParseErrorMessage: "Jira: invalid JSON from issue create",
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const jiraIssueUpdateSchema = z.object({
|
|
178
|
+
issueKey: z.string().min(1),
|
|
179
|
+
summary: z.string().min(1).optional(),
|
|
180
|
+
description: z.string().optional(),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
registerWriteTool(
|
|
184
|
+
"jira_issue_update",
|
|
185
|
+
{
|
|
186
|
+
mutates: "jira.issue.update",
|
|
187
|
+
recoverable: true,
|
|
188
|
+
scopeTargetOf: (p) => ({ kind: "issue", value: p.issueKey }),
|
|
189
|
+
},
|
|
190
|
+
"Update summary and/or description on a Jira issue (PUT /rest/api/3/issue/{key}).",
|
|
191
|
+
jiraIssueUpdateSchema,
|
|
192
|
+
async (parsed) => {
|
|
193
|
+
if (parsed.summary === undefined && parsed.description === undefined) {
|
|
194
|
+
throw new Error("Provide summary and/or description to update");
|
|
195
|
+
}
|
|
196
|
+
const { baseUrl, email, token } = requireJiraConfig();
|
|
197
|
+
const fields: Record<string, unknown> = {};
|
|
198
|
+
if (parsed.summary !== undefined) {
|
|
199
|
+
fields["summary"] = parsed.summary;
|
|
200
|
+
}
|
|
201
|
+
if (parsed.description !== undefined) {
|
|
202
|
+
fields["description"] = plainToAdf(parsed.description);
|
|
203
|
+
}
|
|
204
|
+
const key = encodeURIComponent(parsed.issueKey);
|
|
205
|
+
const body = JSON.stringify({ fields });
|
|
206
|
+
const res = await jiraFetch(baseUrl, email, token, `/rest/api/3/issue/${key}`, {
|
|
207
|
+
method: "PUT",
|
|
208
|
+
body,
|
|
209
|
+
});
|
|
210
|
+
if (!res.ok) {
|
|
211
|
+
throw new Error(`Jira ${String(res.status)}: ${res.text.slice(0, 400)}`);
|
|
212
|
+
}
|
|
213
|
+
return jsonResult({ ok: true, issueKey: parsed.issueKey });
|
|
214
|
+
},
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const jiraCommentAddSchema = z.object({
|
|
218
|
+
issueKey: z.string().min(1),
|
|
219
|
+
body: z.string().min(1),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
registerWriteTool(
|
|
223
|
+
"jira_comment_add",
|
|
224
|
+
{
|
|
225
|
+
mutates: "jira.comment.add",
|
|
226
|
+
recoverable: true,
|
|
227
|
+
scopeTargetOf: (p) => ({ kind: "issue", value: p.issueKey }),
|
|
228
|
+
},
|
|
229
|
+
"Add a comment to a Jira issue (POST /rest/api/3/issue/{key}/comment).",
|
|
230
|
+
jiraCommentAddSchema,
|
|
231
|
+
async (parsed) => {
|
|
232
|
+
const { baseUrl, email, token } = requireJiraConfig();
|
|
233
|
+
const key = encodeURIComponent(parsed.issueKey);
|
|
234
|
+
const payload = JSON.stringify({ body: plainToAdf(parsed.body) });
|
|
235
|
+
const res = await jiraFetch(baseUrl, email, token, `/rest/api/3/issue/${key}/comment`, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
body: payload,
|
|
238
|
+
});
|
|
239
|
+
return mcpJsonResultFromTextIfOk("Jira", res, {
|
|
240
|
+
jsonParseErrorMessage: "Jira: invalid JSON from comment add",
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const jiraBoardListSchema = z.object({
|
|
246
|
+
startAt: z.number().int().min(0).optional(),
|
|
247
|
+
maxResults: z.number().int().min(1).max(50).optional(),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
reg(
|
|
251
|
+
"jira_board_list",
|
|
252
|
+
"List Jira boards (GET /rest/agile/1.0/board).",
|
|
253
|
+
jiraBoardListSchema,
|
|
254
|
+
async (parsed) => {
|
|
255
|
+
const { baseUrl, email, token } = requireJiraConfig();
|
|
256
|
+
const start = parsed.startAt ?? 0;
|
|
257
|
+
const max = parsed.maxResults ?? 50;
|
|
258
|
+
const res = await jiraFetch(
|
|
259
|
+
baseUrl,
|
|
260
|
+
email,
|
|
261
|
+
token,
|
|
262
|
+
`/rest/agile/1.0/board?startAt=${String(start)}&maxResults=${String(max)}`,
|
|
263
|
+
);
|
|
264
|
+
return mcpJsonResultFromTextIfOk("Jira", res, {
|
|
265
|
+
jsonParseErrorMessage: "Jira: invalid JSON from board list",
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const jiraSprintListSchema = z.object({
|
|
271
|
+
boardId: z.number().int().min(1),
|
|
272
|
+
startAt: z.number().int().min(0).optional(),
|
|
273
|
+
maxResults: z.number().int().min(1).max(50).optional(),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
reg(
|
|
277
|
+
"jira_sprint_list",
|
|
278
|
+
"List sprints for a board (GET /rest/agile/1.0/board/{boardId}/sprint).",
|
|
279
|
+
jiraSprintListSchema,
|
|
280
|
+
async (parsed) => {
|
|
281
|
+
const { baseUrl, email, token } = requireJiraConfig();
|
|
282
|
+
const start = parsed.startAt ?? 0;
|
|
283
|
+
const max = parsed.maxResults ?? 50;
|
|
284
|
+
const bid = String(parsed.boardId);
|
|
285
|
+
const res = await jiraFetch(
|
|
286
|
+
baseUrl,
|
|
287
|
+
email,
|
|
288
|
+
token,
|
|
289
|
+
`/rest/agile/1.0/board/${bid}/sprint?startAt=${String(start)}&maxResults=${String(max)}`,
|
|
290
|
+
);
|
|
291
|
+
return mcpJsonResultFromTextIfOk("Jira", res, {
|
|
292
|
+
jsonParseErrorMessage: "Jira: invalid JSON from sprint list",
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const jiraEpicListSchema = z.object({
|
|
298
|
+
projectKey: z.string().min(1),
|
|
299
|
+
maxResults: z.number().int().min(1).max(100).optional(),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
reg(
|
|
303
|
+
"jira_epic_list",
|
|
304
|
+
"List epics in a project via JQL search.",
|
|
305
|
+
jiraEpicListSchema,
|
|
306
|
+
async (parsed) => {
|
|
307
|
+
const { baseUrl, email, token } = requireJiraConfig();
|
|
308
|
+
const pk = parsed.projectKey;
|
|
309
|
+
const jql = `project = ${pk} AND issuetype = Epic ORDER BY updated DESC`;
|
|
310
|
+
const body = JSON.stringify({
|
|
311
|
+
jql,
|
|
312
|
+
startAt: 0,
|
|
313
|
+
maxResults: parsed.maxResults ?? 50,
|
|
314
|
+
fields: ["summary", "updated", "status"],
|
|
315
|
+
});
|
|
316
|
+
const res = await jiraFetch(baseUrl, email, token, "/rest/api/3/search", {
|
|
317
|
+
method: "POST",
|
|
318
|
+
body,
|
|
319
|
+
});
|
|
320
|
+
return mcpJsonResultFromTextIfOk("Jira", res, {
|
|
321
|
+
jsonParseErrorMessage: "Jira: invalid JSON from epic search",
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
);
|
|
325
|
+
}
|
|
@@ -1,163 +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
|
-
import {
|
|
5
|
-
createRegisterSimpleTool,
|
|
6
|
-
createZodToolRegistrar,
|
|
7
|
-
mcpJsonResult as jsonResult,
|
|
8
|
-
} from "../../../shared/mcp-tool-kit.ts";
|
|
9
|
-
import { runCliJson, runCliOk } from "../../../shared/run-cli-json.ts";
|
|
10
|
-
|
|
11
|
-
function requireKubeconfigPath(): string {
|
|
12
|
-
const p = process.env["KUBECONFIG"]?.trim();
|
|
13
|
-
if (p === undefined || p === "") {
|
|
14
|
-
throw new Error("KUBECONFIG env must be set to a kubeconfig file path");
|
|
15
|
-
}
|
|
16
|
-
return p;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function kubectlBase(): string[] {
|
|
20
|
-
const args = ["kubectl"];
|
|
21
|
-
const ctx = process.env["KUBE_CONTEXT"]?.trim();
|
|
22
|
-
if (ctx !== undefined && ctx !== "") {
|
|
23
|
-
args.push("--context", ctx);
|
|
24
|
-
}
|
|
25
|
-
return args;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function kubeEnv(): Record<string, string | undefined> {
|
|
29
|
-
return { KUBECONFIG: requireKubeconfigPath() };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async function kubectlJson(rest: string[]): Promise<unknown> {
|
|
33
|
-
const cmd = [...kubectlBase(), ...rest, "-o", "json"];
|
|
34
|
-
const r = await runCliJson(cmd, kubeEnv());
|
|
35
|
-
if (!r.ok) {
|
|
36
|
-
throw new Error(r.message);
|
|
37
|
-
}
|
|
38
|
-
return r.data;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const optionalNamespaceSchema = z.object({ namespace: z.string().min(1).optional() });
|
|
42
|
-
|
|
43
|
-
async function k8sListNamespacedResource(p: { namespace?: string | undefined }, resource: string) {
|
|
44
|
-
const ns = p.namespace ?? "default";
|
|
45
|
-
const data = await kubectlJson(["get", resource, "-n", ns]);
|
|
46
|
-
return jsonResult(data ?? {});
|
|
47
|
-
}
|
|
3
|
+
import { registerKubernetesTools } from "./tools.ts";
|
|
48
4
|
|
|
49
5
|
const mcp = new McpServer({ name: "nimbus-kubernetes", version: "0.1.0" });
|
|
50
6
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const reg = createZodToolRegistrar(createRegisterSimpleTool(mcp));
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Every MUTATING kubernetes tool goes through here. Outside the gateway this adds the
|
|
57
|
-
* consent gate, the write-scope allow-list, the mutation budget and the audit record; inside
|
|
58
|
-
* the gateway it is a pass-through, because executor.ts (I2) is the gate there.
|
|
59
|
-
*/
|
|
60
|
-
const registerWriteTool = createWriteToolRegistrar(mcp, {
|
|
61
|
-
connector: "kubernetes",
|
|
62
|
-
scopeEnv: "NIMBUS_MCP_KUBERNETES_WRITE_SCOPE",
|
|
63
|
-
scopeKinds: ["namespace"],
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
reg(
|
|
67
|
-
"k8s_pod_list",
|
|
68
|
-
"List pods in a namespace (default namespace: default).",
|
|
69
|
-
optionalNamespaceSchema,
|
|
70
|
-
(p) => k8sListNamespacedResource(p, "pods"),
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
reg("k8s_deployment_list", "List deployments in a namespace.", optionalNamespaceSchema, (p) =>
|
|
74
|
-
k8sListNamespacedResource(p, "deployments"),
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
reg("k8s_event_list", "List events in a namespace.", optionalNamespaceSchema, (p) =>
|
|
78
|
-
k8sListNamespacedResource(p, "events"),
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
registerWriteTool(
|
|
82
|
-
"k8s_rollout_restart",
|
|
83
|
-
{
|
|
84
|
-
mutates: "kubernetes.rollout.restart",
|
|
85
|
-
recoverable: true,
|
|
86
|
-
scopeTargetOf: (p) => ({ kind: "namespace", value: p.namespace ?? "default" }),
|
|
87
|
-
},
|
|
88
|
-
"Restart a rollout (e.g. deployment).",
|
|
89
|
-
z.object({
|
|
90
|
-
namespace: z.string().min(1).optional(),
|
|
91
|
-
resourceType: z.string().min(1),
|
|
92
|
-
name: z.string().min(1),
|
|
93
|
-
}),
|
|
94
|
-
async (p) => {
|
|
95
|
-
const ns = p.namespace ?? "default";
|
|
96
|
-
const cmd = [...kubectlBase(), "rollout", "restart", p.resourceType, p.name, "-n", ns];
|
|
97
|
-
const r = await runCliOk(cmd, kubeEnv());
|
|
98
|
-
if (!r.ok) {
|
|
99
|
-
throw new Error(r.message);
|
|
100
|
-
}
|
|
101
|
-
return jsonResult({ ok: true });
|
|
102
|
-
},
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
registerWriteTool(
|
|
106
|
-
"k8s_pod_delete",
|
|
107
|
-
{
|
|
108
|
-
mutates: "kubernetes.pod.delete",
|
|
109
|
-
// A deleted pod is not restorable; its identity is all that remains to record.
|
|
110
|
-
recoverable: false,
|
|
111
|
-
capturePreState: (p) => Promise.resolve({ namespace: p.namespace, podName: p.podName }),
|
|
112
|
-
scopeTargetOf: (p) => ({ kind: "namespace", value: p.namespace ?? "default" }),
|
|
113
|
-
},
|
|
114
|
-
"Delete a pod.",
|
|
115
|
-
z.object({
|
|
116
|
-
namespace: z.string().min(1).optional(),
|
|
117
|
-
podName: z.string().min(1),
|
|
118
|
-
}),
|
|
119
|
-
async (p) => {
|
|
120
|
-
const ns = p.namespace ?? "default";
|
|
121
|
-
const cmd = [...kubectlBase(), "delete", "pod", p.podName, "-n", ns];
|
|
122
|
-
const r = await runCliOk(cmd, kubeEnv());
|
|
123
|
-
if (!r.ok) {
|
|
124
|
-
throw new Error(r.message);
|
|
125
|
-
}
|
|
126
|
-
return jsonResult({ ok: true });
|
|
127
|
-
},
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
registerWriteTool(
|
|
131
|
-
"k8s_deployment_scale",
|
|
132
|
-
{
|
|
133
|
-
mutates: "kubernetes.deployment.scale",
|
|
134
|
-
recoverable: true,
|
|
135
|
-
scopeTargetOf: (p) => ({ kind: "namespace", value: p.namespace ?? "default" }),
|
|
136
|
-
},
|
|
137
|
-
"Scale a deployment.",
|
|
138
|
-
z.object({
|
|
139
|
-
namespace: z.string().min(1).optional(),
|
|
140
|
-
deploymentName: z.string().min(1),
|
|
141
|
-
replicas: z.number().int().min(0),
|
|
142
|
-
}),
|
|
143
|
-
async (p) => {
|
|
144
|
-
const ns = p.namespace ?? "default";
|
|
145
|
-
const cmd = [
|
|
146
|
-
...kubectlBase(),
|
|
147
|
-
"scale",
|
|
148
|
-
"deployment",
|
|
149
|
-
p.deploymentName,
|
|
150
|
-
`-n`,
|
|
151
|
-
ns,
|
|
152
|
-
`--replicas=${String(p.replicas)}`,
|
|
153
|
-
];
|
|
154
|
-
const r = await runCliOk(cmd, kubeEnv());
|
|
155
|
-
if (!r.ok) {
|
|
156
|
-
throw new Error(r.message);
|
|
157
|
-
}
|
|
158
|
-
return jsonResult({ ok: true });
|
|
159
|
-
},
|
|
160
|
-
);
|
|
7
|
+
registerKubernetesTools(mcp);
|
|
161
8
|
|
|
162
9
|
const transport = new StdioServerTransport();
|
|
163
10
|
await mcp.connect(transport);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { cliArg } from "../../../shared/cli-json-kit.ts";
|
|
3
|
+
import type { ConsentServer } from "../../../shared/consent-kit.ts";
|
|
4
|
+
import { createWriteToolRegistrar } from "../../../shared/consent-kit.ts";
|
|
5
|
+
import {
|
|
6
|
+
createRegisterSimpleTool,
|
|
7
|
+
createZodToolRegistrar,
|
|
8
|
+
mcpJsonResult as jsonResult,
|
|
9
|
+
} from "../../../shared/mcp-tool-kit.ts";
|
|
10
|
+
import { runCliJsonThrowing, runCliOkThrowing } from "../../../shared/run-cli-json.ts";
|
|
11
|
+
|
|
12
|
+
function requireKubeconfigPath(): string {
|
|
13
|
+
const p = process.env["KUBECONFIG"]?.trim();
|
|
14
|
+
if (p === undefined || p === "") {
|
|
15
|
+
throw new Error("KUBECONFIG env must be set to a kubeconfig file path");
|
|
16
|
+
}
|
|
17
|
+
return p;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function kubectlBase(): string[] {
|
|
21
|
+
const args = ["kubectl"];
|
|
22
|
+
const ctx = process.env["KUBE_CONTEXT"]?.trim();
|
|
23
|
+
if (ctx !== undefined && ctx !== "") {
|
|
24
|
+
args.push("--context", ctx);
|
|
25
|
+
}
|
|
26
|
+
return args;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function kubeEnv(): Record<string, string | undefined> {
|
|
30
|
+
return { KUBECONFIG: requireKubeconfigPath() };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function kubectlJson(rest: string[]): Promise<unknown> {
|
|
34
|
+
const cmd = [...kubectlBase(), ...rest, "-o", "json"];
|
|
35
|
+
return runCliJsonThrowing(cmd, kubeEnv());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const optionalNamespaceSchema = z.object({ namespace: cliArg.optional() });
|
|
39
|
+
|
|
40
|
+
async function k8sListNamespacedResource(p: { namespace?: string | undefined }, resource: string) {
|
|
41
|
+
const ns = p.namespace ?? "default";
|
|
42
|
+
const data = await kubectlJson(["get", resource, "-n", ns]);
|
|
43
|
+
return jsonResult(data ?? {});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Tool names exposed by this connector — for contract/introspection tests. */
|
|
47
|
+
export const KUBERNETES_TOOL_NAMES = [
|
|
48
|
+
"k8s_pod_list",
|
|
49
|
+
"k8s_deployment_list",
|
|
50
|
+
"k8s_event_list",
|
|
51
|
+
"k8s_rollout_restart",
|
|
52
|
+
"k8s_pod_delete",
|
|
53
|
+
"k8s_deployment_scale",
|
|
54
|
+
] as const;
|
|
55
|
+
|
|
56
|
+
export function registerKubernetesTools(
|
|
57
|
+
server: ConsentServer & { tool: (...args: never) => unknown },
|
|
58
|
+
): void {
|
|
59
|
+
const reg = createZodToolRegistrar(createRegisterSimpleTool(server));
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Every MUTATING kubernetes 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: "kubernetes",
|
|
68
|
+
scopeEnv: "NIMBUS_MCP_KUBERNETES_WRITE_SCOPE",
|
|
69
|
+
scopeKinds: ["namespace"],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
reg(
|
|
73
|
+
"k8s_pod_list",
|
|
74
|
+
"List pods in a namespace (default namespace: default).",
|
|
75
|
+
optionalNamespaceSchema,
|
|
76
|
+
(p) => k8sListNamespacedResource(p, "pods"),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
reg("k8s_deployment_list", "List deployments in a namespace.", optionalNamespaceSchema, (p) =>
|
|
80
|
+
k8sListNamespacedResource(p, "deployments"),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
reg("k8s_event_list", "List events in a namespace.", optionalNamespaceSchema, (p) =>
|
|
84
|
+
k8sListNamespacedResource(p, "events"),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
registerWriteTool(
|
|
88
|
+
"k8s_rollout_restart",
|
|
89
|
+
{
|
|
90
|
+
mutates: "kubernetes.rollout.restart",
|
|
91
|
+
recoverable: true,
|
|
92
|
+
scopeTargetOf: (p) => ({ kind: "namespace", value: p.namespace ?? "default" }),
|
|
93
|
+
},
|
|
94
|
+
"Restart a rollout (e.g. deployment).",
|
|
95
|
+
z.object({
|
|
96
|
+
namespace: cliArg.optional(),
|
|
97
|
+
resourceType: cliArg,
|
|
98
|
+
name: cliArg,
|
|
99
|
+
}),
|
|
100
|
+
async (p) => {
|
|
101
|
+
const ns = p.namespace ?? "default";
|
|
102
|
+
const cmd = [...kubectlBase(), "rollout", "restart", p.resourceType, p.name, "-n", ns];
|
|
103
|
+
await runCliOkThrowing(cmd, kubeEnv());
|
|
104
|
+
return jsonResult({ ok: true });
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
registerWriteTool(
|
|
109
|
+
"k8s_pod_delete",
|
|
110
|
+
{
|
|
111
|
+
mutates: "kubernetes.pod.delete",
|
|
112
|
+
// A deleted pod is not restorable; its identity is all that remains to record — including
|
|
113
|
+
// the namespace the delete defaulted to, without which the record cannot say where it was.
|
|
114
|
+
recoverable: false,
|
|
115
|
+
capturePreState: (p) =>
|
|
116
|
+
Promise.resolve({ namespace: p.namespace ?? "default", podName: p.podName }),
|
|
117
|
+
scopeTargetOf: (p) => ({ kind: "namespace", value: p.namespace ?? "default" }),
|
|
118
|
+
},
|
|
119
|
+
"Delete a pod.",
|
|
120
|
+
z.object({
|
|
121
|
+
namespace: cliArg.optional(),
|
|
122
|
+
podName: cliArg,
|
|
123
|
+
}),
|
|
124
|
+
async (p) => {
|
|
125
|
+
const ns = p.namespace ?? "default";
|
|
126
|
+
const cmd = [...kubectlBase(), "delete", "pod", p.podName, "-n", ns];
|
|
127
|
+
await runCliOkThrowing(cmd, kubeEnv());
|
|
128
|
+
return jsonResult({ ok: true });
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
registerWriteTool(
|
|
133
|
+
"k8s_deployment_scale",
|
|
134
|
+
{
|
|
135
|
+
mutates: "kubernetes.deployment.scale",
|
|
136
|
+
recoverable: true,
|
|
137
|
+
scopeTargetOf: (p) => ({ kind: "namespace", value: p.namespace ?? "default" }),
|
|
138
|
+
},
|
|
139
|
+
"Scale a deployment.",
|
|
140
|
+
z.object({
|
|
141
|
+
namespace: cliArg.optional(),
|
|
142
|
+
deploymentName: cliArg,
|
|
143
|
+
replicas: z.number().int().min(0),
|
|
144
|
+
}),
|
|
145
|
+
async (p) => {
|
|
146
|
+
const ns = p.namespace ?? "default";
|
|
147
|
+
const cmd = [
|
|
148
|
+
...kubectlBase(),
|
|
149
|
+
"scale",
|
|
150
|
+
"deployment",
|
|
151
|
+
p.deploymentName,
|
|
152
|
+
`-n`,
|
|
153
|
+
ns,
|
|
154
|
+
`--replicas=${String(p.replicas)}`,
|
|
155
|
+
];
|
|
156
|
+
await runCliOkThrowing(cmd, kubeEnv());
|
|
157
|
+
return jsonResult({ ok: true });
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
}
|