dmx

Enterprise Deployment

Rolling dmx out across an engineering organization comes down to three pieces, each owned by different people:

PieceWhat it isWho owns it
Developer machinesuv, plus the MCP config that starts dmx in each IDEIT, through MDM
Shared processOrg and team repos of loops, skills, and validatorsPlatform or developer-experience team
App repos.dmx/ in each repository, with a pinned shared-source versionEach team

This page covers the first piece in detail and links to the rest.

What each machine needs

uv. IDEs start dmx with uvx, which ships with uv. Nothing else is installed: uvx fetches dmx and its dependencies on first run and caches them per user.

Python 3.11 or later. dmx needs it, and macOS ships an older Python. When no suitable Python is installed, uv downloads its own managed copy automatically. You don't need to install Python separately unless your network blocks that download.

MCP servers for your Git host and ticketing. /dmx/init checks for the GitHub MCP server, plus the Atlassian server if you use Jira. Deploy those alongside dmx.

Network access to these hosts, or internal mirrors of them:

HostUsed for
astral.sh, releases.astral.sh, github.comInstalling uv
pypi.org, files.pythonhosted.orgDownloading dmx and its dependencies
github.com (astral-sh/python-build-standalone releases)Downloading a managed Python, when the machine has no Python 3.11+
Your Git host/dmx/sync cloning shared sources

Mirrors, proxies, and certificates

uvx reads a system-wide uv config file at /etc/uv/uv.toml, so you can point every user at internal mirrors once per machine:

# /etc/uv/uv.toml
index-url = "https://pypi.internal.example.com/simple"
python-install-mirror = "https://mirror.internal.example.com/python-build-standalone"
system-certs = true
  • index-url replaces PyPI.
  • python-install-mirror replaces https://github.com/astral-sh/python-build-standalone/releases/download.
  • system-certs makes uv trust certificates in the macOS keychain, which you need behind a TLS-inspecting proxy. On uv versions before 0.11, the setting is native-tls.

See uv's configuration files and settings reference.

Deploy the MCP config

Every IDE uses the same mcpServers config. For a managed rollout, make two changes to the version in MCP Setup:

  • Use the absolute path to uvx, such as /usr/local/bin/uvx. Apps opened from the Dock don't see the PATH set in shell profiles, so a bare uvx can fail to start.
  • Pin the version (deepmodel-dmx==0.5.0), so every machine runs the release you tested.
{
  "mcpServers": {
    "dmx": {
      "command": "/usr/local/bin/uvx",
      "args": ["--from", "deepmodel-dmx==0.5.0", "dmx", "serve"]
    }
  }
}

Where each IDE reads it:

IDEPer-user fileNotes
Cursor~/.cursor/mcp.json
VS Code (GitHub Copilot)~/.copilot/mcp-config.json
Claude Code desktop app~/Library/Application Support/Claude/claude_desktop_config.jsonCovers the app's Code tab only. The claude CLI and IDE extensions don't read it.
Claude Code CLI and IDE extensionsNone suitable for MDMUse managed-mcp.json (below) or a committed .mcp.json

These files often already exist, with other servers and settings in them. Merge the dmx entry in. Don't replace the file.

Claude Code CLI: managed-mcp.json

Claude Code in the terminal and IDE extensions keeps per-user servers in ~/.claude.json, alongside its own state, so don't write to that file from MDM. Instead, Claude Code reads a system-wide file that only an administrator can write:

PlatformPath
macOS/Library/Application Support/ClaudeCode/managed-mcp.json
Linux and WSL/etc/claude-code/managed-mcp.json
WindowsC:\Program Files\ClaudeCode\managed-mcp.json

It uses the same mcpServers format, but it takes exclusive control: Claude Code loads only the servers in this file, and developers can't add their own. List every MCP server your developers need, including the GitHub server /dmx/init checks for and the Atlassian server if you use Jira. Every user on the machine can read the file, so don't put tokens in it. See Claude Code's managed MCP documentation.

If that's more control than you want, commit a .mcp.json to each repo instead. Claude Code and VS Code both read it. See Team Setup.

Jamf Pro policy script

This script installs uv and adds dmx to the MCP config of every installed IDE for the logged-in user. It keeps any other servers and settings in those files. If a developer already has a dmx entry, the script updates only its command and args, so their env and other settings are kept. It doesn't touch Claude Code's managed-mcp.json, because that file needs your full server list.

#!/bin/bash
#
# deploy-dmx.sh: install uv and add dmx to each IDE's MCP config.
#
# Run as a Jamf Pro policy script (runs as root).
#   Parameter 4: dmx version to pin, for example 0.5.0 (required)
#
# What it does:
#   1. Installs uv to /usr/local/bin if uvx isn't there yet.
#   2. For the logged-in user, adds a "dmx" entry to the MCP config of each
#      installed IDE: Cursor, VS Code, and the Claude desktop app. Other
#      servers and settings in those files are kept. If a "dmx" entry already
#      exists, only its command and args are updated, so settings such as
#      env are kept.
#
# A file that isn't plain JSON (for example, one with comments) is left
# unchanged and reported, so you can add dmx to it by hand.

set -euo pipefail

DMX_VERSION="${4:-}"
UV_DIR="/usr/local/bin"

if [[ -z "$DMX_VERSION" ]]; then
  echo "Set parameter 4 to the dmx version to install, for example 0.5.0." >&2
  exit 1
fi

# 1. Install uv for every user on the machine.
if [[ ! -x "$UV_DIR/uvx" ]]; then
  mkdir -p "$UV_DIR"
  curl -LsSf https://astral.sh/uv/install.sh | env UV_UNMANAGED_INSTALL="$UV_DIR" sh
fi

# 2. Find the logged-in user. Jamf's $3 isn't set for every trigger.
user="$(/usr/bin/stat -f%Su /dev/console)"
if [[ -z "$user" || "$user" == "root" || "$user" == "loginwindow" ]]; then
  echo "No user is logged in. uv is installed; run again at login to add the MCP config."
  exit 0
fi
home="$(/usr/bin/dscl . -read "/Users/$user" NFSHomeDirectory | /usr/bin/awk '{print $2}')"

# The server entry. An absolute path to uvx, because apps opened from the
# Dock don't see the PATH set in shell profiles.
entry="{\"command\":\"$UV_DIR/uvx\",\"args\":[\"--from\",\"deepmodel-dmx==$DMX_VERSION\",\"dmx\",\"serve\"]}"

# JavaScript for Automation, built into macOS: sets the entry's command and
# args under mcpServers and keeps everything else in the file.
merge_js="$(/usr/bin/mktemp /tmp/dmx-merge.XXXXXX)"
trap 'rm -f "$merge_js"' EXIT
cat > "$merge_js" <<'JS'
ObjC.import('Foundation');
function run(argv) {
  const [path, name, entryJson] = argv;
  const fm = $.NSFileManager.defaultManager;
  let config = {};
  if (fm.fileExistsAtPath(path)) {
    const text = ObjC.unwrap($.NSString.stringWithContentsOfFileEncodingError(path, $.NSUTF8StringEncoding, null));
    if (text === undefined) return `skipped ${path}: could not read the file`;
    if (text.trim() !== '') {
      try {
        config = JSON.parse(text);
      } catch (e) {
        return `skipped ${path}: not plain JSON (${e.message}). Add dmx to it by hand.`;
      }
    }
  }
  if (typeof config !== 'object' || config === null || Array.isArray(config)) {
    return `skipped ${path}: expected a JSON object`;
  }
  if (config.mcpServers === undefined) config.mcpServers = {};
  if (typeof config.mcpServers !== 'object' || config.mcpServers === null || Array.isArray(config.mcpServers)) {
    return `skipped ${path}: mcpServers is not an object`;
  }
  const entry = JSON.parse(entryJson);
  const current = config.mcpServers[name];
  if (typeof current === 'object' && current !== null && !Array.isArray(current)) {
    current.command = entry.command;
    current.args = entry.args;
  } else {
    config.mcpServers[name] = entry;
  }
  const out = $.NSString.alloc.initWithUTF8String(JSON.stringify(config, null, 2) + '\n');
  if (!out.writeToFileAtomicallyEncodingError(path, true, $.NSUTF8StringEncoding, null)) {
    return `failed ${path}: could not write the file`;
  }
  return `updated ${path}`;
}
JS
chmod 644 "$merge_js"

# Write a config file as the user, so it keeps the right owner.
add_dmx() {
  local file="$1"
  /usr/bin/sudo -u "$user" /bin/mkdir -p "$(dirname "$file")"
  /usr/bin/sudo -u "$user" /usr/bin/osascript -l JavaScript "$merge_js" "$file" dmx "$entry"
}

# Only configure IDEs that are installed or already have a config folder.
if [[ -d "/Applications/Cursor.app" || -d "$home/.cursor" ]]; then
  add_dmx "$home/.cursor/mcp.json"
fi
if [[ -d "/Applications/Visual Studio Code.app" || -d "$home/.copilot" ]]; then
  add_dmx "$home/.copilot/mcp-config.json"
fi
if [[ -d "/Applications/Claude.app" || -d "$home/Library/Application Support/Claude" ]]; then
  add_dmx "$home/Library/Application Support/Claude/claude_desktop_config.json"
fi

echo "Done. Restart any open IDEs to load dmx."

To use it:

  1. In Jamf Pro, add the script under Settings → Computer management → Scripts, and label parameter 4 dmx version.
  2. Create a policy that runs the script, with parameter 4 set to the version you're rolling out, such as 0.5.0.
  3. Trigger it at Login, so the user's home directory is available, and at Recurring Check-in, so an existing session picks up a new version. Set the frequency to Ongoing if you want later version bumps to apply. The script is safe to re-run.

How it works:

  • uv goes into /usr/local/bin through uv's official installer with UV_UNMANAGED_INSTALL, which doesn't edit anyone's shell profile. uv's self-update is off for that kind of install, so update uv by re-running the installer from MDM.
  • The logged-in user comes from /dev/console, because Jamf's $3 (the username) isn't set for every trigger.
  • Config files are merged with JavaScript for Automation (osascript -l JavaScript), which is built into macOS, so the script doesn't depend on jq or Python. It runs as the user, so the files keep the right owner. Files are rewritten with 2-space indentation; their content and key order don't change.
  • A file with comments (VS Code allows them) or other non-JSON content is left unchanged and reported in the policy log. Add dmx to it by hand.

Test it on a pilot group before a wide rollout. Open IDEs need a restart to load dmx.

Pin and roll out versions

  1. Pin one version in the deployed config, deepmodel-dmx==X.Y.Z.
  2. Try a new release with a pilot group. The Changelog and Upgrading dmx list what changes.
  3. Roll it out by changing the version in your MDM policy, or in each repo's committed config.
  4. Refresh each repo's rules with /dmx/upgrade, then commit. When a repo's rules are older than the installed dmx, run_loop, loop_status, and list_skills remind developers to run it.

Shared process across the organization

The MCP config gets dmx onto machines. Shared sources give every team the same loops, skills, and validators:

  • An org repo holds the baseline: Spec and Release Loops, security and compliance validators, house skills.
  • Team repos layer their own loops and skills on top.
  • Each app repo pins both in .dmx/shared-sources.yaml and vendors them with /dmx/sync.

Release the org repo with tags. Teams adopt a new version in an ordinary PR. A CODEOWNERS entry for .dmx/shared-sources.yaml, .dmx/vendor/, .dmx/loops/, .dmx/skills/, and validators/ puts your platform team on every change to the process. See Team Setup and Shared Sources.

Credentials

  • /dmx/sync runs plain git, so it uses each machine's own Git credentials: an SSH key, gh auth, or a deploy key on CI. There's nothing dmx-specific to configure.
  • MCP servers that need tokens, such as GitHub or Atlassian, should authenticate per user. Don't put tokens in files you deploy with MDM.
  • dmx itself needs no credentials over stdio. For the HTTP transport, see REQUIRE_API_KEY / MCP_API_KEY.

Verify a rollout

On a developer machine:

  • Claude Code CLI: claude mcp list shows dmx as connected.
  • Cursor, VS Code, or the desktop app: ask the agent "What dmx skills are available?". A working setup lists the /dmx/* commands.
  • dmx itself: /usr/local/bin/uvx --from deepmodel-dmx==0.5.0 dmx version prints the version.

Across repositories, the dmx rule files record the version they came from:

grep -rh "dmx-workflow-version" CLAUDE.md AGENTS.md .cursor .claude .github .agents 2>/dev/null | sort -u

A repo that prints an older version, or nothing, needs /dmx/upgrade.

On this page