@modelcontextprotocol%2Fsdk@1.29.0.patch 31 KB

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