Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions actions/setup/js/mcp_cli_bridge.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,7 @@ function parseToolArgs(args, schemaProperties = {}) {
/** @type {Record<string, unknown>} */
const result = {};
let jsonOutput = false;
const hasSchemaProperties = Object.keys(schemaProperties).length > 0;
const { normalizedSchemaKeyMap, ambiguousNormalizedSchemaKeys } = buildNormalizedSchemaKeyMap(schemaProperties);

for (let i = 0; i < args.length; i++) {
Expand All @@ -458,13 +459,13 @@ function parseToolArgs(args, schemaProperties = {}) {
jsonOutput = true;
} else {
const canonicalKey = resolveSchemaPropertyKey(key, schemaProperties, normalizedSchemaKeyMap, ambiguousNormalizedSchemaKeys);
result[canonicalKey] = coerceToolArgValue(canonicalKey, raw.slice(eqIdx + 1), schemaProperties[canonicalKey], result[canonicalKey]);
result[canonicalKey] = coerceToolArgValue(canonicalKey, raw.slice(eqIdx + 1), schemaProperties[canonicalKey], result[canonicalKey], !hasSchemaProperties);
}
} else if (raw === "json") {
jsonOutput = true;
} else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
const canonicalKey = resolveSchemaPropertyKey(raw, schemaProperties, normalizedSchemaKeyMap, ambiguousNormalizedSchemaKeys);
result[canonicalKey] = coerceToolArgValue(canonicalKey, args[i + 1], schemaProperties[canonicalKey], result[canonicalKey]);
result[canonicalKey] = coerceToolArgValue(canonicalKey, args[i + 1], schemaProperties[canonicalKey], result[canonicalKey], !hasSchemaProperties);
i++;
} else {
const canonicalKey = resolveSchemaPropertyKey(raw, schemaProperties, normalizedSchemaKeyMap, ambiguousNormalizedSchemaKeys);
Expand Down Expand Up @@ -549,9 +550,10 @@ function resolveSchemaPropertyKey(key, schemaProperties, normalizedSchemaKeyMap,
* @param {string} rawValue - Raw CLI value
* @param {{type?: string|string[]}|undefined} schemaProperty - JSON schema property
* @param {unknown} existingValue - Existing value (for repeated flags)
* @param {boolean} [allowNumericFallback=false] - Allow numeric parsing when schema is unavailable
* @returns {unknown}
*/
function coerceToolArgValue(key, rawValue, schemaProperty, existingValue) {
function coerceToolArgValue(key, rawValue, schemaProperty, existingValue, allowNumericFallback = false) {
/** @type {string[]} */
const types = [];
if (schemaProperty && typeof schemaProperty === "object" && "type" in schemaProperty && schemaProperty.type != null) {
Expand Down Expand Up @@ -620,6 +622,26 @@ function coerceToolArgValue(key, rawValue, schemaProperty, existingValue) {
}
}

// When schema metadata is unavailable (e.g. empty tools cache), apply
// conservative numeric coercion fallback for CLI ergonomics.
if (allowNumericFallback && types.length === 0) {
const trimmedValue = rawValue.trim();

if (/^-?\d+$/.test(trimmedValue)) {
const parsedInt = Number.parseInt(trimmedValue, 10);
if (!Number.isNaN(parsedInt) && Number.isSafeInteger(parsedInt)) {
return parsedInt;
}
}

if (/^-?(?:(?:\d+\.\d*|\.\d+)(?:[eE][+-]?\d+)?|\d+[eE][+-]?\d+)$/.test(trimmedValue)) {
const parsedFloat = Number.parseFloat(trimmedValue);
if (!Number.isNaN(parsedFloat) && Number.isFinite(parsedFloat)) {
return parsedFloat;
}
}
Comment on lines +625 to +642
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema-less numeric fallback coerces floats only when the value matches the decimal-point regex, so scientific notation like 1e3 / -2E-4 (which Number()/parseFloat() parse fine) will not be coerced and will still be sent as a string in schema-less mode. Consider extending the float pattern to allow exponent-only forms (e.g. ^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$) or using a single numeric-validation regex shared by both integer/float branches.

Copilot uses AI. Check for mistakes.
}

return rawValue;
}

Expand Down
27 changes: 27 additions & 0 deletions actions/setup/js/mcp_cli_bridge.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,33 @@ describe("mcp_cli_bridge.cjs", () => {
});
});

it("falls back to numeric coercion when schema properties are unavailable", () => {
const { args } = parseToolArgs(["--count", "3", "--max_tokens", "3000"], {});

expect(args).toEqual({
count: 3,
max_tokens: 3000,
});
});

it("coerces scientific notation when schema properties are unavailable", () => {
const { args } = parseToolArgs(["--max_tokens", "1e3", "--threshold", "-2E-4"], {});

expect(args).toEqual({
max_tokens: 1000,
threshold: -0.0002,
});
});

it("preserves non-numeric values when schema properties are unavailable", () => {
const { args } = parseToolArgs(["--start_date", "-1d", "--workflow_name", "daily-issues-report"], {});

expect(args).toEqual({
start_date: "-1d",
workflow_name: "daily-issues-report",
});
});

it("treats MCP result envelopes with isError=true as errors", () => {
formatResponse(
{
Expand Down