diff --git a/cmd/src/main.go b/cmd/src/main.go index 9f8ba4ca33..edfb1073d7 100644 --- a/cmd/src/main.go +++ b/cmd/src/main.go @@ -63,7 +63,6 @@ The commands are: search search for results on Sourcegraph search-jobs manages search jobs serve-git serves your local git repositories over HTTP for Sourcegraph to pull - tool exposes tools for AI agents to interact with Sourcegraph (EXPERIMENTAL) users,user manages users codeowners manages code ownership information version display and compare the src-cli version against the recommended version for your instance diff --git a/cmd/src/mcp.go b/cmd/src/mcp.go index 604c4c9fb9..d7a9127f6a 100644 --- a/cmd/src/mcp.go +++ b/cmd/src/mcp.go @@ -43,10 +43,73 @@ func mcpMain(args []string) error { if !ok { return fmt.Errorf("tool definition for %q not found - run src mcp list-tools to see a list of available tools", subcmd) } - return handleMcpTool(tool, args[1:]) + + 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) } -func handleMcpTool(tool *mcp.ToolDef, args []string) error { - fmt.Printf("handling tool %q args: %+v", tool.Name, args) +func printSchemas(tool *mcp.ToolDef) error { + input, err := json.MarshalIndent(tool.InputSchema, "", " ") + if err != nil { + return err + } + output, err := json.MarshalIndent(tool.OutputSchema, "", " ") + 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) + } + } + + 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")) + } + + return nil +} + +func handleMcpTool(ctx context.Context, client api.Client, tool *mcp.ToolDef, vars map[string]any) error { + 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, "", " ") + if err != nil { + return err + } + fmt.Println(string(output)) return nil } diff --git a/go.mod b/go.mod index 818ed24368..c0c693132e 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/sourcegraph/scip v0.6.1 github.com/sourcegraph/sourcegraph/lib v0.0.0-20240709083501-1af563b61442 github.com/stretchr/testify v1.11.1 - golang.org/x/net v0.46.0 + golang.org/x/net v0.47.0 golang.org/x/sync v0.18.0 google.golang.org/api v0.256.0 google.golang.org/protobuf v1.36.10 @@ -67,7 +67,7 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v24.0.4+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/docker v25.0.6+incompatible // indirect + github.com/docker/docker v28.0.0+incompatible // indirect github.com/docker/docker-credential-helpers v0.8.0 // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect @@ -75,7 +75,7 @@ require ( github.com/felixge/fgprof v0.9.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-chi/chi/v5 v5.0.10 // indirect + github.com/go-chi/chi/v5 v5.2.2 // indirect github.com/go-jose/go-jose/v4 v4.1.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gofrs/uuid/v5 v5.0.0 // indirect @@ -91,11 +91,12 @@ require ( github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0-rc4 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect - github.com/rs/cors v1.9.0 // indirect + github.com/rs/cors v1.11.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/beaut v0.0.0-20240611013027-627e4c25335a // indirect github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect @@ -116,7 +117,7 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.7.1 // indirect - golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 // indirect + golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect @@ -216,14 +217,14 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/mod v0.28.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.30.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.14.0 // indirect - golang.org/x/tools v0.37.0 // indirect + golang.org/x/tools v0.38.0 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/grpc v1.76.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect; direct diff --git a/go.sum b/go.sum index 8c25134d27..22f5dcf5a7 100644 --- a/go.sum +++ b/go.sum @@ -154,8 +154,8 @@ github.com/docker/cli v24.0.4+incompatible h1:Y3bYF9ekNTm2VFz5U/0BlMdJy73D+Y1iAA github.com/docker/cli v24.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v25.0.6+incompatible h1:5cPwbwriIcsua2REJe8HqQV+6WlWc1byg2QSXzBxBGg= -github.com/docker/docker v25.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.0.0+incompatible h1:Olh0KS820sJ7nPsBKChVhk5pzqcwDR15fumfAd/p9hM= +github.com/docker/docker v28.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8= github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= @@ -188,8 +188,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= -github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= -github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= +github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= @@ -344,6 +344,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -402,8 +404,8 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= -github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= @@ -513,16 +515,16 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w= golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -530,8 +532,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -563,8 +565,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053 h1:dHQOQddU4YHS5gY33/6klKjq7Gp3WwMyOXGNp5nzRj8= -golang.org/x/telemetry v0.0.0-20250908211612-aef8a434d053/go.mod h1:+nZKN+XVh4LCiA9DV3ywrzN4gumyCnKjau3NGb9SGoE= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU= +golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= @@ -574,8 +576,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -586,8 +588,8 @@ golang.org/x/tools v0.0.0-20200624163319-25775e59acb7/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/mcp/mcp_args.go b/internal/mcp/mcp_args.go new file mode 100644 index 0000000000..ecfb98e2b0 --- /dev/null +++ b/internal/mcp/mcp_args.go @@ -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 +} diff --git a/internal/mcp/mcp_args_test.go b/internal/mcp/mcp_args_test.go new file mode 100644 index 0000000000..17d5b466e0 --- /dev/null +++ b/internal/mcp/mcp_args_test.go @@ -0,0 +1,97 @@ +package mcp + +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 := loadToolDefinitions(toolJSON) + if err != nil { + t.Fatalf("failed to load tool json: %v", err) + } + + flagSet, vars, err := BuildArgFlagSet(defs["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) + } + +} diff --git a/internal/mcp/mcp_parse.go b/internal/mcp/mcp_parse.go index 8a4b12bb22..cbd7b23bcc 100644 --- a/internal/mcp/mcp_parse.go +++ b/internal/mcp/mcp_parse.go @@ -63,7 +63,7 @@ type decoder struct { errors []error } -func LoadToolDefinitions() (map[string]*ToolDef, error) { +func LoadDefaultToolDefinitions() (map[string]*ToolDef, error) { return loadToolDefinitions(mcpToolListJSON) } diff --git a/internal/mcp/mcp_request.go b/internal/mcp/mcp_request.go new file mode 100644 index 0000000000..dbcb0ed97b --- /dev/null +++ b/internal/mcp/mcp_request.go @@ -0,0 +1,93 @@ +package mcp + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + + "github.com/sourcegraph/src-cli/internal/api" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +const McpURLPath = ".api/mcp/v1" + +func DoToolRequest(ctx context.Context, client api.Client, tool *ToolDef, vars map[string]any) (*http.Response, error) { + jsonRPC := struct { + Version string `json:"jsonrpc"` + ID int `json:"id"` + Method string `json:"method"` + Params any `json:"params"` + }{ + Version: "2.0", + ID: 1, + Method: "tools/call", + Params: struct { + Name string `json:"name"` + Arguments map[string]any `json:"arguments"` + }{ + Name: tool.RawName, + Arguments: vars, + }, + } + + buf := bytes.NewBuffer(nil) + data, err := json.Marshal(jsonRPC) + if err != nil { + return nil, err + } + buf.Write(data) + + req, err := client.NewHTTPRequest(ctx, http.MethodPost, McpURLPath, buf) + if err != nil { + return nil, err + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "*/*") + + return client.Do(req) +} + +func DecodeToolResponse(resp *http.Response) (map[string]json.RawMessage, error) { + data, err := readSSEResponseData(resp) + if err != nil { + return nil, err + } + + if data == nil { + return map[string]json.RawMessage{}, nil + } + + jsonRPCResp := struct { + Version string `json:"jsonrpc"` + ID int `json:"id"` + Result struct { + Content []json.RawMessage `json:"content"` + StructuredContent map[string]json.RawMessage `json:"structuredContent"` + } `json:"result"` + }{} + if err := json.Unmarshal(data, &jsonRPCResp); err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal MCP JSON-RPC response") + } + + return jsonRPCResp.Result.StructuredContent, nil +} +func readSSEResponseData(resp *http.Response) ([]byte, error) { + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + // The response is an SSE reponse + // event: + // data: + lines := bytes.SplitSeq(data, []byte("\n")) + for line := range lines { + if jsonData, ok := bytes.CutPrefix(line, []byte("data: ")); ok { + return jsonData, nil + } + } + return nil, errors.New("no data found in SSE response") + +}