Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions cmd/wfctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"fmt"
"os"
"time"
)

var version = "dev"
Expand All @@ -29,6 +30,8 @@ var commands = map[string]func([]string) error{
"generate": runGenerate,
"git": runGit,
"registry": runRegistry,
"update": runUpdate,
"mcp": runMCP,
}

func usage() {
Expand Down Expand Up @@ -59,6 +62,8 @@ Commands:
generate Code generation (github-actions: generate CI/CD workflows from config)
git Git integration (connect: link to GitHub repo, push: commit and push)
registry Registry management (list, add, remove plugin registry sources)
update Update wfctl to the latest version (use --check to only check)
mcp Start the MCP server over stdio for AI assistant integration

Run 'wfctl <command> -h' for command-specific help.
`, version)
Expand Down Expand Up @@ -87,8 +92,25 @@ func main() {
os.Exit(1)
}

// Start the update check in the background before running the command so
// that it runs concurrently. For long-running commands (mcp, run) we skip
// it entirely. After the command finishes we wait briefly for the result.
var updateNoticeDone <-chan struct{}
if cmd != "mcp" && cmd != "run" {
updateNoticeDone = checkForUpdateNotice()
}

if err := fn(os.Args[2:]); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err) //nolint:gosec // G705: CLI error output
os.Exit(1)
}

// Wait briefly for the update notice after the command completes.
// A 1-second ceiling ensures we never meaningfully delay the shell prompt.
if updateNoticeDone != nil {
select {
case <-updateNoticeDone:
case <-time.After(time.Second):
}
}
}
53 changes: 53 additions & 0 deletions cmd/wfctl/mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package main

import (
"flag"
"fmt"

workflowmcp "github.com/GoCodeAlone/workflow/mcp"
)

// runMCP starts the workflow MCP (Model Context Protocol) server over stdio.
// This exposes workflow engine tools and resources to AI assistants.
func runMCP(args []string) error {
fs := flag.NewFlagSet("mcp", flag.ContinueOnError)
pluginDir := fs.String("plugin-dir", "data/plugins", "Plugin data directory")
fs.Usage = func() {
fmt.Fprintf(fs.Output(), `Usage: wfctl mcp [options]

Start the workflow MCP (Model Context Protocol) server over stdio.
This exposes workflow engine tools and resources to AI assistants such as
Claude Desktop, VS Code with GitHub Copilot, and Cursor.

The server provides tools for listing module types, validating configs,
generating schemas, and inspecting workflow YAML configurations.

Options:
`)
fs.PrintDefaults()
fmt.Fprintf(fs.Output(), `
Example Claude Desktop configuration (~/.config/claude/claude_desktop_config.json):

{
"mcpServers": {
"workflow": {
"command": "wfctl",
"args": ["mcp", "-plugin-dir", "/path/to/data/plugins"]
}
}
}

See docs/mcp.md for full setup instructions.
`)
}
if err := fs.Parse(args); err != nil {
return err
}

// Propagate the CLI version so the MCP handshake and version output
// reflect the release version set at build time.
workflowmcp.Version = version

srv := workflowmcp.NewServer(*pluginDir)
return srv.ServeStdio()
}
21 changes: 21 additions & 0 deletions cmd/wfctl/mcp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import (
"testing"
)

func TestRunMCP_Usage(t *testing.T) {
// Passing -h should return an error from flag parsing (ExitOnError calls os.Exit,
// but we use ContinueOnError in runMCP so it returns an error instead).
err := runMCP([]string{"-h"})
if err == nil {
t.Fatal("expected error from -h flag")
}
}

func TestRunMCP_UnknownFlag(t *testing.T) {
err := runMCP([]string{"--unknown-flag"})
if err == nil {
t.Fatal("expected error for unknown flag")
}
}
Loading
Loading