mcp-cohesity 2.0.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/LICENSE +77 -0
- package/README.md +494 -0
- package/dist/cohesity-client.d.ts +47 -0
- package/dist/cohesity-client.js +126 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +149 -0
- package/dist/tools/active-directory.d.ts +11 -0
- package/dist/tools/active-directory.js +82 -0
- package/dist/tools/alerts.d.ts +6 -0
- package/dist/tools/alerts.js +111 -0
- package/dist/tools/antivirus.d.ts +24 -0
- package/dist/tools/antivirus.js +180 -0
- package/dist/tools/audit-logs.d.ts +10 -0
- package/dist/tools/audit-logs.js +161 -0
- package/dist/tools/clones.d.ts +24 -0
- package/dist/tools/clones.js +107 -0
- package/dist/tools/cluster-reports.d.ts +10 -0
- package/dist/tools/cluster-reports.js +224 -0
- package/dist/tools/cluster.d.ts +6 -0
- package/dist/tools/cluster.js +33 -0
- package/dist/tools/external-targets.d.ts +3 -0
- package/dist/tools/external-targets.js +145 -0
- package/dist/tools/kms.d.ts +14 -0
- package/dist/tools/kms.js +164 -0
- package/dist/tools/notifications.d.ts +3 -0
- package/dist/tools/notifications.js +156 -0
- package/dist/tools/protection.d.ts +3 -0
- package/dist/tools/protection.js +514 -0
- package/dist/tools/recovery.d.ts +6 -0
- package/dist/tools/recovery.js +101 -0
- package/dist/tools/reports.d.ts +3 -0
- package/dist/tools/reports.js +346 -0
- package/dist/tools/restore.d.ts +3 -0
- package/dist/tools/restore.js +220 -0
- package/dist/tools/roles.d.ts +11 -0
- package/dist/tools/roles.js +95 -0
- package/dist/tools/run-actions.d.ts +17 -0
- package/dist/tools/run-actions.js +190 -0
- package/dist/tools/runs.d.ts +6 -0
- package/dist/tools/runs.js +94 -0
- package/dist/tools/source-registration.d.ts +11 -0
- package/dist/tools/source-registration.js +456 -0
- package/dist/tools/sources.d.ts +3 -0
- package/dist/tools/sources.js +161 -0
- package/dist/tools/stats.d.ts +3 -0
- package/dist/tools/stats.js +164 -0
- package/dist/tools/storage.d.ts +3 -0
- package/dist/tools/storage.js +191 -0
- package/dist/tools/tiering.d.ts +3 -0
- package/dist/tools/tiering.js +132 -0
- package/dist/tools/users.d.ts +13 -0
- package/dist/tools/users.js +203 -0
- package/package.json +57 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
function toolResult(text, isError = false) {
|
|
3
|
+
return { content: [{ type: "text", text }], isError };
|
|
4
|
+
}
|
|
5
|
+
export function registerRestoreTools(server, client) {
|
|
6
|
+
// ── Recover VM ───────────────────────────────────────────────────────
|
|
7
|
+
server.tool("recover_vm", "Recover a VMware VM from a Cohesity snapshot. By default restores to the original location (overwrite). Use list_snapshots to find the snapshot ID. For in-place recovery, only snapshot_id and name are required.", {
|
|
8
|
+
name: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe("Name for this recovery task"),
|
|
11
|
+
snapshot_id: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe("Snapshot ID to recover from (obtained from list_snapshots)"),
|
|
14
|
+
recover_to_new_source: z
|
|
15
|
+
.boolean()
|
|
16
|
+
.optional()
|
|
17
|
+
.default(false)
|
|
18
|
+
.describe("If true, recover to a different vCenter/datastore instead of original location"),
|
|
19
|
+
power_on_vms: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.optional()
|
|
22
|
+
.default(true)
|
|
23
|
+
.describe("Power on the VM after recovery"),
|
|
24
|
+
restore_network: z
|
|
25
|
+
.boolean()
|
|
26
|
+
.optional()
|
|
27
|
+
.default(true)
|
|
28
|
+
.describe("Restore network settings on the recovered VM"),
|
|
29
|
+
prefix: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Prefix to add to the recovered VM name (useful to avoid conflicts)"),
|
|
33
|
+
suffix: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Suffix to add to the recovered VM name"),
|
|
37
|
+
}, async ({ name, snapshot_id, recover_to_new_source, power_on_vms, restore_network, prefix, suffix }) => {
|
|
38
|
+
try {
|
|
39
|
+
const recoveryTargetConfig = {
|
|
40
|
+
recoverToNewSource: recover_to_new_source,
|
|
41
|
+
};
|
|
42
|
+
if (!recover_to_new_source && (prefix || suffix)) {
|
|
43
|
+
recoveryTargetConfig.originalSourceConfig = {
|
|
44
|
+
...(prefix ? { prefix } : {}),
|
|
45
|
+
...(suffix ? { suffix } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const body = {
|
|
49
|
+
name,
|
|
50
|
+
snapshotEnvironment: "kVMware",
|
|
51
|
+
vmwareParams: {
|
|
52
|
+
objects: [{ snapshotId: snapshot_id }],
|
|
53
|
+
recoveryAction: "RecoverVMs",
|
|
54
|
+
recoverVmParams: {
|
|
55
|
+
targetEnvironment: "kVMware",
|
|
56
|
+
powerOnVms: power_on_vms,
|
|
57
|
+
restoreNetwork: restore_network,
|
|
58
|
+
vmwareTargetParams: {
|
|
59
|
+
recoveryTargetConfig,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
const result = await client.postV2("data-protect/recoveries", body);
|
|
65
|
+
return toolResult(JSON.stringify(result, null, 2));
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return toolResult(`Error recovering VM: ${error}`, true);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// ── Search Files in Snapshots ───────────────────────────────────────
|
|
72
|
+
server.tool("search_files", "Search for files and folders across Cohesity snapshots by name. Returns matching files with their paths, source VM, and snapshot info. NOTE: Cohesity prefixes VMware file paths with a volume label (e.g. 'lvol_2/home/user/file') — pass these paths as-is to recover_files, which will automatically strip the prefix when restoring to the original location.", {
|
|
73
|
+
search_string: z
|
|
74
|
+
.string()
|
|
75
|
+
.describe("File or folder name to search for (supports partial matches)"),
|
|
76
|
+
source_environments: z
|
|
77
|
+
.array(z.enum(["kVMware", "kPhysical", "kGenericNas", "kIsilon", "kNetapp", "kFlashBlade"]))
|
|
78
|
+
.optional()
|
|
79
|
+
.default(["kVMware"])
|
|
80
|
+
.describe("Source environment types to search within"),
|
|
81
|
+
object_ids: z
|
|
82
|
+
.array(z.number())
|
|
83
|
+
.optional()
|
|
84
|
+
.describe("Limit search to specific object (VM) IDs"),
|
|
85
|
+
max_results: z
|
|
86
|
+
.number()
|
|
87
|
+
.optional()
|
|
88
|
+
.default(25)
|
|
89
|
+
.describe("Maximum number of results to return"),
|
|
90
|
+
}, async ({ search_string, source_environments, object_ids, max_results }) => {
|
|
91
|
+
try {
|
|
92
|
+
const body = {
|
|
93
|
+
objectType: "Files",
|
|
94
|
+
searchString: search_string,
|
|
95
|
+
count: max_results,
|
|
96
|
+
fileParams: {
|
|
97
|
+
sourceEnvironments: source_environments,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
if (object_ids) {
|
|
101
|
+
body.fileParams.objectIds = object_ids;
|
|
102
|
+
}
|
|
103
|
+
const result = await client.postV2("data-protect/search/indexed-objects", body);
|
|
104
|
+
return toolResult(JSON.stringify(result, null, 2));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return toolResult(`Error searching files: ${error}`, true);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
// ── Recover Files ────────────────────────────────────────────────────
|
|
111
|
+
server.tool("recover_files", "Recover specific files or folders from a VMware VM snapshot back to the original VM. Use list_snapshots to get a snapshot ID and search_files to find file paths. Pass file paths exactly as returned by search_files (e.g. 'lvol_2/home/user/file') — the volume prefix is automatically stripped when restoring to original path. The recover_method 'UseExistingAgent' requires a Cohesity agent installed on the VM; 'UseHypervisorApis' uses VMware Tools (requires VM credentials).", {
|
|
112
|
+
name: z
|
|
113
|
+
.string()
|
|
114
|
+
.describe("Name for this recovery task"),
|
|
115
|
+
snapshot_id: z
|
|
116
|
+
.string()
|
|
117
|
+
.describe("Snapshot ID to recover files from (obtained from list_snapshots)"),
|
|
118
|
+
file_paths: z
|
|
119
|
+
.array(z.string())
|
|
120
|
+
.describe("Absolute paths of files or folders to recover (e.g. ['/etc/hosts', '/var/log'])"),
|
|
121
|
+
recover_to_original_path: z
|
|
122
|
+
.boolean()
|
|
123
|
+
.optional()
|
|
124
|
+
.default(true)
|
|
125
|
+
.describe("If true, restore to original path. If false, alternate_path must be provided."),
|
|
126
|
+
alternate_path: z
|
|
127
|
+
.string()
|
|
128
|
+
.optional()
|
|
129
|
+
.describe("Alternate path to restore files to (used when recover_to_original_path is false)"),
|
|
130
|
+
recover_method: z
|
|
131
|
+
.enum(["UseExistingAgent", "UseHypervisorApis", "AutoDeploy"])
|
|
132
|
+
.optional()
|
|
133
|
+
.default("UseExistingAgent")
|
|
134
|
+
.describe("Method to deliver files to the VM. UseExistingAgent requires Cohesity agent. UseHypervisorApis requires VMware Tools + credentials."),
|
|
135
|
+
overwrite_existing: z
|
|
136
|
+
.boolean()
|
|
137
|
+
.optional()
|
|
138
|
+
.default(true)
|
|
139
|
+
.describe("Overwrite existing files at the destination"),
|
|
140
|
+
preserve_attributes: z
|
|
141
|
+
.boolean()
|
|
142
|
+
.optional()
|
|
143
|
+
.default(true)
|
|
144
|
+
.describe("Preserve original file timestamps and permissions"),
|
|
145
|
+
vm_username: z
|
|
146
|
+
.string()
|
|
147
|
+
.optional()
|
|
148
|
+
.describe("VM guest OS username (required for UseHypervisorApis method)"),
|
|
149
|
+
vm_password: z
|
|
150
|
+
.string()
|
|
151
|
+
.optional()
|
|
152
|
+
.describe("VM guest OS password (required for UseHypervisorApis method)"),
|
|
153
|
+
}, async ({ name, snapshot_id, file_paths, recover_to_original_path, alternate_path, recover_method, overwrite_existing, preserve_attributes, vm_username, vm_password, }) => {
|
|
154
|
+
try {
|
|
155
|
+
// Cohesity indexes VMware files with a volume prefix (e.g. "lvol_2/home/user/file").
|
|
156
|
+
// When restoring to original path, recoverToOriginalPath:true fails for LVM volumes
|
|
157
|
+
// because Cohesity strips the prefix and can't resolve which physical volume to write to.
|
|
158
|
+
// Workaround: keep lvol_N/ prefix in paths, use recoverToOriginalPath:false, and set
|
|
159
|
+
// alternatePath to the real parent directory (e.g. /home/zerto). This bypasses the
|
|
160
|
+
// volume mapping logic while placing the file in its original location.
|
|
161
|
+
let resolvedPaths = file_paths;
|
|
162
|
+
let resolvedOriginalPath = recover_to_original_path;
|
|
163
|
+
let resolvedAlternatePath = alternate_path;
|
|
164
|
+
if (recover_to_original_path) {
|
|
165
|
+
// Derive the common parent directory (stripped of lvol prefix) as the alternate path.
|
|
166
|
+
// For a single file "lvol_2/home/zerto/file.sh" → alternatePath = "/home/zerto"
|
|
167
|
+
// For a directory "lvol_2/home/zerto" → alternatePath = "/home/zerto"
|
|
168
|
+
const parents = file_paths.map((p) => {
|
|
169
|
+
const stripped = p.replace(/^lvol_\d+\//, "/").replace(/\/+/g, "/");
|
|
170
|
+
// If it looks like a file (has extension or no trailing slash), use its parent dir
|
|
171
|
+
const lastSlash = stripped.lastIndexOf("/");
|
|
172
|
+
return lastSlash > 0 ? stripped.substring(0, lastSlash) : "/";
|
|
173
|
+
});
|
|
174
|
+
// Use the shortest common parent so all files land in the right place
|
|
175
|
+
resolvedAlternatePath = parents.reduce((a, b) => (a.length <= b.length ? a : b));
|
|
176
|
+
resolvedOriginalPath = false; // use alternate_path workaround
|
|
177
|
+
// Keep lvol_N/ prefix in paths — Cohesity needs it to locate the volume
|
|
178
|
+
resolvedPaths = file_paths;
|
|
179
|
+
}
|
|
180
|
+
const originalTargetConfig = {
|
|
181
|
+
recoverMethod: recover_method,
|
|
182
|
+
recoverToOriginalPath: resolvedOriginalPath,
|
|
183
|
+
};
|
|
184
|
+
if (!resolvedOriginalPath && resolvedAlternatePath) {
|
|
185
|
+
originalTargetConfig.alternatePath = resolvedAlternatePath;
|
|
186
|
+
}
|
|
187
|
+
if (vm_username && vm_password) {
|
|
188
|
+
originalTargetConfig.targetVmCredentials = {
|
|
189
|
+
username: vm_username,
|
|
190
|
+
password: vm_password,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const body = {
|
|
194
|
+
name,
|
|
195
|
+
snapshotEnvironment: "kVMware",
|
|
196
|
+
vmwareParams: {
|
|
197
|
+
objects: [{ snapshotId: snapshot_id }],
|
|
198
|
+
recoveryAction: "RecoverFiles",
|
|
199
|
+
recoverFileAndFolderParams: {
|
|
200
|
+
filesAndFolders: resolvedPaths.map((p) => ({ absolutePath: p })),
|
|
201
|
+
targetEnvironment: "kVMware",
|
|
202
|
+
vmwareTargetParams: {
|
|
203
|
+
recoverToOriginalTarget: true,
|
|
204
|
+
overwriteExisting: overwrite_existing,
|
|
205
|
+
preserveAttributes: preserve_attributes,
|
|
206
|
+
originalTargetConfig,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
const result = await client.postV2("data-protect/recoveries", body);
|
|
212
|
+
return toolResult(JSON.stringify(result, null, 2));
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
return toolResult(`Error recovering files: ${error}`, true);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
// cancel_protection_run lives in tools/run-actions.ts (spec-driven version);
|
|
219
|
+
// intentionally not re-registered here.
|
|
220
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role management tools — list, create, update, and delete Cohesity roles.
|
|
3
|
+
*
|
|
4
|
+
* GET /v2/roles — Roles
|
|
5
|
+
* POST /v2/roles — CreateRoleParameters { name, privileges[], description? }
|
|
6
|
+
* PUT /v2/roles/{name} — UpdateRoleParameters { privileges[], description? }
|
|
7
|
+
* DELETE /v2/roles/{name} — 204 No Content
|
|
8
|
+
*/
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { CohesityClient } from "../cohesity-client.js";
|
|
11
|
+
export declare function registerRoleTools(server: McpServer, client: CohesityClient): void;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role management tools — list, create, update, and delete Cohesity roles.
|
|
3
|
+
*
|
|
4
|
+
* GET /v2/roles — Roles
|
|
5
|
+
* POST /v2/roles — CreateRoleParameters { name, privileges[], description? }
|
|
6
|
+
* PUT /v2/roles/{name} — UpdateRoleParameters { privileges[], description? }
|
|
7
|
+
* DELETE /v2/roles/{name} — 204 No Content
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
const reply = (text, isError = false) => ({
|
|
11
|
+
content: [{ type: "text", text }],
|
|
12
|
+
isError,
|
|
13
|
+
});
|
|
14
|
+
export function registerRoleTools(server, client) {
|
|
15
|
+
// ── List Roles ─────────────────────────────────────────────────────────
|
|
16
|
+
server.tool("list_roles", "List Cohesity roles (both built-in and user-created). Each role bundles a set of privileges.", {
|
|
17
|
+
names: z.array(z.string()).optional().describe("Restrict to roles with these names"),
|
|
18
|
+
tenant_ids: z.array(z.string()).optional().describe("Restrict to these tenant IDs"),
|
|
19
|
+
include_tenants: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Include roles from nested tenants the caller can see"),
|
|
23
|
+
}, async (args) => {
|
|
24
|
+
try {
|
|
25
|
+
const qp = {};
|
|
26
|
+
if (args.names?.length)
|
|
27
|
+
qp.names = args.names.join(",");
|
|
28
|
+
if (args.tenant_ids?.length)
|
|
29
|
+
qp.tenantIds = args.tenant_ids.join(",");
|
|
30
|
+
if (args.include_tenants !== undefined)
|
|
31
|
+
qp.includeTenants = String(args.include_tenants);
|
|
32
|
+
const data = await client.getV2("roles", qp);
|
|
33
|
+
return reply(JSON.stringify(data, null, 2));
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return reply(`Error listing roles: ${err}`, true);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// ── Create Role ────────────────────────────────────────────────────────
|
|
40
|
+
// CreateRoleParameters = { name (required) } + UpdateRoleParameters
|
|
41
|
+
// UpdateRoleParameters requires `privileges` (min 1 item).
|
|
42
|
+
server.tool("create_role", "Create a custom Cohesity role with a specific set of privileges. Privilege names are the same strings shown in the cluster's Role configuration UI (e.g., PROTECTION_VIEW, PROTECTION_MODIFY, CLUSTER_VIEW).", {
|
|
43
|
+
name: z.string().describe("Role name (must be unique on the cluster)"),
|
|
44
|
+
privileges: z
|
|
45
|
+
.array(z.string())
|
|
46
|
+
.min(1)
|
|
47
|
+
.describe("List of privilege strings to grant"),
|
|
48
|
+
description: z.string().optional().describe("Free-form description"),
|
|
49
|
+
}, async (args) => {
|
|
50
|
+
try {
|
|
51
|
+
const body = {
|
|
52
|
+
name: args.name,
|
|
53
|
+
privileges: args.privileges,
|
|
54
|
+
};
|
|
55
|
+
if (args.description)
|
|
56
|
+
body.description = args.description;
|
|
57
|
+
const result = await client.postV2("roles", body);
|
|
58
|
+
return reply(`Role '${args.name}' created.\n${JSON.stringify(result, null, 2)}`);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return reply(`Error creating role: ${err}`, true);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// ── Update Role ────────────────────────────────────────────────────────
|
|
65
|
+
// PUT /v2/roles/{name} — privileges array must always be supplied.
|
|
66
|
+
server.tool("update_role", "Update a Cohesity role's privileges and/or description. The full privilege list must be supplied (this is a replace, not a merge).", {
|
|
67
|
+
name: z.string().describe("Role name to update"),
|
|
68
|
+
privileges: z
|
|
69
|
+
.array(z.string())
|
|
70
|
+
.min(1)
|
|
71
|
+
.describe("Full replacement list of privileges"),
|
|
72
|
+
description: z.string().optional().describe("New description"),
|
|
73
|
+
}, async (args) => {
|
|
74
|
+
try {
|
|
75
|
+
const body = { privileges: args.privileges };
|
|
76
|
+
if (args.description !== undefined)
|
|
77
|
+
body.description = args.description;
|
|
78
|
+
const result = await client.putV2(`roles/${encodeURIComponent(args.name)}`, body);
|
|
79
|
+
return reply(`Role '${args.name}' updated.\n${JSON.stringify(result, null, 2)}`);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return reply(`Error updating role: ${err}`, true);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
// ── Delete Role ────────────────────────────────────────────────────────
|
|
86
|
+
server.tool("delete_role", "Delete a Cohesity role by name. Built-in roles cannot be deleted.", { name: z.string().describe("Role name to delete") }, async (args) => {
|
|
87
|
+
try {
|
|
88
|
+
await client.deleteV2(`roles/${encodeURIComponent(args.name)}`);
|
|
89
|
+
return reply(`Role '${args.name}' deleted.`);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
return reply(`Error deleting role: ${err}`, true);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protection run actions and snapshot management tools:
|
|
3
|
+
* - cancel a running protection run (or specific objects within it)
|
|
4
|
+
* - cancel a recovery task
|
|
5
|
+
* - set DataLock/WORM on a snapshot (Compliance or Administrative)
|
|
6
|
+
* - place a snapshot on Legal Hold (or release it)
|
|
7
|
+
* - extend or shorten snapshot retention
|
|
8
|
+
* - delete a snapshot immediately
|
|
9
|
+
*
|
|
10
|
+
* Endpoints used (all verified against cluster_v2_api.yaml):
|
|
11
|
+
* POST /v2/data-protect/protection-groups/{id}/runs/actions
|
|
12
|
+
* POST /v2/data-protect/recoveries/{id}/cancel
|
|
13
|
+
* PUT /v2/data-protect/protection-groups/{id}/runs
|
|
14
|
+
*/
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { CohesityClient } from "../cohesity-client.js";
|
|
17
|
+
export declare function registerRunActionTools(server: McpServer, client: CohesityClient): void;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protection run actions and snapshot management tools:
|
|
3
|
+
* - cancel a running protection run (or specific objects within it)
|
|
4
|
+
* - cancel a recovery task
|
|
5
|
+
* - set DataLock/WORM on a snapshot (Compliance or Administrative)
|
|
6
|
+
* - place a snapshot on Legal Hold (or release it)
|
|
7
|
+
* - extend or shorten snapshot retention
|
|
8
|
+
* - delete a snapshot immediately
|
|
9
|
+
*
|
|
10
|
+
* Endpoints used (all verified against cluster_v2_api.yaml):
|
|
11
|
+
* POST /v2/data-protect/protection-groups/{id}/runs/actions
|
|
12
|
+
* POST /v2/data-protect/recoveries/{id}/cancel
|
|
13
|
+
* PUT /v2/data-protect/protection-groups/{id}/runs
|
|
14
|
+
*/
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
const reply = (text, isError = false) => ({
|
|
17
|
+
content: [{ type: "text", text }],
|
|
18
|
+
isError,
|
|
19
|
+
});
|
|
20
|
+
const RUN_ID_PATTERN = /^\d+:\d+$/;
|
|
21
|
+
const TASK_ID_PATTERN = /^\d+:\d+:\d+$/;
|
|
22
|
+
export function registerRunActionTools(server, client) {
|
|
23
|
+
// ── Cancel Protection Run ──────────────────────────────────────────────
|
|
24
|
+
// POST /v2/data-protect/protection-groups/{id}/runs/actions
|
|
25
|
+
// body: PerformActionOnProtectionGroupRunRequest { action: "Cancel", cancelParams: [...] }
|
|
26
|
+
server.tool("cancel_protection_run", "Cancel a running protection group run. Can cancel the entire run or a subset of objects/copies (local snapshot, archival, replication). Already-completed object tasks are not cancelled.", {
|
|
27
|
+
protection_group_id: z.string().describe("Protection group ID owning the run"),
|
|
28
|
+
run_id: z
|
|
29
|
+
.string()
|
|
30
|
+
.regex(RUN_ID_PATTERN, "Run ID must be in the form <number>:<number>")
|
|
31
|
+
.describe("Run ID to cancel"),
|
|
32
|
+
local_task_id: z
|
|
33
|
+
.string()
|
|
34
|
+
.regex(TASK_ID_PATTERN)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Local backup task ID — cancel just the local copy"),
|
|
37
|
+
archival_task_ids: z
|
|
38
|
+
.array(z.string().regex(TASK_ID_PATTERN))
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("Archival task IDs — cancel just these archival copies"),
|
|
41
|
+
replication_task_ids: z
|
|
42
|
+
.array(z.string().regex(TASK_ID_PATTERN))
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Replication task IDs — cancel just these replication copies"),
|
|
45
|
+
object_ids: z
|
|
46
|
+
.array(z.number())
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Entity IDs — cancel just these objects within the run"),
|
|
49
|
+
}, async (args) => {
|
|
50
|
+
try {
|
|
51
|
+
const cancelEntry = { runId: args.run_id };
|
|
52
|
+
if (args.local_task_id)
|
|
53
|
+
cancelEntry.localTaskId = args.local_task_id;
|
|
54
|
+
if (args.archival_task_ids?.length)
|
|
55
|
+
cancelEntry.archivalTaskId = args.archival_task_ids;
|
|
56
|
+
if (args.replication_task_ids?.length)
|
|
57
|
+
cancelEntry.replicationTaskId = args.replication_task_ids;
|
|
58
|
+
if (args.object_ids?.length)
|
|
59
|
+
cancelEntry.objectIds = args.object_ids;
|
|
60
|
+
const body = {
|
|
61
|
+
action: "Cancel",
|
|
62
|
+
cancelParams: [cancelEntry],
|
|
63
|
+
};
|
|
64
|
+
const result = await client.postV2(`data-protect/protection-groups/${args.protection_group_id}/runs/actions`, body);
|
|
65
|
+
return reply(`Cancel request submitted.\n${JSON.stringify(result, null, 2)}`);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
return reply(`Error cancelling protection run: ${err}`, true);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
// ── Cancel Recovery Task ───────────────────────────────────────────────
|
|
72
|
+
// POST /v2/data-protect/recoveries/{id}/cancel — 204 No Content on success.
|
|
73
|
+
server.tool("cancel_recovery_task", "Cancel an in-flight recovery task by its ID. Returns 204 No Content on success.", {
|
|
74
|
+
recovery_id: z
|
|
75
|
+
.string()
|
|
76
|
+
.regex(TASK_ID_PATTERN)
|
|
77
|
+
.describe("Recovery task ID to cancel"),
|
|
78
|
+
}, async (args) => {
|
|
79
|
+
try {
|
|
80
|
+
await client.postV2(`data-protect/recoveries/${args.recovery_id}/cancel`, {});
|
|
81
|
+
return reply(`Recovery ${args.recovery_id} cancelled.`);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
return reply(`Error cancelling recovery ${args.recovery_id}: ${err}`, true);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// ── Set DataLock (WORM) on a Snapshot ──────────────────────────────────
|
|
88
|
+
// PUT /v2/data-protect/protection-groups/{id}/runs (UpdateProtectionGroupRunRequestBody).
|
|
89
|
+
// Sets localSnapshotConfig.dataLock = "Compliance" | "Administrative".
|
|
90
|
+
server.tool("set_snapshot_datalock", "Apply DataLock (WORM) protection to a protection group run's local snapshot. Once locked, the snapshot cannot be deleted until its retention expires. Compliance lock is even more restrictive — it cannot be released by an administrator.", {
|
|
91
|
+
protection_group_id: z.string().describe("Protection group ID owning the run"),
|
|
92
|
+
run_id: z
|
|
93
|
+
.string()
|
|
94
|
+
.regex(RUN_ID_PATTERN)
|
|
95
|
+
.describe("Run ID whose snapshot will be locked"),
|
|
96
|
+
data_lock_mode: z
|
|
97
|
+
.enum(["Compliance", "Administrative"])
|
|
98
|
+
.describe("Compliance is permanent; Administrative can be released by an admin"),
|
|
99
|
+
}, async (args) => {
|
|
100
|
+
try {
|
|
101
|
+
const body = {
|
|
102
|
+
updateProtectionGroupRunParams: [
|
|
103
|
+
{
|
|
104
|
+
runId: args.run_id,
|
|
105
|
+
localSnapshotConfig: { dataLock: args.data_lock_mode },
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
};
|
|
109
|
+
const result = await client.putV2(`data-protect/protection-groups/${args.protection_group_id}/runs`, body);
|
|
110
|
+
return reply(`DataLock '${args.data_lock_mode}' applied.\n${JSON.stringify(result, null, 2)}`);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return reply(`Error setting DataLock: ${err}`, true);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
// ── Legal Hold ─────────────────────────────────────────────────────────
|
|
117
|
+
// localSnapshotConfig.enableLegalHold = true/false.
|
|
118
|
+
// Requires Data Security Role on the caller.
|
|
119
|
+
server.tool("set_snapshot_legal_hold", "Place a snapshot on legal hold (or release it). While on hold, the snapshot cannot be deleted regardless of retention policy. Requires Data Security Role.", {
|
|
120
|
+
protection_group_id: z.string().describe("Protection group ID owning the run"),
|
|
121
|
+
run_id: z.string().regex(RUN_ID_PATTERN).describe("Run ID whose snapshot to hold"),
|
|
122
|
+
enable: z.boolean().describe("true to place on hold, false to release"),
|
|
123
|
+
}, async (args) => {
|
|
124
|
+
try {
|
|
125
|
+
const body = {
|
|
126
|
+
updateProtectionGroupRunParams: [
|
|
127
|
+
{
|
|
128
|
+
runId: args.run_id,
|
|
129
|
+
localSnapshotConfig: { enableLegalHold: args.enable },
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
const result = await client.putV2(`data-protect/protection-groups/${args.protection_group_id}/runs`, body);
|
|
134
|
+
return reply(`Legal hold ${args.enable ? "applied" : "released"}.\n${JSON.stringify(result, null, 2)}`);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return reply(`Error updating legal hold: ${err}`, true);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
// ── Extend / Shorten Snapshot Retention ────────────────────────────────
|
|
141
|
+
// localSnapshotConfig.daysToKeep — positive extends, negative shortens.
|
|
142
|
+
// If the resulting expiry is before now, the snapshot is deleted immediately.
|
|
143
|
+
server.tool("extend_snapshot_retention", "Extend or shorten a snapshot's retention. Positive days_delta extends, negative shortens. If the resulting expiry falls before now, the snapshot is deleted immediately.", {
|
|
144
|
+
protection_group_id: z.string().describe("Protection group ID owning the run"),
|
|
145
|
+
run_id: z.string().regex(RUN_ID_PATTERN).describe("Run ID to adjust"),
|
|
146
|
+
days_delta: z
|
|
147
|
+
.number()
|
|
148
|
+
.int()
|
|
149
|
+
.describe("Days to add (positive) or subtract (negative) from current retention"),
|
|
150
|
+
}, async (args) => {
|
|
151
|
+
try {
|
|
152
|
+
const body = {
|
|
153
|
+
updateProtectionGroupRunParams: [
|
|
154
|
+
{
|
|
155
|
+
runId: args.run_id,
|
|
156
|
+
localSnapshotConfig: { daysToKeep: args.days_delta },
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
const result = await client.putV2(`data-protect/protection-groups/${args.protection_group_id}/runs`, body);
|
|
161
|
+
return reply(`Retention adjusted by ${args.days_delta} days.\n${JSON.stringify(result, null, 2)}`);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
return reply(`Error adjusting retention: ${err}`, true);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
// ── Delete Snapshot Immediately ────────────────────────────────────────
|
|
168
|
+
// localSnapshotConfig.deleteSnapshot = true. All other params ignored.
|
|
169
|
+
// Will be rejected if the snapshot is on legal hold or under DataLock.
|
|
170
|
+
server.tool("delete_snapshot", "Delete a protection group run's local snapshot immediately. WARNING: irreversible. Will fail if the snapshot is under DataLock or Legal Hold.", {
|
|
171
|
+
protection_group_id: z.string().describe("Protection group ID owning the run"),
|
|
172
|
+
run_id: z.string().regex(RUN_ID_PATTERN).describe("Run ID whose snapshot to delete"),
|
|
173
|
+
}, async (args) => {
|
|
174
|
+
try {
|
|
175
|
+
const body = {
|
|
176
|
+
updateProtectionGroupRunParams: [
|
|
177
|
+
{
|
|
178
|
+
runId: args.run_id,
|
|
179
|
+
localSnapshotConfig: { deleteSnapshot: true },
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
const result = await client.putV2(`data-protect/protection-groups/${args.protection_group_id}/runs`, body);
|
|
184
|
+
return reply(`Snapshot delete requested.\n${JSON.stringify(result, null, 2)}`);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
return reply(`Error deleting snapshot: ${err}`, true);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protection run tools — list and inspect backup run history.
|
|
3
|
+
*/
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { CohesityClient } from "../cohesity-client.js";
|
|
6
|
+
export declare function registerRunsTools(server: McpServer, client: CohesityClient): void;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protection run tools — list and inspect backup run history.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
/** Shorthand for building MCP tool return values. */
|
|
6
|
+
const reply = (text, isError = false) => ({
|
|
7
|
+
content: [{ type: "text", text }],
|
|
8
|
+
isError,
|
|
9
|
+
});
|
|
10
|
+
/** Run type identifiers accepted by the V2 protection runs endpoint. */
|
|
11
|
+
const RUN_TYPES = ["kRegular", "kFull", "kLog", "kSystem"];
|
|
12
|
+
/** Status values accepted by the V2 protection runs endpoint for filtering. */
|
|
13
|
+
const RUN_STATUSES = [
|
|
14
|
+
"Accepted",
|
|
15
|
+
"Running",
|
|
16
|
+
"Canceled",
|
|
17
|
+
"Canceling",
|
|
18
|
+
"Failed",
|
|
19
|
+
"Missed",
|
|
20
|
+
"Succeeded",
|
|
21
|
+
"SucceededWithWarning",
|
|
22
|
+
"OnHold",
|
|
23
|
+
"Finalizing",
|
|
24
|
+
"Skipped",
|
|
25
|
+
"LegalHold",
|
|
26
|
+
];
|
|
27
|
+
export function registerRunsTools(server, client) {
|
|
28
|
+
// ── List Protection Runs ───────────────────────────────────────────────
|
|
29
|
+
server.tool("list_protection_runs", "List recent backup runs for a Cohesity protection group with status, duration, and data size", {
|
|
30
|
+
protection_group_id: z.string().describe("Protection group ID to list runs for"),
|
|
31
|
+
run_types: z
|
|
32
|
+
.array(z.enum(RUN_TYPES))
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Filter by run type (incremental, full, log, system)"),
|
|
35
|
+
local_backup_run_status: z
|
|
36
|
+
.array(z.enum(RUN_STATUSES))
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("Filter by run status"),
|
|
39
|
+
start_time_usecs: z
|
|
40
|
+
.number()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe("Only return runs started after this timestamp (microseconds)"),
|
|
43
|
+
end_time_usecs: z
|
|
44
|
+
.number()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe("Only return runs ended before this timestamp (microseconds)"),
|
|
47
|
+
max_results: z
|
|
48
|
+
.number()
|
|
49
|
+
.optional()
|
|
50
|
+
.default(25)
|
|
51
|
+
.describe("Cap on the number of runs returned"),
|
|
52
|
+
}, async (args) => {
|
|
53
|
+
try {
|
|
54
|
+
const qp = {
|
|
55
|
+
maxCount: String(args.max_results),
|
|
56
|
+
includeObjectDetails: "false",
|
|
57
|
+
};
|
|
58
|
+
if (args.run_types)
|
|
59
|
+
qp.runTypes = args.run_types.join(",");
|
|
60
|
+
if (args.local_backup_run_status)
|
|
61
|
+
qp.localBackupRunStatus = args.local_backup_run_status.join(",");
|
|
62
|
+
if (args.start_time_usecs !== undefined)
|
|
63
|
+
qp.startTimeUsecs = String(args.start_time_usecs);
|
|
64
|
+
if (args.end_time_usecs !== undefined)
|
|
65
|
+
qp.endTimeUsecs = String(args.end_time_usecs);
|
|
66
|
+
const data = await client.getV2(`data-protect/protection-groups/${args.protection_group_id}/runs`, qp);
|
|
67
|
+
return reply(JSON.stringify(data, null, 2));
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
return reply(`Error fetching runs for group ${args.protection_group_id}: ${err}`, true);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// ── Get Protection Run ─────────────────────────────────────────────────
|
|
74
|
+
server.tool("get_protection_run", "Get detailed information about a specific Cohesity backup run including per-object status and statistics", {
|
|
75
|
+
protection_group_id: z.string().describe("Protection group ID the run belongs to"),
|
|
76
|
+
run_id: z.string().describe("Run ID to retrieve details for"),
|
|
77
|
+
include_object_details: z
|
|
78
|
+
.boolean()
|
|
79
|
+
.optional()
|
|
80
|
+
.default(true)
|
|
81
|
+
.describe("Include per-object backup details"),
|
|
82
|
+
}, async (args) => {
|
|
83
|
+
try {
|
|
84
|
+
const qp = {
|
|
85
|
+
includeObjectDetails: String(args.include_object_details),
|
|
86
|
+
};
|
|
87
|
+
const data = await client.getV2(`data-protect/protection-groups/${args.protection_group_id}/runs/${args.run_id}`, qp);
|
|
88
|
+
return reply(JSON.stringify(data, null, 2));
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
return reply(`Error fetching run ${args.run_id} for group ${args.protection_group_id}: ${err}`, true);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source registration tools — register, update, and unregister protection sources.
|
|
3
|
+
* Covers VMware (vCenter / ESXi / vCloud), Physical, Azure, AWS, M365, and Generic NAS.
|
|
4
|
+
*
|
|
5
|
+
* All payload shapes are derived from the cluster's OpenAPI v2 spec
|
|
6
|
+
* (cluster_v2_api.yaml). Field names match the spec exactly; deviating from
|
|
7
|
+
* the spec produces KValidationError from the cluster.
|
|
8
|
+
*/
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { CohesityClient } from "../cohesity-client.js";
|
|
11
|
+
export declare function registerSourceRegistrationTools(server: McpServer, client: CohesityClient): void;
|