@mastra/mcp 0.4.3 → 0.5.0-alpha.1

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/eslint.config.js CHANGED
@@ -3,4 +3,9 @@ import { createConfig } from '@internal/lint/eslint';
3
3
  const config = await createConfig();
4
4
 
5
5
  /** @type {import("eslint").Linter.Config[]} */
6
- export default [...config];
6
+ export default [
7
+ ...config,
8
+ {
9
+ ignores: ['**/integration-tests/**'],
10
+ },
11
+ ];
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/home/runner/work/mastra/mastra/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/typescript@5.8.3/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/home/runner/work/mastra/mastra/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/typescript@5.8.3/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
19
+ else
20
+ exec node "$basedir/../typescript/bin/tsc" "$@"
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/home/runner/work/mastra/mastra/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/typescript@5.8.3/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/home/runner/work/mastra/mastra/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/typescript@5.8.3/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
19
+ else
20
+ exec node "$basedir/../typescript/bin/tsserver" "$@"
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/home/runner/work/mastra/mastra/node_modules/.pnpm/vitest@1.6.1_@edge-runtime+vm@3.2.0_@types+node@20.17.32_jsdom@26.0.0_bufferutil@4.0.9__b7b40f6481a5fb1015de024b8d25deb4/node_modules/vitest/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/vitest@1.6.1_@edge-runtime+vm@3.2.0_@types+node@20.17.32_jsdom@26.0.0_bufferutil@4.0.9__b7b40f6481a5fb1015de024b8d25deb4/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/home/runner/work/mastra/mastra/node_modules/.pnpm/vitest@1.6.1_@edge-runtime+vm@3.2.0_@types+node@20.17.32_jsdom@26.0.0_bufferutil@4.0.9__b7b40f6481a5fb1015de024b8d25deb4/node_modules/vitest/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/vitest@1.6.1_@edge-runtime+vm@3.2.0_@types+node@20.17.32_jsdom@26.0.0_bufferutil@4.0.9__b7b40f6481a5fb1015de024b8d25deb4/node_modules:/home/runner/work/mastra/mastra/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../vitest/vitest.mjs" "$@"
19
+ else
20
+ exec node "$basedir/../vitest/vitest.mjs" "$@"
21
+ fi
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "mcp-server-integration-tests",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "scripts": {
6
+ "test:mcp": "vitest run ./src/server.test.ts",
7
+ "dev": "mastra dev"
8
+ },
9
+ "dependencies": {
10
+ "@ai-sdk/openai": "^1.3.21",
11
+ "@ai-sdk/react": "^1.2.11",
12
+ "@mastra/client-js": "workspace:*",
13
+ "@mastra/core": "workspace:*",
14
+ "@mastra/mcp": "workspace:*",
15
+ "dotenv": "^16.5.0",
16
+ "zod": "^3.24.3"
17
+ },
18
+ "devDependencies": {
19
+ "@testing-library/react": "^16.2.0",
20
+ "@types/node": "^20.17.27",
21
+ "mastra": "workspace:*",
22
+ "typescript": "^5.8.2",
23
+ "vitest": "^1.6.1"
24
+ }
25
+ }
@@ -0,0 +1,20 @@
1
+ import { openai } from '@ai-sdk/openai';
2
+ import { createTool } from '@mastra/core';
3
+ import { Agent } from '@mastra/core/agent';
4
+ import { z } from 'zod';
5
+ import { weatherTool } from '../tools/weather';
6
+
7
+ export const weatherAgent = new Agent({
8
+ name: 'test',
9
+ instructions:
10
+ 'You are a weather agent. When asked about weather in any city, use the get_weather tool with the city name as the postal code. When asked for clipboard contents you also get that.',
11
+ model: openai('gpt-4o'),
12
+ tools: {
13
+ get_weather: weatherTool,
14
+ clipboard: createTool({
15
+ id: 'clipboard',
16
+ description: 'Returns the contents of the users clipboard',
17
+ inputSchema: z.object({}),
18
+ }),
19
+ },
20
+ });
@@ -0,0 +1,12 @@
1
+ import { Mastra } from '@mastra/core';
2
+ import { weatherAgent } from './agents/weather';
3
+ import { myMcpServer } from './mcp';
4
+
5
+ export const mastra = new Mastra({
6
+ agents: {
7
+ test: weatherAgent,
8
+ },
9
+ mcpServers: {
10
+ myMcpServer,
11
+ },
12
+ });
@@ -0,0 +1,46 @@
1
+ import { createTool } from '@mastra/core/tools';
2
+ import { MCPServer } from '@mastra/mcp';
3
+ import { z } from 'zod';
4
+
5
+ export const myMcpServer = new MCPServer({
6
+ name: 'My Calculation & Data MCP Server',
7
+ version: '1.0.0',
8
+ tools: {
9
+ calculator: createTool({
10
+ id: 'calculator',
11
+ description: 'Performs basic arithmetic operations (add, subtract).',
12
+ inputSchema: z.object({
13
+ num1: z.number().describe('The first number.'),
14
+ num2: z.number().describe('The second number.'),
15
+ operation: z.enum(['add', 'subtract']).describe('The operation to perform.'),
16
+ }),
17
+ execute: async ({ context }) => {
18
+ const { num1, num2, operation } = context;
19
+ if (operation === 'add') {
20
+ return num1 + num2;
21
+ }
22
+ if (operation === 'subtract') {
23
+ return num1 - num2;
24
+ }
25
+ throw new Error('Invalid operation');
26
+ },
27
+ }),
28
+ fetchWeather: createTool({
29
+ id: 'fetchWeather',
30
+ description: 'Fetches a (simulated) weather forecast for a given city.',
31
+ inputSchema: z.object({
32
+ city: z.string().describe('The city to get weather for, e.g., London, Paris.'),
33
+ }),
34
+ execute: async ({ context }) => {
35
+ const { city } = context;
36
+ const temperatures = {
37
+ london: '15°C',
38
+ paris: '18°C',
39
+ tokyo: '22°C',
40
+ };
41
+ const temp = temperatures[city.toLowerCase() as keyof typeof temperatures] || '20°C';
42
+ return `The weather in ${city} is ${temp} and sunny.`;
43
+ },
44
+ }),
45
+ },
46
+ });
@@ -0,0 +1,13 @@
1
+ import { createTool } from '@mastra/core/tools';
2
+ import { z } from 'zod';
3
+
4
+ export const weatherTool = createTool({
5
+ id: 'get_weather',
6
+ description: 'Get the weather for a given location',
7
+ inputSchema: z.object({
8
+ postalCode: z.string().describe('The location to get the weather for'),
9
+ }),
10
+ execute: async ({ context: { postalCode } }) => {
11
+ return `The weather in ${postalCode} is sunny. It is currently 70 degrees and feels like 65 degrees.`;
12
+ },
13
+ });
@@ -0,0 +1,149 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createServer } from 'node:http';
3
+ import path from 'path';
4
+ import { MCPClient } from '@mastra/mcp';
5
+ import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
6
+
7
+ vi.setConfig({ testTimeout: 20000, hookTimeout: 20000 });
8
+
9
+ // Helper to find an available port
10
+ async function getAvailablePort(): Promise<number> {
11
+ return new Promise((resolve, reject) => {
12
+ const server = createServer();
13
+ server.listen(0, () => {
14
+ const { port } = server.address() as { port: number };
15
+ server.close(() => resolve(port));
16
+ });
17
+ server.on('error', reject);
18
+ });
19
+ }
20
+
21
+ describe('MCPServer through Mastra HTTP Integration (Subprocess)', () => {
22
+ let mastraServer: ReturnType<typeof spawn>;
23
+ let port: number;
24
+ const mcpServerId = 'myMcpServer';
25
+ const testToolId = 'calculator';
26
+ let client: MCPClient;
27
+
28
+ beforeAll(async () => {
29
+ port = await getAvailablePort();
30
+
31
+ mastraServer = spawn(
32
+ 'pnpm',
33
+ [
34
+ path.resolve(import.meta.dirname, `..`, `..`, `..`, `cli`, `dist`, `index.js`),
35
+ 'dev',
36
+ '--port',
37
+ port.toString(),
38
+ ],
39
+ {
40
+ stdio: 'pipe',
41
+ detached: true, // Run in a new process group so we can kill it and children
42
+ },
43
+ );
44
+
45
+ // Wait for server to be ready
46
+ await new Promise<void>((resolve, reject) => {
47
+ let output = '';
48
+ mastraServer.stdout?.on('data', data => {
49
+ output += data.toString();
50
+ console.log(output);
51
+ if (output.includes('http://localhost:')) {
52
+ resolve();
53
+ }
54
+ });
55
+ mastraServer.stderr?.on('data', data => {
56
+ console.error('Mastra server error:', data.toString());
57
+ });
58
+
59
+ setTimeout(() => reject(new Error('Mastra server failed to start')), 10000);
60
+ });
61
+
62
+ client = new MCPClient({
63
+ servers: {
64
+ [mcpServerId]: {
65
+ url: new URL(`http://localhost:${port}/api/servers/${mcpServerId}/mcp`),
66
+ },
67
+ },
68
+ });
69
+ });
70
+
71
+ afterAll(() => {
72
+ // Kill the server and its process group
73
+ if (mastraServer?.pid) {
74
+ try {
75
+ process.kill(-mastraServer.pid, 'SIGTERM');
76
+ } catch (e) {
77
+ console.error('Failed to kill Mastra server:', e);
78
+ }
79
+ }
80
+ });
81
+
82
+ it('should allow an HTTP client to call a tool via Mastra MCP endpoint (Subprocess)', async () => {
83
+ const toolCallPayload = {
84
+ jsonrpc: '2.0',
85
+ id: `test-${Date.now()}`,
86
+ method: 'CallTool',
87
+ params: {
88
+ name: testToolId,
89
+ args: { num1: 10, num2: 5, operation: 'add' },
90
+ },
91
+ };
92
+
93
+ const tools = await client.getTools();
94
+ console.log('Tools:', tools);
95
+
96
+ const tool = tools['myMcpServer_calculator'];
97
+ console.log('Tool:', tool);
98
+
99
+ const result = await tool.execute({ context: toolCallPayload.params.args });
100
+ console.log('Result:', result);
101
+
102
+ expect(result).toBeDefined();
103
+ expect(result.isError).toBe(false);
104
+ expect(result.content).toBeInstanceOf(Array);
105
+ expect(result.content.length).toBeGreaterThan(0);
106
+
107
+ const toolOutput = result.content[0];
108
+ expect(toolOutput.type).toBe('text');
109
+
110
+ const expectedToolResult = 15;
111
+ expect(JSON.parse(toolOutput.text)).toEqual(expectedToolResult);
112
+ }, 25000);
113
+
114
+ it('should allow a client to call a tool via Mastra MCP SSE endpoints (Subprocess)', async () => {
115
+ const sseUrl = new URL(`http://localhost:${port}/api/servers/${mcpServerId}/sse`);
116
+
117
+ // Configure MCPClient for SSE transport
118
+ const sseClient = new MCPClient({
119
+ servers: {
120
+ [mcpServerId]: {
121
+ url: sseUrl, // URL for establishing SSE connection
122
+ },
123
+ },
124
+ });
125
+
126
+ const toolCallPayloadParams = { num1: 10, num2: 5, operation: 'add' };
127
+
128
+ // Get tools (this will connect the client internally if not already connected)
129
+ const tools = await sseClient.getTools();
130
+
131
+ const toolName = `${mcpServerId}_${testToolId}`;
132
+ const tool = tools[toolName];
133
+ expect(tool, `Tool '${toolName}' should be available via SSE client`).toBeDefined();
134
+
135
+ // Execute the tool
136
+ const result = await tool.execute({ context: toolCallPayloadParams });
137
+
138
+ expect(result).toBeDefined();
139
+ expect(result.isError).toBe(false);
140
+ expect(result.content).toBeInstanceOf(Array);
141
+ expect(result.content.length).toBeGreaterThan(0);
142
+
143
+ const toolOutput = result.content[0];
144
+ expect(toolOutput.type).toBe('text');
145
+
146
+ const expectedToolResult = 15; // 10 + 5
147
+ expect(JSON.parse(toolOutput.text)).toEqual(expectedToolResult);
148
+ }, 25000);
149
+ });
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true
10
+ },
11
+ "include": ["src/**/*"],
12
+ "exclude": ["node_modules"]
13
+ }
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ testTimeout: 60000,
8
+ hookTimeout: 30000,
9
+ coverage: {
10
+ provider: 'v8',
11
+ reporter: ['text', 'json', 'html'],
12
+ },
13
+ },
14
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/mcp",
3
- "version": "0.4.3",
3
+ "version": "0.5.0-alpha.1",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,15 +26,18 @@
26
26
  "date-fns": "^4.1.0",
27
27
  "exit-hook": "^4.0.0",
28
28
  "fast-deep-equal": "^3.1.3",
29
+ "json-schema-to-zod": "^2.6.0",
29
30
  "uuid": "^11.1.0",
30
- "@mastra/core": "^0.9.3"
31
+ "@mastra/core": "^0.9.4-alpha.1"
32
+ },
33
+ "peerDependencies": {
34
+ "zod": "^3.0.0"
31
35
  },
32
36
  "devDependencies": {
33
37
  "@ai-sdk/anthropic": "^1.1.15",
34
38
  "@microsoft/api-extractor": "^7.52.5",
35
39
  "@types/node": "^20.17.27",
36
40
  "eslint": "^9.23.0",
37
- "json-schema-to-zod": "^2.6.0",
38
41
  "tsup": "^8.4.0",
39
42
  "tsx": "^4.19.3",
40
43
  "typescript": "^5.8.2",
@@ -46,7 +49,8 @@
46
49
  "scripts": {
47
50
  "build": "tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting",
48
51
  "build:watch": "pnpm build --watch",
49
- "test": "vitest run",
52
+ "test:integration": "cd integration-tests && pnpm test:mcp",
53
+ "test": "pnpm test:integration && vitest run",
50
54
  "lint": "eslint ."
51
55
  }
52
56
  }
@@ -2,7 +2,12 @@ import type { IncomingMessage, ServerResponse } from 'http';
2
2
  import { createServer } from 'http';
3
3
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
4
  import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
5
- import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ ListResourcesRequestSchema,
9
+ ReadResourceRequestSchema
10
+ } from '@modelcontextprotocol/sdk/types.js';
6
11
  import { z } from 'zod';
7
12
  import { zodToJsonSchema } from 'zod-to-json-schema';
8
13
 
@@ -27,6 +32,7 @@ const server = new Server(
27
32
  {
28
33
  capabilities: {
29
34
  tools: {},
35
+ resources: {}
30
36
  },
31
37
  },
32
38
  );
@@ -86,6 +92,92 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
86
92
  ],
87
93
  }));
88
94
 
95
+ // Resources implementation
96
+ const weatherResources = [
97
+ {
98
+ uri: 'weather://current',
99
+ name: 'Current Weather Data',
100
+ description: 'Real-time weather data for the current location',
101
+ mimeType: 'application/json'
102
+ },
103
+ {
104
+ uri: 'weather://forecast',
105
+ name: 'Weather Forecast',
106
+ description: '5-day weather forecast',
107
+ mimeType: 'application/json'
108
+ },
109
+ {
110
+ uri: 'weather://historical',
111
+ name: 'Historical Weather Data',
112
+ description: 'Weather data from the past 30 days',
113
+ mimeType: 'application/json'
114
+ }
115
+ ];
116
+
117
+ // List available resources
118
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
119
+ resources: weatherResources
120
+ }));
121
+
122
+ // Read resource contents
123
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
124
+ const uri = request.params.uri;
125
+
126
+ if (uri === 'weather://current') {
127
+ return {
128
+ contents: [
129
+ {
130
+ uri,
131
+ mimeType: 'application/json',
132
+ text: JSON.stringify({
133
+ location: 'San Francisco',
134
+ temperature: 18,
135
+ conditions: 'Partly Cloudy',
136
+ humidity: 65,
137
+ windSpeed: 12,
138
+ updated: new Date().toISOString()
139
+ })
140
+ }
141
+ ]
142
+ };
143
+ } else if (uri === 'weather://forecast') {
144
+ return {
145
+ contents: [
146
+ {
147
+ uri,
148
+ mimeType: 'application/json',
149
+ text: JSON.stringify([
150
+ { day: 1, high: 19, low: 12, conditions: 'Sunny' },
151
+ { day: 2, high: 22, low: 14, conditions: 'Clear' },
152
+ { day: 3, high: 20, low: 13, conditions: 'Partly Cloudy' },
153
+ { day: 4, high: 18, low: 11, conditions: 'Rain' },
154
+ { day: 5, high: 17, low: 10, conditions: 'Showers' }
155
+ ])
156
+ }
157
+ ]
158
+ };
159
+ } else if (uri === 'weather://historical') {
160
+ return {
161
+ contents: [
162
+ {
163
+ uri,
164
+ mimeType: 'application/json',
165
+ text: JSON.stringify({
166
+ averageHigh: 20,
167
+ averageLow: 12,
168
+ rainDays: 8,
169
+ sunnyDays: 18,
170
+ recordHigh: 28,
171
+ recordLow: 7
172
+ })
173
+ }
174
+ ]
175
+ };
176
+ }
177
+
178
+ throw new Error(`Resource not found: ${uri}`);
179
+ });
180
+
89
181
  server.setRequestHandler(CallToolRequestSchema, async request => {
90
182
  try {
91
183
  switch (request.params.name) {
@@ -94,6 +94,70 @@ describe('MCPClient', () => {
94
94
  expect(connectedToolsets.weather).toHaveProperty('getWeather');
95
95
  });
96
96
 
97
+ it('should get resources from connected MCP servers', async () => {
98
+ const resources = await mcp.getResources();
99
+
100
+ expect(resources).toHaveProperty('weather');
101
+ expect(resources.weather).toBeDefined();
102
+ expect(resources.weather).toHaveLength(3);
103
+
104
+ // Verify that each expected resource exists with the correct structure
105
+ const weatherResources = resources.weather;
106
+ const currentWeather = weatherResources.find(r => r.uri === 'weather://current');
107
+ expect(currentWeather).toBeDefined();
108
+ expect(currentWeather).toMatchObject({
109
+ uri: 'weather://current',
110
+ name: 'Current Weather Data',
111
+ description: expect.any(String),
112
+ mimeType: 'application/json',
113
+ });
114
+
115
+ const forecast = weatherResources.find(r => r.uri === 'weather://forecast');
116
+ expect(forecast).toBeDefined();
117
+ expect(forecast).toMatchObject({
118
+ uri: 'weather://forecast',
119
+ name: 'Weather Forecast',
120
+ description: expect.any(String),
121
+ mimeType: 'application/json',
122
+ });
123
+
124
+ const historical = weatherResources.find(r => r.uri === 'weather://historical');
125
+ expect(historical).toBeDefined();
126
+ expect(historical).toMatchObject({
127
+ uri: 'weather://historical',
128
+ name: 'Historical Weather Data',
129
+ description: expect.any(String),
130
+ mimeType: 'application/json',
131
+ });
132
+ });
133
+
134
+ it('should handle errors when getting resources', async () => {
135
+ const errorClient = new MCPClient({
136
+ id: 'error-test-client',
137
+ servers: {
138
+ weather: {
139
+ url: new URL('http://localhost:60808/sse'),
140
+ },
141
+ nonexistentServer: {
142
+ command: 'nonexistent-command',
143
+ args: [],
144
+ },
145
+ },
146
+ });
147
+
148
+ try {
149
+ const resources = await errorClient.getResources();
150
+
151
+ expect(resources).toHaveProperty('weather');
152
+ expect(resources.weather).toBeDefined();
153
+ expect(resources.weather.length).toBeGreaterThan(0);
154
+
155
+ expect(resources).not.toHaveProperty('nonexistentServer');
156
+ } finally {
157
+ await errorClient.disconnect();
158
+ }
159
+ });
160
+
97
161
  it('should handle connection errors gracefully', async () => {
98
162
  const badConfig = new MCPClient({
99
163
  servers: {
@@ -1,5 +1,6 @@
1
1
  import { MastraBase } from '@mastra/core/base';
2
2
  import { DEFAULT_REQUEST_TIMEOUT_MSEC } from '@modelcontextprotocol/sdk/shared/protocol.js';
3
+ import type { Resource } from '@modelcontextprotocol/sdk/types.js';
3
4
  import equal from 'fast-deep-equal';
4
5
  import { v5 as uuidv5 } from 'uuid';
5
6
  import { InternalMastraMCPClient } from './client';
@@ -123,6 +124,23 @@ To fix this you have three different options:
123
124
  return connectedToolsets;
124
125
  }
125
126
 
127
+ /**
128
+ * Get all resources from connected MCP servers
129
+ * @returns A record of server names to their resources
130
+ */
131
+ public async getResources() {
132
+ this.addToInstanceCache();
133
+ const connectedResources: Record<string, Resource[]> = {};
134
+
135
+ await this.eachClientResources(async ({ serverName, resources }) => {
136
+ if (resources && Array.isArray(resources)) {
137
+ connectedResources[serverName] = resources;
138
+ }
139
+ });
140
+
141
+ return connectedResources;
142
+ }
143
+
126
144
  /**
127
145
  * Get the current session IDs for all connected MCP clients using the Streamable HTTP transport.
128
146
  * Returns an object mapping server names to their session IDs.
@@ -186,7 +204,7 @@ To fix this you have three different options:
186
204
  }
187
205
 
188
206
  private async eachClientTools(
189
- cb: (input: {
207
+ cb: (args: {
190
208
  serverName: string;
191
209
  tools: Record<string, any>; // <- any because we don't have proper tool schemas
192
210
  client: InstanceType<typeof InternalMastraMCPClient>;
@@ -200,6 +218,35 @@ To fix this you have three different options:
200
218
  }),
201
219
  );
202
220
  }
221
+
222
+ /**
223
+ * Helper method to iterate through each connected MCP client and retrieve resources
224
+ * @param cb Callback function to process resources from each server
225
+ */
226
+ private async eachClientResources(
227
+ cb: (args: {
228
+ serverName: string;
229
+ resources: Resource[];
230
+ client: InstanceType<typeof InternalMastraMCPClient>;
231
+ }) => Promise<void>,
232
+ ) {
233
+ await Promise.all(
234
+ Object.entries(this.serverConfigs).map(async ([serverName, serverConfig]) => {
235
+ try {
236
+ const client = await this.getConnectedClient(serverName, serverConfig);
237
+ const response = await client.resources();
238
+ // Ensure response has the expected structure
239
+ if (response && 'resources' in response && Array.isArray(response.resources)) {
240
+ await cb({ serverName, resources: response.resources, client });
241
+ }
242
+ } catch (e) {
243
+ this.logger.error(`Error getting resources from server ${serverName}`, {
244
+ error: e instanceof Error ? e.message : String(e),
245
+ });
246
+ }
247
+ }),
248
+ );
249
+ }
203
250
  }
204
251
 
205
252
  /**