# Project export: Flursor

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: Cal Hacks 12.0
- Tagline: Cursor for audio engineering
- Devpost: https://devpost.com/software/flursor
- GitHub: http://github.com/Abhibob/flursor
- Team: 1 GitHub contributor(s) — Abhibob (1 commits)

## Devpost submission (written by the team)

### Inspiration

The project was inspired by Cursor. It is similar to Cursor, but for audio engineering or music production.

### What it does

It has a promptable AI that integrates completely with natural-language prompts into the audio editing software REAPER. We chose REAPER since it was scriptable with Lua and inexpensive.

### How we built it

The project has many components. It runs an MCP (model context protocol) server which integrates with REAPER directly using the OSC protocol and Lua scripts that can control REAPER. The MCP allows for an API from which an agent can make tool calls in REAPER. Then, we have our agent, which consists of a planner and executor and uses GPT-5. The planner plans out the tool calls to make based on the prompt, and the executor calls the MCP server to make said changes. Finally, we use MOSNet (MOS stands for Median Opinion Score) to gauge how the AI-determined MOS, or quality, of the audio changed after or before a prompt based on a 4-bar snippet. The MOSNet component is completely working and at the end of a prompt all data is sent to an endpoint to be cached for future training (MOS improvement, prompt, tool calls). However, the MOS integration with the agent fails sometimes when export of the audio from REAPER fails, which is something that can be improved on in the Lua script.

### Challenges we ran into

We ran into many challenges, such as the MCP not working due to our Lua code being buggy, and our executor and planner having difficulty coming up with the current commands to the MCP server. In addition, we had to run all the different components together and fix CORS issues with requests.

### Accomplishments we're proud of

We're proud of overcoming almost all our issues and coming to a robust MVP for agentic audio engineering, and there's still a long way to go for a full-fledged suite. However, we managed to get working Lua scripting and a completely working agentic AI system that was able to make tool calls in REAPER, building the MCP from scratch ourselves.

### What we learned

We learned that multi-component projects like this can be super rewarding at the end when all the components work together and pay off for a good product.

### What's next

We will have to fix issues in Lua with audio export and increase the functionality of the tool calls to making very specific selections and therefore be able to completely process very complex prompts, as well as incorporating reinforcement learning to train a model better to use our MCP.

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 20 recognized source files, 68 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- TensorFlow (technology) — detected in the code
- OpenAI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (23 of 23)

```
agent/agentic_mos_judge.py
agent/agentic.py
agent/api.py
agent/run.sh
mcp/deep_reaper_tests.py
mcp/MCP_CommandRunner.lua
mcp/mcp_server.py
mcp/pytest.ini
mcp/reaper_osc_bridge.py
mcp/TEST_curl.sh
mcp/TEST_mcp.py
mosnet/mosnet_api.py
requirements.txt
run.sh
ui/eslint.config.js
ui/index.html
ui/package.json
ui/README.md
ui/src/App.css
ui/src/App.jsx
ui/src/index.css
ui/src/main.jsx
ui/vite.config.js
```

### Dependencies

- requirements.txt: asyncio@>=3.4.3, fastapi@>=0.111, librosa@>=0.10, numpy@>=1.24, pydantic@>=2.7, pytest@>=8.3.4, python-dotenv@>=1.0.0, python-multipart@>=0.0.9, python-osc@>=1.9.0, requests@>=2.31, scipy@>=1.11, soundfile@>=0.12, tensorflow@>=2.12, uvicorn@>=0.30, uvicorn[standard]@>=0.30, websockets@>=13.0
- ui/package.json: @eslint/js@^9.36.0, @types/react@^19.1.16, @types/react-dom@^19.1.9, @vitejs/plugin-react@^5.0.4, eslint@^9.36.0, eslint-plugin-react-hooks@^5.2.0, eslint-plugin-react-refresh@^0.4.22, globals@^16.4.0, lucide-react@^0.548.0, react@^19.1.1, react-dom@^19.1.1, vite@^7.1.7

### Recent commits (newest first)

- added files

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
fastapi>=0.111
uvicorn[standard]>=0.30
librosa>=0.10
soundfile>=0.12
numpy>=1.24
scipy>=1.11
pydantic>=2.7
requests>=2.31
tensorflow>=2.12
python-multipart>=0.0.9
fastapi>=0.111
uvicorn>=0.30
websockets>=13.0
asyncio>=3.4.3
pydantic>=2.7
python-osc>=1.9.0
python-dotenv>=1.0.0
pytest>=8.3.4
```

### ui/package.json

```
{
  "name": "ui",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "lucide-react": "^0.548.0",
    "react": "^19.1.1",
    "react-dom": "^19.1.1"
  },
  "devDependencies": {
    "@eslint/js": "^9.36.0",
    "@types/react": "^19.1.16",
    "@types/react-dom": "^19.1.9",
    "@vitejs/plugin-react": "^5.0.4",
    "eslint": "^9.36.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.22",
    "globals": "^16.4.0",
    "vite": "^7.1.7"
  }
}

```

### ui/src/main.jsx

```javascript
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import AgentChatUI from "./App";

const rootEl = document.getElementById("root");
if (!rootEl) throw new Error("Root element #root not found");

ReactDOM.createRoot(rootEl).render(
  <React.StrictMode>
    <AgentChatUI />
  </React.StrictMode>
);

```

### ui/src/App.jsx

```javascript
import React, { useEffect, useRef, useState } from "react";
import { Send, Plug, Settings, Activity, Loader2, Server, TrendingUp, TrendingDown } from "lucide-react";

const niceTime = () => new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });

const Bubble = ({ role, text, time }) => (
  <div className={`flex ${role === "user" ? "justify-end" : "justify-start"}`}>
    <div className={`max-w-[80%] rounded-2xl px-4 py-3 mb-3 shadow ${role === "user" ? "bg-blue-600 text-white" : "bg-zinc-100 dark:bg-zinc-800 dark:text-zinc-100"}`}>
      <div className="whitespace-pre-wrap leading-relaxed">{text}</div>
      <div className={`mt-1 text-[11px] opacity-60 ${role === "user" ? "text-white" : "text-zinc-500"}`}>{time || niceTime()}</div>
    </div>
  </div>
);

const Tag = ({ icon: Icon, children, tone = "neutral" }) => {
  const cls = tone === "good" ? "bg-emerald-50 text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-300"
            : tone === "bad" ? "bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-300"
            : "bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300";
  return (
    <span className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full ${cls}`}>
      {Icon && <Icon className="h-3.5 w-3.5"/>}{children}
    </span>
  );
};

export default function AgentChatUI() {
  const [messages, setMessages] = useState([{ id: 1, role: "agent", text: "Hey! Tell me what to make — e.g. '808 in C minor at 140 bpm for 4 bars and warm the mix'.", time: niceTime() }]);
  const [input, setInput] = useState("");
  const [sessionId] = useState(() => Math.random().toString(36).slice(2));
  const [sending, setSending] = useState(false);

  const [apiBase, setApiBase] = useState("/api");
  const [mcpUrl, setMcpUrl] = useState("ws://127.0.0.1:8765");
  const [connectMcp, setConnectMcp] = useState(false);

  const [mcpStatus, setMcpStatus] = useState("disconnected");
  const mcpRef = useRef(null);

  const streamRef = useRef(null);
  const listRef = useRef(null);

  const [mosInfo, setMosInfo] = useState(null);

  const scrollToBottom = () => {
    requestAnimationFrame(() => listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" }));
  };
  useEffect(scrollToBottom, [messages.length]);

  useEffect(() => {
    if (!connectMcp) { try { mcpRef.current?.close?.(); } catch {} setMcpStatus("disconnected"); return; }
    try {
      const ws = new WebSocket(mcpUrl);
      mcpRef.current = ws;
      ws.onopen = () => { setMcpStatus("connected"); ws.send(JSON.stringify({ hello: "agent", name: "ui" })); };
      ws.onmessage = (e) => {
        try {
          const msg = JSON.parse(e.data);
          if (msg.status === "ok" && msg.role) return; // hello
          if (msg.category || msg.cmd || msg.STATUS) {
            setMessages((m) => m.concat([{ id: Date.now()+Math.random(), role: "agent", text: `⚙️ ${JSON.stringify(msg)}`, time: niceTime() }]));
          }
        } catch {}
      };
      ws.onclose = () => setMcpStatus("disconnected");
      ws.onerror = () => setMcpStatus("error");
      return () => ws.close();
    } catch {
      setMcpStatus("error");
    }
  }, [connectMcp, mcpUrl]);

  const startStream = () => {
    try {
      const es = new EventSource(`${apiBase.replace(/\/$/, "")}/stream?session=${sessionId}`);
      streamRef.current = es;
      es.onmessage = (e) => {
        try {
          const data = JSON.parse(e.data);
          if (data.type === "log" || data.type === "event") {
            setMessages((m) => m.concat([{ id: Date.now()+Math.random(), role: "agent", text: data.text, time: niceTime() }]));
          } else if (data.type === "mos") {
            setMosInfo({ before: data.mos_before, after: data.mos_after, delta: data.delta, improved: !!data.improved });
          } else if (data.type === "final") {
            setMessages((m) => m.concat([{ id: Date.now()+Math.random(), role: "agent", text: `✅ ${data.text}`, time: niceTime() }]));
          }
        } catch {}
      };
      es.onerror = () => { /* keep-alive */ };
    } catch {
      // stays in mock mode
    }
  };
  const stopStream = () => { try { streamRef.current?.close?.(); } catch {} };

  const send = async () => {
    if (!input.trim()) return;
    const userText = input.trim();
    setInput("");
    setMessages((m) => m.concat([{ id: Date.now(), role: "user", text: userText, time: niceTime() }]));
    setSending(true);

    let usedBackend = false;
    try {
      startStream();
      const res = await fetch(`${apiBase.replace(/\/$/, "")}/chat`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ sessionId, message: userText })
      });
      usedBackend = res.ok;
    } catch {}

    if (!usedBackend) {
      const idBase = Date.now()+Math.random();
      const steps = [
        "Thinking about your goal…",
        "Plan: add instrument → create pattern → add notes",
        "Result: STATUS=ok (mock)",
        "Rendering before/after (mock)",
      ];
      for (const s of steps) {
        await new Promise(r => setTimeout(r, 500));
        setMessages((m) => m.concat([{ id: idBase + Math.random(), role: "agent", text: s, time: niceTime() }]));
      }
      setMosInfo({ before: 3.85, after: 4.12, delta: 0.27, improved: true });
      setMessages((m) => m.concat([{ id: idBase + 9999, role: "agent", text: "Done — MOS improved (Δ=+0.27).", time: niceTime() }]));
    }

    setSending(false);
  };

  return (
    <div style={{ minHeight: '100vh', width: '100%', backgroundColor: '#fafafa', color: '#000', padding: '20px' }}>
      <h1 style={{ fontSize: '32px', color: '#000', marginBottom: '20px' }}>DAW Agent Chat</h1>
      <div className="mx-auto max-w-6xl px-4 py-6 grid grid-cols-1 md:grid-cols-[1fr,360px] gap-4">
        <div className="rounded-3xl border border-zinc-200 dark:border-zinc-800 bg-white/70 dark:bg-zinc-900/60 backdrop-blur shadow-sm flex flex-col h-[85v
[truncated — 6188 more characters]
```

### run.sh

```shell
#!/bin/bash

cd mcp
python3 mcp_server.py &
echo "MCP server started (PID: $!)"
python3 reaper_osc_bridge.py &
echo "Reaper OSC bridge started (PID: $!)"

cd ../agent
python3 -m uvicorn api:app --host 127.0.0.1 --port 5055 --log-level warning &
echo "Agent server started (PID: $!)"

cd ../mosnet
python3 mosnet_api.py --host 127.0.0.1 --port 5008 &
echo "MOSNet server started (PID: $!)"

cd ../ui
npm run dev -- --host 0.0.0.0 --port 4000 &
echo "UI server started (PID: $!)"

echo "All services started in background"
```

### agent/run.sh

```shell
export MCP_URL="ws://127.0.0.1:8765"
export PLANNER=heuristic
export PLANNER_MODEL=gpt-4o-mini
export RENDER_SUPPORTED=0

python3 -m uvicorn api:app --host 127.0.0.1 --port 5055 --log-level info
```

### ui/vite.config.js

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:5055',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },
})

```

### ui/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>DAW Agent Chat</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script>
      tailwind.config = {
        darkMode: 'media'
      }
    </script>
  </head>
  <body style="background-color: #ffffff;">
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

```

### mcp/TEST_curl.sh

```shell
#!/usr/bin/env bash
set -euo pipefail
WS_URL="${MCP_URL:-ws://127.0.0.1:8765}"

check_has_ws() {
  if ! curl --help all 2>/dev/null | grep -q "--ws"; then
    echo "curl without --ws support; skipping"; exit 0
  fi
}
check_has_ws

curl --no-progress-meter --ws --ws-data '{"hello":"agent","name":"curl"}' "$WS_URL" | grep -q '"role":"agent"' && echo "agent hello OK"
curl --no-progress-meter --ws --ws-data '{"hello":"bridge","daw":"curl"}' "$WS_URL" | grep -q '"role":"bridge"' && echo "bridge hello OK"

curl --no-progress-meter --ws --ws-data '{"hello":"agent","name":"smoke"}
{"category":"transport","cmd":"play"}' "$WS_URL" | grep -q '"status":"sent"' && echo "routing ack OK"

```

### ui/eslint.config.js

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{js,jsx}'],
    extends: [
      js.configs.recommended,
      reactHooks.configs['recommended-latest'],
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
      parserOptions: {
        ecmaVersion: 'latest',
        ecmaFeatures: { jsx: true },
        sourceType: 'module',
      },
    },
    rules: {
      'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
    },
  },
])

```

[11 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]