glaip-sdk 0.0.4__py3-none-any.whl → 0.0.5__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 -5
- glaip_sdk/branding.py +18 -17
- glaip_sdk/cli/__init__.py +1 -1
- glaip_sdk/cli/agent_config.py +82 -0
- glaip_sdk/cli/commands/__init__.py +3 -3
- glaip_sdk/cli/commands/agents.py +570 -673
- glaip_sdk/cli/commands/configure.py +2 -2
- glaip_sdk/cli/commands/mcps.py +148 -143
- glaip_sdk/cli/commands/models.py +1 -1
- glaip_sdk/cli/commands/tools.py +250 -179
- glaip_sdk/cli/display.py +244 -0
- glaip_sdk/cli/io.py +106 -0
- glaip_sdk/cli/main.py +14 -18
- glaip_sdk/cli/resolution.py +59 -0
- glaip_sdk/cli/utils.py +305 -264
- glaip_sdk/cli/validators.py +235 -0
- glaip_sdk/client/__init__.py +3 -224
- glaip_sdk/client/agents.py +631 -191
- glaip_sdk/client/base.py +66 -4
- glaip_sdk/client/main.py +226 -0
- glaip_sdk/client/mcps.py +143 -18
- glaip_sdk/client/tools.py +146 -11
- glaip_sdk/config/constants.py +10 -1
- glaip_sdk/models.py +42 -2
- glaip_sdk/rich_components.py +29 -0
- glaip_sdk/utils/__init__.py +18 -171
- glaip_sdk/utils/agent_config.py +181 -0
- glaip_sdk/utils/client_utils.py +159 -79
- glaip_sdk/utils/display.py +100 -0
- glaip_sdk/utils/general.py +94 -0
- glaip_sdk/utils/import_export.py +140 -0
- glaip_sdk/utils/rendering/formatting.py +6 -1
- glaip_sdk/utils/rendering/renderer/__init__.py +67 -8
- glaip_sdk/utils/rendering/renderer/base.py +340 -247
- glaip_sdk/utils/rendering/renderer/debug.py +3 -2
- glaip_sdk/utils/rendering/renderer/panels.py +11 -10
- glaip_sdk/utils/rendering/steps.py +1 -1
- glaip_sdk/utils/resource_refs.py +192 -0
- glaip_sdk/utils/rich_utils.py +29 -0
- glaip_sdk/utils/serialization.py +285 -0
- glaip_sdk/utils/validation.py +273 -0
- {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5.dist-info}/METADATA +6 -5
- glaip_sdk-0.0.5.dist-info/RECORD +55 -0
- glaip_sdk/cli/commands/init.py +0 -93
- glaip_sdk-0.0.4.dist-info/RECORD +0 -41
- {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.4.dist-info → glaip_sdk-0.0.5.dist-info}/entry_points.txt +0 -0
glaip_sdk/client/base.py
CHANGED
|
@@ -12,6 +12,7 @@ from typing import Any, Union
|
|
|
12
12
|
import httpx
|
|
13
13
|
from dotenv import load_dotenv
|
|
14
14
|
|
|
15
|
+
import glaip_sdk
|
|
15
16
|
from glaip_sdk.config.constants import SDK_NAME, SDK_VERSION
|
|
16
17
|
from glaip_sdk.exceptions import (
|
|
17
18
|
AuthenticationError,
|
|
@@ -54,6 +55,7 @@ class BaseClient:
|
|
|
54
55
|
load_env: Whether to load environment variables
|
|
55
56
|
"""
|
|
56
57
|
self._parent_client = parent_client
|
|
58
|
+
self._session_scoped = False # Mark as not session-scoped by default
|
|
57
59
|
|
|
58
60
|
if parent_client is not None:
|
|
59
61
|
# Adopt parent's session/config; DO NOT call super().__init__
|
|
@@ -64,8 +66,11 @@ class BaseClient:
|
|
|
64
66
|
self.http_client = parent_client.http_client
|
|
65
67
|
else:
|
|
66
68
|
# Initialize as standalone client
|
|
67
|
-
if load_env:
|
|
68
|
-
|
|
69
|
+
if load_env and not (api_url and api_key):
|
|
70
|
+
# Only load .env file if explicit credentials not provided
|
|
71
|
+
package_dir = os.path.dirname(glaip_sdk.__file__)
|
|
72
|
+
env_file = os.path.join(package_dir, ".env")
|
|
73
|
+
load_dotenv(env_file)
|
|
69
74
|
|
|
70
75
|
self.api_url = api_url or os.getenv("AIP_API_URL")
|
|
71
76
|
self.api_key = api_key or os.getenv("AIP_API_KEY")
|
|
@@ -105,6 +110,37 @@ class BaseClient:
|
|
|
105
110
|
limits=httpx.Limits(max_keepalive_connections=10, max_connections=100),
|
|
106
111
|
)
|
|
107
112
|
|
|
113
|
+
def _build_async_client(self, timeout: float) -> dict[str, Any]:
|
|
114
|
+
"""Build async client configuration (returns dict of kwargs for httpx.AsyncClient).
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
timeout: Request timeout in seconds
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Dictionary of kwargs for httpx.AsyncClient
|
|
121
|
+
"""
|
|
122
|
+
# For streaming operations, we need more generous read timeouts
|
|
123
|
+
# while keeping reasonable connect timeouts
|
|
124
|
+
timeout_config = httpx.Timeout(
|
|
125
|
+
timeout=timeout, # Total timeout
|
|
126
|
+
connect=min(30.0, timeout), # Connect timeout (max 30s)
|
|
127
|
+
read=timeout, # Read timeout (same as total for streaming)
|
|
128
|
+
write=min(30.0, timeout), # Write timeout (max 30s)
|
|
129
|
+
pool=timeout, # Pool timeout (same as total)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"base_url": self.api_url,
|
|
134
|
+
"headers": {
|
|
135
|
+
"X-API-Key": self.api_key,
|
|
136
|
+
"User-Agent": f"{SDK_NAME}/{SDK_VERSION}",
|
|
137
|
+
},
|
|
138
|
+
"timeout": timeout_config,
|
|
139
|
+
"follow_redirects": True,
|
|
140
|
+
"http2": False,
|
|
141
|
+
"limits": httpx.Limits(max_keepalive_connections=10, max_connections=100),
|
|
142
|
+
}
|
|
143
|
+
|
|
108
144
|
@property
|
|
109
145
|
def timeout(self) -> float:
|
|
110
146
|
"""Get current timeout value."""
|
|
@@ -117,6 +153,7 @@ class BaseClient:
|
|
|
117
153
|
if (
|
|
118
154
|
hasattr(self, "http_client")
|
|
119
155
|
and self.http_client
|
|
156
|
+
and not self._session_scoped
|
|
120
157
|
and not self._parent_client
|
|
121
158
|
):
|
|
122
159
|
self.http_client.close()
|
|
@@ -173,8 +210,30 @@ class BaseClient:
|
|
|
173
210
|
get_endpoint = get_endpoint_fmt.format(id=resource_id)
|
|
174
211
|
return self._request("GET", get_endpoint)
|
|
175
212
|
|
|
213
|
+
def _ensure_client_alive(self):
|
|
214
|
+
"""Ensure HTTP client is alive, recreate if needed."""
|
|
215
|
+
if not hasattr(self, "http_client") or self.http_client is None:
|
|
216
|
+
if not self._parent_client:
|
|
217
|
+
self.http_client = self._build_client(self._timeout)
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
# Check if client is closed by attempting a simple operation
|
|
221
|
+
try:
|
|
222
|
+
# Try to access a property that would fail if closed
|
|
223
|
+
_ = self.http_client.headers
|
|
224
|
+
except (RuntimeError, AttributeError) as e:
|
|
225
|
+
if "closed" in str(e).lower() or "NoneType" in str(e):
|
|
226
|
+
client_log.debug("HTTP client was closed, recreating")
|
|
227
|
+
if not self._parent_client:
|
|
228
|
+
self.http_client = self._build_client(self._timeout)
|
|
229
|
+
else:
|
|
230
|
+
raise
|
|
231
|
+
|
|
176
232
|
def _request(self, method: str, endpoint: str, **kwargs) -> Any:
|
|
177
233
|
"""Make HTTP request with error handling."""
|
|
234
|
+
# Ensure client is alive before making request
|
|
235
|
+
self._ensure_client_alive()
|
|
236
|
+
|
|
178
237
|
client_log.debug(f"Making {method} request to {endpoint}")
|
|
179
238
|
try:
|
|
180
239
|
response = self.http_client.request(method, endpoint, **kwargs)
|
|
@@ -270,6 +329,7 @@ class BaseClient:
|
|
|
270
329
|
if (
|
|
271
330
|
hasattr(self, "http_client")
|
|
272
331
|
and self.http_client
|
|
332
|
+
and not self._session_scoped
|
|
273
333
|
and not self._parent_client
|
|
274
334
|
):
|
|
275
335
|
self.http_client.close()
|
|
@@ -278,6 +338,8 @@ class BaseClient:
|
|
|
278
338
|
"""Context manager entry."""
|
|
279
339
|
return self
|
|
280
340
|
|
|
281
|
-
def __exit__(self,
|
|
341
|
+
def __exit__(self, _exc_type, _exc_val, _exc_tb):
|
|
282
342
|
"""Context manager exit."""
|
|
283
|
-
|
|
343
|
+
# Only close if this is not session-scoped
|
|
344
|
+
if not self._session_scoped:
|
|
345
|
+
self.close()
|
glaip_sdk/client/main.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Main client for AIP SDK.
|
|
3
|
+
|
|
4
|
+
Authors:
|
|
5
|
+
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from glaip_sdk.client.agents import AgentClient
|
|
11
|
+
from glaip_sdk.client.base import BaseClient
|
|
12
|
+
from glaip_sdk.client.mcps import MCPClient
|
|
13
|
+
from glaip_sdk.client.tools import ToolClient
|
|
14
|
+
from glaip_sdk.models import MCP, Agent, Tool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Client(BaseClient):
|
|
18
|
+
"""Main client that composes all specialized clients and shares one HTTP session."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, **kwargs):
|
|
21
|
+
super().__init__(**kwargs)
|
|
22
|
+
# Share the single httpx.Client + config with sub-clients
|
|
23
|
+
shared_config = {
|
|
24
|
+
"parent_client": self,
|
|
25
|
+
"api_url": self.api_url,
|
|
26
|
+
"api_key": self.api_key,
|
|
27
|
+
"timeout": self._timeout,
|
|
28
|
+
}
|
|
29
|
+
self.agents = AgentClient(**shared_config)
|
|
30
|
+
self.tools = ToolClient(**shared_config)
|
|
31
|
+
self.mcps = MCPClient(**shared_config)
|
|
32
|
+
|
|
33
|
+
# ---- Core API Methods (Public Interface) ----
|
|
34
|
+
|
|
35
|
+
# Agents
|
|
36
|
+
def create_agent(self, **kwargs) -> Agent:
|
|
37
|
+
"""Create a new agent."""
|
|
38
|
+
return self.agents.create_agent(**kwargs)
|
|
39
|
+
|
|
40
|
+
def list_agents(
|
|
41
|
+
self,
|
|
42
|
+
agent_type: str | None = None,
|
|
43
|
+
framework: str | None = None,
|
|
44
|
+
name: str | None = None,
|
|
45
|
+
version: str | None = None,
|
|
46
|
+
sync_langflow_agents: bool = False,
|
|
47
|
+
) -> list[Agent]:
|
|
48
|
+
"""List agents with optional filtering.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
agent_type: Filter by agent type (config, code, a2a)
|
|
52
|
+
framework: Filter by framework (langchain, langgraph, google_adk)
|
|
53
|
+
name: Filter by partial name match (case-insensitive)
|
|
54
|
+
version: Filter by exact version match
|
|
55
|
+
sync_langflow_agents: Sync with LangFlow server before listing (only applies when agent_type=langflow)
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of agents matching the filters
|
|
59
|
+
"""
|
|
60
|
+
return self.agents.list_agents(
|
|
61
|
+
agent_type=agent_type,
|
|
62
|
+
framework=framework,
|
|
63
|
+
name=name,
|
|
64
|
+
version=version,
|
|
65
|
+
sync_langflow_agents=sync_langflow_agents,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def get_agent_by_id(self, agent_id: str) -> Agent | None:
|
|
69
|
+
"""Get agent by ID."""
|
|
70
|
+
return self.agents.get_agent_by_id(agent_id)
|
|
71
|
+
|
|
72
|
+
def get_agent(self, agent_id: str) -> Agent | None:
|
|
73
|
+
"""Get agent by ID (alias for get_agent_by_id)."""
|
|
74
|
+
return self.get_agent_by_id(agent_id)
|
|
75
|
+
|
|
76
|
+
def find_agents(self, name: str | None = None) -> list[Agent]:
|
|
77
|
+
"""Find agents by name."""
|
|
78
|
+
return self.agents.find_agents(name)
|
|
79
|
+
|
|
80
|
+
def update_agent(self, agent_id: str, **kwargs) -> Agent:
|
|
81
|
+
"""Update an existing agent."""
|
|
82
|
+
return self.agents.update_agent(agent_id, **kwargs)
|
|
83
|
+
|
|
84
|
+
def delete_agent(self, agent_id: str) -> bool:
|
|
85
|
+
"""Delete an agent."""
|
|
86
|
+
return self.agents.delete_agent(agent_id)
|
|
87
|
+
|
|
88
|
+
def run_agent(self, agent_id: str, message: str, **kwargs) -> str:
|
|
89
|
+
"""Run an agent with a message."""
|
|
90
|
+
return self.agents.run_agent(agent_id, message, **kwargs)
|
|
91
|
+
|
|
92
|
+
def sync_langflow_agents(
|
|
93
|
+
self,
|
|
94
|
+
base_url: str | None = None,
|
|
95
|
+
api_key: str | None = None,
|
|
96
|
+
) -> dict[str, Any]:
|
|
97
|
+
"""Sync LangFlow agents by fetching flows from the LangFlow server.
|
|
98
|
+
|
|
99
|
+
This method synchronizes agents with LangFlow flows. It fetches all flows
|
|
100
|
+
from the configured LangFlow server and creates/updates corresponding agents.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
base_url: Custom LangFlow server base URL. If not provided, uses LANGFLOW_BASE_URL env var.
|
|
104
|
+
api_key: Custom LangFlow API key. If not provided, uses LANGFLOW_API_KEY env var.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Response containing sync results and statistics
|
|
108
|
+
"""
|
|
109
|
+
return self.agents.sync_langflow_agents(base_url=base_url, api_key=api_key)
|
|
110
|
+
|
|
111
|
+
# Tools
|
|
112
|
+
def create_tool(self, **kwargs) -> Tool:
|
|
113
|
+
"""Create a new tool."""
|
|
114
|
+
return self.tools.create_tool(**kwargs)
|
|
115
|
+
|
|
116
|
+
def create_tool_from_code(self, **kwargs) -> Tool:
|
|
117
|
+
"""Create a new tool from code."""
|
|
118
|
+
return self.tools.create_tool_from_code(**kwargs)
|
|
119
|
+
|
|
120
|
+
def list_tools(self, tool_type: str | None = None) -> list[Tool]:
|
|
121
|
+
"""List tools with optional type filtering."""
|
|
122
|
+
return self.tools.list_tools(tool_type=tool_type)
|
|
123
|
+
|
|
124
|
+
def get_tool_by_id(self, tool_id: str) -> Tool | None:
|
|
125
|
+
"""Get tool by ID."""
|
|
126
|
+
return self.tools.get_tool_by_id(tool_id)
|
|
127
|
+
|
|
128
|
+
def get_tool(self, tool_id: str) -> Tool | None:
|
|
129
|
+
"""Backward-compatible alias for get_tool_by_id."""
|
|
130
|
+
return self.get_tool_by_id(tool_id)
|
|
131
|
+
|
|
132
|
+
def find_tools(self, name: str) -> list[Tool]:
|
|
133
|
+
"""Find tools by name."""
|
|
134
|
+
return self.tools.find_tools(name)
|
|
135
|
+
|
|
136
|
+
def update_tool(self, tool_id: str, **kwargs) -> Tool:
|
|
137
|
+
"""Update an existing tool."""
|
|
138
|
+
return self.tools.update_tool(tool_id, **kwargs)
|
|
139
|
+
|
|
140
|
+
def delete_tool(self, tool_id: str) -> bool:
|
|
141
|
+
"""Delete a tool."""
|
|
142
|
+
return self.tools.delete_tool(tool_id)
|
|
143
|
+
|
|
144
|
+
def get_tool_script(self, tool_id: str) -> str:
|
|
145
|
+
"""Get tool script content."""
|
|
146
|
+
return self.tools.get_tool_script(tool_id)
|
|
147
|
+
|
|
148
|
+
def update_tool_via_file(self, tool_id: str, file_path: str) -> Tool:
|
|
149
|
+
"""Update tool via file."""
|
|
150
|
+
return self.tools.update_tool_via_file(tool_id, file_path)
|
|
151
|
+
|
|
152
|
+
# MCPs
|
|
153
|
+
def create_mcp(self, **kwargs) -> MCP:
|
|
154
|
+
"""Create a new MCP."""
|
|
155
|
+
return self.mcps.create_mcp(**kwargs)
|
|
156
|
+
|
|
157
|
+
def list_mcps(self) -> list[MCP]:
|
|
158
|
+
"""List all MCPs."""
|
|
159
|
+
return self.mcps.list_mcps()
|
|
160
|
+
|
|
161
|
+
def get_mcp_by_id(self, mcp_id: str) -> MCP | None:
|
|
162
|
+
"""Get MCP by ID."""
|
|
163
|
+
return self.mcps.get_mcp_by_id(mcp_id)
|
|
164
|
+
|
|
165
|
+
def get_mcp(self, mcp_id: str) -> MCP | None:
|
|
166
|
+
"""Backward-compatible alias for get_mcp_by_id."""
|
|
167
|
+
return self.get_mcp_by_id(mcp_id)
|
|
168
|
+
|
|
169
|
+
def find_mcps(self, name: str) -> list[MCP]:
|
|
170
|
+
"""Find MCPs by name."""
|
|
171
|
+
return self.mcps.find_mcps(name)
|
|
172
|
+
|
|
173
|
+
def update_mcp(self, mcp_id: str, **kwargs) -> MCP:
|
|
174
|
+
"""Update an existing MCP."""
|
|
175
|
+
return self.mcps.update_mcp(mcp_id, **kwargs)
|
|
176
|
+
|
|
177
|
+
def delete_mcp(self, mcp_id: str) -> bool:
|
|
178
|
+
"""Delete an MCP."""
|
|
179
|
+
return self.mcps.delete_mcp(mcp_id)
|
|
180
|
+
|
|
181
|
+
def test_mcp_connection(self, config: dict) -> dict:
|
|
182
|
+
"""Test MCP connection."""
|
|
183
|
+
return self.mcps.test_mcp_connection(config)
|
|
184
|
+
|
|
185
|
+
def test_mcp_connection_from_config(self, config: dict) -> dict:
|
|
186
|
+
"""Test MCP connection from config."""
|
|
187
|
+
return self.mcps.test_mcp_connection_from_config(config)
|
|
188
|
+
|
|
189
|
+
def get_mcp_tools_from_config(self, config: dict) -> list[dict]:
|
|
190
|
+
"""Get MCP tools from config."""
|
|
191
|
+
return self.mcps.get_mcp_tools_from_config(config)
|
|
192
|
+
|
|
193
|
+
# Language Models
|
|
194
|
+
def list_language_models(self) -> list[dict]:
|
|
195
|
+
"""List available language models."""
|
|
196
|
+
data = self._request("GET", "/language-models")
|
|
197
|
+
return data or []
|
|
198
|
+
|
|
199
|
+
# ---- Timeout propagation ----
|
|
200
|
+
@property
|
|
201
|
+
def timeout(self) -> float: # type: ignore[override]
|
|
202
|
+
return super().timeout
|
|
203
|
+
|
|
204
|
+
@timeout.setter
|
|
205
|
+
def timeout(self, value: float) -> None: # type: ignore[override]
|
|
206
|
+
# Rebuild the root http client
|
|
207
|
+
BaseClient.timeout.fset(self, value) # call parent setter
|
|
208
|
+
# Propagate the new session to sub-clients so they don't hold a closed client
|
|
209
|
+
try:
|
|
210
|
+
if hasattr(self, "agents"):
|
|
211
|
+
self.agents.http_client = self.http_client
|
|
212
|
+
if hasattr(self, "tools"):
|
|
213
|
+
self.tools.http_client = self.http_client
|
|
214
|
+
if hasattr(self, "mcps"):
|
|
215
|
+
self.mcps.http_client = self.http_client
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
# ---- Health Check ----
|
|
220
|
+
def ping(self) -> bool:
|
|
221
|
+
"""Check if the API is reachable."""
|
|
222
|
+
try:
|
|
223
|
+
self._request("GET", "/health-check")
|
|
224
|
+
return True
|
|
225
|
+
except Exception:
|
|
226
|
+
return False
|
glaip_sdk/client/mcps.py
CHANGED
|
@@ -9,9 +9,18 @@ import logging
|
|
|
9
9
|
from typing import Any
|
|
10
10
|
|
|
11
11
|
from glaip_sdk.client.base import BaseClient
|
|
12
|
+
from glaip_sdk.config.constants import (
|
|
13
|
+
DEFAULT_MCP_TRANSPORT,
|
|
14
|
+
DEFAULT_MCP_TYPE,
|
|
15
|
+
)
|
|
12
16
|
from glaip_sdk.models import MCP
|
|
13
17
|
from glaip_sdk.utils.client_utils import create_model_instances, find_by_name
|
|
14
18
|
|
|
19
|
+
# API endpoints
|
|
20
|
+
MCPS_ENDPOINT = "/mcps/"
|
|
21
|
+
MCPS_CONNECT_ENDPOINT = "/mcps/connect"
|
|
22
|
+
MCPS_CONNECT_TOOLS_ENDPOINT = "/mcps/connect/tools"
|
|
23
|
+
|
|
15
24
|
# Set up module-level logger
|
|
16
25
|
logger = logging.getLogger("glaip_sdk.mcps")
|
|
17
26
|
|
|
@@ -30,18 +39,18 @@ class MCPClient(BaseClient):
|
|
|
30
39
|
|
|
31
40
|
def list_mcps(self) -> list[MCP]:
|
|
32
41
|
"""List all MCPs."""
|
|
33
|
-
data = self._request("GET",
|
|
42
|
+
data = self._request("GET", MCPS_ENDPOINT)
|
|
34
43
|
return create_model_instances(data, MCP, self)
|
|
35
44
|
|
|
36
45
|
def get_mcp_by_id(self, mcp_id: str) -> MCP:
|
|
37
46
|
"""Get MCP by ID."""
|
|
38
|
-
data = self._request("GET", f"
|
|
47
|
+
data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}")
|
|
39
48
|
return MCP(**data)._set_client(self)
|
|
40
49
|
|
|
41
50
|
def find_mcps(self, name: str | None = None) -> list[MCP]:
|
|
42
51
|
"""Find MCPs by name."""
|
|
43
52
|
# Backend doesn't support name query parameter, so we fetch all and filter client-side
|
|
44
|
-
data = self._request("GET",
|
|
53
|
+
data = self._request("GET", MCPS_ENDPOINT)
|
|
45
54
|
mcps = create_model_instances(data, MCP, self)
|
|
46
55
|
return find_by_name(mcps, name, case_sensitive=False)
|
|
47
56
|
|
|
@@ -53,36 +62,152 @@ class MCPClient(BaseClient):
|
|
|
53
62
|
**kwargs,
|
|
54
63
|
) -> MCP:
|
|
55
64
|
"""Create a new MCP."""
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
# Use the helper method to build a properly structured payload
|
|
66
|
+
payload = self._build_create_payload(
|
|
67
|
+
name=name,
|
|
68
|
+
description=description,
|
|
69
|
+
config=config,
|
|
59
70
|
**kwargs,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if config:
|
|
63
|
-
payload["config"] = config
|
|
71
|
+
)
|
|
64
72
|
|
|
65
73
|
# Create the MCP and fetch full details
|
|
66
74
|
full_mcp_data = self._post_then_fetch(
|
|
67
75
|
id_key="id",
|
|
68
|
-
post_endpoint=
|
|
69
|
-
get_endpoint_fmt="
|
|
76
|
+
post_endpoint=MCPS_ENDPOINT,
|
|
77
|
+
get_endpoint_fmt=f"{MCPS_ENDPOINT}{{id}}",
|
|
70
78
|
json=payload,
|
|
71
79
|
)
|
|
72
80
|
return MCP(**full_mcp_data)._set_client(self)
|
|
73
81
|
|
|
74
82
|
def update_mcp(self, mcp_id: str, **kwargs) -> MCP:
|
|
75
|
-
"""Update an existing MCP.
|
|
76
|
-
|
|
83
|
+
"""Update an existing MCP.
|
|
84
|
+
|
|
85
|
+
Automatically chooses between PUT (full update) and PATCH (partial update)
|
|
86
|
+
based on the provided fields:
|
|
87
|
+
- Uses PUT if name, config, and transport are all provided (full update)
|
|
88
|
+
- Uses PATCH otherwise (partial update)
|
|
89
|
+
"""
|
|
90
|
+
# Check if all required fields for full update are provided
|
|
91
|
+
required_fields = {"name", "config", "transport"}
|
|
92
|
+
provided_fields = set(kwargs.keys())
|
|
93
|
+
|
|
94
|
+
if required_fields.issubset(provided_fields):
|
|
95
|
+
# All required fields provided - use full update (PUT)
|
|
96
|
+
method = "PUT"
|
|
97
|
+
else:
|
|
98
|
+
# Partial update - use PATCH
|
|
99
|
+
method = "PATCH"
|
|
100
|
+
|
|
101
|
+
data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id}", json=kwargs)
|
|
77
102
|
return MCP(**data)._set_client(self)
|
|
78
103
|
|
|
79
104
|
def delete_mcp(self, mcp_id: str) -> None:
|
|
80
105
|
"""Delete an MCP."""
|
|
81
|
-
self._request("DELETE", f"
|
|
106
|
+
self._request("DELETE", f"{MCPS_ENDPOINT}{mcp_id}")
|
|
107
|
+
|
|
108
|
+
def _build_create_payload(
|
|
109
|
+
self,
|
|
110
|
+
name: str,
|
|
111
|
+
description: str,
|
|
112
|
+
transport: str = DEFAULT_MCP_TRANSPORT,
|
|
113
|
+
config: dict[str, Any] | None = None,
|
|
114
|
+
**kwargs,
|
|
115
|
+
) -> dict[str, Any]:
|
|
116
|
+
"""Build payload for MCP creation with proper metadata handling.
|
|
117
|
+
|
|
118
|
+
CENTRALIZED PAYLOAD BUILDING LOGIC:
|
|
119
|
+
- Sets proper defaults and required fields
|
|
120
|
+
- Handles config serialization consistently
|
|
121
|
+
- Processes transport and other metadata properly
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
name: MCP name
|
|
125
|
+
description: MCP description
|
|
126
|
+
transport: MCP transport protocol (defaults to stdio)
|
|
127
|
+
config: MCP configuration dictionary
|
|
128
|
+
**kwargs: Additional parameters
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Complete payload dictionary for MCP creation
|
|
132
|
+
"""
|
|
133
|
+
# Prepare the creation payload with required fields
|
|
134
|
+
payload: dict[str, Any] = {
|
|
135
|
+
"name": name.strip(),
|
|
136
|
+
"type": DEFAULT_MCP_TYPE, # MCPs are always server type
|
|
137
|
+
"transport": transport,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# Add description if provided
|
|
141
|
+
if description:
|
|
142
|
+
payload["description"] = description.strip()
|
|
143
|
+
|
|
144
|
+
# Handle config - ensure it's properly serialized
|
|
145
|
+
if config:
|
|
146
|
+
payload["config"] = config
|
|
147
|
+
|
|
148
|
+
# Add any other kwargs (excluding already handled ones)
|
|
149
|
+
excluded_keys = {"type"} # type is handled above
|
|
150
|
+
for key, value in kwargs.items():
|
|
151
|
+
if key not in excluded_keys:
|
|
152
|
+
payload[key] = value
|
|
153
|
+
|
|
154
|
+
return payload
|
|
155
|
+
|
|
156
|
+
def _build_update_payload(
|
|
157
|
+
self,
|
|
158
|
+
current_mcp: MCP,
|
|
159
|
+
name: str | None = None,
|
|
160
|
+
description: str | None = None,
|
|
161
|
+
**kwargs,
|
|
162
|
+
) -> dict[str, Any]:
|
|
163
|
+
"""Build payload for MCP update with proper current state preservation.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
current_mcp: Current MCP object to update
|
|
167
|
+
name: New MCP name (None to keep current)
|
|
168
|
+
description: New description (None to keep current)
|
|
169
|
+
**kwargs: Additional parameters (config, transport, etc.)
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Complete payload dictionary for MCP update
|
|
173
|
+
|
|
174
|
+
Notes:
|
|
175
|
+
- Preserves current values as defaults when new values not provided
|
|
176
|
+
- Handles config updates properly
|
|
177
|
+
"""
|
|
178
|
+
# Prepare the update payload with current values as defaults
|
|
179
|
+
update_data = {
|
|
180
|
+
"name": name if name is not None else current_mcp.name,
|
|
181
|
+
"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
|
+
),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# Handle description with proper None handling
|
|
188
|
+
if description is not None:
|
|
189
|
+
update_data["description"] = description.strip()
|
|
190
|
+
elif hasattr(current_mcp, "description") and current_mcp.description:
|
|
191
|
+
update_data["description"] = current_mcp.description
|
|
192
|
+
|
|
193
|
+
# Handle config with proper merging
|
|
194
|
+
if "config" in kwargs:
|
|
195
|
+
update_data["config"] = kwargs["config"]
|
|
196
|
+
elif hasattr(current_mcp, "config") and current_mcp.config:
|
|
197
|
+
# Preserve existing config if present
|
|
198
|
+
update_data["config"] = current_mcp.config
|
|
199
|
+
|
|
200
|
+
# Add any other kwargs (excluding already handled ones)
|
|
201
|
+
excluded_keys = {"transport", "config"}
|
|
202
|
+
for key, value in kwargs.items():
|
|
203
|
+
if key not in excluded_keys:
|
|
204
|
+
update_data[key] = value
|
|
205
|
+
|
|
206
|
+
return update_data
|
|
82
207
|
|
|
83
208
|
def get_mcp_tools(self, mcp_id: str) -> list[dict[str, Any]]:
|
|
84
209
|
"""Get tools available from an MCP."""
|
|
85
|
-
data = self._request("GET", f"
|
|
210
|
+
data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}/tools")
|
|
86
211
|
return data or []
|
|
87
212
|
|
|
88
213
|
def test_mcp_connection(self, config: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -98,7 +223,7 @@ class MCPClient(BaseClient):
|
|
|
98
223
|
Exception: If connection test fails
|
|
99
224
|
"""
|
|
100
225
|
try:
|
|
101
|
-
response = self._request("POST",
|
|
226
|
+
response = self._request("POST", MCPS_CONNECT_ENDPOINT, json=config)
|
|
102
227
|
return response
|
|
103
228
|
except Exception as e:
|
|
104
229
|
logger.error(f"Failed to test MCP connection: {e}")
|
|
@@ -128,7 +253,7 @@ class MCPClient(BaseClient):
|
|
|
128
253
|
Exception: If tool fetching fails
|
|
129
254
|
"""
|
|
130
255
|
try:
|
|
131
|
-
response = self._request("POST",
|
|
256
|
+
response = self._request("POST", MCPS_CONNECT_TOOLS_ENDPOINT, json=config)
|
|
132
257
|
if response is None:
|
|
133
258
|
return []
|
|
134
259
|
return response.get("tools", []) or []
|