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.
- alita_sdk/cli/agent_executor.py +2 -1
- alita_sdk/cli/agent_loader.py +34 -4
- alita_sdk/cli/agents.py +433 -203
- alita_sdk/community/__init__.py +8 -4
- alita_sdk/configurations/__init__.py +1 -0
- alita_sdk/configurations/openapi.py +323 -0
- alita_sdk/runtime/clients/client.py +165 -7
- alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
- alita_sdk/runtime/langchain/assistant.py +61 -11
- alita_sdk/runtime/langchain/constants.py +419 -171
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +4 -2
- alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +5 -2
- alita_sdk/runtime/langchain/langraph_agent.py +108 -23
- alita_sdk/runtime/langchain/utils.py +76 -14
- alita_sdk/runtime/skills/__init__.py +91 -0
- alita_sdk/runtime/skills/callbacks.py +498 -0
- alita_sdk/runtime/skills/discovery.py +540 -0
- alita_sdk/runtime/skills/executor.py +610 -0
- alita_sdk/runtime/skills/input_builder.py +371 -0
- alita_sdk/runtime/skills/models.py +330 -0
- alita_sdk/runtime/skills/registry.py +355 -0
- alita_sdk/runtime/skills/skill_runner.py +330 -0
- alita_sdk/runtime/toolkits/__init__.py +5 -0
- alita_sdk/runtime/toolkits/artifact.py +2 -1
- alita_sdk/runtime/toolkits/mcp.py +6 -3
- alita_sdk/runtime/toolkits/mcp_config.py +1048 -0
- alita_sdk/runtime/toolkits/skill_router.py +238 -0
- alita_sdk/runtime/toolkits/tools.py +139 -10
- alita_sdk/runtime/toolkits/vectorstore.py +1 -1
- alita_sdk/runtime/tools/__init__.py +3 -1
- alita_sdk/runtime/tools/artifact.py +15 -0
- alita_sdk/runtime/tools/data_analysis.py +183 -0
- alita_sdk/runtime/tools/llm.py +260 -73
- alita_sdk/runtime/tools/loop.py +3 -1
- alita_sdk/runtime/tools/loop_output.py +3 -1
- alita_sdk/runtime/tools/mcp_server_tool.py +6 -3
- alita_sdk/runtime/tools/router.py +2 -4
- alita_sdk/runtime/tools/sandbox.py +9 -6
- alita_sdk/runtime/tools/skill_router.py +776 -0
- alita_sdk/runtime/tools/tool.py +3 -1
- alita_sdk/runtime/tools/vectorstore.py +7 -2
- alita_sdk/runtime/tools/vectorstore_base.py +7 -2
- alita_sdk/runtime/utils/constants.py +5 -1
- alita_sdk/runtime/utils/mcp_client.py +1 -1
- alita_sdk/runtime/utils/mcp_sse_client.py +1 -1
- alita_sdk/runtime/utils/toolkit_utils.py +2 -0
- alita_sdk/tools/__init__.py +44 -2
- alita_sdk/tools/ado/repos/__init__.py +26 -8
- alita_sdk/tools/ado/repos/repos_wrapper.py +78 -52
- alita_sdk/tools/ado/test_plan/__init__.py +3 -2
- alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +23 -1
- alita_sdk/tools/ado/utils.py +1 -18
- alita_sdk/tools/ado/wiki/__init__.py +2 -1
- alita_sdk/tools/ado/wiki/ado_wrapper.py +23 -1
- alita_sdk/tools/ado/work_item/__init__.py +3 -2
- alita_sdk/tools/ado/work_item/ado_wrapper.py +56 -3
- alita_sdk/tools/advanced_jira_mining/__init__.py +2 -1
- alita_sdk/tools/aws/delta_lake/__init__.py +2 -1
- alita_sdk/tools/azure_ai/search/__init__.py +2 -1
- alita_sdk/tools/azure_ai/search/api_wrapper.py +1 -1
- alita_sdk/tools/base_indexer_toolkit.py +51 -30
- alita_sdk/tools/bitbucket/__init__.py +2 -1
- alita_sdk/tools/bitbucket/api_wrapper.py +1 -1
- alita_sdk/tools/bitbucket/cloud_api_wrapper.py +3 -3
- alita_sdk/tools/browser/__init__.py +1 -1
- alita_sdk/tools/carrier/__init__.py +1 -1
- alita_sdk/tools/chunkers/code/treesitter/treesitter.py +37 -13
- alita_sdk/tools/cloud/aws/__init__.py +2 -1
- alita_sdk/tools/cloud/azure/__init__.py +2 -1
- alita_sdk/tools/cloud/gcp/__init__.py +2 -1
- alita_sdk/tools/cloud/k8s/__init__.py +2 -1
- alita_sdk/tools/code/linter/__init__.py +2 -1
- alita_sdk/tools/code/sonar/__init__.py +2 -1
- alita_sdk/tools/code_indexer_toolkit.py +19 -2
- alita_sdk/tools/confluence/__init__.py +7 -6
- alita_sdk/tools/confluence/api_wrapper.py +7 -8
- alita_sdk/tools/confluence/loader.py +4 -2
- alita_sdk/tools/custom_open_api/__init__.py +2 -1
- alita_sdk/tools/elastic/__init__.py +2 -1
- alita_sdk/tools/elitea_base.py +28 -9
- alita_sdk/tools/figma/__init__.py +52 -6
- alita_sdk/tools/figma/api_wrapper.py +1158 -123
- alita_sdk/tools/figma/figma_client.py +73 -0
- alita_sdk/tools/figma/toon_tools.py +2748 -0
- alita_sdk/tools/github/__init__.py +2 -1
- alita_sdk/tools/github/github_client.py +56 -92
- alita_sdk/tools/github/schemas.py +4 -4
- alita_sdk/tools/gitlab/__init__.py +2 -1
- alita_sdk/tools/gitlab/api_wrapper.py +118 -38
- alita_sdk/tools/gitlab_org/__init__.py +2 -1
- alita_sdk/tools/gitlab_org/api_wrapper.py +60 -62
- alita_sdk/tools/google/bigquery/__init__.py +2 -1
- alita_sdk/tools/google_places/__init__.py +2 -1
- alita_sdk/tools/jira/__init__.py +2 -1
- alita_sdk/tools/keycloak/__init__.py +2 -1
- alita_sdk/tools/localgit/__init__.py +2 -1
- alita_sdk/tools/memory/__init__.py +1 -1
- alita_sdk/tools/ocr/__init__.py +2 -1
- alita_sdk/tools/openapi/__init__.py +490 -118
- alita_sdk/tools/openapi/api_wrapper.py +1368 -0
- alita_sdk/tools/openapi/tool.py +20 -0
- alita_sdk/tools/pandas/__init__.py +11 -5
- alita_sdk/tools/pandas/api_wrapper.py +38 -25
- alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
- alita_sdk/tools/postman/__init__.py +2 -1
- alita_sdk/tools/pptx/__init__.py +2 -1
- alita_sdk/tools/qtest/__init__.py +21 -2
- alita_sdk/tools/qtest/api_wrapper.py +430 -13
- alita_sdk/tools/rally/__init__.py +2 -1
- alita_sdk/tools/rally/api_wrapper.py +1 -1
- alita_sdk/tools/report_portal/__init__.py +2 -1
- alita_sdk/tools/salesforce/__init__.py +2 -1
- alita_sdk/tools/servicenow/__init__.py +11 -10
- alita_sdk/tools/servicenow/api_wrapper.py +1 -1
- alita_sdk/tools/sharepoint/__init__.py +2 -1
- alita_sdk/tools/sharepoint/api_wrapper.py +2 -2
- alita_sdk/tools/slack/__init__.py +3 -2
- alita_sdk/tools/slack/api_wrapper.py +2 -2
- alita_sdk/tools/sql/__init__.py +3 -2
- alita_sdk/tools/testio/__init__.py +2 -1
- alita_sdk/tools/testrail/__init__.py +2 -1
- alita_sdk/tools/utils/content_parser.py +77 -3
- alita_sdk/tools/utils/text_operations.py +163 -71
- alita_sdk/tools/xray/__init__.py +3 -2
- alita_sdk/tools/yagmail/__init__.py +2 -1
- alita_sdk/tools/zephyr/__init__.py +2 -1
- alita_sdk/tools/zephyr_enterprise/__init__.py +2 -1
- alita_sdk/tools/zephyr_essential/__init__.py +2 -1
- alita_sdk/tools/zephyr_scale/__init__.py +3 -2
- alita_sdk/tools/zephyr_scale/api_wrapper.py +2 -2
- alita_sdk/tools/zephyr_squad/__init__.py +2 -1
- {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/METADATA +7 -6
- {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/RECORD +137 -119
- {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/entry_points.txt +0 -0
- {alita_sdk-0.3.532.dist-info → alita_sdk-0.3.602.dist-info}/licenses/LICENSE +0 -0
- {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
|