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
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Main entry point for the Telnyx MCP server."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
# Set up logging before importing anything else
|
|
8
|
+
logging.basicConfig(
|
|
9
|
+
level=logging.INFO,
|
|
10
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
11
|
+
)
|
|
12
|
+
logger = logging.getLogger("telnyx-mcp")
|
|
13
|
+
|
|
14
|
+
# Check for API key
|
|
15
|
+
if not os.getenv("TELNYX_API_KEY"):
|
|
16
|
+
logger.critical("TELNYX_API_KEY environment variable not set")
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
# Import server only after checking for API key
|
|
20
|
+
from .server import run_server
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
run_server()
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Configuration management for the Telnyx MCP server."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import random
|
|
5
|
+
import socket
|
|
6
|
+
from typing import Literal, Optional
|
|
7
|
+
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
from pydantic import Field
|
|
10
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
11
|
+
|
|
12
|
+
# Load environment variables from .env file
|
|
13
|
+
load_dotenv()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Generate a random port number in the dynamic/private port range
|
|
17
|
+
def get_random_high_port() -> int:
|
|
18
|
+
"""
|
|
19
|
+
Generate a random port number in the higher end of the dynamic/private port range.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
int: A random port number between 50000 and 65000
|
|
23
|
+
"""
|
|
24
|
+
return random.randint(50000, 65000)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def is_unix_socket_supported() -> bool:
|
|
28
|
+
"""
|
|
29
|
+
Check if the system supports Unix domain sockets.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
bool: True if Unix domain sockets are supported, False otherwise
|
|
33
|
+
"""
|
|
34
|
+
# On Unix-like systems, we can use os.name to check
|
|
35
|
+
if os.name != "posix":
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
# Further check by trying to create a Unix domain socket
|
|
39
|
+
try:
|
|
40
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
41
|
+
# Clean up the socket
|
|
42
|
+
sock.close()
|
|
43
|
+
return True
|
|
44
|
+
except (AttributeError, OSError):
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Determine API base URL based on environment
|
|
49
|
+
def get_api_base_url() -> str:
|
|
50
|
+
"""
|
|
51
|
+
Return the Telnyx API base URL.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
str: The Telnyx API base URL
|
|
55
|
+
- Value of TELNYX_API_BASE environment variable if set
|
|
56
|
+
- https://api.telnyx.com/v2 otherwise
|
|
57
|
+
"""
|
|
58
|
+
telnyx_api_base = os.getenv("TELNYX_API_BASE")
|
|
59
|
+
if telnyx_api_base:
|
|
60
|
+
return telnyx_api_base
|
|
61
|
+
return "https://api.telnyx.com/v2"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Settings(BaseSettings):
|
|
65
|
+
"""Server settings."""
|
|
66
|
+
|
|
67
|
+
# Telnyx API settings
|
|
68
|
+
telnyx_api_key: str = Field(
|
|
69
|
+
default=os.getenv("TELNYX_API_KEY", ""),
|
|
70
|
+
description="Telnyx API key for authentication",
|
|
71
|
+
)
|
|
72
|
+
telnyx_api_base_url: str = Field(
|
|
73
|
+
default=get_api_base_url(),
|
|
74
|
+
description="Base URL for Telnyx API",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Server settings
|
|
78
|
+
host: str = Field(
|
|
79
|
+
default="0.0.0.0", description="Host to bind the server to"
|
|
80
|
+
)
|
|
81
|
+
port: int = Field(default=8000, description="Port to bind the server to")
|
|
82
|
+
debug: bool = Field(default=False, description="Enable debug mode")
|
|
83
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = (
|
|
84
|
+
Field(default="INFO", description="Logging level")
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Webhook settings
|
|
88
|
+
webhook_enabled: bool = Field(
|
|
89
|
+
default=False, description="Enable webhook receiver"
|
|
90
|
+
)
|
|
91
|
+
webhook_port: int = Field(
|
|
92
|
+
default=get_random_high_port(),
|
|
93
|
+
description="Port to bind the webhook server to (random high port to avoid conflicts)",
|
|
94
|
+
)
|
|
95
|
+
webhook_path: str = Field(
|
|
96
|
+
default="/webhooks", description="Path for webhook endpoint"
|
|
97
|
+
)
|
|
98
|
+
webhook_max_body_size: int = Field(
|
|
99
|
+
default=1_048_576, # 1 MB
|
|
100
|
+
description="Maximum webhook request body size in bytes",
|
|
101
|
+
)
|
|
102
|
+
use_unix_socket: bool = Field(
|
|
103
|
+
default=is_unix_socket_supported(),
|
|
104
|
+
description="Use Unix domain socket instead of TCP/IP port (Unix-like systems only)",
|
|
105
|
+
)
|
|
106
|
+
socket_dir: Optional[str] = Field(
|
|
107
|
+
default=None,
|
|
108
|
+
description="Directory for Unix domain socket (default: system temp directory)",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Ngrok settings
|
|
112
|
+
ngrok_enabled: bool = Field(
|
|
113
|
+
default=bool(os.getenv("NGROK_AUTHTOKEN", ""))
|
|
114
|
+
or bool(os.getenv("NGROK_URL", "")),
|
|
115
|
+
description="Enable ngrok tunnel for webhooks",
|
|
116
|
+
)
|
|
117
|
+
ngrok_authtoken: Optional[str] = Field(
|
|
118
|
+
default=os.getenv("NGROK_AUTHTOKEN", None),
|
|
119
|
+
description="Ngrok authentication token",
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
ngrok_url: Optional[str] = Field(
|
|
123
|
+
default=os.getenv("NGROK_URL", None),
|
|
124
|
+
description="NGROK custom domain - Experimental - TLS errors are possible.",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# MCP settings
|
|
128
|
+
warn_on_duplicate_resources: bool = Field(
|
|
129
|
+
default=True, description="Warn on duplicate resources"
|
|
130
|
+
)
|
|
131
|
+
warn_on_duplicate_tools: bool = Field(
|
|
132
|
+
default=True, description="Warn on duplicate tools"
|
|
133
|
+
)
|
|
134
|
+
warn_on_duplicate_prompts: bool = Field(
|
|
135
|
+
default=True, description="Warn on duplicate prompts"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
model_config = SettingsConfigDict(
|
|
139
|
+
env_file=".env",
|
|
140
|
+
env_file_encoding="utf-8",
|
|
141
|
+
env_nested_delimiter="__",
|
|
142
|
+
extra="ignore",
|
|
143
|
+
validate_default=True,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Create a global settings instance
|
|
148
|
+
settings = Settings()
|
telnyx_mcp_server/mcp.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Simple MCP server using FastMCP framework with STDIO transport."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import ( # Added Sequence
|
|
5
|
+
Any,
|
|
6
|
+
Dict,
|
|
7
|
+
List,
|
|
8
|
+
Optional,
|
|
9
|
+
Sequence,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
from fastmcp import FastMCP
|
|
14
|
+
|
|
15
|
+
# MCPTool is defined in mcp.types, but often exposed via fastmcp or mcp.server
|
|
16
|
+
# For clarity, let's try importing directly if fastmcp doesn't re-export it well.
|
|
17
|
+
try:
|
|
18
|
+
from fastmcp import MCPTool
|
|
19
|
+
except ImportError:
|
|
20
|
+
from mcp.types import (
|
|
21
|
+
Tool as MCPTool, # Fallback if not in fastmcp directly
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from mcp.types import EmbeddedResource, ImageContent, TextContent
|
|
25
|
+
|
|
26
|
+
from .telnyx.client import TelnyxClient # Assuming this path is correct
|
|
27
|
+
from .utils.logger import get_logger # Assuming this path is correct
|
|
28
|
+
|
|
29
|
+
logger = get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
# Load environment variables
|
|
32
|
+
load_dotenv()
|
|
33
|
+
|
|
34
|
+
# Get API key from environment
|
|
35
|
+
api_key = os.getenv("TELNYX_API_KEY", "")
|
|
36
|
+
if not api_key:
|
|
37
|
+
logger.error("TELNYX_API_KEY environment variable must be set")
|
|
38
|
+
raise ValueError("TELNYX_API_KEY environment variable must be set")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FilterableFastMCP(FastMCP):
|
|
42
|
+
"""Extended FastMCP class that supports tool filtering."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, *args, **kwargs):
|
|
45
|
+
# Call super().__init__() first. This will run FastMCP's _setup_handlers,
|
|
46
|
+
# which will register self.list_tools and self.call_tool (from this FilterableFastMCP class)
|
|
47
|
+
super().__init__(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
self._enabled_tools: Optional[List[str]] = None
|
|
50
|
+
self._excluded_tools: List[str] = []
|
|
51
|
+
# Note: The original _original_list_tools_handler and _filtered_list_tools etc. are removed
|
|
52
|
+
# as the filtering is now done by overriding list_tools and call_tool directly.
|
|
53
|
+
|
|
54
|
+
def set_enabled_tools(self, tool_names: List[str]) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Set specific tools to enable. All other tools will be disabled.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
tool_names: List of tool names to enable
|
|
60
|
+
"""
|
|
61
|
+
self._enabled_tools = tool_names
|
|
62
|
+
logger.info(
|
|
63
|
+
f"Tool filtering enabled. Available tools: {', '.join(tool_names)}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def set_excluded_tools(self, tool_names: List[str]) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Set specific tools to exclude. All other tools will remain enabled.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
tool_names: List of tool names to exclude
|
|
72
|
+
"""
|
|
73
|
+
self._excluded_tools = tool_names
|
|
74
|
+
logger.info(
|
|
75
|
+
f"Tool exclusion enabled. Excluded tools: {', '.join(tool_names)}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
async def list_tools(
|
|
79
|
+
self,
|
|
80
|
+
) -> list[MCPTool]: # Matches signature from FastMCP
|
|
81
|
+
"""Filter the list of tools based on enabled/excluded settings."""
|
|
82
|
+
all_mcp_tools = (
|
|
83
|
+
await super().list_tools()
|
|
84
|
+
) # Get all tools as defined by FastMCP
|
|
85
|
+
|
|
86
|
+
# If no filtering is configured, return all tools
|
|
87
|
+
if self._enabled_tools is None and not self._excluded_tools:
|
|
88
|
+
return all_mcp_tools
|
|
89
|
+
|
|
90
|
+
filtered_mcp_tools = []
|
|
91
|
+
for tool_spec in all_mcp_tools:
|
|
92
|
+
# MCPTool has a 'name' attribute according to MCP spec and fastmcp usage
|
|
93
|
+
tool_name = tool_spec.name
|
|
94
|
+
|
|
95
|
+
# Check if tool should be included
|
|
96
|
+
if (
|
|
97
|
+
self._enabled_tools is not None
|
|
98
|
+
and tool_name not in self._enabled_tools
|
|
99
|
+
):
|
|
100
|
+
logger.debug(
|
|
101
|
+
f"Filtering out tool: {tool_name} (not in enabled list)"
|
|
102
|
+
)
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
# Check if tool should be excluded
|
|
106
|
+
if tool_name in self._excluded_tools:
|
|
107
|
+
logger.debug(
|
|
108
|
+
f"Filtering out tool: {tool_name} (in excluded list)"
|
|
109
|
+
)
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
filtered_mcp_tools.append(tool_spec)
|
|
113
|
+
|
|
114
|
+
# This logging can be verbose if many tools, consider conditional logging or removing
|
|
115
|
+
# logger.info(f"Filtered tools from {len(all_mcp_tools)} to {len(filtered_mcp_tools)}")
|
|
116
|
+
return filtered_mcp_tools
|
|
117
|
+
|
|
118
|
+
async def call_tool(
|
|
119
|
+
self,
|
|
120
|
+
name: str,
|
|
121
|
+
arguments: Dict[str, Any], # 'name' instead of 'key' to match FastMCP
|
|
122
|
+
) -> Sequence[
|
|
123
|
+
TextContent | ImageContent | EmbeddedResource
|
|
124
|
+
]: # Matches signature
|
|
125
|
+
"""Filter tool calls based on enabled/excluded settings."""
|
|
126
|
+
# Check if tool is allowed
|
|
127
|
+
if self._enabled_tools is not None and name not in self._enabled_tools:
|
|
128
|
+
logger.warning(f"Attempted to call disabled tool: '{name}'")
|
|
129
|
+
# Consider raising a specific MCP error type if available/appropriate
|
|
130
|
+
raise ValueError(f"Tool '{name}' is not enabled")
|
|
131
|
+
|
|
132
|
+
if name in self._excluded_tools:
|
|
133
|
+
logger.warning(f"Attempted to call excluded tool: '{name}'")
|
|
134
|
+
raise ValueError(f"Tool '{name}' is excluded")
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Call the original FastMCP behavior to execute the tool
|
|
138
|
+
return await super().call_tool(name, arguments)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f"Error calling tool '{name}': {str(e)}")
|
|
141
|
+
raise
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Create a single shared MCP instance with filtering support
|
|
145
|
+
mcp = FilterableFastMCP("Telnyx MCP")
|
|
146
|
+
|
|
147
|
+
# Initialize Telnyx client with API key from environment
|
|
148
|
+
telnyx_client = TelnyxClient(api_key=api_key)
|