myco:claude-plugin-config — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited myco:claude-plugin-config (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 end-to-end lifecycle of the three Claude MCP plugins (unifi-network, unifi-protect, unifi-access) — from local development to production release. The plugin JSON manifests, transport asyncio patterns, setup scripts, and version sync rules are tightly coupled; changing one without understanding the others causes silent failures that are hard to diagnose.
Canonical plugin directories: plugins/unifi-network/, plugins/unifi-protect/, plugins/unifi-access/. Each contains plugin.json (gitignored — Claude Code plugin manifest), scripts/set-env.sh, and scripts/check-prereqs.sh. The shared transport lives at packages/unifi-mcp-shared/src/unifi_mcp_shared/transport.py.
Before any procedure in this domain:
uvx launches the MCP server as a subprocess — it is not PID 1. Flags that change transport binding behavior behave differently here than in a direct python invocation. This matters for every procedure below.plugin.json flags are duplicated across unifi-network, unifi-protect, and unifi-access. A bug introduced in one is almost always present in all three.Every plugins/unifi-*/plugin.json contains UNIFI_MCP_HTTP_FORCE in its environment variable section. This env var is read by the app server's config.yaml via force: ${oc.env:UNIFI_MCP_HTTP_FORCE,false} (e.g., apps/network/src/unifi_network_mcp/config/config.yaml). It controls whether the MCP server attempts to bind an HTTP transport alongside stdio.
The correct value is `"false"` in all three plugin configs.
// plugins/unifi-network/plugin.json (same pattern in unifi-protect, unifi-access)
{
"env": {
"UNIFI_MCP_HTTP_FORCE": "false"
}
}"true" causes silent tool lossSetting UNIFI_MCP_HTTP_FORCE=true causes the server to call run_http() (defined in transport.py). In a uvx-launched context the HTTP bind fails. run_http() catches the resulting SystemExit and returns normally — no exception propagates.
The old transport implementation used asyncio.wait(FIRST_COMPLETED), which saw the HTTP task complete and cancelled the still-running stdio task. The server appeared to start; all tools disappeared silently. No error was logged. This pattern has been replaced (see Procedure 2), but the root cause starts here: UNIFI_MCP_HTTP_FORCE=true in a uvx context triggers the failure chain.
# All three should show "false"
grep -r "UNIFI_MCP_HTTP_FORCE" plugins/unifi-network/plugin.json plugins/unifi-protect/plugin.json plugins/unifi-access/plugin.jsonIf any shows "true", fix it in all three and issue a plugin-only release (see Procedure 6).
File: packages/unifi-mcp-shared/src/unifi_mcp_shared/transport.py
Stdio is the primary control flow — the server's lifecycle is tied to the stdio task running to completion. HTTP runs as a cancellable background task. An HTTP failure (bind error, port conflict) does not cancel stdio.
asyncio.wait(FIRST_COMPLETED) is unsafe hereFIRST_COMPLETED cannot distinguish "HTTP finished its job" from "HTTP swallowed an error and returned." Because run_http() catches SystemExit internally and converts it to a clean return, asyncio.wait treats a bind failure as task success — then cancels stdio. All tools disappear.
The fix replaced asyncio.wait(FIRST_COMPLETED) with an explicit stdio-primary flow. The current implementation at lines 165–168 of transport.py is:
# Correct: stdio drives lifecycle; HTTP is a cancellable background task
http_task = asyncio.create_task(run_http(), name="http")
await asyncio.sleep(0) # yield so http_task starts
try:
await run_stdio() # primary — runs to completion
finally:
http_task.cancel()
...Do not revert to asyncio.wait(FIRST_COMPLETED) when one task can "succeed" by swallowing an exception.
When modifying transport.py:
run_http() to raise SystemExit; verify stdio keeps running.If adding a third transport (e.g., SSE):
await path.FIRST_COMPLETED to detect failure.--plugin-dir vs. Marketplace Cacheclaude --plugin-dir plugins/unifi-network--plugin-dir <path> loads a plugin from a local directory, bypassing marketplace publishing. It is the canonical flag for iterating on plugin.json changes.
--plugin-dir bypasses marketplace publishing, but it does not guarantee the local version takes precedence if Claude has a marketplace-installed version of the same plugin cached. Both can coexist, and the marketplace-installed config may win.
A plugin.json change that looks correct under --plugin-dir may not reflect what a marketplace-installed user experiences.
--plugin-dir and confirm the expected behavior. # macOS example — path varies by platform
find ~/Library/Application\ Support/Claude -name "plugin.json" 2>/dev/null--plugin-dir.--plugin-dir does and does not substitute for| Does | Does not |
|---|---|
| Loads local plugin.json | Replace a marketplace install test |
| Skips marketplace publish wait | Override a cached marketplace config |
| Fast iteration on manifest changes | Validate the uvx invocation path end-to-end |
check-prereqs.shplugins/unifi-*/scripts/check-prereqs.sh validates system state before plugin activation. It catches the most common silent setup failures before they produce user-facing errors:
.claude/settings.local.json before set-env.sh modifies itcom.apple.security.network.client; an entitlement-less interpreter starts the server cleanly but silently fails every controller call with errno 65Run the relevant check-prereqs.sh before:
bash plugins/unifi-network/scripts/check-prereqs.shWhen a new plugin procedure introduces a new prerequisite (new required binary, new system state check, new endpoint):
check-prereqs.sh in the relevant plugin(s).# Pattern for a new required binary
if ! command -v some-tool >/dev/null 2>&1; then
echo " [FAIL] some-tool not found on PATH" >&2
echo " Install with: brew install some-tool" >&2
errors=$((errors + 1))
fiKeep checks minimal and specific. Do not add checks for conditions that Claude or uvx will surface with their own errors.
macOS ships /bin/bash at version 3.2 (circa 2007). This is the shell that runs plugins/unifi-*/scripts/*.sh on most developer machines. Bash 4+ features are unavailable.
The most common failure:
# BROKEN on macOS /bin/bash — bash 4+ only:
declare -A my_map
my_map["key"]="value"
# Result: "unbound variable" or silent no-op — no clear error`case/esac` for lookup tables:
get_endpoint() {
case "$1" in
unifi-network) echo "https://${UNIFI_HOST}/proxy/network" ;;
unifi-protect) echo "https://${UNIFI_HOST}/proxy/protect" ;;
unifi-access) echo "https://${UNIFI_HOST}/proxy/access" ;;
*) echo "unknown" ;;
esac
}Hardcoded variable pairs when the map is small and static:
NETWORK_ENDPOINT="https://${UNIFI_HOST}/proxy/network"
PROTECT_ENDPOINT="https://${UNIFI_HOST}/proxy/protect"
ACCESS_ENDPOINT="https://${UNIFI_HOST}/proxy/access"Test every new script in plugins/ against /bin/bash explicitly:
/bin/bash plugins/unifi-network/scripts/set-env.sh
/bin/bash plugins/unifi-protect/scripts/check-prereqs.shDo not use bash from Homebrew for this test — it is 5.x and will not catch 3.2 incompatibilities even if #!/usr/bin/env bash points to it.
/bin/bash --version # 3.2.x on stock macOS
bash --version # may be 5.x if Homebrew bash is on PATHWhen reviewing any new shell script:
# Grep for common bash 4+ patterns
grep -n 'declare -A\|declare -a\|mapfile\|readarray' plugins/unifi-*/scripts/*.shZero results is the goal.
Plugin versions are strictly slaved to the MCP package release tags. The plugin.json version field must match the tagged MCP package version on PyPI.
When only plugin.json changes (no Python code changes in packages/), issue a no-op patch release:
pyproject.toml (patch increment only).monorepo-release-pipeline).plugin.json version field to match the new tag.Never skip the release. Without a version bump, users with the old plugin version have no signal that a reconfiguration is available.
Issue a no-op patch release when changing:
plugin.json (e.g., UNIFI_MCP_HTTP_FORCE)plugin.jsonplugin.jsonIt does not apply to plugins/unifi-*/scripts/ changes — those are local setup helpers, not packaged artifacts.
# 1. Make the plugin.json change across all affected plugins
# 2. Bump pyproject.toml version (patch increment, e.g., 0.4.2 → 0.4.3)
# 3. Commit both together
git add plugins/unifi-network/plugin.json packages/unifi-mcp-network/pyproject.toml
git commit -m "chore: bump plugin version to 0.4.3 (fix UNIFI_MCP_HTTP_FORCE)"
# 4. Follow monorepo-release-pipeline for tag ordering and PyPI publishThe PyPI ordering gate (shared package published before consumers) applies even for no-op releases. Re-tagging an existing release is not an option — PyPI releases are immutable.
A no-op patch release is cheap, auditable, and unambiguous. The wheel content is identical; what changes is the signal to plugin consumers that a manifest update has occurred. Re-using a tag would break reproducibility and violate PyPI immutability.
Silent tool disappearance is the common failure mode. When MCP tools vanish after a plugin change, check in order:
UNIFI_MCP_HTTP_FORCE in all three plugin.json files — must be "false"transport.py for asyncio.wait(FIRST_COMPLETED) — replace with stdio-primary pattern--plugin-dir is loading the right version vs. a cached marketplace configAll three plugins share the same flags. Fix all three when any flag value is wrong; grep all of them before closing a bug.
Bash 3.2 failures are silent. A script that fails due to declare -A on macOS may not error loudly — it silently omits the intended configuration. /bin/bash testing is non-negotiable.
No-op releases follow the full pipeline. The tag ordering and PyPI sequencing constraints from monorepo-release-pipeline apply even when the Python wheel is unchanged. Don't shortcut the release process.
macOS Python entitlement is a silent killer. A server that starts cleanly and registers all tools but returns "Not connected to controller" on every call is the entitlement symptom — check-prereqs.sh catches this before user-facing failure.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.