Files
LocalAI/core/application/studio_mcpconns.go
T
Stefan Walcz 650b2c7acd fix(chats): give MCP connector tool calls the MCP tool timeout
A tool call through an MCP connector was cut after the connector
package default of 60 seconds, while the model's own MCP servers allow
360 seconds. Research and document tools behind a connector take
minutes, so a saved agent lost them mid-turn with "connection closed:
calling tools/call: client is closing" and the model fell back to a
weaker tool.

The connector service now gets the MCP tool timeout the rest of
LocalAI uses (DistributedConfig.MCPToolTimeoutOrDefault, 360 s by
default). The package default and the per-call output cap are
unchanged.

Tests in core/application start the connector service on JSON stores
against a slow MCP server: the timeout defaults to the MCP tool
timeout, a tool slower than the old cap but within the timeout
returns, and a tool past the timeout is still stopped.

Docs: chats-connectors.md, "How chats use them".

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Stefan Walcz <stefan.walcz@walcz.de>
(cherry picked from commit 99f663a777)
2026-11-03 09:17:42 +00:00

89 lines
3.2 KiB
Go

package application
import (
"context"
"time"
"github.com/mudler/LocalAI/core/http/auth"
"github.com/mudler/LocalAI/core/services/studio"
"github.com/mudler/LocalAI/core/services/studio/connectors"
"github.com/mudler/LocalAI/core/services/studio/mcpconns"
)
// StudioMCPConnectors returns the service that keeps the MCP connectors users
// defined, or nil when the chats have no store.
func (a *Application) StudioMCPConnectors() *mcpconns.Service { return a.studioMCPConns }
// initStudioMCPConnectors builds the connectors service. It shares the sealing
// key of the Telegram connectors: one key per instance, a sealed value tied to
// its record and owner.
func (a *Application) initStudioMCPConnectors() {
if a.studioMCPStore == nil || a.studioConnStore == nil {
return
}
svc, err := mcpconns.New(mcpconns.Options{
Store: a.studioMCPStore,
Key: func(ctx context.Context) ([]byte, error) { return a.studioConnStore.EnsureKey(ctx, connectors.NewKey) },
Permitted: a.studioMCPPermitted,
IsAdmin: a.studioIsAdmin,
AllowPrivate: func() bool { return a.applicationConfig.MCPConnectorsAllowPrivate },
// A local command would run on whichever frontend serves the request.
Distributed: a.Distributed() != nil,
CallTimeout: a.studioMCPCallTimeout(),
})
if err != nil {
return
}
a.studioMCPConns = svc
}
// studioMCPCallTimeout is how long a connector tool call may run: the MCP tool
// timeout the model's MCP servers have (360 s unless the distributed config
// sets another), not the connector package default of 60 s. A research or
// document tool behind a connector takes minutes; at 60 s the chat dropped it
// ("client is closing").
func (a *Application) studioMCPCallTimeout() time.Duration {
return a.applicationConfig.Distributed.MCPToolTimeoutOrDefault()
}
// studioMCPPermitted says whether a user may use MCP now: the server has not
// turned it off and the user has the MCP feature. It is asked on every use.
func (a *Application) studioMCPPermitted(ctx context.Context, user string) bool {
if a.applicationConfig.DisableMCP {
return false
}
if a.authDB == nil {
return true
}
u, err := a.loadStudioUser(ctx, user)
return err == nil && auth.HasFeatureAccess(a.authDB, u, auth.FeatureMCP)
}
// studioIsAdmin says whether a user is an administrator. With auth off the
// local owner counts as one.
func (a *Application) studioIsAdmin(ctx context.Context, user string) bool {
if a.studioUsers() == nil {
return true
}
u, err := a.loadStudioUser(ctx, user)
return err == nil && u.Role == auth.RoleAdmin
}
// studioModelMCPServers lists the servers of the model's own MCP configuration
// for the per-chat switches. It is empty when the user may not use MCP.
func (a *Application) studioModelMCPServers(ctx context.Context, user, model string) []studio.ModelMCPServer {
cfg, ok := a.ModelConfigLoader().GetModelConfig(model)
if !ok || !cfg.MCP.HasMCPServers() || !a.studioMCPPermitted(ctx, user) {
return nil
}
names, err := cfg.MCP.ServerNames()
if err != nil {
return nil
}
out := make([]studio.ModelMCPServer, len(names))
for i, n := range names {
out[i] = studio.ModelMCPServer{Name: n, Auto: cfg.MCP.Auto}
}
return out
}