Use bare ${CLAUDE_PLUGIN_ROOT} so the server launches from the plugin directory#5
Merged
Merged
Conversation
…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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The actual cause of the
-32000that 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 originalcommand: "uv"config:After #3 switched to the launcher script:
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
:-defaultform is handled by environment-variable expansion, which does not knowCLAUDE_PLUGIN_ROOT. So the default always won. Only the bare${CLAUDE_PLUGIN_ROOT}is substituted by the plugin loader.Corrections to the earlier PRs
Failed to spawn: harbor-mcpis uv's own error message, so uv was found and running. It had resolved the wrong project.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.jsonuses the bare${CLAUDE_PLUGIN_ROOT}.tests/unit/test_plugin_config.py, which fails if a:-defaultreappears onCLAUDE_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..mcp.jsonis plugin-only, that local development runs./scripts/start-server.shdirectly, and why a:-defaultmust never be added back to make local discovery work.Verification
Connect behavior itself can only be confirmed by installing 0.3.0, since it depends on the plugin loader's substitution.