impreza-mcp 0.2.1 → 0.4.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.
package/dist/login.js ADDED
@@ -0,0 +1,130 @@
1
+ // `npx impreza-mcp login --code pair_…` — device-pairing exchange (Tier 1).
2
+ //
3
+ // Trades a single-use pairing code (generated in the Impreza clientarea →
4
+ // "Connect an AI assistant") for a scoped, expiring API key, and stores it
5
+ // locally so the MCP server authenticates without the customer ever copying
6
+ // a secret by hand. The api_secret is NEVER printed.
7
+ import { saveCredentials } from './credentials.js';
8
+ import { VERSION } from './version.js';
9
+ export async function runLogin(argv) {
10
+ const opts = parseFlags(argv);
11
+ if (opts.help) {
12
+ printHelp();
13
+ process.exit(0);
14
+ }
15
+ if (!opts.code) {
16
+ console.error('error: --code <pair_…> is required');
17
+ console.error('');
18
+ printHelp();
19
+ process.exit(2);
20
+ }
21
+ const baseURL = (opts.baseURL ?? 'https://api.imprezahost.com').replace(/\/+$/, '');
22
+ if (!baseURL.startsWith('https://')) {
23
+ // Credentials travel in headers on every later call — refuse a
24
+ // downgraded endpoint, same rule the server enforces on IMPREZA_BASE_URL.
25
+ console.error('error: --base-url must be an https:// URL');
26
+ process.exit(2);
27
+ }
28
+ let data;
29
+ try {
30
+ const res = await fetch(`${baseURL}/v1/mcp/pair`, {
31
+ method: 'POST',
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ Accept: 'application/json',
35
+ 'User-Agent': `impreza-mcp/${VERSION}`,
36
+ },
37
+ body: JSON.stringify({ code: opts.code }),
38
+ });
39
+ const text = await res.text();
40
+ let env;
41
+ try {
42
+ env = JSON.parse(text);
43
+ }
44
+ catch {
45
+ console.error(`error: unexpected response from ${baseURL} (HTTP ${res.status})`);
46
+ process.exit(1);
47
+ }
48
+ if (!res.ok || env.success === false || env.error) {
49
+ console.error(`error: ${env.error?.message ?? `pairing failed (HTTP ${res.status})`}`);
50
+ process.exit(1);
51
+ }
52
+ data = env.data ?? {};
53
+ }
54
+ catch (err) {
55
+ console.error(`error: could not reach ${baseURL} — ${err.message}`);
56
+ process.exit(1);
57
+ }
58
+ const apiKey = data.api_key;
59
+ const apiSecret = data.api_secret;
60
+ if (typeof apiKey !== 'string' || typeof apiSecret !== 'string') {
61
+ console.error('error: server did not return a key + secret');
62
+ process.exit(1);
63
+ }
64
+ const resolvedBase = typeof data.base_url === 'string' ? data.base_url.replace(/\/v1\/?$/, '') : baseURL;
65
+ const path = saveCredentials({ apiKey, apiSecret, baseURL: resolvedBase });
66
+ // Success — never echo the secret.
67
+ const scope = Array.isArray(data.scopes) && data.scopes.length ? data.scopes.join(', ') : '(full)';
68
+ const expiry = typeof data.expires_at === 'string' ? data.expires_at : null;
69
+ console.log('Paired. Credentials stored (owner-readable only) at:');
70
+ console.log(` ${path}`);
71
+ console.log(` scope: ${scope}${expiry ? ` expires: ${expiry}` : ''}`);
72
+ console.log('');
73
+ console.log('Add impreza-mcp to your AI tool — no API key needed, it loads from the stored file:');
74
+ console.log('');
75
+ console.log(JSON.stringify({ mcpServers: { impreza: { command: 'npx', args: ['-y', 'impreza-mcp'] } } }, null, 2));
76
+ console.log('');
77
+ console.log('Then restart your AI tool and ask it to run `impreza_list_servers`.');
78
+ process.exit(0);
79
+ }
80
+ function parseFlags(argv) {
81
+ const out = { help: false };
82
+ for (let i = 0; i < argv.length; i++) {
83
+ const a = argv[i];
84
+ if (a === undefined)
85
+ continue;
86
+ if (a === '--help' || a === '-h') {
87
+ out.help = true;
88
+ }
89
+ else if (a === '--code') {
90
+ const next = argv[i + 1];
91
+ if (next === undefined) {
92
+ console.error('error: --code requires a value');
93
+ process.exit(2);
94
+ }
95
+ out.code = next;
96
+ i++;
97
+ }
98
+ else if (a.startsWith('--code=')) {
99
+ out.code = a.slice('--code='.length);
100
+ }
101
+ else if (a === '--base-url') {
102
+ const next = argv[i + 1];
103
+ if (next === undefined) {
104
+ console.error('error: --base-url requires a value');
105
+ process.exit(2);
106
+ }
107
+ out.baseURL = next;
108
+ i++;
109
+ }
110
+ else if (a.startsWith('--base-url=')) {
111
+ out.baseURL = a.slice('--base-url='.length);
112
+ }
113
+ else {
114
+ console.error(`error: unknown flag "${a}"`);
115
+ process.exit(2);
116
+ }
117
+ }
118
+ return out;
119
+ }
120
+ function printHelp() {
121
+ console.log('Usage: npx impreza-mcp login --code pair_xxxxxxxx');
122
+ console.log('');
123
+ console.log('Generate a pairing code in your Impreza clientarea → "Connect an AI');
124
+ console.log('assistant", then run this command to store a scoped API key locally.');
125
+ console.log('');
126
+ console.log('Flags:');
127
+ console.log(' --code <pair_…> The single-use pairing code (required).');
128
+ console.log(' --base-url <url> Override the API base (default https://api.imprezahost.com).');
129
+ }
130
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../src/login.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -208,6 +208,19 @@ const TOOLS = [
208
208
  additionalProperties: false,
209
209
  },
210
210
  },
211
+ {
212
+ name: 'impreza_redeploy_deployment',
213
+ description: 'Rebuild a CUSTOM deployment in place from its current source — re-pull the image, re-clone the watched git ref at its new HEAD, or rebuild — and swap the container with near-zero downtime. Reuses the same deployment, so the domain, host port, and URL never change. This is the in-place way to ship a new build of a running custom app the customer changed — PREFER it over uninstall + recreate. Optional `vars` are merged into the stored environment before the rebuild (rotate a secret / add a var without a teardown); system vars (DEPLOYMENT_ID, DOMAIN_URL, HOST_PORT, ...) are preserved. The source itself is not changed here — to change the image ref or git URL, recreate under the same name (the *.imprezaapps.com domain is preserved either way). Custom deployments only; returns the deployment flipped to `updating` — poll impreza_list_deployments for running/failed.',
214
+ inputSchema: {
215
+ type: 'object',
216
+ properties: {
217
+ deployment_id: { type: 'string', description: 'The dpl_... id of the custom deployment to rebuild.' },
218
+ vars: { type: 'object', description: 'Optional env vars merged into the deployment before the rebuild. System vars are preserved.' },
219
+ },
220
+ required: ['deployment_id'],
221
+ additionalProperties: false,
222
+ },
223
+ },
211
224
  {
212
225
  name: 'impreza_change_domain',
213
226
  description: 'Re-route a RUNNING deployment to a new clearnet hostname without touching its container or data. The agent regenerates its Caddy fragment + reloads zero-downtime; Let\'s Encrypt issues a fresh cert on the first hit. Use to migrate from an auto-subdomain to a custom domain, or vice-versa, or just to rename. Deployment must be in status=running (Phase 9.19).',
@@ -288,6 +301,239 @@ const TOOLS = [
288
301
  additionalProperties: false,
289
302
  },
290
303
  },
304
+ // ── Account + balance (crypto top-up) ──────────────────────────────
305
+ {
306
+ name: 'impreza_account_info',
307
+ description: 'Get the account profile: name, email, account status, currency, and current account balance (credit). ' +
308
+ 'Read the balance + currency here before calling `impreza_topup`.',
309
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
310
+ },
311
+ {
312
+ name: 'impreza_list_services',
313
+ description: "List the customer's billable services (VPS, hosting, dedicated, domains) — each with its service id, " +
314
+ 'product name, status, billing cycle and next due date. Use this to find the numeric `service_id` for the ' +
315
+ '`impreza_vps_*` tools. Optional `status` filter.',
316
+ inputSchema: {
317
+ type: 'object',
318
+ properties: {
319
+ status: { type: 'string', description: 'Filter by service status (e.g. Active, Suspended, Terminated).' },
320
+ },
321
+ additionalProperties: false,
322
+ },
323
+ },
324
+ {
325
+ name: 'impreza_topup',
326
+ description: 'Create an account-balance top-up invoice payable in crypto (BTC, XMR, USDT-TRC20, TRX) — Impreza is no-KYC ' +
327
+ 'and privacy-first. Returns an `invoice_id`, the amount, and a `payment_url` the customer opens to pay; the ' +
328
+ 'balance auto-credits once the payment confirms. Poll `impreza_topup_status` for the state.',
329
+ inputSchema: {
330
+ type: 'object',
331
+ properties: {
332
+ amount: { type: 'number', description: 'Top-up amount in the account currency (1.00–10000.00).' },
333
+ method: { type: 'string', enum: ['btc', 'xmr', 'trx', 'usdt', 'usdt_trc20'], description: 'Optional preferred crypto rail; the payment page still lets the customer switch.' },
334
+ },
335
+ required: ['amount'],
336
+ additionalProperties: false,
337
+ },
338
+ },
339
+ {
340
+ name: 'impreza_topup_status',
341
+ description: 'Poll a top-up invoice created by `impreza_topup`. Returns the invoice status (pending / paid), the amount, ' +
342
+ 'and the resulting account balance once paid.',
343
+ inputSchema: {
344
+ type: 'object',
345
+ properties: {
346
+ invoice_id: { type: 'number', description: 'The invoice_id returned by impreza_topup.' },
347
+ },
348
+ required: ['invoice_id'],
349
+ additionalProperties: false,
350
+ },
351
+ },
352
+ // ── Domains + DNS ──────────────────────────────────────────────────
353
+ {
354
+ name: 'impreza_domain_check',
355
+ description: 'Check domain availability + price before registering. Pass one domain or several comma-separated. Read-only.',
356
+ inputSchema: {
357
+ type: 'object',
358
+ properties: {
359
+ domain: { type: 'string', description: 'Domain(s) to check, e.g. "example.com" or "a.com,b.net".' },
360
+ },
361
+ required: ['domain'],
362
+ additionalProperties: false,
363
+ },
364
+ },
365
+ {
366
+ name: 'impreza_domain_details',
367
+ description: "Get a registered domain's details: status, registration/expiry dates, nameservers, registrar-lock and " +
368
+ 'ID-protection state. Read-only.',
369
+ inputSchema: {
370
+ type: 'object',
371
+ properties: {
372
+ domain: { type: 'string', description: 'The domain name, e.g. example.com.' },
373
+ },
374
+ required: ['domain'],
375
+ additionalProperties: false,
376
+ },
377
+ },
378
+ {
379
+ name: 'impreza_list_dns',
380
+ description: 'List the DNS records (host, type, value, TTL, priority) for a domain on Impreza-managed DNS. Read-only.',
381
+ inputSchema: {
382
+ type: 'object',
383
+ properties: {
384
+ domain: { type: 'string', description: 'The domain name, e.g. example.com.' },
385
+ },
386
+ required: ['domain'],
387
+ additionalProperties: false,
388
+ },
389
+ },
390
+ {
391
+ name: 'impreza_add_dns_record',
392
+ description: 'Add a DNS record to an Impreza-managed domain. `host` is the record name ("@" for the apex, "www", "mail", …); ' +
393
+ '`value` is the target (IP, hostname, or text).',
394
+ inputSchema: {
395
+ type: 'object',
396
+ properties: {
397
+ domain: { type: 'string', description: 'The domain name, e.g. example.com.' },
398
+ type: { type: 'string', description: 'Record type: A, AAAA, CNAME, MX, TXT, NS, SRV, …' },
399
+ host: { type: 'string', description: 'Record name/host, e.g. "@" for the apex, "www", "mail".' },
400
+ value: { type: 'string', description: 'Record value/target (IP, hostname, or text).' },
401
+ ttl: { type: 'number', description: 'Time-to-live in seconds. Default 14400.' },
402
+ priority: { type: 'number', description: 'Priority (MX / SRV only).' },
403
+ },
404
+ required: ['domain', 'type', 'host', 'value'],
405
+ additionalProperties: false,
406
+ },
407
+ },
408
+ {
409
+ name: 'impreza_update_dns_record',
410
+ description: 'Update an existing DNS record on an Impreza-managed domain. Locate the record by `type` + `host` + `old_value`, ' +
411
+ 'and supply `new_value`.',
412
+ inputSchema: {
413
+ type: 'object',
414
+ properties: {
415
+ domain: { type: 'string', description: 'The domain name, e.g. example.com.' },
416
+ type: { type: 'string', description: 'Record type of the record to change.' },
417
+ host: { type: 'string', description: 'Record name/host of the record to change.' },
418
+ old_value: { type: 'string', description: 'Current value of the record (used to locate it).' },
419
+ new_value: { type: 'string', description: 'New value to set.' },
420
+ ttl: { type: 'number', description: 'New TTL in seconds. Default 14400.' },
421
+ priority: { type: 'number', description: 'New priority (MX / SRV only).' },
422
+ },
423
+ required: ['domain', 'type', 'host', 'old_value', 'new_value'],
424
+ additionalProperties: false,
425
+ },
426
+ },
427
+ {
428
+ name: 'impreza_delete_dns_record',
429
+ description: 'Delete a DNS record from an Impreza-managed domain. Identify it by `type` + `host` + `value`.',
430
+ inputSchema: {
431
+ type: 'object',
432
+ properties: {
433
+ domain: { type: 'string', description: 'The domain name, e.g. example.com.' },
434
+ type: { type: 'string', description: 'Record type to delete.' },
435
+ host: { type: 'string', description: 'Record name/host to delete.' },
436
+ value: { type: 'string', description: 'Value of the record to delete.' },
437
+ },
438
+ required: ['domain', 'type', 'host', 'value'],
439
+ additionalProperties: false,
440
+ },
441
+ },
442
+ {
443
+ name: 'impreza_set_nameservers',
444
+ description: 'Replace the authoritative nameservers for a domain (2–4 hostnames). Use to point a domain at Impreza DNS or ' +
445
+ 'an external provider.',
446
+ inputSchema: {
447
+ type: 'object',
448
+ properties: {
449
+ domain: { type: 'string', description: 'The domain name, e.g. example.com.' },
450
+ nameservers: { type: 'array', items: { type: 'string' }, description: 'Ordered list of nameserver hostnames (2–4).' },
451
+ },
452
+ required: ['domain', 'nameservers'],
453
+ additionalProperties: false,
454
+ },
455
+ },
456
+ // ── VPS lifecycle (Proxmox KVM) ────────────────────────────────────
457
+ {
458
+ name: 'impreza_vps_status',
459
+ description: "Get a Proxmox VPS's live power state + resource usage (CPU, memory, disk, network, uptime). Find the " +
460
+ '`service_id` via `impreza_list_services`. Read-only.',
461
+ inputSchema: {
462
+ type: 'object',
463
+ properties: {
464
+ service_id: { type: 'string', description: 'VPS service id (numeric; from impreza_list_services).' },
465
+ },
466
+ required: ['service_id'],
467
+ additionalProperties: false,
468
+ },
469
+ },
470
+ {
471
+ name: 'impreza_vps_power',
472
+ description: 'Change a Proxmox VPS power state: start, shutdown (graceful ACPI), reboot, or stop (hard power-off). ' +
473
+ 'shutdown/stop take the server offline — confirm with the customer first.',
474
+ inputSchema: {
475
+ type: 'object',
476
+ properties: {
477
+ service_id: { type: 'string', description: 'VPS service id (numeric; from impreza_list_services).' },
478
+ action: { type: 'string', enum: ['start', 'shutdown', 'reboot', 'stop'], description: 'Power action to perform.' },
479
+ },
480
+ required: ['service_id', 'action'],
481
+ additionalProperties: false,
482
+ },
483
+ },
484
+ {
485
+ name: 'impreza_vps_list_backups',
486
+ description: 'List the available backups for a Proxmox VPS (id, timestamp, size). Read-only.',
487
+ inputSchema: {
488
+ type: 'object',
489
+ properties: {
490
+ service_id: { type: 'string', description: 'VPS service id (numeric; from impreza_list_services).' },
491
+ },
492
+ required: ['service_id'],
493
+ additionalProperties: false,
494
+ },
495
+ },
496
+ {
497
+ name: 'impreza_vps_create_backup',
498
+ description: 'Trigger an on-demand backup of a Proxmox VPS. Poll `impreza_vps_list_backups` for completion.',
499
+ inputSchema: {
500
+ type: 'object',
501
+ properties: {
502
+ service_id: { type: 'string', description: 'VPS service id (numeric; from impreza_list_services).' },
503
+ },
504
+ required: ['service_id'],
505
+ additionalProperties: false,
506
+ },
507
+ },
508
+ {
509
+ name: 'impreza_vps_list_templates',
510
+ description: 'List the OS templates available for reinstalling a Proxmox VPS (template_id + label). Use to pick a ' +
511
+ '`template_id` for `impreza_vps_reinstall`. Read-only.',
512
+ inputSchema: {
513
+ type: 'object',
514
+ properties: {
515
+ service_id: { type: 'string', description: 'VPS service id (numeric; from impreza_list_services).' },
516
+ },
517
+ required: ['service_id'],
518
+ additionalProperties: false,
519
+ },
520
+ },
521
+ {
522
+ name: 'impreza_vps_reinstall',
523
+ description: 'DESTRUCTIVE: wipe and reinstall a Proxmox VPS from an OS template — ALL DATA ON THE VPS IS ERASED. Pick a ' +
524
+ '`template_id` via `impreza_vps_list_templates` and set a new root password (min 8 chars). Always confirm with ' +
525
+ 'the customer before calling.',
526
+ inputSchema: {
527
+ type: 'object',
528
+ properties: {
529
+ service_id: { type: 'string', description: 'VPS service id (numeric; from impreza_list_services).' },
530
+ template_id: { type: 'number', description: 'OS template id (from impreza_vps_list_templates).' },
531
+ password: { type: 'string', description: 'New root / administrator password (min 8 chars).' },
532
+ },
533
+ required: ['service_id', 'template_id', 'password'],
534
+ additionalProperties: false,
535
+ },
536
+ },
291
537
  ];
292
538
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
293
539
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
@@ -354,6 +600,16 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
354
600
  return toError('deployment_id is required');
355
601
  return toResult(await impreza.post(`/v1/platform/deployments/${encodeURIComponent(dep)}/restart`, {}));
356
602
  }
603
+ case 'impreza_redeploy_deployment': {
604
+ const dep = String(args.deployment_id ?? '');
605
+ if (!dep)
606
+ return toError('deployment_id is required');
607
+ const body = {};
608
+ if (args.vars && typeof args.vars === 'object') {
609
+ body.vars = args.vars;
610
+ }
611
+ return toResult(await impreza.post(`/v1/platform/deployments/custom/${encodeURIComponent(dep)}/redeploy`, body));
612
+ }
357
613
  case 'impreza_change_domain': {
358
614
  const dep = String(args.deployment_id ?? '');
359
615
  const newDomain = String(args.domain ?? '');
@@ -412,6 +668,155 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
412
668
  body.vars = args.vars;
413
669
  return toResult(await impreza.post('/v1/platform/deployments', body));
414
670
  }
671
+ // ── Account + crypto top-up ──────────────────────────────────────
672
+ case 'impreza_account_info':
673
+ return toResult(await impreza.get('/v1/account'));
674
+ case 'impreza_list_services': {
675
+ const query = {};
676
+ if (typeof args.status === 'string')
677
+ query.status = args.status;
678
+ return toResult(await impreza.get('/v1/account/services', query));
679
+ }
680
+ case 'impreza_topup': {
681
+ const amount = typeof args.amount === 'number' ? args.amount : Number(args.amount);
682
+ if (!Number.isFinite(amount) || amount <= 0) {
683
+ return toError('amount is required (a positive number in the account currency)');
684
+ }
685
+ const body = { amount };
686
+ if (typeof args.method === 'string' && args.method)
687
+ body.method = args.method;
688
+ return toResult(await impreza.post('/v1/account/topup', body));
689
+ }
690
+ case 'impreza_topup_status': {
691
+ const invoiceId = String(args.invoice_id ?? '');
692
+ if (!invoiceId)
693
+ return toError('invoice_id is required');
694
+ return toResult(await impreza.get(`/v1/account/topup/${encodeURIComponent(invoiceId)}`));
695
+ }
696
+ // ── Domains + DNS ────────────────────────────────────────────────
697
+ case 'impreza_domain_check': {
698
+ const domain = String(args.domain ?? '');
699
+ if (!domain)
700
+ return toError('domain is required');
701
+ return toResult(await impreza.get('/v1/domains/check', { domains: domain }));
702
+ }
703
+ case 'impreza_domain_details': {
704
+ const domain = String(args.domain ?? '');
705
+ if (!domain)
706
+ return toError('domain is required');
707
+ return toResult(await impreza.get(`/v1/domains/${encodeURIComponent(domain)}`));
708
+ }
709
+ case 'impreza_list_dns': {
710
+ const domain = String(args.domain ?? '');
711
+ if (!domain)
712
+ return toError('domain is required');
713
+ return toResult(await impreza.get(`/v1/domains/${encodeURIComponent(domain)}/dns`));
714
+ }
715
+ case 'impreza_add_dns_record': {
716
+ const domain = String(args.domain ?? '');
717
+ if (!domain)
718
+ return toError('domain is required');
719
+ const body = {
720
+ type: String(args.type ?? ''),
721
+ host: String(args.host ?? ''),
722
+ value: String(args.value ?? ''),
723
+ };
724
+ if (typeof args.ttl === 'number')
725
+ body.ttl = args.ttl;
726
+ if (typeof args.priority === 'number')
727
+ body.priority = args.priority;
728
+ return toResult(await impreza.post(`/v1/domains/${encodeURIComponent(domain)}/dns`, body));
729
+ }
730
+ case 'impreza_update_dns_record': {
731
+ const domain = String(args.domain ?? '');
732
+ if (!domain)
733
+ return toError('domain is required');
734
+ const body = {
735
+ type: String(args.type ?? ''),
736
+ host: String(args.host ?? ''),
737
+ old_value: String(args.old_value ?? ''),
738
+ new_value: String(args.new_value ?? ''),
739
+ };
740
+ if (typeof args.ttl === 'number')
741
+ body.ttl = args.ttl;
742
+ if (typeof args.priority === 'number')
743
+ body.priority = args.priority;
744
+ return toResult(await impreza.put(`/v1/domains/${encodeURIComponent(domain)}/dns`, body));
745
+ }
746
+ case 'impreza_delete_dns_record': {
747
+ const domain = String(args.domain ?? '');
748
+ if (!domain)
749
+ return toError('domain is required');
750
+ const body = {
751
+ type: String(args.type ?? ''),
752
+ host: String(args.host ?? ''),
753
+ value: String(args.value ?? ''),
754
+ };
755
+ return toResult(await impreza.del(`/v1/domains/${encodeURIComponent(domain)}/dns`, body));
756
+ }
757
+ case 'impreza_set_nameservers': {
758
+ const domain = String(args.domain ?? '');
759
+ if (!domain)
760
+ return toError('domain is required');
761
+ if (!Array.isArray(args.nameservers) || args.nameservers.length === 0) {
762
+ return toError('nameservers is required (a non-empty array of hostnames)');
763
+ }
764
+ return toResult(await impreza.put(`/v1/domains/${encodeURIComponent(domain)}/nameservers`, {
765
+ nameservers: args.nameservers,
766
+ }));
767
+ }
768
+ // ── VPS lifecycle (Proxmox) ──────────────────────────────────────
769
+ case 'impreza_vps_status': {
770
+ const sid = svcId(args);
771
+ if (!sid)
772
+ return toError('service_id is required (numeric)');
773
+ return toResult(await impreza.get(`/v1/vps/proxmox/${sid}/status`));
774
+ }
775
+ case 'impreza_vps_power': {
776
+ const sid = svcId(args);
777
+ if (!sid)
778
+ return toError('service_id is required (numeric)');
779
+ const action = String(args.action ?? '').toLowerCase();
780
+ if (!['start', 'shutdown', 'reboot', 'stop'].includes(action)) {
781
+ return toError('action must be one of: start, shutdown, reboot, stop');
782
+ }
783
+ return toResult(await impreza.post(`/v1/vps/proxmox/${sid}/${action}`, {}));
784
+ }
785
+ case 'impreza_vps_list_backups': {
786
+ const sid = svcId(args);
787
+ if (!sid)
788
+ return toError('service_id is required (numeric)');
789
+ return toResult(await impreza.get(`/v1/vps/proxmox/${sid}/backups`));
790
+ }
791
+ case 'impreza_vps_create_backup': {
792
+ const sid = svcId(args);
793
+ if (!sid)
794
+ return toError('service_id is required (numeric)');
795
+ return toResult(await impreza.post(`/v1/vps/proxmox/${sid}/backups`, {}));
796
+ }
797
+ case 'impreza_vps_list_templates': {
798
+ const sid = svcId(args);
799
+ if (!sid)
800
+ return toError('service_id is required (numeric)');
801
+ return toResult(await impreza.get(`/v1/vps/proxmox/${sid}/templates`));
802
+ }
803
+ case 'impreza_vps_reinstall': {
804
+ const sid = svcId(args);
805
+ if (!sid)
806
+ return toError('service_id is required (numeric)');
807
+ const templateId = typeof args.template_id === 'number' ? args.template_id : Number(args.template_id);
808
+ if (!Number.isInteger(templateId) || templateId <= 0) {
809
+ return toError('template_id is required (a positive integer — use impreza_vps_list_templates)');
810
+ }
811
+ const password = String(args.password ?? '');
812
+ if (password.length < 8)
813
+ return toError('password is required (min 8 chars)');
814
+ return toResult(await impreza.post(`/v1/vps/proxmox/${sid}/reinstall`, {
815
+ template_id: templateId,
816
+ password,
817
+ confirm: true,
818
+ }));
819
+ }
415
820
  default:
416
821
  return toError(`unknown tool: ${name}`);
417
822
  }
@@ -514,6 +919,16 @@ async function deployCustom(args) {
514
919
  // ─────────────────────────────────────────────────────────────────────
515
920
  // Result shape — MCP wants either content[] or isError + content[]
516
921
  // ─────────────────────────────────────────────────────────────────────
922
+ /**
923
+ * Normalize a VPS `service_id` arg (JSON may deliver it as a number or a
924
+ * string) to a numeric string, or '' when it isn't a plain positive integer —
925
+ * so the caller returns a clean error instead of building a bad path.
926
+ */
927
+ function svcId(args) {
928
+ const raw = args.service_id;
929
+ const s = typeof raw === 'number' ? String(raw) : String(raw ?? '').trim();
930
+ return /^\d+$/.test(s) ? s : '';
931
+ }
517
932
  function toResult(payload) {
518
933
  return {
519
934
  content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],