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.
- webagents/__init__.py +18 -0
- webagents/__main__.py +55 -0
- webagents/agents/__init__.py +13 -0
- webagents/agents/core/__init__.py +19 -0
- webagents/agents/core/base_agent.py +1834 -0
- webagents/agents/core/handoffs.py +293 -0
- webagents/agents/handoffs/__init__.py +0 -0
- webagents/agents/interfaces/__init__.py +0 -0
- webagents/agents/lifecycle/__init__.py +0 -0
- webagents/agents/skills/__init__.py +109 -0
- webagents/agents/skills/base.py +136 -0
- webagents/agents/skills/core/__init__.py +8 -0
- webagents/agents/skills/core/guardrails/__init__.py +0 -0
- webagents/agents/skills/core/llm/__init__.py +0 -0
- webagents/agents/skills/core/llm/anthropic/__init__.py +1 -0
- webagents/agents/skills/core/llm/litellm/__init__.py +10 -0
- webagents/agents/skills/core/llm/litellm/skill.py +538 -0
- webagents/agents/skills/core/llm/openai/__init__.py +1 -0
- webagents/agents/skills/core/llm/xai/__init__.py +1 -0
- webagents/agents/skills/core/mcp/README.md +375 -0
- webagents/agents/skills/core/mcp/__init__.py +15 -0
- webagents/agents/skills/core/mcp/skill.py +731 -0
- webagents/agents/skills/core/memory/__init__.py +11 -0
- webagents/agents/skills/core/memory/long_term_memory/__init__.py +10 -0
- webagents/agents/skills/core/memory/long_term_memory/memory_skill.py +639 -0
- webagents/agents/skills/core/memory/short_term_memory/__init__.py +9 -0
- webagents/agents/skills/core/memory/short_term_memory/skill.py +341 -0
- webagents/agents/skills/core/memory/vector_memory/skill.py +447 -0
- webagents/agents/skills/core/planning/__init__.py +9 -0
- webagents/agents/skills/core/planning/planner.py +343 -0
- webagents/agents/skills/ecosystem/__init__.py +0 -0
- webagents/agents/skills/ecosystem/crewai/__init__.py +1 -0
- webagents/agents/skills/ecosystem/database/__init__.py +1 -0
- webagents/agents/skills/ecosystem/filesystem/__init__.py +0 -0
- webagents/agents/skills/ecosystem/google/__init__.py +0 -0
- webagents/agents/skills/ecosystem/google/calendar/__init__.py +6 -0
- webagents/agents/skills/ecosystem/google/calendar/skill.py +306 -0
- webagents/agents/skills/ecosystem/n8n/__init__.py +0 -0
- webagents/agents/skills/ecosystem/openai_agents/__init__.py +0 -0
- webagents/agents/skills/ecosystem/web/__init__.py +0 -0
- webagents/agents/skills/ecosystem/zapier/__init__.py +0 -0
- webagents/agents/skills/robutler/__init__.py +11 -0
- webagents/agents/skills/robutler/auth/README.md +63 -0
- webagents/agents/skills/robutler/auth/__init__.py +17 -0
- webagents/agents/skills/robutler/auth/skill.py +354 -0
- webagents/agents/skills/robutler/crm/__init__.py +18 -0
- webagents/agents/skills/robutler/crm/skill.py +368 -0
- webagents/agents/skills/robutler/discovery/README.md +281 -0
- webagents/agents/skills/robutler/discovery/__init__.py +16 -0
- webagents/agents/skills/robutler/discovery/skill.py +230 -0
- webagents/agents/skills/robutler/kv/__init__.py +6 -0
- webagents/agents/skills/robutler/kv/skill.py +80 -0
- webagents/agents/skills/robutler/message_history/__init__.py +9 -0
- webagents/agents/skills/robutler/message_history/skill.py +270 -0
- webagents/agents/skills/robutler/messages/__init__.py +0 -0
- webagents/agents/skills/robutler/nli/__init__.py +13 -0
- webagents/agents/skills/robutler/nli/skill.py +687 -0
- webagents/agents/skills/robutler/notifications/__init__.py +5 -0
- webagents/agents/skills/robutler/notifications/skill.py +141 -0
- webagents/agents/skills/robutler/payments/__init__.py +41 -0
- webagents/agents/skills/robutler/payments/exceptions.py +255 -0
- webagents/agents/skills/robutler/payments/skill.py +610 -0
- webagents/agents/skills/robutler/storage/__init__.py +10 -0
- webagents/agents/skills/robutler/storage/files/__init__.py +9 -0
- webagents/agents/skills/robutler/storage/files/skill.py +445 -0
- webagents/agents/skills/robutler/storage/json/__init__.py +9 -0
- webagents/agents/skills/robutler/storage/json/skill.py +336 -0
- webagents/agents/skills/robutler/storage/kv/skill.py +88 -0
- webagents/agents/skills/robutler/storage.py +389 -0
- webagents/agents/tools/__init__.py +0 -0
- webagents/agents/tools/decorators.py +426 -0
- webagents/agents/tracing/__init__.py +0 -0
- webagents/agents/workflows/__init__.py +0 -0
- webagents/scripts/__init__.py +0 -0
- webagents/server/__init__.py +28 -0
- webagents/server/context/__init__.py +0 -0
- webagents/server/context/context_vars.py +121 -0
- webagents/server/core/__init__.py +0 -0
- webagents/server/core/app.py +843 -0
- webagents/server/core/middleware.py +69 -0
- webagents/server/core/models.py +98 -0
- webagents/server/core/monitoring.py +59 -0
- webagents/server/endpoints/__init__.py +0 -0
- webagents/server/interfaces/__init__.py +0 -0
- webagents/server/middleware.py +330 -0
- webagents/server/models.py +92 -0
- webagents/server/monitoring.py +659 -0
- webagents/utils/__init__.py +0 -0
- webagents/utils/logging.py +359 -0
- webagents-0.1.0.dist-info/METADATA +230 -0
- webagents-0.1.0.dist-info/RECORD +94 -0
- webagents-0.1.0.dist-info/WHEEL +4 -0
- webagents-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
}
|