Skip to content

refactor(controller): invoke agents directly in MCP handler#1855

Open
onematchfox wants to merge 2 commits into
kagent-dev:mainfrom
onematchfox:refactor-mcp-agent-invocation
Open

refactor(controller): invoke agents directly in MCP handler#1855
onematchfox wants to merge 2 commits into
kagent-dev:mainfrom
onematchfox:refactor-mcp-agent-invocation

Conversation

@onematchfox
Copy link
Copy Markdown
Contributor

Replace the HTTP round-trip through the controller's own A2A listener with direct invocation via a new AgentClientRegistry. The registry is owned by A2ARegistrar, which already maintains an A2AClient per agent for its HTTP mux — the registry gives the MCP handler access to those same clients without an extra network hop.

The old approach routed through the controller's public A2A endpoint, meaning requests could traverse the external network (and any ingress or load-balancer in front of it) unnecessarily. The new path stays in-process.

The old handler also cached its own A2AClient per agent in a sync.Map with no eviction, so clients for deleted agents would remain indefinitely. The registry is kept consistent by the registrar's add/update/delete lifecycle, eliminating that staleness.

A2ARegistrar.upsertAgentHandler writes to both the HTTP mux (for inbound /api/a2a/// routing) and the registry (for direct invocation). The registry is exposed via ClientRegistry() and passed to NewMCPHandler in app.go.

Note:

Copilot AI review requested due to automatic review settings May 12, 2026 14:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the controller’s MCP invoke_agent tool path to avoid an HTTP round-trip through the controller’s own A2A listener by invoking agents in-process via a new shared AgentClientRegistry owned/populated by A2ARegistrar.

Changes:

  • Added AgentClientRegistry and wired A2ARegistrar to keep it in sync with agent add/update/delete events.
  • Updated MCP handler to send A2A messages via the registry (direct client invocation) and to recover auth sessions from RequestExtra headers for token propagation.
  • Updated app wiring and added MCP auth-propagation tests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
go/core/pkg/app/app.go Wires A2ARegistrar and passes its ClientRegistry() into the MCP handler.
go/core/internal/mcp/mcp_handler.go Switches MCP invocation to registry-based A2A client calls; rehydrates auth session from MCP RequestExtra.
go/core/internal/mcp/mcp_handler_test.go Adds coverage for auth propagation behavior through MCP -> A2A.
go/core/internal/a2a/agent_client_registry.go Introduces a thread-safe registry of A2A clients keyed by agent route.
go/core/internal/a2a/a2a_registrar.go Populates/cleans the registry alongside existing mux handler lifecycle.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +44 to +54
type a2aBackend struct {
server *httptest.Server
lastAuthHeader string
}

func newA2ABackend(t *testing.T) *a2aBackend {
t.Helper()
b := &a2aBackend{}
b.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b.lastAuthHeader = r.Header.Get("Authorization")
resp := map[string]any{
Comment on lines +52 to 59
// NewMCPHandler creates a new MCP handler that bridges MCP tool calls directly
// to agent A2A clients, bypassing the controller's own HTTP A2A listener.
func NewMCPHandler(kubeClient client.Client, agentClients *a2a.AgentClientRegistry, authenticator auth.AuthProvider) (*MCPHandler, error) {
handler := &MCPHandler{
kubeClient: kubeClient,
a2aBaseURL: a2aBaseURL,
a2aTimeout: a2aTimeout,
agentClients: agentClients,
authenticator: authenticator,
}
Comment on lines +175 to +177
if extra := req.GetExtra(); extra != nil {
if session, err := h.authenticator.Authenticate(ctx, extra.Header, nil); err == nil {
ctx = auth.AuthSessionTo(ctx, session)
Comment on lines 181 to 191
@@ -194,7 +189,6 @@ func (h *MCPHandler) handleInvokeAgent(ctx context.Context, req *mcpsdk.CallTool
}, InvokeAgentOutput{}, nil
}
agentRef := agentNS + "/" + agentName
@onematchfox onematchfox force-pushed the refactor-mcp-agent-invocation branch from d86531b to 7a6babf Compare May 12, 2026 14:39
onematchfox and others added 2 commits May 12, 2026 16:49
… handlers

The Go MCP SDK detaches the HTTP request context before dispatching to tool handlers. From the [SDK source](https://github.com/modelcontextprotocol/go-sdk/blob/v1.5.0/mcp/streamable.go#L485-L487):

>  // Pass req.Context() here, to allow middleware to add context values.
>  // The context is detached in the jsonrpc2 library when handling the
>  // long-running stream.

This means the auth session placed by `AuthnMiddleware` is not visible via `auth.AuthSessionFrom(ctx)` in tool handlers.

The SDK does preserve the original HTTP headers in [RequestExtra.Header](https://github.com/modelcontextprotocol/go-sdk/blob/v1.5.0/mcp/streamable.go#L1155-L1158) though.

Re-authenticate from those headers at the top of handleInvokeAgent so the A2A client's outbound request to the agent carries the user's JWT.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brian Fox <878612+onematchfox@users.noreply.github.com>
Replace the HTTP round-trip through the controller's own A2A listener with direct invocation via a new `AgentClientRegistry`. The registry is owned by `A2ARegistrar`, which already maintains an `A2AClient` per agent for its HTTP mux — the registry gives the MCP handler access to those same clients without an extra network hop.

The old approach routed through the controller's public A2A endpoint, meaning requests could traverse the external network (and any ingress or load-balancer in front of it) unnecessarily. The new path stays in-process.

The old handler also cached its own `A2AClient` per agent in a `sync.Map` with no eviction, so clients for deleted agents would remain indefinitely. The registry is kept consistent by the registrar's add/update/delete lifecycle, eliminating that staleness.

`A2ARegistrar.upsertAgentHandler` writes to both the HTTP mux (for inbound /api/a2a/<ns>/<name>/ routing) and the registry (for direct invocation). The registry is exposed via `ClientRegistry()` and passed to `NewMCPHandler` in app.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Brian Fox <878612+onematchfox@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants