Skip to content

feat(mcp): wait out profile contention instead of hard-failing - #862

Closed
iceblue03 wants to merge 1 commit into
ffroliva:developfrom
iceblue03:feature/mcp-lease-wait-default
Closed

iceblue03 wants to merge 1 commit into
ffroliva:developfrom
iceblue03:feature/mcp-lease-wait-default

Conversation

@iceblue03

@iceblue03 iceblue03 commented Sep 17, 2026 •

Copy link
Copy Markdown

Summary

The CLI's fail-fast profile-lease default (GFLOW_CLI_LEASE_WAIT_SECONDS=0) is right for a one-shot terminal command a human can just retry. The MCP server has no human in that loop: two tool calls landing close together on the same profile (a client retry, two agents sharing a profile) currently hard-fail with ProfileLockedError instead of the second call simply running after the one already in flight finishes.

This adds _apply_mcp_lease_wait_default(), called first thing in all three MCP entry points (run_stdio / run_http / run_sse), before anything touches Settings (which is @lru_cached). It sets GFLOW_CLI_LEASE_WAIT_SECONDS=180 via os.environ.setdefault — long enough to outlast a typical single image/video generation, and a complete no-op for any operator who has already set the var themselves (including explicitly back to 0 for fail-fast).

The underlying wait mechanism itself (profile_lease.py, #478/#503) is untouched — this only changes which default the MCP surface starts from.

Lifecycle

  • Issue triaged with issue-assessment — no issue filed; this is a self-contained reliability gap noticed while exercising the MCP tools directly (concurrent gflow_* calls against the same profile).
  • predict verdict — not applicable; no transport/auth/selector/schema surface changes, only an env-var default at the MCP process entry points.
  • scenario + plan — skipped for a change this narrow (one function, three call sites); happy to write these up if a maintainer wants the full artifact trail before merge.
  • check (scoped) — ruff check, ruff format --check, and pyright all pass clean on the two touched files. I did not run the full repo-wide hygiene/doc-link/coverage suite from a fork checkout.
  • MCP twin — not applicable; there is no new CLI option to mirror. The whole point is that CLI and MCP now intentionally diverge on this one default (fail-fast vs. wait), which is exactly what GFLOW_CLI_LEASE_WAIT_SECONDS already exists to let either surface override.
  • E2E evidence — this changes a timing default around ProfileLease, not a Flow DOM/network surface, so nothing under tests/e2e/ exercises it directly. Coverage is two new focused unit tests in tests/mcp/test_server.py::TestMcpLeaseWaitDefault (default applies when unset; an explicit value, including 0, is never overridden), plus the three existing entry-point tests updated to patch the new call. 56 passed locally.
  • Live-verified — not a generation-path DOM change, so I did not run the 5-layer live-verify ledger. Informal evidence: while exercising gflow_* MCP tools back-to-back against one profile in a live session (character/instructions/credits calls fired close together), none hit ProfileLockedError.
  • Council review — not run; happy for a maintainer to run pr-council-review and I'll address findings.

Validation

  • Focused tests added for the behavior change (TestMcpLeaseWaitDefault, 2 new tests)
  • Documentation: none needed — this is an internal default, not a new user-facing option (GFLOW_CLI_LEASE_WAIT_SECONDS itself is already documented)
  • uv run python scripts/ci/check_doc_links.py — not run (no docs touched)
  • uv run ruff check src tests (scoped to the 2 changed files)
  • uv run pyright src (scoped to src/gflow_cli/mcp/server.py)
  • uv run pytest -q tests/mcp/test_server.py → 56 passed

Contribution Checklist

  • This PR targets develop
  • My commits use my real Git identity
  • Commit includes Signed-off-by: (git commit -s)
  • No secrets, cookies, account tokens, signed URLs, or private captured data included
  • I reviewed the AI-assisted change before submitting

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • MCP server entry points now default the lease wait time to 180 seconds when no value is configured.
    • Explicit lease wait settings, including 0, are preserved.

The CLI's fail-fast profile-lease default (GFLOW_CLI_LEASE_WAIT_SECONDS=0)
is right for a one-shot terminal command a human can just retry. The MCP
server has no human in that loop: two tool calls landing close together
on the same profile (a client retry, two agents sharing a profile) would
otherwise hard-fail with ProfileLockedError instead of simply running
after the call already in flight finishes.

All three MCP entry points (stdio/http/sse) now set a 180s wait default
via os.environ.setdefault before anything touches Settings (cached via
lru_cache) -- long enough to outlast a typical single generation, and a
no-op for anyone who has already set the var themselves.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Hyeonwoo Jo <icecream430909@gmail.com>
Copilot AI lite review requested due to automatic review settings September 17, 2026 14:17
@iceblue03
iceblue03 requested a review from ffroliva as a code owner September 17, 2026 14:17

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added external-contribution Pull request from a non-maintainer or fork needs-copilot-review Ask GitHub Copilot for advisory code review needs-maintainer-review Requires maintainer review before merge labels Sep 17, 2026
@github-actions

Copy link
Copy Markdown

Thanks for the contribution. This PR has been marked as an external contribution and routed for maintainer review.

Before merge, please make sure:

  • The PR targets develop.
  • Commits use a real Git identity or GitHub noreply email.
  • External commits include a DCO sign-off: git commit -s.
  • The PR does not include secrets, cookies, account tokens, signed URLs, or private captured data.
  • Focused tests/docs are included for behavior changes.

GitHub Copilot code review may be requested as an advisory first pass, but maintainer approval is still required.

@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: dd82551f-00e3-4f86-a722-86e832e16cd7

📥 Commits

Reviewing files that changed from the base of the PR and between 7361ae8 and 41108d9.

📒 Files selected for processing (2)
  • src/gflow_cli/mcp/server.py
  • tests/mcp/test_server.py

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The MCP server now applies a 180-second lease-wait default when the environment variable is unset. Stdio, HTTP, and SSE entry points apply the default before setup. Explicit values remain unchanged.

Changes

MCP lease-wait configuration

Layer / File(s) Summary
Lease-wait default and validation
src/gflow_cli/mcp/server.py, tests/mcp/test_server.py
_apply_mcp_lease_wait_default sets GFLOW_CLI_LEASE_WAIT_SECONDS to 180 when unset. Tests verify that explicit values, including 0, remain unchanged.
Server entry-point wiring
src/gflow_cli/mcp/server.py, tests/mcp/test_server.py
The stdio, HTTP, and SSE entry points apply the helper before server setup. Entry-point tests mock the helper.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 41108

The default wait behavior is applied consistently while preserving operator overrides; no actionable merge risk remains.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: MCP profile contention now waits instead of failing immediately.
Docstring Coverage ✅ Passed Docstring coverage is 90.91% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 2 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@ffroliva

Copy link
Copy Markdown
Owner

Thank you @iceblue03 — the goal here is right: an MCP caller has nobody to retry for it, so contention should wait rather than fail.

We're taking this over together with #863, tracked in #864, and shipping both in one release. Two findings from verifying it, detailed in #864:

  • gflow's root command loads and caches settings before mcp run / serve reaches the server, so the default is applied too late to take effect on those entry points.
  • Two calls inside the same MCP server run in one process, where the lease fails fast by design, so they need to be serialised per profile in the server rather than waited out.

You'll be credited in the changelog, and we'll ping you when it ships.

@ffroliva

Copy link
Copy Markdown
Owner

Shipped in v0.78.0 — thank you, and sorry for the wait.

Your diagnosis was right and the goal was right: an MCP caller has nobody to retry a ProfileLockedError for it. We took the change over in #868 with two corrections found while verifying it:

  1. The default never reached Settings on the real entry points. gflow's root command loads and caches settings before mcp run / serve dispatch, so setting the variable inside the server was too late. Measured through gflow mcp run: the variable read 180, the loaded setting stayed 0.0. The fix resets the settings cache — and it now also treats a value from a .env file as the operator's choice, which os.environ.setdefault would have overridden.
  2. Two calls inside one server never waited anyway. Tool calls run in the server's own process, where lease contention fails fast by design (waiting on yourself would deadlock). Those calls are now serialised per profile inside the server, which is what makes the "client retry / two agents on one server" case work. The 180 s wait covers contention with a different process, such as a CLI run.

Credited in the changelog. Closing in favour of #868.

@ffroliva

Copy link
Copy Markdown
Owner

Superseded by #868, shipped in v0.78.0. Your idea and the credit are in the changelog.

@ffroliva ffroliva closed this Sep 17, 2026
@iceblue03

Copy link
Copy Markdown
Author

Superseded by #868 — thanks for taking this further and fixing the settings-cache-timing and same-process-contention issues I missed. Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

external-contribution Pull request from a non-maintainer or fork needs-copilot-review Ask GitHub Copilot for advisory code review needs-maintainer-review Requires maintainer review before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants