glaip-sdk 0.5.5__py3-none-any.whl → 0.6.1__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.
Files changed (49) hide show
  1. glaip_sdk/__init__.py +4 -1
  2. glaip_sdk/agents/__init__.py +27 -0
  3. glaip_sdk/agents/base.py +996 -0
  4. glaip_sdk/cli/commands/common_config.py +36 -0
  5. glaip_sdk/cli/commands/tools.py +2 -5
  6. glaip_sdk/cli/config.py +13 -2
  7. glaip_sdk/cli/main.py +20 -0
  8. glaip_sdk/cli/slash/accounts_controller.py +217 -0
  9. glaip_sdk/cli/slash/accounts_shared.py +19 -0
  10. glaip_sdk/cli/slash/session.py +57 -7
  11. glaip_sdk/cli/slash/tui/accounts.tcss +54 -0
  12. glaip_sdk/cli/slash/tui/accounts_app.py +379 -0
  13. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  14. glaip_sdk/cli/slash/tui/loading.py +58 -0
  15. glaip_sdk/cli/slash/tui/remote_runs_app.py +9 -12
  16. glaip_sdk/client/_agent_payloads.py +10 -9
  17. glaip_sdk/client/agents.py +70 -8
  18. glaip_sdk/client/base.py +1 -0
  19. glaip_sdk/client/main.py +12 -4
  20. glaip_sdk/client/mcps.py +112 -10
  21. glaip_sdk/client/tools.py +151 -7
  22. glaip_sdk/mcps/__init__.py +21 -0
  23. glaip_sdk/mcps/base.py +345 -0
  24. glaip_sdk/models/__init__.py +65 -31
  25. glaip_sdk/models/agent.py +47 -0
  26. glaip_sdk/models/agent_runs.py +0 -1
  27. glaip_sdk/models/common.py +42 -0
  28. glaip_sdk/models/mcp.py +33 -0
  29. glaip_sdk/models/tool.py +33 -0
  30. glaip_sdk/registry/__init__.py +55 -0
  31. glaip_sdk/registry/agent.py +164 -0
  32. glaip_sdk/registry/base.py +139 -0
  33. glaip_sdk/registry/mcp.py +251 -0
  34. glaip_sdk/registry/tool.py +238 -0
  35. glaip_sdk/tools/__init__.py +22 -0
  36. glaip_sdk/tools/base.py +435 -0
  37. glaip_sdk/utils/__init__.py +50 -9
  38. glaip_sdk/utils/bundler.py +267 -0
  39. glaip_sdk/utils/client.py +111 -0
  40. glaip_sdk/utils/client_utils.py +26 -7
  41. glaip_sdk/utils/discovery.py +78 -0
  42. glaip_sdk/utils/import_resolver.py +492 -0
  43. glaip_sdk/utils/instructions.py +101 -0
  44. glaip_sdk/utils/sync.py +142 -0
  45. {glaip_sdk-0.5.5.dist-info → glaip_sdk-0.6.1.dist-info}/METADATA +5 -3
  46. {glaip_sdk-0.5.5.dist-info → glaip_sdk-0.6.1.dist-info}/RECORD +48 -22
  47. glaip_sdk/models.py +0 -241
  48. {glaip_sdk-0.5.5.dist-info → glaip_sdk-0.6.1.dist-info}/WHEEL +0 -0
  49. {glaip_sdk-0.5.5.dist-info → glaip_sdk-0.6.1.dist-info}/entry_points.txt +0 -0
@@ -3,6 +3,7 @@
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 asyncio
@@ -15,7 +16,7 @@ from pathlib import Path
15
16
  from typing import Any, BinaryIO
16
17
 
17
18
  import httpx
18
-
19
+ from glaip_sdk.agents import Agent
19
20
  from glaip_sdk.client._agent_payloads import (
20
21
  AgentCreateRequest,
21
22
  AgentListParams,
@@ -40,7 +41,7 @@ from glaip_sdk.config.constants import (
40
41
  DEFAULT_MODEL,
41
42
  )
42
43
  from glaip_sdk.exceptions import NotFoundError, ValidationError
43
- from glaip_sdk.models import Agent
44
+ from glaip_sdk.models import AgentResponse
44
45
  from glaip_sdk.payload_schemas.agent import list_server_only_fields
45
46
  from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
46
47
  from glaip_sdk.utils.client_utils import (
@@ -73,7 +74,9 @@ _DEFAULT_METADATA_TYPE = "custom"
73
74
 
74
75
 
75
76
  @asynccontextmanager
76
- async def _async_timeout_guard(timeout_seconds: float | None) -> AsyncGenerator[None, None]:
77
+ async def _async_timeout_guard(
78
+ timeout_seconds: float | None,
79
+ ) -> AsyncGenerator[None, None]:
77
80
  """Apply an asyncio timeout when a custom timeout is provided."""
78
81
  if timeout_seconds is None:
79
82
  yield
@@ -260,7 +263,7 @@ class AgentClient(BaseClient):
260
263
  self._renderer_manager = AgentRunRenderingManager(logger)
261
264
  self._tool_client: ToolClient | None = None
262
265
  self._mcp_client: MCPClient | None = None
263
- self._runs_client: "AgentRunsClient | None" = None
266
+ self._runs_client: AgentRunsClient | None = None
264
267
 
265
268
  def list_agents(
266
269
  self,
@@ -359,7 +362,8 @@ class AgentClient(BaseClient):
359
362
  status_code=404,
360
363
  )
361
364
 
362
- return Agent(**data)._set_client(self)
365
+ response = AgentResponse(**data)
366
+ return Agent.from_response(response, client=self)
363
367
 
364
368
  def find_agents(self, name: str | None = None) -> list[Agent]:
365
369
  """Find agents by name."""
@@ -826,7 +830,8 @@ class AgentClient(BaseClient):
826
830
  get_endpoint_fmt=f"{AGENTS_ENDPOINT}{{id}}",
827
831
  json=payload_dict,
828
832
  )
829
- return Agent(**full_agent_data)._set_client(self)
833
+ response = AgentResponse(**full_agent_data)
834
+ return Agent.from_response(response, client=self)
830
835
 
831
836
  def create_agent(
832
837
  self,
@@ -921,8 +926,9 @@ class AgentClient(BaseClient):
921
926
 
922
927
  payload_dict = request.to_payload(current_agent)
923
928
 
924
- response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
925
- return Agent(**response)._set_client(self)
929
+ api_response = self._request("PUT", f"/agents/{agent_id}", json=payload_dict)
930
+ response = AgentResponse(**api_response)
931
+ return Agent.from_response(response, client=self)
926
932
 
927
933
  def update_agent(
928
934
  self,
@@ -969,6 +975,62 @@ class AgentClient(BaseClient):
969
975
  """Delete an agent."""
970
976
  self._request("DELETE", f"/agents/{agent_id}")
971
977
 
978
+ def upsert_agent(self, identifier: str | Agent, **kwargs) -> Agent:
979
+ """Create or update an agent by instance, ID, or name.
980
+
981
+ Args:
982
+ identifier: Agent instance, ID (UUID string), or name
983
+ **kwargs: Agent configuration (instruction, description, tools, etc.)
984
+
985
+ Returns:
986
+ The created or updated agent.
987
+
988
+ Example:
989
+ >>> # By name (creates if not exists)
990
+ >>> agent = client.agents.upsert_agent(
991
+ ... "hello_agent",
992
+ ... instruction="You are a helpful assistant.",
993
+ ... description="A friendly agent",
994
+ ... )
995
+ >>> # By instance
996
+ >>> agent = client.agents.upsert_agent(existing_agent, description="Updated")
997
+ >>> # By ID
998
+ >>> agent = client.agents.upsert_agent("uuid-here", description="Updated")
999
+ """
1000
+ # Handle Agent instance
1001
+ if isinstance(identifier, Agent):
1002
+ if identifier.id:
1003
+ logger.info("Updating agent by instance: %s", identifier.name)
1004
+ return self.update_agent(identifier.id, name=identifier.name, **kwargs)
1005
+ identifier = identifier.name
1006
+
1007
+ # Handle string (ID or name)
1008
+ if isinstance(identifier, str):
1009
+ # Check if it's a UUID
1010
+ if is_uuid(identifier):
1011
+ logger.info("Updating agent by ID: %s", identifier)
1012
+ return self.update_agent(identifier, **kwargs)
1013
+
1014
+ # It's a name - find or create
1015
+ return self._upsert_agent_by_name(identifier, **kwargs)
1016
+
1017
+ raise ValueError(f"Invalid identifier type: {type(identifier)}")
1018
+
1019
+ def _upsert_agent_by_name(self, name: str, **kwargs) -> Agent:
1020
+ """Find agent by name and update, or create if not found."""
1021
+ existing = self.find_agents(name)
1022
+
1023
+ if len(existing) == 1:
1024
+ logger.info("Updating existing agent: %s", name)
1025
+ return self.update_agent(existing[0].id, name=name, **kwargs)
1026
+
1027
+ if len(existing) > 1:
1028
+ raise ValueError(f"Multiple agents found with name '{name}'")
1029
+
1030
+ # Create new agent
1031
+ logger.info("Creating new agent: %s", name)
1032
+ return self.create_agent(name=name, **kwargs)
1033
+
972
1034
  def _prepare_sync_request_data(
973
1035
  self,
974
1036
  message: str,
glaip_sdk/client/base.py CHANGED
@@ -3,6 +3,7 @@
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
glaip_sdk/client/main.py CHANGED
@@ -3,16 +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 typing import Any
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
13
16
  from glaip_sdk.client.shared import build_shared_config
14
17
  from glaip_sdk.client.tools import ToolClient
15
- from glaip_sdk.models import MCP, Agent, Tool
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
16
24
 
17
25
 
18
26
  class Client(BaseClient):
@@ -49,7 +57,7 @@ class Client(BaseClient):
49
57
  name: str | None = None,
50
58
  version: str | None = None,
51
59
  sync_langflow_agents: bool = False,
52
- ) -> list[Agent]:
60
+ ) -> AgentListResult:
53
61
  """List agents with optional filtering.
54
62
 
55
63
  Args:
@@ -60,7 +68,7 @@ class Client(BaseClient):
60
68
  sync_langflow_agents: Sync with LangFlow server before listing (only applies when agent_type=langflow)
61
69
 
62
70
  Returns:
63
- List of agents matching the filters
71
+ AgentListResult with agents and pagination metadata. Supports iteration and indexing.
64
72
  """
65
73
  return self.agents.list_agents(
66
74
  agent_type=agent_type,
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
- DEFAULT_MCP_TRANSPORT,
14
- DEFAULT_MCP_TYPE,
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.models import MCP
17
- from glaip_sdk.utils.client_utils import add_kwargs_to_payload, 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
- return MCP(**data)._set_client(self)
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
- return MCP(**full_mcp_data)._set_client(self)
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
- return MCP(**data)._set_client(self)
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,
@@ -211,9 +307,15 @@ class MCPClient(BaseClient):
211
307
  if isinstance(data, dict):
212
308
  if "tools" in data:
213
309
  return data.get("tools", []) or []
214
- logger.warning("Unexpected MCP tools response keys %s; returning empty list", list(data.keys()))
310
+ logger.warning(
311
+ "Unexpected MCP tools response keys %s; returning empty list",
312
+ list(data.keys()),
313
+ )
215
314
  return []
216
- logger.warning("Unexpected MCP tools response type %s; returning empty list", type(data).__name__)
315
+ logger.warning(
316
+ "Unexpected MCP tools response type %s; returning empty list",
317
+ type(data).__name__,
318
+ )
217
319
  return []
218
320
 
219
321
  def test_mcp_connection(self, config: dict[str, Any]) -> dict[str, Any]:
glaip_sdk/client/tools.py CHANGED
@@ -3,6 +3,7 @@
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
@@ -16,12 +17,14 @@ from glaip_sdk.config.constants import (
16
17
  DEFAULT_TOOL_TYPE,
17
18
  DEFAULT_TOOL_VERSION,
18
19
  )
19
- from glaip_sdk.models import Tool
20
+ from glaip_sdk.models import ToolResponse
21
+ from glaip_sdk.tools import Tool
20
22
  from glaip_sdk.utils.client_utils import (
21
23
  add_kwargs_to_payload,
22
24
  create_model_instances,
23
25
  find_by_name,
24
26
  )
27
+ from glaip_sdk.utils.resource_refs import is_uuid
25
28
 
26
29
  # API endpoints
27
30
  TOOLS_ENDPOINT = "/tools/"
@@ -59,11 +62,11 @@ class ToolClient(BaseClient):
59
62
  def get_tool_by_id(self, tool_id: str) -> Tool:
60
63
  """Get tool by ID."""
61
64
  data = self._request("GET", f"{TOOLS_ENDPOINT}{tool_id}")
62
- return Tool(**data)._set_client(self)
65
+ response = ToolResponse(**data)
66
+ return Tool.from_response(response, client=self)
63
67
 
64
68
  def find_tools(self, name: str | None = None) -> list[Tool]:
65
69
  """Find tools by name."""
66
- # Backend doesn't support name query parameter, so we fetch all and filter client-side
67
70
  data = self._request("GET", TOOLS_ENDPOINT)
68
71
  tools = create_model_instances(data, Tool, self)
69
72
  return find_by_name(tools, name, case_sensitive=False)
@@ -112,6 +115,7 @@ class ToolClient(BaseClient):
112
115
  data = {
113
116
  "name": name,
114
117
  "framework": framework,
118
+ "type": kwargs.pop("tool_type", DEFAULT_TOOL_TYPE), # Default to custom
115
119
  }
116
120
 
117
121
  if description:
@@ -153,7 +157,8 @@ class ToolClient(BaseClient):
153
157
  data=upload_data,
154
158
  )
155
159
 
156
- return Tool(**response)._set_client(self)
160
+ tool_response = ToolResponse(**response)
161
+ return Tool.from_response(tool_response, client=self)
157
162
 
158
163
  def _build_create_payload(
159
164
  self,
@@ -275,6 +280,9 @@ class ToolClient(BaseClient):
275
280
  or getattr(current_tool, "type", None)
276
281
  or DEFAULT_TOOL_TYPE
277
282
  )
283
+ # Convert enum to string value for API payload
284
+ if hasattr(current_type, "value"):
285
+ current_type = current_type.value
278
286
 
279
287
  update_data = {
280
288
  "name": name if name is not None else current_tool.name,
@@ -434,12 +442,147 @@ class ToolClient(BaseClient):
434
442
  def update_tool(self, tool_id: str, **kwargs) -> Tool:
435
443
  """Update an existing tool."""
436
444
  data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id}", json=kwargs)
437
- return Tool(**data)._set_client(self)
445
+ response = ToolResponse(**data)
446
+ return Tool.from_response(response, client=self)
438
447
 
439
448
  def delete_tool(self, tool_id: str) -> None:
440
449
  """Delete a tool."""
441
450
  self._request("DELETE", f"{TOOLS_ENDPOINT}{tool_id}")
442
451
 
452
+ def upsert_tool(
453
+ self,
454
+ identifier: str | Tool,
455
+ code: str | None = None,
456
+ description: str | None = None,
457
+ framework: str = "langchain",
458
+ **kwargs,
459
+ ) -> Tool:
460
+ """Create or update a tool by instance, ID, or name.
461
+
462
+ Args:
463
+ identifier: Tool instance, ID (UUID string), or name
464
+ code: Python code containing the tool plugin (required for create)
465
+ description: Tool description
466
+ framework: Tool framework (defaults to "langchain")
467
+ **kwargs: Additional parameters (tags, version, etc.)
468
+
469
+ Returns:
470
+ The created or updated tool.
471
+
472
+ Example:
473
+ >>> # By name with code (creates if not exists)
474
+ >>> tool = client.tools.upsert_tool(
475
+ ... "greeting",
476
+ ... code=bundled_source,
477
+ ... description="A greeting tool",
478
+ ... )
479
+ >>> # By instance
480
+ >>> tool = client.tools.upsert_tool(existing_tool, code=new_code)
481
+ >>> # By ID
482
+ >>> tool = client.tools.upsert_tool("uuid-here", code=new_code)
483
+ """
484
+ # Handle Tool instance
485
+ if isinstance(identifier, Tool):
486
+ if identifier.id:
487
+ logger.info("Updating tool by instance: %s", identifier.name)
488
+ return self._do_tool_upsert_update(
489
+ identifier.id,
490
+ identifier.name,
491
+ code,
492
+ description,
493
+ framework,
494
+ **kwargs,
495
+ )
496
+ identifier = identifier.name
497
+
498
+ # Handle string (ID or name)
499
+ if isinstance(identifier, str):
500
+ if is_uuid(identifier):
501
+ logger.info("Updating tool by ID: %s", identifier)
502
+ existing = self.get_tool_by_id(identifier)
503
+ return self._do_tool_upsert_update(identifier, existing.name, code, description, framework, **kwargs)
504
+
505
+ # It's a name - find or create
506
+ return self._upsert_tool_by_name(identifier, code, description, framework, **kwargs)
507
+
508
+ raise ValueError(f"Invalid identifier type: {type(identifier)}")
509
+
510
+ def _do_tool_upsert_update(
511
+ self,
512
+ tool_id: str,
513
+ name: str | None,
514
+ code: str | None,
515
+ description: str | None,
516
+ framework: str,
517
+ **kwargs,
518
+ ) -> Tool:
519
+ """Perform the update part of tool upsert."""
520
+ if code:
521
+ # Update via file upload
522
+ with tempfile.NamedTemporaryFile(
523
+ mode="w",
524
+ suffix=".py",
525
+ prefix=f"{name or 'tool'}_",
526
+ delete=False,
527
+ encoding="utf-8",
528
+ ) as temp_file:
529
+ temp_file.write(code)
530
+ temp_file_path = temp_file.name
531
+
532
+ try:
533
+ return self.update_tool_via_file(
534
+ tool_id,
535
+ temp_file_path,
536
+ name=name,
537
+ description=description,
538
+ framework=framework,
539
+ **kwargs,
540
+ )
541
+ finally:
542
+ try:
543
+ os.unlink(temp_file_path)
544
+ except OSError:
545
+ pass
546
+ else:
547
+ # Metadata-only update
548
+ update_kwargs = {"framework": framework, **kwargs}
549
+ if name:
550
+ update_kwargs["name"] = name
551
+ if description:
552
+ update_kwargs["description"] = description
553
+ return self.update_tool(tool_id, **update_kwargs)
554
+
555
+ def _upsert_tool_by_name(
556
+ self,
557
+ name: str,
558
+ code: str | None,
559
+ description: str | None,
560
+ framework: str,
561
+ **kwargs,
562
+ ) -> Tool:
563
+ """Find tool by name and update, or create if not found."""
564
+ existing = self.find_tools(name)
565
+
566
+ if len(existing) == 1:
567
+ logger.info("Updating existing tool: %s", name)
568
+ return self._do_tool_upsert_update(existing[0].id, name, code, description, framework, **kwargs)
569
+
570
+ if len(existing) > 1:
571
+ raise ValueError(f"Multiple tools found with name '{name}'")
572
+
573
+ # Create new tool - code is required
574
+ if not code:
575
+ raise ValueError(f"Tool '{name}' not found and no code provided for creation")
576
+
577
+ logger.info("Creating new tool: %s", name)
578
+ return self.create_tool_from_code(
579
+ name=name,
580
+ code=code,
581
+ framework=framework,
582
+ description=description,
583
+ **kwargs,
584
+ )
585
+
443
586
  def get_tool_script(self, tool_id: str) -> str:
444
587
  """Get the tool script content.
445
588
 
@@ -508,8 +651,9 @@ class ToolClient(BaseClient):
508
651
  data=update_payload,
509
652
  )
510
653
 
511
- return Tool(**response)._set_client(self)
654
+ tool_response = ToolResponse(**response)
655
+ return Tool.from_response(tool_response, client=self)
512
656
 
513
657
  except Exception as e:
514
- logger.error(f"Failed to update tool {tool_id} via file: {e}")
658
+ logger.error("Failed to update tool %s via file: %s", tool_id, e)
515
659
  raise
@@ -0,0 +1,21 @@
1
+ """MCP (Model Context Protocol) package for GL AIP platform.
2
+
3
+ This package provides the MCP class and MCPRegistry for managing
4
+ Model Context Protocol configurations on the GL AIP platform.
5
+
6
+ Example:
7
+ >>> from glaip_sdk.mcps import MCP, get_mcp_registry
8
+ >>> mcp = MCP.from_native("arxiv-search")
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from glaip_sdk.mcps.base import MCP, MCPConfigValue
14
+ from glaip_sdk.registry.mcp import MCPRegistry, get_mcp_registry
15
+
16
+ __all__ = [
17
+ "MCP",
18
+ "MCPConfigValue",
19
+ "MCPRegistry",
20
+ "get_mcp_registry",
21
+ ]