glaip-sdk 0.0.11__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/commands/mcps.py +82 -23
- 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/client/mcps.py +3 -3
- {glaip_sdk-0.0.11.dist-info → glaip_sdk-0.0.12.dist-info}/METADATA +1 -1
- {glaip_sdk-0.0.11.dist-info → glaip_sdk-0.0.12.dist-info}/RECORD +9 -6
- {glaip_sdk-0.0.11.dist-info → glaip_sdk-0.0.12.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.11.dist-info → glaip_sdk-0.0.12.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/commands/mcps.py
CHANGED
|
@@ -25,6 +25,11 @@ from glaip_sdk.cli.display import (
|
|
|
25
25
|
from glaip_sdk.cli.io import (
|
|
26
26
|
fetch_raw_resource_details,
|
|
27
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
|
|
28
33
|
from glaip_sdk.cli.resolution import resolve_resource_reference
|
|
29
34
|
from glaip_sdk.cli.utils import (
|
|
30
35
|
coerce_to_row,
|
|
@@ -137,11 +142,25 @@ def list_mcps(ctx: Any) -> None:
|
|
|
137
142
|
@click.option("--name", required=True, help="MCP name")
|
|
138
143
|
@click.option("--transport", required=True, help="MCP transport protocol")
|
|
139
144
|
@click.option("--description", help="MCP description")
|
|
140
|
-
@click.option(
|
|
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
|
+
)
|
|
141
155
|
@output_flags()
|
|
142
156
|
@click.pass_context
|
|
143
157
|
def create(
|
|
144
|
-
ctx: Any,
|
|
158
|
+
ctx: Any,
|
|
159
|
+
name: str,
|
|
160
|
+
transport: str,
|
|
161
|
+
description: str | None,
|
|
162
|
+
config: str | None,
|
|
163
|
+
auth: str | None,
|
|
145
164
|
) -> None:
|
|
146
165
|
"""Create a new MCP with specified configuration.
|
|
147
166
|
|
|
@@ -150,7 +169,8 @@ def create(
|
|
|
150
169
|
name: MCP name (required)
|
|
151
170
|
transport: MCP transport protocol (required)
|
|
152
171
|
description: Optional MCP description
|
|
153
|
-
config: JSON configuration string
|
|
172
|
+
config: JSON configuration string or @file reference
|
|
173
|
+
auth: JSON authentication object or @file reference
|
|
154
174
|
|
|
155
175
|
Raises:
|
|
156
176
|
ClickException: If JSON parsing fails or API request fails
|
|
@@ -158,26 +178,47 @@ def create(
|
|
|
158
178
|
try:
|
|
159
179
|
client = get_client(ctx)
|
|
160
180
|
|
|
161
|
-
# Parse config if provided
|
|
162
|
-
|
|
163
|
-
if
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
168
215
|
|
|
169
216
|
with spinner_context(
|
|
170
217
|
ctx,
|
|
171
218
|
"[bold blue]Creating MCP…[/bold blue]",
|
|
172
219
|
console_override=console,
|
|
173
220
|
):
|
|
174
|
-
mcp = client.mcps.create_mcp(
|
|
175
|
-
name=name,
|
|
176
|
-
type="server", # MCPs are always server type
|
|
177
|
-
transport=transport,
|
|
178
|
-
description=description,
|
|
179
|
-
config=mcp_config,
|
|
180
|
-
)
|
|
221
|
+
mcp = client.mcps.create_mcp(**create_kwargs)
|
|
181
222
|
|
|
182
223
|
# Handle JSON output
|
|
183
224
|
handle_json_output(ctx, mcp.model_dump())
|
|
@@ -525,7 +566,16 @@ def connect(ctx: Any, config_file: str) -> None:
|
|
|
525
566
|
@click.argument("mcp_ref")
|
|
526
567
|
@click.option("--name", help="New MCP name")
|
|
527
568
|
@click.option("--description", help="New description")
|
|
528
|
-
@click.option(
|
|
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
|
+
)
|
|
529
579
|
@output_flags()
|
|
530
580
|
@click.pass_context
|
|
531
581
|
def update(
|
|
@@ -534,6 +584,7 @@ def update(
|
|
|
534
584
|
name: str | None,
|
|
535
585
|
description: str | None,
|
|
536
586
|
config: str | None,
|
|
587
|
+
auth: str | None,
|
|
537
588
|
) -> None:
|
|
538
589
|
"""Update an existing MCP with new configuration values.
|
|
539
590
|
|
|
@@ -542,7 +593,8 @@ def update(
|
|
|
542
593
|
mcp_ref: MCP reference (ID or name)
|
|
543
594
|
name: New MCP name (optional)
|
|
544
595
|
description: New description (optional)
|
|
545
|
-
config: New JSON configuration string (optional)
|
|
596
|
+
config: New JSON configuration string or @file reference (optional)
|
|
597
|
+
auth: New JSON authentication object or @file reference (optional)
|
|
546
598
|
|
|
547
599
|
Raises:
|
|
548
600
|
ClickException: If MCP not found, JSON invalid, or no fields specified
|
|
@@ -564,10 +616,17 @@ def update(
|
|
|
564
616
|
if description is not None:
|
|
565
617
|
update_data["description"] = description
|
|
566
618
|
if config is not None:
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
+
)
|
|
571
630
|
|
|
572
631
|
if not update_data:
|
|
573
632
|
raise click.ClickException("No update fields specified")
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""MCP configuration and authentication validation for CLI.
|
|
2
|
+
|
|
3
|
+
This module provides validation functions for MCP config and auth structures
|
|
4
|
+
that are used in CLI commands. It ensures data conforms to the MCP schema
|
|
5
|
+
documented in docs/reference/schemas/mcps.md.
|
|
6
|
+
|
|
7
|
+
Authors:
|
|
8
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
from urllib.parse import urlparse
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def format_validation_error(prefix: str, detail: str | None = None) -> str:
|
|
18
|
+
"""Format a validation error message with optional detail.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
prefix: Main error message
|
|
22
|
+
detail: Optional additional detail to append
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Formatted error message string
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
>>> format_validation_error("Invalid config", "Missing 'url' field")
|
|
29
|
+
"Invalid config\\nMissing 'url' field"
|
|
30
|
+
"""
|
|
31
|
+
parts = [prefix]
|
|
32
|
+
if detail:
|
|
33
|
+
parts.append(detail)
|
|
34
|
+
return "\n".join(parts)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def validate_mcp_config_structure(
|
|
38
|
+
config: Any, *, transport: str | None = None, source: str = "--config"
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""Validate MCP configuration structure for CLI commands.
|
|
41
|
+
|
|
42
|
+
Validates that the config is a dictionary with a valid 'url' field.
|
|
43
|
+
The 'url' must be an absolute HTTP/HTTPS URL as required by the MCP schema.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
config: Configuration value to validate (expected to be a dict)
|
|
47
|
+
transport: Optional transport type ('http' or 'sse') for context in errors
|
|
48
|
+
source: Source parameter name for error messages (default: "--config")
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Validated configuration dictionary
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
click.ClickException: If config is not a dict, missing 'url', or URL is invalid
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
>>> validate_mcp_config_structure({"url": "https://api.example.com"})
|
|
58
|
+
{'url': 'https://api.example.com'}
|
|
59
|
+
|
|
60
|
+
>>> validate_mcp_config_structure([1, 2, 3]) # doctest: +SKIP
|
|
61
|
+
ClickException: Invalid --config value
|
|
62
|
+
Expected a JSON object representing MCP configuration.
|
|
63
|
+
|
|
64
|
+
Schema Reference:
|
|
65
|
+
See docs/reference/schemas/mcps.md - Config Object Structure
|
|
66
|
+
- Required field: 'url' (string, must be valid HTTP/HTTPS URL)
|
|
67
|
+
- Additional fields allowed and passed through
|
|
68
|
+
"""
|
|
69
|
+
if not isinstance(config, dict):
|
|
70
|
+
raise click.ClickException(
|
|
71
|
+
format_validation_error(
|
|
72
|
+
f"Invalid {source} value",
|
|
73
|
+
"Expected a JSON object representing MCP configuration.",
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
url_value = config.get("url")
|
|
78
|
+
if not isinstance(url_value, str) or not url_value.strip():
|
|
79
|
+
requirement = "Missing required 'url' field with a non-empty string value."
|
|
80
|
+
if transport:
|
|
81
|
+
requirement += f" Required for transport '{transport}'."
|
|
82
|
+
raise click.ClickException(
|
|
83
|
+
format_validation_error(f"Invalid {source} value", requirement)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
parsed_url = urlparse(url_value)
|
|
87
|
+
if parsed_url.scheme not in {"http", "https"} or not parsed_url.netloc:
|
|
88
|
+
raise click.ClickException(
|
|
89
|
+
format_validation_error(
|
|
90
|
+
f"Invalid {source} value",
|
|
91
|
+
"'url' must be an absolute HTTP or HTTPS URL.",
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return config
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _validate_headers_mapping(
|
|
99
|
+
headers: Any, *, source: str, context: str
|
|
100
|
+
) -> dict[str, str]:
|
|
101
|
+
"""Validate headers mapping for authentication.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
headers: Headers value to validate (expected to be a non-empty dict)
|
|
105
|
+
source: Source parameter name for error messages
|
|
106
|
+
context: Context description for error messages (e.g., "bearer-token authentication")
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Validated headers dictionary with string keys and values
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
click.ClickException: If headers is not a dict, empty, or contains invalid entries
|
|
113
|
+
"""
|
|
114
|
+
if not isinstance(headers, dict) or not headers:
|
|
115
|
+
raise click.ClickException(
|
|
116
|
+
format_validation_error(
|
|
117
|
+
f"Invalid {source} value",
|
|
118
|
+
f"{context} must provide a non-empty 'headers' object with string keys and values.",
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
normalized: dict[str, str] = {}
|
|
123
|
+
for key, value in headers.items():
|
|
124
|
+
if not isinstance(key, str) or not key.strip():
|
|
125
|
+
raise click.ClickException(
|
|
126
|
+
format_validation_error(
|
|
127
|
+
f"Invalid {source} value",
|
|
128
|
+
"Header names must be non-empty strings.",
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
if not isinstance(value, str) or not value.strip():
|
|
132
|
+
raise click.ClickException(
|
|
133
|
+
format_validation_error(
|
|
134
|
+
f"Invalid {source} value",
|
|
135
|
+
f"Header '{key}' must have a non-empty string value.",
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
normalized[key] = value
|
|
139
|
+
return normalized
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _validate_bearer_token_auth(auth: dict[str, Any], source: str) -> dict[str, Any]:
|
|
143
|
+
"""Validate bearer-token authentication.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
auth: Authentication dictionary
|
|
147
|
+
source: Source parameter name for error messages
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Validated bearer-token authentication dictionary
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
click.ClickException: If bearer-token structure is invalid
|
|
154
|
+
"""
|
|
155
|
+
token = auth.get("token")
|
|
156
|
+
if isinstance(token, str) and token.strip():
|
|
157
|
+
return {"type": "bearer-token", "token": token}
|
|
158
|
+
headers = auth.get("headers")
|
|
159
|
+
normalized_headers = _validate_headers_mapping(
|
|
160
|
+
headers, source=source, context="bearer-token authentication"
|
|
161
|
+
)
|
|
162
|
+
return {"type": "bearer-token", "headers": normalized_headers}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _validate_api_key_auth(auth: dict[str, Any], source: str) -> dict[str, Any]:
|
|
166
|
+
"""Validate api-key authentication.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
auth: Authentication dictionary
|
|
170
|
+
source: Source parameter name for error messages
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Validated api-key authentication dictionary
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
click.ClickException: If api-key structure is invalid
|
|
177
|
+
"""
|
|
178
|
+
headers = auth.get("headers")
|
|
179
|
+
if headers is not None:
|
|
180
|
+
normalized_headers = _validate_headers_mapping(
|
|
181
|
+
headers, source=source, context="api-key authentication"
|
|
182
|
+
)
|
|
183
|
+
return {"type": "api-key", "headers": normalized_headers}
|
|
184
|
+
|
|
185
|
+
key = auth.get("key")
|
|
186
|
+
value = auth.get("value")
|
|
187
|
+
if not isinstance(key, str) or not key.strip():
|
|
188
|
+
raise click.ClickException(
|
|
189
|
+
format_validation_error(
|
|
190
|
+
f"Invalid {source} value",
|
|
191
|
+
"api-key authentication requires a non-empty 'key'.",
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
if not isinstance(value, str) or not value.strip():
|
|
195
|
+
raise click.ClickException(
|
|
196
|
+
format_validation_error(
|
|
197
|
+
f"Invalid {source} value",
|
|
198
|
+
"api-key authentication requires a non-empty 'value'.",
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
return {"type": "api-key", "key": key, "value": value}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _validate_custom_header_auth(auth: dict[str, Any], source: str) -> dict[str, Any]:
|
|
205
|
+
"""Validate custom-header authentication.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
auth: Authentication dictionary
|
|
209
|
+
source: Source parameter name for error messages
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Validated custom-header authentication dictionary
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
click.ClickException: If custom-header structure is invalid
|
|
216
|
+
"""
|
|
217
|
+
headers = auth.get("headers")
|
|
218
|
+
normalized_headers = _validate_headers_mapping(
|
|
219
|
+
headers, source=source, context="custom-header authentication"
|
|
220
|
+
)
|
|
221
|
+
return {"type": "custom-header", "headers": normalized_headers}
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def validate_mcp_auth_structure(auth: Any, *, source: str = "--auth") -> dict[str, Any]:
|
|
225
|
+
"""Validate MCP authentication structure for CLI commands.
|
|
226
|
+
|
|
227
|
+
Validates authentication objects according to the MCP schema, supporting:
|
|
228
|
+
- no-auth: No authentication required
|
|
229
|
+
- bearer-token: Bearer token via 'token' field or 'headers'
|
|
230
|
+
- api-key: API key via 'key'/'value' fields or 'headers'
|
|
231
|
+
- custom-header: Custom headers via 'headers' object
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
auth: Authentication value to validate (expected to be a dict or None)
|
|
235
|
+
source: Source parameter name for error messages (default: "--auth")
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Validated authentication dictionary, or empty dict if auth is None
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
click.ClickException: If auth structure is invalid or type is unsupported
|
|
242
|
+
|
|
243
|
+
Examples:
|
|
244
|
+
>>> validate_mcp_auth_structure(None)
|
|
245
|
+
{}
|
|
246
|
+
|
|
247
|
+
>>> validate_mcp_auth_structure({"type": "no-auth"})
|
|
248
|
+
{'type': 'no-auth'}
|
|
249
|
+
|
|
250
|
+
>>> validate_mcp_auth_structure({"type": "bearer-token", "token": "abc123"})
|
|
251
|
+
{'type': 'bearer-token', 'token': 'abc123'}
|
|
252
|
+
|
|
253
|
+
Schema Reference:
|
|
254
|
+
See docs/reference/schemas/mcps.md - Authentication Types
|
|
255
|
+
- Required field: 'type' (string, one of: no-auth, bearer-token, api-key, custom-header)
|
|
256
|
+
- Additional fields depend on type
|
|
257
|
+
"""
|
|
258
|
+
if auth is None:
|
|
259
|
+
return {}
|
|
260
|
+
|
|
261
|
+
if not isinstance(auth, dict):
|
|
262
|
+
raise click.ClickException(
|
|
263
|
+
format_validation_error(
|
|
264
|
+
f"Invalid {source} value",
|
|
265
|
+
"Expected a JSON object representing MCP authentication.",
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
raw_type = auth.get("type")
|
|
270
|
+
if not isinstance(raw_type, str) or not raw_type.strip():
|
|
271
|
+
raise click.ClickException(
|
|
272
|
+
format_validation_error(
|
|
273
|
+
f"Invalid {source} value",
|
|
274
|
+
"Authentication objects must include a non-empty 'type' field.",
|
|
275
|
+
)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
auth_type = raw_type.strip()
|
|
279
|
+
|
|
280
|
+
# Dispatch to type-specific validators
|
|
281
|
+
if auth_type == "no-auth":
|
|
282
|
+
return {"type": "no-auth"}
|
|
283
|
+
if auth_type == "bearer-token":
|
|
284
|
+
return _validate_bearer_token_auth(auth, source)
|
|
285
|
+
if auth_type == "api-key":
|
|
286
|
+
return _validate_api_key_auth(auth, source)
|
|
287
|
+
if auth_type == "custom-header":
|
|
288
|
+
return _validate_custom_header_auth(auth, source)
|
|
289
|
+
|
|
290
|
+
# Unknown type
|
|
291
|
+
raise click.ClickException(
|
|
292
|
+
format_validation_error(
|
|
293
|
+
f"Invalid {source} value",
|
|
294
|
+
f"Unsupported authentication type '{auth_type}'. "
|
|
295
|
+
f"Supported types: no-auth, bearer-token, api-key, custom-header",
|
|
296
|
+
)
|
|
297
|
+
)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""JSON input parser for CLI options.
|
|
2
|
+
|
|
3
|
+
Handles both inline JSON strings and @file references.
|
|
4
|
+
|
|
5
|
+
Authors:
|
|
6
|
+
Putu Ravindra Wiguna (putu.r.wiguna@gdplabs.id)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _format_file_error(
|
|
18
|
+
prefix: str, file_path_str: str, resolved_path: Path, *, detail: str | None = None
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Format a file-related error message with path context.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
prefix: Main error message
|
|
24
|
+
file_path_str: Original file path string provided by user
|
|
25
|
+
resolved_path: Resolved absolute path
|
|
26
|
+
detail: Optional additional detail to append
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Formatted error message string with file path context
|
|
30
|
+
|
|
31
|
+
Examples:
|
|
32
|
+
>>> from pathlib import Path
|
|
33
|
+
>>> _format_file_error("File not found", "config.json", Path("/abs/config.json"))
|
|
34
|
+
'File not found: config.json\\nResolved path: /abs/config.json'
|
|
35
|
+
"""
|
|
36
|
+
parts = [f"{prefix}: {file_path_str}", f"Resolved path: {resolved_path}"]
|
|
37
|
+
if detail:
|
|
38
|
+
parts.append(detail)
|
|
39
|
+
return "\n".join(parts)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _parse_json_from_file(file_path_str: str) -> Any:
|
|
43
|
+
"""Parse JSON from a file path.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
file_path_str: Path to the JSON file (without @ prefix).
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Parsed dictionary from file.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
click.ClickException: If file not found, not readable, empty, or invalid JSON.
|
|
53
|
+
"""
|
|
54
|
+
# Resolve relative paths against CWD
|
|
55
|
+
file_path = Path(file_path_str)
|
|
56
|
+
if not file_path.is_absolute():
|
|
57
|
+
file_path = Path.cwd() / file_path
|
|
58
|
+
|
|
59
|
+
# Check if file exists and is a regular file
|
|
60
|
+
if not file_path.is_file():
|
|
61
|
+
raise click.ClickException(
|
|
62
|
+
_format_file_error("File not found or not a file", file_path_str, file_path)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Check if file is readable
|
|
66
|
+
if not os.access(file_path, os.R_OK):
|
|
67
|
+
raise click.ClickException(
|
|
68
|
+
_format_file_error(
|
|
69
|
+
"File not readable (permission denied)", file_path_str, file_path
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Read file content
|
|
74
|
+
try:
|
|
75
|
+
content = file_path.read_text(encoding="utf-8")
|
|
76
|
+
except Exception as e:
|
|
77
|
+
raise click.ClickException(
|
|
78
|
+
_format_file_error(
|
|
79
|
+
"Error reading file", file_path_str, file_path, detail=f"Error: {e}"
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Check for empty content
|
|
84
|
+
if not content.strip():
|
|
85
|
+
raise click.ClickException(
|
|
86
|
+
_format_file_error("File is empty", file_path_str, file_path)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Parse JSON from file content
|
|
90
|
+
try:
|
|
91
|
+
return json.loads(content)
|
|
92
|
+
except json.JSONDecodeError as e:
|
|
93
|
+
raise click.ClickException(
|
|
94
|
+
_format_file_error(
|
|
95
|
+
"Invalid JSON in file",
|
|
96
|
+
file_path_str,
|
|
97
|
+
file_path,
|
|
98
|
+
detail=f"Error: {e.msg} at line {e.lineno}, column {e.colno}",
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def parse_json_input(value: str | None) -> Any:
|
|
104
|
+
"""Parse JSON input from inline string or file reference.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
value: JSON string or @file reference. If None, returns None.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Parsed JSON value (dict, list, str, int, float, bool, None) or None if value is None.
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
click.ClickException: If file not found, not readable, empty, or invalid JSON.
|
|
114
|
+
|
|
115
|
+
Examples:
|
|
116
|
+
>>> parse_json_input('{"key": "value"}')
|
|
117
|
+
{'key': 'value'}
|
|
118
|
+
|
|
119
|
+
>>> parse_json_input('@/path/to/config.json')
|
|
120
|
+
# Returns content of config.json parsed as JSON
|
|
121
|
+
|
|
122
|
+
>>> parse_json_input(None)
|
|
123
|
+
None
|
|
124
|
+
"""
|
|
125
|
+
if value is None:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
# Check if value is a file reference (strip whitespace first)
|
|
129
|
+
trimmed = value.strip()
|
|
130
|
+
if trimmed.startswith("@"):
|
|
131
|
+
return _parse_json_from_file(trimmed[1:])
|
|
132
|
+
|
|
133
|
+
# Parse inline JSON
|
|
134
|
+
try:
|
|
135
|
+
return json.loads(value)
|
|
136
|
+
except json.JSONDecodeError as e:
|
|
137
|
+
raise click.ClickException(
|
|
138
|
+
f"Invalid JSON in inline value\n"
|
|
139
|
+
f"Error: {e.msg} at line {e.lineno}, column {e.colno}"
|
|
140
|
+
)
|
glaip_sdk/client/mcps.py
CHANGED
|
@@ -57,7 +57,7 @@ class MCPClient(BaseClient):
|
|
|
57
57
|
def create_mcp(
|
|
58
58
|
self,
|
|
59
59
|
name: str,
|
|
60
|
-
description: str,
|
|
60
|
+
description: str | None = None,
|
|
61
61
|
config: dict[str, Any] | None = None,
|
|
62
62
|
**kwargs,
|
|
63
63
|
) -> MCP:
|
|
@@ -108,7 +108,7 @@ class MCPClient(BaseClient):
|
|
|
108
108
|
def _build_create_payload(
|
|
109
109
|
self,
|
|
110
110
|
name: str,
|
|
111
|
-
description: str,
|
|
111
|
+
description: str | None = None,
|
|
112
112
|
transport: str = DEFAULT_MCP_TRANSPORT,
|
|
113
113
|
config: dict[str, Any] | None = None,
|
|
114
114
|
**kwargs,
|
|
@@ -122,7 +122,7 @@ class MCPClient(BaseClient):
|
|
|
122
122
|
|
|
123
123
|
Args:
|
|
124
124
|
name: MCP name
|
|
125
|
-
description: MCP description
|
|
125
|
+
description: MCP description (optional)
|
|
126
126
|
transport: MCP transport protocol (defaults to stdio)
|
|
127
127
|
config: MCP configuration dictionary
|
|
128
128
|
**kwargs: Additional parameters
|
|
@@ -7,12 +7,15 @@ glaip_sdk/cli/auth.py,sha256=eYdtGmJ3XgiO96hq_69GF6b3W-aRWZrDQ-6bHuaRX4M,13517
|
|
|
7
7
|
glaip_sdk/cli/commands/__init__.py,sha256=x0CZlZbZHoHvuzfoTWIyEch6WmNnbPzxajrox6riYp0,173
|
|
8
8
|
glaip_sdk/cli/commands/agents.py,sha256=97dzowjHgk5knyHuI-0z2ojvqNlkebNN1-ikGEoS5sc,40623
|
|
9
9
|
glaip_sdk/cli/commands/configure.py,sha256=eRDzsaKV4fl2lJt8ieS4g2-xRnaa02eAAPW8xBf-tqA,7507
|
|
10
|
-
glaip_sdk/cli/commands/mcps.py,sha256=
|
|
10
|
+
glaip_sdk/cli/commands/mcps.py,sha256=_zBT-8LMg8DTkiMO0d63tUqjy36-bdqrjxZX7bM-vpg,21514
|
|
11
11
|
glaip_sdk/cli/commands/models.py,sha256=Ra3-50BPScNs0Q-j4b7U4iK0hNooucEyVgHpQ11-pt8,1700
|
|
12
12
|
glaip_sdk/cli/commands/tools.py,sha256=MOM9Db3HGL1stF-WvL5cZXjw-iZo2qc-oyKQHy6VwIM,18690
|
|
13
13
|
glaip_sdk/cli/display.py,sha256=jE20swoRKzpYUmc0jgbeonaXKeE9x95hfjWAEdnBYRc,8727
|
|
14
14
|
glaip_sdk/cli/io.py,sha256=GPkw3pQMLBGoD5GH-KlbKpNRlVWFZOXHE17F7V3kQsI,3343
|
|
15
15
|
glaip_sdk/cli/main.py,sha256=3Bl8u9t1MekzaNrAZqsx4TukbzzFdi6Wss6jvTDos00,12930
|
|
16
|
+
glaip_sdk/cli/mcp_validators.py,sha256=PEJRzb7ogRkwNJwJK9k5Xmb8hvoQ58L2Qywqd_3Wayo,10125
|
|
17
|
+
glaip_sdk/cli/parsers/__init__.py,sha256=Ycd4HDfYmA7GUGFt0ndBPBo5uTbv15XsXnYUj-a89ug,183
|
|
18
|
+
glaip_sdk/cli/parsers/json_input.py,sha256=iISa31ZsDNYWfCVRy0cifRIg2gjnhI-XtdDLB-UOshg,4039
|
|
16
19
|
glaip_sdk/cli/resolution.py,sha256=BOw2NchReLKewAwBAZLWw_3_bI7u3tfzQEO7kQbIiGE,2067
|
|
17
20
|
glaip_sdk/cli/slash/__init__.py,sha256=Vdv6Y8bu-pA8dxDlyP4XrhudBPivztUozhLAz9vaLig,682
|
|
18
21
|
glaip_sdk/cli/slash/agent_session.py,sha256=pDOwGXNHuyJIulrGYu1pacyF3oxHWeDQY-Uv92h2qVg,6859
|
|
@@ -25,7 +28,7 @@ glaip_sdk/client/__init__.py,sha256=nYLXfBVTTWwKjP0e63iumPYO4k5FifwWaELQPaPIKIg,
|
|
|
25
28
|
glaip_sdk/client/agents.py,sha256=FSKubF40wptMNIheC3_iawiX2CRbhTcNLFiz4qkPC6k,34659
|
|
26
29
|
glaip_sdk/client/base.py,sha256=O3dv3I7PqY91gH1FehBMRZcSXjwimfeBcJuiXidDmqw,13700
|
|
27
30
|
glaip_sdk/client/main.py,sha256=LlvYHP7-Hy7Eq1ep1kfk337K-Oue5SdKWJpqYfX9eXY,7993
|
|
28
|
-
glaip_sdk/client/mcps.py,sha256
|
|
31
|
+
glaip_sdk/client/mcps.py,sha256=-O-I15qjbwfSA69mouHY6g5_qgPWC4rM98VJLpOkh1A,8975
|
|
29
32
|
glaip_sdk/client/tools.py,sha256=n8DIiOOf1YU_j9JK3Bx2-rDnkpckPi0MI9Ok2s1kwa4,16634
|
|
30
33
|
glaip_sdk/client/validators.py,sha256=NtPsWjQLjj25LiUnmR-WuS8lL5p4MVRaYT9UVRmj9bo,8809
|
|
31
34
|
glaip_sdk/config/constants.py,sha256=ysEobMiXlLZGIOEaqTdHpPF8kmg5nbLn7BIcBvTCuHM,819
|
|
@@ -55,7 +58,7 @@ glaip_sdk/utils/rich_utils.py,sha256=-Ij-1bIJvnVAi6DrfftchIlMcvOTjVmSE0Qqax0EY_s
|
|
|
55
58
|
glaip_sdk/utils/run_renderer.py,sha256=d_VMI6LbvHPUUeRmGqh5wK_lHqDEIAcym2iqpbtDad0,1365
|
|
56
59
|
glaip_sdk/utils/serialization.py,sha256=T1yt_8G2DCFpcxx7XnqFl5slksRXfBCUuLQJTreGYEQ,11806
|
|
57
60
|
glaip_sdk/utils/validation.py,sha256=QNORcdyvuliEs4EH2_mkDgmoyT9utgl7YNhaf45SEf8,6992
|
|
58
|
-
glaip_sdk-0.0.
|
|
59
|
-
glaip_sdk-0.0.
|
|
60
|
-
glaip_sdk-0.0.
|
|
61
|
-
glaip_sdk-0.0.
|
|
61
|
+
glaip_sdk-0.0.12.dist-info/METADATA,sha256=9nmM8Y3WqIoYIkJytD_HKN2CHtHEG-R1Kdi1_SdsLnY,4984
|
|
62
|
+
glaip_sdk-0.0.12.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
63
|
+
glaip_sdk-0.0.12.dist-info/entry_points.txt,sha256=EGs8NO8J1fdFMWA3CsF7sKBEvtHb_fujdCoNPhfMouE,47
|
|
64
|
+
glaip_sdk-0.0.12.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|