signalwire-agents 0.1.27__py3-none-any.whl → 0.1.29__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.
- signalwire_agents/__init__.py +1 -4
- signalwire_agents/cli/config.py +11 -1
- signalwire_agents/cli/simulation/data_overrides.py +6 -2
- signalwire_agents/cli/test_swaig.py +6 -0
- signalwire_agents/core/agent_base.py +1 -12
- signalwire_agents/core/auth_handler.py +233 -0
- signalwire_agents/core/config_loader.py +259 -0
- signalwire_agents/core/contexts.py +75 -0
- signalwire_agents/core/mixins/state_mixin.py +1 -67
- signalwire_agents/core/mixins/tool_mixin.py +0 -65
- signalwire_agents/core/security_config.py +333 -0
- signalwire_agents/core/swml_service.py +19 -25
- signalwire_agents/prefabs/concierge.py +0 -3
- signalwire_agents/prefabs/faq_bot.py +0 -3
- signalwire_agents/prefabs/info_gatherer.py +0 -3
- signalwire_agents/prefabs/receptionist.py +0 -3
- signalwire_agents/prefabs/survey.py +0 -3
- signalwire_agents/search/search_service.py +200 -11
- signalwire_agents/skills/mcp_gateway/README.md +230 -0
- signalwire_agents/skills/mcp_gateway/__init__.py +1 -0
- signalwire_agents/skills/mcp_gateway/skill.py +339 -0
- {signalwire_agents-0.1.27.dist-info → signalwire_agents-0.1.29.dist-info}/METADATA +1 -59
- {signalwire_agents-0.1.27.dist-info → signalwire_agents-0.1.29.dist-info}/RECORD +27 -24
- signalwire_agents/core/state/__init__.py +0 -17
- signalwire_agents/core/state/file_state_manager.py +0 -219
- signalwire_agents/core/state/state_manager.py +0 -101
- {signalwire_agents-0.1.27.dist-info → signalwire_agents-0.1.29.dist-info}/WHEEL +0 -0
- {signalwire_agents-0.1.27.dist-info → signalwire_agents-0.1.29.dist-info}/entry_points.txt +0 -0
- {signalwire_agents-0.1.27.dist-info → signalwire_agents-0.1.29.dist-info}/licenses/LICENSE +0 -0
- {signalwire_agents-0.1.27.dist-info → signalwire_agents-0.1.29.dist-info}/top_level.txt +0 -0
@@ -51,7 +51,6 @@ class FAQBotAgent(AgentBase):
|
|
51
51
|
persona: Optional[str] = None,
|
52
52
|
name: str = "faq_bot",
|
53
53
|
route: str = "/faq",
|
54
|
-
enable_state_tracking: bool = True, # Enable state tracking by default
|
55
54
|
**kwargs
|
56
55
|
):
|
57
56
|
"""
|
@@ -66,7 +65,6 @@ class FAQBotAgent(AgentBase):
|
|
66
65
|
persona: Optional custom personality description
|
67
66
|
name: Agent name for the route
|
68
67
|
route: HTTP route for this agent
|
69
|
-
enable_state_tracking: Whether to enable state tracking (default: True)
|
70
68
|
**kwargs: Additional arguments for AgentBase
|
71
69
|
"""
|
72
70
|
# Initialize the base agent
|
@@ -74,7 +72,6 @@ class FAQBotAgent(AgentBase):
|
|
74
72
|
name=name,
|
75
73
|
route=route,
|
76
74
|
use_pom=True,
|
77
|
-
enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
|
78
75
|
**kwargs
|
79
76
|
)
|
80
77
|
|
@@ -45,7 +45,6 @@ class InfoGathererAgent(AgentBase):
|
|
45
45
|
questions: Optional[List[Dict[str, str]]] = None,
|
46
46
|
name: str = "info_gatherer",
|
47
47
|
route: str = "/info_gatherer",
|
48
|
-
enable_state_tracking: bool = True, # Enable state tracking by default for InfoGatherer
|
49
48
|
**kwargs
|
50
49
|
):
|
51
50
|
"""
|
@@ -59,7 +58,6 @@ class InfoGathererAgent(AgentBase):
|
|
59
58
|
- confirm: (Optional) If set to True, the agent will confirm the answer before submitting
|
60
59
|
name: Agent name for the route
|
61
60
|
route: HTTP route for this agent
|
62
|
-
enable_state_tracking: Whether to enable state tracking (default: True)
|
63
61
|
**kwargs: Additional arguments for AgentBase
|
64
62
|
"""
|
65
63
|
# Initialize the base agent
|
@@ -67,7 +65,6 @@ class InfoGathererAgent(AgentBase):
|
|
67
65
|
name=name,
|
68
66
|
route=route,
|
69
67
|
use_pom=True,
|
70
|
-
enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
|
71
68
|
**kwargs
|
72
69
|
)
|
73
70
|
|
@@ -41,7 +41,6 @@ class ReceptionistAgent(AgentBase):
|
|
41
41
|
route: str = "/receptionist",
|
42
42
|
greeting: str = "Thank you for calling. How can I help you today?",
|
43
43
|
voice: str = "rime.spore",
|
44
|
-
enable_state_tracking: bool = True, # Enable state tracking by default
|
45
44
|
**kwargs
|
46
45
|
):
|
47
46
|
"""
|
@@ -56,7 +55,6 @@ class ReceptionistAgent(AgentBase):
|
|
56
55
|
route: HTTP route for this agent
|
57
56
|
greeting: Initial greeting message
|
58
57
|
voice: Voice ID to use
|
59
|
-
enable_state_tracking: Whether to enable state tracking (default: True)
|
60
58
|
**kwargs: Additional arguments for AgentBase
|
61
59
|
"""
|
62
60
|
# Initialize the base agent
|
@@ -64,7 +62,6 @@ class ReceptionistAgent(AgentBase):
|
|
64
62
|
name=name,
|
65
63
|
route=route,
|
66
64
|
use_pom=True,
|
67
|
-
enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
|
68
65
|
**kwargs
|
69
66
|
)
|
70
67
|
|
@@ -62,7 +62,6 @@ class SurveyAgent(AgentBase):
|
|
62
62
|
max_retries: int = 2,
|
63
63
|
name: str = "survey",
|
64
64
|
route: str = "/survey",
|
65
|
-
enable_state_tracking: bool = True, # Enable state tracking by default
|
66
65
|
**kwargs
|
67
66
|
):
|
68
67
|
"""
|
@@ -83,7 +82,6 @@ class SurveyAgent(AgentBase):
|
|
83
82
|
max_retries: Maximum number of times to retry invalid answers
|
84
83
|
name: Name for the agent (default: "survey")
|
85
84
|
route: HTTP route for the agent (default: "/survey")
|
86
|
-
enable_state_tracking: Whether to enable state tracking (default: True)
|
87
85
|
**kwargs: Additional arguments for AgentBase
|
88
86
|
"""
|
89
87
|
# Initialize the base agent
|
@@ -91,7 +89,6 @@ class SurveyAgent(AgentBase):
|
|
91
89
|
name=name,
|
92
90
|
route=route,
|
93
91
|
use_pom=True,
|
94
|
-
enable_state_tracking=enable_state_tracking, # Pass state tracking parameter to base
|
95
92
|
**kwargs
|
96
93
|
)
|
97
94
|
|
@@ -8,15 +8,23 @@ See LICENSE file in the project root for full license information.
|
|
8
8
|
"""
|
9
9
|
|
10
10
|
import logging
|
11
|
-
from typing import Dict, Any, List, Optional
|
11
|
+
from typing import Dict, Any, List, Optional, Tuple
|
12
12
|
|
13
13
|
try:
|
14
|
-
from fastapi import FastAPI, HTTPException
|
14
|
+
from fastapi import FastAPI, HTTPException, Request, Response, Depends
|
15
|
+
from fastapi.middleware.cors import CORSMiddleware
|
16
|
+
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
15
17
|
from pydantic import BaseModel
|
16
18
|
except ImportError:
|
17
19
|
FastAPI = None
|
18
20
|
HTTPException = None
|
19
21
|
BaseModel = None
|
22
|
+
Request = None
|
23
|
+
Response = None
|
24
|
+
Depends = None
|
25
|
+
CORSMiddleware = None
|
26
|
+
HTTPBasic = None
|
27
|
+
HTTPBasicCredentials = None
|
20
28
|
|
21
29
|
try:
|
22
30
|
from sentence_transformers import SentenceTransformer
|
@@ -25,8 +33,11 @@ except ImportError:
|
|
25
33
|
|
26
34
|
from .query_processor import preprocess_query
|
27
35
|
from .search_engine import SearchEngine
|
36
|
+
from signalwire_agents.core.security_config import SecurityConfig
|
37
|
+
from signalwire_agents.core.config_loader import ConfigLoader
|
38
|
+
from signalwire_agents.core.logging_config import get_logger
|
28
39
|
|
29
|
-
logger =
|
40
|
+
logger = get_logger("search_service")
|
30
41
|
|
31
42
|
# Pydantic models for API
|
32
43
|
if BaseModel:
|
@@ -73,14 +84,30 @@ else:
|
|
73
84
|
class SearchService:
|
74
85
|
"""Local search service with HTTP API"""
|
75
86
|
|
76
|
-
def __init__(self, port: int = 8001, indexes: Dict[str, str] = None
|
87
|
+
def __init__(self, port: int = 8001, indexes: Dict[str, str] = None,
|
88
|
+
basic_auth: Optional[Tuple[str, str]] = None,
|
89
|
+
config_file: Optional[str] = None):
|
90
|
+
# Load configuration first
|
91
|
+
self._load_config(config_file)
|
92
|
+
|
93
|
+
# Override with constructor params if provided
|
77
94
|
self.port = port
|
78
|
-
|
95
|
+
if indexes is not None:
|
96
|
+
self.indexes = indexes
|
97
|
+
|
79
98
|
self.search_engines = {}
|
80
99
|
self.model = None
|
81
100
|
|
101
|
+
# Load security configuration with optional config file
|
102
|
+
self.security = SecurityConfig(config_file=config_file, service_name="search")
|
103
|
+
self.security.log_config("SearchService")
|
104
|
+
|
105
|
+
# Set up authentication
|
106
|
+
self._basic_auth = basic_auth or self.security.get_basic_auth()
|
107
|
+
|
82
108
|
if FastAPI:
|
83
109
|
self.app = FastAPI(title="SignalWire Local Search Service")
|
110
|
+
self._setup_security()
|
84
111
|
self._setup_routes()
|
85
112
|
else:
|
86
113
|
self.app = None
|
@@ -88,22 +115,131 @@ class SearchService:
|
|
88
115
|
|
89
116
|
self._load_resources()
|
90
117
|
|
118
|
+
def _load_config(self, config_file: Optional[str]):
|
119
|
+
"""Load configuration from file if available"""
|
120
|
+
# Initialize defaults
|
121
|
+
self.indexes = {}
|
122
|
+
|
123
|
+
# Find config file
|
124
|
+
if not config_file:
|
125
|
+
config_file = ConfigLoader.find_config_file("search")
|
126
|
+
|
127
|
+
if not config_file:
|
128
|
+
return
|
129
|
+
|
130
|
+
# Load config
|
131
|
+
config_loader = ConfigLoader([config_file])
|
132
|
+
if not config_loader.has_config():
|
133
|
+
return
|
134
|
+
|
135
|
+
logger.info("loading_config_from_file", file=config_file)
|
136
|
+
|
137
|
+
# Get service section
|
138
|
+
service_config = config_loader.get_section('service')
|
139
|
+
if service_config:
|
140
|
+
if 'port' in service_config:
|
141
|
+
self.port = int(service_config['port'])
|
142
|
+
|
143
|
+
if 'indexes' in service_config and isinstance(service_config['indexes'], dict):
|
144
|
+
self.indexes = service_config['indexes']
|
145
|
+
|
146
|
+
def _setup_security(self):
|
147
|
+
"""Setup security middleware and authentication"""
|
148
|
+
if not self.app:
|
149
|
+
return
|
150
|
+
|
151
|
+
# Add CORS middleware if FastAPI has it
|
152
|
+
if CORSMiddleware:
|
153
|
+
self.app.add_middleware(
|
154
|
+
CORSMiddleware,
|
155
|
+
**self.security.get_cors_config()
|
156
|
+
)
|
157
|
+
|
158
|
+
# Add security headers middleware
|
159
|
+
@self.app.middleware("http")
|
160
|
+
async def add_security_headers(request: Request, call_next):
|
161
|
+
response = await call_next(request)
|
162
|
+
|
163
|
+
# Add security headers
|
164
|
+
is_https = request.url.scheme == "https"
|
165
|
+
headers = self.security.get_security_headers(is_https)
|
166
|
+
for header, value in headers.items():
|
167
|
+
response.headers[header] = value
|
168
|
+
|
169
|
+
return response
|
170
|
+
|
171
|
+
# Add host validation middleware
|
172
|
+
@self.app.middleware("http")
|
173
|
+
async def validate_host(request: Request, call_next):
|
174
|
+
host = request.headers.get("host", "").split(":")[0]
|
175
|
+
if host and not self.security.should_allow_host(host):
|
176
|
+
return Response(content="Invalid host", status_code=400)
|
177
|
+
|
178
|
+
return await call_next(request)
|
179
|
+
|
180
|
+
def _get_current_username(self, credentials: HTTPBasicCredentials = None) -> str:
|
181
|
+
"""Validate basic auth credentials"""
|
182
|
+
if not credentials:
|
183
|
+
return None
|
184
|
+
|
185
|
+
correct_username, correct_password = self._basic_auth
|
186
|
+
|
187
|
+
# Compare credentials
|
188
|
+
import secrets
|
189
|
+
username_correct = secrets.compare_digest(credentials.username, correct_username)
|
190
|
+
password_correct = secrets.compare_digest(credentials.password, correct_password)
|
191
|
+
|
192
|
+
if not (username_correct and password_correct):
|
193
|
+
raise HTTPException(
|
194
|
+
status_code=401,
|
195
|
+
detail="Invalid authentication credentials",
|
196
|
+
headers={"WWW-Authenticate": "Basic"},
|
197
|
+
)
|
198
|
+
|
199
|
+
return credentials.username
|
200
|
+
|
91
201
|
def _setup_routes(self):
|
92
202
|
"""Setup FastAPI routes"""
|
93
203
|
if not self.app:
|
94
204
|
return
|
205
|
+
|
206
|
+
# Create security dependency if HTTPBasic is available
|
207
|
+
security = HTTPBasic() if HTTPBasic else None
|
208
|
+
|
209
|
+
# Create dependency for authenticated routes
|
210
|
+
def get_authenticated():
|
211
|
+
if security:
|
212
|
+
return security
|
213
|
+
return None
|
95
214
|
|
96
215
|
@self.app.post("/search", response_model=SearchResponse)
|
97
|
-
async def search(
|
216
|
+
async def search(
|
217
|
+
request: SearchRequest,
|
218
|
+
credentials: HTTPBasicCredentials = None if not security else Depends(security)
|
219
|
+
):
|
220
|
+
if security:
|
221
|
+
self._get_current_username(credentials)
|
98
222
|
return await self._handle_search(request)
|
99
223
|
|
100
224
|
@self.app.get("/health")
|
101
225
|
async def health():
|
102
|
-
return {
|
226
|
+
return {
|
227
|
+
"status": "healthy",
|
228
|
+
"indexes": list(self.indexes.keys()),
|
229
|
+
"ssl_enabled": self.security.ssl_enabled,
|
230
|
+
"auth_required": bool(security)
|
231
|
+
}
|
103
232
|
|
104
233
|
@self.app.post("/reload_index")
|
105
|
-
async def reload_index(
|
234
|
+
async def reload_index(
|
235
|
+
index_name: str,
|
236
|
+
index_path: str,
|
237
|
+
credentials: HTTPBasicCredentials = None if not security else Depends(security)
|
238
|
+
):
|
106
239
|
"""Reload or add new index"""
|
240
|
+
if security:
|
241
|
+
self._get_current_username(credentials)
|
242
|
+
|
107
243
|
self.indexes[index_name] = index_path
|
108
244
|
self.search_engines[index_name] = SearchEngine(index_path, self.model)
|
109
245
|
return {"status": "reloaded", "index": index_name}
|
@@ -235,14 +371,67 @@ class SearchService:
|
|
235
371
|
'query_analysis': response.query_analysis
|
236
372
|
}
|
237
373
|
|
238
|
-
def start(self
|
239
|
-
|
374
|
+
def start(self, host: str = "0.0.0.0", port: Optional[int] = None,
|
375
|
+
ssl_cert: Optional[str] = None, ssl_key: Optional[str] = None):
|
376
|
+
"""
|
377
|
+
Start the service with optional HTTPS support.
|
378
|
+
|
379
|
+
Args:
|
380
|
+
host: Host to bind to (default: "0.0.0.0")
|
381
|
+
port: Port to bind to (default: self.port)
|
382
|
+
ssl_cert: Path to SSL certificate file (overrides environment)
|
383
|
+
ssl_key: Path to SSL key file (overrides environment)
|
384
|
+
"""
|
240
385
|
if not self.app:
|
241
386
|
raise RuntimeError("FastAPI not available. Cannot start HTTP service.")
|
242
387
|
|
388
|
+
port = port or self.port
|
389
|
+
|
390
|
+
# Get SSL configuration
|
391
|
+
ssl_kwargs = {}
|
392
|
+
if ssl_cert and ssl_key:
|
393
|
+
# Use provided SSL files
|
394
|
+
ssl_kwargs = {
|
395
|
+
'ssl_certfile': ssl_cert,
|
396
|
+
'ssl_keyfile': ssl_key
|
397
|
+
}
|
398
|
+
else:
|
399
|
+
# Use security config SSL settings
|
400
|
+
ssl_kwargs = self.security.get_ssl_context_kwargs()
|
401
|
+
|
402
|
+
# Build startup URL
|
403
|
+
scheme = "https" if ssl_kwargs else "http"
|
404
|
+
startup_url = f"{scheme}://{host}:{port}"
|
405
|
+
|
406
|
+
# Get auth credentials
|
407
|
+
username, password = self._basic_auth
|
408
|
+
|
409
|
+
# Log startup information
|
410
|
+
logger.info(
|
411
|
+
"starting_search_service",
|
412
|
+
url=startup_url,
|
413
|
+
ssl_enabled=bool(ssl_kwargs),
|
414
|
+
indexes=list(self.indexes.keys()),
|
415
|
+
username=username
|
416
|
+
)
|
417
|
+
|
418
|
+
# Print user-friendly startup message
|
419
|
+
print(f"\nSignalWire Search Service starting...")
|
420
|
+
print(f"URL: {startup_url}")
|
421
|
+
print(f"Indexes: {', '.join(self.indexes.keys()) if self.indexes else 'None'}")
|
422
|
+
print(f"Basic Auth: {username}:{password}")
|
423
|
+
if ssl_kwargs:
|
424
|
+
print(f"SSL: Enabled")
|
425
|
+
print("")
|
426
|
+
|
243
427
|
try:
|
244
428
|
import uvicorn
|
245
|
-
uvicorn.run(
|
429
|
+
uvicorn.run(
|
430
|
+
self.app,
|
431
|
+
host=host,
|
432
|
+
port=port,
|
433
|
+
**ssl_kwargs
|
434
|
+
)
|
246
435
|
except ImportError:
|
247
436
|
raise RuntimeError("uvicorn not available. Cannot start HTTP service.")
|
248
437
|
|
@@ -0,0 +1,230 @@
|
|
1
|
+
# MCP Gateway Skill
|
2
|
+
|
3
|
+
Bridge MCP (Model Context Protocol) servers with SignalWire SWAIG functions, allowing agents to seamlessly interact with MCP-based tools.
|
4
|
+
|
5
|
+
## Description
|
6
|
+
|
7
|
+
The MCP Gateway skill connects SignalWire agents to MCP servers through a centralized gateway service. It dynamically discovers and registers MCP tools as SWAIG functions, maintaining session state throughout each call.
|
8
|
+
|
9
|
+
## Features
|
10
|
+
|
11
|
+
- Dynamic tool discovery from MCP servers
|
12
|
+
- Session management tied to SignalWire call IDs
|
13
|
+
- Automatic cleanup on call hangup
|
14
|
+
- Support for multiple MCP services
|
15
|
+
- Selective tool loading
|
16
|
+
- HTTPS support with SSL verification
|
17
|
+
- Retry logic for resilient connections
|
18
|
+
|
19
|
+
## Requirements
|
20
|
+
|
21
|
+
- Running MCP Gateway service
|
22
|
+
- Network access to gateway
|
23
|
+
- Gateway credentials (username/password)
|
24
|
+
|
25
|
+
## Configuration
|
26
|
+
|
27
|
+
### Required Parameters
|
28
|
+
|
29
|
+
Either Basic Auth credentials OR Bearer token:
|
30
|
+
- `gateway_url`: URL of the MCP gateway service (default: "http://localhost:8100")
|
31
|
+
- `auth_user` + `auth_password`: Basic auth credentials
|
32
|
+
- OR `auth_token`: Bearer token for authentication
|
33
|
+
|
34
|
+
### Optional Parameters
|
35
|
+
|
36
|
+
- `services`: Array of services to load (default: all available)
|
37
|
+
- `name`: Service name
|
38
|
+
- `tools`: Array of tool names or "*" for all (default: all)
|
39
|
+
- `session_timeout`: Session timeout in seconds (default: 300)
|
40
|
+
- `tool_prefix`: Prefix for SWAIG function names (default: "mcp_")
|
41
|
+
- `retry_attempts`: Number of retry attempts (default: 3)
|
42
|
+
- `request_timeout`: HTTP request timeout in seconds (default: 30)
|
43
|
+
- `verify_ssl`: Verify SSL certificates (default: true)
|
44
|
+
|
45
|
+
## Usage
|
46
|
+
|
47
|
+
### Basic Usage (All Services)
|
48
|
+
|
49
|
+
```python
|
50
|
+
from signalwire_agents import AgentBase
|
51
|
+
|
52
|
+
class MyAgent(AgentBase):
|
53
|
+
def __init__(self):
|
54
|
+
super().__init__(name="My Agent")
|
55
|
+
|
56
|
+
# Load all available MCP services
|
57
|
+
self.add_skill("mcp_gateway", {
|
58
|
+
"gateway_url": "http://localhost:8080",
|
59
|
+
"auth_user": "admin",
|
60
|
+
"auth_password": "changeme"
|
61
|
+
})
|
62
|
+
|
63
|
+
agent = MyAgent()
|
64
|
+
agent.run()
|
65
|
+
```
|
66
|
+
|
67
|
+
### Selective Service Loading
|
68
|
+
|
69
|
+
```python
|
70
|
+
# Load specific services with specific tools
|
71
|
+
self.add_skill("mcp_gateway", {
|
72
|
+
"gateway_url": "https://gateway.example.com",
|
73
|
+
"auth_user": "admin",
|
74
|
+
"auth_password": "secret",
|
75
|
+
"services": [
|
76
|
+
{
|
77
|
+
"name": "todo",
|
78
|
+
"tools": ["add_todo", "list_todos"] # Only these tools
|
79
|
+
},
|
80
|
+
{
|
81
|
+
"name": "calculator",
|
82
|
+
"tools": "*" # All calculator tools
|
83
|
+
}
|
84
|
+
],
|
85
|
+
"session_timeout": 600,
|
86
|
+
"tool_prefix": "ext_"
|
87
|
+
})
|
88
|
+
```
|
89
|
+
|
90
|
+
### HTTPS with Self-Signed Certificate
|
91
|
+
|
92
|
+
```python
|
93
|
+
self.add_skill("mcp_gateway", {
|
94
|
+
"gateway_url": "https://localhost:8443",
|
95
|
+
"auth_user": "admin",
|
96
|
+
"auth_password": "secret",
|
97
|
+
"verify_ssl": False # For self-signed certificates
|
98
|
+
})
|
99
|
+
```
|
100
|
+
|
101
|
+
### Bearer Token Authentication
|
102
|
+
|
103
|
+
```python
|
104
|
+
self.add_skill("mcp_gateway", {
|
105
|
+
"gateway_url": "https://gateway.example.com",
|
106
|
+
"auth_token": "your-bearer-token-here",
|
107
|
+
"services": [{
|
108
|
+
"name": "todo"
|
109
|
+
}]
|
110
|
+
})
|
111
|
+
```
|
112
|
+
|
113
|
+
## Generated Functions
|
114
|
+
|
115
|
+
The skill dynamically generates SWAIG functions based on discovered MCP tools. Function names follow the pattern:
|
116
|
+
|
117
|
+
`{tool_prefix}{service_name}_{tool_name}`
|
118
|
+
|
119
|
+
For example, with default settings:
|
120
|
+
- `mcp_todo_add_todo` - Add a todo item
|
121
|
+
- `mcp_todo_list_todos` - List todo items
|
122
|
+
- `mcp_calculator_add` - Calculator addition
|
123
|
+
|
124
|
+
## Example Conversations
|
125
|
+
|
126
|
+
### Using Todo Service
|
127
|
+
|
128
|
+
```
|
129
|
+
User: "Add a task to buy milk"
|
130
|
+
Assistant: "I'll add that to your todo list."
|
131
|
+
[Calls mcp_todo_add_todo with text="buy milk"]
|
132
|
+
Assistant: "I've added 'buy milk' to your todo list."
|
133
|
+
|
134
|
+
User: "What's on my todo list?"
|
135
|
+
Assistant: "Let me check your todos."
|
136
|
+
[Calls mcp_todo_list_todos]
|
137
|
+
Assistant: "Here are your current todos:
|
138
|
+
○ #1 [medium] buy milk"
|
139
|
+
```
|
140
|
+
|
141
|
+
### Multiple Services
|
142
|
+
|
143
|
+
```
|
144
|
+
User: "Add 'finish report' to my todos and calculate 15% of 200"
|
145
|
+
Assistant: "I'll add that todo and do the calculation for you."
|
146
|
+
[Calls mcp_todo_add_todo with text="finish report"]
|
147
|
+
[Calls mcp_calculator_percent with value=200, percent=15]
|
148
|
+
Assistant: "I've added 'finish report' to your todos. 15% of 200 is 30."
|
149
|
+
```
|
150
|
+
|
151
|
+
## Session Management
|
152
|
+
|
153
|
+
- Each SignalWire call gets its own MCP session
|
154
|
+
- Sessions persist across multiple tool calls
|
155
|
+
- Automatic cleanup on call hangup
|
156
|
+
- Configurable timeout for inactive sessions
|
157
|
+
|
158
|
+
### Custom Session ID
|
159
|
+
|
160
|
+
You can override the session ID by setting `mcp_call_id` in global_data:
|
161
|
+
|
162
|
+
```python
|
163
|
+
# In your agent code
|
164
|
+
self.set_global_data({
|
165
|
+
"mcp_call_id": "custom-session-123"
|
166
|
+
})
|
167
|
+
|
168
|
+
# Or in a SWAIG function
|
169
|
+
result = SwaigFunctionResult("Session changed")
|
170
|
+
result.add_action("set_global_data", {"mcp_call_id": "new-session-456"})
|
171
|
+
```
|
172
|
+
|
173
|
+
This is useful for:
|
174
|
+
- Managing multiple MCP sessions within a single call
|
175
|
+
- Sharing MCP sessions across different calls
|
176
|
+
- Custom session management strategies
|
177
|
+
|
178
|
+
## Troubleshooting
|
179
|
+
|
180
|
+
### Gateway Connection Failed
|
181
|
+
|
182
|
+
Check:
|
183
|
+
1. Gateway service is running
|
184
|
+
2. Correct URL and credentials
|
185
|
+
3. Network connectivity
|
186
|
+
4. Firewall rules
|
187
|
+
|
188
|
+
### SSL Certificate Errors
|
189
|
+
|
190
|
+
For self-signed certificates:
|
191
|
+
```python
|
192
|
+
"verify_ssl": False
|
193
|
+
```
|
194
|
+
|
195
|
+
For custom CA certificates, ensure they're in the system trust store.
|
196
|
+
|
197
|
+
### Tool Not Found
|
198
|
+
|
199
|
+
Verify:
|
200
|
+
1. Service name is correct
|
201
|
+
2. Tool name matches exactly
|
202
|
+
3. Tool is included in service configuration
|
203
|
+
4. MCP server is returning tools correctly
|
204
|
+
|
205
|
+
### Session Timeouts
|
206
|
+
|
207
|
+
Increase timeout if needed:
|
208
|
+
```python
|
209
|
+
"session_timeout": 600 # 10 minutes
|
210
|
+
```
|
211
|
+
|
212
|
+
## Gateway Setup
|
213
|
+
|
214
|
+
To run the MCP Gateway service:
|
215
|
+
|
216
|
+
```bash
|
217
|
+
cd mcp_gateway
|
218
|
+
python3 gateway_service.py
|
219
|
+
|
220
|
+
# Or with custom config
|
221
|
+
python3 gateway_service.py -c myconfig.json
|
222
|
+
```
|
223
|
+
|
224
|
+
## Security Considerations
|
225
|
+
|
226
|
+
1. Always use HTTPS in production
|
227
|
+
2. Use strong authentication credentials
|
228
|
+
3. Limit service access to required tools only
|
229
|
+
4. Monitor gateway logs for suspicious activity
|
230
|
+
5. Set appropriate session timeouts
|
@@ -0,0 +1 @@
|
|
1
|
+
"""MCP Gateway Skill for SignalWire Agents"""
|