Skip to content

Make harbor auth login the only auth path; snapshot the account at setup#3

Merged
walkerhughes merged 2 commits into
mainfrom
feat/login-only-auth-and-account-snapshot
Jul 25, 2026
Merged

Make harbor auth login the only auth path; snapshot the account at setup#3
walkerhughes merged 2 commits into
mainfrom
feat/login-only-auth-and-account-snapshot

Conversation

@walkerhughes

Copy link
Copy Markdown
Owner

Scopes authentication to the logged-in user and drops the .env credential path added in #2. Net 108 lines removed.

Authentication

harbor auth login mints a key scoped to the logged-in user and stores it in ~/.harbor/credentials.json, which harbor reads on its own. That made most of the previous machinery redundant.

Removed:

  • load_dotenv in server.py. Harbor already resolves credentials, so the loader duplicated work for a path we no longer want. server.py is back to 30 lines of glue.
  • tests/unit/test_server.py, which only covered that loader.
  • The HARBOR_API_KEY passthrough in .mcp.json. HARBOR_MCP_ENABLE_WRITES stays: it is a local toggle, not a credential.

HARBOR_API_KEY still works as an override because harbor itself checks it first. It is now documented as a CI and scripting path rather than an interactive one.

CI is unaffected. The test suite launches the server via sys.executable -m harbor_mcp.server and inherits the process environment, so it never goes through .mcp.json.

Setup skill rewrite

Step 1 is now harbor auth status, replacing four separate bash probes (harbor on PATH, credentials.json present, .env present, key line inside .env).

Two details worth calling out:

  • harbor auth status exits 0 whether or not the user is authenticated, so the skill branches on the output string, not the exit code. Keying off $? would report everyone as authenticated.
  • whoami is retained as the validator. harbor auth status only reads a local file, so it reports Logged in for a key that has since been revoked or expired. Status proves a credential exists; whoami proves it is live.

The skill falls back to uv run --project ${CLAUDE_PLUGIN_ROOT} harbor auth login, since harbor ships inside the plugin's own environment. Users no longer need a global harbor install to authenticate, so this removes a prerequisite rather than adding one.

Account snapshot

After auth succeeds, setup calls list_jobs once and summarizes into the conversation: total count, the few most recent, anything running or errored.

Deliberately not cached to disk. Reading a cache file costs the agent exactly one tool call, the same as calling list_jobs, so a cache saves nothing on latency or tokens while returning stale data as job statuses flip and costs accrue. The measured payload is 1412 chars for 5 rows, so on-demand is cheap. The transcript is the cache, and it is session-scoped and never misleadingly stale.

check_task_published and resolve_dataset both require an explicit org and name, so there is nothing to enumerate for the registry; the skill skips them.

Verification

Check Result
Lint ruff check and format clean
Unit suite 69 passed
harbor auth status, authenticated Logged in as walkerhughes (API key sk-harbor-mdAIchfaDmNc…)
harbor auth status, empty HOME Not authenticated. Run \harbor auth login`.` (exit 0 in both cases)
Step 1 fallback, harbor absent from PATH Resolves via the plugin venv
Plugin launch with HARBOR_API_KEY unset whoami returns key_source: "file", no key in payload
list_jobs through the plugin 20 of 55 jobs returned

Also splits the two /plugin commands in the READMEs onto separate blocks. Pasting them together lands both lines in the marketplace prompt's input field and fails with a confusing shorthand error.

…nt at setup

Authentication is now scoped to the logged-in user via `harbor auth login`,
which stores a key in ~/.harbor/credentials.json that harbor reads on its own.
This deletes the .env credential path added in the previous change.

Removed:
- load_dotenv in server.py, back to glue only. Harbor already resolves
  credentials, so the loader was duplicating work for a path we no longer want.
- tests/unit/test_server.py, which only covered that loader.
- The HARBOR_API_KEY passthrough in .mcp.json. HARBOR_MCP_ENABLE_WRITES stays:
  it is a local toggle, not a credential. Nothing in CI regresses, because the
  test suite launches the server directly and inherits the process environment
  rather than going through .mcp.json.

HARBOR_API_KEY still works as an override, because harbor itself checks it
first. That is now documented as a CI and scripting path rather than an
interactive one.

Setup skill rewrite:
- Step 1 is `harbor auth status`, which replaces four separate bash probes
  (harbor on PATH, credentials.json present, .env present, key line in .env).
  It exits 0 whether or not the user is authenticated, so the skill branches on
  the output string, not the exit code.
- Falls back to `uv run --project ${CLAUDE_PLUGIN_ROOT} harbor auth status`,
  since harbor ships inside the plugin's environment. Users no longer need a
  global harbor install to authenticate.
- `whoami` is retained as the validator: `harbor auth status` only reads a local
  file, so it reports success for a key that has since been revoked.
- New step calls list_jobs once and summarizes the account into the
  conversation, so the session starts with context. Deliberately not cached to
  disk: reading a cache costs the same one tool call as calling list_jobs, and
  job status and cost change underneath it.

Net 108 lines removed.

Verified: ruff clean, 69 unit tests pass. `harbor auth status` exercised in both
branches, including with harbor absent from PATH and with an empty HOME. With
HARBOR_API_KEY unset, the plugin launch command authenticates from
credentials.json (whoami reports key_source "file", no key in the payload) and
list_jobs returns 20 of 55 jobs.
@walkerhughes
walkerhughes force-pushed the feat/login-only-auth-and-account-snapshot branch from cf44a0a to 8f20714 Compare July 25, 2026 06:18
Fixes the plugin failing to connect with an opaque JSON-RPC -32000.

Root cause: .mcp.json named "uv" as the command, which assumes uv is on
whatever PATH the MCP client spawns the server with. It often is not. This
machine's uv is a Homebrew install at /opt/homebrew/bin/uv, and that directory
is absent from the minimal PATH some launch contexts provide, so the spawn
failed with "env: uv: No such file or directory" before the server ever ran.

Reproduced directly: launching the installed plugin under
`env -i PATH=/usr/bin:/bin` fails, while the same command with the full PATH
succeeds. This is also why warming the plugin's venv did not help; the venv was
never the problem, and cold start measures ~4.8s including a full handshake.

scripts/start-server.sh now:
- resolves the plugin root from its own location rather than
  CLAUDE_PLUGIN_ROOT, so it works however it is launched
- searches PATH, then /opt/homebrew/bin, /usr/local/bin, ~/.local/bin and
  ~/.cargo/bin, covering Homebrew on both architectures, the standalone
  installer and cargo install
- exits 127 with an actionable message on stderr when uv is genuinely missing,
  instead of surfacing as -32000 with no explanation
- keeps stdout clean, since that is the JSON-RPC channel

Also drops the .mcp.json env block entirely. HARBOR_MCP_ENABLE_WRITES is
inherited from the environment like any other variable, and removing the block
means nothing in the config can interfere with PATH inheritance.

Verified: under `env -i PATH=/usr/bin:/bin`, the previously failing case, the
server now completes initialize and lists all 15 tools. With every uv candidate
pointed at a nonexistent path, it exits 127, writes 0 bytes to stdout, and
prints the remediation to stderr. The local dev path (cwd=harbor-mcp,
CLAUDE_PLUGIN_ROOT unset) still starts. Ruff clean, 69 unit tests pass.
@walkerhughes
walkerhughes merged commit 0637d00 into main Jul 25, 2026
3 checks passed
walkerhughes added a commit that referenced this pull request Jul 25, 2026
… fix (#4)

* fix: drop inherited VIRTUAL_ENV in the launcher

uv already ignores a VIRTUAL_ENV that does not match the project, but it warns
loudly on stderr. That warning reads like a cause of failure and already sent
one debugging session chasing it instead of the real PATH problem.

Verified: with a foreign VIRTUAL_ENV set, the warning count on stderr is now 0
and the server still lists all 15 tools. The minimal-PATH case that produced
-32000 still starts.

* fix: bump plugin version to 0.2.0 so installed copies pick up the launcher

The launcher fix in #3 never reached anyone. Claude Code extracts an installed
plugin to a cache path keyed by the manifest version
(~/.claude/plugins/cache/<marketplace>/<plugin>/<version>), and #3 left that at
0.1.0. The marketplace git clone advanced to 0637d00 and carries the launcher,
but the extracted copy under cache/.../harbor-mcp/0.1.0 still has
"command": "uv" and no scripts/ directory, so the -32000 persisted through
marketplace updates and reinstalls alike.

Confirmed on a live install: installed_plugins.json still records version 0.1.0
with gitCommitSha 2c1dfc2, the commit from #2.

- Bumps the version to 0.2.0, which changes the cache key and forces a fresh
  extraction.
- Recovers the VIRTUAL_ENV fix that was pushed to #3's branch after it merged
  and was therefore orphaned.
- Adds a plugin-version workflow that fails a PR touching shipped plugin files
  without a version bump. Tests and evals are excluded, since they are not
  copied into the plugin cache and cannot strand a user. Verified both ways: it
  passes with the bump and blocks when the version is left unchanged.
- Documents the requirement in the root README.
walkerhughes added a commit that referenced this pull request Jul 25, 2026
…lugin dir (#5)

This is the actual cause of the -32000 that has blocked the plugin since #2.
Both earlier diagnoses were wrong.

The MCP server log tells it plainly. Under the original `command: "uv"` config:

  Server stderr: error: Failed to spawn: `harbor-mcp`
  Caused by: No such file or directory (os error 2)

and after #3 switched to the launcher script:

  Server stderr: bash: ./scripts/start-server.sh: No such file or directory
  Connection failed after 10ms: MCP error -32000: Connection closed

Both are the same bug. `${CLAUDE_PLUGIN_ROOT:-.}` expanded to `.`, never to the
plugin directory, so the server was launched against whatever project the user
was in. The `:-default` form is handled by environment-variable expansion, which
does not know CLAUDE_PLUGIN_ROOT, so the default always won. Only the bare form
is substituted by the plugin loader.

What the earlier diagnoses got wrong:
- Not a uv PATH problem. "Failed to spawn: harbor-mcp" is uv's own error, so uv
  was found and running; it had simply resolved the wrong project.
- Not a cold-start timeout. The log shows a 30000ms timeout and failures at
  10-40ms, and the venv is present.

Adds tests/unit/test_plugin_config.py, which fails if a :-default reappears on
CLAUDE_PLUGIN_ROOT or if the referenced launcher is missing or non-executable.
Verified it catches the real bug: reintroducing `:-.` fails two of its tests.
This class of breakage is invisible to every other test, since it only surfaces
at MCP connect time as an opaque -32000 in
~/Library/Caches/claude-cli-nodejs/<project>/mcp-logs-plugin-harbor-mcp-harbor-hub/.

Bumps to 0.3.0 so installed copies actually receive it. Local development now
runs ./scripts/start-server.sh directly, since .mcp.json is plugin-only; the
README says so and explains why a :-default must not be added back.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant