Initial commit: omnichannel-mcp — MCP-сервер управления платформой Omnichannel
ci / test (push) Successful in 2m22s
ci / lint (push) Successful in 4m31s

This commit is contained in:
Maksim Totmin
2026-10-07 20:13:23 +07:00
commit fdbd4a4fb6
54 changed files with 5672 additions and 0 deletions
+338
View File
@@ -0,0 +1,338 @@
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)
})
})
}