glaip-sdk 0.1.3__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.
Files changed (141) hide show
  1. glaip_sdk/__init__.py +5 -2
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1191 -0
  5. glaip_sdk/branding.py +13 -0
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/auth.py +254 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents.py +213 -73
  11. glaip_sdk/cli/commands/common_config.py +101 -0
  12. glaip_sdk/cli/commands/configure.py +729 -113
  13. glaip_sdk/cli/commands/mcps.py +241 -72
  14. glaip_sdk/cli/commands/models.py +11 -5
  15. glaip_sdk/cli/commands/tools.py +49 -57
  16. glaip_sdk/cli/commands/transcripts.py +755 -0
  17. glaip_sdk/cli/config.py +48 -4
  18. glaip_sdk/cli/constants.py +38 -0
  19. glaip_sdk/cli/context.py +8 -0
  20. glaip_sdk/cli/core/__init__.py +79 -0
  21. glaip_sdk/cli/core/context.py +124 -0
  22. glaip_sdk/cli/core/output.py +846 -0
  23. glaip_sdk/cli/core/prompting.py +649 -0
  24. glaip_sdk/cli/core/rendering.py +187 -0
  25. glaip_sdk/cli/display.py +35 -19
  26. glaip_sdk/cli/hints.py +57 -0
  27. glaip_sdk/cli/io.py +6 -3
  28. glaip_sdk/cli/main.py +228 -119
  29. glaip_sdk/cli/masking.py +21 -33
  30. glaip_sdk/cli/pager.py +9 -10
  31. glaip_sdk/cli/parsers/__init__.py +1 -3
  32. glaip_sdk/cli/slash/__init__.py +0 -9
  33. glaip_sdk/cli/slash/accounts_controller.py +578 -0
  34. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  35. glaip_sdk/cli/slash/agent_session.py +62 -21
  36. glaip_sdk/cli/slash/prompt.py +21 -0
  37. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  38. glaip_sdk/cli/slash/session.py +771 -140
  39. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  40. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  41. glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
  42. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  43. glaip_sdk/cli/slash/tui/loading.py +58 -0
  44. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  45. glaip_sdk/cli/transcript/__init__.py +12 -52
  46. glaip_sdk/cli/transcript/cache.py +255 -44
  47. glaip_sdk/cli/transcript/capture.py +27 -1
  48. glaip_sdk/cli/transcript/history.py +815 -0
  49. glaip_sdk/cli/transcript/viewer.py +72 -499
  50. glaip_sdk/cli/update_notifier.py +14 -5
  51. glaip_sdk/cli/utils.py +243 -1252
  52. glaip_sdk/cli/validators.py +5 -6
  53. glaip_sdk/client/__init__.py +2 -1
  54. glaip_sdk/client/_agent_payloads.py +45 -9
  55. glaip_sdk/client/agent_runs.py +147 -0
  56. glaip_sdk/client/agents.py +287 -29
  57. glaip_sdk/client/base.py +1 -0
  58. glaip_sdk/client/main.py +19 -10
  59. glaip_sdk/client/mcps.py +122 -12
  60. glaip_sdk/client/run_rendering.py +133 -88
  61. glaip_sdk/client/shared.py +21 -0
  62. glaip_sdk/client/tools.py +155 -10
  63. glaip_sdk/config/constants.py +11 -0
  64. glaip_sdk/mcps/__init__.py +21 -0
  65. glaip_sdk/mcps/base.py +345 -0
  66. glaip_sdk/models/__init__.py +90 -0
  67. glaip_sdk/models/agent.py +47 -0
  68. glaip_sdk/models/agent_runs.py +116 -0
  69. glaip_sdk/models/common.py +42 -0
  70. glaip_sdk/models/mcp.py +33 -0
  71. glaip_sdk/models/tool.py +33 -0
  72. glaip_sdk/payload_schemas/__init__.py +1 -13
  73. glaip_sdk/registry/__init__.py +55 -0
  74. glaip_sdk/registry/agent.py +164 -0
  75. glaip_sdk/registry/base.py +139 -0
  76. glaip_sdk/registry/mcp.py +253 -0
  77. glaip_sdk/registry/tool.py +232 -0
  78. glaip_sdk/rich_components.py +58 -2
  79. glaip_sdk/runner/__init__.py +59 -0
  80. glaip_sdk/runner/base.py +84 -0
  81. glaip_sdk/runner/deps.py +115 -0
  82. glaip_sdk/runner/langgraph.py +706 -0
  83. glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
  84. glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
  85. glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
  86. glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
  87. glaip_sdk/runner/tool_adapter/__init__.py +18 -0
  88. glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
  89. glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
  90. glaip_sdk/tools/__init__.py +22 -0
  91. glaip_sdk/tools/base.py +435 -0
  92. glaip_sdk/utils/__init__.py +58 -12
  93. glaip_sdk/utils/a2a/__init__.py +34 -0
  94. glaip_sdk/utils/a2a/event_processor.py +188 -0
  95. glaip_sdk/utils/bundler.py +267 -0
  96. glaip_sdk/utils/client.py +111 -0
  97. glaip_sdk/utils/client_utils.py +39 -7
  98. glaip_sdk/utils/datetime_helpers.py +58 -0
  99. glaip_sdk/utils/discovery.py +78 -0
  100. glaip_sdk/utils/display.py +23 -15
  101. glaip_sdk/utils/export.py +143 -0
  102. glaip_sdk/utils/general.py +0 -33
  103. glaip_sdk/utils/import_export.py +12 -7
  104. glaip_sdk/utils/import_resolver.py +492 -0
  105. glaip_sdk/utils/instructions.py +101 -0
  106. glaip_sdk/utils/rendering/__init__.py +115 -1
  107. glaip_sdk/utils/rendering/formatting.py +5 -30
  108. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  109. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  110. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  111. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  112. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  113. glaip_sdk/utils/rendering/models.py +1 -0
  114. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  115. glaip_sdk/utils/rendering/renderer/base.py +217 -1476
  116. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  117. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  118. glaip_sdk/utils/rendering/renderer/stream.py +4 -12
  119. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  120. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  121. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  122. glaip_sdk/utils/rendering/state.py +204 -0
  123. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  124. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  125. glaip_sdk/utils/rendering/steps/format.py +176 -0
  126. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  127. glaip_sdk/utils/rendering/timing.py +36 -0
  128. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  129. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  130. glaip_sdk/utils/resource_refs.py +25 -13
  131. glaip_sdk/utils/runtime_config.py +425 -0
  132. glaip_sdk/utils/serialization.py +18 -0
  133. glaip_sdk/utils/sync.py +142 -0
  134. glaip_sdk/utils/tool_detection.py +33 -0
  135. glaip_sdk/utils/validation.py +16 -24
  136. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
  137. glaip_sdk-0.6.10.dist-info/RECORD +159 -0
  138. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
  139. glaip_sdk/models.py +0 -240
  140. glaip_sdk-0.1.3.dist-info/RECORD +0 -83
  141. {glaip_sdk-0.1.3.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
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,11 +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 (
23
+ add_kwargs_to_payload,
21
24
  create_model_instances,
22
25
  find_by_name,
23
26
  )
27
+ from glaip_sdk.utils.resource_refs import is_uuid
24
28
 
25
29
  # API endpoints
26
30
  TOOLS_ENDPOINT = "/tools/"
@@ -58,11 +62,11 @@ class ToolClient(BaseClient):
58
62
  def get_tool_by_id(self, tool_id: str) -> Tool:
59
63
  """Get tool by ID."""
60
64
  data = self._request("GET", f"{TOOLS_ENDPOINT}{tool_id}")
61
- return Tool(**data)._set_client(self)
65
+ response = ToolResponse(**data)
66
+ return Tool.from_response(response, client=self)
62
67
 
63
68
  def find_tools(self, name: str | None = None) -> list[Tool]:
64
69
  """Find tools by name."""
65
- # Backend doesn't support name query parameter, so we fetch all and filter client-side
66
70
  data = self._request("GET", TOOLS_ENDPOINT)
67
71
  tools = create_model_instances(data, Tool, self)
68
72
  return find_by_name(tools, name, case_sensitive=False)
@@ -111,6 +115,7 @@ class ToolClient(BaseClient):
111
115
  data = {
112
116
  "name": name,
113
117
  "framework": framework,
118
+ "type": kwargs.pop("tool_type", DEFAULT_TOOL_TYPE), # Default to custom
114
119
  }
115
120
 
116
121
  if description:
@@ -152,7 +157,8 @@ class ToolClient(BaseClient):
152
157
  data=upload_data,
153
158
  )
154
159
 
155
- return Tool(**response)._set_client(self)
160
+ tool_response = ToolResponse(**response)
161
+ return Tool.from_response(tool_response, client=self)
156
162
 
157
163
  def _build_create_payload(
158
164
  self,
@@ -200,9 +206,7 @@ class ToolClient(BaseClient):
200
206
 
201
207
  # Add any other kwargs (excluding already handled ones)
202
208
  excluded_keys = {"tags", "version"}
203
- for key, value in kwargs.items():
204
- if key not in excluded_keys:
205
- payload[key] = value
209
+ add_kwargs_to_payload(payload, kwargs, excluded_keys)
206
210
 
207
211
  return payload
208
212
 
@@ -276,6 +280,9 @@ class ToolClient(BaseClient):
276
280
  or getattr(current_tool, "type", None)
277
281
  or DEFAULT_TOOL_TYPE
278
282
  )
283
+ # Convert enum to string value for API payload
284
+ if hasattr(current_type, "value"):
285
+ current_type = current_type.value
279
286
 
280
287
  update_data = {
281
288
  "name": name if name is not None else current_tool.name,
@@ -435,12 +442,149 @@ class ToolClient(BaseClient):
435
442
  def update_tool(self, tool_id: str, **kwargs) -> Tool:
436
443
  """Update an existing tool."""
437
444
  data = self._request("PUT", f"{TOOLS_ENDPOINT}{tool_id}", json=kwargs)
438
- return Tool(**data)._set_client(self)
445
+ response = ToolResponse(**data)
446
+ return Tool.from_response(response, client=self)
439
447
 
440
448
  def delete_tool(self, tool_id: str) -> None:
441
449
  """Delete a tool."""
442
450
  self._request("DELETE", f"{TOOLS_ENDPOINT}{tool_id}")
443
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
+ name_lower = name.lower()
566
+ exact_matches = [tool for tool in existing if tool.name and tool.name.lower() == name_lower]
567
+
568
+ if len(exact_matches) == 1:
569
+ logger.info("Updating existing tool: %s", name)
570
+ return self._do_tool_upsert_update(exact_matches[0].id, name, code, description, framework, **kwargs)
571
+
572
+ if len(exact_matches) > 1:
573
+ raise ValueError(f"Multiple tools found with name '{name}'")
574
+
575
+ # Create new tool - code is required
576
+ if not code:
577
+ raise ValueError(f"Tool '{name}' not found and no code provided for creation")
578
+
579
+ logger.info("Creating new tool: %s", name)
580
+ return self.create_tool_from_code(
581
+ name=name,
582
+ code=code,
583
+ framework=framework,
584
+ description=description,
585
+ **kwargs,
586
+ )
587
+
444
588
  def get_tool_script(self, tool_id: str) -> str:
445
589
  """Get the tool script content.
446
590
 
@@ -509,8 +653,9 @@ class ToolClient(BaseClient):
509
653
  data=update_payload,
510
654
  )
511
655
 
512
- return Tool(**response)._set_client(self)
656
+ tool_response = ToolResponse(**response)
657
+ return Tool.from_response(tool_response, client=self)
513
658
 
514
659
  except Exception as e:
515
- logger.error(f"Failed to update tool {tool_id} via file: {e}")
660
+ logger.error("Failed to update tool %s via file: %s", tool_id, e)
516
661
  raise
@@ -39,3 +39,14 @@ DEFAULT_MCP_TRANSPORT = "stdio"
39
39
 
40
40
  # Default error messages
41
41
  DEFAULT_ERROR_MESSAGE = "Unknown error"
42
+
43
+ # Agent configuration fields used for CLI args and payload building
44
+ AGENT_CONFIG_FIELDS = (
45
+ "name",
46
+ "instruction",
47
+ "model",
48
+ "tools",
49
+ "agents",
50
+ "mcps",
51
+ "timeout",
52
+ )
@@ -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
+ ]
glaip_sdk/mcps/base.py ADDED
@@ -0,0 +1,345 @@
1
+ """MCP (Model Context Protocol) helper for glaip_sdk.
2
+
3
+ Provides a simple, migration-ready way to declare and resolve MCPs with
4
+ in-memory caching and create-on-missing functionality.
5
+
6
+ The MCP class also supports runtime operations (update, delete, get_tools)
7
+ when retrieved from the API via client.mcps.get().
8
+
9
+ Authors:
10
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
11
+
12
+ Example - Lazy Reference:
13
+ >>> from glaip_sdk.mcps import MCP
14
+ >>>
15
+ >>> # Create from known ID
16
+ >>> mcp = MCP.from_id("mcp_abc123")
17
+ >>>
18
+ >>> # Create lookup-only by name (error if not found)
19
+ >>> mcp = MCP.from_native("arxiv-search")
20
+ >>>
21
+ >>> # Create for lookup/creation by name (create if missing)
22
+ >>> mcp = MCP(name="my-filesystem-mcp", transport="sse", config={"url": "..."})
23
+
24
+ Example - Runtime Operations:
25
+ >>> from glaip_sdk import Glaip
26
+ >>>
27
+ >>> client = Glaip()
28
+ >>> mcp = client.mcps.get("mcp-123")
29
+ >>> tools = mcp.get_tools() # Get tools from MCP
30
+ >>> mcp.update(description="Updated description")
31
+ >>> mcp.delete()
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from typing import TYPE_CHECKING, Any
37
+
38
+ if TYPE_CHECKING:
39
+ from glaip_sdk.models import MCPResponse
40
+
41
+ # Type alias for MCP configuration values
42
+ MCPConfigValue = str | int | bool | list[str] | dict[str, str]
43
+
44
+ _MCP_NOT_DEPLOYED_MSG = "MCP not available on platform. No ID set."
45
+ _CLIENT_NOT_AVAILABLE_MSG = "Client not available. Use client.mcps.get() to get a client-connected MCP."
46
+
47
+
48
+ class MCP:
49
+ """MCP reference helper for declaring MCPs in Agent definitions.
50
+
51
+ Supports both lazy references and runtime operations:
52
+ - Lazy reference: Created via from_native() or from_id()
53
+ - Runtime: Created via from_response() or client.mcps.get()
54
+
55
+ Attributes:
56
+ name: Human-readable MCP name (used for lookup/creation).
57
+ id: Backend MCP ID (used for direct fetch if known).
58
+ transport: Transport type (e.g., "sse", "stdio", "websocket").
59
+ config: Transport configuration dict (URLs, args, env vars).
60
+ description: Optional description for the MCP.
61
+ metadata: Optional additional metadata dict.
62
+ authentication: Authentication configuration.
63
+
64
+ Example - Lazy Reference:
65
+ >>> # Create from known ID
66
+ >>> mcp = MCP.from_id("mcp_abc123")
67
+ >>>
68
+ >>> # Create lookup-only by name (error if not found)
69
+ >>> mcp = MCP.from_native("arxiv-search")
70
+ >>>
71
+ >>> # Create for lookup/creation by name (create if missing)
72
+ >>> mcp = MCP(name="my-filesystem-mcp", transport="sse", config={"url": "..."})
73
+
74
+ Example - Runtime Operations:
75
+ >>> mcp = client.mcps.get("mcp-123")
76
+ >>> mcp.update(description="New description")
77
+ >>> mcp.delete()
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ name: str | None = None,
83
+ *,
84
+ id: str | None = None, # noqa: A002 - Allow shadowing builtin for API compat
85
+ transport: str | None = None,
86
+ config: dict[str, MCPConfigValue] | None = None,
87
+ description: str | None = None,
88
+ metadata: dict[str, Any] | None = None,
89
+ authentication: dict[str, Any] | None = None,
90
+ _lookup_only: bool = False,
91
+ _client: Any = None,
92
+ ) -> None:
93
+ """Initialize an MCP.
94
+
95
+ Args:
96
+ name: Human-readable MCP name.
97
+ id: Backend MCP ID.
98
+ transport: Transport type (e.g., "sse", "stdio").
99
+ config: Transport configuration dict.
100
+ description: Optional description.
101
+ metadata: Optional metadata dict.
102
+ authentication: Authentication configuration.
103
+ _lookup_only: If True, don't create if not found.
104
+ _client: Internal client reference.
105
+
106
+ Raises:
107
+ ValueError: If neither name nor id is provided.
108
+ """
109
+ if not name and not id:
110
+ raise ValueError("At least one of 'name' or 'id' must be provided")
111
+
112
+ self.name = name
113
+ self._id = id
114
+ self.transport = transport
115
+ self.config = config
116
+ self.description = description
117
+ self.metadata = metadata
118
+ self.authentication = authentication
119
+ self._lookup_only = _lookup_only
120
+ self._client = _client
121
+
122
+ @property
123
+ def id(self) -> str | None: # noqa: A003 - Allow shadowing builtin for API compat
124
+ """MCP ID on the platform."""
125
+ return self._id
126
+
127
+ @id.setter
128
+ def id(self, value: str | None) -> None: # noqa: A003
129
+ """Set the MCP ID."""
130
+ self._id = value
131
+
132
+ def __repr__(self) -> str:
133
+ """Return string representation."""
134
+ if self._id:
135
+ return f"MCP(id={self._id!r}, name={self.name!r})"
136
+ if self._lookup_only:
137
+ return f"MCP.from_native({self.name!r})"
138
+ return f"MCP(name={self.name!r})"
139
+
140
+ def __eq__(self, other: object) -> bool:
141
+ """Check equality based on id if available, else name."""
142
+ if not isinstance(other, MCP):
143
+ return NotImplemented
144
+ if self._id and other._id:
145
+ return self._id == other._id
146
+ return self.name == other.name
147
+
148
+ def __hash__(self) -> int:
149
+ """Hash based on id if available, else name."""
150
+ if self._id:
151
+ return hash(self._id)
152
+ return hash(self.name)
153
+
154
+ def model_dump(self, *, exclude_none: bool = False) -> dict[str, Any]:
155
+ """Return a dict representation of the MCP.
156
+
157
+ Provides Pydantic-style serialization for backward compatibility.
158
+
159
+ Args:
160
+ exclude_none: If True, exclude None values from the output.
161
+
162
+ Returns:
163
+ Dictionary containing MCP attributes.
164
+ """
165
+ data = {
166
+ "id": self._id,
167
+ "name": self.name,
168
+ "transport": self.transport,
169
+ "config": self.config,
170
+ "description": self.description,
171
+ "metadata": self.metadata,
172
+ "authentication": self.authentication,
173
+ }
174
+ if exclude_none:
175
+ return {k: v for k, v in data.items() if v is not None}
176
+ return data
177
+
178
+ @classmethod
179
+ def from_native(cls, name: str) -> MCP:
180
+ """Create a lookup-only MCP reference by name.
181
+
182
+ Use this when referencing an MCP that already exists on the platform.
183
+ Resolution will NOT create the MCP if not found - it will raise an error.
184
+
185
+ Args:
186
+ name: The name of the existing MCP.
187
+
188
+ Returns:
189
+ MCP instance configured for lookup-only resolution.
190
+
191
+ Raises:
192
+ ValueError: If name is empty.
193
+
194
+ Example:
195
+ >>> mcp = MCP.from_native("arxiv-search")
196
+ >>> # Registry will find by name, error if not found or ambiguous
197
+ """
198
+ if not name:
199
+ raise ValueError("Name cannot be empty")
200
+ return cls(name=name, _lookup_only=True)
201
+
202
+ @classmethod
203
+ def from_id(cls, mcp_id: str) -> MCP:
204
+ """Create an MCP helper for lookup-only by ID.
205
+
206
+ This creates a minimal MCP reference that will be resolved
207
+ from the backend using the ID. Use this when you know the
208
+ backend MCP ID but don't have the full configuration.
209
+
210
+ Args:
211
+ mcp_id: The backend MCP ID.
212
+
213
+ Returns:
214
+ An MCP instance with only the ID set, marked for lookup-only.
215
+
216
+ Raises:
217
+ ValueError: If mcp_id is empty.
218
+
219
+ Example:
220
+ >>> mcp = MCP.from_id("550e8400-e29b-41d4-a716-446655440000")
221
+ >>> # Registry will fetch directly by ID
222
+ """
223
+ if not mcp_id:
224
+ raise ValueError("ID cannot be empty")
225
+ return cls(id=mcp_id, _lookup_only=True)
226
+
227
+ # ─────────────────────────────────────────────────────────────────
228
+ # Runtime Methods (require client connection)
229
+ # ─────────────────────────────────────────────────────────────────
230
+
231
+ def _set_client(self, client: Any) -> MCP:
232
+ """Set the client reference for this MCP.
233
+
234
+ Args:
235
+ client: The Glaip client instance.
236
+
237
+ Returns:
238
+ Self for method chaining.
239
+ """
240
+ self._client = client
241
+ return self
242
+
243
+ def get_tools(self) -> list[dict[str, Any]]:
244
+ """Get tools available from this MCP.
245
+
246
+ Returns:
247
+ List of tool definitions from the MCP.
248
+
249
+ Raises:
250
+ ValueError: If the MCP has no ID.
251
+ RuntimeError: If client is not available.
252
+ """
253
+ if not self._id:
254
+ raise ValueError(_MCP_NOT_DEPLOYED_MSG)
255
+ if not self._client:
256
+ raise RuntimeError(_CLIENT_NOT_AVAILABLE_MSG)
257
+
258
+ # Delegate to the client's MCP tools endpoint
259
+ return self._client.mcps.get_tools(mcp_id=self._id)
260
+
261
+ def update(self, **kwargs: Any) -> MCP:
262
+ """Update the MCP with new configuration.
263
+
264
+ Args:
265
+ **kwargs: MCP properties to update (name, description, config, etc.).
266
+
267
+ Returns:
268
+ Self with updated properties.
269
+
270
+ Raises:
271
+ ValueError: If the MCP has no ID.
272
+ RuntimeError: If client is not available.
273
+ """
274
+ if not self._id:
275
+ raise ValueError(_MCP_NOT_DEPLOYED_MSG)
276
+ if not self._client:
277
+ raise RuntimeError(_CLIENT_NOT_AVAILABLE_MSG)
278
+
279
+ response = self._client.mcps.update(mcp_id=self._id, **kwargs)
280
+
281
+ # Update local properties from response
282
+ if hasattr(response, "name") and response.name:
283
+ self.name = response.name
284
+ if hasattr(response, "description"):
285
+ self.description = response.description
286
+ if hasattr(response, "config"):
287
+ self.config = response.config
288
+ if hasattr(response, "transport"):
289
+ self.transport = response.transport
290
+
291
+ return self
292
+
293
+ def delete(self) -> None:
294
+ """Delete the MCP from the platform.
295
+
296
+ Raises:
297
+ ValueError: If the MCP has no ID.
298
+ RuntimeError: If client is not available.
299
+ """
300
+ if not self._id:
301
+ raise ValueError(_MCP_NOT_DEPLOYED_MSG)
302
+ if not self._client:
303
+ raise RuntimeError(_CLIENT_NOT_AVAILABLE_MSG)
304
+
305
+ self._client.mcps.delete(mcp_id=self._id)
306
+ self._id = None
307
+ self._client = None
308
+
309
+ @classmethod
310
+ def from_response(
311
+ cls,
312
+ response: MCPResponse,
313
+ client: Any = None,
314
+ ) -> MCP:
315
+ """Create an MCP instance from an API response.
316
+
317
+ This allows you to work with MCPs retrieved from the API
318
+ as full MCP instances with all methods available.
319
+
320
+ Args:
321
+ response: The MCPResponse from an API call.
322
+ client: The Glaip client instance for API operations.
323
+
324
+ Returns:
325
+ An MCP instance initialized from the response.
326
+
327
+ Example:
328
+ >>> response = client.mcps.get("mcp-123")
329
+ >>> mcp = MCP.from_response(response, client)
330
+ >>> tools = mcp.get_tools()
331
+ """
332
+ mcp = cls(
333
+ name=response.name,
334
+ id=response.id,
335
+ description=getattr(response, "description", None),
336
+ transport=getattr(response, "transport", None),
337
+ config=getattr(response, "config", None),
338
+ metadata=getattr(response, "metadata", None),
339
+ authentication=getattr(response, "authentication", None),
340
+ )
341
+
342
+ if client:
343
+ mcp._set_client(client)
344
+
345
+ return mcp