feat(mcp): install a backend from a library entry

The backend library route installs a backend from an add-on library
entry, and the admin REST surface and the MCP catalog must stay in step.
Add the install_library_backend tool with an in-process and an HTTP
client, the route map entry, the safety-rule listing and a skill recipe.

Listing library entries stays on the add-on routes, which the assistant
does not expose, so no list tool is added.

Assisted-by: Claude Code:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
Ettore Di Giacinto committed 2026-10-21 20:25:56 +00:00
1 parent 57816ed7e6
commit 7493cff727
16 files changed
+226 -106

No files matched your search

+6
View File
@@ -116,6 +116,12 @@ example Tables) follow a different, code-enforced rule, documented in
`tools.Consumers` (`core/services/addons/tools/consumers.go`) as route or
reject. The matrix test scans the sources and fails for an unclassified file.
One backend route sits outside the add-on surface: `POST /backends/library/install`
installs a backend (not an add-on) from a library entry. It is exposed as the
`install_library_backend` tool and is a plain admin mutation under safety
rule 1. Listing library entries stays on `/api/addons` and has no assistant
tool, for the reason above.
Do not rewrite the unrelated assistant tools to match this rule.
## Distributed mode
+3
View File
@@ -681,6 +681,9 @@ func (a *Application) start() (startErr error) {
assistantClient.StatsRecorder = a.statsRecorder
assistantClient.FallbackUser = a.fallbackUser
assistantClient.VoiceProfiles = a.voiceProfileStore
if a.backendLibrary != nil {
assistantClient.BackendLibrary = a.backendLibrary
}
if a.distributed != nil && a.distributed.Unloader != nil {
assistantClient.LoadStopper = a.distributed.Unloader
}
+3
View File
@@ -62,6 +62,9 @@ type LocalAIClient interface {
// ListKnownBackends returns the same shape as REST /backends/known.
ListKnownBackends(ctx context.Context) ([]schema.KnownBackend, error)
InstallBackend(ctx context.Context, req InstallBackendRequest) (jobID string, err error)
// InstallLibraryBackend installs a backend from a library entry. It
// returns when the install is done.
InstallLibraryBackend(ctx context.Context, req InstallLibraryBackendRequest) (*InstallLibraryBackendResult, error)
UpgradeBackend(ctx context.Context, name string) (jobID string, err error)
// ---- System ----
+23 -22
View File
@@ -49,28 +49,29 @@ var toolToHTTPRoute = map[string]string{
ToolListFailoverChains: "GET /api/failover",
// Mutating tools.
ToolInstallModel: "POST /models/apply",
ToolImportModelURI: "POST /models/import-uri",
ToolDeleteModel: "POST /models/delete/:name",
ToolEditModelConfig: "PATCH /api/models/config-json/:name",
ToolReloadModels: "POST /models/reload",
ToolCancelModelLoad: "POST /api/models/:id/load-cancel",
ToolLoadModel: "POST /backend/load",
ToolInstallBackend: "POST /backends/apply",
ToolUpgradeBackend: "POST /backends/upgrade/:name",
ToolToggleModelState: "PUT /models/toggle-state/:name/:action",
ToolToggleModelPinned: "PUT /models/toggle-pinned/:name/:action",
ToolSetBranding: "POST /api/settings (instance_name, instance_tagline)",
ToolSetAlias: "PATCH /api/models/config-json/:name (swap) or POST /models/import (create)",
ToolSeedRouterCorpus: "POST /api/router/:name/corpus",
ToolClearRouterCorpus: "DELETE /api/router/:name/corpus",
ToolCreateVoiceProfile: "POST /api/voice-profiles",
ToolDeleteVoiceProfile: "DELETE /api/voice-profiles/:id",
ToolSetNodeVRAMBudget: "PUT /api/nodes/:id/vram-budget",
ToolSetScheduling: "POST /api/nodes/scheduling",
ToolDeleteScheduling: "DELETE /api/nodes/scheduling/:model",
ToolPinFailoverTarget: "POST /api/failover/:chain/pin",
ToolUnpinFailoverTarget: "DELETE /api/failover/:chain/pin",
ToolInstallModel: "POST /models/apply",
ToolImportModelURI: "POST /models/import-uri",
ToolDeleteModel: "POST /models/delete/:name",
ToolEditModelConfig: "PATCH /api/models/config-json/:name",
ToolReloadModels: "POST /models/reload",
ToolCancelModelLoad: "POST /api/models/:id/load-cancel",
ToolLoadModel: "POST /backend/load",
ToolInstallBackend: "POST /backends/apply",
ToolInstallLibraryBackend: "POST /backends/library/install",
ToolUpgradeBackend: "POST /backends/upgrade/:name",
ToolToggleModelState: "PUT /models/toggle-state/:name/:action",
ToolToggleModelPinned: "PUT /models/toggle-pinned/:name/:action",
ToolSetBranding: "POST /api/settings (instance_name, instance_tagline)",
ToolSetAlias: "PATCH /api/models/config-json/:name (swap) or POST /models/import (create)",
ToolSeedRouterCorpus: "POST /api/router/:name/corpus",
ToolClearRouterCorpus: "DELETE /api/router/:name/corpus",
ToolCreateVoiceProfile: "POST /api/voice-profiles",
ToolDeleteVoiceProfile: "DELETE /api/voice-profiles/:id",
ToolSetNodeVRAMBudget: "PUT /api/nodes/:id/vram-budget",
ToolSetScheduling: "POST /api/nodes/scheduling",
ToolDeleteScheduling: "DELETE /api/nodes/scheduling/:model",
ToolPinFailoverTarget: "POST /api/failover/:chain/pin",
ToolUnpinFailoverTarget: "DELETE /api/failover/:chain/pin",
}
// allKnownTools is the union of expectedFullCatalog (defined in
+15
View File
@@ -76,6 +76,21 @@ type InstallBackendRequest struct {
BackendName string `json:"backend_name" jsonschema:"Backend identifier (e.g. llama-cpp)."`
}
// InstallLibraryBackendRequest is the input for install_library_backend.
type InstallLibraryBackendRequest struct {
SourceID string `json:"source_id" jsonschema:"The library the entry comes from, as shown in the Add-ons library list."`
Name string `json:"name" jsonschema:"The backend name of the library entry."`
Version string `json:"version" jsonschema:"The version or the channel of the library entry."`
}
// InstallLibraryBackendResult is the output of install_library_backend. The
// install runs to the end before the call returns, so there is no job id.
type InstallLibraryBackendResult struct {
Status string `json:"status"`
Name string `json:"name"`
SourceID string `json:"source_id"`
}
// Backend is the LLM-facing summary returned by list_backends. We don't
// expose gallery.SystemBackend directly because it carries filesystem
// paths (RunFile, IsSystem, IsMeta, the full Metadata) the LLM doesn't
+48 -39
View File
@@ -23,45 +23,46 @@ type fakeClient struct {
calls []fakeCall
// Per-method overrides. Tests set these.
gallerySearch func(GallerySearchQuery) ([]gallery.Metadata, error)
listInstalledModels func(Capability) ([]InstalledModel, error)
listGalleries func() ([]config.Gallery, error)
getJobStatus func(string) (*JobStatus, error)
getModelConfig func(string) (*ModelConfigView, error)
installModel func(InstallModelRequest) (string, error)
importModelURI func(ImportModelURIRequest) (*ImportModelURIResponse, error)
deleteModel func(string) error
editModelConfig func(string, map[string]any) error
setAlias func(string, string) error
listAliases func() ([]AliasInfo, error)
reloadModels func() error
loadModel func(string) ([]string, error)
listBackends func() ([]Backend, error)
listKnownBackends func() ([]schema.KnownBackend, error)
installBackend func(InstallBackendRequest) (string, error)
upgradeBackend func(string) (string, error)
systemInfo func() (*SystemInfo, error)
listNodes func() ([]Node, error)
listScheduling func() ([]ModelSchedulingConfig, error)
getScheduling func(string) (*ModelSchedulingConfig, error)
setScheduling func(SetSchedulingRequest) (*ModelSchedulingConfig, error)
deleteScheduling func(string) error
setNodeVRAMBudget func(string, string) error
vramEstimate func(VRAMEstimateRequest) (*vram.EstimateResult, error)
toggleModelState func(string, modeladmin.Action) error
toggleModelPinned func(string, modeladmin.Action) error
getBranding func() (*Branding, error)
setBranding func(SetBrandingRequest) (*Branding, error)
listVoiceProfiles func() ([]VoiceProfile, error)
createVoiceProfile func(CreateVoiceProfileRequest) (*VoiceProfile, error)
deleteVoiceProfile func(string) error
getUsageStats func(UsageStatsQuery) (*UsageStats, error)
getPIIEvents func(PIIEventsQuery) ([]PIIEvent, error)
getMiddlewareStatus func() (*MiddlewareStatus, error)
getRouterDecisions func(RouterDecisionsQuery) ([]RouterDecision, error)
listFailoverChains func() ([]FailoverChainInfo, error)
pinFailoverTarget func(string, string) error
unpinFailoverTarget func(string) error
gallerySearch func(GallerySearchQuery) ([]gallery.Metadata, error)
listInstalledModels func(Capability) ([]InstalledModel, error)
listGalleries func() ([]config.Gallery, error)
getJobStatus func(string) (*JobStatus, error)
getModelConfig func(string) (*ModelConfigView, error)
installModel func(InstallModelRequest) (string, error)
importModelURI func(ImportModelURIRequest) (*ImportModelURIResponse, error)
deleteModel func(string) error
editModelConfig func(string, map[string]any) error
setAlias func(string, string) error
listAliases func() ([]AliasInfo, error)
reloadModels func() error
loadModel func(string) ([]string, error)
listBackends func() ([]Backend, error)
listKnownBackends func() ([]schema.KnownBackend, error)
installBackend func(InstallBackendRequest) (string, error)
installLibraryBackend func(InstallLibraryBackendRequest) (*InstallLibraryBackendResult, error)
upgradeBackend func(string) (string, error)
systemInfo func() (*SystemInfo, error)
listNodes func() ([]Node, error)
listScheduling func() ([]ModelSchedulingConfig, error)
getScheduling func(string) (*ModelSchedulingConfig, error)
setScheduling func(SetSchedulingRequest) (*ModelSchedulingConfig, error)
deleteScheduling func(string) error
setNodeVRAMBudget func(string, string) error
vramEstimate func(VRAMEstimateRequest) (*vram.EstimateResult, error)
toggleModelState func(string, modeladmin.Action) error
toggleModelPinned func(string, modeladmin.Action) error
getBranding func() (*Branding, error)
setBranding func(SetBrandingRequest) (*Branding, error)
listVoiceProfiles func() ([]VoiceProfile, error)
createVoiceProfile func(CreateVoiceProfileRequest) (*VoiceProfile, error)
deleteVoiceProfile func(string) error
getUsageStats func(UsageStatsQuery) (*UsageStats, error)
getPIIEvents func(PIIEventsQuery) ([]PIIEvent, error)
getMiddlewareStatus func() (*MiddlewareStatus, error)
getRouterDecisions func(RouterDecisionsQuery) ([]RouterDecision, error)
listFailoverChains func() ([]FailoverChainInfo, error)
pinFailoverTarget func(string, string) error
unpinFailoverTarget func(string) error
}
type fakeCall struct {
@@ -218,6 +219,14 @@ func (f *fakeClient) InstallBackend(_ context.Context, req InstallBackendRequest
return "", errNotConfigured
}
func (f *fakeClient) InstallLibraryBackend(_ context.Context, req InstallLibraryBackendRequest) (*InstallLibraryBackendResult, error) {
f.record("InstallLibraryBackend", req)
if f.installLibraryBackend != nil {
return f.installLibraryBackend(req)
}
return nil, errNotConfigured
}
func (f *fakeClient) UpgradeBackend(_ context.Context, name string) (string, error) {
f.record("UpgradeBackend", name)
if f.upgradeBackend != nil {
+13
View File
@@ -428,6 +428,19 @@ func (c *Client) InstallBackend(ctx context.Context, req localaitools.InstallBac
return resp.ID, nil
}
func (c *Client) InstallLibraryBackend(ctx context.Context, req localaitools.InstallLibraryBackendRequest) (*localaitools.InstallLibraryBackendResult, error) {
body := map[string]any{"sourceId": req.SourceID, "name": req.Name, "version": req.Version}
var resp struct {
Status string `json:"status"`
Name string `json:"name"`
SourceID string `json:"sourceId"`
}
if err := c.do(ctx, http.MethodPost, routeBackendsLibraryInstall, body, &resp); err != nil {
return nil, err
}
return &localaitools.InstallLibraryBackendResult{Status: resp.Status, Name: resp.Name, SourceID: resp.SourceID}, nil
}
func (c *Client) UpgradeBackend(ctx context.Context, name string) (string, error) {
var resp struct {
ID string `json:"uuid"`
@@ -482,3 +482,22 @@ var _ = Describe("Bearer token", func() {
Expect(sawAuth).To(Equal("Bearer secret-key"))
})
})
var _ = Describe("InstallLibraryBackend", func() {
It("posts the library entry to the backend library route", func() {
var path string
var body map[string]any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path = r.Method + " " + r.URL.Path
Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed())
_ = json.NewEncoder(w).Encode(map[string]any{"status": "installed", "name": "demo", "sourceId": "official"})
}))
DeferCleanup(srv.Close)
res, err := New(srv.URL, "").InstallLibraryBackend(context.Background(), localaitools.InstallLibraryBackendRequest{SourceID: "official", Name: "demo", Version: "1.0.0"})
Expect(err).ToNot(HaveOccurred())
Expect(path).To(Equal("POST /backends/library/install"))
Expect(body).To(Equal(map[string]any{"sourceId": "official", "name": "demo", "version": "1.0.0"}))
Expect(res.Status).To(Equal("installed"))
})
})
+25 -24
View File
@@ -11,30 +11,31 @@ import (
// registrations in core/http/routes/localai.go — the Tool↔REST drift detector
// in coverage_test.go documents the mapping.
const (
routeWelcome = "/"
routeModelsApply = "/models/apply"
routeModelsAvail = "/models/available"
routeModelsGall = "/models/galleries"
routeModelsImport = "/models/import-uri"
routeModelImport = "/models/import"
routeAliases = "/api/aliases"
routeModelsReload = "/models/reload"
routeBackendLoad = "/backend/load"
routeBackends = "/backends"
routeBackendsKnown = "/backends/known"
routeBackendsApply = "/backends/apply"
routeNodes = "/api/nodes"
routeScheduling = "/api/nodes/scheduling"
routeVRAMEstimate = "/api/models/vram-estimate"
routeBranding = "/api/branding"
routeSettings = "/api/settings"
routeUsage = "/api/usage"
routeUsageAll = "/api/usage/all"
routePIIEvents = "/api/pii/events"
routeMiddleware = "/api/middleware/status"
routeRouterDecisions = "/api/router/decisions"
routeVoiceProfiles = "/api/voice-profiles"
routeFailover = "/api/failover"
routeWelcome = "/"
routeModelsApply = "/models/apply"
routeModelsAvail = "/models/available"
routeModelsGall = "/models/galleries"
routeModelsImport = "/models/import-uri"
routeModelImport = "/models/import"
routeAliases = "/api/aliases"
routeModelsReload = "/models/reload"
routeBackendLoad = "/backend/load"
routeBackends = "/backends"
routeBackendsKnown = "/backends/known"
routeBackendsApply = "/backends/apply"
routeBackendsLibraryInstall = "/backends/library/install"
routeNodes = "/api/nodes"
routeScheduling = "/api/nodes/scheduling"
routeVRAMEstimate = "/api/models/vram-estimate"
routeBranding = "/api/branding"
routeSettings = "/api/settings"
routeUsage = "/api/usage"
routeUsageAll = "/api/usage/all"
routePIIEvents = "/api/pii/events"
routeMiddleware = "/api/middleware/status"
routeRouterDecisions = "/api/router/decisions"
routeVoiceProfiles = "/api/voice-profiles"
routeFailover = "/api/failover"
)
func routeJobStatus(jobID string) string {
+22
View File
@@ -56,6 +56,9 @@ type Client struct {
// stopper the HTTP endpoint uses, so both paths stop work the same way.
LoadStopper nodes.LoadAttemptStopper
VoiceProfiles *voiceprofile.Store
// BackendLibrary backs install_library_backend. nil when the add-on
// library is off; the tool then reports it is not enabled.
BackendLibrary LibraryBackendInstaller
// StatsRecorder and FallbackUser are optional — they back the
// get_usage_stats tool. nil StatsRecorder makes the tool return an
@@ -478,6 +481,25 @@ func (c *Client) InstallBackend(ctx context.Context, req localaitools.InstallBac
return id.String(), nil
}
// LibraryBackendInstaller installs a backend from a library entry. It is the
// installer the REST route uses, so both paths share one code path.
type LibraryBackendInstaller interface {
Install(ctx context.Context, sourceID, backend, version string, status func(string, string, string, float64)) error
}
func (c *Client) InstallLibraryBackend(ctx context.Context, req localaitools.InstallLibraryBackendRequest) (*localaitools.InstallLibraryBackendResult, error) {
if req.SourceID == "" || req.Name == "" || req.Version == "" {
return nil, errors.New("source_id, name and version are required")
}
if c.BackendLibrary == nil {
return nil, errors.New("the backend library is not enabled on this instance")
}
if err := c.BackendLibrary.Install(ctx, req.SourceID, req.Name, req.Version, nil); err != nil {
return nil, err
}
return &localaitools.InstallLibraryBackendResult{Status: "installed", Name: req.Name, SourceID: req.SourceID}, nil
}
func (c *Client) UpgradeBackend(ctx context.Context, name string) (string, error) {
if name == "" {
return "", errors.New("name is required")
+1 -1
View File
@@ -2,7 +2,7 @@
These rules are non-negotiable. The user trusts you to operate their server without unintended changes.
1. **Confirm before mutating.** Before calling any of these tools — `install_model`, `import_model_uri`, `delete_model`, `install_backend`, `upgrade_backend`, `edit_model_config`, `reload_models`, `load_model`, `cancel_model_load`, `toggle_model_state`, `toggle_model_pinned`, `set_branding`, `set_alias`, `seed_router_corpus`, `clear_router_corpus`, `create_voice_profile`, `delete_voice_profile`, `set_node_vram_budget`, `set_scheduling`, `delete_scheduling`, `pin_failover_target`, `unpin_failover_target` — first state in plain language what you are about to do (which tool, which target, which arguments) and wait for the user's explicit confirmation in the next turn. "Yes", "do it", "go ahead", "proceed" all count as confirmation. Anything else does not.
1. **Confirm before mutating.** Before calling any of these tools — `install_model`, `import_model_uri`, `delete_model`, `install_backend`, `install_library_backend`, `upgrade_backend`, `edit_model_config`, `reload_models`, `load_model`, `cancel_model_load`, `toggle_model_state`, `toggle_model_pinned`, `set_branding`, `set_alias`, `seed_router_corpus`, `clear_router_corpus`, `create_voice_profile`, `delete_voice_profile`, `set_node_vram_budget`, `set_scheduling`, `delete_scheduling`, `pin_failover_target`, `unpin_failover_target` — first state in plain language what you are about to do (which tool, which target, which arguments) and wait for the user's explicit confirmation in the next turn. "Yes", "do it", "go ahead", "proceed" all count as confirmation. Anything else does not.
2. **Disambiguate before mutating.** If the user's request is ambiguous (several gallery candidates match, the model name has multiple installed versions, the backend has variants), present the candidates as a numbered list and ask the user to pick before calling any mutating tool.
+1
View File
@@ -32,6 +32,7 @@ The MCP `tools/list` endpoint also exposes the full input schema for each of the
- `import_model_uri` — Install a model from an arbitrary URI (HuggingFace, OCI, http(s), file://). May return `ambiguous_backend` when several backends apply; call again with `backend_preference` to disambiguate.
- `delete_model` — Delete an installed model.
- `install_backend` — Install a backend.
- `install_library_backend` — Install a backend from an add-on library entry by `source_id`, `name` and `version`. The call returns when the install is done, with no job id. The backend gallery is not changed.
- `upgrade_backend` — Upgrade an installed backend by name.
- `edit_model_config` — Patch (deep-merge) JSON into an installed model's config.
- `reload_models` — Reload all model configs from disk.
@@ -0,0 +1,7 @@
# Skill: Install a backend from the add-on library
1. The user names a library backend, or asks for one that the backend gallery does not have. You need the `source_id` (the library), the backend `name` and the `version` or channel, as the Add-ons library list shows them. Ask the user for any of the three that is missing. Do not guess them.
2. Tell the user that the backend runs native code, that the backend gallery is not changed, and that the library checks the signature of the image.
3. Wait for explicit confirmation, then call `install_library_backend` with `source_id`, `name` and `version`. The call returns when the install is done.
4. On an error, report the reason as it is. A name installed from another origin, a failed integrity check and an image over the size limit are refusals of the host. Do not retry with other values.
5. After a success, call `list_backends` to confirm the backend is installed.
+4
View File
@@ -146,6 +146,7 @@ var _ = Describe("Tool dispatch", func() {
{ToolImportModelURI, ImportModelURIRequest{URI: "Qwen/Qwen3-4B-GGUF"}, "ImportModelURI"},
{ToolDeleteModel, map[string]any{"name": "foo"}, "DeleteModel"},
{ToolInstallBackend, InstallBackendRequest{BackendName: "llama-cpp"}, "InstallBackend"},
{ToolInstallLibraryBackend, InstallLibraryBackendRequest{SourceID: "official", Name: "demo", Version: "1.0.0"}, "InstallLibraryBackend"},
{ToolUpgradeBackend, map[string]any{"name": "llama-cpp"}, "UpgradeBackend"},
{ToolEditModelConfig, map[string]any{"name": "foo", "patch": map[string]any{"context_size": 4096}}, "EditModelConfig"},
{ToolReloadModels, struct{}{}, "ReloadModels"},
@@ -169,6 +170,9 @@ var _ = Describe("Tool dispatch", func() {
installModel: func(InstallModelRequest) (string, error) { return "job-1", nil },
installBackend: func(InstallBackendRequest) (string, error) { return "job-2", nil },
upgradeBackend: func(string) (string, error) { return "job-3", nil },
installLibraryBackend: func(InstallLibraryBackendRequest) (*InstallLibraryBackendResult, error) {
return &InstallLibraryBackendResult{Status: "installed"}, nil
},
}
ctx, sess, done := connectInMemory(fc, Options{})
DeferCleanup(done)
+22 -20
View File
@@ -30,26 +30,27 @@ const (
// Mutating tools — guarded by Options.DisableMutating and the
// LLM-side safety prompt (see prompts/10_safety.md).
ToolInstallModel = "install_model"
ToolImportModelURI = "import_model_uri"
ToolDeleteModel = "delete_model"
ToolEditModelConfig = "edit_model_config"
ToolReloadModels = "reload_models"
ToolCancelModelLoad = "cancel_model_load"
ToolLoadModel = "load_model"
ToolInstallBackend = "install_backend"
ToolUpgradeBackend = "upgrade_backend"
ToolToggleModelState = "toggle_model_state"
ToolToggleModelPinned = "toggle_model_pinned"
ToolSetBranding = "set_branding"
ToolSetAlias = "set_alias"
ToolSeedRouterCorpus = "seed_router_corpus"
ToolClearRouterCorpus = "clear_router_corpus"
ToolCreateVoiceProfile = "create_voice_profile"
ToolDeleteVoiceProfile = "delete_voice_profile"
ToolSetNodeVRAMBudget = "set_node_vram_budget"
ToolSetScheduling = "set_scheduling"
ToolDeleteScheduling = "delete_scheduling"
ToolInstallModel = "install_model"
ToolImportModelURI = "import_model_uri"
ToolDeleteModel = "delete_model"
ToolEditModelConfig = "edit_model_config"
ToolReloadModels = "reload_models"
ToolCancelModelLoad = "cancel_model_load"
ToolLoadModel = "load_model"
ToolInstallBackend = "install_backend"
ToolUpgradeBackend = "upgrade_backend"
ToolInstallLibraryBackend = "install_library_backend"
ToolToggleModelState = "toggle_model_state"
ToolToggleModelPinned = "toggle_model_pinned"
ToolSetBranding = "set_branding"
ToolSetAlias = "set_alias"
ToolSeedRouterCorpus = "seed_router_corpus"
ToolClearRouterCorpus = "clear_router_corpus"
ToolCreateVoiceProfile = "create_voice_profile"
ToolDeleteVoiceProfile = "delete_voice_profile"
ToolSetNodeVRAMBudget = "set_node_vram_budget"
ToolSetScheduling = "set_scheduling"
ToolDeleteScheduling = "delete_scheduling"
// ToolPinFailoverTarget and ToolUnpinFailoverTarget live here (rather
// than grouped with ToolListFailoverChains below) so mutatingToolNames
// stays a contiguous scan of this block.
@@ -83,6 +84,7 @@ var mutatingToolNames = []string{
ToolLoadModel,
ToolInstallBackend,
ToolUpgradeBackend,
ToolInstallLibraryBackend,
ToolToggleModelState,
ToolToggleModelPinned,
ToolSetBranding,
+14
View File
@@ -47,6 +47,20 @@ func registerBackendTools(s *mcp.Server, client LocalAIClient, opts Options) {
return jsonResult(map[string]any{"job_id": jobID}), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: ToolInstallLibraryBackend,
Description: "Install a backend from an add-on library entry (a backend entry of a library source). The library resolves the image and checks its signature, so the call names only the source, the backend and the version. The backend gallery is not changed and a backend runs native code. The call returns when the install is done. Requires user confirmation per safety rule 1.",
}, func(ctx context.Context, _ *mcp.CallToolRequest, args InstallLibraryBackendRequest) (*mcp.CallToolResult, any, error) {
if args.SourceID == "" || args.Name == "" || args.Version == "" {
return errorResultf("source_id, name and version are required"), nil, nil
}
res, err := client.InstallLibraryBackend(ctx, args)
if err != nil {
return errorResult(err), nil, nil
}
return jsonResult(res), nil, nil
})
mcp.AddTool(s, &mcp.Tool{
Name: ToolUpgradeBackend,
Description: "Upgrade an installed backend by name. Requires user confirmation per safety rule 1.",