@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/prompts.ts
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: z.string().describe("What the app should do."),
17
- dir: z.string().optional().describe("Where to create it.")
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: z.string().describe("What the site should be."),
45
- host: z.string().describe("Where to host it, e.g. promo.mycompany.com")
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: z.string().describe("Where to install it, e.g. blog.mycompany.com")
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: z.string().describe("Absolute path to the project.") }
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 = join(dirname(fileURLToPath(import.meta.url)), "..", "resources");
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 readFile(join(RESOURCE_DIR, doc.file), "utf8");
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 join2 } from "path";
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 z2 } from "zod";
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: z2.string().describe("Absolute path to the project directory."),
370
- runtime: z2.enum(["lumen", "node", "python", "php"]).optional(),
371
- host: z2.string().optional().describe("Deploy host (a domain on the subscription)."),
372
- folder: z2.string().optional().describe('Sub-path under the host; "" = site root.'),
373
- overwrite: z2.boolean().optional().describe("Replace an existing roxyon.json.")
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: z2.string().describe("Absolute path to the project directory (must contain roxyon.json)."),
425
- confirm: z2.boolean().optional().describe("Actually deploy. Omit for a dry-run plan."),
426
- build: z2.boolean().optional().describe("Run the configured build command (default true)."),
427
- follow: z2.boolean().optional().describe("For app runtimes, wait until running/failed (default true).")
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: z2.string().optional(),
509
- dir: z2.string().optional().describe("A project directory whose roxyon.json links an application.")
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: z2.string().optional(),
544
- dir: z2.string().optional(),
545
- lines: z2.number().int().min(1).max(1e3).optional().describe("How many lines (default 100).")
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: z2.string().optional(),
562
- dir: z2.string().optional(),
563
- confirm: z2.boolean().optional()
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: z2.string().optional(), dir: z2.string().optional() }
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: z2.string().optional(),
596
- dir: z2.string().optional(),
597
- vars: z2.record(z2.string()).describe('{ KEY: "value", ... } to set.'),
598
- remove: z2.array(z2.string()).optional().describe("Keys to delete."),
599
- confirm: z2.boolean().optional()
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: z2.string().optional(),
635
- dir: z2.string().optional(),
636
- repoUrl: z2.string().describe("SSH git URL, e.g. git@github.com:you/app.git"),
637
- branch: z2.string().optional().describe("Branch to deploy (default main)."),
638
- confirm: z2.boolean().optional()
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: z2.string().describe("The full hostname, e.g. promo.mycompany.com"),
681
- subscription: z2.string().optional().describe("Which subscription to attach it to (omit if the account has one)."),
682
- spa: z2.boolean().optional().describe("Single-page app: unmatched paths serve /index.html."),
683
- phpVersion: z2.string().optional().describe(
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: z2.boolean().optional()
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: z2.string().describe("Target host, e.g. promo.mycompany.com"),
726
- folder: z2.string().optional().describe('Sub-path under the host; "" = site root.'),
727
- files: z2.array(
728
- z2.object({
729
- path: z2.string().describe("Relative path, e.g. index.html, about/index.html"),
730
- content: z2.string(),
731
- encoding: z2.enum(["utf8", "base64"]).optional()
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: z2.boolean().optional().describe("Replace the whole docroot (keeps .well-known)."),
735
- spa: z2.boolean().optional().describe("Also flip the host to SPA routing."),
736
- confirm: z2.boolean().optional()
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: z2.string(),
794
- folder: z2.string().optional(),
795
- path: z2.string().optional().describe("List under this sub-path only.")
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: z2.string(),
818
- folder: z2.string().optional(),
819
- path: z2.string().describe("Relative path, e.g. index.html")
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: z2.string().describe("DB name suffix: lowercase letters, digits, underscore."),
843
- subscription: z2.string().optional(),
844
- password: z2.string().optional().describe("Omit to have Roxyon generate one."),
845
- confirm: z2.boolean().optional()
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: z2.string().optional() }
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: z2.string(), confirm: z2.boolean().optional() }
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: z2.string().describe('The part before @, e.g. "info".'),
916
- domain: z2.string().describe("A hostname already on the account."),
917
- password: z2.string().optional().describe("Omit to have Roxyon generate one."),
918
- quota: z2.number().int().positive().optional().describe("Quota in MB (default 1024)."),
919
- forwardTo: z2.string().optional(),
920
- saveCopy: z2.boolean().optional(),
921
- confirm: z2.boolean().optional()
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: z2.string().optional() }
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: z2.string(), confirm: z2.boolean().optional() }
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: z2.string().optional(),
986
- password: z2.string().optional().describe("Omit to have Roxyon generate one (12+ chars, mixed case, number, symbol)."),
987
- confirm: z2.boolean().optional()
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: z2.string().describe("The site host, e.g. blog.mycompany.com \u2014 created if it does not exist."),
1020
- folder: z2.string().optional().describe('Sub-path under the host; "" = site root (recommended).'),
1021
- subscription: z2.string().optional(),
1022
- dbName: z2.string().optional().describe(
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: z2.string().optional().describe('Default "8.3".'),
1026
- confirm: z2.boolean().optional()
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(join2(tmpdir(), "roxyon-wp-"));
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-HMGCNPGW.js.map
1445
+ //# sourceMappingURL=chunk-KU3LB5HH.js.map