Skip to content

[3391 by Claude Opus 5.5] Zero-config MCP through ProPR Connect in hosted UI tunnel mode - #3392

Merged
integry merged 6 commits into
mainfrom
3391/claude-opus-5-5-zero-config-mcp-through-p-20261029-2313-cnt
Oct 30, 2026
Merged

integry merged 6 commits into
mainfrom
3391/claude-opus-5-5-zero-config-mcp-through-p-20261029-2313-cnt

Conversation

@propr-dev

@propr-dev propr-dev Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

AI Implementation Summary

Closes #3391

Branch: 3391/claude-opus-5-5-zero-config-mcp-through-p-20261029-2313-cnt
Commits: ✅ Changes committed (b56670d)


AI Processing Completed

Execution Details:

Summary:
Zero-config MCP through ProPR Connect is implemented. With the hosted UI tunnel on (PROPR_UI_TUNNEL_ENABLED=true and PROPR_INSTANCE_ID set), a Plus user only needs to add https://mcp.propr.dev/mcp to their MCP client. The 15 new backend tests, 3 new UI tests and the related existing suites pass, and typecheck and zero-warning lint are clean. Nothing ran against the real Connect gateway; the registration tests use a stand-in for it.

Each item from the issue (only when its variable is unset; explicit values still win):

  1. Origin: MCP_PUBLIC_ORIGIN comes from the same proprInstanceProxyUrl() the tunnel-mode API_PUBLIC_URL uses, so it is https://t-<id>.propr.dev.
  2. Instance id: MCP_INSTANCE_ID is PROPR_INSTANCE_ID.
  3. Key: a random 32-byte key is created once at <data dir>/mcp/encryption-key, next to the web-push keys, with owner-only permissions. Its fingerprint is saved in the database on first use. If the file later goes missing, MCP reports KEY_MISSING instead of making a new key; a replaced file reports KEY_CHANGED. The key is never logged.
  4. Trust: MCP_CONNECT_TRUST is implied.
  5. On by default: the existing admin-only connect_enabled database setting is now the "Allow MCP clients through ProPR Connect" toggle in Settings. MCP_ENABLED=false still forces it off.
  6. Automatic registration: a background job in packages/api/mcp/connectRegistration.ts runs the same flow as mcp:connect:register. It leaves a current registration alone. It registers again when the registration is missing, older than a day, for a different tunnel id, or rejected by Connect with a 409. Failures retry after 5 s, doubling up to 15 minutes, and API startup never waits for it. /health, /api/status and the Settings page report connect_mcp. The npm script still works for manual repair and now uses the same derived config.
  7. Outside tunnel mode: behaviour is unchanged. The job only reports whether a manual registration exists and never registers by itself.

Decisions you may want to revisit:

  • Env templates: .env.example and the CLI copy shipped MCP_ENABLED=false and MCP_CONNECT_TRUST=false. That would have forced the feature off for every new stack, so both are now blank with advanced-override comments. Existing .env files that still contain those lines keep MCP off.
  • Scope ceiling: default-on tunnel mode uses the Settings scope ceiling, which defaults to read, plan, review. An admin has to widen it before clients can execute or merge.
  • Tunnel token: the API no longer requires PROPR_UI_TUNNEL_TOKEN in tunnel mode, because the launcher only gives it to the cloudflared sidecar. The explicit setup outside tunnel mode still requires it.
  • Re-provisioned tunnel: when the instance id comes from PROPR_INSTANCE_ID and that changes, the instance creates a new identity and users reconnect. An explicit MCP_INSTANCE_ID still can't be overwritten.
  • Plus: there is no local Plus check. Connect enforces it, so a Community installation shows failed:ACCESS_REVOKED.

The safety guarantees are unchanged: requests still need a valid Connect-signed delegation, the instance fails closed when online validation is unavailable, and the local /api/mcp checks still apply. Turning it on by default only makes the instance reachable through Connect.

Docs and tests:

  • Docs: the Connect section in docs/mcp.md is now "MCP works automatically with the hosted UI tunnel", with the explicit settings kept under "Advanced". The contract doc says the gateway is live, and the feature guide is updated.
  • New tests: packages/api/test/mcpTunnelMode.test.ts, added to test:mcp, covers every case listed in the issue. McpServerSection.test.tsx covers the toggle.
  • Changed existing test: I added connect_mcp, registered, pending and disabled to the allowed-words list in mcpObservableSurface.test.ts, because it checks every code word in docs/mcp.md.

I captured two Settings previews, toggle on (Connected) and off (Disabled), using the existing Playwright settings fixture with mocked data.

Detailed Logs:

  • Conversation: 244 messages
  • Session: 0e67c8f5-75fe-4e3f-8340-621f41ee5138

Log files stored at:

  • conversation: /usr/src/app/logs/executions/issue-3391-2026-10-29T23-37-23-251Z-conversation.json
  • output: /usr/src/app/logs/executions/issue-3391-2026-10-29T23-37-23-251Z-output.txt
Latest Conversation Messages
ASSISTANT: [content unavailable]

ASSISTANT: Zero-config MCP through ProPR Connect is implemented. With the hosted UI tunnel on (`PROPR_UI_TUNNEL_ENABLED=true` and `PROPR_INSTANCE_ID` set), a Plus user only needs to add `https://mcp.propr.dev/mc...


This PR was created automatically by ProPR after processing issue #3391.


💡 Need changes?

Comment on this PR to request refinements — the AI agent monitors comments and will update the implementation based on your feedback. Keep iterating until you're satisfied!


Visual preview

MCP through ProPR Connect turned off

MCP through ProPR Connect turned off

The same section after an admin turns the toggle off: status Disabled, with no connection details.

Connect sign-in required. Original retained until 2027-01-27T23:37:28.124Z.

View the full-resolution original in ProPR Connect

MCP Server settings in hosted UI tunnel mode

MCP Server settings in hosted UI tunnel mode

Settings → Integrations & MCP → MCP Server with the hosted tunnel: the admin toggle 'Allow MCP clients through ProPR Connect' is on, the status reports Connected (connect_mcp: registered), and the connection URL is https://mcp\.propr\.dev/mcp\. Rendered with the Playwright settings fixture and mocked A

Connect sign-in required. Original retained until 2027-01-27T23:37:29.268Z.

View the full-resolution original in ProPR Connect

… UI tunnel on (`PROPR_UI_TUNNEL_ENABLED=true` and `PROPR_INSTANCE_ID` set), a Plus user only needs to add `https://mcp.propr.dev/mcp` to their MCP client. The 15 new backend tests, 3 new UI tests and the related existing suites pass, and typecheck and zero-warning lint are clean. Nothing ran against the real Connect gateway; the registration tests use a stand-in for it.

Each item from the issue (only when its variable is unset; explicit values still win):
1. Origin: `MCP_PUBLIC_ORIGIN` comes from the same `proprInstanceProxyUrl()` the tunnel-mode `API_PUBLIC_URL` uses, so it is `https://t-<id>.propr.dev`.
2. Instance id: `MCP_INSTANCE_ID` is `PROPR_INSTANCE_ID`.
3. Key: a random 32-byte key is created once at `<data dir>/mcp/encryption-key`, next to the web-push keys, with owner-only permissions. Its fingerprint is saved in the database on first use. If the file later goes missing, MCP reports `KEY_MISSING` instead of making a new key; a replaced file reports `KEY_CHANGED`. The key is never logged.
4. Trust: `MCP_CONNECT_TRUST` is implied.
5. On by default: the existing admin-only `connect_enabled` database setting is now the "Allow MCP clients through ProPR Connect" toggle in Settings. `MCP_ENABLED=false` still forces it off.
6. Automatic registration: a background job in `packages/api/mcp/connectRegistration.ts` runs the same flow as `mcp:connect:register`. It leaves a current registration alone. It registers again when the registration is missing, older than a day, for a different tunnel id, or rejected by Connect with a 409. Failures retry after 5 s, doubling up to 15 minutes, and API startup never waits for it. `/health`, `/api/status` and the Settings page report `connect_mcp`. The npm script still works for manual repair and now uses the same derived config.
7. Outside tunnel mode: behaviour is unchanged. The job only reports whether a manual registration exists and never registers by itself.

Decisions you may want to revisit:
- Env templates: `.env.example` and the CLI copy shipped `MCP_ENABLED=false` and `MCP_CONNECT_TRUST=false`. That would have forced the feature off for every new stack, so both are now blank with advanced-override comments. Existing `.env` files that still contain those lines keep MCP off.
- Scope ceiling: default-on tunnel mode uses the Settings scope ceiling, which defaults to `read`, `plan`, `review`. An admin has to widen it before clients can execute or merge.
- Tunnel token: the API no longer requires `PROPR_UI_TUNNEL_TOKEN` in tunnel mode, because the launcher only gives it to the cloudflared sidecar. The explicit setup outside tunnel mode still requires it.
- Re-provisioned tunnel: when the instance id comes from `PROPR_INSTANCE_ID` and that changes, the instance creates a new identity and users reconnect. An explicit `MCP_INSTANCE_ID` still can't be overwritten.
- Plus: there is no local Plus check. Connect enforces it, so a Community installation shows `failed:ACCESS_REVOKED`.

The safety guarantees are unchanged: requests still need a valid Connect-signed delegation, the instance fails closed when online validation is unavailable, and the local `/api/mcp` checks still apply. Turning it on by default only makes the instance reachable through Connect.

Docs and tests:
- Docs: the Connect section in `docs/mcp.md` is now "MCP works automatically with the hosted UI tunnel", with the explicit settings kept under "Advanced". The contract doc says the gateway is live, and the feature guide is updated.
- New tests: `packages/api/test/mcpTunnelMode.test.ts`, added to `test:mcp`, covers every case listed in the issue. `McpServerSection.test.tsx` covers the toggle.
- Changed existing test: I added `connect_mcp`, `registered`, `pending` and `disabled` to the allowed-words list in `mcpObservableSurface.test.ts`, because it checks every code word in `docs/mcp.md`.

I captured two Settings previews, toggle on (Connected) and off (Disabled), using the existing Playwright settings fixture with mocked data.
@integry

integry commented Oct 29, 2026

Copy link
Copy Markdown
Owner

/review

@propr-dev

propr-dev Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ AI Code Review Complete requested by @integry

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — codex:gpt-6-astra

Overall Evaluation

The PR implements automatic tunnel-derived MCP configuration, persistent encryption keys, background registration, and Settings visibility. It needs changes before merge: the new scope ceiling is ineffective for Connect delegations, an explicit encryption-key change can silently replace an identity, and rejection recovery can abandon its retry.

✅ Persistent-key protection — Generated keys use private filesystem permissions and a database fingerprint; missing or replaced files fail closed.

✅ Background registration — Startup registration runs asynchronously, with bounded exponential backoff and shutdown timer cleanup.

This review uses static tracing of the supplied code; no commands or tests were run. The issue specification is truncated, so conclusions rely on visible requirements and changed-code correctness. Current checks show no failures, with seven still pending.

Merge blockers

Every finding below was introduced by this PR and must be resolved before merging.

F1: 🔴 Enforce the ceiling on Connect delegations

  • Required behavior: The new tunnel-managed configuration must enforce its advertised administrator scope ceiling.

  • Evidence: packages/api/mcp/configResolver.ts:resolveHostedTunnelMcpConfig — attaches adminSettings.scopeCeiling to the automatically enabled Connect configuration.

    1. A tunnel-mode instance resolves with the default ceiling read, plan, review.
    2. A valid Connect delegation includes an additional scope such as execute, with otherwise valid membership, repository access, and online validation.
    3. The supplied policy.ts:168–195 constructs the grant directly from the delegation’s scopes; policy.ts:35–57 returns those scopes unchanged.
    4. requireScope checks that returned list, allowing the additional scope despite the administrator ceiling.

    The OAuth provider receives the ceiling, but the Connect delegation path bypasses direct OAuth grant issuance. Signature verification and online validation establish the delegation’s authority without enforcing this local restriction. static trace: the changed resolver and supplied authentication/authorization consumers demonstrate the missing enforcement.

  • Minimum fix: Apply the current administrator ceiling to effective Connect scopes before authorization, including after Settings changes. Add a regression covering a valid delegation exceeding the default and subsequently narrowed ceilings.

F2: 🔴 Prevent silent identity replacement under an explicit key

  • Required behavior: Automatic registration must preserve the persistent instance identity and fail closed when its encryption key cannot decrypt existing identity records.

  • Evidence: packages/api/mcp/configResolver.ts:loadHostedTunnelMcpConfig, packages/api/mcp/connect.ts:registerMcpInstance

    1. A hosted instance successfully registers and stores its encrypted connect_identity.
    2. The operator restarts with a different, syntactically valid MCP_ENCRYPTION_KEY, leaving the instance ID unchanged.
    3. loadHostedTunnelMcpConfig skips persisted-key validation because an explicit key exists.
    4. The supplied McpStore.get catches the decryption failure and returns undefined. Registration therefore appears missing.
    5. Automatic registration calls registerMcpInstance, which also sees undefined, generates a new signing identity, and upserts it over the existing row before contacting Connect.

    The original identity is destroyed locally, and existing encrypted records become unreadable. The replaceIdentity guard only protects a successfully decrypted identity; it cannot distinguish an absent row from an unreadable one. static trace: the explicit-key branch, automatic registration path, and supplied store miss/upsert semantics establish this sequence.

  • Minimum fix: Distinguish an absent identity row from an undecryptable existing row before generating a key. Fail closed for the latter in the shared registration function so automatic and manual registration both preserve the stored identity.

F3: 🔴 Retain forced registration after a failed attempt

  • Required behavior: A rejected registration must continue retrying after a transient failure until registration succeeds.

  • Evidence: packages/api/mcp/connectRegistration.ts:attempt — clears forceRegister before awaiting registration and skips registration when a fresh local receipt remains.

    1. The instance has a matching registration receipt younger than one day.
    2. Connect returns 409 during online validation, setting forceRegister=true.
    3. The forced registration attempt clears that flag, then fails with CONNECT_UNAVAILABLE.
    4. The manager retains the local receipt, reports registered, and schedules a short retry.
    5. On that retry, the receipt still appears current and fresh, while forceRegister is false. The manager skips registration and schedules the next check for 24 hours later.

    Consequently, the promised retry makes no registration request and recovery depends on another client rejection or the daily refresh. Local receipt validation cannot prove that Connect accepts the registration after explicitly rejecting it. static trace: the rejection listener, flag reset, freshness shortcut, and retry scheduling establish this sequence.

  • Minimum fix: Preserve the forced-registration obligation through failures and clear it only after successful registration, without losing a newer rejection arriving during the awaited request. Test a fresh receipt followed by 409, transient registration failure, and timer-driven recovery.

Suggestions

These are optional follow-ups and are not sent to /fix.

S1: 🟢 Exercise the paired gateway harness

The new registration tests use a stand-in that accepts registration payloads. Running the existing paired gateway harness with tunnel-derived configuration would strengthen evidence for the zero-config flow, including entitlement refusal and registration compatibility. This is additional integration assurance, not a demonstrated gateway defect or an independent merge blocker.

Score

The implementation has useful persistence safeguards and focused tests, but three correctness defects remain in authorization and registration recovery. Pending checks are not themselves blockers.

Score: 5/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 1m 20s
  • Tokens: 73,175 (71,547 in / 1,628 out)
  • Cost: $0.69

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix F1 F2 F3 S1

🤖 Review by ProPR

@integry

integry commented Oct 29, 2026

Copy link
Copy Markdown
Owner

/fix

@propr-dev

propr-dev Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ Applied the requested follow-up changes in commit 778cdff

Addressed 1 AI review comment (IDs: 6511168178)

Addressed findings F1, F2, F3

Summary of Changes

I fixed all three findings, and each has a regression test that I confirmed fails on the old code and passes now. The full npm run test:mcp run had 439 of 446 tests passing. The 7 failures were all in relevanceServiceCommit.test.ts, caused by this container's read-only /tmp/git-processor. That file passes 7/7 when pointed at writable clone paths. Typecheck and zero-warning lint for packages/api are clean.

F1 – scope ceiling on Connect delegations

  • policy.ts: a Connect delegation's scopes are now filtered by the current admin ceiling before requireScope sees them. This is the same filter verifyAccessToken already applies to direct OAuth tokens.
  • The ceiling is read live on every request through the existing resolver-backed callback, so narrowing it in Settings applies to the next request. allowedScopes() in oauth.ts is now public so the policy can reuse it.
  • Test (mcpDelegation.test.ts): a valid delegation asking for read, review, execute, merge under the default ceiling gets only read, review, and execute is refused. After the ceiling is narrowed it gets only read, and review is refused.

F2 – identity not replaced when the key can't decrypt it

  • McpStore.has() reports whether a row exists even when it can't be decrypted. get() still treats such a row as missing.
  • registerMcpInstance now stops with KEY_CHANGED when an identity row exists but can't be decrypted, even with replaceIdentity. It does this before generating a key or contacting Connect. Automatic registration and mcp:connect:register both go through this function, so both are covered.
  • Automatic registration then reports failed:KEY_CHANGED and retries with backoff.
  • Test (mcpTunnelMode.test.ts): after registering, the explicit MCP_ENCRYPTION_KEY is changed. Both automatic and manual registration then fail, the stored identity row is unchanged, and nothing is sent to Connect.

F3 – forced registration survives a failed attempt

  • connectRegistration.ts: the forced-registration flag is no longer cleared before the request. It is cleared only after a successful registration, and only if no new 409 arrived during that request (tracked with a rejection counter). A rejection that may postdate the request therefore still forces another registration.
  • Test: a fresh receipt, then a 409, then a registration that fails because Connect is unavailable. The state stays registered with a 5 s retry instead of the daily refresh. The timer-driven retry registers again and success clears the flag. A second part sends a 409 during an in-flight registration and checks that one more registration follows.

There are no visual changes, so I made no previews.

Something I found but didn't change: with an explicit MCP_ENCRYPTION_KEY in tunnel mode, loadHostedTunnelMcpConfig still never compares the key with the stored fingerprint. Records other than the Connect identity, like grants and credentials, will therefore just look missing after a key change instead of producing a KEY_CHANGED setup error. F2 asked only for the fail-closed check in the shared registration function, so I didn't add this.


🤖 Implementation Details

  • Model: Claude Opus 5.5 (medium)
  • Requested By: @integry
  • Time: 10m 27s
  • Tokens: 2,350,541 (2,334,868 in / 15,673 out)
  • Cost: $1.22
  • Subscription usage: Session +1%

Undo Changes • View Task Execution


💡 ProPR Slash Commands
Command Description Example
/merge Merge target branch into this PR and resolve conflicts /merge
/review Request an AI code review /review or /review claude-sonnet
/fix Address review blockers, or blockers and suggestions with /fix all /fix, /fix all, or /fix F3 S5
/switch Change the AI model for this PR /switch claude-opus
/use Override the model for a single follow-up run /use claude-sonnet
/ultrafix Loop review→fix cycles until score goal is met /ultrafix or /ultrafix goal=8 max=10

🤖 Generated by ProPR
Processing comment ID: 6511180777✓

…that I confirmed fails on the old code and passes now. The full `npm run test:mcp` run had 439 of 446 tests passing. The 7 failures were all in `relevanceServiceCommit.test.ts`, caused by this container's read-only `/tmp/git-processor`. That file passes 7/7 when pointed at writable clone paths. Typecheck and zero-warning lint for `packages/api` are clean.

I fixed all three findings, and each has a regression test that I confirmed fails on the old code and passes now. The full `npm run test:mcp` run had 439 of 446 tests passing. The 7 failures were all in `relevanceServiceCommit.test.ts`, caused by this container's read-only `/tmp/git-processor`. That file passes 7/7 when pointed at writable clone paths. Typecheck and zero-warning lint for `packages/api` are clean.

**F1 – scope ceiling on Connect delegations**
- `policy.ts`: a Connect delegation's scopes are now filtered by the current admin ceiling before `requireScope` sees them. This is the same filter `verifyAccessToken` already applies to direct OAuth tokens.
- The ceiling is read live on every request through the existing resolver-backed callback, so narrowing it in Settings applies to the next request. `allowedScopes()` in `oauth.ts` is now public so the policy can reuse it.
- Test (`mcpDelegation.test.ts`): a valid delegation asking for `read, review, execute, merge` under the default ceiling gets only `read, review`, and `execute` is refused. After the ceiling is narrowed it gets only `read`, and `review` is refused.

**F2 – identity not replaced when the key can't decrypt it**
- `McpStore.has()` reports whether a row exists even when it can't be decrypted. `get()` still treats such a row as missing.
- `registerMcpInstance` now stops with `KEY_CHANGED` when an identity row exists but can't be decrypted, even with `replaceIdentity`. It does this before generating a key or contacting Connect. Automatic registration and `mcp:connect:register` both go through this function, so both are covered.
- Automatic registration then reports `failed:KEY_CHANGED` and retries with backoff.
- Test (`mcpTunnelMode.test.ts`): after registering, the explicit `MCP_ENCRYPTION_KEY` is changed. Both automatic and manual registration then fail, the stored identity row is unchanged, and nothing is sent to Connect.

**F3 – forced registration survives a failed attempt**
- `connectRegistration.ts`: the forced-registration flag is no longer cleared before the request. It is cleared only after a successful registration, and only if no new 409 arrived during that request (tracked with a rejection counter). A rejection that may postdate the request therefore still forces another registration.
- Test: a fresh receipt, then a 409, then a registration that fails because Connect is unavailable. The state stays `registered` with a 5 s retry instead of the daily refresh. The timer-driven retry registers again and success clears the flag. A second part sends a 409 during an in-flight registration and checks that one more registration follows.

There are no visual changes, so I made no previews.

**Something I found but didn't change:** with an explicit `MCP_ENCRYPTION_KEY` in tunnel mode, `loadHostedTunnelMcpConfig` still never compares the key with the stored fingerprint. Records other than the Connect identity, like grants and credentials, will therefore just look missing after a key change instead of producing a `KEY_CHANGED` setup error. F2 asked only for the fail-closed check in the shared registration function, so I didn't add this.

PR: #3392
Comment by: @integry (ID: 6511180777)
Model: Claude Opus 5.5
@integry

integry commented Oct 30, 2026

Copy link
Copy Markdown
Owner

/review

@propr-dev

propr-dev Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

⚠️ AI Code Review Complete requested by @integry

Posted 0 review, 1 failed:

View Task Details

@propr-dev

propr-dev Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — codex:gpt-6-astra

❌ Review failed: Analysis failed: Unable to find image 'propr/runtime-agent:7662b103e3c08511' locally
docker: Error response from daemon: pull access denied for propr/runtime-agent, repository does not exist or may require 'docker login': denied: requested access to the resource is denied

Run 'docker run --help' for more information

@integry

integry commented Oct 30, 2026

Copy link
Copy Markdown
Owner

/review

@propr-dev

propr-dev Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ AI Code Review Complete requested by @integry

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — codex:gpt-6-astra

Overall Evaluation

The PR implements automatic tunnel configuration, durable encryption keys, background Connect registration, and Settings controls. It needs a localized retry-scheduling correction before merge.

✅ Key preservation — Missing keys and undecryptable persisted identities fail closed instead of silently replacing existing identity material.

✅ Scope enforcement — Connect delegations are filtered through the current instance scope ceiling, with coverage for subsequent ceiling reductions.

The supplied issue description is truncated; this review uses the visible objective and complete supplied diff. Verification was static only, as requested. Current checks report 26 passed, none failed, and one pending.

Merge blockers

Every finding below was introduced by this PR and must be resolved before merging.

F4: 🔴 Preserve retries scheduled during registration

  • Required behavior: A Connect rejection must retain an effective automatic re-registration obligation until a registration sent after that rejection succeeds.

  • Evidence: packages/api/mcp/connectRegistration.ts, attempt, runOnce, and onConnectRegistrationRejected.

    1. Automatic registration starts, records lastRegistrationAt, captures rejectionsBefore, and awaits register(config, store, ...).
    2. During that await, a concurrent MCP validation receives HTTP 409. The listener sets forceRegister, increments rejections, and schedules re-registration after the remaining 60-second cooldown.
    3. The outstanding registration completes successfully. Because the rejection counter changed, attempt() correctly retains forceRegister.
    4. Nevertheless, runOnce() handles the successful result by unconditionally calling schedule(CONNECT_REGISTRATION_REFRESH_MS). That cancels the rejection's timer and replaces it with a 24-hour timer.

    The newer rejection therefore loses its prompt automatic recovery attempt. Without another request or configuration event, the outstanding registration obligation waits a full day, potentially leaving the client unable to reconnect throughout that interval. Coalescing does not prevent this: the listener's cooldown branch schedules a timer rather than setting rerun.

    static trace: The supplied test “a 409 keeps forcing registration through transient failures until one succeeds” explicitly injects a rejection during registration, but subsequently advances its clock and calls manager.trigger() manually. It proves preservation of the flag while bypassing the overwritten timer.

    proposed regression: After that injected rejection and successful registration, assert that the next scheduled attempt respects the rejection cooldown and completes through the timer without a manual trigger.

  • Minimum fix: On successful registration, check whether a newer rejection still leaves forceRegister set. Preserve or schedule its remaining cooldown instead of replacing it with the daily refresh timer.

Suggestions

These are optional follow-ups and are not sent to /fix.

S2: 🟢 Exercise the paired gateway flow

The new tests use a registration stand-in that accepts requests without exercising gateway verification. Running the documented cross-repository harness with tunnel-derived configuration would strengthen confidence in registration and first-request credential handoff. This is optional integration evidence, not a demonstrated additional blocker.

Score

The implementation has useful failure protections and focused coverage, but its automatic recovery scheduler can postpone an outstanding rejection retry for a day. The otherwise passing current checks do not cover that timer-driven sequence.

Score: 6/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 1m 16s
  • Tokens: 74,964 (73,596 in / 1,368 out)
  • Cost: $0.69

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix F4 S2

🤖 Review by ProPR

@integry

integry commented Oct 30, 2026

Copy link
Copy Markdown
Owner

/fix

@propr-dev

propr-dev Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ Applied the requested follow-up changes in commit 2dc9009

Addressed 1 AI review comment (IDs: 6511797302)

Addressed finding F4

Summary of Changes

I fixed F4. When a Connect rejection arrives while a registration request is still in flight, the successful registration no longer replaces that rejection's retry timer with the 24-hour refresh.

Change (packages/api/mcp/connectRegistration.ts): after a successful registration, runOnce now calls a new nextCheckMs() instead of always scheduling the daily refresh. If a newer rejection has left forceRegister set, it schedules the time left on that rejection's 60-second cooldown (counted from lastRegistrationAt). Otherwise it schedules the daily refresh as before. It reads forceRegister when it schedules, so a rejection that lands between attempt() returning and runOnce resuming is covered too.

Regression (packages/api/test/mcpTunnelMode.test.ts): I extended "a 409 keeps forcing registration through transient failures until one succeeds". After a rejection injected during a successful registration, the test now checks two things:

  • The next scheduled check is the 60-second cooldown, not the daily refresh.
  • Firing that timer by itself sends the next registration. The test no longer calls manager.trigger() by hand for this step. After that registration succeeds, the schedule goes back to the daily refresh.

Verification:

  • mcpTunnelMode.test.ts passes 17/17.
  • With the old line put back temporarily, the new assertion fails (16 pass, 1 fail).
  • Zero-warning eslint on both files and tsc --noEmit for packages/api are clean.

Other paths I checked, unchanged:

  • Failed registration: this path also replaces the rejection's timer, with the backoff retry (5 s, growing to a 15-minute cap). The registration obligation is still kept, because forceRegister stays set and a retry is always scheduled, so this is a slower retry rather than a lost one. I left it alone.
  • Rejection after the cooldown has passed: if the rejection arrives during a request that has run longer than 60 seconds, the listener calls trigger(). That sets rerun, so the loop runs again right away. This path is not affected.

🤖 Implementation Details

  • Model: Claude Opus 5.5 (medium)
  • Requested By: @integry
  • Time: 5m 50s
  • Tokens: 465,996 (461,413 in / 4,583 out)
  • Cost: $0.38

Undo Changes • View Task Execution


💡 ProPR Slash Commands
Command Description Example
/merge Merge target branch into this PR and resolve conflicts /merge
/review Request an AI code review /review or /review claude-sonnet
/fix Address review blockers, or blockers and suggestions with /fix all /fix, /fix all, or /fix F3 S5
/switch Change the AI model for this PR /switch claude-opus
/use Override the model for a single follow-up run /use claude-sonnet
/ultrafix Loop review→fix cycles until score goal is met /ultrafix or /ultrafix goal=8 max=10

🤖 Generated by ProPR
Processing comment ID: 6511800858✓

…ration request is still in flight, the successful registration no longer replaces that rejection's retry timer with the 24-hour refresh.

I fixed F4. When a Connect rejection arrives while a registration request is still in flight, the successful registration no longer replaces that rejection's retry timer with the 24-hour refresh.

**Change (`packages/api/mcp/connectRegistration.ts`):** after a successful registration, `runOnce` now calls a new `nextCheckMs()` instead of always scheduling the daily refresh. If a newer rejection has left `forceRegister` set, it schedules the time left on that rejection's 60-second cooldown (counted from `lastRegistrationAt`). Otherwise it schedules the daily refresh as before. It reads `forceRegister` when it schedules, so a rejection that lands between `attempt()` returning and `runOnce` resuming is covered too.

**Regression (`packages/api/test/mcpTunnelMode.test.ts`):** I extended "a 409 keeps forcing registration through transient failures until one succeeds". After a rejection injected during a successful registration, the test now checks two things:
- The next scheduled check is the 60-second cooldown, not the daily refresh.
- Firing that timer by itself sends the next registration. The test no longer calls `manager.trigger()` by hand for this step. After that registration succeeds, the schedule goes back to the daily refresh.

**Verification:**
- `mcpTunnelMode.test.ts` passes 17/17.
- With the old line put back temporarily, the new assertion fails (16 pass, 1 fail).
- Zero-warning eslint on both files and `tsc --noEmit` for `packages/api` are clean.

**Other paths I checked, unchanged:**
- **Failed registration:** this path also replaces the rejection's timer, with the backoff retry (5 s, growing to a 15-minute cap). The registration obligation is still kept, because `forceRegister` stays set and a retry is always scheduled, so this is a slower retry rather than a lost one. I left it alone.
- **Rejection after the cooldown has passed:** if the rejection arrives during a request that has run longer than 60 seconds, the listener calls `trigger()`. That sets `rerun`, so the loop runs again right away. This path is not affected.

PR: #3392
Comment by: @integry (ID: 6511800858)
Model: Claude Opus 5.5
@integry

integry commented Oct 30, 2026

Copy link
Copy Markdown
Owner

/review

@propr-dev

propr-dev Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ AI Code Review Complete requested by @integry

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — codex:gpt-6-astra

Overall Evaluation

The PR implements automatic tunnel configuration, durable key storage, background registration, and Settings controls. It needs a platform compatibility correction before merge.

✅ Preserved identity — Registration refuses to overwrite an existing identity that the configured encryption key cannot decrypt.

✅ Bounded delegated scopes — Connect grants are intersected with the current instance scope ceiling.

✅ Persistent retry obligation — A registration rejection remains actionable through transient failures and rejections arriving during registration.

The supplied current checks show 27 passed and no failures or pending checks. This review is static; no commands were run. The issue specification is explicitly truncated, so requirement assessment is limited to its visible content.

Merge blockers

Every finding below was introduced by this PR and must be resolved before merging.

F5: 🔴 Key creation fails on native Windows

  • Required behavior: Automatic MCP encryption-key provisioning must work on supported native installations without requiring an explicit MCP encryption key.

  • Evidence: packages/api/mcp/tunnelMode.ts, publishNewKey and readKeyFile

    1. A native Windows API starts with hosted tunnel configuration and no explicit MCP_ENCRYPTION_KEY.
    2. loadTunnelMcpEncryptionKey finds no existing key and calls publishNewKey.
    3. The directory is created, but Windows does not implement the requested POSIX 0700 permissions. Its synthesized mode fails the exact (stat.mode & 0o777) !== 0o700 comparison.
    4. Provisioning throws KEY_STORAGE_INVALID, leaving MCP disabled. Automatic retries repeat the same failure.

    The exact 0600 check in readKeyFile also prevents simply supplying a Windows-created key file. Neither retries nor the fingerprint checks address platform permission semantics. Verification provenance — static trace: the new provisioning path requires POSIX permission bits unconditionally; no Windows ACL or platform-specific implementation is present.

  • Minimum fix: Implement platform-appropriate private key storage and validation on Windows, including directory durability handling, while retaining the existing POSIX checks on Unix. Verify first creation and reuse under an ordinary Windows account.

Suggestions

These are optional follow-ups and are not sent to /fix.

S3: 🟢 Show explicit Connect opt-out accurately

With MCP_CONNECT_TRUST=false, the resolver returns an enabled configuration without Connect, and the registration manager reports disabled. However, tunnelStatusLabel falls through to “Registering,” and the toggle can announce that clients can connect. Handle connectAvailable=false or connectMcp='disabled' explicitly. This is a non-blocking presentation correction: backend trust remains disabled correctly.

S4: 🟢 Exercise the actual gateway contract

The new tests use a registration stand-in that accepts requests and returns matching receipts. Running the existing paired gateway harness with automatic tunnel configuration would strengthen confidence in registration and entitlement refusal. This is additional integration evidence, not a demonstrated contract defect.

Score

The implementation has useful coverage and passing current checks, but automatic key provisioning fails on native Windows. The required correction is localized.

Score: 6/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 2m 18s
  • Tokens: 74,978 (73,288 in / 1,690 out)
  • Cost: $0.71

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix F5 S3 S4

🤖 Review by ProPR

@integry

integry commented Oct 30, 2026

Copy link
Copy Markdown
Owner

/fix

@propr-dev

propr-dev Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ Applied the requested follow-up changes in commit 2d1cccc

Addressed 1 AI review comment (IDs: 6517457951)

Addressed finding F5

Summary of Changes

I fixed F5. On native Windows, the MCP encryption key is now created and reused using Windows file permissions (ACLs) instead of POSIX mode bits. On Linux and macOS the exact 0700/0600 checks are unchanged.

I could not run anything on Windows. The icacls and PowerShell calls have not been run on a real Windows machine. The tests swap in a stand-in for the Windows permission checks. So creating and reusing a key under an ordinary Windows account, which the finding asks for, still needs checking on a Windows machine.

What changed

  • New file, packages/api/mcp/privateKeyStorage.ts: it decides what "private" means on each platform.
    • Linux and macOS: the same mode checks as before.
    • Windows: a key file or directory counts as private only if it is owned by the current account, SYSTEM or Administrators, and only those can access it. Any other entry, such as Users, Everyone or Authenticated Users, makes it fail. The permissions are read with Windows PowerShell, called by its full System32 path.
    • Locking the folder on Windows: if the mcp folder is not private, the key creation code runs icacls <dir> /inheritance:r /grant:r *<SID>:(OI)(CI)F. This removes the permissions it inherited from its parent and gives full access to the current account only, and the new key file inherits that. The folder is checked again afterwards, and if it is still shared no key is written.
    • Saving to disk on Windows: the step that flushes the folder to disk is skipped, because Windows does not support it. The key file itself is still flushed before it is linked into place.
  • tunnelMode.ts: the key reader and key creation code use these platform checks instead of fixed mode bits.
    • Windows ignores the "don't follow symlinks" flag, so the reader now checks the path before opening the file and rejects symlinks. It then confirms the opened file is the same one it checked.
    • The existing error codes are unchanged: a non-private file or folder gives KEY_STORAGE_INVALID, and a failure running PowerShell or icacls gives KEY_STORAGE_UNAVAILABLE.
  • docs/mcp.md: the key-storage row now covers Windows.

Tests: I added packages/api/test/mcpTunnelKeyWindows.test.ts and listed it in test:mcp. It uses real files on disk with the stand-in Windows checks, and covers:

  • First creation succeeds even though the folder looks like 0777, and reuse succeeds even though the file looks like 0666; the folder is locked down exactly once.
  • A key file other accounts can read, or one owned by another account, is rejected and left untouched rather than replaced.
  • A folder that is still shared after the lock-down attempt never receives a key.
  • The rules for which accounts count as private.
  • The Linux/macOS 0600 check still rejects a 0644 file.

The three Windows tests fail on the previous tunnelMode.ts and pass now. The new file and mcpTunnelMode.test.ts together pass 22 of 22. Typecheck and zero-warning lint are clean for the changed packages/api files.

Separate finding, not changed: packages/api/services/instanceWebPushConfiguration.ts has the same POSIX-only 0700/0600 checks, so automatic Web Push key creation would fail on native Windows the same way. That code predates this PR, which only extracted instanceDataDirectory from it, so I left it alone.

The change has no visual effect, so I made no previews.


🤖 Implementation Details

  • Model: Claude Opus 5.5 (medium)
  • Requested By: @integry
  • Time: 5m 40s
  • Tokens: 1,314,747 (1,294,609 in / 20,138 out)
  • Cost: $0.99

Undo Changes • View Task Execution


💡 ProPR Slash Commands
Command Description Example
/merge Merge target branch into this PR and resolve conflicts /merge
/review Request an AI code review /review or /review claude-sonnet
/fix Address review blockers, or blockers and suggestions with /fix all /fix, /fix all, or /fix F3 S5
/switch Change the AI model for this PR /switch claude-opus
/use Override the model for a single follow-up run /use claude-sonnet
/ultrafix Loop review→fix cycles until score goal is met /ultrafix or /ultrafix goal=8 max=10

🤖 Generated by ProPR
Processing comment ID: 6517634976✓

…w created and reused using Windows file permissions (ACLs) instead of POSIX mode bits. On Linux and macOS the exact `0700`/`0600` checks are unchanged.

I fixed F5. On native Windows, the MCP encryption key is now created and reused using Windows file permissions (ACLs) instead of POSIX mode bits. On Linux and macOS the exact `0700`/`0600` checks are unchanged.

**I could not run anything on Windows.** The `icacls` and PowerShell calls have not been run on a real Windows machine. The tests swap in a stand-in for the Windows permission checks. So creating and reusing a key under an ordinary Windows account, which the finding asks for, still needs checking on a Windows machine.

**What changed**
- **New file, `packages/api/mcp/privateKeyStorage.ts`**: it decides what "private" means on each platform.
  - **Linux and macOS**: the same mode checks as before.
  - **Windows**: a key file or directory counts as private only if it is owned by the current account, SYSTEM or Administrators, and only those can access it. Any other entry, such as Users, Everyone or Authenticated Users, makes it fail. The permissions are read with Windows PowerShell, called by its full System32 path.
  - **Locking the folder on Windows**: if the `mcp` folder is not private, the key creation code runs `icacls <dir> /inheritance:r /grant:r *<SID>:(OI)(CI)F`. This removes the permissions it inherited from its parent and gives full access to the current account only, and the new key file inherits that. The folder is checked again afterwards, and if it is still shared no key is written.
  - **Saving to disk on Windows**: the step that flushes the folder to disk is skipped, because Windows does not support it. The key file itself is still flushed before it is linked into place.
- **`tunnelMode.ts`**: the key reader and key creation code use these platform checks instead of fixed mode bits.
  - Windows ignores the "don't follow symlinks" flag, so the reader now checks the path before opening the file and rejects symlinks. It then confirms the opened file is the same one it checked.
  - The existing error codes are unchanged: a non-private file or folder gives `KEY_STORAGE_INVALID`, and a failure running PowerShell or `icacls` gives `KEY_STORAGE_UNAVAILABLE`.
- **`docs/mcp.md`**: the key-storage row now covers Windows.

**Tests**: I added `packages/api/test/mcpTunnelKeyWindows.test.ts` and listed it in `test:mcp`. It uses real files on disk with the stand-in Windows checks, and covers:
- First creation succeeds even though the folder looks like `0777`, and reuse succeeds even though the file looks like `0666`; the folder is locked down exactly once.
- A key file other accounts can read, or one owned by another account, is rejected and left untouched rather than replaced.
- A folder that is still shared after the lock-down attempt never receives a key.
- The rules for which accounts count as private.
- The Linux/macOS `0600` check still rejects a `0644` file.

The three Windows tests fail on the previous `tunnelMode.ts` and pass now. The new file and `mcpTunnelMode.test.ts` together pass 22 of 22. Typecheck and zero-warning lint are clean for the changed `packages/api` files.

**Separate finding, not changed:** `packages/api/services/instanceWebPushConfiguration.ts` has the same POSIX-only `0700`/`0600` checks, so automatic Web Push key creation would fail on native Windows the same way. That code predates this PR, which only extracted `instanceDataDirectory` from it, so I left it alone.

The change has no visual effect, so I made no previews.

PR: #3392
Comment by: @integry (ID: 6517634976)
Model: Claude Opus 5.5
@propr-dev

propr-dev Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

CI failed: Full Test Suite Shard 2/4

Please investigate and fix this CI failure.

  • Check: Full Test Suite Shard 2/4
  • Result: failure
  • Commit: 2d1cccc6d1de (2d1cccc6d1ded51e654eda153794471b51a0b83e)
  • Details: View CI failure

Failure evidence

No failure output was provided by the CI service.

@propr-dev

propr-dev Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ Applied the requested follow-up changes in commit c2bcfec

Summary of Changes

I fixed the Shard 2/4 failure with a one-line doc change. The failing test passes again locally.

Cause: the shard failed in packages/api/test/mcpObservableSurface.test.ts. That test finds every backticked lowercase word in docs/mcp.md and requires each to be either a tool the admin can see or listed as an allowed non-tool word. The last commit (the F5 Windows ACL fix) added the line "on Windows the `mcp` directory's ACL…" to the MCP_ENCRYPTION_KEY row. A bare `mcp` is neither, so the test failed with: "docs/mcp.md names mcp, but it is neither an admin-visible tool nor an explicit non-tool token". No other test in that shard failed.

Fix: in docs/mcp.md (line 227), the phrase now reads "on Windows the `<data dir>/mcp` directory's ACL". This matches the path written earlier in the same sentence and is more precise. It also no longer reads as a tool name, so the allow-list in the test didn't need a new entry.

Checked:

  • Running mcpObservableSurface.test.ts and mcpDocs.test.ts through scripts/run-test-suite.mjs: both pass.
  • usageTipsCatalog and buildDocsManifest tests pass. The usage-tip generator reads a different paragraph of docs/mcp.md, so its output doesn't change.

I didn't run the rest of Shard 2/4.


🤖 Implementation Details

  • Model: Claude Opus 5.5 (medium)
  • Requested By: @propr-dev[bot]
  • Time: 3m 11s
  • Tokens: 662,506 (659,808 in / 2,698 out)
  • Cost: $0.40

Undo Changes • View Task Execution


💡 ProPR Slash Commands
Command Description Example
/merge Merge target branch into this PR and resolve conflicts /merge
/review Request an AI code review /review or /review claude-sonnet
/fix Address review blockers, or blockers and suggestions with /fix all /fix, /fix all, or /fix F3 S5
/switch Change the AI model for this PR /switch claude-opus
/use Override the model for a single follow-up run /use claude-sonnet
/ultrafix Loop review→fix cycles until score goal is met /ultrafix or /ultrafix goal=8 max=10

🤖 Generated by ProPR
Processing comment ID: 6518132330✓

…he failing test passes again locally.

I fixed the Shard 2/4 failure with a one-line doc change. The failing test passes again locally.

**Cause:** the shard failed in `packages/api/test/mcpObservableSurface.test.ts`. That test finds every backticked lowercase word in `docs/mcp.md` and requires each to be either a tool the admin can see or listed as an allowed non-tool word. The last commit (the F5 Windows ACL fix) added the line "on Windows the `` `mcp` `` directory's ACL…" to the `MCP_ENCRYPTION_KEY` row. A bare `` `mcp` `` is neither, so the test failed with: *"docs/mcp.md names mcp, but it is neither an admin-visible tool nor an explicit non-tool token"*. No other test in that shard failed.

**Fix:** in `docs/mcp.md` (line 227), the phrase now reads "on Windows the `` `<data dir>/mcp` `` directory's ACL". This matches the path written earlier in the same sentence and is more precise. It also no longer reads as a tool name, so the allow-list in the test didn't need a new entry.

**Checked:**
- Running `mcpObservableSurface.test.ts` and `mcpDocs.test.ts` through `scripts/run-test-suite.mjs`: both pass.
- `usageTipsCatalog` and `buildDocsManifest` tests pass. The usage-tip generator reads a different paragraph of `docs/mcp.md`, so its output doesn't change.

I didn't run the rest of Shard 2/4.

PR: #3392
Comment by: @propr-dev[bot] (ID: 6518132330)
Model: Claude Opus 5.5
@integry

integry commented Oct 30, 2026

Copy link
Copy Markdown
Owner

/review

@propr-dev

propr-dev Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ AI Code Review Complete requested by @integry

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — codex:gpt-6-astra

Overall Evaluation

The PR implements automatic tunnel configuration, persistent encryption keys, background registration, and Settings controls. It needs a localized correction to how Settings reports an explicit Connect opt-out.

✅ Identity preservation — Registration refuses to overwrite an existing identity that the configured encryption key cannot decrypt.

✅ Delegation scope enforcement — Connect delegations are filtered through the current instance scope ceiling, with coverage for narrowing that ceiling.

✅ Recovery coverage — Tests cover registration retries, tunnel replacement, missing keys, and rejections arriving during registration.

The issue specification is truncated, so requirements beyond its visible portion could not be assessed. This review used static analysis only; no commands were run. Current checks show zero failures and three pending.

Merge blockers

Every finding below was introduced by this PR and must be resolved before merging.

F6: 🔴 Report explicit Connect opt-out correctly

  • Required behavior: Explicit MCP_CONNECT_TRUST=false must take precedence, and the new Settings status must accurately describe whether Connect access is enabled.

  • Evidence: propr-ui/src/pages/SettingsPage/mcpConnectTunnelStatus.ts:tunnelStatusLabel, packages/api/mcp/configResolver.ts:resolveHostedTunnelMcpStatus

    1. Start a hosted tunnel instance with MCP_CONNECT_TRUST=false, valid remaining configuration, and the default connect_enabled=true.
    2. loadHostedTunnelMcpConfig correctly returns a direct MCP configuration without config.connect. Consequently, status reports enabled=true, connectAvailable=false, and no connectResource.
    3. The registration manager correctly reports connectMcp='disabled' and schedules no registration.
    4. tunnelStatusLabel checks general MCP enablement but never handles disabled Connect access, so it returns Registering indefinitely. The toggle remains checked, and the description promises access through Connect with no extra setup.

    Users therefore see an enabled Connect control and an apparent registration in progress even though operator configuration prevents registration entirely. Toggling off and on cannot resolve this. Existing override coverage verifies backend configuration, while the UI tests omit this combination. Verification provenance: static trace through the supplied resolver, registration manager, and status helpers.

  • Minimum fix: Distinguish Connect availability from general MCP enablement in the tunnel status and controls. Report the explicit trust opt-out as disabled by configuration, explain that the Settings toggle cannot override it, and cover this status combination in the UI tests.

Suggestions

These are optional follow-ups and are not sent to /fix.

S5: 🟢 Validate automatic registration against Connect

The new registration tests use an accepting stand-in. Running the documented paired gateway harness with the automatically derived configuration would strengthen evidence that the zero-configuration flow works across both services. This is additional integration confidence, not a demonstrated code blocker.

S6: 🟢 Clarify direct versus hosted instructions

docs/docs/features/mcp.md still opens with “disabled by default” and follows the hosted instructions with an unconditional instruction to connect to /api/mcp. Qualifying those statements as direct-instance guidance would make the two connection paths easier to follow.

Score

The backend implementation has substantial recovery and authorization coverage, but the explicit opt-out produces a misleading new Settings state that needs correction. No current checks have failed; three remain pending.

Score: 6/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 1m 37s
  • Tokens: 80,246 (79,028 in / 1,218 out)
  • Cost: $0.74

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix F6 S5 S6

🤖 Review by ProPR

@integry

integry commented Oct 30, 2026

Copy link
Copy Markdown
Owner

/fix

@propr-dev

propr-dev Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ Applied the requested follow-up changes in commit 4201315

Addressed 1 AI review comment (IDs: 6518544702)

Addressed finding F6

Summary of Changes

I fixed F6. With MCP_CONNECT_TRUST=false in hosted tunnel mode, the MCP section in Settings now says Disabled by configuration instead of Registering forever.

What changed

  • Backend: in tunnel mode, the MCP status now includes a new connectTrustDisabled: true flag whenever MCP_CONNECT_TRUST is set to anything other than true (packages/api/mcp/configResolver.ts). An unset value still counts as on, because tunnel mode defaults it to true. The flag is also added to the UI's status type in propr-ui/src/api/adminMcpApi.ts.
  • Status text (mcpConnectTunnelStatus.ts): the label is Disabled by configuration with a neutral colour, checked just after the operator-off case. The description now reads: "MCP_CONNECT_TRUST=false turns off MCP through ProPR Connect, and this setting cannot override it. Remove MCP_CONNECT_TRUST to use Connect." It no longer says clients connect with no extra setup.
  • Toggle (McpServerSection.tsx): "Allow MCP clients through ProPR Connect" is now unchecked and greyed out in this state. The saved connect_enabled value is kept as it was. No Connect URL is shown, since none is available.

Tests

  • The backend tunnel-mode test (mcpTunnelMode.test.ts) now checks that the opt-out reports connectTrustDisabled, Connect as unavailable and no Connect URL. It also checks that the default setup does not report the flag. All 17 tests in the file pass. With the backend change removed, the new check fails.
  • A new UI test in McpServerSection.test.tsx covers this exact case: label, unchecked and disabled toggle, the explanation, no "no extra setup" text and no Connect URL. All 4 tests pass. Against the previous UI code, the new test fails.
  • Typecheck and zero-warning lint pass for propr-ui and packages/api.

Completeness check: the other places that touch this state are the admin MCP route, the connect_mcp field in the status route, and the registration manager. All of them already handle the opt-out correctly, so nothing else needed changing. I didn't find any other problems.

I captured a Settings screenshot of the new state with the project's Playwright setup and fake API data. It is attached as a preview.

Visual preview

MCP server with MCP_CONNECT_TRUST=false

MCP server with MCP_CONNECT_TRUST=false

Hosted-tunnel Settings: status reads 'Disabled by configuration', the Connect toggle is unchecked and disabled, and the description explains the toggle cannot override MCP_CONNECT_TRUST=false

Connect sign-in required. Original retained until 2027-01-28T09:24:31.089Z.

View the full-resolution original in ProPR Connect


🤖 Implementation Details

  • Model: Claude Opus 5.5 (medium)
  • Requested By: @integry
  • Time: 5m 9s
  • Tokens: 1,417,409 (1,407,731 in / 9,678 out)
  • Cost: $0.78

Undo Changes • View Task Execution


💡 ProPR Slash Commands
Command Description Example
/merge Merge target branch into this PR and resolve conflicts /merge
/review Request an AI code review /review or /review claude-sonnet
/fix Address review blockers, or blockers and suggestions with /fix all /fix, /fix all, or /fix F3 S5
/switch Change the AI model for this PR /switch claude-opus
/use Override the model for a single follow-up run /use claude-sonnet
/ultrafix Loop review→fix cycles until score goal is met /ultrafix or /ultrafix goal=8 max=10

🤖 Generated by ProPR
Processing comment ID: 6519315886✓

… mode, the MCP section in Settings now says **Disabled by configuration** instead of **Registering** forever.

I fixed F6. With `MCP_CONNECT_TRUST=false` in hosted tunnel mode, the MCP section in Settings now says **Disabled by configuration** instead of **Registering** forever.

**What changed**
- **Backend:** in tunnel mode, the MCP status now includes a new `connectTrustDisabled: true` flag whenever `MCP_CONNECT_TRUST` is set to anything other than `true` (`packages/api/mcp/configResolver.ts`). An unset value still counts as on, because tunnel mode defaults it to `true`. The flag is also added to the UI's status type in `propr-ui/src/api/adminMcpApi.ts`.
- **Status text** (`mcpConnectTunnelStatus.ts`): the label is **Disabled by configuration** with a neutral colour, checked just after the operator-off case. The description now reads: "MCP_CONNECT_TRUST=false turns off MCP through ProPR Connect, and this setting cannot override it. Remove MCP_CONNECT_TRUST to use Connect." It no longer says clients connect with no extra setup.
- **Toggle** (`McpServerSection.tsx`): "Allow MCP clients through ProPR Connect" is now unchecked and greyed out in this state. The saved `connect_enabled` value is kept as it was. No Connect URL is shown, since none is available.

**Tests**
- The backend tunnel-mode test (`mcpTunnelMode.test.ts`) now checks that the opt-out reports `connectTrustDisabled`, Connect as unavailable and no Connect URL. It also checks that the default setup does not report the flag. All 17 tests in the file pass. With the backend change removed, the new check fails.
- A new UI test in `McpServerSection.test.tsx` covers this exact case: label, unchecked and disabled toggle, the explanation, no "no extra setup" text and no Connect URL. All 4 tests pass. Against the previous UI code, the new test fails.
- Typecheck and zero-warning lint pass for `propr-ui` and `packages/api`.

**Completeness check:** the other places that touch this state are the admin MCP route, the `connect_mcp` field in the status route, and the registration manager. All of them already handle the opt-out correctly, so nothing else needed changing. I didn't find any other problems.

I captured a Settings screenshot of the new state with the project's Playwright setup and fake API data. It is attached as a preview.

PR: #3392
Comment by: @integry (ID: 6519315886)
Model: Claude Opus 5.5
@integry

integry commented Oct 30, 2026

Copy link
Copy Markdown
Owner

/review

@propr-dev

propr-dev Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

✅ AI Code Review Complete requested by @integry

Posted 1 review:

View Task Details

@propr-dev

propr-dev Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

🔍 AI Code Review — codex:gpt-6-astra

Overall Evaluation

The PR implements tunnel-derived MCP configuration, persistent encryption keys, automatic Connect registration, and Settings controls. No merge-blocking defect was verified in the supplied code. It is ready to merge within the visible scope, conditional on the remaining checks passing.

✅ Persistent identity protection — Missing or changed encryption keys fail closed, and registration refuses to overwrite an identity it cannot decrypt.

✅ Delegation scope enforcement — Connect grants are filtered against the current instance scope ceiling before becoming principal scopes.

✅ Registration recovery — Background registration preserves retry obligations across transient failures and rejections arriving during registration.

This was a static review; no commands or tests were run. The supplied original issue body is explicitly truncated, so unseen acceptance criteria could not be assessed. Current checks report no failures and five pending checks.

Merge blockers

No merge blockers.

Suggestions

These are optional follow-ups and are not sent to /fix.

S7: 🟢 Exercise the actual gateway

The new registration tests use a stand-in that accepts registration payloads. Running the existing paired Connect harness with tunnel-derived configuration would strengthen evidence that automatic registration works against the gateway’s actual validation and entitlement rules. This is additional integration confidence, not a demonstrated code blocker.

S8: 🟢 Clarify connection instructions

docs/docs/features/mcp.md still opens with “disabled by default” and follows the hosted-tunnel section with unconditional instructions to use the instance’s /api/mcp endpoint and /mcp/apps. Qualifying these as direct-instance instructions would make the new Connect workflow clearer. The added hosted-tunnel paragraph already provides the correct gateway URL, so this is documentation cleanup.

Score

The implementation has substantial focused coverage and protects the important key, identity, and scope boundaries. Static review found no actionable regression; actual-gateway validation and the pending checks remain outstanding evidence.

Score: 8/10


🤖 Review Details

  • Model: GPT-6 Astra
  • Time: 1m 43s
  • Tokens: 78,442 (77,257 in / 1,185 out)
  • Cost: $0.72

View Task


💡 Next step: Comment /fix to address every F# merge blocker, /fix all for every pending blocker and suggestion, or name records explicitly, as in /fix F3 S5.
F# and S# IDs increment across review comments and remain permanent, so selectors such as /fix F3 F5 stay unambiguous across cycles. S# suggestions stay optional: they are implemented only when you name them or request /fix all, and they never relax a merge blocker.

Copy and edit to choose records from this review:

/fix S7 S8

🤖 Review by ProPR

@integry
integry merged commit c4c7228 into main Oct 30, 2026
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Zero-config MCP through ProPR Connect in hosted UI tunnel mode

1 participant