An MCP (Model Context Protocol) server that exposes macOS-native device trust signals — the facts about a Mac that cannot be gathered from a Linux container or a cloud runner. Every tool is strictly read-only: nothing on the device is ever mutated.
| Signal | Tool |
|---|---|
| Who is this machine? (serial, UUID, chip, activation lock) | signalgrid_device_identity |
| What OS build is it on? | signalgrid_os_info |
| Are SIP / FileVault / Gatekeeper / firewall on? | signalgrid_security_posture |
| What remote access is exposed? (SSH, Screen Sharing, SMB, ARD) | signalgrid_sharing_services |
| Is it MDM/DEP enrolled? What profiles are installed? | signalgrid_mdm_status |
| Is it patched? Are auto-updates on? | signalgrid_software_updates |
| Are XProtect / MRT malware definitions current? | signalgrid_xprotect_status |
| What got installed, and when? | signalgrid_install_history |
| DNS, proxies, VPNs, interfaces | signalgrid_network_posture |
| What's listening on the network? | signalgrid_listening_services |
| What persists across reboots? (launchd items) | signalgrid_launch_items |
| Any third-party kernel extensions? | signalgrid_kernel_extensions |
| Any stranded/conflicting system extensions? | signalgrid_system_extensions |
| Who has accounts? Who is admin? | signalgrid_local_users |
| What apps are installed, and who signed them? | signalgrid_installed_apps |
| What's running right now? | signalgrid_process_snapshot |
| Is removable storage (a data-egress channel) connected? | signalgrid_removable_media |
| Does the device auto-lock when left idle? (walk-up risk) | signalgrid_screen_lock |
| Is this app properly signed & notarized? | signalgrid_codesign_inspect |
| Everything above the fold, in one call | signalgrid_posture_report |
| The SignalGrid decision for this Mac (allow / step-up / restrict / deny) | signalgrid_trust_verdict |
The aggregate report is also exposed as an MCP resource at signalgrid://posture.
Call signalgrid_posture_report with include_verdict: true to get the raw facts
and the folded allow/step-up/restrict/deny decision in one round-trip (the same
fail-safe computation as signalgrid_trust_verdict).
- Read-only, always. Every tool carries
readOnlyHint: true,destructiveHint: false. No command mutates state. - Unknown ≠ off. Posture checks distinguish "the check ran and said X"
from "the check could not run" (missing binary, timeout, needs elevation).
enabled: nullalways means unknown — investigate, don't grade. - No shell, no injection. Every command is an argv list executed without a shell; user input is never interpolated into a command line.
- Context-efficient. Large inventories (apps, processes, launch items,
install history, listeners) are paginated (
limit/offset, standardtotal/has_more/next_offsetenvelope), filterable (name_contains), and render as a compact markdown table by default or JSON on request. - Degrades gracefully. On a non-macOS host, or when a probe needs elevation, tools return structured error/unknown text — they never crash.
SignalGrid's decision fabric classifies every signal by how it is obtained —
api (a vendor read API), native (a first-party integration), grid_collected
(SignalGrid does the lifting itself), or unavailable (a real gap). This
server is the grid_collected path for macOS: SIP, FileVault, Gatekeeper, MDM
enrollment, XProtect currency and the rest are facts no cloud API hands you
faithfully in real time — you read them on the device.
Every signal here is therefore classified grid_collected at medium
fidelity — deliberately not high. The reads are authoritative, but the fabric
never over-trusts a signal it had to collect itself, and some probes degrade to
unknown without elevation.
The server publishes this mapping so a connecting fabric can discover each signal's provenance, as the MCP resource:
signalgrid://sourcing
It lists every posture-report section → the fabric signal it feeds → its
acquisition method and fidelity. tests/test_sourcing.py pins the manifest as a
bijection with the report sections, so no section can go un-sourced and no
stale entry can linger. (The per-signal descriptions are prose, not checked
against collector output.)
Requires Python ≥ 3.10 on the Mac being assessed.
cd signalgrid-mcp
pip install -e . # or: uv pip install -e .stdio transport (the server must run on the Mac it is assessing, as a subprocess of the MCP client):
signalgrid-mcp # console script
# or
python -m signalgrid_mcp.server
# or (back-compat)
python server.py{
"mcpServers": {
"signalgrid": {
"command": "python3",
"args": ["/Users/<you>/signalgrid-mcp/server.py"]
}
}
}npx @modelcontextprotocol/inspector python3 server.pyThe server intentionally runs unelevated. Some probes therefore report
null/unknown rather than an answer:
profiles list,systemsetup, and somelaunchctl print system/...targets want root.tmutil latestbackupand the Time Machine preference need Full Disk Access.- Unelevated
lsofonly sees the current user's listeners.
This is by design: an unattended trust agent should not hold root. Treat
null as "unresolved signal" and escalate out-of-band if it matters.
One command sets up, tests, and inspects the live server end to end:
./verify.shIt creates a venv, installs, runs pytest, then inspects the server over MCP
stdio — listing and calling every tool. See RUNBOOK.md for the
step-by-step Mac verification, including how to chase down any signal that reads
unknown against real macOS output.
pip install -e ".[dev]"
pytestThe smoke tests run on any OS (they exercise the graceful-degradation paths on
Linux CI); the meaningful signal values obviously require macOS. For a Node-free
protocol inspection (no browser, no npx):
python tools/inspect_stdio.py # human-readable
python tools/inspect_stdio.py --json # machine-readable summarysignalgrid-mcp/
├── server.py # back-compat stdio entry point
├── verify.sh # one-command turnkey verify (install + test + inspect)
├── RUNBOOK.md # step-by-step Mac verification runbook
├── src/signalgrid_mcp/
│ ├── app.py # FastMCP instance + shared annotations
│ ├── runner.py # subprocess plumbing (run/text/probe/run_json)
│ ├── formatting.py # pagination, filtering, markdown/JSON rendering
│ ├── sourcing.py # grid_collected sourcing manifest (signalgrid://sourcing)
│ ├── server.py # entry point (main)
│ └── tools/ # one module per signal domain
├── tools/inspect_stdio.py # Node-free MCP inspector (protocol/read-only/honesty)
├── tests/test_smoke.py
├── tests/test_parsers.py # parser fixtures pinned against captured output
└── evaluation.xml # MCP eval suite (read-only Q&A pairs)