telnyx-mcp-server-fastmcp 0.1.3__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.
- telnyx_mcp_server/__init__.py +0 -0
- telnyx_mcp_server/__main__.py +23 -0
- telnyx_mcp_server/config.py +148 -0
- telnyx_mcp_server/mcp.py +148 -0
- telnyx_mcp_server/server.py +497 -0
- telnyx_mcp_server/telnyx/__init__.py +1 -0
- telnyx_mcp_server/telnyx/client.py +363 -0
- telnyx_mcp_server/telnyx/services/__init__.py +0 -0
- telnyx_mcp_server/telnyx/services/assistants.py +155 -0
- telnyx_mcp_server/telnyx/services/call_control.py +217 -0
- telnyx_mcp_server/telnyx/services/cloud_storage.py +289 -0
- telnyx_mcp_server/telnyx/services/connections.py +92 -0
- telnyx_mcp_server/telnyx/services/embeddings.py +52 -0
- telnyx_mcp_server/telnyx/services/messaging.py +93 -0
- telnyx_mcp_server/telnyx/services/messaging_profiles.py +196 -0
- telnyx_mcp_server/telnyx/services/numbers.py +193 -0
- telnyx_mcp_server/telnyx/services/secrets.py +74 -0
- telnyx_mcp_server/tools/__init__.py +126 -0
- telnyx_mcp_server/tools/assistants.py +313 -0
- telnyx_mcp_server/tools/call_control.py +242 -0
- telnyx_mcp_server/tools/cloud_storage.py +183 -0
- telnyx_mcp_server/tools/connections.py +78 -0
- telnyx_mcp_server/tools/embeddings.py +80 -0
- telnyx_mcp_server/tools/messaging.py +57 -0
- telnyx_mcp_server/tools/messaging_profiles.py +123 -0
- telnyx_mcp_server/tools/phone_numbers.py +161 -0
- telnyx_mcp_server/tools/secrets.py +75 -0
- telnyx_mcp_server/tools/sms_conversations.py +455 -0
- telnyx_mcp_server/tools/webhooks.py +111 -0
- telnyx_mcp_server/utils/__init__.py +0 -0
- telnyx_mcp_server/utils/error_handler.py +30 -0
- telnyx_mcp_server/utils/logger.py +32 -0
- telnyx_mcp_server/utils/service.py +33 -0
- telnyx_mcp_server/webhook/__init__.py +25 -0
- telnyx_mcp_server/webhook/handler.py +596 -0
- telnyx_mcp_server/webhook/server.py +369 -0
- telnyx_mcp_server_fastmcp-0.1.3.dist-info/METADATA +430 -0
- telnyx_mcp_server_fastmcp-0.1.3.dist-info/RECORD +40 -0
- telnyx_mcp_server_fastmcp-0.1.3.dist-info/WHEEL +4 -0
- telnyx_mcp_server_fastmcp-0.1.3.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Secrets manager related MCP tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from ..mcp import mcp
|
|
8
|
+
from ..telnyx.services.secrets import SecretsService
|
|
9
|
+
from ..utils.error_handler import handle_telnyx_error
|
|
10
|
+
from ..utils.logger import get_logger
|
|
11
|
+
from ..utils.service import get_authenticated_service
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@mcp.tool()
|
|
17
|
+
async def list_integration_secrets(request: Dict[str, Any]) -> Dict[str, Any]:
|
|
18
|
+
"""List integration secrets.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
page: Optional integer. Page number. Defaults to 1.
|
|
22
|
+
page_size: Optional integer. Page size. Defaults to 25.
|
|
23
|
+
filter_type: Optional. Filter by secret type (bearer, basic).
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Dict[str, Any]: Response data containing Integration Secret Object(s) (record_type: "integration_secret")
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
service = get_authenticated_service(SecretsService)
|
|
30
|
+
return service.list_integration_secrets(request)
|
|
31
|
+
except Exception as e:
|
|
32
|
+
logger.error(f"Error listing integration secrets: {e}")
|
|
33
|
+
raise handle_telnyx_error(e)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@mcp.tool()
|
|
37
|
+
async def create_integration_secret(request: Dict[str, Any]) -> Dict[str, Any]:
|
|
38
|
+
"""Create an integration secret.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
identifier: Required. The unique identifier of the secret.
|
|
42
|
+
type: Required. The type of secret (bearer, basic).
|
|
43
|
+
token: Optional. The token for the secret (required for bearer type).
|
|
44
|
+
username: Optional. The username for the secret (required for basic type).
|
|
45
|
+
password: Optional. The password for the secret (required for basic type).
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Dict[str, Any]: Response data containing the created Integration Secret Object (record_type: "integration_secret")
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
service = get_authenticated_service(SecretsService)
|
|
52
|
+
return service.create_integration_secret(request)
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.error(f"Error creating integration secret: {e}")
|
|
55
|
+
raise handle_telnyx_error(e)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@mcp.tool()
|
|
59
|
+
async def delete_integration_secret(
|
|
60
|
+
id: str = Field(..., description="Secret ID as string"),
|
|
61
|
+
) -> Dict[str, Any]:
|
|
62
|
+
"""Delete an integration secret.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
id: Required. Secret ID.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dict[str, Any]: Empty response on success
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
service = get_authenticated_service(SecretsService)
|
|
72
|
+
return service.delete_integration_secret(id=id)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(f"Error deleting integration secret: {e}")
|
|
75
|
+
raise handle_telnyx_error(e)
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""SMS Conversations resource for MCP server."""
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Dict
|
|
6
|
+
|
|
7
|
+
from ..mcp import mcp
|
|
8
|
+
from ..utils.logger import get_logger
|
|
9
|
+
from ..webhook import get_webhook_history
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
# Resource URI for SMS conversations
|
|
14
|
+
SMS_CONVERSATIONS_URI = "resource://sms/conversations"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _extract_conversation_details(webhook_events):
|
|
18
|
+
"""
|
|
19
|
+
Extract SMS conversation details from webhook events.
|
|
20
|
+
|
|
21
|
+
This function organizes SMS events into conversations based on phone numbers.
|
|
22
|
+
It tracks messages between the same pair of numbers.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
webhook_events: List of webhook events
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
List of conversation summaries
|
|
29
|
+
"""
|
|
30
|
+
# Dictionary to store conversations by conversation ID (combo of to/from numbers)
|
|
31
|
+
conversations = defaultdict(
|
|
32
|
+
lambda: {
|
|
33
|
+
"messages": [],
|
|
34
|
+
"participants": set(),
|
|
35
|
+
"last_message_time": None,
|
|
36
|
+
"started_at": None,
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Process all webhook events
|
|
41
|
+
for event in webhook_events:
|
|
42
|
+
# Check if this is an SMS-related event
|
|
43
|
+
event_type = event.get("event_type", "")
|
|
44
|
+
payload = event.get("payload", {})
|
|
45
|
+
|
|
46
|
+
if "message" in event_type.lower() or "sms" in event_type.lower():
|
|
47
|
+
try:
|
|
48
|
+
# Handle Telnyx webhook structure which has nested payload
|
|
49
|
+
data_payload = None
|
|
50
|
+
|
|
51
|
+
# First try to navigate through the Telnyx webhook structure
|
|
52
|
+
if payload and "data" in payload:
|
|
53
|
+
data = payload["data"]
|
|
54
|
+
if "payload" in data:
|
|
55
|
+
data_payload = data["payload"]
|
|
56
|
+
|
|
57
|
+
# If we found the proper payload structure, use it
|
|
58
|
+
if data_payload:
|
|
59
|
+
# Extract from phone number
|
|
60
|
+
from_info = data_payload.get("from", {})
|
|
61
|
+
from_number = None
|
|
62
|
+
if (
|
|
63
|
+
isinstance(from_info, dict)
|
|
64
|
+
and "phone_number" in from_info
|
|
65
|
+
):
|
|
66
|
+
from_number = from_info["phone_number"]
|
|
67
|
+
elif isinstance(from_info, str):
|
|
68
|
+
from_number = from_info
|
|
69
|
+
|
|
70
|
+
# Extract to phone number
|
|
71
|
+
to_info = data_payload.get("to", [])
|
|
72
|
+
to_number = None
|
|
73
|
+
if isinstance(to_info, list) and len(to_info) > 0:
|
|
74
|
+
first_to = to_info[0]
|
|
75
|
+
if (
|
|
76
|
+
isinstance(first_to, dict)
|
|
77
|
+
and "phone_number" in first_to
|
|
78
|
+
):
|
|
79
|
+
to_number = first_to["phone_number"]
|
|
80
|
+
elif (
|
|
81
|
+
isinstance(to_info, dict) and "phone_number" in to_info
|
|
82
|
+
):
|
|
83
|
+
to_number = to_info["phone_number"]
|
|
84
|
+
elif isinstance(to_info, str):
|
|
85
|
+
to_number = to_info
|
|
86
|
+
|
|
87
|
+
# Get message text
|
|
88
|
+
message_text = data_payload.get("text", "")
|
|
89
|
+
|
|
90
|
+
# Extract direction and timestamp
|
|
91
|
+
direction = data_payload.get("direction", "")
|
|
92
|
+
|
|
93
|
+
# Try different timestamp fields
|
|
94
|
+
message_time = None
|
|
95
|
+
timestamp_fields = [
|
|
96
|
+
"received_at",
|
|
97
|
+
"sent_at",
|
|
98
|
+
"completed_at",
|
|
99
|
+
"timestamp",
|
|
100
|
+
]
|
|
101
|
+
for field in timestamp_fields:
|
|
102
|
+
if field in data_payload and data_payload[field]:
|
|
103
|
+
message_time = data_payload[field]
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
if not message_time:
|
|
107
|
+
occurred_at = data.get("occurred_at")
|
|
108
|
+
if occurred_at:
|
|
109
|
+
message_time = occurred_at
|
|
110
|
+
else:
|
|
111
|
+
message_time = event.get(
|
|
112
|
+
"timestamp", datetime.now().isoformat()
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
# Fallback to simpler webhook format
|
|
116
|
+
# Extract message details from payload
|
|
117
|
+
data = payload.get("data", payload)
|
|
118
|
+
|
|
119
|
+
# Extract message details
|
|
120
|
+
from_number = None
|
|
121
|
+
from_info = data.get("from", {})
|
|
122
|
+
if (
|
|
123
|
+
isinstance(from_info, dict)
|
|
124
|
+
and "phone_number" in from_info
|
|
125
|
+
):
|
|
126
|
+
from_number = from_info["phone_number"]
|
|
127
|
+
elif isinstance(from_info, str):
|
|
128
|
+
from_number = from_info
|
|
129
|
+
|
|
130
|
+
to_number = None
|
|
131
|
+
# Handle different payload structures
|
|
132
|
+
to_info = data.get("to", [])
|
|
133
|
+
if isinstance(to_info, list) and len(to_info) > 0:
|
|
134
|
+
first_to = to_info[0]
|
|
135
|
+
if (
|
|
136
|
+
isinstance(first_to, dict)
|
|
137
|
+
and "phone_number" in first_to
|
|
138
|
+
):
|
|
139
|
+
to_number = first_to["phone_number"]
|
|
140
|
+
elif (
|
|
141
|
+
isinstance(to_info, dict) and "phone_number" in to_info
|
|
142
|
+
):
|
|
143
|
+
to_number = to_info["phone_number"]
|
|
144
|
+
elif isinstance(to_info, str):
|
|
145
|
+
to_number = to_info
|
|
146
|
+
|
|
147
|
+
# Extract message content
|
|
148
|
+
message_text = data.get("text", "")
|
|
149
|
+
|
|
150
|
+
# Try different timestamp fields based on webhook format
|
|
151
|
+
timestamp_fields = [
|
|
152
|
+
"timestamp",
|
|
153
|
+
"received_at",
|
|
154
|
+
"sent_at",
|
|
155
|
+
"created_at",
|
|
156
|
+
"updated_at",
|
|
157
|
+
]
|
|
158
|
+
message_time = None
|
|
159
|
+
for field in timestamp_fields:
|
|
160
|
+
if field in data and data[field]:
|
|
161
|
+
message_time = data[field]
|
|
162
|
+
break
|
|
163
|
+
|
|
164
|
+
if not message_time:
|
|
165
|
+
message_time = event.get(
|
|
166
|
+
"timestamp", datetime.now().isoformat()
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Determine direction
|
|
170
|
+
direction = data.get("direction", "")
|
|
171
|
+
|
|
172
|
+
# Skip if we can't identify the numbers
|
|
173
|
+
if not from_number or not to_number:
|
|
174
|
+
logger.warning(
|
|
175
|
+
f"Could not identify from or to number in event: {event_type}"
|
|
176
|
+
)
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# Create a unique conversation ID (sort numbers to ensure consistency)
|
|
180
|
+
conv_participants = sorted([from_number, to_number])
|
|
181
|
+
conversation_id = (
|
|
182
|
+
f"{conv_participants[0]}:{conv_participants[1]}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Determine direction if not already set
|
|
186
|
+
if not direction:
|
|
187
|
+
if "outbound" in event_type.lower():
|
|
188
|
+
direction = "outbound"
|
|
189
|
+
elif (
|
|
190
|
+
"inbound" in event_type.lower()
|
|
191
|
+
or "received" in event_type.lower()
|
|
192
|
+
):
|
|
193
|
+
direction = "inbound"
|
|
194
|
+
else:
|
|
195
|
+
direction = "unknown"
|
|
196
|
+
|
|
197
|
+
# Get message ID if available
|
|
198
|
+
message_id = None
|
|
199
|
+
if data_payload and "id" in data_payload:
|
|
200
|
+
message_id = data_payload["id"]
|
|
201
|
+
elif "id" in data:
|
|
202
|
+
message_id = data["id"]
|
|
203
|
+
|
|
204
|
+
# Create message object
|
|
205
|
+
message = {
|
|
206
|
+
"id": message_id,
|
|
207
|
+
"from": from_number,
|
|
208
|
+
"to": to_number,
|
|
209
|
+
"text": message_text,
|
|
210
|
+
"timestamp": message_time,
|
|
211
|
+
"direction": direction,
|
|
212
|
+
"event_type": event_type,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Log the extracted message for debugging
|
|
216
|
+
logger.debug(
|
|
217
|
+
f"Extracted message: {from_number} -> {to_number}: '{message_text}'"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# Add/update conversation details
|
|
221
|
+
conversations[conversation_id]["messages"].append(message)
|
|
222
|
+
conversations[conversation_id]["participants"].add(from_number)
|
|
223
|
+
conversations[conversation_id]["participants"].add(to_number)
|
|
224
|
+
|
|
225
|
+
# Update timestamps
|
|
226
|
+
if not conversations[conversation_id]["started_at"]:
|
|
227
|
+
conversations[conversation_id]["started_at"] = message_time
|
|
228
|
+
|
|
229
|
+
conversations[conversation_id]["last_message_time"] = (
|
|
230
|
+
message_time
|
|
231
|
+
)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.error(f"Error processing message event: {e}")
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
# Convert to list of conversations
|
|
237
|
+
result = []
|
|
238
|
+
for conv_id, data in conversations.items():
|
|
239
|
+
# Only include conversations with at least one message
|
|
240
|
+
if len(data["messages"]) > 0:
|
|
241
|
+
# Sort messages by timestamp
|
|
242
|
+
sorted_messages = sorted(
|
|
243
|
+
data["messages"], key=lambda m: m.get("timestamp", "")
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# Create conversation summary
|
|
247
|
+
conversation = {
|
|
248
|
+
"conversation_id": conv_id,
|
|
249
|
+
"participants": list(data["participants"]),
|
|
250
|
+
"message_count": len(sorted_messages),
|
|
251
|
+
"started_at": data["started_at"],
|
|
252
|
+
"last_message_time": data["last_message_time"],
|
|
253
|
+
"last_message": sorted_messages[-1]["text"]
|
|
254
|
+
if sorted_messages
|
|
255
|
+
else "",
|
|
256
|
+
"messages": sorted_messages,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
result.append(conversation)
|
|
260
|
+
|
|
261
|
+
# Sort conversations by last message time (most recent first)
|
|
262
|
+
return sorted(
|
|
263
|
+
result, key=lambda c: c.get("last_message_time", ""), reverse=True
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@mcp.resource(SMS_CONVERSATIONS_URI)
|
|
268
|
+
def get_sms_conversations() -> Dict[str, Any]:
|
|
269
|
+
"""
|
|
270
|
+
Get a list of ongoing SMS conversations.
|
|
271
|
+
|
|
272
|
+
This resource extracts and organizes SMS conversations from webhook events.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Dict[str, Any]: List of SMS conversations with participants and messages
|
|
276
|
+
"""
|
|
277
|
+
try:
|
|
278
|
+
# Get all webhook history
|
|
279
|
+
webhook_events = get_webhook_history()
|
|
280
|
+
|
|
281
|
+
# Log webhook count
|
|
282
|
+
logger.info(f"Got {len(webhook_events)} webhook events for processing")
|
|
283
|
+
|
|
284
|
+
# Log some details about the first event
|
|
285
|
+
if webhook_events:
|
|
286
|
+
first_event = webhook_events[0]
|
|
287
|
+
event_type = first_event.get("event_type", "unknown")
|
|
288
|
+
logger.info(f"First event type: {event_type}")
|
|
289
|
+
|
|
290
|
+
# Log structure of webhook payload
|
|
291
|
+
logger.info(
|
|
292
|
+
f"Webhook payload structure: {list(first_event.keys())}"
|
|
293
|
+
)
|
|
294
|
+
if "payload" in first_event:
|
|
295
|
+
payload = first_event["payload"]
|
|
296
|
+
if isinstance(payload, dict):
|
|
297
|
+
logger.info(f"Payload keys: {list(payload.keys())}")
|
|
298
|
+
|
|
299
|
+
# Log data structure if present
|
|
300
|
+
if "data" in payload:
|
|
301
|
+
data = payload["data"]
|
|
302
|
+
if isinstance(data, dict):
|
|
303
|
+
logger.info(f"Data keys: {list(data.keys())}")
|
|
304
|
+
|
|
305
|
+
# Log inner payload structure if present
|
|
306
|
+
if "payload" in data:
|
|
307
|
+
inner_payload = data["payload"]
|
|
308
|
+
if isinstance(inner_payload, dict):
|
|
309
|
+
logger.info(
|
|
310
|
+
f"Inner payload keys: {list(inner_payload.keys())}"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Extract conversations from webhook events
|
|
314
|
+
conversations = _extract_conversation_details(webhook_events)
|
|
315
|
+
|
|
316
|
+
# Log the number of conversations found
|
|
317
|
+
logger.info(
|
|
318
|
+
f"Found {len(conversations)} SMS conversations from webhook history"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
"conversations": conversations,
|
|
323
|
+
"count": len(conversations),
|
|
324
|
+
"updated_at": datetime.now().isoformat(),
|
|
325
|
+
"source": "webhook_history",
|
|
326
|
+
"webhook_count": len(webhook_events),
|
|
327
|
+
}
|
|
328
|
+
except Exception as e:
|
|
329
|
+
logger.error(f"Error retrieving SMS conversations: {e}")
|
|
330
|
+
logger.error(f"Exception traceback: {str(e.__traceback__)}")
|
|
331
|
+
return {
|
|
332
|
+
"error": f"Failed to retrieve SMS conversations: {str(e)}",
|
|
333
|
+
"conversations": [],
|
|
334
|
+
"count": 0,
|
|
335
|
+
"updated_at": datetime.now().isoformat(),
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@mcp.resource("resource://sms/recent/{limit}")
|
|
340
|
+
def get_recent_conversations(limit: int = 5) -> Dict[str, Any]:
|
|
341
|
+
"""
|
|
342
|
+
Get a list of the most recent SMS conversations.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
limit: Maximum number of conversations to return
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Dict[str, Any]: List of recent SMS conversations with participants and messages
|
|
349
|
+
"""
|
|
350
|
+
try:
|
|
351
|
+
# Get all conversations
|
|
352
|
+
all_conversations = get_sms_conversations()
|
|
353
|
+
|
|
354
|
+
# Get the conversations (they're already sorted by most recent first)
|
|
355
|
+
conversations = all_conversations.get("conversations", [])
|
|
356
|
+
|
|
357
|
+
# Limit the number of conversations
|
|
358
|
+
limited_conversations = conversations[: min(limit, len(conversations))]
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
"conversations": limited_conversations,
|
|
362
|
+
"count": len(limited_conversations),
|
|
363
|
+
"total_available": len(conversations),
|
|
364
|
+
"limit": limit,
|
|
365
|
+
"updated_at": datetime.now().isoformat(),
|
|
366
|
+
}
|
|
367
|
+
except Exception as e:
|
|
368
|
+
logger.error(f"Error retrieving recent conversations: {e}")
|
|
369
|
+
return {
|
|
370
|
+
"error": f"Failed to retrieve recent conversations: {str(e)}",
|
|
371
|
+
"conversations": [],
|
|
372
|
+
"count": 0,
|
|
373
|
+
"updated_at": datetime.now().isoformat(),
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# Resource template for individual conversations
|
|
378
|
+
@mcp.resource("resource://sms/conversation/{conversation_id}")
|
|
379
|
+
def get_sms_conversation(conversation_id: str) -> Dict[str, Any]:
|
|
380
|
+
"""
|
|
381
|
+
Get details for a specific SMS conversation.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
conversation_id: The ID of the conversation (format: "number1:number2")
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Dict[str, Any]: Detailed conversation data including all messages
|
|
388
|
+
"""
|
|
389
|
+
try:
|
|
390
|
+
# Get all conversations
|
|
391
|
+
all_conversations = get_sms_conversations()
|
|
392
|
+
|
|
393
|
+
# Find the specific conversation
|
|
394
|
+
for conversation in all_conversations.get("conversations", []):
|
|
395
|
+
if conversation.get("conversation_id") == conversation_id:
|
|
396
|
+
return {
|
|
397
|
+
"conversation": conversation,
|
|
398
|
+
"updated_at": datetime.now().isoformat(),
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
# Conversation not found
|
|
402
|
+
return {
|
|
403
|
+
"error": f"Conversation {conversation_id} not found",
|
|
404
|
+
"updated_at": datetime.now().isoformat(),
|
|
405
|
+
}
|
|
406
|
+
except Exception as e:
|
|
407
|
+
logger.error(
|
|
408
|
+
f"Error retrieving SMS conversation {conversation_id}: {e}"
|
|
409
|
+
)
|
|
410
|
+
return {
|
|
411
|
+
"error": f"Failed to retrieve SMS conversation: {str(e)}",
|
|
412
|
+
"updated_at": datetime.now().isoformat(),
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# Resource template for conversations by phone number
|
|
417
|
+
@mcp.resource("resource://sms/by_number/{phone_number}")
|
|
418
|
+
def get_conversations_by_number(phone_number: str) -> Dict[str, Any]:
|
|
419
|
+
"""
|
|
420
|
+
Get all conversations involving a specific phone number.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
phone_number: The phone number to find conversations for
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Dict[str, Any]: List of conversations involving the phone number
|
|
427
|
+
"""
|
|
428
|
+
try:
|
|
429
|
+
# Get all conversations
|
|
430
|
+
all_conversations = get_sms_conversations()
|
|
431
|
+
|
|
432
|
+
# Filter conversations by phone number
|
|
433
|
+
matching_conversations = []
|
|
434
|
+
for conversation in all_conversations.get("conversations", []):
|
|
435
|
+
participants = conversation.get("participants", [])
|
|
436
|
+
if phone_number in participants:
|
|
437
|
+
matching_conversations.append(conversation)
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
"phone_number": phone_number,
|
|
441
|
+
"conversations": matching_conversations,
|
|
442
|
+
"count": len(matching_conversations),
|
|
443
|
+
"updated_at": datetime.now().isoformat(),
|
|
444
|
+
}
|
|
445
|
+
except Exception as e:
|
|
446
|
+
logger.error(
|
|
447
|
+
f"Error retrieving conversations for number {phone_number}: {e}"
|
|
448
|
+
)
|
|
449
|
+
return {
|
|
450
|
+
"error": f"Failed to retrieve conversations: {str(e)}",
|
|
451
|
+
"phone_number": phone_number,
|
|
452
|
+
"conversations": [],
|
|
453
|
+
"count": 0,
|
|
454
|
+
"updated_at": datetime.now().isoformat(),
|
|
455
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Webhook management and information tools."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
from pydantic.networks import AnyUrl
|
|
6
|
+
|
|
7
|
+
from ..config import settings
|
|
8
|
+
from ..mcp import mcp
|
|
9
|
+
from ..utils.logger import get_logger
|
|
10
|
+
from ..webhook import get_webhook_history, webhook_handler
|
|
11
|
+
|
|
12
|
+
logger = get_logger(__name__)
|
|
13
|
+
|
|
14
|
+
# Resource URI for webhook info
|
|
15
|
+
WEBHOOK_INFO_URI = "resource://webhook/info"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@mcp.resource(WEBHOOK_INFO_URI)
|
|
19
|
+
def get_webhook_info() -> Dict[str, Any]:
|
|
20
|
+
"""
|
|
21
|
+
Get information about the webhook tunnel.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Dict[str, Any]: Webhook tunnel information
|
|
25
|
+
"""
|
|
26
|
+
# Get last error from webhook history if available
|
|
27
|
+
last_error = None
|
|
28
|
+
try:
|
|
29
|
+
for event in get_webhook_history(5):
|
|
30
|
+
if event.get("event_type") == "ngrok.error":
|
|
31
|
+
last_error = event.get("payload", {}).get(
|
|
32
|
+
"error", "Unknown error"
|
|
33
|
+
)
|
|
34
|
+
break
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"webhook_tunnel": {
|
|
40
|
+
"enabled": settings.webhook_enabled,
|
|
41
|
+
"public_url": webhook_handler.public_url,
|
|
42
|
+
"active": webhook_handler.listener is not None,
|
|
43
|
+
"path": settings.webhook_path,
|
|
44
|
+
"full_url": f"{webhook_handler.public_url}{settings.webhook_path}"
|
|
45
|
+
if webhook_handler.public_url
|
|
46
|
+
else None,
|
|
47
|
+
"last_error": last_error,
|
|
48
|
+
"status": "active"
|
|
49
|
+
if webhook_handler.listener is not None
|
|
50
|
+
else "inactive",
|
|
51
|
+
},
|
|
52
|
+
"ngrok": {
|
|
53
|
+
"enabled": settings.ngrok_enabled,
|
|
54
|
+
"auth_token_provided": bool(settings.ngrok_authtoken),
|
|
55
|
+
"custom_domain_setting": bool(settings.ngrok_url),
|
|
56
|
+
"using_dynamic_url": webhook_handler.public_url is not None
|
|
57
|
+
and (
|
|
58
|
+
settings.ngrok_url is None
|
|
59
|
+
or settings.ngrok_url not in webhook_handler.public_url
|
|
60
|
+
),
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def notify_webhook_info_updated(session):
|
|
66
|
+
"""Notify clients that the webhook info resource has been updated."""
|
|
67
|
+
try:
|
|
68
|
+
await session.send_resource_updated(AnyUrl(WEBHOOK_INFO_URI))
|
|
69
|
+
logger.info(
|
|
70
|
+
f"Sent resource update notification for {WEBHOOK_INFO_URI}"
|
|
71
|
+
)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.error(f"Failed to send resource update notification: {str(e)}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@mcp.tool()
|
|
77
|
+
async def get_webhook_events(
|
|
78
|
+
limit: int = 10, event_type: str = None
|
|
79
|
+
) -> Dict[str, Any]:
|
|
80
|
+
"""
|
|
81
|
+
Get the most recent webhook events received by the handler.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
limit: Maximum number of events to return (default: 10)
|
|
85
|
+
event_type: Filter events by type (default: all types)
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Dict containing webhook events and metadata
|
|
89
|
+
"""
|
|
90
|
+
# Get webhook history
|
|
91
|
+
history = get_webhook_history()
|
|
92
|
+
|
|
93
|
+
# Apply event_type filter if specified
|
|
94
|
+
if event_type:
|
|
95
|
+
history = [h for h in history if h.get("event_type") == event_type]
|
|
96
|
+
|
|
97
|
+
# Apply limit
|
|
98
|
+
if limit > 0:
|
|
99
|
+
history = history[:limit]
|
|
100
|
+
|
|
101
|
+
webhook_status = {
|
|
102
|
+
"count": len(history),
|
|
103
|
+
"tunnel_enabled": settings.webhook_enabled,
|
|
104
|
+
"has_webhooks": len(history) > 0,
|
|
105
|
+
"webhook_url": webhook_handler.public_url,
|
|
106
|
+
"webhook_endpoint": f"{webhook_handler.public_url}{settings.webhook_path}"
|
|
107
|
+
if webhook_handler.public_url
|
|
108
|
+
else None,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {"events": history, "status": webhook_status}
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Error handling utilities."""
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
from .logger import get_logger
|
|
6
|
+
|
|
7
|
+
logger = get_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def handle_telnyx_error(error: Exception) -> Exception:
|
|
11
|
+
"""Handle Telnyx API errors.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
error: Original error
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Exception: Handled error
|
|
18
|
+
"""
|
|
19
|
+
if isinstance(error, requests.HTTPError):
|
|
20
|
+
response = error.response
|
|
21
|
+
try:
|
|
22
|
+
data = response.json()
|
|
23
|
+
if "errors" in data and isinstance(data["errors"], list):
|
|
24
|
+
errors = data["errors"]
|
|
25
|
+
if errors:
|
|
26
|
+
error_message = errors[0].get("detail", str(error))
|
|
27
|
+
return Exception(f"Telnyx API error: {error_message}")
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
return error
|