@modelcontextprotocol%2Fsdk@1.29.0.patch 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647
  1. diff --git a/dist/cjs/client/index.d.ts b/dist/cjs/client/index.d.ts
  2. index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644
  3. --- a/dist/cjs/client/index.d.ts
  4. +++ b/dist/cjs/client/index.d.ts
  5. @@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
  6. *
  7. * For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
  8. */
  9. + callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
  10. + callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
  11. callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
  12. [x: string]: unknown;
  13. content: ({
  14. diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts
  15. index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644
  16. --- a/dist/esm/client/index.d.ts
  17. +++ b/dist/esm/client/index.d.ts
  18. @@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
  19. *
  20. * For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
  21. */
  22. + callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
  23. + callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
  24. callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
  25. [x: string]: unknown;
  26. content: ({
  27. diff --git a/dist/cjs/client/index.js b/dist/cjs/client/index.js
  28. index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c101584c84 100644
  29. --- a/dist/cjs/client/index.js
  30. +++ b/dist/cjs/client/index.js
  31. @@ -288,41 +288,16 @@ class Client extends protocol_js_1.Protocol {
  32. }
  33. async connect(transport, options) {
  34. await super.connect(transport);
  35. + transport.onsessionexpired = async () => {
  36. + await this._initialize(transport);
  37. + };
  38. // When transport sessionId is already set this means we are trying to reconnect.
  39. // In this case we don't need to initialize again.
  40. if (transport.sessionId !== undefined) {
  41. return;
  42. }
  43. try {
  44. - const result = await this.request({
  45. - method: 'initialize',
  46. - params: {
  47. - protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION,
  48. - capabilities: this._capabilities,
  49. - clientInfo: this._clientInfo
  50. - }
  51. - }, types_js_1.InitializeResultSchema, options);
  52. - if (result === undefined) {
  53. - throw new Error(`Server sent invalid initialize result: ${result}`);
  54. - }
  55. - if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
  56. - throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
  57. - }
  58. - this._serverCapabilities = result.capabilities;
  59. - this._serverVersion = result.serverInfo;
  60. - // HTTP transports must set the protocol version in each header after initialization.
  61. - if (transport.setProtocolVersion) {
  62. - transport.setProtocolVersion(result.protocolVersion);
  63. - }
  64. - this._instructions = result.instructions;
  65. - await this.notification({
  66. - method: 'notifications/initialized'
  67. - });
  68. - // Set up list changed handlers now that we know server capabilities
  69. - if (this._pendingListChangedConfig) {
  70. - this._setupListChangedHandlers(this._pendingListChangedConfig);
  71. - this._pendingListChangedConfig = undefined;
  72. - }
  73. + await this._initialize(transport, options);
  74. }
  75. catch (error) {
  76. // Disconnect if initialization fails.
  77. @@ -330,6 +305,37 @@ class Client extends protocol_js_1.Protocol {
  78. throw error;
  79. }
  80. }
  81. + async _initialize(transport, options) {
  82. + const result = await this.request({
  83. + method: 'initialize',
  84. + params: {
  85. + protocolVersion: types_js_1.LATEST_PROTOCOL_VERSION,
  86. + capabilities: this._capabilities,
  87. + clientInfo: this._clientInfo
  88. + }
  89. + }, types_js_1.InitializeResultSchema, options);
  90. + if (result === undefined) {
  91. + throw new Error(`Server sent invalid initialize result: ${result}`);
  92. + }
  93. + if (!types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
  94. + throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
  95. + }
  96. + this._serverCapabilities = result.capabilities;
  97. + this._serverVersion = result.serverInfo;
  98. + // HTTP transports must set the protocol version in each header after initialization.
  99. + if (transport.setProtocolVersion) {
  100. + transport.setProtocolVersion(result.protocolVersion);
  101. + }
  102. + this._instructions = result.instructions;
  103. + await this.notification({
  104. + method: 'notifications/initialized'
  105. + });
  106. + // Set up list changed handlers now that we know server capabilities
  107. + if (this._pendingListChangedConfig) {
  108. + this._setupListChangedHandlers(this._pendingListChangedConfig);
  109. + this._pendingListChangedConfig = undefined;
  110. + }
  111. + }
  112. /**
  113. * After initialization has completed, this will be populated with the server's reported capabilities.
  114. */
  115. @@ -541,9 +547,11 @@ class Client extends protocol_js_1.Protocol {
  116. * Called after listTools() to pre-compile validators for better performance.
  117. */
  118. - cacheToolMetadata(tools) {
  119. - this._cachedToolOutputValidators.clear();
  120. - this._cachedKnownTaskTools.clear();
  121. - this._cachedRequiredTaskTools.clear();
  122. + cacheToolMetadata(tools, reset = true) {
  123. + if (reset) {
  124. + this._cachedToolOutputValidators.clear();
  125. + this._cachedKnownTaskTools.clear();
  126. + this._cachedRequiredTaskTools.clear();
  127. + }
  128. for (const tool of tools) {
  129. // If the tool has an outputSchema, create and cache the validator
  130. if (tool.outputSchema) {
  131. @@ -569,7 +577,7 @@ class Client extends protocol_js_1.Protocol {
  132. async listTools(params, options) {
  133. const result = await this.request({ method: 'tools/list', params }, types_js_1.ListToolsResultSchema, options);
  134. // Cache the tools and their output schemas for future validation
  135. - this.cacheToolMetadata(result.tools);
  136. + this.cacheToolMetadata(result.tools, params?.cursor === undefined);
  137. return result;
  138. }
  139. /**
  140. diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js
  141. index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644
  142. --- a/dist/cjs/client/streamableHttp.js
  143. +++ b/dist/cjs/client/streamableHttp.js
  144. @@ -204,7 +204,7 @@ class StreamableHTTPClientTransport {
  145. if (!event.event || event.event === 'message') {
  146. try {
  147. const message = types_js_1.JSONRPCMessageSchema.parse(JSON.parse(event.data));
  148. - if ((0, types_js_1.isJSONRPCResultResponse)(message)) {
  149. + if ((0, types_js_1.isJSONRPCResultResponse)(message) || (0, types_js_1.isJSONRPCErrorResponse)(message)) {
  150. // Mark that we received a response - no need to reconnect for this request
  151. receivedResponse = true;
  152. if (replayMessageId !== undefined) {
  153. @@ -290,7 +290,38 @@ class StreamableHTTPClientTransport {
  154. this.onclose?.();
  155. }
  156. async send(message, options) {
  157. + return this._send(message, options, false);
  158. + }
  159. + async _recoverSession(expiredSessionId) {
  160. + if (this._sessionRecovery) {
  161. + await this._sessionRecovery;
  162. + return true;
  163. + }
  164. + if (this._sessionId !== expiredSessionId)
  165. + return true;
  166. + this._sessionId = undefined;
  167. + this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired?.());
  168. try {
  169. + await this._sessionRecovery;
  170. + }
  171. + catch (error) {
  172. + this._sessionId = undefined;
  173. + await this.close();
  174. + throw error;
  175. + }
  176. + finally {
  177. + this._sessionRecovery = undefined;
  178. + }
  179. + return true;
  180. + }
  181. + async _send(message, options, isSessionRetry) {
  182. + try {
  183. + if (this._sessionRecovery && !(0, types_js_1.isInitializeRequest)(message) && !(0, types_js_1.isInitializedNotification)(message)) {
  184. + await this._sessionRecovery;
  185. + if (options?.isRequestActive?.() === false) {
  186. + throw new Error('Request is no longer active');
  187. + }
  188. + }
  189. const { resumptionToken, onresumptiontoken } = options || {};
  190. if (resumptionToken) {
  191. // If we have at last event ID, we need to reconnect the SSE stream
  192. @@ -298,6 +329,7 @@ class StreamableHTTPClientTransport {
  193. return;
  194. }
  195. const headers = await this._commonHeaders();
  196. + const requestSessionId = headers.get('mcp-session-id') ?? undefined;
  197. headers.set('content-type', 'application/json');
  198. headers.set('accept', 'application/json, text/event-stream');
  199. const init = {
  200. @@ -310,11 +342,20 @@ class StreamableHTTPClientTransport {
  201. const response = await (this._fetch ?? fetch)(this._url, init);
  202. // Handle session ID received during initialization
  203. const sessionId = response.headers.get('mcp-session-id');
  204. - if (sessionId) {
  205. + if (sessionId && (requestSessionId === undefined || this._sessionId === requestSessionId)) {
  206. this._sessionId = sessionId;
  207. }
  208. if (!response.ok) {
  209. const text = await response.text().catch(() => null);
  210. + if (response.status === 404 && requestSessionId && !isSessionRetry && !(0, types_js_1.isInitializedNotification)(message)) {
  211. + const recovered = await this._recoverSession(requestSessionId);
  212. + if (options?.isRequestActive?.() === false) {
  213. + throw new Error('Request is no longer active');
  214. + }
  215. + if (recovered) {
  216. + return this._send(message, options, true);
  217. + }
  218. + }
  219. if (response.status === 401 && this._authProvider) {
  220. // Prevent infinite recursion when server returns 401 after successful auth
  221. if (this._hasCompletedAuthFlow) {
  222. @@ -335,7 +376,7 @@ class StreamableHTTPClientTransport {
  223. // Mark that we completed auth flow
  224. this._hasCompletedAuthFlow = true;
  225. // Purposely _not_ awaited, so we don't call onerror twice
  226. - return this.send(message);
  227. + return this._send(message, options, isSessionRetry);
  228. }
  229. if (response.status === 403 && this._authProvider) {
  230. const { resourceMetadataUrl, scope, error } = (0, auth_js_1.extractWWWAuthenticateParams)(response);
  231. @@ -362,7 +403,7 @@ class StreamableHTTPClientTransport {
  232. if (result !== 'AUTHORIZED') {
  233. throw new auth_js_1.UnauthorizedError();
  234. }
  235. - return this.send(message);
  236. + return this._send(message, options, isSessionRetry);
  237. }
  238. }
  239. throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
  240. diff --git a/dist/cjs/shared/protocol.js b/dist/cjs/shared/protocol.js
  241. index 3617e787f0ba70447c99501aee7aa67584d89758..4a96d6a0328fa348b96f3869ab7e0bb77538182b 100644
  242. --- a/dist/cjs/shared/protocol.js
  243. +++ b/dist/cjs/shared/protocol.js
  244. @@ -744,7 +744,12 @@ class Protocol {
  245. }
  246. else {
  247. // No related task - send through transport normally
  248. - this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => {
  249. + this._transport.send(jsonrpcRequest, {
  250. + relatedRequestId,
  251. + resumptionToken,
  252. + onresumptiontoken,
  253. + isRequestActive: () => this._responseHandlers.has(messageId)
  254. + }).catch(error => {
  255. this._cleanupTimeout(messageId);
  256. reject(error);
  257. });
  258. diff --git a/dist/cjs/client/auth.d.ts b/dist/cjs/client/auth.d.ts
  259. index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644
  260. --- a/dist/cjs/client/auth.d.ts
  261. +++ b/dist/cjs/client/auth.d.ts
  262. @@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise<OA
  263. * @returns A Promise that resolves to an OAuthError instance
  264. */
  265. export declare function parseErrorResponse(input: Response | string): Promise<OAuthError>;
  266. +/**
  267. + * Selects scopes per the MCP spec and augments them for refresh token support.
  268. + */
  269. +export declare function determineScope(options: {
  270. + requestedScope?: string;
  271. + resourceMetadata?: OAuthProtectedResourceMetadata;
  272. + authServerMetadata?: AuthorizationServerMetadata;
  273. + clientMetadata: OAuthClientMetadata;
  274. +}): string | undefined;
  275. /**
  276. * Orchestrates the full auth flow with a server.
  277. *
  278. diff --git a/dist/cjs/client/auth.js b/dist/cjs/client/auth.js
  279. index c2e4fa91d26f5336889f6afa416147db75fc4872..178d7cfd96412d53bc14bbc13a8f76c11f727ee7 100644
  280. --- a/dist/cjs/client/auth.js
  281. +++ b/dist/cjs/client/auth.js
  282. @@ -7,6 +7,7 @@ exports.UnauthorizedError = void 0;
  283. exports.selectClientAuthMethod = selectClientAuthMethod;
  284. exports.parseErrorResponse = parseErrorResponse;
  285. exports.auth = auth;
  286. +exports.determineScope = determineScope;
  287. exports.isHttpsUrl = isHttpsUrl;
  288. exports.selectResourceURL = selectResourceURL;
  289. exports.extractWWWAuthenticateParams = extractWWWAuthenticateParams;
  290. @@ -186,6 +187,19 @@ async function auth(provider, options) {
  291. throw error;
  292. }
  293. }
  294. +/**
  295. + * Selects scopes per the MCP spec and augments them for refresh token support.
  296. + */
  297. +function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) {
  298. + let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope;
  299. + if (effectiveScope &&
  300. + authServerMetadata?.scopes_supported?.includes('offline_access') &&
  301. + !effectiveScope.split(' ').includes('offline_access') &&
  302. + clientMetadata.grant_types?.includes('refresh_token')) {
  303. + effectiveScope = `${effectiveScope} offline_access`;
  304. + }
  305. + return effectiveScope;
  306. +}
  307. async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
  308. // Check if the provider has cached discovery state to skip discovery
  309. const cachedState = await provider.discoveryState?.();
  310. @@ -241,12 +255,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
  311. });
  312. }
  313. const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
  314. - // Apply scope selection strategy (SEP-835):
  315. - // 1. WWW-Authenticate scope (passed via `scope` param)
  316. - // 2. PRM scopes_supported
  317. - // 3. Client metadata scope (user-configured fallback)
  318. - // The resolved scope is used consistently for both DCR and the authorization request.
  319. - const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
  320. + const resolvedScope = determineScope({
  321. + requestedScope: scope,
  322. + resourceMetadata,
  323. + authServerMetadata: metadata,
  324. + clientMetadata: provider.clientMetadata
  325. + });
  326. // Handle client registration if needed
  327. let clientInformation = await Promise.resolve(provider.clientInformation());
  328. if (!clientInformation) {
  329. @@ -741,7 +755,7 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
  330. if (scope) {
  331. authorizationUrl.searchParams.set('scope', scope);
  332. }
  333. - if (scope?.includes('offline_access')) {
  334. + if (scope?.split(' ').includes('offline_access')) {
  335. // if the request includes the OIDC-only "offline_access" scope,
  336. // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
  337. // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
  338. diff --git a/dist/esm/client/auth.d.ts b/dist/esm/client/auth.d.ts
  339. index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644
  340. --- a/dist/esm/client/auth.d.ts
  341. +++ b/dist/esm/client/auth.d.ts
  342. @@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise<OA
  343. * @returns A Promise that resolves to an OAuthError instance
  344. */
  345. export declare function parseErrorResponse(input: Response | string): Promise<OAuthError>;
  346. +/**
  347. + * Selects scopes per the MCP spec and augments them for refresh token support.
  348. + */
  349. +export declare function determineScope(options: {
  350. + requestedScope?: string;
  351. + resourceMetadata?: OAuthProtectedResourceMetadata;
  352. + authServerMetadata?: AuthorizationServerMetadata;
  353. + clientMetadata: OAuthClientMetadata;
  354. +}): string | undefined;
  355. /**
  356. * Orchestrates the full auth flow with a server.
  357. *
  358. diff --git a/dist/esm/client/auth.js b/dist/esm/client/auth.js
  359. index e183040fc2bba22ca1ccc784984f3310854403b7..d367661e580ee61a96654f7af78b2af61dcad98b 100644
  360. --- a/dist/esm/client/auth.js
  361. +++ b/dist/esm/client/auth.js
  362. @@ -161,6 +161,19 @@ export async function auth(provider, options) {
  363. throw error;
  364. }
  365. }
  366. +/**
  367. + * Selects scopes per the MCP spec and augments them for refresh token support.
  368. + */
  369. +export function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) {
  370. + let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope;
  371. + if (effectiveScope &&
  372. + authServerMetadata?.scopes_supported?.includes('offline_access') &&
  373. + !effectiveScope.split(' ').includes('offline_access') &&
  374. + clientMetadata.grant_types?.includes('refresh_token')) {
  375. + effectiveScope = `${effectiveScope} offline_access`;
  376. + }
  377. + return effectiveScope;
  378. +}
  379. async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
  380. // Check if the provider has cached discovery state to skip discovery
  381. const cachedState = await provider.discoveryState?.();
  382. @@ -216,12 +229,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
  383. });
  384. }
  385. const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
  386. - // Apply scope selection strategy (SEP-835):
  387. - // 1. WWW-Authenticate scope (passed via `scope` param)
  388. - // 2. PRM scopes_supported
  389. - // 3. Client metadata scope (user-configured fallback)
  390. - // The resolved scope is used consistently for both DCR and the authorization request.
  391. - const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
  392. + const resolvedScope = determineScope({
  393. + requestedScope: scope,
  394. + resourceMetadata,
  395. + authServerMetadata: metadata,
  396. + clientMetadata: provider.clientMetadata
  397. + });
  398. // Handle client registration if needed
  399. let clientInformation = await Promise.resolve(provider.clientInformation());
  400. if (!clientInformation) {
  401. @@ -716,7 +729,7 @@ export async function startAuthorization(authorizationServerUrl, { metadata, cli
  402. if (scope) {
  403. authorizationUrl.searchParams.set('scope', scope);
  404. }
  405. - if (scope?.includes('offline_access')) {
  406. + if (scope?.split(' ').includes('offline_access')) {
  407. // if the request includes the OIDC-only "offline_access" scope,
  408. // we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
  409. // https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
  410. diff --git a/dist/esm/client/index.js b/dist/esm/client/index.js
  411. index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8eb9e4caff 100644
  412. --- a/dist/esm/client/index.js
  413. +++ b/dist/esm/client/index.js
  414. @@ -284,41 +284,16 @@ export class Client extends Protocol {
  415. }
  416. async connect(transport, options) {
  417. await super.connect(transport);
  418. + transport.onsessionexpired = async () => {
  419. + await this._initialize(transport);
  420. + };
  421. // When transport sessionId is already set this means we are trying to reconnect.
  422. // In this case we don't need to initialize again.
  423. if (transport.sessionId !== undefined) {
  424. return;
  425. }
  426. try {
  427. - const result = await this.request({
  428. - method: 'initialize',
  429. - params: {
  430. - protocolVersion: LATEST_PROTOCOL_VERSION,
  431. - capabilities: this._capabilities,
  432. - clientInfo: this._clientInfo
  433. - }
  434. - }, InitializeResultSchema, options);
  435. - if (result === undefined) {
  436. - throw new Error(`Server sent invalid initialize result: ${result}`);
  437. - }
  438. - if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
  439. - throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
  440. - }
  441. - this._serverCapabilities = result.capabilities;
  442. - this._serverVersion = result.serverInfo;
  443. - // HTTP transports must set the protocol version in each header after initialization.
  444. - if (transport.setProtocolVersion) {
  445. - transport.setProtocolVersion(result.protocolVersion);
  446. - }
  447. - this._instructions = result.instructions;
  448. - await this.notification({
  449. - method: 'notifications/initialized'
  450. - });
  451. - // Set up list changed handlers now that we know server capabilities
  452. - if (this._pendingListChangedConfig) {
  453. - this._setupListChangedHandlers(this._pendingListChangedConfig);
  454. - this._pendingListChangedConfig = undefined;
  455. - }
  456. + await this._initialize(transport, options);
  457. }
  458. catch (error) {
  459. // Disconnect if initialization fails.
  460. @@ -326,6 +301,37 @@ export class Client extends Protocol {
  461. throw error;
  462. }
  463. }
  464. + async _initialize(transport, options) {
  465. + const result = await this.request({
  466. + method: 'initialize',
  467. + params: {
  468. + protocolVersion: LATEST_PROTOCOL_VERSION,
  469. + capabilities: this._capabilities,
  470. + clientInfo: this._clientInfo
  471. + }
  472. + }, InitializeResultSchema, options);
  473. + if (result === undefined) {
  474. + throw new Error(`Server sent invalid initialize result: ${result}`);
  475. + }
  476. + if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) {
  477. + throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
  478. + }
  479. + this._serverCapabilities = result.capabilities;
  480. + this._serverVersion = result.serverInfo;
  481. + // HTTP transports must set the protocol version in each header after initialization.
  482. + if (transport.setProtocolVersion) {
  483. + transport.setProtocolVersion(result.protocolVersion);
  484. + }
  485. + this._instructions = result.instructions;
  486. + await this.notification({
  487. + method: 'notifications/initialized'
  488. + });
  489. + // Set up list changed handlers now that we know server capabilities
  490. + if (this._pendingListChangedConfig) {
  491. + this._setupListChangedHandlers(this._pendingListChangedConfig);
  492. + this._pendingListChangedConfig = undefined;
  493. + }
  494. + }
  495. /**
  496. * After initialization has completed, this will be populated with the server's reported capabilities.
  497. */
  498. @@ -537,9 +543,11 @@ export class Client extends Protocol {
  499. * Called after listTools() to pre-compile validators for better performance.
  500. */
  501. - cacheToolMetadata(tools) {
  502. - this._cachedToolOutputValidators.clear();
  503. - this._cachedKnownTaskTools.clear();
  504. - this._cachedRequiredTaskTools.clear();
  505. + cacheToolMetadata(tools, reset = true) {
  506. + if (reset) {
  507. + this._cachedToolOutputValidators.clear();
  508. + this._cachedKnownTaskTools.clear();
  509. + this._cachedRequiredTaskTools.clear();
  510. + }
  511. for (const tool of tools) {
  512. // If the tool has an outputSchema, create and cache the validator
  513. if (tool.outputSchema) {
  514. @@ -565,7 +573,7 @@ export class Client extends Protocol {
  515. async listTools(params, options) {
  516. const result = await this.request({ method: 'tools/list', params }, ListToolsResultSchema, options);
  517. // Cache the tools and their output schemas for future validation
  518. - this.cacheToolMetadata(result.tools);
  519. + this.cacheToolMetadata(result.tools, params?.cursor === undefined);
  520. return result;
  521. }
  522. /**
  523. diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js
  524. index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da884fcc627a 100644
  525. --- a/dist/esm/client/streamableHttp.js
  526. +++ b/dist/esm/client/streamableHttp.js
  527. @@ -1,5 +1,5 @@
  528. import { createFetchWithInit, normalizeHeaders } from '../shared/transport.js';
  529. -import { isInitializedNotification, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
  530. +import { isInitializedNotification, isInitializeRequest, isJSONRPCErrorResponse, isJSONRPCRequest, isJSONRPCResultResponse, JSONRPCMessageSchema } from '../types.js';
  531. import { auth, extractWWWAuthenticateParams, UnauthorizedError } from './auth.js';
  532. import { EventSourceParserStream } from 'eventsource-parser/stream';
  533. // Default reconnection options for StreamableHTTP connections
  534. @@ -200,7 +200,7 @@ export class StreamableHTTPClientTransport {
  535. if (!event.event || event.event === 'message') {
  536. try {
  537. const message = JSONRPCMessageSchema.parse(JSON.parse(event.data));
  538. - if (isJSONRPCResultResponse(message)) {
  539. + if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
  540. // Mark that we received a response - no need to reconnect for this request
  541. receivedResponse = true;
  542. if (replayMessageId !== undefined) {
  543. @@ -286,7 +286,38 @@ export class StreamableHTTPClientTransport {
  544. this.onclose?.();
  545. }
  546. async send(message, options) {
  547. + return this._send(message, options, false);
  548. + }
  549. + async _recoverSession(expiredSessionId) {
  550. + if (this._sessionRecovery) {
  551. + await this._sessionRecovery;
  552. + return true;
  553. + }
  554. + if (this._sessionId !== expiredSessionId)
  555. + return true;
  556. + this._sessionId = undefined;
  557. + this._sessionRecovery = Promise.resolve().then(() => this.onsessionexpired?.());
  558. try {
  559. + await this._sessionRecovery;
  560. + }
  561. + catch (error) {
  562. + this._sessionId = undefined;
  563. + await this.close();
  564. + throw error;
  565. + }
  566. + finally {
  567. + this._sessionRecovery = undefined;
  568. + }
  569. + return true;
  570. + }
  571. + async _send(message, options, isSessionRetry) {
  572. + try {
  573. + if (this._sessionRecovery && !isInitializeRequest(message) && !isInitializedNotification(message)) {
  574. + await this._sessionRecovery;
  575. + if (options?.isRequestActive?.() === false) {
  576. + throw new Error('Request is no longer active');
  577. + }
  578. + }
  579. const { resumptionToken, onresumptiontoken } = options || {};
  580. if (resumptionToken) {
  581. // If we have at last event ID, we need to reconnect the SSE stream
  582. @@ -294,6 +325,7 @@ export class StreamableHTTPClientTransport {
  583. return;
  584. }
  585. const headers = await this._commonHeaders();
  586. + const requestSessionId = headers.get('mcp-session-id') ?? undefined;
  587. headers.set('content-type', 'application/json');
  588. headers.set('accept', 'application/json, text/event-stream');
  589. const init = {
  590. @@ -306,11 +338,20 @@ export class StreamableHTTPClientTransport {
  591. const response = await (this._fetch ?? fetch)(this._url, init);
  592. // Handle session ID received during initialization
  593. const sessionId = response.headers.get('mcp-session-id');
  594. - if (sessionId) {
  595. + if (sessionId && (requestSessionId === undefined || this._sessionId === requestSessionId)) {
  596. this._sessionId = sessionId;
  597. }
  598. if (!response.ok) {
  599. const text = await response.text().catch(() => null);
  600. + if (response.status === 404 && requestSessionId && !isSessionRetry && !isInitializedNotification(message)) {
  601. + const recovered = await this._recoverSession(requestSessionId);
  602. + if (options?.isRequestActive?.() === false) {
  603. + throw new Error('Request is no longer active');
  604. + }
  605. + if (recovered) {
  606. + return this._send(message, options, true);
  607. + }
  608. + }
  609. if (response.status === 401 && this._authProvider) {
  610. // Prevent infinite recursion when server returns 401 after successful auth
  611. if (this._hasCompletedAuthFlow) {
  612. @@ -331,7 +372,7 @@ export class StreamableHTTPClientTransport {
  613. // Mark that we completed auth flow
  614. this._hasCompletedAuthFlow = true;
  615. // Purposely _not_ awaited, so we don't call onerror twice
  616. - return this.send(message);
  617. + return this._send(message, options, isSessionRetry);
  618. }
  619. if (response.status === 403 && this._authProvider) {
  620. const { resourceMetadataUrl, scope, error } = extractWWWAuthenticateParams(response);
  621. @@ -358,7 +399,7 @@ export class StreamableHTTPClientTransport {
  622. if (result !== 'AUTHORIZED') {
  623. throw new UnauthorizedError();
  624. }
  625. - return this.send(message);
  626. + return this._send(message, options, isSessionRetry);
  627. }
  628. }
  629. throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
  630. diff --git a/dist/esm/shared/protocol.js b/dist/esm/shared/protocol.js
  631. index bfa2b7120a0f50c569364ea5264e6f811076f44f..abd8dfd707c155f71dae7aeeeeaf7547368ac749 100644
  632. --- a/dist/esm/shared/protocol.js
  633. +++ b/dist/esm/shared/protocol.js
  634. @@ -740,7 +740,12 @@ export class Protocol {
  635. }
  636. else {
  637. // No related task - send through transport normally
  638. - this._transport.send(jsonrpcRequest, { relatedRequestId, resumptionToken, onresumptiontoken }).catch(error => {
  639. + this._transport.send(jsonrpcRequest, {
  640. + relatedRequestId,
  641. + resumptionToken,
  642. + onresumptiontoken,
  643. + isRequestActive: () => this._responseHandlers.has(messageId)
  644. + }).catch(error => {
  645. this._cleanupTimeout(messageId);
  646. reject(error);
  647. });