diff --git a/actions/setup/js/add_labels.cjs b/actions/setup/js/add_labels.cjs index fc08cb270ab..c8266f685ea 100644 --- a/actions/setup/js/add_labels.cjs +++ b/actions/setup/js/add_labels.cjs @@ -44,7 +44,8 @@ const main = createCountGatedHandler({ handlerType: HANDLER_TYPE, setup: async (config, maxCount, isStaged) => { const { allowed: allowedLabels = [], blocked: blockedPatterns = [] } = config; - const issueIntentEnabled = config.issue_intent === true; + const issueIntentEnabled = config.issue_intent !== false; + const issueIntentStrict = config.issue_intent === true; // strict mode: plain-string labels rejected, metadata required const requiredLabels = Array.isArray(config.required_labels) ? config.required_labels : []; const requiredTitlePrefix = config.required_title_prefix || ""; const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config); @@ -98,6 +99,24 @@ const main = createCountGatedHandler({ let requestedLabelNames; try { const requestedLabelInputs = normalizeIssueIntentLabelInputs(requestedLabels); + + // In strict mode (issue_intent: true), reject plain string labels — metadata is required + if (issueIntentStrict) { + const plainStringLabels = requestedLabelInputs.filter(label => typeof label === "string"); + if (plainStringLabels.length > 0) { + const error = `Plain string label names are not permitted when issue_intent is explicitly enabled. Provide label objects with a "name" field and intent metadata (rationale, confidence). Plain labels: ${plainStringLabels.map(l => JSON.stringify(l)).join(", ")}`; + core.warning(error); + return { success: false, error }; + } + // In strict mode, objects without rationale and confidence are also rejected + const missingMetadataLabels = requestedLabelInputs.filter(label => typeof label === "object" && (!label.rationale || !label.confidence)); + if (missingMetadataLabels.length > 0) { + const error = `Label objects must include both "rationale" and "confidence" when issue_intent is explicitly enabled. Missing metadata on: ${missingMetadataLabels.map(l => JSON.stringify(l.name)).join(", ")}`; + core.warning(error); + return { success: false, error }; + } + } + requestedLabelNames = requestedLabelInputs.map(label => { if (typeof label === "string") { return label; diff --git a/actions/setup/js/add_labels.test.cjs b/actions/setup/js/add_labels.test.cjs index e27ebf0406f..f30d722a5fc 100644 --- a/actions/setup/js/add_labels.test.cjs +++ b/actions/setup/js/add_labels.test.cjs @@ -513,7 +513,8 @@ describe("add_labels", () => { }); it("should prefer the metadata-bearing entry when a duplicate label name appears", async () => { - const handler = await main({ max: 10, issue_intent: true }); + // Default (omitted issue_intent) accepts both strings and objects; deduplication favours the metadata-bearing entry. + const handler = await main({ max: 10 }); const addLabelsCalls = []; mockGithub.rest.issues.addLabels = async params => { @@ -559,6 +560,118 @@ describe("add_labels", () => { expect(addLabelsCalls[0].labels).toEqual(["bug"]); }); + it("should forward per-label intent metadata by default when issue_intent is omitted", async () => { + const handler = await main({ max: 10 }); + const addLabelsCalls = []; + + mockGithub.rest.issues.addLabels = async params => { + addLabelsCalls.push(params); + return {}; + }; + + const result = await handler( + { + item_number: 456, + labels: [{ name: "bug", rationale: "Known crash path", confidence: "HIGH", suggest: true }], + }, + {} + ); + + expect(result.success).toBe(true); + expect(addLabelsCalls).toHaveLength(1); + expect(addLabelsCalls[0].labels).toEqual([{ name: "bug", rationale: "Known crash path", confidence: "HIGH", suggest: true }]); + }); + + it("should accept plain string labels by default when issue_intent is omitted", async () => { + const handler = await main({ max: 10 }); + const addLabelsCalls = []; + + mockGithub.rest.issues.addLabels = async params => { + addLabelsCalls.push(params); + return {}; + }; + + const result = await handler( + { + item_number: 456, + labels: ["bug", "enhancement"], + }, + {} + ); + + expect(result.success).toBe(true); + expect(addLabelsCalls).toHaveLength(1); + expect(addLabelsCalls[0].labels).toEqual(["bug", "enhancement"]); + }); + + it("should reject plain string labels when issue_intent is explicitly true (strict mode)", async () => { + const handler = await main({ max: 10, issue_intent: true }); + + const result = await handler( + { + item_number: 456, + labels: ["bug", "enhancement"], + }, + {} + ); + + expect(result.success).toBe(false); + expect(result.error).toContain("Plain string label names are not permitted when issue_intent is explicitly enabled"); + expect(result.error).toContain('"bug"'); + }); + + it("should reject label objects missing rationale or confidence in strict mode", async () => { + const handler = await main({ max: 10, issue_intent: true }); + + const result = await handler( + { + item_number: 456, + labels: [{ name: "bug" }], + }, + {} + ); + + expect(result.success).toBe(false); + expect(result.error).toContain('both "rationale" and "confidence"'); + expect(result.error).toContain('"bug"'); + }); + + it("should reject label object missing confidence in strict mode even when rationale is present", async () => { + const handler = await main({ max: 10, issue_intent: true }); + + const result = await handler( + { + item_number: 456, + labels: [{ name: "bug", rationale: "Crash on upload" }], + }, + {} + ); + + expect(result.success).toBe(false); + expect(result.error).toContain('both "rationale" and "confidence"'); + }); + + it("should accept label objects with both rationale and confidence in strict mode", async () => { + const handler = await main({ max: 10, issue_intent: true }); + const addLabelsCalls = []; + + mockGithub.rest.issues.addLabels = async params => { + addLabelsCalls.push(params); + return {}; + }; + + const result = await handler( + { + item_number: 456, + labels: [{ name: "bug", rationale: "Crash on upload", confidence: "HIGH" }], + }, + {} + ); + + expect(result.success).toBe(true); + expect(addLabelsCalls).toHaveLength(1); + }); + it("should sanitize and trim label names", async () => { const handler = await main({ max: 10 }); const addLabelsCalls = []; diff --git a/actions/setup/js/generate_safe_outputs_tools.cjs b/actions/setup/js/generate_safe_outputs_tools.cjs index fd6d61f6ad2..69d631be0e4 100644 --- a/actions/setup/js/generate_safe_outputs_tools.cjs +++ b/actions/setup/js/generate_safe_outputs_tools.cjs @@ -205,6 +205,21 @@ async function main() { } if (isIssueIntentEnabledForTool(tool.name, config[tool.name])) { enhancedTool.description = `${enhancedTool.description || ""} ${ISSUE_INTENT_SUFFIX}`.trim(); + // For add_labels strict mode, replace the labels items schema with an object-only + // variant that requires name, rationale, and confidence. + if (tool.name === "add_labels") { + const labelsSchema = enhancedTool.inputSchema?.properties?.labels; + if (labelsSchema && labelsSchema.items && Array.isArray(labelsSchema.items.oneOf)) { + const objectSchema = labelsSchema.items.oneOf.find(/** @param {{type: string}} s */ s => s.type === "object"); + if (objectSchema) { + labelsSchema.items = { + ...objectSchema, + required: ["name", "rationale", "confidence"], + }; + delete labelsSchema.items.oneOf; + } + } + } } if (isIssueIntentDisabledForTool(tool.name, config[tool.name])) { stripIssueIntentSchemaFields(enhancedTool); diff --git a/actions/setup/js/generate_safe_outputs_tools.test.cjs b/actions/setup/js/generate_safe_outputs_tools.test.cjs index 2a2ff46a06a..0acae5c936f 100644 --- a/actions/setup/js/generate_safe_outputs_tools.test.cjs +++ b/actions/setup/js/generate_safe_outputs_tools.test.cjs @@ -387,6 +387,90 @@ describe("generate_safe_outputs_tools", () => { expect(result.find((/** @type {{name: string, description: string}} */ t) => t.name === "assign_to_user").description).not.toContain(intentSuffix); }); + it("makes add_labels label items object-only with required name/rationale/confidence when issue_intent is true", () => { + const addLabelsSourceTool = { + name: "add_labels", + description: "Adds labels.", + inputSchema: { + type: "object", + properties: { + labels: { + type: "array", + items: { + oneOf: [ + { type: "string" }, + { + type: "object", + required: ["name"], + properties: { + name: { type: "string" }, + rationale: { type: "string" }, + confidence: { type: "string" }, + suggest: { type: "boolean" }, + }, + additionalProperties: false, + }, + ], + }, + }, + }, + required: ["labels"], + }, + }; + fs.writeFileSync(toolsSourcePath, JSON.stringify([addLabelsSourceTool])); + fs.writeFileSync(configPath, JSON.stringify({ add_labels: { issue_intent: true } })); + fs.writeFileSync(toolsMetaPath, JSON.stringify({ description_suffixes: {}, repo_params: {}, dynamic_tools: [] })); + + runScript(); + + const result = JSON.parse(fs.readFileSync(outputPath, "utf8")); + const tool = result.find((/** @type {{name: string}} */ t) => t.name === "add_labels"); + expect(tool).toBeDefined(); + const items = tool.inputSchema.properties.labels.items; + // No longer a oneOf — plain strings are removed + expect(items.oneOf).toBeUndefined(); + expect(items.type).toBe("object"); + // name, rationale, and confidence are all required + expect(items.required).toEqual(expect.arrayContaining(["name", "rationale", "confidence"])); + }); + + it("does not modify add_labels label items schema when issue_intent is omitted or false", () => { + const addLabelsSourceTool = { + name: "add_labels", + description: "Adds labels.", + inputSchema: { + type: "object", + properties: { + labels: { + type: "array", + items: { + oneOf: [{ type: "string" }, { type: "object", required: ["name"], properties: { name: { type: "string" } } }], + }, + }, + }, + required: ["labels"], + }, + }; + fs.writeFileSync(toolsSourcePath, JSON.stringify([addLabelsSourceTool])); + // Test omitted (default) + fs.writeFileSync(configPath, JSON.stringify({ add_labels: {} })); + fs.writeFileSync(toolsMetaPath, JSON.stringify({ description_suffixes: {}, repo_params: {}, dynamic_tools: [] })); + + runScript(); + + const defaultResult = JSON.parse(fs.readFileSync(outputPath, "utf8")); + const defaultTool = defaultResult.find((/** @type {{name: string}} */ t) => t.name === "add_labels"); + expect(defaultTool.inputSchema.properties.labels.items.oneOf).toBeDefined(); + + // Test explicit false + fs.writeFileSync(configPath, JSON.stringify({ add_labels: { issue_intent: false } })); + runScript(); + + const disabledResult = JSON.parse(fs.readFileSync(outputPath, "utf8")); + const disabledTool = disabledResult.find((/** @type {{name: string}} */ t) => t.name === "add_labels"); + expect(disabledTool.inputSchema.properties.labels.items.oneOf).toBeDefined(); + }); + it("reflects required/optional/absent intent fields per tool configuration", () => { fs.writeFileSync( toolsSourcePath, diff --git a/actions/setup/js/set_issue_field.cjs b/actions/setup/js/set_issue_field.cjs index 1595ca83699..5644e4637ad 100644 --- a/actions/setup/js/set_issue_field.cjs +++ b/actions/setup/js/set_issue_field.cjs @@ -164,7 +164,7 @@ async function main(config = {}) { const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config); const githubClient = await createAuthenticatedGitHubClient(config); const isStaged = isStagedMode(config); - const issueIntentEnabled = config.issue_intent === true; + const issueIntentEnabled = config.issue_intent !== false; core.info(`Set issue field configuration: max=${maxCount}`); const requiredLabels = Array.isArray(config.required_labels) ? config.required_labels : []; diff --git a/actions/setup/js/set_issue_field.test.cjs b/actions/setup/js/set_issue_field.test.cjs index 21f94f84535..d615009c2ee 100644 --- a/actions/setup/js/set_issue_field.test.cjs +++ b/actions/setup/js/set_issue_field.test.cjs @@ -554,4 +554,35 @@ describe("set_issue_field (Handler Factory Architecture)", () => { expect(mutationCall[1].issueFields[0]).not.toHaveProperty("confidence"); expect(mutationCall[1].issueFields[0]).not.toHaveProperty("suggest"); }); + + it("should include intent metadata by default when issue_intent is omitted", async () => { + const { main } = require("./set_issue_field.cjs"); + const defaultHandler = await main({ max: 5 }); + + const result = await defaultHandler( + { + type: "set_issue_field", + issue_number: 42, + field_name: "Customer Impact", + value: "High", + rationale: "Customer-reported with SLA breach risk", + confidence: "high", + suggest: true, + }, + {} + ); + + expect(result.success).toBe(true); + const mutationCall = mockGraphql.mock.calls.find(([query]) => query.includes("setIssueFieldValue")); + expect(mutationCall).toBeTruthy(); + expect(mutationCall[1].issueFields[0]).toEqual( + expect.objectContaining({ + fieldId: textFieldId, + textValue: "High", + rationale: "Customer-reported with SLA breach risk", + confidence: "HIGH", + suggest: true, + }) + ); + }); }); diff --git a/actions/setup/js/set_issue_type.cjs b/actions/setup/js/set_issue_type.cjs index bd8a65421bf..41f00eb2c1d 100644 --- a/actions/setup/js/set_issue_type.cjs +++ b/actions/setup/js/set_issue_type.cjs @@ -181,7 +181,7 @@ async function main(config = {}) { core.info(`Set issue type configuration: max=${maxCount}`); const requiredLabels = Array.isArray(config.required_labels) ? config.required_labels : []; const requiredTitlePrefix = config.required_title_prefix || ""; - const issueIntentEnabled = config.issue_intent === true; + const issueIntentEnabled = config.issue_intent !== false; if (requiredLabels.length > 0) core.info(`Required labels (all): ${requiredLabels.join(", ")}`); if (requiredTitlePrefix) core.info(`Required title prefix: ${requiredTitlePrefix}`); if (allowedTypes.length > 0) { diff --git a/actions/setup/js/set_issue_type.test.cjs b/actions/setup/js/set_issue_type.test.cjs index 41ceeec6689..d1c225ceee2 100644 --- a/actions/setup/js/set_issue_type.test.cjs +++ b/actions/setup/js/set_issue_type.test.cjs @@ -495,4 +495,58 @@ describe("set_issue_type (Handler Factory Architecture)", () => { }); expect(mockGithub.graphql).not.toHaveBeenCalledWith(expect.stringContaining("updateIssue"), expect.anything()); }); + + it("should use GraphQL intent path by default when issue_intent is omitted", async () => { + const { main } = require("./set_issue_type.cjs"); + const defaultHandler = await main({ max: 5 }); + + const result = await defaultHandler( + { + type: "set_issue_type", + issue_number: 42, + issue_type: "Bug", + }, + {} + ); + + expect(result.success).toBe(true); + expect(mockGithub.rest.issues.update).not.toHaveBeenCalled(); + expect(mockGithub.graphql).toHaveBeenCalledWith( + expect.stringContaining("IssueTypeUpdateInput"), + expect.objectContaining({ + issueId: "I_kwDO_testissue", + issueType: expect.objectContaining({ issueTypeId: "IT_kwDO_bug" }), + }) + ); + }); + + it("should forward intent metadata by default when issue_intent is omitted", async () => { + const { main } = require("./set_issue_type.cjs"); + const defaultHandler = await main({ max: 5 }); + + const result = await defaultHandler( + { + type: "set_issue_type", + issue_number: 42, + issue_type: "Bug", + rationale: "Author explicitly requests a bug fix", + confidence: "high", + }, + {} + ); + + expect(result.success).toBe(true); + expect(mockGithub.rest.issues.update).not.toHaveBeenCalled(); + expect(mockGithub.graphql).toHaveBeenCalledWith( + expect.stringContaining("IssueTypeUpdateInput"), + expect.objectContaining({ + issueId: "I_kwDO_testissue", + issueType: { + issueTypeId: "IT_kwDO_bug", + rationale: "Author explicitly requests a bug fix", + confidence: "HIGH", + }, + }) + ); + }); }); diff --git a/docs/adr/46207-default-on-issue-intent-metadata.md b/docs/adr/46207-default-on-issue-intent-metadata.md new file mode 100644 index 00000000000..2b61f592012 --- /dev/null +++ b/docs/adr/46207-default-on-issue-intent-metadata.md @@ -0,0 +1,44 @@ +# ADR-46207: Default-On Issue Intent Metadata for Handler Flags + +**Date**: 2026-07-18 +**Status**: Draft +**Deciders**: Unknown + +--- + +### Context + +Three GitHub Action handlers — `set_issue_type`, `set_issue_field`, and `add_labels` — guarded intent metadata forwarding behind an explicit opt-in (`config.issue_intent === true`). When workflow frontmatter omitted `issue_intent`, the handlers silently discarded agent-supplied `rationale`, `confidence`, and `suggest` fields. Two other handlers (`close_issue`, `assign_to_agent`) already used the opposite convention: default-on with an explicit opt-out (`!== false`). This inconsistency meant agents relying on a minimal workflow config had their intent metadata silently dropped depending on which handler they invoked. + +### Decision + +We will change the `issue_intent` gate in `set_issue_type`, `set_issue_field`, and `add_labels` from opt-in (`=== true`) to opt-out (`!== false`). Omitting `issue_intent` from workflow frontmatter now forwards intent metadata through the GraphQL/intent-aware path by default, matching `close_issue` and `assign_to_agent`. For `add_labels` only, we will introduce an `issueIntentStrict` mode (`config.issue_intent === true`) that rejects plain-string label inputs and requires structured objects with metadata fields, enabling stricter enforcement when the feature is explicitly enabled. We will also add the `issue-intent` boolean property to the JSON schema for all three handlers so that `issue_intent: false` becomes a valid opt-out in workflow frontmatter. + +### Alternatives Considered + +#### Alternative 1: Keep Explicit Opt-In, Improve Documentation + +Leave the `=== true` gate unchanged and add documentation or linter warnings reminding authors to set `issue_intent: true` when they want metadata forwarded. This avoids any breaking change risk but perpetuates the inconsistency — agents that omit `issue_intent` continue to lose metadata silently, and different handlers continue to behave differently for the same omitted config key. + +#### Alternative 2: Global Default Configuration Key + +Introduce a top-level `issue_intent_default` key in the workflow config that governs the default for all handlers, rather than changing per-handler logic. This provides configurability but adds a second layer of indirection, increases schema complexity, and still requires existing workflows to migrate if they want the new default without setting the global key. + +### Consequences + +#### Positive +- Intent metadata (rationale, confidence, suggest) is now forwarded by default across all supported handlers, eliminating silent data loss when frontmatter is minimal. +- Handler behavior is consistent: all intent-aware handlers share the same `!== false` opt-out convention, reducing cognitive load for workflow authors. +- The `issue-intent` schema addition makes opt-out (`issue_intent: false`) valid syntax in workflow frontmatter, closing a gap where opting out was impossible through standard schema-validated config. + +#### Negative +- Existing workflows that relied on the `issue_intent` feature being off by default will now forward metadata unless they explicitly add `issue_intent: false` — a breaking behavior change requiring a migration step. +- The `issueIntentStrict` mode for `add_labels` (enabled when `issue_intent: true`) introduces a new failure path: plain-string label inputs are rejected outright, which may break callers that pass string labels while setting `issue_intent: true` for other reasons. + +#### Neutral +- The `pkg/parser/schemas/main_workflow_schema.json` diff also reformats existing JSON object properties from compact single-line form to multi-line form; this is a cosmetic change with no behavioral effect but increases diff noise. +- Tests covering the new default-on behavior and strict-mode rejection were added inline alongside the handler changes. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 6501bb02d3e..515e0414ab0 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -7800,6 +7800,10 @@ "type": "string", "description": "Title prefix constraint: the issue/PR title must start with this prefix for the operation to proceed." }, + "issue-intent": { + "type": "boolean", + "description": "Enable issue-intent metadata support (rationale/confidence/suggest) for this output type." + }, "staged": { "$ref": "#/$defs/templatable_boolean", "description": "When true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", @@ -9294,6 +9298,10 @@ "$ref": "#/$defs/github_token", "description": "Optional GitHub token used only for branch writes to head-repo. Use this when the upstream pull request repository and the fork head repository require different credentials." }, + "issue-intent": { + "type": "boolean", + "description": "Enable issue-intent metadata support (rationale/confidence/suggest) for this output type." + }, "staged": { "$ref": "#/$defs/templatable_boolean", "description": "When true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", @@ -9382,6 +9390,10 @@ "$ref": "#/$defs/github_token", "description": "GitHub token to use for this specific output type. Overrides global github-token if specified." }, + "issue-intent": { + "type": "boolean", + "description": "Enable issue-intent metadata support (rationale/confidence/suggest) for this output type." + }, "staged": { "$ref": "#/$defs/templatable_boolean", "description": "When true, emit step summary messages instead of making GitHub API calls for this specific output type (preview mode)", @@ -12778,12 +12790,24 @@ "capabilities": { "type": "object", "properties": { - "tools-allowlist": { "type": "boolean" }, - "max-turns": { "type": "boolean" }, - "web-search": { "type": "boolean" }, - "max-continuations": { "type": "boolean" }, - "native-agent-file": { "type": "boolean" }, - "bare-mode": { "type": "boolean" } + "tools-allowlist": { + "type": "boolean" + }, + "max-turns": { + "type": "boolean" + }, + "web-search": { + "type": "boolean" + }, + "max-continuations": { + "type": "boolean" + }, + "native-agent-file": { + "type": "boolean" + }, + "bare-mode": { + "type": "boolean" + } }, "additionalProperties": false }, @@ -12792,11 +12816,15 @@ "properties": { "files": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } }, "path-prefixes": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } } }, "additionalProperties": false @@ -12804,53 +12832,105 @@ "installation": { "type": "object", "properties": { - "package-manager": { "type": "string" }, - "package-name": { "type": "string" }, - "version": { "type": "string" }, - "step-name": { "type": "string" }, - "binary-name": { "type": "string" }, - "include-node-setup": { "type": "boolean" }, - "post-install-scripts": { "type": "boolean" }, - "cooldown": { "type": "boolean" }, - "verify-command": { "type": "string" }, - "verify-step-name": { "type": "string" }, - "docs-url": { "type": "string" } + "package-manager": { + "type": "string" + }, + "package-name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "step-name": { + "type": "string" + }, + "binary-name": { + "type": "string" + }, + "include-node-setup": { + "type": "boolean" + }, + "post-install-scripts": { + "type": "boolean" + }, + "cooldown": { + "type": "boolean" + }, + "verify-command": { + "type": "string" + }, + "verify-step-name": { + "type": "string" + }, + "docs-url": { + "type": "string" + } }, "additionalProperties": false }, "config-file": { "type": "object", "properties": { - "path": { "type": "string" }, - "step-name": { "type": "string" }, - "content": { "type": "string" }, - "merge-strategy": { "type": "string" } + "path": { + "type": "string" + }, + "step-name": { + "type": "string" + }, + "content": { + "type": "string" + }, + "merge-strategy": { + "type": "string" + } }, "additionalProperties": false }, "execution": { "type": "object", "properties": { - "command-name": { "type": "string" }, + "command-name": { + "type": "string" + }, "args": { "type": "array", - "items": { "type": "string" } + "items": { + "type": "string" + } + }, + "step-name": { + "type": "string" + }, + "model-env-var": { + "type": "string" + }, + "model-env-provider-prefix": { + "type": "string" + }, + "model-flag": { + "type": "string" }, - "step-name": { "type": "string" }, - "model-env-var": { "type": "string" }, - "model-env-provider-prefix": { "type": "string" }, - "model-flag": { "type": "string" }, - "mcp-config-env-var": { "type": "string" }, - "mcp-config-flag": { "type": "string" }, - "write-timestamp": { "type": "boolean" }, - "provider-env-mode": { "type": "string" } + "mcp-config-env-var": { + "type": "string" + }, + "mcp-config-flag": { + "type": "string" + }, + "write-timestamp": { + "type": "boolean" + }, + "provider-env-mode": { + "type": "string" + } }, "additionalProperties": false }, "mcp": { "type": "object", "properties": { - "config-path": { "type": "string" } + "config-path": { + "type": "string" + } }, "additionalProperties": false },