package tools import ( "context" "omnichannel-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) }) }) }