@sweny-ai/core 0.1.51 → 0.1.52

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.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Starter workflow templates offered during `sweny init`.
3
+ *
4
+ * Each template is a valid SWEny workflow YAML. Templates are self-contained
5
+ * strings — no filesystem reads needed, works from npm install.
6
+ */
7
+ export interface WorkflowTemplate {
8
+ id: string;
9
+ name: string;
10
+ description: string;
11
+ yaml: string;
12
+ }
13
+ export declare const WORKFLOW_TEMPLATES: WorkflowTemplate[];
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Starter workflow templates offered during `sweny init`.
3
+ *
4
+ * Each template is a valid SWEny workflow YAML. Templates are self-contained
5
+ * strings — no filesystem reads needed, works from npm install.
6
+ */
7
+ export const WORKFLOW_TEMPLATES = [
8
+ {
9
+ id: "pr-review",
10
+ name: "PR Review Bot",
11
+ description: "Automated code review on pull requests",
12
+ yaml: `id: pr-review
13
+ name: PR Review Bot
14
+ description: Review pull requests for code quality, security issues, and best practices.
15
+ entry: fetch-diff
16
+
17
+ nodes:
18
+ fetch-diff:
19
+ name: Fetch PR Changes
20
+ instruction: |
21
+ Fetch the pull request diff. Identify which files changed,
22
+ what was added, modified, or removed. Summarize the scope
23
+ of the change.
24
+ skills: [github]
25
+
26
+ review-code:
27
+ name: Review Code
28
+ instruction: |
29
+ Review the code changes for:
30
+ - Security vulnerabilities (injection, auth issues, secrets)
31
+ - Logic errors and edge cases
32
+ - Code style and readability
33
+ - Test coverage gaps
34
+
35
+ Be specific — reference file names and line numbers.
36
+ Categorize findings as critical, important, or minor.
37
+ skills: [github]
38
+
39
+ post-review:
40
+ name: Post Review Comment
41
+ instruction: |
42
+ Post a structured review comment on the pull request.
43
+ Lead with a summary, then list findings by severity.
44
+ If no issues found, approve with a brief note.
45
+ skills: [github]
46
+
47
+ edges:
48
+ - from: fetch-diff
49
+ to: review-code
50
+ - from: review-code
51
+ to: post-review
52
+ `,
53
+ },
54
+ {
55
+ id: "issue-triage",
56
+ name: "Issue Triage",
57
+ description: "Classify, prioritize, and label incoming issues",
58
+ yaml: `id: issue-triage
59
+ name: Issue Triage
60
+ description: Automatically classify, prioritize, and label new issues.
61
+ entry: classify
62
+
63
+ nodes:
64
+ classify:
65
+ name: Classify Issue
66
+ instruction: |
67
+ Read the issue title and body. Classify it as one of:
68
+ - bug: Something is broken
69
+ - feature: A new capability request
70
+ - question: Asking for help or clarification
71
+ - chore: Maintenance, refactoring, docs
72
+
73
+ Also assess priority:
74
+ - P0: Production is down or data loss
75
+ - P1: Major feature broken, workaround exists
76
+ - P2: Minor issue, low impact
77
+ - P3: Nice to have, no urgency
78
+ skills: [github]
79
+
80
+ label-and-assign:
81
+ name: Apply Labels
82
+ instruction: |
83
+ Based on the classification, apply the appropriate labels
84
+ to the issue (e.g. "bug", "P1", "needs-triage").
85
+ Add a comment explaining the classification rationale.
86
+ skills: [github]
87
+
88
+ edges:
89
+ - from: classify
90
+ to: label-and-assign
91
+ `,
92
+ },
93
+ {
94
+ id: "security-scan",
95
+ name: "Security Audit",
96
+ description: "Scan code and dependencies for security issues",
97
+ yaml: `id: security-scan
98
+ name: Security Audit
99
+ description: Scan repository code and dependencies for security vulnerabilities.
100
+ entry: scan-code
101
+
102
+ nodes:
103
+ scan-code:
104
+ name: Scan Code for Secrets
105
+ instruction: |
106
+ Search the repository for exposed secrets, API keys,
107
+ passwords, and tokens in code, config files, and
108
+ environment templates. Check for common patterns:
109
+ hardcoded credentials, leaked keys, insecure defaults.
110
+ skills: [github]
111
+
112
+ scan-deps:
113
+ name: Scan Dependencies
114
+ instruction: |
115
+ Review dependency manifests (package.json, requirements.txt,
116
+ go.mod, etc.) for known vulnerable versions. Check for
117
+ outdated packages with security advisories.
118
+ skills: [github]
119
+
120
+ compile-report:
121
+ name: Compile Security Report
122
+ instruction: |
123
+ Compile findings from the code and dependency scans into
124
+ a structured security report. Categorize by severity
125
+ (critical, high, medium, low). Include remediation steps
126
+ for each finding.
127
+ skills: [github]
128
+
129
+ edges:
130
+ - from: scan-code
131
+ to: scan-deps
132
+ - from: scan-deps
133
+ to: compile-report
134
+ `,
135
+ },
136
+ {
137
+ id: "release-notes",
138
+ name: "Release Notes",
139
+ description: "Generate release notes from commits and PRs",
140
+ yaml: `id: release-notes
141
+ name: Release Notes Generator
142
+ description: Generate structured release notes from recent commits and merged PRs.
143
+ entry: gather
144
+
145
+ nodes:
146
+ gather:
147
+ name: Gather Changes
148
+ instruction: |
149
+ List all commits and merged pull requests since the last
150
+ release tag. For each, extract: title, author, PR number,
151
+ and a brief description of what changed.
152
+ skills: [github]
153
+
154
+ categorize:
155
+ name: Categorize Changes
156
+ instruction: |
157
+ Group the changes into categories:
158
+ - Features: New capabilities
159
+ - Fixes: Bug fixes
160
+ - Improvements: Performance, refactoring, DX
161
+ - Breaking Changes: Anything that requires migration
162
+
163
+ Flag any breaking changes prominently.
164
+ skills: [github]
165
+
166
+ write-notes:
167
+ name: Write Release Notes
168
+ instruction: |
169
+ Write polished release notes in markdown. Lead with
170
+ highlights, then list changes by category. Credit
171
+ contributors. Keep it concise but informative.
172
+ skills: [github]
173
+
174
+ edges:
175
+ - from: gather
176
+ to: categorize
177
+ - from: categorize
178
+ to: write-notes
179
+ `,
180
+ },
181
+ ];
package/dist/executor.js CHANGED
@@ -21,7 +21,11 @@ import { consoleLogger } from "./types.js";
21
21
  * - `trace`: full ordered execution trace including loops and routing decisions
22
22
  */
23
23
  export async function execute(workflow, input, options) {
24
- const { skills, claude, observer } = options;
24
+ const { claude, observer } = options;
25
+ // Merge inline workflow skills into the skill map so they resolve at runtime.
26
+ // Inline skills (instruction/mcp only) become Skill objects with empty tools/config.
27
+ // The caller's skill map takes precedence — inline skills only fill gaps.
28
+ const skills = mergeInlineSkills(options.skills, workflow.skills);
25
29
  const config = resolveConfig(skills, options.config);
26
30
  const logger = options.logger ?? consoleLogger;
27
31
  const results = new Map();
@@ -41,11 +45,13 @@ export async function execute(workflow, input, options) {
41
45
  logger.info(`→ ${node.name}`, { node: currentId });
42
46
  // Gather tools from the node's skills
43
47
  const tools = resolveTools(node.skills, skills);
48
+ const skillInstructions = resolveSkillInstructions(node.skills, skills);
44
49
  // Runtime guard: if this node declares skills but none resolved, the node
45
50
  // cannot do its job (e.g. "create a Linear issue" with no linear skill).
46
51
  // The startup validate() warns about this possibility, but only throw when
47
52
  // the node is actually reached — unreachable nodes with missing skills are fine.
48
- if (node.skills.length > 0 && tools.length === 0) {
53
+ // Instruction-only skills (no tools) are valid — they inject context into the prompt.
54
+ if (node.skills.length > 0 && tools.length === 0 && skillInstructions.length === 0) {
49
55
  throw new Error(`Node "${currentId}" requires skills [${node.skills.join(", ")}] but none are configured. ` +
50
56
  `Set the required environment variables and try again.`);
51
57
  }
@@ -66,7 +72,7 @@ export async function execute(workflow, input, options) {
66
72
  },
67
73
  }));
68
74
  // Prepend rules and context to instruction if provided
69
- const instruction = buildNodeInstruction(node.instruction, input);
75
+ const instruction = buildNodeInstruction(node.instruction, input, skillInstructions);
70
76
  // Run Claude on this node
71
77
  const result = await claude.run({
72
78
  instruction,
@@ -116,30 +122,35 @@ export async function execute(workflow, input, options) {
116
122
  * Rules get "You MUST follow" framing; context gets "Background" framing.
117
123
  * Falls back to legacy `additionalContext` if rules/context aren't set.
118
124
  */
119
- function buildNodeInstruction(baseInstruction, input) {
125
+ function buildNodeInstruction(baseInstruction, input, skillInstructions) {
120
126
  const inp = input;
121
- if (!inp)
122
- return baseInstruction;
123
127
  const sections = [];
124
- // New structured format
125
- const rules = typeof inp.rules === "string" && inp.rules ? inp.rules : "";
126
- const context = typeof inp.context === "string" && inp.context ? inp.context : "";
127
- if (rules) {
128
- sections.push(`## Rules — You MUST Follow These\n\n${rules}`);
129
- }
130
- if (context) {
131
- sections.push(`## Background Context\n\n${context}`);
128
+ if (inp) {
129
+ const rules = typeof inp.rules === "string" && inp.rules ? inp.rules : "";
130
+ const context = typeof inp.context === "string" && inp.context ? inp.context : "";
131
+ if (rules) {
132
+ sections.push(`## Rules — You MUST Follow These\n\n${rules}`);
133
+ }
134
+ if (context) {
135
+ sections.push(`## Background Context\n\n${context}`);
136
+ }
137
+ // Legacy fallback
138
+ if (sections.length === 0) {
139
+ const legacy = typeof inp.additionalContext === "string" ? inp.additionalContext : "";
140
+ if (legacy) {
141
+ sections.push(`## Additional Context & Rules\n\n${legacy}`);
142
+ }
143
+ }
132
144
  }
133
- // Legacy fallback
134
- if (sections.length === 0) {
135
- const legacy = typeof inp.additionalContext === "string" ? inp.additionalContext : "";
136
- if (legacy) {
137
- sections.push(`## Additional Context & Rules\n\n${legacy}`);
145
+ // Skill instructions — injected between context and base instruction
146
+ if (skillInstructions && skillInstructions.length > 0) {
147
+ for (const { name, instruction } of skillInstructions) {
148
+ sections.push(`## Skill: ${name}\n\n${instruction}`);
138
149
  }
139
150
  }
140
151
  if (sections.length === 0)
141
152
  return baseInstruction;
142
- return `${sections.join("\n\n")}\n\n---\n\n${baseInstruction}`;
153
+ return `${sections.join("\n\n---\n\n")}\n\n---\n\n${baseInstruction}`;
143
154
  }
144
155
  /**
145
156
  * Call observer without letting exceptions crash the workflow.
@@ -157,12 +168,44 @@ function safeObserve(observer, event, logger) {
157
168
  (logger ?? consoleLogger).warn(`Observer error (non-fatal): ${err.message}`);
158
169
  }
159
170
  }
171
+ /**
172
+ * Merge inline workflow skill definitions into the skill map.
173
+ * Inline skills (from workflow.skills) become Skill objects with empty tools/config.
174
+ * The caller's skill map takes precedence — inline skills only fill gaps.
175
+ */
176
+ function mergeInlineSkills(skills, inlineSkills) {
177
+ if (!inlineSkills || Object.keys(inlineSkills).length === 0)
178
+ return skills;
179
+ const merged = new Map(skills);
180
+ for (const [id, def] of Object.entries(inlineSkills)) {
181
+ if (merged.has(id))
182
+ continue; // caller-provided skill takes precedence
183
+ merged.set(id, {
184
+ id,
185
+ name: def.name ?? id,
186
+ description: def.description ?? `Inline skill: ${id}`,
187
+ category: "general",
188
+ config: {},
189
+ tools: [],
190
+ instruction: def.instruction,
191
+ mcp: def.mcp,
192
+ });
193
+ }
194
+ return merged;
195
+ }
160
196
  function resolveTools(skillIds, skills) {
161
197
  return skillIds
162
198
  .map((id) => skills.get(id))
163
199
  .filter((s) => s != null)
164
200
  .flatMap((s) => s.tools);
165
201
  }
202
+ /** Collect instruction strings from skills that have them, in array order. */
203
+ function resolveSkillInstructions(skillIds, skills) {
204
+ return skillIds
205
+ .map((id) => skills.get(id))
206
+ .filter((s) => s != null && s.instruction != null)
207
+ .map((s) => ({ name: s.name, instruction: s.instruction }));
208
+ }
166
209
  /**
167
210
  * Resolve config values: check explicit overrides first, then env vars.
168
211
  * Throws if a required field is missing.
@@ -313,3 +313,236 @@ describe("file skill", () => {
313
313
  expect(existsSync(path.join(outputDir, "deep/nested/file.json"))).toBe(true);
314
314
  });
315
315
  });
316
+ // ─── Skill instruction injection tests ──────────────────────────
317
+ describe("skill instruction injection", () => {
318
+ let outputDir;
319
+ beforeEach(() => {
320
+ outputDir = freshDir("skill-inject");
321
+ });
322
+ it("injects skill instructions into the prompt", async () => {
323
+ const instructionSkill = {
324
+ id: "code-standards",
325
+ name: "Code Standards",
326
+ description: "Team conventions",
327
+ category: "general",
328
+ config: {},
329
+ tools: [],
330
+ instruction: "Always use camelCase for variable names.",
331
+ };
332
+ const workflow = {
333
+ id: "test-inject",
334
+ name: "Inject Test",
335
+ description: "",
336
+ entry: "step",
337
+ nodes: {
338
+ step: {
339
+ name: "Do Work",
340
+ instruction: "Write some code.",
341
+ skills: ["code-standards"],
342
+ },
343
+ },
344
+ edges: [],
345
+ };
346
+ let capturedInstruction = "";
347
+ const claude = {
348
+ async run(opts) {
349
+ capturedInstruction = opts.instruction;
350
+ return { status: "success", data: {}, toolCalls: [] };
351
+ },
352
+ async evaluate() {
353
+ return "";
354
+ },
355
+ };
356
+ await execute(workflow, {}, {
357
+ skills: createSkillMap([instructionSkill]),
358
+ claude,
359
+ config: {},
360
+ });
361
+ expect(capturedInstruction).toContain("## Skill: Code Standards");
362
+ expect(capturedInstruction).toContain("Always use camelCase for variable names.");
363
+ expect(capturedInstruction).toContain("Write some code.");
364
+ });
365
+ it("injects multiple skill instructions in array order", async () => {
366
+ const skillA = {
367
+ id: "skill-a",
368
+ name: "Skill A",
369
+ description: "First",
370
+ category: "general",
371
+ config: {},
372
+ tools: [],
373
+ instruction: "AAA instruction",
374
+ };
375
+ const skillB = {
376
+ id: "skill-b",
377
+ name: "Skill B",
378
+ description: "Second",
379
+ category: "general",
380
+ config: {},
381
+ tools: [],
382
+ instruction: "BBB instruction",
383
+ };
384
+ const workflow = {
385
+ id: "test-multi",
386
+ name: "Multi Inject",
387
+ description: "",
388
+ entry: "step",
389
+ nodes: {
390
+ step: {
391
+ name: "Work",
392
+ instruction: "Do the work.",
393
+ skills: ["skill-a", "skill-b"],
394
+ },
395
+ },
396
+ edges: [],
397
+ };
398
+ let capturedInstruction = "";
399
+ const claude = {
400
+ async run(opts) {
401
+ capturedInstruction = opts.instruction;
402
+ return { status: "success", data: {}, toolCalls: [] };
403
+ },
404
+ async evaluate() {
405
+ return "";
406
+ },
407
+ };
408
+ await execute(workflow, {}, {
409
+ skills: createSkillMap([skillA, skillB]),
410
+ claude,
411
+ config: {},
412
+ });
413
+ const posA = capturedInstruction.indexOf("AAA instruction");
414
+ const posB = capturedInstruction.indexOf("BBB instruction");
415
+ const posBase = capturedInstruction.indexOf("Do the work.");
416
+ expect(posA).toBeLessThan(posB);
417
+ expect(posB).toBeLessThan(posBase);
418
+ });
419
+ it("allows instruction-only nodes (no tools, no error)", async () => {
420
+ const instructionSkill = {
421
+ id: "rubric",
422
+ name: "Evaluation Rubric",
423
+ description: "Scoring criteria",
424
+ category: "general",
425
+ config: {},
426
+ tools: [],
427
+ instruction: "Score quality 1-5.",
428
+ };
429
+ const workflow = {
430
+ id: "test-notools",
431
+ name: "No Tools",
432
+ description: "",
433
+ entry: "judge",
434
+ nodes: {
435
+ judge: {
436
+ name: "Judge",
437
+ instruction: "Evaluate the output.",
438
+ skills: ["rubric"],
439
+ },
440
+ },
441
+ edges: [],
442
+ };
443
+ const claude = {
444
+ async run() {
445
+ return { status: "success", data: { score: 4 }, toolCalls: [] };
446
+ },
447
+ async evaluate() {
448
+ return "";
449
+ },
450
+ };
451
+ // Should NOT throw "none are configured"
452
+ const { results } = await execute(workflow, {}, {
453
+ skills: createSkillMap([instructionSkill]),
454
+ claude,
455
+ config: {},
456
+ });
457
+ expect(results.get("judge")?.status).toBe("success");
458
+ });
459
+ it("wires inline workflow.skills into the executor automatically", async () => {
460
+ const workflow = {
461
+ id: "test-inline",
462
+ name: "Inline Skills",
463
+ description: "",
464
+ entry: "review",
465
+ nodes: {
466
+ review: {
467
+ name: "Review",
468
+ instruction: "Review the code.",
469
+ skills: ["my-rubric"],
470
+ },
471
+ },
472
+ edges: [],
473
+ skills: {
474
+ "my-rubric": {
475
+ name: "My Rubric",
476
+ instruction: "Check for security issues first, then correctness.",
477
+ },
478
+ },
479
+ };
480
+ let capturedInstruction = "";
481
+ const claude = {
482
+ async run(opts) {
483
+ capturedInstruction = opts.instruction;
484
+ return { status: "success", data: {}, toolCalls: [] };
485
+ },
486
+ async evaluate() {
487
+ return "";
488
+ },
489
+ };
490
+ // No "my-rubric" in the skill map — executor must pull it from workflow.skills
491
+ const { results } = await execute(workflow, {}, {
492
+ skills: createSkillMap([]),
493
+ claude,
494
+ config: {},
495
+ });
496
+ expect(results.get("review")?.status).toBe("success");
497
+ expect(capturedInstruction).toContain("## Skill: My Rubric");
498
+ expect(capturedInstruction).toContain("Check for security issues first");
499
+ });
500
+ it("caller-provided skills take precedence over inline workflow.skills", async () => {
501
+ const callerSkill = {
502
+ id: "shared",
503
+ name: "Caller Version",
504
+ description: "From caller",
505
+ category: "general",
506
+ config: {},
507
+ tools: [],
508
+ instruction: "Caller instruction wins.",
509
+ };
510
+ const workflow = {
511
+ id: "test-precedence",
512
+ name: "Precedence",
513
+ description: "",
514
+ entry: "step",
515
+ nodes: {
516
+ step: {
517
+ name: "Step",
518
+ instruction: "Do work.",
519
+ skills: ["shared"],
520
+ },
521
+ },
522
+ edges: [],
523
+ skills: {
524
+ shared: {
525
+ name: "Inline Version",
526
+ instruction: "Inline instruction should NOT appear.",
527
+ },
528
+ },
529
+ };
530
+ let capturedInstruction = "";
531
+ const claude = {
532
+ async run(opts) {
533
+ capturedInstruction = opts.instruction;
534
+ return { status: "success", data: {}, toolCalls: [] };
535
+ },
536
+ async evaluate() {
537
+ return "";
538
+ },
539
+ };
540
+ await execute(workflow, {}, {
541
+ skills: createSkillMap([callerSkill]),
542
+ claude,
543
+ config: {},
544
+ });
545
+ expect(capturedInstruction).toContain("Caller instruction wins.");
546
+ expect(capturedInstruction).not.toContain("Inline instruction should NOT appear.");
547
+ });
548
+ });
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  * })
22
22
  * ```
23
23
  */
24
- export type { Skill, SkillCategory, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, } from "./types.js";
24
+ export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, } from "./types.js";
25
25
  export { consoleLogger } from "./types.js";
26
26
  export { execute } from "./executor.js";
27
27
  export type { ExecuteOptions } from "./executor.js";
@@ -29,11 +29,11 @@ export { ClaudeClient } from "./claude.js";
29
29
  export type { ClaudeClientOptions } from "./claude.js";
30
30
  export { github, linear, slack, sentry, datadog, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
31
31
  export type { SkillValidationResult } from "./skills/index.js";
32
- export { loadCustomSkills, configuredSkills } from "./skills/custom-loader.js";
33
- export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
32
+ export { loadCustomSkills, discoverSkills, configuredSkills } from "./skills/custom-loader.js";
33
+ export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, parseWorkflow, validateWorkflow, workflowJsonSchema, } from "./schema.js";
34
34
  export type { WorkflowError } from "./schema.js";
35
- export { buildAutoMcpServers, buildProviderContext } from "./mcp.js";
36
- export type { ProviderContextOptions } from "./mcp.js";
35
+ export { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "./mcp.js";
36
+ export type { ProviderContextOptions, SkillMcpOptions } from "./mcp.js";
37
37
  export type { McpServerConfig, McpAutoConfig } from "./types.js";
38
38
  export { buildWorkflow, refineWorkflow } from "./workflow-builder.js";
39
39
  export type { BuildWorkflowOptions } from "./workflow-builder.js";
package/dist/index.js CHANGED
@@ -29,11 +29,11 @@ export { ClaudeClient } from "./claude.js";
29
29
  // Skills
30
30
  export { github, linear, slack, sentry, datadog, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredBuiltinSkills, validateWorkflowSkills, } from "./skills/index.js";
31
31
  // Node-only skill discovery (filesystem-based)
32
- export { loadCustomSkills, configuredSkills } from "./skills/custom-loader.js";
32
+ export { loadCustomSkills, discoverSkills, configuredSkills } from "./skills/custom-loader.js";
33
33
  // Schema & validation
34
- export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
34
+ export { workflowZ, nodeZ, edgeZ, skillZ, mcpServerConfigZ, skillDefinitionZ, parseWorkflow, validateWorkflow, workflowJsonSchema, } from "./schema.js";
35
35
  // MCP auto-injection
36
- export { buildAutoMcpServers, buildProviderContext } from "./mcp.js";
36
+ export { buildAutoMcpServers, buildSkillMcpServers, buildProviderContext } from "./mcp.js";
37
37
  // Workflow builder
38
38
  export { buildWorkflow, refineWorkflow } from "./workflow-builder.js";
39
39
  // Templates
package/dist/mcp.d.ts CHANGED
@@ -6,6 +6,8 @@ export interface SkillMcpOptions {
6
6
  credentials: Record<string, string>;
7
7
  /** User-supplied MCP servers — always win on key conflict. */
8
8
  userMcpServers?: Record<string, McpServerConfig>;
9
+ /** MCP servers declared by custom skills. Only wired when the skill is referenced. */
10
+ skillMcpServers?: Record<string, McpServerConfig>;
9
11
  }
10
12
  /**
11
13
  * Build MCP server configs for the skills a workflow references.
package/dist/mcp.js CHANGED
@@ -155,6 +155,14 @@ export function buildSkillMcpServers(opts) {
155
155
  env: { ASANA_ACCESS_TOKEN: creds.ASANA_ACCESS_TOKEN },
156
156
  };
157
157
  }
158
+ // Skill-declared MCP servers — only include for referenced skills.
159
+ if (opts.skillMcpServers) {
160
+ for (const [skillId, mcpConfig] of Object.entries(opts.skillMcpServers)) {
161
+ if (refs.has(skillId) && !auto[skillId]) {
162
+ auto[skillId] = mcpConfig;
163
+ }
164
+ }
165
+ }
158
166
  // User-supplied servers always win on key conflict.
159
167
  return { ...auto, ...(opts.userMcpServers ?? {}) };
160
168
  }