glaip-sdk 0.6.15b2__py3-none-any.whl → 0.6.16__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/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1196 -0
- glaip_sdk/cli/__init__.py +9 -0
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +78 -0
- glaip_sdk/cli/auth.py +699 -0
- glaip_sdk/cli/commands/__init__.py +5 -0
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +1509 -0
- glaip_sdk/cli/commands/common_config.py +104 -0
- glaip_sdk/cli/commands/configure.py +896 -0
- glaip_sdk/cli/commands/mcps.py +1356 -0
- glaip_sdk/cli/commands/models.py +69 -0
- glaip_sdk/cli/commands/tools.py +576 -0
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/commands/update.py +61 -0
- glaip_sdk/cli/config.py +95 -0
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +150 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +851 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +355 -0
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +112 -0
- glaip_sdk/cli/main.py +615 -0
- glaip_sdk/cli/masking.py +136 -0
- glaip_sdk/cli/mcp_validators.py +287 -0
- glaip_sdk/cli/pager.py +266 -0
- glaip_sdk/cli/parsers/__init__.py +7 -0
- glaip_sdk/cli/parsers/json_input.py +177 -0
- glaip_sdk/cli/resolution.py +67 -0
- glaip_sdk/cli/rich_helpers.py +27 -0
- glaip_sdk/cli/slash/__init__.py +15 -0
- 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 +285 -0
- glaip_sdk/cli/slash/prompt.py +256 -0
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +1708 -0
- glaip_sdk/cli/slash/tui/__init__.py +9 -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 +31 -0
- glaip_sdk/cli/transcript/cache.py +536 -0
- glaip_sdk/cli/transcript/capture.py +329 -0
- glaip_sdk/cli/transcript/export.py +38 -0
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +77 -0
- glaip_sdk/cli/transcript/viewer.py +374 -0
- glaip_sdk/cli/update_notifier.py +290 -0
- glaip_sdk/cli/utils.py +263 -0
- glaip_sdk/cli/validators.py +238 -0
- glaip_sdk/client/__init__.py +11 -0
- glaip_sdk/client/_agent_payloads.py +520 -0
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +1335 -0
- glaip_sdk/client/base.py +502 -0
- glaip_sdk/client/main.py +249 -0
- glaip_sdk/client/mcps.py +370 -0
- glaip_sdk/client/run_rendering.py +700 -0
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +661 -0
- glaip_sdk/client/validators.py +198 -0
- glaip_sdk/config/constants.py +52 -0
- 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 +7 -0
- glaip_sdk/payload_schemas/agent.py +85 -0
- 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/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +112 -0
- glaip_sdk/runner/langgraph.py +782 -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 +86 -0
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +194 -0
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +486 -0
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +135 -0
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +61 -0
- glaip_sdk/utils/import_export.py +168 -0
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -0
- glaip_sdk/utils/rendering/formatting.py +264 -0
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/layout/panels.py +156 -0
- glaip_sdk/utils/rendering/layout/progress.py +202 -0
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +85 -0
- glaip_sdk/utils/rendering/renderer/__init__.py +55 -0
- glaip_sdk/utils/rendering/renderer/base.py +1024 -0
- glaip_sdk/utils/rendering/renderer/config.py +27 -0
- glaip_sdk/utils/rendering/renderer/console.py +55 -0
- glaip_sdk/utils/rendering/renderer/debug.py +178 -0
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +202 -0
- 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 +182 -0
- 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 +100 -0
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/steps/event_processor.py +778 -0
- 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 +195 -0
- glaip_sdk/utils/run_renderer.py +41 -0
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +424 -0
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +264 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.16.dist-info}/METADATA +4 -5
- glaip_sdk-0.6.16.dist-info/RECORD +160 -0
- glaip_sdk-0.6.15b2.dist-info/RECORD +0 -12
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.16.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.16.dist-info}/entry_points.txt +0 -0
- {glaip_sdk-0.6.15b2.dist-info → glaip_sdk-0.6.16.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Validation utilities for AIP SDK.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from glaip_sdk.exceptions import AmbiguousResourceError, NotFoundError, ValidationError
|
|
11
|
+
from glaip_sdk.models import Tool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ResourceValidator:
|
|
15
|
+
"""Validates and resolves resource references."""
|
|
16
|
+
|
|
17
|
+
RESERVED_NAMES = {
|
|
18
|
+
"research-agent",
|
|
19
|
+
"github-agent",
|
|
20
|
+
"aws-pricing-filter-generator-agent",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def is_reserved_name(cls, name: str) -> bool:
|
|
25
|
+
"""Check if a name is reserved."""
|
|
26
|
+
return name in cls.RESERVED_NAMES
|
|
27
|
+
|
|
28
|
+
def _is_uuid_string(self, value: str) -> bool:
|
|
29
|
+
"""Check if a string is a valid UUID."""
|
|
30
|
+
try:
|
|
31
|
+
UUID(value)
|
|
32
|
+
return True
|
|
33
|
+
except ValueError:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
def _resolve_tool_by_name(self, tool_name: str, client: Any) -> str:
|
|
37
|
+
"""Resolve tool name to ID."""
|
|
38
|
+
found_tools = client.find_tools(name=tool_name)
|
|
39
|
+
if len(found_tools) == 1:
|
|
40
|
+
return str(found_tools[0].id)
|
|
41
|
+
elif len(found_tools) > 1:
|
|
42
|
+
raise AmbiguousResourceError(f"Multiple tools found with name '{tool_name}': {[t.id for t in found_tools]}")
|
|
43
|
+
else:
|
|
44
|
+
raise NotFoundError(f"Tool not found: {tool_name}")
|
|
45
|
+
|
|
46
|
+
def _resolve_tool_by_name_attribute(self, tool: Tool, client: Any) -> str:
|
|
47
|
+
"""Resolve tool by name attribute."""
|
|
48
|
+
found_tools = client.find_tools(name=tool.name)
|
|
49
|
+
if len(found_tools) == 1:
|
|
50
|
+
return str(found_tools[0].id)
|
|
51
|
+
elif len(found_tools) > 1:
|
|
52
|
+
raise AmbiguousResourceError(f"Multiple tools found with name '{tool.name}': {[t.id for t in found_tools]}")
|
|
53
|
+
else:
|
|
54
|
+
raise NotFoundError(f"Tool not found: {tool.name}")
|
|
55
|
+
|
|
56
|
+
def _process_tool_string(self, tool: str, client: Any) -> str:
|
|
57
|
+
"""Process a string tool reference."""
|
|
58
|
+
if self._is_uuid_string(tool):
|
|
59
|
+
return tool # Already a UUID string
|
|
60
|
+
else:
|
|
61
|
+
return self._resolve_tool_by_name(tool, client)
|
|
62
|
+
|
|
63
|
+
def _process_tool_object(self, tool: Tool, client: Any) -> str:
|
|
64
|
+
"""Process a Tool object reference."""
|
|
65
|
+
if hasattr(tool, "id") and tool.id is not None:
|
|
66
|
+
return str(tool.id)
|
|
67
|
+
elif isinstance(tool, UUID):
|
|
68
|
+
return str(tool)
|
|
69
|
+
elif hasattr(tool, "name") and tool.name is not None:
|
|
70
|
+
return self._resolve_tool_by_name_attribute(tool, client)
|
|
71
|
+
else:
|
|
72
|
+
raise ValidationError(f"Invalid tool reference: {tool} - must have 'id' or 'name' attribute")
|
|
73
|
+
|
|
74
|
+
def _process_single_tool(self, tool: str | Tool, client: Any) -> str:
|
|
75
|
+
"""Process a single tool reference and return its ID."""
|
|
76
|
+
if isinstance(tool, str):
|
|
77
|
+
return self._process_tool_string(tool, client)
|
|
78
|
+
else:
|
|
79
|
+
return self._process_tool_object(tool, client)
|
|
80
|
+
|
|
81
|
+
@classmethod
|
|
82
|
+
def extract_tool_ids(cls, tools: list[str | Tool], client: Any) -> list[str]:
|
|
83
|
+
"""Extract tool IDs from a list of tool names, IDs, or Tool objects.
|
|
84
|
+
|
|
85
|
+
For agent creation, the backend expects tool IDs (UUIDs).
|
|
86
|
+
This method handles:
|
|
87
|
+
- Tool objects (extracts their ID)
|
|
88
|
+
- UUID strings (passes through)
|
|
89
|
+
- Tool names (finds tool and extracts ID)
|
|
90
|
+
"""
|
|
91
|
+
tool_ids = []
|
|
92
|
+
for tool in tools:
|
|
93
|
+
try:
|
|
94
|
+
tool_id = cls()._process_single_tool(tool, client)
|
|
95
|
+
tool_ids.append(tool_id)
|
|
96
|
+
except (AmbiguousResourceError, NotFoundError) as err:
|
|
97
|
+
# Determine the tool name for the error message
|
|
98
|
+
tool_name = tool if isinstance(tool, str) else getattr(tool, "name", str(tool))
|
|
99
|
+
raise ValidationError(f"Failed to resolve tool name '{tool_name}' to ID: {err}") from err
|
|
100
|
+
except Exception as err:
|
|
101
|
+
# For other exceptions, wrap them appropriately
|
|
102
|
+
tool_name = tool if isinstance(tool, str) else getattr(tool, "name", str(tool))
|
|
103
|
+
raise ValidationError(f"Failed to resolve tool name '{tool_name}' to ID: {err}") from err
|
|
104
|
+
|
|
105
|
+
return tool_ids
|
|
106
|
+
|
|
107
|
+
def _resolve_agent_by_name(self, agent_name: str, client: Any) -> str:
|
|
108
|
+
"""Resolve agent name to ID."""
|
|
109
|
+
found_agents = client.find_agents(name=agent_name)
|
|
110
|
+
if len(found_agents) == 1:
|
|
111
|
+
return str(found_agents[0].id)
|
|
112
|
+
elif len(found_agents) > 1:
|
|
113
|
+
raise AmbiguousResourceError(
|
|
114
|
+
f"Multiple agents found with name '{agent_name}': {[a.id for a in found_agents]}"
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
raise NotFoundError(f"Agent not found: {agent_name}")
|
|
118
|
+
|
|
119
|
+
def _resolve_agent_by_name_attribute(self, agent: Any, client: Any) -> str:
|
|
120
|
+
"""Resolve agent by name attribute."""
|
|
121
|
+
found_agents = client.find_agents(name=agent.name)
|
|
122
|
+
if len(found_agents) == 1:
|
|
123
|
+
return str(found_agents[0].id)
|
|
124
|
+
elif len(found_agents) > 1:
|
|
125
|
+
raise AmbiguousResourceError(
|
|
126
|
+
f"Multiple agents found with name '{agent.name}': {[a.id for a in found_agents]}"
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
raise NotFoundError(f"Agent not found: {agent.name}")
|
|
130
|
+
|
|
131
|
+
def _process_agent_string(self, agent: str, client: Any) -> str:
|
|
132
|
+
"""Process a string agent reference."""
|
|
133
|
+
if self._is_uuid_string(agent):
|
|
134
|
+
return agent # Already a UUID string
|
|
135
|
+
else:
|
|
136
|
+
return self._resolve_agent_by_name(agent, client)
|
|
137
|
+
|
|
138
|
+
def _process_agent_object(self, agent: Any, client: Any) -> str:
|
|
139
|
+
"""Process an Agent object reference."""
|
|
140
|
+
if hasattr(agent, "id") and agent.id is not None:
|
|
141
|
+
return str(agent.id)
|
|
142
|
+
elif isinstance(agent, UUID):
|
|
143
|
+
return str(agent)
|
|
144
|
+
elif hasattr(agent, "name") and agent.name is not None:
|
|
145
|
+
return self._resolve_agent_by_name_attribute(agent, client)
|
|
146
|
+
else:
|
|
147
|
+
raise ValidationError(f"Invalid agent reference: {agent} - must have 'id' or 'name' attribute")
|
|
148
|
+
|
|
149
|
+
def _process_single_agent(self, agent: str | Any, client: Any) -> str:
|
|
150
|
+
"""Process a single agent reference and return its ID."""
|
|
151
|
+
if isinstance(agent, str):
|
|
152
|
+
return self._process_agent_string(agent, client)
|
|
153
|
+
else:
|
|
154
|
+
return self._process_agent_object(agent, client)
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def extract_agent_ids(cls, agents: list[str | Any], client: Any) -> list[str]:
|
|
158
|
+
"""Extract agent IDs from a list of agent names, IDs, or agent objects.
|
|
159
|
+
|
|
160
|
+
For agent creation, the backend expects agent IDs (UUIDs).
|
|
161
|
+
This method handles:
|
|
162
|
+
- Agent objects (extracts their ID)
|
|
163
|
+
- UUID strings (passes through)
|
|
164
|
+
- Agent names (finds agent and extracts ID)
|
|
165
|
+
"""
|
|
166
|
+
agent_ids = []
|
|
167
|
+
for agent in agents:
|
|
168
|
+
try:
|
|
169
|
+
agent_id = cls()._process_single_agent(agent, client)
|
|
170
|
+
agent_ids.append(agent_id)
|
|
171
|
+
except (AmbiguousResourceError, NotFoundError) as err:
|
|
172
|
+
# Determine the agent name for the error message
|
|
173
|
+
agent_name = agent if isinstance(agent, str) else getattr(agent, "name", str(agent))
|
|
174
|
+
raise ValidationError(f"Failed to resolve agent name '{agent_name}' to ID: {err}") from err
|
|
175
|
+
except Exception as err:
|
|
176
|
+
# For other exceptions, wrap them appropriately
|
|
177
|
+
agent_name = agent if isinstance(agent, str) else getattr(agent, "name", str(agent))
|
|
178
|
+
raise ValidationError(f"Failed to resolve agent name '{agent_name}' to ID: {err}") from err
|
|
179
|
+
|
|
180
|
+
return agent_ids
|
|
181
|
+
|
|
182
|
+
@classmethod
|
|
183
|
+
def validate_tools_exist(cls, tool_ids: list[str], client: Any) -> None:
|
|
184
|
+
"""Validate that all tool IDs exist."""
|
|
185
|
+
for tool_id in tool_ids:
|
|
186
|
+
try:
|
|
187
|
+
client.get_tool_by_id(tool_id)
|
|
188
|
+
except NotFoundError as err:
|
|
189
|
+
raise ValidationError(f"Tool not found: {tool_id}") from err
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def validate_agents_exist(cls, agent_ids: list[str], client: Any) -> None:
|
|
193
|
+
"""Validate that all agent IDs exist."""
|
|
194
|
+
for agent_id in agent_ids:
|
|
195
|
+
try:
|
|
196
|
+
client.get_agent_by_id(agent_id)
|
|
197
|
+
except NotFoundError as err:
|
|
198
|
+
raise ValidationError(f"Agent not found: {agent_id}") from err
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Configuration constants for the AIP SDK.
|
|
2
|
+
|
|
3
|
+
Authors:
|
|
4
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Default language model configuration
|
|
8
|
+
DEFAULT_MODEL = "gpt-5-nano"
|
|
9
|
+
DEFAULT_AGENT_RUN_TIMEOUT = 300
|
|
10
|
+
|
|
11
|
+
# User agent and version
|
|
12
|
+
SDK_NAME = "glaip-sdk"
|
|
13
|
+
|
|
14
|
+
# Reserved names that cannot be used for agents/tools
|
|
15
|
+
RESERVED_NAMES = {
|
|
16
|
+
"system",
|
|
17
|
+
"admin",
|
|
18
|
+
"root",
|
|
19
|
+
"test",
|
|
20
|
+
"example",
|
|
21
|
+
"demo",
|
|
22
|
+
"sample",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Agent creation/update constants
|
|
26
|
+
DEFAULT_AGENT_TYPE = "config"
|
|
27
|
+
DEFAULT_AGENT_FRAMEWORK = "langchain"
|
|
28
|
+
DEFAULT_AGENT_VERSION = "1.0"
|
|
29
|
+
DEFAULT_AGENT_PROVIDER = "openai"
|
|
30
|
+
|
|
31
|
+
# Tool creation/update constants
|
|
32
|
+
DEFAULT_TOOL_TYPE = "custom"
|
|
33
|
+
DEFAULT_TOOL_FRAMEWORK = "langchain"
|
|
34
|
+
DEFAULT_TOOL_VERSION = "1.0"
|
|
35
|
+
|
|
36
|
+
# MCP creation/update constants
|
|
37
|
+
DEFAULT_MCP_TYPE = "server"
|
|
38
|
+
DEFAULT_MCP_TRANSPORT = "stdio"
|
|
39
|
+
|
|
40
|
+
# Default error messages
|
|
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
|