Files
omnichannel-configserver-mcp/internal/tools/observe.go
T
2026-10-07 20:13:23 +07:00

339 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
})
})
}