| "use strict";
|
| Object.defineProperty(exports, "__esModule", { value: true });
|
| exports.StreamableHTTPClientTransport = exports.StreamableHTTPError = void 0;
|
| const transport_js_1 = require("../shared/transport.js");
|
| const types_js_1 = require("../types.js");
|
| const auth_js_1 = require("./auth.js");
|
| const stream_1 = require("eventsource-parser/stream");
|
|
|
| const DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS = {
|
| initialReconnectionDelay: 1000,
|
| maxReconnectionDelay: 30000,
|
| reconnectionDelayGrowFactor: 1.5,
|
| maxRetries: 2
|
| };
|
| class StreamableHTTPError extends Error {
|
| constructor(code, message) {
|
| super(`Streamable HTTP error: ${message}`);
|
| this.code = code;
|
| }
|
| }
|
| exports.StreamableHTTPError = StreamableHTTPError;
|
| |
| |
| |
| |
|
|
| class StreamableHTTPClientTransport {
|
| constructor(url, opts) {
|
| this._hasCompletedAuthFlow = false;
|
| this._url = url;
|
| this._resourceMetadataUrl = undefined;
|
| this._scope = undefined;
|
| this._requestInit = opts?.requestInit;
|
| this._authProvider = opts?.authProvider;
|
| this._fetch = opts?.fetch;
|
| this._fetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, opts?.requestInit);
|
| this._sessionId = opts?.sessionId;
|
| this._reconnectionOptions = opts?.reconnectionOptions ?? DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS;
|
| }
|
| async _authThenStart() {
|
| if (!this._authProvider) {
|
| throw new auth_js_1.UnauthorizedError('No auth provider');
|
| }
|
| let result;
|
| try {
|
| result = await (0, auth_js_1.auth)(this._authProvider, {
|
| serverUrl: this._url,
|
| resourceMetadataUrl: this._resourceMetadataUrl,
|
| scope: this._scope,
|
| fetchFn: this._fetchWithInit
|
| });
|
| }
|
| catch (error) {
|
| this.onerror?.(error);
|
| throw error;
|
| }
|
| if (result !== 'AUTHORIZED') {
|
| throw new auth_js_1.UnauthorizedError();
|
| }
|
| return await this._startOrAuthSse({ resumptionToken: undefined });
|
| }
|
| async _commonHeaders() {
|
| const headers = {};
|
| if (this._authProvider) {
|
| const tokens = await this._authProvider.tokens();
|
| if (tokens) {
|
| headers['Authorization'] = `Bearer ${tokens.access_token}`;
|
| }
|
| }
|
| if (this._sessionId) {
|
| headers['mcp-session-id'] = this._sessionId;
|
| }
|
| if (this._protocolVersion) {
|
| headers['mcp-protocol-version'] = this._protocolVersion;
|
| }
|
| const extraHeaders = (0, transport_js_1.normalizeHeaders)(this._requestInit?.headers);
|
| return new Headers({
|
| ...headers,
|
| ...extraHeaders
|
| });
|
| }
|
| async _startOrAuthSse(options) {
|
| const { resumptionToken } = options;
|
| try {
|
|
|
|
|
| const headers = await this._commonHeaders();
|
| headers.set('Accept', 'text/event-stream');
|
|
|
| if (resumptionToken) {
|
| headers.set('last-event-id', resumptionToken);
|
| }
|
| const response = await (this._fetch ?? fetch)(this._url, {
|
| method: 'GET',
|
| headers,
|
| signal: this._abortController?.signal
|
| });
|
| if (!response.ok) {
|
| await response.body?.cancel();
|
| if (response.status === 401 && this._authProvider) {
|
|
|
| return await this._authThenStart();
|
| }
|
|
|
|
|
| if (response.status === 405) {
|
| return;
|
| }
|
| throw new StreamableHTTPError(response.status, `Failed to open SSE stream: ${response.statusText}`);
|
| }
|
| this._handleSseStream(response.body, options, true);
|
| }
|
| catch (error) {
|
| this.onerror?.(error);
|
| throw error;
|
| }
|
| }
|
| |
| |
| |
| |
| |
|
|
| _getNextReconnectionDelay(attempt) {
|
|
|
| if (this._serverRetryMs !== undefined) {
|
| return this._serverRetryMs;
|
| }
|
|
|
| const initialDelay = this._reconnectionOptions.initialReconnectionDelay;
|
| const growFactor = this._reconnectionOptions.reconnectionDelayGrowFactor;
|
| const maxDelay = this._reconnectionOptions.maxReconnectionDelay;
|
|
|
| return Math.min(initialDelay * Math.pow(growFactor, attempt), maxDelay);
|
| }
|
| |
| |
| |
| |
| |
|
|
| _scheduleReconnection(options, attemptCount = 0) {
|
|
|
| const maxRetries = this._reconnectionOptions.maxRetries;
|
|
|
| if (attemptCount >= maxRetries) {
|
| this.onerror?.(new Error(`Maximum reconnection attempts (${maxRetries}) exceeded.`));
|
| return;
|
| }
|
|
|
| const delay = this._getNextReconnectionDelay(attemptCount);
|
|
|
| this._reconnectionTimeout = setTimeout(() => {
|
|
|
| this._startOrAuthSse(options).catch(error => {
|
| this.onerror?.(new Error(`Failed to reconnect SSE stream: ${error instanceof Error ? error.message : String(error)}`));
|
|
|
| this._scheduleReconnection(options, attemptCount + 1);
|
| });
|
| }, delay);
|
| }
|
| _handleSseStream(stream, options, isReconnectable) {
|
| if (!stream) {
|
| return;
|
| }
|
| const { onresumptiontoken, replayMessageId } = options;
|
| let lastEventId;
|
|
|
|
|
| let hasPrimingEvent = false;
|
|
|
|
|
| let receivedResponse = false;
|
| const processStream = async () => {
|
|
|
|
|
| try {
|
|
|
| const reader = stream
|
| .pipeThrough(new TextDecoderStream())
|
| .pipeThrough(new stream_1.EventSourceParserStream({
|
| onRetry: (retryMs) => {
|
|
|
| this._serverRetryMs = retryMs;
|
| }
|
| }))
|
| .getReader();
|
| while (true) {
|
| const { value: event, done } = await reader.read();
|
| if (done) {
|
| break;
|
| }
|
|
|
| if (event.id) {
|
| lastEventId = event.id;
|
|
|
| hasPrimingEvent = true;
|
| onresumptiontoken?.(event.id);
|
| }
|
|
|
| if (!event.data) {
|
| continue;
|
| }
|
| if (!event.event || event.event === 'message') {
|
| try {
|
| const message = types_js_1.JSONRPCMessageSchema.parse(JSON.parse(event.data));
|
| if ((0, types_js_1.isJSONRPCResultResponse)(message)) {
|
|
|
| receivedResponse = true;
|
| if (replayMessageId !== undefined) {
|
| message.id = replayMessageId;
|
| }
|
| }
|
| this.onmessage?.(message);
|
| }
|
| catch (error) {
|
| this.onerror?.(error);
|
| }
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| const canResume = isReconnectable || hasPrimingEvent;
|
| const needsReconnect = canResume && !receivedResponse;
|
| if (needsReconnect && this._abortController && !this._abortController.signal.aborted) {
|
| this._scheduleReconnection({
|
| resumptionToken: lastEventId,
|
| onresumptiontoken,
|
| replayMessageId
|
| }, 0);
|
| }
|
| }
|
| catch (error) {
|
|
|
| this.onerror?.(new Error(`SSE stream disconnected: ${error}`));
|
|
|
|
|
|
|
| const canResume = isReconnectable || hasPrimingEvent;
|
| const needsReconnect = canResume && !receivedResponse;
|
| if (needsReconnect && this._abortController && !this._abortController.signal.aborted) {
|
|
|
| try {
|
| this._scheduleReconnection({
|
| resumptionToken: lastEventId,
|
| onresumptiontoken,
|
| replayMessageId
|
| }, 0);
|
| }
|
| catch (error) {
|
| this.onerror?.(new Error(`Failed to reconnect: ${error instanceof Error ? error.message : String(error)}`));
|
| }
|
| }
|
| }
|
| };
|
| processStream();
|
| }
|
| async start() {
|
| if (this._abortController) {
|
| throw new Error('StreamableHTTPClientTransport already started! If using Client class, note that connect() calls start() automatically.');
|
| }
|
| this._abortController = new AbortController();
|
| }
|
| |
| |
|
|
| async finishAuth(authorizationCode) {
|
| if (!this._authProvider) {
|
| throw new auth_js_1.UnauthorizedError('No auth provider');
|
| }
|
| const result = await (0, auth_js_1.auth)(this._authProvider, {
|
| serverUrl: this._url,
|
| authorizationCode,
|
| resourceMetadataUrl: this._resourceMetadataUrl,
|
| scope: this._scope,
|
| fetchFn: this._fetchWithInit
|
| });
|
| if (result !== 'AUTHORIZED') {
|
| throw new auth_js_1.UnauthorizedError('Failed to authorize');
|
| }
|
| }
|
| async close() {
|
| if (this._reconnectionTimeout) {
|
| clearTimeout(this._reconnectionTimeout);
|
| this._reconnectionTimeout = undefined;
|
| }
|
| this._abortController?.abort();
|
| this.onclose?.();
|
| }
|
| async send(message, options) {
|
| try {
|
| const { resumptionToken, onresumptiontoken } = options || {};
|
| if (resumptionToken) {
|
|
|
| this._startOrAuthSse({ resumptionToken, replayMessageId: (0, types_js_1.isJSONRPCRequest)(message) ? message.id : undefined }).catch(err => this.onerror?.(err));
|
| return;
|
| }
|
| const headers = await this._commonHeaders();
|
| headers.set('content-type', 'application/json');
|
| headers.set('accept', 'application/json, text/event-stream');
|
| const init = {
|
| ...this._requestInit,
|
| method: 'POST',
|
| headers,
|
| body: JSON.stringify(message),
|
| signal: this._abortController?.signal
|
| };
|
| const response = await (this._fetch ?? fetch)(this._url, init);
|
|
|
| const sessionId = response.headers.get('mcp-session-id');
|
| if (sessionId) {
|
| this._sessionId = sessionId;
|
| }
|
| if (!response.ok) {
|
| const text = await response.text().catch(() => null);
|
| if (response.status === 401 && this._authProvider) {
|
|
|
| if (this._hasCompletedAuthFlow) {
|
| throw new StreamableHTTPError(401, 'Server returned 401 after successful authentication');
|
| }
|
| const { resourceMetadataUrl, scope } = (0, auth_js_1.extractWWWAuthenticateParams)(response);
|
| this._resourceMetadataUrl = resourceMetadataUrl;
|
| this._scope = scope;
|
| const result = await (0, auth_js_1.auth)(this._authProvider, {
|
| serverUrl: this._url,
|
| resourceMetadataUrl: this._resourceMetadataUrl,
|
| scope: this._scope,
|
| fetchFn: this._fetchWithInit
|
| });
|
| if (result !== 'AUTHORIZED') {
|
| throw new auth_js_1.UnauthorizedError();
|
| }
|
|
|
| this._hasCompletedAuthFlow = true;
|
|
|
| return this.send(message);
|
| }
|
| if (response.status === 403 && this._authProvider) {
|
| const { resourceMetadataUrl, scope, error } = (0, auth_js_1.extractWWWAuthenticateParams)(response);
|
| if (error === 'insufficient_scope') {
|
| const wwwAuthHeader = response.headers.get('WWW-Authenticate');
|
|
|
| if (this._lastUpscopingHeader === wwwAuthHeader) {
|
| throw new StreamableHTTPError(403, 'Server returned 403 after trying upscoping');
|
| }
|
| if (scope) {
|
| this._scope = scope;
|
| }
|
| if (resourceMetadataUrl) {
|
| this._resourceMetadataUrl = resourceMetadataUrl;
|
| }
|
|
|
| this._lastUpscopingHeader = wwwAuthHeader ?? undefined;
|
| const result = await (0, auth_js_1.auth)(this._authProvider, {
|
| serverUrl: this._url,
|
| resourceMetadataUrl: this._resourceMetadataUrl,
|
| scope: this._scope,
|
| fetchFn: this._fetch
|
| });
|
| if (result !== 'AUTHORIZED') {
|
| throw new auth_js_1.UnauthorizedError();
|
| }
|
| return this.send(message);
|
| }
|
| }
|
| throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
|
| }
|
|
|
| this._hasCompletedAuthFlow = false;
|
| this._lastUpscopingHeader = undefined;
|
|
|
| if (response.status === 202) {
|
| await response.body?.cancel();
|
|
|
|
|
| if ((0, types_js_1.isInitializedNotification)(message)) {
|
|
|
| this._startOrAuthSse({ resumptionToken: undefined }).catch(err => this.onerror?.(err));
|
| }
|
| return;
|
| }
|
|
|
| const messages = Array.isArray(message) ? message : [message];
|
| const hasRequests = messages.filter(msg => 'method' in msg && 'id' in msg && msg.id !== undefined).length > 0;
|
|
|
| const contentType = response.headers.get('content-type');
|
| if (hasRequests) {
|
| if (contentType?.includes('text/event-stream')) {
|
|
|
|
|
|
|
| this._handleSseStream(response.body, { onresumptiontoken }, false);
|
| }
|
| else if (contentType?.includes('application/json')) {
|
|
|
| const data = await response.json();
|
| const responseMessages = Array.isArray(data)
|
| ? data.map(msg => types_js_1.JSONRPCMessageSchema.parse(msg))
|
| : [types_js_1.JSONRPCMessageSchema.parse(data)];
|
| for (const msg of responseMessages) {
|
| this.onmessage?.(msg);
|
| }
|
| }
|
| else {
|
| await response.body?.cancel();
|
| throw new StreamableHTTPError(-1, `Unexpected content type: ${contentType}`);
|
| }
|
| }
|
| else {
|
|
|
| await response.body?.cancel();
|
| }
|
| }
|
| catch (error) {
|
| this.onerror?.(error);
|
| throw error;
|
| }
|
| }
|
| get sessionId() {
|
| return this._sessionId;
|
| }
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| async terminateSession() {
|
| if (!this._sessionId) {
|
| return;
|
| }
|
| try {
|
| const headers = await this._commonHeaders();
|
| const init = {
|
| ...this._requestInit,
|
| method: 'DELETE',
|
| headers,
|
| signal: this._abortController?.signal
|
| };
|
| const response = await (this._fetch ?? fetch)(this._url, init);
|
| await response.body?.cancel();
|
|
|
|
|
| if (!response.ok && response.status !== 405) {
|
| throw new StreamableHTTPError(response.status, `Failed to terminate session: ${response.statusText}`);
|
| }
|
| this._sessionId = undefined;
|
| }
|
| catch (error) {
|
| this.onerror?.(error);
|
| throw error;
|
| }
|
| }
|
| setProtocolVersion(version) {
|
| this._protocolVersion = version;
|
| }
|
| get protocolVersion() {
|
| return this._protocolVersion;
|
| }
|
| |
| |
| |
| |
| |
| |
|
|
| async resumeStream(lastEventId, options) {
|
| await this._startOrAuthSse({
|
| resumptionToken: lastEventId,
|
| onresumptiontoken: options?.onresumptiontoken
|
| });
|
| }
|
| }
|
| exports.StreamableHTTPClientTransport = StreamableHTTPClientTransport;
|
| |