diff --git a/README.md b/README.md index f00b4a7..c432173 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,22 @@ MCP сервер для автоматизации деплоя приложен Получает настройки деплоя по умолчанию для различных фреймворков. +### `create_floating_ip` + +Создает новый floating IP адрес в указанной зоне доступности. + +### `create_vpc` + +Создает новую виртуальную приватную сеть (VPC) в указанной зоне доступности. + +### `create_database` + +Создает новую базу данных в Timeweb Cloud с указанными параметрами. + +### `get_database_presets` + +Получает список доступных пресетов конфигураций для создания баз данных. + ## Промпты ### `create_app_prompt` diff --git a/package-lock.json b/package-lock.json index f98ed72..817f2f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "timeweb-mcp-server", "version": "0.1.3", - "license": "MIT", + "license": "UNLICENSED", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.4", "axios": "^1.11.0", @@ -525,10 +525,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", - "license": "MIT", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", diff --git a/src/actions/create-database.action.ts b/src/actions/create-database.action.ts new file mode 100644 index 0000000..2ec9c27 --- /dev/null +++ b/src/actions/create-database.action.ts @@ -0,0 +1,7 @@ +import { dbaasApiClient } from "../api"; +import { Database } from "../types/database.type"; +import { CreateDbParams } from "../types/create-db-params.type"; + +export const createDatabaseAction = async (params: CreateDbParams): Promise => { + return await dbaasApiClient.createDatabase(params); +}; diff --git a/src/actions/create-floating-ip.action.ts b/src/actions/create-floating-ip.action.ts new file mode 100644 index 0000000..318c613 --- /dev/null +++ b/src/actions/create-floating-ip.action.ts @@ -0,0 +1,10 @@ +import { dbaasApiClient } from "../api"; +import { FloatingIp } from "../types/floating-ip.type"; +import { AvailabilityZones } from "../types/availability-zones.enum"; + +export const createFloatingIpAction = async ( + availabilityZone: AvailabilityZones, + isDdosGuard: boolean = false +): Promise => { + return await dbaasApiClient.createFloatingIp(availabilityZone, isDdosGuard); +}; diff --git a/src/actions/create-vpc.action.ts b/src/actions/create-vpc.action.ts new file mode 100644 index 0000000..709ae07 --- /dev/null +++ b/src/actions/create-vpc.action.ts @@ -0,0 +1,11 @@ +import { dbaasApiClient } from "../api"; +import { AvailabilityZones } from "../types/availability-zones.enum"; +import { Vpc } from "../types/vpc.type"; + +export const createVpcAction = async ( + availabilityZone: AvailabilityZones, + name: string, + subnetV4: string +): Promise => { + return await dbaasApiClient.createVpc(availabilityZone, name, subnetV4); +}; diff --git a/src/actions/get-database-presets.action.ts b/src/actions/get-database-presets.action.ts new file mode 100644 index 0000000..8aa1b21 --- /dev/null +++ b/src/actions/get-database-presets.action.ts @@ -0,0 +1,7 @@ +import { dbaasApiClient } from "../api"; +import { DatabasePreset } from "../types/database-preset.type"; + +export const getDatabasePresetsAction = + async (): Promise => { + return await dbaasApiClient.getDatabasePresets(); + }; diff --git a/src/actions/get-vpcs.action.ts b/src/actions/get-vpcs.action.ts new file mode 100644 index 0000000..0270a29 --- /dev/null +++ b/src/actions/get-vpcs.action.ts @@ -0,0 +1,7 @@ +import { dbaasApiClient } from "../api"; + +export const getVpcsAction = async () => { + const response = await dbaasApiClient.getVpcs(); + + return response.vpcs; +}; diff --git a/src/api/dbaas.ts b/src/api/dbaas.ts new file mode 100644 index 0000000..59beeee --- /dev/null +++ b/src/api/dbaas.ts @@ -0,0 +1,120 @@ +import { BaseApiClient } from "./client"; +import { CreateFloatingIpRequestDto } from "../types/dto/create-floating-ip-request.dto"; +import { CreateFloatingIpResponseDto } from "../types/dto/create-floating-ip-response.dto"; +import { CreateVpcRequestDto } from "../types/dto/create-vpc-request.dto"; +import { CreateVpcResponseDto } from "../types/dto/create-vpc-response.dto"; +import { CreateDatabaseRequestDto } from "../types/dto/create-database-request.dto"; +import { CreateDatabaseResponseDto } from "../types/dto/create-database-response.dto"; +import { Database } from "../types/database.type"; +import { + DatabasePreset, + DatabasePresetsResponse, +} from "../types/database-preset.type"; +import { AvailabilityZones } from "../types/availability-zones.enum"; +import { CreateDbParams } from "../types/create-db-params.type"; +import { Vpc } from "../types/vpc.type"; +import { FloatingIp } from "../types/floating-ip.type"; +import { GetVpcsResponseDto } from "../types/dto/get-vpcs-response.dto"; + +export class DbaasApiClient extends BaseApiClient { + /** + * Создает новый floating IP адрес + */ + async createFloatingIp( + availabilityZone: AvailabilityZones, + isDdosGuard: boolean = false + ): Promise { + const requestData: CreateFloatingIpRequestDto = { + availability_zone: availabilityZone, + is_ddos_guard: isDdosGuard, + }; + + const response = await this.post( + "/api/v1/floating-ips", + requestData + ); + return response.ip; + } + + /** + * Получить список VPC пользователя + */ + async getVpcs(): Promise { + return this.get("/api/v2/vpcs"); + } + + /** + * Создает новую виртуальную приватную сеть (VPC) + */ + async createVpc( + availabilityZone: AvailabilityZones, + name: string, + subnetV4: string + ): Promise { + const requestData: CreateVpcRequestDto = { + availability_zone: availabilityZone, + name: name, + subnet_v4: subnetV4, + }; + + const response = await this.post( + "/api/v2/vpcs", + requestData + ); + return response.vpc; + } + + /** + * Создает новую базу данных + */ + async createDatabase({ + name, + type, + presetId, + availabilityZone, + adminPassword, + floatingIp, + vpcId, + hashType, + }: CreateDbParams): Promise { + const requestData: CreateDatabaseRequestDto = { + admin: { + password: adminPassword, + for_all: false, + }, + name: name, + type: type, + preset_id: presetId, + availability_zone: availabilityZone, + hash_type: hashType, + auto_backups: { + copy_count: 1, + creation_start_at: new Date().toISOString(), + interval: "day", + day_of_week: 5, + }, + network: { + floating_ip: floatingIp, + id: vpcId, + }, + }; + + const response = await this.post( + "/api/v1/databases", + requestData + ); + return response.db; + } + + /** + * Получает список пресетов баз данных + */ + async getDatabasePresets(): Promise { + const response = await this.get( + "/api/v2/presets/dbs" + ); + return response.databases_presets; + } +} + +export const dbaasApiClient: DbaasApiClient = new DbaasApiClient(); diff --git a/src/api/index.ts b/src/api/index.ts index 44ae57d..f9db26e 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1 +1,2 @@ export { appsApiClient } from "./apps"; +export { dbaasApiClient } from "./dbaas"; diff --git a/src/resources/database-presets.resource.ts b/src/resources/database-presets.resource.ts new file mode 100644 index 0000000..e43b130 --- /dev/null +++ b/src/resources/database-presets.resource.ts @@ -0,0 +1,47 @@ +import { getDatabasePresetsAction } from "../actions/get-database-presets.action"; +import { ResourceNames } from "../types/resource-names.enum"; + +export const databasePresetsResource = { + name: ResourceNames.DATABASE_PRESETS, + title: "Пресеты баз данных", + description: "Список доступных пресетов конфигураций для создания баз данных", + mimeType: "application/json", + handler: async () => { + try { + const presets = await getDatabasePresetsAction(); + + if (!presets || !presets.length) { + return { + contents: [ + { + type: "text" as const, + text: "❌ Не удалось получить список пресетов баз данных", + }, + ], + }; + } + + const content = `📊 **Пресеты баз данных Timeweb Cloud**\n\n;${JSON.stringify(presets, null, 2)}`; + + return { + contents: [ + { + type: "text" as const, + text: content, + }, + ], + }; + } catch (error) { + return { + contents: [ + { + type: "text" as const, + text: `❌ Ошибка получения пресетов баз данных: ${ + error instanceof Error ? error.message : "Неизвестная ошибка" + }`, + }, + ], + }; + } + }, +}; diff --git a/src/resources/get-vpcs.resource.ts b/src/resources/get-vpcs.resource.ts new file mode 100644 index 0000000..5d6eb9b --- /dev/null +++ b/src/resources/get-vpcs.resource.ts @@ -0,0 +1,34 @@ +import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ResourceNames } from "../types/resource-names.enum"; +import { createResourceResponse } from "../utils"; +import { getVpcsAction } from "../actions/get-vpcs.action"; +import { ToolNames } from "../types/tool-names.enum"; + +export const getVpcsResource = { + name: ResourceNames.GET_VPCS, + uri: "vpc://all", + template: new ResourceTemplate("vpc://all", { list: undefined }), + title: "Список VPC пользователя", + description: + "Список виртуальных частных сетей (VPC) пользователя в Timeweb Cloud", + handler: async (uri: URL) => { + try { + const result = await getVpcsAction(); + + if (!result || result.length === 0) { + return createResourceResponse(uri.href, `Нет доступных VPC. Создайте первую VPC с помощью tool ${ToolNames.CREATE_VPC}`); + } + + return createResourceResponse(uri.href, JSON.stringify(result, null, 2)); + } catch (error: unknown) { + if (error instanceof Error) { + return createResourceResponse( + uri.href, + `Не удалось получить список VPC. Причина: ${error.message}` + ); + } + + return createResourceResponse(uri.href, `Не удалось получить список VPC`); + } + }, +}; diff --git a/src/resources/index.ts b/src/resources/index.ts index 2a2fc41..9ee41af 100644 --- a/src/resources/index.ts +++ b/src/resources/index.ts @@ -2,3 +2,5 @@ export { vcsProvidersResource } from "./vcs-providers.resource"; export { vcsProviderRepositoriesResource } from "./vcs-provider-repositories.resource"; export { allowedPresetsResource } from "./allowed-presets.resource"; export { deploySettingsResource } from "./deploy-settings.resource"; +export { databasePresetsResource } from "./database-presets.resource"; +export { getVpcsResource } from "./get-vpcs.resource"; diff --git a/src/tools/create-database.tool.ts b/src/tools/create-database.tool.ts new file mode 100644 index 0000000..dbdadbe --- /dev/null +++ b/src/tools/create-database.tool.ts @@ -0,0 +1,166 @@ +import { z } from "zod"; +import { createToolResponse, getPresetDatabaseType } from "../utils"; +import { createDatabaseAction } from "../actions/create-database.action"; +import { ToolNames } from "../types/tool-names.enum"; +import { DatabaseTypes } from "../types/database-types.enum"; +import { AvailabilityZones } from "../types/availability-zones.enum"; +import { dbaasApiClient } from "../api"; +import { ResourceNames } from "../types/resource-names.enum"; + +const inputSchema = { + name: z + .string({ + description: "Название базы данных", + }) + .min(3, "Название не может быть пустым") + .describe("ОБЯЗАТЕЛЬНОЕ ПОЛЕ - название базы данных"), + type: z + .nativeEnum(DatabaseTypes) + .describe("ОБЯЗАТЕЛЬНОЕ ПОЛЕ - тип базы данных"), + preset_id: z + .number({ + description: "ID пресета конфигурации базы данных", + }) + .int("ID пресета должен быть целым числом") + .positive("ID пресета должен быть положительным числом") + .describe("ОБЯЗАТЕЛЬНОЕ ПОЛЕ - ID пресета"), + availability_zone: z + .nativeEnum(AvailabilityZones) + .describe("ОБЯЗАТЕЛЬНОЕ ПОЛЕ - зона доступности"), + admin_password: z + .string({ + description: "Пароль администратора базы данных", + }) + .min(8, "Пароль должен содержать минимум 8 символов") + .describe("ОБЯЗАТЕЛЬНОЕ ПОЛЕ - пароль администратора"), + floating_ip: z + .string({ + description: "Floating IP адрес для подключения к базе данных", + }) + .ip({ + version: "v4", + message: "Floating IP должен быть валидным IPv4 адресом", + }) + .describe("ОБЯЗАТЕЛЬНОЕ ПОЛЕ - floating IP адрес"), + vpc_id: z + .string({ + description: `ID виртуальной приватной сети (VPC). Получить список VPC можно в ресурсах ${ResourceNames.GET_VPCS}`, + }) + .min(1, "ID VPC не может быть пустым") + .describe("ОБЯЗАТЕЛЬНОЕ ПОЛЕ - ID VPC"), + hash_type: z + .string({ + description: "Тип хеширования паролей", + }) + .default("caching_sha2") + .describe( + "НЕ ОБЯЗАТЕЛЬНОЕ ПОЛЕ - тип хеширования (по умолчанию: caching_sha2)" + ), + auto_backups: z + .boolean({ + description: "Включить ли автоматические резервные копии", + }) + .default(true) + .describe( + "НЕ ОБЯЗАТЕЛЬНОЕ ПОЛЕ - автоматические резервные копии (по умолчанию: true)" + ), +}; + +const handler = async (params: { + name: string; + type: DatabaseTypes; + preset_id: number; + availability_zone: AvailabilityZones; + admin_password: string; + floating_ip: string; + vpc_id: string; + hash_type?: "caching_sha2"; +}) => { + try { + const availableZones = Object.values(AvailabilityZones); + + if (!availableZones.includes(params.availability_zone)) { + return createToolResponse( + `❌ Неверная зона доступности: ${params.availability_zone}. Выбери из списка: ${availableZones.join(", ")}` + ); + } + + const availableTypes = Object.values(DatabaseTypes); + + if (!availableTypes.includes(params.type)) { + return createToolResponse( + `❌ Неверный тип базы данных: ${params.type}. Выбери из списка: ${availableTypes.join(", ")}` + ); + } + + const allPresets = await dbaasApiClient.getDatabasePresets(); + const availablePresets = allPresets.filter(preset => preset.type === getPresetDatabaseType(params.type)); + + if (!availablePresets || !availablePresets.length) { + return createToolResponse( + `❌ Не удалось получить список пресетов баз данных. Убедитесь, что тип базы данных "${params.type}" соответствует типу пресета` + ); + } + + const preset = availablePresets.find(preset => preset.id === params.preset_id); + + if (!preset) { + return createToolResponse( + `❌ Не корректный ID пресета для создания базы данных "${params.name}" в Timeweb Cloud. Используй доступые пресеты для этого типа базы данных ${JSON.stringify(availablePresets, null, 2)}` + ); + } + + const db = await createDatabaseAction({ + name: params.name, + type: params.type, + presetId: params.preset_id, + availabilityZone: params.availability_zone, + adminPassword: params.admin_password, + floatingIp: params.floating_ip, + vpcId: params.vpc_id, + hashType: params.hash_type || "caching_sha2", + }); + + if (!db) { + return createToolResponse( + `❌ Не удалось создать базу данных "${params.name}"` + ); + } + + return createToolResponse(`✅ База данных успешно создана! + +📋 Детали созданной базы данных: +• Название: ${db.name} +• ID: ${db.id} +• Тип: ${db.type} +• Статус: ${db.status} +• Порт: ${db.port} +• Зона доступности: ${db.availability_zone} +• Локация: ${db.location} +• Пресет: ${db.preset_id} +• Проект: ${db.project_id} +• Тип хеширования: ${db.hash_type} +• Публичная сеть: ${db.is_enabled_public_network ? "✅" : "❌"} +• Автобэкапы: ${db.is_autobackups_enabled ? "✅" : "❌"} +• Безопасное соединение: ${db.is_secure_connection_enabled ? "✅" : "❌"} +• Создана: ${new Date(db.created_at).toLocaleString("ru-RU")} +ДАННЫЕ ДЛЯ ПОДКЛЮЧЕНИЯ К БАЗЕ ДАННЫХ БУДУТ ДОСТУПНЫ ПОСЛЕ СОЗДАНИЯ В ПАНЕЛИ УПРАВЛЕНИЯ TIMEWEB CLOUD +🎉 База данных скоро будет готова к использованию!`); + } catch (error) { + if (error instanceof Error) { + return createToolResponse( + `❌ Ошибка создания базы данных. Причина: ${error.message}` + ); + } + return createToolResponse(`❌ Неизвестная ошибка при создании базы данных`); + } +}; + +export const createDatabaseTool = { + name: ToolNames.CREATE_DATABASE, + title: "Создание базы данных", + description: + "Создает новую базу данных в Timeweb Cloud с указанными параметрами", + inputSchema, + handler, +}; diff --git a/src/tools/create-floating-ip.tool.ts b/src/tools/create-floating-ip.tool.ts new file mode 100644 index 0000000..7432fa0 --- /dev/null +++ b/src/tools/create-floating-ip.tool.ts @@ -0,0 +1,69 @@ +import { z } from "zod"; +import { createToolResponse } from "../utils"; +import { ToolNames } from "../types/tool-names.enum"; +import { createFloatingIpAction } from "../actions/create-floating-ip.action"; +import { AvailabilityZones } from "../types/availability-zones.enum"; + +const inputSchema = { + availability_zone: z.nativeEnum(AvailabilityZones) + .describe("ОБЯЗАТЕЛЬНОЕ ПОЛЕ - зона доступности"), + is_ddos_guard: z + .boolean({ + description: "Включить ли DDoS защиту для floating IP", + }) + .default(false) + .describe("НЕ ОБЯЗАТЕЛЬНОЕ ПОЛЕ - DDoS защита (по умолчанию: false)"), +}; + +const handler = async (params: { + availability_zone: AvailabilityZones; + is_ddos_guard?: boolean; +}) => { + try { + const availableZones = Object.values(AvailabilityZones); + + if (!availableZones.includes(params.availability_zone)) { + return createToolResponse( + `❌ Неверная зона доступности: ${params.availability_zone}. Выбери из списка: ${availableZones.join(", ")}` + ); + } + + const ip = await createFloatingIpAction( + params.availability_zone, + params.is_ddos_guard || false + ); + + if (!ip) { + return createToolResponse( + `❌ Не удалось создать floating IP в зоне "${params.availability_zone}"` + ); + } + + return createToolResponse(`✅ Floating IP успешно создан! + +📋 Детали созданного IP: +• IP адрес: ${ip.ip} +• ID: ${ip.id} +• Зона доступности: ${ip.availability_zone} +• DDoS Guard: ${ip.is_ddos_guard ? "✅ Включен" : "❌ Отключен"} +• Комментарий: ${ip.comment || "Нет"} +• Создан: ${new Date(ip.created_at).toLocaleString("ru-RU")} + +🎉 Floating IP готов к использованию!`); + } catch (error) { + if (error instanceof Error) { + return createToolResponse( + `❌ Ошибка создания floating IP. Причина: ${error.message}` + ); + } + return createToolResponse(`❌ Неизвестная ошибка при создании floating IP`); + } +}; + +export const createFloatingIpTool = { + name: ToolNames.CREATE_FLOATING_IP, + title: "Создание Floating IP", + description: "Создает новый floating IP адрес в указанной зоне доступности", + inputSchema, + handler, +}; diff --git a/src/tools/create-vpc.tool.ts b/src/tools/create-vpc.tool.ts new file mode 100644 index 0000000..7f75fec --- /dev/null +++ b/src/tools/create-vpc.tool.ts @@ -0,0 +1,86 @@ +import { z } from "zod"; +import { createToolResponse } from "../utils"; +import { createVpcAction } from "../actions/create-vpc.action"; +import { ToolNames } from "../types/tool-names.enum"; +import { AvailabilityZones } from "../types/availability-zones.enum"; + +const inputSchema = { + availability_zone: z + .nativeEnum(AvailabilityZones) + .describe("ОБЯЗАТЕЛЬНОЕ ПОЛЕ - зона доступности"), + name: z + .string({ + description: "Название виртуальной приватной сети", + }) + .describe("Название VPC") + .default(""), + subnet_v4: z + .string({ + description: "IPv4 подсеть в формате CIDR (например: 192.168.0.0/24)", + }) + .regex( + /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/, + "Подсеть должна быть в формате CIDR (например: 192.168.0.0/24)" + ) + .describe("ОБЯЗАТЕЛЬНОЕ ПОЛЕ - IPv4 подсеть в формате CIDR"), +}; + +const handler = async (params: { + availability_zone: AvailabilityZones; + name: string; + subnet_v4: string; +}) => { + try { + const availableZones = Object.values(AvailabilityZones); + + if (!availableZones.includes(params.availability_zone)) { + return createToolResponse( + `❌ Неверная зона доступности: ${params.availability_zone}. Выбери из списка: ${availableZones.join(", ")}` + ); + } + + const vpc = await createVpcAction( + params.availability_zone, + params.name, + params.subnet_v4 + ); + + if (!vpc) { + return createToolResponse( + `❌ Не удалось создать VPC "${params.name}" в зоне "${params.availability_zone}"` + ); + } + + return createToolResponse(`✅ Виртуальная приватная сеть успешно создана! + +📋 Детали созданной VPC: +• Название: ${vpc.name} +• ID: ${vpc.id} +• Зона доступности: ${vpc.availability_zone} +• Локация: ${vpc.location} +• Подсеть IPv4: ${vpc.subnet_v4} +• Тип: ${vpc.type} +• Описание: ${vpc.description || "Нет"} +• Публичный IP: ${vpc.public_ip || "Нет"} +• Создана: ${new Date(vpc.created_at).toLocaleString("ru-RU")} +• Занятые адреса: ${vpc.busy_address.join(", ")} + +🎉 VPC готова к использованию!`); + } catch (error) { + if (error instanceof Error) { + return createToolResponse( + `❌ Ошибка создания VPC. Причина: ${error.message}` + ); + } + return createToolResponse(`❌ Неизвестная ошибка при создании VPC`); + } +}; + +export const createVpcTool = { + name: ToolNames.CREATE_VPC, + title: "Создание виртуальной приватной сети (VPC)", + description: + "Создает новую виртуальную приватную сеть в указанной зоне доступности", + inputSchema, + handler, +}; diff --git a/src/tools/get-database-presets.tool.ts b/src/tools/get-database-presets.tool.ts new file mode 100644 index 0000000..4f6b2a4 --- /dev/null +++ b/src/tools/get-database-presets.tool.ts @@ -0,0 +1,37 @@ +import { createToolResponse } from "../utils"; +import { getDatabasePresetsAction } from "../actions/get-database-presets.action"; +import { ToolNames } from "../types/tool-names.enum"; + +const handler = async () => { + try { + const presets = await getDatabasePresetsAction(); + + if (!presets || !presets.length) { + return createToolResponse( + `❌ Не удалось получить список пресетов баз данных` + ); + } + + const response = `📊 **Пресеты баз данных Timeweb Cloud**\n\n;${JSON.stringify(presets, null, 2)}`; + + return createToolResponse(response); + } catch (error) { + if (error instanceof Error) { + return createToolResponse( + `❌ Ошибка получения пресетов баз данных. Причина: ${error.message}` + ); + } + return createToolResponse( + `❌ Неизвестная ошибка при получении пресетов баз данных` + ); + } +}; + +export const getDatabasePresetsTool = { + name: ToolNames.GET_DATABASE_PRESETS, + title: "Получение пресетов баз данных", + description: + "Получает список доступных пресетов конфигураций для создания баз данных", + inputSchema: {}, + handler, +}; diff --git a/src/tools/get-vpcs.tool.ts b/src/tools/get-vpcs.tool.ts new file mode 100644 index 0000000..956e119 --- /dev/null +++ b/src/tools/get-vpcs.tool.ts @@ -0,0 +1,42 @@ +import { createToolResponse } from "../utils"; +import { getVpcsAction } from "../actions/get-vpcs.action"; +import { ToolNames } from "../types/tool-names.enum"; + +const handler = async () => { + try { + const vpcs = await getVpcsAction(); + + if (!vpcs || vpcs.length === 0) { + return createToolResponse( + `💡 VPC не найдены. Создайте первую VPC с помощью tool ${ToolNames.CREATE_VPC}` + ); + } + + return createToolResponse(`📋 Список VPC: + + ${JSON.stringify(vpcs, null, 2)} + + 💡 Всего VPC: ${vpcs.length} + + 🎉 Список VPC успешно получен!`); + } catch (error) { + if (error instanceof Error) { + return createToolResponse( + `❌ Ошибка при получении списка VPC. Причина: ${error.message}` + ); + } + + return createToolResponse( + `❌ Неизвестная ошибка при получении списка VPC.` + ); + } +}; + +export const getVpcsTool = { + name: ToolNames.GET_VPCS, + title: "Получение списка VPC", + description: + "Получает список всех виртуальных частных сетей (VPC) пользователя", + inputSchema: {}, + handler, +}; diff --git a/src/tools/index.ts b/src/tools/index.ts index 0ac846c..4490036 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -5,3 +5,8 @@ export { addVcsProviderTool } from "./add-vcs-provider.tool"; export { getVcsProviderRepositoriesTool } from "./get-vcs-provider-repositories.tool"; export { getVcsProviderByRepositoryUrlTool } from "./get-vcs-provider-by-repository-url.tool"; export { getDeploySettingsTool } from "./get-deploy-settings.tool"; +export { createFloatingIpTool } from "./create-floating-ip.tool"; +export { createVpcTool } from "./create-vpc.tool"; +export { getVpcsTool } from "./get-vpcs.tool"; +export { createDatabaseTool } from "./create-database.tool"; +export { getDatabasePresetsTool } from "./get-database-presets.tool"; diff --git a/src/types/availability-zones.enum.ts b/src/types/availability-zones.enum.ts new file mode 100644 index 0000000..91302c5 --- /dev/null +++ b/src/types/availability-zones.enum.ts @@ -0,0 +1,6 @@ +export enum AvailabilityZones { + SPB_3 = 'spb-3', + MSK_1 = 'msk-1', + AMZ_1 = 'amz-1', + FRA_1 = 'fra-1', +} \ No newline at end of file diff --git a/src/types/create-db-params.type.ts b/src/types/create-db-params.type.ts new file mode 100644 index 0000000..0bba93a --- /dev/null +++ b/src/types/create-db-params.type.ts @@ -0,0 +1,13 @@ +import { AvailabilityZones } from "./availability-zones.enum"; +import { DatabaseTypes } from "./database-types.enum"; + +export interface CreateDbParams { + name: string, + type: DatabaseTypes, + presetId: number, + availabilityZone: AvailabilityZones, + adminPassword: string, + floatingIp: string, + vpcId: string, + hashType: "caching_sha2" +} \ No newline at end of file diff --git a/src/types/database-preset.type.ts b/src/types/database-preset.type.ts new file mode 100644 index 0000000..72b9d83 --- /dev/null +++ b/src/types/database-preset.type.ts @@ -0,0 +1,24 @@ +import { PresetDatabaseTypes } from "./preset-database-types.enum"; +import { ServiceLocations } from "./service-locations.enum"; + +export interface DatabasePreset { + id: number; + description: string; + description_short: string; + cpu: number; + ram: number; + disk: number; + type: PresetDatabaseTypes; + price: number; + location: ServiceLocations; + vds_node_configuration_id: number; +} + +export interface DatabasePresetsMeta { + total: number; +} + +export interface DatabasePresetsResponse { + meta: DatabasePresetsMeta; + databases_presets: DatabasePreset[]; +} diff --git a/src/types/database-types.enum.ts b/src/types/database-types.enum.ts new file mode 100644 index 0000000..ea201a1 --- /dev/null +++ b/src/types/database-types.enum.ts @@ -0,0 +1,20 @@ +export enum DatabaseTypes { + MYSQL8_4 = 'mysql8_4', + MYSQL8 = 'mysql', + POSTGRESQL17 = 'postgres17', + POSTGRESQL16 = 'postgres16', + POSTGRESQL15 = 'postgres15', + POSTGRESQL14 = 'postgres14', + REDIS8_1 = 'redis8_1', + REDIS7 = 'redis7', + MONGODB8_0 = 'mongodb8_0', + MONGODB7 = 'mongodb7', + OPENSEARCH2_19 = 'opensearch2_19', + OPENSEARCH = 'opensearch', + CLICKHOUSE25 = 'clickhouse25', + CLICKHOUSE24 = 'clickhouse24', + CLICKHOUSE = 'clickhouse', + KAFKA = 'kafka', + RABBITMQ4 = 'rabbitmq4_0', + RABBITMQ = 'rabbitmq', +} diff --git a/src/types/database.type.ts b/src/types/database.type.ts new file mode 100644 index 0000000..773c140 --- /dev/null +++ b/src/types/database.type.ts @@ -0,0 +1,46 @@ +import { DatabaseTypes } from "./database-types.enum"; +import { ServiceLocations } from "./service-locations.enum"; + +export interface Database { + id: number; + created_at: string; + location: ServiceLocations; + name: string; + type: DatabaseTypes; + hash_type: "caching_sha2"; + avatar_link: string | null; + port: number; + status: string; + preset_id: number; + disk: any | null; + networks: any[]; + config_parameters: Record; + is_enabled_public_network: boolean; + availability_zone: string; + is_autobackups_enabled: boolean; + cpu: any | null; + cpu_frequency: any | null; + ram: any | null; + configurator_id: any | null; + project_id: number | null; + replica_list: any[]; + is_secure_connection_enabled: boolean; + domains: any[]; +} + +export interface DatabaseAdmin { + password: string; + for_all: boolean; +} + +export interface DatabaseAutoBackups { + copy_count: number; + creation_start_at: string; + interval: "day" | "month"; + day_of_week: number; +} + +export interface DatabaseNetwork { + floating_ip: string; + id: string; +} diff --git a/src/types/dto/create-database-request.dto.ts b/src/types/dto/create-database-request.dto.ts new file mode 100644 index 0000000..7cac919 --- /dev/null +++ b/src/types/dto/create-database-request.dto.ts @@ -0,0 +1,19 @@ +import { + DatabaseAdmin, + DatabaseAutoBackups, + DatabaseNetwork, +} from "../database.type"; +import { DatabaseTypes } from "../database-types.enum"; +import { AvailabilityZones } from "../availability-zones.enum"; + +export interface CreateDatabaseRequestDto { + admin: DatabaseAdmin; + name: string; + type: DatabaseTypes; + preset_id: number; + availability_zone: AvailabilityZones; + hash_type: "caching_sha2"; + project_id?: number; + auto_backups: DatabaseAutoBackups; + network: DatabaseNetwork; +} diff --git a/src/types/dto/create-database-response.dto.ts b/src/types/dto/create-database-response.dto.ts new file mode 100644 index 0000000..6499ce2 --- /dev/null +++ b/src/types/dto/create-database-response.dto.ts @@ -0,0 +1,5 @@ +import { Database } from "../database.type"; + +export interface CreateDatabaseResponseDto { + db: Database; +} diff --git a/src/types/dto/create-floating-ip-request.dto.ts b/src/types/dto/create-floating-ip-request.dto.ts new file mode 100644 index 0000000..7313add --- /dev/null +++ b/src/types/dto/create-floating-ip-request.dto.ts @@ -0,0 +1,6 @@ +import { AvailabilityZones } from "../availability-zones.enum"; + +export interface CreateFloatingIpRequestDto { + availability_zone: AvailabilityZones; + is_ddos_guard: boolean; +} diff --git a/src/types/dto/create-floating-ip-response.dto.ts b/src/types/dto/create-floating-ip-response.dto.ts new file mode 100644 index 0000000..29176de --- /dev/null +++ b/src/types/dto/create-floating-ip-response.dto.ts @@ -0,0 +1,5 @@ +import { FloatingIp } from "../floating-ip.type"; + +export interface CreateFloatingIpResponseDto { + ip: FloatingIp; +} diff --git a/src/types/dto/create-vpc-request.dto.ts b/src/types/dto/create-vpc-request.dto.ts new file mode 100644 index 0000000..f400f81 --- /dev/null +++ b/src/types/dto/create-vpc-request.dto.ts @@ -0,0 +1,7 @@ +import { AvailabilityZones } from "../availability-zones.enum"; + +export interface CreateVpcRequestDto { + availability_zone: AvailabilityZones; + name: string; + subnet_v4: string; +} diff --git a/src/types/dto/create-vpc-response.dto.ts b/src/types/dto/create-vpc-response.dto.ts new file mode 100644 index 0000000..b4778a4 --- /dev/null +++ b/src/types/dto/create-vpc-response.dto.ts @@ -0,0 +1,5 @@ +import { Vpc } from "../vpc.type"; + +export interface CreateVpcResponseDto { + vpc: Vpc; +} diff --git a/src/types/dto/get-vpcs-response.dto.ts b/src/types/dto/get-vpcs-response.dto.ts new file mode 100644 index 0000000..85331c0 --- /dev/null +++ b/src/types/dto/get-vpcs-response.dto.ts @@ -0,0 +1,8 @@ +import { Vpc } from "../vpc.type"; + +export interface GetVpcsResponseDto { + meta: { + total: number; + }; + vpcs: Vpc[]; +} diff --git a/src/types/floating-ip.type.ts b/src/types/floating-ip.type.ts new file mode 100644 index 0000000..8176785 --- /dev/null +++ b/src/types/floating-ip.type.ts @@ -0,0 +1,10 @@ +import { AvailabilityZones } from "./availability-zones.enum"; + +export interface FloatingIp { + id: string; + ip: string; + is_ddos_guard: boolean; + availability_zone: AvailabilityZones; + comment: string; + created_at: string; +} diff --git a/src/types/preset-database-types.enum.ts b/src/types/preset-database-types.enum.ts new file mode 100644 index 0000000..23bc1ef --- /dev/null +++ b/src/types/preset-database-types.enum.ts @@ -0,0 +1,10 @@ +export enum PresetDatabaseTypes { + MYSQL = 'mysql', + POSTGRESQL = 'postgres', + REDIS = 'redis', + MONGODB = 'mongodb', + OPENSEARCH = 'opensearch', + CLICKHOUSE = 'clickhouse', + KAFKA = 'kafka', + RABBITMQ = 'rabbitmq', +} diff --git a/src/types/resource-names.enum.ts b/src/types/resource-names.enum.ts index 538bc1d..b1d7cc1 100644 --- a/src/types/resource-names.enum.ts +++ b/src/types/resource-names.enum.ts @@ -3,4 +3,6 @@ export enum ResourceNames { VCS_PROVIDERS = "vcs_providers", VCS_PROVIDER_REPOSITORIES = "vcs_provider_repositories", DEPLOY_SETTINGS = "deploy_settings", + DATABASE_PRESETS = "database_presets", + GET_VPCS = "get_vpcs", } diff --git a/src/types/service-locations.enum.ts b/src/types/service-locations.enum.ts new file mode 100644 index 0000000..aa115c1 --- /dev/null +++ b/src/types/service-locations.enum.ts @@ -0,0 +1,6 @@ +export enum ServiceLocations { + SAINT_PETERSBURG = 'ru-1', // SPB_3 + MOSCOW = 'ru-3', // MSK_1 + NETHERLANDS = 'nl-1', // AMZ_1 + FRANKFURT = 'de-1', // FRA_1 +} \ No newline at end of file diff --git a/src/types/tool-names.enum.ts b/src/types/tool-names.enum.ts index b4b45ff..bbeb818 100644 --- a/src/types/tool-names.enum.ts +++ b/src/types/tool-names.enum.ts @@ -6,4 +6,9 @@ export enum ToolNames { GET_VCS_PROVIDER_REPOSITORIES = "get_vcs_provider_repositories", GET_VCS_PROVIDER_BY_REPOSITORY_URL = "get_vcs_provider_by_repository_url", GET_DEPLOY_SETTINGS = "get_deploy_settings", + CREATE_FLOATING_IP = "create_floating_ip", + CREATE_VPC = "create_vpc", + GET_VPCS = "get_vpcs", + CREATE_DATABASE = "create_database", + GET_DATABASE_PRESETS = "get_database_presets", } diff --git a/src/types/vpc.type.ts b/src/types/vpc.type.ts new file mode 100644 index 0000000..7b784ac --- /dev/null +++ b/src/types/vpc.type.ts @@ -0,0 +1,15 @@ +import { AvailabilityZones } from "./availability-zones.enum"; +import { ServiceLocations } from "./service-locations.enum"; + +export interface Vpc { + id: string; + description: string | null; + subnet_v4: string; + location: ServiceLocations; + created_at: string; + availability_zone: AvailabilityZones; + name: string; + public_ip: string | null; + type: string; + busy_address: string[]; +} diff --git a/src/utils.ts b/src/utils.ts index 3c1e5ae..9ef2ee5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,8 @@ import { readFileSync } from "fs"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; +import { DatabaseTypes } from "./types/database-types.enum"; +import { PresetDatabaseTypes } from "./types/preset-database-types.enum"; export const getVersion = (): string => { try { @@ -25,3 +27,34 @@ export const createToolResponse = (text: string) => ({ export const createResourceResponse = (uri: string, text: string) => ({ contents: [{ uri, text, mimeType: "application/json" }], }); + +export const getPresetDatabaseType = (type: DatabaseTypes) => { + switch (type) { + case DatabaseTypes.MYSQL8_4: + case DatabaseTypes.MYSQL8: + return PresetDatabaseTypes.MYSQL; + case DatabaseTypes.POSTGRESQL14: + case DatabaseTypes.POSTGRESQL15: + case DatabaseTypes.POSTGRESQL16: + case DatabaseTypes.POSTGRESQL17: + return PresetDatabaseTypes.POSTGRESQL; + case DatabaseTypes.REDIS7: + case DatabaseTypes.REDIS8_1: + return PresetDatabaseTypes.REDIS; + case DatabaseTypes.MONGODB7: + case DatabaseTypes.MONGODB8_0: + return PresetDatabaseTypes.MONGODB; + case DatabaseTypes.OPENSEARCH: + case DatabaseTypes.OPENSEARCH2_19: + return PresetDatabaseTypes.OPENSEARCH; + case DatabaseTypes.CLICKHOUSE: + case DatabaseTypes.CLICKHOUSE24: + case DatabaseTypes.CLICKHOUSE25: + return PresetDatabaseTypes.CLICKHOUSE; + case DatabaseTypes.KAFKA: + return PresetDatabaseTypes.KAFKA; + case DatabaseTypes.RABBITMQ: + case DatabaseTypes.RABBITMQ4: + return PresetDatabaseTypes.RABBITMQ; + } +}; \ No newline at end of file