diff --git a/pkg/vmcp/composer/elicitation_integration_test.go b/pkg/vmcp/composer/elicitation_integration_test.go index d4edd40c05..a1a431e9ea 100644 --- a/pkg/vmcp/composer/elicitation_integration_test.go +++ b/pkg/vmcp/composer/elicitation_integration_test.go @@ -527,3 +527,61 @@ func TestDefaultElicitationHandler_SDKErrorHandling(t *testing.T) { }) } } + +func TestWorkflowEngine_ElicitationMessageTemplateExpansion(t *testing.T) { + t.Parallel() + + te := newTestEngine(t) + mockSDK := mocks.NewMockSDKElicitationRequester(te.Ctrl) + + // Capture the elicitation request to verify the message was expanded + var capturedReq mcp.ElicitationRequest + mockSDK.EXPECT().RequestElicitation(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, req mcp.ElicitationRequest) (*mcp.ElicitationResult, error) { + capturedReq = req + return &mcp.ElicitationResult{ + ElicitationResponse: mcp.ElicitationResponse{ + Action: mcp.ElicitationResponseActionAccept, + Content: map[string]any{"confirmed": true}, + }, + }, nil + }, + ) + + handler := NewDefaultElicitationHandler(mockSDK) + stateStore := NewInMemoryStateStore(1*time.Minute, 1*time.Hour) + engine := NewWorkflowEngine(te.Router, te.Backend, handler, stateStore, nil, nil) + + workflow := &WorkflowDefinition{ + Name: "template-elicit", + Steps: []WorkflowStep{ + { + ID: "ask", + Type: StepTypeElicitation, + Elicitation: &ElicitationConfig{ + Message: "Deploy {{.params.repo}} to {{.params.env}}?", + Schema: map[string]any{ + "type": "object", + "properties": map[string]any{ + "confirmed": map[string]any{"type": "boolean"}, + }, + }, + Timeout: 1 * time.Minute, + }, + }, + }, + } + + params := map[string]any{ + "repo": "acme/widget", + "env": "production", + } + + result, err := engine.ExecuteWorkflow(context.Background(), workflow, params) + require.NoError(t, err) + assert.Equal(t, WorkflowStatusCompleted, result.Status) + + // Verify that template placeholders were expanded in the message + assert.Equal(t, "Deploy acme/widget to production?", capturedReq.Params.Message, + "elicitation message should have template expressions expanded") +} diff --git a/pkg/vmcp/composer/workflow_engine.go b/pkg/vmcp/composer/workflow_engine.go index ecba1b7b10..3603d24e1e 100644 --- a/pkg/vmcp/composer/workflow_engine.go +++ b/pkg/vmcp/composer/workflow_engine.go @@ -634,6 +634,21 @@ func (e *workflowEngine) executeElicitationStep( return err } + // Expand template expressions in elicitation message (e.g. {{.params.owner}}) + if step.Elicitation.Message != "" { + wrapper := map[string]any{"message": step.Elicitation.Message} + expanded, expandErr := e.templateExpander.Expand(ctx, wrapper, workflowCtx) + if expandErr != nil { + err := fmt.Errorf("%w: failed to expand elicitation message for step %s: %v", + ErrTemplateExpansion, step.ID, expandErr) + workflowCtx.RecordStepFailure(step.ID, err) + return err + } + if msg, ok := expanded["message"].(string); ok { + step.Elicitation.Message = msg + } + } + // Request elicitation (synchronous - blocks until response or timeout) // Per MCP 2025-06-18: SDK handles JSON-RPC ID correlation internally response, err := e.elicitationHandler.RequestElicitation(ctx, workflowCtx.WorkflowID, step.ID, step.Elicitation)