Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2c8c29a
Initial plan
Copilot Jul 15, 2026
f99884e
fix: add maxLength: 65536 to body/content fields to exempt from SM-IS…
Copilot Jul 15, 2026
e9b3119
fix: enforce explicit schema maxLength in MCP validation and add miss…
Copilot Jul 15, 2026
9158159
fix: align explicit maxLength checks with schema semantics
Copilot Jul 15, 2026
3c25c4c
chore: investigate failing CI run 29458119068
Copilot Jul 16, 2026
dfa0c53
chore: investigate failing CI job
Copilot Jul 16, 2026
87d0cae
fix: improve MCP maxLength violation messages for E006
Copilot Jul 16, 2026
b8c1056
chore: drop unrelated skill list edits from PR
Copilot Jul 16, 2026
a822144
chore: remove unrelated skill list edits from this PR
Copilot Jul 16, 2026
910cedd
chore: start merge-main conflict resolution plan
Copilot Jul 16, 2026
45301e3
Merge remote-tracking branch 'refs/remotes/origin/main' into copilot/…
Copilot Jul 16, 2026
711a2bf
chore: start pr-finisher triage
Copilot Jul 16, 2026
cd21e63
Merge remote-tracking branch 'origin/main' into copilot/fix-maxlength…
Copilot Jul 16, 2026
61b4c24
merge main and recompile workflows
Copilot Jul 16, 2026
e8488d4
Merge remote-tracking branch 'origin/main' into copilot/fix-maxlength…
pelikhan Jul 16, 2026
b76570d
recompile
pelikhan Jul 16, 2026
438ea54
Merge branch 'main' into copilot/fix-maxlength-exemption-body-fields
pelikhan Jul 16, 2026
2b3691d
chore: outline plan to fix failing CI job
Copilot Jul 16, 2026
b8b497b
test: increase create_issue body maxLength fixture in large-content M…
Copilot Jul 16, 2026
dcb5f8e
Merge branch 'main' into copilot/fix-maxlength-exemption-body-fields
pelikhan Jul 16, 2026
aa730de
test: make firewall container pin assertion follow default version pin
Copilot Jul 16, 2026
85d3aab
test: fix AWF digest assertions to track current default pin set
Copilot Jul 16, 2026
fe4e793
test: update stale pin assertions for CI stability
Copilot Jul 16, 2026
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
7 changes: 3 additions & 4 deletions actions/setup/js/mcp_scripts_mcp_server_http.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ require("./shim.cjs");

const { randomUUID } = require("crypto");
const { MCPServer, MCPHTTPTransport } = require("./mcp_http_transport.cjs");
const { validateRequiredFields, validateStringInputLengths } = require("./mcp_scripts_validation.cjs");
const { validateRequiredFields, validateStringInputLengths, buildStringLengthValidationError } = require("./mcp_scripts_validation.cjs");
const { generateEnhancedErrorMessage } = require("./mcp_enhanced_errors.cjs");
const { createLogger } = require("./mcp_logger.cjs");
const { bootstrapMCPScriptsServer, cleanupConfigFile } = require("./mcp_scripts_bootstrap.cjs");
Expand Down Expand Up @@ -94,11 +94,10 @@ function createMCPServer(configPath, options = {}) {
throw new Error(generateEnhancedErrorMessage(missing, tool.name, tool.inputSchema));
}

// SM-IS-01: Validate per-string input length limits (10 KB max per string parameter).
// SM-IS-01: Validate per-string input length limits (default 10 KB, or explicit schema maxLength when set).
const oversized = validateStringInputLengths(args, tool.inputSchema);
if (oversized.length) {
const details = oversized.map(v => `'${v.field}' (${v.byteLength} bytes)`).join(", ");
throw new Error(`Input string parameter(s) exceed the 10 KB limit for tool '${tool.name}': ${details}`);
throw new Error(buildStringLengthValidationError(tool.name, oversized));
}

// Call the handler
Expand Down
31 changes: 25 additions & 6 deletions actions/setup/js/mcp_scripts_validation.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ function validateRequiredFields(args, inputSchema) {
* string-typed input parameter. Inputs exceeding the configured maximum MUST be rejected with a
* validation error before the tool script is invoked. Implementations MUST NOT silently truncate
* oversized inputs.
* When a field declares an explicit schema maxLength, that explicit character limit is enforced here;
* otherwise the default SM-IS-01 10KB byte limit applies.
*
* Scope: validates only top-level (direct) properties of the schema where `type === "string"`.
* Nested object/array schemas are not recursively validated, consistent with the SM-IS-01
Expand All @@ -47,7 +49,7 @@ function validateRequiredFields(args, inputSchema) {
* @param {Object} args - The arguments object to validate
* @param {Object} inputSchema - The input schema describing property types
* @param {number} [maxBytes] - Maximum allowed bytes per string (defaults to MAX_STRING_INPUT_BYTES)
* @returns {{ field: string, byteLength: number }[]} Array of violations (empty if all within limit)
* @returns {{ field: string, actualLength: number, limit: number, unit: "bytes" | "characters" }[]} Array of violations (empty if all within limit)
*/
function validateStringInputLengths(args, inputSchema, maxBytes) {
const limit = typeof maxBytes === "number" ? maxBytes : MAX_STRING_INPUT_BYTES;
Expand All @@ -56,15 +58,19 @@ function validateStringInputLengths(args, inputSchema, maxBytes) {

for (const [field, schema] of Object.entries(properties)) {
if (schema && schema.type === "string") {
// Skip fields with an explicit maxLength — handler-level validation enforces their limit.
if (typeof schema.maxLength === "number") {
continue;
}
const value = args[field];
if (typeof value === "string") {
if (typeof schema.maxLength === "number") {
const characterLength = Array.from(value).length;
if (characterLength > schema.maxLength) {
violations.push({ field, actualLength: characterLength, limit: schema.maxLength, unit: "characters" });
}
continue;
}

const byteLength = Buffer.byteLength(value, "utf8");
if (byteLength > limit) {
violations.push({ field, byteLength });
violations.push({ field, actualLength: byteLength, limit, unit: "bytes" });
}
}
}
Expand All @@ -73,6 +79,18 @@ function validateStringInputLengths(args, inputSchema, maxBytes) {
return violations;
}

/**
* Build actionable E006 validation message for string length violations.
*
* @param {string} toolName - Tool name being validated
* @param {{ field: string, actualLength: number, limit: number, unit: "bytes" | "characters" }[]} violations - Violations returned by validateStringInputLengths
* @returns {string} E006 message
*/
function buildStringLengthValidationError(toolName, violations) {
const details = violations.map(v => `'${v.field}' exceeds maximum length of ${v.limit} ${v.unit} (got ${v.actualLength} ${v.unit})`).join(", ");
return `E006: Input string parameter(s) exceed maximum length for tool '${toolName}': ${details}`;
}

/**
* Validate that string-typed arguments meet the schema's minLength constraints.
* Trims values before comparing (matching downstream validator behavior).
Expand Down Expand Up @@ -105,6 +123,7 @@ function validateStringMinLengths(args, inputSchema) {
module.exports = {
validateRequiredFields,
validateStringInputLengths,
buildStringLengthValidationError,
validateStringMinLengths,
MAX_STRING_INPUT_BYTES,
};
42 changes: 28 additions & 14 deletions actions/setup/js/mcp_scripts_validation.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ describe("mcp_scripts_validation.cjs", () => {

expect(violations).toHaveLength(1);
expect(violations[0].field).toBe("message");
expect(violations[0].byteLength).toBe(MAX_STRING_INPUT_BYTES + 1);
expect(violations[0].actualLength).toBe(MAX_STRING_INPUT_BYTES + 1);
expect(violations[0].unit).toBe("bytes");
});

it("should not flag a string at exactly the 10 KB limit", async () => {
Expand Down Expand Up @@ -282,40 +283,53 @@ describe("mcp_scripts_validation.cjs", () => {
expect(violations).toEqual([]);
});

it("should skip string fields with an explicit maxLength (handler-level validation)", async () => {
it("should enforce explicit maxLength for string fields", async () => {
const { validateStringInputLengths, MAX_STRING_INPUT_BYTES } = await import("./mcp_scripts_validation.cjs");

// A value that exceeds the default 10 KB limit but is within maxLength
const valueExceedingDefaultLimit = "a".repeat(MAX_STRING_INPUT_BYTES + 1);
const args = { body: valueExceedingDefaultLimit };
// Exceeds explicit maxLength
const valueExceedingExplicitMax = "a".repeat(MAX_STRING_INPUT_BYTES + 1);
const args = { body: valueExceedingExplicitMax };
const schema = {
type: "object",
properties: { body: { type: "string", maxLength: MAX_STRING_INPUT_BYTES + 1000 } },
properties: { body: { type: "string", maxLength: MAX_STRING_INPUT_BYTES } },
};

// Should not flag — handler-level validation is responsible for this field
const violations = validateStringInputLengths(args, schema);
expect(violations).toEqual([]);
expect(violations).toHaveLength(1);
expect(violations[0].field).toBe("body");
});

it("should still check string fields without maxLength when other fields have maxLength", async () => {
it("should still check string fields without maxLength when other fields have explicit maxLength", async () => {
const { validateStringInputLengths, MAX_STRING_INPUT_BYTES } = await import("./mcp_scripts_validation.cjs");

const oversizedValue = "a".repeat(MAX_STRING_INPUT_BYTES + 1);
const args = { body: oversizedValue, title: oversizedValue };
const schema = {
type: "object",
properties: {
// body has maxLength — skipped by generic check
body: { type: "string", maxLength: 65536 },
// title has no maxLength — checked by generic check
// body has explicit maxLength lower than value
body: { type: "string", maxLength: MAX_STRING_INPUT_BYTES },
// title has no maxLength — checked against default 10KB
title: { type: "string" },
},
};

const violations = validateStringInputLengths(args, schema);
expect(violations).toHaveLength(1);
expect(violations[0].field).toBe("title");
expect(violations).toHaveLength(2);
expect(violations.map(v => v.field).sort()).toEqual(["body", "title"]);
});

it("should enforce explicit maxLength using character count", async () => {
const { validateStringInputLengths } = await import("./mcp_scripts_validation.cjs");

const args = { body: "🚀".repeat(3) };
const schema = {
type: "object",
properties: { body: { type: "string", maxLength: 3 } },
};

const violations = validateStringInputLengths(args, schema);
expect(violations).toEqual([]);
});
});

Expand Down
12 changes: 5 additions & 7 deletions actions/setup/js/mcp_server_core.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const fs = require("fs");
const path = require("path");

const { ReadBuffer } = require("./read_buffer.cjs");
const { validateRequiredFields, validateStringInputLengths, validateStringMinLengths } = require("./mcp_scripts_validation.cjs");
const { validateRequiredFields, validateStringInputLengths, buildStringLengthValidationError, validateStringMinLengths } = require("./mcp_scripts_validation.cjs");
const { getErrorMessage } = require("./error_helpers.cjs");
const { generateEnhancedErrorMessage } = require("./mcp_enhanced_errors.cjs");
const { createDependencyInstallGate } = require("./mcp_dependencies_manager.cjs");
Expand Down Expand Up @@ -784,13 +784,12 @@ async function handleRequest(server, request, defaultHandler) {
};
}

// SM-IS-01: Validate per-string input length limits (10 KB max per string parameter).
// SM-IS-01: Validate per-string input length limits (default 10 KB, or explicit schema maxLength when set).
const oversizedFields = validateStringInputLengths(args, tool.inputSchema);
if (oversizedFields.length) {
const details = oversizedFields.map(v => `'${v.field}' (${v.byteLength} bytes)`).join(", ");
throw {
code: -32602,
message: `Input string parameter(s) exceed the 10 KB limit for tool '${name}': ${details}`,
message: buildStringLengthValidationError(name, oversizedFields),
};
}

Expand Down Expand Up @@ -954,11 +953,10 @@ async function handleMessage(server, req, defaultHandler) {
return;
}

// SM-IS-01: Validate per-string input length limits (10 KB max per string parameter).
// SM-IS-01: Validate per-string input length limits (default 10 KB, or explicit schema maxLength when set).
const oversized = validateStringInputLengths(args, tool.inputSchema);
if (oversized.length) {
const details = oversized.map(v => `'${v.field}' (${v.byteLength} bytes)`).join(", ");
server.replyError(id, -32602, `Input string parameter(s) exceed the 10 KB limit for tool '${name}': ${details}`);
server.replyError(id, -32602, buildStringLengthValidationError(name, oversized));
return;
}

Expand Down
2 changes: 1 addition & 1 deletion actions/setup/js/safe_outputs_mcp_large_content.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe.sequential("safe_outputs_mcp_server.cjs large content handling", () =>
tools = JSON.parse(fs.readFileSync(path.join(__dirname, "safe_outputs_tools.json"), "utf8")),
createIssueTool = tools.find(tool => tool.name === "create_issue");
if (createIssueTool?.inputSchema?.properties?.body) {
createIssueTool.inputSchema.properties.body.maxLength = 2e5;
createIssueTool.inputSchema.properties.body.maxLength = 2e6;
}
fs.writeFileSync(toolsJsonPath, JSON.stringify(tools));
}),
Expand Down
Loading
Loading