Skip to content

Ship tastytrade-mcp as a plugin, with credentials in ~/.tastytrade-mcp#9

Merged
walkerhughes merged 1 commit into
mainfrom
feat/tastytrade-plugin
Jul 25, 2026
Merged

Ship tastytrade-mcp as a plugin, with credentials in ~/.tastytrade-mcp#9
walkerhughes merged 1 commit into
mainfrom
feat/tastytrade-plugin

Conversation

@walkerhughes

Copy link
Copy Markdown
Owner

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 no auth login equivalent, 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_REFRESH are set, otherwise the file. Mixing a file client_id with an environment refresh_token would 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 600 remediation. The client secret and refresh token together grant full account access, including order placement when TT_ENABLE_TRADING=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 or file.

Plugin

Manifest, 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.

A packaging bug found along the way

The launcher cds into the project and runs python -m src.server rather than the tastytrade-mcp console script, because that script does not work:

Test on the built wheel Result
import server attempted relative import with no known parent package
import tools.options attempted relative import beyond top-level package
tastytrade-mcp ModuleNotFoundError: No module named 'src'
site-packages receives a stray top-level __init__.py

sources = { "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 (move src/ to src/tastytrade_mcp/, which leaves all relative imports untouched).

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 plus a last-resort Exception clause. 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-version now discovers every */.claude-plugin/plugin.json instead 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.py mirrors harbor's guard: no :- default, launcher exists and is executable, no source .env, no credential named in the plugin config, semver version.

Verification

Check Result
ruff check / format clean
mypy no issues, 29 files
Unit tests 90 passed
With integration 102 passed, 92.91% coverage (gate 80%)
Eval verifiers 12 passed, 0 failed
claude plugin validate passes both manifests
Launcher from unrelated cwd handshake OK, 12 tools
Launcher under env -i PATH=/usr/bin:/bin handshake OK (the -32000 case)
No credentials anywhere structured error, no traceback

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.

…-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.
@walkerhughes
walkerhughes merged commit debe5a6 into main Jul 25, 2026
2 checks passed
@walkerhughes
walkerhughes deleted the feat/tastytrade-plugin branch July 25, 2026 15:26
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