@sweny-ai/core 0.1.39 → 0.1.40

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/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";
@@ -561,8 +561,40 @@ export async function workflowRunAction(file, options) {
561
561
  const isJson = Boolean(options.json);
562
562
  const isTTY = !isJson && (process.stderr.isTTY ?? false);
563
563
  const skills = createSkillMap(configuredSkills(process.env, process.cwd()));
564
- const mcpAutoConfig = buildMcpAutoConfig(config);
565
- const mcpServers = buildAutoMcpServers(mcpAutoConfig);
564
+ // Hard-fail at startup if the workflow references skills that aren't available.
565
+ // Each node lists alternatives by category (e.g. [sentry, datadog, betterstack]);
566
+ // we require at least one configured skill per category.
567
+ const validation = validateWorkflowSkills(workflow, skills);
568
+ if (validation.errors.length > 0) {
569
+ console.error(chalk.red(`\n Workflow cannot run — missing required skills:\n`));
570
+ for (const err of validation.errors)
571
+ console.error(chalk.red(` \u2717 ${err}`));
572
+ if (validation.missing.length > 0) {
573
+ console.error(chalk.dim(`\n Set the missing env vars and try again:`));
574
+ for (const m of validation.missing) {
575
+ if (m.missingEnv.length > 0) {
576
+ console.error(chalk.dim(` - ${m.id}: ${m.missingEnv.join(", ")}`));
577
+ }
578
+ }
579
+ }
580
+ console.error("");
581
+ process.exit(1);
582
+ return;
583
+ }
584
+ for (const warn of validation.warnings)
585
+ console.error(chalk.yellow(` \u26A0 ${warn}`));
586
+ // Engine-driven MCP wiring: only inject MCPs for skills that the workflow
587
+ // actually references AND whose env vars are present.
588
+ const referencedSkillIds = new Set();
589
+ for (const node of Object.values(workflow.nodes)) {
590
+ for (const id of node.skills)
591
+ referencedSkillIds.add(id);
592
+ }
593
+ const mcpServers = buildSkillMcpServers({
594
+ referencedSkills: referencedSkillIds,
595
+ credentials: buildCredentialMap(),
596
+ userMcpServers: Object.keys(config.mcpServers).length > 0 ? config.mcpServers : undefined,
597
+ });
566
598
  const claude = new ClaudeClient({
567
599
  maxTurns: config.maxInvestigateTurns || 50,
568
600
  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,154 @@ 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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.39",
3
+ "version": "0.1.40",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"