mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-30 10:04:32 -04:00
mcp.tools.execute and mcp.discovery were the only NATS subjects that combined a queue group with a reply, and no carrier in this design provides both. They never needed one: a queue group is a way of choosing a subscriber, and choosing is a query. The frontend now lists the approved, non-draining agent nodes, asks the node_connections table in one joined statement which of those tunnels a live replica holds, prefers one this replica holds so the call skips the relay hop, and issues an ordinary control RPC on the path task 4 already mounted. A peer-held tunnel is reached through the relay. That is a choice a broker's hidden balancing could not make. The selection reads presence and nothing else. It is filtered only on node type and on the two statuses an operator controls, never on a health verdict written on another clock, because refusing a worker that is connected and answering is the same defect as picking one that is gone. An empty fleet answers ErrNoAgentWorker, which is deliberately neither ErrWorkerUnroutable nor anything cluster.IsWorkerAnswer accepts: nothing was asked of any worker, so no reap guard may act on it. A reply carrying an Error is the worker's own answer and is returned unchanged; it is never offered to a second worker, which would turn "this MCP server rejected your arguments" into "the fleet is broken" and could run a tool twice. A call that never reached a worker is retried against a different pick, at most three times, and whatever error is finally returned is returned unwrapped so its identity survives the loop. MCP prompts and resources now answer 501 in distributed mode instead of an empty 200. They are served only from sessions the frontend holds, and in distributed mode it holds none. That gap predates the removal of the bus and is not closed by it; this only stops it being silent. Agent workers keep every other subject, including nodes.<id>.backend.stop. Their minted JWT loses the two MCP subjects and keeps a non-empty allow list, because NATS reads an empty one as no restriction at all. Assisted-by: Claude Opus 5 [claude-code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
42 lines
2.0 KiB
Go
42 lines
2.0 KiB
Go
// SPDX-License-Identifier: MIT
|
|
|
|
package application
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/services/nodes"
|
|
)
|
|
|
|
// newAgentControl builds the frontend's agent control client: the SELECTION
|
|
// that decides which agent worker answers a verb, and the control client that
|
|
// carries the verb to it.
|
|
//
|
|
// A named function rather than two more lines in initDistributed, for the
|
|
// reason distributedSchedulerOptions is one: initDistributed opens a database
|
|
// and a bus, so no unit spec reaches it, and the argument that matters here has
|
|
// no symptom when it is wrong. An empty instance id makes every connection read
|
|
// report nothing as held by this replica, so every MCP call takes a relay hop
|
|
// through a peer even when this replica holds the worker's tunnel itself, and
|
|
// nothing anywhere says so: the calls all succeed, just through one more
|
|
// process than they need. It is refused here instead of shipped as latency.
|
|
//
|
|
// The other two refusals are the ordinary kind. A selector with no registry has
|
|
// nothing to select from and a client with no transport reaches nobody, and
|
|
// both would present as MCP being quietly unavailable in a deployment that
|
|
// looks healthy.
|
|
func newAgentControl(cfg config.DistributedConfig, registry *nodes.NodeRegistry,
|
|
conns nodes.AgentConnectionReader, control *nodes.ControlClient) (*nodes.AgentControlClient, error) {
|
|
if cfg.InstanceID == "" {
|
|
return nil, fmt.Errorf("the agent control client was built with no instance id: every MCP call would relay through a peer even for a worker whose tunnel this replica holds")
|
|
}
|
|
if registry == nil || conns == nil {
|
|
return nil, fmt.Errorf("the agent control client was built with no way to find a connected agent worker")
|
|
}
|
|
if control == nil {
|
|
return nil, fmt.Errorf("the agent control client was built with no control transport to reach an agent worker over")
|
|
}
|
|
return nodes.NewAgentControlClient(nodes.NewAgentSelector(registry, conns, cfg.InstanceID), control), nil
|
|
}
|