monacloud-mcp 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -99,7 +99,21 @@ Các tên cũ như `monapay_create_qr`, `monapay_link_bank_start`, `monapay_crea
99
99
  | `cloud_prices` / `cloud_packages` | Đơn giá và gói cấu hình |
100
100
  | `cloud_agent_deploy` | Stub có cấu trúc cho runtime MONA Agent wave sau |
101
101
 
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 đọc `GET /v1/balance` trước khi gọi compute MONA Cloud.
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
+
104
+ ## Thử 0đ bằng sandbox
105
+
106
+ 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.
107
+
108
+ Ở chế độ này MCP bỏ qua `GET /v1/balance`, tự gửi `X-Vibecloud-Sandbox: 1` đến compute API và bảo đảm response có `sandbox: true`. Ví dụ:
109
+
110
+ ```text
111
+ cloud_vps_create({ app_name: "shop-demo", package_slug: "standard-2", sandbox: true })
112
+ cloud_job_status({ job_id: "...", sandbox: true })
113
+ cloud_services_list({ sandbox: true })
114
+ ```
115
+
116
+ `cloud_job_status` tự tìm job sandbox theo ID mà không cần header. `cloud_services_list({ sandbox: true })` dùng khả năng `include_sandbox` của API để gộp service thật và sandbox. Sandbox không tạo hạ tầng thật, không trừ tiền và có thể bị dọn theo TTL của compute API. `cloud_agent_deploy` vẫn là stub cho đến khi runtime MONA Agent được phát hành, nhưng response sandbox được đánh dấu nhất quán.
103
117
 
104
118
  ### MONA Agent
105
119
 
@@ -115,7 +129,7 @@ Các tên cũ như `monapay_create_qr`, `monapay_link_bank_start`, `monapay_crea
115
129
 
116
130
  ## Spend guard và lỗi cho AI
117
131
 
118
- Trước lệnh compute MONA Cloud có thể phát sinh tiền, MCP đọc ví chung. HTTP `402 insufficient_funds` và `402 budget_exceeded` được chuẩn hoá thành text JSON:
132
+ Trước lệnh compute MONA Cloud thật có thể phát sinh tiền, MCP đọc ví chung. Sandbox bỏ qua bước này vì chi phí 0đ. HTTP `402 insufficient_funds` và `402 budget_exceeded` được chuẩn hoá thành text JSON:
119
133
 
120
134
  ```json
121
135
  {
@@ -156,6 +170,7 @@ Chi tiết dành riêng cho agent: [`docs/ai-agent.md`](docs/ai-agent.md).
156
170
  | `MONACLOUD_API` | `https://api.monacloud.vn` | Compute API của MONA Cloud |
157
171
  | `MONACLOUD_CONSOLE_URL` | `https://monacloud.vn/console` | URL trả cho bước human |
158
172
  | `MONACLOUD_TOKEN` | — | PAT/access token ưu tiên token store |
173
+ | `MONACLOUD_SANDBOX` | — | Đặt `1` để mọi lệnh compute hỗ trợ sandbox chạy thử 0đ, không cần ví |
159
174
  | `MONACLOUD_CONFIG_DIR` | `~/.config/monacloud` | Token và link cache |
160
175
  | `MONACLOUD_TEMPLATES_DIR` | `~/monacloud/templates` | Catalog agent local |
161
176
  | `MONACLOUD_TEMPLATES_URL` | — | Catalog JSON từ xa, chỉ dùng khi local không có |
package/STATUS.md CHANGED
@@ -1,17 +1,17 @@
1
- # STATUS — monacloud-mcp 0.2.0
1
+ # STATUS — monacloud-mcp 0.2.1
2
2
 
3
3
  Updated: 2026-09-04 (Asia/Ho_Chi_Minh)
4
4
 
5
5
  ## Hoàn thành
6
6
 
7
- - [x] Package Node 20 + TypeScript, ESM, stdio MCP; bin `monacloud-mcp`, version `0.2.0`.
7
+ - [x] Package Node 20 + TypeScript, ESM, stdio MCP; bin `monacloud-mcp`, version `0.2.1`.
8
8
  - [x] OAuth device flow client `monacloud-mcp`, scope `offline_access`, token store private `0600`, refresh tự động; CLI `login`, `logout`, `whoami`.
9
9
  - [x] 10 tool `cloud_*`: identity, balance, ledger, topup VietQR, usage, services aggregation, budget, token limit và console URL.
10
10
  - [x] Import package `monapay-mcp` và re-export toàn bộ tool, không copy code. Dependency offline tại lần verify cuối là 0.5.5/47 tool (superset 24 tool yêu cầu); cộng `monapay_link` chuyển tiếp.
11
11
  - [x] 11 tool compute tên mới `cloud_*`: link, VPS, database, job poll, list, start/stop/rebuild, prices, packages và agent-deploy stub.
12
12
  - [x] 11 alias kỹ thuật `vibecloud_*` vẫn gọi được cho client cũ, không xuất hiện trong README/hướng dẫn agent.
13
13
  - [x] 3 tool `agent_*`: list/get catalog local/remote/built-in và deploy qua cùng stub MONA Cloud.
14
- - [x] Spend guard đọc billing balance trước create/start/rebuild; chuẩn hoá `402 insufficient_funds|budget_exceeded` thành `{code,message,next_step,request_id?}`. Stop luôn được phép.
14
+ - [x] Spend guard đọc billing balance trước create/start/rebuild thật; `sandbox: true` hoặc `MONACLOUD_SANDBOX=1` bỏ qua Billing, gửi header compute và đánh dấu response. Stop luôn được phép.
15
15
  - [x] 2 resource đúng brief: `monacloud://llms`, `monacloud://status`.
16
16
  - [x] 1 prompt `dung-app-ban-hang-monacloud`: VPS → DB → VA/QR → webhook → deploy.
17
17
  - [x] Không log token; token và linked credential được lưu bằng atomic write, directory `0700`, file `0600`.
@@ -28,7 +28,7 @@ $ node_modules/.bin/tsc -p tsconfig.json
28
28
  exit 0, không có diagnostic
29
29
 
30
30
  $ node --test
31
- tests 9, pass 9, fail 0
31
+ tests 13, pass 13, fail 0
32
32
  ```
33
33
 
34
34
  Không chạy `npm install`; toàn bộ build/test dùng `node_modules` có sẵn và không gọi Internet.
@@ -47,7 +47,7 @@ CODEX DONE
47
47
 
48
48
  - Tên người đọc và 11 tool chính đã chuyển sang MONA Cloud / `cloud_*`; VibeCloud chỉ còn trong alias và định danh kỹ thuật tương thích.
49
49
  - Resource `monacloud://llms`, prompt dựng app, README và `docs/ai-agent.md` chỉ hướng dẫn tên mới.
50
- - Package, MCP server và binary version đồng bộ `0.2.0`; build và test được ghi ở gate mới bên trên.
50
+ - Package, MCP server và binary version đồng bộ `0.2.1`; build và test được ghi ở gate mới bên trên.
51
51
 
52
52
  ## QC Claude (03/09 tối) — ZERO-DASHBOARD TEST LOCAL: PASS
53
53
  - Build tsc sạch · `node --test` 7/7 · stdio thật: **72 tool** (cloud 10 · monapay 48 · vibecloud 11 · agent 3), 2 resource, 1 prompt.
package/dist/clients.d.ts CHANGED
@@ -20,11 +20,18 @@ export declare class CloudClients {
20
20
  budgetGet(): Promise<unknown>;
21
21
  tokenLimit(spendLimitVnd: number, period: string, tokenId?: string): Promise<unknown>;
22
22
  spendGuard(): Promise<JsonObject>;
23
+ sandboxEnabled(requested?: boolean): boolean;
23
24
  private vibecloudToken;
24
25
  vibecloud<T = unknown>(path: string, options?: {
25
26
  method?: string;
26
27
  body?: unknown;
28
+ query?: Record<string, string | number | undefined>;
29
+ headers?: Record<string, string>;
27
30
  }): Promise<T>;
31
+ guardedVibecloud(path: string, options?: {
32
+ method?: string;
33
+ body?: unknown;
34
+ }, requestedSandbox?: boolean): Promise<unknown>;
28
35
  vibecloudLink(): Promise<JsonObject>;
29
36
  monapayLink(): Promise<JsonObject>;
30
37
  prices(): Promise<unknown>;
package/dist/clients.js CHANGED
@@ -69,6 +69,9 @@ export class CloudClients {
69
69
  }
70
70
  return current;
71
71
  }
72
+ sandboxEnabled(requested = false) {
73
+ return requested || this.auth.env.MONACLOUD_SANDBOX === '1';
74
+ }
72
75
  async vibecloudToken() {
73
76
  if (this.auth.env.VIBECLOUD_API_TOKEN)
74
77
  return this.auth.env.VIBECLOUD_API_TOKEN;
@@ -86,6 +89,16 @@ export class CloudClients {
86
89
  fetchImpl: this.fetchImpl,
87
90
  });
88
91
  }
92
+ async guardedVibecloud(path, options = {}, requestedSandbox = false) {
93
+ const sandbox = this.sandboxEnabled(requestedSandbox);
94
+ if (!sandbox)
95
+ await this.spendGuard();
96
+ const result = await this.vibecloud(path, {
97
+ ...options,
98
+ ...(sandbox ? { headers: { 'X-Vibecloud-Sandbox': '1' } } : {}),
99
+ });
100
+ return sandbox ? { ...asObject(result), sandbox: true } : result;
101
+ }
89
102
  async vibecloudLink() {
90
103
  if (this.auth.env.VIBECLOUD_API_TOKEN) {
91
104
  return { linked: true, product: 'vibecloud', mode: 'legacy_env', next_step: 'Dùng các tool cloud_*.' };
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ async function main() {
22
22
  return;
23
23
  }
24
24
  if (command === '--version' || command === '-v') {
25
- console.log('0.2.0');
25
+ console.log('0.2.1');
26
26
  return;
27
27
  }
28
28
  if (command === '--help' || command === '-h' || command === 'help') {
package/dist/server.js CHANGED
@@ -9,7 +9,9 @@ import { requestJson, unwrapData } from './http.js';
9
9
  import { createMonaPayClient, importedMonaPayTools, normalizeImportedResult } from './monapay.js';
10
10
  import { TemplateCatalog } from './templates.js';
11
11
  const ENTITY = 'MONA Cloud là hệ công cụ cho vibecoder Việt Nam: một MONA Pass, một ví VND và một MCP để chạy app/VPS, thu tiền MONA Pay và dùng các sản phẩm MONA.';
12
- const TERMINAL_JOBS = new Set(['succeeded', 'failed', 'cancelled']);
12
+ const TERMINAL_JOBS = new Set(['done', 'succeeded', 'failed', 'cancelled']);
13
+ const SANDBOX_DESCRIPTION = 'sandbox=true: thử 0đ, không cần ví';
14
+ const sandboxSchema = z.boolean().optional().describe(SANDBOX_DESCRIPTION);
13
15
  const asObject = (value) => (value && typeof value === 'object' ? value : {});
14
16
  function collection(value) {
15
17
  const data = unwrapData(value);
@@ -29,6 +31,7 @@ const sizingSchema = {
29
31
  cpu: z.number().int().min(1).max(16).optional(),
30
32
  ram_gb: z.number().int().min(1).max(64).optional(),
31
33
  disk_gb: z.number().int().min(10).max(1000).optional(),
34
+ sandbox: sandboxSchema,
32
35
  };
33
36
  const provisionSchema = z.object(sizingSchema).superRefine((value, context) => {
34
37
  const dimensions = [value.cpu, value.ram_gb, value.disk_gb];
@@ -41,28 +44,34 @@ const provisionSchema = z.object(sizingSchema).superRefine((value, context) => {
41
44
  context.addIssue({ code: 'custom', message: 'Cần package_slug hoặc đủ cpu, ram_gb và disk_gb.' });
42
45
  }
43
46
  });
44
- function agentRuntimeStub(template) {
47
+ function agentRuntimeStub(template, sandbox = false) {
45
48
  return {
46
49
  status: 'not_available',
47
50
  code: 'agent_runtime_pending',
48
51
  template,
49
52
  message: 'Runtime MONA Agent trên MONA Cloud chưa được phát hành; tool này là slot wave kế tiếp theo brief.',
50
53
  next_step: 'Dùng agent_templates_get để lấy nội dung template và triển khai thủ công, hoặc thử lại khi MONA Cloud bật runtime agent.',
54
+ ...(sandbox ? { sandbox: true } : {}),
51
55
  };
52
56
  }
53
- async function pollJob(clients, jobId, wait, intervalSeconds, timeoutSeconds) {
57
+ async function pollJob(clients, jobId, wait, intervalSeconds, timeoutSeconds, sandbox) {
54
58
  let result = await clients.vibecloud(`/api/jobs/${encodeURIComponent(jobId)}`);
55
59
  if (!wait)
56
- return result;
60
+ return sandbox ? { ...asObject(result), sandbox: true } : result;
57
61
  const deadline = Date.now() + timeoutSeconds * 1000;
58
62
  while (!TERMINAL_JOBS.has(String(asObject(result).status || '')) && Date.now() < deadline) {
59
63
  await delay(intervalSeconds * 1000);
60
64
  result = await clients.vibecloud(`/api/jobs/${encodeURIComponent(jobId)}`);
61
65
  }
62
66
  if (!TERMINAL_JOBS.has(String(asObject(result).status || ''))) {
63
- return { ...asObject(result), polling: 'timeout', next_step: `Gọi lại cloud_job_status với job_id=${jobId}.` };
67
+ return {
68
+ ...asObject(result),
69
+ ...(sandbox ? { sandbox: true } : {}),
70
+ polling: 'timeout',
71
+ next_step: `Gọi lại cloud_job_status với job_id=${jobId}.`,
72
+ };
64
73
  }
65
- return result;
74
+ return sandbox ? { ...asObject(result), sandbox: true } : result;
66
75
  }
67
76
  async function providerHealth(name, url, fetchImpl) {
68
77
  const started = Date.now();
@@ -81,7 +90,7 @@ export function createServer(dependencies = {}) {
81
90
  const auth = new AuthManager(config, env, fetchImpl);
82
91
  const clients = new CloudClients(config, auth, fetchImpl);
83
92
  const catalog = new TemplateCatalog(config, fetchImpl);
84
- const server = new McpServer({ name: 'monacloud-mcp', version: '0.2.0' }, { instructions: `${ENTITY}\nDùng cloud_* cho tài khoản, ví và hạ tầng; monapay_* cho thu tiền; agent_* cho catalog. Không bao giờ yêu cầu mật khẩu sản phẩm. Chỉ dừng hỏi người dùng khi cần nạp tiền, OTP ngân hàng hoặc KYC bắt buộc.` });
93
+ const server = new McpServer({ name: 'monacloud-mcp', version: '0.2.1' }, { instructions: `${ENTITY}\nDùng cloud_* cho tài khoản, ví và hạ tầng; monapay_* cho thu tiền; agent_* cho catalog. Không bao giờ yêu cầu mật khẩu sản phẩm. Chỉ dừng hỏi người dùng khi cần nạp tiền, OTP ngân hàng hoặc KYC bắt buộc.` });
85
94
  server.registerTool('cloud_whoami', {
86
95
  title: 'Tài khoản MONA Cloud',
87
96
  description: 'Xác minh MONA Pass hiện tại. / Return the current MONA Pass profile.',
@@ -187,82 +196,91 @@ export function createServer(dependencies = {}) {
187
196
  }, () => runTool(() => clients.vibecloudLink()));
188
197
  server.registerTool('cloud_vps_create', {
189
198
  title: 'Tạo VPS MONA Cloud',
190
- description: 'Kiểm tra ví chung rồi tạo LXC VPS; trả job_id để poll.',
199
+ description: `Kiểm tra ví chung rồi tạo LXC VPS; trả job_id để poll. ${SANDBOX_DESCRIPTION}.`,
191
200
  inputSchema: provisionSchema,
192
201
  }, (body) => runTool(async () => {
193
- await clients.spendGuard();
194
- return clients.vibecloud('/api/lxc', { method: 'POST', body });
202
+ const { sandbox, ...payload } = body;
203
+ return clients.guardedVibecloud('/api/lxc', { method: 'POST', body: payload }, sandbox);
195
204
  }));
196
205
  server.registerTool('vibecloud_create_vps', {
197
206
  title: 'Alias cũ của cloud_vps_create',
198
- description: 'Alias tương thích; dùng cloud_vps_create cho tích hợp mới.',
207
+ description: `Alias tương thích; dùng cloud_vps_create cho tích hợp mới. ${SANDBOX_DESCRIPTION}.`,
199
208
  inputSchema: provisionSchema,
200
209
  }, (body) => runTool(async () => {
201
- await clients.spendGuard();
202
- return clients.vibecloud('/api/lxc', { method: 'POST', body });
210
+ const { sandbox, ...payload } = body;
211
+ return clients.guardedVibecloud('/api/lxc', { method: 'POST', body: payload }, sandbox);
203
212
  }));
204
213
  const databaseSchema = provisionSchema.extend({ engine: z.enum(['mongodb', 'postgresql', 'mysql']).default('mongodb') });
205
214
  server.registerTool('cloud_db_create', {
206
215
  title: 'Tạo database MONA Cloud',
207
- description: 'Kiểm tra ví chung rồi tạo MongoDB/PostgreSQL/MySQL; trả job_id để poll.',
216
+ description: `Kiểm tra ví chung rồi tạo MongoDB/PostgreSQL/MySQL; trả job_id để poll. ${SANDBOX_DESCRIPTION}.`,
208
217
  inputSchema: databaseSchema,
209
218
  }, (body) => runTool(async () => {
210
- await clients.spendGuard();
211
- return clients.vibecloud('/api/databases', { method: 'POST', body });
219
+ const { sandbox, ...payload } = body;
220
+ return clients.guardedVibecloud('/api/databases', { method: 'POST', body: payload }, sandbox);
212
221
  }));
213
222
  server.registerTool('vibecloud_create_database', {
214
223
  title: 'Alias cũ của cloud_db_create',
215
- description: 'Alias tương thích; dùng cloud_db_create cho tích hợp mới.',
224
+ description: `Alias tương thích; dùng cloud_db_create cho tích hợp mới. ${SANDBOX_DESCRIPTION}.`,
216
225
  inputSchema: databaseSchema,
217
226
  }, (body) => runTool(async () => {
218
- await clients.spendGuard();
219
- return clients.vibecloud('/api/databases', { method: 'POST', body });
227
+ const { sandbox, ...payload } = body;
228
+ return clients.guardedVibecloud('/api/databases', { method: 'POST', body: payload }, sandbox);
220
229
  }));
221
230
  const jobStatusSchema = {
222
231
  job_id: z.string().min(1),
223
232
  wait: z.boolean().default(true),
224
233
  interval_sec: z.number().int().min(1).max(30).default(3),
225
234
  timeout_sec: z.number().int().min(1).max(600).default(180),
235
+ sandbox: sandboxSchema,
226
236
  };
227
237
  server.registerTool('cloud_job_status', {
228
238
  title: 'Theo dõi job MONA Cloud',
229
- description: 'Đọc một job; mặc định poll tới succeeded/failed/cancelled hoặc timeout.',
239
+ description: 'Đọc job thật hoặc sandbox theo ID; API tự nhận diện sandbox nên không cần header.',
230
240
  inputSchema: jobStatusSchema,
231
- }, ({ job_id, wait, interval_sec, timeout_sec }) => runTool(() => pollJob(clients, job_id, wait, interval_sec, timeout_sec)));
241
+ }, ({ job_id, wait, interval_sec, timeout_sec, sandbox }) => runTool(() => pollJob(clients, job_id, wait, interval_sec, timeout_sec, clients.sandboxEnabled(sandbox))));
232
242
  server.registerTool('vibecloud_job_status', {
233
243
  title: 'Alias cũ của cloud_job_status',
234
- description: 'Alias tương thích; dùng cloud_job_status cho tích hợp mới.',
244
+ description: 'Alias tương thích; dùng cloud_job_status cho tích hợp mới. Job sandbox được tự nhận diện, không cần header.',
235
245
  inputSchema: jobStatusSchema,
236
- }, ({ job_id, wait, interval_sec, timeout_sec }) => runTool(() => pollJob(clients, job_id, wait, interval_sec, timeout_sec)));
246
+ }, ({ job_id, wait, interval_sec, timeout_sec, sandbox }) => runTool(() => pollJob(clients, job_id, wait, interval_sec, timeout_sec, clients.sandboxEnabled(sandbox))));
237
247
  server.registerTool('cloud_services_list', {
238
248
  title: 'Danh sách service MONA Cloud',
239
- description: 'Liệt kê VPS database của MONA Pass hiện tại.',
240
- }, () => runTool(() => clients.vibecloud('/api/services')));
249
+ description: 'Liệt kê VPS/database; sandbox=true gộp cả service thử bằng API, không cần header.',
250
+ inputSchema: { sandbox: sandboxSchema },
251
+ }, ({ sandbox }) => runTool(() => clients.vibecloud('/api/services', {
252
+ query: clients.sandboxEnabled(sandbox) ? { include_sandbox: 1 } : undefined,
253
+ })));
241
254
  server.registerTool('vibecloud_list_services', {
242
255
  title: 'Alias cũ của cloud_services_list',
243
- description: 'Alias tương thích; dùng cloud_services_list cho tích hợp mới.',
244
- }, () => runTool(() => clients.vibecloud('/api/services')));
256
+ description: 'Alias tương thích; dùng cloud_services_list cho tích hợp mới. sandbox=true gộp service thử 0đ, không cần header.',
257
+ inputSchema: { sandbox: sandboxSchema },
258
+ }, ({ sandbox }) => runTool(() => clients.vibecloud('/api/services', {
259
+ query: clients.sandboxEnabled(sandbox) ? { include_sandbox: 1 } : undefined,
260
+ })));
245
261
  for (const action of ['start', 'stop', 'rebuild']) {
246
262
  const name = `cloud_service_${action}`;
247
263
  server.registerTool(name, {
248
264
  title: `${action} service MONA Cloud`,
249
265
  description: action === 'stop'
250
266
  ? 'Dừng VPS/database MONA Cloud; luôn cho phép dừng để người dùng hạn chế chi phí.'
251
- : `${action} VPS/database MONA Cloud. Lệnh có thể phát sinh chi phí và kiểm tra ví trước.`,
252
- inputSchema: { service_id: z.string().min(1) },
253
- }, ({ service_id }) => runTool(async () => {
267
+ : `${action} VPS/database MONA Cloud. Lệnh có thể phát sinh chi phí và kiểm tra ví trước. ${SANDBOX_DESCRIPTION}.`,
268
+ inputSchema: { service_id: z.string().min(1), sandbox: sandboxSchema },
269
+ }, ({ service_id, sandbox }) => runTool(async () => {
270
+ const path = `/api/services/${encodeURIComponent(service_id)}/${action}`;
254
271
  if (action !== 'stop')
255
- await clients.spendGuard();
256
- return clients.vibecloud(`/api/services/${encodeURIComponent(service_id)}/${action}`, { method: 'POST' });
272
+ return clients.guardedVibecloud(path, { method: 'POST' }, sandbox);
273
+ return clients.vibecloud(path, { method: 'POST' });
257
274
  }));
258
275
  server.registerTool(`vibecloud_${action}`, {
259
276
  title: `Alias cũ của ${name}`,
260
- description: `Alias tương thích; dùng ${name} cho tích hợp mới.`,
261
- inputSchema: { service_id: z.string().min(1) },
262
- }, ({ service_id }) => runTool(async () => {
277
+ description: `Alias tương thích; dùng ${name} cho tích hợp mới.${action === 'stop' ? '' : ` ${SANDBOX_DESCRIPTION}.`}`,
278
+ inputSchema: { service_id: z.string().min(1), sandbox: sandboxSchema },
279
+ }, ({ service_id, sandbox }) => runTool(async () => {
280
+ const path = `/api/services/${encodeURIComponent(service_id)}/${action}`;
263
281
  if (action !== 'stop')
264
- await clients.spendGuard();
265
- return clients.vibecloud(`/api/services/${encodeURIComponent(service_id)}/${action}`, { method: 'POST' });
282
+ return clients.guardedVibecloud(path, { method: 'POST' }, sandbox);
283
+ return clients.vibecloud(path, { method: 'POST' });
266
284
  }));
267
285
  }
268
286
  server.registerTool('cloud_prices', {
@@ -283,14 +301,14 @@ export function createServer(dependencies = {}) {
283
301
  }, () => runTool(() => clients.packages()));
284
302
  server.registerTool('cloud_agent_deploy', {
285
303
  title: 'Deploy MONA Agent trên MONA Cloud',
286
- description: 'Slot runtime agent wave kế tiếp; hiện trả trạng thái stub rõ ràng.',
287
- inputSchema: { template: z.string().regex(/^[a-z0-9][a-z0-9-]{0,79}$/) },
288
- }, ({ template }) => runTool(() => agentRuntimeStub(template)));
304
+ description: `Slot runtime agent wave kế tiếp; hiện trả trạng thái stub rõ ràng. ${SANDBOX_DESCRIPTION}.`,
305
+ inputSchema: { template: z.string().regex(/^[a-z0-9][a-z0-9-]{0,79}$/), sandbox: sandboxSchema },
306
+ }, ({ template, sandbox }) => runTool(() => agentRuntimeStub(template, clients.sandboxEnabled(sandbox))));
289
307
  server.registerTool('vibecloud_agent_deploy', {
290
308
  title: 'Alias cũ của cloud_agent_deploy',
291
- description: 'Alias tương thích; dùng cloud_agent_deploy cho tích hợp mới.',
292
- inputSchema: { template: z.string().regex(/^[a-z0-9][a-z0-9-]{0,79}$/) },
293
- }, ({ template }) => runTool(() => agentRuntimeStub(template)));
309
+ description: `Alias tương thích; dùng cloud_agent_deploy cho tích hợp mới. ${SANDBOX_DESCRIPTION}.`,
310
+ inputSchema: { template: z.string().regex(/^[a-z0-9][a-z0-9-]{0,79}$/), sandbox: sandboxSchema },
311
+ }, ({ template, sandbox }) => runTool(() => agentRuntimeStub(template, clients.sandboxEnabled(sandbox))));
294
312
  server.registerTool('agent_templates_list', {
295
313
  title: 'Catalog MONA Agent',
296
314
  description: 'Đọc template từ thư mục local, URL catalog hoặc catalog wave 1 tích hợp.',
package/docs/ai-agent.md CHANGED
@@ -5,7 +5,7 @@ Tài liệu này dành cho Claude, Codex, Cursor và agent dùng Model Context P
5
5
  ## Quy tắc vận hành
6
6
 
7
7
  1. Bắt đầu bằng `cloud_whoami`. Nếu nhận `login_required`, yêu cầu người dùng chạy `monacloud-mcp login`; không hỏi username hoặc password trong chat.
8
- 2. Trước khi provision, đọc `cloud_balance`, `cloud_packages` và `cloud_prices`.
8
+ 2. Trước khi provision thật, đọc `cloud_balance`, `cloud_packages` và `cloud_prices`. Với `sandbox: true`, không cần đọc hoặc nạp ví.
9
9
  3. Khi cần nạp, gọi `cloud_topup`; đưa nguyên `qr_data_url`, số tiền, nội dung và hạn thanh toán cho người dùng. Chỉ tiếp tục sau khi `cloud_balance` phản ánh tiền vào.
10
10
  4. Với OTP ngân hàng, dừng đúng sau `monapay_link_bank_start` và `monapay_notification_register`. Hỏi người dùng mã vừa nhận; không suy đoán, brute-force hoặc ghi OTP vào source/log.
11
11
  5. Mọi endpoint webhook phải xác minh HMAC, chống replay theo timestamp và idempotent theo `transaction_code`.
@@ -130,7 +130,7 @@ Deploy source lên VPS từ credential của job, đưa database connection stri
130
130
 
131
131
  ## Spend guard
132
132
 
133
- `cloud_vps_create`, `cloud_db_create`, `cloud_service_start` và `cloud_service_rebuild` đọc `GET /v1/balance` trước khi gọi sản phẩm. Upstream vẫn là nguồn quyết định cuối và có thể trả:
133
+ `cloud_vps_create`, `cloud_db_create`, `cloud_service_start` và `cloud_service_rebuild` đọc `GET /v1/balance` trước khi gọi sản phẩm thật. Upstream vẫn là nguồn quyết định cuối và có thể trả:
134
134
 
135
135
  ```json
136
136
  {
@@ -142,6 +142,21 @@ Deploy source lên VPS từ credential của job, đưa database connection stri
142
142
 
143
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. `cloud_service_stop` luôn được phép vì chặn stop có thể làm tăng rủi ro chi phí.
144
144
 
145
+ ## Thử 0đ bằng sandbox
146
+
147
+ Thêm `sandbox: true` khi gọi `cloud_vps_create`, `cloud_db_create`, `cloud_service_start`, `cloud_service_rebuild` hoặc `cloud_agent_deploy`. Có thể đặt `MONACLOUD_SANDBOX=1` để bật cho toàn bộ MCP process. Alias `vibecloud_*` tương ứng hoạt động giống hệt.
148
+
149
+ ```text
150
+ cloud_vps_create({ app_name: "shop-demo", package_slug: "standard-2", sandbox: true })
151
+ cloud_db_create({ app_name: "shop-demo", engine: "postgresql", package_slug: "standard-2", sandbox: true })
152
+ cloud_job_status({ job_id: "...", sandbox: true })
153
+ cloud_services_list({ sandbox: true })
154
+ ```
155
+
156
+ Với lệnh tạo/start/rebuild sandbox, MCP không gọi Billing, tự gửi `X-Vibecloud-Sandbox: 1`, và response có `sandbox: true`. `cloud_job_status` tra job theo ID mà không cần header; trạng thái `done` là terminal hợp lệ của sandbox. `cloud_services_list({ sandbox: true })` dùng `include_sandbox=1`, cũng không cần header, để gộp service thật và sandbox.
157
+
158
+ Sandbox không gọi Proxmox, không cấp IP thật, không trừ tiền và có TTL do compute API quản lý. Chỉ chuyển sang lệnh thật khi người dùng yêu cầu rõ ràng. `cloud_agent_deploy` hiện vẫn trả `agent_runtime_pending`; sandbox chỉ đánh dấu response nhất quán, không giả vờ đã deploy.
159
+
145
160
  ## Catalog MONA Agent
146
161
 
147
162
  `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ả.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monacloud-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Unified MCP server for MONA Cloud, MONA Pay and MONA Agent.",
5
5
  "license": "MIT",
6
6
  "type": "module",