-
Notifications
You must be signed in to change notification settings - Fork 70
feat(mcp): add mcp list-tools to list available tool calls #1213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2612c77
d129dcd
5c8cc69
99de29b
0fe7d46
b3ad44a
1126848
7464a05
fb74076
31dcefd
e27f347
861bec5
0649d9b
be73930
de877c3
20a05df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "flag" | ||
| "fmt" | ||
|
|
||
| "github.com/sourcegraph/src-cli/internal/mcp" | ||
| ) | ||
|
|
||
| func init() { | ||
| flagSet := flag.NewFlagSet("mcp", flag.ExitOnError) | ||
| commands = append(commands, &command{ | ||
| flagSet: flagSet, | ||
| handler: mcpMain, | ||
| }) | ||
| } | ||
| func mcpMain(args []string) error { | ||
| fmt.Println("NOTE: This command is still experimental") | ||
| tools, err := mcp.LoadToolDefinitions() | ||
|
Check failure on line 19 in cmd/src/mcp.go
|
||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| subcmd := args[0] | ||
| if subcmd == "list-tools" { | ||
| fmt.Println("The following tools are available:") | ||
| for name := range tools { | ||
| fmt.Printf(" • %s\n", name) | ||
| } | ||
| fmt.Println("\nUSAGE:") | ||
| fmt.Printf(" • Invoke a tool\n") | ||
| fmt.Printf(" src mcp <tool-name> <flags>\n") | ||
| fmt.Printf("\n • View the Input / Output Schema of a tool\n") | ||
| fmt.Printf(" src mcp <tool-name> schema\n") | ||
| fmt.Printf("\n • List the available flags of a tool\n") | ||
| fmt.Printf(" src mcp <tool-name> -h\n") | ||
| fmt.Printf("\n • View the Input / Output Schema of a tool\n") | ||
| fmt.Printf(" src mcp <tool-name> schema\n") | ||
| return nil | ||
| } | ||
|
|
||
| tool, ok := tools[subcmd] | ||
| if !ok { | ||
| return fmt.Errorf("tool definition for %q not found - run src mcp list-tools to see a list of available tools", subcmd) | ||
| } | ||
|
|
||
| flagArgs := args[1:] // skip subcommand name | ||
| if len(args) > 1 && args[1] == "schema" { | ||
| return printSchemas(tool) | ||
| } | ||
|
|
||
| flags, vars, err := mcp.BuildArgFlagSet(tool) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if err := flags.Parse(flagArgs); err != nil { | ||
| return err | ||
| } | ||
| mcp.DerefFlagValues(vars) | ||
|
|
||
| if err := validateToolArgs(tool.InputSchema, args, vars); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| apiClient := cfg.apiClient(nil, flags.Output()) | ||
| return handleMcpTool(context.Background(), apiClient, tool, vars) | ||
|
Check failure on line 66 in cmd/src/mcp.go
|
||
| } | ||
|
|
||
| func printSchemas(tool *mcp.ToolDef) error { | ||
| input, err := json.MarshalIndent(tool.InputSchema, "", " ") | ||
|
Check failure on line 70 in cmd/src/mcp.go
|
||
| if err != nil { | ||
| return err | ||
| } | ||
| output, err := json.MarshalIndent(tool.OutputSchema, "", " ") | ||
|
Check failure on line 74 in cmd/src/mcp.go
|
||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| fmt.Printf("Input:\n%v\nOutput:\n%v\n", string(input), string(output)) | ||
| return nil | ||
| } | ||
|
|
||
| func validateToolArgs(inputSchema mcp.SchemaObject, args []string, vars map[string]any) error { | ||
| for _, reqName := range inputSchema.Required { | ||
| if vars[reqName] == nil { | ||
| return errors.Newf("no value provided for required flag --%s", reqName) | ||
|
Check failure on line 86 in cmd/src/mcp.go
|
||
| } | ||
| } | ||
|
|
||
| if len(args) < len(inputSchema.Required) { | ||
| return errors.Newf("not enough arguments provided - the following flags are required:\n%s", strings.Join(inputSchema.Required, "\n")) | ||
|
Check failure on line 91 in cmd/src/mcp.go
|
||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func handleMcpTool(ctx context.Context, client api.Client, tool *mcp.ToolDef, vars map[string]any) error { | ||
|
Check failure on line 97 in cmd/src/mcp.go
|
||
| resp, err := mcp.DoToolRequest(ctx, client, tool, vars) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| result, err := mcp.DecodeToolResponse(resp) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| output, err := json.MarshalIndent(result, "", " ") | ||
|
Check failure on line 109 in cmd/src/mcp.go
|
||
| if err != nil { | ||
| return err | ||
| } | ||
| fmt.Println(string(output)) | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| package mcp | ||
|
|
||
| import ( | ||
| "flag" | ||
| "fmt" | ||
| "reflect" | ||
| "strings" | ||
|
|
||
| "github.com/sourcegraph/sourcegraph/lib/errors" | ||
| ) | ||
|
|
||
| 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 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 | ||
| } | ||
| if isNil(vv) { | ||
| delete(vars, k) | ||
| } else { | ||
| vars[k] = vv | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func isNil(v any) bool { | ||
| if v == nil { | ||
| return true | ||
| } | ||
| rv := reflect.ValueOf(v) | ||
| switch rv.Kind() { | ||
| case reflect.Slice, reflect.Map, reflect.Pointer, reflect.Interface: | ||
| return rv.IsNil() | ||
| default: | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| 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{} | ||
|
|
||
| for name, pVal := range tool.InputSchema.Properties { | ||
| switch pv := pVal.(type) { | ||
| case *SchemaPrimitive: | ||
| switch pv.Type { | ||
| 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.Type) | ||
|
|
||
| } | ||
| case *SchemaArray: | ||
| strSlice := new(strSliceFlag) | ||
| fs.Var(strSlice, name, pv.Description) | ||
| flagVars[name] = strSlice | ||
| case *SchemaObject: | ||
| // TODO(burmudar): we can support SchemaObject as part of stdin echo '{ stuff }' | sg mcp commit-search | ||
| // not supported yet | ||
| // Also support sg mcp commit-search --json '{ stuff }' | ||
| } | ||
| } | ||
|
|
||
| return fs, flagVars, nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor: Why not just a normal ascii * instead of unicode stars? Ignore me if I am too much of an ASCII lover.