Skip to content

Commit 6bd5399

Browse files
intel352claude
andcommitted
feat(mcp): add modernize + registry_search tools, remove standalone binary
- Extract modernize rules into shared `modernize/` package for use by both the CLI command and MCP server - Add `modernize` MCP tool for detecting/fixing YAML config anti-patterns - Add `registry_search` MCP tool for querying workflow-registry plugins - Remove duplicate `cmd/workflow-mcp-server/` binary (use `wfctl mcp`) - Update MCP docs to reflect wfctl-only entry point Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 168b9d7 commit 6bd5399

10 files changed

Lines changed: 346 additions & 186 deletions

File tree

Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: build build-ui build-go test bench bench-baseline bench-compare lint fmt vet fix install-hooks clean ko-build
1+
.PHONY: build build-ui build-go test bench bench-baseline bench-compare lint fmt vet fix install-hooks clean ko-build build-wfctl
22

33
# Common benchmark flags
44
BENCH_FLAGS = -bench=. -benchmem -run=^$$ -timeout=30m
@@ -18,9 +18,9 @@ build-ui:
1818
build-go:
1919
go build -o server ./cmd/server
2020

21-
# Build MCP server binary
22-
build-mcp:
23-
go build -o workflow-mcp-server ./cmd/workflow-mcp-server
21+
# Build wfctl CLI (includes MCP server)
22+
build-wfctl:
23+
go build -o wfctl ./cmd/wfctl
2424

2525
# Run all tests with race detection
2626
test:

cmd/wfctl/mcp.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
func runMCP(args []string) error {
1313
fs := flag.NewFlagSet("mcp", flag.ContinueOnError)
1414
pluginDir := fs.String("plugin-dir", "data/plugins", "Plugin data directory")
15+
registryDir := fs.String("registry-dir", "", "Path to cloned workflow-registry for plugin search")
1516
fs.Usage = func() {
1617
fmt.Fprintf(fs.Output(), `Usage: wfctl mcp [options]
1718
@@ -20,7 +21,8 @@ This exposes workflow engine tools and resources to AI assistants such as
2021
Claude Desktop, VS Code with GitHub Copilot, and Cursor.
2122
2223
The server provides tools for listing module types, validating configs,
23-
generating schemas, and inspecting workflow YAML configurations.
24+
generating schemas, inspecting workflow YAML configurations, modernizing
25+
configs (detecting/fixing anti-patterns), and searching the plugin registry.
2426
2527
Options:
2628
`)
@@ -32,7 +34,7 @@ Example Claude Desktop configuration (~/.config/claude/claude_desktop_config.jso
3234
"mcpServers": {
3335
"workflow": {
3436
"command": "wfctl",
35-
"args": ["mcp", "-plugin-dir", "/path/to/data/plugins"]
37+
"args": ["mcp", "-plugin-dir", "/path/to/data/plugins", "-registry-dir", "/path/to/workflow-registry"]
3638
}
3739
}
3840
}
@@ -48,6 +50,11 @@ See docs/mcp.md for full setup instructions.
4850
// reflect the release version set at build time.
4951
workflowmcp.Version = version
5052

51-
srv := workflowmcp.NewServer(*pluginDir)
53+
var opts []workflowmcp.ServerOption
54+
if *registryDir != "" {
55+
opts = append(opts, workflowmcp.WithRegistryDir(*registryDir))
56+
}
57+
58+
srv := workflowmcp.NewServer(*pluginDir, opts...)
5259
return srv.ServeStdio()
5360
}

cmd/wfctl/modernize.go

Lines changed: 5 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,11 @@ import (
44
"flag"
55
"fmt"
66
"os"
7-
"strings"
87

8+
"github.com/GoCodeAlone/workflow/modernize"
99
"gopkg.in/yaml.v3"
1010
)
1111

12-
// Finding represents a single issue detected by a modernize rule.
13-
type Finding struct {
14-
RuleID string
15-
Line int
16-
Message string
17-
Fixable bool
18-
}
19-
20-
// Change represents a modification applied by a rule's Fix function.
21-
type Change struct {
22-
RuleID string
23-
Line int
24-
Description string
25-
}
26-
27-
// Rule defines a modernize transformation rule.
28-
type Rule struct {
29-
ID string
30-
Description string
31-
Severity string // "error" or "warning"
32-
Check func(root *yaml.Node, raw []byte) []Finding
33-
Fix func(root *yaml.Node) []Change
34-
}
35-
3612
func runModernize(args []string) error {
3713
fs := flag.NewFlagSet("modernize", flag.ContinueOnError)
3814
apply := fs.Bool("apply", false, "Apply fixes in-place (default: dry-run)")
@@ -64,7 +40,7 @@ Options:
6440
return err
6541
}
6642

67-
rules := allModernizeRules()
43+
rules := modernize.AllRules()
6844

6945
if *listRules {
7046
fmt.Println("Available modernize rules:")
@@ -80,7 +56,7 @@ Options:
8056
}
8157

8258
// Filter rules
83-
rules = filterRules(rules, *rulesFlag, *excludeFlag)
59+
rules = modernize.FilterRules(rules, *rulesFlag, *excludeFlag)
8460

8561
// Collect files
8662
var files []string
@@ -145,7 +121,7 @@ Options:
145121
}
146122

147123
// modernizeFile checks (and optionally fixes) a single YAML file.
148-
func modernizeFile(path string, rules []Rule, apply bool) ([]Finding, int, error) {
124+
func modernizeFile(path string, rules []modernize.Rule, apply bool) ([]modernize.Finding, int, error) {
149125
data, err := os.ReadFile(path)
150126
if err != nil {
151127
return nil, 0, err
@@ -157,7 +133,7 @@ func modernizeFile(path string, rules []Rule, apply bool) ([]Finding, int, error
157133
}
158134

159135
// Check phase
160-
var allFindings []Finding
136+
var allFindings []modernize.Finding
161137
for _, r := range rules {
162138
findings := r.Check(&doc, data)
163139
allFindings = append(allFindings, findings...)
@@ -189,36 +165,3 @@ func modernizeFile(path string, rules []Rule, apply bool) ([]Finding, int, error
189165

190166
return allFindings, fixCount, nil
191167
}
192-
193-
// filterRules filters the rule list based on include/exclude flags.
194-
func filterRules(rules []Rule, include, exclude string) []Rule {
195-
if include == "" && exclude == "" {
196-
return rules
197-
}
198-
199-
includeSet := make(map[string]bool)
200-
if include != "" {
201-
for _, id := range strings.Split(include, ",") {
202-
includeSet[strings.TrimSpace(id)] = true
203-
}
204-
}
205-
206-
excludeSet := make(map[string]bool)
207-
if exclude != "" {
208-
for _, id := range strings.Split(exclude, ",") {
209-
excludeSet[strings.TrimSpace(id)] = true
210-
}
211-
}
212-
213-
var filtered []Rule
214-
for _, r := range rules {
215-
if len(includeSet) > 0 && !includeSet[r.ID] {
216-
continue
217-
}
218-
if excludeSet[r.ID] {
219-
continue
220-
}
221-
filtered = append(filtered, r)
222-
}
223-
return filtered
224-
}

cmd/wfctl/modernize_test.go

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"strings"
66
"testing"
77

8+
"github.com/GoCodeAlone/workflow/modernize"
89
"gopkg.in/yaml.v3"
910
)
1011

@@ -25,37 +26,37 @@ func TestRunModernize_NoFiles(t *testing.T) {
2526
}
2627

2728
func TestFilterRules_Empty(t *testing.T) {
28-
rules := []Rule{
29+
rules := []modernize.Rule{
2930
{ID: "rule-a", Description: "A"},
3031
{ID: "rule-b", Description: "B"},
3132
}
3233

3334
// No filters — all rules returned
34-
result := filterRules(rules, "", "")
35+
result := modernize.FilterRules(rules, "", "")
3536
if len(result) != 2 {
3637
t.Fatalf("expected 2 rules, got %d", len(result))
3738
}
3839
}
3940

4041
func TestFilterRules_Include(t *testing.T) {
41-
rules := []Rule{
42+
rules := []modernize.Rule{
4243
{ID: "rule-a", Description: "A"},
4344
{ID: "rule-b", Description: "B"},
4445
}
4546

46-
result := filterRules(rules, "rule-a", "")
47+
result := modernize.FilterRules(rules, "rule-a", "")
4748
if len(result) != 1 || result[0].ID != "rule-a" {
4849
t.Fatalf("expected only rule-a, got %v", result)
4950
}
5051
}
5152

5253
func TestFilterRules_Exclude(t *testing.T) {
53-
rules := []Rule{
54+
rules := []modernize.Rule{
5455
{ID: "rule-a", Description: "A"},
5556
{ID: "rule-b", Description: "B"},
5657
}
5758

58-
result := filterRules(rules, "", "rule-b")
59+
result := modernize.FilterRules(rules, "", "rule-b")
5960
if len(result) != 1 || result[0].ID != "rule-a" {
6061
t.Fatalf("expected only rule-a, got %v", result)
6162
}
@@ -69,7 +70,7 @@ func TestModernizeFile_ValidYAML(t *testing.T) {
6970
t.Fatal(err)
7071
}
7172

72-
findings, fixes, err := modernizeFile(tmpFile, []Rule{}, false)
73+
findings, fixes, err := modernizeFile(tmpFile, []modernize.Rule{}, false)
7374
if err != nil {
7475
t.Fatalf("unexpected error: %v", err)
7576
}
@@ -88,7 +89,7 @@ func TestModernizeFile_InvalidYAML(t *testing.T) {
8889
t.Fatal(err)
8990
}
9091

91-
_, _, err := modernizeFile(tmpFile, []Rule{}, false)
92+
_, _, err := modernizeFile(tmpFile, []modernize.Rule{}, false)
9293
if err == nil {
9394
t.Fatal("expected error for invalid YAML")
9495
}
@@ -108,8 +109,8 @@ func parseTestYAML(t *testing.T, input string) *yaml.Node {
108109
}
109110

110111
// findRule is a test helper that looks up a rule by ID.
111-
func findRule(id string) *Rule {
112-
for _, r := range allModernizeRules() {
112+
func findRule(id string) *modernize.Rule {
113+
for _, r := range modernize.AllRules() {
113114
if r.ID == id {
114115
return &r
115116
}
@@ -185,8 +186,8 @@ pipelines:
185186
config:
186187
field: steps.check-xss.matched
187188
`
188-
rules := allModernizeRules()
189-
var rule Rule
189+
rules := modernize.AllRules()
190+
var rule modernize.Rule
190191
for _, r := range rules {
191192
if r.ID == "hyphen-steps" {
192193
rule = r
@@ -230,8 +231,8 @@ pipelines:
230231
body:
231232
value: "{{ .steps.check-xss.result }}"
232233
`
233-
rules := allModernizeRules()
234-
var rule Rule
234+
rules := modernize.AllRules()
235+
var rule modernize.Rule
235236
for _, r := range rules {
236237
if r.ID == "hyphen-steps" {
237238
rule = r
@@ -483,7 +484,7 @@ modules:
483484
}
484485

485486
func TestModernizeAllRulesRegistered(t *testing.T) {
486-
rules := allModernizeRules()
487+
rules := modernize.AllRules()
487488
expectedIDs := []string{
488489
"hyphen-steps",
489490
"conditional-field",
@@ -508,16 +509,16 @@ func TestModernizeAllRulesRegistered(t *testing.T) {
508509
}
509510

510511
func TestFilterRulesIntegration(t *testing.T) {
511-
rules := allModernizeRules()
512+
rules := modernize.AllRules()
512513

513514
// Include filter
514-
filtered := filterRules(rules, "hyphen-steps,empty-routes", "")
515+
filtered := modernize.FilterRules(rules, "hyphen-steps,empty-routes", "")
515516
if len(filtered) != 2 {
516517
t.Errorf("expected 2 rules with include filter, got %d", len(filtered))
517518
}
518519

519520
// Exclude filter
520-
filtered = filterRules(rules, "", "camelcase-config")
521+
filtered = modernize.FilterRules(rules, "", "camelcase-config")
521522
if len(filtered) != len(rules)-1 {
522523
t.Errorf("expected %d rules with exclude filter, got %d", len(rules)-1, len(filtered))
523524
}
@@ -553,11 +554,11 @@ pipelines:
553554
body:
554555
name: "{{ .steps.fetch.row.name }}"
555556
`
556-
rules := allModernizeRules()
557+
rules := modernize.AllRules()
557558
doc := parseTestYAML(t, input)
558559

559560
// Check phase
560-
var allFindings []Finding
561+
var allFindings []modernize.Finding
561562
for _, r := range rules {
562563
allFindings = append(allFindings, r.Check(doc, []byte(input))...)
563564
}

cmd/workflow-mcp-server/main.go

Lines changed: 0 additions & 39 deletions
This file was deleted.

0 commit comments

Comments
 (0)