Initial commit: omnichannel-mcp — MCP-сервер управления платформой Omnichannel
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// configure.go — изменяющие инструменты конфигурации (compose/env и версии).
|
||||
// Требуют read_only=false и обычно помечаются require_approval в agent.yaml.
|
||||
|
||||
func registerConfigureTools(s *mcp.Server) {
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "set_compose",
|
||||
Description: "Заменить текущий docker-compose сервиса, создать версию и отправить агенту. Изменяет конфигурацию.",
|
||||
InputSchema: schema(map[string]any{
|
||||
"server": serverProp(),
|
||||
"app_id": intProps("Идентификатор сервиса", true),
|
||||
"content": strProps("Полный текст docker-compose (YAML)", true),
|
||||
}, []string{"app_id", "content"}),
|
||||
}, appPatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := requestArgs(req)
|
||||
id, res := appIDArg(args)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
content, err := requireString(args, "content")
|
||||
if err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
api, res := openAPI(ctx, args, true)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
defer api.Close()
|
||||
return result(api.SetCompose(ctx, id, content))
|
||||
})
|
||||
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "set_env",
|
||||
Description: "Заменить текущий env-файл сервиса (например .env), создать версию и отправить агенту. Изменяет конфигурацию.",
|
||||
InputSchema: schema(map[string]any{
|
||||
"server": serverProp(),
|
||||
"app_id": intProps("Идентификатор сервиса", true),
|
||||
"filename": strProps("Имя env-файла, например .env", true),
|
||||
"content": strProps("Полное содержимое env-файла", true),
|
||||
}, []string{"app_id", "filename", "content"}),
|
||||
}, envPatterns, 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
|
||||
}
|
||||
content, err := requireString(args, "content")
|
||||
if err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
api, res := openAPI(ctx, args, true)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
defer api.Close()
|
||||
return result(api.SetEnv(ctx, id, filename, content))
|
||||
})
|
||||
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "restore_compose",
|
||||
Description: "Восстановить версию compose (создаёт новую текущую версию) и отправить агенту. Разрушительно: требует confirm=\"true\".",
|
||||
InputSchema: schema(map[string]any{
|
||||
"server": serverProp(),
|
||||
"version_id": intProps("Идентификатор версии compose", true),
|
||||
"confirm": boolProps("Подтверждение разрушительной операции (обязательно true)", true),
|
||||
}, []string{"version_id", "confirm"}),
|
||||
}, versionPatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := requestArgs(req)
|
||||
if res := confirmGate(args, "restore_compose"); res != nil {
|
||||
return res, nil
|
||||
}
|
||||
id, res := versionIDArg(args)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
api, res := openAPI(ctx, args, true)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
defer api.Close()
|
||||
return result(api.RestoreCompose(ctx, id))
|
||||
})
|
||||
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "restore_env_version",
|
||||
Description: "Восстановить версию env-файла (создаёт новую текущую версию) и отправить агенту. Разрушительно: требует confirm=\"true\".",
|
||||
InputSchema: schema(map[string]any{
|
||||
"server": serverProp(),
|
||||
"version_id": intProps("Идентификатор версии env", true),
|
||||
"confirm": boolProps("Подтверждение разрушительной операции (обязательно true)", true),
|
||||
}, []string{"version_id", "confirm"}),
|
||||
}, versionPatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := requestArgs(req)
|
||||
if res := confirmGate(args, "restore_env_version"); res != nil {
|
||||
return res, nil
|
||||
}
|
||||
id, res := versionIDArg(args)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
api, res := openAPI(ctx, args, true)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
defer api.Close()
|
||||
return result(api.RestoreEnvVersion(ctx, id))
|
||||
})
|
||||
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "update_config",
|
||||
Description: "Повторно отправить агенту текущую конфигурацию сервиса из БД (compose + env). Изменяет файлы на хосте.",
|
||||
InputSchema: schema(map[string]any{
|
||||
"server": serverProp(),
|
||||
"app_id": intProps("Идентификатор сервиса", true),
|
||||
}, []string{"app_id"}),
|
||||
}, appPatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := requestArgs(req)
|
||||
id, res := appIDArg(args)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
api, res := openAPI(ctx, args, true)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
defer api.Close()
|
||||
return result(api.UpdateConfig(ctx, id))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// maintain.go — обслуживание: очистка старых версий.
|
||||
|
||||
func registerMaintainTools(s *mcp.Server) {
|
||||
// cleanup_old_versions не эмитит паттерн по конкретному ресурсу: операция
|
||||
// затрагивает всю БД версий, поэтому используем release-паттерн стенда.
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "cleanup_old_versions",
|
||||
Description: "Удалить версии compose/env и логи миграций старше N дней (текущие версии не трогаются). Разрушительно: требует confirm=\"true\".",
|
||||
InputSchema: schema(map[string]any{
|
||||
"server": serverProp(),
|
||||
"days": intProps("Порог в днях (по умолчанию 90)", false),
|
||||
"confirm": boolProps("Подтверждение разрушительной операции (обязательно true)", true),
|
||||
}, []string{"confirm"}),
|
||||
}, releasePatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := requestArgs(req)
|
||||
if res := confirmGate(args, "cleanup_old_versions"); res != nil {
|
||||
return res, nil
|
||||
}
|
||||
days := getInt(args, "days", 90)
|
||||
if days <= 0 {
|
||||
return errorResult("days должен быть положительным"), nil
|
||||
}
|
||||
api, res := openAPI(ctx, args, true)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
defer api.Close()
|
||||
return result(api.CleanupOldVersions(ctx, days))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"omnichannel-mcp/internal/configserver"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// operate.go — жизненный цикл сервиса (deploy/restart/down/migrate) и загрузка
|
||||
// фронта. Мутации; обычно require_approval в agent.yaml.
|
||||
|
||||
func registerOperateTools(s *mcp.Server) {
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "deploy",
|
||||
Description: "Поднять сервис (docker-compose up -d). Создаёт задачу агенту; wait=true — дождаться результата.",
|
||||
InputSchema: lifecycleSchema(),
|
||||
}, appPatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return runLifecycle(ctx, req, "deploy", false)
|
||||
})
|
||||
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "restart",
|
||||
Description: "Перезапустить сервис (down + up -d). Создаёт задачу агенту; wait=true — дождаться результата.",
|
||||
InputSchema: lifecycleSchema(),
|
||||
}, appPatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return runLifecycle(ctx, req, "restart", false)
|
||||
})
|
||||
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "migrate",
|
||||
Description: "Запустить миграции сервиса (docker-compose run migration). Создаёт задачу агенту.",
|
||||
InputSchema: lifecycleSchema(),
|
||||
}, appPatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return runLifecycle(ctx, req, "migrate", false)
|
||||
})
|
||||
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "down",
|
||||
Description: "Остановить сервис (docker-compose down). Разрушительно: требует confirm=\"true\".",
|
||||
InputSchema: schema(map[string]any{
|
||||
"server": serverProp(),
|
||||
"app_id": intProps("Идентификатор сервиса", true),
|
||||
"wait": boolProps("Дождаться завершения (по умолчанию false)", false),
|
||||
"confirm": boolProps("Подтверждение разрушительной операции (обязательно true)", true),
|
||||
}, []string{"app_id", "confirm"}),
|
||||
}, appPatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return runLifecycle(ctx, req, "down", true)
|
||||
})
|
||||
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "update_front",
|
||||
Description: "Загрузить локальную сборку фронта на хост сервиса (полная замена build_folder). Разрушительно: требует confirm=\"true\". Каталог — внутри front_roots.",
|
||||
InputSchema: schema(map[string]any{
|
||||
"server": serverProp(),
|
||||
"app_id": intProps("Идентификатор фронт-сервиса", true),
|
||||
"build_dir": strProps("Локальный каталог сборки (абсолютный путь внутри front_roots)", true),
|
||||
"confirm": boolProps("Подтверждение разрушительной операции (обязательно true)", true),
|
||||
}, []string{"app_id", "build_dir", "confirm"}),
|
||||
}, appPatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := requestArgs(req)
|
||||
if res := confirmGate(args, "update_front"); res != nil {
|
||||
return res, nil
|
||||
}
|
||||
id, res := appIDArg(args)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
buildDir, err := requireString(args, "build_dir")
|
||||
if err != nil {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
api, res := openAPI(ctx, args, true)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
defer api.Close()
|
||||
return result(api.UpdateFront(ctx, id, buildDir))
|
||||
})
|
||||
}
|
||||
|
||||
// lifecycleSchema — общая схема deploy/restart/migrate/down.
|
||||
func lifecycleSchema() map[string]any {
|
||||
return schema(map[string]any{
|
||||
"server": serverProp(),
|
||||
"app_id": intProps("Идентификатор сервиса", true),
|
||||
"wait": boolProps("Дождаться завершения задачи (по умолчанию false)", false),
|
||||
}, []string{"app_id"})
|
||||
}
|
||||
|
||||
// runLifecycle — общий путь операций жизненного цикла: вызвать действие, при
|
||||
// wait=true ограниченно дождаться задачи агента.
|
||||
func runLifecycle(ctx context.Context, req *mcp.CallToolRequest, action string, needConfirm bool) (*mcp.CallToolResult, error) {
|
||||
args := requestArgs(req)
|
||||
if needConfirm {
|
||||
if res := confirmGate(args, action); res != nil {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
id, res := appIDArg(args)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
wait := getBool(args, "wait", false)
|
||||
|
||||
api, res := openAPI(ctx, args, true)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
defer api.Close()
|
||||
|
||||
var (
|
||||
ack *configserver.TaskAck
|
||||
err error
|
||||
)
|
||||
switch action {
|
||||
case "deploy":
|
||||
ack, err = api.Deploy(ctx, id)
|
||||
case "restart":
|
||||
ack, err = api.Restart(ctx, id)
|
||||
case "down":
|
||||
ack, err = api.Down(ctx, id)
|
||||
case "migrate":
|
||||
ack, err = api.Migrate(ctx, id)
|
||||
}
|
||||
if err != nil {
|
||||
return result(nil, err)
|
||||
}
|
||||
|
||||
// Без wait отдаём task_id, чтобы не блокировать последовательный loop агента.
|
||||
if !wait {
|
||||
return result(ack, nil)
|
||||
}
|
||||
task, err := api.WaitTask(ctx, ack.TaskID)
|
||||
if err != nil {
|
||||
return result(nil, err)
|
||||
}
|
||||
return result(map[string]any{"ack": ack, "task": task}, nil)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"omnichannel-mcp/internal/configserver"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// overview.go — сводные read-only инструменты для эксплуатации. Они не
|
||||
// изменяют состояние, а собирают картину несколькими GET-вызовами (best-effort:
|
||||
// сбой одного источника не рушит остальные, а попадает в поле errors).
|
||||
|
||||
func registerOverviewTools(s *mcp.Server) {
|
||||
addTool(s, &mcp.Tool{
|
||||
Name: "overview",
|
||||
Description: "Сводка состояния стенда одним вызовом: health, статистика, карта сервисов, приложения не в OK, последние failed-задачи. Read-only.",
|
||||
InputSchema: schema(map[string]any{"server": serverProp()}, nil),
|
||||
}, handleOverview)
|
||||
|
||||
addTool(s, &mcp.Tool{
|
||||
Name: "release_status",
|
||||
Description: "Состояние развёртывания релиза: release_job, release-задачи, приложения, сопоставление IP. Read-only.",
|
||||
InputSchema: schema(map[string]any{"server": serverProp()}, nil),
|
||||
}, handleReleaseStatus)
|
||||
}
|
||||
|
||||
func handleOverview(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||||
out := map[string]any{"server": api.Alias(), "read_only": api.Config.ReadOnly}
|
||||
var errs []string
|
||||
|
||||
if status, body, err := api.Health(ctx); err != nil {
|
||||
errs = append(errs, "health: "+err.Error())
|
||||
} else {
|
||||
out["health"] = map[string]any{"http_status": status, "body": body}
|
||||
}
|
||||
if st, err := api.Stats(ctx); err != nil {
|
||||
errs = append(errs, "stats: "+err.Error())
|
||||
} else {
|
||||
out["stats"] = st
|
||||
}
|
||||
if sm, err := api.ServiceMap(ctx); err != nil {
|
||||
errs = append(errs, "service_map: "+err.Error())
|
||||
} else {
|
||||
out["service_map"] = sm
|
||||
}
|
||||
if apps, err := api.ListApplications(ctx, false); err != nil {
|
||||
errs = append(errs, "applications: "+err.Error())
|
||||
} else {
|
||||
out["unhealthy_applications"] = unhealthy(apps)
|
||||
}
|
||||
if tasks, err := api.ListTasks(ctx, configserver.TaskFilter{Status: "failed", Limit: 10}); err != nil {
|
||||
errs = append(errs, "failed_tasks: "+err.Error())
|
||||
} else {
|
||||
out["recent_failed_tasks"] = tasks
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
out["errors"] = errs
|
||||
}
|
||||
return out, nil
|
||||
})
|
||||
}
|
||||
|
||||
func handleReleaseStatus(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
return observeCall(ctx, req, func(api *configserver.API) (any, error) {
|
||||
out := map[string]any{"server": api.Alias()}
|
||||
var errs []string
|
||||
|
||||
if job, err := api.ReleaseJob(ctx); err != nil {
|
||||
errs = append(errs, "release_job: "+err.Error())
|
||||
} else {
|
||||
out["release_job"] = job
|
||||
}
|
||||
if tasks, err := api.DeploymentTasks(ctx); err != nil {
|
||||
errs = append(errs, "deployment_tasks: "+err.Error())
|
||||
} else {
|
||||
out["deployment_tasks"] = tasks
|
||||
}
|
||||
if apps, err := api.ListApplications(ctx, false); err != nil {
|
||||
errs = append(errs, "applications: "+err.Error())
|
||||
} else {
|
||||
out["applications"] = apps
|
||||
}
|
||||
if ipm, err := api.IPMatch(ctx); err != nil {
|
||||
errs = append(errs, "ip_match: "+err.Error())
|
||||
} else {
|
||||
out["ip_match"] = ipm
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
out["errors"] = errs
|
||||
}
|
||||
return out, nil
|
||||
})
|
||||
}
|
||||
|
||||
// unhealthy возвращает приложения со статусом, отличным от OK.
|
||||
func unhealthy(apps []configserver.Application) []configserver.Application {
|
||||
out := []configserver.Application{}
|
||||
for _, app := range apps {
|
||||
if app.Status != "OK" {
|
||||
out = append(out, app)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// patterns.go — доменные эмиттеры probe-паттернов разрешений для omnichannel-mcp.
|
||||
//
|
||||
// Канонический паттерн несёт алиас сервера и ресурс, чтобы мультисерверная
|
||||
// политика оператора могла различать стенды:
|
||||
//
|
||||
// server=<alias> app=<id>
|
||||
// server=<alias> app=<id> file=<filename>
|
||||
// server=<alias> version=<id>
|
||||
// server=<alias> release
|
||||
//
|
||||
// «always» не эмитим: каждая разрушительная операция должна получать отдельный
|
||||
// ask/allow у оператора (как в proxmox-модуле).
|
||||
|
||||
// effectiveServer возвращает алиас сервера для паттерна: явный аргумент либо
|
||||
// default из конфига. Без конфига — "default".
|
||||
func effectiveServer(args map[string]any) string {
|
||||
if s := getString(args, "server", ""); s != "" {
|
||||
return s
|
||||
}
|
||||
if m := manager(); m != nil {
|
||||
if cfg, _, err := m.Config(configPathArg(args)); err == nil && cfg.Default != "" {
|
||||
return cfg.Default
|
||||
}
|
||||
}
|
||||
return "default"
|
||||
}
|
||||
|
||||
func serverPart(args map[string]any) string { return "server=" + effectiveServer(args) }
|
||||
|
||||
// appPatterns — паттерн ресурса-приложения (deploy/restart/down/migrate/...).
|
||||
func appPatterns(args map[string]any) ([]string, []string) {
|
||||
parts := []string{serverPart(args)}
|
||||
if id := getInt(args, "app_id", 0); id > 0 {
|
||||
parts = append(parts, "app="+strconv.Itoa(id))
|
||||
}
|
||||
return []string{strings.Join(parts, " ")}, nil
|
||||
}
|
||||
|
||||
// envPatterns — паттерн env-файла (set_env/restore_env_version).
|
||||
func envPatterns(args map[string]any) ([]string, []string) {
|
||||
parts := []string{serverPart(args)}
|
||||
if id := getInt(args, "app_id", 0); id > 0 {
|
||||
parts = append(parts, "app="+strconv.Itoa(id))
|
||||
}
|
||||
if fn := getString(args, "filename", ""); fn != "" {
|
||||
parts = append(parts, "file="+fn)
|
||||
}
|
||||
return []string{strings.Join(parts, " ")}, nil
|
||||
}
|
||||
|
||||
// versionPatterns — паттерн версии конфигурации (restore_compose).
|
||||
func versionPatterns(args map[string]any) ([]string, []string) {
|
||||
parts := []string{serverPart(args)}
|
||||
if id := getInt(args, "version_id", 0); id > 0 {
|
||||
parts = append(parts, "version="+strconv.Itoa(id))
|
||||
}
|
||||
return []string{strings.Join(parts, " ")}, nil
|
||||
}
|
||||
|
||||
// releasePatterns — паттерн операций релиза/развёртывания.
|
||||
func releasePatterns(args map[string]any) ([]string, []string) {
|
||||
return []string{serverPart(args) + " release"}, nil
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Package tools реализует MCP-инструменты модуля omnichannel-mcp поверх
|
||||
// доменного пакета configserver. Слой тонкий: разобрать аргументы, вызвать
|
||||
// домен, вернуть текст/ошибку. Бизнес-логики здесь нет.
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"omnichannel-mcp/internal/configserver"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// mgr — одиночный Manager процесса (см. ARCHITECTURE.md §2). Устанавливается
|
||||
// один раз в RegisterAll; Manager потокобезопасен.
|
||||
var mgr *configserver.Manager
|
||||
|
||||
func manager() *configserver.Manager { return mgr }
|
||||
|
||||
// RegisterAll — единая точка регистрации инструментов, сгруппированных по
|
||||
// фазам жизненного цикла: observe (чтение) → configure → operate → release →
|
||||
// maintain. Имена инструментов — без префикса (префикс может добавить MCP-хост).
|
||||
func RegisterAll(s *mcp.Server, m *configserver.Manager) {
|
||||
mgr = m
|
||||
registerObserveTools(s)
|
||||
registerConfigureTools(s)
|
||||
registerOperateTools(s)
|
||||
registerReleaseTools(s)
|
||||
registerMaintainTools(s)
|
||||
registerOverviewTools(s)
|
||||
}
|
||||
|
||||
// openAPI резолвит конфиг/сервер и открывает фасад API. Любая ошибка на этом
|
||||
// шаге (нет конфига, неизвестный алиас, read_only) — доменная: отдаём модели.
|
||||
func openAPI(ctx context.Context, args map[string]any, write bool) (*configserver.API, *mcp.CallToolResult) {
|
||||
m := manager()
|
||||
if m == nil {
|
||||
return nil, errorResult("omnichannel-mcp: manager не инициализирован")
|
||||
}
|
||||
alias := getString(args, "server", "")
|
||||
api, err := m.Open(ctx, configPathArg(args), alias, write)
|
||||
if err != nil {
|
||||
return nil, errorResult(err.Error())
|
||||
}
|
||||
return api, nil
|
||||
}
|
||||
|
||||
// configPathArg возвращает опциональный путь к конфигу из аргументов вызова.
|
||||
// Основной способ задания конфига — флаг `-config`; per-call переопределение
|
||||
// (`config_path`) поддержано для многотенантных обёрток. `_tenant_config` —
|
||||
// устаревший алиас, принимается для совместимости.
|
||||
func configPathArg(args map[string]any) string {
|
||||
if v := getString(args, "config_path", ""); v != "" {
|
||||
return v
|
||||
}
|
||||
return getString(args, "_tenant_config", "")
|
||||
}
|
||||
|
||||
// result приводит (значение, ошибка) домена к результату инструмента:
|
||||
// доменные ошибки — recoverable (текст видит модель), инфраструктурные — Go-ошибка.
|
||||
func result(v any, err error) (*mcp.CallToolResult, error) {
|
||||
if err != nil {
|
||||
if configserver.IsDomainError(err) {
|
||||
return errorResult(err.Error()), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return textResult(jsonText(v)), nil
|
||||
}
|
||||
|
||||
// confirmGate требует confirm="true" для разрушительных операций. Это второй
|
||||
// слой поверх require_approval в agent.yaml.
|
||||
func confirmGate(args map[string]any, what string) *mcp.CallToolResult {
|
||||
if !getBool(args, "confirm", false) {
|
||||
return errorResult("операция \"" + what + "\" требует confirm=\"true\"")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// serverProp — общее описание аргумента выбора сервера.
|
||||
func serverProp() map[string]any {
|
||||
return strProps("Алиас сервера из config.json (по умолчанию — default)", false)
|
||||
}
|
||||
|
||||
// appIDArg читает обязательный app_id.
|
||||
func appIDArg(args map[string]any) (int, *mcp.CallToolResult) {
|
||||
id := getInt(args, "app_id", 0)
|
||||
if id <= 0 {
|
||||
return 0, errorResult("нужен положительный app_id")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// versionIDArg читает обязательный version_id.
|
||||
func versionIDArg(args map[string]any) (int, *mcp.CallToolResult) {
|
||||
id := getInt(args, "version_id", 0)
|
||||
if id <= 0 {
|
||||
return 0, errorResult("нужен положительный version_id")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"omnichannel-mcp/internal/configserver"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// release.go — страница «Развёртывание»: файлы схемы, seed/fill, релиз.
|
||||
|
||||
func registerReleaseTools(s *mcp.Server) {
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "save_deployment_files",
|
||||
Description: "Сохранить файлы развёртывания (schema/manifest/prebuild_vars/ip_overrides). Передавай только изменяемые поля.",
|
||||
InputSchema: schema(map[string]any{
|
||||
"server": serverProp(),
|
||||
"schema": strProps("JSON-схема развёртывания (опционально)", false),
|
||||
"manifest": strProps("JSON-манифест (опционально)", false),
|
||||
"prebuild_vars": strProps("Строка prebuild_vars (опционально)", false),
|
||||
"ip_overrides": strProps("JSON ip_overrides (опционально)", false),
|
||||
}, nil),
|
||||
}, releasePatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := requestArgs(req)
|
||||
upd := configserver.DeploymentUpdate{
|
||||
Schema: optionalString(args, "schema"),
|
||||
Manifest: optionalString(args, "manifest"),
|
||||
PrebuildVars: optionalString(args, "prebuild_vars"),
|
||||
IPOverrides: optionalString(args, "ip_overrides"),
|
||||
}
|
||||
if upd.Schema == nil && upd.Manifest == nil && upd.PrebuildVars == nil && upd.IPOverrides == nil {
|
||||
return errorResult("нужно передать хотя бы одно из полей: schema, manifest, prebuild_vars, ip_overrides"), nil
|
||||
}
|
||||
api, res := openAPI(ctx, args, true)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
defer api.Close()
|
||||
return result(api.SaveDeploymentFiles(ctx, upd))
|
||||
})
|
||||
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "seed_hosts",
|
||||
Description: "Создать служебные записи хостов по IP из schema.json. Изменяет БД.",
|
||||
InputSchema: schema(map[string]any{"server": serverProp()}, nil),
|
||||
}, releasePatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := requestArgs(req)
|
||||
api, res := openAPI(ctx, args, true)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
defer api.Close()
|
||||
return result(api.SeedHosts(ctx))
|
||||
})
|
||||
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "fill_vars",
|
||||
Description: "Заполнить prebuild_vars из schema.json. Изменяет файл конфигурации развёртывания.",
|
||||
InputSchema: schema(map[string]any{"server": serverProp()}, nil),
|
||||
}, releasePatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := requestArgs(req)
|
||||
api, res := openAPI(ctx, args, true)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
defer api.Close()
|
||||
v, err := api.FillVars(ctx)
|
||||
return result(v, err)
|
||||
})
|
||||
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "download_release",
|
||||
Description: "Загрузить релиз. load_images=false — синхронно (пакеты + sync-задачи); true — фоновая загрузка образов (прогресс через release_job). Разрушительно/тяжело: требует confirm=\"true\".",
|
||||
InputSchema: schema(map[string]any{
|
||||
"server": serverProp(),
|
||||
"load_images": boolProps("Загружать docker-образы в фоне (по умолчанию false)", false),
|
||||
"gitlab_token": strProps("GitLab-токен (опционально; иначе из конфига/env сервера)", false),
|
||||
"confirm": boolProps("Подтверждение операции (обязательно true)", true),
|
||||
}, []string{"confirm"}),
|
||||
}, releasePatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := requestArgs(req)
|
||||
if res := confirmGate(args, "download_release"); res != nil {
|
||||
return res, nil
|
||||
}
|
||||
api, res := openAPI(ctx, args, true)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
defer api.Close()
|
||||
ack, err := api.DownloadRelease(ctx, getString(args, "gitlab_token", ""), getBool(args, "load_images", false))
|
||||
return result(ack, err)
|
||||
})
|
||||
|
||||
addPatternTool(s, &mcp.Tool{
|
||||
Name: "start_services",
|
||||
Description: "Создать задачи запуска сервисов для хостов из schema.json. Изменяет состояние стенда.",
|
||||
InputSchema: schema(map[string]any{"server": serverProp()}, nil),
|
||||
}, releasePatterns, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
args := requestArgs(req)
|
||||
api, res := openAPI(ctx, args, true)
|
||||
if res != nil {
|
||||
return res, nil
|
||||
}
|
||||
defer api.Close()
|
||||
return result(api.StartServices(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// optionalString возвращает указатель на строковый аргумент, если он задан
|
||||
// (в том числе пустой строкой), иначе nil.
|
||||
func optionalString(args map[string]any, key string) *string {
|
||||
if _, ok := args[key]; !ok {
|
||||
return nil
|
||||
}
|
||||
v := getString(args, key, "")
|
||||
return &v
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"git.totmin.ru/en2zmax/forge-toolkit"
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// toolkit.go — тонкие адаптеры к общему тулкиту (модуль forge-toolkit).
|
||||
// Локальные имена сохраняют хендлеры независимыми от пакета-источника.
|
||||
|
||||
func requestArgs(req *mcp.CallToolRequest) map[string]any { return toolkit.RequestArgs(req) }
|
||||
|
||||
func getString(args map[string]any, key, def string) string { return toolkit.GetString(args, key, def) }
|
||||
|
||||
func requireString(args map[string]any, key string) (string, error) {
|
||||
return toolkit.RequireString(args, key)
|
||||
}
|
||||
|
||||
func getInt(args map[string]any, key string, def int) int { return toolkit.GetInt(args, key, def) }
|
||||
|
||||
func getBool(args map[string]any, key string, def bool) bool { return toolkit.GetBool(args, key, def) }
|
||||
|
||||
func schema(properties map[string]any, required []string) map[string]any {
|
||||
return toolkit.Schema(properties, required)
|
||||
}
|
||||
|
||||
func strProps(desc string, required bool, enum ...string) map[string]any {
|
||||
return toolkit.StrProps(desc, required, enum...)
|
||||
}
|
||||
|
||||
func intProps(desc string, required bool) map[string]any { return toolkit.IntProps(desc, required) }
|
||||
|
||||
func boolProps(desc string, required bool) map[string]any { return toolkit.BoolProps(desc, required) }
|
||||
|
||||
func textResult(text string) *mcp.CallToolResult { return toolkit.Text(text) }
|
||||
|
||||
func errorResult(msg string) *mcp.CallToolResult { return toolkit.Error(msg) }
|
||||
|
||||
// addTool регистрирует инструмент без probe-паттернов (read-only).
|
||||
func addTool(s *mcp.Server, tool *mcp.Tool, h mcp.ToolHandler) { s.AddTool(tool, h) }
|
||||
|
||||
// patternsFn — эмиттер permission-паттернов разрешений для probe-запросов.
|
||||
type patternsFn = toolkit.PatternsFn
|
||||
|
||||
// addPatternTool регистрирует изменяющий инструмент с поддержкой probe: хост
|
||||
// получает канонический паттерн ресурса, не выполняя действие.
|
||||
func addPatternTool(s *mcp.Server, tool *mcp.Tool, fn patternsFn, h mcp.ToolHandler) {
|
||||
toolkit.RegisterPatternTool(s, tool, fn, h)
|
||||
}
|
||||
|
||||
// jsonText сериализует значение в читаемый JSON для ответа модели.
|
||||
func jsonText(v any) string {
|
||||
b, err := json.MarshalIndent(v, "", " ")
|
||||
if err != nil {
|
||||
return "{}"
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package tools
|
||||
|
||||
import "strconv"
|
||||
|
||||
// util.go — небольшие помощники MCP-слоя.
|
||||
|
||||
// truncateText ограничивает длинный вывод (задачи, compose, env), чтобы он не
|
||||
// съел контекст модели. По умолчанию оставляем хвост: ошибки сборки/деплоя
|
||||
// обычно в конце вывода. Второй результат — был ли вывод обрезан.
|
||||
func truncateText(s string, maxBytes int, tail bool) (string, bool) {
|
||||
if maxBytes <= 0 || len(s) <= maxBytes {
|
||||
return s, false
|
||||
}
|
||||
cut := len(s) - maxBytes
|
||||
if tail {
|
||||
return "…(обрезано " + strconv.Itoa(cut) + " байт, показан хвост)\n" + s[len(s)-maxBytes:], true
|
||||
}
|
||||
return s[:maxBytes] + "\n…(обрезано " + strconv.Itoa(cut) + " байт, показано начало)", true
|
||||
}
|
||||
Reference in New Issue
Block a user