glaip-sdk 0.1.0__py3-none-any.whl → 0.6.10__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 (156) hide show
  1. glaip_sdk/__init__.py +5 -2
  2. glaip_sdk/_version.py +10 -3
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1191 -0
  5. glaip_sdk/branding.py +15 -6
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/agent_config.py +2 -6
  8. glaip_sdk/cli/auth.py +265 -45
  9. glaip_sdk/cli/commands/__init__.py +2 -2
  10. glaip_sdk/cli/commands/accounts.py +746 -0
  11. glaip_sdk/cli/commands/agents.py +251 -173
  12. glaip_sdk/cli/commands/common_config.py +101 -0
  13. glaip_sdk/cli/commands/configure.py +735 -143
  14. glaip_sdk/cli/commands/mcps.py +266 -134
  15. glaip_sdk/cli/commands/models.py +13 -9
  16. glaip_sdk/cli/commands/tools.py +67 -88
  17. glaip_sdk/cli/commands/transcripts.py +755 -0
  18. glaip_sdk/cli/commands/update.py +3 -8
  19. glaip_sdk/cli/config.py +49 -7
  20. glaip_sdk/cli/constants.py +38 -0
  21. glaip_sdk/cli/context.py +8 -0
  22. glaip_sdk/cli/core/__init__.py +79 -0
  23. glaip_sdk/cli/core/context.py +124 -0
  24. glaip_sdk/cli/core/output.py +846 -0
  25. glaip_sdk/cli/core/prompting.py +649 -0
  26. glaip_sdk/cli/core/rendering.py +187 -0
  27. glaip_sdk/cli/display.py +45 -32
  28. glaip_sdk/cli/hints.py +57 -0
  29. glaip_sdk/cli/io.py +14 -17
  30. glaip_sdk/cli/main.py +232 -143
  31. glaip_sdk/cli/masking.py +21 -33
  32. glaip_sdk/cli/mcp_validators.py +5 -15
  33. glaip_sdk/cli/pager.py +12 -19
  34. glaip_sdk/cli/parsers/__init__.py +1 -3
  35. glaip_sdk/cli/parsers/json_input.py +11 -22
  36. glaip_sdk/cli/resolution.py +3 -9
  37. glaip_sdk/cli/rich_helpers.py +1 -3
  38. glaip_sdk/cli/slash/__init__.py +0 -9
  39. glaip_sdk/cli/slash/accounts_controller.py +578 -0
  40. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  41. glaip_sdk/cli/slash/agent_session.py +65 -29
  42. glaip_sdk/cli/slash/prompt.py +24 -10
  43. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  44. glaip_sdk/cli/slash/session.py +807 -225
  45. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  46. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  47. glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
  48. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  49. glaip_sdk/cli/slash/tui/loading.py +58 -0
  50. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  51. glaip_sdk/cli/transcript/__init__.py +12 -52
  52. glaip_sdk/cli/transcript/cache.py +258 -60
  53. glaip_sdk/cli/transcript/capture.py +72 -21
  54. glaip_sdk/cli/transcript/history.py +815 -0
  55. glaip_sdk/cli/transcript/launcher.py +1 -3
  56. glaip_sdk/cli/transcript/viewer.py +79 -499
  57. glaip_sdk/cli/update_notifier.py +177 -24
  58. glaip_sdk/cli/utils.py +242 -1308
  59. glaip_sdk/cli/validators.py +16 -18
  60. glaip_sdk/client/__init__.py +2 -1
  61. glaip_sdk/client/_agent_payloads.py +53 -37
  62. glaip_sdk/client/agent_runs.py +147 -0
  63. glaip_sdk/client/agents.py +320 -92
  64. glaip_sdk/client/base.py +78 -35
  65. glaip_sdk/client/main.py +19 -10
  66. glaip_sdk/client/mcps.py +123 -15
  67. glaip_sdk/client/run_rendering.py +136 -101
  68. glaip_sdk/client/shared.py +21 -0
  69. glaip_sdk/client/tools.py +163 -34
  70. glaip_sdk/client/validators.py +20 -48
  71. glaip_sdk/config/constants.py +11 -0
  72. glaip_sdk/exceptions.py +1 -3
  73. glaip_sdk/mcps/__init__.py +21 -0
  74. glaip_sdk/mcps/base.py +345 -0
  75. glaip_sdk/models/__init__.py +90 -0
  76. glaip_sdk/models/agent.py +47 -0
  77. glaip_sdk/models/agent_runs.py +116 -0
  78. glaip_sdk/models/common.py +42 -0
  79. glaip_sdk/models/mcp.py +33 -0
  80. glaip_sdk/models/tool.py +33 -0
  81. glaip_sdk/payload_schemas/__init__.py +1 -13
  82. glaip_sdk/payload_schemas/agent.py +1 -3
  83. glaip_sdk/registry/__init__.py +55 -0
  84. glaip_sdk/registry/agent.py +164 -0
  85. glaip_sdk/registry/base.py +139 -0
  86. glaip_sdk/registry/mcp.py +253 -0
  87. glaip_sdk/registry/tool.py +232 -0
  88. glaip_sdk/rich_components.py +58 -2
  89. glaip_sdk/runner/__init__.py +59 -0
  90. glaip_sdk/runner/base.py +84 -0
  91. glaip_sdk/runner/deps.py +115 -0
  92. glaip_sdk/runner/langgraph.py +706 -0
  93. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  94. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  95. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  96. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  97. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  98. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  99. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  100. glaip_sdk/tools/__init__.py +22 -0
  101. glaip_sdk/tools/base.py +435 -0
  102. glaip_sdk/utils/__init__.py +58 -12
  103. glaip_sdk/utils/a2a/__init__.py +34 -0
  104. glaip_sdk/utils/a2a/event_processor.py +188 -0
  105. glaip_sdk/utils/agent_config.py +4 -14
  106. glaip_sdk/utils/bundler.py +267 -0
  107. glaip_sdk/utils/client.py +111 -0
  108. glaip_sdk/utils/client_utils.py +46 -28
  109. glaip_sdk/utils/datetime_helpers.py +58 -0
  110. glaip_sdk/utils/discovery.py +78 -0
  111. glaip_sdk/utils/display.py +25 -21
  112. glaip_sdk/utils/export.py +143 -0
  113. glaip_sdk/utils/general.py +1 -36
  114. glaip_sdk/utils/import_export.py +15 -16
  115. glaip_sdk/utils/import_resolver.py +492 -0
  116. glaip_sdk/utils/instructions.py +101 -0
  117. glaip_sdk/utils/rendering/__init__.py +115 -1
  118. glaip_sdk/utils/rendering/formatting.py +7 -35
  119. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  120. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
  121. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
  122. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  123. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  124. glaip_sdk/utils/rendering/models.py +3 -6
  125. glaip_sdk/utils/rendering/renderer/__init__.py +9 -49
  126. glaip_sdk/utils/rendering/renderer/base.py +258 -1577
  127. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  128. glaip_sdk/utils/rendering/renderer/debug.py +30 -34
  129. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  130. glaip_sdk/utils/rendering/renderer/stream.py +10 -51
  131. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  132. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  133. glaip_sdk/utils/rendering/renderer/toggle.py +1 -3
  134. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  135. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  136. glaip_sdk/utils/rendering/state.py +204 -0
  137. glaip_sdk/utils/rendering/step_tree_state.py +1 -3
  138. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  139. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +76 -517
  140. glaip_sdk/utils/rendering/steps/format.py +176 -0
  141. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  142. glaip_sdk/utils/rendering/timing.py +36 -0
  143. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  144. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  145. glaip_sdk/utils/resource_refs.py +29 -26
  146. glaip_sdk/utils/runtime_config.py +425 -0
  147. glaip_sdk/utils/serialization.py +32 -46
  148. glaip_sdk/utils/sync.py +142 -0
  149. glaip_sdk/utils/tool_detection.py +33 -0
  150. glaip_sdk/utils/validation.py +20 -28
  151. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
  152. glaip_sdk-0.6.10.dist-info/RECORD +159 -0
  153. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
  154. glaip_sdk/models.py +0 -259
  155. glaip_sdk-0.1.0.dist-info/RECORD +0 -82
  156. {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
@@ -43,17 +43,19 @@ from glaip_sdk.cli.resolution import resolve_resource_reference
43
43
  from glaip_sdk.cli.rich_helpers import print_markup
44
44
  from glaip_sdk.cli.utils import (
45
45
  coerce_to_row,
46
+ fetch_resource_for_export,
47
+ format_datetime_fields,
46
48
  get_client,
47
49
  output_list,
48
50
  output_result,
49
51
  spinner_context,
52
+ with_client_and_spinner,
50
53
  )
51
54
  from glaip_sdk.config.constants import (
52
55
  DEFAULT_MCP_TYPE,
53
56
  )
54
57
  from glaip_sdk.icons import ICON_TOOL
55
58
  from glaip_sdk.rich_components import AIPPanel
56
- from glaip_sdk.utils import format_datetime
57
59
  from glaip_sdk.utils.import_export import convert_export_to_import_format
58
60
  from glaip_sdk.utils.serialization import (
59
61
  build_mcp_export_payload,
@@ -61,6 +63,7 @@ from glaip_sdk.utils.serialization import (
61
63
  )
62
64
 
63
65
  console = Console()
66
+ MAX_DESCRIPTION_LEN = 50
64
67
 
65
68
 
66
69
  def _is_sensitive_data(val: Any) -> bool:
@@ -76,9 +79,7 @@ def _is_sensitive_data(val: Any) -> bool:
76
79
  return False
77
80
 
78
81
  sensitive_patterns = {"token", "password", "secret", "key", "credential"}
79
- return any(
80
- pattern in str(k).lower() for k in val.keys() for pattern in sensitive_patterns
81
- )
82
+ return any(pattern in str(k).lower() for k in val.keys() for pattern in sensitive_patterns)
82
83
 
83
84
 
84
85
  def _redact_sensitive_dict(val: dict[str, Any]) -> dict[str, Any]:
@@ -143,13 +144,8 @@ def _build_empty_override_warnings(empty_fields: list[str]) -> list[str]:
143
144
  if not empty_fields:
144
145
  return []
145
146
 
146
- warnings = [
147
- "\n[yellow]⚠️ Warning: Empty values provided via CLI will override import values[/yellow]"
148
- ]
149
- warnings.extend(
150
- f"- [yellow]{field}: will be set to empty string[/yellow]"
151
- for field in empty_fields
152
- )
147
+ warnings = ["\n[yellow]⚠️ Warning: Empty values provided via CLI will override import values[/yellow]"]
148
+ warnings.extend(f"- [yellow]{field}: will be set to empty string[/yellow]" for field in empty_fields)
153
149
  return warnings
154
150
 
155
151
 
@@ -250,9 +246,7 @@ def _build_update_data_from_sources(
250
246
  )
251
247
  if auth is not None:
252
248
  parsed_auth = parse_json_input(auth)
253
- update_data["authentication"] = validate_mcp_auth_structure(
254
- parsed_auth, source="--auth"
255
- )
249
+ update_data["authentication"] = validate_mcp_auth_structure(parsed_auth, source="--auth")
256
250
 
257
251
  return update_data
258
252
 
@@ -308,30 +302,37 @@ def mcps_group() -> None:
308
302
  pass
309
303
 
310
304
 
311
- def _resolve_mcp(
312
- ctx: Any, client: Any, ref: str, select: int | None = None
313
- ) -> Any | None:
314
- """Resolve MCP reference (ID or name) with ambiguity handling.
305
+ def _resolve_mcp(ctx: Any, client: Any, ref: str, select: int | None = None) -> Any | None:
306
+ """Resolve an MCP server by ID or name, with interactive selection support.
307
+
308
+ This function provides MCP-specific resolution logic. It delegates to
309
+ resolve_resource_reference for MCP-specific resolution, supporting UUID
310
+ lookups and name-based fuzzy matching.
315
311
 
316
312
  Args:
317
- ctx: Click context object
318
- client: API client instance
319
- ref: MCP reference (ID or name)
320
- select: Index to select when multiple matches found
313
+ ctx: Click context for command execution.
314
+ client: API client for backend operations.
315
+ ref: MCP identifier (UUID or name string).
316
+ select: Optional selection index when multiple MCPs match (1-based).
321
317
 
322
318
  Returns:
323
- MCP object if found, None otherwise
319
+ MCP instance if resolution succeeds, None if not found.
324
320
 
325
321
  Raises:
326
- ClickException: If MCP not found or selection invalid
322
+ click.ClickException: When resolution fails or selection is invalid.
327
323
  """
324
+ # Configure MCP-specific resolution functions
325
+ mcp_client = client.mcps
326
+ get_by_id_func = mcp_client.get_mcp_by_id
327
+ find_by_name_func = mcp_client.find_mcps
328
+ # Use MCP-specific resolution with standard fuzzy matching
328
329
  return resolve_resource_reference(
329
330
  ctx,
330
331
  client,
331
332
  ref,
332
333
  "mcp",
333
- client.mcps.get_mcp_by_id,
334
- client.mcps.find_mcps,
334
+ get_by_id_func,
335
+ find_by_name_func,
335
336
  "MCP",
336
337
  select=select,
337
338
  )
@@ -524,12 +525,11 @@ def list_mcps(ctx: Any) -> None:
524
525
  ClickException: If API request fails
525
526
  """
526
527
  try:
527
- client = get_client(ctx)
528
- with spinner_context(
528
+ with with_client_and_spinner(
529
529
  ctx,
530
530
  "[bold blue]Fetching MCPs…[/bold blue]",
531
531
  console_override=console,
532
- ):
532
+ ) as client:
533
533
  mcps = client.mcps.list_mcps()
534
534
 
535
535
  # Define table columns: (data_key, header, style, width)
@@ -541,22 +541,26 @@ def list_mcps(ctx: Any) -> None:
541
541
 
542
542
  # Transform function for safe dictionary access
543
543
  def transform_mcp(mcp: Any) -> dict[str, Any]:
544
+ """Transform an MCP object to a display row dictionary.
545
+
546
+ Args:
547
+ mcp: MCP object to transform.
548
+
549
+ Returns:
550
+ Dictionary with id, name, and config fields.
551
+ """
544
552
  row = coerce_to_row(mcp, ["id", "name", "config"])
545
553
  # Ensure id is always a string
546
554
  row["id"] = str(row["id"])
547
555
  # Truncate config field for display
548
556
  if row["config"] != "N/A":
549
- row["config"] = (
550
- str(row["config"])[:50] + "..."
551
- if len(str(row["config"])) > 50
552
- else str(row["config"])
553
- )
557
+ row["config"] = str(row["config"])[:50] + "..." if len(str(row["config"])) > 50 else str(row["config"])
554
558
  return row
555
559
 
556
560
  output_list(ctx, mcps, "🔌 Available MCPs", columns, transform_mcp)
557
561
 
558
562
  except Exception as e:
559
- raise click.ClickException(str(e))
563
+ raise click.ClickException(str(e)) from e
560
564
 
561
565
 
562
566
  @mcps_group.command()
@@ -590,7 +594,7 @@ def create(
590
594
  auth: str | None,
591
595
  import_file: str | None,
592
596
  ) -> None:
593
- """Create a new MCP with specified configuration.
597
+ r"""Create a new MCP with specified configuration.
594
598
 
595
599
  You can create an MCP by providing all parameters via CLI options, or by
596
600
  importing from a file and optionally overriding specific fields.
@@ -608,6 +612,7 @@ def create(
608
612
  Raises:
609
613
  ClickException: If JSON parsing fails or API request fails
610
614
 
615
+ \b
611
616
  Examples:
612
617
  Create from CLI options:
613
618
  aip mcps create --name my-mcp --transport http --config '{"url": "https://api.example.com"}'
@@ -619,11 +624,11 @@ def create(
619
624
  aip mcps create --import mcp-export.json --name new-name --transport sse
620
625
  """
621
626
  try:
622
- client = get_client(ctx)
627
+ # Get API client instance for MCP operations
628
+ api_client = get_client(ctx)
623
629
 
624
- import_payload = (
625
- _load_import_ready_payload(import_file) if import_file is not None else None
626
- )
630
+ # Process import file if specified, otherwise use None
631
+ import_payload = _load_import_ready_payload(import_file) if import_file is not None else None
627
632
 
628
633
  merged_payload, missing_fields = _merge_import_payload(
629
634
  import_payload,
@@ -636,8 +641,7 @@ def create(
636
641
 
637
642
  if missing_fields:
638
643
  raise click.ClickException(
639
- "Missing required fields after combining import and CLI values: "
640
- + ", ".join(missing_fields)
644
+ "Missing required fields after combining import and CLI values: " + ", ".join(missing_fields)
641
645
  )
642
646
 
643
647
  effective_name = merged_payload["name"]
@@ -667,7 +671,7 @@ def create(
667
671
  if mcp_metadata is not None:
668
672
  create_kwargs["mcp_metadata"] = mcp_metadata
669
673
 
670
- mcp = client.mcps.create_mcp(**create_kwargs)
674
+ mcp = api_client.mcps.create_mcp(**create_kwargs)
671
675
 
672
676
  # Handle JSON output
673
677
  handle_json_output(ctx, mcp.model_dump())
@@ -714,21 +718,14 @@ def _handle_mcp_export(
714
718
  detected_format = detect_export_format(export_path)
715
719
 
716
720
  # Always export comprehensive data - re-fetch with full details
717
- try:
718
- with spinner_context(
719
- ctx,
720
- "[bold blue]Fetching complete MCP details…[/bold blue]",
721
- console_override=console,
722
- ):
723
- mcp = client.mcps.get_mcp_by_id(mcp.id)
724
- except Exception as e:
725
- print_markup(
726
- f"[{WARNING_STYLE}]⚠️ Could not fetch full MCP details: {e}[/]",
727
- console=console,
728
- )
729
- print_markup(
730
- f"[{WARNING_STYLE}]⚠️ Proceeding with available data[/]", console=console
731
- )
721
+
722
+ mcp = fetch_resource_for_export(
723
+ ctx,
724
+ mcp,
725
+ resource_type="MCP",
726
+ get_by_id_func=client.mcps.get_mcp_by_id,
727
+ console_override=console,
728
+ )
732
729
 
733
730
  # Determine if we should prompt for secrets
734
731
  prompt_for_secrets = not no_auth_prompt and sys.stdin.isatty()
@@ -736,8 +733,7 @@ def _handle_mcp_export(
736
733
  # Warn user if non-interactive mode forces placeholder usage
737
734
  if not no_auth_prompt and not sys.stdin.isatty():
738
735
  print_markup(
739
- f"[{WARNING_STYLE}]⚠️ Non-interactive mode detected. "
740
- "Using placeholder values for secrets.[/]",
736
+ f"[{WARNING_STYLE}]⚠️ Non-interactive mode detected. Using placeholder values for secrets.[/]",
741
737
  console=console,
742
738
  )
743
739
 
@@ -772,8 +768,7 @@ def _handle_mcp_export(
772
768
  write_resource_export(export_path, export_payload, detected_format)
773
769
 
774
770
  print_markup(
775
- f"[{SUCCESS_STYLE}]✅ Complete MCP configuration exported to: "
776
- f"{export_path} (format: {detected_format})[/]",
771
+ f"[{SUCCESS_STYLE}]✅ Complete MCP configuration exported to: {export_path} (format: {detected_format})[/]",
777
772
  console=console,
778
773
  )
779
774
 
@@ -801,11 +796,7 @@ def _display_mcp_details(ctx: Any, client: Any, mcp: Any) -> None:
801
796
 
802
797
  if raw_mcp_data:
803
798
  # Use raw API data - this preserves ALL fields
804
- formatted_data = raw_mcp_data.copy()
805
- if "created_at" in formatted_data:
806
- formatted_data["created_at"] = format_datetime(formatted_data["created_at"])
807
- if "updated_at" in formatted_data:
808
- formatted_data["updated_at"] = format_datetime(formatted_data["updated_at"])
799
+ formatted_data = format_datetime_fields(raw_mcp_data)
809
800
 
810
801
  output_result(
811
802
  ctx,
@@ -832,8 +823,7 @@ def _display_mcp_details(ctx: Any, client: Any, mcp: Any) -> None:
832
823
  @click.option(
833
824
  "--export",
834
825
  type=click.Path(dir_okay=False, writable=True),
835
- help="Export complete MCP configuration to file "
836
- "(format auto-detected from .json/.yaml extension)",
826
+ help="Export complete MCP configuration to file (format auto-detected from .json/.yaml extension)",
837
827
  )
838
828
  @click.option(
839
829
  "--no-auth-prompt",
@@ -855,7 +845,7 @@ def get(
855
845
  no_auth_prompt: bool,
856
846
  auth_placeholder: str,
857
847
  ) -> None:
858
- """Get MCP details and optionally export configuration to file.
848
+ r"""Get MCP details and optionally export configuration to file.
859
849
 
860
850
  Args:
861
851
  ctx: Click context containing output format preferences
@@ -867,6 +857,7 @@ def get(
867
857
  Raises:
868
858
  ClickException: If MCP not found or export fails
869
859
 
860
+ \b
870
861
  Examples:
871
862
  aip mcps get my-mcp
872
863
  aip mcps get my-mcp --export mcp.json # Export as JSON
@@ -880,70 +871,222 @@ def get(
880
871
 
881
872
  # Handle export option
882
873
  if export:
883
- _handle_mcp_export(
884
- ctx, client, mcp, Path(export), no_auth_prompt, auth_placeholder
885
- )
874
+ _handle_mcp_export(ctx, client, mcp, Path(export), no_auth_prompt, auth_placeholder)
886
875
 
887
876
  # Display MCP details
888
877
  _display_mcp_details(ctx, client, mcp)
889
878
 
890
879
  except Exception as e:
891
- raise click.ClickException(str(e))
880
+ raise click.ClickException(str(e)) from e
881
+
882
+
883
+ def _get_tools_from_config(ctx: Any, client: Any, config_file: str) -> tuple[list[dict[str, Any]], str]:
884
+ """Get tools from MCP config file.
885
+
886
+ Args:
887
+ ctx: Click context
888
+ client: GlaIP client instance
889
+ config_file: Path to config file
890
+
891
+ Returns:
892
+ Tuple of (tools list, title string)
893
+ """
894
+ config_data = load_resource_from_file_with_validation(Path(config_file), "MCP config")
895
+
896
+ # Validate config structure
897
+ transport = config_data.get("transport")
898
+ if "config" not in config_data:
899
+ raise click.ClickException("Invalid MCP config: missing 'config' section in the file.")
900
+ config_data["config"] = validate_mcp_config_structure(
901
+ config_data["config"],
902
+ transport=transport,
903
+ source=config_file,
904
+ )
905
+
906
+ # Get tools from config without saving
907
+ with spinner_context(
908
+ ctx,
909
+ "[bold blue]Fetching tools from config…[/bold blue]",
910
+ console_override=console,
911
+ ):
912
+ tools = client.mcps.get_mcp_tools_from_config(config_data)
913
+
914
+ title = f"{ICON_TOOL} Tools from config: {Path(config_file).name}"
915
+ return tools, title
916
+
917
+
918
+ def _get_tools_from_mcp(ctx: Any, client: Any, mcp_ref: str | None) -> tuple[list[dict[str, Any]], str]:
919
+ """Get tools from saved MCP.
920
+
921
+ Args:
922
+ ctx: Click context
923
+ client: GlaIP client instance
924
+ mcp_ref: MCP reference (ID or name)
925
+
926
+ Returns:
927
+ Tuple of (tools list, title string)
928
+ """
929
+ mcp = _resolve_mcp(ctx, client, mcp_ref)
930
+
931
+ with spinner_context(
932
+ ctx,
933
+ "[bold blue]Fetching MCP tools…[/bold blue]",
934
+ console_override=console,
935
+ ):
936
+ tools = client.mcps.get_mcp_tools(mcp.id)
937
+
938
+ title = f"{ICON_TOOL} Tools from MCP: {mcp.name}"
939
+ return tools, title
940
+
941
+
942
+ def _output_tool_names(ctx: Any, tools: list[dict[str, Any]]) -> None:
943
+ """Output only tool names.
944
+
945
+ Args:
946
+ ctx: Click context
947
+ tools: List of tool dictionaries
948
+ """
949
+ view = get_ctx_value(ctx, "view", "rich")
950
+ tool_names = [tool.get("name", "N/A") for tool in tools]
951
+
952
+ if view == "json":
953
+ handle_json_output(ctx, tool_names)
954
+ elif view == "plain":
955
+ if tool_names:
956
+ for name in tool_names:
957
+ console.print(name, markup=False)
958
+ console.print(f"Total: {len(tool_names)} tools", markup=False)
959
+ else:
960
+ console.print("No tools found", markup=False)
961
+ else:
962
+ if tool_names:
963
+ for name in tool_names:
964
+ console.print(name)
965
+ console.print(f"[dim]Total: {len(tool_names)} tools[/dim]")
966
+ else:
967
+ console.print("[yellow]No tools found[/yellow]")
968
+
969
+
970
+ def _transform_tool(tool: dict[str, Any]) -> dict[str, Any]:
971
+ """Transform a tool dictionary to a display row dictionary.
972
+
973
+ Args:
974
+ tool: Tool dictionary to transform.
975
+
976
+ Returns:
977
+ Dictionary with name and description fields.
978
+ """
979
+ description = tool.get("description", "N/A")
980
+ if len(description) > MAX_DESCRIPTION_LEN:
981
+ description = description[: MAX_DESCRIPTION_LEN - 3] + "..."
982
+ return {
983
+ "name": tool.get("name", "N/A"),
984
+ "description": description,
985
+ }
986
+
987
+
988
+ def _output_tools_table(ctx: Any, tools: list[dict[str, Any]], title: str) -> None:
989
+ """Output tools in table format.
990
+
991
+ Args:
992
+ ctx: Click context
993
+ tools: List of tool dictionaries
994
+ title: Table title
995
+ """
996
+ columns = [
997
+ ("name", "Name", ACCENT_STYLE, None),
998
+ ("description", "Description", INFO, 50),
999
+ ]
1000
+
1001
+ output_list(
1002
+ ctx,
1003
+ tools,
1004
+ title,
1005
+ columns,
1006
+ _transform_tool,
1007
+ )
1008
+
1009
+
1010
+ def _validate_tool_command_args(mcp_ref: str | None, config_file: str | None) -> None:
1011
+ """Validate that exactly one of mcp_ref or config_file is provided.
1012
+
1013
+ Args:
1014
+ mcp_ref: MCP reference (ID or name)
1015
+ config_file: Path to config file
1016
+
1017
+ Raises:
1018
+ ClickException: If validation fails
1019
+ """
1020
+ if not mcp_ref and not config_file:
1021
+ raise click.ClickException(
1022
+ "Either MCP_REF or --from-config must be provided.\n"
1023
+ "Examples:\n"
1024
+ " aip mcps tools <MCP_ID>\n"
1025
+ " aip mcps tools --from-config mcp-config.json"
1026
+ )
1027
+ if mcp_ref and config_file:
1028
+ raise click.ClickException(
1029
+ "Cannot use both MCP_REF and --from-config at the same time.\n"
1030
+ "Use either:\n"
1031
+ " aip mcps tools <MCP_ID>\n"
1032
+ " aip mcps tools --from-config mcp-config.json"
1033
+ )
892
1034
 
893
1035
 
894
1036
  @mcps_group.command("tools")
895
- @click.argument("mcp_ref")
1037
+ @click.argument("mcp_ref", required=False)
1038
+ @click.option(
1039
+ "--from-config",
1040
+ "--config",
1041
+ "config_file",
1042
+ type=click.Path(exists=True, dir_okay=False),
1043
+ help="Get tools from MCP config file without saving to DB (JSON or YAML)",
1044
+ )
1045
+ @click.option(
1046
+ "--names-only",
1047
+ is_flag=True,
1048
+ help="Show only tool names (useful for allowed_tools config)",
1049
+ )
896
1050
  @output_flags()
897
1051
  @click.pass_context
898
- def list_tools(ctx: Any, mcp_ref: str) -> None:
899
- """List tools available from a specific MCP.
1052
+ def list_tools(ctx: Any, mcp_ref: str | None, config_file: str | None, names_only: bool) -> None:
1053
+ """List tools available from a specific MCP or config file.
900
1054
 
901
1055
  Args:
902
1056
  ctx: Click context containing output format preferences
903
- mcp_ref: MCP reference (ID or name)
1057
+ mcp_ref: MCP reference (ID or name) - required if --from-config not used
1058
+ config_file: Path to MCP config file - alternative to mcp_ref
1059
+ names_only: Show only tool names instead of full table
904
1060
 
905
1061
  Raises:
906
1062
  ClickException: If MCP not found or tools fetch fails
907
- """
908
- try:
909
- client = get_client(ctx)
910
1063
 
911
- # Resolve MCP using helper function
912
- mcp = _resolve_mcp(ctx, client, mcp_ref)
1064
+ Examples:
1065
+ Get tools from saved MCP:
1066
+ aip mcps tools <MCP_ID>
913
1067
 
914
- # Get tools from MCP
915
- with spinner_context(
916
- ctx,
917
- "[bold blue]Fetching MCP tools…[/bold blue]",
918
- console_override=console,
919
- ):
920
- tools = client.mcps.get_mcp_tools(mcp.id)
1068
+ Get tools from config file (without saving to DB):
1069
+ aip mcps tools --from-config mcp-config.json
921
1070
 
922
- # Define table columns: (data_key, header, style, width)
923
- columns = [
924
- ("name", "Name", ACCENT_STYLE, None),
925
- ("description", "Description", INFO, 50),
926
- ]
1071
+ Get just tool names for allowed_tools config:
1072
+ aip mcps tools <MCP_ID> --names-only
1073
+ """
1074
+ try:
1075
+ _validate_tool_command_args(mcp_ref, config_file)
1076
+ client = get_client(ctx)
927
1077
 
928
- # Transform function for safe dictionary access
929
- def transform_tool(tool: dict[str, Any]) -> dict[str, Any]:
930
- return {
931
- "name": tool.get("name", "N/A"),
932
- "description": tool.get("description", "N/A")[:47] + "..."
933
- if len(tool.get("description", "")) > 47
934
- else tool.get("description", "N/A"),
935
- }
1078
+ if config_file:
1079
+ tools, title = _get_tools_from_config(ctx, client, config_file)
1080
+ else:
1081
+ tools, title = _get_tools_from_mcp(ctx, client, mcp_ref)
936
1082
 
937
- output_list(
938
- ctx,
939
- tools,
940
- f"{ICON_TOOL} Tools from MCP: {mcp.name}",
941
- columns,
942
- transform_tool,
943
- )
1083
+ if names_only:
1084
+ _output_tool_names(ctx, tools)
1085
+ else:
1086
+ _output_tools_table(ctx, tools, title)
944
1087
 
945
1088
  except Exception as e:
946
- raise click.ClickException(str(e))
1089
+ raise click.ClickException(str(e)) from e
947
1090
 
948
1091
 
949
1092
  @mcps_group.command("connect")
@@ -996,20 +1139,17 @@ def connect(ctx: Any, config_file: str) -> None:
996
1139
  handle_json_output(ctx, result)
997
1140
  else:
998
1141
  success_panel = AIPPanel(
999
- f"[{SUCCESS_STYLE}]✓[/] MCP connection successful!\n\n"
1000
- f"[bold]Result:[/bold] {result}",
1142
+ f"[{SUCCESS_STYLE}]✓[/] MCP connection successful!\n\n[bold]Result:[/bold] {result}",
1001
1143
  title="🔌 Connection",
1002
1144
  border_style=SUCCESS,
1003
1145
  )
1004
1146
  console.print(success_panel)
1005
1147
 
1006
1148
  except Exception as e:
1007
- raise click.ClickException(str(e))
1149
+ raise click.ClickException(str(e)) from e
1008
1150
 
1009
1151
 
1010
- def _generate_update_preview(
1011
- mcp: Any, update_data: dict[str, Any], cli_overrides: dict[str, Any]
1012
- ) -> str:
1152
+ def _generate_update_preview(mcp: Any, update_data: dict[str, Any], cli_overrides: dict[str, Any]) -> str:
1013
1153
  """Generate formatted preview of changes for user confirmation.
1014
1154
 
1015
1155
  Args:
@@ -1020,9 +1160,7 @@ def _generate_update_preview(
1020
1160
  Returns:
1021
1161
  Formatted preview string showing old→new values
1022
1162
  """
1023
- lines = [
1024
- f"\n[bold]The following fields will be updated for MCP '{mcp.name}':[/bold]\n"
1025
- ]
1163
+ lines = [f"\n[bold]The following fields will be updated for MCP '{mcp.name}':[/bold]\n"]
1026
1164
 
1027
1165
  empty_overrides = []
1028
1166
 
@@ -1048,9 +1186,7 @@ def _generate_update_preview(
1048
1186
  @mcps_group.command()
1049
1187
  @click.argument("mcp_ref")
1050
1188
  @click.option("--name", help="New MCP name")
1051
- @click.option(
1052
- "--transport", type=click.Choice(["http", "sse"]), help="New transport protocol"
1053
- )
1189
+ @click.option("--transport", type=click.Choice(["http", "sse"]), help="New transport protocol")
1054
1190
  @click.option("--description", help="New description")
1055
1191
  @click.option(
1056
1192
  "--config",
@@ -1082,7 +1218,7 @@ def update(
1082
1218
  import_file: str | None,
1083
1219
  y: bool,
1084
1220
  ) -> None:
1085
- """Update an existing MCP with new configuration values.
1221
+ r"""Update an existing MCP with new configuration values.
1086
1222
 
1087
1223
  You can update an MCP by providing individual fields via CLI options, or by
1088
1224
  importing from a file and optionally overriding specific fields.
@@ -1107,6 +1243,7 @@ def update(
1107
1243
  CLI options override imported values when both are specified.
1108
1244
  Uses PATCH for import-based updates, PUT/PATCH for CLI-only updates.
1109
1245
 
1246
+ \b
1110
1247
  Examples:
1111
1248
  Update with CLI options:
1112
1249
  aip mcps update my-mcp --name new-name --transport sse
@@ -1121,12 +1258,11 @@ def update(
1121
1258
  client = get_client(ctx)
1122
1259
 
1123
1260
  # Validate that at least one update method is provided
1124
- cli_flags_provided = any(
1125
- v is not None for v in [name, transport, description, config, auth]
1126
- )
1261
+ cli_flags_provided = any(v is not None for v in [name, transport, description, config, auth])
1127
1262
  if not import_file and not cli_flags_provided:
1128
1263
  raise click.ClickException(
1129
- "No update fields specified. Use --import or one of: --name, --transport, --description, --config, --auth"
1264
+ "No update fields specified. Use --import or one of: "
1265
+ "--name, --transport, --description, --config, --auth"
1130
1266
  )
1131
1267
 
1132
1268
  # Resolve MCP using helper function
@@ -1140,18 +1276,14 @@ def update(
1140
1276
  return
1141
1277
 
1142
1278
  # Build update data from import and CLI flags
1143
- update_data = _build_update_data_from_sources(
1144
- import_payload, mcp, name, transport, description, config, auth
1145
- )
1279
+ update_data = _build_update_data_from_sources(import_payload, mcp, name, transport, description, config, auth)
1146
1280
 
1147
1281
  if not update_data:
1148
1282
  raise click.ClickException("No update fields specified")
1149
1283
 
1150
1284
  # Show confirmation preview for import-based updates (unless -y flag)
1151
1285
  if import_payload and not y:
1152
- cli_overrides = _collect_cli_overrides(
1153
- name, transport, description, config, auth
1154
- )
1286
+ cli_overrides = _collect_cli_overrides(name, transport, description, config, auth)
1155
1287
  preview = _generate_update_preview(mcp, update_data, cli_overrides)
1156
1288
  print_markup(preview)
1157
1289