339 lines
13 KiB
Go
339 lines
13 KiB
Go
package tools
|
||
|
||
import (
|
||
"context"
|
||
|
||
"omnichannel-configserver-mcp/internal/configserver"
|
||
|
||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||
)
|
||
|
||
// observe.go — read-only инструменты: наблюдение, диагностика, аудит.
|
||
|
||
// observeCall — общий каркас read-инструмента: открыть API, вызвать домен,
|
||
// вернуть результат. Убирает повторяющуюся обвязку из хендлеров.
|
||
func observeCall(ctx context.Context, req *mcp.CallToolRequest, call func(*configserver.API) (any, error)) (*mcp.CallToolResult, error) {
|
||
args := requestArgs(req)
|
||
api, res := openAPI(ctx, args, false)
|
||
if res != nil {
|
||
return res, nil
|
||
}
|
||
defer api.Close()
|
||
v, err := call(api)
|
||
return result(v, err)
|
||
}
|
||
|
||
func registerObserveTools(s *mcp.Server) {
|
||
registerHealthTools(s)
|
||
registerApplicationTools(s)
|
||
registerConfigReadTools(s)
|
||
registerTaskTools(s)
|
||
registerDeploymentReadTools(s)
|
||
registerSettingsTools(s)
|
||
}
|
||
|
||
func registerHealthTools(s *mcp.Server) {
|
||
addTool(s, &mcp.Tool{
|
||
Name: "server_health",
|
||
Description: "Живость config_server (GET /health, без авторизации). Read-only. Помогает отличить проблему стенда от проблемы авторизации.",
|
||
InputSchema: schema(map[string]any{"server": serverProp()}, nil),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
status, body, err := api.Health(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return map[string]any{"server": api.Alias(), "http_status": status, "body": body}, nil
|
||
})
|
||
})
|
||
|
||
addTool(s, &mcp.Tool{
|
||
Name: "whoami",
|
||
Description: "Проверить авторизацию: кто залогинен на config_server. Read-only.",
|
||
InputSchema: schema(map[string]any{"server": serverProp()}, nil),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.WhoAmI(ctx)
|
||
})
|
||
})
|
||
|
||
addTool(s, &mcp.Tool{
|
||
Name: "server_info",
|
||
Description: "Сведения о сервере и версии API (capability-probe): алиас, адрес, режим read_only, доступные JSON-эндпоинты. Read-only. Вызывай первым при неясностях.",
|
||
InputSchema: schema(map[string]any{"server": serverProp()}, nil),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
caps, err := api.Probe(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return map[string]any{
|
||
"server": api.Alias(),
|
||
"base_url": api.Srv.BaseURL,
|
||
"read_only": api.Config.ReadOnly,
|
||
"readonly_creds": api.Srv.ReadonlyUsername != "",
|
||
"capabilities": caps,
|
||
}, nil
|
||
})
|
||
})
|
||
}
|
||
|
||
func registerApplicationTools(s *mcp.Server) {
|
||
addTool(s, &mcp.Tool{
|
||
Name: "list_applications",
|
||
Description: "Список сервисов с хостом и статусом. Read-only.",
|
||
InputSchema: schema(map[string]any{
|
||
"server": serverProp(),
|
||
"include_presence": boolProps("Включить служебные записи хостов (__agent_presence__)", false),
|
||
}, nil),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
args := requestArgs(req)
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.ListApplications(ctx, getBool(args, "include_presence", false))
|
||
})
|
||
})
|
||
|
||
addTool(s, &mcp.Tool{
|
||
Name: "get_application",
|
||
Description: "Детализация сервиса: статус, текущий compose, версии compose, env-файлы и их содержимое, журнал миграций. Read-only.",
|
||
InputSchema: schema(map[string]any{
|
||
"server": serverProp(),
|
||
"app_id": intProps("Идентификатор сервиса", true),
|
||
}, []string{"app_id"}),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
args := requestArgs(req)
|
||
id, res := appIDArg(args)
|
||
if res != nil {
|
||
return res, nil
|
||
}
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.GetApplication(ctx, id)
|
||
})
|
||
})
|
||
}
|
||
|
||
func registerConfigReadTools(s *mcp.Server) {
|
||
addTool(s, &mcp.Tool{
|
||
Name: "get_config",
|
||
Description: "Текущие compose и env сервиса из БД. Read-only.",
|
||
InputSchema: schema(map[string]any{
|
||
"server": serverProp(),
|
||
"app_id": intProps("Идентификатор сервиса", true),
|
||
}, []string{"app_id"}),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
args := requestArgs(req)
|
||
id, res := appIDArg(args)
|
||
if res != nil {
|
||
return res, nil
|
||
}
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.GetConfig(ctx, id)
|
||
})
|
||
})
|
||
|
||
addTool(s, &mcp.Tool{
|
||
Name: "env_files",
|
||
Description: "Список текущих env-файлов сервиса. Read-only.",
|
||
InputSchema: schema(map[string]any{
|
||
"server": serverProp(),
|
||
"app_id": intProps("Идентификатор сервиса", true),
|
||
}, []string{"app_id"}),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
args := requestArgs(req)
|
||
id, res := appIDArg(args)
|
||
if res != nil {
|
||
return res, nil
|
||
}
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.EnvFiles(ctx, id)
|
||
})
|
||
})
|
||
|
||
addTool(s, &mcp.Tool{
|
||
Name: "compose_version",
|
||
Description: "Содержимое конкретной версии compose по version_id. Read-only.",
|
||
InputSchema: schema(map[string]any{
|
||
"server": serverProp(),
|
||
"version_id": intProps("Идентификатор версии compose", true),
|
||
}, []string{"version_id"}),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
args := requestArgs(req)
|
||
id, res := versionIDArg(args)
|
||
if res != nil {
|
||
return res, nil
|
||
}
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.ComposeVersion(ctx, id)
|
||
})
|
||
})
|
||
|
||
addTool(s, &mcp.Tool{
|
||
Name: "env_version",
|
||
Description: "Содержимое конкретной версии env-файла по version_id. Read-only.",
|
||
InputSchema: schema(map[string]any{
|
||
"server": serverProp(),
|
||
"version_id": intProps("Идентификатор версии env", true),
|
||
}, []string{"version_id"}),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
args := requestArgs(req)
|
||
id, res := versionIDArg(args)
|
||
if res != nil {
|
||
return res, nil
|
||
}
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.EnvVersion(ctx, id)
|
||
})
|
||
})
|
||
|
||
addTool(s, &mcp.Tool{
|
||
Name: "env_versions",
|
||
Description: "Все версии конкретного env-файла сервиса. Read-only.",
|
||
InputSchema: schema(map[string]any{
|
||
"server": serverProp(),
|
||
"app_id": intProps("Идентификатор сервиса", true),
|
||
"filename": strProps("Имя env-файла, например .env", true),
|
||
}, []string{"app_id", "filename"}),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
args := requestArgs(req)
|
||
id, res := appIDArg(args)
|
||
if res != nil {
|
||
return res, nil
|
||
}
|
||
filename, err := requireString(args, "filename")
|
||
if err != nil {
|
||
return errorResult(err.Error()), nil
|
||
}
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.EnvVersions(ctx, id, filename)
|
||
})
|
||
})
|
||
}
|
||
|
||
func registerTaskTools(s *mcp.Server) {
|
||
addTool(s, &mcp.Tool{
|
||
Name: "list_tasks",
|
||
Description: "Список задач агентов с фильтрами (аудит, поиск failed/pending). Read-only.",
|
||
InputSchema: schema(map[string]any{
|
||
"server": serverProp(),
|
||
"host_ip": strProps("Фильтр по IP хоста", false),
|
||
"status": strProps("Фильтр по статусу", false, "pending", "in_progress", "completed", "failed"),
|
||
"task_type": strProps("Фильтр по типу задачи (deploy, restart, down, migration, update_config, release_sync, release_start, update_front)", false),
|
||
"limit": intProps("Максимум записей (1..500, по умолчанию 100)", false),
|
||
}, nil),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
args := requestArgs(req)
|
||
filter := configserver.TaskFilter{
|
||
HostIP: getString(args, "host_ip", ""),
|
||
Status: getString(args, "status", ""),
|
||
TaskType: getString(args, "task_type", ""),
|
||
Limit: getInt(args, "limit", 100),
|
||
}
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.ListTasks(ctx, filter)
|
||
})
|
||
})
|
||
|
||
addTool(s, &mcp.Tool{
|
||
Name: "get_task",
|
||
Description: "Статус и вывод задачи. wait=true — дождаться завершения (ограничено task_poll_max_sec); иначе вернуть текущее состояние. Read-only.",
|
||
InputSchema: schema(map[string]any{
|
||
"server": serverProp(),
|
||
"task_id": strProps("UUID задачи", true),
|
||
"wait": boolProps("Дождаться завершения (по умолчанию false)", false),
|
||
"tail": boolProps("Оставить хвост вывода при обрезке (по умолчанию true — ошибки обычно в конце)", false),
|
||
"output_bytes": intProps("Лимит вывода в байтах (по умолчанию — max_output_bytes сервера)", false),
|
||
}, []string{"task_id"}),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
args := requestArgs(req)
|
||
taskID, err := requireString(args, "task_id")
|
||
if err != nil {
|
||
return errorResult(err.Error()), nil
|
||
}
|
||
wait := getBool(args, "wait", false)
|
||
tail := getBool(args, "tail", true)
|
||
limit := getInt(args, "output_bytes", 0)
|
||
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
var task *configserver.Task
|
||
if wait {
|
||
task, err = api.WaitTask(ctx, taskID)
|
||
} else {
|
||
task, err = api.GetTask(ctx, taskID)
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if limit <= 0 {
|
||
limit = api.Srv.MaxOutputBytes
|
||
}
|
||
out := *task
|
||
out.Output, _ = truncateText(out.Output, limit, tail)
|
||
return out, nil
|
||
})
|
||
})
|
||
}
|
||
|
||
func registerDeploymentReadTools(s *mcp.Server) {
|
||
addTool(s, &mcp.Tool{
|
||
Name: "get_deployment_files",
|
||
Description: "Файлы развёртывания: schema, manifest, prebuild_vars, ip_overrides. Read-only.",
|
||
InputSchema: schema(map[string]any{"server": serverProp()}, nil),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.DeploymentFiles(ctx)
|
||
})
|
||
})
|
||
|
||
addTool(s, &mcp.Tool{
|
||
Name: "ip_match",
|
||
Description: "Диагностика сопоставления IP из schema.json с IP агентов в БД. Read-only.",
|
||
InputSchema: schema(map[string]any{"server": serverProp()}, nil),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.IPMatch(ctx)
|
||
})
|
||
})
|
||
|
||
addTool(s, &mcp.Tool{
|
||
Name: "release_job",
|
||
Description: "Статус фоновой загрузки релиза (status: idle/running/completed/failed, фаза, прогресс образов). Read-only.",
|
||
InputSchema: schema(map[string]any{"server": serverProp()}, nil),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.ReleaseJob(ctx)
|
||
})
|
||
})
|
||
|
||
addTool(s, &mcp.Tool{
|
||
Name: "deployment_tasks",
|
||
Description: "Последние release-задачи (sync/start). Read-only.",
|
||
InputSchema: schema(map[string]any{"server": serverProp()}, nil),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.DeploymentTasks(ctx)
|
||
})
|
||
})
|
||
}
|
||
|
||
func registerSettingsTools(s *mcp.Server) {
|
||
addTool(s, &mcp.Tool{
|
||
Name: "stats",
|
||
Description: "Статистика: количество версий compose/env и логов миграций. Read-only.",
|
||
InputSchema: schema(map[string]any{"server": serverProp()}, nil),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.Stats(ctx)
|
||
})
|
||
})
|
||
|
||
addTool(s, &mcp.Tool{
|
||
Name: "service_map",
|
||
Description: "Карта хостов и сервисов. Read-only.",
|
||
InputSchema: schema(map[string]any{"server": serverProp()}, nil),
|
||
}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||
return api.ServiceMap(ctx)
|
||
})
|
||
})
|
||
}
|