#
tokens: 33115/50000 23/23 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .github
│   └── workflows
│       └── ci.yml
├── .gitignore
├── docs
│   ├── client_configuration.md
│   ├── configuration.md
│   ├── examples.md
│   ├── README.md
│   ├── troubleshooting.md
│   └── usage.md
├── LICENSE
├── pyproject.toml
├── README.md
├── setup.py
├── src
│   ├── __init__.py
│   ├── mcp
│   │   ├── __init__.py
│   │   ├── protocol.py
│   │   └── validation.py
│   ├── notification
│   │   ├── __init__.py
│   │   ├── macos.py
│   │   ├── manager.py
│   │   ├── platform.py
│   │   └── toast.py
│   └── server
│       ├── __init__.py
│       ├── commands.py
│       └── connection.py
└── tests
    ├── __init__.py
    ├── test_commands.py
    ├── test_connection.py
    ├── test_macos.py
    ├── test_manager.py
    ├── test_protocol.py
    └── test_toast.py
```

# Files

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

```
  1 | # Byte-compiled / optimized / DLL files
  2 | __pycache__/
  3 | *.py[cod]
  4 | *$py.class
  5 | 
  6 | # C extensions
  7 | *.so
  8 | 
  9 | # Distribution / packaging
 10 | .Python
 11 | build/
 12 | develop-eggs/
 13 | dist/
 14 | downloads/
 15 | eggs/
 16 | .eggs/
 17 | lib/
 18 | lib64/
 19 | parts/
 20 | sdist/
 21 | var/
 22 | wheels/
 23 | share/python-wheels/
 24 | *.egg-info/
 25 | .installed.cfg
 26 | *.egg
 27 | MANIFEST
 28 | 
 29 | # PyInstaller
 30 | #  Usually these files are written by a python script from a template
 31 | #  before PyInstaller builds the exe, so as to inject date/other infos into it.
 32 | *.manifest
 33 | *.spec
 34 | 
 35 | # Installer logs
 36 | pip-log.txt
 37 | pip-delete-this-directory.txt
 38 | 
 39 | # Unit test / coverage reports
 40 | htmlcov/
 41 | .tox/
 42 | .nox/
 43 | .coverage
 44 | .coverage.*
 45 | .cache
 46 | nosetests.xml
 47 | coverage.xml
 48 | *.cover
 49 | *.py,cover
 50 | .hypothesis/
 51 | .pytest_cache/
 52 | cover/
 53 | 
 54 | # Translations
 55 | *.mo
 56 | *.pot
 57 | 
 58 | # Django stuff:
 59 | *.log
 60 | local_settings.py
 61 | db.sqlite3
 62 | db.sqlite3-journal
 63 | 
 64 | # Flask stuff:
 65 | instance/
 66 | .webassets-cache
 67 | 
 68 | # Scrapy stuff:
 69 | .scrapy
 70 | 
 71 | # Sphinx documentation
 72 | docs/_build/
 73 | 
 74 | # PyBuilder
 75 | .pybuilder/
 76 | target/
 77 | 
 78 | # Jupyter Notebook
 79 | .ipynb_checkpoints
 80 | 
 81 | # IPython
 82 | profile_default/
 83 | ipython_config.py
 84 | 
 85 | # pyenv
 86 | #   For a library or package, you might want to ignore these files since the code is
 87 | #   intended to run in multiple environments; otherwise, check them in:
 88 | # .python-version
 89 | 
 90 | # pipenv
 91 | #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 92 | #   However, in case of collaboration, if having platform-specific dependencies or dependencies
 93 | #   having no cross-platform support, pipenv may install dependencies that don't work, or not
 94 | #   install all needed dependencies.
 95 | #Pipfile.lock
 96 | 
 97 | # UV
 98 | #   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
 99 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
100 | #   commonly ignored for libraries.
101 | #uv.lock
102 | 
103 | # poetry
104 | #   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105 | #   This is especially recommended for binary packages to ensure reproducibility, and is more
106 | #   commonly ignored for libraries.
107 | #   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108 | #poetry.lock
109 | 
110 | # pdm
111 | #   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112 | #pdm.lock
113 | #   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114 | #   in version control.
115 | #   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116 | .pdm.toml
117 | .pdm-python
118 | .pdm-build/
119 | 
120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121 | __pypackages__/
122 | 
123 | # Celery stuff
124 | celerybeat-schedule
125 | celerybeat.pid
126 | 
127 | # SageMath parsed files
128 | *.sage.py
129 | 
130 | # Environments
131 | .env
132 | .venv
133 | env/
134 | venv/
135 | ENV/
136 | env.bak/
137 | venv.bak/
138 | 
139 | # Spyder project settings
140 | .spyderproject
141 | .spyproject
142 | 
143 | # Rope project settings
144 | .ropeproject
145 | 
146 | # mkdocs documentation
147 | /site
148 | 
149 | # mypy
150 | .mypy_cache/
151 | .dmypy.json
152 | dmypy.json
153 | 
154 | # Pyre type checker
155 | .pyre/
156 | 
157 | # pytype static type analyzer
158 | .pytype/
159 | 
160 | # Cython debug symbols
161 | cython_debug/
162 | 
163 | # PyCharm
164 | #  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165 | #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166 | #  and can be added to the global gitignore or merged into this file.  For a more nuclear
167 | #  option (not recommended) you can uncomment the following to ignore the entire idea folder.
168 | #.idea/
169 | 
170 | # Ruff stuff:
171 | .ruff_cache/
172 | 
173 | # PyPI configuration file
174 | .pypirc
175 | 
```

--------------------------------------------------------------------------------
/src/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | toast-mcp-server package.
3 | """
4 | 
```

--------------------------------------------------------------------------------
/src/notification/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | Notification system package.
3 | """
4 | 
```

--------------------------------------------------------------------------------
/src/server/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | Server implementation package.
3 | """
4 | 
```

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

```python
1 | """
2 | Test package for toast-mcp-server.
3 | """
4 | 
```

--------------------------------------------------------------------------------
/src/mcp/__init__.py:
--------------------------------------------------------------------------------

```python
1 | """
2 | MCP protocol implementation package.
3 | """
4 | 
```

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

```python
 1 | from setuptools import setup, find_packages
 2 | 
 3 | setup(
 4 |     name="toast-mcp-server",
 5 |     version="0.1.0",
 6 |     packages=find_packages(),
 7 |     install_requires=[
 8 |         "win10toast;platform_system=='Windows'",
 9 |     ],
10 |     python_requires=">=3.8",
11 | )
12 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [build-system]
 2 | requires = ["setuptools>=42", "wheel"]
 3 | build-backend = "setuptools.build_meta"
 4 | 
 5 | [project]
 6 | name = "toast-mcp-server"
 7 | version = "0.1.0"
 8 | description = "A Model Context Protocol (MCP) server with Windows 10 and macOS desktop notifications support"
 9 | readme = "README.md"
10 | requires-python = ">=3.8"
11 | license = {text = "MIT"}
12 | dependencies = [
13 |     "win10toast;platform_system=='Windows'",
14 | ]
15 | 
16 | [tool.pytest.ini_options]
17 | testpaths = ["tests"]
18 | python_files = "test_*.py"
19 | 
```

--------------------------------------------------------------------------------
/src/notification/platform.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Platform detection utilities for toast-mcp-server.
 3 | 
 4 | This module provides utilities for detecting the current platform
 5 | and selecting the appropriate notification system.
 6 | """
 7 | 
 8 | import logging
 9 | import platform
10 | from typing import Dict, Any, Optional, List, Union, Callable
11 | 
12 | from src.mcp.protocol import NotificationType
13 | 
14 | logger = logging.getLogger(__name__)
15 | 
16 | 
17 | def is_windows() -> bool:
18 |     """
19 |     Check if the current platform is Windows.
20 |     
21 |     Returns:
22 |         True if the current platform is Windows, False otherwise
23 |     """
24 |     return platform.system() == "Windows"
25 | 
26 | 
27 | def is_macos() -> bool:
28 |     """
29 |     Check if the current platform is macOS.
30 |     
31 |     Returns:
32 |         True if the current platform is macOS, False otherwise
33 |     """
34 |     return platform.system() == "Darwin"
35 | 
36 | 
37 | def is_linux() -> bool:
38 |     """
39 |     Check if the current platform is Linux.
40 |     
41 |     Returns:
42 |         True if the current platform is Linux, False otherwise
43 |     """
44 |     return platform.system() == "Linux"
45 | 
46 | 
47 | def get_platform_name() -> str:
48 |     """
49 |     Get the name of the current platform.
50 |     
51 |     Returns:
52 |         Name of the current platform ("windows", "macos", "linux", or "unknown")
53 |     """
54 |     if is_windows():
55 |         return "windows"
56 |     elif is_macos():
57 |         return "macos"
58 |     elif is_linux():
59 |         return "linux"
60 |     else:
61 |         return "unknown"
62 | 
```

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

```yaml
  1 | name: CI/CD
  2 | 
  3 | on:
  4 |   push:
  5 |     branches: [ main, devin/* ]
  6 |   pull_request:
  7 |     branches: [ main ]
  8 | 
  9 | jobs:
 10 |   test:
 11 |     runs-on: ubuntu-latest
 12 |     strategy:
 13 |       matrix:
 14 |         python-version: [3.8, 3.9, '3.10']
 15 | 
 16 |     steps:
 17 |     - uses: actions/checkout@v3
 18 |     
 19 |     - name: Set up Python ${{ matrix.python-version }}
 20 |       uses: actions/setup-python@v4
 21 |       with:
 22 |         python-version: ${{ matrix.python-version }}
 23 |     
 24 |     - name: Install dependencies
 25 |       run: |
 26 |         python -m pip install --upgrade pip
 27 |         if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
 28 |         pip install pytest pytest-cov flake8
 29 |         pip install -e .
 30 |     
 31 |     - name: Lint with flake8
 32 |       run: |
 33 |         # stop the build if there are Python syntax errors or undefined names
 34 |         flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
 35 |         # exit-zero treats all errors as warnings
 36 |         flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
 37 |     
 38 |     - name: Test with pytest
 39 |       run: |
 40 |         pytest --cov=src tests/
 41 | 
 42 |   build:
 43 |     runs-on: ubuntu-latest
 44 |     needs: test
 45 |     if: github.event_name == 'push' && github.ref == 'refs/heads/main'
 46 |     
 47 |     steps:
 48 |     - uses: actions/checkout@v3
 49 |     
 50 |     - name: Set up Python
 51 |       uses: actions/setup-python@v4
 52 |       with:
 53 |         python-version: '3.10'
 54 |     
 55 |     - name: Install dependencies
 56 |       run: |
 57 |         python -m pip install --upgrade pip
 58 |         if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
 59 |         pip install build
 60 |     
 61 |     - name: Build package
 62 |       run: |
 63 |         python -m build
 64 |     
 65 |     - name: Archive production artifacts
 66 |       uses: actions/upload-artifact@v3
 67 |       with:
 68 |         name: dist
 69 |         path: |
 70 |           dist/
 71 |           
 72 |   deploy:
 73 |     runs-on: ubuntu-latest
 74 |     needs: build
 75 |     if: github.event_name == 'push' && github.ref == 'refs/heads/main' && startsWith(github.event.head_commit.message, 'Release')
 76 |     
 77 |     steps:
 78 |     - uses: actions/checkout@v3
 79 |     
 80 |     - name: Download artifacts
 81 |       uses: actions/download-artifact@v3
 82 |       with:
 83 |         name: dist
 84 |         path: dist
 85 |     
 86 |     - name: Set up Python
 87 |       uses: actions/setup-python@v4
 88 |       with:
 89 |         python-version: '3.10'
 90 |     
 91 |     - name: Install dependencies
 92 |       run: |
 93 |         python -m pip install --upgrade pip
 94 |         pip install twine
 95 |     
 96 |     - name: Create GitHub Release
 97 |       id: create_release
 98 |       uses: actions/create-release@v1
 99 |       env:
100 |         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
101 |       with:
102 |         tag_name: v${{ github.run_number }}
103 |         release_name: Release v${{ github.run_number }}
104 |         body: |
105 |           Automated release from CI/CD pipeline
106 |         draft: false
107 |         prerelease: false
108 | 
```

--------------------------------------------------------------------------------
/tests/test_toast.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the Windows 10 Toast Notification implementation.
  3 | """
  4 | 
  5 | import unittest
  6 | from unittest.mock import patch, MagicMock
  7 | from src.notification.toast import (
  8 |     ToastNotificationManager, NotificationFactory, 
  9 |     show_notification, NotificationType
 10 | )
 11 | 
 12 | 
 13 | class TestToastNotification(unittest.TestCase):
 14 |     """Test cases for the Windows 10 Toast Notification implementation."""
 15 |     
 16 |     @patch('src.notification.toast.ToastNotifier')
 17 |     def test_toast_notification_manager_init(self, mock_toaster):
 18 |         """Test initializing the toast notification manager."""
 19 |         manager = ToastNotificationManager()
 20 |         self.assertIsNotNone(manager.toaster)
 21 |     
 22 |     @patch('src.notification.toast.ToastNotifier')
 23 |     def test_show_notification(self, mock_toaster):
 24 |         """Test showing a notification."""
 25 |         mock_instance = MagicMock()
 26 |         mock_toaster.return_value = mock_instance
 27 |         
 28 |         manager = ToastNotificationManager()
 29 |         result = manager.show_notification(
 30 |             title="Test Title",
 31 |             message="Test Message",
 32 |             notification_type=NotificationType.INFO,
 33 |             duration=5
 34 |         )
 35 |         
 36 |         mock_instance.show_toast.assert_called_once()
 37 |         self.assertTrue(result)
 38 |         
 39 |         args, kwargs = mock_instance.show_toast.call_args
 40 |         self.assertEqual(kwargs["title"], "Test Title")
 41 |         self.assertEqual(kwargs["msg"], "Test Message")
 42 |         self.assertEqual(kwargs["duration"], 5)
 43 |         self.assertTrue(kwargs["threaded"])
 44 |     
 45 |     @patch('src.notification.toast.ToastNotifier')
 46 |     def test_show_notification_with_exception(self, mock_toaster):
 47 |         """Test showing a notification with an exception."""
 48 |         mock_instance = MagicMock()
 49 |         mock_instance.show_toast.side_effect = Exception("Test exception")
 50 |         mock_toaster.return_value = mock_instance
 51 |         
 52 |         manager = ToastNotificationManager()
 53 |         result = manager.show_notification(
 54 |             title="Test Title",
 55 |             message="Test Message"
 56 |         )
 57 |         
 58 |         self.assertFalse(result)
 59 |     
 60 |     @patch('src.notification.toast.ToastNotificationManager')
 61 |     def test_notification_factory(self, mock_manager_class):
 62 |         """Test the notification factory."""
 63 |         mock_manager = MagicMock()
 64 |         mock_manager_class.return_value = mock_manager
 65 |         mock_manager.show_notification.return_value = True
 66 |         
 67 |         factory = NotificationFactory()
 68 |         
 69 |         result = factory.create_info_notification("Info Title", "Info Message")
 70 |         self.assertTrue(result)
 71 |         mock_manager.show_notification.assert_called_with(
 72 |             title="Info Title",
 73 |             message="Info Message",
 74 |             notification_type=NotificationType.INFO,
 75 |             duration=5
 76 |         )
 77 |         
 78 |         result = factory.create_warning_notification("Warning Title", "Warning Message")
 79 |         self.assertTrue(result)
 80 |         mock_manager.show_notification.assert_called_with(
 81 |             title="Warning Title",
 82 |             message="Warning Message",
 83 |             notification_type=NotificationType.WARNING,
 84 |             duration=7
 85 |         )
 86 |         
 87 |         result = factory.create_error_notification("Error Title", "Error Message")
 88 |         self.assertTrue(result)
 89 |         mock_manager.show_notification.assert_called_with(
 90 |             title="Error Title",
 91 |             message="Error Message",
 92 |             notification_type=NotificationType.ERROR,
 93 |             duration=10
 94 |         )
 95 |         
 96 |         result = factory.create_success_notification("Success Title", "Success Message")
 97 |         self.assertTrue(result)
 98 |         mock_manager.show_notification.assert_called_with(
 99 |             title="Success Title",
100 |             message="Success Message",
101 |             notification_type=NotificationType.SUCCESS,
102 |             duration=5
103 |         )
104 |     
105 |     @patch('src.notification.toast.notification_factory')
106 |     def test_show_notification_helper(self, mock_factory):
107 |         """Test the show_notification helper function."""
108 |         mock_factory.create_info_notification.return_value = True
109 |         mock_factory.create_warning_notification.return_value = True
110 |         mock_factory.create_error_notification.return_value = True
111 |         mock_factory.create_success_notification.return_value = True
112 |         
113 |         result = show_notification("Info Title", "Info Message", "info")
114 |         self.assertTrue(result)
115 |         mock_factory.create_info_notification.assert_called_with("Info Title", "Info Message", 5)
116 |         
117 |         result = show_notification("Warning Title", "Warning Message", "warning")
118 |         self.assertTrue(result)
119 |         mock_factory.create_warning_notification.assert_called_with("Warning Title", "Warning Message", 5)
120 |         
121 |         result = show_notification("Error Title", "Error Message", "error")
122 |         self.assertTrue(result)
123 |         mock_factory.create_error_notification.assert_called_with("Error Title", "Error Message", 5)
124 |         
125 |         result = show_notification("Success Title", "Success Message", "success")
126 |         self.assertTrue(result)
127 |         mock_factory.create_success_notification.assert_called_with("Success Title", "Success Message", 5)
128 |         
129 |         result = show_notification("Invalid Title", "Invalid Message", "invalid")
130 |         self.assertTrue(result)
131 |         mock_factory.create_info_notification.assert_called_with("Invalid Title", "Invalid Message", 5)
132 | 
133 | 
134 | if __name__ == "__main__":
135 |     unittest.main()
136 | 
```

--------------------------------------------------------------------------------
/src/mcp/validation.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Validation utilities for MCP protocol messages.
  3 | 
  4 | This module provides functions for validating MCP messages and their contents
  5 | to ensure they conform to the protocol specification.
  6 | """
  7 | 
  8 | import re
  9 | from typing import Dict, Any, List, Optional, Tuple, Union
 10 | 
 11 | from .protocol import NotificationType, MessageType
 12 | 
 13 | 
 14 | def validate_notification(data: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
 15 |     """
 16 |     Validate a notification message.
 17 |     
 18 |     Args:
 19 |         data: The notification message data to validate
 20 |         
 21 |     Returns:
 22 |         A tuple of (is_valid, error_message)
 23 |     """
 24 |     if "title" not in data:
 25 |         return False, "Missing required field: title"
 26 |     
 27 |     if "message" not in data:
 28 |         return False, "Missing required field: message"
 29 |     
 30 |     if not isinstance(data["title"], str):
 31 |         return False, "Title must be a string"
 32 |     
 33 |     if len(data["title"]) > 100:
 34 |         return False, "Title exceeds maximum length of 100 characters"
 35 |     
 36 |     if not isinstance(data["message"], str):
 37 |         return False, "Message must be a string"
 38 |     
 39 |     if len(data["message"]) > 1000:
 40 |         return False, "Message exceeds maximum length of 1000 characters"
 41 |     
 42 |     if "notification_type" in data:
 43 |         try:
 44 |             NotificationType(data["notification_type"])
 45 |         except ValueError:
 46 |             valid_types = [t.value for t in NotificationType]
 47 |             return False, f"Invalid notification type. Must be one of: {', '.join(valid_types)}"
 48 |     
 49 |     if "duration" in data:
 50 |         if not isinstance(data["duration"], int):
 51 |             return False, "Duration must be an integer"
 52 |         
 53 |         if data["duration"] < 1 or data["duration"] > 60:
 54 |             return False, "Duration must be between 1 and 60 seconds"
 55 |     
 56 |     if "client_id" in data:
 57 |         if not isinstance(data["client_id"], str):
 58 |             return False, "Client ID must be a string"
 59 |         
 60 |         if not re.match(r'^[a-zA-Z0-9_\-\.]{1,50}$', data["client_id"]):
 61 |             return False, "Client ID contains invalid characters or exceeds maximum length"
 62 |     
 63 |     if "icon" in data and not isinstance(data["icon"], str):
 64 |         return False, "Icon must be a string"
 65 |     
 66 |     if "actions" in data:
 67 |         if not isinstance(data["actions"], list):
 68 |             return False, "Actions must be a list"
 69 |         
 70 |         for i, action in enumerate(data["actions"]):
 71 |             if not isinstance(action, dict):
 72 |                 return False, f"Action at index {i} must be a dictionary"
 73 |             
 74 |             if "id" not in action:
 75 |                 return False, f"Action at index {i} missing required field: id"
 76 |             
 77 |             if "text" not in action:
 78 |                 return False, f"Action at index {i} missing required field: text"
 79 |             
 80 |             if not isinstance(action["id"], str) or not isinstance(action["text"], str):
 81 |                 return False, f"Action id and text must be strings"
 82 |     
 83 |     return True, None
 84 | 
 85 | 
 86 | def validate_message(message_type: MessageType, data: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
 87 |     """
 88 |     Validate a message based on its type.
 89 |     
 90 |     Args:
 91 |         message_type: The type of message to validate
 92 |         data: The message data to validate
 93 |         
 94 |     Returns:
 95 |         A tuple of (is_valid, error_message)
 96 |     """
 97 |     if message_type == MessageType.NOTIFICATION:
 98 |         return validate_notification(data)
 99 |     
100 |     elif message_type == MessageType.RESPONSE:
101 |         if "success" not in data:
102 |             return False, "Response message missing required field: success"
103 |         
104 |         if not isinstance(data["success"], bool):
105 |             return False, "Success field must be a boolean"
106 |         
107 |         return True, None
108 |     
109 |     elif message_type == MessageType.ERROR:
110 |         if "code" not in data:
111 |             return False, "Error message missing required field: code"
112 |         
113 |         if "message" not in data:
114 |             return False, "Error message missing required field: message"
115 |         
116 |         if not isinstance(data["code"], int):
117 |             return False, "Error code must be an integer"
118 |         
119 |         if not isinstance(data["message"], str):
120 |             return False, "Error message must be a string"
121 |         
122 |         return True, None
123 |     
124 |     elif message_type in (MessageType.PING, MessageType.PONG):
125 |         return True, None
126 |     
127 |     return False, f"Unknown message type: {message_type}"
128 | 
129 | 
130 | def validate_message_format(data: Dict[str, Any]) -> Tuple[bool, Optional[str], Optional[MessageType]]:
131 |     """
132 |     Validate the format of a message.
133 |     
134 |     Args:
135 |         data: The message data to validate
136 |         
137 |     Returns:
138 |         A tuple of (is_valid, error_message, message_type)
139 |     """
140 |     if not isinstance(data, dict):
141 |         return False, "Message must be a dictionary", None
142 |     
143 |     if "type" not in data:
144 |         return False, "Message missing required field: type", None
145 |     
146 |     try:
147 |         message_type = MessageType(data["type"])
148 |     except ValueError:
149 |         valid_types = [t.value for t in MessageType]
150 |         return False, f"Invalid message type. Must be one of: {', '.join(valid_types)}", None
151 |     
152 |     if "data" not in data:
153 |         data["data"] = {}  # Add empty data for types that don't require it
154 |     
155 |     if not isinstance(data["data"], dict):
156 |         return False, "Message data must be a dictionary", None
157 |     
158 |     is_valid, error_message = validate_message(message_type, data["data"])
159 |     
160 |     return is_valid, error_message, message_type
161 | 
```

--------------------------------------------------------------------------------
/tests/test_protocol.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the MCP protocol implementation.
  3 | """
  4 | 
  5 | import unittest
  6 | import json
  7 | from src.mcp.protocol import (
  8 |     MCPMessage, NotificationMessage, ResponseMessage, ErrorMessage,
  9 |     PingMessage, PongMessage, MessageType, NotificationType, parse_message
 10 | )
 11 | from src.mcp.validation import validate_message_format, validate_notification
 12 | 
 13 | 
 14 | class TestMCPProtocol(unittest.TestCase):
 15 |     """Test cases for the MCP protocol implementation."""
 16 |     
 17 |     def test_notification_message_creation(self):
 18 |         """Test creating a notification message."""
 19 |         notification = NotificationMessage(
 20 |             title="Test Notification",
 21 |             message="This is a test notification",
 22 |             notification_type=NotificationType.INFO,
 23 |             duration=10,
 24 |             client_id="test-client",
 25 |             icon="info-icon",
 26 |             actions=[{"id": "action1", "text": "Click Me"}]
 27 |         )
 28 |         
 29 |         self.assertEqual(notification.msg_type, MessageType.NOTIFICATION)
 30 |         self.assertEqual(notification.data["title"], "Test Notification")
 31 |         self.assertEqual(notification.data["message"], "This is a test notification")
 32 |         self.assertEqual(notification.data["notification_type"], "info")
 33 |         self.assertEqual(notification.data["duration"], 10)
 34 |         self.assertEqual(notification.data["client_id"], "test-client")
 35 |         self.assertEqual(notification.data["icon"], "info-icon")
 36 |         self.assertEqual(len(notification.data["actions"]), 1)
 37 |         self.assertEqual(notification.data["actions"][0]["id"], "action1")
 38 |     
 39 |     def test_notification_message_serialization(self):
 40 |         """Test serializing a notification message to JSON."""
 41 |         notification = NotificationMessage(
 42 |             title="Test Notification",
 43 |             message="This is a test notification"
 44 |         )
 45 |         
 46 |         json_str = notification.to_json()
 47 |         data = json.loads(json_str)
 48 |         
 49 |         self.assertEqual(data["type"], "notification")
 50 |         self.assertEqual(data["data"]["title"], "Test Notification")
 51 |         self.assertEqual(data["data"]["message"], "This is a test notification")
 52 |         self.assertEqual(data["data"]["notification_type"], "info")  # Default type
 53 |     
 54 |     def test_notification_message_deserialization(self):
 55 |         """Test deserializing a notification message from JSON."""
 56 |         json_str = json.dumps({
 57 |             "type": "notification",
 58 |             "data": {
 59 |                 "title": "Test Notification",
 60 |                 "message": "This is a test notification",
 61 |                 "notification_type": "warning",
 62 |                 "duration": 15
 63 |             }
 64 |         })
 65 |         
 66 |         message = MCPMessage.from_json(json_str)
 67 |         
 68 |         self.assertIsInstance(message, NotificationMessage)
 69 |         self.assertEqual(message.data["title"], "Test Notification")
 70 |         self.assertEqual(message.data["message"], "This is a test notification")
 71 |         self.assertEqual(message.data["notification_type"], "warning")
 72 |         self.assertEqual(message.data["duration"], 15)
 73 |     
 74 |     def test_response_message(self):
 75 |         """Test creating and serializing a response message."""
 76 |         response = ResponseMessage(
 77 |             success=True,
 78 |             message="Operation completed successfully",
 79 |             data={"id": 123}
 80 |         )
 81 |         
 82 |         self.assertEqual(response.msg_type, MessageType.RESPONSE)
 83 |         self.assertTrue(response.data["success"])
 84 |         self.assertEqual(response.data["message"], "Operation completed successfully")
 85 |         self.assertEqual(response.data["data"]["id"], 123)
 86 |         
 87 |         json_str = response.to_json()
 88 |         data = json.loads(json_str)
 89 |         
 90 |         self.assertEqual(data["type"], "response")
 91 |         self.assertTrue(data["data"]["success"])
 92 |     
 93 |     def test_error_message(self):
 94 |         """Test creating and serializing an error message."""
 95 |         error = ErrorMessage(
 96 |             error_code=404,
 97 |             error_message="Resource not found",
 98 |             details={"resource_id": "abc123"}
 99 |         )
100 |         
101 |         self.assertEqual(error.msg_type, MessageType.ERROR)
102 |         self.assertEqual(error.data["code"], 404)
103 |         self.assertEqual(error.data["message"], "Resource not found")
104 |         self.assertEqual(error.data["details"]["resource_id"], "abc123")
105 |         
106 |         json_str = error.to_json()
107 |         data = json.loads(json_str)
108 |         
109 |         self.assertEqual(data["type"], "error")
110 |         self.assertEqual(data["data"]["code"], 404)
111 |     
112 |     def test_ping_pong_messages(self):
113 |         """Test creating ping and pong messages."""
114 |         ping = PingMessage()
115 |         pong = PongMessage()
116 |         
117 |         self.assertEqual(ping.msg_type, MessageType.PING)
118 |         self.assertEqual(pong.msg_type, MessageType.PONG)
119 |         
120 |         ping_json = ping.to_json()
121 |         pong_json = pong.to_json()
122 |         
123 |         ping_data = json.loads(ping_json)
124 |         pong_data = json.loads(pong_json)
125 |         
126 |         self.assertEqual(ping_data["type"], "ping")
127 |         self.assertEqual(pong_data["type"], "pong")
128 |     
129 |     def test_parse_message(self):
130 |         """Test parsing messages from different formats."""
131 |         json_str = json.dumps({
132 |             "type": "notification",
133 |             "data": {
134 |                 "title": "Test",
135 |                 "message": "Test message"
136 |             }
137 |         })
138 |         
139 |         message1 = parse_message(json_str)
140 |         self.assertIsInstance(message1, NotificationMessage)
141 |         
142 |         dict_data = {
143 |             "type": "response",
144 |             "data": {
145 |                 "success": True
146 |             }
147 |         }
148 |         
149 |         message2 = parse_message(dict_data)
150 |         self.assertIsInstance(message2, ResponseMessage)
151 |     
152 |     def test_validation(self):
153 |         """Test message validation."""
154 |         valid_notification = {
155 |             "type": "notification",
156 |             "data": {
157 |                 "title": "Valid Title",
158 |                 "message": "Valid message content",
159 |                 "notification_type": "info",
160 |                 "duration": 5
161 |             }
162 |         }
163 |         
164 |         is_valid, error, msg_type = validate_message_format(valid_notification)
165 |         self.assertTrue(is_valid)
166 |         self.assertIsNone(error)
167 |         self.assertEqual(msg_type, MessageType.NOTIFICATION)
168 |         
169 |         invalid_notification = {
170 |             "type": "notification",
171 |             "data": {
172 |                 "message": "Message without title"
173 |             }
174 |         }
175 |         
176 |         is_valid, error, msg_type = validate_message_format(invalid_notification)
177 |         self.assertFalse(is_valid)
178 |         self.assertIn("Missing required field: title", error)
179 |         
180 |         valid_data = {
181 |             "title": "Test",
182 |             "message": "Test message",
183 |             "duration": 10
184 |         }
185 |         
186 |         is_valid, error = validate_notification(valid_data)
187 |         self.assertTrue(is_valid)
188 |         self.assertIsNone(error)
189 |         
190 |         invalid_data = {
191 |             "title": "Test",
192 |             "message": "Test message",
193 |             "duration": 100  # Too long
194 |         }
195 |         
196 |         is_valid, error = validate_notification(invalid_data)
197 |         self.assertFalse(is_valid)
198 |         self.assertIn("Duration must be between 1 and 60 seconds", error)
199 | 
200 | 
201 | if __name__ == "__main__":
202 |     unittest.main()
203 | 
```

--------------------------------------------------------------------------------
/src/notification/toast.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Windows 10 Toast Notification implementation for toast-mcp-server.
  3 | 
  4 | This module provides functionality to display Windows 10 toast notifications
  5 | using the win10toast library.
  6 | """
  7 | 
  8 | import logging
  9 | from typing import Dict, Any, Optional, List, Union
 10 | from win10toast import ToastNotifier
 11 | 
 12 | from src.mcp.protocol import NotificationType
 13 | 
 14 | logger = logging.getLogger(__name__)
 15 | 
 16 | 
 17 | class ToastNotificationManager:
 18 |     """
 19 |     Manager for Windows 10 Toast Notifications.
 20 |     
 21 |     This class handles the creation and display of Windows 10 toast notifications
 22 |     using the win10toast library.
 23 |     """
 24 |     
 25 |     def __init__(self):
 26 |         """Initialize the toast notification manager."""
 27 |         self.toaster = ToastNotifier()
 28 |         logger.info("Toast notification manager initialized")
 29 |     
 30 |     def show_notification(self,
 31 |                          title: str,
 32 |                          message: str,
 33 |                          notification_type: NotificationType = NotificationType.INFO,
 34 |                          duration: int = 5,
 35 |                          icon_path: Optional[str] = None,
 36 |                          threaded: bool = True) -> bool:
 37 |         """
 38 |         Show a Windows 10 toast notification.
 39 |         
 40 |         Args:
 41 |             title: Title of the notification
 42 |             message: Content of the notification
 43 |             notification_type: Type of notification (info, warning, error, success)
 44 |             duration: Duration to display the notification in seconds
 45 |             icon_path: Path to the icon file to display with the notification
 46 |             threaded: Whether to show the notification in a separate thread
 47 |             
 48 |         Returns:
 49 |             True if the notification was successfully displayed, False otherwise
 50 |         """
 51 |         if not icon_path:
 52 |             icon_path = self._get_default_icon(notification_type)
 53 |         
 54 |         logger.debug(f"Showing notification: {title} ({notification_type.value})")
 55 |         
 56 |         try:
 57 |             self.toaster.show_toast(
 58 |                 title=title,
 59 |                 msg=message,
 60 |                 icon_path=icon_path,
 61 |                 duration=duration,
 62 |                 threaded=threaded
 63 |             )
 64 |             return True
 65 |         except Exception as e:
 66 |             logger.error(f"Failed to show notification: {str(e)}")
 67 |             return False
 68 |     
 69 |     def _get_default_icon(self, notification_type: NotificationType) -> str:
 70 |         """
 71 |         Get the default icon path for a notification type.
 72 |         
 73 |         Args:
 74 |             notification_type: Type of notification
 75 |             
 76 |         Returns:
 77 |             Path to the default icon for the notification type
 78 |         """
 79 |         icons = {
 80 |             NotificationType.INFO: "icons/info.ico",
 81 |             NotificationType.WARNING: "icons/warning.ico",
 82 |             NotificationType.ERROR: "icons/error.ico",
 83 |             NotificationType.SUCCESS: "icons/success.ico"
 84 |         }
 85 |         
 86 |         return icons.get(notification_type, "icons/default.ico")
 87 | 
 88 | 
 89 | class NotificationFactory:
 90 |     """
 91 |     Factory for creating notifications based on notification type.
 92 |     
 93 |     This class provides methods for creating and displaying different types
 94 |     of notifications with appropriate default settings.
 95 |     """
 96 |     
 97 |     def __init__(self):
 98 |         """Initialize the notification factory."""
 99 |         self.toast_manager = ToastNotificationManager()
100 |     
101 |     def create_info_notification(self, title: str, message: str, duration: int = 5) -> bool:
102 |         """
103 |         Create and show an information notification.
104 |         
105 |         Args:
106 |             title: Title of the notification
107 |             message: Content of the notification
108 |             duration: Duration to display the notification in seconds
109 |             
110 |         Returns:
111 |             True if the notification was successfully displayed, False otherwise
112 |         """
113 |         return self.toast_manager.show_notification(
114 |             title=title,
115 |             message=message,
116 |             notification_type=NotificationType.INFO,
117 |             duration=duration
118 |         )
119 |     
120 |     def create_warning_notification(self, title: str, message: str, duration: int = 7) -> bool:
121 |         """
122 |         Create and show a warning notification.
123 |         
124 |         Args:
125 |             title: Title of the notification
126 |             message: Content of the notification
127 |             duration: Duration to display the notification in seconds
128 |             
129 |         Returns:
130 |             True if the notification was successfully displayed, False otherwise
131 |         """
132 |         return self.toast_manager.show_notification(
133 |             title=title,
134 |             message=message,
135 |             notification_type=NotificationType.WARNING,
136 |             duration=duration
137 |         )
138 |     
139 |     def create_error_notification(self, title: str, message: str, duration: int = 10) -> bool:
140 |         """
141 |         Create and show an error notification.
142 |         
143 |         Args:
144 |             title: Title of the notification
145 |             message: Content of the notification
146 |             duration: Duration to display the notification in seconds
147 |             
148 |         Returns:
149 |             True if the notification was successfully displayed, False otherwise
150 |         """
151 |         return self.toast_manager.show_notification(
152 |             title=title,
153 |             message=message,
154 |             notification_type=NotificationType.ERROR,
155 |             duration=duration
156 |         )
157 |     
158 |     def create_success_notification(self, title: str, message: str, duration: int = 5) -> bool:
159 |         """
160 |         Create and show a success notification.
161 |         
162 |         Args:
163 |             title: Title of the notification
164 |             message: Content of the notification
165 |             duration: Duration to display the notification in seconds
166 |             
167 |         Returns:
168 |             True if the notification was successfully displayed, False otherwise
169 |         """
170 |         return self.toast_manager.show_notification(
171 |             title=title,
172 |             message=message,
173 |             notification_type=NotificationType.SUCCESS,
174 |             duration=duration
175 |         )
176 | 
177 | 
178 | notification_factory = NotificationFactory()
179 | 
180 | 
181 | def show_notification(title: str, message: str, notification_type: str = "info", duration: int = 5) -> bool:
182 |     """
183 |     Show a notification with the specified parameters.
184 |     
185 |     This is a convenience function for showing notifications without directly
186 |     interacting with the NotificationFactory or ToastNotificationManager classes.
187 |     
188 |     Args:
189 |         title: Title of the notification
190 |         message: Content of the notification
191 |         notification_type: Type of notification ("info", "warning", "error", "success")
192 |         duration: Duration to display the notification in seconds
193 |         
194 |     Returns:
195 |         True if the notification was successfully displayed, False otherwise
196 |     """
197 |     try:
198 |         notification_type_enum = NotificationType(notification_type)
199 |     except ValueError:
200 |         logger.warning(f"Invalid notification type: {notification_type}, using INFO")
201 |         notification_type_enum = NotificationType.INFO
202 |     
203 |     if notification_type_enum == NotificationType.INFO:
204 |         return notification_factory.create_info_notification(title, message, duration)
205 |     elif notification_type_enum == NotificationType.WARNING:
206 |         return notification_factory.create_warning_notification(title, message, duration)
207 |     elif notification_type_enum == NotificationType.ERROR:
208 |         return notification_factory.create_error_notification(title, message, duration)
209 |     elif notification_type_enum == NotificationType.SUCCESS:
210 |         return notification_factory.create_success_notification(title, message, duration)
211 |     
212 |     return notification_factory.create_info_notification(title, message, duration)
213 | 
```

--------------------------------------------------------------------------------
/src/notification/manager.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Unified notification manager for toast-mcp-server.
  3 | 
  4 | This module provides a unified interface for displaying notifications
  5 | on different platforms, automatically selecting the appropriate
  6 | notification system based on the current platform.
  7 | """
  8 | 
  9 | import logging
 10 | from typing import Dict, Any, Optional, List, Union
 11 | 
 12 | from src.mcp.protocol import NotificationType
 13 | from src.notification.platform import is_windows, is_macos, get_platform_name
 14 | 
 15 | logger = logging.getLogger(__name__)
 16 | 
 17 | 
 18 | class NotificationManager:
 19 |     """
 20 |     Unified notification manager for multiple platforms.
 21 |     
 22 |     This class provides a unified interface for displaying notifications
 23 |     on different platforms, automatically selecting the appropriate
 24 |     notification system based on the current platform.
 25 |     """
 26 |     
 27 |     def __init__(self):
 28 |         """Initialize the notification manager."""
 29 |         self._platform = get_platform_name()
 30 |         self._notification_system = self._get_notification_system()
 31 |         logger.info(f"Notification manager initialized for platform: {self._platform}")
 32 |     
 33 |     def _get_notification_system(self):
 34 |         """
 35 |         Get the appropriate notification system for the current platform.
 36 |         
 37 |         Returns:
 38 |             Notification system for the current platform
 39 |         """
 40 |         if is_windows():
 41 |             from src.notification.toast import ToastNotificationManager
 42 |             return ToastNotificationManager()
 43 |         elif is_macos():
 44 |             from src.notification.macos import MacOSNotificationManager
 45 |             return MacOSNotificationManager()
 46 |         else:
 47 |             logger.warning(f"No notification system available for platform: {self._platform}")
 48 |             return None
 49 |     
 50 |     def show_notification(self,
 51 |                          title: str,
 52 |                          message: str,
 53 |                          notification_type: NotificationType = NotificationType.INFO,
 54 |                          duration: int = 5,
 55 |                          **kwargs) -> bool:
 56 |         """
 57 |         Show a notification on the current platform.
 58 |         
 59 |         Args:
 60 |             title: Title of the notification
 61 |             message: Content of the notification
 62 |             notification_type: Type of notification (info, warning, error, success)
 63 |             duration: Duration to display the notification in seconds (ignored on some platforms)
 64 |             **kwargs: Additional platform-specific parameters
 65 |             
 66 |         Returns:
 67 |             True if the notification was successfully displayed, False otherwise
 68 |         """
 69 |         if not self._notification_system:
 70 |             logger.error(f"Cannot show notification on unsupported platform: {self._platform}")
 71 |             return False
 72 |         
 73 |         logger.debug(f"Showing notification on {self._platform}: {title} ({notification_type.value})")
 74 |         
 75 |         try:
 76 |             if is_windows():
 77 |                 return self._notification_system.show_notification(
 78 |                     title=title,
 79 |                     message=message,
 80 |                     notification_type=notification_type,
 81 |                     duration=duration,
 82 |                     **kwargs
 83 |                 )
 84 |             elif is_macos():
 85 |                 subtitle = kwargs.get("subtitle")
 86 |                 sound = kwargs.get("sound", True)
 87 |                 
 88 |                 return self._notification_system.show_notification(
 89 |                     title=title,
 90 |                     message=message,
 91 |                     notification_type=notification_type,
 92 |                     duration=duration,
 93 |                     subtitle=subtitle,
 94 |                     sound=sound
 95 |                 )
 96 |             else:
 97 |                 return False
 98 |                 
 99 |         except Exception as e:
100 |             logger.error(f"Failed to show notification: {str(e)}")
101 |             return False
102 | 
103 | 
104 | class NotificationFactory:
105 |     """
106 |     Factory for creating notifications based on notification type.
107 |     
108 |     This class provides methods for creating and displaying different types
109 |     of notifications with appropriate default settings.
110 |     """
111 |     
112 |     def __init__(self):
113 |         """Initialize the notification factory."""
114 |         self.notification_manager = NotificationManager()
115 |     
116 |     def create_info_notification(self, title: str, message: str, **kwargs) -> bool:
117 |         """
118 |         Create and show an information notification.
119 |         
120 |         Args:
121 |             title: Title of the notification
122 |             message: Content of the notification
123 |             **kwargs: Additional platform-specific parameters
124 |             
125 |         Returns:
126 |             True if the notification was successfully displayed, False otherwise
127 |         """
128 |         return self.notification_manager.show_notification(
129 |             title=title,
130 |             message=message,
131 |             notification_type=NotificationType.INFO,
132 |             **kwargs
133 |         )
134 |     
135 |     def create_warning_notification(self, title: str, message: str, **kwargs) -> bool:
136 |         """
137 |         Create and show a warning notification.
138 |         
139 |         Args:
140 |             title: Title of the notification
141 |             message: Content of the notification
142 |             **kwargs: Additional platform-specific parameters
143 |             
144 |         Returns:
145 |             True if the notification was successfully displayed, False otherwise
146 |         """
147 |         return self.notification_manager.show_notification(
148 |             title=title,
149 |             message=message,
150 |             notification_type=NotificationType.WARNING,
151 |             **kwargs
152 |         )
153 |     
154 |     def create_error_notification(self, title: str, message: str, **kwargs) -> bool:
155 |         """
156 |         Create and show an error notification.
157 |         
158 |         Args:
159 |             title: Title of the notification
160 |             message: Content of the notification
161 |             **kwargs: Additional platform-specific parameters
162 |             
163 |         Returns:
164 |             True if the notification was successfully displayed, False otherwise
165 |         """
166 |         return self.notification_manager.show_notification(
167 |             title=title,
168 |             message=message,
169 |             notification_type=NotificationType.ERROR,
170 |             **kwargs
171 |         )
172 |     
173 |     def create_success_notification(self, title: str, message: str, **kwargs) -> bool:
174 |         """
175 |         Create and show a success notification.
176 |         
177 |         Args:
178 |             title: Title of the notification
179 |             message: Content of the notification
180 |             **kwargs: Additional platform-specific parameters
181 |             
182 |         Returns:
183 |             True if the notification was successfully displayed, False otherwise
184 |         """
185 |         return self.notification_manager.show_notification(
186 |             title=title,
187 |             message=message,
188 |             notification_type=NotificationType.SUCCESS,
189 |             **kwargs
190 |         )
191 | 
192 | 
193 | notification_factory = NotificationFactory()
194 | 
195 | 
196 | def show_notification(title: str, message: str, notification_type: str = "info", **kwargs) -> bool:
197 |     """
198 |     Show a notification with the specified parameters.
199 |     
200 |     This is a convenience function for showing notifications without directly
201 |     interacting with the NotificationFactory or platform-specific notification classes.
202 |     
203 |     Args:
204 |         title: Title of the notification
205 |         message: Content of the notification
206 |         notification_type: Type of notification ("info", "warning", "error", "success")
207 |         **kwargs: Additional platform-specific parameters
208 |         
209 |     Returns:
210 |         True if the notification was successfully displayed, False otherwise
211 |     """
212 |     try:
213 |         notification_type_enum = NotificationType(notification_type)
214 |     except ValueError:
215 |         logger.warning(f"Invalid notification type: {notification_type}, using INFO")
216 |         notification_type_enum = NotificationType.INFO
217 |     
218 |     if notification_type_enum == NotificationType.INFO:
219 |         return notification_factory.create_info_notification(title, message, **kwargs)
220 |     elif notification_type_enum == NotificationType.WARNING:
221 |         return notification_factory.create_warning_notification(title, message, **kwargs)
222 |     elif notification_type_enum == NotificationType.ERROR:
223 |         return notification_factory.create_error_notification(title, message, **kwargs)
224 |     elif notification_type_enum == NotificationType.SUCCESS:
225 |         return notification_factory.create_success_notification(title, message, **kwargs)
226 |     
227 |     return notification_factory.create_info_notification(title, message, **kwargs)
228 | 
```

--------------------------------------------------------------------------------
/src/server/commands.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Command processing system for toast-mcp-server.
  3 | 
  4 | This module handles the processing of commands received from clients,
  5 | including command registration, validation, and execution.
  6 | """
  7 | 
  8 | import logging
  9 | import asyncio
 10 | from typing import Dict, Any, Optional, Callable, Awaitable, List, Union, Tuple
 11 | 
 12 | from src.mcp.protocol import (
 13 |     MCPMessage, ResponseMessage, ErrorMessage, MessageType
 14 | )
 15 | from src.notification.toast import show_notification
 16 | 
 17 | logger = logging.getLogger(__name__)
 18 | 
 19 | 
 20 | CommandHandler = Callable[[Dict[str, Any], str], Awaitable[Tuple[bool, str, Optional[Dict[str, Any]]]]]
 21 | CommandValidator = Callable[[Dict[str, Any]], Tuple[bool, Optional[str]]]
 22 | 
 23 | 
 24 | class Command:
 25 |     """
 26 |     Represents a command that can be executed by the server.
 27 |     
 28 |     Commands are registered with the CommandProcessor and can be executed
 29 |     when received from clients.
 30 |     """
 31 |     
 32 |     def __init__(self, 
 33 |                  name: str, 
 34 |                  handler: CommandHandler, 
 35 |                  validator: Optional[CommandValidator] = None,
 36 |                  description: str = "",
 37 |                  requires_auth: bool = False):
 38 |         """
 39 |         Initialize a new command.
 40 |         
 41 |         Args:
 42 |             name: Name of the command
 43 |             handler: Function to handle the command execution
 44 |             validator: Optional function to validate command parameters
 45 |             description: Description of the command
 46 |             requires_auth: Whether the command requires authentication
 47 |         """
 48 |         self.name = name
 49 |         self.handler = handler
 50 |         self.validator = validator
 51 |         self.description = description
 52 |         self.requires_auth = requires_auth
 53 |     
 54 |     async def execute(self, params: Dict[str, Any], client_id: str) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
 55 |         """
 56 |         Execute the command.
 57 |         
 58 |         Args:
 59 |             params: Parameters for the command
 60 |             client_id: ID of the client executing the command
 61 |             
 62 |         Returns:
 63 |             Tuple of (success, message, data)
 64 |         """
 65 |         if self.validator:
 66 |             is_valid, error = self.validator(params)
 67 |             if not is_valid:
 68 |                 return False, f"Invalid parameters: {error}", None
 69 |         
 70 |         try:
 71 |             return await self.handler(params, client_id)
 72 |         except Exception as e:
 73 |             logger.error(f"Error executing command {self.name}: {str(e)}")
 74 |             return False, f"Error executing command: {str(e)}", None
 75 | 
 76 | 
 77 | class CommandProcessor:
 78 |     """
 79 |     Processes commands received from clients.
 80 |     
 81 |     This class manages the registration and execution of commands,
 82 |     as well as the handling of command responses.
 83 |     """
 84 |     
 85 |     def __init__(self):
 86 |         """Initialize the command processor."""
 87 |         self.commands: Dict[str, Command] = {}
 88 |         self.authenticated_clients: List[str] = []
 89 |         logger.info("Command processor initialized")
 90 |     
 91 |     def register_command(self, command: Command) -> None:
 92 |         """
 93 |         Register a command with the processor.
 94 |         
 95 |         Args:
 96 |             command: The command to register
 97 |         """
 98 |         self.commands[command.name] = command
 99 |         logger.debug(f"Registered command: {command.name}")
100 |     
101 |     def register_commands(self, commands: List[Command]) -> None:
102 |         """
103 |         Register multiple commands with the processor.
104 |         
105 |         Args:
106 |             commands: List of commands to register
107 |         """
108 |         for command in commands:
109 |             self.register_command(command)
110 |     
111 |     async def process_command(self, 
112 |                              command_name: str, 
113 |                              params: Dict[str, Any], 
114 |                              client_id: str) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
115 |         """
116 |         Process a command.
117 |         
118 |         Args:
119 |             command_name: Name of the command to execute
120 |             params: Parameters for the command
121 |             client_id: ID of the client executing the command
122 |             
123 |         Returns:
124 |             Tuple of (success, message, data)
125 |         """
126 |         if command_name not in self.commands:
127 |             return False, f"Unknown command: {command_name}", None
128 |         
129 |         command = self.commands[command_name]
130 |         
131 |         if command.requires_auth and client_id not in self.authenticated_clients:
132 |             return False, "Authentication required", None
133 |         
134 |         return await command.execute(params, client_id)
135 |     
136 |     def authenticate_client(self, client_id: str) -> None:
137 |         """
138 |         Mark a client as authenticated.
139 |         
140 |         Args:
141 |             client_id: ID of the client to authenticate
142 |         """
143 |         if client_id not in self.authenticated_clients:
144 |             self.authenticated_clients.append(client_id)
145 |             logger.debug(f"Client authenticated: {client_id}")
146 |     
147 |     def deauthenticate_client(self, client_id: str) -> None:
148 |         """
149 |         Remove a client's authentication.
150 |         
151 |         Args:
152 |             client_id: ID of the client to deauthenticate
153 |         """
154 |         if client_id in self.authenticated_clients:
155 |             self.authenticated_clients.remove(client_id)
156 |             logger.debug(f"Client deauthenticated: {client_id}")
157 | 
158 | 
159 | 
160 | async def handle_show_notification(params: Dict[str, Any], client_id: str) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
161 |     """
162 |     Handle the 'show_notification' command.
163 |     
164 |     Args:
165 |         params: Parameters for the command
166 |         client_id: ID of the client executing the command
167 |         
168 |     Returns:
169 |         Tuple of (success, message, data)
170 |     """
171 |     title = params.get("title", "")
172 |     message = params.get("message", "")
173 |     notification_type = params.get("type", "info")
174 |     duration = params.get("duration", 5)
175 |     
176 |     success = show_notification(title, message, notification_type, duration)
177 |     
178 |     if success:
179 |         return True, "Notification displayed successfully", None
180 |     else:
181 |         return False, "Failed to display notification", None
182 | 
183 | 
184 | async def handle_list_commands(params: Dict[str, Any], client_id: str) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
185 |     """
186 |     Handle the 'list_commands' command.
187 |     
188 |     Args:
189 |         params: Parameters for the command
190 |         client_id: ID of the client executing the command
191 |         
192 |     Returns:
193 |         Tuple of (success, message, data)
194 |     """
195 |     commands = [
196 |         {"name": "show_notification", "description": "Display a Windows 10 toast notification"},
197 |         {"name": "list_commands", "description": "List available commands"}
198 |     ]
199 |     
200 |     return True, "Commands retrieved successfully", {"commands": commands}
201 | 
202 | 
203 | def validate_show_notification(params: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
204 |     """
205 |     Validate parameters for the 'show_notification' command.
206 |     
207 |     Args:
208 |         params: Parameters to validate
209 |         
210 |     Returns:
211 |         Tuple of (is_valid, error_message)
212 |     """
213 |     if "title" not in params:
214 |         return False, "Missing required parameter: title"
215 |     
216 |     if "message" not in params:
217 |         return False, "Missing required parameter: message"
218 |     
219 |     if not isinstance(params.get("title"), str):
220 |         return False, "Title must be a string"
221 |     
222 |     if not isinstance(params.get("message"), str):
223 |         return False, "Message must be a string"
224 |     
225 |     if "type" in params and params["type"] not in ["info", "warning", "error", "success"]:
226 |         return False, "Invalid notification type"
227 |     
228 |     if "duration" in params and not isinstance(params["duration"], int):
229 |         return False, "Duration must be an integer"
230 |     
231 |     return True, None
232 | 
233 | 
234 | DEFAULT_COMMANDS = [
235 |     Command(
236 |         name="show_notification",
237 |         handler=handle_show_notification,
238 |         validator=validate_show_notification,
239 |         description="Display a Windows 10 toast notification"
240 |     ),
241 |     Command(
242 |         name="list_commands",
243 |         handler=handle_list_commands,
244 |         description="List available commands"
245 |     )
246 | ]
247 | 
248 | 
249 | command_processor = CommandProcessor()
250 | 
251 | command_processor.register_commands(DEFAULT_COMMANDS)
252 | 
253 | 
254 | async def process_command_message(message_data: Dict[str, Any], client_id: str) -> MCPMessage:
255 |     """
256 |     Process a command message and return a response.
257 |     
258 |     Args:
259 |         message_data: Dictionary containing the command message data
260 |         client_id: ID of the client sending the command
261 |         
262 |     Returns:
263 |         Response message to send back to the client
264 |     """
265 |     command_name = message_data.get("command")
266 |     params = message_data.get("params", {})
267 |     
268 |     if not command_name:
269 |         return ErrorMessage(400, "Missing command name")
270 |     
271 |     success, message, data = await command_processor.process_command(command_name, params, client_id)
272 |     
273 |     if success:
274 |         return ResponseMessage(True, message, data)
275 |     else:
276 |         return ErrorMessage(400, message)
277 | 
```

--------------------------------------------------------------------------------
/tests/test_connection.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the client connection management implementation.
  3 | """
  4 | 
  5 | import unittest
  6 | import asyncio
  7 | import json
  8 | from unittest.mock import patch, MagicMock, AsyncMock
  9 | from src.server.connection import (
 10 |     ClientConnection, ConnectionManager, run_server
 11 | )
 12 | from src.mcp.protocol import (
 13 |     MCPMessage, NotificationMessage, ResponseMessage, ErrorMessage,
 14 |     PingMessage, PongMessage, MessageType, NotificationType
 15 | )
 16 | 
 17 | 
 18 | class TestClientConnection(unittest.TestCase):
 19 |     """Test cases for the ClientConnection class."""
 20 |     
 21 |     def setUp(self):
 22 |         """Set up test fixtures."""
 23 |         self.reader = AsyncMock(spec=asyncio.StreamReader)
 24 |         self.writer = AsyncMock(spec=asyncio.StreamWriter)
 25 |         self.server = MagicMock(spec=ConnectionManager)
 26 |         
 27 |         self.writer.get_extra_info.return_value = ('127.0.0.1', 12345)
 28 |         
 29 |         self.client = ClientConnection(
 30 |             self.reader, self.writer, "test_client", self.server
 31 |         )
 32 |     
 33 |     @patch('src.server.connection.logger')
 34 |     async def test_handle_connection(self, mock_logger):
 35 |         """Test handling a client connection."""
 36 |         self.reader.readline.side_effect = [
 37 |             b'{"type": "ping", "data": {}}\n',
 38 |             b''
 39 |         ]
 40 |         
 41 |         await self.client.handle()
 42 |         
 43 |         self.reader.readline.assert_called()
 44 |         
 45 |         self.assertFalse(self.client.connected)
 46 |         self.server.remove_client.assert_called_once_with("test_client")
 47 |         self.writer.close.assert_called_once()
 48 |         self.writer.wait_closed.assert_called_once()
 49 |     
 50 |     @patch('src.server.connection.logger')
 51 |     async def test_process_message_ping(self, mock_logger):
 52 |         """Test processing a ping message."""
 53 |         await self.client._process_message('{"type": "ping", "data": {}}')
 54 |         
 55 |         self.writer.write.assert_called_once()
 56 |         written_data = self.writer.write.call_args[0][0].decode()
 57 |         self.assertIn('"type": "pong"', written_data)
 58 |     
 59 |     @patch('src.server.connection.show_notification')
 60 |     @patch('src.server.connection.logger')
 61 |     async def test_process_message_notification(self, mock_logger, mock_show_notification):
 62 |         """Test processing a notification message."""
 63 |         mock_show_notification.return_value = True
 64 |         
 65 |         await self.client._process_message(
 66 |             '{"type": "notification", "data": {"title": "Test", "message": "Test message"}}'
 67 |         )
 68 |         
 69 |         mock_show_notification.assert_called_once_with(
 70 |             "Test", "Test message", "info", 5
 71 |         )
 72 |         
 73 |         self.writer.write.assert_called_once()
 74 |         written_data = self.writer.write.call_args[0][0].decode()
 75 |         self.assertIn('"type": "response"', written_data)
 76 |         self.assertIn('"success": true', written_data)
 77 |     
 78 |     @patch('src.server.connection.logger')
 79 |     async def test_process_message_invalid_json(self, mock_logger):
 80 |         """Test processing an invalid JSON message."""
 81 |         await self.client._process_message('invalid json')
 82 |         
 83 |         self.writer.write.assert_called_once()
 84 |         written_data = self.writer.write.call_args[0][0].decode()
 85 |         self.assertIn('"type": "error"', written_data)
 86 |         self.assertIn('"code": 400', written_data)
 87 |     
 88 |     @patch('src.server.connection.logger')
 89 |     async def test_send_message(self, mock_logger):
 90 |         """Test sending a message to the client."""
 91 |         message = PingMessage()
 92 |         
 93 |         await self.client.send_message(message)
 94 |         
 95 |         self.writer.write.assert_called_once()
 96 |         self.writer.drain.assert_called_once()
 97 |         
 98 |         written_data = self.writer.write.call_args[0][0].decode()
 99 |         self.assertIn('"type": "ping"', written_data)
100 |     
101 |     @patch('src.server.connection.logger')
102 |     async def test_close(self, mock_logger):
103 |         """Test closing the client connection."""
104 |         await self.client.close()
105 |         
106 |         self.assertFalse(self.client.connected)
107 |         self.server.remove_client.assert_called_once_with("test_client")
108 |         self.writer.close.assert_called_once()
109 |         self.writer.wait_closed.assert_called_once()
110 |         
111 |         self.server.remove_client.reset_mock()
112 |         self.writer.close.reset_mock()
113 |         self.writer.wait_closed.reset_mock()
114 |         
115 |         await self.client.close()
116 |         
117 |         self.server.remove_client.assert_not_called()
118 |         self.writer.close.assert_not_called()
119 |         self.writer.wait_closed.assert_not_called()
120 | 
121 | 
122 | class TestConnectionManager(unittest.TestCase):
123 |     """Test cases for the ConnectionManager class."""
124 |     
125 |     def setUp(self):
126 |         """Set up test fixtures."""
127 |         self.manager = ConnectionManager("127.0.0.1", 8765)
128 |     
129 |     @patch('src.server.connection.asyncio.start_server')
130 |     @patch('src.server.connection.logger')
131 |     async def test_start_server(self, mock_logger, mock_start_server):
132 |         """Test starting the server."""
133 |         mock_server = AsyncMock()
134 |         mock_server.sockets = [MagicMock()]
135 |         mock_server.sockets[0].getsockname.return_value = ('127.0.0.1', 8765)
136 |         mock_start_server.return_value = mock_server
137 |         
138 |         task = asyncio.create_task(self.manager.start())
139 |         
140 |         await asyncio.sleep(0.1)
141 |         
142 |         task.cancel()
143 |         
144 |         try:
145 |             await task
146 |         except asyncio.CancelledError:
147 |             pass
148 |         
149 |         mock_start_server.assert_called_once_with(
150 |             self.manager._handle_new_connection, "127.0.0.1", 8765
151 |         )
152 |     
153 |     @patch('src.server.connection.ClientConnection')
154 |     @patch('src.server.connection.asyncio.create_task')
155 |     @patch('src.server.connection.logger')
156 |     async def test_handle_new_connection(self, mock_logger, mock_create_task, mock_client_connection):
157 |         """Test handling a new connection."""
158 |         reader = AsyncMock(spec=asyncio.StreamReader)
159 |         writer = AsyncMock(spec=asyncio.StreamWriter)
160 |         mock_client = MagicMock()
161 |         mock_client_connection.return_value = mock_client
162 |         
163 |         await self.manager._handle_new_connection(reader, writer)
164 |         
165 |         mock_client_connection.assert_called_once_with(
166 |             reader, writer, "client_1", self.manager
167 |         )
168 |         
169 |         self.assertEqual(len(self.manager.clients), 1)
170 |         self.assertEqual(self.manager.clients["client_1"], mock_client)
171 |         
172 |         mock_create_task.assert_called_once_with(mock_client.handle())
173 |     
174 |     @patch('src.server.connection.logger')
175 |     def test_remove_client(self, mock_logger):
176 |         """Test removing a client."""
177 |         self.manager.clients["test_client"] = MagicMock()
178 |         
179 |         self.manager.remove_client("test_client")
180 |         
181 |         self.assertEqual(len(self.manager.clients), 0)
182 |         
183 |         self.manager.remove_client("non_existent_client")
184 |     
185 |     @patch('src.server.connection.logger')
186 |     async def test_broadcast(self, mock_logger):
187 |         """Test broadcasting a message to all clients."""
188 |         client1 = MagicMock()
189 |         client1.send_message = AsyncMock()
190 |         client2 = MagicMock()
191 |         client2.send_message = AsyncMock()
192 |         
193 |         self.manager.clients["client1"] = client1
194 |         self.manager.clients["client2"] = client2
195 |         
196 |         message = PingMessage()
197 |         
198 |         await self.manager.broadcast(message)
199 |         
200 |         client1.send_message.assert_called_once_with(message)
201 |         client2.send_message.assert_called_once_with(message)
202 |         
203 |         client1.send_message.reset_mock()
204 |         client2.send_message.reset_mock()
205 |         
206 |         await self.manager.broadcast(message, exclude="client1")
207 |         
208 |         client1.send_message.assert_not_called()
209 |         client2.send_message.assert_called_once_with(message)
210 |     
211 |     @patch('src.server.connection.logger')
212 |     async def test_stop(self, mock_logger):
213 |         """Test stopping the server."""
214 |         client1 = MagicMock()
215 |         client1.close = AsyncMock()
216 |         client2 = MagicMock()
217 |         client2.close = AsyncMock()
218 |         
219 |         self.manager.clients["client1"] = client1
220 |         self.manager.clients["client2"] = client2
221 |         
222 |         self.manager.server = MagicMock()
223 |         self.manager.server.wait_closed = AsyncMock()
224 |         
225 |         await self.manager.stop()
226 |         
227 |         client1.close.assert_called_once()
228 |         client2.close.assert_called_once()
229 |         
230 |         self.manager.server.close.assert_called_once()
231 |         self.manager.server.wait_closed.assert_called_once()
232 | 
233 | 
234 | @patch('src.server.connection.ConnectionManager')
235 | @patch('src.server.connection.logging')
236 | async def test_run_server(mock_logging, mock_manager_class):
237 |     """Test running the server."""
238 |     mock_manager = AsyncMock()
239 |     mock_manager_class.return_value = mock_manager
240 |     
241 |     await run_server("127.0.0.1", 8765)
242 |     
243 |     mock_logging.basicConfig.assert_called_once()
244 |     
245 |     mock_manager_class.assert_called_once_with("127.0.0.1", 8765)
246 |     mock_manager.start.assert_called_once()
247 | 
248 | 
249 | if __name__ == "__main__":
250 |     unittest.main()
251 | 
```

--------------------------------------------------------------------------------
/tests/test_macos.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the macOS Notification implementation.
  3 | """
  4 | 
  5 | import unittest
  6 | from unittest.mock import patch, MagicMock
  7 | import platform
  8 | import subprocess
  9 | from src.notification.macos import (
 10 |     MacOSNotificationManager, MacOSNotificationFactory, 
 11 |     show_macos_notification, NotificationType
 12 | )
 13 | 
 14 | 
 15 | class TestMacOSNotification(unittest.TestCase):
 16 |     """Test cases for the macOS Notification implementation."""
 17 |     
 18 |     @patch('src.notification.macos.platform.system')
 19 |     def test_is_macos(self, mock_system):
 20 |         """Test platform detection."""
 21 |         mock_system.return_value = "Darwin"
 22 |         manager = MacOSNotificationManager()
 23 |         self.assertTrue(manager._is_macos())
 24 |         
 25 |         mock_system.return_value = "Linux"
 26 |         manager = MacOSNotificationManager()
 27 |         self.assertFalse(manager._is_macos())
 28 |     
 29 |     @patch('src.notification.macos.platform.system')
 30 |     @patch('src.notification.macos.subprocess.run')
 31 |     def test_show_notification(self, mock_run, mock_system):
 32 |         """Test showing a notification."""
 33 |         mock_system.return_value = "Darwin"
 34 |         mock_process = MagicMock()
 35 |         mock_process.returncode = 0
 36 |         mock_run.return_value = mock_process
 37 |         
 38 |         manager = MacOSNotificationManager()
 39 |         result = manager.show_notification(
 40 |             title="Test Title",
 41 |             message="Test Message",
 42 |             notification_type=NotificationType.INFO
 43 |         )
 44 |         
 45 |         self.assertTrue(result)
 46 |         mock_run.assert_called_once()
 47 |         
 48 |         args, kwargs = mock_run.call_args
 49 |         self.assertEqual(args[0][0], "osascript")
 50 |         self.assertEqual(args[0][1], "-e")
 51 |         self.assertIn("display notification", args[0][2])
 52 |         self.assertIn("Test Message", args[0][2])
 53 |         self.assertIn("Test Title", args[0][2])
 54 |     
 55 |     @patch('src.notification.macos.platform.system')
 56 |     @patch('src.notification.macos.subprocess.run')
 57 |     def test_show_notification_with_subtitle(self, mock_run, mock_system):
 58 |         """Test showing a notification with a subtitle."""
 59 |         mock_system.return_value = "Darwin"
 60 |         mock_process = MagicMock()
 61 |         mock_process.returncode = 0
 62 |         mock_run.return_value = mock_process
 63 |         
 64 |         manager = MacOSNotificationManager()
 65 |         result = manager.show_notification(
 66 |             title="Test Title",
 67 |             message="Test Message",
 68 |             subtitle="Test Subtitle",
 69 |             notification_type=NotificationType.INFO
 70 |         )
 71 |         
 72 |         self.assertTrue(result)
 73 |         mock_run.assert_called_once()
 74 |         
 75 |         args, kwargs = mock_run.call_args
 76 |         self.assertIn("subtitle", args[0][2])
 77 |         self.assertIn("Test Subtitle", args[0][2])
 78 |     
 79 |     @patch('src.notification.macos.platform.system')
 80 |     @patch('src.notification.macos.subprocess.run')
 81 |     def test_show_notification_without_sound(self, mock_run, mock_system):
 82 |         """Test showing a notification without sound."""
 83 |         mock_system.return_value = "Darwin"
 84 |         mock_process = MagicMock()
 85 |         mock_process.returncode = 0
 86 |         mock_run.return_value = mock_process
 87 |         
 88 |         manager = MacOSNotificationManager()
 89 |         result = manager.show_notification(
 90 |             title="Test Title",
 91 |             message="Test Message",
 92 |             sound=False,
 93 |             notification_type=NotificationType.INFO
 94 |         )
 95 |         
 96 |         self.assertTrue(result)
 97 |         mock_run.assert_called_once()
 98 |         
 99 |         args, kwargs = mock_run.call_args
100 |         self.assertNotIn("sound name", args[0][2])
101 |     
102 |     @patch('src.notification.macos.platform.system')
103 |     @patch('src.notification.macos.subprocess.run')
104 |     def test_show_notification_on_non_macos(self, mock_run, mock_system):
105 |         """Test showing a notification on a non-macOS platform."""
106 |         mock_system.return_value = "Linux"
107 |         
108 |         manager = MacOSNotificationManager()
109 |         result = manager.show_notification(
110 |             title="Test Title",
111 |             message="Test Message",
112 |             notification_type=NotificationType.INFO
113 |         )
114 |         
115 |         self.assertFalse(result)
116 |         mock_run.assert_not_called()
117 |     
118 |     @patch('src.notification.macos.platform.system')
119 |     @patch('src.notification.macos.subprocess.run')
120 |     def test_show_notification_with_subprocess_error(self, mock_run, mock_system):
121 |         """Test showing a notification with a subprocess error."""
122 |         mock_system.return_value = "Darwin"
123 |         mock_run.side_effect = subprocess.SubprocessError("Test error")
124 |         
125 |         manager = MacOSNotificationManager()
126 |         result = manager.show_notification(
127 |             title="Test Title",
128 |             message="Test Message",
129 |             notification_type=NotificationType.INFO
130 |         )
131 |         
132 |         self.assertFalse(result)
133 |     
134 |     @patch('src.notification.macos.platform.system')
135 |     @patch('src.notification.macos.subprocess.run')
136 |     def test_show_notification_with_return_code_error(self, mock_run, mock_system):
137 |         """Test showing a notification with a return code error."""
138 |         mock_system.return_value = "Darwin"
139 |         mock_process = MagicMock()
140 |         mock_process.returncode = 1
141 |         mock_process.stderr = "Test error"
142 |         mock_run.return_value = mock_process
143 |         
144 |         manager = MacOSNotificationManager()
145 |         result = manager.show_notification(
146 |             title="Test Title",
147 |             message="Test Message",
148 |             notification_type=NotificationType.INFO
149 |         )
150 |         
151 |         self.assertFalse(result)
152 |     
153 |     @patch('src.notification.macos.MacOSNotificationManager')
154 |     def test_notification_factory(self, mock_manager_class):
155 |         """Test the notification factory."""
156 |         mock_manager = MagicMock()
157 |         mock_manager_class.return_value = mock_manager
158 |         mock_manager.show_notification.return_value = True
159 |         
160 |         factory = MacOSNotificationFactory()
161 |         
162 |         result = factory.create_info_notification("Info Title", "Info Message", "Info Subtitle")
163 |         self.assertTrue(result)
164 |         mock_manager.show_notification.assert_called_with(
165 |             title="Info Title",
166 |             message="Info Message",
167 |             notification_type=NotificationType.INFO,
168 |             subtitle="Info Subtitle"
169 |         )
170 |         
171 |         result = factory.create_warning_notification("Warning Title", "Warning Message")
172 |         self.assertTrue(result)
173 |         mock_manager.show_notification.assert_called_with(
174 |             title="Warning Title",
175 |             message="Warning Message",
176 |             notification_type=NotificationType.WARNING,
177 |             subtitle=None
178 |         )
179 |         
180 |         result = factory.create_error_notification("Error Title", "Error Message")
181 |         self.assertTrue(result)
182 |         mock_manager.show_notification.assert_called_with(
183 |             title="Error Title",
184 |             message="Error Message",
185 |             notification_type=NotificationType.ERROR,
186 |             subtitle=None
187 |         )
188 |         
189 |         result = factory.create_success_notification("Success Title", "Success Message")
190 |         self.assertTrue(result)
191 |         mock_manager.show_notification.assert_called_with(
192 |             title="Success Title",
193 |             message="Success Message",
194 |             notification_type=NotificationType.SUCCESS,
195 |             subtitle=None
196 |         )
197 |     
198 |     @patch('src.notification.macos.macos_notification_factory')
199 |     def test_show_macos_notification_helper(self, mock_factory):
200 |         """Test the show_macos_notification helper function."""
201 |         mock_factory.create_info_notification.return_value = True
202 |         mock_factory.create_warning_notification.return_value = True
203 |         mock_factory.create_error_notification.return_value = True
204 |         mock_factory.create_success_notification.return_value = True
205 |         
206 |         result = show_macos_notification("Info Title", "Info Message", "info", "Info Subtitle")
207 |         self.assertTrue(result)
208 |         mock_factory.create_info_notification.assert_called_with("Info Title", "Info Message", "Info Subtitle")
209 |         
210 |         result = show_macos_notification("Warning Title", "Warning Message", "warning")
211 |         self.assertTrue(result)
212 |         mock_factory.create_warning_notification.assert_called_with("Warning Title", "Warning Message", None)
213 |         
214 |         result = show_macos_notification("Error Title", "Error Message", "error")
215 |         self.assertTrue(result)
216 |         mock_factory.create_error_notification.assert_called_with("Error Title", "Error Message", None)
217 |         
218 |         result = show_macos_notification("Success Title", "Success Message", "success")
219 |         self.assertTrue(result)
220 |         mock_factory.create_success_notification.assert_called_with("Success Title", "Success Message", None)
221 |         
222 |         result = show_macos_notification("Invalid Title", "Invalid Message", "invalid")
223 |         self.assertTrue(result)
224 |         mock_factory.create_info_notification.assert_called_with("Invalid Title", "Invalid Message", None)
225 | 
226 | 
227 | if __name__ == "__main__":
228 |     unittest.main()
229 | 
```

--------------------------------------------------------------------------------
/src/mcp/protocol.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | MCP (Model Context Protocol) implementation for toast-mcp-server.
  3 | 
  4 | This module defines the protocol specification and message handling for
  5 | communication between MCP clients and the notification server.
  6 | """
  7 | 
  8 | import json
  9 | import logging
 10 | from enum import Enum
 11 | from typing import Dict, Any, Optional, List, Union
 12 | 
 13 | logger = logging.getLogger(__name__)
 14 | 
 15 | class MessageType(Enum):
 16 |     """Enum defining the types of messages in the MCP protocol."""
 17 |     NOTIFICATION = "notification"
 18 |     RESPONSE = "response"
 19 |     ERROR = "error"
 20 |     PING = "ping"
 21 |     PONG = "pong"
 22 | 
 23 | 
 24 | class NotificationType(Enum):
 25 |     """Enum defining the types of notifications supported."""
 26 |     INFO = "info"
 27 |     WARNING = "warning"
 28 |     ERROR = "error"
 29 |     SUCCESS = "success"
 30 | 
 31 | 
 32 | class MCPMessage:
 33 |     """Base class for MCP protocol messages."""
 34 |     
 35 |     def __init__(self, msg_type: MessageType, data: Dict[str, Any] = None):
 36 |         """
 37 |         Initialize a new MCP message.
 38 |         
 39 |         Args:
 40 |             msg_type: The type of message
 41 |             data: Optional data payload for the message
 42 |         """
 43 |         self.msg_type = msg_type
 44 |         self.data = data or {}
 45 |         
 46 |     def to_dict(self) -> Dict[str, Any]:
 47 |         """Convert the message to a dictionary representation."""
 48 |         return {
 49 |             "type": self.msg_type.value,
 50 |             "data": self.data
 51 |         }
 52 |     
 53 |     def to_json(self) -> str:
 54 |         """Convert the message to a JSON string."""
 55 |         return json.dumps(self.to_dict())
 56 |     
 57 |     @classmethod
 58 |     def from_dict(cls, data: Dict[str, Any]) -> 'MCPMessage':
 59 |         """
 60 |         Create a message from a dictionary.
 61 |         
 62 |         Args:
 63 |             data: Dictionary containing message data
 64 |             
 65 |         Returns:
 66 |             An MCPMessage instance
 67 |             
 68 |         Raises:
 69 |             ValueError: If the message type is invalid or required fields are missing
 70 |         """
 71 |         if "type" not in data:
 72 |             raise ValueError("Message missing required 'type' field")
 73 |         
 74 |         try:
 75 |             msg_type = MessageType(data["type"])
 76 |         except ValueError:
 77 |             raise ValueError(f"Invalid message type: {data['type']}")
 78 |         
 79 |         msg_data = data.get("data", {})
 80 |         
 81 |         if msg_type == MessageType.NOTIFICATION:
 82 |             return NotificationMessage.from_dict(data)
 83 |         elif msg_type == MessageType.RESPONSE:
 84 |             return ResponseMessage.from_dict(data)
 85 |         elif msg_type == MessageType.ERROR:
 86 |             return ErrorMessage.from_dict(data)
 87 |         elif msg_type == MessageType.PING:
 88 |             return PingMessage()
 89 |         elif msg_type == MessageType.PONG:
 90 |             return PongMessage()
 91 |         
 92 |         return cls(msg_type, msg_data)
 93 |     
 94 |     @classmethod
 95 |     def from_json(cls, json_str: str) -> 'MCPMessage':
 96 |         """
 97 |         Create a message from a JSON string.
 98 |         
 99 |         Args:
100 |             json_str: JSON string containing message data
101 |             
102 |         Returns:
103 |             An MCPMessage instance
104 |             
105 |         Raises:
106 |             ValueError: If the JSON is invalid or the message format is incorrect
107 |         """
108 |         try:
109 |             data = json.loads(json_str)
110 |         except json.JSONDecodeError:
111 |             raise ValueError("Invalid JSON format")
112 |         
113 |         return cls.from_dict(data)
114 | 
115 | 
116 | class NotificationMessage(MCPMessage):
117 |     """Message for sending notifications to the server."""
118 |     
119 |     def __init__(self, 
120 |                  title: str, 
121 |                  message: str, 
122 |                  notification_type: NotificationType = NotificationType.INFO,
123 |                  duration: int = 5,
124 |                  client_id: str = None,
125 |                  icon: str = None,
126 |                  actions: List[Dict[str, str]] = None):
127 |         """
128 |         Initialize a new notification message.
129 |         
130 |         Args:
131 |             title: Title of the notification
132 |             message: Content of the notification
133 |             notification_type: Type of notification (info, warning, error, success)
134 |             duration: Duration to display the notification in seconds
135 |             client_id: Optional identifier for the client sending the notification
136 |             icon: Optional icon to display with the notification
137 |             actions: Optional list of actions that can be taken on the notification
138 |         """
139 |         data = {
140 |             "title": title,
141 |             "message": message,
142 |             "notification_type": notification_type.value,
143 |             "duration": duration
144 |         }
145 |         
146 |         if client_id:
147 |             data["client_id"] = client_id
148 |             
149 |         if icon:
150 |             data["icon"] = icon
151 |             
152 |         if actions:
153 |             data["actions"] = actions
154 |             
155 |         super().__init__(MessageType.NOTIFICATION, data)
156 |     
157 |     @classmethod
158 |     def from_dict(cls, data: Dict[str, Any]) -> 'NotificationMessage':
159 |         """Create a NotificationMessage from a dictionary."""
160 |         msg_data = data.get("data", {})
161 |         
162 |         if "title" not in msg_data or "message" not in msg_data:
163 |             raise ValueError("Notification message missing required fields: title and message")
164 |         
165 |         try:
166 |             notification_type = NotificationType(msg_data.get("notification_type", "info"))
167 |         except ValueError:
168 |             notification_type = NotificationType.INFO
169 |             logger.warning(f"Invalid notification type: {msg_data.get('notification_type')}, using INFO")
170 |         
171 |         return cls(
172 |             title=msg_data["title"],
173 |             message=msg_data["message"],
174 |             notification_type=notification_type,
175 |             duration=msg_data.get("duration", 5),
176 |             client_id=msg_data.get("client_id"),
177 |             icon=msg_data.get("icon"),
178 |             actions=msg_data.get("actions")
179 |         )
180 | 
181 | 
182 | class ResponseMessage(MCPMessage):
183 |     """Message for server responses to clients."""
184 |     
185 |     def __init__(self, success: bool, message: str = None, data: Dict[str, Any] = None):
186 |         """
187 |         Initialize a new response message.
188 |         
189 |         Args:
190 |             success: Whether the operation was successful
191 |             message: Optional message describing the result
192 |             data: Optional additional data to include in the response
193 |         """
194 |         response_data = {
195 |             "success": success
196 |         }
197 |         
198 |         if message:
199 |             response_data["message"] = message
200 |             
201 |         if data:
202 |             response_data["data"] = data
203 |             
204 |         super().__init__(MessageType.RESPONSE, response_data)
205 |     
206 |     @classmethod
207 |     def from_dict(cls, data: Dict[str, Any]) -> 'ResponseMessage':
208 |         """Create a ResponseMessage from a dictionary."""
209 |         msg_data = data.get("data", {})
210 |         
211 |         if "success" not in msg_data:
212 |             raise ValueError("Response message missing required field: success")
213 |         
214 |         return cls(
215 |             success=msg_data["success"],
216 |             message=msg_data.get("message"),
217 |             data=msg_data.get("data")
218 |         )
219 | 
220 | 
221 | class ErrorMessage(MCPMessage):
222 |     """Message for error responses."""
223 |     
224 |     def __init__(self, error_code: int, error_message: str, details: Any = None):
225 |         """
226 |         Initialize a new error message.
227 |         
228 |         Args:
229 |             error_code: Numeric error code
230 |             error_message: Human-readable error message
231 |             details: Optional additional error details
232 |         """
233 |         error_data = {
234 |             "code": error_code,
235 |             "message": error_message
236 |         }
237 |         
238 |         if details:
239 |             error_data["details"] = details
240 |             
241 |         super().__init__(MessageType.ERROR, error_data)
242 |     
243 |     @classmethod
244 |     def from_dict(cls, data: Dict[str, Any]) -> 'ErrorMessage':
245 |         """Create an ErrorMessage from a dictionary."""
246 |         msg_data = data.get("data", {})
247 |         
248 |         if "code" not in msg_data or "message" not in msg_data:
249 |             raise ValueError("Error message missing required fields: code and message")
250 |         
251 |         return cls(
252 |             error_code=msg_data["code"],
253 |             error_message=msg_data["message"],
254 |             details=msg_data.get("details")
255 |         )
256 | 
257 | 
258 | class PingMessage(MCPMessage):
259 |     """Message for ping requests."""
260 |     
261 |     def __init__(self):
262 |         """Initialize a new ping message."""
263 |         super().__init__(MessageType.PING)
264 | 
265 | 
266 | class PongMessage(MCPMessage):
267 |     """Message for pong responses."""
268 |     
269 |     def __init__(self):
270 |         """Initialize a new pong message."""
271 |         super().__init__(MessageType.PONG)
272 | 
273 | 
274 | def parse_message(data: Union[str, Dict[str, Any]]) -> MCPMessage:
275 |     """
276 |     Parse a message from either a JSON string or a dictionary.
277 |     
278 |     Args:
279 |         data: JSON string or dictionary containing message data
280 |         
281 |     Returns:
282 |         An MCPMessage instance
283 |         
284 |     Raises:
285 |         ValueError: If the message format is invalid
286 |     """
287 |     if isinstance(data, str):
288 |         return MCPMessage.from_json(data)
289 |     elif isinstance(data, dict):
290 |         return MCPMessage.from_dict(data)
291 |     else:
292 |         raise ValueError(f"Unsupported message format: {type(data)}")
293 | 
```

--------------------------------------------------------------------------------
/src/notification/macos.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | macOS Notification implementation for toast-mcp-server.
  3 | 
  4 | This module provides functionality to display macOS notifications
  5 | using the osascript command to interact with the macOS Notification Center.
  6 | """
  7 | 
  8 | import logging
  9 | import subprocess
 10 | import platform
 11 | from typing import Dict, Any, Optional, List, Union
 12 | 
 13 | from src.mcp.protocol import NotificationType
 14 | 
 15 | logger = logging.getLogger(__name__)
 16 | 
 17 | 
 18 | class MacOSNotificationManager:
 19 |     """
 20 |     Manager for macOS Notifications.
 21 |     
 22 |     This class handles the creation and display of macOS notifications
 23 |     using the osascript command to interact with the Notification Center.
 24 |     """
 25 |     
 26 |     def __init__(self):
 27 |         """Initialize the macOS notification manager."""
 28 |         if not self._is_macos():
 29 |             logger.warning("MacOSNotificationManager initialized on non-macOS platform")
 30 |         logger.info("macOS notification manager initialized")
 31 |     
 32 |     def _is_macos(self) -> bool:
 33 |         """
 34 |         Check if the current platform is macOS.
 35 |         
 36 |         Returns:
 37 |             True if the current platform is macOS, False otherwise
 38 |         """
 39 |         return platform.system() == "Darwin"
 40 |     
 41 |     def show_notification(self,
 42 |                          title: str,
 43 |                          message: str,
 44 |                          notification_type: NotificationType = NotificationType.INFO,
 45 |                          duration: int = 5,
 46 |                          sound: bool = True,
 47 |                          subtitle: Optional[str] = None) -> bool:
 48 |         """
 49 |         Show a macOS notification.
 50 |         
 51 |         Args:
 52 |             title: Title of the notification
 53 |             message: Content of the notification
 54 |             notification_type: Type of notification (info, warning, error, success)
 55 |             duration: Duration parameter is ignored on macOS (included for API compatibility)
 56 |             sound: Whether to play a sound with the notification
 57 |             subtitle: Optional subtitle for the notification
 58 |             
 59 |         Returns:
 60 |             True if the notification was successfully displayed, False otherwise
 61 |         """
 62 |         if not self._is_macos():
 63 |             logger.error("Cannot show macOS notification on non-macOS platform")
 64 |             return False
 65 |         
 66 |         logger.debug(f"Showing notification: {title} ({notification_type.value})")
 67 |         
 68 |         try:
 69 |             script = self._build_notification_script(title, message, subtitle, sound)
 70 |             
 71 |             result = subprocess.run(
 72 |                 ["osascript", "-e", script],
 73 |                 capture_output=True,
 74 |                 text=True,
 75 |                 check=True
 76 |             )
 77 |             
 78 |             if result.returncode == 0:
 79 |                 return True
 80 |             else:
 81 |                 logger.error(f"Failed to show notification: {result.stderr}")
 82 |                 return False
 83 |                 
 84 |         except subprocess.SubprocessError as e:
 85 |             logger.error(f"Failed to show notification: {str(e)}")
 86 |             return False
 87 |         except Exception as e:
 88 |             logger.error(f"Unexpected error showing notification: {str(e)}")
 89 |             return False
 90 |     
 91 |     def _build_notification_script(self, 
 92 |                                   title: str, 
 93 |                                   message: str, 
 94 |                                   subtitle: Optional[str] = None,
 95 |                                   sound: bool = True) -> str:
 96 |         """
 97 |         Build the AppleScript command for displaying a notification.
 98 |         
 99 |         Args:
100 |             title: Title of the notification
101 |             message: Content of the notification
102 |             subtitle: Optional subtitle for the notification
103 |             sound: Whether to play a sound with the notification
104 |             
105 |         Returns:
106 |             AppleScript command string
107 |         """
108 |         title_escaped = title.replace('"', '\\"')
109 |         message_escaped = message.replace('"', '\\"')
110 |         
111 |         script = f'display notification "{message_escaped}" with title "{title_escaped}"'
112 |         
113 |         if subtitle:
114 |             subtitle_escaped = subtitle.replace('"', '\\"')
115 |             script += f' subtitle "{subtitle_escaped}"'
116 |         
117 |         if sound:
118 |             script += " sound name \"Ping\""
119 |         
120 |         return script
121 | 
122 | 
123 | class MacOSNotificationFactory:
124 |     """
125 |     Factory for creating macOS notifications based on notification type.
126 |     
127 |     This class provides methods for creating and displaying different types
128 |     of notifications with appropriate default settings.
129 |     """
130 |     
131 |     def __init__(self):
132 |         """Initialize the notification factory."""
133 |         self.notification_manager = MacOSNotificationManager()
134 |     
135 |     def create_info_notification(self, title: str, message: str, subtitle: Optional[str] = None) -> bool:
136 |         """
137 |         Create and show an information notification.
138 |         
139 |         Args:
140 |             title: Title of the notification
141 |             message: Content of the notification
142 |             subtitle: Optional subtitle for the notification
143 |             
144 |         Returns:
145 |             True if the notification was successfully displayed, False otherwise
146 |         """
147 |         return self.notification_manager.show_notification(
148 |             title=title,
149 |             message=message,
150 |             notification_type=NotificationType.INFO,
151 |             subtitle=subtitle
152 |         )
153 |     
154 |     def create_warning_notification(self, title: str, message: str, subtitle: Optional[str] = None) -> bool:
155 |         """
156 |         Create and show a warning notification.
157 |         
158 |         Args:
159 |             title: Title of the notification
160 |             message: Content of the notification
161 |             subtitle: Optional subtitle for the notification
162 |             
163 |         Returns:
164 |             True if the notification was successfully displayed, False otherwise
165 |         """
166 |         return self.notification_manager.show_notification(
167 |             title=title,
168 |             message=message,
169 |             notification_type=NotificationType.WARNING,
170 |             subtitle=subtitle
171 |         )
172 |     
173 |     def create_error_notification(self, title: str, message: str, subtitle: Optional[str] = None) -> bool:
174 |         """
175 |         Create and show an error notification.
176 |         
177 |         Args:
178 |             title: Title of the notification
179 |             message: Content of the notification
180 |             subtitle: Optional subtitle for the notification
181 |             
182 |         Returns:
183 |             True if the notification was successfully displayed, False otherwise
184 |         """
185 |         return self.notification_manager.show_notification(
186 |             title=title,
187 |             message=message,
188 |             notification_type=NotificationType.ERROR,
189 |             subtitle=subtitle
190 |         )
191 |     
192 |     def create_success_notification(self, title: str, message: str, subtitle: Optional[str] = None) -> bool:
193 |         """
194 |         Create and show a success notification.
195 |         
196 |         Args:
197 |             title: Title of the notification
198 |             message: Content of the notification
199 |             subtitle: Optional subtitle for the notification
200 |             
201 |         Returns:
202 |             True if the notification was successfully displayed, False otherwise
203 |         """
204 |         return self.notification_manager.show_notification(
205 |             title=title,
206 |             message=message,
207 |             notification_type=NotificationType.SUCCESS,
208 |             subtitle=subtitle
209 |         )
210 | 
211 | 
212 | macos_notification_factory = MacOSNotificationFactory()
213 | 
214 | 
215 | def show_macos_notification(title: str, message: str, notification_type: str = "info", subtitle: Optional[str] = None) -> bool:
216 |     """
217 |     Show a macOS notification with the specified parameters.
218 |     
219 |     This is a convenience function for showing notifications without directly
220 |     interacting with the MacOSNotificationFactory or MacOSNotificationManager classes.
221 |     
222 |     Args:
223 |         title: Title of the notification
224 |         message: Content of the notification
225 |         notification_type: Type of notification ("info", "warning", "error", "success")
226 |         subtitle: Optional subtitle for the notification
227 |         
228 |     Returns:
229 |         True if the notification was successfully displayed, False otherwise
230 |     """
231 |     try:
232 |         notification_type_enum = NotificationType(notification_type)
233 |     except ValueError:
234 |         logger.warning(f"Invalid notification type: {notification_type}, using INFO")
235 |         notification_type_enum = NotificationType.INFO
236 |     
237 |     if notification_type_enum == NotificationType.INFO:
238 |         return macos_notification_factory.create_info_notification(title, message, subtitle)
239 |     elif notification_type_enum == NotificationType.WARNING:
240 |         return macos_notification_factory.create_warning_notification(title, message, subtitle)
241 |     elif notification_type_enum == NotificationType.ERROR:
242 |         return macos_notification_factory.create_error_notification(title, message, subtitle)
243 |     elif notification_type_enum == NotificationType.SUCCESS:
244 |         return macos_notification_factory.create_success_notification(title, message, subtitle)
245 |     
246 |     return macos_notification_factory.create_info_notification(title, message, subtitle)
247 | 
```

--------------------------------------------------------------------------------
/tests/test_manager.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the unified notification manager.
  3 | """
  4 | 
  5 | import unittest
  6 | from unittest.mock import patch, MagicMock
  7 | import platform
  8 | from src.notification.manager import (
  9 |     NotificationManager, NotificationFactory, 
 10 |     show_notification, NotificationType
 11 | )
 12 | from src.notification.platform import is_windows, is_macos
 13 | 
 14 | 
 15 | class TestNotificationManager(unittest.TestCase):
 16 |     """Test cases for the NotificationManager class."""
 17 |     
 18 |     @patch('src.notification.manager.get_platform_name')
 19 |     @patch('src.notification.manager.is_windows')
 20 |     @patch('src.notification.manager.is_macos')
 21 |     def test_init_windows(self, mock_is_macos, mock_is_windows, mock_get_platform):
 22 |         """Test initializing the notification manager on Windows."""
 23 |         mock_get_platform.return_value = "windows"
 24 |         mock_is_windows.return_value = True
 25 |         mock_is_macos.return_value = False
 26 |         
 27 |         with patch('src.notification.manager.ToastNotificationManager') as mock_toast:
 28 |             manager = NotificationManager()
 29 |             
 30 |             mock_toast.assert_called_once()
 31 |     
 32 |     @patch('src.notification.manager.get_platform_name')
 33 |     @patch('src.notification.manager.is_windows')
 34 |     @patch('src.notification.manager.is_macos')
 35 |     def test_init_macos(self, mock_is_macos, mock_is_windows, mock_get_platform):
 36 |         """Test initializing the notification manager on macOS."""
 37 |         mock_get_platform.return_value = "macos"
 38 |         mock_is_windows.return_value = False
 39 |         mock_is_macos.return_value = True
 40 |         
 41 |         with patch('src.notification.manager.MacOSNotificationManager') as mock_macos:
 42 |             manager = NotificationManager()
 43 |             
 44 |             mock_macos.assert_called_once()
 45 |     
 46 |     @patch('src.notification.manager.get_platform_name')
 47 |     @patch('src.notification.manager.is_windows')
 48 |     @patch('src.notification.manager.is_macos')
 49 |     def test_init_unsupported(self, mock_is_macos, mock_is_windows, mock_get_platform):
 50 |         """Test initializing the notification manager on an unsupported platform."""
 51 |         mock_get_platform.return_value = "linux"
 52 |         mock_is_windows.return_value = False
 53 |         mock_is_macos.return_value = False
 54 |         
 55 |         manager = NotificationManager()
 56 |         
 57 |         self.assertIsNone(manager._notification_system)
 58 |     
 59 |     @patch('src.notification.manager.get_platform_name')
 60 |     @patch('src.notification.manager.is_windows')
 61 |     @patch('src.notification.manager.is_macos')
 62 |     def test_show_notification_windows(self, mock_is_macos, mock_is_windows, mock_get_platform):
 63 |         """Test showing a notification on Windows."""
 64 |         mock_get_platform.return_value = "windows"
 65 |         mock_is_windows.return_value = True
 66 |         mock_is_macos.return_value = False
 67 |         
 68 |         mock_system = MagicMock()
 69 |         mock_system.show_notification.return_value = True
 70 |         
 71 |         with patch('src.notification.manager.ToastNotificationManager', return_value=mock_system):
 72 |             manager = NotificationManager()
 73 |             
 74 |             result = manager.show_notification(
 75 |                 title="Test Title",
 76 |                 message="Test Message",
 77 |                 notification_type=NotificationType.INFO,
 78 |                 duration=5,
 79 |                 icon_path="test.ico"
 80 |             )
 81 |             
 82 |             self.assertTrue(result)
 83 |             mock_system.show_notification.assert_called_once_with(
 84 |                 title="Test Title",
 85 |                 message="Test Message",
 86 |                 notification_type=NotificationType.INFO,
 87 |                 duration=5,
 88 |                 icon_path="test.ico"
 89 |             )
 90 |     
 91 |     @patch('src.notification.manager.get_platform_name')
 92 |     @patch('src.notification.manager.is_windows')
 93 |     @patch('src.notification.manager.is_macos')
 94 |     def test_show_notification_macos(self, mock_is_macos, mock_is_windows, mock_get_platform):
 95 |         """Test showing a notification on macOS."""
 96 |         mock_get_platform.return_value = "macos"
 97 |         mock_is_windows.return_value = False
 98 |         mock_is_macos.return_value = True
 99 |         
100 |         mock_system = MagicMock()
101 |         mock_system.show_notification.return_value = True
102 |         
103 |         with patch('src.notification.manager.MacOSNotificationManager', return_value=mock_system):
104 |             manager = NotificationManager()
105 |             
106 |             result = manager.show_notification(
107 |                 title="Test Title",
108 |                 message="Test Message",
109 |                 notification_type=NotificationType.INFO,
110 |                 duration=5,
111 |                 subtitle="Test Subtitle",
112 |                 sound=True
113 |             )
114 |             
115 |             self.assertTrue(result)
116 |             mock_system.show_notification.assert_called_once_with(
117 |                 title="Test Title",
118 |                 message="Test Message",
119 |                 notification_type=NotificationType.INFO,
120 |                 duration=5,
121 |                 subtitle="Test Subtitle",
122 |                 sound=True
123 |             )
124 |     
125 |     @patch('src.notification.manager.get_platform_name')
126 |     @patch('src.notification.manager.is_windows')
127 |     @patch('src.notification.manager.is_macos')
128 |     def test_show_notification_unsupported(self, mock_is_macos, mock_is_windows, mock_get_platform):
129 |         """Test showing a notification on an unsupported platform."""
130 |         mock_get_platform.return_value = "linux"
131 |         mock_is_windows.return_value = False
132 |         mock_is_macos.return_value = False
133 |         
134 |         manager = NotificationManager()
135 |         
136 |         result = manager.show_notification(
137 |             title="Test Title",
138 |             message="Test Message",
139 |             notification_type=NotificationType.INFO
140 |         )
141 |         
142 |         self.assertFalse(result)
143 |     
144 |     @patch('src.notification.manager.get_platform_name')
145 |     @patch('src.notification.manager.is_windows')
146 |     @patch('src.notification.manager.is_macos')
147 |     def test_show_notification_exception(self, mock_is_macos, mock_is_windows, mock_get_platform):
148 |         """Test showing a notification that raises an exception."""
149 |         mock_get_platform.return_value = "windows"
150 |         mock_is_windows.return_value = True
151 |         mock_is_macos.return_value = False
152 |         
153 |         mock_system = MagicMock()
154 |         mock_system.show_notification.side_effect = Exception("Test exception")
155 |         
156 |         with patch('src.notification.manager.ToastNotificationManager', return_value=mock_system):
157 |             manager = NotificationManager()
158 |             
159 |             result = manager.show_notification(
160 |                 title="Test Title",
161 |                 message="Test Message",
162 |                 notification_type=NotificationType.INFO
163 |             )
164 |             
165 |             self.assertFalse(result)
166 | 
167 | 
168 | class TestNotificationFactory(unittest.TestCase):
169 |     """Test cases for the NotificationFactory class."""
170 |     
171 |     @patch('src.notification.manager.NotificationManager')
172 |     def test_notification_factory(self, mock_manager_class):
173 |         """Test the notification factory."""
174 |         mock_manager = MagicMock()
175 |         mock_manager_class.return_value = mock_manager
176 |         mock_manager.show_notification.return_value = True
177 |         
178 |         factory = NotificationFactory()
179 |         
180 |         result = factory.create_info_notification("Info Title", "Info Message", param="value")
181 |         self.assertTrue(result)
182 |         mock_manager.show_notification.assert_called_with(
183 |             title="Info Title",
184 |             message="Info Message",
185 |             notification_type=NotificationType.INFO,
186 |             param="value"
187 |         )
188 |         
189 |         result = factory.create_warning_notification("Warning Title", "Warning Message")
190 |         self.assertTrue(result)
191 |         mock_manager.show_notification.assert_called_with(
192 |             title="Warning Title",
193 |             message="Warning Message",
194 |             notification_type=NotificationType.WARNING
195 |         )
196 |         
197 |         result = factory.create_error_notification("Error Title", "Error Message")
198 |         self.assertTrue(result)
199 |         mock_manager.show_notification.assert_called_with(
200 |             title="Error Title",
201 |             message="Error Message",
202 |             notification_type=NotificationType.ERROR
203 |         )
204 |         
205 |         result = factory.create_success_notification("Success Title", "Success Message")
206 |         self.assertTrue(result)
207 |         mock_manager.show_notification.assert_called_with(
208 |             title="Success Title",
209 |             message="Success Message",
210 |             notification_type=NotificationType.SUCCESS
211 |         )
212 | 
213 | 
214 | @patch('src.notification.manager.notification_factory')
215 | class TestShowNotification(unittest.TestCase):
216 |     """Test cases for the show_notification helper function."""
217 |     
218 |     def test_show_notification(self, mock_factory):
219 |         """Test the show_notification helper function."""
220 |         mock_factory.create_info_notification.return_value = True
221 |         mock_factory.create_warning_notification.return_value = True
222 |         mock_factory.create_error_notification.return_value = True
223 |         mock_factory.create_success_notification.return_value = True
224 |         
225 |         result = show_notification("Info Title", "Info Message", "info", param="value")
226 |         self.assertTrue(result)
227 |         mock_factory.create_info_notification.assert_called_with("Info Title", "Info Message", param="value")
228 |         
229 |         result = show_notification("Warning Title", "Warning Message", "warning")
230 |         self.assertTrue(result)
231 |         mock_factory.create_warning_notification.assert_called_with("Warning Title", "Warning Message")
232 |         
233 |         result = show_notification("Error Title", "Error Message", "error")
234 |         self.assertTrue(result)
235 |         mock_factory.create_error_notification.assert_called_with("Error Title", "Error Message")
236 |         
237 |         result = show_notification("Success Title", "Success Message", "success")
238 |         self.assertTrue(result)
239 |         mock_factory.create_success_notification.assert_called_with("Success Title", "Success Message")
240 |         
241 |         result = show_notification("Invalid Title", "Invalid Message", "invalid")
242 |         self.assertTrue(result)
243 |         mock_factory.create_info_notification.assert_called_with("Invalid Title", "Invalid Message")
244 | 
245 | 
246 | if __name__ == "__main__":
247 |     unittest.main()
248 | 
```

--------------------------------------------------------------------------------
/src/server/connection.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Client connection management for toast-mcp-server.
  3 | 
  4 | This module handles client connections, including connection establishment,
  5 | message handling, and connection termination.
  6 | """
  7 | 
  8 | import asyncio
  9 | import logging
 10 | import json
 11 | from typing import Dict, Set, Any, Optional, Callable, Awaitable, List
 12 | 
 13 | from src.mcp.protocol import (
 14 |     MCPMessage, NotificationMessage, ResponseMessage, ErrorMessage,
 15 |     PingMessage, PongMessage, MessageType, parse_message
 16 | )
 17 | from src.mcp.validation import validate_message_format
 18 | from src.notification.toast import show_notification
 19 | 
 20 | logger = logging.getLogger(__name__)
 21 | 
 22 | 
 23 | class ClientConnection:
 24 |     """
 25 |     Represents a connection to a client.
 26 |     
 27 |     This class handles the communication with a single client, including
 28 |     receiving and sending messages.
 29 |     """
 30 |     
 31 |     def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, 
 32 |                  client_id: str, server: 'ConnectionManager'):
 33 |         """
 34 |         Initialize a new client connection.
 35 |         
 36 |         Args:
 37 |             reader: Stream reader for receiving data from the client
 38 |             writer: Stream writer for sending data to the client
 39 |             client_id: Unique identifier for the client
 40 |             server: Reference to the connection manager
 41 |         """
 42 |         self.reader = reader
 43 |         self.writer = writer
 44 |         self.client_id = client_id
 45 |         self.server = server
 46 |         self.connected = True
 47 |         self.addr = writer.get_extra_info('peername')
 48 |         logger.info(f"Client connected: {self.client_id} from {self.addr}")
 49 |     
 50 |     async def handle(self) -> None:
 51 |         """
 52 |         Handle the client connection.
 53 |         
 54 |         This method continuously reads messages from the client and processes them
 55 |         until the connection is closed.
 56 |         """
 57 |         try:
 58 |             while self.connected:
 59 |                 data = await self.reader.readline()
 60 |                 if not data:
 61 |                     break
 62 |                 
 63 |                 await self._process_message(data.decode().strip())
 64 |                 
 65 |         except asyncio.CancelledError:
 66 |             logger.info(f"Connection handling cancelled for client: {self.client_id}")
 67 |         except Exception as e:
 68 |             logger.error(f"Error handling client {self.client_id}: {str(e)}")
 69 |         finally:
 70 |             await self.close()
 71 |     
 72 |     async def _process_message(self, data: str) -> None:
 73 |         """
 74 |         Process a message received from the client.
 75 |         
 76 |         Args:
 77 |             data: JSON string containing the message data
 78 |         """
 79 |         logger.debug(f"Received message from {self.client_id}: {data}")
 80 |         
 81 |         try:
 82 |             message_data = json.loads(data)
 83 |             is_valid, error, msg_type = validate_message_format(message_data)
 84 |             
 85 |             if not is_valid:
 86 |                 await self.send_error(400, f"Invalid message format: {error}")
 87 |                 return
 88 |             
 89 |             if msg_type == MessageType.NOTIFICATION:
 90 |                 await self._handle_notification(message_data)
 91 |             elif msg_type == MessageType.PING:
 92 |                 await self._handle_ping()
 93 |             else:
 94 |                 await self.send_error(400, f"Unsupported message type: {msg_type.value}")
 95 |                 
 96 |         except json.JSONDecodeError:
 97 |             await self.send_error(400, "Invalid JSON format")
 98 |         except Exception as e:
 99 |             logger.error(f"Error processing message from {self.client_id}: {str(e)}")
100 |             await self.send_error(500, "Internal server error")
101 |     
102 |     async def _handle_notification(self, message_data: Dict[str, Any]) -> None:
103 |         """
104 |         Handle a notification message.
105 |         
106 |         Args:
107 |             message_data: Dictionary containing the notification message data
108 |         """
109 |         try:
110 |             notification = NotificationMessage.from_dict(message_data)
111 |             
112 |             title = notification.data["title"]
113 |             message = notification.data["message"]
114 |             notification_type = notification.data.get("notification_type", "info")
115 |             duration = notification.data.get("duration", 5)
116 |             
117 |             success = show_notification(title, message, notification_type, duration)
118 |             
119 |             if success:
120 |                 await self.send_response(True, "Notification displayed successfully")
121 |             else:
122 |                 await self.send_response(False, "Failed to display notification")
123 |                 
124 |         except Exception as e:
125 |             logger.error(f"Error handling notification from {self.client_id}: {str(e)}")
126 |             await self.send_error(500, f"Error handling notification: {str(e)}")
127 |     
128 |     async def _handle_ping(self) -> None:
129 |         """Handle a ping message by sending a pong response."""
130 |         try:
131 |             pong = PongMessage()
132 |             await self.send_message(pong)
133 |             
134 |         except Exception as e:
135 |             logger.error(f"Error handling ping from {self.client_id}: {str(e)}")
136 |             await self.send_error(500, f"Error handling ping: {str(e)}")
137 |     
138 |     async def send_message(self, message: MCPMessage) -> None:
139 |         """
140 |         Send a message to the client.
141 |         
142 |         Args:
143 |             message: The message to send
144 |         """
145 |         try:
146 |             json_str = message.to_json() + "\n"
147 |             self.writer.write(json_str.encode())
148 |             await self.writer.drain()
149 |             
150 |             logger.debug(f"Sent message to {self.client_id}: {message.msg_type.value}")
151 |             
152 |         except Exception as e:
153 |             logger.error(f"Error sending message to {self.client_id}: {str(e)}")
154 |             await self.close()
155 |     
156 |     async def send_response(self, success: bool, message: str = None, data: Dict[str, Any] = None) -> None:
157 |         """
158 |         Send a response message to the client.
159 |         
160 |         Args:
161 |             success: Whether the operation was successful
162 |             message: Optional message describing the result
163 |             data: Optional additional data to include in the response
164 |         """
165 |         response = ResponseMessage(success, message, data)
166 |         await self.send_message(response)
167 |     
168 |     async def send_error(self, error_code: int, error_message: str, details: Any = None) -> None:
169 |         """
170 |         Send an error message to the client.
171 |         
172 |         Args:
173 |             error_code: Numeric error code
174 |             error_message: Human-readable error message
175 |             details: Optional additional error details
176 |         """
177 |         error = ErrorMessage(error_code, error_message, details)
178 |         await self.send_message(error)
179 |     
180 |     async def close(self) -> None:
181 |         """Close the client connection."""
182 |         if not self.connected:
183 |             return
184 |         
185 |         self.connected = False
186 |         
187 |         try:
188 |             self.writer.close()
189 |             await self.writer.wait_closed()
190 |             
191 |             self.server.remove_client(self.client_id)
192 |             
193 |             logger.info(f"Client disconnected: {self.client_id}")
194 |             
195 |         except Exception as e:
196 |             logger.error(f"Error closing connection for {self.client_id}: {str(e)}")
197 | 
198 | 
199 | class ConnectionManager:
200 |     """
201 |     Manages client connections to the server.
202 |     
203 |     This class handles the creation and management of client connections,
204 |     including accepting new connections and broadcasting messages to clients.
205 |     """
206 |     
207 |     def __init__(self, host: str = "127.0.0.1", port: int = 8765):
208 |         """
209 |         Initialize the connection manager.
210 |         
211 |         Args:
212 |             host: Host address to bind to
213 |             port: Port to listen on
214 |         """
215 |         self.host = host
216 |         self.port = port
217 |         self.clients: Dict[str, ClientConnection] = {}
218 |         self.server = None
219 |         self.next_client_id = 1
220 |         logger.info(f"Connection manager initialized with host={host}, port={port}")
221 |     
222 |     async def start(self) -> None:
223 |         """Start the server and begin accepting connections."""
224 |         try:
225 |             self.server = await asyncio.start_server(
226 |                 self._handle_new_connection, self.host, self.port
227 |             )
228 |             
229 |             addr = self.server.sockets[0].getsockname()
230 |             logger.info(f"Server started on {addr}")
231 |             
232 |             async with self.server:
233 |                 await self.server.serve_forever()
234 |                 
235 |         except Exception as e:
236 |             logger.error(f"Error starting server: {str(e)}")
237 |             raise
238 |     
239 |     async def _handle_new_connection(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
240 |         """
241 |         Handle a new client connection.
242 |         
243 |         Args:
244 |             reader: Stream reader for receiving data from the client
245 |             writer: Stream writer for sending data to the client
246 |         """
247 |         client_id = f"client_{self.next_client_id}"
248 |         self.next_client_id += 1
249 |         
250 |         client = ClientConnection(reader, writer, client_id, self)
251 |         self.clients[client_id] = client
252 |         
253 |         asyncio.create_task(client.handle())
254 |     
255 |     def remove_client(self, client_id: str) -> None:
256 |         """
257 |         Remove a client from the connection manager.
258 |         
259 |         Args:
260 |             client_id: ID of the client to remove
261 |         """
262 |         if client_id in self.clients:
263 |             del self.clients[client_id]
264 |             logger.debug(f"Removed client: {client_id}")
265 |     
266 |     async def broadcast(self, message: MCPMessage, exclude: Optional[str] = None) -> None:
267 |         """
268 |         Broadcast a message to all connected clients.
269 |         
270 |         Args:
271 |             message: The message to broadcast
272 |             exclude: Optional client ID to exclude from the broadcast
273 |         """
274 |         for client_id, client in list(self.clients.items()):
275 |             if exclude and client_id == exclude:
276 |                 continue
277 |             
278 |             try:
279 |                 await client.send_message(message)
280 |             except Exception as e:
281 |                 logger.error(f"Error broadcasting to {client_id}: {str(e)}")
282 |     
283 |     async def stop(self) -> None:
284 |         """Stop the server and close all client connections."""
285 |         logger.info("Stopping server...")
286 |         
287 |         for client_id, client in list(self.clients.items()):
288 |             try:
289 |                 await client.close()
290 |             except Exception as e:
291 |                 logger.error(f"Error closing client {client_id}: {str(e)}")
292 |         
293 |         if self.server:
294 |             self.server.close()
295 |             await self.server.wait_closed()
296 |             logger.info("Server stopped")
297 | 
298 | 
299 | async def run_server(host: str = "127.0.0.1", port: int = 8765) -> None:
300 |     """
301 |     Run the MCP server.
302 |     
303 |     Args:
304 |         host: Host address to bind to
305 |         port: Port to listen on
306 |     """
307 |     logging.basicConfig(
308 |         level=logging.INFO,
309 |         format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
310 |     )
311 |     
312 |     manager = ConnectionManager(host, port)
313 |     await manager.start()
314 | 
```

--------------------------------------------------------------------------------
/tests/test_commands.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the command processing system.
  3 | """
  4 | 
  5 | import unittest
  6 | import asyncio
  7 | from unittest.mock import patch, MagicMock, AsyncMock
  8 | from src.server.commands import (
  9 |     Command, CommandProcessor, process_command_message,
 10 |     handle_show_notification, handle_list_commands,
 11 |     validate_show_notification, DEFAULT_COMMANDS
 12 | )
 13 | from src.mcp.protocol import (
 14 |     ResponseMessage, ErrorMessage
 15 | )
 16 | 
 17 | 
 18 | class TestCommand(unittest.TestCase):
 19 |     """Test cases for the Command class."""
 20 |     
 21 |     async def test_execute_command(self):
 22 |         """Test executing a command."""
 23 |         mock_handler = AsyncMock()
 24 |         mock_handler.return_value = (True, "Success", {"data": "value"})
 25 |         
 26 |         command = Command(
 27 |             name="test_command",
 28 |             handler=mock_handler,
 29 |             description="Test command"
 30 |         )
 31 |         
 32 |         success, message, data = await command.execute({}, "test_client")
 33 |         
 34 |         mock_handler.assert_called_once_with({}, "test_client")
 35 |         
 36 |         self.assertTrue(success)
 37 |         self.assertEqual(message, "Success")
 38 |         self.assertEqual(data, {"data": "value"})
 39 |     
 40 |     async def test_execute_command_with_validator(self):
 41 |         """Test executing a command with a validator."""
 42 |         mock_handler = AsyncMock()
 43 |         mock_handler.return_value = (True, "Success", {"data": "value"})
 44 |         
 45 |         mock_validator = MagicMock()
 46 |         mock_validator.return_value = (True, None)
 47 |         
 48 |         command = Command(
 49 |             name="test_command",
 50 |             handler=mock_handler,
 51 |             validator=mock_validator,
 52 |             description="Test command"
 53 |         )
 54 |         
 55 |         success, message, data = await command.execute({"param": "value"}, "test_client")
 56 |         
 57 |         mock_validator.assert_called_once_with({"param": "value"})
 58 |         
 59 |         mock_handler.assert_called_once_with({"param": "value"}, "test_client")
 60 |         
 61 |         self.assertTrue(success)
 62 |         self.assertEqual(message, "Success")
 63 |         self.assertEqual(data, {"data": "value"})
 64 |     
 65 |     async def test_execute_command_with_invalid_params(self):
 66 |         """Test executing a command with invalid parameters."""
 67 |         mock_handler = AsyncMock()
 68 |         
 69 |         mock_validator = MagicMock()
 70 |         mock_validator.return_value = (False, "Invalid parameter")
 71 |         
 72 |         command = Command(
 73 |             name="test_command",
 74 |             handler=mock_handler,
 75 |             validator=mock_validator,
 76 |             description="Test command"
 77 |         )
 78 |         
 79 |         success, message, data = await command.execute({}, "test_client")
 80 |         
 81 |         mock_validator.assert_called_once_with({})
 82 |         
 83 |         mock_handler.assert_not_called()
 84 |         
 85 |         self.assertFalse(success)
 86 |         self.assertEqual(message, "Invalid parameters: Invalid parameter")
 87 |         self.assertIsNone(data)
 88 |     
 89 |     async def test_execute_command_with_exception(self):
 90 |         """Test executing a command that raises an exception."""
 91 |         mock_handler = AsyncMock()
 92 |         mock_handler.side_effect = Exception("Test exception")
 93 |         
 94 |         command = Command(
 95 |             name="test_command",
 96 |             handler=mock_handler,
 97 |             description="Test command"
 98 |         )
 99 |         
100 |         success, message, data = await command.execute({}, "test_client")
101 |         
102 |         mock_handler.assert_called_once_with({}, "test_client")
103 |         
104 |         self.assertFalse(success)
105 |         self.assertEqual(message, "Error executing command: Test exception")
106 |         self.assertIsNone(data)
107 | 
108 | 
109 | class TestCommandProcessor(unittest.TestCase):
110 |     """Test cases for the CommandProcessor class."""
111 |     
112 |     def setUp(self):
113 |         """Set up test fixtures."""
114 |         self.processor = CommandProcessor()
115 |     
116 |     def test_register_command(self):
117 |         """Test registering a command."""
118 |         command = Command(
119 |             name="test_command",
120 |             handler=AsyncMock(),
121 |             description="Test command"
122 |         )
123 |         
124 |         self.processor.register_command(command)
125 |         
126 |         self.assertIn("test_command", self.processor.commands)
127 |         self.assertEqual(self.processor.commands["test_command"], command)
128 |     
129 |     def test_register_commands(self):
130 |         """Test registering multiple commands."""
131 |         command1 = Command(
132 |             name="command1",
133 |             handler=AsyncMock(),
134 |             description="Command 1"
135 |         )
136 |         
137 |         command2 = Command(
138 |             name="command2",
139 |             handler=AsyncMock(),
140 |             description="Command 2"
141 |         )
142 |         
143 |         self.processor.register_commands([command1, command2])
144 |         
145 |         self.assertIn("command1", self.processor.commands)
146 |         self.assertIn("command2", self.processor.commands)
147 |         self.assertEqual(self.processor.commands["command1"], command1)
148 |         self.assertEqual(self.processor.commands["command2"], command2)
149 |     
150 |     async def test_process_command(self):
151 |         """Test processing a command."""
152 |         mock_handler = AsyncMock()
153 |         mock_handler.return_value = (True, "Success", {"data": "value"})
154 |         
155 |         command = Command(
156 |             name="test_command",
157 |             handler=mock_handler,
158 |             description="Test command"
159 |         )
160 |         
161 |         self.processor.register_command(command)
162 |         
163 |         success, message, data = await self.processor.process_command(
164 |             "test_command", {"param": "value"}, "test_client"
165 |         )
166 |         
167 |         mock_handler.assert_called_once_with({"param": "value"}, "test_client")
168 |         
169 |         self.assertTrue(success)
170 |         self.assertEqual(message, "Success")
171 |         self.assertEqual(data, {"data": "value"})
172 |     
173 |     async def test_process_unknown_command(self):
174 |         """Test processing an unknown command."""
175 |         success, message, data = await self.processor.process_command(
176 |             "unknown_command", {}, "test_client"
177 |         )
178 |         
179 |         self.assertFalse(success)
180 |         self.assertEqual(message, "Unknown command: unknown_command")
181 |         self.assertIsNone(data)
182 |     
183 |     async def test_process_command_requiring_auth(self):
184 |         """Test processing a command that requires authentication."""
185 |         mock_handler = AsyncMock()
186 |         
187 |         command = Command(
188 |             name="auth_command",
189 |             handler=mock_handler,
190 |             description="Auth command",
191 |             requires_auth=True
192 |         )
193 |         
194 |         self.processor.register_command(command)
195 |         
196 |         success, message, data = await self.processor.process_command(
197 |             "auth_command", {}, "test_client"
198 |         )
199 |         
200 |         mock_handler.assert_not_called()
201 |         
202 |         self.assertFalse(success)
203 |         self.assertEqual(message, "Authentication required")
204 |         self.assertIsNone(data)
205 |         
206 |         self.processor.authenticate_client("test_client")
207 |         
208 |         success, message, data = await self.processor.process_command(
209 |             "auth_command", {}, "test_client"
210 |         )
211 |         
212 |         mock_handler.assert_called_once_with({}, "test_client")
213 |     
214 |     def test_authenticate_deauthenticate_client(self):
215 |         """Test authenticating and deauthenticating a client."""
216 |         self.processor.authenticate_client("test_client")
217 |         
218 |         self.assertIn("test_client", self.processor.authenticated_clients)
219 |         
220 |         self.processor.authenticate_client("test_client")
221 |         
222 |         self.assertEqual(self.processor.authenticated_clients.count("test_client"), 1)
223 |         
224 |         self.processor.deauthenticate_client("test_client")
225 |         
226 |         self.assertNotIn("test_client", self.processor.authenticated_clients)
227 |         
228 |         self.processor.deauthenticate_client("non_existent_client")
229 | 
230 | 
231 | class TestCommandHandlers(unittest.TestCase):
232 |     """Test cases for the command handlers."""
233 |     
234 |     @patch('src.server.commands.show_notification')
235 |     async def test_handle_show_notification(self, mock_show_notification):
236 |         """Test the show_notification command handler."""
237 |         mock_show_notification.return_value = True
238 |         
239 |         success, message, data = await handle_show_notification(
240 |             {
241 |                 "title": "Test Title",
242 |                 "message": "Test Message",
243 |                 "type": "info",
244 |                 "duration": 5
245 |             },
246 |             "test_client"
247 |         )
248 |         
249 |         mock_show_notification.assert_called_once_with(
250 |             "Test Title", "Test Message", "info", 5
251 |         )
252 |         
253 |         self.assertTrue(success)
254 |         self.assertEqual(message, "Notification displayed successfully")
255 |         self.assertIsNone(data)
256 |         
257 |         mock_show_notification.reset_mock()
258 |         mock_show_notification.return_value = False
259 |         
260 |         success, message, data = await handle_show_notification(
261 |             {
262 |                 "title": "Test Title",
263 |                 "message": "Test Message"
264 |             },
265 |             "test_client"
266 |         )
267 |         
268 |         self.assertFalse(success)
269 |         self.assertEqual(message, "Failed to display notification")
270 |         self.assertIsNone(data)
271 |     
272 |     async def test_handle_list_commands(self):
273 |         """Test the list_commands command handler."""
274 |         success, message, data = await handle_list_commands({}, "test_client")
275 |         
276 |         self.assertTrue(success)
277 |         self.assertEqual(message, "Commands retrieved successfully")
278 |         self.assertIsInstance(data, dict)
279 |         self.assertIn("commands", data)
280 |         self.assertIsInstance(data["commands"], list)
281 |         self.assertTrue(len(data["commands"]) > 0)
282 |     
283 |     def test_validate_show_notification(self):
284 |         """Test the show_notification validator."""
285 |         is_valid, error = validate_show_notification({
286 |             "title": "Test Title",
287 |             "message": "Test Message",
288 |             "type": "info",
289 |             "duration": 5
290 |         })
291 |         
292 |         self.assertTrue(is_valid)
293 |         self.assertIsNone(error)
294 |         
295 |         is_valid, error = validate_show_notification({
296 |             "message": "Test Message"
297 |         })
298 |         
299 |         self.assertFalse(is_valid)
300 |         self.assertEqual(error, "Missing required parameter: title")
301 |         
302 |         is_valid, error = validate_show_notification({
303 |             "title": "Test Title"
304 |         })
305 |         
306 |         self.assertFalse(is_valid)
307 |         self.assertEqual(error, "Missing required parameter: message")
308 |         
309 |         is_valid, error = validate_show_notification({
310 |             "title": "Test Title",
311 |             "message": "Test Message",
312 |             "type": "invalid"
313 |         })
314 |         
315 |         self.assertFalse(is_valid)
316 |         self.assertEqual(error, "Invalid notification type")
317 |         
318 |         is_valid, error = validate_show_notification({
319 |             "title": "Test Title",
320 |             "message": "Test Message",
321 |             "duration": "5"  # Should be an integer
322 |         })
323 |         
324 |         self.assertFalse(is_valid)
325 |         self.assertEqual(error, "Duration must be an integer")
326 | 
327 | 
328 | @patch('src.server.commands.command_processor')
329 | class TestProcessCommandMessage(unittest.TestCase):
330 |     """Test cases for the process_command_message function."""
331 |     
332 |     async def test_process_command_message(self, mock_processor):
333 |         """Test processing a command message."""
334 |         mock_processor.process_command = AsyncMock()
335 |         mock_processor.process_command.return_value = (True, "Success", {"data": "value"})
336 |         
337 |         message = await process_command_message(
338 |             {
339 |                 "command": "test_command",
340 |                 "params": {"param": "value"}
341 |             },
342 |             "test_client"
343 |         )
344 |         
345 |         mock_processor.process_command.assert_called_once_with(
346 |             "test_command", {"param": "value"}, "test_client"
347 |         )
348 |         
349 |         self.assertIsInstance(message, ResponseMessage)
350 |         self.assertTrue(message.data["success"])
351 |         self.assertEqual(message.data["message"], "Success")
352 |         self.assertEqual(message.data["data"], {"data": "value"})
353 |     
354 |     async def test_process_command_message_failure(self, mock_processor):
355 |         """Test processing a command message that fails."""
356 |         mock_processor.process_command = AsyncMock()
357 |         mock_processor.process_command.return_value = (False, "Failure", None)
358 |         
359 |         message = await process_command_message(
360 |             {
361 |                 "command": "test_command",
362 |                 "params": {"param": "value"}
363 |             },
364 |             "test_client"
365 |         )
366 |         
367 |         self.assertIsInstance(message, ErrorMessage)
368 |         self.assertEqual(message.data["code"], 400)
369 |         self.assertEqual(message.data["message"], "Failure")
370 |     
371 |     async def test_process_command_message_missing_command(self, mock_processor):
372 |         """Test processing a command message with a missing command name."""
373 |         message = await process_command_message(
374 |             {
375 |                 "params": {"param": "value"}
376 |             },
377 |             "test_client"
378 |         )
379 |         
380 |         mock_processor.process_command.assert_not_called()
381 |         
382 |         self.assertIsInstance(message, ErrorMessage)
383 |         self.assertEqual(message.data["code"], 400)
384 |         self.assertEqual(message.data["message"], "Missing command name")
385 | 
386 | 
387 | class TestDefaultCommands(unittest.TestCase):
388 |     """Test cases for the default commands."""
389 |     
390 |     def test_default_commands(self):
391 |         """Test that default commands are defined."""
392 |         self.assertIsInstance(DEFAULT_COMMANDS, list)
393 |         
394 |         self.assertTrue(len(DEFAULT_COMMANDS) > 0)
395 |         
396 |         for command in DEFAULT_COMMANDS:
397 |             self.assertIsInstance(command, Command)
398 | 
399 | 
400 | if __name__ == "__main__":
401 |     unittest.main()
402 | 
```