monacloud-mcp 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/jobs.js ADDED
@@ -0,0 +1,42 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { CloudError } from './errors.js';
3
+ import { unwrapData } from './http.js';
4
+ export const TERMINAL_JOBS = new Set(['done', 'succeeded', 'failed', 'cancelled', 'error']);
5
+ const object = (value) => (value && typeof value === 'object' ? value : {});
6
+ export async function pollJob(clients, jobId, wait, intervalSeconds, timeoutSeconds, sandbox) {
7
+ const deadline = Date.now() + timeoutSeconds * 1000;
8
+ const read = async () => object(unwrapData(await clients.vibecloud(`/api/jobs/${encodeURIComponent(jobId)}`, {
9
+ timeoutMs: wait ? Math.max(1, Math.min(30_000, deadline - Date.now())) : 30_000,
10
+ })));
11
+ let result = await read();
12
+ while (wait && !TERMINAL_JOBS.has(String(result.status)) && Date.now() < deadline) {
13
+ await delay(Math.min(intervalSeconds * 1000, Math.max(0, deadline - Date.now())));
14
+ if (Date.now() >= deadline)
15
+ break;
16
+ result = await read();
17
+ }
18
+ return {
19
+ ...result,
20
+ ...(sandbox ? { sandbox: true } : {}),
21
+ ...(wait && !TERMINAL_JOBS.has(String(result.status)) ? {
22
+ polling: 'timeout', job_id: jobId,
23
+ next_step: `Gọi lại cloud_job_status với job_id=${jobId}; không tạo lại app.`,
24
+ } : {}),
25
+ };
26
+ }
27
+ export async function finishAppJob(clients, created, sandbox, wait, interval, timeout) {
28
+ const start = object(unwrapData(created));
29
+ const jobId = typeof start.job_id === 'string' ? start.job_id
30
+ : !start.url && typeof start.id === 'string' ? start.id : undefined;
31
+ const job = jobId && wait ? await pollJob(clients, jobId, true, interval, timeout, sandbox) : start;
32
+ const result = object(job.result);
33
+ if (['failed', 'error', 'cancelled'].includes(String(job.status)) || result.status === 'error') {
34
+ throw new CloudError('app_deploy_failed', `Deploy thất bại${jobId ? ` (job ${jobId})` : ''}: ${job.error || result.errorMessage || result.error || 'kiểm tra log build'}`, 'Gọi cloud_app_logs và cloud_app_get để sửa lỗi; chỉ cloud_app_deploy lại sau khi sửa.');
35
+ }
36
+ return { ...start, ...job, ...(jobId ? { job_id: jobId } : {}),
37
+ ...(result.estimated_app_host ? { estimate: result.estimated_app_host } : {}),
38
+ ...(typeof result.app_id === 'string' ? { app_id: result.app_id } : {}),
39
+ ...(typeof result.url === 'string' ? { url: result.url } : {}),
40
+ ...(typeof result.application_id === 'string' ? { application_id: result.application_id } : {}),
41
+ ...(sandbox ? { sandbox: true } : {}) };
42
+ }
package/dist/mail.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { CloudClients } from './clients.js';
3
+ import type { Config } from './config.js';
4
+ export declare function registerMailTools(server: McpServer, clients: CloudClients, config: Config): void;
package/dist/mail.js ADDED
@@ -0,0 +1,183 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { domainToASCII } from 'node:url';
3
+ import { z } from 'zod';
4
+ import { runTool } from './errors.js';
5
+ const id = z.string().trim().min(1).max(255);
6
+ const email = z.string().max(320).regex(/^[^\s@<>,;]+@[^\s@<>,;]+\.[^\s@<>,;]+$/, 'Cần địa chỉ email hợp lệ.');
7
+ const mailbox = z.union([
8
+ email,
9
+ z.string().max(998).regex(/^[^<>\r\n]+<[^\s@<>,;]+@[^\s@<>,;]+\.[^\s@<>,;]+>$/, 'Dùng email hoặc Tên <email>.'),
10
+ ]);
11
+ const recipients = z.union([mailbox, z.array(mailbox).min(1).max(50)]);
12
+ const subject = z.string().min(1).max(998).regex(/^[^\r\n]+$/, 'Subject không chứa xuống dòng.');
13
+ const domain = z.string().trim().toLowerCase().transform(domainToASCII).pipe(z.string().max(253).regex(/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/, 'Cần domain hợp lệ, không kèm URL.'));
14
+ const httpsUrl = z.string().url().refine((value) => new URL(value).protocol === 'https:', 'Cần URL HTTPS.');
15
+ const timestamp = z.iso.datetime({ offset: true });
16
+ const dateOrTimestamp = z.union([z.iso.date(), timestamp]);
17
+ const idempotency = {
18
+ idempotency_key: z.string().min(1).max(255).regex(/^[\x21-\x7e]+$/, 'Idempotency key dùng ký tự ASCII, không khoảng trắng.').optional()
19
+ .describe('Giữ cùng key khi thử lại cùng yêu cầu trong 24 giờ. / Reuse for retries.'),
20
+ };
21
+ const empty = z.object({}).strict();
22
+ const events = z.enum([
23
+ 'email.sent', 'email.delivered', 'email.deferred', 'email.bounced',
24
+ 'email.complained', 'email.suppressed', 'email.failed', 'domain.verified',
25
+ ]);
26
+ const status = z.enum(['queued', 'sent', 'delivered', 'deferred', 'bounced', 'complained', 'suppressed', 'failed', 'sandbox']);
27
+ function idempotencyHeaders(key) {
28
+ return { 'Idempotency-Key': key || `mcp-mail-${randomUUID()}` };
29
+ }
30
+ export function registerMailTools(server, clients, config) {
31
+ server.registerTool('mail_account', {
32
+ title: 'Tài khoản MONA Mail',
33
+ description: `Khi bắt đầu tích hợp email, đọc tài khoản, quota và bước kế tiếp. / Use first to read account and quota at ${config.monamailApi}.`,
34
+ inputSchema: empty,
35
+ }, () => runTool(() => clients.mail('/v1/account')));
36
+ server.registerTool('mail_plans', {
37
+ title: 'Gói MONA Mail',
38
+ description: 'Khi chọn gói gửi mail, đọc giá và quota hiện hành. / Use to compare current email plans.',
39
+ inputSchema: empty,
40
+ }, () => runTool(() => clients.mail('/v1/plans')));
41
+ server.registerTool('mail_plan_set', {
42
+ title: 'Đổi gói MONA Mail',
43
+ description: 'Khi cần đổi quota, chọn gói; gói trả phí trừ ví VND, thiếu tiền gọi cloud_topup. / Use to change the email plan.',
44
+ inputSchema: z.object({ plan: z.enum(['free', 'khoi-nghiep', 'kinh-doanh', 'doanh-nghiep']) }).strict(),
45
+ }, ({ plan }) => runTool(() => clients.mail('/v1/account/plan', { method: 'PUT', body: { plan } })));
46
+ server.registerTool('mail_send', {
47
+ title: 'Gửi email giao dịch',
48
+ description: 'Khi gửi OTP hoặc thông báo, dùng domain đã verify; onboarding@monamail.vn chỉ gửi tới email chủ. sandbox=true thử 0đ, không gửi ra Internet. / Use to send transactional email or test in sandbox.',
49
+ inputSchema: z.object({
50
+ from: mailbox,
51
+ to: recipients,
52
+ subject: subject.optional().describe('Bắt buộc nếu không dùng template_id. / Required without a template.'),
53
+ html: z.string().min(1).optional(),
54
+ text: z.string().min(1).optional(),
55
+ reply_to: recipients.optional(),
56
+ tags: z.array(z.string().regex(/^[a-z0-9_-]+$/)).max(10).optional(),
57
+ template_id: id.optional(),
58
+ variables: z.record(z.string(), z.unknown()).optional(),
59
+ unsubscribe_url: httpsUrl.optional(),
60
+ ...idempotency,
61
+ sandbox: z.boolean().optional().describe('Gửi X-Mona-Sandbox: 1, không tính quota hoặc trừ ví. / Test without delivery or charges.'),
62
+ }).strict().superRefine((value, context) => {
63
+ if (!value.template_id) {
64
+ if (!value.subject)
65
+ context.addIssue({ code: 'custom', path: ['subject'], message: 'Cần subject khi không dùng template_id.' });
66
+ if (!value.html && !value.text)
67
+ context.addIssue({ code: 'custom', path: ['html'], message: 'Cần ít nhất html hoặc text khi không dùng template_id.' });
68
+ }
69
+ }),
70
+ }, ({ sandbox, idempotency_key, ...body }) => runTool(async () => {
71
+ const result = await clients.mail('/v1/emails', {
72
+ method: 'POST',
73
+ headers: { ...idempotencyHeaders(idempotency_key), ...(sandbox ? { 'X-Mona-Sandbox': '1' } : {}) },
74
+ body,
75
+ });
76
+ return sandbox ? { ...result, sandbox: true } : result;
77
+ }));
78
+ server.registerTool('mail_status', {
79
+ title: 'Trạng thái email',
80
+ description: 'Khi cần xác nhận thư đã giao, đọc trạng thái, events và sandbox_preview. / Use to inspect an email after sending.',
81
+ inputSchema: z.object({ email_id: id }).strict(),
82
+ }, ({ email_id }) => runTool(() => clients.mail(`/v1/emails/${encodeURIComponent(email_id)}`)));
83
+ server.registerTool('mail_list', {
84
+ title: 'Danh sách email',
85
+ description: 'Khi tra lịch sử gửi, lọc theo trạng thái, người nhận hoặc thời gian. / Use to search sent email history.',
86
+ inputSchema: z.object({
87
+ limit: z.number().int().min(1).max(100).default(20),
88
+ status: status.optional(),
89
+ to: email.optional(),
90
+ since: timestamp.optional(),
91
+ }).strict(),
92
+ }, (query) => runTool(() => clients.mail('/v1/emails', { query })));
93
+ server.registerTool('mail_domain_add', {
94
+ title: 'Thêm domain gửi email',
95
+ description: 'Khi gửi bằng domain của app, thêm domain. Trả record DNS; nếu người dùng dùng Cloudflare có thể gọi mail_domain_cloudflare với token của họ (không lưu). / Use to register a sender domain and get DNS records.',
96
+ inputSchema: z.object({
97
+ domain: domain.refine((value) => value !== 'monamail.vn' && !value.endsWith('.monamail.vn'), 'Dùng domain của anh chị, không dùng domain MONA Mail.'),
98
+ ...idempotency,
99
+ }).strict(),
100
+ }, ({ domain, idempotency_key }) => runTool(async () => ({
101
+ ...await clients.mail('/v1/domains', {
102
+ method: 'POST', headers: idempotencyHeaders(idempotency_key), body: { domain },
103
+ }),
104
+ instructions: 'Thêm các records vào DNS rồi gọi mail_domain_verify. Nếu dùng Cloudflare, gọi mail_domain_cloudflare với token người dùng cung cấp; token chỉ dùng một lần, không lưu. DKIM đúng là đủ để verify; SPF/DMARC là cảnh báo. Nếu đã có SPF, thêm include:_spf.monamail.vn vào record hiện có.',
105
+ })));
106
+ server.registerTool('mail_domain_verify', {
107
+ title: 'Xác minh DNS domain',
108
+ description: 'Khi đã thêm DNS, kiểm DKIM và trạng thái domain. / Use after adding DNS records to verify the sender domain.',
109
+ inputSchema: z.object({ domain_id: id, ...idempotency }).strict(),
110
+ }, ({ domain_id, idempotency_key }) => runTool(() => clients.mail(`/v1/domains/${encodeURIComponent(domain_id)}/verify`, {
111
+ method: 'POST', headers: idempotencyHeaders(idempotency_key),
112
+ })));
113
+ server.registerTool('mail_domain_cloudflare', {
114
+ title: 'Thêm DNS qua Cloudflare',
115
+ description: 'Khi người dùng cung cấp token Cloudflare, thêm DNS rồi verify domain. Token dùng một lần, không lưu, không log. / Use a user-provided Cloudflare token to configure DNS and verify.',
116
+ inputSchema: z.object({ domain_id: id, api_token: z.string().trim().min(1).max(4096), ...idempotency }).strict(),
117
+ }, ({ domain_id, api_token, idempotency_key }) => runTool(() => clients.mail(`/v1/domains/${encodeURIComponent(domain_id)}/cloudflare`, {
118
+ method: 'POST', headers: idempotencyHeaders(idempotency_key), body: { api_token },
119
+ })));
120
+ server.registerTool('mail_domains_list', {
121
+ title: 'Domain MONA Mail',
122
+ description: 'Khi chọn địa chỉ gửi, xem domain và trạng thái xác minh. / Use to find verified sender domains.',
123
+ inputSchema: empty,
124
+ }, () => runTool(() => clients.mail('/v1/domains')));
125
+ server.registerTool('mail_api_key_create', {
126
+ title: 'Tạo API key MONA Mail',
127
+ description: 'Khi tích hợp SDK vào app, tạo key live hoặc test. Key chỉ trả một lần; ghi vào .env của app dưới tên MONAMAIL_API_KEY, không cần in ra chat. / Use to create an app key; store the one-time secret in .env.',
128
+ inputSchema: z.object({ name: id, mode: z.enum(['live', 'test']), ...idempotency }).strict(),
129
+ }, ({ idempotency_key, ...body }) => runTool(() => clients.mail('/v1/api-keys', {
130
+ method: 'POST', headers: idempotencyHeaders(idempotency_key), body,
131
+ })));
132
+ server.registerTool('mail_api_keys_list', {
133
+ title: 'Danh sách API key',
134
+ description: 'Khi kiểm tra key của app, đọc prefix và trạng thái; không trả secret. / Use to inspect existing API key metadata.',
135
+ inputSchema: empty,
136
+ }, () => runTool(() => clients.mail('/v1/api-keys')));
137
+ server.registerTool('mail_api_key_revoke', {
138
+ title: 'Thu hồi API key',
139
+ description: 'Khi key không còn dùng hoặc bị lộ, thu hồi bằng key_id. / Use to revoke an unused or compromised API key.',
140
+ inputSchema: z.object({ key_id: id }).strict(),
141
+ }, ({ key_id }) => runTool(() => clients.mail(`/v1/api-keys/${encodeURIComponent(key_id)}`, { method: 'DELETE' })));
142
+ server.registerTool('mail_webhook_create', {
143
+ title: 'Tạo webhook email',
144
+ description: 'Khi app cần nhận sự kiện gửi hoặc bounce, đăng ký HTTPS webhook; lưu secret một lần vào .env, không log. / Use to subscribe an app to email events.',
145
+ inputSchema: z.object({ url: httpsUrl, events: z.array(events).min(1).max(8), ...idempotency }).strict(),
146
+ }, ({ idempotency_key, ...body }) => runTool(() => clients.mail('/v1/webhooks', {
147
+ method: 'POST', headers: idempotencyHeaders(idempotency_key), body,
148
+ })));
149
+ server.registerTool('mail_webhooks_list', {
150
+ title: 'Danh sách webhook email',
151
+ description: 'Khi kiểm tra cấu hình sự kiện của app, liệt kê webhook. / Use to inspect registered email webhooks.',
152
+ inputSchema: empty,
153
+ }, () => runTool(() => clients.mail('/v1/webhooks')));
154
+ server.registerTool('mail_webhook_test', {
155
+ title: 'Thử webhook email',
156
+ description: 'Khi đã có endpoint, gửi mẫu email.delivered để kiểm tra HTTP response. / Use to test webhook delivery to an app.',
157
+ inputSchema: z.object({ webhook_id: id, ...idempotency }).strict(),
158
+ }, ({ webhook_id, idempotency_key }) => runTool(() => clients.mail(`/v1/webhooks/${encodeURIComponent(webhook_id)}/test`, {
159
+ method: 'POST', headers: idempotencyHeaders(idempotency_key),
160
+ })));
161
+ server.registerTool('mail_suppressions_list', {
162
+ title: 'Địa chỉ ngừng gửi',
163
+ description: 'Khi thư bị suppressed, xem địa chỉ và lý do ngừng gửi. / Use to diagnose suppressed recipients.',
164
+ inputSchema: empty,
165
+ }, () => runTool(() => clients.mail('/v1/suppressions')));
166
+ server.registerTool('mail_suppression_remove', {
167
+ title: 'Gỡ suppression tài khoản',
168
+ description: 'Khi đã xử lý nguyên nhân chặn, gỡ suppression của tài khoản; lớp toàn hệ không gỡ được. / Use to remove an account-level suppression.',
169
+ inputSchema: z.object({ email }).strict(),
170
+ }, ({ email }) => runTool(() => clients.mail(`/v1/suppressions/${encodeURIComponent(email)}`, { method: 'DELETE' })));
171
+ server.registerTool('mail_template_create', {
172
+ title: 'Tạo mẫu email',
173
+ description: 'Khi app dùng lại nội dung mail, tạo template với biến {{ten_bien}}. / Use to create a reusable email template.',
174
+ inputSchema: z.object({ name: id, subject, html: z.string().min(1), text: z.string().min(1).optional(), ...idempotency }).strict(),
175
+ }, ({ idempotency_key, ...body }) => runTool(() => clients.mail('/v1/templates', {
176
+ method: 'POST', headers: idempotencyHeaders(idempotency_key), body,
177
+ })));
178
+ server.registerTool('mail_stats', {
179
+ title: 'Thống kê gửi email',
180
+ description: 'Khi đánh giá khả năng giao thư, đọc tỷ lệ delivered và bounce theo thời gian. / Use to review email delivery statistics.',
181
+ inputSchema: z.object({ from: dateOrTimestamp.optional(), to: dateOrTimestamp.optional() }).strict().refine((value) => !value.from || !value.to || Date.parse(value.from) <= Date.parse(value.to), 'Thời điểm from phải trước hoặc bằng to.'),
182
+ }, (query) => runTool(() => clients.mail('/v1/stats', { query })));
183
+ }
package/dist/server.js CHANGED
@@ -1,4 +1,3 @@
1
- import { setTimeout as delay } from 'node:timers/promises';
2
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
2
  import { z } from 'zod';
4
3
  import { readConfig } from './config.js';
@@ -8,8 +7,10 @@ import { errorResult, runTool, textResult, toAgentError } from './errors.js';
8
7
  import { requestJson, unwrapData } from './http.js';
9
8
  import { createMonaPayClient, importedMonaPayTools, normalizeImportedResult } from './monapay.js';
10
9
  import { TemplateCatalog } from './templates.js';
11
- const ENTITY = 'MONA Cloud là hệ công cụ cho vibecoder Việt Nam: một MONA Pass, một ví VND và một MCP để chạy app/VPS, thu tiền MONA Pay và dùng các sản phẩm MONA.';
12
- const TERMINAL_JOBS = new Set(['done', 'succeeded', 'failed', 'cancelled']);
10
+ import { registerMailTools } from './mail.js';
11
+ import { APP_FLOW, createVps, registerComputeTools } from './compute.js';
12
+ import { pollJob } from './jobs.js';
13
+ const ENTITY = 'MONA Cloud là hệ công cụ cho vibecoder Việt Nam: một MONA Pass, một ví VND và một MCP để chạy app/VPS, thu tiền MONA Pay và dùng các sản phẩm MONA. Dùng mail_* để gửi email giao dịch (MONA Mail). MONA Mail là dịch vụ gửi email giao dịch cho phần mềm và AI agent của người Việt: một API, trả VND, không cần thẻ, thuộc nhóm MONA Cloud của The MONA Group.';
13
14
  const SANDBOX_DESCRIPTION = 'sandbox=true: thử 0đ, không cần ví';
14
15
  const sandboxSchema = z.boolean().optional().describe(SANDBOX_DESCRIPTION);
15
16
  const asObject = (value) => (value && typeof value === 'object' ? value : {});
@@ -44,35 +45,34 @@ const provisionSchema = z.object(sizingSchema).superRefine((value, context) => {
44
45
  context.addIssue({ code: 'custom', message: 'Cần package_slug hoặc đủ cpu, ram_gb và disk_gb.' });
45
46
  }
46
47
  });
48
+ const vpsSchema = z.object({
49
+ ...sizingSchema,
50
+ billing_mode: z.enum(['hourly', 'monthly']).optional(),
51
+ plan_code: z.string().trim().min(2).max(64).optional(),
52
+ period: z.enum(['month', 'year']).optional(),
53
+ }).strict().superRefine((value, context) => {
54
+ if (value.billing_mode === 'monthly') {
55
+ if (!value.plan_code)
56
+ context.addIssue({ code: 'custom', message: 'Monthly cần plan_code.' });
57
+ return;
58
+ }
59
+ if (value.plan_code || value.period)
60
+ context.addIssue({ code: 'custom', message: 'plan_code/period chỉ dùng với billing_mode=monthly.' });
61
+ const checked = provisionSchema.safeParse(value);
62
+ if (!checked.success)
63
+ for (const issue of checked.error.issues)
64
+ context.addIssue({ code: 'custom', message: issue.message, path: issue.path });
65
+ });
47
66
  function agentRuntimeStub(template, sandbox = false) {
48
67
  return {
49
68
  status: 'not_available',
50
69
  code: 'agent_runtime_pending',
51
70
  template,
52
71
  message: 'Runtime MONA Agent trên MONA Cloud chưa được phát hành; tool này là slot wave kế tiếp theo brief.',
53
- next_step: 'Dùng agent_templates_get để lấy nội dung template và triển khai thủ công, hoặc thử lại khi MONA Cloud bật runtime agent.',
72
+ next_step: `Dùng agent_templates_get để đọc template. ${APP_FLOW}`,
54
73
  ...(sandbox ? { sandbox: true } : {}),
55
74
  };
56
75
  }
57
- async function pollJob(clients, jobId, wait, intervalSeconds, timeoutSeconds, sandbox) {
58
- let result = await clients.vibecloud(`/api/jobs/${encodeURIComponent(jobId)}`);
59
- if (!wait)
60
- return sandbox ? { ...asObject(result), sandbox: true } : result;
61
- const deadline = Date.now() + timeoutSeconds * 1000;
62
- while (!TERMINAL_JOBS.has(String(asObject(result).status || '')) && Date.now() < deadline) {
63
- await delay(intervalSeconds * 1000);
64
- result = await clients.vibecloud(`/api/jobs/${encodeURIComponent(jobId)}`);
65
- }
66
- if (!TERMINAL_JOBS.has(String(asObject(result).status || ''))) {
67
- return {
68
- ...asObject(result),
69
- ...(sandbox ? { sandbox: true } : {}),
70
- polling: 'timeout',
71
- next_step: `Gọi lại cloud_job_status với job_id=${jobId}.`,
72
- };
73
- }
74
- return sandbox ? { ...asObject(result), sandbox: true } : result;
75
- }
76
76
  async function providerHealth(name, url, fetchImpl) {
77
77
  const started = Date.now();
78
78
  try {
@@ -90,7 +90,7 @@ export function createServer(dependencies = {}) {
90
90
  const auth = new AuthManager(config, env, fetchImpl);
91
91
  const clients = new CloudClients(config, auth, fetchImpl);
92
92
  const catalog = new TemplateCatalog(config, fetchImpl);
93
- const server = new McpServer({ name: 'monacloud-mcp', version: '0.2.1' }, { instructions: `${ENTITY}\nDùng cloud_* cho tài khoản, ví và hạ tầng; monapay_* cho thu tiền; agent_* cho catalog. Không bao giờ yêu cầu mật khẩu sản phẩm. Chỉ dừng hỏi người dùng khi cần nạp tiền, OTP ngân hàng hoặc KYC bắt buộc.` });
93
+ const server = new McpServer({ name: 'monacloud-mcp', version: '0.3.0' }, { instructions: `${ENTITY}\nDùng cloud_* cho tài khoản, ví và hạ tầng; monapay_* cho thu tiền; mail_* để gửi email giao dịch (MONA Mail); agent_* cho catalog. Không bao giờ yêu cầu mật khẩu sản phẩm. Đọc → ước tính → hỏi duyệt nếu chưa được duyệt → làm. Dừng khi cần DNS, nạp tiền, duyệt chi phí, OTP hoặc KYC. ${APP_FLOW}` });
94
94
  server.registerTool('cloud_whoami', {
95
95
  title: 'Tài khoản MONA Cloud',
96
96
  description: 'Xác minh MONA Pass hiện tại. / Return the current MONA Pass profile.',
@@ -196,20 +196,14 @@ export function createServer(dependencies = {}) {
196
196
  }, () => runTool(() => clients.vibecloudLink()));
197
197
  server.registerTool('cloud_vps_create', {
198
198
  title: 'Tạo VPS MONA Cloud',
199
- description: `Kiểm tra ví chung rồi tạo LXC VPS; trả job_id để poll. ${SANDBOX_DESCRIPTION}.`,
200
- inputSchema: provisionSchema,
201
- }, (body) => runTool(async () => {
202
- const { sandbox, ...payload } = body;
203
- return clients.guardedVibecloud('/api/lxc', { method: 'POST', body: payload }, sandbox);
204
- }));
199
+ description: `Đọc cloud_plan_list (monthly) hoặc cloud_prices/cloud_packages (hourly), ước tính rồi hỏi duyệt trước tạo. Monthly bỏ CPU/RAM/đĩa, lấy từ plan_code; period=month|year. Kiểm ví đủ giá gói, trả estimate và job_id. / Estimate, approve, create VPS. ${SANDBOX_DESCRIPTION}.`,
200
+ inputSchema: vpsSchema,
201
+ }, (body) => runTool(() => createVps(clients, body)));
205
202
  server.registerTool('vibecloud_create_vps', {
206
203
  title: 'Alias cũ của cloud_vps_create',
207
204
  description: `Alias tương thích; dùng cloud_vps_create cho tích hợp mới. ${SANDBOX_DESCRIPTION}.`,
208
- inputSchema: provisionSchema,
209
- }, (body) => runTool(async () => {
210
- const { sandbox, ...payload } = body;
211
- return clients.guardedVibecloud('/api/lxc', { method: 'POST', body: payload }, sandbox);
212
- }));
205
+ inputSchema: vpsSchema,
206
+ }, (body) => runTool(() => createVps(clients, body)));
213
207
  const databaseSchema = provisionSchema.extend({ engine: z.enum(['mongodb', 'postgresql', 'mysql']).default('mongodb') });
214
208
  server.registerTool('cloud_db_create', {
215
209
  title: 'Tạo database MONA Cloud',
@@ -309,9 +303,11 @@ export function createServer(dependencies = {}) {
309
303
  description: `Alias tương thích; dùng cloud_agent_deploy cho tích hợp mới. ${SANDBOX_DESCRIPTION}.`,
310
304
  inputSchema: { template: z.string().regex(/^[a-z0-9][a-z0-9-]{0,79}$/), sandbox: sandboxSchema },
311
305
  }, ({ template, sandbox }) => runTool(() => agentRuntimeStub(template, clients.sandboxEnabled(sandbox))));
306
+ registerComputeTools(server, clients);
307
+ registerMailTools(server, clients, config);
312
308
  server.registerTool('agent_templates_list', {
313
309
  title: 'Catalog MONA Agent',
314
- description: 'Đọc template từ thư mục local, URL catalog hoặc catalog wave 1 tích hợp.',
310
+ description: 'Đọc template từ thư mục local, URL catalog hoặc catalog wave 1 tích hợp. Deploy repo git: dùng cloud_app_create, sandbox trước nếu chưa có host.',
315
311
  }, () => runTool(() => catalog.list()));
316
312
  server.registerTool('agent_templates_get', {
317
313
  title: 'Chi tiết MONA Agent template',
@@ -320,7 +316,7 @@ export function createServer(dependencies = {}) {
320
316
  }, ({ template }) => runTool(() => catalog.get(template)));
321
317
  server.registerTool('agent_deploy', {
322
318
  title: 'Dùng ngay MONA Agent template',
323
- description: 'Gọi cùng runtime với cloud_agent_deploy.',
319
+ description: 'Gọi cùng runtime với cloud_agent_deploy. Với deploy repo git, dùng cloud_app_create (đang mở), sandbox trước nếu chưa có host.',
324
320
  inputSchema: { template: z.string().regex(/^[a-z0-9][a-z0-9-]{0,79}$/) },
325
321
  }, ({ template }) => runTool(() => agentRuntimeStub(template)));
326
322
  const imported = importedMonaPayTools(config, env, fetchImpl);
@@ -351,11 +347,11 @@ export function createServer(dependencies = {}) {
351
347
  }, async (uri) => ({ contents: [{
352
348
  uri: uri.href,
353
349
  mimeType: 'text/plain',
354
- text: `${ENTITY}\n\nHuman chỉ đăng ký MONA Pass, nạp tiền và cung cấp OTP/KYC bắt buộc. AI dùng MCP làm phần còn lại.\n\n- cloud_*: tài khoản, ví, ledger, usage, budget, VPS, database, job, vòng đời service và giá/gói.\n- monapay_*: nối ngân hàng, checkout/QR, giao dịch, webhook và email.\n- agent_*: catalog và deploy template.\n\nMONA Cloud: https://monacloud.vn\nCompute API: https://api.monacloud.vn\nMONA Pay: https://monapay.vn\n`,
350
+ text: `${ENTITY}\n\n${APP_FLOW}\n\nHuman đăng ký MONA Pass, duyệt chi phí, thêm DNS khi cần, nạp tiền và cung cấp OTP/KYC bắt buộc. AI dùng MCP làm phần còn lại.\n\n- cloud_*: tài khoản, ví, ledger, usage, budget, VPS, database, job, vòng đời service, plans, subscriptions, invoices/PDF, credit và apps từ git (đang mở).\n- monapay_*: nối ngân hàng, checkout/QR, giao dịch, webhook và email.\n- mail_*: tài khoản, domain, API key, gửi mail, trạng thái, webhook, suppression (MONA Mail https://monamail.vn, API https://api.monamail.vn)\n- agent_*: catalog và deploy template.\n\nMONA Cloud: https://monacloud.vn\nCompute API: https://api.monacloud.vn\nMONA Pay: https://monapay.vn\n`,
355
351
  }] }));
356
352
  server.registerResource('monacloud-status', 'monacloud://status', {
357
353
  title: 'Trạng thái hệ MONA Cloud',
358
- description: 'Health tổng hợp của MONA Pass, billing, compute MONA Cloud và MONA Pay.',
354
+ description: 'Health tổng hợp của MONA Pass, billing, compute MONA Cloud, MONA Pay và MONA Mail.',
359
355
  mimeType: 'application/json',
360
356
  }, async (uri) => {
361
357
  const members = await Promise.all([
@@ -363,6 +359,7 @@ export function createServer(dependencies = {}) {
363
359
  providerHealth('billing', `${config.billingUrl}/v1/healthz`, fetchImpl),
364
360
  providerHealth('monacloud-compute', `${config.vibecloudApi}/api/prices`, fetchImpl),
365
361
  providerHealth('monapay', `${config.monapayApi}/health`, fetchImpl),
362
+ providerHealth('monamail', `${config.monamailApi}/v1/healthz`, fetchImpl),
366
363
  ]);
367
364
  return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ checked_at: new Date().toISOString(), members }, null, 2) }] };
368
365
  });
@@ -377,7 +374,30 @@ export function createServer(dependencies = {}) {
377
374
  role: 'user',
378
375
  content: {
379
376
  type: 'text',
380
- text: `Dựng app bán hàng ${app_name || 'của tôi'} bằng ${framework || 'stack phù hợp'} trên MONA Cloud theo đúng thứ tự:\n1. Gọi cloud_whoami, cloud_balance, cloud_packages và cloud_prices. Nếu thiếu tiền, gọi cloud_topup rồi dừng để người dùng quét VietQR.\n2. Gọi cloud_vps_create và cloud_db_create; poll từng job bằng cloud_job_status tới succeeded.\n3. Gọi monapay_link nếu MONA Pay còn ở lớp chuyển tiếp. Gọi monapay_whoami; nếu chưa có VA, nối ngân hàng bằng chuỗi monapay_link_bank_start → HỎI OTP → verify → notification_register → HỎI OTP lần 2 → verify. Không tự đoán OTP.\n4. Viết endpoint webhook có HMAC và idempotency theo transaction_code; đăng ký bằng monapay_create_webhook, bắn monapay_test_webhook và đọc monapay_webhook_logs.\n5. Tích hợp monapay_create_checkout hoặc monapay_create_qr vào app, chỉ giao hàng sau CHECKOUT_PAID.\n6. Deploy code lên VPS vừa tạo, kiểm tra health và báo URL/credential cần lưu. Không yêu cầu người dùng mở dashboard ngoài bước nạp tiền/OTP bắt buộc.`,
377
+ text: `Dựng app bán hàng ${app_name || 'của tôi'} bằng ${framework || 'stack phù hợp'} trên MONA Cloud theo đúng thứ tự:\n1. Gọi cloud_whoami, cloud_balance, cloud_plan_list, cloud_packages và cloud_prices. Báo ước tính và hỏi duyệt chi phí nếu chưa được duyệt. Nếu thiếu tiền, gọi cloud_topup rồi dừng để người dùng quét VietQR.\n2. Nếu dùng VPS gọi cloud_vps_create và cloud_db_create; nếu deploy repo dùng cloud_app_host_list rồi cloud_app_create (đang mở), sandbox trước nếu chưa có host. Poll từng job bằng cloud_job_status tới done/succeeded.\n3. Gọi monapay_link nếu MONA Pay còn ở lớp chuyển tiếp. Gọi monapay_whoami; nếu chưa có VA, nối ngân hàng bằng chuỗi monapay_link_bank_start → HỎI OTP → verify → notification_register → HỎI OTP lần 2 → verify. Không tự đoán OTP.\n4. Viết endpoint webhook có HMAC và idempotency theo transaction_code; đăng ký bằng monapay_create_webhook, bắn monapay_test_webhook và đọc monapay_webhook_logs.\n5. Tích hợp monapay_create_checkout hoặc monapay_create_qr vào app, chỉ giao hàng sau CHECKOUT_PAID.\n6. Với repo git, dùng cloud_app_create sau khi duyệt ước tính; kiểm cloud_app_get/cloud_app_logs và health rồi báo URL. Chỉ dùng VPS thủ công khi người dùng chọn. Không yêu cầu người dùng mở dashboard ngoài bước nạp tiền/OTP bắt buộc.`,
378
+ },
379
+ }] }));
380
+ server.registerPrompt('gui-mail-otp-monamail', {
381
+ title: 'Gửi mail OTP bằng MONA Mail',
382
+ description: 'Tích hợp OTP zero-dashboard: account → onboarding → DNS → verify → API key → SDK → webhook bounced.',
383
+ argsSchema: {
384
+ app_name: z.string().optional(),
385
+ framework: z.string().optional(),
386
+ domain: z.string().optional(),
387
+ },
388
+ }, ({ app_name, framework, domain }) => ({ messages: [{
389
+ role: 'user',
390
+ content: {
391
+ type: 'text',
392
+ text: `Tích hợp gửi mail OTP cho ${app_name || 'app của tôi'} bằng ${framework || 'stack hiện có của app'}, domain ${domain || 'lấy từ cấu hình app'}, dùng MONA Mail theo thứ tự:
393
+ 1. Gọi mail_account để lấy email chủ, quota, domain và next_step. MCP dùng MONA Pass sẵn có, tài khoản Mail được tạo tự động ở request đầu.
394
+ 2. Nếu chưa có domain verified, gửi thử mail_send từ onboarding@monamail.vn tới đúng email chủ vừa lấy, subject "Thử OTP MONA Mail", text là nội dung thử và idempotency_key riêng cho lượt thử. Gọi mail_status theo id; chỉ báo đã giao khi status=delivered. Nếu yêu cầu sandbox, dùng sandbox: true rồi kiểm sandbox_preview, không báo đã gửi thư thật.
395
+ 3. Gọi mail_domain_add cho domain của app. Trả nguyên records để người dùng thêm DNS; đây là điểm được dừng hỏi. Nếu đã có token Cloudflare của người dùng, gọi mail_domain_cloudflare để thêm DNS và verify; token chỉ dùng một lần, không lưu hoặc log. Nếu cần token, xin ngay tại bước DNS.
396
+ 4. Gọi mail_domain_verify, đọc checks và chờ status=verified; DKIM đúng là đủ, SPF/DMARC là cảnh báo. Khi đã có SPF, gộp include:_spf.monamail.vn vào record hiện có.
397
+ 5. Gọi mail_api_key_create với name theo app và mode live; khi thử sandbox chọn mode test (mm_test_). Key chỉ trả một lần: ghi trực tiếp vào .env của app dưới tên MONAMAIL_API_KEY, bỏ .env khỏi git, không in key ra chat hoặc log. Chỉ báo prefix nếu cần nhận diện.
398
+ 6. Viết code gửi OTP ở server bằng SDK npm monamail: import { MonaMail } from 'monamail'; const monamail = new MonaMail(process.env.MONAMAIL_API_KEY); const { id } = await monamail.emails.send({ from: 'Tên app <noreply@DOMAIN_DA_VERIFY>', to: emailNguoiNhan, subject: 'Mã OTP', text: 'Mã OTP: ' + otp, tags: ['otp'], idempotency_key: requestId }); Thay placeholder bằng domain đã verify, sinh OTP ngẫu nhiên có hạn dùng và giới hạn số lần thử. Giữ cùng idempotency_key khi retry cùng yêu cầu trong 24 giờ. Gọi mail_status để kiểm kết quả.
399
+ 7. Viết HTTPS endpoint nhận email.bounced; xác minh HMAC trên timestamp và raw body bằng MonaMail.verifyWebhook, chống replay và xử lý idempotent theo event id. Gọi mail_webhook_create với events: ['email.bounced'], lưu secret riêng trong .env, rồi mail_webhook_test (payload thử là email.delivered). Kiểm suppression bằng mail_suppressions_list khi có bounce.
400
+ Chỉ dừng hỏi người dùng ở bước thêm DNS hoặc nạp tiền. Nếu mail_plan_set trả insufficient_funds, gọi cloud_topup rồi chờ người dùng nạp và cloud_balance cập nhật. Dùng mail_plans để đọc giá hiện hành, không hard-code giá. Không yêu cầu mở dashboard để lấy API key.`,
381
401
  },
382
402
  }] }));
383
403
  return server;
package/dist/templates.js CHANGED
@@ -111,7 +111,7 @@ export class TemplateCatalog {
111
111
  ...template,
112
112
  source: 'built-in-wave-1-catalog',
113
113
  status: 'catalog_only',
114
- next_step: 'Catalog runtime chưa được phát hành; dùng cloud_agent_deploy để kiểm tra trạng thái triển khai.',
114
+ next_step: 'Catalog runtime chưa được phát hành. Deploy repo git: dùng cloud_app_host_list rồi cloud_app_create (đang mở), sandbox trước nếu chưa có app host; báo ước tính và hỏi duyệt trước khi tạo thật.',
115
115
  };
116
116
  }
117
117
  }
package/docs/ai-agent.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # MONA Cloud MCP — hướng dẫn cho AI agent
2
2
 
3
- Tài liệu này dành cho Claude, Codex, Cursor và agent dùng Model Context Protocol. Mục tiêu là hoàn thành tích hợp end-to-end mà không yêu cầu người dùng mở dashboard, trừ ba việc hợp lệ: đăng ký MONA Pass, nạp tiền và OTP/KYC bắt buộc.
3
+ Tài liệu này dành cho Claude, Codex, Cursor và agent dùng Model Context Protocol. Mục tiêu là hoàn thành tích hợp end-to-end mà không yêu cầu người dùng mở dashboard; các bước human gồm đăng ký MONA Pass, duyệt chi phí, thêm DNS khi cần, nạp tiền và OTP/KYC bắt buộc.
4
4
 
5
5
  ## Quy tắc vận hành
6
6
 
7
7
  1. Bắt đầu bằng `cloud_whoami`. Nếu nhận `login_required`, yêu cầu người dùng chạy `monacloud-mcp login`; không hỏi username hoặc password trong chat.
8
- 2. Trước khi provision thật, đọc `cloud_balance`, `cloud_packages` và `cloud_prices`. Với `sandbox: true`, không cần đọc hoặc nạp ví.
8
+ 2. Trước khi provision thật, đọc `cloud_balance`, `cloud_plan_list` hoặc `cloud_packages`/`cloud_prices`; báo ước tính và hỏi duyệt chi phí nếu chưa được duyệt. Với `sandbox: true`, không cần đọc hoặc nạp ví.
9
9
  3. Khi cần nạp, gọi `cloud_topup`; đưa nguyên `qr_data_url`, số tiền, nội dung và hạn thanh toán cho người dùng. Chỉ tiếp tục sau khi `cloud_balance` phản ánh tiền vào.
10
10
  4. Với OTP ngân hàng, dừng đúng sau `monapay_link_bank_start` và `monapay_notification_register`. Hỏi người dùng mã vừa nhận; không suy đoán, brute-force hoặc ghi OTP vào source/log.
11
11
  5. Mọi endpoint webhook phải xác minh HMAC, chống replay theo timestamp và idempotent theo `transaction_code`.
@@ -119,7 +119,7 @@ Dùng `monapay_create_qr` khi đã có đủ thông tin ACB/VA và cần render
119
119
 
120
120
  ### 6. Deploy và xác minh
121
121
 
122
- Deploy source lên VPS từ credential của job, đưa database connection string qua secret environment, chạy migration, bật HTTPS và gọi health endpoint. Sau đó:
122
+ Với repo git, ưu tiên `cloud_app_create` (đang mở): đọc `cloud_app_host_list`, sandbox trước nếu chưa có host, báo ước tính và duyệt rồi deploy thật. Dùng secret env, chạy migration và kiểm HTTPS/health. Chỉ triển khai VPS thủ công nếu người dùng chọn. Sau đó:
123
123
 
124
124
  - tạo checkout sandbox;
125
125
  - tạo transaction sandbox;
@@ -128,6 +128,49 @@ Deploy source lên VPS từ credential của job, đưa database connection stri
128
128
  - đọc `cloud_services` để báo lại toàn bộ service/VA/webhook;
129
129
  - không echo credential nhạy cảm trong báo cáo.
130
130
 
131
+ ## MONA Mail
132
+
133
+ MONA Mail là dịch vụ gửi email giao dịch cho phần mềm và AI agent của người Việt: một API, trả VND, không cần thẻ, thuộc nhóm MONA Cloud của The MONA Group.
134
+
135
+ 20 tool `mail_*` gọi `MONAMAIL_API` (mặc định `https://api.monamail.vn`) bằng access token MONA Pass hiện tại. JWT cần audience `monamail` hoặc `mona-products`; scope `product` đã có trong MCP. Tài khoản Mail được tạo tự động từ `sub` và `email` ở request đầu, không cần link hoặc đăng ký sản phẩm riêng.
136
+
137
+ Dùng prompt `gui-mail-otp-monamail` với `app_name?`, `framework?`, `domain?` và thực hiện:
138
+
139
+ 1. `mail_account` lấy email chủ, quota, domain và `next_step`. `mail_plans` đọc gói và giá hiện hành khi cần đổi quota.
140
+ 2. Nếu chưa có domain verified, `mail_send` từ `onboarding@monamail.vn` tới đúng email chủ. Gọi `mail_status` theo `id`; `queued` hoặc `sent` chưa có nghĩa thư đã tới hộp nhận.
141
+ 3. `mail_domain_add` trả records DKIM/SPF/DMARC. Dừng ở bước DNS để user thêm records hoặc cung cấp token Cloudflare. Nếu đã có token, gọi `mail_domain_cloudflare` ngay; token dùng một lần, không ghi file, cache, log hoặc chat. Nếu có SPF sẵn, gộp `include:_spf.monamail.vn` vào record hiện có.
142
+ 4. `mail_domain_verify` đọc `checks`; chỉ gửi bằng domain riêng khi `status=verified`. DKIM đúng là đủ để verify, SPF/DMARC là cảnh báo.
143
+ 5. `mail_api_key_create` với `name` của app và `mode: "live"` hoặc `"test"`. Key chỉ trả một lần; ghi thẳng vào `.env` dưới tên `MONAMAIL_API_KEY`, bảo đảm `.env` không vào git. Chỉ dùng prefix để nhận diện key trong báo cáo. MCP vẫn dùng MONA Pass, app dùng key của SDK.
144
+ 6. Tích hợp SDK `monamail` tại server; sinh OTP ngẫu nhiên, có hạn dùng và giới hạn số lần thử. Gửi bằng `monamail.emails.send`, sau đó kiểm trạng thái qua `mail_status`.
145
+ 7. Viết HTTPS endpoint cho `email.bounced`, đăng ký `mail_webhook_create`, lưu secret riêng trong `.env`, gọi `mail_webhook_test`. Payload test là `email.delivered`, endpoint cần tiếp nhận mẫu này để kiểm đường truyền. Xem `mail_suppressions_list` khi địa chỉ bị ngừng gửi; `mail_suppression_remove` chỉ gỡ lớp account, API có thể trả `global_suppression` cho lớp toàn hệ.
146
+
147
+ Ví dụ code Node.js trong app sau khi có key và domain verified:
148
+
149
+ ```ts
150
+ import { MonaMail } from 'monamail';
151
+
152
+ const monamail = new MonaMail(process.env.MONAMAIL_API_KEY);
153
+ const { id } = await monamail.emails.send({
154
+ from: 'Shop <noreply@shop.vn>',
155
+ to: emailNguoiNhan,
156
+ subject: 'Mã OTP',
157
+ text: `Mã OTP: ${otp}`,
158
+ tags: ['otp'],
159
+ idempotency_key: requestId,
160
+ });
161
+ ```
162
+
163
+ `emailNguoiNhan`, `otp` và `requestId` lấy từ luồng xác thực của app. Dùng `MonaMail.verifyWebhook({ secret, timestamp, body, signature })` kiểm `X-Mona-Signature` trên `"<X-Mona-Timestamp>.<raw_body>"`. Chống replay theo timestamp, xử lý event một lần theo payload `id`/`X-Mona-Event-Id`.
164
+
165
+ Ranh giới human của luồng Mail sau đăng nhập là **thêm DNS và nạp tiền**. Khi `mail_plan_set` trả `insufficient_funds`, gọi `cloud_topup`, đưa VietQR cho user và chờ `cloud_balance` cập nhật trước khi thử lại. Không yêu cầu user mở dashboard lấy API key. Lỗi Mail giữ `code`, `message`, `next_step`, `request_id` từ API, gồm `domain_not_verified`, `quota_exceeded`, `budget_exceeded` và `idempotency_conflict`.
166
+
167
+ ### Sandbox và idempotency Mail
168
+
169
+ - MCP: `mail_send({ ..., sandbox: true })` gửi `X-Mona-Sandbox: 1`, kết quả có `sandbox: true`.
170
+ - App: key `mm_test_` bật sandbox qua SDK; key `mm_live_` dùng để gửi thật. `MONACLOUD_SANDBOX` dành cho compute, không tự bật sandbox Mail.
171
+ - Sandbox đi qua pipeline nhưng không gửi ra Internet, không tính quota hoặc trừ ví. Dùng `mail_status` để đọc trạng thái `sandbox` và `sandbox_preview`; không báo thư sandbox đã được giao thật.
172
+ - Mọi POST Mail có header `Idempotency-Key`: dùng tham số `idempotency_key` hoặc MCP tự tạo `mcp-mail-<uuid>`. Khi retry phải truyền cùng key và giữ nguyên body trong TTL 24 giờ. Cùng key/body trả response cũ, khác body trả 409 `idempotency_conflict`. Nếu bỏ key, mỗi lần gọi MCP là yêu cầu mới.
173
+
131
174
  ## Spend guard
132
175
 
133
176
  `cloud_vps_create`, `cloud_db_create`, `cloud_service_start` và `cloud_service_rebuild` đọc `GET /v1/balance` trước khi gọi sản phẩm thật. Upstream vẫn là nguồn quyết định cuối và có thể trả:
@@ -168,7 +211,12 @@ Sandbox không gọi Proxmox, không cấp IP thật, không trừ tiền và c
168
211
  - Đọc `monacloud://llms` khi cần bối cảnh toàn hệ sinh thái.
169
212
  - Đọc `monacloud://status` trước khi kết luận lỗi là do credential hoặc request.
170
213
  - Dùng prompt `dung-app-ban-hang-monacloud` để lấy chuỗi tác vụ chuẩn.
214
+ - Dùng prompt `gui-mail-otp-monamail` để tích hợp OTP, DNS, API key và webhook Mail.
171
215
 
172
216
  ## Tiêu chí hoàn tất
173
217
 
174
218
  Một lượt được coi là xong khi app và database chạy, webhook HMAC test pass, sandbox checkout chuyển `paid`, credential nằm trong secret store, và agent báo lại URL cùng trạng thái dịch vụ. Không coi việc “đã tạo job” là hoàn tất; phải poll tới terminal state và kiểm health thực tế.
219
+
220
+ ## Wave A/B (0.3.0)
221
+
222
+ Xem [bảng tool và hợp đồng HTTP](wave-ab.md) cho 15 tool mới, VPS monthly, subscriptions, invoices/PDF, credit và app từ git. Khi user nói “deploy repo”, dùng `cloud_app_create`; `agent_deploy` vẫn là stub cho runtime template. Đọc → ước tính → hỏi duyệt nếu chưa được duyệt → làm. Chưa có app host thì sandbox trước, sau khi hoàn tất kiểm URL thật.
@@ -0,0 +1,73 @@
1
+ # Wave A/B — gói tháng, hoá đơn và app từ git (0.3.0)
2
+
3
+ Luồng mặc định: **đọc → ước tính VND → hỏi duyệt nếu chưa được duyệt → làm → kiểm kết quả**. Các tool dưới đây đều có alias `vibecloud_` cùng hậu tố, schema và hành vi; riêng alias VPS cũ vẫn là `vibecloud_create_vps`.
4
+
5
+ ## Bảng tool
6
+
7
+ | Tool | HTTP compute | Dùng khi |
8
+ |---|---|---|
9
+ | `cloud_plan_list` | GET `/api/plans` | Xem giá tháng/năm, cấu hình, included_db/backup; gợi ý gói rẻ nhất đủ CPU/RAM/đĩa yêu cầu, không tự chọn admin_only |
10
+ | `cloud_vps_create` | POST `/api/lxc` | Hourly như cũ; monthly nhận `plan_code`, `period: month|year`, trả `estimate` và job |
11
+ | `cloud_subscription_list` | GET `/api/subscriptions` | Đọc gói, kỳ gia hạn, auto-renew |
12
+ | `cloud_subscription_update` | POST `/api/services/{service_id}/subscription` | `plan_code`, `period`, `auto_renew`, `cancel_action: hourly|stop`; ít nhất một thay đổi |
13
+ | `cloud_invoice_list` | GET `/api/invoices` | Đọc hoá đơn tài khoản |
14
+ | `cloud_invoice_pdf` | GET `/api/invoices/{invoice_id}.pdf` | Lưu nguyên bytes vào file tạm 0600 trong thư mục riêng 0700, trả `path` |
15
+ | `cloud_credit_redeem` | POST `/api/credit-codes/redeem` | Body `{code}`, trim/uppercase; không tự đoán mã |
16
+ | `cloud_app_create` | POST `/api/apps` → GET `/api/jobs/{id}` | Deploy public repo HTTPS, tự poll và trả URL; **đang mở** |
17
+ | `cloud_app_list` | GET `/api/apps` | Tránh tạo trùng app; **đang mở** |
18
+ | `cloud_app_get` | GET `/api/apps/{app_id}` | Status, URL, last_deploy, app host; **đang mở** |
19
+ | `cloud_app_deploy` | POST `/api/apps/{app_id}/deploy` → job | Deploy lại sau sửa code/env; **đang mở** |
20
+ | `cloud_app_env_set` | PUT `/api/apps/{app_id}/env` | Body `{env: {KEY: "value"}}`, thay toàn bộ env; deploy lại để áp dụng; **đang mở** |
21
+ | `cloud_app_domain_add` | POST `/api/apps/{app_id}/domains` | Body `{host}`, trả hướng dẫn CNAME; **đang mở** |
22
+ | `cloud_app_logs` | GET `/api/apps/{app_id}/logs?deployment=...` | Tối đa 500 dòng, giữ log có secret riêng tư; **đang mở** |
23
+ | `cloud_app_delete` | DELETE `/api/apps/{app_id}` | Xoá app/domain/A record sau khi được duyệt; host vẫn có thể tính phí; **đang mở** |
24
+ | `cloud_app_host_list` | GET `/api/app-hosts` | Đọc host, tài nguyên và phí; **đang mở** |
25
+
26
+ ## Gói tháng/năm
27
+
28
+ ```text
29
+ cloud_plan_list({ ram_gb: 4 })
30
+ cloud_balance()
31
+ # Báo đúng price_month_vnd hoặc price_year_vnd, hỏi duyệt nếu chưa có.
32
+ cloud_vps_create({ app_name: "shop", billing_mode: "monthly", plan_code: "kinh-doanh", period: "month" })
33
+ cloud_job_status({ job_id: "<id trả về>" })
34
+ cloud_subscription_list()
35
+ cloud_subscription_update({ service_id: "<service_id>", auto_renew: false, cancel_action: "stop" })
36
+ cloud_invoice_list()
37
+ cloud_invoice_pdf({ invoice_id: "<invoice_id>" })
38
+ cloud_credit_redeem({ code: "<mã người dùng cung cấp>" })
39
+ ```
40
+
41
+ Monthly bỏ `package_slug`, CPU/RAM/đĩa trong payload trước khi gửi, lấy cấu hình từ plan. Spend guard so sánh số dư với **toàn bộ giá kỳ đã chọn**, không chỉ số dư dương. Plan giá 0 được phép với ví 0; API vẫn kiểm quyền gói admin. Giá đọc từ API, không hard-code. Nâng gói tính prorate và hạ gói kỳ sau do backend quyết định; MCP không tự tính khoản prorate hoặc chặn huỷ khi ví hết tiền.
42
+
43
+ Wave A hiện từ chối monthly trong sandbox. `cloud_vps_create` monthly + sandbox lấy CPU/RAM/đĩa từ plan, gửi request **hourly sandbox** với `X-Vibecloud-Sandbox: 1`; trả `estimate` là giá gói thật, `requested_billing_mode: monthly`, `preview_billing_mode: hourly`. Không tạo subscription và không trừ tiền. Được duyệt rồi mới gọi monthly thật, tắt cả tham số sandbox và `MONACLOUD_SANDBOX` nếu env đang bật.
44
+
45
+ PDF là file tạm trên máy chạy MCP, không phải máy từ xa của người chat. Sao chép file nếu cần giữ lâu; hệ điều hành có thể dọn thư mục tạm. Không dùng filename từ server làm đường dẫn local, không follow redirect có bearer token, không chuyển PDF qua text/JSON.
46
+
47
+ ## Deploy repo: 3 bước
48
+
49
+ 1. Đọc repo/nhánh/build/domain, `cloud_app_list`, `cloud_app_host_list`, `cloud_prices`, `cloud_packages`. Nếu chưa có host, gọi `cloud_app_create` sandbox trước để lấy `result.estimated_app_host` (MCP cũng trả thành `estimate`) và URL thử. Đọc `cloud_balance` trước tạo thật.
50
+ 2. Báo chi phí host, repo, nhánh và domain, hỏi duyệt nếu chưa được duyệt. Gói tháng cần giá đúng kỳ; app dùng host hiện có vẫn tiếp tục phí host. Sandbox không cần ví, không cần duyệt chi phí.
51
+ 3. `cloud_app_create` thật, chờ terminal job, đọc `cloud_app_get`/`cloud_app_logs`, kiểm HTTPS/health rồi báo URL. Khi có host, truyền `app_host_id` để dùng lại. Custom domain dùng `cloud_app_domain_add`, làm theo CNAME và chờ SSL.
52
+
53
+ ```text
54
+ cloud_app_create({ repo_url: "https://github.com/example/shop.git", branch: "main", build_type: "nixpacks", port: 3000, env: {}, sandbox: true })
55
+ # Đọc estimate, duyệt, rồi:
56
+ cloud_app_create({ repo_url: "https://github.com/example/shop.git", branch: "main", build_type: "nixpacks", port: 3000, env: {}, sandbox: false })
57
+ cloud_app_env_set({ app_id: "<app_id>", env: { NODE_ENV: "production" } })
58
+ cloud_app_deploy({ app_id: "<app_id>" })
59
+ cloud_app_domain_add({ app_id: "<app_id>", host: "shop.example.vn" })
60
+ cloud_app_logs({ app_id: "<app_id>", deployment: "<deployment_id>" })
61
+ ```
62
+
63
+ `cloud_app_create` mặc định branch=`main`, build_type=`dockerfile`, dockerfile=`Dockerfile`, env=`{}`, port=`3000`, wait=`true`, interval_sec=`3`, timeout_sec=`600`. Build có `dockerfile|nixpacks|static`. `wait: false` trả job để client tự poll; nên dùng nếu MCP host giới hạn mỗi tool dưới 10 phút. `done`/`succeeded` là hoàn tất; `failed`/`error`/`cancelled` trả lỗi `app_deploy_failed`. Timeout giữ job_id và next_step để poll tiếp; không tự gửi lại POST.
64
+
65
+ Wave B hiện nhận **public HTTPS repo** không credential/query/fragment. CLI đổi remote `git@host:owner/repo.git` hoặc `ssh://git@host/owner/repo.git` sang HTTPS; điều đó không cấp quyền repo private. Không đưa token vào URL. Env là map string, tối đa 200 key/64 KiB; không in secret hoặc log build ra công khai.
66
+
67
+ Sandbox app dùng `X-Vibecloud-Sandbox: 1` cho các endpoint app. Job được tìm theo ID; không cần header khi poll. `MONACLOUD_SANDBOX=1` cũng bật sandbox và không thể bị `sandbox:false` ghi đè. Lỗi 404/503 khi endpoint chưa mở được trả nguyên theo API, không có thành công giả.
68
+
69
+ Prompt mẫu: “Deploy repo hiện tại lên MONA Cloud. Kiểm host và giá, sandbox trước nếu chưa có host, báo chi phí để tôi duyệt rồi deploy thật và kiểm URL.”
70
+
71
+ ## Nguồn hợp đồng offline
72
+
73
+ Snapshot `ctx/openapi.json` được giao vẫn ghi API `0.2.0`, chưa có Wave A/B. Đã đối chiếu **file local** `vibecloud/app/schemas.py`, `app/routers/api.py` cho Wave A; Wave B theo `ctx/BRIEF-CODEX-WAVE-B-APPS.md` §2–4 và đối chiếu thêm `vibecloud/app/routers/apps.py`, `app/sandbox.py`. Vì brief cũ trong `SPEC-WAVE-B-APP-TU-GIT.md` ghi POST env và `?deploy=`, implementation dùng hợp đồng mới hơn: **PUT env, `?deployment=`**. Không sửa snapshot cũ để tránh gán nhầm provenance; không gọi endpoint production.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monacloud-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Unified MCP server for MONA Cloud, MONA Pay and MONA Agent.",
5
5
  "license": "MIT",
6
6
  "type": "module",