monacloud-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js ADDED
@@ -0,0 +1,312 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { z } from 'zod';
4
+ import { readConfig } from './config.js';
5
+ import { AuthManager } from './auth.js';
6
+ import { CloudClients } from './clients.js';
7
+ import { errorResult, runTool, textResult, toAgentError } from './errors.js';
8
+ import { requestJson, unwrapData } from './http.js';
9
+ import { createMonaPayClient, importedMonaPayTools, normalizeImportedResult } from './monapay.js';
10
+ import { TemplateCatalog } from './templates.js';
11
+ const ENTITY = 'MONA Cloud là hệ công cụ cho vibecoder Việt Nam: một MONA ID, một ví VND và một MCP để deploy VibeCloud, thu tiền MONA Pay và chạy các sản phẩm MONA.';
12
+ const TERMINAL_JOBS = new Set(['succeeded', 'failed', 'cancelled']);
13
+ const asObject = (value) => (value && typeof value === 'object' ? value : {});
14
+ function collection(value) {
15
+ const data = unwrapData(value);
16
+ if (Array.isArray(data))
17
+ return data.filter((item) => item && typeof item === 'object');
18
+ const object = asObject(data);
19
+ for (const key of ['items', 'data', 'results', 'bank_accounts', 'virtual_accounts', 'webhooks']) {
20
+ const nested = object[key];
21
+ if (Array.isArray(nested))
22
+ return nested.filter((item) => item && typeof item === 'object');
23
+ }
24
+ return [];
25
+ }
26
+ const sizingSchema = {
27
+ app_name: z.string().min(2).max(80),
28
+ package_slug: z.string().min(1).max(64).optional(),
29
+ cpu: z.number().int().min(1).max(16).optional(),
30
+ ram_gb: z.number().int().min(1).max(64).optional(),
31
+ disk_gb: z.number().int().min(10).max(1000).optional(),
32
+ };
33
+ const provisionSchema = z.object(sizingSchema).superRefine((value, context) => {
34
+ const dimensions = [value.cpu, value.ram_gb, value.disk_gb];
35
+ const hasAny = dimensions.some((item) => item !== undefined);
36
+ const hasAll = dimensions.every((item) => item !== undefined);
37
+ if (value.package_slug && hasAny) {
38
+ context.addIssue({ code: 'custom', message: 'Dùng package_slug hoặc bộ cpu/ram_gb/disk_gb, không dùng cả hai.' });
39
+ }
40
+ if (!value.package_slug && !hasAll) {
41
+ context.addIssue({ code: 'custom', message: 'Cần package_slug hoặc đủ cpu, ram_gb và disk_gb.' });
42
+ }
43
+ });
44
+ function agentRuntimeStub(template) {
45
+ return {
46
+ status: 'not_available',
47
+ code: 'agent_runtime_pending',
48
+ template,
49
+ message: 'Runtime MONA Agent trên VibeCloud chưa được phát hành; tool này là slot wave kế tiếp theo brief.',
50
+ next_step: 'Dùng agent_templates_get để lấy nội dung template và triển khai thủ công, hoặc thử lại khi VibeCloud bật runtime agent.',
51
+ };
52
+ }
53
+ async function pollJob(clients, jobId, wait, intervalSeconds, timeoutSeconds) {
54
+ let result = await clients.vibecloud(`/api/jobs/${encodeURIComponent(jobId)}`);
55
+ if (!wait)
56
+ return result;
57
+ const deadline = Date.now() + timeoutSeconds * 1000;
58
+ while (!TERMINAL_JOBS.has(String(asObject(result).status || '')) && Date.now() < deadline) {
59
+ await delay(intervalSeconds * 1000);
60
+ result = await clients.vibecloud(`/api/jobs/${encodeURIComponent(jobId)}`);
61
+ }
62
+ if (!TERMINAL_JOBS.has(String(asObject(result).status || ''))) {
63
+ return { ...asObject(result), polling: 'timeout', next_step: `Gọi lại vibecloud_job_status với job_id=${jobId}.` };
64
+ }
65
+ return result;
66
+ }
67
+ async function providerHealth(name, url, fetchImpl) {
68
+ const started = Date.now();
69
+ try {
70
+ const data = await requestJson(url, { fetchImpl, timeoutMs: 5_000 });
71
+ return { name, ok: true, latency_ms: Date.now() - started, data };
72
+ }
73
+ catch (error) {
74
+ return { name, ok: false, latency_ms: Date.now() - started, error: toAgentError(error) };
75
+ }
76
+ }
77
+ export function createServer(dependencies = {}) {
78
+ const env = dependencies.env || process.env;
79
+ const config = dependencies.config || readConfig(env);
80
+ const fetchImpl = dependencies.fetchImpl || fetch;
81
+ const auth = new AuthManager(config, env, fetchImpl);
82
+ const clients = new CloudClients(config, auth, fetchImpl);
83
+ const catalog = new TemplateCatalog(config, fetchImpl);
84
+ const server = new McpServer({ name: 'monacloud-mcp', version: '0.1.0' }, { instructions: `${ENTITY}\nDùng cloud_* cho tài khoản/ví, vibecloud_* cho hạ tầng, monapay_* cho thu tiền và agent_* cho catalog. Không bao giờ yêu cầu mật khẩu sản phẩm. Chỉ dừng hỏi người dùng khi cần nạp tiền, OTP ngân hàng hoặc KYC bắt buộc.` });
85
+ server.registerTool('cloud_whoami', {
86
+ title: 'Tài khoản MONA Cloud',
87
+ description: 'Xác minh MONA ID hiện tại. / Return the current MONA ID profile.',
88
+ }, () => runTool(async () => ({ ...(await auth.userinfo()), console_url: config.consoleUrl })));
89
+ server.registerTool('cloud_balance', {
90
+ title: 'Số dư ví MONA Cloud',
91
+ description: 'Đọc số dư ví VND chung trước khi tạo tài nguyên có phí.',
92
+ }, () => runTool(() => clients.balance()));
93
+ server.registerTool('cloud_ledger', {
94
+ title: 'Sổ cái ví MONA Cloud',
95
+ description: 'Đọc các dòng nạp, trừ và hoàn tiền trong ledger.',
96
+ inputSchema: {
97
+ cursor: z.string().max(2048).optional(),
98
+ limit: z.number().int().min(1).max(100).default(50),
99
+ },
100
+ }, ({ cursor, limit }) => runTool(() => clients.ledger(cursor, limit)));
101
+ server.registerTool('cloud_topup', {
102
+ title: 'Nạp ví bằng VietQR',
103
+ description: 'Tạo yêu cầu nạp ví và trả VietQR; đây là bước con người thanh toán hợp lệ.',
104
+ inputSchema: {
105
+ amount: z.number().int().min(1_000).max(1_000_000_000).describe('Số tiền nguyên VND'),
106
+ idempotency_key: z.string().min(1).max(255).optional(),
107
+ },
108
+ }, ({ amount, idempotency_key }) => runTool(async () => ({
109
+ ...asObject(await clients.topup(amount, idempotency_key)),
110
+ instructions: 'Mở app ngân hàng, quét qr_data_url và chuyển đúng số tiền/nội dung. Sau khi tiền vào, gọi cloud_balance.',
111
+ })));
112
+ server.registerTool('cloud_usage', {
113
+ title: 'Chi phí MONA Cloud theo kỳ',
114
+ description: 'Đọc usage và tổng tiền theo tháng, có thể lọc sản phẩm.',
115
+ inputSchema: {
116
+ period: z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/, 'Dùng YYYY-MM'),
117
+ product: z.string().min(1).max(100).optional(),
118
+ },
119
+ }, ({ period, product }) => runTool(() => clients.usage(period, product)));
120
+ server.registerTool('cloud_services', {
121
+ title: 'Mọi dịch vụ đang chạy',
122
+ description: 'Gom service VibeCloud cùng tài khoản ảo và webhook MONA Pay.',
123
+ }, () => runTool(async () => {
124
+ const [vibecloud, monapay] = await Promise.all([
125
+ clients.vibecloud('/api/services')
126
+ .then((services) => ({ ok: true, services }))
127
+ .catch((error) => ({ ok: false, error: toAgentError(error) })),
128
+ (async () => {
129
+ try {
130
+ const client = createMonaPayClient(config, env, fetchImpl);
131
+ const [bankResponse, webhookResponse] = await Promise.all([
132
+ client.listBankAccounts({ page: 1, limit: 100 }),
133
+ client.listWebhooks(),
134
+ ]);
135
+ const banks = collection(bankResponse);
136
+ const virtualAccounts = (await Promise.all(banks.map(async (bank) => {
137
+ const id = typeof bank.id === 'string' ? bank.id : undefined;
138
+ return id ? collection(await client.listVirtualAccounts(id, { page: 1, limit: 100 })) : [];
139
+ }))).flat();
140
+ return { ok: true, bank_accounts: banks, virtual_accounts: virtualAccounts, webhooks: collection(webhookResponse) };
141
+ }
142
+ catch (error) {
143
+ return { ok: false, error: toAgentError(error) };
144
+ }
145
+ })(),
146
+ ]);
147
+ return { vibecloud, monapay };
148
+ }));
149
+ server.registerTool('cloud_budget_set', {
150
+ title: 'Đặt ngân sách MONA Cloud',
151
+ description: 'Đặt giới hạn chi tiêu theo product, project hoặc token.',
152
+ inputSchema: {
153
+ scope: z.enum(['product', 'project', 'token']),
154
+ scope_id: z.string().min(1).max(255),
155
+ limit_vnd: z.number().int().min(0),
156
+ period: z.enum(['day', 'month']),
157
+ },
158
+ }, (body) => runTool(() => clients.budgetSet(body)));
159
+ server.registerTool('cloud_budget_get', {
160
+ title: 'Đọc ngân sách MONA Cloud',
161
+ description: 'Liệt kê giới hạn và mức đã dùng theo kỳ.',
162
+ }, () => runTool(() => clients.budgetGet()));
163
+ server.registerTool('cloud_token_limit', {
164
+ title: 'Giới hạn chi tiêu của token',
165
+ description: 'Đặt spend guard riêng cho token hiện tại hoặc token_id chỉ định.',
166
+ inputSchema: {
167
+ spend_limit_vnd: z.number().int().min(0),
168
+ period: z.enum(['day', 'month']),
169
+ token_id: z.string().min(1).max(255).optional(),
170
+ },
171
+ }, ({ spend_limit_vnd, period, token_id }) => runTool(() => clients.tokenLimit(spend_limit_vnd, period, token_id)));
172
+ server.registerTool('cloud_open_console', {
173
+ title: 'Mở MONA Cloud Console',
174
+ description: 'Trả URL console chung để nạp ví, đổi budget hoặc quản lý tài khoản.',
175
+ }, () => textResult({ url: config.consoleUrl, next_step: `Mở ${config.consoleUrl} trong trình duyệt.` }));
176
+ server.registerTool('monapay_link', {
177
+ title: 'Liên kết MONA Pay chuyển tiếp',
178
+ description: 'Tạm đổi MONA ID thành client credential MONA Pay và cache cục bộ; bỏ khi MONA Pay nhận JWT trực tiếp.',
179
+ }, () => runTool(() => clients.monapayLink()));
180
+ server.registerTool('vibecloud_link', {
181
+ title: 'Liên kết VibeCloud chuyển tiếp',
182
+ description: 'Xác nhận VibeCloud nhận MONA ID trực tiếp; chỉ đổi thành vc_live token khi upstream còn ở chế độ cũ.',
183
+ }, () => runTool(() => clients.vibecloudLink()));
184
+ server.registerTool('vibecloud_create_vps', {
185
+ title: 'Tạo VPS VibeCloud',
186
+ description: 'Kiểm tra ví chung rồi tạo LXC VPS; trả job_id để poll.',
187
+ inputSchema: provisionSchema,
188
+ }, (body) => runTool(async () => {
189
+ await clients.spendGuard();
190
+ return clients.vibecloud('/api/lxc', { method: 'POST', body });
191
+ }));
192
+ server.registerTool('vibecloud_create_database', {
193
+ title: 'Tạo database VibeCloud',
194
+ description: 'Kiểm tra ví chung rồi tạo MongoDB/PostgreSQL/MySQL; trả job_id để poll.',
195
+ inputSchema: provisionSchema.extend({ engine: z.enum(['mongodb', 'postgresql', 'mysql']).default('mongodb') }),
196
+ }, (body) => runTool(async () => {
197
+ await clients.spendGuard();
198
+ return clients.vibecloud('/api/databases', { method: 'POST', body });
199
+ }));
200
+ server.registerTool('vibecloud_job_status', {
201
+ title: 'Theo dõi job VibeCloud',
202
+ description: 'Đọc một job; mặc định poll tới succeeded/failed/cancelled hoặc timeout.',
203
+ inputSchema: {
204
+ job_id: z.string().min(1),
205
+ wait: z.boolean().default(true),
206
+ interval_sec: z.number().int().min(1).max(30).default(3),
207
+ timeout_sec: z.number().int().min(1).max(600).default(180),
208
+ },
209
+ }, ({ job_id, wait, interval_sec, timeout_sec }) => runTool(() => pollJob(clients, job_id, wait, interval_sec, timeout_sec)));
210
+ server.registerTool('vibecloud_list_services', {
211
+ title: 'Danh sách service VibeCloud',
212
+ description: 'Liệt kê VPS và database của MONA ID hiện tại.',
213
+ }, () => runTool(() => clients.vibecloud('/api/services')));
214
+ for (const action of ['start', 'stop', 'rebuild']) {
215
+ server.registerTool(`vibecloud_${action}`, {
216
+ title: `${action} service VibeCloud`,
217
+ description: action === 'stop'
218
+ ? 'Dừng VPS/database VibeCloud; luôn cho phép dừng để người dùng hạn chế chi phí.'
219
+ : `${action} VPS/database VibeCloud. Lệnh có thể phát sinh chi phí và kiểm tra ví trước.`,
220
+ inputSchema: { service_id: z.string().min(1) },
221
+ }, ({ service_id }) => runTool(async () => {
222
+ if (action !== 'stop')
223
+ await clients.spendGuard();
224
+ return clients.vibecloud(`/api/services/${encodeURIComponent(service_id)}/${action}`, { method: 'POST' });
225
+ }));
226
+ }
227
+ server.registerTool('vibecloud_prices', {
228
+ title: 'Bảng giá VibeCloud',
229
+ description: 'Đọc đơn giá giờ hiện hành.',
230
+ }, () => runTool(() => clients.prices()));
231
+ server.registerTool('vibecloud_packages', {
232
+ title: 'Gói cấu hình VibeCloud',
233
+ description: 'Liệt kê package_slug và cấu hình CPU/RAM/đĩa.',
234
+ }, () => runTool(() => clients.packages()));
235
+ server.registerTool('vibecloud_agent_deploy', {
236
+ title: 'Deploy MONA Agent trên VibeCloud',
237
+ description: 'Slot runtime agent wave kế tiếp; hiện trả trạng thái stub rõ ràng.',
238
+ inputSchema: { template: z.string().regex(/^[a-z0-9][a-z0-9-]{0,79}$/) },
239
+ }, ({ template }) => runTool(() => agentRuntimeStub(template)));
240
+ server.registerTool('agent_templates_list', {
241
+ title: 'Catalog MONA Agent',
242
+ description: 'Đọc template từ thư mục local, URL catalog hoặc catalog wave 1 tích hợp.',
243
+ }, () => runTool(() => catalog.list()));
244
+ server.registerTool('agent_templates_get', {
245
+ title: 'Chi tiết MONA Agent template',
246
+ description: 'Đọc README, AGENTS.md, tools, deploy, checklist và skills của một template.',
247
+ inputSchema: { template: z.string().regex(/^[a-z0-9][a-z0-9-]{0,79}$/) },
248
+ }, ({ template }) => runTool(() => catalog.get(template)));
249
+ server.registerTool('agent_deploy', {
250
+ title: 'Dùng ngay MONA Agent template',
251
+ description: 'Gọi cùng runtime với vibecloud_agent_deploy.',
252
+ inputSchema: { template: z.string().regex(/^[a-z0-9][a-z0-9-]{0,79}$/) },
253
+ }, ({ template }) => runTool(() => agentRuntimeStub(template)));
254
+ const imported = importedMonaPayTools(config, env, fetchImpl);
255
+ for (const [name, tool] of Object.entries(imported)) {
256
+ if (!tool.enabled)
257
+ continue;
258
+ const register = server.registerTool.bind(server);
259
+ register(name, {
260
+ title: tool.title,
261
+ description: tool.description,
262
+ inputSchema: tool.inputSchema,
263
+ outputSchema: tool.outputSchema,
264
+ annotations: tool.annotations,
265
+ _meta: tool._meta,
266
+ }, async (...args) => {
267
+ try {
268
+ return await normalizeImportedResult(tool.handler(...args));
269
+ }
270
+ catch (error) {
271
+ return errorResult(error);
272
+ }
273
+ });
274
+ }
275
+ server.registerResource('monacloud-llms', 'monacloud://llms', {
276
+ title: 'MONA Cloud llms.txt tổng',
277
+ description: 'Bản máy đọc mô tả stack và luồng AI-first của MONA Cloud.',
278
+ mimeType: 'text/plain',
279
+ }, async (uri) => ({ contents: [{
280
+ uri: uri.href,
281
+ mimeType: 'text/plain',
282
+ text: `${ENTITY}\n\nHuman chỉ đăng ký MONA ID, nạp tiền và cung cấp OTP/KYC bắt buộc. AI dùng MCP làm phần còn lại.\n\n- cloud_*: tài khoản, ví, ledger, usage, budget, dịch vụ.\n- vibecloud_*: VPS, database, job, start/stop/rebuild, giá/gói.\n- monapay_*: nối ngân hàng, checkout/QR, giao dịch, webhook và email.\n- agent_*: catalog và deploy template.\n\nConsole: ${config.consoleUrl}\nVibeCloud: https://vibecloud.vn\nMONA Pay: https://monapay.vn\n`,
283
+ }] }));
284
+ server.registerResource('monacloud-status', 'monacloud://status', {
285
+ title: 'Trạng thái hệ MONA Cloud',
286
+ description: 'Health tổng hợp của MONA ID, billing, VibeCloud và MONA Pay.',
287
+ mimeType: 'application/json',
288
+ }, async (uri) => {
289
+ const members = await Promise.all([
290
+ providerHealth('mona-id', `${config.issuer}/.well-known/openid-configuration`, fetchImpl),
291
+ providerHealth('billing', `${config.billingUrl}/v1/healthz`, fetchImpl),
292
+ providerHealth('vibecloud', `${config.vibecloudApi}/api/prices`, fetchImpl),
293
+ providerHealth('monapay', `${config.monapayApi}/health`, fetchImpl),
294
+ ]);
295
+ return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ checked_at: new Date().toISOString(), members }, null, 2) }] };
296
+ });
297
+ server.registerPrompt('dung-app-ban-hang-monacloud', {
298
+ title: 'Dựng app bán hàng có thu tiền trên MONA Cloud',
299
+ description: 'Chuỗi zero-dashboard: VPS → DB → VA/QR → webhook → deploy.',
300
+ argsSchema: {
301
+ app_name: z.string().optional(),
302
+ framework: z.string().optional(),
303
+ },
304
+ }, ({ app_name, framework }) => ({ messages: [{
305
+ role: 'user',
306
+ content: {
307
+ type: 'text',
308
+ text: `Dựng app bán hàng ${app_name || 'của tôi'} bằng ${framework || 'stack phù hợp'} trên MONA Cloud theo đúng thứ tự:\n1. Gọi cloud_whoami, cloud_balance, vibecloud_packages và vibecloud_prices. Nếu thiếu tiền, gọi cloud_topup rồi dừng để người dùng quét VietQR.\n2. Gọi vibecloud_create_vps và vibecloud_create_database; poll từng job bằng vibecloud_job_status tới succeeded.\n3. Gọi monapay_link nếu MONA Pay còn ở lớp chuyển tiếp. Gọi monapay_whoami; nếu chưa có VA, nối ngân hàng bằng chuỗi monapay_link_bank_start → HỎI OTP → verify → notification_register → HỎI OTP lần 2 → verify. Không tự đoán OTP.\n4. Viết endpoint webhook có HMAC và idempotency theo transaction_code; đăng ký bằng monapay_create_webhook, bắn monapay_test_webhook và đọc monapay_webhook_logs.\n5. Tích hợp monapay_create_checkout hoặc monapay_create_qr vào app, chỉ giao hàng sau CHECKOUT_PAID.\n6. Deploy code lên VPS vừa tạo, kiểm tra health và báo URL/credential cần lưu. Không yêu cầu người dùng mở dashboard ngoài bước nạp tiền/OTP bắt buộc.`,
309
+ },
310
+ }] }));
311
+ return server;
312
+ }
@@ -0,0 +1,19 @@
1
+ import type { Config } from './config.js';
2
+ type CatalogEntry = {
3
+ slug: string;
4
+ name: string;
5
+ description: string;
6
+ products: string[];
7
+ };
8
+ export declare class TemplateCatalog {
9
+ readonly config: Config;
10
+ readonly fetchImpl: typeof fetch;
11
+ constructor(config: Config, fetchImpl?: typeof fetch);
12
+ private assertSlug;
13
+ list(): Promise<{
14
+ source: string;
15
+ templates: CatalogEntry[];
16
+ }>;
17
+ get(slug: string): Promise<Record<string, unknown>>;
18
+ }
19
+ export {};
@@ -0,0 +1,117 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { CloudError } from './errors.js';
4
+ import { requestJson } from './http.js';
5
+ const BUILTIN = [
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
+ { 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', 'monahddt', 'MONA Mail'] },
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
+ { 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: ['VibeCloud', 'MONA Base', 'Ollama'] },
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'] },
12
+ ];
13
+ const allowedSlug = /^[a-z0-9][a-z0-9-]{0,79}$/;
14
+ const standardFiles = ['README.md', 'AGENTS.md', 'tools.json', 'deploy.md', 'CHECKLIST.md'];
15
+ async function directoryExists(path) {
16
+ try {
17
+ return (await readdir(path, { withFileTypes: true })).length >= 0;
18
+ }
19
+ catch (error) {
20
+ if (error.code === 'ENOENT')
21
+ return false;
22
+ throw error;
23
+ }
24
+ }
25
+ async function readOptional(path) {
26
+ try {
27
+ return await readFile(path, 'utf8');
28
+ }
29
+ catch (error) {
30
+ if (error.code === 'ENOENT')
31
+ return undefined;
32
+ throw error;
33
+ }
34
+ }
35
+ function summaryFromReadme(slug, readme) {
36
+ const lines = (readme || '').split(/\r?\n/).map((line) => line.trim());
37
+ const name = lines.find((line) => /^#\s+/.test(line))?.replace(/^#\s+/, '') || slug;
38
+ const description = lines.find((line) => line && !line.startsWith('#')) || 'MONA Agent template';
39
+ return { slug, name, description, products: [] };
40
+ }
41
+ export class TemplateCatalog {
42
+ config;
43
+ fetchImpl;
44
+ constructor(config, fetchImpl = fetch) {
45
+ this.config = config;
46
+ this.fetchImpl = fetchImpl;
47
+ }
48
+ assertSlug(slug) {
49
+ if (!allowedSlug.test(slug)) {
50
+ throw new CloudError('invalid_template', 'Tên template không hợp lệ.', 'Gọi agent_templates_list rồi dùng đúng slug trong catalog.');
51
+ }
52
+ }
53
+ async list() {
54
+ if (await directoryExists(this.config.templatesDir)) {
55
+ const entries = await readdir(this.config.templatesDir, { withFileTypes: true });
56
+ const templates = [];
57
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
58
+ if (!entry.isDirectory() || !allowedSlug.test(entry.name))
59
+ continue;
60
+ templates.push(summaryFromReadme(entry.name, await readOptional(join(this.config.templatesDir, entry.name, 'README.md'))));
61
+ }
62
+ return { source: this.config.templatesDir, templates };
63
+ }
64
+ if (this.config.templatesUrl) {
65
+ const remote = await requestJson(`${this.config.templatesUrl}/catalog.json`, { fetchImpl: this.fetchImpl });
66
+ return { source: this.config.templatesUrl, templates: Array.isArray(remote.templates) ? remote.templates : [] };
67
+ }
68
+ return { source: 'built-in-wave-1-catalog', templates: BUILTIN };
69
+ }
70
+ async get(slug) {
71
+ this.assertSlug(slug);
72
+ if (await directoryExists(this.config.templatesDir)) {
73
+ const directory = join(this.config.templatesDir, slug);
74
+ let entries;
75
+ try {
76
+ entries = await readdir(directory, { withFileTypes: true });
77
+ }
78
+ catch (error) {
79
+ if (error.code === 'ENOENT') {
80
+ throw new CloudError('template_not_found', `Không có template ${slug}.`, 'Gọi agent_templates_list để xem slug hiện có.');
81
+ }
82
+ throw error;
83
+ }
84
+ const files = {};
85
+ for (const filename of standardFiles) {
86
+ const content = await readOptional(join(directory, filename));
87
+ if (content !== undefined)
88
+ files[filename] = content;
89
+ }
90
+ const skillsEntry = entries.find((entry) => entry.isDirectory() && entry.name === 'skills');
91
+ if (skillsEntry) {
92
+ const skillFiles = await readdir(join(directory, 'skills'), { withFileTypes: true });
93
+ for (const file of skillFiles) {
94
+ if (!file.isFile() || !/\.(md|json|txt)$/i.test(file.name))
95
+ continue;
96
+ const content = await readFile(join(directory, 'skills', file.name), 'utf8');
97
+ files[`skills/${file.name}`] = content;
98
+ }
99
+ }
100
+ return { slug, source: directory, files };
101
+ }
102
+ if (this.config.templatesUrl) {
103
+ const remote = await requestJson(`${this.config.templatesUrl}/templates/${encodeURIComponent(slug)}.json`, { fetchImpl: this.fetchImpl });
104
+ return { slug, source: this.config.templatesUrl, ...remote };
105
+ }
106
+ const template = BUILTIN.find((entry) => entry.slug === slug);
107
+ if (!template) {
108
+ throw new CloudError('template_not_found', `Không có template ${slug}.`, 'Gọi agent_templates_list để xem slug hiện có.');
109
+ }
110
+ return {
111
+ ...template,
112
+ source: 'built-in-wave-1-catalog',
113
+ status: 'catalog_only',
114
+ next_step: 'Catalog runtime chưa được phát hành; dùng vibecloud_agent_deploy để kiểm tra trạng thái triển khai.',
115
+ };
116
+ }
117
+ }
@@ -0,0 +1,159 @@
1
+ # MONA Cloud MCP — hướng dẫn cho AI agent
2
+
3
+ Tài liệu này dành cho Claude, Codex, Cursor và agent dùng Model Context Protocol. Mục tiêu là hoàn thành tích hợp end-to-end mà không yêu cầu người dùng mở dashboard, trừ ba việc hợp lệ: đăng ký MONA ID, nạp tiền và OTP/KYC bắt buộc.
4
+
5
+ ## Quy tắc vận hành
6
+
7
+ 1. Bắt đầu bằng `cloud_whoami`. Nếu nhận `login_required`, yêu cầu người dùng chạy `monacloud-mcp login`; không hỏi username hoặc password trong chat.
8
+ 2. Trước khi provision, đọc `cloud_balance`, `vibecloud_packages` và `vibecloud_prices`.
9
+ 3. Khi cần nạp, gọi `cloud_topup`; đưa nguyên `qr_data_url`, số tiền, nội dung và hạn thanh toán cho người dùng. Chỉ tiếp tục sau khi `cloud_balance` phản ánh tiền vào.
10
+ 4. Với OTP ngân hàng, dừng đúng sau `monapay_link_bank_start` và `monapay_notification_register`. Hỏi người dùng mã vừa nhận; không suy đoán, brute-force hoặc ghi OTP vào source/log.
11
+ 5. Mọi endpoint webhook phải xác minh HMAC, chống replay theo timestamp và idempotent theo `transaction_code`.
12
+ 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.
13
+ 7. Nếu tool trả `isError`, parse text content thành `{code,message,next_step,request_id?}` và làm theo `next_step`. Không retry vô hạn lệnh ghi.
14
+
15
+ ## Xác thực
16
+
17
+ MCP dùng OAuth 2.0 Device Authorization Grant với public client `monacloud-mcp`, scope:
18
+
19
+ ```text
20
+ openid profile email product billing-api offline_access
21
+ ```
22
+
23
+ Token store mặc định là `~/.config/monacloud/token.json`, mode `0600`. Access token tự refresh bằng offline refresh token. Trong CI, `MONACLOUD_TOKEN` có thể cung cấp PAT trực tiếp.
24
+
25
+ Không in hoặc copy token vào source, issue, log, output tool hay nội dung webhook. Sản phẩm đích xác minh JWT MONA ID và audience tương ứng.
26
+
27
+ ### Adapter chuyển tiếp
28
+
29
+ MONA Pay và VibeCloud có thể chưa nhận JWT MONA ID trực tiếp tại thời điểm wave 1:
30
+
31
+ - gọi `monapay_link` một lần để nhận/cache `client_id/client_secret` MONA Pay;
32
+ - gọi `vibecloud_link` để xác nhận direct MONA ID; tool chỉ nhận/cache `vc_live_*` khi upstream từ chối JWT và còn bật adapter cũ;
33
+ - linked credential nằm ở `~/.config/monacloud/links.json`, mode `0600`;
34
+ - không gọi link lại ở mỗi request;
35
+ - khi upstream nhận JWT trực tiếp, bỏ adapter mà không đổi tên tool nghiệp vụ.
36
+
37
+ Nếu tài khoản MONA Pay cũ cần OTP email để link, trả quyền điều khiển cho người dùng ở đúng bước OTP rồi tiếp tục `monapay_link`.
38
+
39
+ ## Luồng dựng app bán hàng
40
+
41
+ ### 1. Kiểm tra account, ví và cấu hình hạ tầng
42
+
43
+ ```text
44
+ cloud_whoami
45
+ cloud_balance
46
+ vibecloud_packages
47
+ vibecloud_prices
48
+ ```
49
+
50
+ Chọn `package_slug` nếu có thể. Chỉ dùng custom sizing khi cần; không gửi đồng thời `package_slug` và `cpu/ram_gb/disk_gb`.
51
+
52
+ Nếu ví thiếu:
53
+
54
+ ```text
55
+ cloud_topup({ amount: 200000, idempotency_key: "topup:<project>:<stable-id>" })
56
+ ```
57
+
58
+ Giữ cùng idempotency key khi retry cùng một yêu cầu nạp.
59
+
60
+ ### 2. Tạo compute và database
61
+
62
+ ```text
63
+ vibecloud_create_vps({ app_name: "shop-demo", package_slug: "standard-2" })
64
+ vibecloud_create_database({ app_name: "shop-demo", engine: "postgresql", package_slug: "standard-2" })
65
+ ```
66
+
67
+ Mỗi response là job. Dùng:
68
+
69
+ ```text
70
+ vibecloud_job_status({ job_id: "...", wait: true, interval_sec: 3, timeout_sec: 180 })
71
+ ```
72
+
73
+ Chỉ dùng `result`/credential khi trạng thái `succeeded`. Với `failed`, đọc `error`; sửa nguyên nhân rồi mới retry. Không đưa database password vào commit hoặc response công khai.
74
+
75
+ ### 3. Chuẩn bị MONA Pay
76
+
77
+ ```text
78
+ monapay_link
79
+ monapay_whoami
80
+ ```
81
+
82
+ Nếu chưa có tài khoản ngân hàng/VA:
83
+
84
+ ```text
85
+ monapay_link_bank_start
86
+ → DỪNG, hỏi OTP ACB
87
+ monapay_link_bank_verify_otp
88
+ monapay_notification_register
89
+ → DỪNG, hỏi OTP ACB lần hai
90
+ monapay_notification_verify_otp
91
+ ```
92
+
93
+ Đây là hai điểm human-in-the-loop bắt buộc. Không yêu cầu người dùng đưa username/password ACB hoặc MONA Pay.
94
+
95
+ ### 4. Viết và test webhook
96
+
97
+ 1. Lấy code mẫu bằng `monapay_generate_webhook_snippet`.
98
+ 2. Tạo HTTPS endpoint trong app.
99
+ 3. Xác minh `X-Mona-Signature = sha256=HMAC-SHA256(secret, "<timestamp>.<raw_body>")`.
100
+ 4. Từ chối timestamp lệch quá 300 giây.
101
+ 5. Dùng `transaction_code` làm unique idempotency key.
102
+ 6. Đăng ký bằng `monapay_create_webhook` với `HMAC_SHA256` và secret ngẫu nhiên ít nhất 32 ký tự.
103
+ 7. Gọi `monapay_test_webhook`; đọc `monapay_webhook_logs` và `monapay_webhook_stats`.
104
+
105
+ ### 5. Thu tiền
106
+
107
+ Ưu tiên hosted checkout khi app cần link thanh toán:
108
+
109
+ ```text
110
+ monapay_create_checkout({
111
+ amount: 250000,
112
+ order_code: "DH_10234",
113
+ return_url: "https://shop.example/checkout/return",
114
+ idempotency_key: "checkout:DH_10234"
115
+ })
116
+ ```
117
+
118
+ Dùng `monapay_create_qr` khi đã có đủ thông tin ACB/VA và cần render QR trực tiếp. Sandbox dùng `monapay_sandbox_transaction`; không test bằng tiền thật.
119
+
120
+ ### 6. Deploy và xác minh
121
+
122
+ Deploy source lên VPS từ credential của job, đưa database connection string qua secret environment, chạy migration, bật HTTPS và gọi health endpoint. Sau đó:
123
+
124
+ - tạo checkout sandbox;
125
+ - tạo transaction sandbox;
126
+ - xác nhận webhook chỉ xử lý một lần khi event bị gửi lại;
127
+ - xác nhận đơn chuyển `paid` sau webhook;
128
+ - đọc `cloud_services` để báo lại toàn bộ service/VA/webhook;
129
+ - không echo credential nhạy cảm trong báo cáo.
130
+
131
+ ## Spend guard
132
+
133
+ `vibecloud_create_vps`, `vibecloud_create_database`, `vibecloud_start` và `vibecloud_rebuild` đọc `GET /v1/balance` trước khi gọi sản phẩm. Upstream vẫn là nguồn quyết định cuối và có thể trả:
134
+
135
+ ```json
136
+ {
137
+ "code": "insufficient_funds",
138
+ "message": "Ví thiếu tiền...",
139
+ "next_step": "Nạp ví tại https://monacloud.vn/console rồi gọi lại tool."
140
+ }
141
+ ```
142
+
143
+ hoặc `budget_exceeded`. Khi gặp hai code này, không retry. Gọi `cloud_topup`, `cloud_budget_get` hoặc yêu cầu user tăng budget. `vibecloud_stop` luôn được phép vì chặn stop có thể làm tăng rủi ro chi phí.
144
+
145
+ ## Catalog MONA Agent
146
+
147
+ `agent_templates_list` trả nguồn catalog và danh sách slug. `agent_templates_get` trả toàn bộ file text cần để agent build local. Không tự nối path từ input ngoài; chỉ dùng slug đã trả.
148
+
149
+ `agent_deploy` và `vibecloud_agent_deploy` hiện là stub có chủ đích, trả `agent_runtime_pending`; không giả vờ đã provision. Khi nhận code này, có thể đọc template và triển khai thủ công hoặc báo rõ runtime hosted chưa được bật.
150
+
151
+ ## Resource hữu ích
152
+
153
+ - Đọc `monacloud://llms` khi cần bối cảnh toàn hệ sinh thái.
154
+ - Đọc `monacloud://status` trước khi kết luận lỗi là do credential hoặc request.
155
+ - Dùng prompt `dung-app-ban-hang-monacloud` để lấy chuỗi tác vụ chuẩn.
156
+
157
+ ## Tiêu chí hoàn tất
158
+
159
+ Một lượt được coi là xong khi app và database chạy, webhook HMAC test pass, sandbox checkout chuyển `paid`, credential nằm trong secret store, và agent báo lại URL cùng trạng thái dịch vụ. Không coi việc “đã tạo job” là hoàn tất; phải poll tới terminal state và kiểm health thực tế.
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "monacloud-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Unified MCP server for MONA Cloud, MONA Pay, VibeCloud and MONA Agent.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "monacloud-mcp": "dist/index.js"
9
+ },
10
+ "main": "dist/server.js",
11
+ "types": "dist/server.d.ts",
12
+ "files": [
13
+ "dist",
14
+ "docs",
15
+ "README.md",
16
+ "STATUS.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "test": "npm run build && node --test test/*.test.mjs",
22
+ "start": "node dist/index.js"
23
+ },
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.30.0",
29
+ "monapay-mcp": "^0.3.0",
30
+ "zod": "^4.4.3"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^26.4.0",
34
+ "typescript": "^7.0.2"
35
+ }
36
+ }