glaip-sdk 0.0.1b5__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.
@@ -0,0 +1,473 @@
1
+ """MCP management commands.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ import json
8
+
9
+ import click
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+
14
+ from glaip_sdk.utils import is_uuid
15
+
16
+ from ..utils import (
17
+ get_client,
18
+ handle_ambiguous_resource,
19
+ output_flags,
20
+ output_list,
21
+ output_result,
22
+ )
23
+
24
+ console = Console()
25
+
26
+
27
+ @click.group(name="mcps", no_args_is_help=True)
28
+ def mcps_group():
29
+ """MCP management operations."""
30
+ pass
31
+
32
+
33
+ def _resolve_mcp(ctx, client, ref, select=None):
34
+ """Resolve MCP reference (ID or name) with ambiguity handling."""
35
+ if is_uuid(ref):
36
+ return client.mcps.get_mcp_by_id(ref)
37
+
38
+ # Find MCPs by name
39
+ matches = client.mcps.find_mcps(name=ref)
40
+ if not matches:
41
+ raise click.ClickException(f"MCP '{ref}' not found")
42
+
43
+ if len(matches) == 1:
44
+ return matches[0]
45
+
46
+ # Multiple matches - handle ambiguity
47
+ if select:
48
+ idx = int(select) - 1
49
+ if not (0 <= idx < len(matches)):
50
+ raise click.ClickException(f"--select must be 1..{len(matches)}")
51
+ return matches[idx]
52
+
53
+ return handle_ambiguous_resource(ctx, "MCP", ref, matches)
54
+
55
+
56
+ @mcps_group.command(name="list")
57
+ @output_flags()
58
+ @click.pass_context
59
+ def list_mcps(ctx):
60
+ """List all MCPs."""
61
+ try:
62
+ client = get_client(ctx)
63
+ mcps = client.mcps.list_mcps()
64
+
65
+ # Define table columns: (data_key, header, style, width)
66
+ columns = [
67
+ ("id", "ID", "dim", 36),
68
+ ("name", "Name", "cyan", None),
69
+ ("config", "Config", "blue", None),
70
+ ]
71
+
72
+ # Transform function for safe dictionary access
73
+ def transform_mcp(mcp):
74
+ # Handle both dict and object formats
75
+ if isinstance(mcp, dict):
76
+ return {
77
+ "id": str(mcp.get("id", "N/A")),
78
+ "name": mcp.get("name", "N/A"),
79
+ "config": str(mcp.get("config", "N/A"))[:50] + "..."
80
+ if mcp.get("config")
81
+ else "N/A",
82
+ }
83
+ else:
84
+ # Fallback to attribute access
85
+ return {
86
+ "id": str(getattr(mcp, "id", "N/A")),
87
+ "name": getattr(mcp, "name", "N/A"),
88
+ "config": str(getattr(mcp, "config", "N/A"))[:50] + "..."
89
+ if getattr(mcp, "config", None)
90
+ else "N/A",
91
+ }
92
+
93
+ output_list(ctx, mcps, "🔌 Available MCPs", columns, transform_mcp)
94
+
95
+ except Exception as e:
96
+ raise click.ClickException(str(e))
97
+
98
+
99
+ @mcps_group.command()
100
+ @click.option("--name", required=True, help="MCP name")
101
+ @click.option("--transport", required=True, help="MCP transport protocol")
102
+ @click.option("--description", help="MCP description")
103
+ @click.option("--config", help="JSON configuration string")
104
+ @output_flags()
105
+ @click.pass_context
106
+ def create(ctx, name, transport, description, config):
107
+ """Create a new MCP."""
108
+ try:
109
+ client = get_client(ctx)
110
+
111
+ # Parse config if provided
112
+ mcp_config = {}
113
+ if config:
114
+ try:
115
+ mcp_config = json.loads(config)
116
+ except json.JSONDecodeError:
117
+ raise click.ClickException("Invalid JSON in --config")
118
+
119
+ mcp = client.mcps.create_mcp(
120
+ name=name,
121
+ type="server", # Always server type
122
+ transport=transport,
123
+ description=description,
124
+ config=mcp_config,
125
+ )
126
+
127
+ if ctx.obj.get("view") == "json":
128
+ click.echo(json.dumps(mcp.model_dump(), indent=2))
129
+ else:
130
+ # Rich output
131
+ panel = Panel(
132
+ f"[green]✅ MCP '{mcp.name}' created successfully![/green]\n\n"
133
+ f"ID: {mcp.id}\n"
134
+ f"Type: server (default)\n"
135
+ f"Description: {description or 'No description'}",
136
+ title="🔌 MCP Created",
137
+ border_style="green",
138
+ )
139
+ console.print(panel)
140
+
141
+ except Exception as e:
142
+ if ctx.obj.get("view") == "json":
143
+ click.echo(json.dumps({"error": str(e)}, indent=2))
144
+ else:
145
+ console.print(f"[red]Error creating MCP: {e}[/red]")
146
+ raise click.ClickException(str(e))
147
+
148
+
149
+ @mcps_group.command()
150
+ @click.argument("mcp_ref")
151
+ @output_flags()
152
+ @click.pass_context
153
+ def get(ctx, mcp_ref):
154
+ """Get MCP details."""
155
+ try:
156
+ client = get_client(ctx)
157
+
158
+ # Get MCP by ID or name (shows all matches for names)
159
+ if is_uuid(mcp_ref):
160
+ mcp = client.mcps.get_mcp_by_id(mcp_ref)
161
+ else:
162
+ # Find MCPs by name
163
+ matches = client.mcps.find_mcps(name=mcp_ref)
164
+ if not matches:
165
+ raise click.ClickException(f"MCP '{mcp_ref}' not found")
166
+ elif len(matches) == 1:
167
+ mcp = matches[0]
168
+ else:
169
+ # Show all matches
170
+ console.print(
171
+ f"[yellow]Multiple MCPs found with name '{mcp_ref}':[/yellow]"
172
+ )
173
+ for i, match in enumerate(matches, 1):
174
+ console.print(f" {i}. {match.name} (ID: {match.id})")
175
+ console.print("[yellow]Use the ID for unambiguous operations.[/yellow]")
176
+ return
177
+
178
+ # Create result data with actual available fields
179
+ result_data = {
180
+ "id": str(getattr(mcp, "id", "N/A")),
181
+ "name": getattr(mcp, "name", "N/A"),
182
+ "type": getattr(mcp, "type", "N/A"),
183
+ "config": getattr(mcp, "config", "N/A"),
184
+ "status": getattr(mcp, "status", "N/A"),
185
+ "connection_status": getattr(mcp, "connection_status", "N/A"),
186
+ }
187
+
188
+ output_result(
189
+ ctx, result_data, title="MCP Details", panel_title=f"🔌 {mcp.name}"
190
+ )
191
+
192
+ except Exception as e:
193
+ raise click.ClickException(str(e))
194
+
195
+
196
+ @mcps_group.command("tools")
197
+ @click.argument("mcp_ref")
198
+ @output_flags()
199
+ @click.pass_context
200
+ def list_tools(ctx, mcp_ref):
201
+ """List tools from MCP."""
202
+ try:
203
+ client = get_client(ctx)
204
+
205
+ # Get MCP by ID or name (shows all matches for names)
206
+ if is_uuid(mcp_ref):
207
+ mcp = client.get_mcp_by_id(mcp_ref)
208
+ else:
209
+ # Find MCPs by name
210
+ matches = client.find_mcps(name=mcp_ref)
211
+ if not matches:
212
+ raise click.ClickException(f"MCP '{mcp_ref}' not found")
213
+ elif len(matches) == 1:
214
+ mcp = matches[0]
215
+ else:
216
+ # Show all matches
217
+ console.print(
218
+ f"[yellow]Multiple MCPs found with name '{mcp_ref}':[/yellow]"
219
+ )
220
+ for i, match in enumerate(matches, 1):
221
+ console.print(f" {i}. {match.name} (ID: {match.id})")
222
+ console.print("[yellow]Use the ID for unambiguous operations.[/yellow]")
223
+ return
224
+
225
+ # Get tools from MCP
226
+ tools = client.mcps.get_mcp_tools(mcp.id)
227
+
228
+ if ctx.obj.get("view") == "json":
229
+ click.echo(json.dumps(tools, indent=2))
230
+ else: # rich output
231
+ if tools:
232
+ table = Table(
233
+ title=f"🔧 Tools from MCP: {mcp.name}",
234
+ show_header=True,
235
+ header_style="bold magenta",
236
+ )
237
+ table.add_column("Name", style="cyan", no_wrap=True)
238
+ table.add_column("Description", style="green")
239
+ table.add_column("Type", style="yellow")
240
+
241
+ for tool in tools:
242
+ table.add_row(
243
+ tool.get("name", "N/A"),
244
+ tool.get("description", "N/A")[:50] + "..."
245
+ if len(tool.get("description", "")) > 50
246
+ else tool.get("description", "N/A"),
247
+ tool.get("type", "N/A"),
248
+ )
249
+ console.print(table)
250
+ else:
251
+ console.print(f"[yellow]No tools found in MCP '{mcp.name}'[/yellow]")
252
+
253
+ except Exception as e:
254
+ raise click.ClickException(str(e))
255
+
256
+
257
+ @mcps_group.command("tools-from-config")
258
+ @click.option(
259
+ "--from-file",
260
+ "config_file",
261
+ type=click.Path(exists=True),
262
+ required=True,
263
+ help="MCP config JSON file",
264
+ )
265
+ @output_flags()
266
+ @click.pass_context
267
+ def tools_from_config(ctx, config_file):
268
+ """Fetch tools from MCP config."""
269
+ try:
270
+ client = get_client(ctx)
271
+
272
+ # Load MCP config from file
273
+ with open(config_file) as f:
274
+ config = json.load(f)
275
+
276
+ if ctx.obj.get("view") != "json":
277
+ console.print(
278
+ f"[yellow]Fetching tools from MCP config in {config_file}...[/yellow]"
279
+ )
280
+
281
+ # Get tools from MCP config
282
+ tools = client.mcps.get_tools_from_mcp_config(config)
283
+
284
+ if ctx.obj.get("view") == "json":
285
+ click.echo(json.dumps(tools, indent=2))
286
+ else: # rich output
287
+ if tools:
288
+ table = Table(
289
+ title="🔧 Tools from MCP Config",
290
+ show_header=True,
291
+ header_style="bold magenta",
292
+ )
293
+ table.add_column("Name", style="cyan", no_wrap=True)
294
+ table.add_column("Description", style="green")
295
+ table.add_column("Type", style="yellow")
296
+
297
+ for tool in tools:
298
+ table.add_row(
299
+ tool.get("name", "N/A"),
300
+ tool.get("description", "N/A")[:50] + "..."
301
+ if len(tool.get("description", "")) > 50
302
+ else tool.get("description", "N/A"),
303
+ tool.get("type", "N/A"),
304
+ )
305
+ console.print(table)
306
+ else:
307
+ console.print("[yellow]No tools found in MCP config[/yellow]")
308
+
309
+ except Exception as e:
310
+ raise click.ClickException(str(e))
311
+
312
+
313
+ @mcps_group.command("test-connection")
314
+ @click.option(
315
+ "--from-file",
316
+ "config_file",
317
+ required=True,
318
+ help="MCP config JSON file",
319
+ )
320
+ @output_flags()
321
+ @click.pass_context
322
+ def test_connection(ctx, config_file):
323
+ """Test MCP connection using config file."""
324
+ try:
325
+ client = get_client(ctx)
326
+
327
+ # Load MCP config from file
328
+ with open(config_file) as f:
329
+ config = json.load(f)
330
+
331
+ if ctx.obj.get("view") != "json":
332
+ console.print(
333
+ f"[yellow]Testing MCP connection with config from {config_file}...[/yellow]"
334
+ )
335
+
336
+ # Test connection using config
337
+ result = client.mcps.test_mcp_connection_from_config(config)
338
+
339
+ if ctx.obj.get("view") == "json":
340
+ click.echo(json.dumps(result, indent=2))
341
+ else:
342
+ success_panel = Panel(
343
+ f"[green]✓[/green] MCP connection test successful!\n\n"
344
+ f"[bold]Result:[/bold] {result}",
345
+ title="🔌 Connection Test",
346
+ border_style="green",
347
+ )
348
+ console.print(success_panel)
349
+
350
+ except Exception as e:
351
+ raise click.ClickException(str(e))
352
+
353
+
354
+ @mcps_group.command()
355
+ @click.argument("mcp_ref")
356
+ @click.option("--name", help="New MCP name")
357
+ @click.option("--description", help="New description")
358
+ @click.option("--config", help="JSON configuration string")
359
+ @output_flags()
360
+ @click.pass_context
361
+ def update(ctx, mcp_ref, name, description, config):
362
+ """Update an existing MCP."""
363
+ try:
364
+ client = get_client(ctx)
365
+
366
+ # Get MCP by ID or name (shows all matches for names)
367
+ if is_uuid(mcp_ref):
368
+ mcp = client.mcps.get_mcp_by_id(mcp_ref)
369
+ else:
370
+ # Find MCPs by name
371
+ matches = client.mcps.find_mcps(name=mcp_ref)
372
+ if not matches:
373
+ raise click.ClickException(f"MCP '{mcp_ref}' not found")
374
+ elif len(matches) == 1:
375
+ mcp = matches[0]
376
+ else:
377
+ # Show all matches
378
+ console.print(
379
+ f"[yellow]Multiple MCPs found with name '{mcp_ref}':[/yellow]"
380
+ )
381
+ for i, match in enumerate(matches, 1):
382
+ console.print(f" {i}. {match.name} (ID: {match.id})")
383
+ console.print("[yellow]Use the ID for unambiguous operations.[/yellow]")
384
+ return
385
+
386
+ # Build update data
387
+ update_data = {}
388
+ if name is not None:
389
+ update_data["name"] = name
390
+ if description is not None:
391
+ update_data["description"] = description
392
+ if config is not None:
393
+ try:
394
+ update_data["config"] = json.loads(config)
395
+ except json.JSONDecodeError:
396
+ raise click.ClickException("Invalid JSON in --config")
397
+
398
+ if not update_data:
399
+ raise click.ClickException("No update fields specified")
400
+
401
+ # Update MCP
402
+ updated_mcp = client.mcps.update_mcp(mcp.id, update_data)
403
+
404
+ if ctx.obj.get("view") == "json":
405
+ click.echo(json.dumps(updated_mcp.model_dump(), indent=2))
406
+ else:
407
+ console.print(
408
+ f"[green]✅ MCP '{updated_mcp.name}' updated successfully[/green]"
409
+ )
410
+
411
+ except Exception as e:
412
+ if ctx.obj.get("view") == "json":
413
+ click.echo(json.dumps({"error": str(e)}, indent=2))
414
+ else:
415
+ console.print(f"[red]Error updating MCP: {e}[/red]")
416
+ raise click.ClickException(str(e))
417
+
418
+
419
+ @mcps_group.command()
420
+ @click.argument("mcp_ref")
421
+ @click.option("-y", "--yes", is_flag=True, help="Skip confirmation")
422
+ @output_flags()
423
+ @click.pass_context
424
+ def delete(ctx, mcp_ref, yes):
425
+ """Delete an MCP."""
426
+ try:
427
+ client = get_client(ctx)
428
+
429
+ # Get MCP by ID or name (shows all matches for names)
430
+ if is_uuid(mcp_ref):
431
+ mcp = client.mcps.get_mcp_by_id(mcp_ref)
432
+ else:
433
+ # Find MCPs by name
434
+ matches = client.mcps.find_mcps(name=mcp_ref)
435
+ if not matches:
436
+ raise click.ClickException(f"MCP '{mcp_ref}' not found")
437
+ elif len(matches) == 1:
438
+ mcp = matches[0]
439
+ else:
440
+ # Show all matches
441
+ console.print(
442
+ f"[yellow]Multiple MCPs found with name '{mcp_ref}':[/yellow]"
443
+ )
444
+ for i, match in enumerate(matches, 1):
445
+ console.print(f" {i}. {match.name} (ID: {match.id})")
446
+ console.print("[yellow]Use the ID for unambiguous operations.[/yellow]")
447
+ return
448
+
449
+ # Confirm deletion
450
+ if not yes and not click.confirm(
451
+ f"Are you sure you want to delete MCP '{mcp.name}'?"
452
+ ):
453
+ if ctx.obj.get("view") != "json":
454
+ console.print("Deletion cancelled.")
455
+ return
456
+
457
+ client.mcps.delete_mcp(mcp.id)
458
+
459
+ if ctx.obj.get("view") == "json":
460
+ click.echo(
461
+ json.dumps(
462
+ {"success": True, "message": f"MCP '{mcp.name}' deleted"}, indent=2
463
+ )
464
+ )
465
+ else:
466
+ console.print(f"[green]✅ MCP '{mcp.name}' deleted successfully[/green]")
467
+
468
+ except Exception as e:
469
+ if ctx.obj.get("view") == "json":
470
+ click.echo(json.dumps({"error": str(e)}, indent=2))
471
+ else:
472
+ console.print(f"[red]Error deleting MCP: {e}[/red]")
473
+ raise click.ClickException(str(e))
@@ -0,0 +1,52 @@
1
+ """Language models commands.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ import click
8
+ from rich.console import Console
9
+
10
+ from ..utils import get_client, output_flags, output_list
11
+
12
+ console = Console()
13
+
14
+
15
+ @click.group(name="models", no_args_is_help=True)
16
+ def models_group():
17
+ """Language model operations."""
18
+ pass
19
+
20
+
21
+ @models_group.command(name="list")
22
+ @output_flags()
23
+ @click.pass_context
24
+ def list_models(ctx):
25
+ """List available language models."""
26
+ try:
27
+ client = get_client(ctx)
28
+ models = client.list_language_models()
29
+
30
+ # Define table columns: (data_key, header, style, width)
31
+ columns = [
32
+ ("id", "ID", "dim", 36),
33
+ ("provider", "Provider", "cyan", None),
34
+ ("name", "Model", "green", None),
35
+ ("base_url", "Base URL", "yellow", None),
36
+ ]
37
+
38
+ # Transform function for safe dictionary access
39
+ def transform_model(model):
40
+ return {
41
+ "id": str(model.get("id", "N/A")),
42
+ "provider": model.get("provider", "N/A"),
43
+ "name": model.get("name", "N/A"),
44
+ "base_url": model.get("base_url", "Default") or "Default",
45
+ }
46
+
47
+ output_list(
48
+ ctx, models, "🧠 Available Language Models", columns, transform_model
49
+ )
50
+
51
+ except Exception as e:
52
+ raise click.ClickException(str(e))