@sweny-ai/core 0.1.40 → 0.1.42
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/dist/cli/credentials.d.ts +13 -0
- package/dist/cli/credentials.js +60 -0
- package/dist/cli/credentials.test.d.ts +1 -0
- package/dist/cli/credentials.test.js +172 -0
- package/dist/cli/init.js +1 -1
- package/dist/cli/init.test.js +1 -1
- package/dist/cli/main.js +3 -39
- package/dist/mcp.test.js +170 -0
- package/package.json +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read env vars into the flat credential map expected by buildAutoMcpServers
|
|
3
|
+
* and buildSkillMcpServers.
|
|
4
|
+
*
|
|
5
|
+
* Several env vars have two accepted spellings — the user-facing one
|
|
6
|
+
* (`NR_API_KEY`, `JIRA_BASE_URL`, `SENTRY_BASE_URL`, …) used in init.ts,
|
|
7
|
+
* config.ts, and docs, and the canonical one used by the MCP wiring layer
|
|
8
|
+
* (`NEW_RELIC_API_KEY`, `JIRA_URL`, `SENTRY_URL`, …). We accept either
|
|
9
|
+
* spelling and write the value under the canonical key the wiring functions
|
|
10
|
+
* look up. The user-facing spelling wins on conflict (it's what docs tell
|
|
11
|
+
* users to set).
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildCredentialMap(env?: NodeJS.ProcessEnv): Record<string, string>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read env vars into the flat credential map expected by buildAutoMcpServers
|
|
3
|
+
* and buildSkillMcpServers.
|
|
4
|
+
*
|
|
5
|
+
* Several env vars have two accepted spellings — the user-facing one
|
|
6
|
+
* (`NR_API_KEY`, `JIRA_BASE_URL`, `SENTRY_BASE_URL`, …) used in init.ts,
|
|
7
|
+
* config.ts, and docs, and the canonical one used by the MCP wiring layer
|
|
8
|
+
* (`NEW_RELIC_API_KEY`, `JIRA_URL`, `SENTRY_URL`, …). We accept either
|
|
9
|
+
* spelling and write the value under the canonical key the wiring functions
|
|
10
|
+
* look up. The user-facing spelling wins on conflict (it's what docs tell
|
|
11
|
+
* users to set).
|
|
12
|
+
*/
|
|
13
|
+
export function buildCredentialMap(env = process.env) {
|
|
14
|
+
const creds = {};
|
|
15
|
+
// [aliases…, canonical lookup key]
|
|
16
|
+
// Earlier aliases win on conflict — first non-empty match populates the canonical key.
|
|
17
|
+
const sources = [
|
|
18
|
+
[["GITHUB_TOKEN"], "GITHUB_TOKEN"],
|
|
19
|
+
[["GITLAB_TOKEN"], "GITLAB_TOKEN"],
|
|
20
|
+
[["GITLAB_URL"], "GITLAB_URL"],
|
|
21
|
+
[["LINEAR_API_KEY"], "LINEAR_API_KEY"],
|
|
22
|
+
// User-facing JIRA_BASE_URL → canonical JIRA_URL (the key the MCP layer reads).
|
|
23
|
+
[["JIRA_BASE_URL", "JIRA_URL"], "JIRA_URL"],
|
|
24
|
+
[["JIRA_EMAIL"], "JIRA_EMAIL"],
|
|
25
|
+
[["JIRA_API_TOKEN"], "JIRA_API_TOKEN"],
|
|
26
|
+
[["DD_API_KEY"], "DD_API_KEY"],
|
|
27
|
+
[["DD_APP_KEY"], "DD_APP_KEY"],
|
|
28
|
+
[["SENTRY_AUTH_TOKEN"], "SENTRY_AUTH_TOKEN"],
|
|
29
|
+
[["SENTRY_ORG"], "SENTRY_ORG"],
|
|
30
|
+
// User-facing SENTRY_BASE_URL → canonical SENTRY_URL.
|
|
31
|
+
[["SENTRY_BASE_URL", "SENTRY_URL"], "SENTRY_URL"],
|
|
32
|
+
// User-facing NR_* → canonical NEW_RELIC_*.
|
|
33
|
+
[["NR_API_KEY", "NEW_RELIC_API_KEY"], "NEW_RELIC_API_KEY"],
|
|
34
|
+
[["NR_REGION", "NEW_RELIC_REGION"], "NEW_RELIC_REGION"],
|
|
35
|
+
[["BETTERSTACK_API_TOKEN"], "BETTERSTACK_API_TOKEN"],
|
|
36
|
+
[["BETTERSTACK_TELEMETRY_TOKEN"], "BETTERSTACK_TELEMETRY_TOKEN"],
|
|
37
|
+
[["BETTERSTACK_UPTIME_TOKEN"], "BETTERSTACK_UPTIME_TOKEN"],
|
|
38
|
+
[["BETTERSTACK_SOURCE_ID"], "BETTERSTACK_SOURCE_ID"],
|
|
39
|
+
[["BETTERSTACK_TABLE_NAME"], "BETTERSTACK_TABLE_NAME"],
|
|
40
|
+
[["SLACK_BOT_TOKEN"], "SLACK_BOT_TOKEN"],
|
|
41
|
+
[["SLACK_TEAM_ID"], "SLACK_TEAM_ID"],
|
|
42
|
+
// mcp.ts accepts either NOTION_TOKEN or NOTION_API_KEY — populate both
|
|
43
|
+
// canonical keys so either env-var name reaches the wiring layer.
|
|
44
|
+
[["NOTION_TOKEN"], "NOTION_TOKEN"],
|
|
45
|
+
[["NOTION_API_KEY"], "NOTION_API_KEY"],
|
|
46
|
+
[["PAGERDUTY_API_TOKEN"], "PAGERDUTY_API_TOKEN"],
|
|
47
|
+
[["MONDAY_TOKEN"], "MONDAY_TOKEN"],
|
|
48
|
+
[["ASANA_ACCESS_TOKEN"], "ASANA_ACCESS_TOKEN"],
|
|
49
|
+
];
|
|
50
|
+
for (const [aliases, canonical] of sources) {
|
|
51
|
+
for (const alias of aliases) {
|
|
52
|
+
const v = env[alias];
|
|
53
|
+
if (v) {
|
|
54
|
+
creds[canonical] = v;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return creds;
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildCredentialMap } from "./credentials.js";
|
|
3
|
+
import { buildSkillMcpServers } from "../mcp.js";
|
|
4
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
5
|
+
// Integration tests: env vars → buildCredentialMap → buildSkillMcpServers
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
7
|
+
//
|
|
8
|
+
// These tests catch env-var name drift between:
|
|
9
|
+
// - the user-facing env var name (what docs / init.ts / config.ts read)
|
|
10
|
+
// - the canonical key the MCP wiring functions look up
|
|
11
|
+
//
|
|
12
|
+
// Unit tests in mcp.test.ts inject the credentials map directly. That
|
|
13
|
+
// validates each MCP wiring path but cannot catch a buildCredentialMap
|
|
14
|
+
// that puts the value under the wrong key. THESE tests close that gap
|
|
15
|
+
// by going through the real env-var → canonical-key translation.
|
|
16
|
+
//
|
|
17
|
+
// If a future contributor renames an env var and forgets to update both
|
|
18
|
+
// sides, one of these will fail loudly.
|
|
19
|
+
describe("buildCredentialMap ↔ buildSkillMcpServers integration", () => {
|
|
20
|
+
// ── New Relic: NR_API_KEY user-facing → NEW_RELIC_API_KEY canonical ──
|
|
21
|
+
it("user setting NR_API_KEY auto-wires the New Relic MCP", () => {
|
|
22
|
+
const creds = buildCredentialMap({ NR_API_KEY: "NRAK-abc" });
|
|
23
|
+
expect(creds.NEW_RELIC_API_KEY).toBe("NRAK-abc");
|
|
24
|
+
const mcps = buildSkillMcpServers({
|
|
25
|
+
referencedSkills: new Set(["newrelic"]),
|
|
26
|
+
credentials: creds,
|
|
27
|
+
});
|
|
28
|
+
expect(mcps.newrelic).toBeDefined();
|
|
29
|
+
expect(mcps.newrelic?.url).toBe("https://mcp.newrelic.com/mcp/");
|
|
30
|
+
});
|
|
31
|
+
it("user setting NR_REGION=eu picks the EU New Relic endpoint", () => {
|
|
32
|
+
const creds = buildCredentialMap({ NR_API_KEY: "NRAK-abc", NR_REGION: "eu" });
|
|
33
|
+
const mcps = buildSkillMcpServers({
|
|
34
|
+
referencedSkills: new Set(["newrelic"]),
|
|
35
|
+
credentials: creds,
|
|
36
|
+
});
|
|
37
|
+
expect(mcps.newrelic?.url).toBe("https://mcp.eu.newrelic.com/mcp/");
|
|
38
|
+
});
|
|
39
|
+
it("user setting NEW_RELIC_API_KEY (canonical name) also works", () => {
|
|
40
|
+
const creds = buildCredentialMap({ NEW_RELIC_API_KEY: "NRAK-xyz" });
|
|
41
|
+
expect(creds.NEW_RELIC_API_KEY).toBe("NRAK-xyz");
|
|
42
|
+
});
|
|
43
|
+
// ── Sentry: SENTRY_BASE_URL user-facing → SENTRY_URL canonical ──
|
|
44
|
+
it("user setting SENTRY_BASE_URL for self-hosted reaches the MCP layer", () => {
|
|
45
|
+
const creds = buildCredentialMap({
|
|
46
|
+
SENTRY_AUTH_TOKEN: "sntrys_abc",
|
|
47
|
+
SENTRY_BASE_URL: "https://sentry.acme.com",
|
|
48
|
+
});
|
|
49
|
+
expect(creds.SENTRY_URL).toBe("https://sentry.acme.com");
|
|
50
|
+
const mcps = buildSkillMcpServers({
|
|
51
|
+
referencedSkills: new Set(["sentry"]),
|
|
52
|
+
credentials: creds,
|
|
53
|
+
});
|
|
54
|
+
expect(mcps.sentry).toBeDefined();
|
|
55
|
+
expect(mcps.sentry?.env?.SENTRY_HOST).toBe("sentry.acme.com");
|
|
56
|
+
});
|
|
57
|
+
it("user setting SENTRY_URL (canonical name) also reaches MCP layer", () => {
|
|
58
|
+
const creds = buildCredentialMap({
|
|
59
|
+
SENTRY_AUTH_TOKEN: "sntrys_abc",
|
|
60
|
+
SENTRY_URL: "https://sentry.acme.com",
|
|
61
|
+
});
|
|
62
|
+
expect(creds.SENTRY_URL).toBe("https://sentry.acme.com");
|
|
63
|
+
});
|
|
64
|
+
it("default sentry.io URL does not set SENTRY_HOST", () => {
|
|
65
|
+
const creds = buildCredentialMap({ SENTRY_AUTH_TOKEN: "sntrys_abc" });
|
|
66
|
+
const mcps = buildSkillMcpServers({
|
|
67
|
+
referencedSkills: new Set(["sentry"]),
|
|
68
|
+
credentials: creds,
|
|
69
|
+
});
|
|
70
|
+
expect(mcps.sentry).toBeDefined();
|
|
71
|
+
expect(mcps.sentry?.env?.SENTRY_HOST).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
// ── Jira: JIRA_BASE_URL user-facing → JIRA_URL canonical ──
|
|
74
|
+
it("user setting JIRA_BASE_URL auto-wires the Jira MCP", () => {
|
|
75
|
+
const creds = buildCredentialMap({
|
|
76
|
+
JIRA_BASE_URL: "https://acme.atlassian.net",
|
|
77
|
+
JIRA_EMAIL: "alice@acme.com",
|
|
78
|
+
JIRA_API_TOKEN: "tok_abc",
|
|
79
|
+
});
|
|
80
|
+
expect(creds.JIRA_URL).toBe("https://acme.atlassian.net");
|
|
81
|
+
const mcps = buildSkillMcpServers({
|
|
82
|
+
referencedSkills: new Set(["jira"]),
|
|
83
|
+
credentials: creds,
|
|
84
|
+
});
|
|
85
|
+
expect(mcps.jira).toBeDefined();
|
|
86
|
+
expect(mcps.jira?.env?.JIRA_URL).toBe("https://acme.atlassian.net");
|
|
87
|
+
});
|
|
88
|
+
it("user setting JIRA_URL (canonical name) also auto-wires Jira", () => {
|
|
89
|
+
const creds = buildCredentialMap({
|
|
90
|
+
JIRA_URL: "https://acme.atlassian.net",
|
|
91
|
+
JIRA_EMAIL: "alice@acme.com",
|
|
92
|
+
JIRA_API_TOKEN: "tok_abc",
|
|
93
|
+
});
|
|
94
|
+
const mcps = buildSkillMcpServers({
|
|
95
|
+
referencedSkills: new Set(["jira"]),
|
|
96
|
+
credentials: creds,
|
|
97
|
+
});
|
|
98
|
+
expect(mcps.jira).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
it("Jira MCP is NOT wired without all 3 creds", () => {
|
|
101
|
+
const creds = buildCredentialMap({
|
|
102
|
+
JIRA_BASE_URL: "https://acme.atlassian.net",
|
|
103
|
+
JIRA_EMAIL: "alice@acme.com",
|
|
104
|
+
// missing JIRA_API_TOKEN
|
|
105
|
+
});
|
|
106
|
+
const mcps = buildSkillMcpServers({
|
|
107
|
+
referencedSkills: new Set(["jira"]),
|
|
108
|
+
credentials: creds,
|
|
109
|
+
});
|
|
110
|
+
expect(mcps.jira).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
// ── Notion: both env-var spellings ──
|
|
113
|
+
it("user setting NOTION_API_KEY (official Notion name) auto-wires Notion MCP", () => {
|
|
114
|
+
const creds = buildCredentialMap({ NOTION_API_KEY: "secret_abc" });
|
|
115
|
+
expect(creds.NOTION_API_KEY).toBe("secret_abc");
|
|
116
|
+
const mcps = buildSkillMcpServers({
|
|
117
|
+
referencedSkills: new Set(["notion"]),
|
|
118
|
+
credentials: creds,
|
|
119
|
+
});
|
|
120
|
+
expect(mcps.notion).toBeDefined();
|
|
121
|
+
expect(mcps.notion?.env?.NOTION_TOKEN).toBe("secret_abc");
|
|
122
|
+
});
|
|
123
|
+
it("user setting NOTION_TOKEN auto-wires Notion MCP", () => {
|
|
124
|
+
const creds = buildCredentialMap({ NOTION_TOKEN: "secret_xyz" });
|
|
125
|
+
const mcps = buildSkillMcpServers({
|
|
126
|
+
referencedSkills: new Set(["notion"]),
|
|
127
|
+
credentials: creds,
|
|
128
|
+
});
|
|
129
|
+
expect(mcps.notion?.env?.NOTION_TOKEN).toBe("secret_xyz");
|
|
130
|
+
});
|
|
131
|
+
// ── BetterStack token fallbacks ──
|
|
132
|
+
it("user setting BETTERSTACK_API_TOKEN auto-wires BetterStack MCP", () => {
|
|
133
|
+
const creds = buildCredentialMap({ BETTERSTACK_API_TOKEN: "bs_abc" });
|
|
134
|
+
const mcps = buildSkillMcpServers({
|
|
135
|
+
referencedSkills: new Set(["betterstack"]),
|
|
136
|
+
credentials: creds,
|
|
137
|
+
});
|
|
138
|
+
expect(mcps.betterstack).toBeDefined();
|
|
139
|
+
expect(mcps.betterstack?.headers?.Authorization).toBe("Bearer bs_abc");
|
|
140
|
+
});
|
|
141
|
+
it("user setting BETTERSTACK_TELEMETRY_TOKEN reaches the MCP layer", () => {
|
|
142
|
+
const creds = buildCredentialMap({ BETTERSTACK_TELEMETRY_TOKEN: "bs_tel_abc" });
|
|
143
|
+
const mcps = buildSkillMcpServers({
|
|
144
|
+
referencedSkills: new Set(["betterstack"]),
|
|
145
|
+
credentials: creds,
|
|
146
|
+
});
|
|
147
|
+
expect(mcps.betterstack?.headers?.Authorization).toBe("Bearer bs_tel_abc");
|
|
148
|
+
});
|
|
149
|
+
// ── Happy-path: all the common providers at once ──
|
|
150
|
+
it("end-to-end: a typical SRE triage env wires every expected MCP", () => {
|
|
151
|
+
const creds = buildCredentialMap({
|
|
152
|
+
GITHUB_TOKEN: "ghp_abc",
|
|
153
|
+
LINEAR_API_KEY: "lin_abc",
|
|
154
|
+
DD_API_KEY: "dd_api",
|
|
155
|
+
DD_APP_KEY: "dd_app",
|
|
156
|
+
SLACK_BOT_TOKEN: "xoxb-abc",
|
|
157
|
+
});
|
|
158
|
+
const mcps = buildSkillMcpServers({
|
|
159
|
+
referencedSkills: new Set(["github", "linear", "datadog", "slack"]),
|
|
160
|
+
credentials: creds,
|
|
161
|
+
});
|
|
162
|
+
expect(Object.keys(mcps).sort()).toEqual(["datadog", "github", "linear", "slack"]);
|
|
163
|
+
});
|
|
164
|
+
it("returns empty creds for an empty env", () => {
|
|
165
|
+
expect(buildCredentialMap({})).toEqual({});
|
|
166
|
+
});
|
|
167
|
+
it("ignores empty-string env values (treats them as unset)", () => {
|
|
168
|
+
const creds = buildCredentialMap({ GITHUB_TOKEN: "", LINEAR_API_KEY: "lin_abc" });
|
|
169
|
+
expect(creds.GITHUB_TOKEN).toBeUndefined();
|
|
170
|
+
expect(creds.LINEAR_API_KEY).toBe("lin_abc");
|
|
171
|
+
});
|
|
172
|
+
});
|
package/dist/cli/init.js
CHANGED
|
@@ -238,7 +238,7 @@ export function buildActionWorkflow(credentials, cronExpression) {
|
|
|
238
238
|
lines.push(" runs-on: ubuntu-latest");
|
|
239
239
|
lines.push(" steps:");
|
|
240
240
|
lines.push(" - uses: actions/checkout@v4");
|
|
241
|
-
lines.push(" - uses: swenyai/sweny@
|
|
241
|
+
lines.push(" - uses: swenyai/sweny@v5");
|
|
242
242
|
lines.push(" with:");
|
|
243
243
|
lines.push(" workflow: triage");
|
|
244
244
|
if (secretCreds.length > 0) {
|
package/dist/cli/init.test.js
CHANGED
|
@@ -271,7 +271,7 @@ describe("buildActionWorkflow", () => {
|
|
|
271
271
|
expect(workflow).toContain('cron: "0 8 * * 1"');
|
|
272
272
|
expect(workflow).toContain("workflow_dispatch:");
|
|
273
273
|
expect(workflow).toContain("actions/checkout@v4");
|
|
274
|
-
expect(workflow).toContain("swenyai/sweny@
|
|
274
|
+
expect(workflow).toContain("swenyai/sweny@v5");
|
|
275
275
|
});
|
|
276
276
|
it("generates a workflow with daily cron", () => {
|
|
277
277
|
const creds = collectCredentials({
|
package/dist/cli/main.js
CHANGED
|
@@ -21,6 +21,7 @@ import { toMermaid, toMermaidBlock } from "../mermaid.js";
|
|
|
21
21
|
import { DagRenderer } from "./renderer.js";
|
|
22
22
|
import * as readline from "node:readline";
|
|
23
23
|
import { loadDotenv, loadConfigFile } from "./config-file.js";
|
|
24
|
+
import { buildCredentialMap } from "./credentials.js";
|
|
24
25
|
import { runInit } from "./init.js";
|
|
25
26
|
import { runE2eInit, runE2eRun } from "./e2e.js";
|
|
26
27
|
import { registerTriageCommand, registerImplementCommand, parseCliInputs, validateInputs, validateWarnings, } from "./config.js";
|
|
@@ -100,45 +101,8 @@ program
|
|
|
100
101
|
});
|
|
101
102
|
registerSetupCommand(program);
|
|
102
103
|
// ── Credential map builder ──────────────────────────────────────────
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
*/
|
|
106
|
-
function buildCredentialMap() {
|
|
107
|
-
const creds = {};
|
|
108
|
-
const env = process.env;
|
|
109
|
-
const keys = [
|
|
110
|
-
"GITHUB_TOKEN",
|
|
111
|
-
"GITLAB_TOKEN",
|
|
112
|
-
"GITLAB_URL",
|
|
113
|
-
"LINEAR_API_KEY",
|
|
114
|
-
"JIRA_URL",
|
|
115
|
-
"JIRA_EMAIL",
|
|
116
|
-
"JIRA_API_TOKEN",
|
|
117
|
-
"DD_API_KEY",
|
|
118
|
-
"DD_APP_KEY",
|
|
119
|
-
"SENTRY_AUTH_TOKEN",
|
|
120
|
-
"SENTRY_ORG",
|
|
121
|
-
"SENTRY_URL",
|
|
122
|
-
"NR_API_KEY",
|
|
123
|
-
"NR_REGION",
|
|
124
|
-
"BETTERSTACK_API_TOKEN",
|
|
125
|
-
"BETTERSTACK_TELEMETRY_TOKEN",
|
|
126
|
-
"BETTERSTACK_UPTIME_TOKEN",
|
|
127
|
-
"BETTERSTACK_SOURCE_ID",
|
|
128
|
-
"BETTERSTACK_TABLE_NAME",
|
|
129
|
-
"SLACK_BOT_TOKEN",
|
|
130
|
-
"SLACK_TEAM_ID",
|
|
131
|
-
"NOTION_TOKEN",
|
|
132
|
-
"PAGERDUTY_API_TOKEN",
|
|
133
|
-
"MONDAY_TOKEN",
|
|
134
|
-
"ASANA_ACCESS_TOKEN",
|
|
135
|
-
];
|
|
136
|
-
for (const k of keys) {
|
|
137
|
-
if (env[k])
|
|
138
|
-
creds[k] = env[k];
|
|
139
|
-
}
|
|
140
|
-
return creds;
|
|
141
|
-
}
|
|
104
|
+
// buildCredentialMap lives in ./credentials.ts so tests can import it
|
|
105
|
+
// without triggering main.ts's top-level program.parse() side effect.
|
|
142
106
|
/**
|
|
143
107
|
* Build the McpAutoConfig from CLI config for buildAutoMcpServers.
|
|
144
108
|
*/
|
package/dist/mcp.test.js
CHANGED
|
@@ -495,4 +495,174 @@ describe("buildSkillMcpServers", () => {
|
|
|
495
495
|
expect(result["datadog"]).toBeDefined();
|
|
496
496
|
expect(result["slack"]).toBeDefined();
|
|
497
497
|
});
|
|
498
|
+
// ── GitLab ─────────────────────────────────────────────────────────
|
|
499
|
+
it("wires gitlab MCP when referenced and GITLAB_TOKEN present", () => {
|
|
500
|
+
const result = buildSkillMcpServers({
|
|
501
|
+
referencedSkills: new Set(["gitlab"]),
|
|
502
|
+
credentials: { GITLAB_TOKEN: "glpat_abc" },
|
|
503
|
+
});
|
|
504
|
+
expect(result["gitlab"]).toEqual({
|
|
505
|
+
type: "stdio",
|
|
506
|
+
command: "npx",
|
|
507
|
+
args: ["-y", "@modelcontextprotocol/server-gitlab@latest"],
|
|
508
|
+
env: { GITLAB_PERSONAL_ACCESS_TOKEN: "glpat_abc" },
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
it("wires gitlab MCP with self-hosted GITLAB_API_URL", () => {
|
|
512
|
+
const result = buildSkillMcpServers({
|
|
513
|
+
referencedSkills: new Set(["gitlab"]),
|
|
514
|
+
credentials: { GITLAB_TOKEN: "glpat_abc", GITLAB_URL: "https://gitlab.example.com" },
|
|
515
|
+
});
|
|
516
|
+
expect(result["gitlab"]?.env).toEqual({
|
|
517
|
+
GITLAB_PERSONAL_ACCESS_TOKEN: "glpat_abc",
|
|
518
|
+
GITLAB_API_URL: "https://gitlab.example.com/api/v4",
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
// ── Jira ───────────────────────────────────────────────────────────
|
|
522
|
+
it("wires jira MCP when all 3 creds present", () => {
|
|
523
|
+
const result = buildSkillMcpServers({
|
|
524
|
+
referencedSkills: new Set(["jira"]),
|
|
525
|
+
credentials: {
|
|
526
|
+
JIRA_URL: "https://acme.atlassian.net",
|
|
527
|
+
JIRA_EMAIL: "alice@acme.com",
|
|
528
|
+
JIRA_API_TOKEN: "tok_abc",
|
|
529
|
+
},
|
|
530
|
+
});
|
|
531
|
+
expect(result["jira"]).toEqual({
|
|
532
|
+
type: "stdio",
|
|
533
|
+
command: "npx",
|
|
534
|
+
args: ["-y", "@sooperset/mcp-atlassian@latest"],
|
|
535
|
+
env: {
|
|
536
|
+
JIRA_URL: "https://acme.atlassian.net",
|
|
537
|
+
JIRA_EMAIL: "alice@acme.com",
|
|
538
|
+
JIRA_API_TOKEN: "tok_abc",
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
it("does NOT wire jira MCP when JIRA_EMAIL is missing", () => {
|
|
543
|
+
const result = buildSkillMcpServers({
|
|
544
|
+
referencedSkills: new Set(["jira"]),
|
|
545
|
+
credentials: { JIRA_URL: "https://acme.atlassian.net", JIRA_API_TOKEN: "tok_abc" },
|
|
546
|
+
});
|
|
547
|
+
expect(result["jira"]).toBeUndefined();
|
|
548
|
+
});
|
|
549
|
+
// ── New Relic ──────────────────────────────────────────────────────
|
|
550
|
+
it("wires newrelic MCP with US (default) endpoint", () => {
|
|
551
|
+
const result = buildSkillMcpServers({
|
|
552
|
+
referencedSkills: new Set(["newrelic"]),
|
|
553
|
+
credentials: { NEW_RELIC_API_KEY: "NRAK-abc" },
|
|
554
|
+
});
|
|
555
|
+
expect(result["newrelic"]).toEqual({
|
|
556
|
+
type: "http",
|
|
557
|
+
url: "https://mcp.newrelic.com/mcp/",
|
|
558
|
+
headers: { "Api-Key": "NRAK-abc" },
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
it("wires newrelic MCP with EU endpoint when NEW_RELIC_REGION=eu", () => {
|
|
562
|
+
const result = buildSkillMcpServers({
|
|
563
|
+
referencedSkills: new Set(["newrelic"]),
|
|
564
|
+
credentials: { NEW_RELIC_API_KEY: "NRAK-abc", NEW_RELIC_REGION: "eu" },
|
|
565
|
+
});
|
|
566
|
+
expect(result["newrelic"]?.url).toBe("https://mcp.eu.newrelic.com/mcp/");
|
|
567
|
+
});
|
|
568
|
+
it("does NOT wire newrelic MCP without NEW_RELIC_API_KEY", () => {
|
|
569
|
+
const result = buildSkillMcpServers({
|
|
570
|
+
referencedSkills: new Set(["newrelic"]),
|
|
571
|
+
credentials: { NEW_RELIC_REGION: "eu" },
|
|
572
|
+
});
|
|
573
|
+
expect(result["newrelic"]).toBeUndefined();
|
|
574
|
+
});
|
|
575
|
+
// ── Notion (both env-var spellings) ────────────────────────────────
|
|
576
|
+
it("wires notion MCP with NOTION_TOKEN", () => {
|
|
577
|
+
const result = buildSkillMcpServers({
|
|
578
|
+
referencedSkills: new Set(["notion"]),
|
|
579
|
+
credentials: { NOTION_TOKEN: "secret_abc" },
|
|
580
|
+
});
|
|
581
|
+
expect(result["notion"]).toEqual({
|
|
582
|
+
type: "stdio",
|
|
583
|
+
command: "npx",
|
|
584
|
+
args: ["-y", "@notionhq/notion-mcp-server@latest"],
|
|
585
|
+
env: { NOTION_TOKEN: "secret_abc" },
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
it("wires notion MCP with NOTION_API_KEY fallback", () => {
|
|
589
|
+
const result = buildSkillMcpServers({
|
|
590
|
+
referencedSkills: new Set(["notion"]),
|
|
591
|
+
credentials: { NOTION_API_KEY: "secret_xyz" },
|
|
592
|
+
});
|
|
593
|
+
expect(result["notion"]?.env?.NOTION_TOKEN).toBe("secret_xyz");
|
|
594
|
+
});
|
|
595
|
+
it("prefers NOTION_TOKEN over NOTION_API_KEY when both set", () => {
|
|
596
|
+
const result = buildSkillMcpServers({
|
|
597
|
+
referencedSkills: new Set(["notion"]),
|
|
598
|
+
credentials: { NOTION_TOKEN: "primary", NOTION_API_KEY: "fallback" },
|
|
599
|
+
});
|
|
600
|
+
expect(result["notion"]?.env?.NOTION_TOKEN).toBe("primary");
|
|
601
|
+
});
|
|
602
|
+
// ── PagerDuty / Monday / Asana ─────────────────────────────────────
|
|
603
|
+
it("wires pagerduty MCP with Token-token auth scheme", () => {
|
|
604
|
+
const result = buildSkillMcpServers({
|
|
605
|
+
referencedSkills: new Set(["pagerduty"]),
|
|
606
|
+
credentials: { PAGERDUTY_API_TOKEN: "pd_abc" },
|
|
607
|
+
});
|
|
608
|
+
expect(result["pagerduty"]).toEqual({
|
|
609
|
+
type: "http",
|
|
610
|
+
url: "https://mcp.pagerduty.com/mcp",
|
|
611
|
+
headers: { Authorization: "Token token=pd_abc" },
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
it("wires monday MCP", () => {
|
|
615
|
+
const result = buildSkillMcpServers({
|
|
616
|
+
referencedSkills: new Set(["monday"]),
|
|
617
|
+
credentials: { MONDAY_TOKEN: "mon_abc" },
|
|
618
|
+
});
|
|
619
|
+
expect(result["monday"]).toEqual({
|
|
620
|
+
type: "stdio",
|
|
621
|
+
command: "npx",
|
|
622
|
+
args: ["-y", "@mondaydotcomorg/monday-api-mcp@latest"],
|
|
623
|
+
env: { MONDAY_TOKEN: "mon_abc" },
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
it("wires asana MCP", () => {
|
|
627
|
+
const result = buildSkillMcpServers({
|
|
628
|
+
referencedSkills: new Set(["asana"]),
|
|
629
|
+
credentials: { ASANA_ACCESS_TOKEN: "asana_abc" },
|
|
630
|
+
});
|
|
631
|
+
expect(result["asana"]).toEqual({
|
|
632
|
+
type: "stdio",
|
|
633
|
+
command: "npx",
|
|
634
|
+
args: ["-y", "asana-mcp@latest"],
|
|
635
|
+
env: { ASANA_ACCESS_TOKEN: "asana_abc" },
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
// ── BetterStack token fallback ─────────────────────────────────────
|
|
639
|
+
it("wires betterstack with BETTERSTACK_TELEMETRY_TOKEN fallback", () => {
|
|
640
|
+
const result = buildSkillMcpServers({
|
|
641
|
+
referencedSkills: new Set(["betterstack"]),
|
|
642
|
+
credentials: { BETTERSTACK_TELEMETRY_TOKEN: "bs_tel_abc" },
|
|
643
|
+
});
|
|
644
|
+
expect(result["betterstack"]).toEqual({
|
|
645
|
+
type: "http",
|
|
646
|
+
url: "https://mcp.betterstack.com",
|
|
647
|
+
headers: { Authorization: "Bearer bs_tel_abc" },
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
it("wires betterstack with BETTERSTACK_UPTIME_TOKEN fallback", () => {
|
|
651
|
+
const result = buildSkillMcpServers({
|
|
652
|
+
referencedSkills: new Set(["betterstack"]),
|
|
653
|
+
credentials: { BETTERSTACK_UPTIME_TOKEN: "bs_up_abc" },
|
|
654
|
+
});
|
|
655
|
+
expect(result["betterstack"]?.headers?.Authorization).toBe("Bearer bs_up_abc");
|
|
656
|
+
});
|
|
657
|
+
it("prefers BETTERSTACK_API_TOKEN over telemetry/uptime fallbacks", () => {
|
|
658
|
+
const result = buildSkillMcpServers({
|
|
659
|
+
referencedSkills: new Set(["betterstack"]),
|
|
660
|
+
credentials: {
|
|
661
|
+
BETTERSTACK_API_TOKEN: "primary",
|
|
662
|
+
BETTERSTACK_TELEMETRY_TOKEN: "fallback1",
|
|
663
|
+
BETTERSTACK_UPTIME_TOKEN: "fallback2",
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
expect(result["betterstack"]?.headers?.Authorization).toBe("Bearer primary");
|
|
667
|
+
});
|
|
498
668
|
});
|