@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/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +33 -0
- package/README.md +45 -0
- package/dist/_tsup-dts-rollup.d.cts +51 -17
- package/dist/_tsup-dts-rollup.d.ts +51 -17
- package/dist/index.cjs +537 -6404
- package/dist/index.js +536 -6383
- package/eslint.config.js +6 -1
- package/integration-tests/node_modules/.bin/tsc +21 -0
- package/integration-tests/node_modules/.bin/tsserver +21 -0
- package/integration-tests/node_modules/.bin/vitest +21 -0
- package/integration-tests/package.json +25 -0
- package/integration-tests/src/mastra/agents/weather.ts +20 -0
- package/integration-tests/src/mastra/index.ts +12 -0
- package/integration-tests/src/mastra/mcp/index.ts +46 -0
- package/integration-tests/src/mastra/tools/weather.ts +13 -0
- package/integration-tests/src/server.test.ts +149 -0
- package/integration-tests/tsconfig.json +13 -0
- package/integration-tests/vitest.config.ts +14 -0
- package/package.json +8 -4
- package/src/__fixtures__/weather.ts +93 -1
- package/src/configuration.test.ts +64 -0
- package/src/configuration.ts +48 -1
- package/src/server.test.ts +3 -4
- package/src/server.ts +180 -88
package/eslint.config.js
CHANGED
|
@@ -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,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.
|
|
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.
|
|
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": "
|
|
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 {
|
|
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: {
|
package/src/configuration.ts
CHANGED
|
@@ -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: (
|
|
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
|
/**
|