impreza-mcp 0.6.0 → 0.7.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/README.md CHANGED
@@ -39,7 +39,7 @@ ticket away from being linked to a legal name.
39
39
 
40
40
  ## Status
41
41
 
42
- **Full surface live.** All 97 tools shipped — app deployment plus account +
42
+ **Full surface live.** All 102 tools shipped — app deployment plus account +
43
43
  crypto balance, catalog + ordering, domains/DNS + registration, invoices, VPS
44
44
  lifecycle with snapshots and backups, dedicated / bare-metal servers, plan
45
45
  upgrades, and Titan / Google Workspace mailboxes — with a setup wizard that
@@ -48,6 +48,30 @@ generates ready-to-paste config snippets for 5 AI tools.
48
48
  The local (`npx`) server and the hosted OAuth connector expose the **same 97
49
49
  tools**, so nothing is lost by picking either path.
50
50
 
51
+ ### New in 0.6.1
52
+
53
+ **`tools/list` now reflects what your account actually owns.** About a third of
54
+ the tools only make sense if you have the machine behind them — VPS power
55
+ controls with no VPS can only ever answer "not found" — so those are left out
56
+ of the listing until you own one. Typical accounts see around 70 tools instead
57
+ of 97, which is roughly six thousand fewer tokens of context spent before you
58
+ ask anything.
59
+
60
+ Three things worth knowing about how it behaves:
61
+
62
+ - **The purchase path is never filtered.** An account that owns nothing is the
63
+ one that needs to buy something, so ordering, top-up, invoices and the
64
+ catalogue are always listed.
65
+ - **Buying something grows the list mid-session.** The server sends
66
+ `notifications/tools/list_changed` on the same response as the order, so a
67
+ client that honours it picks up the new tools without reconnecting.
68
+ - **It fails open.** If this server cannot reach the API to ask, it lists
69
+ everything rather than guess.
70
+
71
+ Hiding a tool is not an authorization boundary — the API still refuses anything
72
+ your account does not own. This only stops the listing from carrying tools that
73
+ could never work for you.
74
+
51
75
  ### New in 0.6.0
52
76
 
53
77
  Four things that only make sense on a host built for anonymity:
package/dist/server.js CHANGED
@@ -214,6 +214,68 @@ const TOOLS = [
214
214
  additionalProperties: false,
215
215
  },
216
216
  },
217
+ {
218
+ name: 'impreza_backup_app',
219
+ description: "Back up a running deployment's data into the customer's OWN Impreza S3 bucket. Packs the app's data directory and uploads it in chunks with a per-chunk SHA-256 manifest written alongside it, so the backup stays verifiable with the customer's own S3 credentials and without us. Returns immediately with a backup_id — a large app takes minutes, so poll impreza_list_backups for the outcome. Needs an active S3 service on the account; without one it answers NO_STORAGE. One job at a time per app.",
220
+ inputSchema: {
221
+ type: 'object',
222
+ properties: {
223
+ deployment_id: { type: 'string', description: 'The dpl_... id to back up.' },
224
+ },
225
+ required: ['deployment_id'],
226
+ additionalProperties: false,
227
+ },
228
+ },
229
+ {
230
+ name: 'impreza_list_backups',
231
+ description: "List this account's app backups and restores, newest first — id, kind, status, chunk count, size, and any error. This is how you find out whether a job started with impreza_backup_app or impreza_restore_app has finished. A `replaced_path` on a restore means the data it displaced is STILL on the server using disk until impreza_discard_replaced removes it. Pass deployment_id to narrow to one app. Not the same as impreza_vps_list_backups, which is Proxmox snapshots of a whole VPS.",
232
+ inputSchema: {
233
+ type: 'object',
234
+ properties: {
235
+ deployment_id: { type: 'string', description: 'Optional — only this app\'s jobs.' },
236
+ },
237
+ additionalProperties: false,
238
+ },
239
+ },
240
+ {
241
+ name: 'impreza_restore_app',
242
+ description: "Restore a completed backup over its app's current data, then restart the app. Every chunk's SHA-256 is checked BEFORE anything is touched, and the data being replaced is moved aside rather than deleted — so restoring the wrong backup is recoverable. That saved copy keeps using disk until impreza_discard_replaced removes it. Returns a job id; poll impreza_list_backups. This overwrites the app's current state: confirm which backup the customer means before calling.",
243
+ inputSchema: {
244
+ type: 'object',
245
+ properties: {
246
+ backup_id: { type: 'string', description: 'The completed backup to restore, from impreza_list_backups.' },
247
+ },
248
+ required: ['backup_id'],
249
+ additionalProperties: false,
250
+ },
251
+ },
252
+ {
253
+ name: 'impreza_discard_replaced',
254
+ description: "Permanently delete the copy of the old data that a restore set aside, freeing that disk on the server. Pass the RESTORE job's id — the one whose replaced_path is set. Irreversible, and that copy is the customer's way back from a restore they may not have wanted, so only call it once they confirm the restored app is the one they want.",
255
+ inputSchema: {
256
+ type: 'object',
257
+ properties: {
258
+ backup_id: { type: 'string', description: 'The restore job that set data aside.' },
259
+ },
260
+ required: ['backup_id'],
261
+ additionalProperties: false,
262
+ },
263
+ },
264
+ {
265
+ name: 'impreza_backup_schedule',
266
+ description: "Change how often an app is backed up automatically and how many copies are kept. Every running app is already on a schedule by DEFAULT — daily, keeping 3 — even with nothing configured, so this changes an existing policy rather than creating one; to read the current setting, call impreza_list_backups with that deployment_id. Set enabled=false to stop automatic backups for the app (manual ones still work). Old copies are removed only after a NEWER one has completed and been verified, and only automatic copies are ever pruned — a backup taken by hand stays until someone deletes it.",
267
+ inputSchema: {
268
+ type: 'object',
269
+ properties: {
270
+ deployment_id: { type: 'string', description: 'The dpl_... id whose schedule changes.' },
271
+ enabled: { type: 'boolean', description: 'Whether automatic backups run at all. Default true.' },
272
+ frequency: { type: 'string', enum: ['daily', 'weekly'], description: 'How often. Default daily.' },
273
+ keep: { type: 'integer', description: 'How many automatic copies to keep, 1-30. Default 3.' },
274
+ },
275
+ required: ['deployment_id'],
276
+ additionalProperties: false,
277
+ },
278
+ },
217
279
  {
218
280
  name: 'impreza_restart_deployment',
219
281
  description: 'Restart a deployment\'s docker-compose stack (non-destructive). The container is stopped + started; data volumes preserved. Status flips to `installing` briefly then back to `running`. Works for both catalog and custom deployments.',
@@ -1616,6 +1678,11 @@ const TOOL_ANNOTATIONS = {
1616
1678
  // since HEAD moves under it.
1617
1679
  impreza_redeploy_deployment: A_WRITE_EXT,
1618
1680
  impreza_restart_deployment: A_WRITE_IDEM,
1681
+ impreza_backup_app: A_WRITE_EXT,
1682
+ impreza_list_backups: A_READ,
1683
+ impreza_restore_app: A_DESTRUCTIVE,
1684
+ impreza_discard_replaced: A_DESTRUCTIVE,
1685
+ impreza_backup_schedule: A_WRITE_IDEM,
1619
1686
  impreza_change_domain: A_WRITE_EXT_IDEM,
1620
1687
  impreza_add_onion: A_WRITE_EXT_IDEM,
1621
1688
  // Both touch the hook on GitHub; disconnect documents itself as idempotent
@@ -1871,9 +1938,45 @@ const ANNOTATED_TOOLS = TOOLS.map((tool) => {
1871
1938
  throw new Error(`Tool ${tool.name} has no entry in TOOL_ANNOTATIONS`);
1872
1939
  return { ...tool, annotations };
1873
1940
  });
1941
+ /**
1942
+ * Which tools this account should not be shown.
1943
+ *
1944
+ * The hosted server filters `tools/list` by what the account owns — about 42
1945
+ * of the tools only make sense if you have the product behind them, and an
1946
+ * account with one VPS was carrying twenty-five that could only ever answer
1947
+ * "not found".
1948
+ *
1949
+ * This process has no database, so it asks for the ANSWER rather than the
1950
+ * rules. A copy of the prefix-to-family map here would drift the first time a
1951
+ * family is added server-side, and would need an npm release to catch up.
1952
+ *
1953
+ * Fails open in every direction — a request that errors, times out, or comes
1954
+ * back malformed hides nothing. Too many tools is the behaviour we shipped
1955
+ * yesterday; too few silently removes capability the customer pays for, and
1956
+ * they would have no way to tell why.
1957
+ */
1958
+ async function hiddenTools() {
1959
+ try {
1960
+ const res = await impreza.get('/v1/entitlements');
1961
+ if (res?.known !== true || !Array.isArray(res.hidden)) {
1962
+ return new Set();
1963
+ }
1964
+ return new Set(res.hidden.filter((n) => typeof n === 'string'));
1965
+ }
1966
+ catch {
1967
+ return new Set();
1968
+ }
1969
+ }
1874
1970
  server.setRequestHandler(ListToolsRequestSchema, async () => {
1875
1971
  const offer = wantsUi();
1876
- const tools = ANNOTATED_TOOLS.map((tool) => {
1972
+ // Fetched per call rather than cached for the process: a stdio session
1973
+ // outlives a purchase, and a customer who buys a VPS mid-session should see
1974
+ // its tools on the next listing rather than after a restart. tools/list is
1975
+ // called rarely enough that the extra round trip does not matter.
1976
+ const hidden = await hiddenTools();
1977
+ const tools = ANNOTATED_TOOLS
1978
+ .filter((tool) => !hidden.has(tool.name))
1979
+ .map((tool) => {
1877
1980
  const uri = offer ? panelFor(tool.name) : undefined;
1878
1981
  // The nested `ui.resourceUri`, not the flat `_meta["ui/resourceUri"]`:
1879
1982
  // the spec deprecated the flat key and removes it before GA.
@@ -1940,6 +2043,39 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1940
2043
  body.since_seconds = args.since_seconds;
1941
2044
  return toResult(await impreza.post(`/v1/platform/deployments/${encodeURIComponent(dep)}/logs`, body));
1942
2045
  }
2046
+ case 'impreza_backup_app': {
2047
+ const dep = String(args.deployment_id ?? '');
2048
+ if (!dep)
2049
+ return toError('deployment_id is required');
2050
+ return toResult(await impreza.post('/v1/backups', { deployment_id: dep }));
2051
+ }
2052
+ case 'impreza_backup_schedule': {
2053
+ const dep = String(args.deployment_id ?? '');
2054
+ if (!dep)
2055
+ return toError('deployment_id is required');
2056
+ const body = {};
2057
+ for (const k of ['enabled', 'frequency', 'keep']) {
2058
+ if (k in args)
2059
+ body[k] = args[k];
2060
+ }
2061
+ return toResult(await impreza.post(`/v1/backups/schedule/${encodeURIComponent(dep)}`, body));
2062
+ }
2063
+ case 'impreza_list_backups': {
2064
+ const dep = String(args.deployment_id ?? '');
2065
+ return toResult(await impreza.get(`/v1/backups${dep ? `?deployment_id=${encodeURIComponent(dep)}` : ''}`));
2066
+ }
2067
+ case 'impreza_restore_app': {
2068
+ const bid = String(args.backup_id ?? '');
2069
+ if (!bid)
2070
+ return toError('backup_id is required');
2071
+ return toResult(await impreza.post(`/v1/backups/${encodeURIComponent(bid)}/restore`, {}));
2072
+ }
2073
+ case 'impreza_discard_replaced': {
2074
+ const bid = String(args.backup_id ?? '');
2075
+ if (!bid)
2076
+ return toError('backup_id is required');
2077
+ return toResult(await impreza.post(`/v1/backups/${encodeURIComponent(bid)}/discard-previous`, {}));
2078
+ }
1943
2079
  case 'impreza_restart_deployment': {
1944
2080
  const dep = String(args.deployment_id ?? '');
1945
2081
  if (!dep)