v0.7.1 — v0.7.0 catch-up + workflow idempotency fix
🎯 In-band MCP setup — no log-spelunking, no restart
v0.7.1 is the catch-up release that ships v0.7.0's work to PyPI. The
v0.7.0 Pre-release attempt was blocked by two GitHub Actions workflow
quirks (TestPyPI CDN propagation lag + non-idempotent twine upload) that
made test-publish.yml fail spuriously. v0.7.0 was tagged on git and
uploaded to TestPyPI, but never reached PyPI — bumping to 0.7.1 with the
workflow fix (PR #35) is the cleanest path to a green ship, mirroring
the v0.4.0/v0.5.0 → v0.6.0 catch-up pattern.
v0.7.1 = v0.7.0 + workflow fix — functionally identical to v0.7.0
from the user perspective.
agent: list_schemas() → {"error": "not_configured", "exception_class": "ConfigurationError", "next_step": "Call setup_via_dialog..."}
agent: setup_via_dialog(host=..., user=..., dbname=...)
→ OS dialog appears, user types password
→ SELECT 1 against Redshift to verify
→ {"status": "configured", "tested": true}
agent: list_schemas() → works (lazy resolve, no restart)
✨ New MCP tools (11 → 13)
| Tool | What it does |
|---|---|
setup_via_dialog |
Bootstrap or update a profile in-session. Fields go via tool args (non-secret); password collected via OS-native dialog server-side (macOS osascript / Linux zenity). Tests connection against Redshift before declaring success. Returns one of 7 status codes covering happy path + dialog-cancelled / permission-denied / dialog-unavailable / platform-unsupported / empty-password / connection-failed. |
get_setup_status |
Read-only configuration check (no Redshift touch). Returns whether config.toml fields and keychain password exist; safely callable at session start; never returns the password value. |
🆕 Degraded-mode startup
Pre-v0.7.x: server crashed with ValueError("Profile 'default' is not configured...") if no profile existed. Agents had no in-band signal — they had to read ~/Library/Logs/Claude/mcp-server-*.log (client-specific path) to find the recovery hint.
v0.7.x:
- Server boots regardless of profile state — enters the MCP stdio loop and accepts tool calls
- DB tools return a structured
{"error": "not_configured", ...}response withnext_stepfield pointing atsetup_via_dialog - Lazy connection resolution: every tool call re-reads
config.toml+ keychain, so newly written profiles take effect without restarting the MCP client resolve_connection_paramsnow raisesConfigurationError(aValueErrorsubclass for backward-compat)
🔐 Password collection: OS dialog, never the wire
The same osascript / zenity discipline already used by the Claude Code /redshift-setup skill is now in the CLI + MCP tool:
| Subcommand / Tool | Password input |
|---|---|
redshift-comment-mcp set-password --dialog (CLI) |
macOS osascript / Linux zenity → keychain |
redshift-comment-mcp set-password --stdin (CLI) |
One line from stdin → keychain (headless / CI) |
setup_via_dialog (MCP tool) |
Same dialog mechanism via subprocess.run(capture_output=True) — Python str → keyring |
Password value path: user keyboard → OS dialog process stdout (in-pipe) → Python string in-process → keyring.set_password() → OS keychain. Never touches MCP wire, chat, Bash stdout, process args, or shell history.
🛡️ macOS Apple Events permission detection
v0.7.x distinguishes 3 macOS-specific osascript failure modes that pre-v0.7.x all collapsed to "user cancelled":
| Real situation | New status code | Agent guidance in message |
|---|---|---|
| User clicked Cancel button | dialog_cancelled |
Ask user whether intentional; default to retry |
| macOS Automation > System Events permission denied (error -1743) | permission_denied |
Open System Settings → Privacy & Security → Automation → enable System Events; or tccutil reset AppleEvents to force a fresh prompt |
| osascript binary not found | dialog_unavailable |
Fall back to set-password --stdin from terminal |
Detection uses the locale-stable numeric code (-1743) plus case-insensitive English text fallback ("Not authorized" / "Not authorised").
📋 Unified error-response schema
All four "this is broken" responses now carry the same field set so agents can pattern-match a single shape:
Per CWE-209 + OWASP Error Handling Cheat Sheet, raw str(exc) is not included for write_profile_failed / keychain_write_failed (third-party exception args may carry sensitive context). The exception class name is preserved as a separate field for diagnostic branching. Full exception detail is logged server-side with exc_info=True.
⚡ Connection test before declaring success
setup_via_dialog now runs SELECT 1 against Redshift after writing the profile + password. Catches host typos / VPN-not-connected / wrong password / firewall block / paused-cluster early — agent gets either {"status": "configured", "tested": true} or {"status": "configured_but_connection_failed", "connection_error": "..."} instead of a misleading "configured" followed by an immediate DB tool failure.
Failures are also logged server-side at WARNING level with profile + cluster coords + the underlying error, so operators can correlate transcript-reported issues with server log entries.
🤖 FastMCP instructions block updated
The handshake-time instructions= text now describes:
- The degraded-mode contract (server boots without profile)
- Both entry points: proactive
get_setup_statusat session start vs reactivenot_configuredon tool call - All response statuses agents need to handle, including
permission_deniedandconfigured_but_connection_failed
Agents see the recovery pattern BEFORE encountering an error, not just inside the error itself.
📚 Trilingual README updates
README.md / README.ja.md / README.zh-TW.md all updated with:
- Tool count 11 → 13 + new
setup_via_dialogandget_setup_statusrows - Code-agent bootstrap diagram showing the v0.7.x pure-MCP flow
- Fallback section for headless / no-GUI hosts (
--stdinpipe path)
🧪 Test coverage
| Tier | Before | After | New tests |
|---|---|---|---|
| Unit | 264 passed, 2 skipped | 299 passed, 2 skipped | +35 |
| Integration (live cluster) | 22 passed, 1 skipped | 22 passed, 1 skipped | unchanged shape |
| E2E (MCP wire) | 4 passed | 6 passed | +2 (setup_via_dialog wire registration + SETUP RECOVERY instructions handshake) |
Highlights:
test_bootstrap_then_use_end_to_end— the keystone scenario: server boots without profile → agent calls setup_via_dialog → next DB tool call works without restarttest_server_main_does_not_crash_when_no_profile— regression net against re-introducing upfront resolutiontest_every_db_tool_handles_not_configured— parametric over all 11 DB tools (catches missing@_guardedon new tools)test_get_setup_status_never_returns_password_value— hard security invariant; JSON-serializes response and scans for the password substringtest_collect_password_via_dialog_detects_permission_denied— parametric over realistic macOS stderr shapes (numeric, English, British spelling, lowercase, locale-translated)
🔧 Refactor + dev experience
RedshiftTools.__init__now takes aCallable[[], RedshiftConnectionConfig]provider instead of a built config — enables lazy re-resolution. 58 existingRedshiftTools(config)test call sites mechanically rewritten toRedshiftTools(lambda: config).@_guardeddecorator on all 11 DB tools centralizes theConfigurationError → not_configured responseconversion. Two new tools (setup_via_dialog,get_setup_status) deliberately skip the guard since they must run when no profile exists; documented inline + behaviorally tested.- 5 dialog-failure response builders extracted to module-level functions with a
_DIALOG_FAILURE_BUILDERSdispatch dict —setup_via_dialogbody shrinks from ~250 → ~164 lines.
🛠️ Release workflow fixes (v0.7.1 only — PR #35)
Two GitHub Actions workflow flaws surfaced by the v0.7.0 release attempt, fixed in v0.7.1:
test-publish.yml"Test installation from TestPyPI" step now retries pip install up to 6× with 30s backoff (3 min total) — absorbs the typical 1-3 min lag between TestPyPI's JSON API and its simple index (which pip uses).twine uploadnow uses--skip-existingin bothtest-publish.ymlandpublish.yml— re-runs are idempotent (no more 400 Bad Request on duplicate upload). Before this fix, any transient failure during the publish workflow corrupted the version slot until the next bump.
Forward-looking: future releases on the same workflows will not require version skipping just to retry.
📥 Install
# PyPI / generic MCP client
pip install --upgrade redshift-comment-mcp
# or
uvx redshift-comment-mcp
# Claude Code plugin
claude plugin install redshift-comment-mcppip install --upgrade from v0.6.0 lands directly on v0.7.1 (v0.7.0 is not on PyPI; see workflow fixes section above).
For multi-cluster claude_desktop_config.json / mcp.json examples and the in-band setup_via_dialog agent flow, see README §"Setting up with uvx".
🔗 Forward look: MCP Elicitation primitive
The MCP 2026-07-28 RC spec introduces an Elicitation primitive — a protocol-level mechanism for servers to request direct user input through the client's native UI. Once GA and FastMCP exposes it, the subprocess + osascript dialog hack here becomes obsolete: the server can request a password via the protocol and the client presents a native secure input widget. Documented in code at redshift_tools.py:1122-1140 so future maintainers know this is interim, not permanent architecture.
📊 External references grounding this release
- CWE-209: Generation of Error Message Containing Sensitive Information
- OWASP Error Handling Cheat Sheet
- OWASP LLM02:2025 Sensitive Information Disclosure
- Apple discussions: osascript -1743 stderr format
- Late Night Software: -1743 forum thread
- Snyk: keyring.errors.KeyringError advisory
- MCP 2026-07-28 RC: Elicitation primitive
📊 Full changelog
PRs in this release: #34 (degraded-mode + setup_via_dialog, 7 commits across 6 polish rounds + 1 industry-grounded calibration) + #35 (workflow idempotency fix + bump 0.7.0 → 0.7.1)
{ "error": "not_configured" | "write_profile_failed" | "keychain_write_failed" | "missing_field", "exception_class": "ConfigurationError" | "PermissionError" | "KeyringLocked" | "ValidationError" | ..., "message": "<sanitized agent-actionable text>" }