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,497 @@
1
+ """Telnyx MCP server using STDIO transport."""
2
+
3
+ import argparse
4
+ import atexit
5
+ import os
6
+ import signal
7
+ import sys
8
+ import threading
9
+ import time
10
+ from typing import List, Optional
11
+
12
+ from dotenv import load_dotenv
13
+
14
+ from .config import settings
15
+ from .mcp import mcp # Import the shared MCP instance
16
+ from .telnyx.client import TelnyxClient
17
+ from .telnyx.services.assistants import AssistantsService
18
+ from .telnyx.services.connections import ConnectionsService
19
+ from .telnyx.services.messaging import MessagingService
20
+ from .telnyx.services.numbers import NumbersService
21
+ from .tools import * # Import all tools
22
+ from .utils.logger import get_logger
23
+ from .webhook import (
24
+ start_webhook_handler,
25
+ stop_webhook_handler,
26
+ )
27
+
28
+ def parse_args(args=None) -> argparse.Namespace:
29
+ """Parse command line arguments."""
30
+ parser = argparse.ArgumentParser(description="Telnyx MCP Server")
31
+
32
+ # Webhook arguments
33
+ parser.add_argument(
34
+ "--webhook-enabled",
35
+ action="store_true",
36
+ default=settings.webhook_enabled,
37
+ help="Enable webhook receiver",
38
+ )
39
+ parser.add_argument(
40
+ "--ngrok-enabled",
41
+ action="store_true",
42
+ default=settings.ngrok_enabled,
43
+ help="Enable ngrok tunnel for webhooks",
44
+ )
45
+ parser.add_argument(
46
+ "--ngrok-authtoken",
47
+ type=str,
48
+ default=settings.ngrok_authtoken,
49
+ help="Ngrok authentication token",
50
+ )
51
+ parser.add_argument(
52
+ "--ngrok-url",
53
+ type=str,
54
+ default=settings.ngrok_url,
55
+ help="Predefined ngrok URL to use instead of creating a new tunnel",
56
+ )
57
+ parser.add_argument(
58
+ "--tools",
59
+ help="Comma-separated list of tool names to enable. If not specified, all tools are enabled.",
60
+ type=str,
61
+ )
62
+ parser.add_argument(
63
+ "--exclude-tools",
64
+ help="Comma-separated list of tool names to disable.",
65
+ type=str,
66
+ )
67
+ parser.add_argument(
68
+ "--list-tools",
69
+ help="List all available tools and exit.",
70
+ action="store_true",
71
+ )
72
+
73
+ # Ignore unknown arguments to prevent errors with additional CLI parameters
74
+ # This allows the server to be started with additional CLI args that may be
75
+ # used by other implementers without causing errors
76
+ return parser.parse_known_args(args)[0]
77
+
78
+
79
+ # Check if just listing tools first
80
+ try:
81
+ args = parse_args()
82
+ just_listing = args.list_tools
83
+ except:
84
+ just_listing = False
85
+
86
+ logger = get_logger(__name__)
87
+
88
+ # Load environment variables
89
+ load_dotenv()
90
+
91
+ # Initialize Telnyx client with API key from environment
92
+ api_key = os.getenv("TELNYX_API_KEY", "")
93
+ if not api_key:
94
+ logger.critical("TELNYX_API_KEY environment variable must be set")
95
+ sys.exit(1)
96
+ if not just_listing:
97
+ api_key = os.getenv("TELNYX_API_KEY", "")
98
+ if not api_key:
99
+ logger.error("TELNYX_API_KEY environment variable must be set")
100
+ print("Error: TELNYX_API_KEY environment variable must be set")
101
+ sys.exit(1)
102
+ else:
103
+ # Use a dummy key for listing tools (won't be used for API calls)
104
+ api_key = "dummy_key_for_listing_tools"
105
+
106
+ # Import MCP and setup services
107
+ from .mcp import mcp # Import the shared MCP instance
108
+ from .telnyx.client import TelnyxClient
109
+ from .telnyx.services.assistants import AssistantsService
110
+ from .telnyx.services.connections import ConnectionsService
111
+ from .telnyx.services.embeddings import EmbeddingsService
112
+ from .telnyx.services.messaging import MessagingService
113
+ from .telnyx.services.numbers import NumbersService
114
+ from .telnyx.services.secrets import SecretsService
115
+ from .tools import * # Import all tools
116
+
117
+ telnyx_client = TelnyxClient(api_key=api_key)
118
+ numbers_service = NumbersService(telnyx_client)
119
+ connections_service = ConnectionsService(telnyx_client)
120
+ messaging_service = MessagingService(telnyx_client)
121
+ assistants_service = AssistantsService(telnyx_client)
122
+ embeddings_service = EmbeddingsService(telnyx_client)
123
+ secrets_service = SecretsService(telnyx_client)
124
+
125
+
126
+ def get_enabled_tools() -> Optional[List[str]]:
127
+ """
128
+ Get list of enabled tools from CLI args or environment variable.
129
+
130
+ Returns:
131
+ Optional[List[str]]: List of tool names to enable, or None to enable all tools
132
+ """
133
+ try:
134
+ args = parse_args()
135
+
136
+ # CLI args take precedence over environment variables
137
+ if args.tools:
138
+ logger.info(f"Using tool list from CLI arguments: {args.tools}")
139
+ return [tool.strip() for tool in args.tools.split(",")]
140
+ except:
141
+ # If argument parsing fails, fall back to environment variables
142
+ pass
143
+
144
+ # Check for environment variable
145
+ env_tools = os.getenv("TELNYX_MCP_TOOLS")
146
+ if env_tools:
147
+ logger.info(f"Using tool list from environment variable: {env_tools}")
148
+ return [tool.strip() for tool in env_tools.split(",")]
149
+
150
+ return None
151
+
152
+
153
+ def get_excluded_tools() -> List[str]:
154
+ """
155
+ Get list of tools to exclude from CLI args or environment variable.
156
+
157
+ Returns:
158
+ List[str]: List of tool names to disable
159
+ """
160
+ try:
161
+ args = parse_args()
162
+
163
+ # CLI args take precedence over environment variables
164
+ if args.exclude_tools:
165
+ logger.info(
166
+ f"Using tool exclusion list from CLI arguments: {args.exclude_tools}"
167
+ )
168
+ return [tool.strip() for tool in args.exclude_tools.split(",")]
169
+ except:
170
+ # If argument parsing fails, fall back to environment variables
171
+ pass
172
+
173
+ # Check for environment variable
174
+ env_exclude_tools = os.getenv("TELNYX_MCP_EXCLUDE_TOOLS")
175
+ if env_exclude_tools:
176
+ logger.info(
177
+ f"Using tool exclusion list from environment variable: {env_exclude_tools}"
178
+ )
179
+ return [tool.strip() for tool in env_exclude_tools.split(",")]
180
+
181
+ return []
182
+
183
+
184
+ async def list_all_tools() -> None:
185
+ """List all available tools and exit."""
186
+
187
+ # Create an instance of the MCP server to list the tools
188
+ tools_dict = await mcp.get_tools()
189
+
190
+ # Sort and print the tools
191
+ tools = sorted(tools_dict.items(), key=lambda x: x[0])
192
+
193
+ print("\nAvailable MCP Tools:")
194
+ print("===================")
195
+ for name, tool in tools:
196
+ description = (
197
+ tool.description if tool.description else "No description"
198
+ )
199
+ print(f"- {name}: {description}")
200
+ print(
201
+ "\nUse --tools to specify a comma-separated list of tools to enable."
202
+ )
203
+ print(
204
+ "Use --exclude-tools to specify a comma-separated list of tools to disable."
205
+ )
206
+ print(f"\nTotal tools: {len(tools)}")
207
+
208
+
209
+ def setup_webhook_server(args: argparse.Namespace) -> None:
210
+ """
211
+ Set up the webhook handler based on command line arguments.
212
+
213
+ Args:
214
+ args: Command line arguments
215
+ """
216
+ # Update settings from command line arguments
217
+ settings.webhook_enabled = args.webhook_enabled
218
+ settings.ngrok_enabled = args.ngrok_enabled
219
+
220
+ if args.ngrok_authtoken:
221
+ settings.ngrok_authtoken = args.ngrok_authtoken
222
+
223
+ # CRITICAL: Ignore any ngrok_url setting to prevent TLS errors
224
+ # This effectively makes custom domains impossible to set
225
+ if hasattr(settings, "ngrok_url"):
226
+ # Always force to None regardless of command line args or env vars
227
+ try:
228
+ object.__setattr__(settings, "ngrok_url", None)
229
+ logger.info(
230
+ "Forced ngrok_url to None (custom domains cause TLS errors)"
231
+ )
232
+ except Exception as attr_err:
233
+ logger.warning(
234
+ f"Could not override ngrok_url attribute: {attr_err}"
235
+ )
236
+
237
+ # Log settings
238
+ logger.info(f"Webhook enabled: {settings.webhook_enabled}")
239
+ logger.info(f"Ngrok enabled: {settings.ngrok_enabled}")
240
+
241
+ if settings.ngrok_enabled:
242
+ logger.info(
243
+ f"Ngrok auth token provided: {bool(settings.ngrok_authtoken)}"
244
+ )
245
+ logger.info(f"Ngrok URL provided: {bool(settings.ngrok_url)}")
246
+ # Dump the source of the setting to see where it's coming from
247
+ logger.info(f"Ngrok URL setting type: {type(settings.ngrok_url)}")
248
+ logger.info(f"Ngrok URL setting value: {settings.ngrok_url!r}")
249
+ logger.info(
250
+ f"Raw environment value: {os.getenv('NGROK_URL', 'NOT_SET')!r}"
251
+ )
252
+
253
+ if settings.ngrok_url:
254
+ logger.info(f"Using Ngrok URL: {settings.ngrok_url}")
255
+
256
+ # Start webhook handler if enabled
257
+ if settings.webhook_enabled:
258
+ logger.info("Starting webhook handler...")
259
+
260
+ # Only clean up our resources using the SDK, don't kill processes
261
+ try:
262
+ import ngrok
263
+
264
+ try:
265
+ logger.info("Cleaning up existing ngrok SDK resources...")
266
+ ngrok.disconnect()
267
+ time.sleep(1) # Brief pause to allow cleanup
268
+ except Exception as cleanup_err:
269
+ logger.warning(
270
+ f"NGrok cleanup warning (non-fatal): {cleanup_err}"
271
+ )
272
+
273
+ # Force disable any custom domain in NGrok config
274
+ if settings.ngrok_enabled and hasattr(ngrok, "set_config"):
275
+ try:
276
+ ngrok.set_config(domain=None)
277
+ logger.info(
278
+ "Explicitly disabled custom domain in NGrok configuration"
279
+ )
280
+ except Exception as config_err:
281
+ logger.warning(
282
+ f"Could not override NGrok domain config: {config_err}"
283
+ )
284
+
285
+ # Check for existing sessions and warn (but don't kill them)
286
+ if os.name == "posix" and hasattr(ngrok, "list_tunnels"):
287
+ try:
288
+ tunnels = ngrok.list_tunnels()
289
+ if tunnels:
290
+ logger.warning(
291
+ f"Found {len(tunnels)} existing NGrok tunnels"
292
+ )
293
+ logger.warning(
294
+ "This may cause session limit errors if you've reached your account limit"
295
+ )
296
+ except Exception:
297
+ pass
298
+ except ImportError:
299
+ logger.warning("NGrok module not available for pre-cleanup")
300
+
301
+ # Start the webhook handler
302
+ webhook_url = start_webhook_handler()
303
+
304
+ if webhook_url:
305
+ logger.info(f"Webhook public URL: {webhook_url}")
306
+ logger.info(
307
+ f"Webhook endpoint URL: {webhook_url}{settings.webhook_path}"
308
+ )
309
+ logger.info(
310
+ f"Webhook information available through resource://webhook/info"
311
+ )
312
+ else:
313
+ # This is a fatal error - webhooks are required
314
+ logger.critical("FATAL ERROR: Failed to start webhook handler")
315
+ logger.critical("Webhooks are required for MCP server operation")
316
+ logger.critical("The most likely causes are:")
317
+ logger.critical("1. NGrok session limit reached - check dashboard")
318
+ logger.critical("2. Custom domain TLS certificate issues")
319
+ logger.critical("3. Network connectivity problems")
320
+ logger.critical(
321
+ "The server will exit. Please resolve these issues before restarting."
322
+ )
323
+ sys.exit(1) # Exit with error code - fail decisively
324
+
325
+ # Register cleanup handlers
326
+ atexit.register(cleanup_webhook_server)
327
+ signal.signal(signal.SIGINT, signal_handler)
328
+ signal.signal(signal.SIGTERM, signal_handler)
329
+
330
+ # Add SIGHUP handler for Unix-like systems
331
+ if os.name == "posix":
332
+ signal.signal(signal.SIGHUP, signal_handler)
333
+
334
+
335
+ # Global variables
336
+ parent_watcher_thread: Optional[threading.Thread] = None
337
+ parent_pid: int = os.getppid()
338
+
339
+
340
+ def watch_parent_process() -> None:
341
+ """
342
+ Watch the parent process and exit if it's gone.
343
+
344
+ This function runs in a background thread and periodically checks if the
345
+ parent process (Claude Desktop) is still running. If the parent process
346
+ is gone, it cleans up resources and exits.
347
+ """
348
+ global parent_watcher_thread
349
+
350
+ def _watch_loop():
351
+ """Inner function to watch the parent process in a loop."""
352
+ consecutive_errors = 0
353
+ max_consecutive_errors = 3
354
+
355
+ # Default check interval is 5 seconds
356
+ check_interval = 5
357
+
358
+ while True:
359
+ try:
360
+ parent_running = False
361
+
362
+ # Use different methods based on platform
363
+ if os.name == "posix":
364
+ try:
365
+ # Check if parent process still exists by sending signal 0
366
+ # This doesn't actually send a signal, just checks if the process exists
367
+ os.kill(parent_pid, 0)
368
+ parent_running = True
369
+ except OSError:
370
+ # The parent process is gone
371
+ parent_running = False
372
+ else:
373
+ # On Windows, check if we can get process info
374
+ try:
375
+ # Import only when needed to avoid unnecessary dependency
376
+ import ctypes
377
+
378
+ kernel32 = ctypes.windll.kernel32
379
+ handle = kernel32.OpenProcess(1, False, parent_pid)
380
+ if handle != 0:
381
+ # Process exists, close the handle
382
+ kernel32.CloseHandle(handle)
383
+ parent_running = True
384
+ else:
385
+ # Process doesn't exist
386
+ parent_running = False
387
+ except Exception as e:
388
+ logger.debug(
389
+ f"Error checking Windows parent process: {e}"
390
+ )
391
+ # On error, assume parent is still running
392
+ parent_running = True
393
+
394
+ # If parent is not running, clean up and exit
395
+ if not parent_running:
396
+ logger.warning(
397
+ "Parent process (Claude Desktop) is gone, shutting down..."
398
+ )
399
+ cleanup_webhook_server()
400
+ logger.info("Exiting MCP server cleanly")
401
+ os._exit(0) # Force exit without running finalizers
402
+
403
+ # Reset error counter on successful check
404
+ consecutive_errors = 0
405
+
406
+ # Sleep for the check interval
407
+ time.sleep(check_interval)
408
+
409
+ except Exception as e:
410
+ # Count consecutive errors
411
+ consecutive_errors += 1
412
+ logger.error(f"Error in parent process watcher: {e}")
413
+
414
+ if consecutive_errors >= max_consecutive_errors:
415
+ logger.critical(
416
+ f"Too many consecutive errors ({consecutive_errors}) "
417
+ "in parent process watcher. Continuing but may not "
418
+ "detect parent process termination properly."
419
+ )
420
+
421
+ # Increase sleep time on error to prevent tight loops
422
+ error_sleep = min(check_interval * consecutive_errors, 30)
423
+ time.sleep(error_sleep)
424
+
425
+ # Create and start the watcher thread
426
+ parent_watcher_thread = threading.Thread(
427
+ target=_watch_loop, daemon=True, name="ParentProcessWatcher"
428
+ )
429
+ parent_watcher_thread.start()
430
+ logger.info(f"Started watching parent process (PID: {parent_pid})")
431
+
432
+
433
+ def cleanup_webhook_server() -> None:
434
+ """Clean up webhook handler resources."""
435
+ if settings.webhook_enabled:
436
+ stop_webhook_handler()
437
+
438
+
439
+ def signal_handler(sig, frame) -> None:
440
+ """Handle termination signals."""
441
+ logger.info(f"Received signal {sig}, shutting down...")
442
+ cleanup_webhook_server()
443
+ sys.exit(0)
444
+
445
+
446
+ def run_server() -> None:
447
+ """Run the server using STDIO transport."""
448
+ try:
449
+ # Check if just listing tools
450
+ args = parse_args()
451
+ if args.list_tools:
452
+ import asyncio
453
+
454
+ asyncio.run(list_all_tools())
455
+ return
456
+ except:
457
+ # If argument parsing fails, proceed with normal server startup
458
+ pass
459
+
460
+ # Parse command line arguments
461
+ args = parse_args()
462
+
463
+ logger.info("Starting Telnyx MCP server with STDIO transport")
464
+ logger.info("Using API key from environment variables")
465
+
466
+ # Get tool filtering settings
467
+ enabled_tools = get_enabled_tools()
468
+ excluded_tools = get_excluded_tools()
469
+
470
+ # Configure tool filtering in MCP instance
471
+ if enabled_tools is not None:
472
+ logger.info(f"Enabling specific tools: {', '.join(enabled_tools)}")
473
+ mcp.set_enabled_tools(enabled_tools)
474
+
475
+ if excluded_tools:
476
+ logger.info(f"Excluding tools: {', '.join(excluded_tools)}")
477
+ mcp.set_excluded_tools(excluded_tools)
478
+
479
+ # Start the parent process watcher
480
+ watch_parent_process()
481
+
482
+ # Set up webhook server if enabled
483
+ setup_webhook_server(args)
484
+
485
+ # Use FastMCP's run method to start the server
486
+ mcp.run()
487
+
488
+
489
+ # This function is used when running the server with uvx
490
+ def main() -> None:
491
+ """Entry point for running the server with uvx."""
492
+ run_server()
493
+
494
+
495
+ # This allows the script to be run directly
496
+ if __name__ == "__main__":
497
+ main()
@@ -0,0 +1 @@
1
+ """Telnyx package."""