alita-sdk 0.3.532__py3-none-any.whl → 0.3.602__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.

Potentially problematic release.


This version of alita-sdk might be problematic. Click here for more details.

Files changed (137) hide show
  1. alita_sdk/cli/agent_executor.py +2 -1
  2. alita_sdk/cli/agent_loader.py +34 -4
  3. alita_sdk/cli/agents.py +433 -203
  4. alita_sdk/community/__init__.py +8 -4
  5. alita_sdk/configurations/__init__.py +1 -0
  6. alita_sdk/configurations/openapi.py +323 -0
  7. alita_sdk/runtime/clients/client.py +165 -7
  8. alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
  9. alita_sdk/runtime/langchain/assistant.py +61 -11
  10. alita_sdk/runtime/langchain/constants.py +419 -171
  11. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +4 -2
  12. alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
  13. alita_sdk/runtime/langchain/langraph_agent.py +108 -23
  14. alita_sdk/runtime/langchain/utils.py +76 -14
  15. alita_sdk/runtime/skills/__init__.py +91 -0
  16. alita_sdk/runtime/skills/callbacks.py +498 -0
  17. alita_sdk/runtime/skills/discovery.py +540 -0
  18. alita_sdk/runtime/skills/executor.py +610 -0
  19. alita_sdk/runtime/skills/input_builder.py +371 -0
  20. alita_sdk/runtime/skills/models.py +330 -0
  21. alita_sdk/runtime/skills/registry.py +355 -0
  22. alita_sdk/runtime/skills/skill_runner.py +330 -0
  23. alita_sdk/runtime/toolkits/__init__.py +5 -0
  24. alita_sdk/runtime/toolkits/artifact.py +2 -1
  25. alita_sdk/runtime/toolkits/mcp.py +6 -3
  26. alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
  27. alita_sdk/runtime/toolkits/skill_router.py +238 -0
  28. alita_sdk/runtime/toolkits/tools.py +139 -10
  29. alita_sdk/runtime/toolkits/vectorstore.py +1 -1
  30. alita_sdk/runtime/tools/__init__.py +3 -1
  31. alita_sdk/runtime/tools/artifact.py +15 -0
  32. alita_sdk/runtime/tools/data_analysis.py +183 -0
  33. alita_sdk/runtime/tools/llm.py +260 -73
  34. alita_sdk/runtime/tools/loop.py +3 -1
  35. alita_sdk/runtime/tools/loop_output.py +3 -1
  36. alita_sdk/runtime/tools/mcp_server_tool.py +6 -3
  37. alita_sdk/runtime/tools/router.py +2 -4
  38. alita_sdk/runtime/tools/sandbox.py +9 -6
  39. alita_sdk/runtime/tools/skill_router.py +776 -0
  40. alita_sdk/runtime/tools/tool.py +3 -1
  41. alita_sdk/runtime/tools/vectorstore.py +7 -2
  42. alita_sdk/runtime/tools/vectorstore_base.py +7 -2
  43. alita_sdk/runtime/utils/constants.py +5 -1
  44. alita_sdk/runtime/utils/mcp_client.py +1 -1
  45. alita_sdk/runtime/utils/mcp_sse_client.py +1 -1
  46. alita_sdk/runtime/utils/toolkit_utils.py +2 -0
  47. alita_sdk/tools/__init__.py +44 -2
  48. alita_sdk/tools/ado/repos/__init__.py +26 -8
  49. alita_sdk/tools/ado/repos/repos_wrapper.py +78 -52
  50. alita_sdk/tools/ado/test_plan/__init__.py +3 -2
  51. alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
  52. alita_sdk/tools/ado/utils.py +1 -18
  53. alita_sdk/tools/ado/wiki/__init__.py +2 -1
  54. alita_sdk/tools/ado/wiki/ado_wrapper.py +23 -1
  55. alita_sdk/tools/ado/work_item/__init__.py +3 -2
  56. alita_sdk/tools/ado/work_item/ado_wrapper.py +56 -3
  57. alita_sdk/tools/advanced_jira_mining/__init__.py +2 -1
  58. alita_sdk/tools/aws/delta_lake/__init__.py +2 -1
  59. alita_sdk/tools/azure_ai/search/__init__.py +2 -1
  60. alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
  61. alita_sdk/tools/base_indexer_toolkit.py +51 -30
  62. alita_sdk/tools/bitbucket/__init__.py +2 -1
  63. alita_sdk/tools/bitbucket/api_wrapper.py +1 -1
  64. alita_sdk/tools/bitbucket/cloud_api_wrapper.py +3 -3
  65. alita_sdk/tools/browser/__init__.py +1 -1
  66. alita_sdk/tools/carrier/__init__.py +1 -1
  67. alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
  68. alita_sdk/tools/cloud/aws/__init__.py +2 -1
  69. alita_sdk/tools/cloud/azure/__init__.py +2 -1
  70. alita_sdk/tools/cloud/gcp/__init__.py +2 -1
  71. alita_sdk/tools/cloud/k8s/__init__.py +2 -1
  72. alita_sdk/tools/code/linter/__init__.py +2 -1
  73. alita_sdk/tools/code/sonar/__init__.py +2 -1
  74. alita_sdk/tools/code_indexer_toolkit.py +19 -2
  75. alita_sdk/tools/confluence/__init__.py +7 -6
  76. alita_sdk/tools/confluence/api_wrapper.py +7 -8
  77. alita_sdk/tools/confluence/loader.py +4 -2
  78. alita_sdk/tools/custom_open_api/__init__.py +2 -1
  79. alita_sdk/tools/elastic/__init__.py +2 -1
  80. alita_sdk/tools/elitea_base.py +28 -9
  81. alita_sdk/tools/figma/__init__.py +52 -6
  82. alita_sdk/tools/figma/api_wrapper.py +1158 -123
  83. alita_sdk/tools/figma/figma_client.py +73 -0
  84. alita_sdk/tools/figma/toon_tools.py +2748 -0
  85. alita_sdk/tools/github/__init__.py +2 -1
  86. alita_sdk/tools/github/github_client.py +56 -92
  87. alita_sdk/tools/github/schemas.py +4 -4
  88. alita_sdk/tools/gitlab/__init__.py +2 -1
  89. alita_sdk/tools/gitlab/api_wrapper.py +118 -38
  90. alita_sdk/tools/gitlab_org/__init__.py +2 -1
  91. alita_sdk/tools/gitlab_org/api_wrapper.py +60 -62
  92. alita_sdk/tools/google/bigquery/__init__.py +2 -1
  93. alita_sdk/tools/google_places/__init__.py +2 -1
  94. alita_sdk/tools/jira/__init__.py +2 -1
  95. alita_sdk/tools/keycloak/__init__.py +2 -1
  96. alita_sdk/tools/localgit/__init__.py +2 -1
  97. alita_sdk/tools/memory/__init__.py +1 -1
  98. alita_sdk/tools/ocr/__init__.py +2 -1
  99. alita_sdk/tools/openapi/__init__.py +490 -118
  100. alita_sdk/tools/openapi/api_wrapper.py +1368 -0
  101. alita_sdk/tools/openapi/tool.py +20 -0
  102. alita_sdk/tools/pandas/__init__.py +11 -5
  103. alita_sdk/tools/pandas/api_wrapper.py +38 -25
  104. alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
  105. alita_sdk/tools/postman/__init__.py +2 -1
  106. alita_sdk/tools/pptx/__init__.py +2 -1
  107. alita_sdk/tools/qtest/__init__.py +21 -2
  108. alita_sdk/tools/qtest/api_wrapper.py +430 -13
  109. alita_sdk/tools/rally/__init__.py +2 -1
  110. alita_sdk/tools/rally/api_wrapper.py +1 -1
  111. alita_sdk/tools/report_portal/__init__.py +2 -1
  112. alita_sdk/tools/salesforce/__init__.py +2 -1
  113. alita_sdk/tools/servicenow/__init__.py +11 -10
  114. alita_sdk/tools/servicenow/api_wrapper.py +1 -1
  115. alita_sdk/tools/sharepoint/__init__.py +2 -1
  116. alita_sdk/tools/sharepoint/api_wrapper.py +2 -2
  117. alita_sdk/tools/slack/__init__.py +3 -2
  118. alita_sdk/tools/slack/api_wrapper.py +2 -2
  119. alita_sdk/tools/sql/__init__.py +3 -2
  120. alita_sdk/tools/testio/__init__.py +2 -1
  121. alita_sdk/tools/testrail/__init__.py +2 -1
  122. alita_sdk/tools/utils/content_parser.py +77 -3
  123. alita_sdk/tools/utils/text_operations.py +163 -71
  124. alita_sdk/tools/xray/__init__.py +3 -2
  125. alita_sdk/tools/yagmail/__init__.py +2 -1
  126. alita_sdk/tools/zephyr/__init__.py +2 -1
  127. alita_sdk/tools/zephyr_enterprise/__init__.py +2 -1
  128. alita_sdk/tools/zephyr_essential/__init__.py +2 -1
  129. alita_sdk/tools/zephyr_scale/__init__.py +3 -2
  130. alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
  131. alita_sdk/tools/zephyr_squad/__init__.py +2 -1
  132. {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/METADATA +7 -6
  133. {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/RECORD +137 -119
  134. {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/WHEEL +0 -0
  135. {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/entry_points.txt +0 -0
  136. {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/licenses/LICENSE +0 -0
  137. {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1048 @@
1
+ """
2
+ MCP Config Toolkit for Alita SDK.
3
+
4
+ This toolkit enables connection to pre-configured MCP servers defined in YAML config.
5
+ Supports both stdio (local subprocess) and http (remote) MCP servers.
6
+
7
+ Configuration is loaded from:
8
+ 1. SDK config file (ALITA_MCP_SERVERS_CONFIG environment variable)
9
+ 2. Direct configuration passed to get_toolkit()
10
+
11
+ Example config (mcp_servers.yml):
12
+ ```yaml
13
+ mcp_servers:
14
+ # Stdio server (local subprocess)
15
+ playwright:
16
+ type: stdio
17
+ runtime: npm
18
+ package: "@playwright/mcp@latest"
19
+ command: npx
20
+ args: ["@playwright/mcp@latest"]
21
+ stateful: true
22
+ description: "Browser automation via Playwright"
23
+
24
+ # HTTP server (remote)
25
+ github_copilot:
26
+ type: http
27
+ url: "https://api.githubcopilot.com/mcp/"
28
+ description: "GitHub Copilot MCP"
29
+ config_schema:
30
+ properties:
31
+ github_token:
32
+ type: string
33
+ secret: true
34
+ required: true
35
+ headers:
36
+ Authorization: "Bearer {github_token}"
37
+ ```
38
+ """
39
+
40
+ import asyncio
41
+ import logging
42
+ import os
43
+ import re
44
+ import threading
45
+ from typing import List, Optional, Dict, Any
46
+
47
+ import yaml
48
+ from langchain_core.tools import BaseToolkit, BaseTool
49
+ from pydantic import BaseModel, Field
50
+
51
+ logger = logging.getLogger(__name__)
52
+
53
+ name = "mcp_config"
54
+
55
+ # Global session manager for stdio process lifecycle
56
+ _session_manager_lock = threading.Lock()
57
+ _session_manager: Optional['McpStdioSessionManager'] = None
58
+
59
+
60
+ def _create_stdio_tool_func(original_tool_name: str, server_name: str, server_config: Dict[str, Any]):
61
+ """
62
+ Create a tool function that uses the MCP SDK directly with proper session lifecycle.
63
+
64
+ Creates a fresh session for each tool call and cleans up after.
65
+ This ensures subprocesses don't leak.
66
+ """
67
+
68
+ def tool_func(**kwargs) -> str:
69
+ """Execute the MCP tool with proper session management using MCP SDK directly."""
70
+ import json
71
+
72
+ async def _execute():
73
+ try:
74
+ from mcp import ClientSession, StdioServerParameters
75
+ from mcp.client.stdio import stdio_client
76
+ except ImportError:
77
+ raise ImportError(
78
+ "mcp package is required for stdio MCP servers. "
79
+ "Install with: pip install mcp"
80
+ )
81
+
82
+ command = server_config['command']
83
+ args = server_config.get('args', [])
84
+
85
+ # Build environment
86
+ env = dict(os.environ)
87
+ static_env = server_config.get('env', {})
88
+ env.update(static_env)
89
+
90
+ server_params = StdioServerParameters(
91
+ command=command,
92
+ args=args,
93
+ env=env
94
+ )
95
+
96
+ logger.debug(f"[MCP Config] Starting stdio session for tool {original_tool_name}")
97
+
98
+ # Use MCP SDK's context managers for proper subprocess cleanup
99
+ async with stdio_client(server_params) as (read_stream, write_stream):
100
+ async with ClientSession(read_stream, write_stream) as session:
101
+ # Initialize the connection
102
+ await session.initialize()
103
+
104
+ # Call the tool directly
105
+ logger.debug(f"[MCP Config] Calling tool {original_tool_name} with args: {kwargs}")
106
+ result = await session.call_tool(original_tool_name, kwargs)
107
+
108
+ # Format the result
109
+ if hasattr(result, 'content') and result.content:
110
+ # MCP returns CallToolResult with content list
111
+ content_parts = []
112
+ for content_item in result.content:
113
+ if hasattr(content_item, 'text'):
114
+ content_parts.append(content_item.text)
115
+ elif hasattr(content_item, 'data'):
116
+ content_parts.append(str(content_item.data))
117
+ else:
118
+ content_parts.append(str(content_item))
119
+ return '\n'.join(content_parts)
120
+ elif hasattr(result, 'text'):
121
+ return result.text
122
+ else:
123
+ return str(result)
124
+
125
+ # Run async code - use asyncio.run for clean event loop management
126
+ try:
127
+ return asyncio.run(_execute())
128
+ except Exception as e:
129
+ logger.error(f"[MCP Config] Error executing tool {original_tool_name}: {e}")
130
+ raise
131
+
132
+ return tool_func
133
+
134
+
135
+ # Legacy session manager - kept for backwards compatibility but not used
136
+ class McpStdioSessionManager:
137
+ """
138
+ Manages MCP stdio server sessions across agent executions.
139
+
140
+ NOTE: This is deprecated. Use McpStdioToolWrapper instead for proper cleanup.
141
+ """
142
+
143
+ def __init__(self):
144
+ self.sessions: Dict[str, Any] = {}
145
+ self.clients: Dict[str, Any] = {}
146
+ self._session_contexts: Dict[str, Any] = {}
147
+ self._lock = threading.Lock()
148
+
149
+ async def get_or_create_session(
150
+ self,
151
+ server_name: str,
152
+ server_config: Dict[str, Any]
153
+ ) -> tuple:
154
+ """Get existing session or create a new one."""
155
+ with self._lock:
156
+ if server_name in self.sessions:
157
+ return self.sessions[server_name], self.clients[server_name]
158
+
159
+ try:
160
+ from langchain_mcp_adapters.client import MultiServerMCPClient
161
+ except ImportError:
162
+ raise ImportError(
163
+ "langchain-mcp-adapters is required for stdio MCP servers. "
164
+ "Install with: pip install langchain-mcp-adapters"
165
+ )
166
+
167
+ mcp_config = {
168
+ 'transport': 'stdio',
169
+ 'command': server_config['command'],
170
+ 'args': server_config.get('args', [])
171
+ }
172
+
173
+ env = server_config.get('env', {})
174
+ if env:
175
+ mcp_config['env'] = env
176
+
177
+ logger.info(f"[MCP Config] Starting stdio server: {server_name}")
178
+
179
+ client = MultiServerMCPClient({server_name: mcp_config})
180
+ session_context = client.session(server_name)
181
+ session = await session_context.__aenter__()
182
+
183
+ with self._lock:
184
+ self.sessions[server_name] = session
185
+ self.clients[server_name] = client
186
+ self._session_contexts[server_name] = session_context
187
+
188
+ return session, client
189
+
190
+ async def cleanup(self):
191
+ """Cleanup all sessions."""
192
+ with self._lock:
193
+ for name, ctx in self._session_contexts.items():
194
+ try:
195
+ await ctx.__aexit__(None, None, None)
196
+ except Exception as e:
197
+ logger.warning(f"[MCP Config] Error cleaning up {name}: {e}")
198
+ self.sessions.clear()
199
+ self.clients.clear()
200
+ self._session_contexts.clear()
201
+
202
+
203
+ def get_session_manager() -> McpStdioSessionManager:
204
+ """Get or create the global session manager."""
205
+ global _session_manager
206
+ with _session_manager_lock:
207
+ if _session_manager is None:
208
+ _session_manager = McpStdioSessionManager()
209
+ return _session_manager
210
+
211
+
212
+ def load_mcp_servers_config(config_path: Optional[str] = None) -> Dict[str, Any]:
213
+ """Load MCP servers configuration from YAML file.
214
+
215
+ Config is loaded from (in order of priority):
216
+ 1. Explicit config_path parameter
217
+ 2. ALITA_MCP_SERVERS_CONFIG environment variable
218
+ 3. Plugin config: /data/plugins/indexer_worker/config.yml
219
+ 4. Template config: /data/configs/indexer_worker.yml
220
+ """
221
+ if config_path is None:
222
+ config_path = os.environ.get('ALITA_MCP_SERVERS_CONFIG')
223
+
224
+ # List of paths to check in order
225
+ paths_to_check = []
226
+ if config_path:
227
+ paths_to_check.append(config_path)
228
+ else:
229
+ # Plugin's runtime config (merged by pylon)
230
+ paths_to_check.append('/data/plugins/indexer_worker/config.yml')
231
+ # Template config in configs directory
232
+ paths_to_check.append('/data/configs/indexer_worker.yml')
233
+
234
+ for path in paths_to_check:
235
+ if os.path.exists(path):
236
+ try:
237
+ with open(path) as f:
238
+ config = yaml.safe_load(f)
239
+ mcp_servers = config.get('mcp_servers', {})
240
+ if mcp_servers:
241
+ logger.info(f"[MCP Config] Loaded {len(mcp_servers)} MCP servers from {path}")
242
+ return mcp_servers
243
+ except Exception as e:
244
+ logger.warning(f"[MCP Config] Failed to load config from {path}: {e}")
245
+
246
+ logger.debug("[MCP Config] No MCP servers configuration found")
247
+ return {}
248
+
249
+
250
+ _server_configs: Optional[Dict[str, Any]] = None
251
+
252
+
253
+ def get_mcp_server_config(server_name: str) -> Optional[Dict[str, Any]]:
254
+ """Get configuration for a specific MCP server."""
255
+ global _server_configs
256
+ if _server_configs is None:
257
+ _server_configs = load_mcp_servers_config()
258
+ return _server_configs.get(server_name)
259
+
260
+
261
+ def get_all_mcp_server_configs() -> Dict[str, Any]:
262
+ """Get all configured MCP server definitions."""
263
+ global _server_configs
264
+ if _server_configs is None:
265
+ _server_configs = load_mcp_servers_config()
266
+ return _server_configs
267
+
268
+
269
+ def _substitute_placeholders(value: Any, user_config: Dict[str, Any]) -> Any:
270
+ """Substitute {param} placeholders with values from user_config."""
271
+ if isinstance(value, str):
272
+ def replacer(match):
273
+ key = match.group(1)
274
+ return str(user_config.get(key, match.group(0)))
275
+ return re.sub(r'\{(\w+)\}', replacer, value)
276
+ elif isinstance(value, dict):
277
+ return {k: _substitute_placeholders(v, user_config) for k, v in value.items()}
278
+ elif isinstance(value, list):
279
+ return [_substitute_placeholders(v, user_config) for v in value]
280
+ return value
281
+
282
+
283
+ class McpConfigToolkit(BaseToolkit):
284
+ """
285
+ MCP Config Toolkit for pre-configured MCP servers.
286
+
287
+ Supports both stdio (local subprocess) and http (remote) servers
288
+ defined in mcp_servers.yml configuration.
289
+ """
290
+
291
+ tools: List[BaseTool] = []
292
+ toolkit_name: Optional[str] = None
293
+ server_name: Optional[str] = None
294
+ server_type: Optional[str] = None
295
+
296
+ model_config = {"arbitrary_types_allowed": True}
297
+
298
+ @staticmethod
299
+ def toolkit_config_schema() -> BaseModel:
300
+ """Generate the configuration schema for MCP config toolkit."""
301
+ from pydantic import create_model
302
+
303
+ return create_model(
304
+ 'mcp_config',
305
+ server_name=(
306
+ str,
307
+ Field(
308
+ description="Name of the MCP server to connect to",
309
+ json_schema_extra={
310
+ 'tooltip': 'Server name as defined in mcp_servers.yml config',
311
+ 'example': 'playwright'
312
+ }
313
+ )
314
+ ),
315
+ selected_tools=(
316
+ Optional[List[str]],
317
+ Field(
318
+ default=None,
319
+ description="List of specific tools to enable (empty = all tools)",
320
+ )
321
+ ),
322
+ excluded_tools=(
323
+ Optional[List[str]],
324
+ Field(
325
+ default=None,
326
+ description="List of tools to exclude",
327
+ )
328
+ ),
329
+ )
330
+
331
+ @staticmethod
332
+ def get_available_servers() -> List[Dict[str, Any]]:
333
+ """Get list of available MCP servers from config."""
334
+ servers = get_all_mcp_server_configs()
335
+ result = []
336
+
337
+ for name, config in servers.items():
338
+ server_type = config.get('type', 'stdio')
339
+ result.append({
340
+ 'name': name,
341
+ 'type': server_type,
342
+ 'description': config.get('description', f'MCP server: {name}'),
343
+ 'stateful': config.get('stateful', False),
344
+ 'config_schema': config.get('config_schema', {'properties': {}}),
345
+ })
346
+
347
+ return result
348
+
349
+ @classmethod
350
+ def get_toolkit(
351
+ cls,
352
+ server_name: str,
353
+ server_config: Optional[Dict[str, Any]] = None,
354
+ user_config: Optional[Dict[str, Any]] = None,
355
+ selected_tools: Optional[List[str]] = None,
356
+ excluded_tools: Optional[List[str]] = None,
357
+ toolkit_name: Optional[str] = None,
358
+ client=None,
359
+ **kwargs
360
+ ) -> 'McpConfigToolkit':
361
+ """
362
+ Create an MCP toolkit instance from config.
363
+
364
+ Automatically routes to stdio or http handler based on server type.
365
+ """
366
+ if server_config is None:
367
+ server_config = get_mcp_server_config(server_name)
368
+ if server_config is None:
369
+ raise ValueError(
370
+ f"MCP server '{server_name}' not found in configuration. "
371
+ f"Available servers: {list(get_all_mcp_server_configs().keys())}"
372
+ )
373
+
374
+ if user_config is None:
375
+ user_config = {}
376
+
377
+ server_type = server_config.get('type', 'stdio')
378
+
379
+ if server_type == 'stdio':
380
+ tools = cls._load_stdio_tools(
381
+ server_name=server_name,
382
+ server_config=server_config,
383
+ user_config=user_config,
384
+ selected_tools=selected_tools,
385
+ excluded_tools=excluded_tools,
386
+ toolkit_name=toolkit_name or server_name,
387
+ )
388
+ elif server_type == 'http':
389
+ tools = cls._load_http_tools(
390
+ server_name=server_name,
391
+ server_config=server_config,
392
+ user_config=user_config,
393
+ selected_tools=selected_tools,
394
+ excluded_tools=excluded_tools,
395
+ toolkit_name=toolkit_name or server_name,
396
+ client=client,
397
+ )
398
+ else:
399
+ raise ValueError(f"Unknown MCP server type: {server_type}")
400
+
401
+ return cls(
402
+ tools=tools,
403
+ toolkit_name=toolkit_name or server_name,
404
+ server_name=server_name,
405
+ server_type=server_type
406
+ )
407
+
408
+ @classmethod
409
+ def _load_stdio_tools(
410
+ cls,
411
+ server_name: str,
412
+ server_config: Dict[str, Any],
413
+ user_config: Dict[str, Any],
414
+ selected_tools: Optional[List[str]],
415
+ excluded_tools: Optional[List[str]],
416
+ toolkit_name: str,
417
+ ) -> List[BaseTool]:
418
+ """Load tools from stdio MCP server."""
419
+ # Resolve environment variables from user config
420
+ env = dict(server_config.get('env', {}))
421
+ env_mapping = server_config.get('env_mapping', {})
422
+
423
+ for env_var, config_ref in env_mapping.items():
424
+ config_key = config_ref.strip('{}')
425
+ if config_key in user_config:
426
+ value = user_config[config_key]
427
+ if isinstance(value, list):
428
+ value = ','.join(str(v) for v in value)
429
+ env[env_var] = str(value)
430
+
431
+ resolved_config = {**server_config, 'env': env}
432
+
433
+ # Load tools asynchronously
434
+ try:
435
+ loop = asyncio.get_event_loop()
436
+ except RuntimeError:
437
+ loop = asyncio.new_event_loop()
438
+ asyncio.set_event_loop(loop)
439
+
440
+ return loop.run_until_complete(
441
+ cls._load_stdio_tools_async(
442
+ server_name=server_name,
443
+ server_config=resolved_config,
444
+ selected_tools=selected_tools,
445
+ excluded_tools=excluded_tools,
446
+ toolkit_name=toolkit_name,
447
+ )
448
+ )
449
+
450
+ @classmethod
451
+ async def _load_stdio_tools_async(
452
+ cls,
453
+ server_name: str,
454
+ server_config: Dict[str, Any],
455
+ selected_tools: Optional[List[str]],
456
+ excluded_tools: Optional[List[str]],
457
+ toolkit_name: str,
458
+ ) -> List[BaseTool]:
459
+ """Load tools from stdio MCP server asynchronously.
460
+
461
+ Uses McpStdioToolWrapper which creates a fresh session per tool invocation
462
+ and properly cleans up subprocesses after each call.
463
+ """
464
+ try:
465
+ from mcp import ClientSession, StdioServerParameters
466
+ from mcp.client.stdio import stdio_client
467
+ except ImportError:
468
+ raise ImportError(
469
+ "mcp package is required for stdio MCP servers. "
470
+ "Install with: pip install mcp"
471
+ )
472
+
473
+ command = server_config['command']
474
+ args = server_config.get('args', [])
475
+
476
+ # Build environment
477
+ env = dict(os.environ)
478
+ static_env = server_config.get('env', {})
479
+ env.update(static_env)
480
+
481
+ server_params = StdioServerParameters(
482
+ command=command,
483
+ args=args,
484
+ env=env
485
+ )
486
+
487
+ # Discover tools by temporarily connecting to the server
488
+ tool_definitions = []
489
+ async with stdio_client(server_params) as (read_stream, write_stream):
490
+ async with ClientSession(read_stream, write_stream) as session:
491
+ await session.initialize()
492
+ tool_list = await session.list_tools()
493
+
494
+ for tool in tool_list.tools:
495
+ tool_definitions.append({
496
+ 'name': tool.name,
497
+ 'description': tool.description or '',
498
+ 'inputSchema': tool.inputSchema if hasattr(tool, 'inputSchema') else None,
499
+ })
500
+
501
+ logger.info(f"[MCP Config] Discovered {len(tool_definitions)} tools from stdio server {server_name}")
502
+
503
+ # Apply filtering
504
+ if selected_tools:
505
+ tool_definitions = [t for t in tool_definitions if t['name'] in selected_tools]
506
+ if excluded_tools:
507
+ tool_definitions = [t for t in tool_definitions if t['name'] not in excluded_tools]
508
+
509
+ # Create tools using StructuredTool.from_function for proper LangChain integration
510
+ from langchain_core.tools import StructuredTool
511
+
512
+ tools = []
513
+ for tool_def in tool_definitions:
514
+ tool_name = tool_def['name']
515
+ tool_desc = tool_def['description']
516
+
517
+ # Add toolkit context to description
518
+ if toolkit_name and not tool_desc.startswith(f"[{toolkit_name}]"):
519
+ tool_desc = f"[{toolkit_name}] {tool_desc}"
520
+
521
+ # Create the tool function that handles session lifecycle
522
+ tool_func = _create_stdio_tool_func(tool_name, server_name, server_config)
523
+
524
+ # Build args_schema from inputSchema if available
525
+ args_schema = None
526
+ input_schema = tool_def.get('inputSchema')
527
+ if input_schema and isinstance(input_schema, dict):
528
+ try:
529
+ from pydantic import create_model, Field as PydanticField
530
+ fields = {}
531
+ properties = input_schema.get('properties', {})
532
+ required = input_schema.get('required', [])
533
+
534
+ for prop_name, prop_def in properties.items():
535
+ prop_type = prop_def.get('type', 'string')
536
+ prop_desc = prop_def.get('description', '')
537
+
538
+ # Map JSON schema types to Python types
539
+ type_map = {
540
+ 'string': str,
541
+ 'integer': int,
542
+ 'number': float,
543
+ 'boolean': bool,
544
+ 'array': list,
545
+ 'object': dict,
546
+ }
547
+ python_type = type_map.get(prop_type, str)
548
+
549
+ if prop_name not in required:
550
+ python_type = Optional[python_type]
551
+ fields[prop_name] = (python_type, PydanticField(default=None, description=prop_desc))
552
+ else:
553
+ fields[prop_name] = (python_type, PydanticField(description=prop_desc))
554
+
555
+ if fields:
556
+ args_schema = create_model(f'{tool_name}Args', **fields)
557
+ except Exception as e:
558
+ logger.debug(f"[MCP Config] Could not create args_schema for {tool_name}: {e}")
559
+
560
+ # Create StructuredTool with the function
561
+ tool = StructuredTool.from_function(
562
+ func=tool_func,
563
+ name=tool_name,
564
+ description=tool_desc,
565
+ args_schema=args_schema,
566
+ )
567
+
568
+ tools.append(tool)
569
+
570
+ return tools
571
+
572
+ @classmethod
573
+ def _load_http_tools(
574
+ cls,
575
+ server_name: str,
576
+ server_config: Dict[str, Any],
577
+ user_config: Dict[str, Any],
578
+ selected_tools: Optional[List[str]],
579
+ excluded_tools: Optional[List[str]],
580
+ toolkit_name: str,
581
+ client=None,
582
+ ) -> List[BaseTool]:
583
+ """Load tools from HTTP MCP server using existing McpToolkit."""
584
+ from .mcp import McpToolkit
585
+
586
+ # Substitute placeholders in URL and headers
587
+ url = _substitute_placeholders(server_config.get('url', ''), user_config)
588
+ headers = _substitute_placeholders(server_config.get('headers', {}), user_config)
589
+ timeout = server_config.get('timeout', 60)
590
+
591
+ logger.info(f"[MCP Config] Connecting to HTTP server {server_name} at {url}")
592
+
593
+ # Use existing McpToolkit for HTTP servers
594
+ mcp_toolkit = McpToolkit.get_toolkit(
595
+ url=url,
596
+ headers=headers,
597
+ timeout=timeout,
598
+ selected_tools=selected_tools or [],
599
+ toolkit_name=toolkit_name,
600
+ client=client,
601
+ )
602
+
603
+ tools = mcp_toolkit.get_tools()
604
+
605
+ # Apply excluded_tools filter (McpToolkit only supports selected_tools)
606
+ if excluded_tools:
607
+ tools = [t for t in tools if t.name not in excluded_tools]
608
+
609
+ logger.info(f"[MCP Config] Loaded {len(tools)} tools from HTTP server {server_name}")
610
+
611
+ return tools
612
+
613
+ def get_tools(self) -> List[BaseTool]:
614
+ """Return all tools from this MCP server."""
615
+ return self.tools
616
+
617
+
618
+ # Utility functions for toolkit registration
619
+
620
+ def _discover_tools_for_http_server(url: str, headers: Optional[Dict[str, Any]] = None, timeout: int = 30) -> List[Dict[str, Any]]:
621
+ """
622
+ Attempt to discover tools from an HTTP MCP server.
623
+
624
+ Returns list of tool dictionaries with 'name' and 'description' keys.
625
+ Returns empty list if discovery fails (e.g., auth required).
626
+ """
627
+ try:
628
+ from ..utils.mcp_client import McpClient
629
+ import asyncio
630
+
631
+ async def _discover():
632
+ client = McpClient(
633
+ url=url,
634
+ headers=headers or {},
635
+ timeout=timeout
636
+ )
637
+ tools = []
638
+ try:
639
+ async with client:
640
+ await client.initialize()
641
+ discovered = await client.list_tools()
642
+ for tool in discovered:
643
+ tools.append({
644
+ 'name': tool.get('name', 'unknown'),
645
+ 'description': tool.get('description', '')
646
+ })
647
+ except Exception as e:
648
+ logger.debug(f"[MCP Config] Tool discovery failed: {e}")
649
+ return tools
650
+
651
+ # Run async discovery
652
+ try:
653
+ loop = asyncio.get_event_loop()
654
+ if loop.is_running():
655
+ # If already in async context, create a new thread
656
+ import concurrent.futures
657
+ with concurrent.futures.ThreadPoolExecutor() as executor:
658
+ future = executor.submit(asyncio.run, _discover())
659
+ return future.result(timeout=timeout)
660
+ else:
661
+ return loop.run_until_complete(_discover())
662
+ except RuntimeError:
663
+ return asyncio.run(_discover())
664
+
665
+ except Exception as e:
666
+ logger.debug(f"[MCP Config] Tool discovery error: {e}")
667
+ return []
668
+
669
+
670
+ def _create_check_connection_for_stdio(server_name: str, server_config: Dict[str, Any]):
671
+ """
672
+ Create a check_connection static method for a stdio MCP server.
673
+
674
+ This method starts the MCP server subprocess, discovers tools, and returns them.
675
+ Returns dict with 'tools' on success, or error message string on failure.
676
+ """
677
+ def check_connection(settings: dict) -> dict | str | None:
678
+ """
679
+ Discover tools from the stdio MCP server.
680
+
681
+ Args:
682
+ settings: Dictionary containing user-provided configuration
683
+
684
+ Returns:
685
+ Dict with 'tools' list on success, error message string on failure
686
+ """
687
+ import asyncio
688
+ import subprocess
689
+ import signal
690
+
691
+ # Resolve environment variables from user config
692
+ env = dict(os.environ) # Start with current environment
693
+ static_env = server_config.get('env', {})
694
+ env.update(static_env)
695
+
696
+ env_mapping = server_config.get('env_mapping', {})
697
+ for env_var, config_ref in env_mapping.items():
698
+ config_key = config_ref.strip('{}')
699
+ if config_key in settings:
700
+ value = settings[config_key]
701
+ if isinstance(value, list):
702
+ value = ','.join(str(v) for v in value)
703
+ env[env_var] = str(value)
704
+
705
+ logger.info(f"[MCP Config] Discovering tools from stdio server {server_name}")
706
+
707
+ async def _discover():
708
+ try:
709
+ from mcp import ClientSession, StdioServerParameters
710
+ from mcp.client.stdio import stdio_client
711
+ except ImportError:
712
+ raise ImportError(
713
+ "mcp package is required for stdio MCP servers. "
714
+ "Install with: pip install mcp"
715
+ )
716
+
717
+ command = server_config['command']
718
+ args = server_config.get('args', [])
719
+
720
+ server_params = StdioServerParameters(
721
+ command=command,
722
+ args=args,
723
+ env=env
724
+ )
725
+
726
+ tools = []
727
+ args_schemas = {}
728
+
729
+ # Use the MCP SDK's stdio_client context manager for proper cleanup
730
+ async with stdio_client(server_params) as (read_stream, write_stream):
731
+ async with ClientSession(read_stream, write_stream) as session:
732
+ # Initialize the connection
733
+ await session.initialize()
734
+
735
+ # List tools
736
+ tool_list = await session.list_tools()
737
+
738
+ for tool in tool_list.tools:
739
+ tool_name = tool.name
740
+ tool_desc = tool.description or ''
741
+ input_schema = tool.inputSchema if hasattr(tool, 'inputSchema') else None
742
+
743
+ tools.append({
744
+ 'name': tool_name,
745
+ 'description': tool_desc,
746
+ 'inputSchema': input_schema,
747
+ })
748
+ if input_schema:
749
+ args_schemas[tool_name] = input_schema
750
+
751
+ logger.info(f"[MCP Config] stdio_client context exited for {server_name}")
752
+ return {'tools': tools, 'args_schemas': args_schemas}
753
+
754
+ try:
755
+ # Run async discovery - always use asyncio.run for clean event loop
756
+ result = asyncio.run(_discover())
757
+
758
+ logger.info(f"[MCP Config] Discovered {len(result.get('tools', []))} tools from stdio server {server_name}")
759
+ return result
760
+
761
+ except Exception as e:
762
+ error_msg = f"Failed to discover tools from {server_name}: {str(e)}"
763
+ logger.error(f"[MCP Config] {error_msg}")
764
+ return error_msg
765
+
766
+ return check_connection
767
+
768
+
769
+ def _create_check_connection_for_http(server_name: str, server_config: Dict[str, Any]):
770
+ """
771
+ Create a check_connection static method for an HTTP MCP server.
772
+
773
+ This method discovers tools from the MCP server using the provided credentials.
774
+ Returns dict with 'tools' on success, or error message string on failure.
775
+ """
776
+ def check_connection(settings: dict) -> dict | str | None:
777
+ """
778
+ Discover tools from the MCP server.
779
+
780
+ Args:
781
+ settings: Dictionary containing user-provided credentials
782
+
783
+ Returns:
784
+ Dict with 'tools' list on success, error message string on failure
785
+ """
786
+ url = server_config.get('url', '')
787
+ headers_template = server_config.get('headers', {})
788
+ timeout = server_config.get('timeout', 60)
789
+
790
+ # Substitute placeholders in headers with user-provided values
791
+ headers = _substitute_placeholders(headers_template, settings)
792
+
793
+ logger.info(f"[MCP Config] Discovering tools from {server_name} at {url}")
794
+
795
+ try:
796
+ from ..utils.mcp_client import McpClient
797
+ import asyncio
798
+
799
+ async def _discover():
800
+ client = McpClient(
801
+ url=url,
802
+ headers=headers,
803
+ timeout=timeout
804
+ )
805
+ tools = []
806
+ args_schemas = {}
807
+ try:
808
+ async with client:
809
+ await client.initialize()
810
+ discovered = await client.list_tools()
811
+ for tool in discovered:
812
+ tool_name = tool.get('name', 'unknown')
813
+ input_schema = tool.get('inputSchema')
814
+ tools.append({
815
+ 'name': tool_name,
816
+ 'description': tool.get('description', ''),
817
+ 'inputSchema': input_schema, # Include schema in tool object
818
+ })
819
+ # Also build args_schemas dict for API format
820
+ if input_schema:
821
+ args_schemas[tool_name] = input_schema
822
+ return {'tools': tools, 'args_schemas': args_schemas}
823
+ except Exception as e:
824
+ logger.error(f"[MCP Config] Tool discovery failed for {server_name}: {e}")
825
+ raise
826
+
827
+ # Run async discovery
828
+ try:
829
+ loop = asyncio.get_event_loop()
830
+ if loop.is_running():
831
+ import concurrent.futures
832
+ with concurrent.futures.ThreadPoolExecutor() as executor:
833
+ future = executor.submit(asyncio.run, _discover())
834
+ result = future.result(timeout=timeout)
835
+ else:
836
+ result = loop.run_until_complete(_discover())
837
+ except RuntimeError:
838
+ result = asyncio.run(_discover())
839
+
840
+ logger.info(f"[MCP Config] Discovered {len(result.get('tools', []))} tools from {server_name}")
841
+ return result
842
+
843
+ except Exception as e:
844
+ error_msg = f"Failed to discover tools: {str(e)}"
845
+ logger.error(f"[MCP Config] {error_msg}")
846
+ return error_msg
847
+
848
+ return check_connection
849
+
850
+
851
+ def get_mcp_config_toolkit_schemas() -> List[BaseModel]:
852
+ """
853
+ Get toolkit configuration schemas for all configured MCP servers.
854
+
855
+ Returns Pydantic models that the platform can use to display available toolkits.
856
+ Each configured MCP server appears as a separate toolkit in the UI.
857
+ """
858
+ from pydantic import create_model, ConfigDict, SecretStr
859
+ from typing import Literal
860
+
861
+ schemas = []
862
+ servers = get_all_mcp_server_configs()
863
+
864
+ for server_name, config in servers.items():
865
+ server_type = config.get('type', 'stdio')
866
+ server_schema = config.get('config_schema', {'properties': {}})
867
+ description = config.get('description', f'MCP server: {server_name}')
868
+ display_name = config.get('display_name', server_name.replace('_', ' ').title())
869
+
870
+ # Attempt to discover tools from the MCP server
871
+ discovered_tools = []
872
+ tools_discovery_status = 'pending' # 'discovered', 'auth_required', 'failed', 'pending'
873
+
874
+ if server_type == 'http':
875
+ url = config.get('url', '')
876
+ # Only attempt discovery if no auth is required (no placeholder in headers)
877
+ headers = config.get('headers', {})
878
+ has_auth_placeholder = any('{' in str(v) for v in headers.values()) if headers else False
879
+
880
+ if not has_auth_placeholder and url:
881
+ # No auth placeholders, try to discover tools
882
+ logger.info(f"[MCP Config] Attempting tool discovery for {server_name} (no auth required)")
883
+ discovered_tools = _discover_tools_for_http_server(url, headers)
884
+ tools_discovery_status = 'discovered' if discovered_tools else 'failed'
885
+ else:
886
+ # Auth is required, can't discover without credentials
887
+ logger.info(f"[MCP Config] Skipping tool discovery for {server_name} (auth required)")
888
+ tools_discovery_status = 'auth_required'
889
+
890
+ # Use statically configured tools as fallback
891
+ static_tools = config.get('tools', [])
892
+ if not discovered_tools and static_tools:
893
+ discovered_tools = static_tools
894
+ if tools_discovery_status != 'auth_required':
895
+ tools_discovery_status = 'static'
896
+
897
+ # Get tool names for field options
898
+ tool_names = [t.get('name', '') for t in discovered_tools if t.get('name')]
899
+
900
+ # Build field definitions for the Pydantic model
901
+ field_definitions = {
902
+ # Hidden field to identify the server
903
+ 'server_name': (
904
+ str,
905
+ Field(
906
+ default=server_name,
907
+ description="MCP server name",
908
+ json_schema_extra={'hidden': True}
909
+ )
910
+ ),
911
+ }
912
+
913
+ # Add tool selection field only if tools were discovered
914
+ # For auth-required servers, tools are discovered via check_connection after credentials are provided
915
+ if tool_names:
916
+ field_definitions['selected_tools'] = (
917
+ Optional[List[str]],
918
+ Field(
919
+ default=None,
920
+ description="Specific tools to enable (empty = all tools)",
921
+ json_schema_extra={
922
+ 'tooltip': 'Leave empty to enable all tools from this MCP server',
923
+ 'ui:widget': 'multiselect',
924
+ 'options': tool_names
925
+ }
926
+ )
927
+ )
928
+
929
+ # Add user-configurable fields from config_schema
930
+ for param_name, param_config in server_schema.get('properties', {}).items():
931
+ field_type, field_info = _create_pydantic_field(param_name, param_config)
932
+ field_definitions[param_name] = (field_type, field_info)
933
+
934
+ # Determine categories based on server type
935
+ # Use 'mcp' as the UI category for visibility in toolkits page
936
+ # Keep MCP-specific info in extra_categories for search/filtering
937
+ if server_type == 'stdio':
938
+ categories = ['mcp']
939
+ extra_categories = ['local', 'subprocess', config.get('runtime', 'npm')]
940
+ else:
941
+ categories = ['mcp']
942
+ extra_categories = ['remote', 'http', 'sse']
943
+
944
+ # Create the Pydantic model for this MCP server
945
+ model = create_model(
946
+ f'mcp_{server_name}',
947
+ **field_definitions,
948
+ __config__=ConfigDict(
949
+ json_schema_extra={
950
+ 'metadata': {
951
+ 'label': display_name,
952
+ 'icon_url': config.get('icon_url'),
953
+ 'categories': categories,
954
+ 'extra_categories': extra_categories,
955
+ 'description': description,
956
+ # Section for configuration registration (toolkits page)
957
+ 'section': 'toolkits',
958
+ # Custom metadata for MCP config
959
+ 'mcp_server_type': server_type,
960
+ 'mcp_server_name': server_name,
961
+ 'stateful': config.get('stateful', False),
962
+ # Tool discovery results - enables UI to show available tools
963
+ 'tools': discovered_tools,
964
+ 'tools_discovery_status': tools_discovery_status,
965
+ # Custom button label for tool discovery
966
+ 'check_connection_label': 'Load Tools',
967
+ }
968
+ }
969
+ )
970
+ )
971
+
972
+ # Attach check_connection method for tool discovery
973
+ if server_type == 'http':
974
+ model.check_connection = staticmethod(_create_check_connection_for_http(server_name, config))
975
+ elif server_type == 'stdio':
976
+ model.check_connection = staticmethod(_create_check_connection_for_stdio(server_name, config))
977
+
978
+ schemas.append(model)
979
+
980
+ return schemas
981
+
982
+
983
+ def _create_pydantic_field(param_name: str, param_config: Dict[str, Any]) -> tuple:
984
+ """Create a Pydantic field definition from config schema parameter."""
985
+ from pydantic import SecretStr
986
+
987
+ param_type = param_config.get('type', 'string')
988
+ is_required = param_config.get('required', False)
989
+ is_secret = param_config.get('secret', False)
990
+ default_value = param_config.get('default')
991
+ description = param_config.get('description', '')
992
+
993
+ # Map config types to Python types
994
+ if param_type == 'string':
995
+ if is_secret:
996
+ python_type = SecretStr
997
+ else:
998
+ python_type = str
999
+ elif param_type == 'integer':
1000
+ python_type = int
1001
+ elif param_type == 'number':
1002
+ python_type = float
1003
+ elif param_type == 'boolean':
1004
+ python_type = bool
1005
+ elif param_type == 'array':
1006
+ item_type = param_config.get('items', {}).get('type', 'string')
1007
+ if item_type == 'string':
1008
+ python_type = List[str]
1009
+ elif item_type == 'integer':
1010
+ python_type = List[int]
1011
+ else:
1012
+ python_type = List[Any]
1013
+ else:
1014
+ python_type = str
1015
+
1016
+ # Make optional if not required
1017
+ if not is_required:
1018
+ python_type = Optional[python_type]
1019
+
1020
+ # Build field kwargs
1021
+ field_kwargs = {
1022
+ 'description': description,
1023
+ }
1024
+
1025
+ if default_value is not None:
1026
+ field_kwargs['default'] = default_value
1027
+ elif not is_required:
1028
+ field_kwargs['default'] = None
1029
+
1030
+ # Add extra schema info
1031
+ json_schema_extra = {}
1032
+ if is_secret:
1033
+ json_schema_extra['secret'] = True
1034
+ json_schema_extra['format'] = 'password'
1035
+ if param_config.get('tooltip'):
1036
+ json_schema_extra['tooltip'] = param_config['tooltip']
1037
+ if param_config.get('example'):
1038
+ json_schema_extra['example'] = param_config['example']
1039
+
1040
+ if json_schema_extra:
1041
+ field_kwargs['json_schema_extra'] = json_schema_extra
1042
+
1043
+ return python_type, Field(**field_kwargs)
1044
+
1045
+
1046
+ # Backward compatibility aliases
1047
+ McpStdioToolkit = McpConfigToolkit
1048
+ get_mcp_stdio_toolkit_schemas = get_mcp_config_toolkit_schemas