From d34b7311d52bb503e492aa9795e41c2a146824c8 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Thu, 27 Nov 2025 14:43:04 +0200 Subject: [PATCH 1/6] build flagset from inputschema to parse args --- cmd/src/mcp_args.go | 56 +++++++++++++++++++++++ cmd/src/mcp_args_test.go | 97 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 cmd/src/mcp_args.go create mode 100644 cmd/src/mcp_args_test.go diff --git a/cmd/src/mcp_args.go b/cmd/src/mcp_args.go new file mode 100644 index 0000000000..50320bdafc --- /dev/null +++ b/cmd/src/mcp_args.go @@ -0,0 +1,56 @@ +package main + +import ( + "flag" + "fmt" + "strings" +) + +var _ flag.Value = (*strSliceFlag)(nil) + +type strSliceFlag struct { + vals []string +} + +func (s *strSliceFlag) Set(v string) error { + s.vals = append(s.vals, v) + return nil +} + +func (s *strSliceFlag) String() string { + return strings.Join(s.vals, ",") +} + +func buildArgFlagSet(tool *MCPToolDef) (*flag.FlagSet, map[string]any, error) { + fs := flag.NewFlagSet(tool.Name(), flag.ContinueOnError) + flagVars := map[string]any{} + + for name, pVal := range tool.InputSchema.Properties { + switch pv := pVal.(type) { + case *SchemaPrimitive: + switch pv.Kind { + case "integer": + dst := fs.Int(name, 0, pv.Description) + flagVars[name] = dst + + case "boolean": + dst := fs.Bool(name, false, pv.Description) + flagVars[name] = dst + case "string": + dst := fs.String(name, "", pv.Description) + flagVars[name] = dst + default: + return nil, nil, fmt.Errorf("unknown schema primitive kind %q", pv.Kind) + + } + case *SchemaArray: + strSlice := new(strSliceFlag) + fs.Var(strSlice, name, pv.Description) + flagVars[name] = strSlice + case *SchemaObject: + // not supported yet + } + } + + return fs, flagVars, nil +} diff --git a/cmd/src/mcp_args_test.go b/cmd/src/mcp_args_test.go new file mode 100644 index 0000000000..2c174986ef --- /dev/null +++ b/cmd/src/mcp_args_test.go @@ -0,0 +1,97 @@ +package main + +import ( + "testing" +) + +func TestFlagSetParse(t *testing.T) { + toolJSON := []byte(`{ + "tools": [ + { + "name": "sg_test_tool", + "description": "test description", + "inputSchema": { + "type": "object", + "$schema": "https://localhost/schema-draft/2025-07", + "required": ["values"], + "properties": { + "repos": { + "type": "array", + "items": { + "type": "string" + } + }, + "tag": { + "type": "string", + "items": true + }, + "count": { + "type": "integer" + }, + "boolFlag": { + "type": "boolean" + } + } + }, + "outputSchema": { + "type": "object", + "$schema": "https://localhost/schema-draft/2025-07", + "properties": { + "result": { "type": "string" } + } + } + } + ] + }`) + + defs, err := LoadMCPToolDefinitions(toolJSON) + if err != nil { + t.Fatalf("failed to load tool json: %v", err) + } + + flagSet, vars, err := buildArgFlagSet(defs["sg_test_tool"]) + if err != nil { + t.Fatalf("failed to build flagset from mcp tool definition: %v", err) + } + + if len(vars) == 0 { + t.Fatalf("vars from buildArgFlagSet should not be empty") + } + + args := []string{"-repos=A", "-repos=B", "-count=10", "-boolFlag", "-tag=testTag"} + + if err := flagSet.Parse(args); err != nil { + t.Fatalf("flagset parsing failed: %v", err) + } + derefFlagValues(vars) + + if v, ok := vars["repos"].([]string); ok { + if len(v) != 2 { + t.Fatalf("expected flag 'repos' values to have length %d but got %d", 2, len(v)) + } + } else { + t.Fatalf("expected flag 'repos' to have type of []string but got %T", v) + } + if v, ok := vars["tag"].(string); ok { + if v != "testTag" { + t.Fatalf("expected flag 'tag' values to have value %q but got %q", "testTag", v) + } + } else { + t.Fatalf("expected flag 'tag' to have type of string but got %T", v) + } + if v, ok := vars["count"].(int); ok { + if v != 10 { + t.Fatalf("expected flag 'count' values to have value %d but got %d", 10, v) + } + } else { + t.Fatalf("expected flag 'count' to have type of int but got %T", v) + } + if v, ok := vars["boolFlag"].(bool); ok { + if v != true { + t.Fatalf("expected flag 'boolFlag' values to have value %v but got %v", true, v) + } + } else { + t.Fatalf("expected flag 'boolFlag' to have type of bool but got %T", v) + } + +} From f1665ca5b085d77236cee6a9757856d5c4f2c133 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Fri, 28 Nov 2025 11:18:33 +0200 Subject: [PATCH 2/6] initial handleMcpTool method - validate flags with inputSchema --- cmd/src/mcp.go | 30 +++++++++++++++++++++++++++++- cmd/src/mcp_args.go | 14 ++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/cmd/src/mcp.go b/cmd/src/mcp.go index ec5683e4f7..109097dc25 100644 --- a/cmd/src/mcp.go +++ b/cmd/src/mcp.go @@ -3,6 +3,7 @@ package main import ( "flag" "fmt" + "strings" "github.com/sourcegraph/src-cli/internal/mcp" ) @@ -38,6 +39,33 @@ func mcpMain(args []string) error { } func handleMcpTool(tool *mcp.ToolDef, args []string) error { - fmt.Printf("handling tool %q args: %+v", tool.Name, args) + fs, vars, err := buildArgFlagSet(tool) + if err != nil { + return err + } + + if err := fs.Parse(args); err != nil { + return err + } + + inputSchema := tool.InputSchema + + for _, reqName := range inputSchema.Required { + if vars[reqName] == nil { + return fmt.Errorf("no value provided for required flag --%s", reqName) + } + } + + if len(args) < len(inputSchema.Required) { + return fmt.Errorf("not enough arguments provided - the following flags are required:\n%s", strings.Join(inputSchema.Required, "\n")) + } + + derefFlagValues(vars) + + fmt.Println("Flags") + for name, val := range vars { + fmt.Printf("--%s=%v\n", name, val) + } + return nil } diff --git a/cmd/src/mcp_args.go b/cmd/src/mcp_args.go index 50320bdafc..01caf144f0 100644 --- a/cmd/src/mcp_args.go +++ b/cmd/src/mcp_args.go @@ -3,6 +3,7 @@ package main import ( "flag" "fmt" + "reflect" "strings" ) @@ -21,6 +22,19 @@ func (s *strSliceFlag) String() string { return strings.Join(s.vals, ",") } +func derefFlagValues(vars map[string]any) { + for k, v := range vars { + rfl := reflect.ValueOf(v) + if rfl.Kind() == reflect.Pointer { + vv := rfl.Elem().Interface() + if slice, ok := vv.(strSliceFlag); ok { + vv = slice.vals + } + vars[k] = vv + } + } +} + func buildArgFlagSet(tool *MCPToolDef) (*flag.FlagSet, map[string]any, error) { fs := flag.NewFlagSet(tool.Name(), flag.ContinueOnError) flagVars := map[string]any{} From bcde96d1b365ea44e3675a562cce9eeb2d2c5545 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Tue, 2 Dec 2025 12:28:59 +0200 Subject: [PATCH 3/6] export LoadToolDefinitions and add LoadDefaultToolDefinitions --- cmd/src/mcp.go | 2 +- cmd/src/mcp_args.go | 12 +++++++----- cmd/src/mcp_args_test.go | 4 +++- internal/mcp/mcp_parse.go | 6 +++--- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/cmd/src/mcp.go b/cmd/src/mcp.go index 109097dc25..b2ebc50886 100644 --- a/cmd/src/mcp.go +++ b/cmd/src/mcp.go @@ -17,7 +17,7 @@ func init() { } func mcpMain(args []string) error { fmt.Println("NOTE: This command is still experimental") - tools, err := mcp.LoadToolDefinitions() + tools, err := mcp.LoadDefaultToolDefinitions() if err != nil { return err } diff --git a/cmd/src/mcp_args.go b/cmd/src/mcp_args.go index 01caf144f0..5d964f0c0d 100644 --- a/cmd/src/mcp_args.go +++ b/cmd/src/mcp_args.go @@ -5,6 +5,8 @@ import ( "fmt" "reflect" "strings" + + "github.com/sourcegraph/src-cli/internal/mcp" ) var _ flag.Value = (*strSliceFlag)(nil) @@ -35,13 +37,13 @@ func derefFlagValues(vars map[string]any) { } } -func buildArgFlagSet(tool *MCPToolDef) (*flag.FlagSet, map[string]any, error) { - fs := flag.NewFlagSet(tool.Name(), flag.ContinueOnError) +func buildArgFlagSet(tool *mcp.ToolDef) (*flag.FlagSet, map[string]any, error) { + fs := flag.NewFlagSet(tool.Name, flag.ContinueOnError) flagVars := map[string]any{} for name, pVal := range tool.InputSchema.Properties { switch pv := pVal.(type) { - case *SchemaPrimitive: + case *mcp.SchemaPrimitive: switch pv.Kind { case "integer": dst := fs.Int(name, 0, pv.Description) @@ -57,11 +59,11 @@ func buildArgFlagSet(tool *MCPToolDef) (*flag.FlagSet, map[string]any, error) { return nil, nil, fmt.Errorf("unknown schema primitive kind %q", pv.Kind) } - case *SchemaArray: + case *mcp.SchemaArray: strSlice := new(strSliceFlag) fs.Var(strSlice, name, pv.Description) flagVars[name] = strSlice - case *SchemaObject: + case *mcp.SchemaObject: // not supported yet } } diff --git a/cmd/src/mcp_args_test.go b/cmd/src/mcp_args_test.go index 2c174986ef..9d4673e04c 100644 --- a/cmd/src/mcp_args_test.go +++ b/cmd/src/mcp_args_test.go @@ -2,6 +2,8 @@ package main import ( "testing" + + "github.com/sourcegraph/src-cli/internal/mcp" ) func TestFlagSetParse(t *testing.T) { @@ -44,7 +46,7 @@ func TestFlagSetParse(t *testing.T) { ] }`) - defs, err := LoadMCPToolDefinitions(toolJSON) + defs, err := mcp.LoadToolDefinitions(toolJSON) if err != nil { t.Fatalf("failed to load tool json: %v", err) } diff --git a/internal/mcp/mcp_parse.go b/internal/mcp/mcp_parse.go index e610a271e9..7df4692237 100644 --- a/internal/mcp/mcp_parse.go +++ b/internal/mcp/mcp_parse.go @@ -68,11 +68,11 @@ type parser struct { errors []error } -func LoadToolDefinitions() (map[string]*ToolDef, error) { - return loadToolDefinitions(mcpToolListJSON) +func LoadDefaultToolDefinitions() (map[string]*ToolDef, error) { + return LoadToolDefinitions(mcpToolListJSON) } -func loadToolDefinitions(data []byte) (map[string]*ToolDef, error) { +func LoadToolDefinitions(data []byte) (map[string]*ToolDef, error) { defs := struct { Tools []struct { Name string `json:"name"` From 1c9d1aa2abae304b56e6a92dd9a3840280397fe1 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Tue, 2 Dec 2025 12:34:42 +0200 Subject: [PATCH 4/6] move mcp args to internal/mcp --- cmd/src/mcp.go | 4 ++-- {cmd/src => internal/mcp}/mcp_args.go | 14 ++++++-------- {cmd/src => internal/mcp}/mcp_args_test.go | 10 ++++------ internal/mcp/mcp_parse.go | 4 ++-- 4 files changed, 14 insertions(+), 18 deletions(-) rename {cmd/src => internal/mcp}/mcp_args.go (82%) rename {cmd/src => internal/mcp}/mcp_args_test.go (91%) diff --git a/cmd/src/mcp.go b/cmd/src/mcp.go index b2ebc50886..6e46721f9b 100644 --- a/cmd/src/mcp.go +++ b/cmd/src/mcp.go @@ -39,7 +39,7 @@ func mcpMain(args []string) error { } func handleMcpTool(tool *mcp.ToolDef, args []string) error { - fs, vars, err := buildArgFlagSet(tool) + fs, vars, err := mcp.BuildArgFlagSet(tool) if err != nil { return err } @@ -60,7 +60,7 @@ func handleMcpTool(tool *mcp.ToolDef, args []string) error { return fmt.Errorf("not enough arguments provided - the following flags are required:\n%s", strings.Join(inputSchema.Required, "\n")) } - derefFlagValues(vars) + mcp.DerefFlagValues(vars) fmt.Println("Flags") for name, val := range vars { diff --git a/cmd/src/mcp_args.go b/internal/mcp/mcp_args.go similarity index 82% rename from cmd/src/mcp_args.go rename to internal/mcp/mcp_args.go index 5d964f0c0d..c31270e152 100644 --- a/cmd/src/mcp_args.go +++ b/internal/mcp/mcp_args.go @@ -1,12 +1,10 @@ -package main +package mcp import ( "flag" "fmt" "reflect" "strings" - - "github.com/sourcegraph/src-cli/internal/mcp" ) var _ flag.Value = (*strSliceFlag)(nil) @@ -24,7 +22,7 @@ func (s *strSliceFlag) String() string { return strings.Join(s.vals, ",") } -func derefFlagValues(vars map[string]any) { +func DerefFlagValues(vars map[string]any) { for k, v := range vars { rfl := reflect.ValueOf(v) if rfl.Kind() == reflect.Pointer { @@ -37,13 +35,13 @@ func derefFlagValues(vars map[string]any) { } } -func buildArgFlagSet(tool *mcp.ToolDef) (*flag.FlagSet, map[string]any, error) { +func BuildArgFlagSet(tool *ToolDef) (*flag.FlagSet, map[string]any, error) { fs := flag.NewFlagSet(tool.Name, flag.ContinueOnError) flagVars := map[string]any{} for name, pVal := range tool.InputSchema.Properties { switch pv := pVal.(type) { - case *mcp.SchemaPrimitive: + case *SchemaPrimitive: switch pv.Kind { case "integer": dst := fs.Int(name, 0, pv.Description) @@ -59,11 +57,11 @@ func buildArgFlagSet(tool *mcp.ToolDef) (*flag.FlagSet, map[string]any, error) { return nil, nil, fmt.Errorf("unknown schema primitive kind %q", pv.Kind) } - case *mcp.SchemaArray: + case *SchemaArray: strSlice := new(strSliceFlag) fs.Var(strSlice, name, pv.Description) flagVars[name] = strSlice - case *mcp.SchemaObject: + case *SchemaObject: // not supported yet } } diff --git a/cmd/src/mcp_args_test.go b/internal/mcp/mcp_args_test.go similarity index 91% rename from cmd/src/mcp_args_test.go rename to internal/mcp/mcp_args_test.go index 9d4673e04c..33708c9bf5 100644 --- a/cmd/src/mcp_args_test.go +++ b/internal/mcp/mcp_args_test.go @@ -1,9 +1,7 @@ -package main +package mcp import ( "testing" - - "github.com/sourcegraph/src-cli/internal/mcp" ) func TestFlagSetParse(t *testing.T) { @@ -46,12 +44,12 @@ func TestFlagSetParse(t *testing.T) { ] }`) - defs, err := mcp.LoadToolDefinitions(toolJSON) + defs, err := loadToolDefinitions(toolJSON) if err != nil { t.Fatalf("failed to load tool json: %v", err) } - flagSet, vars, err := buildArgFlagSet(defs["sg_test_tool"]) + flagSet, vars, err := BuildArgFlagSet(defs["sg_test_tool"]) if err != nil { t.Fatalf("failed to build flagset from mcp tool definition: %v", err) } @@ -65,7 +63,7 @@ func TestFlagSetParse(t *testing.T) { if err := flagSet.Parse(args); err != nil { t.Fatalf("flagset parsing failed: %v", err) } - derefFlagValues(vars) + DerefFlagValues(vars) if v, ok := vars["repos"].([]string); ok { if len(v) != 2 { diff --git a/internal/mcp/mcp_parse.go b/internal/mcp/mcp_parse.go index 7df4692237..55e9650fd2 100644 --- a/internal/mcp/mcp_parse.go +++ b/internal/mcp/mcp_parse.go @@ -69,10 +69,10 @@ type parser struct { } func LoadDefaultToolDefinitions() (map[string]*ToolDef, error) { - return LoadToolDefinitions(mcpToolListJSON) + return loadToolDefinitions(mcpToolListJSON) } -func LoadToolDefinitions(data []byte) (map[string]*ToolDef, error) { +func loadToolDefinitions(data []byte) (map[string]*ToolDef, error) { defs := struct { Tools []struct { Name string `json:"name"` From a5f1bd0931cd16badbc5377f492eff17e2b2e802 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Tue, 2 Dec 2025 17:14:43 +0200 Subject: [PATCH 5/6] guard against nil tool definitions when building flagsets --- internal/mcp/mcp_args.go | 4 ++++ internal/mcp/mcp_args_test.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/mcp/mcp_args.go b/internal/mcp/mcp_args.go index c31270e152..c86f9e573f 100644 --- a/internal/mcp/mcp_args.go +++ b/internal/mcp/mcp_args.go @@ -1,6 +1,7 @@ package mcp import ( + "errors" "flag" "fmt" "reflect" @@ -36,6 +37,9 @@ func DerefFlagValues(vars map[string]any) { } func BuildArgFlagSet(tool *ToolDef) (*flag.FlagSet, map[string]any, error) { + if tool == nil { + return nil, nil, errors.New("cannot build flagset on nil Tool Definition") + } fs := flag.NewFlagSet(tool.Name, flag.ContinueOnError) flagVars := map[string]any{} diff --git a/internal/mcp/mcp_args_test.go b/internal/mcp/mcp_args_test.go index 33708c9bf5..17d5b466e0 100644 --- a/internal/mcp/mcp_args_test.go +++ b/internal/mcp/mcp_args_test.go @@ -49,7 +49,7 @@ func TestFlagSetParse(t *testing.T) { t.Fatalf("failed to load tool json: %v", err) } - flagSet, vars, err := BuildArgFlagSet(defs["sg_test_tool"]) + flagSet, vars, err := BuildArgFlagSet(defs["test-tool"]) if err != nil { t.Fatalf("failed to build flagset from mcp tool definition: %v", err) } From f7526701e7a1c15c293e92ab3f89062bf443f662 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Tue, 2 Dec 2025 17:19:47 +0200 Subject: [PATCH 6/6] use github.com/sourcegraph/sourcegraphh/lib/errors --- internal/mcp/mcp_args.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/mcp/mcp_args.go b/internal/mcp/mcp_args.go index c86f9e573f..5b5b1ccc01 100644 --- a/internal/mcp/mcp_args.go +++ b/internal/mcp/mcp_args.go @@ -1,11 +1,12 @@ package mcp import ( - "errors" "flag" "fmt" "reflect" "strings" + + "github.com/sourcegraph/sourcegraph/lib/errors" ) var _ flag.Value = (*strSliceFlag)(nil)