Skip to content

Commit 2b11fb9

Browse files
authored
feat: add API v2 MCP tasks gateway (#2576)
## Summary - replace the legacy MCP implementation with an authenticated stateless API v2 endpoint - expose AgentInstance discovery and invocation through the in-process public A2A gateway - support the MCP Tasks extension for durable polling, cancellation, and input-required continuation, with synchronous fallback - remove the legacy MCP handler and in-memory agent client registry ## Testing - go test ./core/... - make -C go lint - go test ./core/v2/mcp ./core/v2/a2agateway -timeout=2m - local Kind E2E: TestMCPAgentInstanceInteraction, TestMCPAskUserContinuation, TestMCPCancelTask --------- Signed-off-by: Eitan Yarmush <eitan.yarmush@solo.io>
1 parent 2a25933 commit 2b11fb9

13 files changed

Lines changed: 1547 additions & 629 deletions

File tree

docs/plans/api-v2-execution-plan.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,10 @@ MCP:
326326

327327
- Discover ready AgentInstances.
328328
- Invoke them only through the public gateway A2A path.
329+
- Expose durable A2A turns through the MCP Tasks extension, including polling,
330+
cancellation, and input-required continuation.
331+
- Keep synchronous `tools/call` fallback for clients without Tasks support.
332+
- Store no MCP-owned task or session state.
329333
- Never expose Actor or private MCP endpoints.
330334

331335
Content:

go/core/cmd/controller-v2/main.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@ import (
3535
"github.com/kagent-dev/kagent/go/core/internal/service/kubecrud"
3636
sessionservice "github.com/kagent-dev/kagent/go/core/internal/service/session"
3737
taskservice "github.com/kagent-dev/kagent/go/core/internal/service/task"
38+
"github.com/kagent-dev/kagent/go/core/pkg/auth"
3839
"github.com/kagent-dev/kagent/go/core/pkg/migrations"
3940
legacysubstrate "github.com/kagent-dev/kagent/go/core/pkg/sandboxbackend/substrate"
4041
"github.com/kagent-dev/kagent/go/core/v2/a2agateway"
4142
"github.com/kagent-dev/kagent/go/core/v2/agentinstance"
4243
"github.com/kagent-dev/kagent/go/core/v2/checkpoint"
4344
v2controller "github.com/kagent-dev/kagent/go/core/v2/controller"
45+
v2mcp "github.com/kagent-dev/kagent/go/core/v2/mcp"
4446
"golang.org/x/sync/errgroup"
4547
k8sruntime "k8s.io/apimachinery/pkg/runtime"
4648
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -126,6 +128,12 @@ func main() {
126128
if err != nil {
127129
log.Fatal(err)
128130
}
131+
gateway := a2agateway.New(store, authorizer, gatewayDialer, instanceWorkflow,
132+
env("A2A_GATEWAY_URL", "http://127.0.0.1:8084"))
133+
mcpHandler, err := v2mcp.New(instances, gateway)
134+
if err != nil {
135+
log.Fatal(err)
136+
}
129137
server, err := grpcserver.New(grpcserver.Config{
130138
BindAddress: env("GRPC_BIND_ADDRESS", ":8084"),
131139
Reflection: envBool("GRPC_REFLECTION"),
@@ -139,10 +147,7 @@ func main() {
139147
AgentTemplateService: kubecrud.NewService(manager.GetClient(), authorizer, &kagentv1alpha3.AgentTemplate{}, &kagentv1alpha3.AgentTemplateList{}, "AgentTemplate"),
140148
HarnessService: kubecrud.NewService(manager.GetClient(), authorizer, &kagentv1alpha3.Harness{}, &kagentv1alpha3.HarnessList{}, "Harness"),
141149
CheckpointService: checkpoints,
142-
// `instanceWorkflow` is what upstream added: the gateway needs it to suspend an
143-
// instance once a turn reaches a quiescent boundary.
144-
A2AHandler: a2agateway.New(store, authorizer, gatewayDialer, instanceWorkflow,
145-
env("A2A_GATEWAY_URL", "http://127.0.0.1:8084")),
150+
A2AHandler: gateway,
146151
})
147152
if err != nil {
148153
log.Fatal(err)
@@ -156,12 +161,14 @@ func main() {
156161
// It answered every path with an empty 200 and ignored the request entirely, so
157162
// a browser calling an RPC got a success with no body — which reads as a
158163
// serialisation fault in the client rather than as a server that never had the
159-
// endpoint. The router below hands anything that is not gRPC-Web to the same
160-
// health response as before.
161-
httpHandler := server.WebHandlerOr(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
164+
// endpoint. The router below serves MCP and hands other non-gRPC-Web requests
165+
// to the same health response as before.
166+
mux := http.NewServeMux()
167+
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
162168
w.WriteHeader(http.StatusOK)
163-
}))
164-
health := &http.Server{Addr: env("HTTP_BIND_ADDRESS", ":8083"), Handler: httpHandler}
169+
})
170+
mux.Handle("/mcp", auth.AuthnMiddleware(authenticator)(mcpHandler))
171+
health := &http.Server{Addr: env("HTTP_BIND_ADDRESS", ":8083"), Handler: server.WebHandlerOr(mux)}
165172
group, ctx := errgroup.WithContext(ctx)
166173
group.Go(func() error { return runtime.Start(ctx) })
167174
group.Go(func() error { return manager.Start(ctx) })

go/core/internal/a2a/agent_client_registry.go

Lines changed: 0 additions & 45 deletions
This file was deleted.

0 commit comments

Comments
 (0)