Skip to main content
Glama
onesignal_server.py76.7 kB
import os import json import re import requests import logging from typing import List, Dict, Any, Optional, Union from mcp.server.fastmcp import FastMCP, Context from dotenv import load_dotenv # Server information __version__ = "2.1.0" # Configure logging logging.basicConfig( level=logging.INFO, # Default level, will be overridden by env var if set format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler() ] ) logger = logging.getLogger("onesignal-mcp") # Load environment variables from .env file load_dotenv() logger.info("Environment variables loaded") # Get log level from environment, default to INFO, and ensure it's uppercase log_level_str = os.getenv("LOG_LEVEL", "INFO").upper() valid_log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] if log_level_str not in valid_log_levels: logger.warning(f"Invalid LOG_LEVEL '{log_level_str}' found in environment. Using INFO instead.") log_level_str = "INFO" # Apply the validated log level logger.setLevel(log_level_str) # Initialize the MCP server, passing the validated log level mcp = FastMCP("onesignal-server", log_level=log_level_str) logger.info(f"OneSignal MCP server initialized with log level: {log_level_str}") # OneSignal API configuration # v2 API keys use the new base URL without /api/v1 ONESIGNAL_API_URL = "https://api.onesignal.com" ONESIGNAL_ORG_API_KEY = os.getenv("ONESIGNAL_ORG_API_KEY", "") # Class to manage app configurations class AppConfig: def __init__(self, app_id: str, api_key: str, name: str = None): self.app_id = app_id self.api_key = api_key self.name = name or app_id def __str__(self): return f"{self.name} ({self.app_id})" # Dictionary to store app configurations app_configs: Dict[str, AppConfig] = {} # Load app configurations from environment variables # Mandible app configuration mandible_app_id = os.getenv("ONESIGNAL_MANDIBLE_APP_ID", "") or os.getenv("ONESIGNAL_APP_ID", "") mandible_api_key = os.getenv("ONESIGNAL_MANDIBLE_API_KEY", "") or os.getenv("ONESIGNAL_API_KEY", "") if mandible_app_id and mandible_api_key: app_configs["mandible"] = AppConfig(mandible_app_id, mandible_api_key, "Mandible") current_app_key = "mandible" logger.info(f"Mandible app configured with ID: {mandible_app_id}") # Weird Brains app configuration weirdbrains_app_id = os.getenv("ONESIGNAL_WEIRDBRAINS_APP_ID", "") weirdbrains_api_key = os.getenv("ONESIGNAL_WEIRDBRAINS_API_KEY", "") if weirdbrains_app_id and weirdbrains_api_key: app_configs["weirdbrains"] = AppConfig(weirdbrains_app_id, weirdbrains_api_key, "Weird Brains") if not current_app_key: current_app_key = "weirdbrains" logger.info(f"Weird Brains app configured with ID: {weirdbrains_app_id}") # Fallback for default app configuration if not app_configs: default_app_id = os.getenv("ONESIGNAL_APP_ID", "") default_api_key = os.getenv("ONESIGNAL_API_KEY", "") if default_app_id and default_api_key: app_configs["default"] = AppConfig(default_app_id, default_api_key, "Default App") current_app_key = "default" logger.info(f"Default app configured with ID: {default_app_id}") else: current_app_key = None logger.warning("No app configurations found. Use add_app to add an app configuration.") # Function to add a new app configuration def add_app_config(key: str, app_id: str, api_key: str, name: str = None) -> None: """Add a new app configuration to the available apps. Args: key: Unique identifier for this app configuration app_id: OneSignal App ID api_key: OneSignal REST API Key name: Display name for the app (optional) """ app_configs[key] = AppConfig(app_id, api_key, name or key) logger.info(f"Added app configuration '{key}' with ID: {app_id}") # Function to switch the current app def set_current_app(app_key: str) -> bool: """Set the current app to use for API requests. Args: app_key: The key of the app configuration to use Returns: True if successful, False if the app key doesn't exist """ global current_app_key if app_key in app_configs: current_app_key = app_key logger.info(f"Switched to app '{app_key}'") return True logger.error(f"Failed to switch app: '{app_key}' not found") return False # Function to get the current app configuration def get_current_app() -> Optional[AppConfig]: """Get the current app configuration. Returns: The current AppConfig or None if no app is set """ if current_app_key and current_app_key in app_configs: return app_configs[current_app_key] logger.warning("No current app is set. Use switch_app(key) to select an app.") return None # Helper function to determine whether to use Organization API Key def requires_org_api_key(endpoint: str) -> bool: """Determine if an endpoint requires the Organization API Key instead of a REST API Key. Args: endpoint: The API endpoint path Returns: True if the endpoint requires Organization API Key, False otherwise """ # Organization-level endpoints that require Organization API Key # Note: "apps" alone requires org key, but "apps/{app_id}/segments" etc. use app key import re # Exact matches that require org key if endpoint == "apps": return True # apps/{app_id} for viewing/updating a single app requires org key # But apps/{app_id}/segments, apps/{app_id}/users, etc. use app key if re.match(r"^apps/[^/]+$", endpoint): return True # apps/{app_id}/auth/tokens for API key management requires org key if re.match(r"^apps/[^/]+/auth/tokens", endpoint): return True # Export endpoints require org key if endpoint == "notifications/csv_export" or endpoint.startswith("notifications/csv_export/"): return True return False # Helper function for OneSignal API requests async def make_onesignal_request( endpoint: str, method: str = "GET", data: Dict[str, Any] = None, params: Dict[str, Any] = None, use_org_key: bool = None, app_key: str = None ) -> Dict[str, Any]: """Make a request to the OneSignal API with proper authentication. Args: endpoint: API endpoint path method: HTTP method (GET, POST, PUT, DELETE) data: Request body for POST/PUT requests params: Query parameters for GET requests use_org_key: Whether to use the organization API key instead of the REST API key If None, will be automatically determined based on the endpoint app_key: The key of the app configuration to use (uses current app if None) Returns: API response as dictionary """ headers = { "Content-Type": "application/json", "Accept": "application/json", } # If use_org_key is not explicitly specified, determine it based on the endpoint if use_org_key is None: use_org_key = requires_org_api_key(endpoint) # Determine which app configuration to use app_config = None if not use_org_key: if app_key and app_key in app_configs: app_config = app_configs[app_key] elif current_app_key and current_app_key in app_configs: app_config = app_configs[current_app_key] if not app_config: error_msg = "No app configuration available. Use set_current_app or specify app_key." logger.error(error_msg) return {"error": error_msg} # Check if it's a v2 API key if app_config.api_key.startswith("os_v2_"): headers["Authorization"] = f"Key {app_config.api_key}" else: headers["Authorization"] = f"Basic {app_config.api_key}" else: if not ONESIGNAL_ORG_API_KEY: error_msg = "Organization API Key not configured. Set the ONESIGNAL_ORG_API_KEY environment variable." logger.error(error_msg) return {"error": error_msg} # Check if it's a v2 API key if ONESIGNAL_ORG_API_KEY.startswith("os_v2_"): headers["Authorization"] = f"Key {ONESIGNAL_ORG_API_KEY}" else: headers["Authorization"] = f"Basic {ONESIGNAL_ORG_API_KEY}" url = f"{ONESIGNAL_API_URL}/{endpoint}" # If using app-specific endpoint and not using org key, add app_id to params if not already present if not use_org_key and app_config: if params is None: params = {} if "app_id" not in params and not endpoint.startswith("apps/"): params["app_id"] = app_config.app_id # For POST/PUT requests, add app_id to data if not already present if data is not None and method in ["POST", "PUT"] and "app_id" not in data and not endpoint.startswith("apps/"): data["app_id"] = app_config.app_id try: logger.debug(f"Making {method} request to {url}") logger.debug(f"Using {'Organization API Key' if use_org_key else 'App REST API Key'}") logger.debug(f"Authorization header type: {headers['Authorization'].split(' ')[0]}") if method == "GET": response = requests.get(url, headers=headers, params=params, timeout=30) elif method == "POST": response = requests.post(url, headers=headers, params=params, json=data, timeout=30) elif method == "PUT": response = requests.put(url, headers=headers, params=params, json=data, timeout=30) elif method == "DELETE": response = requests.delete(url, headers=headers, params=params, timeout=30) elif method == "PATCH": response = requests.patch(url, headers=headers, params=params, json=data, timeout=30) else: error_msg = f"Unsupported HTTP method: {method}" logger.error(error_msg) return {"error": error_msg} response.raise_for_status() return response.json() if response.text else {} except requests.exceptions.RequestException as e: error_message = f"Error: {str(e)}" try: if hasattr(e, 'response') and e.response is not None: error_data = e.response.json() if isinstance(error_data, dict): error_message = f"Error: {error_data.get('errors', [e.response.reason])[0]}" except Exception: pass logger.error(f"API request failed: {error_message}") return {"error": error_message} except Exception as e: error_message = f"Unexpected error: {str(e)}" logger.exception(error_message) return {"error": error_message} # Resource for OneSignal configuration information @mcp.resource("onesignal://config") def get_onesignal_config() -> str: """Get information about the OneSignal configuration""" current_app = get_current_app() app_list = "\n".join([f"- {key}: {app}" for key, app in app_configs.items()]) return f""" OneSignal Server Configuration: Version: {__version__} API URL: {ONESIGNAL_API_URL} Organization API Key Status: {'Configured' if ONESIGNAL_ORG_API_KEY else 'Not configured'} Available Apps: {app_list or "No apps configured"} Current App: {current_app.name if current_app else 'None'} This MCP server provides tools for: - Viewing and managing messages (push notifications, emails, SMS) - Managing users and subscriptions - Viewing and managing segments - Creating and managing templates - Viewing app information - Managing multiple OneSignal applications Make sure you have set the appropriate environment variables in your .env file. """ # === App Management Tools === @mcp.tool() async def list_apps() -> str: """List all configured OneSignal apps in this server.""" if not app_configs: return "No apps configured. Use add_app to add a new app configuration." current_app = get_current_app() result = ["Configured OneSignal Apps:"] for key, app in app_configs.items(): current_marker = " (current)" if current_app and key == current_app_key else "" result.append(f"- {key}: {app.name} (App ID: {app.app_id}){current_marker}") return "\n".join(result) @mcp.tool() async def add_app(key: str, app_id: str, api_key: str, name: str = None) -> str: """Add a new OneSignal app configuration locally. Args: key: Unique identifier for this app configuration app_id: OneSignal App ID api_key: OneSignal REST API Key name: Display name for the app (optional) """ if not key or not app_id or not api_key: return "Error: All parameters (key, app_id, api_key) are required." if key in app_configs: return f"Error: App key '{key}' already exists. Use a different key or update_app to modify it." add_app_config(key, app_id, api_key, name) # If this is the first app, set it as current global current_app_key if len(app_configs) == 1: current_app_key = key return f"Successfully added app '{key}' with name '{name or key}'." @mcp.tool() async def update_local_app_config(key: str, app_id: str = None, api_key: str = None, name: str = None) -> str: """Update an existing local OneSignal app configuration. Args: key: The key of the app configuration to update locally app_id: New OneSignal App ID (optional) api_key: New OneSignal REST API Key (optional) name: New display name for the app (optional) """ if key not in app_configs: return f"Error: App key '{key}' not found." app = app_configs[key] updated = [] if app_id: app.app_id = app_id updated.append("App ID") if api_key: app.api_key = api_key updated.append("API Key") if name: app.name = name updated.append("Name") if not updated: return "No changes were made. Specify at least one parameter to update." logger.info(f"Updated app '{key}': {', '.join(updated)}") return f"Successfully updated app '{key}': {', '.join(updated)}." @mcp.tool() async def remove_app(key: str) -> str: """Remove a local OneSignal app configuration. Args: key: The key of the app configuration to remove locally """ if key not in app_configs: return f"Error: App key '{key}' not found." global current_app_key if current_app_key == key: if len(app_configs) > 1: # Set current to another app other_keys = [k for k in app_configs.keys() if k != key] current_app_key = other_keys[0] logger.info(f"Current app changed to '{current_app_key}' after removing '{key}'") else: current_app_key = None logger.warning("No current app set after removing the only app configuration") del app_configs[key] logger.info(f"Removed app configuration '{key}'") return f"Successfully removed app '{key}'." @mcp.tool() async def switch_app(key: str) -> str: """Switch the current app to use for API requests. Args: key: The key of the app configuration to use """ if key not in app_configs: return f"Error: App key '{key}' not found. Available apps: {', '.join(app_configs.keys()) or 'None'}" global current_app_key current_app_key = key app = app_configs[key] return f"Switched to app '{key}' ({app.name})." # === Message Management Tools === @mcp.tool() async def send_push_notification(title: str, message: str, segments: List[str] = None, external_ids: List[str] = None, data: Dict[str, Any] = None) -> Dict[str, Any]: """Send a new push notification through OneSignal. Args: title: Notification title. message: Notification message content. segments: List of segments to include (e.g., ["Subscribed Users"]). external_ids: List of external user IDs to target. data: Additional data to include with the notification (optional). """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} if not segments and not external_ids: segments = ["Subscribed Users"] # Default if no target specified notification_data = { "app_id": app_config.app_id, "contents": {"en": message}, "headings": {"en": title}, "target_channel": "push" } if segments: notification_data["included_segments"] = segments if external_ids: # Assuming make_onesignal_request handles converting list to JSON notification_data["include_external_user_ids"] = external_ids if data: notification_data["data"] = data # This endpoint uses app-specific REST API Key result = await make_onesignal_request("notifications", method="POST", data=notification_data, use_org_key=False) return result @mcp.tool() async def view_messages(limit: int = 20, offset: int = 0, kind: int = None) -> Dict[str, Any]: """View recent messages sent through OneSignal. Args: limit: Maximum number of messages to return (default: 20, max: 50) offset: Result offset for pagination (default: 0) kind: Filter by message type (0=Dashboard, 1=API, 3=Automated) (optional) """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} params = {"limit": min(limit, 50), "offset": offset} if kind is not None: params["kind"] = kind # This endpoint uses app-specific REST API Key result = await make_onesignal_request("notifications", method="GET", params=params, use_org_key=False) # Return the raw JSON result for flexibility return result @mcp.tool() async def view_message_details(message_id: str) -> Dict[str, Any]: """Get detailed information about a specific message. Args: message_id: The ID of the message to retrieve details for """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} # This endpoint uses app-specific REST API Key result = await make_onesignal_request(f"notifications/{message_id}", method="GET", use_org_key=False) # Return the raw JSON result return result @mcp.tool() async def view_message_history(message_id: str, event: str) -> Dict[str, Any]: """View the history / recipients of a message based on events. Args: message_id: The ID of the message. event: The event type to track (e.g., 'sent', 'clicked'). """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} data = { "app_id": app_config.app_id, "events": event, "email": get_current_app().name + "-history@example.com" # Requires an email to send the CSV report } # Endpoint uses REST API Key result = await make_onesignal_request(f"notifications/{message_id}/history", method="POST", data=data, use_org_key=False) return result @mcp.tool() async def cancel_message(message_id: str) -> Dict[str, Any]: """Cancel a scheduled message that hasn't been delivered yet. Args: message_id: The ID of the message to cancel """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} # This endpoint uses app-specific REST API Key result = await make_onesignal_request(f"notifications/{message_id}", method="DELETE", use_org_key=False) return result # === Segment Management Tools === @mcp.tool() async def view_segments() -> str: """List all segments available in your OneSignal app.""" app_config = get_current_app() if not app_config: return "No app currently selected. Use switch_app to select an app." # This endpoint requires app_id in the URL path endpoint = f"apps/{app_config.app_id}/segments" result = await make_onesignal_request(endpoint, method="GET", use_org_key=False) # Check if result is a dictionary with an error if isinstance(result, dict) and "error" in result: return f"Error retrieving segments: {result['error']}" # Handle different response formats if isinstance(result, dict): # Some endpoints return segments in a wrapper object segments = result.get("segments", []) elif isinstance(result, list): # Direct list of segments segments = result else: return f"Unexpected response format: {type(result)}" if not segments: return "No segments found." output = "Segments:\n\n" for segment in segments: if isinstance(segment, dict): output += f"ID: {segment.get('id')}\n" output += f"Name: {segment.get('name')}\n" output += f"Created: {segment.get('created_at')}\n" output += f"Updated: {segment.get('updated_at')}\n" output += f"Active: {segment.get('is_active', False)}\n" output += f"Read Only: {segment.get('read_only', False)}\n\n" return output @mcp.tool() async def create_segment(name: str, filters: str) -> str: """Create a new segment in your OneSignal app. Args: name: Name of the segment filters: JSON string representing the filters for this segment (e.g., '[{"field":"tag","key":"level","relation":"=","value":"10"}]') """ try: parsed_filters = json.loads(filters) except json.JSONDecodeError: return "Error: The filters parameter must be a valid JSON string." data = { "name": name, "filters": parsed_filters } endpoint = f"apps/{get_current_app().app_id}/segments" result = await make_onesignal_request(endpoint, method="POST", data=data) if "error" in result: return f"Error creating segment: {result['error']}" return f"Segment '{name}' created successfully with ID: {result.get('id')}" @mcp.tool() async def delete_segment(segment_id: str) -> str: """Delete a segment from your OneSignal app. Args: segment_id: ID of the segment to delete """ endpoint = f"apps/{get_current_app().app_id}/segments/{segment_id}" result = await make_onesignal_request(endpoint, method="DELETE") if "error" in result: return f"Error deleting segment: {result['error']}" return f"Segment '{segment_id}' deleted successfully" # === Template Management Tools === @mcp.tool() async def view_templates() -> str: """List all templates available in your OneSignal app.""" app_config = get_current_app() if not app_config: return "No app currently selected. Use switch_app to select an app." # Templates endpoint uses query param for app_id params = {"app_id": app_config.app_id} result = await make_onesignal_request("templates", method="GET", params=params, use_org_key=False) if "error" in result: return f"Error retrieving templates: {result['error']}" templates = result.get("templates", []) if not templates: return "No templates found." output = "Templates:\n\n" for template in templates: output += f"ID: {template.get('id')}\n" output += f"Name: {template.get('name')}\n" output += f"Created: {template.get('created_at')}\n" output += f"Updated: {template.get('updated_at')}\n\n" return output @mcp.tool() async def view_template_details(template_id: str) -> str: """Get detailed information about a specific template. Args: template_id: The ID of the template to retrieve details for """ params = {"app_id": get_current_app().app_id} result = await make_onesignal_request(f"templates/{template_id}", method="GET", params=params) if "error" in result: return f"Error fetching template details: {result['error']}" # Format the template details in a readable way heading = result.get("headings", {}).get("en", "No heading") if isinstance(result.get("headings"), dict) else "No heading" content = result.get("contents", {}).get("en", "No content") if isinstance(result.get("contents"), dict) else "No content" details = [ f"ID: {result.get('id')}", f"Name: {result.get('name')}", f"Title: {heading}", f"Message: {content}", f"Platform: {result.get('platform')}", f"Created: {result.get('created_at')}" ] return "\n".join(details) @mcp.tool() async def create_template(name: str, title: str, message: str) -> str: """Create a new template in your OneSignal app. Args: name: Name of the template title: Title/heading of the template message: Content/message of the template """ app_config = get_current_app() if not app_config: return "No app currently selected. Use switch_app to select an app." data = { "app_id": app_config.app_id, "name": name, "headings": {"en": title}, "contents": {"en": message} } # Templates endpoint uses app_id in the body result = await make_onesignal_request("templates", method="POST", data=data) if "error" in result: return f"Error creating template: {result['error']}" return f"Template '{name}' created successfully with ID: {result.get('id')}" # === App Information Tools === @mcp.tool() async def view_app_details() -> str: """Get detailed information about the configured OneSignal app.""" app_config = get_current_app() if not app_config: return "No app currently selected. Use switch_app to select an app." # This endpoint requires the app_id in the URL and Organization API Key result = await make_onesignal_request(f"apps/{app_config.app_id}", method="GET", use_org_key=True) if "error" in result: return f"Error retrieving app details: {result['error']}" output = f"ID: {result.get('id')}\n" output += f"Name: {result.get('name')}\n" output += f"Created: {result.get('created_at')}\n" output += f"Updated: {result.get('updated_at')}\n" output += f"GCM: {'Configured' if result.get('gcm_key') else 'Not Configured'}\n" output += f"APNS: {'Configured' if result.get('apns_env') else 'Not Configured'}\n" output += f"Chrome: {'Configured' if result.get('chrome_web_key') else 'Not Configured'}\n" output += f"Safari: {'Configured' if result.get('safari_site_origin') else 'Not Configured'}\n" output += f"Email: {'Configured' if result.get('email_marketing') else 'Not Configured'}\n" output += f"SMS: {'Configured' if result.get('sms_marketing') else 'Not Configured'}\n" return output @mcp.tool() async def view_apps() -> str: """List all OneSignal applications for the organization (requires Organization API Key).""" result = await make_onesignal_request("apps", method="GET", use_org_key=True) if "error" in result: if "401" in str(result["error"]) or "403" in str(result["error"]): return ("Error: Your Organization API Key is either not configured or doesn't have permission to view all apps. " "Make sure you've set the ONESIGNAL_ORG_API_KEY environment variable with a valid Organization API Key. " "Organization API Keys can be found in your OneSignal dashboard under Organizations > Keys & IDs.") return f"Error fetching applications: {result['error']}" if not result: return "No applications found." apps_info = [] for app in result: apps_info.append( f"ID: {app.get('id')}\n" f"Name: {app.get('name')}\n" f"GCM: {'Configured' if app.get('gcm_key') else 'Not Configured'}\n" f"APNS: {'Configured' if app.get('apns_env') else 'Not Configured'}\n" f"Created: {app.get('created_at')}" ) return "Applications:\n\n" + "\n\n".join(apps_info) # === Organization-level Tools === @mcp.tool() async def create_app(name: str, site_name: str = None) -> str: """Create a new OneSignal application at the organization level (requires Organization API Key). Args: name: Name of the new application site_name: Optional name of the website for the application """ data = { "name": name } if site_name: data["site_name"] = site_name result = await make_onesignal_request("apps", method="POST", data=data, use_org_key=True) if "error" in result: if "401" in str(result["error"]) or "403" in str(result["error"]): return ("Error: Your Organization API Key is either not configured or doesn't have permission to create apps. " "Make sure you've set the ONESIGNAL_ORG_API_KEY environment variable with a valid Organization API Key.") return f"Error creating application: {result['error']}" return f"Application '{name}' created successfully with ID: {result.get('id')}" @mcp.tool() async def update_app(app_id: str, name: str = None, site_name: str = None) -> str: """Update an existing OneSignal application at the organization level (requires Organization API Key). Args: app_id: ID of the app to update name: New name for the application (optional) site_name: New site name for the application (optional) """ data = {} if name: data["name"] = name if site_name: data["site_name"] = site_name if not data: return "Error: No update parameters provided. Specify at least one parameter to update." result = await make_onesignal_request(f"apps/{app_id}", method="PUT", data=data, use_org_key=True) if "error" in result: if "401" in str(result["error"]) or "403" in str(result["error"]): return ("Error: Your Organization API Key is either not configured or doesn't have permission to update apps. " "Make sure you've set the ONESIGNAL_ORG_API_KEY environment variable with a valid Organization API Key.") return f"Error updating application: {result['error']}" return f"Application '{app_id}' updated successfully" @mcp.tool() async def view_app_api_keys(app_id: str) -> str: """View API keys for a specific OneSignal app (requires Organization API Key). Args: app_id: The ID of the app to retrieve API keys for """ result = await make_onesignal_request(f"apps/{app_id}/auth/tokens", use_org_key=True) if "error" in result: if "401" in str(result["error"]) or "403" in str(result["error"]): return ("Error: Your Organization API Key is either not configured or doesn't have permission to view API keys. " "Make sure you've set the ONESIGNAL_ORG_API_KEY environment variable with a valid Organization API Key.") return f"Error fetching API keys: {result['error']}" if not result.get("tokens", []): return f"No API keys found for app ID: {app_id}" keys_info = [] for key in result.get("tokens", []): keys_info.append( f"ID: {key.get('id')}\n" f"Name: {key.get('name')}\n" f"Created: {key.get('created_at')}\n" f"Updated: {key.get('updated_at')}\n" f"IP Allowlist Mode: {key.get('ip_allowlist_mode', 'disabled')}" ) return f"API Keys for App {app_id}:\n\n" + "\n\n".join(keys_info) @mcp.tool() async def create_app_api_key(app_id: str, name: str) -> str: """Create a new API key for a specific OneSignal app (requires Organization API Key). Args: app_id: The ID of the app to create an API key for name: Name for the new API key """ data = { "name": name } result = await make_onesignal_request(f"apps/{app_id}/auth/tokens", method="POST", data=data, use_org_key=True) if "error" in result: if "401" in str(result["error"]) or "403" in str(result["error"]): return ("Error: Your Organization API Key is either not configured or doesn't have permission to create API keys. " "Make sure you've set the ONESIGNAL_ORG_API_KEY environment variable with a valid Organization API Key.") return f"Error creating API key: {result['error']}" # Format the API key details for display key_details = ( f"API Key '{name}' created successfully!\n\n" f"Key ID: {result.get('id')}\n" f"Token: {result.get('token')}\n\n" f"IMPORTANT: Save this token now! You won't be able to see the full token again." ) return key_details # === User Management Tools === @mcp.tool() async def create_user(name: str = None, email: str = None, external_id: str = None, tags: Dict[str, str] = None) -> Dict[str, Any]: """Create a new user in OneSignal. Args: name: User's name (optional) email: User's email address (optional) external_id: External user ID for identification (required for most use cases) tags: Additional user tags/properties (optional) """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} # Build the proper User Model structure data = {} # Identity section - external_id is the main identifier if external_id: data["identity"] = {"external_id": external_id} # Properties section - includes tags and other user properties properties = {} if tags: properties["tags"] = tags if properties: data["properties"] = properties # Subscriptions section - add email subscription if provided if email: data["subscriptions"] = [{ "type": "Email", "token": email, "enabled": True }] if not data.get("identity") and not data.get("subscriptions"): return {"error": "Must provide at least an external_id or email to create a user"} # Use the correct endpoint: apps/{app_id}/users result = await make_onesignal_request(f"apps/{app_config.app_id}/users", method="POST", data=data) return result @mcp.tool() async def view_user(user_id: str) -> Dict[str, Any]: """Get detailed information about a specific user. Args: user_id: The user identifier. Can be a onesignal_id or external_id. Will try onesignal_id first, then external_id. """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} # Try onesignal_id first (UUID format check) import re uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' if re.match(uuid_pattern, user_id.lower()): alias_label = "onesignal_id" else: alias_label = "external_id" # Use the correct endpoint: apps/{app_id}/users/by/{alias_label}/{alias_id} result = await make_onesignal_request( f"apps/{app_config.app_id}/users/by/{alias_label}/{user_id}", method="GET" ) return result @mcp.tool() async def update_user(user_id: str, name: str = None, email: str = None, tags: Dict[str, str] = None) -> Dict[str, Any]: """Update an existing user's information. Args: user_id: The user identifier (onesignal_id or external_id) name: New name for the user (optional) email: New email address (optional) tags: New or updated tags/properties (optional) """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} # Build properties update properties = {} if tags: properties["tags"] = tags if not properties and not email: return {"error": "No update parameters provided. Specify tags or email."} data = {} if properties: data["properties"] = properties # Determine alias type import re uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' if re.match(uuid_pattern, user_id.lower()): alias_label = "onesignal_id" else: alias_label = "external_id" # Use the correct endpoint: apps/{app_id}/users/by/{alias_label}/{alias_id} result = await make_onesignal_request( f"apps/{app_config.app_id}/users/by/{alias_label}/{user_id}", method="PATCH", data=data ) return result @mcp.tool() async def delete_user(user_id: str) -> Dict[str, Any]: """Delete a user and all their subscriptions. Args: user_id: The user identifier (onesignal_id or external_id) """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} # Determine alias type import re uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' if re.match(uuid_pattern, user_id.lower()): alias_label = "onesignal_id" else: alias_label = "external_id" # Use the correct endpoint: apps/{app_id}/users/by/{alias_label}/{alias_id} result = await make_onesignal_request( f"apps/{app_config.app_id}/users/by/{alias_label}/{user_id}", method="DELETE" ) return result @mcp.tool() async def view_user_identity(user_id: str) -> Dict[str, Any]: """Get user identity information (aliases). Args: user_id: The user identifier (onesignal_id or external_id) """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} # Determine alias type import re uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' if re.match(uuid_pattern, user_id.lower()): alias_label = "onesignal_id" else: alias_label = "external_id" # Use the correct endpoint: apps/{app_id}/users/by/{alias_label}/{alias_id}/identity result = await make_onesignal_request( f"apps/{app_config.app_id}/users/by/{alias_label}/{user_id}/identity", method="GET" ) return result @mcp.tool() async def create_or_update_alias(user_id: str, alias_label: str, alias_id: str) -> Dict[str, Any]: """Create or update a user alias. Args: user_id: The OneSignal User ID (onesignal_id or external_id to look up the user) alias_label: The type/label of the alias to create (e.g., "phone_number", "custom_id") alias_id: The alias identifier value """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} data = { "identity": { alias_label: alias_id } } # Determine lookup alias type import re uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' if re.match(uuid_pattern, user_id.lower()): lookup_alias = "onesignal_id" else: lookup_alias = "external_id" # Use the correct endpoint: apps/{app_id}/users/by/{alias_label}/{alias_id}/identity result = await make_onesignal_request( f"apps/{app_config.app_id}/users/by/{lookup_alias}/{user_id}/identity", method="PATCH", data=data ) return result @mcp.tool() async def delete_alias(user_id: str, alias_label: str) -> Dict[str, Any]: """Delete a user alias. Args: user_id: The OneSignal User ID (onesignal_id or external_id to look up the user) alias_label: The type/label of the alias to delete """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} # Determine lookup alias type import re uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' if re.match(uuid_pattern, user_id.lower()): lookup_alias = "onesignal_id" else: lookup_alias = "external_id" # Use the correct endpoint: apps/{app_id}/users/by/{alias_label}/{alias_id}/identity/{alias_to_delete} result = await make_onesignal_request( f"apps/{app_config.app_id}/users/by/{lookup_alias}/{user_id}/identity/{alias_label}", method="DELETE" ) return result # === Subscription Management Tools === @mcp.tool() async def create_subscription(user_id: str, subscription_type: str, identifier: str) -> Dict[str, Any]: """Create a new subscription for a user. Args: user_id: The user identifier (onesignal_id or external_id) subscription_type: Type of subscription (Email, SMS, iOSPush, AndroidPush, etc.) identifier: Email address, phone number (E.164), or push token """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} data = { "subscription": { "type": subscription_type, "token": identifier, "enabled": True } } # Determine lookup alias type import re uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' if re.match(uuid_pattern, user_id.lower()): alias_label = "onesignal_id" else: alias_label = "external_id" # Use the correct endpoint: apps/{app_id}/users/by/{alias_label}/{alias_id}/subscriptions result = await make_onesignal_request( f"apps/{app_config.app_id}/users/by/{alias_label}/{user_id}/subscriptions", method="POST", data=data ) return result @mcp.tool() async def update_subscription(user_id: str, subscription_id: str, enabled: bool = None) -> Dict[str, Any]: """Update a user's subscription. Args: user_id: The user identifier (onesignal_id or external_id) - used for context subscription_id: The ID of the subscription to update enabled: Whether the subscription should be enabled or disabled (optional) """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} data = {} if enabled is not None: data["enabled"] = enabled # Set notification_types based on enabled status data["notification_types"] = 1 if enabled else -31 if not data: return {"error": "No update parameters provided. Specify enabled status."} # Subscription updates use: apps/{app_id}/subscriptions/{subscription_id} result = await make_onesignal_request( f"apps/{app_config.app_id}/subscriptions/{subscription_id}", method="PATCH", data=data ) return result @mcp.tool() async def delete_subscription(user_id: str, subscription_id: str) -> Dict[str, Any]: """Delete a user's subscription. Args: user_id: The user identifier (onesignal_id or external_id) - used for context subscription_id: The ID of the subscription to delete """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} # Subscription deletes use: apps/{app_id}/subscriptions/{subscription_id} result = await make_onesignal_request( f"apps/{app_config.app_id}/subscriptions/{subscription_id}", method="DELETE" ) return result @mcp.tool() async def transfer_subscription(user_id: str, subscription_id: str, new_user_id: str) -> Dict[str, Any]: """Transfer a subscription from one user to another. Args: user_id: The current user identifier (for context) subscription_id: The ID of the subscription to transfer new_user_id: The user identifier (onesignal_id or external_id) to transfer the subscription to """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} # Determine the new user's identity for the transfer import re uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' # Build the identity object for the new user if re.match(uuid_pattern, new_user_id.lower()): identity = {"onesignal_id": new_user_id} else: identity = {"external_id": new_user_id} data = {"identity": identity} # Transfer endpoint: apps/{app_id}/subscriptions/{subscription_id}/owner result = await make_onesignal_request( f"apps/{app_config.app_id}/subscriptions/{subscription_id}/owner", method="PATCH", data=data ) return result @mcp.tool() async def unsubscribe_email(token: str) -> Dict[str, Any]: """Unsubscribe an email subscription using an unsubscribe token. Args: token: The unsubscribe token from the email """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} data = { "token": token } result = await make_onesignal_request("email/unsubscribe", method="POST", data=data) return result # === NEW: Email & SMS Messaging Tools === @mcp.tool() async def send_email(subject: str, body: str, email_body: str = None, include_emails: List[str] = None, segments: List[str] = None, external_ids: List[str] = None, template_id: str = None) -> Dict[str, Any]: """Send an email through OneSignal. Args: subject: Email subject line body: Plain text email content email_body: HTML email content (optional) include_emails: List of specific email addresses to target segments: List of segments to include external_ids: List of external user IDs to target template_id: Email template ID to use """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} email_data = { "app_id": app_config.app_id, "email_subject": subject, "email_body": email_body or body, "target_channel": "email" } # Set targeting if include_emails: email_data["include_emails"] = include_emails elif external_ids: email_data["include_external_user_ids"] = external_ids elif segments: email_data["included_segments"] = segments else: email_data["included_segments"] = ["Subscribed Users"] if template_id: email_data["template_id"] = template_id result = await make_onesignal_request("notifications", method="POST", data=email_data) return result @mcp.tool() async def send_sms(message: str, phone_numbers: List[str] = None, segments: List[str] = None, external_ids: List[str] = None, media_url: str = None) -> Dict[str, Any]: """Send an SMS/MMS through OneSignal. Args: message: SMS message content phone_numbers: List of phone numbers in E.164 format segments: List of segments to include external_ids: List of external user IDs to target media_url: URL for MMS media attachment """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} sms_data = { "app_id": app_config.app_id, "contents": {"en": message}, "target_channel": "sms" } if phone_numbers: sms_data["include_phone_numbers"] = phone_numbers elif external_ids: sms_data["include_external_user_ids"] = external_ids elif segments: sms_data["included_segments"] = segments else: return {"error": "SMS requires phone_numbers, external_ids, or segments"} if media_url: sms_data["mms_media_url"] = media_url result = await make_onesignal_request("notifications", method="POST", data=sms_data) return result @mcp.tool() async def send_transactional_message(channel: str, content: Dict[str, str], recipients: Dict[str, Any], template_id: str = None, custom_data: Dict[str, Any] = None) -> Dict[str, Any]: """Send a transactional message (immediate delivery, no scheduling). Args: channel: Channel to send on ("push", "email", "sms") content: Message content (format depends on channel) recipients: Targeting information (include_external_user_ids, include_emails, etc.) template_id: Template ID to use custom_data: Custom data to include """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} message_data = { "app_id": app_config.app_id, "target_channel": channel, "is_transactional": True } # Set content based on channel if channel == "email": message_data["email_subject"] = content.get("subject", "") message_data["email_body"] = content.get("body", "") else: message_data["contents"] = content # Set recipients message_data.update(recipients) if template_id: message_data["template_id"] = template_id if custom_data: message_data["data"] = custom_data result = await make_onesignal_request("notifications", method="POST", data=message_data) return result # === NEW: Enhanced Template Management === @mcp.tool() async def update_template(template_id: str, name: str = None, title: str = None, message: str = None) -> Dict[str, Any]: """Update an existing template. Args: template_id: ID of the template to update name: New name for the template title: New title/heading for the template message: New content/message for the template """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} data = {} if name: data["name"] = name if title: data["headings"] = {"en": title} if message: data["contents"] = {"en": message} if not data: return {"error": "No update parameters provided"} params = {"app_id": app_config.app_id} result = await make_onesignal_request(f"templates/{template_id}", method="PATCH", data=data, params=params) return result @mcp.tool() async def delete_template(template_id: str) -> Dict[str, Any]: """Delete a template from your OneSignal app. Args: template_id: ID of the template to delete """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} params = {"app_id": app_config.app_id} result = await make_onesignal_request(f"templates/{template_id}", method="DELETE", params=params) if "error" not in result: return {"success": f"Template '{template_id}' deleted successfully"} return result @mcp.tool() async def copy_template_to_app(template_id: str, target_app_id: str, new_name: str = None) -> Dict[str, Any]: """Copy a template to another OneSignal app. Args: template_id: ID of the template to copy target_app_id: ID of the app to copy the template to new_name: Optional new name for the copied template """ data = {"app_id": target_app_id} if new_name: data["name"] = new_name result = await make_onesignal_request(f"templates/{template_id}/copy", method="POST", data=data) return result # === NEW: Live Activities (iOS) === @mcp.tool() async def start_live_activity(activity_id: str, push_token: str, subscription_id: str, activity_attributes: Dict[str, Any], content_state: Dict[str, Any]) -> Dict[str, Any]: """Start a new iOS Live Activity. Args: activity_id: Unique identifier for the activity push_token: Push token for the Live Activity subscription_id: Subscription ID for the user activity_attributes: Static attributes for the activity content_state: Initial dynamic content state """ data = { "activity_id": activity_id, "push_token": push_token, "subscription_id": subscription_id, "activity_attributes": activity_attributes, "content_state": content_state } result = await make_onesignal_request(f"live_activities/{activity_id}/start", method="POST", data=data) return result @mcp.tool() async def update_live_activity(activity_id: str, name: str, event: str, content_state: Dict[str, Any], dismissal_date: int = None, priority: int = None, sound: str = None) -> Dict[str, Any]: """Update an existing iOS Live Activity. Args: activity_id: ID of the activity to update name: Name identifier for the update event: Event type ("update" or "end") content_state: Updated dynamic content state dismissal_date: Unix timestamp for automatic dismissal priority: Notification priority (5-10) sound: Sound file name for the update """ data = { "name": name, "event": event, "content_state": content_state } if dismissal_date: data["dismissal_date"] = dismissal_date if priority: data["priority"] = priority if sound: data["sound"] = sound result = await make_onesignal_request(f"live_activities/{activity_id}/update", method="POST", data=data) return result @mcp.tool() async def end_live_activity(activity_id: str, subscription_id: str, dismissal_date: int = None, priority: int = None) -> Dict[str, Any]: """End an iOS Live Activity. Args: activity_id: ID of the activity to end subscription_id: Subscription ID associated with the activity dismissal_date: Unix timestamp for dismissal priority: Notification priority (5-10) """ data = { "subscription_id": subscription_id, "event": "end" } if dismissal_date: data["dismissal_date"] = dismissal_date if priority: data["priority"] = priority result = await make_onesignal_request(f"live_activities/{activity_id}/end", method="POST", data=data) return result # === NEW: Analytics & Outcomes === @mcp.tool() async def view_outcomes(outcome_names: List[str], outcome_time_range: str = None, outcome_platforms: List[str] = None, outcome_attribution: str = None) -> Dict[str, Any]: """View outcomes data for your OneSignal app. Args: outcome_names: List of outcome names to fetch data for outcome_time_range: Time range for data (e.g., "1d", "1mo") outcome_platforms: Filter by platforms (e.g., ["ios", "android"]) outcome_attribution: Attribution model ("direct" or "influenced") """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} params = {"outcome_names": outcome_names} if outcome_time_range: params["outcome_time_range"] = outcome_time_range if outcome_platforms: params["outcome_platforms"] = outcome_platforms if outcome_attribution: params["outcome_attribution"] = outcome_attribution result = await make_onesignal_request(f"apps/{app_config.app_id}/outcomes", method="GET", params=params) return result # === NEW: Export Functions === @mcp.tool() async def export_messages_csv(start_date: str = None, end_date: str = None, event_types: List[str] = None) -> Dict[str, Any]: """Export messages/notifications data to CSV. Args: start_date: Start date for export (ISO 8601 format) end_date: End date for export (ISO 8601 format) event_types: List of event types to export """ data = {} if start_date: data["start_date"] = start_date if end_date: data["end_date"] = end_date if event_types: data["event_types"] = event_types result = await make_onesignal_request("notifications/csv_export", method="POST", data=data, use_org_key=True) return result # === NEW: API Key Management === @mcp.tool() async def delete_app_api_key(app_id: str, key_id: str) -> Dict[str, Any]: """Delete an API key from a specific OneSignal app. Args: app_id: The ID of the app key_id: The ID of the API key to delete """ result = await make_onesignal_request(f"apps/{app_id}/auth/tokens/{key_id}", method="DELETE", use_org_key=True) if "error" not in result: return {"success": f"API key '{key_id}' deleted successfully"} return result @mcp.tool() async def update_app_api_key(app_id: str, key_id: str, name: str = None, scopes: List[str] = None) -> Dict[str, Any]: """Update an API key for a specific OneSignal app. Args: app_id: The ID of the app key_id: The ID of the API key to update name: New name for the API key scopes: New list of permission scopes """ data = {} if name: data["name"] = name if scopes: data["scopes"] = scopes if not data: return {"error": "No update parameters provided"} result = await make_onesignal_request(f"apps/{app_id}/auth/tokens/{key_id}", method="PATCH", data=data, use_org_key=True) return result @mcp.tool() async def rotate_app_api_key(app_id: str, key_id: str) -> Dict[str, Any]: """Rotate an API key (generate new token while keeping permissions). Args: app_id: The ID of the app key_id: The ID of the API key to rotate """ result = await make_onesignal_request(f"apps/{app_id}/auth/tokens/{key_id}/rotate", method="POST", use_org_key=True) if "error" not in result: return { "success": f"API key rotated successfully", "new_token": result.get("token"), "warning": "Save the new token now! You won't be able to see it again." } return result # === Custom Events API === @mcp.tool() async def create_custom_events(events: List[Dict[str, Any]]) -> Dict[str, Any]: """Create custom events to track user actions in OneSignal. Custom events can represent any action users take in your application, such as completing a purchase, viewing content, or achieving milestones. Used for Journey automation and user behavior tracking. Args: events: List of event objects. Each event should have: - name (str): Event name (e.g., "purchase", "signup") - external_id (str): User's external ID (recommended) - onesignal_id (str): User's OneSignal ID (alternative to external_id) - timestamp (str): ISO 8601 timestamp (optional, defaults to now) - properties (dict): Additional event properties (optional) Example: events = [ {"name": "purchase", "external_id": "user123", "properties": {"amount": 99.99}}, {"name": "level_complete", "external_id": "user456", "properties": {"level": 5}} ] """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} data = {"events": events} result = await make_onesignal_request( f"apps/{app_config.app_id}/custom_events", method="POST", data=data ) if "error" not in result: return {"success": f"Successfully created {len(events)} custom event(s)"} return result # === Export Audience Activity API === @mcp.tool() async def export_audience_activity(message_id: str) -> Dict[str, Any]: """Export a CSV report of audience-level delivery and engagement data for a message. This includes sent, delivered, clicked, failed, and unsubscribed events across Push, Email, and SMS channels. Args: message_id: The ID of the message to export activity for Returns: A dict with csv_file_url to download the report (valid for 3 days). Note: Large exports may take several minutes. Poll the URL until available. """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} params = {"app_id": app_config.app_id} result = await make_onesignal_request( f"notifications/{message_id}/export_events", method="POST", params=params ) if "error" not in result and "csv_file_url" in result: return { "success": True, "csv_file_url": result["csv_file_url"], "note": "File may take a few minutes to generate. Poll the URL until available. Valid for 3 days." } return result # === Enhanced Push Notification with Advanced Options === @mcp.tool() async def send_advanced_push_notification( title: str, message: str, # Targeting options (use one) segments: List[str] = None, external_ids: List[str] = None, filters: List[Dict[str, Any]] = None, # Scheduling send_after: str = None, delayed_option: str = None, delivery_time_of_day: str = None, # Advanced options data: Dict[str, Any] = None, url: str = None, web_url: str = None, app_url: str = None, # Images big_picture: str = None, ios_attachments: Dict[str, str] = None, chrome_web_image: str = None, # Buttons buttons: List[Dict[str, str]] = None, web_buttons: List[Dict[str, str]] = None, # Delivery options ttl: int = None, priority: int = None, throttle_rate_per_minute: int = None, # iOS specific ios_badge_type: str = None, ios_badge_count: int = None, ios_sound: str = None, # Android specific android_channel_id: str = None, android_accent_color: str = None, android_led_color: str = None, # Idempotency idempotency_key: str = None, # Name for tracking name: str = None ) -> Dict[str, Any]: """Send a push notification with advanced options. Args: title: Notification title message: Notification message content Targeting (use one): - segments: List of segments (e.g., ["Subscribed Users"]) - external_ids: List of external user IDs - filters: List of filter objects for dynamic targeting Example: [{"field": "tag", "key": "level", "relation": ">", "value": "10"}] Scheduling: - send_after: ISO 8601 datetime or Unix timestamp for scheduled delivery - delayed_option: "timezone" for per-user timezone delivery, "last-active" for user activity - delivery_time_of_day: Time of day for timezone delivery (e.g., "9:00AM") Content: - data: Custom data payload (JSON) - url: URL to open on click - web_url: URL for web push only - app_url: Deep link URL for mobile Images: - big_picture: URL for large image (Android/Web) - ios_attachments: Dict of attachment ID to URL for iOS - chrome_web_image: URL for Chrome web push image Buttons: - buttons: Mobile action buttons [{"id": "btn1", "text": "Click Me", "icon": "icon.png"}] - web_buttons: Web action buttons [{"id": "btn1", "text": "Click", "url": "https://..."}] Delivery: - ttl: Time to live in seconds (default: 3 days) - priority: 1-10, with 10 being high priority - throttle_rate_per_minute: Messages per minute (Pro+ plans) iOS: - ios_badge_type: "Increase", "SetTo", or "None" - ios_badge_count: Badge number - ios_sound: Sound file name Android: - android_channel_id: Notification channel ID - android_accent_color: ARGB hex color (e.g., "FF00FF00") - android_led_color: LED color ARGB hex Other: - idempotency_key: Unique key to prevent duplicate sends (valid 24h) - name: Internal name for tracking in dashboard """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} notification_data = { "app_id": app_config.app_id, "contents": {"en": message}, "headings": {"en": title}, "target_channel": "push" } # Targeting if filters: notification_data["filters"] = filters elif external_ids: notification_data["include_aliases"] = {"external_id": external_ids} notification_data["target_channel"] = "push" elif segments: notification_data["included_segments"] = segments else: notification_data["included_segments"] = ["Subscribed Users"] # Scheduling if send_after: notification_data["send_after"] = send_after if delayed_option: notification_data["delayed_option"] = delayed_option if delivery_time_of_day: notification_data["delivery_time_of_day"] = delivery_time_of_day # Content if data: notification_data["data"] = data if url: notification_data["url"] = url if web_url: notification_data["web_url"] = web_url if app_url: notification_data["app_url"] = app_url # Images if big_picture: notification_data["big_picture"] = big_picture if ios_attachments: notification_data["ios_attachments"] = ios_attachments if chrome_web_image: notification_data["chrome_web_image"] = chrome_web_image # Buttons if buttons: notification_data["buttons"] = buttons if web_buttons: notification_data["web_buttons"] = web_buttons # Delivery options if ttl is not None: notification_data["ttl"] = ttl if priority is not None: notification_data["priority"] = priority if throttle_rate_per_minute is not None: notification_data["throttle_rate_per_minute"] = throttle_rate_per_minute # iOS specific if ios_badge_type: notification_data["ios_badgeType"] = ios_badge_type if ios_badge_count is not None: notification_data["ios_badgeCount"] = ios_badge_count if ios_sound: notification_data["ios_sound"] = ios_sound # Android specific if android_channel_id: notification_data["android_channel_id"] = android_channel_id if android_accent_color: notification_data["android_accent_color"] = android_accent_color if android_led_color: notification_data["android_led_color"] = android_led_color # Name if name: notification_data["name"] = name # Headers for idempotency headers = {} if idempotency_key: notification_data["idempotency_key"] = idempotency_key result = await make_onesignal_request("notifications", method="POST", data=notification_data) return result # === Enhanced Email with Advanced Options === @mcp.tool() async def send_advanced_email( subject: str, body: str, # Targeting (use one) include_emails: List[str] = None, external_ids: List[str] = None, segments: List[str] = None, filters: List[Dict[str, Any]] = None, # Content email_body: str = None, email_preheader: str = None, template_id: str = None, # Sender info email_from_name: str = None, email_from_address: str = None, email_reply_to_address: str = None, # Scheduling send_after: str = None, # Tracking disable_email_click_tracking: bool = None, # Other name: str = None, idempotency_key: str = None ) -> Dict[str, Any]: """Send an email with advanced options. Args: subject: Email subject line body: Plain text email content (fallback) Targeting (use one): - include_emails: List of specific email addresses - external_ids: List of external user IDs - segments: List of segments - filters: Dynamic targeting filters Content: - email_body: HTML email body (overrides body) - email_preheader: Preview text shown in inbox - template_id: Use a predefined email template Sender: - email_from_name: Sender display name - email_from_address: Sender email address - email_reply_to_address: Reply-to address Scheduling: - send_after: ISO 8601 datetime for scheduled send Tracking: - disable_email_click_tracking: Disable click tracking Other: - name: Internal name for tracking - idempotency_key: Prevent duplicate sends """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} email_data = { "app_id": app_config.app_id, "email_subject": subject, "email_body": email_body or f"<html><body>{body}</body></html>", "target_channel": "email" } # Targeting if filters: email_data["filters"] = filters elif include_emails: email_data["include_email_tokens"] = include_emails elif external_ids: email_data["include_aliases"] = {"external_id": external_ids} elif segments: email_data["included_segments"] = segments else: email_data["included_segments"] = ["Subscribed Users"] # Content if email_preheader: email_data["email_preheader"] = email_preheader if template_id: email_data["template_id"] = template_id # Sender info if email_from_name: email_data["email_from_name"] = email_from_name if email_from_address: email_data["email_from_address"] = email_from_address if email_reply_to_address: email_data["email_reply_to_address"] = email_reply_to_address # Scheduling if send_after: email_data["send_after"] = send_after # Tracking if disable_email_click_tracking is not None: email_data["disable_email_click_tracking"] = disable_email_click_tracking # Other if name: email_data["name"] = name if idempotency_key: email_data["idempotency_key"] = idempotency_key result = await make_onesignal_request("notifications", method="POST", data=email_data) return result # === Enhanced SMS with Advanced Options === @mcp.tool() async def send_advanced_sms( message: str, # Targeting (use one) phone_numbers: List[str] = None, external_ids: List[str] = None, segments: List[str] = None, filters: List[Dict[str, Any]] = None, # MMS media_url: str = None, # Scheduling send_after: str = None, # Sender sms_from: str = None, # Other name: str = None, idempotency_key: str = None ) -> Dict[str, Any]: """Send an SMS/MMS with advanced options. Args: message: SMS message content Targeting (use one): - phone_numbers: List of phone numbers in E.164 format - external_ids: List of external user IDs - segments: List of segments - filters: Dynamic targeting filters MMS: - media_url: URL for MMS media attachment (image/video) Scheduling: - send_after: ISO 8601 datetime for scheduled send Sender: - sms_from: Sender phone number (must be configured in OneSignal) Other: - name: Internal name for tracking - idempotency_key: Prevent duplicate sends """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} sms_data = { "app_id": app_config.app_id, "contents": {"en": message}, "target_channel": "sms" } # Targeting if filters: sms_data["filters"] = filters elif phone_numbers: sms_data["include_phone_numbers"] = phone_numbers elif external_ids: sms_data["include_aliases"] = {"external_id": external_ids} elif segments: sms_data["included_segments"] = segments else: return {"error": "Must specify phone_numbers, external_ids, segments, or filters"} # MMS if media_url: sms_data["sms_media_urls"] = [media_url] # Scheduling if send_after: sms_data["send_after"] = send_after # Sender if sms_from: sms_data["sms_from"] = sms_from # Other if name: sms_data["name"] = name if idempotency_key: sms_data["idempotency_key"] = idempotency_key result = await make_onesignal_request("notifications", method="POST", data=sms_data) return result # === Start Live Activity (Enhanced) === @mcp.tool() async def start_live_activity_advanced( activity_type: str, activity_id: str, event_attributes: Dict[str, Any], event_updates: Dict[str, Any], name: str, title: str, message: str, # Targeting (use one) external_ids: List[str] = None, subscription_ids: List[str] = None, segments: List[str] = None, filters: List[Dict[str, Any]] = None, # Options stale_date: int = None, priority: int = None, ios_relevance_score: float = None, idempotency_key: str = None ) -> Dict[str, Any]: """Start a Live Activity on iOS devices with full control. Args: activity_type: The Live Activity type defined in your app (e.g., "DeliveryAttributes") activity_id: Unique ID to track this Live Activity (use UUID) event_attributes: Static data to initialize the Live Activity event_updates: Dynamic content for the Live Activity (ContentState) name: Internal name for tracking title: Push notification title message: Push notification message Targeting (use one): - external_ids: List of user external IDs - subscription_ids: List of subscription IDs - segments: List of segments - filters: Dynamic targeting filters Options: - stale_date: Unix timestamp when activity becomes stale - priority: 5 (normal) or 10 (high) - ios_relevance_score: 0-1, higher shows in Dynamic Island - idempotency_key: Prevent duplicate sends """ app_config = get_current_app() if not app_config: return {"error": "No app currently selected. Use switch_app to select an app."} data = { "event": "start", "activity_id": activity_id, "event_attributes": event_attributes, "event_updates": event_updates, "name": name, "contents": {"en": message}, "headings": {"en": title} } # Targeting if external_ids: data["include_aliases"] = {"external_id": external_ids} elif subscription_ids: data["include_subscription_ids"] = subscription_ids elif segments: data["included_segments"] = segments elif filters: data["filters"] = filters else: return {"error": "Must specify targeting: external_ids, subscription_ids, segments, or filters"} # Options if stale_date is not None: data["stale_date"] = stale_date if priority is not None: data["priority"] = priority if ios_relevance_score is not None: data["ios_relevance_score"] = ios_relevance_score if idempotency_key: data["idempotency_key"] = idempotency_key result = await make_onesignal_request( f"apps/{app_config.app_id}/activities/activity/{activity_type}", method="POST", data=data ) return result # Run the server if __name__ == "__main__": # Run the server mcp.run()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chansearrington/OneSignal-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server