mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 12:33:16 -05:00
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)
89 lines
3.2 KiB
Go
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
|
|
}
|