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,217 @@
1
+ """Telnyx call control service."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from ...utils.logger import get_logger
6
+ from ..client import TelnyxClient
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ class CallControlService:
12
+ """Service for managing Telnyx calls."""
13
+
14
+ def __init__(self, client: Optional[TelnyxClient] = None):
15
+ """Initialize the service with a Telnyx client."""
16
+ self.client = client or TelnyxClient()
17
+
18
+ def list_call_control_applications(
19
+ self,
20
+ request: Dict[str, Any],
21
+ ) -> Dict[str, Any]:
22
+ """List call control applications.
23
+
24
+ Args:
25
+ request: Request parameters for listing call control applications
26
+
27
+ Returns:
28
+ Dict[str, Any]: Response data
29
+ """
30
+ params = {
31
+ "page[number]": request.get("page", 1),
32
+ "page[size]": request.get("page_size", 20),
33
+ }
34
+
35
+ if request.get("filter_application_name_contains"):
36
+ params["filter[application_name][contains]"] = request[
37
+ "filter_application_name_contains"
38
+ ]
39
+
40
+ if request.get("filter_outbound_voice_profile_id"):
41
+ params["filter[outbound.outbound_voice_profile_id]"] = request[
42
+ "filter_outbound_voice_profile_id"
43
+ ]
44
+
45
+ if request.get("sort"):
46
+ params["sort"] = request["sort"]
47
+
48
+ response = self.client.get("call_control_applications", params=params)
49
+ if isinstance(response, dict):
50
+ return response
51
+ return response.json()
52
+
53
+ def get_call_control_application(
54
+ self,
55
+ request: Dict[str, Any],
56
+ ) -> Dict[str, Any]:
57
+ """Retrieve a specific call control application.
58
+
59
+ Args:
60
+ request: Request parameters containing the call control application ID
61
+
62
+ Returns:
63
+ Dict[str, Any]: Response data
64
+ """
65
+ application_id = request.get("id")
66
+ response = self.client.get(
67
+ f"call_control_applications/{application_id}"
68
+ )
69
+ if isinstance(response, dict):
70
+ return response
71
+ return response.json()
72
+
73
+ def create_call_control_application(
74
+ self,
75
+ request: Dict[str, Any],
76
+ ) -> Dict[str, Any]:
77
+ """Create a call control application.
78
+
79
+ Args:
80
+ request: Request parameters for creating a call control application
81
+
82
+ Returns:
83
+ Dict[str, Any]: Response data
84
+ """
85
+ # Create a copy of the request to avoid modifying the original
86
+ data = request.copy()
87
+
88
+ response = self.client.post("call_control_applications", data=data)
89
+ if isinstance(response, dict):
90
+ return response
91
+ return response.json()
92
+
93
+ def make_call(self, data: Dict[str, Any]) -> Dict[str, Any]:
94
+ """Make a call.
95
+
96
+ Args:
97
+ data: Call request parameters
98
+ Note: connection_id and call_control_application_id are the same thing,
99
+ either can be used to specify the application for the call.
100
+
101
+ Returns:
102
+ Dict[str, Any]: Response data
103
+ """
104
+
105
+ # Rename from_ to from as required by the API
106
+ if "from_" in data:
107
+ data["from"] = data.pop("from_")
108
+
109
+ response = self.client.post("/calls", data=data)
110
+ return response
111
+
112
+ def hangup(
113
+ self, call_control_id: str, data: Dict[str, Any]
114
+ ) -> Dict[str, Any]:
115
+ """Hang up a call.
116
+
117
+ Args:
118
+ call_control_id: Call control ID
119
+ data: Hangup request parameters
120
+
121
+ Returns:
122
+ Dict[str, Any]: Response data
123
+ """
124
+ response = self.client.post(
125
+ f"/calls/{call_control_id}/actions/hangup", data=data
126
+ )
127
+ return response
128
+
129
+ def playback_start(
130
+ self, call_control_id: str, data: Dict[str, Any]
131
+ ) -> Dict[str, Any]:
132
+ """Start audio playback on a call.
133
+
134
+ Args:
135
+ call_control_id: Call control ID
136
+ data: Playback request parameters
137
+
138
+ Returns:
139
+ Dict[str, Any]: Response data
140
+ """
141
+ response = self.client.post(
142
+ f"/calls/{call_control_id}/actions/playback_start", data=data
143
+ )
144
+ return response
145
+
146
+ def playback_stop(
147
+ self, call_control_id: str, data: Dict[str, Any]
148
+ ) -> Dict[str, Any]:
149
+ """Stop audio playback on a call.
150
+
151
+ Args:
152
+ call_control_id: Call control ID
153
+ data: Playback stop request parameters
154
+
155
+ Returns:
156
+ Dict[str, Any]: Response data
157
+ """
158
+ response = self.client.post(
159
+ f"/calls/{call_control_id}/actions/playback_stop", data=data
160
+ )
161
+ return response
162
+
163
+ def send_dtmf(
164
+ self, call_control_id: str, data: Dict[str, Any]
165
+ ) -> Dict[str, Any]:
166
+ """Send DTMF tones on a call.
167
+
168
+ Args:
169
+ call_control_id: Call control ID
170
+ data: DTMF request parameters
171
+
172
+ Returns:
173
+ Dict[str, Any]: Response data
174
+ """
175
+ response = self.client.post(
176
+ f"/calls/{call_control_id}/actions/send_dtmf", data=data
177
+ )
178
+ return response
179
+
180
+ def speak(
181
+ self, call_control_id: str, data: Dict[str, Any]
182
+ ) -> Dict[str, Any]:
183
+ """Speak text on a call.
184
+
185
+ Args:
186
+ call_control_id: Call control ID
187
+ data: Speak request parameters
188
+
189
+ Returns:
190
+ Dict[str, Any]: Response data
191
+ """
192
+ response = self.client.post(
193
+ f"/calls/{call_control_id}/actions/speak", data=data
194
+ )
195
+ return response
196
+
197
+ def transfer(
198
+ self, call_control_id: str, data: Dict[str, Any]
199
+ ) -> Dict[str, Any]:
200
+ """Transfer a call.
201
+
202
+ Args:
203
+ call_control_id: Call control ID
204
+ data: Transfer request parameters
205
+
206
+ Returns:
207
+ Dict[str, Any]: Response data
208
+ """
209
+
210
+ # Rename from_ to from as required by the API
211
+ if "from_" in data:
212
+ data["from"] = data.pop("from_")
213
+
214
+ response = self.client.post(
215
+ f"/calls/{call_control_id}/actions/transfer", data=data
216
+ )
217
+ return response
@@ -0,0 +1,289 @@
1
+ from functools import lru_cache
2
+ import os
3
+ from typing import Dict, List, Optional, TypedDict
4
+
5
+ import boto3
6
+ from botocore.client import Config
7
+
8
+
9
+ class BucketInfo(TypedDict):
10
+ """Type definition for bucket information"""
11
+
12
+ name: str
13
+ region: str
14
+
15
+
16
+ class CloudStorageService:
17
+ """Service for interacting with Telnyx Cloud Storage (S3-compatible)"""
18
+
19
+ VALID_REGIONS = ["us-west-1", "us-central-1", "us-east-1"]
20
+
21
+ def __init__(
22
+ self,
23
+ access_key_id: str,
24
+ secret_access_key: str,
25
+ default_region: str = "us-central-1",
26
+ bucket_name: Optional[str] = None,
27
+ ):
28
+ """Initialize the cloud storage service.
29
+
30
+ Args:
31
+ access_key_id: AWS access key ID
32
+ secret_access_key: AWS secret access key
33
+ default_region: Default region to use when bucket location is unknown
34
+ bucket_name: Default bucket name to use for operations
35
+
36
+ Raises:
37
+ ValueError: If an invalid region is provided
38
+ """
39
+ if default_region not in self.VALID_REGIONS:
40
+ raise ValueError(
41
+ f"Invalid region. Must be one of: {', '.join(self.VALID_REGIONS)}"
42
+ )
43
+
44
+ # Create S3 clients for each region
45
+ self.s3_clients: Dict[str, boto3.client] = {}
46
+ for region in self.VALID_REGIONS:
47
+ endpoint_url = f"https://{region}.telnyxcloudstorage.com"
48
+ self.s3_clients[region] = boto3.client(
49
+ "s3",
50
+ aws_access_key_id=access_key_id,
51
+ aws_secret_access_key=secret_access_key,
52
+ endpoint_url=endpoint_url,
53
+ region_name=region,
54
+ config=Config(signature_version="s3v4"),
55
+ )
56
+
57
+ self.default_region = default_region
58
+ self.default_bucket_name = bucket_name
59
+ self._bucket_region_cache: Dict[str, str] = {}
60
+
61
+ def list_buckets(self) -> List[BucketInfo]:
62
+ """List all buckets across all regions.
63
+
64
+ Returns:
65
+ List[BucketInfo]: List of dictionaries containing bucket information:
66
+ - name: Name of the bucket
67
+ - region: Region where the bucket is located
68
+
69
+ Note:
70
+ This method may take some time as it needs to:
71
+ 1. List buckets from each region
72
+ 2. Get the location of each bucket
73
+ 3. Deduplicate the results
74
+ """
75
+ seen_buckets = set()
76
+ buckets: List[BucketInfo] = []
77
+
78
+ # Try listing buckets from each region
79
+ for region in self.VALID_REGIONS:
80
+ try:
81
+ response = self.s3_clients[region].list_buckets()
82
+ for bucket in response.get("Buckets", []):
83
+ bucket_name = bucket["Name"]
84
+
85
+ # Skip if we've already processed this bucket
86
+ if bucket_name in seen_buckets:
87
+ continue
88
+
89
+ try:
90
+ # Get the actual region for this bucket
91
+ bucket_region = self._get_bucket_region(bucket_name)
92
+ buckets.append(
93
+ {"name": bucket_name, "region": bucket_region}
94
+ )
95
+ seen_buckets.add(bucket_name)
96
+ except ValueError:
97
+ # Skip buckets whose region we can't determine
98
+ continue
99
+
100
+ except Exception:
101
+ # If listing fails in this region, continue to the next
102
+ continue
103
+
104
+ return buckets
105
+
106
+ def _get_bucket_name(self, bucket_name: Optional[str] = None) -> str:
107
+ """Get the bucket name to use for an operation.
108
+
109
+ Args:
110
+ bucket_name: Name of the bucket. If None, uses default bucket.
111
+
112
+ Returns:
113
+ str: The bucket name to use
114
+
115
+ Raises:
116
+ ValueError: If no bucket name is provided or set as default
117
+ """
118
+ bucket_name = bucket_name or self.default_bucket_name
119
+ if not bucket_name:
120
+ raise ValueError(
121
+ "Bucket name must be provided either during initialization or method call"
122
+ )
123
+ return bucket_name
124
+
125
+ @lru_cache(maxsize=100)
126
+ def _get_bucket_region(self, bucket_name: str) -> str:
127
+ """Get the region where a bucket is located (with caching).
128
+
129
+ Args:
130
+ bucket_name: Name of the bucket
131
+
132
+ Returns:
133
+ str: The region where the bucket is located
134
+
135
+ Raises:
136
+ ValueError: If the bucket's region cannot be determined
137
+ """
138
+ # Try to get location from default region first
139
+ try:
140
+ response = self.s3_clients[
141
+ self.default_region
142
+ ].get_bucket_location(Bucket=bucket_name)
143
+ location = response.get("LocationConstraint") or "us-east-1"
144
+ if location in self.VALID_REGIONS:
145
+ return location
146
+ except Exception:
147
+ pass
148
+
149
+ # If that fails, try each region until we find it
150
+ for region in self.VALID_REGIONS:
151
+ if region == self.default_region:
152
+ continue
153
+ try:
154
+ response = self.s3_clients[region].get_bucket_location(
155
+ Bucket=bucket_name
156
+ )
157
+ location = response.get("LocationConstraint") or "us-east-1"
158
+ if location in self.VALID_REGIONS:
159
+ return location
160
+ except Exception:
161
+ continue
162
+
163
+ raise ValueError(
164
+ f"Could not determine region for bucket: {bucket_name}"
165
+ )
166
+
167
+ def _get_client_for_bucket(self, bucket_name: str) -> boto3.client:
168
+ """Get the appropriate S3 client for the given bucket.
169
+
170
+ Args:
171
+ bucket_name: Name of the bucket
172
+
173
+ Returns:
174
+ boto3.client: The S3 client for the bucket's region
175
+ """
176
+ region = self._get_bucket_region(bucket_name)
177
+ return self.s3_clients[region]
178
+
179
+ def upload_file(
180
+ self,
181
+ file_path: str,
182
+ object_name: Optional[str] = None,
183
+ bucket_name: Optional[str] = None,
184
+ ):
185
+ """Upload a file to cloud storage.
186
+
187
+ Args:
188
+ file_path: **ABSOLUTE PATH** to the file to upload
189
+ object_name: Name to give the object in storage (defaults to file name)
190
+ bucket_name: Bucket to upload to (defaults to instance default)
191
+
192
+ Returns:
193
+ str: `Success!!` if it uploaded, otherwise returns an exception message
194
+ """
195
+ if not object_name:
196
+ object_name = os.path.basename(file_path)
197
+
198
+ bucket = self._get_bucket_name(bucket_name)
199
+ s3 = self._get_client_for_bucket(bucket)
200
+ try:
201
+ s3.upload_file(file_path, bucket, object_name)
202
+ return "Success!!"
203
+ except Exception as e:
204
+ return f"An error occurred: {e}"
205
+
206
+ def create_bucket(self, bucket_name: str, region: str) -> str:
207
+ """Create a new bucket.
208
+
209
+ Args:
210
+ bucket_name: Name of the bucket to create
211
+ region: Region to create the bucket in
212
+ Returns:
213
+ str: `Success!!` if it uploaded, otherwise returns an exception message
214
+ """
215
+ region = region or self.default_region
216
+ s3 = self.s3_clients[region]
217
+ try:
218
+ s3.create_bucket(
219
+ Bucket=bucket_name,
220
+ CreateBucketConfiguration={"LocationConstraint": region},
221
+ )
222
+ return "Success!!"
223
+ except Exception as e:
224
+ return f"An error occurred: {e}"
225
+
226
+ def download_file(
227
+ self,
228
+ object_name: str,
229
+ file_path: str,
230
+ bucket_name: Optional[str] = None,
231
+ ) -> None:
232
+ """Download a file from cloud storage.
233
+
234
+ Args:
235
+ object_name: Name of the object to download
236
+ file_path: Path where to save the downloaded file
237
+ bucket_name: Bucket to download from (defaults to instance default)
238
+ """
239
+ bucket = self._get_bucket_name(bucket_name)
240
+ s3 = self._get_client_for_bucket(bucket)
241
+ s3.download_file(bucket, object_name, file_path)
242
+
243
+ def list_objects(
244
+ self, prefix: str = "", bucket_name: Optional[str] = None
245
+ ) -> List[str]:
246
+ """List objects in a bucket with optional prefix filtering.
247
+
248
+ Args:
249
+ prefix: Only list objects beginning with this prefix
250
+ bucket_name: Bucket to list from (defaults to instance default)
251
+
252
+ Returns:
253
+ List[str]: List of object names
254
+ """
255
+ bucket = self._get_bucket_name(bucket_name)
256
+ s3 = self._get_client_for_bucket(bucket)
257
+ paginator = s3.get_paginator("list_objects_v2")
258
+
259
+ object_names = []
260
+ for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
261
+ if "Contents" in page:
262
+ object_names.extend(obj["Key"] for obj in page["Contents"])
263
+
264
+ return object_names
265
+
266
+ def delete_object(
267
+ self, object_name: str, bucket_name: Optional[str] = None
268
+ ) -> None:
269
+ """Delete an object from cloud storage.
270
+
271
+ Args:
272
+ object_name: Name of the object to delete
273
+ bucket_name: Bucket to delete from (defaults to instance default)
274
+ """
275
+ bucket = self._get_bucket_name(bucket_name)
276
+ s3 = self._get_client_for_bucket(bucket)
277
+ s3.delete_object(Bucket=bucket, Key=object_name)
278
+
279
+ def get_bucket_location(self, bucket_name: Optional[str] = None) -> str:
280
+ """Get the region where a bucket is located.
281
+
282
+ Args:
283
+ bucket_name: Name of the bucket. If None, uses default bucket.
284
+
285
+ Returns:
286
+ str: The region where the bucket is located
287
+ """
288
+ bucket = self._get_bucket_name(bucket_name)
289
+ return self._get_bucket_region(bucket)
@@ -0,0 +1,92 @@
1
+ """Telnyx connections service."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from ...utils.logger import get_logger
6
+ from ..client import TelnyxClient
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ class ConnectionsService:
12
+ """Service for managing Telnyx connections."""
13
+
14
+ def __init__(self, client: Optional[TelnyxClient] = None):
15
+ """Initialize the service with a Telnyx client."""
16
+ self.client = client or TelnyxClient()
17
+
18
+ def list_connections(
19
+ self,
20
+ request: Dict[str, Any],
21
+ ) -> Dict[str, Any]:
22
+ """List connections.
23
+
24
+ Args:
25
+ request: Request parameters for listing connections
26
+
27
+ Returns:
28
+ Dict[str, Any]: Response data
29
+ """
30
+ params = {
31
+ "page[number]": request.get("page", 1),
32
+ "page[size]": request.get("page_size", 20),
33
+ }
34
+
35
+ if request.get("filter_connection_name_contains"):
36
+ params["filter[connection_name_contains]"] = request[
37
+ "filter_connection_name_contains"
38
+ ]
39
+
40
+ if request.get("filter_outbound_voice_profile_id"):
41
+ params["filter[outbound_voice_profile_id]"] = request[
42
+ "filter_outbound_voice_profile_id"
43
+ ]
44
+
45
+ if request.get("sort"):
46
+ params["sort"] = request["sort"]
47
+
48
+ response = self.client.get("connections", params=params)
49
+ if isinstance(response, dict):
50
+ return response
51
+ return response.json()
52
+
53
+ def get_connection(self, connection_id: str) -> Dict[str, Any]:
54
+ """Get a connection by ID.
55
+
56
+ Args:
57
+ connection_id: Connection ID
58
+
59
+ Returns:
60
+ Dict[str, Any]: Response data
61
+ """
62
+ response = self.client.get(f"connections/{connection_id}")
63
+ if isinstance(response, dict):
64
+ return response
65
+ return response.json()
66
+
67
+ def update_connection(
68
+ self,
69
+ connection_id: str,
70
+ data: Dict[str, Any],
71
+ ) -> Dict[str, Any]:
72
+ """Update a connection.
73
+
74
+ Note:
75
+ The Telnyx API does not support updating connections directly.
76
+ Only GET, HEAD, and OPTIONS methods are allowed.
77
+ Please create a new connection with the desired settings instead.
78
+
79
+ Args:
80
+ connection_id: Connection ID
81
+ data: Update data
82
+
83
+ Returns:
84
+ Dict[str, Any]: Response data
85
+
86
+ Raises:
87
+ Exception: The Telnyx API does not support updating connections directly.
88
+ """
89
+ raise Exception(
90
+ "The Telnyx API does not support updating connections directly. "
91
+ "Please create a new connection with the desired settings instead."
92
+ )
@@ -0,0 +1,52 @@
1
+ """Telnyx embeddings service."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from ...utils.logger import get_logger
6
+ from ..client import TelnyxClient
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ class EmbeddingsService:
12
+ """Telnyx embeddings service."""
13
+
14
+ def __init__(self, client: Optional[TelnyxClient] = None):
15
+ """Initialize the service.
16
+
17
+ Args:
18
+ client: Telnyx API client (creates a new one if not provided)
19
+ """
20
+ self.client = client or TelnyxClient()
21
+
22
+ def list_embedded_buckets(
23
+ self,
24
+ ) -> Dict[str, Any]:
25
+ """List embedded buckets.
26
+
27
+ Returns:
28
+ Dict[str, Any]: Response data
29
+ """
30
+ return self.client.get("ai/embeddings/buckets")
31
+
32
+ def embed_url(
33
+ self,
34
+ request: Dict[str, Any],
35
+ ) -> Dict[str, Any]:
36
+ """Embed a URL.
37
+ Args:
38
+ request: request containing the url to embed `{url: str}`
39
+
40
+
41
+ Returns:
42
+ Dict[str, Any]: Response data
43
+ """
44
+ return self.client.post("ai/embeddings/url", data=request)
45
+
46
+ def create_embeddings(self, request: Dict[str, Any]) -> Dict[str, Any]:
47
+ """Create embeddings for a list of texts.
48
+
49
+ Returns:
50
+ Dict[str, Any]: Response data
51
+ """
52
+ return self.client.post("ai/embeddings", data=request)