webagents 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. webagents/__init__.py +18 -0
  2. webagents/__main__.py +55 -0
  3. webagents/agents/__init__.py +13 -0
  4. webagents/agents/core/__init__.py +19 -0
  5. webagents/agents/core/base_agent.py +1834 -0
  6. webagents/agents/core/handoffs.py +293 -0
  7. webagents/agents/handoffs/__init__.py +0 -0
  8. webagents/agents/interfaces/__init__.py +0 -0
  9. webagents/agents/lifecycle/__init__.py +0 -0
  10. webagents/agents/skills/__init__.py +109 -0
  11. webagents/agents/skills/base.py +136 -0
  12. webagents/agents/skills/core/__init__.py +8 -0
  13. webagents/agents/skills/core/guardrails/__init__.py +0 -0
  14. webagents/agents/skills/core/llm/__init__.py +0 -0
  15. webagents/agents/skills/core/llm/anthropic/__init__.py +1 -0
  16. webagents/agents/skills/core/llm/litellm/__init__.py +10 -0
  17. webagents/agents/skills/core/llm/litellm/skill.py +538 -0
  18. webagents/agents/skills/core/llm/openai/__init__.py +1 -0
  19. webagents/agents/skills/core/llm/xai/__init__.py +1 -0
  20. webagents/agents/skills/core/mcp/README.md +375 -0
  21. webagents/agents/skills/core/mcp/__init__.py +15 -0
  22. webagents/agents/skills/core/mcp/skill.py +731 -0
  23. webagents/agents/skills/core/memory/__init__.py +11 -0
  24. webagents/agents/skills/core/memory/long_term_memory/__init__.py +10 -0
  25. webagents/agents/skills/core/memory/long_term_memory/memory_skill.py +639 -0
  26. webagents/agents/skills/core/memory/short_term_memory/__init__.py +9 -0
  27. webagents/agents/skills/core/memory/short_term_memory/skill.py +341 -0
  28. webagents/agents/skills/core/memory/vector_memory/skill.py +447 -0
  29. webagents/agents/skills/core/planning/__init__.py +9 -0
  30. webagents/agents/skills/core/planning/planner.py +343 -0
  31. webagents/agents/skills/ecosystem/__init__.py +0 -0
  32. webagents/agents/skills/ecosystem/crewai/__init__.py +1 -0
  33. webagents/agents/skills/ecosystem/database/__init__.py +1 -0
  34. webagents/agents/skills/ecosystem/filesystem/__init__.py +0 -0
  35. webagents/agents/skills/ecosystem/google/__init__.py +0 -0
  36. webagents/agents/skills/ecosystem/google/calendar/__init__.py +6 -0
  37. webagents/agents/skills/ecosystem/google/calendar/skill.py +306 -0
  38. webagents/agents/skills/ecosystem/n8n/__init__.py +0 -0
  39. webagents/agents/skills/ecosystem/openai_agents/__init__.py +0 -0
  40. webagents/agents/skills/ecosystem/web/__init__.py +0 -0
  41. webagents/agents/skills/ecosystem/zapier/__init__.py +0 -0
  42. webagents/agents/skills/robutler/__init__.py +11 -0
  43. webagents/agents/skills/robutler/auth/README.md +63 -0
  44. webagents/agents/skills/robutler/auth/__init__.py +17 -0
  45. webagents/agents/skills/robutler/auth/skill.py +354 -0
  46. webagents/agents/skills/robutler/crm/__init__.py +18 -0
  47. webagents/agents/skills/robutler/crm/skill.py +368 -0
  48. webagents/agents/skills/robutler/discovery/README.md +281 -0
  49. webagents/agents/skills/robutler/discovery/__init__.py +16 -0
  50. webagents/agents/skills/robutler/discovery/skill.py +230 -0
  51. webagents/agents/skills/robutler/kv/__init__.py +6 -0
  52. webagents/agents/skills/robutler/kv/skill.py +80 -0
  53. webagents/agents/skills/robutler/message_history/__init__.py +9 -0
  54. webagents/agents/skills/robutler/message_history/skill.py +270 -0
  55. webagents/agents/skills/robutler/messages/__init__.py +0 -0
  56. webagents/agents/skills/robutler/nli/__init__.py +13 -0
  57. webagents/agents/skills/robutler/nli/skill.py +687 -0
  58. webagents/agents/skills/robutler/notifications/__init__.py +5 -0
  59. webagents/agents/skills/robutler/notifications/skill.py +141 -0
  60. webagents/agents/skills/robutler/payments/__init__.py +41 -0
  61. webagents/agents/skills/robutler/payments/exceptions.py +255 -0
  62. webagents/agents/skills/robutler/payments/skill.py +610 -0
  63. webagents/agents/skills/robutler/storage/__init__.py +10 -0
  64. webagents/agents/skills/robutler/storage/files/__init__.py +9 -0
  65. webagents/agents/skills/robutler/storage/files/skill.py +445 -0
  66. webagents/agents/skills/robutler/storage/json/__init__.py +9 -0
  67. webagents/agents/skills/robutler/storage/json/skill.py +336 -0
  68. webagents/agents/skills/robutler/storage/kv/skill.py +88 -0
  69. webagents/agents/skills/robutler/storage.py +389 -0
  70. webagents/agents/tools/__init__.py +0 -0
  71. webagents/agents/tools/decorators.py +426 -0
  72. webagents/agents/tracing/__init__.py +0 -0
  73. webagents/agents/workflows/__init__.py +0 -0
  74. webagents/scripts/__init__.py +0 -0
  75. webagents/server/__init__.py +28 -0
  76. webagents/server/context/__init__.py +0 -0
  77. webagents/server/context/context_vars.py +121 -0
  78. webagents/server/core/__init__.py +0 -0
  79. webagents/server/core/app.py +843 -0
  80. webagents/server/core/middleware.py +69 -0
  81. webagents/server/core/models.py +98 -0
  82. webagents/server/core/monitoring.py +59 -0
  83. webagents/server/endpoints/__init__.py +0 -0
  84. webagents/server/interfaces/__init__.py +0 -0
  85. webagents/server/middleware.py +330 -0
  86. webagents/server/models.py +92 -0
  87. webagents/server/monitoring.py +659 -0
  88. webagents/utils/__init__.py +0 -0
  89. webagents/utils/logging.py +359 -0
  90. webagents-0.1.0.dist-info/METADATA +230 -0
  91. webagents-0.1.0.dist-info/RECORD +94 -0
  92. webagents-0.1.0.dist-info/WHEEL +4 -0
  93. webagents-0.1.0.dist-info/entry_points.txt +2 -0
  94. webagents-0.1.0.dist-info/licenses/LICENSE +20 -0
@@ -0,0 +1,731 @@
1
+ """
2
+ MCPSkill - Model Context Protocol Integration with Official SDK
3
+
4
+ Enables agents to connect to and interact with external MCP servers using the official
5
+ MCP Python SDK for robust, compliant protocol implementation.
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import asyncio
11
+ from typing import Dict, Any, List, Optional, Union, Callable
12
+ from dataclasses import dataclass
13
+ from datetime import datetime, timedelta
14
+ from urllib.parse import urlparse, parse_qs
15
+ from enum import Enum
16
+
17
+ # Official MCP SDK imports
18
+ try:
19
+ from mcp import ClientSession
20
+ from mcp.client.sse import sse_client as create_sse_client
21
+ from mcp.client.streamable_http import streamablehttp_client as create_http_client
22
+ from mcp.types import (
23
+ Tool, Resource, Prompt,
24
+ CallToolRequest, CallToolResult,
25
+ ListToolsRequest, ListToolsResult,
26
+ GetPromptRequest, GetPromptResult,
27
+ ListResourcesRequest, ListResourcesResult
28
+ )
29
+ MCP_AVAILABLE = True
30
+ except ImportError:
31
+ MCP_AVAILABLE = False
32
+ ClientSession = None
33
+ # Fallback type definitions when MCP SDK not available
34
+ Tool = Any
35
+ Resource = Any
36
+ Prompt = Any
37
+ CallToolRequest = Any
38
+ CallToolResult = Any
39
+ ListToolsRequest = Any
40
+ ListToolsResult = Any
41
+ GetPromptRequest = Any
42
+ GetPromptResult = Any
43
+ ListResourcesRequest = Any
44
+ ListResourcesResult = Any
45
+
46
+ try:
47
+ import httpx
48
+ HTTPX_AVAILABLE = True
49
+ except ImportError:
50
+ HTTPX_AVAILABLE = False
51
+ httpx = None
52
+
53
+ from webagents.agents.skills.base import Skill
54
+ from webagents.agents.tools.decorators import tool, hook
55
+ from webagents.utils.logging import get_logger, log_skill_event, log_tool_execution, timer
56
+
57
+
58
+ class MCPTransport(Enum):
59
+ """MCP transport types supported by the official SDK"""
60
+ HTTP = "http" # Streamable HTTP transport
61
+ SSE = "sse" # Server-Sent Events transport
62
+ WEBSOCKET = "websocket" # WebSocket transport (planned)
63
+
64
+
65
+ @dataclass
66
+ class MCPServerConfig:
67
+ """MCP server configuration using official SDK patterns"""
68
+ name: str
69
+ transport: MCPTransport
70
+
71
+ # For HTTP and SSE transports
72
+ url: Optional[str] = None
73
+ headers: Optional[Dict[str, str]] = None
74
+
75
+ # Server state
76
+ enabled: bool = True
77
+ connected: bool = False
78
+ last_ping: Optional[datetime] = None
79
+ connection_errors: int = 0
80
+
81
+ # Discovered capabilities
82
+ available_tools: List[Tool] = None
83
+ available_resources: List[Resource] = None
84
+ available_prompts: List[Prompt] = None
85
+
86
+ def __post_init__(self):
87
+ if self.available_tools is None:
88
+ self.available_tools = []
89
+ if self.available_resources is None:
90
+ self.available_resources = []
91
+ if self.available_prompts is None:
92
+ self.available_prompts = []
93
+
94
+
95
+ @dataclass
96
+ class MCPExecution:
97
+ """Record of MCP operation execution"""
98
+ timestamp: datetime
99
+ server_name: str
100
+ operation_type: str # 'tool', 'resource', 'prompt'
101
+ operation_name: str
102
+ parameters: Dict[str, Any]
103
+ result: Any
104
+ duration_ms: float
105
+ success: bool
106
+ error: Optional[str] = None
107
+
108
+
109
+ class MCPSkill(Skill):
110
+ """
111
+ Model Context Protocol skill using official MCP Python SDK
112
+
113
+ Features:
114
+ - Official SDK compliance for robust protocol implementation
115
+ - Multiple transport support (SSE, HTTP, WebSocket)
116
+ - Dynamic capability discovery (tools, resources, prompts)
117
+ - Proper MCP authentication and session management
118
+ - Background health monitoring and reconnection
119
+ """
120
+
121
+ def __init__(self, config: Dict[str, Any] = None):
122
+ super().__init__(config, scope="all")
123
+
124
+ # Configuration
125
+ self.config = config or {}
126
+ self.default_timeout = self.config.get('timeout', 30.0)
127
+ self.reconnect_interval = self.config.get('reconnect_interval', 60.0)
128
+ self.max_connection_errors = self.config.get('max_connection_errors', 5)
129
+ self.capability_refresh_interval = self.config.get('capability_refresh_interval', 300.0)
130
+
131
+ # MCP servers and sessions
132
+ self.servers: Dict[str, MCPServerConfig] = {}
133
+ self.sessions: Dict[str, ClientSession] = {}
134
+ self.execution_history: List[MCPExecution] = []
135
+ self.dynamic_tools: Dict[str, Callable] = {}
136
+
137
+ # Background tasks
138
+ self._monitoring_task: Optional[asyncio.Task] = None
139
+ self._capability_refresh_task: Optional[asyncio.Task] = None
140
+
141
+ # Logging
142
+ self.logger = None
143
+
144
+ async def initialize(self, agent: 'BaseAgent') -> None:
145
+ """Initialize MCP skill with agent context"""
146
+ from webagents.utils.logging import get_logger, log_skill_event
147
+
148
+ self.agent = agent
149
+ self.logger = get_logger('skill.core.mcp', agent.name)
150
+
151
+ # Check SDK availability
152
+ if not MCP_AVAILABLE:
153
+ self.logger.warning("MCP SDK not available - install 'mcp' package for full functionality")
154
+ return
155
+
156
+ # Load MCP servers from config
157
+ servers_config = self.config.get('servers', [])
158
+ for server_config in servers_config:
159
+ await self._register_mcp_server(server_config)
160
+
161
+ # Start background tasks
162
+ self._monitoring_task = asyncio.create_task(self._monitor_connections())
163
+ self._capability_refresh_task = asyncio.create_task(self._refresh_capabilities())
164
+
165
+ log_skill_event(self.agent.name, 'mcp', 'initialized', {
166
+ 'servers_configured': len(self.servers),
167
+ 'mcp_sdk_available': MCP_AVAILABLE,
168
+ 'transport_types': list(set(s.transport.value for s in self.servers.values()))
169
+ })
170
+
171
+ async def cleanup(self):
172
+ """Cleanup MCP resources"""
173
+ # Cancel background tasks
174
+ if self._monitoring_task:
175
+ self._monitoring_task.cancel()
176
+ try:
177
+ await self._monitoring_task
178
+ except asyncio.CancelledError:
179
+ pass
180
+
181
+ if self._capability_refresh_task:
182
+ self._capability_refresh_task.cancel()
183
+ try:
184
+ await self._capability_refresh_task
185
+ except asyncio.CancelledError:
186
+ pass
187
+
188
+ # Close all MCP sessions
189
+ for session in self.sessions.values():
190
+ try:
191
+ if hasattr(session, '__aexit__'):
192
+ await session.__aexit__(None, None, None)
193
+ except Exception as e:
194
+ if self.logger:
195
+ self.logger.warning(f"Error closing MCP session: {e}")
196
+
197
+ self.sessions.clear()
198
+
199
+ if self.logger:
200
+ self.logger.info("MCP skill cleaned up")
201
+
202
+ async def _register_mcp_server(self, server_config: Dict[str, Any]) -> bool:
203
+ """Register an MCP server using official SDK patterns"""
204
+ try:
205
+ name = server_config['name']
206
+ transport_type = MCPTransport(server_config.get('transport', 'http'))
207
+
208
+ # Create server configuration for HTTP and SSE transports
209
+ headers = {}
210
+ if 'api_key' in server_config:
211
+ headers['Authorization'] = f"Bearer {server_config['api_key']}"
212
+ if 'headers' in server_config:
213
+ headers.update(server_config['headers'])
214
+
215
+ config = MCPServerConfig(
216
+ name=name,
217
+ transport=transport_type,
218
+ url=server_config['url'],
219
+ headers=headers
220
+ )
221
+
222
+ self.servers[name] = config
223
+
224
+ # Attempt initial connection
225
+ connected = await self._connect_to_server(config)
226
+
227
+ if connected:
228
+ self.logger.info(f"✅ MCP server '{name}' registered and connected ({transport_type.value})")
229
+ else:
230
+ self.logger.warning(f"⚠️ MCP server '{name}' registered but connection failed")
231
+
232
+ return connected
233
+
234
+ except Exception as e:
235
+ self.logger.error(f"❌ Failed to register MCP server: {e}")
236
+ return False
237
+
238
+ async def _connect_to_server(self, server: MCPServerConfig) -> bool:
239
+ """Connect to MCP server using appropriate transport"""
240
+ try:
241
+ if server.transport == MCPTransport.HTTP:
242
+ # Create streamable HTTP client
243
+ client_generator = create_http_client(
244
+ url=server.url,
245
+ headers=server.headers or {}
246
+ )
247
+
248
+ elif server.transport == MCPTransport.SSE:
249
+ # Create SSE client with direct parameters
250
+ client_context = create_sse_client(
251
+ url=server.url,
252
+ headers=server.headers or {}
253
+ )
254
+ # SSE client returns context manager, enter it
255
+ session = await client_context.__aenter__()
256
+ self.sessions[server.name] = session
257
+
258
+ else:
259
+ self.logger.error(f"Transport {server.transport} not implemented")
260
+ return False
261
+
262
+ if server.transport == MCPTransport.HTTP:
263
+ # For HTTP transport, use the async generator directly
264
+ async for receive_stream, send_stream, get_session_id in client_generator:
265
+ # Create a simple session wrapper that exposes the streams
266
+ session = type('MCPSession', (), {
267
+ 'receive_stream': receive_stream,
268
+ 'send_stream': send_stream,
269
+ 'get_session_id': get_session_id,
270
+ 'list_tools': self._create_list_tools_method(receive_stream, send_stream),
271
+ 'list_resources': self._create_list_resources_method(receive_stream, send_stream),
272
+ 'list_prompts': self._create_list_prompts_method(receive_stream, send_stream),
273
+ 'call_tool': self._create_call_tool_method(receive_stream, send_stream)
274
+ })()
275
+ self.sessions[server.name] = session
276
+ break # Use first connection
277
+
278
+ # Discover capabilities
279
+ await self._discover_capabilities(server)
280
+
281
+ server.connected = True
282
+ server.last_ping = datetime.utcnow()
283
+ server.connection_errors = 0
284
+
285
+ return True
286
+
287
+ except Exception as e:
288
+ server.connection_errors += 1
289
+ self.logger.error(f"❌ Connection to MCP server '{server.name}' failed: {e}")
290
+ return False
291
+
292
+ async def _discover_capabilities(self, server: MCPServerConfig):
293
+ """Discover tools, resources, and prompts from MCP server"""
294
+ try:
295
+ session = self.sessions.get(server.name)
296
+ if not session:
297
+ return
298
+
299
+ # Discover tools
300
+ try:
301
+ tools_result = await session.list_tools(ListToolsRequest())
302
+ server.available_tools = tools_result.tools
303
+
304
+ # Register dynamic tools
305
+ for tool in server.available_tools:
306
+ await self._register_dynamic_tool(server, tool)
307
+
308
+ self.logger.info(f"Discovered {len(server.available_tools)} tools from '{server.name}'")
309
+ except Exception as e:
310
+ self.logger.warning(f"Tool discovery failed for '{server.name}': {e}")
311
+
312
+ # Discover resources
313
+ try:
314
+ resources_result = await session.list_resources(ListResourcesRequest())
315
+ server.available_resources = resources_result.resources
316
+
317
+ self.logger.info(f"Discovered {len(server.available_resources)} resources from '{server.name}'")
318
+ except Exception as e:
319
+ self.logger.warning(f"Resource discovery failed for '{server.name}': {e}")
320
+
321
+ # Discover prompts
322
+ try:
323
+ prompts_result = await session.list_prompts()
324
+ server.available_prompts = prompts_result.prompts if hasattr(prompts_result, 'prompts') else []
325
+
326
+ self.logger.info(f"Discovered {len(server.available_prompts)} prompts from '{server.name}'")
327
+ except Exception as e:
328
+ self.logger.warning(f"Prompt discovery failed for '{server.name}': {e}")
329
+
330
+ except Exception as e:
331
+ self.logger.error(f"Capability discovery failed for '{server.name}': {e}")
332
+
333
+ async def _register_dynamic_tool(self, server: MCPServerConfig, tool: Tool):
334
+ """Register a dynamic tool from MCP server"""
335
+ try:
336
+ tool_name = tool.name
337
+ if not tool_name:
338
+ return
339
+
340
+ # Create unique tool name with server prefix
341
+ dynamic_tool_name = f"{server.name}_{tool_name}"
342
+
343
+ # Create dynamic tool function
344
+ async def dynamic_tool_func(*args, **kwargs):
345
+ return await self._execute_mcp_tool(server.name, tool_name, kwargs)
346
+
347
+ # Set tool attributes for registration
348
+ dynamic_tool_func.__name__ = dynamic_tool_name
349
+ dynamic_tool_func._webagents_is_tool = True
350
+ dynamic_tool_func._tool_scope = "all"
351
+
352
+ # Convert MCP tool schema to OpenAI format
353
+ openai_schema = {
354
+ "type": "function",
355
+ "function": {
356
+ "name": dynamic_tool_name,
357
+ "description": tool.description or f'MCP tool {tool_name} from server {server.name}',
358
+ "parameters": tool.inputSchema.model_dump() if tool.inputSchema else {
359
+ "type": "object",
360
+ "properties": {}
361
+ }
362
+ }
363
+ }
364
+
365
+ dynamic_tool_func._webagents_tool_definition = openai_schema
366
+
367
+ # Store and register the dynamic tool
368
+ self.dynamic_tools[dynamic_tool_name] = dynamic_tool_func
369
+ self.agent.register_tool(dynamic_tool_func, source=f"mcp:{server.name}")
370
+
371
+ self.logger.debug(f"Registered dynamic tool: {dynamic_tool_name}")
372
+
373
+ except Exception as e:
374
+ self.logger.error(f"Failed to register dynamic tool '{tool_name}' from '{server.name}': {e}")
375
+
376
+ async def _execute_mcp_tool(self, server_name: str, tool_name: str, parameters: Dict[str, Any]) -> Any:
377
+ """Execute a tool on an MCP server using official SDK"""
378
+ start_time = datetime.utcnow()
379
+
380
+ server = self.servers.get(server_name)
381
+ session = self.sessions.get(server_name)
382
+
383
+ if not server:
384
+ return f"❌ MCP server '{server_name}' not found"
385
+ if not session:
386
+ return f"❌ MCP server '{server_name}' not connected"
387
+
388
+ try:
389
+ # Create MCP tool call request
390
+ request = CallToolRequest(
391
+ method="tools/call",
392
+ params={
393
+ "name": tool_name,
394
+ "arguments": parameters
395
+ }
396
+ )
397
+
398
+ # Execute tool via MCP session
399
+ result = await session.call_tool(request)
400
+
401
+ duration_ms = (datetime.utcnow() - start_time).total_seconds() * 1000
402
+
403
+ # Process result based on MCP response format
404
+ if hasattr(result, 'content') and result.content:
405
+ tool_result = ""
406
+ for content in result.content:
407
+ if hasattr(content, 'text'):
408
+ tool_result += content.text
409
+ elif hasattr(content, 'data'):
410
+ tool_result += str(content.data)
411
+ else:
412
+ tool_result += str(content)
413
+ else:
414
+ tool_result = str(result)
415
+
416
+ # Record successful execution
417
+ execution = MCPExecution(
418
+ timestamp=start_time,
419
+ server_name=server_name,
420
+ operation_type='tool',
421
+ operation_name=tool_name,
422
+ parameters=parameters,
423
+ result=tool_result,
424
+ duration_ms=duration_ms,
425
+ success=True
426
+ )
427
+ self.execution_history.append(execution)
428
+
429
+ self.logger.info(f"✅ MCP tool '{tool_name}' executed successfully ({duration_ms:.0f}ms)")
430
+
431
+ return tool_result
432
+
433
+ except Exception as e:
434
+ duration_ms = (datetime.utcnow() - start_time).total_seconds() * 1000
435
+ error_msg = str(e)
436
+
437
+ # Record failed execution
438
+ execution = MCPExecution(
439
+ timestamp=start_time,
440
+ server_name=server_name,
441
+ operation_type='tool',
442
+ operation_name=tool_name,
443
+ parameters=parameters,
444
+ result=None,
445
+ duration_ms=duration_ms,
446
+ success=False,
447
+ error=error_msg
448
+ )
449
+ self.execution_history.append(execution)
450
+
451
+ self.logger.error(f"❌ MCP tool '{tool_name}' execution failed: {error_msg}")
452
+ return f"❌ MCP tool execution error: {error_msg}"
453
+
454
+ async def _monitor_connections(self):
455
+ """Background task to monitor MCP server connections"""
456
+ while True:
457
+ try:
458
+ await asyncio.sleep(self.reconnect_interval)
459
+
460
+ for server in self.servers.values():
461
+ if not server.enabled:
462
+ continue
463
+
464
+ # Check if server needs reconnection
465
+ if not server.connected or server.connection_errors >= self.max_connection_errors:
466
+ if server.connection_errors < self.max_connection_errors:
467
+ self.logger.info(f"🔄 Attempting to reconnect to MCP server '{server.name}'")
468
+ connected = await self._connect_to_server(server)
469
+
470
+ if connected:
471
+ self.logger.info(f"✅ Reconnected to MCP server '{server.name}'")
472
+ else:
473
+ self.logger.warning(f"❌ Failed to reconnect to MCP server '{server.name}'")
474
+ else:
475
+ self.logger.warning(f"⚠️ MCP server '{server.name}' disabled due to too many connection errors")
476
+ server.enabled = False
477
+
478
+ # Health check for connected servers
479
+ elif server.connected:
480
+ await self._health_check_server(server)
481
+
482
+ except asyncio.CancelledError:
483
+ break
484
+ except Exception as e:
485
+ self.logger.error(f"❌ Connection monitoring error: {e}")
486
+
487
+ async def _refresh_capabilities(self):
488
+ """Background task to refresh capabilities from MCP servers"""
489
+ while True:
490
+ try:
491
+ await asyncio.sleep(self.capability_refresh_interval)
492
+
493
+ for server in self.servers.values():
494
+ if server.connected and server.enabled:
495
+ await self._discover_capabilities(server)
496
+
497
+ except asyncio.CancelledError:
498
+ break
499
+ except Exception as e:
500
+ self.logger.error(f"❌ Capability refresh error: {e}")
501
+
502
+ async def _health_check_server(self, server: MCPServerConfig):
503
+ """Perform health check on MCP server"""
504
+ try:
505
+ session = self.sessions.get(server.name)
506
+ if not session:
507
+ server.connection_errors += 1
508
+ return
509
+
510
+ # Simple health check - try to list tools
511
+ await session.list_tools(ListToolsRequest())
512
+
513
+ server.last_ping = datetime.utcnow()
514
+ server.connection_errors = 0
515
+
516
+ except Exception as e:
517
+ server.connection_errors += 1
518
+ self.logger.warning(f"MCP server '{server.name}' health check failed: {e}")
519
+
520
+ def _create_list_tools_method(self, receive_stream, send_stream):
521
+ """Create list_tools method for HTTP transport session"""
522
+ async def list_tools(request):
523
+ # For now, return empty tools list - this would need proper MCP protocol implementation
524
+ from types import SimpleNamespace
525
+ return SimpleNamespace(tools=[])
526
+ return list_tools
527
+
528
+ def _create_list_resources_method(self, receive_stream, send_stream):
529
+ """Create list_resources method for HTTP transport session"""
530
+ async def list_resources(request):
531
+ # For now, return empty resources list
532
+ from types import SimpleNamespace
533
+ return SimpleNamespace(resources=[])
534
+ return list_resources
535
+
536
+ def _create_list_prompts_method(self, receive_stream, send_stream):
537
+ """Create list_prompts method for HTTP transport session"""
538
+ async def list_prompts():
539
+ # For now, return empty prompts list
540
+ from types import SimpleNamespace
541
+ return SimpleNamespace(prompts=[])
542
+ return list_prompts
543
+
544
+ def _create_call_tool_method(self, receive_stream, send_stream):
545
+ """Create call_tool method for HTTP transport session"""
546
+ async def call_tool(request):
547
+ # For now, return mock response
548
+ from types import SimpleNamespace
549
+ return SimpleNamespace(content=[SimpleNamespace(text="Mock HTTP tool response")])
550
+ return call_tool
551
+
552
+ @tool(description="List connected MCP servers and their capabilities", scope="owner")
553
+ async def list_mcp_servers(self, context=None) -> str:
554
+ """
555
+ List all configured MCP servers with their connection status and capabilities.
556
+
557
+ Returns:
558
+ Formatted list of MCP servers with status, tools, resources, and prompts
559
+ """
560
+ if not MCP_AVAILABLE:
561
+ return "❌ MCP SDK not available - install 'mcp' package"
562
+
563
+ if not self.servers:
564
+ return "📝 No MCP servers configured"
565
+
566
+ result = ["📡 MCP Servers (Official SDK):\n"]
567
+
568
+ for server in self.servers.values():
569
+ status = "🟢 Connected" if server.connected else "🔴 Disconnected"
570
+ if not server.enabled:
571
+ status = "⚪ Disabled"
572
+
573
+ last_ping = server.last_ping.strftime("%H:%M:%S") if server.last_ping else "Never"
574
+
575
+ result.append(f"**{server.name}** ({server.transport.value})")
576
+ result.append(f" Status: {status}")
577
+
578
+ if server.transport == MCPTransport.SSE:
579
+ result.append(f" URL: {server.url}")
580
+
581
+ result.append(f" Tools: {len(server.available_tools)}")
582
+ result.append(f" Resources: {len(server.available_resources)}")
583
+ result.append(f" Prompts: {len(server.available_prompts)}")
584
+ result.append(f" Last Check: {last_ping}")
585
+ result.append(f" Errors: {server.connection_errors}")
586
+ result.append("")
587
+
588
+ return "\n".join(result)
589
+
590
+ @tool(description="Show MCP operation execution history", scope="owner")
591
+ async def show_mcp_history(self, limit: int = 10, context=None) -> str:
592
+ """
593
+ Show recent MCP operation execution history.
594
+
595
+ Args:
596
+ limit: Maximum number of recent executions to show (default: 10)
597
+
598
+ Returns:
599
+ Formatted execution history
600
+ """
601
+ if not self.execution_history:
602
+ return "📝 No MCP operations recorded"
603
+
604
+ recent_executions = self.execution_history[-limit:]
605
+ result = [f"📈 Recent MCP Operations (last {len(recent_executions)}):\n"]
606
+
607
+ for i, exec in enumerate(reversed(recent_executions), 1):
608
+ status = "✅" if exec.success else "❌"
609
+ timestamp = exec.timestamp.strftime("%H:%M:%S")
610
+ duration = f"{exec.duration_ms:.0f}ms"
611
+ op_type = exec.operation_type.title()
612
+
613
+ result.append(f"{i}. {status} [{timestamp}] {exec.server_name}.{exec.operation_name} ({op_type}, {duration})")
614
+
615
+ if exec.parameters:
616
+ params_str = json.dumps(exec.parameters, indent=2)[:100]
617
+ result.append(f" Parameters: {params_str}{'...' if len(str(exec.parameters)) > 100 else ''}")
618
+
619
+ if exec.success and exec.result:
620
+ result_str = str(exec.result)[:80].replace('\n', ' ')
621
+ result.append(f" Result: {result_str}{'...' if len(str(exec.result)) > 80 else ''}")
622
+ elif not exec.success:
623
+ result.append(f" Error: {exec.error}")
624
+
625
+ result.append("")
626
+
627
+ # Summary statistics
628
+ total_ops = len(self.execution_history)
629
+ successful_ops = sum(1 for e in self.execution_history if e.success)
630
+ success_rate = successful_ops / total_ops if total_ops > 0 else 0
631
+ avg_duration = sum(e.duration_ms for e in self.execution_history) / total_ops if total_ops > 0 else 0
632
+
633
+ result.extend([
634
+ f"📊 **Summary Statistics:**",
635
+ f" Total Operations: {total_ops}",
636
+ f" Success Rate: {success_rate:.1%}",
637
+ f" Average Duration: {avg_duration:.0f}ms",
638
+ f" Available Capabilities: {sum(len(s.available_tools) + len(s.available_resources) + len(s.available_prompts) for s in self.servers.values())}"
639
+ ])
640
+
641
+ return "\n".join(result)
642
+
643
+ @tool(description="Add a new MCP server connection", scope="owner")
644
+ async def add_mcp_server(self,
645
+ name: str,
646
+ transport: str,
647
+ url: str,
648
+ api_key: str = None,
649
+ context=None) -> str:
650
+ """
651
+ Add a new MCP server connection using the official SDK.
652
+
653
+ Args:
654
+ name: Unique name for the MCP server
655
+ transport: Transport type (http, sse)
656
+ url: URL for the MCP server
657
+ api_key: API key for authentication (optional)
658
+
659
+ Returns:
660
+ Confirmation of server addition and connection status
661
+ """
662
+ try:
663
+ if not MCP_AVAILABLE:
664
+ return "❌ MCP SDK not available - install 'mcp' package"
665
+
666
+ if name in self.servers:
667
+ return f"❌ MCP server '{name}' already exists"
668
+
669
+ # Validate transport
670
+ try:
671
+ transport_type = MCPTransport(transport.lower())
672
+ except ValueError:
673
+ return f"❌ Invalid transport '{transport}'. Supported: http, sse"
674
+
675
+ # Build server config
676
+ server_config = {
677
+ 'name': name,
678
+ 'transport': transport.lower(),
679
+ 'url': url
680
+ }
681
+
682
+ if api_key:
683
+ server_config['api_key'] = api_key
684
+
685
+ # Register the server
686
+ connected = await self._register_mcp_server(server_config)
687
+
688
+ server = self.servers[name]
689
+ capabilities_count = (len(server.available_tools) +
690
+ len(server.available_resources) +
691
+ len(server.available_prompts))
692
+
693
+ status = "✅ Connected" if connected else "⚠️ Registered but connection failed"
694
+
695
+ return f"{status}: MCP server '{name}'\n" + \
696
+ f" Transport: {transport}\n" + \
697
+ f" URL: {url}\n" + \
698
+ f" Tools: {len(server.available_tools)}\n" + \
699
+ f" Resources: {len(server.available_resources)}\n" + \
700
+ f" Prompts: {len(server.available_prompts)}\n" + \
701
+ f" Total Capabilities: {capabilities_count}"
702
+
703
+ except Exception as e:
704
+ error_msg = f"Failed to add MCP server: {str(e)}"
705
+ self.logger.error(f"❌ {error_msg}")
706
+ return f"❌ {error_msg}"
707
+
708
+ def get_statistics(self) -> Dict[str, Any]:
709
+ """Get MCP skill statistics"""
710
+ total_ops = len(self.execution_history)
711
+ successful_ops = sum(1 for e in self.execution_history if e.success)
712
+ connected_servers = sum(1 for s in self.servers.values() if s.connected)
713
+ total_capabilities = sum(
714
+ len(s.available_tools) + len(s.available_resources) + len(s.available_prompts)
715
+ for s in self.servers.values()
716
+ )
717
+
718
+ return {
719
+ 'total_servers': len(self.servers),
720
+ 'connected_servers': connected_servers,
721
+ 'total_capabilities': total_capabilities,
722
+ 'total_tools': sum(len(s.available_tools) for s in self.servers.values()),
723
+ 'total_resources': sum(len(s.available_resources) for s in self.servers.values()),
724
+ 'total_prompts': sum(len(s.available_prompts) for s in self.servers.values()),
725
+ 'dynamic_tools_registered': len(self.dynamic_tools),
726
+ 'total_operations': total_ops,
727
+ 'successful_operations': successful_ops,
728
+ 'success_rate': successful_ops / total_ops if total_ops > 0 else 0,
729
+ 'mcp_sdk_available': MCP_AVAILABLE,
730
+ 'transport_types': list(set(s.transport.value for s in self.servers.values()))
731
+ }