@mastra/mcp 0.4.1-alpha.2 → 0.4.1-alpha.3
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 +8 -0
- package/README.md +92 -63
- package/dist/_tsup-dts-rollup.d.cts +44 -11
- package/dist/_tsup-dts-rollup.d.ts +44 -11
- package/dist/index.cjs +128 -45
- package/dist/index.js +128 -45
- package/package.json +3 -3
- package/src/client.test.ts +121 -33
- package/src/client.ts +158 -55
- package/src/configuration.test.ts +4 -19
- package/src/configuration.ts +17 -1
package/dist/index.cjs
CHANGED
|
@@ -6,6 +6,7 @@ var utils = require('@mastra/core/utils');
|
|
|
6
6
|
var index_js = require('@modelcontextprotocol/sdk/client/index.js');
|
|
7
7
|
var sse_js = require('@modelcontextprotocol/sdk/client/sse.js');
|
|
8
8
|
var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
|
|
9
|
+
var streamableHttp_js = require('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
|
9
10
|
var protocol_js = require('@modelcontextprotocol/sdk/shared/protocol.js');
|
|
10
11
|
var types_js = require('@modelcontextprotocol/sdk/types.js');
|
|
11
12
|
var exitHook = require('exit-hook');
|
|
@@ -4119,11 +4120,12 @@ function convertLogLevelToLoggerMethod(level) {
|
|
|
4119
4120
|
}
|
|
4120
4121
|
var MastraMCPClient = class extends base.MastraBase {
|
|
4121
4122
|
name;
|
|
4122
|
-
transport;
|
|
4123
4123
|
client;
|
|
4124
4124
|
timeout;
|
|
4125
4125
|
logHandler;
|
|
4126
4126
|
enableServerLogs;
|
|
4127
|
+
serverConfig;
|
|
4128
|
+
transport;
|
|
4127
4129
|
constructor({
|
|
4128
4130
|
name,
|
|
4129
4131
|
version = "1.0.0",
|
|
@@ -4136,19 +4138,7 @@ var MastraMCPClient = class extends base.MastraBase {
|
|
|
4136
4138
|
this.timeout = timeout;
|
|
4137
4139
|
this.logHandler = server.logger;
|
|
4138
4140
|
this.enableServerLogs = server.enableServerLogs ?? true;
|
|
4139
|
-
|
|
4140
|
-
if (`url` in serverConfig) {
|
|
4141
|
-
this.transport = new sse_js.SSEClientTransport(serverConfig.url, {
|
|
4142
|
-
requestInit: serverConfig.requestInit,
|
|
4143
|
-
eventSourceInit: serverConfig.eventSourceInit
|
|
4144
|
-
});
|
|
4145
|
-
} else {
|
|
4146
|
-
this.transport = new stdio_js.StdioClientTransport({
|
|
4147
|
-
...serverConfig,
|
|
4148
|
-
// without ...getDefaultEnvironment() commands like npx will fail because there will be no PATH env var
|
|
4149
|
-
env: { ...stdio_js.getDefaultEnvironment(), ...serverConfig.env || {} }
|
|
4150
|
-
});
|
|
4151
|
-
}
|
|
4141
|
+
this.serverConfig = server;
|
|
4152
4142
|
this.client = new index_js.Client(
|
|
4153
4143
|
{
|
|
4154
4144
|
name,
|
|
@@ -4168,11 +4158,12 @@ var MastraMCPClient = class extends base.MastraBase {
|
|
|
4168
4158
|
*/
|
|
4169
4159
|
log(level, message, details) {
|
|
4170
4160
|
const loggerMethod = convertLogLevelToLoggerMethod(level);
|
|
4171
|
-
this.
|
|
4161
|
+
const msg = `[${this.name}] ${message}`;
|
|
4162
|
+
this.logger[loggerMethod](msg, details);
|
|
4172
4163
|
if (this.logHandler) {
|
|
4173
4164
|
this.logHandler({
|
|
4174
4165
|
level,
|
|
4175
|
-
message,
|
|
4166
|
+
message: msg,
|
|
4176
4167
|
timestamp: /* @__PURE__ */ new Date(),
|
|
4177
4168
|
serverName: this.name,
|
|
4178
4169
|
details
|
|
@@ -4195,44 +4186,121 @@ var MastraMCPClient = class extends base.MastraBase {
|
|
|
4195
4186
|
);
|
|
4196
4187
|
}
|
|
4197
4188
|
}
|
|
4189
|
+
async connectStdio(command) {
|
|
4190
|
+
this.log("debug", `Using Stdio transport for command: ${command}`);
|
|
4191
|
+
try {
|
|
4192
|
+
this.transport = new stdio_js.StdioClientTransport({
|
|
4193
|
+
command,
|
|
4194
|
+
args: this.serverConfig.args,
|
|
4195
|
+
env: { ...stdio_js.getDefaultEnvironment(), ...this.serverConfig.env || {} }
|
|
4196
|
+
});
|
|
4197
|
+
await this.client.connect(this.transport, { timeout: this.serverConfig.timeout ?? this.timeout });
|
|
4198
|
+
this.log("debug", `Successfully connected to MCP server via Stdio`);
|
|
4199
|
+
} catch (e) {
|
|
4200
|
+
this.log("error", e instanceof Error ? e.stack || e.message : JSON.stringify(e));
|
|
4201
|
+
throw e;
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
async connectHttp(url) {
|
|
4205
|
+
const { requestInit, eventSourceInit } = this.serverConfig;
|
|
4206
|
+
this.log("debug", `Attempting to connect to URL: ${url}`);
|
|
4207
|
+
let shouldTrySSE = url.pathname.endsWith(`/sse`);
|
|
4208
|
+
if (!shouldTrySSE) {
|
|
4209
|
+
try {
|
|
4210
|
+
this.log("debug", "Trying Streamable HTTP transport...");
|
|
4211
|
+
const streamableTransport = new streamableHttp_js.StreamableHTTPClientTransport(url, {
|
|
4212
|
+
requestInit,
|
|
4213
|
+
reconnectionOptions: this.serverConfig.reconnectionOptions,
|
|
4214
|
+
sessionId: this.serverConfig.sessionId
|
|
4215
|
+
});
|
|
4216
|
+
await this.client.connect(streamableTransport, {
|
|
4217
|
+
timeout: (
|
|
4218
|
+
// this is hardcoded to 3s because the long default timeout would be extremely slow for sse backwards compat (60s)
|
|
4219
|
+
3e3
|
|
4220
|
+
)
|
|
4221
|
+
});
|
|
4222
|
+
this.transport = streamableTransport;
|
|
4223
|
+
this.log("debug", "Successfully connected using Streamable HTTP transport.");
|
|
4224
|
+
} catch (error) {
|
|
4225
|
+
this.log("debug", `Streamable HTTP transport failed: ${error}`);
|
|
4226
|
+
shouldTrySSE = true;
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
if (shouldTrySSE) {
|
|
4230
|
+
this.log("debug", "Falling back to deprecated HTTP+SSE transport...");
|
|
4231
|
+
try {
|
|
4232
|
+
const sseTransport = new sse_js.SSEClientTransport(url, { requestInit, eventSourceInit });
|
|
4233
|
+
await this.client.connect(sseTransport, { timeout: this.serverConfig.timeout ?? this.timeout });
|
|
4234
|
+
this.transport = sseTransport;
|
|
4235
|
+
this.log("debug", "Successfully connected using deprecated HTTP+SSE transport.");
|
|
4236
|
+
} catch (sseError) {
|
|
4237
|
+
this.log(
|
|
4238
|
+
"error",
|
|
4239
|
+
`Failed to connect with SSE transport after failing to connect to Streamable HTTP transport first. SSE error: ${sseError}`
|
|
4240
|
+
);
|
|
4241
|
+
throw new Error("Could not connect to server with any available HTTP transport");
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4198
4245
|
isConnected = false;
|
|
4199
4246
|
async connect() {
|
|
4200
4247
|
if (this.isConnected) return;
|
|
4248
|
+
const { command, url } = this.serverConfig;
|
|
4249
|
+
if (command) {
|
|
4250
|
+
await this.connectStdio(command);
|
|
4251
|
+
} else if (url) {
|
|
4252
|
+
await this.connectHttp(url);
|
|
4253
|
+
} else {
|
|
4254
|
+
throw new Error("Server configuration must include either a command or a url.");
|
|
4255
|
+
}
|
|
4256
|
+
this.isConnected = true;
|
|
4257
|
+
const originalOnClose = this.client.onclose;
|
|
4258
|
+
this.client.onclose = () => {
|
|
4259
|
+
this.log("debug", `MCP server connection closed`);
|
|
4260
|
+
this.isConnected = false;
|
|
4261
|
+
if (typeof originalOnClose === `function`) {
|
|
4262
|
+
originalOnClose();
|
|
4263
|
+
}
|
|
4264
|
+
};
|
|
4265
|
+
exitHook.asyncExitHook(
|
|
4266
|
+
async () => {
|
|
4267
|
+
this.log("debug", `Disconnecting MCP server during exit`);
|
|
4268
|
+
await this.disconnect();
|
|
4269
|
+
},
|
|
4270
|
+
{ wait: 5e3 }
|
|
4271
|
+
);
|
|
4272
|
+
process.on("SIGTERM", () => exitHook.gracefulExit());
|
|
4273
|
+
this.log("debug", `Successfully connected to MCP server`);
|
|
4274
|
+
}
|
|
4275
|
+
/**
|
|
4276
|
+
* Get the current session ID if using the Streamable HTTP transport.
|
|
4277
|
+
* Returns undefined if not connected or not using Streamable HTTP.
|
|
4278
|
+
*/
|
|
4279
|
+
get sessionId() {
|
|
4280
|
+
if (this.transport instanceof streamableHttp_js.StreamableHTTPClientTransport) {
|
|
4281
|
+
return this.transport.sessionId;
|
|
4282
|
+
}
|
|
4283
|
+
return void 0;
|
|
4284
|
+
}
|
|
4285
|
+
async disconnect() {
|
|
4286
|
+
if (!this.transport) {
|
|
4287
|
+
this.log("debug", "Disconnect called but no transport was connected.");
|
|
4288
|
+
return;
|
|
4289
|
+
}
|
|
4290
|
+
this.log("debug", `Disconnecting from MCP server`);
|
|
4201
4291
|
try {
|
|
4202
|
-
this.
|
|
4203
|
-
|
|
4204
|
-
timeout: this.timeout
|
|
4205
|
-
});
|
|
4206
|
-
this.isConnected = true;
|
|
4207
|
-
const originalOnClose = this.client.onclose;
|
|
4208
|
-
this.client.onclose = () => {
|
|
4209
|
-
this.log("debug", `MCP server connection closed`);
|
|
4210
|
-
this.isConnected = false;
|
|
4211
|
-
if (typeof originalOnClose === `function`) {
|
|
4212
|
-
originalOnClose();
|
|
4213
|
-
}
|
|
4214
|
-
};
|
|
4215
|
-
exitHook.asyncExitHook(
|
|
4216
|
-
async () => {
|
|
4217
|
-
this.log("debug", `Disconnecting MCP server during exit`);
|
|
4218
|
-
await this.disconnect();
|
|
4219
|
-
},
|
|
4220
|
-
{ wait: 5e3 }
|
|
4221
|
-
);
|
|
4222
|
-
process.on("SIGTERM", () => exitHook.gracefulExit());
|
|
4223
|
-
this.log("info", `Successfully connected to MCP server`);
|
|
4292
|
+
await this.transport.close();
|
|
4293
|
+
this.log("debug", "Successfully disconnected from MCP server");
|
|
4224
4294
|
} catch (e) {
|
|
4225
|
-
this.log("error",
|
|
4295
|
+
this.log("error", "Error during MCP server disconnect", {
|
|
4226
4296
|
error: e instanceof Error ? e.stack : JSON.stringify(e, null, 2)
|
|
4227
4297
|
});
|
|
4228
|
-
this.isConnected = false;
|
|
4229
4298
|
throw e;
|
|
4299
|
+
} finally {
|
|
4300
|
+
this.transport = void 0;
|
|
4301
|
+
this.isConnected = false;
|
|
4230
4302
|
}
|
|
4231
4303
|
}
|
|
4232
|
-
async disconnect() {
|
|
4233
|
-
this.log("debug", `Disconnecting from MCP server`);
|
|
4234
|
-
return await this.client.close();
|
|
4235
|
-
}
|
|
4236
4304
|
// 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"
|
|
4237
4305
|
async resources() {
|
|
4238
4306
|
this.log("debug", `Requesting resources from MCP server`);
|
|
@@ -4345,6 +4413,19 @@ To fix this you have three different options:
|
|
|
4345
4413
|
});
|
|
4346
4414
|
return connectedToolsets;
|
|
4347
4415
|
}
|
|
4416
|
+
/**
|
|
4417
|
+
* Get the current session IDs for all connected MCP clients using the Streamable HTTP transport.
|
|
4418
|
+
* Returns an object mapping server names to their session IDs.
|
|
4419
|
+
*/
|
|
4420
|
+
get sessionIds() {
|
|
4421
|
+
const sessionIds = {};
|
|
4422
|
+
for (const [serverName, client] of this.mcpClientsById.entries()) {
|
|
4423
|
+
if (client.sessionId) {
|
|
4424
|
+
sessionIds[serverName] = client.sessionId;
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
return sessionIds;
|
|
4428
|
+
}
|
|
4348
4429
|
mcpClientsById = /* @__PURE__ */ new Map();
|
|
4349
4430
|
async getConnectedClient(name, config) {
|
|
4350
4431
|
const exists = this.mcpClientsById.has(name);
|
|
@@ -4367,7 +4448,9 @@ To fix this you have three different options:
|
|
|
4367
4448
|
this.logger.error(`MCPConfiguration errored connecting to MCP server ${name}`, {
|
|
4368
4449
|
error: e instanceof Error ? e.message : String(e)
|
|
4369
4450
|
});
|
|
4370
|
-
throw new Error(
|
|
4451
|
+
throw new Error(
|
|
4452
|
+
`Failed to connect to MCP server ${name}: ${e instanceof Error ? e.stack || e.message : String(e)}`
|
|
4453
|
+
);
|
|
4371
4454
|
}
|
|
4372
4455
|
this.logger.debug(`Connected to ${name} MCP server`);
|
|
4373
4456
|
return mcpClient;
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ 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';
|
|
@@ -4095,11 +4096,12 @@ function convertLogLevelToLoggerMethod(level) {
|
|
|
4095
4096
|
}
|
|
4096
4097
|
var MastraMCPClient = class extends MastraBase {
|
|
4097
4098
|
name;
|
|
4098
|
-
transport;
|
|
4099
4099
|
client;
|
|
4100
4100
|
timeout;
|
|
4101
4101
|
logHandler;
|
|
4102
4102
|
enableServerLogs;
|
|
4103
|
+
serverConfig;
|
|
4104
|
+
transport;
|
|
4103
4105
|
constructor({
|
|
4104
4106
|
name,
|
|
4105
4107
|
version = "1.0.0",
|
|
@@ -4112,19 +4114,7 @@ var MastraMCPClient = class extends MastraBase {
|
|
|
4112
4114
|
this.timeout = timeout;
|
|
4113
4115
|
this.logHandler = server.logger;
|
|
4114
4116
|
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
|
-
}
|
|
4117
|
+
this.serverConfig = server;
|
|
4128
4118
|
this.client = new Client(
|
|
4129
4119
|
{
|
|
4130
4120
|
name,
|
|
@@ -4144,11 +4134,12 @@ var MastraMCPClient = class extends MastraBase {
|
|
|
4144
4134
|
*/
|
|
4145
4135
|
log(level, message, details) {
|
|
4146
4136
|
const loggerMethod = convertLogLevelToLoggerMethod(level);
|
|
4147
|
-
this.
|
|
4137
|
+
const msg = `[${this.name}] ${message}`;
|
|
4138
|
+
this.logger[loggerMethod](msg, details);
|
|
4148
4139
|
if (this.logHandler) {
|
|
4149
4140
|
this.logHandler({
|
|
4150
4141
|
level,
|
|
4151
|
-
message,
|
|
4142
|
+
message: msg,
|
|
4152
4143
|
timestamp: /* @__PURE__ */ new Date(),
|
|
4153
4144
|
serverName: this.name,
|
|
4154
4145
|
details
|
|
@@ -4171,44 +4162,121 @@ var MastraMCPClient = class extends MastraBase {
|
|
|
4171
4162
|
);
|
|
4172
4163
|
}
|
|
4173
4164
|
}
|
|
4165
|
+
async connectStdio(command) {
|
|
4166
|
+
this.log("debug", `Using Stdio transport for command: ${command}`);
|
|
4167
|
+
try {
|
|
4168
|
+
this.transport = new StdioClientTransport({
|
|
4169
|
+
command,
|
|
4170
|
+
args: this.serverConfig.args,
|
|
4171
|
+
env: { ...getDefaultEnvironment(), ...this.serverConfig.env || {} }
|
|
4172
|
+
});
|
|
4173
|
+
await this.client.connect(this.transport, { timeout: this.serverConfig.timeout ?? this.timeout });
|
|
4174
|
+
this.log("debug", `Successfully connected to MCP server via Stdio`);
|
|
4175
|
+
} catch (e) {
|
|
4176
|
+
this.log("error", e instanceof Error ? e.stack || e.message : JSON.stringify(e));
|
|
4177
|
+
throw e;
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
async connectHttp(url) {
|
|
4181
|
+
const { requestInit, eventSourceInit } = this.serverConfig;
|
|
4182
|
+
this.log("debug", `Attempting to connect to URL: ${url}`);
|
|
4183
|
+
let shouldTrySSE = url.pathname.endsWith(`/sse`);
|
|
4184
|
+
if (!shouldTrySSE) {
|
|
4185
|
+
try {
|
|
4186
|
+
this.log("debug", "Trying Streamable HTTP transport...");
|
|
4187
|
+
const streamableTransport = new StreamableHTTPClientTransport(url, {
|
|
4188
|
+
requestInit,
|
|
4189
|
+
reconnectionOptions: this.serverConfig.reconnectionOptions,
|
|
4190
|
+
sessionId: this.serverConfig.sessionId
|
|
4191
|
+
});
|
|
4192
|
+
await this.client.connect(streamableTransport, {
|
|
4193
|
+
timeout: (
|
|
4194
|
+
// this is hardcoded to 3s because the long default timeout would be extremely slow for sse backwards compat (60s)
|
|
4195
|
+
3e3
|
|
4196
|
+
)
|
|
4197
|
+
});
|
|
4198
|
+
this.transport = streamableTransport;
|
|
4199
|
+
this.log("debug", "Successfully connected using Streamable HTTP transport.");
|
|
4200
|
+
} catch (error) {
|
|
4201
|
+
this.log("debug", `Streamable HTTP transport failed: ${error}`);
|
|
4202
|
+
shouldTrySSE = true;
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
if (shouldTrySSE) {
|
|
4206
|
+
this.log("debug", "Falling back to deprecated HTTP+SSE transport...");
|
|
4207
|
+
try {
|
|
4208
|
+
const sseTransport = new SSEClientTransport(url, { requestInit, eventSourceInit });
|
|
4209
|
+
await this.client.connect(sseTransport, { timeout: this.serverConfig.timeout ?? this.timeout });
|
|
4210
|
+
this.transport = sseTransport;
|
|
4211
|
+
this.log("debug", "Successfully connected using deprecated HTTP+SSE transport.");
|
|
4212
|
+
} catch (sseError) {
|
|
4213
|
+
this.log(
|
|
4214
|
+
"error",
|
|
4215
|
+
`Failed to connect with SSE transport after failing to connect to Streamable HTTP transport first. SSE error: ${sseError}`
|
|
4216
|
+
);
|
|
4217
|
+
throw new Error("Could not connect to server with any available HTTP transport");
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4174
4221
|
isConnected = false;
|
|
4175
4222
|
async connect() {
|
|
4176
4223
|
if (this.isConnected) return;
|
|
4224
|
+
const { command, url } = this.serverConfig;
|
|
4225
|
+
if (command) {
|
|
4226
|
+
await this.connectStdio(command);
|
|
4227
|
+
} else if (url) {
|
|
4228
|
+
await this.connectHttp(url);
|
|
4229
|
+
} else {
|
|
4230
|
+
throw new Error("Server configuration must include either a command or a url.");
|
|
4231
|
+
}
|
|
4232
|
+
this.isConnected = true;
|
|
4233
|
+
const originalOnClose = this.client.onclose;
|
|
4234
|
+
this.client.onclose = () => {
|
|
4235
|
+
this.log("debug", `MCP server connection closed`);
|
|
4236
|
+
this.isConnected = false;
|
|
4237
|
+
if (typeof originalOnClose === `function`) {
|
|
4238
|
+
originalOnClose();
|
|
4239
|
+
}
|
|
4240
|
+
};
|
|
4241
|
+
asyncExitHook(
|
|
4242
|
+
async () => {
|
|
4243
|
+
this.log("debug", `Disconnecting MCP server during exit`);
|
|
4244
|
+
await this.disconnect();
|
|
4245
|
+
},
|
|
4246
|
+
{ wait: 5e3 }
|
|
4247
|
+
);
|
|
4248
|
+
process.on("SIGTERM", () => gracefulExit());
|
|
4249
|
+
this.log("debug", `Successfully connected to MCP server`);
|
|
4250
|
+
}
|
|
4251
|
+
/**
|
|
4252
|
+
* Get the current session ID if using the Streamable HTTP transport.
|
|
4253
|
+
* Returns undefined if not connected or not using Streamable HTTP.
|
|
4254
|
+
*/
|
|
4255
|
+
get sessionId() {
|
|
4256
|
+
if (this.transport instanceof StreamableHTTPClientTransport) {
|
|
4257
|
+
return this.transport.sessionId;
|
|
4258
|
+
}
|
|
4259
|
+
return void 0;
|
|
4260
|
+
}
|
|
4261
|
+
async disconnect() {
|
|
4262
|
+
if (!this.transport) {
|
|
4263
|
+
this.log("debug", "Disconnect called but no transport was connected.");
|
|
4264
|
+
return;
|
|
4265
|
+
}
|
|
4266
|
+
this.log("debug", `Disconnecting from MCP server`);
|
|
4177
4267
|
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`);
|
|
4268
|
+
await this.transport.close();
|
|
4269
|
+
this.log("debug", "Successfully disconnected from MCP server");
|
|
4200
4270
|
} catch (e) {
|
|
4201
|
-
this.log("error",
|
|
4271
|
+
this.log("error", "Error during MCP server disconnect", {
|
|
4202
4272
|
error: e instanceof Error ? e.stack : JSON.stringify(e, null, 2)
|
|
4203
4273
|
});
|
|
4204
|
-
this.isConnected = false;
|
|
4205
4274
|
throw e;
|
|
4275
|
+
} finally {
|
|
4276
|
+
this.transport = void 0;
|
|
4277
|
+
this.isConnected = false;
|
|
4206
4278
|
}
|
|
4207
4279
|
}
|
|
4208
|
-
async disconnect() {
|
|
4209
|
-
this.log("debug", `Disconnecting from MCP server`);
|
|
4210
|
-
return await this.client.close();
|
|
4211
|
-
}
|
|
4212
4280
|
// 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
4281
|
async resources() {
|
|
4214
4282
|
this.log("debug", `Requesting resources from MCP server`);
|
|
@@ -4321,6 +4389,19 @@ To fix this you have three different options:
|
|
|
4321
4389
|
});
|
|
4322
4390
|
return connectedToolsets;
|
|
4323
4391
|
}
|
|
4392
|
+
/**
|
|
4393
|
+
* Get the current session IDs for all connected MCP clients using the Streamable HTTP transport.
|
|
4394
|
+
* Returns an object mapping server names to their session IDs.
|
|
4395
|
+
*/
|
|
4396
|
+
get sessionIds() {
|
|
4397
|
+
const sessionIds = {};
|
|
4398
|
+
for (const [serverName, client] of this.mcpClientsById.entries()) {
|
|
4399
|
+
if (client.sessionId) {
|
|
4400
|
+
sessionIds[serverName] = client.sessionId;
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
return sessionIds;
|
|
4404
|
+
}
|
|
4324
4405
|
mcpClientsById = /* @__PURE__ */ new Map();
|
|
4325
4406
|
async getConnectedClient(name, config) {
|
|
4326
4407
|
const exists = this.mcpClientsById.has(name);
|
|
@@ -4343,7 +4424,9 @@ To fix this you have three different options:
|
|
|
4343
4424
|
this.logger.error(`MCPConfiguration errored connecting to MCP server ${name}`, {
|
|
4344
4425
|
error: e instanceof Error ? e.message : String(e)
|
|
4345
4426
|
});
|
|
4346
|
-
throw new Error(
|
|
4427
|
+
throw new Error(
|
|
4428
|
+
`Failed to connect to MCP server ${name}: ${e instanceof Error ? e.stack || e.message : String(e)}`
|
|
4429
|
+
);
|
|
4347
4430
|
}
|
|
4348
4431
|
this.logger.debug(`Connected to ${name} MCP server`);
|
|
4349
4432
|
return mcpClient;
|
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.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,11 +22,11 @@
|
|
|
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
28
|
"uuid": "^11.1.0",
|
|
29
|
-
"@mastra/core": "^0.9.1-alpha.
|
|
29
|
+
"@mastra/core": "^0.9.1-alpha.3"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@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
|
});
|