myco:claude-plugin-config-transport — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited myco:claude-plugin-config-transport (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill covers the full lifecycle of Claude MCP plugin integration in unifi-mcp: from configuring transport flags correctly, through understanding the asyncio transport architecture that keeps MCP tools available, to verifying plugin setup, writing portable shell scripts, and publishing plugin-only releases. The procedures share a common prerequisite — understanding the uvx launch context — because a wrong flag can trigger a transport race that silently drops all MCP tools with no visible error.
Relevant files:
apps/network/src/unifi_network_mcp/config/config.yamlapps/protect/src/unifi_protect_mcp/config/config.yamlapps/access/src/unifi_access_mcp/config/config.yamlplugins/unifi-network/scripts/set-env.sh, plugins/unifi-network/scripts/check-prereqs.shplugins/unifi-protect/scripts/set-env.sh, plugins/unifi-protect/scripts/check-prereqs.shplugins/unifi-access/scripts/set-env.sh, plugins/unifi-access/scripts/check-prereqs.shpackages/unifi-mcp-shared/src/unifi_mcp_shared/transport.pypackages/unifi-mcp-shared/src/unifi_mcp_shared/strict_dispatch.py.agents/plugins/marketplace.json (multi-target plugin registry: Claude, Codex, OpenClaw).codex-plugin/ (Codex-specific manifest directory, PR #246).agents/plugins/openclaw/ (OpenClaw-specific manifest directory, PR #248)Before modifying any plugin configuration or transport code:
uvx, the plugin process is not PID 1. This distinction controls which transport path activates inside transport.py and whether HTTP transport will be enabled. The PID check is in resolve_http_config() in transport.py.apps/network/, apps/protect/, and apps/access/ in parallel. Partial application leaves inconsistent behavior.plugins/unifi-network/, plugins/unifi-protect/, and plugins/unifi-access/. Applying to only one or two creates inconsistent behavior that is nearly impossible for users to diagnose.UNIFI_MCP_HTTP_FORCE is an environment variable consumed by each app's config.yaml. It maps to the http.force config key:
# apps/network/src/unifi_network_mcp/config/config.yaml (same pattern in protect, access)
http:
enabled: ${oc.env:UNIFI_MCP_HTTP_ENABLED,true}
force: ${oc.env:UNIFI_MCP_HTTP_FORCE,false}
transport: ${oc.env:UNIFI_MCP_HTTP_TRANSPORT,streamable-http}The safe default is `false` (or unset). Never set UNIFI_MCP_HTTP_FORCE=true in a uvx-launched context (non-PID-1). Setting it to true forces an HTTP bind attempt that is unavailable in the uvx sandbox.
UNIFI_MCP_HTTP_FORCE=true silently destroys all MCP toolsThe failure chain when UNIFI_MCP_HTTP_FORCE=true in a uvx-launched (non-PID-1) context:
run_http() in transport.py catches the SystemExit from the failed bind and returns normally — no exception propagates.asyncio.wait(FIRST_COMPLETED) implementation saw the HTTP task as "done" and interpreted normal return as success.This was the root cause of Issue #200, fixed in PR #202. The fix required changes in two places: the config flag handling AND the asyncio transport pattern (see Procedure B).
apps/network/, apps/protect/, apps/access/ config.yaml filesUNIFI_MCP_HTTP_FORCE value is false (or the env var is unset) for all uvx-launched contextstrue in the plugin setup pathtransport.pyThe current implementation in packages/unifi-mcp-shared/src/unifi_mcp_shared/transport.py uses stdio as the primary control flow — it runs to completion. HTTP runs as a cancellable background task. An HTTP failure must never cancel stdio. This pattern extends to all transport targets including Codex (PR #246) and OpenClaw (PR #248).
# SAFE — current implementation (run_transports function)
http_task = asyncio.create_task(run_http(), name="http")
await asyncio.sleep(0) # yield so http_task can start
try:
await run_stdio() # blocks until stdio EOF / client disconnect
logger.info("FastMCP stdio server exited.")
finally:
if not http_task.done():
http_task.cancel()
try:
await http_task
except asyncio.CancelledError:
passrun_http() catches its own SystemExit internally (port-bind failures from uvicorn), so a failed HTTP transport returns normally rather than propagating. Stdio lifecycle is unaffected. This design assumes the transport target (Claude, Codex, OpenClaw) uses stdio as the primary transport and HTTP as optional.
Do not replace the above pattern with asyncio.wait(FIRST_COMPLETED):
# UNSAFE — do not reintroduce this pattern
done, pending = await asyncio.wait(
{stdio_task, http_task},
return_when=asyncio.FIRST_COMPLETED,
)
for task in pending:
task.cancel() # cancels stdio if http_task "succeeds" silentlyasyncio.wait(FIRST_COMPLETED) is unsafe when any task can "succeed" by swallowing its own error. A task that catches an exception internally and returns None looks identical to a task that completed successfully. This was the pre-#202 implementation — it is described in the docstring of run_transports() as "the previous implementation."
Design rules for transport code:
FIRST_COMPLETED across transport tasks where either task can swallow errors.Two-layer verification after any transport change:
run_http() to catch and swallow a SystemExit, then asserts that the stdio task was not cancelled and ran to completion.claude --plugin-dir and the Marketplace Cacheclaude --plugin-dir <path/to/plugins/unifi-network>This loads the plugin directly from the local directory, bypassing the marketplace install step. It is the correct flag for iterating on plugin scripts or skills without publishing a release.
--plugin-dir does not deactivate a marketplace-installed version of the same plugin. If a marketplace-installed version has a conflicting config cached, it may take precedence over the local-dir version, or the two configs may interfere with each other in unpredictable ways.
To confirm the correct plugin is active:
# 1. Check which binary Claude is actually running
ps aux | grep unifi-mcp
# 2. Diff local scripts against the marketplace-installed versions
ls plugins/unifi-network/scripts/
# Compare against marketplace cache (location varies by OS — check Claude's
# plugin data directory)
# 3. If behavior doesn't match local config, temporarily uninstall the
# marketplace version and re-test with --plugin-dir alone--plugin-dir does NOT substitute forcheck-prereqs.sh pre-flight (Procedure D).check-prereqs.sh Pre-flightplugins/unifi-*/scripts/check-prereqs.sh validates required environment variables and system state before plugin activation. It catches missing credentials and misconfigured endpoints before they produce cryptic user-facing failures. The scripts explicitly state Bash 3.2 compatibility. OpenClaw plugins have identical prerequisites to Claude and Codex — the check-prereqs.sh scripts are target-agnostic.
Run before activating any plugin setup change:
bash plugins/unifi-network/scripts/check-prereqs.sh
bash plugins/unifi-protect/scripts/check-prereqs.sh
bash plugins/unifi-access/scripts/check-prereqs.shOn Windows, each plugin's scripts/ directory also provides a PowerShell prereq companion script. Consult the plugin's runtime SKILL.md manifest for the exact invocation — it accepts a -Target <claude|codex|openclaw> parameter and -PluginName flag.
Run each plugin separately — the three plugins can have different prerequisite sets. A failure in unifi-protect does not imply a failure in unifi-network.
When a plugin gains a new required environment variable or system dependency, add a check to both the .sh and the PowerShell companion script in the same PR:
# Pattern for a required variable with a helpful error message
if [ -z "${NEW_VAR:-}" ]; then
echo "ERROR: NEW_VAR is not set. See README for setup instructions." >&2
exit 1
fi
# Pattern for a required command
if ! command -v my-tool >/dev/null 2>&1; then
echo "ERROR: my-tool is not installed." >&2
exit 1
fiDo not rely on the plugin process itself to surface missing prerequisites — process-level errors are much harder to diagnose than a clean pre-flight failure with a human-readable message.
macOS ships /bin/bash at version 3.2 (circa 2007). Homebrew may install a newer bash, but users run plugin scripts without Homebrew in their PATH. Any .sh script in plugins/ must work against the system shell. This constraint is documented in each check-prereqs.sh: "Bash 3.2 compatible (works on stock macOS /bin/bash)."
On Windows, use PowerShell (`.ps1`) — not `.sh`. Each plugin's scripts/ directory provides PowerShell equivalents for environment setup and prereq checking. Never add .sh scripts that assume a bash version higher than 3.2 and expect them to work on macOS.
`declare -A` (associative arrays) is bash 4+ only. Using it in any plugins/*/scripts/*.sh causes a silent no-op or unbound variable error on macOS. The set-env.sh scripts use sed-based manipulation instead of associative arrays for exactly this reason (fixed in PR #202).
If a new script needs key→value mapping:
# Option 1 — case/esac dispatch
get_default() {
case "$1" in
FOO_HOST) echo "192.168.1.1" ;;
FOO_PORT) echo "443" ;;
*) echo "" ;;
esac
}
FOO_HOST="${FOO_HOST:-$(get_default FOO_HOST)}"
# Option 2 — explicit variable pairs (simplest for small sets)
FOO_HOST="${FOO_HOST:-192.168.1.1}"
FOO_PORT="${FOO_PORT:-443}"Every new shell script added to plugins/ must be tested against the system bash, not Homebrew bash:
# Syntax check
/bin/bash -n plugins/unifi-network/scripts/my-new-script.sh
# Runtime check
/bin/bash plugins/unifi-network/scripts/my-new-script.shIf CI runs on Linux, add a macOS job or test locally — Linux bash is almost always 4+, so CI will not catch 3.2 incompatibilities.
Plugin versions are strictly slaved to MCP package release tags. The version referenced in plugin config must match a tagged MCP package version published to PyPI. There is no independent plugin version number.
As of PR #246 (Codex) and PR #248 (OpenClaw), unifi-mcp supports three plugin targets: Claude, Codex, and OpenClaw. The multi-target registry lives in .agents/plugins/marketplace.json. When a version bump is released, all target registries in .agents/plugins/marketplace.json must be updated in the same commit. Codex-specific manifests are in .codex-plugin/ and OpenClaw-specific manifests are in .agents/plugins/openclaw/ — these must be synchronized with the main app configs and the root marketplace.json entry.
If a PR modifies only plugin manifests or scripts (no Python code changes in shared/), the correct release mechanism is a no-op patch release:
packages/unifi-mcp-shared/pyproject.toml..agents/plugins/marketplace.json — all three target sections (claude, codex, openclaw) for all three plugins (network, protect, access).codex-plugin/plugin.json — version field in the Codex-specific manifest.agents/plugins/openclaw/plugin.json — version field in the OpenClaw-specific manifestapps/network/src/unifi_network_mcp/config/config.yaml — version stringapps/protect/src/unifi_protect_mcp/config/config.yaml — version stringapps/access/src/unifi_access_mcp/config/config.yaml — version stringbump-plugin-versions.yml CI workflow should atomically cover all three locations in a single workflow commit — do not bump them separately.monorepo-release-pipeline skill).Never skip the release for plugin-only changes. Without a new tag, the plugin version field cannot advance, the config change has no anchored release identity, and existing users stay pinned to the cached old config until a tagged release forces cache invalidation.
| Change type | Release needed? | Release type |
|---|---|---|
Python code change in shared/ | Yes | Functional patch / minor / major |
| HTTP transport config change only | Yes | No-op patch |
check-prereqs.sh or set-env.sh change | Yes | No-op patch |
PowerShell plugin script (.ps1) change | Yes | No-op patch |
| Plugin skill SKILL.md change | Yes | No-op patch |
Codex manifest or .codex-plugin/ change | Yes | No-op patch |
OpenClaw manifest or .agents/plugins/openclaw/ change | Yes | No-op patch |
| Multi-target registry (.agents/plugins/marketplace.json) change | Yes | No-op patch |
| README / docs only | No | — |
When all MCP tools disappear after a plugin or transport change, the natural instinct is to check credentials or network connectivity. Check `UNIFI_MCP_HTTP_FORCE` (is it set to `true` somewhere?) and the transport pattern in `transport.py` first. The stdio cancellation failure surfaces no error to Claude or the user — tools simply stop appearing in the tool list.
The Issue #200 / PR #202 fix required changes in two places:
apps/*/config/config.yaml — ensure UNIFI_MCP_HTTP_FORCE is not forced to true in non-PID-1 contextstransport.py — the stdio-primary pattern prevents a swallowed HTTP failure from cancelling stdioIf you are investigating a transport issue, always audit both the config flag injection path and the asyncio pattern before concluding the fix is complete.
Config changes to HTTP transport behavior must be applied to all three apps (network, protect, access). Script changes must be applied to all three plugin directories (unifi-network, unifi-protect, unifi-access). Partial application produces inconsistent behavior depending on which app/plugin the user has installed.
--plugin-dir local tests are necessary but not sufficientA passing local test with --plugin-dir does not guarantee the marketplace-installed version behaves the same way. Always do a final marketplace-path test before shipping, especially after config or script changes.
The three plugin targets (Claude, Codex, and OpenClaw) have identical transport initialization patterns. Changes to transport.py apply to all three; transport-specific gotchas that were previously labeled as Codex-only (PR #246) now apply equally to OpenClaw (PR #248). When debugging transport issues, test against all three runtime targets if possible — a transport bug may surface in one target before appearing in others due to timing or initialization order differences.
The .agents/plugins/marketplace.json root registry, .codex-plugin/plugin.json, and .agents/plugins/openclaw/plugin.json must have matching version strings. If a release bumps only .agents/plugins/marketplace.json but forgets the target-specific manifests, the Codex and OpenClaw plugins will keep referencing the old version while the marketplace registry has advanced. This creates version skew that manifests as "tool unavailability for OpenClaw users while Codex works fine" — a confusing symptom that points to version mismatch, not transport issues. Always update all three registry locations atomically.
The .agents/skills/*/ directory structure can collide with plugin name patterns if not carefully scoped. Plugin skill manifests registered in .agents/plugins/*/skills/*/ must use fully qualified names (e.g., unifi-mcp:skill-name) to avoid colliding with agent-owned skills. Always verify that a new plugin skill manifest's fully qualified name does not collide with existing agent or plugin skill names. Test on all three targets (Claude, Codex, OpenClaw) — skill resolution may differ by target.
***REDACTED*** markers into mutation args (PR #350)As of PR #350, all plugin tool responses redact sensitive fields by default — Wi-Fi passphrases, VPN private/preshared keys, SNMP community strings, and Access credential token/PIN values are replaced with ***REDACTED*** in read, list, and preview responses.
When you need an unredacted value for a write operation: Pass include_sensitive=true to the relevant read tool first to retrieve the real value, then use that value in the mutation call.
Never feed a `REDACTED` string back into a mutation argument. StrictKwargFastMCP (in packages/unifi-mcp-shared/src/unifi_mcp_shared/strict_dispatch.py) rejects tools/call requests that pass a redaction marker as an argument value. The rejection may not surface a clear diagnostic to the agent — the tool call fails without indicating that a redacted placeholder was the cause. This guard prevents accidental passthrough of masked values into real API writes.
Affected workflows: WLAN passphrase updates, VPN key rotation, SNMP community string changes, Access credential token/PIN mutations. Plugin skills that guide agents through these workflows must explicitly mention the include_sensitive=true pattern — agents that rely on a prior read response and pass the field value through verbatim will hit this guard.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.