monacloud-mcp 0.3.0 → 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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `monacloud-mcp` là MCP hợp nhất của MONA Cloud: cài một lần, đăng nhập một MONA Pass và dùng chung ví VND để quản lý tài khoản, chạy app/VPS, tích hợp MONA Pay, gửi email giao dịch bằng MONA Mail và đọc catalog MONA Agent ngay trong Claude Code, Codex hoặc Cursor.
4
4
 
5
- Human chỉ cần đăng ký, thêm DNS khi cần, nạp tiền và cung cấp OTP/KYC khi bắt buộc. Các bước tạo tài nguyên, đọc trạng thái, cấu hình webhook, test và deploy được thiết kế để AI agent làm qua MCP.
5
+ Human đăng ký, duyệt chi phí, thêm DNS khi cần, nạp tiền và cung cấp OTP/KYC khi bắt buộc. Các bước tạo tài nguyên, đọc trạng thái, cấu hình webhook, test và deploy được thiết kế để AI agent làm qua MCP.
6
6
 
7
7
  ## Yêu cầu
8
8
 
@@ -76,7 +76,7 @@ Tên tool dùng `snake_case` để giữ tương thích với prompt MONA Pay c
76
76
 
77
77
  ### MONA Pay
78
78
 
79
- `monacloud-mcp` import `monapay-mcp` và re-export toàn bộ tool của package, không copy implementation. Dependency range là `^0.3.0`; bộ dependency offline tại lần verify này là 0.5.5 với 47 tool MONA Pay, gồm các nhóm:
79
+ `monacloud-mcp` import `monapay-mcp` và re-export toàn bộ tool của package, không copy implementation. Dependency range là `^0.5.5`; bộ dependency offline tại lần verify này là 0.5.5 với 47 tool MONA Pay, gồm các nhóm:
80
80
 
81
81
  - hồ sơ và nối ACB/VA bằng hai lần OTP;
82
82
  - payment profile, checkout, VietQR, sandbox transaction và đối soát;
@@ -91,7 +91,7 @@ Các tên cũ như `monapay_create_qr`, `monapay_link_bank_start`, `monapay_crea
91
91
  | Tool | Công dụng |
92
92
  |---|---|
93
93
  | `cloud_link` | Xác nhận direct MONA Pass; chỉ đổi sang `vc_live_*` nếu upstream còn ở chế độ cũ |
94
- | `cloud_vps_create` | Tạo LXC theo `package_slug` hoặc CPU/RAM/đĩa |
94
+ | `cloud_vps_create` | Hourly theo cấu hình; monthly theo `plan_code`, `period: month/year`, kiểm ví đủ giá kỳ |
95
95
  | `cloud_db_create` | Tạo MongoDB, PostgreSQL hoặc MySQL |
96
96
  | `cloud_job_status` | Poll job tới trạng thái cuối, có timeout |
97
97
  | `cloud_services_list` | Liệt kê VPS/database |
@@ -160,7 +160,7 @@ cloud_services_list({ sandbox: true })
160
160
 
161
161
  - `monacloud://llms`: mô tả máy đọc của toàn stack MONA Cloud.
162
162
  - `monacloud://status`: health tổng hợp MONA Pass, Billing, compute MONA Cloud, MONA Pay và MONA Mail (`/v1/healthz`).
163
- - Prompt `dung-app-ban-hang-monacloud`: chuỗi VPS → database → VA/QR → webhook → deploy, chỉ dừng để hỏi nạp tiền hoặc OTP bắt buộc.
163
+ - Prompt `dung-app-ban-hang-monacloud`: chuỗi VPS → database → VA/QR → webhook → deploy, đọc giá, duyệt chi phí rồi tạo; repo git dùng `cloud_app_create` (đang mở).
164
164
  - Prompt `gui-mail-otp-monamail(app_name?, framework?, domain?)`: account → thử onboarding → domain/DNS → verify → API key → `.env` → SDK OTP → webhook bounced; chỉ dừng ở bước thêm DNS hoặc nạp tiền.
165
165
 
166
166
  ## Spend guard và lỗi cho AI
@@ -237,3 +237,33 @@ node --test
237
237
  ```
238
238
 
239
239
  Test dùng Node built-in, MCP transport thật qua stdio, process con và mock HTTP local. Trong sandbox cấm bind socket, test tự dùng fetch fixture tương đương. Bộ test không gọi Internet.
240
+
241
+ ## Mới ở 0.3.0: gói tháng, hoá đơn và app từ git
242
+
243
+ | Tool mới | Công dụng |
244
+ |---|---|
245
+ | `cloud_plan_list` | Bảng gói và giá tháng/năm, gợi ý theo CPU/RAM/đĩa |
246
+ | `cloud_subscription_list` / `cloud_subscription_update` | Đọc gói, đổi chu kỳ, auto-renew; huỷ với `cancel_action: hourly/stop` |
247
+ | `cloud_invoice_list` / `cloud_invoice_pdf` | Xem hoá đơn và tải PDF về file tạm riêng tư |
248
+ | `cloud_credit_redeem` | Dùng mã credit được người dùng cung cấp |
249
+ | `cloud_app_create` / `cloud_app_list` / `cloud_app_get` | Deploy public repo HTTPS, poll job và đọc URL; **đang mở** |
250
+ | `cloud_app_deploy` / `cloud_app_env_set` | Deploy lại, thay env; **đang mở** |
251
+ | `cloud_app_domain_add` / `cloud_app_logs` / `cloud_app_delete` | CNAME domain, log build, xoá app; **đang mở** |
252
+ | `cloud_app_host_list` | Xem host và tài nguyên/chi phí; **đang mở** |
253
+
254
+ 15 tool mới có 15 alias `vibecloud_` tương ứng. Tổng với MONA Pay 0.5.5 đang có: **133 tool**, gồm 20 Mail và 26 alias compute. Package/MCP binary version: **0.3.0**.
255
+
256
+ ```text
257
+ cloud_plan_list({ ram_gb: 4 })
258
+ # Đọc ví, báo giá, chờ duyệt rồi tạo:
259
+ cloud_vps_create({ app_name: "shop", billing_mode: "monthly", plan_code: "kinh-doanh", period: "month" })
260
+ cloud_invoice_pdf({ invoice_id: "<id>" })
261
+ cloud_app_host_list()
262
+ cloud_app_create({ repo_url: "https://github.com/example/shop.git", branch: "main", build_type: "nixpacks", sandbox: true })
263
+ ```
264
+
265
+ Monthly bỏ qua CPU/RAM/đĩa và package_slug, đọc cấu hình/giá từ plan và guard toàn bộ giá tháng/năm. Backend chưa nhận monthly sandbox: MCP thử cấu hình plan qua hourly sandbox 0đ, trả giá thật trong `estimate`, không tạo subscription. Khi tạo thật phải tắt `MONACLOUD_SANDBOX` nếu đã bật env.
266
+
267
+ Khi user nói “deploy repo”, đọc host; chưa có host thì sandbox `cloud_app_create` để ước tính, hỏi duyệt rồi tạo thật và kiểm URL. Tool chờ job tối đa 600 giây; dùng `wait:false` và `cloud_job_status` nếu host MCP có timeout ngắn. Endpoint Wave B đang mở, lỗi API được báo thật.
268
+
269
+ Hợp đồng HTTP, đầy đủ tham số, ví dụ billing/deploy, PDF và giới hạn rollout: [docs/wave-ab.md](docs/wave-ab.md).
package/STATUS.md CHANGED
@@ -2,6 +2,46 @@
2
2
 
3
3
  Updated: 2026-09-05 (Asia/Ho_Chi_Minh)
4
4
 
5
+ ## 0.3.0 — Wave A/B hoàn tất (05/09/2026)
6
+
7
+ - [x] 6 tool billing: `cloud_plan_list`, `cloud_subscription_list`, `cloud_subscription_update`, `cloud_invoice_list`, `cloud_invoice_pdf`, `cloud_credit_redeem`.
8
+ - [x] `cloud_vps_create`/`vibecloud_create_vps` nhận hourly/monthly, plan_code, month/year. Monthly bỏ sizing hourly; estimate và spend guard dùng đúng toàn bộ giá kỳ từ API, gồm gói 0đ.
9
+ - [x] 9 tool app từ git: `cloud_app_create`, `cloud_app_list`, `cloud_app_get`, `cloud_app_deploy`, `cloud_app_env_set`, `cloud_app_domain_add`, `cloud_app_logs`, `cloud_app_delete`, `cloud_app_host_list`. Mô tả VI/EN ghi **đang mở**, gọi endpoint thật qua client; test HTTP mock.
10
+ - [x] 15 alias `vibecloud_` mới, dùng chung schema/handler. Tổng stdio **133 tool = 83 cũ + 20 Mail + 30 Wave A/B**, 2 resource, 2 prompt. Giữ nguyên tích hợp Mail/MONA Pay đang có.
11
+ - [x] App create/deploy poll tối đa 600 giây; trả URL/app_id/application_id, chuẩn hoá `result.estimated_app_host` thành `estimate`. Job error/failed/cancelled trả `app_deploy_failed`; timeout giữ job_id, không POST lại.
12
+ - [x] Sandbox header `X-Vibecloud-Sandbox: 1`, env `MONACLOUD_SANDBOX=1`, bỏ Billing; job poll theo ID. Monthly sandbox thử cấu hình plan qua **hourly sandbox**, báo rõ chưa tạo subscription vì Wave A từ chối monthly sandbox.
13
+ - [x] PDF giữ nguyên binary, file 0600/thư mục tạm 0700, không dùng remote filename hay follow redirect. Lỗi HTTP không tạo file giả.
14
+ - [x] Instructions/resource, prompt dựng app, agent_* và catalog hướng “deploy repo” sang cloud_app_create; đọc → ước tính → duyệt nếu cần → làm, sandbox trước khi chưa có host.
15
+ - [x] MCP và CLI cùng version **0.3.0**. README, [docs/wave-ab.md](docs/wave-ab.md), agent guide và CLI docs có bảng tool/lệnh, ví dụ, giới hạn rollout.
16
+
17
+ Ví dụ gọi:
18
+
19
+ ```text
20
+ cloud_plan_list({ ram_gb: 4 })
21
+ cloud_vps_create({ app_name: "shop", billing_mode: "monthly", plan_code: "kinh-doanh", period: "month" })
22
+ cloud_subscription_update({ service_id: "<id>", auto_renew: false, cancel_action: "stop" })
23
+ cloud_invoice_pdf({ invoice_id: "<id>" })
24
+ cloud_credit_redeem({ code: "<mã đã duyệt>" })
25
+ cloud_app_create({ repo_url: "https://github.com/example/shop.git", build_type: "nixpacks", sandbox: true })
26
+ ```
27
+
28
+ Gate cuối:
29
+
30
+ ```text
31
+ mcp: npm test → 35 tests, 35 pass, 0 fail, 0 skip (bao gồm tsc)
32
+ cli: npm test → 31 tests, 31 pass, 0 fail, 0 skip
33
+ ```
34
+
35
+ CLI có test executable → MCP thật → HTTP mock cho plans, VPS monthly, PDF, sandbox, duyệt chi phí và deploy/build lỗi. Stdio xác nhận version 0.3.0, 133 tool và không có stderr chứa secret.
36
+
37
+ Nguồn hợp đồng: snapshot `ctx/openapi.json` vẫn là **0.2.0**, thiếu Wave A/B. Đối chiếu offline schema/router Wave A và Wave B tại repo `vibecloud` trên cùng máy; giữ snapshot gốc và ghi provenance trong docs. Wave B public HTTPS repo, PUT env, query `deployment`, estimate `result.estimated_app_host`. CLI chuyển SSH remote phổ biến sang public HTTPS tương đương, không cấp quyền private repo.
38
+
39
+ Không chạy npm install, không gọi Internet/production, không publish. Toàn bộ phát triển/test trong hai workspace và thư mục tạm. Wave B live chưa kiểm vì brief yêu cầu offline.
40
+
41
+ Kiểm tra đóng gói offline: `npm pack --dry-run --json --offline --ignore-scripts` PASS (MCP 30 file), đủ source build/template/docs mới. Dùng npm CLI trực tiếp với config rỗng và `NODE_USE_SYSTEM_CA=0` để tránh lỗi Keychain/SecItemCopyMatching của Node trên runner. Không tạo tarball và không publish.
42
+
43
+ CODEX DONE
44
+
5
45
  ## 0.3.0 — MONA Mail
6
46
 
7
47
  - [x] Thêm đủ 20 tool `mail_*` theo CONTRACT-MONA-MAIL-API §5 trong `src/mail.ts`, đăng ký sau nhóm compute `cloud_*`.
package/dist/clients.d.ts CHANGED
@@ -33,6 +33,7 @@ export declare class CloudClients {
33
33
  body?: unknown;
34
34
  query?: Record<string, string | number | undefined>;
35
35
  headers?: Record<string, string>;
36
+ timeoutMs?: number;
36
37
  }): Promise<T>;
37
38
  guardedVibecloud(path: string, options?: {
38
39
  method?: string;
package/dist/clients.js CHANGED
@@ -124,6 +124,7 @@ export class CloudClients {
124
124
  }
125
125
  async vibecloud(path, options = {}) {
126
126
  return requestJson(`${this.config.vibecloudApi}${path}`, {
127
+ timeoutMs: 30_000,
127
128
  ...options,
128
129
  token: await this.vibecloudToken(),
129
130
  fetchImpl: this.fetchImpl,
package/dist/compute.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { CloudClients } from './clients.js';
3
3
  type Row = Record<string, unknown>;
4
- export declare const APP_FLOW = "Khi user n\u00F3i \"deploy repo\", d\u00F9ng cloud_app_create (app t\u1EEB git \u0111ang m\u1EDF). \u0110\u1ECDc cloud_app_host_list, cloud_prices/cloud_packages v\u00E0 cloud_balance; n\u1EBFu ch\u01B0a c\u00F3 app host th\u00EC cloud_app_create sandbox=true tr\u01B0\u1EDBc \u0111\u1EC3 l\u1EA5y \u01B0\u1EDBc t\u00EDnh. \u0110\u1ECDc \u2192 \u01B0\u1EDBc t\u00EDnh VND \u2192 h\u1ECFi duy\u1EC7t n\u1EBFu ch\u01B0a \u0111\u01B0\u1EE3c duy\u1EC7t \u2192 l\u00E0m th\u1EADt \u2192 poll t\u1EDBi done/succeeded \u2192 ki\u1EC3m URL. Kh\u00F4ng g\u1ECDi agent_deploy cho repo git. / Deploy a repository with cloud_app_create; inspect, estimate, obtain approval, then deploy.";
4
+ export declare const APP_FLOW = "Khi user n\u00F3i \"deploy repo\", d\u00F9ng cloud_app_create (app t\u1EEB git \u0111\u00E3 live: repo public \u2192 URL https). \u0110\u1ECDc cloud_app_host_list, cloud_prices/cloud_packages v\u00E0 cloud_balance; n\u1EBFu ch\u01B0a c\u00F3 app host th\u00EC cloud_app_create sandbox=true tr\u01B0\u1EDBc \u0111\u1EC3 l\u1EA5y \u01B0\u1EDBc t\u00EDnh. \u0110\u1ECDc \u2192 \u01B0\u1EDBc t\u00EDnh VND \u2192 h\u1ECFi duy\u1EC7t n\u1EBFu ch\u01B0a \u0111\u01B0\u1EE3c duy\u1EC7t \u2192 l\u00E0m th\u1EADt \u2192 poll t\u1EDBi done/succeeded \u2192 ki\u1EC3m URL. Kh\u00F4ng g\u1ECDi agent_deploy cho repo git. / Deploy a repository with cloud_app_create; inspect, estimate, obtain approval, then deploy.";
5
5
  export declare function rows(value: unknown, key: string): Row[];
6
6
  export declare function planEstimate(clients: CloudClients, code: string, cycle?: string): Promise<{
7
7
  plan: Row;
package/dist/compute.js CHANGED
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
  import { CloudError, runTool } from './errors.js';
3
3
  import { unwrapData } from './http.js';
4
4
  import { finishAppJob } from './jobs.js';
5
- export const APP_FLOW = 'Khi user nói "deploy repo", dùng cloud_app_create (app từ git đang mở). Đọc cloud_app_host_list, cloud_prices/cloud_packages và cloud_balance; nếu chưa có app host thì cloud_app_create sandbox=true trước để lấy ước tính. Đọc → ước tính VND → hỏi duyệt nếu chưa được duyệt → làm thật → poll tới done/succeeded → kiểm URL. Không gọi agent_deploy cho repo git. / Deploy a repository with cloud_app_create; inspect, estimate, obtain approval, then deploy.';
5
+ export const APP_FLOW = 'Khi user nói "deploy repo", dùng cloud_app_create (app từ git đã live: repo public → URL https). Đọc cloud_app_host_list, cloud_prices/cloud_packages và cloud_balance; nếu chưa có app host thì cloud_app_create sandbox=true trước để lấy ước tính. Đọc → ước tính VND → hỏi duyệt nếu chưa được duyệt → làm thật → poll tới done/succeeded → kiểm URL. Không gọi agent_deploy cho repo git. / Deploy a repository with cloud_app_create; inspect, estimate, obtain approval, then deploy.';
6
6
  const sandbox = z.boolean().optional().describe('Thử 0đ, không tạo hạ tầng thật / Sandbox, no charge');
7
7
  const id = z.string().trim().min(1).max(255);
8
8
  const planCode = z.string().trim().min(2).max(64);
@@ -70,33 +70,32 @@ export function registerComputeTools(server, clients) {
70
70
  register('cloud_invoice_list', 'Đọc hoá đơn hàng tháng của tài khoản. / List monthly invoices.', z.object({}).strict(), () => clients.vibecloud('/api/invoices'));
71
71
  register('cloud_invoice_pdf', 'Tải PDF hoá đơn vào file tạm riêng tư, trả path; sao chép ra nơi cần giữ trước khi hệ điều hành dọn. / Download invoice PDF to a private temporary file.', z.object({ invoice_id: id }).strict(), ({ invoice_id }) => clients.invoicePdf(invoice_id));
72
72
  register('cloud_credit_redeem', 'Dùng mã credit người dùng cung cấp sau khi họ đồng ý; không thử đoán mã. / Redeem an approved promotional credit code.', z.object({ code: z.string().trim().min(2).max(64) }).strict(), ({ code }) => clients.vibecloud('/api/credit-codes/redeem', { method: 'POST', body: { code: code.toUpperCase() } }));
73
- const appNotice = 'App từ git đang mở; endpoint thể chưa live. / Git apps rollout in progress. ';
73
+ const appNotice = 'App từ git đã live: app host đầu tiên ~2–3 phút, deploy sau đó 10–20 giây. / Git apps are live. ';
74
74
  const poll = { wait: z.boolean().default(true), interval_sec: z.number().int().min(1).max(30).default(3), timeout_sec: z.number().int().min(1).max(600).default(600) };
75
- const env = z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), z.string());
75
+ const env = z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), z.string().refine((value) => !value.includes('\0'), 'Env không chứa NUL.')).refine((value) => Object.keys(value).length <= 200 && Object.entries(value).reduce((sum, [key, val]) => sum + key.length + val.length, 0) <= 65536, 'Env tối đa 200 key / 64 KiB.');
76
76
  const domain = z.string().trim().min(3).max(253).regex(/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$/, 'Chỉ hostname, không URL/path.');
77
77
  const appId = z.object({ app_id: id, sandbox }).strict();
78
+ const gitPath = z.string().trim().min(1).max(255).refine((value) => !/[\x00-\x1f]/.test(value) && !value.startsWith('-') && !value.startsWith('/') && !value.split('/').includes('..'), 'Nhánh/path git không hợp lệ.');
78
79
  const repo = z.string().trim().min(1).max(2048).refine((value) => {
79
- if (/^git@[a-zA-Z0-9.-]+:[^\s]+$/.test(value))
80
- return true;
81
80
  try {
82
81
  const u = new URL(value);
83
- return ['https:', 'ssh:'].includes(u.protocol) && !u.password && (u.protocol === 'ssh:' || !u.username) && !u.search && !u.hash && u.pathname.length > 1;
82
+ return u.protocol === 'https:' && !u.port && !u.password && !u.username && !u.search && !u.hash && u.pathname.length > 1;
84
83
  }
85
84
  catch {
86
85
  return false;
87
86
  }
88
- }, 'Dùng HTTPS hoặc SSH git URL không chứa token/mật khẩu.');
87
+ }, 'Dùng public HTTPS git URL không chứa token/mật khẩu; private repo chưa được hỗ trợ.');
89
88
  const compute = async (path, options, requested) => {
90
89
  const isSandbox = clients.sandboxEnabled(requested);
91
90
  const result = await clients.vibecloud(path, { ...options, ...(isSandbox ? { headers: { 'X-Vibecloud-Sandbox': '1' } } : {}) });
92
91
  return isSandbox ? { ...(Array.isArray(result) ? { items: result } : object(result)), sandbox: true } : result;
93
92
  };
94
- register('cloud_app_create', appNotice + APP_FLOW, z.object({ repo_url: repo, branch: z.string().trim().min(1).max(255).default('main'), build_type: z.enum(['dockerfile', 'nixpacks', 'static']).default('dockerfile'), dockerfile: z.string().min(1).max(1024).default('Dockerfile'), env: env.default({}), domain: domain.optional(), app_host_id: id.optional(), port: z.number().int().min(1).max(65535).default(3000), sandbox, ...poll }).strict(), async ({ sandbox: requested, wait, interval_sec, timeout_sec, ...body }) => finishAppJob(clients, await clients.guardedVibecloud('/api/apps', { method: 'POST', body }, requested), clients.sandboxEnabled(requested), wait, interval_sec, timeout_sec));
93
+ register('cloud_app_create', appNotice + APP_FLOW, z.object({ repo_url: repo, branch: gitPath.default('main'), build_type: z.enum(['dockerfile', 'nixpacks', 'static']).default('dockerfile'), dockerfile: gitPath.default('Dockerfile'), env: env.default({}), domain: domain.optional(), app_host_id: id.optional(), port: z.number().int().min(1).max(65535).default(3000), sandbox, ...poll }).strict(), async ({ sandbox: requested, wait, interval_sec, timeout_sec, ...body }) => finishAppJob(clients, await clients.guardedVibecloud('/api/apps', { method: 'POST', body }, requested), clients.sandboxEnabled(requested), wait, interval_sec, timeout_sec));
95
94
  register('cloud_app_list', appNotice + 'Liệt kê app trước khi tạo để tránh trùng. / List apps.', z.object({ sandbox }).strict(), ({ sandbox }) => compute('/api/apps', {}, sandbox));
96
95
  register('cloud_app_host_list', appNotice + 'Đọc app host; chưa có host thì sandbox cloud_app_create trước để ước tính. / List app hosts.', z.object({ sandbox }).strict(), ({ sandbox }) => compute('/api/app-hosts', {}, sandbox));
97
96
  register('cloud_app_get', appNotice + 'Đọc status, URL, lần deploy và app host. / Inspect app.', appId, ({ app_id, sandbox }) => compute(`/api/apps/${encodeURIComponent(app_id)}`, {}, sandbox));
98
97
  register('cloud_app_deploy', appNotice + 'Deploy lại sau khi đọc app/log và được duyệt; poll job tới kết quả. / Redeploy and poll.', appId.extend(poll), async ({ app_id, sandbox, wait, interval_sec, timeout_sec }) => finishAppJob(clients, await clients.guardedVibecloud(`/api/apps/${encodeURIComponent(app_id)}/deploy`, { method: 'POST' }, sandbox), clients.sandboxEnabled(sandbox), wait, interval_sec, timeout_sec));
99
- register('cloud_app_env_set', appNotice + 'Đặt env sau khi được duyệt, gửi đầy đủ map cần giữ; không log secret. / Set app environment.', appId.extend({ env }), ({ app_id, env, sandbox }) => compute(`/api/apps/${encodeURIComponent(app_id)}/env`, { method: 'PUT', body: { env } }, sandbox));
98
+ register('cloud_app_env_set', appNotice + 'Thay toàn bộ env sau khi được duyệt, gửi đầy đủ map cần giữ; không log secret. Gọi cloud_app_deploy sau đó để áp dụng. / Set app environment.', appId.extend({ env }), ({ app_id, env, sandbox }) => compute(`/api/apps/${encodeURIComponent(app_id)}/env`, { method: 'PUT', body: { env } }, sandbox));
100
99
  register('cloud_app_domain_add', appNotice + 'Thêm domain đã được duyệt, trả hướng dẫn CNAME từ API; chờ DNS trước kiểm HTTPS. / Attach custom domain.', appId.extend({ host: domain }), ({ app_id, host, sandbox }) => compute(`/api/apps/${encodeURIComponent(app_id)}/domains`, { method: 'POST', body: { host } }, sandbox));
101
100
  register('cloud_app_logs', appNotice + 'Đọc tối đa 500 dòng log; có thể chứa secret, không đưa nguyên log ra công khai. / Read deployment logs.', appId.extend({ deployment: id.optional() }), ({ app_id, deployment, sandbox }) => compute(`/api/apps/${encodeURIComponent(app_id)}/logs`, { query: { deployment } }, sandbox));
102
101
  register('cloud_app_delete', appNotice + 'Sau khi user duyệt xoá: xoá app/domain/A record; app host vẫn có thể tính phí. / Delete an approved app.', appId, ({ app_id, sandbox }) => compute(`/api/apps/${encodeURIComponent(app_id)}`, { method: 'DELETE' }, sandbox));
package/dist/jobs.d.ts CHANGED
@@ -3,6 +3,8 @@ export declare const TERMINAL_JOBS: Set<string>;
3
3
  export declare function pollJob(clients: CloudClients, jobId: string, wait: boolean, intervalSeconds: number, timeoutSeconds: number, sandbox: boolean): Promise<Record<string, unknown>>;
4
4
  export declare function finishAppJob(clients: CloudClients, created: unknown, sandbox: boolean, wait: boolean, interval: number, timeout: number): Promise<{
5
5
  job_id?: string | undefined;
6
+ estimate?: {} | undefined;
7
+ app_id?: string | undefined;
6
8
  url?: string | undefined;
7
9
  application_id?: string | undefined;
8
10
  sandbox?: boolean | undefined;
package/dist/jobs.js CHANGED
@@ -5,7 +5,9 @@ export const TERMINAL_JOBS = new Set(['done', 'succeeded', 'failed', 'cancelled'
5
5
  const object = (value) => (value && typeof value === 'object' ? value : {});
6
6
  export async function pollJob(clients, jobId, wait, intervalSeconds, timeoutSeconds, sandbox) {
7
7
  const deadline = Date.now() + timeoutSeconds * 1000;
8
- const read = async () => object(unwrapData(await clients.vibecloud(`/api/jobs/${encodeURIComponent(jobId)}`)));
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
+ })));
9
11
  let result = await read();
10
12
  while (wait && !TERMINAL_JOBS.has(String(result.status)) && Date.now() < deadline) {
11
13
  await delay(Math.min(intervalSeconds * 1000, Math.max(0, deadline - Date.now())));
@@ -32,6 +34,8 @@ export async function finishAppJob(clients, created, sandbox, wait, interval, ti
32
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.');
33
35
  }
34
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 } : {}),
35
39
  ...(typeof result.url === 'string' ? { url: result.url } : {}),
36
40
  ...(typeof result.application_id === 'string' ? { application_id: result.application_id } : {}),
37
41
  ...(sandbox ? { sandbox: true } : {}) };
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; các bước human gồm đăng ký MONA Pass, thêm DNS khi cần, 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` `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 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 duyệt rồi deploy thật. Dùng secret env, chạy migration 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;
@@ -216,3 +216,7 @@ Sandbox không gọi Proxmox, không cấp IP thật, không trừ tiền và c
216
216
  ## Tiêu chí hoàn tất
217
217
 
218
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.3.0",
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",