@mastra/mcp 0.4.0-alpha.8 → 0.4.0
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 +41 -0
- package/dist/_tsup-dts-rollup.d.cts +132 -0
- package/dist/_tsup-dts-rollup.d.ts +132 -0
- package/dist/index.cjs +2154 -5
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2133 -7
- package/package.json +3 -2
- package/src/__fixtures__/server-weather.ts +16 -0
- package/src/__fixtures__/tools.ts +103 -0
- package/src/index.ts +1 -0
- package/src/logger.ts +104 -0
- package/src/server.test.ts +115 -0
- package/src/server.ts +253 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/mcp",
|
|
3
|
-
"version": "0.4.0
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
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.0
|
|
29
|
+
"@mastra/core": "^0.9.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@ai-sdk/anthropic": "^1.1.15",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"vitest": "^3.0.9",
|
|
39
39
|
"zod": "^3.24.2",
|
|
40
40
|
"zod-to-json-schema": "^3.22.4",
|
|
41
|
+
"json-schema-to-zod": "^2.6.0",
|
|
41
42
|
"@internal/lint": "0.0.2"
|
|
42
43
|
},
|
|
43
44
|
"scripts": {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MCPServer } from '../server';
|
|
2
|
+
import { weatherTool } from './tools';
|
|
3
|
+
|
|
4
|
+
const server = new MCPServer({
|
|
5
|
+
name: 'My MCP Server',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
tools: {
|
|
8
|
+
weatherTool,
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
server.startStdio().catch(error => {
|
|
13
|
+
const errorMessage = 'Fatal error running server';
|
|
14
|
+
console.error(errorMessage, error);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createTool } from '@mastra/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
interface GeocodingResponse {
|
|
5
|
+
results: {
|
|
6
|
+
latitude: number;
|
|
7
|
+
longitude: number;
|
|
8
|
+
name: string;
|
|
9
|
+
}[];
|
|
10
|
+
}
|
|
11
|
+
interface WeatherResponse {
|
|
12
|
+
current: {
|
|
13
|
+
time: string;
|
|
14
|
+
temperature_2m: number;
|
|
15
|
+
apparent_temperature: number;
|
|
16
|
+
relative_humidity_2m: number;
|
|
17
|
+
wind_speed_10m: number;
|
|
18
|
+
wind_gusts_10m: number;
|
|
19
|
+
weather_code: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const weatherTool = createTool({
|
|
24
|
+
id: 'get-weather',
|
|
25
|
+
description: 'Get current weather for a location',
|
|
26
|
+
inputSchema: z.object({
|
|
27
|
+
location: z.string().describe('City name'),
|
|
28
|
+
}),
|
|
29
|
+
outputSchema: z.object({
|
|
30
|
+
temperature: z.number(),
|
|
31
|
+
feelsLike: z.number(),
|
|
32
|
+
humidity: z.number(),
|
|
33
|
+
windSpeed: z.number(),
|
|
34
|
+
windGust: z.number(),
|
|
35
|
+
conditions: z.string(),
|
|
36
|
+
location: z.string(),
|
|
37
|
+
}),
|
|
38
|
+
execute: async ({ context }) => {
|
|
39
|
+
console.log('weather tool', context);
|
|
40
|
+
return await getWeather(context.location);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const getWeather = async (location: string) => {
|
|
45
|
+
const geocodingUrl = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(location)}&count=1`;
|
|
46
|
+
const geocodingResponse = await fetch(geocodingUrl);
|
|
47
|
+
const geocodingData = (await geocodingResponse.json()) as GeocodingResponse;
|
|
48
|
+
|
|
49
|
+
if (!geocodingData.results?.[0]) {
|
|
50
|
+
throw new Error(`Location '${location}' not found`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const { latitude, longitude, name } = geocodingData.results[0];
|
|
54
|
+
|
|
55
|
+
const weatherUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t=temperature_2m,apparent_temperature,relative_humidity_2m,wind_speed_10m,wind_gusts_10m,weather_code`;
|
|
56
|
+
|
|
57
|
+
const response = await fetch(weatherUrl);
|
|
58
|
+
const data = (await response.json()) as WeatherResponse;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
temperature: data.current.temperature_2m,
|
|
62
|
+
feelsLike: data.current.apparent_temperature,
|
|
63
|
+
humidity: data.current.relative_humidity_2m,
|
|
64
|
+
windSpeed: data.current.wind_speed_10m,
|
|
65
|
+
windGust: data.current.wind_gusts_10m,
|
|
66
|
+
conditions: getWeatherCondition(data.current.weather_code),
|
|
67
|
+
location: name,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function getWeatherCondition(code: number): string {
|
|
72
|
+
const conditions: Record<number, string> = {
|
|
73
|
+
0: 'Clear sky',
|
|
74
|
+
1: 'Mainly clear',
|
|
75
|
+
2: 'Partly cloudy',
|
|
76
|
+
3: 'Overcast',
|
|
77
|
+
45: 'Foggy',
|
|
78
|
+
48: 'Depositing rime fog',
|
|
79
|
+
51: 'Light drizzle',
|
|
80
|
+
53: 'Moderate drizzle',
|
|
81
|
+
55: 'Dense drizzle',
|
|
82
|
+
56: 'Light freezing drizzle',
|
|
83
|
+
57: 'Dense freezing drizzle',
|
|
84
|
+
61: 'Slight rain',
|
|
85
|
+
63: 'Moderate rain',
|
|
86
|
+
65: 'Heavy rain',
|
|
87
|
+
66: 'Light freezing rain',
|
|
88
|
+
67: 'Heavy freezing rain',
|
|
89
|
+
71: 'Slight snow fall',
|
|
90
|
+
73: 'Moderate snow fall',
|
|
91
|
+
75: 'Heavy snow fall',
|
|
92
|
+
77: 'Snow grains',
|
|
93
|
+
80: 'Slight rain showers',
|
|
94
|
+
81: 'Moderate rain showers',
|
|
95
|
+
82: 'Violent rain showers',
|
|
96
|
+
85: 'Slight snow showers',
|
|
97
|
+
86: 'Heavy snow showers',
|
|
98
|
+
95: 'Thunderstorm',
|
|
99
|
+
96: 'Thunderstorm with slight hail',
|
|
100
|
+
99: 'Thunderstorm with heavy hail',
|
|
101
|
+
};
|
|
102
|
+
return conditions[code] || 'Unknown';
|
|
103
|
+
}
|
package/src/index.ts
CHANGED
package/src/logger.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
|
+
|
|
6
|
+
// Logger interface for type safety
|
|
7
|
+
export interface Logger {
|
|
8
|
+
info: (message: string, data?: any) => Promise<void>;
|
|
9
|
+
warning: (message: string, data?: any) => Promise<void>;
|
|
10
|
+
error: (message: string, error?: any) => Promise<void>;
|
|
11
|
+
debug: (message: string, data?: any) => Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const writeErrorLog = (message: string, data?: any) => {
|
|
15
|
+
const now = new Date();
|
|
16
|
+
const timestamp = now.toISOString();
|
|
17
|
+
const hourTimestamp = timestamp.slice(0, 13); // YYYY-MM-DDTHH
|
|
18
|
+
|
|
19
|
+
// Create log message
|
|
20
|
+
const logMessage = {
|
|
21
|
+
timestamp,
|
|
22
|
+
message,
|
|
23
|
+
...(data ? (typeof data === 'object' ? data : { data }) : {}),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Write to file
|
|
27
|
+
try {
|
|
28
|
+
// Ensure cache directory exists
|
|
29
|
+
const cacheDir = path.join(os.homedir(), '.cache', 'mastra', 'mcp-docs-server-logs');
|
|
30
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
31
|
+
|
|
32
|
+
// Create log file path with timestamp
|
|
33
|
+
const logFile = path.join(cacheDir, `${hourTimestamp}.log`);
|
|
34
|
+
|
|
35
|
+
// Append log entry to file
|
|
36
|
+
fs.appendFileSync(logFile, JSON.stringify(logMessage) + '\n', 'utf8');
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// If file writing fails, at least we still have stderr
|
|
39
|
+
console.error('Failed to write to log file:', err);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Create logger factory to inject server instance
|
|
44
|
+
export function createLogger(server?: Server): Logger {
|
|
45
|
+
const sendLog = async (level: 'error' | 'debug' | 'info' | 'warning', message: string, data?: any) => {
|
|
46
|
+
if (!server) return;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await server.sendLoggingMessage({
|
|
50
|
+
level,
|
|
51
|
+
data: {
|
|
52
|
+
message,
|
|
53
|
+
...(data ? (typeof data === 'object' ? data : { data }) : {}),
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (
|
|
58
|
+
error instanceof Error &&
|
|
59
|
+
(error.message === 'Not connected' ||
|
|
60
|
+
error.message.includes('does not support logging') ||
|
|
61
|
+
error.message.includes('Connection closed'))
|
|
62
|
+
) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
console.error(`Failed to send ${level} log:`, error instanceof Error ? error.message : error);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
info: async (message: string, data?: any) => {
|
|
71
|
+
// Log to stderr to avoid MCP protocol conflicts on stdout
|
|
72
|
+
console.error(message, data ? data : '');
|
|
73
|
+
await sendLog('info', message, data);
|
|
74
|
+
},
|
|
75
|
+
warning: async (message: string, data?: any) => {
|
|
76
|
+
// Log to stderr to avoid MCP protocol conflicts on stdout
|
|
77
|
+
console.error(message, data ? data : '');
|
|
78
|
+
await sendLog('warning', message, data);
|
|
79
|
+
},
|
|
80
|
+
error: async (message: string, error?: any) => {
|
|
81
|
+
const errorData =
|
|
82
|
+
error instanceof Error
|
|
83
|
+
? {
|
|
84
|
+
message: error.message,
|
|
85
|
+
stack: error.stack,
|
|
86
|
+
name: error.name,
|
|
87
|
+
}
|
|
88
|
+
: error;
|
|
89
|
+
writeErrorLog(message, errorData);
|
|
90
|
+
console.error(message, errorData ? errorData : '');
|
|
91
|
+
await sendLog('error', message, errorData);
|
|
92
|
+
},
|
|
93
|
+
debug: async (message: string, data?: any) => {
|
|
94
|
+
if (process.env.DEBUG || process.env.NODE_ENV === 'development') {
|
|
95
|
+
// Log to stderr to avoid MCP protocol conflicts on stdout
|
|
96
|
+
console.error(message, data ? data : '');
|
|
97
|
+
await sendLog('debug', message, data);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Create a default logger instance
|
|
104
|
+
export const logger = createLogger();
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest';
|
|
5
|
+
import { weatherTool } from './__fixtures__/tools';
|
|
6
|
+
import { MCPConfiguration } from './configuration';
|
|
7
|
+
import { MCPServer } from './server';
|
|
8
|
+
|
|
9
|
+
const PORT = 9100 + Math.floor(Math.random() * 1000);
|
|
10
|
+
let server: MCPServer;
|
|
11
|
+
let httpServer: http.Server;
|
|
12
|
+
|
|
13
|
+
vi.setConfig({ testTimeout: 20000, hookTimeout: 20000 });
|
|
14
|
+
|
|
15
|
+
describe('MCPServer', () => {
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
server = new MCPServer({
|
|
18
|
+
name: 'Test MCP Server',
|
|
19
|
+
version: '0.1.0',
|
|
20
|
+
tools: { weatherTool },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
httpServer = http.createServer(async (req, res) => {
|
|
24
|
+
const url = new URL(req.url || '', `http://localhost:${PORT}`);
|
|
25
|
+
await server.startSSE({
|
|
26
|
+
url,
|
|
27
|
+
ssePath: '/sse',
|
|
28
|
+
messagePath: '/message',
|
|
29
|
+
req,
|
|
30
|
+
res,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await new Promise(resolve => httpServer.listen(PORT, resolve));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterAll(async () => {
|
|
38
|
+
await new Promise(resolve => httpServer.close(resolve));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('MCPServer SSE transport', () => {
|
|
42
|
+
let sseRes: Response | undefined;
|
|
43
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
44
|
+
|
|
45
|
+
afterEach(async () => {
|
|
46
|
+
if (reader) {
|
|
47
|
+
try {
|
|
48
|
+
await reader.cancel();
|
|
49
|
+
} catch {}
|
|
50
|
+
reader = undefined;
|
|
51
|
+
}
|
|
52
|
+
if (sseRes && 'body' in sseRes && sseRes.body) {
|
|
53
|
+
try {
|
|
54
|
+
await sseRes.body.cancel();
|
|
55
|
+
} catch {}
|
|
56
|
+
sseRes = undefined;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should parse SSE stream and contain tool output', async () => {
|
|
61
|
+
sseRes = await fetch(`http://localhost:${PORT}/sse`, {
|
|
62
|
+
headers: { Accept: 'text/event-stream' },
|
|
63
|
+
});
|
|
64
|
+
expect(sseRes.status).toBe(200);
|
|
65
|
+
reader = sseRes.body?.getReader();
|
|
66
|
+
expect(reader).toBeDefined();
|
|
67
|
+
await fetch(`http://localhost:${PORT}/message`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({ tool: 'weatherTool', input: { location: 'Austin' } }),
|
|
71
|
+
});
|
|
72
|
+
if (reader) {
|
|
73
|
+
const { value } = await reader.read();
|
|
74
|
+
const text = value ? new TextDecoder().decode(value) : '';
|
|
75
|
+
expect(text).toMatch(/data:/);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return 503 if message sent before SSE connection', async () => {
|
|
80
|
+
// Manually clear the SSE transport
|
|
81
|
+
(server as any).sseTransport = undefined;
|
|
82
|
+
const res = await fetch(`http://localhost:${PORT}/message`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { 'Content-Type': 'application/json' },
|
|
85
|
+
body: JSON.stringify({ tool: 'weatherTool', input: { location: 'Austin' } }),
|
|
86
|
+
});
|
|
87
|
+
expect(res.status).toBe(503);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('MCPServer stdio transport', () => {
|
|
92
|
+
it('should connect and expose stdio transport', async () => {
|
|
93
|
+
await server.startStdio();
|
|
94
|
+
expect(server.getStdioTransport()).toBeInstanceOf(StdioServerTransport);
|
|
95
|
+
});
|
|
96
|
+
it('should use stdio transport to get tools', async () => {
|
|
97
|
+
const existingConfig = new MCPConfiguration({
|
|
98
|
+
servers: {
|
|
99
|
+
weather: {
|
|
100
|
+
command: 'npx',
|
|
101
|
+
args: ['-y', 'tsx', path.join(__dirname, '__fixtures__/server-weather.ts')],
|
|
102
|
+
env: {
|
|
103
|
+
FAKE_CREDS: 'test',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const tools = await existingConfig.getTools();
|
|
110
|
+
expect(Object.keys(tools).length).toBeGreaterThan(0);
|
|
111
|
+
expect(Object.keys(tools)[0]).toBe('weather_weatherTool');
|
|
112
|
+
await existingConfig.disconnect();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { isVercelTool, isZodType, resolveSerializedZodOutput } from '@mastra/core';
|
|
2
|
+
import type { ToolsInput } from '@mastra/core/agent';
|
|
3
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import jsonSchemaToZod from 'json-schema-to-zod';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
10
|
+
import { createLogger } from './logger';
|
|
11
|
+
|
|
12
|
+
const logger = createLogger();
|
|
13
|
+
|
|
14
|
+
type ConvertedTool = {
|
|
15
|
+
name: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
inputSchema: any;
|
|
18
|
+
zodSchema: z.ZodTypeAny;
|
|
19
|
+
execute: any;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class MCPServer {
|
|
23
|
+
private server: Server;
|
|
24
|
+
private convertedTools: Record<string, ConvertedTool>;
|
|
25
|
+
private stdioTransport?: StdioServerTransport;
|
|
26
|
+
private sseTransport?: SSEServerTransport;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the current stdio transport.
|
|
30
|
+
*/
|
|
31
|
+
public getStdioTransport(): StdioServerTransport | undefined {
|
|
32
|
+
return this.stdioTransport;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the current SSE transport.
|
|
37
|
+
*/
|
|
38
|
+
public getSseTransport(): SSEServerTransport | undefined {
|
|
39
|
+
return this.sseTransport;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get a read-only view of the registered tools (for testing/introspection).
|
|
44
|
+
*/
|
|
45
|
+
tools(): Readonly<Record<string, ConvertedTool>> {
|
|
46
|
+
return this.convertedTools;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Construct a new MCPServer instance.
|
|
51
|
+
* @param opts.name - Server name
|
|
52
|
+
* @param opts.version - Server version
|
|
53
|
+
* @param opts.tools - Tool definitions to register
|
|
54
|
+
*/
|
|
55
|
+
constructor({ name, version, tools }: { name: string; version: string; tools: ToolsInput }) {
|
|
56
|
+
this.server = new Server({ name, version }, { capabilities: { tools: {}, logging: { enabled: true } } });
|
|
57
|
+
this.convertedTools = this.convertTools(tools);
|
|
58
|
+
void logger.info(
|
|
59
|
+
`Initialized MCPServer '${name}' v${version} with tools: ${Object.keys(this.convertedTools).join(', ')}`,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
this.registerListToolsHandler();
|
|
63
|
+
this.registerCallToolHandler();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Convert and validate all provided tools, logging registration status.
|
|
68
|
+
* @param tools Tool definitions
|
|
69
|
+
* @returns Converted tools registry
|
|
70
|
+
*/
|
|
71
|
+
private convertTools(tools: ToolsInput): Record<string, ConvertedTool> {
|
|
72
|
+
const convertedTools: Record<string, ConvertedTool> = {};
|
|
73
|
+
for (const toolName of Object.keys(tools)) {
|
|
74
|
+
let inputSchema: any;
|
|
75
|
+
let zodSchema: z.ZodTypeAny;
|
|
76
|
+
const toolInstance = tools[toolName];
|
|
77
|
+
if (!toolInstance) {
|
|
78
|
+
void logger.warning(`Tool instance for '${toolName}' is undefined. Skipping.`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (typeof toolInstance.execute !== 'function') {
|
|
82
|
+
void logger.warning(`Tool '${toolName}' does not have a valid execute function. Skipping.`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// Vercel tools: .parameters is either Zod or JSON schema
|
|
86
|
+
if (isVercelTool(toolInstance)) {
|
|
87
|
+
if (isZodType(toolInstance.parameters)) {
|
|
88
|
+
zodSchema = toolInstance.parameters;
|
|
89
|
+
inputSchema = zodToJsonSchema(zodSchema);
|
|
90
|
+
} else if (typeof toolInstance.parameters === 'object') {
|
|
91
|
+
zodSchema = resolveSerializedZodOutput(jsonSchemaToZod(toolInstance.parameters));
|
|
92
|
+
inputSchema = toolInstance.parameters;
|
|
93
|
+
} else {
|
|
94
|
+
zodSchema = z.object({});
|
|
95
|
+
inputSchema = zodToJsonSchema(zodSchema);
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// Mastra tools: .inputSchema is always Zod
|
|
99
|
+
zodSchema = toolInstance?.inputSchema ?? z.object({});
|
|
100
|
+
inputSchema = zodToJsonSchema(zodSchema);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Wrap execute to support both signatures (typed, returns Promise<any>)
|
|
104
|
+
const execute: (args: any, execOptions?: any) => Promise<any> = async (args, execOptions) => {
|
|
105
|
+
if (isVercelTool(toolInstance)) {
|
|
106
|
+
return (await toolInstance.execute?.(args, execOptions)) ?? undefined;
|
|
107
|
+
}
|
|
108
|
+
return (await toolInstance.execute?.({ context: args }, execOptions)) ?? undefined;
|
|
109
|
+
};
|
|
110
|
+
convertedTools[toolName] = {
|
|
111
|
+
name: toolName,
|
|
112
|
+
description: toolInstance?.description,
|
|
113
|
+
inputSchema,
|
|
114
|
+
zodSchema,
|
|
115
|
+
execute,
|
|
116
|
+
};
|
|
117
|
+
void logger.info(`Registered tool: '${toolName}' [${toolInstance?.description || 'No description'}]`);
|
|
118
|
+
}
|
|
119
|
+
void logger.info(`Total tools registered: ${Object.keys(convertedTools).length}`);
|
|
120
|
+
return convertedTools;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Register the ListTools handler for listing all available tools.
|
|
125
|
+
*/
|
|
126
|
+
private registerListToolsHandler() {
|
|
127
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
128
|
+
await logger.debug('Handling ListTools request');
|
|
129
|
+
return {
|
|
130
|
+
tools: Object.values(this.convertedTools).map(tool => ({
|
|
131
|
+
name: tool.name,
|
|
132
|
+
description: tool.description,
|
|
133
|
+
inputSchema: tool.inputSchema,
|
|
134
|
+
})),
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Register the CallTool handler for executing a tool by name.
|
|
141
|
+
*/
|
|
142
|
+
private registerCallToolHandler() {
|
|
143
|
+
this.server.setRequestHandler(CallToolRequestSchema, async request => {
|
|
144
|
+
const startTime = Date.now();
|
|
145
|
+
try {
|
|
146
|
+
const tool = this.convertedTools[request.params.name];
|
|
147
|
+
if (!tool) {
|
|
148
|
+
await logger.warning(`CallTool: Unknown tool '${request.params.name}' requested.`);
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }],
|
|
151
|
+
isError: true,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
await logger.debug(`CallTool: Invoking '${request.params.name}' with arguments:`, request.params.arguments);
|
|
155
|
+
const args = tool.zodSchema.parse(request.params.arguments ?? {});
|
|
156
|
+
const result = await tool.execute(args, request.params);
|
|
157
|
+
const duration = Date.now() - startTime;
|
|
158
|
+
await logger.info(`Tool '${request.params.name}' executed successfully in ${duration}ms.`);
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: 'text',
|
|
163
|
+
text: JSON.stringify(result),
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
isError: false,
|
|
167
|
+
};
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const duration = Date.now() - startTime;
|
|
170
|
+
if (error instanceof z.ZodError) {
|
|
171
|
+
await logger.warning('Invalid tool arguments', {
|
|
172
|
+
tool: request.params.name,
|
|
173
|
+
errors: error.errors,
|
|
174
|
+
duration: `${duration}ms`,
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: 'text',
|
|
180
|
+
text: `Invalid arguments: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
isError: true,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
await logger.error(`Tool execution failed: ${request.params.name}`, error);
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}` }],
|
|
189
|
+
isError: true,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Start the MCP server using stdio transport (for Windsurf integration).
|
|
197
|
+
*/
|
|
198
|
+
async startStdio() {
|
|
199
|
+
this.stdioTransport = new StdioServerTransport();
|
|
200
|
+
await this.server.connect(this.stdioTransport);
|
|
201
|
+
await logger.info('Started MCP Server (stdio)');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Handles MCP-over-SSE protocol for user-provided HTTP servers.
|
|
206
|
+
* Call this from your HTTP server for both the SSE and message endpoints.
|
|
207
|
+
*
|
|
208
|
+
* @param url Parsed URL of the incoming request
|
|
209
|
+
* @param ssePath Path for establishing the SSE connection (e.g. '/sse')
|
|
210
|
+
* @param messagePath Path for POSTing client messages (e.g. '/message')
|
|
211
|
+
* @param req Incoming HTTP request
|
|
212
|
+
* @param res HTTP response (must support .write/.end)
|
|
213
|
+
*/
|
|
214
|
+
async startSSE({
|
|
215
|
+
url,
|
|
216
|
+
ssePath,
|
|
217
|
+
messagePath,
|
|
218
|
+
req,
|
|
219
|
+
res,
|
|
220
|
+
}: {
|
|
221
|
+
url: URL;
|
|
222
|
+
ssePath: string;
|
|
223
|
+
messagePath: string;
|
|
224
|
+
req: any;
|
|
225
|
+
res: any;
|
|
226
|
+
}) {
|
|
227
|
+
if (url.pathname === ssePath) {
|
|
228
|
+
await logger.debug('Received SSE connection');
|
|
229
|
+
this.sseTransport = new SSEServerTransport(messagePath, res);
|
|
230
|
+
await this.server.connect(this.sseTransport);
|
|
231
|
+
|
|
232
|
+
this.server.onclose = async () => {
|
|
233
|
+
await this.server.close();
|
|
234
|
+
this.sseTransport = undefined;
|
|
235
|
+
};
|
|
236
|
+
res.on('close', () => {
|
|
237
|
+
this.sseTransport = undefined;
|
|
238
|
+
});
|
|
239
|
+
} else if (url.pathname === messagePath) {
|
|
240
|
+
await logger.debug('Received message');
|
|
241
|
+
if (!this.sseTransport) {
|
|
242
|
+
res.writeHead(503);
|
|
243
|
+
res.end('SSE connection not established');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
await this.sseTransport.handlePostMessage(req, res);
|
|
247
|
+
} else {
|
|
248
|
+
await logger.debug('Unknown path:', url.pathname);
|
|
249
|
+
res.writeHead(404);
|
|
250
|
+
res.end();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|