Skip to content

Use bare ${CLAUDE_PLUGIN_ROOT} so the server launches from the plugin directory#5

Merged
walkerhughes merged 1 commit into
mainfrom
fix/plugin-root-expansion
Jul 25, 2026
Merged

Use bare ${CLAUDE_PLUGIN_ROOT} so the server launches from the plugin directory#5
walkerhughes merged 1 commit into
mainfrom
fix/plugin-root-expansion

Conversation

@walkerhughes

Copy link
Copy Markdown
Owner

The actual cause of the -32000 that has blocked this plugin since #2. Both of my earlier diagnoses were wrong, and the MCP server log settles it.

Evidence

The log lives at ~/Library/Caches/claude-cli-nodejs/<project>/mcp-logs-plugin-harbor-mcp-harbor-hub/. Under the original command: "uv" config:

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

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

Same bug both times. ${CLAUDE_PLUGIN_ROOT:-.} expanded to ., never to the plugin directory, so the server was launched against whatever project the user happened to be in.

Why: the :-default form is handled by environment-variable expansion, which does not know CLAUDE_PLUGIN_ROOT. So the default always won. Only the bare ${CLAUDE_PLUGIN_ROOT} is substituted by the plugin loader.

Corrections to the earlier PRs

Claim Reality
#3: "uv is not on the spawn PATH" Wrong. Failed to spawn: harbor-mcp is uv's own error message, so uv was found and running. It had resolved the wrong project.
Two sessions: "cold-start venv timeout" Wrong. The log shows a 30000ms timeout with failures at 10-40ms, and the venv exists.

The launcher script from #3 is still worth keeping (it drops a noisy VIRTUAL_ENV, gives a real error when uv is genuinely absent, and self-locates its root), but it was not the fix.

Changes

  • .mcp.json uses the bare ${CLAUDE_PLUGIN_ROOT}.
  • New 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 three tests. This class of breakage is invisible to every other test, because it only surfaces at MCP connect time.
  • Version 0.2.0 to 0.3.0, so installed copies actually receive it.
  • README documents that .mcp.json is plugin-only, that local development runs ./scripts/start-server.sh directly, and why a :-default must never be added back to make local discovery work.

Verification

72 unit tests pass (69 + 3 new), ruff clean
guard with the fix in place        -> 3 passed
guard with `:-.` reintroduced      -> 2 failed (correctly blocks)

Connect behavior itself can only be confirmed by installing 0.3.0, since it depends on the plugin loader's substitution.

…lugin dir

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.
@walkerhughes
walkerhughes merged commit d7b42e8 into main Jul 25, 2026
4 checks passed
@walkerhughes
walkerhughes deleted the fix/plugin-root-expansion branch July 25, 2026 07:07
walkerhughes added a commit that referenced this pull request Jul 25, 2026
…-mcp (#9)

Credentials now resolve from ~/.tastytrade-mcp/credentials.json, mirroring how
the harbor CLI stores its key, so the server no longer depends on a .env in the
client's working directory.

Credentials (src/credentials.py):
- Precedence is whole-source, not per-field: the environment when all three of
  TT_CLIENT_ID/TT_SECRET/TT_REFRESH are set, otherwise the file. Mixing a file
  client_id with an environment refresh_token would fail auth in a way that
  looks like a revoked token rather than a misconfiguration.
- A partially set environment is a hard error rather than a silent fallthrough
  to the file, because that fallthrough is invisible and the resulting 401 is
  misleading. This session already lost time to exactly that shape of bug.
- Refuses a file readable by group or others, with a chmod remediation. The
  secret and refresh token together grant full account access, including order
  placement when TT_ENABLE_TRADING is true.
- client.py previously did os.environ["TT_CLIENT_ID"], so a missing credential
  raised a bare KeyError. Tests that pass all three explicitly still skip
  resolution entirely, so the mock-API suite needs no credentials.

Plugin:
- .claude-plugin/plugin.json, marketplace entry, and scripts/start-server.sh
  reusing the launcher proven in #5: uv discovery across common install
  locations, VIRTUAL_ENV dropped, plugin root resolved from the script's own
  location, and a bare ${CLAUDE_PLUGIN_ROOT} with no :- default.
- The launcher cds into the project and runs `python -m src.server` rather than
  the tastytrade-mcp console script, which is broken: pyproject flattens src/
  via sources = { "src" = "" } while every internal import is relative, so an
  installed wheel cannot import itself (attempted relative import beyond
  top-level package) and the entry point still names src.server. Pre-existing
  and tracked separately; the launcher works around it.

Error handling:
- guarded_tool had no catch-all, so its docstring promise that a tool never
  hands back a traceback was false for anything outside four exception types.
  Added CredentialsError handling and a last-resort Exception clause.
- The 401 guidance named only the environment variables. It now explains that
  resolved-but-rejected means stale rather than missing, names both sources, and
  says not to ask the user to paste a secret into the chat.

CI:
- plugin-version now discovers every */.claude-plugin/plugin.json instead of
  hardcoding harbor-mcp, so a new plugin is gated without editing it, and a
  brand-new manifest is skipped rather than failing.
- tests/unit/test_plugin_config.py mirrors harbor's guard: no :- default, the
  launcher exists and is executable, no `source .env`, no credential named in
  the plugin config, and a semver version.

Verified: ruff and mypy clean; 90 unit and 102 total tests pass at 92.91%
coverage; the 12 eval verifiers still pass. The launcher completes an MCP
handshake and lists all 12 tools from an unrelated cwd, and under
`env -i PATH=/usr/bin:/bin`, the case that produced -32000. With no credentials
anywhere, a tool call returns the structured error with suggestions rather than
a traceback.
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