package server import ( "context" "fmt" "os" "reflect" "github.com/scaleway/scaleway-cli/v2/core" "github.com/scaleway/scaleway-sdk-go/scw" ) func McpServerServe() *core.Command { type serveArgs struct { Transport string `json:"transport"` Address string `json:"address"` ReadOnly bool `json:"read-only"` Namespaces string `json:"namespaces"` Resources string `json:"resources"` Verbs string `json:"verbs"` } return &core.Command{ Groups: []string{"utility"}, Namespace: "mcp", Resource: "server", Verb: "serve", Short: "Start the MCP server", Long: "Runs the MCP server, exposing all CLI commands as MCP tools for AI assistants. Supports stdio (default) and streamable HTTP transports.", AllowAnonymousClient: true, DisableTelemetry: true, ArgsType: reflect.TypeFor[serveArgs](), Examples: []*core.Example{ { Short: "Start the MCP server with stdio transport (default)", Raw: `scw mcp server serve`, }, { Short: "Start the MCP server in read-only mode (only get/list operations)", Raw: `scw mcp server serve --read-only`, }, { Short: "Only serve commands from specific namespaces", Raw: `scw mcp server serve namespaces=instance,iam,object`, }, { Short: "Only serve commands from specific resources", Raw: `scw mcp server serve resources=server,volume,bucket`, }, { Short: "Only serve commands with specific verbs", Raw: `scw mcp server serve verbs=get,list,create`, }, { Short: "Combine filters to serve only instance server get/list commands", Raw: `scw mcp server serve namespaces=instance resources=server verbs=get,list`, }, }, ArgSpecs: core.ArgSpecs{ { Name: "transport", Short: "Transport mode: stdio (default) or streamable-http", Required: false, Positional: false, Default: core.DefaultValueSetter("stdio"), }, { Name: "address", Short: "Address to bind for streamable-http transports (e.g., :8080)", Required: false, Positional: false, Default: core.DefaultValueSetter(":8080"), }, { Name: "read-only", Short: "Only register read-only commands (get, list operations)", Required: false, Positional: false, Default: core.DefaultValueSetter("false"), }, { Name: "namespaces", Short: "Only serve commands from specified namespaces (comma-separated)", Required: false, Positional: false, }, { Name: "resources", Short: "Only serve commands from specified resources (comma-separated)", Required: false, Positional: false, }, { Name: "verbs", Short: "Only serve commands with specified verbs (comma-separated)", Required: false, Positional: false, }, }, Run: func(ctx context.Context, argsI any) (any, error) { args := argsI.(*serveArgs) meta := core.ExtractMeta(ctx) // Reload the client with the profile to ensure proper authentication if err := core.ReloadClient(ctx); err != nil { return nil, fmt.Errorf("failed to initialize authenticated client: %w", err) } // Log startup information to stderr fmt.Fprintf( os.Stderr, "Starting MCP server version %s\n", meta.BuildInfo.Version.String(), ) fmt.Fprintf(os.Stderr, "Using profile: %s\n", meta.ProfileFlag) fmt.Fprintf(os.Stderr, "Config path: %s\n", meta.ConfigPathFlag) fmt.Fprintf(os.Stderr, "Transport mode: %s\n", args.Transport) fmt.Fprintf(os.Stderr, "Read-only mode: %v\n", args.ReadOnly) if len(SplitArg(args.Namespaces)) > 0 { fmt.Fprintf(os.Stderr, "Enabled namespaces: %v\n", SplitArg(args.Namespaces)) } if len(SplitArg(args.Resources)) > 0 { fmt.Fprintf(os.Stderr, "Enabled resources: %v\n", SplitArg(args.Resources)) } if len(SplitArg(args.Verbs)) > 0 { fmt.Fprintf(os.Stderr, "Enabled verbs: %v\n", SplitArg(args.Verbs)) } // Get all CLI commands commands := core.ExtractCommands(ctx) cliCommands := commands.GetAll() // Copy OverrideEnv from context for _, envKey := range []string{"HOME", "PATH", scw.ScwAccessKeyEnv, scw.ScwSecretKeyEnv, scw.ScwDefaultOrganizationIDEnv, scw.ScwDefaultProjectIDEnv, scw.ScwDefaultRegionEnv, scw.ScwDefaultZoneEnv} { if val := core.ExtractEnv(ctx, envKey); val != "" { meta.OverrideEnv[envKey] = val } } // Step 1: Filter commands based on the given config filteredCommands := FilterCommands(cliCommands, CommandFilterConfig{ ReadOnly: args.ReadOnly, EnabledNamespaces: SplitArg(args.Namespaces), EnabledResources: SplitArg(args.Resources), EnabledVerbs: SplitArg(args.Verbs), }) // Step 2: Inject meta into context for tool/resources execution ctx = core.InjectMeta(ctx, meta) buildInfo := core.ExtractBuildInfo(ctx) // Step 3: Create the MCP server with pre-filtered commands mcpServer := NewMCPServer(filteredCommands, *buildInfo) // Step 2: Serve the MCP server with the specified transport return mcpServer.Serve(ctx, args.Transport, args.Address) }, ExcludeFromMCP: true, // Skip mcp namespaces to avoid recursive server calls } } func McpServer() *core.Command { return &core.Command{ Groups: []string{"utility"}, Short: `MCP server management commands`, Long: `Commands for managing the MCP server that exposes Scaleway CLI commands as AI tools.`, Namespace: "mcp", Resource: "server", ExcludeFromMCP: true, } } func McpServerListResources() *core.Command { type listResourcesArgs struct { Namespaces string `json:"namespaces"` Resources string `json:"resources"` ReadOnly bool `json:"read-only"` } return &core.Command{ Groups: []string{"utility"}, Namespace: "mcp", Resource: "server", Verb: "list-resources", Short: "List available MCP resources", Long: "Lists all CLI commands that would be exposed as MCP resources by the server. Resources are read-only endpoints for list commands that can be accessed via URI. Use filters to see which resources are available for specific namespaces or resources.", AllowAnonymousClient: true, DisableTelemetry: true, ArgsType: reflect.TypeFor[listResourcesArgs](), Examples: []*core.Example{ { Short: "List all available MCP resources", Raw: `scw mcp server list-resources`, }, { Short: "List resources for a specific namespace", Raw: `scw mcp server list-resources namespaces=instance`, }, { Short: "List resources for a specific resource type", Raw: `scw mcp server list-resources resources=server`, }, { Short: "List only read-only resources", Raw: `scw mcp server list-resources read-only=true`, }, }, ArgSpecs: core.ArgSpecs{ { Name: "namespaces", Short: "Filter by namespaces (e.g., instance, iam, object)", Required: false, Positional: false, }, { Name: "resources", Short: "Filter by resources (e.g., server, volume, bucket)", Required: false, Positional: false, }, { Name: "read-only", Short: "Only list read-only resources", Required: false, Positional: false, Default: core.DefaultValueSetter("false"), }, }, Run: func(ctx context.Context, argsI any) (any, error) { args := argsI.(*listResourcesArgs) // Get all CLI commands from the meta context commands := core.ExtractCommands(ctx) cliCommands := commands.GetAll() // Step 1: Filter commands based on the given config filteredCommands := FilterCommands(cliCommands, CommandFilterConfig{ ReadOnly: args.ReadOnly, EnabledNamespaces: SplitArg(args.Namespaces), EnabledResources: SplitArg(args.Resources), }) // Step 2: Create the MCP server with pre-filtered commands buildInfo := core.ExtractBuildInfo(ctx) mcpServer := NewMCPServer(filteredCommands, *buildInfo) // Step 2: List resources from the MCP server return mcpServer.ListResources(), nil }, ExcludeFromMCP: true, } }