@gcnv/gcnv-mcp-server 1.0.3

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.
Files changed (33) hide show
  1. package/GEMINI.md +200 -0
  2. package/LICENSE +201 -0
  3. package/README.md +374 -0
  4. package/build/index.js +185 -0
  5. package/build/logger.js +19 -0
  6. package/build/registry/register-tools.js +101 -0
  7. package/build/registry/tool-registry.js +27 -0
  8. package/build/tools/active-directory-tools.js +124 -0
  9. package/build/tools/backup-policy-tools.js +140 -0
  10. package/build/tools/backup-tools.js +178 -0
  11. package/build/tools/backup-vault-tools.js +147 -0
  12. package/build/tools/handlers/active-directory-handler.js +321 -0
  13. package/build/tools/handlers/backup-handler.js +451 -0
  14. package/build/tools/handlers/backup-policy-handler.js +275 -0
  15. package/build/tools/handlers/backup-vault-handler.js +370 -0
  16. package/build/tools/handlers/kms-config-handler.js +327 -0
  17. package/build/tools/handlers/operation-handler.js +254 -0
  18. package/build/tools/handlers/quota-rule-handler.js +411 -0
  19. package/build/tools/handlers/replication-handler.js +504 -0
  20. package/build/tools/handlers/snapshot-handler.js +320 -0
  21. package/build/tools/handlers/storage-pool-handler.js +346 -0
  22. package/build/tools/handlers/volume-handler.js +353 -0
  23. package/build/tools/kms-config-tools.js +162 -0
  24. package/build/tools/operation-tools.js +64 -0
  25. package/build/tools/quota-rule-tools.js +166 -0
  26. package/build/tools/replication-tools.js +227 -0
  27. package/build/tools/snapshot-tools.js +124 -0
  28. package/build/tools/storage-pool-tools.js +215 -0
  29. package/build/tools/volume-tools.js +216 -0
  30. package/build/types/tool.js +1 -0
  31. package/build/utils/netapp-client-factory.js +53 -0
  32. package/gemini-extension.json +12 -0
  33. package/package.json +66 -0
@@ -0,0 +1,411 @@
1
+ import { NetAppClientFactory } from '../../utils/netapp-client-factory.js';
2
+ import { logger } from '../../logger.js';
3
+ const log = logger.child({ module: 'quota-rule-handler' });
4
+ // Basic runtime validation so we fail fast before calling the NetApp API
5
+ function validatePathArgs(args, requireQuotaRuleId = false) {
6
+ const errors = [];
7
+ const required = [
8
+ { key: 'projectId', value: args.projectId },
9
+ { key: 'location', value: args.location },
10
+ { key: 'volumeId', value: args.volumeId },
11
+ ];
12
+ if (requireQuotaRuleId) {
13
+ required.push({ key: 'quotaRuleId', value: args.quotaRuleId });
14
+ }
15
+ for (const field of required) {
16
+ if (typeof field.value !== 'string' || field.value.trim() === '') {
17
+ errors.push(`Missing or invalid ${field.key}`);
18
+ }
19
+ }
20
+ return errors;
21
+ }
22
+ function parseOptionalNumber(value, fieldName) {
23
+ if (value === undefined)
24
+ return {};
25
+ const num = Number(value);
26
+ if (!Number.isFinite(num) || num < 0) {
27
+ return { error: `${fieldName} must be a non-negative number` };
28
+ }
29
+ return { value: num };
30
+ }
31
+ // Helper to format quota rule data
32
+ function formatQuotaRuleData(rule) {
33
+ const result = {};
34
+ if (!rule)
35
+ return result;
36
+ if (rule.name) {
37
+ const nameParts = rule.name.split('/');
38
+ result.name = rule.name;
39
+ result.quotaRuleId = nameParts[nameParts.length - 1];
40
+ }
41
+ if (rule.target)
42
+ result.target = rule.target;
43
+ if (rule.type) {
44
+ result.type = rule.type;
45
+ result.quotaType = rule.type;
46
+ }
47
+ else if (rule.quotaType) {
48
+ // fallback if API ever returns legacy field name
49
+ result.quotaType = rule.quotaType;
50
+ result.type = rule.quotaType;
51
+ }
52
+ if (rule.diskLimitMib !== undefined) {
53
+ const mib = Number(rule.diskLimitMib);
54
+ result.diskLimitMib = mib;
55
+ }
56
+ if (rule.state)
57
+ result.state = rule.state;
58
+ if (rule.createTime) {
59
+ result.createTime = new Date(rule.createTime.seconds * 1000);
60
+ }
61
+ if (rule.description)
62
+ result.description = rule.description;
63
+ if (rule.labels)
64
+ result.labels = rule.labels;
65
+ return result;
66
+ }
67
+ // Validate and normalize quota rule type to numeric enum value
68
+ function parseQuotaType(input) {
69
+ const enumMap = {
70
+ TYPE_UNSPECIFIED: 0,
71
+ INDIVIDUAL_USER_QUOTA: 1,
72
+ INDIVIDUAL_GROUP_QUOTA: 2,
73
+ DEFAULT_USER_QUOTA: 3,
74
+ DEFAULT_GROUP_QUOTA: 4,
75
+ };
76
+ if (input === undefined || input === null)
77
+ return {};
78
+ // Accept numeric enum values directly
79
+ if (typeof input === 'number') {
80
+ if (Object.values(enumMap).includes(input)) {
81
+ return { value: input };
82
+ }
83
+ return { error: 'quotaType/type must be a valid enum number (0-4)' };
84
+ }
85
+ if (typeof input === 'string') {
86
+ const trimmed = input.trim().toUpperCase();
87
+ if (enumMap[trimmed] !== undefined) {
88
+ return { value: enumMap[trimmed] };
89
+ }
90
+ return {
91
+ error: 'quotaType/type must be one of TYPE_UNSPECIFIED, INDIVIDUAL_USER_QUOTA, INDIVIDUAL_GROUP_QUOTA, DEFAULT_USER_QUOTA, DEFAULT_GROUP_QUOTA, or the corresponding enum number',
92
+ };
93
+ }
94
+ return { error: 'quotaType/type must be a string enum name or enum number' };
95
+ }
96
+ // Create Quota Rule Handler
97
+ export const createQuotaRuleHandler = async (args) => {
98
+ try {
99
+ const { projectId, location, volumeId, quotaRuleId, target, quotaType, type, diskLimitMib, description, labels, } = args;
100
+ const errors = [
101
+ ...validatePathArgs({ projectId, location, volumeId, quotaRuleId }, true),
102
+ ];
103
+ if (!target || typeof target !== 'string' || target.trim() === '') {
104
+ errors.push('target is required');
105
+ }
106
+ const { value: parsedType, error: typeError } = parseQuotaType(type ?? quotaType);
107
+ if (typeError)
108
+ errors.push(typeError);
109
+ if (parsedType === undefined)
110
+ errors.push('quotaType/type is required');
111
+ const { value: parsedDiskLimitMib, error: diskLimitError } = parseOptionalNumber(diskLimitMib, 'diskLimitMib');
112
+ if (diskLimitError)
113
+ errors.push(diskLimitError);
114
+ if (parsedDiskLimitMib === undefined)
115
+ errors.push('diskLimitMib is required');
116
+ if (errors.length > 0) {
117
+ return {
118
+ isError: true,
119
+ content: [
120
+ {
121
+ type: 'text',
122
+ text: `Invalid input: ${errors.join('; ')}`,
123
+ },
124
+ ],
125
+ };
126
+ }
127
+ const netAppClient = NetAppClientFactory.createClient();
128
+ const parent = `projects/${projectId}/locations/${location}/volumes/${volumeId}`;
129
+ const quotaRule = {};
130
+ if (target)
131
+ quotaRule.target = target;
132
+ if (parsedType !== undefined)
133
+ quotaRule.type = parsedType;
134
+ if (parsedDiskLimitMib !== undefined)
135
+ quotaRule.diskLimitMib = parsedDiskLimitMib;
136
+ if (description)
137
+ quotaRule.description = description;
138
+ if (labels)
139
+ quotaRule.labels = labels;
140
+ const request = {
141
+ parent,
142
+ quotaRuleId,
143
+ quotaRule,
144
+ };
145
+ log.info({ request }, 'Create Quota Rule request');
146
+ const [operation] = await netAppClient.createQuotaRule(request);
147
+ log.info({ operation }, 'Create Quota Rule operation');
148
+ return {
149
+ content: [
150
+ {
151
+ type: 'text',
152
+ text: JSON.stringify({
153
+ name: `projects/${projectId}/locations/${location}/volumes/${volumeId}/quotaRules/${quotaRuleId}`,
154
+ operation: operation,
155
+ }, null, 2),
156
+ },
157
+ ],
158
+ structuredContent: {
159
+ name: `projects/${projectId}/locations/${location}/volumes/${volumeId}/quotaRules/${quotaRuleId}`,
160
+ operationId: operation.name || '',
161
+ },
162
+ };
163
+ }
164
+ catch (error) {
165
+ log.error({ err: error }, 'Error creating quota rule');
166
+ return {
167
+ isError: true,
168
+ content: [
169
+ {
170
+ type: 'text',
171
+ text: `Error creating quota rule: ${error.message || 'Unknown error'}`,
172
+ },
173
+ ],
174
+ };
175
+ }
176
+ };
177
+ // Delete Quota Rule Handler
178
+ export const deleteQuotaRuleHandler = async (args) => {
179
+ try {
180
+ const { projectId, location, volumeId, quotaRuleId } = args;
181
+ const errors = validatePathArgs({ projectId, location, volumeId, quotaRuleId }, true);
182
+ if (errors.length > 0) {
183
+ return {
184
+ isError: true,
185
+ content: [
186
+ {
187
+ type: 'text',
188
+ text: `Invalid input: ${errors.join('; ')}`,
189
+ },
190
+ ],
191
+ };
192
+ }
193
+ const netAppClient = NetAppClientFactory.createClient();
194
+ const name = `projects/${projectId}/locations/${location}/volumes/${volumeId}/quotaRules/${quotaRuleId}`;
195
+ const [operation] = await netAppClient.deleteQuotaRule({ name });
196
+ return {
197
+ content: [
198
+ {
199
+ type: 'text',
200
+ text: JSON.stringify({
201
+ message: `Quota rule ${quotaRuleId} deletion requested`,
202
+ operation: operation,
203
+ }, null, 2),
204
+ },
205
+ ],
206
+ structuredContent: {
207
+ success: true,
208
+ operationId: operation.name || '',
209
+ },
210
+ };
211
+ }
212
+ catch (error) {
213
+ log.error({ err: error }, 'Error deleting quota rule');
214
+ return {
215
+ isError: true,
216
+ content: [
217
+ {
218
+ type: 'text',
219
+ text: `Error deleting quota rule: ${error.message || 'Unknown error'}`,
220
+ },
221
+ ],
222
+ };
223
+ }
224
+ };
225
+ // Get Quota Rule Handler
226
+ export const getQuotaRuleHandler = async (args) => {
227
+ try {
228
+ const { projectId, location, volumeId, quotaRuleId } = args;
229
+ const errors = validatePathArgs({ projectId, location, volumeId, quotaRuleId }, true);
230
+ if (errors.length > 0) {
231
+ return {
232
+ isError: true,
233
+ content: [
234
+ {
235
+ type: 'text',
236
+ text: `Invalid input: ${errors.join('; ')}`,
237
+ },
238
+ ],
239
+ };
240
+ }
241
+ const netAppClient = NetAppClientFactory.createClient();
242
+ const name = `projects/${projectId}/locations/${location}/volumes/${volumeId}/quotaRules/${quotaRuleId}`;
243
+ const [quotaRule] = await netAppClient.getQuotaRule({ name });
244
+ const formatted = formatQuotaRuleData(quotaRule);
245
+ return {
246
+ content: [
247
+ {
248
+ type: 'text',
249
+ text: JSON.stringify(formatted, null, 2),
250
+ },
251
+ ],
252
+ structuredContent: formatted,
253
+ };
254
+ }
255
+ catch (error) {
256
+ log.error({ err: error }, 'Error getting quota rule');
257
+ return {
258
+ isError: true,
259
+ content: [
260
+ {
261
+ type: 'text',
262
+ text: `Error getting quota rule: ${error.message || 'Unknown error'}`,
263
+ },
264
+ ],
265
+ };
266
+ }
267
+ };
268
+ // List Quota Rules Handler
269
+ export const listQuotaRulesHandler = async (args) => {
270
+ try {
271
+ const { projectId, location, volumeId, filter, pageSize, pageToken, orderBy } = args;
272
+ const errors = validatePathArgs({ projectId, location, volumeId }, false);
273
+ const { value: parsedPageSize, error: pageSizeError } = parseOptionalNumber(pageSize, 'pageSize');
274
+ if (pageSizeError)
275
+ errors.push(pageSizeError);
276
+ if (errors.length > 0) {
277
+ return {
278
+ isError: true,
279
+ content: [
280
+ {
281
+ type: 'text',
282
+ text: `Invalid input: ${errors.join('; ')}`,
283
+ },
284
+ ],
285
+ };
286
+ }
287
+ const netAppClient = NetAppClientFactory.createClient();
288
+ const parent = `projects/${projectId}/locations/${location}/volumes/${volumeId}`;
289
+ const request = { parent };
290
+ if (filter)
291
+ request.filter = filter;
292
+ if (parsedPageSize !== undefined)
293
+ request.pageSize = parsedPageSize;
294
+ if (pageToken)
295
+ request.pageToken = pageToken;
296
+ if (orderBy)
297
+ request.orderBy = orderBy;
298
+ const [quotaRules, , response] = await netAppClient.listQuotaRules(request);
299
+ const formatted = quotaRules.map(formatQuotaRuleData);
300
+ return {
301
+ content: [
302
+ {
303
+ type: 'text',
304
+ text: JSON.stringify({ quotaRules, nextPageToken: response?.nextPageToken }, null, 2),
305
+ },
306
+ ],
307
+ structuredContent: {
308
+ quotaRules: formatted,
309
+ nextPageToken: response?.nextPageToken,
310
+ },
311
+ };
312
+ }
313
+ catch (error) {
314
+ log.error({ err: error }, 'Error listing quota rules');
315
+ return {
316
+ isError: true,
317
+ content: [
318
+ {
319
+ type: 'text',
320
+ text: `Error listing quota rules: ${error.message || 'Unknown error'}`,
321
+ },
322
+ ],
323
+ };
324
+ }
325
+ };
326
+ // Update Quota Rule Handler
327
+ export const updateQuotaRuleHandler = async (args) => {
328
+ try {
329
+ const { projectId, location, volumeId, quotaRuleId, target, quotaType, type, diskLimitMib, description, labels, } = args;
330
+ const errors = [
331
+ ...validatePathArgs({ projectId, location, volumeId, quotaRuleId }, true),
332
+ ];
333
+ const { value: parsedDiskLimitMib, error: diskLimitError } = parseOptionalNumber(diskLimitMib, 'diskLimitMib');
334
+ if (diskLimitError)
335
+ errors.push(diskLimitError);
336
+ const { value: parsedType, error: typeError } = parseQuotaType(type ?? quotaType);
337
+ if (typeError)
338
+ errors.push(typeError);
339
+ const netAppClient = NetAppClientFactory.createClient();
340
+ const name = `projects/${projectId}/locations/${location}/volumes/${volumeId}/quotaRules/${quotaRuleId}`;
341
+ const updateMask = [];
342
+ const quotaRule = { name };
343
+ if (target !== undefined) {
344
+ quotaRule.target = target;
345
+ updateMask.push('target');
346
+ }
347
+ if (parsedType !== undefined) {
348
+ quotaRule.type = parsedType;
349
+ updateMask.push('type');
350
+ }
351
+ if (parsedDiskLimitMib !== undefined) {
352
+ quotaRule.diskLimitMib = parsedDiskLimitMib;
353
+ updateMask.push('disk_limit_mib');
354
+ }
355
+ if (description !== undefined) {
356
+ quotaRule.description = description;
357
+ updateMask.push('description');
358
+ }
359
+ if (labels !== undefined) {
360
+ quotaRule.labels = labels;
361
+ updateMask.push('labels');
362
+ }
363
+ if (updateMask.length === 0) {
364
+ errors.push('Provide at least one field to update');
365
+ }
366
+ if (errors.length > 0) {
367
+ return {
368
+ isError: true,
369
+ content: [
370
+ {
371
+ type: 'text',
372
+ text: `Invalid input: ${errors.join('; ')}`,
373
+ },
374
+ ],
375
+ };
376
+ }
377
+ const request = {
378
+ quotaRule,
379
+ updateMask: updateMask.length > 0 ? { paths: updateMask } : undefined,
380
+ };
381
+ log.info({ request }, 'Update Quota Rule request');
382
+ const [operation] = await netAppClient.updateQuotaRule(request);
383
+ return {
384
+ content: [
385
+ {
386
+ type: 'text',
387
+ text: JSON.stringify({
388
+ message: `Quota rule ${quotaRuleId} update requested`,
389
+ operation: operation,
390
+ }, null, 2),
391
+ },
392
+ ],
393
+ structuredContent: {
394
+ name: name,
395
+ operationId: operation.name || '',
396
+ },
397
+ };
398
+ }
399
+ catch (error) {
400
+ log.error({ err: error }, 'Error updating quota rule');
401
+ return {
402
+ isError: true,
403
+ content: [
404
+ {
405
+ type: 'text',
406
+ text: `Error updating quota rule: ${error.message || 'Unknown error'}`,
407
+ },
408
+ ],
409
+ };
410
+ }
411
+ };