fix(mcp): wire per-user MCP tool discovery into agent pipeline

MCP servers with require_user_credentials (e.g. Notion) were defined
but never loaded into the agent's tool registry. Three gaps:

1. getUserMCPTools was defined but never called — add call in
   makeBuildFilteredTools before FilterTools runs each iteration.

2. hasMCPTools stayed false when only user-credential servers existed,
   so agentToolPolicyWithMCP never injected "group:mcp" into alsoAllow.
   Now set true when mcpUserCredSrvs is non-empty.

3. Per-user BridgeTools were registered in the registry but never added
   to the "mcp" tool group, so expandSpec("group:mcp") returned empty.
   Add MergeToolGroup helper for additive group updates.

Also add debug log when getUserMCPTools skips due to empty userID.
This commit is contained in:
therichardngai-code authored and GitHub committed 2026-04-15 04:09:11 +07:00
1 parent 7d6fcc3b7e
commit e79a8bbd39
4 files changed
+40

No files matched your search

+10
View File
@@ -15,6 +15,9 @@ import (
// connections are established via pool.AcquireUser() and BridgeTools created.
func (l *Loop) getUserMCPTools(ctx context.Context, userID string) []tools.Tool {
if len(l.mcpUserCredSrvs) == 0 || l.mcpPool == nil || l.mcpStore == nil || userID == "" {
if userID == "" && len(l.mcpUserCredSrvs) > 0 {
slog.Debug("mcp.user_tools_skipped", "reason", "empty_user_id", "servers", len(l.mcpUserCredSrvs))
}
return nil
}
@@ -100,6 +103,13 @@ func (l *Loop) getUserMCPTools(ctx context.Context, userID string) []tools.Tool
if len(userTools) > 0 {
l.mcpUserTools.Store(userID, userTools)
// Update "mcp" tool group so policy expansion via alsoAllow includes
// per-user tools. MergeToolGroup is additive — safe for concurrent users.
var names []string
for _, t := range userTools {
names = append(names, t.Name())
}
tools.MergeToolGroup("mcp", names)
slog.Info("mcp.user_tools_loaded", "user", userID, "tools", len(userTools))
}
return userTools
@@ -191,6 +191,11 @@ func (l *Loop) makeInjectReminders(req *RunRequest) func(ctx context.Context, in
func (l *Loop) makeBuildFilteredTools(req *RunRequest) func(state *pipeline.RunState) ([]providers.ToolDefinition, error) {
return func(state *pipeline.RunState) ([]providers.ToolDefinition, error) {
// Load per-user MCP tools (Notion, etc.) into registry before filtering.
// Servers with require_user_credentials are deferred at startup and
// connected per-request here with the actual user's credentials.
l.getUserMCPTools(state.Ctx, state.Input.UserID)
maxIter := l.maxIterations
if req.MaxIterations > 0 && req.MaxIterations < maxIter {
maxIter = req.MaxIterations
+6
View File
@@ -305,6 +305,12 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc {
slog.Warn("failed to load MCP servers for agent", "agent", agentKey, "error", err)
} else {
mcpUserCredSrvs = mcpMgr.UserCredServers()
// User-credential servers (Notion, etc.) are deferred at startup
// but will produce tools per-request via getUserMCPTools.
// Set flag so agentToolPolicyWithMCP injects "group:mcp" into alsoAllow.
if len(mcpUserCredSrvs) > 0 {
hasMCPTools = true
}
if mcpMgr.IsSearchMode() {
// Search mode: too many tools — register mcp_tool_search meta-tool.
// Also wire lazy activator so deferred tools can be called by name directly.
+19
View File
@@ -50,6 +50,25 @@ func RegisterToolGroup(name string, members []string) {
toolGroupsMu.Unlock()
}
// MergeToolGroup adds members to an existing tool group without removing existing entries.
// Used by per-user MCP tool loading to extend the "mcp" group incrementally.
func MergeToolGroup(name string, members []string) {
toolGroupsMu.Lock()
defer toolGroupsMu.Unlock()
existing := toolGroups[name]
seen := make(map[string]bool, len(existing))
for _, m := range existing {
seen[m] = true
}
for _, m := range members {
if !seen[m] {
existing = append(existing, m)
seen[m] = true
}
}
toolGroups[name] = existing
}
// UnregisterToolGroup removes a dynamic tool group.
func UnregisterToolGroup(name string) {
toolGroupsMu.Lock()