@mastra/mcp 0.4.1-alpha.2 → 0.4.1-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,9 +4,11 @@ import { jsonSchemaToModel } from '@mastra/core/utils';
4
4
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
5
  import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
6
6
  import { StdioClientTransport, getDefaultEnvironment } from '@modelcontextprotocol/sdk/client/stdio.js';
7
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
7
8
  import { DEFAULT_REQUEST_TIMEOUT_MSEC } from '@modelcontextprotocol/sdk/shared/protocol.js';
8
9
  import { ListResourcesResultSchema, CallToolResultSchema, ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
9
10
  import { asyncExitHook, gracefulExit } from 'exit-hook';
11
+ import equal from 'fast-deep-equal';
10
12
  import { v5 } from 'uuid';
11
13
  import { isVercelTool, isZodType, resolveSerializedZodOutput } from '@mastra/core';
12
14
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
@@ -4093,13 +4095,15 @@ function convertLogLevelToLoggerMethod(level) {
4093
4095
  return "info";
4094
4096
  }
4095
4097
  }
4096
- var MastraMCPClient = class extends MastraBase {
4098
+ var InternalMastraMCPClient = class _InternalMastraMCPClient extends MastraBase {
4097
4099
  name;
4098
- transport;
4099
4100
  client;
4100
4101
  timeout;
4101
4102
  logHandler;
4102
4103
  enableServerLogs;
4104
+ static hasWarned = false;
4105
+ serverConfig;
4106
+ transport;
4103
4107
  constructor({
4104
4108
  name,
4105
4109
  version = "1.0.0",
@@ -4108,23 +4112,17 @@ var MastraMCPClient = class extends MastraBase {
4108
4112
  timeout = DEFAULT_REQUEST_TIMEOUT_MSEC
4109
4113
  }) {
4110
4114
  super({ name: "MastraMCPClient" });
4115
+ if (!_InternalMastraMCPClient.hasWarned) {
4116
+ console.warn(
4117
+ "[DEPRECATION] MastraMCPClient is deprecated and will be removed in a future release. Please use MCPClient instead."
4118
+ );
4119
+ _InternalMastraMCPClient.hasWarned = true;
4120
+ }
4111
4121
  this.name = name;
4112
4122
  this.timeout = timeout;
4113
4123
  this.logHandler = server.logger;
4114
4124
  this.enableServerLogs = server.enableServerLogs ?? true;
4115
- const { logger: logger3, enableServerLogs, ...serverConfig } = server;
4116
- if (`url` in serverConfig) {
4117
- this.transport = new SSEClientTransport(serverConfig.url, {
4118
- requestInit: serverConfig.requestInit,
4119
- eventSourceInit: serverConfig.eventSourceInit
4120
- });
4121
- } else {
4122
- this.transport = new StdioClientTransport({
4123
- ...serverConfig,
4124
- // without ...getDefaultEnvironment() commands like npx will fail because there will be no PATH env var
4125
- env: { ...getDefaultEnvironment(), ...serverConfig.env || {} }
4126
- });
4127
- }
4125
+ this.serverConfig = server;
4128
4126
  this.client = new Client(
4129
4127
  {
4130
4128
  name,
@@ -4144,11 +4142,12 @@ var MastraMCPClient = class extends MastraBase {
4144
4142
  */
4145
4143
  log(level, message, details) {
4146
4144
  const loggerMethod = convertLogLevelToLoggerMethod(level);
4147
- this.logger[loggerMethod](message, details);
4145
+ const msg = `[${this.name}] ${message}`;
4146
+ this.logger[loggerMethod](msg, details);
4148
4147
  if (this.logHandler) {
4149
4148
  this.logHandler({
4150
4149
  level,
4151
- message,
4150
+ message: msg,
4152
4151
  timestamp: /* @__PURE__ */ new Date(),
4153
4152
  serverName: this.name,
4154
4153
  details
@@ -4171,44 +4170,121 @@ var MastraMCPClient = class extends MastraBase {
4171
4170
  );
4172
4171
  }
4173
4172
  }
4173
+ async connectStdio(command) {
4174
+ this.log("debug", `Using Stdio transport for command: ${command}`);
4175
+ try {
4176
+ this.transport = new StdioClientTransport({
4177
+ command,
4178
+ args: this.serverConfig.args,
4179
+ env: { ...getDefaultEnvironment(), ...this.serverConfig.env || {} }
4180
+ });
4181
+ await this.client.connect(this.transport, { timeout: this.serverConfig.timeout ?? this.timeout });
4182
+ this.log("debug", `Successfully connected to MCP server via Stdio`);
4183
+ } catch (e) {
4184
+ this.log("error", e instanceof Error ? e.stack || e.message : JSON.stringify(e));
4185
+ throw e;
4186
+ }
4187
+ }
4188
+ async connectHttp(url) {
4189
+ const { requestInit, eventSourceInit } = this.serverConfig;
4190
+ this.log("debug", `Attempting to connect to URL: ${url}`);
4191
+ let shouldTrySSE = url.pathname.endsWith(`/sse`);
4192
+ if (!shouldTrySSE) {
4193
+ try {
4194
+ this.log("debug", "Trying Streamable HTTP transport...");
4195
+ const streamableTransport = new StreamableHTTPClientTransport(url, {
4196
+ requestInit,
4197
+ reconnectionOptions: this.serverConfig.reconnectionOptions,
4198
+ sessionId: this.serverConfig.sessionId
4199
+ });
4200
+ await this.client.connect(streamableTransport, {
4201
+ timeout: (
4202
+ // this is hardcoded to 3s because the long default timeout would be extremely slow for sse backwards compat (60s)
4203
+ 3e3
4204
+ )
4205
+ });
4206
+ this.transport = streamableTransport;
4207
+ this.log("debug", "Successfully connected using Streamable HTTP transport.");
4208
+ } catch (error) {
4209
+ this.log("debug", `Streamable HTTP transport failed: ${error}`);
4210
+ shouldTrySSE = true;
4211
+ }
4212
+ }
4213
+ if (shouldTrySSE) {
4214
+ this.log("debug", "Falling back to deprecated HTTP+SSE transport...");
4215
+ try {
4216
+ const sseTransport = new SSEClientTransport(url, { requestInit, eventSourceInit });
4217
+ await this.client.connect(sseTransport, { timeout: this.serverConfig.timeout ?? this.timeout });
4218
+ this.transport = sseTransport;
4219
+ this.log("debug", "Successfully connected using deprecated HTTP+SSE transport.");
4220
+ } catch (sseError) {
4221
+ this.log(
4222
+ "error",
4223
+ `Failed to connect with SSE transport after failing to connect to Streamable HTTP transport first. SSE error: ${sseError}`
4224
+ );
4225
+ throw new Error("Could not connect to server with any available HTTP transport");
4226
+ }
4227
+ }
4228
+ }
4174
4229
  isConnected = false;
4175
4230
  async connect() {
4176
4231
  if (this.isConnected) return;
4232
+ const { command, url } = this.serverConfig;
4233
+ if (command) {
4234
+ await this.connectStdio(command);
4235
+ } else if (url) {
4236
+ await this.connectHttp(url);
4237
+ } else {
4238
+ throw new Error("Server configuration must include either a command or a url.");
4239
+ }
4240
+ this.isConnected = true;
4241
+ const originalOnClose = this.client.onclose;
4242
+ this.client.onclose = () => {
4243
+ this.log("debug", `MCP server connection closed`);
4244
+ this.isConnected = false;
4245
+ if (typeof originalOnClose === `function`) {
4246
+ originalOnClose();
4247
+ }
4248
+ };
4249
+ asyncExitHook(
4250
+ async () => {
4251
+ this.log("debug", `Disconnecting MCP server during exit`);
4252
+ await this.disconnect();
4253
+ },
4254
+ { wait: 5e3 }
4255
+ );
4256
+ process.on("SIGTERM", () => gracefulExit());
4257
+ this.log("debug", `Successfully connected to MCP server`);
4258
+ }
4259
+ /**
4260
+ * Get the current session ID if using the Streamable HTTP transport.
4261
+ * Returns undefined if not connected or not using Streamable HTTP.
4262
+ */
4263
+ get sessionId() {
4264
+ if (this.transport instanceof StreamableHTTPClientTransport) {
4265
+ return this.transport.sessionId;
4266
+ }
4267
+ return void 0;
4268
+ }
4269
+ async disconnect() {
4270
+ if (!this.transport) {
4271
+ this.log("debug", "Disconnect called but no transport was connected.");
4272
+ return;
4273
+ }
4274
+ this.log("debug", `Disconnecting from MCP server`);
4177
4275
  try {
4178
- this.log("debug", `Connecting to MCP server`);
4179
- await this.client.connect(this.transport, {
4180
- timeout: this.timeout
4181
- });
4182
- this.isConnected = true;
4183
- const originalOnClose = this.client.onclose;
4184
- this.client.onclose = () => {
4185
- this.log("debug", `MCP server connection closed`);
4186
- this.isConnected = false;
4187
- if (typeof originalOnClose === `function`) {
4188
- originalOnClose();
4189
- }
4190
- };
4191
- asyncExitHook(
4192
- async () => {
4193
- this.log("debug", `Disconnecting MCP server during exit`);
4194
- await this.disconnect();
4195
- },
4196
- { wait: 5e3 }
4197
- );
4198
- process.on("SIGTERM", () => gracefulExit());
4199
- this.log("info", `Successfully connected to MCP server`);
4276
+ await this.transport.close();
4277
+ this.log("debug", "Successfully disconnected from MCP server");
4200
4278
  } catch (e) {
4201
- this.log("error", `Failed connecting to MCP server`, {
4279
+ this.log("error", "Error during MCP server disconnect", {
4202
4280
  error: e instanceof Error ? e.stack : JSON.stringify(e, null, 2)
4203
4281
  });
4204
- this.isConnected = false;
4205
4282
  throw e;
4283
+ } finally {
4284
+ this.transport = void 0;
4285
+ this.isConnected = false;
4206
4286
  }
4207
4287
  }
4208
- async disconnect() {
4209
- this.log("debug", `Disconnecting from MCP server`);
4210
- return await this.client.close();
4211
- }
4212
4288
  // TODO: do the type magic to return the right method type. Right now we get infinitely deep infered type errors from Zod without using "any"
4213
4289
  async resources() {
4214
4290
  this.log("debug", `Requesting resources from MCP server`);
@@ -4258,48 +4334,74 @@ var MastraMCPClient = class extends MastraBase {
4258
4334
  return toolsRes;
4259
4335
  }
4260
4336
  };
4261
- var mastraMCPConfigurationInstances = /* @__PURE__ */ new Map();
4262
- var MCPConfiguration = class extends MastraBase {
4337
+ var MastraMCPClient = InternalMastraMCPClient;
4338
+ var mcpClientInstances = /* @__PURE__ */ new Map();
4339
+ var MCPClient = class extends MastraBase {
4263
4340
  serverConfigs = {};
4264
4341
  id;
4265
4342
  defaultTimeout;
4343
+ mcpClientsById = /* @__PURE__ */ new Map();
4344
+ disconnectPromise = null;
4266
4345
  constructor(args) {
4267
- super({ name: "MCPConfiguration" });
4346
+ super({ name: "MCPClient" });
4268
4347
  this.defaultTimeout = args.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
4269
4348
  this.serverConfigs = args.servers;
4270
4349
  this.id = args.id ?? this.makeId();
4271
- const existingInstance = mastraMCPConfigurationInstances.get(this.id);
4350
+ if (args.id) {
4351
+ this.id = args.id;
4352
+ const cached = mcpClientInstances.get(this.id);
4353
+ if (cached && !equal(cached.serverConfigs, args.servers)) {
4354
+ const existingInstance2 = mcpClientInstances.get(this.id);
4355
+ if (existingInstance2) {
4356
+ void existingInstance2.disconnect();
4357
+ }
4358
+ }
4359
+ } else {
4360
+ this.id = this.makeId();
4361
+ }
4362
+ const existingInstance = mcpClientInstances.get(this.id);
4272
4363
  if (existingInstance) {
4273
4364
  if (!args.id) {
4274
- throw new Error(`MCPConfiguration was initialized multiple times with the same configuration options.
4365
+ throw new Error(`MCPClient was initialized multiple times with the same configuration options.
4275
4366
 
4276
4367
  This error is intended to prevent memory leaks.
4277
4368
 
4278
4369
  To fix this you have three different options:
4279
- 1. If you need multiple MCPConfiguration class instances with identical server configurations, set an id when configuring: new MCPConfiguration({ id: "my-unique-id" })
4280
- 2. Call "await configuration.disconnect()" after you're done using the configuration and before you recreate another instance with the same options. If the identical MCPConfiguration instance is already closed at the time of re-creating it, you will not see this error.
4281
- 3. If you only need one instance of MCPConfiguration in your app, refactor your code so it's only created one time (ex. move it out of a loop into a higher scope code block)
4370
+ 1. If you need multiple MCPClient class instances with identical server configurations, set an id when configuring: new MCPClient({ id: "my-unique-id" })
4371
+ 2. Call "await client.disconnect()" after you're done using the client and before you recreate another instance with the same options. If the identical MCPClient instance is already closed at the time of re-creating it, you will not see this error.
4372
+ 3. If you only need one instance of MCPClient in your app, refactor your code so it's only created one time (ex. move it out of a loop into a higher scope code block)
4282
4373
  `);
4283
4374
  }
4284
4375
  return existingInstance;
4285
4376
  }
4377
+ mcpClientInstances.set(this.id, this);
4286
4378
  this.addToInstanceCache();
4287
4379
  return this;
4288
4380
  }
4289
4381
  addToInstanceCache() {
4290
- if (!mastraMCPConfigurationInstances.has(this.id)) {
4291
- mastraMCPConfigurationInstances.set(this.id, this);
4382
+ if (!mcpClientInstances.has(this.id)) {
4383
+ mcpClientInstances.set(this.id, this);
4292
4384
  }
4293
4385
  }
4294
4386
  makeId() {
4295
4387
  const text = JSON.stringify(this.serverConfigs).normalize("NFKC");
4296
- const idNamespace = v5(`MCPConfiguration`, v5.DNS);
4388
+ const idNamespace = v5(`MCPClient`, v5.DNS);
4297
4389
  return v5(text, idNamespace);
4298
4390
  }
4299
4391
  async disconnect() {
4300
- mastraMCPConfigurationInstances.delete(this.id);
4301
- await Promise.all(Array.from(this.mcpClientsById.values()).map((client) => client.disconnect()));
4302
- this.mcpClientsById.clear();
4392
+ if (this.disconnectPromise) {
4393
+ return this.disconnectPromise;
4394
+ }
4395
+ this.disconnectPromise = (async () => {
4396
+ try {
4397
+ mcpClientInstances.delete(this.id);
4398
+ await Promise.all(Array.from(this.mcpClientsById.values()).map((client) => client.disconnect()));
4399
+ this.mcpClientsById.clear();
4400
+ } finally {
4401
+ this.disconnectPromise = null;
4402
+ }
4403
+ })();
4404
+ return this.disconnectPromise;
4303
4405
  }
4304
4406
  async getTools() {
4305
4407
  this.addToInstanceCache();
@@ -4321,16 +4423,34 @@ To fix this you have three different options:
4321
4423
  });
4322
4424
  return connectedToolsets;
4323
4425
  }
4324
- mcpClientsById = /* @__PURE__ */ new Map();
4426
+ /**
4427
+ * Get the current session IDs for all connected MCP clients using the Streamable HTTP transport.
4428
+ * Returns an object mapping server names to their session IDs.
4429
+ */
4430
+ get sessionIds() {
4431
+ const sessionIds = {};
4432
+ for (const [serverName, client] of this.mcpClientsById.entries()) {
4433
+ if (client.sessionId) {
4434
+ sessionIds[serverName] = client.sessionId;
4435
+ }
4436
+ }
4437
+ return sessionIds;
4438
+ }
4325
4439
  async getConnectedClient(name, config) {
4440
+ if (this.disconnectPromise) {
4441
+ await this.disconnectPromise;
4442
+ }
4326
4443
  const exists = this.mcpClientsById.has(name);
4444
+ const existingClient = this.mcpClientsById.get(name);
4327
4445
  if (exists) {
4328
- const mcpClient2 = this.mcpClientsById.get(name);
4329
- await mcpClient2.connect();
4330
- return mcpClient2;
4446
+ if (!existingClient) {
4447
+ throw new Error(`Client ${name} exists but is undefined`);
4448
+ }
4449
+ await existingClient.connect();
4450
+ return existingClient;
4331
4451
  }
4332
4452
  this.logger.debug(`Connecting to ${name} MCP server`);
4333
- const mcpClient = new MastraMCPClient({
4453
+ const mcpClient = new InternalMastraMCPClient({
4334
4454
  name,
4335
4455
  server: config,
4336
4456
  timeout: config.timeout ?? this.defaultTimeout
@@ -4340,10 +4460,12 @@ To fix this you have three different options:
4340
4460
  await mcpClient.connect();
4341
4461
  } catch (e) {
4342
4462
  this.mcpClientsById.delete(name);
4343
- this.logger.error(`MCPConfiguration errored connecting to MCP server ${name}`, {
4463
+ this.logger.error(`MCPClient errored connecting to MCP server ${name}`, {
4344
4464
  error: e instanceof Error ? e.message : String(e)
4345
4465
  });
4346
- throw new Error(`Failed to connect to MCP server ${name}: ${e instanceof Error ? e.message : String(e)}`);
4466
+ throw new Error(
4467
+ `Failed to connect to MCP server ${name}: ${e instanceof Error ? e.stack || e.message : String(e)}`
4468
+ );
4347
4469
  }
4348
4470
  this.logger.debug(`Connected to ${name} MCP server`);
4349
4471
  return mcpClient;
@@ -4358,6 +4480,14 @@ To fix this you have three different options:
4358
4480
  );
4359
4481
  }
4360
4482
  };
4483
+ var MCPConfiguration = class extends MCPClient {
4484
+ constructor(args) {
4485
+ super(args);
4486
+ this.logger.warn(
4487
+ `MCPConfiguration has been renamed to MCPClient and MCPConfiguration is deprecated. The API is identical but the MCPConfiguration export will be removed in the future. Update your imports now to prevent future errors.`
4488
+ );
4489
+ }
4490
+ };
4361
4491
 
4362
4492
  // ../../node_modules/.pnpm/json-schema-to-zod@2.6.0/node_modules/json-schema-to-zod/dist/esm/parsers/parseAnyOf.js
4363
4493
  var parseAnyOf = (schema, refs) => {
@@ -6478,4 +6608,4 @@ var MCPServer = class {
6478
6608
  }
6479
6609
  };
6480
6610
 
6481
- export { MCPConfiguration, MCPServer, MastraMCPClient };
6611
+ export { MCPClient, MCPConfiguration, MCPServer, MastraMCPClient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/mcp",
3
- "version": "0.4.1-alpha.2",
3
+ "version": "0.4.1-alpha.4",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,11 +22,12 @@
22
22
  "author": "",
23
23
  "license": "Elastic-2.0",
24
24
  "dependencies": {
25
- "@modelcontextprotocol/sdk": "^1.9.0",
25
+ "@modelcontextprotocol/sdk": "^1.10.2",
26
26
  "date-fns": "^4.1.0",
27
27
  "exit-hook": "^4.0.0",
28
+ "fast-deep-equal": "^3.1.3",
28
29
  "uuid": "^11.1.0",
29
- "@mastra/core": "^0.9.1-alpha.2"
30
+ "@mastra/core": "^0.9.1-alpha.3"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@ai-sdk/anthropic": "^1.1.15",
@@ -1,49 +1,137 @@
1
- import { anthropic } from '@ai-sdk/anthropic';
2
- import { Agent } from '@mastra/core/agent';
3
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
1
+ import { randomUUID } from 'node:crypto';
2
+ import { createServer } from 'node:http';
3
+ import type { Server as HttpServer } from 'node:http';
4
+ import type { AddressInfo } from 'node:net';
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
7
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
8
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
9
+ import { z } from 'zod';
4
10
 
5
11
  import { MastraMCPClient } from './client.js';
6
12
 
7
- const everArtClient = new MastraMCPClient({
8
- name: 'everart',
9
- server: {
10
- command: '/usr/local/bin/docker',
11
- args: ['run', '-i', '--rm', '--network=host', '-e', 'EVERART_API_KEY', 'mcp/everart'],
12
- env: {
13
- EVERART_API_KEY: process.env.EVERART_API_KEY!,
13
+ async function setupTestServer(withSessionManagement: boolean) {
14
+ const httpServer: HttpServer = createServer();
15
+ const mcpServer = new McpServer(
16
+ { name: 'test-http-server', version: '1.0.0' },
17
+ {
18
+ capabilities: {
19
+ logging: {},
20
+ tools: {},
21
+ },
14
22
  },
15
- },
16
- });
23
+ );
17
24
 
18
- const agent = new Agent({
19
- name: 'everart',
20
- instructions: 'You are my artist. Include the url in your response.',
21
- model: anthropic('claude-3-5-sonnet-20241022'),
22
- });
25
+ mcpServer.tool(
26
+ 'greet',
27
+ 'A simple greeting tool',
28
+ {
29
+ name: z.string().describe('Name to greet').default('World'),
30
+ },
31
+ async ({ name }): Promise<CallToolResult> => {
32
+ return {
33
+ content: [{ type: 'text', text: `Hello, ${name}!` }],
34
+ };
35
+ },
36
+ );
23
37
 
24
- describe.skip('MastraMCPClient', () => {
25
- beforeAll(async () => {
26
- await everArtClient.connect();
38
+ const serverTransport = new StreamableHTTPServerTransport({
39
+ sessionIdGenerator: withSessionManagement ? () => randomUUID() : undefined,
27
40
  });
28
41
 
29
- afterAll(async () => {
30
- await everArtClient.disconnect();
42
+ await mcpServer.connect(serverTransport);
43
+
44
+ httpServer.on('request', async (req, res) => {
45
+ await serverTransport.handleRequest(req, res);
31
46
  });
32
47
 
33
- it('Converting tools into Mastra', async () => {
34
- const list = await everArtClient.resources();
48
+ const baseUrl = await new Promise<URL>(resolve => {
49
+ httpServer.listen(0, '127.0.0.1', () => {
50
+ const addr = httpServer.address() as AddressInfo;
51
+ resolve(new URL(`http://127.0.0.1:${addr.port}/mcp`));
52
+ });
53
+ });
35
54
 
36
- expect(list.resources.length).toBeGreaterThan(0);
55
+ return { httpServer, mcpServer, serverTransport, baseUrl };
56
+ }
37
57
 
38
- // The MCP server tools are now available to your Mastra Agents
39
- const tools = await everArtClient.tools();
58
+ describe('MastraMCPClient with Streamable HTTP', () => {
59
+ let testServer: {
60
+ httpServer: HttpServer;
61
+ mcpServer: McpServer;
62
+ serverTransport: StreamableHTTPServerTransport;
63
+ baseUrl: URL;
64
+ };
65
+ let client: MastraMCPClient;
40
66
 
41
- await agent.generate('Can you make me a picture of a dog?', {
42
- toolsets: {
43
- everart: tools,
44
- },
67
+ describe('Stateless Mode', () => {
68
+ beforeEach(async () => {
69
+ testServer = await setupTestServer(false);
70
+ client = new MastraMCPClient({
71
+ name: 'test-stateless-client',
72
+ server: {
73
+ url: testServer.baseUrl,
74
+ },
75
+ });
76
+ await client.connect();
77
+ });
78
+
79
+ afterEach(async () => {
80
+ await client?.disconnect().catch(() => {});
81
+ await testServer?.mcpServer.close().catch(() => {});
82
+ await testServer?.serverTransport.close().catch(() => {});
83
+ testServer?.httpServer.close();
84
+ });
85
+
86
+ it('should connect and list tools', async () => {
87
+ const tools = await client.tools();
88
+ expect(tools).toHaveProperty('greet');
89
+ expect(tools.greet.description).toBe('A simple greeting tool');
45
90
  });
46
91
 
47
- expect(Object.keys(tools).length).toBeGreaterThan(0);
48
- }, 50000);
92
+ it('should call a tool', async () => {
93
+ const tools = await client.tools();
94
+ const result = await tools.greet.execute({ context: { name: 'Stateless' } });
95
+ expect(result).toEqual({ content: [{ type: 'text', text: 'Hello, Stateless!' }] });
96
+ });
97
+ });
98
+
99
+ describe('Stateful Mode', () => {
100
+ beforeEach(async () => {
101
+ testServer = await setupTestServer(true);
102
+ client = new MastraMCPClient({
103
+ name: 'test-stateful-client',
104
+ server: {
105
+ url: testServer.baseUrl,
106
+ },
107
+ });
108
+ await client.connect();
109
+ });
110
+
111
+ afterEach(async () => {
112
+ await client?.disconnect().catch(() => {});
113
+ await testServer?.mcpServer.close().catch(() => {});
114
+ await testServer?.serverTransport.close().catch(() => {});
115
+ testServer?.httpServer.close();
116
+ });
117
+
118
+ it('should connect and list tools', async () => {
119
+ const tools = await client.tools();
120
+ expect(tools).toHaveProperty('greet');
121
+ });
122
+
123
+ it('should capture the session ID after connecting', async () => {
124
+ // The setupTestServer(true) is configured for stateful mode
125
+ // The client should capture the session ID from the server's response
126
+ expect(client.sessionId).toBeDefined();
127
+ expect(typeof client.sessionId).toBe('string');
128
+ expect(client.sessionId?.length).toBeGreaterThan(0);
129
+ });
130
+
131
+ it('should call a tool', async () => {
132
+ const tools = await client.tools();
133
+ const result = await tools.greet.execute({ context: { name: 'Stateful' } });
134
+ expect(result).toEqual({ content: [{ type: 'text', text: 'Hello, Stateful!' }] });
135
+ });
136
+ });
49
137
  });