diff --git a/servers/twilio/Dockerfile b/servers/twilio/Dockerfile new file mode 100644 index 0000000..478aaf2 --- /dev/null +++ b/servers/twilio/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy server code +COPY server.py . + +# Expose port +EXPOSE 8000 + +# Run the FastMCP server +CMD ["python", "-m", "fastmcp", "run", "server.py", "--transport", "streamable-http", "--port", "8000"] diff --git a/servers/twilio/README.md b/servers/twilio/README.md new file mode 100644 index 0000000..3720f03 --- /dev/null +++ b/servers/twilio/README.md @@ -0,0 +1,254 @@ +# Twilio MCP Server + +MCP server for interacting with the Twilio communication API. Send SMS and WhatsApp messages, make phone calls, and manage your Twilio account. + +## Features + +- **SMS Messaging**: Send and manage SMS/MMS messages +- **WhatsApp Messaging**: Send WhatsApp messages via Twilio +- **Voice Calls**: Initiate and manage phone calls +- **Phone Number Management**: List and lookup phone numbers +- **Account Management**: Check balance and account status + +## Setup + +### Prerequisites + +- Twilio account (sign up at [twilio.com](https://www.twilio.com/try-twilio)) +- Twilio phone number (for sending SMS/calls) +- Account credentials from [Twilio Console](https://console.twilio.com) + +### Environment Variables + +- `TWILIO_ACCOUNT_SID` (required): Your Twilio Account SID +- `TWILIO_AUTH_TOKEN` (required): Your Twilio Auth Token + +**How to get credentials:** +1. Go to [console.twilio.com](https://console.twilio.com) +2. Find your Account SID and Auth Token in the dashboard +3. Keep these secure - they provide full access to your Twilio account + +## Available Tools + +### Messaging Tools + +#### `send_sms` +Send an SMS message. + +**Parameters:** +- `to` (string, required): Recipient phone number in E.164 format (e.g., +1234567890) +- `from_` (string, required): Your Twilio phone number in E.164 format +- `body` (string, required): Message content (up to 1600 characters) +- `media_url` (string, optional): URL of media to send (converts to MMS) + +**Example:** +```python +result = await send_sms( + to="+1234567890", + from_="+1987654321", + body="Hello from Twilio MCP!", + media_url="https://example.com/image.jpg" +) +``` + +#### `send_whatsapp` +Send a WhatsApp message via Twilio. + +**Parameters:** +- `to` (string, required): Recipient WhatsApp number (e.g., whatsapp:+1234567890) +- `from_` (string, required): Your Twilio WhatsApp number (e.g., whatsapp:+1987654321) +- `body` (string, required): Message content +- `media_url` (string, optional): URL of media to send + +**Example:** +```python +result = await send_whatsapp( + to="whatsapp:+1234567890", + from_="whatsapp:+1987654321", + body="Hello from WhatsApp!", + media_url="https://example.com/image.jpg" +) +``` + +#### `list_messages` +List sent and received messages with optional filters. + +**Parameters:** +- `to` (string, optional): Filter by recipient phone number +- `from_` (string, optional): Filter by sender phone number +- `date_sent` (string, optional): Filter by date in YYYY-MM-DD format +- `page_size` (int, optional): Number of results (default: 50, max: 1000) + +**Example:** +```python +messages = await list_messages(from_="+1987654321", page_size=20) +``` + +#### `get_message` +Get details of a specific message. + +**Parameters:** +- `message_sid` (string, required): The Twilio message SID (e.g., SMxxxxx) + +**Example:** +```python +message = await get_message(message_sid="SM1234567890abcdef") +``` + +### Voice Call Tools + +#### `make_call` +Initiate an outbound phone call. + +**Parameters:** +- `to` (string, required): Recipient phone number in E.164 format +- `from_` (string, required): Your Twilio phone number in E.164 format +- `url` (string, required): URL that returns TwiML instructions for the call +- `method` (string, optional): HTTP method (GET or POST, default: POST) +- `status_callback` (string, optional): URL for call status updates + +**Example:** +```python +call = await make_call( + to="+1234567890", + from_="+1987654321", + url="https://example.com/twiml", + status_callback="https://example.com/status" +) +``` + +#### `list_calls` +List call logs with optional filters. + +**Parameters:** +- `to` (string, optional): Filter by recipient phone number +- `from_` (string, optional): Filter by caller phone number +- `status` (string, optional): Filter by status (queued, ringing, in-progress, completed, etc.) +- `page_size` (int, optional): Number of results (default: 50, max: 1000) + +**Example:** +```python +calls = await list_calls(status="completed", page_size=10) +``` + +#### `get_call` +Get details of a specific call. + +**Parameters:** +- `call_sid` (string, required): The Twilio call SID (e.g., CAxxxxx) + +**Example:** +```python +call = await get_call(call_sid="CA1234567890abcdef") +``` + +### Account & Phone Number Tools + +#### `get_account_balance` +Get your current Twilio account balance. + +**Example:** +```python +balance = await get_account_balance() +# Returns: {"balance": "100.00", "currency": "USD"} +``` + +#### `list_phone_numbers` +List phone numbers owned by your Twilio account. + +**Parameters:** +- `page_size` (int, optional): Number of results (default: 50, max: 1000) +- `phone_number` (string, optional): Filter by specific phone number +- `friendly_name` (string, optional): Filter by friendly name + +**Example:** +```python +numbers = await list_phone_numbers(page_size=10) +``` + +#### `lookup_phone_number` +Validate and get information about a phone number. + +**Parameters:** +- `phone_number` (string, required): Phone number to lookup in E.164 format +- `country_code` (string, optional): ISO country code if using national format (e.g., 'US') +- `type_` (string, optional): Additional data ('carrier' or 'caller-name') + +**Example:** +```python +info = await lookup_phone_number( + phone_number="+1234567890", + type_="carrier" +) +``` + +## Phone Number Format + +All phone numbers must be in **E.164 format**: +- Start with `+` followed by country code +- Example US number: `+14155552671` +- Example UK number: `+442071838750` + +## Rate Limits and Pricing + +### Rate Limits +- **SMS**: 100 messages per second per account (contact Twilio to increase) +- **Voice**: 100 concurrent calls per account (contact Twilio to increase) +- **API Requests**: 10,000 requests per second + +### Pricing (as of 2024, subject to change) +- **SMS (US)**: $0.0079 per message +- **WhatsApp**: $0.005 per conversation (first 1,000 free monthly) +- **Voice (US)**: $0.0140 per minute +- **Phone Number**: $1.15 per month +- **Lookup API**: $0.005 per request (carrier info extra) + +Visit [Twilio Pricing](https://www.twilio.com/pricing) for current rates. + +## Security Best Practices + +1. **Never commit credentials**: Keep `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN` in environment variables +2. **Use API keys**: Consider using [Twilio API Keys](https://www.twilio.com/docs/iam/keys/api-key) for additional security +3. **Enable IP filtering**: Restrict API access to specific IP addresses in Twilio Console +4. **Monitor usage**: Set up usage alerts in Twilio Console to detect unusual activity +5. **Rotate credentials**: Periodically rotate your Auth Token +6. **Use test credentials**: Use test credentials for development (prefix with `AC` for test accounts) + +## TwiML Resources + +For making calls with `make_call`, you need a URL that returns TwiML (Twilio Markup Language): + +**Simple TwiML example:** +```xml + + + Hello! This is a call from Twilio. + +``` + +Learn more about TwiML at [twilio.com/docs/voice/twiml](https://www.twilio.com/docs/voice/twiml) + +## API Documentation + +For detailed information about the Twilio API: +- [Twilio API Documentation](https://www.twilio.com/docs/usage/api) +- [Messaging API](https://www.twilio.com/docs/sms) +- [Voice API](https://www.twilio.com/docs/voice) +- [WhatsApp API](https://www.twilio.com/docs/whatsapp) +- [Lookup API](https://www.twilio.com/docs/lookup) + +## Error Handling + +Common errors and solutions: + +- **21211**: Invalid phone number - Ensure E.164 format +- **21608**: Unverified number - Verify trial account numbers in Console +- **21610**: Message blocked - Check compliance and opt-in requirements +- **20003**: Authentication error - Verify credentials are correct +- **20429**: Rate limit exceeded - Implement exponential backoff + +## Support + +- [Twilio Support](https://support.twilio.com) +- [Twilio Community](https://www.twilio.com/community) +- [Twilio Console](https://console.twilio.com) diff --git a/servers/twilio/requirements.txt b/servers/twilio/requirements.txt new file mode 100644 index 0000000..42f0167 --- /dev/null +++ b/servers/twilio/requirements.txt @@ -0,0 +1,4 @@ +fastmcp>=0.2.0 +httpx>=0.27.0 +python-dotenv>=1.0.0 +uvicorn>=0.30.0 diff --git a/servers/twilio/server.json b/servers/twilio/server.json new file mode 100644 index 0000000..f9ae7a6 --- /dev/null +++ b/servers/twilio/server.json @@ -0,0 +1,68 @@ +{ + "name": "ai.nimbletools/twilio", + "version": "1.0.0", + "description": "Twilio API: send SMS, WhatsApp messages, make calls, and manage communication", + "category": "communication-collaboration", + "status": "active", + "homepage": "https://github.com/NimbleBrainInc/mcp-registry/tree/main/servers/twilio", + "repository": { + "url": "https://github.com/NimbleBrainInc/mcp-registry", + "source": "github", + "branch": "main" + }, + "websiteUrl": "https://www.twilio.com/", + "license": "MIT", + "tags": [ + "twilio", + "sms", + "whatsapp", + "voice", + "calls", + "messaging", + "communication", + "notifications", + "requires-api-key" + ], + "transport": { + "type": "streamable-http", + "url": "https://mcp.nimbletools.ai/mcp" + }, + "environmentVariables": { + "TWILIO_ACCOUNT_SID": { + "description": "Twilio Account SID (get from https://console.twilio.com)", + "required": true, + "secret": true + }, + "TWILIO_AUTH_TOKEN": { + "description": "Twilio Auth Token (get from https://console.twilio.com)", + "required": true, + "secret": true + } + }, + "_meta": { + "display": { + "icon": "https://cdn.simpleicons.org/twilio", + "color": "#F22F46" + }, + "deployment": { + "protocol": "http", + "registry": "ghcr.io", + "image": "ghcr.io/nimblebraininc/mcp-twilio:latest", + "port": 8000, + "mcpPath": "/mcp" + }, + "resources": { + "limits": { + "cpu": "200m", + "memory": "256Mi" + }, + "requests": { + "cpu": "100m", + "memory": "128Mi" + } + }, + "capabilities": { + "tools": true + } + } +} diff --git a/servers/twilio/server.py b/servers/twilio/server.py new file mode 100644 index 0000000..4d12a93 --- /dev/null +++ b/servers/twilio/server.py @@ -0,0 +1,311 @@ +""" +Twilio MCP Server +Provides tools for interacting with the Twilio communication API. +""" + +import os +from typing import Optional +import httpx +from fastmcp import FastMCP + +# Initialize FastMCP server +mcp = FastMCP("Twilio MCP Server") + +# Get API credentials from environment +TWILIO_ACCOUNT_SID = os.getenv("TWILIO_ACCOUNT_SID") +TWILIO_AUTH_TOKEN = os.getenv("TWILIO_AUTH_TOKEN") +BASE_URL = "https://api.twilio.com/2010-04-01" + + +def get_auth(): + """Get HTTP Basic Auth credentials for Twilio API.""" + if not TWILIO_ACCOUNT_SID or not TWILIO_AUTH_TOKEN: + raise ValueError("TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN environment variables are required") + return (TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) + + +@mcp.tool() +async def send_sms(to: str, from_: str, body: str, media_url: Optional[str] = None) -> dict: + """ + Send an SMS message via Twilio. + + Args: + to: Recipient phone number in E.164 format (e.g., +1234567890) + from_: Your Twilio phone number in E.164 format + body: Message content (up to 1600 characters) + media_url: Optional URL of media to send (MMS) + + Returns: + Dictionary containing message details including SID and status + """ + async with httpx.AsyncClient() as client: + data = { + "To": to, + "From": from_, + "Body": body, + } + if media_url: + data["MediaUrl"] = media_url + + response = await client.post( + f"{BASE_URL}/Accounts/{TWILIO_ACCOUNT_SID}/Messages.json", + auth=get_auth(), + data=data, + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def send_whatsapp(to: str, from_: str, body: str, media_url: Optional[str] = None) -> dict: + """ + Send a WhatsApp message via Twilio. + + Args: + to: Recipient WhatsApp number in E.164 format with 'whatsapp:' prefix (e.g., whatsapp:+1234567890) + from_: Your Twilio WhatsApp number with 'whatsapp:' prefix + body: Message content + media_url: Optional URL of media to send + + Returns: + Dictionary containing message details including SID and status + """ + async with httpx.AsyncClient() as client: + # Ensure whatsapp: prefix + if not to.startswith("whatsapp:"): + to = f"whatsapp:{to}" + if not from_.startswith("whatsapp:"): + from_ = f"whatsapp:{from_}" + + data = { + "To": to, + "From": from_, + "Body": body, + } + if media_url: + data["MediaUrl"] = media_url + + response = await client.post( + f"{BASE_URL}/Accounts/{TWILIO_ACCOUNT_SID}/Messages.json", + auth=get_auth(), + data=data, + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def list_messages(to: Optional[str] = None, from_: Optional[str] = None, date_sent: Optional[str] = None, page_size: int = 50) -> dict: + """ + List sent and received messages. + + Args: + to: Filter by recipient phone number (optional) + from_: Filter by sender phone number (optional) + date_sent: Filter by date sent in YYYY-MM-DD format (optional) + page_size: Number of results to return (default: 50, max: 1000) + + Returns: + Dictionary containing list of messages + """ + async with httpx.AsyncClient() as client: + params = {"PageSize": page_size} + if to: + params["To"] = to + if from_: + params["From"] = from_ + if date_sent: + params["DateSent"] = date_sent + + response = await client.get( + f"{BASE_URL}/Accounts/{TWILIO_ACCOUNT_SID}/Messages.json", + auth=get_auth(), + params=params, + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def get_message(message_sid: str) -> dict: + """ + Get details of a specific message. + + Args: + message_sid: The Twilio message SID (e.g., SMxxxxx or MMxxxxx) + + Returns: + Dictionary containing message details + """ + async with httpx.AsyncClient() as client: + response = await client.get( + f"{BASE_URL}/Accounts/{TWILIO_ACCOUNT_SID}/Messages/{message_sid}.json", + auth=get_auth(), + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def make_call(to: str, from_: str, url: str, method: str = "POST", status_callback: Optional[str] = None) -> dict: + """ + Initiate an outbound phone call. + + Args: + to: Recipient phone number in E.164 format + from_: Your Twilio phone number in E.164 format + url: URL that returns TwiML instructions for the call + method: HTTP method to use for url (GET or POST, default: POST) + status_callback: Optional URL for call status updates + + Returns: + Dictionary containing call details including SID and status + """ + async with httpx.AsyncClient() as client: + data = { + "To": to, + "From": from_, + "Url": url, + "Method": method, + } + if status_callback: + data["StatusCallback"] = status_callback + + response = await client.post( + f"{BASE_URL}/Accounts/{TWILIO_ACCOUNT_SID}/Calls.json", + auth=get_auth(), + data=data, + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def list_calls(to: Optional[str] = None, from_: Optional[str] = None, status: Optional[str] = None, page_size: int = 50) -> dict: + """ + List call logs. + + Args: + to: Filter by recipient phone number (optional) + from_: Filter by caller phone number (optional) + status: Filter by status (queued, ringing, in-progress, completed, etc.) + page_size: Number of results to return (default: 50, max: 1000) + + Returns: + Dictionary containing list of calls + """ + async with httpx.AsyncClient() as client: + params = {"PageSize": page_size} + if to: + params["To"] = to + if from_: + params["From"] = from_ + if status: + params["Status"] = status + + response = await client.get( + f"{BASE_URL}/Accounts/{TWILIO_ACCOUNT_SID}/Calls.json", + auth=get_auth(), + params=params, + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def get_call(call_sid: str) -> dict: + """ + Get details of a specific call. + + Args: + call_sid: The Twilio call SID (e.g., CAxxxxx) + + Returns: + Dictionary containing call details + """ + async with httpx.AsyncClient() as client: + response = await client.get( + f"{BASE_URL}/Accounts/{TWILIO_ACCOUNT_SID}/Calls/{call_sid}.json", + auth=get_auth(), + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def get_account_balance() -> dict: + """ + Get the current account balance. + + Returns: + Dictionary containing balance information including currency and amount + """ + async with httpx.AsyncClient() as client: + response = await client.get( + f"{BASE_URL}/Accounts/{TWILIO_ACCOUNT_SID}/Balance.json", + auth=get_auth(), + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def list_phone_numbers(page_size: int = 50, phone_number: Optional[str] = None, friendly_name: Optional[str] = None) -> dict: + """ + List phone numbers owned by your Twilio account. + + Args: + page_size: Number of results to return (default: 50, max: 1000) + phone_number: Filter by specific phone number (optional) + friendly_name: Filter by friendly name (optional) + + Returns: + Dictionary containing list of phone numbers + """ + async with httpx.AsyncClient() as client: + params = {"PageSize": page_size} + if phone_number: + params["PhoneNumber"] = phone_number + if friendly_name: + params["FriendlyName"] = friendly_name + + response = await client.get( + f"{BASE_URL}/Accounts/{TWILIO_ACCOUNT_SID}/IncomingPhoneNumbers.json", + auth=get_auth(), + params=params, + ) + response.raise_for_status() + return response.json() + + +@mcp.tool() +async def lookup_phone_number(phone_number: str, country_code: Optional[str] = None, type_: Optional[str] = None) -> dict: + """ + Validate and get information about a phone number using Twilio Lookup API. + + Args: + phone_number: Phone number to lookup in E.164 format or national format + country_code: ISO country code if using national format (e.g., 'US') + type_: Additional data to retrieve ('carrier' or 'caller-name', optional) + + Returns: + Dictionary containing phone number details, validation status, and carrier info + """ + async with httpx.AsyncClient() as client: + params = {} + if country_code: + params["CountryCode"] = country_code + if type_: + params["Type"] = type_ + + response = await client.get( + f"https://lookups.twilio.com/v1/PhoneNumbers/{phone_number}", + auth=get_auth(), + params=params, + ) + response.raise_for_status() + return response.json() + + +if __name__ == "__main__": + mcp.run() diff --git a/servers/twilio/test.json b/servers/twilio/test.json new file mode 100644 index 0000000..1003684 --- /dev/null +++ b/servers/twilio/test.json @@ -0,0 +1,68 @@ +{ + "tests": [ + { + "name": "Get Account Balance", + "tool": "get_account_balance", + "params": {}, + "expectedFields": [ + "account_sid", + "balance", + "currency" + ], + "assertions": [ + { + "type": "exists", + "path": "account_sid" + }, + { + "type": "exists", + "path": "currency" + } + ] + }, + { + "name": "List Phone Numbers", + "tool": "list_phone_numbers", + "params": { + "page_size": 5 + }, + "expectedFields": [ + "incoming_phone_numbers", + "uri", + "first_page_uri", + "next_page_uri", + "previous_page_uri", + "page", + "page_size" + ], + "assertions": [ + { + "type": "exists", + "path": "incoming_phone_numbers" + } + ] + }, + { + "name": "List Messages", + "tool": "list_messages", + "params": { + "page_size": 5 + }, + "expectedFields": [ + "messages", + "uri", + "first_page_uri", + "next_page_uri", + "previous_page_uri", + "page", + "page_size" + ], + "assertions": [ + { + "type": "exists", + "path": "messages" + } + ] + } + ] +}