glaip-sdk 0.1.0__py3-none-any.whl → 0.6.10__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/__init__.py +5 -2
- glaip_sdk/_version.py +10 -3
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1191 -0
- glaip_sdk/branding.py +15 -6
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +265 -45
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +251 -173
- glaip_sdk/cli/commands/common_config.py +101 -0
- glaip_sdk/cli/commands/configure.py +735 -143
- glaip_sdk/cli/commands/mcps.py +266 -134
- glaip_sdk/cli/commands/models.py +13 -9
- glaip_sdk/cli/commands/tools.py +67 -88
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/commands/update.py +3 -8
- glaip_sdk/cli/config.py +49 -7
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +846 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +45 -32
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +14 -17
- glaip_sdk/cli/main.py +232 -143
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/mcp_validators.py +5 -15
- glaip_sdk/cli/pager.py +12 -19
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/parsers/json_input.py +11 -22
- glaip_sdk/cli/resolution.py +3 -9
- glaip_sdk/cli/rich_helpers.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +578 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +65 -29
- glaip_sdk/cli/slash/prompt.py +24 -10
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +807 -225
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +258 -60
- glaip_sdk/cli/transcript/capture.py +72 -21
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +1 -3
- glaip_sdk/cli/transcript/viewer.py +79 -499
- glaip_sdk/cli/update_notifier.py +177 -24
- glaip_sdk/cli/utils.py +242 -1308
- glaip_sdk/cli/validators.py +16 -18
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +53 -37
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +320 -92
- glaip_sdk/client/base.py +78 -35
- glaip_sdk/client/main.py +19 -10
- glaip_sdk/client/mcps.py +123 -15
- glaip_sdk/client/run_rendering.py +136 -101
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +163 -34
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/exceptions.py +1 -3
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/payload_schemas/agent.py +1 -3
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +232 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +706 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +58 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +4 -14
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +46 -28
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +25 -21
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +1 -36
- glaip_sdk/utils/import_export.py +15 -16
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +7 -35
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +3 -6
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -49
- glaip_sdk/utils/rendering/renderer/base.py +258 -1577
- glaip_sdk/utils/rendering/renderer/config.py +1 -5
- glaip_sdk/utils/rendering/renderer/debug.py +30 -34
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +10 -51
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/toggle.py +1 -3
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/step_tree_state.py +1 -3
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +76 -517
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +29 -26
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +32 -46
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +20 -28
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
- glaip_sdk-0.6.10.dist-info/RECORD +159 -0
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
- glaip_sdk/models.py +0 -259
- glaip_sdk-0.1.0.dist-info/RECORD +0 -82
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
glaip_sdk/client/base.py
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import logging
|
|
9
10
|
import os
|
|
11
|
+
from collections.abc import Iterable, Mapping
|
|
10
12
|
from typing import Any, NoReturn, Union
|
|
11
13
|
|
|
12
14
|
import httpx
|
|
@@ -151,12 +153,7 @@ class BaseClient:
|
|
|
151
153
|
def timeout(self, value: float) -> None:
|
|
152
154
|
"""Set timeout and rebuild client."""
|
|
153
155
|
self._timeout = value
|
|
154
|
-
if (
|
|
155
|
-
hasattr(self, "http_client")
|
|
156
|
-
and self.http_client
|
|
157
|
-
and not self._session_scoped
|
|
158
|
-
and not self._parent_client
|
|
159
|
-
):
|
|
156
|
+
if hasattr(self, "http_client") and self.http_client and not self._session_scoped and not self._parent_client:
|
|
160
157
|
self.http_client.close()
|
|
161
158
|
self.http_client = self._build_client(value)
|
|
162
159
|
|
|
@@ -246,18 +243,14 @@ class BaseClient:
|
|
|
246
243
|
client_log.debug(f"Response status: {response.status_code}")
|
|
247
244
|
return response
|
|
248
245
|
except httpx.ConnectError as e:
|
|
249
|
-
client_log.warning(
|
|
250
|
-
f"Connection error on {method} {endpoint}, retrying once: {e}"
|
|
251
|
-
)
|
|
246
|
+
client_log.warning(f"Connection error on {method} {endpoint}, retrying once: {e}")
|
|
252
247
|
try:
|
|
253
248
|
response = self.http_client.request(method, endpoint, **kwargs)
|
|
254
|
-
client_log.debug(
|
|
255
|
-
f"Retry successful, response status: {response.status_code}"
|
|
256
|
-
)
|
|
249
|
+
client_log.debug(f"Retry successful, response status: {response.status_code}")
|
|
257
250
|
return response
|
|
258
251
|
except httpx.ConnectError:
|
|
259
252
|
client_log.error(f"Retry failed for {method} {endpoint}: {e}")
|
|
260
|
-
raise
|
|
253
|
+
raise
|
|
261
254
|
|
|
262
255
|
def _request(self, method: str, endpoint: str, **kwargs) -> Any:
|
|
263
256
|
"""Make HTTP request with error handling and unwrap success envelopes."""
|
|
@@ -298,7 +291,7 @@ class BaseClient:
|
|
|
298
291
|
return parsed.get("data", parsed) if unwrap else parsed
|
|
299
292
|
else:
|
|
300
293
|
error_type = parsed.get("error", "UnknownError")
|
|
301
|
-
message = parsed.
|
|
294
|
+
message = self._format_error_dict({key: value for key, value in parsed.items() if key != "success"})
|
|
302
295
|
self._raise_api_error(
|
|
303
296
|
400,
|
|
304
297
|
message,
|
|
@@ -341,12 +334,58 @@ class BaseClient:
|
|
|
341
334
|
return validation_message
|
|
342
335
|
return f"Validation error: {parsed}"
|
|
343
336
|
|
|
337
|
+
formatted_details = None
|
|
338
|
+
if "details" in parsed:
|
|
339
|
+
formatted_details = self._format_error_details(parsed["details"])
|
|
340
|
+
|
|
344
341
|
message = parsed.get("message")
|
|
345
342
|
if message:
|
|
343
|
+
if formatted_details:
|
|
344
|
+
return f"{message}\n{formatted_details}"
|
|
346
345
|
return message
|
|
347
346
|
|
|
347
|
+
if formatted_details:
|
|
348
|
+
return formatted_details
|
|
349
|
+
|
|
348
350
|
return str(parsed) if parsed else DEFAULT_ERROR_MESSAGE
|
|
349
351
|
|
|
352
|
+
def _format_error_details(self, details: Any) -> str | None:
|
|
353
|
+
"""Render generic error details into a human-readable string."""
|
|
354
|
+
if details is None:
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
if isinstance(details, dict):
|
|
358
|
+
return self._format_detail_mapping(details)
|
|
359
|
+
|
|
360
|
+
if isinstance(details, (list, tuple, set)):
|
|
361
|
+
return self._format_detail_iterable(details)
|
|
362
|
+
|
|
363
|
+
return f"Details: {details}"
|
|
364
|
+
|
|
365
|
+
@staticmethod
|
|
366
|
+
def _format_detail_mapping(details: Mapping[str, Any]) -> str | None:
|
|
367
|
+
"""Format details provided as a mapping."""
|
|
368
|
+
entries = [f" {key}: {value}" for key, value in details.items()]
|
|
369
|
+
if not entries:
|
|
370
|
+
return None
|
|
371
|
+
return "Details:\n" + "\n".join(entries)
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def _format_detail_iterable(details: Iterable[Any]) -> str | None:
|
|
375
|
+
"""Format details provided as an iterable collection."""
|
|
376
|
+
entries: list[str] = []
|
|
377
|
+
for item in details:
|
|
378
|
+
if isinstance(item, Mapping):
|
|
379
|
+
inner = ", ".join(f"{k}={v}" for k, v in item.items())
|
|
380
|
+
entries.append(f" - {inner if inner else '{}'}")
|
|
381
|
+
else:
|
|
382
|
+
entries.append(f" - {item}")
|
|
383
|
+
|
|
384
|
+
if not entries:
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
return "Details:\n" + "\n".join(entries)
|
|
388
|
+
|
|
350
389
|
def _format_validation_errors(self, errors: list[Any]) -> str | None:
|
|
351
390
|
"""Render validation errors into a human-readable string."""
|
|
352
391
|
entries: list[str] = []
|
|
@@ -365,6 +404,22 @@ class BaseClient:
|
|
|
365
404
|
|
|
366
405
|
return "Validation errors:\n" + "\n".join(entries)
|
|
367
406
|
|
|
407
|
+
@staticmethod
|
|
408
|
+
def _is_no_content_response(response: httpx.Response) -> bool:
|
|
409
|
+
"""Return True when the response contains no content."""
|
|
410
|
+
return response.status_code == 204
|
|
411
|
+
|
|
412
|
+
@staticmethod
|
|
413
|
+
def _is_success_status(response: httpx.Response) -> bool:
|
|
414
|
+
"""Return True for successful HTTP status codes."""
|
|
415
|
+
return 200 <= response.status_code < 300
|
|
416
|
+
|
|
417
|
+
def _handle_error_response(self, response: httpx.Response) -> None:
|
|
418
|
+
"""Raise an API error for non-success responses."""
|
|
419
|
+
error_message = self._get_error_message(response)
|
|
420
|
+
parsed_content = self._parse_response_content(response)
|
|
421
|
+
self._raise_api_error(response.status_code, error_message, payload=parsed_content)
|
|
422
|
+
|
|
368
423
|
def _handle_response(
|
|
369
424
|
self,
|
|
370
425
|
response: httpx.Response,
|
|
@@ -373,24 +428,17 @@ class BaseClient:
|
|
|
373
428
|
) -> Any:
|
|
374
429
|
"""Handle HTTP response with proper error handling."""
|
|
375
430
|
# Handle no-content success before general error handling
|
|
376
|
-
if response
|
|
431
|
+
if self._is_no_content_response(response):
|
|
377
432
|
return None
|
|
378
433
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
self._raise_api_error(
|
|
385
|
-
response.status_code, error_message, payload=parsed_content
|
|
386
|
-
)
|
|
387
|
-
return None # Won't be reached but helps with type checking
|
|
388
|
-
|
|
389
|
-
parsed = self._parse_response_content(response)
|
|
390
|
-
if parsed is None:
|
|
391
|
-
return None
|
|
434
|
+
if self._is_success_status(response):
|
|
435
|
+
parsed = self._parse_response_content(response)
|
|
436
|
+
if parsed is None:
|
|
437
|
+
return None
|
|
438
|
+
return self._handle_success_response(parsed, unwrap=unwrap)
|
|
392
439
|
|
|
393
|
-
|
|
440
|
+
self._handle_error_response(response)
|
|
441
|
+
return None
|
|
394
442
|
|
|
395
443
|
def _raise_api_error(
|
|
396
444
|
self,
|
|
@@ -435,12 +483,7 @@ class BaseClient:
|
|
|
435
483
|
|
|
436
484
|
def close(self) -> None:
|
|
437
485
|
"""Close the HTTP client."""
|
|
438
|
-
if (
|
|
439
|
-
hasattr(self, "http_client")
|
|
440
|
-
and self.http_client
|
|
441
|
-
and not self._session_scoped
|
|
442
|
-
and not self._parent_client
|
|
443
|
-
):
|
|
486
|
+
if hasattr(self, "http_client") and self.http_client and not self._session_scoped and not self._parent_client:
|
|
444
487
|
self.http_client.close()
|
|
445
488
|
|
|
446
489
|
def __enter__(self) -> "BaseClient":
|
glaip_sdk/client/main.py
CHANGED
|
@@ -3,15 +3,24 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
|
-
from
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
9
12
|
|
|
10
13
|
from glaip_sdk.client.agents import AgentClient
|
|
11
14
|
from glaip_sdk.client.base import BaseClient
|
|
12
15
|
from glaip_sdk.client.mcps import MCPClient
|
|
16
|
+
from glaip_sdk.client.shared import build_shared_config
|
|
13
17
|
from glaip_sdk.client.tools import ToolClient
|
|
14
|
-
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
20
|
+
from glaip_sdk.agents import Agent
|
|
21
|
+
from glaip_sdk.client._agent_payloads import AgentListResult
|
|
22
|
+
from glaip_sdk.mcps import MCP
|
|
23
|
+
from glaip_sdk.tools import Tool
|
|
15
24
|
|
|
16
25
|
|
|
17
26
|
class Client(BaseClient):
|
|
@@ -25,12 +34,7 @@ class Client(BaseClient):
|
|
|
25
34
|
"""
|
|
26
35
|
super().__init__(**kwargs)
|
|
27
36
|
# Share the single httpx.Client + config with sub-clients
|
|
28
|
-
shared_config =
|
|
29
|
-
"parent_client": self,
|
|
30
|
-
"api_url": self.api_url,
|
|
31
|
-
"api_key": self.api_key,
|
|
32
|
-
"timeout": self._timeout,
|
|
33
|
-
}
|
|
37
|
+
shared_config = build_shared_config(self)
|
|
34
38
|
self.agents = AgentClient(**shared_config)
|
|
35
39
|
self.tools = ToolClient(**shared_config)
|
|
36
40
|
self.mcps = MCPClient(**shared_config)
|
|
@@ -53,7 +57,7 @@ class Client(BaseClient):
|
|
|
53
57
|
name: str | None = None,
|
|
54
58
|
version: str | None = None,
|
|
55
59
|
sync_langflow_agents: bool = False,
|
|
56
|
-
) ->
|
|
60
|
+
) -> AgentListResult:
|
|
57
61
|
"""List agents with optional filtering.
|
|
58
62
|
|
|
59
63
|
Args:
|
|
@@ -64,7 +68,7 @@ class Client(BaseClient):
|
|
|
64
68
|
sync_langflow_agents: Sync with LangFlow server before listing (only applies when agent_type=langflow)
|
|
65
69
|
|
|
66
70
|
Returns:
|
|
67
|
-
|
|
71
|
+
AgentListResult with agents and pagination metadata. Supports iteration and indexing.
|
|
68
72
|
"""
|
|
69
73
|
return self.agents.list_agents(
|
|
70
74
|
agent_type=agent_type,
|
|
@@ -217,6 +221,11 @@ class Client(BaseClient):
|
|
|
217
221
|
|
|
218
222
|
@timeout.setter
|
|
219
223
|
def timeout(self, value: float) -> None: # type: ignore[override]
|
|
224
|
+
"""Set the client timeout and propagate to sub-clients.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
value: Timeout value in seconds.
|
|
228
|
+
"""
|
|
220
229
|
# Rebuild the root http client
|
|
221
230
|
BaseClient.timeout.fset(self, value) # call parent setter
|
|
222
231
|
# Propagate the new session to sub-clients so they don't hold a closed client
|
glaip_sdk/client/mcps.py
CHANGED
|
@@ -3,18 +3,22 @@
|
|
|
3
3
|
|
|
4
4
|
Authors:
|
|
5
5
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import logging
|
|
9
10
|
from typing import Any
|
|
10
11
|
|
|
11
12
|
from glaip_sdk.client.base import BaseClient
|
|
12
|
-
from glaip_sdk.config.constants import
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
from glaip_sdk.config.constants import DEFAULT_MCP_TRANSPORT, DEFAULT_MCP_TYPE
|
|
14
|
+
from glaip_sdk.mcps import MCP
|
|
15
|
+
from glaip_sdk.models import MCPResponse
|
|
16
|
+
from glaip_sdk.utils.client_utils import (
|
|
17
|
+
add_kwargs_to_payload,
|
|
18
|
+
create_model_instances,
|
|
19
|
+
find_by_name,
|
|
15
20
|
)
|
|
16
|
-
from glaip_sdk.
|
|
17
|
-
from glaip_sdk.utils.client_utils import create_model_instances, find_by_name
|
|
21
|
+
from glaip_sdk.utils.resource_refs import is_uuid
|
|
18
22
|
|
|
19
23
|
# API endpoints
|
|
20
24
|
MCPS_ENDPOINT = "/mcps/"
|
|
@@ -45,7 +49,8 @@ class MCPClient(BaseClient):
|
|
|
45
49
|
def get_mcp_by_id(self, mcp_id: str) -> MCP:
|
|
46
50
|
"""Get MCP by ID."""
|
|
47
51
|
data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}")
|
|
48
|
-
|
|
52
|
+
response = MCPResponse(**data)
|
|
53
|
+
return MCP.from_response(response, client=self)
|
|
49
54
|
|
|
50
55
|
def find_mcps(self, name: str | None = None) -> list[MCP]:
|
|
51
56
|
"""Find MCPs by name."""
|
|
@@ -77,7 +82,8 @@ class MCPClient(BaseClient):
|
|
|
77
82
|
get_endpoint_fmt=f"{MCPS_ENDPOINT}{{id}}",
|
|
78
83
|
json=payload,
|
|
79
84
|
)
|
|
80
|
-
|
|
85
|
+
response = MCPResponse(**full_mcp_data)
|
|
86
|
+
return MCP.from_response(response, client=self)
|
|
81
87
|
|
|
82
88
|
def update_mcp(self, mcp_id: str, **kwargs) -> MCP:
|
|
83
89
|
"""Update an existing MCP.
|
|
@@ -99,12 +105,102 @@ class MCPClient(BaseClient):
|
|
|
99
105
|
method = "PATCH"
|
|
100
106
|
|
|
101
107
|
data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id}", json=kwargs)
|
|
102
|
-
|
|
108
|
+
response = MCPResponse(**data)
|
|
109
|
+
return MCP.from_response(response, client=self)
|
|
103
110
|
|
|
104
111
|
def delete_mcp(self, mcp_id: str) -> None:
|
|
105
112
|
"""Delete an MCP."""
|
|
106
113
|
self._request("DELETE", f"{MCPS_ENDPOINT}{mcp_id}")
|
|
107
114
|
|
|
115
|
+
def upsert_mcp(
|
|
116
|
+
self,
|
|
117
|
+
identifier: str | MCP,
|
|
118
|
+
description: str | None = None,
|
|
119
|
+
config: dict[str, Any] | None = None,
|
|
120
|
+
**kwargs,
|
|
121
|
+
) -> MCP:
|
|
122
|
+
"""Create or update an MCP by instance, ID, or name.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
identifier: MCP instance, ID (UUID string), or name
|
|
126
|
+
description: MCP description
|
|
127
|
+
config: MCP configuration dictionary
|
|
128
|
+
**kwargs: Additional parameters (transport, metadata, etc.)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The created or updated MCP.
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
>>> # By name (creates if not exists)
|
|
135
|
+
>>> mcp = client.mcps.upsert_mcp(
|
|
136
|
+
... "deepwiki",
|
|
137
|
+
... transport="sse",
|
|
138
|
+
... config={"url": "https://mcp.deepwiki.com/sse"},
|
|
139
|
+
... )
|
|
140
|
+
>>> # By instance
|
|
141
|
+
>>> mcp = client.mcps.upsert_mcp(existing_mcp, description="Updated")
|
|
142
|
+
>>> # By ID
|
|
143
|
+
>>> mcp = client.mcps.upsert_mcp("uuid-here", description="Updated")
|
|
144
|
+
"""
|
|
145
|
+
# Handle MCP instance
|
|
146
|
+
if isinstance(identifier, MCP):
|
|
147
|
+
if identifier.id:
|
|
148
|
+
logger.info("Updating MCP by instance: %s", identifier.name)
|
|
149
|
+
return self._do_upsert_update(identifier.id, identifier.name, description, config, **kwargs)
|
|
150
|
+
# MCP without ID - treat name as identifier
|
|
151
|
+
identifier = identifier.name
|
|
152
|
+
|
|
153
|
+
# Handle string (ID or name)
|
|
154
|
+
if isinstance(identifier, str):
|
|
155
|
+
if is_uuid(identifier):
|
|
156
|
+
logger.info("Updating MCP by ID: %s", identifier)
|
|
157
|
+
existing = self.get_mcp_by_id(identifier)
|
|
158
|
+
return self._do_upsert_update(identifier, existing.name, description, config, **kwargs)
|
|
159
|
+
|
|
160
|
+
# It's a name - find or create
|
|
161
|
+
return self._upsert_by_name(identifier, description, config, **kwargs)
|
|
162
|
+
|
|
163
|
+
raise ValueError(f"Invalid identifier type: {type(identifier)}")
|
|
164
|
+
|
|
165
|
+
def _do_upsert_update(
|
|
166
|
+
self,
|
|
167
|
+
mcp_id: str,
|
|
168
|
+
name: str | None,
|
|
169
|
+
description: str | None,
|
|
170
|
+
config: dict[str, Any] | None,
|
|
171
|
+
**kwargs,
|
|
172
|
+
) -> MCP:
|
|
173
|
+
"""Perform the update part of upsert."""
|
|
174
|
+
update_kwargs = {**kwargs}
|
|
175
|
+
if name is not None:
|
|
176
|
+
update_kwargs["name"] = name
|
|
177
|
+
if description is not None:
|
|
178
|
+
update_kwargs["description"] = description
|
|
179
|
+
if config is not None:
|
|
180
|
+
update_kwargs["config"] = config
|
|
181
|
+
return self.update_mcp(mcp_id, **update_kwargs)
|
|
182
|
+
|
|
183
|
+
def _upsert_by_name(
|
|
184
|
+
self,
|
|
185
|
+
name: str,
|
|
186
|
+
description: str | None,
|
|
187
|
+
config: dict[str, Any] | None,
|
|
188
|
+
**kwargs,
|
|
189
|
+
) -> MCP:
|
|
190
|
+
"""Find by name and update, or create if not found."""
|
|
191
|
+
existing = self.find_mcps(name)
|
|
192
|
+
|
|
193
|
+
if len(existing) == 1:
|
|
194
|
+
logger.info("Updating existing MCP: %s", name)
|
|
195
|
+
return self._do_upsert_update(existing[0].id, name, description, config, **kwargs)
|
|
196
|
+
|
|
197
|
+
if len(existing) > 1:
|
|
198
|
+
raise ValueError(f"Multiple MCPs found with name '{name}'")
|
|
199
|
+
|
|
200
|
+
# Create new MCP
|
|
201
|
+
logger.info("Creating new MCP: %s", name)
|
|
202
|
+
return self.create_mcp(name=name, description=description, config=config, **kwargs)
|
|
203
|
+
|
|
108
204
|
def _build_create_payload(
|
|
109
205
|
self,
|
|
110
206
|
name: str,
|
|
@@ -147,9 +243,7 @@ class MCPClient(BaseClient):
|
|
|
147
243
|
|
|
148
244
|
# Add any other kwargs (excluding already handled ones)
|
|
149
245
|
excluded_keys = {"type"} # type is handled above
|
|
150
|
-
|
|
151
|
-
if key not in excluded_keys:
|
|
152
|
-
payload[key] = value
|
|
246
|
+
add_kwargs_to_payload(payload, kwargs, excluded_keys)
|
|
153
247
|
|
|
154
248
|
return payload
|
|
155
249
|
|
|
@@ -179,9 +273,7 @@ class MCPClient(BaseClient):
|
|
|
179
273
|
update_data = {
|
|
180
274
|
"name": name if name is not None else current_mcp.name,
|
|
181
275
|
"type": DEFAULT_MCP_TYPE, # Required by backend, MCPs are always server type
|
|
182
|
-
"transport": kwargs.get(
|
|
183
|
-
"transport", getattr(current_mcp, "transport", DEFAULT_MCP_TRANSPORT)
|
|
184
|
-
),
|
|
276
|
+
"transport": kwargs.get("transport", getattr(current_mcp, "transport", DEFAULT_MCP_TRANSPORT)),
|
|
185
277
|
}
|
|
186
278
|
|
|
187
279
|
# Handle description with proper None handling
|
|
@@ -208,7 +300,23 @@ class MCPClient(BaseClient):
|
|
|
208
300
|
def get_mcp_tools(self, mcp_id: str) -> list[dict[str, Any]]:
|
|
209
301
|
"""Get tools available from an MCP."""
|
|
210
302
|
data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}/tools")
|
|
211
|
-
|
|
303
|
+
if data is None:
|
|
304
|
+
return []
|
|
305
|
+
if isinstance(data, list):
|
|
306
|
+
return data
|
|
307
|
+
if isinstance(data, dict):
|
|
308
|
+
if "tools" in data:
|
|
309
|
+
return data.get("tools", []) or []
|
|
310
|
+
logger.warning(
|
|
311
|
+
"Unexpected MCP tools response keys %s; returning empty list",
|
|
312
|
+
list(data.keys()),
|
|
313
|
+
)
|
|
314
|
+
return []
|
|
315
|
+
logger.warning(
|
|
316
|
+
"Unexpected MCP tools response type %s; returning empty list",
|
|
317
|
+
type(data).__name__,
|
|
318
|
+
)
|
|
319
|
+
return []
|
|
212
320
|
|
|
213
321
|
def test_mcp_connection(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
214
322
|
"""Test MCP connection using configuration.
|