Skip to content

Commit e39c3f2

Browse files
fylornclaude
andcommitted
fix(mcp wizard): break Step 1 ⇄ Step 4 loop + correct Step 2 banner for anon
Three bugs the MS Docs install path surfaced: 1. **Step 1 ⇄ Step 4 dead loop** (the worst — admin couldn't ever reach Step 2/3 to override). `goNext` short-circuited Step 1 to Step 4 whenever the probe said `anonymous_ok`, AND `goBack` from Step 4 jumped to Step 1. So Back → Next → Back → Next bounced between 1 and 4 forever. Fix: move the anonymous shortcut from `goNext` (recurring traversal rule) to step-source's `runProbeAndNext` (one-shot probe-completion decision). `goBack` from Step 4 with anonymous now lands on Step 2 — the symmetric place to actually adjust the shape. 2. **Step 2 ProbeSummary lied for anonymous probes**. The fallback branch assumed "no OAuth metadata ⇒ recommend static token" and rendered "Reachable — auth required, no OAuth metadata" with the probe's "Connected — 3 tools available" detail underneath, which is internally contradictory. Added an `if (probe.anonymous_ok)` branch upfront so admins navigating back to Step 2 see "Reachable — public service" with the matching "Anonymous — no credential forwarded" recommendation. 3. **i18n**: `anonymousRec` was missing from probeSummary; added in en + zh. Renamed `anonymousTitle` from "Reachable — anonymous" to "Reachable — public service" / "可达 — 公开服务" so the Step 2 banner reads cleanly. Also added `goToStep` prop to StepSource so the probe handler can hop directly to Step 4 without going through `onNext`'s sequential-traversal contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7c56c2e commit e39c3f2

5 files changed

Lines changed: 45 additions & 19 deletions

File tree

web/src/components/mcp/wizard/server-wizard.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,14 @@ export function ServerWizardPage() {
5959

6060
const goNext = () => {
6161
const cur = wiz.state.step;
62-
// Anonymous shortcut: skip Step 2 and Step 3 if Step 1 detected
63-
// a public server. Admin can still go back and pick a non-anon
64-
// shape to override.
65-
if (cur === 1 && wiz.state.probe?.anonymous_ok && wiz.state.auth_shape === 'anonymous') {
66-
wiz.goToStep(4);
67-
return;
68-
}
62+
// Step 2 → Step 4 when admin chose anonymous (Step 3 is meaningless
63+
// — no credential to own). Step 1 used to short-circuit here too,
64+
// but that created a back→next loop: "Back" from Step 4 lands on
65+
// Step 1, then "Next" auto-skipped to Step 4 again, blocking the
66+
// admin from ever reaching Step 2. The anonymous shortcut from
67+
// Step 1 now lives inside `step-source.tsx`'s probe handler and
68+
// fires exactly once on probe completion, not on every traversal.
6969
if (cur === 2 && wiz.state.auth_shape === 'anonymous') {
70-
// No credential to own when there's no auth.
7170
wiz.patch({ credential_owner: 'per_user', shared_pending: null });
7271
wiz.goToStep(4);
7372
return;
@@ -79,9 +78,12 @@ export function ServerWizardPage() {
7978

8079
const goBack = () => {
8180
const cur = wiz.state.step;
82-
// Mirror the skip logic in reverse.
81+
// From Step 4, mirror the forward anonymous shortcut: jump back
82+
// to Step 2 (where the admin can change the shape away from
83+
// anonymous). Going back to Step 1 would force a re-probe and
84+
// re-trigger the auto-skip, which is the loop we just fixed.
8385
if (cur === 4 && wiz.state.auth_shape === 'anonymous') {
84-
wiz.goToStep(1);
86+
wiz.goToStep(2);
8587
return;
8688
}
8789
if (cur > 1) {
@@ -219,6 +221,7 @@ export function ServerWizardPage() {
219221
patch={wiz.patch}
220222
patchOAuth={wiz.patchOAuth}
221223
onNext={goNext}
224+
goToStep={wiz.goToStep}
222225
onCancel={cancel}
223226
templateLoading={wiz.templateLoading}
224227
/>

web/src/components/mcp/wizard/step-auth-shape.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,17 +216,17 @@ export function StepAuthShape({ state, patch, patchOAuth, onNext, onBack }: Prop
216216

217217
/**
218218
* Summary banner — surfaces the Step 1 probe verdict and the resulting
219-
* recommendation so the auto-pick isn't a black box. Three branches:
219+
* recommendation so the auto-pick isn't a black box. Four branches:
220220
*
221+
* - `anonymous_ok` ⇒ success; public service, "Anonymous" is the
222+
* correct pick. (Reachable when the admin navigates back to
223+
* Step 2 after the probe shortcutted them to Step 4.)
221224
* - `auth_required` + OAuth metadata + DCR succeeded ⇒ green; we
222225
* pre-filled everything (issuer, client_id, client_secret).
223226
* - `auth_required` + OAuth metadata + no DCR ⇒ amber; admin needs
224227
* to register the upstream OAuth app and paste Client ID back.
225228
* - `auth_required` + no OAuth metadata ⇒ neutral; static token
226229
* is the recommendation.
227-
*
228-
* Anonymous-OK probes never reach Step 2 (the wizard shell skips
229-
* straight to Step 4), so we don't render that case here.
230230
*/
231231
function ProbeSummary({
232232
url,
@@ -245,7 +245,12 @@ function ProbeSummary({
245245
let detail: string;
246246
let recommendation: string;
247247

248-
if (oauthProbe?.issuer && oauthProbe.client_id) {
248+
if (probe.anonymous_ok) {
249+
tone = 'success';
250+
title = t('mcpServers.wizard.probeSummary.anonymousTitle');
251+
detail = probe.message;
252+
recommendation = t('mcpServers.wizard.probeSummary.anonymousRec');
253+
} else if (oauthProbe?.issuer && oauthProbe.client_id) {
249254
tone = 'success';
250255
title = t('mcpServers.wizard.probeSummary.oauthDcrTitle');
251256
detail = t('mcpServers.wizard.probeSummary.oauthDcrDetail', {

web/src/components/mcp/wizard/step-source.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ interface Props {
1313
patch: (partial: Partial<WizardState>) => void;
1414
patchOAuth: (partial: Partial<WizardState['oauth']>) => void;
1515
onNext: () => void;
16+
/** Direct jump used by the anonymous-OK probe shortcut to land on
17+
* Step 4. We can't call `onNext` because it intentionally always
18+
* goes to Step 2 — the auto-skip is a one-shot probe-completion
19+
* decision, not a recurring traversal rule. */
20+
goToStep: (step: 1 | 2 | 3 | 4) => void;
1621
onCancel: () => void;
1722
/** True while the `?template=<slug>` prefill fetch is in flight. */
1823
templateLoading?: boolean;
@@ -37,6 +42,7 @@ export function StepSource({
3742
patch,
3843
patchOAuth,
3944
onNext,
45+
goToStep,
4046
onCancel,
4147
templateLoading,
4248
}: Props) {
@@ -151,6 +157,16 @@ export function StepSource({
151157
setError(probe.message);
152158
return;
153159
}
160+
// Anonymous shortcut: probe passed without credentials → no
161+
// auth shape to pick, no credential owner to choose. Jump to
162+
// Step 4 (confirm) directly. This skip is INTENTIONALLY a
163+
// one-shot probe-completion decision, not a permanent traversal
164+
// rule — once the admin lands on Step 4 they can `Back` through
165+
// every step normally to adjust shape / credentials.
166+
if (probe.anonymous_ok) {
167+
goToStep(4);
168+
return;
169+
}
154170
onNext();
155171
} catch (err) {
156172
setError(err instanceof Error ? err.message : 'Probe failed');

web/src/i18n/en.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -686,8 +686,9 @@
686686
"oauthManualRec": "OAuth 2.0 (manual app registration required).",
687687
"staticTitle": "Reachable — auth required, no OAuth metadata",
688688
"staticRec": "Static token (PAT / API key) — pick the header shape and continue.",
689-
"anonymousTitle": "Reachable — anonymous",
690-
"anonymousDetail": "Anonymous tools/list returned {{count}} tool(s) without any credential. Auth shape and credential ownership steps were skipped."
689+
"anonymousTitle": "Reachable — public service",
690+
"anonymousDetail": "Anonymous tools/list returned {{count}} tool(s) without any credential. Auth shape and credential ownership steps were skipped.",
691+
"anonymousRec": "Anonymous — no credential is forwarded. Continue to confirm."
691692
}
692693
},
693694
"credentialOwner": {

web/src/i18n/zh.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -686,8 +686,9 @@
686686
"oauthManualRec": "OAuth 2.0(需在上游手动注册应用)。",
687687
"staticTitle": "可达 — 需要认证,未发现 OAuth 元数据",
688688
"staticRec": "静态 token(PAT / API key)— 选择 header 形态后继续。",
689-
"anonymousTitle": "可达 — 匿名服务",
690-
"anonymousDetail": "匿名 tools/list 返回 {{count}} 个工具,无需凭据。已自动跳过「认证形态」和「凭据归属」两步。"
689+
"anonymousTitle": "可达 — 公开服务",
690+
"anonymousDetail": "匿名 tools/list 返回 {{count}} 个工具,无需凭据。已自动跳过「认证形态」和「凭据归属」两步。",
691+
"anonymousRec": "匿名 — 不转发任何凭据。点「下一步」继续到确认。"
691692
}
692693
},
693694
"credentialOwner": {

0 commit comments

Comments
 (0)