mirror of
https://github.com/mudler/LocalAI.git
synced 2026-06-20 14:49:09 -04:00
feat(mcp): add set_alias and list_aliases tools
Expose model-alias management over the LocalAI Assistant MCP surface:
list_aliases (read-only, GET /api/aliases) and set_alias (mutating).
SetAlias is swap-first: PATCH /api/models/config-json/:name swaps an
existing alias's target (validated, non-destructive) and a 404 falls
back to POST /models/import to create a fresh {name, alias} config. The
inproc client mirrors this via ConfigService.PatchConfig + a create path
modeled on ImportModelEndpoint. Deletion reuses delete_model.
Assisted-by: Claude:claude-opus-4 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
@@ -51,6 +51,12 @@ func (stubClient) EditModelConfig(_ context.Context, _ string, _ map[string]any)
|
||||
return nil
|
||||
}
|
||||
func (stubClient) ReloadModels(_ context.Context) error { return nil }
|
||||
func (stubClient) SetAlias(_ context.Context, _, _ string) error {
|
||||
return nil
|
||||
}
|
||||
func (stubClient) ListAliases(_ context.Context) ([]localaitools.AliasInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (stubClient) ListBackends(_ context.Context) ([]localaitools.Backend, error) {
|
||||
return []localaitools.Backend{{Name: "stub-backend", Installed: true}}, nil
|
||||
}
|
||||
|
||||
@@ -38,6 +38,14 @@ type LocalAIClient interface {
|
||||
ReloadModels(ctx context.Context) error
|
||||
ImportModelURI(ctx context.Context, req ImportModelURIRequest) (*ImportModelURIResponse, error)
|
||||
|
||||
// ---- Model aliases ----
|
||||
// SetAlias creates the alias `name` pointing at `target`, or swaps an
|
||||
// existing alias's target. The server validates that `target` is an
|
||||
// existing, non-alias, enabled model. Deletion reuses DeleteModel.
|
||||
SetAlias(ctx context.Context, name, target string) error
|
||||
// ListAliases returns every configured alias and its target.
|
||||
ListAliases(ctx context.Context) ([]AliasInfo, error)
|
||||
|
||||
// ---- Backends ----
|
||||
// ListBackends returns installed backends. The shape stays a thin
|
||||
// localaitools.Backend rather than gallery.SystemBackend because the
|
||||
|
||||
@@ -41,6 +41,7 @@ var toolToHTTPRoute = map[string]string{
|
||||
ToolGetPIIEvents: "GET /api/pii/events",
|
||||
ToolGetMiddlewareStatus: "GET /api/middleware/status",
|
||||
ToolGetRouterDecisions: "GET /api/router/decisions",
|
||||
ToolListAliases: "GET /api/aliases",
|
||||
|
||||
// Mutating tools.
|
||||
ToolInstallModel: "POST /models/apply",
|
||||
@@ -53,6 +54,7 @@ var toolToHTTPRoute = map[string]string{
|
||||
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)",
|
||||
}
|
||||
|
||||
// allKnownTools is the union of expectedFullCatalog (defined in
|
||||
|
||||
@@ -52,6 +52,14 @@ type ModelConfigView struct {
|
||||
JSON map[string]any `json:"json,omitempty" jsonschema:"Parsed JSON view of the same config (convenience for diffing)."`
|
||||
}
|
||||
|
||||
// AliasInfo is one alias -> target pair, the shape list_aliases returns and
|
||||
// GET /api/aliases emits. Kept aligned with localai.AliasInfo so the
|
||||
// MCP wire output matches the REST endpoint by construction.
|
||||
type AliasInfo struct {
|
||||
Name string `json:"name"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
// InstallModelRequest is the input for install_model.
|
||||
type InstallModelRequest struct {
|
||||
GalleryName string `json:"gallery_name,omitempty" jsonschema:"The gallery the model lives in (from gallery_search). Optional when ModelName is unique across galleries."`
|
||||
|
||||
@@ -32,6 +32,8 @@ type fakeClient struct {
|
||||
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
|
||||
listBackends func() ([]Backend, error)
|
||||
listKnownBackends func() ([]schema.KnownBackend, error)
|
||||
@@ -143,6 +145,22 @@ func (f *fakeClient) EditModelConfig(_ context.Context, name string, patch map[s
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) SetAlias(_ context.Context, name, target string) error {
|
||||
f.record("SetAlias", []any{name, target})
|
||||
if f.setAlias != nil {
|
||||
return f.setAlias(name, target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ListAliases(_ context.Context) ([]AliasInfo, error) {
|
||||
f.record("ListAliases", nil)
|
||||
if f.listAliases != nil {
|
||||
return f.listAliases()
|
||||
}
|
||||
return []AliasInfo{}, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ReloadModels(_ context.Context) error {
|
||||
f.record("ReloadModels", nil)
|
||||
if f.reloadModels != nil {
|
||||
|
||||
@@ -338,6 +338,42 @@ func (c *Client) ReloadModels(ctx context.Context) error {
|
||||
return c.do(ctx, http.MethodPost, routeModelsReload, nil, nil)
|
||||
}
|
||||
|
||||
// ---- Model aliases ----
|
||||
|
||||
// SetAlias is swap-first: it PATCHes the alias config (a deep-merge that
|
||||
// validates the target and preserves any other fields), and only creates a
|
||||
// fresh config when the PATCH reports the model doesn't exist yet. We prefer
|
||||
// PATCH over POST /models/import for existing names because import rewrites
|
||||
// the whole file, whereas PATCH gives a reliable 404 not-found signal
|
||||
// (ErrHTTPNotFound) to branch on and never clobbers an existing config.
|
||||
func (c *Client) SetAlias(ctx context.Context, name, target string) error {
|
||||
if name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if target == "" {
|
||||
return errors.New("target is required")
|
||||
}
|
||||
err := c.do(ctx, http.MethodPatch, routeModelConfigJSON(name), map[string]any{"alias": target}, nil)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, ErrHTTPNotFound) {
|
||||
return err
|
||||
}
|
||||
// No such config yet: create it. The import endpoint validates the alias
|
||||
// target server-side, same as the PATCH path.
|
||||
return c.do(ctx, http.MethodPost, routeModelImport, map[string]any{"name": name, "alias": target}, nil)
|
||||
}
|
||||
|
||||
func (c *Client) ListAliases(ctx context.Context) ([]localaitools.AliasInfo, error) {
|
||||
// /api/aliases returns []{name,target} directly — pass it through.
|
||||
var out []localaitools.AliasInfo
|
||||
if err := c.do(ctx, http.MethodGet, routeAliases, nil, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ---- Backends ----
|
||||
|
||||
func (c *Client) ListBackends(ctx context.Context) ([]localaitools.Backend, error) {
|
||||
|
||||
@@ -166,6 +166,92 @@ var _ = Describe("httpapi.Client against the LocalAI admin REST surface", func()
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Model aliases", func() {
|
||||
Describe("ListAliases", func() {
|
||||
It("passes the GET /api/aliases payload through unchanged", func() {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
Expect(r.Method).To(Equal(http.MethodGet))
|
||||
Expect(r.URL.Path).To(Equal("/api/aliases"))
|
||||
_ = json.NewEncoder(w).Encode([]map[string]any{
|
||||
{"name": "gpt-4", "target": "qwen"},
|
||||
})
|
||||
}))
|
||||
DeferCleanup(srv.Close)
|
||||
|
||||
out, err := New(srv.URL, "").ListAliases(context.Background())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(out).To(HaveLen(1))
|
||||
Expect(out[0].Name).To(Equal("gpt-4"))
|
||||
Expect(out[0].Target).To(Equal("qwen"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("SetAlias", func() {
|
||||
It("swaps an existing alias via PATCH without falling back to import", func() {
|
||||
var patched, imported bool
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodPatch && r.URL.Path == "/api/models/config-json/gpt-4":
|
||||
patched = true
|
||||
var body map[string]any
|
||||
Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed())
|
||||
Expect(body).To(HaveKeyWithValue("alias", "qwen"))
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
|
||||
case r.URL.Path == "/models/import":
|
||||
imported = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
default:
|
||||
http.Error(w, "unexpected", http.StatusTeapot)
|
||||
}
|
||||
}))
|
||||
DeferCleanup(srv.Close)
|
||||
|
||||
Expect(New(srv.URL, "").SetAlias(context.Background(), "gpt-4", "qwen")).To(Succeed())
|
||||
Expect(patched).To(BeTrue(), "PATCH should be attempted first")
|
||||
Expect(imported).To(BeFalse(), "import must not run when PATCH succeeds")
|
||||
})
|
||||
|
||||
It("creates a fresh alias via import when PATCH reports the model is missing", func() {
|
||||
var imported bool
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodPatch:
|
||||
http.Error(w, "model configuration not found", http.StatusNotFound)
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/models/import":
|
||||
imported = true
|
||||
var body map[string]any
|
||||
Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed())
|
||||
Expect(body).To(HaveKeyWithValue("name", "gpt-4"))
|
||||
Expect(body).To(HaveKeyWithValue("alias", "qwen"))
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"success": true})
|
||||
default:
|
||||
http.Error(w, "unexpected", http.StatusTeapot)
|
||||
}
|
||||
}))
|
||||
DeferCleanup(srv.Close)
|
||||
|
||||
Expect(New(srv.URL, "").SetAlias(context.Background(), "gpt-4", "qwen")).To(Succeed())
|
||||
Expect(imported).To(BeTrue(), "import should create the alias on a 404")
|
||||
})
|
||||
|
||||
It("surfaces a non-404 PATCH error without attempting import", func() {
|
||||
var imported bool
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/models/import" {
|
||||
imported = true
|
||||
}
|
||||
http.Error(w, "target is an alias", http.StatusBadRequest)
|
||||
}))
|
||||
DeferCleanup(srv.Close)
|
||||
|
||||
err := New(srv.URL, "").SetAlias(context.Background(), "gpt-4", "bad")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).To(ContainSubstring("target is an alias"))
|
||||
Expect(imported).To(BeFalse(), "a 400 swap error must not trigger create")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("ErrHTTPNotFound", func() {
|
||||
Context("on a clean 404 status", func() {
|
||||
var (
|
||||
|
||||
@@ -16,6 +16,8 @@ const (
|
||||
routeModelsAvail = "/models/available"
|
||||
routeModelsGall = "/models/galleries"
|
||||
routeModelsImport = "/models/import-uri"
|
||||
routeModelImport = "/models/import"
|
||||
routeAliases = "/api/aliases"
|
||||
routeModelsReload = "/models/reload"
|
||||
routeBackends = "/backends"
|
||||
routeBackendsKnown = "/backends/known"
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/mudler/LocalAI/core/config"
|
||||
@@ -25,7 +27,9 @@ import (
|
||||
localaitools "github.com/mudler/LocalAI/pkg/mcp/localaitools"
|
||||
"github.com/mudler/LocalAI/pkg/model"
|
||||
"github.com/mudler/LocalAI/pkg/system"
|
||||
"github.com/mudler/LocalAI/pkg/utils"
|
||||
"github.com/mudler/LocalAI/pkg/vram"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Client implements localaitools.LocalAIClient by calling LocalAI services
|
||||
@@ -298,6 +302,78 @@ func (c *Client) ReloadModels(_ context.Context) error {
|
||||
return c.ConfigLoader.LoadModelConfigsFromPath(c.SystemState.Model.ModelsPath)
|
||||
}
|
||||
|
||||
// ---- Model aliases ----
|
||||
|
||||
// SetAlias is swap-first to match the httpapi client: PatchConfig swaps an
|
||||
// existing alias's target (validating it and preserving other fields) and
|
||||
// returns ErrNotFound when the config doesn't exist yet, which is the signal
|
||||
// to create it. createAlias mirrors the create path of ImportModelEndpoint.
|
||||
func (c *Client) SetAlias(ctx context.Context, name, target string) error {
|
||||
if name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
if target == "" {
|
||||
return errors.New("target is required")
|
||||
}
|
||||
_, err := c.modelAdmin.PatchConfig(ctx, name, map[string]any{"alias": target})
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, modeladmin.ErrNotFound) {
|
||||
return err
|
||||
}
|
||||
return c.createAlias(name, target)
|
||||
}
|
||||
|
||||
// createAlias writes a fresh `{name, alias}` config to disk and reloads,
|
||||
// mirroring localai.ImportModelEndpoint's create path: validate, validate the
|
||||
// alias target, verify the path is trusted, write, reload, best-effort preload.
|
||||
func (c *Client) createAlias(name, target string) error {
|
||||
if c.SystemState == nil {
|
||||
return errors.New("system state not available")
|
||||
}
|
||||
cfg := config.ModelConfig{Name: name, Alias: target}
|
||||
if valid, vErr := cfg.Validate(); !valid {
|
||||
if vErr != nil {
|
||||
return vErr
|
||||
}
|
||||
return errors.New("invalid alias configuration")
|
||||
}
|
||||
if err := c.ConfigLoader.ValidateAliasTarget(&cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
modelsPath := c.SystemState.Model.ModelsPath
|
||||
if err := utils.VerifyPath(name+".yaml", modelsPath); err != nil {
|
||||
return fmt.Errorf("model path not trusted: %w", err)
|
||||
}
|
||||
// Marshal only the user-provided fields (not the full struct with Go
|
||||
// zero values), matching what the import endpoint persists for an alias.
|
||||
yamlData, err := yaml.Marshal(map[string]any{"name": name, "alias": target})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal alias config: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(modelsPath, name+".yaml"), yamlData, 0644); err != nil {
|
||||
return fmt.Errorf("write alias config: %w", err)
|
||||
}
|
||||
if err := c.ConfigLoader.LoadModelConfigsFromPath(modelsPath, c.AppConfig.ToConfigLoaderOptions()...); err != nil {
|
||||
return fmt.Errorf("reload configs: %w", err)
|
||||
}
|
||||
// Preload is best-effort — a failure here doesn't undo the create.
|
||||
_ = c.ConfigLoader.Preload(modelsPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) ListAliases(_ context.Context) ([]localaitools.AliasInfo, error) {
|
||||
// Mirror localai.ListAliasesEndpoint: every config whose Alias is set.
|
||||
out := []localaitools.AliasInfo{}
|
||||
for _, cfg := range c.ConfigLoader.GetAllModelsConfigs() {
|
||||
if cfg.IsAlias() {
|
||||
out = append(out, localaitools.AliasInfo{Name: cfg.Name, Target: cfg.Alias})
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ---- Backends ----
|
||||
|
||||
func (c *Client) ListBackends(_ context.Context) ([]localaitools.Backend, error) {
|
||||
|
||||
@@ -3,6 +3,8 @@ package inproc
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
@@ -47,3 +49,78 @@ var _ = Describe("inproc.Client cancellation", func() {
|
||||
Expect(errors.Is(err, context.Canceled)).To(BeTrue(), "got: %v", err)
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("inproc.Client model aliases", func() {
|
||||
var (
|
||||
ctx context.Context
|
||||
tempDir string
|
||||
cl *config.ModelConfigLoader
|
||||
c *Client
|
||||
seedModel func(name, body string)
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
tempDir = GinkgoT().TempDir()
|
||||
systemState, err := system.GetSystemState(system.WithModelPath(tempDir))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
appConfig := config.NewApplicationConfig(config.WithSystemState(systemState))
|
||||
cl = config.NewModelConfigLoader(tempDir)
|
||||
// Gallery/model loaders are unused by the alias methods, so nil is fine.
|
||||
c = New(appConfig, systemState, cl, nil, nil)
|
||||
|
||||
seedModel = func(name, body string) {
|
||||
Expect(os.WriteFile(filepath.Join(tempDir, name+".yaml"), []byte(body), 0644)).To(Succeed())
|
||||
Expect(cl.LoadModelConfigsFromPath(tempDir)).To(Succeed())
|
||||
}
|
||||
})
|
||||
|
||||
Describe("ListAliases", func() {
|
||||
It("returns only configs whose alias field is set", func() {
|
||||
seedModel("real", "name: real\nbackend: llama-cpp\n")
|
||||
seedModel("gpt-4", "name: gpt-4\nalias: real\n")
|
||||
|
||||
out, err := c.ListAliases(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(out).To(ConsistOf(localaitools.AliasInfo{Name: "gpt-4", Target: "real"}))
|
||||
})
|
||||
|
||||
It("returns an empty slice when there are no aliases", func() {
|
||||
seedModel("real", "name: real\nbackend: llama-cpp\n")
|
||||
out, err := c.ListAliases(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(out).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("SetAlias", func() {
|
||||
It("creates a new alias config on disk when the name is unused", func() {
|
||||
seedModel("real", "name: real\nbackend: llama-cpp\n")
|
||||
|
||||
Expect(c.SetAlias(ctx, "gpt-4", "real")).To(Succeed())
|
||||
|
||||
Expect(filepath.Join(tempDir, "gpt-4.yaml")).To(BeAnExistingFile())
|
||||
out, err := c.ListAliases(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(out).To(ConsistOf(localaitools.AliasInfo{Name: "gpt-4", Target: "real"}))
|
||||
})
|
||||
|
||||
It("swaps an existing alias's target in place", func() {
|
||||
seedModel("real", "name: real\nbackend: llama-cpp\n")
|
||||
seedModel("other", "name: other\nbackend: llama-cpp\n")
|
||||
seedModel("gpt-4", "name: gpt-4\nalias: real\n")
|
||||
|
||||
Expect(c.SetAlias(ctx, "gpt-4", "other")).To(Succeed())
|
||||
|
||||
out, err := c.ListAliases(ctx)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(out).To(ConsistOf(localaitools.AliasInfo{Name: "gpt-4", Target: "other"}))
|
||||
})
|
||||
|
||||
It("rejects an alias whose target does not exist", func() {
|
||||
err := c.SetAlias(ctx, "gpt-4", "missing")
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(filepath.Join(tempDir, "gpt-4.yaml")).ToNot(BeAnExistingFile())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -43,6 +43,7 @@ func NewServer(client LocalAIClient, opts Options) *mcp.Server {
|
||||
})
|
||||
|
||||
registerModelTools(srv, client, opts)
|
||||
registerAliasTools(srv, client, opts)
|
||||
registerBackendTools(srv, client, opts)
|
||||
registerConfigTools(srv, client, opts)
|
||||
registerSystemTools(srv, client, opts)
|
||||
|
||||
@@ -88,10 +88,12 @@ var expectedFullCatalog = sortedStrings(
|
||||
ToolInstallModel,
|
||||
ToolListBackends,
|
||||
ToolListGalleries,
|
||||
ToolListAliases,
|
||||
ToolListInstalledModels,
|
||||
ToolListKnownBackends,
|
||||
ToolListNodes,
|
||||
ToolReloadModels,
|
||||
ToolSetAlias,
|
||||
ToolSetBranding,
|
||||
ToolSystemInfo,
|
||||
ToolToggleModelPinned,
|
||||
@@ -110,6 +112,7 @@ var expectedReadOnlyCatalog = sortedStrings(
|
||||
ToolGetPIIEvents,
|
||||
ToolGetRouterDecisions,
|
||||
ToolGetUsageStats,
|
||||
ToolListAliases,
|
||||
ToolListBackends,
|
||||
ToolListGalleries,
|
||||
ToolListInstalledModels,
|
||||
@@ -165,6 +168,8 @@ var _ = Describe("Tool dispatch", func() {
|
||||
{ToolReloadModels, struct{}{}, "ReloadModels"},
|
||||
{ToolToggleModelState, map[string]any{"name": "foo", "action": "enable"}, "ToggleModelState"},
|
||||
{ToolToggleModelPinned, map[string]any{"name": "foo", "action": "pin"}, "ToggleModelPinned"},
|
||||
{ToolSetAlias, map[string]any{"name": "gpt-4", "target": "real"}, "SetAlias"},
|
||||
{ToolListAliases, struct{}{}, "ListAliases"},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
||||
@@ -36,6 +36,11 @@ const (
|
||||
ToolToggleModelState = "toggle_model_state"
|
||||
ToolToggleModelPinned = "toggle_model_pinned"
|
||||
ToolSetBranding = "set_branding"
|
||||
ToolSetAlias = "set_alias"
|
||||
|
||||
// ToolListAliases is read-only but lives here so the alias tools stay
|
||||
// grouped; the catalog tests assert its read-only placement.
|
||||
ToolListAliases = "list_aliases"
|
||||
)
|
||||
|
||||
// DefaultServerName is the MCP Implementation.Name surfaced when
|
||||
|
||||
48
pkg/mcp/localaitools/tools_aliases.go
Normal file
48
pkg/mcp/localaitools/tools_aliases.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package localaitools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// registerAliasTools wires the conversational alias-management tools. An
|
||||
// alias redirects all traffic for one model name to another configured
|
||||
// model; list_aliases enumerates them, set_alias creates or swaps the
|
||||
// target. Deletion reuses the existing delete_model tool, which works on
|
||||
// any config including an alias.
|
||||
func registerAliasTools(s *mcp.Server, client LocalAIClient, opts Options) {
|
||||
mcp.AddTool(s, &mcp.Tool{
|
||||
Name: ToolListAliases,
|
||||
Description: "List every configured model alias and the target model it routes to.",
|
||||
}, func(ctx context.Context, _ *mcp.CallToolRequest, _ struct{}) (*mcp.CallToolResult, any, error) {
|
||||
aliases, err := client.ListAliases(ctx)
|
||||
if err != nil {
|
||||
return errorResult(err), nil, nil
|
||||
}
|
||||
return jsonResult(aliases), nil, nil
|
||||
})
|
||||
|
||||
if opts.DisableMutating {
|
||||
return
|
||||
}
|
||||
|
||||
mcp.AddTool(s, &mcp.Tool{
|
||||
Name: ToolSetAlias,
|
||||
Description: "Create a model alias (name -> target) or swap an existing alias's target. The target must be an existing, non-alias, enabled model. Requires user confirmation per safety rule 1.",
|
||||
}, func(ctx context.Context, _ *mcp.CallToolRequest, args struct {
|
||||
Name string `json:"name" jsonschema:"The alias name clients will call."`
|
||||
Target string `json:"target" jsonschema:"The existing model the alias routes to."`
|
||||
}) (*mcp.CallToolResult, any, error) {
|
||||
if args.Name == "" {
|
||||
return errorResultf("name is required"), nil, nil
|
||||
}
|
||||
if args.Target == "" {
|
||||
return errorResultf("target is required"), nil, nil
|
||||
}
|
||||
if err := client.SetAlias(ctx, args.Name, args.Target); err != nil {
|
||||
return errorResult(err), nil, nil
|
||||
}
|
||||
return jsonResult(AliasInfo{Name: args.Name, Target: args.Target}), nil, nil
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user