diff --git a/README.md b/README.md
index ff72d49d..4d287844 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ flow complements existing CLI tools by adding multi-project organization, built-
- **Flexible execution** - Serial, parallel, conditional, and interactive workflows
- **Workflow generation** - Create projects and workflows from reusable templates
- **Composable workflows** - Reference and chain workflows within and across projects
+- **Platform integrations** - GitHub Actions, AI assistants (MCP), and more

diff --git a/cmd/internal/mcp.go b/cmd/internal/mcp.go
new file mode 100644
index 00000000..4668845b
--- /dev/null
+++ b/cmd/internal/mcp.go
@@ -0,0 +1,30 @@
+package internal
+
+import (
+ "github.com/spf13/cobra"
+
+ "github.com/flowexec/flow/internal/context"
+ "github.com/flowexec/flow/internal/logger"
+ "github.com/flowexec/flow/internal/mcp"
+)
+
+func RegisterMCPCmd(ctx *context.Context, rootCmd *cobra.Command) {
+ subCmd := &cobra.Command{
+ Use: "mcp",
+ Short: "Start Model Context Provider (MCP) server for AI assistant integration",
+ Long: "Start a Model Context Protocol server that enables AI assistants to interact with your flow executables, " +
+ "workspaces, and configurations through natural language. AI assistants can discover, validate, and execute " +
+ "flow workflows, making your automation platform accessible through conversational interfaces/clients.\n\n" +
+ "This server used stdio for transport. For more information on MCP, see https://modelcontextprotocol.io",
+ Args: cobra.NoArgs,
+ Run: func(cmd *cobra.Command, args []string) { mcpFunc(ctx, cmd, args) },
+ }
+ rootCmd.AddCommand(subCmd)
+}
+
+func mcpFunc(_ *context.Context, _ *cobra.Command, _ []string) {
+ server := mcp.NewServer(&mcp.FlowCLIExecutor{})
+ if err := server.Run(); err != nil {
+ logger.Log().FatalErr(err)
+ }
+}
diff --git a/cmd/root.go b/cmd/root.go
index 3c7916f0..d42c183f 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -82,4 +82,5 @@ func RegisterSubCommands(ctx *context.Context, rootCmd *cobra.Command) {
internal.RegisterTemplateCmd(ctx, rootCmd)
internal.RegisterLogsCmd(ctx, rootCmd)
internal.RegisterSyncCmd(ctx, rootCmd)
+ internal.RegisterMCPCmd(ctx, rootCmd)
}
diff --git a/docs/README.md b/docs/README.md
index edafa27e..38711fc7 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -26,6 +26,7 @@ and cross-project composition. Go beyond task running to workflow management tha
- **Cross-Project Composition**: Reference and share workflows between different projects
- **Visual Workflow Browser**: Discover and run workflows with powerful filtering and search
- **Flexible Configuration**: YAML-based definitions with arguments, secrets, and conditional logic
+- **Platform Integrations**: GitHub Actions, AI assistants (MCP), and more
**Ready to organize your automation?** → [Install flow](installation.md) → [Quick start guide](quickstart.md)
diff --git a/docs/cli/README.md b/docs/cli/README.md
index 065f2f2f..0a4cdbf2 100644
--- a/docs/cli/README.md
+++ b/docs/cli/README.md
@@ -23,6 +23,7 @@ See https://flowexec.io for more information.
* [flow config](flow_config.md) - Update flow configuration values.
* [flow exec](flow_exec.md) - Execute any executable by reference.
* [flow logs](flow_logs.md) - View execution history and logs.
+* [flow mcp](flow_mcp.md) - Start Model Context Provider (MCP) server for AI assistant integration
* [flow secret](flow_secret.md) - Manage secrets stored in a vault.
* [flow sync](flow_sync.md) - Refresh workspace cache and discover new executables.
* [flow template](flow_template.md) - Manage flowfile templates.
diff --git a/docs/cli/flow_mcp.md b/docs/cli/flow_mcp.md
new file mode 100644
index 00000000..4f6e372f
--- /dev/null
+++ b/docs/cli/flow_mcp.md
@@ -0,0 +1,31 @@
+## flow mcp
+
+Start Model Context Provider (MCP) server for AI assistant integration
+
+### Synopsis
+
+Start a Model Context Protocol server that enables AI assistants to interact with your flow executables, workspaces, and configurations through natural language. AI assistants can discover, validate, and execute flow workflows, making your automation platform accessible through conversational interfaces/clients.
+
+This server used stdio for transport. For more information on MCP, see https://modelcontextprotocol.io
+
+```
+flow mcp [flags]
+```
+
+### Options
+
+```
+ -h, --help help for mcp
+```
+
+### Options inherited from parent commands
+
+```
+ -L, --log-level string Log verbosity level (debug, info, fatal) (default "info")
+ --sync Sync flow cache and workspaces
+```
+
+### SEE ALSO
+
+* [flow](flow.md) - flow is a command line interface designed to make managing and running development workflows easier.
+
diff --git a/docs/guide/integrations.md b/docs/guide/integrations.md
index 7d6a3564..ced37150 100644
--- a/docs/guide/integrations.md
+++ b/docs/guide/integrations.md
@@ -1,8 +1,49 @@
# Integrations
-flow integrates with popular CI/CD platforms and containerized environments to bring your automation anywhere.
+flow integrates with popular CI/CD platforms, AI assistants, and containerized environments to bring your automation anywhere.
-## GitHub Actions
+
+## AI Assistant Integration
+
+### Model Context Protocol (MCP)
+
+Connect flow to AI assistants through the local Model Context Protocol server for natural language workflow management.
+The flow MCP server enables AI assistants to discover, understand, and execute your flow workflows through conversational interfaces.
+
+#### Basic Usage
+
+Add the MCP server command to your favorite MCP client:
+
+```shell
+flow mcp
+```
+
+The server uses stdio transport and provides AI assistants with:
+
+**Available Tools:**
+- `get_info` - Get flow information, schemas, and current context
+- `execute` - Execute flow workflows
+- `list_workspaces` - List all registered workspaces
+- `get_workspace` - Get details about a specific workspace
+- `switch_workspace` - Change the current workspace
+- `list_executables` - List and filter executables across workspaces
+- `get_executable` - Get detailed information about an executable
+- `get_execution_logs` - Retrieve recent execution logs
+- `sync_executables` - Sync workspace and executable state
+
+**Available Prompts:**
+- `generate_executable` - Generate flow executable configurations
+- `generate_project_executables` - Generate complete project automation sets
+- `debug_executable` - Debug failing executables
+- `migrate_automation` - Convert existing automation to flow
+- `explain_flow` - Explain flow concepts and usage
+
+> [!NOTE]
+> **Learn more about MCP**: Visit the [Model Context Protocol](https://modelcontextprotocol.io) documentation for client setup and integration details.
+
+## CI/CD & Deployment
+
+### GitHub Actions
Execute flow workflows directly in your GitHub Actions pipelines with the official action.
@@ -22,7 +63,7 @@ jobs:
> **Complete documentation**: Visit the [Flow Execute Action](https://github.com/marketplace/actions/flow-execute) on GitHub Marketplace.
-## Docker
+### Docker
Run flow in containerized environments for CI/CD pipelines or isolated execution.
diff --git a/go.mod b/go.mod
index 90285e2d..9ffd2e3a 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,7 @@ require (
github.com/flowexec/vault v0.1.2
github.com/gen2brain/beeep v0.11.1
github.com/jahvon/glamour v0.8.1-patch3
+ github.com/mark3labs/mcp-go v0.36.0
github.com/mattn/go-runewidth v0.0.16
github.com/muesli/termenv v0.16.0
github.com/onsi/ginkgo/v2 v2.23.4
@@ -41,6 +42,8 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymanbagabas/go-udiff v0.2.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
+ github.com/bahlo/generic-list-go v0.2.0 // indirect
+ github.com/buger/jsonparser v1.1.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
github.com/charmbracelet/huh v0.7.0 // indirect
@@ -66,8 +69,10 @@ require (
github.com/gorilla/css v1.0.1 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/invopop/jsonschema v0.13.0 // indirect
github.com/jackmordaunt/icns/v3 v3.0.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
@@ -85,15 +90,17 @@ require (
github.com/sergeymakinen/go-bmp v1.0.0 // indirect
github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
- github.com/spf13/cast v1.7.0 // indirect
+ github.com/spf13/cast v1.7.1 // indirect
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
+ github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yuin/goldmark v1.7.4 // indirect
github.com/yuin/goldmark-emoji v1.0.3 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
- golang.org/x/net v0.40.0 // indirect
+ golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
- golang.org/x/tools v0.33.0 // indirect
+ golang.org/x/tools v0.34.0 // indirect
)
diff --git a/go.sum b/go.sum
index 5dc28c75..c908f066 100644
--- a/go.sum
+++ b/go.sum
@@ -28,6 +28,10 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
+github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
+github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
+github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
@@ -113,10 +117,13 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
+github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o=
github.com/jackmordaunt/icns/v3 v3.0.1/go.mod h1:5sHL59nqTd2ynTnowxB/MDQFhKNqkK8X687uKNygaSQ=
github.com/jahvon/glamour v0.8.1-patch3 h1:LfyMACZavV8yxK4UsPENNQQOqafWuq4ZdLuEAP2ZLE8=
github.com/jahvon/glamour v0.8.1-patch3/go.mod h1:30MVJwG3rcEHrN277NrA4DKzndSL9lBtEmpcfOygwCQ=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -125,6 +132,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+github.com/mark3labs/mcp-go v0.36.0 h1:rIZaijrRYPeSbJG8/qNDe0hWlGrCJ7FWHNMz2SQpTis=
+github.com/mark3labs/mcp-go v0.36.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
@@ -180,8 +191,8 @@ github.com/sergeymakinen/go-ico v1.0.0-beta.0 h1:m5qKH7uPKLdrygMWxbamVn+tl2HfiA3
github.com/sergeymakinen/go-ico v1.0.0-beta.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
-github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
-github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
@@ -196,8 +207,12 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
+github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
+github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
@@ -213,8 +228,8 @@ golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -226,8 +241,8 @@ golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
-golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
-golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
+golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
+golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/internal/context/context.go b/internal/context/context.go
index 37f9d974..8350b82a 100644
--- a/internal/context/context.go
+++ b/internal/context/context.go
@@ -5,8 +5,6 @@ import (
"fmt"
"os"
"path/filepath"
- "runtime"
- "strings"
"github.com/flowexec/tuikit"
"github.com/flowexec/tuikit/themes"
@@ -67,13 +65,7 @@ func NewContext(ctx context.Context, stdIn, stdOut *os.File) *Context {
}
workspaceCache := cache.NewWorkspaceCache()
- if workspaceCache == nil {
- panic("workspace cache initialization error")
- }
executableCache := cache.NewExecutableCache(workspaceCache)
- if executableCache == nil {
- panic("executable cache initialization error")
- }
ctxx, cancel := context.WithCancel(ctx)
c := &Context{
@@ -189,43 +181,11 @@ func ExpandRef(ctx *Context, ref executable.Ref) executable.Ref {
}
func currentWorkspace(cfg *config.Config) (*workspace.Workspace, error) {
- var ws, wsPath string
- mode := cfg.WorkspaceMode
-
- switch mode {
- case config.ConfigWorkspaceModeDynamic:
- wd, err := os.Getwd()
- if err != nil {
- return nil, err
- }
- if runtime.GOOS == "darwin" {
- // On macOS, paths that start with /tmp (and some other system directories)
- // are actually symbolic links to paths under /private. The OS may return
- // either form of the path - e.g., both "/tmp/file" and "/private/tmp/file"
- // refer to the same location. We strip the "/private" prefix for consistent
- // path comparison, while preserving the original paths for filesystem operations.
- wd = strings.TrimPrefix(wd, "/private")
- }
-
- for wsName, path := range cfg.Workspaces {
- rel, err := filepath.Rel(filepath.Clean(path), filepath.Clean(wd))
- if err != nil {
- return nil, err
- }
- if !strings.HasPrefix(rel, "..") {
- ws = wsName
- wsPath = path
- break
- }
- }
- fallthrough
- case config.ConfigWorkspaceModeFixed:
- if ws != "" && wsPath != "" {
- break
- }
- ws = cfg.CurrentWorkspace
- wsPath = cfg.Workspaces[ws]
+ ws, err := cfg.CurrentWorkspaceName()
+ if err != nil {
+ return nil, err
}
+ wsPath := cfg.Workspaces[ws]
if ws == "" || wsPath == "" {
return nil, fmt.Errorf("current workspace not found")
}
diff --git a/internal/mcp/command_executor.go b/internal/mcp/command_executor.go
new file mode 100644
index 00000000..f842b55c
--- /dev/null
+++ b/internal/mcp/command_executor.go
@@ -0,0 +1,40 @@
+package mcp
+
+import (
+ "os"
+ "os/exec"
+
+ "github.com/pkg/errors"
+)
+
+const cliBinaryEnvKey = "FLOW_CLI_BINARY"
+
+//go:generate mockgen -destination=mocks/command_executor.go -package=mocks . CommandExecutor
+type CommandExecutor interface {
+ Execute(args ...string) (string, error)
+}
+
+// FlowCLIExecutor runs the flow CLI with provided arguments. The CLI is being executed instead of importing the
+// internal flow package directly to avoid duplicating the code that's defined in the cmd package, to make testing
+// easier, and to avoid having to refactor the Context obj which is not currently designed in a way to be copied/reused
+// across "executions". Maybe consider refactoring this when the context is refactored.
+//
+// The binary name can be overridden by setting the FLOW_CLI_BINARY environment variable.
+type FlowCLIExecutor struct{}
+
+func (c *FlowCLIExecutor) Execute(args ...string) (string, error) {
+ name := "flow"
+ if envName := os.Getenv(cliBinaryEnvKey); envName != "" {
+ name = envName
+ }
+ cmd := exec.Command(name, args...)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ // Only return an error if it's not an exit error.
+ exitErr := &exec.ExitError{}
+ if !errors.As(err, &exitErr) {
+ return string(output), err
+ }
+ }
+ return string(output), nil
+}
diff --git a/internal/mcp/mocks/command_executor.go b/internal/mcp/mocks/command_executor.go
new file mode 100644
index 00000000..6eb96be4
--- /dev/null
+++ b/internal/mcp/mocks/command_executor.go
@@ -0,0 +1,58 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/flowexec/flow/internal/mcp (interfaces: CommandExecutor)
+//
+// Generated by this command:
+//
+// mockgen -destination=mocks/command_executor.go -package=mocks . CommandExecutor
+//
+
+// Package mocks is a generated GoMock package.
+package mocks
+
+import (
+ reflect "reflect"
+
+ gomock "go.uber.org/mock/gomock"
+)
+
+// MockCommandExecutor is a mock of CommandExecutor interface.
+type MockCommandExecutor struct {
+ ctrl *gomock.Controller
+ recorder *MockCommandExecutorMockRecorder
+}
+
+// MockCommandExecutorMockRecorder is the mock recorder for MockCommandExecutor.
+type MockCommandExecutorMockRecorder struct {
+ mock *MockCommandExecutor
+}
+
+// NewMockCommandExecutor creates a new mock instance.
+func NewMockCommandExecutor(ctrl *gomock.Controller) *MockCommandExecutor {
+ mock := &MockCommandExecutor{ctrl: ctrl}
+ mock.recorder = &MockCommandExecutorMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockCommandExecutor) EXPECT() *MockCommandExecutorMockRecorder {
+ return m.recorder
+}
+
+// Execute mocks base method.
+func (m *MockCommandExecutor) Execute(arg0 ...string) (string, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "Execute", varargs...)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Execute indicates an expected call of Execute.
+func (mr *MockCommandExecutorMockRecorder) Execute(arg0 ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockCommandExecutor)(nil).Execute), arg0...)
+}
diff --git a/internal/mcp/prompts.go b/internal/mcp/prompts.go
new file mode 100644
index 00000000..61f2ab41
--- /dev/null
+++ b/internal/mcp/prompts.go
@@ -0,0 +1,362 @@
+//nolint:lll
+package mcp
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+)
+
+func addServerPrompts(s *server.MCPServer) {
+ generateExecutable := mcp.NewPrompt("generate_executable",
+ mcp.WithPromptDescription("Generate a Flow executable configuration"),
+ mcp.WithArgument("task_description",
+ mcp.ArgumentDescription("What task should this executable perform?"),
+ mcp.RequiredArgument(),
+ ),
+ mcp.WithArgument("verb",
+ mcp.ArgumentDescription("Preferred action verb (build, test, deploy, run, etc.)"),
+ ),
+ mcp.WithArgument("execution_type",
+ mcp.ArgumentDescription("e.g. single command, shell file, workflow / multiple steps, REST, launch, etc."),
+ ),
+ mcp.WithArgument("context",
+ mcp.ArgumentDescription("Technology stack, environment, constraints"),
+ ),
+ )
+ s.AddPrompt(generateExecutable, generateExecutablePrompt)
+
+ generateProjectExecutables := mcp.NewPrompt("generate_project_executables",
+ mcp.WithPromptDescription("Generate a complete set of executables for a project"),
+ mcp.WithArgument("project_type",
+ mcp.ArgumentDescription("e.g. web app, API, CLI tool, mobile app, etc."),
+ mcp.RequiredArgument(),
+ ),
+ mcp.WithArgument("tech_stack",
+ mcp.ArgumentDescription("e.g., 'Node.js + React', 'Go + Docker', 'Python + FastAPI'"),
+ ),
+ mcp.WithArgument("development_stage",
+ mcp.ArgumentDescription("e.g. 'new' (starting fresh), 'existing' (has some automation), 'mature' (complex workflows)"),
+ ),
+ )
+ s.AddPrompt(generateProjectExecutables, generateProjectExecutablesPrompt)
+
+ debugExecutable := mcp.NewPrompt("debug_executable",
+ mcp.WithPromptDescription("Debug a failing Flow executable"),
+ mcp.WithArgument("executable_ref",
+ mcp.ArgumentDescription("The executable that's failing (e.g., 'build app', 'deploy prod')"),
+ mcp.RequiredArgument(),
+ ),
+ mcp.WithArgument("failure_description",
+ mcp.ArgumentDescription("Brief description of what's going wrong"),
+ ),
+ )
+ s.AddPrompt(debugExecutable, debugExecutablePrompt)
+
+ migrateAutomation := mcp.NewPrompt("migrate_automation",
+ mcp.WithPromptDescription("Convert existing automation (Makefile, npm scripts, shell scripts, etc.) to Flow executables"),
+ mcp.WithArgument("automation_type",
+ mcp.ArgumentDescription("Current automation system (Makefile, package.json scripts, shell scripts, GitHub Actions, etc.)"),
+ mcp.RequiredArgument(),
+ ),
+ mcp.WithArgument("current_tasks",
+ mcp.ArgumentDescription("List or describe the tasks you currently run (e.g., 'build, test, deploy to staging')"),
+ mcp.RequiredArgument(),
+ ),
+ mcp.WithArgument("pain_points",
+ mcp.ArgumentDescription("What's frustrating about your current setup?"),
+ ),
+ )
+ s.AddPrompt(migrateAutomation, migrateAutomationPrompt)
+
+ explainFlow := mcp.NewPrompt("explain_flow",
+ mcp.WithPromptDescription("Explain Flow concepts and help with getting started"),
+ mcp.WithArgument("topic",
+ mcp.ArgumentDescription("What to explain: 'basics', 'workspaces', 'executables', 'secrets', 'templates', 'getting-started'"),
+ ),
+ mcp.WithArgument("user_background",
+ mcp.ArgumentDescription("Your experience level: 'new' (never used Flow), 'beginner' (basic usage), 'intermediate' (regular user)"),
+ ),
+ )
+ s.AddPrompt(explainFlow, explainFlowPrompt)
+}
+
+func generateExecutablePrompt(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+ args := request.Params.Arguments
+ taskDescription := getArgOrDefault(args, "task_description", "")
+ verb := getArgOrDefault(args, "verb", "")
+ executionType := getArgOrDefault(args, "execution_type", "")
+ promptContext := getArgOrDefault(args, "context", "")
+
+ fmtStr := `I need to create a Flow executable for this task:
+
+**Task**: %s
+**Preferred Verb**: %s
+**Execution Type**: %s
+**Context**: %s
+
+Please generate a complete Flow executable configuration that:
+
+1. **Determines the Best Approach**:
+ - Choose the most appropriate executable type (exec, serial, parallel, request, launch)
+ - Select a suitable verb if none was provided
+ - Design the proper parameter and argument structure
+
+2. **Creates Production-Ready Configuration**:
+ - Valid YAML syntax following Flow conventions
+ - Appropriate error handling (timeouts, retries where applicable)
+ - Clear documentation and descriptions
+ - Secure parameter handling (secrets, environment variables)
+
+3. **Includes Usage Guidance**:
+ - How to run the executable
+ - What parameters or arguments are needed
+ - Expected behavior and outputs
+ - Any prerequisites or setup required
+
+4. **Follows Best Practices**:
+ - Proper naming conventions
+ - Appropriate visibility settings
+ - Helpful tags for organization
+ - Integration considerations
+
+If you need clarification about the task requirements, ask specific questions to ensure the generated executable meets the intended use case.`
+
+ return mcp.NewGetPromptResult(
+ "Generate Flow Executable",
+ []mcp.PromptMessage{
+ mcp.NewPromptMessage(
+ mcp.RoleUser,
+ mcp.NewTextContent(fmt.Sprintf(fmtStr, taskDescription, verb, executionType, promptContext)),
+ ),
+ },
+ ), nil
+}
+
+func generateProjectExecutablesPrompt(_ context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+ args := request.Params.Arguments
+ projectType := getArgOrDefault(args, "project_type", "")
+ techStack := getArgOrDefault(args, "tech_stack", "")
+ developmentStage := getArgOrDefault(args, "development_stage", "new")
+
+ fmtStr := `I want to set up Flow executables for my project:
+
+**Project Type**: %s
+**Technology Stack**: %s
+**Development Stage**: %s
+
+Please generate a comprehensive set of Flow executables that covers:
+
+1. **Core Development Workflow**:
+ - Build/compile processes
+ - Testing (unit, integration, e2e as applicable)
+ - Development server/watch modes
+ - Code quality checks (linting, formatting)
+
+2. **Deployment & Operations**:
+ - Local environment setup
+ - Staging/production deployment
+ - Environment configuration management
+ - Health checks and monitoring
+
+3. **Project-Specific Tasks**:
+ - Tasks specific to %s projects
+ - Technology stack integrations for %s
+ - Common maintenance and utility tasks
+
+4. **Organization & Structure**:
+ - Logical grouping with namespaces
+ - Proper executable naming and descriptions
+ - Dependencies and execution order
+ - Parameter sharing between related tasks
+
+5. **Documentation**:
+ - Clear descriptions for each executable
+ - Usage examples and common workflows
+ - Getting started guide for team members
+ - Integration with existing project structure
+
+Provide complete YAML configurations ready to use, organized in a way that makes sense for a project type and development stage.`
+
+ return mcp.NewGetPromptResult(
+ "Generate Project Executables",
+ []mcp.PromptMessage{
+ mcp.NewPromptMessage(
+ mcp.RoleUser,
+ mcp.NewTextContent(fmt.Sprintf(fmtStr, projectType, techStack, developmentStage, projectType, techStack)),
+ ),
+ },
+ ), nil
+}
+
+func debugExecutablePrompt(_ context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+ args := req.Params.Arguments
+ execRef := getArgOrDefault(args, "executable_ref", "")
+ failureDescription := getArgOrDefault(args, "failure_description", "")
+
+ fmtStr := `I need help debugging a Flow executable that's failing:
+
+**Executable**: %s
+**Problem Description**: %s
+
+Please help me troubleshoot by:
+
+1. **Initial Analysis**:
+ - Use the available MCP tools to check the executable configuration
+ - Retrieve recent logs to understand what's happening
+ - Identify common patterns in the failure
+
+2. **Root Cause Investigation**:
+ - Analyze the executable's configuration for issues
+ - Check for common Flow executable problems
+ - Consider environment and dependency issues
+ - Review parameter and argument handling
+
+3. **Diagnostic Commands**:
+ - Specific Flow CLI commands to gather more information
+ - How to test components individually
+ - Ways to isolate the problem
+
+4. **Solution Recommendations**:
+ - Specific fixes for identified issues
+ - Configuration adjustments needed
+ - Alternative approaches if current method is problematic
+ - Prevention strategies for similar issues
+
+5. **Verification Steps**:
+ - How to test that fixes work
+ - Commands to validate the executable
+ - Signs that everything is working correctly
+
+Start by using the available tools to gather information about the executable and its recent execution history, then provide targeted debugging advice.`
+
+ return mcp.NewGetPromptResult(
+ "Debug Flow Executable",
+ []mcp.PromptMessage{
+ mcp.NewPromptMessage(
+ mcp.RoleUser,
+ mcp.NewTextContent(fmt.Sprintf(fmtStr, execRef, failureDescription)),
+ ),
+ },
+ ), nil
+}
+
+func migrateAutomationPrompt(_ context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+ args := req.Params.Arguments
+ automationType := getArgOrDefault(args, "automation_type", "")
+ currentTasks := getArgOrDefault(args, "current_tasks", "")
+ painPoints := getArgOrDefault(args, "pain_points", "")
+
+ fmtStr := `I want to migrate my existing automation to Flow:
+
+**Current System**: %s
+**Tasks I Run**: %s
+**Pain Points**: %s
+
+Please help me convert this to Flow by providing:
+
+1. **Migration Strategy**:
+ - How to organize these tasks as Flow executables
+ - Recommended workspace and namespace structure
+ - Gradual migration approach to minimize disruption
+
+2. **Flow Equivalents**:
+ - Convert each existing task to a Flow executable
+ - Preserve all current functionality
+ - Improve error handling and logging where possible
+ - Maintain compatibility during transition
+
+3. **Enhanced Capabilities**:
+ - Take advantage of Flow's parameter and secret management
+ - Add proper documentation and discoverability
+ - Implement conditional logic and workflows where beneficial
+ - Better organization and team sharing
+
+4. **Specific Improvements**:
+ - Address the pain points I mentioned
+ - Flow-specific enhancements over current approach
+ - Better maintenance and debugging capabilities
+ - Integration opportunities with other tools
+
+5. **Implementation Plan**:
+ - Step-by-step migration instructions
+ - How to test converted workflows
+ - Rollback strategy if needed
+
+Provide concrete YAML configurations and specific commands to make the migration as smooth as possible.
+Focus on solving the current pain points while maintaining familiar workflows.`
+
+ return mcp.NewGetPromptResult(
+ "Migrate Automation to Flow",
+ []mcp.PromptMessage{
+ mcp.NewPromptMessage(
+ mcp.RoleUser,
+ mcp.NewTextContent(fmt.Sprintf(fmtStr, automationType, currentTasks, painPoints)),
+ ),
+ },
+ ), nil
+}
+
+func explainFlowPrompt(_ context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
+ args := req.Params.Arguments
+ topic := getArgOrDefault(args, "topic", "basics")
+ userBackground := getArgOrDefault(args, "user_background", "new")
+
+ fmtStr := `Please explain Flow concepts to me:
+
+**Topic**: %s
+**My Experience Level**: %s
+
+Please provide a clear explanation that:
+
+1. **Matches My Level**:
+ - Adjust complexity and detail for users with my experience level
+ - Use appropriate examples and analogies
+ - Build on concepts I likely already understand
+
+2. **Covers Key Concepts**:
+ - Core ideas and terminology for the desired topic
+ - How it fits into the broader Flow ecosystem
+ - Common use cases and patterns
+
+3. **Provides Practical Examples**:
+ - Real-world scenarios and configurations
+ - Common commands and workflows
+ - Hands-on exercises if appropriate
+
+4. **Addresses Common Questions**:
+ - Typical confusion points for users with my experience level
+ - How this relates to other automation tools
+ - Best practices and gotchas to avoid
+
+5. **Next Steps**:
+ - What to learn or try next
+ - Resources for deeper understanding
+ - How to get started with practical implementation
+
+Make the explanation conversational and practical, focusing on helping me understand not just what something is,
+but why it's useful and how to use it effectively. You should fetch any additional information you need directly
+from the Flow documentation at https://flowexec.io/#/README
+
+Do not add confusion by mentioning other tools or platforms unless directly relevant to the explanation. Clearly state
+when you do not have enough information to provide a complete answer, and suggest where I can find more details.`
+
+ return mcp.NewGetPromptResult(
+ "Explain Flow Concepts",
+ []mcp.PromptMessage{
+ mcp.NewPromptMessage(
+ mcp.RoleUser,
+ mcp.NewTextContent(fmt.Sprintf(fmtStr, topic, userBackground)),
+ ),
+ },
+ ), nil
+}
+
+func getArgOrDefault(args map[string]string, key, defaultVal string) string {
+ if val, exists := args[key]; exists {
+ return val
+ }
+ return defaultVal
+}
diff --git a/internal/mcp/resources/concepts-guide.md b/internal/mcp/resources/concepts-guide.md
new file mode 100644
index 00000000..0ec6c932
--- /dev/null
+++ b/internal/mcp/resources/concepts-guide.md
@@ -0,0 +1,288 @@
+# Flow Concepts Guide
+
+## What is flow?
+
+flow is a local-first, customizable CLI automation platform designed to streamline development and operations workflows.
+It helps developers organize, discover, and execute tasks across projects through a unified interface.
+
+More comprehensive details can be fetched from the [flow documentation](https://flowexec.io).
+
+## Core Philosophy
+
+- **Local-First**: All data and execution happens on your machine
+- **Declarative**: Define what you want to happen, not how
+- **Discoverable**: Find and explore workflows through interactive interfaces
+- **Composable**: Build complex workflows from simple building blocks
+- **Workspace-Centric**: Organize tasks by project/workspace context
+
+## Key Concepts
+
+### Executables
+**Executables** are the core building blocks of flow - they define actions that can be performed.
+Think of them as "smart scripts" with metadata, parameters, and rich configuration options.
+
+**Types of Executables:**
+- **exec**: Run shell commands or scripts
+- **serial**: Execute multiple steps sequentially
+- **parallel**: Execute multiple steps concurrently
+- **request**: Make HTTP API calls
+- **launch**: Open applications or URIs
+- **render**: Generate and display markdown content
+
+**Example executable:**
+```yaml
+executables:
+ - verb: build
+ name: web-app
+ description: Build the web application
+ exec:
+ cmd: npm run build
+ params:
+ - envKey: NODE_ENV
+ text: production
+```
+
+#### Executable References
+Executables are identified by its reference: combination of **Verb** and **ID**. The ID is in the form `/:`.
+A full reference must be unique across all registered workspaces.
+
+For instance:
+- `build app` - current workspace, root namespace
+- `build backend/app` - current workspace, backend namespace
+- `build my-project/backend:app` - specific workspace and namespace
+- `build` - current workspace, root namespace, no name set
+
+### Verbs
+**Verbs** describe what action an executable performs. Flow groups related verbs together, allowing natural language-like commands.
+By default, flow provide the following verb alias groups:
+
+- **Execution Group**: exec, run, execute
+- **Retrieval Group**: get, fetch, retrieve
+- **Display Group**: show, view, list
+- **Configuration Group**: configure, setup
+- **Update Group**: update, upgrade
+
+Additional aliases can be defined in the workspace or executable configuration to customize verb behavior.
+
+Users can invoke executables using any verb using the CLI (e.g. `flow build app` and `flow deploy app`)
+
+Run `flow exec --help` for more information on available verbs and execution details.
+
+### Workspaces
+**Workspaces** are project containers that organize related executables. Each workspace:
+- Has a root directory containing the workspace configuration file (`flow.yaml`)
+- Can contain multiple namespaces (defined in flow files)
+- Provides isolation between projects
+
+#### Workspace Modes
+**Dynamic**: The flow workspace is determined by the current working directory that the flow CLI is executed from
+**Fixed**: The flow workspace much be switched explicitly
+
+Only executables with `public` visibility can be executed across workspaces without switching. If the current workspace
+is not the workspace under review, you should switch into that workspace to avoid execution errors.
+
+### Namespaces
+**Namespaces** provide logical grouping within workspaces. They help organize executables by:
+- Feature area (auth, payments, notifications)
+- Environment (dev, staging, production)
+- Technology stack (backend, frontend, mobile)
+- Team ownership (platform, product, data)
+
+
+Executable names are optional. When not specified, the verb is used as the identifier,
+e.g. `flow build` refers to the executable with the verb "build" in the current workspace.
+
+### Executable Definitions (Flow Files)
+**Flow files** (`.flow`, `.flow.yaml`, `.flow.yml`) are YAML configuration files that define executables. They support:
+- Multiple executables per file
+- Shared metadata (namespace, tags, descriptions)
+- Environment variable management
+- Conditional execution logic
+- Can be located anywhere within the workspace directory
+
+### Secrets and Vaults
+**Vaults** provide secure secret storage with multiple encryption backends:
+- **AES256**: Symmetric encryption with generated keys
+- **Age**: Asymmetric encryption for team sharing
+
+Executables reference secrets using `secretRef` parameters, keeping sensitive data separate from configuration.
+
+### Templates
+**Templates** enable scaffolding new workspaces and executables. They support:
+- Interactive form collection
+- Go / Expr template rendering
+- File artifact copying
+- Pre/post-run hooks
+- Conditional generation
+
+## Execution Model
+
+### Sync
+
+Anytime a new workspace is registered, an executable is added, or it's identifier changes, flow state will need to be synchronized.
+This ensures that the latest workspace and executable definitions are available in the cache for execution.
+This can be done using the `flow sync` command or with the `--sync` flag on any command.
+
+### Management Commands
+
+Flow provides a set of management commands to interact with workspaces, executables, and configurations:
+```bash
+flow config # Manage flow's user configuration (get, set, reset)
+flow workspace # Manage workspaces (add, get, switch, list, remove)
+flow browse # Discover executables (interactive TUI)
+flow vault # Manage secrets and vaults (create, edit, get, list, remove, switch)
+flow cache # Manage cache data (get, set, remove, list, clear)
+flow secret # Manage secrets (get, list, remove, set)
+flow template # Manage templates (add, generate, get, list)
+flow logs # View execution logs
+```
+
+### Execution Command Structure
+```bash
+flow [arguments] [flags]
+```
+
+**Examples:**
+- `flow build app` - Build the app executable
+- `flow test backend/unit` - Run unit tests in backend namespace
+- `flow deploy prod-project/k8s:webapp` - Deploy webapp in specific workspace/namespace
+
+### Conditional Execution
+flow supports runtime conditions using the Expr language for serial and parallel executables:
+```yaml
+serial:
+ execs:
+ - if: os == "darwin"
+ cmd: brew install mytool
+ - if: env["CI"] == "true"
+ cmd: run-ci-specific-setup
+ - if: len(store["build-id"]) > 0 # checks the cache for a build ID key
+ cmd: use-cached-build
+```
+
+### State Management
+flow provides state persistence through:
+- **Cache**: Key-value store for sharing data between executables. State is only persisted during execution
+ - Use `flow cache set KEY VALUE` and `flow cache get KEY` to manage cache entries within executable scripts
+ - Users can also manage global keys outside the execution context using `flow cache` commands
+- **Context variables**: OS, architecture, workspace, and path information
+- **Environment inheritance**: Variables (`params` and `args`) flow from parent to child executables (for serial and parallel)
+- **Temporary directories**: Isolated scratch space for executable runs
+ - A temporary directory is created for the execution when the executable's `dir` is set to `f:tmp`)
+
+## Workflow Patterns
+
+### Simple Task
+```yaml
+executables:
+ - verb: test
+ name: unit
+ exec:
+ cmd: npm test
+```
+
+### Multi-Step Workflow
+```yaml
+executables:
+ - verb: deploy
+ name: application
+ serial:
+ execs:
+ - ref: build app
+ - ref: test unit
+ - cmd: docker build -t myapp .
+ - cmd: kubectl apply -f k8s/
+```
+
+### Parallel Execution
+```yaml
+executables:
+ - verb: test
+ name: all
+ parallel:
+ maxThreads: 3
+ execs:
+ - ref: test unit
+ - ref: test integration
+ - ref: test e2e
+```
+
+## Integration Points
+
+### CLI Interface
+The primary interface is the `flow` CLI command:
+- Interactive TUI for browsing and discovery
+- Direct command execution
+- Workspace and configuration management
+- Secret and vault operations
+
+### Desktop Application (Upcoming)
+A companion GUI application providing:
+- Visual workflow browsing
+- Execution monitoring
+- Configuration editing
+- Documentation viewing
+
+## Common Use Cases
+
+Flow is NOT just a DevOps tool. It's a general-purpose automation platform for ANY repetitive task:
+
+### Development & Operations
+- Build, test, deploy applications
+- Manage infrastructure and environments
+- Automate CI/CD workflows
+- Code generation and scaffolding
+
+### Personal and Team Productivity
+- Standardized development setup
+- Shared workflow templates
+- Manage todo lists via APIs (Todoist, Notion, etc.)
+- Organize notes and knowledge bases
+- Automate file organization and backups
+- Schedule and run maintenance tasks
+
+### Content & Media
+- Process images, videos, documents
+- Generate reports and documentation
+- Manage blog posts and publications
+- Sync content between platforms
+
+### System Administration
+- Monitor system health and resources
+- Manage configurations and settings
+- Automate routine maintenance
+- Handle log analysis and cleanup
+
+### Custom Integrations
+- Reusable libraries of executables
+- Integration with / wrapper for existing CLI tools and APIs
+- Connect different APIs and services
+- Build personal dashboards and tools
+- Automate data synchronization
+- Create custom workflows for unique needs
+
+**Key Insight**: If someone has a repetitive task involving commands, APIs, or file operations, Flow can likely automate it.
+
+## Best Practices
+
+### Executable Design
+- Use descriptive verbs and names
+- Include clear descriptions and documentation
+- Make executables idempotent when possible
+- Handle errors gracefully with meaningful messages
+
+### Workspace Organization
+- Group related functionality in the same flow file (and namespace if it makes sense)
+- Use consistent naming conventions
+- Document workspace purpose and setup
+- Share common patterns through templates
+
+### Secret Management
+- Never commit secrets to flow files
+- Use descriptive secret references
+- Include information on required secrets in executable documentation
+
+### Workflow Composition
+- Break complex tasks into smaller, reusable executables
+- Use conditional logic for environment differences
+- Leverage parallel execution for independent tasks
diff --git a/internal/mcp/resources/file-types-guide.md b/internal/mcp/resources/file-types-guide.md
new file mode 100644
index 00000000..544795bf
--- /dev/null
+++ b/internal/mcp/resources/file-types-guide.md
@@ -0,0 +1,142 @@
+# Flow File Types Guide
+
+Flow users use three distinct types of configuration files that serve different purposes:
+
+## 1. Workspace Configuration: `flow.yaml`
+- **Location**: Root of each workspace directory
+- **Purpose**: Configure workspace-level settings
+- **Contains**: Workspace metadata, executable filters, display settings
+- **Quantity**: One per workspace
+- **Example path**: `/my-project/flow.yaml`
+
+**Example content:**
+```yaml
+displayName: "My Project"
+description: "A web application project"
+tags: ["web", "typescript"]
+```
+
+**Key fields**:
+- `displayName`: Human-readable workspace name
+- `description`: Workspace description (markdown supported)
+- `tags`: Workspace-level tags for organization
+
+## 2. Executable Definitions: *.flow files
+
+- Extensions: *.flow, *.flow.yaml, *.flow.yml
+- Purpose: Define executable tasks and workflows
+- Contains: Executable definitions with verbs, commands, parameters
+- Quantity: Multiple per workspace
+- Example paths: /my-project/build.flow, /my-project/deploy.flow.yaml, /my-project/.execs/test.flow.yml
+
+**Note**: The `.flow` extension is reserved for flow files, while `flow.yaml` is the workspace configuration file.
+Do not try to create executables in a `.flow` directory, if grouping of flow files is desired use the `.execs` directory
+if a name is needed.
+
+**Example content:**
+```yaml
+namespace: backend
+executables:
+ - verb: build
+ name: api
+ exec:
+ cmd: npm run build
+ - verb: test
+ name: unit
+ exec:
+ cmd: npm test
+```
+
+**Key fields:**
+- `namespace`: Optional logical grouping within workspace
+- `executables`: Array of executable definitions
+- Each executable has: `verb`, execution type (`exec`, `serial`, `parallel`, etc.)
+
+## 3. Flow File Templates: `.flow.tmpl`
+- **Extensions**: `.flow.tmpl`, `.flow.tmpl.yaml`, `.flow.tmpl.yml`
+- **Purpose**: Generate new flow files and workspace scaffolding
+- **Contains**: Template configuration with forms, artifacts, and flow file template
+- **Quantity**: Multiple templates can be registered
+- **Schema**: Template schema
+- **Example paths**: `/templates/k8s-app.flow.tmpl`, `/scaffolds/web-project.flow.template`
+
+**Example content:**
+```yaml
+form:
+ - key: "AppName"
+ prompt: "What is the application name?"
+ required: true
+ - key: "Namespace"
+ prompt: "What namespace should be used?"
+ default: "default"
+ - key: "Deploy"
+ prompt: "Deploy immediately after creation?"
+ type: "confirm"
+
+artifacts:
+ - srcName: "deployment.yaml"
+ asTemplate: true
+ dstName: "k8s-deployment.yaml"
+ if: form["Deploy"]
+
+template: |
+ namespace: {{ .form.Namespace }}
+ executables:
+ - verb: build
+ name: {{ .form.AppName }}
+ exec:
+ cmd: docker build -t {{ .form.AppName }} .
+ - verb: deploy
+ name: {{ .form.AppName }}
+ exec:
+ cmd: kubectl apply -f k8s-deployment.yaml
+```
+
+**Key fields:**
+- `form`: Interactive form fields for user input
+- `artifacts`: Files to copy/generate alongside the flow file
+- `template`: Go template string that generates the actual flow file
+- `preRun`/`postRun`: Optional executables to run during generation
+
+## Key Differences
+
+| Aspect | flow.yaml | .flow files | .flow.tmpl files |
+|--------|-----------|---------------------------|------------------|
+| **Purpose** | Workspace configuration | Executable definitions | Template generation |
+| **Scope** | Entire workspace | Individual tasks | Scaffolding/generation |
+| **Location** | Workspace root only | Anywhere in workspace | Template directories |
+| **Schema** | Workspace schema | FlowFile schema | Template schema |
+| **Contains** | Settings, filters, metadata | Executables, verbs, commands | Forms, templates, artifacts |
+| **Executable** | No | Yes (defines executables) | No (generates executables) |
+| **Usage** | Automatic (workspace config) | `flow ` | `flow template generate` |
+
+## Common Confusion Points
+
+### ❌ Don't Mix These Up:
+- **flow.yaml is NOT executable** - it only configures the workspace
+- **.flow files define executables** - they contain the actual tasks you run
+- **.flow.tmpl files generate other files** - they're not executed directly
+
+### ✅ Remember:
+- **flow.yaml** = "How should this workspace behave?"
+- **.flow** = "What tasks can I run?"
+- **.flow.tmpl** = "How do I generate new flow files?"
+
+## Examples Project Structure:
+```
+my-web-app/
+├── flow.yaml # Workspace config
+├── backend.flow # Backend executables
+├── frontend.flow.yaml # Frontend executables
+├── deployment/
+│ └── k8s.flow # Deployment executables
+└── templates/
+ └── microservice.flow.tmpl # Service template
+```
+
+## Typical Workflow:
+1. **Create workspace**: Add `flow.yaml` to configure workspace
+2. **Define executables**: Create `.flow` files with tasks
+3. **Use templates**: (optional) Generate new `.flow` files from `.flow.tmpl` templates
+4. **Sync workspace state**: Update flow's cache with `flow sync`
+5. **Execute tasks**: Run executables with `flow `
diff --git a/internal/mcp/resources/flowfile_schema.json b/internal/mcp/resources/flowfile_schema.json
new file mode 100644
index 00000000..ec4004e9
--- /dev/null
+++ b/internal/mcp/resources/flowfile_schema.json
@@ -0,0 +1,714 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://flowexec.io/schemas/flowfile_schema.json",
+ "title": "FlowFile",
+ "description": "Configuration for a group of Flow CLI executables. The file must have the extension `.flow`, `.flow.yaml`, or `.flow.yml` \nin order to be discovered by the CLI. It's configuration is used to define a group of executables with shared metadata \n(namespace, tags, etc). A workspace can have multiple flow files located anywhere in the workspace directory\n",
+ "type": "object",
+ "definitions": {
+ "CommonAliases": {
+ "description": "Alternate names that can be used to reference the executable in the CLI.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "CommonTags": {
+ "description": "A list of tags.\nTags can be used with list commands to filter returned data.\n",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "CommonVisibility": {
+ "description": "The visibility of the executables to Flow.\nIf not set, the visibility will default to `public`.\n\n`public` executables can be executed and listed from anywhere.\n`private` executables can be executed and listed only within their own workspace.\n`internal` executables can be executed within their own workspace but are not listed.\n`hidden` executables cannot be executed or listed.\n",
+ "type": "string",
+ "default": "public",
+ "enum": [
+ "public",
+ "private",
+ "internal",
+ "hidden"
+ ]
+ },
+ "Executable": {
+ "title": "Executable",
+ "description": "The executable schema defines the structure of an executable in the Flow CLI.\nExecutables are the building blocks of workflows and are used to define the actions that can be performed in a workspace.\n",
+ "type": "object",
+ "required": [
+ "verb"
+ ],
+ "properties": {
+ "aliases": {
+ "$ref": "#/definitions/CommonAliases",
+ "default": []
+ },
+ "description": {
+ "description": "A description of the executable.\nThis description is rendered as markdown in the interactive UI.\n",
+ "type": "string",
+ "default": ""
+ },
+ "exec": {
+ "$ref": "#/definitions/ExecutableExecExecutableType"
+ },
+ "launch": {
+ "$ref": "#/definitions/ExecutableLaunchExecutableType"
+ },
+ "name": {
+ "description": "An optional name for the executable.\n\nName is used to reference the executable in the CLI using the format `workspace/namespace:name`.\n[Verb group + Name] must be unique within the namespace of the workspace.\n",
+ "type": "string",
+ "default": ""
+ },
+ "parallel": {
+ "$ref": "#/definitions/ExecutableParallelExecutableType"
+ },
+ "render": {
+ "$ref": "#/definitions/ExecutableRenderExecutableType"
+ },
+ "request": {
+ "$ref": "#/definitions/ExecutableRequestExecutableType"
+ },
+ "serial": {
+ "$ref": "#/definitions/ExecutableSerialExecutableType"
+ },
+ "tags": {
+ "$ref": "#/definitions/CommonTags",
+ "default": []
+ },
+ "timeout": {
+ "description": "The maximum amount of time the executable is allowed to run before being terminated.\nThe timeout is specified in Go duration format (e.g. 30s, 5m, 1h).\n",
+ "type": "string"
+ },
+ "verb": {
+ "$ref": "#/definitions/ExecutableVerb",
+ "default": "exec"
+ },
+ "verbAliases": {
+ "description": "A list of aliases for the verb. This allows the executable to be referenced with multiple verbs.",
+ "type": "array",
+ "default": [],
+ "items": {
+ "$ref": "#/definitions/Verb"
+ }
+ },
+ "visibility": {
+ "$ref": "#/definitions/CommonVisibility"
+ }
+ }
+ },
+ "ExecutableArgument": {
+ "type": "object",
+ "properties": {
+ "default": {
+ "description": "The default value to use if the argument is not provided.\nIf the argument is required and no default is provided, the executable will fail.\n",
+ "type": "string",
+ "default": ""
+ },
+ "envKey": {
+ "description": "The name of the environment variable that will be assigned the value.",
+ "type": "string",
+ "default": ""
+ },
+ "flag": {
+ "description": "The flag to use when setting the argument from the command line.\nEither `flag` or `pos` must be set, but not both.\n",
+ "type": "string",
+ "default": ""
+ },
+ "outputFile": {
+ "description": "A path where the argument value will be temporarily written to disk.\nThe file will be created before execution and cleaned up afterwards.\n",
+ "type": "string",
+ "default": ""
+ },
+ "pos": {
+ "description": "The position of the argument in the command line ArgumentList. Values start at 1.\nEither `flag` or `pos` must be set, but not both.\n",
+ "type": "integer"
+ },
+ "required": {
+ "description": "If the argument is required, the executable will fail if the argument is not provided.\nIf the argument is not required, the default value will be used if the argument is not provided.\n",
+ "type": "boolean",
+ "default": false
+ },
+ "type": {
+ "description": "The type of the argument. This is used to determine how to parse the value of the argument.",
+ "type": "string",
+ "default": "string",
+ "enum": [
+ "string",
+ "int",
+ "float",
+ "bool"
+ ]
+ }
+ }
+ },
+ "ExecutableArgumentList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/ExecutableArgument"
+ }
+ },
+ "ExecutableDirectory": {
+ "description": "The directory to execute the command in.\nIf unset, the directory of the flow file will be used.\nIf set to `f:tmp`, a temporary directory will be created for the process.\nIf prefixed with `./`, the path will be relative to the current working directory.\nIf prefixed with `//`, the path will be relative to the workspace root.\nEnvironment variables in the path will be expended at runtime.\n",
+ "type": "string",
+ "default": ""
+ },
+ "ExecutableExecExecutableType": {
+ "description": "Standard executable type. Runs a command/file in a subprocess.",
+ "type": "object",
+ "properties": {
+ "args": {
+ "$ref": "#/definitions/ExecutableArgumentList"
+ },
+ "cmd": {
+ "description": "The command to execute.\nOnly one of `cmd` or `file` must be set.\n",
+ "type": "string",
+ "default": ""
+ },
+ "dir": {
+ "$ref": "#/definitions/ExecutableDirectory",
+ "default": ""
+ },
+ "file": {
+ "description": "The file to execute.\nOnly one of `cmd` or `file` must be set.\n",
+ "type": "string",
+ "default": ""
+ },
+ "logMode": {
+ "description": "The log mode to use when running the executable.\nThis can either be `hidden`, `json`, `logfmt` or `text`\n",
+ "type": "string",
+ "default": "logfmt"
+ },
+ "params": {
+ "$ref": "#/definitions/ExecutableParameterList"
+ }
+ }
+ },
+ "ExecutableLaunchExecutableType": {
+ "description": "Launches an application or opens a URI.",
+ "type": "object",
+ "required": [
+ "uri"
+ ],
+ "properties": {
+ "app": {
+ "description": "The application to launch the URI with.",
+ "type": "string",
+ "default": ""
+ },
+ "args": {
+ "$ref": "#/definitions/ExecutableArgumentList"
+ },
+ "params": {
+ "$ref": "#/definitions/ExecutableParameterList"
+ },
+ "uri": {
+ "description": "The URI to launch. This can be a file path or a web URL.",
+ "type": "string",
+ "default": ""
+ }
+ }
+ },
+ "ExecutableParallelExecutableType": {
+ "type": "object",
+ "required": [
+ "execs"
+ ],
+ "properties": {
+ "args": {
+ "$ref": "#/definitions/ExecutableArgumentList"
+ },
+ "dir": {
+ "$ref": "#/definitions/ExecutableDirectory",
+ "default": ""
+ },
+ "execs": {
+ "$ref": "#/definitions/ExecutableParallelRefConfigList",
+ "description": "A list of executables to run in parallel.\nEach executable can be a command or a reference to another executable.\n"
+ },
+ "failFast": {
+ "description": "End the parallel execution as soon as an exec exits with a non-zero status. This is the default behavior.\nWhen set to false, all execs will be run regardless of the exit status of parallel execs.\n",
+ "type": "boolean"
+ },
+ "maxThreads": {
+ "description": "The maximum number of threads to use when executing the parallel executables.",
+ "type": "integer",
+ "default": 5
+ },
+ "params": {
+ "$ref": "#/definitions/ExecutableParameterList"
+ }
+ }
+ },
+ "ExecutableParallelRefConfig": {
+ "description": "Configuration for a parallel executable.",
+ "type": "object",
+ "properties": {
+ "args": {
+ "description": "Arguments to pass to the executable.",
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ },
+ "cmd": {
+ "description": "The command to execute.\nOne of `cmd` or `ref` must be set.\n",
+ "type": "string",
+ "default": ""
+ },
+ "if": {
+ "description": "An expression that determines whether the executable should run, using the Expr language syntax.\nThe expression is evaluated at runtime and must resolve to a boolean value.\n\nThe expression has access to OS/architecture information (os, arch), environment variables (env), stored data\n(store), and context information (ctx) like workspace and paths.\n\nFor example, `os == \"darwin\"` will only run on macOS, `len(store[\"feature\"]) \u003e 0` will run if a value exists\nin the store, and `env[\"CI\"] == \"true\"` will run in CI environments.\nSee the [Expr documentation](https://expr-lang.org/docs/language-definition) for more information.\n",
+ "type": "string",
+ "default": ""
+ },
+ "ref": {
+ "$ref": "#/definitions/ExecutableRef",
+ "description": "A reference to another executable to run in serial.\nOne of `cmd` or `ref` must be set.\n",
+ "default": ""
+ },
+ "retries": {
+ "description": "The number of times to retry the executable if it fails.",
+ "type": "integer",
+ "default": 0
+ }
+ }
+ },
+ "ExecutableParallelRefConfigList": {
+ "description": "A list of executables to run in parallel. The executables can be defined by it's exec `cmd` or `ref`.\n",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/ExecutableParallelRefConfig"
+ }
+ },
+ "ExecutableParameter": {
+ "description": "A parameter is a value that can be passed to an executable and all of its sub-executables.\nOnly one of `text`, `secretRef`, `prompt`, or `file` must be set. Specifying more than one will result in an error.\n",
+ "type": "object",
+ "properties": {
+ "envKey": {
+ "description": "The name of the environment variable that will be assigned the value.",
+ "type": "string",
+ "default": ""
+ },
+ "outputFile": {
+ "description": "A path where the parameter value will be temporarily written to disk.\nThe file will be created before execution and cleaned up afterwards.\n",
+ "type": "string",
+ "default": ""
+ },
+ "prompt": {
+ "description": "A prompt to be displayed to the user when collecting an input value.",
+ "type": "string",
+ "default": ""
+ },
+ "secretRef": {
+ "description": "A reference to a secret to be passed to the executable.",
+ "type": "string",
+ "default": ""
+ },
+ "text": {
+ "description": "A static value to be passed to the executable.",
+ "type": "string",
+ "default": ""
+ }
+ }
+ },
+ "ExecutableParameterList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/ExecutableParameter"
+ }
+ },
+ "ExecutableRef": {
+ "description": "A reference to an executable.\nThe format is `\u003cverb\u003e \u003cworkspace\u003e/\u003cnamespace\u003e:\u003cexecutable name\u003e`.\nFor example, `exec ws/ns:my-workflow`.\n\n- If the workspace is not specified, the current workspace will be used.\n- If the namespace is not specified, the current namespace will be used.\n- Excluding the name will reference the executable with a matching verb but an unspecified name and namespace (e.g. `exec ws` or simply `exec`).\n",
+ "type": "string"
+ },
+ "ExecutableRenderExecutableType": {
+ "description": "Renders a markdown template file with data.",
+ "type": "object",
+ "required": [
+ "templateFile"
+ ],
+ "properties": {
+ "args": {
+ "$ref": "#/definitions/ExecutableArgumentList"
+ },
+ "dir": {
+ "$ref": "#/definitions/ExecutableDirectory",
+ "default": ""
+ },
+ "params": {
+ "$ref": "#/definitions/ExecutableParameterList"
+ },
+ "templateDataFile": {
+ "description": "The path to the JSON or YAML file containing the template data.",
+ "type": "string",
+ "default": ""
+ },
+ "templateFile": {
+ "description": "The path to the markdown template file to render.",
+ "type": "string",
+ "default": ""
+ }
+ }
+ },
+ "ExecutableRequestExecutableType": {
+ "description": "Makes an HTTP request.",
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "args": {
+ "$ref": "#/definitions/ExecutableArgumentList"
+ },
+ "body": {
+ "description": "The body of the request.",
+ "type": "string",
+ "default": ""
+ },
+ "headers": {
+ "description": "A map of headers to include in the request.",
+ "type": "object",
+ "default": {},
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "logResponse": {
+ "description": "If set to true, the response will be logged as program output.",
+ "type": "boolean",
+ "default": false
+ },
+ "method": {
+ "description": "The HTTP method to use when making the request.",
+ "type": "string",
+ "default": "GET",
+ "enum": [
+ "GET",
+ "POST",
+ "PUT",
+ "PATCH",
+ "DELETE"
+ ]
+ },
+ "params": {
+ "$ref": "#/definitions/ExecutableParameterList"
+ },
+ "responseFile": {
+ "$ref": "#/definitions/ExecutableRequestResponseFile"
+ },
+ "timeout": {
+ "description": "The timeout for the request in Go duration format (e.g. 30s, 5m, 1h).",
+ "type": "string",
+ "default": "30m0s"
+ },
+ "transformResponse": {
+ "description": "[Expr](https://expr-lang.org/docs/language-definition) expression used to transform the response before\nsaving it to a file or outputting it.\n\nThe following variables are available in the expression:\n - `status`: The response status string.\n - `code`: The response status code.\n - `body`: The response body.\n - `headers`: The response headers.\n\nFor example, to capitalize a JSON body field's value, you can use `upper(fromJSON(body)[\"field\"])`.\n",
+ "type": "string",
+ "default": ""
+ },
+ "url": {
+ "description": "The URL to make the request to.",
+ "type": "string",
+ "default": ""
+ },
+ "validStatusCodes": {
+ "description": "A list of valid status codes. If the response status code is not in this list, the executable will fail.\nIf not set, the response status code will not be checked.\n",
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ },
+ "ExecutableRequestResponseFile": {
+ "description": "Configuration for saving the response of a request to a file.",
+ "type": "object",
+ "required": [
+ "filename"
+ ],
+ "properties": {
+ "dir": {
+ "$ref": "#/definitions/ExecutableDirectory",
+ "default": ""
+ },
+ "filename": {
+ "description": "The name of the file to save the response to.",
+ "type": "string",
+ "default": ""
+ },
+ "saveAs": {
+ "description": "The format to save the response as.",
+ "type": "string",
+ "default": "raw",
+ "enum": [
+ "raw",
+ "json",
+ "indented-json",
+ "yaml",
+ "yml"
+ ]
+ }
+ }
+ },
+ "ExecutableSerialExecutableType": {
+ "description": "Executes a list of executables in serial.",
+ "type": "object",
+ "required": [
+ "execs"
+ ],
+ "properties": {
+ "args": {
+ "$ref": "#/definitions/ExecutableArgumentList"
+ },
+ "dir": {
+ "$ref": "#/definitions/ExecutableDirectory",
+ "default": ""
+ },
+ "execs": {
+ "$ref": "#/definitions/ExecutableSerialRefConfigList",
+ "description": "A list of executables to run in serial.\nEach executable can be a command or a reference to another executable.\n"
+ },
+ "failFast": {
+ "description": "End the serial execution as soon as an exec exits with a non-zero status. This is the default behavior.\nWhen set to false, all execs will be run regardless of the exit status of the previous exec.\n",
+ "type": "boolean"
+ },
+ "params": {
+ "$ref": "#/definitions/ExecutableParameterList"
+ }
+ }
+ },
+ "ExecutableSerialRefConfig": {
+ "description": "Configuration for a serial executable.",
+ "type": "object",
+ "properties": {
+ "args": {
+ "description": "Arguments to pass to the executable.",
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ },
+ "cmd": {
+ "description": "The command to execute.\nOne of `cmd` or `ref` must be set.\n",
+ "type": "string",
+ "default": ""
+ },
+ "if": {
+ "description": "An expression that determines whether the executable should run, using the Expr language syntax.\nThe expression is evaluated at runtime and must resolve to a boolean value.\n\nThe expression has access to OS/architecture information (os, arch), environment variables (env), stored data\n(store), and context information (ctx) like workspace and paths.\n\nFor example, `os == \"darwin\"` will only run on macOS, `len(store[\"feature\"]) \u003e 0` will run if a value exists\nin the store, and `env[\"CI\"] == \"true\"` will run in CI environments.\nSee the [Expr documentation](https://expr-lang.org/docs/language-definition) for more information.\n",
+ "type": "string",
+ "default": ""
+ },
+ "ref": {
+ "$ref": "#/definitions/ExecutableRef",
+ "description": "A reference to another executable to run in serial.\nOne of `cmd` or `ref` must be set.\n",
+ "default": ""
+ },
+ "retries": {
+ "description": "The number of times to retry the executable if it fails.",
+ "type": "integer",
+ "default": 0
+ },
+ "reviewRequired": {
+ "description": "If set to true, the user will be prompted to review the output of the executable before continuing.",
+ "type": "boolean",
+ "default": false
+ }
+ }
+ },
+ "ExecutableSerialRefConfigList": {
+ "description": "A list of executables to run in serial. The executables can be defined by it's exec `cmd` or `ref`.\n",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/ExecutableSerialRefConfig"
+ }
+ },
+ "ExecutableVerb": {
+ "description": "Keywords that describe the action an executable performs. Executables are configured with a single verb,\nbut core verbs have aliases that can be used interchangeably when referencing executables. This allows users \nto use the verb that best describes the action they are performing.\n\n### Default Verb Aliases\n\n- **Execution Group**: `exec`, `run`, `execute`\n- **Retrieval Group**: `get`, `fetch`, `retrieve`\n- **Display Group**: `show`, `view`, `list`\n- **Configuration Group**: `configure`, `setup`\n- **Update Group**: `update`, `upgrade`\n\n### Usage Notes\n\n1. [Verb + Name] must be unique within the namespace of the workspace.\n2. When referencing an executable, users can use any verb from the default or configured alias group.\n3. All other verbs are standalone and self-descriptive.\n\n### Examples\n\n- An executable configured with the `exec` verb can also be referenced using \"run\" or \"execute\".\n- An executable configured with `get` can also be called with \"list\", \"show\", or \"view\".\n- Operations like `backup`, `migrate`, `flush` are standalone verbs without aliases.\n- Use domain-specific verbs like `deploy`, `scale`, `tunnel` for clear operational intent.\n\nBy providing minimal aliasing with comprehensive verb coverage, flow enables natural language operations\nwhile maintaining simplicity and flexibility for diverse development and operations workflows.\n",
+ "type": "string",
+ "default": "exec",
+ "enum": [
+ "abort",
+ "activate",
+ "add",
+ "analyze",
+ "apply",
+ "archive",
+ "audit",
+ "backup",
+ "benchmark",
+ "build",
+ "bundle",
+ "check",
+ "clean",
+ "clear",
+ "commit",
+ "compile",
+ "compress",
+ "configure",
+ "connect",
+ "copy",
+ "create",
+ "deactivate",
+ "debug",
+ "decompress",
+ "decrypt",
+ "delete",
+ "deploy",
+ "destroy",
+ "disable",
+ "disconnect",
+ "edit",
+ "enable",
+ "encrypt",
+ "erase",
+ "exec",
+ "execute",
+ "export",
+ "expose",
+ "fetch",
+ "fix",
+ "flush",
+ "format",
+ "generate",
+ "get",
+ "import",
+ "index",
+ "init",
+ "inspect",
+ "install",
+ "join",
+ "kill",
+ "launch",
+ "lint",
+ "list",
+ "load",
+ "lock",
+ "login",
+ "logout",
+ "manage",
+ "merge",
+ "migrate",
+ "modify",
+ "monitor",
+ "mount",
+ "new",
+ "notify",
+ "open",
+ "package",
+ "partition",
+ "patch",
+ "pause",
+ "ping",
+ "preload",
+ "prefetch",
+ "profile",
+ "provision",
+ "publish",
+ "purge",
+ "push",
+ "queue",
+ "reboot",
+ "recover",
+ "refresh",
+ "release",
+ "reload",
+ "remove",
+ "request",
+ "reset",
+ "restart",
+ "restore",
+ "retrieve",
+ "rollback",
+ "run",
+ "save",
+ "scale",
+ "scan",
+ "schedule",
+ "seed",
+ "send",
+ "serve",
+ "set",
+ "setup",
+ "show",
+ "snapshot",
+ "start",
+ "stash",
+ "stop",
+ "tag",
+ "teardown",
+ "terminate",
+ "test",
+ "tidy",
+ "trace",
+ "transform",
+ "trigger",
+ "tunnel",
+ "undeploy",
+ "uninstall",
+ "unmount",
+ "unset",
+ "update",
+ "upgrade",
+ "validate",
+ "verify",
+ "view",
+ "watch"
+ ]
+ },
+ "FromFile": {
+ "description": "A list of `.sh` files to convert into generated executables in the file's executable group.",
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ },
+ "Ref": {},
+ "Verb": {}
+ },
+ "properties": {
+ "description": {
+ "description": "A description of the executables defined within the flow file. This description will used as a shared description\nfor all executables in the flow file.\n",
+ "type": "string",
+ "default": ""
+ },
+ "descriptionFile": {
+ "description": "A path to a markdown file that contains the description of the executables defined within the flow file.",
+ "type": "string",
+ "default": ""
+ },
+ "executables": {
+ "type": "array",
+ "default": [],
+ "items": {
+ "$ref": "#/definitions/Executable"
+ }
+ },
+ "fromFile": {
+ "$ref": "#/definitions/FromFile",
+ "description": "DEPRECATED: Use `imports` instead",
+ "default": []
+ },
+ "imports": {
+ "$ref": "#/definitions/FromFile",
+ "default": []
+ },
+ "namespace": {
+ "description": "The namespace to be given to all executables in the flow file.\nIf not set, the executables in the file will be grouped into the root (*) namespace. \nNamespaces can be reused across multiple flow files.\n\nNamespaces are used to reference executables in the CLI using the format `workspace:namespace/name`.\n",
+ "type": "string",
+ "default": ""
+ },
+ "tags": {
+ "description": "Tags to be applied to all executables defined within the flow file.",
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ },
+ "visibility": {
+ "$ref": "#/definitions/CommonVisibility"
+ }
+ }
+}
\ No newline at end of file
diff --git a/internal/mcp/resources/server-instructions.md b/internal/mcp/resources/server-instructions.md
new file mode 100644
index 00000000..7b69e4ca
--- /dev/null
+++ b/internal/mcp/resources/server-instructions.md
@@ -0,0 +1,72 @@
+# Flow MCP Server Instructions
+
+You are connected to a Flow CLI automation platform via MCP. Flow is a versatile local automation platform that helps
+users organize and execute ANY type of workflow through declarative YAML configuration -
+from development/operations tasks to personal productivity tools, content management, and custom integrations.
+
+## Essential Context to Load First
+
+Unless this information is provided to you, always start new conversations by calling the `get_info` tool.
+This provides all essential context including:
+- Current workspace, namespace, and vault context
+- File type distinctions and schemas
+- Flow concepts and platform guide
+
+You should only need to run this at the start of the conversation as the response is unlikely to change unless you or the user
+explicitly switches context or configurations.
+
+## Flow Concepts
+
+If the user prompts with any of these concepts, then they are likely referring to the flow automation platform.
+
+- Executables (the building blocks): these are automated tasks for ANY purpose.
+ - These are defined in flow files (with the *.flow or *.flow.yaml extensions)
+- Workspaces (project organization): organize executables by project, domain, or purpose (e.g. `web-dev`, `personal-automation`, `content-management`, `home-lab`
+ - The configuration for these are defined at the root of the project in a `flow.yaml` file
+
+## Best Practices
+
+### Safety
+- **Always confirm** before running `execute` with potentially destructive commands
+- **Validate YAML** before suggesting users save it to files. The JSON Schemas are provided by the `get_info` tool
+- **Check current context** before making workspace assumptions
+- **Use appropriate filters** when using tools that may return long lists. For instance, provide the appropriate arguments for the `list_executables` tool if you know the target workspace, a keyword, or verb for the executable that you're looking for.
+
+### Helpful Guidance
+- **Explain file types** when users seem confused about flow.yaml vs .flow files
+- **Suggest appropriate verbs** based on what users want to accomplish. The name and namespaces are optional but should be used if it would provide meaningful context.
+- **Recommend workspace organization** for different domains (dev, personal, content, etc.). Try to follow existing project patterns; flow files can be defined anywhere in a workspace.
+- **Show executable reference formats** when users ask about running tasks
+- **Think creatively** about automation opportunities - if they mention repetitive tasks, suggest flow solutions
+- **Encourage experimentation** - Flow's `request` type is perfect for API integrations, `launch` for opening files/apps, the secret vault for injecting secrets into executions, etc.
+
+### Common Patterns
+- **List → Detail → Execute**: Help users discover, understand, then run executables
+- **Validate → Fix → Validate**: Help users create correct YAML configurations
+- **Context → Recommend**: Use current workspace/namespace to suggest relevant actions
+
+## Response Style
+
+### JSON Tool Responses
+When tools return JSON, present it in a user-friendly way:
+- **Summarize key information** instead of dumping raw JSON; using details from the descriptions if available.
+- **Highlight important details** like dependencies or required vault secrets.
+- **Format long lists** in readable bullet points or tables
+- **Explain next steps** users might want to take
+- **Provide useful suggestion** when you notice opportunities for utilizing more of flow's robust features to simplify configurations
+
+### Error Handling
+- **Interpret error messages** from Flow CLI and explain in plain language
+- **Suggest fixes** for common configuration mistakes
+- **Guide users** through validation and correction process
+
+### Educational Approach
+- **Teach Flow concepts** while helping with immediate tasks
+- **Show examples** of executable configurations for various use cases
+- **Inspire creativity** - help users see automation opportunities they might not have considered
+
+## Tool Usage Gotchas
+- `execute` can be used to run flow executables, not arbitrary shell commands or non-exec flow commands.
+ - If an executable has `args` defined, you must provide them in the `args` field of the `execute` tool.
+ - If an executable has `params` defined where the type is `prompt`, you must provide them in the `params` field of the `execute` tool. Do this by providing a mapping of the EnvKey to the value you want to provide.
+- When running tools, be aware that the current workspace may the output / response. Switch to the workspace that you expect if it's not already set in the context.
diff --git a/internal/mcp/resources/template_schema.json b/internal/mcp/resources/template_schema.json
new file mode 100644
index 00000000..22c8a942
--- /dev/null
+++ b/internal/mcp/resources/template_schema.json
@@ -0,0 +1,172 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://flowexec.io/schemas/template_schema.json",
+ "title": "Template",
+ "description": "Configuration for a flowfile template; templates can be used to generate flow files.",
+ "type": "object",
+ "required": [
+ "template"
+ ],
+ "definitions": {
+ "Artifact": {
+ "description": "File source and destination configuration.\nGo templating from form data is supported in all fields.\n",
+ "type": "object",
+ "required": [
+ "srcName"
+ ],
+ "properties": {
+ "asTemplate": {
+ "description": "If true, the artifact will be copied as a template file. The file will be rendered using Go templating from \nthe form data. [Sprig functions](https://masterminds.github.io/sprig/) are available for use in the template.\n",
+ "type": "boolean",
+ "default": false
+ },
+ "dstDir": {
+ "description": "The directory to copy the file to. If not set, the file will be copied to the root of the flow file directory.\nThe directory will be created if it does not exist.\n",
+ "type": "string",
+ "default": ""
+ },
+ "dstName": {
+ "description": "The name of the file to copy to. If not set, the file will be copied with the same name.",
+ "type": "string",
+ "default": ""
+ },
+ "if": {
+ "description": "An expression that determines whether the the artifact should be copied, using the Expr language syntax. \nThe expression is evaluated at runtime and must resolve to a boolean value. If the condition is not met, \nthe artifact will not be copied.\n\nThe expression has access to OS/architecture information (os, arch), environment variables (env), form input \n(form), and context information (name, workspace, directory, etc.).\n\nSee the [flow documentation](https://flowexec.io/#/guide/templating) for more information.\n",
+ "type": "string",
+ "default": ""
+ },
+ "srcDir": {
+ "description": "The directory to copy the file from. \nIf not set, the file will be copied from the directory of the template file.\n",
+ "type": "string",
+ "default": ""
+ },
+ "srcName": {
+ "description": "The name of the file to copy.",
+ "type": "string"
+ }
+ }
+ },
+ "ExecutableRef": {
+ "description": "A reference to an executable.\nThe format is `\u003cverb\u003e \u003cworkspace\u003e/\u003cnamespace\u003e:\u003cexecutable name\u003e`.\nFor example, `exec ws/ns:my-workflow`.\n\n- If the workspace is not specified, the current workspace will be used.\n- If the namespace is not specified, the current namespace will be used.\n- Excluding the name will reference the executable with a matching verb but an unspecified name and namespace (e.g. `exec ws` or simply `exec`).\n",
+ "type": "string"
+ },
+ "Field": {
+ "description": "A field to be displayed to the user when generating a flow file from a template.",
+ "type": "object",
+ "required": [
+ "key",
+ "prompt"
+ ],
+ "properties": {
+ "default": {
+ "description": "The default value to use if a value is not set.",
+ "type": "string",
+ "default": ""
+ },
+ "description": {
+ "description": "A description of the field.",
+ "type": "string",
+ "default": ""
+ },
+ "group": {
+ "description": "The group to display the field in. Fields with the same group will be displayed together.",
+ "type": "integer",
+ "default": 0
+ },
+ "key": {
+ "description": "The key to associate the data with. This is used as the key in the template data map.",
+ "type": "string"
+ },
+ "prompt": {
+ "description": "A prompt to be displayed to the user when collecting an input value.",
+ "type": "string"
+ },
+ "required": {
+ "description": "If true, a value must be set. If false, the default value will be used if a value is not set.",
+ "type": "boolean",
+ "default": false
+ },
+ "type": {
+ "description": "The type of input field to display.",
+ "type": "string",
+ "default": "text",
+ "enum": [
+ "text",
+ "masked",
+ "multiline",
+ "confirm"
+ ]
+ },
+ "validate": {
+ "description": "A regular expression to validate the input value against.",
+ "type": "string",
+ "default": ""
+ }
+ }
+ },
+ "TemplateRefConfig": {
+ "description": "Configuration for a template executable.",
+ "type": "object",
+ "properties": {
+ "args": {
+ "description": "Arguments to pass to the executable.",
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ },
+ "cmd": {
+ "description": "The command to execute.\nOne of `cmd` or `ref` must be set.\n",
+ "type": "string",
+ "default": ""
+ },
+ "if": {
+ "description": "An expression that determines whether the executable should be run, using the Expr language syntax. \nThe expression is evaluated at runtime and must resolve to a boolean value. If the condition is not met, \nthe executable will be skipped.\n\nThe expression has access to OS/architecture information (os, arch), environment variables (env), form input \n(form), and context information (name, workspace, directory, etc.).\n\nSee the [flow documentation](https://flowexec.io/#/guide/templating) for more information.\n",
+ "type": "string",
+ "default": ""
+ },
+ "ref": {
+ "$ref": "#/definitions/ExecutableRef",
+ "description": "A reference to another executable to run in serial.\nOne of `cmd` or `ref` must be set.\n",
+ "default": ""
+ }
+ }
+ }
+ },
+ "properties": {
+ "artifacts": {
+ "description": "A list of artifacts to be copied after generating the flow file.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Artifact"
+ }
+ },
+ "form": {
+ "description": "Form fields to be displayed to the user when generating a flow file from a template. \nThe form will be rendered first, and the user's input can be used to render the template.\nFor example, a form field with the key `name` can be used in the template as `{{.name}}`.\n",
+ "type": "array",
+ "default": [],
+ "items": {
+ "$ref": "#/definitions/Field"
+ }
+ },
+ "postRun": {
+ "description": "A list of exec executables to run after generating the flow file.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/TemplateRefConfig"
+ }
+ },
+ "preRun": {
+ "description": "A list of exec executables to run before generating the flow file.",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/TemplateRefConfig"
+ }
+ },
+ "template": {
+ "description": "The flow file template to generate. The template must be a valid flow file after rendering.",
+ "type": "string"
+ }
+ }
+}
\ No newline at end of file
diff --git a/internal/mcp/resources/workspace_schema.json b/internal/mcp/resources/workspace_schema.json
new file mode 100644
index 00000000..19672aa7
--- /dev/null
+++ b/internal/mcp/resources/workspace_schema.json
@@ -0,0 +1,74 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://flowexec.io/schemas/workspace_schema.json",
+ "title": "Workspace",
+ "description": "Configuration for a workspace in the Flow CLI.\nThis configuration is used to define the settings for a workspace.\nEvery workspace has a workspace config file named `flow.yaml` in the root of the workspace directory.\n",
+ "type": "object",
+ "definitions": {
+ "CommonTags": {
+ "description": "A list of tags.\nTags can be used with list commands to filter returned data.\n",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "ExecutableFilter": {
+ "type": "object",
+ "properties": {
+ "excluded": {
+ "description": "A list of directories or file patterns to exclude from the executable search.\nSupports directory paths (e.g., \"node_modules/\", \"vendor/\") and glob patterns for filenames (e.g., \"*.js.flow\", \"*temp*\").\nCommon exclusions like node_modules/, vendor/, third_party/, external/, and *.js.flow are excluded by default.\n",
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ },
+ "included": {
+ "description": "A list of directories or file patterns to include in the executable search.\nSupports directory paths (e.g., \"src/\", \"scripts/\") and glob patterns for filenames (e.g., \"*.test.flow\", \"example*\").\n",
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "VerbAliases": {
+ "description": "A map of executable verbs to valid aliases. This allows you to use custom aliases for exec commands in the workspace.\nSetting this will override all of the default flow command aliases. The verbs and its mapped aliases must be valid flow verbs.\n\nIf set to an empty object, verb aliases will be disabled.\n",
+ "type": "object",
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "properties": {
+ "description": {
+ "description": "A description of the workspace. This description is rendered as markdown in the interactive UI.",
+ "type": "string",
+ "default": ""
+ },
+ "descriptionFile": {
+ "description": "A path to a markdown file that contains the description of the workspace.",
+ "type": "string",
+ "default": ""
+ },
+ "displayName": {
+ "description": "The display name of the workspace. This is used in the interactive UI.",
+ "type": "string",
+ "default": ""
+ },
+ "executables": {
+ "$ref": "#/definitions/ExecutableFilter"
+ },
+ "tags": {
+ "$ref": "#/definitions/CommonTags",
+ "default": []
+ },
+ "verbAliases": {
+ "$ref": "#/definitions/VerbAliases"
+ }
+ }
+}
\ No newline at end of file
diff --git a/internal/mcp/server.go b/internal/mcp/server.go
new file mode 100644
index 00000000..26743bf1
--- /dev/null
+++ b/internal/mcp/server.go
@@ -0,0 +1,43 @@
+package mcp
+
+import (
+ _ "embed"
+ "os"
+
+ "github.com/mark3labs/mcp-go/server"
+
+ "github.com/flowexec/flow/internal/io"
+)
+
+//go:embed resources/server-instructions.md
+var serverInstructions string
+
+type Server struct {
+ srv *server.MCPServer
+ executor CommandExecutor
+}
+
+func NewServer(executor CommandExecutor) *Server {
+ srv := server.NewMCPServer(
+ "Flow",
+ "1.0.0",
+ server.WithToolCapabilities(false),
+ server.WithPromptCapabilities(false),
+ server.WithInstructions(serverInstructions),
+ )
+ addServerTools(srv, executor)
+ addServerPrompts(srv)
+
+ return &Server{srv: srv, executor: executor}
+}
+
+func (s *Server) Run() error {
+ _ = os.Setenv(io.DisableInteractiveEnvKey, "true")
+
+ return server.ServeStdio(s.srv)
+}
+
+// GetMCPServer returns the underlying MCP server for testing purposes
+func (s *Server) GetMCPServer() *server.MCPServer {
+ return s.srv
+}
diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go
new file mode 100644
index 00000000..6e6d6360
--- /dev/null
+++ b/internal/mcp/server_test.go
@@ -0,0 +1,327 @@
+package mcp_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/mark3labs/mcp-go/client"
+ "github.com/mark3labs/mcp-go/mcp"
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ "go.uber.org/mock/gomock"
+
+ "github.com/flowexec/flow/internal/filesystem"
+ flowMcp "github.com/flowexec/flow/internal/mcp"
+ "github.com/flowexec/flow/internal/mcp/mocks"
+)
+
+func TestServer(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "MCP Server Suite")
+}
+
+var _ = Describe("MCP Server", func() {
+ var (
+ flowServer *flowMcp.Server
+ mockExecutor *mocks.MockCommandExecutor
+ mcpClient *client.Client
+ ctx context.Context
+ )
+
+ BeforeEach(func() {
+ ctx = context.Background()
+ ctrl := gomock.NewController(GinkgoT())
+ mockExecutor = mocks.NewMockCommandExecutor(ctrl)
+ flowServer = flowMcp.NewServer(mockExecutor)
+
+ var err error
+ mcpClient, err = client.NewInProcessClient(flowServer.GetMCPServer())
+ Expect(err).ToNot(HaveOccurred())
+
+ // Initialize the client
+ initRequest := mcp.InitializeRequest{
+ Params: mcp.InitializeParams{
+ ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
+ ClientInfo: mcp.Implementation{
+ Name: "flow-test-client",
+ Version: "1.0.0",
+ },
+ Capabilities: mcp.ClientCapabilities{},
+ },
+ }
+
+ _, err = mcpClient.Initialize(ctx, initRequest)
+ Expect(err).ToNot(HaveOccurred())
+ })
+
+ AfterEach(func() {
+ if mcpClient != nil {
+ mcpClient.Close()
+ }
+ })
+
+ Describe("Server Initialization", func() {
+ It("should create server successfully", func() {
+ Expect(flowServer).ToNot(BeNil())
+ Expect(mcpClient).ToNot(BeNil())
+ })
+ })
+
+ Describe("Tool Registration", func() {
+ It("should register all expected tools", func() {
+ toolsResult, err := mcpClient.ListTools(ctx, mcp.ListToolsRequest{})
+ Expect(err).ToNot(HaveOccurred())
+
+ toolNames := make([]string, len(toolsResult.Tools))
+ for i, tool := range toolsResult.Tools {
+ toolNames[i] = tool.Name
+ }
+
+ expectedTools := []string{
+ "get_info",
+ "get_workspace",
+ "list_workspaces",
+ "switch_workspace",
+ "get_executable",
+ "list_executables",
+ "execute",
+ "get_execution_logs",
+ "sync_executables",
+ }
+
+ for _, expectedTool := range expectedTools {
+ Expect(toolNames).To(ContainElement(expectedTool))
+ }
+ })
+ })
+
+ Describe("Prompt Registration", func() {
+ It("should register all expected prompts", func() {
+ promptsResult, err := mcpClient.ListPrompts(ctx, mcp.ListPromptsRequest{})
+ Expect(err).ToNot(HaveOccurred())
+
+ promptNames := make([]string, len(promptsResult.Prompts))
+ for i, prompt := range promptsResult.Prompts {
+ promptNames[i] = prompt.Name
+ }
+
+ expectedPrompts := []string{
+ "generate_executable",
+ "generate_project_executables",
+ "debug_executable",
+ "explain_flow",
+ "migrate_automation",
+ }
+
+ for _, expectedPrompt := range expectedPrompts {
+ Expect(promptNames).To(ContainElement(expectedPrompt))
+
+ result, err := mcpClient.GetPrompt(ctx, mcp.GetPromptRequest{
+ Params: mcp.GetPromptParams{
+ Name: expectedPrompt,
+ },
+ })
+ Expect(err).ToNot(HaveOccurred())
+ Expect(result.Description).ToNot(BeEmpty())
+ Expect(result.Messages).ToNot(BeEmpty())
+ Expect(result.Messages[0].Role).To(Equal(mcp.RoleUser))
+ Expect(result.Messages[0].Content).ToNot(BeNil())
+ }
+ })
+ })
+
+ Describe("Tool Execution", func() {
+ Context("get_info tool", func() {
+ It("should return flow information", func() {
+ testDir := GinkgoTB().TempDir()
+ GinkgoTB().Setenv(filesystem.FlowConfigDirEnvVar, testDir)
+ err := filesystem.InitConfig()
+ Expect(err).ToNot(HaveOccurred())
+ _, err = filesystem.LoadConfig()
+ Expect(err).ToNot(HaveOccurred())
+
+ result, err := mcpClient.CallTool(ctx, newCallToolRequest("get_info", nil))
+ Expect(err).ToNot(HaveOccurred())
+ content := getTextContent(result)
+ Expect(content).To(ContainSubstring("currentContext"))
+ Expect(content).To(ContainSubstring("usageGuides"))
+ Expect(content).To(ContainSubstring("schemas"))
+ })
+ })
+
+ Context("get_workspace tool", func() {
+ It("should call executor with correct arguments", func() {
+ expectedOutput := "get ws execution results"
+ mockExecutor.EXPECT().
+ Execute("workspace", "get", "test-workspace", "--output", "json").
+ Return(expectedOutput, nil)
+
+ result, err := mcpClient.CallTool(ctx, newCallToolRequest("get_workspace", map[string]interface{}{
+ "workspace_name": "test-workspace",
+ }))
+
+ Expect(err).ToNot(HaveOccurred())
+ Expect(getTextContent(result)).To(Equal(expectedOutput))
+ })
+ })
+
+ Context("list_workspaces tool", func() {
+ It("should call executor with correct arguments", func() {
+ expectedOutput := "list ws execution result"
+ mockExecutor.EXPECT().
+ Execute("workspace", "list", "--output", "json").
+ Return(expectedOutput, nil)
+
+ result, err := mcpClient.CallTool(ctx, newCallToolRequest("list_workspaces", nil))
+
+ Expect(err).ToNot(HaveOccurred())
+ Expect(getTextContent(result)).To(Equal(expectedOutput))
+ })
+ })
+
+ Context("switch_workspace tool", func() {
+ It("should call executor with correct arguments", func() {
+ mockExecutor.EXPECT().
+ Execute("workspace", "switch", "test-workspace").
+ Return("", nil)
+
+ _, err := mcpClient.CallTool(ctx, newCallToolRequest("switch_workspace", map[string]interface{}{
+ "workspace_name": "test-workspace",
+ }))
+
+ Expect(err).ToNot(HaveOccurred())
+ })
+ })
+
+ Context("get_executable tool", func() {
+ It("should call executor with correct arguments for full reference", func() {
+ expectedOutput := "get exec execution results"
+ mockExecutor.EXPECT().
+ Execute("browse", "--output", "json", "test", "test:test-exec").
+ Return(expectedOutput, nil)
+
+ result, err := mcpClient.CallTool(ctx, newCallToolRequest("get_executable", map[string]interface{}{
+ "executable_verb": "test",
+ "executable_id": "test:test-exec",
+ }))
+
+ Expect(err).ToNot(HaveOccurred())
+ Expect(getTextContent(result)).To(Equal(expectedOutput))
+ })
+
+ It("should handle missing executable_id", func() {
+ expectedOutput := "get exec execution results without id"
+ mockExecutor.EXPECT().
+ Execute("browse", "--output", "json", "test").
+ Return(expectedOutput, nil)
+
+ result, err := mcpClient.CallTool(ctx, newCallToolRequest("get_executable", map[string]interface{}{
+ "executable_verb": "test",
+ }))
+
+ Expect(err).ToNot(HaveOccurred())
+ Expect(getTextContent(result)).To(Equal(expectedOutput))
+ })
+ })
+
+ Context("list_executables tool", func() {
+ It("should call executor with correct arguments", func() {
+ expectedOutput := "list execs execution results"
+ mockExecutor.EXPECT().
+ Execute("browse", "--output", "json", "--workspace", "*", "--namespace", "*").
+ Return(expectedOutput, nil)
+
+ result, err := mcpClient.CallTool(ctx, newCallToolRequest("list_executables", nil))
+
+ Expect(err).ToNot(HaveOccurred())
+ Expect(getTextContent(result)).To(Equal(expectedOutput))
+ })
+ })
+
+ Context("execute tool", func() {
+ It("should call executor with provided arguments", func() {
+ expectedOutput := "execution result"
+ mockExecutor.EXPECT().
+ Execute("test", "test:test-flow", "arg1", "arg2").
+ Return(expectedOutput, nil)
+
+ result, err := mcpClient.CallTool(ctx, newCallToolRequest("execute", map[string]interface{}{
+ "executable_verb": "test",
+ "executable_id": "test:test-flow",
+ "args": "arg1 arg2",
+ }))
+
+ Expect(err).ToNot(HaveOccurred())
+ Expect(getTextContent(result)).To(Equal(expectedOutput))
+ })
+
+ It("should handle no args", func() {
+ expectedOutput := "execution result with no args"
+ mockExecutor.EXPECT().
+ Execute("test", "test:test-flow").
+ Return(expectedOutput, nil)
+
+ result, err := mcpClient.CallTool(ctx, newCallToolRequest("execute", map[string]interface{}{
+ "executable_verb": "test",
+ "executable_id": "test:test-flow",
+ }))
+
+ Expect(err).ToNot(HaveOccurred())
+ Expect(getTextContent(result)).To(Equal(expectedOutput))
+ })
+ })
+
+ Context("get_execution_logs tool", func() {
+ It("should call executor with correct arguments", func() {
+ expectedOutput := "execution logs result"
+ mockExecutor.EXPECT().
+ Execute("logs", "--output", "json", "--last").
+ Return(expectedOutput, nil)
+
+ result, err := mcpClient.CallTool(ctx, newCallToolRequest("get_execution_logs", map[string]interface{}{
+ "last": true,
+ }))
+
+ Expect(err).ToNot(HaveOccurred())
+ Expect(getTextContent(result)).To(Equal(expectedOutput))
+ })
+ })
+
+ Context("sync_executables tool", func() {
+ It("should call executor with correct arguments", func() {
+ mockExecutor.EXPECT().
+ Execute("sync").
+ Return("Synced executables", nil)
+
+ _, err := mcpClient.CallTool(ctx, newCallToolRequest("sync_executables", nil))
+
+ Expect(err).ToNot(HaveOccurred())
+ })
+ })
+ })
+})
+
+// Helper function to create a CallToolRequest
+func newCallToolRequest(name string, args map[string]interface{}) mcp.CallToolRequest {
+ if args == nil {
+ args = make(map[string]interface{})
+ }
+ return mcp.CallToolRequest{
+ Params: mcp.CallToolParams{
+ Name: name,
+ Arguments: args,
+ },
+ }
+}
+
+// Helper function to extract text content from mcp.CallToolResult
+func getTextContent(result *mcp.CallToolResult) string {
+ if result == nil || len(result.Content) == 0 {
+ return ""
+ }
+ if textContent, ok := result.Content[0].(mcp.TextContent); ok {
+ return textContent.Text
+ }
+ GinkgoTB().Fatalf("Expected text content, got %T", result.Content[0])
+ return ""
+}
diff --git a/internal/mcp/tools.go b/internal/mcp/tools.go
new file mode 100644
index 00000000..60eccdad
--- /dev/null
+++ b/internal/mcp/tools.go
@@ -0,0 +1,357 @@
+//nolint:nilerr
+package mcp
+
+import (
+ "context"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "github.com/mark3labs/mcp-go/mcp"
+ "github.com/mark3labs/mcp-go/server"
+ "github.com/pkg/errors"
+
+ "github.com/flowexec/flow/internal/filesystem"
+ "github.com/flowexec/flow/types/executable"
+)
+
+var (
+ //go:embed resources/concepts-guide.md
+ conceptsMD string
+
+ //go:embed resources/file-types-guide.md
+ fileTypesMD string
+
+ // The below schemas are updated by the docsgen tool. We embed instead of fetching to avoid unnecessary network
+ // requests and to ensure that the MCP server always has the schema needed for the current CLI version.
+
+ //go:embed resources/flowfile_schema.json
+ flowFileSchema string
+
+ //go:embed resources/template_schema.json
+ templateSchema string
+
+ //go:embed resources/workspace_schema.json
+ workspaceSchema string
+)
+
+//nolint:funlen
+func addServerTools(srv *server.MCPServer, executor CommandExecutor) {
+ // Ideally this information would just be exposed via resources but many MCP clients don't support resources.
+ // This implementation should be revisited in the future.
+ // See https://modelcontextprotocol.io/clients
+ getFlowInfo := mcp.NewTool("get_info",
+ mcp.WithDescription(
+ "Get information about flow, it's usage, and the current workflow execution context. "+
+ "This includes file JSON schemas for flow executable, template, and workspace files, concepts guides, "+
+ "and the current user configuration and state details."))
+ getFlowInfo.Annotations = mcp.ToolAnnotation{
+ Title: "Get flow information and current context",
+ DestructiveHint: boolPtr(false), ReadOnlyHint: boolPtr(true),
+ IdempotentHint: boolPtr(false), OpenWorldHint: boolPtr(false),
+ }
+ srv.AddTool(getFlowInfo, getInfoHandler)
+
+ getWorkspace := mcp.NewTool("get_workspace",
+ mcp.WithString("workspace_name", mcp.Required(), mcp.Description("Registered workspace name")),
+ mcp.WithDescription("Get details about a registered flow workspaces"),
+ )
+ getWorkspace.Annotations = mcp.ToolAnnotation{
+ Title: "Get a specific workspace by name",
+ DestructiveHint: boolPtr(false), ReadOnlyHint: boolPtr(true),
+ IdempotentHint: boolPtr(true), OpenWorldHint: boolPtr(true),
+ }
+ srv.AddTool(getWorkspace, getWorkspaceHandler(executor))
+
+ listWorkspaces := mcp.NewTool("list_workspaces",
+ mcp.WithDescription("List all registered flow workspaces"),
+ )
+ listWorkspaces.Annotations = mcp.ToolAnnotation{
+ Title: "List workspaces",
+ DestructiveHint: boolPtr(false), ReadOnlyHint: boolPtr(true),
+ IdempotentHint: boolPtr(true), OpenWorldHint: boolPtr(true),
+ }
+ srv.AddTool(listWorkspaces, listWorkspacesHandler(executor))
+
+ switchWorkspace := mcp.NewTool("switch_workspace",
+ mcp.WithString("workspace_name", mcp.Required(), mcp.Description("Registered workspace name")),
+ mcp.WithDescription("Change the current workspace"),
+ )
+ switchWorkspace.Annotations = mcp.ToolAnnotation{
+ Title: "Change the current workspace",
+ DestructiveHint: boolPtr(false), ReadOnlyHint: boolPtr(false),
+ IdempotentHint: boolPtr(true), OpenWorldHint: boolPtr(false),
+ }
+ srv.AddTool(switchWorkspace, switchWorkspaceHandler(executor))
+
+ getExecutable := mcp.NewTool("get_executable",
+ mcp.WithDescription("Get detailed information about an executable"),
+ mcp.WithString("executable_verb", mcp.Required(),
+ mcp.Enum(executable.SortedValidVerbs()...),
+ mcp.Description("Executable verb")),
+ mcp.WithString("executable_id",
+ mcp.Pattern(`^([a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)?:)?[a-zA-Z0-9_-]+$`),
+ mcp.Description("Executable ID (workspace/namespace:name or just name if using the current workspace/namespace)")),
+ )
+ getExecutable.Annotations = mcp.ToolAnnotation{
+ Title: "Get a specific executable by reference",
+ DestructiveHint: boolPtr(false), ReadOnlyHint: boolPtr(true),
+ IdempotentHint: boolPtr(true), OpenWorldHint: boolPtr(true),
+ }
+ srv.AddTool(getExecutable, getExecutableHandler(executor))
+
+ listExecutables := mcp.NewTool("list_executables",
+ mcp.WithDescription("List and filter executables across all workspaces"),
+ mcp.WithString("workspace", mcp.Description("Workspace name (optional)")),
+ mcp.WithString("namespace", mcp.Description("Namespace filter (optional)")),
+ mcp.WithString("verb", mcp.Description("Verb filter (optional)")),
+ mcp.WithString("keyword", mcp.Description("Keyword filter (optional)")),
+ mcp.WithString("tag", mcp.Description("Tag filter (optional)")),
+ )
+ listExecutables.Annotations = mcp.ToolAnnotation{
+ Title: "List executables",
+ DestructiveHint: boolPtr(false), ReadOnlyHint: boolPtr(true),
+ IdempotentHint: boolPtr(true), OpenWorldHint: boolPtr(true),
+ }
+ srv.AddTool(listExecutables, listExecutablesHandler(executor))
+
+ executeFlow := mcp.NewTool("execute",
+ mcp.WithDescription("Execute a flow executable"),
+ mcp.WithString("executable_verb", mcp.Required(),
+ mcp.Enum(executable.SortedValidVerbs()...),
+ mcp.Description("Executable verb")),
+ mcp.WithString("executable_id",
+ mcp.Pattern(`^([a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)?:)?[a-zA-Z0-9_-]+$`),
+ mcp.Description(
+ "Executable ID (workspace/namespace:name or just name if using the current workspace/namespace). "+
+ "If the executable does not have a name, you can specify just the workspace (`ws/`), namespace (`ns:`) "+
+ "both (`ws/ns:`) or neither if the current workspace/namespace should be used.")),
+ mcp.WithString("args", mcp.Description("Arguments to pass")),
+ mcp.WithBoolean("sync", mcp.Description("Sync executable changes before execution")),
+ )
+ executeFlow.Annotations = mcp.ToolAnnotation{
+ Title: "Execute executable",
+ ReadOnlyHint: boolPtr(false), DestructiveHint: boolPtr(true),
+ IdempotentHint: boolPtr(false), OpenWorldHint: boolPtr(true),
+ }
+ srv.AddTool(executeFlow, executeFlowHandler(executor))
+
+ getExecutionLogs := mcp.NewTool("get_execution_logs",
+ mcp.WithDescription("Get a list of the recent flow execution logs"),
+ mcp.WithBoolean("last", mcp.Description("Get only the last execution logs")))
+ getExecutionLogs.Annotations = mcp.ToolAnnotation{
+ Title: "Get execution logs",
+ DestructiveHint: boolPtr(false), ReadOnlyHint: boolPtr(true),
+ IdempotentHint: boolPtr(true), OpenWorldHint: boolPtr(true),
+ }
+ srv.AddTool(getExecutionLogs, getExecutionLogsHandler(executor))
+
+ sync := mcp.NewTool("sync_executables",
+ mcp.WithDescription("Sync the flow workspace and executable state"))
+ sync.Annotations = mcp.ToolAnnotation{
+ Title: "Sync executable and workspace state",
+ DestructiveHint: boolPtr(false), ReadOnlyHint: boolPtr(false),
+ IdempotentHint: boolPtr(false), OpenWorldHint: boolPtr(true),
+ }
+ srv.AddTool(sync, syncStateHandler(executor))
+}
+
+func getInfoHandler(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ cfg, err := filesystem.LoadConfig()
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to load user config")
+ }
+ cfg.SetDefaults()
+
+ wsName, err := cfg.CurrentWorkspaceName()
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to get current workspace name")
+ }
+
+ infoData := map[string]interface{}{
+ "usageGuides": map[string]interface{}{
+ "concepts": conceptsMD,
+ "fileTypes": fileTypesMD,
+ },
+ "schemas": map[string]interface{}{
+ "flowFileSchema": flowFileSchema,
+ "workspaceConfigSchema": workspaceSchema,
+ "templateFileSchema": templateSchema,
+ },
+ "currentContext": map[string]interface{}{
+ "workspace": wsName,
+ "namespace": cfg.CurrentNamespace,
+ "vault": cfg.CurrentVault,
+ "workspaceMode": cfg.WorkspaceMode,
+ "workspacePath": cfg.Workspaces[cfg.CurrentWorkspace],
+ },
+ }
+ jsonData, err := json.Marshal(infoData)
+ if err != nil {
+ return nil, err
+ }
+
+ return mcp.NewToolResultText(string(jsonData)), nil
+}
+
+func getWorkspaceHandler(executor CommandExecutor) server.ToolHandlerFunc {
+ return func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ wsName, err := request.RequireString("workspace_name")
+ if err != nil {
+ return mcp.NewToolResultError("workspace_name is required"), nil
+ }
+
+ output, err := executor.Execute("workspace", "get", wsName, "--output", "json")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("Failed to get workspaces %s: %s", wsName, output)), nil
+ }
+
+ return mcp.NewToolResultText(output), nil
+ }
+}
+
+func listWorkspacesHandler(executor CommandExecutor) server.ToolHandlerFunc {
+ return func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ output, err := executor.Execute("workspace", "list", "--output", "json")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("Failed to list workspaces: %s", output)), nil
+ }
+
+ return mcp.NewToolResultText(output), nil
+ }
+}
+
+func switchWorkspaceHandler(executor CommandExecutor) server.ToolHandlerFunc {
+ return func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ wsName, err := request.RequireString("workspace_name")
+ if err != nil {
+ return mcp.NewToolResultError("workspace_name is required"), nil
+ }
+
+ output, err := executor.Execute("workspace", "switch", wsName)
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("Failed to switch workspace to %s: %s", wsName, output)), nil
+ }
+
+ return mcp.NewToolResultText(output), nil
+ }
+}
+
+func getExecutableHandler(executor CommandExecutor) server.ToolHandlerFunc {
+ return func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ executableVerb, err := request.RequireString("executable_verb")
+ if err != nil {
+ return mcp.NewToolResultError("executable_verb is required"), nil
+ }
+ executableID := request.GetString("executable_id", "")
+
+ cmdArgs := []string{"browse", "--output", "json", executableVerb}
+ if executableID != "" {
+ cmdArgs = append(cmdArgs, executableID)
+ }
+
+ output, err := executor.Execute(cmdArgs...)
+ if err != nil {
+ ref := strings.Join([]string{executableVerb, executableID}, " ")
+ return mcp.NewToolResultError(fmt.Sprintf("Failed to get executable %s: %s", ref, output)), nil
+ }
+
+ return mcp.NewToolResultText(output), nil
+ }
+}
+
+func listExecutablesHandler(executor CommandExecutor) server.ToolHandlerFunc {
+ return func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ wsFilter := request.GetString("workspace", executable.WildcardWorkspace)
+ nsFilter := request.GetString("namespace", executable.WildcardNamespace)
+ verbFilter := request.GetString("verb", "")
+ keywordFilter := request.GetString("keyword", "")
+ tagFilter := request.GetString("tag", "")
+
+ cmdArgs := []string{"browse", "--output", "json", "--workspace", wsFilter, "--namespace", nsFilter}
+ if verbFilter != "" {
+ cmdArgs = append(cmdArgs, "--verb", verbFilter)
+ }
+ if keywordFilter != "" {
+ cmdArgs = append(cmdArgs, "--filter", keywordFilter)
+ }
+ if tagFilter != "" {
+ cmdArgs = append(cmdArgs, "--tag", tagFilter)
+ }
+
+ output, err := executor.Execute(cmdArgs...)
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("Failed to list executables: %s", output)), nil
+ }
+
+ return mcp.NewToolResultText(output), nil
+ }
+}
+
+func executeFlowHandler(executor CommandExecutor) server.ToolHandlerFunc {
+ return func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ executableVerb, err := request.RequireString("executable_verb")
+ if err != nil {
+ return mcp.NewToolResultError("executable_verb is required"), nil
+ }
+ executableID := request.GetString("executable_id", "")
+
+ args := request.GetString("args", "")
+ sync := request.GetBool("sync", false)
+
+ cmdArgs := []string{executableVerb}
+ if executableID != "" {
+ cmdArgs = append(cmdArgs, executableID)
+ }
+ if args != "" {
+ cmdArgs = append(cmdArgs, strings.Fields(args)...)
+ }
+ if sync {
+ cmdArgs = append(cmdArgs, "--sync")
+ }
+
+ output, _ := executor.Execute(cmdArgs...)
+ // if err != nil {
+ // ref := strings.Join([]string{executableVerb, executableID}, " ")
+ // return mcp.NewToolResultError(fmt.Sprintf("%s execution failed: %s", ref, output)), nil
+ // }
+
+ return mcp.NewToolResultText(output), nil
+ }
+}
+
+func getExecutionLogsHandler(executor CommandExecutor) server.ToolHandlerFunc {
+ return func(_ context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ last := request.GetBool("last", false)
+ cmdArgs := []string{"logs", "--output", "json"}
+ if last {
+ cmdArgs = append(cmdArgs, "--last")
+ }
+
+ output, err := executor.Execute(cmdArgs...)
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("Failed to get flow execution logs: %s", output)), nil
+ }
+
+ return mcp.NewToolResultText(output), nil
+ }
+}
+
+func syncStateHandler(executor CommandExecutor) server.ToolHandlerFunc {
+ return func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ output, err := executor.Execute("sync")
+ if err != nil {
+ return mcp.NewToolResultError(fmt.Sprintf("Failed to sync flow's state: %s", output)), nil
+ }
+
+ return mcp.NewToolResultText(output), nil
+ }
+}
+
+func boolPtr(b bool) *bool {
+ if b {
+ return &b
+ }
+ return nil
+}
diff --git a/tools/docsgen/json.go b/tools/docsgen/json.go
index 8ee990c0..9ecfbf85 100644
--- a/tools/docsgen/json.go
+++ b/tools/docsgen/json.go
@@ -11,13 +11,23 @@ import (
)
const (
- schemaDir = "docs/schemas"
- idBase = "https://flowexec.io/schemas"
+ schemaDir = "docs/schemas"
+ mcpSchemaDir = "internal/mcp/resources"
+ idBase = "https://flowexec.io/schemas"
)
+// The JSON schema that's bundled in to the MCP server should always match the schemas that are provided via the docs
+// site. Not all schema are needed so below is just an allowlist of schemas that we embed.
+var mcpSchemaResources = []string{
+ schema.WorkspaceDefinitionTitle,
+ schema.FlowfileDefinitionTitle,
+ schema.TemplateDefinitionTitle,
+}
+
func generateJSONSchemas() {
sm := schema.RegisteredSchemaMap()
for fn, s := range sm {
+ //nolint:nestif
if slices.Contains(TopLevelPages, fn.Title()) {
updateFileID(s, fn)
for key, value := range s.Properties {
@@ -36,19 +46,33 @@ func generateJSONSchemas() {
if err != nil {
panic(err)
}
- filePath := filepath.Clean(filepath.Join(rootDir(), schemaDir, fn.JSONSchemaFile()))
- file, err := os.Create(filePath)
- if err != nil {
+ docsPath := filepath.Join(rootDir(), schemaDir, fn.JSONSchemaFile())
+ if err := writeSchemaFile(string(schemaJSON), docsPath); err != nil {
panic(err)
}
- defer file.Close()
- if _, err := file.WriteString(string(schemaJSON)); err != nil {
- panic(err)
+ if slices.Contains(mcpSchemaResources, fn.Title()) {
+ mcpPath := filepath.Join(rootDir(), mcpSchemaDir, fn.JSONSchemaFile())
+ if err := writeSchemaFile(string(schemaJSON), mcpPath); err != nil {
+ panic(err)
+ }
}
}
}
}
+func writeSchemaFile(content, path string) error {
+ filePath := filepath.Clean(path)
+ file, err := os.Create(filePath)
+ if err != nil {
+ panic(err)
+ }
+ defer file.Close()
+ if _, err := file.WriteString(content); err != nil {
+ panic(err)
+ }
+ return nil
+}
+
func updateFileID(s *schema.JSONSchema, file schema.FileName) {
s.ID, _ = url.JoinPath(idBase, file.JSONSchemaFile())
}
diff --git a/types/config/config.go b/types/config/config.go
index 70056539..145c3d18 100644
--- a/types/config/config.go
+++ b/types/config/config.go
@@ -3,7 +3,11 @@ package config
import (
"encoding/json"
"fmt"
+ "os"
+ "path/filepath"
+ "runtime"
"slices"
+ "strings"
tuikitIO "github.com/flowexec/tuikit/io"
"golang.org/x/exp/maps"
@@ -61,6 +65,49 @@ func (c *Config) CurrentVaultName() string {
return *c.CurrentVault
}
+func (c *Config) CurrentWorkspaceName() (string, error) {
+ var ws string
+ mode := c.WorkspaceMode
+
+ switch mode {
+ case ConfigWorkspaceModeDynamic:
+ wd, err := os.Getwd()
+ if err != nil {
+ return "", err
+ }
+ if runtime.GOOS == "darwin" {
+ // On macOS, paths that start with /tmp (and some other system directories)
+ // are actually symbolic links to paths under /private. The OS may return
+ // either form of the path - e.g., both "/tmp/file" and "/private/tmp/file"
+ // refer to the same location. We strip the "/private" prefix for consistent
+ // path comparison, while preserving the original paths for filesystem operations.
+ wd = strings.TrimPrefix(wd, "/private")
+ }
+
+ for wsName, path := range c.Workspaces {
+ rel, err := filepath.Rel(filepath.Clean(path), filepath.Clean(wd))
+ if err != nil {
+ return "", err
+ }
+ if !strings.HasPrefix(rel, "..") {
+ ws = wsName
+ break
+ }
+ }
+ fallthrough
+ case ConfigWorkspaceModeFixed:
+ if ws != "" {
+ break
+ }
+ ws = c.CurrentWorkspace
+ }
+ if ws == "" {
+ return "", fmt.Errorf("current workspace not found")
+ }
+
+ return ws, nil
+}
+
func (c *Config) SendTextNotification() bool {
return c.Interactive != nil && c.Interactive.Enabled &&
c.Interactive.NotifyOnCompletion != nil && *c.Interactive.NotifyOnCompletion