diff --git a/client/src/components/Settings/IntegrationsTab.test.tsx b/client/src/components/Settings/IntegrationsTab.test.tsx
index e04a95619..e3ce2e6e9 100644
--- a/client/src/components/Settings/IntegrationsTab.test.tsx
+++ b/client/src/components/Settings/IntegrationsTab.test.tsx
@@ -644,11 +644,11 @@ describe('IntegrationsTab', () => {
expect(createBtn).toBeDisabled();
});
- it('FE-COMP-INTEGRATIONS-032: error toast shown when create OAuth client fails', async () => {
+ it('FE-COMP-INTEGRATIONS-032: a refused client registration toasts the reason the server gave', async () => {
const user = userEvent.setup();
server.use(
http.post('/api/oauth/clients', () =>
- HttpResponse.json({ error: 'server error' }, { status: 500 })
+ HttpResponse.json({ error: 'Redirect URI must use HTTPS, loopback HTTP, or a private custom scheme: http://192.168.1.5/cb' }, { status: 400 })
)
);
enableMcp();
@@ -657,9 +657,24 @@ describe('IntegrationsTab', () => {
await user.click(screen.getByRole('button', { name: /New Client/i }));
await screen.findByText('Register OAuth Client');
await user.type(screen.getByPlaceholderText(/Claude Web, My MCP App/i), 'Fail Client');
+ await user.type(screen.getByPlaceholderText(/https:\/\/your-app/i), 'http://192.168.1.5/cb');
+ await user.click(screen.getByRole('button', { name: /Register Client/i }));
+ expect(await screen.findByText(/Redirect URI must use HTTPS/)).toBeInTheDocument();
+ expect(screen.queryByText('Failed to register OAuth client')).not.toBeInTheDocument();
+ });
+
+ it('FE-COMP-INTEGRATIONS-032b: a failure the server says nothing about keeps the generic text', async () => {
+ const user = userEvent.setup();
+ server.use(http.post('/api/oauth/clients', () => HttpResponse.error()));
+ enableMcp();
+ render(<>>);
+ await screen.findByText('MCP Configuration');
+ await user.click(screen.getByRole('button', { name: /New Client/i }));
+ await screen.findByText('Register OAuth Client');
+ await user.type(screen.getByPlaceholderText(/Claude Web, My MCP App/i), 'Fail Client');
await user.type(screen.getByPlaceholderText(/https:\/\/your-app/i), 'http://localhost');
await user.click(screen.getByRole('button', { name: /Register Client/i }));
- expect(await screen.findByText(/Failed to register/i)).toBeInTheDocument();
+ expect(await screen.findByText('Failed to register OAuth client')).toBeInTheDocument();
});
});
diff --git a/client/src/components/Settings/IntegrationsTab.tsx b/client/src/components/Settings/IntegrationsTab.tsx
index 23085f799..4310717a9 100644
--- a/client/src/components/Settings/IntegrationsTab.tsx
+++ b/client/src/components/Settings/IntegrationsTab.tsx
@@ -13,6 +13,7 @@ import ApiKeysSection from './ApiKeysSection'
import { PRESET_SCOPES_DEFAULT, PRESET_SCOPES_READONLY } from '../../api/oauthScopes'
import ScopeGroupPicker from '../OAuth/ScopeGroupPicker'
import { useAuthStore } from '../../store/authStore'
+import { getApiErrorMessage } from '../../utils/apiError'
interface OAuthPreset {
id: string
@@ -266,8 +267,11 @@ function useIntegrations() {
setOauthNewUris('')
setOauthNewScopes([])
setOauthIsMachine(false)
- } catch {
- toast.error(t('settings.oauth.toast.createError'))
+ } catch (err) {
+ // The server names the rule that refused the client (a scope, the
+ // ten-client cap, a redirect URI it will not take); swallowing it left
+ // "Failed to register OAuth client" and nothing to act on.
+ toast.error(getApiErrorMessage(err, t('settings.oauth.toast.createError')))
} finally {
setOauthCreating(false)
}
diff --git a/client/src/mobile/screens/settings/MSettingsMcp.tsx b/client/src/mobile/screens/settings/MSettingsMcp.tsx
index b4304cf6e..873837007 100644
--- a/client/src/mobile/screens/settings/MSettingsMcp.tsx
+++ b/client/src/mobile/screens/settings/MSettingsMcp.tsx
@@ -11,6 +11,7 @@ import MChip from '../../components/MChip'
import { MSetCard, MSetEyebrow, MSetInput, MSetTextarea, MSetButton, MSetHint, MSetRow } from './MSettingsUi'
import MConfirmSheet from './MConfirmSheet'
import MSegmented from '../../components/MSegmented'
+import { getApiErrorMessage } from '../../../utils/apiError'
interface OAuthPreset {
id: string
@@ -188,8 +189,11 @@ export default function MSettingsMcp() {
setNewUris('')
setNewScopes([])
setIsMachine(false)
- } catch {
- toast.error(t('settings.oauth.toast.createError'))
+ } catch (err) {
+ // The server names the rule that refused the client (a scope, the
+ // ten-client cap, a redirect URI it will not take); swallowing it left
+ // "Failed to register OAuth client" and nothing to act on.
+ toast.error(getApiErrorMessage(err, t('settings.oauth.toast.createError')))
} finally {
setCreating(false)
}
diff --git a/client/tests/unit/mobile/settings/MSettingsMcp.test.tsx b/client/tests/unit/mobile/settings/MSettingsMcp.test.tsx
index 7839c7599..fbacac3c3 100644
--- a/client/tests/unit/mobile/settings/MSettingsMcp.test.tsx
+++ b/client/tests/unit/mobile/settings/MSettingsMcp.test.tsx
@@ -265,17 +265,19 @@ describe('MSettingsMcp', () => {
expect(writeText).toHaveBeenNthCalledWith(2, 'sec-copy');
});
- it('FE-MOB-SETMCP-011: a failed registration surfaces an error toast and keeps the form open', async () => {
+ it('FE-MOB-SETMCP-011: a refused registration surfaces the reason the server gave and keeps the form open', async () => {
const user = userEvent.setup();
- server.use(http.post('/api/oauth/clients', () => HttpResponse.json({ error: 'boom' }, { status: 500 })));
+ server.use(http.post('/api/oauth/clients', () =>
+ HttpResponse.json({ error: 'Redirect URI must use HTTPS, loopback HTTP, or a private custom scheme: http://192.168.1.5/cb' }, { status: 400 })));
renderScreen();
await user.click(screen.getByRole('button', { name: 'New Client' }));
await user.type(await screen.findByPlaceholderText(/Claude Web, My MCP App/), 'Broken');
- fireEvent.change(screen.getByPlaceholderText(/your-app/), { target: { value: 'http://localhost' } });
+ fireEvent.change(screen.getByPlaceholderText(/your-app/), { target: { value: 'http://192.168.1.5/cb' } });
await user.click(screen.getByRole('button', { name: 'Register Client' }));
- await screen.findByText('Failed to register OAuth client');
+ await screen.findByText(/Redirect URI must use HTTPS/);
+ expect(screen.queryByText('Failed to register OAuth client')).toBeNull();
expect(screen.getByText('Register OAuth Client')).toBeInTheDocument();
});