mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-23 10:45:33 -04:00
786 lines
40 KiB
Diff
786 lines
40 KiB
Diff
diff --git a/dist/cjs/client/auth.js b/dist/cjs/client/auth.js
|
|
index c2e4fa91d26f5336889f6afa416147db75fc4872..6fe80dd336252838adf47b0a8e022e55d11e3657 100644
|
|
--- a/dist/cjs/client/auth.js
|
|
+++ b/dist/cjs/client/auth.js
|
|
@@ -186,7 +186,7 @@ async function auth(provider, options) {
|
|
throw error;
|
|
}
|
|
}
|
|
-async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
|
|
+async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn, resourceMetadataFetchFn }) {
|
|
// Check if the provider has cached discovery state to skip discovery
|
|
const cachedState = await provider.discoveryState?.();
|
|
let resourceMetadata;
|
|
@@ -198,6 +198,9 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
|
|
if (!effectiveResourceMetadataUrl && cachedState?.resourceMetadataUrl) {
|
|
effectiveResourceMetadataUrl = new URL(cachedState.resourceMetadataUrl);
|
|
}
|
|
+ const protectedResourceFetchFn = !effectiveResourceMetadataUrl || effectiveResourceMetadataUrl.origin === new URL(serverUrl).origin
|
|
+ ? (resourceMetadataFetchFn ?? fetchFn)
|
|
+ : fetchFn;
|
|
if (cachedState?.authorizationServerUrl) {
|
|
// Restore discovery state from cache
|
|
authorizationServerUrl = cachedState.authorizationServerUrl;
|
|
@@ -207,7 +210,7 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
|
|
// If resource metadata wasn't cached, try to fetch it for selectResourceURL
|
|
if (!resourceMetadata) {
|
|
try {
|
|
- resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl }, fetchFn);
|
|
+ resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl }, protectedResourceFetchFn);
|
|
}
|
|
catch {
|
|
// RFC 9728 not available — selectResourceURL will handle undefined
|
|
@@ -225,7 +228,11 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
|
|
}
|
|
else {
|
|
// Full discovery via RFC 9728
|
|
- const serverInfo = await discoverOAuthServerInfo(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl, fetchFn });
|
|
+ const serverInfo = await discoverOAuthServerInfo(serverUrl, {
|
|
+ resourceMetadataUrl: effectiveResourceMetadataUrl,
|
|
+ fetchFn,
|
|
+ resourceMetadataFetchFn: protectedResourceFetchFn
|
|
+ });
|
|
authorizationServerUrl = serverInfo.authorizationServerUrl;
|
|
metadata = serverInfo.authorizationServerMetadata;
|
|
resourceMetadata = serverInfo.resourceMetadata;
|
|
@@ -688,7 +695,7 @@ async function discoverOAuthServerInfo(serverUrl, opts) {
|
|
let resourceMetadata;
|
|
let authorizationServerUrl;
|
|
try {
|
|
- resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.fetchFn);
|
|
+ resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.resourceMetadataFetchFn ?? opts?.fetchFn);
|
|
if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) {
|
|
authorizationServerUrl = resourceMetadata.authorization_servers[0];
|
|
}
|
|
diff --git a/dist/cjs/client/index.d.ts b/dist/cjs/client/index.d.ts
|
|
index 6f567a193626587a2730b5a49293ca5dfd4181ea..5b7c841c000508e389ce617f559f7c2a5126ca9f 100644
|
|
--- a/dist/cjs/client/index.d.ts
|
|
+++ b/dist/cjs/client/index.d.ts
|
|
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
|
|
*
|
|
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
|
|
*/
|
|
+ callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
|
|
+ callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
|
|
callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
|
|
[x: string]: unknown;
|
|
content: ({
|
|
diff --git a/dist/cjs/client/index.js b/dist/cjs/client/index.js
|
|
index 6ac1da14dc7f6211ae70f7711c124b76098816d8..88e58b90b673cb0f9c60920edec7d2ebb16f112b 100644
|
|
--- a/dist/cjs/client/index.js
|
|
+++ b/dist/cjs/client/index.js
|
|
@@ -288,41 +288,16 @@ class Client extends protocol_js_1.Protocol {
|
|
}
|
|
async connect(transport, options) {
|
|
await super.connect(transport);
|
|
+ transport.onsessionexpired = async () => {
|
|
+ await this._initialize(transport);
|
|
+ };
|
|
// When transport sessionId is already set this means we are trying to reconnect.
|
|
// In this case we don't need to initialize again.
|
|
if (transport.sessionId !== undefined) {
|
|
return;
|
|
}
|
|
try {
|
|
- const result = await this.request({
|
|
- method: 'initialize',
|
|
- params: {
|
|
- protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION,
|
|
- capabilities: this._capabilities,
|
|
- clientInfo: this._clientInfo
|
|
- }
|
|
- }, types_js_1.InitializeResultSchema, options);
|
|
- if (result === undefined) {
|
|
- throw new Error(`Server sent invalid initialize result: ${result}`);
|
|
- }
|
|
- if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
|
|
- throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
|
|
- }
|
|
- this._serverCapabilities = result.capabilities;
|
|
- this._serverVersion = result.serverInfo;
|
|
- // HTTP transports must set the protocol version in each header after initialization.
|
|
- if (transport.setProtocolVersion) {
|
|
- transport.setProtocolVersion(result.protocolVersion);
|
|
- }
|
|
- this._instructions = result.instructions;
|
|
- await this.notification({
|
|
- method: 'notifications/initialized'
|
|
- });
|
|
- // Set up list changed handlers now that we know server capabilities
|
|
- if (this._pendingListChangedConfig) {
|
|
- this._setupListChangedHandlers(this._pendingListChangedConfig);
|
|
- this._pendingListChangedConfig = undefined;
|
|
- }
|
|
+ await this._initialize(transport, options);
|
|
}
|
|
catch (error) {
|
|
// Disconnect if initialization fails.
|
|
@@ -330,6 +305,37 @@ class Client extends protocol_js_1.Protocol {
|
|
throw error;
|
|
}
|
|
}
|
|
+ async _initialize(transport, options) {
|
|
+ const result = await this.request({
|
|
+ method: 'initialize',
|
|
+ params: {
|
|
+ protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION,
|
|
+ capabilities: this._capabilities,
|
|
+ clientInfo: this._clientInfo
|
|
+ }
|
|
+ }, types_js_1.InitializeResultSchema, options);
|
|
+ if (result === undefined) {
|
|
+ throw new Error(`Server sent invalid initialize result: ${result}`);
|
|
+ }
|
|
+ if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
|
|
+ throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
|
|
+ }
|
|
+ this._serverCapabilities = result.capabilities;
|
|
+ this._serverVersion = result.serverInfo;
|
|
+ // HTTP transports must set the protocol version in each header after initialization.
|
|
+ if (transport.setProtocolVersion) {
|
|
+ transport.setProtocolVersion(result.protocolVersion);
|
|
+ }
|
|
+ this._instructions = result.instructions;
|
|
+ await this.notification({
|
|
+ method: 'notifications/initialized'
|
|
+ });
|
|
+ // Set up list changed handlers now that we know server capabilities
|
|
+ if (this._pendingListChangedConfig) {
|
|
+ this._setupListChangedHandlers(this._pendingListChangedConfig);
|
|
+ this._pendingListChangedConfig = undefined;
|
|
+ }
|
|
+ }
|
|
/**
|
|
* After initialization has completed, this will be populated with the server's reported capabilities.
|
|
*/
|
|
diff --git a/dist/cjs/client/sse.js b/dist/cjs/client/sse.js
|
|
index 8ca8dad652e76c299927f836cf622c0be877e1d4..14cc1b969ac51ecb6f894608f079f4f04d8acd62 100644
|
|
--- a/dist/cjs/client/sse.js
|
|
+++ b/dist/cjs/client/sse.js
|
|
@@ -27,7 +27,9 @@ class SSEClientTransport {
|
|
this._requestInit = opts?.requestInit;
|
|
this._authProvider = opts?.authProvider;
|
|
this._fetch = opts?.fetch;
|
|
- this._fetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, opts?.requestInit);
|
|
+ this._resourceFetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, { ...opts?.requestInit, redirect: 'error' });
|
|
+ const oauthRequestInit = opts?.requestInit ? { ...opts.requestInit, headers: undefined } : undefined;
|
|
+ this._fetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, oauthRequestInit);
|
|
}
|
|
async _authThenStart() {
|
|
if (!this._authProvider) {
|
|
@@ -39,7 +41,8 @@ class SSEClientTransport {
|
|
serverUrl: this._url,
|
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
scope: this._scope,
|
|
- fetchFn: this._fetchWithInit
|
|
+ fetchFn: this._fetchWithInit,
|
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
|
});
|
|
}
|
|
catch (error) {
|
|
@@ -63,10 +66,11 @@ class SSEClientTransport {
|
|
headers['mcp-protocol-version'] = this._protocolVersion;
|
|
}
|
|
const extraHeaders = (0, transport_js_1.normalizeHeaders)(this._requestInit?.headers);
|
|
- return new Headers({
|
|
- ...headers,
|
|
- ...extraHeaders
|
|
- });
|
|
+ const result = new Headers(extraHeaders);
|
|
+ for (const [name, value] of Object.entries(headers)) {
|
|
+ result.set(name, value);
|
|
+ }
|
|
+ return result;
|
|
}
|
|
_startOrAuth() {
|
|
const fetchImpl = (this?._eventSourceInit?.fetch ?? this._fetch ?? fetch);
|
|
@@ -149,7 +153,8 @@ class SSEClientTransport {
|
|
authorizationCode,
|
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
scope: this._scope,
|
|
- fetchFn: this._fetchWithInit
|
|
+ fetchFn: this._fetchWithInit,
|
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
|
});
|
|
if (result !== 'AUTHORIZED') {
|
|
throw new auth_js_1.UnauthorizedError('Failed to authorize');
|
|
@@ -185,7 +190,8 @@ class SSEClientTransport {
|
|
serverUrl: this._url,
|
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
scope: this._scope,
|
|
- fetchFn: this._fetchWithInit
|
|
+ fetchFn: this._fetchWithInit,
|
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
|
});
|
|
if (result !== 'AUTHORIZED') {
|
|
throw new auth_js_1.UnauthorizedError();
|
|
diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js
|
|
index a29a7d3a0f14d9cd800ef5b296485237350c666f..54ac759eb19df7c6f16fc031a689270acb5c3922 100644
|
|
--- a/dist/cjs/client/streamableHttp.js
|
|
+++ b/dist/cjs/client/streamableHttp.js
|
|
@@ -33,7 +33,9 @@ class StreamableHTTPClientTransport {
|
|
this._requestInit = opts?.requestInit;
|
|
this._authProvider = opts?.authProvider;
|
|
this._fetch = opts?.fetch;
|
|
- this._fetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, opts?.requestInit);
|
|
+ this._resourceFetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, { ...opts?.requestInit, redirect: 'error' });
|
|
+ const oauthRequestInit = opts?.requestInit ? { ...opts.requestInit, headers: undefined } : undefined;
|
|
+ this._fetchWithInit = (0, transport_js_1.createFetchWithInit)(opts?.fetch, oauthRequestInit);
|
|
this._sessionId = opts?.sessionId;
|
|
this._reconnectionOptions = opts?.reconnectionOptions ?? DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS;
|
|
}
|
|
@@ -47,7 +49,8 @@ class StreamableHTTPClientTransport {
|
|
serverUrl: this._url,
|
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
scope: this._scope,
|
|
- fetchFn: this._fetchWithInit
|
|
+ fetchFn: this._fetchWithInit,
|
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
|
});
|
|
}
|
|
catch (error) {
|
|
@@ -74,10 +77,11 @@ class StreamableHTTPClientTransport {
|
|
headers['mcp-protocol-version'] = this._protocolVersion;
|
|
}
|
|
const extraHeaders = (0, transport_js_1.normalizeHeaders)(this._requestInit?.headers);
|
|
- return new Headers({
|
|
- ...headers,
|
|
- ...extraHeaders
|
|
- });
|
|
+ const result = new Headers(extraHeaders);
|
|
+ for (const [name, value] of Object.entries(headers)) {
|
|
+ result.set(name, value);
|
|
+ }
|
|
+ return result;
|
|
}
|
|
async _startOrAuthSse(options) {
|
|
const { resumptionToken } = options;
|
|
@@ -275,7 +279,8 @@ class StreamableHTTPClientTransport {
|
|
authorizationCode,
|
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
scope: this._scope,
|
|
- fetchFn: this._fetchWithInit
|
|
+ fetchFn: this._fetchWithInit,
|
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
|
});
|
|
if (result !== 'AUTHORIZED') {
|
|
throw new auth_js_1.UnauthorizedError('Failed to authorize');
|
|
@@ -290,7 +295,38 @@ class StreamableHTTPClientTransport {
|
|
this.onclose?.();
|
|
}
|
|
async send(message, options) {
|
|
+ return this._send(message, options, false);
|
|
+ }
|
|
+ async _recoverSession(expiredSessionId) {
|
|
+ if (this._sessionRecovery) {
|
|
+ await this._sessionRecovery;
|
|
+ return true;
|
|
+ }
|
|
+ if (this._sessionId !== expiredSessionId)
|
|
+ return true;
|
|
+ this._sessionId = undefined;
|
|
+ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired?.());
|
|
try {
|
|
+ await this._sessionRecovery;
|
|
+ }
|
|
+ catch (error) {
|
|
+ this._sessionId = undefined;
|
|
+ await this.close();
|
|
+ throw error;
|
|
+ }
|
|
+ finally {
|
|
+ this._sessionRecovery = undefined;
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+ async _send(message, options, isSessionRetry) {
|
|
+ try {
|
|
+ if (this._sessionRecovery && !(0, types_js_1.isInitializeRequest)(message) && !(0, types_js_1.isInitializedNotification)(message)) {
|
|
+ await this._sessionRecovery;
|
|
+ if (options?.isRequestActive?.() === false) {
|
|
+ throw new Error('Request is no longer active');
|
|
+ }
|
|
+ }
|
|
const { resumptionToken, onresumptiontoken } = options || {};
|
|
if (resumptionToken) {
|
|
// If we have at last event ID, we need to reconnect the SSE stream
|
|
@@ -298,6 +334,7 @@ class StreamableHTTPClientTransport {
|
|
return;
|
|
}
|
|
const headers = await this._commonHeaders();
|
|
+ const requestSessionId = headers.get('mcp-session-id') ?? undefined;
|
|
headers.set('content-type', 'application/json');
|
|
headers.set('accept', 'application/json, text/event-stream');
|
|
const init = {
|
|
@@ -310,11 +347,20 @@ class StreamableHTTPClientTransport {
|
|
const response = await (this._fetch ?? fetch)(this._url, init);
|
|
// Handle session ID received during initialization
|
|
const sessionId = response.headers.get('mcp-session-id');
|
|
- if (sessionId) {
|
|
+ if (sessionId && (requestSessionId === undefined || this._sessionId === requestSessionId)) {
|
|
this._sessionId = sessionId;
|
|
}
|
|
if (!response.ok) {
|
|
const text = await response.text().catch(() => null);
|
|
+ if (response.status === 404 && requestSessionId && !isSessionRetry && !(0, types_js_1.isInitializedNotification)(message)) {
|
|
+ const recovered = await this._recoverSession(requestSessionId);
|
|
+ if (options?.isRequestActive?.() === false) {
|
|
+ throw new Error('Request is no longer active');
|
|
+ }
|
|
+ if (recovered) {
|
|
+ return this._send(message, options, true);
|
|
+ }
|
|
+ }
|
|
if (response.status === 401 && this._authProvider) {
|
|
// Prevent infinite recursion when server returns 401 after successful auth
|
|
if (this._hasCompletedAuthFlow) {
|
|
@@ -327,7 +373,8 @@ class StreamableHTTPClientTransport {
|
|
serverUrl: this._url,
|
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
scope: this._scope,
|
|
- fetchFn: this._fetchWithInit
|
|
+ fetchFn: this._fetchWithInit,
|
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
|
});
|
|
if (result !== 'AUTHORIZED') {
|
|
throw new auth_js_1.UnauthorizedError();
|
|
@@ -335,7 +382,7 @@ class StreamableHTTPClientTransport {
|
|
// Mark that we completed auth flow
|
|
this._hasCompletedAuthFlow = true;
|
|
// Purposely _not_ awaited, so we don't call onerror twice
|
|
- return this.send(message);
|
|
+ return this._send(message, options, isSessionRetry);
|
|
}
|
|
if (response.status === 403 && this._authProvider) {
|
|
const { resourceMetadataUrl, scope, error } = (0, auth_js_1.extractWWWAuthenticateParams)(response);
|
|
@@ -357,12 +404,13 @@ class StreamableHTTPClientTransport {
|
|
serverUrl: this._url,
|
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
scope: this._scope,
|
|
- fetchFn: this._fetch
|
|
+ fetchFn: this._fetchWithInit,
|
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
|
});
|
|
if (result !== 'AUTHORIZED') {
|
|
throw new auth_js_1.UnauthorizedError();
|
|
}
|
|
- return this.send(message);
|
|
+ return this._send(message, options, isSessionRetry);
|
|
}
|
|
}
|
|
throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
|
|
diff --git a/dist/cjs/shared/protocol.js b/dist/cjs/shared/protocol.js
|
|
index 3617e787f0ba70447c99501aee7aa67584d89758..4ee4d158391558fdc1f977f5134b7cacfc45e8c3 100644
|
|
--- a/dist/cjs/shared/protocol.js
|
|
+++ b/dist/cjs/shared/protocol.js
|
|
@@ -744,7 +744,12 @@ class Protocol {
|
|
}
|
|
else {
|
|
// No related task - send through transport normally
|
|
- this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => {
|
|
+ this._transport.send(jsonrpcRequest, {
|
|
+ relatedRequestId,
|
|
+ resumptionToken,
|
|
+ onresumptiontoken,
|
|
+ isRequestActive: () => this._responseHandlers.has(messageId)
|
|
+ }).catch(error => {
|
|
this._cleanupTimeout(messageId);
|
|
reject(error);
|
|
});
|
|
diff --git a/dist/esm/client/auth.js b/dist/esm/client/auth.js
|
|
index e183040fc2bba22ca1ccc784984f3310854403b7..6b88910ca88f9714abe7cad26fbf3a31c4fb376c 100644
|
|
--- a/dist/esm/client/auth.js
|
|
+++ b/dist/esm/client/auth.js
|
|
@@ -161,7 +161,7 @@ export async function auth(provider, options) {
|
|
throw error;
|
|
}
|
|
}
|
|
-async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
|
|
+async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn, resourceMetadataFetchFn }) {
|
|
// Check if the provider has cached discovery state to skip discovery
|
|
const cachedState = await provider.discoveryState?.();
|
|
let resourceMetadata;
|
|
@@ -173,6 +173,9 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
|
|
if (!effectiveResourceMetadataUrl && cachedState?.resourceMetadataUrl) {
|
|
effectiveResourceMetadataUrl = new URL(cachedState.resourceMetadataUrl);
|
|
}
|
|
+ const protectedResourceFetchFn = !effectiveResourceMetadataUrl || effectiveResourceMetadataUrl.origin === new URL(serverUrl).origin
|
|
+ ? (resourceMetadataFetchFn ?? fetchFn)
|
|
+ : fetchFn;
|
|
if (cachedState?.authorizationServerUrl) {
|
|
// Restore discovery state from cache
|
|
authorizationServerUrl = cachedState.authorizationServerUrl;
|
|
@@ -182,7 +185,7 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
|
|
// If resource metadata wasn't cached, try to fetch it for selectResourceURL
|
|
if (!resourceMetadata) {
|
|
try {
|
|
- resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl }, fetchFn);
|
|
+ resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl }, protectedResourceFetchFn);
|
|
}
|
|
catch {
|
|
// RFC 9728 not available — selectResourceURL will handle undefined
|
|
@@ -200,7 +203,11 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
|
|
}
|
|
else {
|
|
// Full discovery via RFC 9728
|
|
- const serverInfo = await discoverOAuthServerInfo(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl, fetchFn });
|
|
+ const serverInfo = await discoverOAuthServerInfo(serverUrl, {
|
|
+ resourceMetadataUrl: effectiveResourceMetadataUrl,
|
|
+ fetchFn,
|
|
+ resourceMetadataFetchFn: protectedResourceFetchFn
|
|
+ });
|
|
authorizationServerUrl = serverInfo.authorizationServerUrl;
|
|
metadata = serverInfo.authorizationServerMetadata;
|
|
resourceMetadata = serverInfo.resourceMetadata;
|
|
@@ -663,7 +670,7 @@ export async function discoverOAuthServerInfo(serverUrl, opts) {
|
|
let resourceMetadata;
|
|
let authorizationServerUrl;
|
|
try {
|
|
- resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.fetchFn);
|
|
+ resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.resourceMetadataFetchFn ?? opts?.fetchFn);
|
|
if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) {
|
|
authorizationServerUrl = resourceMetadata.authorization_servers[0];
|
|
}
|
|
diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts
|
|
index 6f567a193626587a2730b5a49293ca5dfd4181ea..5b7c841c000508e389ce617f559f7c2a5126ca9f 100644
|
|
--- a/dist/esm/client/index.d.ts
|
|
+++ b/dist/esm/client/index.d.ts
|
|
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
|
|
*
|
|
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
|
|
*/
|
|
+ callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
|
|
+ callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
|
|
callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
|
|
[x: string]: unknown;
|
|
content: ({
|
|
diff --git a/dist/esm/client/index.js b/dist/esm/client/index.js
|
|
index 49b12c6cd918c457420fef7ad5528a9443d1a191..339153cb7e9299b7a9bdec0e56e41cedf425fb31 100644
|
|
--- a/dist/esm/client/index.js
|
|
+++ b/dist/esm/client/index.js
|
|
@@ -284,41 +284,16 @@ export class Client extends Protocol {
|
|
}
|
|
async connect(transport, options) {
|
|
await super.connect(transport);
|
|
+ transport.onsessionexpired = async () => {
|
|
+ await this._initialize(transport);
|
|
+ };
|
|
// When transport sessionId is already set this means we are trying to reconnect.
|
|
// In this case we don't need to initialize again.
|
|
if (transport.sessionId !== undefined) {
|
|
return;
|
|
}
|
|
try {
|
|
- const result = await this.request({
|
|
- method: 'initialize',
|
|
- params: {
|
|
- protocolVersion: LATEST_PROTOCOL_VERSION,
|
|
- capabilities: this._capabilities,
|
|
- clientInfo: this._clientInfo
|
|
- }
|
|
- }, InitializeResultSchema, options);
|
|
- if (result === undefined) {
|
|
- throw new Error(`Server sent invalid initialize result: ${result}`);
|
|
- }
|
|
- if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
|
|
- throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
|
|
- }
|
|
- this._serverCapabilities = result.capabilities;
|
|
- this._serverVersion = result.serverInfo;
|
|
- // HTTP transports must set the protocol version in each header after initialization.
|
|
- if (transport.setProtocolVersion) {
|
|
- transport.setProtocolVersion(result.protocolVersion);
|
|
- }
|
|
- this._instructions = result.instructions;
|
|
- await this.notification({
|
|
- method: 'notifications/initialized'
|
|
- });
|
|
- // Set up list changed handlers now that we know server capabilities
|
|
- if (this._pendingListChangedConfig) {
|
|
- this._setupListChangedHandlers(this._pendingListChangedConfig);
|
|
- this._pendingListChangedConfig = undefined;
|
|
- }
|
|
+ await this._initialize(transport, options);
|
|
}
|
|
catch (error) {
|
|
// Disconnect if initialization fails.
|
|
@@ -326,6 +301,37 @@ export class Client extends Protocol {
|
|
throw error;
|
|
}
|
|
}
|
|
+ async _initialize(transport, options) {
|
|
+ const result = await this.request({
|
|
+ method: 'initialize',
|
|
+ params: {
|
|
+ protocolVersion: LATEST_PROTOCOL_VERSION,
|
|
+ capabilities: this._capabilities,
|
|
+ clientInfo: this._clientInfo
|
|
+ }
|
|
+ }, InitializeResultSchema, options);
|
|
+ if (result === undefined) {
|
|
+ throw new Error(`Server sent invalid initialize result: ${result}`);
|
|
+ }
|
|
+ if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
|
|
+ throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
|
|
+ }
|
|
+ this._serverCapabilities = result.capabilities;
|
|
+ this._serverVersion = result.serverInfo;
|
|
+ // HTTP transports must set the protocol version in each header after initialization.
|
|
+ if (transport.setProtocolVersion) {
|
|
+ transport.setProtocolVersion(result.protocolVersion);
|
|
+ }
|
|
+ this._instructions = result.instructions;
|
|
+ await this.notification({
|
|
+ method: 'notifications/initialized'
|
|
+ });
|
|
+ // Set up list changed handlers now that we know server capabilities
|
|
+ if (this._pendingListChangedConfig) {
|
|
+ this._setupListChangedHandlers(this._pendingListChangedConfig);
|
|
+ this._pendingListChangedConfig = undefined;
|
|
+ }
|
|
+ }
|
|
/**
|
|
* After initialization has completed, this will be populated with the server's reported capabilities.
|
|
*/
|
|
diff --git a/dist/esm/client/sse.js b/dist/esm/client/sse.js
|
|
index 58c474156ba4b33090ac092be6f3284e695a7ffd..b813e467b497697208167ad9bfccdecd1200ffac 100644
|
|
--- a/dist/esm/client/sse.js
|
|
+++ b/dist/esm/client/sse.js
|
|
@@ -23,7 +23,9 @@ export class SSEClientTransport {
|
|
this._requestInit = opts?.requestInit;
|
|
this._authProvider = opts?.authProvider;
|
|
this._fetch = opts?.fetch;
|
|
- this._fetchWithInit = createFetchWithInit(opts?.fetch, opts?.requestInit);
|
|
+ this._resourceFetchWithInit = createFetchWithInit(opts?.fetch, { ...opts?.requestInit, redirect: 'error' });
|
|
+ const oauthRequestInit = opts?.requestInit ? { ...opts.requestInit, headers: undefined } : undefined;
|
|
+ this._fetchWithInit = createFetchWithInit(opts?.fetch, oauthRequestInit);
|
|
}
|
|
async _authThenStart() {
|
|
if (!this._authProvider) {
|
|
@@ -35,7 +37,8 @@ export class SSEClientTransport {
|
|
serverUrl: this._url,
|
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
scope: this._scope,
|
|
- fetchFn: this._fetchWithInit
|
|
+ fetchFn: this._fetchWithInit,
|
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
|
});
|
|
}
|
|
catch (error) {
|
|
@@ -59,10 +62,11 @@ export class SSEClientTransport {
|
|
headers['mcp-protocol-version'] = this._protocolVersion;
|
|
}
|
|
const extraHeaders = normalizeHeaders(this._requestInit?.headers);
|
|
- return new Headers({
|
|
- ...headers,
|
|
- ...extraHeaders
|
|
- });
|
|
+ const result = new Headers(extraHeaders);
|
|
+ for (const [name, value] of Object.entries(headers)) {
|
|
+ result.set(name, value);
|
|
+ }
|
|
+ return result;
|
|
}
|
|
_startOrAuth() {
|
|
const fetchImpl = (this?._eventSourceInit?.fetch ?? this._fetch ?? fetch);
|
|
@@ -145,7 +149,8 @@ export class SSEClientTransport {
|
|
authorizationCode,
|
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
scope: this._scope,
|
|
- fetchFn: this._fetchWithInit
|
|
+ fetchFn: this._fetchWithInit,
|
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
|
});
|
|
if (result !== 'AUTHORIZED') {
|
|
throw new UnauthorizedError('Failed to authorize');
|
|
@@ -181,7 +186,8 @@ export class SSEClientTransport {
|
|
serverUrl: this._url,
|
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
scope: this._scope,
|
|
- fetchFn: this._fetchWithInit
|
|
+ fetchFn: this._fetchWithInit,
|
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
|
});
|
|
if (result !== 'AUTHORIZED') {
|
|
throw new UnauthorizedError();
|
|
diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js
|
|
index 624172aa24ae255a67c083f9c19053343e4a0581..88f7f5c3528d0ce6115efbebfdda98eba7077b8b 100644
|
|
--- a/dist/esm/client/streamableHttp.js
|
|
+++ b/dist/esm/client/streamableHttp.js
|
|
@@ -1,5 +1,5 @@
|
|
import { createFetchWithInit, normalizeHeaders } from '../shared/transport.js';
|
|
-import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
|
|
+import { isInitializedNotification, isInitializeRequest, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
|
|
import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js';
|
|
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
|
// Default reconnection options for StreamableHTTP connections
|
|
@@ -29,7 +29,9 @@ export class StreamableHTTPClientTransport {
|
|
this._requestInit = opts?.requestInit;
|
|
this._authProvider = opts?.authProvider;
|
|
this._fetch = opts?.fetch;
|
|
- this._fetchWithInit = createFetchWithInit(opts?.fetch, opts?.requestInit);
|
|
+ this._resourceFetchWithInit = createFetchWithInit(opts?.fetch, { ...opts?.requestInit, redirect: 'error' });
|
|
+ const oauthRequestInit = opts?.requestInit ? { ...opts.requestInit, headers: undefined } : undefined;
|
|
+ this._fetchWithInit = createFetchWithInit(opts?.fetch, oauthRequestInit);
|
|
this._sessionId = opts?.sessionId;
|
|
this._reconnectionOptions = opts?.reconnectionOptions ?? DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS;
|
|
}
|
|
@@ -43,7 +45,8 @@ export class StreamableHTTPClientTransport {
|
|
serverUrl: this._url,
|
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
scope: this._scope,
|
|
- fetchFn: this._fetchWithInit
|
|
+ fetchFn: this._fetchWithInit,
|
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
|
});
|
|
}
|
|
catch (error) {
|
|
@@ -70,10 +73,11 @@ export class StreamableHTTPClientTransport {
|
|
headers['mcp-protocol-version'] = this._protocolVersion;
|
|
}
|
|
const extraHeaders = normalizeHeaders(this._requestInit?.headers);
|
|
- return new Headers({
|
|
- ...headers,
|
|
- ...extraHeaders
|
|
- });
|
|
+ const result = new Headers(extraHeaders);
|
|
+ for (const [name, value] of Object.entries(headers)) {
|
|
+ result.set(name, value);
|
|
+ }
|
|
+ return result;
|
|
}
|
|
async _startOrAuthSse(options) {
|
|
const { resumptionToken } = options;
|
|
@@ -271,7 +275,8 @@ export class StreamableHTTPClientTransport {
|
|
authorizationCode,
|
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
scope: this._scope,
|
|
- fetchFn: this._fetchWithInit
|
|
+ fetchFn: this._fetchWithInit,
|
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
|
});
|
|
if (result !== 'AUTHORIZED') {
|
|
throw new UnauthorizedError('Failed to authorize');
|
|
@@ -286,7 +291,38 @@ export class StreamableHTTPClientTransport {
|
|
this.onclose?.();
|
|
}
|
|
async send(message, options) {
|
|
+ return this._send(message, options, false);
|
|
+ }
|
|
+ async _recoverSession(expiredSessionId) {
|
|
+ if (this._sessionRecovery) {
|
|
+ await this._sessionRecovery;
|
|
+ return true;
|
|
+ }
|
|
+ if (this._sessionId !== expiredSessionId)
|
|
+ return true;
|
|
+ this._sessionId = undefined;
|
|
+ this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired?.());
|
|
try {
|
|
+ await this._sessionRecovery;
|
|
+ }
|
|
+ catch (error) {
|
|
+ this._sessionId = undefined;
|
|
+ await this.close();
|
|
+ throw error;
|
|
+ }
|
|
+ finally {
|
|
+ this._sessionRecovery = undefined;
|
|
+ }
|
|
+ return true;
|
|
+ }
|
|
+ async _send(message, options, isSessionRetry) {
|
|
+ try {
|
|
+ if (this._sessionRecovery && !isInitializeRequest(message) && !isInitializedNotification(message)) {
|
|
+ await this._sessionRecovery;
|
|
+ if (options?.isRequestActive?.() === false) {
|
|
+ throw new Error('Request is no longer active');
|
|
+ }
|
|
+ }
|
|
const { resumptionToken, onresumptiontoken } = options || {};
|
|
if (resumptionToken) {
|
|
// If we have at last event ID, we need to reconnect the SSE stream
|
|
@@ -294,6 +330,7 @@ export class StreamableHTTPClientTransport {
|
|
return;
|
|
}
|
|
const headers = await this._commonHeaders();
|
|
+ const requestSessionId = headers.get('mcp-session-id') ?? undefined;
|
|
headers.set('content-type', 'application/json');
|
|
headers.set('accept', 'application/json, text/event-stream');
|
|
const init = {
|
|
@@ -306,11 +343,20 @@ export class StreamableHTTPClientTransport {
|
|
const response = await (this._fetch ?? fetch)(this._url, init);
|
|
// Handle session ID received during initialization
|
|
const sessionId = response.headers.get('mcp-session-id');
|
|
- if (sessionId) {
|
|
+ if (sessionId && (requestSessionId === undefined || this._sessionId === requestSessionId)) {
|
|
this._sessionId = sessionId;
|
|
}
|
|
if (!response.ok) {
|
|
const text = await response.text().catch(() => null);
|
|
+ if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitializedNotification(message)) {
|
|
+ const recovered = await this._recoverSession(requestSessionId);
|
|
+ if (options?.isRequestActive?.() === false) {
|
|
+ throw new Error('Request is no longer active');
|
|
+ }
|
|
+ if (recovered) {
|
|
+ return this._send(message, options, true);
|
|
+ }
|
|
+ }
|
|
if (response.status === 401 && this._authProvider) {
|
|
// Prevent infinite recursion when server returns 401 after successful auth
|
|
if (this._hasCompletedAuthFlow) {
|
|
@@ -323,7 +369,8 @@ export class StreamableHTTPClientTransport {
|
|
serverUrl: this._url,
|
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
scope: this._scope,
|
|
- fetchFn: this._fetchWithInit
|
|
+ fetchFn: this._fetchWithInit,
|
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
|
});
|
|
if (result !== 'AUTHORIZED') {
|
|
throw new UnauthorizedError();
|
|
@@ -331,7 +378,7 @@ export class StreamableHTTPClientTransport {
|
|
// Mark that we completed auth flow
|
|
this._hasCompletedAuthFlow = true;
|
|
// Purposely _not_ awaited, so we don't call onerror twice
|
|
- return this.send(message);
|
|
+ return this._send(message, options, isSessionRetry);
|
|
}
|
|
if (response.status === 403 && this._authProvider) {
|
|
const { resourceMetadataUrl, scope, error } = extractWWWAuthenticateParams(response);
|
|
@@ -353,12 +400,13 @@ export class StreamableHTTPClientTransport {
|
|
serverUrl: this._url,
|
|
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
scope: this._scope,
|
|
- fetchFn: this._fetch
|
|
+ fetchFn: this._fetchWithInit,
|
|
+ resourceMetadataFetchFn: this._resourceFetchWithInit
|
|
});
|
|
if (result !== 'AUTHORIZED') {
|
|
throw new UnauthorizedError();
|
|
}
|
|
- return this.send(message);
|
|
+ return this._send(message, options, isSessionRetry);
|
|
}
|
|
}
|
|
throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
|
|
diff --git a/dist/esm/shared/protocol.js b/dist/esm/shared/protocol.js
|
|
index bfa2b7120a0f50c569364ea5264e6f811076f44f..dec477d16a0fd796854542c1144279a6e86567f2 100644
|
|
--- a/dist/esm/shared/protocol.js
|
|
+++ b/dist/esm/shared/protocol.js
|
|
@@ -740,7 +740,12 @@ export class Protocol {
|
|
}
|
|
else {
|
|
// No related task - send through transport normally
|
|
- this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => {
|
|
+ this._transport.send(jsonrpcRequest, {
|
|
+ relatedRequestId,
|
|
+ resumptionToken,
|
|
+ onresumptiontoken,
|
|
+ isRequestActive: () => this._responseHandlers.has(messageId)
|
|
+ }).catch(error => {
|
|
this._cleanupTimeout(messageId);
|
|
reject(error);
|
|
});
|