Skip to content

Commit 2262ef9

Browse files
nicklaunchesclaude
andcommitted
Send a stored MCP token with the scheme it names
Every token stored against a custom MCP server went out as Bearer. A vendor that forwards the Authorization header to an API which only speaks Basic still answers the handshake and the tool listing, so the connector looked connected and its tools were offered, and every real call came back 401. DataForSEO's hosted server behaves exactly that way. A token that already begins with Basic or Bearer is now sent as written; a bare token is still Bearer, so nothing already working changes. The scheme travels with the credential rather than as another setting on the server row, so rotating a token can change how it is presented and nothing else has to know. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaHWJ1niprhBc5NzJ9pxme
1 parent 257c128 commit 2262ef9

3 files changed

Lines changed: 52 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
88

99
## Unreleased
1010

11+
### A custom MCP server token is sent with the scheme it names
12+
13+
Every token stored against a custom MCP server went out as `Bearer`, whatever the vendor asked for.
14+
A server that forwards the header to an API speaking Basic auth still answers the handshake and the
15+
tool listing, so the Plugins page showed the connector connected and its tools offered, and every
16+
real call came back 401. DataForSEO's hosted server behaves exactly this way.
17+
18+
A token that begins with `Basic ` or `Bearer ` is now sent as written, so paste the credential the
19+
vendor gives you, scheme and all. A bare token is still sent as `Bearer`, so nothing already
20+
working needs to change.
21+
1122
### Coworkers are made in a wizard and managed in a dialog
1223

1324
Creating a coworker is now a three-step wizard — who it is, who may see it, then where it runs,

‎server/src/plugins/mcp.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,22 @@ function vendorFailure(error: unknown): string {
198198
return error instanceof Error ? error.message : String(error);
199199
}
200200

201+
/**
202+
* The Authorization header a stored token becomes.
203+
*
204+
* Bearer by default, which is what an MCP server's own token usually is. A token that already
205+
* names its scheme is sent as written, because some vendors forward the header straight to an API
206+
* that only speaks Basic: DataForSEO's hosted server answers the handshake and the tool listing to
207+
* anything, then returns 401 on every real call made with Bearer, so a deployment that could only
208+
* say Bearer looked connected and never worked. The scheme travels with the credential rather than
209+
* as a setting on the server row, so rotating a token can change how it is presented and nothing
210+
* else has to know.
211+
*/
212+
export function authorizationHeader(token: string): string {
213+
const trimmed = token.trim();
214+
return /^(basic|bearer)\s+\S/i.test(trimmed) ? trimmed : `Bearer ${trimmed}`;
215+
}
216+
201217
/**
202218
* Build, use and close a client.
203219
*
@@ -210,7 +226,7 @@ async function withClient<T>(
210226
): Promise<T> {
211227
const transport = new StreamableHTTPClientTransport(new URL(connection.url), {
212228
requestInit: connection.token
213-
? { headers: { Authorization: `Bearer ${connection.token}` } }
229+
? { headers: { Authorization: authorizationHeader(connection.token) } }
214230
: undefined,
215231
});
216232
const client = new Client({ name: "openbot", version: "1.0.0" });
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { describe, expect, test } from "bun:test";
2+
import { authorizationHeader } from "../src/plugins/mcp";
3+
4+
describe("authorizationHeader", () => {
5+
test("a bare token is sent as Bearer", () => {
6+
expect(authorizationHeader("abc123")).toBe("Bearer abc123");
7+
});
8+
test("a token that names Basic is sent as written", () => {
9+
expect(authorizationHeader("Basic dXNlcjpwYXNz")).toBe(
10+
"Basic dXNlcjpwYXNz",
11+
);
12+
});
13+
test("a token that names Bearer is not doubled", () => {
14+
expect(authorizationHeader("Bearer abc123")).toBe("Bearer abc123");
15+
});
16+
test("the scheme is matched without regard to case, and whitespace is trimmed", () => {
17+
expect(authorizationHeader(" basic dXNlcjpwYXNz ")).toBe(
18+
"basic dXNlcjpwYXNz",
19+
);
20+
});
21+
test("a scheme word with nothing after it is treated as a bare token", () => {
22+
expect(authorizationHeader("Basic")).toBe("Bearer Basic");
23+
});
24+
});

0 commit comments

Comments
 (0)