Skip to content

Commit 66f8243

Browse files
committed
Add a MCP endpoint to the host orchestrator with create_cvd and list_cvds tools
Assisted-by: Antigravity:Gemini Next
1 parent f2e80f6 commit 66f8243

8 files changed

Lines changed: 168 additions & 4 deletions

File tree

frontend/MODULE.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ bazel_dep(name = "gazelle", version = "0.50.0")
55

66
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
77
go_deps.from_file(go_work = "//:go.work")
8+
89
# Note: The following com_github_google_android_cuttlefish_frontend_src_* repos
910
# are listed here because Gazelle's go_deps extension reports them as direct
1011
# dependencies based on go.mod files. However, they are NOT pulled from external
@@ -22,6 +23,7 @@ use_repo(
2223
"com_github_gorilla_mux",
2324
"com_github_gorilla_websocket",
2425
"com_github_hashicorp_go_multierror",
26+
"com_github_modelcontextprotocol_go_sdk",
2527
"com_github_pion_logging",
2628
"com_github_pion_webrtc_v3",
2729
"org_golang_google_grpc",

frontend/go.work

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
go 1.18
1+
go 1.25.0
22

33
use (
44
./src/host_orchestrator

frontend/src/host_orchestrator/go.mod

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
module github.com/google/android-cuttlefish/frontend/src/host_orchestrator
22

3-
go 1.17
3+
go 1.25.0
44

55
require (
66
github.com/google/android-cuttlefish/frontend/src/liboperator v0.0.0-20240822182916-7bea0dafdbde
77
github.com/google/btree v1.1.3
8-
github.com/google/go-cmp v0.5.9
8+
github.com/google/go-cmp v0.7.0
99
github.com/google/uuid v1.6.0
1010
github.com/gorilla/mux v1.8.0
1111
github.com/hashicorp/go-multierror v1.1.1
1212
)
1313

1414
require (
1515
github.com/golang/protobuf v1.5.3 // indirect
16+
github.com/google/jsonschema-go v0.4.3 // indirect
1617
github.com/gorilla/websocket v1.5.3 // indirect
18+
github.com/modelcontextprotocol/go-sdk v1.6.0 // indirect
19+
github.com/segmentio/asm v1.1.3 // indirect
20+
github.com/segmentio/encoding v0.5.4 // indirect
21+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
1722
golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
18-
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect
23+
golang.org/x/oauth2 v0.35.0 // indirect
24+
golang.org/x/sys v0.41.0 // indirect
1925
golang.org/x/text v0.3.0 // indirect
2026
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
2127
google.golang.org/grpc v1.40.0 // indirect

frontend/src/host_orchestrator/go.sum

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
4343
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
4444
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
4545
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
46+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
47+
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
48+
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
4649
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
4750
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
4851
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
5154
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
5255
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
5356
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
57+
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
58+
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
59+
github.com/modelcontextprotocol/go-sdk v1.6.0 h1:PPLS3kn7WtOEnR+Af4X5H96SG0qSab8R/ZQT/HkhPkY=
60+
github.com/modelcontextprotocol/go-sdk v1.6.0/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ=
5461
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5562
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
5663
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
64+
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
65+
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
66+
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
67+
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
5768
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
5869
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
5970
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
71+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
72+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
6073
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
6174
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
6275
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
7487
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
7588
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
7689
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
90+
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
91+
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
7792
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
7893
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
7994
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
8398
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
8499
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
85100
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
101+
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
102+
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
86103
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
87104
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
88105
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

frontend/src/host_orchestrator/orchestrator/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ go_library(
1919
"instancemanager.go",
2020
"listcvdsaction.go",
2121
"listscreenrecordingsaction.go",
22+
"mcp.go",
2223
"operation.go",
2324
"resetcvdaction.go",
2425
"startcvdaction.go",
@@ -37,6 +38,7 @@ go_library(
3738
"@com_github_google_btree//:btree",
3839
"@com_github_google_uuid//:uuid",
3940
"@com_github_gorilla_mux//:mux",
41+
"@com_github_modelcontextprotocol_go_sdk//mcp",
4042
],
4143
)
4244

@@ -48,6 +50,7 @@ go_test(
4850
"imagedirectories_test.go",
4951
"instancemanager_test.go",
5052
"listcvdsaction_test.go",
53+
"mcp_test.go",
5154
"operation_test.go",
5255
"userartifacts_test.go",
5356
"validation_test.go",

frontend/src/host_orchestrator/orchestrator/controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ func (c *Controller) AddRoutes(router *mux.Router) {
136136
router.Handle("/cvd_imgs_dirs/{id}", httpHandler(&updateImageDirectoryHandler{c.ImageDirectoriesManager, c.OperationManager, c.UserArtifactsManager})).Methods("PUT")
137137
router.Handle("/cvd_imgs_dirs/{id}", httpHandler(&deleteImageDirectoryHandler{c.ImageDirectoriesManager, c.OperationManager})).Methods("DELETE")
138138
router.Handle("/reset", httpHandler(&resetCVDHandler{c.OperationManager})).Methods("POST")
139+
router.PathPrefix("/v0/sse").Handler(NewMCPHandler(c.Config, c.OperationManager, c.UserArtifactsManager)).Methods("DELETE", "GET", "POST")
139140
// Debug endpoints.
140141
router.Handle("/_debug/varz", httpHandler(&getDebugVariablesHandler{c.DebugVariablesManager})).Methods("GET")
141142
router.Handle("/_debug/statusz", okHandler()).Methods("GET")
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package orchestrator
16+
17+
import (
18+
"context"
19+
"encoding/json"
20+
"fmt"
21+
"net/http"
22+
"os/exec"
23+
24+
apiv1 "github.com/google/android-cuttlefish/frontend/src/host_orchestrator/api/v1"
25+
"github.com/google/uuid"
26+
"github.com/modelcontextprotocol/go-sdk/mcp"
27+
)
28+
29+
type ListCVDsParams struct {
30+
Group string `json:"group,omitempty"`
31+
}
32+
33+
func NewMCPHandler(config Config, om OperationManager, uam UserArtifactsManager) http.Handler {
34+
server := mcp.NewServer(&mcp.Implementation{Name: "host-orchestrator", Version: "1.0"}, &mcp.ServerOptions{
35+
InitializedHandler: func(context.Context, *mcp.InitializedRequest) {
36+
fmt.Println("initialized!")
37+
},
38+
GetSessionID: func() string {
39+
return ""
40+
},
41+
})
42+
43+
mcp.AddTool(server, &mcp.Tool{
44+
Name: "list_cvds",
45+
Description: "List all cuttlefish instances",
46+
}, func(ctx context.Context, req *mcp.CallToolRequest, args ListCVDsParams) (*mcp.CallToolResult, any, error) {
47+
action := NewListCVDsAction(ListCVDsActionOpts{
48+
Group: args.Group,
49+
Paths: config.Paths,
50+
ExecContext: exec.CommandContext,
51+
})
52+
resp, err := action.Run()
53+
if err != nil {
54+
return nil, nil, err
55+
}
56+
jsonData, err := json.Marshal(resp)
57+
if err != nil {
58+
return nil, nil, err
59+
}
60+
return &mcp.CallToolResult{
61+
Content: []mcp.Content{
62+
&mcp.TextContent{Text: string(jsonData)},
63+
},
64+
}, nil, nil
65+
})
66+
67+
mcp.AddTool(server, &mcp.Tool{
68+
Name: "create_cvd",
69+
Description: "Create cuttlefish instances",
70+
}, func(ctx context.Context, mcpReq *mcp.CallToolRequest, args apiv1.CreateCVDRequest) (*mcp.CallToolResult, any, error) {
71+
dummyReq, _ := http.NewRequest("POST", "http://dummy", nil)
72+
creds := getFetchCredentials(config.BuildAPICredentials, dummyReq)
73+
cvdBundleFetcher := newFetchCVDCommandArtifactsFetcher(exec.CommandContext, creds, config.AndroidBuildServiceURL)
74+
opts := CreateCVDActionOpts{
75+
Request: &args,
76+
HostValidator: &HostValidator{ExecContext: exec.CommandContext},
77+
Paths: config.Paths,
78+
OperationManager: om,
79+
ExecContext: exec.CommandContext,
80+
CVDBundleFetcher: cvdBundleFetcher,
81+
UUIDGen: func() string { return uuid.New().String() },
82+
UserArtifactsDirResolver: uam,
83+
FetchCredentials: creds,
84+
BuildAPIBaseURL: config.AndroidBuildServiceURL,
85+
}
86+
resp, err := NewCreateCVDAction(opts).Run()
87+
if err != nil {
88+
return nil, nil, err
89+
}
90+
jsonData, err := json.Marshal(resp)
91+
if err != nil {
92+
return nil, nil, err
93+
}
94+
return &mcp.CallToolResult{
95+
Content: []mcp.Content{
96+
&mcp.TextContent{Text: string(jsonData)},
97+
},
98+
}, nil, nil
99+
})
100+
101+
return mcp.NewStreamableHTTPHandler(func(request *http.Request) *mcp.Server {
102+
return server
103+
}, &mcp.StreamableHTTPOptions{JSONResponse: true})
104+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package orchestrator
16+
17+
import (
18+
"testing"
19+
)
20+
21+
func TestNewMCPHandler(t *testing.T) {
22+
config := Config{
23+
Paths: IMPaths{
24+
RootDir: "/tmp",
25+
},
26+
}
27+
handler := NewMCPHandler(config, nil, nil)
28+
if handler == nil {
29+
t.Fatal("Expected handler to be non-nil")
30+
}
31+
}

0 commit comments

Comments
 (0)