| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647 |
- diff --git a/dist/cjs/client/index.d.ts b/dist/cjs/client/index.d.ts
- index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 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/esm/client/index.d.ts b/dist/esm/client/index.d.ts
- index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 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/cjs/client/index.js b/dist/cjs/client/index.js
- index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c101584c84 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.
- */
- @@ -541,9 +547,11 @@ class Client extends protocol_js_1.Protocol {
- * Called after listTools() to pre-compile validators for better performance.
- */
- - cacheToolMetadata(tools) {
- - this._cachedToolOutputValidators.clear();
- - this._cachedKnownTaskTools.clear();
- - this._cachedRequiredTaskTools.clear();
- + cacheToolMetadata(tools, reset = true) {
- + if (reset) {
- + this._cachedToolOutputValidators.clear();
- + this._cachedKnownTaskTools.clear();
- + this._cachedRequiredTaskTools.clear();
- + }
- for (const tool of tools) {
- // If the tool has an outputSchema, create and cache the validator
- if (tool.outputSchema) {
- @@ -569,7 +577,7 @@ class Client extends protocol_js_1.Protocol {
- async listTools(params, options) {
- const result = await this.request({ method: 'tools/list', params }, types_js_1.ListToolsResultSchema, options);
- // Cache the tools and their output schemas for future validation
- - this.cacheToolMetadata(result.tools);
- + this.cacheToolMetadata(result.tools, params?.cursor === undefined);
- return result;
- }
- /**
- diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js
- index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644
- --- a/dist/cjs/client/streamableHttp.js
- +++ b/dist/cjs/client/streamableHttp.js
- @@ -204,7 +204,7 @@ class StreamableHTTPClientTransport {
- 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)) {
- + if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) {
- // Mark that we received a response - no need to reconnect for this request
- receivedResponse = true;
- if (replayMessageId !== undefined) {
- @@ -290,7 +290,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 +329,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 +342,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) {
- @@ -335,7 +376,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);
- @@ -362,7 +403,7 @@ class StreamableHTTPClientTransport {
- 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..4a96d6a0328fa348b96f3869ab7e0bb77538182b 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/cjs/client/auth.d.ts b/dist/cjs/client/auth.d.ts
- index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644
- --- a/dist/cjs/client/auth.d.ts
- +++ b/dist/cjs/client/auth.d.ts
- @@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise<OA
- * @returns A Promise that resolves to an OAuthError instance
- */
- export declare function parseErrorResponse(input: Response | string): Promise<OAuthError>;
- +/**
- + * Selects scopes per the MCP spec and augments them for refresh token support.
- + */
- +export declare function determineScope(options: {
- + requestedScope?: string;
- + resourceMetadata?: OAuthProtectedResourceMetadata;
- + authServerMetadata?: AuthorizationServerMetadata;
- + clientMetadata: OAuthClientMetadata;
- +}): string | undefined;
- /**
- * Orchestrates the full auth flow with a server.
- *
- diff --git a/dist/cjs/client/auth.js b/dist/cjs/client/auth.js
- index c2e4fa91d26f5336889f6afa416147db75fc4872..178d7cfd96412d53bc14bbc13a8f76c11f727ee7 100644
- --- a/dist/cjs/client/auth.js
- +++ b/dist/cjs/client/auth.js
- @@ -7,6 +7,7 @@ exports.UnauthorizedError = void 0;
- exports.selectClientAuthMethod = selectClientAuthMethod;
- exports.parseErrorResponse = parseErrorResponse;
- exports.auth = auth;
- +exports.determineScope = determineScope;
- exports.isHttpsUrl = isHttpsUrl;
- exports.selectResourceURL = selectResourceURL;
- exports.extractWWWAuthenticateParams = extractWWWAuthenticateParams;
- @@ -186,6 +187,19 @@ async function auth(provider, options) {
- throw error;
- }
- }
- +/**
- + * Selects scopes per the MCP spec and augments them for refresh token support.
- + */
- +function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) {
- + let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope;
- + if (effectiveScope &&
- + authServerMetadata?.scopes_supported?.includes('offline_access') &&
- + !effectiveScope.split(' ').includes('offline_access') &&
- + clientMetadata.grant_types?.includes('refresh_token')) {
- + effectiveScope = `${effectiveScope} offline_access`;
- + }
- + return effectiveScope;
- +}
- async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
- // Check if the provider has cached discovery state to skip discovery
- const cachedState = await provider.discoveryState?.();
- @@ -241,12 +255,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
- });
- }
- const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
- - // Apply scope selection strategy (SEP-835):
- - // 1. WWW-Authenticate scope (passed via `scope` param)
- - // 2. PRM scopes_supported
- - // 3. Client metadata scope (user-configured fallback)
- - // The resolved scope is used consistently for both DCR and the authorization request.
- - const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
- + const resolvedScope = determineScope({
- + requestedScope: scope,
- + resourceMetadata,
- + authServerMetadata: metadata,
- + clientMetadata: provider.clientMetadata
- + });
- // Handle client registration if needed
- let clientInformation = await Promise.resolve(provider.clientInformation());
- if (!clientInformation) {
- @@ -741,7 +755,7 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
- if (scope) {
- authorizationUrl.searchParams.set('scope', scope);
- }
- - if (scope?.includes('offline_access')) {
- + if (scope?.split(' ').includes('offline_access')) {
- // if the request includes the OIDC-only "offline_access" scope,
- // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
- // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
- diff --git a/dist/esm/client/auth.d.ts b/dist/esm/client/auth.d.ts
- index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644
- --- a/dist/esm/client/auth.d.ts
- +++ b/dist/esm/client/auth.d.ts
- @@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise<OA
- * @returns A Promise that resolves to an OAuthError instance
- */
- export declare function parseErrorResponse(input: Response | string): Promise<OAuthError>;
- +/**
- + * Selects scopes per the MCP spec and augments them for refresh token support.
- + */
- +export declare function determineScope(options: {
- + requestedScope?: string;
- + resourceMetadata?: OAuthProtectedResourceMetadata;
- + authServerMetadata?: AuthorizationServerMetadata;
- + clientMetadata: OAuthClientMetadata;
- +}): string | undefined;
- /**
- * Orchestrates the full auth flow with a server.
- *
- diff --git a/dist/esm/client/auth.js b/dist/esm/client/auth.js
- index e183040fc2bba22ca1ccc784984f3310854403b7..d367661e580ee61a96654f7af78b2af61dcad98b 100644
- --- a/dist/esm/client/auth.js
- +++ b/dist/esm/client/auth.js
- @@ -161,6 +161,19 @@ export async function auth(provider, options) {
- throw error;
- }
- }
- +/**
- + * Selects scopes per the MCP spec and augments them for refresh token support.
- + */
- +export function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) {
- + let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope;
- + if (effectiveScope &&
- + authServerMetadata?.scopes_supported?.includes('offline_access') &&
- + !effectiveScope.split(' ').includes('offline_access') &&
- + clientMetadata.grant_types?.includes('refresh_token')) {
- + effectiveScope = `${effectiveScope} offline_access`;
- + }
- + return effectiveScope;
- +}
- async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
- // Check if the provider has cached discovery state to skip discovery
- const cachedState = await provider.discoveryState?.();
- @@ -216,12 +229,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
- });
- }
- const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
- - // Apply scope selection strategy (SEP-835):
- - // 1. WWW-Authenticate scope (passed via `scope` param)
- - // 2. PRM scopes_supported
- - // 3. Client metadata scope (user-configured fallback)
- - // The resolved scope is used consistently for both DCR and the authorization request.
- - const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
- + const resolvedScope = determineScope({
- + requestedScope: scope,
- + resourceMetadata,
- + authServerMetadata: metadata,
- + clientMetadata: provider.clientMetadata
- + });
- // Handle client registration if needed
- let clientInformation = await Promise.resolve(provider.clientInformation());
- if (!clientInformation) {
- @@ -716,7 +729,7 @@ export async function startAuthorization(authorizationServerUrl, { metadata, cli
- if (scope) {
- authorizationUrl.searchParams.set('scope', scope);
- }
- - if (scope?.includes('offline_access')) {
- + if (scope?.split(' ').includes('offline_access')) {
- // if the request includes the OIDC-only "offline_access" scope,
- // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
- // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
- diff --git a/dist/esm/client/index.js b/dist/esm/client/index.js
- index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8eb9e4caff 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.
- */
- @@ -537,9 +543,11 @@ export class Client extends Protocol {
- * Called after listTools() to pre-compile validators for better performance.
- */
- - cacheToolMetadata(tools) {
- - this._cachedToolOutputValidators.clear();
- - this._cachedKnownTaskTools.clear();
- - this._cachedRequiredTaskTools.clear();
- + cacheToolMetadata(tools, reset = true) {
- + if (reset) {
- + this._cachedToolOutputValidators.clear();
- + this._cachedKnownTaskTools.clear();
- + this._cachedRequiredTaskTools.clear();
- + }
- for (const tool of tools) {
- // If the tool has an outputSchema, create and cache the validator
- if (tool.outputSchema) {
- @@ -565,7 +573,7 @@ export class Client extends Protocol {
- async listTools(params, options) {
- const result = await this.request({ method: 'tools/list', params }, ListToolsResultSchema, options);
- // Cache the tools and their output schemas for future validation
- - this.cacheToolMetadata(result.tools);
- + this.cacheToolMetadata(result.tools, params?.cursor === undefined);
- return result;
- }
- /**
- diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js
- index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da884fcc627a 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, isJSONRPCErrorResponse, 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
- @@ -200,7 +200,7 @@ export class StreamableHTTPClientTransport {
- if (!event.event || event.event === 'message') {
- try {
- const message = JSONRPCMessageSchema.parse(JSON.parse(event.data));
- - if (isJSONRPCResultResponse(message)) {
- + if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
- // Mark that we received a response - no need to reconnect for this request
- receivedResponse = true;
- if (replayMessageId !== undefined) {
- @@ -286,7 +286,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 +325,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 +338,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) {
- @@ -331,7 +372,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);
- @@ -358,7 +399,7 @@ export class StreamableHTTPClientTransport {
- 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..abd8dfd707c155f71dae7aeeeeaf7547368ac749 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);
- });
|