Skip to content

Commit 4f53586

Browse files
kristopolousclaude
andcommitted
fix(connectors): add "Needs Bearer prefix" checkbox to the connect dialog
Bright Data (and any other header-auth MCP connector, e.g. GitHub, Tavily) rejected pasted API keys with a 401 because nothing in the Connect/Replace Key dialog told the user their key needs a `Bearer ` scheme prefix. Add an explicit, opt-in checkbox to that dialog: when ticked, `Bearer ` is prepended to the pasted key before it's saved, unless it's already there. Minimal, single-file change — no new types, no catalog-parsing heuristics, no change to how keys are stored or resolved server-side. Fixes #490 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MxyEPwQ73B27NTr83U69Ba
1 parent a3a1395 commit 4f53586

3 files changed

Lines changed: 141 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@truefoundry/trueforge-ui": patch
3+
---
4+
5+
Fix header-auth MCP connectors (e.g. Bright Data) rejecting pasted API keys with 401 because the required `Bearer ` scheme prefix wasn't applied. The Connect / Replace Key dialog now has a "Needs \"Bearer\" prefix" checkbox — when ticked, the prefix is prepended to the pasted key before it's saved, unless it's already present.

packages/trueforge-ui/src/containers/SettingsBuilder/ConnectorSettings.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ const ConnectorSettings = () => {
4444
const [connectorAwaitingKey, setConnectorAwaitingKey] = useState<ConnectorCatalogEntry | null>(null);
4545
const [selectedConnector, setSelectedConnector] = useState<ConnectorBase | null>(null);
4646
const [apiKey, setApiKey] = useState('');
47+
const [needsBearerPrefix, setNeedsBearerPrefix] = useState(false);
4748

4849
const connectorIconMap = useMemo(() => {
4950
return (catalog ?? []).reduce(
@@ -141,6 +142,7 @@ const ConnectorSettings = () => {
141142
const closeApiKeyModal = () => {
142143
setConnectorAwaitingKey(null);
143144
setApiKey('');
145+
setNeedsBearerPrefix(false);
144146
setFormError(null);
145147
};
146148

@@ -166,6 +168,7 @@ const ConnectorSettings = () => {
166168
const handleConnect = (entry: ConnectorCatalogEntry) => {
167169
if (entry.auth.type === 'header') {
168170
setApiKey('');
171+
setNeedsBearerPrefix(false);
169172
setFormError(null);
170173
setConnectorAwaitingKey(entry);
171174
return;
@@ -183,9 +186,11 @@ const ConnectorSettings = () => {
183186
const entry = connectorAwaitingKey;
184187
setFormError(null);
185188
void runMutation(async () => {
189+
const trimmedKey = apiKey.trim();
190+
const keyWithPrefix = needsBearerPrefix && !/^bearer\s/i.test(trimmedKey) ? `Bearer ${trimmedKey}` : trimmedKey;
186191
const auth: ConnectorAuth = {
187192
type: 'header',
188-
apiKey: apiKey.trim(),
193+
apiKey: keyWithPrefix,
189194
...(entry.auth.type === 'header' && entry.auth.headerName ? { headerName: entry.auth.headerName } : {}),
190195
};
191196
const existing = connectors.ordered.find(({ connector }) => connector.id === entry.id);
@@ -305,6 +310,7 @@ const ConnectorSettings = () => {
305310
onClick={event => {
306311
event.stopPropagation();
307312
setApiKey('');
313+
setNeedsBearerPrefix(false);
308314
setConnectorAwaitingKey(connector);
309315
}}
310316
>
@@ -509,6 +515,20 @@ const ConnectorSettings = () => {
509515
required
510516
className={auiInputClass('h-11')}
511517
/>
518+
<label className="mt-2 flex w-fit cursor-pointer items-center gap-2 select-none">
519+
<input
520+
type="checkbox"
521+
className="size-4 shrink-0 cursor-pointer accent-primary-button-bg disabled:cursor-not-allowed disabled:opacity-50"
522+
checked={needsBearerPrefix}
523+
disabled={busy}
524+
onChange={event => {
525+
setNeedsBearerPrefix(event.target.checked);
526+
}}
527+
/>
528+
<span className="text-sm text-text-secondary">
529+
Needs &quot;Bearer&quot; prefix (some providers reject the raw key otherwise)
530+
</span>
531+
</label>
512532
</div>
513533
{formError ? <p className="text-failure-bg text-sm">{formError}</p> : null}
514534
</div>
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// @vitest-environment jsdom
2+
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
3+
import { beforeAll, describe, expect, it } from 'vitest';
4+
5+
import ConnectorSettings from '@/containers/SettingsBuilder/ConnectorSettings.js';
6+
import { ServerProvider } from '@/server/ServerContext.js';
7+
import type { ConnectorAuth, ConnectorBase, ConnectorCatalogEntry } from '@/server/types.js';
8+
import { createMockAgentUIServer, createMockCatalog } from '../../server/mockServer.js';
9+
10+
beforeAll(() => {
11+
HTMLDialogElement.prototype.showModal = function showModal() {
12+
this.setAttribute('open', '');
13+
};
14+
HTMLDialogElement.prototype.close = function close() {
15+
this.removeAttribute('open');
16+
this.dispatchEvent(new Event('close'));
17+
};
18+
});
19+
20+
const brightData: ConnectorCatalogEntry = {
21+
id: 'bright-data',
22+
name: 'bright-data',
23+
description: 'Search the web and scrape pages, including sites behind bot protection.',
24+
url: 'https://mcp.brightdata.com/mcp',
25+
auth: { type: 'header', headerName: 'Authorization' },
26+
};
27+
28+
const connectedBrightData: ConnectorBase = {
29+
id: 'bright-data',
30+
name: 'bright-data',
31+
description: brightData.description ?? '',
32+
url: brightData.url,
33+
auth: { type: 'header', headerName: 'Authorization' },
34+
requiresAuth: false,
35+
authenticated: true,
36+
};
37+
38+
function renderConnectorSettings(onCreateConnector: (auth: ConnectorAuth) => void) {
39+
const server = createMockAgentUIServer({
40+
catalog: createMockCatalog({
41+
connectorCatalog: {
42+
getConnectorCatalog: async () => [brightData],
43+
listConnectors: async () => [],
44+
getConnector: async () => connectedBrightData,
45+
getToolsByConnectorId: async () => [],
46+
createConnector: async req => {
47+
onCreateConnector(req.auth);
48+
return connectedBrightData;
49+
},
50+
updateConnector: async () => connectedBrightData,
51+
authenticateConnector: async () => ({ authorization_endpoint: '' }),
52+
disconnectConnector: async () => connectedBrightData,
53+
},
54+
}),
55+
});
56+
57+
render(
58+
<ServerProvider server={server}>
59+
<ConnectorSettings />
60+
</ServerProvider>,
61+
);
62+
}
63+
64+
async function openConnectModal() {
65+
const row = await screen.findByText('bright-data');
66+
const connectButton = within(row.closest('article') as HTMLElement).getByRole('button', { name: 'Connect' });
67+
fireEvent.click(connectButton);
68+
return within(await screen.findByRole('dialog'));
69+
}
70+
71+
describe('ConnectorSettings "Needs Bearer prefix" checkbox', () => {
72+
it('leaves the key untouched when the checkbox is unticked', async () => {
73+
let submittedAuth: ConnectorAuth | undefined;
74+
renderConnectorSettings(auth => {
75+
submittedAuth = auth;
76+
});
77+
78+
const dialog = await openConnectModal();
79+
fireEvent.change(dialog.getByLabelText('API key / token'), { target: { value: 'abc123' } });
80+
fireEvent.click(dialog.getByRole('button', { name: 'Connect' }));
81+
82+
await waitFor(() => expect(submittedAuth).toBeDefined());
83+
expect(submittedAuth).toMatchObject({ apiKey: 'abc123' });
84+
});
85+
86+
it('prepends "Bearer " when the checkbox is ticked', async () => {
87+
let submittedAuth: ConnectorAuth | undefined;
88+
renderConnectorSettings(auth => {
89+
submittedAuth = auth;
90+
});
91+
92+
const dialog = await openConnectModal();
93+
fireEvent.change(dialog.getByLabelText('API key / token'), { target: { value: 'abc123' } });
94+
fireEvent.click(dialog.getByRole('checkbox'));
95+
fireEvent.click(dialog.getByRole('button', { name: 'Connect' }));
96+
97+
await waitFor(() => expect(submittedAuth).toBeDefined());
98+
expect(submittedAuth).toMatchObject({ apiKey: 'Bearer abc123' });
99+
});
100+
101+
it('does not double-prefix a key the user already typed with "Bearer "', async () => {
102+
let submittedAuth: ConnectorAuth | undefined;
103+
renderConnectorSettings(auth => {
104+
submittedAuth = auth;
105+
});
106+
107+
const dialog = await openConnectModal();
108+
fireEvent.change(dialog.getByLabelText('API key / token'), { target: { value: 'Bearer abc123' } });
109+
fireEvent.click(dialog.getByRole('checkbox'));
110+
fireEvent.click(dialog.getByRole('button', { name: 'Connect' }));
111+
112+
await waitFor(() => expect(submittedAuth).toBeDefined());
113+
expect(submittedAuth).toMatchObject({ apiKey: 'Bearer abc123' });
114+
});
115+
});

0 commit comments

Comments
 (0)