glaip-sdk 0.0.3__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.
Files changed (47) hide show
  1. glaip_sdk/__init__.py +5 -5
  2. glaip_sdk/branding.py +146 -0
  3. glaip_sdk/cli/__init__.py +1 -1
  4. glaip_sdk/cli/agent_config.py +82 -0
  5. glaip_sdk/cli/commands/__init__.py +3 -3
  6. glaip_sdk/cli/commands/agents.py +786 -271
  7. glaip_sdk/cli/commands/configure.py +19 -19
  8. glaip_sdk/cli/commands/mcps.py +151 -141
  9. glaip_sdk/cli/commands/models.py +1 -1
  10. glaip_sdk/cli/commands/tools.py +252 -178
  11. glaip_sdk/cli/display.py +244 -0
  12. glaip_sdk/cli/io.py +106 -0
  13. glaip_sdk/cli/main.py +27 -20
  14. glaip_sdk/cli/resolution.py +59 -0
  15. glaip_sdk/cli/utils.py +372 -213
  16. glaip_sdk/cli/validators.py +235 -0
  17. glaip_sdk/client/__init__.py +3 -224
  18. glaip_sdk/client/agents.py +632 -171
  19. glaip_sdk/client/base.py +66 -4
  20. glaip_sdk/client/main.py +226 -0
  21. glaip_sdk/client/mcps.py +143 -18
  22. glaip_sdk/client/tools.py +327 -104
  23. glaip_sdk/config/constants.py +10 -1
  24. glaip_sdk/models.py +43 -3
  25. glaip_sdk/rich_components.py +29 -0
  26. glaip_sdk/utils/__init__.py +18 -171
  27. glaip_sdk/utils/agent_config.py +181 -0
  28. glaip_sdk/utils/client_utils.py +159 -79
  29. glaip_sdk/utils/display.py +100 -0
  30. glaip_sdk/utils/general.py +94 -0
  31. glaip_sdk/utils/import_export.py +140 -0
  32. glaip_sdk/utils/rendering/formatting.py +6 -1
  33. glaip_sdk/utils/rendering/renderer/__init__.py +67 -8
  34. glaip_sdk/utils/rendering/renderer/base.py +340 -247
  35. glaip_sdk/utils/rendering/renderer/debug.py +3 -2
  36. glaip_sdk/utils/rendering/renderer/panels.py +11 -10
  37. glaip_sdk/utils/rendering/steps.py +1 -1
  38. glaip_sdk/utils/resource_refs.py +192 -0
  39. glaip_sdk/utils/rich_utils.py +29 -0
  40. glaip_sdk/utils/serialization.py +285 -0
  41. glaip_sdk/utils/validation.py +273 -0
  42. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/METADATA +6 -5
  43. glaip_sdk-0.0.5.dist-info/RECORD +55 -0
  44. glaip_sdk/cli/commands/init.py +0 -177
  45. glaip_sdk-0.0.3.dist-info/RECORD +0 -40
  46. {glaip_sdk-0.0.3.dist-info → glaip_sdk-0.0.5.dist-info}/WHEEL +0 -0
  47. {glaip_sdk-0.0.3.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
- load_dotenv()
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, exc_type, exc_val, exc_tb):
341
+ def __exit__(self, _exc_type, _exc_val, _exc_tb):
282
342
  """Context manager exit."""
283
- self.close()
343
+ # Only close if this is not session-scoped
344
+ if not self._session_scoped:
345
+ self.close()
@@ -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", "/mcps/")
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"/mcps/{mcp_id}")
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", "/mcps/")
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
- payload = {
57
- "name": name,
58
- "description": description,
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="/mcps/",
69
- get_endpoint_fmt="/mcps/{id}",
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
- data = self._request("PUT", f"/mcps/{mcp_id}", json=kwargs)
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"/mcps/{mcp_id}")
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"/mcps/{mcp_id}/tools")
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", "/mcps/connect", json=config)
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", "/mcps/connect/tools", json=config)
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 []