avancement planning
This commit is contained in:
+73
-51
@@ -1,6 +1,7 @@
|
||||
import { EventSource } from "eventsource";
|
||||
import { JSONRPCMessageSchema } from "../types.js";
|
||||
import { auth, extractResourceMetadataUrl, UnauthorizedError } from "./auth.js";
|
||||
import { EventSource } from 'eventsource';
|
||||
import { createFetchWithInit, normalizeHeaders } from '../shared/transport.js';
|
||||
import { JSONRPCMessageSchema } from '../types.js';
|
||||
import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js';
|
||||
export class SseError extends Error {
|
||||
constructor(code, message, event) {
|
||||
super(`SSE error: ${message}`);
|
||||
@@ -11,83 +12,92 @@ export class SseError extends Error {
|
||||
/**
|
||||
* Client transport for SSE: this will connect to a server using Server-Sent Events for receiving
|
||||
* messages and make separate POST requests for sending messages.
|
||||
* @deprecated SSEClientTransport is deprecated. Prefer to use StreamableHTTPClientTransport where possible instead. Note that because some servers are still using SSE, clients may need to support both transports during the migration period.
|
||||
*/
|
||||
export class SSEClientTransport {
|
||||
constructor(url, opts) {
|
||||
this._url = url;
|
||||
this._resourceMetadataUrl = undefined;
|
||||
this._eventSourceInit = opts === null || opts === void 0 ? void 0 : opts.eventSourceInit;
|
||||
this._requestInit = opts === null || opts === void 0 ? void 0 : opts.requestInit;
|
||||
this._authProvider = opts === null || opts === void 0 ? void 0 : opts.authProvider;
|
||||
this._fetch = opts === null || opts === void 0 ? void 0 : opts.fetch;
|
||||
this._scope = undefined;
|
||||
this._eventSourceInit = opts?.eventSourceInit;
|
||||
this._requestInit = opts?.requestInit;
|
||||
this._authProvider = opts?.authProvider;
|
||||
this._fetch = opts?.fetch;
|
||||
this._fetchWithInit = createFetchWithInit(opts?.fetch, opts?.requestInit);
|
||||
}
|
||||
async _authThenStart() {
|
||||
var _a;
|
||||
if (!this._authProvider) {
|
||||
throw new UnauthorizedError("No auth provider");
|
||||
throw new UnauthorizedError('No auth provider');
|
||||
}
|
||||
let result;
|
||||
try {
|
||||
result = await auth(this._authProvider, { serverUrl: this._url, resourceMetadataUrl: this._resourceMetadataUrl, fetchFn: this._fetch });
|
||||
result = await auth(this._authProvider, {
|
||||
serverUrl: this._url,
|
||||
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||
scope: this._scope,
|
||||
fetchFn: this._fetchWithInit
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error);
|
||||
this.onerror?.(error);
|
||||
throw error;
|
||||
}
|
||||
if (result !== "AUTHORIZED") {
|
||||
if (result !== 'AUTHORIZED') {
|
||||
throw new UnauthorizedError();
|
||||
}
|
||||
return await this._startOrAuth();
|
||||
}
|
||||
async _commonHeaders() {
|
||||
var _a;
|
||||
const headers = {};
|
||||
if (this._authProvider) {
|
||||
const tokens = await this._authProvider.tokens();
|
||||
if (tokens) {
|
||||
headers["Authorization"] = `Bearer ${tokens.access_token}`;
|
||||
headers['Authorization'] = `Bearer ${tokens.access_token}`;
|
||||
}
|
||||
}
|
||||
if (this._protocolVersion) {
|
||||
headers["mcp-protocol-version"] = this._protocolVersion;
|
||||
headers['mcp-protocol-version'] = this._protocolVersion;
|
||||
}
|
||||
return new Headers({ ...headers, ...(_a = this._requestInit) === null || _a === void 0 ? void 0 : _a.headers });
|
||||
const extraHeaders = normalizeHeaders(this._requestInit?.headers);
|
||||
return new Headers({
|
||||
...headers,
|
||||
...extraHeaders
|
||||
});
|
||||
}
|
||||
_startOrAuth() {
|
||||
var _a, _b, _c;
|
||||
const fetchImpl = ((_c = (_b = (_a = this === null || this === void 0 ? void 0 : this._eventSourceInit) === null || _a === void 0 ? void 0 : _a.fetch) !== null && _b !== void 0 ? _b : this._fetch) !== null && _c !== void 0 ? _c : fetch);
|
||||
const fetchImpl = (this?._eventSourceInit?.fetch ?? this._fetch ?? fetch);
|
||||
return new Promise((resolve, reject) => {
|
||||
this._eventSource = new EventSource(this._url.href, {
|
||||
...this._eventSourceInit,
|
||||
fetch: async (url, init) => {
|
||||
const headers = await this._commonHeaders();
|
||||
headers.set("Accept", "text/event-stream");
|
||||
headers.set('Accept', 'text/event-stream');
|
||||
const response = await fetchImpl(url, {
|
||||
...init,
|
||||
headers,
|
||||
headers
|
||||
});
|
||||
if (response.status === 401 && response.headers.has('www-authenticate')) {
|
||||
this._resourceMetadataUrl = extractResourceMetadataUrl(response);
|
||||
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
|
||||
this._resourceMetadataUrl = resourceMetadataUrl;
|
||||
this._scope = scope;
|
||||
}
|
||||
return response;
|
||||
},
|
||||
}
|
||||
});
|
||||
this._abortController = new AbortController();
|
||||
this._eventSource.onerror = (event) => {
|
||||
var _a;
|
||||
this._eventSource.onerror = event => {
|
||||
if (event.code === 401 && this._authProvider) {
|
||||
this._authThenStart().then(resolve, reject);
|
||||
return;
|
||||
}
|
||||
const error = new SseError(event.code, event.message, event);
|
||||
reject(error);
|
||||
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error);
|
||||
this.onerror?.(error);
|
||||
};
|
||||
this._eventSource.onopen = () => {
|
||||
// The connection is open, but we need to wait for the endpoint to be received.
|
||||
};
|
||||
this._eventSource.addEventListener("endpoint", (event) => {
|
||||
var _a;
|
||||
this._eventSource.addEventListener('endpoint', (event) => {
|
||||
const messageEvent = event;
|
||||
try {
|
||||
this._endpoint = new URL(messageEvent.data, this._url);
|
||||
@@ -97,30 +107,29 @@ export class SSEClientTransport {
|
||||
}
|
||||
catch (error) {
|
||||
reject(error);
|
||||
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error);
|
||||
this.onerror?.(error);
|
||||
void this.close();
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
this._eventSource.onmessage = (event) => {
|
||||
var _a, _b;
|
||||
const messageEvent = event;
|
||||
let message;
|
||||
try {
|
||||
message = JSONRPCMessageSchema.parse(JSON.parse(messageEvent.data));
|
||||
}
|
||||
catch (error) {
|
||||
(_a = this.onerror) === null || _a === void 0 ? void 0 : _a.call(this, error);
|
||||
this.onerror?.(error);
|
||||
return;
|
||||
}
|
||||
(_b = this.onmessage) === null || _b === void 0 ? void 0 : _b.call(this, message);
|
||||
this.onmessage?.(message);
|
||||
};
|
||||
});
|
||||
}
|
||||
async start() {
|
||||
if (this._eventSource) {
|
||||
throw new Error("SSEClientTransport already started! If using Client class, note that connect() calls start() automatically.");
|
||||
throw new Error('SSEClientTransport already started! If using Client class, note that connect() calls start() automatically.');
|
||||
}
|
||||
return await this._startOrAuth();
|
||||
}
|
||||
@@ -129,51 +138,64 @@ export class SSEClientTransport {
|
||||
*/
|
||||
async finishAuth(authorizationCode) {
|
||||
if (!this._authProvider) {
|
||||
throw new UnauthorizedError("No auth provider");
|
||||
throw new UnauthorizedError('No auth provider');
|
||||
}
|
||||
const result = await auth(this._authProvider, { serverUrl: this._url, authorizationCode, resourceMetadataUrl: this._resourceMetadataUrl, fetchFn: this._fetch });
|
||||
if (result !== "AUTHORIZED") {
|
||||
throw new UnauthorizedError("Failed to authorize");
|
||||
const result = await auth(this._authProvider, {
|
||||
serverUrl: this._url,
|
||||
authorizationCode,
|
||||
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||
scope: this._scope,
|
||||
fetchFn: this._fetchWithInit
|
||||
});
|
||||
if (result !== 'AUTHORIZED') {
|
||||
throw new UnauthorizedError('Failed to authorize');
|
||||
}
|
||||
}
|
||||
async close() {
|
||||
var _a, _b, _c;
|
||||
(_a = this._abortController) === null || _a === void 0 ? void 0 : _a.abort();
|
||||
(_b = this._eventSource) === null || _b === void 0 ? void 0 : _b.close();
|
||||
(_c = this.onclose) === null || _c === void 0 ? void 0 : _c.call(this);
|
||||
this._abortController?.abort();
|
||||
this._eventSource?.close();
|
||||
this.onclose?.();
|
||||
}
|
||||
async send(message) {
|
||||
var _a, _b, _c;
|
||||
if (!this._endpoint) {
|
||||
throw new Error("Not connected");
|
||||
throw new Error('Not connected');
|
||||
}
|
||||
try {
|
||||
const headers = await this._commonHeaders();
|
||||
headers.set("content-type", "application/json");
|
||||
headers.set('content-type', 'application/json');
|
||||
const init = {
|
||||
...this._requestInit,
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(message),
|
||||
signal: (_a = this._abortController) === null || _a === void 0 ? void 0 : _a.signal,
|
||||
signal: this._abortController?.signal
|
||||
};
|
||||
const response = await ((_b = this._fetch) !== null && _b !== void 0 ? _b : fetch)(this._endpoint, init);
|
||||
const response = await (this._fetch ?? fetch)(this._endpoint, init);
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => null);
|
||||
if (response.status === 401 && this._authProvider) {
|
||||
this._resourceMetadataUrl = extractResourceMetadataUrl(response);
|
||||
const result = await auth(this._authProvider, { serverUrl: this._url, resourceMetadataUrl: this._resourceMetadataUrl, fetchFn: this._fetch });
|
||||
if (result !== "AUTHORIZED") {
|
||||
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
|
||||
this._resourceMetadataUrl = resourceMetadataUrl;
|
||||
this._scope = scope;
|
||||
const result = await auth(this._authProvider, {
|
||||
serverUrl: this._url,
|
||||
resourceMetadataUrl: this._resourceMetadataUrl,
|
||||
scope: this._scope,
|
||||
fetchFn: this._fetchWithInit
|
||||
});
|
||||
if (result !== 'AUTHORIZED') {
|
||||
throw new UnauthorizedError();
|
||||
}
|
||||
// Purposely _not_ awaited, so we don't call onerror twice
|
||||
return this.send(message);
|
||||
}
|
||||
const text = await response.text().catch(() => null);
|
||||
throw new Error(`Error POSTing to endpoint (HTTP ${response.status}): ${text}`);
|
||||
}
|
||||
// Release connection - POST responses don't have content we need
|
||||
await response.body?.cancel();
|
||||
}
|
||||
catch (error) {
|
||||
(_c = this.onerror) === null || _c === void 0 ? void 0 : _c.call(this, error);
|
||||
this.onerror?.(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user