glaip-sdk 0.0.10__py3-none-any.whl → 0.0.12__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.
@@ -5,6 +5,7 @@ Authors:
5
5
  """
6
6
 
7
7
  import json
8
+ import sys
8
9
  from pathlib import Path
9
10
  from typing import Any
10
11
 
@@ -21,12 +22,14 @@ from glaip_sdk.cli.display import (
21
22
  handle_json_output,
22
23
  handle_rich_output,
23
24
  )
24
- from glaip_sdk.cli.io import (
25
- export_resource_to_file_with_validation as export_resource_to_file,
26
- )
27
25
  from glaip_sdk.cli.io import (
28
26
  fetch_raw_resource_details,
29
27
  )
28
+ from glaip_sdk.cli.mcp_validators import (
29
+ validate_mcp_auth_structure,
30
+ validate_mcp_config_structure,
31
+ )
32
+ from glaip_sdk.cli.parsers.json_input import parse_json_input
30
33
  from glaip_sdk.cli.resolution import resolve_resource_reference
31
34
  from glaip_sdk.cli.utils import (
32
35
  coerce_to_row,
@@ -40,20 +43,41 @@ from glaip_sdk.cli.utils import (
40
43
  )
41
44
  from glaip_sdk.rich_components import AIPPanel
42
45
  from glaip_sdk.utils import format_datetime
46
+ from glaip_sdk.utils.serialization import (
47
+ build_mcp_export_payload,
48
+ write_resource_export,
49
+ )
43
50
 
44
51
  console = Console()
45
52
 
46
53
 
47
54
  @click.group(name="mcps", no_args_is_help=True)
48
55
  def mcps_group() -> None:
49
- """MCP management operations."""
56
+ """MCP management operations.
57
+
58
+ Provides commands for creating, listing, updating, deleting, and managing
59
+ Model Context Protocol (MCP) configurations.
60
+ """
50
61
  pass
51
62
 
52
63
 
53
64
  def _resolve_mcp(
54
65
  ctx: Any, client: Any, ref: str, select: int | None = None
55
66
  ) -> Any | None:
56
- """Resolve MCP reference (ID or name) with ambiguity handling."""
67
+ """Resolve MCP reference (ID or name) with ambiguity handling.
68
+
69
+ Args:
70
+ ctx: Click context object
71
+ client: API client instance
72
+ ref: MCP reference (ID or name)
73
+ select: Index to select when multiple matches found
74
+
75
+ Returns:
76
+ MCP object if found, None otherwise
77
+
78
+ Raises:
79
+ ClickException: If MCP not found or selection invalid
80
+ """
57
81
  return resolve_resource_reference(
58
82
  ctx,
59
83
  client,
@@ -70,7 +94,14 @@ def _resolve_mcp(
70
94
  @output_flags()
71
95
  @click.pass_context
72
96
  def list_mcps(ctx: Any) -> None:
73
- """List all MCPs."""
97
+ """List all MCPs in a formatted table.
98
+
99
+ Args:
100
+ ctx: Click context containing output format preferences
101
+
102
+ Raises:
103
+ ClickException: If API request fails
104
+ """
74
105
  try:
75
106
  client = get_client(ctx)
76
107
  with spinner_context(
@@ -111,36 +142,83 @@ def list_mcps(ctx: Any) -> None:
111
142
  @click.option("--name", required=True, help="MCP name")
112
143
  @click.option("--transport", required=True, help="MCP transport protocol")
113
144
  @click.option("--description", help="MCP description")
114
- @click.option("--config", help="JSON configuration string")
145
+ @click.option(
146
+ "--config",
147
+ help="JSON configuration string or @file reference (e.g., @config.json)",
148
+ )
149
+ @click.option(
150
+ "--auth",
151
+ "--authentication",
152
+ "auth",
153
+ help="JSON authentication object or @file reference (e.g., @auth.json)",
154
+ )
115
155
  @output_flags()
116
156
  @click.pass_context
117
157
  def create(
118
- ctx: Any, name: str, transport: str, description: str | None, config: str | None
158
+ ctx: Any,
159
+ name: str,
160
+ transport: str,
161
+ description: str | None,
162
+ config: str | None,
163
+ auth: str | None,
119
164
  ) -> None:
120
- """Create a new MCP."""
165
+ """Create a new MCP with specified configuration.
166
+
167
+ Args:
168
+ ctx: Click context containing output format preferences
169
+ name: MCP name (required)
170
+ transport: MCP transport protocol (required)
171
+ description: Optional MCP description
172
+ config: JSON configuration string or @file reference
173
+ auth: JSON authentication object or @file reference
174
+
175
+ Raises:
176
+ ClickException: If JSON parsing fails or API request fails
177
+ """
121
178
  try:
122
179
  client = get_client(ctx)
123
180
 
124
- # Parse config if provided
125
- mcp_config = {}
126
- if config:
127
- try:
128
- mcp_config = json.loads(config)
129
- except json.JSONDecodeError:
130
- raise click.ClickException("Invalid JSON in --config")
181
+ # Parse config if provided (supports inline JSON or @file)
182
+ raw_config = parse_json_input(config)
183
+ if raw_config is None:
184
+ mcp_config: dict[str, Any] = {}
185
+ else:
186
+ mcp_config = validate_mcp_config_structure(
187
+ raw_config,
188
+ transport=transport,
189
+ source="--config",
190
+ )
191
+
192
+ # Parse authentication if provided (supports inline JSON or @file)
193
+ mcp_auth = parse_json_input(auth)
194
+ validated_auth = (
195
+ validate_mcp_auth_structure(mcp_auth, source="--auth")
196
+ if mcp_auth is not None
197
+ else None
198
+ )
199
+
200
+ # Build kwargs for create_mcp
201
+ create_kwargs = {
202
+ "name": name,
203
+ "type": "server", # MCPs are always server type
204
+ "transport": transport,
205
+ "config": mcp_config,
206
+ }
207
+
208
+ # Only add description if provided
209
+ if description is not None:
210
+ create_kwargs["description"] = description
211
+
212
+ # Only add authentication if provided
213
+ if validated_auth:
214
+ create_kwargs["authentication"] = validated_auth
131
215
 
132
216
  with spinner_context(
133
217
  ctx,
134
218
  "[bold blue]Creating MCP…[/bold blue]",
135
219
  console_override=console,
136
220
  ):
137
- mcp = client.mcps.create_mcp(
138
- name=name,
139
- type="server", # MCPs are always server type
140
- transport=transport,
141
- description=description,
142
- config=mcp_config,
143
- )
221
+ mcp = client.mcps.create_mcp(**create_kwargs)
144
222
 
145
223
  # Handle JSON output
146
224
  handle_json_output(ctx, mcp.model_dump())
@@ -163,22 +241,191 @@ def create(
163
241
  raise click.ClickException(str(e))
164
242
 
165
243
 
244
+ def _handle_mcp_export(
245
+ ctx: Any,
246
+ client: Any,
247
+ mcp: Any,
248
+ export_path: Path,
249
+ no_auth_prompt: bool,
250
+ auth_placeholder: str,
251
+ ) -> None:
252
+ """Handle MCP export to file with format detection and auth handling.
253
+
254
+ Args:
255
+ ctx: Click context for spinner management
256
+ client: API client for fetching MCP details
257
+ mcp: MCP object to export
258
+ export_path: Target file path (format detected from extension)
259
+ no_auth_prompt: Skip interactive secret prompts if True
260
+ auth_placeholder: Placeholder text for missing secrets
261
+
262
+ Note:
263
+ Supports JSON (.json) and YAML (.yaml/.yml) export formats.
264
+ In interactive mode, prompts for secret values.
265
+ In non-interactive mode, uses placeholder values.
266
+ """
267
+ # Auto-detect format from file extension
268
+ detected_format = detect_export_format(export_path)
269
+
270
+ # Always export comprehensive data - re-fetch with full details
271
+ try:
272
+ with spinner_context(
273
+ ctx,
274
+ "[bold blue]Fetching complete MCP details…[/bold blue]",
275
+ console_override=console,
276
+ ):
277
+ mcp = client.mcps.get_mcp_by_id(mcp.id)
278
+ except Exception as e:
279
+ console.print(
280
+ Text(f"[yellow]⚠️ Could not fetch full MCP details: {e}[/yellow]")
281
+ )
282
+ console.print(Text("[yellow]⚠️ Proceeding with available data[/yellow]"))
283
+
284
+ # Determine if we should prompt for secrets
285
+ prompt_for_secrets = not no_auth_prompt and sys.stdin.isatty()
286
+
287
+ # Warn user if non-interactive mode forces placeholder usage
288
+ if not no_auth_prompt and not sys.stdin.isatty():
289
+ console.print(
290
+ Text(
291
+ "[yellow]⚠️ Non-interactive mode detected. "
292
+ "Using placeholder values for secrets.[/yellow]"
293
+ )
294
+ )
295
+
296
+ # Build and write export payload
297
+ if prompt_for_secrets:
298
+ # Interactive mode: no spinner during prompts
299
+ export_payload = build_mcp_export_payload(
300
+ mcp,
301
+ prompt_for_secrets=prompt_for_secrets,
302
+ placeholder=auth_placeholder,
303
+ console=console,
304
+ )
305
+ with spinner_context(
306
+ ctx,
307
+ "[bold blue]Writing export file…[/bold blue]",
308
+ console_override=console,
309
+ ):
310
+ write_resource_export(export_path, export_payload, detected_format)
311
+ else:
312
+ # Non-interactive mode: spinner for entire export process
313
+ with spinner_context(
314
+ ctx,
315
+ "[bold blue]Exporting MCP configuration…[/bold blue]",
316
+ console_override=console,
317
+ ):
318
+ export_payload = build_mcp_export_payload(
319
+ mcp,
320
+ prompt_for_secrets=prompt_for_secrets,
321
+ placeholder=auth_placeholder,
322
+ console=console,
323
+ )
324
+ write_resource_export(export_path, export_payload, detected_format)
325
+
326
+ console.print(
327
+ Text(
328
+ f"[green]✅ Complete MCP configuration exported to: "
329
+ f"{export_path} (format: {detected_format})[/green]"
330
+ )
331
+ )
332
+
333
+
334
+ def _display_mcp_details(ctx: Any, client: Any, mcp: Any) -> None:
335
+ """Display MCP details using raw API data or fallback to Pydantic model.
336
+
337
+ Args:
338
+ ctx: Click context containing output format preferences
339
+ client: API client for fetching raw MCP data
340
+ mcp: MCP object to display details for
341
+
342
+ Note:
343
+ Attempts to fetch raw API data first to preserve all fields.
344
+ Falls back to Pydantic model data if raw data unavailable.
345
+ Formats datetime fields for better readability.
346
+ """
347
+ # Try to fetch raw API data first to preserve ALL fields
348
+ with spinner_context(
349
+ ctx,
350
+ "[bold blue]Fetching detailed MCP data…[/bold blue]",
351
+ console_override=console,
352
+ ):
353
+ raw_mcp_data = fetch_raw_resource_details(client, mcp, "mcps")
354
+
355
+ if raw_mcp_data:
356
+ # Use raw API data - this preserves ALL fields
357
+ formatted_data = raw_mcp_data.copy()
358
+ if "created_at" in formatted_data:
359
+ formatted_data["created_at"] = format_datetime(formatted_data["created_at"])
360
+ if "updated_at" in formatted_data:
361
+ formatted_data["updated_at"] = format_datetime(formatted_data["updated_at"])
362
+
363
+ output_result(
364
+ ctx,
365
+ formatted_data,
366
+ title="MCP Details",
367
+ panel_title=f"🔌 {raw_mcp_data.get('name', 'Unknown')}",
368
+ )
369
+ else:
370
+ # Fall back to Pydantic model data
371
+ console.print("[yellow]Falling back to Pydantic model data[/yellow]")
372
+ result_data = {
373
+ "id": str(getattr(mcp, "id", "N/A")),
374
+ "name": getattr(mcp, "name", "N/A"),
375
+ "type": getattr(mcp, "type", "N/A"),
376
+ "config": getattr(mcp, "config", "N/A"),
377
+ "status": getattr(mcp, "status", "N/A"),
378
+ "connection_status": getattr(mcp, "connection_status", "N/A"),
379
+ }
380
+ output_result(
381
+ ctx, result_data, title="MCP Details", panel_title=f"🔌 {mcp.name}"
382
+ )
383
+
384
+
166
385
  @mcps_group.command()
167
386
  @click.argument("mcp_ref")
168
387
  @click.option(
169
388
  "--export",
170
389
  type=click.Path(dir_okay=False, writable=True),
171
- help="Export complete MCP configuration to file (format auto-detected from .json/.yaml extension)",
390
+ help="Export complete MCP configuration to file "
391
+ "(format auto-detected from .json/.yaml extension)",
392
+ )
393
+ @click.option(
394
+ "--no-auth-prompt",
395
+ is_flag=True,
396
+ help="Skip interactive secret prompts and use placeholder values.",
397
+ )
398
+ @click.option(
399
+ "--auth-placeholder",
400
+ default="<INSERT VALUE>",
401
+ show_default=True,
402
+ help="Placeholder text used when secrets are unavailable.",
172
403
  )
173
404
  @output_flags()
174
405
  @click.pass_context
175
- def get(ctx: Any, mcp_ref: str, export: str | None) -> None:
176
- """Get MCP details.
406
+ def get(
407
+ ctx: Any,
408
+ mcp_ref: str,
409
+ export: str | None,
410
+ no_auth_prompt: bool,
411
+ auth_placeholder: str,
412
+ ) -> None:
413
+ """Get MCP details and optionally export configuration to file.
414
+
415
+ Args:
416
+ ctx: Click context containing output format preferences
417
+ mcp_ref: MCP reference (ID or name)
418
+ export: Optional file path to export MCP configuration
419
+ no_auth_prompt: Skip interactive secret prompts if True
420
+ auth_placeholder: Placeholder text for missing secrets
421
+
422
+ Raises:
423
+ ClickException: If MCP not found or export fails
177
424
 
178
425
  Examples:
179
426
  aip mcps get my-mcp
180
- aip mcps get my-mcp --export mcp.json # Exports complete configuration as JSON
181
- aip mcps get my-mcp --export mcp.yaml # Exports complete configuration as YAML
427
+ aip mcps get my-mcp --export mcp.json # Export as JSON
428
+ aip mcps get my-mcp --export mcp.yaml # Export as YAML
182
429
  """
183
430
  try:
184
431
  client = get_client(ctx)
@@ -188,83 +435,12 @@ def get(ctx: Any, mcp_ref: str, export: str | None) -> None:
188
435
 
189
436
  # Handle export option
190
437
  if export:
191
- export_path = Path(export)
192
- # Auto-detect format from file extension
193
- detected_format = detect_export_format(export_path)
194
-
195
- # Always export comprehensive data - re-fetch MCP with full details if needed
196
- try:
197
- with spinner_context(
198
- ctx,
199
- "[bold blue]Fetching complete MCP details…[/bold blue]",
200
- console_override=console,
201
- ):
202
- mcp = client.mcps.get_mcp_by_id(mcp.id)
203
- except Exception as e:
204
- console.print(
205
- Text(f"[yellow]⚠️ Could not fetch full MCP details: {e}[/yellow]")
206
- )
207
- console.print(
208
- Text("[yellow]⚠️ Proceeding with available data[/yellow]")
209
- )
210
-
211
- with spinner_context(
212
- ctx,
213
- "[bold blue]Exporting MCP configuration…[/bold blue]",
214
- console_override=console,
215
- ):
216
- export_resource_to_file(mcp, export_path, detected_format)
217
- console.print(
218
- Text(
219
- f"[green]✅ Complete MCP configuration exported to: {export_path} (format: {detected_format})[/green]"
220
- )
221
- )
222
-
223
- # Try to fetch raw API data first to preserve ALL fields
224
- with spinner_context(
225
- ctx,
226
- "[bold blue]Fetching detailed MCP data…[/bold blue]",
227
- console_override=console,
228
- ):
229
- raw_mcp_data = fetch_raw_resource_details(client, mcp, "mcps")
230
-
231
- if raw_mcp_data:
232
- # Use raw API data - this preserves ALL fields
233
- # Format dates for better display (minimal postprocessing)
234
- formatted_data = raw_mcp_data.copy()
235
- if "created_at" in formatted_data:
236
- formatted_data["created_at"] = format_datetime(
237
- formatted_data["created_at"]
238
- )
239
- if "updated_at" in formatted_data:
240
- formatted_data["updated_at"] = format_datetime(
241
- formatted_data["updated_at"]
242
- )
243
-
244
- # Display using output_result with raw data
245
- output_result(
246
- ctx,
247
- formatted_data,
248
- title="MCP Details",
249
- panel_title=f"🔌 {raw_mcp_data.get('name', 'Unknown')}",
438
+ _handle_mcp_export(
439
+ ctx, client, mcp, Path(export), no_auth_prompt, auth_placeholder
250
440
  )
251
- else:
252
- # Fall back to original method if raw fetch fails
253
- console.print("[yellow]Falling back to Pydantic model data[/yellow]")
254
-
255
- # Create result data with actual available fields
256
- result_data = {
257
- "id": str(getattr(mcp, "id", "N/A")),
258
- "name": getattr(mcp, "name", "N/A"),
259
- "type": getattr(mcp, "type", "N/A"),
260
- "config": getattr(mcp, "config", "N/A"),
261
- "status": getattr(mcp, "status", "N/A"),
262
- "connection_status": getattr(mcp, "connection_status", "N/A"),
263
- }
264
441
 
265
- output_result(
266
- ctx, result_data, title="MCP Details", panel_title=f"🔌 {mcp.name}"
267
- )
442
+ # Display MCP details
443
+ _display_mcp_details(ctx, client, mcp)
268
444
 
269
445
  except Exception as e:
270
446
  raise click.ClickException(str(e))
@@ -275,7 +451,15 @@ def get(ctx: Any, mcp_ref: str, export: str | None) -> None:
275
451
  @output_flags()
276
452
  @click.pass_context
277
453
  def list_tools(ctx: Any, mcp_ref: str) -> None:
278
- """List tools from MCP."""
454
+ """List tools available from a specific MCP.
455
+
456
+ Args:
457
+ ctx: Click context containing output format preferences
458
+ mcp_ref: MCP reference (ID or name)
459
+
460
+ Raises:
461
+ ClickException: If MCP not found or tools fetch fails
462
+ """
279
463
  try:
280
464
  client = get_client(ctx)
281
465
 
@@ -325,7 +509,19 @@ def list_tools(ctx: Any, mcp_ref: str) -> None:
325
509
  @output_flags()
326
510
  @click.pass_context
327
511
  def connect(ctx: Any, config_file: str) -> None:
328
- """Connect to MCP using config file."""
512
+ """Test MCP connection using a configuration file.
513
+
514
+ Args:
515
+ ctx: Click context containing output format preferences
516
+ config_file: Path to MCP configuration JSON file
517
+
518
+ Raises:
519
+ ClickException: If config file invalid or connection test fails
520
+
521
+ Note:
522
+ Loads MCP configuration from JSON file and tests connectivity.
523
+ Displays success or failure with connection details.
524
+ """
329
525
  try:
330
526
  client = get_client(ctx)
331
527
 
@@ -337,7 +533,8 @@ def connect(ctx: Any, config_file: str) -> None:
337
533
  if view != "json":
338
534
  console.print(
339
535
  Text(
340
- f"[yellow]Connecting to MCP with config from {config_file}...[/yellow]"
536
+ f"[yellow]Connecting to MCP with config from "
537
+ f"{config_file}...[/yellow]"
341
538
  )
342
539
  )
343
540
 
@@ -369,7 +566,16 @@ def connect(ctx: Any, config_file: str) -> None:
369
566
  @click.argument("mcp_ref")
370
567
  @click.option("--name", help="New MCP name")
371
568
  @click.option("--description", help="New description")
372
- @click.option("--config", help="JSON configuration string")
569
+ @click.option(
570
+ "--config",
571
+ help="JSON configuration string or @file reference (e.g., @config.json)",
572
+ )
573
+ @click.option(
574
+ "--auth",
575
+ "--authentication",
576
+ "auth",
577
+ help="JSON authentication object or @file reference (e.g., @auth.json)",
578
+ )
373
579
  @output_flags()
374
580
  @click.pass_context
375
581
  def update(
@@ -378,8 +584,25 @@ def update(
378
584
  name: str | None,
379
585
  description: str | None,
380
586
  config: str | None,
587
+ auth: str | None,
381
588
  ) -> None:
382
- """Update an existing MCP."""
589
+ """Update an existing MCP with new configuration values.
590
+
591
+ Args:
592
+ ctx: Click context containing output format preferences
593
+ mcp_ref: MCP reference (ID or name)
594
+ name: New MCP name (optional)
595
+ description: New description (optional)
596
+ config: New JSON configuration string or @file reference (optional)
597
+ auth: New JSON authentication object or @file reference (optional)
598
+
599
+ Raises:
600
+ ClickException: If MCP not found, JSON invalid, or no fields specified
601
+
602
+ Note:
603
+ At least one field must be specified for update.
604
+ Uses PUT for complete updates or PATCH for partial updates.
605
+ """
383
606
  try:
384
607
  client = get_client(ctx)
385
608
 
@@ -393,10 +616,17 @@ def update(
393
616
  if description is not None:
394
617
  update_data["description"] = description
395
618
  if config is not None:
396
- try:
397
- update_data["config"] = json.loads(config)
398
- except json.JSONDecodeError:
399
- raise click.ClickException("Invalid JSON in --config")
619
+ parsed_config = parse_json_input(config)
620
+ update_data["config"] = validate_mcp_config_structure(
621
+ parsed_config,
622
+ transport=getattr(mcp, "transport", None),
623
+ source="--config",
624
+ )
625
+ if auth is not None:
626
+ parsed_auth = parse_json_input(auth)
627
+ update_data["authentication"] = validate_mcp_auth_structure(
628
+ parsed_auth, source="--auth"
629
+ )
400
630
 
401
631
  if not update_data:
402
632
  raise click.ClickException("No update fields specified")
@@ -425,7 +655,20 @@ def update(
425
655
  @output_flags()
426
656
  @click.pass_context
427
657
  def delete(ctx: Any, mcp_ref: str, yes: bool) -> None:
428
- """Delete an MCP."""
658
+ """Delete an MCP after confirmation.
659
+
660
+ Args:
661
+ ctx: Click context containing output format preferences
662
+ mcp_ref: MCP reference (ID or name)
663
+ yes: Skip confirmation prompt if True
664
+
665
+ Raises:
666
+ ClickException: If MCP not found or deletion fails
667
+
668
+ Note:
669
+ Requires confirmation unless --yes flag is provided.
670
+ Deletion is permanent and cannot be undone.
671
+ """
429
672
  try:
430
673
  client = get_client(ctx)
431
674
 
glaip_sdk/cli/main.py CHANGED
@@ -24,6 +24,7 @@ from glaip_sdk.cli.commands.configure import (
24
24
  from glaip_sdk.cli.commands.mcps import mcps_group
25
25
  from glaip_sdk.cli.commands.models import models_group
26
26
  from glaip_sdk.cli.commands.tools import tools_group
27
+ from glaip_sdk.cli.update_notifier import maybe_notify_update
27
28
  from glaip_sdk.cli.utils import spinner_context, update_spinner
28
29
  from glaip_sdk.config.constants import (
29
30
  DEFAULT_AGENT_RUN_TIMEOUT,
@@ -82,6 +83,13 @@ def main(
82
83
 
83
84
  ctx.obj["tty"] = not no_tty
84
85
 
86
+ if not ctx.resilient_parsing and ctx.obj["tty"]:
87
+ console = Console()
88
+ maybe_notify_update(
89
+ _SDK_VERSION,
90
+ console=console,
91
+ )
92
+
85
93
  if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
86
94
  if _should_launch_slash(ctx) and SlashSession is not None:
87
95
  session = SlashSession(ctx)