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.
Files changed (40) hide show
  1. telnyx_mcp_server/__init__.py +0 -0
  2. telnyx_mcp_server/__main__.py +23 -0
  3. telnyx_mcp_server/config.py +148 -0
  4. telnyx_mcp_server/mcp.py +148 -0
  5. telnyx_mcp_server/server.py +497 -0
  6. telnyx_mcp_server/telnyx/__init__.py +1 -0
  7. telnyx_mcp_server/telnyx/client.py +363 -0
  8. telnyx_mcp_server/telnyx/services/__init__.py +0 -0
  9. telnyx_mcp_server/telnyx/services/assistants.py +155 -0
  10. telnyx_mcp_server/telnyx/services/call_control.py +217 -0
  11. telnyx_mcp_server/telnyx/services/cloud_storage.py +289 -0
  12. telnyx_mcp_server/telnyx/services/connections.py +92 -0
  13. telnyx_mcp_server/telnyx/services/embeddings.py +52 -0
  14. telnyx_mcp_server/telnyx/services/messaging.py +93 -0
  15. telnyx_mcp_server/telnyx/services/messaging_profiles.py +196 -0
  16. telnyx_mcp_server/telnyx/services/numbers.py +193 -0
  17. telnyx_mcp_server/telnyx/services/secrets.py +74 -0
  18. telnyx_mcp_server/tools/__init__.py +126 -0
  19. telnyx_mcp_server/tools/assistants.py +313 -0
  20. telnyx_mcp_server/tools/call_control.py +242 -0
  21. telnyx_mcp_server/tools/cloud_storage.py +183 -0
  22. telnyx_mcp_server/tools/connections.py +78 -0
  23. telnyx_mcp_server/tools/embeddings.py +80 -0
  24. telnyx_mcp_server/tools/messaging.py +57 -0
  25. telnyx_mcp_server/tools/messaging_profiles.py +123 -0
  26. telnyx_mcp_server/tools/phone_numbers.py +161 -0
  27. telnyx_mcp_server/tools/secrets.py +75 -0
  28. telnyx_mcp_server/tools/sms_conversations.py +455 -0
  29. telnyx_mcp_server/tools/webhooks.py +111 -0
  30. telnyx_mcp_server/utils/__init__.py +0 -0
  31. telnyx_mcp_server/utils/error_handler.py +30 -0
  32. telnyx_mcp_server/utils/logger.py +32 -0
  33. telnyx_mcp_server/utils/service.py +33 -0
  34. telnyx_mcp_server/webhook/__init__.py +25 -0
  35. telnyx_mcp_server/webhook/handler.py +596 -0
  36. telnyx_mcp_server/webhook/server.py +369 -0
  37. telnyx_mcp_server_fastmcp-0.1.3.dist-info/METADATA +430 -0
  38. telnyx_mcp_server_fastmcp-0.1.3.dist-info/RECORD +40 -0
  39. telnyx_mcp_server_fastmcp-0.1.3.dist-info/WHEEL +4 -0
  40. 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