@sweny-ai/core 0.1.39 → 0.1.41
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/main.js +39 -43
- package/dist/mcp.d.ts +23 -0
- package/dist/mcp.js +160 -0
- package/dist/mcp.test.js +322 -1
- 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/main.js
CHANGED
|
@@ -10,9 +10,9 @@ import { execute } from "../executor.js";
|
|
|
10
10
|
import { triageWorkflow, implementWorkflow, seedContentWorkflow } from "../workflows/index.js";
|
|
11
11
|
import { consoleLogger } from "../types.js";
|
|
12
12
|
import { ClaudeClient } from "../claude.js";
|
|
13
|
-
import { createSkillMap } from "../skills/index.js";
|
|
13
|
+
import { createSkillMap, validateWorkflowSkills } from "../skills/index.js";
|
|
14
14
|
import { configuredSkills } from "../skills/custom-loader.js";
|
|
15
|
-
import { buildAutoMcpServers, buildProviderContext } from "../mcp.js";
|
|
15
|
+
import { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "../mcp.js";
|
|
16
16
|
import { loadAdditionalContext } from "../templates.js";
|
|
17
17
|
import { validateWorkflow as validateWorkflowSchema } from "../schema.js";
|
|
18
18
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
@@ -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
|
*/
|
|
@@ -561,8 +525,40 @@ export async function workflowRunAction(file, options) {
|
|
|
561
525
|
const isJson = Boolean(options.json);
|
|
562
526
|
const isTTY = !isJson && (process.stderr.isTTY ?? false);
|
|
563
527
|
const skills = createSkillMap(configuredSkills(process.env, process.cwd()));
|
|
564
|
-
|
|
565
|
-
|
|
528
|
+
// Hard-fail at startup if the workflow references skills that aren't available.
|
|
529
|
+
// Each node lists alternatives by category (e.g. [sentry, datadog, betterstack]);
|
|
530
|
+
// we require at least one configured skill per category.
|
|
531
|
+
const validation = validateWorkflowSkills(workflow, skills);
|
|
532
|
+
if (validation.errors.length > 0) {
|
|
533
|
+
console.error(chalk.red(`\n Workflow cannot run — missing required skills:\n`));
|
|
534
|
+
for (const err of validation.errors)
|
|
535
|
+
console.error(chalk.red(` \u2717 ${err}`));
|
|
536
|
+
if (validation.missing.length > 0) {
|
|
537
|
+
console.error(chalk.dim(`\n Set the missing env vars and try again:`));
|
|
538
|
+
for (const m of validation.missing) {
|
|
539
|
+
if (m.missingEnv.length > 0) {
|
|
540
|
+
console.error(chalk.dim(` - ${m.id}: ${m.missingEnv.join(", ")}`));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
console.error("");
|
|
545
|
+
process.exit(1);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
for (const warn of validation.warnings)
|
|
549
|
+
console.error(chalk.yellow(` \u26A0 ${warn}`));
|
|
550
|
+
// Engine-driven MCP wiring: only inject MCPs for skills that the workflow
|
|
551
|
+
// actually references AND whose env vars are present.
|
|
552
|
+
const referencedSkillIds = new Set();
|
|
553
|
+
for (const node of Object.values(workflow.nodes)) {
|
|
554
|
+
for (const id of node.skills)
|
|
555
|
+
referencedSkillIds.add(id);
|
|
556
|
+
}
|
|
557
|
+
const mcpServers = buildSkillMcpServers({
|
|
558
|
+
referencedSkills: referencedSkillIds,
|
|
559
|
+
credentials: buildCredentialMap(),
|
|
560
|
+
userMcpServers: Object.keys(config.mcpServers).length > 0 ? config.mcpServers : undefined,
|
|
561
|
+
});
|
|
566
562
|
const claude = new ClaudeClient({
|
|
567
563
|
maxTurns: config.maxInvestigateTurns || 50,
|
|
568
564
|
cwd: process.cwd(),
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
import type { McpAutoConfig, McpServerConfig } from "./types.js";
|
|
2
|
+
export interface SkillMcpOptions {
|
|
3
|
+
/** Skill IDs referenced by the workflow being executed. Only these get MCPs. */
|
|
4
|
+
referencedSkills: Set<string>;
|
|
5
|
+
/** Flat credential map (env vars). MCPs are only wired when their creds are set. */
|
|
6
|
+
credentials: Record<string, string>;
|
|
7
|
+
/** User-supplied MCP servers — always win on key conflict. */
|
|
8
|
+
userMcpServers?: Record<string, McpServerConfig>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Build MCP server configs for the skills a workflow references.
|
|
12
|
+
*
|
|
13
|
+
* For each skill ID in `referencedSkills`, look up its MCP wiring (if any)
|
|
14
|
+
* and include it only when the required credentials are present in `credentials`.
|
|
15
|
+
*
|
|
16
|
+
* Skills with no MCP variant (e.g. `notification`, `supabase`) are silently
|
|
17
|
+
* skipped — their in-process tools are wired separately by the executor.
|
|
18
|
+
*
|
|
19
|
+
* Unknown skill IDs (not built-in, not in this MCP catalog) are silently
|
|
20
|
+
* skipped here; the engine's hard-fail validation handles them upstream.
|
|
21
|
+
*
|
|
22
|
+
* User-supplied servers always win on key conflict.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildSkillMcpServers(opts: SkillMcpOptions): Record<string, McpServerConfig>;
|
|
2
25
|
/**
|
|
3
26
|
* Auto-configure well-known MCP servers based on which providers
|
|
4
27
|
* and workspace tools the user has enabled.
|
package/dist/mcp.js
CHANGED
|
@@ -1,3 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build MCP server configs for the skills a workflow references.
|
|
3
|
+
*
|
|
4
|
+
* For each skill ID in `referencedSkills`, look up its MCP wiring (if any)
|
|
5
|
+
* and include it only when the required credentials are present in `credentials`.
|
|
6
|
+
*
|
|
7
|
+
* Skills with no MCP variant (e.g. `notification`, `supabase`) are silently
|
|
8
|
+
* skipped — their in-process tools are wired separately by the executor.
|
|
9
|
+
*
|
|
10
|
+
* Unknown skill IDs (not built-in, not in this MCP catalog) are silently
|
|
11
|
+
* skipped here; the engine's hard-fail validation handles them upstream.
|
|
12
|
+
*
|
|
13
|
+
* User-supplied servers always win on key conflict.
|
|
14
|
+
*/
|
|
15
|
+
export function buildSkillMcpServers(opts) {
|
|
16
|
+
const auto = {};
|
|
17
|
+
const refs = opts.referencedSkills;
|
|
18
|
+
const creds = opts.credentials;
|
|
19
|
+
// GitHub MCP — wired when workflow uses `github` skill and a token is set.
|
|
20
|
+
if (refs.has("github") && creds.GITHUB_TOKEN) {
|
|
21
|
+
auto["github"] = {
|
|
22
|
+
type: "stdio",
|
|
23
|
+
command: "npx",
|
|
24
|
+
args: ["-y", "@modelcontextprotocol/server-github@latest"],
|
|
25
|
+
env: { GITHUB_PERSONAL_ACCESS_TOKEN: creds.GITHUB_TOKEN },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// GitLab MCP.
|
|
29
|
+
if (refs.has("gitlab") && creds.GITLAB_TOKEN) {
|
|
30
|
+
const env = { GITLAB_PERSONAL_ACCESS_TOKEN: creds.GITLAB_TOKEN };
|
|
31
|
+
const baseUrl = creds.GITLAB_URL || "https://gitlab.com";
|
|
32
|
+
if (baseUrl !== "https://gitlab.com")
|
|
33
|
+
env.GITLAB_API_URL = `${baseUrl}/api/v4`;
|
|
34
|
+
auto["gitlab"] = {
|
|
35
|
+
type: "stdio",
|
|
36
|
+
command: "npx",
|
|
37
|
+
args: ["-y", "@modelcontextprotocol/server-gitlab@latest"],
|
|
38
|
+
env,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Linear MCP — official remote HTTP endpoint.
|
|
42
|
+
if (refs.has("linear") && creds.LINEAR_API_KEY) {
|
|
43
|
+
auto["linear"] = {
|
|
44
|
+
type: "http",
|
|
45
|
+
url: "https://mcp.linear.app/mcp",
|
|
46
|
+
headers: { Authorization: `Bearer ${creds.LINEAR_API_KEY}` },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// Jira / Confluence — needs all 3 creds.
|
|
50
|
+
if (refs.has("jira") && creds.JIRA_URL && creds.JIRA_EMAIL && creds.JIRA_API_TOKEN) {
|
|
51
|
+
auto["jira"] = {
|
|
52
|
+
type: "stdio",
|
|
53
|
+
command: "npx",
|
|
54
|
+
args: ["-y", "@sooperset/mcp-atlassian@latest"],
|
|
55
|
+
env: {
|
|
56
|
+
JIRA_URL: creds.JIRA_URL,
|
|
57
|
+
JIRA_EMAIL: creds.JIRA_EMAIL,
|
|
58
|
+
JIRA_API_TOKEN: creds.JIRA_API_TOKEN,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
// Datadog MCP.
|
|
63
|
+
if (refs.has("datadog") && creds.DD_API_KEY && creds.DD_APP_KEY) {
|
|
64
|
+
auto["datadog"] = {
|
|
65
|
+
type: "http",
|
|
66
|
+
url: "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp",
|
|
67
|
+
headers: { DD_API_KEY: creds.DD_API_KEY, DD_APPLICATION_KEY: creds.DD_APP_KEY },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// Sentry MCP — server reads SENTRY_ACCESS_TOKEN, not SENTRY_AUTH_TOKEN.
|
|
71
|
+
if (refs.has("sentry") && creds.SENTRY_AUTH_TOKEN) {
|
|
72
|
+
const env = { SENTRY_ACCESS_TOKEN: creds.SENTRY_AUTH_TOKEN };
|
|
73
|
+
const sentryUrl = creds.SENTRY_URL;
|
|
74
|
+
if (sentryUrl && sentryUrl !== "https://sentry.io") {
|
|
75
|
+
try {
|
|
76
|
+
env.SENTRY_HOST = new URL(sentryUrl).hostname;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// malformed URL — leave SENTRY_HOST unset
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
auto["sentry"] = {
|
|
83
|
+
type: "stdio",
|
|
84
|
+
command: "npx",
|
|
85
|
+
args: ["-y", "@sentry/mcp-server@latest"],
|
|
86
|
+
env,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// New Relic MCP — region-aware HTTP endpoint, header is "Api-Key".
|
|
90
|
+
if (refs.has("newrelic") && creds.NEW_RELIC_API_KEY) {
|
|
91
|
+
const region = creds.NEW_RELIC_REGION;
|
|
92
|
+
const url = region === "eu" ? "https://mcp.eu.newrelic.com/mcp/" : "https://mcp.newrelic.com/mcp/";
|
|
93
|
+
auto["newrelic"] = {
|
|
94
|
+
type: "http",
|
|
95
|
+
url,
|
|
96
|
+
headers: { "Api-Key": creds.NEW_RELIC_API_KEY },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// BetterStack MCP — accept any of the legacy/telemetry/uptime tokens.
|
|
100
|
+
const bsToken = creds.BETTERSTACK_API_TOKEN || creds.BETTERSTACK_TELEMETRY_TOKEN || creds.BETTERSTACK_UPTIME_TOKEN;
|
|
101
|
+
if (refs.has("betterstack") && bsToken) {
|
|
102
|
+
auto["betterstack"] = {
|
|
103
|
+
type: "http",
|
|
104
|
+
url: "https://mcp.betterstack.com",
|
|
105
|
+
headers: { Authorization: `Bearer ${bsToken}` },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// Slack MCP — wired when workflow uses `slack` skill and bot token is set.
|
|
109
|
+
if (refs.has("slack") && creds.SLACK_BOT_TOKEN) {
|
|
110
|
+
const env = { SLACK_BOT_TOKEN: creds.SLACK_BOT_TOKEN };
|
|
111
|
+
if (creds.SLACK_TEAM_ID)
|
|
112
|
+
env.SLACK_TEAM_ID = creds.SLACK_TEAM_ID;
|
|
113
|
+
auto["slack"] = {
|
|
114
|
+
type: "stdio",
|
|
115
|
+
command: "npx",
|
|
116
|
+
args: ["-y", "@modelcontextprotocol/server-slack@latest"],
|
|
117
|
+
env,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// Notion MCP — accept either NOTION_TOKEN or NOTION_API_KEY.
|
|
121
|
+
if (refs.has("notion")) {
|
|
122
|
+
const token = creds.NOTION_TOKEN || creds.NOTION_API_KEY;
|
|
123
|
+
if (token) {
|
|
124
|
+
auto["notion"] = {
|
|
125
|
+
type: "stdio",
|
|
126
|
+
command: "npx",
|
|
127
|
+
args: ["-y", "@notionhq/notion-mcp-server@latest"],
|
|
128
|
+
env: { NOTION_TOKEN: token },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// PagerDuty MCP — auth scheme is `Token token=<key>`.
|
|
133
|
+
if (refs.has("pagerduty") && creds.PAGERDUTY_API_TOKEN) {
|
|
134
|
+
auto["pagerduty"] = {
|
|
135
|
+
type: "http",
|
|
136
|
+
url: "https://mcp.pagerduty.com/mcp",
|
|
137
|
+
headers: { Authorization: `Token token=${creds.PAGERDUTY_API_TOKEN}` },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Monday.com MCP.
|
|
141
|
+
if (refs.has("monday") && creds.MONDAY_TOKEN) {
|
|
142
|
+
auto["monday"] = {
|
|
143
|
+
type: "stdio",
|
|
144
|
+
command: "npx",
|
|
145
|
+
args: ["-y", "@mondaydotcomorg/monday-api-mcp@latest"],
|
|
146
|
+
env: { MONDAY_TOKEN: creds.MONDAY_TOKEN },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// Asana MCP.
|
|
150
|
+
if (refs.has("asana") && creds.ASANA_ACCESS_TOKEN) {
|
|
151
|
+
auto["asana"] = {
|
|
152
|
+
type: "stdio",
|
|
153
|
+
command: "npx",
|
|
154
|
+
args: ["-y", "asana-mcp@latest"],
|
|
155
|
+
env: { ASANA_ACCESS_TOKEN: creds.ASANA_ACCESS_TOKEN },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// User-supplied servers always win on key conflict.
|
|
159
|
+
return { ...auto, ...(opts.userMcpServers ?? {}) };
|
|
160
|
+
}
|
|
1
161
|
/**
|
|
2
162
|
* Auto-configure well-known MCP servers based on which providers
|
|
3
163
|
* and workspace tools the user has enabled.
|
package/dist/mcp.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { buildAutoMcpServers } from "./mcp.js";
|
|
2
|
+
import { buildAutoMcpServers, buildSkillMcpServers } from "./mcp.js";
|
|
3
3
|
// ─── Helpers ─────────────────────────────────────────────────────
|
|
4
4
|
function cfg(overrides = {}) {
|
|
5
5
|
return { credentials: {}, ...overrides };
|
|
@@ -345,3 +345,324 @@ describe("buildAutoMcpServers", () => {
|
|
|
345
345
|
expect(result["slack"]).toBeUndefined();
|
|
346
346
|
});
|
|
347
347
|
});
|
|
348
|
+
// ─── buildSkillMcpServers (engine-driven) ────────────────────────────
|
|
349
|
+
describe("buildSkillMcpServers", () => {
|
|
350
|
+
it("returns empty when no skills referenced and no creds present", () => {
|
|
351
|
+
const result = buildSkillMcpServers({
|
|
352
|
+
referencedSkills: new Set(),
|
|
353
|
+
credentials: {},
|
|
354
|
+
});
|
|
355
|
+
expect(result).toEqual({});
|
|
356
|
+
});
|
|
357
|
+
it("does not wire MCP for a skill not referenced by the workflow", () => {
|
|
358
|
+
// Token is set, but the workflow doesn't ask for github → no MCP.
|
|
359
|
+
const result = buildSkillMcpServers({
|
|
360
|
+
referencedSkills: new Set(["linear"]),
|
|
361
|
+
credentials: { GITHUB_TOKEN: "ghp_abc", LINEAR_API_KEY: "lin_abc" },
|
|
362
|
+
});
|
|
363
|
+
expect(result["github"]).toBeUndefined();
|
|
364
|
+
expect(result["linear"]).toBeDefined();
|
|
365
|
+
});
|
|
366
|
+
it("does not wire MCP for a referenced skill whose creds are missing", () => {
|
|
367
|
+
const result = buildSkillMcpServers({
|
|
368
|
+
referencedSkills: new Set(["github", "linear"]),
|
|
369
|
+
credentials: { LINEAR_API_KEY: "lin_abc" }, // no GITHUB_TOKEN
|
|
370
|
+
});
|
|
371
|
+
expect(result["github"]).toBeUndefined();
|
|
372
|
+
expect(result["linear"]).toBeDefined();
|
|
373
|
+
});
|
|
374
|
+
it("wires github MCP when referenced and GITHUB_TOKEN present", () => {
|
|
375
|
+
const result = buildSkillMcpServers({
|
|
376
|
+
referencedSkills: new Set(["github"]),
|
|
377
|
+
credentials: { GITHUB_TOKEN: "ghp_abc" },
|
|
378
|
+
});
|
|
379
|
+
expect(result["github"]).toEqual({
|
|
380
|
+
type: "stdio",
|
|
381
|
+
command: "npx",
|
|
382
|
+
args: ["-y", "@modelcontextprotocol/server-github@latest"],
|
|
383
|
+
env: { GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_abc" },
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
it("wires linear MCP when referenced and LINEAR_API_KEY present", () => {
|
|
387
|
+
const result = buildSkillMcpServers({
|
|
388
|
+
referencedSkills: new Set(["linear"]),
|
|
389
|
+
credentials: { LINEAR_API_KEY: "lin_abc" },
|
|
390
|
+
});
|
|
391
|
+
expect(result["linear"]).toEqual({
|
|
392
|
+
type: "http",
|
|
393
|
+
url: "https://mcp.linear.app/mcp",
|
|
394
|
+
headers: { Authorization: "Bearer lin_abc" },
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
it("wires sentry MCP with SENTRY_HOST for self-hosted SENTRY_URL", () => {
|
|
398
|
+
const result = buildSkillMcpServers({
|
|
399
|
+
referencedSkills: new Set(["sentry"]),
|
|
400
|
+
credentials: { SENTRY_AUTH_TOKEN: "sntryu_abc", SENTRY_URL: "https://sentry.mycompany.com" },
|
|
401
|
+
});
|
|
402
|
+
expect(result["sentry"]?.env).toMatchObject({
|
|
403
|
+
SENTRY_ACCESS_TOKEN: "sntryu_abc",
|
|
404
|
+
SENTRY_HOST: "sentry.mycompany.com",
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
it("wires datadog MCP when both keys present", () => {
|
|
408
|
+
const result = buildSkillMcpServers({
|
|
409
|
+
referencedSkills: new Set(["datadog"]),
|
|
410
|
+
credentials: { DD_API_KEY: "dd_api", DD_APP_KEY: "dd_app" },
|
|
411
|
+
});
|
|
412
|
+
expect(result["datadog"]).toEqual({
|
|
413
|
+
type: "http",
|
|
414
|
+
url: "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp",
|
|
415
|
+
headers: { DD_API_KEY: "dd_api", DD_APPLICATION_KEY: "dd_app" },
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
it("does not wire datadog MCP when DD_APP_KEY is missing", () => {
|
|
419
|
+
const result = buildSkillMcpServers({
|
|
420
|
+
referencedSkills: new Set(["datadog"]),
|
|
421
|
+
credentials: { DD_API_KEY: "dd_api" },
|
|
422
|
+
});
|
|
423
|
+
expect(result["datadog"]).toBeUndefined();
|
|
424
|
+
});
|
|
425
|
+
it("wires slack MCP without an opt-in flag — workflow reference is the opt-in", () => {
|
|
426
|
+
const result = buildSkillMcpServers({
|
|
427
|
+
referencedSkills: new Set(["slack"]),
|
|
428
|
+
credentials: { SLACK_BOT_TOKEN: "xoxb-abc" },
|
|
429
|
+
});
|
|
430
|
+
expect(result["slack"]).toEqual({
|
|
431
|
+
type: "stdio",
|
|
432
|
+
command: "npx",
|
|
433
|
+
args: ["-y", "@modelcontextprotocol/server-slack@latest"],
|
|
434
|
+
env: { SLACK_BOT_TOKEN: "xoxb-abc" },
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
it("wires betterstack MCP only when the workflow references betterstack", () => {
|
|
438
|
+
// Token alone is not enough — must be referenced.
|
|
439
|
+
const noRef = buildSkillMcpServers({
|
|
440
|
+
referencedSkills: new Set(["sentry"]),
|
|
441
|
+
credentials: { BETTERSTACK_API_TOKEN: "bs_abc", SENTRY_AUTH_TOKEN: "sntry_x" },
|
|
442
|
+
});
|
|
443
|
+
expect(noRef["betterstack"]).toBeUndefined();
|
|
444
|
+
const withRef = buildSkillMcpServers({
|
|
445
|
+
referencedSkills: new Set(["betterstack"]),
|
|
446
|
+
credentials: { BETTERSTACK_API_TOKEN: "bs_abc" },
|
|
447
|
+
});
|
|
448
|
+
expect(withRef["betterstack"]).toEqual({
|
|
449
|
+
type: "http",
|
|
450
|
+
url: "https://mcp.betterstack.com",
|
|
451
|
+
headers: { Authorization: "Bearer bs_abc" },
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
it("user-supplied MCP servers win on key conflict", () => {
|
|
455
|
+
const custom = { type: "stdio", command: "my-github", args: [] };
|
|
456
|
+
const result = buildSkillMcpServers({
|
|
457
|
+
referencedSkills: new Set(["github"]),
|
|
458
|
+
credentials: { GITHUB_TOKEN: "ghp_abc" },
|
|
459
|
+
userMcpServers: { github: custom },
|
|
460
|
+
});
|
|
461
|
+
expect(result["github"]).toEqual(custom);
|
|
462
|
+
});
|
|
463
|
+
it("user-supplied servers can add MCPs alongside auto-wired ones", () => {
|
|
464
|
+
const my = { type: "http", url: "https://my.example.com" };
|
|
465
|
+
const result = buildSkillMcpServers({
|
|
466
|
+
referencedSkills: new Set(["github"]),
|
|
467
|
+
credentials: { GITHUB_TOKEN: "ghp_abc" },
|
|
468
|
+
userMcpServers: { "my-tool": my },
|
|
469
|
+
});
|
|
470
|
+
expect(result["github"]).toBeDefined();
|
|
471
|
+
expect(result["my-tool"]).toEqual(my);
|
|
472
|
+
});
|
|
473
|
+
it("silently skips skill IDs with no MCP catalog entry (e.g. notification)", () => {
|
|
474
|
+
const result = buildSkillMcpServers({
|
|
475
|
+
referencedSkills: new Set(["notification", "supabase", "github"]),
|
|
476
|
+
credentials: { GITHUB_TOKEN: "ghp_abc" },
|
|
477
|
+
});
|
|
478
|
+
expect(result["notification"]).toBeUndefined();
|
|
479
|
+
expect(result["supabase"]).toBeUndefined();
|
|
480
|
+
expect(result["github"]).toBeDefined();
|
|
481
|
+
});
|
|
482
|
+
it("wires multiple skills simultaneously", () => {
|
|
483
|
+
const result = buildSkillMcpServers({
|
|
484
|
+
referencedSkills: new Set(["github", "linear", "datadog", "slack"]),
|
|
485
|
+
credentials: {
|
|
486
|
+
GITHUB_TOKEN: "ghp_abc",
|
|
487
|
+
LINEAR_API_KEY: "lin_abc",
|
|
488
|
+
DD_API_KEY: "dd_api",
|
|
489
|
+
DD_APP_KEY: "dd_app",
|
|
490
|
+
SLACK_BOT_TOKEN: "xoxb-abc",
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
expect(result["github"]).toBeDefined();
|
|
494
|
+
expect(result["linear"]).toBeDefined();
|
|
495
|
+
expect(result["datadog"]).toBeDefined();
|
|
496
|
+
expect(result["slack"]).toBeDefined();
|
|
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
|
+
});
|
|
668
|
+
});
|