diff --git a/frontend/.gitignore b/frontend/.gitignore index 3f550451fdf..8963af934f6 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,3 +1,4 @@ /src/host_orchestrator/host_orchestrator /src/cvdserver_bootstrapper/cvdserver_bootstrapper /src/operator/operator +go.work.sum diff --git a/frontend/MODULE.bazel b/frontend/MODULE.bazel index 0a44427de22..4170dc94db8 100644 --- a/frontend/MODULE.bazel +++ b/frontend/MODULE.bazel @@ -5,6 +5,7 @@ bazel_dep(name = "gazelle", version = "0.50.0") go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps") go_deps.from_file(go_work = "//:go.work") + # Note: The following com_github_google_android_cuttlefish_frontend_src_* repos # are listed here because Gazelle's go_deps extension reports them as direct # dependencies based on go.mod files. However, they are NOT pulled from external @@ -22,6 +23,7 @@ use_repo( "com_github_gorilla_mux", "com_github_gorilla_websocket", "com_github_hashicorp_go_multierror", + "com_github_modelcontextprotocol_go_sdk", "com_github_pion_logging", "com_github_pion_webrtc_v3", "org_golang_google_grpc", diff --git a/frontend/go.work b/frontend/go.work index 4bb7b44bb4d..5874550c36e 100644 --- a/frontend/go.work +++ b/frontend/go.work @@ -1,4 +1,4 @@ -go 1.18 +go 1.25 use ( ./src/host_orchestrator diff --git a/frontend/src/host_orchestrator/go.mod b/frontend/src/host_orchestrator/go.mod index a51932e2287..26fde81297d 100644 --- a/frontend/src/host_orchestrator/go.mod +++ b/frontend/src/host_orchestrator/go.mod @@ -1,11 +1,11 @@ module github.com/google/android-cuttlefish/frontend/src/host_orchestrator -go 1.17 +go 1.18 require ( github.com/google/android-cuttlefish/frontend/src/liboperator v0.0.0-20240822182916-7bea0dafdbde github.com/google/btree v1.1.3 - github.com/google/go-cmp v0.5.9 + github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/gorilla/mux v1.8.0 github.com/hashicorp/go-multierror v1.1.1 @@ -13,9 +13,15 @@ require ( require ( github.com/golang/protobuf v1.5.3 // indirect + github.com/google/jsonschema-go v0.4.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/modelcontextprotocol/go-sdk v1.6.0 // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/segmentio/encoding v0.5.4 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect - golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.3.0 // indirect google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect google.golang.org/grpc v1.40.0 // indirect diff --git a/frontend/src/host_orchestrator/go.sum b/frontend/src/host_orchestrator/go.sum index 1e84a2343dc..04776ea512e 100644 --- a/frontend/src/host_orchestrator/go.sum +++ b/frontend/src/host_orchestrator/go.sum @@ -43,6 +43,9 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= +github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -51,12 +54,22 @@ github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB7 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/modelcontextprotocol/go-sdk v1.6.0 h1:PPLS3kn7WtOEnR+Af4X5H96SG0qSab8R/ZQT/HkhPkY= +github.com/modelcontextprotocol/go-sdk v1.6.0/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -74,6 +87,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -83,6 +98,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/frontend/src/host_orchestrator/orchestrator/BUILD.bazel b/frontend/src/host_orchestrator/orchestrator/BUILD.bazel index 8e63f8f70ca..1975dd92150 100644 --- a/frontend/src/host_orchestrator/orchestrator/BUILD.bazel +++ b/frontend/src/host_orchestrator/orchestrator/BUILD.bazel @@ -19,6 +19,7 @@ go_library( "instancemanager.go", "listcvdsaction.go", "listscreenrecordingsaction.go", + "mcp.go", "operation.go", "resetcvdaction.go", "startcvdaction.go", @@ -37,6 +38,7 @@ go_library( "@com_github_google_btree//:btree", "@com_github_google_uuid//:uuid", "@com_github_gorilla_mux//:mux", + "@com_github_modelcontextprotocol_go_sdk//mcp", ], ) @@ -48,6 +50,7 @@ go_test( "imagedirectories_test.go", "instancemanager_test.go", "listcvdsaction_test.go", + "mcp_test.go", "operation_test.go", "userartifacts_test.go", "validation_test.go", diff --git a/frontend/src/host_orchestrator/orchestrator/controller.go b/frontend/src/host_orchestrator/orchestrator/controller.go index 87f507bd0c7..38ce78793ea 100644 --- a/frontend/src/host_orchestrator/orchestrator/controller.go +++ b/frontend/src/host_orchestrator/orchestrator/controller.go @@ -136,6 +136,7 @@ func (c *Controller) AddRoutes(router *mux.Router) { router.Handle("/cvd_imgs_dirs/{id}", httpHandler(&updateImageDirectoryHandler{c.ImageDirectoriesManager, c.OperationManager, c.UserArtifactsManager})).Methods("PUT") router.Handle("/cvd_imgs_dirs/{id}", httpHandler(&deleteImageDirectoryHandler{c.ImageDirectoriesManager, c.OperationManager})).Methods("DELETE") router.Handle("/reset", httpHandler(&resetCVDHandler{c.OperationManager})).Methods("POST") + router.PathPrefix("/v0/sse").Handler(NewMCPHandler(c.Config, c.OperationManager, c.UserArtifactsManager)).Methods("DELETE", "GET", "POST") // Debug endpoints. router.Handle("/_debug/varz", httpHandler(&getDebugVariablesHandler{c.DebugVariablesManager})).Methods("GET") router.Handle("/_debug/statusz", okHandler()).Methods("GET") diff --git a/frontend/src/host_orchestrator/orchestrator/mcp.go b/frontend/src/host_orchestrator/orchestrator/mcp.go new file mode 100644 index 00000000000..4f89a8807db --- /dev/null +++ b/frontend/src/host_orchestrator/orchestrator/mcp.go @@ -0,0 +1,104 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package orchestrator + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "os/exec" + + apiv1 "github.com/google/android-cuttlefish/frontend/src/host_orchestrator/api/v1" + "github.com/google/uuid" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +type ListCVDsParams struct { + Group string `json:"group,omitempty"` +} + +func NewMCPHandler(config Config, om OperationManager, uam UserArtifactsManager) http.Handler { + server := mcp.NewServer(&mcp.Implementation{Name: "host-orchestrator", Version: "1.0"}, &mcp.ServerOptions{ + InitializedHandler: func(context.Context, *mcp.InitializedRequest) { + fmt.Println("initialized!") + }, + GetSessionID: func() string { + return "" + }, + }) + + mcp.AddTool(server, &mcp.Tool{ + Name: "list_cvds", + Description: "List all cuttlefish instances", + }, func(ctx context.Context, req *mcp.CallToolRequest, args ListCVDsParams) (*mcp.CallToolResult, any, error) { + action := NewListCVDsAction(ListCVDsActionOpts{ + Group: args.Group, + Paths: config.Paths, + ExecContext: exec.CommandContext, + }) + resp, err := action.Run() + if err != nil { + return nil, nil, err + } + jsonData, err := json.Marshal(resp) + if err != nil { + return nil, nil, err + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(jsonData)}, + }, + }, nil, nil + }) + + mcp.AddTool(server, &mcp.Tool{ + Name: "create_cvd", + Description: "Create cuttlefish instances", + }, func(ctx context.Context, mcpReq *mcp.CallToolRequest, args apiv1.CreateCVDRequest) (*mcp.CallToolResult, any, error) { + dummyReq, _ := http.NewRequest("POST", "http://dummy", nil) + creds := getFetchCredentials(config.BuildAPICredentials, dummyReq) + cvdBundleFetcher := newFetchCVDCommandArtifactsFetcher(exec.CommandContext, creds, config.AndroidBuildServiceURL) + opts := CreateCVDActionOpts{ + Request: &args, + HostValidator: &HostValidator{ExecContext: exec.CommandContext}, + Paths: config.Paths, + OperationManager: om, + ExecContext: exec.CommandContext, + CVDBundleFetcher: cvdBundleFetcher, + UUIDGen: func() string { return uuid.New().String() }, + UserArtifactsDirResolver: uam, + FetchCredentials: creds, + BuildAPIBaseURL: config.AndroidBuildServiceURL, + } + resp, err := NewCreateCVDAction(opts).Run() + if err != nil { + return nil, nil, err + } + jsonData, err := json.Marshal(resp) + if err != nil { + return nil, nil, err + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(jsonData)}, + }, + }, nil, nil + }) + + return mcp.NewStreamableHTTPHandler(func(request *http.Request) *mcp.Server { + return server + }, &mcp.StreamableHTTPOptions{JSONResponse: true}) +} diff --git a/frontend/src/host_orchestrator/orchestrator/mcp_test.go b/frontend/src/host_orchestrator/orchestrator/mcp_test.go new file mode 100644 index 00000000000..21c5a827296 --- /dev/null +++ b/frontend/src/host_orchestrator/orchestrator/mcp_test.go @@ -0,0 +1,31 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package orchestrator + +import ( + "testing" +) + +func TestNewMCPHandler(t *testing.T) { + config := Config{ + Paths: IMPaths{ + RootDir: "/tmp", + }, + } + handler := NewMCPHandler(config, nil, nil) + if handler == nil { + t.Fatal("Expected handler to be non-nil") + } +}