glaip-sdk 0.6.25__py3-none-any.whl → 0.6.26__py3-none-any.whl

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 (51) hide show
  1. glaip_sdk/cli/commands/agents/__init__.py +119 -0
  2. glaip_sdk/cli/commands/agents/_common.py +561 -0
  3. glaip_sdk/cli/commands/agents/create.py +151 -0
  4. glaip_sdk/cli/commands/agents/delete.py +64 -0
  5. glaip_sdk/cli/commands/agents/get.py +89 -0
  6. glaip_sdk/cli/commands/agents/list.py +129 -0
  7. glaip_sdk/cli/commands/agents/run.py +264 -0
  8. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  9. glaip_sdk/cli/commands/agents/update.py +112 -0
  10. glaip_sdk/cli/commands/mcps/__init__.py +98 -0
  11. glaip_sdk/cli/commands/mcps/_common.py +490 -0
  12. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  13. glaip_sdk/cli/commands/mcps/create.py +153 -0
  14. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  15. glaip_sdk/cli/commands/mcps/get.py +212 -0
  16. glaip_sdk/cli/commands/mcps/list.py +69 -0
  17. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  18. glaip_sdk/cli/commands/mcps/update.py +146 -0
  19. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  20. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  21. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  22. glaip_sdk/cli/commands/tools/_common.py +80 -0
  23. glaip_sdk/cli/commands/tools/create.py +228 -0
  24. glaip_sdk/cli/commands/tools/delete.py +61 -0
  25. glaip_sdk/cli/commands/tools/get.py +103 -0
  26. glaip_sdk/cli/commands/tools/list.py +69 -0
  27. glaip_sdk/cli/commands/tools/script.py +49 -0
  28. glaip_sdk/cli/commands/tools/update.py +102 -0
  29. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  30. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  31. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  32. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  33. glaip_sdk/client/_agent_payloads.py +32 -500
  34. glaip_sdk/client/agents.py +1 -1
  35. glaip_sdk/client/main.py +1 -1
  36. glaip_sdk/client/mcps.py +44 -13
  37. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  38. glaip_sdk/client/payloads/agent/requests.py +495 -0
  39. glaip_sdk/client/payloads/agent/responses.py +43 -0
  40. glaip_sdk/client/tools.py +38 -3
  41. glaip_sdk/tools/base.py +41 -10
  42. glaip_sdk/utils/import_resolver.py +40 -2
  43. {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.6.26.dist-info}/METADATA +1 -1
  44. {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.6.26.dist-info}/RECORD +48 -16
  45. glaip_sdk/cli/commands/agents.py +0 -1502
  46. glaip_sdk/cli/commands/mcps.py +0 -1355
  47. glaip_sdk/cli/commands/tools.py +0 -575
  48. /glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +0 -0
  49. {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.6.26.dist-info}/WHEEL +0 -0
  50. {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.6.26.dist-info}/entry_points.txt +0 -0
  51. {glaip_sdk-0.6.25.dist-info → glaip_sdk-0.6.26.dist-info}/top_level.txt +0 -0
@@ -1,1355 +0,0 @@
1
- """MCP management commands.
2
-
3
- Authors:
4
- Raymond Christopher (raymond.christopher@gdplabs.id)
5
- Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
6
- """
7
-
8
- import json
9
- import sys
10
- from pathlib import Path
11
- from typing import Any
12
-
13
- import click
14
- from rich.console import Console
15
-
16
- from glaip_sdk.branding import (
17
- ACCENT_STYLE,
18
- INFO,
19
- SUCCESS,
20
- SUCCESS_STYLE,
21
- WARNING_STYLE,
22
- )
23
- from glaip_sdk.cli.context import detect_export_format, get_ctx_value, output_flags
24
- from glaip_sdk.cli.display import (
25
- display_api_error,
26
- display_confirmation_prompt,
27
- display_creation_success,
28
- display_deletion_success,
29
- display_update_success,
30
- handle_json_output,
31
- handle_rich_output,
32
- )
33
- from glaip_sdk.cli.io import (
34
- fetch_raw_resource_details,
35
- load_resource_from_file_with_validation,
36
- )
37
- from glaip_sdk.cli.mcp_validators import (
38
- validate_mcp_auth_structure,
39
- validate_mcp_config_structure,
40
- )
41
- from glaip_sdk.cli.parsers.json_input import parse_json_input
42
- from glaip_sdk.cli.resolution import resolve_resource_reference
43
- from glaip_sdk.cli.rich_helpers import print_markup
44
- from glaip_sdk.cli.core.context import get_client
45
- from glaip_sdk.cli.core.output import (
46
- coerce_to_row,
47
- fetch_resource_for_export,
48
- format_datetime_fields,
49
- output_list,
50
- output_result,
51
- )
52
- from glaip_sdk.cli.core.rendering import spinner_context, with_client_and_spinner
53
- from glaip_sdk.config.constants import (
54
- DEFAULT_MCP_TYPE,
55
- )
56
- from glaip_sdk.icons import ICON_TOOL
57
- from glaip_sdk.rich_components import AIPPanel
58
- from glaip_sdk.utils.import_export import convert_export_to_import_format
59
- from glaip_sdk.utils.serialization import (
60
- build_mcp_export_payload,
61
- write_resource_export,
62
- )
63
-
64
- console = Console()
65
- MAX_DESCRIPTION_LEN = 50
66
-
67
-
68
- def _is_sensitive_data(val: Any) -> bool:
69
- """Check if value contains sensitive authentication data.
70
-
71
- Args:
72
- val: Value to check for sensitive information
73
-
74
- Returns:
75
- True if the value appears to contain sensitive data
76
- """
77
- if not isinstance(val, dict):
78
- return False
79
-
80
- sensitive_patterns = {"token", "password", "secret", "key", "credential"}
81
- return any(pattern in str(k).lower() for k in val.keys() for pattern in sensitive_patterns)
82
-
83
-
84
- def _redact_sensitive_dict(val: dict[str, Any]) -> dict[str, Any]:
85
- """Redact sensitive fields from a dictionary.
86
-
87
- Args:
88
- val: Dictionary to redact
89
-
90
- Returns:
91
- Redacted dictionary
92
- """
93
- redacted = val.copy()
94
- sensitive_patterns = {"token", "password", "secret", "key", "credential"}
95
- for k in redacted.keys():
96
- if any(pattern in k.lower() for pattern in sensitive_patterns):
97
- redacted[k] = "<REDACTED>"
98
- return redacted
99
-
100
-
101
- def _format_dict_value(val: dict[str, Any]) -> str:
102
- """Format a dictionary value for display.
103
-
104
- Args:
105
- val: Dictionary to format
106
-
107
- Returns:
108
- Formatted string representation
109
- """
110
- if _is_sensitive_data(val):
111
- redacted = _redact_sensitive_dict(val)
112
- return json.dumps(redacted, indent=2)
113
- return json.dumps(val, indent=2)
114
-
115
-
116
- def _format_preview_value(val: Any) -> str:
117
- """Format a value for display in update preview with sensitive data redaction.
118
-
119
- Args:
120
- val: Value to format
121
-
122
- Returns:
123
- Formatted string representation of the value
124
- """
125
- if val is None:
126
- return "[dim]None[/dim]"
127
- if isinstance(val, dict):
128
- return _format_dict_value(val)
129
- if isinstance(val, str):
130
- return f'"{val}"' if val else '""'
131
- return str(val)
132
-
133
-
134
- def _build_empty_override_warnings(empty_fields: list[str]) -> list[str]:
135
- """Build warning lines for empty CLI overrides.
136
-
137
- Args:
138
- empty_fields: List of field names with empty string overrides
139
-
140
- Returns:
141
- List of formatted warning lines
142
- """
143
- if not empty_fields:
144
- return []
145
-
146
- warnings = ["\n[yellow]⚠️ Warning: Empty values provided via CLI will override import values[/yellow]"]
147
- warnings.extend(f"- [yellow]{field}: will be set to empty string[/yellow]" for field in empty_fields)
148
- return warnings
149
-
150
-
151
- def _validate_import_payload_fields(import_payload: dict[str, Any]) -> bool:
152
- """Validate that import payload contains updatable fields.
153
-
154
- Args:
155
- import_payload: Import payload to validate
156
-
157
- Returns:
158
- True if payload has updatable fields, False otherwise
159
- """
160
- updatable_fields = {"name", "transport", "description", "config", "authentication"}
161
- has_updatable = any(field in import_payload for field in updatable_fields)
162
-
163
- if not has_updatable:
164
- available_fields = set(import_payload.keys())
165
- print_markup(
166
- "[yellow]⚠️ No updatable fields found in import file.[/yellow]\n"
167
- f"[dim]Found fields: {', '.join(sorted(available_fields))}[/dim]\n"
168
- f"[dim]Updatable fields: {', '.join(sorted(updatable_fields))}[/dim]"
169
- )
170
- return has_updatable
171
-
172
-
173
- def _get_config_transport(
174
- transport: str | None,
175
- import_payload: dict[str, Any] | None,
176
- mcp: Any,
177
- ) -> str | None:
178
- """Get the transport value for config validation.
179
-
180
- Args:
181
- transport: CLI transport flag
182
- import_payload: Optional import payload
183
- mcp: Current MCP object
184
-
185
- Returns:
186
- Transport value or None
187
- """
188
- if import_payload:
189
- return transport or import_payload.get("transport")
190
- return transport or getattr(mcp, "transport", None)
191
-
192
-
193
- def _build_update_data_from_sources(
194
- import_payload: dict[str, Any] | None,
195
- mcp: Any,
196
- name: str | None,
197
- transport: str | None,
198
- description: str | None,
199
- config: str | None,
200
- auth: str | None,
201
- ) -> dict[str, Any]:
202
- """Build update data from import payload and CLI flags.
203
-
204
- Args:
205
- import_payload: Optional import payload
206
- mcp: Current MCP object
207
- name: CLI name flag
208
- transport: CLI transport flag
209
- description: CLI description flag
210
- config: CLI config flag
211
- auth: CLI auth flag
212
-
213
- Returns:
214
- Dictionary with update data
215
- """
216
- update_data = {}
217
-
218
- # Start with import data if available
219
- if import_payload:
220
- updatable_fields = [
221
- "name",
222
- "transport",
223
- "description",
224
- "config",
225
- "authentication",
226
- ]
227
- for field in updatable_fields:
228
- if field in import_payload:
229
- update_data[field] = import_payload[field]
230
-
231
- # CLI flags override import values
232
- if name is not None:
233
- update_data["name"] = name
234
- if transport is not None:
235
- update_data["transport"] = transport
236
- if description is not None:
237
- update_data["description"] = description
238
- if config is not None:
239
- parsed_config = parse_json_input(config)
240
- config_transport = _get_config_transport(transport, import_payload, mcp)
241
- update_data["config"] = validate_mcp_config_structure(
242
- parsed_config,
243
- transport=config_transport,
244
- source="--config",
245
- )
246
- if auth is not None:
247
- parsed_auth = parse_json_input(auth)
248
- update_data["authentication"] = validate_mcp_auth_structure(parsed_auth, source="--auth")
249
-
250
- return update_data
251
-
252
-
253
- def _collect_cli_overrides(
254
- name: str | None,
255
- transport: str | None,
256
- description: str | None,
257
- config: str | None,
258
- auth: str | None,
259
- ) -> dict[str, Any]:
260
- """Collect CLI flags that were explicitly provided.
261
-
262
- Args:
263
- name: CLI name flag
264
- transport: CLI transport flag
265
- description: CLI description flag
266
- config: CLI config flag
267
- auth: CLI auth flag
268
-
269
- Returns:
270
- Dictionary of provided CLI overrides
271
- """
272
- cli_overrides = {}
273
- if name is not None:
274
- cli_overrides["name"] = name
275
- if transport is not None:
276
- cli_overrides["transport"] = transport
277
- if description is not None:
278
- cli_overrides["description"] = description
279
- if config is not None:
280
- cli_overrides["config"] = config
281
- if auth is not None:
282
- cli_overrides["auth"] = auth
283
- return cli_overrides
284
-
285
-
286
- def _handle_cli_error(ctx: Any, error: Exception, operation: str) -> None:
287
- """Render CLI error once and exit with non-zero status."""
288
- handle_json_output(ctx, error=error)
289
- if get_ctx_value(ctx, "view") != "json":
290
- display_api_error(error, operation)
291
- ctx.exit(1)
292
-
293
-
294
- @click.group(name="mcps", no_args_is_help=True)
295
- def mcps_group() -> None:
296
- """MCP management operations.
297
-
298
- Provides commands for creating, listing, updating, deleting, and managing
299
- Model Context Protocol (MCP) configurations.
300
- """
301
- pass
302
-
303
-
304
- def _resolve_mcp(ctx: Any, client: Any, ref: str, select: int | None = None) -> Any | None:
305
- """Resolve an MCP server by ID or name, with interactive selection support.
306
-
307
- This function provides MCP-specific resolution logic. It delegates to
308
- resolve_resource_reference for MCP-specific resolution, supporting UUID
309
- lookups and name-based fuzzy matching.
310
-
311
- Args:
312
- ctx: Click context for command execution.
313
- client: API client for backend operations.
314
- ref: MCP identifier (UUID or name string).
315
- select: Optional selection index when multiple MCPs match (1-based).
316
-
317
- Returns:
318
- MCP instance if resolution succeeds, None if not found.
319
-
320
- Raises:
321
- click.ClickException: When resolution fails or selection is invalid.
322
- """
323
- # Configure MCP-specific resolution functions
324
- mcp_client = client.mcps
325
- get_by_id_func = mcp_client.get_mcp_by_id
326
- find_by_name_func = mcp_client.find_mcps
327
- # Use MCP-specific resolution with standard fuzzy matching
328
- return resolve_resource_reference(
329
- ctx,
330
- client,
331
- ref,
332
- "mcp",
333
- get_by_id_func,
334
- find_by_name_func,
335
- "MCP",
336
- select=select,
337
- )
338
-
339
-
340
- def _strip_server_only_fields(import_data: dict[str, Any]) -> dict[str, Any]:
341
- """Remove fields that should not be forwarded during import-driven creation.
342
-
343
- Args:
344
- import_data: Raw import payload loaded from disk.
345
-
346
- Returns:
347
- A shallow copy of the data with server-managed fields removed.
348
- """
349
- cleaned = dict(import_data)
350
- for key in (
351
- "id",
352
- "type",
353
- "status",
354
- "connection_status",
355
- "created_at",
356
- "updated_at",
357
- ):
358
- cleaned.pop(key, None)
359
- return cleaned
360
-
361
-
362
- def _load_import_ready_payload(import_file: str) -> dict[str, Any]:
363
- """Load and normalise an imported MCP definition for create operations.
364
-
365
- Args:
366
- import_file: Path to an MCP export file (JSON or YAML).
367
-
368
- Returns:
369
- Normalised import payload ready for CLI/REST usage.
370
-
371
- Raises:
372
- click.ClickException: If the file cannot be parsed or validated.
373
- """
374
- raw_data = load_resource_from_file_with_validation(Path(import_file), "MCP")
375
- import_data = convert_export_to_import_format(raw_data)
376
- import_data = _strip_server_only_fields(import_data)
377
-
378
- transport = import_data.get("transport")
379
-
380
- if "config" in import_data:
381
- import_data["config"] = validate_mcp_config_structure(
382
- import_data["config"],
383
- transport=transport,
384
- source="import file",
385
- )
386
-
387
- if "authentication" in import_data:
388
- import_data["authentication"] = validate_mcp_auth_structure(
389
- import_data["authentication"],
390
- source="import file",
391
- )
392
-
393
- return import_data
394
-
395
-
396
- def _coerce_cli_string(value: str | None) -> str | None:
397
- """Normalise CLI string values so blanks are treated as missing.
398
-
399
- Args:
400
- value: User-provided string option.
401
-
402
- Returns:
403
- The stripped string, or ``None`` when the value is blank/whitespace-only.
404
- """
405
- if value is None:
406
- return None
407
- trimmed = value.strip()
408
- # Treat whitespace-only strings as None
409
- return trimmed if trimmed else None
410
-
411
-
412
- def _merge_config_field(
413
- merged_base: dict[str, Any],
414
- cli_config: str | None,
415
- final_transport: str | None,
416
- ) -> None:
417
- """Merge config field with validation.
418
-
419
- Args:
420
- merged_base: Base payload to update in-place.
421
- cli_config: Raw CLI JSON string for config.
422
- final_transport: Transport type for validation.
423
-
424
- Raises:
425
- click.ClickException: If config JSON parsing or validation fails.
426
- """
427
- if cli_config is not None:
428
- parsed_config = parse_json_input(cli_config)
429
- merged_base["config"] = validate_mcp_config_structure(
430
- parsed_config,
431
- transport=final_transport,
432
- source="--config",
433
- )
434
- elif "config" not in merged_base or merged_base["config"] is None:
435
- merged_base["config"] = {}
436
-
437
-
438
- def _merge_auth_field(
439
- merged_base: dict[str, Any],
440
- cli_auth: str | None,
441
- ) -> None:
442
- """Merge authentication field with validation.
443
-
444
- Args:
445
- merged_base: Base payload to update in-place.
446
- cli_auth: Raw CLI JSON string for authentication.
447
-
448
- Raises:
449
- click.ClickException: If auth JSON parsing or validation fails.
450
- """
451
- if cli_auth is not None:
452
- parsed_auth = parse_json_input(cli_auth)
453
- merged_base["authentication"] = validate_mcp_auth_structure(
454
- parsed_auth,
455
- source="--auth",
456
- )
457
- elif "authentication" not in merged_base:
458
- merged_base["authentication"] = None
459
-
460
-
461
- def _merge_import_payload(
462
- import_data: dict[str, Any] | None,
463
- *,
464
- cli_name: str | None,
465
- cli_transport: str | None,
466
- cli_description: str | None,
467
- cli_config: str | None,
468
- cli_auth: str | None,
469
- ) -> tuple[dict[str, Any], list[str]]:
470
- """Merge import data with CLI overrides while tracking missing fields.
471
-
472
- Args:
473
- import_data: Normalised payload loaded from file (if provided).
474
- cli_name: Name supplied via CLI option.
475
- cli_transport: Transport supplied via CLI option.
476
- cli_description: Description supplied via CLI option.
477
- cli_config: Raw CLI JSON string for config.
478
- cli_auth: Raw CLI JSON string for authentication.
479
-
480
- Returns:
481
- A tuple of (merged_payload, missing_required_fields).
482
-
483
- Raises:
484
- click.ClickException: If config/auth JSON parsing or validation fails.
485
- """
486
- merged_base = import_data.copy() if import_data else {}
487
-
488
- # Merge simple string fields using truthy CLI overrides
489
- for field, cli_value in (
490
- ("name", _coerce_cli_string(cli_name)),
491
- ("transport", _coerce_cli_string(cli_transport)),
492
- ("description", _coerce_cli_string(cli_description)),
493
- ):
494
- if cli_value is not None:
495
- merged_base[field] = cli_value
496
-
497
- # Determine final transport before validating config
498
- final_transport = merged_base.get("transport")
499
-
500
- # Merge config and authentication with validation
501
- _merge_config_field(merged_base, cli_config, final_transport)
502
- _merge_auth_field(merged_base, cli_auth)
503
-
504
- # Validate required fields
505
- missing_fields = []
506
- for required in ("name", "transport"):
507
- value = merged_base.get(required)
508
- if not isinstance(value, str) or not value.strip():
509
- missing_fields.append(required)
510
-
511
- return merged_base, missing_fields
512
-
513
-
514
- @mcps_group.command(name="list")
515
- @output_flags()
516
- @click.pass_context
517
- def list_mcps(ctx: Any) -> None:
518
- """List all MCPs in a formatted table.
519
-
520
- Args:
521
- ctx: Click context containing output format preferences
522
-
523
- Raises:
524
- ClickException: If API request fails
525
- """
526
- try:
527
- with with_client_and_spinner(
528
- ctx,
529
- "[bold blue]Fetching MCPs…[/bold blue]",
530
- console_override=console,
531
- ) as client:
532
- mcps = client.mcps.list_mcps()
533
-
534
- # Define table columns: (data_key, header, style, width)
535
- columns = [
536
- ("id", "ID", "dim", 36),
537
- ("name", "Name", ACCENT_STYLE, None),
538
- ("config", "Config", INFO, None),
539
- ]
540
-
541
- # Transform function for safe dictionary access
542
- def transform_mcp(mcp: Any) -> dict[str, Any]:
543
- """Transform an MCP object to a display row dictionary.
544
-
545
- Args:
546
- mcp: MCP object to transform.
547
-
548
- Returns:
549
- Dictionary with id, name, and config fields.
550
- """
551
- row = coerce_to_row(mcp, ["id", "name", "config"])
552
- # Ensure id is always a string
553
- row["id"] = str(row["id"])
554
- # Truncate config field for display
555
- if row["config"] != "N/A":
556
- row["config"] = str(row["config"])[:50] + "..." if len(str(row["config"])) > 50 else str(row["config"])
557
- return row
558
-
559
- output_list(ctx, mcps, "🔌 Available MCPs", columns, transform_mcp)
560
-
561
- except Exception as e:
562
- raise click.ClickException(str(e)) from e
563
-
564
-
565
- @mcps_group.command()
566
- @click.option("--name", help="MCP name")
567
- @click.option("--transport", help="MCP transport protocol")
568
- @click.option("--description", help="MCP description")
569
- @click.option(
570
- "--config",
571
- help="JSON configuration string or @file reference (e.g., @config.json)",
572
- )
573
- @click.option(
574
- "--auth",
575
- "--authentication",
576
- "auth",
577
- help="JSON authentication object or @file reference (e.g., @auth.json)",
578
- )
579
- @click.option(
580
- "--import",
581
- "import_file",
582
- type=click.Path(exists=True, dir_okay=False),
583
- help="Import MCP configuration from JSON or YAML export",
584
- )
585
- @output_flags()
586
- @click.pass_context
587
- def create(
588
- ctx: Any,
589
- name: str | None,
590
- transport: str | None,
591
- description: str | None,
592
- config: str | None,
593
- auth: str | None,
594
- import_file: str | None,
595
- ) -> None:
596
- r"""Create a new MCP with specified configuration.
597
-
598
- You can create an MCP by providing all parameters via CLI options, or by
599
- importing from a file and optionally overriding specific fields.
600
-
601
- Args:
602
- ctx: Click context containing output format preferences
603
- name: MCP name (required unless provided via --import)
604
- transport: MCP transport protocol (required unless provided via --import)
605
- description: Optional MCP description
606
- config: JSON configuration string or @file reference
607
- auth: JSON authentication object or @file reference
608
- import_file: Optional path to import configuration from export file.
609
- CLI options override imported values.
610
-
611
- Raises:
612
- ClickException: If JSON parsing fails or API request fails
613
-
614
- \b
615
- Examples:
616
- Create from CLI options:
617
- aip mcps create --name my-mcp --transport http --config '{"url": "https://api.example.com"}'
618
-
619
- Import from file:
620
- aip mcps create --import mcp-export.json
621
-
622
- Import with overrides:
623
- aip mcps create --import mcp-export.json --name new-name --transport sse
624
- """
625
- try:
626
- # Get API client instance for MCP operations
627
- api_client = get_client(ctx)
628
-
629
- # Process import file if specified, otherwise use None
630
- import_payload = _load_import_ready_payload(import_file) if import_file is not None else None
631
-
632
- merged_payload, missing_fields = _merge_import_payload(
633
- import_payload,
634
- cli_name=name,
635
- cli_transport=transport,
636
- cli_description=description,
637
- cli_config=config,
638
- cli_auth=auth,
639
- )
640
-
641
- if missing_fields:
642
- raise click.ClickException(
643
- "Missing required fields after combining import and CLI values: " + ", ".join(missing_fields)
644
- )
645
-
646
- effective_name = merged_payload["name"]
647
- effective_transport = merged_payload["transport"]
648
- effective_description = merged_payload.get("description")
649
- effective_config = merged_payload.get("config") or {}
650
- effective_auth = merged_payload.get("authentication")
651
-
652
- with spinner_context(
653
- ctx,
654
- "[bold blue]Creating MCP…[/bold blue]",
655
- console_override=console,
656
- ):
657
- create_kwargs: dict[str, Any] = {
658
- "name": effective_name,
659
- "config": effective_config,
660
- "transport": effective_transport,
661
- }
662
-
663
- if effective_description is not None:
664
- create_kwargs["description"] = effective_description
665
-
666
- if effective_auth:
667
- create_kwargs["authentication"] = effective_auth
668
-
669
- mcp_metadata = merged_payload.get("mcp_metadata")
670
- if mcp_metadata is not None:
671
- create_kwargs["mcp_metadata"] = mcp_metadata
672
-
673
- mcp = api_client.mcps.create_mcp(**create_kwargs)
674
-
675
- # Handle JSON output
676
- handle_json_output(ctx, mcp.model_dump())
677
-
678
- # Handle Rich output
679
- rich_panel = display_creation_success(
680
- "MCP",
681
- mcp.name,
682
- mcp.id,
683
- Type=getattr(mcp, "type", DEFAULT_MCP_TYPE),
684
- Transport=getattr(mcp, "transport", effective_transport),
685
- Description=effective_description or "No description",
686
- )
687
- handle_rich_output(ctx, rich_panel)
688
-
689
- except Exception as e:
690
- _handle_cli_error(ctx, e, "MCP creation")
691
-
692
-
693
- def _handle_mcp_export(
694
- ctx: Any,
695
- client: Any,
696
- mcp: Any,
697
- export_path: Path,
698
- no_auth_prompt: bool,
699
- auth_placeholder: str,
700
- ) -> None:
701
- """Handle MCP export to file with format detection and auth handling.
702
-
703
- Args:
704
- ctx: Click context for spinner management
705
- client: API client for fetching MCP details
706
- mcp: MCP object to export
707
- export_path: Target file path (format detected from extension)
708
- no_auth_prompt: Skip interactive secret prompts if True
709
- auth_placeholder: Placeholder text for missing secrets
710
-
711
- Note:
712
- Supports JSON (.json) and YAML (.yaml/.yml) export formats.
713
- In interactive mode, prompts for secret values.
714
- In non-interactive mode, uses placeholder values.
715
- """
716
- # Auto-detect format from file extension
717
- detected_format = detect_export_format(export_path)
718
-
719
- # Always export comprehensive data - re-fetch with full details
720
-
721
- mcp = fetch_resource_for_export(
722
- ctx,
723
- mcp,
724
- resource_type="MCP",
725
- get_by_id_func=client.mcps.get_mcp_by_id,
726
- console_override=console,
727
- )
728
-
729
- # Determine if we should prompt for secrets
730
- prompt_for_secrets = not no_auth_prompt and sys.stdin.isatty()
731
-
732
- # Warn user if non-interactive mode forces placeholder usage
733
- if not no_auth_prompt and not sys.stdin.isatty():
734
- print_markup(
735
- f"[{WARNING_STYLE}]⚠️ Non-interactive mode detected. Using placeholder values for secrets.[/]",
736
- console=console,
737
- )
738
-
739
- # Build and write export payload
740
- if prompt_for_secrets:
741
- # Interactive mode: no spinner during prompts
742
- export_payload = build_mcp_export_payload(
743
- mcp,
744
- prompt_for_secrets=prompt_for_secrets,
745
- placeholder=auth_placeholder,
746
- console=console,
747
- )
748
- with spinner_context(
749
- ctx,
750
- "[bold blue]Writing export file…[/bold blue]",
751
- console_override=console,
752
- ):
753
- write_resource_export(export_path, export_payload, detected_format)
754
- else:
755
- # Non-interactive mode: spinner for entire export process
756
- with spinner_context(
757
- ctx,
758
- "[bold blue]Exporting MCP configuration…[/bold blue]",
759
- console_override=console,
760
- ):
761
- export_payload = build_mcp_export_payload(
762
- mcp,
763
- prompt_for_secrets=prompt_for_secrets,
764
- placeholder=auth_placeholder,
765
- console=console,
766
- )
767
- write_resource_export(export_path, export_payload, detected_format)
768
-
769
- print_markup(
770
- f"[{SUCCESS_STYLE}]✅ Complete MCP configuration exported to: {export_path} (format: {detected_format})[/]",
771
- console=console,
772
- )
773
-
774
-
775
- def _display_mcp_details(ctx: Any, client: Any, mcp: Any) -> None:
776
- """Display MCP details using raw API data or fallback to Pydantic model.
777
-
778
- Args:
779
- ctx: Click context containing output format preferences
780
- client: API client for fetching raw MCP data
781
- mcp: MCP object to display details for
782
-
783
- Note:
784
- Attempts to fetch raw API data first to preserve all fields.
785
- Falls back to Pydantic model data if raw data unavailable.
786
- Formats datetime fields for better readability.
787
- """
788
- # Try to fetch raw API data first to preserve ALL fields
789
- with spinner_context(
790
- ctx,
791
- "[bold blue]Fetching detailed MCP data…[/bold blue]",
792
- console_override=console,
793
- ):
794
- raw_mcp_data = fetch_raw_resource_details(client, mcp, "mcps")
795
-
796
- if raw_mcp_data:
797
- # Use raw API data - this preserves ALL fields
798
- formatted_data = format_datetime_fields(raw_mcp_data)
799
-
800
- output_result(
801
- ctx,
802
- formatted_data,
803
- title="MCP Details",
804
- panel_title=f"🔌 {raw_mcp_data.get('name', 'Unknown')}",
805
- )
806
- else:
807
- # Fall back to Pydantic model data
808
- console.print(f"[{WARNING_STYLE}]Falling back to Pydantic model data[/]")
809
- result_data = {
810
- "id": str(getattr(mcp, "id", "N/A")),
811
- "name": getattr(mcp, "name", "N/A"),
812
- "type": getattr(mcp, "type", "N/A"),
813
- "config": getattr(mcp, "config", "N/A"),
814
- "status": getattr(mcp, "status", "N/A"),
815
- "connection_status": getattr(mcp, "connection_status", "N/A"),
816
- }
817
- output_result(ctx, result_data, title=f"🔌 {mcp.name}")
818
-
819
-
820
- @mcps_group.command()
821
- @click.argument("mcp_ref")
822
- @click.option(
823
- "--export",
824
- type=click.Path(dir_okay=False, writable=True),
825
- help="Export complete MCP configuration to file (format auto-detected from .json/.yaml extension)",
826
- )
827
- @click.option(
828
- "--no-auth-prompt",
829
- is_flag=True,
830
- help="Skip interactive secret prompts and use placeholder values.",
831
- )
832
- @click.option(
833
- "--auth-placeholder",
834
- default="<INSERT VALUE>",
835
- show_default=True,
836
- help="Placeholder text used when secrets are unavailable.",
837
- )
838
- @output_flags()
839
- @click.pass_context
840
- def get(
841
- ctx: Any,
842
- mcp_ref: str,
843
- export: str | None,
844
- no_auth_prompt: bool,
845
- auth_placeholder: str,
846
- ) -> None:
847
- r"""Get MCP details and optionally export configuration to file.
848
-
849
- Args:
850
- ctx: Click context containing output format preferences
851
- mcp_ref: MCP reference (ID or name)
852
- export: Optional file path to export MCP configuration
853
- no_auth_prompt: Skip interactive secret prompts if True
854
- auth_placeholder: Placeholder text for missing secrets
855
-
856
- Raises:
857
- ClickException: If MCP not found or export fails
858
-
859
- \b
860
- Examples:
861
- aip mcps get my-mcp
862
- aip mcps get my-mcp --export mcp.json # Export as JSON
863
- aip mcps get my-mcp --export mcp.yaml # Export as YAML
864
- """
865
- try:
866
- client = get_client(ctx)
867
-
868
- # Resolve MCP using helper function
869
- mcp = _resolve_mcp(ctx, client, mcp_ref)
870
-
871
- # Handle export option
872
- if export:
873
- _handle_mcp_export(ctx, client, mcp, Path(export), no_auth_prompt, auth_placeholder)
874
-
875
- # Display MCP details
876
- _display_mcp_details(ctx, client, mcp)
877
-
878
- except Exception as e:
879
- raise click.ClickException(str(e)) from e
880
-
881
-
882
- def _get_tools_from_config(ctx: Any, client: Any, config_file: str) -> tuple[list[dict[str, Any]], str]:
883
- """Get tools from MCP config file.
884
-
885
- Args:
886
- ctx: Click context
887
- client: GlaIP client instance
888
- config_file: Path to config file
889
-
890
- Returns:
891
- Tuple of (tools list, title string)
892
- """
893
- config_data = load_resource_from_file_with_validation(Path(config_file), "MCP config")
894
-
895
- # Validate config structure
896
- transport = config_data.get("transport")
897
- if "config" not in config_data:
898
- raise click.ClickException("Invalid MCP config: missing 'config' section in the file.")
899
- config_data["config"] = validate_mcp_config_structure(
900
- config_data["config"],
901
- transport=transport,
902
- source=config_file,
903
- )
904
-
905
- # Get tools from config without saving
906
- with spinner_context(
907
- ctx,
908
- "[bold blue]Fetching tools from config…[/bold blue]",
909
- console_override=console,
910
- ):
911
- tools = client.mcps.get_mcp_tools_from_config(config_data)
912
-
913
- title = f"{ICON_TOOL} Tools from config: {Path(config_file).name}"
914
- return tools, title
915
-
916
-
917
- def _get_tools_from_mcp(ctx: Any, client: Any, mcp_ref: str | None) -> tuple[list[dict[str, Any]], str]:
918
- """Get tools from saved MCP.
919
-
920
- Args:
921
- ctx: Click context
922
- client: GlaIP client instance
923
- mcp_ref: MCP reference (ID or name)
924
-
925
- Returns:
926
- Tuple of (tools list, title string)
927
- """
928
- mcp = _resolve_mcp(ctx, client, mcp_ref)
929
-
930
- with spinner_context(
931
- ctx,
932
- "[bold blue]Fetching MCP tools…[/bold blue]",
933
- console_override=console,
934
- ):
935
- tools = client.mcps.get_mcp_tools(mcp.id)
936
-
937
- title = f"{ICON_TOOL} Tools from MCP: {mcp.name}"
938
- return tools, title
939
-
940
-
941
- def _output_tool_names(ctx: Any, tools: list[dict[str, Any]]) -> None:
942
- """Output only tool names.
943
-
944
- Args:
945
- ctx: Click context
946
- tools: List of tool dictionaries
947
- """
948
- view = get_ctx_value(ctx, "view", "rich")
949
- tool_names = [tool.get("name", "N/A") for tool in tools]
950
-
951
- if view == "json":
952
- handle_json_output(ctx, tool_names)
953
- elif view == "plain":
954
- if tool_names:
955
- for name in tool_names:
956
- console.print(name, markup=False)
957
- console.print(f"Total: {len(tool_names)} tools", markup=False)
958
- else:
959
- console.print("No tools found", markup=False)
960
- else:
961
- if tool_names:
962
- for name in tool_names:
963
- console.print(name)
964
- console.print(f"[dim]Total: {len(tool_names)} tools[/dim]")
965
- else:
966
- console.print("[yellow]No tools found[/yellow]")
967
-
968
-
969
- def _transform_tool(tool: dict[str, Any]) -> dict[str, Any]:
970
- """Transform a tool dictionary to a display row dictionary.
971
-
972
- Args:
973
- tool: Tool dictionary to transform.
974
-
975
- Returns:
976
- Dictionary with name and description fields.
977
- """
978
- description = tool.get("description", "N/A")
979
- if len(description) > MAX_DESCRIPTION_LEN:
980
- description = description[: MAX_DESCRIPTION_LEN - 3] + "..."
981
- return {
982
- "name": tool.get("name", "N/A"),
983
- "description": description,
984
- }
985
-
986
-
987
- def _output_tools_table(ctx: Any, tools: list[dict[str, Any]], title: str) -> None:
988
- """Output tools in table format.
989
-
990
- Args:
991
- ctx: Click context
992
- tools: List of tool dictionaries
993
- title: Table title
994
- """
995
- columns = [
996
- ("name", "Name", ACCENT_STYLE, None),
997
- ("description", "Description", INFO, 50),
998
- ]
999
-
1000
- output_list(
1001
- ctx,
1002
- tools,
1003
- title,
1004
- columns,
1005
- _transform_tool,
1006
- )
1007
-
1008
-
1009
- def _validate_tool_command_args(mcp_ref: str | None, config_file: str | None) -> None:
1010
- """Validate that exactly one of mcp_ref or config_file is provided.
1011
-
1012
- Args:
1013
- mcp_ref: MCP reference (ID or name)
1014
- config_file: Path to config file
1015
-
1016
- Raises:
1017
- ClickException: If validation fails
1018
- """
1019
- if not mcp_ref and not config_file:
1020
- raise click.ClickException(
1021
- "Either MCP_REF or --from-config must be provided.\n"
1022
- "Examples:\n"
1023
- " aip mcps tools <MCP_ID>\n"
1024
- " aip mcps tools --from-config mcp-config.json"
1025
- )
1026
- if mcp_ref and config_file:
1027
- raise click.ClickException(
1028
- "Cannot use both MCP_REF and --from-config at the same time.\n"
1029
- "Use either:\n"
1030
- " aip mcps tools <MCP_ID>\n"
1031
- " aip mcps tools --from-config mcp-config.json"
1032
- )
1033
-
1034
-
1035
- @mcps_group.command("tools")
1036
- @click.argument("mcp_ref", required=False)
1037
- @click.option(
1038
- "--from-config",
1039
- "--config",
1040
- "config_file",
1041
- type=click.Path(exists=True, dir_okay=False),
1042
- help="Get tools from MCP config file without saving to DB (JSON or YAML)",
1043
- )
1044
- @click.option(
1045
- "--names-only",
1046
- is_flag=True,
1047
- help="Show only tool names (useful for allowed_tools config)",
1048
- )
1049
- @output_flags()
1050
- @click.pass_context
1051
- def list_tools(ctx: Any, mcp_ref: str | None, config_file: str | None, names_only: bool) -> None:
1052
- """List tools available from a specific MCP or config file.
1053
-
1054
- Args:
1055
- ctx: Click context containing output format preferences
1056
- mcp_ref: MCP reference (ID or name) - required if --from-config not used
1057
- config_file: Path to MCP config file - alternative to mcp_ref
1058
- names_only: Show only tool names instead of full table
1059
-
1060
- Raises:
1061
- ClickException: If MCP not found or tools fetch fails
1062
-
1063
- Examples:
1064
- Get tools from saved MCP:
1065
- aip mcps tools <MCP_ID>
1066
-
1067
- Get tools from config file (without saving to DB):
1068
- aip mcps tools --from-config mcp-config.json
1069
-
1070
- Get just tool names for allowed_tools config:
1071
- aip mcps tools <MCP_ID> --names-only
1072
- """
1073
- try:
1074
- _validate_tool_command_args(mcp_ref, config_file)
1075
- client = get_client(ctx)
1076
-
1077
- if config_file:
1078
- tools, title = _get_tools_from_config(ctx, client, config_file)
1079
- else:
1080
- tools, title = _get_tools_from_mcp(ctx, client, mcp_ref)
1081
-
1082
- if names_only:
1083
- _output_tool_names(ctx, tools)
1084
- else:
1085
- _output_tools_table(ctx, tools, title)
1086
-
1087
- except Exception as e:
1088
- raise click.ClickException(str(e)) from e
1089
-
1090
-
1091
- @mcps_group.command("connect")
1092
- @click.option(
1093
- "--from-file",
1094
- "config_file",
1095
- required=True,
1096
- help="MCP config JSON file",
1097
- )
1098
- @output_flags()
1099
- @click.pass_context
1100
- def connect(ctx: Any, config_file: str) -> None:
1101
- """Test MCP connection using a configuration file.
1102
-
1103
- Args:
1104
- ctx: Click context containing output format preferences
1105
- config_file: Path to MCP configuration JSON file
1106
-
1107
- Raises:
1108
- ClickException: If config file invalid or connection test fails
1109
-
1110
- Note:
1111
- Loads MCP configuration from JSON file and tests connectivity.
1112
- Displays success or failure with connection details.
1113
- """
1114
- try:
1115
- client = get_client(ctx)
1116
-
1117
- # Load MCP config from file
1118
- with open(config_file) as f:
1119
- config = json.load(f)
1120
-
1121
- view = get_ctx_value(ctx, "view", "rich")
1122
- if view != "json":
1123
- print_markup(
1124
- f"[{WARNING_STYLE}]Connecting to MCP with config from {config_file}...[/]",
1125
- console=console,
1126
- )
1127
-
1128
- # Test connection using config
1129
- with spinner_context(
1130
- ctx,
1131
- "[bold blue]Connecting to MCP…[/bold blue]",
1132
- console_override=console,
1133
- ):
1134
- result = client.mcps.test_mcp_connection_from_config(config)
1135
-
1136
- view = get_ctx_value(ctx, "view", "rich")
1137
- if view == "json":
1138
- handle_json_output(ctx, result)
1139
- else:
1140
- success_panel = AIPPanel(
1141
- f"[{SUCCESS_STYLE}]✓[/] MCP connection successful!\n\n[bold]Result:[/bold] {result}",
1142
- title="🔌 Connection",
1143
- border_style=SUCCESS,
1144
- )
1145
- console.print(success_panel)
1146
-
1147
- except Exception as e:
1148
- raise click.ClickException(str(e)) from e
1149
-
1150
-
1151
- def _generate_update_preview(mcp: Any, update_data: dict[str, Any], cli_overrides: dict[str, Any]) -> str:
1152
- """Generate formatted preview of changes for user confirmation.
1153
-
1154
- Args:
1155
- mcp: Current MCP object
1156
- update_data: Data that will be sent in update request
1157
- cli_overrides: CLI flags that were explicitly provided
1158
-
1159
- Returns:
1160
- Formatted preview string showing old→new values
1161
- """
1162
- lines = [f"\n[bold]The following fields will be updated for MCP '{mcp.name}':[/bold]\n"]
1163
-
1164
- empty_overrides = []
1165
-
1166
- # Show each field that will be updated
1167
- for field, new_value in update_data.items():
1168
- old_value = getattr(mcp, field, None)
1169
-
1170
- # Track empty CLI overrides
1171
- if field in cli_overrides and cli_overrides[field] == "":
1172
- empty_overrides.append(field)
1173
-
1174
- old_display = _format_preview_value(old_value)
1175
- new_display = _format_preview_value(new_value)
1176
-
1177
- lines.append(f"- [cyan]{field}[/cyan]: {old_display} → {new_display}")
1178
-
1179
- # Add warnings for empty CLI overrides
1180
- lines.extend(_build_empty_override_warnings(empty_overrides))
1181
-
1182
- return "\n".join(lines)
1183
-
1184
-
1185
- @mcps_group.command()
1186
- @click.argument("mcp_ref")
1187
- @click.option("--name", help="New MCP name")
1188
- @click.option("--transport", type=click.Choice(["http", "sse"]), help="New transport protocol")
1189
- @click.option("--description", help="New description")
1190
- @click.option(
1191
- "--config",
1192
- help="JSON configuration string or @file reference (e.g., @config.json)",
1193
- )
1194
- @click.option(
1195
- "--auth",
1196
- "--authentication",
1197
- "auth",
1198
- help="JSON authentication object or @file reference (e.g., @auth.json)",
1199
- )
1200
- @click.option(
1201
- "--import",
1202
- "import_file",
1203
- type=click.Path(exists=True, dir_okay=False, readable=True),
1204
- help="Import MCP configuration from JSON or YAML export",
1205
- )
1206
- @click.option("-y", is_flag=True, help="Skip confirmation prompt when using --import")
1207
- @output_flags()
1208
- @click.pass_context
1209
- def update(
1210
- ctx: Any,
1211
- mcp_ref: str,
1212
- name: str | None,
1213
- transport: str | None,
1214
- description: str | None,
1215
- config: str | None,
1216
- auth: str | None,
1217
- import_file: str | None,
1218
- y: bool,
1219
- ) -> None:
1220
- r"""Update an existing MCP with new configuration values.
1221
-
1222
- You can update an MCP by providing individual fields via CLI options, or by
1223
- importing from a file and optionally overriding specific fields.
1224
-
1225
- Args:
1226
- ctx: Click context containing output format preferences
1227
- mcp_ref: MCP reference (ID or name)
1228
- name: New MCP name (optional)
1229
- transport: New transport protocol (optional)
1230
- description: New description (optional)
1231
- config: New JSON configuration string or @file reference (optional)
1232
- auth: New JSON authentication object or @file reference (optional)
1233
- import_file: Optional path to import configuration from export file.
1234
- CLI options override imported values.
1235
- y: Skip confirmation prompt when using --import
1236
-
1237
- Raises:
1238
- ClickException: If MCP not found, JSON invalid, or no fields specified
1239
-
1240
- Note:
1241
- Must specify either --import OR at least one CLI field.
1242
- CLI options override imported values when both are specified.
1243
- Uses PATCH for import-based updates, PUT/PATCH for CLI-only updates.
1244
-
1245
- \b
1246
- Examples:
1247
- Update with CLI options:
1248
- aip mcps update my-mcp --name new-name --transport sse
1249
-
1250
- Import from file:
1251
- aip mcps update my-mcp --import mcp-export.json
1252
-
1253
- Import with overrides:
1254
- aip mcps update my-mcp --import mcp-export.json --name new-name -y
1255
- """
1256
- try:
1257
- client = get_client(ctx)
1258
-
1259
- # Validate that at least one update method is provided
1260
- cli_flags_provided = any(v is not None for v in [name, transport, description, config, auth])
1261
- if not import_file and not cli_flags_provided:
1262
- raise click.ClickException(
1263
- "No update fields specified. Use --import or one of: "
1264
- "--name, --transport, --description, --config, --auth"
1265
- )
1266
-
1267
- # Resolve MCP using helper function
1268
- mcp = _resolve_mcp(ctx, client, mcp_ref)
1269
-
1270
- # Load and validate import data if provided
1271
- import_payload = None
1272
- if import_file:
1273
- import_payload = _load_import_ready_payload(import_file)
1274
- if not _validate_import_payload_fields(import_payload):
1275
- return
1276
-
1277
- # Build update data from import and CLI flags
1278
- update_data = _build_update_data_from_sources(import_payload, mcp, name, transport, description, config, auth)
1279
-
1280
- if not update_data:
1281
- raise click.ClickException("No update fields specified")
1282
-
1283
- # Show confirmation preview for import-based updates (unless -y flag)
1284
- if import_payload and not y:
1285
- cli_overrides = _collect_cli_overrides(name, transport, description, config, auth)
1286
- preview = _generate_update_preview(mcp, update_data, cli_overrides)
1287
- print_markup(preview)
1288
-
1289
- if not click.confirm("\nContinue with update?", default=False):
1290
- print_markup("[yellow]Update cancelled.[/yellow]")
1291
- return
1292
-
1293
- # Update MCP
1294
- with spinner_context(
1295
- ctx,
1296
- "[bold blue]Updating MCP…[/bold blue]",
1297
- console_override=console,
1298
- ):
1299
- updated_mcp = client.mcps.update_mcp(mcp.id, **update_data)
1300
-
1301
- handle_json_output(ctx, updated_mcp.model_dump())
1302
- handle_rich_output(ctx, display_update_success("MCP", updated_mcp.name))
1303
-
1304
- except Exception as e:
1305
- _handle_cli_error(ctx, e, "MCP update")
1306
-
1307
-
1308
- @mcps_group.command()
1309
- @click.argument("mcp_ref")
1310
- @click.option("-y", "--yes", is_flag=True, help="Skip confirmation")
1311
- @output_flags()
1312
- @click.pass_context
1313
- def delete(ctx: Any, mcp_ref: str, yes: bool) -> None:
1314
- """Delete an MCP after confirmation.
1315
-
1316
- Args:
1317
- ctx: Click context containing output format preferences
1318
- mcp_ref: MCP reference (ID or name)
1319
- yes: Skip confirmation prompt if True
1320
-
1321
- Raises:
1322
- ClickException: If MCP not found or deletion fails
1323
-
1324
- Note:
1325
- Requires confirmation unless --yes flag is provided.
1326
- Deletion is permanent and cannot be undone.
1327
- """
1328
- try:
1329
- client = get_client(ctx)
1330
-
1331
- # Resolve MCP using helper function
1332
- mcp = _resolve_mcp(ctx, client, mcp_ref)
1333
-
1334
- # Confirm deletion
1335
- if not yes and not display_confirmation_prompt("MCP", mcp.name):
1336
- return
1337
-
1338
- with spinner_context(
1339
- ctx,
1340
- "[bold blue]Deleting MCP…[/bold blue]",
1341
- console_override=console,
1342
- ):
1343
- client.mcps.delete_mcp(mcp.id)
1344
-
1345
- handle_json_output(
1346
- ctx,
1347
- {
1348
- "success": True,
1349
- "message": f"MCP '{mcp.name}' deleted",
1350
- },
1351
- )
1352
- handle_rich_output(ctx, display_deletion_success("MCP", mcp.name))
1353
-
1354
- except Exception as e:
1355
- _handle_cli_error(ctx, e, "MCP deletion")