Skip to content

feat(mcp): add per-session agent filtering via URL query parameter#1620

Open
MatteoMori wants to merge 3 commits intokagent-dev:mainfrom
MatteoMori:feat/mcp-agent-filtering
Open

feat(mcp): add per-session agent filtering via URL query parameter#1620
MatteoMori wants to merge 3 commits intokagent-dev:mainfrom
MatteoMori:feat/mcp-agent-filtering

Conversation

@MatteoMori
Copy link
Copy Markdown

Context

Platform engineers deploying kagent in multi-tenant environments need a way to give teams access to specific agents without exposing the entire agent catalogue.
This change enables hard, server-enforced scoping at the MCP layer, without touching Kubernetes RBAC or CRDs.

As AI coding assistants (Claude Code, Cursor, etc.) become standard engineering tools, this also provides a clean integration path: each team connects to a scoped MCP endpoint and can only discover and invoke the agents they are authorised to use.

MCP gateways such as LiteLLM can register multiple scoped kagent entries and map them to team API keys; giving platform teams a single control plane for both LLM and agent access without any bespoke auth logic in kagent itself.

What

The MCP endpoint now accepts an optional agents query parameter which is a comma-separated list of namespace/name values:

http://kagent-controller:8083/mcp?agents=kagent/k8s-agent,kagent/helm-agent

Scenario Behaviour
list_agents with filter Returns only agents in the allow-list
invoke_agent — allowed agent Forwarded to A2A as normal
invoke_agent — blocked agent Rejected, IsError: true
invoke_agent — agent not in cluster Rejected, IsError: true
No ?agents= parameter No change — all agents accessible

Fully backwards compatible.

Usage with Claude Code

# Just for testing
kubectl port-forward -n kagent svc/kagent-controller 8083:8083

claude mcp add kagent-k8s \
  --transport http \
  "http://localhost:8083/mcp?agents=kagent%2Fk8s-agent"

claude mcp add kagent-platform \
  --transport http \
  "http://localhost:8083/mcp?agents=kagent%2Fk8s-agent,kagent%2Fhelm-agent,kagent%2Fobservability-agent"

Proof

✏️ Scoped to k8s-agent only: Claude sees one agent:
Screenshot 2026-04-03 at 20 28 29

✏️ Scoped to platform team (k8s + helm + observability): Claude sees three agents:
Screenshot 2026-04-03 at 20 29 52

@MatteoMori MatteoMori requested a review from EItanya as a code owner April 4, 2026 09:38
Copilot AI review requested due to automatic review settings April 4, 2026 09:38
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

This PR adds per-session agent filtering to the MCP endpoint via an optional ?agents= URL query parameter, enabling platform engineers to scope agent access in multi-tenant environments without requiring Kubernetes RBAC changes. The feature is fully backward compatible—when the parameter is absent, all agents remain accessible as before.

Changes:

  • Added context-based agent filtering infrastructure using an unexported context key type to prevent collisions
  • Implemented parseAllowedAgents() to parse comma-separated agent refs from the query parameter
  • Modified ServeHTTP() to detect and store the allow-list in the request context before delegating to the MCP handler
  • Updated handleListAgents() to filter returned agents based on the session's allow-list
  • Updated handleInvokeAgent() to reject invocations of agents outside the allow-list before touching downstream services
  • Added comprehensive test coverage with unit tests for parsing and context retrieval, plus integration tests for filtering behavior

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
go/core/internal/mcp/mcp_handler.go Added agent filtering implementation with context propagation, updated handlers to enforce filtering at both list and invoke levels
go/core/internal/mcp/mcp_handler_test.go New comprehensive test suite covering parsing, context handling, and filtering behavior across list_agents and invoke_agent scenarios

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

Copy link
Copy Markdown
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.

PR Review — feat(mcp): add per-session agent filtering via URL query parameter

Comment left by Claude on behalf of @EItanya.

Overall this is a well-scoped, backwards-compatible feature with good test coverage. The implementation is clean and the integration tests that exercise the full MCP initialize → tools/call flow are particularly nice. One thing to discuss:


Context propagation & session-level enforcement (main concern)

The allow-list is parsed from the URL on every HTTP request in ServeHTTP and injected into the request context. For MCP StreamableHTTP sessions, the session is identified by the Mcp-Session-Id header — meaning a client that obtains a valid session ID could send a subsequent request to the same endpoint without the ?agents= query parameter, and the allow-list context value would be nil, effectively bypassing the filter.

Given the code comments call this "the hard access-control boundary" and the PR description says "hard, server-enforced scoping," this is worth addressing or at least acknowledging:

@EItanya
Copy link
Copy Markdown
Contributor

EItanya commented Apr 6, 2026

Suggestion: Move allow-list into the server factory (per-session, not per-request)

Comment left by Claude on behalf of @EItanya.

The problem

Today the allow-list is parsed from ?agents= on every HTTP request in ServeHTTP and injected into the request context. But the Mcp-Session-Id header is what ties requests to a session — so a client (or misconfigured gateway) could send a follow-up request to the same session without the query parameter, and the context value would be nil, silently disabling the filter.

Why the server factory is a better fit

The NewStreamableHTTPHandler factory — func(*http.Request) *Server — is called exactly once per session, on the initialize request. The *http.Request it receives is the one that carries your ?agents= query parameter. If you parse the allow-list there and close over it in the tool handlers, the filter becomes immutable for the session's lifetime. No context plumbing, no per-request re-parsing, no bypass window.

How to implement it

The change is mostly moving code around, not writing new code.

1. Extract server creation into a helper:

func (h *MCPHandler) newMCPServer(allowed map[string]struct{}) *mcpsdk.Server {
    server := mcpsdk.NewServer(&mcpsdk.Implementation{
        Name:    "kagent-agents",
        Version: version.Version,
    }, nil)

    mcpsdk.AddTool(server, &mcpsdk.Tool{
        Name:        "list_agents",
        Description: "List invokable kagent agents (accepted + deploymentReady)",
    }, func(ctx context.Context, req *mcpsdk.CallToolRequest, input ListAgentsInput) (*mcpsdk.CallToolResult, ListAgentsOutput, error) {
        return h.handleListAgents(ctx, req, input, allowed)
    })

    mcpsdk.AddTool(server, &mcpsdk.Tool{
        Name:        "invoke_agent",
        Description: "Invoke a kagent agent via A2A",
    }, func(ctx context.Context, req *mcpsdk.CallToolRequest, input InvokeAgentInput) (*mcpsdk.CallToolResult, InvokeAgentOutput, error) {
        return h.handleInvokeAgent(ctx, req, input, allowed)
    })

    return server
}

2. Use it in the factory (in NewMCPHandler):

handler.httpHandler = mcpsdk.NewStreamableHTTPHandler(
    func(r *http.Request) *mcpsdk.Server {
        return handler.newMCPServer(parseAllowedAgents(r))
    },
    nil,
)

3. Add allowed param to the handler methods:

func (h *MCPHandler) handleListAgents(ctx context.Context, req *mcpsdk.CallToolRequest, input ListAgentsInput, allowed map[string]struct{}) (...)
func (h *MCPHandler) handleInvokeAgent(ctx context.Context, req *mcpsdk.CallToolRequest, input InvokeAgentInput, allowed map[string]struct{}) (...)

Then use allowed directly instead of allowedAgentsFromContext(ctx).

4. Delete the context plumbing:

Remove contextKey, allowedAgentsKey, allowedAgentsFromContext(), and the context injection in ServeHTTP. ServeHTTP goes back to a one-liner.

5. Tests: TestAllowedAgentsFromContext can be deleted. The integration tests (TestListAgents_*, TestInvokeAgent_*) should pass as-is since they already exercise the full HTTP flow with ?agents= in the URL.


The net effect is fewer lines of code, no context key type, and the filter is genuinely session-scoped rather than request-scoped. Happy to help if you have questions!

@MatteoMori MatteoMori force-pushed the feat/mcp-agent-filtering branch from d8167a8 to 604cc4c Compare April 9, 2026 16:37
MatteoMori and others added 2 commits April 9, 2026 17:39
The MCP endpoint now accepts an optional "agents" query parameter that
restricts which kagent agents are visible and invokable within a session:

  http://kagent-controller:8083/mcp?agents=kagent/k8s-agent,kagent/helm-agent

When present:
- list_agents returns only agents in the allow-list
- invoke_agent rejects any call targeting an agent outside the allow-list,
  returning IsError=true before touching any downstream A2A service

When absent, the original behaviour is preserved: all ready agents are
accessible. The feature is fully backwards compatible.

This enables MCP gateway solutions (e.g. LiteLLM) to register multiple
scoped endpoints — one per team or role — and enforce hard access control
via their own key/permission system, without requiring changes to kagent's
Kubernetes RBAC or CRD model.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: MatteoMori <morimatteo14@gmail.com>
Parse the ?agents= allow-list in the NewStreamableHTTPHandler factory,
which is called once on the MCP initialize request. Closing over the
result in the tool handlers makes the filter immutable for the session's
lifetime, eliminating a bypass window where a follow-up request without
the query parameter would produce a nil allow-list.

Remove context plumbing (contextKey, allowedAgentsKey,
allowedAgentsFromContext) and pass allowed directly to handleListAgents
and handleInvokeAgent. ServeHTTP is now a one-liner passthrough.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: MatteoMori <morimatteo14@gmail.com>
@MatteoMori MatteoMori force-pushed the feat/mcp-agent-filtering branch from 604cc4c to 987080a Compare April 9, 2026 16:41
@MatteoMori
Copy link
Copy Markdown
Author

@EItanya Hey, thank you for your feedback. Does it look better now?

@iplay88keys
Copy link
Copy Markdown
Contributor

iplay88keys commented Apr 9, 2026

@MatteoMori, I think this can be done with Agentgateway using an agentgatewaypolicy. See the description in this PR as a general example where I was denying agents access to specific tools.

If I recall correctly, if you swap that to an allow policy and specify who is allowed and what tools/agents they are allowed, all other requests get denied.

@MatteoMori
Copy link
Copy Markdown
Author

@iplay88keys
Hi! Thanks for the suggestion. While Agentgateway could potentially address this use case, relying on it introduces a strict dependency on a specific AI Gateway.

In larger enterprise environments with established tooling, requiring a specific gateway can be a significant barrier to adoption. My goal with this PR is to ensure kAgent remains agnostic and easily integrates with the wider ecosystem. By hardening the security posture directly within kAgent, we make it much easier to offer as a secure platform in multi-tenant environments without forcing a specific infrastructure stack

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.

4 participants