@mailjet/mailjet-mcp-server 1.0.2

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.
@@ -0,0 +1,656 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import yaml from "js-yaml";
6
+ import https from "node:https";
7
+ import { createReadStream } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import process from "node:process";
10
+ import { z } from "zod";
11
+ import { MailjetApiSchema } from "./mailjet-openapi-schema.js";
12
+ import packageInfo from "../package.json" with { type: "json" };
13
+
14
+ const __dirname = import.meta.dirname;
15
+
16
+ export const server = new McpServer({
17
+ name: "mailjet",
18
+ version: packageInfo.version,
19
+ });
20
+
21
+ // Mailjet API authentication credentials in the documented BASIC Auth form "api_key:secret_key"
22
+ const API_KEY = process.env.MAILJET_API_KEY;
23
+ // Alternate non-US region of the API server
24
+ const API_REGION = process.env.MAILJET_API_REGION?.toLowerCase();
25
+ // API server hostname based on region
26
+ const API_HOSTNAME = `api.${API_REGION ? `${API_REGION}.` : ""}mailjet.com`;
27
+ // Path to openapi spec file
28
+ const OPENAPI_SPEC = resolve(__dirname, "openapi-mailjet.yaml");
29
+
30
+ /**
31
+ * Extracts all endpoints from the OpenAPI specification and organizes them by HTTP method.
32
+ *
33
+ * @param {z.infer<typeof MailjetApiSchema>} openApiSpec - Parsed OpenAPI specification
34
+ */
35
+ function extractEndpoints(openApiSpec) {
36
+ try {
37
+ // Initialize the endpoints dictionary
38
+ const endpoints = {
39
+ /** @type {string[]} DELETE - List of DELETE endpoints supported by the API */
40
+ DELETE: [],
41
+ /** @type {string[]} GET - List of GET endpoints supported by the API */
42
+ GET: [],
43
+ /** @type {string[]} PUT - List of PUT endpoints supported by the API */
44
+ PUT: [],
45
+ /** @type {string[]} POST - List of POST endpoints supported by the API */
46
+ POST: [],
47
+ };
48
+
49
+ const paths = openApiSpec.paths;
50
+
51
+ Object.keys(paths).forEach((path) => {
52
+ const pathItem = paths[path];
53
+
54
+ // Check for each HTTP method
55
+ if (pathItem.get) {
56
+ endpoints.GET.push(path);
57
+ }
58
+ if (pathItem.post) {
59
+ endpoints.POST.push(path);
60
+ }
61
+ if (pathItem.put) {
62
+ endpoints.PUT.push(path);
63
+ }
64
+ if (pathItem.delete) {
65
+ endpoints.DELETE.push(path);
66
+ }
67
+ });
68
+
69
+ return endpoints;
70
+ } catch (error) {
71
+ console.error("Error extracting endpoints:", error);
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Loads and parses the OpenAPI specification from a YAML file
78
+ * @param {string} filePath - Path to the OpenAPI YAML file
79
+ * @returns {Promise<unknown>} - Parsed OpenAPI specification
80
+ */
81
+ export async function loadOpenApiSpec(filePath) {
82
+ try {
83
+ const streamedFile = createReadStream(filePath, { encoding: "utf-8" });
84
+
85
+ /** @type {string} file contents read into a string */
86
+ const contents = await new Promise((resolve, reject) => {
87
+ let data = "";
88
+ streamedFile.on("data", (chunk) => {
89
+ data += chunk;
90
+ });
91
+ streamedFile.on("end", () => {
92
+ resolve(data);
93
+ });
94
+ streamedFile.on("error", (err) => {
95
+ reject(err);
96
+ });
97
+ });
98
+
99
+ return yaml.load(contents);
100
+ } catch (/** @type {any} */ error) {
101
+ console.error(`Error loading OpenAPI spec: ${error.message}`);
102
+ // Don't exit in test mode
103
+ if (process.env.NODE_ENV !== "test") {
104
+ process.exit(1);
105
+ }
106
+ throw error; // Throw so tests can catch it
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Retrieves operation details from the OpenAPI spec for a given method and path
112
+ * @param {z.infer<typeof MailjetApiSchema>} openApiSpec - Parsed OpenAPI specification
113
+ * @param {keyof ReturnType<typeof extractEndpoints>} method - HTTP method (GET, POST, etc.)
114
+ * @param { ReturnType<typeof extractEndpoints>[keyof ReturnType<typeof extractEndpoints>][number] } path - API endpoint path
115
+ * @returns Operation details or null if not found
116
+ */
117
+ export function getOperationDetails(openApiSpec, method, path) {
118
+ const lowerMethod = method.toLowerCase();
119
+
120
+ // @ts-ignore lowercased string loses type info
121
+ if (!openApiSpec.paths?.[path]?.[lowerMethod]) {
122
+ return null;
123
+ }
124
+
125
+ return {
126
+ /** @type {NonNullable<z.infer<typeof MailjetApiSchema>["paths"][string]["delete" | "get" | "post" | "put"]>} */
127
+ // @ts-ignore We know this exists because of the if condition above
128
+ operation: openApiSpec.paths[path][lowerMethod],
129
+ operationId:
130
+ openApiSpec.paths[path]["get"]?.operationId ??
131
+ `${method}-${sanitizeToolId(path).replace(/-+/g, "-")}`,
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Converts OpenAPI schema definitions to Zod validation schemas
137
+ * @param {any} schema - OpenAPI schema object
138
+ * @param {z.infer<typeof MailjetApiSchema>} fullSpec - Complete OpenAPI specification
139
+ * @returns {z.ZodType} - Corresponding Zod schema
140
+ */
141
+ export function openapiToZod(schema, fullSpec) {
142
+ if (!schema) {
143
+ return z.any();
144
+ }
145
+
146
+ // Handle schema references (e.g. #/components/schemas/...)
147
+ if (schema.$ref) {
148
+ // For #/components/schemas/ type references
149
+ if (schema.$ref.startsWith("#/")) {
150
+ const refPath = schema.$ref.substring(2).split("/");
151
+ /** @type any */
152
+ let referenced = fullSpec;
153
+ for (const segment of refPath) {
154
+ if (!referenced || !referenced[segment]) {
155
+ console.error(`Failed to resolve reference: ${schema.$ref}, segment: ${segment}`);
156
+ return z.any().describe(`Failed reference: ${schema.$ref}`);
157
+ }
158
+ referenced = referenced[segment];
159
+ }
160
+
161
+ return openapiToZod(referenced, fullSpec);
162
+ }
163
+
164
+ // Handle other reference formats if needed
165
+ console.error(`Unsupported reference format: ${schema.$ref}`);
166
+ return z.any().describe(`Unsupported reference: ${schema.$ref}`);
167
+ }
168
+
169
+ // Convert different schema types to Zod equivalents
170
+ switch (schema.type) {
171
+ case "string":
172
+ let zodString = z.string();
173
+ if (schema.enum) {
174
+ return z.enum(schema.enum);
175
+ }
176
+ if (schema.format === "email") {
177
+ zodString = zodString.email();
178
+ }
179
+ if (schema.format === "uri") {
180
+ zodString = zodString.describe(`URI: ${schema.description || ""}`);
181
+ }
182
+ return zodString.describe(schema.description || "");
183
+
184
+ case "number":
185
+ case "integer":
186
+ let zodNumber = z.number();
187
+ if (schema.minimum !== undefined) {
188
+ zodNumber = zodNumber.min(schema.minimum);
189
+ }
190
+ if (schema.maximum !== undefined) {
191
+ zodNumber = zodNumber.max(schema.maximum);
192
+ }
193
+ return zodNumber.describe(schema.description || "");
194
+
195
+ case "boolean":
196
+ return z.boolean().describe(schema.description || "");
197
+
198
+ case "array":
199
+ return z.array(openapiToZod(schema.items, fullSpec)).describe(schema.description || "");
200
+
201
+ case "object":
202
+ if (!schema.properties) {
203
+ return z.record(z.string(), z.any());
204
+ }
205
+
206
+ /** @type Record<string, z.ZodType> */
207
+ const shape = {};
208
+ for (const [key, prop] of Object.entries(schema.properties)) {
209
+ shape[key] = schema.required?.includes(key)
210
+ ? openapiToZod(prop, fullSpec)
211
+ : openapiToZod(prop, fullSpec).optional();
212
+ }
213
+ return z.object(shape).describe(schema.description || "");
214
+
215
+ default:
216
+ // For schemas without a type but with properties
217
+ if (schema.properties) {
218
+ /** @type Record<string, z.ZodType> */
219
+ const shape = {};
220
+ for (const [key, prop] of Object.entries(schema.properties)) {
221
+ shape[key] = schema.required?.includes(key)
222
+ ? openapiToZod(prop, fullSpec)
223
+ : openapiToZod(prop, fullSpec).optional();
224
+ }
225
+ return z.object(shape).describe(schema.description || "");
226
+ }
227
+
228
+ // For YAML that defines "oneOf", "anyOf", etc.
229
+ if (schema.oneOf) {
230
+ const unionTypes = schema.oneOf.map((/** @type unknown */ s) => openapiToZod(s, fullSpec));
231
+ if (unionTypes.length === 1) {
232
+ return unionTypes[0].describe(schema.description || "");
233
+ }
234
+ return z.union(unionTypes).describe(schema.description || "");
235
+ }
236
+
237
+ if (schema.anyOf) {
238
+ const unionTypes = schema.anyOf.map((/** @type unknown */ s) => openapiToZod(s, fullSpec));
239
+ if (unionTypes.length === 1) {
240
+ return unionTypes[0].describe(schema.description || "");
241
+ }
242
+ return z.union(unionTypes).describe(schema.description || "");
243
+ }
244
+
245
+ return z.any().describe(schema.description || "");
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Processes OpenAPI parameters into Zod schemas
251
+ * @param {NonNullable<z.infer<typeof MailjetApiSchema>["paths"][string]["parameters"]>} parameters - OpenAPI parameter objects
252
+ * @param {Record<string, z.ZodType>} paramsSchema - Target schema object to populate
253
+ * @param {z.infer<typeof MailjetApiSchema>} openApiSpec - Complete OpenAPI specification
254
+ */
255
+ export function processParameters(parameters, paramsSchema, openApiSpec) {
256
+ for (const param of parameters) {
257
+ const zodParam = openapiToZod(param.schema, openApiSpec);
258
+ paramsSchema[param.name] = param.required ? zodParam : zodParam.optional();
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Resolves a schema reference within an OpenAPI spec
264
+ * @param {string} ref - Reference string (e.g. #/components/schemas/ModelName)
265
+ * @param {z.infer<typeof MailjetApiSchema>} openApiSpec - Complete OpenAPI specification
266
+ * @returns Resolved schema
267
+ */
268
+ export function resolveReference(ref, openApiSpec) {
269
+ const refPath = ref.replace("#/", "").split("/");
270
+ // top-level reference key is missing in mailjet schema
271
+ return refPath.reduce((obj, path) => obj[path], openApiSpec);
272
+ }
273
+
274
+ /**
275
+ * Processes request body schema into Zod schemas
276
+ * @param {NonNullable<z.infer<typeof MailjetApiSchema>["paths"][string]["delete" | "get" | "post" | "put"]>['requestBody']} requestBody - OpenAPI request body object
277
+ * @param {Record<string, z.ZodType>} paramsSchema - Target schema object to populate
278
+ * @param {z.infer<typeof MailjetApiSchema>} openApiSpec - Complete OpenAPI specification
279
+ */
280
+ export function processRequestBody(requestBody, paramsSchema, openApiSpec) {
281
+ if (!requestBody?.content) {
282
+ return;
283
+ }
284
+
285
+ // All requests are currently JSON
286
+ const contentType = "application/json";
287
+
288
+ let bodySchema = requestBody.content[contentType].schema;
289
+
290
+ // Handle schema references.
291
+ if (bodySchema?.$ref) {
292
+ bodySchema = resolveReference(bodySchema.$ref, openApiSpec);
293
+ }
294
+
295
+ // Process schema properties
296
+ if (bodySchema?.properties) {
297
+ for (const [prop, schema] of Object.entries(bodySchema.properties)) {
298
+ let propSchema = schema;
299
+
300
+ // Handle nested references
301
+ if (propSchema.$ref) {
302
+ propSchema = resolveReference(propSchema.$ref, openApiSpec);
303
+ }
304
+
305
+ const zodProp = openapiToZod(propSchema, openApiSpec);
306
+ paramsSchema[prop] = bodySchema?.required?.includes(prop) ? zodProp : zodProp.optional();
307
+ }
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Builds a Zod parameter schema from an OpenAPI operation
313
+ * @param {NonNullable<ReturnType<typeof getOperationDetails>>['operation']} operation - OpenAPI operation object
314
+ * @param {z.infer<typeof MailjetApiSchema>} openApiSpec - Complete OpenAPI specification
315
+ * @returns Zod parameter schema
316
+ */
317
+ export function buildParamsSchema(operation, openApiSpec) {
318
+ /** @type {Record<string, z.ZodType>} */
319
+ const paramsSchema = {};
320
+
321
+ // Process path parameters
322
+ const pathParams = operation?.parameters?.filter((p) => p.in === "path") || [];
323
+ processParameters(pathParams, paramsSchema, openApiSpec);
324
+
325
+ // Process query parameters
326
+ const queryParams = operation?.parameters?.filter((p) => p.in === "query") || [];
327
+ processParameters(queryParams, paramsSchema, openApiSpec);
328
+
329
+ // Process request body if it exists
330
+ if (operation?.requestBody) {
331
+ processRequestBody(operation.requestBody, paramsSchema, openApiSpec);
332
+ }
333
+
334
+ return paramsSchema;
335
+ }
336
+
337
+ /**
338
+ * Sanitizes an operation ID to be used as a tool ID
339
+ * @param {string} operationId - The operation ID to sanitize
340
+ * @returns Sanitized tool ID
341
+ */
342
+ export function sanitizeToolId(operationId) {
343
+ return operationId.replace(/[^\w-]/g, "-").toLowerCase();
344
+ }
345
+
346
+ /**
347
+ * Processes path parameters from the request parameters
348
+ * @param {string} path - API endpoint path with placeholders
349
+ * @param {NonNullable<ReturnType<typeof getOperationDetails>>['operation']} operation - OpenAPI operation object
350
+ * @param {Record<string, string | number>} params - Request parameters
351
+ * @returns Processed path and remaining parameters
352
+ */
353
+ export function processPathParameters(path, operation, params) {
354
+ let actualPath = path;
355
+ const pathParams = operation.parameters?.filter((p) => p.in === "path") || [];
356
+ const remainingParams = { ...params };
357
+
358
+ for (const param of pathParams) {
359
+ if (params[param.name]) {
360
+ actualPath = actualPath.replace(`{${param.name}}`, encodeURIComponent(params[param.name]));
361
+ delete remainingParams[param.name];
362
+ } else {
363
+ throw new Error(`Required path parameter '${param.name}' is missing`);
364
+ }
365
+ }
366
+
367
+ return { actualPath, remainingParams };
368
+ }
369
+
370
+ /**
371
+ * Separates parameters into query parameters and body parameters
372
+ * @param {Record<string, string | number>} params - Request parameters
373
+ * @param {NonNullable<ReturnType<typeof getOperationDetails>>['operation']} operation - OpenAPI operation object
374
+ * @param {keyof ReturnType<typeof extractEndpoints>} method - HTTP method (GET, POST, etc.)
375
+ * @returns Separated query and body parameters
376
+ */
377
+ export function separateParameters(params, operation, method) {
378
+ /** @type Record<string, string | number> */
379
+ const queryParams = {};
380
+ /** @type Record<string, string | number> */
381
+ const bodyParams = {};
382
+
383
+ // Get query parameters from operation definition
384
+ const definedQueryParams =
385
+ operation.parameters?.filter((p) => p.in === "query").map((p) => p.name) || [];
386
+
387
+ // Sort parameters into body or query
388
+ for (const [key, value] of Object.entries(params)) {
389
+ if (definedQueryParams.includes(key)) {
390
+ queryParams[key] = value;
391
+ } else {
392
+ bodyParams[key] = value;
393
+ }
394
+ }
395
+
396
+ // For GET requests, move all params to query
397
+ if (method.toUpperCase() === "GET") {
398
+ Object.assign(queryParams, bodyParams);
399
+ Object.keys(bodyParams).forEach((key) => delete bodyParams[key]);
400
+ }
401
+
402
+ return { queryParams, bodyParams };
403
+ }
404
+
405
+ /**
406
+ * Appends query string parameters to a path
407
+ * @param {string} path - API endpoint path
408
+ * @param {Record<string, string | number>} queryParams - Query parameters
409
+ * @returns Path with query string
410
+ */
411
+ export function appendQueryString(path, queryParams) {
412
+ if (Object.keys(queryParams).length === 0) {
413
+ return path;
414
+ }
415
+
416
+ const queryString = new URLSearchParams();
417
+
418
+ for (const [key, value] of Object.entries(queryParams)) {
419
+ if (value !== undefined && value !== null) {
420
+ queryString.append(key, value.toString());
421
+ }
422
+ }
423
+
424
+ return `${path}?${queryString.toString()}`;
425
+ }
426
+
427
+ /**
428
+ * Makes an authenticated request to the Mailjet API
429
+ * @param {keyof ReturnType<typeof extractEndpoints>} method - HTTP method (GET, POST, etc.)
430
+ * @param {string} path - API endpoint path
431
+ * @param {Record<string, string | number> | null} data - Request payload data (for POST/PUT requests)
432
+ * @returns {Promise<JSON>} - Response data as JSON
433
+ */
434
+ export async function makeMailjetRequest(method, path, data = null) {
435
+ return new Promise((resolve, reject) => {
436
+ // Normalize path format (handle paths with or without leading slash)
437
+ const cleanPath = path.startsWith("/") ? path.substring(1) : path;
438
+
439
+ if (!API_KEY) {
440
+ throw new Error(`Required MAILJET_API_KEY environment variable is missing`);
441
+ }
442
+
443
+ // Create basic auth credentials from API key
444
+ const auth = Buffer.from(`${API_KEY}`).toString("base64");
445
+ const options = {
446
+ hostname: API_HOSTNAME,
447
+ path: `/${cleanPath}`,
448
+ method: method,
449
+ headers: {
450
+ Authorization: `Basic ${auth}`,
451
+ "Content-Type": "application/json",
452
+ "User-Agent": `Mailjet/MCP-SERVER-STDIO/${packageInfo.version}`,
453
+ },
454
+ };
455
+
456
+ // Create and send the HTTP request
457
+ const req = https.request(options, (res) => {
458
+ let responseData = "";
459
+
460
+ res.on("data", (chunk) => {
461
+ responseData += chunk;
462
+ });
463
+
464
+ res.on("end", () => {
465
+ try {
466
+ const parsedData = JSON.parse(responseData);
467
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
468
+ resolve(parsedData);
469
+ } else {
470
+ reject(new Error(`Mailjet API error: ${parsedData.message || responseData}`));
471
+ }
472
+ } catch (/** @type any */ e) {
473
+ reject(new Error(`Failed to parse response: ${e.message}`));
474
+ }
475
+ });
476
+ });
477
+
478
+ req.on("error", (error) => {
479
+ reject(error);
480
+ });
481
+
482
+ // For non-GET requests, serialize and send the form data
483
+ if (data && method !== "GET") {
484
+ // Convert object to URL encoded form data
485
+ const formData = new URLSearchParams();
486
+ for (const [key, value] of Object.entries(data)) {
487
+ if (Array.isArray(value)) {
488
+ for (const item of value) {
489
+ formData.append(key, item);
490
+ }
491
+ } else if (value !== undefined && value !== null) {
492
+ formData.append(key, value.toString());
493
+ }
494
+ }
495
+
496
+ req.write(formData.toString());
497
+ }
498
+
499
+ req.end();
500
+ });
501
+ }
502
+
503
+ /**
504
+ * Validates Mailjet API credentials by making a read-only test request.
505
+ *
506
+ * @param {string} credentials - The Mailjet API key in "PUBLIC_KEY:SECRET_KEY" format.
507
+ * @returns {Promise<boolean>} - Returns true if the keys are valid, false otherwise.
508
+ */
509
+ export async function validateMailjetKeys(credentials) {
510
+ // Ensure the credentials string exists and is properly formatted
511
+ if (!credentials || typeof credentials !== 'string' || !credentials.includes(':')) {
512
+ return false;
513
+ }
514
+
515
+ // Mailjet API requires Basic Authentication (Base64 encoded string of "public:private")
516
+ const encodedAuth = Buffer.from(credentials).toString('base64');
517
+
518
+ try {
519
+ const response = await fetch('https://api.mailjet.com/v3/REST/user', {
520
+ method: 'GET',
521
+ headers: {
522
+ 'Authorization': `Basic ${encodedAuth}`,
523
+ 'Content-Type': 'application/json'
524
+ }
525
+ });
526
+
527
+ // If Mailjet returns a 200 OK, the keys are perfectly valid
528
+ if (response.ok) {
529
+ return true;
530
+ }
531
+
532
+ // A 401 Unauthorized or any other non-2xx status means invalid keys/account
533
+ return false;
534
+
535
+ } catch (error) {
536
+ // Catch network errors (e.g., DNS failure, no internet connection)
537
+ return false;
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Registers a tool with the MCP server
543
+ * @param {string} toolId - Unique tool identifier
544
+ * @param {string} toolDescription - Human-readable description
545
+ * @param {Record<string, z.ZodType>} paramsSchema - Zod schema for parameters
546
+ * @param {keyof ReturnType<typeof extractEndpoints>} method - Supported methods (GET, POST, etc.)
547
+ * @param {string} path - API endpoint path
548
+ * @param {NonNullable<ReturnType<typeof getOperationDetails>>['operation']} operation - OpenAPI operation object
549
+ */
550
+ export function registerTool(toolId, toolDescription, paramsSchema, method, path, operation) {
551
+ server.tool(toolId, toolDescription, paramsSchema, async (params) => {
552
+ try {
553
+ const { actualPath, remainingParams } = processPathParameters(path, operation, params);
554
+ const { queryParams, bodyParams } = separateParameters(remainingParams, operation, method);
555
+ const finalPath = appendQueryString(actualPath, queryParams);
556
+
557
+ // Make the API request
558
+ const result = await makeMailjetRequest(
559
+ method,
560
+ finalPath,
561
+ method === "GET" ? null : bodyParams,
562
+ );
563
+
564
+ return {
565
+ content: [
566
+ {
567
+ type: "text",
568
+ text: `✅ ${method} ${finalPath} completed successfully:\n${JSON.stringify(result, null, 2)}`,
569
+ },
570
+ ],
571
+ };
572
+ } catch (/** @type any */ error) {
573
+ return {
574
+ content: [
575
+ {
576
+ type: "text",
577
+ text: `Error: ${error.message || String(error)}`,
578
+ },
579
+ ],
580
+ };
581
+ }
582
+ });
583
+ }
584
+
585
+ /**
586
+ * Generates MCP tools from the OpenAPI specification
587
+ * @param {z.infer<typeof MailjetApiSchema>} openApiSpec - Parsed OpenAPI specification
588
+ */
589
+ export function generateToolsFromOpenApi(openApiSpec) {
590
+ const endpoints = extractEndpoints(openApiSpec);
591
+
592
+ for (const path of endpoints.GET) {
593
+ const method = "GET";
594
+ try {
595
+ const operationDetails = getOperationDetails(openApiSpec, method, path);
596
+
597
+ if (!operationDetails) {
598
+ console.warn(`Could not match endpoint: ${method} ${path} in OpenAPI spec`);
599
+ continue;
600
+ }
601
+
602
+ const { operation, operationId } = operationDetails;
603
+ const paramsSchema = buildParamsSchema(operation, openApiSpec);
604
+ const toolId = sanitizeToolId(operationId);
605
+ const toolDescription = operation?.summary || `${method.toUpperCase()} ${path}`;
606
+
607
+ registerTool(toolId, toolDescription, paramsSchema, method, path, operation);
608
+ } catch (/** @type {any} */ error) {
609
+ console.error(`Failed to process endpoint ${method} ${path}: ${error.message}`);
610
+ }
611
+ }
612
+
613
+ return;
614
+ }
615
+
616
+ /**
617
+ * Main function to initialize and start the MCP server
618
+ */
619
+ export async function main() {
620
+ try {
621
+ const isValidApiKey = await validateMailjetKeys(API_KEY)
622
+
623
+ if(!API_KEY || !isValidApiKey) {
624
+ throw new Error(`⚠️ Please provide a valid MAILJET_API_KEY env var before running the mcp server.`);
625
+ }
626
+
627
+ // Load and parse OpenAPI spec
628
+ const openApiSpec = await loadOpenApiSpec(OPENAPI_SPEC);
629
+
630
+ try {
631
+ const parsedOpenApiSpec = MailjetApiSchema.parse(openApiSpec);
632
+
633
+ // Generate tools from the spec
634
+ generateToolsFromOpenApi(parsedOpenApiSpec);
635
+ } catch (/** @type { any } */ error) {
636
+ throw Error(error);
637
+ }
638
+
639
+ // Connect to the transport
640
+ const transport = new StdioServerTransport();
641
+ await server.connect(transport);
642
+ // This is an STDIO server and log msgs are sent to stdio by default
643
+ // So send to console.error to avoid errors on server startup
644
+ console.error(`Mailjet MCP Server ${packageInfo.version} running on stdio`);
645
+ } catch (error) {
646
+ console.error("Fatal error in main():", error);
647
+ if (process.env.NODE_ENV !== "test") {
648
+ process.exit(1);
649
+ }
650
+ }
651
+ }
652
+
653
+ // Only auto-execute when not in test environment
654
+ if (process.env.NODE_ENV !== "test") {
655
+ main();
656
+ }