Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b6be7e942e | |||
| 680de3a536 | |||
| 4aaba87ec3 |
+20
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{}{})
|
||||
|
||||
@@ -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
-106
@@ -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,110 +247,10 @@ func testAction(ctx *cli.Context) error {
|
||||
inputJSON = ctx.Args().Get(1)
|
||||
}
|
||||
|
||||
// Validate input JSON
|
||||
var inputData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(inputJSON), &inputData); err != nil {
|
||||
return fmt.Errorf("invalid JSON input: %w", err)
|
||||
}
|
||||
|
||||
// Get registry
|
||||
reg := registry.DefaultRegistry
|
||||
if regName := ctx.String("registry"); regName != "" {
|
||||
if regName != "mdns" {
|
||||
return fmt.Errorf("registry %s not yet supported, use mdns", regName)
|
||||
}
|
||||
}
|
||||
|
||||
// Create MCP options
|
||||
opts := mcp.Options{
|
||||
Registry: reg,
|
||||
Context: context.Background(),
|
||||
Logger: log.New(os.Stderr, "", 0),
|
||||
}
|
||||
|
||||
// Parse tool name (format: "service.endpoint" or "service.Handler.Method")
|
||||
parts := parseTool(toolName)
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("invalid tool name format. Expected: service.endpoint or service.Handler.Method")
|
||||
}
|
||||
|
||||
serviceName := parts[0]
|
||||
endpointName := parts[1]
|
||||
|
||||
// If tool name has 3 parts, combine last two for endpoint (e.g., Handler.Method)
|
||||
if len(parts) == 3 {
|
||||
endpointName = parts[1] + "." + parts[2]
|
||||
}
|
||||
|
||||
// Discover the tool from registry
|
||||
services, err := opts.Registry.GetService(serviceName)
|
||||
if err != nil || len(services) == 0 {
|
||||
return fmt.Errorf("service %s not found: %w", serviceName, err)
|
||||
}
|
||||
|
||||
// Find the endpoint
|
||||
var endpoint *registry.Endpoint
|
||||
for _, ep := range services[0].Endpoints {
|
||||
if ep.Name == endpointName {
|
||||
endpoint = ep
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if endpoint == nil {
|
||||
return fmt.Errorf("endpoint %s not found in service %s", endpointName, serviceName)
|
||||
}
|
||||
|
||||
// Display test info
|
||||
fmt.Printf("Testing tool: %s\n", toolName)
|
||||
fmt.Printf("Service: %s\n", serviceName)
|
||||
fmt.Printf("Endpoint: %s\n", endpointName)
|
||||
fmt.Printf("Input: %s\n\n", inputJSON)
|
||||
|
||||
// Convert input to JSON bytes for RPC call
|
||||
inputBytes, err := json.Marshal(inputData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal input: %w", err)
|
||||
}
|
||||
|
||||
// Make RPC call using bytes codec
|
||||
c := opts.Client
|
||||
if c == nil {
|
||||
c = client.DefaultClient
|
||||
}
|
||||
|
||||
// Create request with bytes frame
|
||||
req := c.NewRequest(serviceName, endpointName, &bytes.Frame{Data: inputBytes})
|
||||
|
||||
// Make the call
|
||||
var rsp bytes.Frame
|
||||
if err := c.Call(opts.Context, req, &rsp); err != nil {
|
||||
fmt.Printf("❌ Call failed: %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse and display response
|
||||
fmt.Println("✅ Call successful!")
|
||||
fmt.Println("\nResponse:")
|
||||
|
||||
// Try to pretty-print JSON response
|
||||
var result interface{}
|
||||
if err := json.Unmarshal(rsp.Data, &result); err == nil {
|
||||
prettyJSON, err := json.MarshalIndent(result, "", " ")
|
||||
if err == nil {
|
||||
fmt.Println(string(prettyJSON))
|
||||
} else {
|
||||
fmt.Println(string(rsp.Data))
|
||||
}
|
||||
} else {
|
||||
// Not JSON, print raw
|
||||
fmt.Println(string(rsp.Data))
|
||||
}
|
||||
fmt.Printf("Input: %s\n", inputJSON)
|
||||
fmt.Println("\nResult:")
|
||||
fmt.Println("(Not yet implemented - coming soon)")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseTool splits a tool name into service and endpoint parts
|
||||
func parseTool(toolName string) []string {
|
||||
return strings.Split(toolName, ".")
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseTool(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
toolName string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
name: "simple two-part tool",
|
||||
toolName: "service.endpoint",
|
||||
want: []string{"service", "endpoint"},
|
||||
},
|
||||
{
|
||||
name: "three-part tool (service.Handler.Method)",
|
||||
toolName: "greeter.Greeter.Hello",
|
||||
want: []string{"greeter", "Greeter", "Hello"},
|
||||
},
|
||||
{
|
||||
name: "single part (invalid)",
|
||||
toolName: "service",
|
||||
want: []string{"service"},
|
||||
},
|
||||
{
|
||||
name: "four-part tool",
|
||||
toolName: "users.Users.Get.All",
|
||||
want: []string{"users", "Users", "Get", "All"},
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
toolName: "",
|
||||
want: []string{""},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := parseTool(tt.toolName)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseTool(%q) = %v, want %v", tt.toolName, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user