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
@@ -0,0 +1,490 @@
1
+ """Common helpers and group definition for MCP commands.
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
+ from rich.console import Console
14
+
15
+ from glaip_sdk.cli.context import get_ctx_value
16
+ from glaip_sdk.cli.display import display_api_error, handle_json_output
17
+ from glaip_sdk.cli.io import load_resource_from_file_with_validation
18
+ from glaip_sdk.cli.mcp_validators import validate_mcp_auth_structure, validate_mcp_config_structure
19
+ from glaip_sdk.cli.parsers.json_input import parse_json_input
20
+ from glaip_sdk.cli.resolution import resolve_resource_reference
21
+ from glaip_sdk.cli.rich_helpers import print_markup
22
+ from glaip_sdk.cli.commands.shared.formatters import _format_empty_override_warnings, _format_preview_value
23
+ from glaip_sdk.utils.import_export import convert_export_to_import_format
24
+
25
+ console = Console()
26
+
27
+
28
+ @click.group(name="mcps", no_args_is_help=True)
29
+ def mcps_group() -> None:
30
+ """MCP management operations.
31
+
32
+ Provides commands for creating, listing, updating, deleting, and managing
33
+ Model Context Protocol (MCP) configurations.
34
+ """
35
+ pass
36
+
37
+
38
+ def _resolve_mcp(ctx: Any, client: Any, ref: str, select: int | None = None) -> Any | None:
39
+ """Resolve an MCP server by ID or name, with interactive selection support.
40
+
41
+ This function provides MCP-specific resolution logic. It delegates to
42
+ resolve_resource_reference for MCP-specific resolution, supporting UUID
43
+ lookups and name-based fuzzy matching.
44
+
45
+ Args:
46
+ ctx: Click context for command execution.
47
+ client: API client for backend operations.
48
+ ref: MCP identifier (UUID or name string).
49
+ select: Optional selection index when multiple MCPs match (1-based).
50
+
51
+ Returns:
52
+ MCP instance if resolution succeeds, None if not found.
53
+
54
+ Raises:
55
+ click.ClickException: When resolution fails or selection is invalid.
56
+ """
57
+ # Configure MCP-specific resolution functions
58
+ mcp_client = client.mcps
59
+ get_by_id_func = mcp_client.get_mcp_by_id
60
+ find_by_name_func = mcp_client.find_mcps
61
+ # Use MCP-specific resolution with standard fuzzy matching
62
+ return resolve_resource_reference(
63
+ ctx,
64
+ client,
65
+ ref,
66
+ "mcp",
67
+ get_by_id_func,
68
+ find_by_name_func,
69
+ "MCP",
70
+ select=select,
71
+ )
72
+
73
+
74
+ def _strip_server_only_fields(import_data: dict[str, Any]) -> dict[str, Any]:
75
+ """Remove fields that should not be forwarded during import-driven creation.
76
+
77
+ Args:
78
+ import_data: Raw import payload loaded from disk.
79
+
80
+ Returns:
81
+ A shallow copy of the data with server-managed fields removed.
82
+ """
83
+ cleaned = dict(import_data)
84
+ for key in (
85
+ "id",
86
+ "type",
87
+ "status",
88
+ "connection_status",
89
+ "created_at",
90
+ "updated_at",
91
+ ):
92
+ cleaned.pop(key, None)
93
+ return cleaned
94
+
95
+
96
+ def _load_import_ready_payload(import_file: str) -> dict[str, Any]:
97
+ """Load and normalise an imported MCP definition for create operations.
98
+
99
+ Args:
100
+ import_file: Path to an MCP export file (JSON or YAML).
101
+
102
+ Returns:
103
+ Normalised import payload ready for CLI/REST usage.
104
+
105
+ Raises:
106
+ click.ClickException: If the file cannot be parsed or validated.
107
+ """
108
+ raw_data = load_resource_from_file_with_validation(Path(import_file), "MCP")
109
+ import_data = convert_export_to_import_format(raw_data)
110
+ import_data = _strip_server_only_fields(import_data)
111
+
112
+ transport = import_data.get("transport")
113
+
114
+ if "config" in import_data:
115
+ import_data["config"] = validate_mcp_config_structure(
116
+ import_data["config"],
117
+ transport=transport,
118
+ source="import file",
119
+ )
120
+
121
+ if "authentication" in import_data:
122
+ import_data["authentication"] = validate_mcp_auth_structure(
123
+ import_data["authentication"],
124
+ source="import file",
125
+ )
126
+
127
+ return import_data
128
+
129
+
130
+ def _coerce_cli_string(value: str | None) -> str | None:
131
+ """Normalise CLI string values so blanks are treated as missing.
132
+
133
+ Args:
134
+ value: User-provided string option.
135
+
136
+ Returns:
137
+ The stripped string, or ``None`` when the value is blank/whitespace-only.
138
+ """
139
+ if value is None:
140
+ return None
141
+ trimmed = value.strip()
142
+ # Treat whitespace-only strings as None
143
+ return trimmed if trimmed else None
144
+
145
+
146
+ def _merge_config_field(
147
+ merged_base: dict[str, Any],
148
+ cli_config: str | None,
149
+ final_transport: str | None,
150
+ ) -> None:
151
+ """Merge config field with validation.
152
+
153
+ Args:
154
+ merged_base: Base payload to update in-place.
155
+ cli_config: Raw CLI JSON string for config.
156
+ final_transport: Transport type for validation.
157
+
158
+ Raises:
159
+ click.ClickException: If config JSON parsing or validation fails.
160
+ """
161
+ if cli_config is not None:
162
+ parsed_config = parse_json_input(cli_config)
163
+ merged_base["config"] = validate_mcp_config_structure(
164
+ parsed_config,
165
+ transport=final_transport,
166
+ source="--config",
167
+ )
168
+ elif "config" not in merged_base or merged_base["config"] is None:
169
+ merged_base["config"] = {}
170
+
171
+
172
+ def _merge_auth_field(
173
+ merged_base: dict[str, Any],
174
+ cli_auth: str | None,
175
+ ) -> None:
176
+ """Merge authentication field with validation.
177
+
178
+ Args:
179
+ merged_base: Base payload to update in-place.
180
+ cli_auth: Raw CLI JSON string for authentication.
181
+
182
+ Raises:
183
+ click.ClickException: If auth JSON parsing or validation fails.
184
+ """
185
+ if cli_auth is not None:
186
+ parsed_auth = parse_json_input(cli_auth)
187
+ merged_base["authentication"] = validate_mcp_auth_structure(
188
+ parsed_auth,
189
+ source="--auth",
190
+ )
191
+ elif "authentication" not in merged_base:
192
+ merged_base["authentication"] = None
193
+
194
+
195
+ def _merge_import_payload(
196
+ import_data: dict[str, Any] | None,
197
+ *,
198
+ cli_name: str | None,
199
+ cli_transport: str | None,
200
+ cli_description: str | None,
201
+ cli_config: str | None,
202
+ cli_auth: str | None,
203
+ ) -> tuple[dict[str, Any], list[str]]:
204
+ """Merge import data with CLI overrides while tracking missing fields.
205
+
206
+ Args:
207
+ import_data: Normalised payload loaded from file (if provided).
208
+ cli_name: Name supplied via CLI option.
209
+ cli_transport: Transport supplied via CLI option.
210
+ cli_description: Description supplied via CLI option.
211
+ cli_config: Raw CLI JSON string for config.
212
+ cli_auth: Raw CLI JSON string for authentication.
213
+
214
+ Returns:
215
+ A tuple of (merged_payload, missing_required_fields).
216
+
217
+ Raises:
218
+ click.ClickException: If config/auth JSON parsing or validation fails.
219
+ """
220
+ merged_base = import_data.copy() if import_data else {}
221
+
222
+ # Merge simple string fields using truthy CLI overrides
223
+ for field, cli_value in (
224
+ ("name", _coerce_cli_string(cli_name)),
225
+ ("transport", _coerce_cli_string(cli_transport)),
226
+ ("description", _coerce_cli_string(cli_description)),
227
+ ):
228
+ if cli_value is not None:
229
+ merged_base[field] = cli_value
230
+
231
+ # Determine final transport before validating config
232
+ final_transport = merged_base.get("transport")
233
+
234
+ # Merge config and authentication with validation
235
+ _merge_config_field(merged_base, cli_config, final_transport)
236
+ _merge_auth_field(merged_base, cli_auth)
237
+
238
+ # Validate required fields
239
+ missing_fields = []
240
+ for required in ("name", "transport"):
241
+ value = merged_base.get(required)
242
+ if not isinstance(value, str) or not value.strip():
243
+ missing_fields.append(required)
244
+
245
+ return merged_base, missing_fields
246
+
247
+
248
+ def _validate_import_payload_fields(import_payload: dict[str, Any]) -> bool:
249
+ """Validate that import payload contains updatable fields.
250
+
251
+ Args:
252
+ import_payload: Import payload to validate
253
+
254
+ Returns:
255
+ True if payload has updatable fields, False otherwise
256
+ """
257
+ updatable_fields = {"name", "transport", "description", "config", "authentication"}
258
+ has_updatable = any(field in import_payload for field in updatable_fields)
259
+
260
+ if not has_updatable:
261
+ available_fields = set(import_payload.keys())
262
+ print_markup(
263
+ "[yellow]⚠️ No updatable fields found in import file.[/yellow]\n"
264
+ f"[dim]Found fields: {', '.join(sorted(available_fields))}[/dim]\n"
265
+ f"[dim]Updatable fields: {', '.join(sorted(updatable_fields))}[/dim]"
266
+ )
267
+ return has_updatable
268
+
269
+
270
+ def _get_config_transport(
271
+ transport: str | None,
272
+ import_payload: dict[str, Any] | None,
273
+ mcp: Any,
274
+ ) -> str | None:
275
+ """Get the transport value for config validation.
276
+
277
+ Args:
278
+ transport: CLI transport flag
279
+ import_payload: Optional import payload
280
+ mcp: Current MCP object
281
+
282
+ Returns:
283
+ Transport value or None
284
+ """
285
+ if import_payload:
286
+ return transport or import_payload.get("transport")
287
+ return transport or getattr(mcp, "transport", None)
288
+
289
+
290
+ def _collect_cli_overrides(
291
+ name: str | None,
292
+ transport: str | None,
293
+ description: str | None,
294
+ config: str | None,
295
+ auth: str | None,
296
+ ) -> dict[str, Any]:
297
+ """Collect CLI flags that were explicitly provided.
298
+
299
+ Args:
300
+ name: CLI name flag
301
+ transport: CLI transport flag
302
+ description: CLI description flag
303
+ config: CLI config flag
304
+ auth: CLI auth flag
305
+
306
+ Returns:
307
+ Dictionary of provided CLI overrides
308
+ """
309
+ cli_overrides = {}
310
+ if name is not None:
311
+ cli_overrides["name"] = name
312
+ if transport is not None:
313
+ cli_overrides["transport"] = transport
314
+ if description is not None:
315
+ cli_overrides["description"] = description
316
+ if config is not None:
317
+ cli_overrides["config"] = config
318
+ if auth is not None:
319
+ cli_overrides["auth"] = auth
320
+ return cli_overrides
321
+
322
+
323
+ def _handle_cli_error(ctx: Any, error: Exception, operation: str) -> None:
324
+ """Render CLI error once and exit with non-zero status."""
325
+ handle_json_output(ctx, error=error)
326
+ if get_ctx_value(ctx, "view") != "json":
327
+ display_api_error(error, operation)
328
+ ctx.exit(1)
329
+
330
+
331
+ def _assemble_update_data_from_import_payload(import_payload: dict[str, Any] | None) -> dict[str, Any]:
332
+ """Assemble update_data dictionary from import payload.
333
+
334
+ Args:
335
+ import_payload: Import payload dictionary or None
336
+
337
+ Returns:
338
+ Dictionary with update fields from import payload
339
+ """
340
+ update_data: dict[str, Any] = {}
341
+ if import_payload:
342
+ for field in ("name", "transport", "description", "config", "authentication"):
343
+ if field in import_payload:
344
+ update_data[field] = import_payload[field]
345
+ return update_data
346
+
347
+
348
+ def _assemble_update_data_from_cli_options(
349
+ update_data: dict[str, Any],
350
+ name: str | None,
351
+ transport: str | None,
352
+ description: str | None,
353
+ config: str | None,
354
+ auth: str | None,
355
+ import_payload: dict[str, Any] | None,
356
+ mcp: Any,
357
+ ) -> dict[str, Any]:
358
+ """Build update_data dictionary from CLI options, overriding import values.
359
+
360
+ Args:
361
+ update_data: Existing update_data dictionary (from import payload)
362
+ name: MCP name option
363
+ transport: Transport option
364
+ description: Description option
365
+ config: Config option
366
+ auth: Auth option
367
+ import_payload: Import payload dictionary or None
368
+ mcp: Current MCP object
369
+
370
+ Returns:
371
+ Updated dictionary with CLI option values
372
+ """
373
+ if name is not None:
374
+ update_data["name"] = name
375
+ if transport is not None:
376
+ update_data["transport"] = transport
377
+ if description is not None:
378
+ update_data["description"] = description
379
+ if config is not None:
380
+ parsed_config = parse_json_input(config)
381
+ config_transport = _get_config_transport(transport, import_payload, mcp)
382
+ update_data["config"] = validate_mcp_config_structure(
383
+ parsed_config,
384
+ transport=config_transport,
385
+ source="--config",
386
+ )
387
+ if auth is not None:
388
+ parsed_auth = parse_json_input(auth)
389
+ update_data["authentication"] = validate_mcp_auth_structure(parsed_auth, source="--auth")
390
+ return update_data
391
+
392
+
393
+ def _generate_update_preview(mcp: Any, update_data: dict[str, Any], cli_overrides: dict[str, Any]) -> str:
394
+ """Generate formatted preview of changes for user confirmation.
395
+
396
+ Args:
397
+ mcp: Current MCP object
398
+ update_data: Data that will be sent in update request
399
+ cli_overrides: CLI flags that were explicitly provided
400
+
401
+ Returns:
402
+ Formatted preview string showing old→new values
403
+ """
404
+ lines = [f"\n[bold]The following fields will be updated for MCP '{mcp.name}':[/bold]\n"]
405
+
406
+ empty_overrides = []
407
+
408
+ # Show each field that will be updated
409
+ for field, new_value in update_data.items():
410
+ old_value = getattr(mcp, field, None)
411
+
412
+ # Track empty CLI overrides
413
+ if field in cli_overrides and cli_overrides[field] == "":
414
+ empty_overrides.append(field)
415
+
416
+ old_display = _format_preview_value(old_value)
417
+ new_display = _format_preview_value(new_value)
418
+
419
+ lines.append(f"- [cyan]{field}[/cyan]: {old_display} → {new_display}")
420
+
421
+ # Add warnings for empty CLI overrides
422
+ lines.extend(_format_empty_override_warnings(empty_overrides))
423
+
424
+ return "\n".join(lines)
425
+
426
+
427
+ def _validate_update_inputs(
428
+ name: str | None,
429
+ transport: str | None,
430
+ description: str | None,
431
+ config: str | None,
432
+ auth: str | None,
433
+ import_file: str | None,
434
+ ) -> None:
435
+ """Validate that at least one update method is provided.
436
+
437
+ Args:
438
+ name: MCP name option
439
+ transport: Transport option
440
+ description: Description option
441
+ config: Config option
442
+ auth: Auth option
443
+ import_file: Import file option
444
+
445
+ Raises:
446
+ ClickException: If no update fields are specified
447
+ """
448
+ cli_flags_provided = any(v is not None for v in [name, transport, description, config, auth])
449
+ if not import_file and not cli_flags_provided:
450
+ raise click.ClickException(
451
+ "No update fields specified. Use --import or one of: --name, --transport, --description, --config, --auth"
452
+ )
453
+
454
+
455
+ def _handle_update_preview_and_confirmation(
456
+ import_payload: dict[str, Any] | None,
457
+ y: bool,
458
+ mcp: Any,
459
+ update_data: dict[str, Any],
460
+ name: str | None,
461
+ transport: str | None,
462
+ description: str | None,
463
+ config: str | None,
464
+ auth: str | None,
465
+ ) -> bool:
466
+ """Handle preview display and user confirmation for import-based updates.
467
+
468
+ Args:
469
+ import_payload: Import payload dictionary or None
470
+ y: Skip confirmation flag
471
+ mcp: Current MCP object
472
+ update_data: Data that will be sent in update request
473
+ name: MCP name option
474
+ transport: Transport option
475
+ description: Description option
476
+ config: Config option
477
+ auth: Auth option
478
+
479
+ Returns:
480
+ True if update should proceed, False if cancelled
481
+ """
482
+ if import_payload and not y:
483
+ cli_overrides = _collect_cli_overrides(name, transport, description, config, auth)
484
+ preview = _generate_update_preview(mcp, update_data, cli_overrides)
485
+ print_markup(preview)
486
+
487
+ if not click.confirm("\nContinue with update?", default=False):
488
+ print_markup("[yellow]Update cancelled.[/yellow]")
489
+ return False
490
+ return True
@@ -0,0 +1,82 @@
1
+ """Connect to MCP command.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from typing import Any
11
+
12
+ import click
13
+
14
+ from glaip_sdk.branding import SUCCESS, SUCCESS_STYLE, WARNING_STYLE
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.rendering import spinner_context
18
+ from glaip_sdk.cli.display import handle_json_output
19
+ from glaip_sdk.cli.rich_helpers import print_markup
20
+ from glaip_sdk.rich_components import AIPPanel
21
+
22
+ from ._common import console, mcps_group
23
+
24
+
25
+ @mcps_group.command("connect")
26
+ @click.option(
27
+ "--from-file",
28
+ "config_file",
29
+ required=True,
30
+ help="MCP config JSON file",
31
+ )
32
+ @output_flags()
33
+ @click.pass_context
34
+ def connect(ctx: Any, config_file: str) -> None:
35
+ """Test MCP connection using a configuration file.
36
+
37
+ Args:
38
+ ctx: Click context containing output format preferences
39
+ config_file: Path to MCP configuration JSON file
40
+
41
+ Raises:
42
+ ClickException: If config file invalid or connection test fails
43
+
44
+ Note:
45
+ Loads MCP configuration from JSON file and tests connectivity.
46
+ Displays success or failure with connection details.
47
+ """
48
+ try:
49
+ client = get_client(ctx)
50
+
51
+ # Load MCP config from file
52
+ with open(config_file) as f:
53
+ config = json.load(f)
54
+
55
+ view = get_ctx_value(ctx, "view", "rich")
56
+ if view != "json":
57
+ print_markup(
58
+ f"[{WARNING_STYLE}]Connecting to MCP with config from {config_file}...[/]",
59
+ console=console,
60
+ )
61
+
62
+ # Test connection using config
63
+ with spinner_context(
64
+ ctx,
65
+ "[bold blue]Connecting to MCP…[/bold blue]",
66
+ console_override=console,
67
+ ):
68
+ result = client.mcps.test_mcp_connection_from_config(config)
69
+
70
+ view = get_ctx_value(ctx, "view", "rich")
71
+ if view == "json":
72
+ handle_json_output(ctx, result)
73
+ else:
74
+ success_panel = AIPPanel(
75
+ f"[{SUCCESS_STYLE}]✓[/] MCP connection successful!\n\n[bold]Result:[/bold] {result}",
76
+ title="🔌 Connection",
77
+ border_style=SUCCESS,
78
+ )
79
+ console.print(success_panel)
80
+
81
+ except Exception as e:
82
+ raise click.ClickException(str(e)) from e