-
Notifications
You must be signed in to change notification settings - Fork 372
Feat/expose kagent agents in mcp #1201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feat/expose kagent agents in mcp #1201
Conversation
|
@eitansuez @ilackarms @yuval-k @peterj |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some comments on tools below.
Perhaps we can add support for HTTP transport with --transport http --port <port> as well instead of just stdio? The existing mcp deploy command supports both.
It would also be a good idea to add e2e tests for invoking agents through MCP.
| } | ||
| toolName, agentNS, agentName := agent.ID, agent.Agent.Namespace, agent.Agent.Name | ||
| s.AddTool(mcp.NewTool(toolName, | ||
| mcp.WithDescription("kagent agent "+agentNS+"/"+agentName), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of one tool per agent, I think it would be more efficient to create a list_agents tool and a separate invoke_agent tool that takes in an agent name from the list of agents alongside the prompt / task. When listing the agents perhaps you can include the description for the agent for better info on which one to choose.
This way there will be only 2 tools exposed via this MCP server, which is more token-efficient and functionally equivalent if you have 20 agents. This is similar to the Agent Skills pattern for progressive discovery.
| s.AddTool(mcp.NewTool(toolName, | ||
| mcp.WithDescription("kagent agent "+agentNS+"/"+agentName), | ||
| mcp.WithString("context_id", mcp.Description("A2A context ID")), | ||
| mcp.WithNumber("history_length", mcp.Description("Requested history length")), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can possibly remove history_length I don't see how it can be useful since the agent calling this would just be guessing a number for input. It seems like the MCP client will have no idea what is context_id as well.
I believe the easiest solutions would be either just
- remove it (so only single-turn conversations are possible) or
- return this context id created on the first A2A request to the client, and it can use it to continue the conversation.
Alternatively we can manage a session internally in the MCP server based on the MCP sessions ID, so the agent calling this would not worry about context ID at all.
c4e28d8 to
548088d
Compare
|
Hi @supreme-gg-gg made the changes - STDIO server: kagent mcp serve-agents |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good start, I tried this and it seems to work pretty well in both stdio and http with MCP clients like Cursor. I left a few more comments. I think the test can be improved as well.
^ screenshot from Cursor using agents via MCP after I set it up
Lmk once you fixed them and I'll give it another review.
| return mcp.NewToolResultErrorFromErr("list agents", err), nil | ||
| } | ||
| type agentSummary struct { | ||
| Ref string `json:"ref"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Id doesn't seem to be useful, maybe just ref (name) and description?
| agentRef = agentNS + "/" + agentName | ||
|
|
||
| sessionID := "unknown" | ||
| if session := mcpserver.ClientSessionFromContext(ctx); session != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of falling back to unknown, use a unique invocation ID per session if none is available to keep the context separate. When callers with proper session support use this they will get unknown as session and it will cause unexpected behaviour with multiple concurrent users like potentially wrong context history.
| stdioServer := mcpserver.NewStdioServer(s) | ||
| return stdioServer.Listen(cmd.Context(), os.Stdin, os.Stdout) | ||
| case "http": | ||
| addr := fmt.Sprintf("%s:%d", serveAgentsHost, serveAgentsPort) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps some logging to indicate the server is running successfully like "MCP server listening on xxx"
| serveAgentsPort int | ||
| ) | ||
|
|
||
| var a2aContextBySessionAndAgent sync.Map |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This map never cleans up old session contexts. This might be an issue for HTTP server
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I quickly looked at the docs for mcp-go and seems like this hook will help:
// Create hooks for session lifecycle
hooks := &mcpserver.Hooks{}
// Clean up context storage when session ends
hooks.AddOnUnregisterSession(func(ctx context.Context, session mcpserver.ClientSession) {
sessionID := session.SessionID()
// Delete all entries for this session
a2aContextBySessionAndAgent.Range(func(key, value any) bool {
if keyStr, ok := key.(string); ok {
// Keys are formatted as "sessionID|agentRef"
if strings.HasPrefix(keyStr, sessionID+"|") {
a2aContextBySessionAndAgent.Delete(key)
}
}
return true
})
})
s := mcpserver.NewMCPServer(
"kagent-agents",
version.Version,
mcpserver.WithToolCapabilities(false),
mcpserver.WithHooks(hooks),
)hope it helps
| Use: "serve-agents", | ||
| Short: "Serve kagent agents via MCP", | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| cfg, cfgErr := config.Get() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just check for config error on server startup instead of inside tool invocation because if config has a problem you won't be able to get the client and kagent url to do anything that follows.
go/test/e2e/mcp_serve_agents_test.go
Outdated
| require.NotEmpty(t, callResult.Content) | ||
| require.Contains(t, callResult.Content[0].Text, "kebab-agent") | ||
|
|
||
| writeLine(`{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"invoke_agent","arguments":{"agent":"kebab-agent","task":"What can you do?"}}}`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's best to follow the pattern in invoke_api_test.go and setup the kebab agent and especially use mockLLM otherwise this might fail in CI
| if err != nil { | ||
| return mcp.NewToolResultErrorFromErr("encode agents", err), nil | ||
| } | ||
| return result, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to bug, you need to return mcp.NewToolResultStructured like below as well
548088d to
9c77d56
Compare
|
Hi @supreme-gg-gg |
EItanya
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These changes make sense to me, I think we can handle fixes in follow-ups. Unfortunately you will need to sign your commits to pass DCO
| var fallbackInvocationCounter uint64 | ||
|
|
||
| var ServeAgentsCmd = &cobra.Command{ | ||
| Use: "serve-agents", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we change this to serve-mcp
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done 🫡
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done 🫡
|
You will need to sign all of your commits for me to approve |
solves #1160
Title
Expose kagent agents as dynamic MCP tools over stdio (
kagent mcp serve-agents)Summary
This PR adds a new CLI subcommand,
kagent mcp serve-agents, which runs an MCP server over stdio and dynamically exposes kagent agents as MCP tools.Each eligible kagent agent is surfaced as a discoverable MCP tool at runtime. MCP-capable clients (IDEs, agent frameworks, assistants) can list and invoke these tools without any static tool definitions or manual schema maintenance.
All execution continues to flow through the existing A2A (agent-to-agent) API, preserving current agent semantics, permissions, and behavior.
What This Enables
MCP clients can automatically discover kagent agents via
tools/listAgents can be invoked using standard MCP
tools/callNo static MCP schemas need to be authored or updated
Agent additions/removals are reflected immediately at runtime
This turns kagent into a dynamic MCP tool provider backed by real agents.
New CLI Command
Behavior
Starts an MCP server over stdin/stdout
Intended to be launched and managed by an MCP client
Dynamically registers one MCP tool per eligible kagent agent
Does not open network ports or run an HTTP server
Tool Model
Tool Naming
One tool per agent
Tool name is a deterministic, MCP-safe identifier derived from the agent ID
Tool Description
Tool Inputs
Field | Type | Required | Description -- | -- | -- | -- task | string | yes | User prompt / task sent to the agent context_id | string | no | A2A context ID for conversational continuity history_length | number | no | History length forwarded to A2A configurationRuntime Flow
Startup
On startup,
serve-agents:Loads local CLI configuration
Queries the kagent backend for agents
Filters agents that are:
Accepted
DeploymentReady
Have a non-nil agent payload
Registers one MCP tool per eligible agent
If discovery fails, the MCP server still starts with an empty tool set.
Tool Invocation
When a tool is called:
Validate required inputs (
task)Construct the A2A endpoint:
Send the task via the existing A2A protocol
Extract human-readable text from the A2A response:
Message text (preferred)
Task status + artifact text
Raw JSON fallback if no text is extractable
Return the result as MCP tool output
All requests are handled synchronously over stdio.
Implementation Notes
Command Wiring
Adds a new Cobra command under
kagent mcpNo changes to existing CLI behavior or defaults
MCP Server
Uses
mark3labs/mcp-goServer name:
kagent-agentsVersion:
version.VersionRuns over stdio using:
Agent Discovery
Filtering is applied client-side.
Error Handling
Tool-level errors are returned as MCP tool errors:
Missing or invalid inputs
A2A client creation failures
A2A request failures
JSON marshaling issues
Startup failures (config or agent discovery):
Do not crash the MCP server
Result in an empty tool list
The process remains long-running and usable.
Scope and Non-Goals
In Scope
MCP stdio server
Dynamic per-agent tool registration
A2A-backed execution
Text extraction and fallback handling
Out of Scope
Server-side kagent changes
New agent logic
MCP transports other than stdio
Authentication or permission changes
Files Changed
root.goRegisters
serve-agentsunderkagent mcpserve_agents.goImplements the MCP stdio server, agent discovery, tool registration, and tool execution
How to Test (End-to-End)
1. Start kagent backend (one-time)
Port-forward the controller (keep running):
Verify backend:
2. Build the CLI
3. MCP handshake + tool discovery
Expected: tools listed, one per eligible agent.
4. Invoke a real agent via MCP
Expected: real cluster data returned as MCP tool output.
Result
This PR makes kagent agents immediately usable as MCP tools with no static schemas, no server-side changes, and full reuse of existing A2A execution paths.