diff --git a/src/macos_mcp/__main__.py b/src/macos_mcp/__main__.py index dd4a891..93e3c43 100644 --- a/src/macos_mcp/__main__.py +++ b/src/macos_mcp/__main__.py @@ -39,6 +39,7 @@ import logging import os from pathlib import Path +import plistlib import secrets import shutil import socket @@ -936,28 +937,15 @@ def _resolve_program() -> list[str]: def _build_plist(program_args: list[str]) -> str: log_out = CONFIG_DIR / "server.log" log_err = CONFIG_DIR / "server.error.log" - args_xml = "\n".join(f" {a}" for a in program_args) - return f"""\ - - - - - Label - {_AGENT_LABEL} - ProgramArguments - -{args_xml} - - RunAtLoad - - KeepAlive - - StandardOutPath - {log_out} - StandardErrorPath - {log_err} - -""" + payload = { + "Label": _AGENT_LABEL, + "ProgramArguments": program_args, + "RunAtLoad": True, + "KeepAlive": True, + "StandardOutPath": str(log_out), + "StandardErrorPath": str(log_err), + } + return plistlib.dumps(payload, fmt=plistlib.FMT_XML, sort_keys=False).decode("utf-8") def _launchctl(*args: str) -> subprocess.CompletedProcess: diff --git a/tests/test_launchd_plist.py b/tests/test_launchd_plist.py new file mode 100644 index 0000000..e7cf515 --- /dev/null +++ b/tests/test_launchd_plist.py @@ -0,0 +1,21 @@ +import plistlib + +import macos_mcp.__main__ as server + + +def test_build_plist_escapes_special_characters(mocker, tmp_path): + config_dir = tmp_path / "config & logs " + mocker.patch.object(server, "CONFIG_DIR", config_dir) + args = ["/tmp/tool & helper", "serve", "--label=", "a>b"] + + rendered = server._build_plist(args) + parsed = plistlib.loads(rendered.encode("utf-8")) + + assert parsed == { + "Label": "com.macos-mcp.server", + "ProgramArguments": args, + "RunAtLoad": True, + "KeepAlive": True, + "StandardOutPath": str(config_dir / "server.log"), + "StandardErrorPath": str(config_dir / "server.error.log"), + }