"""RFC 7386 JSON merge patch — the one merge rule the overlay uses.
Why merge patch rather than a bespoke merger:
- an absent field means "no opinion" — the local value stays (the whole
point of the overlay);
- ``null`` means "drop the override", so a field can be handed back to the
local config without deleting anything from the local files;
- objects merge recursively, which is what per-profile and per-MCP-server
overlays need;
- arrays are replaced wholesale — "add one tool" spells out the list, and
nothing silently reorders a toolset.
The function never mutates its arguments: callers keep the local base intact
and can re-merge at any time (a refresh must not stack overlay on overlay).
"""
from __future__ import annotations
import copy
def merge_patch(target: object, patch: object) -> object:
"""Return *patch* applied over *target*, per RFC 7386."""
if not isinstance(patch, dict):
return copy.deepcopy(patch)
result: dict = copy.deepcopy(target) if isinstance(target, dict) else {}
for key, value in patch.items():
if value is None:
result.pop(key, None)
else:
result[key] = merge_patch(result.get(key), value)
return result