diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index 233b8f39bf..31bd8d077f 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -353,38 +353,38 @@ jobs: "name": "noop" }, { - "description": "Post a message to a Slack channel. Message must be 200 characters or less. Supports basic Slack markdown: *bold*, _italic_, ~strike~, `code`, ```code block```, \u003equote, and links \u003curl|text\u003e. Requires GH_AW_SLACK_CHANNEL_ID environment variable to be set.", + "description": "Add a comment to a Notion page", "inputSchema": { "additionalProperties": false, "properties": { - "message": { - "description": "The message to post (max 200 characters, supports Slack markdown)", + "comment": { + "description": "The comment text to add", "type": "string" } }, "required": [ - "message" + "comment" ], "type": "object" }, - "name": "post_to_slack_channel" + "name": "notion_add_comment" }, { - "description": "Add a comment to a Notion page", + "description": "Post a message to a Slack channel. Message must be 200 characters or less. Supports basic Slack markdown: *bold*, _italic_, ~strike~, `code`, ```code block```, \u003equote, and links \u003curl|text\u003e. Requires GH_AW_SLACK_CHANNEL_ID environment variable to be set.", "inputSchema": { "additionalProperties": false, "properties": { - "comment": { - "description": "The comment text to add", + "message": { + "description": "The message to post (max 200 characters, supports Slack markdown)", "type": "string" } }, "required": [ - "comment" + "message" ], "type": "object" }, - "name": "notion_add_comment" + "name": "post_to_slack_channel" } ] EOF diff --git a/pkg/workflow/safe_outputs_config_generation.go b/pkg/workflow/safe_outputs_config_generation.go index bf43d2b412..73ea6b4a13 100644 --- a/pkg/workflow/safe_outputs_config_generation.go +++ b/pkg/workflow/safe_outputs_config_generation.go @@ -3,6 +3,7 @@ package workflow import ( "encoding/json" "fmt" + "sort" ) // ======================================== @@ -473,6 +474,7 @@ func generateCustomJobToolDefinition(jobName string, jobConfig *SafeJobConfig) m // Add required fields array if any inputs are required if len(requiredFields) > 0 { + sort.Strings(requiredFields) inputSchema["required"] = requiredFields } diff --git a/pkg/workflow/safe_outputs_custom_job_tools_test.go b/pkg/workflow/safe_outputs_custom_job_tools_test.go index fa5a3188e6..47cec99d64 100644 --- a/pkg/workflow/safe_outputs_custom_job_tools_test.go +++ b/pkg/workflow/safe_outputs_custom_job_tools_test.go @@ -207,3 +207,76 @@ func TestCustomJobToolsWithDifferentInputTypes(t *testing.T) { assert.NotContains(t, required, "count", "Count should not be required") assert.NotContains(t, required, "mode", "Mode should not be required") } + +// TestCustomJobToolsRequiredFieldsSorted verifies that the required array +// is sorted alphabetically for stable output +func TestCustomJobToolsRequiredFieldsSorted(t *testing.T) { + workflowData := &WorkflowData{ + SafeOutputs: &SafeOutputsConfig{ + Jobs: map[string]*SafeJobConfig{ + "sorted_test": { + Description: "Job to test sorted required fields", + Inputs: map[string]*InputDefinition{ + "zebra": { + Description: "Last alphabetically", + Required: true, + Type: "string", + }, + "apple": { + Description: "First alphabetically", + Required: true, + Type: "string", + }, + "middle": { + Description: "Middle alphabetically", + Required: true, + Type: "string", + }, + "banana": { + Description: "Second alphabetically", + Required: true, + Type: "string", + }, + }, + }, + }, + }, + } + + // Generate the tools JSON + toolsJSON, err := generateFilteredToolsJSON(workflowData) + require.NoError(t, err, "Should generate tools JSON") + + // Parse the JSON + var tools []map[string]any + err = json.Unmarshal([]byte(toolsJSON), &tools) + require.NoError(t, err, "Should parse tools JSON") + + // Find the sorted_test tool + var sortedTestTool map[string]any + for _, tool := range tools { + if name, ok := tool["name"].(string); ok && name == "sorted_test" { + sortedTestTool = tool + break + } + } + + require.NotNil(t, sortedTestTool, "Should find sorted_test tool in tools.json") + + // Verify the input schema + inputSchema, ok := sortedTestTool["inputSchema"].(map[string]any) + require.True(t, ok, "Should have inputSchema") + + // Verify required fields are sorted + required, ok := inputSchema["required"].([]any) + require.True(t, ok, "Should have required array") + assert.Len(t, required, 4, "Should have 4 required fields") + + // Check that the required array is sorted alphabetically + expectedOrder := []string{"apple", "banana", "middle", "zebra"} + for i, expectedField := range expectedOrder { + actualField, ok := required[i].(string) + require.True(t, ok, "Required field should be a string") + assert.Equal(t, expectedField, actualField, "Required field at index %d should be %s", i, expectedField) + } +}