Skip to content

Commit bffdde1

Browse files
committed
Gate MCP tool calls from Claude Desktop Extensions
1 parent 79618bd commit bffdde1

3 files changed

Lines changed: 1133 additions & 53 deletions

File tree

control-plane/internal/api/install.go

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ func buildPlanOps(req installPlanRequest) ([]fileOp, []string, []string) {
133133
case "claude-code":
134134
ops = append(ops, claudeCodePlan(req.DaemonURL, req.ConfigDirOverride, req.AgentlockBinary, req.StatusLineScript, req.HarnessConfigDirs, req.ExistingFiles))
135135
case "claude-desktop":
136-
op, ws := claudeDesktopPlan(req.DaemonURL, req.ConfigDirOverride, req.AgentlockBinary, req.HarnessConfigDirs, req.ExistingFiles)
137-
ops = append(ops, op)
136+
desktopOps, ws := claudeDesktopPlan(req.DaemonURL, req.ConfigDirOverride, req.AgentlockBinary, req.HarnessConfigDirs, req.ExistingFiles)
137+
ops = append(ops, desktopOps...)
138138
warnings = append(warnings, ws...)
139139
case "codex":
140140
codexOps, ws := codexPlan(req.DaemonURL, req.ConfigDirOverride, req.AgentlockBinary, req.HarnessConfigDirs, req.ExistingFiles)
@@ -481,7 +481,13 @@ func installApplyHandler(d Deps) http.HandlerFunc {
481481
func harnessForPath(path string) string {
482482
dir := filepath.Base(filepath.Dir(path))
483483
switch {
484-
case strings.HasSuffix(path, "claude_desktop_config.json"):
484+
case strings.HasSuffix(path, "claude_desktop_config.json"),
485+
strings.HasSuffix(path, "extensions-installations.json"):
486+
return "claude-desktop"
487+
case strings.HasSuffix(path, "manifest.json") &&
488+
strings.Contains(path, "/Claude Extensions/"):
489+
// Bundle manifest of a Desktop Extension. Path shape:
490+
// <config-dir>/Claude Extensions/<ext-id>/manifest.json.
485491
return "claude-desktop"
486492
case strings.HasSuffix(path, "settings.json"):
487493
// Gemini also writes settings.json (in ~/.gemini); disambiguate
@@ -682,7 +688,18 @@ func installUninstallHandler(d Deps) http.HandlerFunc {
682688
case "gemini":
683689
newBytes, removed, stripErr = stripGeminiSettings(existing)
684690
case "claude-desktop":
685-
newBytes, removed, stripErr = stripClaudeDesktopConfig(existing)
691+
// Three file shapes land here:
692+
// - claude_desktop_config.json (manual mcpServers path)
693+
// - extensions-installations.json (Desktop Extensions registry)
694+
// - <Claude Extensions>/<id>/manifest.json (bundle manifests, the actual launch source)
695+
if strings.HasSuffix(e.SettingsPath, "extensions-installations.json") {
696+
newBytes, removed, stripErr = stripExtensionRegistry(existing)
697+
} else if strings.HasSuffix(e.SettingsPath, "manifest.json") &&
698+
strings.Contains(e.SettingsPath, "/Claude Extensions/") {
699+
newBytes, removed, stripErr = stripBundleManifest(existing)
700+
} else {
701+
newBytes, removed, stripErr = stripClaudeDesktopConfig(existing)
702+
}
686703
default:
687704
// Default to Claude's settings.json shape. Older manifests
688705
// without a Harness field land here, which is the right
@@ -890,26 +907,81 @@ func installUninstallHarnessesHandler(d Deps) http.HandlerFunc {
890907
}
891908
ops = append(ops, op)
892909
case "claude-desktop":
893-
p, err := claudeDesktopConfigPath(req.ConfigDirOverride, req.HarnessConfigDirs)
910+
// Strip both files Claude Desktop install touches: the
911+
// mcpServers config (claude_desktop_config.json) and
912+
// the Desktop Extensions registry
913+
// (extensions-installations.json). Each is independent
914+
// — one can be missing without affecting the other.
915+
cfgP, err := claudeDesktopConfigPath(req.ConfigDirOverride, req.HarnessConfigDirs)
894916
if err != nil {
895917
failures++
896918
ops = append(ops, uninstallOp{Op: "strip", Path: "<unresolved>", Error: err.Error()})
897-
continue
898-
}
899-
existing := []byte(req.ExistingFiles[p])
900-
newBytes, removed, stripErr := stripClaudeDesktopConfig(existing)
901-
op := uninstallOp{Op: "strip", Path: p}
902-
if stripErr != nil {
903-
failures++
904-
op.Error = stripErr.Error()
905-
log.Printf("install.uninstall_harnesses: strip claude-desktop %s: %v", p, stripErr)
906919
} else {
907-
op.EntriesRemoved = removed
908-
if removed > 0 {
909-
op.Content = string(newBytes)
920+
cfgExisting := []byte(req.ExistingFiles[cfgP])
921+
newBytes, removed, stripErr := stripClaudeDesktopConfig(cfgExisting)
922+
op := uninstallOp{Op: "strip", Path: cfgP}
923+
if stripErr != nil {
924+
failures++
925+
op.Error = stripErr.Error()
926+
log.Printf("install.uninstall_harnesses: strip claude-desktop %s: %v", cfgP, stripErr)
927+
} else {
928+
op.EntriesRemoved = removed
929+
if removed > 0 {
930+
op.Content = string(newBytes)
931+
}
932+
}
933+
ops = append(ops, op)
934+
}
935+
if regP, err := extensionsRegistryPath(req.ConfigDirOverride, req.HarnessConfigDirs); err == nil {
936+
regExisting := []byte(req.ExistingFiles[regP])
937+
if len(regExisting) > 0 {
938+
newBytes, removed, stripErr := stripExtensionRegistry(regExisting)
939+
op := uninstallOp{Op: "strip", Path: regP}
940+
if stripErr != nil {
941+
failures++
942+
op.Error = stripErr.Error()
943+
log.Printf("install.uninstall_harnesses: strip claude-desktop extensions %s: %v", regP, stripErr)
944+
} else {
945+
op.EntriesRemoved = removed
946+
if removed > 0 {
947+
op.Content = string(newBytes)
948+
}
949+
}
950+
ops = append(ops, op)
951+
}
952+
}
953+
// Strip every per-extension bundle manifest the CLI sent
954+
// us. The on-disk manifest is THE launch source for
955+
// Desktop Extensions (probed empirically) so this is
956+
// what actually un-gates the user.
957+
bundlesDir, _ := claudeDesktopExtensionsDir(req.ConfigDirOverride, req.HarnessConfigDirs)
958+
if bundlesDir != "" {
959+
absBundles := bundlesDir
960+
if a, err := filepath.Abs(bundlesDir); err == nil {
961+
absBundles = a
962+
}
963+
for path, body := range req.ExistingFiles {
964+
if !strings.HasSuffix(path, "/manifest.json") {
965+
continue
966+
}
967+
if filepath.Dir(filepath.Dir(path)) != absBundles {
968+
continue
969+
}
970+
newBytes, removed, stripErr := stripBundleManifest([]byte(body))
971+
op := uninstallOp{Op: "strip", Path: path}
972+
if stripErr != nil {
973+
failures++
974+
op.Error = stripErr.Error()
975+
log.Printf("install.uninstall_harnesses: strip claude-desktop bundle %s: %v", path, stripErr)
976+
} else {
977+
op.EntriesRemoved = removed
978+
if removed > 0 {
979+
op.Content = string(newBytes)
980+
}
981+
}
982+
ops = append(ops, op)
910983
}
911984
}
912-
ops = append(ops, op)
913985
default:
914986
if devHome == "" || !knownHarnessID(h) {
915987
// No real installer + not in dev mode → nothing to do.

0 commit comments

Comments
 (0)