@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/mcp",
3
- "version": "0.4.0-alpha.8",
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-alpha.7"
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}&current=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
@@ -1,2 +1,3 @@
1
1
  export * from './client';
2
2
  export * from './configuration';
3
+ export * from './server';
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
+ }