Skip to content

Conversation

@riturajFi
Copy link

@riturajFi riturajFi commented Jan 12, 2026


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/list

  • Agents can be invoked using standard MCP tools/call

  • No 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

kagent mcp serve-agents

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

kagent agent <namespace>/<name>

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 configuration

Runtime Flow

Startup

On startup, serve-agents:

  1. Loads local CLI configuration

  2. Queries the kagent backend for agents

  3. Filters agents that are:

    • Accepted

    • DeploymentReady

    • Have a non-nil agent payload

  4. 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:

  1. Validate required inputs (task)

  2. Construct the A2A endpoint:

    {KAgentURL}/api/a2a/{namespace}/{name}
    
  3. Send the task via the existing A2A protocol

  4. Extract human-readable text from the A2A response:

    • Message text (preferred)

    • Task status + artifact text

    • Raw JSON fallback if no text is extractable

  5. 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 mcp

  • No changes to existing CLI behavior or defaults

MCP Server

  • Uses mark3labs/mcp-go

  • Server name: kagent-agents

  • Version: version.Version

  • Runs over stdio using:

    mcpserver.NewStdioServer(s).Listen(ctx, os.Stdin, os.Stdout)
    

Agent Discovery

cfg.Client().Agent.ListAgents(ctx)

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.go
    Registers serve-agents under kagent mcp

  • serve_agents.go
    Implements the MCP stdio server, agent discovery, tool registration, and tool execution


How to Test (End-to-End)

These steps emulate a real MCP client and validate full end-to-end behavior.

1. Start kagent backend (one-time)

export KAGENT_DEFAULT_MODEL_PROVIDER=openAI
export OPENAI_API_KEY=...

make create-kind-cluster
make use-kind-cluster
make helm-install

Port-forward the controller (keep running):

kubectl -n kagent port-forward svc/kagent-controller 8083:8083

Verify backend:


2. Build the CLI

go -C go build -o ./bin/kagent ./cli/cmd/kagent
./go/bin/kagent mcp --help

3. MCP handshake + tool discovery

cat <<'EOF' | ./go/bin/kagent mcp serve-agents
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"manual","version":"0.0.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
EOF

Expected: tools listed, one per eligible agent.


4. Invoke a real agent via MCP

cat <<'EOF' | ./go/bin/kagent mcp serve-agents
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"manual","version":"0.0.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"kagent__NS__k8s_agent","arguments":{"task":"list all pods in all namespaces"}}}
EOF

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.

@riturajFi
Copy link
Author

riturajFi commented Jan 12, 2026

@eitansuez @ilackarms @yuval-k @peterj
Kindly check this PR changes solving #1160

@riturajFi riturajFi marked this pull request as ready for review January 12, 2026 07:01
Copy link
Contributor

@supreme-gg-gg supreme-gg-gg left a 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),
Copy link
Contributor

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")),
Copy link
Contributor

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

  1. remove it (so only single-turn conversations are possible) or
  2. 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.

@riturajFi riturajFi force-pushed the feat/expose-kagent-agents-in-mcp branch from c4e28d8 to 548088d Compare January 13, 2026 08:36
@riturajFi
Copy link
Author

Hi @supreme-gg-gg made the changes -
Updated serve_agents.go to address the review comments:
Switched from “one tool per agent” → 2 tools: list_agents (includes agent ref, id, description) + invoke_agent (takes agent + task).
Removed history_length; context_id is now managed internally per MCP session+agent and returned in structuredContent.
Added HTTP transport: kagent mcp serve-agents --transport http --host --port (streamable-http at http://:/mcp).
Added an e2e test: mcp_serve_agents_test.go (runs kagent mcp serve-agents over stdio and invokes kebab-agent).
Commits: 6c4b1d9e (tools + HTTP transport), 333ac100 (e2e test).
How to try it

STDIO server: kagent mcp serve-agents
HTTP server: kagent mcp serve-agents --transport http --host 127.0.0.1 --port 3000 (connect to http://127.0.0.1:3000/mcp)
Run the new e2e test: cd go && go test ./test/e2e -run TestE2EInvokeAgentThroughMCPServeAgents -v

Copy link
Contributor

@supreme-gg-gg supreme-gg-gg left a 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 2026-01-13 at 4 10 28 PM

^ 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"`
Copy link
Contributor

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 {
Copy link
Contributor

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)
Copy link
Contributor

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
Copy link
Contributor

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

Copy link
Contributor

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()
Copy link
Contributor

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.

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?"}}}`)
Copy link
Contributor

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
Copy link
Contributor

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

@riturajFi riturajFi force-pushed the feat/expose-kagent-agents-in-mcp branch from 548088d to 9c77d56 Compare January 14, 2026 12:27
@riturajFi
Copy link
Author

Hi @supreme-gg-gg
I have implemented the changes.
Could you kindly review the changes done?

EItanya
EItanya previously approved these changes Jan 14, 2026
Copy link
Contributor

@EItanya EItanya left a 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",
Copy link
Contributor

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

Copy link
Author

Choose a reason for hiding this comment

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

Done 🫡

Copy link
Author

Choose a reason for hiding this comment

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

Done 🫡

@EItanya
Copy link
Contributor

EItanya commented Jan 15, 2026

You will need to sign all of your commits for me to approve

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.

3 participants