Skip to content

v0.7.1 — v0.7.0 catch-up + workflow idempotency fix

Choose a tag to compare

@kouko kouko released this 28 May 23:15
c27ea4c

🎯 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 with next_step field pointing at setup_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_params now raises ConfigurationError (a ValueError subclass 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:

{
  "error": "not_configured" | "write_profile_failed" | "keychain_write_failed" | "missing_field",
  "exception_class": "ConfigurationError" | "PermissionError" | "KeyringLocked" | "ValidationError" | ...,
  "message": "<sanitized agent-actionable text>"
}

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_status at session start vs reactive not_configured on tool call
  • All response statuses agents need to handle, including permission_denied and configured_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_dialog and get_setup_status rows
  • Code-agent bootstrap diagram showing the v0.7.x pure-MCP flow
  • Fallback section for headless / no-GUI hosts (--stdin pipe 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 restart
  • test_server_main_does_not_crash_when_no_profile — regression net against re-introducing upfront resolution
  • test_every_db_tool_handles_not_configured — parametric over all 11 DB tools (catches missing @_guarded on new tools)
  • test_get_setup_status_never_returns_password_value — hard security invariant; JSON-serializes response and scans for the password substring
  • test_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 a Callable[[], RedshiftConnectionConfig] provider instead of a built config — enables lazy re-resolution. 58 existing RedshiftTools(config) test call sites mechanically rewritten to RedshiftTools(lambda: config).
  • @_guarded decorator on all 11 DB tools centralizes the ConfigurationError → not_configured response conversion. 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_BUILDERS dispatch dict — setup_via_dialog body 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:

  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).
  2. twine upload now uses --skip-existing in both test-publish.yml and publish.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-mcp

pip 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

📊 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)