[BUG] [Claude Cowork] Managed config managedMcpServers and inferenceModels arrays silently dropped from .mobileconfig — writer↔reader type contract mismatch

Resolved 💬 2 comments Opened Apr 27, 2026 by anuragrazorpay Closed May 29, 2026

Bug Description

Claude Cowork's macOS managed-prefs reader cannot consume managedMcpServers or inferenceModels from a .mobileconfig profile installed via standard MDM tooling. Apple's Configuration Profile writer encodes nested-array config keys as JSON-encoded <string> values (not as native <array> plist elements), and Claude's reader rejects them with a type-mismatch error logged to ~/Library/Logs/Claude/main.log:

Key 'managedMcpServers' exists but is not a boolean
Key 'inferenceModels' exists but is not a boolean

Net effect: enterprises that distribute Claude Cowork via Jamf / Intune / any standard MDM and try to pre-configure managed MCP servers or model lists hit a silent failure. The "Connectors & extensions" panel shows "None configured" even though the profile is correctly installed and profiles list confirms it active.

Boolean and string keys in the same profile (disableAutoUpdates, inferenceProvider, inferenceGatewayBaseUrl, etc.) load correctly — proving this is specifically a type-mismatch on array-shaped keys.

---

What Should Happen

Either:

  1. The reader accepts arrays serialized as JSON-encoded strings (interop with what mobileconfig writers actually produce in the wild), OR
  2. The writer (Anthropic's profile generator, if one exists) outputs native <array> plist elements, OR
  3. At minimum, the error message accurately describes the type mismatch (e.g. Key 'managedMcpServers' expected array, got string) instead of the misleading is not a boolean, so admins can self-diagnose

The current behavior is the worst of all worlds: the profile installs, the keys are present, no error surfaces in the Settings UI, and only journald/Console.app reveals the silent rejection.

---

Reproduction Steps

  1. Create a .mobileconfig Configuration Profile targeting PayloadType = com.anthropic.claudefordesktop with both array-typed keys:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>PayloadType</key>
<string>com.anthropic.claudefordesktop</string>
<key>PayloadIdentifier</key>
<string>com.anthropic.claudefordesktop.settings</string>
<key>PayloadUUID</key>
<string>00000000-0000-0000-0000-000000000000</string>
<key>PayloadVersion</key>
<integer>1</integer>

<!-- Boolean key — works fine -->
<key>disableAutoUpdates</key>
<true/>

<!-- String key — works fine -->
<key>inferenceProvider</key>
<string>gateway</string>
<key>inferenceGatewayBaseUrl</key>
<string>https://gateway.example.com</string>
<key>inferenceGatewayApiKey</key>
<string>sk-REDACTED</string>

<!-- ⚠️ ARRAY keys — these get serialized as <string> by mobileconfig writers, reader rejects -->
<key>inferenceModels</key>
<string>[{"name":"some-model"}]</string>

<key>managedMcpServers</key>
<string>[{"name":"Test MCP","url":"https://mcp.example.com/mcp","oauth":true,"transport":"http"}]</string>

</dict>
</array>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadIdentifier</key>
<string>com.anthropic.claudefordesktop.profile</string>
<key>PayloadUUID</key>
<string>00000000-0000-0000-0000-000000000001</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadScope</key>
<string>User</string>
</dict>
</plist>
```

  1. Install the profile (via MDM, or for repro: sudo profiles install -path=test.mobileconfig).
  2. Confirm it's active: profiles list shows com.anthropic.claudefordesktop.profile.
  3. Confirm the underlying plist was written with the array values as JSON-encoded strings:

``bash
plutil -p "/Library/Managed Preferences/com.anthropic.claudefordesktop.plist"
``

Expected output (truncated, showing the type mismatch):
``
{
"disableAutoUpdates" => 1 ← native boolean ✅
"inferenceProvider" => "gateway" ← native string ✅
"managedMcpServers" => "[{\"name\":\"Test MCP\",...}]" ← string-of-JSON, NOT array ❌
"inferenceModels" => "[{\"name\":\"some-model\"}]" ← string-of-JSON, NOT array ❌
}
``

  1. Launch Claude.app, open Settings → Connectors & extensions.
  2. Observed: panel shows "None configured" for managed MCP servers. Model picker is empty / falls back to defaults.
  3. Expected: managed MCP servers visible and ready to connect; managed model list populated.
  4. Tail the log to see the silent rejection:

``bash
tail -f ~/Library/Logs/Claude/main.log | grep -E "managedMcpServers|inferenceModels|is not a boolean"
``

Reproducible output:
``
Key 'managedMcpServers' exists but is not a boolean
Key 'inferenceModels' exists but is not a boolean
``

---

Why this happens (root cause)

Apple's .mobileconfig Configuration Profile format does support <array> natively, but most profile-generation tools (Jamf Pro UI, Intune custom-config, ProfileCreator, even Apple Configurator's manual entry) serialize JSON-shaped values as <string> because the property-list editor treats unknown nested types as opaque strings.

The CFPreferences-based reader in macOS apps then needs to either:

(a) accept both shapes (native array + JSON-encoded string), parsing JSON-strings on read, OR
(b) document that array keys MUST be authored as native <array> plist elements and provide a profile-generator tool

Claude.app currently does neither — it expects native arrays but receives strings, and the type-coercion attempt logs as "is not a boolean" (which is misleading: the reader appears to be checking-as-boolean as a generic type-check fallback, then giving up).

---

Workaround (what enterprises are doing today)

Drop the array keys (managedMcpServers, inferenceModels) from the .mobileconfig entirely. Instead, deploy a per-user JSON config at:

~/Library/Application Support/Claude-3p/configLibrary/<UUID>.json
~/Library/Application Support/Claude-3p/configLibrary/_meta.json
~/Library/Application Support/Claude-3p/claude_desktop_config.json

— with a deploymentMode: "3p" toggle. This bypasses the managed-prefs path entirely, but loses the MDM-managed assurances (audit trail, central rotation, etc.) and requires a custom Jamf/Intune script per fleet.

This works but is significantly more operational overhead than a clean profile-based deploy.

---

Environment Info

  • Platform: macOS (darwin), tested on Sonoma + Sequoia
  • Claude Cowork version: v1.4758
  • MDM: Jamf Pro 11.x (also reproduces with manual profiles install)
  • Profile install method: standard .mobileconfig via profiles install and via Jamf Pro Configuration Profile payload

---

Impact

  • Any enterprise deploying Claude Cowork via standard MDM tooling cannot pre-configure managed MCP servers or restrict the model picker through profile-based config alone.
  • The misleading error message (is not a boolean for what is clearly a string-vs-array mismatch) makes self-diagnosis very hard — most admins would not search for that text and find this bug.
  • Forces enterprises onto the per-user JSON workaround, which trades MDM auditability for a custom Jamf script.

---

Suggested fix priorities (in order of decreasing impact / increasing effort)

  1. Improve the error message (~1 hour): change "is not a boolean" to "expected array, got string (managed-config writers may serialize arrays as JSON strings — see [docs link])". Fixes diagnosability without changing semantics.
  1. Accept JSON-string fallback for array keys ⭐ (~1 day): if CFPreferencesCopyAppValue for an array-typed key returns a String, attempt JSONDecoder().decode(...) before erroring. Wire-compatible with native arrays — accepts both shapes:

```swift
// Today (rejects JSON-encoded strings, hits the misleading "not a boolean" log)
guard let servers = CFPreferencesCopyAppValue(
"managedMcpServers" as CFString, kBundleID
) as? [[String: Any]] else {
log("Key 'managedMcpServers' exists but is not a boolean")
return []
}
return decode(servers)

// Proposed (accepts both shapes, logs accurate errors)
let raw = CFPreferencesCopyAppValue("managedMcpServers" as CFString, kBundleID)

// Path 1 — native plist array (canonical, what the docs would say to author)
if let nativeArray = raw as? [[String: Any]] {
return decode(nativeArray)
}

// Path 2 — JSON-encoded string (what mobileconfig/Jamf/Intune writers actually emit)
if let jsonString = raw as? String,
let data = jsonString.data(using: .utf8),
let decoded = try? JSONDecoder().decode([ManagedMcpServer].self, from: data) {
return decoded
}

// Accurate error if neither shape worked
if raw != nil {
log("Key 'managedMcpServers': expected array or JSON-encoded string, got \(type(of: raw!))")
}
return []
```

Same shape applies to inferenceModels. The fallback path is a pure superset — anyone authoring native <array> plists keeps working, and the 99% of MDM-deployed profiles (which serialize as <string>) start working too.

  1. Document the array-key authoring convention (~few hours): publish a sample .mobileconfig with <array> (not <string>) for these keys, so admins who can hand-author plist arrays know the canonical shape. (Note: this alone doesn't help — most MDM tooling can't emit native arrays through their UI, which is why fix #2 is the high-leverage one.)

Path #2 has the highest leverage — it makes Claude Cowork tolerant of the de-facto MDM ecosystem without requiring every admin to hand-author plist arrays. The pseudocode above is ~15 lines of net-new code, fully backward-compatible.

---

Additional context / files

I have a full sanitized export I can share if helpful: the actual .mobileconfig file as written by Jamf Pro (showing the array-as-string serialization), the resulting /Library/Managed Preferences/com.anthropic.claudefordesktop.plist, and main.log excerpts. Happy to attach as a follow-up comment or share via a private channel if there's a path for that.

Filed by: an enterprise team rolling out Claude Cowork with a custom MCP backend. Hit this on day one of the rollout; lost several hours diagnosing it before the "is not a boolean" log message led to the type-mismatch root cause.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗