Ship tastytrade-mcp as a plugin, with credentials in ~/.tastytrade-mcp#9
Merged
Conversation
…-mcp 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.
Credentials now resolve from
~/.tastytrade-mcp/credentials.json, mirroring how the harbor CLI stores its key, and tastytrade-mcp installs as a plugin from this marketplace.Credentials
New
src/credentials.py. TastyTrade has noauth loginequivalent, so the file is created by hand from a registered OAuth application; the README documents the shape and how to obtain each value.Precedence is whole-source, not per-field: the environment when all three of
TT_CLIENT_ID/TT_SECRET/TT_REFRESHare set, otherwise the file. Mixing a fileclient_idwith an environmentrefresh_tokenwould fail authentication in a way that looks like a revoked token rather than a misconfiguration.A partially set environment is a hard error, not a silent fallthrough to the file. That fallthrough is invisible and the resulting 401 is actively misleading, which is the same shape of bug that cost real debugging time earlier in this repo's history.
The file is refused if group- or world-readable, with a
chmod 600remediation. The client secret and refresh token together grant full account access, including order placement whenTT_ENABLE_TRADING=true.client.pypreviously didos.environ["TT_CLIENT_ID"], so a missing credential raised a bareKeyError. Tests that pass all three explicitly still skip resolution entirely, so the mock-API suite needs no credentials or file.Plugin
Manifest, marketplace entry, and
scripts/start-server.shreusing the launcher proven in #5: uv discovery across common install locations,VIRTUAL_ENVdropped, plugin root resolved from the script's own location, and a bare${CLAUDE_PLUGIN_ROOT}with no:-default.A packaging bug found along the way
The launcher
cds into the project and runspython -m src.serverrather than thetastytrade-mcpconsole script, because that script does not work:import serverattempted relative import with no known parent packageimport tools.optionsattempted relative import beyond top-level packagetastytrade-mcpModuleNotFoundError: No module named 'src'__init__.pysources = { "src" = "" }flattens the package while every internal import is relative, so an installed wheel cannot import itself. This is pre-existing and does not block the plugin; the launcher sets cwd explicitly, so the invocation is deterministic. Worth its own PR (movesrc/tosrc/tastytrade_mcp/, which leaves all relative imports untouched).Error handling
guarded_toolhad no catch-all, so its docstring promise that a tool never hands back a traceback was false for anything outside four exception types. AddedCredentialsErrorhandling plus a last-resortExceptionclause. With no credentials anywhere, a tool call now returns:{ "error": "No Tastytrade credentials found. Create /.../.tastytrade-mcp/credentials.json containing {\"client_id\": \"...\", \"client_secret\": \"...\", \"refresh_token\": \"...\"} with mode 600, or set TT_CLIENT_ID, TT_SECRET and TT_REFRESH.", "suggestions": ["See the tastytrade-mcp README for how to obtain each value."] }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-versionnow discovers every*/.claude-plugin/plugin.jsoninstead of hardcoding harbor-mcp, so a future plugin is gated without editing the workflow, and a brand-new manifest is skipped rather than failing. Verified locally against this branch: harbor skips (unchanged), tastytrade skips (new).tests/unit/test_plugin_config.pymirrors harbor's guard: no:-default, launcher exists and is executable, nosource .env, no credential named in the plugin config, semver version.Verification
claude plugin validateenv -i PATH=/usr/bin:/bin-32000case)The one validator warning is expected:
CLAUDE.md at the plugin root is not loaded as project context. That file is development guidance for working on this subdirectory, which is where it belongs, and it is correctly not shipped as plugin context.