@edgehero/pi-dispatch-receiver 4.1.0 → 4.2.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgehero/pi-dispatch-receiver",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "type": "module",
5
5
  "description": "The pi-dispatch trigger edge: verifies GitHub, GitLab, Forgejo and Azure DevOps webhooks, or polls GitHub with no public URL, and queues one pi coding agent job per event (per replica, if set) for the worker.",
6
6
  "keywords": [
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@octokit/webhooks": "14.2.0",
51
- "@edgehero/pi-dispatch": "^4.1.0",
51
+ "@edgehero/pi-dispatch": "^4.2.0",
52
52
  "bullmq": "5.80.4",
53
53
  "ioredis": "5.11.1"
54
54
  }
package/src/config.mjs CHANGED
@@ -318,9 +318,9 @@ function loadTriggers(env, readFile, fileExists) {
318
318
  }
319
319
  const group = groups[run.kind];
320
320
  if (on.type === "label") {
321
- group.label.push({ index, predicate: { any: on.any, all: on.all, none: on.none }, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas, repository: run.repository });
321
+ group.label.push({ index, predicate: { any: on.any, all: on.all, none: on.none }, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, mcp: run.mcp, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas, repository: run.repository });
322
322
  } else if (on.type === "comment") {
323
- group.comment = { index, phrase: on.phrase, defaultFlow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas, repository: run.repository }; // parseTriggers guarantees at most one per forge
323
+ group.comment = { index, phrase: on.phrase, defaultFlow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, mcp: run.mcp, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas, repository: run.repository }; // parseTriggers guarantees at most one per forge
324
324
  } else if (on.type === "pull_request") {
325
325
  // The close-only split (issue #231): a close rule routes through the close gate (the CLOSER's
326
326
  // resolved authority) and every other PR rule through the author gate, so they live in separate
@@ -329,17 +329,17 @@ function loadTriggers(env, readFile, fileExists) {
329
329
  // split reads the SHARED close-word table, never a re-typed word.
330
330
  const closeWord = PR_CLOSE_ACTIONS[run.kind];
331
331
  if (closeWord !== undefined && on.action.includes(closeWord)) {
332
- group.prClose.push({ index, actions: new Set(on.action), number: on.number, once: on.once, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
332
+ group.prClose.push({ index, actions: new Set(on.action), number: on.number, once: on.once, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, mcp: run.mcp, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
333
333
  } else {
334
334
  // `reviewStates` is null rather than an empty Set when unnarrowed: the filter tests it for
335
335
  // presence, and an empty Set would read as "no verdict matches" and silently refuse everything.
336
- group.pullRequest.push({ index, actions: new Set(on.action), reviewStates: on.reviewState ? new Set(on.reviewState) : null, predicate: { any: on.any, all: on.all, none: on.none }, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
336
+ group.pullRequest.push({ index, actions: new Set(on.action), reviewStates: on.reviewState ? new Set(on.reviewState) : null, predicate: { any: on.any, all: on.all, none: on.none }, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, mcp: run.mcp, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
337
337
  }
338
338
  } else if (on.type === "issue") {
339
339
  // The close-trigger kind (issue #231). No predicate and no reviewStates by construction (the
340
340
  // loader refused both), and `number`/`once` ride conditionally-present exactly as the loader
341
341
  // normalized them -- the filter's close route reads them, nothing else does.
342
- group.issue.push({ index, actions: new Set(on.action), number: on.number, once: on.once, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
342
+ group.issue.push({ index, actions: new Set(on.action), number: on.number, once: on.once, flow: run.flow, command: run.command, packages: run.packages, image: run.image, backend: run.backend, excludeTools: run.excludeTools, mcp: run.mcp, provider: run.provider, model: run.model, maxTurns: run.maxTurns, models: run.models, maxCostUsd: run.maxCostUsd, skillsDir: run.skillsDir, instructions: run.instructions, resume: run.resume, secrets: run.secrets, secretsProfile: run.secretsProfile, waitFor: run.waitFor, replicas: run.replicas });
343
343
  }
344
344
  }
345
345
 
@@ -117,6 +117,9 @@ export function filterAzure(subset, triggers, knownFlows, selfId, authorized, de
117
117
  ...(resolved.backend !== undefined ? { backend: resolved.backend } : {}),
118
118
  // #291. A separate spread, filter.mjs's backend rule: a dropped exclusion runs the job WITH the tool.
119
119
  ...(resolved.excludeTools !== undefined ? { excludeTools: resolved.excludeTools } : {}),
120
+ // REQ-MCP-OPERATOR-SERVERS. Its own spread, the excludeTools rule above: a dropped list runs
121
+ // the job without its servers.
122
+ ...(resolved.mcp !== undefined ? { mcp: resolved.mcp } : {}),
120
123
  // #502. One spread PER FIELD, the excludeTools rule above: folded into one conditional, a trigger
121
124
  // that named only a model would have its provider's absence decide whether the model survives,
122
125
  // and a dropped model runs the job on the deployment default while the file reads as chosen.
@@ -203,7 +206,7 @@ function matchLabelRules(subset, triggers, labels, action) {
203
206
  ...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
204
207
  repository: rule.repository,
205
208
  packages: rule.packages,
206
- image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
209
+ image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
207
210
  provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
208
211
  skillsDir: rule.skillsDir,
209
212
  secrets: rule.secrets,
@@ -248,7 +251,7 @@ function routeComment(subset, triggers, knownFlows, targetType) {
248
251
  ...(command !== undefined ? { command } : { flow }),
249
252
  repository: triggers.comment.repository,
250
253
  packages: triggers.comment.packages,
251
- image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools,
254
+ image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools, mcp: triggers.comment.mcp,
252
255
  provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, maxCostUsd: triggers.comment.maxCostUsd,
253
256
  skillsDir: triggers.comment.skillsDir,
254
257
  secrets: triggers.comment.secrets,
@@ -282,7 +285,7 @@ function routePullRequest(subset, triggers, action) {
282
285
  ...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
283
286
  repository: rule.repository,
284
287
  packages: rule.packages,
285
- image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
288
+ image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
286
289
  provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
287
290
  skillsDir: rule.skillsDir,
288
291
  secrets: rule.secrets,
@@ -131,6 +131,9 @@ export function filterForgejo(eventName, subset, triggers, knownFlows, selfId, a
131
131
  ...(resolved.backend !== undefined ? { backend: resolved.backend } : {}),
132
132
  // #291. A separate spread, filter.mjs's backend rule: a dropped exclusion runs the job WITH the tool.
133
133
  ...(resolved.excludeTools !== undefined ? { excludeTools: resolved.excludeTools } : {}),
134
+ // REQ-MCP-OPERATOR-SERVERS. Its own spread, the excludeTools rule above: a dropped list runs
135
+ // the job without its servers.
136
+ ...(resolved.mcp !== undefined ? { mcp: resolved.mcp } : {}),
134
137
  // #502. One spread PER FIELD, the excludeTools rule above: folded into one conditional, a trigger
135
138
  // that named only a model would have its provider's absence decide whether the model survives,
136
139
  // and a dropped model runs the job on the deployment default while the file reads as chosen.
@@ -183,7 +186,7 @@ function routeIssueLabel(subset, triggers) {
183
186
  // A command rule (issue #189) skips flow resolution entirely: the label match IS the dispatch.
184
187
  ...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
185
188
  packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
186
- image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
189
+ image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
187
190
  provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
188
191
  skillsDir: rule.skillsDir,
189
192
  secrets: rule.secrets,
@@ -233,7 +236,7 @@ function routeComment(subset, triggers, knownFlows) {
233
236
  enqueue: true,
234
237
  ...(command !== undefined ? { command } : { flow }),
235
238
  packages: triggers.comment.packages,
236
- image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools,
239
+ image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools, mcp: triggers.comment.mcp,
237
240
  provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, maxCostUsd: triggers.comment.maxCostUsd,
238
241
  skillsDir: triggers.comment.skillsDir,
239
242
  secrets: triggers.comment.secrets,
@@ -265,7 +268,7 @@ function routePullRequest(subset, triggers, action) {
265
268
  // A command rule (issue #189) skips flow resolution entirely: the rule match IS the dispatch.
266
269
  ...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
267
270
  packages: rule.packages,
268
- image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
271
+ image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
269
272
  provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
270
273
  skillsDir: rule.skillsDir,
271
274
  secrets: rule.secrets,
@@ -321,7 +324,7 @@ function routeIssueClose(subset, triggers, authorized) {
321
324
  enqueue: true,
322
325
  ...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
323
326
  packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
324
- image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
327
+ image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
325
328
  provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
326
329
  skillsDir: rule.skillsDir,
327
330
  secrets: rule.secrets,
@@ -358,7 +361,7 @@ function routePullRequestClose(subset, triggers, authorized) {
358
361
  enqueue: true,
359
362
  ...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
360
363
  packages: rule.packages,
361
- image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
364
+ image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
362
365
  provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
363
366
  skillsDir: rule.skillsDir,
364
367
  secrets: rule.secrets,
@@ -134,6 +134,9 @@ export function filterGitLab(subset, triggers, knownFlows, selfId, authorized, d
134
134
  ...(resolved.backend !== undefined ? { backend: resolved.backend } : {}),
135
135
  // #291. A separate spread, filter.mjs's backend rule: a dropped exclusion runs the job WITH the tool.
136
136
  ...(resolved.excludeTools !== undefined ? { excludeTools: resolved.excludeTools } : {}),
137
+ // REQ-MCP-OPERATOR-SERVERS. Its own spread, the excludeTools rule above: a dropped list runs
138
+ // the job without its servers.
139
+ ...(resolved.mcp !== undefined ? { mcp: resolved.mcp } : {}),
137
140
  // #502. One spread PER FIELD, the excludeTools rule above: folded into one conditional, a trigger
138
141
  // that named only a model would have its provider's absence decide whether the model survives,
139
142
  // and a dropped model runs the job on the deployment default while the file reads as chosen.
@@ -193,7 +196,7 @@ function routeLabel(subset, triggers, targetType) {
193
196
  // A command rule (issue #189) skips flow resolution entirely: the label match IS the dispatch.
194
197
  ...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
195
198
  packages: rule.packages,
196
- image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
199
+ image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
197
200
  provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
198
201
  skillsDir: rule.skillsDir,
199
202
  secrets: rule.secrets,
@@ -258,7 +261,7 @@ function mrResult(subset, rule, matched) {
258
261
  // A command rule (issue #189) skips flow resolution entirely: the rule match IS the dispatch.
259
262
  ...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
260
263
  packages: rule.packages,
261
- image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
264
+ image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
262
265
  provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
263
266
  skillsDir: rule.skillsDir,
264
267
  secrets: rule.secrets,
@@ -305,7 +308,7 @@ function routeIssueClose(subset, triggers, authorized) {
305
308
  enqueue: true,
306
309
  ...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
307
310
  packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
308
- image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
311
+ image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
309
312
  provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
310
313
  skillsDir: rule.skillsDir,
311
314
  secrets: rule.secrets,
@@ -340,7 +343,7 @@ function routeMergeRequestClose(subset, triggers, authorized) {
340
343
  enqueue: true,
341
344
  ...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
342
345
  packages: rule.packages,
343
- image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
346
+ image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
344
347
  provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
345
348
  skillsDir: rule.skillsDir,
346
349
  secrets: rule.secrets,
@@ -393,7 +396,7 @@ function routeNote(subset, triggers, knownFlows) {
393
396
  enqueue: true,
394
397
  ...(command !== undefined ? { command } : { flow }),
395
398
  packages: triggers.comment.packages,
396
- image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools,
399
+ image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools, mcp: triggers.comment.mcp,
397
400
  provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, maxCostUsd: triggers.comment.maxCostUsd,
398
401
  skillsDir: triggers.comment.skillsDir,
399
402
  secrets: triggers.comment.secrets,
package/src/filter.mjs CHANGED
@@ -157,6 +157,9 @@ export function filter(eventName, subset, cfg, selfId, deliveryId, closerAuthori
157
157
  // dropped exclusion runs the job WITH the tool, the destructive absence the loader's own sweep
158
158
  // refuses a misspelling for, arriving through the plumbing instead of the spelling.
159
159
  ...(resolved.excludeTools !== undefined ? { excludeTools: resolved.excludeTools } : {}),
160
+ // REQ-MCP-OPERATOR-SERVERS. Its own spread, the excludeTools rule above: a dropped list runs
161
+ // the job without its servers.
162
+ ...(resolved.mcp !== undefined ? { mcp: resolved.mcp } : {}),
160
163
  // #502. One spread PER FIELD, the excludeTools rule above: folded into one conditional, a trigger
161
164
  // that named only a model would have its provider's absence decide whether the model survives,
162
165
  // and a dropped model runs the job on the deployment default while the file reads as chosen.
@@ -221,7 +224,7 @@ function routeIssueLabel(subset, triggers) {
221
224
  // A command rule (issue #189) skips flow resolution entirely: the label match IS the dispatch.
222
225
  ...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
223
226
  packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
224
- image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
227
+ image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
225
228
  provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
226
229
  skillsDir: rule.skillsDir,
227
230
  secrets: rule.secrets,
@@ -282,7 +285,7 @@ function routeComment(subset, triggers, knownFlows) {
282
285
  // The single comment trigger IS the matched rule here, so its opt-in is the job's. A `<phrase>
283
286
  // <flow>` override changes WHICH flow runs, never which triggers.json entry authorized it.
284
287
  packages: triggers.comment.packages,
285
- image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools,
288
+ image: triggers.comment.image, backend: triggers.comment.backend, excludeTools: triggers.comment.excludeTools, mcp: triggers.comment.mcp,
286
289
  provider: triggers.comment.provider, model: triggers.comment.model, maxTurns: triggers.comment.maxTurns, models: triggers.comment.models, maxCostUsd: triggers.comment.maxCostUsd,
287
290
  skillsDir: triggers.comment.skillsDir,
288
291
  secrets: triggers.comment.secrets,
@@ -381,7 +384,7 @@ function routePullRequest(subset, triggers, action) {
381
384
  // A command rule (issue #189) skips flow resolution entirely: the rule match IS the dispatch.
382
385
  ...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
383
386
  packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
384
- image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
387
+ image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
385
388
  provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
386
389
  skillsDir: rule.skillsDir,
387
390
  secrets: rule.secrets,
@@ -498,7 +501,7 @@ function routeClose(rules, number, closerAuthorized, matchedFor, targetFor) {
498
501
  // A command rule (issue #189) skips flow resolution entirely: the rule match IS the dispatch.
499
502
  ...(rule.command !== undefined ? { command: rule.command } : { flow: rule.flow }),
500
503
  packages: rule.packages, // the MATCHED rule's fields -- rules in one file may differ on them
501
- image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools,
504
+ image: rule.image, backend: rule.backend, excludeTools: rule.excludeTools, mcp: rule.mcp,
502
505
  provider: rule.provider, model: rule.model, maxTurns: rule.maxTurns, models: rule.models, maxCostUsd: rule.maxCostUsd,
503
506
  skillsDir: rule.skillsDir,
504
507
  secrets: rule.secrets,