Add send_duo_session_input MCP tool
What does this MR do and why?
Adds the send_duo_session_input MCP tool: answer a Duo Agent Platform session that's waiting — approve or reject the step, or reply to its question. Part of #617171 (closed) and &21395 (closed).
Route-backed: route_setting :mcp on POST /ai/duo_workflows/workflows/:workflow_id/resume, plus a Base::AggregatedService, same as save_merge_request/semantic_search. Adds a tool-selection description, a real required list, and a message pointing at get_duo_session instead of a bare 403. No ApiTool changes; !254979 (merged) builds start_duo_session (#607619 (closed)) the same way, closing #627559 (closed).
- Only
input_requiredis resumable (Workflow#resumable?); other statuses get refused with a clear message. @bastirehm confirmed the CI executor can emitrequire_plan_approvalandrequire_tool_call_approval, so answering a session inplan_approval_requiredortool_call_approval_requiredneeds a follow-up. That means this MR does not close #617171 (closed) on its own.
References
Related to #617171 (closed) (not closed by this MR, see the note above), #607619 (closed), !254979 (merged) and #627559 (closed).
Screenshots or screen recordings
| Before | After |
|---|---|
How to set up and validate locally
- There's no checkbox for the
mcpscope in the UI yet, so make the token inrails console:
PersonalAccessToken.create!(
user: User.find_by_username('root'), name: 'mcp', scopes: [:mcp],
expires_at: 7.days.from_now,
organization: Organizations::Organization.default_organization
).token- Confirm the tool is listed and the route tool underneath it is hidden:
curl -s -X POST http://gdk.test:3000/api/v4/mcp \
-H "PRIVATE-TOKEN: $TOKEN" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}' \
| jq '.result.tools[].name' | grep duosend_duo_session_input should show up
- Unknown session id, the cheapest end-to-end check:
curl -s -X POST http://gdk.test:3000/api/v4/mcp \
-H "PRIVATE-TOKEN: $TOKEN" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"send_duo_session_input",
"arguments":{"workflow_id":99999999,"human_approval":true}}}' | jq '.result'What I got: text "404 Workflow Not Found" and structuredContent.error of {"message": "404 Not found"}.
- A session that isn't resumable, which is the error message this whole tool exists for. Make one first, in
rails console:
Ai::DuoWorkflows::Workflow.create!(
user: User.find_by_username('root'),
project: Project.find_by_full_path('gitlab-duo/test'),
goal: 'test', workflow_definition: 'software_development', status: 6
).idStatus 6 is input_required, but with no finished workload pipeline Workflow#resumable? is false, so the endpoint answers 403.
Then call it (964 was the id I got back):
curl -s -X POST http://gdk.test:3000/api/v4/mcp \
-H "PRIVATE-TOKEN: $TOKEN" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"send_duo_session_input",
"arguments":{"workflow_id":964,"human_approval":true,"human_message":"ga door"}}}' \
| jq '.result.content[0].text'What I got:
"Session 964 is not waiting for input, or you cannot resume it. Call get_duo_session with workflow_id=964 to check its status; only sessions with status input_required whose last CI job has finished accept input."The bare 403 Forbidden is still there in structuredContent.error, so nothing is hidden from a caller that actually wants it.
- Happy path:
w = Ai::DuoWorkflows::Workflow.find(972)
w.update_column(:status, 6)
[w.reload.status_name, w.resumable?]
# => ["input_required", true]Then send the input:
curl -s -X POST http://gdk.test:3000/api/v4/mcp \
-H "PRIVATE-TOKEN: $TOKEN" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"tools/call","id":4,"params":{"name":"send_duo_session_input",
"arguments":{"workflow_id":972,"human_approval":true,"human_message":"looks good"}}}' \
| jq '.result.structuredContent'What I got:
{"workflow_id": 972, "status": "input_required", "workload_id": 501, "poll_after_seconds": 120}The workload_id differs from the one the session already had, so a second duo_workflow pipeline was queued. The exact number varies per run; the count going up is the check:
Ai::DuoWorkflows::Workflow.find(972).workflows_workloads.count
# => 2MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.