Skip to content

Commit 1c6c9b7

Browse files
committed
fix merge :(
1 parent afe137a commit 1c6c9b7

File tree

4 files changed

+36
-209
lines changed

4 files changed

+36
-209
lines changed

src/client/sse.test.ts

Lines changed: 28 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -294,15 +294,34 @@ describe('SSEClientTransport', () => {
294294
expect(lastServerRequest.headers.authorization).toBe(authToken);
295295
});
296296

297-
it('passes custom headers to fetch requests', async () => {
298-
const customHeaders = {
299-
Authorization: 'Bearer test-token',
300-
'X-Custom-Header': 'custom-value'
301-
};
302-
297+
it.each([
298+
{
299+
description: 'plain object headers',
300+
headers: {
301+
Authorization: 'Bearer test-token',
302+
'X-Custom-Header': 'custom-value'
303+
}
304+
},
305+
{
306+
description: 'Headers object',
307+
headers: ((): HeadersInit => {
308+
const h = new Headers();
309+
h.set('Authorization', 'Bearer test-token');
310+
h.set('X-Custom-Header', 'custom-value');
311+
return h;
312+
})()
313+
},
314+
{
315+
description: 'array of tuples',
316+
headers: ((): HeadersInit => [
317+
['Authorization', 'Bearer test-token'],
318+
['X-Custom-Header', 'custom-value']
319+
])()
320+
}
321+
])('passes custom headers to fetch requests ($description)', async ({ headers }) => {
303322
transport = new SSEClientTransport(resourceBaseUrl, {
304323
requestInit: {
305-
headers: customHeaders
324+
headers
306325
}
307326
});
308327

@@ -335,8 +354,8 @@ describe('SSEClientTransport', () => {
335354
);
336355

337356
const calledHeaders = (global.fetch as Mock).mock.calls[0][1].headers;
338-
expect(calledHeaders.get('Authorization')).toBe(customHeaders.Authorization);
339-
expect(calledHeaders.get('X-Custom-Header')).toBe(customHeaders['X-Custom-Header']);
357+
expect(calledHeaders.get('Authorization')).toBe('Bearer test-token');
358+
expect(calledHeaders.get('X-Custom-Header')).toBe('custom-value');
340359
expect(calledHeaders.get('content-type')).toBe('application/json');
341360
} finally {
342361
// Restore original fetch
@@ -345,36 +364,6 @@ describe('SSEClientTransport', () => {
345364
});
346365
});
347366

348-
it.each([
349-
{
350-
description: "plain object headers",
351-
headers: {
352-
Authorization: "Bearer test-token",
353-
"X-Custom-Header": "custom-value",
354-
},
355-
},
356-
{
357-
description: "Headers object",
358-
headers: ((): HeadersInit => {
359-
const h = new Headers();
360-
h.set("Authorization", "Bearer test-token");
361-
h.set("X-Custom-Header", "custom-value");
362-
return h;
363-
})(),
364-
},
365-
{
366-
description: "array of tuples",
367-
headers: ((): HeadersInit => ([
368-
["Authorization", "Bearer test-token"],
369-
["X-Custom-Header", "custom-value"],
370-
]))(),
371-
},
372-
])("passes custom headers to fetch requests ($description)", async ({ headers }) => {
373-
transport = new SSEClientTransport(resourceBaseUrl, {
374-
requestInit: {
375-
headers,
376-
},
377-
});
378367
describe('auth handling', () => {
379368
const authServerMetadataUrls = ['/.well-known/oauth-authorization-server', '/.well-known/openid-configuration'];
380369

@@ -430,49 +419,6 @@ describe('SSEClientTransport', () => {
430419
}
431420
});
432421

433-
await transport.send(message);
434-
435-
// Verify fetch was called with correct headers
436-
expect(global.fetch).toHaveBeenCalledWith(
437-
expect.any(URL),
438-
expect.objectContaining({
439-
headers: expect.any(Headers),
440-
}),
441-
);
442-
443-
const calledHeaders = (global.fetch as jest.Mock).mock.calls[0][1]
444-
.headers;
445-
expect(calledHeaders.get("Authorization")).toBe("Bearer test-token");
446-
expect(calledHeaders.get("X-Custom-Header")).toBe("custom-value");
447-
expect(calledHeaders.get("content-type")).toBe("application/json");
448-
} finally {
449-
// Restore original fetch
450-
global.fetch = originalFetch;
451-
}
452-
});
453-
});
454-
455-
describe("auth handling", () => {
456-
const authServerMetadataUrls = [
457-
"/.well-known/oauth-authorization-server",
458-
"/.well-known/openid-configuration",
459-
];
460-
461-
let mockAuthProvider: jest.Mocked<OAuthClientProvider>;
462-
463-
beforeEach(() => {
464-
mockAuthProvider = {
465-
get redirectUrl() { return "http://localhost/callback"; },
466-
get clientMetadata() { return { redirect_uris: ["http://localhost/callback"] }; },
467-
clientInformation: jest.fn(() => ({ client_id: "test-client-id", client_secret: "test-client-secret" })),
468-
tokens: jest.fn(),
469-
saveTokens: jest.fn(),
470-
redirectToAuthorization: jest.fn(),
471-
saveCodeVerifier: jest.fn(),
472-
codeVerifier: jest.fn(),
473-
invalidateCredentials: jest.fn(),
474-
};
475-
});
476422
await transport.start();
477423

478424
expect(lastServerRequest.headers.authorization).toBe('Bearer test-token');

src/client/sse.ts

Lines changed: 8 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1-
import { EventSource, type ErrorEvent, type EventSourceInit } from "eventsource";
2-
import { Transport, FetchLike } from "../shared/transport.js";
3-
import { JSONRPCMessage, JSONRPCMessageSchema } from "../types.js";
4-
import { auth, AuthResult, extractResourceMetadataUrl, OAuthClientProvider, UnauthorizedError } from "./auth.js";
5-
import { normalizeHeaders } from "../shared/headers.js";
61
import { EventSource, type ErrorEvent, type EventSourceInit } from 'eventsource';
7-
import { Transport, FetchLike, createFetchWithInit } from '../shared/transport.js';
2+
import { Transport, FetchLike, createFetchWithInit, normalizeHeaders } from '../shared/transport.js';
83
import { JSONRPCMessage, JSONRPCMessageSchema } from '../types.js';
94
import { auth, AuthResult, extractWWWAuthenticateParams, OAuthClientProvider, UnauthorizedError } from './auth.js';
105

@@ -93,59 +88,6 @@ export class SSEClientTransport implements Transport {
9388
this._fetchWithInit = createFetchWithInit(opts?.fetch, opts?.requestInit);
9489
}
9590

96-
return await this._startOrAuth();
97-
}
98-
99-
private async _commonHeaders(): Promise<Headers> {
100-
const headers: HeadersInit & Record<string, string> = {};
101-
if (this._authProvider) {
102-
const tokens = await this._authProvider.tokens();
103-
if (tokens) {
104-
headers["Authorization"] = `Bearer ${tokens.access_token}`;
105-
}
106-
}
107-
if (this._protocolVersion) {
108-
headers["mcp-protocol-version"] = this._protocolVersion;
109-
}
110-
111-
const extraHeaders = normalizeHeaders(this._requestInit?.headers);
112-
113-
return new Headers({
114-
...headers,
115-
...extraHeaders,
116-
});
117-
}
118-
119-
private _startOrAuth(): Promise<void> {
120-
const fetchImpl = (this?._eventSourceInit?.fetch ?? this._fetch ?? fetch) as typeof fetch
121-
return new Promise((resolve, reject) => {
122-
this._eventSource = new EventSource(
123-
this._url.href,
124-
{
125-
...this._eventSourceInit,
126-
fetch: async (url, init) => {
127-
const headers = await this._commonHeaders();
128-
headers.set("Accept", "text/event-stream");
129-
const response = await fetchImpl(url, {
130-
...init,
131-
headers,
132-
})
133-
134-
if (response.status === 401 && response.headers.has('www-authenticate')) {
135-
this._resourceMetadataUrl = extractResourceMetadataUrl(response);
136-
}
137-
138-
return response
139-
},
140-
},
141-
);
142-
this._abortController = new AbortController();
143-
144-
this._eventSource.onerror = (event) => {
145-
if (event.code === 401 && this._authProvider) {
146-
147-
this._authThenStart().then(resolve, reject);
148-
return;
14991
private async _authThenStart(): Promise<void> {
15092
if (!this._authProvider) {
15193
throw new UnauthorizedError('No auth provider');
@@ -172,7 +114,7 @@ export class SSEClientTransport implements Transport {
172114
}
173115

174116
private async _commonHeaders(): Promise<Headers> {
175-
const headers: HeadersInit = {};
117+
const headers: HeadersInit & Record<string, string> = {};
176118
if (this._authProvider) {
177119
const tokens = await this._authProvider.tokens();
178120
if (tokens) {
@@ -183,7 +125,12 @@ export class SSEClientTransport implements Transport {
183125
headers['mcp-protocol-version'] = this._protocolVersion;
184126
}
185127

186-
return new Headers({ ...headers, ...this._requestInit?.headers });
128+
const extraHeaders = normalizeHeaders(this._requestInit?.headers);
129+
130+
return new Headers({
131+
...headers,
132+
...extraHeaders
133+
});
187134
}
188135

189136
private _startOrAuth(): Promise<void> {

src/client/streamableHttp.ts

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
import { Transport, FetchLike } from "../shared/transport.js";
2-
import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResponse, JSONRPCMessage, JSONRPCMessageSchema } from "../types.js";
3-
import { auth, AuthResult, extractResourceMetadataUrl, OAuthClientProvider, UnauthorizedError } from "./auth.js";
4-
import { EventSourceParserStream } from "eventsource-parser/stream";
5-
import { normalizeHeaders } from "../shared/headers.js";
61
import { Transport, FetchLike, createFetchWithInit, normalizeHeaders } from '../shared/transport.js';
72
import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResponse, JSONRPCMessage, JSONRPCMessageSchema } from '../types.js';
83
import { auth, AuthResult, extractWWWAuthenticateParams, OAuthClientProvider, UnauthorizedError } from './auth.js';
@@ -183,38 +178,6 @@ export class StreamableHTTPClientTransport implements Transport {
183178
return await this._startOrAuthSse({ resumptionToken: undefined });
184179
}
185180

186-
const extraHeaders = normalizeHeaders(this._requestInit?.headers);
187-
188-
return new Headers({
189-
...headers,
190-
...extraHeaders,
191-
});
192-
}
193-
194-
195-
private async _startOrAuthSse(options: StartSSEOptions): Promise<void> {
196-
const { resumptionToken } = options;
197-
try {
198-
// Try to open an initial SSE stream with GET to listen for server messages
199-
// This is optional according to the spec - server may not support it
200-
const headers = await this._commonHeaders();
201-
headers.set("Accept", "text/event-stream");
202-
203-
// Include Last-Event-ID header for resumable streams if provided
204-
if (resumptionToken) {
205-
headers.set("last-event-id", resumptionToken);
206-
}
207-
208-
const response = await (this._fetch ?? fetch)(this._url, {
209-
method: "GET",
210-
headers,
211-
signal: this._abortController?.signal,
212-
});
213-
214-
if (!response.ok) {
215-
if (response.status === 401 && this._authProvider) {
216-
// Need to authenticate
217-
return await this._authThenStart();
218181
private async _commonHeaders(): Promise<Headers> {
219182
const headers: HeadersInit & Record<string, string> = {};
220183
if (this._authProvider) {
@@ -259,20 +222,6 @@ export class StreamableHTTPClientTransport implements Transport {
259222
signal: this._abortController?.signal
260223
});
261224

262-
/**
263-
* Schedule a reconnection attempt with exponential backoff
264-
*
265-
* @param lastEventId The ID of the last received event for resumability
266-
* @param attemptCount Current reconnection attempt count for this specific stream
267-
*/
268-
private _scheduleReconnection(options: StartSSEOptions, attemptCount = 0): void {
269-
// Use provided options or default options
270-
const maxRetries = this._reconnectionOptions.maxRetries;
271-
272-
// Check if we've exceeded maximum retry attempts
273-
if (maxRetries > 0 && attemptCount >= maxRetries) {
274-
this.onerror?.(new Error(`Maximum reconnection attempts (${maxRetries}) exceeded.`));
275-
return;
276225
if (!response.ok) {
277226
await response.body?.cancel();
278227

src/shared/headers.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)