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,369 @@
1
+ """Lightweight Unix Socket server for handling webhooks without port conflicts."""
2
+
3
+ from collections import deque
4
+ from datetime import datetime
5
+ from http.server import BaseHTTPRequestHandler
6
+ import json
7
+ import os
8
+ from socketserver import UnixStreamServer
9
+ import tempfile
10
+ import threading
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ from ..config import settings
14
+ from ..utils.logger import get_logger
15
+
16
+ logger = get_logger(__name__)
17
+
18
+ # Store webhook history (most recent first)
19
+ webhook_history = deque(maxlen=100) # Store last 100 webhooks
20
+
21
+
22
+ class UnixSocketHandler(BaseHTTPRequestHandler):
23
+ """HTTP request handler for Unix domain sockets."""
24
+
25
+ # Ensure HTTP/1.1 is used consistently
26
+ protocol_version = "HTTP/1.1"
27
+
28
+ # Override address_string to fix the IndexError with Unix sockets
29
+ def address_string(self):
30
+ """Return a string representation of the client address."""
31
+ # For Unix sockets, client_address is often a string or None
32
+ # Just return a placeholder instead of trying to index it
33
+ return "unix-client"
34
+
35
+ # Override log_message to ensure it doesn't fail with Unix sockets
36
+ def log_message(self, format, *args):
37
+ """Log an arbitrary message to the server log."""
38
+ # Use a more robust implementation that won't fail with Unix sockets
39
+ try:
40
+ message = format % args
41
+ logger.info(
42
+ f"[UnixSocketHandler] {self.command} {self.path} - {message}"
43
+ )
44
+ except Exception as e:
45
+ # Failsafe logging that doesn't rely on string formatting
46
+ logger.info(
47
+ f"[UnixSocketHandler] Request processed: {self.command} {self.path}"
48
+ )
49
+
50
+ def do_POST(self):
51
+ """Handle POST requests from the ngrok tunnel."""
52
+ try:
53
+ # Get content length
54
+ content_length = int(self.headers.get("Content-Length", 0))
55
+ if content_length > settings.webhook_max_body_size:
56
+ self.send_error(413, "Request entity too large")
57
+ return
58
+
59
+ # Read the request body
60
+ body = self.rfile.read(content_length)
61
+
62
+ # Process the webhook request
63
+ self._process_webhook(body)
64
+
65
+ # Prepare a valid JSON response
66
+ response = json.dumps(
67
+ {
68
+ "status": "success",
69
+ "message": "Webhook received",
70
+ "timestamp": datetime.now().isoformat(),
71
+ }
72
+ )
73
+ response_bytes = response.encode("utf-8")
74
+
75
+ try:
76
+ # Send a properly formatted HTTP response with more robust error handling
77
+ self.send_response(200)
78
+ # Set headers with proper HTTP/1.1 compliance
79
+ self.send_header(
80
+ "Content-Type", "application/json; charset=utf-8"
81
+ )
82
+ self.send_header("Content-Length", str(len(response_bytes)))
83
+ self.send_header(
84
+ "Connection", "close"
85
+ ) # Important for HTTP/1.1
86
+ # Add more headers to help with debugging
87
+ self.send_header("X-Webhook-Handler", "Telnyx-MCP-Unix-Socket")
88
+ self.send_header("Cache-Control", "no-store, no-cache")
89
+ self.end_headers()
90
+
91
+ # Write the response in a try-except block
92
+ try:
93
+ self.wfile.write(response_bytes)
94
+ self.wfile.flush() # Ensure the response is sent completely
95
+ except (BrokenPipeError, ConnectionResetError) as pipe_error:
96
+ # Client disconnected - log but don't re-raise
97
+ logger.warning(
98
+ f"Client disconnected during response: {pipe_error}"
99
+ )
100
+ except Exception as write_error:
101
+ logger.error(f"Error writing response: {write_error}")
102
+ # Don't re-raise - we've already processed the webhook
103
+ except Exception as resp_error:
104
+ logger.error(f"Error sending HTTP response: {resp_error}")
105
+ # Don't re-raise - we've already processed the webhook
106
+ except Exception as e:
107
+ logger.error(f"Error handling webhook: {str(e)}")
108
+ # Send a properly formatted error response with enhanced robustness
109
+ try:
110
+ error_response = json.dumps(
111
+ {
112
+ "status": "error",
113
+ "message": str(e),
114
+ "timestamp": datetime.now().isoformat(),
115
+ }
116
+ )
117
+ error_bytes = error_response.encode("utf-8")
118
+
119
+ self.send_response(500)
120
+ self.send_header(
121
+ "Content-Type", "application/json; charset=utf-8"
122
+ )
123
+ self.send_header("Content-Length", str(len(error_bytes)))
124
+ self.send_header("Connection", "close")
125
+ self.send_header("X-Webhook-Handler", "Telnyx-MCP-Unix-Socket")
126
+ self.end_headers()
127
+
128
+ # Write error response with exception handling
129
+ try:
130
+ self.wfile.write(error_bytes)
131
+ self.wfile.flush()
132
+ except (BrokenPipeError, ConnectionResetError):
133
+ # Client disconnected - just log it
134
+ logger.warning("Client disconnected during error response")
135
+ except Exception as write_err:
136
+ logger.error(f"Error writing error response: {write_err}")
137
+ except Exception as resp_err:
138
+ logger.error(f"Failed to send error response: {resp_err}")
139
+
140
+ def do_GET(self):
141
+ """Handle GET requests for health checks."""
142
+ try:
143
+ # Prepare a valid JSON response
144
+ response = json.dumps(
145
+ {
146
+ "status": "ok",
147
+ "time": datetime.now().isoformat(),
148
+ "path": self.path,
149
+ "webhook_server": "Telnyx-MCP-Unix-Socket",
150
+ }
151
+ )
152
+ response_bytes = response.encode("utf-8")
153
+
154
+ # Send a properly formatted HTTP response with robust error handling
155
+ self.send_response(200)
156
+ self.send_header("Content-Type", "application/json; charset=utf-8")
157
+ self.send_header("Content-Length", str(len(response_bytes)))
158
+ self.send_header("Connection", "close")
159
+ self.send_header("X-Webhook-Handler", "Telnyx-MCP-Unix-Socket")
160
+ self.send_header("Cache-Control", "no-store, no-cache")
161
+ self.end_headers()
162
+
163
+ # Write the response with exception handling
164
+ try:
165
+ self.wfile.write(response_bytes)
166
+ self.wfile.flush() # Ensure the response is sent completely
167
+ except (BrokenPipeError, ConnectionResetError) as pipe_error:
168
+ # Client disconnected - log but don't re-raise
169
+ logger.warning(
170
+ f"Client disconnected during health check response: {pipe_error}"
171
+ )
172
+ except Exception as write_error:
173
+ logger.error(
174
+ f"Error writing health check response: {write_error}"
175
+ )
176
+ except Exception as e:
177
+ logger.error(f"Error handling health check request: {str(e)}")
178
+ # Try to send an error response
179
+ try:
180
+ error_response = json.dumps(
181
+ {"status": "error", "message": str(e)}
182
+ )
183
+ error_bytes = error_response.encode("utf-8")
184
+ self.send_response(500)
185
+ self.send_header(
186
+ "Content-Type", "application/json; charset=utf-8"
187
+ )
188
+ self.send_header("Content-Length", str(len(error_bytes)))
189
+ self.send_header("Connection", "close")
190
+ self.end_headers()
191
+ self.wfile.write(error_bytes)
192
+ self.wfile.flush()
193
+ except Exception:
194
+ # If that also fails, just log and continue
195
+ logger.error("Failed to send health check error response")
196
+
197
+ def _process_webhook(self, body: bytes) -> None:
198
+ """Process the webhook request."""
199
+ try:
200
+ # Parse the request body as JSON
201
+ if body:
202
+ payload = json.loads(body)
203
+
204
+ # Extract event type from payload - check both root level and nested data
205
+ # This handles both {event_type: "x"} and {data: {event_type: "x"}} formats
206
+ if "event_type" in payload:
207
+ event_type = payload.get("event_type", "unknown")
208
+ else:
209
+ # Fall back to checking in data object or default to unknown
210
+ event_type = payload.get("data", {}).get(
211
+ "event_type", "unknown"
212
+ )
213
+
214
+ # Log the webhook
215
+ logger.info(f"Received webhook event: {event_type}")
216
+ logger.debug(
217
+ f"Webhook payload: {json.dumps(payload, indent=2)}"
218
+ )
219
+
220
+ # Store in history
221
+ webhook_history.appendleft(
222
+ {
223
+ "timestamp": datetime.now().isoformat(),
224
+ "event_type": event_type,
225
+ "payload": payload,
226
+ "headers": dict(self.headers),
227
+ }
228
+ )
229
+
230
+ logger.debug(
231
+ f"Added webhook to history (total: {len(webhook_history)})"
232
+ )
233
+ else:
234
+ logger.warning("Received empty webhook body")
235
+ except json.JSONDecodeError:
236
+ logger.error("Failed to parse webhook payload as JSON")
237
+ except Exception as e:
238
+ logger.error(f"Error processing webhook: {str(e)}")
239
+ raise # Re-raise to allow proper error response
240
+
241
+
242
+ class UnixSocketHTTPServer(UnixStreamServer):
243
+ """HTTP server using Unix domain sockets."""
244
+
245
+ def __init__(self, server_address, RequestHandlerClass):
246
+ """Initialize the server with a Unix socket."""
247
+ super().__init__(server_address, RequestHandlerClass)
248
+ self._thread = None
249
+ self._running = False
250
+
251
+ def start(self):
252
+ """Start the server in a background thread."""
253
+ self._running = True
254
+ self._thread = threading.Thread(target=self.serve_forever)
255
+ self._thread.daemon = True
256
+ self._thread.start()
257
+ logger.info(f"Unix socket server started on {self.server_address}")
258
+
259
+ def stop(self):
260
+ """Stop the server."""
261
+ if self._running:
262
+ self._running = False
263
+ self.shutdown()
264
+ if self._thread and self._thread.is_alive():
265
+ self._thread.join(timeout=5.0)
266
+ self.server_close()
267
+
268
+ # Clean up the socket file
269
+ try:
270
+ if os.path.exists(self.server_address):
271
+ os.unlink(self.server_address)
272
+ logger.info(f"Removed socket file: {self.server_address}")
273
+ except OSError as e:
274
+ logger.warning(f"Error removing socket file: {e}")
275
+
276
+
277
+ # Global server instance
278
+ socket_server = None
279
+ socket_path = None
280
+
281
+
282
+ def generate_socket_path() -> str:
283
+ """Generate a unique socket path."""
284
+ temp_dir = tempfile.mkdtemp(prefix="telnyx_mcp_")
285
+ socket_name = f"webhook-{os.getpid()}.sock"
286
+ path = os.path.join(temp_dir, socket_name)
287
+ return path
288
+
289
+
290
+ def get_webhook_history(limit: int = None) -> List[Dict[str, Any]]:
291
+ """
292
+ Get the webhook history.
293
+
294
+ Args:
295
+ limit: Maximum number of webhooks to return (None for all)
296
+
297
+ Returns:
298
+ List of webhook events, most recent first
299
+ """
300
+ if limit is None or limit > len(webhook_history):
301
+ return list(webhook_history)
302
+ return list(webhook_history)[:limit]
303
+
304
+
305
+ def start_webhook_server() -> Optional[str]:
306
+ """
307
+ Start the webhook server using a Unix domain socket.
308
+
309
+ Returns:
310
+ Optional[str]: The socket path if successful, None otherwise
311
+ """
312
+ global socket_server, socket_path
313
+
314
+ if not settings.webhook_enabled:
315
+ logger.info("Webhook server is disabled")
316
+ return None
317
+
318
+ try:
319
+ # Generate a unique socket path
320
+ socket_path = generate_socket_path()
321
+ logger.info(f"Using Unix domain socket at: {socket_path}")
322
+
323
+ # Create the directory if it doesn't exist
324
+ os.makedirs(os.path.dirname(socket_path), exist_ok=True)
325
+
326
+ # Clean up any existing socket file
327
+ if os.path.exists(socket_path):
328
+ try:
329
+ os.unlink(socket_path)
330
+ except OSError as e:
331
+ logger.warning(f"Could not remove existing socket file: {e}")
332
+
333
+ # Create and start the server
334
+ socket_server = UnixSocketHTTPServer(socket_path, UnixSocketHandler)
335
+ socket_server.start()
336
+
337
+ # Register cleanup on exit - but avoid signal handlers which won't work in all threads
338
+ import atexit
339
+
340
+ atexit.register(stop_webhook_server)
341
+
342
+ return socket_path
343
+ except Exception as e:
344
+ logger.error(f"Failed to start webhook server: {e}")
345
+ return None
346
+
347
+
348
+ def stop_webhook_server() -> None:
349
+ """Stop the webhook server."""
350
+ global socket_server, socket_path
351
+
352
+ if socket_server:
353
+ logger.info("Stopping webhook server...")
354
+ socket_server.stop()
355
+ socket_server = None
356
+
357
+ # Clean up the socket directory
358
+ if socket_path:
359
+ try:
360
+ dir_path = os.path.dirname(socket_path)
361
+ if os.path.exists(dir_path) and dir_path.startswith(
362
+ tempfile.gettempdir()
363
+ ):
364
+ os.rmdir(dir_path)
365
+ logger.info(f"Removed socket directory: {dir_path}")
366
+ except OSError as e:
367
+ logger.warning(f"Error removing socket directory: {e}")
368
+
369
+ socket_path = None