#
tokens: 4822/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .github
│   └── workflows
│       └── release.yml
├── .gitignore
├── LICENSE
├── pyproject.toml
├── README.md
├── src
│   └── mcp_sonic_pi
│       ├── __init__.py
│       └── server.py
└── uv.lock
```

# Files

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

```
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
#   For a library or package, you might want to ignore these files since the code is
#   intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# UV
#   Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#uv.lock

# poetry
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
#   This is especially recommended for binary packages to ensure reproducibility, and is more
#   commonly ignored for libraries.
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
#   in version control.
#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# Ruff stuff:
.ruff_cache/

# PyPI configuration file
.pypirc

```

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

```markdown
# mcp-sonic-pi: MCP server for Sonic Pi

**mcp-sonic-pi** connects any MCP client with [Sonic Pi](https://sonic-pi.net/) enabling you to create music with English.

[📺 Demo](https://x.com/vortex_ape/status/1903470754999463969)

## Requirements

- Python 3.10+
- Sonic Pi installed and running

## Quickstart

Start using `mcp-sonic-pi` with an MCP client by running:

```bash
uvx mcp-sonic-pi
```

To start using this MCP server with Claude, add the following entry to your `claude_desktop_config.json`:

```
{
  "mcpServers": {
    "sonic-pi": {
      "args": [
        "mcp-sonic-pi"
      ],
      "command": "/path/to/uvx"
    }
  }
}
```

**Note**: Ensure Sonic Pi is running before starting the MCP server.

## Contributing

Contributions are welcome! Please feel free to submit a pull request.

## License

This project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details.

```

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

```python

```

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

```toml
[project]
name = "mcp-sonic-pi"
version = "0.1.0"
description = "MCP server for Sonic Pi"
requires-python = ">=3.10"
authors = [
    {name = "Vinayak Mehta", email = "[email protected]"},
]
dependencies = [
    "mcp>=0.1.0",
    "python-dotenv>=1.0.0",
    "fastapi>=0.109.0",
    "uvicorn>=0.27.0",
    "python-osc>=1.9.3",
    "python-sonic>=0.4.4",
    "ruff>=0.11.2",
]
readme = "README.md"
license = {text = "Apache-2.0"}
classifiers = [
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "License :: OSI Approved :: Apache Software License",
]

[project.urls]
"Homepage" = "https://github.com/vinayak-mehta/mcp-sonic-pi"
"Bug Tracker" = "https://github.com/vinayak-mehta/mcp-sonic-pi/issues"

[project.scripts]
mcp-sonic-pi = "mcp_sonic_pi.server:main"

[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
package-dir = {"" = "src"}
packages = ["mcp_sonic_pi"]

```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml

name: Publish Python 🐍 distribution 📦 to PyPI

on: push

jobs:
  build:
    name: Build distribution 📦
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.x"
      - name: Install pypa/build
        run: >-
          python3 -m
          pip install
          build
          --user
      - name: Build a binary wheel and a source tarball
        run: python3 -m build
      - name: Store the distribution packages
        uses: actions/upload-artifact@v4
        with:
          name: python-package-distributions
          path: dist/

  publish-to-pypi:
    name: >-
      Publish Python 🐍 distribution 📦 to PyPI
    if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
    needs:
      - build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/mcp-sonic-pi
    permissions:
      id-token: write # IMPORTANT: mandatory for trusted publishing

    steps:
      - name: Download all the dists
        uses: actions/download-artifact@v4
        with:
          name: python-package-distributions
          path: dist/
      - name: Publish distribution 📦 to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1

  github-release:
    name: >-
      Sign the Python 🐍 distribution 📦 with Sigstore
      and upload them to GitHub Release
    needs:
      - publish-to-pypi
    runs-on: ubuntu-latest

    permissions:
      contents: write # IMPORTANT: mandatory for making GitHub Releases
      id-token: write # IMPORTANT: mandatory for sigstore

    steps:
      - name: Download all the dists
        uses: actions/download-artifact@v4
        with:
          name: python-package-distributions
          path: dist/
      - name: Sign the dists with Sigstore
        uses: sigstore/[email protected]
        with:
          inputs: >-
            ./dist/*.tar.gz
            ./dist/*.whl
      - name: Create GitHub Release
        env:
          GITHUB_TOKEN: ${{ github.token }}
        run: >-
          gh release create
          "$GITHUB_REF_NAME"
          --repo "$GITHUB_REPOSITORY"
          --notes ""
      - name: Upload artifact signatures to GitHub Release
        env:
          GITHUB_TOKEN: ${{ github.token }}
        # Upload to GitHub Release using the `gh` CLI.
        # `dist/` contains the built packages, and the
        # sigstore-produced signatures and certificates.
        run: >-
          gh release upload
          "$GITHUB_REF_NAME" dist/**
          --repo "$GITHUB_REPOSITORY"

```

--------------------------------------------------------------------------------
/src/mcp_sonic_pi/server.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
MCP Server for controlling Sonic Pi via psonic library
"""

import platform
import subprocess

from mcp.server.fastmcp import Context, FastMCP

mcp = FastMCP("sonic-pi")


def check_sonic_pi_running():
    """Check if Sonic Pi is running on the system"""
    system = platform.system()

    if system == "Darwin":
        result = subprocess.run(
            ["pgrep", "-x", "Sonic Pi"], capture_output=True, text=True
        )
        return result.returncode == 0

    return False


try:
    from psonic import *

    PSONIC_AVAILABLE = True
except Exception as e:
    print(f"Error initializing psonic: {e}")
    PSONIC_AVAILABLE = False


@mcp.tool()
async def initialize_sonic_pi() -> str:
    """Initialize the Sonic Pi server

    Returns:
        The system prompt for the server
    """
    if not check_sonic_pi_running():
        return "Error: Sonic Pi does not appear to be running. Please start Sonic Pi first."

    if not PSONIC_AVAILABLE:
        return (
            "Error: The psonic library couldn't be initialized. Check Sonic Pi status."
        )

    try:
        set_server_parameter_from_log("127.0.0.1")
        return system_prompt()
    except Exception as e:
        return f"Error initializing server: {str(e)}"


@mcp.tool()
async def play_music(code: str) -> str:
    """Play music using Sonic Pi code.

    Args:
        code: Sonic Pi Ruby code

    Returns:
        A confirmation message
    """
    if not check_sonic_pi_running():
        return "Error: Sonic Pi does not appear to be running. Please start Sonic Pi first."

    if not PSONIC_AVAILABLE:
        return (
            "Error: The psonic library couldn't be initialized. Check Sonic Pi status."
        )

    try:
        stop()
        run(code)
        send_message("/trigger/prophet", 70, 100, 8)
        return "Code is now running. If you don't hear anything, check Sonic Pi for errors."
    except Exception as e:
        return f"Error running code: {str(e)}"


@mcp.tool()
async def stop_music() -> str:
    """Stop all currently playing Sonic Pi music.

    Returns:
        A confirmation message
    """
    if not check_sonic_pi_running():
        return "Error: Sonic Pi does not appear to be running. Please start Sonic Pi first."

    if not PSONIC_AVAILABLE:
        return (
            "Error: The psonic library couldn't be initialized. Check Sonic Pi status."
        )

    try:
        stop()
        return "Music stopped"
    except Exception as e:
        return f"Error stopping music: {str(e)}"


@mcp.tool()
def get_beat_pattern(style: str) -> str:
    """Get drum beat patterns for Sonic Pi.

    Args:
        style: Beat style (blues, rock, jazz, hiphop, etc.)

    Returns:
        Sonic Pi code for the requested beat pattern
    """
    beats = {
        "blues": """
# Blues Beat
use_bpm 100
swing = 0.15  # Shuffle feel (0 for straight timing)
live_loop :blues_drums do
  sample :hat_tap, amp: 0.9
  sample :drum_bass_hard, amp: 0.9
  sleep 0.5+swing
  sample :hat_tap, amp: 0.7
  sample :drum_bass_hard, amp: 0.8
  sleep 0.5-swing
  sample :drum_snare_hard, amp: 0.8
  sample :hat_tap, amp: 0.8
  sleep 0.5+swing
  sample :hat_tap, amp: 0.7
  sleep 0.5-swing
end
""",
        "rock": """
# Rock Beat
use_bpm 120
live_loop :rock_drums do
  sample :drum_bass_hard, amp: 1
  sample :drum_cymbal_closed, amp: 0.7
  sleep 0.5
  sample :drum_cymbal_closed, amp: 0.7
  sleep 0.5
  sample :drum_snare_hard, amp: 0.9
  sample :drum_cymbal_closed, amp: 0.7
  sleep 0.5
  sample :drum_cymbal_closed, amp: 0.7
  sleep 0.5
end
""",
        "hiphop": """
# Hip-Hop Beat
use_bpm 90
live_loop :hip_hop_drums do
  sample :drum_bass_hard, amp: 1.2
  sleep 1
  sample :drum_snare_hard, amp: 0.9
  sleep 1
  sample :drum_bass_hard, amp: 1.2
  sleep 0.5
  sample :drum_bass_hard, amp: 0.8
  sleep 0.5
  sample :drum_snare_hard, amp: 0.9
  sleep 1
end
""",
        "electronic": """
# Electronic Beat
use_bpm 128
live_loop :electronic_beat do
  sample :bd_haus, amp: 1
  sample :drum_cymbal_closed, amp: 0.3
  sleep 0.5

  sample :drum_cymbal_closed, amp: 0.3
  sleep 0.5

  sample :bd_haus, amp: 0.9
  sample :drum_snare_hard, amp: 0.8
  sample :drum_cymbal_closed, amp: 0.3
  sleep 0.5

  sample :drum_cymbal_closed, amp: 0.3
  sleep 0.5
end
""",
    }

    if style.lower() in beats:
        return beats[style.lower()]
    else:
        return f"Beat style '{style}' not found. Available styles: {', '.join(beats.keys())}"


@mcp.prompt()
def system_prompt():
    return """
    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.

    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.

    When the user asks you to play a chord progression, construct one using the following chord format, and add it to the existing beat.

    Chords have the following format: chord  tonic (symbol), name (symbol)

    Here's an example chord with C tonic and various names:
    (chord :C, '1')
    (chord :C, '5')
    (chord :C, '+5')
    (chord :C, 'm+5')
    (chord :C, :sus2)
    (chord :C, :sus4)
    (chord :C, '6')
    (chord :C, :m6)
    (chord :C, '7sus2')
    (chord :C, '7sus4')
    (chord :C, '7-5')
    (chord :C, 'm7-5')
    (chord :C, '7+5')
    (chord :C, 'm7+5')
    (chord :C, '9')
    (chord :C, :m9)
    (chord :C, 'm7+9')
    (chord :C, :maj9)
    (chord :C, '9sus4')
    (chord :C, '6*9')
    (chord :C, 'm6*9')
    (chord :C, '7-9')
    (chord :C, 'm7-9')
    (chord :C, '7-10')
    (chord :C, '9+5')
    (chord :C, 'm9+5')
    (chord :C, '7+5-9')
    (chord :C, 'm7+5-9')
    (chord :C, '11')
    (chord :C, :m11)
    (chord :C, :maj11)
    (chord :C, '11+')
    (chord :C, 'm11+')
    (chord :C, '13')
    (chord :C, :m13)
    (chord :C, :add2)
    (chord :C, :add4)
    (chord :C, :add9)
    (chord :C, :add11)
    (chord :C, :add13)
    (chord :C, :madd2)
    (chord :C, :madd4)
    (chord :C, :madd9)
    (chord :C, :madd11)
    (chord :C, :madd13)
    (chord :C, :major)
    (chord :C, :M)
    (chord :C, :minor)
    (chord :C, :m)
    (chord :C, :major7)
    (chord :C, :dom7)
    (chord :C, '7')
    (chord :C, :M7)
    (chord :C, :minor7)
    (chord :C, :m7)
    (chord :C, :augmented)
    (chord :C, :a)
    (chord :C, :diminished)
    (chord :C, :dim)
    (chord :C, :i)
    (chord :C, :diminished7)
    (chord :C, :dim7)
    (chord :C, :i7)

    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.
"""


def main():
    if not check_sonic_pi_running():
        print("⚠️ Warning: Sonic Pi doesn't appear to be running")
        print("Please start Sonic Pi before using this MCP server")
    else:
        print("✅ Sonic Pi is running")

    if not PSONIC_AVAILABLE:
        print("⚠️ Warning: psonic library is not properly initialized")
        print("Make sure Sonic Pi is running and log file is accessible")
    else:
        print("✅ psonic library initialized")

    print("Sonic Pi MCP Server initialized")

    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()

```