glaip-sdk 0.0.10__py3-none-any.whl → 0.0.11__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.
- glaip_sdk/cli/auth.py +414 -0
- glaip_sdk/cli/commands/agents.py +25 -3
- glaip_sdk/cli/commands/mcps.py +276 -92
- glaip_sdk/cli/main.py +8 -0
- glaip_sdk/cli/slash/session.py +24 -9
- glaip_sdk/cli/update_notifier.py +107 -0
- glaip_sdk/cli/utils.py +23 -8
- glaip_sdk/models.py +0 -2
- glaip_sdk/utils/import_export.py +3 -7
- glaip_sdk/utils/serialization.py +93 -2
- {glaip_sdk-0.0.10.dist-info → glaip_sdk-0.0.11.dist-info}/METADATA +2 -1
- {glaip_sdk-0.0.10.dist-info → glaip_sdk-0.0.11.dist-info}/RECORD +14 -12
- {glaip_sdk-0.0.10.dist-info → glaip_sdk-0.0.11.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.10.dist-info → glaip_sdk-0.0.11.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/commands/mcps.py
CHANGED
|
@@ -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,9 +22,6 @@ 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
|
)
|
|
@@ -40,20 +38,41 @@ from glaip_sdk.cli.utils import (
|
|
|
40
38
|
)
|
|
41
39
|
from glaip_sdk.rich_components import AIPPanel
|
|
42
40
|
from glaip_sdk.utils import format_datetime
|
|
41
|
+
from glaip_sdk.utils.serialization import (
|
|
42
|
+
build_mcp_export_payload,
|
|
43
|
+
write_resource_export,
|
|
44
|
+
)
|
|
43
45
|
|
|
44
46
|
console = Console()
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
@click.group(name="mcps", no_args_is_help=True)
|
|
48
50
|
def mcps_group() -> None:
|
|
49
|
-
"""MCP management operations.
|
|
51
|
+
"""MCP management operations.
|
|
52
|
+
|
|
53
|
+
Provides commands for creating, listing, updating, deleting, and managing
|
|
54
|
+
Model Context Protocol (MCP) configurations.
|
|
55
|
+
"""
|
|
50
56
|
pass
|
|
51
57
|
|
|
52
58
|
|
|
53
59
|
def _resolve_mcp(
|
|
54
60
|
ctx: Any, client: Any, ref: str, select: int | None = None
|
|
55
61
|
) -> Any | None:
|
|
56
|
-
"""Resolve MCP reference (ID or name) with ambiguity handling.
|
|
62
|
+
"""Resolve MCP reference (ID or name) with ambiguity handling.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
ctx: Click context object
|
|
66
|
+
client: API client instance
|
|
67
|
+
ref: MCP reference (ID or name)
|
|
68
|
+
select: Index to select when multiple matches found
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
MCP object if found, None otherwise
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
ClickException: If MCP not found or selection invalid
|
|
75
|
+
"""
|
|
57
76
|
return resolve_resource_reference(
|
|
58
77
|
ctx,
|
|
59
78
|
client,
|
|
@@ -70,7 +89,14 @@ def _resolve_mcp(
|
|
|
70
89
|
@output_flags()
|
|
71
90
|
@click.pass_context
|
|
72
91
|
def list_mcps(ctx: Any) -> None:
|
|
73
|
-
"""List all MCPs.
|
|
92
|
+
"""List all MCPs in a formatted table.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
ctx: Click context containing output format preferences
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ClickException: If API request fails
|
|
99
|
+
"""
|
|
74
100
|
try:
|
|
75
101
|
client = get_client(ctx)
|
|
76
102
|
with spinner_context(
|
|
@@ -117,7 +143,18 @@ def list_mcps(ctx: Any) -> None:
|
|
|
117
143
|
def create(
|
|
118
144
|
ctx: Any, name: str, transport: str, description: str | None, config: str | None
|
|
119
145
|
) -> None:
|
|
120
|
-
"""Create a new MCP.
|
|
146
|
+
"""Create a new MCP with specified configuration.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
ctx: Click context containing output format preferences
|
|
150
|
+
name: MCP name (required)
|
|
151
|
+
transport: MCP transport protocol (required)
|
|
152
|
+
description: Optional MCP description
|
|
153
|
+
config: JSON configuration string for MCP settings
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
ClickException: If JSON parsing fails or API request fails
|
|
157
|
+
"""
|
|
121
158
|
try:
|
|
122
159
|
client = get_client(ctx)
|
|
123
160
|
|
|
@@ -163,22 +200,191 @@ def create(
|
|
|
163
200
|
raise click.ClickException(str(e))
|
|
164
201
|
|
|
165
202
|
|
|
203
|
+
def _handle_mcp_export(
|
|
204
|
+
ctx: Any,
|
|
205
|
+
client: Any,
|
|
206
|
+
mcp: Any,
|
|
207
|
+
export_path: Path,
|
|
208
|
+
no_auth_prompt: bool,
|
|
209
|
+
auth_placeholder: str,
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Handle MCP export to file with format detection and auth handling.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
ctx: Click context for spinner management
|
|
215
|
+
client: API client for fetching MCP details
|
|
216
|
+
mcp: MCP object to export
|
|
217
|
+
export_path: Target file path (format detected from extension)
|
|
218
|
+
no_auth_prompt: Skip interactive secret prompts if True
|
|
219
|
+
auth_placeholder: Placeholder text for missing secrets
|
|
220
|
+
|
|
221
|
+
Note:
|
|
222
|
+
Supports JSON (.json) and YAML (.yaml/.yml) export formats.
|
|
223
|
+
In interactive mode, prompts for secret values.
|
|
224
|
+
In non-interactive mode, uses placeholder values.
|
|
225
|
+
"""
|
|
226
|
+
# Auto-detect format from file extension
|
|
227
|
+
detected_format = detect_export_format(export_path)
|
|
228
|
+
|
|
229
|
+
# Always export comprehensive data - re-fetch with full details
|
|
230
|
+
try:
|
|
231
|
+
with spinner_context(
|
|
232
|
+
ctx,
|
|
233
|
+
"[bold blue]Fetching complete MCP details…[/bold blue]",
|
|
234
|
+
console_override=console,
|
|
235
|
+
):
|
|
236
|
+
mcp = client.mcps.get_mcp_by_id(mcp.id)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
console.print(
|
|
239
|
+
Text(f"[yellow]⚠️ Could not fetch full MCP details: {e}[/yellow]")
|
|
240
|
+
)
|
|
241
|
+
console.print(Text("[yellow]⚠️ Proceeding with available data[/yellow]"))
|
|
242
|
+
|
|
243
|
+
# Determine if we should prompt for secrets
|
|
244
|
+
prompt_for_secrets = not no_auth_prompt and sys.stdin.isatty()
|
|
245
|
+
|
|
246
|
+
# Warn user if non-interactive mode forces placeholder usage
|
|
247
|
+
if not no_auth_prompt and not sys.stdin.isatty():
|
|
248
|
+
console.print(
|
|
249
|
+
Text(
|
|
250
|
+
"[yellow]⚠️ Non-interactive mode detected. "
|
|
251
|
+
"Using placeholder values for secrets.[/yellow]"
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Build and write export payload
|
|
256
|
+
if prompt_for_secrets:
|
|
257
|
+
# Interactive mode: no spinner during prompts
|
|
258
|
+
export_payload = build_mcp_export_payload(
|
|
259
|
+
mcp,
|
|
260
|
+
prompt_for_secrets=prompt_for_secrets,
|
|
261
|
+
placeholder=auth_placeholder,
|
|
262
|
+
console=console,
|
|
263
|
+
)
|
|
264
|
+
with spinner_context(
|
|
265
|
+
ctx,
|
|
266
|
+
"[bold blue]Writing export file…[/bold blue]",
|
|
267
|
+
console_override=console,
|
|
268
|
+
):
|
|
269
|
+
write_resource_export(export_path, export_payload, detected_format)
|
|
270
|
+
else:
|
|
271
|
+
# Non-interactive mode: spinner for entire export process
|
|
272
|
+
with spinner_context(
|
|
273
|
+
ctx,
|
|
274
|
+
"[bold blue]Exporting MCP configuration…[/bold blue]",
|
|
275
|
+
console_override=console,
|
|
276
|
+
):
|
|
277
|
+
export_payload = build_mcp_export_payload(
|
|
278
|
+
mcp,
|
|
279
|
+
prompt_for_secrets=prompt_for_secrets,
|
|
280
|
+
placeholder=auth_placeholder,
|
|
281
|
+
console=console,
|
|
282
|
+
)
|
|
283
|
+
write_resource_export(export_path, export_payload, detected_format)
|
|
284
|
+
|
|
285
|
+
console.print(
|
|
286
|
+
Text(
|
|
287
|
+
f"[green]✅ Complete MCP configuration exported to: "
|
|
288
|
+
f"{export_path} (format: {detected_format})[/green]"
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _display_mcp_details(ctx: Any, client: Any, mcp: Any) -> None:
|
|
294
|
+
"""Display MCP details using raw API data or fallback to Pydantic model.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
ctx: Click context containing output format preferences
|
|
298
|
+
client: API client for fetching raw MCP data
|
|
299
|
+
mcp: MCP object to display details for
|
|
300
|
+
|
|
301
|
+
Note:
|
|
302
|
+
Attempts to fetch raw API data first to preserve all fields.
|
|
303
|
+
Falls back to Pydantic model data if raw data unavailable.
|
|
304
|
+
Formats datetime fields for better readability.
|
|
305
|
+
"""
|
|
306
|
+
# Try to fetch raw API data first to preserve ALL fields
|
|
307
|
+
with spinner_context(
|
|
308
|
+
ctx,
|
|
309
|
+
"[bold blue]Fetching detailed MCP data…[/bold blue]",
|
|
310
|
+
console_override=console,
|
|
311
|
+
):
|
|
312
|
+
raw_mcp_data = fetch_raw_resource_details(client, mcp, "mcps")
|
|
313
|
+
|
|
314
|
+
if raw_mcp_data:
|
|
315
|
+
# Use raw API data - this preserves ALL fields
|
|
316
|
+
formatted_data = raw_mcp_data.copy()
|
|
317
|
+
if "created_at" in formatted_data:
|
|
318
|
+
formatted_data["created_at"] = format_datetime(formatted_data["created_at"])
|
|
319
|
+
if "updated_at" in formatted_data:
|
|
320
|
+
formatted_data["updated_at"] = format_datetime(formatted_data["updated_at"])
|
|
321
|
+
|
|
322
|
+
output_result(
|
|
323
|
+
ctx,
|
|
324
|
+
formatted_data,
|
|
325
|
+
title="MCP Details",
|
|
326
|
+
panel_title=f"🔌 {raw_mcp_data.get('name', 'Unknown')}",
|
|
327
|
+
)
|
|
328
|
+
else:
|
|
329
|
+
# Fall back to Pydantic model data
|
|
330
|
+
console.print("[yellow]Falling back to Pydantic model data[/yellow]")
|
|
331
|
+
result_data = {
|
|
332
|
+
"id": str(getattr(mcp, "id", "N/A")),
|
|
333
|
+
"name": getattr(mcp, "name", "N/A"),
|
|
334
|
+
"type": getattr(mcp, "type", "N/A"),
|
|
335
|
+
"config": getattr(mcp, "config", "N/A"),
|
|
336
|
+
"status": getattr(mcp, "status", "N/A"),
|
|
337
|
+
"connection_status": getattr(mcp, "connection_status", "N/A"),
|
|
338
|
+
}
|
|
339
|
+
output_result(
|
|
340
|
+
ctx, result_data, title="MCP Details", panel_title=f"🔌 {mcp.name}"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
166
344
|
@mcps_group.command()
|
|
167
345
|
@click.argument("mcp_ref")
|
|
168
346
|
@click.option(
|
|
169
347
|
"--export",
|
|
170
348
|
type=click.Path(dir_okay=False, writable=True),
|
|
171
|
-
help="Export complete MCP configuration to file
|
|
349
|
+
help="Export complete MCP configuration to file "
|
|
350
|
+
"(format auto-detected from .json/.yaml extension)",
|
|
351
|
+
)
|
|
352
|
+
@click.option(
|
|
353
|
+
"--no-auth-prompt",
|
|
354
|
+
is_flag=True,
|
|
355
|
+
help="Skip interactive secret prompts and use placeholder values.",
|
|
356
|
+
)
|
|
357
|
+
@click.option(
|
|
358
|
+
"--auth-placeholder",
|
|
359
|
+
default="<INSERT VALUE>",
|
|
360
|
+
show_default=True,
|
|
361
|
+
help="Placeholder text used when secrets are unavailable.",
|
|
172
362
|
)
|
|
173
363
|
@output_flags()
|
|
174
364
|
@click.pass_context
|
|
175
|
-
def get(
|
|
176
|
-
|
|
365
|
+
def get(
|
|
366
|
+
ctx: Any,
|
|
367
|
+
mcp_ref: str,
|
|
368
|
+
export: str | None,
|
|
369
|
+
no_auth_prompt: bool,
|
|
370
|
+
auth_placeholder: str,
|
|
371
|
+
) -> None:
|
|
372
|
+
"""Get MCP details and optionally export configuration to file.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
ctx: Click context containing output format preferences
|
|
376
|
+
mcp_ref: MCP reference (ID or name)
|
|
377
|
+
export: Optional file path to export MCP configuration
|
|
378
|
+
no_auth_prompt: Skip interactive secret prompts if True
|
|
379
|
+
auth_placeholder: Placeholder text for missing secrets
|
|
380
|
+
|
|
381
|
+
Raises:
|
|
382
|
+
ClickException: If MCP not found or export fails
|
|
177
383
|
|
|
178
384
|
Examples:
|
|
179
385
|
aip mcps get my-mcp
|
|
180
|
-
aip mcps get my-mcp --export mcp.json #
|
|
181
|
-
aip mcps get my-mcp --export mcp.yaml #
|
|
386
|
+
aip mcps get my-mcp --export mcp.json # Export as JSON
|
|
387
|
+
aip mcps get my-mcp --export mcp.yaml # Export as YAML
|
|
182
388
|
"""
|
|
183
389
|
try:
|
|
184
390
|
client = get_client(ctx)
|
|
@@ -188,83 +394,12 @@ def get(ctx: Any, mcp_ref: str, export: str | None) -> None:
|
|
|
188
394
|
|
|
189
395
|
# Handle export option
|
|
190
396
|
if export:
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
)
|
|
397
|
+
_handle_mcp_export(
|
|
398
|
+
ctx, client, mcp, Path(export), no_auth_prompt, auth_placeholder
|
|
221
399
|
)
|
|
222
400
|
|
|
223
|
-
#
|
|
224
|
-
|
|
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')}",
|
|
250
|
-
)
|
|
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
|
-
|
|
265
|
-
output_result(
|
|
266
|
-
ctx, result_data, title="MCP Details", panel_title=f"🔌 {mcp.name}"
|
|
267
|
-
)
|
|
401
|
+
# Display MCP details
|
|
402
|
+
_display_mcp_details(ctx, client, mcp)
|
|
268
403
|
|
|
269
404
|
except Exception as e:
|
|
270
405
|
raise click.ClickException(str(e))
|
|
@@ -275,7 +410,15 @@ def get(ctx: Any, mcp_ref: str, export: str | None) -> None:
|
|
|
275
410
|
@output_flags()
|
|
276
411
|
@click.pass_context
|
|
277
412
|
def list_tools(ctx: Any, mcp_ref: str) -> None:
|
|
278
|
-
"""List tools from MCP.
|
|
413
|
+
"""List tools available from a specific MCP.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
ctx: Click context containing output format preferences
|
|
417
|
+
mcp_ref: MCP reference (ID or name)
|
|
418
|
+
|
|
419
|
+
Raises:
|
|
420
|
+
ClickException: If MCP not found or tools fetch fails
|
|
421
|
+
"""
|
|
279
422
|
try:
|
|
280
423
|
client = get_client(ctx)
|
|
281
424
|
|
|
@@ -325,7 +468,19 @@ def list_tools(ctx: Any, mcp_ref: str) -> None:
|
|
|
325
468
|
@output_flags()
|
|
326
469
|
@click.pass_context
|
|
327
470
|
def connect(ctx: Any, config_file: str) -> None:
|
|
328
|
-
"""
|
|
471
|
+
"""Test MCP connection using a configuration file.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
ctx: Click context containing output format preferences
|
|
475
|
+
config_file: Path to MCP configuration JSON file
|
|
476
|
+
|
|
477
|
+
Raises:
|
|
478
|
+
ClickException: If config file invalid or connection test fails
|
|
479
|
+
|
|
480
|
+
Note:
|
|
481
|
+
Loads MCP configuration from JSON file and tests connectivity.
|
|
482
|
+
Displays success or failure with connection details.
|
|
483
|
+
"""
|
|
329
484
|
try:
|
|
330
485
|
client = get_client(ctx)
|
|
331
486
|
|
|
@@ -337,7 +492,8 @@ def connect(ctx: Any, config_file: str) -> None:
|
|
|
337
492
|
if view != "json":
|
|
338
493
|
console.print(
|
|
339
494
|
Text(
|
|
340
|
-
f"[yellow]Connecting to MCP with config from
|
|
495
|
+
f"[yellow]Connecting to MCP with config from "
|
|
496
|
+
f"{config_file}...[/yellow]"
|
|
341
497
|
)
|
|
342
498
|
)
|
|
343
499
|
|
|
@@ -379,7 +535,22 @@ def update(
|
|
|
379
535
|
description: str | None,
|
|
380
536
|
config: str | None,
|
|
381
537
|
) -> None:
|
|
382
|
-
"""Update an existing MCP.
|
|
538
|
+
"""Update an existing MCP with new configuration values.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
ctx: Click context containing output format preferences
|
|
542
|
+
mcp_ref: MCP reference (ID or name)
|
|
543
|
+
name: New MCP name (optional)
|
|
544
|
+
description: New description (optional)
|
|
545
|
+
config: New JSON configuration string (optional)
|
|
546
|
+
|
|
547
|
+
Raises:
|
|
548
|
+
ClickException: If MCP not found, JSON invalid, or no fields specified
|
|
549
|
+
|
|
550
|
+
Note:
|
|
551
|
+
At least one field must be specified for update.
|
|
552
|
+
Uses PUT for complete updates or PATCH for partial updates.
|
|
553
|
+
"""
|
|
383
554
|
try:
|
|
384
555
|
client = get_client(ctx)
|
|
385
556
|
|
|
@@ -425,7 +596,20 @@ def update(
|
|
|
425
596
|
@output_flags()
|
|
426
597
|
@click.pass_context
|
|
427
598
|
def delete(ctx: Any, mcp_ref: str, yes: bool) -> None:
|
|
428
|
-
"""Delete an MCP.
|
|
599
|
+
"""Delete an MCP after confirmation.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
ctx: Click context containing output format preferences
|
|
603
|
+
mcp_ref: MCP reference (ID or name)
|
|
604
|
+
yes: Skip confirmation prompt if True
|
|
605
|
+
|
|
606
|
+
Raises:
|
|
607
|
+
ClickException: If MCP not found or deletion fails
|
|
608
|
+
|
|
609
|
+
Note:
|
|
610
|
+
Requires confirmation unless --yes flag is provided.
|
|
611
|
+
Deletion is permanent and cannot be undone.
|
|
612
|
+
"""
|
|
429
613
|
try:
|
|
430
614
|
client = get_client(ctx)
|
|
431
615
|
|
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)
|
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -76,6 +76,8 @@ class SlashSession:
|
|
|
76
76
|
self._setup_prompt_toolkit()
|
|
77
77
|
self._register_defaults()
|
|
78
78
|
self._branding = AIPBranding.create_from_sdk()
|
|
79
|
+
self._suppress_login_layout = False
|
|
80
|
+
self._default_actions_shown = False
|
|
79
81
|
|
|
80
82
|
# ------------------------------------------------------------------
|
|
81
83
|
# Session orchestration
|
|
@@ -96,7 +98,9 @@ class SlashSession:
|
|
|
96
98
|
if not self._ensure_configuration():
|
|
97
99
|
return
|
|
98
100
|
|
|
99
|
-
self._render_header(initial=
|
|
101
|
+
self._render_header(initial=not self._welcome_rendered)
|
|
102
|
+
if not self._default_actions_shown:
|
|
103
|
+
self._show_default_quick_actions()
|
|
100
104
|
self._render_home_hint()
|
|
101
105
|
self._run_interactive_loop()
|
|
102
106
|
|
|
@@ -154,6 +158,7 @@ class SlashSession:
|
|
|
154
158
|
self.console.print(
|
|
155
159
|
"[yellow]Configuration required.[/] Launching `/login` wizard..."
|
|
156
160
|
)
|
|
161
|
+
self._suppress_login_layout = True
|
|
157
162
|
try:
|
|
158
163
|
self._cmd_login([], False)
|
|
159
164
|
except KeyboardInterrupt:
|
|
@@ -161,6 +166,8 @@ class SlashSession:
|
|
|
161
166
|
"[red]Configuration aborted. Closing the command palette.[/red]"
|
|
162
167
|
)
|
|
163
168
|
return False
|
|
169
|
+
finally:
|
|
170
|
+
self._suppress_login_layout = False
|
|
164
171
|
|
|
165
172
|
return True
|
|
166
173
|
|
|
@@ -260,13 +267,12 @@ class SlashSession:
|
|
|
260
267
|
try:
|
|
261
268
|
self.ctx.invoke(configure_command)
|
|
262
269
|
self._config_cache = None
|
|
263
|
-
self.
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
)
|
|
270
|
+
if self._suppress_login_layout:
|
|
271
|
+
self._welcome_rendered = False
|
|
272
|
+
self._default_actions_shown = False
|
|
273
|
+
else:
|
|
274
|
+
self._render_header(initial=True)
|
|
275
|
+
self._show_default_quick_actions()
|
|
270
276
|
except click.ClickException as exc:
|
|
271
277
|
self.console.print(f"[red]{exc}[/red]")
|
|
272
278
|
return True
|
|
@@ -349,7 +355,7 @@ class SlashSession:
|
|
|
349
355
|
# running.
|
|
350
356
|
return True
|
|
351
357
|
|
|
352
|
-
self.console.print("[cyan]Closing the command palette.")
|
|
358
|
+
self.console.print("[cyan]Closing the command palette.[/cyan]")
|
|
353
359
|
return False
|
|
354
360
|
|
|
355
361
|
# ------------------------------------------------------------------
|
|
@@ -772,6 +778,15 @@ class SlashSession:
|
|
|
772
778
|
label = recent.get("name") or recent.get("id") or "-"
|
|
773
779
|
lines.append(f"[dim]Recent agent[/dim]: {label} [{recent.get('id', '-')}]")
|
|
774
780
|
|
|
781
|
+
def _show_default_quick_actions(self) -> None:
|
|
782
|
+
self._show_quick_actions(
|
|
783
|
+
[
|
|
784
|
+
(self.STATUS_COMMAND, "Verify the connection"),
|
|
785
|
+
(self.AGENTS_COMMAND, "Pick an agent to inspect or run"),
|
|
786
|
+
]
|
|
787
|
+
)
|
|
788
|
+
self._default_actions_shown = True
|
|
789
|
+
|
|
775
790
|
def _render_home_hint(self) -> None:
|
|
776
791
|
self.console.print(
|
|
777
792
|
AIPPanel(
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Utility helpers for checking and displaying SDK update notifications.
|
|
2
|
+
|
|
3
|
+
Author:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from packaging.version import InvalidVersion, Version
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
from glaip_sdk.rich_components import AIPPanel
|
|
17
|
+
|
|
18
|
+
FetchLatestVersion = Callable[[], str | None]
|
|
19
|
+
|
|
20
|
+
PYPI_JSON_URL = "https://pypi.org/pypi/{package}/json"
|
|
21
|
+
DEFAULT_TIMEOUT = 1.5 # seconds
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_version(value: str) -> Version | None:
|
|
25
|
+
"""Parse a version string into a `Version`, returning None on failure."""
|
|
26
|
+
try:
|
|
27
|
+
return Version(value)
|
|
28
|
+
except InvalidVersion:
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _fetch_latest_version(package_name: str) -> str | None:
|
|
33
|
+
"""Fetch the latest published version from PyPI."""
|
|
34
|
+
url = PYPI_JSON_URL.format(package=package_name)
|
|
35
|
+
timeout = httpx.Timeout(DEFAULT_TIMEOUT)
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
with httpx.Client(timeout=timeout) as client:
|
|
39
|
+
response = client.get(url, headers={"Accept": "application/json"})
|
|
40
|
+
response.raise_for_status()
|
|
41
|
+
payload = response.json()
|
|
42
|
+
except httpx.HTTPError:
|
|
43
|
+
return None
|
|
44
|
+
except ValueError:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
info = payload.get("info") if isinstance(payload, dict) else None
|
|
48
|
+
latest_version = info.get("version") if isinstance(info, dict) else None
|
|
49
|
+
if isinstance(latest_version, str) and latest_version.strip():
|
|
50
|
+
return latest_version.strip()
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _should_check_for_updates() -> bool:
|
|
55
|
+
"""Return False when update checks are explicitly disabled."""
|
|
56
|
+
return os.getenv("AIP_NO_UPDATE_CHECK") is None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_update_panel(
|
|
60
|
+
current_version: str,
|
|
61
|
+
latest_version: str,
|
|
62
|
+
) -> AIPPanel:
|
|
63
|
+
"""Create a Rich panel that prompts the user to update."""
|
|
64
|
+
message = (
|
|
65
|
+
f"[bold yellow]✨ Update available![/bold yellow] "
|
|
66
|
+
f"{current_version} → {latest_version}\n\n"
|
|
67
|
+
"See the latest release notes:\n"
|
|
68
|
+
f"https://pypi.org/project/glaip-sdk/{latest_version}/\n\n"
|
|
69
|
+
"[cyan]Run[/cyan] [bold]aip update[/bold] to install."
|
|
70
|
+
)
|
|
71
|
+
return AIPPanel(
|
|
72
|
+
message,
|
|
73
|
+
title="[bold green]AIP SDK Update[/bold green]",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def maybe_notify_update(
|
|
78
|
+
current_version: str,
|
|
79
|
+
*,
|
|
80
|
+
package_name: str = "glaip-sdk",
|
|
81
|
+
console: Console | None = None,
|
|
82
|
+
fetch_latest_version: FetchLatestVersion | None = None,
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Check PyPI for a newer version and display a prompt if one exists.
|
|
85
|
+
|
|
86
|
+
This function deliberately swallows network errors to avoid impacting CLI
|
|
87
|
+
startup time when offline or when PyPI is unavailable.
|
|
88
|
+
"""
|
|
89
|
+
if not _should_check_for_updates():
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
fetcher = fetch_latest_version or (lambda: _fetch_latest_version(package_name))
|
|
93
|
+
latest_version = fetcher()
|
|
94
|
+
if not latest_version:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
current = _parse_version(current_version)
|
|
98
|
+
latest = _parse_version(latest_version)
|
|
99
|
+
if current is None or latest is None or latest <= current:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
active_console = console or Console()
|
|
103
|
+
panel = _build_update_panel(current_version, latest_version)
|
|
104
|
+
active_console.print(panel)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
__all__ = ["maybe_notify_update"]
|