diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index 2c996184978..4eef8f439b3 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -1000,7 +1000,7 @@ jobs: "url": "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp?toolsets=core", "headers": { "DD_API_KEY": "\${DD_API_KEY}", - "DD_APPLICATION_KEY": "\${DD_APPLICATION_KEY}", + "DD_APPLICATION_KEY": "\${DD_APP_KEY}", "DD_SITE": "\${DD_SITE}" }, "tools": [ diff --git a/actions/setup/js/mcp_server_core.cjs b/actions/setup/js/mcp_server_core.cjs index 5df8f625e17..2bf3dc99987 100644 --- a/actions/setup/js/mcp_server_core.cjs +++ b/actions/setup/js/mcp_server_core.cjs @@ -623,6 +623,13 @@ async function handleRequest(server, request, defaultHandler) { const missing = validateRequiredFields(args, tool.inputSchema); if (missing.length) { + const hasRequiredFields = tool.inputSchema && Array.isArray(tool.inputSchema.required) && tool.inputSchema.required.length > 0; + if (hasRequiredFields && Object.keys(args).length === 0) { + throw { + code: -32602, + message: `Empty arguments are not allowed — this tool is write-once, not a discovery probe. To inspect the schema, use the tools/list MCP method. To signal that no action is needed, call \`noop\` with a \`message\`.`, + }; + } throw { code: -32602, message: generateEnhancedErrorMessage(missing, name, tool.inputSchema), @@ -751,6 +758,15 @@ async function handleMessage(server, req, defaultHandler) { const missing = validateRequiredFields(args, tool.inputSchema); if (missing.length) { + const hasRequiredFields = tool.inputSchema && Array.isArray(tool.inputSchema.required) && tool.inputSchema.required.length > 0; + if (hasRequiredFields && Object.keys(args).length === 0) { + server.replyError( + id, + -32602, + `Empty arguments are not allowed — this tool is write-once, not a discovery probe. To inspect the schema, use the tools/list MCP method. To signal that no action is needed, call \`noop\` with a \`message\`.` + ); + return; + } server.replyError(id, -32602, generateEnhancedErrorMessage(missing, name, tool.inputSchema)); return; } diff --git a/actions/setup/js/mcp_server_core.test.cjs b/actions/setup/js/mcp_server_core.test.cjs index 821fa38580f..40e5ca244d1 100644 --- a/actions/setup/js/mcp_server_core.test.cjs +++ b/actions/setup/js/mcp_server_core.test.cjs @@ -204,7 +204,7 @@ describe("mcp_server_core.cjs", () => { expect(results[0].error.message).toContain("Tool not found"); }); - it("should return error for missing required fields", async () => { + it("should return error for empty arguments object (probe detection)", async () => { const { handleMessage } = await import("./mcp_server_core.cjs"); await handleMessage(server, { @@ -213,7 +213,27 @@ describe("mcp_server_core.cjs", () => { method: "tools/call", params: { name: "test_tool", - arguments: {}, // missing required 'input' + arguments: {}, // completely empty — probe attempt + }, + }); + + expect(results).toHaveLength(1); + expect(results[0].error.code).toBe(-32602); + expect(results[0].error.message).toContain("write-once, not a discovery probe"); + expect(results[0].error.message).toContain("tools/list"); + expect(results[0].error.message).toContain("noop"); + }); + + it("should return enhanced error for partially-supplied but invalid required fields", async () => { + const { handleMessage } = await import("./mcp_server_core.cjs"); + + await handleMessage(server, { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { + name: "test_tool", + arguments: { input: "" }, // present but empty string — not a probe }, }); diff --git a/actions/setup/js/safe_outputs_mcp_error_messages.test.cjs b/actions/setup/js/safe_outputs_mcp_error_messages.test.cjs index a895936d140..f9327f933a5 100644 --- a/actions/setup/js/safe_outputs_mcp_error_messages.test.cjs +++ b/actions/setup/js/safe_outputs_mcp_error_messages.test.cjs @@ -189,7 +189,7 @@ describe("Safe Outputs MCP Error Message Validation", () => { method: "tools/call", params: { name: "test_tool", - arguments: {}, // missing 'title' + arguments: { title: "" }, // empty string triggers missing-field error (not probe detection) }, }); @@ -221,7 +221,7 @@ describe("Safe Outputs MCP Error Message Validation", () => { method: "tools/call", params: { name: "test_tool", - arguments: {}, // missing both fields + arguments: { title: "", body: "" }, // empty strings trigger missing-field errors (not probe detection) }, }); @@ -277,13 +277,19 @@ describe("Safe Outputs MCP Error Message Validation", () => { handler: args => ({ content: [{ type: "text", text: "ok" }] }), }); + // Use empty-string values to trigger field-level errors (not probe detection) + const emptyArgs = testTool.required.reduce((acc, field) => { + acc[field] = ""; + return acc; + }, {}); + await handleMessage(server, { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: testTool.name, - arguments: {}, + arguments: emptyArgs, }, }); @@ -333,13 +339,19 @@ describe("Safe Outputs MCP Error Message Validation", () => { registerTool(server, toolWithHandler); + // Use empty-string values to trigger field-level errors (not probe detection) + const emptyArgs = tool.inputSchema.required.reduce((acc, field) => { + acc[field] = ""; + return acc; + }, {}); + await handleMessage(server, { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: tool.name, - arguments: {}, // Empty arguments to trigger missing field errors + arguments: emptyArgs, // Empty strings to trigger missing field errors }, }); @@ -404,7 +416,7 @@ describe("Safe Outputs MCP Error Message Validation", () => { method: "tools/call", params: { name: "create_issue", - arguments: {}, // Missing required fields + arguments: { title: "", body: "" }, // Empty strings to trigger missing-field errors (not probe detection) }, }); diff --git a/actions/setup/js/safe_outputs_tools.json b/actions/setup/js/safe_outputs_tools.json index df9aff5ce53..e2ffb4e79ca 100644 --- a/actions/setup/js/safe_outputs_tools.json +++ b/actions/setup/js/safe_outputs_tools.json @@ -1,18 +1,18 @@ [ { "name": "create_issue", - "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. This is a write-once declaration for a real intended issue, not a sandbox or probe: do not call it with placeholder titles/bodies or auth experiments. If you are not ready to open the real issue, use noop or report_incomplete instead. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead.", + "description": "WRITE-ONCE: do NOT call this tool with empty or placeholder arguments to probe or discover its schema — required fields (title, body) are listed in this schema; if you are not ready to open the real issue, call `noop` instead. Creates a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead.", "inputSchema": { "type": "object", "required": ["title", "body"], "properties": { "title": { "type": "string", - "description": "Concise issue title summarizing the bug, feature, or task. The title appears as the main heading, so keep it brief and descriptive." + "description": "Concise issue title summarizing the bug, feature, or task. Must be the final intended title — not a placeholder or test value. The title appears as the main heading, so keep it brief and descriptive." }, "body": { "type": "string", - "description": "Detailed issue description in Markdown. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate." + "description": "Detailed issue description in Markdown. Must be the final intended body — not a placeholder or test value. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate." }, "labels": { "type": "array", @@ -245,14 +245,14 @@ }, { "name": "add_comment", - "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. This is a write-once declaration for a real intended comment, not a sandbox or probe: do not call it with placeholder bodies or auth experiments. If you are not ready to post the real comment, use noop or report_incomplete instead. For creating new items, use create_issue, create_discussion, or create_pull_request instead.", + "description": "WRITE-ONCE: do NOT call this tool with empty or placeholder arguments to probe or discover its schema — the required `body` field is listed in this schema; if you are not ready to post a real comment, call `noop` instead. Adds a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead.", "inputSchema": { "type": "object", "required": ["body"], "properties": { "body": { "type": "string", - "description": "The comment text in Markdown format. This is the 'body' field - do not use 'comment_body' or other variations. Provide helpful, relevant information that adds value to the conversation." + "description": "The comment text in Markdown format. Must be the final intended comment — not a placeholder or test value. This is the 'body' field - do not use 'comment_body' or other variations. Provide helpful, relevant information that adds value to the conversation." }, "item_number": { "type": ["number", "string"], diff --git a/pkg/workflow/js/safe_outputs_tools.json b/pkg/workflow/js/safe_outputs_tools.json index 768f9757fc3..e6c5c2e45db 100644 --- a/pkg/workflow/js/safe_outputs_tools.json +++ b/pkg/workflow/js/safe_outputs_tools.json @@ -1,7 +1,7 @@ [ { "name": "create_issue", - "description": "Create a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. This is a write-once declaration for a real intended issue, not a sandbox or probe: do not call it with placeholder titles/bodies or auth experiments. If you are not ready to open the real issue, use noop or report_incomplete instead. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead.", + "description": "WRITE-ONCE: do NOT call this tool with empty or placeholder arguments to probe or discover its schema — required fields (title, body) are listed in this schema; if you are not ready to open the real issue, call `noop` instead. Creates a new GitHub issue for tracking bugs, feature requests, or tasks. Use this for actionable work items that need assignment, labeling, and status tracking. For reports, announcements, or status updates that don't require task tracking, use create_discussion instead.", "inputSchema": { "type": "object", "required": [ @@ -11,11 +11,11 @@ "properties": { "title": { "type": "string", - "description": "Concise issue title summarizing the bug, feature, or task. The title appears as the main heading, so keep it brief and descriptive." + "description": "Concise issue title summarizing the bug, feature, or task. Must be the final intended title — not a placeholder or test value. The title appears as the main heading, so keep it brief and descriptive." }, "body": { "type": "string", - "description": "Detailed issue description in Markdown. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate." + "description": "Detailed issue description in Markdown. Must be the final intended body — not a placeholder or test value. Do NOT repeat the title as a heading since it already appears as the issue's h1. Include context, reproduction steps, or acceptance criteria as appropriate." }, "labels": { "type": "array", @@ -280,7 +280,7 @@ }, { "name": "add_comment", - "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. This is a write-once declaration for a real intended comment, not a sandbox or probe: do not call it with placeholder bodies or auth experiments. If you are not ready to post the real comment, use noop or report_incomplete instead. For creating new items, use create_issue, create_discussion, or create_pull_request instead. IMPORTANT: Comments are subject to validation constraints enforced by the MCP server - maximum 65536 characters for the complete comment (including footer which is added automatically), 10 mentions (@username), and 50 links. Exceeding these limits will result in an immediate error with specific guidance. NOTE: By default, this tool requires discussions:write permission. If your GitHub App lacks Discussions permission, set 'discussions: false' in the workflow's safe-outputs.add-comment configuration to exclude this permission.", + "description": "WRITE-ONCE: do NOT call this tool with empty or placeholder arguments to probe or discover its schema — the required `body` field is listed in this schema; if you are not ready to post a real comment, call `noop` instead. Adds a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. IMPORTANT: Comments are subject to validation constraints enforced by the MCP server - maximum 65536 characters for the complete comment (including footer which is added automatically), 10 mentions (@username), and 50 links. Exceeding these limits will result in an immediate error with specific guidance. NOTE: By default, this tool requires discussions:write permission. If your GitHub App lacks Discussions permission, set 'discussions: false' in the workflow's safe-outputs.add-comment configuration to exclude this permission.", "inputSchema": { "type": "object", "required": [ @@ -289,7 +289,7 @@ "properties": { "body": { "type": "string", - "description": "The comment text in Markdown format. This is the 'body' field - do not use 'comment_body' or other variations. Provide helpful, relevant information that adds value to the conversation. CONSTRAINTS: The complete comment (your body text + automatically added footer) must not exceed 65536 characters total. Maximum 10 mentions (@username), maximum 50 links (http/https URLs). A footer (~200-500 characters) is automatically appended with workflow attribution, so leave adequate space. If these limits are exceeded, the tool call will fail with a detailed error message indicating which constraint was violated." + "description": "The comment text in Markdown format. Must be the final intended comment — not a placeholder or test value. This is the 'body' field - do not use 'comment_body' or other variations. Provide helpful, relevant information that adds value to the conversation. CONSTRAINTS: The complete comment (your body text + automatically added footer) must not exceed 65536 characters total. Maximum 10 mentions (@username), maximum 50 links (http/https URLs). A footer (~200-500 characters) is automatically appended with workflow attribution, so leave adequate space. If these limits are exceeded, the tool call will fail with a detailed error message indicating which constraint was violated." }, "item_number": { "type": [