Skip to content

Commit 5e9cd61

Browse files
Merge pull request #1220 from escapade-mckv/bug-fixes
Bug fixes
2 parents 7553271 + 07c088a commit 5e9cd61

File tree

25 files changed

+785
-193
lines changed

25 files changed

+785
-193
lines changed

backend/agent/prompt.py

Lines changed: 159 additions & 32 deletions
Large diffs are not rendered by default.

backend/agent/run.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ async def register_mcp_tools(self, agent_config: dict) -> Optional[MCPToolWrappe
202202
"schema": schema
203203
}
204204

205+
logger.info(f"⚡ Registered {len(updated_schemas)} MCP tools (Redis cache enabled)")
205206
return mcp_wrapper_instance
206207
except Exception as e:
207208
logger.error(f"Failed to initialize MCP tools: {e}")

backend/agent/tools/agent_builder_tools/agent_config_tool.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,16 @@ async def get_current_agent_config(self) -> ToolResult:
314314
"updated_at": agent_data.get("updated_at"),
315315
"current_version": agent_config.get("version_name", "v1") if version_data else "No version data"
316316
}
317-
318-
tools_count = len([t for t, cfg in config_summary["agentpress_tools"].items() if cfg.get("enabled")])
317+
318+
enabled_tools = []
319+
for tool_name, tool_config in config_summary["agentpress_tools"].items():
320+
if isinstance(tool_config, bool):
321+
if tool_config:
322+
enabled_tools.append(tool_name)
323+
elif isinstance(tool_config, dict):
324+
if tool_config.get("enabled", False):
325+
enabled_tools.append(tool_name)
326+
tools_count = len(enabled_tools)
319327
mcps_count = len(config_summary["configured_mcps"])
320328
custom_mcps_count = len(config_summary["custom_mcps"])
321329

backend/agent/tools/agent_builder_tools/credential_profile_tool.py

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,15 @@ async def create_credential_profile(
140140

141141
if result.connected_account.redirect_url:
142142
response_data["connection_link"] = result.connected_account.redirect_url
143-
response_data["instructions"] = f"🔗 **IMPORTANT: Please visit the connection link to authenticate your {result.toolkit.name} account with this profile. After connecting, you'll be able to use {result.toolkit.name} tools in your agent.**"
143+
# Include both the toolkit name and slug in a parseable format
144+
# Format: [toolkit:slug:name] to help frontend identify the service accurately
145+
response_data["instructions"] = f"""🔗 **{result.toolkit.name} Authentication Required**
146+
147+
Please authenticate your {result.toolkit.name} account by clicking the link below:
148+
149+
[toolkit:{toolkit_slug}:{result.toolkit.name}] Authentication: {result.connected_account.redirect_url}
150+
151+
After connecting, you'll be able to use {result.toolkit.name} tools in your agent."""
144152
else:
145153
response_data["instructions"] = f"This {result.toolkit.name} profile has been created and is ready to use."
146154

@@ -256,12 +264,48 @@ async def configure_profile_for_agent(
256264
change_description=f"Configured {display_name or profile.display_name} with {len(enabled_tools)} tools"
257265
)
258266

267+
# Dynamically register the MCP tools in the current runtime
268+
try:
269+
from agent.tools.mcp_tool_wrapper import MCPToolWrapper
270+
271+
mcp_config_for_wrapper = {
272+
'name': profile.toolkit_name,
273+
'qualifiedName': f"composio.{profile.toolkit_slug}",
274+
'config': {
275+
'profile_id': profile_id,
276+
'toolkit_slug': profile.toolkit_slug,
277+
'mcp_qualified_name': profile.mcp_qualified_name
278+
},
279+
'enabledTools': enabled_tools,
280+
'instructions': '',
281+
'isCustom': True,
282+
'customType': 'composio'
283+
}
284+
285+
mcp_wrapper_instance = MCPToolWrapper(mcp_configs=[mcp_config_for_wrapper])
286+
await mcp_wrapper_instance.initialize_and_register_tools()
287+
updated_schemas = mcp_wrapper_instance.get_schemas()
288+
289+
for method_name, schema_list in updated_schemas.items():
290+
for schema in schema_list:
291+
self.thread_manager.tool_registry.tools[method_name] = {
292+
"instance": mcp_wrapper_instance,
293+
"schema": schema
294+
}
295+
logger.info(f"Dynamically registered MCP tool: {method_name}")
296+
297+
logger.info(f"Successfully registered {len(updated_schemas)} MCP tools dynamically for {profile.toolkit_name}")
298+
299+
except Exception as e:
300+
logger.warning(f"Could not dynamically register MCP tools in current runtime: {str(e)}. Tools will be available on next agent run.")
301+
259302
return self.success_response({
260-
"message": f"Profile '{profile.profile_name}' updated with {len(enabled_tools)} tools",
303+
"message": f"Profile '{profile.profile_name}' configured with {len(enabled_tools)} tools and registered in current runtime",
261304
"enabled_tools": enabled_tools,
262305
"total_tools": len(enabled_tools),
263306
"version_id": new_version.version_id,
264-
"version_name": new_version.version_name
307+
"version_name": new_version.version_name,
308+
"runtime_registration": "success"
265309
})
266310

267311
except Exception as e:

backend/agent/tools/agent_builder_tools/workflow_tool.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,13 @@ async def _get_available_tools_for_agent(self) -> List[str]:
5252

5353
agentpress_tools = agent_config.get('agentpress_tools', {})
5454
for tool_key, tool_names in tool_mapping.items():
55-
if agentpress_tools.get(tool_key, {}).get('enabled', False):
56-
available_tools.extend(tool_names)
55+
tool_config = agentpress_tools.get(tool_key, False)
56+
if isinstance(tool_config, bool):
57+
if tool_config:
58+
available_tools.extend(tool_names)
59+
elif isinstance(tool_config, dict):
60+
if tool_config.get('enabled', False):
61+
available_tools.extend(tool_names)
5762

5863
configured_mcps = agent_config.get('configured_mcps', [])
5964
for mcp in configured_mcps:

backend/agent/tools/mcp_tool_wrapper.py

Lines changed: 196 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,120 @@
33
from mcp_module import mcp_service
44
from utils.logger import logger
55
import inspect
6+
import asyncio
7+
import time
8+
import hashlib
9+
import json
610
from agent.tools.utils.mcp_connection_manager import MCPConnectionManager
711
from agent.tools.utils.custom_mcp_handler import CustomMCPHandler
812
from agent.tools.utils.dynamic_tool_builder import DynamicToolBuilder
913
from agent.tools.utils.mcp_tool_executor import MCPToolExecutor
14+
from services import redis as redis_service
1015

1116

17+
class MCPSchemaRedisCache:
18+
def __init__(self, ttl_seconds: int = 3600, key_prefix: str = "mcp_schema:"):
19+
self._ttl = ttl_seconds
20+
self._key_prefix = key_prefix
21+
self._redis_client = None
22+
23+
async def _ensure_redis(self):
24+
if not self._redis_client:
25+
try:
26+
self._redis_client = await redis_service.get_client()
27+
except Exception as e:
28+
logger.warning(f"Redis not available for MCP cache: {e}")
29+
return False
30+
return True
31+
32+
def _get_cache_key(self, config: Dict[str, Any]) -> str:
33+
config_str = json.dumps(config, sort_keys=True)
34+
config_hash = hashlib.md5(config_str.encode()).hexdigest()
35+
return f"{self._key_prefix}{config_hash}"
36+
37+
async def get(self, config: Dict[str, Any]) -> Optional[Dict[str, Any]]:
38+
if not await self._ensure_redis():
39+
return None
40+
41+
try:
42+
key = self._get_cache_key(config)
43+
cached_data = await self._redis_client.get(key)
44+
45+
if cached_data:
46+
logger.debug(f"⚡ Redis cache hit for MCP: {config.get('name', config.get('qualifiedName', 'Unknown'))}")
47+
return json.loads(cached_data)
48+
else:
49+
logger.debug(f"Redis cache miss for MCP: {config.get('name', config.get('qualifiedName', 'Unknown'))}")
50+
return None
51+
52+
except Exception as e:
53+
logger.warning(f"Error reading from Redis cache: {e}")
54+
return None
55+
56+
async def set(self, config: Dict[str, Any], data: Dict[str, Any]):
57+
if not await self._ensure_redis():
58+
return
59+
60+
try:
61+
key = self._get_cache_key(config)
62+
serialized_data = json.dumps(data)
63+
64+
await self._redis_client.setex(key, self._ttl, serialized_data)
65+
logger.debug(f"✅ Cached MCP schema in Redis for {config.get('name', config.get('qualifiedName', 'Unknown'))} (TTL: {self._ttl}s)")
66+
67+
except Exception as e:
68+
logger.warning(f"Error writing to Redis cache: {e}")
69+
70+
async def clear_pattern(self, pattern: Optional[str] = None):
71+
if not await self._ensure_redis():
72+
return
73+
try:
74+
if pattern:
75+
search_pattern = f"{self._key_prefix}{pattern}*"
76+
else:
77+
search_pattern = f"{self._key_prefix}*"
78+
79+
keys = []
80+
async for key in self._redis_client.scan_iter(match=search_pattern):
81+
keys.append(key)
82+
83+
if keys:
84+
await self._redis_client.delete(*keys)
85+
logger.info(f"Cleared {len(keys)} MCP schema cache entries from Redis")
86+
87+
except Exception as e:
88+
logger.warning(f"Error clearing Redis cache: {e}")
89+
90+
async def get_stats(self) -> Dict[str, Any]:
91+
if not await self._ensure_redis():
92+
return {"available": False}
93+
try:
94+
count = 0
95+
async for _ in self._redis_client.scan_iter(match=f"{self._key_prefix}*"):
96+
count += 1
97+
98+
return {
99+
"available": True,
100+
"cached_schemas": count,
101+
"ttl_seconds": self._ttl,
102+
"key_prefix": self._key_prefix
103+
}
104+
except Exception as e:
105+
logger.warning(f"Error getting cache stats: {e}")
106+
return {"available": False, "error": str(e)}
107+
108+
109+
_redis_cache = MCPSchemaRedisCache(ttl_seconds=3600)
110+
12111
class MCPToolWrapper(Tool):
13-
def __init__(self, mcp_configs: Optional[List[Dict[str, Any]]] = None):
112+
def __init__(self, mcp_configs: Optional[List[Dict[str, Any]]] = None, use_cache: bool = True):
14113
self.mcp_manager = mcp_service
15114
self.mcp_configs = mcp_configs or []
16115
self._initialized = False
17116
self._schemas: Dict[str, List[ToolSchema]] = {}
18117
self._dynamic_tools = {}
19118
self._custom_tools = {}
119+
self.use_cache = use_cache
20120

21121
self.connection_manager = MCPConnectionManager()
22122
self.custom_handler = CustomMCPHandler(self.connection_manager)
@@ -32,23 +132,109 @@ async def _ensure_initialized(self):
32132
self._initialized = True
33133

34134
async def _initialize_servers(self):
135+
start_time = time.time()
136+
35137
standard_configs = [cfg for cfg in self.mcp_configs if not cfg.get('isCustom', False)]
36138
custom_configs = [cfg for cfg in self.mcp_configs if cfg.get('isCustom', False)]
37139

140+
cached_configs = []
141+
cached_tools_data = []
142+
143+
initialization_tasks = []
144+
38145
if standard_configs:
39-
await self._initialize_standard_servers(standard_configs)
146+
for config in standard_configs:
147+
if self.use_cache:
148+
cached_data = await _redis_cache.get(config)
149+
if cached_data:
150+
cached_configs.append(config.get('qualifiedName', 'Unknown'))
151+
cached_tools_data.append(cached_data)
152+
continue
153+
154+
task = self._initialize_single_standard_server(config)
155+
initialization_tasks.append(('standard', config, task))
40156

41157
if custom_configs:
42-
await self.custom_handler.initialize_custom_mcps(custom_configs)
158+
for config in custom_configs:
159+
if self.use_cache:
160+
cached_data = await _redis_cache.get(config)
161+
if cached_data:
162+
cached_configs.append(config.get('name', 'Unknown'))
163+
cached_tools_data.append(cached_data)
164+
continue
165+
166+
task = self._initialize_single_custom_mcp(config)
167+
initialization_tasks.append(('custom', config, task))
168+
169+
if cached_tools_data:
170+
logger.info(f"⚡ Loaded {len(cached_configs)} MCP schemas from Redis cache: {', '.join(cached_configs)}")
171+
for cached_data in cached_tools_data:
172+
try:
173+
if cached_data.get('type') == 'standard':
174+
logger.debug("Standard MCP tools found in cache but require connection to restore")
175+
elif cached_data.get('type') == 'custom':
176+
custom_tools = cached_data.get('tools', {})
177+
if custom_tools:
178+
self.custom_handler.custom_tools.update(custom_tools)
179+
logger.debug(f"Restored {len(custom_tools)} custom tools from cache")
180+
except Exception as e:
181+
logger.warning(f"Failed to restore cached tools: {e}")
182+
183+
if initialization_tasks:
184+
logger.info(f"🚀 Initializing {len(initialization_tasks)} MCP servers in parallel (cache enabled: {self.use_cache})...")
185+
186+
tasks = [task for _, _, task in initialization_tasks]
187+
results = await asyncio.gather(*tasks, return_exceptions=True)
188+
189+
successful = 0
190+
failed = 0
191+
192+
for i, result in enumerate(results):
193+
task_type, config, _ = initialization_tasks[i]
194+
if isinstance(result, Exception):
195+
failed += 1
196+
config_name = config.get('name', config.get('qualifiedName', 'Unknown'))
197+
logger.error(f"Failed to initialize MCP server '{config_name}': {result}")
198+
else:
199+
successful += 1
200+
if self.use_cache and result:
201+
await _redis_cache.set(config, result)
202+
203+
elapsed_time = time.time() - start_time
204+
logger.info(f"⚡ MCP initialization completed in {elapsed_time:.2f}s - {successful} successful, {failed} failed, {len(cached_configs)} from cache")
205+
else:
206+
if cached_configs:
207+
elapsed_time = time.time() - start_time
208+
logger.info(f"⚡ All {len(cached_configs)} MCP schemas loaded from Redis cache in {elapsed_time:.2f}s - instant startup!")
209+
else:
210+
logger.info("No MCP servers to initialize")
211+
212+
async def _initialize_single_standard_server(self, config: Dict[str, Any]):
213+
try:
214+
logger.debug(f"Connecting to standard MCP server: {config['qualifiedName']}")
215+
await self.mcp_manager.connect_server(config)
216+
logger.debug(f"✓ Connected to MCP server: {config['qualifiedName']}")
217+
218+
tools_info = self.mcp_manager.get_all_tools_openapi()
219+
return {'tools': tools_info, 'type': 'standard', 'timestamp': time.time()}
220+
except Exception as e:
221+
logger.error(f"✗ Failed to connect to MCP server {config['qualifiedName']}: {e}")
222+
raise e
223+
224+
async def _initialize_single_custom_mcp(self, config: Dict[str, Any]):
225+
try:
226+
logger.debug(f"Initializing custom MCP: {config.get('name', 'Unknown')}")
227+
await self.custom_handler._initialize_single_custom_mcp(config)
228+
logger.debug(f"✓ Initialized custom MCP: {config.get('name', 'Unknown')}")
229+
230+
custom_tools = self.custom_handler.get_custom_tools()
231+
return {'tools': custom_tools, 'type': 'custom', 'timestamp': time.time()}
232+
except Exception as e:
233+
logger.error(f"✗ Failed to initialize custom MCP {config.get('name', 'Unknown')}: {e}")
234+
raise e
43235

44236
async def _initialize_standard_servers(self, standard_configs: List[Dict[str, Any]]):
45-
for config in standard_configs:
46-
try:
47-
logger.info(f"Attempting to connect to MCP server: {config['qualifiedName']}")
48-
await self.mcp_manager.connect_server(config)
49-
logger.info(f"Successfully connected to MCP server: {config['qualifiedName']}")
50-
except Exception as e:
51-
logger.error(f"Failed to connect to MCP server {config['qualifiedName']}: {e}")
237+
pass
52238

53239
async def _create_dynamic_tools(self):
54240
try:
@@ -77,7 +263,6 @@ async def _create_dynamic_tools(self):
77263

78264
logger.info(f"Created {len(self._dynamic_tools)} dynamic MCP tool methods")
79265

80-
# Re-register schemas to pick up the dynamic methods
81266
self._register_schemas()
82267
logger.info(f"Re-registered schemas after creating dynamic tools - total: {len(self._schemas)}")
83268

backend/agent/tools/utils/custom_mcp_handler.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,33 @@
1212
class CustomMCPHandler:
1313
def __init__(self, connection_manager: MCPConnectionManager):
1414
self.connection_manager = connection_manager
15-
self.custom_tools: Dict[str, Dict[str, Any]] = {}
15+
self.custom_tools = {}
1616

1717
async def initialize_custom_mcps(self, custom_configs: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
18+
initialization_tasks = []
19+
1820
for config in custom_configs:
19-
try:
20-
await self._initialize_single_custom_mcp(config)
21-
except Exception as e:
22-
logger.error(f"Failed to initialize custom MCP {config.get('name', 'Unknown')}: {e}")
23-
continue
21+
task = self._initialize_single_custom_mcp_safe(config)
22+
initialization_tasks.append(task)
23+
24+
if initialization_tasks:
25+
logger.info(f"Initializing {len(initialization_tasks)} custom MCPs in parallel...")
26+
results = await asyncio.gather(*initialization_tasks, return_exceptions=True)
27+
28+
for i, result in enumerate(results):
29+
if isinstance(result, Exception):
30+
config_name = custom_configs[i].get('name', 'Unknown')
31+
logger.error(f"Failed to initialize custom MCP {config_name}: {result}")
2432

2533
return self.custom_tools
2634

35+
async def _initialize_single_custom_mcp_safe(self, config: Dict[str, Any]):
36+
try:
37+
await self._initialize_single_custom_mcp(config)
38+
except Exception as e:
39+
logger.error(f"Failed to initialize custom MCP {config.get('name', 'Unknown')}: {e}")
40+
return e
41+
2742
async def _initialize_single_custom_mcp(self, config: Dict[str, Any]):
2843
custom_type = config.get('customType', 'sse')
2944
server_config = config.get('config', {})

0 commit comments

Comments
 (0)