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,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."""
|