monacloud-mcp 0.7.0 → 0.8.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 +14 -5
- package/STATUS.md +4 -0
- package/dist/clients.d.ts +10 -1
- package/dist/clients.js +81 -9
- package/dist/compute.d.ts +1 -1
- package/dist/compute.js +1 -1
- package/dist/qr.d.ts +46 -0
- package/dist/qr.js +208 -0
- package/dist/server.js +20 -10
- package/dist/templates.js +1 -1
- package/docs/ai-agent.md +4 -4
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
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ạo Base beta thay Supabase, 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 đăng
|
|
5
|
+
Human đăng ký MONA Pass một lần, duyệt chi phí, thêm DNS khi cần, quét QR nạp ví do AI tạo và cung cấp OTP/KYC khi bắt buộc. Các bước tạo yêu cầu nạp, 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
|
+
|
|
7
|
+
## 0.8.1 — luật nạp ví đứng đầu instructions
|
|
8
|
+
|
|
9
|
+
Codex chỉ chắc chắn đọc 512 ký tự đầu của `instructions` MCP, nên luật "ví thiếu → AI tự gọi `cloud_topup`, in QR, không bảo mở console" được đưa lên đầu; mô tả hệ và APP_FLOW đặt sau. Kiểm thử 17/09: Claude Code (sonnet) và Gemini (Antigravity CLI) tự gọi `cloud_topup` và in QR khi người dùng nói "ví hết tiền, nạp 50k"; Codex `exec` cần cho phép gọi MCP (mặc định chặn approval) rồi cũng làm được.
|
|
10
|
+
|
|
11
|
+
## 0.8.0 — Nạp ví ngay trong terminal
|
|
12
|
+
|
|
13
|
+
Ví thiếu tiền thì AI gọi `cloud_topup(amount)`; MCP trả **`qr_ascii`** (QR VietQR chuẩn EMVCo/NAPAS in bằng khối đầy `██`, Claude Code/Codex/Gemini CLI hiện được ngay; terminal nền sáng dùng `qr_ascii_light`, bản gọn `qr_ascii_small`; đã giải mã được bằng ZBar và OpenCV ở cả hai nền), `qr_file` (PNG tại `~/.config/monacloud/`), `qr_url` (ảnh VietQR) cùng ngân hàng, số tài khoản, số tiền, nội dung chuyển khoản. Người dùng mở app ngân hàng quét màn hình, tiền vào tự cộng ví; AI gọi `cloud_topup_status(topup_id)` tới khi `paid` rồi làm tiếp. Không còn bảo người dùng mở console để nạp; base64 không còn nằm trong text (tiết kiệm 15–25k token mỗi lần). Khi ví chung (`billing`) chưa nhận token hoặc merchant MONA Pay chưa cấu hình, `cloud_topup` tự chuyển sang đường compute `/api/payments/vietqr` (ví local, prefix VIBECLOUD, báo có tự cộng).
|
|
6
14
|
|
|
7
15
|
## 0.5.0 — MONA Base beta
|
|
8
16
|
|
|
@@ -12,7 +20,7 @@ Thêm `cloud_base_create/list/get/delete/credentials` cùng alias `vibecloud_bas
|
|
|
12
20
|
|
|
13
21
|
Prompt Claude Code: **“Đưa dự án này lên MONA Cloud, dùng thư mục hiện tại”**.
|
|
14
22
|
|
|
15
|
-
AI làm 99%: `cloud_app_detect(local_dir)` offline → đọc host và giá → sandbox nếu cần host mới → hỏi duyệt chi phí một lần → `cloud_app_create(local_dir)` → kiểm và trả URL. Có domain riêng thì `cloud_app_domain_add` và hướng dẫn CNAME. Human đăng ký MONA Pass qua device flow và
|
|
23
|
+
AI làm 99%: `cloud_app_detect(local_dir)` offline → đọc host và giá → sandbox nếu cần host mới → hỏi duyệt chi phí một lần → `cloud_app_create(local_dir)` → kiểm và trả URL. Có domain riêng thì `cloud_app_domain_add` và hướng dẫn CNAME. Human đăng ký MONA Pass qua device flow; hết credit 20k thì AI gọi `cloud_topup` và in QR, human chỉ quét.
|
|
16
24
|
|
|
17
25
|
| Tool | Đầu vào / hành vi 0.4.0 |
|
|
18
26
|
|---|---|
|
|
@@ -94,7 +102,8 @@ Tên tool dùng `snake_case` để giữ tương thích với prompt MONA Pay c
|
|
|
94
102
|
| `cloud_whoami` | Hồ sơ MONA Pass hiện tại |
|
|
95
103
|
| `cloud_balance` | Số dư ví VND chung |
|
|
96
104
|
| `cloud_ledger` | Ledger nạp/trừ/hoàn tiền có cursor |
|
|
97
|
-
| `cloud_topup(amount)` | Tạo yêu cầu nạp, trả `
|
|
105
|
+
| `cloud_topup(amount)` | Tạo yêu cầu nạp, trả `qr_ascii` (in trong terminal), `qr_file`, `qr_url`, ngân hàng/số TK/số tiền/nội dung; tự fallback compute khi ví chung chưa sẵn sàng |
|
|
106
|
+
| `cloud_topup_status(topup_id)` | Trạng thái yêu cầu nạp (pending/paid) kèm số dư, để chờ tiền vào rồi làm tiếp |
|
|
98
107
|
| `cloud_usage(period)` | Usage theo tháng, có thể lọc sản phẩm |
|
|
99
108
|
| `cloud_services` | Gom VPS/database, VA và webhook MONA Pay |
|
|
100
109
|
| `cloud_budget_set` / `cloud_budget_get` | Đặt và đọc budget theo product/project/token |
|
|
@@ -211,7 +220,7 @@ Trước lệnh compute MONA Cloud thật có thể phát sinh tiền, MCP đọ
|
|
|
211
220
|
{
|
|
212
221
|
"code": "budget_exceeded",
|
|
213
222
|
"message": "Ví thiếu 20.000 đ hoặc đã chạm giới hạn chi tiêu.",
|
|
214
|
-
"next_step": "
|
|
223
|
+
"next_step": "Gọi cloud_topup để lấy QR nạp ví hoặc cloud_budget_set để tăng ngân sách rồi gọi lại tool.",
|
|
215
224
|
"request_id": "req_..."
|
|
216
225
|
}
|
|
217
226
|
```
|
|
@@ -227,7 +236,7 @@ Người dùng nói:
|
|
|
227
236
|
Agent thực hiện:
|
|
228
237
|
|
|
229
238
|
1. `cloud_whoami` → `cloud_balance` → `cloud_packages`.
|
|
230
|
-
2. Nếu ví thiếu: `cloud_topup(200000)`,
|
|
239
|
+
2. Nếu ví thiếu: `cloud_topup(200000)`, in nguyên khối `qr_ascii` cho người dùng quét bằng app ngân hàng, rồi `cloud_topup_status` tới khi `paid`.
|
|
231
240
|
3. `cloud_vps_create` + `cloud_db_create`, sau đó `cloud_job_status` cho từng job.
|
|
232
241
|
4. `monapay_link` nếu adapter chuyển tiếp chưa có credential; `monapay_whoami` để kiểm tra VA.
|
|
233
242
|
5. Nếu chưa có VA: bắt đầu nối ACB, hỏi người dùng OTP đúng hai điểm bắt buộc, không tự đoán.
|
package/STATUS.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# STATUS — monacloud-mcp 0.5.0
|
|
2
2
|
|
|
3
|
+
## 0.8.0 — nạp ví ngay trong terminal (17/09/2026)
|
|
4
|
+
- [x] `cloud_topup` trả `qr_ascii` (VietQR EMVCo/NAPAS dựng local, CRC16 khớp mẫu acb-ipn) + `qr_file` PNG + `qr_url` + ngân hàng/số TK/số tiền/nội dung; bỏ base64 khỏi text. Fallback compute `/api/payments/vietqr` khi billing 401/403/404/502/503 (merchant MONA Pay placeholder). Thêm `cloud_topup_status`.
|
|
5
|
+
- [x] Lời cho AI: instructions/llms/prompt/README/docs nói rõ AI tự tạo QR, người dùng chỉ quét, không mở console (luật AI-first §1, Mon chốt 17/09).
|
|
6
|
+
|
|
3
7
|
Updated: 2026-09-14 (Asia/Ho_Chi_Minh)
|
|
4
8
|
|
|
5
9
|
## MONA Base beta — hoàn tất offline
|
package/dist/clients.d.ts
CHANGED
|
@@ -14,9 +14,18 @@ export declare class CloudClients {
|
|
|
14
14
|
headers?: Record<string, string>;
|
|
15
15
|
}): Promise<T>;
|
|
16
16
|
balance(): Promise<unknown>;
|
|
17
|
+
/** Số dư ví local compute (nơi yêu cầu nạp qua /api/payments/vietqr được cộng), đọc từ /api/me. */
|
|
18
|
+
localBalance(): Promise<JsonObject>;
|
|
17
19
|
ledger(cursor?: string, limit?: number): Promise<unknown>;
|
|
18
20
|
usage(period: string, product?: string): Promise<unknown>;
|
|
19
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Tạo yêu cầu nạp ví. Ưu tiên ví chung (billing /v1/topups → MONA Pay); khi billing chưa nhận token
|
|
23
|
+
* hoặc merchant MONA Pay chưa cấu hình (401/403/404/502/503) thì tạo qua compute /api/payments/vietqr
|
|
24
|
+
* (ví local, prefix VIBECLOUD, báo có tự cộng). Người dùng chỉ quét QR; không cần console.
|
|
25
|
+
*/
|
|
26
|
+
topup(amount: number, idempotencyKey?: string): Promise<JsonObject>;
|
|
27
|
+
/** Trạng thái một yêu cầu nạp (pending/paid) kèm số dư hiện tại, để AI chờ tiền vào rồi làm tiếp. */
|
|
28
|
+
topupStatus(topupId: string): Promise<JsonObject>;
|
|
20
29
|
budgetSet(body: {
|
|
21
30
|
scope: string;
|
|
22
31
|
scope_id: string;
|
package/dist/clients.js
CHANGED
|
@@ -54,7 +54,7 @@ export class CloudClients {
|
|
|
54
54
|
? 'insufficient_funds'
|
|
55
55
|
: pickString(objects, ['code']) || error.code;
|
|
56
56
|
const fallback = code === 'insufficient_funds'
|
|
57
|
-
?
|
|
57
|
+
? 'Gọi cloud_topup để lấy QR nạp ví (in qr_ascii cho người dùng quét), chờ cloud_topup_status paid hoặc cloud_balance cập nhật rồi thử lại.'
|
|
58
58
|
: code === 'quota_exceeded'
|
|
59
59
|
? 'Gọi mail_plans rồi mail_plan_set để đổi gói trước khi gửi tiếp.'
|
|
60
60
|
: code === 'budget_exceeded'
|
|
@@ -85,22 +85,94 @@ export class CloudClients {
|
|
|
85
85
|
currency: 'VND',
|
|
86
86
|
wallet: 'local',
|
|
87
87
|
source: '/api/me',
|
|
88
|
-
note: 'Số dư ví local MONA Cloud compute (billing.monacloud.vn không nhận token này).
|
|
88
|
+
note: 'Số dư ví local MONA Cloud compute (billing.monacloud.vn không nhận token này). Thiếu tiền thì gọi cloud_topup: QR VietQR in ngay trong terminal, người dùng quét bằng app ngân hàng, không cần mở console. / Local compute wallet balance; top up with cloud_topup.',
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
/** Số dư ví local compute (nơi yêu cầu nạp qua /api/payments/vietqr được cộng), đọc từ /api/me. */
|
|
93
|
+
async localBalance() {
|
|
94
|
+
const root = asObject(unwrapData(await this.vibecloud('/api/me')));
|
|
95
|
+
const me = asObject(root.user ?? root);
|
|
96
|
+
const credit = Number(me.credit_vnd ?? 0);
|
|
97
|
+
const promo = Number(me.promo_credit_vnd ?? 0);
|
|
98
|
+
const total = me.total_credit_vnd !== undefined ? Number(me.total_credit_vnd) : credit + promo;
|
|
99
|
+
return {
|
|
100
|
+
balance_vnd: Number.isFinite(total) ? total : 0,
|
|
101
|
+
credit_vnd: Number.isFinite(credit) ? credit : 0,
|
|
102
|
+
promo_credit_vnd: Number.isFinite(promo) ? promo : 0,
|
|
103
|
+
currency: 'VND',
|
|
104
|
+
wallet: 'local',
|
|
105
|
+
source: '/api/me',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
92
108
|
ledger(cursor, limit = 50) {
|
|
93
109
|
return this.billing('/v1/ledger', { query: { cursor, limit } });
|
|
94
110
|
}
|
|
95
111
|
usage(period, product) {
|
|
96
112
|
return this.billing('/v1/usage', { query: { period, product } });
|
|
97
113
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
114
|
+
/**
|
|
115
|
+
* Tạo yêu cầu nạp ví. Ưu tiên ví chung (billing /v1/topups → MONA Pay); khi billing chưa nhận token
|
|
116
|
+
* hoặc merchant MONA Pay chưa cấu hình (401/403/404/502/503) thì tạo qua compute /api/payments/vietqr
|
|
117
|
+
* (ví local, prefix VIBECLOUD, báo có tự cộng). Người dùng chỉ quét QR; không cần console.
|
|
118
|
+
*/
|
|
119
|
+
async topup(amount, idempotencyKey) {
|
|
120
|
+
const key = idempotencyKey || `mcp-topup-${randomUUID()}`;
|
|
121
|
+
try {
|
|
122
|
+
const central = asObject(unwrapData(await this.billing('/v1/topups', {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Idempotency-Key': key },
|
|
125
|
+
body: { amount_vnd: amount },
|
|
126
|
+
})));
|
|
127
|
+
return { amount_vnd: amount, ...central, wallet: 'central', source: '/v1/topups' };
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const status = error instanceof CloudError ? error.status ?? 0 : 0;
|
|
131
|
+
if (!(error instanceof CloudError) || ![401, 403, 404, 502, 503].includes(status))
|
|
132
|
+
throw error;
|
|
133
|
+
const local = asObject(unwrapData(await this.vibecloud('/api/payments/vietqr', {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
body: { amount },
|
|
136
|
+
})));
|
|
137
|
+
return {
|
|
138
|
+
topup_id: local.payment_id,
|
|
139
|
+
order_ref: local.description,
|
|
140
|
+
amount_vnd: typeof local.amount === 'number' ? local.amount : amount,
|
|
141
|
+
status: local.status ?? 'pending',
|
|
142
|
+
qr_url: local.qr_url,
|
|
143
|
+
qr_data_url: local.qr_data_url,
|
|
144
|
+
wallet: 'local',
|
|
145
|
+
source: '/api/payments/vietqr',
|
|
146
|
+
fallback_reason: error.code,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/** Trạng thái một yêu cầu nạp (pending/paid) kèm số dư hiện tại, để AI chờ tiền vào rồi làm tiếp. */
|
|
151
|
+
async topupStatus(topupId) {
|
|
152
|
+
if (/^[0-9a-f]{24}$/i.test(topupId)) {
|
|
153
|
+
const root = asObject(unwrapData(await this.vibecloud(`/api/payments/${encodeURIComponent(topupId)}`)));
|
|
154
|
+
const payment = asObject(root.payment ?? root);
|
|
155
|
+
const status = typeof payment.status === 'string' ? payment.status : 'pending';
|
|
156
|
+
return {
|
|
157
|
+
topup_id: topupId,
|
|
158
|
+
status,
|
|
159
|
+
paid: status === 'paid',
|
|
160
|
+
amount_vnd: payment.amount,
|
|
161
|
+
order_ref: payment.description,
|
|
162
|
+
updated_at: payment.updated_at ?? payment.paid_at,
|
|
163
|
+
wallet: 'local',
|
|
164
|
+
balance: await this.localBalance(),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const ledger = await this.ledger(undefined, 100).catch(() => undefined);
|
|
168
|
+
const paid = ledger !== undefined && JSON.stringify(ledger).includes(topupId);
|
|
169
|
+
return {
|
|
170
|
+
topup_id: topupId,
|
|
171
|
+
status: paid ? 'paid' : 'pending',
|
|
172
|
+
paid,
|
|
173
|
+
wallet: 'central',
|
|
174
|
+
balance: await this.balance(),
|
|
175
|
+
};
|
|
104
176
|
}
|
|
105
177
|
budgetSet(body) {
|
|
106
178
|
return this.billing('/v1/budgets', { method: 'POST', body });
|
|
@@ -125,7 +197,7 @@ export class CloudClients {
|
|
|
125
197
|
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.');
|
|
126
198
|
}
|
|
127
199
|
if (typeof balance === 'number' && (requiredVnd === undefined ? balance <= 0 : balance < requiredVnd)) {
|
|
128
|
-
throw new CloudError('insufficient_funds', `Ví thiếu tiền: có ${balance} đ${requiredVnd === undefined ? '' : `, cần ${requiredVnd} đ`}.`,
|
|
200
|
+
throw new CloudError('insufficient_funds', `Ví thiếu tiền: có ${balance} đ${requiredVnd === undefined ? '' : `, cần ${requiredVnd} đ`}.`, 'Gọi cloud_topup để lấy QR nạp ví in ngay trong terminal; người dùng quét xong thì gọi lại tool.');
|
|
129
201
|
}
|
|
130
202
|
return current;
|
|
131
203
|
}
|
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 = "AI l\u00E0m 99%: cloud_app_detect(local_dir) offline \u2192 cloud_app_host_list, cloud_prices/cloud_packages/cloud_plan_list v\u00E0 cloud_balance \u2192 cloud_app_create(local_dir, sandbox=true) n\u1EBFu ch\u01B0a c\u00F3 app host \u0111\u1EC3 \u01B0\u1EDBc t\u00EDnh chi ph\u00ED gi\u1EDD/g\u00F3i \u2192 h\u1ECFi human m\u1ED9t l\u1EA7n duy\u1EC7t chi ph\u00ED n\u1EBFu ch\u01B0a \u0111\u01B0\u1EE3c duy\u1EC7t \u2192 cloud_app_create(local_dir, sandbox=false) \u2192 poll t\u1EDBi done/succeeded \u2192 ki\u1EC3m v\u00E0 tr\u1EA3 URL. C\u1EA7n DB/Supabase? cloud_base_create (beta) \u2014 chung account/v\u00ED MONA Cloud, kh\u1EDBp app deploy. N\u1EBFu human c\u00F3 domain: cloud_app_domain_add v\u00E0 h\u01B0\u1EDBng d\u1EABn CNAME t\u1EEB API. Human \u0111\u0103ng k\u00FD MONA Pass b\u1EB1ng device flow
|
|
4
|
+
export declare const APP_FLOW = "AI l\u00E0m 99%: cloud_app_detect(local_dir) offline \u2192 cloud_app_host_list, cloud_prices/cloud_packages/cloud_plan_list v\u00E0 cloud_balance \u2192 cloud_app_create(local_dir, sandbox=true) n\u1EBFu ch\u01B0a c\u00F3 app host \u0111\u1EC3 \u01B0\u1EDBc t\u00EDnh chi ph\u00ED gi\u1EDD/g\u00F3i \u2192 h\u1ECFi human m\u1ED9t l\u1EA7n duy\u1EC7t chi ph\u00ED n\u1EBFu ch\u01B0a \u0111\u01B0\u1EE3c duy\u1EC7t \u2192 cloud_app_create(local_dir, sandbox=false) \u2192 poll t\u1EDBi done/succeeded \u2192 ki\u1EC3m v\u00E0 tr\u1EA3 URL. C\u1EA7n DB/Supabase? cloud_base_create (beta) \u2014 chung account/v\u00ED MONA Cloud, kh\u1EDBp app deploy. N\u1EBFu human c\u00F3 domain: cloud_app_domain_add v\u00E0 h\u01B0\u1EDBng d\u1EABn CNAME t\u1EEB API. Human \u0111\u0103ng k\u00FD MONA Pass b\u1EB1ng device flow m\u1ED9t l\u1EA7n; h\u1EBFt credit 20k th\u00EC AI g\u1ECDi cloud_topup v\u00E0 in QR (qr_ascii) ngay trong terminal, human ch\u1EC9 qu\u00E9t b\u1EB1ng app ng\u00E2n h\u00E0ng, kh\u00F4ng m\u1EDF console; AI l\u00E0m c\u00E1c b\u01B0\u1EDBc c\u00F2n l\u1EA1i. App git cloud_app_create(repo_url) \u0111\u00E3 live; kh\u00F4ng g\u1ECDi agent_deploy cho deploy d\u1EF1 \u00E1n. / Detect locally, estimate, obtain cost approval once, upload and deploy, create an optional Base, return URL, then attach an optional domain.";
|
|
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
|
@@ -4,7 +4,7 @@ import { unwrapData } from './http.js';
|
|
|
4
4
|
import { finishAppJob } from './jobs.js';
|
|
5
5
|
import { detectProject } from './local.js';
|
|
6
6
|
import { localAppTools } from './local-app.js';
|
|
7
|
-
export const APP_FLOW = 'AI làm 99%: cloud_app_detect(local_dir) offline → cloud_app_host_list, cloud_prices/cloud_packages/cloud_plan_list và cloud_balance → cloud_app_create(local_dir, sandbox=true) nếu chưa có app host để ước tính chi phí giờ/gói → hỏi human một lần duyệt chi phí nếu chưa được duyệt → cloud_app_create(local_dir, sandbox=false) → poll tới done/succeeded → kiểm và trả URL. Cần DB/Supabase? cloud_base_create (beta) — chung account/ví MONA Cloud, khớp app deploy. Nếu human có domain: cloud_app_domain_add và hướng dẫn CNAME từ API. Human đăng ký MONA Pass bằng device flow
|
|
7
|
+
export const APP_FLOW = 'AI làm 99%: cloud_app_detect(local_dir) offline → cloud_app_host_list, cloud_prices/cloud_packages/cloud_plan_list và cloud_balance → cloud_app_create(local_dir, sandbox=true) nếu chưa có app host để ước tính chi phí giờ/gói → hỏi human một lần duyệt chi phí nếu chưa được duyệt → cloud_app_create(local_dir, sandbox=false) → poll tới done/succeeded → kiểm và trả URL. Cần DB/Supabase? cloud_base_create (beta) — chung account/ví MONA Cloud, khớp app deploy. Nếu human có domain: cloud_app_domain_add và hướng dẫn CNAME từ API. Human đăng ký MONA Pass bằng device flow một lần; hết credit 20k thì AI gọi cloud_topup và in QR (qr_ascii) ngay trong terminal, human chỉ quét bằng app ngân hàng, không mở console; AI làm các bước còn lại. App git cloud_app_create(repo_url) đã live; không gọi agent_deploy cho deploy dự án. / Detect locally, estimate, obtain cost approval once, upload and deploy, create an optional Base, return URL, then attach an optional domain.';
|
|
8
8
|
const sandbox = z.boolean().optional().describe('Thử 0đ, không tạo hạ tầng thật / Sandbox, no charge');
|
|
9
9
|
const id = z.string().trim().min(1).max(255);
|
|
10
10
|
const planCode = z.string().trim().min(2).max(64);
|
package/dist/qr.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
type JsonObject = Record<string, unknown>;
|
|
2
|
+
export declare const BANK_BIN: Record<string, string>;
|
|
3
|
+
/** CRC-16/CCITT-FALSE (poly 0x1021, init 0xFFFF) theo chuẩn EMVCo, trả 4 ký tự hex hoa. */
|
|
4
|
+
export declare function crc16(input: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Nội dung chuyển khoản: bỏ dấu tiếng Việt, chỉ giữ chữ/số/khoảng trắng, tối đa 50 ký tự.
|
|
7
|
+
* Giống cách img.vietqr.io chuẩn hoá (VIBECLOUD-123 → VIBECLOUD123) để khớp regex báo có /VIBECLOUD\d+/ ở app-api.
|
|
8
|
+
*/
|
|
9
|
+
export declare function transferContent(value: string): string;
|
|
10
|
+
export type VietQrParts = {
|
|
11
|
+
bin: string;
|
|
12
|
+
account: string;
|
|
13
|
+
amount?: number;
|
|
14
|
+
addInfo?: string;
|
|
15
|
+
};
|
|
16
|
+
/** Chuỗi VietQR EMVCo (NAPAS, dịch vụ QRIBFTTA = chuyển tới số tài khoản), có CRC16. */
|
|
17
|
+
export declare function buildVietQrPayload(parts: VietQrParts): string;
|
|
18
|
+
export type VietQrImage = {
|
|
19
|
+
bankCode: string;
|
|
20
|
+
bin?: string;
|
|
21
|
+
account: string;
|
|
22
|
+
amount?: number;
|
|
23
|
+
addInfo?: string;
|
|
24
|
+
accountName?: string;
|
|
25
|
+
};
|
|
26
|
+
/** Đọc link ảnh img.vietqr.io/image/<BANK|BIN>-<account>-<template>.png?amount=&addInfo=&accountName= */
|
|
27
|
+
export declare function parseVietQrImageUrl(value: string | undefined): VietQrImage | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* QR bằng ký tự khối đầy '██' (1 module = 2 cột × 1 dòng, vùng lặng 2 module), in thẳng trong terminal.
|
|
30
|
+
* Chỉ dùng U+2588 nên mọi font monospace vẽ kín ô, không hở nét như nửa khối.
|
|
31
|
+
* darkTerminal=true (mặc định: nền tối, chữ sáng) vẽ module SÁNG bằng khối → QR đúng cực (đen trên trắng).
|
|
32
|
+
* darkTerminal=false (nền sáng, chữ đen) vẽ module TỐI bằng khối.
|
|
33
|
+
*/
|
|
34
|
+
export declare function renderQrAscii(payload: string, darkTerminal?: boolean): string;
|
|
35
|
+
/** Bản nửa khối (▀▄█) gọn hơn, 2 hàng module / 1 dòng; tuỳ font terminal có thể hở nét. */
|
|
36
|
+
export declare function renderQrAsciiSmall(payload: string): Promise<string>;
|
|
37
|
+
export declare function renderQrPng(payload: string): Promise<Buffer>;
|
|
38
|
+
export declare function dataUrlToBuffer(value: unknown): Buffer | undefined;
|
|
39
|
+
export declare function saveQrFile(dir: string, name: string, buffer: Buffer): Promise<string>;
|
|
40
|
+
/**
|
|
41
|
+
* Chuẩn hoá kết quả nạp ví để AI đưa cho người dùng: bỏ base64 khỏi text (tốn 15–25k token, CLI không vẽ),
|
|
42
|
+
* thêm qr_ascii (terminal nền tối) / qr_ascii_light (nền sáng) / qr_ascii_small (nửa khối), qr_file (PNG trên máy
|
|
43
|
+
* người dùng), giữ qr_url (ảnh VietQR) và các trường tiền/nội dung.
|
|
44
|
+
*/
|
|
45
|
+
export declare function presentTopup(topup: JsonObject, qrDir: string): Promise<JsonObject>;
|
|
46
|
+
export {};
|
package/dist/qr.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// QR nạp ví hiện được ngay trong terminal (Claude Code / Codex / Gemini CLI).
|
|
2
|
+
// Các CLI không vẽ ảnh PNG từ tool result, nên MCP tự dựng chuỗi VietQR EMVCo (NAPAS)
|
|
3
|
+
// rồi in QR bằng ký tự khối; kèm file PNG + link ảnh để người dùng chọn cách quét.
|
|
4
|
+
import QRCode from 'qrcode';
|
|
5
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
// Mã ngân hàng (như img.vietqr.io dùng) → BIN NAPAS. Bổ sung khi MONA nhận tiền ở ngân hàng khác.
|
|
8
|
+
export const BANK_BIN = {
|
|
9
|
+
ACB: '970416',
|
|
10
|
+
VCB: '970436', VIETCOMBANK: '970436',
|
|
11
|
+
TCB: '970407', TECHCOMBANK: '970407',
|
|
12
|
+
MB: '970422', MBB: '970422', MBBANK: '970422',
|
|
13
|
+
BIDV: '970418',
|
|
14
|
+
VPB: '970432', VPBANK: '970432',
|
|
15
|
+
TPB: '970423', TPBANK: '970423',
|
|
16
|
+
VIB: '970441',
|
|
17
|
+
STB: '970403', SACOMBANK: '970403',
|
|
18
|
+
CTG: '970415', ICB: '970415', VIETINBANK: '970415',
|
|
19
|
+
VBA: '970405', AGRIBANK: '970405',
|
|
20
|
+
MSB: '970426',
|
|
21
|
+
SHB: '970443',
|
|
22
|
+
HDB: '970437', HDBANK: '970437',
|
|
23
|
+
OCB: '970448',
|
|
24
|
+
EIB: '970431', EXIMBANK: '970431',
|
|
25
|
+
LPB: '970449', LPBANK: '970449',
|
|
26
|
+
SEAB: '970440', SEABANK: '970440',
|
|
27
|
+
NAB: '970428', NAMABANK: '970428',
|
|
28
|
+
BAB: '970409', BACABANK: '970409',
|
|
29
|
+
NCB: '970419',
|
|
30
|
+
ABB: '970425', ABBANK: '970425',
|
|
31
|
+
PVCB: '970412', PVCOMBANK: '970412',
|
|
32
|
+
VAB: '970427', VIETABANK: '970427',
|
|
33
|
+
KLB: '970452', KIENLONGBANK: '970452',
|
|
34
|
+
PGB: '970430', PGBANK: '970430',
|
|
35
|
+
BVB: '970438', BAOVIETBANK: '970438',
|
|
36
|
+
VCCB: '970454', BVBANK: '970454',
|
|
37
|
+
CAKE: '546034',
|
|
38
|
+
TIMO: '963388',
|
|
39
|
+
};
|
|
40
|
+
const tlv = (id, value) => `${id}${String(value.length).padStart(2, '0')}${value}`;
|
|
41
|
+
/** CRC-16/CCITT-FALSE (poly 0x1021, init 0xFFFF) theo chuẩn EMVCo, trả 4 ký tự hex hoa. */
|
|
42
|
+
export function crc16(input) {
|
|
43
|
+
let crc = 0xffff;
|
|
44
|
+
for (const byte of Buffer.from(input, 'utf8')) {
|
|
45
|
+
crc ^= byte << 8;
|
|
46
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
47
|
+
crc = crc & 0x8000 ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return crc.toString(16).toUpperCase().padStart(4, '0');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Nội dung chuyển khoản: bỏ dấu tiếng Việt, chỉ giữ chữ/số/khoảng trắng, tối đa 50 ký tự.
|
|
54
|
+
* Giống cách img.vietqr.io chuẩn hoá (VIBECLOUD-123 → VIBECLOUD123) để khớp regex báo có /VIBECLOUD\d+/ ở app-api.
|
|
55
|
+
*/
|
|
56
|
+
export function transferContent(value) {
|
|
57
|
+
return value
|
|
58
|
+
.normalize('NFD')
|
|
59
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
60
|
+
.replace(/đ/g, 'd')
|
|
61
|
+
.replace(/Đ/g, 'D')
|
|
62
|
+
.replace(/[^A-Za-z0-9 ]/g, '')
|
|
63
|
+
.replace(/\s+/g, ' ')
|
|
64
|
+
.trim()
|
|
65
|
+
.slice(0, 50);
|
|
66
|
+
}
|
|
67
|
+
/** Chuỗi VietQR EMVCo (NAPAS, dịch vụ QRIBFTTA = chuyển tới số tài khoản), có CRC16. */
|
|
68
|
+
export function buildVietQrPayload(parts) {
|
|
69
|
+
if (!/^\d{6}$/.test(parts.bin))
|
|
70
|
+
throw new Error(`BIN ngân hàng không hợp lệ: ${parts.bin}`);
|
|
71
|
+
if (!/^[A-Za-z0-9]{1,19}$/.test(parts.account))
|
|
72
|
+
throw new Error(`Số tài khoản không hợp lệ: ${parts.account}`);
|
|
73
|
+
const amount = parts.amount !== undefined ? Math.round(parts.amount) : undefined;
|
|
74
|
+
const merchant = tlv('00', 'A000000727') + tlv('01', tlv('00', parts.bin) + tlv('01', parts.account)) + tlv('02', 'QRIBFTTA');
|
|
75
|
+
let payload = tlv('00', '01') + tlv('01', amount ? '12' : '11') + tlv('38', merchant) + tlv('53', '704');
|
|
76
|
+
if (amount && amount > 0)
|
|
77
|
+
payload += tlv('54', String(amount));
|
|
78
|
+
payload += tlv('58', 'VN');
|
|
79
|
+
const content = parts.addInfo ? transferContent(parts.addInfo) : '';
|
|
80
|
+
if (content)
|
|
81
|
+
payload += tlv('62', tlv('08', content));
|
|
82
|
+
payload += '6304';
|
|
83
|
+
return payload + crc16(payload);
|
|
84
|
+
}
|
|
85
|
+
/** Đọc link ảnh img.vietqr.io/image/<BANK|BIN>-<account>-<template>.png?amount=&addInfo=&accountName= */
|
|
86
|
+
export function parseVietQrImageUrl(value) {
|
|
87
|
+
if (!value)
|
|
88
|
+
return undefined;
|
|
89
|
+
let url;
|
|
90
|
+
try {
|
|
91
|
+
url = new URL(value);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const match = /\/image\/([A-Za-z0-9]+)-([A-Za-z0-9]+)-[A-Za-z0-9_]+\.(?:png|jpe?g)$/i.exec(url.pathname);
|
|
97
|
+
if (!match)
|
|
98
|
+
return undefined;
|
|
99
|
+
const bankCode = match[1].toUpperCase();
|
|
100
|
+
const bin = /^\d{6}$/.test(bankCode) ? bankCode : BANK_BIN[bankCode];
|
|
101
|
+
const amountRaw = url.searchParams.get('amount');
|
|
102
|
+
const amount = amountRaw && /^\d+$/.test(amountRaw) ? Number(amountRaw) : undefined;
|
|
103
|
+
return {
|
|
104
|
+
bankCode,
|
|
105
|
+
...(bin ? { bin } : {}),
|
|
106
|
+
account: match[2],
|
|
107
|
+
...(amount !== undefined ? { amount } : {}),
|
|
108
|
+
...(url.searchParams.get('addInfo') ? { addInfo: url.searchParams.get('addInfo') } : {}),
|
|
109
|
+
...(url.searchParams.get('accountName') ? { accountName: url.searchParams.get('accountName') } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* QR bằng ký tự khối đầy '██' (1 module = 2 cột × 1 dòng, vùng lặng 2 module), in thẳng trong terminal.
|
|
114
|
+
* Chỉ dùng U+2588 nên mọi font monospace vẽ kín ô, không hở nét như nửa khối.
|
|
115
|
+
* darkTerminal=true (mặc định: nền tối, chữ sáng) vẽ module SÁNG bằng khối → QR đúng cực (đen trên trắng).
|
|
116
|
+
* darkTerminal=false (nền sáng, chữ đen) vẽ module TỐI bằng khối.
|
|
117
|
+
*/
|
|
118
|
+
export function renderQrAscii(payload, darkTerminal = true) {
|
|
119
|
+
const code = QRCode.create(payload, { errorCorrectionLevel: 'M' });
|
|
120
|
+
const size = code.modules.size;
|
|
121
|
+
const data = code.modules.data;
|
|
122
|
+
const quiet = 2;
|
|
123
|
+
const block = '██';
|
|
124
|
+
const blank = ' ';
|
|
125
|
+
const fill = (dark) => (dark === darkTerminal ? blank : block);
|
|
126
|
+
const width = size + quiet * 2;
|
|
127
|
+
const edge = fill(false).repeat(width);
|
|
128
|
+
const lines = [];
|
|
129
|
+
for (let i = 0; i < quiet; i += 1)
|
|
130
|
+
lines.push(edge);
|
|
131
|
+
for (let row = 0; row < size; row += 1) {
|
|
132
|
+
let line = fill(false).repeat(quiet);
|
|
133
|
+
for (let col = 0; col < size; col += 1)
|
|
134
|
+
line += fill(Boolean(data[row * size + col]));
|
|
135
|
+
lines.push(line + fill(false).repeat(quiet));
|
|
136
|
+
}
|
|
137
|
+
for (let i = 0; i < quiet; i += 1)
|
|
138
|
+
lines.push(edge);
|
|
139
|
+
return lines.join('\n');
|
|
140
|
+
}
|
|
141
|
+
/** Bản nửa khối (▀▄█) gọn hơn, 2 hàng module / 1 dòng; tuỳ font terminal có thể hở nét. */
|
|
142
|
+
export function renderQrAsciiSmall(payload) {
|
|
143
|
+
// qrcode 1.5.x: renderer utf8 lỗi 'Invalid array length' khi truyền margin → dùng mặc định.
|
|
144
|
+
return QRCode.toString(payload, { type: 'utf8' });
|
|
145
|
+
}
|
|
146
|
+
export function renderQrPng(payload) {
|
|
147
|
+
return QRCode.toBuffer(payload, { type: 'png', errorCorrectionLevel: 'M', margin: 2, scale: 8 });
|
|
148
|
+
}
|
|
149
|
+
export function dataUrlToBuffer(value) {
|
|
150
|
+
if (typeof value !== 'string')
|
|
151
|
+
return undefined;
|
|
152
|
+
const match = /^data:image\/(?:png|jpe?g);base64,([A-Za-z0-9+/=\s]+)$/i.exec(value);
|
|
153
|
+
if (!match)
|
|
154
|
+
return undefined;
|
|
155
|
+
const buffer = Buffer.from(match[1].replace(/\s+/g, ''), 'base64');
|
|
156
|
+
return buffer.length ? buffer : undefined;
|
|
157
|
+
}
|
|
158
|
+
export async function saveQrFile(dir, name, buffer) {
|
|
159
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
160
|
+
const file = join(dir, name);
|
|
161
|
+
await writeFile(file, buffer, { mode: 0o600 });
|
|
162
|
+
return file;
|
|
163
|
+
}
|
|
164
|
+
const safeName = (value) => value.replace(/[^A-Za-z0-9_-]/g, '_').slice(0, 64) || 'topup';
|
|
165
|
+
/**
|
|
166
|
+
* Chuẩn hoá kết quả nạp ví để AI đưa cho người dùng: bỏ base64 khỏi text (tốn 15–25k token, CLI không vẽ),
|
|
167
|
+
* thêm qr_ascii (terminal nền tối) / qr_ascii_light (nền sáng) / qr_ascii_small (nửa khối), qr_file (PNG trên máy
|
|
168
|
+
* người dùng), giữ qr_url (ảnh VietQR) và các trường tiền/nội dung.
|
|
169
|
+
*/
|
|
170
|
+
export async function presentTopup(topup, qrDir) {
|
|
171
|
+
const { qr_data_url: qrDataUrl, ...rest } = topup;
|
|
172
|
+
const orderRef = typeof rest.order_ref === 'string' ? rest.order_ref : typeof rest.topup_id === 'string' ? rest.topup_id : 'topup';
|
|
173
|
+
const image = parseVietQrImageUrl(typeof rest.qr_url === 'string' ? rest.qr_url : undefined);
|
|
174
|
+
const amount = typeof rest.amount_vnd === 'number' ? rest.amount_vnd : image?.amount;
|
|
175
|
+
const result = {
|
|
176
|
+
...rest,
|
|
177
|
+
...(amount !== undefined ? { amount_vnd: amount } : {}),
|
|
178
|
+
};
|
|
179
|
+
let payload;
|
|
180
|
+
if (image?.bin) {
|
|
181
|
+
try {
|
|
182
|
+
payload = buildVietQrPayload({ bin: image.bin, account: image.account, amount, addInfo: image.addInfo ?? orderRef });
|
|
183
|
+
result.bank = image.bankCode;
|
|
184
|
+
result.bank_bin = image.bin;
|
|
185
|
+
result.bank_account = image.account;
|
|
186
|
+
if (image.accountName)
|
|
187
|
+
result.bank_account_name = image.accountName;
|
|
188
|
+
result.transfer_content = transferContent(image.addInfo ?? orderRef);
|
|
189
|
+
result.qr_payload = payload;
|
|
190
|
+
result.qr_ascii = renderQrAscii(payload, true);
|
|
191
|
+
result.qr_ascii_light = renderQrAscii(payload, false);
|
|
192
|
+
result.qr_ascii_small = await renderQrAsciiSmall(payload);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
result.qr_ascii_error = error instanceof Error ? error.message : String(error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const png = dataUrlToBuffer(qrDataUrl) ?? (payload ? await renderQrPng(payload) : undefined);
|
|
199
|
+
if (png) {
|
|
200
|
+
try {
|
|
201
|
+
result.qr_file = await saveQrFile(qrDir, `topup-${safeName(orderRef)}.png`, png);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
result.qr_file_error = error instanceof Error ? error.message : String(error);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -11,6 +11,7 @@ import { registerMailTools } from './mail.js';
|
|
|
11
11
|
import { APP_FLOW, createVps, registerComputeTools } from './compute.js';
|
|
12
12
|
import { pollJob } from './jobs.js';
|
|
13
13
|
import { registerBaseTools } from './base.js';
|
|
14
|
+
import { presentTopup } from './qr.js';
|
|
14
15
|
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. Base beta thay Supabase bằng cloud_base_*, chung account/ví MONA Cloud và khớp app deploy. 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.';
|
|
15
16
|
const SANDBOX_DESCRIPTION = 'sandbox=true: thử 0đ, không cần ví';
|
|
16
17
|
const sandboxSchema = z.boolean().optional().describe(SANDBOX_DESCRIPTION);
|
|
@@ -91,7 +92,9 @@ export function createServer(dependencies = {}) {
|
|
|
91
92
|
const auth = new AuthManager(config, env, fetchImpl);
|
|
92
93
|
const clients = new CloudClients(config, auth, fetchImpl);
|
|
93
94
|
const catalog = new TemplateCatalog(config, fetchImpl);
|
|
94
|
-
const server = new McpServer({ name: 'monacloud-mcp', version: '0.
|
|
95
|
+
const server = new McpServer({ name: 'monacloud-mcp', version: '0.8.1' },
|
|
96
|
+
// Codex chỉ chắc chắn đọc 512 ký tự đầu của instructions → luật quan trọng nhất đặt trước, mô tả hệ đặt sau.
|
|
97
|
+
{ instructions: `Ví thiếu tiền: AI tự gọi cloud_topup, in nguyên khối qr_ascii (QR VietQR) cho người dùng quét bằng app ngân hàng ngay trong terminal, rồi cloud_topup_status tới khi paid; không bảo người dùng mở console để nạp. Dù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 chờ người dùng ở: quét QR nạp, duyệt chi phí, thêm DNS, OTP hoặc KYC.\n${ENTITY}\n${APP_FLOW}` });
|
|
95
98
|
server.registerTool('cloud_whoami', {
|
|
96
99
|
title: 'Tài khoản MONA Cloud',
|
|
97
100
|
description: 'Xác minh MONA Pass hiện tại. / Return the current MONA Pass profile.',
|
|
@@ -109,16 +112,23 @@ export function createServer(dependencies = {}) {
|
|
|
109
112
|
},
|
|
110
113
|
}, ({ cursor, limit }) => runTool(() => clients.ledger(cursor, limit)));
|
|
111
114
|
server.registerTool('cloud_topup', {
|
|
112
|
-
title: 'Nạp ví bằng VietQR',
|
|
113
|
-
description: '
|
|
115
|
+
title: 'Nạp ví bằng VietQR ngay trong terminal',
|
|
116
|
+
description: 'AI tự tạo yêu cầu nạp ví và nhận QR VietQR: qr_ascii (khối đầy, in thẳng trong Claude Code/Codex/Gemini; nền sáng dùng qr_ascii_light), qr_file (PNG trên máy), qr_url (ảnh). Người dùng chỉ quét bằng app ngân hàng, không cần mở console. Tiền vào tự cộng ví; xác nhận bằng cloud_topup_status.',
|
|
114
117
|
inputSchema: {
|
|
115
|
-
amount: z.number().int().min(
|
|
118
|
+
amount: z.number().int().min(10_000).max(500_000_000).describe('Số tiền nguyên VND, tối thiểu 10.000'),
|
|
116
119
|
idempotency_key: z.string().min(1).max(255).optional(),
|
|
117
120
|
},
|
|
118
121
|
}, ({ amount, idempotency_key }) => runTool(async () => ({
|
|
119
|
-
...asObject(await clients.topup(amount, idempotency_key)),
|
|
120
|
-
instructions: '
|
|
122
|
+
...(await presentTopup(asObject(await clients.topup(amount, idempotency_key)), config.configDir)),
|
|
123
|
+
instructions: 'In NGUYÊN khối qr_ascii cho người dùng (terminal nền tối, mặc định; terminal nền sáng in qr_ascii_light; giữ nguyên ký tự, mỗi dòng một hàng, không bọc lại) kèm ngân hàng, số tài khoản, số tiền và nội dung chuyển khoản. Người dùng mở app ngân hàng quét QR trên màn hình; nếu terminal hiển thị mờ thì mở qr_file hoặc qr_url. Chuyển ĐÚNG số tiền và nội dung. Sau khi người dùng báo đã chuyển, gọi cloud_topup_status(topup_id) tới khi paid rồi làm tiếp; không tạo lại yêu cầu nạp khi chưa thấy tiền vào.',
|
|
121
124
|
})));
|
|
125
|
+
server.registerTool('cloud_topup_status', {
|
|
126
|
+
title: 'Trạng thái yêu cầu nạp ví',
|
|
127
|
+
description: 'Đọc trạng thái một yêu cầu nạp (pending/paid) sau khi người dùng quét QR, kèm số dư hiện tại. Dùng để chờ tiền vào rồi tiếp tục việc đang làm.',
|
|
128
|
+
inputSchema: {
|
|
129
|
+
topup_id: z.string().min(1).max(128).describe('topup_id do cloud_topup trả'),
|
|
130
|
+
},
|
|
131
|
+
}, ({ topup_id }) => runTool(() => clients.topupStatus(topup_id)));
|
|
122
132
|
server.registerTool('cloud_usage', {
|
|
123
133
|
title: 'Chi phí MONA Cloud theo kỳ',
|
|
124
134
|
description: 'Đọc usage và tổng tiền theo tháng, có thể lọc sản phẩm.',
|
|
@@ -181,7 +191,7 @@ export function createServer(dependencies = {}) {
|
|
|
181
191
|
}, ({ spend_limit_vnd, period, token_id }) => runTool(() => clients.tokenLimit(spend_limit_vnd, period, token_id)));
|
|
182
192
|
server.registerTool('cloud_open_console', {
|
|
183
193
|
title: 'Mở MONA Cloud Console',
|
|
184
|
-
description: 'Trả URL console
|
|
194
|
+
description: 'Trả URL console khi người dùng muốn tự xem hoá đơn hoặc quản lý tài khoản. Nạp ví KHÔNG cần console: dùng cloud_topup (QR in trong terminal).',
|
|
185
195
|
}, () => textResult({ url: config.consoleUrl, next_step: `Mở ${config.consoleUrl} trong trình duyệt.` }));
|
|
186
196
|
server.registerTool('monapay_link', {
|
|
187
197
|
title: 'Liên kết MONA Pay chuyển tiếp',
|
|
@@ -349,7 +359,7 @@ export function createServer(dependencies = {}) {
|
|
|
349
359
|
}, async (uri) => ({ contents: [{
|
|
350
360
|
uri: uri.href,
|
|
351
361
|
mimeType: 'text/plain',
|
|
352
|
-
text: `${ENTITY}\n\n${APP_FLOW}\n\nHuman đăng ký MONA Pass, duyệt chi phí, thêm DNS khi cần, nạp
|
|
362
|
+
text: `${ENTITY}\n\n${APP_FLOW}\n\nHuman đăng ký MONA Pass một lần, duyệt chi phí, thêm DNS khi cần, quét QR nạp ví do AI tạo bằng cloud_topup (in ngay trong terminal, không mở console) 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, Base beta thay Supabase, job, vòng đời service, plans, subscriptions, invoices/PDF, credit và apps từ git hoặc thư mục local (source=upload); cần DB/Supabase thì dùng cloud_base_create — chung account, khớp app deploy; cloud_app_detect hoàn toàn offline.\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`,
|
|
353
363
|
}] }));
|
|
354
364
|
server.registerResource('monacloud-status', 'monacloud://status', {
|
|
355
365
|
title: 'Trạng thái hệ MONA Cloud',
|
|
@@ -376,7 +386,7 @@ export function createServer(dependencies = {}) {
|
|
|
376
386
|
role: 'user',
|
|
377
387
|
content: {
|
|
378
388
|
type: 'text',
|
|
379
|
-
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. ${APP_FLOW}\nTheo đú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
|
|
389
|
+
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. ${APP_FLOW}\nTheo đú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, in nguyên khối qr_ascii kèm số tiền/nội dung để người dùng quét bằng app ngân hàng, rồi cloud_topup_status tới khi paid; không bảo người dùng mở console.\n2. Nếu dùng VPS gọi cloud_vps_create và cloud_db_create. Cần DB/Supabase thì ưu tiên cloud_base_create (beta) — chung account, khớp app deploy. Nếu deploy dự án dùng cloud_app_detect(local_dir), cloud_app_host_list rồi cloud_app_create(local_dir), 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 dự án local, dùng cloud_app_create(local_dir) sau khi duyệt ước tính; git dùng repo_url; 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; nạp tiền cũng qua cloud_topup, người dùng chỉ quét QR và nhập OTP bắt buộc.`,
|
|
380
390
|
},
|
|
381
391
|
}] }));
|
|
382
392
|
server.registerPrompt('gui-mail-otp-monamail', {
|
|
@@ -399,7 +409,7 @@ export function createServer(dependencies = {}) {
|
|
|
399
409
|
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.
|
|
400
410
|
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ả.
|
|
401
411
|
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.
|
|
402
|
-
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
|
|
412
|
+
Chỉ dừng hỏi người dùng ở bước thêm DNS hoặc nạp tiền (quét QR). Nếu mail_plan_set trả insufficient_funds, gọi cloud_topup, in nguyên khối qr_ascii để người dùng quét bằng app ngân hàng, rồi cloud_topup_status tới khi paid 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.`,
|
|
403
413
|
},
|
|
404
414
|
}] }));
|
|
405
415
|
return server;
|
package/dist/templates.js
CHANGED
|
@@ -5,7 +5,7 @@ import { requestJson } from './http.js';
|
|
|
5
5
|
const BUILTIN = [
|
|
6
6
|
{ slug: 'cskh-zalo', name: 'CSKH Zalo', description: 'Trả lời khách từ tài liệu shop và chuyển ca khó cho người.', products: ['MONA AI', 'MONA Base', 'Zalo ZNS'] },
|
|
7
7
|
{ slug: 'sales-chot-don', name: 'Sales chốt đơn', description: 'Tư vấn, báo giá, tạo QR MONA Pay và xác nhận tiền vào.', products: ['MONA Pay', 'MONA AI'] },
|
|
8
|
-
{ slug: 'ke-toan-hddt', name: 'Kế toán HĐĐT', description: 'Đọc tiền vào, phát hành hoá đơn điện tử và nhắc công nợ.', products: ['MONA Pay', '
|
|
8
|
+
{ slug: 'ke-toan-hddt', name: 'Kế toán HĐĐT', description: 'Đọc tiền vào, phát hành hoá đơn điện tử và nhắc công nợ.', products: ['MONA Pay', 'MONA eInvoice', 'MONA Mail'] },
|
|
9
9
|
{ slug: 'content-seo', name: 'Content SEO', description: 'Viết và đăng bài theo voice thương hiệu, có gate QC.', products: ['MONA AI'] },
|
|
10
10
|
{ slug: 'noi-bo-kin', name: 'Nội bộ kín', description: 'Trợ lý đọc tài liệu công ty, dữ liệu không rời server.', products: ['MONA Cloud', 'MONA Base', 'Ollama'] },
|
|
11
11
|
{ slug: 'tro-giang-academy', name: 'Trợ giảng Academy', description: 'Trợ giảng cho academy của giảng viên.', products: ['mona.academy', 'MONA AI'] },
|
package/docs/ai-agent.md
CHANGED
|
@@ -13,13 +13,13 @@ Prompt Claude Code: **“Đưa dự án này lên MONA Cloud, dùng thư mục h
|
|
|
13
13
|
5. Sửa code local: `cloud_app_deploy({app_id, local_dir})`. Chỉ đổi env: `cloud_app_env_set` với map đầy đủ rồi `cloud_app_deploy({app_id})`. Timeout giữ job_id/app_id để tiếp tục, không tạo app mới.
|
|
14
14
|
6. Cần DB/Supabase thì gọi `cloud_base_create` (beta): sandbox để lấy ước tính, duyệt chi phí rồi tạo thật. Base dùng chung account/ví MONA Cloud và khớp app deploy. Lưu credential vào secret store hoặc `.env` không commit; không log `anon_key`, `service_key`, `db_url`.
|
|
15
15
|
|
|
16
|
-
Human chỉ cần đăng ký MONA Pass/device flow, duyệt chi phí một lần và nạp
|
|
16
|
+
Human chỉ cần đăng ký MONA Pass/device flow một lần, duyệt chi phí một lần và quét QR nạp ví do AI tạo (`cloud_topup`) khi hết credit 20k; DNS là bước thêm khi dùng domain riêng. AI làm các bước kỹ thuật còn lại. `.env*` kể cả `.env.example` không vào ZIP; truyền secret cần thiết qua `env`, không công khai. Xem [hợp đồng local deploy](local-deploy.md). VPS/database thủ công dưới đây chỉ dùng khi dự án cần hoặc người dùng chọn.
|
|
17
17
|
|
|
18
18
|
## Quy tắc vận hành
|
|
19
19
|
|
|
20
20
|
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.
|
|
21
21
|
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í.
|
|
22
|
-
3. Khi cần nạp, gọi `cloud_topup`;
|
|
22
|
+
3. Khi cần nạp, gọi `cloud_topup`; in NGUYÊN khối `qr_ascii` (không bọc lại) kèm ngân hàng, số tài khoản, số tiền, nội dung chuyển khoản; nhắc `qr_file`/`qr_url` nếu terminal hiển thị mờ. Không bảo người dùng mở console. Chỉ tiếp tục sau khi `cloud_topup_status` trả `paid` hoặc `cloud_balance` phản ánh tiền vào.
|
|
23
23
|
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.
|
|
24
24
|
5. Mọi endpoint webhook phải xác minh HMAC, chống replay theo timestamp và idempotent theo `transaction_code`.
|
|
25
25
|
6. Không giao hàng chỉ dựa vào browser redirect. Chỉ xác nhận đơn sau webhook `CHECKOUT_PAID` hoặc đối soát server-side.
|
|
@@ -175,7 +175,7 @@ const { id } = await monamail.emails.send({
|
|
|
175
175
|
|
|
176
176
|
`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`.
|
|
177
177
|
|
|
178
|
-
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`,
|
|
178
|
+
Ranh giới human của luồng Mail sau đăng nhập là **thêm DNS và quét QR nạp tiền**. Khi `mail_plan_set` trả `insufficient_funds`, gọi `cloud_topup`, in `qr_ascii` cho user quét và chờ `cloud_topup_status` paid hoặc `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`.
|
|
179
179
|
|
|
180
180
|
### Sandbox và idempotency Mail
|
|
181
181
|
|
|
@@ -192,7 +192,7 @@ Ranh giới human của luồng Mail sau đăng nhập là **thêm DNS và nạp
|
|
|
192
192
|
{
|
|
193
193
|
"code": "insufficient_funds",
|
|
194
194
|
"message": "Ví thiếu tiền...",
|
|
195
|
-
"next_step": "
|
|
195
|
+
"next_step": "Gọi cloud_topup để lấy QR nạp ví rồi gọi lại tool."
|
|
196
196
|
}
|
|
197
197
|
```
|
|
198
198
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monacloud-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for MONA Cloud
|
|
3
|
+
"version": "0.8.1",
|
|
4
|
+
"description": "MCP server for MONA Cloud — deploy apps (Vercel alternative), Supabase-compatible Postgres/Auth/Storage bases (Supabase alternative), VND wallet, MONA Pay & Mail — for AI agents (Claude, Cursor, Codex) in Vietnam.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://monacloud.vn",
|
|
7
7
|
"repository": {
|
|
@@ -52,10 +52,12 @@
|
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
54
54
|
"monapay-mcp": "^0.5.5",
|
|
55
|
+
"qrcode": "^1.5.4",
|
|
55
56
|
"zod": "^4.4.3"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@types/node": "^26.4.0",
|
|
60
|
+
"@types/qrcode": "^1.5.6",
|
|
59
61
|
"typescript": "^7.0.2"
|
|
60
62
|
}
|
|
61
63
|
}
|