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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/golang/protobuf v1.5.4
github.com/google/go-github/v84 v84.0.0
github.com/google/jsonschema-go v0.4.3
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3
Expand All @@ -24,6 +25,7 @@ require (
github.com/markbates/goth v1.81.0
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.4.3
github.com/modelcontextprotocol/go-sdk v1.6.1
github.com/nulab/autog v0.11.0
github.com/pierrecomputer/sdk/packages/code-storage-go v0.8.0
github.com/playwright-community/playwright-go v0.5200.1
Expand Down Expand Up @@ -65,6 +67,9 @@ require (
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
Expand Down Expand Up @@ -112,7 +117,7 @@ require (
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/spf13/pflag v1.0.6
github.com/subosito/gotenv v1.2.0 // indirect
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ github.com/google/go-github/v84 v84.0.0/go.mod h1:WwYL1z1ajRdlaPszjVu/47x1L0PXuk
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
Expand Down Expand Up @@ -434,6 +436,8 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU=
github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
Expand Down Expand Up @@ -497,6 +501,10 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
Expand Down Expand Up @@ -536,6 +544,8 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
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.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down Expand Up @@ -899,6 +909,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
44 changes: 44 additions & 0 deletions mcp-demo/canvas.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
apiVersion: v1
kind: Canvas
metadata:
name: "Deploy Gate (MCP demo)"
description: "Webhook-triggered deploy gate: business hours, then human approval, then deploy. Built by an AI agent through the SuperPlane MCP server."
spec:
nodes:
- id: "trigger-deploy"
name: "Deploy requested"
type: "TYPE_TRIGGER"
component: "webhook"
configuration: {}
- id: "gate-hours"
name: "Only during work hours"
type: "TYPE_ACTION"
component: "timeGate"
configuration:
days: ["monday", "tuesday", "wednesday", "thursday", "friday"]
timeRange: "09:00 - 17:00"
timezone: "1"
- id: "gate-approval"
name: "Require approval"
type: "TYPE_ACTION"
component: "approval"
configuration:
items:
- type: "anyone"
- id: "do-deploy"
name: "Run deploy"
type: "TYPE_ACTION"
component: "http"
configuration:
method: "POST"
url: "https://example.com/deploy"
edges:
- sourceId: "trigger-deploy"
targetId: "gate-hours"
channel: "default"
- sourceId: "gate-hours"
targetId: "gate-approval"
channel: "default"
- sourceId: "gate-approval"
targetId: "do-deploy"
channel: "approved"
1 change: 1 addition & 0 deletions pkg/cli/commands/canvases/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func validateAndPrintCreateResponse(

canvas := *resp.Canvas
resource := models.CanvasResourceFromCanvas(canvas)
resource.URL = BuildCanvasURL(ctx, canvas.Metadata.GetOrganizationId(), canvas.Metadata.GetId())
if !ctx.Renderer.IsText() {
return ctx.Renderer.Render(resource)
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/cli/commands/canvases/models/canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ type Canvas struct {
Metadata *openapi_client.CanvasesCanvasMetadata `json:"metadata" yaml:"metadata"`
Spec *openapi_client.CanvasesCanvasSpec `json:"spec,omitempty" yaml:"spec,omitempty"`
AutoLayout *openapi_client.CanvasesCanvasAutoLayout `json:"autoLayout,omitempty" yaml:"autoLayout,omitempty"`
// URL is the canonical web URL for the canvas. It is computed on output
// (e.g. by the create command) and omitted from YAML so round-tripping a
// canvas file stays clean.
URL string `json:"url,omitempty" yaml:"-"`
}

func ParseCanvas(raw []byte) (*Canvas, error) {
Expand Down
89 changes: 89 additions & 0 deletions pkg/cli/commands/mcp/execute.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package mcp

import (
"bytes"
"context"
"encoding/json"
"fmt"
"os/exec"
"strings"

mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
)

// makeHandler returns an MCP tool handler that executes the mapped CLI command
// by shelling out to this same binary with `--output json`.
func makeHandler(self string, path []string) mcpsdk.ToolHandler {
return func(ctx context.Context, req *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) {
args := map[string]any{}
if raw := req.Params.Arguments; len(raw) > 0 {
if err := json.Unmarshal(raw, &args); err != nil {
return errorResult(fmt.Sprintf("invalid arguments: %v", err)), nil
}
}

cmdArgs := append([]string{}, path...)

// Positional args come right after the command path.
if pos, ok := args["args"]; ok {
if list, ok := pos.([]any); ok {
for _, item := range list {
cmdArgs = append(cmdArgs, fmt.Sprint(item))
}
}
}

// Remaining keys map to flags.
for k, v := range args {
if k == "args" {
continue
}
switch vv := v.(type) {
case bool:
if vv {
cmdArgs = append(cmdArgs, "--"+k)
}
case []any:
for _, item := range vv {
cmdArgs = append(cmdArgs, "--"+k, fmt.Sprint(item))
}
default:
cmdArgs = append(cmdArgs, "--"+k, fmt.Sprint(vv))
}
}

// Always return structured output to the agent.
cmdArgs = append(cmdArgs, "--output", "json")

var stdout, stderr bytes.Buffer
c := exec.CommandContext(ctx, self, cmdArgs...)
c.Stdout = &stdout
c.Stderr = &stderr

if err := c.Run(); err != nil {
msg := strings.TrimSpace(stderr.String())
if msg == "" {
msg = err.Error()
}
return errorResult(msg), nil
}

out := stdout.String()
if strings.TrimSpace(out) == "" {
out = "(command completed successfully with no output)"
}

return &mcpsdk.CallToolResult{
Content: []mcpsdk.Content{&mcpsdk.TextContent{Text: out}},
}, nil
}
}

// errorResult reports a failed tool call back to the agent without failing the
// MCP request itself.
func errorResult(msg string) *mcpsdk.CallToolResult {
return &mcpsdk.CallToolResult{
IsError: true,
Content: []mcpsdk.Content{&mcpsdk.TextContent{Text: msg}},
}
}
42 changes: 42 additions & 0 deletions pkg/cli/commands/mcp/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package mcp

import (
"github.com/spf13/cobra"
"github.com/superplanehq/superplane/pkg/cli/core"
)

const (
serverName = "superplane"
serverVersion = "0.1.0"
)

// NewCommand returns the `superplane mcp` command. It runs a Model Context
// Protocol (MCP) server over stdio that auto-exposes every SuperPlane CLI
// command as an MCP tool, so AI coding agents (Claude Code, Codex) can drive
// SuperPlane directly.
//
// The options argument is accepted for symmetry with the other command groups
// but is unused: tools execute by shelling out to this same binary, reusing the
// CLI's existing context/auth on disk.
func NewCommand(_ core.BindOptions) *cobra.Command {
var readOnly bool

cmd := &cobra.Command{
Use: "mcp",
Short: "Run an MCP server exposing the SuperPlane CLI to AI coding agents",
Long: "Starts a Model Context Protocol (MCP) server over stdio.\n\n" +
"Every SuperPlane CLI command is auto-exposed as an MCP tool, so agents like\n" +
"Claude Code and Codex can list canvases, create workflows, inspect executions,\n" +
"and more by calling tools instead of memorizing the CLI.\n\n" +
"Add it to Claude Code with:\n" +
" claude mcp add superplane -- superplane mcp",
RunE: func(cmd *cobra.Command, args []string) error {
return runServer(cmd, readOnly)
},
}

cmd.Flags().BoolVar(&readOnly, "read-only", false,
"expose only read-only tools (list/get/describe/show/...)")

return cmd
}
37 changes: 37 additions & 0 deletions pkg/cli/commands/mcp/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package mcp

import (
"context"
"fmt"
"os"

mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/spf13/cobra"
)

// runServer builds the MCP server from the live command tree and serves it over
// stdio until the transport closes.
func runServer(cmd *cobra.Command, readOnly bool) error {
self, err := os.Executable()
if err != nil {
return fmt.Errorf("cannot locate the superplane binary: %w", err)
}

server := mcpsdk.NewServer(
&mcpsdk.Implementation{Name: serverName, Version: serverVersion},
nil,
)

// cmd.Root() is the fully assembled CLI tree at run time, so we avoid an
// import cycle with the parent cli package.
for _, t := range collectTools(cmd.Root(), readOnly) {
server.AddTool(t.tool, makeHandler(self, t.path))
}

ctx := cmd.Context()
if ctx == nil {
ctx = context.Background()
}

return server.Run(ctx, &mcpsdk.StdioTransport{})
}
Loading