# Directory Structure ``` ├── .github │ └── workflows │ └── release.yml ├── .gitignore ├── LICENSE ├── pyproject.toml ├── README.md ├── src │ └── mcp_sonic_pi │ ├── __init__.py │ └── server.py └── uv.lock ``` # 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 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # mcp-sonic-pi: MCP server for Sonic Pi 2 | 3 | **mcp-sonic-pi** connects any MCP client with [Sonic Pi](https://sonic-pi.net/) enabling you to create music with English. 4 | 5 | [📺 Demo](https://x.com/vortex_ape/status/1903470754999463969) 6 | 7 | ## Requirements 8 | 9 | - Python 3.10+ 10 | - Sonic Pi installed and running 11 | 12 | ## Quickstart 13 | 14 | Start using `mcp-sonic-pi` with an MCP client by running: 15 | 16 | ```bash 17 | uvx mcp-sonic-pi 18 | ``` 19 | 20 | To start using this MCP server with Claude, add the following entry to your `claude_desktop_config.json`: 21 | 22 | ``` 23 | { 24 | "mcpServers": { 25 | "sonic-pi": { 26 | "args": [ 27 | "mcp-sonic-pi" 28 | ], 29 | "command": "/path/to/uvx" 30 | } 31 | } 32 | } 33 | ``` 34 | 35 | **Note**: Ensure Sonic Pi is running before starting the MCP server. 36 | 37 | ## Contributing 38 | 39 | Contributions are welcome! Please feel free to submit a pull request. 40 | 41 | ## License 42 | 43 | This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details. 44 | ``` -------------------------------------------------------------------------------- /src/mcp_sonic_pi/__init__.py: -------------------------------------------------------------------------------- ```python 1 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "mcp-sonic-pi" 3 | version = "0.1.0" 4 | description = "MCP server for Sonic Pi" 5 | requires-python = ">=3.10" 6 | authors = [ 7 | {name = "Vinayak Mehta", email = "[email protected]"}, 8 | ] 9 | dependencies = [ 10 | "mcp>=0.1.0", 11 | "python-dotenv>=1.0.0", 12 | "fastapi>=0.109.0", 13 | "uvicorn>=0.27.0", 14 | "python-osc>=1.9.3", 15 | "python-sonic>=0.4.4", 16 | "ruff>=0.11.2", 17 | ] 18 | readme = "README.md" 19 | license = {text = "Apache-2.0"} 20 | classifiers = [ 21 | "Programming Language :: Python :: 3", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "Programming Language :: Python :: 3.12", 25 | "Programming Language :: Python :: 3.13", 26 | "License :: OSI Approved :: Apache Software License", 27 | ] 28 | 29 | [project.urls] 30 | "Homepage" = "https://github.com/vinayak-mehta/mcp-sonic-pi" 31 | "Bug Tracker" = "https://github.com/vinayak-mehta/mcp-sonic-pi/issues" 32 | 33 | [project.scripts] 34 | mcp-sonic-pi = "mcp_sonic_pi.server:main" 35 | 36 | [build-system] 37 | requires = ["setuptools>=42", "wheel"] 38 | build-backend = "setuptools.build_meta" 39 | 40 | [tool.setuptools] 41 | package-dir = {"" = "src"} 42 | packages = ["mcp_sonic_pi"] 43 | ``` -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- ```yaml 1 | 2 | name: Publish Python 🐍 distribution 📦 to PyPI 3 | 4 | on: push 5 | 6 | jobs: 7 | build: 8 | name: Build distribution 📦 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | persist-credentials: false 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: "3.x" 19 | - name: Install pypa/build 20 | run: >- 21 | python3 -m 22 | pip install 23 | build 24 | --user 25 | - name: Build a binary wheel and a source tarball 26 | run: python3 -m build 27 | - name: Store the distribution packages 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: python-package-distributions 31 | path: dist/ 32 | 33 | publish-to-pypi: 34 | name: >- 35 | Publish Python 🐍 distribution 📦 to PyPI 36 | if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes 37 | needs: 38 | - build 39 | runs-on: ubuntu-latest 40 | environment: 41 | name: pypi 42 | url: https://pypi.org/p/mcp-sonic-pi 43 | permissions: 44 | id-token: write # IMPORTANT: mandatory for trusted publishing 45 | 46 | steps: 47 | - name: Download all the dists 48 | uses: actions/download-artifact@v4 49 | with: 50 | name: python-package-distributions 51 | path: dist/ 52 | - name: Publish distribution 📦 to PyPI 53 | uses: pypa/gh-action-pypi-publish@release/v1 54 | 55 | github-release: 56 | name: >- 57 | Sign the Python 🐍 distribution 📦 with Sigstore 58 | and upload them to GitHub Release 59 | needs: 60 | - publish-to-pypi 61 | runs-on: ubuntu-latest 62 | 63 | permissions: 64 | contents: write # IMPORTANT: mandatory for making GitHub Releases 65 | id-token: write # IMPORTANT: mandatory for sigstore 66 | 67 | steps: 68 | - name: Download all the dists 69 | uses: actions/download-artifact@v4 70 | with: 71 | name: python-package-distributions 72 | path: dist/ 73 | - name: Sign the dists with Sigstore 74 | uses: sigstore/[email protected] 75 | with: 76 | inputs: >- 77 | ./dist/*.tar.gz 78 | ./dist/*.whl 79 | - name: Create GitHub Release 80 | env: 81 | GITHUB_TOKEN: ${{ github.token }} 82 | run: >- 83 | gh release create 84 | "$GITHUB_REF_NAME" 85 | --repo "$GITHUB_REPOSITORY" 86 | --notes "" 87 | - name: Upload artifact signatures to GitHub Release 88 | env: 89 | GITHUB_TOKEN: ${{ github.token }} 90 | # Upload to GitHub Release using the `gh` CLI. 91 | # `dist/` contains the built packages, and the 92 | # sigstore-produced signatures and certificates. 93 | run: >- 94 | gh release upload 95 | "$GITHUB_REF_NAME" dist/** 96 | --repo "$GITHUB_REPOSITORY" 97 | ``` -------------------------------------------------------------------------------- /src/mcp_sonic_pi/server.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python3 2 | """ 3 | MCP Server for controlling Sonic Pi via psonic library 4 | """ 5 | 6 | import platform 7 | import subprocess 8 | 9 | from mcp.server.fastmcp import Context, FastMCP 10 | 11 | mcp = FastMCP("sonic-pi") 12 | 13 | 14 | def check_sonic_pi_running(): 15 | """Check if Sonic Pi is running on the system""" 16 | system = platform.system() 17 | 18 | if system == "Darwin": 19 | result = subprocess.run( 20 | ["pgrep", "-x", "Sonic Pi"], capture_output=True, text=True 21 | ) 22 | return result.returncode == 0 23 | 24 | return False 25 | 26 | 27 | try: 28 | from psonic import * 29 | 30 | PSONIC_AVAILABLE = True 31 | except Exception as e: 32 | print(f"Error initializing psonic: {e}") 33 | PSONIC_AVAILABLE = False 34 | 35 | 36 | @mcp.tool() 37 | async def initialize_sonic_pi() -> str: 38 | """Initialize the Sonic Pi server 39 | 40 | Returns: 41 | The system prompt for the server 42 | """ 43 | if not check_sonic_pi_running(): 44 | return "Error: Sonic Pi does not appear to be running. Please start Sonic Pi first." 45 | 46 | if not PSONIC_AVAILABLE: 47 | return ( 48 | "Error: The psonic library couldn't be initialized. Check Sonic Pi status." 49 | ) 50 | 51 | try: 52 | set_server_parameter_from_log("127.0.0.1") 53 | return system_prompt() 54 | except Exception as e: 55 | return f"Error initializing server: {str(e)}" 56 | 57 | 58 | @mcp.tool() 59 | async def play_music(code: str) -> str: 60 | """Play music using Sonic Pi code. 61 | 62 | Args: 63 | code: Sonic Pi Ruby code 64 | 65 | Returns: 66 | A confirmation message 67 | """ 68 | if not check_sonic_pi_running(): 69 | return "Error: Sonic Pi does not appear to be running. Please start Sonic Pi first." 70 | 71 | if not PSONIC_AVAILABLE: 72 | return ( 73 | "Error: The psonic library couldn't be initialized. Check Sonic Pi status." 74 | ) 75 | 76 | try: 77 | stop() 78 | run(code) 79 | send_message("/trigger/prophet", 70, 100, 8) 80 | return "Code is now running. If you don't hear anything, check Sonic Pi for errors." 81 | except Exception as e: 82 | return f"Error running code: {str(e)}" 83 | 84 | 85 | @mcp.tool() 86 | async def stop_music() -> str: 87 | """Stop all currently playing Sonic Pi music. 88 | 89 | Returns: 90 | A confirmation message 91 | """ 92 | if not check_sonic_pi_running(): 93 | return "Error: Sonic Pi does not appear to be running. Please start Sonic Pi first." 94 | 95 | if not PSONIC_AVAILABLE: 96 | return ( 97 | "Error: The psonic library couldn't be initialized. Check Sonic Pi status." 98 | ) 99 | 100 | try: 101 | stop() 102 | return "Music stopped" 103 | except Exception as e: 104 | return f"Error stopping music: {str(e)}" 105 | 106 | 107 | @mcp.tool() 108 | def get_beat_pattern(style: str) -> str: 109 | """Get drum beat patterns for Sonic Pi. 110 | 111 | Args: 112 | style: Beat style (blues, rock, jazz, hiphop, etc.) 113 | 114 | Returns: 115 | Sonic Pi code for the requested beat pattern 116 | """ 117 | beats = { 118 | "blues": """ 119 | # Blues Beat 120 | use_bpm 100 121 | swing = 0.15 # Shuffle feel (0 for straight timing) 122 | live_loop :blues_drums do 123 | sample :hat_tap, amp: 0.9 124 | sample :drum_bass_hard, amp: 0.9 125 | sleep 0.5+swing 126 | sample :hat_tap, amp: 0.7 127 | sample :drum_bass_hard, amp: 0.8 128 | sleep 0.5-swing 129 | sample :drum_snare_hard, amp: 0.8 130 | sample :hat_tap, amp: 0.8 131 | sleep 0.5+swing 132 | sample :hat_tap, amp: 0.7 133 | sleep 0.5-swing 134 | end 135 | """, 136 | "rock": """ 137 | # Rock Beat 138 | use_bpm 120 139 | live_loop :rock_drums do 140 | sample :drum_bass_hard, amp: 1 141 | sample :drum_cymbal_closed, amp: 0.7 142 | sleep 0.5 143 | sample :drum_cymbal_closed, amp: 0.7 144 | sleep 0.5 145 | sample :drum_snare_hard, amp: 0.9 146 | sample :drum_cymbal_closed, amp: 0.7 147 | sleep 0.5 148 | sample :drum_cymbal_closed, amp: 0.7 149 | sleep 0.5 150 | end 151 | """, 152 | "hiphop": """ 153 | # Hip-Hop Beat 154 | use_bpm 90 155 | live_loop :hip_hop_drums do 156 | sample :drum_bass_hard, amp: 1.2 157 | sleep 1 158 | sample :drum_snare_hard, amp: 0.9 159 | sleep 1 160 | sample :drum_bass_hard, amp: 1.2 161 | sleep 0.5 162 | sample :drum_bass_hard, amp: 0.8 163 | sleep 0.5 164 | sample :drum_snare_hard, amp: 0.9 165 | sleep 1 166 | end 167 | """, 168 | "electronic": """ 169 | # Electronic Beat 170 | use_bpm 128 171 | live_loop :electronic_beat do 172 | sample :bd_haus, amp: 1 173 | sample :drum_cymbal_closed, amp: 0.3 174 | sleep 0.5 175 | 176 | sample :drum_cymbal_closed, amp: 0.3 177 | sleep 0.5 178 | 179 | sample :bd_haus, amp: 0.9 180 | sample :drum_snare_hard, amp: 0.8 181 | sample :drum_cymbal_closed, amp: 0.3 182 | sleep 0.5 183 | 184 | sample :drum_cymbal_closed, amp: 0.3 185 | sleep 0.5 186 | end 187 | """, 188 | } 189 | 190 | if style.lower() in beats: 191 | return beats[style.lower()] 192 | else: 193 | return f"Beat style '{style}' not found. Available styles: {', '.join(beats.keys())}" 194 | 195 | 196 | @mcp.prompt() 197 | def system_prompt(): 198 | return """ 199 | You are a Sonic Pi assistant that helps users create musical compositions using code. Your knowledge includes various rhythm patterns, chord progressions, scales, and proper Sonic Pi syntax. Respond with accurate, executable Sonic Pi code based on user requests. Remember to call initialize_sonic_pi first before playing any music with Sonic Pi. 200 | 201 | When the user asks you to play a beat, you should use the get_beat_pattern tool to get the beat pattern, play the beat and add nothing else on top of it. 202 | 203 | When the user asks you to play a chord progression, construct one using the following chord format, and add it to the existing beat. 204 | 205 | Chords have the following format: chord tonic (symbol), name (symbol) 206 | 207 | Here's an example chord with C tonic and various names: 208 | (chord :C, '1') 209 | (chord :C, '5') 210 | (chord :C, '+5') 211 | (chord :C, 'm+5') 212 | (chord :C, :sus2) 213 | (chord :C, :sus4) 214 | (chord :C, '6') 215 | (chord :C, :m6) 216 | (chord :C, '7sus2') 217 | (chord :C, '7sus4') 218 | (chord :C, '7-5') 219 | (chord :C, 'm7-5') 220 | (chord :C, '7+5') 221 | (chord :C, 'm7+5') 222 | (chord :C, '9') 223 | (chord :C, :m9) 224 | (chord :C, 'm7+9') 225 | (chord :C, :maj9) 226 | (chord :C, '9sus4') 227 | (chord :C, '6*9') 228 | (chord :C, 'm6*9') 229 | (chord :C, '7-9') 230 | (chord :C, 'm7-9') 231 | (chord :C, '7-10') 232 | (chord :C, '9+5') 233 | (chord :C, 'm9+5') 234 | (chord :C, '7+5-9') 235 | (chord :C, 'm7+5-9') 236 | (chord :C, '11') 237 | (chord :C, :m11) 238 | (chord :C, :maj11) 239 | (chord :C, '11+') 240 | (chord :C, 'm11+') 241 | (chord :C, '13') 242 | (chord :C, :m13) 243 | (chord :C, :add2) 244 | (chord :C, :add4) 245 | (chord :C, :add9) 246 | (chord :C, :add11) 247 | (chord :C, :add13) 248 | (chord :C, :madd2) 249 | (chord :C, :madd4) 250 | (chord :C, :madd9) 251 | (chord :C, :madd11) 252 | (chord :C, :madd13) 253 | (chord :C, :major) 254 | (chord :C, :M) 255 | (chord :C, :minor) 256 | (chord :C, :m) 257 | (chord :C, :major7) 258 | (chord :C, :dom7) 259 | (chord :C, '7') 260 | (chord :C, :M7) 261 | (chord :C, :minor7) 262 | (chord :C, :m7) 263 | (chord :C, :augmented) 264 | (chord :C, :a) 265 | (chord :C, :diminished) 266 | (chord :C, :dim) 267 | (chord :C, :i) 268 | (chord :C, :diminished7) 269 | (chord :C, :dim7) 270 | (chord :C, :i7) 271 | 272 | Remember that all Sonic Pi code must be valid Ruby code, with proper indentation, parameter passing, and loop definitions. When composing patterns, always ensure the timing adds up correctly within each loop. 273 | """ 274 | 275 | 276 | def main(): 277 | if not check_sonic_pi_running(): 278 | print("⚠️ Warning: Sonic Pi doesn't appear to be running") 279 | print("Please start Sonic Pi before using this MCP server") 280 | else: 281 | print("✅ Sonic Pi is running") 282 | 283 | if not PSONIC_AVAILABLE: 284 | print("⚠️ Warning: psonic library is not properly initialized") 285 | print("Make sure Sonic Pi is running and log file is accessible") 286 | else: 287 | print("✅ psonic library initialized") 288 | 289 | print("Sonic Pi MCP Server initialized") 290 | 291 | mcp.run(transport="stdio") 292 | 293 | 294 | if __name__ == "__main__": 295 | main() 296 | ```