monacloud-mcp 0.10.10 → 0.10.11
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/base.js +4 -0
- package/dist/compute.js +4 -0
- package/dist/domains.js +22 -0
- package/dist/mail.js +31 -0
- package/dist/monapay.d.ts +12 -0
- package/dist/monapay.js +41 -0
- package/dist/remote-cli.d.ts +2 -0
- package/dist/remote-cli.js +30 -0
- package/dist/remote-verify.d.ts +17 -0
- package/dist/remote-verify.js +62 -0
- package/dist/remote.d.ts +21 -0
- package/dist/remote.js +338 -0
- package/dist/server.js +41 -3
- package/docs/remote.md +37 -0
- package/package.json +9 -3
package/dist/base.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { CloudError, runTool } from './errors.js';
|
|
3
3
|
import { pollJob, TERMINAL_JOBS } from './jobs.js';
|
|
4
4
|
import { unwrapData } from './http.js';
|
|
5
|
+
import { completeToolMetadata } from './monapay.js';
|
|
5
6
|
const object = (value) => value && typeof value === 'object' ? value : {};
|
|
6
7
|
const id = z.string().trim().min(1).max(255);
|
|
7
8
|
const sandbox = z.boolean().optional().describe('Thử 0đ, chỉ ước tính và không tạo hạ tầng thật / Sandbox estimate only');
|
|
@@ -45,7 +46,10 @@ async function createBase(clients, body) {
|
|
|
45
46
|
export function registerBaseTools(server, clients) {
|
|
46
47
|
const register = (name, description, schema, handler) => {
|
|
47
48
|
for (const toolName of [name, name.replace(/^cloud_/, 'vibecloud_')]) {
|
|
49
|
+
const metadata = completeToolMetadata(toolName, undefined, undefined);
|
|
48
50
|
server.registerTool(toolName, {
|
|
51
|
+
title: metadata.title,
|
|
52
|
+
annotations: metadata.annotations,
|
|
49
53
|
description: toolName === name ? `${description} ${baseDescription}` : `Alias tương thích của ${name}. / Compatibility alias. ${description} ${baseDescription}`,
|
|
50
54
|
inputSchema: schema,
|
|
51
55
|
}, (args) => runTool(() => handler(args)));
|
package/dist/compute.js
CHANGED
|
@@ -4,6 +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
|
+
import { completeToolMetadata } from './monapay.js';
|
|
7
8
|
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. Sau khi app có URL, CHỦ ĐỘNG hỏi tên miền riêng: human đã có domain → cloud_app_domain_add (CNAME); chưa có → cloud_domain_search gợi 3–5 đuôi (.vn trước nếu khách Việt) kèm giá VND, hỏi xác nhận chính tả + duyệt tiền → cloud_domain_buy (đã login) hoặc cloud_domain_reserve (chưa login: QR + claim_url) → cloud_domain_attach trỏ DNS + SSL. 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; 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
9
|
const sandbox = z.boolean().optional().describe('Thử 0đ, không tạo hạ tầng thật / Sandbox, no charge');
|
|
9
10
|
const id = z.string().trim().min(1).max(255);
|
|
@@ -49,7 +50,10 @@ export function registerComputeTools(server, clients) {
|
|
|
49
50
|
// Every new compute tool keeps a mechanical legacy alias, using the same schema and handler.
|
|
50
51
|
const register = (name, description, schema, handler) => {
|
|
51
52
|
for (const toolName of [name, name.replace(/^cloud_/, 'vibecloud_')]) {
|
|
53
|
+
const metadata = completeToolMetadata(toolName, undefined, undefined);
|
|
52
54
|
server.registerTool(toolName, {
|
|
55
|
+
title: metadata.title,
|
|
56
|
+
annotations: metadata.annotations,
|
|
53
57
|
description: toolName === name ? description : `Alias tương thích của ${name}. / Compatibility alias. ${description}`,
|
|
54
58
|
inputSchema: schema,
|
|
55
59
|
}, (args) => runTool(() => handler(args)));
|
package/dist/domains.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { runTool } from './errors.js';
|
|
3
|
+
import { toolAnnotationsForName } from './monapay.js';
|
|
3
4
|
const domainName = z.string().trim().toLowerCase()
|
|
4
5
|
.min(3).max(253)
|
|
5
6
|
.describe('Tên miền đầy đủ (vd: example.vn) hoặc chỉ tên chưa có đuôi (vd: example).');
|
|
@@ -24,6 +25,7 @@ const registrantSchema = z.object({
|
|
|
24
25
|
}).strict();
|
|
25
26
|
export function registerDomainTools(server, clients) {
|
|
26
27
|
server.registerTool('cloud_domain_search', {
|
|
28
|
+
annotations: toolAnnotationsForName('cloud_domain_search'),
|
|
27
29
|
title: 'Tìm kiếm + báo giá tên miền',
|
|
28
30
|
description: 'Kiểm tra tên miền còn trống và xem giá mua (VND, đã VAT). KHÔNG cần đăng nhập MONA Pass — gọi được ngay cả khi người dùng chưa có tài khoản; dùng trước khi mua/giữ chỗ để biết available/price.',
|
|
29
31
|
inputSchema: z.object({
|
|
@@ -49,11 +51,13 @@ export function registerDomainTools(server, clients) {
|
|
|
49
51
|
return results;
|
|
50
52
|
}));
|
|
51
53
|
server.registerTool('cloud_domain_registrant_get', {
|
|
54
|
+
annotations: toolAnnotationsForName('cloud_domain_registrant_get'),
|
|
52
55
|
title: 'Xem thông tin chủ thể đăng ký tên miền',
|
|
53
56
|
description: 'Lấy thông tin chủ thể (registrant) đã lưu. Cần dữ liệu này để mua tên miền.',
|
|
54
57
|
inputSchema: z.object({}).strict(),
|
|
55
58
|
}, () => runTool(() => clients.vibecloud('/api/domains/registrant')));
|
|
56
59
|
server.registerTool('cloud_domain_registrant_set', {
|
|
60
|
+
annotations: toolAnnotationsForName('cloud_domain_registrant_set'),
|
|
57
61
|
title: 'Cập nhật thông tin chủ thể đăng ký tên miền',
|
|
58
62
|
description: 'Lưu thông tin chủ thể để đăng ký tên miền. HỎI NGƯỜI DÙNG cung cấp NGAY TRONG PHIÊN: họ tên, email, điện thoại, địa chỉ. Tên miền .vn cá nhân cần thêm CCCD 12 số + ngày sinh (DD/MM/YYYY) + giới tính; .vn tổ chức cần org_name + tax_code (MST) + representative. Lưu 1 lần, tái dùng cho các domain sau. Không tự bịa dữ liệu — thiếu trường nào thì hỏi đúng trường đó.',
|
|
59
63
|
inputSchema: registrantSchema,
|
|
@@ -62,6 +66,7 @@ export function registerDomainTools(server, clients) {
|
|
|
62
66
|
body: args,
|
|
63
67
|
})));
|
|
64
68
|
server.registerTool('cloud_domain_buy', {
|
|
69
|
+
annotations: toolAnnotationsForName('cloud_domain_buy'),
|
|
65
70
|
title: 'Mua tên miền',
|
|
66
71
|
description: [
|
|
67
72
|
'Mua tên miền và trừ ví VND — làm trọn trong phiên; người dùng chỉ quét QR và xác nhận, không phải đăng ký trước hay tự làm trên web. Trước khi gọi:',
|
|
@@ -93,6 +98,7 @@ export function registerDomainTools(server, clients) {
|
|
|
93
98
|
}));
|
|
94
99
|
// --- Guest → reserve → claim (spec monadomain §12): mua TRƯỚC khi có tài khoản ---------------
|
|
95
100
|
server.registerTool('cloud_domain_reserve', {
|
|
101
|
+
annotations: toolAnnotationsForName('cloud_domain_reserve'),
|
|
96
102
|
title: 'Giữ chỗ tên miền + QR trả tiền (không cần tài khoản)',
|
|
97
103
|
description: [
|
|
98
104
|
'Giữ chỗ tên miền 30 phút cho người dùng CHƯA có MONA Pass (hoặc chưa login trên máy này) — không trừ tiền, không đăng ký thật, chỉ khoá tên trong hệ MONA Cloud.',
|
|
@@ -119,6 +125,7 @@ export function registerDomainTools(server, clients) {
|
|
|
119
125
|
...(clients.sandboxEnabled(sandbox === true) ? { headers: { 'X-Vibecloud-Sandbox': '1' } } : {}),
|
|
120
126
|
})));
|
|
121
127
|
server.registerTool('cloud_domain_reserve_status', {
|
|
128
|
+
annotations: toolAnnotationsForName('cloud_domain_reserve_status'),
|
|
122
129
|
title: 'Trạng thái giữ chỗ tên miền',
|
|
123
130
|
description: 'Xem reservation đã nhận tiền chưa / đã claim chưa / còn hạn không. Guest truyền claim_token (hoặc guest_token) nhận từ cloud_domain_reserve; chủ reservation đã login thì không cần. Đọc next_step trong kết quả để biết bước kế.',
|
|
124
131
|
inputSchema: z.object({
|
|
@@ -127,6 +134,7 @@ export function registerDomainTools(server, clients) {
|
|
|
127
134
|
}).strict(),
|
|
128
135
|
}, ({ reservation_id, claim_token }) => runTool(() => clients.vibecloudGuestOk(`/api/domains/reserve/${encodeURIComponent(reservation_id)}`, { query: claim_token ? { t: claim_token } : undefined, guestToken: claim_token })));
|
|
129
136
|
server.registerTool('cloud_domain_claim', {
|
|
137
|
+
annotations: toolAnnotationsForName('cloud_domain_claim'),
|
|
130
138
|
title: 'Nhận reservation về tài khoản + mua (claim = đăng ký = trả tiền)',
|
|
131
139
|
description: [
|
|
132
140
|
'Gắn reservation vào MONA Pass đang đăng nhập, kéo tiền đã chuyển (nếu có) về ví rồi MUA ngay. Cần đăng nhập (monacloud-mcp login) — lần đầu đăng nhập MONA Cloud tự tạo hồ sơ + ví, không có form đăng ký riêng.',
|
|
@@ -141,6 +149,7 @@ export function registerDomainTools(server, clients) {
|
|
|
141
149
|
}).strict(),
|
|
142
150
|
}, ({ reservation_id, ...body }) => runTool(() => clients.vibecloud(`/api/domains/reserve/${encodeURIComponent(reservation_id)}/claim`, { method: 'POST', body })));
|
|
143
151
|
server.registerTool('cloud_domain_reserve_release', {
|
|
152
|
+
annotations: toolAnnotationsForName('cloud_domain_reserve_release'),
|
|
144
153
|
title: 'Nhả chỗ tên miền đã giữ',
|
|
145
154
|
description: 'Huỷ reservation chưa nhận tiền (người dùng đổi ý / chọn tên khác). Đã có tiền vào thì không huỷ được — dùng cloud_domain_claim.',
|
|
146
155
|
inputSchema: z.object({
|
|
@@ -149,6 +158,7 @@ export function registerDomainTools(server, clients) {
|
|
|
149
158
|
}).strict(),
|
|
150
159
|
}, ({ reservation_id, claim_token }) => runTool(() => clients.vibecloudGuestOk(`/api/domains/reserve/${encodeURIComponent(reservation_id)}`, { method: 'DELETE', query: claim_token ? { t: claim_token } : undefined, guestToken: claim_token })));
|
|
151
160
|
server.registerTool('cloud_domain_list', {
|
|
161
|
+
annotations: toolAnnotationsForName('cloud_domain_list'),
|
|
152
162
|
title: 'Danh sách tên miền đã import/mua',
|
|
153
163
|
description: 'Liệt kê tên miền đã đưa vào MONA Cloud (import + mua). sandbox=true → xem domain sandbox.',
|
|
154
164
|
inputSchema: z.object({
|
|
@@ -159,6 +169,7 @@ export function registerDomainTools(server, clients) {
|
|
|
159
169
|
return clients.vibecloud('/api/domains', { headers });
|
|
160
170
|
}));
|
|
161
171
|
server.registerTool('cloud_domain_verify_start', {
|
|
172
|
+
annotations: toolAnnotationsForName('cloud_domain_verify_start'),
|
|
162
173
|
title: 'Bắt đầu nộp hồ sơ .vn',
|
|
163
174
|
description: 'Sinh bản khai đã điền sẵn và link upload hồ sơ .vn. Dùng sau khi mua tên miền .vn (status=pending_verification).',
|
|
164
175
|
inputSchema: z.object({
|
|
@@ -168,6 +179,7 @@ export function registerDomainTools(server, clients) {
|
|
|
168
179
|
method: 'POST',
|
|
169
180
|
})));
|
|
170
181
|
server.registerTool('cloud_domain_verify_status', {
|
|
182
|
+
annotations: toolAnnotationsForName('cloud_domain_verify_status'),
|
|
171
183
|
title: 'Trạng thái hồ sơ .vn',
|
|
172
184
|
description: 'Kiểm tra trạng thái duyệt hồ sơ đăng ký .vn (profile_status từ MONA Host).',
|
|
173
185
|
inputSchema: z.object({
|
|
@@ -175,6 +187,7 @@ export function registerDomainTools(server, clients) {
|
|
|
175
187
|
}).strict(),
|
|
176
188
|
}, ({ id }) => runTool(() => clients.vibecloud(`/api/domains/${encodeURIComponent(id)}/verify`)));
|
|
177
189
|
server.registerTool('cloud_domain_health', {
|
|
190
|
+
annotations: toolAnnotationsForName('cloud_domain_health'),
|
|
178
191
|
title: 'Kiểm tra sức khoẻ tên miền',
|
|
179
192
|
description: 'Kiểm tra hạn đăng ký, trạng thái hồ sơ, NS, SSL và cảnh báo tên miền đã mua.',
|
|
180
193
|
inputSchema: z.object({
|
|
@@ -182,6 +195,7 @@ export function registerDomainTools(server, clients) {
|
|
|
182
195
|
}).strict(),
|
|
183
196
|
}, ({ id }) => runTool(() => clients.vibecloud(`/api/domains/${encodeURIComponent(id)}/health`)));
|
|
184
197
|
server.registerTool('cloud_domain_renew', {
|
|
198
|
+
annotations: toolAnnotationsForName('cloud_domain_renew'),
|
|
185
199
|
title: 'Gia hạn tên miền (trừ ví VND)',
|
|
186
200
|
description: [
|
|
187
201
|
'Gia hạn tên miền đã mua qua MONA Cloud. LUÔN gọi dry_run=true trước để lấy price_vnd (đã VAT), HỎI người dùng duyệt số tiền, rồi gọi lại dry_run=false — lúc đó trừ ví VND và gia hạn thật ở registrar.',
|
|
@@ -194,6 +208,7 @@ export function registerDomainTools(server, clients) {
|
|
|
194
208
|
}).strict(),
|
|
195
209
|
}, ({ id, billing_cycle, dry_run }) => runTool(() => clients.guardedVibecloud(`/api/domains/${encodeURIComponent(id)}/renew`, { method: 'POST', body: { billing_cycle, dry_run: dry_run !== false } }, false)));
|
|
196
210
|
server.registerTool('cloud_domain_wait', {
|
|
211
|
+
annotations: toolAnnotationsForName('cloud_domain_wait'),
|
|
197
212
|
title: 'Chờ tên miền active',
|
|
198
213
|
description: 'Long-poll cho đến khi tên miền chuyển sang active/failed (mặc định timeout=60s). Dùng sau cloud_domain_buy để chờ MONA Host xử lý.',
|
|
199
214
|
inputSchema: z.object({
|
|
@@ -205,6 +220,7 @@ export function registerDomainTools(server, clients) {
|
|
|
205
220
|
return clients.vibecloud(`/api/domains/${encodeURIComponent(id)}/wait${params}`);
|
|
206
221
|
}));
|
|
207
222
|
server.registerTool('cloud_domain_webhook_set', {
|
|
223
|
+
annotations: toolAnnotationsForName('cloud_domain_webhook_set'),
|
|
208
224
|
title: 'Đăng ký webhook tên miền',
|
|
209
225
|
description: 'Đăng ký URL nhận sự kiện domain.status_changed (ký HMAC). Mỗi user 1 webhook; gọi lại để cập nhật.',
|
|
210
226
|
inputSchema: z.object({
|
|
@@ -216,6 +232,7 @@ export function registerDomainTools(server, clients) {
|
|
|
216
232
|
body: { url, secret },
|
|
217
233
|
})));
|
|
218
234
|
server.registerTool('cloud_domain_attach', {
|
|
235
|
+
annotations: toolAnnotationsForName('cloud_domain_attach'),
|
|
219
236
|
title: 'Gắn tên miền vào ứng dụng',
|
|
220
237
|
description: [
|
|
221
238
|
'Gắn tên miền vào app đã deploy. Tên miền .vn chưa active vẫn gọi được — attach đặt trước, server tự hoàn tất sau khi hồ sơ được duyệt và gửi webhook domain.status_changed.',
|
|
@@ -242,26 +259,31 @@ export function registerDomainTools(server, clients) {
|
|
|
242
259
|
priority: z.number().int().min(0).max(65535).optional().describe('Ưu tiên (MX/SRV).'),
|
|
243
260
|
};
|
|
244
261
|
server.registerTool('cloud_domain_dns_list', {
|
|
262
|
+
annotations: toolAnnotationsForName('cloud_domain_dns_list'),
|
|
245
263
|
title: 'Xem bản ghi DNS của tên miền',
|
|
246
264
|
description: 'Liệt kê bản ghi DNS (A/CNAME/MX/TXT...) của tên miền đăng ký tại MONA Cloud. domain_id lấy từ cloud_domain_list.',
|
|
247
265
|
inputSchema: z.object({ domain_id: objectId }).strict(),
|
|
248
266
|
}, ({ domain_id }) => runTool(() => clients.vibecloud(`/api/domains/${encodeURIComponent(domain_id)}/records`)));
|
|
249
267
|
server.registerTool('cloud_domain_dns_add', {
|
|
268
|
+
annotations: toolAnnotationsForName('cloud_domain_dns_add'),
|
|
250
269
|
title: 'Thêm bản ghi DNS',
|
|
251
270
|
description: 'Thêm 1 bản ghi DNS. Vd trỏ web: type=A, name=@, data=<IP>. Trỏ www: type=CNAME, name=www, data=<domain>. AI làm trọn.',
|
|
252
271
|
inputSchema: z.object({ domain_id: objectId, ...recordSchema }).strict(),
|
|
253
272
|
}, ({ domain_id, ...record }) => runTool(() => clients.vibecloud(`/api/domains/${encodeURIComponent(domain_id)}/records`, { method: 'POST', body: record })));
|
|
254
273
|
server.registerTool('cloud_domain_dns_update', {
|
|
274
|
+
annotations: toolAnnotationsForName('cloud_domain_dns_update'),
|
|
255
275
|
title: 'Sửa bản ghi DNS',
|
|
256
276
|
description: 'Sửa 1 bản ghi DNS theo record_id (lấy từ cloud_domain_dns_list).',
|
|
257
277
|
inputSchema: z.object({ domain_id: objectId, record_id: z.string().min(1).max(128), ...recordSchema }).strict(),
|
|
258
278
|
}, ({ domain_id, record_id, ...record }) => runTool(() => clients.vibecloud(`/api/domains/${encodeURIComponent(domain_id)}/records/${encodeURIComponent(record_id)}`, { method: 'PUT', body: record })));
|
|
259
279
|
server.registerTool('cloud_domain_dns_delete', {
|
|
280
|
+
annotations: toolAnnotationsForName('cloud_domain_dns_delete'),
|
|
260
281
|
title: 'Xoá bản ghi DNS',
|
|
261
282
|
description: 'Xoá 1 bản ghi DNS theo record_id.',
|
|
262
283
|
inputSchema: z.object({ domain_id: objectId, record_id: z.string().min(1).max(128) }).strict(),
|
|
263
284
|
}, ({ domain_id, record_id }) => runTool(() => clients.vibecloud(`/api/domains/${encodeURIComponent(domain_id)}/records/${encodeURIComponent(record_id)}`, { method: 'DELETE' })));
|
|
264
285
|
server.registerTool('cloud_domain_ns_set', {
|
|
286
|
+
annotations: toolAnnotationsForName('cloud_domain_ns_set'),
|
|
265
287
|
title: 'Đổi nameserver (NS) của tên miền',
|
|
266
288
|
description: 'Đổi NS cho tên miền (≥2). Vd giữ DNS ở MONA: ns1.mona.host, ns2.mona.host. Hoặc chuyển sang Cloudflare/nhà khác. AI làm hoàn toàn.',
|
|
267
289
|
inputSchema: z.object({
|
package/dist/mail.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { domainToASCII } from 'node:url';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { runTool } from './errors.js';
|
|
5
|
+
import { toolAnnotationsForName } from './monapay.js';
|
|
5
6
|
const id = z.string().trim().min(1).max(255);
|
|
6
7
|
const email = z.string().max(320).regex(/^[^\s@<>,;]+@[^\s@<>,;]+\.[^\s@<>,;]+$/, 'Cần địa chỉ email hợp lệ.');
|
|
7
8
|
const mailbox = z.union([
|
|
@@ -32,21 +33,25 @@ function idempotencyHeaders(key) {
|
|
|
32
33
|
}
|
|
33
34
|
export function registerMailTools(server, clients, config) {
|
|
34
35
|
server.registerTool('mail_account', {
|
|
36
|
+
annotations: toolAnnotationsForName('mail_account'),
|
|
35
37
|
title: 'Tài khoản MONA Mail',
|
|
36
38
|
description: `Khi bắt đầu tích hợp email, đọc tài khoản, quota và bước kế tiếp. / Use first to read account and quota at ${config.monamailApi}.`,
|
|
37
39
|
inputSchema: empty,
|
|
38
40
|
}, () => runTool(() => clients.mail('/v1/account')));
|
|
39
41
|
server.registerTool('mail_plans', {
|
|
42
|
+
annotations: toolAnnotationsForName('mail_plans'),
|
|
40
43
|
title: 'Gói MONA Mail',
|
|
41
44
|
description: 'Khi chọn gói gửi mail, đọc giá và quota hiện hành. / Use to compare current email plans.',
|
|
42
45
|
inputSchema: empty,
|
|
43
46
|
}, () => runTool(() => clients.mail('/v1/plans')));
|
|
44
47
|
server.registerTool('mail_plan_set', {
|
|
48
|
+
annotations: toolAnnotationsForName('mail_plan_set'),
|
|
45
49
|
title: 'Đổi gói MONA Mail',
|
|
46
50
|
description: 'Khi cần đổi quota, chọn gói; gói trả phí trừ ví VND, thiếu tiền gọi cloud_topup. / Use to change the email plan.',
|
|
47
51
|
inputSchema: z.object({ plan: z.enum(['free', 'khoi-nghiep', 'kinh-doanh', 'doanh-nghiep']) }).strict(),
|
|
48
52
|
}, ({ plan }) => runTool(() => clients.mail('/v1/account/plan', { method: 'PUT', body: { plan } })));
|
|
49
53
|
server.registerTool('mail_send', {
|
|
54
|
+
annotations: toolAnnotationsForName('mail_send'),
|
|
50
55
|
title: 'Gửi email giao dịch',
|
|
51
56
|
description: 'Khi gửi OTP hoặc thông báo, dùng domain đã verify; onboarding@monamail.vn chỉ gửi tới email chủ. sandbox=true thử 0đ, không gửi ra Internet. / Use to send transactional email or test in sandbox.',
|
|
52
57
|
inputSchema: z.object({
|
|
@@ -79,11 +84,13 @@ export function registerMailTools(server, clients, config) {
|
|
|
79
84
|
return sandbox ? { ...result, sandbox: true } : result;
|
|
80
85
|
}));
|
|
81
86
|
server.registerTool('mail_status', {
|
|
87
|
+
annotations: toolAnnotationsForName('mail_status'),
|
|
82
88
|
title: 'Trạng thái email',
|
|
83
89
|
description: 'Khi cần xác nhận thư đã giao, đọc trạng thái, events và sandbox_preview. / Use to inspect an email after sending.',
|
|
84
90
|
inputSchema: z.object({ email_id: id }).strict(),
|
|
85
91
|
}, ({ email_id }) => runTool(() => clients.mail(`/v1/emails/${encodeURIComponent(email_id)}`)));
|
|
86
92
|
server.registerTool('mail_list', {
|
|
93
|
+
annotations: toolAnnotationsForName('mail_list'),
|
|
87
94
|
title: 'Danh sách email',
|
|
88
95
|
description: 'Khi tra lịch sử gửi, lọc theo trạng thái, người nhận hoặc thời gian. / Use to search sent email history.',
|
|
89
96
|
inputSchema: z.object({
|
|
@@ -94,6 +101,7 @@ export function registerMailTools(server, clients, config) {
|
|
|
94
101
|
}).strict(),
|
|
95
102
|
}, (query) => runTool(() => clients.mail('/v1/emails', { query })));
|
|
96
103
|
server.registerTool('mail_domain_add', {
|
|
104
|
+
annotations: toolAnnotationsForName('mail_domain_add'),
|
|
97
105
|
title: 'Thêm domain gửi email',
|
|
98
106
|
description: 'Khi gửi bằng domain của app, thêm domain. Trả record DNS; nếu người dùng dùng Cloudflare có thể gọi mail_domain_cloudflare với token của họ (không lưu). / Use to register a sender domain and get DNS records.',
|
|
99
107
|
inputSchema: z.object({
|
|
@@ -107,6 +115,7 @@ export function registerMailTools(server, clients, config) {
|
|
|
107
115
|
instructions: 'Thêm các records vào DNS rồi gọi mail_domain_verify. Nếu dùng Cloudflare, gọi mail_domain_cloudflare với token người dùng cung cấp; token chỉ dùng một lần, không lưu. DKIM đúng là đủ để verify; SPF/DMARC là cảnh báo. Nếu đã có SPF, thêm include:_spf.monamail.vn vào record hiện có.',
|
|
108
116
|
})));
|
|
109
117
|
server.registerTool('mail_domain_verify', {
|
|
118
|
+
annotations: toolAnnotationsForName('mail_domain_verify'),
|
|
110
119
|
title: 'Xác minh DNS domain',
|
|
111
120
|
description: 'Khi đã thêm DNS, kiểm DKIM và trạng thái domain. / Use after adding DNS records to verify the sender domain.',
|
|
112
121
|
inputSchema: z.object({ domain_id: id, ...idempotency }).strict(),
|
|
@@ -114,6 +123,7 @@ export function registerMailTools(server, clients, config) {
|
|
|
114
123
|
method: 'POST', headers: idempotencyHeaders(idempotency_key),
|
|
115
124
|
})));
|
|
116
125
|
server.registerTool('mail_domain_cloudflare', {
|
|
126
|
+
annotations: toolAnnotationsForName('mail_domain_cloudflare'),
|
|
117
127
|
title: 'Thêm DNS qua Cloudflare',
|
|
118
128
|
description: 'Khi người dùng cung cấp token Cloudflare, thêm DNS rồi verify domain. Token dùng một lần, không lưu, không log. / Use a user-provided Cloudflare token to configure DNS and verify.',
|
|
119
129
|
inputSchema: z.object({ domain_id: id, api_token: z.string().trim().min(1).max(4096), ...idempotency }).strict(),
|
|
@@ -121,11 +131,13 @@ export function registerMailTools(server, clients, config) {
|
|
|
121
131
|
method: 'POST', headers: idempotencyHeaders(idempotency_key), body: { api_token },
|
|
122
132
|
})));
|
|
123
133
|
server.registerTool('mail_domains_list', {
|
|
134
|
+
annotations: toolAnnotationsForName('mail_domains_list'),
|
|
124
135
|
title: 'Domain MONA Mail',
|
|
125
136
|
description: 'Khi chọn địa chỉ gửi, xem domain và trạng thái xác minh. / Use to find verified sender domains.',
|
|
126
137
|
inputSchema: empty,
|
|
127
138
|
}, () => runTool(() => clients.mail('/v1/domains')));
|
|
128
139
|
server.registerTool('mail_api_key_create', {
|
|
140
|
+
annotations: toolAnnotationsForName('mail_api_key_create'),
|
|
129
141
|
title: 'Tạo API key MONA Mail',
|
|
130
142
|
description: 'Khi tích hợp SDK vào app, tạo key live hoặc test. Key chỉ trả một lần; ghi vào .env của app dưới tên MONAMAIL_API_KEY, không cần in ra chat. / Use to create an app key; store the one-time secret in .env.',
|
|
131
143
|
inputSchema: z.object({ name: id, mode: z.enum(['live', 'test']), ...idempotency }).strict(),
|
|
@@ -133,16 +145,19 @@ export function registerMailTools(server, clients, config) {
|
|
|
133
145
|
method: 'POST', headers: idempotencyHeaders(idempotency_key), body,
|
|
134
146
|
})));
|
|
135
147
|
server.registerTool('mail_api_keys_list', {
|
|
148
|
+
annotations: toolAnnotationsForName('mail_api_keys_list'),
|
|
136
149
|
title: 'Danh sách API key',
|
|
137
150
|
description: 'Khi kiểm tra key của app, đọc prefix và trạng thái; không trả secret. / Use to inspect existing API key metadata.',
|
|
138
151
|
inputSchema: empty,
|
|
139
152
|
}, () => runTool(() => clients.mail('/v1/api-keys')));
|
|
140
153
|
server.registerTool('mail_api_key_revoke', {
|
|
154
|
+
annotations: toolAnnotationsForName('mail_api_key_revoke'),
|
|
141
155
|
title: 'Thu hồi API key',
|
|
142
156
|
description: 'Khi key không còn dùng hoặc bị lộ, thu hồi bằng key_id. / Use to revoke an unused or compromised API key.',
|
|
143
157
|
inputSchema: z.object({ key_id: id }).strict(),
|
|
144
158
|
}, ({ key_id }) => runTool(() => clients.mail(`/v1/api-keys/${encodeURIComponent(key_id)}`, { method: 'DELETE' })));
|
|
145
159
|
server.registerTool('mail_webhook_create', {
|
|
160
|
+
annotations: toolAnnotationsForName('mail_webhook_create'),
|
|
146
161
|
title: 'Tạo webhook email',
|
|
147
162
|
description: 'Khi app cần nhận sự kiện gửi hoặc bounce, đăng ký HTTPS webhook; lưu secret một lần vào .env, không log. / Use to subscribe an app to email events.',
|
|
148
163
|
inputSchema: z.object({ url: httpsUrl, events: z.array(events).min(1).max(8), ...idempotency }).strict(),
|
|
@@ -150,11 +165,13 @@ export function registerMailTools(server, clients, config) {
|
|
|
150
165
|
method: 'POST', headers: idempotencyHeaders(idempotency_key), body,
|
|
151
166
|
})));
|
|
152
167
|
server.registerTool('mail_webhooks_list', {
|
|
168
|
+
annotations: toolAnnotationsForName('mail_webhooks_list'),
|
|
153
169
|
title: 'Danh sách webhook email',
|
|
154
170
|
description: 'Khi kiểm tra cấu hình sự kiện của app, liệt kê webhook. / Use to inspect registered email webhooks.',
|
|
155
171
|
inputSchema: empty,
|
|
156
172
|
}, () => runTool(() => clients.mail('/v1/webhooks')));
|
|
157
173
|
server.registerTool('mail_webhook_test', {
|
|
174
|
+
annotations: toolAnnotationsForName('mail_webhook_test'),
|
|
158
175
|
title: 'Thử webhook email',
|
|
159
176
|
description: 'Khi đã có endpoint, gửi mẫu email.delivered để kiểm tra HTTP response. / Use to test webhook delivery to an app.',
|
|
160
177
|
inputSchema: z.object({ webhook_id: id, ...idempotency }).strict(),
|
|
@@ -162,16 +179,19 @@ export function registerMailTools(server, clients, config) {
|
|
|
162
179
|
method: 'POST', headers: idempotencyHeaders(idempotency_key),
|
|
163
180
|
})));
|
|
164
181
|
server.registerTool('mail_suppressions_list', {
|
|
182
|
+
annotations: toolAnnotationsForName('mail_suppressions_list'),
|
|
165
183
|
title: 'Địa chỉ ngừng gửi',
|
|
166
184
|
description: 'Khi thư bị suppressed, xem địa chỉ và lý do ngừng gửi. / Use to diagnose suppressed recipients.',
|
|
167
185
|
inputSchema: empty,
|
|
168
186
|
}, () => runTool(() => clients.mail('/v1/suppressions')));
|
|
169
187
|
server.registerTool('mail_suppression_remove', {
|
|
188
|
+
annotations: toolAnnotationsForName('mail_suppression_remove'),
|
|
170
189
|
title: 'Gỡ suppression tài khoản',
|
|
171
190
|
description: 'Khi đã xử lý nguyên nhân chặn, gỡ suppression của tài khoản; lớp toàn hệ không gỡ được. / Use to remove an account-level suppression.',
|
|
172
191
|
inputSchema: z.object({ email }).strict(),
|
|
173
192
|
}, ({ email }) => runTool(() => clients.mail(`/v1/suppressions/${encodeURIComponent(email)}`, { method: 'DELETE' })));
|
|
174
193
|
server.registerTool('mail_template_create', {
|
|
194
|
+
annotations: toolAnnotationsForName('mail_template_create'),
|
|
175
195
|
title: 'Tạo mẫu email',
|
|
176
196
|
description: 'Khi app dùng lại nội dung mail, tạo template với biến {{ten_bien}}. / Use to create a reusable email template.',
|
|
177
197
|
inputSchema: z.object({ name: id, subject, html: z.string().min(1), text: z.string().min(1).optional(), ...idempotency }).strict(),
|
|
@@ -179,12 +199,14 @@ export function registerMailTools(server, clients, config) {
|
|
|
179
199
|
method: 'POST', headers: idempotencyHeaders(idempotency_key), body,
|
|
180
200
|
})));
|
|
181
201
|
server.registerTool('mail_stats', {
|
|
202
|
+
annotations: toolAnnotationsForName('mail_stats'),
|
|
182
203
|
title: 'Thống kê gửi email',
|
|
183
204
|
description: 'Khi đánh giá khả năng giao thư, đọc tỷ lệ delivered và bounce theo thời gian. / Use to review email delivery statistics.',
|
|
184
205
|
inputSchema: z.object({ from: dateOrTimestamp.optional(), to: dateOrTimestamp.optional() }).strict().refine((value) => !value.from || !value.to || Date.parse(value.from) <= Date.parse(value.to), 'Thời điểm from phải trước hoặc bằng to.'),
|
|
185
206
|
}, (query) => runTool(() => clients.mail('/v1/stats', { query })));
|
|
186
207
|
// ── Hộp thư AI agent (nhận + đọc + trả lời) ─────────────────────────────
|
|
187
208
|
server.registerTool('mail_inbox_create', {
|
|
209
|
+
annotations: toolAnnotationsForName('mail_inbox_create'),
|
|
188
210
|
title: 'Tạo hộp thư agent',
|
|
189
211
|
description: 'Khi cần một địa chỉ email cho agent trực (đọc thư, trả lời), tạo hộp. Bỏ domain để dùng miền agent.monamail.vn (nhận ngay); truyền domain đã verify để có địa chỉ brand. / Use to create an inbox an AI agent can read and reply from.',
|
|
190
212
|
inputSchema: z.object({ agent_id: agentId, domain: domain.optional() }).strict(),
|
|
@@ -192,21 +214,25 @@ export function registerMailTools(server, clients, config) {
|
|
|
192
214
|
method: 'POST', body: domain ? { agent_id, domain } : { agent_id },
|
|
193
215
|
})));
|
|
194
216
|
server.registerTool('mail_inbox_list', {
|
|
217
|
+
annotations: toolAnnotationsForName('mail_inbox_list'),
|
|
195
218
|
title: 'Danh sách hộp agent',
|
|
196
219
|
description: 'Khi cần biết agent đang trực những hộp nào, liệt kê hộp còn hoạt động. / Use to list active agent inboxes.',
|
|
197
220
|
inputSchema: empty,
|
|
198
221
|
}, () => runTool(() => clients.mail('/v1/inboxes')));
|
|
199
222
|
server.registerTool('mail_inbox_get', {
|
|
223
|
+
annotations: toolAnnotationsForName('mail_inbox_get'),
|
|
200
224
|
title: 'Xem hộp agent',
|
|
201
225
|
description: 'Khi cần địa chỉ và trạng thái của một hộp, đọc chi tiết. / Use to read one inbox.',
|
|
202
226
|
inputSchema: z.object({ inbox_id: id }).strict(),
|
|
203
227
|
}, ({ inbox_id }) => runTool(() => clients.mail(`/v1/inboxes/${encodeURIComponent(inbox_id)}`)));
|
|
204
228
|
server.registerTool('mail_inbox_delete', {
|
|
229
|
+
annotations: toolAnnotationsForName('mail_inbox_delete'),
|
|
205
230
|
title: 'Xóa hộp agent',
|
|
206
231
|
description: 'Khi hộp không còn dùng, xóa mềm; thư cũ còn đọc tới hết retention. / Use to soft-delete an inbox.',
|
|
207
232
|
inputSchema: z.object({ inbox_id: id }).strict(),
|
|
208
233
|
}, ({ inbox_id }) => runTool(() => clients.mail(`/v1/inboxes/${encodeURIComponent(inbox_id)}`, { method: 'DELETE' })));
|
|
209
234
|
server.registerTool('mail_inbox_messages', {
|
|
235
|
+
annotations: toolAnnotationsForName('mail_inbox_messages'),
|
|
210
236
|
title: 'Thư trong hộp agent',
|
|
211
237
|
description: 'Khi trực hộp, liệt kê thư nhận; lọc seen=false để lấy thư chưa xử lý, phân trang bằng cursor. / Use to list messages an agent needs to handle.',
|
|
212
238
|
inputSchema: z.object({
|
|
@@ -220,17 +246,20 @@ export function registerMailTools(server, clients, config) {
|
|
|
220
246
|
query: { limit, cursor, since, ...(seen === undefined ? {} : { seen: String(seen) }) },
|
|
221
247
|
})));
|
|
222
248
|
server.registerTool('mail_inbox_message', {
|
|
249
|
+
annotations: toolAnnotationsForName('mail_inbox_message'),
|
|
223
250
|
title: 'Đọc thư trong hộp',
|
|
224
251
|
description: 'Khi cần nội dung đầy đủ, header và đính kèm của một thư, đọc chi tiết; lần đọc đầu đánh dấu đã xem. / Use to read a full inbox message before replying.',
|
|
225
252
|
inputSchema: z.object({ inbox_id: id, message_id: id }).strict(),
|
|
226
253
|
}, ({ inbox_id, message_id }) => runTool(() => clients.mail(`/v1/inboxes/${encodeURIComponent(inbox_id)}/messages/${encodeURIComponent(message_id)}`)));
|
|
227
254
|
server.registerTool('mail_inbox_reply', {
|
|
255
|
+
annotations: toolAnnotationsForName('mail_inbox_reply'),
|
|
228
256
|
title: 'Trả lời thư trong hộp',
|
|
229
257
|
description: 'Khi agent trả lời khách, gửi đúng thread (In-Reply-To, References) từ địa chỉ hộp; cần text hoặc html. Việc khó rút lại (hứa giá, chuyển tiền) thì báo người, đừng tự gửi. / Use to reply to an inbox message in-thread.',
|
|
230
258
|
inputSchema: z.object({ inbox_id: id, message_id: id, text: z.string().min(1).optional(), html: z.string().min(1).optional() })
|
|
231
259
|
.strict().refine((value) => value.text || value.html, 'Cần text hoặc html để trả lời.'),
|
|
232
260
|
}, ({ inbox_id, message_id, ...body }) => runTool(() => clients.mail(`/v1/inboxes/${encodeURIComponent(inbox_id)}/messages/${encodeURIComponent(message_id)}/reply`, { method: 'POST', body })));
|
|
233
261
|
server.registerTool('mail_inbox_wait', {
|
|
262
|
+
annotations: toolAnnotationsForName('mail_inbox_wait'),
|
|
234
263
|
title: 'Chờ thư trong hộp',
|
|
235
264
|
description: 'Khi cần chờ thư tới (ví dụ mã OTP hoặc thư khớp mẫu), chờ tối đa timeout giây; có thư khớp trả full nội dung + extracted_code, hết giờ trả 204. Gọi lại nếu cần chờ lâu hơn. / Use to wait for an OTP or a matching message.',
|
|
236
265
|
inputSchema: z.object({ inbox_id: id, match: inboxMatch, timeout: z.number().int().min(0).max(300).default(120) }).strict(),
|
|
@@ -238,6 +267,7 @@ export function registerMailTools(server, clients, config) {
|
|
|
238
267
|
query: { match, timeout },
|
|
239
268
|
})));
|
|
240
269
|
server.registerTool('mail_inbox_update', {
|
|
270
|
+
annotations: toolAnnotationsForName('mail_inbox_update'),
|
|
241
271
|
title: 'Cấu hình hộp agent',
|
|
242
272
|
description: 'Khi cần đặt cách hộp xử lý thư: mode redirect (tự forward thư về email chủ) | store (chỉ lưu) | ai (để AI trực), địa chỉ forward_to, chat Telegram nhận báo, hoặc bật/tắt hộp. / Use to configure an inbox: redirect-forward email, Telegram chat, mode, active.',
|
|
243
273
|
inputSchema: z.object({
|
|
@@ -249,6 +279,7 @@ export function registerMailTools(server, clients, config) {
|
|
|
249
279
|
}).strict().refine((v) => Object.keys(v).length > 1, 'Cần ít nhất một trường để cập nhật.'),
|
|
250
280
|
}, ({ inbox_id, ...body }) => runTool(() => clients.mail(`/v1/inboxes/${encodeURIComponent(inbox_id)}`, { method: 'PATCH', body })));
|
|
251
281
|
server.registerTool('mail_inbox_batch', {
|
|
282
|
+
annotations: toolAnnotationsForName('mail_inbox_batch'),
|
|
252
283
|
title: 'Tạo nhiều hộp agent',
|
|
253
284
|
description: 'Khi cần tạo nhiều địa chỉ một lần (ví dụ mỗi nhân viên một hộp), tạo hàng loạt trong quota gói; lỗi từng hộp không hủy cả batch. / Use to create many inboxes at once.',
|
|
254
285
|
inputSchema: z.object({
|
package/dist/monapay.d.ts
CHANGED
|
@@ -10,6 +10,18 @@ type ImportedTool = {
|
|
|
10
10
|
handler: (...args: unknown[]) => unknown;
|
|
11
11
|
enabled: boolean;
|
|
12
12
|
};
|
|
13
|
+
export type ToolAnnotations = {
|
|
14
|
+
readOnlyHint: boolean;
|
|
15
|
+
destructiveHint: boolean;
|
|
16
|
+
idempotentHint: boolean;
|
|
17
|
+
openWorldHint: boolean;
|
|
18
|
+
};
|
|
19
|
+
/** Mechanical metadata shared by native tools and tools imported from monapay-mcp. */
|
|
20
|
+
export declare function toolAnnotationsForName(name: string): ToolAnnotations;
|
|
21
|
+
export declare function completeToolMetadata(name: string, title: unknown, annotations: unknown): {
|
|
22
|
+
title: string;
|
|
23
|
+
annotations: ToolAnnotations;
|
|
24
|
+
};
|
|
13
25
|
export declare function createMonaPayClient(config: Config, env?: NodeJS.ProcessEnv, fetchImpl?: typeof fetch): MonaPayClient;
|
|
14
26
|
export declare function importedMonaPayTools(config: Config, env?: NodeJS.ProcessEnv, fetchImpl?: typeof fetch): Record<string, ImportedTool>;
|
|
15
27
|
export declare function normalizeImportedResult(result: unknown): Promise<unknown>;
|
package/dist/monapay.js
CHANGED
|
@@ -2,6 +2,47 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { createServer as createMonaPayServer } from 'monapay-mcp';
|
|
3
3
|
import { MonaPayClient } from 'monapay-mcp/dist/client.js';
|
|
4
4
|
import { CloudError, errorResult } from './errors.js';
|
|
5
|
+
// cloud_subscription_update có thể huỷ gia hạn / dừng máy (cancel_action=stop) → xếp destructive để agent hỏi lại trước khi gọi.
|
|
6
|
+
const DESTRUCTIVE = /(?:^|_)(?:delete|remove|cancel|release|rotate|stop|destroy|reset|revoke|suspend)(?:_|$)|^cloud_subscription_update$/;
|
|
7
|
+
const READ_ONLY = /(?:^|_)(?:list|get|status|search|balance|ledger|health|whoami|usage|services|quote|detect|wait|logs|stats|templates|tlds|prices|packages)(?:_|$)/;
|
|
8
|
+
const IDEMPOTENT_WRITE = /(?:^|_)(?:set|update|attach)(?:_|$)/;
|
|
9
|
+
/** Mechanical metadata shared by native tools and tools imported from monapay-mcp. */
|
|
10
|
+
export function toolAnnotationsForName(name) {
|
|
11
|
+
const canonical = name.replace(/^vibecloud_/, 'cloud_');
|
|
12
|
+
const destructiveHint = DESTRUCTIVE.test(canonical);
|
|
13
|
+
const readOnlyHint = !destructiveHint && (READ_ONLY.test(canonical)
|
|
14
|
+
|| canonical === 'cloud_open_console'
|
|
15
|
+
|| canonical === 'mail_account'
|
|
16
|
+
|| canonical === 'mail_plans'
|
|
17
|
+
|| canonical === 'cloud_invoice_pdf'
|
|
18
|
+
|| canonical === 'cloud_base_credentials'
|
|
19
|
+
|| canonical === 'mail_inbox_message'
|
|
20
|
+
|| canonical === 'mail_inbox_messages'
|
|
21
|
+
|| canonical === 'monapay_me'
|
|
22
|
+
|| canonical === 'monapay_verify_signature'
|
|
23
|
+
|| canonical === 'monapay_generate_webhook_snippet'
|
|
24
|
+
|| canonical === 'monapay_quickstart');
|
|
25
|
+
const idempotentHint = readOnlyHint
|
|
26
|
+
|| (destructiveHint && !/(?:^|_)(?:rotate|reset)(?:_|$)/.test(canonical))
|
|
27
|
+
|| (!destructiveHint && IDEMPOTENT_WRITE.test(canonical));
|
|
28
|
+
return {
|
|
29
|
+
readOnlyHint,
|
|
30
|
+
destructiveHint,
|
|
31
|
+
idempotentHint,
|
|
32
|
+
openWorldHint: canonical !== 'cloud_app_detect',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function completeToolMetadata(name, title, annotations) {
|
|
36
|
+
const existing = annotations && typeof annotations === 'object'
|
|
37
|
+
? annotations
|
|
38
|
+
: {};
|
|
39
|
+
return {
|
|
40
|
+
title: typeof title === 'string' && title.trim()
|
|
41
|
+
? title
|
|
42
|
+
: `Công cụ MONA ${name.replace(/^(?:cloud|vibecloud|monapay|mail|agent)_/, '').replaceAll('_', ' ')}`,
|
|
43
|
+
annotations: { ...toolAnnotationsForName(name), ...existing },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
5
46
|
function linkedCredentials(config) {
|
|
6
47
|
try {
|
|
7
48
|
const parsed = JSON.parse(readFileSync(config.linksFile, 'utf8'));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createServer as createHttpServer } from "node:http";
|
|
3
|
+
import { createRemoteServer } from "./remote.js";
|
|
4
|
+
const bind = process.env.MCP_REMOTE_BIND ?? "127.0.0.1";
|
|
5
|
+
const port = Number.parseInt(process.env.MCP_REMOTE_PORT ?? "8765", 10);
|
|
6
|
+
const publicUrl = process.env.MCP_REMOTE_PUBLIC_URL ?? "https://mcp.monacloud.vn";
|
|
7
|
+
const issuer = process.env.MONACLOUD_ISSUER ?? "https://pass.monacloud.vn/realms/mona";
|
|
8
|
+
const initialAccessToken = process.env.MONA_PASS_INITIAL_ACCESS_TOKEN;
|
|
9
|
+
if (!initialAccessToken)
|
|
10
|
+
throw new Error("MONA_PASS_INITIAL_ACCESS_TOKEN is required");
|
|
11
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535)
|
|
12
|
+
throw new Error("MCP_REMOTE_PORT is invalid");
|
|
13
|
+
const dataDir = process.env.MCP_REMOTE_DATA_DIR ?? "./data";
|
|
14
|
+
const remote = await createRemoteServer({ publicUrl, issuer, initialAccessToken, dataDir });
|
|
15
|
+
const httpServer = createHttpServer(remote.app);
|
|
16
|
+
httpServer.listen(port, bind, () => {
|
|
17
|
+
console.log(JSON.stringify({ event: "listening", bind, port }));
|
|
18
|
+
});
|
|
19
|
+
let stopping = false;
|
|
20
|
+
async function shutdown() {
|
|
21
|
+
if (stopping)
|
|
22
|
+
return;
|
|
23
|
+
stopping = true;
|
|
24
|
+
await remote.close();
|
|
25
|
+
await new Promise((resolve, reject) => {
|
|
26
|
+
httpServer.close((error) => error ? reject(error) : resolve());
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
process.on("SIGTERM", () => void shutdown());
|
|
30
|
+
process.on("SIGINT", () => void shutdown());
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface VerifiedAccessToken {
|
|
2
|
+
token: string;
|
|
3
|
+
clientId: string;
|
|
4
|
+
scopes: string[];
|
|
5
|
+
expiresAt: number;
|
|
6
|
+
extra: {
|
|
7
|
+
sub: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export interface JwtVerifierOptions {
|
|
11
|
+
issuer: string;
|
|
12
|
+
fetch?: typeof globalThis.fetch;
|
|
13
|
+
jwksTtlMs?: number;
|
|
14
|
+
isRegisteredClient?: (clientId: string) => boolean | Promise<boolean>;
|
|
15
|
+
now?: () => number;
|
|
16
|
+
}
|
|
17
|
+
export declare function createAccessTokenVerifier(options: JwtVerifierOptions): (token: string) => Promise<VerifiedAccessToken>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createPublicKey, verify as verifySignature } from "node:crypto";
|
|
2
|
+
function decodeJson(value) {
|
|
3
|
+
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
4
|
+
}
|
|
5
|
+
function includesAccount(aud) {
|
|
6
|
+
return typeof aud === "string" ? aud === "account" : Array.isArray(aud) && aud.includes("account");
|
|
7
|
+
}
|
|
8
|
+
export function createAccessTokenVerifier(options) {
|
|
9
|
+
const issuer = options.issuer.replace(/\/$/, "");
|
|
10
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
11
|
+
const ttl = options.jwksTtlMs ?? 10 * 60_000;
|
|
12
|
+
const now = options.now ?? Date.now;
|
|
13
|
+
let cached;
|
|
14
|
+
async function getJwks() {
|
|
15
|
+
if (cached && cached.expires > now())
|
|
16
|
+
return cached.document;
|
|
17
|
+
const response = await fetchImpl(`${issuer}/protocol/openid-connect/certs`);
|
|
18
|
+
if (!response.ok)
|
|
19
|
+
throw new Error(`JWKS request failed: ${response.status}`);
|
|
20
|
+
const document = await response.json();
|
|
21
|
+
if (!Array.isArray(document.keys))
|
|
22
|
+
throw new Error("JWKS response has no keys");
|
|
23
|
+
cached = { expires: now() + ttl, document };
|
|
24
|
+
return document;
|
|
25
|
+
}
|
|
26
|
+
return async function verifyAccessToken(token) {
|
|
27
|
+
const parts = token.split(".");
|
|
28
|
+
if (parts.length !== 3)
|
|
29
|
+
throw new Error("Malformed JWT");
|
|
30
|
+
const header = decodeJson(parts[0]);
|
|
31
|
+
const claims = decodeJson(parts[1]);
|
|
32
|
+
if (header.alg !== "RS256" || !header.kid)
|
|
33
|
+
throw new Error("Unsupported JWT algorithm");
|
|
34
|
+
const jwks = await getJwks();
|
|
35
|
+
const jwk = jwks.keys?.find((key) => key.kid === header.kid && (!key.alg || key.alg === "RS256"));
|
|
36
|
+
if (!jwk)
|
|
37
|
+
throw new Error("JWT signing key not found");
|
|
38
|
+
const publicKey = createPublicKey({ key: jwk, format: "jwk" });
|
|
39
|
+
const valid = verifySignature("RSA-SHA256", Buffer.from(`${parts[0]}.${parts[1]}`), publicKey, Buffer.from(parts[2], "base64url"));
|
|
40
|
+
if (!valid)
|
|
41
|
+
throw new Error("Invalid JWT signature");
|
|
42
|
+
const currentSeconds = Math.floor(now() / 1000);
|
|
43
|
+
if (claims.iss !== issuer)
|
|
44
|
+
throw new Error("Invalid token issuer");
|
|
45
|
+
if (!claims.exp || claims.exp <= currentSeconds)
|
|
46
|
+
throw new Error("Expired access token");
|
|
47
|
+
if (!claims.sub || !claims.azp)
|
|
48
|
+
throw new Error("Missing token subject or authorized party");
|
|
49
|
+
const registered = options.isRegisteredClient
|
|
50
|
+
? await options.isRegisteredClient(claims.azp)
|
|
51
|
+
: false;
|
|
52
|
+
if (!includesAccount(claims.aud) && !registered)
|
|
53
|
+
throw new Error("Invalid token audience");
|
|
54
|
+
return {
|
|
55
|
+
token,
|
|
56
|
+
clientId: claims.azp,
|
|
57
|
+
scopes: claims.scope?.split(/\s+/).filter(Boolean) ?? [],
|
|
58
|
+
expiresAt: claims.exp,
|
|
59
|
+
extra: { sub: claims.sub },
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
}
|
package/dist/remote.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { createServer as createMcpServer } from "./server.js";
|
|
3
|
+
import { type VerifiedAccessToken } from "./remote-verify.js";
|
|
4
|
+
export interface RemoteServerOptions {
|
|
5
|
+
publicUrl: string;
|
|
6
|
+
issuer: string;
|
|
7
|
+
initialAccessToken: string;
|
|
8
|
+
/** Thư mục lưu client DCR (clients.json) để sống qua restart; bỏ trống = chỉ giữ trong RAM (test). */
|
|
9
|
+
dataDir?: string;
|
|
10
|
+
env?: NodeJS.ProcessEnv;
|
|
11
|
+
fetch?: typeof globalThis.fetch;
|
|
12
|
+
verifyAccessToken?: (token: string) => Promise<VerifiedAccessToken>;
|
|
13
|
+
createServer?: typeof createMcpServer;
|
|
14
|
+
now?: () => number;
|
|
15
|
+
}
|
|
16
|
+
export interface RemoteServer {
|
|
17
|
+
app: ReturnType<typeof express>;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
sessionCount(): number;
|
|
20
|
+
}
|
|
21
|
+
export declare function createRemoteServer(options: RemoteServerOptions): Promise<RemoteServer>;
|