glaip-sdk 0.6.19__py3-none-any.whl → 0.7.27__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 (135) hide show
  1. glaip_sdk/agents/base.py +283 -30
  2. glaip_sdk/agents/component.py +233 -0
  3. glaip_sdk/branding.py +113 -2
  4. glaip_sdk/cli/account_store.py +15 -0
  5. glaip_sdk/cli/auth.py +14 -8
  6. glaip_sdk/cli/commands/accounts.py +1 -1
  7. glaip_sdk/cli/commands/agents/__init__.py +116 -0
  8. glaip_sdk/cli/commands/agents/_common.py +562 -0
  9. glaip_sdk/cli/commands/agents/create.py +155 -0
  10. glaip_sdk/cli/commands/agents/delete.py +64 -0
  11. glaip_sdk/cli/commands/agents/get.py +89 -0
  12. glaip_sdk/cli/commands/agents/list.py +129 -0
  13. glaip_sdk/cli/commands/agents/run.py +264 -0
  14. glaip_sdk/cli/commands/agents/sync_langflow.py +72 -0
  15. glaip_sdk/cli/commands/agents/update.py +112 -0
  16. glaip_sdk/cli/commands/common_config.py +1 -1
  17. glaip_sdk/cli/commands/configure.py +1 -2
  18. glaip_sdk/cli/commands/mcps/__init__.py +94 -0
  19. glaip_sdk/cli/commands/mcps/_common.py +459 -0
  20. glaip_sdk/cli/commands/mcps/connect.py +82 -0
  21. glaip_sdk/cli/commands/mcps/create.py +152 -0
  22. glaip_sdk/cli/commands/mcps/delete.py +73 -0
  23. glaip_sdk/cli/commands/mcps/get.py +212 -0
  24. glaip_sdk/cli/commands/mcps/list.py +69 -0
  25. glaip_sdk/cli/commands/mcps/tools.py +235 -0
  26. glaip_sdk/cli/commands/mcps/update.py +190 -0
  27. glaip_sdk/cli/commands/models.py +2 -4
  28. glaip_sdk/cli/commands/shared/__init__.py +21 -0
  29. glaip_sdk/cli/commands/shared/formatters.py +91 -0
  30. glaip_sdk/cli/commands/tools/__init__.py +69 -0
  31. glaip_sdk/cli/commands/tools/_common.py +80 -0
  32. glaip_sdk/cli/commands/tools/create.py +228 -0
  33. glaip_sdk/cli/commands/tools/delete.py +61 -0
  34. glaip_sdk/cli/commands/tools/get.py +103 -0
  35. glaip_sdk/cli/commands/tools/list.py +69 -0
  36. glaip_sdk/cli/commands/tools/script.py +49 -0
  37. glaip_sdk/cli/commands/tools/update.py +102 -0
  38. glaip_sdk/cli/commands/transcripts/__init__.py +90 -0
  39. glaip_sdk/cli/commands/transcripts/_common.py +9 -0
  40. glaip_sdk/cli/commands/transcripts/clear.py +5 -0
  41. glaip_sdk/cli/commands/transcripts/detail.py +5 -0
  42. glaip_sdk/cli/commands/{transcripts.py → transcripts_original.py} +2 -1
  43. glaip_sdk/cli/commands/update.py +163 -17
  44. glaip_sdk/cli/config.py +1 -0
  45. glaip_sdk/cli/entrypoint.py +20 -0
  46. glaip_sdk/cli/main.py +112 -35
  47. glaip_sdk/cli/pager.py +3 -3
  48. glaip_sdk/cli/resolution.py +2 -1
  49. glaip_sdk/cli/slash/accounts_controller.py +3 -1
  50. glaip_sdk/cli/slash/agent_session.py +1 -1
  51. glaip_sdk/cli/slash/remote_runs_controller.py +3 -1
  52. glaip_sdk/cli/slash/session.py +343 -20
  53. glaip_sdk/cli/slash/tui/__init__.py +29 -1
  54. glaip_sdk/cli/slash/tui/accounts.tcss +97 -6
  55. glaip_sdk/cli/slash/tui/accounts_app.py +1117 -126
  56. glaip_sdk/cli/slash/tui/clipboard.py +316 -0
  57. glaip_sdk/cli/slash/tui/context.py +92 -0
  58. glaip_sdk/cli/slash/tui/indicators.py +341 -0
  59. glaip_sdk/cli/slash/tui/keybind_registry.py +235 -0
  60. glaip_sdk/cli/slash/tui/layouts/__init__.py +14 -0
  61. glaip_sdk/cli/slash/tui/layouts/harlequin.py +184 -0
  62. glaip_sdk/cli/slash/tui/loading.py +43 -21
  63. glaip_sdk/cli/slash/tui/remote_runs_app.py +178 -20
  64. glaip_sdk/cli/slash/tui/terminal.py +407 -0
  65. glaip_sdk/cli/slash/tui/theme/__init__.py +15 -0
  66. glaip_sdk/cli/slash/tui/theme/catalog.py +79 -0
  67. glaip_sdk/cli/slash/tui/theme/manager.py +112 -0
  68. glaip_sdk/cli/slash/tui/theme/tokens.py +55 -0
  69. glaip_sdk/cli/slash/tui/toast.py +388 -0
  70. glaip_sdk/cli/transcript/history.py +1 -1
  71. glaip_sdk/cli/transcript/viewer.py +1 -1
  72. glaip_sdk/cli/tui_settings.py +125 -0
  73. glaip_sdk/cli/update_notifier.py +215 -7
  74. glaip_sdk/cli/validators.py +1 -1
  75. glaip_sdk/client/__init__.py +2 -1
  76. glaip_sdk/client/_schedule_payloads.py +89 -0
  77. glaip_sdk/client/agents.py +293 -17
  78. glaip_sdk/client/base.py +25 -0
  79. glaip_sdk/client/hitl.py +136 -0
  80. glaip_sdk/client/main.py +7 -5
  81. glaip_sdk/client/mcps.py +44 -13
  82. glaip_sdk/client/payloads/agent/__init__.py +23 -0
  83. glaip_sdk/client/{_agent_payloads.py → payloads/agent/requests.py} +28 -48
  84. glaip_sdk/client/payloads/agent/responses.py +43 -0
  85. glaip_sdk/client/run_rendering.py +109 -30
  86. glaip_sdk/client/schedules.py +439 -0
  87. glaip_sdk/client/tools.py +52 -23
  88. glaip_sdk/config/constants.py +22 -2
  89. glaip_sdk/guardrails/__init__.py +80 -0
  90. glaip_sdk/guardrails/serializer.py +91 -0
  91. glaip_sdk/hitl/__init__.py +35 -2
  92. glaip_sdk/hitl/base.py +64 -0
  93. glaip_sdk/hitl/callback.py +43 -0
  94. glaip_sdk/hitl/local.py +1 -31
  95. glaip_sdk/hitl/remote.py +523 -0
  96. glaip_sdk/models/__init__.py +47 -1
  97. glaip_sdk/models/_provider_mappings.py +101 -0
  98. glaip_sdk/models/_validation.py +97 -0
  99. glaip_sdk/models/agent.py +2 -1
  100. glaip_sdk/models/agent_runs.py +2 -1
  101. glaip_sdk/models/constants.py +141 -0
  102. glaip_sdk/models/model.py +170 -0
  103. glaip_sdk/models/schedule.py +224 -0
  104. glaip_sdk/payload_schemas/agent.py +1 -0
  105. glaip_sdk/payload_schemas/guardrails.py +34 -0
  106. glaip_sdk/ptc.py +145 -0
  107. glaip_sdk/registry/tool.py +270 -57
  108. glaip_sdk/runner/__init__.py +20 -3
  109. glaip_sdk/runner/deps.py +4 -1
  110. glaip_sdk/runner/langgraph.py +251 -27
  111. glaip_sdk/runner/logging_config.py +77 -0
  112. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +30 -9
  113. glaip_sdk/runner/ptc_adapter.py +98 -0
  114. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +25 -2
  115. glaip_sdk/schedules/__init__.py +22 -0
  116. glaip_sdk/schedules/base.py +291 -0
  117. glaip_sdk/tools/base.py +67 -14
  118. glaip_sdk/utils/__init__.py +1 -0
  119. glaip_sdk/utils/agent_config.py +8 -2
  120. glaip_sdk/utils/bundler.py +138 -2
  121. glaip_sdk/utils/import_resolver.py +427 -49
  122. glaip_sdk/utils/runtime_config.py +3 -2
  123. glaip_sdk/utils/sync.py +31 -11
  124. glaip_sdk/utils/tool_detection.py +274 -6
  125. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/METADATA +22 -8
  126. glaip_sdk-0.7.27.dist-info/RECORD +227 -0
  127. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/WHEEL +1 -1
  128. glaip_sdk-0.7.27.dist-info/entry_points.txt +2 -0
  129. glaip_sdk/cli/commands/agents.py +0 -1509
  130. glaip_sdk/cli/commands/mcps.py +0 -1356
  131. glaip_sdk/cli/commands/tools.py +0 -576
  132. glaip_sdk/cli/utils.py +0 -263
  133. glaip_sdk-0.6.19.dist-info/RECORD +0 -163
  134. glaip_sdk-0.6.19.dist-info/entry_points.txt +0 -2
  135. {glaip_sdk-0.6.19.dist-info → glaip_sdk-0.7.27.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,73 @@
1
+ """Delete MCP command.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import click
12
+
13
+ from glaip_sdk.cli.context import output_flags
14
+ from glaip_sdk.cli.display import (
15
+ display_confirmation_prompt,
16
+ display_deletion_success,
17
+ handle_json_output,
18
+ handle_rich_output,
19
+ )
20
+ from glaip_sdk.cli.core.context import get_client
21
+ from glaip_sdk.cli.core.rendering import spinner_context
22
+
23
+ from ._common import _handle_cli_error, _resolve_mcp, console, mcps_group
24
+
25
+
26
+ @mcps_group.command()
27
+ @click.argument("mcp_ref")
28
+ @click.option("-y", "--yes", is_flag=True, help="Skip confirmation")
29
+ @output_flags()
30
+ @click.pass_context
31
+ def delete(ctx: Any, mcp_ref: str, yes: bool) -> None:
32
+ """Delete an MCP after confirmation.
33
+
34
+ Args:
35
+ ctx: Click context containing output format preferences
36
+ mcp_ref: MCP reference (ID or name)
37
+ yes: Skip confirmation prompt if True
38
+
39
+ Raises:
40
+ ClickException: If MCP not found or deletion fails
41
+
42
+ Note:
43
+ Requires confirmation unless --yes flag is provided.
44
+ Deletion is permanent and cannot be undone.
45
+ """
46
+ try:
47
+ client = get_client(ctx)
48
+
49
+ # Resolve MCP using helper function
50
+ mcp = _resolve_mcp(ctx, client, mcp_ref)
51
+
52
+ # Confirm deletion
53
+ if not yes and not display_confirmation_prompt("MCP", mcp.name):
54
+ return
55
+
56
+ with spinner_context(
57
+ ctx,
58
+ "[bold blue]Deleting MCP…[/bold blue]",
59
+ console_override=console,
60
+ ):
61
+ client.mcps.delete_mcp(mcp.id)
62
+
63
+ handle_json_output(
64
+ ctx,
65
+ {
66
+ "success": True,
67
+ "message": f"MCP '{mcp.name}' deleted",
68
+ },
69
+ )
70
+ handle_rich_output(ctx, display_deletion_success("MCP", mcp.name))
71
+
72
+ except Exception as e:
73
+ _handle_cli_error(ctx, e, "MCP deletion")
@@ -0,0 +1,212 @@
1
+ """Get MCP command.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import click
13
+
14
+ from glaip_sdk.branding import SUCCESS_STYLE, WARNING_STYLE
15
+ from glaip_sdk.cli.context import detect_export_format, output_flags
16
+ from glaip_sdk.cli.core.context import get_client
17
+ from glaip_sdk.cli.core.output import fetch_resource_for_export, format_datetime_fields, output_result
18
+ from glaip_sdk.cli.core.rendering import spinner_context
19
+ from glaip_sdk.cli.io import fetch_raw_resource_details
20
+ from glaip_sdk.cli.rich_helpers import print_markup
21
+ from glaip_sdk.utils.serialization import build_mcp_export_payload, write_resource_export
22
+ import sys
23
+
24
+ from ._common import _resolve_mcp, console, mcps_group
25
+
26
+
27
+ def _handle_mcp_export(
28
+ ctx: Any,
29
+ client: Any,
30
+ mcp: Any,
31
+ export_path: Path,
32
+ no_auth_prompt: bool,
33
+ auth_placeholder: str,
34
+ ) -> None:
35
+ """Handle MCP export to file with format detection and auth handling.
36
+
37
+ Args:
38
+ ctx: Click context for spinner management
39
+ client: API client for fetching MCP details
40
+ mcp: MCP object to export
41
+ export_path: Target file path (format detected from extension)
42
+ no_auth_prompt: Skip interactive secret prompts if True
43
+ auth_placeholder: Placeholder text for missing secrets
44
+
45
+ Note:
46
+ Supports JSON (.json) and YAML (.yaml/.yml) export formats.
47
+ In interactive mode, prompts for secret values.
48
+ In non-interactive mode, uses placeholder values.
49
+ """
50
+ # Auto-detect format from file extension
51
+ detected_format = detect_export_format(export_path)
52
+
53
+ # Always export comprehensive data - re-fetch with full details
54
+ mcp = fetch_resource_for_export(
55
+ ctx,
56
+ mcp,
57
+ resource_type="MCP",
58
+ get_by_id_func=client.mcps.get_mcp_by_id,
59
+ console_override=console,
60
+ )
61
+
62
+ # Determine if we should prompt for secrets
63
+ prompt_for_secrets = not no_auth_prompt and sys.stdin.isatty()
64
+
65
+ # Warn user if non-interactive mode forces placeholder usage
66
+ if not no_auth_prompt and not sys.stdin.isatty():
67
+ print_markup(
68
+ f"[{WARNING_STYLE}]⚠️ Non-interactive mode detected. Using placeholder values for secrets.[/]",
69
+ console=console,
70
+ )
71
+
72
+ # Build and write export payload
73
+ if prompt_for_secrets:
74
+ # Interactive mode: no spinner during prompts
75
+ export_payload = build_mcp_export_payload(
76
+ mcp,
77
+ prompt_for_secrets=prompt_for_secrets,
78
+ placeholder=auth_placeholder,
79
+ console=console,
80
+ )
81
+ with spinner_context(
82
+ ctx,
83
+ "[bold blue]Writing export file…[/bold blue]",
84
+ console_override=console,
85
+ ):
86
+ write_resource_export(export_path, export_payload, detected_format)
87
+ else:
88
+ # Non-interactive mode: spinner for entire export process
89
+ with spinner_context(
90
+ ctx,
91
+ "[bold blue]Exporting MCP configuration…[/bold blue]",
92
+ console_override=console,
93
+ ):
94
+ export_payload = build_mcp_export_payload(
95
+ mcp,
96
+ prompt_for_secrets=prompt_for_secrets,
97
+ placeholder=auth_placeholder,
98
+ console=console,
99
+ )
100
+ write_resource_export(export_path, export_payload, detected_format)
101
+
102
+ print_markup(
103
+ f"[{SUCCESS_STYLE}]✅ Complete MCP configuration exported to: {export_path} (format: {detected_format})[/]",
104
+ console=console,
105
+ )
106
+
107
+
108
+ def _display_mcp_details(ctx: Any, client: Any, mcp: Any) -> None:
109
+ """Display MCP details using raw API data or fallback to Pydantic model.
110
+
111
+ Args:
112
+ ctx: Click context containing output format preferences
113
+ client: API client for fetching raw MCP data
114
+ mcp: MCP object to display details for
115
+
116
+ Note:
117
+ Attempts to fetch raw API data first to preserve all fields.
118
+ Falls back to Pydantic model data if raw data unavailable.
119
+ Formats datetime fields for better readability.
120
+ """
121
+ # Try to fetch raw API data first to preserve ALL fields
122
+ with spinner_context(
123
+ ctx,
124
+ "[bold blue]Fetching detailed MCP data…[/bold blue]",
125
+ console_override=console,
126
+ ):
127
+ raw_mcp_data = fetch_raw_resource_details(client, mcp, "mcps")
128
+
129
+ if raw_mcp_data:
130
+ # Use raw API data - this preserves ALL fields
131
+ formatted_data = format_datetime_fields(raw_mcp_data)
132
+
133
+ output_result(
134
+ ctx,
135
+ formatted_data,
136
+ title="MCP Details",
137
+ panel_title=f"🔌 {raw_mcp_data.get('name', 'Unknown')}",
138
+ )
139
+ else:
140
+ # Fall back to Pydantic model data
141
+ console.print(f"[{WARNING_STYLE}]Falling back to Pydantic model data[/]")
142
+ result_data = {
143
+ "id": str(getattr(mcp, "id", "N/A")),
144
+ "name": getattr(mcp, "name", "N/A"),
145
+ "type": getattr(mcp, "type", "N/A"),
146
+ "config": getattr(mcp, "config", "N/A"),
147
+ "status": getattr(mcp, "status", "N/A"),
148
+ "connection_status": getattr(mcp, "connection_status", "N/A"),
149
+ }
150
+ output_result(ctx, result_data, title=f"🔌 {mcp.name}")
151
+
152
+
153
+ @mcps_group.command()
154
+ @click.argument("mcp_ref")
155
+ @click.option(
156
+ "--export",
157
+ type=click.Path(dir_okay=False, writable=True),
158
+ help="Export complete MCP configuration to file (format auto-detected from .json/.yaml extension)",
159
+ )
160
+ @click.option(
161
+ "--no-auth-prompt",
162
+ is_flag=True,
163
+ help="Skip interactive secret prompts and use placeholder values.",
164
+ )
165
+ @click.option(
166
+ "--auth-placeholder",
167
+ default="<INSERT VALUE>",
168
+ show_default=True,
169
+ help="Placeholder text used when secrets are unavailable.",
170
+ )
171
+ @output_flags()
172
+ @click.pass_context
173
+ def get(
174
+ ctx: Any,
175
+ mcp_ref: str,
176
+ export: str | None,
177
+ no_auth_prompt: bool,
178
+ auth_placeholder: str,
179
+ ) -> None:
180
+ r"""Get MCP details and optionally export configuration to file.
181
+
182
+ Args:
183
+ ctx: Click context containing output format preferences
184
+ mcp_ref: MCP reference (ID or name)
185
+ export: Optional file path to export MCP configuration
186
+ no_auth_prompt: Skip interactive secret prompts if True
187
+ auth_placeholder: Placeholder text for missing secrets
188
+
189
+ Raises:
190
+ ClickException: If MCP not found or export fails
191
+
192
+ \b
193
+ Examples:
194
+ aip mcps get my-mcp
195
+ aip mcps get my-mcp --export mcp.json # Export as JSON
196
+ aip mcps get my-mcp --export mcp.yaml # Export as YAML
197
+ """
198
+ try:
199
+ client = get_client(ctx)
200
+
201
+ # Resolve MCP using helper function
202
+ mcp = _resolve_mcp(ctx, client, mcp_ref)
203
+
204
+ # Handle export option
205
+ if export:
206
+ _handle_mcp_export(ctx, client, mcp, Path(export), no_auth_prompt, auth_placeholder)
207
+
208
+ # Display MCP details
209
+ _display_mcp_details(ctx, client, mcp)
210
+
211
+ except Exception as e:
212
+ raise click.ClickException(str(e)) from e
@@ -0,0 +1,69 @@
1
+ """List MCPs command.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import click
12
+
13
+ from glaip_sdk.branding import ACCENT_STYLE, INFO
14
+ from glaip_sdk.cli.context import output_flags
15
+ from glaip_sdk.cli.core.output import coerce_to_row, output_list
16
+ from glaip_sdk.cli.core.rendering import with_client_and_spinner
17
+
18
+ from ._common import console, mcps_group
19
+
20
+
21
+ @mcps_group.command(name="list")
22
+ @output_flags()
23
+ @click.pass_context
24
+ def list_mcps(ctx: Any) -> None:
25
+ """List all MCPs in a formatted table.
26
+
27
+ Args:
28
+ ctx: Click context containing output format preferences
29
+
30
+ Raises:
31
+ ClickException: If API request fails
32
+ """
33
+ try:
34
+ with with_client_and_spinner(
35
+ ctx,
36
+ "[bold blue]Fetching MCPs…[/bold blue]",
37
+ console_override=console,
38
+ ) as client:
39
+ mcps = client.mcps.list_mcps()
40
+
41
+ # Define table columns: (data_key, header, style, width)
42
+ columns = [
43
+ ("id", "ID", "dim", 36),
44
+ ("name", "Name", ACCENT_STYLE, None),
45
+ ("config", "Config", INFO, None),
46
+ ]
47
+
48
+ # Transform function for safe dictionary access
49
+ def transform_mcp(mcp: Any) -> dict[str, Any]:
50
+ """Transform an MCP object to a display row dictionary.
51
+
52
+ Args:
53
+ mcp: MCP object to transform.
54
+
55
+ Returns:
56
+ Dictionary with id, name, and config fields.
57
+ """
58
+ row = coerce_to_row(mcp, ["id", "name", "config"])
59
+ # Ensure id is always a string
60
+ row["id"] = str(row["id"])
61
+ # Truncate config field for display
62
+ if row["config"] != "N/A":
63
+ row["config"] = str(row["config"])[:50] + "..." if len(str(row["config"])) > 50 else str(row["config"])
64
+ return row
65
+
66
+ output_list(ctx, mcps, "🔌 Available MCPs", columns, transform_mcp)
67
+
68
+ except Exception as e:
69
+ raise click.ClickException(str(e)) from e
@@ -0,0 +1,235 @@
1
+ """List MCP tools command.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import click
13
+
14
+ from glaip_sdk.branding import ACCENT_STYLE, INFO
15
+ from glaip_sdk.cli.context import get_ctx_value, output_flags
16
+ from glaip_sdk.cli.core.context import get_client
17
+ from glaip_sdk.cli.core.output import output_list
18
+ from glaip_sdk.cli.core.rendering import spinner_context
19
+ from glaip_sdk.cli.display import handle_json_output
20
+ from glaip_sdk.cli.io import load_resource_from_file_with_validation
21
+ from glaip_sdk.cli.mcp_validators import validate_mcp_config_structure
22
+ from glaip_sdk.icons import ICON_TOOL
23
+
24
+ from ._common import _resolve_mcp, console, mcps_group
25
+
26
+ MAX_DESCRIPTION_LEN = 50
27
+
28
+
29
+ def _get_tools_from_config(ctx: Any, client: Any, config_file: str) -> tuple[list[dict[str, Any]], str]:
30
+ """Get tools from MCP config file.
31
+
32
+ Args:
33
+ ctx: Click context
34
+ client: GlaIP client instance
35
+ config_file: Path to config file
36
+
37
+ Returns:
38
+ Tuple of (tools list, title string)
39
+ """
40
+ config_data = load_resource_from_file_with_validation(Path(config_file), "MCP config")
41
+
42
+ # Validate config structure
43
+ transport = config_data.get("transport")
44
+ if "config" not in config_data:
45
+ raise click.ClickException("Invalid MCP config: missing 'config' section in the file.")
46
+ config_data["config"] = validate_mcp_config_structure(
47
+ config_data["config"],
48
+ transport=transport,
49
+ source=config_file,
50
+ )
51
+
52
+ # Get tools from config without saving
53
+ with spinner_context(
54
+ ctx,
55
+ "[bold blue]Fetching tools from config…[/bold blue]",
56
+ console_override=console,
57
+ ):
58
+ tools = client.mcps.get_mcp_tools_from_config(config_data)
59
+
60
+ title = f"{ICON_TOOL} Tools from config: {Path(config_file).name}"
61
+ return tools, title
62
+
63
+
64
+ def _get_tools_from_mcp(ctx: Any, client: Any, mcp_ref: str | None) -> tuple[list[dict[str, Any]], str]:
65
+ """Get tools from saved MCP.
66
+
67
+ Args:
68
+ ctx: Click context
69
+ client: GlaIP client instance
70
+ mcp_ref: MCP reference (ID or name)
71
+
72
+ Returns:
73
+ Tuple of (tools list, title string)
74
+ """
75
+ mcp = _resolve_mcp(ctx, client, mcp_ref)
76
+
77
+ with spinner_context(
78
+ ctx,
79
+ "[bold blue]Fetching MCP tools…[/bold blue]",
80
+ console_override=console,
81
+ ):
82
+ tools = client.mcps.get_mcp_tools(mcp.id)
83
+
84
+ title = f"{ICON_TOOL} Tools from MCP: {mcp.name}"
85
+ return tools, title
86
+
87
+
88
+ def _output_tool_names(ctx: Any, tools: list[dict[str, Any]]) -> None:
89
+ """Output only tool names.
90
+
91
+ Args:
92
+ ctx: Click context
93
+ tools: List of tool dictionaries
94
+ """
95
+ view = get_ctx_value(ctx, "view", "rich")
96
+ tool_names = [tool.get("name", "N/A") for tool in tools]
97
+
98
+ if view == "json":
99
+ handle_json_output(ctx, tool_names)
100
+ elif view == "plain":
101
+ if tool_names:
102
+ for name in tool_names:
103
+ console.print(name, markup=False)
104
+ console.print(f"Total: {len(tool_names)} tools", markup=False)
105
+ else:
106
+ console.print("No tools found", markup=False)
107
+ else:
108
+ if tool_names:
109
+ for name in tool_names:
110
+ console.print(name)
111
+ console.print(f"[dim]Total: {len(tool_names)} tools[/dim]")
112
+ else:
113
+ console.print("[yellow]No tools found[/yellow]")
114
+
115
+
116
+ def _transform_tool(tool: dict[str, Any]) -> dict[str, Any]:
117
+ """Transform a tool dictionary to a display row dictionary.
118
+
119
+ Args:
120
+ tool: Tool dictionary to transform.
121
+
122
+ Returns:
123
+ Dictionary with name and description fields.
124
+ """
125
+ description = tool.get("description", "N/A")
126
+ if len(description) > MAX_DESCRIPTION_LEN:
127
+ description = description[: MAX_DESCRIPTION_LEN - 3] + "..."
128
+ return {
129
+ "name": tool.get("name", "N/A"),
130
+ "description": description,
131
+ }
132
+
133
+
134
+ def _output_tools_table(ctx: Any, tools: list[dict[str, Any]], title: str) -> None:
135
+ """Output tools in table format.
136
+
137
+ Args:
138
+ ctx: Click context
139
+ tools: List of tool dictionaries
140
+ title: Table title
141
+ """
142
+ columns = [
143
+ ("name", "Name", ACCENT_STYLE, None),
144
+ ("description", "Description", INFO, 50),
145
+ ]
146
+
147
+ output_list(
148
+ ctx,
149
+ tools,
150
+ title,
151
+ columns,
152
+ _transform_tool,
153
+ )
154
+
155
+
156
+ def _validate_tool_command_args(mcp_ref: str | None, config_file: str | None) -> None:
157
+ """Validate that exactly one of mcp_ref or config_file is provided.
158
+
159
+ Args:
160
+ mcp_ref: MCP reference (ID or name)
161
+ config_file: Path to config file
162
+
163
+ Raises:
164
+ ClickException: If validation fails
165
+ """
166
+ if not mcp_ref and not config_file:
167
+ raise click.ClickException(
168
+ "Either MCP_REF or --from-config must be provided.\n"
169
+ "Examples:\n"
170
+ " aip mcps tools <MCP_ID>\n"
171
+ " aip mcps tools --from-config mcp-config.json"
172
+ )
173
+ if mcp_ref and config_file:
174
+ raise click.ClickException(
175
+ "Cannot use both MCP_REF and --from-config at the same time.\n"
176
+ "Use either:\n"
177
+ " aip mcps tools <MCP_ID>\n"
178
+ " aip mcps tools --from-config mcp-config.json"
179
+ )
180
+
181
+
182
+ @mcps_group.command("tools")
183
+ @click.argument("mcp_ref", required=False)
184
+ @click.option(
185
+ "--from-config",
186
+ "--config",
187
+ "config_file",
188
+ type=click.Path(exists=True, dir_okay=False),
189
+ help="Get tools from MCP config file without saving to DB (JSON or YAML)",
190
+ )
191
+ @click.option(
192
+ "--names-only",
193
+ is_flag=True,
194
+ help="Show only tool names (useful for allowed_tools config)",
195
+ )
196
+ @output_flags()
197
+ @click.pass_context
198
+ def list_tools(ctx: Any, mcp_ref: str | None, config_file: str | None, names_only: bool) -> None:
199
+ """List tools available from a specific MCP or config file.
200
+
201
+ Args:
202
+ ctx: Click context containing output format preferences
203
+ mcp_ref: MCP reference (ID or name) - required if --from-config not used
204
+ config_file: Path to MCP config file - alternative to mcp_ref
205
+ names_only: Show only tool names instead of full table
206
+
207
+ Raises:
208
+ ClickException: If MCP not found or tools fetch fails
209
+
210
+ Examples:
211
+ Get tools from saved MCP:
212
+ aip mcps tools <MCP_ID>
213
+
214
+ Get tools from config file (without saving to DB):
215
+ aip mcps tools --from-config mcp-config.json
216
+
217
+ Get just tool names for allowed_tools config:
218
+ aip mcps tools <MCP_ID> --names-only
219
+ """
220
+ try:
221
+ _validate_tool_command_args(mcp_ref, config_file)
222
+ client = get_client(ctx)
223
+
224
+ if config_file:
225
+ tools, title = _get_tools_from_config(ctx, client, config_file)
226
+ else:
227
+ tools, title = _get_tools_from_mcp(ctx, client, mcp_ref)
228
+
229
+ if names_only:
230
+ _output_tool_names(ctx, tools)
231
+ else:
232
+ _output_tools_table(ctx, tools, title)
233
+
234
+ except Exception as e:
235
+ raise click.ClickException(str(e)) from e