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/README.md +84 -6
- package/STATUS.md +79 -3
- package/dist/clients.d.ts +16 -2
- package/dist/clients.js +90 -7
- package/dist/compute.d.ts +18 -0
- package/dist/compute.js +102 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +1 -1
- package/dist/index.js +1 -1
- package/dist/jobs.d.ts +11 -0
- package/dist/jobs.js +42 -0
- package/dist/mail.d.ts +4 -0
- package/dist/mail.js +183 -0
- package/dist/server.js +60 -40
- package/dist/templates.js +1 -1
- package/docs/ai-agent.md +51 -3
- package/docs/wave-ab.md +73 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# monacloud-mcp
|
|
2
2
|
|
|
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 và đọc catalog MONA Agent ngay trong Claude Code, Codex hoặc Cursor.
|
|
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
|
|
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.
|
|
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` |
|
|
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 |
|
|
@@ -101,6 +101,41 @@ Các tên cũ như `monapay_create_qr`, `monapay_link_bank_start`, `monapay_crea
|
|
|
101
101
|
|
|
102
102
|
`cloud_service_stop` không bị chặn bởi số dư để người dùng luôn có thể hạn chế chi phí. Những lệnh tạo/start/rebuild thật đọc `GET /v1/balance` trước khi gọi compute MONA Cloud.
|
|
103
103
|
|
|
104
|
+
### MONA Mail
|
|
105
|
+
|
|
106
|
+
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.
|
|
107
|
+
|
|
108
|
+
20 tool `mail_*` dùng MONA Pass sẵn có; tài khoản Mail được tạo tự động ở request đầu. Site: https://monamail.vn, API: https://api.monamail.vn.
|
|
109
|
+
|
|
110
|
+
| Tên | Việc |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `mail_account` | Đọc tài khoản, email chủ, quota và bước kế tiếp |
|
|
113
|
+
| `mail_plans` | Đọc giá và quota hiện hành của các gói |
|
|
114
|
+
| `mail_plan_set` | Đổi gói, trừ ví VND khi chọn gói trả phí |
|
|
115
|
+
| `mail_send` | Gửi OTP/thông báo hoặc thử bằng `sandbox: true` |
|
|
116
|
+
| `mail_status` | Đọc trạng thái, events và nội dung sandbox |
|
|
117
|
+
| `mail_list` | Lọc lịch sử theo trạng thái, người nhận và thời gian |
|
|
118
|
+
| `mail_domain_add` | Thêm domain, trả records DNS và hướng dẫn |
|
|
119
|
+
| `mail_domain_verify` | Kiểm DKIM và xác minh domain |
|
|
120
|
+
| `mail_domain_cloudflare` | Thêm DNS bằng token Cloudflare của người dùng, dùng một lần, không lưu/log |
|
|
121
|
+
| `mail_domains_list` | Liệt kê domain cùng trạng thái |
|
|
122
|
+
| `mail_api_key_create` | Tạo key live/test, secret chỉ trả một lần |
|
|
123
|
+
| `mail_api_keys_list` | Xem prefix và trạng thái key |
|
|
124
|
+
| `mail_api_key_revoke` | Thu hồi key của app |
|
|
125
|
+
| `mail_webhook_create` | Đăng ký HTTPS endpoint và các sự kiện email |
|
|
126
|
+
| `mail_webhooks_list` | Liệt kê webhook |
|
|
127
|
+
| `mail_webhook_test` | Gửi payload `email.delivered` mẫu |
|
|
128
|
+
| `mail_suppressions_list` | Xem địa chỉ ngừng gửi và lý do |
|
|
129
|
+
| `mail_suppression_remove` | Gỡ suppression của tài khoản; không gỡ lớp toàn hệ |
|
|
130
|
+
| `mail_template_create` | Tạo mẫu với biến `{{ten_bien}}` |
|
|
131
|
+
| `mail_stats` | Đọc thống kê giao thư và bounce theo thời gian |
|
|
132
|
+
|
|
133
|
+
`mail_send` nhận 1 đến 50 người nhận, tối đa 10 tags, `subject` tối đa 998 ký tự và ít nhất một trong `html`/`text`. Khi dùng `template_id`, template thay cho `subject`/`html`/`text`. Sender `onboarding@monamail.vn` chỉ gửi tới email chủ từ `mail_account`; domain riêng cần verified trước khi gửi.
|
|
134
|
+
|
|
135
|
+
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. MCP tiếp tục dùng MONA Pass; app dùng SDK `monamail` với key `mm_live_` hoặc `mm_test_`. Sandbox bằng `mail_send({ ..., sandbox: true })` gửi header `X-Mona-Sandbox: 1` và trả `sandbox: true`; sandbox không gửi ra Internet, không tính quota hoặc trừ ví. Dùng `mail_status` để xem `sandbox_preview`.
|
|
136
|
+
|
|
137
|
+
Mọi POST gửi `Idempotency-Key` từ `idempotency_key` hoặc tự tạo `mcp-mail-<uuid>`. Truyền key ổn định khi cần retry: trong 24 giờ, cùng key và body trả response cũ; khác body trả `idempotency_conflict`. MCP đưa key vào header, không đưa `sandbox` vào body API. Giá gói lấy bằng `mail_plans`; lỗi thiếu ví từ `mail_plan_set` hướng dẫn gọi `cloud_topup`.
|
|
138
|
+
|
|
104
139
|
## Thử 0đ bằng sandbox
|
|
105
140
|
|
|
106
141
|
Truyền `sandbox: true` cho `cloud_vps_create`, `cloud_db_create`, `cloud_service_start`, `cloud_service_rebuild` hoặc `cloud_agent_deploy` để thử luồng mà không cần số dư ví. Có thể bật mặc định cho cả MCP process bằng `MONACLOUD_SANDBOX=1`; các alias `vibecloud_*` có cùng hành vi.
|
|
@@ -124,8 +159,9 @@ cloud_services_list({ sandbox: true })
|
|
|
124
159
|
## Resource và prompt
|
|
125
160
|
|
|
126
161
|
- `monacloud://llms`: mô tả máy đọc của toàn stack MONA Cloud.
|
|
127
|
-
- `monacloud://status`: health tổng hợp MONA Pass, Billing, compute MONA Cloud và MONA
|
|
128
|
-
- Prompt `dung-app-ban-hang-monacloud`: chuỗi VPS → database → VA/QR → webhook → deploy,
|
|
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, đọc giá, duyệt chi phí rồi tạo; repo git dùng `cloud_app_create` (đang mở).
|
|
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.
|
|
129
165
|
|
|
130
166
|
## Spend guard và lỗi cho AI
|
|
131
167
|
|
|
@@ -160,6 +196,17 @@ Agent thực hiện:
|
|
|
160
196
|
|
|
161
197
|
Chi tiết dành riêng cho agent: [`docs/ai-agent.md`](docs/ai-agent.md).
|
|
162
198
|
|
|
199
|
+
## Ví dụ một lượt: gửi mail OTP
|
|
200
|
+
|
|
201
|
+
Người dùng nói: “Tích hợp gửi mail OTP cho app shop bằng MONA Mail, domain shop.vn.”
|
|
202
|
+
|
|
203
|
+
1. `mail_account`: lấy email chủ, quota và domain đã xác minh.
|
|
204
|
+
2. Nếu chưa có domain, `mail_send` từ `onboarding@monamail.vn` tới email chủ, dùng idempotency key riêng; `mail_status` kiểm kết quả.
|
|
205
|
+
3. `mail_domain_add({ domain: "shop.vn" })`: đưa records để user thêm DNS, hoặc gọi `mail_domain_cloudflare` bằng token của họ; sau đó `mail_domain_verify`.
|
|
206
|
+
4. `mail_api_key_create({ name: "shop-otp", mode: "live" })`: ghi key trực tiếp vào `.env` dưới tên `MONAMAIL_API_KEY`.
|
|
207
|
+
5. Viết server dùng `new MonaMail(process.env.MONAMAIL_API_KEY)` và `monamail.emails.send(...)` với `tags: ["otp"]`; kiểm thư bằng `mail_status`.
|
|
208
|
+
6. Viết endpoint HMAC, `mail_webhook_create` với `events: ["email.bounced"]`, lưu secret rồi `mail_webhook_test`; khi thiếu ví gọi `cloud_topup` và chờ user nạp.
|
|
209
|
+
|
|
163
210
|
## Biến môi trường
|
|
164
211
|
|
|
165
212
|
| Biến | Mặc định | Ý nghĩa |
|
|
@@ -167,6 +214,7 @@ Chi tiết dành riêng cho agent: [`docs/ai-agent.md`](docs/ai-agent.md).
|
|
|
167
214
|
| `MONACLOUD_ISSUER` | `https://pass.monacloud.vn/realms/mona` | OIDC issuer |
|
|
168
215
|
| `MONACLOUD_BILLING_URL` | `https://billing.monacloud.vn` | Billing/ví API |
|
|
169
216
|
| `MONAPAY_API` | `https://api.monapay.vn` | MONA Pay API |
|
|
217
|
+
| `MONAMAIL_API` | `https://api.monamail.vn` | MONA Mail API, dùng Bearer MONA Pass của MCP |
|
|
170
218
|
| `MONACLOUD_API` | `https://api.monacloud.vn` | Compute API của MONA Cloud |
|
|
171
219
|
| `MONACLOUD_CONSOLE_URL` | `https://monacloud.vn/console` | URL trả cho bước human |
|
|
172
220
|
| `MONACLOUD_TOKEN` | — | PAT/access token ưu tiên token store |
|
|
@@ -189,3 +237,33 @@ node --test
|
|
|
189
237
|
```
|
|
190
238
|
|
|
191
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
|
@@ -1,8 +1,76 @@
|
|
|
1
|
-
# STATUS — monacloud-mcp 0.
|
|
1
|
+
# STATUS — monacloud-mcp 0.3.0
|
|
2
2
|
|
|
3
|
-
Updated: 2026-09-
|
|
3
|
+
Updated: 2026-09-05 (Asia/Ho_Chi_Minh)
|
|
4
4
|
|
|
5
|
-
##
|
|
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
|
+
|
|
45
|
+
## 0.3.0 — MONA Mail
|
|
46
|
+
|
|
47
|
+
- [x] Thêm đủ 20 tool `mail_*` theo CONTRACT-MONA-MAIL-API §5 trong `src/mail.ts`, đăng ký sau nhóm compute `cloud_*`.
|
|
48
|
+
- [x] `MONAMAIL_API` mặc định `https://api.monamail.vn`; `CloudClients.mail()` dùng `requestJson` và `auth.accessToken()` MONA Pass, không có token cache Mail riêng.
|
|
49
|
+
- [x] Mọi POST có `Idempotency-Key` từ `idempotency_key` hoặc `mcp-mail-<uuid>`; `mail_send` chuyển `sandbox: true` thành `X-Mona-Sandbox: 1` và đánh dấu response.
|
|
50
|
+
- [x] Schema chặn email sai, trên 50 người nhận, trên 10 tags, subject trên 998 ký tự, gói/mode/event ngoài enum, webhook không dùng HTTPS và tham số lạ. Domain chuyển lowercase/IDN sang punycode.
|
|
51
|
+
- [x] Tool mô tả VI/EN có tình huống sử dụng; domain trả records và hướng dẫn DNS/Cloudflare. API key chỉ trả một lần để app lưu `.env` với tên `MONAMAIL_API_KEY`; không thêm log token/key, không lưu token Cloudflare.
|
|
52
|
+
- [x] Lỗi Mail qua `CloudError`: giữ thông tin API, gồm `403 domain_not_verified`; `402 mail_plan_set` trả `insufficient_funds` và bước tiếp theo nhắc `cloud_topup`. `src/http.ts`, `src/errors.ts` và hành vi lỗi tool cũ giữ nguyên.
|
|
53
|
+
- [x] Instructions/entity, `monacloud://llms` và health MONA Mail `/v1/healthz` đã cập nhật. Có prompt `gui-mail-otp-monamail` với account → onboarding → DNS → verify → key → `.env` → SDK OTP → webhook bounced.
|
|
54
|
+
- [x] README có bảng 20 tool, ví dụ OTP 6 bước và env Mail; `docs/ai-agent.md` có ranh giới human DNS/nạp tiền, sandbox, webhook HMAC và idempotency 24 giờ.
|
|
55
|
+
- [x] Version package, MCP server và CLI đồng bộ `0.3.0`. `src/index.ts` chỉ đổi version; không sửa `src/monapay.ts`.
|
|
56
|
+
|
|
57
|
+
Gate ngày 05/09/2026:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
$ node_modules/.bin/tsc -p tsconfig.json
|
|
61
|
+
exit 0, không có diagnostic
|
|
62
|
+
|
|
63
|
+
$ node --test test/*.test.mjs
|
|
64
|
+
tests 23, pass 23, fail 0
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Stdio process thật xác nhận **103 tool = 83 cũ + 20 Mail**, 2 resource, 2 prompt, server version `0.3.0`. Test bao phủ auth/header/body, sandbox, records DNS, key một lần, HTTP 402/403/409/429, GET/query/DELETE 204, schema và prompt. Test cũ về compute, MONA Pay, alias và catalog đều xanh.
|
|
68
|
+
|
|
69
|
+
Lựa chọn theo contract: `subject` được bỏ khi có `template_id` vì §3 cho phép template thay nội dung; gửi trực tiếp bắt buộc subject và ít nhất html/text. Các POST ngoài `mail_send` cũng nhận `idempotency_key` tuỳ chọn để retry an toàn. Giá/quota và quyền domain vẫn do API quyết định, không hard-code giá gói.
|
|
70
|
+
|
|
71
|
+
Toàn bộ thay đổi và thư mục tạm của gate nằm trong workspace MCP; thư mục tạm đã dọn sau gate. Không chạy `npm install`, không gọi Internet hoặc production; dùng dependency có sẵn và fetch mock. Chưa kiểm gửi mail thật/DNS/SMTP production vì brief yêu cầu offline.
|
|
72
|
+
|
|
73
|
+
## Hoàn thành ở 0.2.1
|
|
6
74
|
|
|
7
75
|
- [x] Package Node 20 + TypeScript, ESM, stdio MCP; bin `monacloud-mcp`, version `0.2.1`.
|
|
8
76
|
- [x] OAuth device flow client `monacloud-mcp`, scope `offline_access`, token store private `0600`, refresh tự động; CLI `login`, `logout`, `whoami`.
|
|
@@ -55,3 +123,11 @@ CODEX DONE
|
|
|
55
123
|
- Lưu ý test tự động: trang consent Keycloak có form action TƯƠNG ĐỐI (`/realms/mona/login-actions/consent?...`) → script phải ghép host; urllib Python rớt cookie ở hop 302 → dùng curl cookie jar.
|
|
56
124
|
- Chưa: publish npm (`npm publish` — Claude làm khi deploy), `monapay_link`/`vibecloud_link` chờ endpoint link phía sản phẩm (gói E monapay, brief VIBECLOUD-MONAID đã có `/api/auth/monaid/link`).
|
|
57
125
|
- 04/09: publish npm `monacloud-mcp` 0.1.0 rồi 0.1.1 (sửa dep monapay-mcp ^0.3.0 → ^0.5.5 vì 0.5.x không khớp ^0.3).
|
|
126
|
+
- 04/09: sandbox bỏ spend guard (Codex) → publish 0.2.1; verify: cloud_vps_create sandbox:true với ví 0đ → job queued; không sandbox → insufficient_funds đúng.
|
|
127
|
+
|
|
128
|
+
CODEX B DONE
|
|
129
|
+
|
|
130
|
+
## QC Claude 05/09 (JOB B mail_*) — PASS local
|
|
131
|
+
- tsc sạch · `node --test` 23/23 · 20 tool `mail_*` + prompt `gui-mail-otp-monamail` + health `monamail` trong `monacloud://status`.
|
|
132
|
+
- Claude vá: `idempotencyHeaders` luôn gửi `Idempotency-Key` (mặc định `mcp-mail-<uuid>`) thay vì chỉ khi có tham số — AI retry an toàn theo contract §5.
|
|
133
|
+
- Chưa publish npm 0.3.0: chờ `api.monamail.vn` live (deploy theo `handoff/DEPLOY-MONA-MAIL-RUNBOOK.md`) rồi zero-dashboard test prod bằng `handoff/tests/test_mail_zero_dashboard.py`.
|
package/dist/clients.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ export declare class CloudClients {
|
|
|
7
7
|
readonly fetchImpl: typeof fetch;
|
|
8
8
|
constructor(config: Config, auth: AuthManager, fetchImpl?: typeof fetch);
|
|
9
9
|
private billing;
|
|
10
|
+
mail<T = unknown>(path: string, options?: {
|
|
11
|
+
method?: string;
|
|
12
|
+
body?: unknown;
|
|
13
|
+
query?: Record<string, string | number | undefined>;
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
}): Promise<T>;
|
|
10
16
|
balance(): Promise<unknown>;
|
|
11
17
|
ledger(cursor?: string, limit?: number): Promise<unknown>;
|
|
12
18
|
usage(period: string, product?: string): Promise<unknown>;
|
|
@@ -19,7 +25,7 @@ export declare class CloudClients {
|
|
|
19
25
|
}): Promise<unknown>;
|
|
20
26
|
budgetGet(): Promise<unknown>;
|
|
21
27
|
tokenLimit(spendLimitVnd: number, period: string, tokenId?: string): Promise<unknown>;
|
|
22
|
-
spendGuard(): Promise<JsonObject>;
|
|
28
|
+
spendGuard(requiredVnd?: number): Promise<JsonObject>;
|
|
23
29
|
sandboxEnabled(requested?: boolean): boolean;
|
|
24
30
|
private vibecloudToken;
|
|
25
31
|
vibecloud<T = unknown>(path: string, options?: {
|
|
@@ -27,14 +33,22 @@ export declare class CloudClients {
|
|
|
27
33
|
body?: unknown;
|
|
28
34
|
query?: Record<string, string | number | undefined>;
|
|
29
35
|
headers?: Record<string, string>;
|
|
36
|
+
timeoutMs?: number;
|
|
30
37
|
}): Promise<T>;
|
|
31
38
|
guardedVibecloud(path: string, options?: {
|
|
32
39
|
method?: string;
|
|
33
40
|
body?: unknown;
|
|
34
|
-
}, requestedSandbox?: boolean): Promise<unknown>;
|
|
41
|
+
}, requestedSandbox?: boolean, requiredVnd?: number): Promise<unknown>;
|
|
35
42
|
vibecloudLink(): Promise<JsonObject>;
|
|
36
43
|
monapayLink(): Promise<JsonObject>;
|
|
37
44
|
prices(): Promise<unknown>;
|
|
38
45
|
packages(): Promise<unknown>;
|
|
46
|
+
plans(): Promise<unknown>;
|
|
47
|
+
invoicePdf(invoiceId: string): Promise<{
|
|
48
|
+
invoice_id: string;
|
|
49
|
+
path: string;
|
|
50
|
+
mime_type: string;
|
|
51
|
+
size_bytes: number;
|
|
52
|
+
}>;
|
|
39
53
|
}
|
|
40
54
|
export {};
|
package/dist/clients.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
2
5
|
import { readLinks, writeLinks } from './auth.js';
|
|
3
6
|
import { CloudError } from './errors.js';
|
|
4
|
-
import { requestJson, unwrapData } from './http.js';
|
|
7
|
+
import { apiError, requestJson, unwrapData } from './http.js';
|
|
5
8
|
const asObject = (value) => (value && typeof value === 'object' ? value : {});
|
|
6
9
|
function pickString(objects, keys) {
|
|
7
10
|
for (const object of objects) {
|
|
@@ -29,6 +32,40 @@ export class CloudClients {
|
|
|
29
32
|
fetchImpl: this.fetchImpl,
|
|
30
33
|
});
|
|
31
34
|
}
|
|
35
|
+
async mail(path, options = {}) {
|
|
36
|
+
try {
|
|
37
|
+
return await requestJson(`${this.config.monamailApi}${path}`, {
|
|
38
|
+
...options,
|
|
39
|
+
headers: {
|
|
40
|
+
...(options.method === 'POST' ? { 'Idempotency-Key': `mcp-mail-${randomUUID()}` } : {}),
|
|
41
|
+
...options.headers,
|
|
42
|
+
},
|
|
43
|
+
token: await this.auth.accessToken(),
|
|
44
|
+
fetchImpl: this.fetchImpl,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (!(error instanceof CloudError) || !error.status)
|
|
49
|
+
throw error;
|
|
50
|
+
// Mail keeps the API's error contract without changing legacy HTTP errors.
|
|
51
|
+
const root = asObject(error.details);
|
|
52
|
+
const objects = [root, asObject(root.detail)];
|
|
53
|
+
const code = path === '/v1/account/plan' && error.status === 402
|
|
54
|
+
? 'insufficient_funds'
|
|
55
|
+
: pickString(objects, ['code']) || error.code;
|
|
56
|
+
const fallback = code === 'insufficient_funds'
|
|
57
|
+
? `Gọi cloud_topup để nạp ví, chờ cloud_balance cập nhật rồi thử lại. Console: ${this.config.consoleUrl}.`
|
|
58
|
+
: code === 'quota_exceeded'
|
|
59
|
+
? 'Gọi mail_plans rồi mail_plan_set để đổi gói trước khi gửi tiếp.'
|
|
60
|
+
: code === 'budget_exceeded'
|
|
61
|
+
? 'Gọi cloud_budget_get và điều chỉnh ngân sách trước khi thử lại.'
|
|
62
|
+
: error.nextStep;
|
|
63
|
+
const nextStep = pickString(objects, ['next_step']) || fallback;
|
|
64
|
+
throw new CloudError(code, pickString(objects, ['message']) || error.message, code === 'insufficient_funds' && !nextStep.includes('cloud_topup')
|
|
65
|
+
? `${nextStep} Gọi cloud_topup để nạp ví.`
|
|
66
|
+
: nextStep, { status: error.status, details: error.details, requestId: error.requestId });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
32
69
|
balance() {
|
|
33
70
|
return this.billing('/v1/balance');
|
|
34
71
|
}
|
|
@@ -61,11 +98,14 @@ export class CloudClients {
|
|
|
61
98
|
fetchImpl: this.fetchImpl,
|
|
62
99
|
});
|
|
63
100
|
}
|
|
64
|
-
async spendGuard() {
|
|
65
|
-
const current = asObject(await this.balance());
|
|
101
|
+
async spendGuard(requiredVnd) {
|
|
102
|
+
const current = asObject(unwrapData(await this.balance()));
|
|
66
103
|
const balance = current.balance_vnd;
|
|
67
|
-
if (typeof balance
|
|
68
|
-
throw new CloudError('
|
|
104
|
+
if (requiredVnd !== undefined && (typeof balance !== 'number' || !Number.isFinite(balance))) {
|
|
105
|
+
throw new CloudError('invalid_balance', 'Không đọc được số dư để duyệt giá gói.', 'Gọi cloud_balance rồi thử lại.');
|
|
106
|
+
}
|
|
107
|
+
if (typeof balance === 'number' && (requiredVnd === undefined ? balance <= 0 : balance < requiredVnd)) {
|
|
108
|
+
throw new CloudError('insufficient_funds', `Ví thiếu tiền: có ${balance} đ${requiredVnd === undefined ? '' : `, cần ${requiredVnd} đ`}.`, `Gọi cloud_topup để nạp ví tại ${this.config.consoleUrl} rồi gọi lại tool.`);
|
|
69
109
|
}
|
|
70
110
|
return current;
|
|
71
111
|
}
|
|
@@ -84,15 +124,16 @@ export class CloudClients {
|
|
|
84
124
|
}
|
|
85
125
|
async vibecloud(path, options = {}) {
|
|
86
126
|
return requestJson(`${this.config.vibecloudApi}${path}`, {
|
|
127
|
+
timeoutMs: 30_000,
|
|
87
128
|
...options,
|
|
88
129
|
token: await this.vibecloudToken(),
|
|
89
130
|
fetchImpl: this.fetchImpl,
|
|
90
131
|
});
|
|
91
132
|
}
|
|
92
|
-
async guardedVibecloud(path, options = {}, requestedSandbox = false) {
|
|
133
|
+
async guardedVibecloud(path, options = {}, requestedSandbox = false, requiredVnd) {
|
|
93
134
|
const sandbox = this.sandboxEnabled(requestedSandbox);
|
|
94
135
|
if (!sandbox)
|
|
95
|
-
await this.spendGuard();
|
|
136
|
+
await this.spendGuard(requiredVnd);
|
|
96
137
|
const result = await this.vibecloud(path, {
|
|
97
138
|
...options,
|
|
98
139
|
...(sandbox ? { headers: { 'X-Vibecloud-Sandbox': '1' } } : {}),
|
|
@@ -167,4 +208,46 @@ export class CloudClients {
|
|
|
167
208
|
packages() {
|
|
168
209
|
return requestJson(`${this.config.vibecloudApi}/api/packages`, { fetchImpl: this.fetchImpl });
|
|
169
210
|
}
|
|
211
|
+
plans() {
|
|
212
|
+
return requestJson(`${this.config.vibecloudApi}/api/plans`, { fetchImpl: this.fetchImpl });
|
|
213
|
+
}
|
|
214
|
+
async invoicePdf(invoiceId) {
|
|
215
|
+
const token = await this.vibecloudToken();
|
|
216
|
+
let response;
|
|
217
|
+
try {
|
|
218
|
+
response = await this.fetchImpl(`${this.config.vibecloudApi}/api/invoices/${encodeURIComponent(invoiceId)}.pdf`, {
|
|
219
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/pdf' },
|
|
220
|
+
signal: AbortSignal.timeout(30_000),
|
|
221
|
+
redirect: 'error',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
throw new CloudError('network_error', 'Không tải được PDF hoá đơn.', 'Gọi lại cloud_invoice_pdf với cùng invoice_id.');
|
|
226
|
+
}
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
const body = await response.text();
|
|
229
|
+
let parsed;
|
|
230
|
+
try {
|
|
231
|
+
parsed = JSON.parse(body);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
parsed = { message: body.slice(0, 1000) };
|
|
235
|
+
}
|
|
236
|
+
throw apiError(response, parsed);
|
|
237
|
+
}
|
|
238
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
239
|
+
if (bytes.subarray(0, 5).toString('ascii') !== '%PDF-') {
|
|
240
|
+
throw new CloudError('invalid_pdf', 'API không trả nội dung PDF hợp lệ.', 'Kiểm tra invoice_id rồi thử lại.');
|
|
241
|
+
}
|
|
242
|
+
const directory = await mkdtemp(join(tmpdir(), 'monacloud-invoice-'));
|
|
243
|
+
const path = join(directory, 'invoice.pdf');
|
|
244
|
+
try {
|
|
245
|
+
await writeFile(path, bytes, { mode: 0o600, flag: 'wx' });
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
await rm(directory, { recursive: true, force: true });
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
return { invoice_id: invoiceId, path, mime_type: 'application/pdf', size_bytes: bytes.length };
|
|
252
|
+
}
|
|
170
253
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { CloudClients } from './clients.js';
|
|
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 \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
|
+
export declare function rows(value: unknown, key: string): Row[];
|
|
6
|
+
export declare function planEstimate(clients: CloudClients, code: string, cycle?: string): Promise<{
|
|
7
|
+
plan: Row;
|
|
8
|
+
estimate: {
|
|
9
|
+
billing_mode: string;
|
|
10
|
+
plan_code: string;
|
|
11
|
+
period: string;
|
|
12
|
+
amount_vnd: number;
|
|
13
|
+
currency: string;
|
|
14
|
+
};
|
|
15
|
+
}>;
|
|
16
|
+
export declare function createVps(clients: CloudClients, body: Row): Promise<unknown>;
|
|
17
|
+
export declare function registerComputeTools(server: McpServer, clients: CloudClients): void;
|
|
18
|
+
export {};
|
package/dist/compute.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CloudError, runTool } from './errors.js';
|
|
3
|
+
import { unwrapData } from './http.js';
|
|
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 đã 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
|
+
const sandbox = z.boolean().optional().describe('Thử 0đ, không tạo hạ tầng thật / Sandbox, no charge');
|
|
7
|
+
const id = z.string().trim().min(1).max(255);
|
|
8
|
+
const planCode = z.string().trim().min(2).max(64);
|
|
9
|
+
const period = z.enum(['month', 'year']);
|
|
10
|
+
const object = (value) => value && typeof value === 'object' ? value : {};
|
|
11
|
+
export function rows(value, key) {
|
|
12
|
+
const data = unwrapData(value);
|
|
13
|
+
const list = Array.isArray(data) ? data : object(data)[key];
|
|
14
|
+
return Array.isArray(list) ? list.filter((row) => row && typeof row === 'object') : [];
|
|
15
|
+
}
|
|
16
|
+
export async function planEstimate(clients, code, cycle = 'month') {
|
|
17
|
+
const plan = rows(await clients.plans(), 'plans').find((row) => row.code === code && row.active !== false);
|
|
18
|
+
if (!plan)
|
|
19
|
+
throw new CloudError('plan_not_found', `Không có gói đang mở: ${code}.`, 'Gọi cloud_plan_list để chọn plan_code.');
|
|
20
|
+
const amount = plan[cycle === 'year' ? 'price_year_vnd' : 'price_month_vnd'];
|
|
21
|
+
if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0) {
|
|
22
|
+
throw new CloudError('invalid_plan_price', 'Không đọc được giá gói.', 'Gọi cloud_plan_list rồi thử lại; không tự đoán giá.');
|
|
23
|
+
}
|
|
24
|
+
return { plan, estimate: { billing_mode: 'monthly', plan_code: code, period: cycle, amount_vnd: amount, currency: 'VND' } };
|
|
25
|
+
}
|
|
26
|
+
export async function createVps(clients, body) {
|
|
27
|
+
const { sandbox: requested, ...payload } = body;
|
|
28
|
+
if (payload.billing_mode !== 'monthly')
|
|
29
|
+
return clients.guardedVibecloud('/api/lxc', { method: 'POST', body: payload }, requested === true);
|
|
30
|
+
const { plan, estimate } = await planEstimate(clients, String(payload.plan_code), String(payload.period || 'month'));
|
|
31
|
+
// Wave A rejects mixed sizing. Strip all hourly sizing before the real request.
|
|
32
|
+
delete payload.cpu;
|
|
33
|
+
delete payload.ram_gb;
|
|
34
|
+
delete payload.disk_gb;
|
|
35
|
+
delete payload.package_slug;
|
|
36
|
+
const isSandbox = clients.sandboxEnabled(requested === true);
|
|
37
|
+
const outgoing = isSandbox
|
|
38
|
+
? { app_name: payload.app_name, cpu: plan.cpu, ram_gb: plan.ram_gb, disk_gb: plan.disk_gb }
|
|
39
|
+
: payload;
|
|
40
|
+
const result = await clients.guardedVibecloud('/api/lxc', { method: 'POST', body: outgoing }, isSandbox, estimate.amount_vnd);
|
|
41
|
+
return { ...object(result), estimate, ...(isSandbox ? {
|
|
42
|
+
preview_billing_mode: 'hourly', requested_billing_mode: 'monthly',
|
|
43
|
+
next_step: 'Sandbox chỉ thử cấu hình gói qua hourly (0đ); chưa tạo subscription. Duyệt giá estimate rồi gọi cloud_vps_create billing_mode=monthly, sandbox=false.',
|
|
44
|
+
} : {}) };
|
|
45
|
+
}
|
|
46
|
+
export function registerComputeTools(server, clients) {
|
|
47
|
+
// Every new compute tool keeps a mechanical legacy alias, using the same schema and handler.
|
|
48
|
+
const register = (name, description, schema, handler) => {
|
|
49
|
+
for (const toolName of [name, name.replace(/^cloud_/, 'vibecloud_')]) {
|
|
50
|
+
server.registerTool(toolName, {
|
|
51
|
+
description: toolName === name ? description : `Alias tương thích của ${name}. / Compatibility alias. ${description}`,
|
|
52
|
+
inputSchema: schema,
|
|
53
|
+
}, (args) => runTool(() => handler(args)));
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
register('cloud_plan_list', 'Bảng gói cùng giá tháng/năm; gợi ý gói rẻ nhất đủ CPU/RAM/đĩa yêu cầu, admin_only không tự chọn. Đọc trước khi duyệt chi phí. / List plans, prices and sizing recommendation.', z.object({ cpu: z.number().int().positive().optional(), ram_gb: z.number().positive().optional(), disk_gb: z.number().positive().optional() }).strict(), async (needs) => {
|
|
57
|
+
const plans = rows(await clients.plans(), 'plans');
|
|
58
|
+
const eligible = plans.filter((plan) => plan.active !== false && !plan.admin_only
|
|
59
|
+
&& ['cpu', 'ram_gb', 'disk_gb'].every((key) => typeof plan[key] === 'number' && Number(plan[key]) >= (needs[key] || 0))
|
|
60
|
+
&& typeof plan.price_month_vnd === 'number').sort((a, b) => Number(a.price_month_vnd) - Number(b.price_month_vnd));
|
|
61
|
+
return { plans, recommendation: eligible[0] ? { plan_code: eligible[0].code, reason: 'Gói giá tháng thấp nhất đáp ứng cấu hình yêu cầu; kiểm tra tải thực tế, backup và included_db.' } : null,
|
|
62
|
+
next_step: 'Chọn plan_code và month/year, báo giá tương ứng rồi hỏi duyệt trước cloud_vps_create.' };
|
|
63
|
+
});
|
|
64
|
+
register('cloud_subscription_list', 'Đọc các gói đang dùng, kỳ gia hạn và auto-renew. / List subscriptions.', z.object({}).strict(), () => clients.vibecloud('/api/subscriptions'));
|
|
65
|
+
register('cloud_subscription_update', 'Đổi gói/chu kỳ/gia hạn sau khi đọc subscription và giá, ước tính rồi được duyệt. Upgrade tính prorate, downgrade kỳ sau. Huỷ: auto_renew=false, cancel_action=hourly|stop. / Update or cancel renewal.', z.object({ service_id: id, plan_code: planCode.optional(), period: period.optional(), auto_renew: z.boolean().optional(), cancel_action: z.enum(['hourly', 'stop']).optional() }).strict()
|
|
66
|
+
.refine((args) => [args.plan_code, args.period, args.auto_renew, args.cancel_action].some((value) => value !== undefined), 'Cần ít nhất một thay đổi subscription.'), async ({ service_id, ...body }) => {
|
|
67
|
+
// Cancellation must remain possible with an empty wallet. Proration is authoritative at the API.
|
|
68
|
+
return clients.vibecloud(`/api/services/${encodeURIComponent(service_id)}/subscription`, { method: 'POST', body });
|
|
69
|
+
});
|
|
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
|
+
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
|
+
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 đã live: app host đầu tiên ~2–3 phút, deploy sau đó 10–20 giây. / Git apps are live. ';
|
|
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().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
|
+
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
|
+
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ệ.');
|
|
79
|
+
const repo = z.string().trim().min(1).max(2048).refine((value) => {
|
|
80
|
+
try {
|
|
81
|
+
const u = new URL(value);
|
|
82
|
+
return u.protocol === 'https:' && !u.port && !u.password && !u.username && !u.search && !u.hash && u.pathname.length > 1;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}, 'Dùng public HTTPS git URL không chứa token/mật khẩu; private repo chưa được hỗ trợ.');
|
|
88
|
+
const compute = async (path, options, requested) => {
|
|
89
|
+
const isSandbox = clients.sandboxEnabled(requested);
|
|
90
|
+
const result = await clients.vibecloud(path, { ...options, ...(isSandbox ? { headers: { 'X-Vibecloud-Sandbox': '1' } } : {}) });
|
|
91
|
+
return isSandbox ? { ...(Array.isArray(result) ? { items: result } : object(result)), sandbox: true } : result;
|
|
92
|
+
};
|
|
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));
|
|
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));
|
|
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));
|
|
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));
|
|
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));
|
|
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));
|
|
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));
|
|
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));
|
|
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));
|
|
102
|
+
}
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -8,6 +8,7 @@ export function readConfig(env = process.env) {
|
|
|
8
8
|
issuer: cleanBaseUrl(env.MONACLOUD_ISSUER || 'https://pass.monacloud.vn/realms/mona'),
|
|
9
9
|
billingUrl: cleanBaseUrl(env.MONACLOUD_BILLING_URL || 'https://billing.monacloud.vn'),
|
|
10
10
|
monapayApi: cleanBaseUrl(env.MONAPAY_API || env.MONAPAY_BASE_URL || 'https://api.monapay.vn'),
|
|
11
|
+
monamailApi: cleanBaseUrl(env.MONAMAIL_API || 'https://api.monamail.vn'),
|
|
11
12
|
vibecloudApi: cleanBaseUrl(env.MONACLOUD_API || env.VIBECLOUD_API || env.VIBECLOUD_API_URL || 'https://api.monacloud.vn'),
|
|
12
13
|
consoleUrl: cleanBaseUrl(env.MONACLOUD_CONSOLE_URL || 'https://monacloud.vn/console'),
|
|
13
14
|
clientId: env.MONACLOUD_CLIENT_ID || 'monacloud-mcp',
|
package/dist/http.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CloudError } from './errors.js';
|
|
1
2
|
export type HttpOptions = {
|
|
2
3
|
method?: string;
|
|
3
4
|
token?: string;
|
|
@@ -7,5 +8,6 @@ export type HttpOptions = {
|
|
|
7
8
|
fetchImpl?: typeof fetch;
|
|
8
9
|
timeoutMs?: number;
|
|
9
10
|
};
|
|
11
|
+
export declare function apiError(response: Response, body: unknown): CloudError;
|
|
10
12
|
export declare function requestJson<T = unknown>(url: string, options?: HttpOptions): Promise<T>;
|
|
11
13
|
export declare function unwrapData<T = unknown>(value: unknown): T;
|
package/dist/http.js
CHANGED
|
@@ -8,7 +8,7 @@ const nestedString = (body, key) => {
|
|
|
8
8
|
const nested = detail[key];
|
|
9
9
|
return typeof nested === 'string' && nested ? nested : undefined;
|
|
10
10
|
};
|
|
11
|
-
function apiError(response, body) {
|
|
11
|
+
export function apiError(response, body) {
|
|
12
12
|
const parsed = asRecord(body);
|
|
13
13
|
const requestId = response.headers.get('x-request-id')
|
|
14
14
|
|| nestedString(parsed, 'request_id');
|
package/dist/index.js
CHANGED
package/dist/jobs.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CloudClients } from './clients.js';
|
|
2
|
+
export declare const TERMINAL_JOBS: Set<string>;
|
|
3
|
+
export declare function pollJob(clients: CloudClients, jobId: string, wait: boolean, intervalSeconds: number, timeoutSeconds: number, sandbox: boolean): Promise<Record<string, unknown>>;
|
|
4
|
+
export declare function finishAppJob(clients: CloudClients, created: unknown, sandbox: boolean, wait: boolean, interval: number, timeout: number): Promise<{
|
|
5
|
+
job_id?: string | undefined;
|
|
6
|
+
estimate?: {} | undefined;
|
|
7
|
+
app_id?: string | undefined;
|
|
8
|
+
url?: string | undefined;
|
|
9
|
+
application_id?: string | undefined;
|
|
10
|
+
sandbox?: boolean | undefined;
|
|
11
|
+
}>;
|