#
tokens: 6508/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | 
```