diff --git a/README.md b/README.md index 650b682..488c5bb 100644 --- a/README.md +++ b/README.md @@ -1220,6 +1220,52 @@ spec: ./maestro workflow deploy agent-config.yaml workflow-config.yaml --dry-run ``` +## Tool Management + +The Maestro CLI provides commands for creating Tools for agents. + +### Create Tool + +```bash +# Create tool from YAML +./maestro tool create tool-config.yaml + +# Test without creating (dry run) +./maestro tool create tool-config.yaml --dry-run +``` + +The command automatically: +- Sets the API version to `maestro.ai4quantum.com/v1alpha1` +- Sanitizes resource names for Kubernetes compatibility +- Processes workflow-specific fields for proper deployment + +### Tool Examples +Tool example defined in yaml format is: +```yaml +apiVersion: maestro/v1alpha1 +kind: MCPTool +metadata: + name: fetch + namespace: default +spec: + image: ghcr.io/stackloklabs/gofetch/server:latest + transport: streamable-http +``` +The syntax of the agent definition is defined in the [json schema](https://github.com/AI4quantum/maestro/blob/main/schemas/tool_schema.json). +The schema is same as ToolHive CRD definition except `apiVersion` and `kind`. +Maestro deploy MCP servers for the defined tools. The available tools are listed by [ToolHive `thv list`](https://docs.stacklok.com/toolhive/reference/cli/thv_list) command. + +- **apiVersion**: version of agent definition format. This must be `maestro/v1alpha1` now. +- **kind**: type of object. `MCPTool` for agent definition +- **metadata**: + - **name**: name of tool + - **labels**: array of key, value pairs. This is optional and can be used to associate any information to this agent +- **spec**: + - **image**: Image is the container image for the MCP server. The image location is in [`thv registry info [server] [flags]`](https://docs.stacklok.com/toolhive/reference/cli/thv_registry_info) output + - **transport**: Transport is the transport method for the MCP server (stdio, streamable-http, sse) + +The full schema is documeted in [ToolHive Docs](https://docs.stacklok.com/toolhive/reference/crd-spec) + ## Custom Resource Management The CLI provides commands for creating Kubernetes custom resources: diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 7eb0a3c..06d2213 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -13,6 +13,7 @@ Welcome to the Maestro CLI! This guide will help you get started with managing v - [Document Management](#document-management) - [Agent Management](#agent-management) - [Workflow Management](#workflow-management) +- [Tool Management](#tool-management) - [Custom Resource Management](#custom-resource-management) - [Mermaid Diagram Generation](#mermaid-diagram-generation) - [Validation](#validation) @@ -357,6 +358,52 @@ spec: ./maestro workflow deploy agent-config.yaml workflow-config.yaml --dry-run ``` +## Tool Management + +The Maestro CLI provides commands for creating Tools for agents. + +### Create Tool + +```bash +# Create tool from YAML +./maestro tool create tool-config.yaml + +# Test without creating (dry run) +./maestro tool create tool-config.yaml --dry-run +``` + +The command automatically: +- Sets the API version to `maestro.ai4quantum.com/v1alpha1` +- Sanitizes resource names for Kubernetes compatibility +- Processes workflow-specific fields for proper deployment + +### Tool Examples +Tool example defined in yaml format is: +```yaml +apiVersion: maestro/v1alpha1 +kind: MCPTool +metadata: + name: fetch + namespace: default +spec: + image: ghcr.io/stackloklabs/gofetch/server:latest + transport: streamable-http +``` +The syntax of the agent definition is defined in the [json schema](https://github.com/AI4quantum/maestro/blob/main/schemas/tool_schema.json). +The schema is same as ToolHive CRD definition except `apiVersion` and `kind`. +Maestro deploy MCP servers for the defined tools. The available tools are listed by [ToolHive `thv list`](https://docs.stacklok.com/toolhive/reference/cli/thv_list) command. + +- **apiVersion**: version of agent definition format. This must be `maestro/v1alpha1` now. +- **kind**: type of object. `MCPTool` for agent definition +- **metadata**: + - **name**: name of tool + - **labels**: array of key, value pairs. This is optional and can be used to associate any information to this agent +- **spec**: + - **image**: Image is the container image for the MCP server. The image location is in [`thv registry info [server] [flags]`](https://docs.stacklok.com/toolhive/reference/cli/thv_registry_info) output + - **transport**: Transport is the transport method for the MCP server (stdio, streamable-http, sse) + +The full schema is documeted in [ToolHive Docs](https://docs.stacklok.com/toolhive/reference/crd-spec) + ## Custom Resource Management The Maestro CLI provides commands for creating Kubernetes custom resources for agents and workflows. diff --git a/internal/commands/create.go b/internal/commands/create.go index 4368227..1484992 100644 --- a/internal/commands/create.go +++ b/internal/commands/create.go @@ -146,18 +146,63 @@ func (c *CreateCommand) createAgentsFromYAML(agentsYaml []common.YAMLDocument) e } // createMCPToolsFromYAML creates MCP tools from the YAML configuration -func (c *CreateCommand) createMCPToolsFromYAML(agentsYaml []common.YAMLDocument) error { - // In the Python implementation, this calls create_mcptools from maestro.mcptool - // We'll need to implement the equivalent functionality in Go - +func (c *CreateCommand) createMCPToolsFromYAML(toolsYaml []common.YAMLDocument) error { // For now, we'll just print a message c.Console().Ok("Creating MCP tools from YAML configuration") - // TODO: Implement the actual MCP tool creation logic - // This would involve: - // 1. Parsing the tool definitions - // 2. Creating the tool instances - // 3. Registering them with the system + // Get MCP server URI + serverURI, err := common.GetMaestroMCPServerURI(c.mcpServerURI) + if err != nil { + if common.Progress != nil { + common.Progress.StopWithError("Failed to get MCP server URI") + } + return err + } + + if common.Verbose { + fmt.Printf("Connecting to MCP server at: %s\n", serverURI) + } + + // Create MCP client + client, _ := common.NewMCPClient(serverURI) + if err != nil { + if common.Progress != nil { + common.Progress.StopWithError("Failed to create MCP client") + } + return err + } + defer client.Close() + + if common.Progress != nil { + common.Progress.Update("Executing create tools...") + } + + // Call the run_workflow tool + tool_strings, err := common.YamlToString(toolsYaml) + if err != nil { + fmt.Println("tool file error") + } + + params := map[string]interface{}{ + "tools": tool_strings, + } + + result, err := client.CallMCPServer("create_tools", params) + if err != nil { + if common.Progress != nil { + common.Progress.StopWithError("Create tool failed") + } + return err + } + + if common.Progress != nil { + common.Progress.Stop("Create tools completed successfully") + } + + if !common.Silent { + fmt.Println("OK") + } + fmt.Println(result) return nil } diff --git a/src/commands.go b/src/commands.go index fd0ddd1..2bc65c0 100644 --- a/src/commands.go +++ b/src/commands.go @@ -352,8 +352,8 @@ var agentCmd = &cobra.Command{ Short: "Manage agents", Long: `Manage agent including creating, and serving.`, Aliases: []string{"agent"}, - Example: ` maestro create agents.yaml - maestro agent create agents.yaml`, + Example: ` maestro agent create agents.yaml + maestro agent serve agents.yaml`, } // Workflow commands - run, serve, deploy @@ -367,6 +367,15 @@ var workflowCmd = &cobra.Command{ maestro workflow deploy agents.yaml workflow.yaml`, } +// Tool commands - create +var toolCmd = &cobra.Command{ + Use: "tool", + Short: "Manage tools", + Long: `Manage tool including creating.`, + Aliases: []string{"tool"}, + Example: ` maestro tool create tools.yaml`, +} + // CustomResource commands - create var customResourceCmd = &cobra.Command{ Use: "customresource", diff --git a/src/main.go b/src/main.go index 83af65a..a6ecfa8 100644 --- a/src/main.go +++ b/src/main.go @@ -124,6 +124,7 @@ A command-line interface for working with Maestro configurations.`, ) rootCmd.AddCommand(agentCmd) rootCmd.AddCommand(workflowCmd) + rootCmd.AddCommand(toolCmd) rootCmd.AddCommand(customResourceCmd) rootCmd.AddCommand(metaAgentCmd) rootCmd.AddCommand(vdbCmd) @@ -163,6 +164,8 @@ A command-line interface for working with Maestro configurations.`, workflowCmd.AddCommand(commands.NewRunCommand()) workflowCmd.AddCommand(commands.NewDeployCommand()) + toolCmd.AddCommand(commands.NewCreateCommand()) + customResourceCmd.AddCommand(commands.NewCreateCrCommand()) // Chunking diff --git a/tests/integration/tool/tool_test.go b/tests/integration/tool/tool_test.go new file mode 100644 index 0000000..3479828 --- /dev/null +++ b/tests/integration/tool/tool_test.go @@ -0,0 +1,132 @@ +// Package tool contains integration tests for the tool command +// +// Test Assumptions and Limitations: +// 1. These tests are integration tests that execute the CLI commands as external processes +// 2. Tests use the --dry-run flag when possible to avoid actual resource creation +// 3. Tests are designed to be resilient to different environments (CI, local dev) +// 4. The tests focus on command execution rather than specific output validation +// 5. Some tests may pass even if the underlying functionality has issues, as they +// primarily test that the command doesn't panic or crash +package tool + +import ( + "os" + "os/exec" + "strings" + "testing" +) + +// TestToolCreate tests the tool create command +func TestToolCreate(t *testing.T) { + // Create a valid YAML file for testing + validYAML := `--- +apiVersion: maestro/v1alpha1 +kind: MCPTool +metadata: + name: test-tool +spec: + description: "Test tool for unit tests" + parameters: + - name: param1 + description: "A test parameter" + required: true + type: string + returns: + description: "Test return value" + type: string +` + + tempFile := createTempFile(t, "valid-tool-*.yaml", validYAML) + defer os.Remove(tempFile) + + cmd := exec.Command("../../../maestro", "tool", "create", tempFile, "--dry-run") + output, err := cmd.CombinedOutput() + + outputStr := string(output) + + // This test is expected to fail if no MCP server is running + if err != nil { + // Check if the error is due to MCP server not being available + if strings.Contains(outputStr, "MCP server could not be reached") { + t.Logf("Test skipped: No MCP server running (expected): %s", outputStr) + return + } + t.Fatalf("Tool create command failed with unexpected error: %v, output: %s", err, string(output)) + } + + if !strings.Contains(outputStr, "Creating MCP tools from YAML configuration") { + t.Errorf("Should show MCP tools creation message, got: %s", outputStr) + } +} + +// TestToolCreateWithNonExistentFile tests with non-existent file +func TestToolCreateWithNonExistentFile(t *testing.T) { + cmd := exec.Command("../../../maestro", "tool", "create", "nonexistent.yaml") + output, err := cmd.CombinedOutput() + + outputStr := string(output) + + // Should fail with non-existent file + if err == nil { + t.Error("Tool create command should fail with non-existent file") + } + + if !strings.Contains(outputStr, "no such file or directory") { + t.Errorf("Error message should mention file not found, got: %s", outputStr) + } +} + +// TestToolCreateWithInvalidYAML tests with invalid YAML +func TestToolCreateWithInvalidYAML(t *testing.T) { + // Create an invalid YAML file + invalidYAML := `--- +apiVersion: maestro/v1alpha1 +kind: MCPTool +metadata: + name: test-tool +spec: + description: "Test tool with invalid YAML + parameters: + - name: param1 + description: "A test parameter" + required: true + type: string +` + + tempFile := createTempFile(t, "invalid-tool-*.yaml", invalidYAML) + defer os.Remove(tempFile) + + cmd := exec.Command("../../../maestro", "tool", "create", tempFile) + output, err := cmd.CombinedOutput() + + outputStr := string(output) + + // Should fail with invalid YAML + if err == nil { + t.Error("Tool create command should fail with invalid YAML") + } + + if !strings.Contains(outputStr, "no valid YAML documents found") { + t.Errorf("Error message should mention YAML parsing error, got: %s", outputStr) + } +} + +// Helper function to create a temporary file with content +func createTempFile(t *testing.T, pattern string, content string) string { + tmpfile, err := os.CreateTemp("", pattern) + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + + if _, err := tmpfile.Write([]byte(content)); err != nil { + t.Fatalf("Failed to write to temp file: %v", err) + } + + if err := tmpfile.Close(); err != nil { + t.Fatalf("Failed to close temp file: %v", err) + } + + return tmpfile.Name() +} + +// Made with Bob