feat: self-improving agentic workflow — core MCP library#402
Conversation
Approved design for enabling optional self-improvement loops in Workflow applications. Three-layer architecture: engine MCP library + agent plugin guardrails + validation scenarios. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
11-phase plan covering workflow core MCP library, agent plugin guardrails/blackboard/safety, and 3 validation scenarios (85-87). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds Phase 12 with 14 tasks covering: MCPProvider interface, CLI equivalence tests, trigger runtime, handler registration, admin API, LSP hover, LSP-as-MCP tools, override CLI wiring, PR comment/API header overrides, tutorial doc, blackboard subscribe, additional bypass vectors, multi-agent review pattern, version bumps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract wfctl MCP tools as a Go library for direct invocation without HTTP or subprocess overhead. All 25+ tools available. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds ToolHandlerFunc type alias, toolHandlers map to Server struct, and collectToolHandlers() method to support in-process MCP invocation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add DiagnoseContent(), CompleteAt(), HoverAt() functions for in-process LSP feature invocation without an active client connection. Includes Diagnostic, CompletionResult, and HoverResult types with full test coverage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ugin New trigger type exposes pipelines as MCP tools. New handler type groups pipelines under named MCP servers. Registry module provides admin API for server/tool discovery and audit logging. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Address spec-reviewer feedback: - Add InProcessOption variadic opts to NewInProcessServer with WithInProcessPluginDir, WithInProcessRegistryDir, WithInProcessDocFile, WithInProcessAuditLog, WithInProcessEngine helpers - InProcessServer now holds pre-populated tools map from s.toolHandlers instead of re-fetching on every CallTool invocation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add snake_case JSON tags to Diagnostic, CompletionResult, and HoverResult so fields serialize correctly in MCP tool responses. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… validate Deterministic 3-word passphrases for guardrail overrides. CI validate runs full validation suite with immutability enforcement. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…enge Add explicit time.Time parameter for testability. Use binary.BigEndian uint16 word-index calculation (2 bytes per word) per spec. Update all callers to pass time.Now(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…missing secret - Add 8 missing words (catalog..celery) to prevent empty token components - ciResultsHash: use sha256 instead of truncated string concatenation - Warn to stderr when --override used but WFCTL_ADMIN_SECRET is unset Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…uides New docs: - docs/self-improvement.md — feature overview, architecture, deploy strategies, safety model - docs/guardrails-guide.md — hierarchical scopes, tool access globs, immutable sections, challenge tokens, command safety - docs/mcp-tools-reference.md — all 25+ wfctl tools, LSP tools, workflow-defined MCP tools, in-process vs external comparison - docs/self-improvement-tutorial.md — step-by-step from base app to autonomous self-improvement DOCUMENTATION.md additions: - agent.provider, agent.guardrails, mcp.registry module types - mcp_tool trigger type - mcp workflow handler type - step.agent_execute, step.blackboard_post, step.blackboard_read, step.self_improve_validate, step.self_improve_diff, step.self_improve_deploy, step.lsp_diagnose step types
…fig, add plugin notes - wfctl challenge-token generate → wfctl override generate "sha256:$HASH" - WORKFLOW_ADMIN_SECRET → WFCTL_ADMIN_SECRET throughout all docs - mcp_tool trigger field name: → tool_name: - mcp.registry: remove fabricated fields (storage, db_path, require_schema) - Add workflow-plugin-agent v0.8.0+ requirement notes to DOCUMENTATION.md, self-improvement.md, mcp-tools-reference.md, and tutorial Prerequisites Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…p routes: [] - audit_log → audit_tool_calls (real MCPRegistryConfig field) - Remove default: "status" from MCPToolParameter example (field doesn't exist) - Remove routes: [] from tutorial Step 1 (wfctl modernize anti-pattern) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces foundational building blocks for a self-improving/agentic workflow setup in the core engine: in-process MCP tool invocation, MCP-triggered pipelines, an MCP registry, an in-process LSP helper library, and challenge/override + CI validation plumbing in wfctl, along with extensive documentation.
Changes:
- Add an in-process MCP library (
NewInProcessServer) plus anMCPProviderinterface for direct tool invocation. - Add MCP-facing workflow surface area:
mcp_tooltrigger,mcpworkflow handler, and anmcp.registryregistry with JSON admin handlers. - Add challenge-response override tokens,
wfctl overridecommands, and a newwfctl ci validatesubcommand; document the self-improvement model and references.
Reviewed changes
Copilot reviewed 29 out of 30 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| validation/override_handlers.go | Adds helpers for extracting override tokens from PR comments, headers, and workflow_dispatch inputs. |
| validation/override_handlers_test.go | Unit tests for override token parsing helpers. |
| validation/challenge.go | Implements time-bucketed 3-word HMAC challenge token generation/verification. |
| validation/challenge_test.go | Unit tests for challenge token behavior (shape, expiry, determinism). |
| plugins/mcp/plugin.go | Registers MCP plugin types: mcp.registry, mcp_tool trigger, and mcp workflow handler. |
| module/trigger_mcp_tool.go | Implements mcp_tool trigger and runtime for mapping tool calls to pipeline execution. |
| module/trigger_mcp_tool_test.go | Unit tests for MCP tool config → schema conversion and runtime invocation. |
| module/mcp_registry.go | Adds MCP registry data model plus HTTP JSON handlers for servers/tools listing. |
| module/mcp_registry_test.go | Unit tests for registry operations and JSON handler output. |
| mcp/server.go | Collects tool handlers after registration for in-process dispatch. |
| mcp/provider_interface.go | Introduces an interface for in-process MCP tool invocation. |
| mcp/library.go | Adds NewInProcessServer wrapper for direct tool calls without HTTP/subprocess. |
| mcp/library_test.go | Tests tool listing and basic in-process tool invocation behavior. |
| lsp/library.go | Adds in-process LSP helpers: diagnostics, completions, and hover. |
| lsp/library_test.go | Unit tests for LSP in-process helpers. |
| handlers/mcp.go | Adds mcp workflow handler type scaffold and route→tool definition helper. |
| handlers/mcp_test.go | Unit tests for MCP handler config helpers and CanHandle. |
| DOCUMENTATION.md | Documents new workflow handler/trigger type entries and self-improvement section. |
| docs/self-improvement.md | New feature guide describing architecture, safety model, and quick start. |
| docs/self-improvement-tutorial.md | Step-by-step tutorial for enabling self-improvement in an app config. |
| docs/plans/2026-04-13-self-improving-agentic-workflow-plan.md | Implementation plan document added. |
| docs/plans/2026-04-13-self-improving-agentic-workflow-design.md | Design document added. |
| docs/mcp-tools-reference.md | New MCP tools reference document. |
| docs/guardrails-guide.md | New guardrails configuration guide. |
| cmd/wfctl/override.go | Adds `wfctl override generate |
| cmd/wfctl/override_test.go | Unit tests for override CLI subcommands. |
| cmd/wfctl/main.go | Registers the new override command. |
| cmd/wfctl/ci.go | Wires wfctl ci validate subcommand. |
| cmd/wfctl/ci_validate.go | Implements wfctl ci validate logic (schema + refs + immutability-related checks + override). |
| cmd/wfctl/ci_validate_test.go | Unit tests for wfctl ci validate. |
| switch *format { | ||
| case "json": | ||
| enc := json.NewEncoder(os.Stdout) | ||
| enc.SetIndent("", " ") | ||
| return enc.Encode(map[string]any{"results": results, "passed": allPassed}) | ||
| default: | ||
| for _, r := range results { | ||
| if r.Passed { | ||
| fmt.Printf(" PASS %s\n", r.File) | ||
| } else { | ||
| fmt.Printf(" FAIL %s\n", r.File) | ||
| for _, e := range r.Errors { | ||
| fmt.Printf(" %s\n", e) | ||
| } | ||
| } | ||
| } | ||
| if !allPassed { | ||
| return fmt.Errorf("%d file(s) failed ci validate", ciCountFailed(results)) | ||
| } | ||
| } |
There was a problem hiding this comment.
When --format=json is used, the command returns the JSON output encode result directly and never returns a non-nil error when allPassed is false. This makes CI runs incorrectly succeed on validation failures whenever JSON output is requested. After writing JSON, return an error (or set a failing exit code) when passed is false so the behavior matches the text output mode.
| return fmt.Errorf("immutable-sections: triggers section is empty") | ||
| } | ||
| default: | ||
| // Unknown section — silently skip. |
There was a problem hiding this comment.
checkImmutableSection silently ignores unknown section names. For a CI guardrail, this is risky because typos in --immutable-sections will silently disable the intended check. Consider returning an error for unknown section values (and/or listing allowed values in the usage text).
| // Unknown section — silently skip. | |
| return fmt.Errorf( | |
| "immutable-sections: unknown section %q (allowed: modules, workflows, pipelines, triggers)", | |
| section, | |
| ) |
| // Parse parameters from config. | ||
| var params []MCPToolParameter | ||
| if rawParams, ok := cfg["parameters"].([]any); ok { | ||
| for _, rp := range rawParams { | ||
| pm, ok := rp.(map[string]any) | ||
| if !ok { | ||
| continue | ||
| } | ||
| p := MCPToolParameter{} | ||
| if n, ok := pm["name"].(string); ok { | ||
| p.Name = n | ||
| } | ||
| if typ, ok := pm["type"].(string); ok { | ||
| p.Type = typ | ||
| } | ||
| if req, ok := pm["required"].(bool); ok { | ||
| p.Required = req | ||
| } | ||
| if desc, ok := pm["description"].(string); ok { | ||
| p.Description = desc | ||
| } | ||
| params = append(params, p) | ||
| } |
There was a problem hiding this comment.
Parameter parsing drops the enum field: MCPToolParameter.Enum is never populated from the trigger config, so ToToolDefinition() always emits an empty enum even when the YAML config supplies one. Parse enum (and validate its element types) when building MCPToolParameter.
| Description: description, | ||
| Parameters: params, | ||
| } | ||
|
|
There was a problem hiding this comment.
Registering a tool overwrites any existing entry in t.tools with the same tool_name, which can lead to ambiguous or nondeterministic routing if multiple pipelines declare the same MCP tool. Prefer rejecting duplicate tool registrations unless the mapping is identical (e.g., same workflowType) to make configuration errors fail fast.
| if _, exists := t.tools[toolName]; exists { | |
| return fmt.Errorf("mcp_tool trigger: duplicate tool_name %q is already registered", toolName) | |
| } |
| // ModuleFactories returns the factory for the mcp.registry module type. | ||
| func (p *Plugin) ModuleFactories() map[string]plugin.ModuleFactory { | ||
| return map[string]plugin.ModuleFactory{ | ||
| "mcp.registry": func(name string, _ map[string]any) modular.Module { | ||
| return newRegistryModule(name) | ||
| }, | ||
| } |
There was a problem hiding this comment.
The mcp.registry module factory ignores the provided config map, so options like log_on_init, expose_admin_api, and audit_tool_calls can never take effect. Parse and apply the module config (or remove the config type/fields if not supported yet) so YAML configuration matches runtime behavior.
| // ListTools returns the names of all registered tools. | ||
| ListTools() []string | ||
| // CallTool invokes the named tool with the given arguments. | ||
| // Returns the text content of the result as a string, or an error. |
There was a problem hiding this comment.
The interface comment says CallTool “Returns the text content of the result as a string”, but the signature returns (any, error) and callers may receive non-string results. Update the comment to match the actual behavior (or change the return type if a string result is intended).
| // Returns the text content of the result as a string, or an error. | |
| // Returns the tool result, which may be of any type, or an error. |
| #### `validate_config` | ||
|
|
||
| Validate a workflow YAML configuration string. | ||
|
|
||
| | Parameter | Type | Required | Description | | ||
| |-----------|------|----------|-------------| | ||
| | `config` | string | yes | YAML config content to validate | | ||
| | `skip_unknown_types` | bool | no | Skip unknown module/step type errors | | ||
| | `strict` | bool | no | Fail on any warning | | ||
|
|
||
| **Example:** | ||
| ```json | ||
| { | ||
| "tool": "validate_config", | ||
| "arguments": { | ||
| "config": "modules:\n - name: server\n type: http.server\n config:\n address: ':8080'\n" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| **Returns:** Validation result with errors and warnings. | ||
|
|
||
| --- | ||
|
|
||
| #### `template_validate_config` | ||
|
|
||
| Validate a workflow config that uses template expressions. | ||
|
|
||
| | Parameter | Type | Required | Description | | ||
| |-----------|------|----------|-------------| | ||
| | `config` | string | yes | YAML config with template expressions | | ||
| | `context` | object | no | Template context variables for expression evaluation | | ||
|
|
||
| --- | ||
|
|
||
| #### `inspect_config` | ||
|
|
||
| Inspect a config and get a structured summary. | ||
|
|
||
| | Parameter | Type | Required | Description | | ||
| |-----------|------|----------|-------------| | ||
| | `config` | string | yes | YAML config content | | ||
|
|
||
| **Returns:** Structured summary with module names/types, workflow definitions, pipeline triggers, step counts. | ||
|
|
||
| --- | ||
|
|
||
| #### `diff_configs` | ||
|
|
||
| Compute a semantic diff between two workflow configs. | ||
|
|
||
| | Parameter | Type | Required | Description | | ||
| |-----------|------|----------|-------------| | ||
| | `base` | string | yes | Base config YAML | | ||
| | `proposed` | string | yes | Proposed config YAML | | ||
|
|
There was a problem hiding this comment.
This reference lists MCP tool parameter names that don’t match the actual server implementation (e.g., validate_config/inspect_config use yaml_content, and diff_configs uses old_yaml/new_yaml). Please update the docs to reflect the real argument keys so consumers can call tools successfully.
| | `mcp.registry` | Registry and audit log for MCP tool registrations and invocations | agent | | ||
|
|
There was a problem hiding this comment.
The module type table attributes mcp.registry to workflow-plugin-agent and says it’s not part of the core engine, but this PR adds mcp.registry via the core workflow-plugin-mcp plugin. Please fix the plugin attribution/requirement note so it matches the implementation (or adjust the implementation if the intent is for agent plugin to own it).
| | `mcp.registry` | Registry and audit log for MCP tool registrations and invocations | agent | | |
| ### MCP | |
| > **Requires workflow-plugin-mcp.** Types in this section are provided by the MCP plugin. | |
| > Install with: `wfctl plugin install workflow-plugin-mcp` | |
| | Type | Description | Plugin | | |
| |------|-------------|--------| | |
| | `mcp.registry` | Registry and audit log for MCP tool registrations and invocations | mcp | |
| M[Scenario 69<br/>Self-Improving API + Custom Go] --> N[Scenario 70<br/>Self-Extending MCP Tooling] | ||
| N --> O[Scenario 71<br/>Autonomous Agile Agent] |
There was a problem hiding this comment.
The design doc’s mermaid diagram references scenarios 69–71, but the PR plan/metadata references scenarios 85–87. Align the scenario numbers to avoid confusion when following the design/plan documents.
| M[Scenario 69<br/>Self-Improving API + Custom Go] --> N[Scenario 70<br/>Self-Extending MCP Tooling] | |
| N --> O[Scenario 71<br/>Autonomous Agile Agent] | |
| M[Scenario 85<br/>Self-Improving API + Custom Go] --> N[Scenario 86<br/>Self-Extending MCP Tooling] | |
| N --> O[Scenario 87<br/>Autonomous Agile Agent] |
| // ConfigureWorkflow sets up the MCP workflow from configuration. | ||
| func (h *MCPWorkflowHandler) ConfigureWorkflow(_ modular.Application, workflowConfig any) error { | ||
| _, ok := workflowConfig.(map[string]any) | ||
| if !ok { | ||
| return fmt.Errorf("invalid MCP workflow configuration format") |
There was a problem hiding this comment.
ConfigureWorkflow currently only type-checks that the config is a map and otherwise accepts any content. This means invalid/misspelled keys (e.g., missing server_name/routes or wrong route shapes) won’t be caught at startup. Consider parsing into MCPHandlerConfig and validating required fields so configuration errors fail fast and tool routing can be set up reliably.
| // ConfigureWorkflow sets up the MCP workflow from configuration. | |
| func (h *MCPWorkflowHandler) ConfigureWorkflow(_ modular.Application, workflowConfig any) error { | |
| _, ok := workflowConfig.(map[string]any) | |
| if !ok { | |
| return fmt.Errorf("invalid MCP workflow configuration format") | |
| func parseMCPWorkflowConfig(workflowConfig any) (*MCPHandlerConfig, error) { | |
| rawConfig, ok := workflowConfig.(map[string]any) | |
| if !ok { | |
| return nil, fmt.Errorf("invalid MCP workflow configuration format") | |
| } | |
| for key := range rawConfig { | |
| switch key { | |
| case "server_name", "log_tool_calls", "routes": | |
| default: | |
| return nil, fmt.Errorf("invalid MCP workflow configuration: unknown key %q", key) | |
| } | |
| } | |
| serverNameValue, ok := rawConfig["server_name"] | |
| if !ok { | |
| return nil, fmt.Errorf("invalid MCP workflow configuration: missing required key %q", "server_name") | |
| } | |
| serverName, ok := serverNameValue.(string) | |
| if !ok || strings.TrimSpace(serverName) == "" { | |
| return nil, fmt.Errorf("invalid MCP workflow configuration: %q must be a non-empty string", "server_name") | |
| } | |
| cfg := &MCPHandlerConfig{ | |
| ServerName: serverName, | |
| Routes: make(map[string]MCPHandlerRoute), | |
| } | |
| if logToolCallsValue, exists := rawConfig["log_tool_calls"]; exists { | |
| logToolCalls, ok := logToolCallsValue.(bool) | |
| if !ok { | |
| return nil, fmt.Errorf("invalid MCP workflow configuration: %q must be a boolean", "log_tool_calls") | |
| } | |
| cfg.LogToolCalls = logToolCalls | |
| } | |
| routesValue, ok := rawConfig["routes"] | |
| if !ok { | |
| return nil, fmt.Errorf("invalid MCP workflow configuration: missing required key %q", "routes") | |
| } | |
| rawRoutes, ok := routesValue.(map[string]any) | |
| if !ok { | |
| return nil, fmt.Errorf("invalid MCP workflow configuration: %q must be a map of route definitions", "routes") | |
| } | |
| if len(rawRoutes) == 0 { | |
| return nil, fmt.Errorf("invalid MCP workflow configuration: %q must define at least one route", "routes") | |
| } | |
| for toolName, routeValue := range rawRoutes { | |
| if strings.TrimSpace(toolName) == "" { | |
| return nil, fmt.Errorf("invalid MCP workflow configuration: route name must be non-empty") | |
| } | |
| rawRoute, ok := routeValue.(map[string]any) | |
| if !ok { | |
| return nil, fmt.Errorf("invalid MCP workflow configuration: route %q must be a map", toolName) | |
| } | |
| for key := range rawRoute { | |
| switch key { | |
| case "pipeline", "description": | |
| default: | |
| return nil, fmt.Errorf("invalid MCP workflow configuration: route %q has unknown key %q", toolName, key) | |
| } | |
| } | |
| pipelineValue, ok := rawRoute["pipeline"] | |
| if !ok { | |
| return nil, fmt.Errorf("invalid MCP workflow configuration: route %q is missing required key %q", toolName, "pipeline") | |
| } | |
| pipeline, ok := pipelineValue.(string) | |
| if !ok || strings.TrimSpace(pipeline) == "" { | |
| return nil, fmt.Errorf("invalid MCP workflow configuration: route %q field %q must be a non-empty string", toolName, "pipeline") | |
| } | |
| route := MCPHandlerRoute{ | |
| Pipeline: pipeline, | |
| } | |
| if descriptionValue, exists := rawRoute["description"]; exists { | |
| description, ok := descriptionValue.(string) | |
| if !ok { | |
| return nil, fmt.Errorf("invalid MCP workflow configuration: route %q field %q must be a string", toolName, "description") | |
| } | |
| route.Description = description | |
| } | |
| cfg.Routes[toolName] = route | |
| } | |
| return cfg, nil | |
| } | |
| // ConfigureWorkflow sets up the MCP workflow from configuration. | |
| func (h *MCPWorkflowHandler) ConfigureWorkflow(_ modular.Application, workflowConfig any) error { | |
| _, err := parseMCPWorkflowConfig(workflowConfig) | |
| if err != nil { | |
| return err |
⏱ Benchmark Results✅ No significant performance regressions detected. benchstat comparison (baseline → PR)
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| // wordCount is the number of words in bip39Subset. | ||
| const wordCount = 256 | ||
|
|
||
| // bip39Subset is a fixed 256-word subset of the BIP-39 English word list used | ||
| // for generating readable challenge tokens. Words are chosen to be short, | ||
| // unambiguous, and easy to type. | ||
| var bip39Subset = [wordCount]string{ | ||
| "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", | ||
| "access", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", | ||
| "act", "action", "actor", "actual", "adapt", "add", "addict", "address", | ||
| "adjust", "admit", "adult", "advance", "advice", "aerobic", "afford", "afraid", | ||
| "again", "agent", "agree", "ahead", "aim", "airport", "aisle", "alarm", | ||
| "album", "alcohol", "alert", "alien", "all", "alley", "allow", "almost", | ||
| "alone", "alpha", "already", "alter", "always", "amateur", "amazing", "among", | ||
| "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry", | ||
| "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique", | ||
| "anxiety", "apart", "apple", "approve", "april", "arch", "arctic", "argue", | ||
| "arm", "armed", "armor", "army", "around", "arrange", "arrest", "arrive", | ||
| "arrow", "art", "article", "artist", "aspect", "assault", "asset", "assist", | ||
| "assume", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", | ||
| "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", | ||
| "awake", "aware", "away", "awesome", "awful", "awkward", "axis", "baby", | ||
| "balance", "bamboo", "banana", "banner", "barely", "bargain", "barrel", "base", | ||
| "basic", "basket", "battle", "beach", "beauty", "because", "become", "beef", | ||
| "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", | ||
| "benefit", "best", "betray", "better", "between", "beyond", "bicycle", "bind", | ||
| "biology", "bird", "birth", "bitter", "black", "blade", "blame", "blanket", | ||
| "blast", "bleak", "bless", "blind", "blood", "blossom", "blouse", "blue", | ||
| "blur", "blush", "board", "boat", "body", "boil", "bomb", "bone", | ||
| "bonus", "book", "boost", "border", "boring", "borrow", "boss", "bottom", | ||
| "bounce", "boy", "brain", "brand", "brave", "breeze", "brick", "bridge", | ||
| "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", | ||
| "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", | ||
| "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", | ||
| "business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable", | ||
| "cactus", "cage", "cake", "call", "calm", "camera", "camp", "canal", | ||
| "cancel", "candy", "cannon", "canvas", "canyon", "capable", "capital", "captain", | ||
| "carbon", "card", "cargo", "carpet", "carry", "cart", "case", "castle", | ||
| "catalog", "catch", "category", "cause", "caution", "cave", "ceiling", "celery", | ||
| } |
There was a problem hiding this comment.
bip39Subset is declared as a 256-word array (wordCount = 256), but only ~50 words are initialized here, leaving the remaining entries as empty strings. computeToken can therefore generate tokens with empty word segments, breaking the intended 3-word passphrase format and making tests/token verification flaky. Populate all 256 words or switch to a slice and compute modulo len(bip39Subset) to guarantee non-empty tokens.
| // HandleToolCall executes the bound pipeline with the given tool arguments. | ||
| func (r *MCPToolTriggerRuntime) HandleToolCall(ctx context.Context, args map[string]any) (any, error) { | ||
| if r.executor == nil { | ||
| return nil, fmt.Errorf("mcp_tool: no pipeline executor available") | ||
| } | ||
| result, err := r.executor.ExecutePipeline(ctx, r.pipeline, args) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return result, nil | ||
| } |
There was a problem hiding this comment.
workflowType injected by the engine uses the pipeline:<name> prefix (see engine.wrapPipelineTriggerConfig). HandleToolCall passes this value directly to PipelineExecutor.ExecutePipeline, but ExecutePipeline expects the bare pipeline name (it prefixes with pipeline: internally). This will cause tool calls to fail to locate the pipeline. Trim the pipeline: prefix before calling ExecutePipeline, or store the bare pipeline name in the runtime.
| runtime := NewMCPToolTriggerRuntime(cfg, "pipeline:analyze-logs", executor) | ||
|
|
||
| args := map[string]any{"timeframe": "1h"} | ||
| result, err := runtime.HandleToolCall(context.Background(), args) | ||
| if err != nil { | ||
| t.Fatalf("unexpected error: %v", err) | ||
| } | ||
| if !called { | ||
| t.Error("expected executor to be called") | ||
| } | ||
| if capturedPipeline != "pipeline:analyze-logs" { | ||
| t.Errorf("expected pipeline:analyze-logs, got %s", capturedPipeline) | ||
| } |
There was a problem hiding this comment.
This test asserts that ExecutePipeline is called with a pipeline:<name>-prefixed identifier, but interfaces.PipelineExecutor.ExecutePipeline expects the bare pipeline name (the engine adds the pipeline: prefix internally when dispatching). Update the test to expect the trimmed pipeline name and add coverage for the prefix-trimming behavior in the trigger runtime.
| // parseRegistryConfig converts a raw config map to MCPRegistryConfig. | ||
| func parseRegistryConfig(cfg map[string]any) module.MCPRegistryConfig { | ||
| if cfg == nil { | ||
| return module.MCPRegistryConfig{} | ||
| } | ||
| var out module.MCPRegistryConfig | ||
| if v, ok := cfg["log_on_init"].(bool); ok { | ||
| out.LogOnInit = v | ||
| } | ||
| if v, ok := cfg["expose_admin_api"].(bool); ok { | ||
| out.ExposeAdminAPI = v | ||
| } | ||
| if v, ok := cfg["audit_tool_calls"].(bool); ok { | ||
| out.AuditToolCalls = v | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| // TriggerFactories returns trigger constructors for the mcp_tool trigger type. | ||
| func (p *Plugin) TriggerFactories() map[string]plugin.TriggerFactory { | ||
| return map[string]plugin.TriggerFactory{ | ||
| "mcp_tool": func() any { | ||
| return module.NewMCPToolTrigger() | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // WorkflowHandlers returns workflow handler factories for the mcp workflow type. | ||
| func (p *Plugin) WorkflowHandlers() map[string]plugin.WorkflowHandlerFactory { | ||
| return map[string]plugin.WorkflowHandlerFactory{ | ||
| "mcp": func() any { | ||
| return handlers.NewMCPWorkflowHandler() | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| // registryModule wraps MCPRegistry as a modular.Module. | ||
| type registryModule struct { | ||
| name string | ||
| cfg module.MCPRegistryConfig | ||
| registry *module.MCPRegistry | ||
| } | ||
|
|
||
| func newRegistryModule(name string, cfg module.MCPRegistryConfig) *registryModule { | ||
| return ®istryModule{ | ||
| name: name, | ||
| cfg: cfg, | ||
| registry: module.NewMCPRegistry(), | ||
| } | ||
| } | ||
|
|
||
| func (m *registryModule) Name() string { return m.name } | ||
| func (m *registryModule) Dependencies() []string { return nil } | ||
| func (m *registryModule) Init(_ modular.Application) error { | ||
| if m.cfg.LogOnInit { | ||
| m.registry.Logger().Info("mcp.registry module initialized", "name", m.name) | ||
| } | ||
| return nil | ||
| } |
There was a problem hiding this comment.
MCPRegistryConfig fields expose_admin_api and audit_tool_calls are parsed, but the module wrapper never uses them (only LogOnInit is referenced). As-is, configuring expose_admin_api: true won't actually register /admin/mcp/servers or /admin/mcp/tools, and audit_tool_calls doesn't affect logging. Either wire these behaviors via a plugin wiring hook / router registration and tool-call instrumentation, or remove the config options to avoid a misleading API.
| // ConfigureWorkflow sets up the MCP workflow from configuration. | ||
| func (h *MCPWorkflowHandler) ConfigureWorkflow(_ modular.Application, workflowConfig any) error { | ||
| _, ok := workflowConfig.(map[string]any) | ||
| if !ok { | ||
| return fmt.Errorf("invalid MCP workflow configuration format") | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // ExecuteWorkflow executes an MCP tool call by routing the action to a pipeline. | ||
| func (h *MCPWorkflowHandler) ExecuteWorkflow(_ context.Context, _ string, _ string, _ map[string]any) (map[string]any, error) { | ||
| // MCP tool calls are dispatched through the MCPToolTrigger, not this handler. | ||
| // This handler's ConfigureWorkflow is invoked at startup to validate the workflow config. | ||
| return map[string]any{}, nil | ||
| } |
There was a problem hiding this comment.
The mcp workflow handler currently doesn't parse/validate server_name/routes and doesn't actually expose the configured pipelines as MCP tools (it returns an empty result and states tool calls are dispatched elsewhere). This makes type: mcp configurations appear to work at startup but provide no functional behavior. Either implement route→pipeline registration (e.g., by configuring mcp_tool mappings or integrating with an MCP server/registry) or remove/rename the handler to avoid a non-functional workflow type.
| > **Requires workflow-plugin-agent v0.8.0+.** The `mcp_tool` trigger, `mcp.registry` | ||
| > module, and all `step.agent_execute` / `step.self_improve_*` / `step.blackboard_*` | ||
| > types are provided by this plugin, not the workflow core engine. |
There was a problem hiding this comment.
This section says mcp_tool / mcp.registry are provided by workflow-plugin-agent, but in this PR they are introduced via workflow-plugin-mcp (and also added to plugins/all). Please update the requirement note to reference the MCP plugin, and keep the agent plugin requirement limited to agent/blackboard/self_improve step types.
| > **Requires workflow-plugin-agent v0.8.0+.** The `mcp_tool` trigger, `mcp.registry` | |
| > module, and all `step.agent_execute` / `step.self_improve_*` / `step.blackboard_*` | |
| > types are provided by this plugin, not the workflow core engine. | |
| > **Requires workflow-plugin-mcp.** The `mcp_tool` trigger and `mcp.registry` | |
| > module are provided by this plugin, not the workflow core engine. | |
| > Install with: `wfctl plugin install workflow-plugin-mcp` | |
| > **Agent step types require workflow-plugin-agent v0.8.0+.** The | |
| > `step.agent_execute`, `step.self_improve_*`, and `step.blackboard_*` types are | |
| > provided by this plugin, not the workflow core engine. |
| func (r *MCPToolTriggerRuntime) HandleToolCall(ctx context.Context, args map[string]any) (any, error) { | ||
| if r.executor == nil { | ||
| return nil, fmt.Errorf("mcp_tool: no pipeline executor available") | ||
| } | ||
| result, err := r.executor.ExecutePipeline(ctx, r.pipeline, args) | ||
| if err != nil { |
There was a problem hiding this comment.
interfaces.PipelineExecutor.ExecutePipeline expects the pipeline name (e.g. "analyze-logs"), but HandleToolCall passes r.pipeline which is currently populated from workflowType (e.g. "pipeline:analyze-logs"). With the default engine executor (StdEngine.ExecutePipeline), this will become pipeline:pipeline:analyze-logs and fail to dispatch. Store only the bare pipeline name in the runtime (strip the pipeline: prefix) or call TriggerWorkflow directly with the full workflowType.
| result, callErr := handler(ctx, req) | ||
| if p.auditLogger != nil { | ||
| p.auditLogger.Info("mcp tool call", | ||
| "tool", name, | ||
| "error", callErr, | ||
| ) | ||
| } | ||
| if callErr != nil { | ||
| return nil, callErr | ||
| } | ||
|
|
||
| for _, c := range result.Content { | ||
| if tc, ok := c.(mcp.TextContent); ok { | ||
| return tc.Text, nil | ||
| } | ||
| } | ||
| return result, nil |
There was a problem hiding this comment.
CallTool ignores result.IsError. Many MCP handlers report tool failures by returning a CallToolResult with IsError=true and error=nil; with the current implementation those failures are treated as success and returned as plain text. Consider converting result.IsError into a Go error (e.g., using the text content as the error message) so callers can reliably detect tool failures.
| argsJSON, err := json.Marshal(args) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("marshal args: %w", err) | ||
| } | ||
|
|
||
| var req mcp.CallToolRequest | ||
| req.Params.Name = name | ||
| req.Params.Arguments = make(map[string]any) | ||
| if err := json.Unmarshal(argsJSON, &req.Params.Arguments); err != nil { | ||
| return nil, fmt.Errorf("unmarshal args: %w", err) | ||
| } |
There was a problem hiding this comment.
CallTool marshals args to JSON and unmarshals into req.Params.Arguments. If args is nil, this becomes null and json.Unmarshal into a map fails. Either treat nil as an empty argument object, or skip the marshal/unmarshal step and assign args directly (copying the map if you need isolation).
| if v, ok := cfg["log_on_init"].(bool); ok { | ||
| out.LogOnInit = v | ||
| } | ||
| if v, ok := cfg["expose_admin_api"].(bool); ok { | ||
| out.ExposeAdminAPI = v | ||
| } | ||
| if v, ok := cfg["audit_tool_calls"].(bool); ok { | ||
| out.AuditToolCalls = v | ||
| } | ||
| return out |
There was a problem hiding this comment.
parseRegistryConfig populates ExposeAdminAPI and AuditToolCalls, but nothing in this plugin/module currently uses those fields (only LogOnInit is referenced). Either wire these options into actual HTTP route registration / audit logging, or remove them to avoid shipping config knobs that are no-ops.
| func TestRunCIValidate_ImmutableSections(t *testing.T) { | ||
| f := writeTempConfig(t, "modules:\n - name: server\n type: http.server\n config:\n port: 8080\n") | ||
| // Require workflows section — should fail since it's missing | ||
| err := runCIValidate([]string{"--immutable-sections=workflows", f}) | ||
| if err == nil { | ||
| t.Fatal("expected failure when required workflows section is missing") | ||
| } |
There was a problem hiding this comment.
This test intends to validate the --immutable-sections behavior, but the YAML uses http.server config key port, which fails schema validation first (the module requires address). Use a schema-valid config here so the failure is attributable to the immutable-sections check and the test remains robust.
| > **Requires workflow-plugin-agent v0.8.0+.** The `mcp_tool` trigger, `mcp.registry` | ||
| > module, and all `step.agent_execute` / `step.self_improve_*` / `step.blackboard_*` | ||
| > types are provided by this plugin, not the workflow core engine. | ||
| > Install with: `wfctl plugin install workflow-plugin-agent` |
There was a problem hiding this comment.
This section says mcp_tool trigger and mcp.registry are provided by workflow-plugin-agent, but in this PR they’re introduced by workflow-plugin-mcp (see plugins/mcp/plugin.go). The requirement note should be updated so readers install the correct plugin.
| > **Requires workflow-plugin-agent v0.8.0+.** The `mcp_tool` trigger, `mcp.registry` | |
| > module, and all `step.agent_execute` / `step.self_improve_*` / `step.blackboard_*` | |
| > types are provided by this plugin, not the workflow core engine. | |
| > Install with: `wfctl plugin install workflow-plugin-agent` | |
| > **Requires workflow-plugin-mcp v0.8.0+.** The `mcp_tool` trigger, `mcp` | |
| > workflow handler, and `mcp.registry` module are provided by this plugin, not | |
| > the workflow core engine. | |
| > Install with: `wfctl plugin install workflow-plugin-mcp` | |
| > | |
| > Agent-related step types such as `step.agent_execute`, | |
| > `step.self_improve_*`, and `step.blackboard_*` are provided separately by | |
| > `workflow-plugin-agent`. |
| ) | ||
|
|
||
| result, err := mcp.CallTool(ctx, "validate_config", map[string]any{ | ||
| "config": proposedYAML, |
There was a problem hiding this comment.
The in-process usage example calls validate_config with argument key config, but the tool definition uses yaml_content. As written, the example will produce an MCP tool error (or silently fail if the caller ignores result.IsError). Update the snippet to pass yaml_content.
| "config": proposedYAML, | |
| "yaml_content": proposedYAML, |
| // ConfigureWorkflow sets up the MCP workflow from configuration. | ||
| func (h *MCPWorkflowHandler) ConfigureWorkflow(_ modular.Application, workflowConfig any) error { | ||
| _, ok := workflowConfig.(map[string]any) | ||
| if !ok { | ||
| return fmt.Errorf("invalid MCP workflow configuration format") | ||
| } | ||
| return nil |
There was a problem hiding this comment.
ConfigureWorkflow currently only type-checks workflowConfig and otherwise accepts any map, without parsing/validating expected fields (e.g., server_name, routes) or registering any services. This makes misconfigurations easy to miss at startup compared to other handlers (e.g., CLIWorkflowHandler parses and validates its config). Consider decoding into MCPHandlerConfig and validating required fields/route entries here (or remove this handler if it’s intentionally a no-op).
Summary
NewInProcessServer) withMCPProviderinterface for direct tool invocation without HTTP/subprocessmcp_tooltrigger type andmcpworkflow handler type for exposing pipelines as MCP toolsmcp.registrymodule with admin API (/admin/mcp/servers,/admin/mcp/tools) for audit/discoveryDiagnoseContent,CompleteAt,HoverAt) with MCP tool wiringwfctl ci validatesubcommand with immutability enforcement and override supportDesign
See:
docs/plans/2026-04-13-self-improving-agentic-workflow-design.mdImplementation Plan
See:
docs/plans/2026-04-13-self-improving-agentic-workflow-plan.mdRelated PRs
🤖 Generated with Claude Code