@roxyon/mcp 0.9.0 → 0.10.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,10 +4,7 @@
4
4
  import { createRequire } from "module";
5
5
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
 
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";
7
+ // src/account-tools.ts
11
8
  import { z } from "zod";
12
9
 
13
10
  // src/context.ts
@@ -86,9 +83,109 @@ async function guard(fn) {
86
83
  }
87
84
  }
88
85
 
86
+ // src/account-tools.ts
87
+ var catalogue = [];
88
+ var bridge = null;
89
+ async function loadAccountTools(env, doFetch = globalThis.fetch) {
90
+ const url = (env.ROXYON_ENGAGE_URL ?? "").replace(/\/+$/, "");
91
+ const key = env.ROXYON_CONNECTOR_KEY ?? "";
92
+ if (!url || !key) return 0;
93
+ bridge = { url, key, fetch: doFetch };
94
+ try {
95
+ const res = await doFetch(`${url}/api/connector/tools`, {
96
+ method: "POST",
97
+ headers: { "x-roxyon-connector-key": key },
98
+ signal: AbortSignal.timeout(15e3)
99
+ });
100
+ const body = await res.json();
101
+ if (res.ok && Array.isArray(body.tools))
102
+ catalogue = body.tools.filter((t) => /^[a-z][a-z0-9_]{1,60}$/.test(t.name));
103
+ } catch {
104
+ }
105
+ return catalogue.length;
106
+ }
107
+ function registerAccountTools(server, taken) {
108
+ for (const t of catalogue) {
109
+ const name = `roxyon_${t.name}`;
110
+ if (taken.has(name)) continue;
111
+ taken.add(name);
112
+ server.registerTool(
113
+ name,
114
+ {
115
+ title: `Roxyon: ${t.name.replace(/_/g, " ")}`,
116
+ description: `${t.description}
117
+
118
+ (Acts on the signed-in user's own Roxyon account with their console permissions. Text that says "the client" means this user.)`,
119
+ inputSchema: shapeOf(t.inputSchema),
120
+ annotations: { readOnlyHint: t.readOnly === true, destructiveHint: t.readOnly !== true }
121
+ },
122
+ async (args) => callTool(t.name, args)
123
+ );
124
+ }
125
+ }
126
+ async function callTool(tool, args) {
127
+ const token = currentContext().token;
128
+ if (!bridge || !token)
129
+ return errorResult("This tool works only through the Roxyon connector at mcp.roxyon.com.");
130
+ try {
131
+ const res = await bridge.fetch(`${bridge.url}/api/connector/call`, {
132
+ method: "POST",
133
+ headers: {
134
+ "content-type": "application/json",
135
+ "x-roxyon-connector-key": bridge.key,
136
+ authorization: `Bearer ${token}`
137
+ },
138
+ body: JSON.stringify({ tool, args }),
139
+ signal: AbortSignal.timeout(6e5)
140
+ });
141
+ const body = await res.json().catch(() => null);
142
+ if (!res.ok)
143
+ return errorResult(body?.message ?? body?.error ?? `Roxyon answered ${res.status}`);
144
+ if (!body?.ok) return errorResult(body?.error ?? "That action failed.");
145
+ const result = body.result;
146
+ return text(
147
+ typeof result === "string" ? result : JSON.stringify(result, null, 2),
148
+ result && typeof result === "object" && !Array.isArray(result) ? result : { result }
149
+ );
150
+ } catch (err) {
151
+ return errorResult(
152
+ `Could not reach Roxyon: ${err instanceof Error ? err.message : String(err)}`
153
+ );
154
+ }
155
+ }
156
+ function shapeOf(schema) {
157
+ const props = schema?.properties ?? {};
158
+ const required = new Set(schema?.required ?? []);
159
+ const shape = {};
160
+ for (const [k, v] of Object.entries(props)) {
161
+ const t = zodOf(v);
162
+ shape[k] = required.has(k) ? t : t.optional();
163
+ }
164
+ return shape;
165
+ }
166
+ function zodOf(s) {
167
+ const type = Array.isArray(s.type) ? s.type.find((x) => x !== "null") : s.type;
168
+ let out;
169
+ if (Array.isArray(s.enum) && s.enum.length && s.enum.every((x) => typeof x === "string")) {
170
+ out = z.enum(s.enum);
171
+ } else if (type === "string") out = z.string();
172
+ else if (type === "number") out = z.number();
173
+ else if (type === "integer") out = z.number().int();
174
+ else if (type === "boolean") out = z.boolean();
175
+ else if (type === "array") out = z.array(s.items ? zodOf(s.items) : z.unknown());
176
+ else if (type === "object")
177
+ out = s.properties ? z.object(shapeOf(s)).passthrough() : z.record(z.unknown());
178
+ else out = z.unknown();
179
+ return s.description ? out.describe(s.description) : out;
180
+ }
181
+
89
182
  // src/baas-tools.ts
183
+ import { readFile } from "fs/promises";
184
+ import { join, resolve } from "path";
185
+ import { BAAS_MAX_BATCH, BAAS_MAX_PAGE, BaasDataApi, parseEnv } from "@roxyon/api-client";
186
+ import { z as z2 } from "zod";
90
187
  var MAX_TEXT = 4e4;
91
- var dirArg = z.string().optional().describe("Project directory whose .env holds RX_SECRET_KEY. Defaults to cwd.");
188
+ var dirArg = z2.string().optional().describe("Project directory whose .env holds RX_SECRET_KEY. Defaults to cwd.");
92
189
  function remoteBlock() {
93
190
  if (!currentContext().remote) return null;
94
191
  return errorResult(
@@ -126,13 +223,13 @@ function registerBaasTools(server) {
126
223
  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
224
  inputSchema: {
128
225
  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.")
226
+ class: z2.string(),
227
+ where: z2.record(z2.any()).optional(),
228
+ fields: z2.string().optional(),
229
+ order: z2.string().optional().describe('e.g. "-createdAt"'),
230
+ limit: z2.number().int().min(1).max(BAAS_MAX_PAGE).optional(),
231
+ skip: z2.number().int().min(0).optional(),
232
+ count: z2.boolean().optional().describe("Also return the total number of matches.")
136
233
  }
137
234
  },
138
235
  (args) => guard(async () => {
@@ -157,7 +254,7 @@ ${JSON.stringify(rows, null, 2)}`), { rows, count });
157
254
  {
158
255
  title: "Roxyon BaaS: read one record",
159
256
  description: `Read one record by objectId. ${SERVER_SCOPE}`,
160
- inputSchema: { dir: dirArg, class: z.string(), objectId: z.string() }
257
+ inputSchema: { dir: dirArg, class: z2.string(), objectId: z2.string() }
161
258
  },
162
259
  (args) => guard(async () => {
163
260
  const blocked = remoteBlock();
@@ -174,9 +271,9 @@ ${JSON.stringify(rows, null, 2)}`), { rows, count });
174
271
  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
272
  inputSchema: {
176
273
  dir: dirArg,
177
- class: z.string(),
178
- data: z.record(z.any()),
179
- confirm: z.boolean().optional()
274
+ class: z2.string(),
275
+ data: z2.record(z2.any()),
276
+ confirm: z2.boolean().optional()
180
277
  }
181
278
  },
182
279
  (args) => guard(async () => {
@@ -195,10 +292,10 @@ ${JSON.stringify(rows, null, 2)}`), { rows, count });
195
292
  description: `Set fields on one record by objectId. Needs confirm:true. ${SERVER_SCOPE}`,
196
293
  inputSchema: {
197
294
  dir: dirArg,
198
- class: z.string(),
199
- objectId: z.string(),
200
- data: z.record(z.any()),
201
- confirm: z.boolean().optional()
295
+ class: z2.string(),
296
+ objectId: z2.string(),
297
+ data: z2.record(z2.any()),
298
+ confirm: z2.boolean().optional()
202
299
  }
203
300
  },
204
301
  (args) => guard(async () => {
@@ -219,9 +316,9 @@ ${JSON.stringify(rows, null, 2)}`), { rows, count });
219
316
  description: `Delete one record by objectId. Needs confirm:true. ${SERVER_SCOPE}`,
220
317
  inputSchema: {
221
318
  dir: dirArg,
222
- class: z.string(),
223
- objectId: z.string(),
224
- confirm: z.boolean().optional()
319
+ class: z2.string(),
320
+ objectId: z2.string(),
321
+ confirm: z2.boolean().optional()
225
322
  }
226
323
  },
227
324
  (args) => guard(async () => {
@@ -240,14 +337,14 @@ ${JSON.stringify(rows, null, 2)}`), { rows, count });
240
337
  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
338
  inputSchema: {
242
339
  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()
340
+ requests: z2.array(
341
+ z2.object({
342
+ method: z2.enum(["GET", "POST", "PUT", "DELETE"]),
343
+ path: z2.string(),
344
+ body: z2.record(z2.any()).optional()
248
345
  })
249
346
  ).min(1).max(BAAS_MAX_BATCH),
250
- confirm: z.boolean().optional()
347
+ confirm: z2.boolean().optional()
251
348
  }
252
349
  },
253
350
  (args) => guard(async () => {
@@ -276,7 +373,7 @@ ${JSON.stringify(results, null, 2)}`
276
373
  // src/commerce-tools.ts
277
374
  import { BILLING_CYCLES, Roxyon as Roxyon2 } from "@roxyon/api-client";
278
375
  import { endpointsFromEnv as endpointsFromEnv2 } from "@roxyon/deploy-core";
279
- import { z as z2 } from "zod";
376
+ import { z as z3 } from "zod";
280
377
  function publicRoxyon() {
281
378
  const { apiUrl, consoleUrl } = endpointsFromEnv2();
282
379
  return new Roxyon2({ baseUrl: apiUrl, consoleUrl });
@@ -296,8 +393,8 @@ function registerCommerceTools(server) {
296
393
  title: "Roxyon: product catalog",
297
394
  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
395
  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()
396
+ category: z3.string().optional().describe('Filter by category prefix, e.g. "cloud.vps" or "cloud.hosting".'),
397
+ in_stock_only: z3.boolean().optional()
301
398
  }
302
399
  },
303
400
  (args) => guard(async () => {
@@ -316,10 +413,10 @@ function registerCommerceTools(server) {
316
413
  title: "Roxyon: price a new subscription",
317
414
  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
415
  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".')
416
+ plan: z3.string().describe('planId (or SKU, e.g. "SCALE-S") from roxyon_catalog.'),
417
+ cycle: z3.enum(BILLING_CYCLES),
418
+ currency: z3.string().length(3).optional().describe('ISO 4217, e.g. "USD", "SAR".'),
419
+ country: z3.string().length(2).optional().describe('ISO 3166 alpha-2, decides tax, e.g. "SA".')
323
420
  }
324
421
  },
325
422
  (args) => guard(async () => {
@@ -368,8 +465,8 @@ function registerCommerceTools(server) {
368
465
  title: "Roxyon: my invoices",
369
466
  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
467
  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.")
468
+ status: z3.enum(["pending", "paid", "overdue", "void", "failed", "cancelled"]).optional(),
469
+ limit: z3.number().int().min(1).max(100).optional().describe("Default 25.")
373
470
  }
374
471
  },
375
472
  (args) => guard(async () => {
@@ -390,7 +487,7 @@ function registerCommerceTools(server) {
390
487
  title: "Roxyon: where to pay an invoice",
391
488
  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
489
  inputSchema: {
393
- invoice: z2.string().min(1).max(40).describe("Invoice number or id.")
490
+ invoice: z3.string().min(1).max(40).describe("Invoice number or id.")
394
491
  }
395
492
  },
396
493
  (args) => guard(async () => {
@@ -422,16 +519,16 @@ Pay at ${inv.payUrl} (sign in, open Invoices, choose this invoice).`,
422
519
  title: "Roxyon: place an order (a person pays)",
423
520
  description: 'Order a new web-hosting plan or cloud server for the signed-in user. The platform prices it and creates the subscription and a pending invoice; a PERSON then pays at the returned payUrl (nothing is charged by this tool, and the service is set up only after payment). Always call it first WITHOUT confirm: that dry run returns the exact quote and the token budget and writes nothing. Show it to the user, then call again with confirm:true. Rules: `sku` from roxyon_catalog (out-of-stock plans are refused); servers need `image` (e.g. "ubuntu-24.04"); `username` must be free. Needs a sign-in, or a token created with the "billing:order" scope (`roxyon token create <name> --scopes billing:order --budget <USD> [--skus SCALE-S,\u2026]`): it is refused past that monthly budget (USD, before tax) or outside its SKUs.',
424
521
  inputSchema: {
425
- sku: z2.string().describe('Plan code from roxyon_catalog, e.g. "SCALE-S".'),
426
- cycle: z2.enum(BILLING_CYCLES),
427
- name: z2.string().min(1).max(100).describe("A name for the service."),
428
- username: z2.string().regex(/^[a-z0-9][a-z0-9.-]{1,62}$/).describe("Account/server name: lowercase letters, digits, dots, dashes; must be free."),
429
- image: z2.string().optional().describe('Servers only: OS image, e.g. "ubuntu-24.04".'),
430
- ssh_key: z2.string().optional().describe("Servers only: an SSH public key for root."),
431
- php_version: z2.string().optional().describe('Hosting only, e.g. "8.3".'),
432
- currency: z2.string().length(3).optional(),
433
- country: z2.string().length(2).optional().describe('Decides tax, e.g. "SA".'),
434
- confirm: z2.boolean().optional()
522
+ sku: z3.string().describe('Plan code from roxyon_catalog, e.g. "SCALE-S".'),
523
+ cycle: z3.enum(BILLING_CYCLES),
524
+ name: z3.string().min(1).max(100).describe("A name for the service."),
525
+ username: z3.string().regex(/^[a-z0-9][a-z0-9.-]{1,62}$/).describe("Account/server name: lowercase letters, digits, dots, dashes; must be free."),
526
+ image: z3.string().optional().describe('Servers only: OS image, e.g. "ubuntu-24.04".'),
527
+ ssh_key: z3.string().optional().describe("Servers only: an SSH public key for root."),
528
+ php_version: z3.string().optional().describe('Hosting only, e.g. "8.3".'),
529
+ currency: z3.string().length(3).optional(),
530
+ country: z3.string().length(2).optional().describe('Decides tax, e.g. "SA".'),
531
+ confirm: z3.boolean().optional()
435
532
  }
436
533
  },
437
534
  (args) => guard(async () => {
@@ -475,7 +572,7 @@ Pay at ${inv.payUrl} (sign in, open Invoices, choose this invoice).`,
475
572
  }
476
573
 
477
574
  // src/prompts.ts
478
- import { z as z3 } from "zod";
575
+ import { z as z4 } from "zod";
479
576
  function registerPrompts(server) {
480
577
  server.registerPrompt(
481
578
  "scaffold-lumen-app",
@@ -483,8 +580,8 @@ function registerPrompts(server) {
483
580
  title: "Scaffold a LumenJS + Roxyon app",
484
581
  description: "Guidance for creating a new LumenJS SPA wired to the Roxyon BaaS and ready to deploy.",
485
582
  argsSchema: {
486
- idea: z3.string().describe("What the app should do."),
487
- dir: z3.string().optional().describe("Where to create it.")
583
+ idea: z4.string().describe("What the app should do."),
584
+ dir: z4.string().optional().describe("Where to create it.")
488
585
  }
489
586
  },
490
587
  ({ idea, dir }) => ({
@@ -511,8 +608,8 @@ Steps:
511
608
  title: "Build a site and host it on Roxyon",
512
609
  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.",
513
610
  argsSchema: {
514
- brief: z3.string().describe("What the site should be."),
515
- host: z3.string().describe("Where to host it, e.g. promo.mycompany.com")
611
+ brief: z4.string().describe("What the site should be."),
612
+ host: z4.string().describe("Where to host it, e.g. promo.mycompany.com")
516
613
  }
517
614
  },
518
615
  ({ brief, host }) => ({
@@ -541,7 +638,7 @@ Brief: ${brief}
541
638
  title: "Install WordPress on Roxyon",
542
639
  description: "Provision a host + database and install WordPress core \u2014 local/CLI MCP only.",
543
640
  argsSchema: {
544
- host: z3.string().describe("Where to install it, e.g. blog.mycompany.com")
641
+ host: z4.string().describe("Where to install it, e.g. blog.mycompany.com")
545
642
  }
546
643
  },
547
644
  ({ host }) => ({
@@ -566,7 +663,7 @@ Brief: ${brief}
566
663
  {
567
664
  title: "Deploy this project to Roxyon",
568
665
  description: "Walk through deploying the current project to Roxyon infrastructure.",
569
- argsSchema: { dir: z3.string().describe("Absolute path to the project.") }
666
+ argsSchema: { dir: z4.string().describe("Absolute path to the project.") }
570
667
  },
571
668
  ({ dir }) => ({
572
669
  messages: [
@@ -660,7 +757,7 @@ import {
660
757
  packFiles,
661
758
  saveProjectConfig
662
759
  } from "@roxyon/deploy-core";
663
- import { z as z4 } from "zod";
760
+ import { z as z5 } from "zod";
664
761
  var MAX_FILES = 60;
665
762
  var MAX_FILE_BYTES = 1024 * 1024;
666
763
  var MAX_TOTAL_BYTES = 2 * 1024 * 1024;
@@ -763,11 +860,11 @@ function registerTools(server) {
763
860
  title: "Roxyon: initialise a project",
764
861
  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.",
765
862
  inputSchema: {
766
- dir: z4.string().describe("Absolute path to the project directory."),
767
- runtime: z4.enum(["lumen", "node", "python", "php"]).optional(),
768
- host: z4.string().optional().describe("Deploy host (a domain on the subscription)."),
769
- folder: z4.string().optional().describe('Sub-path under the host; "" = site root.'),
770
- overwrite: z4.boolean().optional().describe("Replace an existing roxyon.json.")
863
+ dir: z5.string().describe("Absolute path to the project directory."),
864
+ runtime: z5.enum(["lumen", "node", "python", "php"]).optional(),
865
+ host: z5.string().optional().describe("Deploy host (a domain on the subscription)."),
866
+ folder: z5.string().optional().describe('Sub-path under the host; "" = site root.'),
867
+ overwrite: z5.boolean().optional().describe("Replace an existing roxyon.json.")
771
868
  }
772
869
  },
773
870
  (args) => guard(async () => {
@@ -818,10 +915,10 @@ ${JSON.stringify(config, null, 2)}`,
818
915
  title: "Roxyon: deploy a project",
819
916
  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).',
820
917
  inputSchema: {
821
- dir: z4.string().describe("Absolute path to the project directory (must contain roxyon.json)."),
822
- confirm: z4.boolean().optional().describe("Actually deploy. Omit for a dry-run plan."),
823
- build: z4.boolean().optional().describe("Run the configured build command (default true)."),
824
- follow: z4.boolean().optional().describe("For app runtimes, wait until running/failed (default true).")
918
+ dir: z5.string().describe("Absolute path to the project directory (must contain roxyon.json)."),
919
+ confirm: z5.boolean().optional().describe("Actually deploy. Omit for a dry-run plan."),
920
+ build: z5.boolean().optional().describe("Run the configured build command (default true)."),
921
+ follow: z5.boolean().optional().describe("For app runtimes, wait until running/failed (default true).")
825
922
  }
826
923
  },
827
924
  (args) => guard(async () => {
@@ -902,8 +999,8 @@ ${JSON.stringify(config, null, 2)}`,
902
999
  title: "Roxyon: application status",
903
1000
  description: "Current status, revisions and last error for an application (by id or project dir).",
904
1001
  inputSchema: {
905
- application: z4.string().optional(),
906
- dir: z4.string().optional().describe("A project directory whose roxyon.json links an application.")
1002
+ application: z5.string().optional(),
1003
+ dir: z5.string().optional().describe("A project directory whose roxyon.json links an application.")
907
1004
  }
908
1005
  },
909
1006
  (args) => guard(async () => {
@@ -937,9 +1034,9 @@ ${app.lastError}` : ""
937
1034
  title: "Roxyon: application logs",
938
1035
  description: "Recent journal lines for an application (by id or project dir).",
939
1036
  inputSchema: {
940
- application: z4.string().optional(),
941
- dir: z4.string().optional(),
942
- lines: z4.number().int().min(1).max(1e3).optional().describe("How many lines (default 100).")
1037
+ application: z5.string().optional(),
1038
+ dir: z5.string().optional(),
1039
+ lines: z5.number().int().min(1).max(1e3).optional().describe("How many lines (default 100).")
943
1040
  }
944
1041
  },
945
1042
  (args) => guard(async () => {
@@ -955,9 +1052,9 @@ ${app.lastError}` : ""
955
1052
  title: "Roxyon: restart an application",
956
1053
  description: "Bounce the application process (no rebuild). Needs confirm:true.",
957
1054
  inputSchema: {
958
- application: z4.string().optional(),
959
- dir: z4.string().optional(),
960
- confirm: z4.boolean().optional()
1055
+ application: z5.string().optional(),
1056
+ dir: z5.string().optional(),
1057
+ confirm: z5.boolean().optional()
961
1058
  }
962
1059
  },
963
1060
  (args) => guard(async () => {
@@ -974,7 +1071,7 @@ ${app.lastError}` : ""
974
1071
  {
975
1072
  title: "Roxyon: read environment variables",
976
1073
  description: "An application's environment variables as KEY=value lines.",
977
- inputSchema: { application: z4.string().optional(), dir: z4.string().optional() }
1074
+ inputSchema: { application: z5.string().optional(), dir: z5.string().optional() }
978
1075
  },
979
1076
  (args) => guard(async () => {
980
1077
  const session = await getSession();
@@ -989,11 +1086,11 @@ ${app.lastError}` : ""
989
1086
  title: "Roxyon: set environment variables",
990
1087
  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.",
991
1088
  inputSchema: {
992
- application: z4.string().optional(),
993
- dir: z4.string().optional(),
994
- vars: z4.record(z4.string()).describe('{ KEY: "value", ... } to set.'),
995
- remove: z4.array(z4.string()).optional().describe("Keys to delete."),
996
- confirm: z4.boolean().optional()
1089
+ application: z5.string().optional(),
1090
+ dir: z5.string().optional(),
1091
+ vars: z5.record(z5.string()).describe('{ KEY: "value", ... } to set.'),
1092
+ remove: z5.array(z5.string()).optional().describe("Keys to delete."),
1093
+ confirm: z5.boolean().optional()
997
1094
  }
998
1095
  },
999
1096
  (args) => guard(async () => {
@@ -1028,11 +1125,11 @@ ${app.lastError}` : ""
1028
1125
  title: "Roxyon: connect a git remote",
1029
1126
  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.",
1030
1127
  inputSchema: {
1031
- application: z4.string().optional(),
1032
- dir: z4.string().optional(),
1033
- repoUrl: z4.string().describe("SSH git URL, e.g. git@github.com:you/app.git"),
1034
- branch: z4.string().optional().describe("Branch to deploy (default main)."),
1035
- confirm: z4.boolean().optional()
1128
+ application: z5.string().optional(),
1129
+ dir: z5.string().optional(),
1130
+ repoUrl: z5.string().describe("SSH git URL, e.g. git@github.com:you/app.git"),
1131
+ branch: z5.string().optional().describe("Branch to deploy (default main)."),
1132
+ confirm: z5.boolean().optional()
1036
1133
  }
1037
1134
  },
1038
1135
  (args) => guard(async () => {
@@ -1074,13 +1171,13 @@ ${res.hint}` : ""
1074
1171
  title: "Roxyon: add a subdomain / host",
1075
1172
  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.",
1076
1173
  inputSchema: {
1077
- host: z4.string().describe("The full hostname, e.g. promo.mycompany.com"),
1078
- subscription: z4.string().optional().describe("Which subscription to attach it to (omit if the account has one)."),
1079
- spa: z4.boolean().optional().describe("Single-page app: unmatched paths serve /index.html."),
1080
- phpVersion: z4.string().optional().describe(
1174
+ host: z5.string().describe("The full hostname, e.g. promo.mycompany.com"),
1175
+ subscription: z5.string().optional().describe("Which subscription to attach it to (omit if the account has one)."),
1176
+ spa: z5.boolean().optional().describe("Single-page app: unmatched paths serve /index.html."),
1177
+ phpVersion: z5.string().optional().describe(
1081
1178
  'PHP version for the vhost, e.g. "8.3" (needed to run PHP apps like WordPress).'
1082
1179
  ),
1083
- confirm: z4.boolean().optional()
1180
+ confirm: z5.boolean().optional()
1084
1181
  }
1085
1182
  },
1086
1183
  (args) => guard(async () => {
@@ -1119,18 +1216,18 @@ ${res.hint}` : ""
1119
1216
  `${(MAX_TOTAL_BYTES / 1024 / 1024).toFixed(1)} MB total (base64-encode binaries).`
1120
1217
  ].join(" "),
1121
1218
  inputSchema: {
1122
- host: z4.string().describe("Target host, e.g. promo.mycompany.com"),
1123
- folder: z4.string().optional().describe('Sub-path under the host; "" = site root.'),
1124
- files: z4.array(
1125
- z4.object({
1126
- path: z4.string().describe("Relative path, e.g. index.html, about/index.html"),
1127
- content: z4.string(),
1128
- encoding: z4.enum(["utf8", "base64"]).optional()
1219
+ host: z5.string().describe("Target host, e.g. promo.mycompany.com"),
1220
+ folder: z5.string().optional().describe('Sub-path under the host; "" = site root.'),
1221
+ files: z5.array(
1222
+ z5.object({
1223
+ path: z5.string().describe("Relative path, e.g. index.html, about/index.html"),
1224
+ content: z5.string(),
1225
+ encoding: z5.enum(["utf8", "base64"]).optional()
1129
1226
  })
1130
1227
  ).min(1),
1131
- clean: z4.boolean().optional().describe("Replace the whole docroot (keeps .well-known)."),
1132
- spa: z4.boolean().optional().describe("Also flip the host to SPA routing."),
1133
- confirm: z4.boolean().optional()
1228
+ clean: z5.boolean().optional().describe("Replace the whole docroot (keeps .well-known)."),
1229
+ spa: z5.boolean().optional().describe("Also flip the host to SPA routing."),
1230
+ confirm: z5.boolean().optional()
1134
1231
  }
1135
1232
  },
1136
1233
  (args) => guard(async () => {
@@ -1187,9 +1284,9 @@ ${res.hint}` : ""
1187
1284
  title: "Roxyon: list a site\u2019s files",
1188
1285
  description: "The files currently deployed to a host's document root.",
1189
1286
  inputSchema: {
1190
- host: z4.string(),
1191
- folder: z4.string().optional(),
1192
- path: z4.string().optional().describe("List under this sub-path only.")
1287
+ host: z5.string(),
1288
+ folder: z5.string().optional(),
1289
+ path: z5.string().optional().describe("List under this sub-path only.")
1193
1290
  }
1194
1291
  },
1195
1292
  (args) => guard(async () => {
@@ -1211,9 +1308,9 @@ ${res.hint}` : ""
1211
1308
  title: "Roxyon: read a site file",
1212
1309
  description: "Read one file from a host's document root (to review or edit it).",
1213
1310
  inputSchema: {
1214
- host: z4.string(),
1215
- folder: z4.string().optional(),
1216
- path: z4.string().describe("Relative path, e.g. index.html")
1311
+ host: z5.string(),
1312
+ folder: z5.string().optional(),
1313
+ path: z5.string().describe("Relative path, e.g. index.html")
1217
1314
  }
1218
1315
  },
1219
1316
  (args) => guard(async () => {
@@ -1236,11 +1333,11 @@ ${f.content.slice(0, 4096)}`,
1236
1333
  title: "Roxyon: create a database (MySQL, PostgreSQL or Redis)",
1237
1334
  description: `Provision a data store on a Roxyon hosting account. engine "mysql" (MySQL/MariaDB, default; localhost:3306), "postgres" (PostgreSQL 15 inside the account, localhost:5432; the first one installs it in a few minutes) or "redis" (Redis inside the account, 127.0.0.1:6379 with a password, 128 MB, saved to disk; one per account; give apps REDIS_URL=redis://:<password>@127.0.0.1:6379). Every hosting account has all three: never tell the user to get a VPS or an outside service for PostgreSQL or Redis. The name is prefixed with the account's own username \u2014 pass just the suffix, e.g. "wordpress" (for Redis any short name, e.g. "cache"). The password is shown once in the result; there is no way to retrieve it later. Needs confirm:true.`,
1238
1335
  inputSchema: {
1239
- name: z4.string().describe("DB name suffix: lowercase letters, digits, underscore."),
1240
- subscription: z4.string().optional(),
1241
- password: z4.string().optional().describe("Omit to have Roxyon generate one."),
1242
- engine: z4.enum(["mysql", "postgres", "redis"]).optional().describe("mysql (default), postgres or redis."),
1243
- confirm: z4.boolean().optional()
1336
+ name: z5.string().describe("DB name suffix: lowercase letters, digits, underscore."),
1337
+ subscription: z5.string().optional(),
1338
+ password: z5.string().optional().describe("Omit to have Roxyon generate one."),
1339
+ engine: z5.enum(["mysql", "postgres", "redis"]).optional().describe("mysql (default), postgres or redis."),
1340
+ confirm: z5.boolean().optional()
1244
1341
  }
1245
1342
  },
1246
1343
  (args) => guard(async () => {
@@ -1284,7 +1381,7 @@ ${r.password}` : "",
1284
1381
  {
1285
1382
  title: "Roxyon: list databases",
1286
1383
  description: "Databases on the active subscription.",
1287
- inputSchema: { subscription: z4.string().optional() }
1384
+ inputSchema: { subscription: z5.string().optional() }
1288
1385
  },
1289
1386
  (args) => guard(async () => {
1290
1387
  const session = await getSession();
@@ -1302,7 +1399,7 @@ ${r.password}` : "",
1302
1399
  {
1303
1400
  title: "Roxyon: delete a database",
1304
1401
  description: "Delete a database by its objectId (from roxyon_database_create/roxyon_list_databases). Needs confirm:true.",
1305
- inputSchema: { database: z4.string(), confirm: z4.boolean().optional() }
1402
+ inputSchema: { database: z5.string(), confirm: z5.boolean().optional() }
1306
1403
  },
1307
1404
  (args) => guard(async () => {
1308
1405
  const session = await getSession();
@@ -1321,13 +1418,13 @@ ${r.password}` : "",
1321
1418
  title: "Roxyon: create a mailbox",
1322
1419
  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.",
1323
1420
  inputSchema: {
1324
- localPart: z4.string().describe('The part before @, e.g. "info".'),
1325
- domain: z4.string().describe("A hostname already on the account."),
1326
- password: z4.string().optional().describe("Omit to have Roxyon generate one."),
1327
- quota: z4.number().int().positive().optional().describe("Quota in MB (default 1024)."),
1328
- forwardTo: z4.string().optional(),
1329
- saveCopy: z4.boolean().optional(),
1330
- confirm: z4.boolean().optional()
1421
+ localPart: z5.string().describe('The part before @, e.g. "info".'),
1422
+ domain: z5.string().describe("A hostname already on the account."),
1423
+ password: z5.string().optional().describe("Omit to have Roxyon generate one."),
1424
+ quota: z5.number().int().positive().optional().describe("Quota in MB (default 1024)."),
1425
+ forwardTo: z5.string().optional(),
1426
+ saveCopy: z5.boolean().optional(),
1427
+ confirm: z5.boolean().optional()
1331
1428
  }
1332
1429
  },
1333
1430
  (args) => guard(async () => {
@@ -1358,7 +1455,7 @@ ${r.password}` : ""
1358
1455
  {
1359
1456
  title: "Roxyon: list mailboxes",
1360
1457
  description: "Mailboxes on the active subscription.",
1361
- inputSchema: { subscription: z4.string().optional() }
1458
+ inputSchema: { subscription: z5.string().optional() }
1362
1459
  },
1363
1460
  (args) => guard(async () => {
1364
1461
  const session = await getSession();
@@ -1374,7 +1471,7 @@ ${r.password}` : ""
1374
1471
  {
1375
1472
  title: "Roxyon: delete a mailbox",
1376
1473
  description: "Delete a mailbox by its objectId (from roxyon_email_create/roxyon_list_emails). Needs confirm:true.",
1377
- inputSchema: { email: z4.string(), confirm: z4.boolean().optional() }
1474
+ inputSchema: { email: z5.string(), confirm: z5.boolean().optional() }
1378
1475
  },
1379
1476
  (args) => guard(async () => {
1380
1477
  const session = await getSession();
@@ -1391,9 +1488,9 @@ ${r.password}` : ""
1391
1488
  title: "Roxyon: reset the SSH/SFTP password",
1392
1489
  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.",
1393
1490
  inputSchema: {
1394
- subscription: z4.string().optional(),
1395
- password: z4.string().optional().describe("Omit to have Roxyon generate one (12+ chars, mixed case, number, symbol)."),
1396
- confirm: z4.boolean().optional()
1491
+ subscription: z5.string().optional(),
1492
+ password: z5.string().optional().describe("Omit to have Roxyon generate one (12+ chars, mixed case, number, symbol)."),
1493
+ confirm: z5.boolean().optional()
1397
1494
  }
1398
1495
  },
1399
1496
  (args) => guard(async () => {
@@ -1425,14 +1522,14 @@ ${r.password}`,
1425
1522
  "this from the CLI-backed MCP, not the hosted mcp.roxyon.com connector. Needs confirm:true."
1426
1523
  ].join(" "),
1427
1524
  inputSchema: {
1428
- host: z4.string().describe("The site host, e.g. blog.mycompany.com \u2014 created if it does not exist."),
1429
- folder: z4.string().optional().describe('Sub-path under the host; "" = site root (recommended).'),
1430
- subscription: z4.string().optional(),
1431
- dbName: z4.string().optional().describe(
1525
+ host: z5.string().describe("The site host, e.g. blog.mycompany.com \u2014 created if it does not exist."),
1526
+ folder: z5.string().optional().describe('Sub-path under the host; "" = site root (recommended).'),
1527
+ subscription: z5.string().optional(),
1528
+ dbName: z5.string().optional().describe(
1432
1529
  "Database name suffix (default derived from the host). See roxyon_database_create."
1433
1530
  ),
1434
- phpVersion: z4.string().optional().describe('Default "8.3".'),
1435
- confirm: z4.boolean().optional()
1531
+ phpVersion: z5.string().optional().describe('Default "8.3".'),
1532
+ confirm: z5.boolean().optional()
1436
1533
  }
1437
1534
  },
1438
1535
  (args) => guard(async () => {
@@ -1498,7 +1595,7 @@ function createServer() {
1498
1595
  const server = new McpServer(
1499
1596
  { name: SERVER_NAME, version: SERVER_VERSION },
1500
1597
  {
1501
- instructions: 'Roxyon lets you build LumenJS SPAs, use the Roxyon BaaS, and deploy apps and web projects to Roxyon infrastructure. Read the roxyon://docs/* resources before writing LumenJS or calling the BaaS. To deploy: roxyon_init once per project, then roxyon_deploy (dry run, then confirm:true). Side-effecting tools require confirm:true. Every Roxyon hosting account includes MySQL, PostgreSQL and Redis (roxyon_database_create with engine "mysql" | "postgres" | "redis"), mailboxes, domains with free SSL, Node/PHP apps, env vars, logs and SSH: never send the user to a VPS or an outside service for any of these. If something the user asks for has no tool here, say exactly which step is missing and that it can be done in the Roxyon Console or by Roxyon AI there (console.roxyon.com/agent); never invent a workaround or claim it is impossible on Roxyon.'
1598
+ instructions: `Roxyon lets you build LumenJS SPAs, use the Roxyon BaaS, and deploy apps and web projects to Roxyon infrastructure. Read the roxyon://docs/* resources before writing LumenJS or calling the BaaS. To deploy: roxyon_init once per project, then roxyon_deploy (dry run, then confirm:true). Side-effecting tools require confirm:true. Every Roxyon hosting account includes MySQL, PostgreSQL and Redis (roxyon_database_create with engine "mysql" | "postgres" | "redis"), mailboxes, domains with free SSL, Node/PHP apps, env vars, logs and SSH: never send the user to a VPS or an outside service for any of these. If something the user asks for has no tool here, say exactly which step is missing and that it can be done in the Roxyon Console or by Roxyon AI there (console.roxyon.com/agent); never invent a workaround or claim it is impossible on Roxyon. Besides the deploy tools, the remote connector offers the same account tools Roxyon AI uses in the console, acting as the signed-in user with their permissions: site files (read, write, move, delete), site_tools (cron jobs, PHP settings, permissions, zip/unzip, search), run_in_site (a shell command in their hosting), DNS records, SSL, mailboxes, forwarding and auto-replies, databases (export, restore, SQL), nightly backups and restores, cloud servers (start/stop/restart, root password, firewall), plans, invoices and payments, Roxyon Base, newsletters, Engage and social posts. Actions that change something need the user's explicit yes first (confirmed: true).`
1502
1599
  }
1503
1600
  );
1504
1601
  registerResources(server);
@@ -1506,13 +1603,20 @@ function createServer() {
1506
1603
  registerTools(server);
1507
1604
  registerBaasTools(server);
1508
1605
  registerCommerceTools(server);
1606
+ const taken = new Set(
1607
+ Object.keys(
1608
+ server._registeredTools
1609
+ )
1610
+ );
1611
+ registerAccountTools(server, taken);
1509
1612
  return server;
1510
1613
  }
1511
1614
 
1512
1615
  export {
1513
1616
  runWithContext,
1617
+ loadAccountTools,
1514
1618
  SERVER_NAME,
1515
1619
  SERVER_VERSION,
1516
1620
  createServer
1517
1621
  };
1518
- //# sourceMappingURL=chunk-UO5XO6D2.js.map
1622
+ //# sourceMappingURL=chunk-BNRJ2C63.js.map