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.
- glaip_sdk/cli/auth.py +414 -0
- glaip_sdk/cli/commands/agents.py +25 -3
- glaip_sdk/cli/commands/mcps.py +356 -113
- glaip_sdk/cli/main.py +8 -0
- glaip_sdk/cli/mcp_validators.py +297 -0
- glaip_sdk/cli/parsers/__init__.py +9 -0
- glaip_sdk/cli/parsers/json_input.py +140 -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/client/mcps.py +3 -3
- 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.12.dist-info}/METADATA +2 -1
- {glaip_sdk-0.0.10.dist-info → glaip_sdk-0.0.12.dist-info}/RECORD +18 -13
- {glaip_sdk-0.0.10.dist-info → glaip_sdk-0.0.12.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.10.dist-info → glaip_sdk-0.0.12.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/auth.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""Authentication export helpers for MCP CLI commands.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for preparing authentication data for export,
|
|
4
|
+
including interactive secret capture and placeholder generation.
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterable, Mapping
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def prepare_authentication_export(
|
|
18
|
+
auth: dict[str, Any] | None,
|
|
19
|
+
*,
|
|
20
|
+
prompt_for_secrets: bool,
|
|
21
|
+
placeholder: str,
|
|
22
|
+
console: Console,
|
|
23
|
+
) -> dict[str, Any] | None:
|
|
24
|
+
"""Prepare authentication data for export with secret handling.
|
|
25
|
+
|
|
26
|
+
This function processes authentication objects from MCP resources and prepares
|
|
27
|
+
them for export. It handles secret capture (interactive or placeholder mode),
|
|
28
|
+
reconstructs proper authentication structures from helper metadata, and ensures
|
|
29
|
+
helper metadata doesn't leak into the final export.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
auth: Authentication dictionary from an MCP resource. May contain helper
|
|
33
|
+
metadata like ``header_keys`` that should be consumed and removed.
|
|
34
|
+
prompt_for_secrets: If True, interactively prompt for missing secrets.
|
|
35
|
+
If False, use ``placeholder`` automatically.
|
|
36
|
+
placeholder: Placeholder text to use for missing secrets when not prompting.
|
|
37
|
+
console: Rich ``Console`` instance for user interaction and warnings.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
A prepared authentication dictionary ready for export, or ``None`` if
|
|
41
|
+
``auth`` is ``None``.
|
|
42
|
+
|
|
43
|
+
Notes:
|
|
44
|
+
- Helper metadata (for example, ``header_keys``) is consumed to rebuild
|
|
45
|
+
structures but never appears in the final output.
|
|
46
|
+
- When ``prompt_for_secrets`` is False and stdin is not a TTY, a warning is
|
|
47
|
+
logged.
|
|
48
|
+
- Empty user input during prompts defaults to the placeholder value.
|
|
49
|
+
"""
|
|
50
|
+
if auth is None:
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
auth_type = auth.get("type")
|
|
54
|
+
|
|
55
|
+
# Handle no-auth case
|
|
56
|
+
if auth_type == "no-auth":
|
|
57
|
+
return {"type": "no-auth"}
|
|
58
|
+
|
|
59
|
+
# Handle bearer-token authentication
|
|
60
|
+
if auth_type == "bearer-token":
|
|
61
|
+
return _prepare_bearer_token_auth(
|
|
62
|
+
auth, prompt_for_secrets, placeholder, console
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Handle api-key authentication
|
|
66
|
+
if auth_type == "api-key":
|
|
67
|
+
return _prepare_api_key_auth(auth, prompt_for_secrets, placeholder, console)
|
|
68
|
+
|
|
69
|
+
# Handle custom-header authentication
|
|
70
|
+
if auth_type == "custom-header":
|
|
71
|
+
return _prepare_custom_header_auth(
|
|
72
|
+
auth, prompt_for_secrets, placeholder, console
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Unknown auth type - return as-is but strip helper metadata
|
|
76
|
+
result = auth.copy()
|
|
77
|
+
result.pop("header_keys", None)
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_token_value(
|
|
82
|
+
prompt_for_secrets: bool, placeholder: str, console: Console
|
|
83
|
+
) -> str:
|
|
84
|
+
"""Get bearer token value either by prompting or using a placeholder.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
prompt_for_secrets: If True, prompt for the token value.
|
|
88
|
+
placeholder: Placeholder to use when not prompting or when input is empty.
|
|
89
|
+
console: Rich ``Console`` used to display informational messages.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The token string, either provided by the user or the placeholder.
|
|
93
|
+
"""
|
|
94
|
+
if prompt_for_secrets:
|
|
95
|
+
console.print(
|
|
96
|
+
"[yellow]Bearer token is missing or redacted. "
|
|
97
|
+
"Please provide the token.[/yellow]"
|
|
98
|
+
)
|
|
99
|
+
token_value = click.prompt(
|
|
100
|
+
"Bearer token (leave blank for placeholder)",
|
|
101
|
+
default="",
|
|
102
|
+
show_default=False,
|
|
103
|
+
)
|
|
104
|
+
return token_value.strip() or placeholder
|
|
105
|
+
|
|
106
|
+
if not click.get_text_stream("stdin").isatty():
|
|
107
|
+
console.print(
|
|
108
|
+
"[yellow]⚠️ Non-interactive mode: "
|
|
109
|
+
"using placeholder for bearer token[/yellow]"
|
|
110
|
+
)
|
|
111
|
+
return placeholder
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _build_bearer_headers(auth: dict[str, Any], token_value: str) -> dict[str, str]:
|
|
115
|
+
"""Build headers for bearer token authentication.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
auth: Original authentication dictionary which may include ``header_keys``.
|
|
119
|
+
token_value: The token value to embed into the headers.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
A dictionary of HTTP headers including the Authorization header when
|
|
123
|
+
applicable.
|
|
124
|
+
"""
|
|
125
|
+
header_keys = auth.get("header_keys", ["Authorization"])
|
|
126
|
+
headers = {}
|
|
127
|
+
for key in header_keys:
|
|
128
|
+
# Prepend "Bearer " if this is Authorization header
|
|
129
|
+
if key.lower() == "authorization":
|
|
130
|
+
headers[key] = f"Bearer {token_value}"
|
|
131
|
+
else:
|
|
132
|
+
headers[key] = token_value
|
|
133
|
+
return headers
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _prepare_bearer_token_auth(
|
|
137
|
+
auth: dict[str, Any],
|
|
138
|
+
prompt_for_secrets: bool,
|
|
139
|
+
placeholder: str,
|
|
140
|
+
console: Console,
|
|
141
|
+
) -> dict[str, Any]:
|
|
142
|
+
"""Prepare bearer-token authentication for export.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
auth: Original authentication dictionary.
|
|
146
|
+
prompt_for_secrets: Whether to prompt for secrets.
|
|
147
|
+
placeholder: Placeholder value for secrets.
|
|
148
|
+
console: Rich ``Console`` for interaction.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
A prepared ``bearer-token`` authentication dictionary.
|
|
152
|
+
"""
|
|
153
|
+
# Check if token exists and is not masked
|
|
154
|
+
token = auth.get("token")
|
|
155
|
+
has_valid_token = token and token not in (None, "", "***", "REDACTED")
|
|
156
|
+
|
|
157
|
+
# If we have a valid token, use it
|
|
158
|
+
if has_valid_token:
|
|
159
|
+
return {"type": "bearer-token", "token": token}
|
|
160
|
+
|
|
161
|
+
# Get token value (prompt or placeholder)
|
|
162
|
+
token_value = _get_token_value(prompt_for_secrets, placeholder, console)
|
|
163
|
+
|
|
164
|
+
# Check if original had headers structure
|
|
165
|
+
if "headers" in auth or "header_keys" in auth:
|
|
166
|
+
headers = _build_bearer_headers(auth, token_value)
|
|
167
|
+
return {"type": "bearer-token", "headers": headers}
|
|
168
|
+
|
|
169
|
+
# Use token field structure
|
|
170
|
+
return {"type": "bearer-token", "token": token_value}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _extract_api_key_name(auth: dict[str, Any]) -> str | None:
|
|
174
|
+
"""Extract the API key name from an authentication dictionary.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
auth: Authentication dictionary that may contain ``key`` or ``header_keys``.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
The API key name if available, otherwise ``None``.
|
|
181
|
+
"""
|
|
182
|
+
key_name = auth.get("key")
|
|
183
|
+
if not key_name and "header_keys" in auth:
|
|
184
|
+
header_keys = auth["header_keys"]
|
|
185
|
+
if isinstance(header_keys, list) and header_keys:
|
|
186
|
+
key_name = header_keys[0]
|
|
187
|
+
return key_name
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _get_api_key_value(
|
|
191
|
+
key_name: str | None,
|
|
192
|
+
prompt_for_secrets: bool,
|
|
193
|
+
placeholder: str,
|
|
194
|
+
console: Console,
|
|
195
|
+
) -> str:
|
|
196
|
+
"""Get API key value either by prompting or using a placeholder.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
key_name: The name of the API key; used in prompt messages.
|
|
200
|
+
prompt_for_secrets: If True, prompt for the API key value.
|
|
201
|
+
placeholder: Placeholder to use when not prompting or when input is empty.
|
|
202
|
+
console: Rich ``Console`` used to display informational messages.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
The API key value, either provided by the user or the placeholder.
|
|
206
|
+
"""
|
|
207
|
+
if prompt_for_secrets:
|
|
208
|
+
console.print(
|
|
209
|
+
f"[yellow]API key value for '{key_name}' is missing or redacted.[/yellow]"
|
|
210
|
+
)
|
|
211
|
+
key_value = click.prompt(
|
|
212
|
+
f"API key value for '{key_name}' (leave blank for placeholder)",
|
|
213
|
+
default="",
|
|
214
|
+
show_default=False,
|
|
215
|
+
)
|
|
216
|
+
return key_value.strip() or placeholder
|
|
217
|
+
|
|
218
|
+
if not click.get_text_stream("stdin").isatty():
|
|
219
|
+
console.print(
|
|
220
|
+
f"[yellow]⚠️ Non-interactive mode: "
|
|
221
|
+
f"using placeholder for API key '{key_name}'[/yellow]"
|
|
222
|
+
)
|
|
223
|
+
return placeholder
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _build_api_key_headers(
|
|
227
|
+
auth: dict[str, Any], key_name: str | None, key_value: str
|
|
228
|
+
) -> dict[str, str]:
|
|
229
|
+
"""Build headers for API key authentication.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
auth: Original authentication dictionary which may include ``header_keys``.
|
|
233
|
+
key_name: The header key name if present.
|
|
234
|
+
key_value: The API key value to populate.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
A dictionary of HTTP headers for API key authentication.
|
|
238
|
+
"""
|
|
239
|
+
header_keys = auth.get("header_keys", [key_name] if key_name else [])
|
|
240
|
+
return {key: key_value for key in header_keys}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _prepare_api_key_auth(
|
|
244
|
+
auth: dict[str, Any],
|
|
245
|
+
prompt_for_secrets: bool,
|
|
246
|
+
placeholder: str,
|
|
247
|
+
console: Console,
|
|
248
|
+
) -> dict[str, Any]:
|
|
249
|
+
"""Prepare api-key authentication for export.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
auth: Original authentication dictionary.
|
|
253
|
+
prompt_for_secrets: Whether to prompt for secrets.
|
|
254
|
+
placeholder: Placeholder value for secrets.
|
|
255
|
+
console: Rich ``Console`` for interaction.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
A prepared ``api-key`` authentication dictionary.
|
|
259
|
+
"""
|
|
260
|
+
# Extract key name and value
|
|
261
|
+
key_name = _extract_api_key_name(auth)
|
|
262
|
+
key_value = auth.get("value")
|
|
263
|
+
|
|
264
|
+
# Check if we have a valid value
|
|
265
|
+
has_valid_value = key_value and key_value not in (None, "", "***", "REDACTED")
|
|
266
|
+
|
|
267
|
+
# Capture or use placeholder for value
|
|
268
|
+
if not has_valid_value:
|
|
269
|
+
key_value = _get_api_key_value(
|
|
270
|
+
key_name, prompt_for_secrets, placeholder, console
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Check if original had headers structure
|
|
274
|
+
if "headers" in auth or "header_keys" in auth:
|
|
275
|
+
headers = _build_api_key_headers(auth, key_name, key_value)
|
|
276
|
+
return {"type": "api-key", "headers": headers}
|
|
277
|
+
|
|
278
|
+
# Use key/value field structure
|
|
279
|
+
return {"type": "api-key", "key": key_name, "value": key_value}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _prepare_custom_header_auth(
|
|
283
|
+
auth: dict[str, Any],
|
|
284
|
+
prompt_for_secrets: bool,
|
|
285
|
+
placeholder: str,
|
|
286
|
+
console: Console,
|
|
287
|
+
) -> dict[str, Any]:
|
|
288
|
+
"""Prepare custom-header authentication for export.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
auth: Original authentication dictionary.
|
|
292
|
+
prompt_for_secrets: Whether to prompt for header values.
|
|
293
|
+
placeholder: Placeholder value when not prompting or input is empty.
|
|
294
|
+
console: Rich ``Console`` for interaction.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
A prepared ``custom-header`` authentication dictionary.
|
|
298
|
+
"""
|
|
299
|
+
existing_headers: dict[str, Any] = auth.get("headers", {})
|
|
300
|
+
header_names = _extract_header_names(existing_headers, auth.get("header_keys", []))
|
|
301
|
+
|
|
302
|
+
if not header_names:
|
|
303
|
+
return {"type": "custom-header", "headers": {}}
|
|
304
|
+
|
|
305
|
+
headers = _build_custom_headers(
|
|
306
|
+
existing_headers=existing_headers,
|
|
307
|
+
header_names=header_names,
|
|
308
|
+
prompt_for_secrets=prompt_for_secrets,
|
|
309
|
+
placeholder=placeholder,
|
|
310
|
+
console=console,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return {"type": "custom-header", "headers": headers}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _extract_header_names(
|
|
317
|
+
existing_headers: Mapping[str, Any] | None, header_keys: Iterable[str] | None
|
|
318
|
+
) -> list[str]:
|
|
319
|
+
"""Extract the list of header names to process.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
existing_headers: Existing headers mapping from the auth object.
|
|
323
|
+
header_keys: Optional helper metadata listing header names.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
A list of header names to process.
|
|
327
|
+
"""
|
|
328
|
+
if existing_headers:
|
|
329
|
+
return list(existing_headers.keys())
|
|
330
|
+
if header_keys:
|
|
331
|
+
return list(header_keys)
|
|
332
|
+
return []
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _is_valid_secret(value: Any) -> bool:
|
|
336
|
+
"""Determine whether a secret value is present and not masked.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
value: The value to test.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
True if the value is non-empty and not one of the masked placeholders.
|
|
343
|
+
"""
|
|
344
|
+
return bool(value) and value not in (None, "", "***", "REDACTED")
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _prompt_or_placeholder(
|
|
348
|
+
name: str,
|
|
349
|
+
prompt_for_secrets: bool,
|
|
350
|
+
placeholder: str,
|
|
351
|
+
console: Console,
|
|
352
|
+
) -> str:
|
|
353
|
+
"""Prompt for a header value or return the placeholder when not prompting.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
name: Header name used in prompt messages.
|
|
357
|
+
prompt_for_secrets: If True, prompt for the value interactively.
|
|
358
|
+
placeholder: Placeholder value used when not prompting or empty input.
|
|
359
|
+
console: Rich ``Console`` instance for user-facing messages.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
The provided value or the placeholder.
|
|
363
|
+
"""
|
|
364
|
+
if prompt_for_secrets:
|
|
365
|
+
console.print(f"[yellow]Header '{name}' is missing or redacted.[/yellow]")
|
|
366
|
+
value = click.prompt(
|
|
367
|
+
f"Value for header '{name}' (leave blank for placeholder)",
|
|
368
|
+
default="",
|
|
369
|
+
show_default=False,
|
|
370
|
+
)
|
|
371
|
+
return value.strip() or placeholder
|
|
372
|
+
|
|
373
|
+
if not click.get_text_stream("stdin").isatty():
|
|
374
|
+
console.print(
|
|
375
|
+
f"[yellow]⚠️ Non-interactive mode: using placeholder for header '{name}'[/yellow]"
|
|
376
|
+
)
|
|
377
|
+
return placeholder
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _build_custom_headers(
|
|
381
|
+
*,
|
|
382
|
+
existing_headers: Mapping[str, Any],
|
|
383
|
+
header_names: Iterable[str],
|
|
384
|
+
prompt_for_secrets: bool,
|
|
385
|
+
placeholder: str,
|
|
386
|
+
console: Console,
|
|
387
|
+
) -> dict[str, str]:
|
|
388
|
+
"""Build a headers mapping for custom-header authentication.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
existing_headers: Existing headers mapping from the auth object.
|
|
392
|
+
header_names: Header names to process.
|
|
393
|
+
prompt_for_secrets: Whether to prompt for missing values.
|
|
394
|
+
placeholder: Placeholder to use for missing or masked values.
|
|
395
|
+
console: Rich ``Console`` used for prompt/warning messages.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
A dictionary mapping header names to resolved values.
|
|
399
|
+
"""
|
|
400
|
+
headers: dict[str, str] = {}
|
|
401
|
+
for name in header_names:
|
|
402
|
+
existing_value = existing_headers.get(name)
|
|
403
|
+
if _is_valid_secret(existing_value):
|
|
404
|
+
headers[name] = str(existing_value)
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
headers[name] = _prompt_or_placeholder(
|
|
408
|
+
name=name,
|
|
409
|
+
prompt_for_secrets=prompt_for_secrets,
|
|
410
|
+
placeholder=placeholder,
|
|
411
|
+
console=console,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
return headers
|
glaip_sdk/cli/commands/agents.py
CHANGED
|
@@ -416,8 +416,19 @@ def list_agents(
|
|
|
416
416
|
return row
|
|
417
417
|
|
|
418
418
|
# Use fuzzy picker for interactive agent selection and details (default behavior)
|
|
419
|
-
# Skip if --simple flag is used
|
|
420
|
-
|
|
419
|
+
# Skip if --simple flag is used, a name filter is applied, or non-rich output is requested
|
|
420
|
+
ctx_obj = ctx.obj if isinstance(getattr(ctx, "obj", None), dict) else {}
|
|
421
|
+
current_view = ctx_obj.get("view")
|
|
422
|
+
interactive_enabled = (
|
|
423
|
+
not simple
|
|
424
|
+
and name is None
|
|
425
|
+
and current_view not in {"json", "plain", "md"}
|
|
426
|
+
and console.is_terminal
|
|
427
|
+
and os.isatty(1)
|
|
428
|
+
and len(agents) > 0
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
if interactive_enabled:
|
|
421
432
|
picked_agent = _fuzzy_pick_for_resources(agents, "agent", "")
|
|
422
433
|
if picked_agent:
|
|
423
434
|
_display_agent_details(ctx, client, picked_agent)
|
|
@@ -426,7 +437,18 @@ def list_agents(
|
|
|
426
437
|
return
|
|
427
438
|
|
|
428
439
|
# Show simple table (either --simple flag or non-interactive)
|
|
429
|
-
output_list(
|
|
440
|
+
output_list(
|
|
441
|
+
ctx,
|
|
442
|
+
agents,
|
|
443
|
+
"🤖 Available Agents",
|
|
444
|
+
columns,
|
|
445
|
+
transform_agent,
|
|
446
|
+
skip_picker=simple
|
|
447
|
+
or any(
|
|
448
|
+
param is not None for param in (agent_type, framework, name, version)
|
|
449
|
+
),
|
|
450
|
+
use_pager=False,
|
|
451
|
+
)
|
|
430
452
|
|
|
431
453
|
except Exception as e:
|
|
432
454
|
raise click.ClickException(str(e))
|