@roxyon/mcp 0.4.0 → 0.5.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.
|
@@ -4,8 +4,422 @@
|
|
|
4
4
|
import { createRequire } from "module";
|
|
5
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
6
|
|
|
7
|
-
// src/
|
|
7
|
+
// src/baas-tools.ts
|
|
8
|
+
import { readFile } from "fs/promises";
|
|
9
|
+
import { join, resolve } from "path";
|
|
10
|
+
import { BAAS_MAX_BATCH, BAAS_MAX_PAGE, BaasDataApi, parseEnv } from "@roxyon/api-client";
|
|
8
11
|
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
// src/context.ts
|
|
14
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
15
|
+
var storage = new AsyncLocalStorage();
|
|
16
|
+
function runWithContext(ctx, fn) {
|
|
17
|
+
return storage.run(ctx, fn);
|
|
18
|
+
}
|
|
19
|
+
function currentContext() {
|
|
20
|
+
return storage.getStore() ?? {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/session.ts
|
|
24
|
+
import { Roxyon, isAuthError } from "@roxyon/api-client";
|
|
25
|
+
import { endpointsFromEnv, loadCredentials, tokenFromEnv } from "@roxyon/deploy-core";
|
|
26
|
+
var NotAuthenticatedError = class extends Error {
|
|
27
|
+
constructor() {
|
|
28
|
+
super(
|
|
29
|
+
"Not signed in to Roxyon. Run `roxyon login` in a terminal, or set ROXYON_TOKEN in this MCP server's environment."
|
|
30
|
+
);
|
|
31
|
+
this.name = "NotAuthenticatedError";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
async function getSession() {
|
|
35
|
+
const { apiUrl, consoleUrl } = endpointsFromEnv();
|
|
36
|
+
const requestToken = currentContext().token;
|
|
37
|
+
if (requestToken) {
|
|
38
|
+
return {
|
|
39
|
+
roxyon: new Roxyon({ sessionToken: requestToken, baseUrl: apiUrl, consoleUrl }),
|
|
40
|
+
source: "oauth"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const envToken = tokenFromEnv();
|
|
44
|
+
if (envToken) {
|
|
45
|
+
return {
|
|
46
|
+
roxyon: new Roxyon({ sessionToken: envToken, baseUrl: apiUrl, consoleUrl }),
|
|
47
|
+
source: "env-token"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const creds = await loadCredentials();
|
|
51
|
+
if (!creds?.sessionToken) throw new NotAuthenticatedError();
|
|
52
|
+
return {
|
|
53
|
+
roxyon: new Roxyon({ sessionToken: creds.sessionToken, baseUrl: apiUrl, consoleUrl }),
|
|
54
|
+
preferredSubscription: creds.subscription,
|
|
55
|
+
source: "stored-login"
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function isSessionGone(err) {
|
|
59
|
+
return err instanceof NotAuthenticatedError || isAuthError(err);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/result.ts
|
|
63
|
+
function text(body, structured) {
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text", text: body }],
|
|
66
|
+
...structured ? { structuredContent: structured } : {}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function errorResult(message, structured) {
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: "text", text: message }],
|
|
72
|
+
...structured ? { structuredContent: structured } : {},
|
|
73
|
+
isError: true
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function guard(fn) {
|
|
77
|
+
try {
|
|
78
|
+
return await fn();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (isSessionGone(err)) {
|
|
81
|
+
return errorResult(
|
|
82
|
+
err.message || "Roxyon session expired \u2014 run `roxyon login` again, or refresh ROXYON_TOKEN."
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/baas-tools.ts
|
|
90
|
+
var MAX_TEXT = 4e4;
|
|
91
|
+
var dirArg = z.string().optional().describe("Project directory whose .env holds RX_SECRET_KEY. Defaults to cwd.");
|
|
92
|
+
function remoteBlock() {
|
|
93
|
+
if (!currentContext().remote) return null;
|
|
94
|
+
return errorResult(
|
|
95
|
+
"The roxyon_baas_* tools read your app secret key from your local project .env, so they only run in the local MCP server (`npx -y @roxyon/mcp`). Manage data in the hosted connector via the console's Roxyon Base panel for now."
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
async function baasFor(dir) {
|
|
99
|
+
const root = resolve(dir ?? process.cwd());
|
|
100
|
+
let fileEnv = {};
|
|
101
|
+
try {
|
|
102
|
+
fileEnv = parseEnv(await readFile(join(root, ".env"), "utf8"));
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
const secretKey = fileEnv.RX_SECRET_KEY || process.env.RX_SECRET_KEY || "";
|
|
106
|
+
if (!secretKey) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`No BaaS V2 secret key found. Put RX_SECRET_KEY=rxsk_\u2026 in ${join(root, ".env")} (console.roxyon.com \u2192 Roxyon Base \u2192 your project \u2192 API keys). Keep it out of git and out of browser code.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return new BaasDataApi({ secretKey });
|
|
112
|
+
}
|
|
113
|
+
function clip(s) {
|
|
114
|
+
return s.length > MAX_TEXT ? `${s.slice(0, MAX_TEXT)}
|
|
115
|
+
\u2026 (truncated \u2014 narrow with fields/where/limit)` : s;
|
|
116
|
+
}
|
|
117
|
+
function dryRun(what) {
|
|
118
|
+
return text(`Would ${what}. Pass confirm:true to do it.`, { dryRun: true });
|
|
119
|
+
}
|
|
120
|
+
var SERVER_SCOPE = "Runs as the app backend (server scope, which passes every class rule). Local MCP only.";
|
|
121
|
+
function registerBaasTools(server) {
|
|
122
|
+
server.registerTool(
|
|
123
|
+
"roxyon_baas_query",
|
|
124
|
+
{
|
|
125
|
+
title: "Roxyon BaaS: query records",
|
|
126
|
+
description: `Read records of a class in the project's own app on BaaS V2. \`where\` is an object like {"Status":"active","Age":{"gt":18}}; for \`in\`, pass a comma-joined string. \`fields\` defaults to "*"; built-in meta columns must be named. Up to ${BAAS_MAX_PAGE} rows a page (use skip to page). ${SERVER_SCOPE}`,
|
|
127
|
+
inputSchema: {
|
|
128
|
+
dir: dirArg,
|
|
129
|
+
class: z.string(),
|
|
130
|
+
where: z.record(z.any()).optional(),
|
|
131
|
+
fields: z.string().optional(),
|
|
132
|
+
order: z.string().optional().describe('e.g. "-createdAt"'),
|
|
133
|
+
limit: z.number().int().min(1).max(BAAS_MAX_PAGE).optional(),
|
|
134
|
+
skip: z.number().int().min(0).optional(),
|
|
135
|
+
count: z.boolean().optional().describe("Also return the total number of matches.")
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
(args) => guard(async () => {
|
|
139
|
+
const blocked = remoteBlock();
|
|
140
|
+
if (blocked) return blocked;
|
|
141
|
+
const api = await baasFor(args.dir);
|
|
142
|
+
const { rows, count } = await api.query(args.class, {
|
|
143
|
+
where: args.where,
|
|
144
|
+
fields: args.fields,
|
|
145
|
+
order: args.order,
|
|
146
|
+
limit: args.limit,
|
|
147
|
+
skip: args.skip,
|
|
148
|
+
count: args.count
|
|
149
|
+
});
|
|
150
|
+
const head = `${rows.length} row(s)${count !== void 0 ? ` of ${count}` : ""}`;
|
|
151
|
+
return text(clip(`${head}
|
|
152
|
+
${JSON.stringify(rows, null, 2)}`), { rows, count });
|
|
153
|
+
})
|
|
154
|
+
);
|
|
155
|
+
server.registerTool(
|
|
156
|
+
"roxyon_baas_get",
|
|
157
|
+
{
|
|
158
|
+
title: "Roxyon BaaS: read one record",
|
|
159
|
+
description: `Read one record by objectId. ${SERVER_SCOPE}`,
|
|
160
|
+
inputSchema: { dir: dirArg, class: z.string(), objectId: z.string() }
|
|
161
|
+
},
|
|
162
|
+
(args) => guard(async () => {
|
|
163
|
+
const blocked = remoteBlock();
|
|
164
|
+
if (blocked) return blocked;
|
|
165
|
+
const api = await baasFor(args.dir);
|
|
166
|
+
const row = await api.get(args.class, args.objectId);
|
|
167
|
+
return text(clip(JSON.stringify(row, null, 2)), { row });
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
server.registerTool(
|
|
171
|
+
"roxyon_baas_create",
|
|
172
|
+
{
|
|
173
|
+
title: "Roxyon BaaS: create a record",
|
|
174
|
+
description: `Insert one record into an existing class (V2 has no schema changes with a secret key: add classes and columns in the console). For relations store the other row's objectId in a string column. Needs confirm:true. ${SERVER_SCOPE}`,
|
|
175
|
+
inputSchema: {
|
|
176
|
+
dir: dirArg,
|
|
177
|
+
class: z.string(),
|
|
178
|
+
data: z.record(z.any()),
|
|
179
|
+
confirm: z.boolean().optional()
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
(args) => guard(async () => {
|
|
183
|
+
const blocked = remoteBlock();
|
|
184
|
+
if (blocked) return blocked;
|
|
185
|
+
if (!args.confirm) return dryRun(`insert into ${args.class}: ${JSON.stringify(args.data)}`);
|
|
186
|
+
const api = await baasFor(args.dir);
|
|
187
|
+
const row = await api.create(args.class, args.data);
|
|
188
|
+
return text(`\u2713 Created ${args.class} ${String(row.objectId ?? "")}.`, { ok: true, row });
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
server.registerTool(
|
|
192
|
+
"roxyon_baas_update",
|
|
193
|
+
{
|
|
194
|
+
title: "Roxyon BaaS: update a record",
|
|
195
|
+
description: `Set fields on one record by objectId. Needs confirm:true. ${SERVER_SCOPE}`,
|
|
196
|
+
inputSchema: {
|
|
197
|
+
dir: dirArg,
|
|
198
|
+
class: z.string(),
|
|
199
|
+
objectId: z.string(),
|
|
200
|
+
data: z.record(z.any()),
|
|
201
|
+
confirm: z.boolean().optional()
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
(args) => guard(async () => {
|
|
205
|
+
const blocked = remoteBlock();
|
|
206
|
+
if (blocked) return blocked;
|
|
207
|
+
if (!args.confirm) {
|
|
208
|
+
return dryRun(`update ${args.class} ${args.objectId}: ${JSON.stringify(args.data)}`);
|
|
209
|
+
}
|
|
210
|
+
const api = await baasFor(args.dir);
|
|
211
|
+
const row = await api.update(args.class, args.objectId, args.data);
|
|
212
|
+
return text(`\u2713 Updated ${args.class} ${args.objectId}.`, { ok: true, row });
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
server.registerTool(
|
|
216
|
+
"roxyon_baas_delete",
|
|
217
|
+
{
|
|
218
|
+
title: "Roxyon BaaS: delete a record",
|
|
219
|
+
description: `Delete one record by objectId. Needs confirm:true. ${SERVER_SCOPE}`,
|
|
220
|
+
inputSchema: {
|
|
221
|
+
dir: dirArg,
|
|
222
|
+
class: z.string(),
|
|
223
|
+
objectId: z.string(),
|
|
224
|
+
confirm: z.boolean().optional()
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
(args) => guard(async () => {
|
|
228
|
+
const blocked = remoteBlock();
|
|
229
|
+
if (blocked) return blocked;
|
|
230
|
+
if (!args.confirm) return dryRun(`delete ${args.class} ${args.objectId}`);
|
|
231
|
+
const api = await baasFor(args.dir);
|
|
232
|
+
await api.delete(args.class, args.objectId);
|
|
233
|
+
return text(`\u2713 Deleted ${args.class} ${args.objectId}.`, { ok: true });
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
server.registerTool(
|
|
237
|
+
"roxyon_baas_batch",
|
|
238
|
+
{
|
|
239
|
+
title: "Roxyon BaaS: batch of record requests",
|
|
240
|
+
description: `1 to ${BAAS_MAX_BATCH} record requests in one call, each answered on its own (one failing doesn't stop the others). path is /Class or /Class/<objectId>; for POST/PUT the body is {"data":{...}}; for GET the body holds query options (where, fields, limit\u2026). A batch with any write needs confirm:true. ${SERVER_SCOPE}`,
|
|
241
|
+
inputSchema: {
|
|
242
|
+
dir: dirArg,
|
|
243
|
+
requests: z.array(
|
|
244
|
+
z.object({
|
|
245
|
+
method: z.enum(["GET", "POST", "PUT", "DELETE"]),
|
|
246
|
+
path: z.string(),
|
|
247
|
+
body: z.record(z.any()).optional()
|
|
248
|
+
})
|
|
249
|
+
).min(1).max(BAAS_MAX_BATCH),
|
|
250
|
+
confirm: z.boolean().optional()
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
(args) => guard(async () => {
|
|
254
|
+
const blocked = remoteBlock();
|
|
255
|
+
if (blocked) return blocked;
|
|
256
|
+
const writes = args.requests.filter((r) => r.method !== "GET");
|
|
257
|
+
if (writes.length && !args.confirm) {
|
|
258
|
+
return dryRun(
|
|
259
|
+
`run ${args.requests.length} request(s), ${writes.length} of them writes: ${writes.map((r) => `${r.method} ${r.path}`).join(", ")}`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
const api = await baasFor(args.dir);
|
|
263
|
+
const results = await api.batch(args.requests);
|
|
264
|
+
const failed = results.filter((r) => r.ok === false).length;
|
|
265
|
+
return text(
|
|
266
|
+
clip(
|
|
267
|
+
`${results.length} answer(s), ${failed} failed
|
|
268
|
+
${JSON.stringify(results, null, 2)}`
|
|
269
|
+
),
|
|
270
|
+
{ results }
|
|
271
|
+
);
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/commerce-tools.ts
|
|
277
|
+
import { BILLING_CYCLES, Roxyon as Roxyon2 } from "@roxyon/api-client";
|
|
278
|
+
import { endpointsFromEnv as endpointsFromEnv2 } from "@roxyon/deploy-core";
|
|
279
|
+
import { z as z2 } from "zod";
|
|
280
|
+
function publicRoxyon() {
|
|
281
|
+
const { apiUrl, consoleUrl } = endpointsFromEnv2();
|
|
282
|
+
return new Roxyon2({ baseUrl: apiUrl, consoleUrl });
|
|
283
|
+
}
|
|
284
|
+
function money(n) {
|
|
285
|
+
return n === void 0 ? "\u2014" : `$${n.toFixed(2)}`;
|
|
286
|
+
}
|
|
287
|
+
function productLine(p) {
|
|
288
|
+
const spec = p.vcpu !== void 0 ? ` \u2014 ${p.vcpu} vCPU (${p.cpuType}), ${p.ramGb} GB RAM, ${p.diskGb} GB disk${p.ipv4Included ? `, ${p.ipv4Included} IPv4` : ""}` : "";
|
|
289
|
+
const prices = BILLING_CYCLES.filter((c) => p.pricesUSD[c] !== void 0).map((c) => `${c} ${money(p.pricesUSD[c])}`).join(", ");
|
|
290
|
+
return `- ${p.sku || p.planId} "${p.name}" [${p.category}]${spec} | USD ${prices}${p.stock === "out" ? " | OUT OF STOCK" : ""} (planId ${p.planId})`;
|
|
291
|
+
}
|
|
292
|
+
function registerCommerceTools(server) {
|
|
293
|
+
server.registerTool(
|
|
294
|
+
"roxyon_catalog",
|
|
295
|
+
{
|
|
296
|
+
title: "Roxyon: product catalog",
|
|
297
|
+
description: 'List what Roxyon sells: cloud servers (cloud.vps.launch / scale / power) and web hosting (cloud.hosting.solo / multiple / isolated). Each product has its planId (what roxyon_quote takes), SKU, specs (vCPU, RAM GB, disk GB, IPv4 included) for servers, list prices in USD per billing cycle BEFORE tax and currency conversion, and stock ("out" = cannot be ordered now). No sign-in needed. Use roxyon_quote for the exact amount a buyer would pay. Read-only.',
|
|
298
|
+
inputSchema: {
|
|
299
|
+
category: z2.string().optional().describe('Filter by category prefix, e.g. "cloud.vps" or "cloud.hosting".'),
|
|
300
|
+
in_stock_only: z2.boolean().optional()
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
(args) => guard(async () => {
|
|
304
|
+
let products = await publicRoxyon().billing.catalog();
|
|
305
|
+
if (args.category) products = products.filter((p) => p.category.startsWith(args.category));
|
|
306
|
+
if (args.in_stock_only) products = products.filter((p) => p.stock === "in");
|
|
307
|
+
return text(
|
|
308
|
+
products.length ? products.map(productLine).join("\n") : "(no products match)",
|
|
309
|
+
{ products }
|
|
310
|
+
);
|
|
311
|
+
})
|
|
312
|
+
);
|
|
313
|
+
server.registerTool(
|
|
314
|
+
"roxyon_quote",
|
|
315
|
+
{
|
|
316
|
+
title: "Roxyon: price a new subscription",
|
|
317
|
+
description: "The exact amount Roxyon's checkout would charge for a NEW subscription to one plan, computed by the platform itself (same server pricing the console checkout uses, setup fee included): line items, subtotal, tax, total, in the requested currency. Pass the buyer currency and country: without them the quote is in USD with no tax, while checkout uses the account's own currency/country. No sign-in needed. Read-only: creates no order. Domains are not priced by this tool yet.",
|
|
318
|
+
inputSchema: {
|
|
319
|
+
plan: z2.string().describe('planId (or SKU, e.g. "SCALE-S") from roxyon_catalog.'),
|
|
320
|
+
cycle: z2.enum(BILLING_CYCLES),
|
|
321
|
+
currency: z2.string().length(3).optional().describe('ISO 4217, e.g. "USD", "SAR".'),
|
|
322
|
+
country: z2.string().length(2).optional().describe('ISO 3166 alpha-2, decides tax, e.g. "SA".')
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
(args) => guard(async () => {
|
|
326
|
+
const roxyon = publicRoxyon();
|
|
327
|
+
const want = args.plan.toLowerCase();
|
|
328
|
+
const product = (await roxyon.billing.catalog()).find(
|
|
329
|
+
(p) => p.planId === args.plan || p.sku.toLowerCase() === want
|
|
330
|
+
);
|
|
331
|
+
if (!product)
|
|
332
|
+
throw new Error(`No product "${args.plan}" in the catalog. See roxyon_catalog.`);
|
|
333
|
+
const q = await roxyon.billing.quote({
|
|
334
|
+
planId: product.planId,
|
|
335
|
+
cycle: args.cycle,
|
|
336
|
+
currency: args.currency,
|
|
337
|
+
country: args.country
|
|
338
|
+
});
|
|
339
|
+
const lines = [
|
|
340
|
+
...q.lineItems.map((l) => `${l.label}: ${l.amount} ${q.currency}`),
|
|
341
|
+
`Subtotal: ${q.subtotal} ${q.currency}`,
|
|
342
|
+
`${q.taxLabel || "Tax"} (${q.taxRate}%): ${q.taxAmount} ${q.currency}`,
|
|
343
|
+
`Total: ${q.total} ${q.currency}`
|
|
344
|
+
];
|
|
345
|
+
if (product?.stock === "out") lines.push("Note: this plan is out of stock right now.");
|
|
346
|
+
return text(lines.join("\n"), { quote: q });
|
|
347
|
+
})
|
|
348
|
+
);
|
|
349
|
+
server.registerTool(
|
|
350
|
+
"roxyon_list_subscriptions",
|
|
351
|
+
{
|
|
352
|
+
title: "Roxyon: my subscriptions (billing)",
|
|
353
|
+
description: "The signed-in user's own subscriptions with billing details: plan + SKU, status, billing cycle, recurring price and currency, next billing date, auto-renew, and for servers the state (building / running / stopped\u2026) and public IP. Only subscriptions the user owns. Read-only.",
|
|
354
|
+
inputSchema: {}
|
|
355
|
+
},
|
|
356
|
+
() => guard(async () => {
|
|
357
|
+
const session = await getSession();
|
|
358
|
+
const subs = await session.roxyon.billing.subscriptions();
|
|
359
|
+
const body = subs.length ? subs.map(
|
|
360
|
+
(s) => `- ${s.name || s.id} [${s.status}] ${s.plan}${s.sku ? ` (${s.sku})` : ""} \u2014 ${s.price} ${s.currency}/${s.cycle || "?"}, next billing ${s.nextBillingDate ?? "\u2014"}${s.autoRenew ? ", auto-renew" : ""}${s.serverState ? `, server ${s.serverState}` : ""}${s.publicIp ? ` ${s.publicIp}` : ""}`
|
|
361
|
+
).join("\n") : "(no subscriptions on this account)";
|
|
362
|
+
return text(body, { subscriptions: subs });
|
|
363
|
+
})
|
|
364
|
+
);
|
|
365
|
+
server.registerTool(
|
|
366
|
+
"roxyon_list_invoices",
|
|
367
|
+
{
|
|
368
|
+
title: "Roxyon: my invoices",
|
|
369
|
+
description: "The signed-in user's own invoices, newest first: number, type, status (pending = unpaid, paid, overdue, void, failed, cancelled), total and currency, due date, and payUrl (the console page where the user pays; null when paid). Read-only: this never pays anything.",
|
|
370
|
+
inputSchema: {
|
|
371
|
+
status: z2.enum(["pending", "paid", "overdue", "void", "failed", "cancelled"]).optional(),
|
|
372
|
+
limit: z2.number().int().min(1).max(100).optional().describe("Default 25.")
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
(args) => guard(async () => {
|
|
376
|
+
const session = await getSession();
|
|
377
|
+
const invoices = await session.roxyon.billing.invoices({
|
|
378
|
+
status: args.status,
|
|
379
|
+
limit: args.limit
|
|
380
|
+
});
|
|
381
|
+
const body = invoices.length ? invoices.map(
|
|
382
|
+
(i) => `- #${i.number || i.id} ${i.type} [${i.status}] ${i.total} ${i.currency}, due ${i.dueDate ?? "\u2014"}${i.payUrl ? ` \u2014 pay at ${i.payUrl}` : ""}`
|
|
383
|
+
).join("\n") : "(no invoices)";
|
|
384
|
+
return text(body, { invoices });
|
|
385
|
+
})
|
|
386
|
+
);
|
|
387
|
+
server.registerTool(
|
|
388
|
+
"roxyon_pay_link",
|
|
389
|
+
{
|
|
390
|
+
title: "Roxyon: where to pay an invoice",
|
|
391
|
+
description: 'The URL where the signed-in user pays one of their own open invoices (status pending, overdue or failed), plus its amount and due date. Give the invoice number (e.g. "INV2610055") or id from roxyon_list_invoices. A human opens the link and pays in the console; this tool never charges anything. Paid or void invoices have no link.',
|
|
392
|
+
inputSchema: {
|
|
393
|
+
invoice: z2.string().min(1).max(40).describe("Invoice number or id.")
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
(args) => guard(async () => {
|
|
397
|
+
const session = await getSession();
|
|
398
|
+
const want = args.invoice.replace(/^#/, "").toLowerCase();
|
|
399
|
+
const inv = (await session.roxyon.billing.invoices({ limit: 100 })).find(
|
|
400
|
+
(i) => i.id.toLowerCase() === want || i.number.toLowerCase() === want
|
|
401
|
+
);
|
|
402
|
+
if (!inv) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`No invoice "${args.invoice}" among your latest 100. See roxyon_list_invoices.`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
if (!inv.payUrl) {
|
|
408
|
+
return text(`Invoice #${inv.number || inv.id} is ${inv.status}: nothing to pay.`, {
|
|
409
|
+
invoice: inv
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return text(
|
|
413
|
+
`Invoice #${inv.number || inv.id}: ${inv.total} ${inv.currency}, due ${inv.dueDate ?? "\u2014"}.
|
|
414
|
+
Pay at ${inv.payUrl} (sign in, open Invoices, choose this invoice).`,
|
|
415
|
+
{ invoice: inv, payUrl: inv.payUrl }
|
|
416
|
+
);
|
|
417
|
+
})
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/prompts.ts
|
|
422
|
+
import { z as z3 } from "zod";
|
|
9
423
|
function registerPrompts(server) {
|
|
10
424
|
server.registerPrompt(
|
|
11
425
|
"scaffold-lumen-app",
|
|
@@ -13,8 +427,8 @@ function registerPrompts(server) {
|
|
|
13
427
|
title: "Scaffold a LumenJS + Roxyon app",
|
|
14
428
|
description: "Guidance for creating a new LumenJS SPA wired to the Roxyon BaaS and ready to deploy.",
|
|
15
429
|
argsSchema: {
|
|
16
|
-
idea:
|
|
17
|
-
dir:
|
|
430
|
+
idea: z3.string().describe("What the app should do."),
|
|
431
|
+
dir: z3.string().optional().describe("Where to create it.")
|
|
18
432
|
}
|
|
19
433
|
},
|
|
20
434
|
({ idea, dir }) => ({
|
|
@@ -28,7 +442,7 @@ function registerPrompts(server) {
|
|
|
28
442
|
Steps:
|
|
29
443
|
1. Read the "roxyon://docs/lumenjs" resource \u2014 follow V1 rules exactly (one bind per region, no nested bind, build option lists in JS not :for, check every BaaS write for an error field).
|
|
30
444
|
2. Read "roxyon://docs/baas" for the data/auth API.
|
|
31
|
-
3. Scaffold with the LumenJS CLI: \`lm create <name>\`${dir ? ` in ${dir}` : ""}. Put views under src/views/, wire the BaaS in src/index.js via config.json.
|
|
445
|
+
3. Scaffold with the LumenJS CLI 2.5.7: \`npx -y -p @lmjs/cli@2.5.7 lm create <name>\`${dir ? ` in ${dir}` : ""}. Put views under src/views/, wire the BaaS in src/index.js via config.json.
|
|
32
446
|
4. When it runs locally (\`lm serve\`), call roxyon_init then roxyon_deploy (dry run first, then confirm:true).`
|
|
33
447
|
}
|
|
34
448
|
}
|
|
@@ -41,8 +455,8 @@ Steps:
|
|
|
41
455
|
title: "Build a site and host it on Roxyon",
|
|
42
456
|
description: "Generate a static site (or no-build LumenJS SPA) in the conversation and publish it to a Roxyon host \u2014 for the hosted connector, no local files.",
|
|
43
457
|
argsSchema: {
|
|
44
|
-
brief:
|
|
45
|
-
host:
|
|
458
|
+
brief: z3.string().describe("What the site should be."),
|
|
459
|
+
host: z3.string().describe("Where to host it, e.g. promo.mycompany.com")
|
|
46
460
|
}
|
|
47
461
|
},
|
|
48
462
|
({ brief, host }) => ({
|
|
@@ -71,7 +485,7 @@ Brief: ${brief}
|
|
|
71
485
|
title: "Install WordPress on Roxyon",
|
|
72
486
|
description: "Provision a host + database and install WordPress core \u2014 local/CLI MCP only.",
|
|
73
487
|
argsSchema: {
|
|
74
|
-
host:
|
|
488
|
+
host: z3.string().describe("Where to install it, e.g. blog.mycompany.com")
|
|
75
489
|
}
|
|
76
490
|
},
|
|
77
491
|
({ host }) => ({
|
|
@@ -96,7 +510,7 @@ Brief: ${brief}
|
|
|
96
510
|
{
|
|
97
511
|
title: "Deploy this project to Roxyon",
|
|
98
512
|
description: "Walk through deploying the current project to Roxyon infrastructure.",
|
|
99
|
-
argsSchema: { dir:
|
|
513
|
+
argsSchema: { dir: z3.string().describe("Absolute path to the project.") }
|
|
100
514
|
},
|
|
101
515
|
({ dir }) => ({
|
|
102
516
|
messages: [
|
|
@@ -118,10 +532,10 @@ Brief: ${brief}
|
|
|
118
532
|
}
|
|
119
533
|
|
|
120
534
|
// src/resources.ts
|
|
121
|
-
import { readFile } from "fs/promises";
|
|
122
|
-
import { dirname, join } from "path";
|
|
535
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
536
|
+
import { dirname, join as join2 } from "path";
|
|
123
537
|
import { fileURLToPath } from "url";
|
|
124
|
-
var RESOURCE_DIR =
|
|
538
|
+
var RESOURCE_DIR = join2(dirname(fileURLToPath(import.meta.url)), "..", "resources");
|
|
125
539
|
var DOCS = [
|
|
126
540
|
{
|
|
127
541
|
uri: "roxyon://docs/lumenjs",
|
|
@@ -166,7 +580,7 @@ function registerResources(server) {
|
|
|
166
580
|
doc.uri,
|
|
167
581
|
{ title: doc.title, description: doc.description, mimeType: "text/markdown" },
|
|
168
582
|
async () => {
|
|
169
|
-
const body = await
|
|
583
|
+
const body = await readFile2(join2(RESOURCE_DIR, doc.file), "utf8");
|
|
170
584
|
return { contents: [{ uri: doc.uri, mimeType: "text/markdown", text: body }] };
|
|
171
585
|
}
|
|
172
586
|
);
|
|
@@ -176,7 +590,7 @@ function registerResources(server) {
|
|
|
176
590
|
// src/tools.ts
|
|
177
591
|
import { mkdtemp } from "fs/promises";
|
|
178
592
|
import { tmpdir } from "os";
|
|
179
|
-
import { join as
|
|
593
|
+
import { join as join3 } from "path";
|
|
180
594
|
import { formatEnv } from "@roxyon/api-client";
|
|
181
595
|
import {
|
|
182
596
|
buildIgnore,
|
|
@@ -190,85 +604,7 @@ import {
|
|
|
190
604
|
packFiles,
|
|
191
605
|
saveProjectConfig
|
|
192
606
|
} from "@roxyon/deploy-core";
|
|
193
|
-
import { z as
|
|
194
|
-
|
|
195
|
-
// src/context.ts
|
|
196
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
197
|
-
var storage = new AsyncLocalStorage();
|
|
198
|
-
function runWithContext(ctx, fn) {
|
|
199
|
-
return storage.run(ctx, fn);
|
|
200
|
-
}
|
|
201
|
-
function currentContext() {
|
|
202
|
-
return storage.getStore() ?? {};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// src/session.ts
|
|
206
|
-
import { Roxyon, isAuthError } from "@roxyon/api-client";
|
|
207
|
-
import { endpointsFromEnv, loadCredentials, tokenFromEnv } from "@roxyon/deploy-core";
|
|
208
|
-
var NotAuthenticatedError = class extends Error {
|
|
209
|
-
constructor() {
|
|
210
|
-
super(
|
|
211
|
-
"Not signed in to Roxyon. Run `roxyon login` in a terminal, or set ROXYON_TOKEN in this MCP server's environment."
|
|
212
|
-
);
|
|
213
|
-
this.name = "NotAuthenticatedError";
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
async function getSession() {
|
|
217
|
-
const { apiUrl, consoleUrl } = endpointsFromEnv();
|
|
218
|
-
const requestToken = currentContext().token;
|
|
219
|
-
if (requestToken) {
|
|
220
|
-
return {
|
|
221
|
-
roxyon: new Roxyon({ sessionToken: requestToken, baseUrl: apiUrl, consoleUrl }),
|
|
222
|
-
source: "oauth"
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
const envToken = tokenFromEnv();
|
|
226
|
-
if (envToken) {
|
|
227
|
-
return {
|
|
228
|
-
roxyon: new Roxyon({ sessionToken: envToken, baseUrl: apiUrl, consoleUrl }),
|
|
229
|
-
source: "env-token"
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
const creds = await loadCredentials();
|
|
233
|
-
if (!creds?.sessionToken) throw new NotAuthenticatedError();
|
|
234
|
-
return {
|
|
235
|
-
roxyon: new Roxyon({ sessionToken: creds.sessionToken, baseUrl: apiUrl, consoleUrl }),
|
|
236
|
-
preferredSubscription: creds.subscription,
|
|
237
|
-
source: "stored-login"
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
function isSessionGone(err) {
|
|
241
|
-
return err instanceof NotAuthenticatedError || isAuthError(err);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// src/result.ts
|
|
245
|
-
function text(body, structured) {
|
|
246
|
-
return {
|
|
247
|
-
content: [{ type: "text", text: body }],
|
|
248
|
-
...structured ? { structuredContent: structured } : {}
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
function errorResult(message, structured) {
|
|
252
|
-
return {
|
|
253
|
-
content: [{ type: "text", text: message }],
|
|
254
|
-
...structured ? { structuredContent: structured } : {},
|
|
255
|
-
isError: true
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
async function guard(fn) {
|
|
259
|
-
try {
|
|
260
|
-
return await fn();
|
|
261
|
-
} catch (err) {
|
|
262
|
-
if (isSessionGone(err)) {
|
|
263
|
-
return errorResult(
|
|
264
|
-
err.message || "Roxyon session expired \u2014 run `roxyon login` again, or refresh ROXYON_TOKEN."
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
return errorResult(err instanceof Error ? err.message : String(err));
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// src/tools.ts
|
|
607
|
+
import { z as z4 } from "zod";
|
|
272
608
|
var MAX_FILES = 60;
|
|
273
609
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
274
610
|
var MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
@@ -366,11 +702,11 @@ function registerTools(server) {
|
|
|
366
702
|
title: "Roxyon: initialise a project",
|
|
367
703
|
description: "Detect a project's type and write roxyon.json. Run this once per project before roxyon_deploy. Does not create anything on the platform.",
|
|
368
704
|
inputSchema: {
|
|
369
|
-
dir:
|
|
370
|
-
runtime:
|
|
371
|
-
host:
|
|
372
|
-
folder:
|
|
373
|
-
overwrite:
|
|
705
|
+
dir: z4.string().describe("Absolute path to the project directory."),
|
|
706
|
+
runtime: z4.enum(["lumen", "node", "python", "php"]).optional(),
|
|
707
|
+
host: z4.string().optional().describe("Deploy host (a domain on the subscription)."),
|
|
708
|
+
folder: z4.string().optional().describe('Sub-path under the host; "" = site root.'),
|
|
709
|
+
overwrite: z4.boolean().optional().describe("Replace an existing roxyon.json.")
|
|
374
710
|
}
|
|
375
711
|
},
|
|
376
712
|
(args) => guard(async () => {
|
|
@@ -421,10 +757,10 @@ ${JSON.stringify(config, null, 2)}`,
|
|
|
421
757
|
title: "Roxyon: deploy a project",
|
|
422
758
|
description: 'Build (if configured) and deploy the project in "dir" to Roxyon. Without confirm:true this returns a dry-run plan (what would build, where it would go, how many files). With confirm:true it runs the build, uploads, and \u2014 for app runtimes \u2014 waits for it to go live. Requires roxyon.json (run roxyon_init first).',
|
|
423
759
|
inputSchema: {
|
|
424
|
-
dir:
|
|
425
|
-
confirm:
|
|
426
|
-
build:
|
|
427
|
-
follow:
|
|
760
|
+
dir: z4.string().describe("Absolute path to the project directory (must contain roxyon.json)."),
|
|
761
|
+
confirm: z4.boolean().optional().describe("Actually deploy. Omit for a dry-run plan."),
|
|
762
|
+
build: z4.boolean().optional().describe("Run the configured build command (default true)."),
|
|
763
|
+
follow: z4.boolean().optional().describe("For app runtimes, wait until running/failed (default true).")
|
|
428
764
|
}
|
|
429
765
|
},
|
|
430
766
|
(args) => guard(async () => {
|
|
@@ -505,8 +841,8 @@ ${JSON.stringify(config, null, 2)}`,
|
|
|
505
841
|
title: "Roxyon: application status",
|
|
506
842
|
description: "Current status, revisions and last error for an application (by id or project dir).",
|
|
507
843
|
inputSchema: {
|
|
508
|
-
application:
|
|
509
|
-
dir:
|
|
844
|
+
application: z4.string().optional(),
|
|
845
|
+
dir: z4.string().optional().describe("A project directory whose roxyon.json links an application.")
|
|
510
846
|
}
|
|
511
847
|
},
|
|
512
848
|
(args) => guard(async () => {
|
|
@@ -540,9 +876,9 @@ ${app.lastError}` : ""
|
|
|
540
876
|
title: "Roxyon: application logs",
|
|
541
877
|
description: "Recent journal lines for an application (by id or project dir).",
|
|
542
878
|
inputSchema: {
|
|
543
|
-
application:
|
|
544
|
-
dir:
|
|
545
|
-
lines:
|
|
879
|
+
application: z4.string().optional(),
|
|
880
|
+
dir: z4.string().optional(),
|
|
881
|
+
lines: z4.number().int().min(1).max(1e3).optional().describe("How many lines (default 100).")
|
|
546
882
|
}
|
|
547
883
|
},
|
|
548
884
|
(args) => guard(async () => {
|
|
@@ -558,9 +894,9 @@ ${app.lastError}` : ""
|
|
|
558
894
|
title: "Roxyon: restart an application",
|
|
559
895
|
description: "Bounce the application process (no rebuild). Needs confirm:true.",
|
|
560
896
|
inputSchema: {
|
|
561
|
-
application:
|
|
562
|
-
dir:
|
|
563
|
-
confirm:
|
|
897
|
+
application: z4.string().optional(),
|
|
898
|
+
dir: z4.string().optional(),
|
|
899
|
+
confirm: z4.boolean().optional()
|
|
564
900
|
}
|
|
565
901
|
},
|
|
566
902
|
(args) => guard(async () => {
|
|
@@ -577,7 +913,7 @@ ${app.lastError}` : ""
|
|
|
577
913
|
{
|
|
578
914
|
title: "Roxyon: read environment variables",
|
|
579
915
|
description: "An application's environment variables as KEY=value lines.",
|
|
580
|
-
inputSchema: { application:
|
|
916
|
+
inputSchema: { application: z4.string().optional(), dir: z4.string().optional() }
|
|
581
917
|
},
|
|
582
918
|
(args) => guard(async () => {
|
|
583
919
|
const session = await getSession();
|
|
@@ -592,11 +928,11 @@ ${app.lastError}` : ""
|
|
|
592
928
|
title: "Roxyon: set environment variables",
|
|
593
929
|
description: "Merge variables into an application's environment and bump its revision. Run roxyon_deploy afterwards to apply. Needs confirm:true. PORT and HOST are platform-managed and ignored.",
|
|
594
930
|
inputSchema: {
|
|
595
|
-
application:
|
|
596
|
-
dir:
|
|
597
|
-
vars:
|
|
598
|
-
remove:
|
|
599
|
-
confirm:
|
|
931
|
+
application: z4.string().optional(),
|
|
932
|
+
dir: z4.string().optional(),
|
|
933
|
+
vars: z4.record(z4.string()).describe('{ KEY: "value", ... } to set.'),
|
|
934
|
+
remove: z4.array(z4.string()).optional().describe("Keys to delete."),
|
|
935
|
+
confirm: z4.boolean().optional()
|
|
600
936
|
}
|
|
601
937
|
},
|
|
602
938
|
(args) => guard(async () => {
|
|
@@ -631,11 +967,11 @@ ${app.lastError}` : ""
|
|
|
631
967
|
title: "Roxyon: connect a git remote",
|
|
632
968
|
description: "Connect a git repository to an application for push-to-deploy. Returns the deploy key and webhook URL to add to the repo. Needs confirm:true.",
|
|
633
969
|
inputSchema: {
|
|
634
|
-
application:
|
|
635
|
-
dir:
|
|
636
|
-
repoUrl:
|
|
637
|
-
branch:
|
|
638
|
-
confirm:
|
|
970
|
+
application: z4.string().optional(),
|
|
971
|
+
dir: z4.string().optional(),
|
|
972
|
+
repoUrl: z4.string().describe("SSH git URL, e.g. git@github.com:you/app.git"),
|
|
973
|
+
branch: z4.string().optional().describe("Branch to deploy (default main)."),
|
|
974
|
+
confirm: z4.boolean().optional()
|
|
639
975
|
}
|
|
640
976
|
},
|
|
641
977
|
(args) => guard(async () => {
|
|
@@ -677,13 +1013,13 @@ ${res.hint}` : ""
|
|
|
677
1013
|
title: "Roxyon: add a subdomain / host",
|
|
678
1014
|
description: "Provision a host \u2014 DNS + web server + automatic HTTPS. The host must be a subdomain of a domain the account already hosts (or a *.roxyon.com subdomain). Returns while it is still coming up; TLS follows a minute or two later. Needs confirm:true.",
|
|
679
1015
|
inputSchema: {
|
|
680
|
-
host:
|
|
681
|
-
subscription:
|
|
682
|
-
spa:
|
|
683
|
-
phpVersion:
|
|
1016
|
+
host: z4.string().describe("The full hostname, e.g. promo.mycompany.com"),
|
|
1017
|
+
subscription: z4.string().optional().describe("Which subscription to attach it to (omit if the account has one)."),
|
|
1018
|
+
spa: z4.boolean().optional().describe("Single-page app: unmatched paths serve /index.html."),
|
|
1019
|
+
phpVersion: z4.string().optional().describe(
|
|
684
1020
|
'PHP version for the vhost, e.g. "8.3" (needed to run PHP apps like WordPress).'
|
|
685
1021
|
),
|
|
686
|
-
confirm:
|
|
1022
|
+
confirm: z4.boolean().optional()
|
|
687
1023
|
}
|
|
688
1024
|
},
|
|
689
1025
|
(args) => guard(async () => {
|
|
@@ -722,18 +1058,18 @@ ${res.hint}` : ""
|
|
|
722
1058
|
`${(MAX_TOTAL_BYTES / 1024 / 1024).toFixed(1)} MB total (base64-encode binaries).`
|
|
723
1059
|
].join(" "),
|
|
724
1060
|
inputSchema: {
|
|
725
|
-
host:
|
|
726
|
-
folder:
|
|
727
|
-
files:
|
|
728
|
-
|
|
729
|
-
path:
|
|
730
|
-
content:
|
|
731
|
-
encoding:
|
|
1061
|
+
host: z4.string().describe("Target host, e.g. promo.mycompany.com"),
|
|
1062
|
+
folder: z4.string().optional().describe('Sub-path under the host; "" = site root.'),
|
|
1063
|
+
files: z4.array(
|
|
1064
|
+
z4.object({
|
|
1065
|
+
path: z4.string().describe("Relative path, e.g. index.html, about/index.html"),
|
|
1066
|
+
content: z4.string(),
|
|
1067
|
+
encoding: z4.enum(["utf8", "base64"]).optional()
|
|
732
1068
|
})
|
|
733
1069
|
).min(1),
|
|
734
|
-
clean:
|
|
735
|
-
spa:
|
|
736
|
-
confirm:
|
|
1070
|
+
clean: z4.boolean().optional().describe("Replace the whole docroot (keeps .well-known)."),
|
|
1071
|
+
spa: z4.boolean().optional().describe("Also flip the host to SPA routing."),
|
|
1072
|
+
confirm: z4.boolean().optional()
|
|
737
1073
|
}
|
|
738
1074
|
},
|
|
739
1075
|
(args) => guard(async () => {
|
|
@@ -790,9 +1126,9 @@ ${res.hint}` : ""
|
|
|
790
1126
|
title: "Roxyon: list a site\u2019s files",
|
|
791
1127
|
description: "The files currently deployed to a host's document root.",
|
|
792
1128
|
inputSchema: {
|
|
793
|
-
host:
|
|
794
|
-
folder:
|
|
795
|
-
path:
|
|
1129
|
+
host: z4.string(),
|
|
1130
|
+
folder: z4.string().optional(),
|
|
1131
|
+
path: z4.string().optional().describe("List under this sub-path only.")
|
|
796
1132
|
}
|
|
797
1133
|
},
|
|
798
1134
|
(args) => guard(async () => {
|
|
@@ -814,9 +1150,9 @@ ${res.hint}` : ""
|
|
|
814
1150
|
title: "Roxyon: read a site file",
|
|
815
1151
|
description: "Read one file from a host's document root (to review or edit it).",
|
|
816
1152
|
inputSchema: {
|
|
817
|
-
host:
|
|
818
|
-
folder:
|
|
819
|
-
path:
|
|
1153
|
+
host: z4.string(),
|
|
1154
|
+
folder: z4.string().optional(),
|
|
1155
|
+
path: z4.string().describe("Relative path, e.g. index.html")
|
|
820
1156
|
}
|
|
821
1157
|
},
|
|
822
1158
|
(args) => guard(async () => {
|
|
@@ -839,10 +1175,10 @@ ${f.content.slice(0, 4096)}`,
|
|
|
839
1175
|
title: "Roxyon: create a database",
|
|
840
1176
|
description: `Provision a MySQL/MariaDB database + user. The name is prefixed with the account's own username (globally unique across the shared cluster) \u2014 pass just the suffix, e.g. "wordpress". The password is shown once in the result; there is no way to retrieve it later. Needs confirm:true.`,
|
|
841
1177
|
inputSchema: {
|
|
842
|
-
name:
|
|
843
|
-
subscription:
|
|
844
|
-
password:
|
|
845
|
-
confirm:
|
|
1178
|
+
name: z4.string().describe("DB name suffix: lowercase letters, digits, underscore."),
|
|
1179
|
+
subscription: z4.string().optional(),
|
|
1180
|
+
password: z4.string().optional().describe("Omit to have Roxyon generate one."),
|
|
1181
|
+
confirm: z4.boolean().optional()
|
|
846
1182
|
}
|
|
847
1183
|
},
|
|
848
1184
|
(args) => guard(async () => {
|
|
@@ -877,7 +1213,7 @@ ${r.password}` : "",
|
|
|
877
1213
|
{
|
|
878
1214
|
title: "Roxyon: list databases",
|
|
879
1215
|
description: "Databases on the active subscription.",
|
|
880
|
-
inputSchema: { subscription:
|
|
1216
|
+
inputSchema: { subscription: z4.string().optional() }
|
|
881
1217
|
},
|
|
882
1218
|
(args) => guard(async () => {
|
|
883
1219
|
const session = await getSession();
|
|
@@ -893,7 +1229,7 @@ ${r.password}` : "",
|
|
|
893
1229
|
{
|
|
894
1230
|
title: "Roxyon: delete a database",
|
|
895
1231
|
description: "Delete a database by its objectId (from roxyon_database_create/roxyon_list_databases). Needs confirm:true.",
|
|
896
|
-
inputSchema: { database:
|
|
1232
|
+
inputSchema: { database: z4.string(), confirm: z4.boolean().optional() }
|
|
897
1233
|
},
|
|
898
1234
|
(args) => guard(async () => {
|
|
899
1235
|
const session = await getSession();
|
|
@@ -912,13 +1248,13 @@ ${r.password}` : "",
|
|
|
912
1248
|
title: "Roxyon: create a mailbox",
|
|
913
1249
|
description: "Provision a mailbox on a host already on the account. The password is shown once in the result; there is no way to retrieve it later. Needs confirm:true.",
|
|
914
1250
|
inputSchema: {
|
|
915
|
-
localPart:
|
|
916
|
-
domain:
|
|
917
|
-
password:
|
|
918
|
-
quota:
|
|
919
|
-
forwardTo:
|
|
920
|
-
saveCopy:
|
|
921
|
-
confirm:
|
|
1251
|
+
localPart: z4.string().describe('The part before @, e.g. "info".'),
|
|
1252
|
+
domain: z4.string().describe("A hostname already on the account."),
|
|
1253
|
+
password: z4.string().optional().describe("Omit to have Roxyon generate one."),
|
|
1254
|
+
quota: z4.number().int().positive().optional().describe("Quota in MB (default 1024)."),
|
|
1255
|
+
forwardTo: z4.string().optional(),
|
|
1256
|
+
saveCopy: z4.boolean().optional(),
|
|
1257
|
+
confirm: z4.boolean().optional()
|
|
922
1258
|
}
|
|
923
1259
|
},
|
|
924
1260
|
(args) => guard(async () => {
|
|
@@ -949,7 +1285,7 @@ ${r.password}` : ""
|
|
|
949
1285
|
{
|
|
950
1286
|
title: "Roxyon: list mailboxes",
|
|
951
1287
|
description: "Mailboxes on the active subscription.",
|
|
952
|
-
inputSchema: { subscription:
|
|
1288
|
+
inputSchema: { subscription: z4.string().optional() }
|
|
953
1289
|
},
|
|
954
1290
|
(args) => guard(async () => {
|
|
955
1291
|
const session = await getSession();
|
|
@@ -965,7 +1301,7 @@ ${r.password}` : ""
|
|
|
965
1301
|
{
|
|
966
1302
|
title: "Roxyon: delete a mailbox",
|
|
967
1303
|
description: "Delete a mailbox by its objectId (from roxyon_email_create/roxyon_list_emails). Needs confirm:true.",
|
|
968
|
-
inputSchema: { email:
|
|
1304
|
+
inputSchema: { email: z4.string(), confirm: z4.boolean().optional() }
|
|
969
1305
|
},
|
|
970
1306
|
(args) => guard(async () => {
|
|
971
1307
|
const session = await getSession();
|
|
@@ -982,9 +1318,9 @@ ${r.password}` : ""
|
|
|
982
1318
|
title: "Roxyon: reset the SSH/SFTP password",
|
|
983
1319
|
description: "Reset a subscription's shell password \u2014 used for SSH, SFTP, and the file manager. Changing it signs out anything using the old one. The password is shown once in the result; the platform never stores or re-shows it. Needs confirm:true.",
|
|
984
1320
|
inputSchema: {
|
|
985
|
-
subscription:
|
|
986
|
-
password:
|
|
987
|
-
confirm:
|
|
1321
|
+
subscription: z4.string().optional(),
|
|
1322
|
+
password: z4.string().optional().describe("Omit to have Roxyon generate one (12+ chars, mixed case, number, symbol)."),
|
|
1323
|
+
confirm: z4.boolean().optional()
|
|
988
1324
|
}
|
|
989
1325
|
},
|
|
990
1326
|
(args) => guard(async () => {
|
|
@@ -1016,14 +1352,14 @@ ${r.password}`,
|
|
|
1016
1352
|
"this from the CLI-backed MCP, not the hosted mcp.roxyon.com connector. Needs confirm:true."
|
|
1017
1353
|
].join(" "),
|
|
1018
1354
|
inputSchema: {
|
|
1019
|
-
host:
|
|
1020
|
-
folder:
|
|
1021
|
-
subscription:
|
|
1022
|
-
dbName:
|
|
1355
|
+
host: z4.string().describe("The site host, e.g. blog.mycompany.com \u2014 created if it does not exist."),
|
|
1356
|
+
folder: z4.string().optional().describe('Sub-path under the host; "" = site root (recommended).'),
|
|
1357
|
+
subscription: z4.string().optional(),
|
|
1358
|
+
dbName: z4.string().optional().describe(
|
|
1023
1359
|
"Database name suffix (default derived from the host). See roxyon_database_create."
|
|
1024
1360
|
),
|
|
1025
|
-
phpVersion:
|
|
1026
|
-
confirm:
|
|
1361
|
+
phpVersion: z4.string().optional().describe('Default "8.3".'),
|
|
1362
|
+
confirm: z4.boolean().optional()
|
|
1027
1363
|
}
|
|
1028
1364
|
},
|
|
1029
1365
|
(args) => guard(async () => {
|
|
@@ -1059,7 +1395,7 @@ ${r.password}`,
|
|
|
1059
1395
|
if (db.status === "failed" || !db.password || !db.host) {
|
|
1060
1396
|
return errorResult(`Could not provision the database: ${db.error ?? "unknown error"}`);
|
|
1061
1397
|
}
|
|
1062
|
-
const scratchDir = await mkdtemp(
|
|
1398
|
+
const scratchDir = await mkdtemp(join3(tmpdir(), "roxyon-wp-"));
|
|
1063
1399
|
await installWordPress({
|
|
1064
1400
|
dir: scratchDir,
|
|
1065
1401
|
db: { name: db.name, username: db.username, password: db.password, host: db.host }
|
|
@@ -1095,6 +1431,8 @@ function createServer() {
|
|
|
1095
1431
|
registerResources(server);
|
|
1096
1432
|
registerPrompts(server);
|
|
1097
1433
|
registerTools(server);
|
|
1434
|
+
registerBaasTools(server);
|
|
1435
|
+
registerCommerceTools(server);
|
|
1098
1436
|
return server;
|
|
1099
1437
|
}
|
|
1100
1438
|
|
|
@@ -1104,4 +1442,4 @@ export {
|
|
|
1104
1442
|
SERVER_VERSION,
|
|
1105
1443
|
createServer
|
|
1106
1444
|
};
|
|
1107
|
-
//# sourceMappingURL=chunk-
|
|
1445
|
+
//# sourceMappingURL=chunk-KU3LB5HH.js.map
|