package tools import ( "context" "fmt" "forge-tools-lexicomai/internal/lexicomai" "github.com/modelcontextprotocol/go-sdk/mcp" ) // Общие помощники обработчиков: нормализация аргументов product/alias и // превращение доменного значения в результат инструмента. // productArg читает обязательный аргумент product и нормализует его // (нижний регистр + проверка, что продукт поддерживается). func productArg(args map[string]any) (string, error) { p, err := requireString(args, "product") if err != nil { return "", err } return lexicomai.NormalizeProduct(p) } // targetArg резолвит машину по alias (пустой alias — «по умолчанию»). func targetArg(m *lexicomai.Manager, args map[string]any) (*lexicomai.Target, error) { alias := getString(args, "alias", "") return m.Target(alias) } func (r *registrar) run(ctx context.Context, req *mcp.CallToolRequest, fn func(*lexicomai.Target) (string, error)) (*mcp.CallToolResult, error) { args := requestArgs(req) t, err := targetArg(r.m, args) if err != nil { return errorResult(err.Error()), nil } out, err := fn(t) if err != nil { return errorResult(err.Error()), nil // доменная ошибка → модели (recoverable) } return textResult(out), nil } // registerInventory — инвентарь и статус (products_list, service_status, // repo_catalog, base_status). func registerInventory(r *registrar) { r.add("products_list", "Инвентарь продуктов по всем машинам конфига: продукт → машина, состояние службы, порт, движок, лицензия, версия. Read-only.", schema(map[string]any{}, nil), func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { out, err := r.m.Inventory(ctx) if err != nil { return errorResult(err.Error()), nil } return textResult(out), nil }) r.add("service_status", "Краткий статус продукта на машине: unit, порт, движок, venv, состояние, лицензия, версия. Read-only.", schema(map[string]any{ "product": strProps("Продукт компании (например ups, spr, cls)", true, lexicomai.DefaultProducts()...), "alias": strProps("Машина (target alias); пусто = по умолчанию", false), }, []string{"product"}), func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := requestArgs(req) p, err := productArg(args) if err != nil { return errorResult(err.Error()), nil } return r.run(ctx, req, func(t *lexicomai.Target) (string, error) { return r.m.Status(ctx, t, p) }) }) r.add("repo_catalog", "Список версий продукта в репозитории (ветки release/beta). Read-only.", schema(map[string]any{ "product": strProps("Продукт компании", true, lexicomai.DefaultProducts()...), }, []string{"product"}), func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := requestArgs(req) p, err := productArg(args) if err != nil { return errorResult(err.Error()), nil } out, err := r.m.RepoCatalog(ctx, p) if err != nil { return errorResult(err.Error()), nil } return textResult(out), nil }) r.add("base_status", "Полнота минимальной базы /opt/pve на машине (.version, bin). Read-only.", schema(map[string]any{ "alias": strProps("Машина (target alias); пусто = по умолчанию", false), }, nil), func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { return r.run(ctx, req, func(t *lexicomai.Target) (string, error) { return r.m.BaseStatus(ctx, t) }) }) } // registerConfig — конфигурация сервиса (service_config_get). func registerConfig(r *registrar) { r.add("service_config_get", "Читает config.json продукта и сообщает top-level ключи (+ наличие .example). Read-only.", schema(map[string]any{ "product": strProps("Продукт компании", true, lexicomai.DefaultProducts()...), "alias": strProps("Машина (target alias); пусто = по умолчанию", false), }, []string{"product"}), func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := requestArgs(req) p, err := productArg(args) if err != nil { return errorResult(err.Error()), nil } return r.run(ctx, req, func(t *lexicomai.Target) (string, error) { return r.m.ConfigGet(ctx, t, p) }) }) } // registerLicense — лицензии (license_get, license_verify). func registerLicense(r *registrar) { r.add("license_get", "Содержимое license.json продукта (service, expire, licenses, key). Read-only.", schema(map[string]any{ "product": strProps("Продукт компании", true, lexicomai.DefaultProducts()...), "alias": strProps("Машина (target alias); пусто = по умолчанию", false), }, []string{"product"}), func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := requestArgs(req) p, err := productArg(args) if err != nil { return errorResult(err.Error()), nil } return r.run(ctx, req, func(t *lexicomai.Target) (string, error) { l, err := r.m.LicenseGet(ctx, t, p) if err != nil { return "", err } return licenseText(l), nil }) }) r.add("license_verify", "Проверка лицензии: поля (service/expire/занятость) + GET /license/check для FastAPI-продуктов. Read-only.", schema(map[string]any{ "product": strProps("Продукт компании", true, lexicomai.DefaultProducts()...), "alias": strProps("Машина (target alias); пусто = по умолчанию", false), }, []string{"product"}), func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := requestArgs(req) p, err := productArg(args) if err != nil { return errorResult(err.Error()), nil } return r.run(ctx, req, func(t *lexicomai.Target) (string, error) { return r.m.LicenseVerify(ctx, t, p) }) }) } // licenseText форматирует лицензию в читаемый вид (ключ — с маской). func licenseText(l *lexicomai.License) string { return "license: service=" + l.Service + " expire=" + l.Expire + " seats=" + itoa(l.Licenses) + " key=" + l.KeyMasked() } func itoa(n int) string { return fmt.Sprintf("%d", n) } // registerHealth — логи, health, диагностика, поиск ошибок. func registerHealth(r *registrar) { r.add("service_logs", "Хвост журнала systemd-юнита продукта (опц. фильтр grep). Read-only.", schema(map[string]any{ "product": strProps("Продукт компании", true, lexicomai.DefaultProducts()...), "alias": strProps("Машина (target alias); пусто = по умолчанию", false), "tail": intProps("Число строк (default 100)", false), "grep": strProps("Подстрока-фильтр (по умолчанию нет)", false), }, []string{"product"}), func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := requestArgs(req) p, err := productArg(args) if err != nil { return errorResult(err.Error()), nil } tail := getInt(args, "tail", 100) grep := getString(args, "grep", "") return r.run(ctx, req, func(t *lexicomai.Target) (string, error) { return r.m.Logs(ctx, t, p, tail, grep) }) }) r.add("service_health", "Живой статус + /openapi.json (жив/версия) + /license/check. Read-only.", schema(map[string]any{ "product": strProps("Продукт компании", true, lexicomai.DefaultProducts()...), "alias": strProps("Машина (target alias); пусто = по умолчанию", false), }, []string{"product"}), func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := requestArgs(req) p, err := productArg(args) if err != nil { return errorResult(err.Error()), nil } return r.run(ctx, req, func(t *lexicomai.Target) (string, error) { return r.m.Health(ctx, t, p) }) }) r.add("service_diagnose", "Чек-лист: юнит, процесс, порт, лицензия, база /opt/pve. Read-only.", schema(map[string]any{ "product": strProps("Продукт компании", true, lexicomai.DefaultProducts()...), "alias": strProps("Машина (target alias); пусто = по умолчанию", false), }, []string{"product"}), func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := requestArgs(req) p, err := productArg(args) if err != nil { return errorResult(err.Error()), nil } return r.run(ctx, req, func(t *lexicomai.Target) (string, error) { return r.m.Diagnose(ctx, t, p) }) }) r.add("errors_search", "Поиск ошибок в журнале продукта за окно (по умолчанию -p err, окно 1h). Read-only.", schema(map[string]any{ "product": strProps("Продукт компании", true, lexicomai.DefaultProducts()...), "alias": strProps("Машина (target alias); пусто = по умолчанию", false), "window": strProps("Окно --since, например 1h/24h (default 1h)", false), "level": strProps("Приоритет -p (default err)", false), "pattern": strProps("Regex-фильтр ошибок (default: error|traceback|exception|failed)", false), }, []string{"product"}), func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) { args := requestArgs(req) p, err := productArg(args) if err != nil { return errorResult(err.Error()), nil } window := getString(args, "window", "1h") level := getString(args, "level", "err") pattern := getString(args, "pattern", "") return r.run(ctx, req, func(t *lexicomai.Target) (string, error) { return r.m.Errors(ctx, t, p, window, level, pattern) }) }) }