From 2a71d84fa7349aa0b5acc7014b1d8c59f26f3082 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:22:19 +0000 Subject: [PATCH 1/8] Initial plan From 39bbd63d50d1622f763606fc377229a4e06d0098 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:36:00 +0000 Subject: [PATCH 2/8] Add debug logging for dispatch_workflow tool registration Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- .../setup/js/safe_outputs_tools_loader.cjs | 14 +++- .../test_dispatch_workflow_debug_logging.cjs | 72 +++++++++++++++++++ .../test_dispatch_workflow_empty_config.cjs | 51 +++++++++++++ .../test_dispatch_workflow_registration.cjs | 68 ++++++++++++++++++ 4 files changed, 202 insertions(+), 3 deletions(-) create mode 100644 actions/setup/js/test_dispatch_workflow_debug_logging.cjs create mode 100644 actions/setup/js/test_dispatch_workflow_empty_config.cjs create mode 100644 actions/setup/js/test_dispatch_workflow_registration.cjs diff --git a/actions/setup/js/safe_outputs_tools_loader.cjs b/actions/setup/js/safe_outputs_tools_loader.cjs index 02d8f6e789..f7d9cd9b0a 100644 --- a/actions/setup/js/safe_outputs_tools_loader.cjs +++ b/actions/setup/js/safe_outputs_tools_loader.cjs @@ -94,9 +94,17 @@ function registerPredefinedTools(server, tools, config, registerTool, normalizeT // Check if this is a dispatch_workflow tool (has _workflow_name metadata) // These tools are dynamically generated with workflow-specific names - if (tool._workflow_name && config.dispatch_workflow) { - registerTool(server, tool); - return; + if (tool._workflow_name) { + server.debug(`Found dispatch_workflow tool: ${tool.name} (_workflow_name: ${tool._workflow_name})`); + if (config.dispatch_workflow) { + server.debug(` dispatch_workflow config exists, registering tool`); + registerTool(server, tool); + return; + } else { + server.debug(` WARNING: dispatch_workflow config is missing or falsy - tool will NOT be registered`); + server.debug(` Config keys: ${Object.keys(config).join(", ")}`); + server.debug(` config.dispatch_workflow value: ${JSON.stringify(config.dispatch_workflow)}`); + } } }); } diff --git a/actions/setup/js/test_dispatch_workflow_debug_logging.cjs b/actions/setup/js/test_dispatch_workflow_debug_logging.cjs new file mode 100644 index 0000000000..96bd2509d9 --- /dev/null +++ b/actions/setup/js/test_dispatch_workflow_debug_logging.cjs @@ -0,0 +1,72 @@ +// Test to verify debug logging for dispatch_workflow registration +const { registerPredefinedTools } = require("./safe_outputs_tools_loader.cjs"); + +// Mock server with captured debug messages +const debugMessages = []; +const mockServer = { + debug: (msg) => { + console.log("[DEBUG]", msg); + debugMessages.push(msg); + }, + tools: {}, +}; + +// Test tools +const tools = [ + { + name: "test_workflow", + description: "Dispatch test workflow", + _workflow_name: "test-workflow", + inputSchema: { type: "object", properties: {} }, + }, +]; + +// Mock functions +const registerTool = (server, tool) => { + console.log("[REGISTER]", tool.name); + server.tools[tool.name] = tool; +}; +const normalizeTool = (name) => name.replace(/-/g, "_").toLowerCase(); + +console.log("=== Test Case 1: dispatch_workflow config EXISTS ==="); +debugMessages.length = 0; +mockServer.tools = {}; +const config1 = { + dispatch_workflow: { max: 1, workflows: ["test-workflow"] }, +}; + +registerPredefinedTools(mockServer, tools, config1, registerTool, normalizeTool); + +console.log("Registered tools:", Object.keys(mockServer.tools)); +console.log("Debug messages:"); +debugMessages.forEach(msg => console.log(" ", msg)); +console.log(""); + +console.log("=== Test Case 2: dispatch_workflow config MISSING ==="); +debugMessages.length = 0; +mockServer.tools = {}; +const config2 = { + missing_tool: {}, + noop: {}, +}; + +registerPredefinedTools(mockServer, tools, config2, registerTool, normalizeTool); + +console.log("Registered tools:", Object.keys(mockServer.tools)); +console.log("Debug messages:"); +debugMessages.forEach(msg => console.log(" ", msg)); +console.log(""); + +if (mockServer.tools.test_workflow) { + console.log("❌ UNEXPECTED: tool was registered even though dispatch_workflow config is missing"); +} else { + console.log("✅ CORRECT: tool was NOT registered when dispatch_workflow config is missing"); +} + +// Check that warning was logged +const hasWarning = debugMessages.some(msg => msg.includes("WARNING")); +if (hasWarning) { + console.log("✅ CORRECT: Warning message was logged"); +} else { + console.log("❌ MISSING: Warning message was not logged"); +} diff --git a/actions/setup/js/test_dispatch_workflow_empty_config.cjs b/actions/setup/js/test_dispatch_workflow_empty_config.cjs new file mode 100644 index 0000000000..7af5c3cd23 --- /dev/null +++ b/actions/setup/js/test_dispatch_workflow_empty_config.cjs @@ -0,0 +1,51 @@ +// Test with empty dispatch_workflow config (no workflows key) +const { registerPredefinedTools } = require("./safe_outputs_tools_loader.cjs"); + +const mockServer = { + debug: (msg) => console.log("[DEBUG]", msg), + tools: {}, +}; + +const tools = [ + { + name: "test_workflow", + description: "Dispatch test workflow", + _workflow_name: "test-workflow", + inputSchema: { type: "object", properties: {} }, + }, +]; + +// Test with empty dispatch_workflow config +const config1 = { + dispatch_workflow: {}, // Empty config +}; + +const registerTool = (server, tool) => { + console.log("[REGISTER]", tool.name); + server.tools[tool.name] = tool; +}; + +const normalizeTool = (name) => name.replace(/-/g, "_").toLowerCase(); + +console.log("=== Test 1: Empty dispatch_workflow config ==="); +console.log("Config.dispatch_workflow:", config1.dispatch_workflow); +console.log("Truthy?", !!config1.dispatch_workflow); + +registerPredefinedTools(mockServer, tools, config1, registerTool, normalizeTool); + +console.log("Registered:", Object.keys(mockServer.tools)); +console.log("Result:", mockServer.tools.test_workflow ? "✅ REGISTERED" : "❌ NOT REGISTERED"); +console.log(""); + +// Test with null/undefined +mockServer.tools = {}; +const config2 = {}; // No dispatch_workflow key + +console.log("=== Test 2: No dispatch_workflow config ==="); +console.log("Config.dispatch_workflow:", config2.dispatch_workflow); +console.log("Truthy?", !!config2.dispatch_workflow); + +registerPredefinedTools(mockServer, tools, config2, registerTool, normalizeTool); + +console.log("Registered:", Object.keys(mockServer.tools)); +console.log("Result:", mockServer.tools.test_workflow ? "✅ REGISTERED" : "❌ NOT REGISTERED"); diff --git a/actions/setup/js/test_dispatch_workflow_registration.cjs b/actions/setup/js/test_dispatch_workflow_registration.cjs new file mode 100644 index 0000000000..a25c5ddd14 --- /dev/null +++ b/actions/setup/js/test_dispatch_workflow_registration.cjs @@ -0,0 +1,68 @@ +// Test script to verify dispatch_workflow tool registration +const { registerPredefinedTools } = require("./safe_outputs_tools_loader.cjs"); + +// Mock server +const mockServer = { + debug: (msg) => console.log("[DEBUG]", msg), + tools: {}, +}; + +// Test tools with dispatch_workflow tool +const tools = [ + { + name: "missing_tool", + description: "Report missing tool", + inputSchema: { type: "object", properties: {} }, + }, + { + name: "test_workflow", + description: "Dispatch test workflow", + _workflow_name: "test-workflow", + inputSchema: { type: "object", properties: {} }, + }, +]; + +// Test config +const config = { + dispatch_workflow: { + max: 1, + workflows: ["test-workflow"], + workflow_files: { "test-workflow": ".yml" }, + }, + missing_tool: {}, +}; + +// Mock registerTool function +const registerTool = (server, tool) => { + console.log("[REGISTER]", tool.name, tool._workflow_name ? `(_workflow_name: ${tool._workflow_name})` : ""); + server.tools[tool.name] = tool; +}; + +// Mock normalizeTool function +const normalizeTool = (name) => name.replace(/-/g, "_").toLowerCase(); + +console.log("=== Testing dispatch_workflow tool registration ==="); +console.log("Tools:", tools.map(t => t.name)); +console.log("Config keys:", Object.keys(config)); +console.log("Config.dispatch_workflow:", config.dispatch_workflow); +console.log(""); + +registerPredefinedTools(mockServer, tools, config, registerTool, normalizeTool); + +console.log(""); +console.log("=== Registration Results ==="); +console.log("Registered tools:", Object.keys(mockServer.tools)); +console.log(""); + +if (mockServer.tools.test_workflow) { + console.log("✅ SUCCESS: test_workflow was registered"); +} else { + console.log("❌ FAILURE: test_workflow was NOT registered"); + console.log("This is the bug!"); +} + +if (mockServer.tools.missing_tool) { + console.log("✅ SUCCESS: missing_tool was registered"); +} else { + console.log("❌ FAILURE: missing_tool was NOT registered"); +} From 3e75559e46b672868fdb760862160869182ca976 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:37:23 +0000 Subject: [PATCH 3/8] Add detailed logging for dispatch_workflow tool loading and registration Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- .../setup/js/safe_outputs_tools_loader.cjs | 10 +++ .../test_dispatch_workflow_debug_logging.cjs | 72 ------------------- .../test_dispatch_workflow_empty_config.cjs | 51 ------------- .../test_dispatch_workflow_registration.cjs | 68 ------------------ 4 files changed, 10 insertions(+), 191 deletions(-) delete mode 100644 actions/setup/js/test_dispatch_workflow_debug_logging.cjs delete mode 100644 actions/setup/js/test_dispatch_workflow_empty_config.cjs delete mode 100644 actions/setup/js/test_dispatch_workflow_registration.cjs diff --git a/actions/setup/js/safe_outputs_tools_loader.cjs b/actions/setup/js/safe_outputs_tools_loader.cjs index f7d9cd9b0a..7ac43a6d98 100644 --- a/actions/setup/js/safe_outputs_tools_loader.cjs +++ b/actions/setup/js/safe_outputs_tools_loader.cjs @@ -27,6 +27,16 @@ function loadTools(server) { server.debug(`Tools file read successfully, attempting to parse JSON`); const tools = JSON.parse(toolsFileContent); server.debug(`Successfully parsed ${tools.length} tools from file`); + + // Log details about dispatch_workflow tools for debugging + const dispatchWorkflowTools = tools.filter(t => t._workflow_name); + if (dispatchWorkflowTools.length > 0) { + server.debug(` Found ${dispatchWorkflowTools.length} dispatch_workflow tools:`); + dispatchWorkflowTools.forEach(t => { + server.debug(` - ${t.name} (workflow: ${t._workflow_name})`); + }); + } + return tools; } catch (error) { server.debug(`Error reading tools file: ${getErrorMessage(error)}`); diff --git a/actions/setup/js/test_dispatch_workflow_debug_logging.cjs b/actions/setup/js/test_dispatch_workflow_debug_logging.cjs deleted file mode 100644 index 96bd2509d9..0000000000 --- a/actions/setup/js/test_dispatch_workflow_debug_logging.cjs +++ /dev/null @@ -1,72 +0,0 @@ -// Test to verify debug logging for dispatch_workflow registration -const { registerPredefinedTools } = require("./safe_outputs_tools_loader.cjs"); - -// Mock server with captured debug messages -const debugMessages = []; -const mockServer = { - debug: (msg) => { - console.log("[DEBUG]", msg); - debugMessages.push(msg); - }, - tools: {}, -}; - -// Test tools -const tools = [ - { - name: "test_workflow", - description: "Dispatch test workflow", - _workflow_name: "test-workflow", - inputSchema: { type: "object", properties: {} }, - }, -]; - -// Mock functions -const registerTool = (server, tool) => { - console.log("[REGISTER]", tool.name); - server.tools[tool.name] = tool; -}; -const normalizeTool = (name) => name.replace(/-/g, "_").toLowerCase(); - -console.log("=== Test Case 1: dispatch_workflow config EXISTS ==="); -debugMessages.length = 0; -mockServer.tools = {}; -const config1 = { - dispatch_workflow: { max: 1, workflows: ["test-workflow"] }, -}; - -registerPredefinedTools(mockServer, tools, config1, registerTool, normalizeTool); - -console.log("Registered tools:", Object.keys(mockServer.tools)); -console.log("Debug messages:"); -debugMessages.forEach(msg => console.log(" ", msg)); -console.log(""); - -console.log("=== Test Case 2: dispatch_workflow config MISSING ==="); -debugMessages.length = 0; -mockServer.tools = {}; -const config2 = { - missing_tool: {}, - noop: {}, -}; - -registerPredefinedTools(mockServer, tools, config2, registerTool, normalizeTool); - -console.log("Registered tools:", Object.keys(mockServer.tools)); -console.log("Debug messages:"); -debugMessages.forEach(msg => console.log(" ", msg)); -console.log(""); - -if (mockServer.tools.test_workflow) { - console.log("❌ UNEXPECTED: tool was registered even though dispatch_workflow config is missing"); -} else { - console.log("✅ CORRECT: tool was NOT registered when dispatch_workflow config is missing"); -} - -// Check that warning was logged -const hasWarning = debugMessages.some(msg => msg.includes("WARNING")); -if (hasWarning) { - console.log("✅ CORRECT: Warning message was logged"); -} else { - console.log("❌ MISSING: Warning message was not logged"); -} diff --git a/actions/setup/js/test_dispatch_workflow_empty_config.cjs b/actions/setup/js/test_dispatch_workflow_empty_config.cjs deleted file mode 100644 index 7af5c3cd23..0000000000 --- a/actions/setup/js/test_dispatch_workflow_empty_config.cjs +++ /dev/null @@ -1,51 +0,0 @@ -// Test with empty dispatch_workflow config (no workflows key) -const { registerPredefinedTools } = require("./safe_outputs_tools_loader.cjs"); - -const mockServer = { - debug: (msg) => console.log("[DEBUG]", msg), - tools: {}, -}; - -const tools = [ - { - name: "test_workflow", - description: "Dispatch test workflow", - _workflow_name: "test-workflow", - inputSchema: { type: "object", properties: {} }, - }, -]; - -// Test with empty dispatch_workflow config -const config1 = { - dispatch_workflow: {}, // Empty config -}; - -const registerTool = (server, tool) => { - console.log("[REGISTER]", tool.name); - server.tools[tool.name] = tool; -}; - -const normalizeTool = (name) => name.replace(/-/g, "_").toLowerCase(); - -console.log("=== Test 1: Empty dispatch_workflow config ==="); -console.log("Config.dispatch_workflow:", config1.dispatch_workflow); -console.log("Truthy?", !!config1.dispatch_workflow); - -registerPredefinedTools(mockServer, tools, config1, registerTool, normalizeTool); - -console.log("Registered:", Object.keys(mockServer.tools)); -console.log("Result:", mockServer.tools.test_workflow ? "✅ REGISTERED" : "❌ NOT REGISTERED"); -console.log(""); - -// Test with null/undefined -mockServer.tools = {}; -const config2 = {}; // No dispatch_workflow key - -console.log("=== Test 2: No dispatch_workflow config ==="); -console.log("Config.dispatch_workflow:", config2.dispatch_workflow); -console.log("Truthy?", !!config2.dispatch_workflow); - -registerPredefinedTools(mockServer, tools, config2, registerTool, normalizeTool); - -console.log("Registered:", Object.keys(mockServer.tools)); -console.log("Result:", mockServer.tools.test_workflow ? "✅ REGISTERED" : "❌ NOT REGISTERED"); diff --git a/actions/setup/js/test_dispatch_workflow_registration.cjs b/actions/setup/js/test_dispatch_workflow_registration.cjs deleted file mode 100644 index a25c5ddd14..0000000000 --- a/actions/setup/js/test_dispatch_workflow_registration.cjs +++ /dev/null @@ -1,68 +0,0 @@ -// Test script to verify dispatch_workflow tool registration -const { registerPredefinedTools } = require("./safe_outputs_tools_loader.cjs"); - -// Mock server -const mockServer = { - debug: (msg) => console.log("[DEBUG]", msg), - tools: {}, -}; - -// Test tools with dispatch_workflow tool -const tools = [ - { - name: "missing_tool", - description: "Report missing tool", - inputSchema: { type: "object", properties: {} }, - }, - { - name: "test_workflow", - description: "Dispatch test workflow", - _workflow_name: "test-workflow", - inputSchema: { type: "object", properties: {} }, - }, -]; - -// Test config -const config = { - dispatch_workflow: { - max: 1, - workflows: ["test-workflow"], - workflow_files: { "test-workflow": ".yml" }, - }, - missing_tool: {}, -}; - -// Mock registerTool function -const registerTool = (server, tool) => { - console.log("[REGISTER]", tool.name, tool._workflow_name ? `(_workflow_name: ${tool._workflow_name})` : ""); - server.tools[tool.name] = tool; -}; - -// Mock normalizeTool function -const normalizeTool = (name) => name.replace(/-/g, "_").toLowerCase(); - -console.log("=== Testing dispatch_workflow tool registration ==="); -console.log("Tools:", tools.map(t => t.name)); -console.log("Config keys:", Object.keys(config)); -console.log("Config.dispatch_workflow:", config.dispatch_workflow); -console.log(""); - -registerPredefinedTools(mockServer, tools, config, registerTool, normalizeTool); - -console.log(""); -console.log("=== Registration Results ==="); -console.log("Registered tools:", Object.keys(mockServer.tools)); -console.log(""); - -if (mockServer.tools.test_workflow) { - console.log("✅ SUCCESS: test_workflow was registered"); -} else { - console.log("❌ FAILURE: test_workflow was NOT registered"); - console.log("This is the bug!"); -} - -if (mockServer.tools.missing_tool) { - console.log("✅ SUCCESS: missing_tool was registered"); -} else { - console.log("❌ FAILURE: missing_tool was NOT registered"); -} From 52a0188637df04e6063f09602783cd6ce38794ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:40:41 +0000 Subject: [PATCH 4/8] Add troubleshooting guide for dispatch_workflow tool registration Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- docs/troubleshooting-dispatch-workflow.md | 192 ++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/troubleshooting-dispatch-workflow.md diff --git a/docs/troubleshooting-dispatch-workflow.md b/docs/troubleshooting-dispatch-workflow.md new file mode 100644 index 0000000000..a4e8346abc --- /dev/null +++ b/docs/troubleshooting-dispatch-workflow.md @@ -0,0 +1,192 @@ +# Troubleshooting dispatch_workflow Tools Not Appearing + +## Problem + +When using `dispatch-workflow` safe output configuration, the expected workflow dispatch tools (e.g., `safeoutputs-my_workflow`) do not appear in the available MCP tools list. + +## Symptoms + +- Workflow has `dispatch-workflow` configured in frontmatter with a list of workflows +- Expected tools like `safeoutputs-add_name`, `safeoutputs-add_emojis` are missing +- Agent reports missing_tool with message about workflow dispatch tools not being available +- Only standard safe output tools appear (missing_tool, missing_data, noop, add_comment) + +## Diagnostic Steps + +### 1. Check MCP Server Logs + +With the enhanced logging (added in PR #XXXX), the MCP server logs will show detailed information about tool loading and registration. + +**Look for these log entries:** + +``` +Reading tools from file: /opt/gh-aw/safeoutputs/tools.json +Successfully parsed N tools from file + Found 3 dispatch_workflow tools: + - add_name (workflow: add-name) + - add_emojis (workflow: add-emojis) + - all_workflows (workflow: all-workflows) +``` + +If you see this, tools.json is correct. Continue to registration logs. + +``` +Found dispatch_workflow tool: add_name (_workflow_name: add-name) + dispatch_workflow config exists, registering tool +``` + +If you see this, the tool is being registered successfully. + +``` +Found dispatch_workflow tool: add_name (_workflow_name: add-name) + WARNING: dispatch_workflow config is missing or falsy - tool will NOT be registered + Config keys: missing_tool, noop, add_comment + config.dispatch_workflow value: undefined +``` + +If you see this WARNING, the config doesn't have dispatch_workflow, which is the root cause. + +### 2. Verify Workflow Configuration + +Check the workflow's frontmatter: + +```yaml +--- +on: issues +engine: copilot +safe-outputs: + dispatch-workflow: + workflows: + - add-name + - add-emojis + - all-workflows + max: 3 +--- +``` + +### 3. Check Compiled Configuration + +Extract the config from the compiled `.lock.yml` file: + +```bash +grep -A 5 "config.json" .github/workflows/my-workflow.lock.yml | grep dispatch_workflow +``` + +**Expected output:** +```json +{"dispatch_workflow":{"max":3,"workflows":["add-name","add-emojis","all-workflows"],"workflow_files":{...}},"missing_data":{},"missing_tool":{},"noop":{}} +``` + +**If dispatch_workflow is missing from config, the workflow needs recompilation.** + +### 4. Verify Target Workflows Exist + +For each workflow in the `workflows` list, verify: + +```bash +# Check if workflow files exist +ls -la .github/workflows/add-name.{md,yml,lock.yml} +ls -la .github/workflows/add-emojis.{md,yml,lock.yml} +ls -la .github/workflows/all-workflows.{md,yml,lock.yml} +``` + +**Requirements:** +- At least one of `.yml` or `.lock.yml` must exist +- If only `.md` exists, compile it first: `gh aw compile .github/workflows/add-name.md` +- The workflow must have `workflow_dispatch` in its `on:` triggers + +## Common Causes and Solutions + +### Cause 1: Workflow Not Recompiled + +**Symptom:** dispatch_workflow missing from config.json in lock file + +**Solution:** +```bash +gh aw compile .github/workflows/my-workflow.md +``` + +### Cause 2: Target Workflows Don't Exist + +**Symptom:** Tools generated with empty inputs, but validation failed during compilation + +**Solution:** +1. Create the target workflows in `.github/workflows/` +2. Ensure they have `workflow_dispatch` trigger: + ```yaml + on: + workflow_dispatch: + inputs: + param1: + description: "Parameter 1" + type: string + ``` +3. Compile them: `gh aw compile .github/workflows/target-workflow.md` +4. Recompile the dispatcher workflow + +### Cause 3: Target Workflows Missing workflow_dispatch + +**Symptom:** Compilation error: "workflow 'X' does not support workflow_dispatch trigger" + +**Solution:** +Add `workflow_dispatch` to the target workflow's triggers: +```yaml +on: + issues: # existing trigger + workflow_dispatch: # add this + inputs: + # optional: define inputs +``` + +### Cause 4: Config Not Loaded Properly + +**Symptom:** MCP server logs show config.dispatch_workflow is undefined + +**Solution:** +Check for file system issues: +```bash +# In the agent job, verify files exist +ls -la /opt/gh-aw/safeoutputs/ +cat /opt/gh-aw/safeoutputs/config.json | jq . +cat /opt/gh-aw/safeoutputs/tools.json | jq '. | map(select(._workflow_name))' +``` + +## Verification + +After applying fixes, verify tools are registered: + +1. Recompile the workflow: `gh aw compile .github/workflows/my-workflow.md` +2. Trigger the workflow +3. Check MCP server logs for successful registration: + ``` + Found dispatch_workflow tool: add_name (_workflow_name: add-name) + dispatch_workflow config exists, registering tool + Registered tool: add_name + ``` +4. Verify tools appear in agent's available tools + +## Prevention + +To prevent this issue: + +1. **Always compile after adding dispatch-workflow**: + ```bash + gh aw compile .github/workflows/my-workflow.md + ``` + +2. **Verify compilation succeeded**: + ```bash + grep dispatch_workflow .github/workflows/my-workflow.lock.yml + ``` + +3. **Create target workflows first** before adding them to dispatch-workflow list + +4. **Use validation** to catch issues early: + ```bash + gh aw compile --validate .github/workflows/my-workflow.md + ``` + +## Related Issues + +- Issue #XXXX: dispatch_workflow tools not appearing in MCP tools +- PR #XXXX: Enhanced diagnostic logging for dispatch_workflow registration From 6a88985ca996a4b4c047603d34474083f573877e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:41:58 +0000 Subject: [PATCH 5/8] Address code review: Add comment explaining WARNING prefix in debug logs Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- actions/setup/js/safe_outputs_tools_loader.cjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/actions/setup/js/safe_outputs_tools_loader.cjs b/actions/setup/js/safe_outputs_tools_loader.cjs index 7ac43a6d98..5d5af91538 100644 --- a/actions/setup/js/safe_outputs_tools_loader.cjs +++ b/actions/setup/js/safe_outputs_tools_loader.cjs @@ -111,6 +111,8 @@ function registerPredefinedTools(server, tools, config, registerTool, normalizeT registerTool(server, tool); return; } else { + // Note: Using server.debug() with "WARNING:" prefix since MCP server only provides + // debug and debugError methods. The prefix helps identify severity in logs. server.debug(` WARNING: dispatch_workflow config is missing or falsy - tool will NOT be registered`); server.debug(` Config keys: ${Object.keys(config).join(", ")}`); server.debug(` config.dispatch_workflow value: ${JSON.stringify(config.dispatch_workflow)}`); From ae4f96e8220f92e92e0f616eb47e9d9ef5929260 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:51:57 +0000 Subject: [PATCH 6/8] Add similar tool suggestions when MCP tool not found Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- actions/setup/js/mcp_server_core.cjs | 87 +++++++++++- actions/setup/js/mcp_server_core.test.cjs | 161 ++++++++++++++++++++++ 2 files changed, 246 insertions(+), 2 deletions(-) diff --git a/actions/setup/js/mcp_server_core.cjs b/actions/setup/js/mcp_server_core.cjs index e41b6c2067..dd8f6dd9b0 100644 --- a/actions/setup/js/mcp_server_core.cjs +++ b/actions/setup/js/mcp_server_core.cjs @@ -466,6 +466,69 @@ function registerTool(server, tool) { server.debug(`Registered tool: ${normalizedName}`); } +/** + * Calculate Levenshtein distance between two strings + * @param {string} a - First string + * @param {string} b - Second string + * @returns {number} Edit distance + */ +function levenshteinDistance(a, b) { + const matrix = []; + + // Initialize first column + for (let i = 0; i <= b.length; i++) { + matrix[i] = [i]; + } + + // Initialize first row + for (let j = 0; j <= a.length; j++) { + matrix[0][j] = j; + } + + // Fill in the rest of the matrix + for (let i = 1; i <= b.length; i++) { + for (let j = 1; j <= a.length; j++) { + if (b.charAt(i - 1) === a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, // substitution + matrix[i][j - 1] + 1, // insertion + matrix[i - 1][j] + 1 // deletion + ); + } + } + } + + return matrix[b.length][a.length]; +} + +/** + * Find similar tool names from available tools + * @param {string} requestedTool - The tool name that was requested + * @param {Object} availableTools - Object of available tools + * @param {number} maxSuggestions - Maximum number of suggestions to return + * @returns {Array<{name: string, distance: number}>} Array of similar tool names with distances + */ +function findSimilarTools(requestedTool, availableTools, maxSuggestions = 3) { + const normalizedRequested = normalizeTool(requestedTool); + const suggestions = []; + + // Calculate distance for each available tool + for (const toolName of Object.keys(availableTools)) { + const distance = levenshteinDistance(normalizedRequested, toolName); + suggestions.push({ name: toolName, distance }); + } + + // Sort by distance (closest first) and take top N + suggestions.sort((a, b) => a.distance - b.distance); + + // Only return suggestions that are reasonably similar + // (distance <= half the length of the requested tool name + 3) + const maxDistance = Math.floor(normalizedRequested.length / 2) + 3; + return suggestions.filter(s => s.distance <= maxDistance).slice(0, maxSuggestions); +} + /** * Normalize a tool name (convert dashes to underscores, lowercase) * @param {string} name - The tool name to normalize @@ -530,9 +593,18 @@ async function handleRequest(server, request, defaultHandler) { } const tool = server.tools[normalizeTool(name)]; if (!tool) { + // Find similar tools to suggest + const similarTools = findSimilarTools(name, server.tools); + let errorMessage = `Tool '${name}' not found`; + + if (similarTools.length > 0) { + const suggestions = similarTools.map(s => s.name).join(", "); + errorMessage += `. Did you mean one of these: ${suggestions}?`; + } + throw { code: -32602, - message: `Tool '${name}' not found`, + message: errorMessage, }; } @@ -649,7 +721,16 @@ async function handleMessage(server, req, defaultHandler) { } const tool = server.tools[normalizeTool(name)]; if (!tool) { - server.replyError(id, -32601, `Tool not found: ${name} (${normalizeTool(name)})`); + // Find similar tools to suggest + const similarTools = findSimilarTools(name, server.tools); + let errorMessage = `Tool not found: ${name} (${normalizeTool(name)})`; + + if (similarTools.length > 0) { + const suggestions = similarTools.map(s => s.name).join(", "); + errorMessage += `. Did you mean one of these: ${suggestions}?`; + } + + server.replyError(id, -32601, errorMessage); return; } @@ -744,4 +825,6 @@ module.exports = { processReadBuffer, start, loadToolHandlers, + findSimilarTools, + levenshteinDistance, }; diff --git a/actions/setup/js/mcp_server_core.test.cjs b/actions/setup/js/mcp_server_core.test.cjs index 8430565909..0fe25fc0ff 100644 --- a/actions/setup/js/mcp_server_core.test.cjs +++ b/actions/setup/js/mcp_server_core.test.cjs @@ -905,4 +905,165 @@ echo "result=$INPUT_MY_INPUT" >> $GITHUB_OUTPUT expect(resultContent.outputs.result).toBe("test-value"); }); }); + + describe("levenshteinDistance", () => { + it("should calculate correct distance for identical strings", async () => { + const { levenshteinDistance } = await import("./mcp_server_core.cjs"); + expect(levenshteinDistance("test", "test")).toBe(0); + }); + + it("should calculate correct distance for single character change", async () => { + const { levenshteinDistance } = await import("./mcp_server_core.cjs"); + expect(levenshteinDistance("cat", "bat")).toBe(1); + expect(levenshteinDistance("cat", "cut")).toBe(1); + }); + + it("should calculate correct distance for insertions/deletions", async () => { + const { levenshteinDistance } = await import("./mcp_server_core.cjs"); + expect(levenshteinDistance("cat", "ca")).toBe(1); + expect(levenshteinDistance("cat", "cats")).toBe(1); + }); + + it("should calculate correct distance for multiple changes", async () => { + const { levenshteinDistance } = await import("./mcp_server_core.cjs"); + expect(levenshteinDistance("cat", "dog")).toBe(3); + }); + }); + + describe("findSimilarTools", () => { + it("should find tools with typos", async () => { + const { findSimilarTools } = await import("./mcp_server_core.cjs"); + const tools = { + add_comment: {}, + add_name: {}, + missing_tool: {}, + }; + + const similar = findSimilarTools("add_comentt", tools, 3); + expect(similar.length).toBeGreaterThan(0); + expect(similar[0].name).toBe("add_comment"); + expect(similar[0].distance).toBeLessThanOrEqual(2); + }); + + it("should find tools with dashes normalized", async () => { + const { findSimilarTools } = await import("./mcp_server_core.cjs"); + const tools = { + dispatch_workflow: {}, + add_comment: {}, + }; + + const similar = findSimilarTools("dispatch-workflow", tools, 3); + expect(similar.length).toBeGreaterThan(0); + expect(similar[0].name).toBe("dispatch_workflow"); + expect(similar[0].distance).toBe(0); // Should match exactly after normalization + }); + + it("should return empty array for completely different names", async () => { + const { findSimilarTools } = await import("./mcp_server_core.cjs"); + const tools = { + short: {}, + }; + + const similar = findSimilarTools("verylongdifferenttoolname", tools, 3); + expect(similar.length).toBe(0); // Distance too large + }); + + it("should limit results to maxSuggestions", async () => { + const { findSimilarTools } = await import("./mcp_server_core.cjs"); + const tools = { + tool_a: {}, + tool_b: {}, + tool_c: {}, + tool_d: {}, + tool_e: {}, + }; + + const similar = findSimilarTools("tool_x", tools, 2); + expect(similar.length).toBeLessThanOrEqual(2); + }); + + it("should sort by distance (closest first)", async () => { + const { findSimilarTools } = await import("./mcp_server_core.cjs"); + const tools = { + add_name: {}, + add_comment: {}, + missing_tool: {}, + }; + + const similar = findSimilarTools("add_nam", tools, 3); + expect(similar.length).toBeGreaterThan(0); + expect(similar[0].name).toBe("add_name"); + expect(similar[0].distance).toBe(1); + }); + }); + + describe("tool not found error with suggestions", () => { + it("should suggest similar tools when tool is not found", async () => { + const { createServer, registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + const server = createServer({ name: "test-server", version: "1.0.0" }); + + // Register some tools + registerTool(server, { + name: "add_comment", + description: "Add comment", + inputSchema: { type: "object", properties: {} }, + handler: () => ({ content: [{ type: "text", text: "ok" }] }), + }); + registerTool(server, { + name: "add_name", + description: "Add name", + inputSchema: { type: "object", properties: {} }, + handler: () => ({ content: [{ type: "text", text: "ok" }] }), + }); + + const results = []; + server.writeMessage = msg => results.push(msg); + server.replyResult = (id, result) => results.push({ jsonrpc: "2.0", id, result }); + server.replyError = (id, code, message) => results.push({ jsonrpc: "2.0", id, error: { code, message } }); + + // Try to call a tool that doesn't exist but is similar + await handleMessage(server, { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "add_comentt", arguments: {} }, + }); + + expect(results).toHaveLength(1); + expect(results[0].error).toBeDefined(); + expect(results[0].error.message).toContain("not found"); + expect(results[0].error.message).toContain("Did you mean one of these"); + expect(results[0].error.message).toContain("add_comment"); + }); + + it("should not suggest tools if none are similar", async () => { + const { createServer, registerTool, handleMessage } = await import("./mcp_server_core.cjs"); + const server = createServer({ name: "test-server", version: "1.0.0" }); + + registerTool(server, { + name: "x", + description: "Test", + inputSchema: { type: "object", properties: {} }, + handler: () => ({ content: [{ type: "text", text: "ok" }] }), + }); + + const results = []; + server.writeMessage = msg => results.push(msg); + server.replyResult = (id, result) => results.push({ jsonrpc: "2.0", id, result }); + server.replyError = (id, code, message) => results.push({ jsonrpc: "2.0", id, error: { code, message } }); + + // Try to call a completely different tool + await handleMessage(server, { + jsonrpc: "2.0", + id: 1, + method: "tools/call", + params: { name: "completelydifferenttoolname", arguments: {} }, + }); + + expect(results).toHaveLength(1); + expect(results[0].error).toBeDefined(); + expect(results[0].error.message).toContain("not found"); + expect(results[0].error.message).not.toContain("Did you mean"); + }); + }); }); From fa396f4265eb5cc78797f7d31953b5b43efa2751 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 09:53:18 +0000 Subject: [PATCH 7/8] Update troubleshooting doc with similar tool suggestions feature Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- docs/troubleshooting-dispatch-workflow.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/troubleshooting-dispatch-workflow.md b/docs/troubleshooting-dispatch-workflow.md index a4e8346abc..9f37ffcd09 100644 --- a/docs/troubleshooting-dispatch-workflow.md +++ b/docs/troubleshooting-dispatch-workflow.md @@ -11,6 +11,19 @@ When using `dispatch-workflow` safe output configuration, the expected workflow - Agent reports missing_tool with message about workflow dispatch tools not being available - Only standard safe output tools appear (missing_tool, missing_data, noop, add_comment) +## New: Similar Tool Suggestions + +**As of PR #XXXX**, when an MCP tool is not found, the system now suggests similar available tools: + +``` +Tool 'dispatch-add-name' not found. Did you mean one of these: add_name, add_comment? +``` + +This helps identify: +- Typos in tool names +- Missing tool prefixes (e.g., forgot `safeoutputs-`) +- Available alternatives + ## Diagnostic Steps ### 1. Check MCP Server Logs @@ -164,6 +177,7 @@ After applying fixes, verify tools are registered: Registered tool: add_name ``` 4. Verify tools appear in agent's available tools +5. **New**: Check that similar tool suggestions appear when testing with typos ## Prevention @@ -186,7 +200,10 @@ To prevent this issue: gh aw compile --validate .github/workflows/my-workflow.md ``` +5. **Test tool names**: The system will now suggest similar tools if you make a typo + ## Related Issues - Issue #XXXX: dispatch_workflow tools not appearing in MCP tools - PR #XXXX: Enhanced diagnostic logging for dispatch_workflow registration +- PR #XXXX: Added similar tool suggestions when tool not found From 2d0c0b424f320edbfb95bc6c79b69f8afd440c78 Mon Sep 17 00:00:00 2001 From: Mara Nikola Kiefer <8320933+mnkiefer@users.noreply.github.com> Date: Sat, 31 Jan 2026 10:59:57 +0100 Subject: [PATCH 8/8] Delete docs/troubleshooting-dispatch-workflow.md --- docs/troubleshooting-dispatch-workflow.md | 209 ---------------------- 1 file changed, 209 deletions(-) delete mode 100644 docs/troubleshooting-dispatch-workflow.md diff --git a/docs/troubleshooting-dispatch-workflow.md b/docs/troubleshooting-dispatch-workflow.md deleted file mode 100644 index 9f37ffcd09..0000000000 --- a/docs/troubleshooting-dispatch-workflow.md +++ /dev/null @@ -1,209 +0,0 @@ -# Troubleshooting dispatch_workflow Tools Not Appearing - -## Problem - -When using `dispatch-workflow` safe output configuration, the expected workflow dispatch tools (e.g., `safeoutputs-my_workflow`) do not appear in the available MCP tools list. - -## Symptoms - -- Workflow has `dispatch-workflow` configured in frontmatter with a list of workflows -- Expected tools like `safeoutputs-add_name`, `safeoutputs-add_emojis` are missing -- Agent reports missing_tool with message about workflow dispatch tools not being available -- Only standard safe output tools appear (missing_tool, missing_data, noop, add_comment) - -## New: Similar Tool Suggestions - -**As of PR #XXXX**, when an MCP tool is not found, the system now suggests similar available tools: - -``` -Tool 'dispatch-add-name' not found. Did you mean one of these: add_name, add_comment? -``` - -This helps identify: -- Typos in tool names -- Missing tool prefixes (e.g., forgot `safeoutputs-`) -- Available alternatives - -## Diagnostic Steps - -### 1. Check MCP Server Logs - -With the enhanced logging (added in PR #XXXX), the MCP server logs will show detailed information about tool loading and registration. - -**Look for these log entries:** - -``` -Reading tools from file: /opt/gh-aw/safeoutputs/tools.json -Successfully parsed N tools from file - Found 3 dispatch_workflow tools: - - add_name (workflow: add-name) - - add_emojis (workflow: add-emojis) - - all_workflows (workflow: all-workflows) -``` - -If you see this, tools.json is correct. Continue to registration logs. - -``` -Found dispatch_workflow tool: add_name (_workflow_name: add-name) - dispatch_workflow config exists, registering tool -``` - -If you see this, the tool is being registered successfully. - -``` -Found dispatch_workflow tool: add_name (_workflow_name: add-name) - WARNING: dispatch_workflow config is missing or falsy - tool will NOT be registered - Config keys: missing_tool, noop, add_comment - config.dispatch_workflow value: undefined -``` - -If you see this WARNING, the config doesn't have dispatch_workflow, which is the root cause. - -### 2. Verify Workflow Configuration - -Check the workflow's frontmatter: - -```yaml ---- -on: issues -engine: copilot -safe-outputs: - dispatch-workflow: - workflows: - - add-name - - add-emojis - - all-workflows - max: 3 ---- -``` - -### 3. Check Compiled Configuration - -Extract the config from the compiled `.lock.yml` file: - -```bash -grep -A 5 "config.json" .github/workflows/my-workflow.lock.yml | grep dispatch_workflow -``` - -**Expected output:** -```json -{"dispatch_workflow":{"max":3,"workflows":["add-name","add-emojis","all-workflows"],"workflow_files":{...}},"missing_data":{},"missing_tool":{},"noop":{}} -``` - -**If dispatch_workflow is missing from config, the workflow needs recompilation.** - -### 4. Verify Target Workflows Exist - -For each workflow in the `workflows` list, verify: - -```bash -# Check if workflow files exist -ls -la .github/workflows/add-name.{md,yml,lock.yml} -ls -la .github/workflows/add-emojis.{md,yml,lock.yml} -ls -la .github/workflows/all-workflows.{md,yml,lock.yml} -``` - -**Requirements:** -- At least one of `.yml` or `.lock.yml` must exist -- If only `.md` exists, compile it first: `gh aw compile .github/workflows/add-name.md` -- The workflow must have `workflow_dispatch` in its `on:` triggers - -## Common Causes and Solutions - -### Cause 1: Workflow Not Recompiled - -**Symptom:** dispatch_workflow missing from config.json in lock file - -**Solution:** -```bash -gh aw compile .github/workflows/my-workflow.md -``` - -### Cause 2: Target Workflows Don't Exist - -**Symptom:** Tools generated with empty inputs, but validation failed during compilation - -**Solution:** -1. Create the target workflows in `.github/workflows/` -2. Ensure they have `workflow_dispatch` trigger: - ```yaml - on: - workflow_dispatch: - inputs: - param1: - description: "Parameter 1" - type: string - ``` -3. Compile them: `gh aw compile .github/workflows/target-workflow.md` -4. Recompile the dispatcher workflow - -### Cause 3: Target Workflows Missing workflow_dispatch - -**Symptom:** Compilation error: "workflow 'X' does not support workflow_dispatch trigger" - -**Solution:** -Add `workflow_dispatch` to the target workflow's triggers: -```yaml -on: - issues: # existing trigger - workflow_dispatch: # add this - inputs: - # optional: define inputs -``` - -### Cause 4: Config Not Loaded Properly - -**Symptom:** MCP server logs show config.dispatch_workflow is undefined - -**Solution:** -Check for file system issues: -```bash -# In the agent job, verify files exist -ls -la /opt/gh-aw/safeoutputs/ -cat /opt/gh-aw/safeoutputs/config.json | jq . -cat /opt/gh-aw/safeoutputs/tools.json | jq '. | map(select(._workflow_name))' -``` - -## Verification - -After applying fixes, verify tools are registered: - -1. Recompile the workflow: `gh aw compile .github/workflows/my-workflow.md` -2. Trigger the workflow -3. Check MCP server logs for successful registration: - ``` - Found dispatch_workflow tool: add_name (_workflow_name: add-name) - dispatch_workflow config exists, registering tool - Registered tool: add_name - ``` -4. Verify tools appear in agent's available tools -5. **New**: Check that similar tool suggestions appear when testing with typos - -## Prevention - -To prevent this issue: - -1. **Always compile after adding dispatch-workflow**: - ```bash - gh aw compile .github/workflows/my-workflow.md - ``` - -2. **Verify compilation succeeded**: - ```bash - grep dispatch_workflow .github/workflows/my-workflow.lock.yml - ``` - -3. **Create target workflows first** before adding them to dispatch-workflow list - -4. **Use validation** to catch issues early: - ```bash - gh aw compile --validate .github/workflows/my-workflow.md - ``` - -5. **Test tool names**: The system will now suggest similar tools if you make a typo - -## Related Issues - -- Issue #XXXX: dispatch_workflow tools not appearing in MCP tools -- PR #XXXX: Enhanced diagnostic logging for dispatch_workflow registration -- PR #XXXX: Added similar tool suggestions when tool not found