glaip-sdk 0.3.0__py3-none-any.whl → 0.5.0__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 (58) hide show
  1. glaip_sdk/cli/account_store.py +522 -0
  2. glaip_sdk/cli/auth.py +224 -8
  3. glaip_sdk/cli/commands/accounts.py +414 -0
  4. glaip_sdk/cli/commands/agents.py +2 -2
  5. glaip_sdk/cli/commands/common_config.py +65 -0
  6. glaip_sdk/cli/commands/configure.py +153 -87
  7. glaip_sdk/cli/commands/mcps.py +191 -44
  8. glaip_sdk/cli/commands/transcripts.py +1 -1
  9. glaip_sdk/cli/config.py +31 -3
  10. glaip_sdk/cli/display.py +1 -1
  11. glaip_sdk/cli/hints.py +57 -0
  12. glaip_sdk/cli/io.py +6 -3
  13. glaip_sdk/cli/main.py +181 -79
  14. glaip_sdk/cli/masking.py +14 -1
  15. glaip_sdk/cli/slash/agent_session.py +2 -1
  16. glaip_sdk/cli/slash/remote_runs_controller.py +1 -1
  17. glaip_sdk/cli/slash/session.py +11 -9
  18. glaip_sdk/cli/slash/tui/remote_runs_app.py +2 -3
  19. glaip_sdk/cli/transcript/capture.py +12 -18
  20. glaip_sdk/cli/transcript/viewer.py +13 -646
  21. glaip_sdk/cli/update_notifier.py +2 -1
  22. glaip_sdk/cli/utils.py +95 -139
  23. glaip_sdk/client/agents.py +2 -4
  24. glaip_sdk/client/main.py +2 -18
  25. glaip_sdk/client/mcps.py +11 -1
  26. glaip_sdk/client/run_rendering.py +90 -111
  27. glaip_sdk/client/shared.py +21 -0
  28. glaip_sdk/models.py +8 -7
  29. glaip_sdk/utils/display.py +23 -15
  30. glaip_sdk/utils/rendering/__init__.py +6 -13
  31. glaip_sdk/utils/rendering/formatting.py +5 -30
  32. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  33. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  34. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  35. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  36. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  37. glaip_sdk/utils/rendering/models.py +1 -0
  38. glaip_sdk/utils/rendering/renderer/__init__.py +10 -28
  39. glaip_sdk/utils/rendering/renderer/base.py +214 -1469
  40. glaip_sdk/utils/rendering/renderer/debug.py +24 -0
  41. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  42. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  43. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  44. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  45. glaip_sdk/utils/rendering/state.py +204 -0
  46. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  47. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  48. glaip_sdk/utils/rendering/steps/format.py +176 -0
  49. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  50. glaip_sdk/utils/rendering/timing.py +36 -0
  51. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  52. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  53. glaip_sdk/utils/validation.py +13 -21
  54. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/METADATA +1 -1
  55. glaip_sdk-0.5.0.dist-info/RECORD +113 -0
  56. glaip_sdk-0.3.0.dist-info/RECORD +0 -94
  57. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/WHEEL +0 -0
  58. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.5.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,65 @@
1
+ """Shared helpers for configuration/account flows."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from rich.text import Text
6
+
7
+ from glaip_sdk import Client
8
+ from glaip_sdk.branding import PRIMARY, SUCCESS_STYLE, WARNING_STYLE, AIPBranding
9
+ from glaip_sdk.cli.utils import sdk_version
10
+
11
+
12
+ def render_branding_header(console: Console, rule_text: str) -> None:
13
+ """Render the standard CLI branding header with a custom rule text."""
14
+ branding = AIPBranding.create_from_sdk(sdk_version=sdk_version(), package_name="glaip-sdk")
15
+ heading = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
16
+ console.print(heading)
17
+ console.print()
18
+ console.print(branding.get_welcome_banner())
19
+ console.rule(rule_text, style=PRIMARY)
20
+
21
+
22
+ def check_connection(
23
+ api_url: str,
24
+ api_key: str,
25
+ console: Console,
26
+ *,
27
+ abort_on_error: bool = False,
28
+ extra_hint: str | None = None,
29
+ ) -> bool:
30
+ """Test connectivity and report results.
31
+
32
+ Returns True on success, False on handled failures. Raises click.Abort when
33
+ abort_on_error is True and a fatal error occurs.
34
+ """
35
+ console.print("\n🔌 Testing connection...")
36
+ client: Client | None = None
37
+ try:
38
+ # Import lazily so test patches targeting glaip_sdk.Client are honored
39
+ from importlib import import_module # noqa: PLC0415
40
+
41
+ client_module = import_module("glaip_sdk")
42
+ client = client_module.Client(api_url=api_url, api_key=api_key)
43
+ try:
44
+ agents = client.list_agents()
45
+ console.print(Text(f"✅ Connection successful! Found {len(agents)} agents", style=SUCCESS_STYLE))
46
+ return True
47
+ except Exception as exc: # pragma: no cover - API failures depend on network
48
+ console.print(Text(f"⚠️ Connection established but API call failed: {exc}", style=WARNING_STYLE))
49
+ console.print(" You may need to check your API permissions or network access")
50
+ if extra_hint:
51
+ console.print(extra_hint)
52
+ if abort_on_error:
53
+ raise click.Abort() from exc
54
+ return False
55
+ except Exception as exc:
56
+ console.print(Text(f"❌ Connection failed: {exc}"))
57
+ console.print(" Please check your API URL and key")
58
+ if extra_hint:
59
+ console.print(extra_hint)
60
+ if abort_on_error:
61
+ raise click.Abort() from exc
62
+ return False
63
+ finally:
64
+ if client is not None:
65
+ client.close()
@@ -10,42 +10,53 @@ import click
10
10
  from rich.console import Console
11
11
  from rich.text import Text
12
12
 
13
- from glaip_sdk import Client
14
- from glaip_sdk.branding import (
15
- ACCENT_STYLE,
16
- ERROR_STYLE,
17
- INFO,
18
- PRIMARY,
19
- SUCCESS,
20
- SUCCESS_STYLE,
21
- WARNING_STYLE,
22
- AIPBranding,
23
- )
13
+ from glaip_sdk.branding import ACCENT_STYLE, ERROR_STYLE, INFO, SUCCESS, SUCCESS_STYLE, WARNING_STYLE
14
+ from glaip_sdk.cli.account_store import get_account_store
15
+ from glaip_sdk.cli.commands.common_config import check_connection, render_branding_header
24
16
  from glaip_sdk.cli.config import CONFIG_FILE, load_config, save_config
17
+ from glaip_sdk.cli.hints import format_command_hint
18
+ from glaip_sdk.cli.masking import mask_api_key_display
25
19
  from glaip_sdk.cli.rich_helpers import markup_text
26
- from glaip_sdk.cli.utils import command_hint, format_command_hint, sdk_version
20
+ from glaip_sdk.cli.utils import command_hint
27
21
  from glaip_sdk.icons import ICON_TOOL
28
22
  from glaip_sdk.rich_components import AIPTable
29
23
 
30
24
  console = Console()
31
25
 
26
+ # Shared deprecation banner for legacy config commands
27
+ CONFIG_DEPRECATION_MSG = (
28
+ f"[{WARNING_STYLE}]Deprecated: 'aip config ...' will be removed in a future release. "
29
+ "Use 'aip accounts ...' (list/add/use/remove) or 'aip configure' for the wizard.[/]"
30
+ )
31
+
32
+
33
+ def _print_config_deprecation() -> None:
34
+ """Print a standardized deprecation warning for legacy config commands."""
35
+ console.print(CONFIG_DEPRECATION_MSG)
36
+
32
37
 
33
38
  @click.group()
34
39
  def config_group() -> None:
35
40
  """Configuration management operations."""
36
- pass
41
+ _print_config_deprecation()
37
42
 
38
43
 
39
44
  @config_group.command("list")
40
- def list_config() -> None:
41
- """List current configuration."""
42
- config = load_config()
45
+ @click.option("--json", "output_json", is_flag=True, help="Output in JSON format")
46
+ @click.pass_context
47
+ def list_config(ctx: click.Context, output_json: bool) -> None:
48
+ """List current configuration.
43
49
 
44
- if not config:
45
- _print_missing_config_hint()
46
- return
50
+ Deprecated: run 'aip accounts list' for profile-aware output.
51
+ """
52
+ console.print(f"[{WARNING_STYLE}]Deprecated: run 'aip accounts list' for profile-aware output.[/]")
53
+
54
+ # Delegate to accounts list by invoking the command
55
+ from glaip_sdk.cli.commands.accounts import accounts_group # noqa: PLC0415
47
56
 
48
- _render_config_table(config)
57
+ list_cmd = accounts_group.get_command(ctx, "list")
58
+ if list_cmd:
59
+ ctx.invoke(list_cmd, output_json=output_json)
49
60
 
50
61
 
51
62
  CONFIG_VALUE_TYPES: dict[str, str] = {
@@ -103,14 +114,49 @@ def _coerce_config_value(key: str, raw_value: str) -> str | bool | int | float:
103
114
  @config_group.command("set")
104
115
  @click.argument("key")
105
116
  @click.argument("value")
106
- def set_config(key: str, value: str) -> None:
107
- """Set a configuration value."""
108
- valid_keys = tuple(CONFIG_VALUE_TYPES.keys())
117
+ @click.option(
118
+ "--account",
119
+ "account_name",
120
+ help="Account name to set value for (defaults to active account)",
121
+ )
122
+ def set_config(key: str, value: str, account_name: str | None) -> None:
123
+ """Set a configuration value.
109
124
 
125
+ For api_url and api_key, this operates on the specified account (or active account).
126
+ Other keys (timeout, history_default_limit) are global settings.
127
+ """
128
+ # For other keys, use legacy config
129
+ valid_keys = tuple(CONFIG_VALUE_TYPES.keys())
110
130
  if key not in valid_keys:
111
131
  console.print(f"[{ERROR_STYLE}]Error: Invalid key '{key}'. Valid keys are: {', '.join(valid_keys)}[/]")
112
132
  raise click.ClickException(f"Invalid configuration key: {key}")
113
133
 
134
+ store = get_account_store()
135
+ # For api_url and api_key, update account profile but also mirror to legacy config
136
+ if key in ("api_url", "api_key"):
137
+ target_account = account_name or store.get_active_account() or "default"
138
+ try:
139
+ account = store.get_account(target_account) or {}
140
+ account[key] = value
141
+ store.add_account(
142
+ target_account,
143
+ account.get("api_url", ""),
144
+ account.get("api_key", ""),
145
+ overwrite=True,
146
+ )
147
+ except Exception:
148
+ # If account store persistence fails (e.g., mocked I/O), continue with legacy config
149
+ pass
150
+
151
+ # Always update legacy config for backward compatibility and test isolation
152
+ legacy_config = load_config()
153
+ legacy_config[key] = value
154
+ save_config(legacy_config)
155
+
156
+ display_value = _mask_api_key(value) if key == "api_key" else value
157
+ console.print(Text(f"✅ Set {key} = {display_value} for account '{target_account}'", style=SUCCESS_STYLE))
158
+ return
159
+
114
160
  coerced_value = _coerce_config_value(key, value)
115
161
  config = load_config()
116
162
  config[key] = coerced_value
@@ -126,12 +172,19 @@ def get_config(key: str) -> None:
126
172
  """Get a configuration value."""
127
173
  config = load_config()
128
174
 
129
- if key not in config:
175
+ value = config.get(key)
176
+
177
+ # Fallback to account store for api_url/api_key when legacy config lacks the key
178
+ if value is None and key in {"api_url", "api_key"}:
179
+ store = get_account_store()
180
+ active = store.get_active_account() or "default"
181
+ account = store.get_account(active) or {}
182
+ value = account.get(key)
183
+
184
+ if value is None:
130
185
  console.print(markup_text(f"[{WARNING_STYLE}]Configuration key '{key}' not found.[/]"))
131
186
  raise click.ClickException(f"Configuration key not found: {key}")
132
187
 
133
- value = config[key]
134
-
135
188
  if key == "api_key":
136
189
  console.print(_mask_api_key(value))
137
190
  else:
@@ -189,41 +242,73 @@ def reset_config(force: bool) -> None:
189
242
  console.print(message)
190
243
 
191
244
 
192
- def _configure_interactive() -> None:
245
+ def _configure_interactive(account_name: str | None = None) -> None:
193
246
  """Shared configuration logic for both configure commands."""
247
+ store = get_account_store()
248
+
249
+ # Determine account name (use provided, active, or default)
250
+ if not account_name:
251
+ account_name = store.get_active_account() or "default"
252
+
253
+ # Get existing account if it exists
254
+ existing = store.get_account(account_name)
255
+
194
256
  _render_configuration_header()
195
- config = load_config()
196
- _prompt_configuration_inputs(config)
197
- _save_configuration(config)
198
- _test_and_report_connection(config)
257
+ config = _prompt_configuration_inputs_for_account(existing)
258
+
259
+ # Save to account store
260
+ api_url = config.get("api_url", "")
261
+ api_key = config.get("api_key", "")
262
+ if api_url and api_key:
263
+ store.add_account(account_name, api_url, api_key, overwrite=True)
264
+ console.print(Text(f"\n✅ Configuration saved to account '{account_name}'", style=SUCCESS_STYLE))
265
+
266
+ _test_and_report_connection_for_account(account_name)
199
267
  _print_post_configuration_hints()
268
+ # Show active account footer
269
+ from glaip_sdk.cli.commands.accounts import _print_active_account_footer # noqa: PLC0415
270
+
271
+ _print_active_account_footer(store)
200
272
 
201
273
 
202
274
  @config_group.command()
203
- def configure() -> None:
204
- """Configure AIP CLI credentials and settings interactively."""
205
- _configure_interactive()
275
+ @click.option(
276
+ "--account",
277
+ "account_name",
278
+ help="Account name to configure (defaults to active account)",
279
+ )
280
+ def configure(account_name: str | None) -> None:
281
+ """Configure AIP CLI credentials and settings interactively.
282
+
283
+ This command is an alias for 'aip accounts add <name>' and will
284
+ configure the specified account (or active account if not specified).
285
+ """
286
+ _configure_interactive(account_name)
206
287
 
207
288
 
208
289
  # Alias command for backward compatibility
209
290
  @click.command()
210
- def configure_command() -> None:
291
+ @click.option(
292
+ "--account",
293
+ "account_name",
294
+ help="Account name to configure (defaults to active account)",
295
+ )
296
+ def configure_command(account_name: str | None) -> None:
211
297
  """Configure AIP CLI credentials and settings interactively.
212
298
 
213
299
  This is an alias for 'aip config configure' for backward compatibility.
300
+ For multi-account support, use 'aip accounts add <name>' instead.
214
301
  """
302
+ console.print(
303
+ f"[{WARNING_STYLE}]Setup tip:[/] Prefer 'aip accounts add <name>' or 'aip configure' from your terminal for "
304
+ "multi-account setup. Launching the interactive wizard now..."
305
+ )
215
306
  # Delegate to the shared function
216
- _configure_interactive()
307
+ _configure_interactive(account_name)
217
308
 
218
309
 
219
310
  # Note: The config command group should be registered in main.py
220
-
221
-
222
- def _mask_api_key(value: str | None) -> str:
223
- """Return a redacted API key string suitable for display."""
224
- if not value:
225
- return ""
226
- return "***" + value[-4:] if len(value) > 4 else "***"
311
+ _mask_api_key = mask_api_key_display
227
312
 
228
313
 
229
314
  def _print_missing_config_hint() -> None:
@@ -250,23 +335,23 @@ def _render_config_table(config: dict[str, str]) -> None:
250
335
 
251
336
  def _render_configuration_header() -> None:
252
337
  """Display the interactive configuration heading/banner."""
253
- branding = AIPBranding.create_from_sdk(sdk_version=sdk_version(), package_name="glaip-sdk")
254
- heading = "[bold]>_ GDP Labs AI Agents Package (AIP CLI)[/bold]"
255
- console.print(heading)
256
- console.print()
257
- console.print(branding.get_welcome_banner())
258
- console.rule("[bold]AIP Configuration[/bold]", style=PRIMARY)
338
+ render_branding_header(console, "[bold]AIP Configuration[/bold]")
259
339
 
260
340
 
261
- def _prompt_configuration_inputs(config: dict[str, str]) -> None:
262
- """Interactively prompt for configuration values."""
341
+ def _prompt_configuration_inputs_for_account(existing: dict[str, str] | None) -> dict[str, str]:
342
+ """Interactively prompt for account configuration values."""
263
343
  console.print("\n[bold]Enter your AIP configuration:[/bold]")
264
- console.print("(Leave blank to keep current values)")
344
+ if existing:
345
+ console.print("(Leave blank to keep current values)")
265
346
  console.print("─" * 50)
266
347
 
348
+ config = existing.copy() if existing else {}
349
+
267
350
  _prompt_api_url(config)
268
351
  _prompt_api_key(config)
269
352
 
353
+ return config
354
+
270
355
 
271
356
  def _prompt_api_url(config: dict[str, str]) -> None:
272
357
  """Ask the user for the API URL, preserving existing values by default."""
@@ -290,43 +375,24 @@ def _prompt_api_key(config: dict[str, str]) -> None:
290
375
  config["api_key"] = new_key
291
376
 
292
377
 
293
- def _save_configuration(config: dict[str, str]) -> None:
294
- """Persist the collected configuration to disk."""
295
- save_config(config)
296
- console.print(Text(f"\n✅ Configuration saved to: {CONFIG_FILE}", style=SUCCESS_STYLE))
378
+ def _test_and_report_connection_for_account(account_name: str) -> None:
379
+ """Sanity-check the provided credentials against the backend."""
380
+ store = get_account_store()
381
+ account = store.get_account(account_name)
382
+ if not account:
383
+ return
297
384
 
385
+ api_url = account.get("api_url", "")
386
+ api_key = account.get("api_key", "")
387
+ if not api_url or not api_key:
388
+ return
298
389
 
299
- def _test_and_report_connection(config: dict[str, str]) -> None:
300
- """Sanity-check the provided credentials against the backend."""
301
- console.print("\n🔌 Testing connection...")
302
- client: Client | None = None
303
- try:
304
- client = Client(api_url=config["api_url"], api_key=config["api_key"])
305
- try:
306
- agents = client.list_agents()
307
- console.print(
308
- Text(
309
- f"✅ Connection successful! Found {len(agents)} agents",
310
- style=SUCCESS_STYLE,
311
- )
312
- )
313
- except Exception as exc: # pragma: no cover - API failures depend on network
314
- console.print(
315
- Text(
316
- f"⚠️ Connection established but API call failed: {exc}",
317
- style=WARNING_STYLE,
318
- )
319
- )
320
- console.print(" You may need to check your API permissions or network access")
321
- except Exception as exc:
322
- console.print(Text(f"❌ Connection failed: {exc}"))
323
- console.print(" Please check your API URL and key")
324
- hint_status = command_hint("status", slash_command="status")
325
- if hint_status:
326
- console.print(f" You can run {format_command_hint(hint_status) or hint_status} later to test again")
327
- finally:
328
- if client is not None:
329
- client.close()
390
+ hint_status = command_hint("status", slash_command="status")
391
+ extra_hint = None
392
+ if hint_status:
393
+ extra_hint = f" You can run {format_command_hint(hint_status) or hint_status} later to test again"
394
+
395
+ check_connection(api_url, api_key, console, abort_on_error=False, extra_hint=extra_hint)
330
396
 
331
397
 
332
398
  def _print_post_configuration_hints() -> None:
@@ -63,6 +63,7 @@ from glaip_sdk.utils.serialization import (
63
63
  )
64
64
 
65
65
  console = Console()
66
+ MAX_DESCRIPTION_LEN = 50
66
67
 
67
68
 
68
69
  def _is_sensitive_data(val: Any) -> bool:
@@ -879,64 +880,210 @@ def get(
879
880
  raise click.ClickException(str(e)) from e
880
881
 
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
+ )
1034
+
1035
+
882
1036
  @mcps_group.command("tools")
883
- @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
+ )
884
1050
  @output_flags()
885
1051
  @click.pass_context
886
- def list_tools(ctx: Any, mcp_ref: str) -> None:
887
- """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.
888
1054
 
889
1055
  Args:
890
1056
  ctx: Click context containing output format preferences
891
- 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
892
1060
 
893
1061
  Raises:
894
1062
  ClickException: If MCP not found or tools fetch fails
895
- """
896
- try:
897
- client = get_client(ctx)
898
1063
 
899
- # Resolve MCP using helper function
900
- mcp = _resolve_mcp(ctx, client, mcp_ref)
901
-
902
- # Get tools from MCP
903
- with spinner_context(
904
- ctx,
905
- "[bold blue]Fetching MCP tools…[/bold blue]",
906
- console_override=console,
907
- ):
908
- tools = client.mcps.get_mcp_tools(mcp.id)
909
-
910
- # Define table columns: (data_key, header, style, width)
911
- columns = [
912
- ("name", "Name", ACCENT_STYLE, None),
913
- ("description", "Description", INFO, 50),
914
- ]
1064
+ Examples:
1065
+ Get tools from saved MCP:
1066
+ aip mcps tools <MCP_ID>
915
1067
 
916
- # Transform function for safe dictionary access
917
- def transform_tool(tool: dict[str, Any]) -> dict[str, Any]:
918
- """Transform a tool dictionary to a display row dictionary.
1068
+ Get tools from config file (without saving to DB):
1069
+ aip mcps tools --from-config mcp-config.json
919
1070
 
920
- Args:
921
- tool: Tool dictionary to transform.
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)
922
1077
 
923
- Returns:
924
- Dictionary with name and description fields.
925
- """
926
- return {
927
- "name": tool.get("name", "N/A"),
928
- "description": tool.get("description", "N/A")[:47] + "..."
929
- if len(tool.get("description", "")) > 47
930
- else tool.get("description", "N/A"),
931
- }
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)
932
1082
 
933
- output_list(
934
- ctx,
935
- tools,
936
- f"{ICON_TOOL} Tools from MCP: {mcp.name}",
937
- columns,
938
- transform_tool,
939
- )
1083
+ if names_only:
1084
+ _output_tool_names(ctx, tools)
1085
+ else:
1086
+ _output_tools_table(ctx, tools, title)
940
1087
 
941
1088
  except Exception as e:
942
1089
  raise click.ClickException(str(e)) from e
@@ -36,8 +36,8 @@ from glaip_sdk.cli.transcript.history import (
36
36
  from glaip_sdk.cli.transcript.viewer import ViewerContext, run_viewer_session
37
37
  from glaip_sdk.cli.utils import format_size, get_ctx_value, parse_json_line
38
38
  from glaip_sdk.rich_components import AIPTable
39
+ from glaip_sdk.utils.rendering.layout.panels import create_final_panel
39
40
  from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
40
- from glaip_sdk.utils.rendering.renderer.panels import create_final_panel
41
41
 
42
42
  console = Console()
43
43