@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.
- package/.github/workflows/run-tests.yml +36 -0
- package/CHANGELOG.md +40 -0
- package/CODEOWNERS +1 -0
- package/LICENSE +191 -0
- package/README.md +93 -0
- package/package.json +39 -0
- package/src/mailjet-mcp.js +656 -0
- package/src/mailjet-openapi-schema.js +63 -0
- package/src/openapi-mailjet.yaml +15364 -0
- package/tests/mailjet-mcp.test.js +249 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
|
|
4
|
+
import * as serverModule from "../src/mailjet-mcp.js";
|
|
5
|
+
|
|
6
|
+
// Mock OpenAPI spec for testing
|
|
7
|
+
const mockOpenApiSpec = {
|
|
8
|
+
paths: {
|
|
9
|
+
"/v3/REST/message": {
|
|
10
|
+
get: {
|
|
11
|
+
summary: "Get message",
|
|
12
|
+
parameters: [
|
|
13
|
+
{ name: "message_ID", in: "query", schema: { type: "string" }, required: false },
|
|
14
|
+
{ name: "required_param", in: "path", schema: { type: "string" }, required: true },
|
|
15
|
+
],
|
|
16
|
+
requestBody: {
|
|
17
|
+
content: {
|
|
18
|
+
"application/json": {
|
|
19
|
+
schema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
foo: { type: "string" },
|
|
23
|
+
bar: { type: "number" },
|
|
24
|
+
},
|
|
25
|
+
required: ["foo"],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
components: {
|
|
34
|
+
schemas: {
|
|
35
|
+
TestSchema: {
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
a: { type: "string" },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
describe("sanitizeToolId", () => {
|
|
46
|
+
it("should replace non-word characters and lowercase", () => {
|
|
47
|
+
assert.strictEqual(serverModule.sanitizeToolId("GET-/v3/REST/message"), "get--v3-rest-message");
|
|
48
|
+
assert.strictEqual(serverModule.sanitizeToolId("Some$Weird*ID"), "some-weird-id");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("appendQueryString", () => {
|
|
53
|
+
it("should append query params correctly", () => {
|
|
54
|
+
const path = "/foo/bar";
|
|
55
|
+
const params = { a: 1, b: "test" };
|
|
56
|
+
const result = serverModule.appendQueryString(path, params);
|
|
57
|
+
assert(result.startsWith("/foo/bar?"));
|
|
58
|
+
assert(result.includes("a=1"));
|
|
59
|
+
assert(result.includes("b=test"));
|
|
60
|
+
});
|
|
61
|
+
it("should return path unchanged if no params", () => {
|
|
62
|
+
assert.strictEqual(serverModule.appendQueryString("/foo", {}), "/foo");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("processPathParameters", () => {
|
|
67
|
+
const operation = {
|
|
68
|
+
parameters: [
|
|
69
|
+
{ name: "id", in: "path", schema: { type: "string" }, required: true },
|
|
70
|
+
{ name: "q", in: "query", schema: { type: "string" }, required: false },
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
it("should substitute path params", () => {
|
|
74
|
+
const params = { id: "123", q: "abc" };
|
|
75
|
+
const { actualPath, remainingParams } = serverModule.processPathParameters(
|
|
76
|
+
"/foo/{id}/bar",
|
|
77
|
+
operation,
|
|
78
|
+
params,
|
|
79
|
+
);
|
|
80
|
+
assert.strictEqual(actualPath, "/foo/123/bar");
|
|
81
|
+
assert.deepStrictEqual(remainingParams, { q: "abc" });
|
|
82
|
+
});
|
|
83
|
+
it("should throw if required path param missing", () => {
|
|
84
|
+
assert.throws(
|
|
85
|
+
() => serverModule.processPathParameters("/foo/{id}/bar", operation, { q: "abc" }),
|
|
86
|
+
/Required path parameter 'id' is missing/,
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("separateParameters", () => {
|
|
92
|
+
const operation = {
|
|
93
|
+
parameters: [
|
|
94
|
+
{ name: "q", in: "query", schema: { type: "string" }, required: false },
|
|
95
|
+
{ name: "body", in: "body", schema: { type: "string" }, required: false },
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
it("should separate query and body params for POST", () => {
|
|
99
|
+
const params = { q: "abc", body: "val", extra: 1 };
|
|
100
|
+
const { queryParams, bodyParams } = serverModule.separateParameters(params, operation, "POST");
|
|
101
|
+
assert.deepStrictEqual(queryParams, { q: "abc" });
|
|
102
|
+
assert.deepStrictEqual(bodyParams, { body: "val", extra: 1 });
|
|
103
|
+
});
|
|
104
|
+
it("should move all params to query for GET", () => {
|
|
105
|
+
const params = { q: "abc", body: "val", extra: 1 };
|
|
106
|
+
const { queryParams, bodyParams } = serverModule.separateParameters(params, operation, "GET");
|
|
107
|
+
assert.deepStrictEqual(queryParams, { q: "abc", body: "val", extra: 1 });
|
|
108
|
+
assert.deepStrictEqual(bodyParams, {});
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("getOperationDetails", () => {
|
|
113
|
+
it("should return operation details if found", () => {
|
|
114
|
+
const result = serverModule.getOperationDetails(mockOpenApiSpec, "GET", "/v3/REST/message");
|
|
115
|
+
assert(result);
|
|
116
|
+
assert.strictEqual(result.operation.summary, "Get message");
|
|
117
|
+
});
|
|
118
|
+
it("should return null if not found", () => {
|
|
119
|
+
const result = serverModule.getOperationDetails(mockOpenApiSpec, "POST", "/v3/REST/message");
|
|
120
|
+
assert.strictEqual(result, null);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("resolveReference", () => {
|
|
125
|
+
it("should resolve a reference path in OpenAPI spec", () => {
|
|
126
|
+
const ref = "#/components/schemas/TestSchema";
|
|
127
|
+
const resolved = serverModule.resolveReference(ref, mockOpenApiSpec);
|
|
128
|
+
assert.deepStrictEqual(resolved, { type: "object", properties: { a: { type: "string" } } });
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("processParameters", () => {
|
|
133
|
+
it("should add required and optional params to schema", () => {
|
|
134
|
+
const params = [
|
|
135
|
+
{ name: "foo", in: "query", schema: { type: "string" }, required: true },
|
|
136
|
+
{ name: "bar", in: "query", schema: { type: "number" }, required: false },
|
|
137
|
+
];
|
|
138
|
+
const paramsSchema = {};
|
|
139
|
+
serverModule.processParameters(params, paramsSchema, mockOpenApiSpec);
|
|
140
|
+
assert(paramsSchema.foo);
|
|
141
|
+
assert(paramsSchema.bar);
|
|
142
|
+
assert(paramsSchema.foo.isOptional() === false);
|
|
143
|
+
assert(paramsSchema.bar.isOptional() === true);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("processRequestBody", () => {
|
|
148
|
+
it("should add body properties to paramsSchema", () => {
|
|
149
|
+
const requestBody = {
|
|
150
|
+
content: {
|
|
151
|
+
"application/json": {
|
|
152
|
+
schema: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
foo: { type: "string" },
|
|
156
|
+
bar: { type: "number" },
|
|
157
|
+
},
|
|
158
|
+
required: ["foo"],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
const paramsSchema = {};
|
|
164
|
+
serverModule.processRequestBody(requestBody, paramsSchema, mockOpenApiSpec);
|
|
165
|
+
assert(paramsSchema.foo);
|
|
166
|
+
assert(paramsSchema.bar);
|
|
167
|
+
assert(paramsSchema.foo.isOptional() === false);
|
|
168
|
+
assert(paramsSchema.bar.isOptional() === true);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("buildParamsSchema", () => {
|
|
173
|
+
it("should build a Zod schema for operation parameters", () => {
|
|
174
|
+
const operation = {
|
|
175
|
+
parameters: [
|
|
176
|
+
{ name: "foo", in: "query", schema: { type: "string" }, required: true },
|
|
177
|
+
{ name: "bar", in: "path", schema: { type: "number" }, required: false },
|
|
178
|
+
],
|
|
179
|
+
requestBody: {
|
|
180
|
+
content: {
|
|
181
|
+
"application/json": {
|
|
182
|
+
schema: {
|
|
183
|
+
type: "object",
|
|
184
|
+
properties: {
|
|
185
|
+
baz: { type: "boolean" },
|
|
186
|
+
},
|
|
187
|
+
required: [],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
const schema = serverModule.buildParamsSchema(operation, mockOpenApiSpec);
|
|
194
|
+
assert(schema.foo);
|
|
195
|
+
assert(schema.bar);
|
|
196
|
+
assert(schema.baz);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("openapiToZod", () => {
|
|
201
|
+
it("should convert string schema", () => {
|
|
202
|
+
const zod = serverModule.openapiToZod({ type: "string" }, mockOpenApiSpec);
|
|
203
|
+
assert(zod);
|
|
204
|
+
assert.strictEqual(zod._def.type, "string");
|
|
205
|
+
});
|
|
206
|
+
it("should convert enum schema", () => {
|
|
207
|
+
const zod = serverModule.openapiToZod({ type: "string", enum: ["a", "b"] }, mockOpenApiSpec);
|
|
208
|
+
assert(zod);
|
|
209
|
+
assert.strictEqual(zod._def.type, "enum");
|
|
210
|
+
});
|
|
211
|
+
it("should convert number schema", () => {
|
|
212
|
+
const zod = serverModule.openapiToZod({ type: "number" }, mockOpenApiSpec);
|
|
213
|
+
assert(zod);
|
|
214
|
+
assert.strictEqual(zod._def.type, "number");
|
|
215
|
+
});
|
|
216
|
+
it("should convert boolean schema", () => {
|
|
217
|
+
const zod = serverModule.openapiToZod({ type: "boolean" }, mockOpenApiSpec);
|
|
218
|
+
assert(zod);
|
|
219
|
+
assert.strictEqual(zod._def.type, "boolean");
|
|
220
|
+
});
|
|
221
|
+
it("should convert array schema", () => {
|
|
222
|
+
const zod = serverModule.openapiToZod(
|
|
223
|
+
{ type: "array", items: { type: "string" } },
|
|
224
|
+
mockOpenApiSpec,
|
|
225
|
+
);
|
|
226
|
+
assert(zod);
|
|
227
|
+
assert.strictEqual(zod._def.type, "array");
|
|
228
|
+
});
|
|
229
|
+
it("should convert object schema", () => {
|
|
230
|
+
const zod = serverModule.openapiToZod(
|
|
231
|
+
{
|
|
232
|
+
type: "object",
|
|
233
|
+
properties: { foo: { type: "string" } },
|
|
234
|
+
required: ["foo"],
|
|
235
|
+
},
|
|
236
|
+
mockOpenApiSpec,
|
|
237
|
+
);
|
|
238
|
+
assert(zod);
|
|
239
|
+
assert.strictEqual(zod._def.type, "object");
|
|
240
|
+
});
|
|
241
|
+
it("should resolve $ref", () => {
|
|
242
|
+
const zod = serverModule.openapiToZod(
|
|
243
|
+
{ $ref: "#/components/schemas/TestSchema" },
|
|
244
|
+
mockOpenApiSpec,
|
|
245
|
+
);
|
|
246
|
+
assert(zod);
|
|
247
|
+
assert.strictEqual(zod._def.type, "object");
|
|
248
|
+
});
|
|
249
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2018",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"checkJs": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"sourceMap": true,
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"skipLibCheck": true,
|
|
17
|
+
"baseUrl": ".",
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*"],
|
|
20
|
+
"exclude": ["node_modules", "dist"]
|
|
21
|
+
}
|