#
tokens: 38911/50000 30/31 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 2. Use http://codebase.md/weirdbrains/onesignal-mcp?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .env.example
├── .github
│   └── workflows
│       └── ci.yml
├── .gitignore
├── check_loaded_key.py
├── CONTRIBUTING.md
├── debug_api_key.py
├── examples
│   ├── send_invite_email.py
│   └── send_notification.py
├── implementation_examples.md
├── LICENSE
├── missing_endpoints_analysis.md
├── onesignal_refactored
│   ├── __init__.py
│   ├── api_client.py
│   ├── config.py
│   ├── server.py
│   └── tools
│       ├── __init__.py
│       ├── analytics.py
│       ├── live_activities.py
│       ├── messages.py
│       └── templates.py
├── onesignal_refactoring_summary.md
├── onesignal_server.py
├── onesignal_tools_list.md
├── README.md
├── requirements.txt
├── setup.py
├── test_api_key_validity.py
├── test_auth_fix.py
├── test_onesignal_mcp.py
├── test_segments_debug.py
└── tests
    ├── __init__.py
    └── test_onesignal_server.py
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Environment variables
 2 | .env
 3 | 
 4 | # Python
 5 | __pycache__/
 6 | *.py[cod]
 7 | *$py.class
 8 | *.so
 9 | .Python
10 | env/
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | *.egg-info/
23 | .installed.cfg
24 | *.egg
25 | 
26 | # Virtual Environment
27 | venv/
28 | ENV/
29 | 
30 | # IDE files
31 | .idea/
32 | .vscode/
33 | *.swp
34 | *.swo
35 | 
```

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
 1 | # App-specific credentials
 2 | # Mandible app
 3 | ONESIGNAL_MANDIBLE_APP_ID=your_mandible_app_id_here
 4 | ONESIGNAL_MANDIBLE_API_KEY=your_mandible_api_key_here
 5 | 
 6 | # Weird Brains app
 7 | ONESIGNAL_WEIRDBRAINS_APP_ID=your_weirdbrains_app_id_here
 8 | ONESIGNAL_WEIRDBRAINS_API_KEY=your_weirdbrains_api_key_here
 9 | 
10 | # Default app credentials (will be used if app-specific credentials not found)
11 | ONESIGNAL_APP_ID=your_default_app_id_here
12 | ONESIGNAL_API_KEY=your_default_api_key_here
13 | 
14 | # Organization API key (used for organization-level operations)
15 | ONESIGNAL_ORG_API_KEY=your_organization_api_key_here
16 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # OneSignal MCP Server
  2 | 
  3 | A comprehensive Model Context Protocol (MCP) server for interacting with the OneSignal API. This server provides a complete interface for managing push notifications, emails, SMS, users, devices, segments, templates, analytics, and more through OneSignal's REST API.
  4 | 
  5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
  6 | [![Version](https://img.shields.io/badge/version-2.0.0-blue.svg)](https://github.com/weirdbrains/onesignal-mcp)
  7 | [![Tools](https://img.shields.io/badge/tools-57-green.svg)](https://github.com/weirdbrains/onesignal-mcp)
  8 | 
  9 | ## Overview
 10 | 
 11 | This MCP server provides comprehensive access to the [OneSignal REST API](https://documentation.onesignal.com/reference/rest-api-overview), offering **57 tools** that cover all major OneSignal operations:
 12 | 
 13 | ### 🚀 Key Features
 14 | 
 15 | - **Multi-channel Messaging**: Send push notifications, emails, SMS, and transactional messages
 16 | - **User & Device Management**: Complete CRUD operations for users, devices, and subscriptions
 17 | - **Advanced Segmentation**: Create and manage user segments with complex filters
 18 | - **Template System**: Create, update, and manage message templates
 19 | - **iOS Live Activities**: Full support for iOS Live Activities
 20 | - **Analytics & Export**: View outcomes data and export to CSV
 21 | - **Multi-App Support**: Manage multiple OneSignal applications seamlessly
 22 | - **API Key Management**: Create, update, rotate, and delete API keys
 23 | - **Organization-level Operations**: Manage apps across your entire organization
 24 | 
 25 | ## Requirements
 26 | 
 27 | - Python 3.7 or higher
 28 | - `python-dotenv` package
 29 | - `requests` package
 30 | - `mcp` package
 31 | - OneSignal account with API credentials
 32 | 
 33 | ## Installation
 34 | 
 35 | ### Option 1: Clone from GitHub
 36 | 
 37 | ```bash
 38 | # Clone the repository
 39 | git clone https://github.com/weirdbrains/onesignal-mcp.git
 40 | cd onesignal-mcp
 41 | 
 42 | # Install dependencies
 43 | pip install -r requirements.txt
 44 | ```
 45 | 
 46 | ### Option 2: Install as a Package (Coming Soon)
 47 | 
 48 | ```bash
 49 | pip install onesignal-mcp
 50 | ```
 51 | 
 52 | ## Configuration
 53 | 
 54 | 1. Create a `.env` file in the root directory with your OneSignal credentials:
 55 |    ```
 56 |    # Default app credentials (optional, you can also add apps via the API)
 57 |    ONESIGNAL_APP_ID=your_app_id_here
 58 |    ONESIGNAL_API_KEY=your_rest_api_key_here
 59 |    
 60 |    # Organization API key (for org-level operations)
 61 |    ONESIGNAL_ORG_API_KEY=your_organization_api_key_here
 62 |    
 63 |    # Optional: Multiple app configurations
 64 |    ONESIGNAL_MANDIBLE_APP_ID=mandible_app_id
 65 |    ONESIGNAL_MANDIBLE_API_KEY=mandible_api_key
 66 |    
 67 |    ONESIGNAL_WEIRDBRAINS_APP_ID=weirdbrains_app_id
 68 |    ONESIGNAL_WEIRDBRAINS_API_KEY=weirdbrains_api_key
 69 |    
 70 |    # Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
 71 |    LOG_LEVEL=INFO
 72 |    ```
 73 | 
 74 | 2. Find your OneSignal credentials:
 75 |    - **App ID**: Settings > Keys & IDs > OneSignal App ID
 76 |    - **REST API Key**: Settings > Keys & IDs > REST API Key
 77 |    - **Organization API Key**: Organization Settings > API Keys
 78 | 
 79 | ## Usage
 80 | 
 81 | ### Running the Server
 82 | 
 83 | ```bash
 84 | python onesignal_server.py
 85 | ```
 86 | 
 87 | The server will start and register itself with the MCP system, making all 57 tools available for use.
 88 | 
 89 | ## Complete Tools Reference (57 Tools)
 90 | 
 91 | ### 📱 App Management (5 tools)
 92 | - `list_apps` - List all configured OneSignal apps
 93 | - `add_app` - Add a new OneSignal app configuration locally
 94 | - `update_local_app_config` - Update an existing local app configuration
 95 | - `remove_app` - Remove a local OneSignal app configuration
 96 | - `switch_app` - Switch the current app to use for API requests
 97 | 
 98 | ### 📨 Messaging (8 tools)
 99 | - `send_push_notification` - Send a push notification
100 | - `send_email` - Send an email through OneSignal
101 | - `send_sms` - Send an SMS/MMS through OneSignal
102 | - `send_transactional_message` - Send immediate delivery messages
103 | - `view_messages` - View recent messages sent
104 | - `view_message_details` - Get detailed information about a message
105 | - `view_message_history` - View message history/recipients
106 | - `cancel_message` - Cancel a scheduled message
107 | 
108 | ### 📱 Devices/Players (6 tools)
109 | - `view_devices` - View devices subscribed to your app
110 | - `view_device_details` - Get detailed information about a device
111 | - `add_player` - Add a new player/device
112 | - `edit_player` - Edit an existing player/device
113 | - `delete_player` - Delete a player/device record
114 | - `edit_tags_with_external_user_id` - Bulk edit tags by external ID
115 | 
116 | ### 🎯 Segments (3 tools)
117 | - `view_segments` - List all segments
118 | - `create_segment` - Create a new segment
119 | - `delete_segment` - Delete a segment
120 | 
121 | ### 📄 Templates (6 tools)
122 | - `view_templates` - List all templates
123 | - `view_template_details` - Get template details
124 | - `create_template` - Create a new template
125 | - `update_template` - Update an existing template
126 | - `delete_template` - Delete a template
127 | - `copy_template_to_app` - Copy template to another app
128 | 
129 | ### 🏢 Apps (6 tools)
130 | - `view_app_details` - Get details about configured app
131 | - `view_apps` - List all organization apps
132 | - `create_app` - Create a new OneSignal application
133 | - `update_app` - Update an existing application
134 | - `view_app_api_keys` - View API keys for an app
135 | - `create_app_api_key` - Create a new API key
136 | 
137 | ### 🔑 API Key Management (3 tools)
138 | - `delete_app_api_key` - Delete an API key
139 | - `update_app_api_key` - Update an API key
140 | - `rotate_app_api_key` - Rotate an API key
141 | 
142 | ### 👤 Users (6 tools)
143 | - `create_user` - Create a new user
144 | - `view_user` - View user details
145 | - `update_user` - Update user information
146 | - `delete_user` - Delete a user
147 | - `view_user_identity` - Get user identity information
148 | - `view_user_identity_by_subscription` - Get identity by subscription
149 | 
150 | ### 🏷️ Aliases (3 tools)
151 | - `create_or_update_alias` - Create or update user alias
152 | - `delete_alias` - Delete a user alias
153 | - `create_alias_by_subscription` - Create alias by subscription ID
154 | 
155 | ### 📬 Subscriptions (5 tools)
156 | - `create_subscription` - Create a new subscription
157 | - `update_subscription` - Update a subscription
158 | - `delete_subscription` - Delete a subscription
159 | - `transfer_subscription` - Transfer subscription between users
160 | - `unsubscribe_email` - Unsubscribe using email token
161 | 
162 | ### 🎯 Live Activities (3 tools)
163 | - `start_live_activity` - Start iOS Live Activity
164 | - `update_live_activity` - Update iOS Live Activity
165 | - `end_live_activity` - End iOS Live Activity
166 | 
167 | ### 📊 Analytics & Export (3 tools)
168 | - `view_outcomes` - View outcomes/conversion data
169 | - `export_players_csv` - Export player data to CSV
170 | - `export_messages_csv` - Export messages to CSV
171 | 
172 | ## Usage Examples
173 | 
174 | ### Multi-Channel Messaging
175 | 
176 | ```python
177 | # Send a push notification
178 | await send_push_notification(
179 |     title="Hello World",
180 |     message="This is a test notification",
181 |     segments=["Subscribed Users"]
182 | )
183 | 
184 | # Send an email
185 | await send_email(
186 |     subject="Welcome!",
187 |     body="Thank you for joining us",
188 |     email_body="<html><body><h1>Welcome!</h1></body></html>",
189 |     include_emails=["[email protected]"]
190 | )
191 | 
192 | # Send an SMS
193 | await send_sms(
194 |     message="Your verification code is 12345",
195 |     phone_numbers=["+15551234567"]
196 | )
197 | 
198 | # Send a transactional message
199 | await send_transactional_message(
200 |     channel="email",
201 |     content={"subject": "Order Confirmation", "body": "Your order has been confirmed"},
202 |     recipients={"include_external_user_ids": ["user123"]}
203 | )
204 | ```
205 | 
206 | ### User and Device Management
207 | 
208 | ```python
209 | # Create a user
210 | user = await create_user(
211 |     name="John Doe",
212 |     email="[email protected]",
213 |     external_id="user123",
214 |     tags={"plan": "premium", "joined": "2024-01-01"}
215 | )
216 | 
217 | # Add a device
218 | device = await add_player(
219 |     device_type=1,  # Android
220 |     identifier="device_token_here",
221 |     language="en",
222 |     tags={"app_version": "1.0.0"}
223 | )
224 | 
225 | # Update user tags across all devices
226 | await edit_tags_with_external_user_id(
227 |     external_user_id="user123",
228 |     tags={"last_active": "2024-01-15", "purchases": "5"}
229 | )
230 | ```
231 | 
232 | ### iOS Live Activities
233 | 
234 | ```python
235 | # Start a Live Activity
236 | await start_live_activity(
237 |     activity_id="delivery_123",
238 |     push_token="live_activity_push_token",
239 |     subscription_id="user_subscription_id",
240 |     activity_attributes={"order_number": "12345"},
241 |     content_state={"status": "preparing", "eta": "15 mins"}
242 | )
243 | 
244 | # Update the Live Activity
245 | await update_live_activity(
246 |     activity_id="delivery_123",
247 |     name="delivery_update",
248 |     event="update",
249 |     content_state={"status": "on_the_way", "eta": "5 mins"}
250 | )
251 | ```
252 | 
253 | ### Analytics and Export
254 | 
255 | ```python
256 | # View conversion outcomes
257 | outcomes = await view_outcomes(
258 |     outcome_names=["purchase", "session_duration"],
259 |     outcome_time_range="7d",
260 |     outcome_platforms=["ios", "android"]
261 | )
262 | 
263 | # Export player data
264 | export = await export_players_csv(
265 |     start_date="2024-01-01T00:00:00Z",
266 |     end_date="2024-01-31T23:59:59Z",
267 |     segment_names=["Active Users"]
268 | )
269 | ```
270 | 
271 | ## Testing
272 | 
273 | The server includes a comprehensive test suite. To run tests:
274 | 
275 | ```bash
276 | # Run the test script
277 | python test_onesignal_mcp.py
278 | 
279 | # Or use unittest
280 | python -m unittest discover tests
281 | ```
282 | 
283 | ## Error Handling
284 | 
285 | The server provides consistent error handling:
286 | - All errors are returned in a standardized format
287 | - Detailed error messages help identify issues
288 | - Automatic retry logic for transient failures
289 | - Proper authentication error messages
290 | 
291 | ## Rate Limiting
292 | 
293 | OneSignal enforces rate limits on API requests:
294 | - Standard limit: 10 requests per second
295 | - Bulk operations: May have lower limits
296 | - The server includes guidance on handling rate limits
297 | 
298 | ## Contributing
299 | 
300 | We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
301 | 
302 | ## License
303 | 
304 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
305 | 
306 | ## Acknowledgements
307 | 
308 | - [OneSignal](https://onesignal.com/) for their excellent notification service
309 | - The MCP community for the Model Context Protocol
310 | - All contributors to this project
311 | 
```

--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Contributing to OneSignal MCP Server
 2 | 
 3 | Thank you for considering contributing to the OneSignal MCP Server! This document provides guidelines and instructions for contributing to this project.
 4 | 
 5 | ## Code of Conduct
 6 | 
 7 | Please be respectful and considerate of others when contributing to this project. We aim to foster an inclusive and welcoming community.
 8 | 
 9 | ## How to Contribute
10 | 
11 | ### Reporting Bugs
12 | 
13 | If you find a bug, please create an issue with the following information:
14 | - A clear, descriptive title
15 | - Steps to reproduce the bug
16 | - Expected behavior
17 | - Actual behavior
18 | - Any relevant logs or error messages
19 | - Your environment (OS, Python version, etc.)
20 | 
21 | ### Suggesting Features
22 | 
23 | If you have an idea for a new feature, please create an issue with:
24 | - A clear, descriptive title
25 | - A detailed description of the feature
26 | - Any relevant examples or use cases
27 | - Why this feature would be beneficial
28 | 
29 | ### Pull Requests
30 | 
31 | 1. Fork the repository
32 | 2. Create a new branch for your changes
33 | 3. Make your changes
34 | 4. Run tests to ensure your changes don't break existing functionality
35 | 5. Submit a pull request
36 | 
37 | ### Development Setup
38 | 
39 | 1. Clone the repository
40 | 2. Install dependencies:
41 |    ```
42 |    pip install -r requirements.txt
43 |    ```
44 | 3. Create a `.env` file with your OneSignal credentials (see `.env.example`)
45 | 
46 | ## Coding Standards
47 | 
48 | - Follow PEP 8 style guidelines
49 | - Write docstrings for all functions, classes, and modules
50 | - Include type hints where appropriate
51 | - Write tests for new functionality
52 | 
53 | ## Testing
54 | 
55 | Before submitting a pull request, please ensure that all tests pass. You can run tests with:
56 | 
57 | ```
58 | # TODO: Add testing instructions once tests are implemented
59 | ```
60 | 
61 | ## Documentation
62 | 
63 | Please update documentation when making changes:
64 | - Update docstrings for modified functions
65 | - Update the README.md if necessary
66 | - Add examples for new functionality
67 | 
68 | ## License
69 | 
70 | By contributing to this project, you agree that your contributions will be licensed under the project's [MIT License](LICENSE).
71 | 
```

--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------

```python
1 | 
2 | 
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
1 | python-dotenv>=1.0.0
2 | requests>=2.31.0
3 | 
```

--------------------------------------------------------------------------------
/onesignal_refactored/tools/__init__.py:
--------------------------------------------------------------------------------

```python
 1 | """OneSignal MCP Server Tools - API endpoint implementations."""
 2 | from . import messages
 3 | from . import templates
 4 | from . import live_activities
 5 | from . import analytics
 6 | 
 7 | __all__ = [
 8 |     "messages",
 9 |     "templates", 
10 |     "live_activities",
11 |     "analytics"
12 | ] 
```

--------------------------------------------------------------------------------
/onesignal_refactored/__init__.py:
--------------------------------------------------------------------------------

```python
 1 | """OneSignal MCP Server - Refactored Implementation."""
 2 | from .config import app_manager, AppConfig
 3 | from .api_client import api_client, OneSignalAPIError
 4 | from .server import mcp, __version__
 5 | 
 6 | __all__ = [
 7 |     "app_manager",
 8 |     "AppConfig", 
 9 |     "api_client",
10 |     "OneSignalAPIError",
11 |     "mcp",
12 |     "__version__"
13 | ] 
```

--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------

```python
 1 | from setuptools import setup, find_packages
 2 | 
 3 | with open("README.md", "r", encoding="utf-8") as fh:
 4 |     long_description = fh.read()
 5 | 
 6 | setup(
 7 |     name="onesignal-mcp",
 8 |     version="1.0.0",
 9 |     author="Weirdbrains",
10 |     author_email="[email protected]",
11 |     description="A Model Context Protocol (MCP) server for interacting with the OneSignal API",
12 |     long_description=long_description,
13 |     long_description_content_type="text/markdown",
14 |     url="https://github.com/weirdbrains/onesignal-mcp",
15 |     packages=find_packages(),
16 |     classifiers=[
17 |         "Programming Language :: Python :: 3",
18 |         "License :: OSI Approved :: MIT License",
19 |         "Operating System :: OS Independent",
20 |     ],
21 |     python_requires=">=3.7",
22 |     install_requires=[
23 |         "python-dotenv>=1.0.0",
24 |         "requests>=2.31.0",
25 |     ],
26 |     include_package_data=True,
27 | )
28 | 
```

--------------------------------------------------------------------------------
/examples/send_notification.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python
 2 | """
 3 | Example script demonstrating how to send a notification using the OneSignal MCP server.
 4 | """
 5 | import asyncio
 6 | import sys
 7 | import os
 8 | 
 9 | # Add the parent directory to the path so we can import the server module
10 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
11 | 
12 | # Import the server module
13 | from onesignal_server import send_notification
14 | 
15 | async def main():
16 |     """Send a test notification to all subscribed users."""
17 |     print("Sending a test notification...")
18 |     
19 |     result = await send_notification(
20 |         title="Hello from OneSignal MCP",
21 |         message="This is a test notification sent from the example script.",
22 |         segment="Subscribed Users",
23 |         data={"custom_key": "custom_value"}
24 |     )
25 |     
26 |     if "error" in result:
27 |         print(f"Error: {result['error']}")
28 |     else:
29 |         print(f"Success! Notification ID: {result.get('id')}")
30 |         print(f"Recipients: {result.get('recipients')}")
31 | 
32 | if __name__ == "__main__":
33 |     asyncio.run(main())
34 | 
```

--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: CI
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [ main ]
 6 |   pull_request:
 7 |     branches: [ main ]
 8 | 
 9 | jobs:
10 |   lint:
11 |     runs-on: ubuntu-latest
12 |     steps:
13 |     - uses: actions/checkout@v3
14 |     - name: Set up Python
15 |       uses: actions/setup-python@v4
16 |       with:
17 |         python-version: '3.9'
18 |     - name: Install dependencies
19 |       run: |
20 |         python -m pip install --upgrade pip
21 |         pip install flake8 black isort
22 |         if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
23 |     - name: Lint with flake8
24 |       run: |
25 |         flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
26 |     - name: Check formatting with black
27 |       run: |
28 |         black --check .
29 |     - name: Check imports with isort
30 |       run: |
31 |         isort --check-only --profile black .
32 | 
33 |   test:
34 |     runs-on: ubuntu-latest
35 |     steps:
36 |     - uses: actions/checkout@v3
37 |     - name: Set up Python
38 |       uses: actions/setup-python@v4
39 |       with:
40 |         python-version: '3.9'
41 |     - name: Install dependencies
42 |       run: |
43 |         python -m pip install --upgrade pip
44 |         pip install pytest pytest-cov
45 |         if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
46 |     - name: Test with pytest
47 |       run: |
48 |         # Uncomment when tests are added
49 |         # pytest --cov=. --cov-report=xml
50 |         echo "Tests will be added in a future update"
51 | 
```

--------------------------------------------------------------------------------
/check_loaded_key.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python3
 2 | """Check what API key is currently loaded from .env"""
 3 | 
 4 | import os
 5 | from dotenv import load_dotenv
 6 | 
 7 | # Load environment variables
 8 | load_dotenv()
 9 | 
10 | # Get the current API key
11 | api_key = os.getenv("ONESIGNAL_MANDIBLE_API_KEY")
12 | 
13 | print("Currently loaded Mandible API key:")
14 | print(f"Length: {len(api_key) if api_key else 'None'}")
15 | print(f"Prefix: {api_key[:20] if api_key else 'None'}...")
16 | print(f"Suffix: ...{api_key[-10:] if api_key else 'None'}")
17 | 
18 | # Also check if we can read the .env file directly
19 | print("\nReading .env file directly:")
20 | try:
21 |     with open('.env', 'r') as f:
22 |         for line in f:
23 |             if 'ONESIGNAL_MANDIBLE_API_KEY' in line and not line.strip().startswith('#'):
24 |                 key_from_file = line.split('=', 1)[1].strip().strip('"').strip("'")
25 |                 print(f"Length: {len(key_from_file)}")
26 |                 print(f"Prefix: {key_from_file[:20]}...")
27 |                 print(f"Suffix: ...{key_from_file[-10:]}")
28 |                 
29 |                 if api_key != key_from_file:
30 |                     print("\n⚠️  WARNING: The loaded key differs from what's in .env!")
31 |                     print("   You need to restart the MCP server to load the new key.")
32 |                 else:
33 |                     print("\n✅ The loaded key matches what's in .env")
34 |                 break
35 | except Exception as e:
36 |     print(f"Error reading .env file: {e}") 
```

--------------------------------------------------------------------------------
/test_segments_debug.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python3
 2 | """Debug script to test the segments endpoint with detailed output"""
 3 | 
 4 | import os
 5 | import requests
 6 | from dotenv import load_dotenv
 7 | 
 8 | # Load environment variables
 9 | load_dotenv()
10 | 
11 | # Get credentials
12 | app_id = os.getenv("ONESIGNAL_MANDIBLE_APP_ID")
13 | api_key = os.getenv("ONESIGNAL_MANDIBLE_API_KEY")
14 | 
15 | print(f"App ID: {app_id}")
16 | print(f"API Key type: {'v2' if api_key.startswith('os_v2_') else 'v1'}")
17 | print(f"API Key prefix: {api_key[:15]}...")
18 | 
19 | # Try different authentication methods
20 | url = f"https://api.onesignal.com/apps/{app_id}/segments"
21 | print(f"\nTesting URL: {url}")
22 | 
23 | # Test 1: Using 'Key' authorization for v2 API key
24 | headers1 = {
25 |     "Authorization": f"Key {api_key}",
26 |     "Accept": "application/json",
27 |     "Content-Type": "application/json"
28 | }
29 | 
30 | print("\n1. Testing with 'Key' authorization header...")
31 | try:
32 |     response = requests.get(url, headers=headers1)
33 |     print(f"Status: {response.status_code}")
34 |     print(f"Response: {response.text[:200]}")
35 | except Exception as e:
36 |     print(f"Error: {e}")
37 | 
38 | # Test 2: Using 'Basic' authorization
39 | headers2 = {
40 |     "Authorization": f"Basic {api_key}",
41 |     "Accept": "application/json",
42 |     "Content-Type": "application/json"
43 | }
44 | 
45 | print("\n2. Testing with 'Basic' authorization header...")
46 | try:
47 |     response = requests.get(url, headers=headers2)
48 |     print(f"Status: {response.status_code}")
49 |     print(f"Response: {response.text[:200]}")
50 | except Exception as e:
51 |     print(f"Error: {e}")
52 | 
53 | # Test 3: Without app_id in URL path (using query param)
54 | url2 = "https://api.onesignal.com/segments"
55 | params = {"app_id": app_id}
56 | 
57 | print(f"\n3. Testing with app_id as query param: {url2}")
58 | print(f"Params: {params}")
59 | try:
60 |     response = requests.get(url2, headers=headers1, params=params)
61 |     print(f"Status: {response.status_code}")
62 |     print(f"Response: {response.text[:200]}")
63 | except Exception as e:
64 |     print(f"Error: {e}") 
```

--------------------------------------------------------------------------------
/examples/send_invite_email.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python
 2 | """
 3 | Example script demonstrating how to send invitation emails using the OneSignal MCP server.
 4 | This showcases the functionality that replaces SendGrid's invitation system.
 5 | """
 6 | import asyncio
 7 | import sys
 8 | import os
 9 | 
10 | # Add the parent directory to the path so we can import the server module
11 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
12 | 
13 | # Import the server module
14 | from onesignal_server import send_invite_email, send_bulk_invites
15 | 
16 | async def send_single_invite():
17 |     """Send a single invitation email."""
18 |     print("Sending a single invitation email...")
19 |     
20 |     result = await send_invite_email(
21 |         email="[email protected]",
22 |         first_name="John",
23 |         invite_url="https://yourapp.com/invite/abc123",
24 |         inviter_name="Jane Smith",
25 |         app_name="Your Amazing App",
26 |         expiry_days=7
27 |     )
28 |     
29 |     if "error" in result:
30 |         print(f"Error: {result['error']}")
31 |     else:
32 |         print(f"Success! Email sent to [email protected]")
33 |         print(f"Details: {result}")
34 | 
35 | async def send_multiple_invites():
36 |     """Send multiple invitation emails at once."""
37 |     print("\nSending multiple invitation emails...")
38 |     
39 |     invites = [
40 |         {
41 |             "email": "[email protected]",
42 |             "first_name": "User",
43 |             "invite_url": "https://yourapp.com/invite/user1",
44 |             "inviter_name": "Team Admin"
45 |         },
46 |         {
47 |             "email": "[email protected]",
48 |             "first_name": "Another",
49 |             "invite_url": "https://yourapp.com/invite/user2",
50 |             "inviter_name": "Team Admin"
51 |         }
52 |     ]
53 |     
54 |     results = await send_bulk_invites(
55 |         invites=invites,
56 |         app_name="Your Amazing App",
57 |         expiry_days=7
58 |     )
59 |     
60 |     print(f"Sent {len(results)} invitation emails")
61 |     for i, result in enumerate(results):
62 |         if "error" in result:
63 |             print(f"Error sending to {invites[i]['email']}: {result['error']}")
64 |         else:
65 |             print(f"Successfully sent to {invites[i]['email']}")
66 | 
67 | async def main():
68 |     """Run both examples."""
69 |     await send_single_invite()
70 |     await send_multiple_invites()
71 | 
72 | if __name__ == "__main__":
73 |     asyncio.run(main())
74 | 
```

--------------------------------------------------------------------------------
/missing_endpoints_analysis.md:
--------------------------------------------------------------------------------

```markdown
 1 | # OneSignal MCP Server - Missing Endpoints Analysis
 2 | 
 3 | ## High Priority Missing Endpoints
 4 | 
 5 | ### 1. Messaging Endpoints
 6 | - **Email-specific endpoint** (`/notifications` with email channel)
 7 |   - Currently only generic push notifications are supported
 8 | - **SMS-specific endpoint** (`/notifications` with SMS channel)
 9 |   - No dedicated SMS sending functionality
10 | - **Transactional Messages** (`/notifications` with specific flags)
11 |   - Critical for automated/triggered messages
12 | 
13 | ### 2. Live Activities (iOS)
14 | - **Start Live Activity** (`/live_activities/{activity_id}/start`)
15 | - **Update Live Activity** (`/live_activities/{activity_id}/update`)
16 | - **End Live Activity** (`/live_activities/{activity_id}/end`)
17 | 
18 | ### 3. Template Management
19 | - **Update Template** (`PATCH /templates/{template_id}`)
20 | - **Delete Template** (`DELETE /templates/{template_id}`)
21 | - **Copy Template to Another App** (`POST /templates/{template_id}/copy`)
22 | 
23 | ### 4. API Key Management
24 | - **Delete API Key** (`DELETE /apps/{app_id}/auth/tokens/{token_id}`)
25 | - **Update API Key** (`PATCH /apps/{app_id}/auth/tokens/{token_id}`)
26 | - **Rotate API Key** (`POST /apps/{app_id}/auth/tokens/{token_id}/rotate`)
27 | 
28 | ### 5. Analytics/Outcomes
29 | - **View Outcomes** (`GET /apps/{app_id}/outcomes`)
30 |   - Essential for tracking conversion metrics
31 | 
32 | ### 6. Export Functionality
33 | - **Export Subscriptions CSV** (`POST /players/csv_export`)
34 | - **Export Audience Activity CSV** (`POST /notifications/csv_export`)
35 | 
36 | ### 7. Player/Device Management (Legacy but still used)
37 | - **Add a Player** (`POST /players`)
38 | - **Edit Player** (`PUT /players/{player_id}`)
39 | - **Edit Tags with External User ID** (`PUT /users/{external_user_id}`)
40 | - **Delete Player Record** (`DELETE /players/{player_id}`)
41 | 
42 | ### 8. Additional User/Subscription Endpoints
43 | - **View User Identity by Subscription** (`GET /apps/{app_id}/subscriptions/{subscription_id}/identity`)
44 | - **Create Alias by Subscription** (`PATCH /apps/{app_id}/subscriptions/{subscription_id}/identity`)
45 | 
46 | ## Medium Priority Missing Features
47 | 
48 | ### 1. In-App Messages
49 | - No endpoints for managing in-app messages
50 | 
51 | ### 2. Webhooks Management
52 | - No endpoints for configuring webhooks
53 | 
54 | ### 3. Journey/Automation APIs
55 | - No support for automated messaging journeys
56 | 
57 | ## Implementation Priority
58 | 
59 | 1. **Email and SMS endpoints** - Critical for multi-channel messaging
60 | 2. **Transactional Messages** - Essential for automated notifications
61 | 3. **Template management completeness** - Update and delete operations
62 | 4. **Export functionality** - Important for data management
63 | 5. **Analytics/Outcomes** - Necessary for measuring effectiveness 
```

--------------------------------------------------------------------------------
/test_auth_fix.py:
--------------------------------------------------------------------------------

```python
 1 | #!/usr/bin/env python3
 2 | """
 3 | Test script to verify the authentication fix for OneSignal MCP server
 4 | """
 5 | 
 6 | import asyncio
 7 | import json
 8 | import logging
 9 | import os
10 | from onesignal_server import (
11 |     view_segments, 
12 |     view_templates,
13 |     get_current_app,
14 |     app_configs,
15 |     make_onesignal_request,
16 |     logger
17 | )
18 | 
19 | # Enable DEBUG logging
20 | logging.basicConfig(level=logging.DEBUG)
21 | logger.setLevel(logging.DEBUG)
22 | 
23 | async def test_direct_api_call():
24 |     """Test direct API call to debug the issue"""
25 |     print("\nTesting direct API call...")
26 |     current_app = get_current_app()
27 |     
28 |     # Test direct segments call
29 |     endpoint = f"apps/{current_app.app_id}/segments"
30 |     print(f"Endpoint: {endpoint}")
31 |     print(f"App ID: {current_app.app_id}")
32 |     print(f"API Key length: {len(current_app.api_key)}")
33 |     
34 |     result = await make_onesignal_request(endpoint, method="GET", use_org_key=False)
35 |     print(f"Direct API result: {json.dumps(result, indent=2)}")
36 | 
37 | async def test_endpoints():
38 |     """Test the fixed endpoints"""
39 |     print("Testing OneSignal Authentication Fix")
40 |     print("=" * 50)
41 |     
42 |     # Check current app configuration
43 |     current_app = get_current_app()
44 |     if not current_app:
45 |         print("❌ No app configured. Please ensure your .env file has:")
46 |         print("   ONESIGNAL_APP_ID=your_app_id_here")
47 |         print("   ONESIGNAL_API_KEY=your_rest_api_key_here")
48 |         return
49 |     
50 |     print(f"✅ Current app: {current_app.name} (ID: {current_app.app_id})")
51 |     print(f"   API Key: {'*' * 20}{current_app.api_key[-10:]}")
52 |     print()
53 |     
54 |     # Test segments endpoint
55 |     print("Testing view_segments()...")
56 |     try:
57 |         segments_result = await view_segments()
58 |         if "Error retrieving segments:" in segments_result:
59 |             print(f"❌ Segments test failed: {segments_result}")
60 |         else:
61 |             print("✅ Segments endpoint is working!")
62 |             print(f"   Result preview: {segments_result[:200]}...")
63 |     except Exception as e:
64 |         print(f"❌ Segments test error: {str(e)}")
65 |     
66 |     print()
67 |     
68 |     # Test templates endpoint
69 |     print("Testing view_templates()...")
70 |     try:
71 |         templates_result = await view_templates()
72 |         if "Error retrieving templates:" in templates_result:
73 |             print(f"❌ Templates test failed: {templates_result}")
74 |         else:
75 |             print("✅ Templates endpoint is working!")
76 |             print(f"   Result preview: {templates_result[:200]}...")
77 |     except Exception as e:
78 |         print(f"❌ Templates test error: {str(e)}")
79 |     
80 |     # Test direct API call for debugging
81 |     await test_direct_api_call()
82 |     
83 |     print()
84 |     print("Test completed!")
85 | 
86 | if __name__ == "__main__":
87 |     asyncio.run(test_endpoints()) 
```

--------------------------------------------------------------------------------
/debug_api_key.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Debug script to check API key format and test authentication
  4 | """
  5 | 
  6 | import os
  7 | import requests
  8 | from dotenv import load_dotenv
  9 | 
 10 | # Load environment variables
 11 | load_dotenv()
 12 | 
 13 | # Get the API credentials
 14 | app_id = os.getenv("ONESIGNAL_MANDIBLE_APP_ID", "")
 15 | api_key = os.getenv("ONESIGNAL_MANDIBLE_API_KEY", "")
 16 | org_key = os.getenv("ONESIGNAL_ORG_API_KEY", "")
 17 | 
 18 | print("API Key Debugging")
 19 | print("=" * 50)
 20 | print(f"App ID: {app_id}")
 21 | print(f"REST API Key length: {len(api_key)}")
 22 | print(f"REST API Key prefix: {api_key[:10] if api_key else 'None'}")
 23 | print(f"Org API Key length: {len(org_key)}")
 24 | print(f"Org API Key prefix: {org_key[:10] if org_key else 'None'}")
 25 | 
 26 | # Check for common issues
 27 | if api_key:
 28 |     if api_key.startswith('"') or api_key.endswith('"'):
 29 |         print("⚠️  WARNING: API key contains quotes")
 30 |     if ' ' in api_key:
 31 |         print("⚠️  WARNING: API key contains spaces")
 32 |     if '\n' in api_key or '\r' in api_key:
 33 |         print("⚠️  WARNING: API key contains newlines")
 34 |     if api_key.startswith('Basic '):
 35 |         print("⚠️  WARNING: API key already contains 'Basic ' prefix")
 36 | 
 37 | # Test segments endpoint with REST API key
 38 | print("\n1. Testing segments endpoint with REST API key...")
 39 | url = f"https://api.onesignal.com/apps/{app_id}/segments"
 40 | headers = {
 41 |     "Authorization": f"Basic {api_key}",
 42 |     "Content-Type": "application/json",
 43 |     "Accept": "application/json"
 44 | }
 45 | 
 46 | print(f"URL: {url}")
 47 | 
 48 | try:
 49 |     response = requests.get(url, headers=headers, timeout=10)
 50 |     print(f"Response status: {response.status_code}")
 51 |     print(f"Response: {response.text[:500] if response.text else 'Empty response'}")
 52 |     
 53 |     if response.status_code == 200:
 54 |         print("✅ Segments endpoint working with REST API key!")
 55 |     else:
 56 |         print("❌ Segments endpoint failed with REST API key")
 57 | except Exception as e:
 58 |     print(f"❌ Request failed: {str(e)}")
 59 | 
 60 | # Test app details endpoint with Org API key
 61 | print("\n2. Testing app details endpoint with Org API key...")
 62 | url = f"https://api.onesignal.com/apps/{app_id}"
 63 | headers = {
 64 |     "Authorization": f"Basic {org_key}",
 65 |     "Content-Type": "application/json",
 66 |     "Accept": "application/json"
 67 | }
 68 | 
 69 | print(f"URL: {url}")
 70 | 
 71 | try:
 72 |     response = requests.get(url, headers=headers, timeout=10)
 73 |     print(f"Response status: {response.status_code}")
 74 |     
 75 |     if response.status_code == 200:
 76 |         print("✅ App details endpoint working with Org API key!")
 77 |     else:
 78 |         print(f"❌ App details endpoint failed: {response.text[:200]}")
 79 | except Exception as e:
 80 |     print(f"❌ Request failed: {str(e)}")
 81 | 
 82 | # Test templates endpoint
 83 | print("\n3. Testing templates endpoint with REST API key...")
 84 | url = f"https://api.onesignal.com/apps/{app_id}/templates"
 85 | headers = {
 86 |     "Authorization": f"Basic {api_key}",
 87 |     "Content-Type": "application/json",
 88 |     "Accept": "application/json"
 89 | }
 90 | 
 91 | print(f"URL: {url}")
 92 | 
 93 | try:
 94 |     response = requests.get(url, headers=headers, timeout=10)
 95 |     print(f"Response status: {response.status_code}")
 96 |     
 97 |     if response.status_code == 200:
 98 |         print("✅ Templates endpoint working with REST API key!")
 99 |     else:
100 |         print(f"❌ Templates endpoint failed: {response.text[:200]}")
101 | except Exception as e:
102 |     print(f"❌ Request failed: {str(e)}") 
```

--------------------------------------------------------------------------------
/onesignal_refactored/tools/live_activities.py:
--------------------------------------------------------------------------------

```python
  1 | """Live Activities management tools for OneSignal MCP server."""
  2 | from typing import Dict, Any, Optional
  3 | from ..api_client import api_client
  4 | 
  5 | 
  6 | async def start_live_activity(
  7 |     activity_id: str,
  8 |     push_token: str,
  9 |     subscription_id: str,
 10 |     activity_attributes: Dict[str, Any],
 11 |     content_state: Dict[str, Any],
 12 |     **kwargs
 13 | ) -> Dict[str, Any]:
 14 |     """
 15 |     Start a new Live Activity for iOS.
 16 |     
 17 |     Args:
 18 |         activity_id: Unique identifier for the activity
 19 |         push_token: Push token for the Live Activity
 20 |         subscription_id: Subscription ID for the user
 21 |         activity_attributes: Static attributes for the activity
 22 |         content_state: Initial dynamic content state
 23 |         **kwargs: Additional parameters
 24 |     """
 25 |     data = {
 26 |         "activity_id": activity_id,
 27 |         "push_token": push_token,
 28 |         "subscription_id": subscription_id,
 29 |         "activity_attributes": activity_attributes,
 30 |         "content_state": content_state
 31 |     }
 32 |     
 33 |     data.update(kwargs)
 34 |     
 35 |     return await api_client.request(
 36 |         f"live_activities/{activity_id}/start",
 37 |         method="POST",
 38 |         data=data
 39 |     )
 40 | 
 41 | 
 42 | async def update_live_activity(
 43 |     activity_id: str,
 44 |     name: str,
 45 |     event: str,
 46 |     content_state: Dict[str, Any],
 47 |     dismissal_date: Optional[int] = None,
 48 |     priority: Optional[int] = None,
 49 |     sound: Optional[str] = None,
 50 |     **kwargs
 51 | ) -> Dict[str, Any]:
 52 |     """
 53 |     Update an existing Live Activity.
 54 |     
 55 |     Args:
 56 |         activity_id: ID of the activity to update
 57 |         name: Name identifier for the update
 58 |         event: Event type ("update" or "end")
 59 |         content_state: Updated dynamic content state
 60 |         dismissal_date: Unix timestamp for automatic dismissal
 61 |         priority: Notification priority (5-10)
 62 |         sound: Sound file name for the update
 63 |         **kwargs: Additional parameters
 64 |     """
 65 |     data = {
 66 |         "name": name,
 67 |         "event": event,
 68 |         "content_state": content_state
 69 |     }
 70 |     
 71 |     if dismissal_date:
 72 |         data["dismissal_date"] = dismissal_date
 73 |     if priority:
 74 |         data["priority"] = priority
 75 |     if sound:
 76 |         data["sound"] = sound
 77 |     
 78 |     data.update(kwargs)
 79 |     
 80 |     return await api_client.request(
 81 |         f"live_activities/{activity_id}/update",
 82 |         method="POST",
 83 |         data=data
 84 |     )
 85 | 
 86 | 
 87 | async def end_live_activity(
 88 |     activity_id: str,
 89 |     subscription_id: str,
 90 |     dismissal_date: Optional[int] = None,
 91 |     priority: Optional[int] = None,
 92 |     **kwargs
 93 | ) -> Dict[str, Any]:
 94 |     """
 95 |     End a Live Activity.
 96 |     
 97 |     Args:
 98 |         activity_id: ID of the activity to end
 99 |         subscription_id: Subscription ID associated with the activity
100 |         dismissal_date: Unix timestamp for dismissal
101 |         priority: Notification priority (5-10)
102 |         **kwargs: Additional parameters
103 |     """
104 |     data = {
105 |         "subscription_id": subscription_id,
106 |         "event": "end"
107 |     }
108 |     
109 |     if dismissal_date:
110 |         data["dismissal_date"] = dismissal_date
111 |     if priority:
112 |         data["priority"] = priority
113 |     
114 |     data.update(kwargs)
115 |     
116 |     return await api_client.request(
117 |         f"live_activities/{activity_id}/end",
118 |         method="POST",
119 |         data=data
120 |     )
121 | 
122 | 
123 | async def get_live_activity_status(
124 |     activity_id: str,
125 |     subscription_id: str
126 | ) -> Dict[str, Any]:
127 |     """
128 |     Get the status of a Live Activity.
129 |     
130 |     Args:
131 |         activity_id: ID of the activity
132 |         subscription_id: Subscription ID associated with the activity
133 |     """
134 |     params = {"subscription_id": subscription_id}
135 |     
136 |     return await api_client.request(
137 |         f"live_activities/{activity_id}/status",
138 |         method="GET",
139 |         params=params
140 |     ) 
```

--------------------------------------------------------------------------------
/onesignal_refactored/tools/analytics.py:
--------------------------------------------------------------------------------

```python
  1 | """Analytics and outcomes tools for OneSignal MCP server."""
  2 | from typing import Dict, Any, Optional, List
  3 | from ..api_client import api_client
  4 | from ..config import app_manager
  5 | 
  6 | 
  7 | async def view_outcomes(
  8 |     outcome_names: List[str],
  9 |     outcome_time_range: Optional[str] = None,
 10 |     outcome_platforms: Optional[List[str]] = None,
 11 |     outcome_attribution: Optional[str] = None,
 12 |     **kwargs
 13 | ) -> Dict[str, Any]:
 14 |     """
 15 |     View outcomes data for your OneSignal app.
 16 |     
 17 |     Args:
 18 |         outcome_names: List of outcome names to fetch data for
 19 |         outcome_time_range: Time range for data (e.g., "1d", "1mo")
 20 |         outcome_platforms: Filter by platforms (e.g., ["ios", "android"])
 21 |         outcome_attribution: Attribution model ("direct" or "influenced")
 22 |         **kwargs: Additional parameters
 23 |     """
 24 |     app_config = app_manager.get_current_app()
 25 |     if not app_config:
 26 |         raise ValueError("No app currently selected")
 27 |     
 28 |     params = {
 29 |         "outcome_names": outcome_names
 30 |     }
 31 |     
 32 |     if outcome_time_range:
 33 |         params["outcome_time_range"] = outcome_time_range
 34 |     if outcome_platforms:
 35 |         params["outcome_platforms"] = outcome_platforms
 36 |     if outcome_attribution:
 37 |         params["outcome_attribution"] = outcome_attribution
 38 |     
 39 |     params.update(kwargs)
 40 |     
 41 |     return await api_client.request(
 42 |         f"apps/{app_config.app_id}/outcomes",
 43 |         method="GET",
 44 |         params=params
 45 |     )
 46 | 
 47 | 
 48 | async def export_players_csv(
 49 |     start_date: Optional[str] = None,
 50 |     end_date: Optional[str] = None,
 51 |     segment_names: Optional[List[str]] = None,
 52 |     **kwargs
 53 | ) -> Dict[str, Any]:
 54 |     """
 55 |     Export player/subscription data to CSV.
 56 |     
 57 |     Args:
 58 |         start_date: Start date for export (ISO 8601 format)
 59 |         end_date: End date for export (ISO 8601 format)
 60 |         segment_names: List of segment names to export
 61 |         **kwargs: Additional export parameters
 62 |     """
 63 |     data = {}
 64 |     
 65 |     if start_date:
 66 |         data["start_date"] = start_date
 67 |     if end_date:
 68 |         data["end_date"] = end_date
 69 |     if segment_names:
 70 |         data["segment_names"] = segment_names
 71 |     
 72 |     data.update(kwargs)
 73 |     
 74 |     return await api_client.request(
 75 |         "players/csv_export",
 76 |         method="POST",
 77 |         data=data,
 78 |         use_org_key=True
 79 |     )
 80 | 
 81 | 
 82 | async def export_audience_activity_csv(
 83 |     start_date: Optional[str] = None,
 84 |     end_date: Optional[str] = None,
 85 |     event_types: Optional[List[str]] = None,
 86 |     **kwargs
 87 | ) -> Dict[str, Any]:
 88 |     """
 89 |     Export audience activity events to CSV.
 90 |     
 91 |     Args:
 92 |         start_date: Start date for export (ISO 8601 format)
 93 |         end_date: End date for export (ISO 8601 format)
 94 |         event_types: List of event types to export
 95 |         **kwargs: Additional export parameters
 96 |     """
 97 |     data = {}
 98 |     
 99 |     if start_date:
100 |         data["start_date"] = start_date
101 |     if end_date:
102 |         data["end_date"] = end_date
103 |     if event_types:
104 |         data["event_types"] = event_types
105 |     
106 |     data.update(kwargs)
107 |     
108 |     return await api_client.request(
109 |         "notifications/csv_export",
110 |         method="POST",
111 |         data=data,
112 |         use_org_key=True
113 |     )
114 | 
115 | 
116 | def format_outcomes_response(outcomes: Dict[str, Any]) -> str:
117 |     """Format outcomes response for display."""
118 |     if not outcomes or "outcomes" not in outcomes:
119 |         return "No outcomes data available."
120 |     
121 |     output = "Outcomes Report:\n\n"
122 |     
123 |     for outcome in outcomes.get("outcomes", []):
124 |         output += f"Outcome: {outcome.get('id')}\n"
125 |         output += f"Total Count: {outcome.get('aggregation', {}).get('count', 0)}\n"
126 |         output += f"Total Value: {outcome.get('aggregation', {}).get('sum', 0)}\n"
127 |         
128 |         # Platform breakdown
129 |         platforms = outcome.get('platforms', {})
130 |         if platforms:
131 |             output += "Platform Breakdown:\n"
132 |             for platform, data in platforms.items():
133 |                 output += f"  {platform}: Count={data.get('count', 0)}, Value={data.get('sum', 0)}\n"
134 |         
135 |         output += "\n"
136 |     
137 |     return output 
```

--------------------------------------------------------------------------------
/onesignal_tools_list.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Complete OneSignal MCP Tools List
  2 | 
  3 | The OneSignal MCP server now includes **57 tools** covering all major OneSignal API functionality:
  4 | 
  5 | ## App Management (5 tools)
  6 | 1. **list_apps** - List all configured OneSignal apps
  7 | 2. **add_app** - Add a new OneSignal app configuration locally
  8 | 3. **update_local_app_config** - Update an existing local app configuration
  9 | 4. **remove_app** - Remove a local OneSignal app configuration
 10 | 5. **switch_app** - Switch the current app to use for API requests
 11 | 
 12 | ## Messaging (8 tools)
 13 | 6. **send_push_notification** - Send a push notification
 14 | 7. **send_email** *(NEW)* - Send an email through OneSignal
 15 | 8. **send_sms** *(NEW)* - Send an SMS/MMS through OneSignal
 16 | 9. **send_transactional_message** *(NEW)* - Send immediate delivery messages
 17 | 10. **view_messages** - View recent messages sent
 18 | 11. **view_message_details** - Get detailed information about a message
 19 | 12. **view_message_history** - View message history/recipients
 20 | 13. **cancel_message** - Cancel a scheduled message
 21 | 
 22 | ## Devices/Players (6 tools)
 23 | 14. **view_devices** - View devices subscribed to your app
 24 | 15. **view_device_details** - Get detailed information about a device
 25 | 16. **add_player** *(NEW)* - Add a new player/device
 26 | 17. **edit_player** *(NEW)* - Edit an existing player/device
 27 | 18. **delete_player** *(NEW)* - Delete a player/device record
 28 | 19. **edit_tags_with_external_user_id** *(NEW)* - Bulk edit tags by external ID
 29 | 
 30 | ## Segments (3 tools)
 31 | 20. **view_segments** - List all segments
 32 | 21. **create_segment** - Create a new segment
 33 | 22. **delete_segment** - Delete a segment
 34 | 
 35 | ## Templates (6 tools)
 36 | 23. **view_templates** - List all templates
 37 | 24. **view_template_details** - Get template details
 38 | 25. **create_template** - Create a new template
 39 | 26. **update_template** *(NEW)* - Update an existing template
 40 | 27. **delete_template** *(NEW)* - Delete a template
 41 | 28. **copy_template_to_app** *(NEW)* - Copy template to another app
 42 | 
 43 | ## Apps (6 tools)
 44 | 29. **view_app_details** - Get details about configured app
 45 | 30. **view_apps** - List all organization apps
 46 | 31. **create_app** - Create a new OneSignal application
 47 | 32. **update_app** - Update an existing application
 48 | 33. **view_app_api_keys** - View API keys for an app
 49 | 34. **create_app_api_key** - Create a new API key
 50 | 
 51 | ## API Key Management (3 tools) *(NEW)*
 52 | 35. **delete_app_api_key** - Delete an API key
 53 | 36. **update_app_api_key** - Update an API key
 54 | 37. **rotate_app_api_key** - Rotate an API key
 55 | 
 56 | ## Users (6 tools)
 57 | 38. **create_user** - Create a new user
 58 | 39. **view_user** - View user details
 59 | 40. **update_user** - Update user information
 60 | 41. **delete_user** - Delete a user
 61 | 42. **view_user_identity** - Get user identity information
 62 | 43. **view_user_identity_by_subscription** *(NEW)* - Get identity by subscription
 63 | 
 64 | ## Aliases (3 tools)
 65 | 44. **create_or_update_alias** - Create or update user alias
 66 | 45. **delete_alias** - Delete a user alias
 67 | 46. **create_alias_by_subscription** *(NEW)* - Create alias by subscription ID
 68 | 
 69 | ## Subscriptions (5 tools)
 70 | 47. **create_subscription** - Create a new subscription
 71 | 48. **update_subscription** - Update a subscription
 72 | 49. **delete_subscription** - Delete a subscription
 73 | 50. **transfer_subscription** - Transfer subscription between users
 74 | 51. **unsubscribe_email** - Unsubscribe using email token
 75 | 
 76 | ## Live Activities (3 tools) *(NEW)*
 77 | 52. **start_live_activity** - Start iOS Live Activity
 78 | 53. **update_live_activity** - Update iOS Live Activity
 79 | 54. **end_live_activity** - End iOS Live Activity
 80 | 
 81 | ## Analytics & Export (3 tools) *(NEW)*
 82 | 55. **view_outcomes** - View outcomes/conversion data
 83 | 56. **export_players_csv** - Export player data to CSV
 84 | 57. **export_messages_csv** - Export messages to CSV
 85 | 
 86 | ## Summary by Category
 87 | - **App Management**: 5 tools
 88 | - **Messaging**: 8 tools (3 new)
 89 | - **Devices/Players**: 6 tools (4 new)
 90 | - **Segments**: 3 tools
 91 | - **Templates**: 6 tools (3 new)
 92 | - **Apps**: 6 tools
 93 | - **API Keys**: 3 tools (all new)
 94 | - **Users**: 6 tools (1 new)
 95 | - **Aliases**: 3 tools (1 new)
 96 | - **Subscriptions**: 5 tools
 97 | - **Live Activities**: 3 tools (all new)
 98 | - **Analytics**: 3 tools (all new)
 99 | 
100 | **Total**: 57 tools (21 newly added) 
```

--------------------------------------------------------------------------------
/onesignal_refactored/tools/templates.py:
--------------------------------------------------------------------------------

```python
  1 | """Template management tools for OneSignal MCP server."""
  2 | from typing import Dict, Any, Optional
  3 | from ..api_client import api_client
  4 | from ..config import app_manager
  5 | 
  6 | 
  7 | async def create_template(
  8 |     name: str,
  9 |     title: str,
 10 |     message: str,
 11 |     **kwargs
 12 | ) -> Dict[str, Any]:
 13 |     """
 14 |     Create a new template in your OneSignal app.
 15 |     
 16 |     Args:
 17 |         name: Name of the template
 18 |         title: Title/heading of the template
 19 |         message: Content/message of the template
 20 |         **kwargs: Additional template parameters
 21 |     """
 22 |     app_config = app_manager.get_current_app()
 23 |     if not app_config:
 24 |         raise ValueError("No app currently selected")
 25 |     
 26 |     data = {
 27 |         "app_id": app_config.app_id,
 28 |         "name": name,
 29 |         "headings": {"en": title},
 30 |         "contents": {"en": message}
 31 |     }
 32 |     
 33 |     data.update(kwargs)
 34 |     
 35 |     return await api_client.request("templates", method="POST", data=data)
 36 | 
 37 | 
 38 | async def update_template(
 39 |     template_id: str,
 40 |     name: Optional[str] = None,
 41 |     title: Optional[str] = None,
 42 |     message: Optional[str] = None,
 43 |     **kwargs
 44 | ) -> Dict[str, Any]:
 45 |     """
 46 |     Update an existing template.
 47 |     
 48 |     Args:
 49 |         template_id: ID of the template to update
 50 |         name: New name for the template
 51 |         title: New title/heading for the template
 52 |         message: New content/message for the template
 53 |         **kwargs: Additional template parameters
 54 |     """
 55 |     data = {}
 56 |     
 57 |     if name:
 58 |         data["name"] = name
 59 |     if title:
 60 |         data["headings"] = {"en": title}
 61 |     if message:
 62 |         data["contents"] = {"en": message}
 63 |     
 64 |     data.update(kwargs)
 65 |     
 66 |     if not data:
 67 |         raise ValueError("No update parameters provided")
 68 |     
 69 |     return await api_client.request(
 70 |         f"templates/{template_id}",
 71 |         method="PATCH",
 72 |         data=data
 73 |     )
 74 | 
 75 | 
 76 | async def view_templates() -> Dict[str, Any]:
 77 |     """List all templates available in your OneSignal app."""
 78 |     return await api_client.request("templates", method="GET")
 79 | 
 80 | 
 81 | async def view_template_details(template_id: str) -> Dict[str, Any]:
 82 |     """
 83 |     Get detailed information about a specific template.
 84 |     
 85 |     Args:
 86 |         template_id: The ID of the template to retrieve
 87 |     """
 88 |     app_config = app_manager.get_current_app()
 89 |     if not app_config:
 90 |         raise ValueError("No app currently selected")
 91 |     
 92 |     params = {"app_id": app_config.app_id}
 93 |     return await api_client.request(
 94 |         f"templates/{template_id}",
 95 |         method="GET",
 96 |         params=params
 97 |     )
 98 | 
 99 | 
100 | async def delete_template(template_id: str) -> Dict[str, Any]:
101 |     """
102 |     Delete a template from your OneSignal app.
103 |     
104 |     Args:
105 |         template_id: ID of the template to delete
106 |     """
107 |     return await api_client.request(
108 |         f"templates/{template_id}",
109 |         method="DELETE"
110 |     )
111 | 
112 | 
113 | async def copy_template_to_app(
114 |     template_id: str,
115 |     target_app_id: str,
116 |     new_name: Optional[str] = None
117 | ) -> Dict[str, Any]:
118 |     """
119 |     Copy a template to another OneSignal app.
120 |     
121 |     Args:
122 |         template_id: ID of the template to copy
123 |         target_app_id: ID of the app to copy the template to
124 |         new_name: Optional new name for the copied template
125 |     """
126 |     data = {"app_id": target_app_id}
127 |     
128 |     if new_name:
129 |         data["name"] = new_name
130 |     
131 |     return await api_client.request(
132 |         f"templates/{template_id}/copy",
133 |         method="POST",
134 |         data=data
135 |     )
136 | 
137 | 
138 | def format_template_list(templates_response: Dict[str, Any]) -> str:
139 |     """Format template list response for display."""
140 |     templates = templates_response.get("templates", [])
141 |     
142 |     if not templates:
143 |         return "No templates found."
144 |     
145 |     output = "Templates:\n\n"
146 |     
147 |     for template in templates:
148 |         output += f"ID: {template.get('id')}\n"
149 |         output += f"Name: {template.get('name')}\n"
150 |         output += f"Created: {template.get('created_at')}\n"
151 |         output += f"Updated: {template.get('updated_at')}\n\n"
152 |     
153 |     return output
154 | 
155 | 
156 | def format_template_details(template: Dict[str, Any]) -> str:
157 |     """Format template details for display."""
158 |     heading = template.get("headings", {}).get("en", "No heading")
159 |     content = template.get("contents", {}).get("en", "No content")
160 |     
161 |     details = [
162 |         f"ID: {template.get('id')}",
163 |         f"Name: {template.get('name')}",
164 |         f"Title: {heading}",
165 |         f"Message: {content}",
166 |         f"Platform: {template.get('platform')}",
167 |         f"Created: {template.get('created_at')}"
168 |     ]
169 |     
170 |     return "\n".join(details) 
```

--------------------------------------------------------------------------------
/test_api_key_validity.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """Test script to verify OneSignal API key validity"""
  3 | 
  4 | import os
  5 | import requests
  6 | from dotenv import load_dotenv
  7 | 
  8 | # Load environment variables
  9 | load_dotenv()
 10 | 
 11 | # Get credentials
 12 | app_id = os.getenv("ONESIGNAL_MANDIBLE_APP_ID")
 13 | api_key = os.getenv("ONESIGNAL_MANDIBLE_API_KEY")
 14 | 
 15 | print("OneSignal API Key Validation Test")
 16 | print("=" * 50)
 17 | 
 18 | if not app_id or not api_key:
 19 |     print("❌ ERROR: Missing credentials!")
 20 |     print("   Make sure your .env file contains:")
 21 |     print("   ONESIGNAL_MANDIBLE_APP_ID=your_app_id")
 22 |     print("   ONESIGNAL_MANDIBLE_API_KEY=your_api_key")
 23 |     exit(1)
 24 | 
 25 | print(f"App ID: {app_id}")
 26 | print(f"API Key length: {len(api_key)}")
 27 | print(f"API Key prefix: {api_key[:15]}...")
 28 | 
 29 | # Determine API key type
 30 | is_v2_key = api_key.startswith("os_v2_")
 31 | print(f"API Key type: {'v2' if is_v2_key else 'v1'}")
 32 | 
 33 | # Set up headers based on key type
 34 | headers = {
 35 |     "Accept": "application/json",
 36 |     "Content-Type": "application/json"
 37 | }
 38 | 
 39 | if is_v2_key:
 40 |     headers["Authorization"] = f"Key {api_key}"
 41 |     print("Using Authorization: Key <api_key>")
 42 | else:
 43 |     headers["Authorization"] = f"Basic {api_key}"
 44 |     print("Using Authorization: Basic <api_key>")
 45 | 
 46 | print("\n" + "=" * 50)
 47 | print("Testing API Key validity...")
 48 | print("=" * 50)
 49 | 
 50 | # Test 1: Get app details (requires valid API key)
 51 | print("\n1. Testing app details endpoint (requires valid app-specific API key)...")
 52 | url = f"https://api.onesignal.com/api/v1/apps/{app_id}"
 53 | params = {}  # No params needed for this endpoint
 54 | 
 55 | try:
 56 |     response = requests.get(url, headers=headers, params=params)
 57 |     print(f"   URL: {url}")
 58 |     print(f"   Status: {response.status_code}")
 59 |     
 60 |     if response.status_code == 200:
 61 |         print("   ✅ SUCCESS: API key is valid!")
 62 |         data = response.json()
 63 |         print(f"   App Name: {data.get('name', 'N/A')}")
 64 |         print(f"   Created: {data.get('created_at', 'N/A')}")
 65 |     elif response.status_code == 401:
 66 |         print("   ❌ FAILED: Authentication error - API key is invalid or doesn't have permission")
 67 |         print(f"   Response: {response.text}")
 68 |     elif response.status_code == 403:
 69 |         print("   ❌ FAILED: Forbidden - API key doesn't have permission for this app")
 70 |         print(f"   Response: {response.text}")
 71 |     else:
 72 |         print(f"   ❌ FAILED: Unexpected status code")
 73 |         print(f"   Response: {response.text}")
 74 | except Exception as e:
 75 |     print(f"   ❌ ERROR: {e}")
 76 | 
 77 | # Test 2: List notifications (requires valid API key with proper permissions)
 78 | print("\n2. Testing notifications endpoint...")
 79 | url = "https://api.onesignal.com/api/v1/notifications"
 80 | params = {"app_id": app_id, "limit": 1}
 81 | 
 82 | try:
 83 |     response = requests.get(url, headers=headers, params=params)
 84 |     print(f"   URL: {url}")
 85 |     print(f"   Params: {params}")
 86 |     print(f"   Status: {response.status_code}")
 87 |     
 88 |     if response.status_code == 200:
 89 |         print("   ✅ SUCCESS: Can list notifications")
 90 |         data = response.json()
 91 |         print(f"   Total notifications: {data.get('total_count', 0)}")
 92 |     else:
 93 |         print(f"   ❌ FAILED: Status {response.status_code}")
 94 |         print(f"   Response: {response.text[:200]}...")
 95 | except Exception as e:
 96 |     print(f"   ❌ ERROR: {e}")
 97 | 
 98 | # Test 3: Check segments endpoint (the one causing issues)
 99 | print("\n3. Testing segments endpoint (the problematic one)...")
100 | url = f"https://api.onesignal.com/api/v1/apps/{app_id}/segments"
101 | 
102 | try:
103 |     response = requests.get(url, headers=headers)
104 |     print(f"   URL: {url}")
105 |     print(f"   Status: {response.status_code}")
106 |     
107 |     if response.status_code == 200:
108 |         print("   ✅ SUCCESS: Can access segments")
109 |         data = response.json()
110 |         print(f"   Segments found: {len(data) if isinstance(data, list) else 'N/A'}")
111 |     else:
112 |         print(f"   ❌ FAILED: Status {response.status_code}")
113 |         print(f"   Response: {response.text}")
114 | except Exception as e:
115 |     print(f"   ❌ ERROR: {e}")
116 | 
117 | print("\n" + "=" * 50)
118 | print("Summary:")
119 | print("=" * 50)
120 | 
121 | if api_key.startswith("os_v2_"):
122 |     print("You're using a v2 API key (starts with 'os_v2_')")
123 |     print("Make sure this key has the necessary permissions in OneSignal dashboard:")
124 |     print("- View App Details")
125 |     print("- View Notifications") 
126 |     print("- View Segments")
127 |     print("\nTo check/update permissions:")
128 |     print("1. Go to OneSignal Dashboard")
129 |     print("2. Navigate to Settings > Keys & IDs")
130 |     print("3. Find your REST API Key")
131 |     print("4. Check the permissions assigned to it")
132 | else:
133 |     print("You're using a v1 API key")
134 |     print("Consider upgrading to a v2 API key for better security and permissions control") 
```

--------------------------------------------------------------------------------
/onesignal_refactored/config.py:
--------------------------------------------------------------------------------

```python
  1 | """Configuration management for OneSignal MCP server."""
  2 | import os
  3 | import logging
  4 | from typing import Dict, Optional
  5 | from dataclasses import dataclass
  6 | from dotenv import load_dotenv
  7 | 
  8 | # Load environment variables
  9 | load_dotenv()
 10 | 
 11 | # Configure logger
 12 | logger = logging.getLogger("onesignal-mcp.config")
 13 | 
 14 | # API Configuration
 15 | ONESIGNAL_API_URL = "https://api.onesignal.com/api/v1"
 16 | ONESIGNAL_ORG_API_KEY = os.getenv("ONESIGNAL_ORG_API_KEY", "")
 17 | 
 18 | 
 19 | @dataclass
 20 | class AppConfig:
 21 |     """Configuration for a OneSignal application."""
 22 |     app_id: str
 23 |     api_key: str
 24 |     name: str
 25 |     
 26 |     def __str__(self):
 27 |         return f"{self.name} ({self.app_id})"
 28 | 
 29 | 
 30 | class AppManager:
 31 |     """Manages OneSignal app configurations."""
 32 |     
 33 |     def __init__(self):
 34 |         self.app_configs: Dict[str, AppConfig] = {}
 35 |         self.current_app_key: Optional[str] = None
 36 |         self._load_from_environment()
 37 |     
 38 |     def _load_from_environment(self):
 39 |         """Load app configurations from environment variables."""
 40 |         # Mandible app configuration
 41 |         mandible_app_id = os.getenv("ONESIGNAL_MANDIBLE_APP_ID", "") or os.getenv("ONESIGNAL_APP_ID", "")
 42 |         mandible_api_key = os.getenv("ONESIGNAL_MANDIBLE_API_KEY", "") or os.getenv("ONESIGNAL_API_KEY", "")
 43 |         if mandible_app_id and mandible_api_key:
 44 |             self.add_app("mandible", mandible_app_id, mandible_api_key, "Mandible")
 45 |             self.current_app_key = "mandible"
 46 |             logger.info(f"Mandible app configured with ID: {mandible_app_id}")
 47 | 
 48 |         # Weird Brains app configuration
 49 |         weirdbrains_app_id = os.getenv("ONESIGNAL_WEIRDBRAINS_APP_ID", "")
 50 |         weirdbrains_api_key = os.getenv("ONESIGNAL_WEIRDBRAINS_API_KEY", "")
 51 |         if weirdbrains_app_id and weirdbrains_api_key:
 52 |             self.add_app("weirdbrains", weirdbrains_app_id, weirdbrains_api_key, "Weird Brains")
 53 |             if not self.current_app_key:
 54 |                 self.current_app_key = "weirdbrains"
 55 |             logger.info(f"Weird Brains app configured with ID: {weirdbrains_app_id}")
 56 | 
 57 |         # Fallback for default app configuration
 58 |         if not self.app_configs:
 59 |             default_app_id = os.getenv("ONESIGNAL_APP_ID", "")
 60 |             default_api_key = os.getenv("ONESIGNAL_API_KEY", "")
 61 |             if default_app_id and default_api_key:
 62 |                 self.add_app("default", default_app_id, default_api_key, "Default App")
 63 |                 self.current_app_key = "default"
 64 |                 logger.info(f"Default app configured with ID: {default_app_id}")
 65 |             else:
 66 |                 logger.warning("No app configurations found. Use add_app to add an app configuration.")
 67 |     
 68 |     def add_app(self, key: str, app_id: str, api_key: str, name: Optional[str] = None) -> None:
 69 |         """Add a new app configuration."""
 70 |         self.app_configs[key] = AppConfig(app_id, api_key, name or key)
 71 |         logger.info(f"Added app configuration '{key}' with ID: {app_id}")
 72 |     
 73 |     def update_app(self, key: str, app_id: Optional[str] = None, 
 74 |                    api_key: Optional[str] = None, name: Optional[str] = None) -> bool:
 75 |         """Update an existing app configuration."""
 76 |         if key not in self.app_configs:
 77 |             return False
 78 |         
 79 |         app = self.app_configs[key]
 80 |         if app_id:
 81 |             app.app_id = app_id
 82 |         if api_key:
 83 |             app.api_key = api_key
 84 |         if name:
 85 |             app.name = name
 86 |         
 87 |         logger.info(f"Updated app configuration '{key}'")
 88 |         return True
 89 |     
 90 |     def remove_app(self, key: str) -> bool:
 91 |         """Remove an app configuration."""
 92 |         if key not in self.app_configs:
 93 |             return False
 94 |         
 95 |         if self.current_app_key == key:
 96 |             # Switch to another app if available
 97 |             other_keys = [k for k in self.app_configs.keys() if k != key]
 98 |             self.current_app_key = other_keys[0] if other_keys else None
 99 |         
100 |         del self.app_configs[key]
101 |         logger.info(f"Removed app configuration '{key}'")
102 |         return True
103 |     
104 |     def set_current_app(self, key: str) -> bool:
105 |         """Set the current app to use for API requests."""
106 |         if key in self.app_configs:
107 |             self.current_app_key = key
108 |             logger.info(f"Switched to app '{key}'")
109 |             return True
110 |         return False
111 |     
112 |     def get_current_app(self) -> Optional[AppConfig]:
113 |         """Get the current app configuration."""
114 |         if self.current_app_key and self.current_app_key in self.app_configs:
115 |             return self.app_configs[self.current_app_key]
116 |         return None
117 |     
118 |     def get_app(self, key: str) -> Optional[AppConfig]:
119 |         """Get a specific app configuration."""
120 |         return self.app_configs.get(key)
121 |     
122 |     def list_apps(self) -> Dict[str, AppConfig]:
123 |         """Get all app configurations."""
124 |         return self.app_configs.copy()
125 | 
126 | 
127 | # Global app manager instance
128 | app_manager = AppManager()
129 | 
130 | 
131 | def requires_org_api_key(endpoint: str) -> bool:
132 |     """Determine if an endpoint requires the Organization API Key."""
133 |     org_level_endpoints = [
134 |         "apps",                    # Managing apps
135 |         "players/csv_export",      # Export users
136 |         "notifications/csv_export" # Export notifications
137 |     ]
138 |     
139 |     return any(endpoint == ep or endpoint.startswith(f"{ep}/") for ep in org_level_endpoints) 
```

--------------------------------------------------------------------------------
/onesignal_refactoring_summary.md:
--------------------------------------------------------------------------------

```markdown
  1 | # OneSignal MCP Server Refactoring Summary
  2 | 
  3 | ## Overview
  4 | This document summarizes the analysis of the OneSignal MCP server implementation against the official OneSignal REST API documentation and provides a comprehensive refactoring plan.
  5 | 
  6 | ## 1. Missing API Endpoints Analysis
  7 | 
  8 | ### High Priority Missing Endpoints
  9 | 
 10 | #### Messaging
 11 | - **Email-specific endpoint** - Send emails with HTML content and templates
 12 | - **SMS-specific endpoint** - Send SMS/MMS messages
 13 | - **Transactional Messages** - Immediate delivery messages without scheduling
 14 | 
 15 | #### Live Activities (iOS)
 16 | - **Start Live Activity** - Initialize iOS Live Activities
 17 | - **Update Live Activity** - Update running Live Activities
 18 | - **End Live Activity** - Terminate Live Activities
 19 | 
 20 | #### Template Management
 21 | - **Update Template** - Modify existing templates
 22 | - **Delete Template** - Remove templates
 23 | - **Copy Template** - Duplicate templates across apps
 24 | 
 25 | #### API Key Management
 26 | - **Delete API Key** - Remove API keys
 27 | - **Update API Key** - Modify API key permissions
 28 | - **Rotate API Key** - Generate new key while maintaining permissions
 29 | 
 30 | #### Analytics & Export
 31 | - **View Outcomes** - Track conversion metrics
 32 | - **Export Subscriptions CSV** - Export user data
 33 | - **Export Audience Activity CSV** - Export event data
 34 | 
 35 | #### Player/Device Management (Legacy)
 36 | - **Add Player** - Register new devices
 37 | - **Edit Player** - Update device information
 38 | - **Edit Tags with External User ID** - Bulk tag updates
 39 | - **Delete Player Record** - Remove device records
 40 | 
 41 | ## 2. Refactored Architecture
 42 | 
 43 | ### New Module Structure
 44 | ```
 45 | onesignal_refactored/
 46 | ├── __init__.py
 47 | ├── config.py              # App configuration management
 48 | ├── api_client.py          # API request handling
 49 | ├── tools/
 50 | │   ├── __init__.py
 51 | │   ├── messages.py        # All messaging endpoints
 52 | │   ├── templates.py       # Template management
 53 | │   ├── live_activities.py # iOS Live Activities
 54 | │   ├── analytics.py       # Outcomes and exports
 55 | │   ├── users.py          # User management
 56 | │   ├── devices.py        # Device/player management
 57 | │   ├── segments.py       # Segment management
 58 | │   ├── apps.py           # App management
 59 | │   └── subscriptions.py  # Subscription management
 60 | └── server.py             # Main MCP server entry point
 61 | ```
 62 | 
 63 | ### Key Improvements
 64 | 
 65 | #### 1. Centralized Configuration (`config.py`)
 66 | - `AppConfig` dataclass for app configurations
 67 | - `AppManager` class for managing multiple apps
 68 | - Environment variable loading
 69 | - Automatic organization API key detection
 70 | 
 71 | #### 2. Unified API Client (`api_client.py`)
 72 | - `OneSignalAPIClient` class with centralized request handling
 73 | - Automatic authentication method selection
 74 | - Consistent error handling with custom exceptions
 75 | - Request/response logging
 76 | 
 77 | #### 3. Modular Tool Organization
 78 | Each module focuses on a specific API domain:
 79 | - Better code organization
 80 | - Easier maintenance
 81 | - Clear separation of concerns
 82 | - Reduced code duplication
 83 | 
 84 | ### 3. New Features Implementation
 85 | 
 86 | #### Email Sending
 87 | ```python
 88 | async def send_email(
 89 |     subject: str,
 90 |     body: str,
 91 |     email_body: Optional[str] = None,
 92 |     segments: Optional[List[str]] = None,
 93 |     include_emails: Optional[List[str]] = None,
 94 |     external_ids: Optional[List[str]] = None,
 95 |     template_id: Optional[str] = None
 96 | ) -> Dict[str, Any]
 97 | ```
 98 | 
 99 | #### SMS Sending
100 | ```python
101 | async def send_sms(
102 |     message: str,
103 |     phone_numbers: Optional[List[str]] = None,
104 |     segments: Optional[List[str]] = None,
105 |     external_ids: Optional[List[str]] = None,
106 |     media_url: Optional[str] = None
107 | ) -> Dict[str, Any]
108 | ```
109 | 
110 | #### Transactional Messages
111 | ```python
112 | async def send_transactional_message(
113 |     channel: str,
114 |     content: Dict[str, str],
115 |     recipients: Dict[str, Any],
116 |     template_id: Optional[str] = None,
117 |     custom_data: Optional[Dict[str, Any]] = None
118 | ) -> Dict[str, Any]
119 | ```
120 | 
121 | #### Live Activities
122 | ```python
123 | async def start_live_activity(...)
124 | async def update_live_activity(...)
125 | async def end_live_activity(...)
126 | ```
127 | 
128 | #### Analytics & Outcomes
129 | ```python
130 | async def view_outcomes(...)
131 | async def export_players_csv(...)
132 | async def export_audience_activity_csv(...)
133 | ```
134 | 
135 | ## 4. Migration Guide
136 | 
137 | ### Step 1: Create New Directory Structure
138 | ```bash
139 | mkdir -p onesignal_refactored/tools
140 | ```
141 | 
142 | ### Step 2: Implement Core Modules
143 | 1. Copy `config.py` for app configuration
144 | 2. Copy `api_client.py` for API requests
145 | 3. Implement tool modules based on provided templates
146 | 
147 | ### Step 3: Update MCP Server Registration
148 | ```python
149 | # In server.py
150 | from mcp.server.fastmcp import FastMCP
151 | from .tools import messages, templates, live_activities, analytics
152 | 
153 | mcp = FastMCP("onesignal-server")
154 | 
155 | # Register all tools
156 | @mcp.tool()
157 | async def send_email(...):
158 |     return await messages.send_email(...)
159 | 
160 | # Continue for all tools...
161 | ```
162 | 
163 | ### Step 4: Test Implementation
164 | 1. Test each new endpoint individually
165 | 2. Verify error handling
166 | 3. Check authentication switching
167 | 4. Validate response formatting
168 | 
169 | ## 5. Benefits of Refactoring
170 | 
171 | 1. **Better Maintainability** - Modular structure makes updates easier
172 | 2. **Reduced Duplication** - Shared API client eliminates repeated code
173 | 3. **Enhanced Error Handling** - Consistent error messages and logging
174 | 4. **Feature Completeness** - Support for all major OneSignal features
175 | 5. **Improved Testing** - Easier to unit test individual modules
176 | 6. **Better Documentation** - Clear module boundaries and responsibilities
177 | 
178 | ## 6. Future Enhancements
179 | 
180 | 1. **Async/Await Optimization** - Better concurrency handling
181 | 2. **Response Caching** - Cache frequently accessed data
182 | 3. **Batch Operations** - Support bulk operations where applicable
183 | 4. **Webhook Support** - Add webhook configuration endpoints
184 | 5. **In-App Messaging** - Support for in-app message management
185 | 6. **Rate Limiting** - Implement client-side rate limiting
186 | 7. **Retry Logic** - Automatic retry for failed requests
187 | 
188 | ## Implementation Priority
189 | 
190 | 1. **Phase 1** - Core refactoring (config, api_client)
191 | 2. **Phase 2** - Missing messaging endpoints (email, SMS, transactional)
192 | 3. **Phase 3** - Template and Live Activity completion
193 | 4. **Phase 4** - Analytics and export functionality
194 | 5. **Phase 5** - Legacy player/device endpoints
195 | 
196 | This refactoring will transform the OneSignal MCP server into a comprehensive, maintainable solution that supports all major OneSignal API features. 
```

--------------------------------------------------------------------------------
/onesignal_refactored/api_client.py:
--------------------------------------------------------------------------------

```python
  1 | """API client for OneSignal REST API requests."""
  2 | import logging
  3 | import requests
  4 | from typing import Dict, Any, Optional
  5 | from .config import (
  6 |     ONESIGNAL_API_URL, 
  7 |     ONESIGNAL_ORG_API_KEY,
  8 |     app_manager,
  9 |     requires_org_api_key
 10 | )
 11 | 
 12 | logger = logging.getLogger("onesignal-mcp.api_client")
 13 | 
 14 | 
 15 | class OneSignalAPIError(Exception):
 16 |     """Custom exception for OneSignal API errors."""
 17 |     pass
 18 | 
 19 | 
 20 | class OneSignalAPIClient:
 21 |     """Client for making requests to the OneSignal API."""
 22 |     
 23 |     def __init__(self):
 24 |         self.api_url = ONESIGNAL_API_URL
 25 |         self.timeout = 30
 26 |     
 27 |     async def request(
 28 |         self,
 29 |         endpoint: str,
 30 |         method: str = "GET",
 31 |         data: Optional[Dict[str, Any]] = None,
 32 |         params: Optional[Dict[str, Any]] = None,
 33 |         use_org_key: Optional[bool] = None,
 34 |         app_key: Optional[str] = None
 35 |     ) -> Dict[str, Any]:
 36 |         """
 37 |         Make a request to the OneSignal API with proper authentication.
 38 |         
 39 |         Args:
 40 |             endpoint: API endpoint path
 41 |             method: HTTP method (GET, POST, PUT, DELETE, PATCH)
 42 |             data: Request body for POST/PUT/PATCH requests
 43 |             params: Query parameters for GET requests
 44 |             use_org_key: Whether to use the organization API key
 45 |             app_key: The key of the app configuration to use
 46 |             
 47 |         Returns:
 48 |             API response as dictionary
 49 |             
 50 |         Raises:
 51 |             OneSignalAPIError: If the API request fails
 52 |         """
 53 |         headers = {
 54 |             "Content-Type": "application/json",
 55 |             "Accept": "application/json",
 56 |         }
 57 |         
 58 |         # Determine authentication method
 59 |         if use_org_key is None:
 60 |             use_org_key = requires_org_api_key(endpoint)
 61 |         
 62 |         # Set authentication header
 63 |         if use_org_key:
 64 |             if not ONESIGNAL_ORG_API_KEY:
 65 |                 raise OneSignalAPIError(
 66 |                     "Organization API Key not configured. "
 67 |                     "Set the ONESIGNAL_ORG_API_KEY environment variable."
 68 |                 )
 69 |             headers["Authorization"] = f"Basic {ONESIGNAL_ORG_API_KEY}"
 70 |         else:
 71 |             # Get app configuration
 72 |             app_config = None
 73 |             if app_key:
 74 |                 app_config = app_manager.get_app(app_key)
 75 |             else:
 76 |                 app_config = app_manager.get_current_app()
 77 |             
 78 |             if not app_config:
 79 |                 raise OneSignalAPIError(
 80 |                     "No app configuration available. "
 81 |                     "Use set_current_app or specify app_key."
 82 |                 )
 83 |             
 84 |             headers["Authorization"] = f"Basic {app_config.api_key}"
 85 |             
 86 |             # Add app_id to params/data if needed
 87 |             if params is None:
 88 |                 params = {}
 89 |             if "app_id" not in params and not endpoint.startswith("apps/"):
 90 |                 params["app_id"] = app_config.app_id
 91 |             
 92 |             if data is not None and method in ["POST", "PUT", "PATCH"]:
 93 |                 if "app_id" not in data and not endpoint.startswith("apps/"):
 94 |                     data["app_id"] = app_config.app_id
 95 |         
 96 |         url = f"{self.api_url}/{endpoint}"
 97 |         
 98 |         try:
 99 |             logger.debug(f"Making {method} request to {url}")
100 |             logger.debug(f"Using {'Organization API Key' if use_org_key else 'App REST API Key'}")
101 |             
102 |             response = self._make_request(method, url, headers, params, data)
103 |             response.raise_for_status()
104 |             
105 |             return response.json() if response.text else {}
106 |             
107 |         except requests.exceptions.HTTPError as e:
108 |             error_message = self._extract_error_message(e)
109 |             logger.error(f"API request failed: {error_message}")
110 |             raise OneSignalAPIError(error_message) from e
111 |         except requests.exceptions.RequestException as e:
112 |             error_message = f"Request failed: {str(e)}"
113 |             logger.error(error_message)
114 |             raise OneSignalAPIError(error_message) from e
115 |         except Exception as e:
116 |             error_message = f"Unexpected error: {str(e)}"
117 |             logger.exception(error_message)
118 |             raise OneSignalAPIError(error_message) from e
119 |     
120 |     def _make_request(
121 |         self,
122 |         method: str,
123 |         url: str,
124 |         headers: Dict[str, str],
125 |         params: Optional[Dict[str, Any]],
126 |         data: Optional[Dict[str, Any]]
127 |     ) -> requests.Response:
128 |         """Make the actual HTTP request."""
129 |         method = method.upper()
130 |         
131 |         if method == "GET":
132 |             return requests.get(url, headers=headers, params=params, timeout=self.timeout)
133 |         elif method == "POST":
134 |             return requests.post(url, headers=headers, json=data, timeout=self.timeout)
135 |         elif method == "PUT":
136 |             return requests.put(url, headers=headers, json=data, timeout=self.timeout)
137 |         elif method == "DELETE":
138 |             return requests.delete(url, headers=headers, timeout=self.timeout)
139 |         elif method == "PATCH":
140 |             return requests.patch(url, headers=headers, json=data, timeout=self.timeout)
141 |         else:
142 |             raise ValueError(f"Unsupported HTTP method: {method}")
143 |     
144 |     def _extract_error_message(self, error: requests.exceptions.HTTPError) -> str:
145 |         """Extract a meaningful error message from the HTTP error."""
146 |         try:
147 |             if hasattr(error, 'response') and error.response is not None:
148 |                 error_data = error.response.json()
149 |                 if isinstance(error_data, dict):
150 |                     # Try different error message formats
151 |                     if 'errors' in error_data:
152 |                         errors = error_data['errors']
153 |                         if isinstance(errors, list) and errors:
154 |                             return f"Error: {errors[0]}"
155 |                         elif isinstance(errors, str):
156 |                             return f"Error: {errors}"
157 |                     elif 'error' in error_data:
158 |                         return f"Error: {error_data['error']}"
159 |                     elif 'message' in error_data:
160 |                         return f"Error: {error_data['message']}"
161 |                 return f"Error: {error.response.reason} (Status: {error.response.status_code})"
162 |         except Exception:
163 |             pass
164 |         return f"Error: {str(error)}"
165 | 
166 | 
167 | # Global API client instance
168 | api_client = OneSignalAPIClient() 
```

--------------------------------------------------------------------------------
/tests/test_onesignal_server.py:
--------------------------------------------------------------------------------

```python
  1 | import unittest
  2 | from unittest.mock import patch, MagicMock
  3 | import os
  4 | import sys
  5 | import json
  6 | import asyncio
  7 | 
  8 | # Add the parent directory to sys.path to import the server module
  9 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
 10 | 
 11 | # Import the server module
 12 | import onesignal_server
 13 | 
 14 | class TestOneSignalServer(unittest.TestCase):
 15 |     """Test cases for the OneSignal MCP server."""
 16 |     
 17 |     def setUp(self):
 18 |         """Set up test environment before each test."""
 19 |         # Mock environment variables
 20 |         self.env_patcher = patch.dict('os.environ', {
 21 |             'ONESIGNAL_APP_ID': 'test-app-id',
 22 |             'ONESIGNAL_API_KEY': 'test-api-key',
 23 |             'ONESIGNAL_ORG_API_KEY': 'test-org-api-key'
 24 |         })
 25 |         self.env_patcher.start()
 26 |         
 27 |         # Reset app configurations for each test
 28 |         onesignal_server.app_configs = {}
 29 |         onesignal_server.current_app_key = None
 30 |         
 31 |         # Initialize with test app
 32 |         onesignal_server.add_app_config('test', 'test-app-id', 'test-api-key', 'Test App')
 33 |         onesignal_server.current_app_key = 'test'
 34 |     
 35 |     def tearDown(self):
 36 |         """Clean up after each test."""
 37 |         self.env_patcher.stop()
 38 |     
 39 |     def test_app_config(self):
 40 |         """Test AppConfig class."""
 41 |         app = onesignal_server.AppConfig('app-id', 'api-key', 'App Name')
 42 |         self.assertEqual(app.app_id, 'app-id')
 43 |         self.assertEqual(app.api_key, 'api-key')
 44 |         self.assertEqual(app.name, 'App Name')
 45 |         self.assertEqual(str(app), 'App Name (app-id)')
 46 |     
 47 |     def test_add_app_config(self):
 48 |         """Test adding app configurations."""
 49 |         onesignal_server.add_app_config('new-app', 'new-app-id', 'new-api-key', 'New App')
 50 |         self.assertIn('new-app', onesignal_server.app_configs)
 51 |         self.assertEqual(onesignal_server.app_configs['new-app'].app_id, 'new-app-id')
 52 |         self.assertEqual(onesignal_server.app_configs['new-app'].api_key, 'new-api-key')
 53 |         self.assertEqual(onesignal_server.app_configs['new-app'].name, 'New App')
 54 |     
 55 |     def test_set_current_app(self):
 56 |         """Test setting the current app."""
 57 |         # Add a second app
 58 |         onesignal_server.add_app_config('second', 'second-app-id', 'second-api-key')
 59 |         
 60 |         # Test switching to an existing app
 61 |         result = onesignal_server.set_current_app('second')
 62 |         self.assertTrue(result)
 63 |         self.assertEqual(onesignal_server.current_app_key, 'second')
 64 |         
 65 |         # Test switching to a non-existent app
 66 |         result = onesignal_server.set_current_app('non-existent')
 67 |         self.assertFalse(result)
 68 |         self.assertEqual(onesignal_server.current_app_key, 'second')  # Should not change
 69 |     
 70 |     def test_get_current_app(self):
 71 |         """Test getting the current app configuration."""
 72 |         current_app = onesignal_server.get_current_app()
 73 |         self.assertIsNotNone(current_app)
 74 |         self.assertEqual(current_app.app_id, 'test-app-id')
 75 |         self.assertEqual(current_app.api_key, 'test-api-key')
 76 |         
 77 |         # Test with no current app
 78 |         onesignal_server.current_app_key = None
 79 |         current_app = onesignal_server.get_current_app()
 80 |         self.assertIsNone(current_app)
 81 |     
 82 |     @patch('requests.get')
 83 |     def test_make_onesignal_request_get(self, mock_get):
 84 |         """Test making a GET request to the OneSignal API."""
 85 |         # Mock the response
 86 |         mock_response = MagicMock()
 87 |         mock_response.json.return_value = {'success': True}
 88 |         mock_response.text = json.dumps({'success': True})
 89 |         mock_get.return_value = mock_response
 90 |         
 91 |         # Make the request and run it through the event loop
 92 |         loop = asyncio.new_event_loop()
 93 |         asyncio.set_event_loop(loop)
 94 |         try:
 95 |             result = loop.run_until_complete(
 96 |                 onesignal_server.make_onesignal_request('notifications', 'GET', params={'limit': 10})
 97 |             )
 98 |             
 99 |             # Check that the request was made correctly
100 |             mock_get.assert_called_once()
101 |             args, kwargs = mock_get.call_args
102 |             self.assertEqual(kwargs['headers']['Authorization'], 'Key test-api-key')
103 |             self.assertEqual(kwargs['params']['app_id'], 'test-app-id')
104 |             self.assertEqual(kwargs['params']['limit'], 10)
105 |             
106 |             # Check the result
107 |             self.assertEqual(result, {'success': True})
108 |         finally:
109 |             loop.close()
110 |     
111 |     @patch('requests.post')
112 |     def test_make_onesignal_request_post(self, mock_post):
113 |         """Test making a POST request to the OneSignal API."""
114 |         # Mock the response
115 |         mock_response = MagicMock()
116 |         mock_response.json.return_value = {'id': 'notification-id'}
117 |         mock_response.text = json.dumps({'id': 'notification-id'})
118 |         mock_post.return_value = mock_response
119 |         
120 |         # Make the request
121 |         data = {'contents': {'en': 'Test message'}}
122 |         loop = asyncio.new_event_loop()
123 |         asyncio.set_event_loop(loop)
124 |         try:
125 |             result = loop.run_until_complete(
126 |                 onesignal_server.make_onesignal_request('notifications', 'POST', data=data)
127 |             )
128 |             
129 |             # Check that the request was made correctly
130 |             mock_post.assert_called_once()
131 |             args, kwargs = mock_post.call_args
132 |             self.assertEqual(kwargs['headers']['Authorization'], 'Key test-api-key')
133 |             self.assertEqual(kwargs['json']['app_id'], 'test-app-id')
134 |             self.assertEqual(kwargs['json']['contents']['en'], 'Test message')
135 |             
136 |             # Check the result
137 |             self.assertEqual(result, {'id': 'notification-id'})
138 |         finally:
139 |             loop.close()
140 |     
141 |     @patch('requests.get')
142 |     @patch('onesignal_server.ONESIGNAL_ORG_API_KEY', 'test-org-api-key')
143 |     def test_make_onesignal_request_with_org_key(self, mock_get):
144 |         """Test making a request with the organization API key."""
145 |         # Mock the response
146 |         mock_response = MagicMock()
147 |         mock_response.json.return_value = {'apps': []}
148 |         mock_response.text = json.dumps({'apps': []})
149 |         mock_get.return_value = mock_response
150 |         
151 |         # Make the request
152 |         loop = asyncio.new_event_loop()
153 |         asyncio.set_event_loop(loop)
154 |         try:
155 |             result = loop.run_until_complete(
156 |                 onesignal_server.make_onesignal_request('apps', 'GET', use_org_key=True)
157 |             )
158 |             
159 |             # Check that the request was made correctly
160 |             mock_get.assert_called_once()
161 |             args, kwargs = mock_get.call_args
162 |             self.assertEqual(kwargs['headers']['Authorization'], 'Key test-org-api-key')
163 |             
164 |             # Check the result
165 |             self.assertEqual(result, {'apps': []})
166 |         finally:
167 |             loop.close()
168 |     
169 |     @patch('requests.get')
170 |     def test_make_onesignal_request_error_handling(self, mock_get):
171 |         """Test error handling in make_onesignal_request."""
172 |         # Mock a request exception
173 |         mock_get.side_effect = Exception('Test error')
174 |         
175 |         # Make the request
176 |         loop = asyncio.new_event_loop()
177 |         asyncio.set_event_loop(loop)
178 |         try:
179 |             result = loop.run_until_complete(
180 |                 onesignal_server.make_onesignal_request('notifications')
181 |             )
182 |             
183 |             # Check the result
184 |             self.assertIn('error', result)
185 |             self.assertEqual(result['error'], 'Unexpected error: Test error')
186 |         finally:
187 |             loop.close()
188 | 
189 | if __name__ == '__main__':
190 |     unittest.main()
191 | 
```

--------------------------------------------------------------------------------
/onesignal_refactored/tools/messages.py:
--------------------------------------------------------------------------------

```python
  1 | """Message management tools for OneSignal MCP server."""
  2 | import json
  3 | from typing import List, Dict, Any, Optional
  4 | from ..api_client import api_client, OneSignalAPIError
  5 | from ..config import app_manager
  6 | 
  7 | 
  8 | async def send_push_notification(
  9 |     title: str,
 10 |     message: str,
 11 |     segments: Optional[List[str]] = None,
 12 |     include_player_ids: Optional[List[str]] = None,
 13 |     external_ids: Optional[List[str]] = None,
 14 |     data: Optional[Dict[str, Any]] = None,
 15 |     **kwargs
 16 | ) -> Dict[str, Any]:
 17 |     """
 18 |     Send a push notification through OneSignal.
 19 |     
 20 |     Args:
 21 |         title: Notification title
 22 |         message: Notification message content
 23 |         segments: List of segments to include
 24 |         include_player_ids: List of specific player IDs to target
 25 |         external_ids: List of external user IDs to target
 26 |         data: Additional data to include with the notification
 27 |         **kwargs: Additional notification parameters
 28 |     """
 29 |     notification_data = {
 30 |         "contents": {"en": message},
 31 |         "headings": {"en": title},
 32 |         "target_channel": "push"
 33 |     }
 34 |     
 35 |     # Set targeting
 36 |     if not any([segments, include_player_ids, external_ids]):
 37 |         segments = ["Subscribed Users"]
 38 |     
 39 |     if segments:
 40 |         notification_data["included_segments"] = segments
 41 |     if include_player_ids:
 42 |         notification_data["include_player_ids"] = include_player_ids
 43 |     if external_ids:
 44 |         notification_data["include_external_user_ids"] = external_ids
 45 |     
 46 |     if data:
 47 |         notification_data["data"] = data
 48 |     
 49 |     # Add any additional parameters
 50 |     notification_data.update(kwargs)
 51 |     
 52 |     return await api_client.request("notifications", method="POST", data=notification_data)
 53 | 
 54 | 
 55 | async def send_email(
 56 |     subject: str,
 57 |     body: str,
 58 |     email_body: Optional[str] = None,
 59 |     segments: Optional[List[str]] = None,
 60 |     include_emails: Optional[List[str]] = None,
 61 |     external_ids: Optional[List[str]] = None,
 62 |     template_id: Optional[str] = None,
 63 |     **kwargs
 64 | ) -> Dict[str, Any]:
 65 |     """
 66 |     Send an email through OneSignal.
 67 |     
 68 |     Args:
 69 |         subject: Email subject line
 70 |         body: Plain text email content
 71 |         email_body: HTML email content (optional)
 72 |         segments: List of segments to include
 73 |         include_emails: List of specific email addresses to target
 74 |         external_ids: List of external user IDs to target
 75 |         template_id: Email template ID to use
 76 |         **kwargs: Additional email parameters
 77 |     """
 78 |     email_data = {
 79 |         "email_subject": subject,
 80 |         "email_body": email_body or body,
 81 |         "target_channel": "email"
 82 |     }
 83 |     
 84 |     # Set targeting
 85 |     if include_emails:
 86 |         email_data["include_emails"] = include_emails
 87 |     elif external_ids:
 88 |         email_data["include_external_user_ids"] = external_ids
 89 |     elif segments:
 90 |         email_data["included_segments"] = segments
 91 |     else:
 92 |         email_data["included_segments"] = ["Subscribed Users"]
 93 |     
 94 |     if template_id:
 95 |         email_data["template_id"] = template_id
 96 |     
 97 |     # Add any additional parameters
 98 |     email_data.update(kwargs)
 99 |     
100 |     return await api_client.request("notifications", method="POST", data=email_data)
101 | 
102 | 
103 | async def send_sms(
104 |     message: str,
105 |     phone_numbers: Optional[List[str]] = None,
106 |     segments: Optional[List[str]] = None,
107 |     external_ids: Optional[List[str]] = None,
108 |     media_url: Optional[str] = None,
109 |     **kwargs
110 | ) -> Dict[str, Any]:
111 |     """
112 |     Send an SMS through OneSignal.
113 |     
114 |     Args:
115 |         message: SMS message content
116 |         phone_numbers: List of phone numbers in E.164 format
117 |         segments: List of segments to include
118 |         external_ids: List of external user IDs to target
119 |         media_url: URL for MMS media attachment
120 |         **kwargs: Additional SMS parameters
121 |     """
122 |     sms_data = {
123 |         "contents": {"en": message},
124 |         "target_channel": "sms"
125 |     }
126 |     
127 |     # Set targeting
128 |     if phone_numbers:
129 |         sms_data["include_phone_numbers"] = phone_numbers
130 |     elif external_ids:
131 |         sms_data["include_external_user_ids"] = external_ids
132 |     elif segments:
133 |         sms_data["included_segments"] = segments
134 |     else:
135 |         raise OneSignalAPIError(
136 |             "SMS requires phone_numbers, external_ids, or segments to be specified"
137 |         )
138 |     
139 |     if media_url:
140 |         sms_data["mms_media_url"] = media_url
141 |     
142 |     # Add any additional parameters
143 |     sms_data.update(kwargs)
144 |     
145 |     return await api_client.request("notifications", method="POST", data=sms_data)
146 | 
147 | 
148 | async def send_transactional_message(
149 |     channel: str,
150 |     content: Dict[str, str],
151 |     recipients: Dict[str, Any],
152 |     template_id: Optional[str] = None,
153 |     custom_data: Optional[Dict[str, Any]] = None,
154 |     **kwargs
155 | ) -> Dict[str, Any]:
156 |     """
157 |     Send a transactional message (immediate delivery, no scheduling).
158 |     
159 |     Args:
160 |         channel: Channel to send on ("push", "email", "sms")
161 |         content: Message content (format depends on channel)
162 |         recipients: Targeting information
163 |         template_id: Template ID to use
164 |         custom_data: Custom data to include
165 |         **kwargs: Additional parameters
166 |     """
167 |     message_data = {
168 |         "target_channel": channel,
169 |         "is_transactional": True
170 |     }
171 |     
172 |     # Set content based on channel
173 |     if channel == "email":
174 |         message_data["email_subject"] = content.get("subject", "")
175 |         message_data["email_body"] = content.get("body", "")
176 |     else:
177 |         message_data["contents"] = content
178 |     
179 |     # Set recipients
180 |     message_data.update(recipients)
181 |     
182 |     if template_id:
183 |         message_data["template_id"] = template_id
184 |     
185 |     if custom_data:
186 |         message_data["data"] = custom_data
187 |     
188 |     # Add any additional parameters
189 |     message_data.update(kwargs)
190 |     
191 |     return await api_client.request("notifications", method="POST", data=message_data)
192 | 
193 | 
194 | async def view_messages(
195 |     limit: int = 20,
196 |     offset: int = 0,
197 |     kind: Optional[int] = None
198 | ) -> Dict[str, Any]:
199 |     """
200 |     View recent messages sent through OneSignal.
201 |     
202 |     Args:
203 |         limit: Maximum number of messages to return (max: 50)
204 |         offset: Result offset for pagination
205 |         kind: Filter by message type (0=Dashboard, 1=API, 3=Automated)
206 |     """
207 |     params = {"limit": min(limit, 50), "offset": offset}
208 |     if kind is not None:
209 |         params["kind"] = kind
210 |     
211 |     return await api_client.request("notifications", method="GET", params=params)
212 | 
213 | 
214 | async def view_message_details(message_id: str) -> Dict[str, Any]:
215 |     """Get detailed information about a specific message."""
216 |     return await api_client.request(f"notifications/{message_id}", method="GET")
217 | 
218 | 
219 | async def cancel_message(message_id: str) -> Dict[str, Any]:
220 |     """Cancel a scheduled message that hasn't been delivered yet."""
221 |     return await api_client.request(f"notifications/{message_id}", method="DELETE")
222 | 
223 | 
224 | async def view_message_history(message_id: str, event: str) -> Dict[str, Any]:
225 |     """
226 |     View the history/recipients of a message based on events.
227 |     
228 |     Args:
229 |         message_id: The ID of the message
230 |         event: The event type to track (e.g., 'sent', 'clicked')
231 |     """
232 |     app_config = app_manager.get_current_app()
233 |     if not app_config:
234 |         raise OneSignalAPIError("No app currently selected")
235 |     
236 |     data = {
237 |         "app_id": app_config.app_id,
238 |         "events": event,
239 |         "email": f"{app_config.name}[email protected]"
240 |     }
241 |     
242 |     return await api_client.request(
243 |         f"notifications/{message_id}/history", 
244 |         method="POST", 
245 |         data=data
246 |     )
247 | 
248 | 
249 | async def export_messages_csv(
250 |     start_date: Optional[str] = None,
251 |     end_date: Optional[str] = None,
252 |     **kwargs
253 | ) -> Dict[str, Any]:
254 |     """
255 |     Export messages to CSV.
256 |     
257 |     Args:
258 |         start_date: Start date for export (ISO 8601 format)
259 |         end_date: End date for export (ISO 8601 format)
260 |         **kwargs: Additional export parameters
261 |     """
262 |     data = {}
263 |     if start_date:
264 |         data["start_date"] = start_date
265 |     if end_date:
266 |         data["end_date"] = end_date
267 |     
268 |     data.update(kwargs)
269 |     
270 |     return await api_client.request(
271 |         "notifications/csv_export",
272 |         method="POST",
273 |         data=data,
274 |         use_org_key=True
275 |     ) 
```

--------------------------------------------------------------------------------
/test_onesignal_mcp.py:
--------------------------------------------------------------------------------

```python
  1 | #!/usr/bin/env python3
  2 | """
  3 | Test script for OneSignal MCP Server
  4 | This script tests all the available functions in the OneSignal MCP server.
  5 | 
  6 | Usage:
  7 | 1. Set up your environment variables in .env file:
  8 |    - ONESIGNAL_APP_ID
  9 |    - ONESIGNAL_API_KEY
 10 |    - ONESIGNAL_ORG_API_KEY
 11 | 
 12 | 2. Run the OneSignal MCP server:
 13 |    python onesignal_server.py
 14 | 
 15 | 3. In another terminal, run this test script:
 16 |    python test_onesignal_mcp.py
 17 | """
 18 | 
 19 | import asyncio
 20 | import json
 21 | import sys
 22 | from datetime import datetime
 23 | from typing import Dict, Any, List
 24 | 
 25 | # Test configuration
 26 | TEST_CONFIG = {
 27 |     "test_email": "[email protected]",
 28 |     "test_phone": "+15551234567",  # E.164 format
 29 |     "test_external_id": "test_user_123",
 30 |     "test_player_id": None,  # Will be populated during tests
 31 |     "test_template_id": None,  # Will be populated during tests
 32 |     "test_user_id": None,  # Will be populated during tests
 33 |     "test_subscription_id": None,  # Will be populated during tests
 34 | }
 35 | 
 36 | # Test results tracking
 37 | test_results = {
 38 |     "passed": 0,
 39 |     "failed": 0,
 40 |     "skipped": 0,
 41 |     "errors": []
 42 | }
 43 | 
 44 | def print_test_header(test_name: str):
 45 |     """Print a formatted test header."""
 46 |     print(f"\n{'='*60}")
 47 |     print(f"Testing: {test_name}")
 48 |     print(f"{'='*60}")
 49 | 
 50 | def print_result(success: bool, message: str, result: Any = None):
 51 |     """Print test result with formatting."""
 52 |     if success:
 53 |         print(f"✅ {message}")
 54 |         test_results["passed"] += 1
 55 |     else:
 56 |         print(f"❌ {message}")
 57 |         test_results["failed"] += 1
 58 |         if result:
 59 |             test_results["errors"].append({
 60 |                 "test": message,
 61 |                 "error": result
 62 |             })
 63 |     
 64 |     if result and isinstance(result, dict):
 65 |         print(f"   Response: {json.dumps(result, indent=2)}")
 66 | 
 67 | async def test_app_management():
 68 |     """Test app management functions."""
 69 |     print_test_header("App Management")
 70 |     
 71 |     # Test listing apps
 72 |     print("\n1. Testing list_apps...")
 73 |     # Simulate function call - in real MCP, this would be via the MCP protocol
 74 |     # For testing, you'll need to call these through the MCP client
 75 |     print("   ⚠️  App management tests require MCP client implementation")
 76 |     test_results["skipped"] += 1
 77 | 
 78 | async def test_messaging():
 79 |     """Test messaging functions."""
 80 |     print_test_header("Messaging Functions")
 81 |     
 82 |     tests = [
 83 |         {
 84 |             "name": "Send Push Notification",
 85 |             "function": "send_push_notification",
 86 |             "params": {
 87 |                 "title": "Test Push",
 88 |                 "message": "This is a test push notification",
 89 |                 "segments": ["Subscribed Users"]
 90 |             }
 91 |         },
 92 |         {
 93 |             "name": "Send Email",
 94 |             "function": "send_email",
 95 |             "params": {
 96 |                 "subject": "Test Email",
 97 |                 "body": "This is a test email",
 98 |                 "include_emails": [TEST_CONFIG["test_email"]]
 99 |             }
100 |         },
101 |         {
102 |             "name": "Send SMS",
103 |             "function": "send_sms",
104 |             "params": {
105 |                 "message": "Test SMS message",
106 |                 "phone_numbers": [TEST_CONFIG["test_phone"]]
107 |             }
108 |         },
109 |         {
110 |             "name": "Send Transactional Message",
111 |             "function": "send_transactional_message",
112 |             "params": {
113 |                 "channel": "push",
114 |                 "content": {"en": "Transactional test"},
115 |                 "recipients": {"include_external_user_ids": [TEST_CONFIG["test_external_id"]]}
116 |             }
117 |         }
118 |     ]
119 |     
120 |     for test in tests:
121 |         print(f"\n{test['name']}...")
122 |         print(f"   Function: {test['function']}")
123 |         print(f"   Params: {json.dumps(test['params'], indent=6)}")
124 |         print("   ⚠️  Requires MCP client to execute")
125 |         test_results["skipped"] += 1
126 | 
127 | async def test_templates():
128 |     """Test template management functions."""
129 |     print_test_header("Template Management")
130 |     
131 |     # Create template test
132 |     print("\n1. Create Template")
133 |     create_params = {
134 |         "name": f"Test Template {datetime.now().strftime('%Y%m%d_%H%M%S')}",
135 |         "title": "Test Template Title",
136 |         "message": "Test template message content"
137 |     }
138 |     print(f"   Params: {json.dumps(create_params, indent=6)}")
139 |     
140 |     # Update template test
141 |     print("\n2. Update Template")
142 |     if TEST_CONFIG["test_template_id"]:
143 |         update_params = {
144 |             "template_id": TEST_CONFIG["test_template_id"],
145 |             "name": "Updated Test Template",
146 |             "title": "Updated Title"
147 |         }
148 |         print(f"   Params: {json.dumps(update_params, indent=6)}")
149 |     else:
150 |         print("   ⚠️  Skipped: No template ID available")
151 |     
152 |     # Delete template test
153 |     print("\n3. Delete Template")
154 |     print("   ⚠️  Skipped: Preserving test template")
155 |     
156 |     test_results["skipped"] += 3
157 | 
158 | async def test_live_activities():
159 |     """Test iOS Live Activities functions."""
160 |     print_test_header("iOS Live Activities")
161 |     
162 |     activity_tests = [
163 |         {
164 |             "name": "Start Live Activity",
165 |             "params": {
166 |                 "activity_id": "test_activity_123",
167 |                 "push_token": "test_push_token",
168 |                 "subscription_id": "test_subscription",
169 |                 "activity_attributes": {"event": "Test Event"},
170 |                 "content_state": {"status": "active"}
171 |             }
172 |         },
173 |         {
174 |             "name": "Update Live Activity",
175 |             "params": {
176 |                 "activity_id": "test_activity_123",
177 |                 "name": "test_update",
178 |                 "event": "update",
179 |                 "content_state": {"status": "updated"}
180 |             }
181 |         },
182 |         {
183 |             "name": "End Live Activity",
184 |             "params": {
185 |                 "activity_id": "test_activity_123",
186 |                 "subscription_id": "test_subscription"
187 |             }
188 |         }
189 |     ]
190 |     
191 |     for test in activity_tests:
192 |         print(f"\n{test['name']}...")
193 |         print(f"   Params: {json.dumps(test['params'], indent=6)}")
194 |         print("   ⚠️  Requires iOS app with Live Activities support")
195 |         test_results["skipped"] += 1
196 | 
197 | async def test_analytics():
198 |     """Test analytics and export functions."""
199 |     print_test_header("Analytics & Export")
200 |     
201 |     # View outcomes
202 |     print("\n1. View Outcomes")
203 |     outcomes_params = {
204 |         "outcome_names": ["session_duration", "purchase"],
205 |         "outcome_time_range": "1d",
206 |         "outcome_platforms": ["ios", "android"]
207 |     }
208 |     print(f"   Params: {json.dumps(outcomes_params, indent=6)}")
209 |     
210 |     # Export functions
211 |     export_tests = [
212 |         {
213 |             "name": "Export Players CSV",
214 |             "params": {
215 |                 "start_date": "2024-01-01T00:00:00Z",
216 |                 "end_date": "2024-12-31T23:59:59Z"
217 |             }
218 |         },
219 |         {
220 |             "name": "Export Messages CSV",
221 |             "params": {
222 |                 "start_date": "2024-01-01T00:00:00Z",
223 |                 "end_date": "2024-12-31T23:59:59Z"
224 |             }
225 |         }
226 |     ]
227 |     
228 |     for test in export_tests:
229 |         print(f"\n{test['name']}...")
230 |         print(f"   Params: {json.dumps(test['params'], indent=6)}")
231 |         print("   ⚠️  Requires Organization API Key")
232 |         test_results["skipped"] += 1
233 | 
234 | async def test_user_management():
235 |     """Test user management functions."""
236 |     print_test_header("User Management")
237 |     
238 |     # Create user
239 |     print("\n1. Create User")
240 |     create_user_params = {
241 |         "name": "Test User",
242 |         "email": TEST_CONFIG["test_email"],
243 |         "external_id": TEST_CONFIG["test_external_id"],
244 |         "tags": {"test": "true", "created": datetime.now().isoformat()}
245 |     }
246 |     print(f"   Params: {json.dumps(create_user_params, indent=6)}")
247 |     
248 |     # Other user operations
249 |     user_tests = [
250 |         "View User",
251 |         "Update User",
252 |         "View User Identity",
253 |         "Create/Update Alias",
254 |         "Delete Alias"
255 |     ]
256 |     
257 |     for test in user_tests:
258 |         print(f"\n{test}")
259 |         print(f"   ⚠️  Requires valid user_id from create_user")
260 |         test_results["skipped"] += 1
261 | 
262 | async def test_player_management():
263 |     """Test player/device management functions."""
264 |     print_test_header("Player/Device Management")
265 |     
266 |     # Add player
267 |     print("\n1. Add Player")
268 |     add_player_params = {
269 |         "device_type": 1,  # Android
270 |         "identifier": "test_device_token",
271 |         "language": "en",
272 |         "tags": {"test_device": "true"}
273 |     }
274 |     print(f"   Params: {json.dumps(add_player_params, indent=6)}")
275 |     
276 |     # Other player operations
277 |     player_tests = [
278 |         "Edit Player",
279 |         "Delete Player",
280 |         "Edit Tags with External User ID"
281 |     ]
282 |     
283 |     for test in player_tests:
284 |         print(f"\n{test}")
285 |         print(f"   ⚠️  Requires valid player_id")
286 |         test_results["skipped"] += 1
287 | 
288 | async def test_subscription_management():
289 |     """Test subscription management functions."""
290 |     print_test_header("Subscription Management")
291 |     
292 |     subscription_tests = [
293 |         {
294 |             "name": "Create Subscription",
295 |             "params": {
296 |                 "user_id": "test_user_id",
297 |                 "subscription_type": "email",
298 |                 "identifier": TEST_CONFIG["test_email"]
299 |             }
300 |         },
301 |         {
302 |             "name": "Update Subscription",
303 |             "params": {
304 |                 "user_id": "test_user_id",
305 |                 "subscription_id": "test_subscription_id",
306 |                 "enabled": False
307 |             }
308 |         },
309 |         {
310 |             "name": "Transfer Subscription",
311 |             "params": {
312 |                 "user_id": "test_user_id",
313 |                 "subscription_id": "test_subscription_id",
314 |                 "new_user_id": "new_test_user_id"
315 |             }
316 |         },
317 |         {
318 |             "name": "Delete Subscription",
319 |             "params": {
320 |                 "user_id": "test_user_id",
321 |                 "subscription_id": "test_subscription_id"
322 |             }
323 |         }
324 |     ]
325 |     
326 |     for test in subscription_tests:
327 |         print(f"\n{test['name']}...")
328 |         print(f"   Params: {json.dumps(test['params'], indent=6)}")
329 |         print("   ⚠️  Requires valid user_id and subscription_id")
330 |         test_results["skipped"] += 1
331 | 
332 | async def test_api_key_management():
333 |     """Test API key management functions."""
334 |     print_test_header("API Key Management")
335 |     
336 |     print("\n⚠️  API Key management requires Organization API Key")
337 |     print("   and should be tested carefully to avoid breaking access")
338 |     
339 |     api_key_tests = [
340 |         "View App API Keys",
341 |         "Create App API Key",
342 |         "Update App API Key",
343 |         "Rotate App API Key",
344 |         "Delete App API Key"
345 |     ]
346 |     
347 |     for test in api_key_tests:
348 |         print(f"\n{test}")
349 |         print("   ⚠️  Skipped for safety")
350 |         test_results["skipped"] += 1
351 | 
352 | def print_summary():
353 |     """Print test summary."""
354 |     print(f"\n\n{'='*60}")
355 |     print("TEST SUMMARY")
356 |     print(f"{'='*60}")
357 |     print(f"✅ Passed:  {test_results['passed']}")
358 |     print(f"❌ Failed:  {test_results['failed']}")
359 |     print(f"⚠️  Skipped: {test_results['skipped']}")
360 |     
361 |     if test_results["errors"]:
362 |         print(f"\n\nERRORS:")
363 |         for error in test_results["errors"]:
364 |             print(f"\n- {error['test']}")
365 |             print(f"  Error: {error['error']}")
366 | 
367 | async def main():
368 |     """Run all tests."""
369 |     print("OneSignal MCP Server Test Suite")
370 |     print("================================")
371 |     print("\nNOTE: This test script shows the structure of all available functions.")
372 |     print("To actually execute these tests, you need to:")
373 |     print("1. Run the OneSignal MCP server")
374 |     print("2. Use an MCP client to connect and call the functions")
375 |     print("3. Have valid OneSignal API credentials in your .env file")
376 |     
377 |     # Run test categories
378 |     await test_app_management()
379 |     await test_messaging()
380 |     await test_templates()
381 |     await test_live_activities()
382 |     await test_analytics()
383 |     await test_user_management()
384 |     await test_player_management()
385 |     await test_subscription_management()
386 |     await test_api_key_management()
387 |     
388 |     # Print summary
389 |     print_summary()
390 | 
391 | if __name__ == "__main__":
392 |     asyncio.run(main()) 
```

--------------------------------------------------------------------------------
/implementation_examples.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Implementation Examples for Missing OneSignal API Endpoints
  2 | 
  3 | This document provides concrete examples of how to add the missing API endpoints to the current `onesignal_server.py` file without requiring a full refactor.
  4 | 
  5 | ## 1. Email Sending Endpoint
  6 | 
  7 | Add this function after the existing `send_push_notification` function:
  8 | 
  9 | ```python
 10 | @mcp.tool()
 11 | async def send_email(subject: str, body: str, email_body: str = None, 
 12 |                      include_emails: List[str] = None, segments: List[str] = None,
 13 |                      external_ids: List[str] = None, template_id: str = None) -> Dict[str, Any]:
 14 |     """Send an email through OneSignal.
 15 |     
 16 |     Args:
 17 |         subject: Email subject line
 18 |         body: Plain text email content  
 19 |         email_body: HTML email content (optional)
 20 |         include_emails: List of specific email addresses to target
 21 |         segments: List of segments to include
 22 |         external_ids: List of external user IDs to target
 23 |         template_id: Email template ID to use
 24 |     """
 25 |     app_config = get_current_app()
 26 |     if not app_config:
 27 |         return {"error": "No app currently selected. Use switch_app to select an app."}
 28 |     
 29 |     email_data = {
 30 |         "app_id": app_config.app_id,
 31 |         "email_subject": subject,
 32 |         "email_body": email_body or body,
 33 |         "target_channel": "email"
 34 |     }
 35 |     
 36 |     # Set targeting
 37 |     if include_emails:
 38 |         email_data["include_emails"] = include_emails
 39 |     elif external_ids:
 40 |         email_data["include_external_user_ids"] = external_ids
 41 |     elif segments:
 42 |         email_data["included_segments"] = segments
 43 |     else:
 44 |         email_data["included_segments"] = ["Subscribed Users"]
 45 |     
 46 |     if template_id:
 47 |         email_data["template_id"] = template_id
 48 |     
 49 |     result = await make_onesignal_request("notifications", method="POST", data=email_data)
 50 |     return result
 51 | ```
 52 | 
 53 | ## 2. SMS Sending Endpoint
 54 | 
 55 | ```python
 56 | @mcp.tool()
 57 | async def send_sms(message: str, phone_numbers: List[str] = None, 
 58 |                    segments: List[str] = None, external_ids: List[str] = None,
 59 |                    media_url: str = None) -> Dict[str, Any]:
 60 |     """Send an SMS/MMS through OneSignal.
 61 |     
 62 |     Args:
 63 |         message: SMS message content
 64 |         phone_numbers: List of phone numbers in E.164 format
 65 |         segments: List of segments to include
 66 |         external_ids: List of external user IDs to target
 67 |         media_url: URL for MMS media attachment
 68 |     """
 69 |     app_config = get_current_app()
 70 |     if not app_config:
 71 |         return {"error": "No app currently selected. Use switch_app to select an app."}
 72 |     
 73 |     sms_data = {
 74 |         "app_id": app_config.app_id,
 75 |         "contents": {"en": message},
 76 |         "target_channel": "sms"
 77 |     }
 78 |     
 79 |     if phone_numbers:
 80 |         sms_data["include_phone_numbers"] = phone_numbers
 81 |     elif external_ids:
 82 |         sms_data["include_external_user_ids"] = external_ids
 83 |     elif segments:
 84 |         sms_data["included_segments"] = segments
 85 |     else:
 86 |         return {"error": "SMS requires phone_numbers, external_ids, or segments"}
 87 |     
 88 |     if media_url:
 89 |         sms_data["mms_media_url"] = media_url
 90 |     
 91 |     result = await make_onesignal_request("notifications", method="POST", data=sms_data)
 92 |     return result
 93 | ```
 94 | 
 95 | ## 3. Template Management Completions
 96 | 
 97 | ### Update Template
 98 | ```python
 99 | @mcp.tool()
100 | async def update_template(template_id: str, name: str = None, 
101 |                          title: str = None, message: str = None) -> Dict[str, Any]:
102 |     """Update an existing template.
103 |     
104 |     Args:
105 |         template_id: ID of the template to update
106 |         name: New name for the template
107 |         title: New title/heading for the template
108 |         message: New content/message for the template
109 |     """
110 |     data = {}
111 |     
112 |     if name:
113 |         data["name"] = name
114 |     if title:
115 |         data["headings"] = {"en": title}
116 |     if message:
117 |         data["contents"] = {"en": message}
118 |     
119 |     if not data:
120 |         return {"error": "No update parameters provided"}
121 |     
122 |     result = await make_onesignal_request(f"templates/{template_id}", 
123 |                                         method="PATCH", data=data)
124 |     return result
125 | ```
126 | 
127 | ### Delete Template
128 | ```python
129 | @mcp.tool()
130 | async def delete_template(template_id: str) -> Dict[str, Any]:
131 |     """Delete a template from your OneSignal app.
132 |     
133 |     Args:
134 |         template_id: ID of the template to delete
135 |     """
136 |     result = await make_onesignal_request(f"templates/{template_id}", 
137 |                                         method="DELETE")
138 |     if "error" not in result:
139 |         return {"success": f"Template '{template_id}' deleted successfully"}
140 |     return result
141 | ```
142 | 
143 | ## 4. Live Activities
144 | 
145 | ### Start Live Activity
146 | ```python
147 | @mcp.tool()
148 | async def start_live_activity(activity_id: str, push_token: str, 
149 |                              subscription_id: str, activity_attributes: Dict[str, Any],
150 |                              content_state: Dict[str, Any]) -> Dict[str, Any]:
151 |     """Start a new iOS Live Activity.
152 |     
153 |     Args:
154 |         activity_id: Unique identifier for the activity
155 |         push_token: Push token for the Live Activity
156 |         subscription_id: Subscription ID for the user
157 |         activity_attributes: Static attributes for the activity
158 |         content_state: Initial dynamic content state
159 |     """
160 |     data = {
161 |         "activity_id": activity_id,
162 |         "push_token": push_token,
163 |         "subscription_id": subscription_id,
164 |         "activity_attributes": activity_attributes,
165 |         "content_state": content_state
166 |     }
167 |     
168 |     result = await make_onesignal_request(f"live_activities/{activity_id}/start",
169 |                                         method="POST", data=data)
170 |     return result
171 | ```
172 | 
173 | ### Update Live Activity
174 | ```python
175 | @mcp.tool()
176 | async def update_live_activity(activity_id: str, name: str, event: str,
177 |                               content_state: Dict[str, Any], 
178 |                               dismissal_date: int = None) -> Dict[str, Any]:
179 |     """Update an existing iOS Live Activity.
180 |     
181 |     Args:
182 |         activity_id: ID of the activity to update
183 |         name: Name identifier for the update
184 |         event: Event type ("update" or "end")
185 |         content_state: Updated dynamic content state
186 |         dismissal_date: Unix timestamp for automatic dismissal
187 |     """
188 |     data = {
189 |         "name": name,
190 |         "event": event,
191 |         "content_state": content_state
192 |     }
193 |     
194 |     if dismissal_date:
195 |         data["dismissal_date"] = dismissal_date
196 |     
197 |     result = await make_onesignal_request(f"live_activities/{activity_id}/update",
198 |                                         method="POST", data=data)
199 |     return result
200 | ```
201 | 
202 | ## 5. Analytics & Outcomes
203 | 
204 | ### View Outcomes
205 | ```python
206 | @mcp.tool()
207 | async def view_outcomes(outcome_names: List[str], 
208 |                        outcome_time_range: str = None,
209 |                        outcome_platforms: List[str] = None) -> Dict[str, Any]:
210 |     """View outcomes data for your OneSignal app.
211 |     
212 |     Args:
213 |         outcome_names: List of outcome names to fetch data for
214 |         outcome_time_range: Time range for data (e.g., "1d", "1mo")
215 |         outcome_platforms: Filter by platforms (e.g., ["ios", "android"])
216 |     """
217 |     app_config = get_current_app()
218 |     if not app_config:
219 |         return {"error": "No app currently selected. Use switch_app to select an app."}
220 |     
221 |     params = {"outcome_names": outcome_names}
222 |     
223 |     if outcome_time_range:
224 |         params["outcome_time_range"] = outcome_time_range
225 |     if outcome_platforms:
226 |         params["outcome_platforms"] = outcome_platforms
227 |     
228 |     result = await make_onesignal_request(f"apps/{app_config.app_id}/outcomes",
229 |                                         method="GET", params=params)
230 |     return result
231 | ```
232 | 
233 | ## 6. Export Functions
234 | 
235 | ### Export Players CSV
236 | ```python
237 | @mcp.tool()
238 | async def export_players_csv(start_date: str = None, end_date: str = None,
239 |                             segment_names: List[str] = None) -> Dict[str, Any]:
240 |     """Export player/subscription data to CSV.
241 |     
242 |     Args:
243 |         start_date: Start date for export (ISO 8601 format)
244 |         end_date: End date for export (ISO 8601 format)
245 |         segment_names: List of segment names to export
246 |     """
247 |     data = {}
248 |     
249 |     if start_date:
250 |         data["start_date"] = start_date
251 |     if end_date:
252 |         data["end_date"] = end_date
253 |     if segment_names:
254 |         data["segment_names"] = segment_names
255 |     
256 |     result = await make_onesignal_request("players/csv_export", 
257 |                                         method="POST", data=data, use_org_key=True)
258 |     return result
259 | ```
260 | 
261 | ## 7. API Key Management
262 | 
263 | ### Delete API Key
264 | ```python
265 | @mcp.tool()
266 | async def delete_app_api_key(app_id: str, key_id: str) -> Dict[str, Any]:
267 |     """Delete an API key from a specific OneSignal app.
268 |     
269 |     Args:
270 |         app_id: The ID of the app
271 |         key_id: The ID of the API key to delete
272 |     """
273 |     result = await make_onesignal_request(f"apps/{app_id}/auth/tokens/{key_id}",
274 |                                         method="DELETE", use_org_key=True)
275 |     if "error" not in result:
276 |         return {"success": f"API key '{key_id}' deleted successfully"}
277 |     return result
278 | ```
279 | 
280 | ### Update API Key
281 | ```python
282 | @mcp.tool()
283 | async def update_app_api_key(app_id: str, key_id: str, name: str = None,
284 |                             scopes: List[str] = None) -> Dict[str, Any]:
285 |     """Update an API key for a specific OneSignal app.
286 |     
287 |     Args:
288 |         app_id: The ID of the app
289 |         key_id: The ID of the API key to update
290 |         name: New name for the API key
291 |         scopes: New list of permission scopes
292 |     """
293 |     data = {}
294 |     
295 |     if name:
296 |         data["name"] = name
297 |     if scopes:
298 |         data["scopes"] = scopes
299 |     
300 |     if not data:
301 |         return {"error": "No update parameters provided"}
302 |     
303 |     result = await make_onesignal_request(f"apps/{app_id}/auth/tokens/{key_id}",
304 |                                         method="PATCH", data=data, use_org_key=True)
305 |     return result
306 | ```
307 | 
308 | ## 8. Player/Device Management
309 | 
310 | ### Add a Player
311 | ```python
312 | @mcp.tool()
313 | async def add_player(device_type: int, identifier: str = None,
314 |                     language: str = None, tags: Dict[str, str] = None) -> Dict[str, Any]:
315 |     """Add a new player/device to OneSignal.
316 |     
317 |     Args:
318 |         device_type: Device type (0=iOS, 1=Android, etc.)
319 |         identifier: Push token or player ID
320 |         language: Language code (e.g., "en")
321 |         tags: Dictionary of tags to assign
322 |     """
323 |     app_config = get_current_app()
324 |     if not app_config:
325 |         return {"error": "No app currently selected. Use switch_app to select an app."}
326 |     
327 |     data = {
328 |         "app_id": app_config.app_id,
329 |         "device_type": device_type
330 |     }
331 |     
332 |     if identifier:
333 |         data["identifier"] = identifier
334 |     if language:
335 |         data["language"] = language
336 |     if tags:
337 |         data["tags"] = tags
338 |     
339 |     result = await make_onesignal_request("players", method="POST", data=data)
340 |     return result
341 | ```
342 | 
343 | ### Edit Player
344 | ```python
345 | @mcp.tool()
346 | async def edit_player(player_id: str, tags: Dict[str, str] = None,
347 |                      external_user_id: str = None, language: str = None) -> Dict[str, Any]:
348 |     """Edit an existing player/device.
349 |     
350 |     Args:
351 |         player_id: The player ID to edit
352 |         tags: Dictionary of tags to update
353 |         external_user_id: External user ID to assign
354 |         language: Language code to update
355 |     """
356 |     app_config = get_current_app()
357 |     if not app_config:
358 |         return {"error": "No app currently selected. Use switch_app to select an app."}
359 |     
360 |     data = {"app_id": app_config.app_id}
361 |     
362 |     if tags:
363 |         data["tags"] = tags
364 |     if external_user_id:
365 |         data["external_user_id"] = external_user_id
366 |     if language:
367 |         data["language"] = language
368 |     
369 |     if len(data) == 1:  # Only app_id
370 |         return {"error": "No update parameters provided"}
371 |     
372 |     result = await make_onesignal_request(f"players/{player_id}", 
373 |                                         method="PUT", data=data)
374 |     return result
375 | ```
376 | 
377 | ## Implementation Notes
378 | 
379 | 1. **Error Handling**: All functions should return consistent error formats
380 | 2. **Authentication**: Use `use_org_key=True` for organization-level endpoints
381 | 3. **Validation**: Add input validation where necessary
382 | 4. **Documentation**: Include clear docstrings with parameter descriptions
383 | 5. **Testing**: Test each endpoint with valid OneSignal credentials
384 | 
385 | These implementations can be added directly to the existing `onesignal_server.py` file to provide immediate support for the missing API endpoints. 
```

--------------------------------------------------------------------------------
/onesignal_refactored/server.py:
--------------------------------------------------------------------------------

```python
  1 | """OneSignal MCP Server - Refactored implementation."""
  2 | import os
  3 | import logging
  4 | from mcp.server.fastmcp import FastMCP, Context
  5 | from .config import app_manager
  6 | from .api_client import OneSignalAPIError
  7 | from .tools import (
  8 |     messages,
  9 |     templates,
 10 |     live_activities,
 11 |     analytics,
 12 | )
 13 | 
 14 | # Version information
 15 | __version__ = "2.0.0"
 16 | 
 17 | # Configure logging
 18 | logging.basicConfig(
 19 |     level=logging.INFO,
 20 |     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
 21 |     handlers=[logging.StreamHandler()]
 22 | )
 23 | logger = logging.getLogger("onesignal-mcp")
 24 | 
 25 | # Get log level from environment
 26 | log_level_str = os.getenv("LOG_LEVEL", "INFO").upper()
 27 | valid_log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
 28 | if log_level_str not in valid_log_levels:
 29 |     logger.warning(f"Invalid LOG_LEVEL '{log_level_str}'. Using INFO instead.")
 30 |     log_level_str = "INFO"
 31 | 
 32 | logger.setLevel(log_level_str)
 33 | 
 34 | # Initialize MCP server
 35 | mcp = FastMCP("onesignal-server", settings={"log_level": log_level_str})
 36 | logger.info(f"OneSignal MCP server v{__version__} initialized")
 37 | 
 38 | # === Configuration Resource ===
 39 | 
 40 | @mcp.resource("onesignal://config")
 41 | def get_onesignal_config() -> str:
 42 |     """Get information about the OneSignal configuration."""
 43 |     current_app = app_manager.get_current_app()
 44 |     app_list = "\n".join([
 45 |         f"- {key}: {app}" for key, app in app_manager.list_apps().items()
 46 |     ])
 47 |     
 48 |     return f"""
 49 | OneSignal Server Configuration:
 50 | Version: {__version__}
 51 | Current App: {current_app.name if current_app else 'None'}
 52 | 
 53 | Available Apps:
 54 | {app_list or "No apps configured"}
 55 | 
 56 | This refactored MCP server provides comprehensive tools for:
 57 | - Multi-channel messaging (Push, Email, SMS)
 58 | - Transactional messages
 59 | - Template management
 60 | - Live Activities (iOS)
 61 | - Analytics and outcomes
 62 | - User and subscription management
 63 | - App configuration management
 64 | - Data export functionality
 65 | """
 66 | 
 67 | # === App Management Tools ===
 68 | 
 69 | @mcp.tool()
 70 | async def list_apps() -> str:
 71 |     """List all configured OneSignal apps."""
 72 |     apps = app_manager.list_apps()
 73 |     if not apps:
 74 |         return "No apps configured. Use add_app to add a new app configuration."
 75 |     
 76 |     current_app = app_manager.get_current_app()
 77 |     result = ["Configured OneSignal Apps:"]
 78 |     
 79 |     for key, app in apps.items():
 80 |         current_marker = " (current)" if current_app and key == app_manager.current_app_key else ""
 81 |         result.append(f"- {key}: {app.name} (App ID: {app.app_id}){current_marker}")
 82 |     
 83 |     return "\n".join(result)
 84 | 
 85 | @mcp.tool()
 86 | async def add_app(key: str, app_id: str, api_key: str, name: str = None) -> str:
 87 |     """Add a new OneSignal app configuration locally."""
 88 |     if not all([key, app_id, api_key]):
 89 |         return "Error: All parameters (key, app_id, api_key) are required."
 90 |     
 91 |     if key in app_manager.list_apps():
 92 |         return f"Error: App key '{key}' already exists."
 93 |     
 94 |     app_manager.add_app(key, app_id, api_key, name)
 95 |     
 96 |     if len(app_manager.list_apps()) == 1:
 97 |         app_manager.set_current_app(key)
 98 |     
 99 |     return f"Successfully added app '{key}' with name '{name or key}'."
100 | 
101 | @mcp.tool()
102 | async def switch_app(key: str) -> str:
103 |     """Switch the current app to use for API requests."""
104 |     if app_manager.set_current_app(key):
105 |         app = app_manager.get_app(key)
106 |         return f"Switched to app '{key}' ({app.name})."
107 |     else:
108 |         available = ", ".join(app_manager.list_apps().keys()) or "None"
109 |         return f"Error: App key '{key}' not found. Available apps: {available}"
110 | 
111 | # === Messaging Tools ===
112 | 
113 | @mcp.tool()
114 | async def send_push_notification(
115 |     title: str,
116 |     message: str,
117 |     segments: list = None,
118 |     include_player_ids: list = None,
119 |     external_ids: list = None,
120 |     data: dict = None
121 | ) -> dict:
122 |     """Send a push notification through OneSignal."""
123 |     try:
124 |         return await messages.send_push_notification(
125 |             title=title,
126 |             message=message,
127 |             segments=segments,
128 |             include_player_ids=include_player_ids,
129 |             external_ids=external_ids,
130 |             data=data
131 |         )
132 |     except OneSignalAPIError as e:
133 |         return {"error": str(e)}
134 | 
135 | @mcp.tool()
136 | async def send_email(
137 |     subject: str,
138 |     body: str,
139 |     include_emails: list = None,
140 |     segments: list = None,
141 |     external_ids: list = None,
142 |     template_id: str = None
143 | ) -> dict:
144 |     """Send an email through OneSignal."""
145 |     try:
146 |         return await messages.send_email(
147 |             subject=subject,
148 |             body=body,
149 |             include_emails=include_emails,
150 |             segments=segments,
151 |             external_ids=external_ids,
152 |             template_id=template_id
153 |         )
154 |     except OneSignalAPIError as e:
155 |         return {"error": str(e)}
156 | 
157 | @mcp.tool()
158 | async def send_sms(
159 |     message: str,
160 |     phone_numbers: list = None,
161 |     segments: list = None,
162 |     external_ids: list = None,
163 |     media_url: str = None
164 | ) -> dict:
165 |     """Send an SMS/MMS through OneSignal."""
166 |     try:
167 |         return await messages.send_sms(
168 |             message=message,
169 |             phone_numbers=phone_numbers,
170 |             segments=segments,
171 |             external_ids=external_ids,
172 |             media_url=media_url
173 |         )
174 |     except OneSignalAPIError as e:
175 |         return {"error": str(e)}
176 | 
177 | @mcp.tool()
178 | async def send_transactional_message(
179 |     channel: str,
180 |     content: dict,
181 |     recipients: dict,
182 |     template_id: str = None,
183 |     custom_data: dict = None
184 | ) -> dict:
185 |     """Send a transactional message (immediate delivery)."""
186 |     try:
187 |         return await messages.send_transactional_message(
188 |             channel=channel,
189 |             content=content,
190 |             recipients=recipients,
191 |             template_id=template_id,
192 |             custom_data=custom_data
193 |         )
194 |     except OneSignalAPIError as e:
195 |         return {"error": str(e)}
196 | 
197 | @mcp.tool()
198 | async def view_messages(limit: int = 20, offset: int = 0, kind: int = None) -> dict:
199 |     """View recent messages sent through OneSignal."""
200 |     try:
201 |         return await messages.view_messages(limit=limit, offset=offset, kind=kind)
202 |     except OneSignalAPIError as e:
203 |         return {"error": str(e)}
204 | 
205 | @mcp.tool()
206 | async def view_message_details(message_id: str) -> dict:
207 |     """Get detailed information about a specific message."""
208 |     try:
209 |         return await messages.view_message_details(message_id)
210 |     except OneSignalAPIError as e:
211 |         return {"error": str(e)}
212 | 
213 | @mcp.tool()
214 | async def cancel_message(message_id: str) -> dict:
215 |     """Cancel a scheduled message."""
216 |     try:
217 |         return await messages.cancel_message(message_id)
218 |     except OneSignalAPIError as e:
219 |         return {"error": str(e)}
220 | 
221 | @mcp.tool()
222 | async def export_messages_csv(start_date: str = None, end_date: str = None) -> dict:
223 |     """Export messages to CSV (requires Organization API Key)."""
224 |     try:
225 |         return await messages.export_messages_csv(
226 |             start_date=start_date,
227 |             end_date=end_date
228 |         )
229 |     except OneSignalAPIError as e:
230 |         return {"error": str(e)}
231 | 
232 | # === Template Tools ===
233 | 
234 | @mcp.tool()
235 | async def create_template(name: str, title: str, message: str) -> dict:
236 |     """Create a new template."""
237 |     try:
238 |         result = await templates.create_template(name=name, title=title, message=message)
239 |         return {"success": f"Template '{name}' created with ID: {result.get('id')}"}
240 |     except Exception as e:
241 |         return {"error": str(e)}
242 | 
243 | @mcp.tool()
244 | async def update_template(
245 |     template_id: str,
246 |     name: str = None,
247 |     title: str = None,
248 |     message: str = None
249 | ) -> dict:
250 |     """Update an existing template."""
251 |     try:
252 |         await templates.update_template(
253 |             template_id=template_id,
254 |             name=name,
255 |             title=title,
256 |             message=message
257 |         )
258 |         return {"success": f"Template '{template_id}' updated successfully"}
259 |     except Exception as e:
260 |         return {"error": str(e)}
261 | 
262 | @mcp.tool()
263 | async def view_templates() -> str:
264 |     """List all templates."""
265 |     try:
266 |         result = await templates.view_templates()
267 |         return templates.format_template_list(result)
268 |     except Exception as e:
269 |         return f"Error: {str(e)}"
270 | 
271 | @mcp.tool()
272 | async def view_template_details(template_id: str) -> str:
273 |     """Get template details."""
274 |     try:
275 |         result = await templates.view_template_details(template_id)
276 |         return templates.format_template_details(result)
277 |     except Exception as e:
278 |         return f"Error: {str(e)}"
279 | 
280 | @mcp.tool()
281 | async def delete_template(template_id: str) -> dict:
282 |     """Delete a template."""
283 |     try:
284 |         await templates.delete_template(template_id)
285 |         return {"success": f"Template '{template_id}' deleted successfully"}
286 |     except Exception as e:
287 |         return {"error": str(e)}
288 | 
289 | @mcp.tool()
290 | async def copy_template_to_app(
291 |     template_id: str,
292 |     target_app_id: str,
293 |     new_name: str = None
294 | ) -> dict:
295 |     """Copy a template to another app."""
296 |     try:
297 |         result = await templates.copy_template_to_app(
298 |             template_id=template_id,
299 |             target_app_id=target_app_id,
300 |             new_name=new_name
301 |         )
302 |         return {"success": f"Template copied successfully. New ID: {result.get('id')}"}
303 |     except Exception as e:
304 |         return {"error": str(e)}
305 | 
306 | # === Live Activities Tools ===
307 | 
308 | @mcp.tool()
309 | async def start_live_activity(
310 |     activity_id: str,
311 |     push_token: str,
312 |     subscription_id: str,
313 |     activity_attributes: dict,
314 |     content_state: dict
315 | ) -> dict:
316 |     """Start a new iOS Live Activity."""
317 |     try:
318 |         return await live_activities.start_live_activity(
319 |             activity_id=activity_id,
320 |             push_token=push_token,
321 |             subscription_id=subscription_id,
322 |             activity_attributes=activity_attributes,
323 |             content_state=content_state
324 |         )
325 |     except OneSignalAPIError as e:
326 |         return {"error": str(e)}
327 | 
328 | @mcp.tool()
329 | async def update_live_activity(
330 |     activity_id: str,
331 |     name: str,
332 |     event: str,
333 |     content_state: dict,
334 |     dismissal_date: int = None,
335 |     priority: int = None,
336 |     sound: str = None
337 | ) -> dict:
338 |     """Update an iOS Live Activity."""
339 |     try:
340 |         return await live_activities.update_live_activity(
341 |             activity_id=activity_id,
342 |             name=name,
343 |             event=event,
344 |             content_state=content_state,
345 |             dismissal_date=dismissal_date,
346 |             priority=priority,
347 |             sound=sound
348 |         )
349 |     except OneSignalAPIError as e:
350 |         return {"error": str(e)}
351 | 
352 | @mcp.tool()
353 | async def end_live_activity(
354 |     activity_id: str,
355 |     subscription_id: str,
356 |     dismissal_date: int = None,
357 |     priority: int = None
358 | ) -> dict:
359 |     """End an iOS Live Activity."""
360 |     try:
361 |         return await live_activities.end_live_activity(
362 |             activity_id=activity_id,
363 |             subscription_id=subscription_id,
364 |             dismissal_date=dismissal_date,
365 |             priority=priority
366 |         )
367 |     except OneSignalAPIError as e:
368 |         return {"error": str(e)}
369 | 
370 | # === Analytics Tools ===
371 | 
372 | @mcp.tool()
373 | async def view_outcomes(
374 |     outcome_names: list,
375 |     outcome_time_range: str = None,
376 |     outcome_platforms: list = None,
377 |     outcome_attribution: str = None
378 | ) -> str:
379 |     """View outcomes data for your app."""
380 |     try:
381 |         result = await analytics.view_outcomes(
382 |             outcome_names=outcome_names,
383 |             outcome_time_range=outcome_time_range,
384 |             outcome_platforms=outcome_platforms,
385 |             outcome_attribution=outcome_attribution
386 |         )
387 |         return analytics.format_outcomes_response(result)
388 |     except Exception as e:
389 |         return f"Error: {str(e)}"
390 | 
391 | @mcp.tool()
392 | async def export_players_csv(
393 |     start_date: str = None,
394 |     end_date: str = None,
395 |     segment_names: list = None
396 | ) -> dict:
397 |     """Export player data to CSV (requires Organization API Key)."""
398 |     try:
399 |         return await analytics.export_players_csv(
400 |             start_date=start_date,
401 |             end_date=end_date,
402 |             segment_names=segment_names
403 |         )
404 |     except OneSignalAPIError as e:
405 |         return {"error": str(e)}
406 | 
407 | @mcp.tool()
408 | async def export_audience_activity_csv(
409 |     start_date: str = None,
410 |     end_date: str = None,
411 |     event_types: list = None
412 | ) -> dict:
413 |     """Export audience activity to CSV (requires Organization API Key)."""
414 |     try:
415 |         return await analytics.export_audience_activity_csv(
416 |             start_date=start_date,
417 |             end_date=end_date,
418 |             event_types=event_types
419 |         )
420 |     except OneSignalAPIError as e:
421 |         return {"error": str(e)}
422 | 
423 | # Run the server
424 | if __name__ == "__main__":
425 |     mcp.run() 
```
Page 1/2FirstPrevNextLast