Compare commits

..

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] b6be7e942e Add clarifying comments for dual metadata handling paths
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 14:15:37 +00:00
copilot-swe-agent[bot] 680de3a536 Apply code formatting with gofmt
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 14:13:30 +00:00
copilot-swe-agent[bot] 4aaba87ec3 Add --header and --metadata flags to micro call command
Co-authored-by: asim <17530+asim@users.noreply.github.com>
2026-02-13 14:12:03 +00:00
copilot-swe-agent[bot] 233df2a626 Initial plan 2026-02-13 14:05:41 +00:00
7 changed files with 150 additions and 98 deletions
+20 -1
View File
@@ -71,6 +71,18 @@ func init() {
{
Name: "call",
Usage: "Call a service",
Flags: []cli.Flag{
&cli.StringSliceFlag{
Name: "header",
Aliases: []string{"H"},
Usage: "Set request headers (can be used multiple times): --header 'Key:Value'",
},
&cli.StringSliceFlag{
Name: "metadata",
Aliases: []string{"m"},
Usage: "Set request metadata (can be used multiple times): --metadata 'Key:Value'",
},
},
Action: func(ctx *cli.Context) error {
args := ctx.Args()
@@ -86,9 +98,16 @@ func init() {
request = args.Get(2)
}
// Create context with metadata if provided
// Note: This is for the direct 'micro call' command.
// Dynamic service calls (e.g., 'micro helloworld call') are handled in CallService.
callCtx := context.TODO()
callCtx = util.AddMetadataToContext(callCtx, ctx.StringSlice("metadata"))
callCtx = util.AddMetadataToContext(callCtx, ctx.StringSlice("header"))
req := client.NewRequest(service, endpoint, &bytes.Frame{Data: []byte(request)})
var rsp bytes.Frame
err := client.Call(context.TODO(), req, &rsp)
err := client.Call(callCtx, req, &rsp)
if err != nil {
return err
}
+38 -7
View File
@@ -15,9 +15,30 @@ import (
"github.com/stretchr/objx"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/client"
"go-micro.dev/v5/metadata"
"go-micro.dev/v5/registry"
)
// AddMetadataToContext parses metadata strings in the format "Key:Value" and adds them to the context
func AddMetadataToContext(ctx context.Context, metadataStrings []string) context.Context {
if len(metadataStrings) == 0 {
return ctx
}
md := make(metadata.Metadata)
for _, m := range metadataStrings {
parts := strings.SplitN(m, ":", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
md[key] = value
}
return metadata.MergeContext(ctx, md, true)
}
// LookupService queries the service for a service with the given alias. If
// no services are found for a given alias, the registry will return nil and
// the error will also be nil. An error is only returned if there was an issue
@@ -132,17 +153,27 @@ func CallService(srv *registry.Service, args []string) error {
return fmt.Errorf("Endpoint %v not found for service %v", endpoint, srv.Name)
}
// parse the flags
// create a context for the call
callCtx := context.TODO()
// parse out --header or --metadata flags before parsing request body
// Note: This is for dynamic service calls (e.g., 'micro helloworld call --header X:Y').
// Direct 'micro call' commands are handled in cli.go.
if headerFlags, ok := flags["header"]; ok {
callCtx = AddMetadataToContext(callCtx, headerFlags)
delete(flags, "header")
}
if metadataFlags, ok := flags["metadata"]; ok {
callCtx = AddMetadataToContext(callCtx, metadataFlags)
delete(flags, "metadata")
}
// parse the flags into request body
body, err := FlagsToRequest(flags, ep.Request)
if err != nil {
return err
}
// create a context for the call based on the cli context
callCtx := context.TODO()
// TODO: parse out --header or --metadata
// construct and execute the request using the json content type
req := client.DefaultClient.NewRequest(srv.Name, endpoint, body, client.WithContentType("application/json"))
var rsp json.RawMessage
@@ -387,7 +418,7 @@ func FlagsToRequest(flags map[string][]string, req *registry.Value) (map[string]
// so we do that here
if strings.Contains(key, "-") {
parts := strings.Split(key, "-")
for i, _ := range parts {
for i := range parts {
pToCreate := strings.Join(parts[0:i], ".")
if i > 0 && i < len(parts) && !result.Has(pToCreate) {
result.Set(pToCreate, map[string]interface{}{})
+74
View File
@@ -1,11 +1,13 @@
package util
import (
"context"
"reflect"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
"go-micro.dev/v5/metadata"
goregistry "go-micro.dev/v5/registry"
)
@@ -377,3 +379,75 @@ func TestDynamicFlagParsing(t *testing.T) {
}
}
func TestAddMetadataToContext(t *testing.T) {
tests := []struct {
name string
metadataStrs []string
expectedKeys []string
expectedValues []string
}{
{
name: "Single metadata",
metadataStrs: []string{"Key1:Value1"},
expectedKeys: []string{"Key1"},
expectedValues: []string{"Value1"},
},
{
name: "Multiple metadata",
metadataStrs: []string{"Key1:Value1", "Key2:Value2"},
expectedKeys: []string{"Key1", "Key2"},
expectedValues: []string{"Value1", "Value2"},
},
{
name: "Metadata with spaces",
metadataStrs: []string{"Key1: Value1 ", " Key2 : Value2"},
expectedKeys: []string{"Key1", "Key2"},
expectedValues: []string{"Value1", "Value2"},
},
{
name: "Metadata with colon in value",
metadataStrs: []string{"Authorization:Bearer token:123"},
expectedKeys: []string{"Authorization"},
expectedValues: []string{"Bearer token:123"},
},
{
name: "Empty metadata",
metadataStrs: []string{},
expectedKeys: []string{},
expectedValues: []string{},
},
{
name: "Invalid metadata format",
metadataStrs: []string{"InvalidFormat"},
expectedKeys: []string{},
expectedValues: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
ctx = AddMetadataToContext(ctx, tt.metadataStrs)
md, ok := metadata.FromContext(ctx)
if len(tt.expectedKeys) == 0 && !ok {
return // Expected no metadata
}
if !ok && len(tt.expectedKeys) > 0 {
t.Fatal("Expected metadata in context but got none")
}
for i, key := range tt.expectedKeys {
value, found := md.Get(key)
if !found {
t.Fatalf("Expected key %s not found in metadata", key)
}
if value != tt.expectedValues[i] {
t.Fatalf("Expected value %s for key %s, got %s", tt.expectedValues[i], key, value)
}
}
})
}
}
+3 -75
View File
@@ -8,13 +8,10 @@ import (
"log"
"os"
"os/signal"
"strings"
"syscall"
"github.com/urfave/cli/v2"
"go-micro.dev/v5/client"
"go-micro.dev/v5/cmd"
"go-micro.dev/v5/codec/bytes"
"go-micro.dev/v5/gateway/mcp"
"go-micro.dev/v5/registry"
)
@@ -250,79 +247,10 @@ func testAction(ctx *cli.Context) error {
inputJSON = ctx.Args().Get(1)
}
// Get registry
reg := registry.DefaultRegistry
// Parse tool name (format: service.endpoint or service.Handler.Method)
parts := strings.Split(toolName, ".")
if len(parts) < 2 {
return fmt.Errorf("invalid tool name format: %s (expected format: service.Endpoint or service.Handler.Method)", toolName)
}
serviceName := parts[0]
var endpointName string
if len(parts) == 2 {
endpointName = parts[1]
} else {
// For format like greeter.Greeter.SayHello, endpoint is Greeter.SayHello
endpointName = strings.Join(parts[1:], ".")
}
// Verify service exists
services, err := reg.GetService(serviceName)
if err != nil || len(services) == 0 {
return fmt.Errorf("service not found: %s", serviceName)
}
// Verify endpoint exists
endpointFound := false
for _, ep := range services[0].Endpoints {
if ep.Name == endpointName {
endpointFound = true
break
}
}
if !endpointFound {
return fmt.Errorf("endpoint not found: %s on service %s", endpointName, serviceName)
}
fmt.Printf("Testing tool: %s\n", toolName)
fmt.Printf("Input: %s\n\n", inputJSON)
// Parse input JSON
var input map[string]interface{}
if err := json.Unmarshal([]byte(inputJSON), &input); err != nil {
return fmt.Errorf("invalid JSON input: %w", err)
}
// Make RPC call using client
c := client.DefaultClient
inputBytes, err := json.Marshal(input)
if err != nil {
return fmt.Errorf("failed to marshal input: %w", err)
}
rpcReq := c.NewRequest(serviceName, endpointName, &bytes.Frame{Data: inputBytes})
var rsp bytes.Frame
if err := c.Call(context.Background(), rpcReq, &rsp); err != nil {
return fmt.Errorf("RPC call failed: %w", err)
}
// Parse and display response
var result interface{}
if err := json.Unmarshal(rsp.Data, &result); err != nil {
// If unmarshal fails, display raw data
fmt.Printf("Result (raw): %s\n", string(rsp.Data))
} else {
// Pretty print JSON result
prettyJSON, err := json.MarshalIndent(result, "", " ")
if err != nil {
fmt.Printf("Result: %v\n", result)
} else {
fmt.Printf("Result:\n%s\n", string(prettyJSON))
}
}
fmt.Printf("Input: %s\n", inputJSON)
fmt.Println("\nResult:")
fmt.Println("(Not yet implemented - coming soon)")
return nil
}
+7 -7
View File
@@ -58,7 +58,7 @@ type templates struct {
authLogin *template.Template
authUsers *template.Template
playground *template.Template
scopes *template.Template
scopes *template.Template
}
type TemplateUser struct {
ID string
@@ -82,7 +82,7 @@ func parseTemplates() *templates {
authLogin: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_login.html")),
authUsers: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/auth_users.html")),
playground: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/playground.html")),
scopes: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/scopes.html")),
scopes: template.Must(template.ParseFS(HTML, "web/templates/base.html", "web/templates/scopes.html")),
}
}
@@ -657,8 +657,8 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
// Discover tools from registry
services, _ := registry.ListServices()
type toolInfo struct {
Name string // original dotted name (e.g. "greeter.Greeter.Hello")
SafeName string // LLM-safe name (dots replaced with underscores)
Name string // original dotted name (e.g. "greeter.Greeter.Hello")
SafeName string // LLM-safe name (dots replaced with underscores)
Description string
Properties map[string]any
}
@@ -897,9 +897,9 @@ func registerHandlers(mux *http.ServeMux, tmpls *templates, storeInst store.Stor
"result": rpcResult,
})
toolResultBlocks = append(toolResultBlocks, map[string]any{
"type": "tool_result",
"tool_use_id": tu.ID,
"content": rpcContent,
"type": "tool_result",
"tool_use_id": tu.ID,
"content": rpcContent,
})
}
result["tool_calls"] = toolCalls
+5 -5
View File
@@ -63,22 +63,22 @@ func homeHandler(w http.ResponseWriter, r *http.Request) {
func usersHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Return all users
userList := make([]*User, 0, len(users))
for _, user := range users {
userList = append(userList, user)
}
json.NewEncoder(w).Encode(userList)
}
func userHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
// Extract user ID from path
id := r.URL.Path[len("/users/"):]
user, exists := users[id]
if !exists {
w.WriteHeader(http.StatusNotFound)
@@ -87,7 +87,7 @@ func userHandler(w http.ResponseWriter, r *http.Request) {
})
return
}
json.NewEncoder(w).Encode(user)
}
+3 -3
View File
@@ -21,8 +21,8 @@ type mockAuth struct {
accounts map[string]*auth.Account // token -> account
}
func (m *mockAuth) Init(...auth.Option) {}
func (m *mockAuth) Options() auth.Options { return auth.Options{} }
func (m *mockAuth) Init(...auth.Option) {}
func (m *mockAuth) Options() auth.Options { return auth.Options{} }
func (m *mockAuth) Generate(string, ...auth.GenerateOption) (*auth.Account, error) {
return nil, nil
}
@@ -540,7 +540,7 @@ func TestScopesFromGatewayOptions(t *testing.T) {
s := newTestServer(Options{
Registry: reg,
Scopes: map[string][]string{
"blog.Blog.Create": {"blog:admin"}, // override service scope
"blog.Blog.Create": {"blog:admin"}, // override service scope
"blog.Blog.Delete": {"blog:admin", "sudo"}, // add scope to tool without service scope
},
})