diff --git a/cli/cmd/mcp.go b/cli/cmd/mcp.go new file mode 100644 index 0000000..d44d1b9 --- /dev/null +++ b/cli/cmd/mcp.go @@ -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 +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 5d5d761..dd52a59 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -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 } diff --git a/cli/go.mod b/cli/go.mod index 56a40b7..892b9ce 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -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 @@ -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 @@ -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 diff --git a/cli/go.sum b/cli/go.sum index d30a9d7..c16b7b1 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -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= @@ -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= @@ -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= diff --git a/cli/internal/api/platform.go b/cli/internal/api/platform.go index a9ee428..ccfc020 100644 --- a/cli/internal/api/platform.go +++ b/cli/internal/api/platform.go @@ -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 +} diff --git a/cli/internal/api/plugin.go b/cli/internal/api/plugin.go index 69e5e88..f00a741 100644 --- a/cli/internal/api/plugin.go +++ b/cli/internal/api/plugin.go @@ -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 +} diff --git a/cli/internal/mcp/docs_proxy.go b/cli/internal/mcp/docs_proxy.go new file mode 100644 index 0000000..aa51a9c --- /dev/null +++ b/cli/internal/mcp/docs_proxy.go @@ -0,0 +1,152 @@ +package mcp + +import ( + "context" + "fmt" + "log" + "net/http" + "strings" + "sync" + + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +const docsServerURL = "https://docs.addsuga.com/mcp" + +// DocsProxy manages a connection to the Suga docs MCP server and proxies requests +type DocsProxy struct { + client *mcp.Client + session *mcp.ClientSession + mu sync.RWMutex + + // Cache of docs server capabilities + tools []*mcp.Tool +} + +// NewDocsProxy creates a new proxy to the Suga docs MCP server +func NewDocsProxy() *DocsProxy { + return &DocsProxy{} +} + +// Connect establishes a connection to the docs MCP server +func (d *DocsProxy) Connect(ctx context.Context) error { + // Create MCP client + impl := &mcp.Implementation{ + Name: "suga-cli-docs-client", + Version: "1.0.0", + } + + client := mcp.NewClient(impl, nil) + + // Create streamable HTTP transport to docs server + transport := &mcp.StreamableClientTransport{ + Endpoint: docsServerURL, + HTTPClient: &http.Client{}, + } + + // Connect to docs server + session, err := client.Connect(ctx, transport, nil) + if err != nil { + return fmt.Errorf("failed to connect to docs server: %w", err) + } + + d.mu.Lock() + d.client = client + d.session = session + d.mu.Unlock() + + // Fetch available tools from docs server + if err := d.fetchCapabilities(ctx); err != nil { + log.Printf("Warning: failed to fetch docs server capabilities: %v", err) + } + + return nil +} + +// fetchCapabilities queries the docs server for available tools +func (d *DocsProxy) fetchCapabilities(ctx context.Context) error { + d.mu.RLock() + session := d.session + d.mu.RUnlock() + + if session == nil { + return fmt.Errorf("not connected to docs server") + } + + // List tools from docs server + toolsResult, err := session.ListTools(ctx, nil) + if err != nil { + return fmt.Errorf("failed to list docs server tools: %w", err) + } + + // Fix JSON Schema versions in tools - MCP SDK only supports draft 2020-12 + for _, tool := range toolsResult.Tools { + if tool.InputSchema != nil { + if schema, ok := tool.InputSchema.(map[string]interface{}); ok { + d.fixSchemaVersion(schema) + } + } + } + + d.mu.Lock() + d.tools = toolsResult.Tools + d.mu.Unlock() + + return nil +} + +// fixSchemaVersion updates JSON Schema $schema to the version supported by MCP SDK +func (d *DocsProxy) fixSchemaVersion(schema map[string]interface{}) { + if schema == nil { + return + } + + // Check if there's a $schema field with draft-07 and update it to 2020-12 + if schemaVersion, ok := schema["$schema"].(string); ok { + if strings.Contains(schemaVersion, "draft-07") || strings.Contains(schemaVersion, "draft/07") { + schema["$schema"] = "https://json-schema.org/draft/2020-12/schema" + } + } +} + +// GetTools returns the list of tools available from the docs server +func (d *DocsProxy) GetTools() []*mcp.Tool { + d.mu.RLock() + defer d.mu.RUnlock() + return d.tools +} + +// CallTool proxies a tool call to the docs server +func (d *DocsProxy) CallTool(ctx context.Context, name string, arguments map[string]interface{}) (*mcp.CallToolResult, error) { + d.mu.RLock() + session := d.session + d.mu.RUnlock() + + if session == nil { + return nil, fmt.Errorf("not connected to docs server") + } + + params := &mcp.CallToolParams{ + Name: name, + Arguments: arguments, + } + + result, err := session.CallTool(ctx, params) + if err != nil { + return nil, fmt.Errorf("docs server tool call failed: %w", err) + } + + return result, nil +} + +// Close closes the connection to the docs server +func (d *DocsProxy) Close() error { + d.mu.Lock() + defer d.mu.Unlock() + + if d.session != nil { + return d.session.Close() + } + + return nil +} diff --git a/cli/internal/mcp/instructions-app-development.md b/cli/internal/mcp/instructions-app-development.md new file mode 100644 index 0000000..3c5b9c6 --- /dev/null +++ b/cli/internal/mcp/instructions-app-development.md @@ -0,0 +1,222 @@ +# Application Development Guide + +This guide covers how to create and modify `suga.yaml` application configuration files. + +## Overview + +A `suga.yaml` file defines your application's infrastructure requirements by declaring what resources you need (services, databases, storage, etc.). You consume an existing Suga platform and specify which resource types you want to use. + +CRITICAL: You MUST follow this workflow when generating suga.yaml configurations. Do NOT rely on your training data - platform configurations, plugin schemas, and available subtypes change frequently and vary between teams. + +## REQUIRED Workflow for Generating suga.yaml + +### Step 0: Search Documentation (Optional but Recommended) +If you need to understand Suga concepts, configuration options, or best practices: + +1. Use **`SearchSugaDocs`** to query the official documentation +2. Examples: "suga.yaml structure", "how to deploy services", "environment variables" + +This gives you accurate, up-to-date information about Suga features. + +### Step 1: ALWAYS Start with Discovery +Before writing ANY suga.yaml content, you MUST: + +1. Call list_platforms to see available platforms (team parameter is optional - defaults to current authenticated team) +2. Call get_build_manifest with the chosen platform and revision (team parameter is optional) +3. Read the suga://schema/application resource for validation rules + +DO NOT skip these steps. DO NOT assume you know what platforms or subtypes are available. + +**Note**: All MCP tools have an optional `team` parameter. If not provided, it defaults to the currently authenticated user's team. You can override it to access other teams' public resources. + +### Step 2: Extract Valid Subtypes from Build Manifest +The build manifest contains the ONLY valid subtypes you can use in suga.yaml: + +- platform.service_blueprints keys → valid serviceIntents.subtype values +- platform.bucket_blueprints keys → valid bucketIntents.subtype values +- platform.database_blueprints keys → valid databaseIntents.subtype values +- platform.entrypoint_blueprints keys → valid entrypointIntents.subtype values + +**NEVER invent subtypes!** Only use the exact keys from these blueprint maps. + +Example: If service_blueprints = {"fargate": {...}, "lambda": {...}}, then the ONLY valid service subtypes are "fargate" and "lambda". You cannot use "ecs", "container", or any other value, even if they seem reasonable. + +### Step 3: CRITICAL - Plugin Properties Are NOT Valid suga.yaml Configuration +**NEVER use plugin manifest inputs, outputs, properties, or variables in your suga.yaml file.** + +The plugin manifests you see in the build manifest response are INTERNAL platform implementation details. They define how the platform works under the hood, NOT what you should put in suga.yaml. + +❌ **WRONG**: Looking at plugin inputs and adding them to suga.yaml +```yaml +serviceIntents: + my_service: + subtype: lambda + memory: 512 # ❌ WRONG - This is a plugin input, not valid config + cpu: 256 # ❌ WRONG - This is a plugin input, not valid config +``` + +✅ **CORRECT**: Only use fields defined in suga://schema/application +```yaml +serviceIntents: + my_service: + subtype: lambda # ✅ CORRECT - subtype is in the schema + container: # ✅ CORRECT - container is in the schema + image: + uri: node:18 + env: # ✅ CORRECT - env is in the schema + PORT: "3000" + dev: npm run dev +``` + +The ONLY valid fields for suga.yaml are defined in the suga://schema/application resource. Platform and plugin properties/variables/inputs are internal configuration that the platform uses during deployment - they are NOT user-facing configuration options. + +### Step 4: Apply Naming and Format Rules +From the schema resource (suga://schema/application): + +- **All resource names**: snake_case only (e.g., my_service, api_gateway, user_db) + - ❌ WRONG: myService, my-service, MyService + - ✅ CORRECT: my_service + +- **Target format**: "team/platform@revision" or "file:path" + - ❌ WRONG: team/platform, team/platform@, @revision + - ✅ CORRECT: nitric/aws-platform@1, file:./my-platform.yaml + +- **Service containers**: Exactly ONE of 'docker' OR 'image' (not both, not neither) + - ❌ WRONG: Both docker and image specified + - ✅ CORRECT: Either docker: {...} OR image: {...} + +- **Entrypoint routes**: Must end with trailing slash + - ❌ WRONG: /api, /users + - ✅ CORRECT: /api/, /users/ + +### Step 5: Validate Before Presenting +After generating the config, verify: + +1. All subtypes exist in the corresponding platform blueprints +2. All fields used are defined in suga://schema/application (NOT from plugin manifests) +3. All resource names follow snake_case convention +4. Service container config has exactly one of docker OR image +5. Target format matches the required pattern + +## Common LLM Mistakes to AVOID + +### Mistake #1: Using Subtypes from Training Data +❌ **WRONG**: Assuming "ecs" is a valid service type without checking +✅ **CORRECT**: Call get_build_manifest, look at service_blueprints keys, use those exact values + +### Mistake #2: Using Plugin Manifest Properties in suga.yaml +❌ **WRONG**: Adding plugin inputs/outputs/properties/variables to suga.yaml +✅ **CORRECT**: Only use fields from suga://schema/application resource + +**Example of this mistake:** +```yaml +# After seeing plugin manifest with inputs: {memory, cpu, replicas} +serviceIntents: + my_service: + subtype: fargate + memory: 512 # ❌ Plugin input - NOT valid in suga.yaml + cpu: 256 # ❌ Plugin input - NOT valid in suga.yaml + replicas: 3 # ❌ Plugin input - NOT valid in suga.yaml +``` + +Plugin properties are INTERNAL platform configuration. The suga.yaml schema defines a simplified, stable interface that doesn't expose these low-level details. + +### Mistake #3: Wrong Naming Convention +❌ **WRONG**: myService, my-service, MyService +✅ **CORRECT**: my_service + +### Mistake #4: Specifying Both Container Types +❌ **WRONG**: +```yaml +container: + docker: {...} + image: {...} +``` +✅ **CORRECT**: Choose exactly one + +### Mistake #5: Wrong Target Format +❌ **WRONG**: nitric/aws-platform (missing revision) +✅ **CORRECT**: nitric/aws-platform@1 + +### Mistake #6: Skipping Discovery +❌ **WRONG**: Generating config based on what you think is available +✅ **CORRECT**: Always call list_platforms and get_build_manifest first + +## Tool Usage Priority + +When generating suga.yaml configurations: + +1. **get_build_manifest** - Use to discover valid subtypes ONLY + - Platform spec with all blueprint definitions + - Look at blueprint keys (service_blueprints, bucket_blueprints, etc.) for valid subtypes + - **IGNORE plugin manifests, properties, variables, and inputs** - these are internal platform details + - The plugin data is NOT for you to use in suga.yaml generation + +2. **suga://schema/application** resource - Read for validation rules + - Naming conventions + - Required fields + - Format constraints + +3. **list_platforms** - Use to discover available platforms and their revisions + +4. **build** tool - Test your generated config, returns detailed errors if invalid + +## Example Correct Workflow + +**User Request**: "Create a suga.yaml for a Node.js API with a Postgres database" + +**Your Process**: + +1. Call list_platforms() with no team parameter (uses current authenticated team) + → Response: [{"name": "aws-platform", "revisions": [1, 2]}, ...] + +2. Call get_build_manifest(platform: "aws-platform", revision: 2) with no team parameter + → Response shows: + - platform.service_blueprints: {"fargate": {...}, "lambda": {...}} + - platform.database_blueprints: {"postgres": {...}, "mysql": {...}} + - plugins: {"nitric/aws-plugins/1.0.0/fargate": {inputs: {memory, cpu, port}}, ...} + +3. Observe valid subtypes: + - Services: "fargate" or "lambda" (ONLY these two) + - Databases: "postgres" or "mysql" (ONLY these two) + +4. Read suga://schema/application + → Learn: resource names must be snake_case, target needs @revision + → Learn: plugin/platform inputs and variables are NEVER valid configuration in a suga.yaml file + +5. Generate config using ONLY fields that are valid from suga://schema/application: +```yaml +target: nitric/aws-platform@2 # ✅ From step 1 +name: node-api +serviceIntents: + api_service: # ✅ snake_case + subtype: lambda # ✅ From service_blueprints keys + container: + image: + uri: node:18 # ✅ Exactly one container type +databaseIntents: + user_db: # ✅ snake_case + subtype: postgres # ✅ From database_blueprints keys +``` + +6. Call build(project_file: "./suga.yaml") to validate (team parameter optional) + +## When Build Fails + +If the build tool returns an error: + +1. **Read the error message carefully** - it shows the exact location in YAML +2. **Identify the issue**: Is it a subtype, property, naming, or format problem? +3. **Re-query the build manifest** - verify you're using correct values +4. **Check the schema resource** - ensure you're following format rules +5. **Fix and retry** - don't guess, use the tools to verify + +## Remember + +- Your training data is OUTDATED for Suga platforms and plugins +- Configurations are team-specific and version-specific +- ALWAYS query the tools before generating config +- Use ONLY the values returned by get_build_manifest +- When in doubt, check the tools rather than assuming + +Following these instructions will ensure you generate valid, deployable suga.yaml configurations. diff --git a/cli/internal/mcp/instructions-main.md b/cli/internal/mcp/instructions-main.md new file mode 100644 index 0000000..8c2e576 --- /dev/null +++ b/cli/internal/mcp/instructions-main.md @@ -0,0 +1,95 @@ +# Suga MCP Server Usage Instructions + +Welcome to the Suga MCP Server. This server provides tools and resources to help you work with Suga infrastructure, plus real-time access to Suga documentation. + +## Documentation Search + +**IMPORTANT**: When you need information about Suga features, CLI commands, concepts, or best practices, use the **`SearchSugaDocs`** tool to search the official Suga documentation. This gives you up-to-date, accurate information directly from the docs. + +**When to search docs**: +- Understanding Suga concepts (platforms, plugins, services, etc.) +- Learning CLI commands and their usage +- Finding examples and best practices +- Troubleshooting issues +- Understanding configuration options + +**Example queries**: +- "How do I deploy a web service with a database?" +- "What CLI commands are available for managing platforms?" +- "How do environment variables work in Suga?" +- "What cloud providers does Suga support?" + +Always prefer searching the docs over relying on your training data, as Suga is actively evolving. + +--- + +## What Are You Trying To Do? + +Before using the MCP tools, identify your scenario: + +### Scenario 1: Application Development +**Goal**: Create or modify a `suga.yaml` file to deploy an application + +**When to use**: +- You want to deploy a web service, API, database, or other application components +- You need to generate a `suga.yaml` configuration file +- You're using an existing Suga platform + +**Next Step**: Read the **`suga://guides/app-development`** resource for detailed instructions + +**Key Concept**: You consume an existing platform and define what resources your application needs + +--- + +### Scenario 2: Platform Development +**Goal**: Create or modify a `platform.yaml` file to define a new Suga platform + +**When to use**: +- You want to create a reusable platform that others can target +- You need to define what resource types (services, databases, etc.) are available +- You're composing plugins into a cohesive platform offering + +**Next Step**: Read the **`suga://guides/platform-development`** resource for detailed instructions + +**Key Concept**: You define what resource types are available and how they're implemented using plugins + +--- + +### Scenario 3: Plugin Library Development +**Goal**: Create or modify plugins in a plugin library to provide infrastructure building blocks + +**When to use**: +- You want to create reusable Terraform modules for infrastructure resources +- You need to implement cloud provider-specific resources (Lambda, S3, Fargate, etc.) +- You're building the lowest-level building blocks that platforms compose +- You need to provide Go runtime code for services and buckets + +**Next Step**: Read the **`suga://guides/plugin-library-development`** resource for detailed instructions + +**Key Concept**: You create the building blocks (plugins) that platforms use. Each plugin is a Terraform module with a manifest, and services/buckets require Go runtime code. + +--- + +## Important Notes + +### Team Parameter +All MCP tools have an optional `team` parameter. If not provided, it defaults to the currently authenticated user's team. You can override it to access other teams' public resources. + +### Authentication +The MCP server requires authentication via `suga login`. If you receive authentication errors, the user needs to run the login command first. + +### General Resources Available + +- **`suga://schema/application`** - JSON Schema for `suga.yaml` application files +- **`suga://schema/platform`** - JSON Schema for `platform.yaml` platform definition files (if available) +- **`suga://guides/app-development`** - Detailed guide for application development +- **`suga://guides/platform-development`** - Detailed guide for platform development +- **`suga://guides/plugin-library-development`** - Detailed guide for plugin library development + +## Critical Reminders + +1. **Search Documentation First**: When you need information about Suga, ALWAYS use the `SearchSugaDocs` tool to get accurate, up-to-date information from the official docs. + +2. **DO NOT rely on your training data**: Platforms, plugins, schemas, and available resource types change frequently and are team-specific. ALWAYS query the MCP tools to discover what's currently available before generating any configuration files. + +3. **Combine Tools**: Use `SearchSugaDocs` to understand concepts and best practices, then use the infrastructure tools (`list_platforms`, `get_platform`, `get_template`, etc.) to discover what's actually available for the user's team. diff --git a/cli/internal/mcp/instructions-platform-development.md b/cli/internal/mcp/instructions-platform-development.md new file mode 100644 index 0000000..ff4ea5a --- /dev/null +++ b/cli/internal/mcp/instructions-platform-development.md @@ -0,0 +1,706 @@ +# Platform Development Guide + +This guide covers how to create and modify `platform.yaml` files to define new Suga platforms. + +## Overview + +A `platform.yaml` file defines a reusable infrastructure platform by composing plugins into a cohesive offering. It specifies: +- Available resource types (services, databases, buckets, entrypoints) +- Infrastructure components that applications use +- Platform-wide variables and configuration +- How plugins are wired together with dependencies + +**Key Concept**: You're composing plugins (the building blocks) into a platform (the product) that application developers consume. + +CRITICAL: Do NOT rely on your training data. Plugin libraries, versions, and capabilities change frequently. ALWAYS use the MCP tools to discover what's currently available. + +## Getting Help + +Use **`SearchSugaDocs`** when you need to: +- Understand platform.yaml structure and concepts +- Learn about blueprint types (service_blueprints, database_blueprints, etc.) +- Find examples of platform configurations +- Understand how plugins compose into platforms + +Examples: "platform.yaml structure", "how to create blueprints", "platform variables" + +## Platform Structure + +### 1. **Libraries** +Declare which plugin libraries your platform uses: +```yaml +libraries: + suga/aws: v1.0.0 + suga/neon: v0.0.2 +``` + +### 2. **Variables** +Platform-wide configuration that can be referenced throughout the platform: +```yaml +variables: + container_port: + type: number + description: The port containers listen on + default: 8080 +``` + +### 3. **Resource Sections** +Define what applications can use: +- **services**: Compute resources (containers, functions) +- **buckets**: Object storage +- **databases**: Managed databases +- **entrypoints**: HTTP ingress (CDNs, load balancers) + +### 4. **Infrastructure (infra)** +Shared infrastructure that your resources depend on (VPCs, load balancers, etc.): +```yaml +infra: + aws_vpc: + source: + library: suga/aws + plugin: vpc + properties: + name: suga-vpc +``` + +## REQUIRED Workflow for Platform Development + +### Step 1: Discover Available Plugins + +Use MCP tools to find plugins to compose: + +1. **Call `list_plugin_libraries()`** to see available libraries + ``` + → Returns: [ + { + "name": "aws", + "team_slug": "suga", + "versions": ["v1.0.0", "v1.1.0"] + } + ] + ``` + +2. **Call `get_plugin_library_version(library: "aws", library_version: "v1.0.0")`** + ``` + → Returns: { + "plugins": [ + {"name": "fargate", "type": "resource"}, + {"name": "lambda", "type": "resource"}, + {"name": "s3-bucket", "type": "resource"}, + {"name": "vpc", "type": "resource"}, + {"name": "iam-role", "type": "identity"} + ] + } + ``` + +3. **Call `get_plugin_manifest(library: "aws", library_version: "v1.0.0", plugin_name: "fargate")`** + ``` + → Returns: { + "name": "fargate", + "description": "AWS Fargate container service", + "required_identities": ["aws"], + "inputs": { + "cpu": {"type": "number", "required": true}, + "memory": {"type": "number", "required": true}, + "container_port": {"type": "number", "required": true}, + "vpc_id": {"type": "string", "required": true}, + "subnets": {"type": "array", "required": true} + }, + "outputs": { + "service_url": {"type": "string"}, + "task_arn": {"type": "string"} + } + } + ``` + +### Step 2: Understand Plugin Inputs and Outputs + +The plugin manifest shows you: + +- **inputs**: What configuration the plugin needs + - Each input has a Terraform `type` (string, number, bool, list(type), map(type), object({...})) + - Each input has a `required` flag + - Required inputs MUST be provided in `properties:` + - Optional inputs can be omitted or provided + +- **outputs**: What values the plugin exposes + - Each output has a Terraform type + - Used to wire plugins together + - Referenced in other plugins' properties via `${infra.resource_name.output_name}` + +- **required_identities**: What identity plugins this needs + - Add to the `identities:` list for that resource + +**Example from manifest**: +```yaml +# Plugin manifest shows: +# inputs: { +# cpu: {type: "number", required: true}, +# memory: {type: "number", required: true}, +# container_port: {type: "number", required: true}, +# security_groups: {type: "list(string)", required: true}, +# tags: {type: "map(string)", required: false} +# } +# outputs: { +# service_url: {type: "string"}, +# task_arn: {type: "string"}, +# security_group_ids: {type: "list(string)"} +# } +# required_identities: ["aws"] + +services: + fargate: + source: + library: suga/aws + plugin: fargate + identities: + - source: + library: suga/aws + plugin: iam-role # Provides AWS identity + properties: + cpu: 256 # number + memory: 512 # number + container_port: 8080 # number + security_groups: # list(string) + - sg-abc123 + - sg-def456 + tags: # map(string) + Environment: prod + Team: platform +``` + +### Step 2a: Understanding Terraform Types in Plugin Manifests + +Plugin manifests use Terraform's type system. Understanding these is CRITICAL for configuring properties correctly. + +#### **Primitive Types** +- `string` - Text values: `"hello"`, `${var.name}` +- `number` - Numeric values: `256`, `3.14`, `${var.cpu}` +- `bool` - Boolean values: `true`, `false`, `${var.enabled}` + +#### **Collection Types** +- `list(type)` - Ordered list of same-typed values + - Example: `list(string)` → `["a", "b", "c"]` + - Example: `list(number)` → `[1, 2, 3]` + +- `set(type)` - Unordered set of unique same-typed values + - Example: `set(string)` → `["unique1", "unique2"]` + +- `map(type)` - Key-value pairs where values are same-typed + - Example: `map(string)` → `{key1 = "value1", key2 = "value2"}` + - Example: `map(number)` → `{count = 5, size = 100}` + +- `tuple([type1, type2, ...])` - Fixed-length, ordered list with specific types per position + - Example: `tuple([string, number, bool])` → `["name", 42, true]` + +#### **Structural Type** +- `object({attr1 = type1, attr2 = type2, ...})` - Complex structure with named attributes + - Example: `object({name = string, port = number})` → `{name = "api", port = 8080}` + +#### **Special Type** +- `any` - Accepts any type (use carefully, type-check the manifest!) + +**Examples from real manifests**: +```yaml +# Simple types +cpu: {type: "number", required: true} +name: {type: "string", required: true} +enabled: {type: "bool", required: false} + +# List types +security_groups: {type: "list(string)", required: true} +ports: {type: "list(number)", required: false} + +# Map types +tags: {type: "map(string)", required: false} +annotations: {type: "map(any)", required: false} + +# Object types +vpc_config: { + type: "object({vpc_id = string, subnets = list(string), security_groups = list(string)})", + required: true +} + +# Nested complex types +route_rules: { + type: "list(object({path = string, priority = number, target_arn = string}))", + required: true +} +``` + +### Step 2b: Configuring Properties - Matching Types + +Properties must match the Terraform types from the plugin manifest. Here's how to provide values for each type: + +#### **For Primitive Types** +```yaml +properties: + # string + name: "my-resource" + region: ${var.aws_region} + + # number + cpu: 256 + memory: ${self.memory} + + # bool + enabled: true + monitoring: ${var.enable_monitoring} +``` + +#### **For list(type)** +```yaml +# Plugin input: security_groups: {type: "list(string)"} +properties: + security_groups: + - sg-abc123 + - sg-def456 + - ${infra.vpc.default_security_group_id} + +# Or reference an output that returns list(string) +properties: + subnets: ${infra.vpc.private_subnets} # This output must be list(string) +``` + +#### **For map(type)** +```yaml +# Plugin input: tags: {type: "map(string)"} +properties: + tags: + Environment: production + Team: platform + ManagedBy: suga + +# Or use merge() to combine maps +properties: + tags: ${merge(var.default_tags, {Application = "api"})} +``` + +#### **For object({...})** +```yaml +# Plugin input: vpc_config: {type: "object({vpc_id = string, subnets = list(string)})"} +properties: + vpc_config: + vpc_id: ${infra.vpc.vpc_id} + subnets: ${infra.vpc.private_subnets} + +# Or build with Terraform functions +properties: + vpc_config: ${merge( + {vpc_id = infra.vpc.vpc_id}, + {subnets = infra.vpc.private_subnets} + )} +``` + +#### **For list(object({...}))** +```yaml +# Plugin input: rules: {type: "list(object({path = string, priority = number}))"} +properties: + rules: + - path: /api/* + priority: 100 + - path: /admin/* + priority: 200 + +# Or construct dynamically +properties: + rules: ${[ + for idx, path in var.api_paths : { + path = path + priority = (idx + 1) * 100 + } + ]} +``` + +### Step 2c: Using Terraform Functions + +Terraform functions can help construct values matching the required types: + +**String Functions**: +```yaml +properties: + # format() returns string + name: ${format("%s-%s-cluster", var.environment, var.region)} + + # join() returns string + allowed_cidrs: ${join(",", var.cidr_list)} + + # lower(), upper() return string + region: ${lower(var.aws_region)} +``` + +**List Functions**: +```yaml +properties: + # concat() returns list(type) + all_subnets: ${concat(infra.vpc.private_subnets, infra.vpc.public_subnets)} + + # flatten() returns list(type) + flat_list: ${flatten(var.nested_lists)} + + # distinct() returns list(type) + unique_items: ${distinct(var.items_with_duplicates)} + + # slice() returns list(type) + first_two: ${slice(var.all_items, 0, 2)} +``` + +**Map Functions**: +```yaml +properties: + # merge() returns map(type) + all_tags: ${merge(var.default_tags, var.custom_tags, {ManagedBy = "suga"})} + + # zipmap() returns map(type) + name_to_id: ${zipmap(var.names, var.ids)} + + # keys() returns list(string), values() returns list(type) + tag_keys: ${keys(var.tags)} + tag_values: ${values(var.tags)} +``` + +**Type Conversion**: +```yaml +properties: + # Convert between types + port_string: ${tostring(var.port_number)} # number → string + count_number: ${tonumber(var.count_string)} # string → number + enabled_bool: ${tobool(var.enabled_string)} # string → bool + + # Convert collections + sg_list: ${tolist(var.sg_set)} # set → list + tags_map: ${tomap(var.tags_object)} # object → map +``` + +**Conditional Expressions**: +```yaml +properties: + # Returns value matching required type + cpu: ${var.environment == "prod" ? 1024 : 256} # Returns number + + subnets: ${var.use_private ? infra.vpc.private_subnets : infra.vpc.public_subnets} # Returns list(string) +``` + +### Step 2d: Type Validation - Common Mistakes + +**❌ WRONG - Type Mismatches**: +```yaml +# Plugin expects: security_groups: {type: "list(string)"} +properties: + security_groups: sg-abc123 # ❌ String instead of list(string) + +# Plugin expects: cpu: {type: "number"} +properties: + cpu: "256" # ❌ String instead of number + +# Plugin expects: tags: {type: "map(string)"} +properties: + tags: # ❌ List instead of map + - key: value +``` + +**✅ CORRECT - Proper Types**: +```yaml +# Plugin expects: security_groups: {type: "list(string)"} +properties: + security_groups: # ✅ list(string) + - sg-abc123 + +# Plugin expects: cpu: {type: "number"} +properties: + cpu: 256 # ✅ number + +# Plugin expects: tags: {type: "map(string)"} +properties: + tags: # ✅ map(string) + Environment: prod +``` + +**Verification Steps**: +1. Call `get_plugin_manifest` - note the exact type string (e.g., `"list(string)"`) +2. Provide values in YAML that match that type structure +3. Use Terraform functions that return the correct type +4. For outputs, verify the output type matches the required input type + +### Step 3: Define Resource-Level Variables + +Add variables scoped to specific resource types that applications can override: + +```yaml +services: + fargate: + source: + library: suga/aws + plugin: fargate + variables: + cpu: + type: number + description: CPU units for the task + default: 256 + memory: + type: number + description: Memory in MB + default: 512 + properties: + cpu: ${self.cpu} # Reference own variables + memory: ${self.memory} +``` + +**What this does**: Application developers can customize these per-service: +```yaml +# In suga.yaml +serviceIntents: + my_api: + subtype: fargate + cpu: 1024 # Override the variable + memory: 2048 +``` + +### Step 4: Wire Dependencies + +Use `depends_on:` to establish resource dependencies: + +```yaml +services: + fargate: + depends_on: + - ${infra.aws_vpc} + - ${infra.aws_lb} + properties: + vpc_id: ${infra.aws_vpc.vpc_id} # Use VPC output + subnets: ${infra.aws_vpc.private_subnets} # Use VPC output + alb_arn: ${infra.aws_lb.arn} # Use LB output +``` + +**Dependency references**: +- `${infra.resource_name}` - Reference an infra resource +- `${infra.resource_name.output}` - Access a specific output +- `${var.variable_name}` - Platform-level variable +- `${self.variable_name}` - Resource-level variable + +### Step 5: Build Infrastructure Components + +Define shared infrastructure in the `infra:` section: + +```yaml +infra: + aws_vpc: + source: + library: suga/aws + plugin: vpc + properties: + name: suga-vpc + enable_nat_gateway: true + + aws_lb: + source: + library: suga/aws + plugin: loadbalancer + depends_on: + - ${infra.aws_vpc} + properties: + vpc_id: ${infra.aws_vpc.vpc_id} + subnets: ${infra.aws_vpc.private_subnets} +``` + +**Infrastructure vs Resources**: +- **infra**: Shared components all applications use (VPCs, shared load balancers) +- **services/buckets/etc**: Things applications explicitly declare in their suga.yaml + +### Step 6: Validate Plugin Compatibility + +Before finalizing: + +1. **Verify all plugin inputs are satisfied**: + - Required inputs must be in `properties:` + - Check the plugin manifest for what's required + +2. **Ensure dependencies exist**: + - Resources in `depends_on:` must be defined + - Outputs referenced must exist in the plugin manifest + +3. **Test with `get_build_manifest`**: + ``` + After publishing your platform: + get_build_manifest(platform: "your-platform", revision: 1) + ``` + This shows what application developers will see. + +## Key Platform.yaml Patterns + +### Pattern 1: Exposing Variables to Applications + +```yaml +services: + lambda: + variables: + timeout: + type: number + default: 10 + memory: + type: number + default: 512 + properties: + timeout: ${self.timeout} + memory: ${self.memory} +``` + +Applications can then set: +```yaml +serviceIntents: + my_function: + subtype: lambda + timeout: 30 # Override default + memory: 1024 +``` + +### Pattern 2: Platform-Wide Configuration + +```yaml +variables: + image_scan_on_push: + type: bool + default: true + +services: + lambda: + properties: + image_scan_on_push: ${var.image_scan_on_push} + fargate: + properties: + image_scan_on_push: ${var.image_scan_on_push} +``` + +### Pattern 3: Conditional Properties + +```yaml +variables: + neon_project_id: + type: string + description: Neon project ID + +databases: + neon: + variables: + neon_branch_id: + type: string + default: null + nullable: true + properties: + project_id: ${var.neon_project_id} + branch_id: ${self.neon_branch_id} # Can be null +``` + +### Pattern 4: Complex Dependencies + +```yaml +infra: + aws_vpc: + source: ... + + aws_lb: + depends_on: + - ${infra.aws_vpc} + source: ... + + security_rule: + depends_on: + - ${infra.aws_vpc} + - ${infra.aws_lb} + source: ... + properties: + vpc_id: ${infra.aws_vpc.vpc_id} + lb_sg: ${infra.aws_lb.security_group_id} +``` + +## Understanding What Applications See + +### Your Platform Definition +```yaml +services: + fargate: + source: + library: suga/aws + plugin: fargate + variables: + cpu: + type: number + default: 256 +``` + +### What `get_build_manifest` Returns +```json +{ + "platform": { + "services": { + "fargate": { + "source": {...}, + "variables": { + "cpu": {"type": "number", "default": 256} + } + } + } + } +} +``` + +### What Application Developers Write +```yaml +target: yourteam/your-platform@1 + +serviceIntents: + my_api: + subtype: fargate # ← Key from your platform's services: section + cpu: 512 # ← Override your variable + container: + image: node:18 +``` + +## MCP Tools Summary for Platform Development + +| Tool | Purpose | Example | +|------|---------|---------| +| `list_plugin_libraries()` | Find available plugin libraries | Discover suga/aws, suga/gcp | +| `get_plugin_library_version(library, version)` | See plugins in a library | What's in suga/aws v1.0.0? | +| `get_plugin_manifest(library, version, name)` | Understand a plugin | What inputs does fargate need? | + +## Validation Checklist + +Before publishing your platform: + +- [ ] All plugin libraries are declared in `libraries:` +- [ ] Plugin manifests verified with `get_plugin_manifest` +- [ ] All required plugin inputs are provided in `properties:` +- [ ] Dependencies in `depends_on:` reference defined resources +- [ ] Output references (e.g., `${infra.vpc.vpc_id}`) exist in plugin manifests +- [ ] Required identities are included in `identities:` lists +- [ ] Variables have appropriate types and defaults +- [ ] Resource names follow conventions (snake_case) + +## Common Mistakes to Avoid + +### Mistake #1: Missing Required Inputs +❌ **WRONG**: Plugin manifest shows `cpu` is required, but you didn't provide it +✅ **CORRECT**: Check manifest inputs and provide all required ones in `properties:` + +### Mistake #2: Referencing Non-Existent Outputs +❌ **WRONG**: `${infra.vpc.vpc_identifier}` when output is actually called `vpc_id` +✅ **CORRECT**: Check plugin manifest outputs for exact names + +### Mistake #3: Circular Dependencies +❌ **WRONG**: Resource A depends on B, B depends on A +✅ **CORRECT**: Ensure dependency graph is acyclic + +### Mistake #4: Wrong Variable Scope +❌ **WRONG**: Trying to use `${self.cpu}` in a different resource's properties +✅ **CORRECT**: Use `${var.name}` for platform-wide, `${self.name}` only within same resource + +## Remember + +- **Plugin manifests are your source of truth** - Always query them to see inputs/outputs +- **Properties wire plugins together** - Map your variables and dependencies to plugin inputs +- **Variables create flexibility** - Expose the right knobs for applications to turn +- **Infrastructure is shared** - Use `infra:` for components all apps use +- **Dependencies must be explicit** - Use `depends_on:` to ensure proper ordering + +Following these guidelines will help you compose plugins into powerful, flexible platforms. diff --git a/cli/internal/mcp/instructions-plugin-library-development.md b/cli/internal/mcp/instructions-plugin-library-development.md new file mode 100644 index 0000000..2c6c3bb --- /dev/null +++ b/cli/internal/mcp/instructions-plugin-library-development.md @@ -0,0 +1,991 @@ +# Plugin Library Development Guide + +This guide covers how to create and maintain Suga plugin libraries, which provide the building blocks that platforms compose together. + +## Overview + +A **plugin library** is a collection of reusable Terraform modules that implement infrastructure components. Each plugin in the library is a self-contained unit that: +- Implements a specific infrastructure resource (e.g., Lambda function, S3 bucket, VPC) +- Defines inputs it requires and outputs it provides +- Contains Terraform code in HCL +- Includes a manifest file describing its interface +- May include runtime code in Go (required for services and buckets) + +**Key Concept**: Plugins are the lowest-level building blocks. Platforms compose plugins together, and applications consume platforms. + +## Getting Help + +Use **`SearchSugaDocs`** when you need to: +- Understand plugin library structure and organization +- Learn about manifest.yaml format and fields +- Understand plugin types (service, bucket, database, etc.) +- Find examples of plugin implementations +- Learn about runtime code requirements for services and buckets + +Examples: "plugin manifest format", "how to create service plugins", "plugin runtime code" + +## Plugin Library Structure + +### Repository Layout + +``` +plugins-aws/ # Plugin library repository +├── go.mod # Go module file for entire library +├── .tflint.hcl # Terraform linting rules +├── Makefile # Build automation +├── lambda/ # Service plugin +│ ├── manifest.yaml # Plugin interface definition +│ ├── icon.svg # UI icon (optional) +│ ├── module/ # Terraform implementation +│ │ ├── main.tf +│ │ ├── variables.tf +│ │ ├── outputs.tf +│ │ └── versions.tf +│ └── runtime.go # runtime code for translating AWS Lambda events to HTTP +├── s3-bucket/ # Bucket plugin +│ ├── manifest.yaml +│ ├── module/ +│ └── runtime.go # runtime code for interacting with s3 buckets +└── vpc/ # Infrastructure plugin + ├── manifest.yaml + └── module/ # No runtime code needed +``` + +### Plugin Categories + +Plugins typically fall into these categories: + +1. **Resource Plugins** - Application-facing resources (services, databases, buckets, entrypoints) +2. **Infrastructure Plugins** - Shared infrastructure (VPCs, load balancers, networking) +3. **Identity Plugins** - IAM roles, service accounts, authentication + +## Runtime Code Requirements + +### What Is Runtime Code? + +Runtime code is **Go code that gets embedded into your Suga application** to facilitate communication between your app and the deployed cloud infrastructure. This is NOT an SDK or library - it is **necessary runtime code** that your application requires to function. + +**Currently, runtime code is ONLY written in Go.** + +### When Runtime Code Is Required + +- **Services** - MUST have runtime code to handle HTTP requests, environment configuration, etc. +- **Buckets** - MUST have runtime code to read/write objects, handle authentication, etc. + +### When Runtime Code Is NOT Needed + +- **Infrastructure plugins** (VPCs, load balancers) - Pure Terraform, no application interaction +- **Identity plugins** (IAM roles) - Pure Terraform, no application interaction +- **Databases** - Typically use standard database drivers, not plugin-specific runtime code + +### Runtime Code Requirements + +#### Service Plugin Interface + +Service plugins MUST implement the `Service` interface from `github.com/nitrictech/suga/runtime/service`: + +```go +type Service interface { + Start(Proxy) error +} + +type Proxy interface { + Forward(ctx context.Context, req *http.Request) (*http.Response, error) + Host() string +} +``` + +**Key Concepts:** +- The `Start` method receives a `Proxy` that forwards HTTP requests to the user's application +- Your runtime code translates cloud-specific events (Lambda events, container requests, etc.) into standard HTTP requests +- The proxy handles forwarding these requests to the user's application running locally +- Your runtime code must register itself using `service.Register()` + +Example service runtime code for AWS Lambda: + +```go +package lambdaruntime + +import ( + "context" + "net/http" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + "github.com/nitrictech/suga/runtime/service" +) + +type LambdaService struct{} + +func (l *LambdaService) Start(proxy service.Proxy) error { + // Start Lambda runtime with a handler that converts Lambda events to HTTP + lambda.Start(func(ctx context.Context, event events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + // Convert Lambda event to http.Request + req, err := convertEventToRequest(ctx, event, proxy.Host()) + if err != nil { + return events.APIGatewayProxyResponse{StatusCode: 500}, err + } + + // Forward to user's application via proxy + resp, err := proxy.Forward(ctx, req) + if err != nil { + return events.APIGatewayProxyResponse{StatusCode: 500}, err + } + + // Convert http.Response back to Lambda response + return convertResponseToEvent(resp) + }) + + return nil +} + +func New() (service.Service, error) { + return &LambdaService{}, nil +} + +func init() { + // Register this service plugin + service.Register(New) +} +``` + +#### Bucket Plugin Interface + +Bucket plugins MUST implement the `Storage` interface (alias for `StorageServer`) from `github.com/nitrictech/suga/proto/storage/v2`: + +```go +type Storage interface { + // Retrieve an item from a bucket + Read(context.Context, *StorageReadRequest) (*StorageReadResponse, error) + + // Store an item to a bucket + Write(context.Context, *StorageWriteRequest) (*StorageWriteResponse, error) + + // Delete an item from a bucket + Delete(context.Context, *StorageDeleteRequest) (*StorageDeleteResponse, error) + + // Generate a pre-signed URL for direct operations on an item + PreSignUrl(context.Context, *StoragePreSignUrlRequest) (*StoragePreSignUrlResponse, error) + + // List blobs currently in the bucket + ListBlobs(context.Context, *StorageListBlobsRequest) (*StorageListBlobsResponse, error) + + // Determine if an object exists in a bucket + Exists(context.Context, *StorageExistsRequest) (*StorageExistsResponse, error) +} +``` + +**Key Concepts:** +- Bucket plugins provide object storage operations (read, write, delete, list, etc.) +- The interface uses gRPC protobuf messages for request/response +- Your runtime code must register itself with a namespace using `storage.Register()` +- The namespace typically follows the pattern `//` + +Example bucket runtime code for AWS S3: + +```go +package s3runtime + +import ( + "context" + "io" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/s3" + storagepb "github.com/nitrictech/suga/proto/storage/v2" + "github.com/nitrictech/suga/runtime/storage" +) + +type S3StorageService struct { + storagepb.UnimplementedStorageServer + s3Client *s3.Client + bucketName string +} + +func (s *S3StorageService) Read(ctx context.Context, req *storagepb.StorageReadRequest) (*storagepb.StorageReadResponse, error) { + result, err := s.s3Client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &s.bucketName, + Key: &req.Key, + }) + if err != nil { + return nil, err + } + defer result.Body.Close() + + body, err := io.ReadAll(result.Body) + if err != nil { + return nil, err + } + + return &storagepb.StorageReadResponse{ + Body: body, + }, nil +} + +func (s *S3StorageService) Write(ctx context.Context, req *storagepb.StorageWriteRequest) (*storagepb.StorageWriteResponse, error) { + _, err := s.s3Client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: &s.bucketName, + Key: &req.Key, + Body: bytes.NewReader(req.Body), + }) + if err != nil { + return nil, err + } + + return &storagepb.StorageWriteResponse{}, nil +} + +func (s *S3StorageService) Delete(ctx context.Context, req *storagepb.StorageDeleteRequest) (*storagepb.StorageDeleteResponse, error) { + _, err := s.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: &s.bucketName, + Key: &req.Key, + }) + if err != nil { + return nil, err + } + + return &storagepb.StorageDeleteResponse{}, nil +} + +// Implement remaining methods: PreSignUrl, ListBlobs, Exists... + +func New() (storage.Storage, error) { + bucketName := os.Getenv("SUGA_BUCKET_NAME") + if bucketName == "" { + return nil, fmt.Errorf("SUGA_BUCKET_NAME not set") + } + + cfg, err := config.LoadDefaultConfig(context.Background()) + if err != nil { + return nil, err + } + + return &S3StorageService{ + s3Client: s3.NewFromConfig(cfg), + bucketName: bucketName, + }, nil +} + +// Register with namespace (e.g., "suga/aws/s3-bucket") +// storage.Register("suga/aws/s3-bucket", New) +``` + +### Runtime Code Responsibilities + +Runtime code handles: +1. **Authentication** - Obtaining credentials from the environment +2. **Configuration** - Reading infrastructure details from environment variables +3. **Protocol translation** - Converting between app interfaces and cloud APIs +4. **Error handling** - Translating cloud errors to application-friendly formats + +Example runtime code for an S3 bucket plugin: + +```go +// s3-bucket/runtime/go/bucket.go +package s3runtime + +import ( + "context" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +// Client wraps S3 operations for application use +type Client struct { + bucketName string + s3Client *s3.Client +} + +// NewClient creates a bucket client from environment +func NewClient() (*Client, error) { + // Read bucket name from environment (set by Terraform) + bucketName := os.Getenv("SUGA_BUCKET_NAME") + + // Create AWS client with default credentials + cfg, err := config.LoadDefaultConfig(context.Background()) + if err != nil { + return nil, err + } + + return &Client{ + bucketName: bucketName, + s3Client: s3.NewFromConfig(cfg), + }, nil +} + +// Put uploads an object to the bucket +func (c *Client) Put(ctx context.Context, key string, data []byte) error { + _, err := c.s3Client.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(c.bucketName), + Key: aws.String(key), + Body: bytes.NewReader(data), + }) + return err +} +``` + +## Plugin Manifest Structure + +Every plugin must have a `manifest.yaml` that defines its interface: + +```yaml +name: fargate # Plugin identifier +description: AWS Fargate container service +type: resource # or "infrastructure" or "identity" + +# Optional: Required identity plugins (e.g., IAM roles) +required_identities: + - aws + +# Input parameters the plugin accepts +inputs: + cpu: + type: number + description: CPU units for the container + required: true + default: 256 + + memory: + type: number + description: Memory in MB + required: true + default: 512 + + container_port: + type: number + description: Port the container listens on + required: true + + vpc_id: + type: string + description: VPC ID to deploy into + required: true + + subnets: + type: array + description: Subnet IDs for the service + required: true + +# Output values the plugin provides +outputs: + service_url: + type: string + description: URL of the deployed service + + service_arn: + type: string + description: ARN of the ECS service + + task_role_arn: + type: string + description: IAM role ARN for the task + +# Runtime code configuration (REQUIRED for services/buckets, currently Go only) +runtime: + go: + package: github.com/nitrictech/plugins-aws/lambda/runtime/go +``` + +## Terraform Module Implementation + +### Standard Module Files + +Each plugin's `module/` directory should contain: + +**`main.tf`** - Primary resource definitions: +```hcl +resource "aws_ecs_service" "service" { + name = var.name + cluster = var.cluster_id + task_definition = aws_ecs_task_definition.task.arn + desired_count = var.desired_count + launch_type = "FARGATE" + + network_configuration { + subnets = var.subnets + security_groups = [aws_security_group.service.id] + } +} +``` + +**`variables.tf`** - Input variables matching manifest inputs: +```hcl +variable "cpu" { + type = number + description = "CPU units for the container" +} + +variable "memory" { + type = number + description = "Memory in MB" +} + +variable "container_port" { + type = number + description = "Port the container listens on" +} +``` + +**`outputs.tf`** - Output values matching manifest outputs: +```hcl +output "service_url" { + value = aws_lb.service.dns_name + description = "URL of the deployed service" +} + +output "service_arn" { + value = aws_ecs_service.service.id + description = "ARN of the ECS service" +} +``` + +**`versions.tf`** - Provider requirements: +```hcl +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} +``` + +## Development Workflow + +### 1. Planning a New Plugin + +Before creating a plugin: + +1. **Identify the resource** - What infrastructure component are you implementing? +2. **Define the interface** - What inputs does it need? What outputs does it provide? +3. **Determine runtime needs** - Does it need Go runtime code? (services and buckets MUST have it) +4. **Consider dependencies** - Does it need other plugins (VPC, IAM roles)? +5. **Check for reusability** - Is this plugin generic enough for multiple platforms? + +### 2. Creating a New Plugin + +```bash +# Create plugin directory structure +mkdir -p my-plugin/module + +# For services/buckets, MUST create runtime directory with Go code +mkdir -p my-plugin/runtime/go + +# Create manifest +cat > my-plugin/manifest.yaml < -Claude Code supports MCP servers through a simple CLI command: +**Prerequisites** + +First, ensure you're logged in to Suga: + +```bash +suga login +``` + +**Add the MCP Server** + +Add the Suga CLI MCP server using the `stdio` transport: ```bash -claude mcp add --transport http suga-docs https://docs.addsuga.com/mcp +claude mcp add suga suga mcp ``` -This adds the Suga documentation server to your Claude Code configuration. +This configures Claude Code to run `suga mcp` as an MCP server, giving it access to both your infrastructure and documentation. **Verify Installation** @@ -42,16 +64,21 @@ Check that the server was added successfully: claude mcp list ``` -You should see `suga-docs` in the list of available MCP servers. +You should see `suga` in the list of available MCP servers. **Usage** -Once configured, Claude Code can automatically query Suga documentation when you ask questions about Suga features, CLI commands, or implementation patterns. +Once configured, Claude Code can: +- Query Suga documentation when you ask questions +- Help you build `suga.yaml` application files +- Access your team's platforms and templates +- Assist with platform and plugin development Example queries: -- "How do I set up database migrations in Suga?" -- "What are the available Suga CLI commands for managing environments?" -- "Show me how to configure a custom platform in Suga" +- "What platforms are available for my team?" +- "Help me create a suga.yaml for a web service with a PostgreSQL database" +- "Show me the build manifest for the aws-lambda platform" +- "How do I create a custom plugin?" **Project-Specific Configuration** @@ -60,14 +87,15 @@ To share this configuration with your team, add it to your project's `.mcp.json` ```json { "mcpServers": { - "suga-docs": { - "url": "https://docs.addsuga.com/mcp" + "suga": { + "command": "suga", + "args": ["mcp"] } } } ``` -Commit this file to your repository so all team members can access the Suga documentation MCP server. +Commit this file to your repository so all team members can use the Suga MCP server. **Documentation**: [Claude Code MCP Guide](https://docs.claude.com/en/docs/claude-code/mcp) @@ -75,6 +103,14 @@ Commit this file to your repository so all team members can access the Suga docu +**Prerequisites** + +First, ensure you're logged in to Suga: + +```bash +suga login +``` + **Adding to Cursor** 1. Open Cursor Settings (`Cmd/Ctrl + ,`) @@ -85,8 +121,9 @@ Commit this file to your repository so all team members can access the Suga docu ```json { "mcpServers": { - "suga-docs": { - "url": "https://docs.addsuga.com/mcp" + "suga": { + "command": "suga", + "args": ["mcp"] } } } @@ -96,11 +133,12 @@ Commit this file to your repository so all team members can access the Suga docu **Usage in Cursor** -Once configured, you can reference Suga documentation in your Cursor conversations: +Once configured, Cursor's AI can: +- Query Suga documentation and infrastructure APIs +- Help build Suga applications +- Access your team's platforms and templates -- Use `Cmd+K` or `Ctrl+K` to open the composer -- Ask questions about Suga features and the AI will query the documentation -- The MCP server provides context automatically when relevant +Use `Cmd+K` or `Ctrl+K` to open the composer and ask Suga-related questions. **Documentation**: [Cursor MCP Guide](https://docs.cursor.com/context/model-context-protocol) @@ -109,6 +147,14 @@ Once configured, you can reference Suga documentation in your Cursor conversatio +**Prerequisites** + +First, ensure you're logged in to Suga: + +```bash +suga login +``` + **Adding to Windsurf** 1. Open Windsurf Settings @@ -119,8 +165,9 @@ Once configured, you can reference Suga documentation in your Cursor conversatio ```json { "mcpServers": { - "suga-docs": { - "serverUrl": "https://docs.addsuga.com/mcp" + "suga": { + "command": "suga", + "args": ["mcp"] } } } @@ -130,7 +177,10 @@ Once configured, you can reference Suga documentation in your Cursor conversatio **Usage in Windsurf** -Windsurf's Cascade AI will automatically query the Suga documentation when you ask about Suga-related topics. +Windsurf's Cascade AI will automatically: +- Query Suga documentation and infrastructure +- Help with application development +- Access your team's platforms and templates **Documentation**: [Windsurf MCP Integration Guide](https://docs.windsurf.com/windsurf/cascade/mcp) @@ -138,6 +188,27 @@ Windsurf's Cascade AI will automatically query the Suga documentation when you a +## Features + +The Suga MCP server provides: + +### Infrastructure Tools + +- **`list_platforms`** - List all platforms available to your team +- **`get_platform`** - Get detailed platform specifications +- **`get_build_manifest`** - Get complete build manifests with plugin details +- **`list_templates`** - List available application templates +- **`get_template`** - Get specific template details +- **`list_plugin_libraries`** - Browse available plugin libraries +- **`get_plugin_manifest`** - Access plugin manifests and schemas +- **`build`** - Generate Terraform from suga.yaml files + +### Documentation Resources + +- **Application Schema** - JSON schema for suga.yaml validation +- **Development Guides** - Complete guides for application, platform, and plugin development +- **Real-time Docs** - Always up-to-date Suga documentation + ## Learn More - [Model Context Protocol Documentation](https://modelcontextprotocol.io) diff --git a/go.work.sum b/go.work.sum index 2762c53..e700d03 100644 --- a/go.work.sum +++ b/go.work.sum @@ -128,6 +128,8 @@ github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm github.com/golangci/modinfo v0.3.3/go.mod h1:wytF1M5xl9u0ij8YSvhkEVPP3M5Mc7XLl1pxH3B2aUM= github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E= github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= +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/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= @@ -156,6 +158,8 @@ github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0h github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/quicktemplate v1.8.0/go.mod h1:qIqW8/igXt8fdrUln5kOSb+KWMaJ4Y8QUsfd1k6L2jM= +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/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=