@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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +15 -0
- package/README.md +144 -92
- package/dist/_tsup-dts-rollup.d.cts +100 -40
- package/dist/_tsup-dts-rollup.d.ts +100 -40
- package/dist/index.cjs +200 -66
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +197 -67
- package/package.json +4 -3
- package/src/client.test.ts +121 -33
- package/src/client.ts +172 -56
- package/src/configuration.test.ts +21 -36
- package/src/configuration.ts +107 -26
- package/src/index.ts +2 -1
- package/src/server-logging.test.ts +4 -4
- package/src/server.test.ts +2 -2
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
4179
|
-
|
|
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",
|
|
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
|
|
4262
|
-
var
|
|
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: "
|
|
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
|
-
|
|
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(`
|
|
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
|
|
4280
|
-
2. Call "await
|
|
4281
|
-
3. If you only need one instance of
|
|
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 (!
|
|
4291
|
-
|
|
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(`
|
|
4388
|
+
const idNamespace = v5(`MCPClient`, v5.DNS);
|
|
4297
4389
|
return v5(text, idNamespace);
|
|
4298
4390
|
}
|
|
4299
4391
|
async disconnect() {
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
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
|
|
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(`
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
30
|
+
"@mastra/core": "^0.9.1-alpha.3"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@ai-sdk/anthropic": "^1.1.15",
|
package/src/client.test.ts
CHANGED
|
@@ -1,49 +1,137 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
await everArtClient.connect();
|
|
38
|
+
const serverTransport = new StreamableHTTPServerTransport({
|
|
39
|
+
sessionIdGenerator: withSessionManagement ? () => randomUUID() : undefined,
|
|
27
40
|
});
|
|
28
41
|
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
await mcpServer.connect(serverTransport);
|
|
43
|
+
|
|
44
|
+
httpServer.on('request', async (req, res) => {
|
|
45
|
+
await serverTransport.handleRequest(req, res);
|
|
31
46
|
});
|
|
32
47
|
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
55
|
+
return { httpServer, mcpServer, serverTransport, baseUrl };
|
|
56
|
+
}
|
|
37
57
|
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
});
|