package server import ( "context" "fmt" "log" "os" "os/signal" "syscall" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/scaleway/scaleway-cli/v2/core" ) // MCPServer wraps the MCP server with CLI command integration type MCPServer struct { server *mcp.Server commands []*CommandTool resources []*CommandResource } // NewMCPServer creates a new MCP server that exposes CLI commands as tools and resources // Meta should be injected into the context by callers before tool/resource execution. func NewMCPServer( commands []*CommandTool, buildInfo core.BuildInfo, ) *MCPServer { versionStr := "" if buildInfo.Version != nil { versionStr = buildInfo.Version.String() } mcpServer := mcp.NewServer(&mcp.Implementation{ Name: "scaleway-mcp", Title: "Scaleway MCP Server", WebsiteURL: "https://cli.scaleway.com", Version: versionStr, Icons: []mcp.Icon{ { Source: "https://raw.githubusercontent.com/scaleway/scaleway-cli/main/docs/static_files/cli-artwork.png", MIMEType: "image/png", Sizes: []string{"48x48"}, }, }, }, &mcp.ServerOptions{ // Enable tools and resources capabilities with listChanged notifications Capabilities: &mcp.ServerCapabilities{ Tools: &mcp.ToolCapabilities{ListChanged: true}, Resources: &mcp.ResourceCapabilities{ListChanged: true}, }, }) s := &MCPServer{ server: mcpServer, commands: commands, resources: make([]*CommandResource, 0), } // Register all commands during initialization for _, cmd := range commands { // Register as a tool mcpTool := cmd.ToMCPTool() // Create a wrapper function for the tool using the correct MCP SDK signature wrapper := func(ctx context.Context, req *mcp.CallToolRequest, input map[string]any) (*mcp.CallToolResult, map[string]any, error) { result, err := cmd.Execute(ctx, input) // Initialize output map to avoid nil serialization issues output := map[string]any{} if err != nil { // Return error - MCP SDK will wrap it output["error"] = err.Error() return result, output, err } // Use StructuredContent (raw Go value) so clients receive proper JSON // instead of a JSON-encoded string. if result.StructuredContent != nil { output["result"] = result.StructuredContent } else { output["result"] = "Command executed successfully (no output)" } return result, output, nil } // Register with MCP SDK mcp.AddTool(mcpServer, mcpTool, wrapper) // Register as a resource if it's a list command if cmd.Command.IsList() { if err := s.LoadResource(cmd.Command); err != nil { // Log but don't fail - some commands might not be compatible log.Printf( "Warning: failed to load resource %s: %v\n", cmd.Command.GetCommandLine("scw"), err, ) } } } return s } // Run starts the MCP server using the specified transport func (s *MCPServer) Run(ctx context.Context, transport mcp.Transport) error { return s.server.Run(ctx, transport) } // Server returns the underlying MCP server for direct transport connections func (s *MCPServer) Server() *mcp.Server { return s.server } // RegisteredCommands returns the list of commands registered as MCP tools func (s *MCPServer) RegisteredCommands() []*CommandTool { return s.commands } // RegisteredResources returns the list of commands registered as MCP resources func (s *MCPServer) RegisteredResources() []*CommandResource { return s.resources } // Serve runs the MCP server with the specified transport. // It handles graceful shutdown and transport selection. func (s *MCPServer) Serve( ctx context.Context, transportMode string, address string, ) (any, error) { fmt.Fprintf(os.Stderr, "%d registered commands\n", len(s.RegisteredCommands())) fmt.Fprintf(os.Stderr, "%d registered resources\n", len(s.RegisteredResources())) // Log transport information fmt.Fprintf(os.Stderr, "Running MCP server with %s transport", transportMode) if transportMode != "stdio" { fmt.Fprintf(os.Stderr, " on %s", address) } fmt.Fprintf(os.Stderr, "\n") // Setup graceful shutdown ctx, cancel := context.WithCancel(ctx) defer cancel() sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigChan cancel() }() // Run server based on transport mode switch transportMode { case "stdio": if err := s.Run(ctx, &mcp.StdioTransport{}); err != nil { return nil, fmt.Errorf("MCP server error: %w", err) } case "streamable-http": return nil, RunStreamableHTTPServer(ctx, s, address) default: return nil, fmt.Errorf( "unknown transport mode: %s (valid modes: stdio, streamable-http)", transportMode, ) } return map[string]string{"status": "shutdown"}, nil }