Skip to content

Add session tools to built-in kagent-agents MCP server#1334

Open
dobesv wants to merge 1 commit intokagent-dev:mainfrom
dobesv:mcp-session-tools
Open

Add session tools to built-in kagent-agents MCP server#1334
dobesv wants to merge 1 commit intokagent-dev:mainfrom
dobesv:mcp-session-tools

Conversation

@dobesv
Copy link

@dobesv dobesv commented Feb 18, 2026

This allows an agent to create an kagent session to run a task instead of simple delegation. This can be used to "hand off" a task to a different agent.

For example you can have an interactive session with a planning agent, it can hand that off to the implementation agent, and you can have an interactive session with the implementation agent.

The interactive session is desirable so the agent can ask questions or get corrections. Doing it via multiple agents is desirable so that you can pick your model parameters and system prompt separately for the task.

This combines the two together - you can setup a RemoteMCPServer that points to the kagent-controller and give the planning agent the power to create a session and give you a link to it to continue the work.

In principle this might move in the direction of having multi-agent coordination where you can interact with the agents and they interact with each other if you give them each others' session IDs or have a coordinating agent that starts multiple agents and then gives them each others' session IDs.

Copilot AI review requested due to automatic review settings February 18, 2026 23:08
Copy link
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

Adds session-management MCP tools to the controller’s built-in MCP server and registers the controller endpoint as a RemoteMCPServer so agents can consume these tools.

Changes:

  • Added a Helm template to create a RemoteMCPServer pointing at the controller’s /mcp endpoint.
  • Extended the controller config/startup to pass DB + UI base URL into the MCP handler.
  • Implemented MCP tools for creating sessions, listing session events, and sending session messages, with accompanying unit tests.

Reviewed changes

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

File Description
helm/kagent/templates/toolserver-controller.yaml Registers controller MCP endpoint as a RemoteMCPServer.
go/pkg/app/app.go Adds --ui-base-url config and wires DB + UI URL into MCP handler construction.
go/internal/mcp/mcp_handler.go Implements session MCP tools and A2A send helper using DB-backed sessions.
go/internal/mcp/mcp_handler_test.go Adds unit tests for the new session MCP tools.

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

Comment on lines +579 to +592
if session.AgentID != nil {
agentRef := utils.ConvertToKubernetesIdentifier(*session.AgentID)
if err := h.sendA2AMessage(ctx, agentRef, &session.ID, input.Content); err != nil {
log.Error(err, "Failed to dispatch message to agent", "sessionID", session.ID, "agent", agentRef)
msg := fmt.Sprintf("Failed to dispatch message to agent: %v. WARNING: do NOT resend the same message — it may have been partially delivered. Check the session events first.", err)
return &mcpsdk.CallToolResult{
Content: []mcpsdk.Content{
&mcpsdk.TextContent{Text: msg},
},
IsError: true,
}, SendSessionMessageOutput{Message: msg}, nil
}
}

Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

If session.AgentID is nil, this handler returns "Message sent successfully." without dispatching anything. Since SendSessionMessageInput implies delivering a message to the agent, this should return an error when the session has no associated agent (or explicitly document/rename the tool if "no-op" is intended).

Suggested change
if session.AgentID != nil {
agentRef := utils.ConvertToKubernetesIdentifier(*session.AgentID)
if err := h.sendA2AMessage(ctx, agentRef, &session.ID, input.Content); err != nil {
log.Error(err, "Failed to dispatch message to agent", "sessionID", session.ID, "agent", agentRef)
msg := fmt.Sprintf("Failed to dispatch message to agent: %v. WARNING: do NOT resend the same message — it may have been partially delivered. Check the session events first.", err)
return &mcpsdk.CallToolResult{
Content: []mcpsdk.Content{
&mcpsdk.TextContent{Text: msg},
},
IsError: true,
}, SendSessionMessageOutput{Message: msg}, nil
}
}
if session.AgentID == nil {
msg := fmt.Sprintf("Cannot send message: session %s has no associated agent.", session.ID)
log.Error(fmt.Errorf(msg), "Session has no associated agent", "sessionID", session.ID)
return &mcpsdk.CallToolResult{
Content: []mcpsdk.Content{
&mcpsdk.TextContent{Text: msg},
},
IsError: true,
}, SendSessionMessageOutput{Message: msg}, nil
}
agentRef := utils.ConvertToKubernetesIdentifier(*session.AgentID)
if err := h.sendA2AMessage(ctx, agentRef, &session.ID, input.Content); err != nil {
log.Error(err, "Failed to dispatch message to agent", "sessionID", session.ID, "agent", agentRef)
msg := fmt.Sprintf("Failed to dispatch message to agent: %v. WARNING: do NOT resend the same message — it may have been partially delivered. Check the session events first.", err)
return &mcpsdk.CallToolResult{
Content: []mcpsdk.Content{
&mcpsdk.TextContent{Text: msg},
},
IsError: true,
}, SendSessionMessageOutput{Message: msg}, nil
}

Copilot uses AI. Check for mistakes.
Comment on lines +392 to +401
// Parse agent reference (namespace/name or just name)
_, agentName, ok := strings.Cut(input.Agent, "/")
if !ok {
return &mcpsdk.CallToolResult{
Content: []mcpsdk.Content{
&mcpsdk.TextContent{Text: "agent must be in format 'namespace/name'"},
},
IsError: true,
}, CreateSessionOutput{}, nil
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The comment says the agent ref can be "namespace/name or just name", but the implementation requires a slash and errors otherwise. Either update the comment to match behavior, or support name-only refs (e.g., via utils.ParseRefString with a default namespace).

Copilot uses AI. Check for mistakes.

// handleGetSessionEvents handles the get_session_events MCP tool
func (h *MCPHandler) handleGetSessionEvents(ctx context.Context, req *mcpsdk.CallToolRequest, input GetSessionEventsInput) (*mcpsdk.CallToolResult, GetSessionEventsOutput, error) {
log := ctrllog.FromContext(ctx).WithName("mcp-handler").WithValues("tool", "get_session_events")
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The structured log value uses tool=get_session_events, but the registered MCP tool name is get_agent_session_events. Aligning these makes logs/metrics easier to correlate with tool calls.

Suggested change
log := ctrllog.FromContext(ctx).WithName("mcp-handler").WithValues("tool", "get_session_events")
log := ctrllog.FromContext(ctx).WithName("mcp-handler").WithValues("tool", "get_agent_session_events")

Copilot uses AI. Check for mistakes.
commandLine.StringVar(&cfg.Database.Path, "sqlite-database-path", "./kagent.db", "The path to the SQLite database file.")
commandLine.StringVar(&cfg.Database.Url, "postgres-database-url", "postgres://postgres:kagent@db.kagent.svc.cluster.local:5432/crud", "The URL of the PostgreSQL database.")

commandLine.StringVar(&cfg.UI.BaseURL, "ui-base-url", "http://localhost:3000", "The base URL of the UI.")
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

--ui-base-url defaults to http://localhost:3000, which will produce broken UI links when the controller runs in-cluster (it will point to the controller Pod’s localhost, not the UI Service). Consider defaulting this to an in-cluster service URL, or leaving it empty and requiring Helm/env configuration (e.g., UI_BASE_URL) so generated session URLs are valid.

Suggested change
commandLine.StringVar(&cfg.UI.BaseURL, "ui-base-url", "http://localhost:3000", "The base URL of the UI.")
commandLine.StringVar(&cfg.UI.BaseURL, "ui-base-url", "", "The base URL of the UI.")

Copilot uses AI. Check for mistakes.
{{- include "kagent.labels" . | nindent 4 }}
spec:
url: "http://{{ include "kagent.fullname" . }}-controller.{{ include "kagent.namespace" . }}:{{ .Values.controller.service.ports.port }}/mcp"
description: "Kagent Controller Built-in Tools (Session Management, Agent Invocation)"
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

Branding is inconsistent with the rest of the chart (e.g., toolserver-kagent.yaml uses "KAgent"). Consider changing "Kagent Controller..." to "KAgent Controller..." for consistency.

Suggested change
description: "Kagent Controller Built-in Tools (Session Management, Agent Invocation)"
description: "KAgent Controller Built-in Tools (Session Management, Agent Invocation)"

Copilot uses AI. Check for mistakes.
Comment on lines +403 to +405
if input.UserID == "" {
input.UserID = defaultUserID
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

The MCP tools accept an optional user_id and default it to admin@kagent.dev, but the MCP endpoint is behind auth.AuthnMiddleware, so the authenticated user is available in the request context. As written, a caller can omit user_id (creating/administering sessions as admin) or supply an arbitrary user_id to read/write other users' sessions. Consider deriving the user ID from auth.AuthSessionFrom(ctx) (and/or rejecting mismatches) instead of trusting input.UserID.

Suggested change
if input.UserID == "" {
input.UserID = defaultUserID
}
// Derive user ID from authenticated session and ensure it matches any provided input.
authSession := auth.AuthSessionFrom(ctx)
if authSession == nil {
log.Error(nil, "no auth session found in context")
return &mcpsdk.CallToolResult{
Content: []mcpsdk.Content{
&mcpsdk.TextContent{Text: "authentication required to create a session"},
},
IsError: true,
}, CreateSessionOutput{}, nil
}
if input.UserID != "" && input.UserID != authSession.UserID {
log.Error(nil, "user_id mismatch between request and authenticated session", "input_user_id", input.UserID, "session_user_id", authSession.UserID)
return &mcpsdk.CallToolResult{
Content: []mcpsdk.Content{
&mcpsdk.TextContent{Text: "user_id does not match authenticated user"},
},
IsError: true,
}, CreateSessionOutput{}, nil
}
input.UserID = authSession.UserID

Copilot uses AI. Check for mistakes.
Comment on lines +486 to +488
if input.UserID == "" {
input.UserID = defaultUserID
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

This tool currently allows callers to supply an arbitrary user_id (or fall back to admin@kagent.dev) when listing session events. Since the MCP endpoint is authenticated via auth.AuthnMiddleware, this should use the authenticated principal's user ID from context (or at least validate that input.UserID matches it) to prevent cross-user data access.

Suggested change
if input.UserID == "" {
input.UserID = defaultUserID
}
// Always use the internal default user ID to prevent callers from selecting arbitrary users.
input.UserID = defaultUserID

Copilot uses AI. Check for mistakes.
Comment on lines +563 to +565
if input.UserID == "" {
input.UserID = defaultUserID
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

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

send_agent_session_message trusts input.UserID (defaulting to admin@kagent.dev) when loading the session. With request auth already in middleware, this enables users to target other users' sessions by passing a different user_id. Use the authenticated user from auth.AuthSessionFrom(ctx) (or validate/ignore the input field).

Copilot uses AI. Check for mistakes.
@dobesv dobesv changed the title Add session utils to built-in MCP server Add session tools to built-in kagent-agents MCP server Feb 18, 2026
@EItanya
Copy link
Contributor

EItanya commented Feb 19, 2026

Hey there, thanks for the contribution. I definitely think this is moving in an awesome direction, but I'm not sure this is the level of abstraction we want to expose to the agents. Perhaps we can look further into the patterns that ADK recommends: https://google.github.io/adk-docs/agents/multi-agents/

@dobesv
Copy link
Author

dobesv commented Feb 19, 2026

My main goal here is handing off some work from one interactive agent session to another.

The multi-agent patterns you linked seem to be a more advanced version of the current agent delegation system which isn't really what I wanted.

I thought enabling some interesting multi-agent workflows beyond my simple use case could be built on top this tool perhaps. But that's not really the main point of this relatively small change.

For now I am assuming I will have to maintain my own fork of kagent more or less indefinitely. I thought I would share some of my changes back upstream in case people have a use for them. If not, feel free to close my pull requests. Even if you do not merge them, others who want these capabilities and are also maintaining their own fork can use them as a reference. Having my changes upstream and not maintaining a customized fork seems a distant dream at this point, I'm not counting on it.

@dobesv
Copy link
Author

dobesv commented Feb 19, 2026

Note that these MCP additions also can be a way to transfer a workflow into kagent from an external MCP client as well, not just internally within kagent. If you use invoke_agent then the session is not interactive within kagent, you still have to continue the work within the original tool to manage it.

Think of the feature in Gemini where you can hand-off work to Jules, or in Claude where you can spawn a job into Claude for the Web.

@EItanya
Copy link
Contributor

EItanya commented Feb 19, 2026

My main goal here is handing off some work from one interactive agent session to another.

The multi-agent patterns you linked seem to be a more advanced version of the current agent delegation system which isn't really what I wanted.

I thought enabling some interesting multi-agent workflows beyond my simple use case could be built on top this tool perhaps. But that's not really the main point of this relatively small change.

For now I am assuming I will have to maintain my own fork of kagent more or less indefinitely. I thought I would share some of my changes back upstream in case people have a use for them. If not, feel free to close my pull requests. Even if you do not merge them, others who want these capabilities and are also maintaining their own fork can use them as a reference. Having my changes upstream and not maintaining a customized fork seems a distant dream at this point, I'm not counting on it.

As I said in your other PR, we're very much open to new features, but it's also important that we keep in mind the rest of the community, and the maintainability and health of the project overall. For example, I am also very very much interested in background tasks, and how we can enable those sorts of workflows, but the best first step would be to create a design and attend the community meeting so that we can all be aligned before the feature is added. The most important thing is to make features which are useful for everyone, and also maintainable by the maintainers.

The link for the community meeting can be found here: https://kagent.dev/community

The meeting happens at 12:00 EST on Tuesdays, looking forward to seeing you there

@dobesv
Copy link
Author

dobesv commented Feb 19, 2026

It did occur to me today that it might be possible to allow opening a delegated agent session in kagent UI in which case I could still use the A2A protocol to create the session and just offer a link to it to the user to continue the work. That might be a more elegant solution, if it's compatible with the way kagent works.

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

Comments