256 lines
10 KiB
Go
256 lines
10 KiB
Go
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)
|
|
})
|
|
})
|
|
}
|