Skip to content

Commit

Permalink
🎨 Start moving commands towards using layers and glazed
Browse files Browse the repository at this point in the history
  • Loading branch information
wesen committed Jan 24, 2025
1 parent 78be450 commit 2c38ad4
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 61 deletions.
34 changes: 33 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,4 +351,36 @@ Refactored SSE and stdio servers to use a common protocol dispatcher to handle M
- Creating a central dispatcher package for handling all protocol methods
- Unifying error handling and response creation
- Adding proper context handling for session IDs
- Reducing code duplication between transport implementations
- Reducing code duplication between transport implementations

## Embed Static Files in Prompto Server Binary

Improved deployment by embedding static files into the binary instead of serving from disk.

- Updated serve.go to use Go's embed package for static files

# Refactor prompts commands to use glazed framework

Refactored the prompts list and execute commands to use the glazed framework for better structured data output and consistent command line interface.

- Converted ListPrompts to a GlazeCommand for structured output
- Converted ExecutePrompt to a WriterCommand for formatted text output
- Added proper parameter handling using glazed parameter layers
- Improved error handling and command initialization

# Change prompt name from argument to flag

Changed the execute prompt command to use a --prompt-name flag instead of a positional argument for better usability and consistency with glazed framework.

- Added --prompt-name flag to execute command
- Removed positional argument requirement
- Updated command help text to reflect new usage

# Add client settings layer

Added a dedicated settings layer for MCP client configuration to improve reusability and consistency:

- Created new ClientParameterLayer for transport and server settings
- Updated client helper to use settings layer instead of cobra flags
- Updated list and execute commands to use the new layer
- Improved error handling and configuration management
37 changes: 30 additions & 7 deletions cmd/mcp-client/cmds/helpers/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import (
"context"
"fmt"

glazed_layers "github.com/go-go-golems/glazed/pkg/cmds/layers"
"github.com/go-go-golems/go-go-mcp/cmd/mcp-client/cmds/layers"
"github.com/go-go-golems/go-go-mcp/pkg/client"
"github.com/go-go-golems/go-go-mcp/pkg/protocol"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)

type ClientSettings = layers.ClientSettings

// CreateClient initializes and returns a new MCP client based on the provided flags.
func CreateClient(cmd *cobra.Command) (*client.Client, error) {
transport, err := cmd.Flags().GetString("transport")
Expand All @@ -27,23 +31,42 @@ func CreateClient(cmd *cobra.Command) (*client.Client, error) {
return nil, fmt.Errorf("failed to get command flag: %w", err)
}

return createClient(&ClientSettings{
Transport: transport,
Server: serverURL,
Command: cmdArgs,
})
}

// CreateClientFromSettings initializes and returns a new MCP client based on the provided settings.
func CreateClientFromSettings(parsedLayers *glazed_layers.ParsedLayers) (*client.Client, error) {
s := &ClientSettings{}
if err := parsedLayers.InitializeStruct(layers.ClientLayerSlug, s); err != nil {
return nil, err
}

return createClient(s)
}

func createClient(s *ClientSettings) (*client.Client, error) {
var t client.Transport
var err error

switch transport {
switch s.Transport {
case "command":
if len(cmdArgs) == 0 {
if len(s.Command) == 0 {
return nil, fmt.Errorf("command is required for command transport")
}
log.Debug().Msgf("Creating command transport with args: %v", cmdArgs)
t, err = client.NewCommandStdioTransport(log.Logger, cmdArgs[0], cmdArgs[1:]...)
log.Debug().Msgf("Creating command transport with args: %v", s.Command)
t, err = client.NewCommandStdioTransport(log.Logger, s.Command[0], s.Command[1:]...)
if err != nil {
return nil, fmt.Errorf("failed to create command transport: %w", err)
}
case "sse":
log.Debug().Msgf("Creating SSE transport with server URL: %s", serverURL)
t = client.NewSSETransport(serverURL, log.Logger)
log.Debug().Msgf("Creating SSE transport with server URL: %s", s.Server)
t = client.NewSSETransport(s.Server, log.Logger)
default:
return nil, fmt.Errorf("invalid transport type: %s", transport)
return nil, fmt.Errorf("invalid transport type: %s", s.Transport)
}

// Create and initialize client
Expand Down
39 changes: 39 additions & 0 deletions cmd/mcp-client/cmds/layers/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package layers

import (
"github.com/go-go-golems/glazed/pkg/cmds/layers"
"github.com/go-go-golems/glazed/pkg/cmds/parameters"
)

type ClientSettings struct {
Transport string `glazed.parameter:"transport"`
Server string `glazed.parameter:"server"`
Command []string `glazed.parameter:"command"`
}

const ClientLayerSlug = "mcp-client"

func NewClientParameterLayer() (layers.ParameterLayer, error) {
return layers.NewParameterLayer(ClientLayerSlug, "MCP Client Settings",
layers.WithParameterDefinitions(
parameters.NewParameterDefinition(
"transport",
parameters.ParameterTypeString,
parameters.WithHelp("Transport type (command or sse)"),
parameters.WithDefault("command"),
),
parameters.NewParameterDefinition(
"server",
parameters.ParameterTypeString,
parameters.WithHelp("Server URL for SSE transport"),
parameters.WithDefault("http://localhost:3001"),
),
parameters.NewParameterDefinition(
"command",
parameters.ParameterTypeStringList,
parameters.WithHelp("Command and arguments for command transport"),
parameters.WithDefault([]string{"mcp-server", "start", "--transport", "stdio"}),
),
),
)
}
218 changes: 165 additions & 53 deletions cmd/mcp-client/cmds/prompts.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
package cmds

import (
"context"
"encoding/json"
"fmt"
"io"

"github.com/go-go-golems/glazed/pkg/cli"
"github.com/go-go-golems/glazed/pkg/cmds"
glazed_layers "github.com/go-go-golems/glazed/pkg/cmds/layers"
"github.com/go-go-golems/go-go-mcp/cmd/mcp-client/cmds/layers"

"github.com/go-go-golems/glazed/pkg/cmds/parameters"
"github.com/go-go-golems/glazed/pkg/middlewares"
"github.com/go-go-golems/glazed/pkg/settings"
"github.com/go-go-golems/glazed/pkg/types"
"github.com/go-go-golems/go-go-mcp/cmd/mcp-client/cmds/helpers"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

Expand All @@ -19,73 +31,173 @@ var PromptsCmd = &cobra.Command{
Long: `List available prompts and execute specific prompts.`,
}

var listPromptsCmd = &cobra.Command{
Use: "list",
Short: "List available prompts",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := helpers.CreateClient(cmd)
if err != nil {
return err
}
defer client.Close(cmd.Context())
type ListPromptsCommand struct {
*cmds.CommandDescription
}

prompts, cursor, err := client.ListPrompts(cmd.Context(), "")
if err != nil {
return err
}
type ExecutePromptCommand struct {
*cmds.CommandDescription
}

for _, prompt := range prompts {
fmt.Printf("Name: %s\n", prompt.Name)
fmt.Printf("Description: %s\n", prompt.Description)
fmt.Printf("Arguments:\n")
for _, arg := range prompt.Arguments {
fmt.Printf(" - %s (required: %v): %s\n",
arg.Name, arg.Required, arg.Description)
}
fmt.Println()
}
type ExecutePromptSettings struct {
Args string `glazed.parameter:"args"`
PromptName string `glazed.parameter:"prompt-name"`
}

if cursor != "" {
fmt.Printf("Next cursor: %s\n", cursor)
}
func NewListPromptsCommand() (*ListPromptsCommand, error) {
glazedParameterLayer, err := settings.NewGlazedParameterLayers()
if err != nil {
return nil, errors.Wrap(err, "could not create Glazed parameter layer")
}

return nil
},
clientLayer, err := layers.NewClientParameterLayer()
if err != nil {
return nil, errors.Wrap(err, "could not create client parameter layer")
}

return &ListPromptsCommand{
CommandDescription: cmds.NewCommandDescription(
"list",
cmds.WithShort("List available prompts"),
cmds.WithLayersList(
glazedParameterLayer,
clientLayer,
),
),
}, nil
}

var executePromptCmd = &cobra.Command{
Use: "execute [prompt-name]",
Short: "Execute a specific prompt",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
client, err := helpers.CreateClient(cmd)
if err != nil {
return err
}
defer client.Close(cmd.Context())
func NewExecutePromptCommand() (*ExecutePromptCommand, error) {
clientLayer, err := layers.NewClientParameterLayer()
if err != nil {
return nil, errors.Wrap(err, "could not create client parameter layer")
}

return &ExecutePromptCommand{
CommandDescription: cmds.NewCommandDescription(
"execute",
cmds.WithShort("Execute a specific prompt"),
cmds.WithFlags(
parameters.NewParameterDefinition(
"args",
parameters.ParameterTypeString,
parameters.WithHelp("Prompt arguments as JSON string"),
parameters.WithDefault(""),
),
),
cmds.WithArguments(
parameters.NewParameterDefinition(
"prompt-name",
parameters.ParameterTypeString,
parameters.WithHelp("Name of the prompt to execute"),
parameters.WithRequired(true),
),
),
cmds.WithLayersList(
clientLayer,
),
),
}, nil
}

func (c *ListPromptsCommand) RunIntoGlazeProcessor(
ctx context.Context,
parsedLayers *glazed_layers.ParsedLayers,
gp middlewares.Processor,
) error {
client, err := helpers.CreateClientFromSettings(parsedLayers)
if err != nil {
return err
}
defer client.Close(ctx)

// Parse prompt arguments
promptArgMap := make(map[string]string)
if promptArgs != "" {
if err := json.Unmarshal([]byte(promptArgs), &promptArgMap); err != nil {
return fmt.Errorf("invalid prompt arguments JSON: %w", err)
prompts, cursor, err := client.ListPrompts(ctx, "")
if err != nil {
return err
}

for _, prompt := range prompts {
row := types.NewRow(
types.MRP("name", prompt.Name),
types.MRP("description", prompt.Description),
)

// Create a JSON array of arguments
args := make([]map[string]interface{}, len(prompt.Arguments))
for i, arg := range prompt.Arguments {
args[i] = map[string]interface{}{
"name": arg.Name,
"required": arg.Required,
"description": arg.Description,
}
}
row.Set("arguments", args)

if err := gp.AddRow(ctx, row); err != nil {
return err
}
}

message, err := client.GetPrompt(cmd.Context(), args[0], promptArgMap)
if err != nil {
if cursor != "" {
// Add cursor as a final row
cursorRow := types.NewRow(
types.MRP("cursor", cursor),
)
if err := gp.AddRow(ctx, cursorRow); err != nil {
return err
}
}

return nil
}

func (c *ExecutePromptCommand) RunIntoWriter(
ctx context.Context,
parsedLayers *glazed_layers.ParsedLayers,
w io.Writer,
) error {
s := &ExecutePromptSettings{}
if err := parsedLayers.InitializeStruct(glazed_layers.DefaultSlug, s); err != nil {
return err
}

client, err := helpers.CreateClientFromSettings(parsedLayers)
if err != nil {
return err
}
defer client.Close(ctx)

// Parse prompt arguments
promptArgMap := make(map[string]string)
if s.Args != "" {
if err := json.Unmarshal([]byte(s.Args), &promptArgMap); err != nil {
return fmt.Errorf("invalid prompt arguments JSON: %w", err)
}
}

message, err := client.GetPrompt(ctx, s.PromptName, promptArgMap)
if err != nil {
return err
}

// Pretty print the response
fmt.Printf("Role: %s\n", message.Role)
fmt.Printf("Content: %s\n", message.Content.Text)
return nil
},
// Write formatted output to writer
_, err = fmt.Fprintf(w, "Role: %s\nContent: %s\n", message.Role, message.Content.Text)
return err
}

func init() {
PromptsCmd.AddCommand(listPromptsCmd)
PromptsCmd.AddCommand(executePromptCmd)
executePromptCmd.Flags().StringVarP(&promptArgs, "args", "a", "", "Prompt arguments as JSON string")
listCmd, err := NewListPromptsCommand()
cobra.CheckErr(err)

executeCmd, err := NewExecutePromptCommand()
cobra.CheckErr(err)

listCobraCmd, err := cli.BuildCobraCommandFromGlazeCommand(listCmd)
cobra.CheckErr(err)

executeCobraCmd, err := cli.BuildCobraCommandFromWriterCommand(executeCmd)
cobra.CheckErr(err)

PromptsCmd.AddCommand(listCobraCmd)
PromptsCmd.AddCommand(executeCobraCmd)
}

0 comments on commit 2c38ad4

Please sign in to comment.