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,363 @@
1
+ """Telnyx API client."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ import requests
6
+
7
+ from ..config import settings
8
+ from ..utils.logger import get_logger
9
+
10
+ logger = get_logger(__name__)
11
+
12
+
13
+ def mask_sensitive_data(data: Dict) -> Dict:
14
+ """Mask sensitive data in dictionaries.
15
+
16
+ Args:
17
+ data: Dictionary potentially containing sensitive data
18
+
19
+ Returns:
20
+ Dict: Masked dictionary
21
+ """
22
+ result = {}
23
+
24
+ for key, value in data.items():
25
+ # Mask sensitive keys
26
+ if any(
27
+ sensitive in key.lower()
28
+ for sensitive in ["key", "token", "auth", "password", "secret"]
29
+ ):
30
+ if isinstance(value, str):
31
+ if len(value) > 8:
32
+ result[key] = f"{value[:5]}...{value[-3:]}"
33
+ else:
34
+ result[key] = "[REDACTED]"
35
+ else:
36
+ result[key] = "[REDACTED]"
37
+ # Handle nested dictionaries
38
+ elif isinstance(value, dict):
39
+ result[key] = mask_sensitive_data(value)
40
+ # Handle lists potentially containing dictionaries
41
+ elif isinstance(value, list):
42
+ if value and isinstance(value[0], dict):
43
+ result[key] = [
44
+ mask_sensitive_data(item)
45
+ if isinstance(item, dict)
46
+ else item
47
+ for item in value
48
+ ]
49
+ else:
50
+ result[key] = value
51
+ else:
52
+ result[key] = value
53
+
54
+ return result
55
+
56
+
57
+ class TelnyxClient:
58
+ """Telnyx API client."""
59
+
60
+ def __init__(
61
+ self,
62
+ api_key: Optional[str] = None,
63
+ base_url: Optional[str] = None,
64
+ headers: Optional[Dict[str, str]] = None,
65
+ ):
66
+ """Initialize the client.
67
+
68
+ Args:
69
+ api_key: Telnyx API key (optional if provided in headers)
70
+ base_url: Base URL for Telnyx API (optional)
71
+ headers: Optional headers dictionary containing authorization
72
+ """
73
+ logger.debug("Initializing TelnyxClient")
74
+ self.api_key = api_key
75
+
76
+ # Extract API key from headers if available
77
+ if headers:
78
+ logger.debug("Headers provided, checking for authorization")
79
+
80
+ # Check Authorization header
81
+ if "authorization" in headers:
82
+ auth_header = headers.get("authorization", "")
83
+
84
+ # Extract token from Bearer header
85
+ if auth_header.lower().startswith("bearer "):
86
+ self.api_key = auth_header[7:] # Remove "Bearer " prefix
87
+ logger.debug(
88
+ "Got API key from Bearer authorization header"
89
+ )
90
+ else:
91
+ logger.debug("No API key in headers")
92
+
93
+ # Use the default API key from settings if none found
94
+ if not self.api_key:
95
+ logger.debug("Using default API key from settings")
96
+ self.api_key = settings.telnyx_api_key
97
+
98
+ # Log API key info (first few chars only for security)
99
+ if self.api_key:
100
+ masked_key = (
101
+ "NONE"
102
+ if not self.api_key
103
+ else f"{self.api_key[:5]}..."
104
+ if len(self.api_key) > 5
105
+ else "[REDACTED]"
106
+ )
107
+ logger.debug(f"API key (masked): {masked_key}")
108
+ else:
109
+ logger.warning("No API key available")
110
+
111
+ self.base_url = base_url or settings.telnyx_api_base_url
112
+ logger.debug(f"Using base URL: {self.base_url}")
113
+
114
+ self.session = requests.Session()
115
+ logger.debug("Created requests Session")
116
+
117
+ # Set up headers with authorization
118
+ header_dict = {
119
+ "Authorization": f"Bearer {self.api_key}",
120
+ "Content-Type": "application/json",
121
+ "Accept": "application/json",
122
+ }
123
+
124
+ logger.debug(f"Setting headers: {', '.join(header_dict.keys())}")
125
+ self.session.headers.update(header_dict)
126
+ logger.debug("TelnyxClient initialization complete")
127
+
128
+ def get(
129
+ self, path: str, params: Optional[Dict[str, Any]] = None
130
+ ) -> Dict[str, Any]:
131
+ """Make a GET request to the Telnyx API.
132
+
133
+ Args:
134
+ path: API path
135
+ params: Query parameters
136
+
137
+ Returns:
138
+ Dict[str, Any]: Response data
139
+ """
140
+ url = f"{self.base_url}/{path.lstrip('/')}"
141
+ logger.info(f"TELNYX REQUEST: GET {url}")
142
+ logger.debug(f"TELNYX REQUEST PARAMS: {params}")
143
+
144
+ # Log masked headers at debug level
145
+ header_dict = dict(self.session.headers)
146
+ masked_headers = mask_sensitive_data(header_dict)
147
+ logger.debug(f"TELNYX REQUEST HEADERS: {masked_headers}")
148
+
149
+ try:
150
+ response = self.session.get(url, params=params)
151
+ logger.info(f"TELNYX RESPONSE STATUS: {response.status_code}")
152
+ logger.debug(f"TELNYX RESPONSE HEADERS: {dict(response.headers)}")
153
+
154
+ if response.status_code >= 400:
155
+ logger.error(f"TELNYX ERROR RESPONSE BODY: {response.text}")
156
+ else:
157
+ # Log a snippet of the successful response
158
+ try:
159
+ response_json = response.json()
160
+ # Log full response at debug level, truncated at info level
161
+ logger.debug(
162
+ f"TELNYX RESPONSE FULL: {mask_sensitive_data(response_json)}"
163
+ )
164
+ logger.info(
165
+ f"TELNYX RESPONSE PREVIEW: {str(response_json)[:200]}..."
166
+ )
167
+ except Exception as json_err:
168
+ logger.warning(
169
+ f"Could not parse response as JSON: {json_err}"
170
+ )
171
+
172
+ response.raise_for_status()
173
+ return response.json()
174
+ except Exception as e:
175
+ logger.error(f"TELNYX REQUEST ERROR: {str(e)}")
176
+ raise
177
+
178
+ def post(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
179
+ """Make a POST request to the Telnyx API.
180
+
181
+ Args:
182
+ path: API path
183
+ data: Request data
184
+
185
+ Returns:
186
+ Dict[str, Any]: Response data
187
+ """
188
+ url = f"{self.base_url}/{path.lstrip('/')}"
189
+ logger.info(f"TELNYX REQUEST: POST {url}")
190
+
191
+ # Log request data at debug level with sensitive information masked
192
+ masked_data = mask_sensitive_data(data)
193
+ logger.debug(f"TELNYX REQUEST DATA: {masked_data}")
194
+
195
+ # Log masked headers at debug level
196
+ header_dict = dict(self.session.headers)
197
+ masked_headers = mask_sensitive_data(header_dict)
198
+ logger.debug(f"TELNYX REQUEST HEADERS: {masked_headers}")
199
+
200
+ try:
201
+ response = self.session.post(url, json=data)
202
+ logger.info(f"TELNYX RESPONSE STATUS: {response.status_code}")
203
+ logger.debug(f"TELNYX RESPONSE HEADERS: {dict(response.headers)}")
204
+
205
+ if response.status_code >= 400:
206
+ logger.error(f"TELNYX ERROR RESPONSE BODY: {response.text}")
207
+ else:
208
+ # Log a snippet of the successful response
209
+ try:
210
+ response_json = response.json()
211
+ # Log full response at debug level, truncated at info level
212
+ logger.debug(
213
+ f"TELNYX RESPONSE FULL: {mask_sensitive_data(response_json)}"
214
+ )
215
+ logger.info(
216
+ f"TELNYX RESPONSE PREVIEW: {str(response_json)[:200]}..."
217
+ )
218
+ except Exception as json_err:
219
+ logger.warning(
220
+ f"Could not parse response as JSON: {json_err}"
221
+ )
222
+
223
+ response.raise_for_status()
224
+ return response.json()
225
+ except Exception as e:
226
+ logger.error(f"TELNYX REQUEST ERROR: {str(e)}")
227
+ raise
228
+
229
+ def put(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
230
+ """Make a PUT request to the Telnyx API.
231
+
232
+ Args:
233
+ path: API path
234
+ data: Request data
235
+
236
+ Returns:
237
+ Dict[str, Any]: Response data
238
+ """
239
+ url = f"{self.base_url}/{path.lstrip('/')}"
240
+ logger.info(f"TELNYX REQUEST: PUT {url}")
241
+
242
+ # Log request data at debug level with sensitive information masked
243
+ masked_data = mask_sensitive_data(data)
244
+ logger.debug(f"TELNYX REQUEST DATA: {masked_data}")
245
+
246
+ # Log masked headers at debug level
247
+ header_dict = dict(self.session.headers)
248
+ masked_headers = mask_sensitive_data(header_dict)
249
+ logger.debug(f"TELNYX REQUEST HEADERS: {masked_headers}")
250
+
251
+ try:
252
+ response = self.session.put(url, json=data)
253
+ logger.info(f"TELNYX RESPONSE STATUS: {response.status_code}")
254
+ logger.debug(f"TELNYX RESPONSE HEADERS: {dict(response.headers)}")
255
+
256
+ if response.status_code >= 400:
257
+ logger.error(f"TELNYX ERROR RESPONSE BODY: {response.text}")
258
+ else:
259
+ # Log a snippet of the successful response
260
+ try:
261
+ response_json = response.json()
262
+ # Log full response at debug level, truncated at info level
263
+ logger.debug(
264
+ f"TELNYX RESPONSE FULL: {mask_sensitive_data(response_json)}"
265
+ )
266
+ logger.info(
267
+ f"TELNYX RESPONSE PREVIEW: {str(response_json)[:200]}..."
268
+ )
269
+ except Exception as json_err:
270
+ logger.warning(
271
+ f"Could not parse response as JSON: {json_err}"
272
+ )
273
+
274
+ response.raise_for_status()
275
+ return response.json()
276
+ except Exception as e:
277
+ logger.error(f"TELNYX REQUEST ERROR: {str(e)}")
278
+ raise
279
+
280
+ def patch(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
281
+ """Make a PATCH request to the Telnyx API.
282
+
283
+ Args:
284
+ path: API path
285
+ data: Request data
286
+
287
+ Returns:
288
+ Dict[str, Any]: Response data
289
+ """
290
+ url = f"{self.base_url}/{path.lstrip('/')}"
291
+ logger.info(f"TELNYX REQUEST: PATCH {url}")
292
+
293
+ # Log request data at debug level with sensitive information masked
294
+ masked_data = mask_sensitive_data(data)
295
+ logger.debug(f"TELNYX REQUEST DATA: {masked_data}")
296
+
297
+ # Log masked headers at debug level
298
+ header_dict = dict(self.session.headers)
299
+ masked_headers = mask_sensitive_data(header_dict)
300
+ logger.debug(f"TELNYX REQUEST HEADERS: {masked_headers}")
301
+
302
+ try:
303
+ response = self.session.patch(url, json=data)
304
+ logger.info(f"TELNYX RESPONSE STATUS: {response.status_code}")
305
+ logger.debug(f"TELNYX RESPONSE HEADERS: {dict(response.headers)}")
306
+
307
+ if response.status_code >= 400:
308
+ logger.error(f"TELNYX ERROR RESPONSE BODY: {response.text}")
309
+ else:
310
+ # Log a snippet of the successful response
311
+ try:
312
+ response_json = response.json()
313
+ # Log full response at debug level, truncated at info level
314
+ logger.debug(
315
+ f"TELNYX RESPONSE FULL: {mask_sensitive_data(response_json)}"
316
+ )
317
+ logger.info(
318
+ f"TELNYX RESPONSE PREVIEW: {str(response_json)[:200]}..."
319
+ )
320
+ except Exception as json_err:
321
+ logger.warning(
322
+ f"Could not parse response as JSON: {json_err}"
323
+ )
324
+
325
+ response.raise_for_status()
326
+ return response.json()
327
+ except Exception as e:
328
+ logger.error(f"TELNYX REQUEST ERROR: {str(e)}")
329
+ raise
330
+
331
+ def delete(self, path: str) -> Dict[str, Any]:
332
+ """Make a DELETE request to the Telnyx API.
333
+
334
+ Args:
335
+ path: API path
336
+
337
+ Returns:
338
+ Dict[str, Any]: Response data
339
+ """
340
+ url = f"{self.base_url}/{path.lstrip('/')}"
341
+ logger.info(f"TELNYX REQUEST: DELETE {url}")
342
+
343
+ # Log masked headers at debug level
344
+ header_dict = dict(self.session.headers)
345
+ masked_headers = mask_sensitive_data(header_dict)
346
+ logger.debug(f"TELNYX REQUEST HEADERS: {masked_headers}")
347
+
348
+ try:
349
+ response = self.session.delete(url)
350
+ logger.info(f"TELNYX RESPONSE STATUS: {response.status_code}")
351
+ logger.debug(f"TELNYX RESPONSE HEADERS: {dict(response.headers)}")
352
+
353
+ if response.status_code >= 400:
354
+ logger.error(f"TELNYX ERROR RESPONSE BODY: {response.text}")
355
+
356
+ response.raise_for_status()
357
+ # Handle empty responses
358
+ if not response.text:
359
+ return {}
360
+ return response.json()
361
+ except Exception as e:
362
+ logger.error(f"TELNYX REQUEST ERROR: {str(e)}")
363
+ raise
File without changes
@@ -0,0 +1,155 @@
1
+ """Telnyx AI Assistants service."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from ..client import TelnyxClient
6
+
7
+
8
+ class AssistantsService:
9
+ """Service for managing Telnyx AI Assistants."""
10
+
11
+ def __init__(self, client: TelnyxClient) -> None:
12
+ """Initialize the service.
13
+
14
+ Args:
15
+ client: Telnyx API client
16
+ """
17
+ self.client = client
18
+
19
+ def create_assistant(
20
+ self,
21
+ request: Dict[str, Any],
22
+ ) -> Dict[str, Any]:
23
+ """Create a new AI Assistant.
24
+
25
+ Args:
26
+ request: Assistant creation request data
27
+
28
+ Returns:
29
+ Dict[str, Any]: Created assistant data
30
+ """
31
+ # Hard code voice settings and enabled features
32
+ request["voice_settings"] = {
33
+ "voice": "Telnyx.KokoroTTS.af_heart",
34
+ "api_key_ref": None,
35
+ }
36
+ request["enabled_features"] = ["telephony"]
37
+
38
+ response = self.client.post("/ai/assistants", data=request)
39
+ return response
40
+
41
+ def list_assistants(self) -> Dict[str, Any]:
42
+ """List all AI Assistants.
43
+
44
+ Returns:
45
+ Dict[str, Any]: List of assistants
46
+ """
47
+ response = self.client.get("/ai/assistants")
48
+ return response
49
+
50
+ def get_assistant(
51
+ self,
52
+ assistant_id: str,
53
+ fetch_dynamic_variables_from_webhook: Optional[bool] = None,
54
+ from_: Optional[str] = None,
55
+ to: Optional[str] = None,
56
+ call_control_id: Optional[str] = None,
57
+ ) -> Dict[str, Any]:
58
+ """Get an AI Assistant by ID.
59
+
60
+ Args:
61
+ assistant_id: Assistant ID
62
+ fetch_dynamic_variables_from_webhook: Whether to fetch dynamic variables from webhook
63
+ from_: From parameter for dynamic variables
64
+ to: To parameter for dynamic variables
65
+ call_control_id: Call control ID for dynamic variables
66
+
67
+ Returns:
68
+ Dict[str, Any]: Assistant data
69
+ """
70
+ params: Dict[str, Any] = {}
71
+ if fetch_dynamic_variables_from_webhook is not None:
72
+ params["fetch_dynamic_variables_from_webhook"] = (
73
+ fetch_dynamic_variables_from_webhook
74
+ )
75
+ if from_ is not None:
76
+ params["from"] = from_
77
+ if to is not None:
78
+ params["to"] = to
79
+ if call_control_id is not None:
80
+ params["call_control_id"] = call_control_id
81
+
82
+ response = self.client.get(
83
+ f"/ai/assistants/{assistant_id}", params=params
84
+ )
85
+ return response
86
+
87
+ def update_assistant(
88
+ self,
89
+ assistant_id: str,
90
+ request: Dict[str, Any],
91
+ ) -> Dict[str, Any]:
92
+ """Update an AI Assistant.
93
+
94
+ Args:
95
+ assistant_id: Assistant ID
96
+ request: Assistant update request data
97
+
98
+ Returns:
99
+ Dict[str, Any]: Updated assistant data
100
+ """
101
+ # Hard code voice settings and enabled features
102
+ if "voice_settings" in request:
103
+ del request["voice_settings"]
104
+
105
+ if "enabled_features" in request:
106
+ del request["enabled_features"]
107
+
108
+ response = self.client.post(
109
+ f"/ai/assistants/{assistant_id}",
110
+ data=request,
111
+ )
112
+ return response
113
+
114
+ def delete_assistant(self, assistant_id: str) -> Dict[str, Any]:
115
+ """Delete an AI Assistant.
116
+
117
+ Args:
118
+ assistant_id: Assistant ID
119
+
120
+ Returns:
121
+ Dict[str, Any]: Deletion response containing id, object, and deleted status
122
+ """
123
+ response = self.client.delete(f"/ai/assistants/{assistant_id}")
124
+ return response
125
+
126
+ def get_assistant_texml(self, assistant_id: str) -> str:
127
+ """Get an assistant's TEXML by ID.
128
+
129
+ Args:
130
+ assistant_id: Assistant ID
131
+
132
+ Returns:
133
+ str: Assistant TEXML content
134
+ """
135
+ response = self.client.get(f"/ai/assistants/{assistant_id}/texml")
136
+ return response
137
+
138
+ def start_assistant_call(
139
+ self, default_texml_app_id: str, to: str, from_: str
140
+ ) -> Dict[str, Any]:
141
+ """Start a call using the assistant's TeXML application.
142
+
143
+ Args:
144
+ default_texml_app_id: The assistant's default TeXML application ID
145
+ to: Destination number to call
146
+ from_: Source number to call from
147
+
148
+ Returns:
149
+ Dict[str, Any]: Response data
150
+ """
151
+ data = {"To": to, "From": from_}
152
+ response = self.client.post(
153
+ f"/texml/calls/{default_texml_app_id}", data=data
154
+ )
155
+ return response