Omni ships with its own MCP server, omni-mcp. This page is for when you want a different one: a community server in your favourite language, one that exposes only part of the API, one with your own prompts, or one that adds a workflow on top. You do not need Omni's source code, only its API. The example code on this page is under the MIT License, so you can start from it and publish what you build.
What a server has to do
An MCP server for Omni is a client of the OmniDAW API that speaks MCP to an AI assistant on its other side. Omni already does the work (tracks, clips, theory, meters, undo); your program connects the two:
- Finds Omni through the endpoint file and opens one TCP connection.
- Sends
api.hellowith the token. - Reads the catalog with
api.methods. - Tells the assistant about each method as an MCP tool.
- When the assistant calls a tool, sends the matching API call and returns the result, or the error as a tool error.
That is all. The catalog already contains the text a model needs, so there is nothing to write per method.
From catalog to tool
Every entry of api.methods maps onto one MCP tool field by field:
| MCP tool field | From the catalog entry |
|---|---|
name | name with the dot replaced by an underscore: clips.write_midi becomes clips_write_midi |
description | summary, which is written for a model |
inputSchema | params, a JSON Schema the MCP SDKs accept as is |
annotations.title | title |
annotations.readOnlyHint | effect is read |
annotations.destructiveHint | effect is destructive |
annotations.idempotentHint | idempotent |
Build the list from api.methods when the server starts instead of writing it by hand. Then a newer Omni brings its new methods to your server without a release.
The effect field is also your permission model. A client can run read tools without asking, ask before edit, and ask clearly before destructive. Because every edit is one undo step in Omni, even a wrong edit is cheap to take back, and edit_undo is itself a tool.
A complete server in Python
The following server turns every method into a tool. It uses the Python client (omnidaw.py, saved in the same folder as the server) and the official MCP SDK (pip install mcp). It was tested against the SDK's own client with Omni running: all 102 tools were listed with their annotations, reads and tool calls worked, and a misspelled parameter came back as a readable tool error. Download the server, or all the examples as one zip.
from __future__ import annotations
import asyncio
import json
import sys
import time
from mcp import types
from mcp.server.lowlevel import Server
from mcp.server.stdio import stdio_server
from omnidaw import Omni
INSTRUCTIONS = (
"Tools drive the project open in Omni, a DAW. Start with session_info and "
"tracks_list. Time is in beats, levels in dB, pitch as MIDI numbers. Every "
"edit is one undo step: edit_undo takes back the newest. Long work "
"(analysis_capture, export_render) returns a job: call jobs_wait with its id."
)
WAIT_FOR_OMNI_SECONDS = 30
_omni: Omni | None = None
#: MCP tool name -> catalog method, filled once at start-up.
_catalog: dict[str, dict] = {}
def connect() -> Omni:
"""The connection to Omni, opened on first use and again after it drops."""
global _omni
if _omni is None:
_omni = Omni("omni-mcp-example")
return _omni
def tool_of(method: dict) -> types.Tool:
"""One catalog method as an MCP tool."""
effect = method["effect"]
return types.Tool(
name=method["name"].replace(".", "_"),
description=method["summary"],
input_schema=method["params"],
annotations=types.ToolAnnotations(
title=method["title"],
read_only_hint=effect == "read",
destructive_hint=effect == "destructive",
idempotent_hint=method["idempotent"],
open_world_hint=False,
),
)
def run_call(method: str, arguments: dict) -> str:
"""The blocking part of a tool call; runs in a thread."""
global _omni
try:
return json.dumps(connect().call(method, arguments), indent=2, ensure_ascii=False)
except (ConnectionError, OSError):
_omni = None # Omni closed or restarted: the next call connects again
raise
def error(text: str) -> types.CallToolResult:
"""A tool error the model can read and correct itself from."""
return types.CallToolResult(is_error=True, content=[types.TextContent(type="text", text=text)])
async def list_tools(ctx, params) -> types.ListToolsResult:
return types.ListToolsResult(tools=[tool_of(m) for m in _catalog.values()])
async def call_tool(ctx, params) -> types.CallToolResult:
method = _catalog.get(params.name)
if method is None:
return error(f"unknown tool {params.name}")
try:
text = await asyncio.to_thread(run_call, method["name"], params.arguments or {})
return types.CallToolResult(content=[types.TextContent(type="text", text=text)])
except (ConnectionError, OSError) as e:
return error(f"Omni is not reachable ({e}). Start Omni with the API on and try again.")
except Exception as e: # an OmniError: bad parameters, unknown track, ...
return error(str(e))
def load_catalog() -> None:
"""Read the tool list from Omni, waiting for it to start if needed."""
global _omni
deadline = time.monotonic() + WAIT_FOR_OMNI_SECONDS
while True:
try:
_catalog.update({m["name"].replace(".", "_"): m for m in connect().methods.values()})
return
except (ConnectionError, OSError) as e:
_omni = None
if time.monotonic() > deadline:
sys.exit(f"Omni is not running with the API on ({e}). Start it and try again.")
time.sleep(1)
async def main() -> None:
server = Server(
"omni-example",
instructions=INSTRUCTIONS,
on_list_tools=list_tools,
on_call_tool=call_tool,
)
async with stdio_server() as (read, write):
await server.run(read, write, server.create_initialization_options())
if __name__ == "__main__":
load_catalog()
asyncio.run(main())
Register it in your assistant like any stdio server. For Claude Desktop, in its configuration file:
{
"mcpServers": {
"omni": {
"command": "python",
"args": ["C:/path/to/omni_mcp_server.py"]
}
}
}
For Claude Code: claude mcp add omni -- python C:/path/to/omni_mcp_server.py.
Note. The code targets version 2 of the Python MCP SDK, where handlers are passed to the Server constructor (on_list_tools, on_call_tool). In older versions of the SDK the same handlers are registered with decorators; the logic of this server stays the same.
Test it without an assistant
The SDK has a client, so you can check your server from a script and see exactly what an assistant would see:
import asyncio, sys
from mcp import Client
from mcp.client.stdio import StdioServerParameters
async def main():
params = StdioServerParameters(command=sys.executable, args=["omni_mcp_server.py"])
async with Client(params) as client:
tools = (await client.list_tools()).tools
print(len(tools), "tools")
result = await client.call_tool("theory_scale", {"root": "A", "mode": "minor"})
print(result.content[0].text[:200])
asyncio.run(main())
Run it from the folder that holds both omnidaw.py and omni_mcp_server.py, with Omni running and its API on.
Things a good server does
Connect lazily and reconnect. Omni may start after your server and may restart while it runs. Open the connection on first use. If a call fails with a connection error, forget the connection; the next call reads endpoint.json again (the port and token change on every Omni run). The example above does this.
Do not resend an edit you did not see answered. If the connection drops in the middle of an edit or destructive call, you cannot know whether Omni ran it. Tell the model to check the state first (tracks_list, clips_list). Reads are always safe to repeat.
Return errors as tool errors. Omni's error messages are written for a model and often name the method to use instead. Pass the text on as an MCP error result (isError) and the assistant usually corrects itself in the next call. Do not turn them into protocol errors.
Handle long work as jobs. analysis_capture, export_render, export_stems, audio_transcribe and audio_split_stems return a job id at once. The model calls jobs_wait (up to 9 seconds per call) until the job is done. Say so in your server's instructions, as the example does.
Offer prompts. MCP prompts are ready-made briefs. Omni's server has six: mix_review, loudness_check, masking_check, master_for_target, arrange_song and compose_loop. Each walks the assistant through a loop such as capture, analyse, propose, apply, capture again. They are the place for opinions about what a good mix is; the API itself stays factual.
Keep the token private. Never put it in a tool result, a prompt or a log line.
Mind the tool count. 102 tools is a lot for small models. If yours struggles, expose a subset by domain (for example tracks, clips, theory, mixer) and add the rest when the assistant asks. The domain is the part of the method name before the dot, and each catalog entry has it as domain.
Getting the catalog without Omni
The bundled server can print the catalog without Omni running, which is handy for generating documentation, types or tests:
omni-mcp --catalog > omni-api.json
The file is the same document api.methods returns.
What the bundled server adds
omni-mcp is installed with Omni: next to omni_host.exe on Windows, and inside the AppImage on Linux, where it starts as Omni.AppImage --mcp. It offers the same tools as the example above, plus a few things you may want to copy:
- Tools before Omni starts. The catalog is built into it, so its tool list exists even when Omni is not running. A server in another language reads the catalog from the running Omni instead, as the Python example does, or from a copy saved with
omni-mcp --catalog. - Prompts and resources. The six prompts above, and two resources: the API catalog and a live overview of the session (project, transport and all tracks).
- Careful reconnecting. After Omni restarts it connects again by itself, and if a connection breaks in the middle of a call it does not repeat that call.
If you write in Rust, the session_info.rs example in the download shows the whole protocol with nothing but serde_json.
