= ({ pods, manualRevokeNotice = null })
))}
+ {error && !draft && !selected && !githubAppSetup && (
+ {error}
+ )}
{rows.map(renderRow)}
{notYet.map(renderNotYet)}
diff --git a/frontend/src/v2/components/V2ConnectorsPage.tsx b/frontend/src/v2/components/V2ConnectorsPage.tsx
index 557d51b7e..f6275004b 100644
--- a/frontend/src/v2/components/V2ConnectorsPage.tsx
+++ b/frontend/src/v2/components/V2ConnectorsPage.tsx
@@ -13,6 +13,7 @@ import { V2Pod, V2PodMember } from '../hooks/useV2Pods';
import { PlatformGlyph } from '../icons/platforms';
import { ActGlyph, MarkGlyph, MarkName } from '../icons/glyphs';
import V2ConnectorTools, { type HostedMcpManualRevokeNotice } from './V2ConnectorTools';
+import { hostedMcpErrorNotice } from './hostedMcpErrorNotice';
import { localizeRelativeTime } from '../utils/localizeRelativeTime';
interface ConnectorGate {
@@ -385,11 +386,13 @@ const V2ConnectorsPage: React.FC = () => {
// Consume the callback result after displaying a fixed notice.
useEffect(() => {
const query = new URLSearchParams(window.location.search);
- if (query.get('hostedMcp') !== 'error' || query.get('code') !== 'unrequested_scope') return;
- setHostedMcpCallbackError(t('connectors.hostedMcpUnrequestedScope'));
+ if (query.get('hostedMcp') !== 'error') return;
+ setHostedMcpCallbackError(hostedMcpErrorNotice(t, query.get('code')));
query.delete('hostedMcp');
query.delete('code');
query.delete('extraScope');
+ query.delete('error');
+ query.delete('error_description');
const remaining = query.toString();
window.history.replaceState(
window.history.state,
diff --git a/frontend/src/v2/components/hostedMcpErrorNotice.ts b/frontend/src/v2/components/hostedMcpErrorNotice.ts
new file mode 100644
index 000000000..828491f28
--- /dev/null
+++ b/frontend/src/v2/components/hostedMcpErrorNotice.ts
@@ -0,0 +1,32 @@
+import type { TFunction } from 'i18next';
+
+// Only reviewed, fixed copy reaches the UI. Callback URLs and start errors
+// carry codes, never text from the provider.
+const noticeKeyByCode: Record = {
+ unknown_entry: 'hostedMcpUnknownEntry',
+ invalid_state: 'hostedMcpInvalidState',
+ state_expired: 'hostedMcpInvalidState',
+ state_consumed: 'hostedMcpInvalidState',
+ browser_mismatch: 'hostedMcpInvalidState',
+ issuer_mismatch: 'hostedMcpInvalidState',
+ consent_denied: 'hostedMcpConsentDenied',
+ vendor_error: 'hostedMcpVendorError',
+ rate_limited: 'hostedMcpRateLimited',
+ issuer_unreachable: 'hostedMcpIssuerUnavailable',
+ issuer_metadata_incomplete: 'hostedMcpIssuerUnavailable',
+ client_not_configured: 'hostedMcpClientSetup',
+ client_registration_required: 'hostedMcpClientSetup',
+ client_auth_unsupported: 'hostedMcpClientSetup',
+ client_auth_refused: 'hostedMcpClientSetup',
+ authorization_parameter_conflict: 'hostedMcpClientSetup',
+ exchange_refused: 'hostedMcpExchangeRefused',
+ exchange_unreachable: 'hostedMcpExchangeUnreachable',
+ exchange_incomplete: 'hostedMcpExchangeIncomplete',
+ exchange_invalid_scope: 'hostedMcpExchangeIncomplete',
+ unrequested_scope: 'hostedMcpUnrequestedScope',
+ authentication_required: 'hostedMcpAuthenticationRequired',
+};
+
+export const hostedMcpErrorNotice = (t: TFunction, code: string | null | undefined): string => (
+ t(`connectors.${noticeKeyByCode[code || ''] || 'hostedMcpUnknownError'}`)
+);