Skip to content
This repository was archived by the owner on Mar 17, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions cli/cmd/mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package cmd

import (
"github.com/nitrictech/suga/cli/pkg/app"
"github.com/samber/do/v2"
"github.com/spf13/cobra"
)

// NewMcpCmd creates the mcp command
func NewMcpCmd(injector do.Injector) *cobra.Command {
mcpCmd := &cobra.Command{
Use: "mcp",
Short: "Start the Suga MCP (Model Context Protocol) server",
Long: `Start the Suga MCP server that provides access to Suga platform APIs
through the Model Context Protocol. This allows AI assistants to interact
with your Suga templates, platforms, and build manifests.

The server uses stdio transport and requires authentication via 'suga login'.`,
Run: func(cmd *cobra.Command, args []string) {
app, err := do.Invoke[*app.SugaApp](injector)
if err != nil {
cobra.CheckErr(err)
}
cobra.CheckErr(app.MCP())
},
}

return mcpCmd
}
Comment thread
tjholm marked this conversation as resolved.
1 change: 1 addition & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func NewRootCmd(injector do.Injector) *cobra.Command {
rootCmd.AddCommand(NewConfigCmd(injector))
rootCmd.AddCommand(NewTeamCmd(injector))
rootCmd.AddCommand(NewPluginCmd(injector))
rootCmd.AddCommand(NewMcpCmd(injector))

return rootCmd
}
Expand Down
3 changes: 3 additions & 0 deletions cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/hashicorp/go-getter v1.7.8
github.com/invopop/jsonschema v0.13.0
github.com/mitchellh/mapstructure v1.5.0
github.com/modelcontextprotocol/go-sdk v1.0.0
github.com/nitrictech/suga/engines v0.0.0-20250822044031-c54426614b80
github.com/nitrictech/suga/proto v0.0.0-20250822044031-c54426614b80
github.com/pkg/errors v0.9.1
Expand Down Expand Up @@ -77,6 +78,7 @@ require (
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/jsonschema-go v0.3.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
Expand Down Expand Up @@ -117,6 +119,7 @@ require (
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // 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.5.2 // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.opencensus.io v0.24.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
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=
Expand Down Expand Up @@ -1001,6 +1003,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74=
github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
Expand Down Expand Up @@ -1106,6 +1110,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
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.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
60 changes: 60 additions & 0 deletions cli/internal/api/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,63 @@ func (c *SugaApiClient) GetPublicPlatform(team, name string, revision int) (*ter
}
return &platformRevision.Revision.Content, nil
}

func (c *SugaApiClient) ListPlatforms(team string) ([]PlatformResponse, error) {
response, err := c.get(fmt.Sprintf("/api/teams/%s/platforms", url.PathEscape(team)), true)
if err != nil {
return nil, err
}
defer response.Body.Close()

if response.StatusCode != 200 {
if response.StatusCode == 404 {
return nil, ErrNotFound
}

if response.StatusCode == 401 {
return nil, ErrUnauthenticated
}

return nil, fmt.Errorf("received non 200 response from %s platforms list endpoint: %d", version.ProductName, response.StatusCode)
}

body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response from %s platforms list endpoint: %v", version.ProductName, err)
}

var platformsResponse PlatformsResponse
err = json.Unmarshal(body, &platformsResponse)
if err != nil {
return nil, fmt.Errorf("unexpected response from %s platforms list endpoint: %v", version.ProductName, err)
}
return platformsResponse.Platforms, nil
}

func (c *SugaApiClient) ListPublicPlatforms() ([]PlatformResponse, error) {
response, err := c.get("/api/public/platforms", true)
if err != nil {
return nil, err
}
defer response.Body.Close()

if response.StatusCode != 200 {
if response.StatusCode == 404 {
return nil, ErrNotFound
}

return nil, fmt.Errorf("received non 200 response from %s public platforms list endpoint: %d", version.ProductName, response.StatusCode)
}

body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response from %s public platforms list endpoint: %v", version.ProductName, err)
}

var platformsResponse PlatformsResponse
err = json.Unmarshal(body, &platformsResponse)
if err != nil {
return nil, fmt.Errorf("unexpected response from %s public platforms list endpoint: %v", version.ProductName, err)
}
return platformsResponse.Platforms, nil
}
120 changes: 120 additions & 0 deletions cli/internal/api/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,123 @@ func (c *SugaApiClient) GetPublicPluginManifest(team, lib, libVersion, name stri

return c.parsePluginManifest(body, "public")
}

func (c *SugaApiClient) ListPluginLibraries(team string) ([]PluginLibraryWithVersions, error) {
response, err := c.get(fmt.Sprintf("/api/teams/%s/plugin_libraries", url.PathEscape(team)), true)
if err != nil {
return nil, err
}
defer response.Body.Close()

if response.StatusCode != 200 {
if response.StatusCode == 404 {
return nil, ErrNotFound
}

if response.StatusCode == 401 {
return nil, ErrUnauthenticated
}

return nil, fmt.Errorf("received non 200 response from %s plugin libraries list endpoint: %d", version.ProductName, response.StatusCode)
}

body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response from %s plugin libraries list endpoint: %v", version.ProductName, err)
}

var librariesResponse ListPluginLibrariesResponse
err = json.Unmarshal(body, &librariesResponse)
if err != nil {
return nil, fmt.Errorf("unexpected response from %s plugin libraries list endpoint: %v", version.ProductName, err)
}
return librariesResponse.Libraries, nil
}

func (c *SugaApiClient) ListPublicPluginLibraries() ([]PluginLibraryWithVersions, error) {
response, err := c.get("/api/public/plugin_libraries", true)
if err != nil {
return nil, err
}
defer response.Body.Close()

if response.StatusCode != 200 {
if response.StatusCode == 404 {
return nil, ErrNotFound
}

return nil, fmt.Errorf("received non 200 response from %s public plugin libraries list endpoint: %d", version.ProductName, response.StatusCode)
}

body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response from %s public plugin libraries list endpoint: %v", version.ProductName, err)
}

var librariesResponse ListPluginLibrariesResponse
err = json.Unmarshal(body, &librariesResponse)
if err != nil {
return nil, fmt.Errorf("unexpected response from %s public plugin libraries list endpoint: %v", version.ProductName, err)
}
return librariesResponse.Libraries, nil
}

func (c *SugaApiClient) GetPluginLibraryVersion(team, lib, libVersion string) (*PluginLibraryVersion, error) {
response, err := c.get(fmt.Sprintf("/api/teams/%s/plugin_libraries/%s/versions/%s", url.PathEscape(team), url.PathEscape(lib), url.PathEscape(libVersion)), true)
if err != nil {
return nil, err
}
defer response.Body.Close()

if response.StatusCode != 200 {
if response.StatusCode == 404 {
return nil, ErrNotFound
}

if response.StatusCode == 401 {
return nil, ErrUnauthenticated
}

return nil, fmt.Errorf("received non 200 response from %s plugin library version endpoint: %d", version.ProductName, response.StatusCode)
}

body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response from %s plugin library version endpoint: %v", version.ProductName, err)
}

var versionResponse GetPluginLibraryVersionResponse
err = json.Unmarshal(body, &versionResponse)
if err != nil {
return nil, fmt.Errorf("unexpected response from %s plugin library version endpoint: %v", version.ProductName, err)
}
return versionResponse.Version, nil
}

func (c *SugaApiClient) GetPublicPluginLibraryVersion(team, lib, libVersion string) (*PluginLibraryVersion, error) {
response, err := c.get(fmt.Sprintf("/api/public/plugin_libraries/%s/%s/versions/%s", url.PathEscape(team), url.PathEscape(lib), url.PathEscape(libVersion)), true)
if err != nil {
return nil, err
}
defer response.Body.Close()

if response.StatusCode != 200 {
if response.StatusCode == 404 {
return nil, ErrNotFound
}

return nil, fmt.Errorf("received non 200 response from %s public plugin library version endpoint: %d", version.ProductName, response.StatusCode)
}

body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response from %s public plugin library version endpoint: %v", version.ProductName, err)
}

var versionResponse GetPluginLibraryVersionResponse
err = json.Unmarshal(body, &versionResponse)
if err != nil {
return nil, fmt.Errorf("unexpected response from %s public plugin library version endpoint: %v", version.ProductName, err)
}
return versionResponse.Version, nil
}
Loading